Skip to content

core/quant: FP8 and NVFP4 values and float64 references in one place - #730

Merged
ashhart merged 2 commits into
ashhart:mainfrom
jschmied:core-quant-refs
Oct 13, 2026
Merged

ashhart merged 2 commits into
ashhart:mainfrom
jschmied:core-quant-refs

Conversation

@jschmied

Copy link
Copy Markdown
Contributor

What this changes

The float64 decoders for FP8 (e4m3, toE4m3) and NVFP4 (e2m1) existed in three copies: tests/cuda/quant_ref_tests.zig, tests/cuda/modelopt_tests.zig and cli/cuda_precision.zig. They now live once, in core/quant/fp8.zig and core/quant/nvfp4.zig, together with a float64 reference for a block-FP8 stack and an NVFP4 stack. The two tests and check-precision decode through them. This is the first piece of the core/quant format modules proposed in #548. No kernel and no engine path changes.

core/exlr8_codec.zig (1.0.8) has its own f32 e4m3Value. That could decode through fp8.e4m3 too, but I left it for a separate change.

Receipt

  • Environment: TensorFold 0efbd447 (1.0.8) plus this commit; one DGX Spark (GB10, sm_121, driver 580.173.02, CUDA 13).
  • Exactness: no kernel or engine change. The GPU tests that use the moved decoders give the same results: tf-cuda-test quant-ref (every product within 0.510 of its float64 tolerance) and tf-cuda-test modelopt (NVFP4 relu^2 experts, rms error up 3.316e-3, down 1.646e-3).
  • Decode and prompt speed: unchanged (no kernel, launch or engine path touched).
  • Tests run: zig build test, 44/44 steps; new host tests "e4m3 codes and their rounding", "a block-FP8 stack against hand-decoded values", "an NVFP4 stack against hand-decoded values". A wrong subnormal exponent in e4m3 fails 5 of them. lean_check.py: 539 findings, as on main.
  • Not run, and why: Metal (no Metal code touched); RTX cards (no hardware here).

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 engine change.)
  • No precision traded for speed. If the bits change, the description says which sums change and why. (No bits change.)
  • 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.

…zig); the CUDA tests and check-precision decode through them
@ashhart
ashhart merged commit 67f27fd into ashhart:main Oct 13, 2026
2 checks passed
jschmied pushed a commit to jschmied/TensorFold that referenced this pull request Oct 13, 2026
jschmied pushed a commit to jschmied/TensorFold that referenced this pull request Oct 13, 2026
jschmied pushed a commit to jschmied/TensorFold that referenced this pull request Oct 13, 2026
jschmied pushed a commit to jschmied/TensorFold that referenced this pull request Oct 13, 2026
@ashhart

ashhart commented Oct 13, 2026

Copy link
Copy Markdown
Owner

Thank you, @jschmied. Three hand-kept copies of the e4m3 and e2m1 decoders were bound to drift apart, and one home for them with float64 references beside the other formats is the right shape. Your commit landed in 1.0.9 under your name (f9bf4b43a), and the PR's head was merged into the history with no change to the tree (f9077462f).

Your head moved after that, with e4m3's two NaN codes: 0x7f and 0xff now decode as NaN on the host, as the GPU's conversion reads them, where they read as ±480. That commit landed too, under your name (35cfecb97), with a CHANGELOG sentence, and your current head is merged as well (67f27fd3b), so GitHub shows this PR as merged. No checkpoint holds those codes, so no reply changes.

Your first commit lands without its runner.addImport("core", mods.core) line in zig/build/cuda.zig, because your #736 had already added the same line there.

Two commits of ours follow it. The first (7d526335e) fixes three inputs a reference accepted but could not read:

  • An f64 scale_inv passed isFloat, and convert.load then reached unreachable. The FP8 reference now takes fp32, bf16 or fp16 scales only (convert.tableDtype).
  • An empty projection (N or K of 0) divided by zero at numel / (n * k). Both references now refuse it.
  • An NVFP4 expert stack whose scale tensor has a different leading dimension from its weights was read as if they matched. Both references now check every leading dimension.

Each fix has a test that failed before it (two aborts and one missing error) and passes after. The second commit (a28f264f7) only shortens the FP8 layout comment to 120 columns.

What we measured, against the build before your change:

  • Host tests. The six tests in fp8.zig and nvfp4.zig (three of them ours) pass on macOS and Linux.
  • Same kernels. On a GB10 (sm_121), every fatbin built from source, and the CUDA server binary, came out byte-identical with and without the change. Only tensorfold, which holds check-precision, and tf-cuda-test differ.
  • Same references. tf-cuda-test quant-ref and tf-cuda-test modelopt printed the same output byte for byte before and after: every product within 0.510 of its float64 tolerance, repacks equal to the host packers, rows 1-16 byte-equal and 300 prompt rows chunk-invariant, and the same relu^2 expert errors.

Nothing more is needed from you.

Bizuayeu added a commit to Bizuayeu/TensorFold that referenced this pull request Oct 13, 2026
nvfp4.zig is the format the GLM-5.3-Flash CUDA port's W4A16 linears read: e2m1 codes two to
a byte (input 2j in byte j's low nibble) [n, k/2], an e4m3 scale per 16 inputs [n, k/16],
and weight_scale_2 as an fp32 scalar that multiplies. dims() checks the four tensors fit
together; Linear.read() maps them from a Checkpoint and marks input_scale as used, since
the W4A4 math's static input scale is not read by W4A16 but a leftover tensor would refuse
the checkpoint; row() gives a row as e2m1 x e4m3 x global in f32, the reference the GPU
kernels will be compared with. stored() says a module is NVFP4 by its weight_scale tensor.
The e2m1 and e4m3 values come from core.quant's nvfp4 and fp8 (ashhart#730), as f32.

The fixture is the whole header of nvidia/GLM-5.3-Flash-NVFP4 (147,661 names, dtypes and
shapes, gzip'd): every NVFP4 linear reads, 42 MoE layers x 288 experts x 3 plus the three
dense MLP layers' 3, all under .mlp., and the MTP layer's experts stay BF16.
Bizuayeu added a commit to Bizuayeu/TensorFold that referenced this pull request Oct 13, 2026
nvfp4.zig is the format the GLM-5.3-Flash CUDA port's W4A16 linears read: e2m1 codes two to
a byte (input 2j in byte j's low nibble) [n, k/2], an e4m3 scale per 16 inputs [n, k/16],
and weight_scale_2 as an fp32 scalar that multiplies. dims() checks the four tensors fit
together; Linear.read() maps them from a Checkpoint and marks input_scale as used, since
the W4A4 math's static input scale is not read by W4A16 but a leftover tensor would refuse
the checkpoint; row() gives a row as e2m1 x e4m3 x global in f32, the reference the GPU
kernels will be compared with. stored() says a module is NVFP4 by its weight_scale tensor.
The e2m1 and e4m3 values come from core.quant's nvfp4 and fp8 (ashhart#730), as f32.

The fixture is the whole header of nvidia/GLM-5.3-Flash-NVFP4 (147,661 names, dtypes and
shapes, gzip'd): every NVFP4 linear reads, 42 MoE layers x 288 experts x 3 plus the three
dense MLP layers' 3, all under .mlp., and the MTP layer's experts stay BF16.
@ashhart ashhart mentioned this pull request Oct 14, 2026
Bizuayeu added a commit to Bizuayeu/TensorFold that referenced this pull request Oct 14, 2026
nvfp4.zig is the format the GLM-5.3-Flash CUDA port's W4A16 linears read: e2m1 codes two to
a byte (input 2j in byte j's low nibble) [n, k/2], an e4m3 scale per 16 inputs [n, k/16],
and weight_scale_2 as an fp32 scalar that multiplies. dims() checks the four tensors fit
together; Linear.read() maps them from a Checkpoint and marks input_scale as used, since
the W4A4 math's static input scale is not read by W4A16 but a leftover tensor would refuse
the checkpoint; row() gives a row as e2m1 x e4m3 x global in f32, the reference the GPU
kernels will be compared with. stored() says a module is NVFP4 by its weight_scale tensor.
The e2m1 and e4m3 values come from core.quant's nvfp4 and fp8 (ashhart#730), as f32.

The fixture is the whole header of nvidia/GLM-5.3-Flash-NVFP4 (147,661 names, dtypes and
shapes, gzip'd): every NVFP4 linear reads, 42 MoE layers x 288 experts x 3 plus the three
dense MLP layers' 3, all under .mlp., and the MTP layer's experts stay BF16.
ashhart pushed a commit that referenced this pull request Oct 16, 2026
nvfp4.zig is the format the GLM-5.3-Flash CUDA port's W4A16 linears read: e2m1 codes two to
a byte (input 2j in byte j's low nibble) [n, k/2], an e4m3 scale per 16 inputs [n, k/16],
and weight_scale_2 as an fp32 scalar that multiplies. dims() checks the four tensors fit
together; Linear.read() maps them from a Checkpoint and marks input_scale as used, since
the W4A4 math's static input scale is not read by W4A16 but a leftover tensor would refuse
the checkpoint; row() gives a row as e2m1 x e4m3 x global in f32, the reference the GPU
kernels will be compared with. stored() says a module is NVFP4 by its weight_scale tensor.
The e2m1 and e4m3 values come from core.quant's nvfp4 and fp8 (#730), as f32.

The fixture is the whole header of nvidia/GLM-5.3-Flash-NVFP4 (147,661 names, dtypes and
shapes, gzip'd): every NVFP4 linear reads, 42 MoE layers x 288 experts x 3 plus the three
dense MLP layers' 3, all under .mlp., and the MTP layer's experts stay BF16.
ashhart pushed a commit that referenced this pull request Oct 18, 2026
nvfp4.zig is the format the GLM-5.3-Flash CUDA port's W4A16 linears read: e2m1 codes two to
a byte (input 2j in byte j's low nibble) [n, k/2], an e4m3 scale per 16 inputs [n, k/16],
and weight_scale_2 as an fp32 scalar that multiplies. dims() checks the four tensors fit
together; Linear.read() maps them from a Checkpoint and marks input_scale as used, since
the W4A4 math's static input scale is not read by W4A16 but a leftover tensor would refuse
the checkpoint; row() gives a row as e2m1 x e4m3 x global in f32, the reference the GPU
kernels will be compared with. stored() says a module is NVFP4 by its weight_scale tensor.
The e2m1 and e4m3 values come from core.quant's nvfp4 and fp8 (#730), as f32.

The fixture is the whole header of nvidia/GLM-5.3-Flash-NVFP4 (147,661 names, dtypes and
shapes, gzip'd): every NVFP4 linear reads, 42 MoE layers x 288 experts x 3 plus the three
dense MLP layers' 3, all under .mlp., and the MTP layer's experts stay BF16.
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
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
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
nvfp4.zig is the format the GLM-5.3-Flash CUDA port's W4A16 linears read: e2m1 codes two to
a byte (input 2j in byte j's low nibble) [n, k/2], an e4m3 scale per 16 inputs [n, k/16],
and weight_scale_2 as an fp32 scalar that multiplies. dims() checks the four tensors fit
together; Linear.read() maps them from a Checkpoint and marks input_scale as used, since
the W4A4 math's static input scale is not read by W4A16 but a leftover tensor would refuse
the checkpoint; row() gives a row as e2m1 x e4m3 x global in f32, the reference the GPU
kernels will be compared with. stored() says a module is NVFP4 by its weight_scale tensor.
The e2m1 and e4m3 values come from core.quant's nvfp4 and fp8 (ashhart#730), as f32.

The fixture is the whole header of nvidia/GLM-5.3-Flash-NVFP4 (147,661 names, dtypes and
shapes, gzip'd): every NVFP4 linear reads, 42 MoE layers x 288 experts x 3 plus the three
dense MLP layers' 3, all under .mlp., and the MTP layer's experts stay BF16.
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
nvfp4.zig is the format the GLM-5.3-Flash CUDA port's W4A16 linears read: e2m1 codes two to
a byte (input 2j in byte j's low nibble) [n, k/2], an e4m3 scale per 16 inputs [n, k/16],
and weight_scale_2 as an fp32 scalar that multiplies. dims() checks the four tensors fit
together; Linear.read() maps them from a Checkpoint and marks input_scale as used, since
the W4A4 math's static input scale is not read by W4A16 but a leftover tensor would refuse
the checkpoint; row() gives a row as e2m1 x e4m3 x global in f32, the reference the GPU
kernels will be compared with. stored() says a module is NVFP4 by its weight_scale tensor.
The e2m1 and e4m3 values come from core.quant's nvfp4 and fp8 (ashhart#730), as f32.

The fixture is the whole header of nvidia/GLM-5.3-Flash-NVFP4 (147,661 names, dtypes and
shapes, gzip'd): every NVFP4 linear reads, 42 MoE layers x 288 experts x 3 plus the three
dense MLP layers' 3, all under .mlp., and the MTP layer's experts stay BF16.
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
nvfp4.zig is the format the GLM-5.3-Flash CUDA port's W4A16 linears read: e2m1 codes two to
a byte (input 2j in byte j's low nibble) [n, k/2], an e4m3 scale per 16 inputs [n, k/16],
and weight_scale_2 as an fp32 scalar that multiplies. dims() checks the four tensors fit
together; Linear.read() maps them from a Checkpoint and marks input_scale as used, since
the W4A4 math's static input scale is not read by W4A16 but a leftover tensor would refuse
the checkpoint; row() gives a row as e2m1 x e4m3 x global in f32, the reference the GPU
kernels will be compared with. stored() says a module is NVFP4 by its weight_scale tensor.
The e2m1 and e4m3 values come from core.quant's nvfp4 and fp8 (ashhart#730), as f32.

The fixture is the whole header of nvidia/GLM-5.3-Flash-NVFP4 (147,661 names, dtypes and
shapes, gzip'd): every NVFP4 linear reads, 42 MoE layers x 288 experts x 3 plus the three
dense MLP layers' 3, all under .mlp., and the MTP layer's experts stay BF16.
BlivionIaG pushed a commit to BlivionIaG/TensorFold that referenced this pull request Oct 20, 2026
nvfp4.zig is the format the GLM-5.3-Flash CUDA port's W4A16 linears read: e2m1 codes two to
a byte (input 2j in byte j's low nibble) [n, k/2], an e4m3 scale per 16 inputs [n, k/16],
and weight_scale_2 as an fp32 scalar that multiplies. dims() checks the four tensors fit
together; Linear.read() maps them from a Checkpoint and marks input_scale as used, since
the W4A4 math's static input scale is not read by W4A16 but a leftover tensor would refuse
the checkpoint; row() gives a row as e2m1 x e4m3 x global in f32, the reference the GPU
kernels will be compared with. stored() says a module is NVFP4 by its weight_scale tensor.
The e2m1 and e4m3 values come from core.quant's nvfp4 and fp8 (ashhart#730), as f32.

The fixture is the whole header of nvidia/GLM-5.3-Flash-NVFP4 (147,661 names, dtypes and
shapes, gzip'd): every NVFP4 linear reads, 42 MoE layers x 288 experts x 3 plus the three
dense MLP layers' 3, all under .mlp., and the MTP layer's experts stay BF16.
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