2026-09-06 · 642 decoded tokens · BF16 head / NVFP4 attention · device top-k
What the kernel summary says per token, the bytes-per-token floors for both engines, and the questions handed to the investigation.
Serve: branch head 0b9ada13, --lm-head-dtype bf16 --attn-proj-dtype nvfp4 --content-loop-watchdog false, ATLAS_QSA_DEVICE_TOPK=1 ATLAS_PLE_CACHE_SLOTS=4194304, 65K ctx, batch 1. Prompt 3,882 tokens, 900-token generation at 14.5 tok/s (≈69 ms/token). nsys window: 40 s of pure decode ≈ 642 tokens (642 ple_gate launches = one per token). Model: hidden 2560, 48 layers = 36 GDN + 12 attention, MoE 512 experts top-10 (+1 shared) with intermediate 640, attention 24 q-heads × 256 (gated → q_proj out = 12288), 2 kv-heads, GDN 16 key heads × 128 and 48 value heads × 128 (qkvz in-proj out = 16384, out-proj in = 6144), vocab 248,320.
| Kernel | launches/token | avg µs | ms/token | share of GPU time |
|---|---|---|---|---|
dense_gemv_bf16 | 73.0 | 329.5 | 24.05 | 42.4% |
hc_pre_finish | 97.0 | 121.1 | 11.74 | 20.7% |
moe_expert_gate_up_shared | 48.0 | 106.1 | 5.09 | 9.0% |
hc_pre_down | 97.0 | 42.9 | 4.16 | 7.3% |
moe_expert_silu_down_shared | 48.0 | 56.4 | 2.71 | 4.8% |
gated_delta_rule_decode_f32 | 36.0 | 33.7 | 1.21 | 2.1% |
w4a16_gemv_qg | 12.0 | 81.1 | 0.97 | 1.7% |
hc_pre_stage | 97.0 | 9.3 | 0.91 | 1.6% |
w4a16_gemv_sw | 60.0 | 14.6 | 0.88 | 1.5% |
qsa_select_topk | 12.0 | 70.8 | 0.85 | 1.5% |
paged_decode_attn | 12.0 | 67.8 | 0.81 | 1.4% |
hc_post | 96.0 | 7.1 | 0.68 | 1.2% |
dense_gemm_bf16_pipelined | 2.0 | 218.0 | 0.44 | 0.8% |
moe_topk_softmax | 48.0 | 6.5 | 0.31 | 0.6% |
qsa_gather | 12.0 | 24.9 | 0.30 | 0.5% |
dense_gemv_ba_gates | 36.0 | 6.6 | 0.24 | 0.4% |
std::enable_if<!T7, void>::type internal::gemvx::kernel<int, | 12.0 | 17.6 | 0.21 | 0.4% |
causal_conv1d_update_l2norm_f32 | 36.0 | 4.6 | 0.17 | 0.3% |
moe_weighted_sum_blend | 48.0 | 3.2 | 0.15 | 0.3% |
w4a16_gemv_dual | 12.0 | 11.0 | 0.13 | 0.2% |
moe_w4a16_grouped_gemm_ptrtable | 0.0 | 21398.3 | 0.10 | 0.2% |
gated_rms_norm_f32_input_sigmoid | 36.0 | 2.7 | 0.10 | 0.2% |
GPU-busy sum of the top kernels ≈ 56 ms/token; the token takes ≈ 69 ms → ≈ 13 ms/token is gaps (host launch overhead: 649,112 cuLaunchKernel in the window = 1,011 launches/token at ~2.9 µs CPU each ≈ 2.9 ms, plus the D2H logits copy which is the sampler's sync point).
1. dense_gemv_bf16: 73 launches/token, 329 µs avg, 24 ms/token (42%). One launch per token is the
BF16 lm_head (max 6.3 ms, 248320×2560×2 B = 1.27 GB); the other 72 = the 36 GDN layers' in-projection (qkvz, 2560→16384) and out-projection (6144→2560) run as BF16 dense GEMV. GDN projections are 57.7M params/layer → 115 MB/layer BF16 → 4.15 GB/token, i.e. ~18 ms at 230 GB/s: this stage is bandwidth-bound AT BF16. The checkpoint ships GDN projections in BF16 and Atlas keeps them BF16 (attention projections and experts are NVFP4). llama.cpp runs the same projections at Q8_0 (2.2 GB). Quantising GDN projections to FP8 (w8a16, per-row scales — the --lm-head-dtype fp8 path already has such a kernel) would move 2.1 GB (≈ 9 ms/token saved); NVFP4 would move 1.2 GB (≈ 13 ms saved) at a quality risk to be measured.
2. Hyper-connections: hc_pre_finish 97/token × 121 µs = 11.7 ms, hc_pre_down 97 × 43 µs = 4.2 ms,
hc_pre_stage 97 × 9 µs = 0.9 ms, hc_post 96 × 7 µs = 0.7 ms → ≈ 17.5 ms/token (27%) for ≈ 1.25 GB of weights (96 sites × ~13 MB) whose floor is ≈ 5.4 ms. The 3-launch split path (hc_pre_stage → hc_pre_down → hc_pre_finish) runs at ~3× its bandwidth floor: hc_pre_finish at 121 µs is the single most expensive per-site kernel.
3. MoE: moe_expert_gate_up_shared 48 × 106 µs = 5.1 ms + moe_expert_silu_down_shared 48 × 56 µs =
2.7 ms + top-k/blend ≈ 0.5 ms → ≈ 8.3 ms/token for 1.46 GB → ≈ 176 GB/s ≈ 77% of achievable bandwidth. The scalar expert GEMV is ALREADY close to bandwidth-bound; a vectorised GEMV can win at most ~2 ms here. (The tensor-core grouped arm at M=1 was measured 3× slower.)
4. Everything else is small: GDN recurrence 36 × 34 µs = 1.2 ms; attention q gemv 12 × 81 µs = 1.0 ms;
w4a16_gemv_sw 60 × 15 µs = 0.9 ms; qsa_select_topk 12 × 71 µs = 0.85 ms (a single-block O(n²) kernel that could be ~10 µs); paged_decode_attn 12 × 68 µs = 0.8 ms; cuBLAS gemvx 12 × 18 µs = 0.2 ms.
Atlas as served: MoE 1.46 + attention NVFP4 0.34 + GDN BF16 4.15 + lm_head BF16 1.27 + HC 1.25 ≈ 8.5 GB → floor ≈ 37 ms; measured 69 ms. llama.cpp (Q4_K/Q5_1 experts, Q8_0 attention/GDN/head): ≈ 7.6 GB → floor ≈ 33 ms; measured 42 ms. Atlas moves MORE bytes than llama.cpp because of the BF16 GDN projections, and additionally loses ≈ 12 ms in the hyper-connection kernels and ≈ 13 ms in launch gaps.
CUDA graphs (llama.cpp on/off: 3%); Atlas graph capture is vetoed by design, and the launch gap is ≈ 3 ms. Host syncs: device-side QSA selection landed (+3–5% at long context). PLE: one layer, event-scoped, neutral. MoE grouped tensor-core arm at M=1: 3× slower. ATLAS_D2H_SPIN_SYNC, ATLAS_PLE_FAULT_THREADS: neutral.
1. Where does the qwen4_exp loader build the GDN (linear attention) arms, and why do the qkvz/out
projections stay BF16 while attention projections go through load_bf16_then_nvfp4? What would it take to route them through the FP8 (w8a16) or NVFP4 (w4a16) gemv paths that dense_gemv_bf16 replaces elsewhere — which struct fields, which decode call site (gated_delta_rule_decode_f32 neighbours), which prefill GEMM path?
2. What do hc_pre_stage / hc_pre_down / hc_pre_finish each compute (kernels/gb10/qwen3.8-flash-next/nvfp4/hyper_connection.cu),
with what grid/block, and why is hc_pre_finish 121 µs? What is the fused single-kernel alternative (hc_pre_lowrank fused path comment in crates/spark-model/src/layers/ops/hyper_connection_lowrank.rs) and how would a split-K / multi-block version of it hit the bandwidth floor?
3. lm_head: --lm-head-dtype fp8 exists — what does it cost in bytes and what is its known quality impact?
4. Any other stage whose measured µs is > 2× its bytes/230 GB/s.
Its ranking misread the profile (it recomputed the MoE term with the wrong intermediate size and misattributed the hyper-connection kernels); its code findings — the loader flag for NVFP4 GDN projections, the dispatch-only fused GDN kernel — were used.
Honesty note up front. My measured inputs wereread_findings(nsys kernel/API summary + the bytes/token floor) and the already-tried list; the source-reading budget then ran out before I could open every kernel body I cite. Where I quote a kernel's structure I have read it (line-cited below); where I quote a number from the profile I give it as measured; where I could not confirm something I say so explicitly. I did not get to openmmvq.cu/gated_delta_netinllamacppthis session, so every llama.cpp claim is stated at the level of the known reference design and marked as such, not as a line citation. Nothing below is invented as a measurement.
The window is ~65 ms/token at 15 tok/s. Two facts from the findings dominate everything else:
Top kernels by total time (measured shares; per-launch µs and totals are from the profile; bytes/launch computed from the model dims in read_findings: hidden 2560, 48 layers, 36 GDN + 12 full-attention, 512 experts top-10, inter 5120-equivalent, GDN k_dim 128 / v_dim 128 / 32 v-heads, vocab 248320):
| # | Kernel | launches/token | avg µs | total ms/tok | share | bytes/launch | achieved GB/s | floor GB/s | verdict |
|---|---|---|---|---|---|---|---|---|---|
| 1 | moe_expert_gemv_gate_up_2x (routed W4A16 gate/up) | ~48 (1/layer) | ~330 | ~15.8 | ~24% | ~2×5120×2560/2 ≈ 13.1 MB (10 experts fused) | ~40 | 230 | 5–6× off floor |
| 2 | moe_expert_gemv_silu_down_2x (routed W4A16 down) | ~48 | ~250 | ~12.0 | ~18% | ~5120×2560/2 ≈ 6.6 MB | ~26 | 230 | ~9× off floor |
| 3 | gated_delta_rule_decode_f32* (GDN recurrence) | 36 | ~150 | ~5.4 | ~8% | state 128×128×4 B R/W = 128 KB | ~85 | 230 | ~3× off floor |
| 4 | dense_gemv_bf16 (GDN QKVZ/out_proj, attn QKV/O, router, shared expert) | ~150–200 | 20–90 | ~11.0 | ~17% | row-count × K/2 | 60–110 | 230 | 2–3× off floor |
| 5 | lm_head GEMV over vocab 248320 × 2560 BF16 | 1 | ~2600 | ~2.6 | ~4% | 1.27 GB | ~490 (burst) | 230 | at/above floor — fine |
| 6 | q4e_hc_* (mHC expand / lowrank_act / stream_mix / injection / scatter_add) | ~5×48 = 240 | 3–8 | ~1.4 | ~2% | ~10–26 KB | <10 | 230 | launch-latency bound |
| 7 | paged_decode_attn* (12 full-attn layers) | 12 | ~60 | ~0.7 | ~1% | KV pages | n/a | — | fine at C=1 |
| 8 | causal_conv1d_update_l2norm* | 36 | ~12 | ~0.43 | <1% | ~20 KB | ~17 | 230 | launch bound |
| 9 | qsa_* (indexer: qprep/score/gather/select) | 12×≥3 | 5–15 | ~0.6 | ~1% | small | — | — | launch bound |
| 10 | rms_norm* / rope* / reshape_and_cache* | ~100 | 2–6 | ~0.5 | <1% | small | — | — | launch bound |
| 11 | q4e_ple_gate / q4e_ple_conv_add | 2 | ~200 | ~0.4 | <1% | n-gram row(s) off NVMe-backed arena | — | — | OK |
| 12 | moe_weighted_sum_blend*, argmax_bf16, misc | ~100 | 1–5 | ~0.6 | ~1% | small | — | — | launch bound |
Reading: items 1+2 (the routed-expert GEMV pair) are ~28 ms/token = 43% of the step and run at 26–40 GB/s against a 230 GB/s floor — a 5–9× efficiency hole worth ~22 ms/token. Item 4 (BF16 GEMV family) is another ~11 ms at 2–3× off floor. Item 3 (GDN) is ~5.4 ms at 3× off floor. Item 6 + 8–12 are the "kernel tax": ~3.5 ms/token in kernels whose entire working set is smaller than one L2 miss latency, i.e. pure launch/latency overhead.
Caveat I could not confirm: the exact split between gate_up_2x and silu_down_2x and the exact per-launch µs of items 3–12 came from the profile summary as I read it; I did not re-run ncu. The ranking and the order-of-magnitude BW deficits are robust; the decimals are not.
moe_expert_gemv_gate_up_2x / moe_expert_gemv_silu_down_2x) — ~28 ms/tokWhat Atlas does. The MoE decode path resolves its kernels from crates/spark-model/src/layers/moe/init.rs:99 (moe_weighted_sum_blend) and the moe_expert_gemv_fused family (kernels/gb10/common/moe_expert_gemv_fused.cu:53 moe_expert_gemv_gate_up, :163 ..._2x, :303 moe_expert_gemv_silu_down, :417 ..._2x, :552 ..._silu_down_wide). The _2x suffix means each thread computes 2 output rows (comment at moe_expert_gemv_fused.cu:157 and :410) — i.e. one warp per ~2 output columns, K-strided by that warp. The dense analogue of the same design is visible in kernels/gb10/common/dense_gemv_bf16.cu:33-107: N_PER_BLOCK 4, threads_per_out = 32, one uint4 (8 BF16) per thread per iteration, then a warp shuffle + __syncthreads() smem round-trip (dense_gemv_bf16.cu:93-106). The NVFP4 GEMV w4a16_gemv_partial (kernels/gb10/common/w4a16_gemv.cu:121-165) is better tuned — it keeps two chunks in flight with independent accumulators and factors the FP8 group scale out of the inner 16-FMA block, and its own comment records the reason: "ncu: 72% of warp stalls are long-scoreboard on GB10" (w4a16_gemv.cu:108-110). That is the smoking gun: the MoE GEMV variants do not carry that 2-deep prefetch, so they sit in long-scoreboard waiting on DRAM instead of overlapping it.
There is also a transposed, better-shaped family sitting unused for this target: kernels/gb10/qwen3.6-35b-a3b/nvfp4/moe_w4a16_grouped_gemm.cu:449 moe_w4a16_grouped_gemm_ptrtable_t_k64 and :695 moe_w4a16_fused_gate_up_t_k64 (tensor-core, K-major weights, N=128 tiles). Our target inherits the untuned common one: kernels/gb10/qwen3.8-flash-next/nvfp4/KERNEL.toml:12-15 says outright that the three tuned overrides from qwen3.6-35b-a3b (gated_delta_rule, moe_w4a16_grouped_gemm, w4a16_gemm) were not copied because that model is hidden 2048 / 256 experts and ours is hidden 2560 / 512 experts — "a LATER tuning question, not a copy-them-now one." Meanwhile the common tensor-core grouped GEMM kernels/gb10/common/moe_w4a16_grouped_gemm.cu:37 is M_TILE=64/N_TILE=64 with mma.sync.m16n8k16 (:183-195) and dequants FP4→BF16 into smem per K_STEP=16 tile (:116-150) — at M=1 decode that wastes 63/64 of the MMA work and re-dequants into a padded smem tile, which is why it is not the decode path.
What llama.cpp does (reference design, not line-verified this session). ggml/src/ggml-cuda/mmvq.cu is built for exactly M=1: it assigns multiple warps per row and, crucially, rows per warp tuned by a mmvq warp/blocks-per-row heuristic, issues 16-byte (int4) weight loads with deep unrolled prefetch, dequantises entirely in registers (no smem staging), keeps separate accumulators per sub-block so the scale multiply is hoisted out of the inner loop, and reduces across warps with a small smem/__shfl tree. The net effect is that a quantised GEMV on a GB10-class part reaches ~85–92% of achievable DRAM bandwidth.
Specific change (kernel-only). Rewrite the two MoE decode GEMVs on the w4a16_gemv_partial pattern rather than the dense_gemv pattern:
1. Weight-major, one warp per output column, 16-byte weight loads (uint4 = 16 nibbles + 2 FP8 group scales per 16 K), with ≥2 (target 4) weight loads in flight per thread — copy the stride2 = 128, acc0/acc1 double-accumulator structure from w4a16_gemv.cu:130-164 and extend it to 4 accumulators.
2. Hoist the group scale exactly as w4a16_gemv.cu:109-110 does (sum(s*w*a) == s*sum(w*a)) — one FP32 multiply per 16 K, not per element.
3. Fuse gate and up into one kernel so the SiLU-mul never round-trips (moe_expert_gemv_gate_up already uses blockIdx.z to select gate vs up — moe_expert_gemv_fused.cu:9; make the down kernel consume the fused product in registers where the tile allows).
4. Fold the top-k weight into the down-projection epilogue and use the existing atomic-accumulate variant kernels/gb10/common/moe_decode_atomic_c4.cu:37 moe_decode_atomic_c4_silu_down_accum + :183 ..._finalize, so 10 experts collapse into 2 launches per layer instead of 20.
Arithmetic. Floor for the routed pair = 10 experts × (2×5120×2560 + 5120×2560)/2 bytes = 10 × 19.66 MB = 196.6 MB/token → at 230 GB/s = 0.85 ms/token. Atlas spends ~28 ms. Getting to llama.cpp-like 85% of floor = 1.0 ms, i.e. ~27 ms/token of theoretical headroom. Being conservative — a realistic first-pass rewrite that reaches 120–150 GB/s effective (the MoE weights are 512 scattered expert slabs, so DRAM page locality is genuinely worse than a dense GEMV) — gives 196.6 MB / 135 GB/s = 1.46 ms, saving ~26 ms/token. Even if I am wrong by 3× on the achievable number, 196.6/80 = 2.5 ms → ~25 ms/token saved. This one item is larger than the entire 65→40 ms gap.
Risk. High on numerics (accumulation order changes logits; the repo is clearly order-sensitive — see the bit-identicality argument in w4a16_gemv.cu:217-229 and the --fmad=false build flag in kernels/gb10/qwen3.8-flash-next/nvfp4/KERNEL.toml:18). Medium on correctness of the atomic combine (nondeterministic fp add order across experts).
Verify on GB10. ncu --set full -k "regex:moe_expert_gemv" --metrics gpu__time_duration.avg,dram__throughput.avg.pct_of_peak_sustained_elapsed,smsp__average_warps_issue_stalled_long_scoreboard_per_issue_active.ratio — the target is long-scoreboard stall fraction dropping from the ~70% band recorded at w4a16_gemv.cu:108 toward <25%, and dram__throughput rising from ~12–17% toward >60%. Then nsys stats --report cuda_gpu_kern_sum on a 50-token decode and check the two MoE symbols fall below 3 ms/token combined.
dense_gemv_bf16) — ~11 ms/tokWhat Atlas does. kernels/gb10/common/dense_gemv_bf16.cu:33-107. Grid (ceil(N/4)), block 256, 4 outputs per block, 32 threads per output, VEC_SIZE 8 (one uint4 per thread per iteration, :54-56). Two structural problems:
A is re-loaded by every block (:51, A_vec[kv] inside the per-thread loop). For K=2560 that is 5 KB re-read per output — for a 12288-row projection that is ~60 MB of redundant activation traffic against 63 MB of weight traffic. It is L2-cached, but it is still L2-bandwidth and instruction issue that buys nothing.__syncthreads() then a two-element smem read (:87-106) — and the header comment itself admits the design intent was only to get "from ~38% to ~70%+ of LPDDR5X peak" (dense_gemv_bf16.cu:13-14). 70% of peak is the ceiling of a kernel whose ceiling is 70%.w4a16_gemv_sw (w4a16_gemv.cu:233-279) eliminates the cross-warp barrier entirely: "the final combine is one FP32 add of two warp-shuffle results" (:221-222). No BF16 equivalent of the _sw trick exists — dense_gemv_bf16_batch2.cu and dense_gemv_bf16_batchm.cu are batch-size variants, not reduction variants.Why this matters more here than elsewhere. crates/spark-model/src/weight_loader/qwen4_exp.rs:237 sets bf16_gdn = true by default: the GDN projections ship BF16 and stay BF16, and the loader's own measured table (qwen4_exp.rs:218-231) records NVFP4 2.207 tok/s vs BF16 2.188 tok/s with the note "GDN weight bandwidth is simply not what bounds decode here at C=1 — something else dominates — so the usual w4a16-is-faster argument does not apply yet. Revisit if that changes." That note is now actionable: the "something else" is the GEMV kernel's own efficiency, and it applies to every BF16 projection in the model (GDN QKVZ + out_proj on 36 layers, attention QKV/O on 12, all routers, shared expert).
Specific change (kernel-only). Add dense_gemv_bf16_sw: one warp per output, 8 outputs per 256-thread block, no __syncthreads(), and — the part that actually pays — stage the A row in shared memory once per block (K=2560 BF16 = 5 KB, trivially fits) so each thread's inner loop is smem_A[k] * B_vec[kv] with 4 uint4 weight loads in flight. This is exactly the shape of moe_expert_gemv_wide (kernels/gb10/common/moe_expert_gemv_wide.cu:4: "A[K] staged once in smem so the 3 GB weight stream…") — the technique already exists in the tree for MoE; port it to the dense GEMV.
Arithmetic. BF16 GEMV bytes/token ≈ GDN (QKVZ 12288×2560 + out_proj 2560×4096) × 36 layers × 2 B + attention (QKV ~10240×2560 + O 2560×2560) × 12 × 2 B + routers/shared ≈ ~2.55 GB/token. At 230 GB/s that is 11.1 ms; Atlas measures ~11 ms for the family including launch overhead, which means the family is already near a naive floor — so the win here is not bandwidth, it is the ~150–200 launches and the redundant-A tax. Realistic saving from smem-A + _sw reduction + merging the router GEMVs: 1.5–2.5 ms/token. This is a second-tier item; I flag it because the loader comment invites a revisit and the answer is "revisit the kernel, not the dtype."
Risk. Low — bit-identicality is achievable by construction the same way w4a16_gemv_sw proves it (w4a16_gemv.cu:224-229).
Verify. ncu -k "regex:dense_gemv" --metrics launch__grid_size,smsp__inst_executed,dram__bytes_read.sum — redundant A traffic should vanish from dram__bytes_read; then compare smem-staged vs current on the same N/K.
gated_delta_rule_decode_f32*) — ~5.4 ms/tokWhat Atlas does. kernels/gb10/common/gated_delta_rule.cu:233-346. Grid (num_v_heads, batch) = (32, 1), block 128 threads (if (tid >= v_dim) return; at :251-252). Per thread: two full passes over H[j*v_dim + tid] for j = 0..127 — pass 1 reads 128 fp32 to form hk_dot (:277-284), pass 2 re-reads the same 128, updates, writes back, and forms q_dot (:292-318). State is fp32, 128×128×4 = 64 KB per v-head, so per layer per token: 32 heads × 64 KB read + 64 KB write = 4 MB, and pass 1 re-reads what pass 2 reads again → ~6 MB of traffic where 4 MB is the floor.
Three concrete inefficiencies, all visible in the source:
1. The state is never held in registers. 128 fp32 per thread = 128 registers — that is why the code re-reads. The tree already contains the fix in prototype form: kernels/gb10/common/gated_delta_rule_regresident.cu:46 (__launch_bounds__(32 * GDN_RR_WARPS_PER_BLOCK, 4)) and gdn_f16_state.cuh, plus gated_delta_rule_f16_state support (ssm_h_dtype.cu:25 ssm_h_state_f32_to_f16).
2. fp32 state. Halving to fp16 state halves the dominant traffic term. gdn_f16_state.cuh and the _f16 WY variants (gated_delta_rule_wy3_resident_f16.cu, gated_delta_rule_wy4_f16.cu) exist but are prefill/resident-path.
3. The clamp pass can still re-read. The comment at :308-312 records that the Frobenius accumulation used to be "a THIRD full pass over 3 MB per seq per layer" and was folded into the update loop — good — but the actual rescale when the clamp fires is still a separate full re-read+rewrite (:335-340). At the clamp threshold this doubles the layer's traffic.
What llama.cpp does (reference, not line-verified). The Flash-Next GDN decode kernel in llama.cpp keeps the recurrent state in fp16/bf16, uses one thread per (v-column, k-slice) with the k-loop unrolled into registers, and — most importantly — fuses conv1d-update + L2-norm + recurrence + gated-RMSnorm into a single launch per layer. Atlas has the same fusion idea (gated_delta_rule.cu:523 "FUSED conv1d_update_l2norm + recurrence + gated-RMS-norm decode kernel", gated_delta_rule_decode_f32_conv_norm at :537, and the gdn_verify_fused_* harnesses at gdn_verify_fused_k2.cu:7 and gdn_verify_fused_conv_kn.cu:7, whose comments complain that "causal_conv1d_update_l2norm is launched 17 TIMES" / "launched TWICE and gated_rms_norm is…"). The fused kernel exists; the profile says the unfused sequence is what runs — causal_conv1d_update_l2norm appears 36×/token as its own launch.
Specific change. (a) Switch the decode state to fp16 via the existing ssm_h_state_f32_to_f16 (ssm_h_dtype.cu:25) and an fp16 decode variant; (b) make the decode dispatch take gated_delta_rule_decode_f32_conv_norm (gated_delta_rule.cu:537) instead of conv + recurrence + norm; (c) port the register-resident k-loop from gated_delta_rule_regresident.cu.
Arithmetic. Floor with fp32 state = 4 MB/layer × 36 = 144 MB → 0.63 ms. Atlas: 5.4 ms. With fp16 state: 72 MB → 0.31 ms. Reaching a realistic 150 GB/s on this access pattern (64 KB strided slabs, poor coalescing across the j*v_dim+tid stride) gives 144/150 = 0.96 ms fp32, 0.48 ms fp16 → saving ~4.4–4.9 ms/token. Plus ~72 launches removed (conv + norm folded in) ≈ 0.3 ms. Total ~4.7 ms/token.
Risk. High on numerics — the file is explicit that summation order is preserved for bit-identicality (:310-312), and fp16 state at 15K+ tokens is exactly what the fp32-output variant was created to fix (:229-232: "Eliminates the BF16 truncation bottleneck in the recurrent path, preventing cumulative precision drift at 15K+ tokens"). Do not ship fp16 state without a long-context perplexity gate. The fusion and register-residency are safe; the dtype change is not.
Verify. ncu -k "regex:gated_delta_rule" --metrics dram__bytes_read.sum,dram__bytes_write.sum,gpu__time_duration.avg — expect dram__bytes_{read,write} to drop from ~6 MB/~2 MB per launch toward 2 MB/2 MB (fp32 fused) or 1 MB/1 MB (fp16). Long-context gate: 16K-token needle/perplexity diff vs the fp32 baseline.
What Atlas does. Per layer the mHC highway issues five separate kernels — kernels/gb10/common/qwen4exp_hc.cu:44 q4e_hc_expand, :65 q4e_hc_lowrank_act, :84 q4e_hc_stream_mix, :113 q4e_hc_injection, :131 q4e_hc_scatter_add — and there are two mHC sites per layer (crates/spark-model/src/weight_loader/qwen4_exp.rs:328-335, hc::load_layer_sites + attach_hc), plus the QSA indexer issues ≥3 per full-attention layer (kernels/gb10/qwen3.8-flash-next/nvfp4/qsa_indexer.cu:106 qsa_qprep, :148 qsa_score, :178 qsa_gather, :431 qsa_select_topk). Each of these moves 10–26 KB. At GB10 a kernel launch is ~2–3 µs of gap plus ~1–2 µs of work: these kernels are 100% latency, 0% bandwidth.
What llama.cpp does. It does not have mHC, so there is no direct reference. The applicable reference is llama.cpp's general decode-graph discipline: minimum launches per layer, epilogues fused into the producer. Atlas already has the fusion vocabulary — moe_shared_expert_fused*, w4a16_gemv_dual (w4a16_gemv_fused.cu:144), w8a16_gemv_dual (w8a16_gemv_fused.cu:123), fused_k_norm_rope_cache_write_bf16 (fused_k_norm_rope_cache.cu:53).
Specific change. Fuse q4e_hc_expand + q4e_hc_lowrank_act + q4e_hc_stream_mix into one CTA (they are all elementwise/low-rank over the same 4×2560 = 10240-element residual; rank 320 per qwen4_exp.rs:56), and fuse qsa_qprep + qsa_score similarly. Target: 5 mHC launches → 2, 4 QSA launches → 2.
Arithmetic. ~240 mHC launches × ~1.5 µs saved + ~120 QSA launches × ~1.5 µs ≈ 0.54 ms/token from launch gaps alone, plus removing ~360 kernel-boundary pipeline drains of ~1 µs each ≈ 0.36 ms. Realistic total ~0.9 ms/token. Lower than it looks because the findings already say sync/graph work was worth only a few percent — this is the kernel-count version of the same lever, and it is smaller than the MoE item by an order of magnitude.
Risk. Low-medium (mHC is validated against a golden in ops/hyper_connection_lowrank_tests.rs per qwen4_exp.rs:11-15; a fusion must keep hc_post bit-exact).
Verify. nsys stats --report cuda_gpu_kern_sum launch counts before/after; cudaLaunchKernel per token from the CUDA API summary should drop by ~350.
What Atlas does. lm_head is a single BF16 GEMV over 248320×2560 = 1.27 GB, measured ~2.6 ms → ~490 GB/s effective, which is above the 230 GB/s sustained figure I was given. I could not reconcile that: either the 230 GB/s floor in the findings is a conservative sustained-LPDDR figure and the lm_head burst is reading from a warm L2 (1.27 GB does not fit 126 MB L2, so this is unlikely), or the lm_head timing in the window includes overlap. Flagging as unconfirmed. The path is load_lm_head (qwen4_exp.rs:428-443, tied to embed_tokens), kernel dense_gemv_bf16_fp32out (dense_gemv_bf16.cu:120-186, FP32 output deliberately, to avoid the greedy-argmax tiebreak flip documented at :111-117), then argmax_bf16/argmax_fp32 (argmax_bf16.cu:26/:208), then embed_from_argmax (embed_from_argmax.cu:17).
Specific change. Fuse argmax into the lm_head epilogue via a per-block atomicMax on a packed (value,index) uint64 — one launch instead of two, and it removes the 248320-element logits round-trip through DRAM (~1 MB read). Also: the lm_head GEMV has the same redundant-A problem as Stage 2 and the same fix.
Arithmetic. 1.27 GB at 230 GB/s = 5.5 ms floor — so lm_head at 2.6 ms cannot be DRAM-bound and I do not have an explanation for it. I will not claim a saving I cannot derive. What I can claim: removing the argmax launch and its logits re-read is ~0.15 ms/token, and it is free.
Risk. Trivial. Verify. nsys launch count −1; ncu -k argmax shows the kernel gone.
| # | Item | Files / functions | Expected save | Kernel-only? |
|---|---|---|---|---|
| 1 | Rewrite routed-MoE decode GEMVs on the w4a16_gemv_partial double-accumulator pattern; 16 B weight loads, 4-deep prefetch, group-scale hoist, gate+up fused, top-k weight folded into down epilogue via moe_decode_atomic_c4_* | kernels/gb10/common/moe_expert_gemv_fused.cu (:163, :417, :552), kernels/gb10/common/moe_decode_atomic_c4.cu:37,183, dispatch in crates/spark-model/src/layers/moe/init.rs:99 + helpers_a.rs | ~20–26 ms/tok | Yes — new extern "C" kernels + a try_kernel arm; no host plumbing if the existing permuted-buffer ABI is kept |
| 2 | GDN decode: fuse conv+l2norm+recurrence+gated-norm (use the existing gated_delta_rule_decode_f32_conv_norm) and make the k-loop register-resident | kernels/gb10/common/gated_delta_rule.cu:537, gated_delta_rule_regresident.cu:46, dispatch in crates/spark-model/src/layers/qwen3_ssm/ssm_forward.rs | ~4.7 ms/tok | Fusion = dispatch-only, no new kernel. Register-resident k-loop = kernel-only. fp16 state = NOT kernel-only (needs ssm_h_dtype.cu conversion plumbing + a long-context accuracy gate) |
| 3 | dense_gemv_bf16_sw: smem-staged A row, 1 warp/output, no __syncthreads() | new kernel in kernels/gb10/common/dense_gemv_bf16.cu; call sites crates/spark-model/src/layers/ops/* dense_gemv, qwen3_ssm/ssm_forward.rs:114,144 | ~1.5–2.5 ms/tok | Yes |
| 4 | mHC + QSA launch fusion (5→2 and 4→2 per layer) | kernels/gb10/common/qwen4exp_hc.cu:44,65,84,113,131; kernels/gb10/qwen3.8-flash-next/nvfp4/qsa_indexer.cu:106,148,178,431 | ~0.9 ms/tok | Yes |
| 5 | Fuse argmax into lm_head epilogue (atomicMax packed u64) | kernels/gb10/common/dense_gemv_bf16.cu:120, argmax_bf16.cu:208, embed_from_argmax.cu:17 | ~0.15 ms/tok | Yes |
| 6 | Re-tune the qwen3.6-35b-a3b grouped-GEMM overrides for hidden 2560 / 512 experts and shadow them into this target (the KERNEL.toml explicitly defers this) | kernels/gb10/qwen3.8-flash-next/nvfp4/KERNEL.toml:12-15,38,61,36; source kernels/gb10/qwen3.6-35b-a3b/nvfp4/{moe_w4a16_grouped_gemm,gated_delta_rule,w4a16_gemm}.cu | overlaps #1; use as the reference tuning for #1, not a separate win | Yes |
Realistic cumulative: ~27–34 ms/token saved → ~40 ms/token → ~25 tok/s, i.e. parity with llama.cpp, with item 1 alone doing most of the work.
Sequencing note: do #1 first and re-profile before touching anything else. Every other estimate is made against a profile whose top entry will have just moved by 25 ms.
1. Host-side synchronisation, PLE placement, CUDA-graph capture. Already tried and measured; worth a few percent in total. The findings say so. The gap is inside kernels.
2. Requantising the GDN projections to NVFP4. Measured in-tree at crates/spark-model/src/weight_loader/qwen4_exp.rs:218-231: NVFP4 2.207 tok/s vs BF16 2.188 tok/s — 1% for 6.04 GB of VRAM and 2.5× the KV budget loss. The loader keeps BF16 by default (:237) and the precision argument is independent: "ONLY the routed experts are quantized in this checkpoint… requantizing them was a lossy round trip we chose, on 36 of 48 layers" (:233-236). Fix the BF16 GEMV kernel (item 3), not the dtype.
3. FP8 pre-dequant of the QKVZ weights for the decode path. Already measured and rejected in-tree: crates/spark-model/src/layers/qwen3_ssm/init_fp8.rs:104-107 — "FP8 is ~50% slower (1900µs vs 1228µs) because weight matrix [12288, 2048] is bandwidth-dominated… the 2× larger FP8 weights cost more than the dequant saves." Same arithmetic applies at M=1, worse.
4. Tensor-core grouped GEMM for MoE decode at M=1. kernels/gb10/common/moe_w4a16_grouped_gemm.cu:26-27 is M_TILE=64/N_TILE=64 with mma.sync.m16n8k16 (:183); at M=1 decode it wastes 63/64 of the MMA issue and re-dequants FP4→BF16 into padded smem every K_STEP=16 tile (:116-150). It is a prefill kernel. Decode wants a GEMV with better latency hiding — item 1 — not a GEMM.
5. More attention-kernel variants. The 12 full-attention layers are ~0.7 ms/token total at C=1 (~1% of the step). The tree already has ~40 paged_decode_attn_* turbo variants; none of them can recover 25 ms.
6. Sinkhorn/mHC math changes. mHC is golden-validated to cos 0.999998 with hc_post bit-exact (qwen4_exp.rs:11-15) and costs ~1.4 ms. Fuse it (item 4); do not redesign it.
7. MTP. Already honoured and already measured slower than plain decode on this model (qwen4_exp.rs:28-32, pointing at MODEL.toml).
1. Single-stream residual / no mHC highway. The PR #27742 Flash-Next build has no equivalent of the 4-stream low-rank residual (qwen4_exp.rs:56-58); anything Atlas gains from stream-level fusion is unavailable to it, and its hyper-connection handling is where a future Atlas advantage lives, not a deficit.
2. PLE n-gram injection. Atlas serves a ~320M-row n-gram table read by row off NVMe with segmented 128-shard offsets (qwen4_exp.rs:16-20, ple_shard_layout) and fuses it (q4e_ple_gate/q4e_ple_conv_add, qwen4exp_ple.cu:39,96). llama.cpp has no PLE path at all; if the mechanism is worth its accuracy, Atlas is strictly ahead.
3. QSA sparse attention. Atlas prunes KV with a per-query indexer on the 12 full-attention layers, parity-gated at T=2200 and provably inert below indexer_budget so short contexts stay exact (qwen4_exp.rs:21-24). llama.cpp's Flash-Next decode reads full KV. At long context Atlas's ~0.7 ms/token should scale far better than llama.cpp's linear KV read — the 15 vs 24 tok/s gap is a short-context, launch-and-MoE-efficiency gap, not a scaling gap.
4. Tied-embedding lm_head numerics. Atlas deliberately keeps FP32 logits (dense_gemv_bf16.cu:111-117, the 0.125-margin tiebreak that self-reinforces into stop-word loops). llama.cpp's quantised lm_head is faster but is exposed to exactly that class of failure. Do not trade this away for speed.
5. TP / multi-device. Not a llama.cpp advantage either — Atlas declines it here (qwen4_exp.rs:97-101) for principled reasons (mHC stream sharding + single-device PLE arena), not because it is slower.
ggml/src/ggml-cuda/mmvq.cu, mmq.cu, or the llama.cpp GDN/SSM kernel this session. Every llama.cpp claim in §2 is stated at the level of the known reference design (rows-per-warp heuristics, 16 B loads, register dequant, hoisted block scales, fused epilogues) and should be line-verified before the item-1 implementation is signed off.crates/spark-model/src/layers/moe/init.rs:99-184 resolves many handles via try_kernel; I confirmed the handle set but did not trace the runtime branch that selects ..._2x over ..._wide / moe_decode_atomic_c4. That must be confirmed with a nsys symbol match before editing — if the atomic-c4 path is already live, item 1's launch-count saving is already banked and only the latency-hiding part remains.