Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Add an RFC for fixed point types.#41

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Draft
zyp wants to merge13 commits intoamaranth-lang:main
base:main
Choose a base branch
Loading
fromzyp:fixed-point-types

Conversation

@zyp
Copy link
Contributor

@zypzyp commentedDec 22, 2023
edited
Loading

vk2seb reacted with heart emoji
@whitequarkwhitequark added meta:nominatedNominated for discussion on the next relevant meeting area:coreRFC affecting APIs in amaranth-lang/amaranth labelsDec 22, 2023
Copy link
Member

@whitequarkwhitequark left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

General comments:

  • We designedlib.data andlib.wiring to be imported as entire modules:from amaranth.lib import data, wiring. This library should be designed (naming-wise) to work that way too.fixed.Shape andfixed.Value are one option, though I can see why others may object to it.
  • I feel like implicit conversions from floats are potentially a source of bugs severe enough that maybe we shouldn't have them at all.
  • How does one perform computation on fixed point values during compilation? I think "you just use floats" is an unsatisfying answer.

The following operations are defined on it:

-`FixedPoint(f_width, /, *, signed)`: Create a`FixedPoint` with`f_width` fractional bits.
-`FixedPoint(i_width, f_width, /, *, signed)`: Create a`FixedPoint` with`i_width` integer bits and`f_width` fractional bits.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

These two lines seem mutually incompatible.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

The idea is that the underlying implementation would beFixedPoint(i_or_f_width, f_width = None, /, *, signed), but I found it clearer to express it like that. Consider how you can dorange(stop) andrange(start, stop).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Makes sense. I wasn't sure if it was that or a typo. Does the first constructor add a sign bit whensigned is true, and otherwise create a number whose range spans from 0 to some value below 1?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Yes, although the exact semantics wrt. the sign bit depends on which Q notation we settle on.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

The dominant notation in the industry, at least in the audio ASIC world, is to include the sign bit in the number of integer bits. For example, -1 to 1 is represented as a Q1.23. That would be my vote.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

@samimia-swks would you happen to have some references we can cite to show that this is the dominant Q notation in the audio ASIC world? It will be useful evidence that may be worth appending to the RFC document itself.

##Reference-level explanation
[reference-level-explanation]:#reference-level-explanation

This RFC proposes a library addition`amaranth.lib.fixedpoint` with the following contents:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Bikeshed:lib.fixed,lib.fixnum.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I would lean towardlib.fixed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

lib.fixed seems good, looking through the usage and discussion;fixed.Shape,fixed.Value are pretty natural to write.


-`FixedPoint(f_width, /, *, signed)`: Create a`FixedPoint` with`f_width` fractional bits.
-`FixedPoint(i_width, f_width, /, *, signed)`: Create a`FixedPoint` with`i_width` integer bits and`f_width` fractional bits.
-`FixedPoint.cast(shape)`: Cast`shape` to a`FixedPoint` instance.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

What is the semantics of this operation?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I just updated it to.cast(shape, f_width=0) locally. The idea is to determinei_width automatically, soFixedPoint.cast(unsigned(8)) would result inUQ(8) andFixedPoint.cast(unsigned(8), f_width = 4) would result inUQ(4, 4).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

WhyUQ(4,4)?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

FixedPoint.cast() is a building block forFixedPointValue.cast(). The idea is to be able to say «here's a value with n fractional bits, please turn it into an appropriately sizedFixedPointValue». Anunsigned(8) with four fractional bits would have four integer bits left and thus cast toUQ(4, 4).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

This API seems confusing and difficult to use. Should we expose it at all?

-`FixedPoint(i_width, f_width, /, *, signed)`: Create a`FixedPoint` with`i_width` integer bits and`f_width` fractional bits.
-`FixedPoint.cast(shape)`: Cast`shape` to a`FixedPoint` instance.
-`.i_width`,`.f_width`,`.signed`: Width and signedness properties.
-`.const(value)`: Create a fixed point constant from an`int` or`float`, rounded to the closest representable value.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Decimal support as well?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

And probablyFraction as well.

-`FixedPoint(f_width, /, *, signed)`: Create a`FixedPoint` with`f_width` fractional bits.
-`FixedPoint(i_width, f_width, /, *, signed)`: Create a`FixedPoint` with`i_width` integer bits and`f_width` fractional bits.
-`FixedPoint.cast(shape)`: Cast`shape` to a`FixedPoint` instance.
-`.i_width`,`.f_width`,`.signed`: Width and signedness properties.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I feel like thei_width andf_width names are difficult enough to read that it's of more importance than bikeshedding to come up with something more readable.

.int_bits,.frac_bits?

cursed option:int, frac = x.width?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

+1 onint_bits,frac_bits...

ld-cd reacted with thumbs up emoji
-`.as_shape()`: Return the underlying`Shape`.
-`.__call__(target)`: Create a`FixedPointValue` over`target`.

`Q(*args)` is an alias for`FixedPoint(*args, signed=True)`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I understand Q-notation is an established one, however since Amaranth defaults to unsigned, I feel that havingSQ andUQ (without having justQ) would serve Amaranth better.

-`.as_value()`: Return the underlying value.
-`.eq(value)`: Assign`value`.
- If`value` is a`FixedPointValue`, the precision will be extended or rounded as required.
- If`value` is an`int` or`float`, the value will be rounded to the closest representable value.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

What happens if you assign 1024 to a Q8.8 value?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Signal(Q(8, 8)).eq(1024) would effectively beSignal(signed(17)).eq(1024 << 8). (Orsigned(16) depending on Q notation.)

Speaking of overflow, I figure fixed point overflow should behave like regular integer overflow. Saturating math feels orthogonal to this RFC and could be added through a later RFC that handles both integer and fixed point values.

@zyp
Copy link
ContributorAuthor

zyp commentedDec 23, 2023

I feel like implicit conversions from floats are potentially a source of bugs severe enough that maybe we shouldn't have them at all.

I think for making constants of a specific shape it's reasonable to have implicit float conversion. As theother argument to binary operators it's probably reasonable to require an explicit conversion to a constant first.

  • How does one perform computation on fixed point values during compilation? I think "you just use floats" is an unsatisfying answer.

An adjacent question is «what willFixedPointValue.get() in the simulator return?» We should probably add a separateFixedPointConstant type.

@whitequark
Copy link
Member

We should probably add a separateFixedPointConstant type.

Are there good fixed point libraries for Python we could use here?


-`Decimal` and/or`Fraction` support?
- This could make sense to have, but both can represent values that's not representable as binary fixed point.
On the other hand, a Python`float` can perfectly represent any fixed point value up to a total width of 53 bits and any`float` value is perfectly representable as fixed point.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

OK, this is a compelling argument for support of floats. I am convinced that it is a good idea to have floats supported in at least some way. But we need to be very careful around overflows.

- Simply truncating is free, rounds towards negative infinity.
- IEEE 754 defaults to round to nearest, ties to even, which is more expensive to implement.
- Should we make it user selectable?
- We still need a default mode used when a higher precision number is passed to`.eq()`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I wonder if we could leave it unspecified and implementation-defined until we can get a numerics expert to chime in.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

In most DSP applications, simple truncating is done (bit picking, which is equivalent to a floor()) because it's free. I would vote for that being the default behavior at least.

Copy link

@ld-cdld-cdApr 2, 2024
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Truncating can also be a big foot gun in some DSP applications that is hard to track down, you can end up with a DC spur that grows over time or over your pipeline with no obvious explanation. A middle ground is round towards zero or symmetrically towards positive/negative infinity (this is nearly free on the output of Xilinx DSP48E1/2 and I believe cheap/free on ECP5 and friends DSPs). The way I personally would handle it would be to make the rounding mode part offixed.Shape type making itfixed.Shape(i_width, f_width, /, *, signed, rounding_mode). Truncate is still a reasonable default for most applications though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Hm, makingrounding_mode part of the signature is a pretty big action, what happens if you add two numbers with differing rounding modes for example?

Copy link

@ld-cdld-cdApr 3, 2024
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

To be clear the semantics I'm imagining here are if you havea andb andb has more fractional bits thana then when you assigna.eq(b) the rounding mode that is used is that ofa so that downstream DSP modules can enforce a rounding mode on their inputs.

The scenarios where I can imagine what you propose causing issues are (I'm guessing there are more outside the bounds of my imagination)

  1. User addsa andb and then.rounds them
  2. A user addsa andb and utilizes the result as a port on their module (I believe this is allowed but still learning the language)

Solutions that solve 1. But not two in no particular order:

  1. Don't actually resolve the round until a signal is assigned to something with a concrete value (this seems like a bad idea and probably doesn't solve the problem)
  2. Make the rounding mode undefined and require an explicit rounding mode to.round, but this doesn't solve the problem of getting a top level port with an undefined rounding mode

Solutions that I believe would solve both:

  1. Choose the rounding mode of the left term
  2. Choose the less footgunny rounding mode (TOEVEN,TOODD>SYMZERO,SYMINF>TRUNC) with ties going to either a defined order or the left term
  3. Ban arithmetic operations between numbers with different rounding modes and require an explicit cast of one of the arguments with something like.with_mode(other.rounding_mode)
  4. Make this undefined behavior and pick one of the above for now

My naive preference inclination would be 3, it seems like a fairly rare scenario and if you end up in it it's probably worth making the user think about what they want the result to be, but I'm not a language designer so take that with a grain of salt

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Further thoughts on 3 as an option:

Operations with a constant fixed point value should probably inherit the rounding mode of the non-constant value without requiring an explicit cast because I think the alternative is annoying to deal with (requiring a shape to be passed any time a constant is declared).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I was playing around with this a while back and I no longer think making it a part of the signature is a good idea as there is no platform independent way of providing rounding that infers well in the context of common DSP use cases. My understanding is that that would mean lowering would require adding a new primitive to the AST which doesn't feel like a good strategy. I think a better approach would be to leave rounding and several other common operations that require platform dependent lowering to a subsequent RFC. Currently my biggest stumbling block was that getting reasonable performance required manually instantiating DSPs which meant the design wasn't simulatible in pysim.


-`fixed.Shape(f_width, /, *, signed)`: Create a`fixed.Shape` with zero integer bits and`f_width` fractional bits.
-`fixed.Shape(i_width, f_width, /, *, signed)`: Create a`fixed.Shape` with`i_width` integer bits and`f_width` fractional bits.
- The sign bit is not included in`i_width` or`f_width`, so a`fixed.Shape(7, 8, signed=True)` will be 16 bits wide.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

This means thatfixed.Shape(7, 0, signed=True) has the same width assigned(8), which seems counterintuitive.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I just managed to hit this myself even though I've read through this a few times

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I've thought about this since I wrote the last draft and concluded that this is probably the wrong decision, so I'm intending to change it when I find time to revise the RFC again.

ld-cd reacted with thumbs up emoji
- If`value` is a`float` and`shape` is not specified, the smallest shape that gives a perfect representation will be selected.
If`shape` is specified,`value` will be rounded to the closest representable value first.
-`.as_integer_ratio()`: Return the value represented as an integer ratio`tuple`.
-`.as_float()`: Return the value represented as a`float`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

What happens if it's not representable? At least we need to havec.as_float(exact=True) which gives an error in that case.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Agree with this. I would even consider havingexact=True being the default.

If`shape` is specified,`value` will be rounded to the closest representable value first.
-`.as_integer_ratio()`: Return the value represented as an integer ratio`tuple`.
-`.as_float()`: Return the value represented as a`float`.
- Operators are extended to return a`fixed.Const` if all operands are constant.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

This is inconsistent with howConst() works.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

This is not addressed yet but will need to be before the next revision.

@whitequarkwhitequark removed the meta:nominatedNominated for discussion on the next relevant meeting labelFeb 19, 2024
@cr1901
Copy link
Contributor

cr1901 commentedFeb 27, 2024
edited
Loading

I have nothing concrete here, just minddumping stuff I wrote down in text files locally.

I assume that__div__ is deliberately not implemented. Thanks to the curse of rational numbers you'd need infinite bits to represent many divisions without loss, such as 1/5. Amaranth extends width so values can be completely represented, but this is impossible for fixed point division.

You can get as much precision as you'd like from dividing fixed point numbers by left-shifting a numerator (increasing itsf_width) before dividing. Consider dividing e.g.1*10^-6/999999*10^-6:

  • 1*10^-6/999999*10^-6 = int(1/999999) * (10^-6/10^-6) = 0*10^0.
    • Q(0,6)/Q(0,6) yields Q(6,0), yields 0 due to not enough precision.

On the other hand, we can left-shift the integer part to get a non-zero result from this divide. :

  • 1*10^-6/999999*10^-6 = 1*10^-6*(10^7*10^-7)/999999*10^-6 = int(10000000/999999)*(10^-13/10^-6) = 10*10^-7
    • Q(0,13)/Q(0,6) yields Q(6,7), yields a non-zero result (close enough).

I arbitrarily chose to shift by 7 decimal places (replace with binary for this RFC), but it's not obvious to me that any given left shift will be universally good for all use cases to prevent divides from returning 0 for non-zero numerators.

Additionally, you may only want the extra precision during the divide; e.g. I could truncate the Q(6,7) in the second example back down to Q(0,7), saving 6 bits, and still have a perfectly valid result to pipe to the rest of a design. Whether/how much to truncate depends on the dynamic range of fixed point values you expect your design to see.

Will fixed point divide be rare enough that divide should be completely ignored? Maybe there should be a divide function that allows the user to tweak:

  • Extra precision used during the integer divide portion.
  • TheQ of the output.

I do not have a concrete use case in mind right now (I could probably shoehorn division into my Celsius to Fahrenheit conversion experiments). Just something I mulled over today.

@whitequark
Copy link
Member

You could always multiply by reciprocal for an application like unit conversion.

@cr1901
Copy link
Contributor

Multiply-by-reciprocal only works if you can calculate it ahead of time- i.e. one of the multiply inputs is constant. Otherwise you'd have to to find the reciprocal while your design runs before doing the multiply, which... requires a divide. The thing you're trying to avoid.

@whitequark
Copy link
Member

This is exactly the case for Celsius to Fahrenheit conversion, no?

@cr1901
Copy link
Contributor

Indeed, I will need to find a different way to shoehorn a division into my Celsius to Fahrenheit experiments :).

Divide by non-constant factor for fixed point is probably uncommon. But I hesitate to say it's "so uncommon it's not worth supporting in some way at all, even if a__div__ impl is impossible".

@zyp
Copy link
ContributorAuthor

zyp commentedFeb 27, 2024

The question of whether__div__ makes sense to have is whether it's feasible for synthesis to infer a divider.

Independent of that, there's already nothing that prevents you from making a divider component with two fixedpoint inputs and a fixedpoint output.

@cr1901
Copy link
Contributor

The question of whetherdiv makes sense to have is whether it's feasible for synthesis to infer a divider.

Do FPGAs have divider IPs? I thought they only had multiplier IP. I assumed amaranthprovides floor division/mod for simulation purposes, and not something you'd want to synthesize.

@whitequark
Copy link
Member

is whether it's feasible for synthesis to infer a divider.

Yosys will actually synthesize a divider for you if you doa // b in Amaranth. Here's the size for a 8/8=8 combinational divider for iCE40:

   Number of cells:                114     SB_CARRY                       64     SB_LUT4                        50

@cr1901
Copy link
Contributor

The question of whetherdiv makes sense to have

Even if dividers synthesize, I don't think__div__ makes sense to have for fixed point; in integer division, "d doesn't dividen evenly" is handled by remainder. Therefore all values can be represented in the output without loss, which is great since Amaranthnever overflows.

In fixed point, there's no remainder, so if "d doesn't evenly dividen" we keep adding bits ton untild evenly divides our modifiedn. This can require infinite number of extra added bits, and if we keep with "Amaranth arithmetic never overflows" semantics, this is impossible to satisfy.

Even if__div__ doesn't make sense, I think there should besomething for divides (a function instead of an overloaded operator?)Last night was me minddumping my thoughts on what fixed point divide should handle.

@zyp
Copy link
ContributorAuthor

zyp commentedFeb 27, 2024

If a perfect__div__ is desirable to have, instead of afixed.Value, we could simply have it return a ratio object containing the two operands. Once such a ratio is passed to e.g.fixed.Value.eq(), the actual division could be performed with the precision of the assignment target.

To avoid scope creep, I'm inclined to leave inferred division out of this RFC. We could instead do a separate RFC later for that.

@cr1901
Copy link
Contributor

To avoid scope creep, I'm inclined to leave inferred division out of this RFC. We could instead do a separate RFC later for that.

That's fine, mind putting your comment re: ratio objects under Future Possibilities/making a mention that division is out of scope?

zyp reacted with thumbs up emoji


-`fixed.Shape(f_width, /, *, signed)`: Create a`fixed.Shape` with zero integer bits and`f_width` fractional bits.
-`fixed.Shape(i_width, f_width, /, *, signed)`: Create a`fixed.Shape` with`i_width` integer bits and`f_width` fractional bits.
- The sign bit is not included in`i_width` or`f_width`, so a`fixed.Shape(7, 8, signed=True)` will be 16 bits wide.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Is havingi_width positive andf_width negative or the converse in scope for this RFC? If it is then I think.round should takei_width as an optional argument

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

What would that mean?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Two scenarios where I think you could reasonably end up in this position with the current RFC

  1. You start off with say UQ0.16 [0,1) and right shift by 2 the natural representation is still 16 bits but with the range [0, 0.25) and I believe the representation is going to end up being UQ-2.18 in this notation?
  2. This is less reasonable but say you throw a 4096 (1<<12) point FFT at values with the format SQ7.10, if you are naive about the scaling and keep the bit width to 18 bits, you end up with SQ19.-2 (ie a step of 4 between every point). This is equivalent in terms of the bits on the wire to starting with SQ0.17 and ending with SQ12.5 but it feels more silly and you can end up there naturally so it should probably be supported

I'm currently working on tracking down some DSP bugs in a project at work that nobody every simulated, in my fixed point emulation code the number format I chose was basically n_bits, exponent which handles these cases more naturally but is probably overall less intelligible. I'd imagine this looking likefixed.Shape(shape, exponent) in amaranth (IEfixed.Shape(signed(18), 2) for SQ19.-2) but that would be a pretty large change I'm not sure it would be a net positive anyways (also don't want to bikeshed here).

Copy link

@ld-cdld-cdApr 4, 2024
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Ok I've played with what the implementation does here, this might be a bit long; The implementation does allow the creation of the above types of representations (IE Q-4.20, Q20.-4) and appears to operate on them correctly, however it for the most part won't generate them on its own. Left shift and right shift preserve the number of bits until eitheri_width orf_width would go negative at which point it pads things out:

Right shift works as expected:

In [2]:fixed.SQ(8,8),fixed.SQ(-4,20),Signal(fixed.SQ(8,8))>>8,Signal(fixed.SQ(8,8))>>12Out[2]:(fixed.Shape(8,8,signed=True),fixed.Shape(-4,20,signed=True), (fixedpointSQ0.16 (sig $signal)), (fixedpointSQ0.20 (sig $signal)))

Left shift seems to get converted to unsigned which I think is not the correct behavior:

In [4]:Signal(fixed.SQ(8,8))<<8,Signal(fixed.SQ(8,8))<<9,Signal(fixed.SQ(8,8))<<10,Signal(fixed.SQ(8,8))<<12Out[4]:((fixedpointSQ16.0 (sig $signal)), (fixedpointUQ18.0 (cat (const1'd0) (sig $signal))), (fixedpointUQ19.0 (cat (const2'd0) (sig $signal))), (fixedpointUQ21.0 (cat (const4'd0) (sig $signal))))

I personally think the rounding semantics are a bit difficult to reason about as they stand:

In [5]: (Signal(fixed.SQ(8,8))<<12).round(-4), (Signal(fixed.SQ(8,8))>>12).round(16)Out[5]:((fixedpointUQ21.-4 (+ (slice (cat (const4'd0) (sig $signal)) 4:21) (slice (cat (const 4'd0) (sig $signal))3:4))), (fixedpointSQ0.16 (+ (slice (sig $signal)4:17) (slice (sig $signal)3:4))))

Rounding with negative fractional bits produces the sensible result (wrt signed to unsigned bug), but rounding off the fractional bits in a right shift looses precision when it does not necessarily need too. I think this is a decent argument for either always preserving the number of bits in a (constant) shift, or including ani_width argument to round. Personally I would advocate for always preserving the number of bits because I think that that is easier to reason about and the argument to round can just bewidth noti_width orf_width.

Addition between a number with no fractional bits and one with no integer bits also produces a number 2 bits wider than I believe is necessary:

In [7]:Signal(fixed.SQ(-4,20))+Signal(fixed.SQ(20,-4))Out[7]: (fixedpointSQ22.20 (+ (sig $signal) (cat (const24'd0) (sig $signal))))

- The sign bit is not included in`i_width` or`f_width`, so a`fixed.Shape(7, 8, signed=True)` will be 16 bits wide.
-`fixed.Shape.cast(shape, f_width=0)`: Cast`shape` to a`fixed.Shape` instance.
-`.i_width`,`.f_width`,`.signed`: Width and signedness properties.
-`.const(value)`: Create a`fixed.Const` from`value`.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

would a.max() and.min() method or property make sense here to get a const with the max/min representable value make sense here or is that outside the scope of theShape API given that the base signed and unsigned types don't have that?

vk2seb and mndza reacted with thumbs up emoji
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

We could definitely have additional functionality on specific shapes, e.g. enum shapes are pretty magical, so are layouts..min()/.max() seem completely reasonable to me.

- If`other` is an`int`, it'll be cast to a`fixed.Const` first.
- If`other` is a`float`: TBD
- The result will be a new`fixed.Value` with enough precision to hold any resulting value without rounding or overflowing.
-`.__lshift__(other)`,`.__rshift__(other)`: Bit shift operators.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

In the implementation at least the semantics of these seem to be different from underlying amaranth values. If the shift amount is an integer these act like.shift_left() and.shift_right(). Those operators should probably be added here and the semantics of__lshift__ and__rshift__ adjusted to match those onunsigned() andsigned().

Note: I may be wrong here, still learning the language

- The result will be a new`fixed.Value` with enough precision to hold any resulting value without rounding or overflowing.
-`.__lshift__(other)`,`.__rshift__(other)`: Bit shift operators.
-`.__neg__()`,`.__pos__()`,`.__abs__()`: Unary arithmetic operators.
-`.__lt__(other)`,`.__le__(other)`,`.__eq__(other)`,`.__ne__(other)`,`.__gt__(other)`,`.__ge__(other)`: Comparison operators.
Copy link

@ld-cdld-cdApr 5, 2024
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

For the comparison operators the rounding behavior seems under defined when the types don't have the same precision, the options seem to be:

  1. Round to the precision of the left argument(what the draft implementation does) EDIT: Draft implementation does not have an opinion on this
  2. Round to the precision of the least precise argument (easiest for the hardware)
  3. Round to the most precise argument (excluding constants maybe?)
  4. Ban comparison between values (but maybe not const and value?) with different precision

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

  1. seems bad, I'm going to implement 3 for my FFT testing and see if it ends up being a problem

- Should we make it user selectable?
- We still need a default mode used when a higher precision number is passed to`.eq()`.

- Are there any other operations that would be good to have?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

.as_value() returns a value with a signdedness that doesn't depend on the signdedness of the fixedpoint shape, should there be a.lower() or.numerator() method that returns.as_value() cast to the appropriate signdedness?

@jrmoserbaltimore
Copy link

Is this still being worked on?

I'm familiar with the Python FxpMath module and so like the idea of denoting Qs.m or SQs.m for significand and mantissa signed, UQs.m for unsigned. The syntax@ld-cd proposed looks good to me. I'm not so into thefixed.Shape(f_width, ...) andfixed.Shape(i_width, f_width, ...) notably because the first parameter means something different depending on if the first two are integers (wat?); but I think it's more clear to have something like SQ() and UQ() requiring boths andm. I also like the syntaxSQ(5, -6) for shifting left (xxxxx000000) andSQ(-4,5) for shifting right (+.+++xxxxx), note thatSQ(-1,m) puts the MSB at the 1/2 (0.1) position.

It looks like there's still question about what to do if the other value is afloat. I assume this means if a Pythonfloat ornumpy floating-point type is passed. I like the explicit conversion approach. I'm not keen on spending a whole bunch of time trying to debug something that isn't behaving the way I expected, or wondering if it's going to behave the way I expect instead of blindly hoping.

The same can be said for passing any constant as a fixed-point value: I'd rather require the constant to be presented asSQ() orUQ() rather than "here's a Python type, have a guess at what's the right thing to do."

In the worst case, you can always add implicit conversion later without it being a breaking change.

Use case: I frequently use lookup tables generated bynumpy. These tables may be floats. If I have a table ofnp.float64 or such I want to be able to somehow indicate that the table is a certain fixed-point type. To my mind, this is pretty simple: I tell it I want aSQ() orUQ() somehow (e.g.SQ(-4, 16, log2_table)) and Amaranth somehow does the right thing (which ultimately means converting the entire addressable portion of the table and embedding it in the design). That's a relatively simple problem.

I'm using an Abed-Siferd predictor to produce an accuratelog2() andexp2() down to more than 4 bits with simple circuitry; I need 19 bits of mantissa. My solution is a 512-entrySQ(-4,16) look-up table added to theSQ(4.5) output of the predictor. Linear interpolation between the resulting values supplies further granularity.

To do this, I need to add dissimilar fixed-point values with proper sign extension: adding a negativeSQ(-4,16) to aQ(12,4) needs to extend all the sign bits to cover theQ(12,4) (whether SQ or UQ). The same goes for aligning outputs.

I'm looking forward to built-in fixed-point math, the lack of which is a deficiency in VHDL and Verilog that I would very much like to escape.

@whitequark
Copy link
Member

whitequark commentedNov 11, 2024
edited
Loading

Is this still being worked on?

We're definitely still very interested in it!

The RFC will need several people driving it forward; usually two is enough (I am usually one of them and the author is another) but this one will likely need three. At the moment I'm seriously ill and will probably continue to be mostly bedridden until January (chronic pain that became worse recently).

I think it's definitely worth it to work out a bunch of kinks before then, and I'll try to participate however I can.

@samimia-swks
Copy link

samimia-swks commentedNov 23, 2024 via email
edited
Loading

This is looking good!One note on the rounding topic:Simple rounding is also known as a mid-tread quantizer. If the desired output format is I.Q (total length: I+Q) we add 0.5 / 2^(Q) and truncate. The cost is a real adder and a clipper if you wanna be safe. Not trivial if you didn’t care about the DC offset error that simple truncation introduces.But you could also do a mid rise quantizer. You truncate to I.Q, but to get rid of the negative DC offset you add that 0.5 / 2^(Q) after truncation. This is technically an adder too but in HW you can just append an LSB of 1 which is free!But now you end up with I+Q+1 bits. If these bits were going across a bus you would transport just the I.Q but the receiver has to remember to pad the LSB.(Note that in integer land, mid-tread is like rounding to nearest integer and mid-rise is like rounding to the nearest midpoint between integers, so you cannot represent a hard zero)With all that said, if you want to resize a number to I+Q bits without adding a DC offset AND you don’t want the cost of an adder, you can simply truncate to I.(Q-1) bits and then pad an LSB of 1!So I would advocate for 3 options with truncate being default:1. truncate (floors to -inf, free, adds dc offset)2. mid-tread (rounds, costs an adder, can represent zero)3. mid-rise (rounds, free, cannot represent zero)

- If`value` is an`int` or`float`, it'll be cast to a`fixed.Const` first.
- If`value` is a`fixed.Value`, the precision will be extended or truncated as required.
-`.reshape(f_bits)`: Return a new`fixed.Value` with`f_bits` fractional bits, truncating or extending precision as required.
-`.reshape(shape)`: Return a new`fixed.Value` with shape`shape`, truncating or extending precision as required.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

The 2 signatures of.reshape() here are my error. They should be collapsed into one.reshape(), although I am also considering dropping the second form if I can't find a killer use-case for it.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

In any case I think this method could use further bikeshedding. I would be just as open to calling this methodtruncate(f_bits=0) ortrunc(f_bits=0), although that loses the notion that it can both increaseand decrease precision, which was part of the reason of not calling thingsround() in the first place.

@vk2seb
Copy link

vk2seb commentedNov 23, 2024
edited
Loading

Thank you@samimia-swks for your input on the rounding details. This is all useful context, although at this point I am inclined to postpone rounding strategies for a future RFC.

In the latest version of the RFC, I have de-scoped rounding completely.
Excerpt from the RFC text touching on this:

 - samimia-swks@: In most DSP applications, simple truncating is done (bit picking, which is equivalent to a floor()) because it's free. I would vote for that being the default behavior at least.  - ld-cd@: (...) Truncate is still a reasonable default for most applications.  - ld-cd@: (...) I think a better approach would be to leave rounding and several other common operations that require platform dependent lowering to a subsequent RFC (...).  - vk2seb@: Both truncation and simple rounding (round to nearest) are commonly used in DSP algorithms. For now, we provide only `reshape()` (truncation, now reflected above). Additional rounding strategies may be added in a future RFC, however we will always need a default rounding strategy, and truncation seems like a sane default.

Even without an opinionatedround() operation, I expect this RFC to be useful. Based on the above comments and experience from my own projects, truncation seems the most common 'form' of rounding (small sample size, so input here is appreciated!). Additionally, there is nothing preventing use of custom rounding strategies on top of the existing RFC, before they potentially become part oflib.fixed in a future RFC.

@whitequark
Copy link
Member

In the latest version of the RFC, I have de-scoped rounding completely.

(I haven't been able to track the changes in depth but the overall approach sounds good. We have previously de-scoped controversial or poorly understood aspects of RFC and it worked quite fine.)

@mndza
Copy link

Really happy to see progress on this RFC. I appreciate all the work that has gone into it.

I'm particularly interested in supporting user-selectable rounding modes beyond simple truncation. As noted by@vk2seb, truncation can introduce DC biases in the signal that are problematic for some DSP applications. However, I'm okay with that being left out of this particular RFC.

Besides that, I like the current proposal as it stands.

@vk2seb
Copy link

Just a small update, I'm still committed to getting this RFC finished, just pushed up a functional implementation of the latest version of this RFC (with some test cases) here:amaranth-lang/amaranth#1578

I'm currently experimenting with this in my own projects to make sure it interacts with the rest of Amaranth without any unexpected surprises. I aim to integrate any findings from that effort into this RFC and move it from draft to review state after fleshing out the missing sections. I can't commit to a hard deadline but would like to have this formally 'ready for review' within a month or so.

@goekce
Copy link

goekce commentedMay 2, 2025
edited
Loading

I tried@vk2seb's implementation and noticed thatlen(Value) andShape.width() are not part of the API. These are described as methods in the guide for standard shapeshttps://amaranth-lang.org/docs/amaranth/latest/guide.html#shapes . Don't you think it would be beneficial to include them in all shapes as a general API to get the number of bits in general, includingfixed?

fromamaranthimport*len(Const(2,unsigned(2)))# 2signed(4).width# 4

@vk2seb
Copy link

vk2seb commentedMay 2, 2025
edited
Loading

I tried@vk2seb's implementation and noticed thatlen(Value) andShape.width() are not part of the API. These are described as methods in the guide for standard shapeshttps://amaranth-lang.org/docs/amaranth/latest/guide.html#shapes . Don't you think it would be beneficial to include them in all shapes as a general API to get the number of bits in general, includingfixed?

fromamaranthimport*len(Const(2,unsigned(2)))# 2signed(4).width# 4

@goekce thanks, I agree thatfixed.Shape should remain similar toShape in this regard and that these methods should also exist onfixed.Shape. will add this.

@goekce
Copy link

Another observation:

If signal values are displayed, then they don't directly show a human-friendly representation like asigned signal:

In [486]: Const(-1)Out[486]: (const 1'sd-1)In [485]: fixed.Const(1.2, fixed.SQ(2,3))Out[485]: fixed.SQ(2, 3) (const 5'sd10)

As I understand,as_float() must be used for this purpose:

In [487]: fixed.Const(1.2, fixed.SQ(2,3)).as_float()Out[487]: 1.25

If it were to include 1.25 directly, then the representation would be more readable. However, all values build on either signed or unsigned values andfixed is implemented by applying afixed shape lens to signed and unsigned values, as seen in the representationfixed.SQ(2, 3) (const 5'sd10). Do I understand correctly?

@vk2seb
Copy link

@goekce the precise implementation of__repr__ is not described in the RFC text, I guess your comment is referring to the WIP implementation fromamaranth-lang/amaranth#1578

We can discuss what the output of__repr__ should be for afixed.Value, and add it to the RFC text. I don't necessarily thinkfixed.SQ(2, 3) (const 5'sd10) is perfect, and am open to other ideas. I do think it should be obvious at a glance thatfixed acts as a lens on an underlyingValue.

@goekce
Copy link

@goekce the precise implementation of__repr__ is not described in the RFC text, I guess your comment is referring to the WIP implementation fromamaranth-lang/amaranth#1578

You are right, thanks!

We can discuss what the output of__repr__ should be for afixed.Value, and add it to the RFC text. I don't necessarily thinkfixed.SQ(2, 3) (const 5'sd10) is perfect, and am open to other ideas. I do think it should be obvious at a glance thatfixed acts as a lens on an underlyingValue.

So you mean thatfixed.SQ(2, 3) (const 5'sd10) could be the minimum and human-readable output should be integrated, right? Then an idea could be:

fixed.SQ(2, 3) (const 5'sd10) [1.25]

@goekce
Copy link

goekce commentedMay 3, 2025
edited
Loading

I also noticed__str__ should be included in the RFC. Using__str__ from the parent class could lead to confusion. In the followingfixed signal is printed as-4:

fromamaranthimport*fromamaranth.libimportfixedfromamaranth.lib.wiringimportComponent,In,OutclassC(Component):o:Out(1)defelaborate(self,platform):m=Module()self.s=Signal(signed(2),init=-1)self.f=Signal(fixed.SQ(2,2),init=-1.0)m.d.sync+=Print('Print signed',self.s)m.d.sync+=Print('Print fixed',self.f)returnmfromamaranth.simimportPeriod,Simulatordut=C()asyncdefbench(ctx):print('ctx.get signed',ctx.get(dut.s))print('ctx.get fixed',ctx.get(dut.f))awaitctx.tick()sim=Simulator(dut)sim.add_clock(Period())sim.add_testbench(bench)sim.run()

Output:

ctx.get signed -1ctx.get fixed fixed.SQ(2, 2) (const 4'sd-4)Print signed -1Print fixed -4

@whitequark
Copy link
Member

So you mean thatfixed.SQ(2, 3) (const 5'sd10) could be the minimum and human-readable output should be integrated, right? Then an idea could be:

The contract of__repr__ is that the value should be printed in a way that makes it possible to reconstruct it by inserting it into the Python REPL (with some unspecified set of imports already done). So I'm against adding[1.25] as a suffix.

If there is a syntax for writing a compile-time fixed point constant then that syntax should be used. (It's been a while since I read the RFC, and I don't quite recall the details...) Otherwise, something like<fixed.SQ(2, 3) ...> should be used.

goekce and vk2seb reacted with thumbs up emoji

@vk2seb
Copy link

vk2seb commentedMay 4, 2025
edited
Loading

If there is a syntax for writing a compile-time fixed point constant then that syntax should be used. (It's been a while since I read the RFC, and I don't quite recall the details...) Otherwise, something like <fixed.SQ(2, 3) ...> should be used.

Thanks, this is helpful. In that case we want different implementations of__repr__ forfixed.Value andfixed.Const. Then we can keep the logical (float) value in first position. Perhaps along the lines of:

>>> from amaranth import *>>> from amaranth.lib import fixed>>> fixed.Const(-0.75)fixed.Const(-0.75, SQ(1, 2))>>> fixed.Const(-0.75) * 3fixed.Value(SQ(3, 2), (* (const 3'sd-3) (const 2'd3)))# Check reconstruction property>>> from amaranth.lib.fixed import SQ, UQ>>> fixed.Const(-0.75, SQ(1, 2))fixed.Const(-0.75, SQ(1, 2))

@vk2seb
Copy link

vk2seb commentedMay 4, 2025
edited
Loading

I also noticed__str__ should be included in the RFC. Using__str__ from the parent class could lead to confusion. In the following fixed signal is printed as -4:

I don't think that's necessary, as__str__ will fall back to__repr__. However, we definitely wantfixed.Shape to implement.format() so that thePrint statement in your example works as expected. This is not implemented in the draft implementation yet.

goekce reacted with thumbs up emoji

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

@whitequarkwhitequarkwhitequark left review comments

+4 more reviewers

@adamgreigadamgreigadamgreig left review comments

@vk2sebvk2sebvk2seb left review comments

@ld-cdld-cdld-cd left review comments

@samimia-swkssamimia-swkssamimia-swks left review comments

Reviewers whose approvals may not affect merge requirements

Assignees

No one assigned

Labels

area:coreRFC affecting APIs in amaranth-lang/amaranth

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

10 participants

@zyp@whitequark@cr1901@jrmoserbaltimore@vk2seb@samimia-swks@mndza@goekce@adamgreig@ld-cd

[8]ページ先頭

©2009-2025 Movatter.jp