Conversation
_free_swa_pages() reads full_to_swa_index_mapping[reps] and emits swa_tokens // page_size without dropping reps that have no SWA peer. Those read 0, so reserved page 0 is handed to free_page_ids(), which is documented "no page twice, no dedup" -- it goes back on the free list once per unmapped rep until swa_attn_allocator.available_size() exceeds .size and free_group_end() asserts. Unmapped reps are legitimate here. SWAComponent.evict_component() passes the node's FULL indices precisely so that "slots with no SWA pair are skipped", and free_swa()/_release_swa() does skip them by filtering swa_indices > 0. free_swa_segment()/_free_swa_pages() does not; its only guard is expect(_SWA_PEER_MAPPED, ...), which is signal-only and off by default, since resolve_level() returns OFF unless SGLANG_INVARIANT_CHECK or SGLANG_ENABLE_ASYNC_ASSERT is set and Bucket.FATAL_UNCONTAINABLE forbids a recover. Page 0 is the padding slot (free_pages = torch.arange(1, num_pages + 1)), so without the assert this hands the padding slot out as a real KV page. Before _free_swa_pages() existed, page_size > 1 reached _release_swa() via free_swa() and was filtered; only the page_size == 1 branch carried the bare expect. The fixed-shape segment path routed page_size > 1 around that filter. The allocator's own debug check already encodes the intended semantics -- torch.unique(ref[ref > 0] // ps) excludes unmapped entries -- so SGLANG_DEBUG_MEMORY_POOL=1 fails on the current code too. Drop the unmapped reps, matching _release_swa(). _SWA_PEER_MAPPED has no remaining users and is removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TianDi101
requested review from
alphabetc1,
hnyls2002 and
ispobock
as code owners
September 8, 2026 13:34
Contributor
Author
|
Closing in favour of a single PR. The same fix now rides on #38269, which is the PR that makes the direct external linker install FULL slots with no SWA peer under unified KV and therefore triggers this path. Landing them together keeps the trigger and the fix in one merge. Commit on that branch: 5264e61. Root cause, the #38159 interaction, and the unit + end-to-end verification are written up at #38269 (comment). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug
SWATokenToKVPoolAllocator.free_group_end()asserts on all ranks a second afterthe server goes live, whenever an external cache linker is attached under
unified KV:
available_size() > sizeis an over-free: more SWA pages were returned than exist.Root cause
SWAComponent.evict_component()deliberately passes the node's FULL indices,and says why:
But
apply_component_action()callsfree_swa_segment(), notfree_swa(). Thetwo paths do not have the same contract:
free_swa()->_release_swa()filtersswa_indices > 0.free_swa_segment()->_free_swa_pages()does not filter. Its only guard isexpect(_SWA_PEER_MAPPED, swa_tokens > 0, ...), which is signal-only and off bydefault:
resolve_level()returnsOFFunlessSGLANG_INVARIANT_CHECK/SGLANG_ENABLE_ASYNC_ASSERTis set, andBucket.FATAL_UNCONTAINABLEforbids arecover. In a normal run it neither detects nor repairs anything.So a rep with no SWA peer reads 0, becomes
swa_pages = 0 // page_size= 0, andfree_page_ids()is documented "Free exactly these pages; no page twice, nodedup." Reserved page 0 therefore goes back on the free list once per unmapped
rep until
available_size()passessize.Page 0 is the padding slot (
free_pages = torch.arange(1, num_pages + 1)), sounguarded this hands the padding slot out as a real KV page; the assert is
catching genuine corruption.
Why it is a regression
Before
_free_swa_pages()existed,free_swa()routedpage_size > 1through_expand_to_full_pages()->_release_swa(), which filters> 0.page_size > 1was safe by construction; only the
page_size == 1branch carried the bareexpect, with the comment "A filter here would make the output shapedata-dependent, which costs a device-to-host sync." The fixed-shape segment path
routes
page_size > 1around that filter.The allocator's own debug check already encodes the intended semantics --
torch.unique(ref[ref > 0] // ps)excludes unmapped entries -- so withSGLANG_DEBUG_MEMORY_POOL=1the current code fails that assert too.Fix
Drop unmapped reps in
_free_swa_pages(), exactly as_release_swa()already doeson the set-shaped path.
Reproducer
Standalone, no model required -- builds a real
SWATokenToKVPoolAllocator(page_size=256) and exercises
free_swa_segment():[0,0,0,0], available 5120 > size 4096[1,2,0,0], available 4608 > size 4096[1,2]End-to-end
DeepSeek-V4-Pro FP4, TP8 + DP attention, EAGLE MTP, page_size 256, concurrency
256, agentic trace replay, external linker on all 8 ranks.
The server is fired up and ready to roll!errors=0, zero asserts.Note on the sync
The filter is a boolean index, so the output shape is data-dependent and it costs
a D2H sync -- the thing the fixed-shape path was built to avoid. In a follow-up I
can move the filter to the release points (
free_group_end()plus the ungroupedfree) so it runs once per free group instead of once per call; that keeps
_free_swa_pages()fixed-shape. Filing the straightforward version first since itis the one with end-to-end validation behind it.
Context
Found while running the AMD DeepSeek-V4 unified-KV external-linker arm from
#38269. That PR is not the cause and does not need to change:
its branch predates
_free_swa_pages(), and the defect is onmainindependently.The linker just makes the unmapped-slot case common, because it installs FULL
slots whose SWA component was skipped under unified KV. Window-evicted tails can
reach the same path without any linker.
CI States
Latest PR Test (Base): ❌ Run #34232788705
Latest PR Test (Extra): ❌ Run #34232788466
Latest PR Test (AMD ROCm 7.2): ❌ Run #34232788658