[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:
co-authored by
Claude Fable 5
parent
8a7c8a72d6
commit
ceeaec2078
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
#########################
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 "
|
||||
|
||||
Reference in New Issue
Block a user