Files
sglang/python/sglang/srt/mem_cache/cp_hicache_trace.py
leavelet a447ae8317 CP HiCache trace: NSA indexer-K round-trip hash + forward-side attn/MoE hashes
Main KV round-trips byte-perfect yet output is garbage, so add the two
unverified components:

1. NSA INDEXER-K round-trip (level 2): hash the device index_k_with_scale_buffer
   at backup (_backup_indexer_from_device_per_layer) and reload (NSA
   load_to_device_per_layer, after _load_indexer), keyed by host-slot fingerprint
   + layer, with khash+nz. The indexer selects which tokens attention attends
   (top-k); if it corrupts on reload -> wrong selection -> garbage even with
   correct main KV.

2. FORWARD-side per-layer hashes (level 3, eager extend path only, cuda-graph
   guarded): attn-input, attn-output (pre-residual), topk_indices (the indexer's
   selection output -- direct consumer of the indexer-K), and MoE-input, in the
   DeepseekV2/GlmMoeDsa decoder layer forward. Localizes where a reload forward
   diverges: topk diverges => indexer-K cache; attn-out diverges (topk ok) =>
   main KV/page mapping; moe-in diverges (attn-out ok) => residual/MoE.

Analyzer compares indexer reload vs backup (host_fp keyed) + flags zero/degenerate
hidden states per stage. Level 3 (SGLANG_CP_HICACHE_KV_TRACE=3) captures everything.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 03:05:30 +00:00

148 lines
5.3 KiB
Python

"""Env-gated structured tracing for CP HiCache KV round-trip corruption debugging.
All output is keyed by ``node_id`` (which threads prepare->backup->evict->reload)
plus a ``rid_map`` line that ties the client request id to its node_id. The
companion analyzer joins these lines by node_id and checks four invariants:
H1 split-misattribution, H2 stale/wrong host ref on reload,
H3 stale compose cache, H4 page-recycle-before-backup race.
Gate: SGLANG_CP_HICACHE_KV_TRACE (0=off, 1=structural lifecycle, 2=+compose/free/ack).
Off by default and cheap to leave in place: when the level is below the call's
level, nothing is computed or logged.
"""
import logging
from sglang.srt.environ import envs
logger = logging.getLogger(__name__)
def trace_level() -> int:
try:
return int(envs.SGLANG_CP_HICACHE_KV_TRACE.get())
except Exception:
return 0
def trace_enabled(level: int = 1) -> bool:
return trace_level() >= level
def rng(t) -> str:
"""Compact summary of a 1-D index tensor/list: (cnt,min,max,first,last).
Debug-only; .item() syncs are acceptable here. Empty -> (0)."""
if t is None:
return "(none)"
try:
n = t.numel() if hasattr(t, "numel") else len(t)
if n == 0:
return "(0)"
if hasattr(t, "numel"):
tf = t.flatten()
return (
f"(cnt={n},min={int(tf.min().item())},max={int(tf.max().item())},"
f"first={int(tf[0].item())},last={int(tf[-1].item())})"
)
return f"(cnt={n},min={min(t)},max={max(t)},first={t[0]},last={t[-1]})"
except Exception as e: # never let tracing crash the hot path
return f"(err:{e})"
def _fmt(v) -> str:
"""Render a value with NO internal spaces so the log is space-splittable
into key=value pairs by the analyzer."""
if isinstance(v, (list, tuple)):
return "[" + ",".join(_fmt(x) for x in v) + "]"
return str(v).replace(" ", "")
def khash(t) -> int:
"""Full-tensor, position-weighted int64 fingerprint of a KV slice.
Sensitive to BOTH value and position (catches permutation/scatter, unlike a
plain sum), covers the whole tensor (not a byte sample), and reduces to one
int. Debug-only: the .item() sync is acceptable when tracing is on. Used to
compare the KV bytes a node's pages hold at backup (correct, post-store, on
the default stream) vs what comes back at reload (post-H2D); inequality =
the round-trip delivered wrong KV (store-vs-copy race or transfer corruption)."""
try:
import torch
if t is None or t.numel() == 0:
return 0
b = t.detach().contiguous().view(torch.uint8).reshape(-1).to(torch.int64)
n = b.numel()
idx = torch.arange(1, n + 1, device=b.device, dtype=torch.int64)
return int((((b * idx).sum() ^ (b.sum() * 1000003) ^ n) & 0x7FFFFFFFFFFFFFFF).item())
except Exception:
return -1
def knz(t) -> int:
"""Count of nonzero bytes in a KV slice. Real KV is never all-zero, so a
reload (or backup) slice with nz==0 = uninitialized/zero KV -- a direct,
self-contained corruption flag (no cross-stage matching needed). Catches the
observed '0|0|0...' garbage where the store never filled the backed-up pages."""
try:
import torch
if t is None or t.numel() == 0:
return 0
b = t.detach().contiguous().view(torch.uint8).reshape(-1)
return int((b != 0).sum().item())
except Exception:
return -1
def fwd_hash(forward_batch, layer_id, stage, t, *, level: int = 3) -> None:
"""Hash a per-layer forward tensor (attn-in/out, topk_indices, MoE-in) to
localize where a reload forward diverges from a fresh one. Level 3 (so the
level-2 KV run is unaffected). Guards: only the EAGER EXTEND path (the reload
case) -- never under cuda-graph decode (.item() sync would corrupt capture);
handles topk_indices=None and tuple/quant hidden_states."""
if trace_level() < level:
return
try:
import torch
fm = getattr(forward_batch, "forward_mode", None)
if fm is not None and hasattr(fm, "is_extend") and not fm.is_extend():
return
if isinstance(t, torch.Tensor):
h, nz, rows = khash(t), knz(t), int(t.shape[0]) if t.dim() else 0
elif t is None:
h, nz, rows = 0, 0, 0
else:
h, nz, rows = -2, -2, -2 # tuple/quant: not a plain tensor
rids = getattr(forward_batch, "rids", None)
lay = getattr(forward_batch, "cp_shared_kv_layout", None)
cptrace(
level,
"fwd_hash",
rid=(rids[0] if rids else "?"),
nreq=(len(rids) if rids else 0),
layer=layer_id,
stage=stage,
h=h,
nz=nz,
rows=rows,
cprank=(getattr(lay, "cp_rank", -1) if lay is not None else -1),
)
except Exception:
pass
def cptrace(level: int, tag: str, **fields) -> None:
"""Emit one ``[CPTRACE <tag>] k=v ...`` line if the gate >= level.
Every value is rendered space-free so ``line.split()`` recovers the
key=value pairs cleanly in the offline analyzer."""
if trace_level() < level:
return
try:
parts = " ".join(f"{k}={_fmt(v)}" for k, v in fields.items())
logger.info("[CPTRACE %s] %s", tag, parts)
except Exception:
pass