[Feature] Unified memory: support decode context parallelism for Kimi-Linear (#36890)
This commit is contained in:
@@ -174,11 +174,7 @@ def update_kv_lens_and_indices(
|
|||||||
local_kv_indices_offsets = local_kv_indices_start + offsets
|
local_kv_indices_offsets = local_kv_indices_start + offsets
|
||||||
|
|
||||||
kv_values = tl.load(kv_indices + kv_indice_offsets, mask=mask)
|
kv_values = tl.load(kv_indices + kv_indice_offsets, mask=mask)
|
||||||
tl.store(
|
tl.store(local_kv_indices + local_kv_indices_offsets, kv_values, mask=mask)
|
||||||
local_kv_indices + local_kv_indices_offsets,
|
|
||||||
kv_values // dcp_world_size,
|
|
||||||
mask=mask,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -88,13 +88,15 @@ def set_mla_kv_buffer_kernel(
|
|||||||
_TMA_BULK_STORE_MIN_LOCS = 768
|
_TMA_BULK_STORE_MIN_LOCS = 768
|
||||||
|
|
||||||
|
|
||||||
def set_mla_kv_buffer_triton(
|
def _set_mla_kv_buffer_impl(
|
||||||
kv_buffer: torch.Tensor,
|
kv_buffer: torch.Tensor,
|
||||||
loc: torch.Tensor,
|
loc: torch.Tensor,
|
||||||
cache_k_nope: torch.Tensor,
|
cache_k_nope: torch.Tensor,
|
||||||
cache_k_rope: torch.Tensor,
|
cache_k_rope: torch.Tensor,
|
||||||
*,
|
*,
|
||||||
reserved_skip_index: int = 0,
|
reserved_skip_index: int,
|
||||||
|
dcp_world_size: int,
|
||||||
|
dcp_rank: int,
|
||||||
):
|
):
|
||||||
"""Dispatch MLA paged-KV scatter writes to the fastest available path.
|
"""Dispatch MLA paged-KV scatter writes to the fastest available path.
|
||||||
|
|
||||||
@@ -121,6 +123,9 @@ def set_mla_kv_buffer_triton(
|
|||||||
|
|
||||||
Writes targeting ``reserved_skip_index`` are skipped. Slot 0 is reserved
|
Writes targeting ``reserved_skip_index`` are skipped. Slot 0 is reserved
|
||||||
for CUDA-graph padding by default; pass -1 to disable skipping.
|
for CUDA-graph padding by default; pass -1 to disable skipping.
|
||||||
|
|
||||||
|
Shared body of the two entry points below; the owner rule reaches it as
|
||||||
|
``1, 0`` (nothing to select) or as the live topology.
|
||||||
"""
|
"""
|
||||||
from sglang.kernels.ops.kvcache.set_mla_kv_buffer import (
|
from sglang.kernels.ops.kvcache.set_mla_kv_buffer import (
|
||||||
can_use_set_mla_kv_buffer,
|
can_use_set_mla_kv_buffer,
|
||||||
@@ -136,7 +141,7 @@ def set_mla_kv_buffer_triton(
|
|||||||
n_loc >= _TMA_BULK_STORE_MIN_LOCS
|
n_loc >= _TMA_BULK_STORE_MIN_LOCS
|
||||||
and is_arch_support_pdl()
|
and is_arch_support_pdl()
|
||||||
and can_use_set_mla_kv_buffer(nope_bytes, rope_bytes)
|
and can_use_set_mla_kv_buffer(nope_bytes, rope_bytes)
|
||||||
and not get_parallel().dcp_enabled
|
and dcp_world_size == 1
|
||||||
):
|
):
|
||||||
jit_set_mla_kv_buffer(
|
jit_set_mla_kv_buffer(
|
||||||
kv_buffer,
|
kv_buffer,
|
||||||
@@ -170,12 +175,54 @@ def set_mla_kv_buffer_triton(
|
|||||||
nope_dim,
|
nope_dim,
|
||||||
rope_dim,
|
rope_dim,
|
||||||
BLOCK=BLOCK,
|
BLOCK=BLOCK,
|
||||||
DCP_RANK=get_parallel().attn_dcp_rank,
|
DCP_RANK=dcp_rank,
|
||||||
DCP_WORLD_SIZE=get_parallel().attn_dcp_size,
|
DCP_WORLD_SIZE=dcp_world_size,
|
||||||
**pdl_kwargs,
|
**pdl_kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def set_mla_kv_buffer_triton(
|
||||||
|
kv_buffer: torch.Tensor,
|
||||||
|
loc: torch.Tensor,
|
||||||
|
cache_k_nope: torch.Tensor,
|
||||||
|
cache_k_rope: torch.Tensor,
|
||||||
|
*,
|
||||||
|
reserved_skip_index: int = 0,
|
||||||
|
):
|
||||||
|
"""Scatter at locs already addressing this rank's rows (widened ->
|
||||||
|
`set_mla_kv_buffer_dcp_sharded_triton`)."""
|
||||||
|
_set_mla_kv_buffer_impl(
|
||||||
|
kv_buffer,
|
||||||
|
loc,
|
||||||
|
cache_k_nope,
|
||||||
|
cache_k_rope,
|
||||||
|
reserved_skip_index=reserved_skip_index,
|
||||||
|
dcp_world_size=1,
|
||||||
|
dcp_rank=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def set_mla_kv_buffer_dcp_sharded_triton(
|
||||||
|
kv_buffer: torch.Tensor,
|
||||||
|
loc: torch.Tensor,
|
||||||
|
cache_k_nope: torch.Tensor,
|
||||||
|
cache_k_rope: torch.Tensor,
|
||||||
|
*,
|
||||||
|
reserved_skip_index: int = 0,
|
||||||
|
):
|
||||||
|
"""Scatter at DCP-WIDENED locs: select this rank's ids and collapse them."""
|
||||||
|
parallel = get_parallel()
|
||||||
|
_set_mla_kv_buffer_impl(
|
||||||
|
kv_buffer,
|
||||||
|
loc,
|
||||||
|
cache_k_nope,
|
||||||
|
cache_k_rope,
|
||||||
|
reserved_skip_index=reserved_skip_index,
|
||||||
|
dcp_world_size=parallel.attn_dcp_size,
|
||||||
|
dcp_rank=parallel.attn_dcp_rank,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@triton.jit
|
@triton.jit
|
||||||
def set_mla_kv_buffer_fp8_quant_kernel(
|
def set_mla_kv_buffer_fp8_quant_kernel(
|
||||||
kv_buffer_fp8_ptr,
|
kv_buffer_fp8_ptr,
|
||||||
|
|||||||
@@ -253,12 +253,8 @@ def handle_unified_memory_pool(server_args: Any) -> None:
|
|||||||
"full-attention slots are VIRTUAL — the host-offload path does not "
|
"full-attention slots are VIRTUAL — the host-offload path does not "
|
||||||
"translate them to physical."
|
"translate them to physical."
|
||||||
)
|
)
|
||||||
assert cfg.dcp_size == 1, (
|
if cfg.dcp_size > 1:
|
||||||
"--enable-unified-memory is not yet compatible with decode context "
|
_validate_unified_memory_dcp(server_args)
|
||||||
"parallelism (--dcp-size > 1): the pool has no DCP-aware masked write "
|
|
||||||
"path (UnifiedMHATokenToKVPool.set_kv_buffer asserts dcp_kv_mask is None), "
|
|
||||||
"so a DCP run would boot and then fail on the first KV write."
|
|
||||||
)
|
|
||||||
# Only monolithic decode cuda-graph capture is wired; piecewise prefill
|
# Only monolithic decode cuda-graph capture is wired; piecewise prefill
|
||||||
# capture is not. Guard when the user opts into it.
|
# capture is not. Guard when the user opts into it.
|
||||||
_cg_cfg = cfg.cuda_graph_config
|
_cg_cfg = cfg.cuda_graph_config
|
||||||
@@ -280,6 +276,53 @@ def handle_unified_memory_pool(server_args: Any) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_unified_memory_dcp(server_args: Any) -> None:
|
||||||
|
"""Gate --enable-unified-memory + --dcp-size > 1 to the audited path.
|
||||||
|
|
||||||
|
Under DCP the unified allocator hands out a WIDENED virtual id space
|
||||||
|
(dcp_size logical ids per stored row) and every read index reaches
|
||||||
|
`translate_kv_loc*` already collapsed by a DCP index kernel. Only the
|
||||||
|
pieces below have been converted to that two-stage contract.
|
||||||
|
"""
|
||||||
|
assert use_mla_backend(server_args), (
|
||||||
|
"--enable-unified-memory with decode context parallelism "
|
||||||
|
"(--dcp-size > 1) supports MLA models only (e.g. kimi-linear): the "
|
||||||
|
"MHA unified pool has no DCP-aware masked write path "
|
||||||
|
"(UnifiedMHATokenToKVPool.set_kv_buffer asserts dcp_kv_mask is None)."
|
||||||
|
)
|
||||||
|
assert not model_config_of(server_args).is_hybrid_swa, (
|
||||||
|
"--enable-unified-memory with decode context parallelism "
|
||||||
|
"(--dcp-size > 1) does not support hybrid sliding-window models: "
|
||||||
|
"UnifiedSWATokenToKVPoolAllocator does not widen its virtual id "
|
||||||
|
"space, and the full->swa mapping is not DCP-sharded."
|
||||||
|
)
|
||||||
|
cfg = resolving_view(server_args)
|
||||||
|
assert cfg.disaggregation_mode == "null", (
|
||||||
|
"--enable-unified-memory with decode context parallelism "
|
||||||
|
"(--dcp-size > 1) does not support PD disaggregation: the transfer "
|
||||||
|
"ships whole page envelopes, which under DCP hold only this rank's "
|
||||||
|
"shard of each widened page. Rejected here rather than at the first KV "
|
||||||
|
"transfer, where translate_kv_indices_for_transfer would abort a "
|
||||||
|
"server that had already booted."
|
||||||
|
)
|
||||||
|
# trtllm_mla (and its cutedsl_mla / tokenspeed_mla subclasses) build the
|
||||||
|
# MLA block table straight from req_to_token with
|
||||||
|
# create_flashmla_kv_indices_triton, whose v2p gather assumes UNWIDENED
|
||||||
|
# page ids; the DCP variant (create_mla_kv_page_table_for_dcp) has no v2p
|
||||||
|
# gather at all. Wire one of them through the other to add those here.
|
||||||
|
dcp_allowed = {"flashinfer"}
|
||||||
|
backends = set(attention_backends_of(resolved_view(server_args)))
|
||||||
|
backends.discard(None)
|
||||||
|
assert backends <= dcp_allowed, (
|
||||||
|
"--enable-unified-memory with decode context parallelism "
|
||||||
|
f"(--dcp-size > 1) requires {sorted(dcp_allowed)} for the "
|
||||||
|
f"full-attention layers; got {sorted(backends)}. The other paged MLA "
|
||||||
|
"backends build their block table from raw (widened) req_to_token "
|
||||||
|
"page ids and do not translate them through the unified pool's "
|
||||||
|
"virtual->physical page table."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def handle_page_major_kv_layout(server_args: Any):
|
def handle_page_major_kv_layout(server_args: Any):
|
||||||
# The unified pool stores state in the page-major envelope-strided layout, so
|
# The unified pool stores state in the page-major envelope-strided layout, so
|
||||||
# enabling it implies --enable-page-major-kv-layout — routing it through the
|
# enabling it implies --enable-page-major-kv-layout — routing it through the
|
||||||
|
|||||||
@@ -930,8 +930,10 @@ class FlashInferMLAIndicesUpdaterDecode:
|
|||||||
ENTRY_PAGE_SIZE=kv_view.entry_page_size,
|
ENTRY_PAGE_SIZE=kv_view.entry_page_size,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# The table above is deliberately VIRTUAL under DCP.
|
||||||
|
n_kernel_ids = paged_kernel_lens_sum
|
||||||
if get_parallel().dcp_enabled:
|
if get_parallel().dcp_enabled:
|
||||||
plan_dcp_decode_metadata(
|
n_kernel_ids = plan_dcp_decode_metadata(
|
||||||
kv_lens,
|
kv_lens,
|
||||||
kv_indptr,
|
kv_indptr,
|
||||||
kv_indices,
|
kv_indices,
|
||||||
@@ -939,6 +941,18 @@ class FlashInferMLAIndicesUpdaterDecode:
|
|||||||
fast_decode_kwargs,
|
fast_decode_kwargs,
|
||||||
bs,
|
bs,
|
||||||
)
|
)
|
||||||
|
# Written back IN PLACE: on cuda-graph replay `kv_indices` IS the
|
||||||
|
# capture-stable buffer the captured wrapper reads, so rebinding the
|
||||||
|
# local name would leave the graph on virtual ids. Only the prefix
|
||||||
|
# just filled is translated; the stale tail never indexes v2p.
|
||||||
|
translator = self.attn_backend.kv_index_translator
|
||||||
|
if (
|
||||||
|
not kv_view.is_translated
|
||||||
|
and n_kernel_ids > 0
|
||||||
|
and translator.needs_read_translate
|
||||||
|
):
|
||||||
|
valid = kv_indices[:n_kernel_ids]
|
||||||
|
valid.copy_(translator.translate_dcp_read_ids(valid))
|
||||||
else:
|
else:
|
||||||
kv_indptr, kv_indices = spec_info.kv_indptr, spec_info.kv_indices
|
kv_indptr, kv_indices = spec_info.kv_indptr, spec_info.kv_indices
|
||||||
|
|
||||||
|
|||||||
@@ -42,12 +42,13 @@ def get_dcp_lens(
|
|||||||
|
|
||||||
|
|
||||||
def filter_dcp_local_kv_indices(kv_indices: torch.Tensor):
|
def filter_dcp_local_kv_indices(kv_indices: torch.Tensor):
|
||||||
|
"""Keep this rank's share of a read-index tensor, still WIDENED.
|
||||||
|
|
||||||
|
Selection only; the caller collapses via translate_dcp_read_ids.
|
||||||
|
"""
|
||||||
parallel = get_parallel()
|
parallel = get_parallel()
|
||||||
if parallel.dcp_enabled:
|
if parallel.dcp_enabled:
|
||||||
kv_indices = (
|
kv_indices = kv_indices[kv_indices % parallel.dcp_size == parallel.dcp_rank]
|
||||||
kv_indices[kv_indices % parallel.dcp_size == parallel.dcp_rank]
|
|
||||||
// parallel.dcp_size
|
|
||||||
)
|
|
||||||
return kv_indices
|
return kv_indices
|
||||||
|
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ def filter_dcp_local_chunk_kv_indices(
|
|||||||
first = (parallel.dcp_rank - start) % dcp_size
|
first = (parallel.dcp_rank - start) % dcp_size
|
||||||
parts.append(kv_indices[offset + first : offset + length : dcp_size])
|
parts.append(kv_indices[offset + first : offset + length : dcp_size])
|
||||||
offset += length
|
offset += length
|
||||||
return torch.cat(parts) // dcp_size
|
return torch.cat(parts)
|
||||||
|
|
||||||
|
|
||||||
def update_local_kv_lens_for_dcp(kv_len_arr):
|
def update_local_kv_lens_for_dcp(kv_len_arr):
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from sglang.kernels.ops.attention.dcp_kernels import (
|
|||||||
)
|
)
|
||||||
from sglang.srt.layers.dcp.layout import update_local_kv_lens_for_dcp
|
from sglang.srt.layers.dcp.layout import update_local_kv_lens_for_dcp
|
||||||
from sglang.srt.layers.dcp.metadata import DecodeContextParallelMetadata
|
from sglang.srt.layers.dcp.metadata import DecodeContextParallelMetadata
|
||||||
|
from sglang.srt.model_executor.forward_context import get_attn_backend
|
||||||
from sglang.srt.runtime_context import get_device, get_parallel
|
from sglang.srt.runtime_context import get_device, get_parallel
|
||||||
|
|
||||||
|
|
||||||
@@ -110,9 +111,10 @@ def prepare_decode_context_parallel_metadata(
|
|||||||
parallel.dcp_size,
|
parallel.dcp_size,
|
||||||
)
|
)
|
||||||
# Prefix lengths are dcp_size-aligned (widened allocator page), so no nonzero().
|
# Prefix lengths are dcp_size-aligned (widened allocator page), so no nonzero().
|
||||||
dcp_local_prefix_kv_indices = (
|
# `get_mla_kv_buffer` is a read door with the caller-translates contract.
|
||||||
|
translator = get_attn_backend().kv_index_translator
|
||||||
|
dcp_local_prefix_kv_indices = translator.translate_dcp_read_ids(
|
||||||
dcp_prefix_kv_indices[parallel.dcp_rank :: parallel.dcp_size]
|
dcp_prefix_kv_indices[parallel.dcp_rank :: parallel.dcp_size]
|
||||||
// parallel.dcp_size
|
|
||||||
)
|
)
|
||||||
dcp_kv_buffer = torch.empty(
|
dcp_kv_buffer = torch.empty(
|
||||||
(
|
(
|
||||||
@@ -139,7 +141,14 @@ def plan_dcp_decode_metadata(
|
|||||||
init_metadata_replay: bool,
|
init_metadata_replay: bool,
|
||||||
fast_decode_kwargs: dict,
|
fast_decode_kwargs: dict,
|
||||||
bs: int,
|
bs: int,
|
||||||
):
|
) -> int:
|
||||||
|
"""Shard `kv_indices` to this DCP rank in place; return the shard's length.
|
||||||
|
|
||||||
|
`kv_lens` / `kv_indptr` are rewritten to the per-rank lengths, and this
|
||||||
|
rank's ids (`loc % dcp_size == dcp_rank`) are compacted into
|
||||||
|
`kv_indices[:total_local_len]`, still WIDENED. The returned length bounds the
|
||||||
|
prefix the caller hands to `KVIndexTranslator.translate_dcp_read_ids`.
|
||||||
|
"""
|
||||||
parallel = get_parallel()
|
parallel = get_parallel()
|
||||||
local_kv_lens = kv_lens.clone()
|
local_kv_lens = kv_lens.clone()
|
||||||
update_local_kv_lens_for_dcp(local_kv_lens)
|
update_local_kv_lens_for_dcp(local_kv_lens)
|
||||||
@@ -185,3 +194,4 @@ def plan_dcp_decode_metadata(
|
|||||||
kv_indices[:total_local_len] = local_kv_indices[:total_local_len]
|
kv_indices[:total_local_len] = local_kv_indices[:total_local_len]
|
||||||
kv_lens.copy_(local_kv_lens)
|
kv_lens.copy_(local_kv_lens)
|
||||||
kv_indptr[: bs + 1] = local_kv_lens_cumsum[: bs + 1]
|
kv_indptr[: bs + 1] = local_kv_lens_cumsum[: bs + 1]
|
||||||
|
return total_local_len
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ from sglang.srt.mem_cache.multi_ended_allocator import (
|
|||||||
UnifiedSWATokenToKVPoolAllocator,
|
UnifiedSWATokenToKVPoolAllocator,
|
||||||
)
|
)
|
||||||
from sglang.srt.mem_cache.swa_memory_pool import SWAKVPool
|
from sglang.srt.mem_cache.swa_memory_pool import SWAKVPool
|
||||||
|
from sglang.srt.runtime_context import get_parallel
|
||||||
|
|
||||||
|
|
||||||
class KVReadTables(msgspec.Struct, frozen=True):
|
class KVReadTables(msgspec.Struct, frozen=True):
|
||||||
@@ -122,6 +123,14 @@ class KVIndexTranslator:
|
|||||||
self._full_p2v_table = alloc.full_p2v_page_table
|
self._full_p2v_table = alloc.full_p2v_page_table
|
||||||
self._full_page_multiplier = alloc.kernel_page_multiplier
|
self._full_page_multiplier = alloc.kernel_page_multiplier
|
||||||
self._translate_full = alloc.translate_kv_loc_for_kernel
|
self._translate_full = alloc.translate_kv_loc_for_kernel
|
||||||
|
# The WRITE loc is the one id that arrives DCP-WIDENED: read indices
|
||||||
|
# are collapsed by the DCP index kernels, `out_cache_loc` still
|
||||||
|
# carries the owner rule in `loc % dcp_size`. Identity with the read
|
||||||
|
# translate when dcp_size == 1.
|
||||||
|
self._translate_write_full = alloc.translate_write_loc_for_kernel
|
||||||
|
# DCP read ids stay WIDENED to the consumer: selecting this rank's
|
||||||
|
# share changes the length, so only the production site can do it.
|
||||||
|
self.defer_read_translate = get_parallel().attn_dcp_size > 1
|
||||||
if isinstance(alloc, UnifiedSWATokenToKVPoolAllocator):
|
if isinstance(alloc, UnifiedSWATokenToKVPoolAllocator):
|
||||||
self._swa_v2p_table = alloc.swa_v2p_page_table
|
self._swa_v2p_table = alloc.swa_v2p_page_table
|
||||||
self._swa_page_multiplier = alloc.swa_kernel_page_multiplier
|
self._swa_page_multiplier = alloc.swa_kernel_page_multiplier
|
||||||
@@ -135,6 +144,8 @@ class KVIndexTranslator:
|
|||||||
self._full_p2v_table = None
|
self._full_p2v_table = None
|
||||||
self._full_page_multiplier = 1
|
self._full_page_multiplier = 1
|
||||||
self._translate_full = None
|
self._translate_full = None
|
||||||
|
self._translate_write_full = None
|
||||||
|
self.defer_read_translate = False
|
||||||
self._swa_v2p_table = None
|
self._swa_v2p_table = None
|
||||||
self._swa_page_multiplier = 1
|
self._swa_page_multiplier = 1
|
||||||
self._swa_write_loc_from_full = (
|
self._swa_write_loc_from_full = (
|
||||||
@@ -192,7 +203,7 @@ class KVIndexTranslator:
|
|||||||
captured graph bakes it) passes its own tables in ``into``;
|
captured graph bakes it) passes its own tables in ``into``;
|
||||||
``into=None`` allocates of width ``max_pages`` instead.
|
``into=None`` allocates of width ``max_pages`` instead.
|
||||||
"""
|
"""
|
||||||
if not self.is_translating:
|
if not self.is_translating or self.defer_read_translate:
|
||||||
return KVIndexTable(
|
return KVIndexTable(
|
||||||
ids=self.req_to_token,
|
ids=self.req_to_token,
|
||||||
row_ids=req_pool_indices,
|
row_ids=req_pool_indices,
|
||||||
@@ -308,20 +319,22 @@ class KVIndexTranslator:
|
|||||||
self._index_table_memo = (weakref.ref(forward_batch), view)
|
self._index_table_memo = (weakref.ref(forward_batch), view)
|
||||||
return view
|
return view
|
||||||
|
|
||||||
def assert_backends_carry_translator(self, backends) -> None:
|
def bind_and_verify_backends(self, backends) -> None:
|
||||||
"""Boot guard: under the unified pool every backend a forward can reach
|
"""Boot: make every reachable backend carry THIS translator.
|
||||||
must carry THIS translator."""
|
|
||||||
if not self.is_translating:
|
Model-layer producers read it off `get_attn_backend()`, so an unset
|
||||||
return
|
attribute is an unreachable hook, not "no translation needed".
|
||||||
|
"""
|
||||||
for backend in backends:
|
for backend in backends:
|
||||||
if backend is None:
|
if backend is None:
|
||||||
continue
|
continue
|
||||||
|
if backend.kv_index_translator is None:
|
||||||
|
backend.kv_index_translator = self
|
||||||
|
continue
|
||||||
assert backend.kv_index_translator is self, (
|
assert backend.kv_index_translator is self, (
|
||||||
f"{type(backend).__name__} does not carry the runner's "
|
f"{type(backend).__name__} carries a KVIndexTranslator that is "
|
||||||
"KVIndexTranslator. A backend (or wrapper) reachable under "
|
"not this runner's. A wrapper must forward the inner backend's "
|
||||||
"--enable-unified-memory must forward `kv_index_translator`, or "
|
"copy, not build its own."
|
||||||
"read-index producers silently skip the virtual->kernel-facing "
|
|
||||||
"translation."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# -- write loc (phase 1; phase 2 lives in build_index_table) ----------------
|
# -- write loc (phase 1; phase 2 lives in build_index_table) ----------------
|
||||||
@@ -338,7 +351,9 @@ class KVIndexTranslator:
|
|||||||
self._index_table_memo = None
|
self._index_table_memo = None
|
||||||
if not self.is_translating or forward_batch.out_cache_loc is None:
|
if not self.is_translating or forward_batch.out_cache_loc is None:
|
||||||
return
|
return
|
||||||
forward_batch.out_cache_loc = self._translate_full(forward_batch.out_cache_loc)
|
forward_batch.out_cache_loc = self._translate_write_full(
|
||||||
|
forward_batch.out_cache_loc
|
||||||
|
)
|
||||||
|
|
||||||
def sliding_window_write_loc_for(
|
def sliding_window_write_loc_for(
|
||||||
self, out_cache_loc: Optional[torch.Tensor]
|
self, out_cache_loc: Optional[torch.Tensor]
|
||||||
@@ -363,6 +378,23 @@ class KVIndexTranslator:
|
|||||||
|
|
||||||
# -- token-level translate surface (the mixin / local-attn consumers) ------
|
# -- token-level translate surface (the mixin / local-attn consumers) ------
|
||||||
|
|
||||||
|
@property
|
||||||
|
def needs_read_translate(self) -> bool:
|
||||||
|
"""Whether `translate_dcp_read_ids` is anything but the identity, so a
|
||||||
|
hot path can skip the call rather than round-trip a no-op copy."""
|
||||||
|
return self.is_translating or get_parallel().attn_dcp_size > 1
|
||||||
|
|
||||||
|
def translate_dcp_read_ids(self, widened_ids: torch.Tensor) -> torch.Tensor:
|
||||||
|
"""Widened logical READ ids -> kernel-facing ids, for either pool.
|
||||||
|
|
||||||
|
The one hook every DCP read-index production site calls; on a static
|
||||||
|
pool `widened // dcp_size` IS the whole virtual->physical translation.
|
||||||
|
"""
|
||||||
|
dcp_size = get_parallel().attn_dcp_size
|
||||||
|
if dcp_size > 1:
|
||||||
|
widened_ids = widened_ids // dcp_size
|
||||||
|
return self.translate_full_attn_ids(widened_ids)
|
||||||
|
|
||||||
def translate_full_attn_ids(
|
def translate_full_attn_ids(
|
||||||
self, kv_indices: torch.Tensor, *, out: Optional[torch.Tensor] = None
|
self, kv_indices: torch.Tensor, *, out: Optional[torch.Tensor] = None
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ from sglang.srt.mem_cache.layout.page_major import (
|
|||||||
from sglang.srt.mem_cache.utils import (
|
from sglang.srt.mem_cache.utils import (
|
||||||
get_mla_kv_buffer_triton,
|
get_mla_kv_buffer_triton,
|
||||||
maybe_init_custom_mem_pool,
|
maybe_init_custom_mem_pool,
|
||||||
|
set_mla_kv_buffer_dcp_sharded_triton,
|
||||||
set_mla_kv_buffer_triton,
|
set_mla_kv_buffer_triton,
|
||||||
set_mla_kv_buffer_triton_fp8_quant,
|
set_mla_kv_buffer_triton_fp8_quant,
|
||||||
set_mla_kv_scale_buffer_triton,
|
set_mla_kv_scale_buffer_triton,
|
||||||
@@ -3657,8 +3658,9 @@ class HybridLinearKVPool(KVCache):
|
|||||||
self.head_num = head_num
|
self.head_num = head_num
|
||||||
self.head_dim = head_dim
|
self.head_dim = head_dim
|
||||||
self.mamba_pool = mamba_pool
|
self.mamba_pool = mamba_pool
|
||||||
# virtual->physical mamba-slot translate for the HiCache offload path;
|
# Identity even though the unified pool holds VIRTUAL mamba ids: its
|
||||||
# identity for a static pool, the allocator's `translate` for the unified pool.
|
# composite allocator implements neither `get_cpu_copy` nor
|
||||||
|
# `load_cpu_copy`, the only readers, so those ids never arrive here.
|
||||||
self._mamba_translate = lambda ids: ids
|
self._mamba_translate = lambda ids: ids
|
||||||
self.use_mla = use_mla
|
self.use_mla = use_mla
|
||||||
if full_kv_pool is not None:
|
if full_kv_pool is not None:
|
||||||
@@ -4078,6 +4080,32 @@ class MLATokenToKVPool(KVCache):
|
|||||||
def get_kv_buffer(self, layer_id: int):
|
def get_kv_buffer(self, layer_id: int):
|
||||||
return self.get_key_buffer(layer_id), self.get_value_buffer(layer_id)
|
return self.get_key_buffer(layer_id), self.get_value_buffer(layer_id)
|
||||||
|
|
||||||
|
# Has the WRITE loc arriving here already had the DCP owner rule resolved?
|
||||||
|
# False: this pool takes a WIDENED loc. The unified pool resolves it in
|
||||||
|
# `KVIndexTranslator.rebind_write_loc` and flips this. Not derivable from
|
||||||
|
# `kernel_page_blocks`: that is `layer_num`, so a rank owning one
|
||||||
|
# full-attention layer is translated with blocks_per_page 1.
|
||||||
|
write_loc_is_dcp_resolved = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _write_loc_dcp_span(self) -> int:
|
||||||
|
"""How many logical ids one stored row spans in the write-loc space."""
|
||||||
|
return 1 if self.write_loc_is_dcp_resolved else get_parallel().attn_dcp_size
|
||||||
|
|
||||||
|
def _scatter_mla_rows(
|
||||||
|
self,
|
||||||
|
dst_buffer: torch.Tensor,
|
||||||
|
loc: torch.Tensor,
|
||||||
|
cache_k_nope: torch.Tensor,
|
||||||
|
cache_k_rope: torch.Tensor,
|
||||||
|
) -> None:
|
||||||
|
if self.write_loc_is_dcp_resolved:
|
||||||
|
set_mla_kv_buffer_triton(dst_buffer, loc, cache_k_nope, cache_k_rope)
|
||||||
|
else:
|
||||||
|
set_mla_kv_buffer_dcp_sharded_triton(
|
||||||
|
dst_buffer, loc, cache_k_nope, cache_k_rope
|
||||||
|
)
|
||||||
|
|
||||||
def set_kv_buffer(
|
def set_kv_buffer(
|
||||||
self,
|
self,
|
||||||
layer: RadixAttention,
|
layer: RadixAttention,
|
||||||
@@ -4095,12 +4123,15 @@ class MLATokenToKVPool(KVCache):
|
|||||||
layer_id_override if layer_id_override is not None else layer.layer_id
|
layer_id_override if layer_id_override is not None else layer.layer_id
|
||||||
)
|
)
|
||||||
assert not self.dsa_kv_cache_store_fp8
|
assert not self.dsa_kv_cache_store_fp8
|
||||||
parallel = get_parallel()
|
# No DCP-aware variant is possible: the two backends reaching this door
|
||||||
if parallel.dcp_enabled:
|
# disagree on the loc space (flashinfer-MLA widened, Triton collapsed).
|
||||||
valid_mask = loc % parallel.attn_dcp_size == parallel.attn_dcp_rank
|
assert self.write_loc_is_dcp_resolved or not get_parallel().dcp_enabled, (
|
||||||
if not valid_mask.all():
|
"MLATokenToKVPool.set_kv_buffer has no DCP-aware write path. Under "
|
||||||
loc = loc[valid_mask]
|
"--dcp-size > 1 the MLA write must go through set_mla_kv_buffer, "
|
||||||
cache_k = cache_k[valid_mask]
|
"whose kernel resolves the owner rule; reaching the combined-row "
|
||||||
|
"door means an attention backend took a write path that never "
|
||||||
|
"declared which loc space it emits."
|
||||||
|
)
|
||||||
if cache_k.dtype != self.dtype:
|
if cache_k.dtype != self.dtype:
|
||||||
cache_k = cache_k.to(self.dtype)
|
cache_k = cache_k.to(self.dtype)
|
||||||
|
|
||||||
@@ -4118,6 +4149,10 @@ class MLATokenToKVPool(KVCache):
|
|||||||
cache_k_nope: torch.Tensor,
|
cache_k_nope: torch.Tensor,
|
||||||
cache_k_rope: torch.Tensor,
|
cache_k_rope: torch.Tensor,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
assert not (
|
||||||
|
self.write_loc_is_dcp_resolved
|
||||||
|
and (self.use_dsa or self.dsa_kv_cache_store_fp8)
|
||||||
|
), "the DSA write paths have no resolved-loc variant"
|
||||||
if _is_hip and self.use_dsa and self.dtype == fp8_dtype:
|
if _is_hip and self.use_dsa and self.dtype == fp8_dtype:
|
||||||
# HIP FP8 path uses raw MLA KV layout (nope + rope) without per-block scales.
|
# HIP FP8 path uses raw MLA KV layout (nope + rope) without per-block scales.
|
||||||
# Fuse BF16/FP16 -> FP8 cast with paged KV write.
|
# Fuse BF16/FP16 -> FP8 cast with paged KV write.
|
||||||
@@ -4139,12 +4174,7 @@ class MLATokenToKVPool(KVCache):
|
|||||||
# Reuse existing two-tensor write kernel (works with FP8 byte layout)
|
# Reuse existing two-tensor write kernel (works with FP8 byte layout)
|
||||||
# cache_k_nope_fp8: (num_tokens, 1, 528) uint8 [nope_fp8(512) | scales(16)]
|
# cache_k_nope_fp8: (num_tokens, 1, 528) uint8 [nope_fp8(512) | scales(16)]
|
||||||
# cache_k_rope_fp8: (num_tokens, 1, 128) uint8 [rope_bf16_bytes(128)]
|
# cache_k_rope_fp8: (num_tokens, 1, 128) uint8 [rope_bf16_bytes(128)]
|
||||||
set_mla_kv_buffer_triton(
|
self._scatter_mla_rows(dst_buffer, loc, cache_k_nope_fp8, cache_k_rope_fp8)
|
||||||
dst_buffer,
|
|
||||||
loc,
|
|
||||||
cache_k_nope_fp8,
|
|
||||||
cache_k_rope_fp8,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
if cache_k_nope.dtype != self.dtype:
|
if cache_k_nope.dtype != self.dtype:
|
||||||
cache_k_nope = cache_k_nope.to(self.dtype)
|
cache_k_nope = cache_k_nope.to(self.dtype)
|
||||||
@@ -4153,12 +4183,7 @@ class MLATokenToKVPool(KVCache):
|
|||||||
cache_k_nope = cache_k_nope.view(self.store_dtype)
|
cache_k_nope = cache_k_nope.view(self.store_dtype)
|
||||||
cache_k_rope = cache_k_rope.view(self.store_dtype)
|
cache_k_rope = cache_k_rope.view(self.store_dtype)
|
||||||
|
|
||||||
set_mla_kv_buffer_triton(
|
self._scatter_mla_rows(dst_buffer, loc, cache_k_nope, cache_k_rope)
|
||||||
dst_buffer,
|
|
||||||
loc,
|
|
||||||
cache_k_nope,
|
|
||||||
cache_k_rope,
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_mla_kv_buffer(
|
def set_mla_kv_buffer(
|
||||||
self,
|
self,
|
||||||
@@ -4168,11 +4193,11 @@ class MLATokenToKVPool(KVCache):
|
|||||||
cache_k_rope: torch.Tensor,
|
cache_k_rope: torch.Tensor,
|
||||||
layer_id_override: Optional[int] = None,
|
layer_id_override: Optional[int] = None,
|
||||||
):
|
):
|
||||||
# loc is widened under DCP; the kernel divides by the world size itself.
|
# loc is widened under DCP unless the pool declares it resolved.
|
||||||
maybe_detect_oob(
|
maybe_detect_oob(
|
||||||
loc,
|
loc,
|
||||||
0,
|
0,
|
||||||
(self.size + self.page_size) * get_parallel().attn_dcp_size,
|
(self.size + self.page_size) * self._write_loc_dcp_span,
|
||||||
"set_mla_kv_buffer (MLA)",
|
"set_mla_kv_buffer (MLA)",
|
||||||
)
|
)
|
||||||
maybe_detect_kernel_facing_loc(
|
maybe_detect_kernel_facing_loc(
|
||||||
@@ -4379,7 +4404,7 @@ class MLATokenToKVPoolFP4(MLATokenToKVPool):
|
|||||||
cache_k_nope = cache_k_nope.view(self.store_dtype)
|
cache_k_nope = cache_k_nope.view(self.store_dtype)
|
||||||
cache_k_rope = cache_k_rope.view(self.store_dtype)
|
cache_k_rope = cache_k_rope.view(self.store_dtype)
|
||||||
|
|
||||||
set_mla_kv_buffer_triton(
|
self._scatter_mla_rows(
|
||||||
self.kv_buffer[layer_id - self.start_layer],
|
self.kv_buffer[layer_id - self.start_layer],
|
||||||
loc,
|
loc,
|
||||||
cache_k_nope_fp4,
|
cache_k_nope_fp4,
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ from sglang.srt.mem_cache.unified_memory_pool import (
|
|||||||
UnifiedKVPool,
|
UnifiedKVPool,
|
||||||
UnifiedMLATokenToKVPool,
|
UnifiedMLATokenToKVPool,
|
||||||
)
|
)
|
||||||
|
from sglang.srt.runtime_context import get_parallel
|
||||||
from sglang.srt.utils.common import get_num_new_pages, next_power_of_2
|
from sglang.srt.utils.common import get_num_new_pages, next_power_of_2
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -263,6 +264,7 @@ class MultiEndedAllocator(BaseTokenToKVPoolAllocator):
|
|||||||
device: str,
|
device: str,
|
||||||
is_id_owner: bool,
|
is_id_owner: bool,
|
||||||
page_size: int = 1,
|
page_size: int = 1,
|
||||||
|
shards_under_dcp: bool = False,
|
||||||
need_sort: bool = False,
|
need_sort: bool = False,
|
||||||
forward_stream: Optional[torch.cuda.Stream] = None,
|
forward_stream: Optional[torch.cuda.Stream] = None,
|
||||||
lazy_compaction: bool = False,
|
lazy_compaction: bool = False,
|
||||||
@@ -270,9 +272,13 @@ class MultiEndedAllocator(BaseTokenToKVPoolAllocator):
|
|||||||
):
|
):
|
||||||
spec = unified_buffer.spec(sub_pool_name)
|
spec = unified_buffer.spec(sub_pool_name)
|
||||||
max_slots = unified_buffer.max_slots(sub_pool_name)
|
max_slots = unified_buffer.max_slots(sub_pool_name)
|
||||||
|
# DCP shards KV tokens only. Mamba state and the SWA rows are
|
||||||
|
# replicated, so they stay slot-granular whatever the process width is.
|
||||||
|
self.shards_under_dcp = shards_under_dcp
|
||||||
|
dcp_size = get_parallel().attn_dcp_size if shards_under_dcp else 1
|
||||||
super().__init__(
|
super().__init__(
|
||||||
size=max_slots,
|
size=max_slots * dcp_size,
|
||||||
page_size=page_size,
|
page_size=page_size * dcp_size,
|
||||||
dtype=spec.get_dtype(),
|
dtype=spec.get_dtype(),
|
||||||
device=device,
|
device=device,
|
||||||
kvcache=kvcache,
|
kvcache=kvcache,
|
||||||
@@ -301,12 +307,26 @@ class MultiEndedAllocator(BaseTokenToKVPoolAllocator):
|
|||||||
self.forward_stream = forward_stream
|
self.forward_stream = forward_stream
|
||||||
|
|
||||||
# --- Page-aware bookkeeping ---
|
# --- Page-aware bookkeeping ---
|
||||||
# `min_page_index` = ceil(min_slot_index / page_size), keeping the
|
# Two page sizes, equal unless decode context parallelism is on:
|
||||||
|
# `page_size` is VIRTUAL (what the scheduler, the tree cache and the
|
||||||
|
# alloc/free surface speak, matching PagedTokenToKVPoolAllocator's
|
||||||
|
# widened DCP contract), `pool_page_size` is the PHYSICAL rows one page
|
||||||
|
# occupies here. Under DCP a virtual page holds dcp_size logical ids per
|
||||||
|
# stored row, of which this rank owns `loc % dcp_size == dcp_rank`;
|
||||||
|
# `KVIndexTranslator.translate_dcp_read_ids` collapses `loc // dcp_size`
|
||||||
|
# before reaching `translate_kv_loc*`, so everything at or below the v2p
|
||||||
|
# table -- byte budget, compaction moves, translate -- stays on
|
||||||
|
# `pool_page_size`.
|
||||||
|
# Page ids are invariant under the widening, so v2p/p2v are unchanged.
|
||||||
|
self.pool_page_size = page_size
|
||||||
|
self.page_size = page_size * dcp_size
|
||||||
|
self.num_pages = max_slots // self.pool_page_size
|
||||||
|
# `min_page_index` = ceil(min_slot_index / pool_page_size), keeping the
|
||||||
# reserved-sink invariant (min_page_index * entry_bytes_per_page >= entry_max).
|
# reserved-sink invariant (min_page_index * entry_bytes_per_page >= entry_max).
|
||||||
self.page_size = page_size
|
self.min_page_index = (
|
||||||
self.num_pages = max_slots // page_size
|
self.min_slot_index + self.pool_page_size - 1
|
||||||
self.min_page_index = (self.min_slot_index + page_size - 1) // page_size
|
) // self.pool_page_size
|
||||||
self.entry_bytes_per_page = self.entry_bytes * page_size
|
self.entry_bytes_per_page = self.entry_bytes * self.pool_page_size
|
||||||
|
|
||||||
# v2p / p2v sized by PAGES. Page 0 is the padding anchor; trailing row is
|
# v2p / p2v sized by PAGES. Page 0 is the padding anchor; trailing row is
|
||||||
# the -1 sentinel.
|
# the -1 sentinel.
|
||||||
@@ -982,6 +1002,10 @@ class MultiEndedAllocator(BaseTokenToKVPoolAllocator):
|
|||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
"""Translate token-granular virtual ids to physical ids.
|
"""Translate token-granular virtual ids to physical ids.
|
||||||
|
|
||||||
|
Under DCP the input is the DCP-collapsed id (`widened // dcp_size`, what
|
||||||
|
`KVIndexTranslator.translate_dcp_read_ids` hands down), so this works on
|
||||||
|
`pool_page_size`.
|
||||||
|
|
||||||
``out=`` writes in-place into a caller-owned buffer — required under
|
``out=`` writes in-place into a caller-owned buffer — required under
|
||||||
cuda-graph capture for buffer-stability (the captured graph records the
|
cuda-graph capture for buffer-stability (the captured graph records the
|
||||||
gather against a fixed ``data_ptr``).
|
gather against a fixed ``data_ptr``).
|
||||||
@@ -1008,7 +1032,8 @@ class MultiEndedAllocator(BaseTokenToKVPoolAllocator):
|
|||||||
# routes any tombstoned read/write to physical slot 0 — reserved
|
# routes any tombstoned read/write to physical slot 0 — reserved
|
||||||
# padding-sink space by the `min_slot_index` invariant (bytes [0, entry_max)
|
# padding-sink space by the `min_slot_index` invariant (bytes [0, entry_max)
|
||||||
# across all sub-pools hold no real data).
|
# across all sub-pools hold no real data).
|
||||||
if self.page_size == 1:
|
ps = self.pool_page_size
|
||||||
|
if ps == 1:
|
||||||
if out is not None:
|
if out is not None:
|
||||||
# `index_select(out=out)` forbids index/out aliasing, but the
|
# `index_select(out=out)` forbids index/out aliasing, but the
|
||||||
# canonical caller does in-place `translate(kv_indices, out=kv_indices)`.
|
# canonical caller does in-place `translate(kv_indices, out=kv_indices)`.
|
||||||
@@ -1019,18 +1044,18 @@ class MultiEndedAllocator(BaseTokenToKVPoolAllocator):
|
|||||||
return out
|
return out
|
||||||
result = torch.index_select(self.virtual_to_physical, 0, virt_tokens)
|
result = torch.index_select(self.virtual_to_physical, 0, virt_tokens)
|
||||||
return torch.clamp_min(result, 0)
|
return torch.clamp_min(result, 0)
|
||||||
# page_size > 1: page math. `virt_pages`/`offsets` are fresh, so they
|
# ps > 1: page math. `virt_pages`/`offsets` are fresh, so they
|
||||||
# cannot alias `out` — `index_select(out=out)` is safe.
|
# cannot alias `out` — `index_select(out=out)` is safe.
|
||||||
virt_pages = virt_tokens // self.page_size
|
virt_pages = virt_tokens // ps
|
||||||
offsets = virt_tokens % self.page_size
|
offsets = virt_tokens % ps
|
||||||
if out is not None:
|
if out is not None:
|
||||||
torch.index_select(self.virtual_to_physical, 0, virt_pages, out=out)
|
torch.index_select(self.virtual_to_physical, 0, virt_pages, out=out)
|
||||||
out.mul_(self.page_size)
|
out.mul_(ps)
|
||||||
out.add_(offsets)
|
out.add_(offsets)
|
||||||
out.clamp_(min=0) # tombstoned page: -1*ps + offset in [-ps, -1]
|
out.clamp_(min=0) # tombstoned page: -1*ps + offset in [-ps, -1]
|
||||||
return out
|
return out
|
||||||
phys_pages = self.virtual_to_physical[virt_pages]
|
phys_pages = self.virtual_to_physical[virt_pages]
|
||||||
result = phys_pages * self.page_size + offsets
|
result = phys_pages * ps + offsets
|
||||||
return torch.clamp_min(result, 0)
|
return torch.clamp_min(result, 0)
|
||||||
|
|
||||||
def translate_kv_loc_for_kernel(
|
def translate_kv_loc_for_kernel(
|
||||||
@@ -1048,7 +1073,7 @@ class MultiEndedAllocator(BaseTokenToKVPoolAllocator):
|
|||||||
clamp to kernel-facing id 0, the page-0 sink. int64 out; a consumer whose
|
clamp to kernel-facing id 0, the page-0 sink. int64 out; a consumer whose
|
||||||
kernel ABI wants int32 narrows where it fills that buffer.
|
kernel ABI wants int32 narrows where it fills that buffer.
|
||||||
"""
|
"""
|
||||||
ps = self.page_size
|
ps = self.pool_page_size
|
||||||
stride = ps * self.kernel_page_multiplier
|
stride = ps * self.kernel_page_multiplier
|
||||||
with record_function("MultiEndedAlloc.translate_kv_loc_for_kernel"):
|
with record_function("MultiEndedAlloc.translate_kv_loc_for_kernel"):
|
||||||
pages = virt_tokens if ps == 1 else virt_tokens // ps
|
pages = virt_tokens if ps == 1 else virt_tokens // ps
|
||||||
@@ -1076,6 +1101,33 @@ class MultiEndedAllocator(BaseTokenToKVPoolAllocator):
|
|||||||
out.add_(offsets)
|
out.add_(offsets)
|
||||||
return out.clamp_(min=0)
|
return out.clamp_(min=0)
|
||||||
|
|
||||||
|
def translate_write_loc_for_kernel(
|
||||||
|
self,
|
||||||
|
widened_loc: torch.Tensor,
|
||||||
|
*,
|
||||||
|
out: Optional[torch.Tensor] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Widened virtual WRITE loc (`out_cache_loc`) -> kernel-facing id.
|
||||||
|
|
||||||
|
Reads arrive already DCP-collapsed (every DCP index kernel divides), but
|
||||||
|
`out_cache_loc` does not: it still carries the owner rule in
|
||||||
|
`loc % dcp_size`. Resolve ownership, collapse, translate; ids this rank
|
||||||
|
does not own go to kernel id 0, the padding sink every write kernel
|
||||||
|
skips. Identity with `translate_kv_loc_for_kernel` at dcp_size == 1.
|
||||||
|
"""
|
||||||
|
parallel = get_parallel()
|
||||||
|
dcp_size = parallel.attn_dcp_size if self.shards_under_dcp else 1
|
||||||
|
if dcp_size == 1:
|
||||||
|
return self.translate_kv_loc_for_kernel(widened_loc, out=out)
|
||||||
|
with record_function("MultiEndedAlloc.translate_write_loc_for_kernel"):
|
||||||
|
owned = (widened_loc % dcp_size) == parallel.attn_dcp_rank
|
||||||
|
dense = self.translate_kv_loc_for_kernel(widened_loc // dcp_size)
|
||||||
|
dense = torch.where(owned, dense, torch.zeros_like(dense))
|
||||||
|
if out is not None:
|
||||||
|
out.copy_(dense)
|
||||||
|
return out
|
||||||
|
return dense
|
||||||
|
|
||||||
# -- alloc --
|
# -- alloc --
|
||||||
|
|
||||||
def alloc(self, need_size: int) -> Optional[torch.Tensor]:
|
def alloc(self, need_size: int) -> Optional[torch.Tensor]:
|
||||||
@@ -1503,15 +1555,15 @@ class MultiEndedAllocator(BaseTokenToKVPoolAllocator):
|
|||||||
"""
|
"""
|
||||||
v_moved = self.physical_to_virtual[src_pages].clone() # read pre-wipe
|
v_moved = self.physical_to_virtual[src_pages].clone() # read pre-wipe
|
||||||
|
|
||||||
# Expand page ids to token ids for the token-granular move kernel.
|
# Expand to PHYSICAL token granularity (the move kernel is
|
||||||
if self.page_size == 1:
|
# token-granular over pool rows).
|
||||||
|
if self.pool_page_size == 1:
|
||||||
src_t, dst_t = src_pages, dst_pages
|
src_t, dst_t = src_pages, dst_pages
|
||||||
else:
|
else:
|
||||||
offsets = torch.arange(
|
ps = self.pool_page_size
|
||||||
self.page_size, dtype=torch.int64, device=self.device
|
offsets = torch.arange(ps, dtype=torch.int64, device=self.device)
|
||||||
)
|
src_t = (src_pages[:, None] * ps + offsets).reshape(-1)
|
||||||
src_t = (src_pages[:, None] * self.page_size + offsets).reshape(-1)
|
dst_t = (dst_pages[:, None] * ps + offsets).reshape(-1)
|
||||||
dst_t = (dst_pages[:, None] * self.page_size + offsets).reshape(-1)
|
|
||||||
|
|
||||||
# Un-translated copy: the public copy_from translates virtual ids,
|
# Un-translated copy: the public copy_from translates virtual ids,
|
||||||
# which we must NOT do here.
|
# which we must NOT do here.
|
||||||
@@ -1571,9 +1623,15 @@ class MultiEndedAllocator(BaseTokenToKVPoolAllocator):
|
|||||||
return None
|
return None
|
||||||
# `oclv` is non-None here (set_inflight_forward clears the slot otherwise).
|
# `oclv` is non-None here (set_inflight_forward clears the slot otherwise).
|
||||||
with record_function("MultiEndedAlloc._materialize_inflight_write_set"):
|
with record_function("MultiEndedAlloc._materialize_inflight_write_set"):
|
||||||
|
# `oclv` is a WIDENED virtual id under DCP; collapse to the id space
|
||||||
|
# translate speaks. The write set is a page set, and a widened page
|
||||||
|
# covers exactly the same page, so the non-owned ids fold in harmlessly.
|
||||||
|
dcp_size = get_parallel().attn_dcp_size if self.shards_under_dcp else 1
|
||||||
|
if dcp_size > 1:
|
||||||
|
oclv = oclv // dcp_size
|
||||||
phys_tokens = self.translate_kv_loc(oclv)
|
phys_tokens = self.translate_kv_loc(oclv)
|
||||||
if self.page_size > 1:
|
if self.pool_page_size > 1:
|
||||||
phys_pages = (phys_tokens // self.page_size).unique()
|
phys_pages = (phys_tokens // self.pool_page_size).unique()
|
||||||
else:
|
else:
|
||||||
phys_pages = phys_tokens
|
phys_pages = phys_tokens
|
||||||
return set(phys_pages.tolist()) # .tolist() syncs schedule_stream
|
return set(phys_pages.tolist()) # .tolist() syncs schedule_stream
|
||||||
@@ -1999,17 +2057,15 @@ class MultiEndedAllocator(BaseTokenToKVPoolAllocator):
|
|||||||
(v_moveds_t >= 0).all(),
|
(v_moveds_t >= 0).all(),
|
||||||
"invalid p2v mapping in MultiEndedAllocator._flush",
|
"invalid p2v mapping in MultiEndedAllocator._flush",
|
||||||
)
|
)
|
||||||
# Expand to token granularity (the move kernel is token-granular).
|
# Expand to PHYSICAL token granularity (the move kernel is
|
||||||
if self.page_size == 1:
|
# token-granular over pool rows).
|
||||||
|
if self.pool_page_size == 1:
|
||||||
src_t, dst_t = src_pages_t, dst_pages_t
|
src_t, dst_t = src_pages_t, dst_pages_t
|
||||||
else:
|
else:
|
||||||
offsets = torch.arange(
|
ps = self.pool_page_size
|
||||||
self.page_size,
|
offsets = torch.arange(ps, dtype=torch.int64, device=self.device)
|
||||||
dtype=torch.int64,
|
src_t = (src_pages_t[:, None] * ps + offsets).reshape(-1)
|
||||||
device=self.device,
|
dst_t = (dst_pages_t[:, None] * ps + offsets).reshape(-1)
|
||||||
)
|
|
||||||
src_t = (src_pages_t[:, None] * self.page_size + offsets).reshape(-1)
|
|
||||||
dst_t = (dst_pages_t[:, None] * self.page_size + offsets).reshape(-1)
|
|
||||||
self._kvcache.move_kv_cache(dst_t, src_t)
|
self._kvcache.move_kv_cache(dst_t, src_t)
|
||||||
# ONE bulk remap (single-writer on schedule_stream).
|
# ONE bulk remap (single-writer on schedule_stream).
|
||||||
self.virtual_to_physical[v_moveds_t] = dst_pages_t
|
self.virtual_to_physical[v_moveds_t] = dst_pages_t
|
||||||
@@ -2711,9 +2767,10 @@ class UnifiedMambaTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
|||||||
lazy_compaction: bool = False,
|
lazy_compaction: bool = False,
|
||||||
):
|
):
|
||||||
full_max = unified_buffer.max_slots("full")
|
full_max = unified_buffer.max_slots("full")
|
||||||
|
dcp_size = get_parallel().attn_dcp_size
|
||||||
super().__init__(
|
super().__init__(
|
||||||
size=full_max - 1,
|
size=(full_max - 1) * dcp_size,
|
||||||
page_size=page_size,
|
page_size=page_size * dcp_size,
|
||||||
dtype=unified_buffer.spec("full").get_dtype(),
|
dtype=unified_buffer.spec("full").get_dtype(),
|
||||||
device=device,
|
device=device,
|
||||||
kvcache=kvcache,
|
kvcache=kvcache,
|
||||||
@@ -2721,11 +2778,13 @@ class UnifiedMambaTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
|||||||
)
|
)
|
||||||
self.unified_buffer = unified_buffer
|
self.unified_buffer = unified_buffer
|
||||||
self._kvcache = kvcache
|
self._kvcache = kvcache
|
||||||
self.page_size = page_size
|
# Widened under DCP, matching the full sub-allocator; see its __init__.
|
||||||
|
self.page_size = page_size * dcp_size
|
||||||
self.lazy_compaction = lazy_compaction
|
self.lazy_compaction = lazy_compaction
|
||||||
|
|
||||||
# FULL is page-aware; MAMBA stays page_size=1 (state is per-request,
|
# FULL is page-aware; MAMBA stays page_size=1 (state is per-request,
|
||||||
# orthogonal to the full side's per-token paging).
|
# orthogonal to the full side's per-token paging), and only FULL shards
|
||||||
|
# under DCP: mamba state is replicated on every rank.
|
||||||
self.full_attn_allocator = MultiEndedAllocator(
|
self.full_attn_allocator = MultiEndedAllocator(
|
||||||
kvcache=kvcache.full_kv_pool,
|
kvcache=kvcache.full_kv_pool,
|
||||||
unified_buffer=unified_buffer,
|
unified_buffer=unified_buffer,
|
||||||
@@ -2733,6 +2792,7 @@ class UnifiedMambaTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
|||||||
device=device,
|
device=device,
|
||||||
is_id_owner=True,
|
is_id_owner=True,
|
||||||
page_size=page_size,
|
page_size=page_size,
|
||||||
|
shards_under_dcp=True,
|
||||||
need_sort=need_sort,
|
need_sort=need_sort,
|
||||||
forward_stream=forward_stream,
|
forward_stream=forward_stream,
|
||||||
lazy_compaction=lazy_compaction,
|
lazy_compaction=lazy_compaction,
|
||||||
@@ -2813,15 +2873,22 @@ class UnifiedMambaTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
|||||||
stays inside the JOINT budget. = mamba bytes/slot ÷ full bytes/token, rounded
|
stays inside the JOINT budget. = mamba bytes/slot ÷ full bytes/token, rounded
|
||||||
UP (conservative). Only on the shared composite (non-shared pools are separate,
|
UP (conservative). Only on the shared composite (non-shared pools are separate,
|
||||||
so the planner sources this via `getattr(..., None)`).
|
so the planner sources this via `getattr(..., None)`).
|
||||||
|
|
||||||
|
The planner charges this against `rem_total_tokens`, which is fed by
|
||||||
|
`available_size()` -- widened under DCP. One widened token is
|
||||||
|
`entry_bytes / dcp_size` local bytes, so the conversion carries the same
|
||||||
|
`dcp_size`; leaving it out under-reserves the shared gap by that factor.
|
||||||
"""
|
"""
|
||||||
return -(
|
return -(
|
||||||
-self.mamba_allocator.entry_bytes_per_page
|
-self.mamba_allocator.entry_bytes_per_page
|
||||||
|
* get_parallel().attn_dcp_size
|
||||||
// self.full_attn_allocator.entry_bytes
|
// self.full_attn_allocator.entry_bytes
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size_full(self) -> int:
|
def size_full(self) -> int:
|
||||||
return self.full_attn_allocator.max_slots - 1
|
# Widened like `size`: a logical token capacity, not a row count.
|
||||||
|
return (self.full_attn_allocator.max_slots - 1) * get_parallel().attn_dcp_size
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size_mamba(self) -> int:
|
def size_mamba(self) -> int:
|
||||||
@@ -2915,6 +2982,15 @@ class UnifiedMambaTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
|||||||
"""Full-pool virtual TOKEN ids -> kernel-facing ids."""
|
"""Full-pool virtual TOKEN ids -> kernel-facing ids."""
|
||||||
return self.full_attn_allocator.translate_kv_loc_for_kernel(loc, out=out)
|
return self.full_attn_allocator.translate_kv_loc_for_kernel(loc, out=out)
|
||||||
|
|
||||||
|
def translate_write_loc_for_kernel(
|
||||||
|
self,
|
||||||
|
loc: torch.Tensor,
|
||||||
|
*,
|
||||||
|
out: Optional[torch.Tensor] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Widened virtual WRITE loc -> DENSE id; see the sub-allocator's copy."""
|
||||||
|
return self.full_attn_allocator.translate_write_loc_for_kernel(loc, out=out)
|
||||||
|
|
||||||
def translate_kv_indices_for_transfer(
|
def translate_kv_indices_for_transfer(
|
||||||
self, kv_indices: torch.Tensor
|
self, kv_indices: torch.Tensor
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
@@ -2923,6 +2999,13 @@ class UnifiedMambaTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
|||||||
PHYSICAL, not kernel-facing: the transfer registers page ENVELOPES (see
|
PHYSICAL, not kernel-facing: the transfer registers page ENVELOPES (see
|
||||||
`UnifiedMLATokenToKVPool.get_contiguous_buf_infos`).
|
`UnifiedMLATokenToKVPool.get_contiguous_buf_infos`).
|
||||||
"""
|
"""
|
||||||
|
# Defensive: `_validate_unified_memory_dcp` rejects this pairing at
|
||||||
|
# argument validation, so reaching it means a config path got past that.
|
||||||
|
assert get_parallel().attn_dcp_size == 1, (
|
||||||
|
"PD-disaggregation transfer with the unified memory pool does not "
|
||||||
|
"support decode context parallelism: the transfer ships whole page "
|
||||||
|
"envelopes, which hold only this rank's shard of each widened page."
|
||||||
|
)
|
||||||
return self.full_attn_allocator.translate_kv_loc(kv_indices.to(torch.int64))
|
return self.full_attn_allocator.translate_kv_loc(kv_indices.to(torch.int64))
|
||||||
|
|
||||||
def set_disagg_move_gate(self, gate: Callable[[], bool]) -> None:
|
def set_disagg_move_gate(self, gate: Callable[[], bool]) -> None:
|
||||||
@@ -3361,6 +3444,17 @@ class UnifiedSWATokenToKVPoolAllocator(SWATokenToKVPoolAllocator):
|
|||||||
"""Full-pool virtual TOKEN ids -> kernel-facing ids."""
|
"""Full-pool virtual TOKEN ids -> kernel-facing ids."""
|
||||||
return self.full_attn_allocator.translate_kv_loc_for_kernel(loc, out=out)
|
return self.full_attn_allocator.translate_kv_loc_for_kernel(loc, out=out)
|
||||||
|
|
||||||
|
def translate_write_loc_for_kernel(
|
||||||
|
self,
|
||||||
|
loc: torch.Tensor,
|
||||||
|
*,
|
||||||
|
out: Optional[torch.Tensor] = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Widened virtual WRITE loc -> kernel-facing id; see the sub-allocator's
|
||||||
|
copy. DCP is rejected for this composite at argument validation, so this
|
||||||
|
is the dcp_size == 1 identity with the read translate."""
|
||||||
|
return self.full_attn_allocator.translate_write_loc_for_kernel(loc, out=out)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def swa_kernel_page_multiplier(self) -> int:
|
def swa_kernel_page_multiplier(self) -> int:
|
||||||
return self.swa_attn_allocator.kernel_page_multiplier
|
return self.swa_attn_allocator.kernel_page_multiplier
|
||||||
|
|||||||
@@ -708,6 +708,10 @@ class UnifiedMLATokenToKVPool(MLATokenToKVPool):
|
|||||||
# Lifetime owned by UnifiedKVPool; do not delete the views.
|
# Lifetime owned by UnifiedKVPool; do not delete the views.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# `rebind_write_loc` already collapsed the widened id and sent the rows this
|
||||||
|
# rank does not own to the padding sink.
|
||||||
|
write_loc_is_dcp_resolved = True
|
||||||
|
|
||||||
def get_kv_size_bytes(self):
|
def get_kv_size_bytes(self):
|
||||||
return 0 # UnifiedKVPool logs the total; per-sub-pool would double-count
|
return 0 # UnifiedKVPool logs the total; per-sub-pool would double-count
|
||||||
|
|
||||||
@@ -1332,7 +1336,7 @@ def init_unified_mamba_pools(
|
|||||||
max_size=req_to_token_pool._shared_mamba_size,
|
max_size=req_to_token_pool._shared_mamba_size,
|
||||||
device=device,
|
device=device,
|
||||||
)
|
)
|
||||||
# `_mamba_translate` feeds the HiCache offload path, GATED OFF here — wired but inert.
|
# Inert: this allocator implements neither reader (see HybridLinearKVPool).
|
||||||
req_to_token_pool.mamba_allocator = mamba_slot_allocator
|
req_to_token_pool.mamba_allocator = mamba_slot_allocator
|
||||||
token_to_kv_pool._mamba_translate = mamba_slot_allocator.translate
|
token_to_kv_pool._mamba_translate = mamba_slot_allocator.translate
|
||||||
# No full-KV translate hook is wired: both MLA doors now receive
|
# No full-KV translate hook is wired: both MLA doors now receive
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ from sglang.kernels.ops.kvcache.mla_buffer import (
|
|||||||
from sglang.kernels.ops.kvcache.mla_buffer import (
|
from sglang.kernels.ops.kvcache.mla_buffer import (
|
||||||
get_mla_kv_buffer_triton as get_mla_kv_buffer_triton,
|
get_mla_kv_buffer_triton as get_mla_kv_buffer_triton,
|
||||||
)
|
)
|
||||||
|
from sglang.kernels.ops.kvcache.mla_buffer import (
|
||||||
|
set_mla_kv_buffer_dcp_sharded_triton as set_mla_kv_buffer_dcp_sharded_triton,
|
||||||
|
)
|
||||||
from sglang.kernels.ops.kvcache.mla_buffer import (
|
from sglang.kernels.ops.kvcache.mla_buffer import (
|
||||||
set_mla_kv_buffer_fp8_quant_kernel as set_mla_kv_buffer_fp8_quant_kernel,
|
set_mla_kv_buffer_fp8_quant_kernel as set_mla_kv_buffer_fp8_quant_kernel,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -91,10 +91,8 @@ class ForwardBatchDeepSeekMHAMixin:
|
|||||||
self.prefix_chunk_starts_cpu[idx],
|
self.prefix_chunk_starts_cpu[idx],
|
||||||
self.prefix_chunk_seq_lens_cpu[idx],
|
self.prefix_chunk_seq_lens_cpu[idx],
|
||||||
)
|
)
|
||||||
# None on a backend that never bound a translator.
|
translator = get_attn_backend().kv_index_translator
|
||||||
src = get_attn_backend().kv_index_translator
|
chunk_kv_indices = translator.translate_dcp_read_ids(chunk_kv_indices)
|
||||||
if src is not None:
|
|
||||||
chunk_kv_indices = src.translate_full_attn_ids(chunk_kv_indices)
|
|
||||||
self.prefix_chunk_kv_indices.append(chunk_kv_indices)
|
self.prefix_chunk_kv_indices.append(chunk_kv_indices)
|
||||||
|
|
||||||
# Here we suppose the length of each chunk is equal
|
# Here we suppose the length of each chunk is equal
|
||||||
|
|||||||
@@ -1020,7 +1020,7 @@ class ModelRunner:
|
|||||||
self.attn_backend = backends.attn_backend
|
self.attn_backend = backends.attn_backend
|
||||||
self.decode_attn_backend = backends.decode_attn_backend
|
self.decode_attn_backend = backends.decode_attn_backend
|
||||||
self.decode_attn_backend_group = backends.decode_attn_backend_group
|
self.decode_attn_backend_group = backends.decode_attn_backend_group
|
||||||
self.kv_index_translator.assert_backends_carry_translator(
|
self.kv_index_translator.bind_and_verify_backends(
|
||||||
[self.attn_backend, self.decode_attn_backend]
|
[self.attn_backend, self.decode_attn_backend]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -499,6 +499,10 @@ class DeepseekMHAForwardMixin:
|
|||||||
# Without this, a chunked-prefill split (extend_prefix_lens != 0) that
|
# Without this, a chunked-prefill split (extend_prefix_lens != 0) that
|
||||||
# reads cached prefix KV crashes with "576 != 656".
|
# reads cached prefix KV crashes with "576 != 656".
|
||||||
kv_indices = filter_dcp_local_kv_indices(kv_indices=kv_indices)
|
kv_indices = filter_dcp_local_kv_indices(kv_indices=kv_indices)
|
||||||
|
# Read door: the pool never translates, so the production site does.
|
||||||
|
kv_indices = get_attn_backend().kv_index_translator.translate_dcp_read_ids(
|
||||||
|
kv_indices
|
||||||
|
)
|
||||||
kv_a, k_pe = get_token_to_kv_pool().get_mla_kv_buffer(
|
kv_a, k_pe = get_token_to_kv_pool().get_mla_kv_buffer(
|
||||||
self.attn_mha, kv_indices, torch.bfloat16
|
self.attn_mha, kv_indices, torch.bfloat16
|
||||||
)
|
)
|
||||||
|
|||||||
+8
-1
@@ -22,7 +22,10 @@ from sglang.srt.layers.quantization.fp8_utils import (
|
|||||||
materialize_bpreshuffle_fp8_scale_tuple,
|
materialize_bpreshuffle_fp8_scale_tuple,
|
||||||
)
|
)
|
||||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
||||||
from sglang.srt.model_executor.forward_context import get_token_to_kv_pool
|
from sglang.srt.model_executor.forward_context import (
|
||||||
|
get_attn_backend,
|
||||||
|
get_token_to_kv_pool,
|
||||||
|
)
|
||||||
from sglang.srt.models.deepseek_common.attention_forward_methods.forward_mha import (
|
from sglang.srt.models.deepseek_common.attention_forward_methods.forward_mha import (
|
||||||
forward_dsa_indexer_for_mha,
|
forward_dsa_indexer_for_mha,
|
||||||
resolve_attn_backend,
|
resolve_attn_backend,
|
||||||
@@ -306,6 +309,10 @@ class DeepseekMHARocmForwardMixin:
|
|||||||
):
|
):
|
||||||
if _use_aiter_gfx95:
|
if _use_aiter_gfx95:
|
||||||
kv_indices = filter_dcp_local_kv_indices(kv_indices=kv_indices)
|
kv_indices = filter_dcp_local_kv_indices(kv_indices=kv_indices)
|
||||||
|
# Read door: the pool never translates, so the production site does.
|
||||||
|
kv_indices = get_attn_backend().kv_index_translator.translate_dcp_read_ids(
|
||||||
|
kv_indices
|
||||||
|
)
|
||||||
kv_a, k_pe = get_token_to_kv_pool().get_mla_kv_buffer(
|
kv_a, k_pe = get_token_to_kv_pool().get_mla_kv_buffer(
|
||||||
self.attn_mha, kv_indices, dst_dtype
|
self.attn_mha, kv_indices, dst_dtype
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -70,7 +70,9 @@ class TestFilterDcpLocalChunkKvIndices(CustomTestCase):
|
|||||||
return torch.cat(runs) if runs else torch.empty(0, dtype=torch.int64)
|
return torch.cat(runs) if runs else torch.empty(0, dtype=torch.int64)
|
||||||
|
|
||||||
def _owner_rule(self, kv, dcp_size, dcp_rank):
|
def _owner_rule(self, kv, dcp_size, dcp_rank):
|
||||||
return kv[kv % dcp_size == dcp_rank] // dcp_size
|
# Selection only: the filters leave ids WIDENED and the collapse now
|
||||||
|
# happens once, in KVIndexTranslator.translate_dcp_read_ids.
|
||||||
|
return kv[kv % dcp_size == dcp_rank]
|
||||||
|
|
||||||
def _run(self, starts, lens, dcp_size, dcp_rank, seed=0):
|
def _run(self, starts, lens, dcp_size, dcp_rank, seed=0):
|
||||||
kv = self._build_chunk(starts, lens, dcp_size, seed)
|
kv = self._build_chunk(starts, lens, dcp_size, seed)
|
||||||
|
|||||||
@@ -70,5 +70,33 @@ class TestKimiLinearUnifiedMemoryFlashMLA(TestKimiLinearUnifiedMemory):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TestKimiLinearUnifiedMemoryDCP(
|
||||||
|
GSM8KMixin, PrefixCacheBranchingMixin, DefaultServerBase
|
||||||
|
):
|
||||||
|
"""Unified memory + decode context parallelism.
|
||||||
|
|
||||||
|
`test_prefix_cache_branching` is the sharp one here: a radix hit replays
|
||||||
|
widened virtual locs whose pages may have moved under compaction, and each
|
||||||
|
rank must recover the same physical page from them while keeping a
|
||||||
|
different row inside it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = KIMI_LINEAR_MODEL
|
||||||
|
cache_chunk_size = 64
|
||||||
|
gsm8k_score_threshold = 0.88
|
||||||
|
other_args = [
|
||||||
|
"--trust-remote-code",
|
||||||
|
"--tp-size",
|
||||||
|
"2",
|
||||||
|
"--dcp-size",
|
||||||
|
"2",
|
||||||
|
"--attention-backend",
|
||||||
|
"flashinfer",
|
||||||
|
"--chunked-prefill-size",
|
||||||
|
"2048",
|
||||||
|
"--enable-unified-memory",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -176,7 +176,10 @@ class TestUnifiedSWATombstoneClamp(unittest.TestCase):
|
|||||||
# A real sub-allocator (not a stand-in): the translation reads its v2p
|
# A real sub-allocator (not a stand-in): the translation reads its v2p
|
||||||
# table, and the pool reaches it through the allocator's own method.
|
# table, and the pool reaches it through the allocator's own method.
|
||||||
swa_allocator = object.__new__(MultiEndedAllocator)
|
swa_allocator = object.__new__(MultiEndedAllocator)
|
||||||
|
# `page_size` is the WIDENED (DCP) surface, `pool_page_size` the physical
|
||||||
|
# rows per page; equal at dcp_size == 1, which is what this fixture is.
|
||||||
swa_allocator.page_size = page_size
|
swa_allocator.page_size = page_size
|
||||||
|
swa_allocator.pool_page_size = page_size
|
||||||
swa_allocator.virtual_to_physical = v2p
|
swa_allocator.virtual_to_physical = v2p
|
||||||
swa_allocator.kernel_page_multiplier = multiplier
|
swa_allocator.kernel_page_multiplier = multiplier
|
||||||
pool = object.__new__(UnifiedSWAKVPool)
|
pool = object.__new__(UnifiedSWAKVPool)
|
||||||
@@ -352,5 +355,61 @@ class TestHybridLinearMLARouting(unittest.TestCase):
|
|||||||
self.assertIs(pool.full_kv_pool.mla_get_calls[0], loc)
|
self.assertIs(pool.full_kv_pool.mla_get_calls[0], loc)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMlaWriteDoorsUnderDcp(unittest.TestCase):
|
||||||
|
"""Which MLA write door is DCP-aware, and which refuses.
|
||||||
|
|
||||||
|
`set_mla_kv_buffer` resolves the owner rule inside its kernel, so it owns
|
||||||
|
the DCP write. `set_kv_buffer` (the combined latent+rope row) cannot: the
|
||||||
|
two backends that could reach it disagree on the loc space -- flashinfer's
|
||||||
|
`k_rope is None` branch passes a WIDENED loc, the Triton backend one it
|
||||||
|
already collapsed -- so there is no single correct translation. It used to
|
||||||
|
select `loc % dcp_size == dcp_rank` and then write WITHOUT dividing, i.e.
|
||||||
|
widened ids straight into a rank-local buffer. Refusing is the contract;
|
||||||
|
a re-added masked-but-undivided write is what this guards."""
|
||||||
|
|
||||||
|
def _bare_mla_pool(self):
|
||||||
|
from sglang.srt.mem_cache.memory_pool import MLATokenToKVPool
|
||||||
|
|
||||||
|
pool = object.__new__(MLATokenToKVPool)
|
||||||
|
pool.size = 64
|
||||||
|
pool.page_size = 1
|
||||||
|
pool.kernel_page_blocks = 1
|
||||||
|
pool.start_layer = 0
|
||||||
|
pool.dtype = torch.float16
|
||||||
|
pool.store_dtype = torch.float16
|
||||||
|
pool.dsa_kv_cache_store_fp8 = False
|
||||||
|
pool.kv_buffer = [torch.zeros((65, 1, 8), dtype=torch.float16)]
|
||||||
|
return pool
|
||||||
|
|
||||||
|
def test_set_kv_buffer_refuses_under_dcp(self):
|
||||||
|
from sglang.srt.runtime_context import get_parallel
|
||||||
|
|
||||||
|
pool = self._bare_mla_pool()
|
||||||
|
layer = types.SimpleNamespace(layer_id=0)
|
||||||
|
loc = torch.tensor([0, 1, 2, 3], dtype=torch.int64)
|
||||||
|
cache_k = torch.ones((4, 1, 8), dtype=torch.float16)
|
||||||
|
|
||||||
|
with get_parallel().override(
|
||||||
|
dcp_enabled=True, attn_dcp_size=2, attn_dcp_rank=1
|
||||||
|
):
|
||||||
|
with self.assertRaises(AssertionError) as cm:
|
||||||
|
pool.set_kv_buffer(layer, _loc_info(loc), cache_k, None)
|
||||||
|
self.assertIn("set_mla_kv_buffer", str(cm.exception))
|
||||||
|
# Nothing was written on the way to refusing.
|
||||||
|
self.assertTrue(bool((pool.kv_buffer[0] == 0).all()))
|
||||||
|
|
||||||
|
def test_set_kv_buffer_still_writes_without_dcp(self):
|
||||||
|
pool = self._bare_mla_pool()
|
||||||
|
layer = types.SimpleNamespace(layer_id=0)
|
||||||
|
loc = torch.tensor([3, 5], dtype=torch.int64)
|
||||||
|
cache_k = torch.ones((2, 1, 8), dtype=torch.float16)
|
||||||
|
|
||||||
|
pool.set_kv_buffer(layer, _loc_info(loc), cache_k, None)
|
||||||
|
|
||||||
|
self.assertTrue(bool((pool.kv_buffer[0][3] == 1).all()))
|
||||||
|
self.assertTrue(bool((pool.kv_buffer[0][5] == 1).all()))
|
||||||
|
self.assertTrue(bool((pool.kv_buffer[0][4] == 0).all()))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from sglang.test.ci.ci_register import register_cpu_ci
|
|||||||
|
|
||||||
register_cpu_ci(est_time=8, suite="base-a-test-cpu")
|
register_cpu_ci(est_time=8, suite="base-a-test-cpu")
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import random
|
import random
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ from sglang.srt.mem_cache.unified_memory_pool import (
|
|||||||
MLASubPoolSpec,
|
MLASubPoolSpec,
|
||||||
UnifiedKVPool,
|
UnifiedKVPool,
|
||||||
)
|
)
|
||||||
|
from sglang.srt.runtime_context import get_parallel
|
||||||
|
|
||||||
_DEV = "cpu"
|
_DEV = "cpu"
|
||||||
|
|
||||||
@@ -3299,5 +3301,199 @@ class TestFloatMultiEndedAllocator(unittest.TestCase):
|
|||||||
fla.bind_peer(sa)
|
fla.bind_peer(sa)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDcpWidening(unittest.TestCase):
|
||||||
|
"""`dcp_size > 1`: the alloc surface speaks a widened virtual id space while
|
||||||
|
the pool keeps storing one row per `dcp_size` logical ids."""
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _dcp(self, dcp_size, dcp_rank=0):
|
||||||
|
"""The width comes from the parallel context, not a constructor
|
||||||
|
argument, so one scope has to hold construction and every read."""
|
||||||
|
with get_parallel().override(
|
||||||
|
dcp_enabled=dcp_size > 1,
|
||||||
|
attn_dcp_size=dcp_size,
|
||||||
|
attn_dcp_rank=dcp_rank,
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
def _build_pair(self, *, page_size, n_full_slots=64):
|
||||||
|
"""(full, mamba) as the composite wires them: only full shards."""
|
||||||
|
full = _make_mha_spec("full", "up", layer_num=2)
|
||||||
|
mamba = _make_mamba_spec("mamba", "down", layer_num=2)
|
||||||
|
pool = UnifiedKVPool(
|
||||||
|
total_bytes=full.entry_bytes() * n_full_slots + mamba.entry_bytes() * 16,
|
||||||
|
sub_pool_specs=[full, mamba],
|
||||||
|
device=_DEV,
|
||||||
|
enable_memory_saver=False,
|
||||||
|
page_size=page_size,
|
||||||
|
)
|
||||||
|
alloc = MultiEndedAllocator(
|
||||||
|
kvcache=_FakeKVCache(pool.max_slots("full")),
|
||||||
|
unified_buffer=pool,
|
||||||
|
sub_pool_name="full",
|
||||||
|
device=_DEV,
|
||||||
|
is_id_owner=True,
|
||||||
|
page_size=page_size,
|
||||||
|
shards_under_dcp=True,
|
||||||
|
)
|
||||||
|
# The peer stays slot-granular: mamba state is replicated, not sharded.
|
||||||
|
mamba = MultiEndedAllocator(
|
||||||
|
kvcache=_FakeKVCache(pool.max_slots("mamba")),
|
||||||
|
unified_buffer=pool,
|
||||||
|
sub_pool_name="mamba",
|
||||||
|
device=_DEV,
|
||||||
|
is_id_owner=True,
|
||||||
|
)
|
||||||
|
alloc.bind_peer(mamba)
|
||||||
|
return alloc, mamba
|
||||||
|
|
||||||
|
def _build(self, *, page_size, n_full_slots=64):
|
||||||
|
return self._build_pair(page_size=page_size, n_full_slots=n_full_slots)[0]
|
||||||
|
|
||||||
|
def test_replicated_peer_stays_slot_granular_under_dcp(self):
|
||||||
|
"""BUG REGRESSION. Widening every sub-allocator off the process DCP
|
||||||
|
width, rather than only the sharding one, makes the Mamba page size
|
||||||
|
dcp_size; state is allocated one slot per request, so the first request
|
||||||
|
fails the page-multiple check."""
|
||||||
|
with self._dcp(4):
|
||||||
|
_, mamba = self._build_pair(page_size=2)
|
||||||
|
self.assertEqual(mamba.page_size, 1)
|
||||||
|
self.assertEqual(mamba.page_size, mamba.pool_page_size)
|
||||||
|
self.assertIsNotNone(mamba.alloc(1))
|
||||||
|
|
||||||
|
def test_capacity_scales_but_physical_pages_do_not(self):
|
||||||
|
for page_size in (1, 8):
|
||||||
|
with self._dcp(1):
|
||||||
|
base = self._build(page_size=page_size)
|
||||||
|
base_pages = base.num_pages
|
||||||
|
base_page_bytes = base.entry_bytes_per_page
|
||||||
|
base_avail = base.available_size()
|
||||||
|
for dcp_size in (2, 4):
|
||||||
|
with self._dcp(dcp_size):
|
||||||
|
a = self._build(page_size=page_size)
|
||||||
|
self.assertEqual(a.page_size, page_size * dcp_size)
|
||||||
|
self.assertEqual(a.pool_page_size, page_size)
|
||||||
|
# Same rows, same bytes per page; only the id space grows.
|
||||||
|
self.assertEqual(a.num_pages, base_pages)
|
||||||
|
self.assertEqual(a.entry_bytes_per_page, base_page_bytes)
|
||||||
|
self.assertEqual(a.available_size(), base_avail * dcp_size)
|
||||||
|
|
||||||
|
def test_alloc_returns_whole_widened_pages(self):
|
||||||
|
with self._dcp(2, 1):
|
||||||
|
a = self._build(page_size=4)
|
||||||
|
ids = a.alloc(3 * 8) # 3 widened pages of 4*2 ids
|
||||||
|
self.assertIsNotNone(ids)
|
||||||
|
pages = ids.view(3, 8)
|
||||||
|
self.assertTrue(
|
||||||
|
torch.equal(pages[:, 1:] - pages[:, :-1], torch.ones(3, 7).long())
|
||||||
|
)
|
||||||
|
self.assertTrue(bool((pages[:, 0] % 8 == 0).all()))
|
||||||
|
# Freeing the widened ids releases exactly the pages they came from.
|
||||||
|
before = a.available_size()
|
||||||
|
a.free(ids)
|
||||||
|
self.assertEqual(a.available_size(), before + 3 * 8)
|
||||||
|
|
||||||
|
def test_every_rank_maps_a_widened_page_to_one_physical_page(self):
|
||||||
|
"""The DCP ranks must agree on the physical page a widened page uses;
|
||||||
|
only the row WITHIN it differs, by `(loc % dcp) -> loc // dcp`."""
|
||||||
|
dcp_size = 4
|
||||||
|
with self._dcp(dcp_size):
|
||||||
|
allocs = [self._build(page_size=2) for _ in range(dcp_size)]
|
||||||
|
ids = [a.alloc(2 * dcp_size * 2) for a in allocs]
|
||||||
|
for i in ids:
|
||||||
|
self.assertIsNotNone(i)
|
||||||
|
# Same allocation order -> same widened ids on every rank.
|
||||||
|
for i in ids[1:]:
|
||||||
|
self.assertTrue(torch.equal(i, ids[0]))
|
||||||
|
for rank, (a, i) in enumerate(zip(allocs, ids)):
|
||||||
|
with self._dcp(dcp_size, rank):
|
||||||
|
owned = (i % dcp_size) == rank
|
||||||
|
self.assertEqual(int(owned.sum()), i.numel() // dcp_size)
|
||||||
|
phys = a.translate_kv_loc(i[owned] // dcp_size)
|
||||||
|
# Collapsed ids land inside this rank's physical rows,
|
||||||
|
# contiguously within each page, never on the reserved sink.
|
||||||
|
self.assertTrue(bool((phys > 0).all()))
|
||||||
|
self.assertTrue(bool((phys < a.max_slots).all()))
|
||||||
|
self.assertEqual(len(set(phys.tolist())), phys.numel())
|
||||||
|
|
||||||
|
def test_write_translate_tombstones_unowned_ids(self):
|
||||||
|
dcp_size = 2
|
||||||
|
for rank in range(dcp_size):
|
||||||
|
with self._dcp(dcp_size, rank):
|
||||||
|
a = self._build(page_size=2)
|
||||||
|
ids = a.alloc(2 * dcp_size * 3)
|
||||||
|
written = a.translate_write_loc_for_kernel(ids)
|
||||||
|
owned = (ids % dcp_size) == rank
|
||||||
|
# Owned ids agree with the read translate of the collapsed id...
|
||||||
|
self.assertTrue(
|
||||||
|
torch.equal(
|
||||||
|
written[owned],
|
||||||
|
a.translate_kv_loc_for_kernel(ids[owned] // dcp_size),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# ...and the rest go to the sink the write kernels skip.
|
||||||
|
self.assertTrue(bool((written[~owned] == 0).all()))
|
||||||
|
self.assertTrue(bool((written[owned] > 0).all()))
|
||||||
|
|
||||||
|
def _build_composite(self, *, page_size):
|
||||||
|
from sglang.srt.mem_cache.multi_ended_allocator import (
|
||||||
|
UnifiedMambaTokenToKVPoolAllocator,
|
||||||
|
)
|
||||||
|
|
||||||
|
full_spec = _make_mha_spec("full", "up", layer_num=2)
|
||||||
|
mamba_spec = _make_mamba_spec("mamba", "down", layer_num=2)
|
||||||
|
pool = UnifiedKVPool(
|
||||||
|
total_bytes=16 * page_size * full_spec.entry_bytes()
|
||||||
|
+ 8 * mamba_spec.entry_bytes(),
|
||||||
|
sub_pool_specs=[full_spec, mamba_spec],
|
||||||
|
device=_DEV,
|
||||||
|
enable_memory_saver=False,
|
||||||
|
page_size=page_size,
|
||||||
|
)
|
||||||
|
full_kv = _FakeKVCache(pool.max_slots("full"))
|
||||||
|
mamba_kv = _FakeKVCache(pool.max_slots("mamba"))
|
||||||
|
mamba_kv._copy_from_physical = lambda src, dst: None
|
||||||
|
|
||||||
|
class _FakeHybridLinearKVPool:
|
||||||
|
full_kv_pool = full_kv
|
||||||
|
mamba_pool = mamba_kv
|
||||||
|
|
||||||
|
return UnifiedMambaTokenToKVPoolAllocator(
|
||||||
|
unified_buffer=pool,
|
||||||
|
kvcache=_FakeHybridLinearKVPool(),
|
||||||
|
device=_DEV,
|
||||||
|
page_size=page_size,
|
||||||
|
need_sort=False,
|
||||||
|
forward_stream=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_mamba_slot_cost_is_in_the_same_units_as_available_size(self):
|
||||||
|
"""The planner charges `mamba_slot_full_token_cost()` against a budget
|
||||||
|
fed by `available_size()`. Both are bytes/entry_bytes conversions, so
|
||||||
|
both carry `dcp_size`; if only the budget widens, every Mamba state is
|
||||||
|
under-reserved by that factor and a batch is admitted whose later
|
||||||
|
allocations cross the shared byte frontier."""
|
||||||
|
for page_size in (1, 8):
|
||||||
|
with self._dcp(1):
|
||||||
|
base = self._build_composite(page_size=page_size)
|
||||||
|
base_cost = base.mamba_slot_full_token_cost()
|
||||||
|
base_avail = base.available_size()
|
||||||
|
self.assertGreater(base_cost, 0)
|
||||||
|
for dcp_size in (2, 4):
|
||||||
|
with self._dcp(dcp_size):
|
||||||
|
a = self._build_composite(page_size=page_size)
|
||||||
|
self.assertEqual(a.available_size(), base_avail * dcp_size)
|
||||||
|
mamba_bytes = a.mamba_allocator.entry_bytes_per_page
|
||||||
|
full_entry = a.full_attn_allocator.entry_bytes
|
||||||
|
cost = a.mamba_slot_full_token_cost()
|
||||||
|
# A widened token is `full_entry / dcp_size` bytes, so the
|
||||||
|
# reservation covers the slot...
|
||||||
|
self.assertGreaterEqual(cost * full_entry, mamba_bytes * dcp_size)
|
||||||
|
# ...and stays tight (rounds up by less than one token).
|
||||||
|
self.assertLess((cost - 1) * full_entry, mamba_bytes * dcp_size)
|
||||||
|
# The un-scaled cost -- the bug -- would not have covered it.
|
||||||
|
self.assertLess(base_cost * full_entry, mamba_bytes * dcp_size)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ def _armed_source(v2p, swa_map):
|
|||||||
)
|
)
|
||||||
src.is_translating = True
|
src.is_translating = True
|
||||||
src._translate_full = lambda t, out=None: v2p[t.to(torch.int64)]
|
src._translate_full = lambda t, out=None: v2p[t.to(torch.int64)]
|
||||||
|
# The WRITE loc has its own translate because under DCP it arrives widened;
|
||||||
|
# at dcp_size == 1 it is the read translate, so arm it with the same fake.
|
||||||
|
src._translate_write_full = src._translate_full
|
||||||
# Phase 2 derives from kernel-facing values through p2v + the swa v2p; arm
|
# Phase 2 derives from kernel-facing values through p2v + the swa v2p; arm
|
||||||
# the inverse of the fake v2p (ps=1, both multipliers 1: kernel == physical,
|
# the inverse of the fake v2p (ps=1, both multipliers 1: kernel == physical,
|
||||||
# and the expected swa loc for virtual t is swa_map[t]).
|
# and the expected swa loc for virtual t is swa_map[t]).
|
||||||
|
|||||||
Reference in New Issue
Block a user