[AMD][DSV4] Reland unified-KV pool sizing and SWA ring accounting, fully gated - #38192
Conversation
…g" (sgl-project#30315) This reverts commit f5819b0 (sgl-project#38163), restoring sgl-project#30315 verbatim. The follow-up commit gates every remaining shared-path change behind the unified-KV switch.
The reverted sgl-project#30315 claimed "no behavior change on the non-unified (fp8) path", but three changes reached shared code with no gate at all: 1. deepseek_v4_memory_pool.py: `c4_state_pool_size = max(caller, num_req_slots * ring)` ran *before* `_unified_kv` was resolved, so the fp8 path also paid the request-addressed floor -- raising its footprint and lowering max_total_num_tokens. The gate is now resolved before any sizing, and unified sets the size exactly instead of via max(); fp8 keeps the caller-supplied SWA-addressed size byte-for-byte. 2. schedule_policy.py: the `swa_needed >= rem_swa_tokens` -> `>` relaxation at both admission sites applied to every hybrid-SWA model. It is correct only where rem_swa_tokens is an exact ring-slot capacity, so it is now conditional; the legacy SWA-token path keeps its conservative `>=`. 3. invariant_checker.py: the unconditional `logical_attn_allocator` unwrap changed which object the invariant was asserted against on every DSV4-HiSparse deployment, not just unified ones. Also tightens the two duck-typed `clear_c4_req_states` call sites (allocation.py, disaggregation/decode.py) to check `_unified_kv` explicitly rather than relying on the hook's internal early return, and skips the `newly_allocated` list comprehension entirely off the unified path. The gate is identity-compared (`is True`) so a duck-typed test stub that auto-creates attributes cannot accidentally select the unified path. Drops the unused `host_hit_len` / `storage_hit_len` parameters sgl-project#30315 added to `_update_prefill_budget`; no call site ever passed them. test_dsv4_c4_state_lifecycle.py: the unified lifecycle test built a bare MagicMock and relied on duck-typing alone, so it now declares `_unified_kv` like the pool it stands in for. Adds the matching negative test that the reset never fires off the unified path.
The reverted PR carried edits that change no behavior and only widen the review surface. Removing them shrinks the diff from 113 hunks / +860/-166 to 92 hunks / +815/-97 with no functional delta. - Restore 14 import blocks that were collapsed to one line. The repo's own ruff-format (v0.15.1, magic trailing comma) keeps the multi-line form, so the collapse did not come from repo tooling. - Collapse two byte-identical Triton branches. `elif use_req_ring:` had the same body as `if compress_ratio == 128:` in attn.py and compressor.py; both operands are tl.constexpr, so `or` folds at compile time. This is already the form compress_hip.py uses in the same series. - Drop a local `attn_head_dim` in _fixed_c4_state_bytes that duplicates the `self.attn_head_dim` the series itself introduces. - Restore seven comments in c_plan.cuh that were re-wrapped to ~80 columns. The governing .clang-format sets ColumnLimit: 120 and clang-format 20.1.7 reports both the old and the new text as clean, so the re-wrap was gratuitous; two of them broke a column-aligned address table.
alloc_extend, alloc_decode and new_pages_available all take a unified early return; alloc_extend_swa_tail did not, while new_pages_available stopped consulting num_swa_pages on that path. The vestigial paged SWA allocator is therefore called with no capacity gate, and a bare assert turns its exhaustion into a scheduler crash instead of a None backoff. Reachable via DecodePreallocQueue, which routes DSV4 page_size > 1 preallocation through this method.
Every unified-KV gate now spells the check `... is True` instead of relying on truthiness. `_unified_kv` is a plain bool on the real pool, so this is a no-op in production, but a duck-typed stub (a bare MagicMock auto-creates a truthy attribute) would otherwise select the unified path in tests. Six sites still used the truthy form: the allocator's master switch in SWATokenToKVPoolAllocator.__init__, the four CompressorHip ring-address branches, the pool-stats observer, the SWA leak-invariant skip, the swa_max_total_num_tokens reconciliation, and the decode-bottleneck diagnostic. SWATokenToKVPoolAllocator.debug_print() reported swa_attn_allocator's available size, which on the unified path is the vestigial paged allocator and contradicts swa_available_size(). It now calls swa_available_size(), so the two agree; off the unified path the two are the same expression. Also condenses four comment blocks added earlier in this series that ran past the two-line limit in .claude/rules/comment-style.md.
The two compressor entry points still gated use_req_ring on bare truthiness. Both take a DeepSeekV4TokenToKVPool, so read _unified_kv directly; the same holds for the four compress_hip sites, which sit after an isinstance assert. The XPU compress plan builder has no use_req_ring parameter, so a unified-KV plan would silently fall back to the SWA-paged layout. Assert instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…bute _compute_dsv4_sizes now reads self._unified, but the white-box fixture builds DSV4PoolConfigurator via object.__new__ and only set five attributes, so test_dsv4_accepts_pool_above_floor raised AttributeError. Set it, and cover the unified branch where the c4 state pool is sized per request instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ies on calculate_pool_sizes subtracts the three request-scoped fixed pools from the byte budget; calculate_pool_sizes_from_max_tokens cannot, because it takes a token count and subtracting bytes there would double-count. That is only safe while every constraint in config_from_budget is a min(), which nothing checked. Assert it, state the caller contract, and add a regression test that caps a budget-derived token count and requires the total footprint to shrink. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Apply .claude/rules/comment-style.md to the branch's added comments: drop function-top preambles whose facts are already stated at the lines they constrain, drop docstrings on private helpers, and condense multi-line rationale to the single non-recoverable fact. Kept: cross-file mirrors (swa_ring_size, CompressStatePool row math), layout and unit facts, magic-number derivations, the kernel write-pad aliasing bound, and the token-count-vs-byte-budget caller contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Additional benchmark detail: the c=512 comparison used 7 baseline, 6 PR, and 3 |
Re: memory accountingThe 11.63 GiB difference between the two arms is accounted for. The reverted The allocation ledger matches the observed PR footprint within allocator A startup accounting assertion or follow-up regression test would still be |
ShangmingCai
left a comment
There was a problem hiding this comment.
Disaggregation part LGTM
|
NV CI clean, (XPU) / stage-b-test-1-gpu-xpu stuck not relevant. |
…lly gated (sgl-project#38192) Co-authored-by: hnyls2002 <lsyincs@gmail.com> Co-authored-by: Liangsheng Yin <hnyls2002@gmail.com>
Motivation
Under unified-KV, the DSV4 kernels address SWA and C4 state as per-request
rings, but the pool sizer still charged both as token-scaled pools. It reserved
memory the runtime never allocates, so full-token capacity came out well below
what the card can hold.
#30315 fixed the sizing but was reverted in #38163 because it was not fully
gated. This reland keeps the fix and puts every branch behind the unified-KV
gate; the non-unified FP8 path is untouched.
Modifications
pool_configurator: under unified-KV, SWA and C4 state are sized per requestslot (
_fixed_swa_bytes,_fixed_c4_state_bytes); the remainder goes tofull KV.
deepseek_v4_memory_pool: C4 state pool becomesnum_req_slots * c4_ring_size, resolved from the same gate as the sizer so the two cannotdrift.
swaallocator:swa_available_size()reports ring capacity in tokens(free req slots x per-slot cost) instead of page counts.
schedule_policy: a request is charged one ring slot at first admissionrather than a per-chunk token budget; continuations are not charged again.
c_plan.cuh: the C4 plan uses request-ring addressing(
req_pool_idx * ring_size + pos % ring_size).Results
6,848,000 -> 12,569,600 (+83.55%).
request-scoped allocations instead of token-scaled pools.
95.2% -> 95.1% (1 question difference; no observed accuracy regression).
no measurable throughput, TTFT, or TPOT regression within run-to-run variance
at concurrency 512 (replicated); concurrency 256 is a single run per arm.
Validation
unified/non-unified gating, and token-cap monotonicity.
pr-gatefails on the current head, which skips the downstream suites.Not yet diagnosed.
Caveat
The unified-KV BF16 layout is pre-existing and is outside the scope of this PR.
CI States
Latest PR Test (Base): ❌ Run #34116192994
Latest PR Test (Extra): ✅ Run #34116192774
Latest PR Test (AMD ROCm 7.2): ❌ Run #34116193084