core: Add float-semantics to support low-precision - #6254
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #6254 +/- ##
==========================================
+ Coverage 86.94% 86.97% +0.02%
==========================================
Files 431 431
Lines 64924 65064 +140
Branches 7419 7451 +32
==========================================
+ Hits 56450 56587 +137
- Misses 6903 6904 +1
- Partials 1571 1573 +2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
superlopuh
left a comment
There was a problem hiding this comment.
This is quite a lot of code to review at a time, let's split this up? I feel like the first thing to add is the overall structure, and none of the methods on _ReducedPrecisionFloatType (which I would make public). Then the next PRs can one-by-one replace the existing methods on the subclasses, like bitwidth could be one PR, and then format could be another PR. Does this sound like a reasonable plan?
|
Definitely happy to split off the |
This PR introduces the FloatSemantics class based on MLIR's `fltSemantics` concept (alongside `fltNonfiniteBehavior` and `fltNanEncoding`). This is used to calculate the `bitwidth` of reduced-precision floats. WIP: * #6254
Adding helpers to `FloatSemantics`. WIP: * #6254
| float_type.format | ||
| # Reduced-precision floats pack via the reduced-float codec, not a struct | ||
| # format string, so (like bf16) they have no `format` attribute. | ||
| assert not hasattr(tf32, "format") |
There was a problem hiding this comment.
This feels off to me, what are we really testing here?
There was a problem hiding this comment.
My gut tells me to just delete this line, I'm not sure what benefit there is to it.
There was a problem hiding this comment.
It's mirroring the same (pre-existing) check for bf16 just above. Either way is fine.
There was a problem hiding this comment.
Moved these next to each other, removed comment. Share the general gut feeling though.
|
If you have the motivation it feels to me like it would be great to support the larger floats also |
Samielakkad
left a comment
There was a problem hiding this comment.
Nice addition overall. One edge case I’d want covered before this lands: iter_unpack currently walks the buffer in size chunks and decodes the last slice even if it is shorter than size.
For the reduced-float types with odd byte widths, that means a truncated buffer can silently decode as if the missing high bytes were zero. That is a little different from struct.iter_unpack, which requires an exact multiple of the element size.
It may be worth either checking len(buffer) % self.size == 0 in iter_unpack / unpack, or adding an explicit test that this silent partial decode is the intended behavior. I would lean toward rejecting the partial chunk so malformed dense data does not quietly round-trip to a different value.
|
Thanks @Samielakkad, I think |
| raise ValueError( | ||
| f"buffer of {len(mv)} bytes is not a multiple of the {size}-byte " | ||
| f"element size of {self.name}" | ||
| ) |
There was a problem hiding this comment.
No, this is also not the right check, as it should be OK to unpack lazily just the prefix. The error should be raised only if the last element cannot be read in full
There was a problem hiding this comment.
I'm also not sure about the comment, the old code was reading potentially past the end of the buffer, not silently truncating? I would have expected an error to be raised there from an out of bounds access for the last element.
There was a problem hiding this comment.
Reading the struct documentation, it says that the bytes buffer should be a multiple of the struct size but not what error is raised if it isn't...
There was a problem hiding this comment.
I think the OOB error didn't happen because of the slicing.
| f"buffer ends with a partial {self.name} value: " | ||
| f"{len(chunk)} of {size} bytes" | ||
| ) | ||
| yield self.decode_bits(int.from_bytes(chunk, "little")) |
There was a problem hiding this comment.
Thanks for updating this, but I still think it would be good to be consistent across implementations. Could you please add f64 to your test for this, and check that the error type is the same for both the struct and this manual implementation, and that the errors are consistent on whether the whole buffer needs to have size multiple of this element's size or just the last element?
There was a problem hiding this comment.
If we're going for consistency, we should throw a struct.error. However, to do so I'd like to inherit from StructPackableType. Although we're not using any of its code, we do provide the same methods, so it's somewhat cleaner. I've also parameterized
@pytest.mark.parametrize("type_", [f64, bf16, tf32])
def test_float_rejects_truncated_buffer(type_: AnyFloat):to demonstrate uniform rejection errors across these types.
There was a problem hiding this comment.
Let's iterate on the PR I just opened (#6273) to decide what errors we want, feels like a good order of solving these issues to me.
| assert type_.unpack(packed, 2) == (1.5, 2.0) | ||
| truncated = packed[:-1] # ends part-way through the final element | ||
| with pytest.raises(struct.error): | ||
| list(type_.iter_unpack(truncated)) |
There was a problem hiding this comment.
This seems important to me, does it raise when you fetch the first element or all the elements
| list(type_.iter_unpack(truncated)) | |
| next(type_.iter_unpack(truncated)) |
There was a problem hiding this comment.
Yes, with a small fix for bf16 to match others.
There was a problem hiding this comment.
In the PR I just opened it actually raises before you fetch the first element
There was a problem hiding this comment.
Found one edge case in f8E8M0FNU: negative inputs lose their sign before rounding, so -4.0 is encoded as +4.0 (0x81). LLVM rejects signed values for this format, while ml_dtypes maps them to 0xff/NaN.
We should pick one of those behaviors rather than silently taking the absolute value. Tests for -0.0 and one negative finite value would catch it.
|
Thanks @superlopuh, and good spot @Samielakkad! It's what one gets for sweeping the hexadecimal spectrum (: |
Extends support for reduced-precision floating point values by adding support for reduced-precision literals, and a central mechanism to implement a variety of different flavours. The types itself were added in #6250. This PR adds support for literals, by introducing the
FloatSemanticsclass based on MLIR'sfltSemanticsconcept (alongsidefltNonfiniteBehaviorandfltNanEncoding).Stack: