cuda: fmt=8 (fp8-e4m3) — dense matmul + expert-group kernels with 128x128 block scales - #817
Open
kreuzzelg wants to merge 1 commit into
Open
cuda: fmt=8 (fp8-e4m3) — dense matmul + expert-group kernels with 128x128 block scales#817kreuzzelg wants to merge 1 commit into
kreuzzelg wants to merge 1 commit into
Conversation
…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.
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.
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_uploadrefused them becauserow_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_matmulgains an fmt=8 branch decoding throughc_e4m3, a 256-float__constant__LUT published from quant.h'sE4M3_LUTvia the newcoli_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, attentionprojprojections, and the async fallback loop with itsgs=0, ng=1arguments.Expert groups.
grouped_hidden_f8_dual/grouped_down_f8mirror 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
wthroughweight_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.projtensors are exempt: they run throughquant_matmul.Test.
tests/test_fp8_cuda.cu(standalone nvcc build, like the g4 oracle): phase 1 checks the grouped kernels against a reference that replicatesmatmul_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 beforecoli_cuda_fp8_set_lutand 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=1builds 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_fp8unchanged, still the single reference implementation).Thanks on Claude Fable 5 :-)