Reduce decode CUDA-graph memory with shared pool and shared logits buffer#439
Reduce decode CUDA-graph memory with shared pool and shared logits buffer#439gameofdimension wants to merge 4 commits into
Conversation
…gits buffer
Capturing the SID-GR decode CUDA graph at beam_width=1024 reserved a
private CUDA-graph memory pool and a [batch, beam, vocab] logits tensor
per captured graph (~30 entries), raising idle GPU memory from ~24 GiB to
~45 GiB and OOM-ing decode at the configured beam width. Both are fixed
without changing any computation, so outputs are numerically unchanged.
1. Shared graph private pool (CudaGraphCacheMixin)
The prefill piecewise runner already shared one graph_pool_handle()
across its captures; the decode runner did not and got a fresh private
pool per capture. Hoist the lazy handle + pool= kwargs + env opt-out
onto CudaGraphCacheMixin (deduplicating the two runners' identical
helpers), and have each runner set a _separate_pools_env class
attribute. Decode captures now reuse one pool instead of ~30.
Opt-out: GR_INFERENCE_DECODE_CUDA_GRAPH_SEPARATE_POOLS /
GR_INFERENCE_PREFILL_CUDA_GRAPH_SEPARATE_POOLS.
2. Shared logits output buffer
lm_head logits ([batch, beam, vocab], ~batch*297 MiB at beam=1024) were
held once per captured graph. Add an out= path to _linear_project
(matmul out= into a caller buffer) and a logits_out= arg to
forward_decode_step; the decode runner allocates one buffer per
(active_beam_width, bucket) and writes every captured graph's logits
into it, so the tensor lives once per shape rather than once per graph.
Opt-out: GR_INFERENCE_DECODE_CUDA_GRAPH_SEPARATE_LOGITS.
New /status fields: decode_cuda_graph_shared_pool,
decode_cuda_graph_shared_output_buffers.
Measured on NVIDIA L20 46GiB (beam=1024, context_len=4000):
- idle after warmup: 45.4 -> 26.9 GiB (graph overhead ~21 -> ~3 GiB).
- beam=1024 decode that previously OOM'd now succeeds.
- graph-ON output_ids match the eager (graph-OFF) baseline token-for-token
across single / batch2 / batch4 (24/24, 0 mismatch); replays confirm the
shared path is hit.
Tests: 9 CPU unit tests added (test_cuda_graph_shared_pool.py,
test_decode_shared_logits.py) covering the pool-sharing helper (opt-out,
handle reuse, dedup contract) and the shared-logits buffer (_linear_project
out= correctness, per-(beam,bucket) caching).
Signed-off-by: gameofdimension <gameofdimension@gmail.com>
Co-Authored-By: Claude Code
Co-Authored-By: GLM-5.2
Greptile SummaryThis PR reduces decode CUDA graph memory use for large beam sizes. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (4): Last reviewed commit: "Merge branch 'main' into enh-reduce-deco..." | Re-trigger Greptile |
The shared logits cache (_shared_logits, keyed by (beam_width, bucket)) is independent of the graph LRU, so a buffer allocated for a decode shape stayed resident for the runner lifetime after _store_graph() evicted the last graph using it. A transient large capture (high beam/bucket) could thus leave its [bucket, beam, vocab] tensor resident and re-grow idle memory after eviction, undermining the memory bound this change adds. Override _store_graph in GRDecodeCudaGraphRunner: after the base store (which performs LRU eviction), drop shared logits buffers whose (beam_width, bucket) shape has no live graph entry, tying buffer lifetime to graph lifetime. Tests: two CPU tests in test_decode_shared_logits.py — eviction frees the orphaned buffer, and a shape shared by multiple graphs is kept until its last graph is evicted. Signed-off-by: gameofdimension <gameofdimension@gmail.com> Co-Authored-By: Claude Code Co-Authored-By: GLM-5.2
… off With GR_INFERENCE_DECODE_CUDA_GRAPH_MAX_ENTRIES <= 0, the base _store_graph() clears _graphs and returns without retaining the newly captured entry, so the override's live_shapes set was empty and it deleted the shared logits buffer that _capture had just wired into entry.output. The buffer itself was not freed (the caller's immediate replay still reaches it via entry.output, which holds the same tensor), but the cache entry was dropped on every capture -- defeating per-shape reuse under max_entries=0 and leaving the override's correctness dependent on that external reference. Always add the just-stored entry's (beam_width, bucket) to live_shapes so its output buffer stays in the cache through the immediate replay regardless of whether the base store retained the entry. A no-op for max_entries > 0, where the entry is already live. Tests: test_shared_logits_buffer_kept_for_just_captured_entry_when_caching_disabled covers the max_entries=0 path (fails without this change). Signed-off-by: gameofdimension <gameofdimension@gmail.com> Co-Authored-By: Claude Code Co-Authored-By: GLM-5.2
|
@cb521 @shijieliu would you please have a look |
|
@gameofdimension yes,I have a fix for this feature, I will reply the customer, THX. |
Is the approach in this PR a correct solution? Does it have any chance of being merged? @cb521 |
|
@gameofdimension Sure, it's a common impl, I will merge it to here. |
I'm a bit confused... Specifically, I'm referring to the GPU memory issue and the proposed fix in this PR. |
Description
Reduces the memory cost of the SID-GR decode CUDA graph so decode at
beam_width=1024no longer OOMs. At beam=1024 the decode graph captured ~30 entries, each reserving its own CUDA-graph private pool and a[batch, beam, vocab]logits tensor, raising idle GPU memory from ~24 GiB to ~45 GiB and triggering OOM during decode. Both are fixed without altering any computation, so outputs are numerically unchanged.graph_pool_handle()across its captures; the decode runner did not, and got a fresh private pool per capture. The lazy-handle +pool=kwargs + env opt-out are hoisted ontoCudaGraphCacheMixin(deduplicating the two runners' identical helpers), and each runner sets a_separate_pools_envclass attribute. Decode captures now reuse one pool instead of one per entry.Opt-out:
GR_INFERENCE_DECODE_CUDA_GRAPH_SEPARATE_POOLS/GR_INFERENCE_PREFILL_CUDA_GRAPH_SEPARATE_POOLS._linear_projectgains anout=path (matmul out=into a caller buffer) andforward_decode_stepalogits_out=arg; the decode runner allocates one[batch, beam, vocab]buffer per(active_beam_width, bucket)and writes every captured graph's lm_head output into it, so the logits lives once per shape rather than once per graph.Opt-out:
GR_INFERENCE_DECODE_CUDA_GRAPH_SEPARATE_LOGITS.New
/statusfields:decode_cuda_graph_shared_pool,decode_cuda_graph_shared_output_buffers.Measured (NVIDIA L20 46 GiB, beam=1024, context_len=4000): idle after warmup 45.4 → 26.9 GiB (decode-graph overhead ~21 → ~3 GiB); beam=1024 decode that previously OOM'd now succeeds;
decode_cuda_graph_shared_pool=1,shared_output_buffers=5, replays confirm the shared path is hit.Accuracy: graph-ON
output_idsmatch the eager (graph-OFF) baseline token-for-token across single / batch2 / batch4 (24/24, 0 mismatch).Checklist