Remove NSA spec-v2 graph metadata host syncs

NSA spec-v2 draft-extend graph replay was still using host-derived sequence lengths and the draft-decode backend allowlist. That kept NSA on eager draft-extend for spec-v2 and left seq_lens_cpu.max()/list-to-GPU tensor construction on the decode critical path.

This ports the small upstream DSA metadata fixes into the local NSA backend: size the captured graph page table to req_to_token width, use the static captured page-table width for graph replay, split v2 draft-extend from variable-length v1 draft-extend, and decide draft-extend graph support from the prefill-style backend.

Constraint: Current branch does not have the full upstream needs_cpu_seq_lens scheduler/FutureMap infra.

Rejected: Cherry-pick the full DSA fused metadata generation series | too broad and overlaps with local NSA fused metadata-copy code.

Confidence: medium

Scope-risk: moderate

Directive: Do not collapse DRAFT_EXTEND and DRAFT_EXTEND_V2 here; v1 keeps variable accept lengths while v2 must stay graph-static.

Tested: local pytest test/registered/spec/eagle/test_eagle_v2_draft_extend_contract.py -q (19 passed)

Tested: local py_compile on nsa_backend.py, nsa_backend_mtp_precompute.py, eagle_worker_v2.py

Tested: remote g0034 cjy-glm5-new pytest test/registered/spec/eagle/test_eagle_v2_draft_extend_contract.py -q (19 passed)

Tested: remote g0034 cjy-glm5-new py_compile on nsa_backend.py, nsa_backend_mtp_precompute.py, eagle_worker_v2.py

Not-tested: full decode E2E with SGLANG_ENABLE_SPEC_V2=1
This commit is contained in:
laoyao0822
2026-06-28 02:29:23 +08:00
parent 4414db594c
commit 648a33ab30
4 changed files with 166 additions and 26 deletions

View File

@@ -163,6 +163,86 @@ def test_eagle_v2_binds_draft_runner_to_draft_extend_attention_backend():
raise AssertionError("draft_runner.attn_backend is not bound to draft_extend backend")
def test_nsa_cuda_graph_page_table_is_sized_to_req_to_token_width():
"""NSA graph page tables must match req_to_token's allocated width.
Spec decode can transiently exceed max_context_len. Sizing the captured
page table from max_context_len makes replay copy widths depend on a
narrower fake table than req_to_token actually owns.
"""
tree = _parse_module("python/sglang/srt/layers/attention/nsa_backend.py")
cls = _find_class(tree, "NativeSparseAttnBackend")
func = _find_method(cls, "init_cuda_graph_state")
text = ast.unparse(func)
assert "self.req_to_token.shape[1]" in text
assert (
"self.max_context_len + (self.speculative_num_draft_tokens or 0)"
not in text
)
def test_nsa_cuda_graph_replay_uses_static_page_table_width():
"""Replay metadata should not read seq_lens_cpu to choose copy width.
CUDA graph replay can copy the captured page-table width because the NSA
kernels bound row access with cache_seqlens. Reading seq_lens_cpu.max()
introduces a host sync in the decode hot path.
"""
tree = _parse_module("python/sglang/srt/layers/attention/nsa_backend.py")
cls = _find_class(tree, "NativeSparseAttnBackend")
func = _find_method(cls, "init_forward_metadata_replay_cuda_graph")
text = ast.unparse(func)
graph_sync_free_prefix = text.split("elif forward_mode.is_draft_extend():", 1)[0]
assert "seq_lens_cpu.max().item()" not in graph_sync_free_prefix
assert "metadata.page_table_1.shape[1]" in text
assert "assert seq_lens_cpu is not None" not in graph_sync_free_prefix
def test_nsa_cuda_graph_replay_keeps_draft_extend_v1_v2_semantics_separate():
"""Spec-v2 draft-extend graph uses static width; spec-v1 stays variable.
The v2 graph path must not use accept_length/tolist to resize replay
tensors. The v1 path still needs variable accepted lengths.
"""
tree = _parse_module("python/sglang/srt/layers/attention/nsa_backend.py")
cls = _find_class(tree, "NativeSparseAttnBackend")
func = _find_method(cls, "init_forward_metadata_replay_cuda_graph")
text = ast.unparse(func)
assert "forward_mode.is_draft_extend_v2()" in text
assert "forward_mode.is_draft_extend(include_v2=True)" not in text
assert "torch.full((bs,), self.speculative_num_draft_tokens" in text
assert "extend_seq_lens = spec_info.accept_length[:bs]" in text
assert "elif forward_mode.is_draft_extend():" in text
v1_branch = text.split("elif forward_mode.is_draft_extend():", 1)[1]
assert "seq_lens_cpu.max().item()" in v1_branch
def test_eagle_v2_cuda_draft_extend_graph_allows_nsa_prefill_backend():
"""Spec-v2 should decide draft-extend graph support from prefill backend.
NSA creates a multi-step backend for draft decode and a NativeSparseAttnBackend
for draft extend. Checking the decode backend keeps NSA on the eager
draft-extend path even after the replay metadata is graph-safe.
"""
tree = _parse_module("python/sglang/srt/speculative/eagle_worker_v2.py")
cls = _find_class(tree, "EagleDraftWorker")
func = _find_method(cls, "init_cuda_graphs")
text = ast.unparse(func)
assert "NativeSparseAttnBackend" in text
assert "self.draft_extend_attn_backend" in text
assert "self.draft_attn_backend, TritonMultiStepDraftBackend" not in text
assert "self.draft_attn_backend, TRTLLMMLAMultiStepDraftBackend" not in text
def _function_calls(func: ast.FunctionDef, name: str) -> int:
count = 0