Stabilize CP shared-KV prefetch around draft cache hits
Cache-hit EAGLE/NextN draft extends can enter the draft DeepEP MoE immediately after CP shared-KV attention. The partial current-reuse path is kept for target layers, but draft cache-hit suffixes now use full materialization until draft has an explicit same-layer reuse contract. Next-layer MLA/index prefetch is also gated by the actual model depth, so the single-layer draft model does not enqueue unused next-layer async work. The temporary stage traces used to isolate the hang are removed. The retained draft current-reuse fallback is a bounded warning because it changes the runtime path intentionally. Constraint: EAGLE/NextN has one executable draft layer and mirrors target KV state. Rejected: Keep partial current reuse for draft cache-hit suffixes | reproduced hangs at draft layer0 before DeepEP MoE completion. Rejected: Keep temporary stage traces | useful for diagnosis but too noisy for normal runs. Confidence: medium Scope-risk: moderate Directive: Do not re-enable draft cache-hit partial current reuse without an explicit draft same-layer reuse contract and ETE validation with CP shared KV + HiCache + EAGLE. Tested: py_compile on edited Python files; git diff --check; temp trace grep returned no matches. Not-tested: Local targeted pytest is blocked by missing pybase64 in this environment; full ETE after log cleanup not run.
This commit is contained in:
@@ -22,6 +22,7 @@ from sglang.srt.layers.attention.nsa.cp_shared_kv_runtime import (
|
|||||||
get_or_build_shared_token_kv_slot_remap,
|
get_or_build_shared_token_kv_slot_remap,
|
||||||
materialize_local_paged_buffer_page_slots_into,
|
materialize_local_paged_buffer_page_slots_into,
|
||||||
materialize_local_token_kv_page_slots_into,
|
materialize_local_token_kv_page_slots_into,
|
||||||
|
merge_materialized_and_current_kv,
|
||||||
remap_logical_pages_to_slot_dense_pages,
|
remap_logical_pages_to_slot_dense_pages,
|
||||||
remap_logical_locs_to_slot_dense_locs_optimized,
|
remap_logical_locs_to_slot_dense_locs_optimized,
|
||||||
slot_range_to_page_slice,
|
slot_range_to_page_slice,
|
||||||
@@ -647,6 +648,108 @@ class CpSharedKVMlaPrefetcher:
|
|||||||
)
|
)
|
||||||
return dense_kv_cache, dense_locs
|
return dense_kv_cache, dense_locs
|
||||||
|
|
||||||
|
def consume_prefix_with_current(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
layer_id: int,
|
||||||
|
kv_cache: torch.Tensor,
|
||||||
|
logical_locs: torch.Tensor,
|
||||||
|
current_kv_cache: torch.Tensor,
|
||||||
|
current_locs: torch.Tensor,
|
||||||
|
current_remap_page_size: int | None = None,
|
||||||
|
current_remap_logical_page_capacity: int | None = None,
|
||||||
|
) -> Optional[tuple[torch.Tensor, torch.Tensor]]:
|
||||||
|
"""Consume the prefetched prefix and append current-layer KV rows.
|
||||||
|
|
||||||
|
This is the partial-current-reuse variant of :meth:`consume`: prefix
|
||||||
|
pages are already materialized/reduced by the prefetch stream, while
|
||||||
|
current/suffix pages are not copied from the shared pool. Current locs
|
||||||
|
in ``logical_locs`` are remapped to the appended current KV rows.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.disabled:
|
||||||
|
self._log_layer(
|
||||||
|
layer_id,
|
||||||
|
"consume_prefix_current_skip reason=disabled layer=%s",
|
||||||
|
layer_id,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
self._log_layer(
|
||||||
|
layer_id,
|
||||||
|
"consume_prefix_current_enter layer=%s prefix_pages=%s total_slots=%s handles=%s",
|
||||||
|
layer_id,
|
||||||
|
self.prefix_pages,
|
||||||
|
self.total_slots,
|
||||||
|
_debug_handle_keys(layer_id, self.handles),
|
||||||
|
)
|
||||||
|
handle = self.handles.get(layer_id)
|
||||||
|
if handle is None:
|
||||||
|
self._log_layer(layer_id, "consume_prefix_current_miss layer=%s", layer_id)
|
||||||
|
return None
|
||||||
|
if handle.event is None:
|
||||||
|
self.launch_pending_reduce()
|
||||||
|
handle = self.handles.get(layer_id)
|
||||||
|
if handle is None or handle.event is None:
|
||||||
|
self._log_layer(
|
||||||
|
layer_id,
|
||||||
|
"consume_prefix_current_miss reason=prefix_reduce_not_ready layer=%s",
|
||||||
|
layer_id,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
handle = self.handles.pop(layer_id)
|
||||||
|
if self.pending_attention_handle is handle:
|
||||||
|
self.pending_attention_handle = None
|
||||||
|
if handle.layer_id != layer_id:
|
||||||
|
self.disabled = True
|
||||||
|
self._log_layer(
|
||||||
|
layer_id,
|
||||||
|
"consume_prefix_current_skip reason=layer_mismatch expected=%s actual=%s",
|
||||||
|
layer_id,
|
||||||
|
handle.layer_id,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
consume_cpu = _cpu_timing_start()
|
||||||
|
wait_cpu = _cpu_timing_start()
|
||||||
|
torch.cuda.current_stream().wait_event(handle.event)
|
||||||
|
wait_ms = _cpu_timing_ms(wait_cpu)
|
||||||
|
dense_kv_cache = handle.dense_kv_cache
|
||||||
|
|
||||||
|
remap_cpu = _cpu_timing_start()
|
||||||
|
logical_locs = filter_locs_mappable_to_physical_pool(
|
||||||
|
logical_locs=logical_locs,
|
||||||
|
layout=self.layout,
|
||||||
|
physical_token_capacity=kv_cache.shape[0],
|
||||||
|
)
|
||||||
|
dense_locs = remap_logical_locs_to_slot_dense_locs_optimized(
|
||||||
|
logical_locs,
|
||||||
|
page_inverse=self.page_inverse,
|
||||||
|
page_size=self.page_size,
|
||||||
|
)
|
||||||
|
mixed_kv_cache, mixed_locs, _ = merge_materialized_and_current_kv(
|
||||||
|
materialized_kv_cache=dense_kv_cache,
|
||||||
|
materialized_dense_locs=dense_locs,
|
||||||
|
current_kv_cache=current_kv_cache,
|
||||||
|
logical_locs=logical_locs,
|
||||||
|
current_locs=current_locs,
|
||||||
|
page_size=current_remap_page_size,
|
||||||
|
logical_page_capacity=current_remap_logical_page_capacity,
|
||||||
|
)
|
||||||
|
remap_ms = _cpu_timing_ms(remap_cpu)
|
||||||
|
total_ms = _cpu_timing_ms(consume_cpu)
|
||||||
|
self._log_layer(
|
||||||
|
layer_id,
|
||||||
|
"consume_prefix_current_hit layer=%s prefix_pages=%s dense_rows=%s current_rows=%s total_ms=%.3f wait_ms=%.3f remap_ms=%.3f",
|
||||||
|
layer_id,
|
||||||
|
self.prefix_pages,
|
||||||
|
int(dense_kv_cache.shape[0]),
|
||||||
|
int(current_kv_cache.shape[0]),
|
||||||
|
total_ms,
|
||||||
|
wait_ms,
|
||||||
|
remap_ms,
|
||||||
|
)
|
||||||
|
return mixed_kv_cache, mixed_locs
|
||||||
|
|
||||||
def start_next_layer_prefix(
|
def start_next_layer_prefix(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ logger = logging.getLogger(__name__)
|
|||||||
_DEBUG_LOG_COUNTS: dict[str, int] = {}
|
_DEBUG_LOG_COUNTS: dict[str, int] = {}
|
||||||
_TAI_MATERIALIZE_FALLBACK_LOG_COUNTS: dict[str, int] = {}
|
_TAI_MATERIALIZE_FALLBACK_LOG_COUNTS: dict[str, int] = {}
|
||||||
_TAI_FUSED_MLA_STORE_FALLBACK_LOG_COUNTS: dict[str, int] = {}
|
_TAI_FUSED_MLA_STORE_FALLBACK_LOG_COUNTS: dict[str, int] = {}
|
||||||
|
_CURRENT_REUSE_FALLBACK_LOG_COUNTS: dict[str, int] = {}
|
||||||
_SLOT_REMAP_CACHE_LOG_COUNTS: dict[str, int] = {}
|
_SLOT_REMAP_CACHE_LOG_COUNTS: dict[str, int] = {}
|
||||||
_MLA_PREFETCH_LOG_PROBE_LAYER = 2
|
_MLA_PREFETCH_LOG_PROBE_LAYER = 2
|
||||||
_MLA_PREFETCH_DEFAULT_MIN_PREFIX_TOKENS = max(
|
_MLA_PREFETCH_DEFAULT_MIN_PREFIX_TOKENS = max(
|
||||||
@@ -70,7 +71,8 @@ def cp_shared_kv_mla_prefetch_min_prefix_pages(
|
|||||||
"""Minimum prefix pages required to enable Phase8 prefetch.
|
"""Minimum prefix pages required to enable Phase8 prefetch.
|
||||||
|
|
||||||
Negative env values mean "use the dynamic default": at least one page per CP
|
Negative env values mean "use the dynamic default": at least one page per CP
|
||||||
lane and, when the runtime page size is known, at least 1K prefix tokens.
|
lane and, when the runtime page size is known, at least the configured
|
||||||
|
prefix-token threshold.
|
||||||
This keeps tiny cache-hit prefixes on the simpler synchronous materialize
|
This keeps tiny cache-hit prefixes on the simpler synchronous materialize
|
||||||
path where prefix prefetch launch/collective overhead can dominate. Set the
|
path where prefix prefetch launch/collective overhead can dominate. Set the
|
||||||
env to 0 to disable the gate, or to a positive absolute page count for
|
env to 0 to disable the gate, or to a positive absolute page count for
|
||||||
@@ -103,6 +105,47 @@ def cp_shared_kv_mla_prefetch_should_log_layer(layer_id: int) -> bool:
|
|||||||
return int(layer_id) == _MLA_PREFETCH_LOG_PROBE_LAYER
|
return int(layer_id) == _MLA_PREFETCH_LOG_PROBE_LAYER
|
||||||
|
|
||||||
|
|
||||||
|
def cp_shared_kv_is_draft_input(forward_batch: Any) -> bool:
|
||||||
|
spec_info = getattr(forward_batch, "spec_info", None)
|
||||||
|
is_draft_input = getattr(spec_info, "is_draft_input", None)
|
||||||
|
return callable(is_draft_input) and is_draft_input()
|
||||||
|
|
||||||
|
|
||||||
|
def cp_shared_kv_should_prefetch_next_layer(
|
||||||
|
forward_batch: Any,
|
||||||
|
layer_id: int,
|
||||||
|
) -> bool:
|
||||||
|
"""Return whether layer ``layer_id`` has a real next layer to prefetch.
|
||||||
|
|
||||||
|
CP shared-KV prefetch is a next-layer optimization. Draft/NextN models reuse
|
||||||
|
decoder layer id 0 but have only one executable layer, so blindly prefetching
|
||||||
|
layer 1 creates unused async work before the draft MoE/DeepEP collective.
|
||||||
|
The decoder layer publishes the current model depth on the ForwardBatch for
|
||||||
|
this check.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if cp_shared_kv_is_draft_input(forward_batch):
|
||||||
|
return False
|
||||||
|
|
||||||
|
num_model_layers = getattr(forward_batch, "cp_shared_kv_num_model_layers", None)
|
||||||
|
if num_model_layers is None:
|
||||||
|
return True
|
||||||
|
return int(layer_id) + 1 < int(num_model_layers)
|
||||||
|
|
||||||
|
|
||||||
|
def _log_current_reuse_fallback(
|
||||||
|
key: str,
|
||||||
|
message: str,
|
||||||
|
*args,
|
||||||
|
limit: int = 8,
|
||||||
|
) -> None:
|
||||||
|
count = _CURRENT_REUSE_FALLBACK_LOG_COUNTS.get(key, 0)
|
||||||
|
if count >= limit:
|
||||||
|
return
|
||||||
|
_CURRENT_REUSE_FALLBACK_LOG_COUNTS[key] = count + 1
|
||||||
|
logger.warning(message, *args)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class SharedTokenKVSlotRemap:
|
class SharedTokenKVSlotRemap:
|
||||||
slot_logical_pages: torch.Tensor
|
slot_logical_pages: torch.Tensor
|
||||||
@@ -723,6 +766,137 @@ def is_current_only_extend_batch(forward_batch) -> bool:
|
|||||||
return seq_lens_list == extend_seq_lens_list
|
return seq_lens_list == extend_seq_lens_list
|
||||||
|
|
||||||
|
|
||||||
|
def can_reuse_current_extend_kv(forward_batch) -> bool:
|
||||||
|
"""Return whether the current extend chunk can be used as dense KV rows.
|
||||||
|
|
||||||
|
Unlike :func:`is_current_only_extend_batch`, this allows a cached/history
|
||||||
|
prefix. The contract is intentionally narrow for now: single-batch extend,
|
||||||
|
CPU length metadata present, and ``out_cache_loc`` exactly covers the current
|
||||||
|
extend chunk. The caller still owns model/backend gates such as CP shared KV
|
||||||
|
enabled, in-seq-split mode, and tensor shape compatibility.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if forward_batch is None:
|
||||||
|
return False
|
||||||
|
forward_mode = getattr(forward_batch, "forward_mode", None)
|
||||||
|
if forward_mode is None or not forward_mode.is_extend_without_speculative():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if int(getattr(forward_batch, "batch_size", 0)) != 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
extend_seq_lens_cpu = getattr(forward_batch, "extend_seq_lens_cpu", None)
|
||||||
|
seq_lens_cpu = getattr(forward_batch, "seq_lens_cpu", None)
|
||||||
|
out_cache_loc = getattr(forward_batch, "out_cache_loc", None)
|
||||||
|
if extend_seq_lens_cpu is None or seq_lens_cpu is None or out_cache_loc is None:
|
||||||
|
return False
|
||||||
|
if len(extend_seq_lens_cpu) != 1 or int(seq_lens_cpu.numel()) != 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
extend_len = int(extend_seq_lens_cpu[0])
|
||||||
|
seq_len = int(seq_lens_cpu[0].item())
|
||||||
|
if extend_len <= 0 or seq_len < extend_len:
|
||||||
|
return False
|
||||||
|
return int(out_cache_loc.numel()) == extend_len
|
||||||
|
|
||||||
|
|
||||||
|
def should_reuse_current_extend_kv(forward_batch) -> bool:
|
||||||
|
"""Return whether MLA should splice current extend KV into materialized KV.
|
||||||
|
|
||||||
|
Partial current reuse appends the freshly computed suffix KV to the
|
||||||
|
materialized prefix buffer. That path is safe for the target model, but the
|
||||||
|
EAGLE/NextN draft layer has a different lifetime contract: it mirrors target
|
||||||
|
KV state and immediately enters DeepEP MoE after attention. A cache-hit
|
||||||
|
draft suffix observed in production can leave all ranks stuck in that MoE
|
||||||
|
collective after the partial-reuse attention path returns. Keep draft
|
||||||
|
cache-hit suffixes on the older full-materialize path until draft gets an
|
||||||
|
explicit same-layer reuse contract.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not cp_shared_kv_current_reuse_enabled():
|
||||||
|
return False
|
||||||
|
|
||||||
|
current_only = is_current_only_extend_batch(forward_batch)
|
||||||
|
partial_current = can_reuse_current_extend_kv(forward_batch)
|
||||||
|
if not (current_only or partial_current):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if cp_shared_kv_is_draft_input(forward_batch) and not current_only:
|
||||||
|
_log_current_reuse_fallback(
|
||||||
|
"draft_partial_current_reuse_disabled",
|
||||||
|
"CP shared KV current-reuse fallback (draft_partial_current_reuse): "
|
||||||
|
"cache-hit EAGLE/NextN draft uses full materialize instead of "
|
||||||
|
"partial current reuse. prefix_lens=%s extend_lens=%s",
|
||||||
|
getattr(forward_batch, "extend_prefix_lens_cpu", None),
|
||||||
|
getattr(forward_batch, "extend_seq_lens_cpu", None),
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def current_loc_remap_fast_path_args(
|
||||||
|
forward_batch,
|
||||||
|
) -> tuple[int | None, int | None]:
|
||||||
|
"""Return page-inverse remap args when the current chunk is page aligned.
|
||||||
|
|
||||||
|
The page-inverse path in :func:`build_current_loc_remap` assumes row 0 of
|
||||||
|
``current_locs`` is the first token of a logical page. That is true for the
|
||||||
|
existing current-only extend path, but not guaranteed for cache-hit partial
|
||||||
|
extend where the cached prefix may end mid-page. For partial extend, return
|
||||||
|
``(None, None)`` so callers take the general sort/search remap path.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not is_current_only_extend_batch(forward_batch):
|
||||||
|
return None, None
|
||||||
|
if len(getattr(forward_batch, "extend_seq_lens_cpu", []) or []) != 1:
|
||||||
|
return None, None
|
||||||
|
token_to_kv_pool = getattr(forward_batch, "token_to_kv_pool", None)
|
||||||
|
layout = getattr(forward_batch, "cp_shared_kv_layout", None)
|
||||||
|
if token_to_kv_pool is None or layout is None:
|
||||||
|
return None, None
|
||||||
|
page_size = int(getattr(token_to_kv_pool, "page_size", 0))
|
||||||
|
if page_size <= 0:
|
||||||
|
return None, None
|
||||||
|
logical_page_capacity = (
|
||||||
|
max(int(getattr(token_to_kv_pool, "size", 0)) // page_size - 1, 0)
|
||||||
|
* int(layout.cp_size)
|
||||||
|
+ 1
|
||||||
|
)
|
||||||
|
return page_size, logical_page_capacity
|
||||||
|
|
||||||
|
|
||||||
|
def merge_materialized_and_current_kv(
|
||||||
|
*,
|
||||||
|
materialized_kv_cache: torch.Tensor,
|
||||||
|
materialized_dense_locs: torch.Tensor,
|
||||||
|
current_kv_cache: torch.Tensor,
|
||||||
|
logical_locs: torch.Tensor,
|
||||||
|
current_locs: torch.Tensor,
|
||||||
|
page_size: int | None = None,
|
||||||
|
logical_page_capacity: int | None = None,
|
||||||
|
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||||
|
"""Append current KV rows to a materialized prefix buffer and remap locs.
|
||||||
|
|
||||||
|
``materialized_dense_locs`` is the remap returned by the prefix/history
|
||||||
|
materialization path. Entries corresponding to current extend tokens are
|
||||||
|
replaced with offsets into the appended ``current_kv_cache``. Non-current
|
||||||
|
entries remain untouched, including ``-1`` invalid sentinels.
|
||||||
|
"""
|
||||||
|
|
||||||
|
current_mask, current_rows = build_current_loc_remap(
|
||||||
|
logical_locs,
|
||||||
|
current_locs,
|
||||||
|
page_size=page_size,
|
||||||
|
logical_page_capacity=logical_page_capacity,
|
||||||
|
)
|
||||||
|
current_offset = int(materialized_kv_cache.shape[0])
|
||||||
|
current_dense_locs = current_rows + current_offset
|
||||||
|
mixed_locs = torch.where(current_mask, current_dense_locs, materialized_dense_locs)
|
||||||
|
mixed_kv_cache = torch.cat([materialized_kv_cache, current_kv_cache], dim=0)
|
||||||
|
return mixed_kv_cache, mixed_locs, current_mask
|
||||||
|
|
||||||
|
|
||||||
def cp_shared_kv_debug_log(
|
def cp_shared_kv_debug_log(
|
||||||
key: str,
|
key: str,
|
||||||
message: str,
|
message: str,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from sglang.srt.layers.attention.nsa.cp_shared_kv_runtime import (
|
|||||||
cp_shared_kv_mla_prefetch_log,
|
cp_shared_kv_mla_prefetch_log,
|
||||||
cp_shared_kv_mla_prefetch_log_enabled,
|
cp_shared_kv_mla_prefetch_log_enabled,
|
||||||
cp_shared_kv_mla_prefetch_should_log_layer,
|
cp_shared_kv_mla_prefetch_should_log_layer,
|
||||||
|
cp_shared_kv_should_prefetch_next_layer,
|
||||||
filter_owned_logical_locs,
|
filter_owned_logical_locs,
|
||||||
get_or_build_shared_paged_buffer_slot_remap,
|
get_or_build_shared_paged_buffer_slot_remap,
|
||||||
is_current_only_extend_batch,
|
is_current_only_extend_batch,
|
||||||
@@ -405,6 +406,8 @@ class Indexer(MultiPlatformOp):
|
|||||||
)
|
)
|
||||||
if index_prefetcher is None:
|
if index_prefetcher is None:
|
||||||
return
|
return
|
||||||
|
if not cp_shared_kv_should_prefetch_next_layer(forward_batch, layer_id):
|
||||||
|
return
|
||||||
index_prefetcher.start_next_layer_prefix(
|
index_prefetcher.start_next_layer_prefix(
|
||||||
next_layer_id=next_layer_id,
|
next_layer_id=next_layer_id,
|
||||||
token_to_kv_pool=forward_batch.token_to_kv_pool,
|
token_to_kv_pool=forward_batch.token_to_kv_pool,
|
||||||
|
|||||||
@@ -17,14 +17,18 @@ from sglang.srt.layers.attention.nsa.cp_shared_kv_runtime import (
|
|||||||
build_current_loc_remap,
|
build_current_loc_remap,
|
||||||
cp_shared_kv_debug_enabled,
|
cp_shared_kv_debug_enabled,
|
||||||
cp_shared_kv_debug_log,
|
cp_shared_kv_debug_log,
|
||||||
cp_shared_kv_current_reuse_enabled,
|
|
||||||
cp_shared_kv_mla_prefetch_log,
|
cp_shared_kv_mla_prefetch_log,
|
||||||
cp_shared_kv_mla_prefetch_log_enabled,
|
cp_shared_kv_mla_prefetch_log_enabled,
|
||||||
cp_shared_kv_mla_prefetch_should_log_layer,
|
cp_shared_kv_mla_prefetch_should_log_layer,
|
||||||
|
cp_shared_kv_is_draft_input,
|
||||||
|
cp_shared_kv_should_prefetch_next_layer,
|
||||||
|
current_loc_remap_fast_path_args,
|
||||||
filter_owned_logical_locs,
|
filter_owned_logical_locs,
|
||||||
get_or_build_shared_token_kv_slot_remap,
|
get_or_build_shared_token_kv_slot_remap,
|
||||||
is_current_only_extend_batch,
|
is_current_only_extend_batch,
|
||||||
materialize_shared_token_kv_buffer,
|
materialize_shared_token_kv_buffer,
|
||||||
|
merge_materialized_and_current_kv,
|
||||||
|
should_reuse_current_extend_kv,
|
||||||
tensor_debug_checksum,
|
tensor_debug_checksum,
|
||||||
tensor_debug_summary,
|
tensor_debug_summary,
|
||||||
)
|
)
|
||||||
@@ -878,12 +882,21 @@ class NativeSparseAttnBackend(
|
|||||||
token_to_batch_idx=token_to_batch_idx,
|
token_to_batch_idx=token_to_batch_idx,
|
||||||
)
|
)
|
||||||
self.forward_metadata = metadata
|
self.forward_metadata = metadata
|
||||||
mla_prefetcher = CpSharedKVMlaPrefetcher.maybe_create(
|
# EAGLE/NextN draft has a single executable layer. The current
|
||||||
forward_batch=forward_batch,
|
# CP-shared prefetch pipeline is a target-model next-layer optimization;
|
||||||
metadata=metadata,
|
# keep it enabled for target extend/verify, but do not create draft
|
||||||
topk_transform_is_paged=(
|
# prefetchers until EAGLE gets an explicit same-layer prefetch contract.
|
||||||
topk_transform_method == TopkTransformMethod.PAGED
|
disable_draft_prefetch = cp_shared_kv_is_draft_input(forward_batch)
|
||||||
),
|
mla_prefetcher = (
|
||||||
|
None
|
||||||
|
if disable_draft_prefetch
|
||||||
|
else CpSharedKVMlaPrefetcher.maybe_create(
|
||||||
|
forward_batch=forward_batch,
|
||||||
|
metadata=metadata,
|
||||||
|
topk_transform_is_paged=(
|
||||||
|
topk_transform_method == TopkTransformMethod.PAGED
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
forward_batch.cp_shared_kv_mla_prefetcher = mla_prefetcher
|
forward_batch.cp_shared_kv_mla_prefetcher = mla_prefetcher
|
||||||
# Use one FIFO stream for index and MLA prefix prefetch. Both paths
|
# Use one FIFO stream for index and MLA prefix prefetch. Both paths
|
||||||
@@ -896,7 +909,9 @@ class NativeSparseAttnBackend(
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
forward_batch.cp_shared_kv_index_prefetcher = (
|
forward_batch.cp_shared_kv_index_prefetcher = (
|
||||||
CpSharedKVIndexPrefetcher.maybe_create(
|
None
|
||||||
|
if disable_draft_prefetch
|
||||||
|
else CpSharedKVIndexPrefetcher.maybe_create(
|
||||||
forward_batch=forward_batch,
|
forward_batch=forward_batch,
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
topk_transform_is_paged=(
|
topk_transform_is_paged=(
|
||||||
@@ -1726,8 +1741,7 @@ class NativeSparseAttnBackend(
|
|||||||
forward_batch, "cp_shared_kv_mla_prefetcher", None
|
forward_batch, "cp_shared_kv_mla_prefetcher", None
|
||||||
)
|
)
|
||||||
can_reuse_current_kv = (
|
can_reuse_current_kv = (
|
||||||
cp_shared_kv_current_reuse_enabled()
|
should_reuse_current_extend_kv(forward_batch)
|
||||||
and is_current_only_extend_batch(forward_batch)
|
|
||||||
and k is not None
|
and k is not None
|
||||||
and k_rope is not None
|
and k_rope is not None
|
||||||
and k.shape[0] == forward_batch.out_cache_loc.numel()
|
and k.shape[0] == forward_batch.out_cache_loc.numel()
|
||||||
@@ -1767,48 +1781,122 @@ class NativeSparseAttnBackend(
|
|||||||
else None,
|
else None,
|
||||||
)
|
)
|
||||||
if can_reuse_current_kv:
|
if can_reuse_current_kv:
|
||||||
|
current_kv_cache = _cat([k, k_rope], dim=-1)
|
||||||
logical_page_table_1 = page_table_1
|
logical_page_table_1 = page_table_1
|
||||||
current_remap_page_size = None
|
current_remap_page_size, current_remap_logical_page_capacity = (
|
||||||
current_remap_logical_page_capacity = None
|
current_loc_remap_fast_path_args(forward_batch)
|
||||||
if len(getattr(forward_batch, "extend_seq_lens_cpu", []) or []) == 1:
|
|
||||||
current_remap_page_size = forward_batch.token_to_kv_pool.page_size
|
|
||||||
current_remap_logical_page_capacity = (
|
|
||||||
max(
|
|
||||||
forward_batch.token_to_kv_pool.size
|
|
||||||
// current_remap_page_size
|
|
||||||
- 1,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
* forward_batch.cp_shared_kv_layout.cp_size
|
|
||||||
+ 1
|
|
||||||
)
|
|
||||||
current_mask, page_table_1 = build_current_loc_remap(
|
|
||||||
logical_page_table_1,
|
|
||||||
forward_batch.out_cache_loc,
|
|
||||||
page_size=current_remap_page_size,
|
|
||||||
logical_page_capacity=current_remap_logical_page_capacity,
|
|
||||||
)
|
)
|
||||||
if cp_shared_kv_debug_enabled():
|
|
||||||
missing_current = (logical_page_table_1 >= 0) & (~current_mask)
|
if is_current_only_extend_batch(forward_batch):
|
||||||
if torch.any(missing_current):
|
current_mask, page_table_1 = build_current_loc_remap(
|
||||||
bad_locs = logical_page_table_1[missing_current]
|
logical_page_table_1,
|
||||||
raise RuntimeError(
|
forward_batch.out_cache_loc,
|
||||||
"CP shared KV current MLA reuse expected current-only "
|
page_size=current_remap_page_size,
|
||||||
"logical locs but found history locs. "
|
logical_page_capacity=current_remap_logical_page_capacity,
|
||||||
f"bad_min={int(bad_locs.min().item())} "
|
|
||||||
f"bad_max={int(bad_locs.max().item())}"
|
|
||||||
)
|
|
||||||
cp_shared_kv_debug_log(
|
|
||||||
"mla_current_reuse",
|
|
||||||
"MLA current reuse cp_rank=%s layer=%s current_locs=%s remapped=%s kv_ck=%s rope_ck=%s",
|
|
||||||
forward_batch.cp_shared_kv_layout.cp_rank,
|
|
||||||
layer.layer_id,
|
|
||||||
tensor_debug_summary(forward_batch.out_cache_loc),
|
|
||||||
tensor_debug_summary(page_table_1),
|
|
||||||
tensor_debug_checksum(k),
|
|
||||||
tensor_debug_checksum(k_rope),
|
|
||||||
)
|
)
|
||||||
kv_cache = _cat([k, k_rope], dim=-1)
|
if cp_shared_kv_debug_enabled():
|
||||||
|
missing_current = (logical_page_table_1 >= 0) & (~current_mask)
|
||||||
|
if torch.any(missing_current):
|
||||||
|
bad_locs = logical_page_table_1[missing_current]
|
||||||
|
raise RuntimeError(
|
||||||
|
"CP shared KV current MLA reuse expected current-only "
|
||||||
|
"logical locs but found history locs. "
|
||||||
|
f"bad_min={int(bad_locs.min().item())} "
|
||||||
|
f"bad_max={int(bad_locs.max().item())}"
|
||||||
|
)
|
||||||
|
cp_shared_kv_debug_log(
|
||||||
|
"mla_current_reuse",
|
||||||
|
"MLA current reuse cp_rank=%s layer=%s current_locs=%s remapped=%s kv_ck=%s rope_ck=%s",
|
||||||
|
forward_batch.cp_shared_kv_layout.cp_rank,
|
||||||
|
layer.layer_id,
|
||||||
|
tensor_debug_summary(forward_batch.out_cache_loc),
|
||||||
|
tensor_debug_summary(page_table_1),
|
||||||
|
tensor_debug_checksum(k),
|
||||||
|
tensor_debug_checksum(k_rope),
|
||||||
|
)
|
||||||
|
kv_cache = current_kv_cache
|
||||||
|
else:
|
||||||
|
prefetched_kv = None
|
||||||
|
if mla_prefetcher is not None:
|
||||||
|
prefetched_kv = mla_prefetcher.consume_prefix_with_current(
|
||||||
|
layer_id=layer.layer_id,
|
||||||
|
kv_cache=kv_cache,
|
||||||
|
logical_locs=logical_page_table_1,
|
||||||
|
current_kv_cache=current_kv_cache,
|
||||||
|
current_locs=forward_batch.out_cache_loc,
|
||||||
|
current_remap_page_size=current_remap_page_size,
|
||||||
|
current_remap_logical_page_capacity=current_remap_logical_page_capacity,
|
||||||
|
)
|
||||||
|
if prefetched_kv is not None:
|
||||||
|
kv_cache, page_table_1 = prefetched_kv
|
||||||
|
else:
|
||||||
|
current_mask, _ = build_current_loc_remap(
|
||||||
|
logical_page_table_1,
|
||||||
|
forward_batch.out_cache_loc,
|
||||||
|
page_size=current_remap_page_size,
|
||||||
|
logical_page_capacity=current_remap_logical_page_capacity,
|
||||||
|
)
|
||||||
|
materialize_locs = torch.where(
|
||||||
|
current_mask,
|
||||||
|
torch.full_like(logical_page_table_1, -1),
|
||||||
|
logical_page_table_1,
|
||||||
|
)
|
||||||
|
prefix_kv_cache, prefix_dense_locs = (
|
||||||
|
materialize_shared_token_kv_buffer(
|
||||||
|
kv_cache=kv_cache,
|
||||||
|
logical_locs=materialize_locs,
|
||||||
|
layout=forward_batch.cp_shared_kv_layout,
|
||||||
|
page_size=forward_batch.token_to_kv_pool.page_size,
|
||||||
|
nvtx_source="mla.partial_current_materialize",
|
||||||
|
nvtx_layer_id=layer.layer_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
kv_cache, page_table_1, _ = merge_materialized_and_current_kv(
|
||||||
|
materialized_kv_cache=prefix_kv_cache,
|
||||||
|
materialized_dense_locs=prefix_dense_locs,
|
||||||
|
current_kv_cache=current_kv_cache,
|
||||||
|
logical_locs=logical_page_table_1,
|
||||||
|
current_locs=forward_batch.out_cache_loc,
|
||||||
|
page_size=current_remap_page_size,
|
||||||
|
logical_page_capacity=current_remap_logical_page_capacity,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
cp_shared_kv_mla_prefetch_log_enabled()
|
||||||
|
and cp_shared_kv_mla_prefetch_should_log_layer(layer.layer_id)
|
||||||
|
):
|
||||||
|
prefix_lens_cpu = getattr(
|
||||||
|
forward_batch, "extend_prefix_lens_cpu", None
|
||||||
|
)
|
||||||
|
extend_lens_cpu = getattr(
|
||||||
|
forward_batch, "extend_seq_lens_cpu", None
|
||||||
|
)
|
||||||
|
cp_shared_kv_mla_prefetch_log(
|
||||||
|
"forward_partial_current_reuse cp_rank=%s layer=%s used_prefetch=%s "
|
||||||
|
"prefix_lens=%s extend_lens=%s current_rows=%s kv_rows=%s page_table_shape=%s",
|
||||||
|
forward_batch.cp_shared_kv_layout.cp_rank,
|
||||||
|
layer.layer_id,
|
||||||
|
prefetched_kv is not None,
|
||||||
|
[int(x) for x in prefix_lens_cpu]
|
||||||
|
if prefix_lens_cpu is not None
|
||||||
|
else None,
|
||||||
|
[int(x) for x in extend_lens_cpu]
|
||||||
|
if extend_lens_cpu is not None
|
||||||
|
else None,
|
||||||
|
int(current_kv_cache.shape[0]),
|
||||||
|
int(kv_cache.shape[0]),
|
||||||
|
tuple(page_table_1.shape),
|
||||||
|
)
|
||||||
|
if cp_shared_kv_debug_enabled():
|
||||||
|
cp_shared_kv_debug_log(
|
||||||
|
"mla_partial_current_reuse",
|
||||||
|
"MLA partial current reuse cp_rank=%s layer=%s current_locs=%s remapped=%s kv_ck=%s rope_ck=%s",
|
||||||
|
forward_batch.cp_shared_kv_layout.cp_rank,
|
||||||
|
layer.layer_id,
|
||||||
|
tensor_debug_summary(forward_batch.out_cache_loc),
|
||||||
|
tensor_debug_summary(page_table_1),
|
||||||
|
tensor_debug_checksum(k),
|
||||||
|
tensor_debug_checksum(k_rope),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
prefetched_kv = None
|
prefetched_kv = None
|
||||||
if mla_prefetcher is not None:
|
if mla_prefetcher is not None:
|
||||||
@@ -1838,8 +1926,9 @@ class NativeSparseAttnBackend(
|
|||||||
nvtx_source="mla.full_materialize",
|
nvtx_source="mla.full_materialize",
|
||||||
nvtx_layer_id=layer.layer_id,
|
nvtx_layer_id=layer.layer_id,
|
||||||
)
|
)
|
||||||
|
if mla_prefetcher is not None and cp_shared_kv_should_prefetch_next_layer(
|
||||||
if mla_prefetcher is not None:
|
forward_batch, layer.layer_id
|
||||||
|
):
|
||||||
mla_prefetcher.start_next_layer_prefix(
|
mla_prefetcher.start_next_layer_prefix(
|
||||||
next_layer_id=layer.layer_id + 1,
|
next_layer_id=layer.layer_id + 1,
|
||||||
token_to_kv_pool=forward_batch.token_to_kv_pool,
|
token_to_kv_pool=forward_batch.token_to_kv_pool,
|
||||||
@@ -1852,7 +1941,6 @@ class NativeSparseAttnBackend(
|
|||||||
)
|
)
|
||||||
if index_prefetcher is not None:
|
if index_prefetcher is not None:
|
||||||
index_prefetcher.launch_pending_reduce()
|
index_prefetcher.launch_pending_reduce()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if nsa_impl == "tilelang":
|
if nsa_impl == "tilelang":
|
||||||
if q_rope is not None:
|
if q_rope is not None:
|
||||||
@@ -1950,7 +2038,6 @@ class NativeSparseAttnBackend(
|
|||||||
)
|
)
|
||||||
if index_prefetcher is not None:
|
if index_prefetcher is not None:
|
||||||
index_prefetcher.wait_attention_window()
|
index_prefetcher.wait_attention_window()
|
||||||
|
|
||||||
return attn_output
|
return attn_output
|
||||||
|
|
||||||
def forward_decode(
|
def forward_decode(
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ from sglang.srt.layers.attention.nsa.utils import (
|
|||||||
nsa_use_prefill_cp,
|
nsa_use_prefill_cp,
|
||||||
)
|
)
|
||||||
from sglang.srt.layers.attention.nsa.cp_shared_kv_runtime import (
|
from sglang.srt.layers.attention.nsa.cp_shared_kv_runtime import (
|
||||||
cp_shared_kv_current_reuse_enabled,
|
cp_shared_kv_should_prefetch_next_layer,
|
||||||
is_current_only_extend_batch,
|
should_reuse_current_extend_kv,
|
||||||
try_tai_fused_mla_store,
|
try_tai_fused_mla_store,
|
||||||
)
|
)
|
||||||
from sglang.srt.layers.communicator import get_attn_tp_context
|
from sglang.srt.layers.communicator import get_attn_tp_context
|
||||||
@@ -114,6 +114,11 @@ class DeepseekMLAForwardMixin:
|
|||||||
if token_to_kv_pool is None:
|
if token_to_kv_pool is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not cp_shared_kv_should_prefetch_next_layer(
|
||||||
|
forward_batch, self.layer_id
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
next_layer_id = int(self.layer_id) + 1
|
next_layer_id = int(self.layer_id) + 1
|
||||||
index_prefetcher = getattr(
|
index_prefetcher = getattr(
|
||||||
forward_batch, "cp_shared_kv_index_prefetcher", None
|
forward_batch, "cp_shared_kv_index_prefetcher", None
|
||||||
@@ -362,9 +367,8 @@ class DeepseekMLAForwardMixin:
|
|||||||
shared_mla_direct_write_done
|
shared_mla_direct_write_done
|
||||||
and getattr(forward_batch, "uses_cp_shared_kv", False)
|
and getattr(forward_batch, "uses_cp_shared_kv", False)
|
||||||
)
|
)
|
||||||
current_reuse_needs_full_current_kv = (
|
current_reuse_needs_full_current_kv = should_reuse_current_extend_kv(
|
||||||
cp_shared_kv_current_reuse_enabled()
|
forward_batch
|
||||||
and is_current_only_extend_batch(forward_batch)
|
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
not shared_kv_materialize_will_read_pool
|
not shared_kv_materialize_will_read_pool
|
||||||
|
|||||||
@@ -1680,14 +1680,28 @@ class DeepseekV2DecoderLayer(nn.Module):
|
|||||||
quant_format,
|
quant_format,
|
||||||
)
|
)
|
||||||
|
|
||||||
hidden_states = self.self_attn(
|
previous_cp_shared_kv_num_model_layers = getattr(
|
||||||
positions=positions,
|
forward_batch, "cp_shared_kv_num_model_layers", None
|
||||||
hidden_states=hidden_states,
|
|
||||||
forward_batch=forward_batch,
|
|
||||||
zero_allocator=zero_allocator,
|
|
||||||
llama_4_scaling=llama_4_scaling,
|
|
||||||
layer_scatter_modes=self.layer_scatter_modes,
|
|
||||||
)
|
)
|
||||||
|
forward_batch.cp_shared_kv_num_model_layers = (
|
||||||
|
1 if self.is_nextn else self.config.num_hidden_layers
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
hidden_states = self.self_attn(
|
||||||
|
positions=positions,
|
||||||
|
hidden_states=hidden_states,
|
||||||
|
forward_batch=forward_batch,
|
||||||
|
zero_allocator=zero_allocator,
|
||||||
|
llama_4_scaling=llama_4_scaling,
|
||||||
|
layer_scatter_modes=self.layer_scatter_modes,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
if previous_cp_shared_kv_num_model_layers is None:
|
||||||
|
delattr(forward_batch, "cp_shared_kv_num_model_layers")
|
||||||
|
else:
|
||||||
|
forward_batch.cp_shared_kv_num_model_layers = (
|
||||||
|
previous_cp_shared_kv_num_model_layers
|
||||||
|
)
|
||||||
|
|
||||||
hidden_states, residual = self.layer_communicator.prepare_mlp(
|
hidden_states, residual = self.layer_communicator.prepare_mlp(
|
||||||
hidden_states, residual, forward_batch
|
hidden_states, residual, forward_batch
|
||||||
|
|||||||
@@ -25,6 +25,14 @@ for _name in ("flash_attn_varlen_func", "flash_attn_with_kvcache"):
|
|||||||
if not hasattr(flash_attn_stub, _name):
|
if not hasattr(flash_attn_stub, _name):
|
||||||
setattr(flash_attn_stub, _name, lambda *args, **kwargs: None)
|
setattr(flash_attn_stub, _name, lambda *args, **kwargs: None)
|
||||||
|
|
||||||
|
sgl_kernel_stub = sys.modules.setdefault(
|
||||||
|
"sgl_kernel", types.ModuleType("sgl_kernel")
|
||||||
|
)
|
||||||
|
if not hasattr(sgl_kernel_stub, "__path__"):
|
||||||
|
sgl_kernel_stub.__path__ = []
|
||||||
|
if not hasattr(sgl_kernel_stub, "flash_attn"):
|
||||||
|
sgl_kernel_stub.flash_attn = flash_attn_stub
|
||||||
|
|
||||||
from sglang.test.ci.ci_register import register_cpu_ci
|
from sglang.test.ci.ci_register import register_cpu_ci
|
||||||
|
|
||||||
register_cpu_ci(est_time=1, suite="stage-a-test-cpu")
|
register_cpu_ci(est_time=1, suite="stage-a-test-cpu")
|
||||||
@@ -34,6 +42,11 @@ def _identity_all_reduce(buffer, *args, **kwargs):
|
|||||||
return buffer
|
return buffer
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeExtendForwardMode:
|
||||||
|
def is_extend_without_speculative(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
|
class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
|
||||||
def test_mla_prefetch_materializes_and_reduces_on_prefetch_stream(
|
def test_mla_prefetch_materializes_and_reduces_on_prefetch_stream(
|
||||||
self,
|
self,
|
||||||
@@ -396,10 +409,9 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
|
|||||||
from sglang.srt.layers.attention.nsa.cp_shared_kv_runtime import (
|
from sglang.srt.layers.attention.nsa.cp_shared_kv_runtime import (
|
||||||
is_current_only_extend_batch,
|
is_current_only_extend_batch,
|
||||||
)
|
)
|
||||||
from sglang.srt.model_executor.forward_batch_info import ForwardMode
|
|
||||||
|
|
||||||
forward_batch = SimpleNamespace(
|
forward_batch = SimpleNamespace(
|
||||||
forward_mode=ForwardMode.EXTEND,
|
forward_mode=_FakeExtendForwardMode(),
|
||||||
extend_prefix_lens_cpu=[0, 0],
|
extend_prefix_lens_cpu=[0, 0],
|
||||||
extend_seq_lens_cpu=[3, 5],
|
extend_seq_lens_cpu=[3, 5],
|
||||||
seq_lens_cpu=torch.tensor([3, 5], dtype=torch.int32),
|
seq_lens_cpu=torch.tensor([3, 5], dtype=torch.int32),
|
||||||
@@ -414,6 +426,299 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
|
|||||||
forward_batch.seq_lens_cpu = torch.tensor([4, 5], dtype=torch.int32)
|
forward_batch.seq_lens_cpu = torch.tensor([4, 5], dtype=torch.int32)
|
||||||
self.assertFalse(is_current_only_extend_batch(forward_batch))
|
self.assertFalse(is_current_only_extend_batch(forward_batch))
|
||||||
|
|
||||||
|
def test_can_reuse_current_extend_kv_allows_partial_cache_hit_single_batch(self):
|
||||||
|
from sglang.srt.layers.attention.nsa.cp_shared_kv_runtime import (
|
||||||
|
can_reuse_current_extend_kv,
|
||||||
|
)
|
||||||
|
|
||||||
|
forward_batch = SimpleNamespace(
|
||||||
|
forward_mode=_FakeExtendForwardMode(),
|
||||||
|
batch_size=1,
|
||||||
|
extend_seq_lens_cpu=[128],
|
||||||
|
seq_lens_cpu=torch.tensor([40384 + 128], dtype=torch.int32),
|
||||||
|
out_cache_loc=torch.arange(128, dtype=torch.int64),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(can_reuse_current_extend_kv(forward_batch))
|
||||||
|
|
||||||
|
forward_batch.batch_size = 2
|
||||||
|
self.assertFalse(can_reuse_current_extend_kv(forward_batch))
|
||||||
|
|
||||||
|
forward_batch.batch_size = 1
|
||||||
|
forward_batch.out_cache_loc = torch.arange(127, dtype=torch.int64)
|
||||||
|
self.assertFalse(can_reuse_current_extend_kv(forward_batch))
|
||||||
|
|
||||||
|
def test_should_reuse_current_extend_kv_disables_draft_cache_hit_suffix(self):
|
||||||
|
from sglang.srt.environ import envs
|
||||||
|
from sglang.srt.layers.attention.nsa import cp_shared_kv_runtime as runtime
|
||||||
|
|
||||||
|
class DraftSpecInfo:
|
||||||
|
def is_draft_input(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
class TargetSpecInfo:
|
||||||
|
def is_draft_input(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
runtime._CURRENT_REUSE_FALLBACK_LOG_COUNTS.clear()
|
||||||
|
forward_batch = SimpleNamespace(
|
||||||
|
forward_mode=_FakeExtendForwardMode(),
|
||||||
|
batch_size=1,
|
||||||
|
extend_prefix_lens_cpu=[40384],
|
||||||
|
extend_seq_lens_cpu=[56],
|
||||||
|
seq_lens_cpu=torch.tensor([40384 + 56], dtype=torch.int32),
|
||||||
|
out_cache_loc=torch.arange(56, dtype=torch.int64),
|
||||||
|
spec_info=DraftSpecInfo(),
|
||||||
|
)
|
||||||
|
|
||||||
|
with envs.SGLANG_CP_SHARED_KV_CURRENT_REUSE.override(True):
|
||||||
|
with self.assertLogs(runtime.logger.name, level="WARNING") as logs:
|
||||||
|
self.assertFalse(runtime.should_reuse_current_extend_kv(forward_batch))
|
||||||
|
self.assertTrue(
|
||||||
|
any(
|
||||||
|
"draft_partial_current_reuse" in message
|
||||||
|
for message in logs.output
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
forward_batch.spec_info = TargetSpecInfo()
|
||||||
|
self.assertTrue(runtime.should_reuse_current_extend_kv(forward_batch))
|
||||||
|
|
||||||
|
forward_batch.spec_info = DraftSpecInfo()
|
||||||
|
forward_batch.extend_prefix_lens_cpu = [0]
|
||||||
|
forward_batch.seq_lens_cpu = torch.tensor([56], dtype=torch.int32)
|
||||||
|
self.assertTrue(runtime.should_reuse_current_extend_kv(forward_batch))
|
||||||
|
|
||||||
|
def test_current_loc_remap_fast_path_args_only_for_current_only_extend(self):
|
||||||
|
from sglang.srt.layers.attention.nsa.cp_shared_kv_runtime import (
|
||||||
|
current_loc_remap_fast_path_args,
|
||||||
|
)
|
||||||
|
from sglang.srt.mem_cache.cp_shared_kv_layout import CpSharedKVLayout
|
||||||
|
|
||||||
|
forward_batch = SimpleNamespace(
|
||||||
|
forward_mode=_FakeExtendForwardMode(),
|
||||||
|
batch_size=1,
|
||||||
|
extend_prefix_lens_cpu=[0],
|
||||||
|
extend_seq_lens_cpu=[128],
|
||||||
|
seq_lens_cpu=torch.tensor([128], dtype=torch.int32),
|
||||||
|
out_cache_loc=torch.arange(128, dtype=torch.int64),
|
||||||
|
token_to_kv_pool=SimpleNamespace(page_size=64, size=4096),
|
||||||
|
cp_shared_kv_layout=CpSharedKVLayout(page_size=64, cp_size=8, cp_rank=0),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(current_loc_remap_fast_path_args(forward_batch), (64, 505))
|
||||||
|
|
||||||
|
forward_batch.extend_prefix_lens_cpu = [40389]
|
||||||
|
forward_batch.seq_lens_cpu = torch.tensor([40389 + 128], dtype=torch.int32)
|
||||||
|
self.assertEqual(current_loc_remap_fast_path_args(forward_batch), (None, None))
|
||||||
|
|
||||||
|
def test_merge_materialized_and_current_kv_remaps_only_current_locs(self):
|
||||||
|
from sglang.srt.layers.attention.nsa.cp_shared_kv_runtime import (
|
||||||
|
merge_materialized_and_current_kv,
|
||||||
|
)
|
||||||
|
|
||||||
|
materialized_kv = torch.arange(0, 8, dtype=torch.float32).view(8, 1, 1)
|
||||||
|
current_kv = torch.arange(100, 103, dtype=torch.float32).view(3, 1, 1)
|
||||||
|
logical_locs = torch.tensor([[4, 20, -1], [21, 7, 99]], dtype=torch.int32)
|
||||||
|
materialized_locs = torch.tensor([[4, -1, -1], [-1, 7, -1]], dtype=torch.int32)
|
||||||
|
current_locs = torch.tensor([20, 21, 22], dtype=torch.int64)
|
||||||
|
|
||||||
|
mixed_kv, mixed_locs, current_mask = merge_materialized_and_current_kv(
|
||||||
|
materialized_kv_cache=materialized_kv,
|
||||||
|
materialized_dense_locs=materialized_locs,
|
||||||
|
current_kv_cache=current_kv,
|
||||||
|
logical_locs=logical_locs,
|
||||||
|
current_locs=current_locs,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(torch.equal(mixed_kv[:8], materialized_kv))
|
||||||
|
self.assertTrue(torch.equal(mixed_kv[8:], current_kv))
|
||||||
|
self.assertEqual(
|
||||||
|
current_mask.tolist(),
|
||||||
|
[[False, True, False], [True, False, False]],
|
||||||
|
)
|
||||||
|
self.assertEqual(mixed_locs.tolist(), [[4, 8, -1], [9, 7, -1]])
|
||||||
|
|
||||||
|
def test_mla_prefetch_consume_prefix_with_current_skips_suffix_materialize(self):
|
||||||
|
from sglang.srt.layers.attention.nsa import cp_shared_kv_prefetch as prefetch
|
||||||
|
from sglang.srt.mem_cache.cp_shared_kv_layout import CpSharedKVLayout
|
||||||
|
|
||||||
|
class FakeCurrentStream:
|
||||||
|
def __init__(self):
|
||||||
|
self.events = []
|
||||||
|
|
||||||
|
def wait_event(self, event):
|
||||||
|
self.events.append(event)
|
||||||
|
|
||||||
|
current_stream = FakeCurrentStream()
|
||||||
|
fake_event = object()
|
||||||
|
dense_kv = torch.arange(0, 16, dtype=torch.float32).view(16, 1, 1)
|
||||||
|
current_kv = torch.arange(100, 102, dtype=torch.float32).view(2, 1, 1)
|
||||||
|
page_inverse = torch.tensor([0, 1, 2, -1, -1, 3], dtype=torch.int64)
|
||||||
|
prefetcher = prefetch.CpSharedKVMlaPrefetcher(
|
||||||
|
layout=CpSharedKVLayout(page_size=4, cp_size=2, cp_rank=0),
|
||||||
|
page_size=4,
|
||||||
|
prefix_pages=2,
|
||||||
|
slot_logical_pages=torch.tensor([1, 2, 5], dtype=torch.int64),
|
||||||
|
page_inverse=page_inverse,
|
||||||
|
dense_num_pages=4,
|
||||||
|
stream=object(),
|
||||||
|
)
|
||||||
|
handle = prefetch.CpSharedKVMlaPrefetchHandle(
|
||||||
|
layer_id=1,
|
||||||
|
dense_kv_cache=dense_kv,
|
||||||
|
prefix_rows=slice(4, 12),
|
||||||
|
event=fake_event,
|
||||||
|
)
|
||||||
|
prefetcher.handles[1] = handle
|
||||||
|
prefetcher.pending_attention_handle = handle
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
prefetch.torch.cuda, "current_stream", return_value=current_stream
|
||||||
|
), patch.object(
|
||||||
|
prefetch,
|
||||||
|
"materialize_local_token_kv_page_slots_into",
|
||||||
|
side_effect=AssertionError("suffix materialize must not run"),
|
||||||
|
):
|
||||||
|
mixed_kv, mixed_locs = prefetcher.consume_prefix_with_current(
|
||||||
|
layer_id=1,
|
||||||
|
kv_cache=torch.zeros((64, 1, 1), dtype=torch.float32),
|
||||||
|
logical_locs=torch.tensor([[4, 20], [21, 7]], dtype=torch.int32),
|
||||||
|
current_kv_cache=current_kv,
|
||||||
|
current_locs=torch.tensor([20, 21], dtype=torch.int64),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(current_stream.events, [fake_event])
|
||||||
|
self.assertEqual(prefetcher.handles, {})
|
||||||
|
self.assertIsNone(prefetcher.pending_attention_handle)
|
||||||
|
self.assertTrue(torch.equal(mixed_kv[:16], dense_kv))
|
||||||
|
self.assertTrue(torch.equal(mixed_kv[16:], current_kv))
|
||||||
|
self.assertEqual(mixed_locs.tolist(), [[4, 16], [17, 7]])
|
||||||
|
|
||||||
|
def test_mla_prefetch_attention_window_waits_on_pending_event(self):
|
||||||
|
from sglang.srt.layers.attention.nsa import cp_shared_kv_prefetch as prefetch
|
||||||
|
from sglang.srt.mem_cache.cp_shared_kv_layout import CpSharedKVLayout
|
||||||
|
|
||||||
|
class FakeCurrentStream:
|
||||||
|
def __init__(self):
|
||||||
|
self.events = []
|
||||||
|
|
||||||
|
def wait_event(self, event):
|
||||||
|
self.events.append(event)
|
||||||
|
|
||||||
|
current_stream = FakeCurrentStream()
|
||||||
|
fake_event = object()
|
||||||
|
prefetcher = prefetch.CpSharedKVMlaPrefetcher(
|
||||||
|
layout=CpSharedKVLayout(page_size=4, cp_size=2, cp_rank=0),
|
||||||
|
page_size=4,
|
||||||
|
prefix_pages=2,
|
||||||
|
slot_logical_pages=torch.tensor([1, 2, 5], dtype=torch.int64),
|
||||||
|
page_inverse=torch.tensor([0, 1, 2, -1, -1, 3], dtype=torch.int64),
|
||||||
|
dense_num_pages=4,
|
||||||
|
stream=object(),
|
||||||
|
)
|
||||||
|
handle = prefetch.CpSharedKVMlaPrefetchHandle(
|
||||||
|
layer_id=1,
|
||||||
|
dense_kv_cache=torch.zeros((16, 1, 1), dtype=torch.float32),
|
||||||
|
prefix_rows=slice(4, 12),
|
||||||
|
event=fake_event,
|
||||||
|
)
|
||||||
|
prefetcher.handles[1] = handle
|
||||||
|
prefetcher.pending_attention_handle = handle
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
prefetch.torch.cuda, "current_stream", return_value=current_stream
|
||||||
|
):
|
||||||
|
prefetcher.wait_attention_window()
|
||||||
|
|
||||||
|
self.assertEqual(current_stream.events, [fake_event])
|
||||||
|
self.assertIsNone(prefetcher.pending_attention_handle)
|
||||||
|
self.assertIs(prefetcher.handles[1], handle)
|
||||||
|
|
||||||
|
def test_mla_prefetch_attention_window_launches_pending_reduce_before_wait(self):
|
||||||
|
from sglang.srt.layers.attention.nsa import cp_shared_kv_prefetch as prefetch
|
||||||
|
from sglang.srt.mem_cache.cp_shared_kv_layout import CpSharedKVLayout
|
||||||
|
|
||||||
|
class FakeCurrentStream:
|
||||||
|
def __init__(self):
|
||||||
|
self.events = []
|
||||||
|
|
||||||
|
def wait_event(self, event):
|
||||||
|
self.events.append(event)
|
||||||
|
|
||||||
|
current_stream = FakeCurrentStream()
|
||||||
|
fake_event = object()
|
||||||
|
prefetcher = prefetch.CpSharedKVMlaPrefetcher(
|
||||||
|
layout=CpSharedKVLayout(page_size=4, cp_size=2, cp_rank=0),
|
||||||
|
page_size=4,
|
||||||
|
prefix_pages=2,
|
||||||
|
slot_logical_pages=torch.tensor([1, 2, 5], dtype=torch.int64),
|
||||||
|
page_inverse=torch.tensor([0, 1, 2, -1, -1, 3], dtype=torch.int64),
|
||||||
|
dense_num_pages=4,
|
||||||
|
stream=object(),
|
||||||
|
)
|
||||||
|
handle = prefetch.CpSharedKVMlaPrefetchHandle(
|
||||||
|
layer_id=1,
|
||||||
|
dense_kv_cache=torch.zeros((16, 1, 1), dtype=torch.float32),
|
||||||
|
prefix_rows=slice(4, 12),
|
||||||
|
event=None,
|
||||||
|
)
|
||||||
|
prefetcher.handles[1] = handle
|
||||||
|
prefetcher.pending_attention_handle = handle
|
||||||
|
|
||||||
|
def finish_reduce():
|
||||||
|
handle.event = fake_event
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
prefetch.torch.cuda, "current_stream", return_value=current_stream
|
||||||
|
), patch.object(
|
||||||
|
prefetcher, "launch_pending_reduce", side_effect=finish_reduce
|
||||||
|
) as launch_pending_reduce:
|
||||||
|
prefetcher.wait_attention_window()
|
||||||
|
|
||||||
|
launch_pending_reduce.assert_called_once_with()
|
||||||
|
self.assertEqual(current_stream.events, [fake_event])
|
||||||
|
self.assertIsNone(prefetcher.pending_attention_handle)
|
||||||
|
|
||||||
|
def test_index_prefetch_attention_window_waits_on_pending_event(self):
|
||||||
|
from sglang.srt.layers.attention.nsa import cp_shared_kv_prefetch as prefetch
|
||||||
|
from sglang.srt.mem_cache.cp_shared_kv_layout import CpSharedKVLayout
|
||||||
|
|
||||||
|
class FakeCurrentStream:
|
||||||
|
def __init__(self):
|
||||||
|
self.events = []
|
||||||
|
|
||||||
|
def wait_event(self, event):
|
||||||
|
self.events.append(event)
|
||||||
|
|
||||||
|
current_stream = FakeCurrentStream()
|
||||||
|
fake_event = object()
|
||||||
|
prefetcher = prefetch.CpSharedKVIndexPrefetcher(
|
||||||
|
layout=CpSharedKVLayout(page_size=4, cp_size=2, cp_rank=0),
|
||||||
|
prefix_pages=2,
|
||||||
|
slot_logical_pages=torch.tensor([1, 2, 5], dtype=torch.int64),
|
||||||
|
page_inverse=torch.tensor([0, 1, 2, -1, -1, 3], dtype=torch.int64),
|
||||||
|
dense_num_pages=4,
|
||||||
|
stream=object(),
|
||||||
|
)
|
||||||
|
handle = prefetch.CpSharedKVIndexPrefetchHandle(
|
||||||
|
layer_id=1,
|
||||||
|
dense_page_buffer=torch.zeros((4, 3), dtype=torch.uint8),
|
||||||
|
prefix_rows=slice(1, 3),
|
||||||
|
event=fake_event,
|
||||||
|
)
|
||||||
|
prefetcher.handles[1] = handle
|
||||||
|
prefetcher.pending_attention_handle = handle
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
prefetch.torch.cuda, "current_stream", return_value=current_stream
|
||||||
|
):
|
||||||
|
prefetcher.wait_attention_window()
|
||||||
|
|
||||||
|
self.assertEqual(current_stream.events, [fake_event])
|
||||||
|
self.assertIsNone(prefetcher.pending_attention_handle)
|
||||||
|
self.assertIs(prefetcher.handles[1], handle)
|
||||||
|
|
||||||
def test_materialize_local_token_kv_pages(self):
|
def test_materialize_local_token_kv_pages(self):
|
||||||
from sglang.srt.layers.attention.nsa.cp_shared_kv_runtime import (
|
from sglang.srt.layers.attention.nsa.cp_shared_kv_runtime import (
|
||||||
build_dense_page_remap,
|
build_dense_page_remap,
|
||||||
@@ -718,14 +1023,17 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
|
|||||||
with envs.SGLANG_CP_SHARED_KV_LOG_MLA_PREFETCH.override(True):
|
with envs.SGLANG_CP_SHARED_KV_LOG_MLA_PREFETCH.override(True):
|
||||||
self.assertTrue(cp_shared_kv_mla_prefetch_log_enabled())
|
self.assertTrue(cp_shared_kv_mla_prefetch_log_enabled())
|
||||||
|
|
||||||
def test_mla_prefetch_min_prefix_pages_defaults_to_1k_tokens_and_can_override(self):
|
def test_mla_prefetch_min_prefix_pages_uses_cached_token_default_and_can_override(self):
|
||||||
from sglang.srt.environ import envs
|
from sglang.srt.environ import envs
|
||||||
from sglang.srt.layers.attention.nsa import cp_shared_kv_runtime as runtime
|
from sglang.srt.layers.attention.nsa import cp_shared_kv_runtime as runtime
|
||||||
|
|
||||||
envs.SGLANG_CP_SHARED_KV_MLA_PREFETCH_MIN_PREFIX_PAGES.clear()
|
envs.SGLANG_CP_SHARED_KV_MLA_PREFETCH_MIN_PREFIX_PAGES.clear()
|
||||||
self.assertEqual(runtime._MLA_PREFETCH_DEFAULT_MIN_PREFIX_TOKENS, 1024)
|
default_tokens = envs.SGLANG_CP_SHARED_KV_MLA_PREFETCH_MIN_PREFIX_TOKENS.get()
|
||||||
|
self.assertEqual(runtime._MLA_PREFETCH_DEFAULT_MIN_PREFIX_TOKENS, default_tokens)
|
||||||
|
expected_pages = (default_tokens + 63) // 64
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
runtime.cp_shared_kv_mla_prefetch_min_prefix_pages(8, page_size=64), 16
|
runtime.cp_shared_kv_mla_prefetch_min_prefix_pages(8, page_size=64),
|
||||||
|
max(8, expected_pages),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
runtime.cp_shared_kv_mla_prefetch_min_prefix_pages(32, page_size=64), 32
|
runtime.cp_shared_kv_mla_prefetch_min_prefix_pages(32, page_size=64), 32
|
||||||
@@ -1823,6 +2131,50 @@ class TestCpSharedKVTaiMaterializeIntegration(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(fake_prefetcher.calls, [(12, token_to_kv_pool)])
|
self.assertEqual(fake_prefetcher.calls, [(12, token_to_kv_pool)])
|
||||||
|
|
||||||
|
def test_index_prefetch_skips_when_current_layer_is_last(self):
|
||||||
|
from sglang.srt.layers.attention.nsa import nsa_indexer
|
||||||
|
from sglang.srt.layers.attention.nsa.cp_shared_kv_runtime import (
|
||||||
|
cp_shared_kv_should_prefetch_next_layer,
|
||||||
|
)
|
||||||
|
|
||||||
|
class FakePrefetcher:
|
||||||
|
def __init__(self):
|
||||||
|
self.calls = []
|
||||||
|
|
||||||
|
def start_next_layer_prefix(self, *, next_layer_id, token_to_kv_pool):
|
||||||
|
self.calls.append((next_layer_id, token_to_kv_pool))
|
||||||
|
|
||||||
|
token_to_kv_pool = object()
|
||||||
|
fake_prefetcher = FakePrefetcher()
|
||||||
|
forward_batch = SimpleNamespace(
|
||||||
|
token_to_kv_pool=token_to_kv_pool,
|
||||||
|
cp_shared_kv_index_prefetcher=fake_prefetcher,
|
||||||
|
cp_shared_kv_num_model_layers=12,
|
||||||
|
)
|
||||||
|
indexer = object.__new__(nsa_indexer.Indexer)
|
||||||
|
|
||||||
|
self.assertFalse(cp_shared_kv_should_prefetch_next_layer(forward_batch, 11))
|
||||||
|
indexer._maybe_start_next_layer_index_prefetch(forward_batch, layer_id=11)
|
||||||
|
|
||||||
|
self.assertEqual(fake_prefetcher.calls, [])
|
||||||
|
|
||||||
|
def test_index_prefetch_skips_eagle_draft_next_layer(self):
|
||||||
|
from sglang.srt.layers.attention.nsa.cp_shared_kv_runtime import (
|
||||||
|
cp_shared_kv_is_draft_input,
|
||||||
|
cp_shared_kv_should_prefetch_next_layer,
|
||||||
|
)
|
||||||
|
|
||||||
|
class FakeSpecInfo:
|
||||||
|
def is_draft_input(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
forward_batch = SimpleNamespace(
|
||||||
|
spec_info=FakeSpecInfo(),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(cp_shared_kv_is_draft_input(forward_batch))
|
||||||
|
self.assertFalse(cp_shared_kv_should_prefetch_next_layer(forward_batch, 0))
|
||||||
|
|
||||||
def test_index_prefetch_consume_miss_logs_fallback_after_first_layer(self):
|
def test_index_prefetch_consume_miss_logs_fallback_after_first_layer(self):
|
||||||
from sglang.srt.layers.attention.nsa import nsa_indexer
|
from sglang.srt.layers.attention.nsa import nsa_indexer
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user