[PD] Support --enable-unified-memory with PD disaggregation (kimi-linear MLA hybrid-Mamba) (#33362)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cheng Wan
2026-08-10 16:07:58 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent 8a7c8a72d6
commit ceeaec2078
15 changed files with 835 additions and 51 deletions
+29 -3
View File
@@ -327,6 +327,8 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
self.transfer_queue = transfer_queue
self.tree_cache = tree_cache
self.gloo_group = gloo_group
# Destinations visible to prefill but not yet on the transfer queue.
self._num_published_destinations = 0
self.tp_rank = tp_rank
self.tp_size = tp_size
self.dp_size = dp_size
@@ -1151,14 +1153,21 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
kv_indices = self.req_to_token_pool.req_to_token[
decode_req.req.req_pool_idx
][total_prefix_len:origin_input_len]
kv_indices = (
self.token_to_kv_pool_allocator.translate_kv_indices_for_transfer(
kv_indices
)
)
seq_len = origin_input_len
def _mamba_payload():
return [
self.req_to_token_pool.req_index_to_mamba_index_mapping[
decode_req.req.req_pool_idx
]
self.req_to_token_pool.translate_mamba_indices(
self.req_to_token_pool.req_index_to_mamba_index_mapping[
decode_req.req.req_pool_idx
]
)
.cpu()
.numpy()
]
@@ -1306,6 +1315,7 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
decode_req.kv_receiver,
decode_req.req.build_rebootstrap_payload(),
)
self._num_published_destinations += 1
preallocated_reqs.append(decode_req)
indices_to_remove.add(i)
decode_req.req.time_stats.set_decode_transfer_queue_entry_time()
@@ -1316,6 +1326,18 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
return preallocated_reqs, failed_reqs
@property
def has_published_destinations(self) -> bool:
"""Whether any destination address is visible to prefill but not yet
protected by the transfer queue."""
return self._num_published_destinations > 0
def note_destinations_queued(self, count: int) -> None:
"""Hand `count` published destinations over to the transfer queue."""
self._num_published_destinations = max(
0, self._num_published_destinations - count
)
@property
def num_tokens_pre_allocated(self):
return sum(
@@ -1798,6 +1820,10 @@ class DecodeTransferQueue(DecodeHiCacheTransferMixin):
def extend(self, decode_reqs: List[DecodeRequest]) -> None:
self.queue.extend(decode_reqs)
# This queue now covers them.
prealloc_queue = self.scheduler.disagg_decode_prealloc_queue
if prealloc_queue is not None:
prealloc_queue.note_destinations_queued(len(decode_reqs))
def _commit_transfer_to_req(self, decode_req: DecodeRequest):
idx = decode_req.metadata_buffer_index
@@ -8,7 +8,7 @@ import struct
import threading
import time
from collections import defaultdict
from typing import List, Optional, Tuple, Union
from typing import List, Optional, Set, Tuple, Union
import numpy as np
import numpy.typing as npt
@@ -59,7 +59,7 @@ from sglang.srt.observability.trace import (
TraceReqContext,
trace_set_thread_info,
)
from sglang.srt.runtime_context import get_parallel, get_schedule
from sglang.srt.runtime_context import get_memory, get_parallel, get_schedule
from sglang.srt.server_args import ServerArgs
from sglang.srt.utils.network import NetworkAddress
@@ -282,35 +282,41 @@ class MooncakeKVManager(CommonKVManager):
def init_engine(self):
self.engine = get_mooncake_transfer_engine()
def register_buffer_to_engine(self):
# Batch register KV data buffers
if self.kv_args.kv_data_ptrs and self.kv_args.kv_data_lens:
self.engine.batch_register(
self.kv_args.kv_data_ptrs, self.kv_args.kv_data_lens
)
def _registerable_regions(self) -> List[Tuple[int, int]]:
"""(ptr, len) regions to (de)register, exact duplicates removed.
# Batch register auxiliary data buffers
if self.kv_args.aux_data_ptrs and self.kv_args.aux_data_lens:
self.engine.batch_register(
self.kv_args.aux_data_ptrs, self.kv_args.aux_data_lens
)
Deduped because the unified memory pool reports one raw buffer as both
its KV and its mamba state component, and double registration fails in
the engine.
"""
regions: List[Tuple[int, int]] = []
seen: Set[Tuple[int, int]] = set()
def add(ptrs: List[int], lens: List[int]) -> None:
for ptr, length in zip(ptrs or [], lens or []):
if (ptr, length) not in seen:
seen.add((ptr, length))
regions.append((ptr, length))
add(self.kv_args.kv_data_ptrs, self.kv_args.kv_data_lens)
add(self.kv_args.aux_data_ptrs, self.kv_args.aux_data_lens)
for ptrs, lens in zip(
self.kv_args.state_data_ptrs, self.kv_args.state_data_lens
):
if ptrs and lens:
self.engine.batch_register(ptrs, lens)
add(ptrs, lens)
return regions
def register_buffer_to_engine(self):
regions = self._registerable_regions()
if regions:
ptrs, lens = zip(*regions)
self.engine.batch_register(list(ptrs), list(lens))
def deregister_buffer_to_engine(self):
if self.kv_args.kv_data_ptrs:
self.engine.batch_deregister(self.kv_args.kv_data_ptrs)
if self.kv_args.aux_data_ptrs:
self.engine.batch_deregister(self.kv_args.aux_data_ptrs)
for ptrs in self.kv_args.state_data_ptrs or []:
if ptrs:
self.engine.batch_deregister(ptrs)
regions = self._registerable_regions()
if regions:
ptrs, _ = zip(*regions)
self.engine.batch_deregister(list(ptrs))
if hasattr(self, "connection_pool"):
with self.connection_lock:
@@ -775,6 +781,52 @@ class MooncakeKVManager(CommonKVManager):
# compared to using multiple threads
return process_layers(layers_params)
def _validate_envelope_kv_layout(
self,
dst_kv_ptrs: list[int],
dst_kv_item_len: Optional[int],
dst_attn_tp_size: Optional[int] = None,
) -> None:
"""Reject a peer whose KV registration shape differs from ours.
The unified memory pool registers ONE whole-envelope region and
addresses the destination as ``dst_ptr + page_id * item_len`` using OUR
``item_len``, so a peer on a different page size / spec, or without
unified memory, would take envelope-sized blocks at the wrong offsets.
Must run before the first RDMA write.
Scoped to unified memory by config, not by region count: a non-unified
PP stage owning a single full-attention layer also registers one region,
and `_send_kvcache_generic` pairs that with the peer by layer id.
"""
if not get_memory().enable_unified_memory:
return
if dst_attn_tp_size is not None and self.attn_tp_size != dst_attn_tp_size:
# The unified mamba state ships as one whole-slot envelope with no
# per-tensor dims, so `_send_mamba_state_slice` cannot reslice it and
# silently falls back to an unsliced copy. Reject here, before any KV
# is written, rather than in `maybe_send_extra` afterwards.
raise RuntimeError(
"--enable-unified-memory does not support different prefill / "
f"decode attention TP sizes (prefill={self.attn_tp_size}, "
f"decode={dst_attn_tp_size}): the whole-envelope state cannot "
"be TP-resliced."
)
src_item_lens = self.kv_args.kv_item_lens
if (
len(src_item_lens) != 1
or len(dst_kv_ptrs) != 1
or dst_kv_item_len is None
or src_item_lens[0] != dst_kv_item_len
):
raise RuntimeError(
"PD KV layout mismatch on the whole-envelope path: prefill has "
f"{len(src_item_lens)} KV region(s) with item_lens="
f"{src_item_lens}, decode has {len(dst_kv_ptrs)} with item_len="
f"{dst_kv_item_len}. With --enable-unified-memory both sides "
"must enable it and use the same page size and model spec."
)
def send_kvcache(
self,
mooncake_session_id: str,
@@ -784,7 +836,12 @@ class MooncakeKVManager(CommonKVManager):
executor: concurrent.futures.ThreadPoolExecutor,
dst_layer_ids: Optional[List[int]] = None,
dst_device_kv_indices: Optional[npt.NDArray[np.int32]] = None,
dst_kv_item_len: Optional[int] = None,
dst_attn_tp_size: Optional[int] = None,
):
self._validate_envelope_kv_layout(
dst_kv_ptrs, dst_kv_item_len, dst_attn_tp_size
)
dst_device_kv_ptrs = None
if dst_device_kv_indices is not None:
compression_ratios = self.kv_args.mla_compression_ratios
@@ -1243,6 +1300,17 @@ class MooncakeKVManager(CommonKVManager):
)
if st == StateType.MAMBA:
if (not src_dim_per_tensor or not dst_dim_per_tensor) and list(
src_item_lens
) != list(dst_item_lens):
raise RuntimeError(
"Mamba state layouts differ between prefill and decode "
f"(src item_lens={src_item_lens}, dst item_lens="
f"{dst_item_lens}) and no per-tensor dim metadata is "
"available to reslice. With --enable-unified-memory, "
"prefill and decode must both enable it and use equal "
"attention TP sizes."
)
if (
target_rank_registration_info is not None
and self.attn_tp_size
@@ -1693,6 +1761,8 @@ class MooncakeKVManager(CommonKVManager):
executor,
dst_layer_ids=target_rank_registration_info.dst_kv_layer_ids,
dst_device_kv_indices=chunked_dst_device_kv_indice,
dst_kv_item_len=target_rank_registration_info.dst_kv_item_len,
dst_attn_tp_size=target_rank_registration_info.dst_attn_tp_size,
)
elif (
self.enable_staging
+30 -3
View File
@@ -790,6 +790,7 @@ class SchedulerDisaggregationPrefillMixin:
# Optimistic bootstrap can fail while this overlapped chunk is
# already running. Drop aborted chunks instead of sending KV.
if is_aborted(req):
self.clear_pending_chunk_send(req)
advance_logprob_pt(i, req)
req.time_stats.set_last_chunked_prefill_finish_time()
continue
@@ -980,7 +981,17 @@ class SchedulerDisaggregationPrefillMixin:
return transferred_rids
def clear_pending_chunk_send(self: Scheduler, req: Req) -> None:
"""Drop `req` from the sent-but-unconcluded chunk set.
Every path that retires a request without a `last_chunk=True` send must
call this: a stale entry holds the unified-memory compaction gate closed
for the process lifetime.
"""
self.disagg_prefill_pending_chunk_rids.discard(req.rid)
def handle_bootstrap_failure(self: Scheduler, req: Req) -> None:
self.clear_pending_chunk_send(req)
error_message = (
f"Prefill bootstrap failed for request rank={self.ps.tp_rank} "
f"{req.rid=} {req.bootstrap_room=}"
@@ -1178,9 +1189,11 @@ class SchedulerDisaggregationPrefillMixin:
def _mamba_payload():
return [
self.req_to_token_pool.req_index_to_mamba_index_mapping[
req.req_pool_idx
]
self.req_to_token_pool.translate_mamba_indices(
self.req_to_token_pool.req_index_to_mamba_index_mapping[
req.req_pool_idx
]
)
.cpu()
.numpy()
]
@@ -1287,6 +1300,13 @@ class SchedulerDisaggregationPrefillMixin:
kv_indices = self.req_to_token_pool.req_to_token[
req.req_pool_idx, seg_start:seg_end
]
# Unified memory: req_to_token holds VIRTUAL ids; the transfer needs
# physical ones. Per segment, since each is its own gather.
kv_indices = (
self.token_to_kv_pool_allocator.translate_kv_indices_for_transfer(
kv_indices
)
)
page_indices = kv_to_page_indices(kv_indices, page_size)
segment_is_last = last_chunk and is_final_segment
if not req.disagg_kv_sender.should_send_kv_chunk(
@@ -1299,6 +1319,12 @@ class SchedulerDisaggregationPrefillMixin:
num_kv_tokens=seg_end - seg_start,
)
req.start_send_idx = end_idx
# A last chunk needs no entry: every `last_chunk=True` call site has
# already put the request on `disagg_prefill_inflight_queue`.
if last_chunk:
self.disagg_prefill_pending_chunk_rids.discard(req.rid)
else:
self.disagg_prefill_pending_chunk_rids.add(req.rid)
def optimistic_release_and_requeue(self: Scheduler, req: Req) -> None:
"""Release KV cache and requeue an optimistic prefill request."""
@@ -1308,6 +1334,7 @@ class SchedulerDisaggregationPrefillMixin:
req.reset_for_retract()
req.output_ids = array("q")
req.start_send_idx = 0
self.clear_pending_chunk_send(req) # re-sends from scratch
req.tmp_end_idx = -1
req.disagg_decode_prefix_len = 0
req.early_send_prefix_end = None
+44
View File
@@ -111,6 +111,50 @@ class DisaggregationMode(Enum):
return "unified"
def unified_memory_disagg_move_gate(scheduler):
"""Compaction move gate for a PD node running the unified memory pool.
Returns a predicate that is True only when no transfer can be in flight, so
compaction never relocates a page the RDMA engine is reading or writing.
Safe to read this state from here: every mover runs on the scheduler thread.
A page is exposed from the moment its address reaches the peer until the
transfer concludes, and for part of that lifetime the request is in NEITHER
end's queue -- so queue emptiness alone is not enough:
- PREFILL: scheduling the final chunk clears `chunked_req` while earlier
chunks may still be draining, and the request only reaches the inflight
queue later, in the result path.
- DECODE: `pop_preallocated` publishes one request's destinations and keeps
allocating for the next, whose allocation can urgently flush the peer
sub-allocator; the batch reaches the transfer queue only after the loop.
"""
if scheduler.disaggregation_mode == DisaggregationMode.PREFILL:
def prefill_gate() -> bool:
return not (
scheduler.disagg_prefill_inflight_queue
or scheduler.disagg_prefill_pending_chunk_rids
)
return prefill_gate
if scheduler.disaggregation_mode == DisaggregationMode.DECODE:
def decode_gate() -> bool:
return not (
scheduler.disagg_decode_transfer_queue.queue
or scheduler.disagg_decode_prealloc_queue.has_published_destinations
)
return decode_gate
raise ValueError(
"unified_memory_disagg_move_gate: scheduler is not a PD node "
f"(mode={scheduler.disaggregation_mode})"
)
#########################
# Synchronization
#########################
+13 -1
View File
@@ -25,7 +25,7 @@ from collections import deque
from contextlib import contextmanager, nullcontext
from functools import partial
from http import HTTPStatus
from typing import TYPE_CHECKING, Any, Deque, Dict, List, Optional, Tuple, Union
from typing import TYPE_CHECKING, Any, Deque, Dict, List, Optional, Set, Tuple, Union
from sglang.srt.runtime_context import (
get_device,
@@ -90,6 +90,7 @@ from sglang.srt.disaggregation.utils import (
TransferBackend,
get_dsa_seed_metadata_dim,
prepare_abort,
unified_memory_disagg_move_gate,
)
from sglang.srt.distributed import get_pp_group, get_world_group
from sglang.srt.distributed.parallel_state import get_tp_group
@@ -1396,9 +1397,19 @@ class Scheduler(
)
# The prefill requests that are in the middle of kv sending
self.disagg_prefill_inflight_queue: List[Req] = []
# Requests with a sent chunk that are not yet on the inflight queue.
self.disagg_prefill_pending_chunk_rids: Set[str] = set()
self.enable_staging = envs.SGLANG_DISAGG_STAGING_BUFFER.get()
if (
self.enable_unified_memory
and self.disaggregation_mode != DisaggregationMode.NULL
):
self.token_to_kv_pool_allocator.set_disagg_move_gate(
unified_memory_disagg_move_gate(self)
)
# Init mm receiver for EPD disaggregation mode
if get_disagg().language_only and get_disagg().encoder_transfer_backend in [
"zmq_to_scheduler",
@@ -2928,6 +2939,7 @@ class Scheduler(
req.time_stats.trace_ctx.abort(abort_info={"reason": "Aborted"})
req.to_finish = None
if self.disaggregation_mode == DisaggregationMode.PREFILL:
self.clear_pending_chunk_send(req)
req.disagg_kv_sender.abort()
maybe_release_metadata_buffer(
req, self.req_to_metadata_buffer_idx_allocator
@@ -82,6 +82,16 @@ class BaseTokenToKVPoolAllocator(abc.ABC):
(0,), dtype=self.release_pages.dtype, device=self.device
)
def translate_kv_indices_for_transfer(
self, kv_indices: torch.Tensor
) -> torch.Tensor:
"""Token ids as the PD-disaggregation transfer engine addresses them.
Identity here: a static pool's token ids index its registered buffers
directly. Virtual-id pools must override.
"""
return kv_indices
def get_cpu_copy(self, indices, mamba_indices=None):
# FIXME: reuse the get_cpu_copy after paged allocator is implemented
raise NotImplementedError()
@@ -353,17 +353,29 @@ class KVCacheConfigurator:
# Unified-pool fast path: build req_to_token + token_to_kv pool + allocator
# from one byte buffer, then return. Gated to the target worker
# (req_to_token_pool is None); supports hybrid Mamba and hybrid SWA (not DSV4).
if (
get_memory().enable_unified_memory
and get_disagg().disaggregation_mode == "null"
and req_to_token_pool is None
):
if get_memory().enable_unified_memory and req_to_token_pool is None:
pd_enabled = get_disagg().disaggregation_mode != "null"
if self.mambaish_config is not None:
if pd_enabled and not self.use_mla_backend:
raise ValueError(
"--enable-unified-memory with PD disaggregation "
"currently supports only MLA hybrid-Mamba models "
"(e.g. kimi-linear); this model uses the MHA full-"
"attention pool. Drop --enable-unified-memory or run "
"without PD disaggregation."
)
bundle = self._init_unified_mamba_pools(
max_num_reqs=sizes.max_running_requests,
max_total_num_tokens=sizes.max_total_num_tokens,
)
elif self.is_hybrid_swa and not is_deepseek_v4(self.model_config.hf_config):
if pd_enabled:
raise ValueError(
"--enable-unified-memory with PD disaggregation does "
"not support hybrid-SWA models yet (no whole-envelope "
"transfer scheme for the SWA sub-pool). Drop "
"--enable-unified-memory or run without PD."
)
bundle = self._init_unified_swa_pools(
max_num_reqs=sizes.max_running_requests,
full_max_total_num_tokens=sizes.full_max_total_num_tokens,
@@ -564,6 +576,11 @@ class KVCacheConfigurator:
speculative_num_draft_tokens=get_spec().speculative_num_draft_tokens,
disable_overlap_schedule=get_schedule().disable_overlap_schedule,
need_sort=get_disagg().disaggregation_mode in ("decode", "prefill"),
decode_pre_alloc_size=(
get_disagg().disaggregation_decode_extra_slots
if get_disagg().disaggregation_mode == "decode"
else 0
),
mamba_full_memory_ratio=get_schedule().mamba_full_memory_ratio,
# Overlap mode: the allocator's `free` drops a wait_stream(forward_stream)
# barrier so eager compaction serializes after the in-flight forward's
@@ -25,7 +25,7 @@ from __future__ import annotations
import inspect
import logging
import os
from typing import Dict, List, Optional, Set, Tuple
from typing import Callable, Dict, List, Optional, Set, Tuple
import torch
from torch.profiler import record_function
@@ -218,6 +218,8 @@ class MultiEndedAllocator(BaseTokenToKVPoolAllocator):
_STATS_INSTANCES.add(self)
_install_signal_handlers_once()
self.live_page_count = 0
# While this returns False, `_flush` must not relocate any page.
self.disagg_move_gate: Optional[Callable[[], bool]] = None
self._latest_forward_done_event: Optional[torch.cuda.Event] = None
# Most-recent forward's (done_event, out_cache_loc_virtual) for `_flush`'s
# write-race check. Single slot: at most ONE forward in flight per call site.
@@ -393,6 +395,12 @@ class MultiEndedAllocator(BaseTokenToKVPoolAllocator):
peer = self._peer
if peer is None or not peer.lazy_compaction:
return 0
if peer.disagg_move_gate is not None and not peer.disagg_move_gate():
# The peer cannot compact while a PD transfer is in flight, so these
# holes are not realizable. Crediting them would let the scheduler
# admit work that `_flush_peer_for_alloc` then cannot satisfy, and
# the caller treats a failed alloc as a memory-estimation bug.
return 0
return len(peer._free_phys_pages) * peer.entry_bytes_per_page
def schedulable_available_size(self) -> int:
@@ -1058,6 +1066,10 @@ class MultiEndedAllocator(BaseTokenToKVPoolAllocator):
self._compact_pending_impl(freed_physical_pages)
def _compact_pending_impl(self, freed_physical_pages: torch.Tensor) -> None:
assert self.disagg_move_gate is None, (
f"_compact_pending({self.sub_pool_name!r}): eager compaction ran with "
"a PD-disaggregation move gate installed; PD requires lazy_compaction."
)
freed_set = set(int(x) for x in freed_physical_pages.tolist())
if not freed_set:
return
@@ -1440,6 +1452,9 @@ class MultiEndedAllocator(BaseTokenToKVPoolAllocator):
"""
if not self.lazy_compaction:
return 0
if self.disagg_move_gate is not None and not self.disagg_move_gate():
# Holes stay in the free list; the next flush picks them up.
return 0
self._stats_n_flush_calls += 1
with record_function("MultiEndedAlloc._flush"):
self._drain_pending_reuse(urgent=urgent)
@@ -1936,6 +1951,26 @@ class UnifiedMambaTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
to the physical translate when `kernel_page_multiplier == 1` (MHA)."""
return self.full_attn_allocator.translate_kv_loc_dense(loc, out=out)
def translate_kv_indices_for_transfer(
self, kv_indices: torch.Tensor
) -> torch.Tensor:
"""Virtual TOKEN ids -> PHYSICAL token ids for the PD transfer engine.
PHYSICAL, not dense: the transfer registers page ENVELOPES (see
`UnifiedMLATokenToKVPool.get_contiguous_buf_infos`).
"""
return self.full_attn_allocator.translate_kv_loc(kv_indices.to(torch.int64))
def set_disagg_move_gate(self, gate: Callable[[], bool]) -> None:
"""Install the PD-disaggregation move gate on both sub-allocators."""
assert self.lazy_compaction, (
"PD disaggregation with the unified memory pool requires lazy "
"compaction (eager free-path compaction moves pages under "
"in-flight transfers)."
)
self.full_attn_allocator.disagg_move_gate = gate
self.mamba_allocator.disagg_move_gate = gate
def is_slot_allocated(self, slot: int) -> bool:
return self.full_attn_allocator.is_slot_allocated(slot)
@@ -42,6 +42,7 @@ from sglang.srt.mem_cache.layout.page_major import (
build_page_major_mha_views,
)
from sglang.srt.mem_cache.memory_pool import (
HybridLinearKVPool,
HybridReqToTokenPool,
MambaPool,
MHATokenToKVPool,
@@ -663,6 +664,20 @@ class UnifiedMLATokenToKVPool(MLATokenToKVPool):
def get_kv_size_bytes(self):
return 0 # UnifiedKVPool logs the total; per-sub-pool would double-count
def get_contiguous_buf_infos(self):
"""PD-transfer registration: ONE entry, the raw buffer, addressed as
``raw_ptr + physical_page_id * page_envelope_bytes``.
The transfer item is the whole page envelope (all layers of one page)
rather than a per-layer region, because the per-layer dense views
overlap and index in dense ids. Both sides must therefore build the
pool with identical specs.
"""
# The address formula omits the anchor; a nonzero one would mis-address.
assert self._unified_buffer.anchor_bytes(self._sub_pool_name) == 0
raw = self._unified_buffer._raw
return [raw.data_ptr()], [raw.numel()], [self._page_bytes]
def move_kv_cache(self, tgt_loc: torch.Tensor, src_loc: torch.Tensor):
"""Relocate whole page envelopes.
@@ -812,6 +827,31 @@ class UnifiedMambaPool(MambaPool):
# Physical-slot copy used by the allocator's `_compact_pending`.
MambaPool.copy_from(self, src_index, dst_index)
# -- PD state transfer (StateType.MAMBA) --
# The transfer item is the whole per-slot envelope, addressed as
# `raw_ptr + physical_slot * entry_bytes`. An envelope cannot be TP-resliced
# or PP-subset, so the per-tensor metadata below stays empty and both sides
# must build identical mamba specs (equal attn TP, pp=1).
def get_contiguous_buf_infos(self):
# The address formula omits the anchor; a nonzero one would mis-address.
assert self._unified_buffer.anchor_bytes(self._sub_pool_name) == 0
spec = self._unified_buffer.mamba_spec(self._sub_pool_name)
raw = self._unified_buffer._raw
return [raw.data_ptr()], [raw.numel()], [spec.entry_bytes()]
def get_state_dim_per_tensor(self):
return []
def get_state_layer_ids(self):
return []
def get_state_slice_outer_counts(self):
return []
def get_state_conv_shard_groups(self):
return []
class UnifiedMambaSlotAllocator:
"""Mamba slot allocator (PHYSICAL view) for the unified memory pool.
@@ -929,6 +969,7 @@ class UnifiedHybridReqToTokenPool(HybridReqToTokenPool):
speculative_num_draft_tokens: Optional[int] = None,
enable_overlap_schedule: bool = True,
start_layer: Optional[int] = None,
pre_alloc_size: int = 0,
):
self._unified_buffer = unified_buffer
self._mamba_sub_pool_name = mamba_sub_pool_name
@@ -936,7 +977,10 @@ class UnifiedHybridReqToTokenPool(HybridReqToTokenPool):
unified_buffer.max_slots(mamba_sub_pool_name) - 1
) # reserve slot 0
super().__init__(
size=size,
# `DecodeReqToTokenPool` semantics: rows cover the preallocated
# requests too, while `self.size` (rebound below) stays the
# running-request cap the scheduler and leak invariant expect.
size=size + pre_alloc_size,
mamba_size=self._shared_mamba_size,
mamba_spec_state_size=mamba_spec_state_size,
max_context_len=max_context_len,
@@ -949,6 +993,8 @@ class UnifiedHybridReqToTokenPool(HybridReqToTokenPool):
enable_overlap_schedule=enable_overlap_schedule,
start_layer=start_layer,
)
self.size = size
self.pre_alloc_size = pre_alloc_size
def _init_mamba_pool(
self,
@@ -1013,6 +1059,17 @@ class UnifiedHybridReqToTokenPool(HybridReqToTokenPool):
return self.mamba_allocator.translate(virtual_ids).to(torch.int32)
class UnifiedHybridLinearKVPool(HybridLinearKVPool):
"""`HybridLinearKVPool` over unified sub-pools (full = Unified{MLA,MHA},
mamba = UnifiedMambaPool)."""
def get_kv_layer_ids(self):
# Empty: the KV component is one whole-envelope entry, so there are no
# per-layer entries to pair by layer id (the sender falls back to
# positional pairing).
return []
# ---------------------------------------------------------------------------
# Factory
# ---------------------------------------------------------------------------
@@ -1054,9 +1111,9 @@ def init_unified_mamba_pools(
mamba_full_memory_ratio: Optional[float] = None, # informational only
forward_stream: Optional[torch.cuda.Stream] = None,
lazy_compaction: bool = False,
decode_pre_alloc_size: int = 0,
) -> UnifiedPoolBundle:
"""Build the Mamba-hybrid unified-memory-pool stack."""
from sglang.srt.mem_cache.memory_pool import HybridLinearKVPool
from sglang.srt.mem_cache.multi_ended_allocator import (
UnifiedMambaTokenToKVPoolAllocator,
)
@@ -1132,6 +1189,7 @@ def init_unified_mamba_pools(
speculative_num_draft_tokens=speculative_num_draft_tokens,
enable_overlap_schedule=not disable_overlap_schedule,
start_layer=start_layer,
pre_alloc_size=decode_pre_alloc_size,
)
if use_mla_backend:
# start_layer stays 0: HybridLinearKVPool patches layer ids to the dense
@@ -1153,7 +1211,7 @@ def init_unified_mamba_pools(
full_attn_layer_ids_for_pool = (
[0] if is_draft_worker else list(full_attention_layer_ids)
)
token_to_kv_pool = HybridLinearKVPool(
token_to_kv_pool = UnifiedHybridLinearKVPool(
page_size=page_size,
size=max_total_num_tokens,
dtype=kv_cache_dtype,
+23 -3
View File
@@ -8007,9 +8007,29 @@ class ServerArgs:
def _handle_unified_memory_pool(self):
if not self.enable_unified_memory:
return
assert self.disaggregation_mode == "null", (
"--enable-unified-memory is not yet compatible with PD " "disaggregation."
)
if self.disaggregation_mode != "null":
# Constraints of the whole-envelope transfer; see
# UnifiedMLATokenToKVPool.get_contiguous_buf_infos.
assert self.disaggregation_transfer_backend == "mooncake", (
"--enable-unified-memory with PD disaggregation supports only "
"the mooncake transfer backend; got "
f"{self.disaggregation_transfer_backend!r}."
)
assert self.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 envs.SGLANG_DISABLE_LAZY_COMPACTION.get(), (
"--enable-unified-memory with PD disaggregation requires lazy "
"compaction; unset SGLANG_DISABLE_LAZY_COMPACTION."
)
assert not self.enable_hisparse, (
"--enable-unified-memory with PD disaggregation is not compatible "
"with --enable-hisparse: the decode-side HiSparse prealloc path "
"ships host/C4 rows straight from the allocator, bypassing the "
"virtual->physical translation the unified pool needs."
)
assert self.speculative_algorithm in (None, "DSPARK"), (
"--enable-unified-memory only supports --speculative-algorithm "
"DSPARK (chain draft); other speculative algorithms are not yet "