feat(m3): support M3 + DSpark (#1190)

This commit is contained in:
Yue Weng
2026-08-21 18:12:15 +08:00
committed by GitHub
parent eba3f1b0b1
commit 739c0194d9
14 changed files with 494 additions and 20 deletions
+1
View File
@@ -29,6 +29,7 @@ on:
- test/ci/eval/kimi-k2.5-nvfp4-eagle3-evalscope-ocr-bench.yaml
- test/ci/eval/kimi-k2.5-mxfp4-eagle3-evalscope-aime25-amd.yaml
- test/ci/eval/kimi-k3-mxfp4-tp8ep8-evalscope-aime26-amd.yaml
- test/ci/eval/minimax-m3-nvfp4-dspark-evalscope-aime26.yaml
- test/ci/eval/minimax-m3-nvfp4-evalscope-aime25.yaml
- test/ci/eval/qwen3.5-122b-a10b-nvfp4-epd-1e1p2d-evalscope-ocr-bench.yaml
- test/ci/eval/qwen3.5-122b-a10b-nvfp4-evalscope-ocr-bench.yaml
+1
View File
@@ -40,6 +40,7 @@ on:
- test/ci/eval/kimi-k3-mxfp4-tp8-two-node-evalscope-aime26-gb300-slurm.yaml
- test/ci/eval/kimi-k3-nvfp4-dspark-tp8-two-node-evalscope-aime26-gb300-slurm.yaml
- test/ci/eval/kimi-k3-nvfp4-tp8-two-node-evalscope-aime26-gb300-slurm.yaml
- test/ci/eval/minimax-m3-nvfp4-dspark-evalscope-aime26.yaml
- test/ci/eval/minimax-m3-nvfp4-evalscope-aime25.yaml
- test/ci/eval/qwen3.5-122b-a10b-nvfp4-epd-1e1p2d-evalscope-ocr-bench.yaml
- test/ci/eval/qwen3.5-122b-a10b-nvfp4-evalscope-ocr-bench.yaml
+16 -1
View File
@@ -174,7 +174,7 @@ the values accepted by the bundled `tokenspeed-smg` package.
| Parameter | Purpose |
| --- | --- |
| `--speculative-config` | JSON speculative decoding configuration. |
| `--speculative-algorithm` | Speculative algorithm, such as `EAGLE3`, `MTP`, or `DFLASH`. |
| `--speculative-algorithm` | Speculative algorithm, such as `EAGLE3`, `MTP`, `DFLASH`, or `DSPARK`. |
| `--speculative-draft-model-path` | Draft model path or repo ID. |
| `--speculative-draft-model-quantization` | Draft model quantization. Defaults to `unquant`. |
| `--speculative-num-steps` | Number of draft model steps. Defaults to `3`. |
@@ -185,6 +185,21 @@ the values accepted by the bundled `tokenspeed-smg` package.
Prefer `--speculative-config` for recipe-style launches because it keeps method,
draft model, and token count together.
`DFLASH` and `DSPARK` are block drafters: one draft forward proposes a whole
block instead of one token per step, so their two token counts are coupled.
`--speculative-num-draft-tokens` is the verify width -- one anchor row plus one
row per drafted token -- and `--speculative-num-steps` must be one less. The
draft checkpoint's `block_size` fixes both, and a mismatch is rejected at
startup rather than silently drafting a wrong-width block. A checkpoint with
`block_size: 8` therefore wants `--speculative-num-draft-tokens 8
--speculative-num-steps 7`.
A block drafter writes its KV at the target's cache locations, so it shares the
target's page table: `--block-size` is a target-side choice and the draft
follows it. Any sliding window the draft checkpoint declares is an attention
mask applied by the draft's own layers, never a cache-retention policy of its
own.
## Observability
| Parameter | Purpose |
+56
View File
@@ -71,6 +71,8 @@ MiniMax M3 uses 128-token MSA blocks. TokenSpeed configures its dense and sparse
attention layers automatically; select the dense backend with
`--attention-backend` and run with `--disable-kvstore`.
### EAGLE3 draft
```bash
tokenspeed serve nvidia/MiniMax-M3-NVFP4 \
--tensor-parallel-size 4 \
@@ -95,6 +97,60 @@ tokenspeed serve nvidia/MiniMax-M3-NVFP4 \
--port 8000
```
### DSpark draft
`nvidia/MiniMax-M3-DSpark` is a six-layer Qwen3-shaped GQA block drafter with a
vanilla Markov head. Keep the target launch shape and swap the speculative
options:
```bash
tokenspeed serve nvidia/MiniMax-M3-NVFP4 \
--tensor-parallel-size 4 \
--max-model-len 262144 \
--max-num-seqs 16 \
--max-prefill-tokens 8192 \
--chunked-prefill-size 8192 \
--gpu-memory-utilization 0.95 \
--disable-cuda-graph-padding \
--attention-backend trtllm \
--kv-cache-dtype fp8 \
--moe-backend flashinfer_trtllm \
--speculative-algorithm DSPARK \
--speculative-draft-model-path nvidia/MiniMax-M3-DSpark \
--speculative-num-steps 7 \
--speculative-eagle-topk 1 \
--speculative-num-draft-tokens 8 \
--disable-kvstore \
--block-size 128 \
--trust-remote-code \
--host 0.0.0.0 \
--port 8000
```
Notes:
- Launch this checkpoint with `--speculative-num-draft-tokens 8` and
`--speculative-num-steps 7`: the verify window is one anchor row plus seven
draft queries, and the step count is always the verify width minus one. The
width is checked against the checkpoint's `block_size` at startup, so a
mismatched launch fails fast instead of drafting a wrong-width block.
- `--block-size 128` is the target's MSA page size. The draft writes its KV at
the target's cache locations and shares the target's page table, so it
inherits that page size; do not set a separate draft block size.
- The draft's 1024-token sliding window is an attention mask its own layers
apply. It is deliberately not a cache-retention policy, because the draft's
pages are the target's pages.
- Target features are captured from the residual stream after each layer in
`dflash_config.target_layer_ids` (`[1, 12, 23, 35, 46, 57]` of M3's 60
layers) and concatenated in ascending layer order to feed the draft's `fc`.
- The draft checkpoint stores fp32 master weights. It is loaded in the target's
dtype rather than the standalone fp32-to-fp16 default, because the two
exchange hidden states and share the target's embedding and LM head.
- Measured on 4x GB200 with the launch above: gsm8k `mean_acc` 0.9727 versus
0.9773 without speculative decoding (paired disagreement 3 vs 9, McNemar
p ~ 0.15 -- within run-to-run noise), at a mean accepted length of 4.99 of 8
and about 1.7x decode throughput at 16 concurrent requests.
## Kimi K2.5 / K2.6
Kimi-style MoE launches usually need remote code, long context, reasoning and
@@ -1027,15 +1027,20 @@ class EventLoop:
) -> ModelConfig:
server_args = self.server_args
quantization = server_args.quantization
dtype = server_args.dtype
if is_draft_worker:
quantization = server_args.speculative_draft_model_quantization
if dtype == "auto":
# A draft is fed the target's hidden states and borrows its
# embedding and LM head, so the two dtypes have to agree.
dtype = self.model_config.dtype
return ModelConfig(
model_path,
trust_remote_code=server_args.trust_remote_code,
revision=server_args.revision,
context_length=server_args.max_model_len,
model_override_args=server_args.hf_overrides,
dtype=server_args.dtype,
dtype=dtype,
quantization=quantization,
server_args=server_args,
is_draft_worker=is_draft_worker,
@@ -33,7 +33,7 @@ from tokenspeed.runtime.utils import get_colorful_logger
logger = get_colorful_logger(__name__)
@triton.jit
@triton.jit(do_not_specialize=["max_pages"])
def compute_out_cache_loc_kernel(
# Input pointers
input_lengths_ptr, # [batch_size] or None for uniform mode
@@ -45,7 +45,7 @@ def compute_out_cache_loc_kernel(
# Scalars
uniform_input_length, # used when input_lengths_ptr is None
page_size: tl.constexpr,
max_pages: tl.constexpr,
max_pages, # runtime: constexpr here recompiles per page-table width
window_pages: tl.constexpr, # 0 = full history; >0 = sliding ring width
BLOCK_SIZE: tl.constexpr,
):
@@ -79,6 +79,11 @@ def compute_out_cache_loc_kernel(
IS the construction that makes addressing a slid-out column
impossible. Columns the scheduler punched to the null page read as
page 0 and route to slot 0 either way.
``max_pages``, by contrast, is a runtime scalar: the page table's width
grows with context, so specializing on it recompiles the kernel once per
distinct width. It only feeds the overflow test, the clamp and the row
stride, none of which need a compile-time constant.
"""
# Program ID represents which request we're processing
req_idx = tl.program_id(0)
@@ -167,7 +172,7 @@ def compute_out_cache_loc(
)
@triton.jit
@triton.jit(do_not_specialize=["max_pages"])
def fused_decode_input_prep_kernel(
# Inputs
req_pool_indices_ptr, # [batch_size]
@@ -180,7 +185,7 @@ def fused_decode_input_prep_kernel(
# Scalars
uniform_input_length,
page_size: tl.constexpr,
max_pages: tl.constexpr,
max_pages, # runtime: constexpr here recompiles per page-table width
BLOCK_SIZE: tl.constexpr,
):
"""One launch fuses the decode-uniform path's four small kernels.
@@ -268,7 +273,7 @@ def fused_decode_input_prep(
)
@triton.jit
@triton.jit(do_not_specialize=["max_pages"])
def dflash_prepare_decode_kernel(
output_tokens_ptr,
accept_lengths_ptr,
@@ -282,7 +287,7 @@ def dflash_prepare_decode_kernel(
verify_width: tl.constexpr,
draft_query_width: tl.constexpr,
page_size: tl.constexpr,
max_pages: tl.constexpr,
max_pages, # runtime: constexpr here recompiles per page-table width
max_draft_prefix,
block_ids_stride: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
@@ -82,6 +82,12 @@ class MHAConfig(BaseAttnConfig):
# Target-stack labels don't fit the draft depth; drop so the pool falls back to full attn
layer_types = ()
sliding_window_tokens = getattr(hf_config, "sliding_window", None)
if draft_block_decode:
# A block drafter writes its KV at the target's cache locations, so
# it cannot carry a retention policy of its own; its window is an
# attention mask the draft applies in its own layers.
layer_types = ()
sliding_window_tokens = None
return cls(
device=server_args.device,
context_len=model_config.context_len + server_args.spec_context_pad,
@@ -143,6 +143,39 @@ class BaseCausalLM(nn.Module):
self.model.layers_to_capture = [val + 1 for val in layer_ids]
def set_dflash_layers_to_capture(
self,
layer_ids: list[int],
incremental_callback=None,
slot_bufs: list | None = None,
) -> None:
"""Capture the target hidden states a DFLASH/DSpark draft consumes.
Checkpoints name layer *outputs*, but a layer captures the residual
entering it -- hence the ``+ 1`` shift, same as EAGLE3.
"""
num_layers = len(self.model.layers)
if len(set(layer_ids)) != len(layer_ids):
raise ValueError("DFLASH target_layer_ids must be unique.")
invalid = [val for val in layer_ids if val < 0 or val + 1 >= num_layers]
if invalid:
raise ValueError(
"DFLASH target_layer_ids must map to capturable target layer "
f"outputs. Got invalid ids {invalid}; valid range is "
f"[0, {num_layers - 2}] for {num_layers} target layers."
)
self.capture_aux_hidden_states = True
capture_layers = sorted(val + 1 for val in layer_ids)
self.model.layers_to_capture = capture_layers
# The draft concatenates captures in ascending layer order.
self.model._dflash_capture_idx_map = {
layer_idx: i for i, layer_idx in enumerate(capture_layers)
}
self.model._dflash_incremental_callback = incremental_callback
self.model._dflash_slot_bufs = slot_bufs
@torch.no_grad()
def forward(
self,
@@ -71,6 +71,12 @@ class BaseTransformerModel(nn.Module):
self.layers = self.resolve_layers(config, quant_config, prefix)
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.layers_to_capture: list[int] = []
# DFLASH incremental projection, populated by
# set_dflash_layers_to_capture().
self._dflash_capture_idx_map: dict[int, int] = {}
self._dflash_incremental_callback = None
self._dflash_slot_bufs: list[torch.Tensor] | None = None
self._dflash_incr_active = False
self._compile_decoder_stack()
@@ -154,6 +160,25 @@ class BaseTransformerModel(nn.Module):
prefix=add_prefix("layers", prefix),
)
def _notify_dflash_capture(
self, layer_idx: int, aux_hidden_states: list[torch.Tensor]
) -> None:
"""Hand the capture layer ``layer_idx`` just appended to the drafter.
An idle forward skips the attention block, so nothing was appended.
"""
callback = self._dflash_incremental_callback
slot_bufs = self._dflash_slot_bufs
capture_idx = self._dflash_capture_idx_map.get(layer_idx)
if callback is None or slot_bufs is None or capture_idx is None:
return
if len(aux_hidden_states) != capture_idx + 1:
return
captured = aux_hidden_states[capture_idx]
num_tokens = captured.shape[0]
slot_bufs[capture_idx][:num_tokens].copy_(captured)
callback(capture_idx, num_tokens)
def forward(
self,
input_ids: torch.Tensor,
@@ -193,6 +218,8 @@ class BaseTransformerModel(nn.Module):
for i, layer in enumerate(self.layers):
capture = i in self.layers_to_capture
with get_global_expert_distribution_recorder().with_current_layer(i):
hidden_states, residual = layer(
@@ -201,11 +228,12 @@ class BaseTransformerModel(nn.Module):
ctx,
out_cache_loc,
residual,
aux_hidden_states=(
aux_hidden_states if i in self.layers_to_capture else None
),
aux_hidden_states=aux_hidden_states if capture else None,
)
if capture and self._dflash_incr_active:
self._notify_dflash_capture(i, aux_hidden_states)
if not ctx.forward_mode.is_idle():
if residual is None:
@@ -231,6 +231,31 @@ def _restore_raw_glm_dsa_fields(config: PretrainedConfig, raw_config: dict) -> N
setattr(config, key, raw_config[key])
def _restore_raw_dflash_fields(config: PretrainedConfig, raw_config: dict) -> None:
"""Re-assert a DFLASH/DSpark draft's sliding window.
``Qwen3DSparkModel`` parses as ``Qwen3Config``, which nulls
``sliding_window`` unless ``use_sliding_window`` is set -- a flag these
checkpoints never write, since they carry the window in ``dflash_config``.
"""
dflash_config = raw_config.get("dflash_config")
if not isinstance(dflash_config, dict):
return
if getattr(config, "sliding_window", None) is not None:
return
sliding_window = raw_config.get("sliding_window")
if sliding_window is None and dflash_config.get("use_swa"):
sliding_window = dflash_config.get("swa_window_size")
if sliding_window is None:
return
config.sliding_window = int(sliding_window)
if hasattr(config, "use_sliding_window"):
# transformers gates the window on this flag; keep the two consistent.
config.use_sliding_window = True
def get_config(
model: str,
trust_remote_code: bool,
@@ -327,6 +352,7 @@ def get_config(
_materialize_architectures(config, raw_config)
_restore_raw_glm_dsa_fields(config, raw_config)
_restore_raw_dflash_fields(config, raw_config)
# extract 'text_config'
text_config = get_hf_text_config(config)
@@ -0,0 +1,58 @@
api_version: ci.tokenspeed.io/v1
name: eval-minimax-m3-nvfp4-dspark-aime26
type: eval
workflow_stage: model-test
triggers:
- manual
- nightly
runner:
labels:
- b200-4gpu
env:
CI: "true"
install:
- bash test/ci_system/install_deps.sh
server:
command: >-
ts serve
--model nvidia/MiniMax-M3-NVFP4
--attn-tp-size 4
--moe-tp-size 4
--max-model-len 262144
--gpu-memory-utilization 0.95
--attention-backend trtllm
--kv-cache-dtype fp8
--moe-backend flashinfer_trtllm
--speculative-algorithm DSPARK
--speculative-draft-model-path nvidia/MiniMax-M3-DSpark
--speculative-num-steps 7
--speculative-eagle-topk 1
--speculative-num-draft-tokens 8
--disable-kvstore
--block-size 128
--trust-remote-code
--enable-cache-report
--host 127.0.0.1
--port 8000
ready:
url: http://127.0.0.1:8000/readiness
timeout: 1800
interval: 10
eval:
install:
- python3 -m uv venv --seed --clear /tmp/evalscope-perf && python3 -m uv pip install --python /tmp/evalscope-perf/bin/python 'evalscope[perf]'
command: >-
HF_HOME=${RUNNER_TEMP:-/tmp}/hf-eval-cache
/tmp/evalscope-perf/bin/evalscope eval
--model nvidia/MiniMax-M3-NVFP4
--api-url http://127.0.0.1:8000/v1
--api-key EMPTY_TOKEN
--datasets aime26
--dataset-hub huggingface
--dataset-args '{"aime26":{"dataset_id":"math-ai/aime26"}}'
--eval-batch-size 16
--stream
--generation-config '{"do_sample":true,"temperature":1.0,"top_p":0.95,"max_tokens":163840,"timeout":1800}'
report:
github_step_summary: true
score_threshold: 0.86
+1 -1
View File
@@ -34,7 +34,7 @@ DATASETS = {
"dataset_args": {"dataset_id": "math-ai/aime25"},
},
"aime26": {
"count": 7,
"count": 8,
"dataset_args": {"dataset_id": "math-ai/aime26"},
},
"gpqa_diamond": {
+216 -2
View File
@@ -1,8 +1,9 @@
"""Semantic coverage for the DSpark drafter's config and CLI contract.
"""Semantic coverage for the DSpark drafter's config and wiring contract.
These are the cheap guards that catch a mis-specified launch before eight GPUs
are committed to loading a 1T-parameter target: block geometry, Markov-head
resolution, algorithm dispatch, and the draft-worker architecture rewrite.
resolution, algorithm dispatch, the draft-worker architecture rewrite, and the
target-side capture the draft is fed from.
"""
from __future__ import annotations
@@ -29,6 +30,8 @@ from tokenspeed.runtime.layers.attention.configs.base import (
from tokenspeed.runtime.layers.attention.configs.mla import (
resolve_mla_kv_cache_dtype,
)
from tokenspeed.runtime.models.base.causal_lm import BaseCausalLM
from tokenspeed.runtime.models.base.transformer_model import BaseTransformerModel
from tokenspeed.runtime.models.dspark import _get_markov_params
from tokenspeed.runtime.utils.hf_transformers_utils import get_config
@@ -251,6 +254,217 @@ def test_non_spec_archs_still_get_the_nextn_rewrite(tmp_path) -> None:
assert config.architectures[0] == "Qwen3ForCausalLMNextN"
# --------------------------------------------------------------------------
# Sliding-window geometry of a windowed DSpark draft
# --------------------------------------------------------------------------
def _write_swa_draft_config(tmp_path, **overrides) -> str:
"""A DSpark draft whose layers are all sliding, as MiniMax-M3's ships."""
return _write_config(
tmp_path,
architectures=["Qwen3DSparkModel"],
layer_types=["sliding_attention", "sliding_attention"],
dflash_config={
"mask_token_id": 127,
"target_layer_ids": [1, 12],
"use_swa": True,
"swa_window_size": 1024,
"markov_rank": 16,
},
**overrides,
)
@pytest.mark.parametrize("declare_top_level", (True, False))
def test_dspark_draft_keeps_the_window_transformers_would_null(
tmp_path, declare_top_level: bool
) -> None:
"""``Qwen3Config`` drops ``sliding_window`` unless ``use_sliding_window``.
DSpark checkpoints never write that flag, so without the restore every
consumer -- draft layer construction, the draft attention config, the cache
recipe -- sees None on a sliding draft. The window is read from the raw
config when present, else from ``dflash_config.swa_window_size``.
"""
extra = {"sliding_window": 1024} if declare_top_level else {}
path = _write_swa_draft_config(tmp_path, **extra)
config = get_config(path, trust_remote_code=False, is_draft_worker=True)
assert config.sliding_window == 1024
# --------------------------------------------------------------------------
# Draft dtype
# --------------------------------------------------------------------------
def _draft_dtype(server_dtype: str, target_dtype: torch.dtype) -> object:
from tokenspeed.runtime.engine import event_loop as event_loop_module
loop = event_loop_module.EventLoop.__new__(event_loop_module.EventLoop)
loop.server_args = SimpleNamespace(
dtype=server_dtype,
quantization=None,
speculative_draft_model_quantization=None,
trust_remote_code=True,
revision=None,
max_model_len=262144,
hf_overrides="{}",
)
loop.model_config = SimpleNamespace(dtype=target_dtype)
with mock.patch.object(event_loop_module, "ModelConfig") as model_config:
loop._load_model_config("draft", is_draft_worker=True)
return model_config.call_args.kwargs["dtype"]
def test_draft_inherits_the_targets_dtype_instead_of_its_own() -> None:
"""A DSpark draft stored as fp32 master weights must not land on fp16.
It is fed the target's hidden states and borrows its LM head, so "auto"
has to resolve against the target; the standalone fp32 -> fp16 rule would
leave the first GEMM mixing bf16 and fp16 with no kernel.
"""
assert _draft_dtype("auto", torch.bfloat16) is torch.bfloat16
def test_an_explicit_dtype_still_wins_for_the_draft() -> None:
assert _draft_dtype("bfloat16", torch.bfloat16) == "bfloat16"
# --------------------------------------------------------------------------
# Draft cache grouping
# --------------------------------------------------------------------------
def _draft_attn_config(algorithm: str, layer_types: tuple[str, ...]):
from tokenspeed.runtime.layers.attention.configs.mha import MHAConfig
server_args = SimpleNamespace(
speculative_algorithm=algorithm,
speculative_num_steps=7,
speculative_num_draft_tokens=8,
kv_cache_dtype="fp8_e4m3",
kv_cache_quant_method="none",
device="cuda",
attention_backend="trtllm",
drafter_attention_backend="trtllm",
spec_context_pad=0,
prefix_granularity=128,
max_num_seqs=16,
data_parallel_size=None,
max_cudagraph_capture_size=80,
chunked_prefill_size=8192,
disaggregation_mode="null",
attn_tp_size=4,
mapping=SimpleNamespace(attn=SimpleNamespace(tp_size=4, dp_size=1)),
)
model_config = SimpleNamespace(
hf_config=SimpleNamespace(layer_types=layer_types, sliding_window=1024),
num_attention_layers=len(layer_types),
context_len=4096,
num_attention_heads=32,
num_key_value_heads=8,
head_dim=128,
dtype=torch.bfloat16,
)
return MHAConfig.generate(server_args, model_config, is_draft=True)
def test_block_draft_shares_the_targets_retention() -> None:
"""A DSpark draft's window is a mask, not a cache-retention policy.
Its KV rows are written at the target's cache locations, so they live and
die with the target's pages; a sliding cache group of its own would both
evict rows the target still owns and collide with the target's planes.
"""
config = _draft_attn_config("DSPARK", ("sliding_attention",) * 6)
assert config.layer_types == ()
assert config.sliding_window_tokens is None
def test_a_non_block_draft_keeps_its_own_labels() -> None:
config = _draft_attn_config("EAGLE3", ("sliding_attention",) * 6)
assert config.layer_types == ("sliding_attention",) * 6
assert config.sliding_window_tokens == 1024
# --------------------------------------------------------------------------
# Target-side capture on the base stack
# --------------------------------------------------------------------------
class _CaptureModel:
"""Stand-in for the transformer stack: the capture state, no modules."""
def __init__(self, num_layers: int) -> None:
self.layers = [object()] * num_layers
self.layers_to_capture = []
self._dflash_capture_idx_map = {}
self._dflash_incremental_callback = None
self._dflash_slot_bufs = None
self._dflash_incr_active = False
notify = BaseTransformerModel._notify_dflash_capture
class _CaptureCausalLM:
"""Stand-in exposing only the setter under test."""
def __init__(self, num_layers: int) -> None:
self.model = _CaptureModel(num_layers)
self.capture_aux_hidden_states = False
set_dflash_layers_to_capture = BaseCausalLM.set_dflash_layers_to_capture
def test_taps_shift_by_one_and_sort_for_positional_concat() -> None:
"""MiniMax-M3's DSpark taps, shuffled: they name completed-layer outputs,
and the draft concatenates the captures in ascending layer order."""
causal_lm = _CaptureCausalLM(num_layers=60)
causal_lm.set_dflash_layers_to_capture([57, 1, 35, 12, 46, 23])
assert causal_lm.model.layers_to_capture == [2, 13, 24, 36, 47, 58]
assert causal_lm.model._dflash_capture_idx_map == {
2: 0,
13: 1,
24: 2,
36: 3,
47: 4,
58: 5,
}
assert causal_lm.capture_aux_hidden_states is True
def test_each_capture_reaches_the_drafter_in_concat_order() -> None:
slot_bufs = [torch.zeros(4, 3) for _ in range(2)]
seen: list[tuple[int, int]] = []
causal_lm = _CaptureCausalLM(num_layers=8)
causal_lm.set_dflash_layers_to_capture(
[1, 5],
incremental_callback=lambda idx, num_tokens: seen.append((idx, num_tokens)),
slot_bufs=slot_bufs,
)
model = causal_lm.model
model._dflash_incr_active = True
aux_hidden_states: list[torch.Tensor] = []
aux_hidden_states.append(torch.ones(2, 3))
model.notify(2, aux_hidden_states)
aux_hidden_states.append(torch.full((2, 3), 2.0))
model.notify(6, aux_hidden_states)
assert seen == [(0, 2), (1, 2)]
assert torch.equal(slot_bufs[0][:2], torch.ones(2, 3))
assert torch.equal(slot_bufs[1][:2], torch.full((2, 3), 2.0))
# --------------------------------------------------------------------------
# CLI contract
# --------------------------------------------------------------------------
+32 -6
View File
@@ -1,14 +1,17 @@
"""Sliding-window write-location kernel.
"""Write-location kernels: the sliding-window ring and the page-table width.
The SWA draft's page table is a ring: absolute position ``p`` lives in
column ``(p // P) % window_pages``. These tests pin the three properties the
multi-step draft depends on: ring mapping (including wrap-around), null-hole
routing to the safe dummy slot, and agreement with the full-history kernel
inside the window (before any wrap).
The SWA draft's page table is a ring: absolute position ``p`` lives in column
``(p // P) % window_pages``. The first half pins ring mapping (including
wrap-around), null-hole routing to the dummy slot, and agreement with the
full-history kernel before any wrap.
The last test pins that table's width as a runtime argument: ``tl.constexpr``
there recompiles the kernel once per distinct width.
"""
from __future__ import annotations
import inspect
import os
import sys
@@ -26,8 +29,11 @@ register_cuda_ci(est_time=10, suite="runtime-1gpu")
from test.runtime.conftest import requires_cuda
from tokenspeed.runtime.execution.cache_loc_kernel import (
compute_out_cache_loc_kernel,
compute_out_cache_loc_sliding,
compute_out_cache_loc_uniform,
dflash_prepare_decode_kernel,
fused_decode_input_prep_kernel,
)
from tokenspeed.runtime.execution.draft_page_staging import CacheView
@@ -169,3 +175,23 @@ def test_capture_replay_address_stability() -> None:
graph.replay()
torch.cuda.synchronize()
assert int(out[0]) == 77 * _P
# --------------------------------------------------------------------------
# Page-table width stays a runtime argument
# --------------------------------------------------------------------------
@pytest.mark.parametrize(
"kernel",
[
compute_out_cache_loc_kernel,
fused_decode_input_prep_kernel,
dflash_prepare_decode_kernel,
],
ids=["out_cache_loc", "fused_decode_input_prep", "dflash_prepare_decode"],
)
def test_max_pages_is_not_constexpr(kernel) -> None:
"""Re-annotating it ``tl.constexpr`` is the regression to catch."""
parameter = inspect.signature(kernel.fn).parameters["max_pages"]
assert parameter.annotation is inspect.Parameter.empty