Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
22b209a
Support unified memory with speculative decoding
Aug 26, 2026
e887a18
fix: use hybrid SWA draft flag for pool setup
merrymercy Sep 1, 2026
ad6c3b4
Use shared byte capacity for unified SWA pools
Aug 27, 2026
1aac878
Avoid double-rounding unified SWA demand
ZYHowell Aug 27, 2026
7ee2c74
Reject unsupported unified pool scheduler modes
Aug 27, 2026
c54b97d
Remove unified pool gate-only tests
Aug 28, 2026
8892c52
Limit shared-capacity changes to enabled paths
Aug 28, 2026
48b2a24
Fix unified SWA eviction over-reclaim
Aug 28, 2026
139a9a1
Remove added unified eviction planner test
YhZhuang Aug 28, 2026
9c31d3d
Remove low-signal unified capacity tests
Sep 1, 2026
1599357
Adapt shared capacity to current main
Sep 1, 2026
c2296be
Address unified SWA admission feedback
Sep 1, 2026
05cfb81
Support unified memory page envelopes in PD transfers
Aug 27, 2026
c4551a5
Use decode request pool for unified SWA PD
Aug 27, 2026
623f762
Reject unified Mooncake speculative transfers
Aug 27, 2026
c9b4c29
Simplify unified draft index plumbing
Aug 28, 2026
5a7d1ef
Fix unified SWA PD capacity accounting
Sep 1, 2026
08a06eb
Prefer same-side eviction for unified SWA
ZYHowell Sep 2, 2026
6c46fed
Merge updated shared-capacity branch into PD page envelopes
ZYHowell Sep 2, 2026
fe5787a
Prefer SWA tombstones before unified FULL eviction
Sep 2, 2026
5d19c4f
Merge updated shared-capacity policy
Sep 2, 2026
e6e491b
Restore upstream unified cache eviction policy
Sep 2, 2026
cc12f23
Merge restored eviction policy into PD page envelopes
Sep 2, 2026
66fed93
Use joint capacity accounting for unified SWA
Sep 2, 2026
aa6d1cd
Merge joint capacity accounting into PD page envelopes
Sep 2, 2026
c41b068
Add unified capacity regression coverage
Sep 4, 2026
43ecd06
Use physical SWA indices for PD transfer
Sep 4, 2026
6fe1e7d
Merge unified capacity fixes into PD envelopes
Sep 4, 2026
d315b7d
Merge remote-tracking branch 'origin/main' into yonghao/ump-shared-ca…
Sep 4, 2026
7fadf8e
Merge commit 'd315b7d65e' into yonghao/ump-pd-page-envelopes
Sep 4, 2026
940d41b
Update PD priority test fixture for draft indices
Sep 4, 2026
d01513d
Update HiSparse test fixture for unified memory
Sep 4, 2026
25d1e3b
Merge commit 'd01513db8e' into yonghao/ump-pd-page-envelopes
Sep 4, 2026
8bc1586
Plan unified FULL/SWA reclaim against physical capacity
Sep 8, 2026
7f10ff8
Merge shared-capacity fixes into page-envelope transfers
Sep 8, 2026
653c010
Restore unified pool cleanup and scoped compatibility gates
Sep 8, 2026
07df3f4
Merge shared-capacity cleanup into PD page envelopes
Sep 8, 2026
f83adaf
Merge upstream main and adapt unified capacity to allocator refactors
Sep 10, 2026
b32e13e
Merge shared capacity refresh and adapt PD envelope allocators
Sep 10, 2026
f604efb
Keep HiSparse fixture compatible with Python 3.10
Sep 10, 2026
e09999e
Merge Python 3.10 fixture compatibility fix from shared capacity
Sep 10, 2026
70312b5
Remove unrelated UMP diff churn
Sep 10, 2026
054c220
Merge shared-capacity cleanup into PD page envelopes
Sep 10, 2026
4e0e1c3
Fix unified pool admission rounding
Sep 12, 2026
69747f6
Fix full-envelope PD transfers and merge capacity fixes
Sep 12, 2026
70671d5
Refactor unified SWA budget handling
Sep 13, 2026
66a2172
Merge shared-capacity cleanup
Sep 13, 2026
cb88f1f
Centralize decode SWA reservation routing
Sep 13, 2026
29545f8
Simplify shared SWA budget paths
Sep 13, 2026
8d4460c
Merge shared SWA budget cleanup
Sep 13, 2026
4d1fa0f
Fix unified capturer indexing and asymmetric SWA reservation
Sep 13, 2026
d0a10e5
Merge shared-capacity fixes and repair unified CPU copy API
Sep 13, 2026
567f4f5
Consolidate unified allocation, admission and capacity policies
Sep 13, 2026
6066493
Merge capacity cleanup and cover real SWA tail allocation
Sep 13, 2026
6c7be62
Refactor SWA admission into allocator-owned memory budgets
ch-wan Sep 13, 2026
4fdb2a7
Fix unaligned unified SWA prefill admission
Sep 14, 2026
5a8f410
Merge unified prefill rounding fix into PD support
Sep 14, 2026
089673a
Refactor unified SWA allocators into sibling capacity policies
Sep 14, 2026
121fdde
Clean up unified PD rejection and transfer interfaces
Sep 14, 2026
3c9040a
Merge reviewed shared-capacity cleanup into PD layer
Sep 14, 2026
564845d
Use shared byte capacity for unified SWA pools
Sep 14, 2026
a7084b3
Keep HiSparse fixture compatible with Python 3.10
Sep 10, 2026
b505c11
Remove unrelated UMP diff churn
Sep 10, 2026
99ee780
Fix unified pool admission rounding
Sep 12, 2026
8fd4aa4
Refactor unified SWA budget handling
Sep 13, 2026
fa6c95a
Simplify shared SWA budget paths
Sep 13, 2026
d9c7447
Fix unified capturer indexing and asymmetric SWA reservation
Sep 13, 2026
6cf450c
Consolidate unified allocation, admission and capacity policies
Sep 13, 2026
0a80658
Refactor SWA admission into allocator-owned memory budgets
ch-wan Sep 13, 2026
1f3b496
Fix unaligned unified SWA prefill admission
Sep 14, 2026
f208606
Refactor unified SWA allocators into sibling capacity policies
Sep 14, 2026
f0a0085
Fix shared prefill deferral and capture and draft capacity bounds
ch-wan Sep 14, 2026
ab2ea29
Validate inherited unified allocator PD and free-group contracts
ch-wan Sep 14, 2026
e3fa99e
Merge updated shared-capacity branch into UMP PD support
Sep 14, 2026
ab7d973
Explain SWA prefill reservation and chunking safety
Sep 14, 2026
ea634ec
Merge shared-capacity updates and simplify PD reservation checks
Sep 14, 2026
81d00a3
Refresh shared KV budgets during decode admission
Sep 14, 2026
405e92b
Port profiled SWA byte-budget fix from #36729
Sep 14, 2026
1c2fe92
Merge latest main after shared-capacity PR landed
ZYHowell Sep 15, 2026
ba4a021
Sync main through b803cfa0c4
ZYHowell Sep 15, 2026
f7e5443
Share precise SWA tail allocation across unified layouts
ZYHowell Sep 16, 2026
e514294
Merge main b02e16a895 and retain both PD transfer test groups
ZYHowell Sep 16, 2026
8431803
Let the pool price a decode-node preallocation
ch-wan Sep 18, 2026
4e50b49
Price tri-pool preallocation on the shared grid, and fix mamba retrac…
ch-wan Sep 18, 2026
7c60449
Pin the pair that actually double-counts, and trim the docstring
ch-wan Sep 18, 2026
b167da6
Cover the hybrid-Mamba retraction path that shipped untested
ch-wan Sep 18, 2026
7200139
Merge main 1fdd6c8921
ch-wan Sep 18, 2026
1f25db5
Move the SWA tail allocation test under the kernels taxonomy
ch-wan Sep 18, 2026
8cf543e
Merge latest main into unified memory PD branch
ZYHowell Sep 18, 2026
8233419
Fix HiSparse capacity test setup for unified PD admission
ZYHowell Sep 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions python/sglang/srt/arg_groups/fields/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,8 @@ class Memory(msgspec.Struct):
"Replace the statically-partitioned hybrid-model pools (full-attn KV + "
"SWA/Mamba state) with one byte buffer split dynamically between "
"sub-pools. Requires the Triton attention / linear-attn / Mamba "
"backends. PD disaggregation is supported over mooncake at equal "
"attention TP with pp=1; not yet compatible with hierarchical / "
"host-tiered KV cache, prefill cuda-graph capture, or speculative "
"decoding other than DSPARK.",
"backends. Supported PD-disaggregation and speculative-decoding "
"configurations are validated at startup.",
] = False
enable_session_radix_cache: A[
bool,
Expand Down
27 changes: 22 additions & 5 deletions python/sglang/srt/arg_groups/kv_cache_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,18 +228,26 @@ def handle_unified_memory_pool(server_args: Any) -> None:
if not cfg.enable_unified_memory:
return
if cfg.disaggregation_mode != "null":
# Constraints of the whole-envelope transfer; see
# UnifiedMLATokenToKVPool.get_contiguous_buf_infos.
assert cfg.disaggregation_transfer_backend == "mooncake", (
"--enable-unified-memory with PD disaggregation supports only "
"the mooncake transfer backend; got "
# Constraints of the whole-envelope transfer; see the unified MHA and
# MLA pool get_contiguous_buf_infos implementations.
supported_backends = server_args._unified_memory_pd_transfer_backends()
assert cfg.disaggregation_transfer_backend in supported_backends, (
"--enable-unified-memory with PD disaggregation supports only these "
f"transfer backends: {', '.join(sorted(supported_backends))}; got "
f"{cfg.disaggregation_transfer_backend!r}."
)
assert cfg.pp_size == 1, (
"--enable-unified-memory with PD disaggregation does not support "
"pipeline parallelism (whole-envelope transfer has no per-layer "
"entries to subset)."
)
assert not (
cfg.disaggregation_transfer_backend == "mooncake"
and cfg.speculative_algorithm is not None
), (
"--enable-unified-memory with PD disaggregation does not support "
"speculative decoding with the Mooncake transfer backend."
)
assert not envs.SGLANG_DISABLE_LAZY_COMPACTION.get(), (
"--enable-unified-memory with PD disaggregation requires lazy "
"compaction; unset SGLANG_DISABLE_LAZY_COMPACTION."
Expand All @@ -250,6 +258,15 @@ def handle_unified_memory_pool(server_args: Any) -> None:
"ships host/C4 rows straight from the allocator, bypassing the "
"virtual->physical translation the unified pool needs."
)
assert cfg.disaggregation_decode_retraction_backup != "host_pool", (
"--enable-unified-memory with PD disaggregation does not support "
"--disaggregation-decode-retraction-backup=host_pool; use "
"cpu_tensor (the automatic default for unified pools)."
)
assert not cfg.disaggregation_decode_enable_offload_kvcache, (
"--enable-unified-memory with PD disaggregation does not yet support "
"--disaggregation-decode-enable-offload-kvcache."
)
assert cfg.speculative_algorithm in (None, "DSPARK"), (
"--enable-unified-memory only supports --speculative-algorithm "
"DSPARK (chain draft); other speculative algorithms are not yet "
Expand Down
214 changes: 127 additions & 87 deletions python/sglang/srt/disaggregation/decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,28 @@ def _uses_swa_tail_prealloc(self) -> bool:
and hasattr(self.token_to_kv_pool_allocator, "alloc_extend_swa_tail")
)

def _uses_swa_reservation(self) -> bool:
return (
self._uses_swa_tail_prealloc()
or self.token_to_kv_pool_allocator.prealloc_fits_assumes_reclaim()
)

def _prealloc_reservation_fits(
self,
full_tokens: int,
swa_tokens: int,
*,
full_allocatable_tokens: int,
swa_allocatable_tokens: Optional[int],
) -> bool:
return self.token_to_kv_pool_allocator.prealloc_fits(
self.tree_cache,
full_tokens,
swa_tokens,
full_budget_tokens=full_allocatable_tokens,
swa_budget_tokens=swa_allocatable_tokens,
)

def _release_matched_prefix_lock(self, req: Req) -> None:
if req.swa_prefix_lock_released:
self.tree_cache.dec_lock_ref(req.last_node, req.lock_receipt, skip_swa=True)
Expand All @@ -463,24 +485,16 @@ def _release_matched_prefix_lock(self, req: Req) -> None:
self.tree_cache.dec_lock_ref(req.last_node, req.lock_receipt)

def _reclaim_swa_tail_capacity(
self, swa_tail_len: int, req_id: str
self, swa_tail_len: int, req_id: str, *, full_len: int = 0
) -> Optional[str]:
page_size = self.token_to_kv_pool_allocator.page_size
required = ceil_align(swa_tail_len, page_size)
available = self.token_to_kv_pool_allocator.swa_available_size()
if available < required:
self.tree_cache.evict_for_alloc(
EvictParams(swa_num_tokens=required - available)
)
available = self.token_to_kv_pool_allocator.swa_available_size()

if available < required:
return (
f"SWA eviction insufficient: needed={required}, "
f"available={available}, req={req_id}"
)

return None
allocator = self.token_to_kv_pool_allocator
page_size = allocator.page_size
shortfall = allocator.reclaim_for_prealloc(
self.tree_cache,
ceil_align(full_len, page_size),
ceil_align(swa_tail_len, page_size),
)
return None if shortfall is None else f"{shortfall}, req={req_id}"

# SWA caches expose full-attention accounting through full_* accessors.
def _radix_full_evictable(self) -> int:
Expand Down Expand Up @@ -581,8 +595,8 @@ def _init_kv_manager(self) -> CommonKVManager:
kv_data_mem_kinds += ["VRAM"] * len(device_kv_data_ptrs[c4_layer_num:])
num_draft_entries = 0
if self.draft_token_to_kv_pool is not None:
# We should also transfer draft model kv cache. The indices are
# always shared with a target model.
# Draft KV shares target virtual ids. Unified target KV is transferred
# with physical ids, so it needs a separate draft index vector.
draft_kv_data_ptrs, draft_kv_data_lens, draft_kv_item_lens = (
self.draft_token_to_kv_pool.get_contiguous_buf_infos()
)
Expand Down Expand Up @@ -814,30 +828,40 @@ def _pre_alloc_fill_len(req: Req) -> int:
return len(req.origin_input_ids) + max(len(req.output_ids) - 1, 0)

def _check_if_req_exceed_kv_capacity(self, req: Req) -> bool:
# HiSparse admits up to the host-backed logical capacity.
if self.scheduler.enable_hisparse:
capacity = self.scheduler.tp_worker.model_runner.max_token_pool_size
else:
capacity = self.max_total_num_tokens
input_len = self._rebootstrap_prefill_len(req)
if input_len > capacity:
message = f"Request {req.rid} exceeds the maximum number of tokens: {input_len} > {capacity}"
message = None
allocator = self.token_to_kv_pool_allocator
full_required, swa_required = self._prealloc_required_tokens(req)
if not self._uses_swa_tail_prealloc():
swa_required = full_required
ceiling_fits = allocator.prealloc_ceiling_fits(full_required, swa_required)
if ceiling_fits is False:
message = (
f"Request {req.rid} exceeds the unified FULL/SWA KV byte "
f"budget: full={full_required}, swa={swa_required}"
)
elif ceiling_fits is None:
# HiSparse admits up to the host-backed logical capacity.
capacity = (
self.scheduler.tp_worker.model_runner.max_token_pool_size
if self.scheduler.enable_hisparse
else self.max_total_num_tokens
)
input_len = self._rebootstrap_prefill_len(req)
if input_len > capacity:
message = f"Request {req.rid} exceeds the maximum number of tokens: {input_len} > {capacity}"
elif self._uses_swa_tail_prealloc():
_, swa_required = self._prealloc_required_tokens(req)
swa_capacity = self.token_to_kv_pool_allocator.size_swa
if swa_required > swa_capacity:
message = (
f"Request {req.rid} requires too many SWA KV tokens for "
f"decode preallocation: {swa_required} > {swa_capacity}"
)
if message is not None:
logger.error(message)
prepare_abort(req, message, status_code=HTTPStatus.BAD_REQUEST)
self.scheduler.output_streamer.stream_output([req], req.return_logprob)
return True
if self._uses_swa_tail_prealloc():
_, swa_required = self._prealloc_required_tokens(req)
swa_capacity = self.token_to_kv_pool_allocator.size_swa
if swa_required > swa_capacity:
message = (
f"Request {req.rid} requires too many SWA KV tokens for "
f"decode preallocation: {swa_required} > {swa_capacity}"
)
logger.error(message)
prepare_abort(req, message, status_code=HTTPStatus.BAD_REQUEST)
self.scheduler.output_streamer.stream_output([req], req.return_logprob)
return True
return False

def extend(self, reqs: List[Req], is_retracted: bool = False) -> None:
Expand Down Expand Up @@ -870,8 +894,8 @@ def resume_retracted_reqs(
# allocate memory
resumed_reqs = []
indices_to_remove = set()
uses_swa_tail_prealloc = self._uses_swa_tail_prealloc()
if uses_swa_tail_prealloc:
swa_allocatable_tokens = None
if self._uses_swa_reservation():
full_allocatable_tokens, swa_allocatable_tokens = (
self._swa_aware_allocatable_token_budgets(count_retracted=False)
)
Expand All @@ -888,17 +912,31 @@ def resume_retracted_reqs(
break

full_required, swa_required = self._prealloc_required_tokens(req)
if full_required > full_allocatable_tokens:
break
if uses_swa_tail_prealloc and swa_required > swa_allocatable_tokens:
if not self._prealloc_reservation_fits(
full_required,
swa_required,
full_allocatable_tokens=full_allocatable_tokens,
swa_allocatable_tokens=swa_allocatable_tokens,
):
break

if self.token_to_kv_pool_allocator.prealloc_fits_assumes_reclaim():
full_len, swa_len = self._prealloc_kv_lens(req)
if (
self._reclaim_swa_tail_capacity(swa_len, req.rid, full_len=full_len)
is not None
):
break

resumed_reqs.append(req)
indices_to_remove.add(i)
req.is_retracted = False
self._pre_alloc(req)
full_allocatable_tokens -= full_required
if uses_swa_tail_prealloc:
full_allocatable_tokens = self._allocatable_token_budgets(
count_retracted=False,
extra_reserved_reqs=len(resumed_reqs),
)
if swa_allocatable_tokens is not None:
swa_allocatable_tokens = self._swa_tail_allocatable_token_budget(
count_retracted=False,
extra_reserved_reqs=len(resumed_reqs),
Expand Down Expand Up @@ -1159,8 +1197,8 @@ def pop_preallocated(
)

uses_swa_tail_prealloc = self._uses_swa_tail_prealloc()
swa_allocatable_tokens = 0
if uses_swa_tail_prealloc:
swa_allocatable_tokens = None
if self._uses_swa_reservation():
retractable_swa_tokens = sum(
self._swa_retractable_len(r) for r in self.scheduler.running_batch.reqs
)
Expand Down Expand Up @@ -1352,47 +1390,47 @@ def pop_preallocated(
required_alloc_tokens + self.num_reserved_decode_tokens
)

if (
max(
required_tokens_for_request,
origin_input_len
- prefix_len
+ min(
decode_req.req.sampling_params.max_new_tokens,
CLIP_MAX_NEW_TOKEN,
)
- retractable_tokens,
full_required_for_admission = max(
required_tokens_for_request,
origin_input_len
- prefix_len
+ min(
decode_req.req.sampling_params.max_new_tokens,
CLIP_MAX_NEW_TOKEN,
)
> full_allocatable_tokens
):
if prefix_match is not None and prefix_match.l1_prefix_len > 0:
self._release_matched_prefix_lock(decode_req.req)
break
if required_tokens_for_request > full_allocatable_tokens:
if prefix_match is not None and prefix_match.l1_prefix_len > 0:
self._release_matched_prefix_lock(decode_req.req)
break

- retractable_tokens,
)
swa_required_for_admission = 0
swa_len = required_alloc_tokens
if uses_swa_tail_prealloc:
_, swa_required = self._prealloc_required_tokens(decode_req.req)
_, swa_len = self._prealloc_kv_lens(decode_req.req)
max_new_tokens = min(
decode_req.req.sampling_params.max_new_tokens,
CLIP_MAX_NEW_TOKEN,
)
if (
max(
swa_required,
swa_len + max_new_tokens - retractable_swa_tokens,
)
> swa_allocatable_tokens
):
if prefix_match is not None and prefix_match.l1_prefix_len > 0:
self._release_matched_prefix_lock(decode_req.req)
break
swa_required_for_admission = max(
swa_required,
swa_len + max_new_tokens - retractable_swa_tokens,
)
elif swa_allocatable_tokens is not None:
swa_required_for_admission = full_required_for_admission

if not self._prealloc_reservation_fits(
full_required_for_admission,
swa_required_for_admission,
full_allocatable_tokens=full_allocatable_tokens,
swa_allocatable_tokens=swa_allocatable_tokens,
):
if prefix_match is not None and prefix_match.l1_prefix_len > 0:
self._release_matched_prefix_lock(decode_req.req)
break

if swa_allocatable_tokens is not None:
reclaim_error = self._reclaim_swa_tail_capacity(
swa_len, decode_req.req.rid
swa_len,
decode_req.req.rid,
full_len=required_alloc_tokens,
)
if reclaim_error is not None:
if prefix_match is not None and prefix_match.l1_prefix_len > 0:
Expand Down Expand Up @@ -1431,7 +1469,7 @@ def pop_preallocated(
extra_reserved_reqs=len(preallocated_reqs) + 1,
hicache_reserved_tokens=reserved_restore_tokens,
)
if uses_swa_tail_prealloc:
if swa_allocatable_tokens is not None:
swa_allocatable_tokens = self._swa_tail_allocatable_token_budget(
retractable_tokens=retractable_tokens,
retractable_swa_tokens=retractable_swa_tokens,
Expand All @@ -1442,6 +1480,9 @@ def pop_preallocated(

page_size = self.token_to_kv_pool_allocator.page_size
kv_transfer_page_size = page_size
raw_kv_indices = self.req_to_token_pool.req_to_token[
decode_req.req.kv.req_pool_idx
][total_prefix_len:origin_input_len]
if self.scheduler.enable_hisparse:
# Direct-to-host sends host/C4 rows; keep allocator.page_size
# logical and use the compressed page size only for these indices.
Expand All @@ -1453,12 +1494,9 @@ def pop_preallocated(
kv_indices = dst_kv_indices[: origin_input_len - prefix_len]
else:
# Only send delta indices (beyond prefix) to prefill.
kv_indices = self.req_to_token_pool.req_to_token[
decode_req.req.kv.req_pool_idx
][total_prefix_len:origin_input_len]
kv_indices = (
self.token_to_kv_pool_allocator.translate_kv_indices_for_transfer(
kv_indices
raw_kv_indices
)
)

Expand Down Expand Up @@ -1735,7 +1773,7 @@ def _allocatable_token_budgets(
# HiSparse pre-alloc only allocates logical indices, so the
# logical pool is the binding constraint for admission control.
available_size = logical_allocator.available_size()
elif self._uses_swa_tail_prealloc():
elif self._uses_swa_reservation():
available_size = self.token_to_kv_pool_allocator.full_available_size()
if get_disagg().disaggregation_decode_enable_radix_cache:
available_size += self._radix_full_evictable()
Expand Down Expand Up @@ -1802,8 +1840,10 @@ def _swa_tail_allocatable_token_budget(
# pool) over-reserves SWA in steady state. Cap by the actual
# remaining headroom up to per-req window cap.
window_size = self.scheduler.sliding_window_size or 0
swa_total = self.token_to_kv_pool_allocator.size_swa
swa_available = self.token_to_kv_pool_allocator.swa_available_size()
allocator = self.token_to_kv_pool_allocator
_, (swa_total, swa_available) = allocator.swa_capacity_and_available(
full_capacity=allocator.size_full, swa_capacity=allocator.size_swa
)
# Per-request SWA ring: cached prefixes still report swa_evictable, but
# evicting them frees no ring space.
swa_evictable = (
Expand Down
Loading
Loading