Repository navigation
[Perf] unified-memory: Free SWA segments without a host sync - #43170
Conversation
af20415 to
82e50c7
Compare
82e50c7 to
0e3774e
Compare
| ( | ||
| self.free_group, | ||
| self.free_page_reps_group, | ||
| self.full_page_reps_group, |
There was a problem hiding this comment.
[bug] Two-pool evict_to_free_tokens still flushes the old 3-tuple, so this new list is invisible there.
_flush_deferred_free_group returns when not any(pending_groups). After this change, free_full_segment queues into full_page_reps_group instead of free_page_reps_group (or full_free_group at page size 1). A group that only has those full-only segment frees is treated as empty on the two-pool path (UnifiedSWAAllocatorBase.evict_to_free_tokens) and left queued, so reclaim_plan can over-evict or fail while the pages are already waiting to be released. The tri-pool override goes through this method and is fine; gpt-oss is two-pool.
Suggestion: Pass self.full_page_reps_group in the two-pool tuple as well, or share one pending-groups helper with this flush.
0e3774e to
e214552
Compare
UnifiedSWAAllocatorBase filtered every segment free by swa liveness with a boolean mask (`reps[swa_v2p > 0]`). A mask index runs `nonzero`, which copies its count to the host and blocks the scheduler until the in-flight forward drains, so under overlap scheduling the next batch is prepared only after the GPU went idle. In gpt-oss-20b serving traces (SM90, page size 256) the out-of-window ratchet (free_swa_segment) and the request release (_release_page_reps at free_group_end) issued about 2.6 such syncs per prefill. Prefill cycles ran +1.3..+3.3% (speculative) and +2.5..+7.0% (non-speculative) longer than with the paged SWA allocator at matched batch size, all of it GPU idle, and EAGLE3 lost 4.3-4.7% throughput on 8192-token prompts. The paged SWA allocator frees the same segments with a fixed shape: its callers split a kv row by host bookkeeping, and it trusts that split. The unified callers make the same split -- free_kv_row_segments sends [swa_dead_lo, swa_evicted_seqlen) through free_full_segment, the tree picks FreeDeviceKV or FreeDeviceKVFullOnly per node, and the ratchet frees only above swa_dead_lo with a cursor that only moves up. So free_swa_segment and free_segment now free every page rep on the swa side, and free_full_segment releases the full side only, through its own free-group pile (which the mamba donor's flush also sees). Each checks its premise with `expect`, as the paged allocator's `swa.peer_mapped` does: asynchronous, and loud from SGLANG_INVARIANT_CHECK=warn up. The two invariants are declared in allocator/swa.py beside the paged allocator's: the registry refuses a name twice, and a fixture that patches sys.modules (the LMCache cursor tests) makes the next test import unified_hybrid_swa again. Page size 1 takes the same path. The set-shaped free() and free_swa() keep their filter; RecoverSWAWithLockedFull relies on it. A second ratchet over a range was filtered to a no-op. No caller issues one; under strict checks it is now reported instead. Tests (CPU): - test_segment_frees_dispatch_no_data_dependent_shape records, with a TorchDispatchMode, every op whose output shape depends on values (a boolean-mask index, nonzero, unique, item). Before: three mask indexes at page size 1 and at 4. After: none, and both sides' capacity comes back. - test_a_double_ratchet_is_reported (replaces test_double_ratchet_is_filtered_not_crashed) and test_a_full_only_free_of_a_bound_swa_page_is_reported fail before (nothing raises) and pass after. - The tri-pool donor test and the eviction fixture name the new pile. - test_importing_the_module_again_registers_nothing_twice runs the module again as that fixture does: a duplicate-invariant ValueError before, none after (test_lmcache_component_cursors.py: 3 errors before, 4 passed after). - The unified CPU scope passes with SGLANG_INVARIANT_CHECK=2 exactly as without it, so no flow in it frees a tombstoned swa page through a segment free.
e214552 to
2509d74
Compare
This PR is part of a stack (oldest at bottom):
Current head
2509d740d3, based on main47eeddcd18. Stack predecessor: #43168. This PR targetsmainafter its stack predecessor merged.Motivation
UnifiedSWAAllocatorBase(--enable-unified-memorywith a hybrid sliding-window model) filtered every segment free by swa liveness with a boolean mask (reps[swa_v2p > 0]). A mask index runsnonzero, which copies its count to the host and blocks the scheduler until the in-flight forward drains, so under the overlap scheduler the next batch is prepared only after the GPU went idle. In gpt-oss-20b serving traces (SM90, page size 256) the free of tokens that left the window (free_swa_segment) and the request release (_release_page_repsatfree_group_end) issued about 2.6 such syncs per prefill. Prefill cycles ran +1.3..+3.3% (speculative) and +2.5..+7.0% (non-speculative) longer than with the paged SWA allocator at matched batch size, all of it GPU idle.Modifications
Two-pool allocation reclaim flushes the deferred FULL-only segment queue before sizing eviction. A regression test fills that queue alone and verifies allocation succeeds without evicting the tree.
The paged SWA allocator frees the same segments with a fixed shape: its callers split a kv row by host bookkeeping, and it trusts that split. The unified callers make the same split:
free_kv_row_segmentssends[swa_dead_lo, swa_evicted_seqlen)throughfree_full_segment, the tree picksFreeDeviceKVorFreeDeviceKVFullOnlyper node, and the out-of-window free releases only tokens aboveswa_dead_lo, with a cursor that only moves up. Sofree_swa_segmentandfree_segmentnow free every page they are handed on the swa side, andfree_full_segmentreleases the full side only, through its own deferred list for the free group (full_free_group), whichflush_deferred_full_freesalso drains when Mamba state needs memory. Each checks its premise withexpect, as the paged allocator'sswa.peer_mappeddoes: asynchronous, and loud fromSGLANG_INVARIANT_CHECK=warnup. The two invariants are declared inallocator/swa.pybeside the paged allocator's: the registry refuses a name it already holds, and a test fixture that patchessys.modulesmakes the next test importunified_hybrid_swaagain. Page size 1 takes the same path. The set-shapedfree()andfree_swa()keep their filter;RecoverSWAWithLockedFullrelies on it.A second out-of-window free over the same range was filtered to a no-op. No caller issues one; under strict checks it is now reported instead.
Tests (CPU):
test_segment_frees_dispatch_no_data_dependent_shaperecords, with aTorchDispatchMode, every op whose output shape depends on values (a boolean-mask index,nonzero,unique,item). Before: three mask indexes at page size 1 and at 4. After: none, and both sides' capacity comes back.test_a_double_ratchet_is_reported(replacestest_double_ratchet_is_filtered_not_crashed) andtest_a_full_only_free_of_a_bound_swa_page_is_reportedfail before (nothing raises) and pass after.test_importing_the_module_again_registers_nothing_twiceruns the module again as such a fixture does: a duplicate-invariant error before, none after (test_lmcache_component_cursors.pyerrors in 3 of its 4 cases with the invariants inunified_hybrid_swaand passes with them inallocator/swa.py).SGLANG_INVARIANT_CHECK=2exactly as without it, so none of them frees an already released sliding-window page through a segment free.Accuracy Tests
Earlier combined stack
b550cacde6adds a test-fixture correction in #43326: the Gemma4 read-stream fixture declares its FULL address space, and the HiSparse registry test uses realCacheInitParamsfor the retained-window calculation. Both complete test files pass locally with GPUs hidden: 4 Gemma4 tests and 26 registry tests. Pre-commit passed. The five descendant patches are unchanged (git range-diff); this follow-up changes no production code. The earlier 455-test run did not cover these two files; GitHub CI has not yet been revalidated for this head.Validation before the test-fixture follow-up, on combined stack
4cf27a551c: 455 tests and 921 subtests passed on B300, plus the two widened-history regressions against Rust TreeCore. The new FULL-fallback metadata cases cover eager execution and CUDA graph replay. New allocation-reclaim, explicit-prefill and JIT-reuse regressions fail on the previous code and pass with the fixes. These are combined-stack checks, not independent model reruns of each PR.The measurements in this section are historical results from before this rebase, at the revisions/configurations stated below. Current combined-stack validation is linked above.
SM90, TP1, gpt-oss-20b, no speculation, radix cache, overlap scheduler, CUDA graphs on,
--mem-fraction-static 0.8. Baseline: the default pool. Unified:--enable-unified-memory. Every server ran with the per-step pool checks and the allocator invariants on (SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_BUSY=1 SGLANG_CHECK_KV_PAGE_INVARIANTS=1 SGLANG_INVARIANT_CHECK=1), so a free that this change gets wrong stops the server or is reported. These ran on this commit over main 7224fce. GSM8K, all 1314 test questions, 5-shot, greedy:gpt-oss-20b's GSM8K score moves by several points between identical runs (up to ~6 in our runs of one configuration), and three of the four rows favour the unified pool.
Under the same checks, two load tests at page size 256: one with a shared prefix (one 4096-token system prompt, 192 requests with 128-token questions and 2048 output tokens, concurrency 192) and one with long outputs (100 requests, 512 input / 4096 output tokens, concurrency 128). Every request completed on both arms and both backends, and no server reported a leak, double free, invariant violation or illegal memory access.
GPU unit tests for unified memory: 4104 passed, 0 failed.
Speed Tests and Profiling
The measurements in this section are historical results from before this rebase, at the revisions/configurations stated below. Current combined-stack validation is linked above.
bench_servingwith random prompts at concurrency 64, in two shapes: 2048 input / 512 output tokens (200 prompts) and 8192 input / 64 output tokens (100 prompts). gpt-oss-20b, no speculation, page size 256, radix cache, overlap scheduler, CUDA graphs on,--mem-fraction-static 0.55. Each entry is the unified pool's gap to the default pool on the same tree, before (main) and after this PR; the default pool runs twice and the gap is against the mean of the two runs, with their spread (A/A) as the noise floor. This table ran on this commit over an earlier main.The triton rows compare with the first default-pool run only, and their A/A is main's: on this PR's tree the second run compiled kernels during the benchmark. On prefill batches with identical new and cached token counts in both arms' logs, unified prefill throughput is 1.000 (fa3) and 0.999 (triton) of the default pool's with this PR, with an interquartile range of 0.996-1.003; on main the median is 0.997 with an interquartile range of 0.920-1.065, the spread the host syncs add.
The same configuration on this commit over main 7224fce, unified pool against the default pool:
Checklist
CI States
Latest PR Test (Base): ❌ Run #39127973340
Latest PR Test (Extra): ❌ Run #39127973371
Latest PR Test (AMD ROCm 10): ❌ Run #39127973401