Add attention-backend unit-test suite under test/registered/attention/unittest (#26517)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cheng Wan
2026-05-28 17:30:31 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 3bdea78ad1
commit f66f56c6bd
69 changed files with 27422 additions and 0 deletions
@@ -0,0 +1 @@
"""Shared fixtures for manual attention backend unit tests."""
@@ -0,0 +1 @@
"""Attention-method fixtures for attention backend unit tests."""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,57 @@
"""Mock `ServerArgs` factory for attention-backend unit tests.
Production attention backends read many `ServerArgs` attributes and call
several `ServerArgs` methods at backend construction time. The set grows
monotonically: new attention features add new attributes/methods to
`ServerArgs`, and a fixture that mocks `server_args` as a manually-
populated `SimpleNamespace` will silently miss the new field and fail
with `AttributeError` the next time a backend looks it up.
`make_mock_server_args` sidesteps this by instantiating a real
`ServerArgs` (the dataclass) with all defaults from the dataclass
definition, then overlaying the caller's explicit overrides. New
`ServerArgs` attributes are picked up automatically with their default
values; methods like `enable_mamba_extra_buffer()` work because the
object is a real `ServerArgs` instance, so methods are bound correctly.
`__post_init__` is intentionally bypassed (via `object.__new__`) so
fixture callers don't have to supply a real `model_path`; the
validation it performs is irrelevant for module-level attention tests.
"""
import dataclasses
from sglang.srt.server_args import ServerArgs
def make_mock_server_args(**overrides) -> ServerArgs:
"""Return a `ServerArgs` instance with all defaults pre-populated.
The instance is built by `object.__new__(ServerArgs)` so `__post_init__`
does not run — fixture callers do not need to supply a valid
`model_path` or other required-field values.
Any field with a `default` or `default_factory` in the dataclass
definition is set automatically. Caller-supplied `overrides` replace
those defaults; unknown keys are also stored (matching `SimpleNamespace`
semantics) so fixtures can attach test-only attributes when needed.
If an override name corresponds to a read-only `@property` on
`ServerArgs`, the value is stored under `_<name>` instead — many
`ServerArgs` properties cache through `_<name>` and return it when
set, so fixture callers can keep using the public name and let this
helper translate.
"""
sa = object.__new__(ServerArgs)
for f in dataclasses.fields(ServerArgs):
if f.default is not dataclasses.MISSING:
setattr(sa, f.name, f.default)
elif f.default_factory is not dataclasses.MISSING:
setattr(sa, f.name, f.default_factory())
for k, v in overrides.items():
cls_attr = getattr(type(sa), k, None)
if isinstance(cls_attr, property):
setattr(sa, f"_{k}", v)
else:
setattr(sa, k, v)
return sa
@@ -0,0 +1 @@
"""Runner orchestration helpers for attention backend unit tests."""
@@ -0,0 +1,947 @@
from dataclasses import dataclass
from typing import Any, Callable
import torch
from sglang.srt.model_executor.forward_context import ForwardContext, forward_context
from ..attention_methods.dense_attention import DEFAULT_DEVICE as DENSE_DEFAULT_DEVICE
from ..attention_methods.dense_attention import DEFAULT_DTYPE as DENSE_DEFAULT_DTYPE
from ..attention_methods.dense_attention import (
DEFAULT_HEAD_DIM,
DEFAULT_HIDDEN_SIZE,
)
from ..attention_methods.dense_attention import (
DEFAULT_MAX_CONTEXT_LEN as DENSE_DEFAULT_MAX_CONTEXT_LEN,
)
from ..attention_methods.dense_attention import (
DENSE_ATOL,
DENSE_RTOL,
DenseAttentionCase,
)
from ..attention_methods.dense_attention import (
_make_forward_batch as _make_dense_forward_batch,
)
from ..attention_methods.dense_attention import (
build_dense_attention_fixture,
dense_fixture_inputs,
expected_dense_output_from_inputs,
make_dense_case_with_prefix_lens,
make_dense_padded_replay_inputs,
make_dense_random_inputs,
prepare_dense_runner_inputs,
run_dense_fixture_eager,
run_dense_forward,
)
from ..attention_methods.dsa_attention import (
DSA_PAGE_SIZE,
DSA_SPARSE_ATOL,
DSA_SPARSE_RTOL,
DSAAttentionCase,
_clone_dsa_sparse_cache,
)
from ..attention_methods.dsa_attention import (
_make_forward_batch as _make_dsa_forward_batch,
)
from ..attention_methods.dsa_attention import (
_restore_dsa_sparse_cache,
build_dsa_sparse_attention_fixture,
dsa_sparse_fixture_inputs,
expected_dsa_sparse_output_from_inputs,
make_dsa_sparse_case_with_prefix_lens,
make_dsa_sparse_random_inputs,
make_dsa_sparse_replay_inputs,
prepare_dsa_sparse_runner_inputs,
run_dsa_sparse_forward,
)
from ..attention_methods.dsv4_attention import (
DSV4_ATOL,
DSV4_RTOL,
DSV4AttentionCase,
)
from ..attention_methods.dsv4_attention import (
_make_forward_batch as _make_dsv4_forward_batch,
)
from ..attention_methods.dsv4_attention import (
build_dsv4_attention_fixture,
dsv4_fixture_inputs,
expected_dsv4_output_from_inputs,
make_dsv4_case_with_prefix_lens,
make_dsv4_padded_replay_inputs,
make_dsv4_random_inputs,
prepare_dsv4_runner_inputs,
run_dsv4_fixture_eager,
run_dsv4_forward,
)
from ..attention_methods.dual_chunk_attention import (
DualChunkAttentionCase,
_clone_dual_chunk_cache,
_restore_dual_chunk_cache,
build_dual_chunk_attention_fixture,
dual_chunk_fixture_inputs,
expected_dual_chunk_output_from_inputs,
make_dual_chunk_case_with_prefix_lens,
make_dual_chunk_random_inputs,
make_dual_chunk_replay_inputs,
prepare_dual_chunk_runner_inputs,
run_dual_chunk_fixture_eager,
run_dual_chunk_forward,
)
from ..attention_methods.gdn_attention import DEFAULT_DEVICE as GDN_DEFAULT_DEVICE
from ..attention_methods.gdn_attention import DEFAULT_DTYPE as GDN_DEFAULT_DTYPE
from ..attention_methods.gdn_attention import (
DEFAULT_HEAD_K_DIM,
DEFAULT_HEAD_V_DIM,
)
from ..attention_methods.gdn_attention import (
DEFAULT_MAX_CONTEXT_LEN as GDN_DEFAULT_MAX_CONTEXT_LEN,
)
from ..attention_methods.gdn_attention import (
GDN_ATOL,
GDN_RTOL,
GDNAttentionCase,
_clone_gdn_cache,
)
from ..attention_methods.gdn_attention import (
_make_forward_batch as _make_gdn_forward_batch,
)
from ..attention_methods.gdn_attention import (
_restore_gdn_cache,
build_gdn_attention_fixture,
expected_gdn_output_from_inputs,
gdn_fixture_inputs,
make_gdn_case_with_prefix_lens,
make_gdn_random_inputs,
make_gdn_replay_inputs,
prepare_gdn_runner_inputs,
run_gdn_fixture_eager,
run_gdn_forward,
)
from ..attention_methods.kda_attention import DEFAULT_DEVICE as KDA_DEFAULT_DEVICE
from ..attention_methods.kda_attention import DEFAULT_DTYPE as KDA_DEFAULT_DTYPE
from ..attention_methods.kda_attention import (
DEFAULT_HEAD_K_DIM as KDA_DEFAULT_HEAD_K_DIM,
)
from ..attention_methods.kda_attention import (
DEFAULT_HEAD_V_DIM as KDA_DEFAULT_HEAD_V_DIM,
)
from ..attention_methods.kda_attention import (
DEFAULT_MAX_CONTEXT_LEN as KDA_DEFAULT_MAX_CONTEXT_LEN,
)
from ..attention_methods.kda_attention import (
KDA_GRAPH_ATOL,
KDA_GRAPH_RTOL,
KDAAttentionCase,
_clone_kda_cache,
)
from ..attention_methods.kda_attention import (
_make_forward_batch as _make_kda_forward_batch,
)
from ..attention_methods.kda_attention import (
_restore_kda_cache,
build_kda_attention_fixture,
expected_kda_output_from_inputs,
kda_fixture_inputs,
make_kda_case_with_prefix_lens,
make_kda_random_inputs,
make_kda_replay_inputs,
prepare_kda_runner_inputs,
run_kda_fixture_eager,
run_kda_forward,
)
from ..attention_methods.lightning_attention import (
DEFAULT_DEVICE as LIGHTNING_DEFAULT_DEVICE,
)
from ..attention_methods.lightning_attention import (
DEFAULT_DTYPE as LIGHTNING_DEFAULT_DTYPE,
)
from ..attention_methods.lightning_attention import (
DEFAULT_HEAD_DIM as LIGHTNING_DEFAULT_HEAD_DIM,
)
from ..attention_methods.lightning_attention import (
DEFAULT_MAX_CONTEXT_LEN as LIGHTNING_DEFAULT_MAX_CONTEXT_LEN,
)
from ..attention_methods.lightning_attention import (
LIGHTNING_GRAPH_ATOL,
LIGHTNING_GRAPH_RTOL,
LightningAttentionCase,
_clone_lightning_cache,
)
from ..attention_methods.lightning_attention import (
_make_forward_batch as _make_lightning_forward_batch,
)
from ..attention_methods.lightning_attention import (
_restore_lightning_cache,
build_lightning_attention_fixture,
expected_lightning_output_from_inputs,
lightning_fixture_inputs,
make_lightning_case_with_prefix_lens,
make_lightning_random_inputs,
make_lightning_replay_inputs,
prepare_lightning_runner_inputs,
run_lightning_fixture_eager,
run_lightning_forward,
)
from ..attention_methods.mamba2_attention import DEFAULT_DEVICE as MAMBA2_DEFAULT_DEVICE
from ..attention_methods.mamba2_attention import DEFAULT_DTYPE as MAMBA2_DEFAULT_DTYPE
from ..attention_methods.mamba2_attention import (
DEFAULT_MAX_CONTEXT_LEN as MAMBA2_DEFAULT_MAX_CONTEXT_LEN,
)
from ..attention_methods.mamba2_attention import (
MAMBA2_GRAPH_ATOL,
MAMBA2_GRAPH_RTOL,
Mamba2AttentionCase,
_clone_mamba2_cache,
)
from ..attention_methods.mamba2_attention import (
_make_forward_batch as _make_mamba2_forward_batch,
)
from ..attention_methods.mamba2_attention import (
_restore_mamba2_cache,
build_mamba2_attention_fixture,
expected_mamba2_output_from_inputs,
make_mamba2_case_with_prefix_lens,
make_mamba2_random_inputs,
make_mamba2_replay_inputs,
mamba2_fixture_inputs,
prepare_mamba2_runner_inputs,
run_mamba2_fixture_eager,
run_mamba2_forward,
)
from ..attention_methods.mla_attention import DEFAULT_DEVICE as MLA_DEFAULT_DEVICE
from ..attention_methods.mla_attention import DEFAULT_DTYPE as MLA_DEFAULT_DTYPE
from ..attention_methods.mla_attention import (
DEFAULT_HIDDEN_SIZE as MLA_DEFAULT_HIDDEN_SIZE,
)
from ..attention_methods.mla_attention import (
DEFAULT_KV_LORA_RANK,
)
from ..attention_methods.mla_attention import (
DEFAULT_MAX_CONTEXT_LEN as MLA_DEFAULT_MAX_CONTEXT_LEN,
)
from ..attention_methods.mla_attention import (
DEFAULT_QK_ROPE_HEAD_DIM,
MLA_ATOL,
MLA_RTOL,
MLAAttentionCase,
)
from ..attention_methods.mla_attention import (
_make_forward_batch as _make_mla_forward_batch,
)
from ..attention_methods.mla_attention import (
build_mla_attention_fixture,
expected_mla_output_from_inputs,
make_mla_case_with_prefix_lens,
make_mla_padded_replay_inputs,
make_mla_random_inputs,
mla_fixture_inputs,
prepare_mla_runner_inputs,
run_mla_fixture_eager,
run_mla_forward,
)
DENSE_CUDA_GRAPH_CAPTURE_BATCH_SIZE = 4
MLA_CUDA_GRAPH_CAPTURE_BATCH_SIZE = 4
GDN_CUDA_GRAPH_CAPTURE_BATCH_SIZE = 3
DSV4_CUDA_GRAPH_CAPTURE_BATCH_SIZE = 2
KDA_CUDA_GRAPH_CAPTURE_BATCH_SIZE = 3
LIGHTNING_CUDA_GRAPH_CAPTURE_BATCH_SIZE = 3
MAMBA2_CUDA_GRAPH_CAPTURE_BATCH_SIZE = 3
@dataclass(frozen=True)
class CudaGraphDecodeAdapter:
build_fixture: Callable[..., Any]
make_case: Callable[[Any, str, tuple[int, ...]], Any]
make_forward_batch: Callable[..., Any]
fixture_inputs: Callable[[Any], dict[str, Any]]
make_capture_inputs: Callable[..., dict[str, Any]]
make_replay_inputs: Callable[..., dict[str, Any]]
prepare_inputs: Callable[..., None]
run_eager: Callable[[Any], torch.Tensor]
run_forward: Callable[[Any, Any, dict[str, Any]], torch.Tensor]
expected_output: Callable[[Any, Any, dict[str, Any], Any], torch.Tensor]
clone_state: Callable[[Any], Any] = lambda _: None
restore_state: Callable[[Any, Any], None] = lambda _fixture, _state: None
allow_padding: bool = True
atol: float = 0.0
rtol: float = 0.0
def _check_decode_cuda_graph_case(case, capture_batch_size: int, *, allow_padding=True):
if not case.forward_mode.is_decode():
raise ValueError(
"CUDA graph runner integration currently expects decode cases."
)
if allow_padding:
if case.batch_size > capture_batch_size:
raise ValueError(
"CUDA graph capture batch size must be at least the replay batch size."
)
elif case.batch_size != capture_batch_size:
raise ValueError(
"This CUDA graph coverage uses an unpadded replay batch; choose a case "
"whose batch size matches the capture batch size."
)
def _init_cuda_graph_capture_metadata(backend, capture_batch_size: int, batch):
backend.init_cuda_graph_state(
max_bs=capture_batch_size,
max_num_tokens=batch.input_ids.numel(),
)
backend.init_forward_metadata_capture_cuda_graph(
bs=capture_batch_size,
num_tokens=batch.input_ids.numel(),
req_pool_indices=batch.req_pool_indices,
seq_lens=batch.seq_lens,
encoder_lens=batch.encoder_lens,
forward_mode=batch.forward_mode,
spec_info=batch.spec_info,
)
def _init_cuda_graph_replay_metadata(backend, capture_batch_size: int, batch):
# Some backends (e.g., `DeepseekV4AttnBackend`) read out-of-band attributes
# off the backend during replay metadata init — production wires this in
# `sglang/srt/model_executor/cuda_graph_runner.py:1234`. Mirror that
# contract so backends that don't use it just store-and-clear the field.
backend._replay_forward_batch = batch
try:
backend.init_forward_metadata_replay_cuda_graph(
bs=capture_batch_size,
req_pool_indices=batch.req_pool_indices,
seq_lens=batch.seq_lens,
seq_lens_sum=batch.seq_lens_sum,
encoder_lens=batch.encoder_lens,
forward_mode=batch.forward_mode,
spec_info=batch.spec_info,
seq_lens_cpu=batch.seq_lens_cpu,
)
finally:
backend._replay_forward_batch = None
def _run_cuda_graph_decode_case(
testcase,
case,
*,
adapter: CudaGraphDecodeAdapter,
build_kwargs: dict,
capture_batch_size: int,
max_context_len: int,
dtype: torch.dtype,
device: str,
):
_check_decode_cuda_graph_case(
case,
capture_batch_size,
allow_padding=adapter.allow_padding,
)
# NOTE: `capture_prefix_len`-vs-replay assertion happens below once the
# graph fixture is built (we need `backend.get_cuda_graph_seq_len_fill_value`).
eager_fixture = adapter.build_fixture(testcase, case, **build_kwargs)
eager_inputs = adapter.fixture_inputs(eager_fixture)
eager_initial_state = adapter.clone_state(eager_fixture)
eager_actual = adapter.run_eager(eager_fixture)
eager_expected = adapter.expected_output(
eager_fixture,
case,
eager_inputs,
eager_initial_state,
)
torch.testing.assert_close(
eager_actual,
eager_expected,
atol=adapter.atol,
rtol=adapter.rtol,
)
graph_fixture = adapter.build_fixture(
testcase,
case,
**build_kwargs,
disable_cuda_graph=False,
runner_batch_size=capture_batch_size,
)
backend = graph_fixture.backend
graph_replay_inputs = adapter.fixture_inputs(graph_fixture)
graph_initial_state = adapter.clone_state(graph_fixture)
capture_prefix_len = max(0, backend.get_cuda_graph_seq_len_fill_value() - 1)
if any(p < capture_prefix_len for p in case.prefix_lens):
raise AssertionError(
f"replay prefix_lens must each be >= capture_prefix_len="
f"{capture_prefix_len} so capture-time random KV does not leak "
f"into replay; got prefix_lens={case.prefix_lens}"
)
capture_case = adapter.make_case(
case,
f"{case.name}_cuda_graph_capture",
(capture_prefix_len,) * capture_batch_size,
)
capture_inputs = adapter.make_capture_inputs(
capture_case,
graph_fixture,
dtype=dtype,
device=device,
)
capture_batch = adapter.make_forward_batch(
capture_case,
graph_fixture.runner,
max_context_len=max_context_len,
device=device,
)
adapter.prepare_inputs(
graph_fixture,
capture_case,
capture_batch,
capture_inputs,
max_context_len=max_context_len,
)
with torch.no_grad(), forward_context(ForwardContext(attn_backend=backend)):
_init_cuda_graph_capture_metadata(backend, capture_batch_size, capture_batch)
# Capture forward is a JIT warmup that mirrors production: the
# captured CUDA graph records kernel launches against buffers
# that *will* be populated by `init_forward_metadata_replay_cuda_graph`
# at replay. The capture-time output itself is discarded in
# production — and we discard it here too. Backends like FA3/FA4
# legitimately assign-but-don't-populate metadata buffers at
# capture, which makes the capture-time output undefined; only
# the replay output is contractually required to match the
# reference.
adapter.run_forward(graph_fixture, capture_batch, capture_inputs)
backend.on_after_cuda_graph_warmup()
adapter.restore_state(graph_fixture, graph_initial_state)
replay_pad_prefix_lens = (capture_prefix_len,) * (
capture_batch_size - case.batch_size
)
replay_case = adapter.make_case(
case,
f"{case.name}_cuda_graph_replay",
case.prefix_lens + replay_pad_prefix_lens,
)
replay_inputs = adapter.make_replay_inputs(
replay_case,
graph_fixture,
replay_pad_prefix_lens,
graph_replay_inputs,
dtype=dtype,
device=device,
)
replay_batch = adapter.make_forward_batch(
replay_case,
graph_fixture.runner,
max_context_len=max_context_len,
device=device,
)
adapter.prepare_inputs(
graph_fixture,
replay_case,
replay_batch,
replay_inputs,
max_context_len=max_context_len,
)
_init_cuda_graph_replay_metadata(backend, capture_batch_size, replay_batch)
replay_actual = adapter.run_forward(
graph_fixture,
replay_batch,
replay_inputs,
)
replay_expected = adapter.expected_output(
graph_fixture,
replay_case,
replay_inputs,
graph_initial_state,
)
torch.testing.assert_close(
replay_actual,
replay_expected,
atol=adapter.atol,
rtol=adapter.rtol,
)
torch.testing.assert_close(
replay_actual[: case.num_input_tokens],
eager_actual,
atol=adapter.atol,
rtol=adapter.rtol,
)
def run_dense_cuda_graph_decode_case(
testcase,
case: DenseAttentionCase,
*,
head_dim: int = DEFAULT_HEAD_DIM,
hidden_size: int = DEFAULT_HIDDEN_SIZE,
max_context_len: int = DENSE_DEFAULT_MAX_CONTEXT_LEN,
dtype: torch.dtype = DENSE_DEFAULT_DTYPE,
device: str = DENSE_DEFAULT_DEVICE,
cuda_graph_capture_batch_size: int = DENSE_CUDA_GRAPH_CAPTURE_BATCH_SIZE,
):
adapter = CudaGraphDecodeAdapter(
build_fixture=build_dense_attention_fixture,
make_case=make_dense_case_with_prefix_lens,
make_forward_batch=_make_dense_forward_batch,
fixture_inputs=dense_fixture_inputs,
make_capture_inputs=make_dense_random_inputs,
make_replay_inputs=make_dense_padded_replay_inputs,
prepare_inputs=prepare_dense_runner_inputs,
run_eager=run_dense_fixture_eager,
run_forward=run_dense_forward,
expected_output=expected_dense_output_from_inputs,
atol=DENSE_ATOL,
rtol=DENSE_RTOL,
)
_run_cuda_graph_decode_case(
testcase,
case,
adapter=adapter,
build_kwargs=dict(
head_dim=head_dim,
hidden_size=hidden_size,
max_context_len=max_context_len,
dtype=dtype,
device=device,
),
capture_batch_size=cuda_graph_capture_batch_size,
max_context_len=max_context_len,
dtype=dtype,
device=device,
)
def run_mla_cuda_graph_decode_case(
testcase,
case: MLAAttentionCase,
*,
kv_lora_rank: int = DEFAULT_KV_LORA_RANK,
qk_rope_head_dim: int = DEFAULT_QK_ROPE_HEAD_DIM,
hidden_size: int = MLA_DEFAULT_HIDDEN_SIZE,
max_context_len: int = MLA_DEFAULT_MAX_CONTEXT_LEN,
dtype: torch.dtype = MLA_DEFAULT_DTYPE,
device: str = MLA_DEFAULT_DEVICE,
cuda_graph_capture_batch_size: int = MLA_CUDA_GRAPH_CAPTURE_BATCH_SIZE,
):
adapter = CudaGraphDecodeAdapter(
build_fixture=build_mla_attention_fixture,
make_case=make_mla_case_with_prefix_lens,
make_forward_batch=_make_mla_forward_batch,
fixture_inputs=mla_fixture_inputs,
make_capture_inputs=make_mla_random_inputs,
make_replay_inputs=make_mla_padded_replay_inputs,
prepare_inputs=prepare_mla_runner_inputs,
run_eager=run_mla_fixture_eager,
run_forward=run_mla_forward,
expected_output=expected_mla_output_from_inputs,
atol=MLA_ATOL,
rtol=MLA_RTOL,
)
_run_cuda_graph_decode_case(
testcase,
case,
adapter=adapter,
build_kwargs=dict(
kv_lora_rank=kv_lora_rank,
qk_rope_head_dim=qk_rope_head_dim,
hidden_size=hidden_size,
max_context_len=max_context_len,
dtype=dtype,
device=device,
),
capture_batch_size=cuda_graph_capture_batch_size,
max_context_len=max_context_len,
dtype=dtype,
device=device,
)
def run_dsv4_cuda_graph_decode_case(
testcase,
case: DSV4AttentionCase,
*,
swa_size: int = 1024,
max_context_len: int = 256,
dtype: torch.dtype = torch.bfloat16,
device: str = "cuda",
cuda_graph_capture_batch_size: int = DSV4_CUDA_GRAPH_CAPTURE_BATCH_SIZE,
):
adapter = CudaGraphDecodeAdapter(
build_fixture=build_dsv4_attention_fixture,
make_case=make_dsv4_case_with_prefix_lens,
make_forward_batch=_make_dsv4_forward_batch,
fixture_inputs=dsv4_fixture_inputs,
make_capture_inputs=make_dsv4_random_inputs,
make_replay_inputs=make_dsv4_padded_replay_inputs,
prepare_inputs=prepare_dsv4_runner_inputs,
run_eager=run_dsv4_fixture_eager,
run_forward=run_dsv4_forward,
expected_output=expected_dsv4_output_from_inputs,
atol=DSV4_ATOL,
rtol=DSV4_RTOL,
)
_run_cuda_graph_decode_case(
testcase,
case,
adapter=adapter,
build_kwargs=dict(
swa_size=swa_size,
max_context_len=max_context_len,
dtype=dtype,
device=device,
),
capture_batch_size=cuda_graph_capture_batch_size,
max_context_len=max_context_len,
dtype=dtype,
device=device,
)
def run_gdn_cuda_graph_decode_case(
testcase,
case: GDNAttentionCase,
*,
head_k_dim: int = DEFAULT_HEAD_K_DIM,
head_v_dim: int = DEFAULT_HEAD_V_DIM,
max_context_len: int = GDN_DEFAULT_MAX_CONTEXT_LEN,
dtype: torch.dtype = GDN_DEFAULT_DTYPE,
device: str = GDN_DEFAULT_DEVICE,
cuda_graph_capture_batch_size: int = GDN_CUDA_GRAPH_CAPTURE_BATCH_SIZE,
):
adapter = CudaGraphDecodeAdapter(
build_fixture=build_gdn_attention_fixture,
make_case=make_gdn_case_with_prefix_lens,
make_forward_batch=_make_gdn_forward_batch,
fixture_inputs=gdn_fixture_inputs,
make_capture_inputs=make_gdn_random_inputs,
make_replay_inputs=make_gdn_replay_inputs,
prepare_inputs=prepare_gdn_runner_inputs,
run_eager=run_gdn_fixture_eager,
run_forward=run_gdn_forward,
expected_output=expected_gdn_output_from_inputs,
clone_state=_clone_gdn_cache,
restore_state=_restore_gdn_cache,
allow_padding=False,
atol=GDN_ATOL,
rtol=GDN_RTOL,
)
_run_cuda_graph_decode_case(
testcase,
case,
adapter=adapter,
build_kwargs=dict(
head_k_dim=head_k_dim,
head_v_dim=head_v_dim,
max_context_len=max_context_len,
dtype=dtype,
device=device,
),
capture_batch_size=cuda_graph_capture_batch_size,
max_context_len=max_context_len,
dtype=dtype,
device=device,
)
def run_kda_cuda_graph_decode_case(
testcase,
case: KDAAttentionCase,
*,
head_k_dim: int = KDA_DEFAULT_HEAD_K_DIM,
head_v_dim: int = KDA_DEFAULT_HEAD_V_DIM,
max_context_len: int = KDA_DEFAULT_MAX_CONTEXT_LEN,
dtype: torch.dtype = KDA_DEFAULT_DTYPE,
device: str = KDA_DEFAULT_DEVICE,
cuda_graph_capture_batch_size: int = KDA_CUDA_GRAPH_CAPTURE_BATCH_SIZE,
):
"""KDA CUDA-graph decode replay. Mirrors `run_gdn_cuda_graph_decode_case`:
KDA inherits the same `MambaAttnBackendBase` capture/replay path through
`HybridLinearAttnBackend`, so the adapter wiring is identical to GDN.
Only DECODE / TARGET_VERIFY are reachable here (the underlying
`_replay_metadata` rejects other modes — see kda/README.md).
"""
adapter = CudaGraphDecodeAdapter(
build_fixture=build_kda_attention_fixture,
make_case=make_kda_case_with_prefix_lens,
make_forward_batch=_make_kda_forward_batch,
fixture_inputs=kda_fixture_inputs,
make_capture_inputs=make_kda_random_inputs,
make_replay_inputs=make_kda_replay_inputs,
prepare_inputs=prepare_kda_runner_inputs,
run_eager=run_kda_fixture_eager,
run_forward=run_kda_forward,
expected_output=expected_kda_output_from_inputs,
clone_state=_clone_kda_cache,
restore_state=_restore_kda_cache,
allow_padding=False,
atol=KDA_GRAPH_ATOL,
rtol=KDA_GRAPH_RTOL,
)
_run_cuda_graph_decode_case(
testcase,
case,
adapter=adapter,
build_kwargs=dict(
head_k_dim=head_k_dim,
head_v_dim=head_v_dim,
max_context_len=max_context_len,
dtype=dtype,
device=device,
),
capture_batch_size=cuda_graph_capture_batch_size,
max_context_len=max_context_len,
dtype=dtype,
device=device,
)
def run_lightning_cuda_graph_decode_case(
testcase,
case: LightningAttentionCase,
*,
head_dim: int = LIGHTNING_DEFAULT_HEAD_DIM,
max_context_len: int = LIGHTNING_DEFAULT_MAX_CONTEXT_LEN,
dtype: torch.dtype = LIGHTNING_DEFAULT_DTYPE,
device: str = LIGHTNING_DEFAULT_DEVICE,
cuda_graph_capture_batch_size: int = LIGHTNING_CUDA_GRAPH_CAPTURE_BATCH_SIZE,
):
"""Lightning (Bailing seg_la) CUDA-graph decode replay. Mirrors GDN/KDA;
Lightning uses `LightningAttentionBackend` (installed directly via
ForwardContext rather than through `HybridLinearAttnBackend`), but the
capture/replay contract is the same shape because the backend also
inherits from `MambaAttnBackendBase`. Loose tolerance to absorb seg_la
Triton kernel CG-replay drift; eager tolerance preserved for non-graph
cases."""
adapter = CudaGraphDecodeAdapter(
build_fixture=build_lightning_attention_fixture,
make_case=make_lightning_case_with_prefix_lens,
make_forward_batch=_make_lightning_forward_batch,
fixture_inputs=lightning_fixture_inputs,
make_capture_inputs=make_lightning_random_inputs,
make_replay_inputs=make_lightning_replay_inputs,
prepare_inputs=prepare_lightning_runner_inputs,
run_eager=run_lightning_fixture_eager,
run_forward=run_lightning_forward,
expected_output=expected_lightning_output_from_inputs,
clone_state=_clone_lightning_cache,
restore_state=_restore_lightning_cache,
allow_padding=False,
atol=LIGHTNING_GRAPH_ATOL,
rtol=LIGHTNING_GRAPH_RTOL,
)
_run_cuda_graph_decode_case(
testcase,
case,
adapter=adapter,
build_kwargs=dict(
head_dim=head_dim,
max_context_len=max_context_len,
dtype=dtype,
device=device,
),
capture_batch_size=cuda_graph_capture_batch_size,
max_context_len=max_context_len,
dtype=dtype,
device=device,
)
def run_mamba2_cuda_graph_decode_case(
testcase,
case: Mamba2AttentionCase,
*,
max_context_len: int = MAMBA2_DEFAULT_MAX_CONTEXT_LEN,
dtype: torch.dtype = MAMBA2_DEFAULT_DTYPE,
device: str = MAMBA2_DEFAULT_DEVICE,
cuda_graph_capture_batch_size: int = MAMBA2_CUDA_GRAPH_CAPTURE_BATCH_SIZE,
):
"""Mamba2 CUDA-graph decode replay. The fixture's
`initialize_mamba_selective_state_update_backend` call makes
`MambaMixer2.forward_decode` reachable; this adapter then drives the
capture/replay lifecycle the same way as GDN/KDA/Lightning, snapshotting
both SSM and conv state between capture and replay so the recurrent
backend output is reproducible.
Loose `MAMBA2_GRAPH_ATOL=1e-1` absorbs CG-replay drift; eager
`MAMBA2_ATOL=5e-2` is kept for non-graph cases.
"""
adapter = CudaGraphDecodeAdapter(
build_fixture=build_mamba2_attention_fixture,
make_case=make_mamba2_case_with_prefix_lens,
make_forward_batch=_make_mamba2_forward_batch,
fixture_inputs=mamba2_fixture_inputs,
make_capture_inputs=make_mamba2_random_inputs,
make_replay_inputs=make_mamba2_replay_inputs,
prepare_inputs=prepare_mamba2_runner_inputs,
run_eager=run_mamba2_fixture_eager,
run_forward=run_mamba2_forward,
expected_output=expected_mamba2_output_from_inputs,
clone_state=_clone_mamba2_cache,
restore_state=_restore_mamba2_cache,
allow_padding=False,
atol=MAMBA2_GRAPH_ATOL,
rtol=MAMBA2_GRAPH_RTOL,
)
_run_cuda_graph_decode_case(
testcase,
case,
adapter=adapter,
build_kwargs=dict(
max_context_len=max_context_len,
dtype=dtype,
device=device,
),
capture_batch_size=cuda_graph_capture_batch_size,
max_context_len=max_context_len,
dtype=dtype,
device=device,
)
def _run_dsa_sparse_eager_for_cg(fixture):
"""Eager wrapper for the DSA sparse CG decode adapter — wraps a
`forward_context` around `run_dsa_sparse_forward` so `module.attn`
sees the active backend (the existing
`run_dsa_sparse_fixture_eager` has its own context but takes an
extra `testcase` arg for `skipTest`, which doesn't fit the
adapter's `run_eager(fixture)` signature)."""
with torch.no_grad(), forward_context(ForwardContext(attn_backend=fixture.backend)):
fixture.backend.init_forward_metadata(fixture.forward_batch)
return run_dsa_sparse_forward(
fixture, fixture.forward_batch, dsa_sparse_fixture_inputs(fixture)
)
def run_dsa_sparse_cuda_graph_decode_case(
testcase,
case: DSAAttentionCase,
*,
hidden_size: int = DEFAULT_HIDDEN_SIZE,
max_context_len: int | None = None,
dtype: torch.dtype = torch.bfloat16,
device: str = DENSE_DEFAULT_DEVICE,
cuda_graph_capture_batch_size: int | None = None,
dsa_decode_backend: str = "flashmla_kv",
fp8_kv_cache: bool = False,
):
"""DSA sparse-topk CUDA-graph decode replay (`flashmla_kv` path).
Sparse decode uses cached MLA latent KV (written by
`_populate_dsa_sparse_prefix_kv` at fixture build), so the
capture/replay K-cache boundary is compatible with piecewise CG —
unlike the dense-fallback MHA_ONE_SHOT path which passes prefix+
extend K inline."""
if not case.forward_mode.is_decode():
raise ValueError(
"run_dsa_sparse_cuda_graph_decode_case expects a DECODE case "
"(the sparse `flashmla_kv` path is the natural CG decode target)."
)
capture_batch_size = cuda_graph_capture_batch_size or case.batch_size
if max_context_len is None:
max_context_len = max(case.seq_lens) if case.seq_lens else DSA_PAGE_SIZE
# Round up to page_size multiple.
if max_context_len % case.page_size:
max_context_len = (
(max_context_len + case.page_size - 1) // case.page_size
) * case.page_size
from ..attention_methods.dsa_attention import (
DSA_SPARSE_FP8_ATOL,
DSA_SPARSE_FP8_RTOL,
)
if fp8_kv_cache:
atol, rtol = DSA_SPARSE_FP8_ATOL, DSA_SPARSE_FP8_RTOL
else:
atol, rtol = DSA_SPARSE_ATOL, DSA_SPARSE_RTOL
adapter = CudaGraphDecodeAdapter(
build_fixture=build_dsa_sparse_attention_fixture,
make_case=make_dsa_sparse_case_with_prefix_lens,
make_forward_batch=_make_dsa_forward_batch,
fixture_inputs=dsa_sparse_fixture_inputs,
make_capture_inputs=make_dsa_sparse_random_inputs,
make_replay_inputs=make_dsa_sparse_replay_inputs,
prepare_inputs=prepare_dsa_sparse_runner_inputs,
run_eager=_run_dsa_sparse_eager_for_cg,
run_forward=run_dsa_sparse_forward,
expected_output=expected_dsa_sparse_output_from_inputs,
clone_state=_clone_dsa_sparse_cache,
restore_state=_restore_dsa_sparse_cache,
allow_padding=False,
atol=atol,
rtol=rtol,
)
_run_cuda_graph_decode_case(
testcase,
case,
adapter=adapter,
build_kwargs=dict(
hidden_size=hidden_size,
max_context_len=max_context_len,
dtype=dtype,
device=device,
dsa_decode_backend=dsa_decode_backend,
fp8_kv_cache=fp8_kv_cache,
),
capture_batch_size=capture_batch_size,
max_context_len=max_context_len,
dtype=dtype,
device=device,
)
def run_dual_chunk_cuda_graph_decode_case(
testcase,
case: DualChunkAttentionCase,
*,
head_dim: int = DEFAULT_HEAD_DIM,
hidden_size: int = DEFAULT_HIDDEN_SIZE,
max_context_len: int = DENSE_DEFAULT_MAX_CONTEXT_LEN,
dtype: torch.dtype = DENSE_DEFAULT_DTYPE,
device: str = DENSE_DEFAULT_DEVICE,
cuda_graph_capture_batch_size: int | None = None,
):
"""Dual-chunk CUDA-graph decode replay. Decode reads cached K/V (set
by `set_kv_buffer` inside `forward_decode`) so the capture/replay
contract is the same shape as dense attention. The
`_clone_dual_chunk_cache` / `_restore_dual_chunk_cache` hooks snapshot
both K and V buffers so the capture forward's writes don't bleed into
replay state."""
if not case.forward_mode.is_decode():
raise ValueError("run_dual_chunk_cuda_graph_decode_case expects a DECODE case.")
capture_batch_size = cuda_graph_capture_batch_size or case.batch_size
adapter = CudaGraphDecodeAdapter(
build_fixture=build_dual_chunk_attention_fixture,
make_case=make_dual_chunk_case_with_prefix_lens,
make_forward_batch=_make_dense_forward_batch,
fixture_inputs=dual_chunk_fixture_inputs,
make_capture_inputs=make_dual_chunk_random_inputs,
make_replay_inputs=make_dual_chunk_replay_inputs,
prepare_inputs=prepare_dual_chunk_runner_inputs,
run_eager=run_dual_chunk_fixture_eager,
run_forward=run_dual_chunk_forward,
expected_output=expected_dual_chunk_output_from_inputs,
clone_state=_clone_dual_chunk_cache,
restore_state=_restore_dual_chunk_cache,
allow_padding=True,
atol=DENSE_ATOL,
rtol=DENSE_RTOL,
)
_run_cuda_graph_decode_case(
testcase,
case,
adapter=adapter,
build_kwargs=dict(
head_dim=head_dim,
hidden_size=hidden_size,
max_context_len=max_context_len,
dtype=dtype,
device=device,
),
capture_batch_size=capture_batch_size,
max_context_len=max_context_len,
dtype=dtype,
device=device,
)
@@ -0,0 +1,224 @@
from dataclasses import dataclass
from typing import Any, Callable
import torch
from sglang.srt.model_executor.forward_context import ForwardContext, forward_context
from .cuda_graph_decode_runner import (
_init_cuda_graph_capture_metadata,
_init_cuda_graph_replay_metadata,
)
@dataclass(frozen=True)
class SpeculativeCudaGraphAdapter:
build_fixture: Callable[..., Any]
make_capture_case: Callable[[Any, str, int, int], Any]
make_replay_case: Callable[[Any, str, tuple[int, ...]], Any]
make_forward_batch: Callable[..., Any]
fixture_inputs: Callable[[Any], dict[str, Any]]
make_capture_inputs: Callable[..., dict[str, Any]]
make_replay_inputs: Callable[..., dict[str, Any]]
prepare_batch: Callable[[Any, Any], None]
prepare_inputs: Callable[..., None]
run_forward: Callable[[Any, Any, dict[str, Any]], torch.Tensor]
expected_output: Callable[[Any, Any, dict[str, Any], Any], torch.Tensor]
max_num_tokens: Callable[[Any, int], int] | None = None
clone_state: Callable[[Any], Any] = lambda _: None
restore_state: Callable[[Any, Any], None] = lambda _fixture, _state: None
allow_padding: bool = True
run_graph_eager: bool = True
compare_replay_to_graph_eager: bool = True
atol: float = 0.0
rtol: float = 0.0
def _check_speculative_cuda_graph_case(
case,
capture_batch_size: int,
*,
allow_padding: bool,
) -> None:
if allow_padding:
if case.batch_size > capture_batch_size:
raise ValueError("CUDA graph capture must cover replay batch size.")
elif case.batch_size != capture_batch_size:
raise ValueError(
"This CUDA graph coverage uses an unpadded replay batch; choose a case "
"whose batch size matches the capture batch size."
)
def run_speculative_cuda_graph_case(
testcase,
case,
*,
adapter: SpeculativeCudaGraphAdapter,
build_kwargs: dict,
capture_batch_size: int,
max_context_len: int,
dtype: torch.dtype,
device: str,
):
_check_speculative_cuda_graph_case(
case,
capture_batch_size,
allow_padding=adapter.allow_padding,
)
graph_fixture = adapter.build_fixture(
testcase,
case,
**build_kwargs,
disable_cuda_graph=False,
runner_batch_size=capture_batch_size,
)
backend = graph_fixture.backend
graph_inputs = adapter.fixture_inputs(graph_fixture)
graph_initial_state = adapter.clone_state(graph_fixture)
graph_eager_actual = None
if adapter.run_graph_eager:
if adapter.max_num_tokens is not None:
backend.init_cuda_graph_state(
max_bs=capture_batch_size,
max_num_tokens=adapter.max_num_tokens(case, capture_batch_size),
)
graph_batch = graph_fixture.forward_batch
adapter.prepare_batch(case, graph_batch)
# Run prepare_inputs in the eager leg too so backends whose reference
# depends on cache state / per-fixture stashes (e.g. DSV4 reads BF16
# K from `fixture._swa_bf16_k_per_req`, populated by
# `prepare_dsv4_runner_inputs`) work the same way as the
# capture/replay legs. Backends whose reference is self-contained
# (dense / MLA — they re-project from `inputs`) are unaffected;
# `prepare_inputs` just re-writes the SWA cache.
adapter.prepare_inputs(
graph_fixture,
case,
graph_batch,
graph_inputs,
max_context_len=max_context_len,
)
graph_expected = adapter.expected_output(
graph_fixture,
case,
graph_inputs,
graph_initial_state,
)
with torch.no_grad(), forward_context(ForwardContext(attn_backend=backend)):
backend.init_forward_metadata(graph_batch)
graph_eager_actual = adapter.run_forward(
graph_fixture,
graph_batch,
graph_inputs,
)
torch.testing.assert_close(
graph_eager_actual,
graph_expected,
atol=adapter.atol,
rtol=adapter.rtol,
)
capture_prefix_len = backend.get_cuda_graph_seq_len_fill_value()
capture_case = adapter.make_capture_case(
case,
f"{case.name}_cuda_graph_capture",
capture_prefix_len,
capture_batch_size,
)
capture_inputs = adapter.make_capture_inputs(
capture_case,
graph_fixture,
dtype=dtype,
device=device,
)
capture_batch = adapter.make_forward_batch(
capture_case,
graph_fixture.runner,
max_context_len=max_context_len,
device=device,
)
adapter.prepare_batch(capture_case, capture_batch)
adapter.prepare_inputs(
graph_fixture,
capture_case,
capture_batch,
capture_inputs,
max_context_len=max_context_len,
)
with torch.no_grad(), forward_context(ForwardContext(attn_backend=backend)):
_init_cuda_graph_capture_metadata(backend, capture_batch_size, capture_batch)
# Capture forward is a JIT warmup that mirrors production: the
# captured CUDA graph records kernel launches against buffers
# that *will* be populated by replay-init at replay. The
# capture-time output itself is discarded in production — and
# we discard it here too. Only the replay output is
# contractually required to match the reference.
adapter.run_forward(graph_fixture, capture_batch, capture_inputs)
backend.on_after_cuda_graph_warmup()
adapter.restore_state(graph_fixture, graph_initial_state)
replay_pad_prefix_lens = (
(capture_prefix_len,) * (capture_batch_size - case.batch_size)
if adapter.allow_padding
else ()
)
replay_case = adapter.make_replay_case(
case,
f"{case.name}_cuda_graph_replay",
replay_pad_prefix_lens,
)
replay_inputs = adapter.make_replay_inputs(
replay_case,
graph_fixture,
replay_pad_prefix_lens,
graph_inputs,
dtype=dtype,
device=device,
)
replay_batch = adapter.make_forward_batch(
replay_case,
graph_fixture.runner,
max_context_len=max_context_len,
device=device,
)
adapter.prepare_batch(replay_case, replay_batch)
adapter.prepare_inputs(
graph_fixture,
replay_case,
replay_batch,
replay_inputs,
max_context_len=max_context_len,
)
replay_expected = adapter.expected_output(
graph_fixture,
replay_case,
replay_inputs,
graph_initial_state,
)
with torch.no_grad(), forward_context(ForwardContext(attn_backend=backend)):
_init_cuda_graph_replay_metadata(backend, capture_batch_size, replay_batch)
replay_actual = adapter.run_forward(
graph_fixture,
replay_batch,
replay_inputs,
)
torch.testing.assert_close(
replay_actual,
replay_expected,
atol=adapter.atol,
rtol=adapter.rtol,
)
if adapter.compare_replay_to_graph_eager:
torch.testing.assert_close(
replay_actual[: case.num_input_tokens],
graph_eager_actual,
atol=adapter.atol,
rtol=adapter.rtol,
)
@@ -0,0 +1,600 @@
from dataclasses import dataclass, replace
from typing import Any, Callable
import torch
from sglang.srt.compilation.piecewise_context_manager import (
enable_piecewise_cuda_graph,
)
from sglang.srt.compilation.piecewise_context_manager import (
set_forward_context as piecewise_forward_context,
)
from sglang.srt.model_executor.breakable_cuda_graph.context import (
enable_breakable_cuda_graph,
)
from sglang.srt.model_executor.forward_context import ForwardContext, forward_context
from ..attention_methods.dense_attention import DEFAULT_DEVICE as DENSE_DEFAULT_DEVICE
from ..attention_methods.dense_attention import DEFAULT_DTYPE as DENSE_DEFAULT_DTYPE
from ..attention_methods.dense_attention import (
DEFAULT_HEAD_DIM,
DEFAULT_HIDDEN_SIZE,
)
from ..attention_methods.dense_attention import (
DEFAULT_MAX_CONTEXT_LEN as DENSE_DEFAULT_MAX_CONTEXT_LEN,
)
from ..attention_methods.dense_attention import (
DENSE_ATOL,
DENSE_RTOL,
DenseAttentionCase,
build_dense_attention_fixture,
dense_attention_layers,
dense_fixture_inputs,
expected_dense_output_from_inputs,
make_dense_token_padded_inputs,
prepare_dense_runner_inputs,
run_dense_fixture_eager,
run_dense_forward,
)
from ..attention_methods.gdn_attention import DEFAULT_DEVICE as GDN_DEFAULT_DEVICE
from ..attention_methods.gdn_attention import DEFAULT_DTYPE as GDN_DEFAULT_DTYPE
from ..attention_methods.gdn_attention import (
DEFAULT_HEAD_K_DIM,
DEFAULT_HEAD_V_DIM,
)
from ..attention_methods.gdn_attention import (
DEFAULT_MAX_CONTEXT_LEN as GDN_DEFAULT_MAX_CONTEXT_LEN,
)
from ..attention_methods.gdn_attention import (
GDN_ATOL,
GDN_RTOL,
GDNAttentionCase,
_clone_gdn_cache,
_restore_gdn_cache,
build_gdn_attention_fixture,
expected_gdn_output_from_inputs,
gdn_attention_layers,
gdn_fixture_inputs,
make_gdn_token_padded_inputs,
prepare_gdn_runner_inputs,
run_gdn_fixture_eager,
run_gdn_forward,
)
from ..attention_methods.kda_attention import DEFAULT_DEVICE as KDA_DEFAULT_DEVICE
from ..attention_methods.kda_attention import DEFAULT_DTYPE as KDA_DEFAULT_DTYPE
from ..attention_methods.kda_attention import (
DEFAULT_HEAD_K_DIM as KDA_DEFAULT_HEAD_K_DIM,
)
from ..attention_methods.kda_attention import (
DEFAULT_HEAD_V_DIM as KDA_DEFAULT_HEAD_V_DIM,
)
from ..attention_methods.kda_attention import (
DEFAULT_MAX_CONTEXT_LEN as KDA_DEFAULT_MAX_CONTEXT_LEN,
)
from ..attention_methods.kda_attention import (
KDA_ATOL,
KDA_RTOL,
KDAAttentionCase,
_clone_kda_cache,
_restore_kda_cache,
build_kda_attention_fixture,
expected_kda_output_from_inputs,
kda_attention_layers,
kda_fixture_inputs,
make_kda_token_padded_inputs,
prepare_kda_runner_inputs,
run_kda_fixture_eager,
run_kda_forward,
)
from ..attention_methods.lightning_attention import (
DEFAULT_DEVICE as LIGHTNING_DEFAULT_DEVICE,
)
from ..attention_methods.lightning_attention import (
DEFAULT_DTYPE as LIGHTNING_DEFAULT_DTYPE,
)
from ..attention_methods.lightning_attention import (
DEFAULT_HEAD_DIM as LIGHTNING_DEFAULT_HEAD_DIM,
)
from ..attention_methods.lightning_attention import (
DEFAULT_MAX_CONTEXT_LEN as LIGHTNING_DEFAULT_MAX_CONTEXT_LEN,
)
from ..attention_methods.lightning_attention import (
LIGHTNING_ATOL,
LIGHTNING_RTOL,
LightningAttentionCase,
_clone_lightning_cache,
_restore_lightning_cache,
build_lightning_attention_fixture,
expected_lightning_split_op_output_from_inputs,
lightning_attention_layers,
lightning_fixture_inputs,
make_lightning_token_padded_inputs,
prepare_lightning_runner_inputs,
run_lightning_fixture_eager,
run_lightning_forward,
)
from ..attention_methods.mamba2_attention import DEFAULT_DEVICE as MAMBA2_DEFAULT_DEVICE
from ..attention_methods.mamba2_attention import DEFAULT_DTYPE as MAMBA2_DEFAULT_DTYPE
from ..attention_methods.mamba2_attention import (
DEFAULT_MAX_CONTEXT_LEN as MAMBA2_DEFAULT_MAX_CONTEXT_LEN,
)
from ..attention_methods.mamba2_attention import (
MAMBA2_ATOL,
MAMBA2_RTOL,
Mamba2AttentionCase,
_clone_mamba2_cache,
_restore_mamba2_cache,
build_mamba2_attention_fixture,
expected_mamba2_output_from_inputs,
make_mamba2_token_padded_inputs,
mamba2_attention_layers,
mamba2_fixture_inputs,
prepare_mamba2_runner_inputs,
run_mamba2_fixture_eager,
run_mamba2_forward,
)
from ..attention_methods.mla_attention import DEFAULT_DEVICE as MLA_DEFAULT_DEVICE
from ..attention_methods.mla_attention import DEFAULT_DTYPE as MLA_DEFAULT_DTYPE
from ..attention_methods.mla_attention import (
DEFAULT_HIDDEN_SIZE as MLA_DEFAULT_HIDDEN_SIZE,
)
from ..attention_methods.mla_attention import (
DEFAULT_KV_LORA_RANK,
)
from ..attention_methods.mla_attention import (
DEFAULT_MAX_CONTEXT_LEN as MLA_DEFAULT_MAX_CONTEXT_LEN,
)
from ..attention_methods.mla_attention import (
DEFAULT_QK_ROPE_HEAD_DIM,
MLA_ATOL,
MLA_RTOL,
MLAAttentionCase,
build_mla_attention_fixture,
expected_mla_output_from_inputs,
make_mla_token_padded_inputs,
mla_attention_layers,
mla_fixture_inputs,
prepare_mla_runner_inputs,
run_mla_fixture_eager,
run_mla_forward,
)
@dataclass(frozen=True)
class SplitOpAdapter:
build_fixture: Callable[..., Any]
fixture_inputs: Callable[[Any], dict[str, Any]]
make_token_padded_inputs: Callable[..., dict[str, Any]]
prepare_inputs: Callable[..., None]
run_eager: Callable[[Any], torch.Tensor]
run_forward: Callable[[Any, Any, dict[str, Any]], torch.Tensor]
expected_output: Callable[[Any, Any, dict[str, Any], Any], torch.Tensor]
attention_layers: Callable[[Any], list[Any]]
clone_state: Callable[[Any], Any] = lambda _: None
restore_state: Callable[[Any, Any], None] = lambda _fixture, _state: None
atol: float = 0.0
rtol: float = 0.0
def _check_extend_split_op_case(case) -> None:
if not case.forward_mode.is_extend_without_speculative():
raise ValueError("PCG/BCG split-op coverage expects non-spec extend cases.")
def _split_op_context(*, breakable: bool):
if breakable:
return enable_breakable_cuda_graph()
return enable_piecewise_cuda_graph()
def _make_static_forward_batch(raw_batch, static_num_tokens: int, device: str):
raw_num_tokens = raw_batch.input_ids.numel()
if static_num_tokens < raw_num_tokens:
raise ValueError("static_num_tokens must cover the live input token count.")
if static_num_tokens == raw_num_tokens:
input_ids = raw_batch.input_ids
positions = raw_batch.positions
out_cache_loc = raw_batch.out_cache_loc
else:
pad_tokens = static_num_tokens - raw_num_tokens
input_ids = torch.cat(
[
raw_batch.input_ids,
torch.zeros(pad_tokens, dtype=raw_batch.input_ids.dtype, device=device),
],
dim=0,
)
positions = torch.cat(
[
raw_batch.positions,
torch.zeros(pad_tokens, dtype=raw_batch.positions.dtype, device=device),
],
dim=0,
)
out_cache_loc = torch.cat(
[
raw_batch.out_cache_loc,
torch.zeros(
pad_tokens,
dtype=raw_batch.out_cache_loc.dtype,
device=device,
),
],
dim=0,
)
raw_batch.num_token_non_padded_cpu = raw_num_tokens
return replace(
raw_batch,
input_ids=input_ids,
positions=positions,
out_cache_loc=out_cache_loc,
padded_static_len=static_num_tokens,
num_token_non_padded_cpu=raw_num_tokens,
)
def _slice_live_tokens(output: torch.Tensor, num_tokens: int) -> torch.Tensor:
if output.dim() >= 2 and output.shape[0] == 1:
return output[:, :num_tokens]
return output[:num_tokens]
def _run_split_op_extend_case(
testcase,
case,
*,
adapter: SplitOpAdapter,
build_kwargs: dict[str, Any],
max_context_len: int,
dtype: torch.dtype,
device: str,
breakable: bool,
static_num_tokens: int | None,
):
_check_extend_split_op_case(case)
eager_fixture = adapter.build_fixture(testcase, case, **build_kwargs)
eager_inputs = adapter.fixture_inputs(eager_fixture)
eager_initial_state = adapter.clone_state(eager_fixture)
eager_actual = adapter.run_eager(eager_fixture)
eager_expected = adapter.expected_output(
eager_fixture,
case,
eager_inputs,
eager_initial_state,
)
torch.testing.assert_close(
eager_actual,
eager_expected,
atol=adapter.atol,
rtol=adapter.rtol,
)
split_fixture = adapter.build_fixture(
testcase,
case,
**build_kwargs,
disable_piecewise_cuda_graph=False,
)
split_inputs = adapter.fixture_inputs(split_fixture)
split_initial_state = adapter.clone_state(split_fixture)
expected = adapter.expected_output(
split_fixture,
case,
split_inputs,
split_initial_state,
)
raw_batch = split_fixture.forward_batch
raw_num_tokens = case.num_input_tokens
static_num_tokens = static_num_tokens or raw_num_tokens
static_batch = _make_static_forward_batch(raw_batch, static_num_tokens, device)
static_inputs = adapter.make_token_padded_inputs(
case,
split_fixture,
static_num_tokens,
split_inputs,
dtype=dtype,
device=device,
)
adapter.prepare_inputs(
split_fixture,
case,
raw_batch,
split_inputs,
max_context_len=max_context_len,
)
with (
torch.no_grad(),
_split_op_context(breakable=breakable),
forward_context(ForwardContext(attn_backend=split_fixture.backend)),
piecewise_forward_context(
static_batch,
adapter.attention_layers(split_fixture),
None,
[],
[],
),
):
split_fixture.backend.init_forward_metadata(raw_batch)
actual = adapter.run_forward(split_fixture, static_batch, static_inputs)
actual = _slice_live_tokens(actual, raw_num_tokens)
torch.testing.assert_close(actual, expected, atol=adapter.atol, rtol=adapter.rtol)
torch.testing.assert_close(
actual,
eager_actual,
atol=adapter.atol,
rtol=adapter.rtol,
)
adapter.restore_state(split_fixture, split_initial_state)
def run_dense_split_op_extend_case(
testcase,
case: DenseAttentionCase,
*,
breakable: bool,
static_num_tokens: int | None = None,
head_dim: int = DEFAULT_HEAD_DIM,
hidden_size: int = DEFAULT_HIDDEN_SIZE,
max_context_len: int = DENSE_DEFAULT_MAX_CONTEXT_LEN,
dtype: torch.dtype = DENSE_DEFAULT_DTYPE,
device: str = DENSE_DEFAULT_DEVICE,
):
adapter = SplitOpAdapter(
build_fixture=build_dense_attention_fixture,
fixture_inputs=dense_fixture_inputs,
make_token_padded_inputs=make_dense_token_padded_inputs,
prepare_inputs=prepare_dense_runner_inputs,
run_eager=run_dense_fixture_eager,
run_forward=run_dense_forward,
expected_output=expected_dense_output_from_inputs,
attention_layers=dense_attention_layers,
atol=DENSE_ATOL,
rtol=DENSE_RTOL,
)
_run_split_op_extend_case(
testcase,
case,
adapter=adapter,
build_kwargs=dict(
head_dim=head_dim,
hidden_size=hidden_size,
max_context_len=max_context_len,
dtype=dtype,
device=device,
),
max_context_len=max_context_len,
dtype=dtype,
device=device,
breakable=breakable,
static_num_tokens=static_num_tokens,
)
def run_mla_split_op_extend_case(
testcase,
case: MLAAttentionCase,
*,
breakable: bool,
static_num_tokens: int | None = None,
kv_lora_rank: int = DEFAULT_KV_LORA_RANK,
qk_rope_head_dim: int = DEFAULT_QK_ROPE_HEAD_DIM,
hidden_size: int = MLA_DEFAULT_HIDDEN_SIZE,
max_context_len: int = MLA_DEFAULT_MAX_CONTEXT_LEN,
dtype: torch.dtype = MLA_DEFAULT_DTYPE,
device: str = MLA_DEFAULT_DEVICE,
):
adapter = SplitOpAdapter(
build_fixture=build_mla_attention_fixture,
fixture_inputs=mla_fixture_inputs,
make_token_padded_inputs=make_mla_token_padded_inputs,
prepare_inputs=prepare_mla_runner_inputs,
run_eager=run_mla_fixture_eager,
run_forward=run_mla_forward,
expected_output=expected_mla_output_from_inputs,
attention_layers=mla_attention_layers,
atol=MLA_ATOL,
rtol=MLA_RTOL,
)
_run_split_op_extend_case(
testcase,
case,
adapter=adapter,
build_kwargs=dict(
kv_lora_rank=kv_lora_rank,
qk_rope_head_dim=qk_rope_head_dim,
hidden_size=hidden_size,
max_context_len=max_context_len,
dtype=dtype,
device=device,
),
max_context_len=max_context_len,
dtype=dtype,
device=device,
breakable=breakable,
static_num_tokens=static_num_tokens,
)
def run_gdn_split_op_extend_case(
testcase,
case: GDNAttentionCase,
*,
breakable: bool,
static_num_tokens: int | None = None,
head_k_dim: int = DEFAULT_HEAD_K_DIM,
head_v_dim: int = DEFAULT_HEAD_V_DIM,
max_context_len: int = GDN_DEFAULT_MAX_CONTEXT_LEN,
dtype: torch.dtype = GDN_DEFAULT_DTYPE,
device: str = GDN_DEFAULT_DEVICE,
):
adapter = SplitOpAdapter(
build_fixture=build_gdn_attention_fixture,
fixture_inputs=gdn_fixture_inputs,
make_token_padded_inputs=make_gdn_token_padded_inputs,
prepare_inputs=prepare_gdn_runner_inputs,
run_eager=run_gdn_fixture_eager,
run_forward=run_gdn_forward,
expected_output=expected_gdn_output_from_inputs,
attention_layers=gdn_attention_layers,
clone_state=_clone_gdn_cache,
restore_state=_restore_gdn_cache,
atol=GDN_ATOL,
rtol=GDN_RTOL,
)
_run_split_op_extend_case(
testcase,
case,
adapter=adapter,
build_kwargs=dict(
head_k_dim=head_k_dim,
head_v_dim=head_v_dim,
max_context_len=max_context_len,
dtype=dtype,
device=device,
),
max_context_len=max_context_len,
dtype=dtype,
device=device,
breakable=breakable,
static_num_tokens=static_num_tokens,
)
def run_kda_split_op_extend_case(
testcase,
case: KDAAttentionCase,
*,
breakable: bool,
static_num_tokens: int | None = None,
head_k_dim: int = KDA_DEFAULT_HEAD_K_DIM,
head_v_dim: int = KDA_DEFAULT_HEAD_V_DIM,
max_context_len: int = KDA_DEFAULT_MAX_CONTEXT_LEN,
dtype: torch.dtype = KDA_DEFAULT_DTYPE,
device: str = KDA_DEFAULT_DEVICE,
):
"""KDA PCG/BCG split-op extend. Verifies the live-token slicing contract
with a larger static token buffer, mirroring GDN's split_op coverage."""
adapter = SplitOpAdapter(
build_fixture=build_kda_attention_fixture,
fixture_inputs=kda_fixture_inputs,
make_token_padded_inputs=make_kda_token_padded_inputs,
prepare_inputs=prepare_kda_runner_inputs,
run_eager=run_kda_fixture_eager,
run_forward=run_kda_forward,
expected_output=expected_kda_output_from_inputs,
attention_layers=kda_attention_layers,
clone_state=_clone_kda_cache,
restore_state=_restore_kda_cache,
atol=KDA_ATOL,
rtol=KDA_RTOL,
)
_run_split_op_extend_case(
testcase,
case,
adapter=adapter,
build_kwargs=dict(
head_k_dim=head_k_dim,
head_v_dim=head_v_dim,
max_context_len=max_context_len,
dtype=dtype,
device=device,
),
max_context_len=max_context_len,
dtype=dtype,
device=device,
breakable=breakable,
static_num_tokens=static_num_tokens,
)
def run_lightning_split_op_extend_case(
testcase,
case: LightningAttentionCase,
*,
breakable: bool,
static_num_tokens: int | None = None,
head_dim: int = LIGHTNING_DEFAULT_HEAD_DIM,
max_context_len: int = LIGHTNING_DEFAULT_MAX_CONTEXT_LEN,
dtype: torch.dtype = LIGHTNING_DEFAULT_DTYPE,
device: str = LIGHTNING_DEFAULT_DEVICE,
):
"""Lightning PCG/BCG split-op extend. Same pattern as KDA/GDN."""
adapter = SplitOpAdapter(
build_fixture=build_lightning_attention_fixture,
fixture_inputs=lightning_fixture_inputs,
make_token_padded_inputs=make_lightning_token_padded_inputs,
prepare_inputs=prepare_lightning_runner_inputs,
run_eager=run_lightning_fixture_eager,
run_forward=run_lightning_forward,
expected_output=expected_lightning_split_op_output_from_inputs,
attention_layers=lightning_attention_layers,
clone_state=_clone_lightning_cache,
restore_state=_restore_lightning_cache,
atol=LIGHTNING_ATOL,
rtol=LIGHTNING_RTOL,
)
_run_split_op_extend_case(
testcase,
case,
adapter=adapter,
build_kwargs=dict(
head_dim=head_dim,
max_context_len=max_context_len,
dtype=dtype,
device=device,
),
max_context_len=max_context_len,
dtype=dtype,
device=device,
breakable=breakable,
static_num_tokens=static_num_tokens,
)
def run_mamba2_split_op_extend_case(
testcase,
case: Mamba2AttentionCase,
*,
breakable: bool,
static_num_tokens: int | None = None,
max_context_len: int = MAMBA2_DEFAULT_MAX_CONTEXT_LEN,
dtype: torch.dtype = MAMBA2_DEFAULT_DTYPE,
device: str = MAMBA2_DEFAULT_DEVICE,
):
"""Mamba2 PCG/BCG split-op extend. Same pattern as KDA. Mamba2's
forward writes through an `empty_like(hidden_states)` buffer that
short-circuits the RadixAttention dispatch path, so the per-head-vs-flat
shape mismatch that blocks Lightning split-op doesn't apply."""
adapter = SplitOpAdapter(
build_fixture=build_mamba2_attention_fixture,
fixture_inputs=mamba2_fixture_inputs,
make_token_padded_inputs=make_mamba2_token_padded_inputs,
prepare_inputs=prepare_mamba2_runner_inputs,
run_eager=run_mamba2_fixture_eager,
run_forward=run_mamba2_forward,
expected_output=expected_mamba2_output_from_inputs,
attention_layers=mamba2_attention_layers,
clone_state=_clone_mamba2_cache,
restore_state=_restore_mamba2_cache,
atol=MAMBA2_ATOL,
rtol=MAMBA2_RTOL,
)
_run_split_op_extend_case(
testcase,
case,
adapter=adapter,
build_kwargs=dict(
max_context_len=max_context_len,
dtype=dtype,
device=device,
),
max_context_len=max_context_len,
dtype=dtype,
device=device,
breakable=breakable,
static_num_tokens=static_num_tokens,
)