Repository navigation
perf(cuda): cp.async multicol int4 GEMV prefetch — measured NO-GO (default off, byte-identical) - #1000
perf(cuda): cp.async multicol int4 GEMV prefetch — measured NO-GO (default off, byte-identical)#1000justinchuby wants to merge 2 commits into
Conversation
…-GO, default off Re-test of the sota/TRT-LLM roadmap's #1 lever (cp.async weight prefetch + L2::evict_first) on the current multicol NC=4 M=1 decode GEMV, prompted by fresh stall data showing Long-Scoreboard is now the top stall (2.35) on this kernel — unlike the prior narrow-kernel no-go (#978 follow-on) which was register/warp-bound. New entry matmul_nbits_gemv_f16_general_bs_wide_multicol_cpasync double-buffers each lane's WIDE_NC 128-bit weight words through shared memory with cp.async.cg.shared.global.L2::evict_first, prefetching K-tile k+1 while tile k computes. Keeps the exact per-lane depth striding + per-column fp32 accumulation order, so output is BYTE-IDENTICAL (glm greedy tokens 0 diffs over 160 tokens, generic + repetitive). SM80+ with a byte-identical direct-load #else fallback (Rule 11). Dispatch gate use_gemv_cpasync(cc): SM80+, int4/block!=32 multicol only, DEFAULT OFF, opt-in via ONNX_GENAI_GEMV_CPASYNC=1. qwen (block-32) and the default decode path are unchanged. Result: NO-GO. glm decode 199.9 -> 191.6 tok/s (-4.2%). ncu on the gate_up matrix: cp.async DID hide global-load latency (Long-Scoreboard 2.35 -> 1.88, -20%) exactly as designed, but it is a NET LOSS — the cvta-to-shared + pipeline bookkeeping raised registers 64 -> 71, dropping achieved occupancy 41.6% -> 33.3%, and the added smem round-trip pushed kernel time 33.9 -> 40.2 us (+18%) and DRAM 1.78 -> 1.50 TB/s. At M=1 this GEMV hides load latency primarily via warp-level parallelism (occupancy), so the register cost of a software prefetch pipeline outweighs the latency it hides. This is the third convergent prefetch negative (register depth-4, prior cp.async, this multicol cp.async): the M=1 decode GEMV is occupancy-bound, not prefetch-bound. Kept behind the env flag as a reproducible negative result; no regression ships by default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fresh-data re-test of TRT-LLM roadmap PORT-3 on the current multicol kernel: byte-identical but -4.2% (occupancy 41.6->33.3% from +7 registers outweighs the 20% Long-Scoreboard reduction). Third convergent prefetch negative; M=1 GEMV is occupancy-bound. Recommends dropping PORT-3/PORT-5 and pointing at PORT-1 (occupancy via cheaper dequant) or landing #996 fp16 + lm_head as the honest path to beat ORT base. 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 #1000 +/- ##
=======================================
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
|
|
Triage, not a rejection. The measured negative is valuable and I want it in the tree; the code half I cannot validate. Why I am not merging as-is: this PR carries a CUDA kernel variant alongside its decision drop, and this machine no longer has a CUDA runtime -- a recursive search finds no What I would like: split the decision drop out and I will land it immediately, the way #1011 just landed. A measured no-go deserves permanent shelf space -- the whole point is that the next person to propose this lever finds the numbers instead of re-running the experiment. The kernel variant can then wait for a reviewer with a working CUDA host, where "default off" and "byte-identical" can actually be demonstrated rather than asserted. If you would rather not split it, say so and I will leave this open until I have GPU hardware again; that is a worse outcome only because the negative record stays unpublished in the meantime. |
Summary — measured NO-GO, default off, byte-identical (reproducible negative)
Re-test of the sota/TRT-LLM roadmap's #1 lever — PORT-3 cp.async double-buffered weight prefetch +
L2::evict_first— on the current multicol NC=4 M=1 int4 decode GEMV. Result: NO-GO, −4.2% glm decode. Kept behindONNX_GENAI_GEMV_CPASYNC=1(default OFF), byte-identical, so nothing regresses by default. This documents the negative with fresh, mechanistically-explained data.Why re-test (the prior no-go was a different kernel)
cp.async was already a measured no-go (
2131046e) — but on the narrow pre-multicol kernel (register-bound, ~80% occupancy, 8–12% DRAM). The current multicol kernel is different, so I re-profiled first: on the current default kernel Long-Scoreboard is now the #1 stall (2.35) — exactly cp.async's target — so the precondition IS met here. That justified a genuine re-test.What it does
matmul_nbits_gemv_f16_general_bs_wide_multicol_cpasyncdouble-buffers each lane's WIDE_NC=4 128-bit weight words through shared memory withcp.async.cg.shared.global.L2::evict_first(Marlin idiom), prefetching K-tile k+1 while tile k computes. Exact per-lane depth striding + per-column fp32 accumulation order preserved ⇒ byte-identical. SM80+ with a byte-identical direct-load#elsefallback (Rule 11). Gate: SM80+, int4/block≠32 multicol only, default off.Result
ncu — cp.async worked, but is occupancy-negative
Mechanism: cp.async cut the global-load stall 20% as designed, but the
cvta-to-shared + pipeline state raised registers 64→71, dropping occupancy 41.6%→33.3%. At M=1 this GEMV hides latency primarily via warp-level parallelism (occupancy), so losing 8 pp of occupancy removes more hiding than the prefetch adds. Third convergent prefetch negative (register depth-4, prior cp.async, this one): the M=1 decode GEMV is occupancy-bound, not prefetch-bound.Roadmap recommendation
Decision
Merge as a documented negative (like
2131046e) or take the decision drop and close — either way no regression ships by default. Decision drop:.squad/decisions/inbox/deckard-cpasync-nogo.md.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com