Files
lightseekorg--tokenspeed/test/runtime/test_flat_cudagraph_per_group.py
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

722 lines
28 KiB
Python

"""M10: flat per-group CUDA-graph pad / capture / replay core-logic tests.
CPU-only (plain tensors, no graph capture): covers the wrapper's flat
placeholder + padding helpers and the MHA backend's flat capture/replay
branches. Graph runtime semantics (pointer-fixed replay) are validated
separately on GPU via the P0 probe.
"""
from __future__ import annotations
import os
import sys
import unittest
from types import SimpleNamespace
# CI Registration (parsed via AST, runtime no-op)
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ci_system.ci_register import register_cuda_ci
register_cuda_ci(est_time=10, suite="runtime-1gpu")
MAX_BS = 4
MAX_NUM_PAGES = 6
def _decode_forward_mode():
return SimpleNamespace(is_extend_or_mixed=lambda: False)
class _TorchCase(unittest.TestCase):
def setUp(self):
try:
import torch
except (ImportError, ModuleNotFoundError) as exc:
self.skipTest(f"needs torch: {exc}")
self.torch = torch
class PadBlockTablesTest(_TorchCase):
def setUp(self):
super().setUp()
from tokenspeed.runtime.execution.cuda_graph_wrapper import (
CudaGraphWrapper,
)
self.pad = CudaGraphWrapper._pad_block_tables_to_padded_bs
def _tables(self):
torch = self.torch
return {
"full_attention": torch.arange(6, dtype=torch.int32).reshape(2, 3),
"sliding_attention": torch.ones((2, 3), dtype=torch.int32),
}
def test_default_pads_tail_rows_with_minus_one(self):
# Radix/V4 path keeps -1 dummy rows: the backend masks dummy tokens
# via is_valid_token before any block-table read.
tables = self._tables()
out = self.pad(tables, actual_bs=2, padded_bs=4)
for gid, src in tables.items():
self.assertEqual(tuple(out[gid].shape), (4, 3))
self.assertTrue((out[gid][:2] == src).all())
self.assertTrue((out[gid][2:] == -1).all())
def test_flat_pads_tail_rows_with_zero(self):
# Flat path passes pad_value=0: dummy rows replay with seq_lens=1 and
# ARE dereferenced, so they must land on the zero-init dummy page 0.
tables = self._tables()
out = self.pad(tables, actual_bs=2, padded_bs=4, pad_value=0)
for gid, src in tables.items():
self.assertEqual(tuple(out[gid].shape), (4, 3))
self.assertTrue((out[gid][:2] == src).all())
self.assertTrue((out[gid][2:] == 0).all())
def test_noop_when_bs_equal(self):
torch = self.torch
tables = {"full_attention": torch.ones((3, 2), dtype=torch.int32)}
out = self.pad(tables, actual_bs=3, padded_bs=3)
self.assertIs(out["full_attention"], tables["full_attention"])
class FlatCacheGroupIdsTest(_TorchCase):
"""Wrapper-side capture contract: group ids only, no fabricated tensors."""
def setUp(self):
super().setUp()
from tokenspeed.runtime.execution.cuda_graph_wrapper import (
CudaGraphWrapper,
)
self.group_ids = CudaGraphWrapper._flat_cache_group_ids
def _wrapper(self, uses_flat=True):
return SimpleNamespace(
attn_backend=SimpleNamespace(uses_flat_cache_groups=uses_flat),
)
def _pool(self, group_ids):
return SimpleNamespace(
paged_cache_group_specs=tuple(
SimpleNamespace(group_id=gid) for gid in group_ids
)
)
def test_ids_in_spec_order(self):
out = self.group_ids(
self._wrapper(),
self._pool(["sliding_attention", "full_attention"]),
)
self.assertEqual(out, ("sliding_attention", "full_attention"))
def test_empty_without_specs(self):
self.assertEqual(self.group_ids(self._wrapper(), self._pool([])), ())
def test_empty_when_backend_not_flat(self):
out = self.group_ids(
self._wrapper(uses_flat=False), self._pool(["full_attention"])
)
self.assertEqual(out, ())
class WrapperReplayFlatTest(_TorchCase):
"""Call-site wiring: the real _init_replay_metadata must row-pad flat
tables with 0 (not the -1 default) before handing them to the backend."""
def _run_replay(self, flat_block_tables, padded_bs, actual_bs):
torch = self.torch
from tokenspeed.runtime.execution.cuda_graph_wrapper import (
CudaGraphWrapper,
)
recorded = {}
def record(bs, req_pool_indices, seq_lens, **kwargs):
recorded["bs"] = bs
recorded.update(kwargs)
mock = SimpleNamespace(
attn_backend=SimpleNamespace(
uses_flat_cache_groups=True,
uses_paged_cache_groups=False,
uses_padded_decode_token_mask=False,
init_forward_metadata_replay_cuda_graph=record,
),
draft_attn_backend=None,
# Production helper, so the pinned pad_value is the real one.
_pad_block_tables_to_padded_bs=(
CudaGraphWrapper._pad_block_tables_to_padded_bs
),
)
CudaGraphWrapper._init_replay_metadata(
mock,
padded_bs,
actual_bs,
torch.arange(padded_bs, dtype=torch.int64),
torch.ones(padded_bs, dtype=torch.int32),
torch.zeros((MAX_BS, MAX_NUM_PAGES), dtype=torch.int32),
_decode_forward_mode(),
flat_block_tables=flat_block_tables,
)
return recorded
def test_flat_replay_path_pads_with_zero(self):
torch = self.torch
src = {
"sliding_attention": torch.tensor([[3, 4], [5, 6]], dtype=torch.int32),
"full_attention": torch.tensor([[7, 8], [9, 1]], dtype=torch.int32),
}
recorded = self._run_replay(src, padded_bs=4, actual_bs=2)
self.assertEqual(recorded["bs"], 4)
out = recorded["flat_block_tables"]
self.assertEqual(set(out), set(src))
for gid, table in out.items():
self.assertEqual(tuple(table.shape), (4, 2))
self.assertTrue((table[:2] == src[gid]).all())
# Dummy rows must land on the zero-init dummy page 0, never -1:
# they replay with seq_lens=1 and their col-0 IS dereferenced.
self.assertTrue((table[2:] == 0).all())
def test_flat_replay_path_noop_without_padding(self):
torch = self.torch
src = {"full_attention": torch.ones((2, 2), dtype=torch.int32)}
recorded = self._run_replay(src, padded_bs=2, actual_bs=2)
self.assertIs(
recorded["flat_block_tables"]["full_attention"],
src["full_attention"],
)
class WrapperCaptureFlatGroupIdsTest(_TorchCase):
"""Call-site wiring: the real _init_capture_metadata must derive
flat_cache_group_ids from the pool's published specs and pass them to
the backend capture hook."""
def _run_capture(self, bs, group_ids, uses_flat=True):
torch = self.torch
from types import MethodType
from tokenspeed.runtime.execution.cuda_graph_wrapper import (
CudaGraphWrapper,
)
recorded = {}
def record(bs, req_pool_indices, seq_lens, forward_mode, **kwargs):
recorded["bs"] = bs
recorded["kwargs"] = kwargs
mock = SimpleNamespace(
input_buffers=SimpleNamespace(
has_mamba=False,
req_pool_indices_buf=torch.arange(MAX_BS, dtype=torch.int64),
seq_lens_buf=torch.ones(MAX_BS, dtype=torch.int32),
),
attn_backend=SimpleNamespace(
uses_paged_cache_groups=False,
uses_flat_cache_groups=uses_flat,
init_forward_metadata_capture_cuda_graph=record,
),
token_to_kv_pool=SimpleNamespace(
paged_cache_group_specs=tuple(
SimpleNamespace(group_id=gid) for gid in group_ids
)
),
drafter=None,
use_target_verify_forward_mode=False,
draft_attn_backend=None,
)
mock._flat_cache_group_ids = MethodType(
CudaGraphWrapper._flat_cache_group_ids, mock
)
CudaGraphWrapper._init_capture_metadata(mock, bs)
return recorded
def test_capture_passes_group_ids_from_pool_specs(self):
recorded = self._run_capture(2, ["sliding_attention", "full_attention"])
self.assertEqual(recorded["bs"], 2)
self.assertEqual(
recorded["kwargs"]["flat_cache_group_ids"],
("sliding_attention", "full_attention"),
)
def test_capture_omits_group_ids_when_backend_not_flat(self):
recorded = self._run_capture(
2, ["sliding_attention", "full_attention"], uses_flat=False
)
self.assertNotIn("flat_cache_group_ids", recorded["kwargs"])
def test_capture_omits_group_ids_without_specs(self):
recorded = self._run_capture(2, [])
self.assertNotIn("flat_cache_group_ids", recorded["kwargs"])
class WrapperEagerFlatGuardTest(_TorchCase):
"""Eager parity guard: a multi-group flat pool + flat-consuming backend
must not reach the backend's single-table fallback without tables."""
def _call(self, group_ids, flat_block_tables=None):
torch = self.torch
from tokenspeed.runtime.execution.cuda_graph_wrapper import (
CudaGraphWrapper,
)
from tokenspeed.runtime.execution.forward_batch_info import ForwardMode
calls = {}
def init_forward_metadata(*args, **kwargs):
calls["init_kwargs"] = kwargs
mock = SimpleNamespace(
input_buffers=SimpleNamespace(
seq_lens_buf=torch.ones(MAX_BS, dtype=torch.int32),
req_pool_indices_buf=torch.arange(MAX_BS, dtype=torch.int64),
),
config=SimpleNamespace(),
attn_backend=SimpleNamespace(
uses_flat_cache_groups=True,
uses_paged_cache_groups=False,
),
token_to_kv_pool=SimpleNamespace(
paged_cache_group_specs=tuple(
SimpleNamespace(group_id=gid) for gid in group_ids
)
),
drafter=None,
_can_use_graph=lambda bs, ctx: False,
_init_forward_metadata=init_forward_metadata,
_forward_func=lambda **kwargs: (None, None, None),
)
ctx = SimpleNamespace(
forward_mode=ForwardMode.EXTEND,
num_extends=2,
global_num_tokens=None,
all_decode_or_idle=False,
capture_hidden_mode=None,
)
CudaGraphWrapper.__call__(
mock,
bs=2,
ctx=ctx,
sampling_info=None,
req_to_page=torch.zeros((MAX_BS, MAX_NUM_PAGES), dtype=torch.int32),
flat_block_tables=flat_block_tables,
)
return calls
def test_multi_group_eager_without_tables_raises(self):
with self.assertRaisesRegex(RuntimeError, "flat_block_tables"):
self._call(["sliding_attention", "full_attention"])
def test_multi_group_eager_with_tables_passes(self):
torch = self.torch
tables = {
"sliding_attention": torch.ones((2, 2), dtype=torch.int32),
"full_attention": torch.ones((2, 2), dtype=torch.int32),
}
calls = self._call(
["sliding_attention", "full_attention"], flat_block_tables=tables
)
self.assertIs(calls["init_kwargs"]["flat_block_tables"], tables)
def test_single_group_eager_without_tables_falls_back(self):
# Documented fallback: with one published group the backend's single
# table IS that group's table, so no tables are required.
calls = self._call(["full_attention"])
self.assertIsNone(calls["init_kwargs"]["flat_block_tables"])
class IdleFlatBlockTablesTest(_TorchCase):
"""bs==0 idle replay tables: one col-0 page-0 entry per dummy row."""
def setUp(self):
super().setUp()
from tokenspeed.runtime.execution.cuda_graph_wrapper import (
CudaGraphWrapper,
)
self.idle = CudaGraphWrapper._idle_flat_block_tables
def _wrapper(self, group_ids):
return SimpleNamespace(
token_to_kv_pool=SimpleNamespace(
paged_cache_group_specs=tuple(
SimpleNamespace(group_id=gid) for gid in group_ids
)
),
device="cpu",
)
def test_page_zero_single_column_per_group(self):
out = self.idle(self._wrapper(["sliding_attention", "full_attention"]), 3)
self.assertEqual(set(out), {"sliding_attention", "full_attention"})
for table in out.values():
self.assertEqual(tuple(table.shape), (3, 1))
self.assertEqual(table.dtype, self.torch.int32)
self.assertTrue((table == 0).all())
def test_none_without_specs(self):
self.assertIsNone(self.idle(self._wrapper([]), 3))
class _BackendCase(_TorchCase):
"""Real MHAAttnBackend methods on a __init__-bypassed instance."""
def setUp(self):
super().setUp()
try:
from tokenspeed.runtime.layers.attention.backends.mha import (
MHAAttnBackend,
)
except (ImportError, ModuleNotFoundError) as exc:
self.skipTest(f"needs tokenspeed_kernel: {exc}")
torch = self.torch
backend = MHAAttnBackend.__new__(MHAAttnBackend)
backend.spec_num_tokens = 1
backend.is_draft = False
backend.draft_block_decode = False
backend.flat_state_group_ids = frozenset()
backend.max_num_pages = MAX_NUM_PAGES
backend.page_size = 2
backend.device = "cpu"
backend.cuda_graph_decode_metadata = {}
backend.cuda_graph_page_table = torch.zeros(
(MAX_BS, MAX_NUM_PAGES), dtype=torch.int32
)
# seq_lens 1 (never 0): flat replay recomputes write locs from these
# (M11), and seq_len 0 would gather at position -1.
backend.cuda_graph_seq_lens = torch.ones(MAX_BS, dtype=torch.int32)
backend.cuda_graph_flat_page_tables = {}
backend.cuda_graph_flat_out_cache_locs = {}
backend._cuda_graph_max_bs = MAX_BS
self.backend = backend
def _capture(self, bs, flat_cache_group_ids=()):
torch = self.torch
self.backend.init_forward_metadata_capture_cuda_graph(
bs,
torch.arange(bs, dtype=torch.int64),
torch.ones(bs, dtype=torch.int32),
_decode_forward_mode(),
flat_cache_group_ids=flat_cache_group_ids,
)
return self.backend.cuda_graph_decode_metadata[bs]
def _replay(self, bs, flat_block_tables=None):
torch = self.torch
kwargs = {}
if flat_block_tables is not None:
kwargs["flat_block_tables"] = flat_block_tables
self.backend.init_forward_metadata_replay_cuda_graph(
bs,
torch.arange(MAX_BS, dtype=torch.int64),
torch.ones(MAX_BS, dtype=torch.int32),
torch.zeros((MAX_BS, MAX_NUM_PAGES), dtype=torch.int32),
_decode_forward_mode(),
**kwargs,
)
_GROUP_IDS = ("sliding_attention", "full_attention")
class BackendCaptureFlatTest(_BackendCase):
def test_page_tables_none_without_group_ids(self):
metadata = self._capture(2)
self.assertIsNone(metadata.page_tables)
self.assertEqual(self.backend.cuda_graph_flat_page_tables, {})
def test_radix_capture_keeps_single_page_table(self):
# Radix/single-table capture: page_table stays a live slice of the
# persistent buffer (replay fills it via the gather path).
metadata = self._capture(2)
self.assertIsNotNone(metadata.page_table)
self.assertEqual(tuple(metadata.page_table.shape), (2, MAX_NUM_PAGES))
self.assertEqual(
metadata.page_table.data_ptr(),
self.backend.cuda_graph_page_table.data_ptr(),
)
def test_flat_capture_sheds_single_page_table(self):
# Flat captures route reads through per-group tables and replay never
# fills the radix single table: page_table must be None, never a
# slice of the never-filled zero buffer.
metadata = self._capture(2, _GROUP_IDS)
self.assertIsNone(metadata.page_table)
def test_allocates_persistent_buffers_and_views(self):
bs = 2
metadata = self._capture(bs, _GROUP_IDS)
bufs = self.backend.cuda_graph_flat_page_tables
self.assertEqual(set(bufs), set(_GROUP_IDS))
for gid, buf in bufs.items():
self.assertEqual(tuple(buf.shape), (MAX_BS, MAX_NUM_PAGES))
self.assertEqual(buf.dtype, self.torch.int32)
view = metadata.page_tables[gid]
self.assertEqual(tuple(view.shape), (bs, MAX_NUM_PAGES))
# Pointer-fixing: metadata views alias the persistent buffer.
self.assertEqual(view.data_ptr(), buf.data_ptr())
def test_second_capture_reuses_buffers(self):
first = self._capture(2, _GROUP_IDS)
bufs = dict(self.backend.cuda_graph_flat_page_tables)
second = self._capture(4, _GROUP_IDS)
self.assertEqual(
{g: b.data_ptr() for g, b in bufs.items()},
{
g: b.data_ptr()
for g, b in self.backend.cuda_graph_flat_page_tables.items()
},
)
self.assertIsNot(first, second)
def test_flat_with_spec_verify_records_expanded_loc_views(self):
# Verify (spec target) keeps [bs]-row tables but records [bs*N]
# write-loc views (token-major), sized off the persistent buffers.
self.backend.spec_num_tokens = 2
torch = self.torch
self.backend.cuda_graph_page_table = torch.zeros(
(MAX_BS * 2, MAX_NUM_PAGES), dtype=torch.int32
)
self.backend.cuda_graph_seq_lens = torch.zeros(MAX_BS * 2, dtype=torch.int32)
self._capture(2, _GROUP_IDS)
meta = self.backend.forward_decode_metadata
for gid in _GROUP_IDS:
self.assertEqual(meta.page_tables[gid].shape[0], 2)
self.assertEqual(meta.out_cache_locs[gid].shape[0], 2 * 2)
def test_flat_with_dflash_block_decode_asserts(self):
self.backend.spec_num_tokens = 2
self.backend.draft_block_decode = True
torch = self.torch
self.backend.cuda_graph_page_table = torch.zeros(
(MAX_BS * 2, MAX_NUM_PAGES), dtype=torch.int32
)
self.backend.cuda_graph_seq_lens = torch.zeros(MAX_BS * 2, dtype=torch.int32)
with self.assertRaisesRegex(AssertionError, "DFLASH"):
self._capture(2, _GROUP_IDS)
class BackendReplayFlatTest(_BackendCase):
def setUp(self):
super().setUp()
# Capture first so persistent buffers exist (replay indexes them).
self._capture(2, _GROUP_IDS)
def test_copies_prefix_and_fills_tail_minus_one(self):
torch = self.torch
src = {
# 0 = null hole (slid-out SWA page); cols narrower than buffer.
"sliding_attention": torch.tensor([[0, 3], [4, 5]], dtype=torch.int32),
"full_attention": torch.tensor([[1, 2], [6, 7]], dtype=torch.int32),
}
self._replay(2, src)
for gid, expected in src.items():
buf = self.backend.cuda_graph_flat_page_tables[gid]
self.assertTrue((buf[:2, :2] == expected).all())
self.assertTrue((buf[:2, 2:] == -1).all())
# Rows beyond bs untouched (still capture-time zeros).
self.assertTrue((buf[2:] == 0).all())
def test_padded_replay_dummy_rows_land_on_page_zero(self):
# After the wrapper's 0-row-pad and the backend's column fill_(-1),
# a dummy row reads only col 0 (seq_lens=1) -> dummy page 0.
torch = self.torch
from tokenspeed.runtime.execution.cuda_graph_wrapper import (
CudaGraphWrapper,
)
src = {
"sliding_attention": torch.tensor([[3, 4]], dtype=torch.int32),
"full_attention": torch.tensor([[5, 6]], dtype=torch.int32),
}
padded = CudaGraphWrapper._pad_block_tables_to_padded_bs(
src, actual_bs=1, padded_bs=2, pad_value=0
)
self._replay(2, padded)
for gid, expected in src.items():
buf = self.backend.cuda_graph_flat_page_tables[gid]
self.assertTrue((buf[:1, :2] == expected).all())
# Dummy row: col 0 must be a dereferenceable page (0), never -1.
self.assertEqual(int(buf[1, 0]), 0)
self.assertTrue((buf[1, :2] == 0).all())
self.assertTrue((buf[:2, 2:] == -1).all())
def test_full_width_src_leaves_no_tail(self):
torch = self.torch
src = {
gid: torch.full((2, MAX_NUM_PAGES), 9, dtype=torch.int32)
for gid in ("sliding_attention", "full_attention")
}
self._replay(2, src)
for gid in src:
buf = self.backend.cuda_graph_flat_page_tables[gid]
self.assertTrue((buf[:2] == 9).all())
def test_overwide_src_asserts(self):
# Both captured groups delivered (the missing-group guard runs
# first); the overwide one trips the width assert.
torch = self.torch
src = {
"sliding_attention": torch.ones((2, MAX_NUM_PAGES + 1), dtype=torch.int32),
"full_attention": torch.ones((2, 2), dtype=torch.int32),
}
with self.assertRaisesRegex(AssertionError, "cols"):
self._replay(2, src)
def test_underpadded_rows_assert(self):
torch = self.torch
src = {
"sliding_attention": torch.ones((1, 2), dtype=torch.int32),
"full_attention": torch.ones((2, 2), dtype=torch.int32),
}
with self.assertRaisesRegex(AssertionError, "rows"):
self._replay(2, src)
def test_missing_tables_with_flat_buffers_raises(self):
# A flat-captured graph replayed without tables must be loud, never
# silently compute over stale/zero page tables.
with self.assertRaisesRegex(RuntimeError, "stale"):
self._replay(2)
def test_missing_tables_empty_dict_raises(self):
with self.assertRaisesRegex(RuntimeError, "flat_block_tables"):
self._replay(2, {})
def test_missing_captured_group_raises(self):
# Per-group hole: a non-empty dict lacking one captured group would
# leave that group's buffer stale — must raise naming the group.
torch = self.torch
src = {"sliding_attention": torch.ones((2, 2), dtype=torch.int32)}
with self.assertRaisesRegex(RuntimeError, "full_attention"):
self._replay(2, src)
def test_bs_zero_missing_tables_skips(self):
# Documented bs==0 skip: buffers keep valid page-0/previous entries;
# outputs are discarded.
before = {
gid: buf.clone()
for gid, buf in self.backend.cuda_graph_flat_page_tables.items()
}
self._replay(0)
for gid, buf in self.backend.cuda_graph_flat_page_tables.items():
self.assertTrue((buf == before[gid]).all())
class BackendStateGroupShedTest(_BackendCase):
"""family="state" groups (GDN/mamba pages) must never reach MHA's flat
buffers, table copies, or write-loc math; the hybrid router still hands
the FULL dict to the mamba backend (see test_gdn_flat_state_paging)."""
_HYBRID_IDS = ("full_attention", "linear_attention")
def setUp(self):
super().setUp()
self.backend.flat_state_group_ids = frozenset({"linear_attention"})
def test_init_cuda_graph_state_learns_state_ids_from_specs(self):
torch = self.torch
self.backend.init_cuda_graph_state(
MAX_BS,
torch.ones(MAX_BS, dtype=torch.int32),
paged_cache_group_specs=(
SimpleNamespace(group_id="full_attention", family="history"),
SimpleNamespace(group_id="linear_attention", family="state"),
),
)
self.assertEqual(
self.backend.flat_state_group_ids, frozenset({"linear_attention"})
)
def test_capture_buffers_exclude_state_group(self):
metadata = self._capture(2, self._HYBRID_IDS)
self.assertEqual(
set(self.backend.cuda_graph_flat_page_tables), {"full_attention"}
)
self.assertEqual(
set(self.backend.cuda_graph_flat_out_cache_locs), {"full_attention"}
)
self.assertEqual(set(metadata.page_tables), {"full_attention"})
self.assertEqual(set(metadata.out_cache_locs), {"full_attention"})
def test_capture_state_only_yields_no_flat_metadata(self):
metadata = self._capture(2, ("linear_attention",))
self.assertIsNone(metadata.page_tables)
self.assertIsNone(metadata.out_cache_locs)
self.assertEqual(self.backend.cuda_graph_flat_page_tables, {})
def test_replay_skips_state_group_delivery(self):
torch = self.torch
self._capture(2, self._HYBRID_IDS)
src = {
"full_attention": torch.tensor([[1, 2], [3, 4]], dtype=torch.int32),
# Hole-heavy state table: MHA must not copy or derive locs
# from it (and has no buffer for it).
"linear_attention": torch.tensor([[0, 5], [0, 6]], dtype=torch.int32),
}
self._replay(2, src)
self.assertNotIn("linear_attention", self.backend.cuda_graph_flat_page_tables)
buf = self.backend.cuda_graph_flat_page_tables["full_attention"]
self.assertTrue((buf[:2, :2] == src["full_attention"]).all())
def test_eager_decode_metadata_sheds_state_group(self):
torch = self.torch
forward_mode = SimpleNamespace(
is_mixed=lambda: False,
is_extend_or_mixed=lambda: False,
)
self.backend.init_forward_metadata(
bs=2,
num_extends=0,
req_pool_indices=torch.arange(2, dtype=torch.int64),
seq_lens=torch.tensor([3, 4], dtype=torch.int32),
req_to_page=torch.zeros((MAX_BS, MAX_NUM_PAGES), dtype=torch.int32),
forward_mode=forward_mode,
flat_block_tables={
"full_attention": torch.tensor([[1, 2], [3, 4]], dtype=torch.int32),
"linear_attention": torch.tensor([[0, 5], [0, 6]], dtype=torch.int32),
},
)
metadata = self.backend.forward_decode_metadata
self.assertEqual(set(metadata.page_tables), {"full_attention"})
self.assertEqual(set(metadata.out_cache_locs), {"full_attention"})
# seq_lens [3, 4], page_size 2 -> last pos 2, 3 -> page col 1 ->
# pages 2, 4 -> locs 2*2+0=4, 4*2+1=9.
self.assertEqual(metadata.out_cache_locs["full_attention"].tolist(), [4, 9])
class BackendReplayNoFlatBuffersTest(_BackendCase):
def _replay_with_recorded_gather(self, bs, flat_block_tables=None):
# The radix single-table fill is a GPU Triton kernel; record the call
# instead of launching it on this test's CPU tensors.
from unittest import mock
import tokenspeed.runtime.layers.attention.backends.mha as mha_mod
with mock.patch.object(mha_mod, "gather_page_table_with_padding") as gather:
self._replay(bs, flat_block_tables)
return gather
def test_replay_without_flat_capture_needs_no_tables(self):
# No flat buffers captured (radix/single-table path): replay without
# tables stays valid and fills the radix single table.
self._capture(2)
gather = self._replay_with_recorded_gather(2)
gather.assert_called_once()
self.assertEqual(self.backend.cuda_graph_flat_page_tables, {})
def test_flat_replay_skips_radix_single_table_fill(self):
# Flat captures read only the per-group buffers: filling the radix
# single table would be dead work (see init_forward_metadata_replay).
torch = self.torch
self._capture(2, _GROUP_IDS)
gather = self._replay_with_recorded_gather(
2,
{gid: torch.ones((2, 2), dtype=torch.int32) for gid in _GROUP_IDS},
)
gather.assert_not_called()
if __name__ == "__main__":
unittest.main()