Skip to content

cuda: fmt=8 (fp8-e4m3) — dense matmul + expert-group kernels with 128x128 block scales - #817

Open
kreuzzelg wants to merge 1 commit into
JustVugg:devfrom
kreuzzelg:cuda-fp8-e4m3
Open

cuda: fmt=8 (fp8-e4m3) — dense matmul + expert-group kernels with 128x128 block scales#817
kreuzzelg wants to merge 1 commit into
JustVugg:devfrom
kreuzzelg:cuda-fp8-e4m3

Conversation

@kreuzzelg

Copy link
Copy Markdown

fmt=8 tensors — raw e4m3 bytes with one f32 scale per 128x128 block of [O,I], matmul_fp8's semantics — were CPU-only: qt_cuda_upload refused them because row_bytes()/weight_at() had no case for the format, and every kernel would have applied per-row scale semantics to a block-scaled container. This is the "route them deliberately" follow-up to the refusals #762 put in place, for the format that gained one more on-disk encoding in #779 (UE8M0 sidecars expand to the same f32 block scales at load, so the GPU only ever sees one encoding).

Decode table. quant_matmul gains an fmt=8 branch decoding through c_e4m3, a 256-float __constant__ LUT published from quant.h's E4M3_LUT via the new coli_cuda_fp8_set_lut — the same single-source-of-truth arrangement as the E8 codebook (#452). Uploads of fmt=8 tensors are refused until the LUT is published: a kernel reading the zero-initialized table would compute silent zeros, the exact failure mode this format's dispatch work exists to prevent. The engine gates on the same condition (g_cuda_fp8_ready), so an old DLL without the symbol degrades to CPU exactly like fmt=6 without its codebook. The loader resolves the symbol as optional.

Geometry from dims alone. The 128 block edge is a fixed property of the format (FP8_BLOCK), so the scale index derives from I and o — the branch ignores the gs/ng parameters entirely and is therefore correct from every call site: dense (coli_cuda_matmul), per-expert MLP, attention proj projections, and the async fallback loop with its gs=0, ng=1 arguments.

Expert groups. grouped_hidden_f8_dual / grouped_down_f8 mirror the g4 kernels (silu fused in the dual epilogue, one byte per weight through the LUT); the sync and async expert-group paths dispatch all-fmt=8 groups to them. Mixed groups take the per-expert loop (sync) or refuse (async), the same split as E8. The resident path already refuses non-s4 and is untouched.

Attention absorb: refused, not implemented. The absorb kernels decode their w through weight_at + absorb_scale, which know per-row and fmt=4 scales only — an fmt=8 kv_b would be mis-decoded. All absorb-path entries now refuse fmt>4 (absorb_fmt_ok), so callers keep their CPU attention path. A block-scale absorb kernel is follow-up work, same shape as routing fmt=4 through the grouped kernels was. proj tensors are exempt: they run through quant_matmul.

Test. tests/test_fp8_cuda.cu (standalone nvcc build, like the g4 oracle): phase 1 checks the grouped kernels against a reference that replicates matmul_fp8 — the e4m3 decode is recomputed arithmetically (sign/exp/mant, E4M3-FN NaN convention) so it cross-checks the LUT rather than assuming it; float-in-block/double-across accumulation; block scales at the checkpoint-realistic 2^-12 magnitude from #779's verification; dims with partial tail blocks on both axes. Phase 2 goes through the public API: the upload gate must refuse before coli_cuda_fp8_set_lut and accept after; dense matmul and the sync expert group check against the oracle; async issue/take must match sync bit for bit.

Verified on sm_86: fp8 oracle 0 mismatches (kernel + API phase), grouped-g4 oracle still 0 mismatches, make cuda-test (q8/q4/q2/f32/e8) and the ragged-attention test green, make glm CUDA=1 builds clean. Not run: a full GLM-5.2-FP8/DeepSeek end-to-end (no fp8 checkpoint on this box) — happy to if that's wanted before merge.

Not touched: Vulkan and Metal (their allowlists already exclude fmt=8), weight_at (fmt=8 never reaches it — every consumer either has a dedicated branch or refuses), the CPU path (matmul_fp8 unchanged, still the single reference implementation).

Thanks on Claude Fable 5 :-)

…scale aware

fmt=8 tensors (raw e4m3 bytes, one f32 scale per 128x128 block of [O,I],
matmul_fp8's semantics) were CPU-only: qt_cuda_upload refused them because
row_bytes()/weight_at() had no case and every kernel would have applied
per-row scale semantics to a block-scaled container.

Backend:
- quant_matmul gains an fmt=8 branch decoding through c_e4m3, a 256-float
  __constant__ LUT published from quant.h's E4M3_LUT via the new
  coli_cuda_fp8_set_lut (same single-source-of-truth arrangement as the E8
  codebook). The block geometry derives from I alone (FP8_BLOCK is a fixed
  property of the format), so the branch is correct from every call site --
  dense, expert MLP, and the async fallback loop included.
- grouped_hidden_f8_dual / grouped_down_f8 mirror the g4 expert kernels
  (silu fused in the dual epilogue); the sync and async expert-group paths
  dispatch all-fmt=8 groups to them. Mixed groups take the per-expert loop
  (sync) or refuse (async), like E8.
- Uploads of fmt=8 are refused until the LUT is published: a kernel reading
  the zero-initialized table would compute silent zeros. The engine gates on
  the same condition (g_cuda_fp8_ready), so an old DLL without the symbol
  degrades to CPU exactly like fmt=6 does without its codebook.
- The absorb-path entries refuse fmt>4 via absorb_fmt_ok: weight_at +
  absorb_scale know per-row and fmt=4 scales only, so fmt=5/6/8 would be
  mis-decoded there. Callers keep their CPU attention path; a block-scale
  absorb kernel is follow-up work. (proj tensors are exempt -- they run
  through quant_matmul.)

Engine: qt_cuda_upload/qt_cuda_update/matmul_qt_ex route fmt=8 weights from
q8 and gate on g_cuda_fp8_ready, published in cuda_boot next to the E8 grid.
Loader: fp8_set_lut resolved as optional, like e8_set_grid.

tests/test_fp8_cuda.cu: oracle vs a reference that replicates matmul_fp8
(LUT decode cross-checked arithmetically, float-in-block/double-across
accumulation, block scales at the checkpoint-realistic 2^-12 magnitude from
JustVugg#779's verification), covering partial tail blocks on both axes; API phase
checks the LUT gate refuses before publish, dense matmul, sync expert group
vs oracle, and async issue/take bit-identical to sync.
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