Skip to content

cuda: raise CUDA graph cache cap from 64 to 512 for tensor parallel - #232

Open
angliubc wants to merge 1 commit into
unslothai:mtp/qwen4exp-nextnfrom
angliubc:fix/cuda-graph-cache-cap-tp
Open

angliubc wants to merge 1 commit into
unslothai:mtp/qwen4exp-nextnfrom
angliubc:fix/cuda-graph-cache-cap-tp

Conversation

@angliubc

Copy link
Copy Markdown

Summary

-sm tensor (meta backend) never captures CUDA graphs in decode when a model produces more than 64 subgraphs per device, because max_cuda_graphs = 64 makes the LRU eviction run on every token, permanently resetting graph warmup.

  • The meta backend dispatches one subgraph per AllReduce split point to every simple backend. For qwen4exp (48 layers, 2 allreduces/layer) that is ~128 subgraphs per device, each keyed separately in cuda_graphs.
  • With the cap at 64, every token's inserts evict the previous token's entries → graph->uid never matches → warmup never completes → zero cudaGraphLaunch, fully eager decode (~14.6k cudaLaunchKernel per token per device ≈ 70 ms of CPU launch overhead).
  • Raise the cap to 512. The existing 10 s idle sweep still bounds the map in steady state.

Measurements

8× V100-SXM2-32GB (NVLink), Qwen3.8-Flash-Next UD-Q4_K_XL, llama-bench -sm tensor -ngl 999 -p 0 -n 64 -r 3:

config before after
TP4 tg64 6.77 t/s 44.41 ± 6.26 t/s
TP4 tg512 — 47.93 t/s (no leak over long runs)
TP8 tg64 5.50 t/s 30.37 ± 5.86 t/s

nsys (TP4, one tg64 run): cudaGraphLaunch 0 → 48,888, cudaLaunchKernel 2.96M → 118k, 1552 graph captures. Layer-split mode needs only one entry per device and is unaffected. Output correctness verified (greedy, temp 0: arithmetic, counting, generation all correct).

The capture storm also disappears: with 128 keys cycling through a 64-entry map, each token re-instantiated 64+ graphs; after the fix, captures happen once and cudaGraphExecUpdate replays them.

Notes

  • Memory cost per entry is one node_properties per graph node (ggml_tensor + src ptr/ne/nb), so 512 entries × ~1000-node subgraphs is tens of MB — negligible next to the captured graphs themselves.
  • I picked 512 rather than unbounded to keep a hard ceiling; happy to tune (256/1024) or switch to env override if preferred.

🤖 Generated with Claude Code

The meta backend (-sm tensor) dispatches one subgraph per AllReduce split
point to every simple backend. For qwen4exp that is ~128 subgraphs per
device, each getting its own entry in the per-device cuda_graphs map.

With max_cuda_graphs = 64 the LRU cap evicted entries on every decode
token, so graph->uid never matched and warmup never completed: zero CUDA
graph captures, fully eager decode (~14.6k kernel launches per token per
device). Measured on 8x V100-SXM2 (NVLink), qwen4exp Q4_K_XL:

  TP4 tg64:   6.77 -> 44.41 ± 6.26 t/s (tg512: 47.93)
  TP8 tg64:   5.50 -> 30.37 ± 5.86 t/s
  nsys tg64:  cudaGraphLaunch 0 -> 48888, cudaLaunchKernel 2.96M -> 118k

Layer-split mode only needs one entry per device and was unaffected.

512 covers large TP graphs; the existing 10s idle sweep still bounds the
map size in steady state.

Co-Authored-By: Claude Code <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant