Keep CP HiCache reuse page-safe without eviction log churn

Page-aligned CP shared KV can pad out_cache_loc beyond valid current rows, so current reuse now gates MLA composition on the valid extend rows and permits draft partial-current reuse once the TAI sparse-page capability check passes. The TAI current-slot path self-tests sparse pages before use and falls back to the torch reference when the installed kernel is stale.

Eviction success and no-op diagnostics were also moved from INFO to DEBUG so owner-lane and host-admission churn does not flood production logs; true write failures remain WARNING.

Constraint: CP shared KV uses page-aligned physical reservations where valid suffix rows can be shorter than padded out_cache_loc.

Constraint: Production failure/fallback logs must remain visible, but hot successful eviction paths should not emit INFO per victim/rank.

Rejected: Keep draft partial-current reuse disabled | would preserve avoidable full materialization on draft cache-hit suffixes.

Rejected: Trust the TAI current-slot kernel unconditionally | stale kernels can corrupt sparse current-page composition.

Confidence: medium

Scope-risk: moderate

Directive: Do not reintroduce INFO logging in eviction hot paths without rate limiting and runtime evidence.

Tested: local py_compile for touched Python files

Tested: local git diff --check

Tested: remote container py_compile for touched Python files

Tested: remote PYTHONPATH=python python -m pytest -q test/registered/unit/mem_cache/test_cp_hicache_metadata.py::TestHiCacheEvictLoggingLevels::test_evict_hot_path_success_logs_are_debug_only test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py -> 82 passed, 5 warnings, 2 subtests passed

Not-tested: full ETE traffic after this commit; draft partial-current accept length still needs user-driven runtime validation
This commit is contained in:
laoyao0822
2026-05-31 03:31:06 +08:00
parent 3c14b1f127
commit 0fc95b6439
7 changed files with 704 additions and 119 deletions
@@ -3365,3 +3365,251 @@ Verification on g0034 container:
Remaining risk:
- This makes the all-layer direct/page-first backup API submit one TAI per-layer memcpy-batch op instead of one stale sgl-kernel all-layer op. It is correct and CUDA-13-safe, but it can increase CPU submit count on all-layer backup paths. Production forward-overlap paths already operate per-layer, so this is acceptable for the current branch goal.
### C81 — 2026-05-31 correction: do not reopen draft partial-current reuse before fixing root cause
Correction:
- Draft partial-current reuse must not be reopened merely by deleting the
`draft_partial_current_reuse_disabled` guard.
- The observed accept-length regression must be fixed first, then the default
can be reopened without an env gate.
- Until that root cause is fixed and verified, the safe contract remains:
draft current-only reuse is allowed, draft cache-hit partial-current reuse is
disabled with the explicit `draft_partial_current_reuse_disabled` warning.
Current known facts:
- Draft HiCache L2(host)→L1(device) load-back exists via `CacheController.load_cp()`
and `CacheController.start_loading()` using `draft_load_queue` and
`draft_mem_pool_host.load_to_device_per_layer(...)` when `has_draft_hicache` is
true.
- That L2→L1 load-back is separate from the CP shared-KV MLA/index next-layer
prefetcher; the latter is still target-only because EAGLE/NextN has one
executable draft layer.
Next target:
- Audit the draft partial-current compose path (`nsa_backend.py` plus
`materialize_prefix_and_reuse_current_kv_page_slots`) for target/draft layout,
layer-id, current-suffix placement, and page-slot mapping mismatches before
reopening the guard.
### C82 — 2026-05-31 fixed-before-open path for draft partial-current reuse
Root-cause guard added before reopening draft partial-current reuse:
- The risky failure mode was not the draft predicate itself; it was accepting a
stale TAI current-slot fill kernel whose masking logic assumed the current
suffix covered a dense page range from first current page to last current page.
- For sparse current pages, that stale kernel could mask unrelated prefix pages
as suffix slack, corrupting partial-current compose while tensor shapes stayed
valid.
- `cp_shared_kv_runtime.py` now validates the installed TAI current-slot fill
kernel once per CUDA device with a sparse-page self-test before using it. If
the self-test fails, the TAI result is not used and the code falls back to the
torch reference path, which preserves correctness.
After this capability fix:
- Draft cache-hit partial-current reuse is reopened directly, without an env
gate.
- Draft next-layer MLA/index prefetcher creation remains disabled; draft has one
executable layer, so a future draft prefetch optimization should use an
explicit same-layer contract.
Verification:
- Remote RED before runtime helper existed:
`test_tai_current_slot_fill_is_skipped_when_sparse_page_self_test_fails`
failed with missing `_tai_current_slot_fill_supports_sparse_pages`.
- Remote GREEN after runtime helper:
`test_tai_current_slot_fill_is_skipped_when_sparse_page_self_test_fails` → pass.
- Remote installed-kernel evidence:
`test_tai_current_slot_fill_sparse_page_self_test_passes_on_installed_kernel`
→ pass.
- Remote tai-kernel sparse-page tests:
`test_fill_current_token_kv_page_slots_handles_sparse_current_pages`,
`test_fill_current_token_kv_page_slots_handles_nonzero_first_page_offset`, and
`test_fill_current_token_kv_page_slots_masks_current_page_slack` → `3 passed`.
Remaining risk:
- Need ETE run with draft `branch=partial_current_sync` active to confirm decode
aggregate accept remains healthy. The code-level stale-kernel corruption path
is now blocked, but runtime traffic is still required before treating this as
final performance/correctness validation.
### C83 — 2026-05-31 Mooncake P2P handshake receives HTTP traffic on port 17000
Observed remote log:
- `readString: too large length from socket: 3564087971010531152`
- `SocketHandShakePlugin: failed to receive handshake message, malformed json`
Root-cause evidence:
- `3564087971010531152 == 0x31762f2054534f50`, little-endian bytes are
`POST /v1`.
- Tcpdump on g0034 captured the offending packet:
`10.20.34.3:<ephemeral> > 10.20.32.34:17000` with HTTP payload
`POST /v1/chat/completions HTTP/1.1` and `Host: 10.20.32.34:17000`.
- In the same run, Mooncake P2P handshake selected port `17000` for one prefill
scheduler process:
`Transfer Engine RPC using P2P handshake, listening on 10.20.32.34:17000`.
- The errors repeat every ~30 seconds and start before request traffic while the
model is still loading, so this is not caused by HiCache per-layer backup
payload corruption.
Current conclusion:
- This is a port/protocol collision: an external HTTP client/prober is sending
OpenAI HTTP traffic to a Mooncake metadata/P2P handshake listener.
- It is noisy and can mask real transfer errors, but it is not evidence that
HiCache write/read payloads are corrupted.
Decision:
- Do not keep the temporary SGLang-side Mooncake port-range override.
- The code change that set `MC_MAX_PRC_PORT=16999` by default was reverted on
2026-05-31 at user request.
- If this noise matters operationally, fix the external endpoint/prober or set
Mooncake `MC_MIN_PRC_PORT`/`MC_MAX_PRC_PORT` explicitly in the launch
environment instead of silently changing SGLang defaults.
### C84 — 2026-05-31 draft partial-current reuse also requires valid-row, not padded-loc, MLA gating
Finding:
- Reopening draft partial-current reuse still was not enough when `out_cache_loc`
was page padded. The MLA gate in `nsa_backend.py` required
`k.shape[0] == forward_batch.out_cache_loc.numel()` and the same for
`k_rope`.
- Under the page-aligned cache contract, `out_cache_loc` may cover the padded
physical page extent while MLA projection tensors only contain the valid
current suffix rows from `extend_seq_lens_cpu`.
- That strict equality silently forces cache-hit draft suffixes (for example
`extend_len=65`, padded locs `128`) away from partial-current reuse and back
toward full materialization.
Fix direction:
- Validate current MLA tensors against valid current rows, not padded loc count.
- Slice `k`, `k_rope`, and `out_cache_loc` to `extend_seq_lens_cpu[0]` before
composing current rows. Padded tail locs are physical reservation/slack, not
visible KV rows.
- Keep draft next-layer MLA/index prefetcher disabled; this is only restoring the
synchronous partial-current compose path for draft cache-hit suffixes.
Verification added:
- RED on remote before implementation:
`test_current_extend_kv_rows_for_reuse_accepts_padded_out_cache_loc` failed
because `current_extend_kv_rows_for_reuse` did not exist.
- RED on remote before implementation:
`test_mla_current_reuse_gate_accepts_padded_out_cache_loc` found the stale
`k.shape[0] == forward_batch.out_cache_loc.numel()` gate.
Remaining runtime check:
- Remote GREEN after implementation:
`test_should_reuse_current_extend_kv_enables_draft_partial_cache_hit_suffix`,
`test_current_extend_kv_rows_for_reuse_accepts_padded_out_cache_loc`,
`test_mla_current_reuse_gate_accepts_padded_out_cache_loc`,
`test_tai_current_slot_fill_is_skipped_when_sparse_page_self_test_fails`, and
`test_tai_current_slot_fill_sparse_page_self_test_passes_on_installed_kernel`
→ `5 passed`.
- Remote broader runtime helper suite:
`PYTHONPATH=python python -m pytest -q test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py`
→ `81 passed, 5 warnings, 2 subtests passed`.
- Still requires user-driven ETE traffic to verify that draft `partial_current_sync`
appears without accept-length collapse.
### C85 — 2026-05-31 CP HiCache eviction path and overhead findings
Code path checked:
- Extend allocation under CP shared KV uses compute-owner page allocation in
`alloc_paged_token_slots_extend()`.
- If owner-lane allocation fails, `_evict_for_compute_owner_lanes()` computes
per-owner deficits and calls `tree_cache.evict(EvictParams(...,
owner_lane_deficits=...))`.
- CP HiCache routes that to `_evict_cp_owner_lane_deficit_nodes()`, which plans
victim nodes by owner-lane contribution and frees backed GPU nodes through
`_evict_backuped()` -> `CacheController.evict_device()` ->
`mem_pool_device_allocator.free()`.
- Load-back can also evict device nodes through `_evict_cp_load_back_owner_lanes()`.
- Host write admission evicts host nodes through
`_reserve_write_cp_indices_no_collective()` ->
`_evict_cp_host_for_write_admission()` -> `CacheController.evict_cp_host()`.
Remote log evidence from `/mnt/beegfs/cjy/log/sglang_cp_hicache_20260530_184620.log`:
- `MemCache-evict _evict_for_compute_owner_lanes`: 3224 attempt lines across 8
ranks, about 403 logical eviction cycles in ~18 minutes.
- Average owner-lane request was ~18.5k tokens; average actual evicted was ~38k
tokens, i.e. about ~19.5k token overshoot per rank-line.
- Small requests were common: 1336 owner-lane evict lines requested less than
4096 tokens, but eviction can still remove an entire victim node.
- Host write-admission eviction appeared too: 1088 lines, about 136 logical
host eviction cycles. Average local host freed was ~4.7k token slots.
Current conclusion:
- Eviction does not copy KV for already-backed nodes; it mainly frees allocator
slots and updates radix/HiCache metadata. It should not consume PCIe bandwidth.
- It can still be CPU/GPU-scheduler expensive because it is synchronous on the
scheduler path, scans/heapifies evictable leaves, emits many INFO logs, and
`PagedTokenToKVPoolAllocator.free()` uses `torch.unique(free_index // page_size)`
per victim.
- Per-owner capacity stats also do CUDA tensor reductions with `.item()` per
owner lane, so failed owner-lane allocations can introduce host syncs.
- The large overshoot is structural: eviction frees whole radix nodes, not the
exact deficit. This can cause L1 churn even after removing the older `* cp_size`
multiplier.
Candidate optimizations to consider:
1. Rate-limit or demote hot-path INFO eviction logs; current log volume alone is
large enough to affect performance during heavy churn.
2. Add a page-aligned fast path in `PagedTokenToKVPoolAllocator.free()` to use
`free_index[::page_size] // page_size` when inputs are page-shaped, avoiding
`torch.unique` for normal HiCache node eviction.
3. Cache/maintain owner-lane free counts incrementally in the CP allocator to
avoid repeated `.sum().item()` reductions during owner-lane admission.
4. Improve victim selection to reduce overshoot for small deficits, or keep a
small-node victim pool for small owner-lane shortages.
### C86 — 2026-05-31 Eviction hot-path logs demoted to DEBUG
User decision:
- Keep failure/fallback logs visible.
- Demote successful/no-op eviction hot-path diagnostics from INFO to DEBUG.
Implemented scope:
- `mem_cache/common.py`
- `evict_from_tree_cache()` capacity-trigger message is DEBUG.
- `_evict_for_compute_owner_lanes()` no-evictable, attempt, and result messages are DEBUG.
- `mem_cache/hiradix_cache.py`
- CP load-back owner-lane eviction summary is DEBUG.
- CP owner-lane no-victim / stale-victim / END summaries are DEBUG.
- Deterministic CP host eviction success summary is DEBUG.
- Generic `evict START/END`, `_evict_backuped`, `_evict_regular`, and host-slot eviction start are DEBUG.
- `write_backup CP retry after deterministic host eviction` is DEBUG.
- `write_backup CP FAILED after deterministic retry` is promoted to WARNING so true failures remain visible.
Not changed:
- Existing `warning`/`error` fallback, capacity failure, OOM, pin-release, and plan-divergence logs remain visible.
- Eviction semantics are unchanged; this only changes log levels.
Verification:
- Added `TestHiCacheEvictLoggingLevels::test_evict_hot_path_success_logs_are_debug_only`.
- RED on remote before implementation: failed because `MemCache-evict` success-path logs still used `logger.info`.
- GREEN on remote after implementation:
`PYTHONPATH=python python -m pytest -q test/registered/unit/mem_cache/test_cp_hicache_metadata.py::TestHiCacheEvictLoggingLevels::test_evict_hot_path_success_logs_are_debug_only`
→ `1 passed`.
- Local and remote `py_compile` passed for touched Python files.
@@ -9,7 +9,9 @@ from typing import Any
import torch
from sglang.srt.environ import envs
from sglang.srt.layers.attention.nsa.utils import log_cp_draft_shared_kv_debug # noqa: F401
from sglang.srt.layers.attention.nsa.utils import (
log_cp_draft_shared_kv_debug,
) # noqa: F401
from sglang.srt.layers.dp_attention import get_attention_cp_group
from sglang.srt.mem_cache.cp_shared_kv_layout import CpSharedKVLayout
@@ -85,11 +87,7 @@ def cp_shared_kv_mla_prefetch_min_prefix_pages(
if page_size is not None and int(page_size) > 0:
min_pages = max(
min_pages,
(
_MLA_PREFETCH_DEFAULT_MIN_PREFIX_TOKENS
+ int(page_size)
- 1
)
(_MLA_PREFETCH_DEFAULT_MIN_PREFIX_TOKENS + int(page_size) - 1)
// int(page_size),
)
return min_pages
@@ -302,6 +300,110 @@ def _load_tai_materialize_kernels():
return None
@lru_cache(maxsize=16)
def _tai_current_slot_fill_sparse_pages_self_test(
device_type: str,
device_index: int | None,
) -> bool:
"""Return whether TAI current-slot fill handles sparse current pages.
Older tai-kernel builds filled current rows correctly but masked every page
between first and last current page as current-page slack. That silently
hid unrelated prefix pages for cache-hit suffixes and was enough to corrupt
CP shared-KV reuse. Validate the installed kernel once before allowing it
on the hot path.
"""
if device_type != "cuda":
return True
if not torch.cuda.is_available():
return False
kernels = _load_tai_materialize_kernels()
fill_kernel = (
getattr(kernels, "fill_current_token_kv_page_slots_and_remap_locs", None)
if kernels is not None
else None
)
if fill_kernel is None:
return False
device = torch.device(device_type, device_index)
try:
page_size = 4
dense_kv = torch.zeros((16, 1), device=device, dtype=torch.float32)
materialized_locs = torch.tensor(
[[4, 5, 8, 9, 12, 13, 14, 15]],
device=device,
dtype=torch.int64,
)
current_kv = torch.tensor(
[[10.0], [11.0], [12.0], [13.0]],
device=device,
dtype=torch.float32,
)
logical_locs = torch.tensor(
[[20, 21, 40, 41, 100, 101, 102, 103]],
device=device,
dtype=torch.int64,
)
current_locs = torch.tensor(
[20, 21, 100, 101],
device=device,
dtype=torch.int64,
)
page_inverse = torch.full((32,), -1, device=device, dtype=torch.long)
page_inverse[0] = 0
page_inverse[5] = 1
page_inverse[10] = 2
page_inverse[25] = 3
mixed_kv, mixed_locs, current_mask = fill_kernel(
dense_kv,
materialized_locs,
current_kv,
logical_locs,
current_locs,
page_inverse,
page_size=page_size,
mask_non_current_in_current_pages=True,
)
expected_locs = torch.tensor(
[[4, 5, 8, 9, 12, 13, -1, -1]],
device=device,
dtype=torch.int64,
)
expected_mask = torch.tensor(
[[True, True, False, False, True, True, False, False]],
device=device,
dtype=torch.bool,
)
expected_kv = torch.zeros_like(dense_kv)
expected_kv[4:6] = current_kv[0:2]
expected_kv[12:14] = current_kv[2:4]
return bool(
torch.equal(mixed_locs, expected_locs)
and torch.equal(current_mask, expected_mask)
and torch.equal(mixed_kv, expected_kv)
)
except Exception as exc:
_log_tai_materialize_fallback(
"fill_current_sparse_page_self_test_failed",
"CP shared KV tai current-slot fill sparse-page self-test failed; "
"falling back to torch reference. error=%s",
exc,
limit=1,
)
return False
def _tai_current_slot_fill_supports_sparse_pages(device: torch.device) -> bool:
return _tai_current_slot_fill_sparse_pages_self_test(
device.type,
device.index,
)
def _tai_materialize_runtime_enabled() -> bool:
# Keep the debug path on the existing torch implementation. The debug path
# intentionally preserves tensor summaries and value assertions used for
@@ -719,6 +821,22 @@ def _try_tai_fill_current_kv_page_slots_and_remap_locs(
)
return None
if dense_kv_cache.is_cuda and not _tai_current_slot_fill_supports_sparse_pages(
dense_kv_cache.device
):
_log_tai_materialize_fallback(
"fill_current_sparse_page_unsupported",
"CP shared KV tai current-slot fill kernel failed the sparse-page "
"capability check; falling back to torch reference to avoid hiding "
"prefix pages in partial-current reuse. page_size=%s current_rows=%s "
"query_locs=%s",
page_size,
int(current_kv_cache.shape[0]),
int(logical_locs.numel()),
limit=1,
)
return None
try:
return fill_kernel(
_contiguous_for_tai(dense_kv_cache),
@@ -866,8 +984,10 @@ def fill_current_index_page_slots(
)
current_pages = torch.div(current_locs, page_size, rounding_mode="floor")
valid_pages = (current_locs >= 0) & (current_pages >= 0) & (
current_pages < int(page_inverse.numel())
valid_pages = (
(current_locs >= 0)
& (current_pages >= 0)
& (current_pages < int(page_inverse.numel()))
)
safe_pages = torch.clamp(
current_pages,
@@ -1075,12 +1195,12 @@ def can_reuse_current_extend_kv(forward_batch) -> bool:
def should_reuse_current_extend_kv(forward_batch) -> bool:
"""Return whether MLA should splice current extend KV into materialized KV.
Current-only reuse is safe for both target and draft because there is no
cached prefix to compose. Partial current reuse is currently a target-model
contract only. EAGLE/NextN draft cache-hit suffixes keep using the older
full-materialize path until the draft splice path has value-level ETE proof;
the 2026-05-30 accept-length regression correlated with enabling that draft
partial splice path.
Current-only and partial-current reuse are both cache-layout operations: the
cached page-aligned prefix is materialized from the shared KV cache and the
current valid suffix is spliced from ``out_cache_loc``. Draft/NextN uses the
same CP shared-KV layout contract as the target model. The stale TAI
sparse-current-page corruption that made this unsafe is blocked at the
current-slot fill capability check before any TAI result is used.
"""
if not cp_shared_kv_current_reuse_enabled():
@@ -1091,25 +1211,40 @@ def should_reuse_current_extend_kv(forward_batch) -> bool:
return True
partial_current = can_reuse_current_extend_kv(forward_batch)
if partial_current and cp_shared_kv_is_draft_input(forward_batch):
prefix_lens_cpu = getattr(forward_batch, "extend_prefix_lens_cpu", None)
extend_lens_cpu = getattr(forward_batch, "extend_seq_lens_cpu", None)
_log_current_reuse_fallback(
"draft_partial_current_reuse_disabled",
"cache-hit EAGLE/NextN draft uses full materialize instead of "
"partial current reuse. prefix_lens=%s extend_lens=%s",
[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,
)
return False
return current_only or partial_current
def current_extend_kv_rows_for_reuse(
forward_batch,
*current_kv_tensors: torch.Tensor | None,
) -> int | None:
"""Return valid current rows if current KV tensors can be spliced.
``out_cache_loc`` may be page padded while MLA/index projections only produce
valid-token rows. Partial-current reuse should therefore validate tensor
rows against ``extend_seq_lens_cpu`` rather than the padded loc tensor length.
"""
if not should_reuse_current_extend_kv(forward_batch):
return None
extend_seq_lens_cpu = getattr(forward_batch, "extend_seq_lens_cpu", None)
if extend_seq_lens_cpu is None or len(extend_seq_lens_cpu) != 1:
return None
valid_current_rows = int(extend_seq_lens_cpu[0])
if valid_current_rows <= 0:
return None
out_cache_loc = getattr(forward_batch, "out_cache_loc", None)
if out_cache_loc is None or int(out_cache_loc.numel()) < valid_current_rows:
return None
for tensor in current_kv_tensors:
if tensor is None or int(tensor.shape[0]) < valid_current_rows:
return None
return valid_current_rows
def current_loc_remap_fast_path_args(
forward_batch,
) -> tuple[int | None, int | None]:
@@ -1366,7 +1501,9 @@ def _debug_assert_no_negative_tensor_values(
)
def build_dense_page_remap(logical_pages: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
def build_dense_page_remap(
logical_pages: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Build a dense per-call page remap for shared-KV runtime materialization."""
dense_pages = logical_pages.clone()
positive_mask = logical_pages > 0
@@ -1389,17 +1526,25 @@ def remap_logical_pages_to_dense_pages(
insert_positions = torch.searchsorted(unique_logical_pages, positive_pages)
if cp_shared_kv_debug_enabled() and insert_positions.numel() > 0:
if unique_logical_pages.numel() == 0:
raise ValueError("unique_logical_pages is empty but logical_pages contains data")
raise ValueError(
"unique_logical_pages is empty but logical_pages contains data"
)
if torch.any(insert_positions >= unique_logical_pages.numel()):
raise ValueError("logical_pages contains entries outside unique_logical_pages")
raise ValueError(
"logical_pages contains entries outside unique_logical_pages"
)
if not torch.equal(unique_logical_pages[insert_positions], positive_pages):
raise ValueError("logical_pages contains entries outside unique_logical_pages")
raise ValueError(
"logical_pages contains entries outside unique_logical_pages"
)
dense_pages[positive_mask] = insert_positions.to(dense_pages.dtype) + 1
return dense_pages
def build_slot_page_remap(logical_pages: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
def build_slot_page_remap(
logical_pages: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Build a fixed-shape page remap without dynamic CUDA output ops.
The compact remap path uses boolean compaction + unique/searchsorted. Those
@@ -1618,9 +1763,7 @@ def build_shared_paged_buffer_slot_remap(
physical_page_capacity=page_buffer.shape[0],
)
slot_logical_pages, dense_pages = build_slot_page_remap(logical_pages)
logical_page_capacity = max(int(page_buffer.shape[0]) - 1, 0) * (
layout.cp_size
) + 1
logical_page_capacity = max(int(page_buffer.shape[0]) - 1, 0) * (layout.cp_size) + 1
page_inverse = build_slot_page_inverse_optimized(
slot_logical_pages,
logical_page_capacity=logical_page_capacity,
@@ -1803,9 +1946,7 @@ def build_current_loc_remap(
safe_query_locs = torch.where(
valid_query, query_flat_long, torch.zeros_like(query_flat_long)
)
query_pages = torch.div(
safe_query_locs, page_size, rounding_mode="floor"
)
query_pages = torch.div(safe_query_locs, page_size, rounding_mode="floor")
query_offsets = torch.remainder(safe_query_locs, page_size)
query_pages_in_range = query_pages < logical_page_capacity
safe_query_pages = torch.clamp(query_pages, max=logical_page_capacity - 1)
@@ -1822,7 +1963,9 @@ def build_current_loc_remap(
row_values.to(compact_row_ids.dtype),
torch.full_like(compact_row_ids.reshape(-1), -1),
)
return matched.reshape(query_locs.shape), compact_flat.reshape(query_locs.shape)
return matched.reshape(query_locs.shape), compact_flat.reshape(
query_locs.shape
)
finally:
if cp_shared_kv_sort_nvtx_enabled():
torch.cuda.nvtx.range_pop()
@@ -1975,7 +2118,9 @@ def materialize_local_token_kv_pages(
owned_physical_pages = layout.logical_pages_to_physical(owned_logical_pages).to(
torch.long
)
dense_page_ids = torch.nonzero(owned_mask, as_tuple=False).flatten().to(torch.long) + 1
dense_page_ids = (
torch.nonzero(owned_mask, as_tuple=False).flatten().to(torch.long) + 1
)
page_offsets = torch.arange(page_size, device=kv_cache.device, dtype=torch.long)
src_tokens = (owned_physical_pages[:, None] * page_size + page_offsets).reshape(-1)
dst_tokens = (dense_page_ids[:, None] * page_size + page_offsets).reshape(-1)
@@ -2236,9 +2381,9 @@ def token_page_copy_debug_checksum(
if not torch.any(owned_mask):
return "owned_pages=0"
owned_logical_pages = unique_logical_pages[owned_mask].to(torch.int64)
owned_physical_pages = layout.logical_pages_to_physical(
owned_logical_pages
).to(torch.long)
owned_physical_pages = layout.logical_pages_to_physical(owned_logical_pages).to(
torch.long
)
dense_page_ids = (
torch.nonzero(owned_mask, as_tuple=False).flatten().to(torch.long) + 1
)
@@ -2270,7 +2415,9 @@ def materialize_local_paged_buffer(
owned_physical_pages = layout.logical_pages_to_physical(owned_logical_pages).to(
torch.long
)
dense_page_ids = torch.nonzero(owned_mask, as_tuple=False).flatten().to(torch.long) + 1
dense_page_ids = (
torch.nonzero(owned_mask, as_tuple=False).flatten().to(torch.long) + 1
)
dense_page_buffer[dense_page_ids] = page_buffer[owned_physical_pages]
return dense_page_buffer
@@ -2369,9 +2516,9 @@ def paged_copy_debug_checksum(
if not torch.any(owned_mask):
return "owned_pages=0"
owned_logical_pages = unique_logical_pages[owned_mask].to(torch.int64)
owned_physical_pages = layout.logical_pages_to_physical(
owned_logical_pages
).to(torch.long)
owned_physical_pages = layout.logical_pages_to_physical(owned_logical_pages).to(
torch.long
)
dense_page_ids = (
torch.nonzero(owned_mask, as_tuple=False).flatten().to(torch.long) + 1
)
@@ -23,13 +23,13 @@ from sglang.srt.layers.attention.nsa.cp_shared_kv_runtime import (
cp_shared_kv_mla_prefetch_should_log_layer,
cp_shared_kv_is_draft_input,
cp_shared_kv_should_prefetch_next_layer,
current_extend_kv_rows_for_reuse,
current_loc_remap_fast_path_args,
filter_owned_logical_locs,
get_or_build_shared_token_kv_slot_remap,
is_current_only_extend_batch,
materialize_prefix_and_reuse_current_kv_page_slots,
materialize_shared_token_kv_buffer,
should_reuse_current_extend_kv,
tensor_debug_checksum,
tensor_debug_summary,
)
@@ -1747,13 +1747,12 @@ class NativeSparseAttnBackend(
mla_prefetcher = getattr(
forward_batch, "cp_shared_kv_mla_prefetcher", None
)
can_reuse_current_kv = (
should_reuse_current_extend_kv(forward_batch)
and k is not None
and k_rope is not None
and k.shape[0] == forward_batch.out_cache_loc.numel()
and k_rope.shape[0] == forward_batch.out_cache_loc.numel()
current_kv_rows_for_reuse = current_extend_kv_rows_for_reuse(
forward_batch,
k,
k_rope,
)
can_reuse_current_kv = current_kv_rows_for_reuse is not None
if cp_shared_kv_mla_prefetch_log_enabled():
if cp_shared_kv_mla_prefetch_should_log_layer(layer.layer_id):
prefix_lens_cpu = getattr(
@@ -1788,8 +1787,15 @@ class NativeSparseAttnBackend(
else None,
)
if can_reuse_current_kv:
current_kv_cache = _cat([k, k_rope], dim=-1)
current_locs_for_reuse = forward_batch.out_cache_loc
assert k is not None and k_rope is not None
assert current_kv_rows_for_reuse is not None
valid_current_rows = int(current_kv_rows_for_reuse)
current_kv_cache = _cat(
[k[:valid_current_rows], k_rope[:valid_current_rows]], dim=-1
)
current_locs_for_reuse = forward_batch.out_cache_loc[
:valid_current_rows
]
logical_page_table_1 = page_table_1
current_remap_page_size, current_remap_logical_page_capacity = (
current_loc_remap_fast_path_args(forward_batch)
+4 -4
View File
@@ -306,7 +306,7 @@ def evict_from_tree_cache(
# Standard allocator
available = allocator.available_size()
if available < num_tokens:
logger.info(
logger.debug(
"[MemCache-evict] evict_from_tree_cache: available=%d < num_tokens=%d deficit=%d, triggering eviction",
available,
num_tokens,
@@ -343,7 +343,7 @@ def _evict_for_compute_owner_lanes(
if isinstance(evictable_size, tuple):
evictable_size = evictable_size[0]
if evictable_size <= 0:
logger.info(
logger.debug(
"[MemCache-evict] _evict_for_compute_owner_lanes: evictable_size=%d <= 0, giving up",
evictable_size,
)
@@ -355,7 +355,7 @@ def _evict_for_compute_owner_lanes(
# load-back pressure.
evict_tokens = max(allocator.page_size, deficit_pages * allocator.page_size)
before_available = allocator.available_size()
logger.info(
logger.debug(
"[MemCache-evict] _evict_for_compute_owner_lanes attempt=%d: deficit_pages=%d evict_tokens=%d before_available=%d evictable_size=%d",
attempt,
deficit_pages,
@@ -371,7 +371,7 @@ def _evict_for_compute_owner_lanes(
)
after_available = allocator.available_size()
evicted_tokens = getattr(evict_result, "num_tokens_evicted", 0)
logger.info(
logger.debug(
"[MemCache-evict] _evict_for_compute_owner_lanes attempt=%d result: evicted=%d after_available=%d",
attempt,
evicted_tokens,
+12 -12
View File
@@ -1220,7 +1220,7 @@ class HiRadixCache(RadixCache):
num_evicted += self._evict_backuped(victim)
refreshed = self._refresh_cp_load_back_plan(plan)
logger.info(
logger.debug(
"[HiCache-load] owner-lane device eviction before CP load-back: "
"node_id=%d victims=%s num_evicted=%d required_by_owner=%s "
"before_available_by_owner=%s before_deficit_by_owner=%s "
@@ -1254,7 +1254,7 @@ class HiRadixCache(RadixCache):
)
eviction_plan = self._plan_cp_load_back_owner_lane_evictions(plan)
if len(eviction_plan.victims) == 0:
logger.info(
logger.debug(
"[HiCache-evict] owner-lane evict found no contributing victims: "
"num_tokens=%d deficits=%s evictable_size=%d available_size=%d",
params.num_tokens,
@@ -1276,7 +1276,7 @@ class HiRadixCache(RadixCache):
self._clear_pin(victim)
if not self._cp_device_leaf_is_load_back_victim(victim):
logger.info(
logger.debug(
"[HiCache-evict] owner-lane evict victim no longer evictable: "
"victim_id=%s deficits=%s",
getattr(victim, "id", None),
@@ -1319,7 +1319,7 @@ class HiRadixCache(RadixCache):
if self._node_backuped(victim):
num_evicted += self._evict_backuped(victim)
logger.info(
logger.debug(
"[HiCache-evict] owner-lane evict END: requested_tokens=%d "
"deficits=%s victims=%s planned_freed_by_owner=%s "
"remaining_deficit_by_owner=%s num_evicted=%d "
@@ -1925,7 +1925,7 @@ class HiRadixCache(RadixCache):
victim.host_value = None
self._remove_host_leaf(victim)
logger.info(
logger.debug(
"[HiCache-evict] deterministic CP host eviction before write: "
"node_id=%d phase=%s victims=%s local_freed=%d planned_freed=%s",
node_id,
@@ -1978,7 +1978,7 @@ class HiRadixCache(RadixCache):
)
)
logger.info(
logger.debug(
"[HiCache-write] write_backup CP retry after deterministic host eviction: "
"node_id=%d deficit_by_owner=%s",
node_id,
@@ -1991,7 +1991,7 @@ class HiRadixCache(RadixCache):
if not isinstance(result, HiCacheWriteFailure):
return result
logger.info(
logger.warning(
"[HiCache-write] write_backup CP FAILED after deterministic retry: "
"node_id=%d len=%d needed_slots=%d",
node_id,
@@ -2800,7 +2800,7 @@ class HiRadixCache(RadixCache):
]
heapq.heapify(eviction_heap)
logger.info(
logger.debug(
"[HiCache-evict] evict START: num_tokens=%d heap_size=%d evictable_size=%d available_size=%d",
num_tokens,
len(eviction_heap),
@@ -2864,7 +2864,7 @@ class HiRadixCache(RadixCache):
self._evict_backuped(node)
self.update_eviction_metrics(num_evicted, start_time)
logger.info(
logger.debug(
"[HiCache-evict] evict END: num_tokens=%d num_evicted=%d num_locked_skipped=%d evictable_size_after=%d available_size_after=%d",
num_tokens,
num_evicted,
@@ -2880,7 +2880,7 @@ class HiRadixCache(RadixCache):
freed_len = self.cache_controller.evict_device(node.value)
assert freed_len > 0
self.evictable_size_ -= device_resident_len
logger.info(
logger.debug(
"[HiCache-evict] _evict_backuped: node_id=%d num_evicted=%d physical_tokens=%d lock_ref=%d backed=%s",
node.id,
freed_len,
@@ -2899,7 +2899,7 @@ class HiRadixCache(RadixCache):
def _evict_regular(self, node: TreeNode):
# evict a node not initiated write to host -- emit BlockRemoved
num_evicted = self._node_device_resident_len(node)
logger.info(
logger.debug(
"[HiCache-evict] _evict_regular: node_id=%d num_evicted=%d",
node.id,
num_evicted,
@@ -2943,7 +2943,7 @@ class HiRadixCache(RadixCache):
return 0
leaves = list(self.evictable_host_leaves)
logger.info(
logger.debug(
"[HiCache-evict] _evict_host_for_physical_slots: required_slots=%d sync=%s leaves=%d",
required_host_slots,
synchronize_across_ranks,
@@ -1,3 +1,5 @@
import inspect
import re
import sys
import types
import unittest
@@ -97,6 +99,8 @@ from sglang.srt.managers.cache_controller import (
HiCacheWriteFailure,
HiCacheWriteReservation,
)
import sglang.srt.mem_cache.common as mem_cache_common
import sglang.srt.mem_cache.hiradix_cache as hiradix_cache
from sglang.srt.mem_cache.base_prefix_cache import (
EvictParams,
InsertParams,
@@ -117,6 +121,57 @@ from sglang.test.test_utils import CustomTestCase
register_cpu_ci(est_time=2, suite="stage-a-test-cpu")
class TestHiCacheEvictLoggingLevels(CustomTestCase):
def assert_debug_marker(self, source: str, marker: str):
self.assertIn(marker, source)
self.assertRegex(
source,
r"logger\.debug\(\s*\n\s*\"" + re.escape(marker),
msg=f"{marker} should be debug-only on the success/no-op hot path",
)
self.assertNotRegex(
source,
r"logger\.info\(\s*\n\s*\"" + re.escape(marker),
msg=f"{marker} must not stay at INFO on the success/no-op hot path",
)
def test_evict_hot_path_success_logs_are_debug_only(self):
common_source = inspect.getsource(mem_cache_common)
hiradix_source = inspect.getsource(hiradix_cache)
for marker in (
"[MemCache-evict] evict_from_tree_cache:",
"[MemCache-evict] _evict_for_compute_owner_lanes: evictable_size",
"[MemCache-evict] _evict_for_compute_owner_lanes attempt=%d:",
"[MemCache-evict] _evict_for_compute_owner_lanes attempt=%d result:",
):
self.assert_debug_marker(common_source, marker)
for marker in (
"[HiCache-load] owner-lane device eviction before CP load-back: ",
"[HiCache-evict] owner-lane evict found no contributing victims: ",
"[HiCache-evict] owner-lane evict victim no longer evictable: ",
"[HiCache-evict] owner-lane evict END: requested_tokens=%d ",
"[HiCache-evict] deterministic CP host eviction before write: ",
"[HiCache-write] write_backup CP retry after deterministic host eviction: ",
"[HiCache-evict] evict START:",
"[HiCache-evict] evict END:",
"[HiCache-evict] _evict_backuped:",
"[HiCache-evict] _evict_regular:",
"[HiCache-evict] _evict_host_for_physical_slots:",
):
self.assert_debug_marker(hiradix_source, marker)
self.assertRegex(
hiradix_source,
r"logger\.warning\(\s*\n\s*\"\[HiCache-write\] write_backup CP FAILED after deterministic retry:",
)
self.assertNotRegex(
hiradix_source,
r"logger\.info\(\s*\n\s*\"\[HiCache-write\] write_backup CP FAILED after deterministic retry:",
)
class TestCpHiCacheImports(CustomTestCase):
def test_cp_hicache_public_imports_without_sgl_kernel(self):
import subprocess
@@ -54,9 +54,7 @@ for _name in ("flash_attn_varlen_func", "flash_attn_with_kvcache"):
if not hasattr(flash_attn_stub, _name):
setattr(flash_attn_stub, _name, lambda *args, **kwargs: None)
sgl_kernel_stub = sys.modules.setdefault(
"sgl_kernel", types.ModuleType("sgl_kernel")
)
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"):
@@ -361,9 +359,7 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
with patch.dict(
sys.modules,
{
"sglang.srt.layers.attention.nsa.index_buf_accessor": index_accessor_stub
},
{"sglang.srt.layers.attention.nsa.index_buf_accessor": index_accessor_stub},
):
from sglang.srt.mem_cache.memory_pool import MLATokenToKVPool
@@ -457,7 +453,9 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
self.assertIs(out, buffer)
dist_all_reduce.assert_called_once()
self.assertIs(dist_all_reduce.call_args.args[0], buffer)
self.assertIs(dist_all_reduce.call_args.kwargs["group"], dummy_group.device_group)
self.assertIs(
dist_all_reduce.call_args.kwargs["group"], dummy_group.device_group
)
def test_build_dense_page_remap_preserves_sentinels(self):
from sglang.srt.layers.attention.nsa.cp_shared_kv_runtime import (
@@ -492,9 +490,7 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
)
current_locs = torch.tensor([100, 64, 256, 128], dtype=torch.int64)
query_locs = torch.tensor(
[[128, -1, 64], [512, 100, 256]], dtype=torch.int32
)
query_locs = torch.tensor([[128, -1, 64], [512, 100, 256]], dtype=torch.int32)
is_current, compact_rows = build_current_loc_remap(query_locs, current_locs)
@@ -569,7 +565,7 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
forward_batch.out_cache_loc = torch.arange(64, dtype=torch.int64)
self.assertFalse(can_reuse_current_extend_kv(forward_batch))
def test_should_reuse_current_extend_kv_disables_draft_partial_cache_hit_suffix(
def test_should_reuse_current_extend_kv_enables_draft_partial_cache_hit_suffix(
self,
):
from sglang.srt.environ import envs
@@ -595,13 +591,7 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
)
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.assertIn(
"[CP_SHARED_KV_FALLBACK][current_reuse]",
logs.output[0],
)
self.assertIn("draft_partial_current_reuse_disabled", logs.output[0])
self.assertTrue(runtime.should_reuse_current_extend_kv(forward_batch))
forward_batch.spec_info = TargetSpecInfo()
forward_batch.cp_shared_kv_mla_prefetcher = object()
@@ -619,6 +609,73 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
forward_batch.cp_shared_kv_mla_prefetcher = None
self.assertTrue(runtime.should_reuse_current_extend_kv(forward_batch))
def test_current_extend_kv_rows_for_reuse_accepts_padded_out_cache_loc(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
forward_batch = SimpleNamespace(
forward_mode=_FakeExtendForwardMode(),
batch_size=1,
extend_prefix_lens_cpu=[40384],
extend_seq_lens_cpu=[65],
seq_lens_cpu=torch.tensor([40384 + 65], dtype=torch.int32),
out_cache_loc=torch.arange(128, dtype=torch.int64),
spec_info=DraftSpecInfo(),
)
k = torch.empty((65, 2, 4), dtype=torch.float32)
k_rope = torch.empty((65, 2, 1), dtype=torch.float32)
with envs.SGLANG_CP_SHARED_KV_CURRENT_REUSE.override(True):
self.assertEqual(
runtime.current_extend_kv_rows_for_reuse(forward_batch, k, k_rope),
65,
)
self.assertIsNone(
runtime.current_extend_kv_rows_for_reuse(
forward_batch,
k[:64],
k_rope,
)
)
def test_mla_current_reuse_gate_accepts_padded_out_cache_loc(self):
from pathlib import Path
source = (
Path(__file__).resolve().parents[4]
/ "python/sglang/srt/layers/attention/nsa_backend.py"
).read_text()
start = source.index(" current_kv_rows_for_reuse =")
end = source.index(
" if cp_shared_kv_mla_prefetch_log_enabled()", start
)
gate_source = source[start:end]
self.assertIn("current_extend_kv_rows_for_reuse", gate_source)
self.assertNotIn(
"k.shape[0] == forward_batch.out_cache_loc.numel()",
gate_source,
)
self.assertNotIn(
"k_rope.shape[0] == forward_batch.out_cache_loc.numel()",
gate_source,
)
body_start = source.index(" if can_reuse_current_kv:", end)
body_end = source.index(
" logical_page_table_1 = page_table_1", body_start
)
body_source = "".join(source[body_start:body_end].split())
self.assertIn("valid_current_rows=int(current_kv_rows_for_reuse)", body_source)
self.assertIn("k[:valid_current_rows]", body_source)
self.assertIn("k_rope[:valid_current_rows]", body_source)
self.assertIn("forward_batch.out_cache_loc[:valid_current_rows]", body_source)
def test_runtime_fallback_helpers_use_standard_warning_marker(self):
from sglang.srt.layers.attention.nsa import cp_shared_kv_runtime as runtime
@@ -738,6 +795,53 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
self.assertEqual(current_mask.tolist(), [[False, True, True, False, False]])
self.assertEqual(mixed_locs.tolist(), [[4, 12, 13, -1, -1]])
def test_tai_current_slot_fill_is_skipped_when_sparse_page_self_test_fails(self):
from sglang.srt.environ import envs
from sglang.srt.layers.attention.nsa import cp_shared_kv_runtime as runtime
class BadKernels:
@staticmethod
def fill_current_token_kv_page_slots_and_remap_locs(*args, **kwargs):
raise AssertionError("stale TAI fill kernel should not be called")
with envs.SGLANG_CP_SHARED_KV_USE_TAI_MATERIALIZE.override(True), patch.object(
runtime,
"_tai_current_slot_fill_supports_sparse_pages",
return_value=False,
), patch.object(
runtime,
"_load_tai_materialize_kernels",
return_value=BadKernels,
):
result = runtime._try_tai_fill_current_kv_page_slots_and_remap_locs(
dense_kv_cache=torch.zeros((16, 1), dtype=torch.float32),
materialized_dense_locs=torch.tensor([[4, 5, 8, 9]], dtype=torch.int64),
current_kv_cache=torch.ones((2, 1), dtype=torch.float32),
logical_locs=torch.tensor([[20, 21, 40, 41]], dtype=torch.int64),
current_locs=torch.tensor([20, 21], dtype=torch.int64),
page_inverse=torch.tensor(
[0, -1, -1, -1, -1, 1, -1, -1, -1, -1, 2],
dtype=torch.long,
),
page_size=4,
mask_non_current_in_current_pages=True,
)
self.assertIsNone(result)
@unittest.skipIf(not torch.cuda.is_available(), "CUDA is required")
def test_tai_current_slot_fill_sparse_page_self_test_passes_on_installed_kernel(
self,
):
from sglang.srt.layers.attention.nsa import cp_shared_kv_runtime as runtime
runtime._tai_current_slot_fill_sparse_pages_self_test.cache_clear()
self.assertTrue(
runtime._tai_current_slot_fill_supports_sparse_pages(
torch.device("cuda", torch.cuda.current_device())
)
)
def test_materialize_prefix_and_reuse_current_kv_page_slots_without_prefetcher(
self,
):
@@ -913,9 +1017,7 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
with patch.object(
prefetch.torch.cuda, "current_stream", return_value=current_stream
), patch.object(
prefetcher, "launch_pending_reduce"
) as launch_pending_reduce:
), patch.object(prefetcher, "launch_pending_reduce") as launch_pending_reduce:
prefetcher.wait_attention_window()
launch_pending_reduce.assert_not_called()
@@ -1402,8 +1504,7 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
self.assertTrue(
any(
"token slot remap cache not reused (missing_cached_value)"
in message
"token slot remap cache not reused (missing_cached_value)" in message
for message in logs.output
)
)
@@ -1549,13 +1650,17 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
)
self.assertIn("prefix_not_page_aligned", logger.call_args.args[1])
def test_mla_prefetch_min_prefix_pages_uses_cached_token_default_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.layers.attention.nsa import cp_shared_kv_runtime as runtime
envs.SGLANG_CP_SHARED_KV_MLA_PREFETCH_MIN_PREFIX_PAGES.clear()
default_tokens = envs.SGLANG_CP_SHARED_KV_MLA_PREFETCH_MIN_PREFIX_TOKENS.get()
self.assertEqual(runtime._MLA_PREFETCH_DEFAULT_MIN_PREFIX_TOKENS, default_tokens)
self.assertEqual(
runtime._MLA_PREFETCH_DEFAULT_MIN_PREFIX_TOKENS, default_tokens
)
expected_pages = (default_tokens + 63) // 64
self.assertEqual(
runtime.cp_shared_kv_mla_prefetch_min_prefix_pages(8, page_size=64),
@@ -1844,9 +1949,7 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
with patch.object(
runtime, "cp_shared_kv_tai_fused_mla_store_enabled", return_value=False
), patch.object(
runtime, "_load_tai_fused_mla_store_kernel"
) as load_kernel:
), patch.object(runtime, "_load_tai_fused_mla_store_kernel") as load_kernel:
used = runtime.try_tai_fused_mla_store(
token_to_kv_pool=FakePool(),
layer=SimpleNamespace(layer_id=0),
@@ -1962,7 +2065,9 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
self.inverse_calls = []
self.remap_calls = []
def build_slot_page_inverse(self, slot_logical_pages, logical_page_capacity):
def build_slot_page_inverse(
self, slot_logical_pages, logical_page_capacity
):
self.inverse_calls.append((slot_logical_pages, logical_page_capacity))
return torch.tensor([0, 1, 2, -1], dtype=torch.long)
@@ -2056,7 +2161,9 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
with patch.object(
runtime, "cp_shared_kv_debug_enabled", return_value=True
), patch.object(runtime, "_all_reduce_materialized_buffer", _identity_all_reduce):
), patch.object(
runtime, "_all_reduce_materialized_buffer", _identity_all_reduce
):
with self.assertRaisesRegex(
RuntimeError,
"CP shared KV materialize got logical token locs outside the physical pool",
@@ -2068,7 +2175,9 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
page_size=4,
)
def test_materialize_token_kv_skips_physical_pool_validation_when_debug_disabled(self):
def test_materialize_token_kv_skips_physical_pool_validation_when_debug_disabled(
self,
):
from sglang.srt.layers.attention.nsa import cp_shared_kv_runtime as runtime
from sglang.srt.mem_cache.cp_shared_kv_layout import CpSharedKVLayout
@@ -2081,7 +2190,9 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
with patch.object(
runtime, "cp_shared_kv_debug_enabled", return_value=False
), patch.object(runtime, "_all_reduce_materialized_buffer", _identity_all_reduce):
), patch.object(
runtime, "_all_reduce_materialized_buffer", _identity_all_reduce
):
_, dense_locs = runtime.materialize_shared_token_kv_buffer(
kv_cache=kv_cache,
logical_locs=logical_locs,
@@ -2100,7 +2211,9 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
logical_locs = torch.tensor([4, -1, 8], dtype=torch.int64)
with patch.object(runtime, "cp_shared_kv_debug_enabled", return_value=True):
with patch.object(runtime, "_all_reduce_materialized_buffer", _identity_all_reduce):
with patch.object(
runtime, "_all_reduce_materialized_buffer", _identity_all_reduce
):
_, dense_locs = runtime.materialize_shared_token_kv_buffer(
kv_cache=kv_cache,
logical_locs=logical_locs,
@@ -2172,7 +2285,9 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
logical_locs = torch.tensor([8, 20, -1], dtype=torch.int64)
remap_source_locs = torch.tensor([4, 8, 12, 16, 20], dtype=torch.int64)
with patch.object(runtime, "_all_reduce_materialized_buffer", _identity_all_reduce):
with patch.object(
runtime, "_all_reduce_materialized_buffer", _identity_all_reduce
):
dense_kv, dense_locs = runtime.materialize_shared_token_kv_buffer(
kv_cache=kv_cache,
logical_locs=logical_locs,
@@ -2223,7 +2338,9 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
kv_cache = torch.arange(0, 32, dtype=torch.float32).view(32, 1, 1)
remap_source_locs = torch.tensor([4, 8, 12, 16, 20], dtype=torch.int64)
with patch.object(runtime, "_all_reduce_materialized_buffer", _identity_all_reduce):
with patch.object(
runtime, "_all_reduce_materialized_buffer", _identity_all_reduce
):
dense_kv_a, dense_locs_a = runtime.materialize_shared_token_kv_buffer(
kv_cache=kv_cache,
logical_locs=torch.tensor([4, 20], dtype=torch.int64),
@@ -2254,7 +2371,9 @@ class TestCpSharedKVRuntimeHelpers(unittest.TestCase):
with patch.object(
runtime, "cp_shared_kv_debug_enabled", return_value=True
), patch.object(runtime, "_all_reduce_materialized_buffer", _identity_all_reduce):
), patch.object(
runtime, "_all_reduce_materialized_buffer", _identity_all_reduce
):
with self.assertRaisesRegex(
RuntimeError,
"CP shared KV materialize got logical pages outside the physical page buffer",
@@ -2401,7 +2520,9 @@ class TestCpSharedKVLazyDebugLogging(unittest.TestCase):
self.assertEqual(k_to_write.shape[0], 2)
self.assertEqual(k_rope_to_write.shape[0], 2)
def test_index_write_filter_does_not_build_debug_summaries_when_debug_disabled(self):
def test_index_write_filter_does_not_build_debug_summaries_when_debug_disabled(
self,
):
from sglang.srt.layers.attention.nsa import nsa_indexer
from sglang.srt.mem_cache.cp_shared_kv_layout import CpSharedKVLayout
@@ -2422,10 +2543,12 @@ class TestCpSharedKVLazyDebugLogging(unittest.TestCase):
"sglang.srt.environ.envs.SGLANG_DEBUG_CP_SHARED_KV.get",
return_value=False,
):
physical_locs, key_to_write = nsa_indexer.Indexer._filter_shared_index_write(
None,
forward_batch,
key,
physical_locs, key_to_write = (
nsa_indexer.Indexer._filter_shared_index_write(
None,
forward_batch,
key,
)
)
self.assertEqual(physical_locs.tolist(), [4, 8])
@@ -2493,8 +2616,12 @@ class TestCpSharedKVTaiMaterializeIntegration(unittest.TestCase):
self.remap_calls = []
self.token_calls = []
def build_slot_page_inverse(self, slot_logical_pages, logical_page_capacity):
self.page_inverse_calls.append((slot_logical_pages, logical_page_capacity))
def build_slot_page_inverse(
self, slot_logical_pages, logical_page_capacity
):
self.page_inverse_calls.append(
(slot_logical_pages, logical_page_capacity)
)
return torch.tensor([0, 1, 2, -1, 3], dtype=torch.long)
def remap_logical_locs_to_slot_dense_locs(
@@ -2567,7 +2694,9 @@ class TestCpSharedKVTaiMaterializeIntegration(unittest.TestCase):
from sglang.srt.mem_cache.cp_shared_kv_layout import CpSharedKVLayout
class FakeTaiKernels:
def build_slot_page_inverse(self, slot_logical_pages, logical_page_capacity):
def build_slot_page_inverse(
self, slot_logical_pages, logical_page_capacity
):
return torch.tensor([0, 1, 2, -1, 3], dtype=torch.long)
def remap_logical_locs_to_slot_dense_locs(