Skip to content

cuda: the NVFP4 lane matmul and prompt GEMM write fp32 too (qlinear decodeF32 and promptF32) - #757

Merged
ashhart merged 1 commit into
ashhart:mainfrom
Bizuayeu:pr/qlinear-f32
Oct 16, 2026
Merged

ashhart merged 1 commit into
ashhart:mainfrom
Bizuayeu:pr/qlinear-f32

Conversation

@Bizuayeu

@Bizuayeu Bizuayeu commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

A tensor-parallel family sums its ranks' partial rows before rounding, so it needs a projection's fp32 sums as the
kernel holds them. qmmf.cu and prompt16.cu already take the output type as a template argument; this
instantiates FP4's fp32 tiles (and reduce_kernel<true> for the split-K path), loads them beside the bf16 ones, and
adds Lane.matmulOut, Prompt.matmulOut and qlinear's decodeF32 and promptF32. Other formats refuse fp32 out
with EpilogueNotBuilt, as the experts do. matmul, decode and prompt keep their signatures, kernels and bits.

The GLM-5.3-Flash CUDA family (#548) is the first user: each rank's attention and MLP projections go out in fp32 and
are summed across ranks in rank order before the one rounding.

Receipt

  • Environment: TensorFold 1.0.9 (c36b536d) plus this commit; one GB10 (DGX Spark, sm_121, CUDA 13.3).
  • Exactness: the bf16 path is untouched (same kernels, same calls). tf-cuda-test quant-ref now also checks nvfp4's
    decodeF32 and promptF32 on synthetic weights ([320, 2560] and [2560, 512], 1 to 300 rows): within the float64
    tolerance the bf16 checks use, the same bits at every row count, and the bf16 out equal to the fp32 out rounded,
    element for element.
  • Decode speed before and after: not applicable (no change to the bf16 kernels or their launch).
  • Prompt speed before and after: not applicable (same).
  • Tests run:
    • tf-cuda-test quant-ref: 4 PASS, 0 FAIL. The new fp32 checks need this change to compile; their largest
      difference is 0.000 of the bf16 checks' tolerance, and no element of the bf16 out differs from the fp32 out rounded.
    • zig build test-cuda-host: 429 passed, 0 failed. Its test "a lane weight whose mode is not its format's is refused
      before any launch" now also covers fp32 out refused for fp8g and bf16.
    • zig build test: 517 passed, 5 skipped, 0 failed.
    • zig fmt --check zig/src zig/tests build.zig: clean. lean_check: 538, the same as main.
  • Not run, and why: Metal (CUDA only).

Checklist

  • Every token still goes through the lane rounds. No serial path, and nothing that needs drafts off.
  • Drafted output equals "draft": false, a resumed prompt equals a fresh one, and concurrent equals solo.
  • No precision traded for speed. If the bits change, the description says which sums change and why.
  • Prompt processing is no slower than the last release.
  • The description says which platforms I ran, Metal M1 to M5 and CUDA, and which I could not.
  • New tests fail before the change, pass after it, and skip cleanly without their dependency.
  • Comments and docstrings are one line. No measurements or history in the source.
  • No personal data, machine names, internal hosts or local paths. No AI attribution lines.

…ecodeF32 and promptF32)

A tensor-parallel family sums its ranks' partial rows before rounding, so it needs a projection's fp32 sums as
the kernel holds them. qmmf.cu and prompt16.cu already take F32 as a template argument; this instantiates FP4's
fp32 tiles (and reduce_kernel<true> for the split path), loads them beside the bf16 ones, and adds
Lane.matmulOut, Prompt.matmulOut and qlinear's decodeF32 and promptF32. Other formats refuse fp32 out with
EpilogueNotBuilt, as the experts do. matmul, decode and prompt keep their signatures and bf16 bits.
@Bizuayeu

Copy link
Copy Markdown
Contributor Author

Rebased onto 1.0.9 (c36b536d), the commit's lines unchanged. Host tests 429/429, zig build test 517 passed, 5 skipped.

BHCC2025 added a commit to BHCC2025/TensorFold that referenced this pull request Oct 13, 2026
A tensor-parallel family sums its ranks' partial rows before rounding, so an EXL3 projection must hand out its fp32
sums as ashhart#757 does for NVFP4. The decode kernel already takes an fp32 output flag; the prompt kernel gains the same
flag (tf_exl3_store), and qlinear's decodeF32 and promptF32 route .exl3 to them. decode and prompt keep their bf16
bits: the bf16 out is the fp32 out rounded. The new exl3 test checks both entries within FP64 bounds, the same bits at
every row count, and the bf16 out as their rounding; it fails with EpilogueNotBuilt before this change.
@ashhart
ashhart merged commit 6aba0ef into ashhart:main Oct 16, 2026
4 checks passed
ashhart added a commit that referenced this pull request Oct 16, 2026
ashhart added a commit that referenced this pull request Oct 16, 2026
The contributor's five GLM commits landed above, with our fixes after them; the head's first seven commits are the same changes as #757, #758, #766, #762, #764 and #763, which the candidate already has from their own heads. This keeps the PR's head in the history with no change to the tree.
@ashhart

ashhart commented Oct 16, 2026

Copy link
Copy Markdown
Owner

Thank you, @Bizuayeu. Writing the fp32 sums the lane matmul and prompt GEMM already hold, instead of rounding them first, is what a rank-order sum needs.

It ships in TensorFold 1.0.12 under your name (67efe47d4), with our 1285e2195 wrapping the new lines at 120 columns. Your head is merged after them with no change to the tree (6aba0ef09), so this pull request shows as merged.

On a GB10, tf-cuda-test quant-ref passes your fp32 cases: decodeF32 and promptF32 stay within the float64 tolerance with the same bits at every row count, and the bf16 output is the fp32 output rounded. All 29 functions in qmmf.fatbin and both in prompt16.fatbin compile to the same SASS as before, instruction for instruction. Nemotron NVFP4 is unchanged: drafted equals "draft": false in all 17 release cells with the same token SHAs, chats equal a cache-off server's, and 2, 4 and 8 streams equal solo; one stream decoded within 0.8% of before and cold prompts of 2k to 64k tokens within 0.4%.

GLM-5.3-Flash on CUDA (#804) is the first caller.

Posted by Ash's AI Assistant Synthena

ashhart pushed a commit that referenced this pull request Oct 18, 2026
A tensor-parallel family sums its ranks' partial rows before rounding, so an EXL3 projection must hand out its fp32
sums as #757 does for NVFP4. The decode kernel already takes an fp32 output flag; the prompt kernel gains the same
flag (tf_exl3_store), and qlinear's decodeF32 and promptF32 route .exl3 to them. decode and prompt keep their bf16
bits: the bf16 out is the fp32 out rounded. The new exl3 test checks both entries within FP64 bounds, the same bits at
every row count, and the bf16 out as their rounding; it fails with EpilogueNotBuilt before this change.
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
The contributor's commit landed above with the same code; this keeps the PR's head in the history with no change to the tree.
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
…date

The contributor's five GLM commits landed above, with our fixes after them; the head's first seven commits are the same changes as ashhart#757, ashhart#758, ashhart#766, ashhart#762, ashhart#764 and ashhart#763, which the candidate already has from their own heads. This keeps the PR's head in the history with no change to the tree.
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
A tensor-parallel family sums its ranks' partial rows before rounding, so an EXL3 projection must hand out its fp32
sums as ashhart#757 does for NVFP4. The decode kernel already takes an fp32 output flag; the prompt kernel gains the same
flag (tf_exl3_store), and qlinear's decodeF32 and promptF32 route .exl3 to them. decode and prompt keep their bf16
bits: the bf16 out is the fp32 out rounded. The new exl3 test checks both entries within FP64 bounds, the same bits at
every row count, and the bf16 out as their rounding; it fails with EpilogueNotBuilt before this change.
@Bizuayeu
Bizuayeu deleted the pr/qlinear-f32 branch October 25, 2026 07:27
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.

2 participants