Complete per-forward tensor dump: all-layer trajectory + final hidden, flush per (rid,rank)

dump_tensors now ACCUMULATES per-(layer,stage) into a forward buffer (every fwd_hash
stage at every layer + nsa q_all/attn_out/topk all layers, kv_cache at a layer spread);
dump_flush writes one file per (rid,rank) at model end with the full trajectory + final
hidden + positions + seq/prefix metadata. Gated to cache-hit extends only (skip fresh
full prefills) so --repeat 2 dumps just the L1-hit + L2-reload (small). dump_diff joins
A(rep1) vs C by idx, relerr per token-order stage -> first divergent layer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 14:35:43 +00:00
parent ed42a6dcbc
commit 9031a294b3
3 changed files with 96 additions and 31 deletions

View File

@@ -2724,13 +2724,20 @@ class NativeSparseAttnBackend(
_cp_dump(
forward_batch,
layer.layer_id,
"nsa",
{
"q_all": q_all,
"kv_cache": kv_cache,
"page_table_1": page_table_1,
"attn_out": attn_output,
"topk_indices": page_table_1,
},
)
_cp_dump(
forward_batch,
layer.layer_id,
"nsa_kv",
{"kv_cache": kv_cache},
big_layers=(0, 19, 39, 59, 77),
)
except Exception:
pass
elif nsa_impl == "flashmla_kv":

View File

@@ -132,6 +132,12 @@ def fwd_hash(forward_batch, layer_id, stage, t, *, level: int = 3) -> None:
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."""
# Complete-dump accumulation runs regardless of trace level (gated internally
# by SGLANG_NSA_DUMP_DIR + rid 'dump-'): captures every stage at every layer.
try:
dump_tensors(forward_batch, layer_id, stage, {"v": t})
except Exception:
pass
if trace_level() < level:
return
try:
@@ -186,46 +192,87 @@ def cptrace(level: int, tag: str, **fields) -> None:
pass
def dump_tensors(forward_batch, layer_id, tensors: dict, *, layers=(0,)) -> None:
"""torch.save per-stage attention tensors for an EXACT L1-hit vs L2-reload diff.
Gated by SGLANG_NSA_DUMP_DIR; fires ONLY for rids starting 'dump-' (so just the
explicitly-marked probe requests dump, not the whole flood), on the given layers
(default layer 0) and only on EXTEND forwards. Skips any tensor above a size cap
so a giant prefix can't blow up disk. Files are named {rid}_cp{rank}_L{layer}.pt
so the same content sent twice (rid 'dump-X-a' / 'dump-X-b') is joined offline by
relative error per tensor (robust to fp nondeterminism, unlike a binary hash)."""
def _dump_enabled_rid(forward_batch):
"""Return (dump_dir, rid, cprank) if this forward should dump, else None.
Gated by SGLANG_NSA_DUMP_DIR + rid starting 'dump-' + EXTEND forward."""
try:
d = envs.SGLANG_NSA_DUMP_DIR.get()
except Exception:
d = ""
if not d:
return None
fm = getattr(forward_batch, "forward_mode", None)
if fm is not None and hasattr(fm, "is_extend") and not fm.is_extend():
return None
rids = getattr(forward_batch, "rids", None)
rid = str(rids[0]) if rids else ""
if not rid.startswith("dump-"):
return None
# Only CACHE-HIT extends (have a reloaded/L1 prefix). Skip fresh full prefills
# (extend_prefix_len==0): they dump the whole sequence (huge) and aren't the
# repro. This lets `--repeat 2` dump only the L1-hit (rep1) + L2-reload (small).
epl = getattr(forward_batch, "extend_prefix_lens_cpu", None)
try:
if epl is not None and int(epl.sum().item()) <= 0:
return None
except Exception:
pass
lay = getattr(forward_batch, "cp_shared_kv_layout", None)
cprank = int(getattr(lay, "cp_rank", -1)) if lay is not None else -1
return d, rid, cprank
def dump_tensors(forward_batch, layer_id, tag, tensors: dict, *, big_layers=None) -> None:
"""ACCUMULATE per-(layer,stage) tensors into a per-forward buffer for a COMPLETE
L1-hit vs L2-reload diff. Written once per (rid,rank) by dump_flush() at model end
(token order, comparable; relerr offline beats fp-nondeterminism). 'big' tensors
(e.g. kv_cache, passed with big_layers=<spread>) are kept ONLY at those layers to
bound size; everything else at every layer. Gated via _dump_enabled_rid."""
if _dump_enabled_rid(forward_batch) is None:
return
try:
import torch
buf = getattr(forward_batch, "_cp_dump_buf", None)
if buf is None:
buf = {}
forward_batch._cp_dump_buf = buf
lbuf = buf.setdefault(int(layer_id), {})
for k, v in tensors.items():
if big_layers is not None and int(layer_id) not in big_layers:
continue
if isinstance(v, torch.Tensor):
if v.numel() > 200_000_000:
lbuf[f"{tag}/{k}_skipped_numel"] = int(v.numel())
continue
lbuf[f"{tag}/{k}"] = v.detach().to("cpu")
except Exception:
pass
def dump_flush(forward_batch, *, final_hidden=None, positions=None) -> None:
"""Write the accumulated per-layer dump buffer to ONE file per (rid,rank) at model
end, plus final hidden / positions / alignment metadata, then clear the buffer."""
info = _dump_enabled_rid(forward_batch)
if info is None:
return
d, rid, cprank = info
try:
import os
import torch
if layer_id not in layers:
return
fm = getattr(forward_batch, "forward_mode", None)
if fm is not None and hasattr(fm, "is_extend") and not fm.is_extend():
return
rids = getattr(forward_batch, "rids", None)
rid = str(rids[0]) if rids else ""
if not rid.startswith("dump-"):
return
lay = getattr(forward_batch, "cp_shared_kv_layout", None)
cprank = int(getattr(lay, "cp_rank", -1)) if lay is not None else -1
payload = {"_meta": {"rid": rid, "cprank": cprank, "layer": int(layer_id)}}
for k, v in tensors.items():
buf = getattr(forward_batch, "_cp_dump_buf", None)
forward_batch._cp_dump_buf = None
payload = {"layers": buf or {}, "rid": rid, "cprank": cprank}
if isinstance(final_hidden, torch.Tensor):
payload["final_hidden"] = final_hidden.detach().to("cpu")
if isinstance(positions, torch.Tensor):
payload["positions"] = positions.detach().to("cpu")
for name in ("seq_lens_cpu", "extend_prefix_lens_cpu", "extend_seq_lens_cpu"):
v = getattr(forward_batch, name, None)
if isinstance(v, torch.Tensor):
if v.numel() > 80_000_000: # ~80M-elem cap; skip huge composed buffers
payload[k + "_skipped_numel"] = int(v.numel())
continue
payload[k] = v.detach().to("cpu")
elif v is not None:
payload[k] = v
payload[name] = v.detach().to("cpu")
os.makedirs(d, exist_ok=True)
torch.save(payload, os.path.join(d, f"{rid}_cp{cprank}_L{layer_id}.pt"))
torch.save(payload, os.path.join(d, f"{rid}_cp{cprank}.pt"))
except Exception:
pass

View File

@@ -2146,6 +2146,17 @@ class DeepseekV2Model(nn.Module):
else:
hidden_states, _ = self.norm(hidden_states, residual)
# Complete-dump flush at model end (gated by SGLANG_NSA_DUMP_DIR + rid 'dump-'):
# write the per-layer buffer + final hidden + metadata to one file per (rid,rank).
try:
from sglang.srt.mem_cache.cp_hicache_trace import dump_flush as _cp_dump_flush
_cp_dump_flush(
forward_batch, final_hidden=hidden_states, positions=positions
)
except Exception:
pass
if getattr(forward_batch, "capture_draft_hidden_states", False):
forward_batch.draft_hidden_states = hidden_states