Skip to content

Size KV pool after CUDA graph capture (opt-in) - #30157

Merged
cctry merged 7 commits into
sgl-project:mainfrom
cctry:port-post-capture-kv-sizing
Jul 7, 2026
Merged

cctry merged 7 commits into
sgl-project:mainfrom
cctry:port-post-capture-kv-sizing

Conversation

@cctry

@cctry cctry commented Jul 5, 2026 •

Copy link
Copy Markdown
Collaborator

Problem: the KV pool is sized before CUDA-graph capture, so graph memory is a heuristic guess baked into mem_fraction_static. On large configs the guess can be off by ~10 GB/GPU; the padded activation reserve silently absorbs the difference, wasting memory on some configs and risking OOM on others.

Fix: add SGLANG_ENABLE_POST_CAPTURE_KV_SIZING (opt-in, default off). When enabled:

  1. Reserve the KV pool as virtual memory only (CUDA VMM arena; tensors get stable pointers, ~0 bytes physical).
  2. Capture CUDA graphs first — their baked-in KV pointers stay valid.
  3. Size the pool from measured free memory, then physically back it (cuMemMap), and drop the per-token activation reserve.

Test

test/registered/mem_cache/test_post_capture_kv_sizing.py launches a server with the flag on and asserts:

  • the post-capture sizing path actually ran (log line present, not a silent skip),
  • /server_info reports max_total_num_tokens > 0,
  • gsm8k accuracy unchanged (>= 0.80 on Llama-3.1-8B).

Registered in the base CI tier (base-b / 1-gpu-large) so it runs on every PR.


CI States

Latest PR Test (Base): ❌ Run #28883219805
Latest PR Test (Extra): ❌ Run #28883219626

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a post-capture KV cache sizing feature using CUDA Virtual Memory Management (VMM) to dynamically size and physically back the KV cache pool based on measured free memory after CUDA graph capture. While the implementation is a significant optimization, several critical issues must be addressed: a missing error check on cuMemAddressReserve, a multi-process race condition during inline compilation due to non-unique library names, a unit mismatch when querying device memory capacity (bytes vs. megabytes) that wastes memory, an incorrect slot calculation for HND (Paged) KV cache leading to severe over-allocation, and an unsafe deallocation order when clearing buffers.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread python/sglang/srt/mem_cache/kv_vmm_backing.py
Comment thread python/sglang/srt/mem_cache/kv_vmm_backing.py
Comment thread python/sglang/srt/model_executor/model_runner_kv_cache_mixin.py
Comment thread python/sglang/srt/mem_cache/memory_pool.py
Comment thread python/sglang/srt/mem_cache/memory_pool.py Outdated
@cctry

cctry commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator Author

/tag-and-rerun-ci

@github-actions github-actions Bot added the run-ci CI: run the baseline test suite on this PR label Jul 5, 2026
@cctry
cctry force-pushed the port-post-capture-kv-sizing branch from f9782e6 to fda9465 Compare July 5, 2026 12:38
cctry and others added 4 commits July 5, 2026 17:02
Add SGLANG_ENABLE_POST_CAPTURE_KV_SIZING (default off). When enabled, the KV
pool is reserved as CUDA VMM virtual memory, CUDA graphs are captured against
its stable pointers, then the pool is sized from measured free memory and
physically backed via cuMemMap -- dropping the heuristic per-token activation
reserve baked into mem_fraction_static.

When the flag is off, behavior is unchanged: the feature is fully gated behind
ServerArgs.post_capture_kv_sizing_planned(), and the mem_fraction_static
heuristic (including reserve_for_graph_mb / reserve_for_deepep_a2a_mb) is
byte-for-byte equivalent to the previous logic.

Adds test/registered/mem_cache/test_post_capture_kv_sizing.py guarding the
feature (server comes up, sizing path runs, pool sized, gsm8k unchanged).
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@cctry
cctry force-pushed the port-post-capture-kv-sizing branch from ace5952 to c1ad7d7 Compare July 6, 2026 00:02
cctry added 2 commits July 5, 2026 18:39
Post-capture sizing measured only KV-pool-free memory and left near-zero
headroom, so the pool over-sized and inference OOM'd on transient activation
(eager decode above the captured graph max_bs, and prefill/logits allocations
that the captured graphs do not hold resident).

Floor the resize headroom to the activation working set via
pre_capture_activation_reserve_mb() (same reserve used pre-capture), and apply
it for all models, not just mamba. Renamed from mamba_pre_capture_reserve_mb
since it covers the generic activation reserve.
At post-capture resize, keep the minimal mem_fraction headroom by default and
floor it to mamba_pre_capture_reserve_mb only when decode capture max_bs <
max_running_requests (eager decode above the captured graph) or a mamba state
cache is present; warn on the eager-decode case. Restore the per-arena stub
suffix to the instance counter so all TP ranks emit identical stub sources and
the shared build directory stays idempotent; the pid-based suffix made each
rank's source unique and raced on the shared build dir.
@cctry
cctry force-pushed the port-post-capture-kv-sizing branch from 7ed5661 to 4a96aa6 Compare July 6, 2026 21:52
Resolve server_args.py conflict: keep the PR's post-capture KV sizing
refactor (reserve_for_graph_mb / reserve_for_deepep_a2a_mb helpers) and
port main's resolved-view fixes (_resolved().enable_dp_attention and
resolved_view(self).moe_a2a_backend) into those helpers.
@cctry

cctry commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

/rerun-test test/registered/mem_cache/test_post_capture_kv_sizing.py

@github-actions

github-actions Bot commented Jul 7, 2026 •

Copy link
Copy Markdown
Contributor

Results for /rerun-test test/registered/mem_cache/test_post_capture_kv_sizing.py:

🚀 1-gpu-h100 (1 test): ✅ View workflow run

cd test/ && python3 registered/mem_cache/test_post_capture_kv_sizing.py

@cctry
cctry merged commit 2ad9a24 into sgl-project:main Jul 7, 2026
90 of 123 checks passed
if eager_decode_gap or self.mambaish_config is not None:
headroom_gb = max(
headroom_gb,
self.server_args.mamba_pre_capture_reserve_mb(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this fallback is too relaxed.

  1. For a decode engine, it reserves > 10 GB, which is too much
  2. why do we call "mamba" here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. this 10GB reservation happens only when max_running_reqs is not set and decode can run at a batch size over the captured size. this should not be a common case
  2. good catch, we need to change this name to be generic

# Derive from the real buffers when they exist (covers arbitrary layouts,
# e.g. vectorized_5d); fall back to _kv_buffer_shapes for the pre-allocation
# post-capture call, which only runs for NHD/HND.
if getattr(self, "k_buffer", None) and getattr(self, "v_buffer", None):

@merrymercy merrymercy Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not use getattr anymore!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i will refactor this to add the attribute always


@property
def post_capture_active(self) -> bool:
return getattr(self.full_kv_pool, "post_capture_active", False)

@merrymercy merrymercy Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not use getattr anymore!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i will remove this one

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

Labels

run-ci CI: run the baseline test suite on this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants