Validating the Fix: Pushing CUDA Graph Capture to Its Limits on Blackwell

Introduction

In the high-stakes world of production ML inference debugging, the most dangerous moment is not when a bug is discovered—it is immediately after a fix is applied, when the temptation to declare victory can overwhelm the discipline of thorough validation. Message [msg 13479] captures this exact moment in the engineering journey of deploying DeepSeek-V4-Flash-NVFP4 on NVIDIA Blackwell GPUs with SGLang. Having just root-caused and fixed a persistent high-concurrency corruption bug—a multi-stream-overlap race condition in CUDA-graph-captured decode that manifested only with bf16 index keys—the assistant now faces the critical task of proving the fix holds under the most aggressive operating conditions possible.

This message is not about discovery. It is about confirmation. It is about deliberately stress-testing a system at its newly expanded operating limits to ensure that the cure is not worse than the disease, and that no latent issues lurk in the previously unexplored territory of high-batch-size captured execution.

The Immediate Preceding Context

To understand message [msg 13479], one must understand what came immediately before it. The assistant had spent dozens of messages methodically debugging a corruption that caused tool-calling LLM agents to "lose the plot" after 3–4 conversation turns—producing nonsensical outputs, hallucinating tool calls, and failing to complete tasks. The root cause, identified through an exhaustive process of hypothesis elimination, canary instrumentation, and A/B testing, was a race condition in SGLang's multi-stream-overlap optimization. When CUDA graph capture was enabled on the decode worker, the C4 sparse indexer launched on an alternate CUDA stream. During graph replay, this alt-stream indexer's bf16 read-path transient intermediates would alias or race with main-stream tensors in the shared captured-graph memory pool, corrupting selection logits and causing the attention mechanism to select wrong tokens.

The fix was elegantly simple: set SGLANG_OPT_USE_MULTI_STREAM_OVERLAP=0 on the decode worker. No code changes, no patches to the SGLang source. Just an environment variable.

With the fix confirmed at the default --cuda-graph-max-bs 32 (meaning only batches up to 32 concurrent requests would run under the captured graph; larger batches fell back to eager execution), the user asked to bump this limit to 96. The assistant complied, updating the serve script, restarting the decode worker, and observing a clean capture with batch size buckets now scaling up to [1,2,4,8,12,16,24,32,40,48,56,64,72,80,88,96]—using only 0.57 GB of GPU memory with 15 GB still free.

The initial validation run at the new limit was encouraging but incomplete. A 96-session, 3-round test completed with 0% corruption, but 49 of the 96 sessions timed out. A 60-session, 4-round test also showed 0% corruption with no errors. However, the peak captured batch size observed during these multi-turn agentic workloads was only 20—far below the new 96-batch ceiling. The high-batch-size captured path remained untested.

This is where message [msg 13479] begins.## The Message: A Deliberate Stress Test

The subject message opens with the assistant's reasoning, which reveals a nuanced understanding of the test results:

Looking at the test results across different concurrency levels, the 96×3 configuration hit timeouts due to overwhelming the decode throughput, while the 60×4 setup had no corruption. The peak concurrent sessions captured was only 20 even with CUDA graphs enabled, suggesting the decode pipeline couldn't sustain higher concurrency without timing out.

This reasoning demonstrates an important analytical skill: distinguishing between different failure modes. The 49 errors in the 96-session run were timeouts, not corruption or memory leaks. The assistant explicitly categorizes them: "the 49 errors in the 96-session run are throughput timeouts, not leaks." This distinction is critical because it tells the engineer that the system is not breaking—it is merely being overwhelmed by a workload that exceeds its capacity. The fix is holding; the system is just under-resourced for that particular concurrency level.

The assistant then identifies a gap in the validation: "The peak captured batch size only reached 20-32 during the multi-turn workload, so the high-bs path wasn't heavily stressed there." This is a crucial admission. The multi-turn agentic workload, with its interleaved tool calls and variable-length responses, never actually pushed the decode batch size to the new 96-batch limit. The captured path for batches 33–96 had been enabled but never exercised. A validation that doesn't reach the new operating regime is not a validation at all.

The assistant's response is to design a targeted experiment: a pure-decode throughput benchmark at C=96 concurrency with 192 total requests and 200 max tokens each. This workload, unlike the multi-turn agentic test, would generate sustained decode pressure and push the captured batch size toward its new ceiling. The command issued is:

/root/venv_sglang211/bin/python /root/bench_tput.py --conc 96 --n 192 --max-tokens 200

Followed by log inspection to confirm the peak captured batch size and verify that no eager fallback occurred (which would indicate batches exceeding the captured range).

The Results: Clean Across the Board

The results are unambiguous and impressive:

C=96  n=192 max_tokens=200 | agg=760.0 tok/s | per-req~811.9 tok/s | p50 lat=22.33s | toks=38400 errs=0 wall=50.5s
=== peak captured (cuda graph: True) bs during bench ===
#running-req: 96
=== any eager (>96) fallback? ===
0

Every metric tells a positive story:

  1. Peak captured batch size of 96: The benchmark successfully exercised the full captured range. The system ran 96 concurrent decode requests under CUDA graph capture, confirming that the fix works at the absolute limit of the new configuration.
  2. Zero eager fallback: The grep -c "cuda graph: False" returned 0, meaning every single decode batch ran under the captured graph. No fallback to eager execution was necessary. This confirms that the memory pool and capture infrastructure can handle the full batch range without degradation.
  3. 760 tok/s aggregate throughput: At C=96 concurrency, the system delivered 760 tokens per second aggregate, with per-request throughput of ~812 tok/s. This is a healthy improvement from the earlier C=64 measurement of 711 tok/s, demonstrating good scaling.
  4. Zero errors: All 38,400 tokens were generated without a single error. No corruption, no OOM, no CUDA errors, no timeouts.
  5. 50.5-second wall time: The benchmark completed in under a minute, indicating the system is not bottlenecked by memory or capture overhead at high batch sizes.

What This Message Reveals About the Engineering Process

Message [msg 13479] is a masterclass in validation methodology. Several principles are on display.

Principle 1: Test at the Limits

The most common mistake in debugging is to confirm a fix only at the conditions where the bug was originally observed. The assistant could have stopped after the 60×4 test showed 0% corruption. But that test never pushed the batch size beyond 20. The bug was fixed at bs≤32, but the configuration had been expanded to bs≤96. A responsible engineer must ask: "Does the fix hold at the new limits?" The assistant asked that question and answered it empirically.

Principle 2: Distinguish Failure Modes

The 49 timeouts in the 96×3 agentic test could have been alarming. A less careful engineer might have attributed them to the fix or the configuration change. The assistant correctly identified them as throughput capacity issues—the agentic workload with its tool-calling rounds and context accumulation simply demanded more decode throughput than the system could provide at that concurrency. This is a scaling limit, not a correctness bug. The pure-decode benchmark, which isolates the decode path from agentic overhead, confirmed this interpretation by completing without errors.

Principle 3: Verify the Mechanism, Not Just the Outcome

The assistant doesn't just check that "it works." The assistant checks that the mechanism works as intended. The log inspection for peak captured batch size (96) and the absence of eager fallback (0) confirm that the system is actually using the captured graph at high batch sizes. This is a deeper form of validation—it confirms that the configuration change is having its intended effect on system behavior, not just that the output looks correct.

Principle 4: Use the Right Workload for the Right Question

The multi-turn agentic workload is good for testing corruption because it exercises the full agentic pipeline—tool calls, context accumulation, variable-length responses. But it's a poor workload for stressing decode batch size because the agentic interleaving limits concurrency. The pure-decode benchmark is the right tool for pushing batch size. The assistant understood this distinction and selected the appropriate workload for the validation question at hand.## Assumptions and Their Validity

Every engineering decision rests on assumptions. Message [msg 13479] is no exception, and examining its assumptions reveals the quality of the assistant's reasoning.

Assumption 1: The fix (multi-stream-overlap=0) is sufficient for all batch sizes. The assistant assumes that because the race condition was a structural property of the multi-stream-overlap mechanism, disabling it eliminates the corruption regardless of batch size. This is a reasonable assumption—the race was not batch-size-dependent in its mechanism—but it is an assumption nonetheless. The validation at bs=96 confirms it empirically.

Assumption 2: A pure-decode benchmark is a valid proxy for agentic workload correctness at high batch sizes. The assistant uses the C=96 benchmark to verify that the captured path works at high bs, but this benchmark does not exercise the multi-turn tool-calling logic that originally triggered the corruption. The earlier agentic tests (96×3, 60×4) already confirmed 0% corruption at lower peak batch sizes. The pure-decode benchmark fills the gap of "does the captured path work at bs=96?" but not "does the agentic pipeline remain uncorrupted at bs=96?" The assistant implicitly assumes that if the captured path is clean and the agentic pipeline is clean at lower bs, then the combination at higher bs will also be clean. This is a reasonable engineering judgment, but it is an assumption worth noting.

Assumption 3: The throughput improvement from C=64 to C=96 is meaningful. The assistant reports C=64 at 711 tok/s and C=96 at 760 tok/s. The improvement is real but modest—only ~7% for a 50% increase in concurrency. This suggests the system is hitting a different bottleneck (likely memory bandwidth or compute capacity per step) rather than being concurrency-limited. The assistant does not comment on this, but the data is presented transparently for the user to interpret.

Assumption 4: Zero eager fallback is a positive indicator. The assistant checks for eager fallback and reports 0 instances. This is indeed positive—it means the captured graph is handling the full workload. However, it's worth noting that eager fallback is not inherently bad; it's a graceful degradation path. The absence of fallback simply confirms that the capture infrastructure is adequately sized.

The Thinking Process: What the Reasoning Reveals

The assistant's reasoning section in message [msg 13479] is unusually candid about the limitations of the initial validation. Rather than declaring victory after the 0% corruption results, the assistant pauses to consider what was not tested:

"The peak concurrent sessions captured was only 20 even with CUDA graphs enabled, suggesting the decode pipeline couldn't sustain higher concurrency without timing out."

This observation shows the assistant reading the data critically, not just for pass/fail but for what it reveals about system behavior. The peak captured batch size of 20, far below the 96-batch ceiling, indicates that the multi-turn workload never actually stressed the new captured range. The assistant could have ignored this and reported "0% corruption at max-bs 96" as a headline. Instead, the assistant identified the gap and designed a targeted experiment to close it.

The reasoning also shows a clear understanding of the relationship between workload characteristics and system behavior. The assistant distinguishes between the agentic workload (which interleaves tool calls and limits decode concurrency) and the pure-decode benchmark (which sustains decode pressure). This is not a trivial distinction—it requires understanding how the SGLang scheduler manages request concurrency, how agentic tool calls introduce idle periods in the decode pipeline, and how the CUDA graph capture interacts with batch size.

Input Knowledge Required to Understand This Message

To fully grasp message [msg 13479], a reader needs knowledge in several domains:

  1. CUDA graph capture mechanics: Understanding that SGLang captures CUDA graphs for common batch sizes to avoid kernel launch overhead, and that captured graphs have a fixed memory pool that cannot be dynamically resized.
  2. SGLang's multi-stream-overlap optimization: The feature that launches certain kernels (like the C4 sparse indexer) on alternate CUDA streams for potential overlap, and how this interacts with graph capture.
  3. The bf16 corruption bug: The specific race condition that was fixed, and why it manifested only with bf16 index keys under captured execution.
  4. PD (prefill-decode) disaggregation architecture: Understanding that the system has separate prefill and decode workers, and that the decode worker is the one running the captured graph.
  5. Agentic workload patterns: How multi-turn tool-calling LLM usage creates variable decode concurrency, with bursts of decode activity followed by idle periods during tool execution.
  6. Throughput benchmarking methodology: Understanding the difference between aggregate throughput (tok/s across all requests) and per-request throughput, and how concurrency affects both.

Output Knowledge Created

This message creates several pieces of actionable knowledge:

  1. The fix holds at max-bs 96: The multi-stream-overlap=0 fix is validated at the highest batch size the system is configured to capture. This gives confidence to push the configuration further if needed.
  2. The system can sustain 760 tok/s at C=96: A concrete throughput baseline for the new configuration, useful for capacity planning and comparison with future optimizations.
  3. No eager fallback at high concurrency: The capture infrastructure has adequate memory headroom (0.57 GB used, 15 GB free) and can handle the full batch range without degradation.
  4. The C=64 throughput improved from 657 to 711 tok/s: This ~8% improvement from the earlier measurement (see [msg 13470]) may be due to the larger captured range reducing eager fallback for intermediate batch sizes, or simply variance in benchmark conditions.
  5. A validated methodology for testing configuration changes: The approach of testing with both agentic workloads (for correctness) and pure-decode benchmarks (for stress-testing the captured path) is itself a reusable pattern.