Fuse the FP8 promotion into MATMUL so BF16 never reaches DDR - #164
Merged
Conversation
The FP8 storage tier could only promote to a BF16 block output. That made it a poor deal for the case it exists to serve: expanding FP8 operands to feed a multiply meant materializing both BF16 tensors in DDR, so peak working set went *up* relative to just storing BF16, and the consumer paid a second submission to read them back. This admits the same explicit promotion feeding a MATMUL as one program. The graph is unchanged in meaning — two TOSA CAST operators still state the widening — but the backend performs it per L1 tile on the compute core, so the caller binds two FP8 operands and one FP32 result and no BF16 tensor is allocated or transferred. For M=K=N=256 that is 384 KiB of DDR against 640 KiB, 2.33x less DDR traffic, and one submission instead of three. Fusing is a placement choice, not a numerical one. FP8 to BF16 is exact for every encoding, and the multiply is the identical bf16->f32 `kernels.mm` the BF16 tier already uses, so results are bit-identical to running the two admitted tiers back to back. `fused_fp8_matmul_is_bit_identical_to_cast_then_matmul` checks exactly that on the NPU by running both paths and comparing the FP32 bytes, rather than against a host oracle that could drift. This is the first admitted tier with graph-interior values, so admission pins the dataflow tightly: each MATMUL operand must come from its own CAST, each CAST must consume a distinct block input, both operands must carry the same FP8 encoding (the kernel instantiates one decoder), and a promoted value that also escapes as a block output is rejected because the fused kernel never writes BF16 to DDR. The advertised FP8 capability gains MATMUL, BF16 as INTERMEDIATE, and FP32 as OUTPUT, so the descriptor covers the graph's interior as well as its boundary. The FP8 decoders are now shared between a sized emitter and the standalone CAST tier's fixed 1,024-element entry points; that tier's kernel source is unchanged byte for byte, so its cache key and on-metal evidence still hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`compile_artifact` drives the helper subprocess in its own process group and is `cfg(unix)`, so the new test must carry the same gate the existing offline-compile test does. The Windows job caught this as an unresolved import. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SnowCheetos
approved these changes
Aug 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The FP8 storage tier could only promote to a BF16 block output. That makes it a poor deal for the
case it exists to serve. Expanding FP8 operands to feed a multiply meant materializing both BF16
tensors in DDR, so the peak working set went up relative to simply storing BF16, and the
consumer paid extra submissions to read them back. A storage tier that costs more memory at the
point of use is not doing its job.
This admits the same explicit promotion feeding a MATMUL as a single program. The graph is unchanged
in meaning — two TOSA
CASToperators still state the widening, so promotion stays explicit perCONTEXT.md— but the backend performs it per L1 tile on the compute core. The caller binds two FP8operands and one FP32 result; no BF16 tensor is ever allocated or transferred.
At the tier's measured ~80 µs fixed per-submission cost, dropping two submissions is worth more than
the bytes for small shapes.
Correctness
Fusing is a placement choice, not a numerical one, and the argument is bit-exact by construction:
FP8 → BF16 is exact for every encoding, the widening uses the same decoders as the validated
CAST tier, and the multiply is the identical bf16→f32
kernels.mmthe BF16 MATMUL tier alreadyuses. So the fused result must equal CAST-then-MATMUL bit for bit.
fused_fp8_matmul_is_bit_identical_to_cast_then_matmulchecks precisely that on the NPU by runningboth paths and comparing the FP32 bytes — not against a host oracle that could drift from either.
The generated MLIR confirms the intended dataflow inside a single core:
All three symbols are defined text in the linked core ELF, so nothing is dead-stripped. The L2→L1
layout transform is expressed in elements and so is dtype-agnostic; widening afterwards is
elementwise and preserves the micro-tile ordering
mmexpects.Admission
This is the first admitted tier with graph-interior values, so the dataflow is pinned tightly. Each
MATMUL operand must be produced by its own CAST; each CAST must consume a distinct block input; both
operands must carry the same FP8 encoding (the kernel instantiates one decoder); and a promoted value
that also escapes as a block output is rejected, because the fused kernel never writes BF16 to DDR.
Anything looser would let a kernel that binds two FP8 inputs stand in for a graph it does not
implement.
The advertised FP8 capability gains
MATMUL,BF16asINTERMEDIATE, andFP32asOUTPUT— thedescriptor must cover the graph's interior as well as its boundary.
fp8_capability_is_storage_ conversion_onlyasserted the old CAST-only surface; it is rewritten asfp8_target_never_produces_ fp8, which pins the property that actually still holds (FP8 is only ever consumed) rather thanbeing deleted.
Refactor note
The FP8 decoders are now shared between a new sized emitter and the standalone CAST tier's fixed
1,024-element entry points. That tier's kernel source is unchanged byte for byte (asserted
against
HEADduring development), so its compiler cache key and existing on-metal evidence stillhold. This also removes the hard-coded
1024the tier's emitter previously carried twice.Compatibility
Purely additive to admission: graphs that were rejected are now admitted. No previously admitted
graph changes behavior. The advertised FP8 capability widens.
Checklist
behavior — or does, and says so above.
layout.json,vectors.json,scenarios.json,requirements.json, and performance budgetsare still authoritative inputs, not regenerated by accident.
CompilerSpec::Fp8Matmulis new, andXDNA_TOSA_FP8_CAPABILITYadvertises MATMUL plus the BF16
INTERMEDIATEand FP32OUTPUTroles.dependency changes at all.
unsafecode was added or modified.Verification
316 passed, 0 failed, 23 ignored across the workspace.
cargo fmt --all -- --check git diff --check python3 ci/check-release-policy.py cargo clippy -p virtio-accel-xdna --all-targets --all-features --no-deps -- -D warnings cargo test --workspace --all-targets --all-features RUSTDOCFLAGS=-D warnings cargo doc -p virtio-accel-xdna --all-features --no-deps python3 ci/check-performance-budgets.py --checkThe offline compile path was exercised for real against the pinned v2026.08 toolchain on this host
(no NPU needed):
fused_fp8_matmul_compiles_to_a_wellformed_artifactcompiles the fused graph andasserts the container binds
[2048, 2048] -> [4096]for a 32x64x32 multiply — FP8 operands at onebyte per element, and no BF16 slot at all. Shapes 32x64x32 through 256x256x256 and both FP8
encodings were compiled successfully during development.
native.rs/hardware.rsare behindcfg(va_xdna), which CI never enables, so they were alsotype-checked and linted with the cfg forced on via the build script's bare-lib-dir escape hatch, and
that path was confirmed non-vacuous by planting a deliberate error in the new test and watching the
check fail.
Not run: the on-metal suite. This host has no NPU or HRX, so
fused_fp8_matmul_is_bit_identical_to_cast_then_matmulis type-checked but unexecuted. It is thetier's acceptance criterion and must pass on the reference
1022:17f0part before this merges.Until then the numerical claim rests on the by-construction argument above plus the verified
generated code, which is evidence but not "on metal".
Follow-ups (not in scope)
The fused kernel inherits the BF16 tier's single-worker structure and its scalar C-tile zeroing and
B re-streaming, so it does not itself close the throughput gap tracked in #149/#151 — it removes a
DDR round trip and two submissions, which is a different axis. A fused FP8 path for the INT8 tier,
and mixed-encoding operands (two decoders), are both deliberately unimplemented.