Repository navigation
perf(cuda): fp16 int4 wide GEMV matching ORT's M=1 arithmetic — glm 199→213 tok/s, accuracy-gated (opt-in) - #996
Merged
Merged
Conversation
…opt-in) Adds an opt-in fp16 sibling of the column register-blocked wide GEMV (matmul_nbits_gemv_f16_general_bs_wide_multicol_fp16), gated by ONNX_GENAI_GEMV_FP16=1. The per-chunk MAC runs in fp16 __hfma2 (two fused MACs/instruction) instead of fp32 FFMA, matching ORT's fp16 arithmetic — the equal-conditions fp16-vs-fp16 path. PRECISION CONTRACT: only the 32 products of ONE quant chunk (single per-block scale) are summed in fp16 into a __half2 accumulator, then reduced to fp32, scaled, and accumulated across chunks in fp32. The K=4096 reduction stays fp32 (NOT a full-fp16 K accumulate). Not byte-identical to fp32 by construction -> opt-in, gated on accuracy (error <= ORT vs f64 oracle), not bit-identity. ncu (glm gate_up): 34.3->27.0us, DRAM 1.76->2.24 TB/s (ORT 2.42), L1/TEX 28->36%, SM 60->54% (now occupancy-bound at 41%). glm base decode 198->210 tok/s (+6.2%); greedy tokens identical to fp32 over 160 tokens on generic + repetitive prompts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds fp16_mixed_gemv_matches_f64_oracle_glm_decode: runs the fp16 kernel (ONNX_GENAI_GEMV_FP16=1) on glm's real block-128 M=1 projection shapes (qkv/o/gate_up/down x sym+asym zp x fp16+fp32 scales) against the same f64 dequant->GEMM oracle, and holds it to the SAME justified Envelope the reviewed fp32 int4 path satisfies (no weakening). Also runs the fp32 path on each problem and reports the fp16-vs-fp32 oracle-error ratio, and asserts the fp16 output diverges from fp32 (proving the fp16 entry -- not the fp32 fallback -- was selected). Pins ONNX_GENAI_GENERAL_SPLITK=0 for deterministic entry selection. Measured: fp16 max_rel=6.3e-3 (8x under the 5e-2 bound); symmetric ~1.05-1.20x the fp32 path, asymmetric-zp 9-13x on rel but abs error stays <<bound (small output magnitude). fp16 keeps the K reduction in fp32 so it is at least as precise as any full-fp16 kernel. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #996 +/- ##
=======================================
Coverage 83.56% 83.56%
=======================================
Files 12 12
Lines 5270 5270
Branches 5270 5270
=======================================
Hits 4404 4404
Misses 682 682
Partials 184 184
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
🔴 Benchmark Regression DetectedComparison of criterion micro-benchmarks: PR head vs merge-base, measured on the same runner in the same job (base first → PR second).
Visual flags: Host infoWhat this cannot catch
|
Replace the per-chunk fp32 flush in the multicol fp16 int4 GEMV with a full-fp16 half2 accumulate across the lane's K chunks, folding the per-block scale in with __hfma2 and deferring fp32 to only the final cross-lane warp_sum. This mirrors ORT's MatMulFloat4BitsKernelM1 arithmetic exactly. Safe because the per-lane fp16 reduction is a wide, shallow tree: with 32 lanes striding K by 32, each lane folds only ~K/1024 chunks (~4 for K=4096), so fp16 accumulation depth is tens, not thousands — negligible mantissa loss (unlike a naive deep single-accumulator fp16 sum, which flips tokens). f64 oracle: fp16 max_rel=1.16e-2, 4.3x under the 5e-2 envelope (same error class as the reviewed fp32 int4 path). Steady glm base decode 212.8 vs fp32 199.4 tok/s. Speed is a tie with the per-chunk-fp32 version (kernel 26.4 vs 27.0us): the limiter is multicol register state + wave/tail quantization (achieved occupancy 41%), not the accumulator, so ORT's "full-fp16 is faster" holds only marginally at the kernel level. Kept behind ONNX_GENAI_GEMV_FP16=1 for the validation phase. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reflect the final shipped kernel (full-fp16 half2 accumulate matching ORT's MatMulFloat4BitsKernelM1) and the honest findings from the ORT source read: LOP3+prmt idioms were already present; full-fp16 vs per-chunk-fp32 is a speed tie (occupancy/wave-bound, not accumulator- bound); WIDE_NC_FP16=2 rejected; f64 max_rel 1.16e-2; glm 212.8 tok/s. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This was referenced Aug 15, 2026
…6 K-accum The const doc comment for GEMV_F16_GENERAL_BS_WIDE_MULTICOL_FP16_ENTRY and the accuracy-gate test doc comment both still described the fp16 GEMV as summing each chunk in fp16 then "reduced to fp32 and accumulated across chunks in fp32 (the K reduction stays fp32)". That contradicts the shipped kernel: the per-lane K reduction runs ENTIRELY in fp16 __half2 accumulators (per-block scale folded via __hfma2), matching ORT's MatMulFloat4BitsKernelM1, with fp32 used ONLY in the final cross-lane warp-shuffle reduction. It is safe (no token-flipping mantissa loss) because each lane strides K by 32 and folds only a handful of chunks — a wide, shallow fp16 tree of depth ~tens, not K. Comments only; no code or arithmetic change. The device-function block comment (gemv_int4_fp16_lane_dot_multicol) was already accurate and is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby
added a commit
that referenced
this pull request
Aug 15, 2026
… flip) Correct the default-on decision drop: withdraw the "one-liner hook to default-on fp16 GEMV later / fp16 fixes fp32 nondeterminism" plan. Chew's #996 verdict is land-as-opt-in — glm has intermittent knife-edge argmax flips vs fp32, so fp16 is not byte-identity-safe for default. Out-of-box GEMV stays fp32 (byte- identical); the honest out-of-box default-on number is ~207.7 short / ~171.7 KV2048 (fp16 OFF). 232 requires user-set ONNX_GENAI_GEMV_FP16=1 and is not the default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby
marked this pull request as ready for review
August 15, 2026 09:26
justinchuby
added a commit
that referenced
this pull request
Aug 15, 2026
Promote the #991 cuBLASLt lm_head decode MatMul to default-on and harden its CUDA-graph capture guard from a single cached shape to a shape-keyed plan cache, so the out-of-box glm base decode banks the cuBLASLt logits projection without an env flag. Capture guard (Gaff's #991 blocker): `launch_dense_capturable` now keeps a shape-keyed `Vec<DenseGemmPlan>` (LRU, MRU-promote on hit, bounded by DENSE_PLAN_CACHE_CAP=8) instead of a single `Option<DenseGemmPlan>`. Every distinct (dtype, M, K, N) the node runs keeps its own preselected cuBLASLt algorithm + persistent workspace, so alternating shapes (prefill M>1, decode M==1, a speculative M=K verify width) all replay with no per-call heuristic query inside the captured region. The reject-cold-miss-during-capture contract is unchanged (now enforced per shape); eviction only ever runs off the non-capturing creation path, and the hot decode plan is kept at the MRU front, so a plan baked into a live captured graph is never freed. Strictly safer than the single-slot path (evicts less; same recapture-on-change discipline). Default-on: `lmhead_cublaslt_enabled()` inverts to default-TRUE-unless-disabled; set ONNX_GENAI_LMHEAD_CUBLASLT=0/false/off for the hand-GEMV escape hatch. #999 GQA flash-decoding is already default-on (its env vars are rollback-only knobs), so no flip is needed there. GEMV fp16 (#996) stays untouched (not in this base) and is a one-liner default flip once it merges. Measured (glm-4-9b-int4, H200, graph-on, fallbacks=0): - default-on short decode 207.72 tok/s (vs escape-hatch-off hand-GEMV 202.24) - default-on KV2048 171.69 tok/s, fallbacks=0 across 8 bucket-growth recaptures - lm_head cuBLASLt(default) == hand-GEMV(off) top-40 logprobs BYTE-IDENTICAL - qwen no regression (152.16 vs 153.60, within noise), fallbacks=0 - f64 oracle 7/7, GQA fp16 2/2, fmt + clippy clean Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby
added a commit
that referenced
this pull request
Aug 15, 2026
… flip) Correct the default-on decision drop: withdraw the "one-liner hook to default-on fp16 GEMV later / fp16 fixes fp32 nondeterminism" plan. Chew's #996 verdict is land-as-opt-in — glm has intermittent knife-edge argmax flips vs fp32, so fp16 is not byte-identity-safe for default. Out-of-box GEMV stays fp32 (byte- identical); the honest out-of-box default-on number is ~207.7 short / ~171.7 KV2048 (fp16 OFF). 232 requires user-set ONNX_GENAI_GEMV_FP16=1 and is not the default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
justinchuby
added a commit
that referenced
this pull request
Aug 15, 2026
…1004) ## Default-on: lm_head cuBLASLt + multi-shape capture guard Promotes #991's cuBLASLt `lm_head` decode MatMul to **default-on** (out-of-box glm base), after clearing Gaff's #991 blocker: the single-shape cuBLASLt plan cache is hardened into a **shape-keyed multi-shape plan cache** so plan creation never happens inside a captured CUDA-graph region across prefill/decode/verify shape switches. Rebased onto main `ad141370` (post-#996); disjoint from #996's opt-in fp16 (which stays OFF by default). ### Capture guard — path (a): shape-keyed multi-shape plan cache #991 already had a warm-once / reject-cold-miss-during-capture contract, but for a SINGLE cached shape (a shape switch evicted + re-ran the heuristic search). This PR generalizes it to a `Vec<DenseGemmPlan>` keyed by `(dtype,M,K,N)`, MRU-ordered, bounded by `DENSE_PLAN_CACHE_CAP = 8`: - **hit** → MRU-promote, launch preselected algo (no heuristic query); - **cold miss while capturing** → `Err` (reject-before-warm, now per shape); - **cold miss not capturing** → create + insert-front + LRU-truncate. - Eviction runs **only** off the non-capturing path; the hot decode plan (M==1) stays MRU-front and is never the LRU victim. Strictly safer than the single slot (evicts far less; same recapture-on-change discipline). **Shape-transition proof** (temporary `DENSE_PLAN_SEARCH` counter, glm KV2048 run = prompt → decode → **8 KV-bucket-growth recaptures**, 8623 replays): - **exactly 2 plan searches for the whole run** — `M=3` prefill (once) + `M=1` decode (once), each on the non-capturing path; - **0 searches** during any of the 8623 replays or 8 recaptures; - **fallbacks=0** (a cold-miss-inside-capture would surface as a fallback; none did). This is the "no per-transition plan search, not just steady-state" guarantee. Counter reverted (not committed). ### Default-on flip (byte-identical wins only) - `lmhead_cublaslt_enabled()` → default-TRUE-unless-disabled; escape hatch `ONNX_GENAI_LMHEAD_CUBLASLT=0|false|off` → hand fp16 GEMV. - **#999 GQA flash-decoding already default-on** (audited — its env vars are rollback-only knobs); no flip needed. - **GEMV fp16 (#996) stays PERMANENTLY opt-in — NOT flipped.** Chew's #996 verdict is land-as-opt-in: the fp16 K-accumulate is not byte-identity-safe vs fp32 (glm knife-edge argmax flips), so it is not a default-on candidate. Out-of- box GEMV stays fp32 (byte-identical); fp16 via `ONNX_GENAI_GEMV_FP16=1`. ### Measured (glm-4-9b-int4, H200, graph-on, median, no env flags) | config | short tok/s | KV2048 tok/s | fallbacks | |---|---|---|---| | **default-on (no flags)** | **~206–208** | **~166–172** | **0** | | escape-hatch `=0` (hand GEMV) | 202.24 | — | 0 | (short 207.72 GPU3 / 206.44 GPU4; KV2048 171.69 / 168.06 / 165.59 across GPU3/6/4 — cross-GPU + shared-host variance; all fallbacks=0.) - Byte-identity: cuBLASLt lm_head (default) vs hand-GEMV (off) top-40 token-0 logprobs **byte-identical** (both select token 315). - qwen no regression (152.16 vs 153.60, noise), fallbacks=0. - f64 oracle 7/7; GQA fp16 2/2; fmt + clippy clean. **Out-of-box number is ~206–208 short / ~166–172 KV2048 and that is final for this PR.** The ~232 combined figure required fp16 GEMV opt-in ON; since fp16 ships permanently opt-in, 232 is only reachable via `ONNX_GENAI_GEMV_FP16=1`.⚠️ Please gate with fresh Chew (byte-identity / f64 oracle) + Gaff (capture-safe, fallbacks=0, win real). --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.
Summary
Opt-in fp16 int4 M=1 wide GEMV that matches ORT's
MatMulFloat4BitsKernelM1arithmetic — full-fp16 (half2) K accumulate, fp32 only in the final cross-lane shuffle — stacked on the merged #992 multicol NC=4 activation-reuse layout. This is the honest base-vs-base, fp16-vs-fp16 lever: ORT's int4 M=1 kernel is fp16, our merged path is fp32-accumulate, which structurally caps us below ORT. Ships permanently opt-in viaONNX_GENAI_GEMV_FP16=1; the default (fp32) path is untouched and byte-identical to main.Merge disposition (Chew 🟡 land-as-opt-in): the fp16 numerics are sound (matches ORT's own fp16 scheme, error well under bound) but the kernel is not byte-identity-safe vs the fp32 path — glm has intermittent knife-edge argmax flips (see Accuracy gate) — so fp16 stays opt-in and is not a default-on candidate. This deliberately does not flip the default.
Result (glm-4-9b-int4, H200,
--steady --tokens 160 --decode-skip 40 --runs 3)ORT base ≈ 250–252 → native-vs-ORT base gap 1.30× → 1.18×.
ncu (glm gate_up,
--graph-profiling node, launch-skip 240)Kernel is now at ~ORT parity (26.4 vs 24.9 µs; 2.29 vs 2.42 TB/s).
Honest findings from the ORT source read
int4x8_to_half2x4_sub,decode_activation8_h2). The only arithmetic delta was per-chunk-fp32 → full-fp16 accumulate.WIDE_NC_FP16=2(fewer cols → fewer regs) rejected: occupancy moved only 41→43% (wave-bound, not register-bound) and the kernel got slower.Accuracy gate (opt-in, non-byte-identical by design)
fp16_mixed_gemv_matches_f64_oracle_glm_decode): fp16max_rel = 1.16e-2, 4.3× under the 5e-2 envelope (same envelope the reviewed fp32 int4 path meets). Full numerics suite 8/8. Chew confirmed: genuinely mirrors ORT's fp16 scheme, error class ≤ ORT by construction.block_size != 32arm); tokens identical flag on/off.Portability / capture
__hfma2is SM53+ (no arch guard). Static grid, capture-safe.Reviewer asks
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com