Skip to content

Implement Zfbfmin BF16 converts - #1879

Open
davidharrishmc wants to merge 3 commits into
openhwfoundation:rva23from
davidharrishmc:dh/zfbfmin
Open

davidharrishmc wants to merge 3 commits into
openhwfoundation:rva23from
davidharrishmc:dh/zfbfmin

Conversation

@davidharrishmc

@davidharrishmc davidharrishmc commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Adds the two Zfbfmin scalar instructions, fcvt.bf16.s and fcvt.s.bf16, and enables the extension on rv32gc and rv64gc.

Approach

BF16 shares single precision's sign bit, 8-bit exponent field and bias, differing only in fraction width (7 vs 23). Both converts therefore run down the existing single precision convert pathfcvt feeding the postprocessor — rather than becoming a format of their own:

  • A BF16 source is widened to single before the unpacker ({bf16, 16'b0} is bit-identical to the single value), so unpackinput is untouched.
  • A Bf16Dst flag swaps the fraction width where the rounder needs it.
  • Because the fraction is genuinely rounded to 7 bits, a BF16 result is the top half of the single precision result, so specialcase packs it with a rebox rather than seven per-field overrides.

Fmt/OutFmt stay at single throughout, which is what keeps this cheap: the exponent datapath, flags.sv and the overflow/underflow thresholds are all correct with no changes at all.

Cost

118 insertions, 32 deletions across 20 files. What is added:

  • round.sv (24+/5−) — guard/round/LSB, sticky and RoundAdd, each folded into the signal where it is defined, so the hand-unrolled per-format logic is untouched and every consumer reads one signal. Load-bearing: without rounding at 7 bits the rebox would be a truncation.
  • specialcase.sv (27+/11−) — the single precision result reboxed to BF16 on the way out, plus the underflow result, whose single precision LSB the rebox would otherwise drop.
  • cvtshiftcalc.sv (6+/1−) — the underflow threshold, -7 for BF16 against -23 for single.
  • unpack.sv (11+/1−) — one mux widening a BF16 source.
  • fcvt.sv (10+/3−) — one extra input to an adder that already sums three terms.

No new wide muxes on the result path and no added logic depth: Guard is format-mux to BF16-mux to CvtToInt-mux either way, and RoundAdd is mux-then-adder either way. This is a gate-level argument, not a synthesis result.

Each BF16 case sits with the signal it belongs to rather than at the point of use: ResNegNF, NormSticky, FpGuard, FpLsbRes, FpRound and RoundAdd each feed a Dst-prefixed signal that accounts for BF16, so CvtResUf, Sticky, Guard/LsbRes/Round and the final rounding add are unchanged from master and a future second consumer of any of those signals gets BF16 for free. The sticky bit is the one value that cannot come from a format arm: those ranges are nested, so a fifth fraction width moves every boundary in each FPSIZES/XLENPOS variant.

Subnormal convert fix

Routing these through fcvt exposed a latent bug in shared logic, fixed here in the same commit.

When a subnormal source produces a subnormal result, LeadingZeros normalizes the significand but ShiftAmt = NF-1+Ce never undoes it, so the fraction lands mis-aligned. Concretely for single 0x007fffff: the unpacked Man has hidden bit 0, LeadingZeros is 1, Ce is 0, and shifting by 51 leaves the zero hidden bit at the top of Mf, so the fraction reads back as 0x3F8000 instead of 0x7F0000.

That combination is only reachable for a same precision convert — narrowing flushes a subnormal source to zero (a subnormal double is ~2^-1022, far below single's 2^-149 minimum) and widening turns one into a normal — and fctrl rejects Rs2D[1:0] != 2'b00. BF16 against single is the first case with equal exponent ranges, which is why this has lain dormant.

The fix adds LeadingZeros back into ShiftAmt on the subnormal branch. No condition is needed: LeadingZeros is already zero for a normal source, since the leading zero count runs on the mantissa including the hidden bit.

Encodings

From binutils' own test (gas/testsuite/gas/riscv/zfbfmin.d), confirmed by assembling and disassembling:

instruction funct5 fmt rs2
fcvt.bf16.s 01000 10 01000
fcvt.s.bf16 01000 00 00110

Testing

riscv-arch-test against Sail-generated signatures on the RTL. The wider runs are what cover the shared ShiftAmt change, exercising the existing F/D/Zfh/Zfa converts:

suite config result
Zfbfmin cvw-rv64gc / cvw-rv32gc All 6 pass, both
F, D, Zfh, Zfhmin, Zfa, Zfbfmin cvw-rv64gc All 297 pass
F, D, Zfh, Zfhmin, Zfa, Zfbfmin cvw-rv32gc All 279 pass

Verilator lint clean on rv64gc, rv32gc and rv32i — no errors, no new warnings.

Requires the companion riscv-arch-test change enabling Zfbfmin on the cvw configs (riscv/riscv-arch-test#2379).

🤖 Generated with Claude Code

@davidharrishmc

Copy link
Copy Markdown
Contributor Author

Still in review. Do not yet merge.

@davidharrishmc
davidharrishmc force-pushed the dh/zfbfmin branch 2 times, most recently from c07fe5a to 9cc241f Compare September 17, 2026 05:20
Add fcvt.bf16.s and fcvt.s.bf16.  BF16 shares single's exponent field and
bias and differs only in fraction width, so both run down the existing
single precision convert path: a BF16 source is widened before the
unpacker, and a Bf16Dst flag swaps the fraction width where the rounder
and the result packing need it.  The exponent datapath and the flag logic
are untouched, and a BF16 result is the top half of the single precision
one, so packing it is a rebox rather than a format of its own.

Undo the leading zero shift when converting a subnormal source to a
subnormal result.  Only a same precision convert reaches that combination,
since narrowing flushes such a source to zero and widening normalizes it,
so this was unreachable until BF16 gave single an equal exponent range.

Enabled on rv32gc and rv64gc, including their imperas.ic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@davidharrishmc
davidharrishmc marked this pull request as ready for review September 17, 2026 05:34
@davidharrishmc

Copy link
Copy Markdown
Contributor Author

Ready for review.

Per review, fold the BF16 cases into ResNegNF, NormSticky, FpGuard,
FpLsbRes, FpRound and RoundAdd rather than overriding them at the point
of use, so CvtResUf, Sticky, Guard/LsbRes/Round and the final rounding
add read one signal each and a second consumer of any of them would get
BF16 without a new override.  The sticky bit is the one value a BF16
result cannot take from a format arm, since the sticky ranges are nested
and a fifth fraction width moves every boundary.

Parameterize the specialcase widths in terms of S_LEN and BF16_NF, and
drop the ZFBFMIN_SUPPORTED generates: Bf16Dst is already gated on the
parameter, so the muxes optimize away on their own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@davidharrishmc

Copy link
Copy Markdown
Contributor Author

Simplifications added thanks to @kparry4

@davidharrishmc
davidharrishmc changed the base branch from main to rva23 September 18, 2026 10:20
Zfbfmin's loads, stores and moves are Zfh's flh, fsh, fmv.x.h and
fmv.h.x, so a config with Zfbfmin but no Zfh cannot get a BF16 value
into a register.  Assert Zfh rather than F, and turn Zfbfmin off in the
derivatives that turn Zfh off, which otherwise inherited it from rv64gc
and rv32gc.

Decode Bf16SrcD and Bf16DstD with the rest of the instruction instead of
re-deriving the opcode, funct7 and rs2 separately, so they respect the
STATUS_FS and supported format and rounding mode checks that gate the
rest of the control word.  fcvt.s.bf16 shares fcvt.s.(d/q/h)'s control
word, so it folds into that branch.

Drop Bf16SrcM, which nothing read, and widen the BF16 source in one
assign rather than a generate block.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant