Repository navigation
perf(cuda): tensor-core (mma.sync FP16) decode attention kernel — port Vulkan's coopmat design #199
Description
Background
Same investigation as #197/#198 (elevated ncu profiling of attention_f32 decode). See
docs/CUDA.md's Future Work entry for the full numbers.
The gap
No tensor-core kernel touches the decode shape on CUDA today. native/kernels/attention_flash_mma.cu
(hand-written mma.sync.m16n8k16 PTX, ldmatrix/mma helpers already exist and work) is
prefill-only — its CudaFlashAttention.CanUse gate requires seqKv==seqLen (no persistent
KV-cache decode), headDim==64, positionOffset==0, no sliding window.
Vulkan already has a decode-capable cooperative-matrix (tensor-core) kernel:
native/vulkan/shaders/attention_f32_coopmat.comp — its own header explicitly documents handling
seqQ=1 decode (a Br=16 query-row tile, 15 rows zero-padded, only row 0 written back) alongside
prefill. This is a real, shipped, tested reference implementation to port the idea from (Vulkan
coopmat and CUDA mma.sync are different APIs, so this is design-porting, not a literal code port).
Scope
- Study
attention_f32_coopmat.comp's decode-handling approach (row-padding scheme) and
attention_flash_mma.cu's existing PTXldmatrix/mma.synchelper functions (reusable —
already proven correct for the prefill kernel). - Design and implement an FP16 (or BF16) Q/K/V/accumulator decode kernel using tensor cores,
informed by both references. - Sequencing: per perf(cuda): port Vulkan's tuned split-KV attention heuristic to replace fixed ATTN_KV_SPLIT=4 #197/perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198, do this AFTER those land — this kernel's ceiling (roughly
8-10x FP16-tensor-core vs FP32-CUDA-core dense throughput on Ampere, a training-knowledge
estimate not yet re-verified) is only realized once the kernel is out of the latency-bound
regime perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198 targets; building this first would likely still be latency-bound and the tensor-core
win would be largely wasted.
Risk — HIGH, precision-sensitive
This is the highest-risk item of the four identified in this investigation. attention_f32.cu's
own file header documents a real precision lesson already learned once: switching the kernel's
softmax from a Schraudolph bit-trick fast-exp to precise expf made the CUDA kernel and the CPU
oracle (DotLLM.Cpu.Kernels.FastMath) disagree by ~1% (5e-3 abs) — both backends now deliberately
share the same fast-exp approximation to stay near-equivalent. An FP16 Q/K/V/accumulator rewrite
reopens exactly this class of question with a NEW precision axis (F32->FP16, not just fast-exp
vs. precise exp). Per this project's CLAUDE.md ("Correctness then Performance" priority, "SIMD
kernels: verify numerical accuracy against scalar reference first, then optimize"):
- Needs a deliberately-characterized new AbsTol/RelTol bar (check
docs/QUANTIZATION.mdand this
codebase's existing fixture-parity conventions for precedent, e.g. the
CudaQwen3MoeHybridParityTestsAbsTol(1e-4)+RelTol(1e-3) bar used elsewhere). - Needs careful CPU-vs-GPU AND FP16-vs-F32-CUDA parity testing before shipping, likely opt-in
behind a flag initially (matching this project's established pattern for perf(cuda): gdn_scan_step_f32 grid-too-small occupancy — cooperative-groups row-split (ncu-informed follow-up to #173) #180/perf(cuda): attention_f32 flash-decoding split-KV for depth-dependent decode cost #183's
reassociation-risk kernels) rather than replacing the default path outright.
Priority
Third of 4 identified gaps — do after #197 (cheap win) and #198 (highest-confidence fix for the
diagnosed root cause). Brainstorm/design this now if useful, but treat implementation as gated on
#198 landing and re-profiling first.
🤖 Generated with Claude Code
Activity
jamesburton commentedon Jul 27, 2026
Design-ahead plan: tensor-core FP16 decode attention (DESIGN ONLY — not a green light to implement)
This is a brainstorm/design document produced ahead of implementation, per this issue's own
sequencing note (do this after #197/#198 land and re-profiling confirms the ceiling is worth
chasing). At the time of writing, #197 and #198 are both still open with no design/implementation
comments — there is nothing yet to build on top of from those issues; this design is
self-contained and notes where it should be revisited once they land.
0. A directly-relevant precedent this repo already has, that the issue description doesn't mention
Before designing anything new, I found the codebase has already tried and rejected a full-FP16
decode-attention path once, for this exact model family. From
src/DotLLM.Cuda/Architectures/CudaQwen3MoeHybridTransformerModel.cs(lines ~110-151, ~2041-2051):This is the single most load-bearing fact for this design. Two things follow from it:
AbsTol(1e-4)+RelTol(1e-3)synthetic-fixture bar used elsewhere in this repo(
CudaQwen3MoeHybridParityTests, etc.) is already known to be unreachable with a Q-cast-to-FP16attention path on this architecture, at small headDim. Don't target it for this kernel. Target
the same 5e-3 abs OR rel bar
CudaTensorCoreAttentionParityTestsalready uses for theprefill flash-mma kernel — that's the established, working precedent for "attention with FP16
Q/K/V" in this repo, not the tight bar.
QK dot product as a CUDA-core scalar reduction over FP16-cast operands (mirroring
attention.cu'sattention_f16shape) — precision loss compounds from both the FP16 operandrounding and however the accumulation was done.
mma.sync.m16n8k16.f16.f16.f32(the PTX thisissue proposes reusing from
attention_flash_mma.cu) is architecturally guaranteed to accumulatein FP32 registers regardless of input dtype — the accumulator was never the weak link for
tensor cores the way it may have been for Option B's scalar path. So this design is not simply
repeating a known failure, but it does inherit the Q-cast-to-FP16 risk Option B hit, on a
small headDim, which is exactly the failure mode reported. This needs to be re-characterized,
not assumed safe by analogy to
attention_flash_mma.cu's prefill parity (which was tested atheadDim=64, not headDim=8/16).
Also unresolved and worth flagging as a prerequisite fact-check:
attention_flash_mma.cuishardcoded
HEAD_DIM=64(#define HEAD_DIM 64, unrolled k-stepsHEAD_DIM/16 == 4,n-subtiles
HEAD_DIM/8 == 8). I could not confirm Bonsai-27B's (qwen35moe) actualheadDiminthis pass — if it isn't 64, the existing flash-mma helpers can be reused as PTX primitives but the
kernel's tiling constants cannot be reused unmodified, and Vulkan's coopmat kernel (which loops in
D_TILE=16chunks up toMAX_HEAD_DIM=256with zero-padding, not a hardcoded head dim) is the morerelevant structural reference, not
attention_flash_mma.cu's fixed-64 layout. Verify Bonsai'sheadDim before writing any kernel code.
1. Current decode path (baseline this kernel replaces)
Traced end-to-end for the Bonsai-27B full-attention layers
(
CudaQwen3MoeHybridTransformerModel.ForwardFullAttnBody, ~line 2050-2085):_f16KCache/_f16VCache, model-private per-layer sidecar).LaunchConvertF16ToF32dequants the entire live[0, seqKv)prefix K and V into a shared F32scratch (
_f32KvReadStagingK/V), thenLaunchAttentionF32(theattention_f32kernel innative/kernels/attention_f32.cu, Schraudolph fast-exp, grid =numHeads, one block per queryhead) runs entirely in F32.
ncuprofiling flagged: grid=24 for Bonsai's 24 heads,16.5-16.8% achieved occupancy, latency-bound (~42 stall cycles/instruction, ~95% cycles with no
eligible warp).
A tensor-core decode kernel that reads the FP16 KV cache directly (skipping the
LaunchConvertF16ToF32dequant step entirely) is a real secondary win this design should capture —not just the matmul throughput, but removing a whole kernel launch + HBM round-trip per layer per
decode step. Q would still need a cast to FP16 (currently produced as F32 from the preceding
RMSNorm/projection in this model's F32 pipeline) — that cast is exactly Option B's risk vector
above, so it needs to be the primary focus of precision characterization, not the KV-cache read
(cache read is already lossy today by design and known to be within tolerance).
2. Kernel structure recommendation
Scope narrow initially: one query head per block/warp-group, tensor cores replacing the CUDA-core
matmul only. Do NOT bundle GQA-group batching (#198) into this kernel's first version.
Reasoning:
amortization, explicitly scoped to stay F32, no new precision risk, per perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198's own text). If
this kernel adopts perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198's block-per-KV-head-group structure and changes dtype at the same time,
any precision regression found in validation is ambiguous — is it the regrid or the dtype? Keep
them separable so a failure in the new precision axis doesn't also indict perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198's already-designed,
lower-risk regrid.
attention_flash_mma.cualready has a provengroupwarps/block pattern (one warp per queryhead sharing a KV head, KV loaded once into shared, reused across the group's warps) that IS a
form of GQA-group batching, at the warp level, for prefill. It is reasonable — and probably the
right move once perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198 lands — to compose the two, but the sequencing should be: (a) land this
kernel scoped to decode's
seqQ=1shape with the SAME warp-per-query-head / KV-shared-oncepattern
attention_flash_mma.cualready has (this is not "GQA-group batching" in the perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198 senseof restructuring the grid to fix the latency-bound occupancy problem; it's just reusing the
existing prefill kernel's grouping, which is already
groupwarps cooperatively loading oneblock's K/V) — (b) once perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198's regrid lands and is validated, evaluate whether this kernel's grid
needs to change to match it, as a small follow-up, with the dtype axis already proven safe by
then.
Row-padding scheme (from
attention_f32_coopmat.comp's decode handling, ported tomma.synctiling):
attention_flash_mma.cutiles the query axis inQ_TILE=16rows vialdmatrix.x4(an 8x8x4 = 16x16 fragment load). For decode,
seqQ=1: allocate the sameQ_TILE=16-row sharedsQbuffer, fill query row 0 with the real (single) query vector and rows 1-15 with zero(matching Vulkan's
validRows/zero-pad approach exactly — "the wasted lanes are not a correctnessissue"), run the identical QK/softmax/PV
mma.syncpipeline unmodified, and on write-back onlyemit row 0 (
gr0 < seqcheck inattention_flash_mma.cu's final loop already does exactly thiskind of bounds check — for decode it becomes "only write if this is the real row", the same
validRowsidea Vulkan names explicitly). This means the existing PTX helpers(
ldmatrix_x4/ldmatrix_x2/mma_m16n8k16) and the QK/softmax/PV structure need zero changes —only the grid/launch config (one query tile total instead of
ceil(seq/16)), the KV loop bound(no causal triangle — decode attends to the full
[0, seqKv)cache, replacingkv_end = min(seq, q0 + Q_TILE)withkv_end = seqKv), and thepositionOffsethandling (decodeneeds
position_offsetsupport, which the current prefill kernel hardcodes to 0 perCudaFlashAttention.CanUse's gate — this is a real gap to close, not just a formality: the causalmask inside the online-softmax loop needs
gq = positionOffsetinstead of assuming prefill'sq0 + lane).GPU utilization cost of the 16-row pad: 15/16 of the tensor-core MMA throughput is wasted on
zero rows for decode. This is fine — the whole point (per the issue's own estimate) is that even a
"wasteful" tensor-core kernel doing 1/16 useful MMA work can still beat a latency-bound CUDA-core
kernel that's achieving ~17% occupancy, if the launch is no longer latency-bound (which is why
#198 needs to land first — more resident warps per block from GQA-group sharing is what fixes the
latency-bound diagnosis; tensor cores alone don't fix "no eligible warp to issue"). This is worth
stating explicitly in case a future implementer is tempted to skip #198: without more resident
warps/block, a decode-shaped mma.sync kernel launched at
grid=numHeads(today's grid) risks beingjust as latency-bound as
attention_f32is today, wasting the entire tensor-core throughputadvantage on stalls, not matmul work.
3. Dtype recommendation
FP16 (not BF16) for Q/K/V storage and mma.sync inputs; FP32 accumulator — matching
attention_flash_mma.cu's existing prefill kernel exactly, not introducing a third variant.Justification:
mma.sync.aligned.m16n8k16.row.col.f32.f16.f16.f32(the exact instruction
attention_flash_mma.cualready uses) accumulates in FP32 by hardwaredefinition. This matches "common flash-attention practice" the issue asks about, and it is
strictly more precise than the rejected Option B's likely FP16-adjacent accumulation.
mma.sync...f32.bf16.bf16.f32, same PTX family, alsoAmpere-supported) trades FP16's extra mantissa bits for FP32-matching exponent range. The
rejected Option B failure was attributed to "drift from the Q-cast," not overflow/underflow — a
mantissa-precision problem, not a range problem — which argues for FP16 (more mantissa bits, same
8-bit-exponent range is not the reported failure mode) over BF16. Also: reusing FP16 keeps this
kernel numerically comparable to
attention_flash_mma.cuandattention_f16(both already FP16in this codebase), so any parity-test infrastructure and tolerance precedent
(
CudaTensorCoreAttentionParityTests's 5e-3 abs/rel bar) transfers directly. Introducing BF16would mean this is the first BF16 CUDA kernel in the repo (grep found no existing
bf16/__nv_bfloat16CUDA kernel) — a second new axis of risk/infrastructure this design shouldnot add unless FP16 is characterized and found insufficient first.
reproduces at Bonsai's real headDim, BF16 is the documented fallback to try next — same PTX
shape, same helper functions, only the fragment element type and conversion intrinsics change.
4. Precision-tolerance characterization plan (must happen BEFORE writing kernel code)
Given the prior Option B failure, this needs more rigor than "port the prefill kernel's 5e-3 bar
and hope it holds":
reuse if it still exists) a small script/unit-test-style harness that does Q→FP16 cast → FP16
dot product accumulated in FP32 (i.e. exactly what
mma.syncwould do) vs the current F32reference, on the same hidden=32/headDim=8 synthetic fixture that broke Option B. If FP32
accumulation alone (no accumulation-precision loss, only the FP16 input rounding) already
breaches the tight 1e-4/1e-3 bar at that headDim, that confirms the Q-cast itself (not
accumulation) was the dominant error source, and the same risk fully carries into this design.
If FP32-accumulated FP16 inputs pass at that fixture size where FP16-accumulated inputs failed,
that's evidence the accumulator precision, not the input cast, was Option B's problem — good
news for this design, since mma.sync's accumulator is FP32. This single check is cheap
(no GPU kernel needed, pure host-side float math) and should be the first thing done, before any
CUDA code.
DotLLM.Cpu, same fast-exp convention asattention_f32.cu) vs the new FP16 tensor-core kernel, small synthetic fixture (mirror thehidden=32/headDim=8 Bonsai fixture specifically, since that's the one with known history —
add the real Bonsai headDim as a second fixture size once known). Target the loose bar (5e-3
abs OR rel), NOT the tight synthetic bar the model-level tests use — per finding #0 above, the
tight bar is already known unreachable for FP16-cast attention on this architecture.
CudaTensorCoreAttentionParityTests): new kernel vsattention_f16(existing CUDA-core FP16 kernel) at realistic magnitude (DOTLLM_CUDA_ATTN_MAGpattern already exists — reuse it), decode shape (
seqQ=1, varyingseqKv— 1, 256, 1024,4096+ to cover the depths perf(cuda): port Vulkan's tuned split-KV attention heuristic to replace fixed ATTN_KV_SPLIT=4 #197/perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198's investigation already profiled at). This is an
apples-to-apples FP16-vs-FP16 check (isolates the tensor-core-vs-CUDA-core question from the
F32-vs-FP16 question, which Tier 1 covers).
real generation (reuse whatever harness produced the "300-step non-compounding drift"
characterization
CudaAttentionF32SplitKvTests.csalready did for perf(cuda): attention_f32 flash-decoding split-KV for depth-dependent decode cost #183's split-KV kernel) withthe new kernel swapped in, opt-in, and confirm output token sequences match the F32 baseline for
a meaningful number of steps at realistic depth (the perf(cuda): port Vulkan's tuned split-KV attention heuristic to replace fixed ATTN_KV_SPLIT=4 #197 issue text already establishes decode
depths of interest: 256, 768, 1024, 2048+). Per this kernel's own math: attention over an
unperturbed KV cache is not persistent-state-compounding the way GDN's scan state is (same
argument
attention_f32.cu's header already makes for why split-KV's reassociation is safe overmany steps) — a numeric perturbation in this layer's attention output at step N does not feed
back into what's cached for this layer at step N. The only cross-step pathway is discrete
(a different sampled token branches the whole generation). So the right validation isn't "does
drift compound" (it structurally can't, the same way it doesn't for perf(cuda): attention_f32 flash-decoding split-KV for depth-dependent decode cost #183) — it's "does any
single decode step produce a large enough output delta to flip the argmax/sampled token", which
is a distribution-level check (logit-diff histograms across many real decode steps), not a
drift-accumulation check.
attention_f16(Tier 2, matchesCudaTensorCoreAttentionParityTestsprecedent for FP16 attention already in this repo). If step1's isolated repro shows the FP32-accumulator does NOT save the Q-cast precision problem at
Bonsai's real headDim, this tolerance target needs to be revisited (either loosen further and
accept degraded quality with explicit measurement of downstream eval impact — e.g. perplexity or
task-accuracy delta, not just tensor-diff — or fall back to the BF16 variant from section 3, or
conclude the kernel isn't viable for this architecture and scope it to models/head-shapes where
it does hold).
5. Default-off / opt-in
Yes, ship opt-in / default-off initially, following the #180 (GDN split) / #183 (attention
split-KV) precedent, for stronger reasons than those two kernels had:
numerical-noise question with well-understood error bounds. This kernel is a dtype change
with a documented prior failure on this exact architecture. That's categorically higher risk
than either precedent.
CudaFlashAttention.Enabled/CanUseand the GDN split'sDOTLLM_GDN_SCAN_APPROX_SPLIT4env var work: a named env var (something likeDOTLLM_CUDA_ATTN_TENSORCORE_DECODE), default OFF, device-eligibility-gated to GeForce Ampere(same
sm_86-only constraintCudaFlashAttentionalready has, since the PTX is Ampere-onlymma.sync), and aCanUse-style per-call gate mirroringCudaFlashAttention.CanUsebut for thedecode shape (
seqQ==1,headDimmatching whatever the kernel is built for,positionOffsetnow supported rather than required to be 0, GQA group ≤ whatever cap the tuned kernel uses).
bar across Bonsai's real headDim, not just the small synthetic fixture; (b) the real-generation
distribution check (section 4.3) shows no measurable argmax-flip rate increase over a
statistically meaningful number of real decode steps; (c)
ncure-profiling (post-perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198, per thisissue's own sequencing) confirms the kernel is no longer latency-bound and the measured wall-clock
decode speedup is a real, reproducible win over the perf(cuda): port Vulkan's tuned split-KV attention heuristic to replace fixed ATTN_KV_SPLIT=4 #197+perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198-improved F32 baseline — not just an
ncu"Est. Speedup" number (this investigation's own stated lesson: those overstate realwall-clock impact).
6. Prerequisites from #197/#198
Neither #197 nor #198 has a design or implementation yet (checked at time of writing — both open,
zero comments). So today, nothing concrete to build on top of. When this is picked up:
recommends NOT taking a hard dependency on perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198's specific regrid for this kernel's first version
(compose later, once both are independently validated) — but perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198's occupancy fix is very likely
a prerequisite for this kernel's throughput win to materialize at all (section 2's "latency-bound"
warning). Practically: this kernel can be designed and written independently of perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198, but should
not be benchmarked/evaluated for go/no-go until perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198's occupancy fix is in, per the issue's own
reasoning about the 8-10x ceiling only being realized outside the latency-bound regime.
kernel, not a structural change this kernel depends on. No blocking relationship either direction.
ncunumbers (16.5% occupancy, 42 stallcycles/instruction) actually change post-perf(cuda): port Vulkan's tuned split-KV attention heuristic to replace fixed ATTN_KV_SPLIT=4 #197/perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198 before sinking implementation time into this
kernel — if perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198 alone gets decode attention to, say, 60-70% occupancy and comfortably
memory/compute-bound, the marginal win from also switching to tensor cores may be much smaller
than the "training-knowledge estimate" 8-10x this issue cites (which was explicitly flagged as
"not yet re-verified").
7. Implementation scope/complexity and riskiest unknown
Scope: Medium-large. The PTX plumbing (
ldmatrix/mma.synchelpers, QK/softmax/PV structure)is genuinely reusable from
attention_flash_mma.cuwith only the row-padding, KV-loop-bound, andpositionOffsetchanges described in section 2 — that part is closer to "port + adapt" than"design from scratch," which de-risks the mechanical implementation. What's NOT reusable /
genuinely new work: (a) the
CanUsegate and dispatch wiring for the decode call site(
CudaQwen3MoeHybridTransformerModel.ForwardFullAttnBody, currently hardcoded toLaunchAttentionF32), (b) reading the FP16 KV cache directly instead of through the F32 dequantstaging buffer (a real code path change, not just a kernel swap), (c) the entire precision
characterization work in section 4, which is the actual gating cost, not the kernel code itself.
Single riskiest technical unknown: whether the Q→FP16 cast precision loss that broke "Option B"
once already, on this exact model family, reproduces for a tensor-core (FP32-accumulated) kernel
at Bonsai's real (currently unconfirmed) headDim. Section 4.1's cheap host-side isolation check
should be the very first thing done — before any CUDA code — because it's a fast, low-cost way to
get an early signal on whether this entire design direction is viable for Bonsai-27B specifically,
or whether it needs to be scoped to a different, less precision-sensitive model family (or shipped
with BF16 inputs / a hybrid F32-Q/FP16-KV scheme instead of full FP16 inputs).
What should happen before any code is written
qwen35moe) actualheadDim— determines whetherattention_flash_mma.cu's fixedHEAD_DIM=64tiling can be reused directly or needsgeneralizing (Vulkan coopmat's
D_TILE-chunked approach as the fallback reference).known-failing hidden=32/headDim=8 Bonsai fixture, before writing any CUDA. This is the fastest
possible go/no-go signal for the whole design.
with real
ncu+ realdotnet run benchthroughput (not just "Est. Speedup") that decodeattention is no longer latency-bound before sinking implementation time into a kernel whose
entire value proposition depends on that being true.
attention_flash_mma.cuper section 2), wire it opt-in persection 5, and run the full Tier 1/Tier 2/real-generation validation from section 4 before
flipping any default.
🤖 Generated with Claude Code
jamesburton commentedon Jul 28, 2026
Precondition check result (before any kernel implementation): confirmed NOT met — recommend #199 stay parked
Per the coordinator's request, before writing any tensor-core kernel code I re-verified the actual
precondition this issue names for going ahead: "this kernel's ceiling is only realized once the
kernel is out of the latency-bound regime #198 targets." #198 merged (PR #201,
attention_f32_gqa_split_kv,opt-in via
DOTLLM_ATTN_GQA_SPLIT=1), but its own real-world A/B came back inconclusive/noise-leveland the
ncure-profile needed to confirm occupancy actually moved was never completed(
ERR_NVGPUCTRPERMlast session). This pass fixes that gap with realncu --set fulldata.Setup
prism-ml/Ternary-Bonsai-27B-gguf/Ternary-Bonsai-27B-Q2_0.gguf(7.17 GB) onto T5500into the HF hub cache (
hf download,HF_HUB_DISABLE_XET=1— the default Xet transfer stalledindefinitely at 0 bytes; classic HTTP download worked cleanly). Fresh worktree
issue/199-cuda-tensorcore-decode-attentionoffdev(b76a6aa, includes perf(cuda): port Vulkan's tuned split-KV attention heuristic to replace fixed ATTN_KV_SPLIT=4 #197/perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198/perf(cuda): combined GQA-group register-blocking + tuned split-KV attention (#197, #198) #201).native/build_ptx.bat+dotnet build -c Release, GPU-lock held for the whole profiling session.ncuitself works fine on this host now (v2025.1.1.0, no elevation needed this session — lastsession's
ERR_NVGPUCTRPERMdid not reproduce).--launch-skip/--launch-countcount every launchmatching
--kernel-name regex:attention_f32, including the initial prefill (seqQ=8, grid=192)and — important, cost me a couple of throwaway captures — the
--depthwarm-context indotllm benchis applied as one big batched forward (grid = depth * numHeads, e.g. 6000 fordepth=250), not per-token decode calls. Only the timed
-nsteps are trueseqQ==1decode.DOTLLM_ATTN_GQA_SPLIT's kernel also only engages onceseqKv >= AttentionGqaSplitMinSeqKv(default 256) — a shallow
-p 8 -n 12probe with no--depthnever crosses that gate andsilently profiles the plain-fallback kernel, which is what my first pass here accidentally did.
Used
-p 8 --depth 250 -n 12(seqKv 258-270) to get real engagement, confirmed via a cheapncu --metrics gpu__time_duration.sumsweep (single lightweight metric, no--set fullreplaycost) before spending a full profiled capture on it.
Results — same shape (Bonsai-27B, seqKv≈258-265), same host,
--set full, 8 launches eachattention_f32(baseline, unchanged)attention_f32_gqa_split_kv(#197/#198, opt-in)(24,1,1)= numHeads(4,3,1)= numKvHeads × kv_split=3 → 12 blocks(Shallow-depth,
seqKv≈8-20, baseline-only sanity capture reproduced the original pre-#197/#198numbers from
docs/CUDA.mdalmost exactly: grid=24, Waves/SM 0.14, Achieved Occupancy 16.6-16.8%,Compute 4.2-4.3%, Memory 8.6-8.7%, Warp Cycles/Instr 42.3-42.8 — confirms the baseline path is
genuinely untouched by #197/#198 and that this session's tooling/methodology reproduces the earlier
elevated-
ncufindings faithfully.)Reading
The per-instruction stall metric did improve roughly 2× (25.6 → 13.2 cycles/instruction) —
the GQA-group register-blocking design is doing what it was meant to do at the warp level. But
that improvement did not translate into either higher achieved occupancy (flat at ~16.6%) or a
shorter kernel (1.66-1.68× longer, not shorter). Two things are working against the intended
fix simultaneously:
numKvHeads × kv_split = 4 × 3 = 12blocks is half thealready-too-small 24-block baseline grid. Waves/SM went from bad (0.14) to worse (0.09).
being fewer, better-amortized K/V reads. This points at the cooperative-launch (
grid.sync())and cross-block partial-result combine overhead dominating wall time, not the QK/PV compute
itself — consistent with perf(cuda): GQA-group K/V-read amortization + more resident warps/block for decode attention #198's own doc noting split-KV's grid-sync+combine overhead "has little
sequential work left to amortize against" at shallow per-split KV depth (here: 258/3 ≈ 86 rows
per split, still fairly thin).
Net: the diagnosed root cause from the original elevated
ncupass — latency-bound, notcompute- or memory-bandwidth-bound, near-zero achieved occupancy from a badly underfilled grid —
is unchanged, and on the raw per-launch-duration axis, worse with #197/#198's fix active. The
explicit precondition for #199 ("out of the latency-bound regime") is not met. Tensor cores
raise compute throughput; compute throughput here is ~9% either way and was never the
bottleneck — a tensor-core rewrite would not address either of the two things actually broken above
(grid fill, cooperative-launch overhead), and would add the real precision risk this issue already
flagged as HIGH/needs-new-tolerance-characterization, for a kernel unlikely to show a measurable
win.
Recommendation
#199 stays parked, not implemented. No tensor-core kernel code was written this pass — this
was specifically the confirm-the-precondition step. Suggested next steps for whoever revisits
decode attention perf, in priority order:
numKvHeads × kv_split(12for Bonsai-27B) rather than staying ≥ the 24-block baseline looks like a tuning bug, not a
fundamental limit —
kv_splitshould probably be chosen to keep total blocks ≥ baseline grid,not computed independently of it. Worth a quick occupancy-heuristic fix + re-profile before
trying anything more invasive.
the K/V amortization, dominates at this depth — worth an
ncusource/instruction-level pass onattention_f32_gqa_split_kvspecifically (not just SoL-level metrics) to confirm before assumingthe split design itself is sound and just mistimed.
~16.6%/0.14 floor should a tensor-core kernel (perf(cuda): tensor-core (mma.sync FP16) decode attention kernel — port Vulkan's coopmat design #199) be reconsidered — and even then, per the
issue's own risk note, it should start as an opt-in precision-gated spike, not a default-path
replacement.
No code changes made. GPU-lock held only for the profiling session on T5500, released cleanly
(
scripts/gpu-lock.sh release issue199). Scratch worktreeE:\Development\dotLLM-worktrees\issue-199-cuda-tensorcore-decodeand the raw.ncu-repcapturesleft on T5500 (not committed — same convention as the
.perf-runs/ncu-2026-07-27/README.mdprecedent: this comment is the durable record) for anyone who wants to re-import them.
docs(cuda): correct the #199 premise-check with a real depth-512 re-p…
perf(cuda): remove redundant barrier in attention_f32_gqa_split_kv; h…
jamesburton commentedon Jul 30, 2026
Merged via PR #243 (30a48ed on dev), default ON. Composed tensor-core + GQA-group decode attention: 2-2.6x faster than baseline, 2-2.5x faster than the #197/#198 kernel itself, real generation-parity validated (pre-gate bit-identical, post-gate PPL improves -0.173%). Full writeup in docs/CUDA.md and [[prismml-bonsai-model]] memory. #200 remains open separately (low priority, waits on continuous batching).