Files
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

238 lines
8.8 KiB
Python

"""Flat assembly line for GDN hybrids (M17 C4).
Three contracts: the Qwen3.5 config exposes ``layer_types`` in the
paged-cache label vocabulary; the page-size equalization decision
(``equalized_block_size``) inflates P to cover the GDN state row;
and an MHAConfig carrying state shapes builds a full-coverage pool with
one (conv, ssm) slab pair per state layer, both cache groups published,
and the ctor geometry check enforcing the equalized P. Flat GDN sizing
itself is plan-driven (plan_component_tensors, test_flat_memory_plan).
"""
from __future__ import annotations
import importlib.util
import os
import pathlib
import sys
import unittest
from unittest import mock
# 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=15, suite="runtime-1gpu")
_CONFIGS_DIR = (
pathlib.Path(__file__).resolve().parents[2]
/ "python"
/ "tokenspeed"
/ "runtime"
/ "configs"
)
_PKG_FLAT_PROBE = (
"tokenspeed.runtime.configs.paged_cache_spec.scheduler_ext_flat_kvcache"
)
def _load(mod_name: str, file_name: str):
spec = importlib.util.spec_from_file_location(mod_name, _CONFIGS_DIR / file_name)
assert spec is not None and spec.loader is not None
mod = importlib.util.module_from_spec(spec)
# Register before exec: on py3.9 @dataclass + `from __future__ import
# annotations` resolves field types via sys.modules[cls.__module__].
sys.modules[mod_name] = mod
spec.loader.exec_module(mod)
return mod
_plan = _load("flat_memory_plan_gdn_assembly_under_test", "flat_memory_plan.py")
equalized_block_size = _plan.equalized_block_size
# Qwen3.5-ish interleaving: 3 linear layers then 1 full, times 12 (48 layers).
QWEN3_5ISH_LAYER_TYPES = (["linear_attention"] * 3 + ["full_attention"]) * 12
# Qwen3.5 defaults at attn TP=1 (configs/qwen3_5_text_base_config.py):
# KV row: 2 (K+V) * 2 kv heads * 256 head_dim * 2 B (bf16) per token-layer.
QWEN3_5ISH_KV_BYTES_PER_SLOT = 2 * 2 * 256 * 2 # 2048
# conv: (2*128*16 + 128*32) x (4 - 1) in bf16; ssm: 32 x 128 x 128 in fp32.
QWEN3_5ISH_CONV_BYTES = (2 * 128 * 16 + 128 * 32) * 3 * 2 # 49152
QWEN3_5ISH_SSM_BYTES = 32 * 128 * 128 * 4 # 2097152
class Qwen3_5LayerTypesTest(unittest.TestCase):
"""The config's layer_types property (interleaving + label vocabulary)."""
def setUp(self):
try:
from tokenspeed.runtime.configs.qwen3_5_text_base_config import (
Qwen3_5BaseTextConfig,
)
except (ImportError, ModuleNotFoundError) as exc:
self.skipTest(f"needs torch + transformers: {exc}")
self.config_cls = Qwen3_5BaseTextConfig
def test_layer_types_interleaving(self):
cfg = self.config_cls(num_hidden_layers=8, full_attention_interval=4)
self.assertEqual(
cfg.layer_types,
(["linear_attention"] * 3 + ["full_attention"]) * 2,
)
def test_layers_block_type_keeps_checkpoint_label(self):
# models/qwen3_5.py keys layer construction on the checkpoint's
# "attention" label; layer_types must not change it.
cfg = self.config_cls(num_hidden_layers=4, full_attention_interval=4)
self.assertEqual(
cfg.layers_block_type, ["linear_attention"] * 3 + ["attention"]
)
def test_tracks_nextn_interval_override(self):
# models/qwen3_5_nextn.py rewrites full_attention_interval AFTER
# construction; layer_types must follow (property, not __init__).
cfg = self.config_cls(num_hidden_layers=2, full_attention_interval=4)
cfg.full_attention_interval = 1
self.assertEqual(cfg.layer_types, ["full_attention"] * 2)
class EqualizedPageSizeTest(unittest.TestCase):
"""Pure equalization decision (no torch)."""
def _equalized(self, block_size, **kwargs):
return equalized_block_size(
layer_types=QWEN3_5ISH_LAYER_TYPES,
kv_bytes_per_slot=QWEN3_5ISH_KV_BYTES_PER_SLOT,
state_const_bytes={
"conv": QWEN3_5ISH_CONV_BYTES,
"ssm": QWEN3_5ISH_SSM_BYTES,
},
block_size=block_size,
**kwargs,
)
def test_inflates_to_cover_state_row(self):
# ceil((49152 + 2097152) / 2048) = 1048 tokens to cover the state
# row; default alignment = original block size 64 -> 1088.
self.assertEqual(self._equalized(64), 1088)
def test_unchanged_when_kv_row_already_covers(self):
self.assertEqual(self._equalized(2048), 2048)
def test_explicit_alignment(self):
# 1048 rounded up to a multiple of 16 -> 1056.
self.assertEqual(self._equalized(64, alignment=16), 1056)
def test_no_state_layers_is_identity(self):
self.assertEqual(
equalized_block_size(
layer_types=["full_attention"] * 4,
kv_bytes_per_slot=QWEN3_5ISH_KV_BYTES_PER_SLOT,
state_const_bytes={},
block_size=64,
),
64,
)
class GdnFlatPoolAssemblyTest(unittest.TestCase):
"""MHAConfig with state shapes -> create_pool: full-coverage pool with
state slabs, both published groups, and the equalized-P geometry gate."""
# 3 linear + 1 full; kv row = 2 * 1 head * 8 dim * 2 B = 32 B/slot;
# state row = conv (4*3 bf16 = 24 B) + ssm (2*4*4 fp32 = 128 B) = 152 B;
# ceil(152 / 32) = 5 > block 4 -> equalized P = 8 (multiple of 4).
LAYER_TYPES = ("linear_attention",) * 3 + ("full_attention",)
CONV_SHAPE = (4, 3)
TEMPORAL_SHAPE = (2, 4, 4)
def setUp(self):
try:
import torch
from tokenspeed.runtime.layers.attention.configs.mha import (
MHAConfig,
)
except (ImportError, ModuleNotFoundError) as exc:
self.skipTest(f"needs torch + tokenspeed_kernel: {exc}")
self.torch = torch
self.MHAConfig = MHAConfig
def _config(self, page_size: int):
torch = self.torch
return self.MHAConfig(
device="cpu",
backend_name=None,
num_attention_heads=2,
num_kv_heads=1,
head_dim=8,
attn_tp_size=1,
dtype=torch.bfloat16,
kv_cache_dtype=torch.bfloat16,
page_size=page_size,
context_len=64,
max_bs=2,
max_graph_bs=2,
kv_cache_quant_method=None,
layer_types=self.LAYER_TYPES,
max_scheduled_tokens=16,
conv_state_shape=self.CONV_SHAPE,
temporal_state_shape=self.TEMPORAL_SHAPE,
conv_dtype=torch.bfloat16,
ssm_dtype=torch.float32,
)
def _pool(self, page_size: int):
with mock.patch(_PKG_FLAT_PROBE, return_value=True):
return self._config(page_size).create_pool(
len(self.LAYER_TYPES), 32, 0, False
)
def test_equalization_decision_matches_pure_helper(self):
self.assertEqual(
equalized_block_size(
layer_types=list(self.LAYER_TYPES),
kv_bytes_per_slot=32,
state_const_bytes={"conv": 24, "ssm": 128},
block_size=4,
),
8,
)
def test_assembly_at_equalized_page_size(self):
pool = self._pool(page_size=8)
# One (conv, ssm) slab pair per state layer, rows over the shared
# page-id space (size // P + 1 null row).
self.assertEqual(len(pool.state_slabs), 3)
conv, ssm = pool.state_slabs[0]
self.assertEqual(tuple(conv.shape), (5, *self.CONV_SHAPE))
self.assertEqual(conv.dtype, self.torch.bfloat16)
self.assertEqual(tuple(ssm.shape), (5, *self.TEMPORAL_SHAPE))
self.assertEqual(ssm.dtype, self.torch.float32)
# Both groups published (upstream signal for flat state paging).
self.assertEqual(
sorted(spec.group_id for spec in pool.paged_cache_group_specs),
["full_attention", "linear_attention"],
)
# Plan-sized coverage (M18a T4): the k/v lists stay layer-indexed,
# but state layers carry no KV tensors (None slots) -- only the
# full-attention layer allocates.
self.assertEqual(len(pool.k_buffer), len(self.LAYER_TYPES))
for layer_id, label in enumerate(self.LAYER_TYPES):
if label == "linear_attention":
self.assertIsNone(pool.k_buffer[layer_id])
self.assertIsNone(pool.v_buffer[layer_id])
else:
self.assertIsNotNone(pool.k_buffer[layer_id])
self.assertIsNotNone(pool.v_buffer[layer_id])
def test_geometry_raises_at_original_page_size(self):
with self.assertRaisesRegex(ValueError, "pre-equalized"):
self._pool(page_size=4)
if __name__ == "__main__":
unittest.main()