Support mxfp8 KV cache in PD transfer (#35718)
This commit is contained in:
@@ -24,6 +24,11 @@ class StateType(str, enum.Enum):
|
|||||||
SWA_RING = "swa_ring"
|
SWA_RING = "swa_ring"
|
||||||
# DeepSeek-V4 online C128 request-scoped state.
|
# DeepSeek-V4 online C128 request-scoped state.
|
||||||
C128_STATE = "c128_state"
|
C128_STATE = "c128_state"
|
||||||
|
# A block-scaled KV dtype keeps its per-block scales in buffers parallel to
|
||||||
|
# K/V, one component per sub-pool so each carries the index payload of the
|
||||||
|
# KV it describes (whole sequence for full attention, window for SWA).
|
||||||
|
BLOCK_SCALE = "block_scale"
|
||||||
|
BLOCK_SCALE_SWA = "block_scale_swa"
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
|
|||||||
@@ -1379,7 +1379,7 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
|
|||||||
)
|
)
|
||||||
return kv_to_page_indices(window_kv_indices_swa, page_size)
|
return kv_to_page_indices(window_kv_indices_swa, page_size)
|
||||||
|
|
||||||
def _dsa_payload():
|
def _full_kv_pages_payload():
|
||||||
kv_indices_full = self.req_to_token_pool.req_to_token[
|
kv_indices_full = self.req_to_token_pool.req_to_token[
|
||||||
decode_req.req.req_pool_idx, :seq_len
|
decode_req.req.req_pool_idx, :seq_len
|
||||||
]
|
]
|
||||||
@@ -1415,15 +1415,15 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
|
|||||||
)
|
)
|
||||||
if clear_c128_state is not None:
|
if clear_c128_state is not None:
|
||||||
clear_c128_state(int(decode_req.req.req_pool_idx))
|
clear_c128_state(int(decode_req.req.req_pool_idx))
|
||||||
# MINIMAX_INDEX_K reuses _dsa_payload: index rows live at the same loc
|
|
||||||
# as main KV on the same page_size.
|
|
||||||
payloads = {
|
payloads = {
|
||||||
StateType.MAMBA: _mamba_payload,
|
StateType.MAMBA: _mamba_payload,
|
||||||
StateType.SWA: _swa_payload,
|
StateType.SWA: _swa_payload,
|
||||||
StateType.DSA: _dsa_payload,
|
StateType.DSA: _full_kv_pages_payload,
|
||||||
StateType.MINIMAX_INDEX_K: _dsa_payload,
|
StateType.MINIMAX_INDEX_K: _full_kv_pages_payload,
|
||||||
StateType.SWA_RING: _swa_ring_payload,
|
StateType.SWA_RING: _swa_ring_payload,
|
||||||
StateType.C128_STATE: _c128_state_payload,
|
StateType.C128_STATE: _c128_state_payload,
|
||||||
|
StateType.BLOCK_SCALE: _full_kv_pages_payload,
|
||||||
|
StateType.BLOCK_SCALE_SWA: _swa_payload,
|
||||||
}
|
}
|
||||||
if _is_npu and isinstance(self.token_to_kv_pool, DeepSeekV4TokenToKVPool):
|
if _is_npu and isinstance(self.token_to_kv_pool, DeepSeekV4TokenToKVPool):
|
||||||
from sglang.srt.hardware_backend.npu.dsv4.dsv4_common_hooks import (
|
from sglang.srt.hardware_backend.npu.dsv4.dsv4_common_hooks import (
|
||||||
|
|||||||
@@ -1255,6 +1255,8 @@ class MooncakeKVManager(CommonKVManager):
|
|||||||
StateType.DSA,
|
StateType.DSA,
|
||||||
StateType.SWA_RING,
|
StateType.SWA_RING,
|
||||||
StateType.C128_STATE,
|
StateType.C128_STATE,
|
||||||
|
StateType.BLOCK_SCALE,
|
||||||
|
StateType.BLOCK_SCALE_SWA,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _requires_exact_state_index_match(self, st: StateType) -> bool:
|
def _requires_exact_state_index_match(self, st: StateType) -> bool:
|
||||||
|
|||||||
@@ -1212,7 +1212,7 @@ class SchedulerDisaggregationPrefillMixin:
|
|||||||
)
|
)
|
||||||
return kv_to_page_indices(window_kv_indices_swa, page_size)
|
return kv_to_page_indices(window_kv_indices_swa, page_size)
|
||||||
|
|
||||||
def _dsa_payload():
|
def _full_kv_pages_payload():
|
||||||
kv_indices_full = self.req_to_token_pool.req_to_token[
|
kv_indices_full = self.req_to_token_pool.req_to_token[
|
||||||
req.req_pool_idx, :seq_len
|
req.req_pool_idx, :seq_len
|
||||||
]
|
]
|
||||||
@@ -1250,15 +1250,15 @@ class SchedulerDisaggregationPrefillMixin:
|
|||||||
state_types = (
|
state_types = (
|
||||||
self.disagg_prefill_bootstrap_queue.kv_manager.kv_args.state_types
|
self.disagg_prefill_bootstrap_queue.kv_manager.kv_args.state_types
|
||||||
)
|
)
|
||||||
# MINIMAX_INDEX_K reuses _dsa_payload: index rows live at the same loc
|
|
||||||
# as main KV on the same page_size.
|
|
||||||
payloads = {
|
payloads = {
|
||||||
StateType.MAMBA: _mamba_payload,
|
StateType.MAMBA: _mamba_payload,
|
||||||
StateType.SWA: _swa_payload,
|
StateType.SWA: _swa_payload,
|
||||||
StateType.DSA: _dsa_payload,
|
StateType.DSA: _full_kv_pages_payload,
|
||||||
StateType.MINIMAX_INDEX_K: _dsa_payload,
|
StateType.MINIMAX_INDEX_K: _full_kv_pages_payload,
|
||||||
StateType.SWA_RING: _swa_ring_payload,
|
StateType.SWA_RING: _swa_ring_payload,
|
||||||
StateType.C128_STATE: _c128_state_payload,
|
StateType.C128_STATE: _c128_state_payload,
|
||||||
|
StateType.BLOCK_SCALE: _full_kv_pages_payload,
|
||||||
|
StateType.BLOCK_SCALE_SWA: _swa_payload,
|
||||||
}
|
}
|
||||||
if _is_npu and isinstance(
|
if _is_npu and isinstance(
|
||||||
self.token_to_kv_pool_allocator.get_kvcache(),
|
self.token_to_kv_pool_allocator.get_kvcache(),
|
||||||
|
|||||||
@@ -1051,8 +1051,10 @@ def setup_state_kv_args(
|
|||||||
from sglang.srt.mem_cache.memory_pool import (
|
from sglang.srt.mem_cache.memory_pool import (
|
||||||
DSATokenToKVPool,
|
DSATokenToKVPool,
|
||||||
HybridLinearKVPool,
|
HybridLinearKVPool,
|
||||||
|
MHATokenToKVPoolMXFP8,
|
||||||
MiniMaxSparseKVPool,
|
MiniMaxSparseKVPool,
|
||||||
)
|
)
|
||||||
|
from sglang.srt.mem_cache.swa_memory_pool import SWAKVPool
|
||||||
|
|
||||||
kv_args.state_types = []
|
kv_args.state_types = []
|
||||||
kv_args.state_data_ptrs = []
|
kv_args.state_data_ptrs = []
|
||||||
@@ -1064,6 +1066,13 @@ def setup_state_kv_args(
|
|||||||
kv_args.is_hybrid_mla_backend = False
|
kv_args.is_hybrid_mla_backend = False
|
||||||
kv_args.state_conv_shard_groups = []
|
kv_args.state_conv_shard_groups = []
|
||||||
|
|
||||||
|
if isinstance(token_to_kv_pool, MHATokenToKVPoolMXFP8):
|
||||||
|
append_state_component(
|
||||||
|
kv_args,
|
||||||
|
StateType.BLOCK_SCALE,
|
||||||
|
*token_to_kv_pool.get_kv_scale_buf_infos(),
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(token_to_kv_pool, MiniMaxSparseKVPool):
|
if isinstance(token_to_kv_pool, MiniMaxSparseKVPool):
|
||||||
if token_to_kv_pool.index_kv_pool is not None:
|
if token_to_kv_pool.index_kv_pool is not None:
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
@@ -1082,6 +1091,23 @@ def setup_state_kv_args(
|
|||||||
append_state_component(
|
append_state_component(
|
||||||
kv_args, StateType.SWA, data_ptrs, data_lens, item_lens
|
kv_args, StateType.SWA, data_ptrs, data_lens, item_lens
|
||||||
)
|
)
|
||||||
|
# MXFP8 KV: each sub-pool's block scales ride as their own component
|
||||||
|
# so they inherit the index payload of the KV they describe.
|
||||||
|
# Only the concrete SWAKVPool owns a full sub-pool; other
|
||||||
|
# BaseSWAKVPool implementations describe their state per entry.
|
||||||
|
if isinstance(token_to_kv_pool, SWAKVPool) and isinstance(
|
||||||
|
token_to_kv_pool.full_kv_pool, MHATokenToKVPoolMXFP8
|
||||||
|
):
|
||||||
|
append_state_component(
|
||||||
|
kv_args,
|
||||||
|
StateType.BLOCK_SCALE,
|
||||||
|
*token_to_kv_pool.get_kv_scale_buf_infos(),
|
||||||
|
)
|
||||||
|
append_state_component(
|
||||||
|
kv_args,
|
||||||
|
StateType.BLOCK_SCALE_SWA,
|
||||||
|
*token_to_kv_pool.get_swa_kv_scale_buf_infos(),
|
||||||
|
)
|
||||||
# unified_kv: the SWA ring lives in the unified buffers (no separate
|
# unified_kv: the SWA ring lives in the unified buffers (no separate
|
||||||
# swa_kv_pool) and is addressed per-row, so ship it as SWA_RING.
|
# swa_kv_pool) and is addressed per-row, so ship it as SWA_RING.
|
||||||
if getattr(token_to_kv_pool, "_unified_kv", False) and hasattr(
|
if getattr(token_to_kv_pool, "_unified_kv", False) and hasattr(
|
||||||
|
|||||||
@@ -3403,6 +3403,9 @@ class MHATokenToKVPoolMXFP8(MHATokenToKVPool):
|
|||||||
],
|
],
|
||||||
device=self.device,
|
device=self.device,
|
||||||
)
|
)
|
||||||
|
# This override replaces the base allocation, so the PD-transfer
|
||||||
|
# descriptors for the packed data buffers are built here too.
|
||||||
|
self._kv_buffer_descs = self._build_kv_buffer_descs()
|
||||||
|
|
||||||
def _clear_buffers(self):
|
def _clear_buffers(self):
|
||||||
del self.k_buffer
|
del self.k_buffer
|
||||||
@@ -3552,11 +3555,21 @@ class MHATokenToKVPoolMXFP8(MHATokenToKVPool):
|
|||||||
def load_cpu_copy(self, kv_cache_cpu, indices, mamba_indices=None):
|
def load_cpu_copy(self, kv_cache_cpu, indices, mamba_indices=None):
|
||||||
raise NotImplementedError("CPU offloading is unsupported for MXFP8 KV cache.")
|
raise NotImplementedError("CPU offloading is unsupported for MXFP8 KV cache.")
|
||||||
|
|
||||||
def get_contiguous_buf_infos(self):
|
def get_kv_scale_buf_infos(self):
|
||||||
raise NotImplementedError(
|
"""(ptrs, lens, item_lens) for the UE8M0 scale buffers, k then v.
|
||||||
"KV transfer / disaggregation is unsupported for MXFP8 KV cache "
|
|
||||||
"(scale buffers are not exposed)."
|
The interleaved layout puts pages on the leading axis, so a page's
|
||||||
)
|
scales are one contiguous row; the flat layout is per slot.
|
||||||
|
"""
|
||||||
|
tensors = self.k_scale_buffer + self.v_scale_buffer
|
||||||
|
ptrs = [t.data_ptr() for t in tensors]
|
||||||
|
lens = [t.nbytes for t in tensors]
|
||||||
|
row_bytes = [t[0].nbytes for t in tensors]
|
||||||
|
if self.mxfp8_sf_interleaved:
|
||||||
|
item_lens = row_bytes
|
||||||
|
else:
|
||||||
|
item_lens = [rb * self.page_size for rb in row_bytes]
|
||||||
|
return ptrs, lens, item_lens
|
||||||
|
|
||||||
def set_kv_buffer_prefix_valid(self, *args, **kwargs):
|
def set_kv_buffer_prefix_valid(self, *args, **kwargs):
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
|
|||||||
@@ -137,6 +137,12 @@ class SWAKVPool(BaseSWAKVPool):
|
|||||||
full_kv_item_lens,
|
full_kv_item_lens,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_kv_scale_buf_infos(self):
|
||||||
|
return self.full_kv_pool.get_kv_scale_buf_infos()
|
||||||
|
|
||||||
|
def get_swa_kv_scale_buf_infos(self):
|
||||||
|
return self.swa_kv_pool.get_kv_scale_buf_infos()
|
||||||
|
|
||||||
def get_state_buf_infos(self):
|
def get_state_buf_infos(self):
|
||||||
swa_kv_data_ptrs, swa_kv_data_lens, swa_kv_item_lens = (
|
swa_kv_data_ptrs, swa_kv_data_lens, swa_kv_item_lens = (
|
||||||
self.swa_kv_pool.get_contiguous_buf_infos()
|
self.swa_kv_pool.get_contiguous_buf_infos()
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from sglang.srt.mem_cache.memory_pool import MHATokenToKVPoolMXFP8
|
||||||
|
from sglang.test.ci.ci_register import register_cpu_ci
|
||||||
|
|
||||||
|
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
|
||||||
|
|
||||||
|
LAYERS = 2
|
||||||
|
PAGE_SIZE = 128
|
||||||
|
|
||||||
|
|
||||||
|
def _pool(interleaved: bool) -> MHATokenToKVPoolMXFP8:
|
||||||
|
"""A pool stub carrying only what the scale accessor reads."""
|
||||||
|
pool = object.__new__(MHATokenToKVPoolMXFP8)
|
||||||
|
pool.page_size = PAGE_SIZE
|
||||||
|
pool.mxfp8_sf_interleaved = interleaved
|
||||||
|
# Interleaved keeps pages on the leading axis; flat keeps slots.
|
||||||
|
shape = (4, 3, 32, 4, 2) if interleaved else (4 * PAGE_SIZE, 3, 2)
|
||||||
|
pool.k_scale_buffer = [
|
||||||
|
torch.zeros(shape, dtype=torch.float8_e8m0fnu) for _ in range(LAYERS)
|
||||||
|
]
|
||||||
|
pool.v_scale_buffer = [
|
||||||
|
torch.zeros(shape, dtype=torch.float8_e8m0fnu) for _ in range(LAYERS)
|
||||||
|
]
|
||||||
|
return pool
|
||||||
|
|
||||||
|
|
||||||
|
class TestMXFP8ScaleTransferBuffers(unittest.TestCase):
|
||||||
|
def test_interleaved_item_len_is_one_page_row(self):
|
||||||
|
pool = _pool(interleaved=True)
|
||||||
|
|
||||||
|
ptrs, lens, item_lens = pool.get_kv_scale_buf_infos()
|
||||||
|
|
||||||
|
self.assertEqual(len(ptrs), 2 * LAYERS)
|
||||||
|
self.assertNotIn(0, lens)
|
||||||
|
row = pool.k_scale_buffer[0][0].nbytes
|
||||||
|
self.assertEqual(item_lens[0], row)
|
||||||
|
self.assertEqual(lens[0] // item_lens[0], pool.k_scale_buffer[0].shape[0])
|
||||||
|
|
||||||
|
def test_flat_item_len_covers_a_whole_page(self):
|
||||||
|
"""A flat buffer is indexed per slot, so a page's worth is page_size rows."""
|
||||||
|
pool = _pool(interleaved=False)
|
||||||
|
|
||||||
|
_, lens, item_lens = pool.get_kv_scale_buf_infos()
|
||||||
|
|
||||||
|
row = pool.k_scale_buffer[0][0].nbytes
|
||||||
|
self.assertEqual(item_lens[0], row * PAGE_SIZE)
|
||||||
|
self.assertEqual(lens[0] // item_lens[0], 4)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user