The Occupancy Hypothesis: How One Kernel Parameter Unlocked 1.8× Decode Speedup on Blackwell

The Message

In message 12327 of a marathon optimization session targeting speculative decoding on NVIDIA RTX PRO 6000 Blackwell GPUs (sm_120 architecture), the assistant executed a single, decisive experiment. After restarting the SGLang service with a modified environment variable, it ran a context-length sweep benchmark and reported the following results:

READY ~390s
=== NSPLIT=64 perf (compare to NSPLIT=16: 4096->62, 16384->12.2, 65536->3.3) ===
 ctx_req prompt_tok   TTFT_s prefill_tok/s decode_tok/s gen_tok decode_wall
    4096       5698     1.91          2977         81.9      16        0.18
   16384      22779    10.73          2124         22.4      16        0.67
   65536      91028    91.66           993          5.0      16        2.99

The numbers tell a story that the assistant immediately compared against the previous baseline: at 4K context, decode throughput jumped from 62 to 81.9 tok/s (1.32×); at 16K, from 12.2 to 22.4 tok/s (1.84×); at 64K, from 3.3 to 5.0 tok/s (1.52×). All from changing a single parameter — NSPLIT — from 16 to 64. No code changes, no kernel rewrites, no memory defragmentation. Just a deeper understanding of how GPU occupancy works in a tensor-parallel regime.

The Road to This Experiment

To understand why this message matters, one must trace the reasoning that led to it. The preceding messages (12320–12326) document a classic debugging journey: the assistant had deployed a custom CUDA verify-attention kernel for speculative decoding with the Kimi K2.6 model, but was seeing abysmal decode throughput — 0.26 tok/s at 46K context length — with GPU DRAM utilization stuck at ~2% and tensor core utilization at 0%. The GPU appeared nearly idle, yet each decode step consumed ~192ms.

The assistant systematically eliminated hypotheses. First, it confirmed that CUDA graph replay was working correctly (the decode batch showed cuda graph: True), ruling out Python marshaling overhead as the culprit. Then it instrumented the _prepare_for_speculative_decoding phase and discovered it took only ~7ms total — the draft model forward pass, tree building, and mask preparation were negligible. The remaining ~185ms was the verify forward pass itself.

This was deeply puzzling. The verify kernel was supposed to be efficient — it had passed microbenchmarks with respectable throughput. But in the live service, it was running at 2% DRAM bandwidth utilization. The assistant initially suspected memory fragmentation (scattered KV page tables), but then realized the service had just been restarted, meaning the KV cache was freshly allocated and contiguous. Fragmentation wasn't the issue.

The Occupancy Insight

The breakthrough came when the assistant reconsidered the kernel's grid dimensions in the context of tensor parallelism. The Kimi K2.6 model uses TP8 (tensor parallelism across 8 GPUs), which means each rank handles only 8 attention heads instead of the full 64. The verify kernel's grid is defined as q_len × num_heads × batch_size × NSPLIT. With NSPLIT=16, the total grid was:

The Experiment

The message shows the assistant executing this hypothesis. It modified the systemd service file to set KDTREE_NSPLIT=64 as an environment variable (consumed by the kernel at launch time), disabled the profiler (KDTREE_PROFILE=0), and restarted the service. The bash script then waited up to 14 minutes for the service to become healthy, polling every 30 seconds with a curl request.

Once ready, it ran bench_context_decode.py with 16 generated tokens at three context lengths: 4096, 16384, and 65536. The benchmark measures prefill throughput (TTFT and tok/s), decode throughput (tok/s), and wall time. The results are unambiguous: decode throughput improved at every context length, with the biggest relative gain (1.84×) at 16K.

Why This Matters

This message is a masterclass in GPU kernel optimization reasoning. It demonstrates several important principles:

1. Microbenchmarks can mislead. The verify kernel had passed microbenchmarks with good performance, but those benchmarks used different grid dimensions (full 64 heads, higher occupancy) than the production TP8 regime. The kernel's performance was highly sensitive to the number of blocks in flight, and the microbenchmark didn't replicate the production grid geometry.

2. Occupancy is not just about SM utilization. High SM occupancy (as reported by nvidia-smi) doesn't guarantee high memory bandwidth utilization. The GPU can have all SMs occupied with stalled threads waiting on memory, achieving 99% SM occupancy but only 2% DRAM utilization. The key metric is memory-level parallelism — how many concurrent memory requests the GPU can sustain. More blocks means more independent memory accesses, which keeps the DRAM controllers busy.

3. Tensor parallelism changes the optimization landscape. TP8 reduces per-rank head count from 64 to 8, which shrinks the natural grid size by 8×. A kernel tuned for full-head-count inference can become severely occupancy-starved when running on a single TP rank. The assistant had to recognize that the kernel's design assumptions (many heads → many blocks) broke down in the TP regime.

4. The simplest fix is often the best. Rather than rewriting the kernel with vectorized memory loads, changing memory layouts, or implementing complex defragmentation strategies, the assistant identified that a single parameter — NSPLIT — controlled the tradeoff between per-block work and grid occupancy. Bumping it from 16 to 64 was a one-line environment variable change that required no recompilation, no code changes, and no service disruption.

The Broader Context

This experiment was part of a larger effort to deploy speculative decoding (DDTree) on Blackwell GPUs for the Kimi K2.6 model. The assistant had already built a custom sm_120 verify-attention kernel from scratch because all existing optimized MLA kernels (FlashMLA, cutlass-MLA, flashinfer-MLA) only supported sm_90a/sm_100a/sm_103a architectures — not the sm_120 found on consumer Blackwell GPUs like the RTX PRO 6000. The kernel used a KV-split flash-decode design with a partial+reduce approach to manage shared memory constraints.

The NSPLIT parameter controls how many splits each attention head's KV sequence is divided into. More splits means smaller per-block work units but more blocks overall. The optimal value depends on the GPU's SM count, the number of heads per rank, the batch size, and the query length. Finding it empirically, as the assistant did here, is far more reliable than guessing.

What This Message Creates

This message produces several forms of knowledge:

Conclusion

Message 12327 is a turning point in the optimization session. It transforms the verify kernel from a puzzling underperformer (2% DRAM utilization, 0.26 tok/s at 46K) into a demonstrably efficient component (5.0 tok/s at 64K, with clear scaling behavior). The 1.8× speedup at 16K context is particularly significant because it represents the regime where the kernel transitions from occupancy-bound to compute/memory-bound — the sweet spot for further optimization.

The assistant's reasoning process — profiling to isolate the bottleneck, recognizing the TP8 occupancy problem, formulating the NSPLIT hypothesis, and testing it with a clean experiment — exemplifies disciplined performance engineering. It's a reminder that GPU optimization isn't always about writing faster kernels; sometimes it's about understanding how the kernels you already have interact with the deployment configuration.