[DSV4] Generalize attention metadata, sparse prefill, and KV pool over compress ratios (#39921)
This commit is contained in:
@@ -44,6 +44,7 @@ def _combine_topk_swa_indices_kernel(
|
|||||||
topk_indices_ptr,
|
topk_indices_ptr,
|
||||||
topk_indices_stride,
|
topk_indices_stride,
|
||||||
query_start_loc_ptr,
|
query_start_loc_ptr,
|
||||||
|
query_pos_ptr,
|
||||||
seq_lens_ptr,
|
seq_lens_ptr,
|
||||||
gather_lens_ptr,
|
gather_lens_ptr,
|
||||||
compressed_base_ptr,
|
compressed_base_ptr,
|
||||||
@@ -62,23 +63,19 @@ def _combine_topk_swa_indices_kernel(
|
|||||||
base = tl.load(query_start_loc_ptr)
|
base = tl.load(query_start_loc_ptr)
|
||||||
query_start = tl.load(query_start_loc_ptr + batch_idx) - base
|
query_start = tl.load(query_start_loc_ptr + batch_idx) - base
|
||||||
query_end = tl.load(query_start_loc_ptr + batch_idx + 1) - base
|
query_end = tl.load(query_start_loc_ptr + batch_idx + 1) - base
|
||||||
query_len = query_end - query_start
|
|
||||||
seq_len = tl.load(seq_lens_ptr + batch_idx)
|
seq_len = tl.load(seq_lens_ptr + batch_idx)
|
||||||
gather_len = tl.load(gather_lens_ptr + batch_idx)
|
gather_len = tl.load(gather_lens_ptr + batch_idx)
|
||||||
compressed_base = tl.load(compressed_base_ptr + batch_idx)
|
compressed_base = tl.load(compressed_base_ptr + batch_idx)
|
||||||
swa_base = tl.load(swa_base_ptr + batch_idx)
|
swa_base = tl.load(swa_base_ptr + batch_idx)
|
||||||
start_pos = seq_len - query_len
|
|
||||||
# SWA portion of the gathered buffer starts from position
|
# SWA portion of the gathered buffer starts from position
|
||||||
# (seq_len - gather_len), not 0. The +pos-gather_start formula maps a
|
# (seq_len - gather_len), not 0. The +pos-gather_start formula maps a
|
||||||
# query's window back into the workspace's SWA region.
|
# query's window back into the workspace's SWA region.
|
||||||
gather_start = seq_len - gather_len
|
gather_start = seq_len - gather_len
|
||||||
|
|
||||||
for token_idx in range(query_start + worker_id, query_end, num_workers):
|
for token_idx in range(query_start + worker_id, query_end, num_workers):
|
||||||
token_idx_in_query = token_idx - query_start
|
pos = tl.load(query_pos_ptr + token_idx)
|
||||||
pos = start_pos + token_idx_in_query
|
# -1 entries inside the top-k span stay -1 (attention skips them).
|
||||||
# Both the C4 indexer and the C128 metadata builder emit
|
# top_k=0 disables the compressed portion for SWA-only layers.
|
||||||
# min((pos+1)//compress_ratio, topk_tokens) valid entries. Caller
|
|
||||||
# passes top_k=0 for SWA-only layers to zero this out.
|
|
||||||
topk_len = tl.minimum((pos + 1) // COMPRESS_RATIO, top_k)
|
topk_len = tl.minimum((pos + 1) // COMPRESS_RATIO, top_k)
|
||||||
swa_len = tl.minimum(pos + 1, WINDOW_SIZE)
|
swa_len = tl.minimum(pos + 1, WINDOW_SIZE)
|
||||||
|
|
||||||
@@ -93,7 +90,7 @@ def _combine_topk_swa_indices_kernel(
|
|||||||
)
|
)
|
||||||
tl.store(
|
tl.store(
|
||||||
combined_indices_ptr + combined_row + offset,
|
combined_indices_ptr + combined_row + offset,
|
||||||
topk_vals + compressed_base,
|
tl.where(topk_vals >= 0, topk_vals + compressed_base, -1),
|
||||||
mask=mask,
|
mask=mask,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -61,10 +61,8 @@ from sglang.srt.disaggregation.utils import (
|
|||||||
build_kv_layer_ids,
|
build_kv_layer_ids,
|
||||||
build_staging_slot_metadata,
|
build_staging_slot_metadata,
|
||||||
get_dsa_tail_state_indices,
|
get_dsa_tail_state_indices,
|
||||||
get_dsv4_c128_state_indices,
|
|
||||||
get_kv_class,
|
get_kv_class,
|
||||||
get_qsa_pending_state_indices,
|
get_qsa_pending_state_indices,
|
||||||
is_dsv4_c128_online_enabled,
|
|
||||||
is_mla_backend,
|
is_mla_backend,
|
||||||
is_unadmitted_reject,
|
is_unadmitted_reject,
|
||||||
poll_and_all_reduce,
|
poll_and_all_reduce,
|
||||||
@@ -1498,23 +1496,18 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
|
|||||||
ring_rows = state_slot * ring_stride + (positions % ring_stride)
|
ring_rows = state_slot * ring_stride + (positions % ring_stride)
|
||||||
return ring_rows.astype(np.int32)
|
return ring_rows.astype(np.int32)
|
||||||
|
|
||||||
def _c128_state_payload():
|
def _request_state_payload():
|
||||||
online = is_dsv4_c128_online_enabled()
|
return self.token_to_kv_pool.request_state_transfer_indices(
|
||||||
ring_size = 1 if online else self.token_to_kv_pool.get_ring_size(128)
|
int(decode_req.req.kv.req_pool_idx), seq_len
|
||||||
return get_dsv4_c128_state_indices(
|
|
||||||
int(decode_req.req.kv.req_pool_idx),
|
|
||||||
seq_len,
|
|
||||||
online=online,
|
|
||||||
ring_size=ring_size,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
state_types = self.kv_manager.kv_args.state_types
|
state_types = self.kv_manager.kv_args.state_types
|
||||||
if StateType.DSV4_REQUEST_STATE in state_types:
|
if StateType.DSV4_REQUEST_STATE in state_types:
|
||||||
clear_c128_state = getattr(
|
clear_request_state = getattr(
|
||||||
self.token_to_kv_pool, "clear_c128_req_state", None
|
self.token_to_kv_pool, "clear_request_scoped_state", None
|
||||||
)
|
)
|
||||||
if clear_c128_state is not None:
|
if clear_request_state is not None:
|
||||||
clear_c128_state(int(decode_req.req.kv.req_pool_idx))
|
clear_request_state(int(decode_req.req.kv.req_pool_idx))
|
||||||
payloads = {
|
payloads = {
|
||||||
StateType.MAMBA: _mamba_payload,
|
StateType.MAMBA: _mamba_payload,
|
||||||
StateType.QSA_PENDING: _qsa_pending_payload,
|
StateType.QSA_PENDING: _qsa_pending_payload,
|
||||||
@@ -1524,7 +1517,7 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
|
|||||||
StateType.DSA_TAIL: _dsa_tail_payload,
|
StateType.DSA_TAIL: _dsa_tail_payload,
|
||||||
StateType.MINIMAX_INDEX_K: _full_kv_pages_payload,
|
StateType.MINIMAX_INDEX_K: _full_kv_pages_payload,
|
||||||
StateType.SWA_RING: _swa_ring_payload,
|
StateType.SWA_RING: _swa_ring_payload,
|
||||||
StateType.DSV4_REQUEST_STATE: _c128_state_payload,
|
StateType.DSV4_REQUEST_STATE: _request_state_payload,
|
||||||
StateType.BLOCK_SCALE: _full_kv_pages_payload,
|
StateType.BLOCK_SCALE: _full_kv_pages_payload,
|
||||||
StateType.BLOCK_SCALE_SWA: _swa_payload,
|
StateType.BLOCK_SCALE_SWA: _swa_payload,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,11 +52,9 @@ from sglang.srt.disaggregation.utils import (
|
|||||||
build_kv_layer_ids,
|
build_kv_layer_ids,
|
||||||
build_staging_slot_metadata,
|
build_staging_slot_metadata,
|
||||||
get_dsa_tail_state_indices,
|
get_dsa_tail_state_indices,
|
||||||
get_dsv4_c128_state_indices,
|
|
||||||
get_kv_class,
|
get_kv_class,
|
||||||
get_qsa_pending_state_indices,
|
get_qsa_pending_state_indices,
|
||||||
is_aborted,
|
is_aborted,
|
||||||
is_dsv4_c128_online_enabled,
|
|
||||||
is_mla_backend,
|
is_mla_backend,
|
||||||
is_unadmitted_reject,
|
is_unadmitted_reject,
|
||||||
poll_and_all_reduce_attn_cp_tp_group,
|
poll_and_all_reduce_attn_cp_tp_group,
|
||||||
@@ -257,7 +255,6 @@ class PrefillBootstrapQueue:
|
|||||||
hf_text_config=self.scheduler.model_config.hf_text_config,
|
hf_text_config=self.scheduler.model_config.hf_text_config,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
kv_args.mla_compression_ratios = None
|
|
||||||
kv_data_ptrs, kv_data_lens, kv_item_lens = (
|
kv_data_ptrs, kv_data_lens, kv_item_lens = (
|
||||||
self.token_to_kv_pool.get_contiguous_buf_infos()
|
self.token_to_kv_pool.get_contiguous_buf_infos()
|
||||||
)
|
)
|
||||||
@@ -316,13 +313,6 @@ class PrefillBootstrapQueue:
|
|||||||
req_to_token_pool=req_to_token_pool,
|
req_to_token_pool=req_to_token_pool,
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(self.token_to_kv_pool, DeepSeekV4TokenToKVPool):
|
|
||||||
# V4's KVCache is organized by compression-ratio
|
|
||||||
# buckets rather than by layer.
|
|
||||||
kv_args.mla_compression_ratios = list(
|
|
||||||
self.token_to_kv_pool.compression_ratios
|
|
||||||
)
|
|
||||||
|
|
||||||
kv_manager_class = get_kv_class(self.transfer_backend, KVClassType.MANAGER)
|
kv_manager_class = get_kv_class(self.transfer_backend, KVClassType.MANAGER)
|
||||||
kv_manager = kv_manager_class(
|
kv_manager = kv_manager_class(
|
||||||
kv_args,
|
kv_args,
|
||||||
@@ -1415,20 +1405,10 @@ class SchedulerDisaggregationPrefillMixin:
|
|||||||
ring_rows = state_slot * ring_stride + (positions % ring_stride)
|
ring_rows = state_slot * ring_stride + (positions % ring_stride)
|
||||||
return ring_rows.astype(np.int32)
|
return ring_rows.astype(np.int32)
|
||||||
|
|
||||||
def _c128_state_payload():
|
def _request_state_payload():
|
||||||
online = is_dsv4_c128_online_enabled()
|
kvcache = self.token_to_kv_pool_allocator.get_kvcache()
|
||||||
ring_size = (
|
return kvcache.request_state_transfer_indices(
|
||||||
1
|
int(req.kv.req_pool_idx), c128_seq_len
|
||||||
if online
|
|
||||||
else self.token_to_kv_pool_allocator.get_kvcache().get_ring_size(
|
|
||||||
128
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return get_dsv4_c128_state_indices(
|
|
||||||
int(req.kv.req_pool_idx),
|
|
||||||
c128_seq_len,
|
|
||||||
online=online,
|
|
||||||
ring_size=ring_size,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
state_types = (
|
state_types = (
|
||||||
@@ -1443,7 +1423,7 @@ class SchedulerDisaggregationPrefillMixin:
|
|||||||
StateType.DSA_TAIL: _dsa_tail_payload,
|
StateType.DSA_TAIL: _dsa_tail_payload,
|
||||||
StateType.MINIMAX_INDEX_K: _full_kv_pages_payload,
|
StateType.MINIMAX_INDEX_K: _full_kv_pages_payload,
|
||||||
StateType.SWA_RING: _swa_ring_payload,
|
StateType.SWA_RING: _swa_ring_payload,
|
||||||
StateType.DSV4_REQUEST_STATE: _c128_state_payload,
|
StateType.DSV4_REQUEST_STATE: _request_state_payload,
|
||||||
StateType.BLOCK_SCALE: _full_kv_pages_payload,
|
StateType.BLOCK_SCALE: _full_kv_pages_payload,
|
||||||
StateType.BLOCK_SCALE_SWA: _swa_payload,
|
StateType.BLOCK_SCALE_SWA: _swa_payload,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from sglang.srt.environ import envs
|
|||||||
from sglang.srt.runtime_context import (
|
from sglang.srt.runtime_context import (
|
||||||
get_disagg,
|
get_disagg,
|
||||||
)
|
)
|
||||||
from sglang.srt.utils import is_hip, is_npu
|
from sglang.srt.utils import is_npu
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sglang.srt.disaggregation.base.conn import KVArgs, StateType
|
from sglang.srt.disaggregation.base.conn import KVArgs, StateType
|
||||||
@@ -46,7 +46,6 @@ if is_npu():
|
|||||||
# Constants & Enums
|
# Constants & Enums
|
||||||
#########################
|
#########################
|
||||||
FAKE_BOOTSTRAP_HOST = "2.2.2.2"
|
FAKE_BOOTSTRAP_HOST = "2.2.2.2"
|
||||||
_IS_HIP = is_hip()
|
|
||||||
|
|
||||||
|
|
||||||
def poll_and_all_reduce_pp(
|
def poll_and_all_reduce_pp(
|
||||||
@@ -78,50 +77,6 @@ def get_dsa_seed_metadata_dim(hf_config) -> int:
|
|||||||
return get_dsa_mtp_topk_width(hf_config)
|
return get_dsa_mtp_topk_width(hf_config)
|
||||||
|
|
||||||
|
|
||||||
def is_dsv4_c128_online_enabled() -> bool:
|
|
||||||
"""Return whether DSV4 C128 uses request-scoped online state."""
|
|
||||||
return not _IS_HIP and envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get()
|
|
||||||
|
|
||||||
|
|
||||||
def get_dsv4_c4_state_indices(
|
|
||||||
req_pool_idx: int,
|
|
||||||
seq_len: int,
|
|
||||||
*,
|
|
||||||
ring_size: int,
|
|
||||||
) -> np.ndarray:
|
|
||||||
# Prefill and decode can have different ring sizes (8 or 16 with EAGLE/MTP);
|
|
||||||
# pair the overlap compressor's live rows by logical token position.
|
|
||||||
if ring_size < 8 or ring_size % 4 != 0:
|
|
||||||
raise ValueError(
|
|
||||||
f"C4 ring_size must be a multiple of 4 and at least 8, got {ring_size}"
|
|
||||||
)
|
|
||||||
|
|
||||||
seq_len = max(0, int(seq_len))
|
|
||||||
state_len = seq_len % 4 + 4
|
|
||||||
positions = np.arange(max(0, seq_len - state_len), seq_len, dtype=np.int64)
|
|
||||||
rows = int(req_pool_idx) * int(ring_size) + positions % int(ring_size)
|
|
||||||
return rows.astype(np.int32)
|
|
||||||
|
|
||||||
|
|
||||||
def get_dsv4_c128_state_indices(
|
|
||||||
req_pool_idx: int,
|
|
||||||
seq_len: int,
|
|
||||||
*,
|
|
||||||
online: bool,
|
|
||||||
ring_size: int,
|
|
||||||
) -> np.ndarray:
|
|
||||||
"""Return the PD transfer row/page indices for DSV4 C128 state."""
|
|
||||||
if seq_len == 0 or seq_len % 128 == 0:
|
|
||||||
return np.empty((0,), dtype=np.int32)
|
|
||||||
if online:
|
|
||||||
return np.array([int(req_pool_idx)], dtype=np.int32)
|
|
||||||
|
|
||||||
assert ring_size % 128 == 0, f"C128 ring_size must be 128-aligned, got {ring_size}"
|
|
||||||
pages_per_req = ring_size // 128
|
|
||||||
page = int(req_pool_idx) * pages_per_req + ((seq_len - 1) % ring_size) // 128
|
|
||||||
return np.array([page], dtype=np.int32)
|
|
||||||
|
|
||||||
|
|
||||||
def get_qsa_pending_state_indices(req: Req) -> np.ndarray:
|
def get_qsa_pending_state_indices(req: Req) -> np.ndarray:
|
||||||
"""Return the request-pool row that owns a QSA pending-state ring."""
|
"""Return the request-pool row that owns a QSA pending-state ring."""
|
||||||
req_pool_idx = req.kv.req_pool_idx
|
req_pool_idx = req.kv.req_pool_idx
|
||||||
@@ -1380,6 +1335,12 @@ def setup_state_kv_args(
|
|||||||
kv_args.state_layer_ids = []
|
kv_args.state_layer_ids = []
|
||||||
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 = []
|
||||||
|
# V4's KVCache is organized by compression-ratio buckets rather than by layer.
|
||||||
|
kv_args.mla_compression_ratios = (
|
||||||
|
list(token_to_kv_pool.compression_ratios)
|
||||||
|
if isinstance(token_to_kv_pool, DeepSeekV4TokenToKVPool)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
def append_dsa_tail(pool) -> None:
|
def append_dsa_tail(pool) -> None:
|
||||||
if not pool.kpool_use_compress:
|
if not pool.kpool_use_compress:
|
||||||
|
|||||||
@@ -544,7 +544,7 @@ class DSV4NPUTokenToKVPoolAllocator(SWATokenToKVPoolAllocator):
|
|||||||
row = req_to_token_pool.req_to_c128_sidecar[int(req_pool_idx)]
|
row = req_to_token_pool.req_to_c128_sidecar[int(req_pool_idx)]
|
||||||
self.release_c128_pages(row[row > 0])
|
self.release_c128_pages(row[row > 0])
|
||||||
row.zero_()
|
row.zero_()
|
||||||
self.get_kvcache().clear_c128_req_state(int(req_pool_idx))
|
self.get_kvcache().clear_request_scoped_state(int(req_pool_idx))
|
||||||
|
|
||||||
def available_size(self):
|
def available_size(self):
|
||||||
return min(
|
return min(
|
||||||
|
|||||||
@@ -88,8 +88,10 @@ def dsv4_state_payloads(
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from sglang.srt.disaggregation.ascend.conn import AscendStateType
|
from sglang.srt.disaggregation.ascend.conn import AscendStateType
|
||||||
from sglang.srt.disaggregation.utils import get_dsv4_c4_state_indices
|
|
||||||
from sglang.srt.hardware_backend.npu.utils import is_npu_arch35
|
from sglang.srt.hardware_backend.npu.utils import is_npu_arch35
|
||||||
|
from sglang.srt.mem_cache.deepseek_v4_compress_state import (
|
||||||
|
c4_state_transfer_indices,
|
||||||
|
)
|
||||||
|
|
||||||
seq_len = max(0, int(seq_len))
|
seq_len = max(0, int(seq_len))
|
||||||
prefix_len = max(0, min(int(prefix_len), seq_len))
|
prefix_len = max(0, min(int(prefix_len), seq_len))
|
||||||
@@ -113,7 +115,7 @@ def dsv4_state_payloads(
|
|||||||
if is_npu_arch35():
|
if is_npu_arch35():
|
||||||
|
|
||||||
def c4_state_indices():
|
def c4_state_indices():
|
||||||
return get_dsv4_c4_state_indices(
|
return c4_state_transfer_indices(
|
||||||
req_pool_idx,
|
req_pool_idx,
|
||||||
seq_len,
|
seq_len,
|
||||||
ring_size=req_to_token_pool.get_dsv4_c4_state_ring_size(),
|
ring_size=req_to_token_pool.get_dsv4_c4_state_ring_size(),
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ class NPUCompressStatePool(CompressStatePool):
|
|||||||
enable_memory_saver: bool,
|
enable_memory_saver: bool,
|
||||||
ratio: int,
|
ratio: int,
|
||||||
ring_size: int,
|
ring_size: int,
|
||||||
|
request_scoped: bool,
|
||||||
swa_page_size: int,
|
swa_page_size: int,
|
||||||
):
|
):
|
||||||
assert ratio in (
|
assert ratio in (
|
||||||
@@ -139,6 +140,7 @@ class NPUCompressStatePool(CompressStatePool):
|
|||||||
enable_memory_saver=enable_memory_saver,
|
enable_memory_saver=enable_memory_saver,
|
||||||
ratio=ratio,
|
ratio=ratio,
|
||||||
online=False,
|
online=False,
|
||||||
|
request_scoped=request_scoped,
|
||||||
swa_page_size=swa_page_size,
|
swa_page_size=swa_page_size,
|
||||||
state_cache_page_size=ring_size,
|
state_cache_page_size=ring_size,
|
||||||
)
|
)
|
||||||
@@ -352,6 +354,7 @@ class DSV4NPUTokenToKVPool(DeepSeekV4TokenToKVPool):
|
|||||||
device=self.device,
|
device=self.device,
|
||||||
enable_memory_saver=enable_memory_saver,
|
enable_memory_saver=enable_memory_saver,
|
||||||
ratio=ratio,
|
ratio=ratio,
|
||||||
|
request_scoped=ratio == 128,
|
||||||
swa_page_size=self.swa_page_size,
|
swa_page_size=self.swa_page_size,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from sglang.kernels.ops.attention.dsv4.dequant_k_cache import (
|
|||||||
gather_dequant_requant_fp8_paged,
|
gather_dequant_requant_fp8_paged,
|
||||||
q8kv8_padded_num_heads,
|
q8kv8_padded_num_heads,
|
||||||
)
|
)
|
||||||
|
from sglang.kernels.ops.attention.dsv4.kv_layout import KVLayout
|
||||||
from sglang.kernels.ops.attention.dsv4.metadata_kernel import (
|
from sglang.kernels.ops.attention.dsv4.metadata_kernel import (
|
||||||
init_compression_metadata as _init_compression_metadata_triton,
|
init_compression_metadata as _init_compression_metadata_triton,
|
||||||
)
|
)
|
||||||
@@ -98,7 +99,7 @@ _is_xpu = is_xpu()
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
SWA_WINDOW = 128
|
SWA_WINDOW = 128
|
||||||
C4_TOPK = 512
|
DEFAULT_INDEX_TOPK = 512
|
||||||
PAGE_INDEX_ALIGNED_SIZE = 64
|
PAGE_INDEX_ALIGNED_SIZE = 64
|
||||||
|
|
||||||
|
|
||||||
@@ -178,7 +179,10 @@ class DSV4AttnMetadata:
|
|||||||
swa_page_indices: torch.Tensor
|
swa_page_indices: torch.Tensor
|
||||||
swa_topk_lengths: torch.Tensor
|
swa_topk_lengths: torch.Tensor
|
||||||
|
|
||||||
c4_sparse_topk: int
|
index_topk: int
|
||||||
|
# Sorted compress ratios present in this stage; absent ratios keep no
|
||||||
|
# buffers or schedules.
|
||||||
|
present_ratios: Tuple[int, ...]
|
||||||
# Shared by all layer stores; locations are in SWA space.
|
# Shared by all layer stores; locations are in SWA space.
|
||||||
swa_out_cache_loc: Optional[torch.Tensor] = None
|
swa_out_cache_loc: Optional[torch.Tensor] = None
|
||||||
c4_out_loc: Optional[torch.Tensor] = None
|
c4_out_loc: Optional[torch.Tensor] = None
|
||||||
@@ -212,6 +216,14 @@ class DSV4AttnMetadata:
|
|||||||
def positions(self) -> torch.Tensor:
|
def positions(self) -> torch.Tensor:
|
||||||
return self.positions_casual
|
return self.positions_casual
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_c4(self) -> bool:
|
||||||
|
return 4 in self.present_ratios
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_c128(self) -> bool:
|
||||||
|
return 128 in self.present_ratios
|
||||||
|
|
||||||
def get_flashmla_metadata(self, compress_ratio: Literal[0, 4, 128]):
|
def get_flashmla_metadata(self, compress_ratio: Literal[0, 4, 128]):
|
||||||
if compress_ratio == 0:
|
if compress_ratio == 0:
|
||||||
return self.c0_flashmla_metadata
|
return self.c0_flashmla_metadata
|
||||||
@@ -222,14 +234,63 @@ class DSV4AttnMetadata:
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"invalid {compress_ratio=}")
|
raise ValueError(f"invalid {compress_ratio=}")
|
||||||
|
|
||||||
|
# Per-ratio extra-cache metadata is stored as flat fields; these accessors
|
||||||
|
# unify the read and write paths over the ratio.
|
||||||
|
|
||||||
|
def sparse_page_indices(self, compress_ratio: int) -> torch.Tensor:
|
||||||
|
"""Slots into the ratio's extra cache, -1 padded: the indexer's top-k for
|
||||||
|
c4, every compressed block up to the position for c128."""
|
||||||
|
if compress_ratio == 4:
|
||||||
|
return self.c4_sparse_page_indices
|
||||||
|
if compress_ratio == 128:
|
||||||
|
return self.c128_page_indices
|
||||||
|
raise ValueError(f"invalid {compress_ratio=}")
|
||||||
|
|
||||||
|
def sparse_topk_lengths(self, compress_ratio: int) -> torch.Tensor:
|
||||||
|
if compress_ratio == 4:
|
||||||
|
return self.c4_sparse_topk_lengths
|
||||||
|
if compress_ratio == 128:
|
||||||
|
return self.c128_topk_lengths_clamp1
|
||||||
|
raise ValueError(f"invalid {compress_ratio=}")
|
||||||
|
|
||||||
|
def sparse_raw_indices(self, compress_ratio: int) -> Optional[torch.Tensor]:
|
||||||
|
"""The top-k as request-local compressed positions, for the sparse
|
||||||
|
prefill workspace; allocated for prefill metadata only. Only the indexer
|
||||||
|
ratios have one (c128 remaps its page indices instead)."""
|
||||||
|
if compress_ratio == 4:
|
||||||
|
return self.c4_sparse_raw_indices
|
||||||
|
raise ValueError(f"invalid {compress_ratio=}")
|
||||||
|
|
||||||
|
def set_sparse_topk(
|
||||||
|
self,
|
||||||
|
compress_ratio: int,
|
||||||
|
*,
|
||||||
|
page_indices: torch.Tensor,
|
||||||
|
topk_lengths: torch.Tensor,
|
||||||
|
raw_indices: Optional[torch.Tensor] = None,
|
||||||
|
) -> None:
|
||||||
|
"""Writer counterpart of the accessors above."""
|
||||||
|
if compress_ratio == 4:
|
||||||
|
self.c4_sparse_page_indices = page_indices
|
||||||
|
self.c4_sparse_topk_lengths = topk_lengths
|
||||||
|
if raw_indices is not None:
|
||||||
|
self.c4_sparse_raw_indices = raw_indices
|
||||||
|
elif compress_ratio == 128:
|
||||||
|
assert raw_indices is None, "c128 has no raw top-k"
|
||||||
|
self.c128_page_indices = page_indices
|
||||||
|
self.c128_topk_lengths_clamp1 = topk_lengths
|
||||||
|
else:
|
||||||
|
raise ValueError(f"invalid {compress_ratio=}")
|
||||||
|
|
||||||
def copy_(self, other: DSV4AttnMetadata) -> None:
|
def copy_(self, other: DSV4AttnMetadata) -> None:
|
||||||
copy_metadata(
|
copy_metadata(
|
||||||
src=other,
|
src=other,
|
||||||
dst=self,
|
dst=self,
|
||||||
check_eq_fields=[
|
check_eq_fields=[
|
||||||
"c4_sparse_topk",
|
"index_topk",
|
||||||
"page_size",
|
"page_size",
|
||||||
"cuda_int32_kwargs",
|
"cuda_int32_kwargs",
|
||||||
|
"present_ratios",
|
||||||
],
|
],
|
||||||
copy_fields=[
|
copy_fields=[
|
||||||
"raw_out_loc",
|
"raw_out_loc",
|
||||||
@@ -269,9 +330,10 @@ class DSV4AttnMetadata:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def refresh_for_breakable_cuda_graph_replay_(self, other: DSV4AttnMetadata) -> None:
|
def refresh_for_breakable_cuda_graph_replay_(self, other: DSV4AttnMetadata) -> None:
|
||||||
assert self.c4_sparse_topk == other.c4_sparse_topk
|
assert self.index_topk == other.index_topk
|
||||||
assert self.page_size == other.page_size
|
assert self.page_size == other.page_size
|
||||||
assert self.cuda_int32_kwargs == other.cuda_int32_kwargs
|
assert self.cuda_int32_kwargs == other.cuda_int32_kwargs
|
||||||
|
assert self.present_ratios == other.present_ratios
|
||||||
|
|
||||||
tensor_copy_fields = [
|
tensor_copy_fields = [
|
||||||
"raw_out_loc",
|
"raw_out_loc",
|
||||||
@@ -331,26 +393,36 @@ class DSV4AttnMetadata:
|
|||||||
f"{self.raw_out_loc.shape=}, {num_tokens=}"
|
f"{self.raw_out_loc.shape=}, {num_tokens=}"
|
||||||
)
|
)
|
||||||
|
|
||||||
(
|
if self.has_c4 or self.has_c128:
|
||||||
self.c4_out_loc,
|
# One kernel produces both ratios; compute_page_indices=False only
|
||||||
_,
|
# drops the [T, max_c128_len] table, which is c128-only.
|
||||||
self.c4_topk_lengths_raw,
|
(
|
||||||
self.c4_topk_lengths_clamp1,
|
c4_out_loc,
|
||||||
self.c128_out_loc,
|
_,
|
||||||
_,
|
c4_topk_lengths_raw,
|
||||||
_,
|
c4_topk_lengths_clamp1,
|
||||||
self.c128_topk_lengths_clamp1,
|
c128_out_loc,
|
||||||
self.c128_page_indices,
|
_,
|
||||||
) = _init_compression_metadata_triton(
|
_,
|
||||||
self.seq_lens_casual,
|
c128_topk_lengths_clamp1,
|
||||||
self.positions_casual,
|
c128_page_indices,
|
||||||
self.raw_out_loc,
|
) = _init_compression_metadata_triton(
|
||||||
self.page_table,
|
self.seq_lens_casual,
|
||||||
self.page_size,
|
self.positions_casual,
|
||||||
compute_page_indices=True,
|
self.raw_out_loc,
|
||||||
)
|
self.page_table,
|
||||||
|
self.page_size,
|
||||||
|
compute_page_indices=self.has_c128,
|
||||||
|
)
|
||||||
|
if self.has_c4:
|
||||||
|
self.c4_out_loc = c4_out_loc
|
||||||
|
self.c4_topk_lengths_raw = c4_topk_lengths_raw
|
||||||
|
self.c4_topk_lengths_clamp1 = c4_topk_lengths_clamp1
|
||||||
|
if self.has_c128:
|
||||||
|
self.c128_out_loc = c128_out_loc
|
||||||
|
self.c128_topk_lengths_clamp1 = c128_topk_lengths_clamp1
|
||||||
|
self.c128_page_indices = _pad_last_dim(c128_page_indices)
|
||||||
|
|
||||||
self.c128_page_indices = _pad_last_dim(self.c128_page_indices)
|
|
||||||
self.swa_page_indices = _pad_last_dim(self.swa_page_indices)
|
self.swa_page_indices = _pad_last_dim(self.swa_page_indices)
|
||||||
|
|
||||||
# Cache-write locations stay in global logical order and are intentionally
|
# Cache-write locations stay in global logical order and are intentionally
|
||||||
@@ -361,6 +433,9 @@ class DSV4AttnMetadata:
|
|||||||
"swa_page_indices",
|
"swa_page_indices",
|
||||||
"swa_topk_lengths",
|
"swa_topk_lengths",
|
||||||
"page_table",
|
"page_table",
|
||||||
|
]
|
||||||
|
# Same treatment, None for stages without that compress ratio.
|
||||||
|
_CP_REINDEX_OPTIONAL_FIELDS = [
|
||||||
"c4_topk_lengths_raw",
|
"c4_topk_lengths_raw",
|
||||||
"c4_topk_lengths_clamp1",
|
"c4_topk_lengths_clamp1",
|
||||||
"c128_page_indices",
|
"c128_page_indices",
|
||||||
@@ -385,15 +460,18 @@ class DSV4AttnMetadata:
|
|||||||
expected_local_len = pre_global_len // cp_size
|
expected_local_len = pre_global_len // cp_size
|
||||||
if num_tokens is None:
|
if num_tokens is None:
|
||||||
num_tokens = pre_global_len
|
num_tokens = pre_global_len
|
||||||
for field_name in self._CP_REINDEX_FIELDS:
|
for field_name in self._CP_REINDEX_FIELDS + self._CP_REINDEX_OPTIONAL_FIELDS:
|
||||||
val = getattr(self, field_name, None)
|
val = getattr(self, field_name, None)
|
||||||
|
if val is None:
|
||||||
|
assert field_name in self._CP_REINDEX_OPTIONAL_FIELDS, (
|
||||||
|
f"CP reindex: {field_name} is None"
|
||||||
|
)
|
||||||
|
continue
|
||||||
assert isinstance(val, torch.Tensor), (
|
assert isinstance(val, torch.Tensor), (
|
||||||
f"CP reindex: {field_name} is {type(val)}, expected Tensor"
|
f"CP reindex: {field_name} is {type(val)}, expected Tensor"
|
||||||
)
|
)
|
||||||
setattr(self, field_name, val[idx].contiguous())
|
val = val[idx].contiguous()
|
||||||
|
setattr(self, field_name, val)
|
||||||
for field_name in self._CP_REINDEX_FIELDS:
|
|
||||||
val = getattr(self, field_name)
|
|
||||||
assert val.shape[0] == expected_local_len, (
|
assert val.shape[0] == expected_local_len, (
|
||||||
f"apply_cp_reindex post-condition: {field_name}.shape[0]={val.shape[0]} "
|
f"apply_cp_reindex post-condition: {field_name}.shape[0]={val.shape[0]} "
|
||||||
f"!= expected_local_len={expected_local_len} (cp_size={cp_size})"
|
f"!= expected_local_len={expected_local_len} (cp_size={cp_size})"
|
||||||
@@ -408,28 +486,37 @@ class DSV4AttnMetadata:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def init_flashmla_related(self, is_prefill: bool = False):
|
def init_flashmla_related(self, is_prefill: bool = False):
|
||||||
# c4_sparse_topk is set from model_config.index_topk per-model
|
# index_topk is set from model_config.index_topk per-model
|
||||||
# (small model: 512, large model: 1024).
|
# (small model: 512, large model: 1024).
|
||||||
assert self.c4_sparse_topk in (512, 1024), (
|
assert self.index_topk in (512, 1024), (
|
||||||
f"unexpected c4_sparse_topk={self.c4_sparse_topk}; "
|
f"unexpected index_topk={self.index_topk}; "
|
||||||
"supported: 512 (small) or 1024 (large)"
|
"supported: 512 (small) or 1024 (large)"
|
||||||
)
|
)
|
||||||
assert self.c4_topk_lengths_clamp1 is not None
|
if self.has_c4:
|
||||||
self.c4_sparse_topk_lengths = torch.clamp(
|
assert self.c4_topk_lengths_clamp1 is not None
|
||||||
self.c4_topk_lengths_clamp1, max=self.c4_sparse_topk
|
self.c4_sparse_topk_lengths = torch.clamp(
|
||||||
)
|
self.c4_topk_lengths_clamp1, max=self.index_topk
|
||||||
self.c4_sparse_page_indices = torch.full(
|
)
|
||||||
(self.c4_topk_lengths_clamp1.size(0), self.c4_sparse_topk),
|
self.c4_sparse_page_indices = torch.full(
|
||||||
-1,
|
(self.c4_topk_lengths_clamp1.size(0), self.index_topk),
|
||||||
dtype=torch.int32,
|
-1,
|
||||||
device=self.c4_topk_lengths_clamp1.device,
|
dtype=torch.int32,
|
||||||
)
|
device=self.c4_topk_lengths_clamp1.device,
|
||||||
self.c4_sparse_page_indices = _pad_last_dim(self.c4_sparse_page_indices)
|
)
|
||||||
if is_prefill:
|
self.c4_sparse_page_indices = _pad_last_dim(self.c4_sparse_page_indices)
|
||||||
self.c4_sparse_raw_indices = torch.empty_like(self.c4_sparse_page_indices)
|
if is_prefill:
|
||||||
|
self.c4_sparse_raw_indices = torch.empty_like(
|
||||||
|
self.c4_sparse_page_indices
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.c4_sparse_topk_lengths = None
|
||||||
|
self.c4_sparse_page_indices = None
|
||||||
|
self.c4_sparse_raw_indices = None
|
||||||
self.c0_flashmla_metadata = _create_flashmla_metadata()
|
self.c0_flashmla_metadata = _create_flashmla_metadata()
|
||||||
self.c4_flashmla_metadata = _create_flashmla_metadata()
|
self.c4_flashmla_metadata = _create_flashmla_metadata() if self.has_c4 else None
|
||||||
self.c128_flashmla_metadata = _create_flashmla_metadata()
|
self.c128_flashmla_metadata = (
|
||||||
|
_create_flashmla_metadata() if self.has_c128 else None
|
||||||
|
)
|
||||||
|
|
||||||
def init_trtllm_sparse_buffers(self) -> None:
|
def init_trtllm_sparse_buffers(self) -> None:
|
||||||
"""Build decode tables with 128 SWA columns followed by compressed KV.
|
"""Build decode tables with 128 SWA columns followed by compressed KV.
|
||||||
@@ -638,11 +725,15 @@ class DeepseekV4AttnBackend(
|
|||||||
self.token_to_kv_pool: DeepSeekV4TokenToKVPool = model_runner.token_to_kv_pool
|
self.token_to_kv_pool: DeepSeekV4TokenToKVPool = model_runner.token_to_kv_pool
|
||||||
self.hisparse_coordinator = model_runner.hisparse_coordinator
|
self.hisparse_coordinator = model_runner.hisparse_coordinator
|
||||||
self.req_to_token = model_runner.req_to_token_pool.req_to_token
|
self.req_to_token = model_runner.req_to_token_pool.req_to_token
|
||||||
|
# Nothing is built for a compress ratio outside the pool's set.
|
||||||
|
self.present_ratios: Tuple[int, ...] = self.token_to_kv_pool.present_ratios
|
||||||
|
self.has_c4: bool = 4 in self.present_ratios
|
||||||
|
self.has_c128: bool = 128 in self.present_ratios
|
||||||
self.MAX_SEQ_LEN_FOR_CAPTURE = self.req_to_token.shape[1]
|
self.MAX_SEQ_LEN_FOR_CAPTURE = self.req_to_token.shape[1]
|
||||||
|
|
||||||
assert isinstance(self.token_to_kv_pool, DeepSeekV4TokenToKVPool)
|
assert isinstance(self.token_to_kv_pool, DeepSeekV4TokenToKVPool)
|
||||||
self.c4_topk = getattr(
|
self.index_topk = getattr(
|
||||||
model_runner.model_config.hf_text_config, "index_topk", C4_TOPK
|
model_runner.model_config.hf_text_config, "index_topk", DEFAULT_INDEX_TOPK
|
||||||
)
|
)
|
||||||
|
|
||||||
kernel = get_exec().kernel
|
kernel = get_exec().kernel
|
||||||
@@ -750,7 +841,7 @@ class DeepseekV4AttnBackend(
|
|||||||
use_prefill_cuda_graph: bool,
|
use_prefill_cuda_graph: bool,
|
||||||
online_c128_state_slot_offset: int,
|
online_c128_state_slot_offset: int,
|
||||||
) -> Optional[FusedCompressMetadata]:
|
) -> Optional[FusedCompressMetadata]:
|
||||||
if not self.online_c128_mtp.enabled():
|
if not self.has_c128 or not self.online_c128_mtp.enabled():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
assert seq_lens_cpu is not None
|
assert seq_lens_cpu is not None
|
||||||
@@ -861,7 +952,7 @@ class DeepseekV4AttnBackend(
|
|||||||
core_attn_metadata,
|
core_attn_metadata,
|
||||||
use_prefill_cuda_graph=use_prefill_cuda_graph,
|
use_prefill_cuda_graph=use_prefill_cuda_graph,
|
||||||
)
|
)
|
||||||
if need_compress
|
if need_compress and self.has_c4
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
if not need_compress:
|
if not need_compress:
|
||||||
@@ -904,13 +995,13 @@ class DeepseekV4AttnBackend(
|
|||||||
online_state_slot_offset=online_c128_state_slot_offset,
|
online_state_slot_offset=online_c128_state_slot_offset,
|
||||||
)
|
)
|
||||||
|
|
||||||
c4_compress_metadata = create(compress_ratio=4)
|
|
||||||
c128_compress_metadata = create(compress_ratio=128)
|
|
||||||
return DSV4Metadata(
|
return DSV4Metadata(
|
||||||
core_attn_metadata,
|
core_attn_metadata,
|
||||||
indexer_metadata,
|
indexer_metadata,
|
||||||
c4_compress_metadata=c4_compress_metadata,
|
c4_compress_metadata=create(compress_ratio=4) if self.has_c4 else None,
|
||||||
c128_compress_metadata=c128_compress_metadata,
|
c128_compress_metadata=(
|
||||||
|
create(compress_ratio=128) if self.has_c128 else None
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def init_forward_metadata_target_verify(
|
def init_forward_metadata_target_verify(
|
||||||
@@ -1045,7 +1136,11 @@ class DeepseekV4AttnBackend(
|
|||||||
out_loc=out_cache_loc,
|
out_loc=out_cache_loc,
|
||||||
need_compress=True,
|
need_compress=True,
|
||||||
)
|
)
|
||||||
indexer_metadata = self.init_forward_metadata_indexer(core_attn_metadata)
|
indexer_metadata = (
|
||||||
|
self.init_forward_metadata_indexer(core_attn_metadata)
|
||||||
|
if self.has_c4
|
||||||
|
else None
|
||||||
|
)
|
||||||
create = functools.partial(
|
create = functools.partial(
|
||||||
create_paged_compressor_data,
|
create_paged_compressor_data,
|
||||||
is_prefill=True,
|
is_prefill=True,
|
||||||
@@ -1061,12 +1156,12 @@ class DeepseekV4AttnBackend(
|
|||||||
online_state_slot_offset=online_c128_state_slot_offset,
|
online_state_slot_offset=online_c128_state_slot_offset,
|
||||||
)
|
)
|
||||||
c128_compress_metadata = raw_metadata.c128_compress_metadata
|
c128_compress_metadata = raw_metadata.c128_compress_metadata
|
||||||
if c128_compress_metadata is None:
|
if c128_compress_metadata is None and self.has_c128:
|
||||||
c128_compress_metadata = create(compress_ratio=128)
|
c128_compress_metadata = create(compress_ratio=128)
|
||||||
return DSV4Metadata(
|
return DSV4Metadata(
|
||||||
core_attn_metadata,
|
core_attn_metadata,
|
||||||
indexer_metadata,
|
indexer_metadata,
|
||||||
c4_compress_metadata=create(compress_ratio=4),
|
c4_compress_metadata=create(compress_ratio=4) if self.has_c4 else None,
|
||||||
c128_compress_metadata=c128_compress_metadata,
|
c128_compress_metadata=c128_compress_metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1086,7 +1181,11 @@ class DeepseekV4AttnBackend(
|
|||||||
out_loc=out_cache_loc,
|
out_loc=out_cache_loc,
|
||||||
need_compress=True,
|
need_compress=True,
|
||||||
)
|
)
|
||||||
indexer_metadata = self.init_forward_metadata_indexer(core_attn_metadata)
|
indexer_metadata = (
|
||||||
|
self.init_forward_metadata_indexer(core_attn_metadata)
|
||||||
|
if self.has_c4
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
create = functools.partial(
|
create = functools.partial(
|
||||||
create_paged_compressor_data,
|
create_paged_compressor_data,
|
||||||
@@ -1100,8 +1199,10 @@ class DeepseekV4AttnBackend(
|
|||||||
return DSV4Metadata(
|
return DSV4Metadata(
|
||||||
core_attn_metadata,
|
core_attn_metadata,
|
||||||
indexer_metadata,
|
indexer_metadata,
|
||||||
c4_compress_metadata=create(compress_ratio=4),
|
c4_compress_metadata=create(compress_ratio=4) if self.has_c4 else None,
|
||||||
c128_compress_metadata=create(compress_ratio=128),
|
c128_compress_metadata=(
|
||||||
|
create(compress_ratio=128) if self.has_c128 else None
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def init_forward_metadata_draft_extend(
|
def init_forward_metadata_draft_extend(
|
||||||
@@ -1464,17 +1565,22 @@ class DeepseekV4AttnBackend(
|
|||||||
)
|
)
|
||||||
if use_sparse_prefill:
|
if use_sparse_prefill:
|
||||||
metadata.sparse_prefill_cache = self._build_sparse_prefill_chunk_cache(
|
metadata.sparse_prefill_cache = self._build_sparse_prefill_chunk_cache(
|
||||||
forward_batch, num_qo_tokens=num_qo_tokens
|
forward_batch, metadata.core_attn_metadata, num_qo_tokens=num_qo_tokens
|
||||||
)
|
)
|
||||||
# Marked for dense prefill too: that path reads only core_attn_metadata,
|
# Marked for dense prefill too: that path reads only core_attn_metadata,
|
||||||
# which init_forward_metadata already snapshotted.
|
# which init_forward_metadata already snapshotted.
|
||||||
metadata.prefill_shared_reads_snapshotted = True
|
metadata.prefill_shared_reads_snapshotted = True
|
||||||
|
|
||||||
def _build_sparse_prefill_chunk_cache(
|
def _build_sparse_prefill_chunk_cache(
|
||||||
self, forward_batch: ForwardBatch, *, num_qo_tokens: int
|
self,
|
||||||
|
forward_batch: ForwardBatch,
|
||||||
|
core_attn_metadata: DSV4AttnMetadata,
|
||||||
|
*,
|
||||||
|
num_qo_tokens: int,
|
||||||
) -> SparsePrefillChunkCache:
|
) -> SparsePrefillChunkCache:
|
||||||
seq_lens_cpu = forward_batch.seq_lens_cpu
|
seq_lens_cpu = forward_batch.seq_lens_cpu
|
||||||
assert seq_lens_cpu is not None
|
assert seq_lens_cpu is not None
|
||||||
|
extend_seq_lens = forward_batch.extend_seq_lens
|
||||||
extend_seq_lens_cpu = forward_batch.extend_seq_lens_cpu
|
extend_seq_lens_cpu = forward_batch.extend_seq_lens_cpu
|
||||||
assert extend_seq_lens_cpu is not None
|
assert extend_seq_lens_cpu is not None
|
||||||
seq_lens_cpu_list = seq_lens_cpu.tolist()
|
seq_lens_cpu_list = seq_lens_cpu.tolist()
|
||||||
@@ -1484,9 +1590,13 @@ class DeepseekV4AttnBackend(
|
|||||||
seq_lens_cpu_list, extend_seq_lens_cpu, strict=True
|
seq_lens_cpu_list, extend_seq_lens_cpu, strict=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
# The rows this forward runs are the extend, one per causal position.
|
||||||
|
query_pos = core_attn_metadata.seq_lens_casual[:num_qo_tokens] - 1
|
||||||
return SparsePrefillChunkCache.build(
|
return SparsePrefillChunkCache.build(
|
||||||
seq_lens=forward_batch.seq_lens.to(torch.int32),
|
seq_lens=forward_batch.seq_lens.to(torch.int32),
|
||||||
extend_seq_lens=forward_batch.extend_seq_lens.to(torch.int32),
|
extend_seq_lens=extend_seq_lens.to(torch.int32),
|
||||||
|
query_lens=extend_seq_lens.to(torch.int32),
|
||||||
|
query_pos=query_pos,
|
||||||
req_pool_indices=forward_batch.req_pool_indices.to(torch.int32),
|
req_pool_indices=forward_batch.req_pool_indices.to(torch.int32),
|
||||||
req_to_token=self.req_to_token,
|
req_to_token=self.req_to_token,
|
||||||
full_to_swa=self.token_to_kv_pool.full_to_swa_index_mapping,
|
full_to_swa=self.token_to_kv_pool.full_to_swa_index_mapping,
|
||||||
@@ -1713,8 +1823,10 @@ class DeepseekV4AttnBackend(
|
|||||||
):
|
):
|
||||||
core = metadata.core_attn_metadata
|
core = metadata.core_attn_metadata
|
||||||
core.c0_flashmla_metadata = _create_flashmla_metadata()
|
core.c0_flashmla_metadata = _create_flashmla_metadata()
|
||||||
core.c4_flashmla_metadata = _create_flashmla_metadata()
|
if core.has_c4:
|
||||||
core.c128_flashmla_metadata = _create_flashmla_metadata()
|
core.c4_flashmla_metadata = _create_flashmla_metadata()
|
||||||
|
if core.has_c128:
|
||||||
|
core.c128_flashmla_metadata = _create_flashmla_metadata()
|
||||||
|
|
||||||
# PREP_IN_CUDA_GRAPH=True: warmup upgraded raw->full on the host;
|
# PREP_IN_CUDA_GRAPH=True: warmup upgraded raw->full on the host;
|
||||||
# restore raw so capture re-runs the upgrade inside the graph.
|
# restore raw so capture re-runs the upgrade inside the graph.
|
||||||
@@ -1777,31 +1889,33 @@ class DeepseekV4AttnBackend(
|
|||||||
swa_k_cache = token_to_kv_pool.get_swa_key_buffer_radix(layer_id)
|
swa_k_cache = token_to_kv_pool.get_swa_key_buffer_radix(layer_id)
|
||||||
|
|
||||||
extra_k_cache, extra_indices, extra_topk_lengths = None, None, None
|
extra_k_cache, extra_indices, extra_topk_lengths = None, None, None
|
||||||
if compress_ratio == 4:
|
if compress_ratio != 0:
|
||||||
extra_k_cache = token_to_kv_pool.get_extra_key_buffer(layer_id)
|
extra_k_cache = token_to_kv_pool.get_extra_key_buffer(layer_id)
|
||||||
extra_indices = core_attn_metadata.c4_sparse_page_indices
|
extra_indices = core_attn_metadata.sparse_page_indices(compress_ratio)
|
||||||
extra_topk_lengths = core_attn_metadata.c4_sparse_topk_lengths
|
extra_topk_lengths = core_attn_metadata.sparse_topk_lengths(
|
||||||
elif compress_ratio == 128:
|
compress_ratio
|
||||||
extra_k_cache = token_to_kv_pool.get_extra_key_buffer(layer_id)
|
)
|
||||||
extra_indices = core_attn_metadata.c128_page_indices
|
|
||||||
extra_topk_lengths = core_attn_metadata.c128_topk_lengths_clamp1
|
|
||||||
|
|
||||||
swa_page_size = token_to_kv_pool.swa_page_size
|
swa_page_size = token_to_kv_pool.swa_page_size
|
||||||
assert swa_k_cache.ndim == 2
|
assert swa_k_cache.ndim == 2
|
||||||
k_cache_total_dim = token_to_kv_pool.swa_kv_pool.kv_cache_total_dim
|
# The kernel detects each cache's format from the last dim of this view.
|
||||||
|
k_cache_total_dim = token_to_kv_pool.get_swa_key_bytes_per_token()
|
||||||
swa_k_cache = swa_k_cache[:, : swa_page_size * k_cache_total_dim].view(
|
swa_k_cache = swa_k_cache[:, : swa_page_size * k_cache_total_dim].view(
|
||||||
swa_k_cache.shape[0], swa_page_size, 1, k_cache_total_dim
|
swa_k_cache.shape[0], swa_page_size, 1, k_cache_total_dim
|
||||||
)
|
)
|
||||||
|
|
||||||
if extra_k_cache is not None:
|
if extra_k_cache is not None:
|
||||||
extra_page_size = token_to_kv_pool.get_extra_key_page_size(layer_id)
|
extra_page_size = token_to_kv_pool.get_extra_key_page_size(layer_id)
|
||||||
|
extra_total_dim = token_to_kv_pool.get_extra_key_bytes_per_token(
|
||||||
|
layer_id
|
||||||
|
)
|
||||||
extra_k_cache = extra_k_cache[
|
extra_k_cache = extra_k_cache[
|
||||||
:, : extra_page_size * k_cache_total_dim
|
:, : extra_page_size * extra_total_dim
|
||||||
].view(
|
].view(
|
||||||
extra_k_cache.shape[0],
|
extra_k_cache.shape[0],
|
||||||
extra_page_size,
|
extra_page_size,
|
||||||
1,
|
1,
|
||||||
k_cache_total_dim,
|
extra_total_dim,
|
||||||
)
|
)
|
||||||
swa_page_indices = core_attn_metadata.swa_page_indices
|
swa_page_indices = core_attn_metadata.swa_page_indices
|
||||||
swa_topk_lengths = core_attn_metadata.swa_topk_lengths
|
swa_topk_lengths = core_attn_metadata.swa_topk_lengths
|
||||||
@@ -1966,7 +2080,7 @@ class DeepseekV4AttnBackend(
|
|||||||
cache = self.forward_metadata.sparse_prefill_cache
|
cache = self.forward_metadata.sparse_prefill_cache
|
||||||
if cache is None:
|
if cache is None:
|
||||||
cache = self._build_sparse_prefill_chunk_cache(
|
cache = self._build_sparse_prefill_chunk_cache(
|
||||||
forward_batch, num_qo_tokens=q_flat.shape[0]
|
forward_batch, core_attn_metadata, num_qo_tokens=q_flat.shape[0]
|
||||||
)
|
)
|
||||||
self.forward_metadata.sparse_prefill_cache = cache
|
self.forward_metadata.sparse_prefill_cache = cache
|
||||||
|
|
||||||
@@ -1984,24 +2098,9 @@ class DeepseekV4AttnBackend(
|
|||||||
else:
|
else:
|
||||||
extra_page_size = token_to_kv_pool.get_extra_key_page_size(layer_id)
|
extra_page_size = token_to_kv_pool.get_extra_key_page_size(layer_id)
|
||||||
extra_k_cache = token_to_kv_pool.get_extra_key_buffer(layer_id)
|
extra_k_cache = token_to_kv_pool.get_extra_key_buffer(layer_id)
|
||||||
if compress_ratio == 128:
|
flat_token_ids, combined_indices, combined_lens = cache.layer_inputs(
|
||||||
assert core_attn_metadata.c128_page_indices is not None
|
compress_ratio, core_attn_metadata, extra_page_size
|
||||||
cache.ensure_c128(core_attn_metadata.c128_page_indices)
|
)
|
||||||
flat_token_ids = cache.c128_flat_token_ids
|
|
||||||
combined_indices = cache.c128_combined_indices
|
|
||||||
combined_lens = cache.c128_combined_lens
|
|
||||||
else:
|
|
||||||
assert core_attn_metadata.c4_sparse_raw_indices is not None, (
|
|
||||||
"sparse-prefill c4 path requires c4_sparse_raw_indices "
|
|
||||||
"(allocated in init_flashmla_related when is_prefill=True)"
|
|
||||||
)
|
|
||||||
cache.ensure_c4(core_attn_metadata.page_table, extra_page_size)
|
|
||||||
flat_token_ids = cache.c4_flat_token_ids
|
|
||||||
combined_indices, combined_lens = cache.combine_c4_layer(
|
|
||||||
c4_sparse_raw_indices=core_attn_metadata.c4_sparse_raw_indices[
|
|
||||||
: cache.num_qo_tokens
|
|
||||||
],
|
|
||||||
)
|
|
||||||
n_compressed = flat_token_ids.shape[0]
|
n_compressed = flat_token_ids.shape[0]
|
||||||
workspace = self.sparse_prefill_workspace.get(
|
workspace = self.sparse_prefill_workspace.get(
|
||||||
n_compressed + cache.swa_token_ids.shape[0]
|
n_compressed + cache.swa_token_ids.shape[0]
|
||||||
@@ -2015,12 +2114,14 @@ class DeepseekV4AttnBackend(
|
|||||||
flat_token_ids,
|
flat_token_ids,
|
||||||
page_size=extra_page_size,
|
page_size=extra_page_size,
|
||||||
out=compressed_slice,
|
out=compressed_slice,
|
||||||
|
layout=token_to_kv_pool.get_extra_key_layout(layer_id),
|
||||||
)
|
)
|
||||||
dequantize_k_cache_paged(
|
dequantize_k_cache_paged(
|
||||||
token_to_kv_pool.get_swa_key_buffer_radix(layer_id),
|
token_to_kv_pool.get_swa_key_buffer_radix(layer_id),
|
||||||
cache.swa_token_ids,
|
cache.swa_token_ids,
|
||||||
page_size=cache.swa_page_size,
|
page_size=cache.swa_page_size,
|
||||||
out=swa_slice,
|
out=swa_slice,
|
||||||
|
layout=token_to_kv_pool.get_swa_key_layout(),
|
||||||
)
|
)
|
||||||
kv = workspace
|
kv = workspace
|
||||||
|
|
||||||
@@ -2141,7 +2242,7 @@ class DeepseekV4AttnBackend(
|
|||||||
cache = self.forward_metadata.sparse_prefill_cache
|
cache = self.forward_metadata.sparse_prefill_cache
|
||||||
if cache is None:
|
if cache is None:
|
||||||
cache = self._build_sparse_prefill_chunk_cache(
|
cache = self._build_sparse_prefill_chunk_cache(
|
||||||
forward_batch, num_qo_tokens=q_flat.shape[0]
|
forward_batch, core_attn_metadata, num_qo_tokens=q_flat.shape[0]
|
||||||
)
|
)
|
||||||
self.forward_metadata.sparse_prefill_cache = cache
|
self.forward_metadata.sparse_prefill_cache = cache
|
||||||
|
|
||||||
@@ -2161,25 +2262,9 @@ class DeepseekV4AttnBackend(
|
|||||||
else:
|
else:
|
||||||
extra_page_size = token_to_kv_pool.get_extra_key_page_size(layer_id)
|
extra_page_size = token_to_kv_pool.get_extra_key_page_size(layer_id)
|
||||||
extra_k_cache = token_to_kv_pool.get_extra_key_buffer(layer_id)
|
extra_k_cache = token_to_kv_pool.get_extra_key_buffer(layer_id)
|
||||||
|
flat_token_ids, combined_indices, combined_lens = cache.layer_inputs(
|
||||||
if compress_ratio == 128:
|
compress_ratio, core_attn_metadata, extra_page_size
|
||||||
assert core_attn_metadata.c128_page_indices is not None
|
)
|
||||||
cache.ensure_c128(core_attn_metadata.c128_page_indices)
|
|
||||||
flat_token_ids = cache.c128_flat_token_ids
|
|
||||||
combined_indices = cache.c128_combined_indices
|
|
||||||
combined_lens = cache.c128_combined_lens
|
|
||||||
else:
|
|
||||||
assert core_attn_metadata.c4_sparse_raw_indices is not None, (
|
|
||||||
"Q8KV8 sparse-prefill c4 path requires c4_sparse_raw_indices "
|
|
||||||
"(allocated in init_flashmla_related when is_prefill=True)"
|
|
||||||
)
|
|
||||||
cache.ensure_c4(core_attn_metadata.page_table, extra_page_size)
|
|
||||||
flat_token_ids = cache.c4_flat_token_ids
|
|
||||||
combined_indices, combined_lens = cache.combine_c4_layer(
|
|
||||||
c4_sparse_raw_indices=core_attn_metadata.c4_sparse_raw_indices[
|
|
||||||
: cache.num_qo_tokens
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
n_compressed = flat_token_ids.shape[0]
|
n_compressed = flat_token_ids.shape[0]
|
||||||
workspace = self.sparse_prefill_workspace.get(
|
workspace = self.sparse_prefill_workspace.get(
|
||||||
@@ -2189,6 +2274,8 @@ class DeepseekV4AttnBackend(
|
|||||||
compressed_slice = workspace[:n_compressed]
|
compressed_slice = workspace[:n_compressed]
|
||||||
swa_slice = workspace[n_compressed:]
|
swa_slice = workspace[n_compressed:]
|
||||||
|
|
||||||
|
# The Q8KV8 gather reads the 584-byte V4 layout only (its kernel is SM90).
|
||||||
|
assert token_to_kv_pool.get_swa_key_layout() is KVLayout.V4
|
||||||
if compressed_slice is not None:
|
if compressed_slice is not None:
|
||||||
gather_dequant_requant_fp8_paged(
|
gather_dequant_requant_fp8_paged(
|
||||||
extra_k_cache,
|
extra_k_cache,
|
||||||
@@ -2352,7 +2439,8 @@ class DeepseekV4AttnBackend(
|
|||||||
page_table=page_table,
|
page_table=page_table,
|
||||||
swa_page_indices=swa_page_indices,
|
swa_page_indices=swa_page_indices,
|
||||||
swa_topk_lengths=swa_topk_lengths,
|
swa_topk_lengths=swa_topk_lengths,
|
||||||
c4_sparse_topk=self.c4_topk,
|
index_topk=self.index_topk,
|
||||||
|
present_ratios=self.present_ratios,
|
||||||
)
|
)
|
||||||
|
|
||||||
if need_compress:
|
if need_compress:
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ if TYPE_CHECKING:
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
SWA_WINDOW = 128
|
SWA_WINDOW = 128
|
||||||
C4_TOPK = 512
|
DEFAULT_INDEX_TOPK = 512
|
||||||
PAGE_INDEX_ALIGNED_SIZE = 64
|
PAGE_INDEX_ALIGNED_SIZE = 64
|
||||||
|
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ class DSV4AttnMetadata:
|
|||||||
swa_page_indices: torch.Tensor
|
swa_page_indices: torch.Tensor
|
||||||
swa_topk_lengths: torch.Tensor
|
swa_topk_lengths: torch.Tensor
|
||||||
|
|
||||||
c4_sparse_topk: int
|
index_topk: int
|
||||||
# Shared by all layer stores; locations are in SWA space.
|
# Shared by all layer stores; locations are in SWA space.
|
||||||
swa_out_cache_loc: Optional[torch.Tensor] = None
|
swa_out_cache_loc: Optional[torch.Tensor] = None
|
||||||
c4_out_loc: Optional[torch.Tensor] = None
|
c4_out_loc: Optional[torch.Tensor] = None
|
||||||
@@ -228,7 +228,7 @@ class DSV4AttnMetadata:
|
|||||||
src=other,
|
src=other,
|
||||||
dst=self,
|
dst=self,
|
||||||
check_eq_fields=[
|
check_eq_fields=[
|
||||||
"c4_sparse_topk",
|
"index_topk",
|
||||||
"page_size",
|
"page_size",
|
||||||
"cuda_int32_kwargs",
|
"cuda_int32_kwargs",
|
||||||
],
|
],
|
||||||
@@ -263,7 +263,7 @@ class DSV4AttnMetadata:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def refresh_for_breakable_cuda_graph_replay_(self, other: DSV4AttnMetadata) -> None:
|
def refresh_for_breakable_cuda_graph_replay_(self, other: DSV4AttnMetadata) -> None:
|
||||||
assert self.c4_sparse_topk == other.c4_sparse_topk
|
assert self.index_topk == other.index_topk
|
||||||
assert self.page_size == other.page_size
|
assert self.page_size == other.page_size
|
||||||
assert self.cuda_int32_kwargs == other.cuda_int32_kwargs
|
assert self.cuda_int32_kwargs == other.cuda_int32_kwargs
|
||||||
|
|
||||||
@@ -393,22 +393,20 @@ class DSV4AttnMetadata:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def init_flashmla_related(self, is_prefill: bool = False):
|
def init_flashmla_related(self, is_prefill: bool = False):
|
||||||
# c4_sparse_topk is set from model_config.index_topk per-model
|
assert self.index_topk in (512, 1024), (
|
||||||
# (small model: 512, large model: 1024).
|
f"unexpected index_topk={self.index_topk}; "
|
||||||
assert self.c4_sparse_topk in (512, 1024), (
|
|
||||||
f"unexpected c4_sparse_topk={self.c4_sparse_topk}; "
|
|
||||||
"supported: 512 (small) or 1024 (large)"
|
"supported: 512 (small) or 1024 (large)"
|
||||||
)
|
)
|
||||||
assert self.c4_topk_lengths_clamp1 is not None
|
assert self.c4_topk_lengths_clamp1 is not None
|
||||||
self.c4_sparse_topk_lengths = torch.clamp(
|
self.c4_sparse_topk_lengths = torch.clamp(
|
||||||
self.c4_topk_lengths_clamp1, max=self.c4_sparse_topk
|
self.c4_topk_lengths_clamp1, max=self.index_topk
|
||||||
)
|
)
|
||||||
assert self.c4_topk_lengths_raw is not None
|
assert self.c4_topk_lengths_raw is not None
|
||||||
self.c4_sparse_topk_lengths_raw = torch.clamp(
|
self.c4_sparse_topk_lengths_raw = torch.clamp(
|
||||||
self.c4_topk_lengths_raw, max=self.c4_sparse_topk
|
self.c4_topk_lengths_raw, max=self.index_topk
|
||||||
)
|
)
|
||||||
self.c4_sparse_page_indices = torch.full(
|
self.c4_sparse_page_indices = torch.full(
|
||||||
(self.c4_topk_lengths_clamp1.size(0), self.c4_sparse_topk),
|
(self.c4_topk_lengths_clamp1.size(0), self.index_topk),
|
||||||
-1,
|
-1,
|
||||||
dtype=torch.int32,
|
dtype=torch.int32,
|
||||||
device=self.c4_topk_lengths_clamp1.device,
|
device=self.c4_topk_lengths_clamp1.device,
|
||||||
@@ -574,8 +572,8 @@ class DeepseekV4HipRadixBackend(
|
|||||||
self.MAX_SEQ_LEN_FOR_CAPTURE = self.req_to_token.shape[1]
|
self.MAX_SEQ_LEN_FOR_CAPTURE = self.req_to_token.shape[1]
|
||||||
|
|
||||||
assert isinstance(self.token_to_kv_pool, DeepSeekV4TokenToKVPool)
|
assert isinstance(self.token_to_kv_pool, DeepSeekV4TokenToKVPool)
|
||||||
self.c4_topk = getattr(
|
self.index_topk = getattr(
|
||||||
model_runner.model_config.hf_text_config, "index_topk", C4_TOPK
|
model_runner.model_config.hf_text_config, "index_topk", DEFAULT_INDEX_TOPK
|
||||||
)
|
)
|
||||||
self.enable_deepseek_v4_fp4_indexer: bool = (
|
self.enable_deepseek_v4_fp4_indexer: bool = (
|
||||||
get_exec().kernel.enable_deepseek_v4_fp4_indexer
|
get_exec().kernel.enable_deepseek_v4_fp4_indexer
|
||||||
@@ -2047,7 +2045,7 @@ class DeepseekV4HipRadixBackend(
|
|||||||
page_table=page_table,
|
page_table=page_table,
|
||||||
swa_page_indices=swa_page_indices,
|
swa_page_indices=swa_page_indices,
|
||||||
swa_topk_lengths=swa_topk_lengths,
|
swa_topk_lengths=swa_topk_lengths,
|
||||||
c4_sparse_topk=self.c4_topk,
|
index_topk=self.index_topk,
|
||||||
)
|
)
|
||||||
|
|
||||||
if need_compress:
|
if need_compress:
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ compressed branch becomes a no-op) and any ``compress_ratio >= 1``.
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
import triton
|
import triton
|
||||||
@@ -114,6 +114,7 @@ def combined_topk_width(topk: int, window_size: int) -> int:
|
|||||||
def combine_topk_swa_indices(
|
def combine_topk_swa_indices(
|
||||||
topk_indices: torch.Tensor,
|
topk_indices: torch.Tensor,
|
||||||
query_start_loc: torch.Tensor,
|
query_start_loc: torch.Tensor,
|
||||||
|
query_pos: torch.Tensor,
|
||||||
seq_lens: torch.Tensor,
|
seq_lens: torch.Tensor,
|
||||||
gather_lens: torch.Tensor,
|
gather_lens: torch.Tensor,
|
||||||
compressed_base: torch.Tensor,
|
compressed_base: torch.Tensor,
|
||||||
@@ -124,43 +125,22 @@ def combine_topk_swa_indices(
|
|||||||
out_indices: Optional[torch.Tensor] = None,
|
out_indices: Optional[torch.Tensor] = None,
|
||||||
out_lens: Optional[torch.Tensor] = None,
|
out_lens: Optional[torch.Tensor] = None,
|
||||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||||
"""Combine topk + SWA indices into a single ``flash_mla_sparse_fwd`` row.
|
"""Combine top-k and SWA indices for flash_mla_sparse_fwd.
|
||||||
|
|
||||||
Args:
|
Top-k indices are int32 [num_tokens, K] in each request's compressed region.
|
||||||
topk_indices: (num_tokens, K) int32. Per-query indices into the
|
Invalid entries within topk_len must be -1; later entries are ignored.
|
||||||
compressed-cache region, **already in request-local space** —
|
query_start_loc can include a cross-chunk offset; query_pos is absolute.
|
||||||
i.e. in ``[0, compressed_gather_len[r])`` for the request that
|
compressed_base and swa_base address the flat workspace. SWA-only layers use
|
||||||
owns each token. Pad entries can be any value; they are ignored
|
topk == 0, but compress_ratio must still be positive.
|
||||||
beyond ``topk_len``.
|
|
||||||
query_start_loc: (num_reqs+1,) int32. Cumulative query lengths; may
|
|
||||||
be in global (cross-chunk) space — kernel rebases by subtracting
|
|
||||||
``query_start_loc[0]``.
|
|
||||||
seq_lens: (num_reqs,) int32. Each request's full sequence length.
|
|
||||||
gather_lens: (num_reqs,) int32. Trailing tokens dequanted into the
|
|
||||||
SWA region for that request.
|
|
||||||
compressed_base: (num_reqs,) int32. Flat workspace offset where
|
|
||||||
request r's compressed region begins. Pass all-zeros (or any
|
|
||||||
value) for SWA-only layers since topk=0 disables this branch.
|
|
||||||
swa_base: (num_reqs,) int32. Flat workspace offset where request
|
|
||||||
r's SWA region begins.
|
|
||||||
window_size: SWA window size.
|
|
||||||
compress_ratio: must be ``>= 1`` even when topk==0.
|
|
||||||
topk: configured topk; pass 0 for SWA-only layers.
|
|
||||||
out_indices: optional preallocated ``(num_tokens, combined_topk)``
|
|
||||||
int32 buffer. If provided, the kernel writes the per-query prefix
|
|
||||||
``[0, topk_len + swa_len)``; positions beyond are not touched.
|
|
||||||
Caller must pre-fill with ``-1`` sentinels (and the chunk-invariant
|
|
||||||
valid-prefix length must hold across reuses).
|
|
||||||
out_lens: optional preallocated ``(num_tokens,)`` int32 buffer; the
|
|
||||||
kernel fully overwrites it, so any dtype-correct buffer works.
|
|
||||||
|
|
||||||
Returns:
|
Returns int32 indices [num_tokens, padded_topk_swa] and per-token scanned-prefix
|
||||||
combined_indices: (num_tokens, padded_topk_swa) int32, padded to a
|
lengths, including -1 entries skipped by attention. Width is padded to 128.
|
||||||
multiple of 128 with -1 sentinels.
|
Preallocated out_indices must contain -1 outside the written prefix;
|
||||||
combined_lens: (num_tokens,) int32, valid prefix length per token.
|
reuse requires chunk-invariant scanned-prefix lengths. out_lens is overwritten.
|
||||||
"""
|
"""
|
||||||
assert topk_indices.dtype == torch.int32
|
assert topk_indices.dtype == torch.int32
|
||||||
assert query_start_loc.dtype == torch.int32
|
assert query_start_loc.dtype == torch.int32
|
||||||
|
assert query_pos.dtype == torch.int32
|
||||||
assert seq_lens.dtype == torch.int32
|
assert seq_lens.dtype == torch.int32
|
||||||
assert gather_lens.dtype == torch.int32
|
assert gather_lens.dtype == torch.int32
|
||||||
assert compressed_base.dtype == torch.int32
|
assert compressed_base.dtype == torch.int32
|
||||||
@@ -201,6 +181,7 @@ def combine_topk_swa_indices(
|
|||||||
topk_indices,
|
topk_indices,
|
||||||
topk_indices.stride(0),
|
topk_indices.stride(0),
|
||||||
query_start_loc,
|
query_start_loc,
|
||||||
|
query_pos,
|
||||||
seq_lens,
|
seq_lens,
|
||||||
gather_lens,
|
gather_lens,
|
||||||
compressed_base,
|
compressed_base,
|
||||||
@@ -284,13 +265,25 @@ def build_swa_token_ids(
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SparsePrefillChunkCache:
|
class CompressedGather:
|
||||||
"""Chunk-invariant scaffolding for ``_forward_prefill_sparse``.
|
"""Positional layout of one compressed cache inside the workspace."""
|
||||||
|
|
||||||
The fields here depend only on the prefill chunk (forward_batch,
|
flat_token_ids: torch.Tensor # (num_reqs * c_max,) int32
|
||||||
req_to_token, full_to_swa_index_mapping, and the c4/c128 page tables)
|
compressed_base: torch.Tensor # (num_reqs,) int32
|
||||||
and not on the per-layer k_cache. Reused across every layer in the
|
swa_base: torch.Tensor # (num_reqs,) int32
|
||||||
chunk to avoid rebuilding tiny tensors 61 times per forward pass.
|
# Tail stays at the -1 sentinel because the valid prefix length is
|
||||||
|
# chunk-invariant per request; subsequent layers only overwrite that prefix.
|
||||||
|
combined_indices: Optional[torch.Tensor] = None
|
||||||
|
combined_lens: Optional[torch.Tensor] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SparsePrefillChunkCache:
|
||||||
|
"""Cache prefill-chunk metadata shared across layers.
|
||||||
|
|
||||||
|
Fields depend on request/token mappings and compressed page tables, not
|
||||||
|
per-layer k_cache; per-layer top-k combinations are recomputed into reused
|
||||||
|
buffers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Geometry computed once per chunk.
|
# Geometry computed once per chunk.
|
||||||
@@ -309,7 +302,8 @@ class SparsePrefillChunkCache:
|
|||||||
# ``page_size`` so that ``slot // page_size`` recovers the right page.
|
# ``page_size`` so that ``slot // page_size`` recovers the right page.
|
||||||
swa_page_size: int
|
swa_page_size: int
|
||||||
seq_lens: torch.Tensor # (num_reqs,) int32
|
seq_lens: torch.Tensor # (num_reqs,) int32
|
||||||
query_start_loc: torch.Tensor # (num_reqs+1,) int32
|
query_start_loc: torch.Tensor # (num_reqs+1,) int32, query rows per request
|
||||||
|
query_pos: torch.Tensor # (num_qo_tokens,) int32, sequence position per row
|
||||||
|
|
||||||
# SWA-side (every layer needs these, all chunk-invariant).
|
# SWA-side (every layer needs these, all chunk-invariant).
|
||||||
swa_token_ids: torch.Tensor # (total_swa,) int32
|
swa_token_ids: torch.Tensor # (total_swa,) int32
|
||||||
@@ -320,26 +314,17 @@ class SparsePrefillChunkCache:
|
|||||||
# c0 pre-computed combine output (entire input set is chunk-invariant).
|
# c0 pre-computed combine output (entire input set is chunk-invariant).
|
||||||
c0_combined_indices: torch.Tensor = field(default=None)
|
c0_combined_indices: torch.Tensor = field(default=None)
|
||||||
c0_combined_lens: torch.Tensor = field(default=None)
|
c0_combined_lens: torch.Tensor = field(default=None)
|
||||||
# c128: positional layout of the c128 cache + pre-computed combine.
|
# Compressed caches keyed by compress ratio: c128 (every block, combined once
|
||||||
c128_flat_token_ids: Optional[torch.Tensor] = None # (num_reqs * c128_max,) int32
|
# per chunk) and the top-k ratios (combined per layer).
|
||||||
c128_combined_indices: Optional[torch.Tensor] = None
|
compressed: Dict[int, CompressedGather] = field(default_factory=dict)
|
||||||
c128_combined_lens: Optional[torch.Tensor] = None
|
|
||||||
|
|
||||||
# c4: positional layout of the c4 cache (combine output is per-layer).
|
|
||||||
c4_flat_token_ids: Optional[torch.Tensor] = None # (num_reqs * c4_max,) int32
|
|
||||||
c4_page_size: Optional[int] = None
|
|
||||||
c4_compressed_base: Optional[torch.Tensor] = None # (num_reqs,) int32
|
|
||||||
c4_swa_base: Optional[torch.Tensor] = None # (num_reqs,) int32
|
|
||||||
# Tail stays at the -1 sentinel because the valid prefix length is
|
|
||||||
# chunk-invariant per request — subsequent layers only overwrite that prefix.
|
|
||||||
c4_combined_indices: Optional[torch.Tensor] = None
|
|
||||||
c4_combined_lens: Optional[torch.Tensor] = None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(
|
def build(
|
||||||
cls,
|
cls,
|
||||||
seq_lens: torch.Tensor,
|
seq_lens: torch.Tensor,
|
||||||
extend_seq_lens: torch.Tensor,
|
extend_seq_lens: torch.Tensor,
|
||||||
|
query_lens: torch.Tensor,
|
||||||
|
query_pos: torch.Tensor,
|
||||||
req_pool_indices: torch.Tensor,
|
req_pool_indices: torch.Tensor,
|
||||||
req_to_token: torch.Tensor,
|
req_to_token: torch.Tensor,
|
||||||
full_to_swa: torch.Tensor,
|
full_to_swa: torch.Tensor,
|
||||||
@@ -349,11 +334,13 @@ class SparsePrefillChunkCache:
|
|||||||
max_seq_len: int,
|
max_seq_len: int,
|
||||||
total_swa: int,
|
total_swa: int,
|
||||||
) -> "SparsePrefillChunkCache":
|
) -> "SparsePrefillChunkCache":
|
||||||
|
"""``query_lens`` / ``query_pos``: the rows this forward runs (the extend, or
|
||||||
|
a CP rank's interleaved share of it); the SWA gather spans the whole extend."""
|
||||||
device = seq_lens.device
|
device = seq_lens.device
|
||||||
num_reqs = seq_lens.shape[0]
|
num_reqs = seq_lens.shape[0]
|
||||||
|
|
||||||
query_start_loc = torch.zeros(num_reqs + 1, dtype=torch.int32, device=device)
|
query_start_loc = torch.zeros(num_reqs + 1, dtype=torch.int32, device=device)
|
||||||
query_start_loc[1:] = torch.cumsum(extend_seq_lens, dim=0).to(torch.int32)
|
query_start_loc[1:] = torch.cumsum(query_lens, dim=0).to(torch.int32)
|
||||||
|
|
||||||
swa_token_ids, swa_first_pos, swa_gather_lens, swa_offsets = (
|
swa_token_ids, swa_first_pos, swa_gather_lens, swa_offsets = (
|
||||||
build_swa_token_ids(
|
build_swa_token_ids(
|
||||||
@@ -375,6 +362,7 @@ class SparsePrefillChunkCache:
|
|||||||
swa_page_size=swa_page_size,
|
swa_page_size=swa_page_size,
|
||||||
seq_lens=seq_lens,
|
seq_lens=seq_lens,
|
||||||
query_start_loc=query_start_loc,
|
query_start_loc=query_start_loc,
|
||||||
|
query_pos=query_pos,
|
||||||
swa_token_ids=swa_token_ids,
|
swa_token_ids=swa_token_ids,
|
||||||
swa_first_pos=swa_first_pos,
|
swa_first_pos=swa_first_pos,
|
||||||
swa_gather_lens=swa_gather_lens,
|
swa_gather_lens=swa_gather_lens,
|
||||||
@@ -389,6 +377,7 @@ class SparsePrefillChunkCache:
|
|||||||
cache.c0_combined_indices, cache.c0_combined_lens = combine_topk_swa_indices(
|
cache.c0_combined_indices, cache.c0_combined_lens = combine_topk_swa_indices(
|
||||||
topk_indices=zero_topk,
|
topk_indices=zero_topk,
|
||||||
query_start_loc=query_start_loc,
|
query_start_loc=query_start_loc,
|
||||||
|
query_pos=query_pos,
|
||||||
seq_lens=seq_lens,
|
seq_lens=seq_lens,
|
||||||
gather_lens=swa_gather_lens,
|
gather_lens=swa_gather_lens,
|
||||||
compressed_base=zero_compressed_base,
|
compressed_base=zero_compressed_base,
|
||||||
@@ -399,7 +388,45 @@ class SparsePrefillChunkCache:
|
|||||||
)
|
)
|
||||||
return cache
|
return cache
|
||||||
|
|
||||||
def ensure_c128(self, c128_page_indices: torch.Tensor) -> None:
|
def _workspace_bases(self, c_max: int) -> tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
"""Flat workspace offsets of each request's compressed and SWA regions:
|
||||||
|
``c_max`` compressed slots per request, then the SWA gather."""
|
||||||
|
device = self.seq_lens.device
|
||||||
|
compressed_base = (
|
||||||
|
torch.arange(self.num_reqs, dtype=torch.int32, device=device) * c_max
|
||||||
|
).to(torch.int32)
|
||||||
|
swa_base = (self.num_reqs * c_max + self.swa_offsets[:-1]).to(torch.int32)
|
||||||
|
return compressed_base, swa_base
|
||||||
|
|
||||||
|
def layer_inputs(
|
||||||
|
self,
|
||||||
|
compress_ratio: int,
|
||||||
|
core_attn_metadata,
|
||||||
|
c_page_size: int,
|
||||||
|
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||||
|
"""``(flat_token_ids, combined_indices, combined_lens)`` for one compressed
|
||||||
|
layer. c128 gathers every block and combines once per chunk; a top-k ratio
|
||||||
|
gathers from the page table once and combines per layer from the indexer's
|
||||||
|
raw top-k."""
|
||||||
|
if compress_ratio == 128:
|
||||||
|
page_indices = core_attn_metadata.sparse_page_indices(128)
|
||||||
|
assert page_indices is not None
|
||||||
|
gather = self.ensure_c128(page_indices)
|
||||||
|
return gather.flat_token_ids, gather.combined_indices, gather.combined_lens
|
||||||
|
raw_indices = core_attn_metadata.sparse_raw_indices(compress_ratio)
|
||||||
|
assert raw_indices is not None, (
|
||||||
|
f"sparse-prefill c{compress_ratio} path requires the raw top-k indices "
|
||||||
|
"(allocated in init_flashmla_related when is_prefill=True)"
|
||||||
|
)
|
||||||
|
gather = self.ensure_compressed(
|
||||||
|
compress_ratio, core_attn_metadata.page_table, c_page_size
|
||||||
|
)
|
||||||
|
combined_indices, combined_lens = self.combine_compressed(
|
||||||
|
compress_ratio, raw_indices[: self.num_qo_tokens]
|
||||||
|
)
|
||||||
|
return gather.flat_token_ids, combined_indices, combined_lens
|
||||||
|
|
||||||
|
def ensure_c128(self, c128_page_indices: torch.Tensor) -> CompressedGather:
|
||||||
"""Populate c128-side fields from per-query c128 page indices.
|
"""Populate c128-side fields from per-query c128 page indices.
|
||||||
|
|
||||||
``c128_page_indices[q, j]`` carries slot ids derived from
|
``c128_page_indices[q, j]`` carries slot ids derived from
|
||||||
@@ -412,25 +439,24 @@ class SparsePrefillChunkCache:
|
|||||||
clamp_min(0) collapses to slot 0, sending dequant to a polluted
|
clamp_min(0) collapses to slot 0, sending dequant to a polluted
|
||||||
slot and producing garbage c128 entries.
|
slot and producing garbage c128 entries.
|
||||||
"""
|
"""
|
||||||
if self.c128_flat_token_ids is not None:
|
gather = self.compressed.get(128)
|
||||||
return
|
if gather is not None:
|
||||||
|
return gather
|
||||||
device = self.seq_lens.device
|
device = self.seq_lens.device
|
||||||
c128_max = max(self.max_seq_len // 128, 1)
|
c128_max = max(self.max_seq_len // 128, 1)
|
||||||
assert c128_max <= c128_page_indices.shape[-1], (
|
assert c128_max <= c128_page_indices.shape[-1], (
|
||||||
f"live c128 extent {c128_max} exceeds metadata capacity "
|
f"live c128 extent {c128_max} exceeds metadata capacity "
|
||||||
f"{c128_page_indices.shape[-1]}"
|
f"{c128_page_indices.shape[-1]}"
|
||||||
)
|
)
|
||||||
last_q_per_req = (self.query_start_loc[1:] - 1).long()
|
# A request without rows on this rank gathers into a region nothing reads.
|
||||||
|
last_q_per_req = (self.query_start_loc[1:] - 1).clamp_min(0).long()
|
||||||
per_req_c128 = c128_page_indices.narrow(1, 0, c128_max).index_select(
|
per_req_c128 = c128_page_indices.narrow(1, 0, c128_max).index_select(
|
||||||
0, last_q_per_req
|
0, last_q_per_req
|
||||||
)
|
)
|
||||||
# Clamp -1 -> 0 so dequant doesn't OOB; combine masks the invalid
|
# Clamp -1 -> 0 so dequant doesn't OOB; combine masks the invalid
|
||||||
# tail via topk_len.
|
# tail via topk_len.
|
||||||
flat_c128_ids = per_req_c128.reshape(-1).clamp_min(0).to(torch.int32)
|
flat_c128_ids = per_req_c128.reshape(-1).clamp_min(0).to(torch.int32)
|
||||||
compressed_base = (
|
compressed_base, swa_base = self._workspace_bases(c128_max)
|
||||||
torch.arange(self.num_reqs, dtype=torch.int32, device=device) * c128_max
|
|
||||||
).to(torch.int32)
|
|
||||||
total_compressed = self.num_reqs * c128_max
|
|
||||||
# Pre-compute the c128 combine output. topk_indices[q, j] = j is the
|
# Pre-compute the c128 combine output. topk_indices[q, j] = j is the
|
||||||
# arange-broadcast pattern; we materialize it once here so the
|
# arange-broadcast pattern; we materialize it once here so the
|
||||||
# combine kernel can read it like any other topk tensor.
|
# combine kernel can read it like any other topk tensor.
|
||||||
@@ -439,10 +465,10 @@ class SparsePrefillChunkCache:
|
|||||||
.expand(self.num_qo_tokens, -1)
|
.expand(self.num_qo_tokens, -1)
|
||||||
.contiguous()
|
.contiguous()
|
||||||
)
|
)
|
||||||
swa_base = (total_compressed + self.swa_offsets[:-1]).to(torch.int32)
|
|
||||||
combined_indices, combined_lens = combine_topk_swa_indices(
|
combined_indices, combined_lens = combine_topk_swa_indices(
|
||||||
topk_indices=topk_indices,
|
topk_indices=topk_indices,
|
||||||
query_start_loc=self.query_start_loc,
|
query_start_loc=self.query_start_loc,
|
||||||
|
query_pos=self.query_pos,
|
||||||
seq_lens=self.seq_lens,
|
seq_lens=self.seq_lens,
|
||||||
gather_lens=self.swa_gather_lens,
|
gather_lens=self.swa_gather_lens,
|
||||||
compressed_base=compressed_base,
|
compressed_base=compressed_base,
|
||||||
@@ -452,89 +478,95 @@ class SparsePrefillChunkCache:
|
|||||||
topk=c128_max,
|
topk=c128_max,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.c128_flat_token_ids = flat_c128_ids
|
gather = CompressedGather(
|
||||||
self.c128_combined_indices = combined_indices
|
flat_token_ids=flat_c128_ids,
|
||||||
self.c128_combined_lens = combined_lens
|
compressed_base=compressed_base,
|
||||||
|
swa_base=swa_base,
|
||||||
|
combined_indices=combined_indices,
|
||||||
|
combined_lens=combined_lens,
|
||||||
|
)
|
||||||
|
self.compressed[128] = gather
|
||||||
|
return gather
|
||||||
|
|
||||||
def ensure_c4(
|
def ensure_compressed(
|
||||||
self,
|
self,
|
||||||
|
compress_ratio: int,
|
||||||
page_table: torch.Tensor,
|
page_table: torch.Tensor,
|
||||||
c4_page_size: int,
|
c_page_size: int,
|
||||||
) -> None:
|
) -> CompressedGather:
|
||||||
"""Populate c4-side fields from the per-query page table.
|
"""``page_table`` is (num_qo_tokens, max_blocks); rows within a request
|
||||||
|
|
||||||
``page_table`` is (num_qo_tokens, max_blocks); rows within a request
|
|
||||||
are duplicates. The combine output is per-layer (depends on the
|
are duplicates. The combine output is per-layer (depends on the
|
||||||
layer's remapped topk_indices), so we only cache the gather-side
|
layer's remapped topk_indices), so we only cache the gather-side
|
||||||
scaffolding plus compressed/swa bases.
|
scaffolding plus compressed/swa bases.
|
||||||
"""
|
"""
|
||||||
if self.c4_flat_token_ids is not None:
|
gather = self.compressed.get(compress_ratio)
|
||||||
return
|
if gather is not None:
|
||||||
|
return gather
|
||||||
device = self.seq_lens.device
|
device = self.seq_lens.device
|
||||||
c4_max = max(self.max_seq_len // 4, 1)
|
c_max = max(self.max_seq_len // compress_ratio, 1)
|
||||||
c4_capacity = page_table.shape[-1] * c4_page_size
|
c_capacity = page_table.shape[-1] * c_page_size
|
||||||
assert c4_max <= c4_capacity, (
|
assert c_max <= c_capacity, (
|
||||||
f"live c4 extent {c4_max} exceeds metadata capacity {c4_capacity}"
|
f"live c{compress_ratio} extent {c_max} exceeds metadata capacity {c_capacity}"
|
||||||
)
|
)
|
||||||
first_q_per_req = self.query_start_loc[:-1].long()
|
first_q_per_req = (
|
||||||
num_blocks = (c4_max + c4_page_size - 1) // c4_page_size
|
self.query_start_loc[:-1].clamp_max(self.num_qo_tokens - 1).long()
|
||||||
|
)
|
||||||
|
num_blocks = (c_max + c_page_size - 1) // c_page_size
|
||||||
assert num_blocks <= page_table.shape[1]
|
assert num_blocks <= page_table.shape[1]
|
||||||
per_req_page_table = page_table.narrow(1, 0, num_blocks).index_select(
|
per_req_page_table = page_table.narrow(1, 0, num_blocks).index_select(
|
||||||
0, first_q_per_req
|
0, first_q_per_req
|
||||||
)
|
)
|
||||||
|
|
||||||
k_arange = torch.arange(c4_max, dtype=torch.int32, device=device)
|
k_arange = torch.arange(c_max, dtype=torch.int32, device=device)
|
||||||
block_idx = (k_arange // c4_page_size).long()
|
block_idx = (k_arange // c_page_size).long()
|
||||||
in_page = (k_arange % c4_page_size).to(torch.int32)
|
in_page = (k_arange % c_page_size).to(torch.int32)
|
||||||
c4_token_ids_2d = (
|
token_ids_2d = (
|
||||||
per_req_page_table.index_select(1, block_idx) * c4_page_size + in_page
|
per_req_page_table.index_select(1, block_idx) * c_page_size + in_page
|
||||||
).to(torch.int32)
|
).to(torch.int32)
|
||||||
flat_c4_ids = c4_token_ids_2d.reshape(-1).clamp_min(0)
|
flat_ids = token_ids_2d.reshape(-1).clamp_min(0)
|
||||||
total_compressed = self.num_reqs * c4_max
|
compressed_base, swa_base = self._workspace_bases(c_max)
|
||||||
compressed_base = (
|
|
||||||
torch.arange(self.num_reqs, dtype=torch.int32, device=device) * c4_max
|
|
||||||
).to(torch.int32)
|
|
||||||
swa_base = (total_compressed + self.swa_offsets[:-1]).to(torch.int32)
|
|
||||||
|
|
||||||
self.c4_flat_token_ids = flat_c4_ids
|
gather = CompressedGather(
|
||||||
self.c4_page_size = c4_page_size
|
flat_token_ids=flat_ids,
|
||||||
self.c4_compressed_base = compressed_base
|
compressed_base=compressed_base,
|
||||||
self.c4_swa_base = swa_base
|
swa_base=swa_base,
|
||||||
|
)
|
||||||
|
self.compressed[compress_ratio] = gather
|
||||||
|
return gather
|
||||||
|
|
||||||
def combine_c4_layer(
|
def combine_compressed(
|
||||||
self,
|
self,
|
||||||
c4_sparse_raw_indices: torch.Tensor,
|
compress_ratio: int,
|
||||||
|
sparse_raw_indices: torch.Tensor,
|
||||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||||
"""Per-layer combine for c4. ``c4_sparse_raw_indices`` is the topk
|
"""``sparse_raw_indices`` is the top-k as request-local compressed
|
||||||
kernel's positional output (``block_in_seq * c_page_size + in_page``)
|
positions, already the workspace coordinate ``combine_topk_swa_indices``
|
||||||
— already in the request-local workspace coordinate that
|
expects, so no remap is needed.
|
||||||
``combine_topk_swa_indices`` expects, so no remap is needed.
|
|
||||||
|
|
||||||
Reuses preallocated ``c4_combined_indices`` / ``c4_combined_lens``
|
|
||||||
buffers across layers — the kernel only overwrites the valid prefix.
|
|
||||||
"""
|
"""
|
||||||
topk = c4_sparse_raw_indices.shape[-1]
|
gather = self.compressed[compress_ratio]
|
||||||
if self.c4_combined_indices is None:
|
topk = sparse_raw_indices.shape[-1]
|
||||||
|
if gather.combined_indices is None:
|
||||||
device = self.seq_lens.device
|
device = self.seq_lens.device
|
||||||
self.c4_combined_indices = torch.full(
|
gather.combined_indices = torch.full(
|
||||||
(self.num_qo_tokens, combined_topk_width(topk, self.swa_window_size)),
|
(self.num_qo_tokens, combined_topk_width(topk, self.swa_window_size)),
|
||||||
-1,
|
-1,
|
||||||
dtype=torch.int32,
|
dtype=torch.int32,
|
||||||
device=device,
|
device=device,
|
||||||
)
|
)
|
||||||
self.c4_combined_lens = torch.zeros(
|
gather.combined_lens = torch.zeros(
|
||||||
self.num_qo_tokens, dtype=torch.int32, device=device
|
self.num_qo_tokens, dtype=torch.int32, device=device
|
||||||
)
|
)
|
||||||
return combine_topk_swa_indices(
|
return combine_topk_swa_indices(
|
||||||
topk_indices=c4_sparse_raw_indices,
|
topk_indices=sparse_raw_indices,
|
||||||
query_start_loc=self.query_start_loc,
|
query_start_loc=self.query_start_loc,
|
||||||
|
query_pos=self.query_pos,
|
||||||
seq_lens=self.seq_lens,
|
seq_lens=self.seq_lens,
|
||||||
gather_lens=self.swa_gather_lens,
|
gather_lens=self.swa_gather_lens,
|
||||||
compressed_base=self.c4_compressed_base,
|
compressed_base=gather.compressed_base,
|
||||||
swa_base=self.c4_swa_base,
|
swa_base=gather.swa_base,
|
||||||
window_size=self.swa_window_size,
|
window_size=self.swa_window_size,
|
||||||
compress_ratio=4,
|
compress_ratio=compress_ratio,
|
||||||
topk=topk,
|
topk=topk,
|
||||||
out_indices=self.c4_combined_indices,
|
out_indices=gather.combined_indices,
|
||||||
out_lens=self.c4_combined_lens,
|
out_lens=gather.combined_lens,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import dataclasses
|
|||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
from math import gcd
|
from math import gcd
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
from sglang.srt.constants import GPU_MEMORY_TYPE_KV_CACHE
|
from sglang.srt.constants import GPU_MEMORY_TYPE_KV_CACHE
|
||||||
@@ -80,6 +81,51 @@ class KVAndScore:
|
|||||||
return KVAndScore(torch.cat([v.kv_score for v in tensors], dim=dim))
|
return KVAndScore(torch.cat([v.kv_score for v in tensors], dim=dim))
|
||||||
|
|
||||||
|
|
||||||
|
def c4_state_transfer_indices(
|
||||||
|
req_pool_idx: int,
|
||||||
|
seq_len: int,
|
||||||
|
*,
|
||||||
|
ring_size: int,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""PD transfer rows of the overlap C4 state: the live tail of the request's ring."""
|
||||||
|
# Prefill and decode can have different ring sizes (8 or 16 with EAGLE/MTP);
|
||||||
|
# pair the overlap compressor's live rows by logical token position.
|
||||||
|
if ring_size < 8 or ring_size % 4 != 0:
|
||||||
|
raise ValueError(
|
||||||
|
f"C4 ring_size must be a multiple of 4 and at least 8, got {ring_size}"
|
||||||
|
)
|
||||||
|
|
||||||
|
seq_len = max(0, int(seq_len))
|
||||||
|
state_len = seq_len % 4 + 4
|
||||||
|
positions = np.arange(max(0, seq_len - state_len), seq_len, dtype=np.int64)
|
||||||
|
rows = int(req_pool_idx) * int(ring_size) + positions % int(ring_size)
|
||||||
|
return rows.astype(np.int32)
|
||||||
|
|
||||||
|
|
||||||
|
def request_scoped_state_transfer_indices(
|
||||||
|
req_pool_idx: int,
|
||||||
|
seq_len: int,
|
||||||
|
*,
|
||||||
|
ratio: int,
|
||||||
|
online: bool,
|
||||||
|
ring_size: int,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""PD transfer indices of a request-scoped compress state: the one pending
|
||||||
|
partial block of ``ratio`` tokens, as the request's single online row or the
|
||||||
|
ring page that holds it. Nothing pends at a block boundary."""
|
||||||
|
if seq_len == 0 or seq_len % ratio == 0:
|
||||||
|
return np.empty((0,), dtype=np.int32)
|
||||||
|
if online:
|
||||||
|
return np.array([int(req_pool_idx)], dtype=np.int32)
|
||||||
|
|
||||||
|
assert ring_size % ratio == 0, (
|
||||||
|
f"ring_size must be a multiple of {ratio}, got {ring_size}"
|
||||||
|
)
|
||||||
|
pages_per_req = ring_size // ratio
|
||||||
|
page = int(req_pool_idx) * pages_per_req + ((seq_len - 1) % ring_size) // ratio
|
||||||
|
return np.array([page], dtype=np.int32)
|
||||||
|
|
||||||
|
|
||||||
class CompressStatePool:
|
class CompressStatePool:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -92,11 +138,16 @@ class CompressStatePool:
|
|||||||
enable_memory_saver: bool,
|
enable_memory_saver: bool,
|
||||||
ratio: int,
|
ratio: int,
|
||||||
online: bool = False,
|
online: bool = False,
|
||||||
|
request_scoped: bool = False,
|
||||||
swa_page_size: int = 0,
|
swa_page_size: int = 0,
|
||||||
online_mtp_max_draft_tokens: int = 0,
|
online_mtp_max_draft_tokens: int = 0,
|
||||||
state_cache_page_size: int = 1,
|
state_cache_page_size: int = 1,
|
||||||
):
|
):
|
||||||
self.ratio = ratio
|
self.ratio = ratio
|
||||||
|
# Request-scoped state is addressed by req_pool_idx (one ring per request
|
||||||
|
# slot) and travels on the PD request-state component; page-scoped state
|
||||||
|
# follows the SWA pages. The pool factory decides which ratios are which.
|
||||||
|
self.request_scoped = request_scoped
|
||||||
self.ring_size = ring_size
|
self.ring_size = ring_size
|
||||||
self.swa_page_size = swa_page_size
|
self.swa_page_size = swa_page_size
|
||||||
self.page_size = state_cache_page_size
|
self.page_size = state_cache_page_size
|
||||||
@@ -143,6 +194,17 @@ class CompressStatePool:
|
|||||||
else:
|
else:
|
||||||
self.kv_score_buffer[-1].clear()
|
self.kv_score_buffer[-1].clear()
|
||||||
|
|
||||||
|
def transfer_indices(self, req_pool_idx: int, seq_len: int) -> np.ndarray:
|
||||||
|
"""PD transfer indices of this pool's state for one request."""
|
||||||
|
assert self.request_scoped, "page-scoped state travels with the SWA pages"
|
||||||
|
return request_scoped_state_transfer_indices(
|
||||||
|
req_pool_idx,
|
||||||
|
seq_len,
|
||||||
|
ratio=self.ratio,
|
||||||
|
online=self.online,
|
||||||
|
ring_size=self.ring_size,
|
||||||
|
)
|
||||||
|
|
||||||
def _alloc_kv_score_buffer(
|
def _alloc_kv_score_buffer(
|
||||||
self, *, dtype: torch.dtype, device: str, enable_memory_saver: bool
|
self, *, dtype: torch.dtype, device: str, enable_memory_saver: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from sglang.kernels.ops.attention.dsv4 import (
|
|||||||
index_buf_accessor as dsv4_index_buf_accessor,
|
index_buf_accessor as dsv4_index_buf_accessor,
|
||||||
)
|
)
|
||||||
from sglang.kernels.ops.attention.dsv4.index_buf_accessor import NopeFp8RopeBf16Pack
|
from sglang.kernels.ops.attention.dsv4.index_buf_accessor import NopeFp8RopeBf16Pack
|
||||||
|
from sglang.kernels.ops.attention.dsv4.kv_layout import KVLayout
|
||||||
from sglang.kernels.ops.attention.dsv4.unified_kv_kernels import layout
|
from sglang.kernels.ops.attention.dsv4.unified_kv_kernels import layout
|
||||||
from sglang.srt.constants import GPU_MEMORY_TYPE_KV_CACHE
|
from sglang.srt.constants import GPU_MEMORY_TYPE_KV_CACHE
|
||||||
from sglang.srt.environ import envs
|
from sglang.srt.environ import envs
|
||||||
@@ -69,6 +70,9 @@ def get_swa_ring_size(sliding_window: int, is_speculative: bool = False) -> int:
|
|||||||
|
|
||||||
|
|
||||||
class DeepSeekV4SingleKVPool(KVCache):
|
class DeepSeekV4SingleKVPool(KVCache):
|
||||||
|
# Paged FlashMLA main-KV format of this pool's rows.
|
||||||
|
kv_layout: KVLayout = KVLayout.V4
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
size: int,
|
size: int,
|
||||||
@@ -834,6 +838,13 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
|||||||
kv_pool_cls=kv_pool_cls,
|
kv_pool_cls=kv_pool_cls,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# The distinct compress ratios this stage has, sorted. Registry pools kept
|
||||||
|
# for a ratio the model lacks (wire-layout alignment) do not count.
|
||||||
|
model_ratios = set(self.compression_ratios)
|
||||||
|
self.present_ratios: Tuple[int, ...] = tuple(
|
||||||
|
ratio for ratio in sorted(self.kv_pools) if ratio in model_ratios
|
||||||
|
)
|
||||||
|
|
||||||
self._init_compressed_layer_mapping()
|
self._init_compressed_layer_mapping()
|
||||||
|
|
||||||
self._init_paged_compress_states(enable_memory_saver)
|
self._init_paged_compress_states(enable_memory_saver)
|
||||||
@@ -1012,9 +1023,7 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
|||||||
self.indexer_compress_state_pools,
|
self.indexer_compress_state_pools,
|
||||||
]:
|
]:
|
||||||
for pool in pools:
|
for pool in pools:
|
||||||
if pool is None:
|
if pool is None or pool.request_scoped:
|
||||||
continue
|
|
||||||
if pool.ratio == 128:
|
|
||||||
continue
|
continue
|
||||||
t = pool.kv_score_buffer.kv_score
|
t = pool.kv_score_buffer.kv_score
|
||||||
assert t.ndim == 2, f"expected 2D buffer, got {t.ndim}D"
|
assert t.ndim == 2, f"expected 2D buffer, got {t.ndim}D"
|
||||||
@@ -1031,7 +1040,7 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
|||||||
data_lens: List[int] = []
|
data_lens: List[int] = []
|
||||||
item_lens: List[int] = []
|
item_lens: List[int] = []
|
||||||
for pool in self.compress_state_pools:
|
for pool in self.compress_state_pools:
|
||||||
if pool is None or pool.ratio != 128:
|
if pool is None or not pool.request_scoped:
|
||||||
continue
|
continue
|
||||||
t = pool.kv_score_buffer.kv_score
|
t = pool.kv_score_buffer.kv_score
|
||||||
assert t.ndim == 2, f"expected 2D buffer, got {t.ndim}D"
|
assert t.ndim == 2, f"expected 2D buffer, got {t.ndim}D"
|
||||||
@@ -1163,6 +1172,7 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
|||||||
enable_memory_saver=enable_memory_saver,
|
enable_memory_saver=enable_memory_saver,
|
||||||
ratio=ratio,
|
ratio=ratio,
|
||||||
online=(ratio == 128 and ONLINE_C128),
|
online=(ratio == 128 and ONLINE_C128),
|
||||||
|
request_scoped=ratio == 128,
|
||||||
swa_page_size=self.swa_page_size,
|
swa_page_size=self.swa_page_size,
|
||||||
online_mtp_max_draft_tokens=(
|
online_mtp_max_draft_tokens=(
|
||||||
self.online_mtp_max_draft_tokens if ratio == 128 else 0
|
self.online_mtp_max_draft_tokens if ratio == 128 else 0
|
||||||
@@ -1267,10 +1277,24 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
|||||||
state[state_locs, :half] = 0
|
state[state_locs, :half] = 0
|
||||||
state[state_locs, half:] = float("-inf")
|
state[state_locs, half:] = float("-inf")
|
||||||
|
|
||||||
def clear_c128_req_state(self, req_pool_idx: int) -> None:
|
def request_state_transfer_indices(self, req_pool_idx: int, seq_len: int):
|
||||||
"""Reset request-scoped C128 state for one req slot."""
|
"""PD transfer indices of the request-state component for one request."""
|
||||||
|
pools = [
|
||||||
|
p for p in self.compress_state_pools if p is not None and p.request_scoped
|
||||||
|
]
|
||||||
|
assert pools, "no request-scoped state pool"
|
||||||
|
# One index list addresses every request-state buffer (one per layer), so
|
||||||
|
# the request-scoped pools must share a ring layout.
|
||||||
|
layout = (pools[0].ratio, pools[0].online, pools[0].ring_size)
|
||||||
|
assert all((p.ratio, p.online, p.ring_size) == layout for p in pools), (
|
||||||
|
"request-scoped state pools must share one ring layout"
|
||||||
|
)
|
||||||
|
return pools[0].transfer_indices(req_pool_idx, seq_len)
|
||||||
|
|
||||||
|
def clear_request_scoped_state(self, req_pool_idx: int) -> None:
|
||||||
|
"""Reset request-scoped state for one req slot."""
|
||||||
for pool in self.compress_state_pools:
|
for pool in self.compress_state_pools:
|
||||||
if pool is None or pool.ratio != 128:
|
if pool is None or not pool.request_scoped:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
state = pool.kv_score_buffer.kv_score
|
state = pool.kv_score_buffer.kv_score
|
||||||
@@ -1332,6 +1356,26 @@ class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
|||||||
assert compress_kv_pool is not None
|
assert compress_kv_pool is not None
|
||||||
return compress_kv_pool.page_size
|
return compress_kv_pool.page_size
|
||||||
|
|
||||||
|
def get_extra_key_layout(self, layer_id: int) -> KVLayout:
|
||||||
|
_, _, compress_kv_pool = self.layer_mapping[layer_id]
|
||||||
|
assert compress_kv_pool is not None
|
||||||
|
return compress_kv_pool.kv_layout
|
||||||
|
|
||||||
|
def get_extra_key_bytes_per_token(self, layer_id: int) -> int:
|
||||||
|
"""Last dim of the ``(pages, page_size, 1, bytes)`` view the attention
|
||||||
|
kernel detects the extra cache's format from."""
|
||||||
|
_, _, compress_kv_pool = self.layer_mapping[layer_id]
|
||||||
|
assert compress_kv_pool is not None
|
||||||
|
return compress_kv_pool.kv_cache_total_dim
|
||||||
|
|
||||||
|
def get_swa_key_layout(self) -> KVLayout:
|
||||||
|
return self.swa_kv_pool.kv_layout
|
||||||
|
|
||||||
|
def get_swa_key_bytes_per_token(self) -> int:
|
||||||
|
"""Last dim of the ``(pages, page_size, 1, bytes)`` view the attention
|
||||||
|
kernel detects the SWA cache's format from."""
|
||||||
|
return self.swa_kv_pool.kv_cache_total_dim
|
||||||
|
|
||||||
def get_extra_key_buffer(self, layer_id: int) -> torch.Tensor | None:
|
def get_extra_key_buffer(self, layer_id: int) -> torch.Tensor | None:
|
||||||
self.wait_layer_transfer(layer_id)
|
self.wait_layer_transfer(layer_id)
|
||||||
_, compress_layer_id, compress_kv_pool = self.layer_mapping[layer_id]
|
_, compress_layer_id, compress_kv_pool = self.layer_mapping[layer_id]
|
||||||
|
|||||||
@@ -1256,14 +1256,10 @@ def _extra_metadata_indices(
|
|||||||
the upgraded `DSV4AttnMetadata`. Mirrors the dispatch in
|
the upgraded `DSV4AttnMetadata`. Mirrors the dispatch in
|
||||||
`DeepseekV4AttnBackend.forward(compress_ratio=...)`.
|
`DeepseekV4AttnBackend.forward(compress_ratio=...)`.
|
||||||
"""
|
"""
|
||||||
if compress_ratio == 4:
|
return (
|
||||||
return (
|
core_metadata.sparse_page_indices(compress_ratio),
|
||||||
core_metadata.c4_sparse_page_indices,
|
core_metadata.sparse_topk_lengths(compress_ratio),
|
||||||
core_metadata.c4_sparse_topk_lengths,
|
)
|
||||||
)
|
|
||||||
if compress_ratio == 128:
|
|
||||||
return core_metadata.c128_page_indices, core_metadata.c128_topk_lengths_clamp1
|
|
||||||
raise ValueError(f"unsupported compress_ratio={compress_ratio}")
|
|
||||||
|
|
||||||
|
|
||||||
def _pure_torch_dsv4_combined_reference(
|
def _pure_torch_dsv4_combined_reference(
|
||||||
@@ -1401,7 +1397,8 @@ def _seed_c4_sparse_indices(
|
|||||||
non-trivial extra contribution.
|
non-trivial extra contribution.
|
||||||
"""
|
"""
|
||||||
md = fixture.backend.forward_metadata.core_metadata
|
md = fixture.backend.forward_metadata.core_metadata
|
||||||
sparse_indices = md.c4_sparse_page_indices
|
ratio = fixture.case.compress_ratio
|
||||||
|
sparse_indices = md.sparse_page_indices(ratio)
|
||||||
num_q, sparse_topk = sparse_indices.shape
|
num_q, sparse_topk = sparse_indices.shape
|
||||||
seed = torch.full(
|
seed = torch.full(
|
||||||
(num_q, sparse_topk),
|
(num_q, sparse_topk),
|
||||||
@@ -1412,12 +1409,13 @@ def _seed_c4_sparse_indices(
|
|||||||
seed[:, :num_entries] = torch.arange(
|
seed[:, :num_entries] = torch.arange(
|
||||||
num_entries, dtype=sparse_indices.dtype, device=sparse_indices.device
|
num_entries, dtype=sparse_indices.dtype, device=sparse_indices.device
|
||||||
)
|
)
|
||||||
md.c4_sparse_page_indices = seed
|
lengths = md.sparse_topk_lengths(ratio)
|
||||||
md.c4_sparse_topk_lengths = torch.full(
|
md.set_sparse_topk(
|
||||||
(num_q,),
|
ratio,
|
||||||
num_entries,
|
page_indices=seed,
|
||||||
dtype=md.c4_sparse_topk_lengths.dtype,
|
topk_lengths=torch.full(
|
||||||
device=md.c4_sparse_topk_lengths.device,
|
(num_q,), num_entries, dtype=lengths.dtype, device=lengths.device
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1438,10 +1436,11 @@ def _seed_c4_sparse_prefill_indices(
|
|||||||
asserted below.
|
asserted below.
|
||||||
"""
|
"""
|
||||||
md = fixture.backend.forward_metadata.core_metadata
|
md = fixture.backend.forward_metadata.core_metadata
|
||||||
raw_indices = md.c4_sparse_raw_indices
|
ratio = fixture.case.compress_ratio
|
||||||
|
raw_indices = md.sparse_raw_indices(ratio)
|
||||||
assert raw_indices is not None, "requires init_flashmla_related(is_prefill=True)"
|
assert raw_indices is not None, "requires init_flashmla_related(is_prefill=True)"
|
||||||
num_q, width = raw_indices.shape
|
num_q, width = raw_indices.shape
|
||||||
lens = (md.positions_casual + 1) // 4
|
lens = (md.positions_casual + 1) // ratio
|
||||||
max_len = int(lens.max().item())
|
max_len = int(lens.max().item())
|
||||||
pool = fixture.runner.token_to_kv_pool
|
pool = fixture.runner.token_to_kv_pool
|
||||||
c4_page_size = pool.get_extra_key_page_size(layer_id=0)
|
c4_page_size = pool.get_extra_key_page_size(layer_id=0)
|
||||||
@@ -1457,9 +1456,13 @@ def _seed_c4_sparse_prefill_indices(
|
|||||||
.expand(num_q, -1)
|
.expand(num_q, -1)
|
||||||
)
|
)
|
||||||
seeded = torch.where(seq < lens.unsqueeze(1), seq, seq.new_full((), -1))
|
seeded = torch.where(seq < lens.unsqueeze(1), seq, seq.new_full((), -1))
|
||||||
md.c4_sparse_raw_indices = seeded
|
lengths = md.sparse_topk_lengths(ratio)
|
||||||
md.c4_sparse_page_indices = seeded.clone()
|
md.set_sparse_topk(
|
||||||
md.c4_sparse_topk_lengths = lens.to(md.c4_sparse_topk_lengths.dtype)
|
ratio,
|
||||||
|
page_indices=seeded.clone(),
|
||||||
|
topk_lengths=lens.to(lengths.dtype),
|
||||||
|
raw_indices=seeded,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_dsv4_target_verify_attention_case(
|
def run_dsv4_target_verify_attention_case(
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class TestDSV4HipBreakableCudaGraphMetadata(unittest.TestCase):
|
|||||||
positions_casual=torch.tensor([base + 5], dtype=torch.int32),
|
positions_casual=torch.tensor([base + 5], dtype=torch.int32),
|
||||||
swa_page_indices=torch.tensor([[base + 6, base + 7]], dtype=torch.int32),
|
swa_page_indices=torch.tensor([[base + 6, base + 7]], dtype=torch.int32),
|
||||||
swa_topk_lengths=torch.tensor([base + 8], dtype=torch.int32),
|
swa_topk_lengths=torch.tensor([base + 8], dtype=torch.int32),
|
||||||
c4_sparse_topk=512,
|
index_topk=512,
|
||||||
swa_out_cache_loc=torch.tensor([base + 9], dtype=torch.int32),
|
swa_out_cache_loc=torch.tensor([base + 9], dtype=torch.int32),
|
||||||
unified=UnifiedKvMetadata(),
|
unified=UnifiedKvMetadata(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -318,6 +318,7 @@ class TestDSV4BreakableCudaGraphMetadataContract(CustomTestCase):
|
|||||||
swa_page_size=128,
|
swa_page_size=128,
|
||||||
seq_lens=torch.tensor([max_seq_len, max_seq_len], **int32),
|
seq_lens=torch.tensor([max_seq_len, max_seq_len], **int32),
|
||||||
query_start_loc=torch.tensor([0, 1, 2], **int32),
|
query_start_loc=torch.tensor([0, 1, 2], **int32),
|
||||||
|
query_pos=torch.tensor([max_seq_len - 1, max_seq_len - 1], **int32),
|
||||||
swa_token_ids=torch.empty(0, **int32),
|
swa_token_ids=torch.empty(0, **int32),
|
||||||
swa_first_pos=torch.zeros(2, **int32),
|
swa_first_pos=torch.zeros(2, **int32),
|
||||||
swa_gather_lens=torch.zeros(2, **int32),
|
swa_gather_lens=torch.zeros(2, **int32),
|
||||||
@@ -340,7 +341,8 @@ class TestDSV4BreakableCudaGraphMetadataContract(CustomTestCase):
|
|||||||
[[base + 11, base + 12], [base + 13, base + 14]], dtype=torch.int32
|
[[base + 11, base + 12], [base + 13, base + 14]], dtype=torch.int32
|
||||||
),
|
),
|
||||||
swa_topk_lengths=torch.tensor([base + 15, base + 16], dtype=torch.int32),
|
swa_topk_lengths=torch.tensor([base + 15, base + 16], dtype=torch.int32),
|
||||||
c4_sparse_topk=128,
|
index_topk=128,
|
||||||
|
present_ratios=(4, 128),
|
||||||
)
|
)
|
||||||
metadata.c4_out_loc = torch.tensor([base + 17, base + 18], dtype=torch.int32)
|
metadata.c4_out_loc = torch.tensor([base + 17, base + 18], dtype=torch.int32)
|
||||||
metadata.c128_out_loc = torch.tensor([base + 19, base + 20], dtype=torch.int32)
|
metadata.c128_out_loc = torch.tensor([base + 19, base + 20], dtype=torch.int32)
|
||||||
@@ -370,6 +372,107 @@ class TestDSV4BreakableCudaGraphMetadataContract(CustomTestCase):
|
|||||||
metadata.c128_flashmla_metadata = object()
|
metadata.c128_flashmla_metadata = object()
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
|
def test_present_ratios_gate_per_ratio_buffers(self):
|
||||||
|
from sglang.srt.layers.attention import deepseek_v4_backend as be
|
||||||
|
|
||||||
|
with mock.patch.object(be, "_create_flashmla_metadata", side_effect=object):
|
||||||
|
c4_only = self._make_core_metadata(0)
|
||||||
|
c4_only.present_ratios = (4,)
|
||||||
|
c4_only.index_topk = 512
|
||||||
|
c4_only.c128_page_indices = None
|
||||||
|
c4_only.c128_topk_lengths_clamp1 = None
|
||||||
|
c4_only.init_flashmla_related(is_prefill=True)
|
||||||
|
self.assertTrue(c4_only.has_c4)
|
||||||
|
self.assertFalse(c4_only.has_c128)
|
||||||
|
self.assertEqual(c4_only.sparse_page_indices(4).shape[0], 2)
|
||||||
|
self.assertIsNotNone(c4_only.sparse_raw_indices(4))
|
||||||
|
self.assertIsNone(c4_only.sparse_page_indices(128))
|
||||||
|
self.assertIsNotNone(c4_only.c4_flashmla_metadata)
|
||||||
|
self.assertIsNone(c4_only.c128_flashmla_metadata)
|
||||||
|
|
||||||
|
c128_only = self._make_core_metadata(0)
|
||||||
|
c128_only.present_ratios = (128,)
|
||||||
|
c128_only.index_topk = 512
|
||||||
|
c128_only.c4_topk_lengths_clamp1 = None
|
||||||
|
c128_only.init_flashmla_related(is_prefill=True)
|
||||||
|
self.assertFalse(c128_only.has_c4)
|
||||||
|
self.assertIsNone(c128_only.sparse_page_indices(4))
|
||||||
|
self.assertIsNone(c128_only.sparse_raw_indices(4))
|
||||||
|
self.assertIs(
|
||||||
|
c128_only.sparse_page_indices(128), c128_only.c128_page_indices
|
||||||
|
)
|
||||||
|
self.assertIsNone(c128_only.c4_flashmla_metadata)
|
||||||
|
self.assertIsNotNone(c128_only.c128_flashmla_metadata)
|
||||||
|
|
||||||
|
# Replay metadata must describe the same set of ratios as its source.
|
||||||
|
src = self._make_core_metadata(100)
|
||||||
|
src.present_ratios = (4,)
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self._make_core_metadata(0).copy_(src)
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self._make_core_metadata(0).refresh_for_breakable_cuda_graph_replay_(src)
|
||||||
|
|
||||||
|
def test_sparse_topk_accessors_route_by_ratio(self):
|
||||||
|
metadata = self._make_core_metadata(0)
|
||||||
|
page_indices = torch.full((2, 4), 3, dtype=torch.int32)
|
||||||
|
lengths = torch.tensor([1, 2], dtype=torch.int32)
|
||||||
|
raw = torch.full((2, 4), 5, dtype=torch.int32)
|
||||||
|
|
||||||
|
metadata.set_sparse_topk(
|
||||||
|
4, page_indices=page_indices, topk_lengths=lengths, raw_indices=raw
|
||||||
|
)
|
||||||
|
self.assertIs(metadata.sparse_page_indices(4), page_indices)
|
||||||
|
self.assertIs(metadata.sparse_topk_lengths(4), lengths)
|
||||||
|
self.assertIs(metadata.sparse_raw_indices(4), raw)
|
||||||
|
|
||||||
|
metadata.set_sparse_topk(128, page_indices=page_indices, topk_lengths=lengths)
|
||||||
|
self.assertIs(metadata.c128_page_indices, page_indices)
|
||||||
|
self.assertIs(metadata.sparse_topk_lengths(128), lengths)
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
metadata.set_sparse_topk(
|
||||||
|
128, page_indices=page_indices, topk_lengths=lengths, raw_indices=raw
|
||||||
|
)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
metadata.sparse_raw_indices(128)
|
||||||
|
for bad_ratio in (0, 7):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
metadata.sparse_page_indices(bad_ratio)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
metadata.set_sparse_topk(
|
||||||
|
bad_ratio, page_indices=page_indices, topk_lengths=lengths
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_cp_reindex_slices_present_fields_and_skips_absent_ones(self):
|
||||||
|
from sglang.srt.layers.attention import deepseek_v4_backend as be
|
||||||
|
|
||||||
|
metadata = self._make_core_metadata(0)
|
||||||
|
metadata.present_ratios = (4,)
|
||||||
|
metadata.c128_page_indices = None
|
||||||
|
metadata.c128_topk_lengths_clamp1 = None
|
||||||
|
parallel = SimpleNamespace(attn_cp_rank=1, attn_cp_size=2)
|
||||||
|
with mock.patch.object(be, "get_parallel", return_value=parallel):
|
||||||
|
metadata.apply_cp_reindex()
|
||||||
|
|
||||||
|
# Rank 1 of 2 keeps row 1 of every per-token field.
|
||||||
|
self.assertEqual(metadata.seq_lens_casual.tolist(), [8])
|
||||||
|
self.assertEqual(metadata.positions_casual.tolist(), [10])
|
||||||
|
self.assertEqual(metadata.page_table.tolist(), [[3, 4]])
|
||||||
|
self.assertEqual(metadata.swa_page_indices.tolist(), [[13, 14]])
|
||||||
|
self.assertEqual(metadata.swa_topk_lengths.tolist(), [16])
|
||||||
|
self.assertEqual(metadata.c4_topk_lengths_raw.tolist(), [22])
|
||||||
|
self.assertEqual(metadata.c4_topk_lengths_clamp1.tolist(), [24])
|
||||||
|
self.assertIsNone(metadata.c128_page_indices)
|
||||||
|
self.assertIsNone(metadata.c128_topk_lengths_clamp1)
|
||||||
|
# Cache-write locations stay in global logical order.
|
||||||
|
self.assertEqual(metadata.raw_out_loc.tolist(), [5, 6])
|
||||||
|
self.assertEqual(metadata.c4_out_loc.tolist(), [17, 18])
|
||||||
|
|
||||||
|
missing = self._make_core_metadata(0)
|
||||||
|
missing.swa_topk_lengths = None
|
||||||
|
with mock.patch.object(be, "get_parallel", return_value=parallel):
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
missing.apply_cp_reindex()
|
||||||
|
|
||||||
def test_bcg_is_explicit_and_dsv4_backend_opt_in_only(self):
|
def test_bcg_is_explicit_and_dsv4_backend_opt_in_only(self):
|
||||||
from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
|
from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
|
||||||
from sglang.srt.layers.attention.deepseek_v4_backend import (
|
from sglang.srt.layers.attention.deepseek_v4_backend import (
|
||||||
@@ -452,7 +555,7 @@ class TestDSV4BreakableCudaGraphMetadataContract(CustomTestCase):
|
|||||||
metadata = backend.forward_metadata
|
metadata = backend.forward_metadata
|
||||||
if builds:
|
if builds:
|
||||||
backend._build_sparse_prefill_chunk_cache.assert_called_once_with(
|
backend._build_sparse_prefill_chunk_cache.assert_called_once_with(
|
||||||
batch, num_qo_tokens=num_qo_tokens
|
batch, metadata.core_attn_metadata, num_qo_tokens=num_qo_tokens
|
||||||
)
|
)
|
||||||
self.assertIs(metadata.sparse_prefill_cache, cache)
|
self.assertIs(metadata.sparse_prefill_cache, cache)
|
||||||
else:
|
else:
|
||||||
@@ -646,12 +749,10 @@ class TestDSV4BreakableCudaGraphMetadataContract(CustomTestCase):
|
|||||||
for max_seq_len in (3, 4, 255, 256, 259, 260):
|
for max_seq_len in (3, 4, 255, 256, 259, 260):
|
||||||
with self.subTest(max_seq_len=max_seq_len):
|
with self.subTest(max_seq_len=max_seq_len):
|
||||||
cache = self._make_sparse_prefill_cache(max_seq_len)
|
cache = self._make_sparse_prefill_cache(max_seq_len)
|
||||||
cache.ensure_c4(page_table, c4_page_size=64)
|
gather = cache.ensure_compressed(4, page_table, c_page_size=64)
|
||||||
expected_extent = max(max_seq_len // 4, 1)
|
expected_extent = max(max_seq_len // 4, 1)
|
||||||
self.assertEqual(cache.c4_flat_token_ids.numel(), 2 * expected_extent)
|
self.assertEqual(gather.flat_token_ids.numel(), 2 * expected_extent)
|
||||||
self.assertEqual(
|
self.assertEqual(gather.compressed_base.tolist(), [0, expected_extent])
|
||||||
cache.c4_compressed_base.tolist(), [0, expected_extent]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_sparse_prefill_c128_uses_live_extent(self):
|
def test_sparse_prefill_c128_uses_live_extent(self):
|
||||||
from sglang.srt.layers.attention.dsv4 import sparse_prefill_utils
|
from sglang.srt.layers.attention.dsv4 import sparse_prefill_utils
|
||||||
@@ -670,9 +771,9 @@ class TestDSV4BreakableCudaGraphMetadataContract(CustomTestCase):
|
|||||||
"combine_topk_swa_indices",
|
"combine_topk_swa_indices",
|
||||||
return_value=combined,
|
return_value=combined,
|
||||||
) as combine:
|
) as combine:
|
||||||
cache.ensure_c128(page_indices)
|
gather = cache.ensure_c128(page_indices)
|
||||||
|
|
||||||
self.assertEqual(cache.c128_flat_token_ids.numel(), 2 * expected_extent)
|
self.assertEqual(gather.flat_token_ids.numel(), 2 * expected_extent)
|
||||||
self.assertEqual(combine.call_args.kwargs["topk"], expected_extent)
|
self.assertEqual(combine.call_args.kwargs["topk"], expected_extent)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
combine.call_args.kwargs["topk_indices"].shape,
|
combine.call_args.kwargs["topk_indices"].shape,
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
"""Unit tests for ``combine_topk_swa_indices`` (DSV4 sparse prefill).
|
||||||
|
|
||||||
|
Checks the Triton kernel against a per-row torch reference:
|
||||||
|
|
||||||
|
1. ``test_trailing_extend_matches_reference``: the V4 layout, where the query
|
||||||
|
rows are the trailing extend tokens of each request and every top-k entry
|
||||||
|
inside the scanned prefix is valid.
|
||||||
|
2. ``test_negative_one_holes_are_kept``: ``-1`` entries inside the top-k prefix
|
||||||
|
stay ``-1`` instead of being shifted by ``compressed_base``.
|
||||||
|
3. ``test_non_trailing_query_positions``: absolute ``query_pos`` that are not
|
||||||
|
the trailing extend tokens, with a cross-chunk ``query_start_loc`` offset.
|
||||||
|
4. ``test_swa_only_layer``: ``topk == 0`` writes only the window.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from sglang.srt.layers.attention.dsv4.sparse_prefill_utils import (
|
||||||
|
combine_topk_swa_indices,
|
||||||
|
)
|
||||||
|
from sglang.test.ci.ci_register import register_cuda_ci
|
||||||
|
|
||||||
|
register_cuda_ci(est_time=10, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipif(
|
||||||
|
not torch.cuda.is_available(), reason="combine_topk_swa_indices requires CUDA"
|
||||||
|
)
|
||||||
|
|
||||||
|
DEVICE = "cuda"
|
||||||
|
WINDOW = 128
|
||||||
|
# flash_mla_sparse_fwd reads rows in 128-wide tiles; the combined width is
|
||||||
|
# padded to that multiple.
|
||||||
|
TOPK_ALIGNMENT = 128
|
||||||
|
|
||||||
|
|
||||||
|
def _i32(values):
|
||||||
|
return torch.tensor(values, dtype=torch.int32, device=DEVICE)
|
||||||
|
|
||||||
|
|
||||||
|
def _reference(
|
||||||
|
topk_indices,
|
||||||
|
query_start_loc,
|
||||||
|
query_pos,
|
||||||
|
seq_lens,
|
||||||
|
gather_lens,
|
||||||
|
compressed_base,
|
||||||
|
swa_base,
|
||||||
|
window_size,
|
||||||
|
compress_ratio,
|
||||||
|
topk,
|
||||||
|
):
|
||||||
|
num_tokens = topk_indices.shape[0]
|
||||||
|
width = -(-(topk + window_size) // TOPK_ALIGNMENT) * TOPK_ALIGNMENT
|
||||||
|
out = torch.full((num_tokens, width), -1, dtype=torch.int32, device=DEVICE)
|
||||||
|
lens = torch.zeros(num_tokens, dtype=torch.int32, device=DEVICE)
|
||||||
|
qsl = query_start_loc.tolist()
|
||||||
|
base = qsl[0]
|
||||||
|
for r in range(seq_lens.shape[0]):
|
||||||
|
gather_start = int(seq_lens[r]) - int(gather_lens[r])
|
||||||
|
for token_idx in range(qsl[r] - base, qsl[r + 1] - base):
|
||||||
|
pos = int(query_pos[token_idx])
|
||||||
|
topk_len = min((pos + 1) // compress_ratio, topk)
|
||||||
|
swa_len = min(pos + 1, window_size)
|
||||||
|
vals = topk_indices[token_idx, :topk_len]
|
||||||
|
out[token_idx, :topk_len] = torch.where(
|
||||||
|
vals >= 0, vals + compressed_base[r], torch.full_like(vals, -1)
|
||||||
|
)
|
||||||
|
window = torch.arange(swa_len, dtype=torch.int32, device=DEVICE)
|
||||||
|
out[token_idx, topk_len : topk_len + swa_len] = (
|
||||||
|
swa_base[r] + window + pos - swa_len + 1 - gather_start
|
||||||
|
)
|
||||||
|
lens[token_idx] = topk_len + swa_len
|
||||||
|
return out, lens
|
||||||
|
|
||||||
|
|
||||||
|
def _check(**kwargs):
|
||||||
|
got_idx, got_lens = combine_topk_swa_indices(**kwargs)
|
||||||
|
ref_idx, ref_lens = _reference(**kwargs)
|
||||||
|
assert torch.equal(got_lens, ref_lens), (got_lens.tolist(), ref_lens.tolist())
|
||||||
|
assert torch.equal(got_idx, ref_idx)
|
||||||
|
|
||||||
|
|
||||||
|
def _trailing_case(seq_lens, extend_lens, topk, compress_ratio, seed=0):
|
||||||
|
"""Rows are the trailing ``extend_lens[r]`` tokens of request ``r``."""
|
||||||
|
gen = torch.Generator(device="cpu").manual_seed(seed)
|
||||||
|
query_pos = []
|
||||||
|
for seq_len, extend_len in zip(seq_lens, extend_lens):
|
||||||
|
query_pos.extend(range(seq_len - extend_len, seq_len))
|
||||||
|
num_tokens = len(query_pos)
|
||||||
|
topk_indices = torch.randint(
|
||||||
|
0, 1 << 20, (num_tokens, topk), generator=gen, dtype=torch.int32
|
||||||
|
).to(DEVICE)
|
||||||
|
starts = [0]
|
||||||
|
for extend_len in extend_lens:
|
||||||
|
starts.append(starts[-1] + extend_len)
|
||||||
|
gather_lens = [min(s, e + WINDOW - 1) for s, e in zip(seq_lens, extend_lens)]
|
||||||
|
return dict(
|
||||||
|
topk_indices=topk_indices,
|
||||||
|
query_start_loc=_i32(starts),
|
||||||
|
query_pos=_i32(query_pos),
|
||||||
|
seq_lens=_i32(seq_lens),
|
||||||
|
gather_lens=_i32(gather_lens),
|
||||||
|
compressed_base=_i32([1000 * r for r in range(len(seq_lens))]),
|
||||||
|
swa_base=_i32([5000 + 300 * r for r in range(len(seq_lens))]),
|
||||||
|
window_size=WINDOW,
|
||||||
|
compress_ratio=compress_ratio,
|
||||||
|
topk=topk,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("compress_ratio", [4, 128])
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"seq_lens, extend_lens",
|
||||||
|
[([96, 144], [3, 2]), ([7, 300, 1000], [7, 130, 5]), ([1], [1])],
|
||||||
|
)
|
||||||
|
def test_trailing_extend_matches_reference(seq_lens, extend_lens, compress_ratio):
|
||||||
|
_check(
|
||||||
|
**_trailing_case(seq_lens, extend_lens, topk=64, compress_ratio=compress_ratio)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_negative_one_holes_are_kept():
|
||||||
|
case = _trailing_case([512, 640], [4, 4], topk=64, compress_ratio=4)
|
||||||
|
topk_indices = case["topk_indices"]
|
||||||
|
# Holes inside the scanned prefix (every row scans the full top-k here).
|
||||||
|
topk_indices[:, 0] = -1
|
||||||
|
topk_indices[:, 5] = -1
|
||||||
|
topk_indices[3, 10:20] = -1
|
||||||
|
_check(**case)
|
||||||
|
got_idx, got_lens = combine_topk_swa_indices(**case)
|
||||||
|
assert (got_idx[:, 0] == -1).all()
|
||||||
|
assert (got_idx[:, 5] == -1).all()
|
||||||
|
assert (got_idx[3, 10:20] == -1).all()
|
||||||
|
# The scanned prefix still counts the holes.
|
||||||
|
assert int(got_lens[0]) == 64 + WINDOW
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_trailing_query_positions():
|
||||||
|
# Two requests; each rank of a two-way interleave holds every other row of
|
||||||
|
# the extend, so the query positions are not the trailing tokens. The
|
||||||
|
# query_start_loc carries a cross-chunk offset that the kernel rebases.
|
||||||
|
seq_lens = [96, 144]
|
||||||
|
extend_lens = [6, 4]
|
||||||
|
query_pos = [90, 92, 94, 140, 142]
|
||||||
|
starts = [10, 13, 15]
|
||||||
|
num_tokens = len(query_pos)
|
||||||
|
topk = 32
|
||||||
|
topk_indices = torch.arange(
|
||||||
|
num_tokens * topk, dtype=torch.int32, device=DEVICE
|
||||||
|
).view(num_tokens, topk)
|
||||||
|
gather_lens = [min(s, e + WINDOW - 1) for s, e in zip(seq_lens, extend_lens)]
|
||||||
|
_check(
|
||||||
|
topk_indices=topk_indices,
|
||||||
|
query_start_loc=_i32(starts),
|
||||||
|
query_pos=_i32(query_pos),
|
||||||
|
seq_lens=_i32(seq_lens),
|
||||||
|
gather_lens=_i32(gather_lens),
|
||||||
|
compressed_base=_i32([0, 24]),
|
||||||
|
swa_base=_i32([48, 181]),
|
||||||
|
window_size=WINDOW,
|
||||||
|
compress_ratio=4,
|
||||||
|
topk=topk,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_swa_only_layer():
|
||||||
|
case = _trailing_case([200, 50], [2, 2], topk=0, compress_ratio=4)
|
||||||
|
case["topk_indices"] = torch.zeros((4, 1), dtype=torch.int32, device=DEVICE)
|
||||||
|
_check(**case)
|
||||||
|
_, got_lens = combine_topk_swa_indices(**case)
|
||||||
|
assert got_lens.tolist() == [WINDOW, WINDOW, 49, 50]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.exit(pytest.main([__file__]))
|
||||||
@@ -17,6 +17,7 @@ import pytest
|
|||||||
import torch
|
import torch
|
||||||
|
|
||||||
from sglang.kernels.ops.attention.dsv4.index_buf_accessor import SetKAndS
|
from sglang.kernels.ops.attention.dsv4.index_buf_accessor import SetKAndS
|
||||||
|
from sglang.kernels.ops.attention.dsv4.kv_layout import KVLayout
|
||||||
from sglang.kernels.ops.attention.dsv4.quant_k_cache import (
|
from sglang.kernels.ops.attention.dsv4.quant_k_cache import (
|
||||||
quant_to_nope_fp8_rope_bf16_pack_triton,
|
quant_to_nope_fp8_rope_bf16_pack_triton,
|
||||||
)
|
)
|
||||||
@@ -25,6 +26,7 @@ from sglang.kernels.ops.attention.sparse_mla_q8kv8_prefill_sm90 import (
|
|||||||
)
|
)
|
||||||
from sglang.srt.layers.attention.deepseek_v4_backend import DeepseekV4AttnBackend
|
from sglang.srt.layers.attention.deepseek_v4_backend import DeepseekV4AttnBackend
|
||||||
from sglang.srt.layers.attention.dsv4.sparse_prefill_utils import (
|
from sglang.srt.layers.attention.dsv4.sparse_prefill_utils import (
|
||||||
|
CompressedGather,
|
||||||
SparsePrefillChunkCache,
|
SparsePrefillChunkCache,
|
||||||
SparsePrefillWorkspace,
|
SparsePrefillWorkspace,
|
||||||
use_dsv4_q8kv8_sparse_prefill,
|
use_dsv4_q8kv8_sparse_prefill,
|
||||||
@@ -90,6 +92,13 @@ class _TokenToKVPool:
|
|||||||
_ = layer_id
|
_ = layer_id
|
||||||
return self._extra_key_buffer
|
return self._extra_key_buffer
|
||||||
|
|
||||||
|
def get_swa_key_layout(self) -> KVLayout:
|
||||||
|
return KVLayout.V4
|
||||||
|
|
||||||
|
def get_extra_key_layout(self, layer_id: int) -> KVLayout:
|
||||||
|
_ = layer_id
|
||||||
|
return KVLayout.V4
|
||||||
|
|
||||||
|
|
||||||
def _sm90_available() -> bool:
|
def _sm90_available() -> bool:
|
||||||
return torch.cuda.is_available() and is_sm90_supported()
|
return torch.cuda.is_available() and is_sm90_supported()
|
||||||
@@ -219,7 +228,12 @@ def _make_sparse_prefill_case(
|
|||||||
* 0.05
|
* 0.05
|
||||||
).to(torch.bfloat16)
|
).to(torch.bfloat16)
|
||||||
attn_sink = torch.zeros(local_heads, dtype=torch.float32, device=device)
|
attn_sink = torch.zeros(local_heads, dtype=torch.float32, device=device)
|
||||||
core_attn_metadata = SimpleNamespace()
|
# position + 1 of the five query rows: seq_lens [96, 144], extend [3, 2]
|
||||||
|
core_attn_metadata = SimpleNamespace(
|
||||||
|
seq_lens_casual=torch.tensor(
|
||||||
|
[94, 95, 96, 143, 144], dtype=torch.int32, device=device
|
||||||
|
)
|
||||||
|
)
|
||||||
return backend, forward_batch, token_to_kv_pool, q, attn_sink, core_attn_metadata
|
return backend, forward_batch, token_to_kv_pool, q, attn_sink, core_attn_metadata
|
||||||
|
|
||||||
|
|
||||||
@@ -236,10 +250,17 @@ def _populate_compress_metadata(
|
|||||||
core_attn_metadata.c4_sparse_raw_indices = torch.zeros(
|
core_attn_metadata.c4_sparse_raw_indices = torch.zeros(
|
||||||
(16, 1), dtype=torch.int32, device=device
|
(16, 1), dtype=torch.int32, device=device
|
||||||
)
|
)
|
||||||
|
# The sparse prefill path selects the ratio's raw top-k through this accessor.
|
||||||
|
core_attn_metadata.sparse_raw_indices = lambda ratio: (
|
||||||
|
core_attn_metadata.c4_sparse_raw_indices if ratio == 4 else None
|
||||||
|
)
|
||||||
elif compress_ratio == 128:
|
elif compress_ratio == 128:
|
||||||
core_attn_metadata.c128_page_indices = torch.zeros(
|
core_attn_metadata.c128_page_indices = torch.zeros(
|
||||||
(16, 1), dtype=torch.int32, device=device
|
(16, 1), dtype=torch.int32, device=device
|
||||||
)
|
)
|
||||||
|
core_attn_metadata.sparse_page_indices = lambda ratio: (
|
||||||
|
core_attn_metadata.c128_page_indices if ratio == 128 else None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
@@ -248,9 +269,9 @@ def _patched_compressed_sparse_cache_paths(compress_ratio: int):
|
|||||||
yield
|
yield
|
||||||
return
|
return
|
||||||
|
|
||||||
old_ensure_c4 = SparsePrefillChunkCache.ensure_c4
|
old_ensure_compressed = SparsePrefillChunkCache.ensure_compressed
|
||||||
old_ensure_c128 = SparsePrefillChunkCache.ensure_c128
|
old_ensure_c128 = SparsePrefillChunkCache.ensure_c128
|
||||||
old_combine_c4_layer = SparsePrefillChunkCache.combine_c4_layer
|
old_combine_compressed = SparsePrefillChunkCache.combine_compressed
|
||||||
|
|
||||||
def _with_compressed_prefix(cache: SparsePrefillChunkCache, n_compressed: int):
|
def _with_compressed_prefix(cache: SparsePrefillChunkCache, n_compressed: int):
|
||||||
shifted_swa = torch.where(
|
shifted_swa = torch.where(
|
||||||
@@ -272,33 +293,48 @@ def _patched_compressed_sparse_cache_paths(compress_ratio: int):
|
|||||||
def fake_ensure_c128(self, c128_page_indices):
|
def fake_ensure_c128(self, c128_page_indices):
|
||||||
_ = c128_page_indices
|
_ = c128_page_indices
|
||||||
n_compressed = 8
|
n_compressed = 8
|
||||||
self.c128_flat_token_ids = torch.arange(
|
device = self.swa_token_ids.device
|
||||||
n_compressed, dtype=torch.int64, device=self.swa_token_ids.device
|
combined_indices, combined_lens = _with_compressed_prefix(self, n_compressed)
|
||||||
)
|
gather = CompressedGather(
|
||||||
self.c128_combined_indices, self.c128_combined_lens = _with_compressed_prefix(
|
flat_token_ids=torch.arange(n_compressed, dtype=torch.int64, device=device),
|
||||||
self, n_compressed
|
compressed_base=torch.zeros(
|
||||||
|
self.num_reqs, dtype=torch.int32, device=device
|
||||||
|
),
|
||||||
|
swa_base=torch.zeros(self.num_reqs, dtype=torch.int32, device=device),
|
||||||
|
combined_indices=combined_indices,
|
||||||
|
combined_lens=combined_lens,
|
||||||
)
|
)
|
||||||
|
self.compressed[128] = gather
|
||||||
|
return gather
|
||||||
|
|
||||||
def fake_ensure_c4(self, page_table, extra_page_size):
|
def fake_ensure_compressed(self, compress_ratio, page_table, extra_page_size):
|
||||||
_ = page_table, extra_page_size
|
_ = page_table, extra_page_size
|
||||||
n_compressed = 8
|
n_compressed = 8
|
||||||
self.c4_flat_token_ids = torch.arange(
|
device = self.swa_token_ids.device
|
||||||
n_compressed, dtype=torch.int64, device=self.swa_token_ids.device
|
gather = CompressedGather(
|
||||||
|
flat_token_ids=torch.arange(n_compressed, dtype=torch.int64, device=device),
|
||||||
|
compressed_base=torch.zeros(
|
||||||
|
self.num_reqs, dtype=torch.int32, device=device
|
||||||
|
),
|
||||||
|
swa_base=torch.zeros(self.num_reqs, dtype=torch.int32, device=device),
|
||||||
)
|
)
|
||||||
|
self.compressed[compress_ratio] = gather
|
||||||
|
return gather
|
||||||
|
|
||||||
def fake_combine_c4_layer(self, c4_sparse_raw_indices):
|
def fake_combine_compressed(self, compress_ratio, sparse_raw_indices):
|
||||||
_ = c4_sparse_raw_indices
|
_ = sparse_raw_indices
|
||||||
return _with_compressed_prefix(self, self.c4_flat_token_ids.shape[0])
|
n_compressed = self.compressed[compress_ratio].flat_token_ids.shape[0]
|
||||||
|
return _with_compressed_prefix(self, n_compressed)
|
||||||
|
|
||||||
SparsePrefillChunkCache.ensure_c128 = fake_ensure_c128
|
SparsePrefillChunkCache.ensure_c128 = fake_ensure_c128
|
||||||
SparsePrefillChunkCache.ensure_c4 = fake_ensure_c4
|
SparsePrefillChunkCache.ensure_compressed = fake_ensure_compressed
|
||||||
SparsePrefillChunkCache.combine_c4_layer = fake_combine_c4_layer
|
SparsePrefillChunkCache.combine_compressed = fake_combine_compressed
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
SparsePrefillChunkCache.ensure_c4 = old_ensure_c4
|
SparsePrefillChunkCache.ensure_compressed = old_ensure_compressed
|
||||||
SparsePrefillChunkCache.ensure_c128 = old_ensure_c128
|
SparsePrefillChunkCache.ensure_c128 = old_ensure_c128
|
||||||
SparsePrefillChunkCache.combine_c4_layer = old_combine_c4_layer
|
SparsePrefillChunkCache.combine_compressed = old_combine_compressed
|
||||||
|
|
||||||
|
|
||||||
def _make_q8kv8_kernel_args(
|
def _make_q8kv8_kernel_args(
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ from sglang.srt.disaggregation.utils import (
|
|||||||
MetadataBuffers,
|
MetadataBuffers,
|
||||||
build_transfer_entry_pairs,
|
build_transfer_entry_pairs,
|
||||||
compute_mamba_state_slice_byte_blocks,
|
compute_mamba_state_slice_byte_blocks,
|
||||||
get_dsv4_c4_state_indices,
|
|
||||||
get_dsv4_c128_state_indices,
|
|
||||||
get_qsa_pending_state_indices,
|
get_qsa_pending_state_indices,
|
||||||
setup_state_kv_args,
|
setup_state_kv_args,
|
||||||
should_send_replicated_state,
|
should_send_replicated_state,
|
||||||
@@ -45,6 +43,11 @@ from sglang.srt.environ import envs
|
|||||||
from sglang.srt.layers.attention.dsa.utils import should_use_dsa_fused_topk
|
from sglang.srt.layers.attention.dsa.utils import should_use_dsa_fused_topk
|
||||||
from sglang.srt.managers.overlap_utils import FutureMap, RelayPayload
|
from sglang.srt.managers.overlap_utils import FutureMap, RelayPayload
|
||||||
from sglang.srt.managers.schedule_batch import ReqKvInfo
|
from sglang.srt.managers.schedule_batch import ReqKvInfo
|
||||||
|
from sglang.srt.mem_cache.deepseek_v4_compress_state import (
|
||||||
|
CompressStatePool,
|
||||||
|
c4_state_transfer_indices,
|
||||||
|
request_scoped_state_transfer_indices,
|
||||||
|
)
|
||||||
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
|
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
|
||||||
from sglang.srt.mem_cache.qsa_kv_pool import (
|
from sglang.srt.mem_cache.qsa_kv_pool import (
|
||||||
QSA_ROPE_STATE_LAYER_ID,
|
QSA_ROPE_STATE_LAYER_ID,
|
||||||
@@ -775,8 +778,8 @@ class TestEagleDsaSeedTransfer(CustomTestCase):
|
|||||||
class TestDSV4C4StateIndices(unittest.TestCase):
|
class TestDSV4C4StateIndices(unittest.TestCase):
|
||||||
def test_non_mtp_to_mtp_maps_the_same_logical_positions(self):
|
def test_non_mtp_to_mtp_maps_the_same_logical_positions(self):
|
||||||
# seq_len=13 keeps logical positions [8, 13) for the overlap C4 state.
|
# seq_len=13 keeps logical positions [8, 13) for the overlap C4 state.
|
||||||
src = get_dsv4_c4_state_indices(2, 13, ring_size=8)
|
src = c4_state_transfer_indices(2, 13, ring_size=8)
|
||||||
dst = get_dsv4_c4_state_indices(2, 13, ring_size=16)
|
dst = c4_state_transfer_indices(2, 13, ring_size=16)
|
||||||
|
|
||||||
np.testing.assert_array_equal(src, np.array([16, 17, 18, 19, 20]))
|
np.testing.assert_array_equal(src, np.array([16, 17, 18, 19, 20]))
|
||||||
np.testing.assert_array_equal(dst, np.array([40, 41, 42, 43, 44]))
|
np.testing.assert_array_equal(dst, np.array([40, 41, 42, 43, 44]))
|
||||||
@@ -784,59 +787,126 @@ class TestDSV4C4StateIndices(unittest.TestCase):
|
|||||||
|
|
||||||
def test_ring_wrap_preserves_position_order(self):
|
def test_ring_wrap_preserves_position_order(self):
|
||||||
np.testing.assert_array_equal(
|
np.testing.assert_array_equal(
|
||||||
get_dsv4_c4_state_indices(0, 10, ring_size=8),
|
c4_state_transfer_indices(0, 10, ring_size=8),
|
||||||
np.array([4, 5, 6, 7, 0, 1], dtype=np.int32),
|
np.array([4, 5, 6, 7, 0, 1], dtype=np.int32),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_short_and_empty_sequences(self):
|
def test_short_and_empty_sequences(self):
|
||||||
np.testing.assert_array_equal(
|
np.testing.assert_array_equal(
|
||||||
get_dsv4_c4_state_indices(3, 3, ring_size=8),
|
c4_state_transfer_indices(3, 3, ring_size=8),
|
||||||
np.array([24, 25, 26], dtype=np.int32),
|
np.array([24, 25, 26], dtype=np.int32),
|
||||||
)
|
)
|
||||||
np.testing.assert_array_equal(
|
np.testing.assert_array_equal(
|
||||||
get_dsv4_c4_state_indices(3, 0, ring_size=8),
|
c4_state_transfer_indices(3, 0, ring_size=8),
|
||||||
np.empty((0,), dtype=np.int32),
|
np.empty((0,), dtype=np.int32),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_invalid_ring_size_is_rejected(self):
|
def test_invalid_ring_size_is_rejected(self):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
get_dsv4_c4_state_indices(0, 8, ring_size=4)
|
c4_state_transfer_indices(0, 8, ring_size=4)
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
get_dsv4_c4_state_indices(0, 8, ring_size=10)
|
c4_state_transfer_indices(0, 8, ring_size=10)
|
||||||
|
|
||||||
|
|
||||||
class TestDSV4C128StateIndices(unittest.TestCase):
|
class TestDSV4C128StateIndices(unittest.TestCase):
|
||||||
def test_online_aligned_boundary_has_no_partial_state(self):
|
def test_online_aligned_boundary_has_no_partial_state(self):
|
||||||
np.testing.assert_array_equal(
|
np.testing.assert_array_equal(
|
||||||
get_dsv4_c128_state_indices(7, 256, online=True, ring_size=1),
|
request_scoped_state_transfer_indices(
|
||||||
|
7, 256, ratio=128, online=True, ring_size=1
|
||||||
|
),
|
||||||
np.empty((0,), dtype=np.int32),
|
np.empty((0,), dtype=np.int32),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_online_partial_boundary_uses_request_slot(self):
|
def test_online_partial_boundary_uses_request_slot(self):
|
||||||
np.testing.assert_array_equal(
|
np.testing.assert_array_equal(
|
||||||
get_dsv4_c128_state_indices(7, 257, online=True, ring_size=1),
|
request_scoped_state_transfer_indices(
|
||||||
|
7, 257, ratio=128, online=True, ring_size=1
|
||||||
|
),
|
||||||
np.array([7], dtype=np.int32),
|
np.array([7], dtype=np.int32),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_offline_aligned_boundary_has_no_partial_state(self):
|
def test_offline_aligned_boundary_has_no_partial_state(self):
|
||||||
np.testing.assert_array_equal(
|
np.testing.assert_array_equal(
|
||||||
get_dsv4_c128_state_indices(7, 256, online=False, ring_size=128),
|
request_scoped_state_transfer_indices(
|
||||||
|
7, 256, ratio=128, online=False, ring_size=128
|
||||||
|
),
|
||||||
np.empty((0,), dtype=np.int32),
|
np.empty((0,), dtype=np.int32),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_offline_partial_boundary_uses_request_local_page(self):
|
def test_offline_partial_boundary_uses_request_local_page(self):
|
||||||
np.testing.assert_array_equal(
|
np.testing.assert_array_equal(
|
||||||
get_dsv4_c128_state_indices(7, 129, online=False, ring_size=256),
|
request_scoped_state_transfer_indices(
|
||||||
|
7, 129, ratio=128, online=False, ring_size=256
|
||||||
|
),
|
||||||
np.array([15], dtype=np.int32),
|
np.array([15], dtype=np.int32),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_state_pool(*, ratio, request_scoped, online=False, ring_size=256):
|
||||||
|
pool = object.__new__(CompressStatePool)
|
||||||
|
pool.ratio = ratio
|
||||||
|
pool.request_scoped = request_scoped
|
||||||
|
pool.online = online
|
||||||
|
pool.ring_size = ring_size
|
||||||
|
return pool
|
||||||
|
|
||||||
|
|
||||||
|
class TestDSV4RequestStateTransfer(unittest.TestCase):
|
||||||
|
def _kv(self, *pools):
|
||||||
|
kv = object.__new__(DeepSeekV4TokenToKVPool)
|
||||||
|
kv.compress_state_pools = [None, *pools]
|
||||||
|
return kv
|
||||||
|
|
||||||
|
def test_pool_delegates_to_its_request_scoped_state_pools(self):
|
||||||
|
# One state pool per compressed layer; all c128 layers share the ring layout.
|
||||||
|
kv = self._kv(
|
||||||
|
_make_state_pool(ratio=4, request_scoped=False),
|
||||||
|
*[
|
||||||
|
_make_state_pool(ratio=128, request_scoped=True, ring_size=256)
|
||||||
|
for _ in range(20)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
np.testing.assert_array_equal(
|
||||||
|
kv.request_state_transfer_indices(7, 129),
|
||||||
|
request_scoped_state_transfer_indices(
|
||||||
|
7, 129, ratio=128, online=False, ring_size=256
|
||||||
|
),
|
||||||
|
)
|
||||||
|
np.testing.assert_array_equal(
|
||||||
|
kv.request_state_transfer_indices(7, 256), np.empty((0,), dtype=np.int32)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_online_pool_ships_the_request_row(self):
|
||||||
|
kv = self._kv(
|
||||||
|
_make_state_pool(ratio=128, request_scoped=True, online=True, ring_size=1)
|
||||||
|
)
|
||||||
|
np.testing.assert_array_equal(
|
||||||
|
kv.request_state_transfer_indices(7, 257), np.array([7], dtype=np.int32)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_requires_request_scoped_pools_with_one_ring_layout(self):
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self._kv(
|
||||||
|
_make_state_pool(ratio=4, request_scoped=False)
|
||||||
|
).request_state_transfer_indices(0, 5)
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
self._kv(
|
||||||
|
_make_state_pool(ratio=128, request_scoped=True, ring_size=128),
|
||||||
|
_make_state_pool(ratio=128, request_scoped=True, ring_size=256),
|
||||||
|
).request_state_transfer_indices(0, 5)
|
||||||
|
|
||||||
|
def test_page_scoped_pool_has_no_transfer_indices(self):
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
_make_state_pool(ratio=4, request_scoped=False).transfer_indices(0, 5)
|
||||||
|
|
||||||
|
|
||||||
def _buf_infos(*ptrs):
|
def _buf_infos(*ptrs):
|
||||||
return list(ptrs), [ptr + 100 for ptr in ptrs], [ptr + 200 for ptr in ptrs]
|
return list(ptrs), [ptr + 100 for ptr in ptrs], [ptr + 200 for ptr in ptrs]
|
||||||
|
|
||||||
|
|
||||||
def _make_dsv4_target(*, unified, mapping=None):
|
def _make_dsv4_target(*, unified, mapping=None):
|
||||||
pool = object.__new__(DeepSeekV4TokenToKVPool)
|
pool = object.__new__(DeepSeekV4TokenToKVPool)
|
||||||
|
pool.compression_ratios = [0, 4, 128]
|
||||||
pool._unified_kv = unified
|
pool._unified_kv = unified
|
||||||
pool.page_size = 256
|
pool.page_size = 256
|
||||||
pool.sliding_window = 128
|
pool.sliding_window = 128
|
||||||
|
|||||||
Reference in New Issue
Block a user