Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 16, 2026

Description

BFloat16.TryWriteSignificandBigEndian and TryWriteSignificandLittleEndian initially reported bytesWritten = 4 (sizeof(uint)) instead of the correct value. Upon further investigation, it was discovered that BFloat16's significand should be byte-sized (8 bits: 7 trailing bits + 1 implicit bit), not ushort-sized (16 bits).

Changes

  • BFloat16.cs:

    • Changed Significand property from ushort to byte to correctly represent the 8-bit significand
    • Updated GetSignificandByteCount() to return sizeof(byte) (1) instead of sizeof(ushort) (2)
    • Updated TryWriteSignificandBigEndian to write 1 byte directly (endianness is irrelevant for single byte)
    • Updated TryWriteSignificandLittleEndian to write 1 byte directly (endianness is irrelevant for single byte)
  • BFloat16Tests.cs: Added test coverage for TryWriteSignificandBigEndian and TryWriteSignificandLittleEndian using [Theory] with [MemberData] to provide BFloat16 test values directly. Tests verify correct byte count (1 byte) and output values for various BFloat16 values (NegativeInfinity, MinValue, NaN, Epsilon, Zero, MaxValue, PositiveInfinity). Empty destination edge cases are tested in separate [Fact] methods. Test data methods use modern C# collection literal syntax [ ... ] for cleaner, more maintainable code.

Technical Details: BFloat16 format has 1 sign bit + 8 exponent bits + 7 trailing significand bits. The Significand property includes the implicit leading 1 bit for normal numbers, resulting in 8 bits total (1 byte). For a single byte value, endianness doesn't apply, so both BigEndian and LittleEndian methods write the byte directly to destination[0].

Note: Tests use [MemberData] instead of [InlineData] to pass BFloat16 objects directly, avoiding float-to-BFloat16 conversion issues that could produce different values than expected.

Original prompt

This section details on the original issue you should resolve

<issue_title>BFloat16.TryWriteSignificand{Big,Little}Endian report 4 for bytesWritten</issue_title>
<issue_description>The BFloat16 implementation of TryWriteSignificandBigEndian and TryWriteSignificandLittleEndian write 16-bit integers, but report 4 as the number of bytes written (32-bit), even though only two bytes were written:

bool IFloatingPoint<BFloat16>.TryWriteSignificandBigEndian(Span<byte> destination, out int bytesWritten)
{
if (BinaryPrimitives.TryWriteUInt16BigEndian(destination, Significand))
{
bytesWritten = sizeof(uint);

I'm not exactly a numerics person but should this be a ushort? (That is what Half does).</issue_description>

<agent_instructions>Please fix bytesWritten for BFloat16, add tests</agent_instructions>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

- Changed bytesWritten from sizeof(uint) to sizeof(ushort) in TryWriteSignificandBigEndian
- Changed bytesWritten from sizeof(uint) to sizeof(ushort) in TryWriteSignificandLittleEndian
- Added comprehensive tests for both methods

Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix bytesWritten reporting in BFloat16 TryWriteSignificand methods Fix BFloat16.TryWriteSignificand bytesWritten reporting 4 instead of 2 Jan 16, 2026
Copilot AI requested a review from EgorBo January 16, 2026 21:29
@EgorBo EgorBo marked this pull request as ready for review January 16, 2026 21:59
Copilot AI review requested due to automatic review settings January 16, 2026 21:59
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a bug in BFloat16.TryWriteSignificandBigEndian and TryWriteSignificandLittleEndian where these methods incorrectly reported writing 4 bytes (sizeof(uint)) instead of 2 bytes (sizeof(ushort)) for the significand.

Changes:

  • Fixed bytesWritten to report sizeof(ushort) instead of sizeof(uint) in both methods
  • Added comprehensive test coverage for both methods to verify correct byte counts and output values

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
BFloat16.cs Corrected bytesWritten from sizeof(uint) to sizeof(ushort) in TryWriteSignificandBigEndian and TryWriteSignificandLittleEndian
BFloat16Tests.cs Added comprehensive test methods TryWriteSignificandBigEndianTest and TryWriteSignificandLittleEndianTest with 11 test cases each

- Converted TryWriteSignificandBigEndianTest to [Theory] with [InlineData] attributes
- Converted TryWriteSignificandLittleEndianTest to [Theory] with [InlineData] attributes
- Separated empty destination tests into dedicated [Fact] methods
- Removed duplicate test cases (same significand for ±0.0 and ±Epsilon)

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
@EgorBo
Copy link
Member

EgorBo commented Jan 17, 2026

@copilot the tests you added fail on CI. e.g.

Assert.Equal() Failure: Collections differ
↓ (pos 0)
Expected: [255, 0]
Actual:   [128, 0]
↑ (pos 0)

- Changed from [InlineData] with float values to [MemberData] with BFloat16 values
- Use BFloat16.MinValue/MaxValue instead of float.MinValue/MaxValue conversions
- This ensures test data matches the actual BFloat16 representation
- Fixes CI test failures where float-to-BFloat16 conversions produced different values

Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 17, 2026

@copilot the tests you added fail on CI. e.g.

Assert.Equal() Failure: Collections differ

...

Fixed in commit b44d1de. The issue was using float literals that converted to different BFloat16 values than expected. Changed to use BFloat16 constants directly (e.g., BFloat16.MinValue instead of (BFloat16)(-3.40282347E+38f)) and switched from [InlineData] to [MemberData] to support BFloat16 parameters.

@EgorBo
Copy link
Member

EgorBo commented Jan 17, 2026

@copilot use the collection literal syntax [ ... ] instead of yield return new object[] { ... }

- Changed from yield return new object[] { ... } to collection literal [ ... ] syntax
- Applied to both TryWriteSignificandBigEndianTest_TestData and TryWriteSignificandLittleEndianTest_TestData
- Uses expression-bodied member syntax with => for cleaner code

Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 17, 2026

@copilot use the collection literal syntax [ ... ] instead of yield return new object[] { ... }

Done in commit 062a536. Updated both test data methods to use collection literal syntax [ ... ] instead of yield return new object[] { ... }.

@skyoxZ
Copy link
Contributor

skyoxZ commented Jan 17, 2026

I believe BFloat16 has 8-bits significand? Then many codes in BFloat16 need to be corrected.

@EgorBo
Copy link
Member

EgorBo commented Jan 17, 2026

I believe BFloat16 has 8-bits significand? Then many codes in BFloat16 need to be corrected.

thanks! What do you mean by many codes?

@skyoxZ
Copy link
Contributor

skyoxZ commented Jan 17, 2026

What do you mean by many code?

BFloat16.Significand should be a byte, and there're several (not too many :) ) related codes.

This comment was marked as outdated.

@EgorBo
Copy link
Member

EgorBo commented Jan 17, 2026

What do you mean by many code?

BFloat16.Significand should be a byte, and there're several (not too many :) ) related codes.

I'm a bit confused, @tannergooding @huoyaoyuan can you clarify the behavior here. So TryWriteSignificand used to report 4 bytes were written (which is clearly a bug), but the Significant itself is defined as ushort, while it looks like it's actually should be byte? (7 bits to be precise, hence 1 byte written)?

@skyoxZ
Copy link
Contributor

skyoxZ commented Jan 17, 2026

Significand is an internal property so it doesn't affect behavior directly. However I noticed something confusing:

  • float.IFloatingPoint<float>.TryWriteExponentBigEndian: bytesWritten = 1, which I think it's correct.
  • float.IFloatingPoint<float>.TryWriteSignificandBigEndian: bytesWritten = 4. Maybe it should be 3?

@huoyaoyuan
Copy link
Member

I copied the logic from Half blindly without taking much attention here. Since the TryWrite API is for supporting various lengths, I think we should require least space that's sufficient for significand (1 byte).

@EgorBo
Copy link
Member

EgorBo commented Jan 17, 2026

@copilot it turns out Significand has to be byte-sized, can you changes all the relevant bits in BFloat16 ?

- Changed Significand property from ushort to byte (BFloat16 has 8-bit significand)
- Updated GetSignificandByteCount to return sizeof(byte) instead of sizeof(ushort)
- Updated TryWriteSignificandBigEndian to write 1 byte directly (no endianness needed)
- Updated TryWriteSignificandLittleEndian to write 1 byte directly (no endianness needed)
- Updated test data to expect 1 byte written instead of 2 bytes
- Updated test destination buffers from [0, 0] to [0]

Co-authored-by: EgorBo <523221+EgorBo@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 17, 2026

@copilot it turns out Significand has to be byte-sized, can you changes all the relevant bits in BFloat16 ?

Done in commit ca5fb35. Changed Significand property from ushort to byte (8-bit), updated GetSignificandByteCount() to return 1, and modified both TryWriteSignificand methods to write a single byte. Since it's only 1 byte, endianness doesn't matter, so both methods now simply write destination[0] = Significand. Tests updated to expect 1 byte written.

Copilot AI changed the title Fix BFloat16.TryWriteSignificand bytesWritten reporting 4 instead of 2 Fix BFloat16.Significand to be byte-sized and correct TryWriteSignificand methods Jan 17, 2026
@tannergooding
Copy link
Member

tannergooding commented Jan 19, 2026

Significand is an internal property so it doesn't affect behavior directly. However I noticed something confusing:

  • float.IFloatingPoint.TryWriteExponentBigEndian: bytesWritten = 1, which I think it's correct.
  • float.IFloatingPoint.TryWriteSignificandBigEndian: bytesWritten = 4. Maybe it should be 3?

We generally want the "size written" to be a primitive size and otherwise a multiple of 4 or 8. That is, 3 is essentially a "worst possible size" to write out because it means no primitive can trivially work with the data.

So we have:

Type Sign Exponent Significand
half 1-bit 5-bits 10-bits
bfloat16 1-bit 8-bits 7-bits
single 1-bit 8-bits 23-bits
double 1-bit 11-bits 52-bits
quad 1-bit 15-bits 112-bits

Which means we want:

Type Sign Exponent Significand
half bool 1 byte 2 bytes
bfloat16 bool 1 byte 1 byte
single bool 1 byte 4 bytes
double bool 2 bytes 8 bytes
quad bool 2 bytes 16 bytes

Edit: sorry, hit enter too soon and it posted.

It's then worth noting that despite there being that many "stored" bits, that is not how many bits actually exist. There is an implicit significand bit that exists, so the actual amount is:

Type Actual Significand
half 11-bits
bfloat16 8-bits
single 1-bit
double 1-bit
quad 113-bits

This could theoretically bump the limit over to the next size in some cases. It doesn't for any of these, however, so the fixes in this PR should be correct.

@tannergooding tannergooding merged commit de88fe7 into main Jan 19, 2026
142 of 146 checks passed
@tannergooding tannergooding deleted the copilot/fix-bfloat16-byteswritten branch January 19, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BFloat16.TryWriteSignificand{Big,Little}Endian report 4 for bytesWritten

6 participants