feat(triton): port eight P1 kernels, all A/B identical to TileLang

gather, gather_input_ids_zos, gather_engram_tokens_zos, expand_swa_index, kv_index_cat,
convert_kv_index, load_kv_index, store_mtp_info. 45 cases, every one bitwise identical to the
TileLang baseline through the shared interface function.

`scripts/h20_ops_ab.py` swaps `ops.<name>_tl` between the two backends and runs the public call
once per side, so the comparison exercises the interface layer's shape handling too, not just the
kernel. `scripts/ab_cases.py` holds the cases: real engine widths and dtypes, negative indices
for gather, -1 padding for the index kernels, page-boundary lengths, the slot-0 no-write path,
and the engram image-sentinel rule.

Five Triton semantics traps worth writing down, all documented in the plan:

- `tl.where` evaluates both arms, so `tl.where(i == 0, 0, LOAD(i - 1))` really reads `LOAD[-1]`;
  that was an illegal memory access, fixed by clamping the index instead.
- A scalar pointer cannot carry a block-shaped mask; it has to be broadcast with
  `+ tl.zeros((BLOCK,), tl.int32)` first.
- Two arms of one kernel cannot use different tile shapes, which is why `kv_index_cat` became two
  launches rather than one fused kernel with a column-block branch.
- Triton's `data_ptr()` already includes a view's storage offset, so passing `out[:, w:w+k]` and
  adding `w` again double-counts -- that showed up as 0x01010101 in the second half.
- Test-case capacity matters: `store_mtp_info` addresses `slot * nextn + step`, so the pool needs
  `(max_slot + 1) * nextn` rows; too few rows let both backends write out of bounds and disagree.
This commit is contained in:
loopyt 2026-10-22 00:11:45 +08:00
parent 4a5606ad2d
commit a6c3e69e67
10 changed files with 916 additions and 2 deletions

View File

@ -104,4 +104,29 @@
## 七、进度
见文末「执行日志」。
| 阶段 | 状态 | 说明 |
|---|---|---|
| P0 基础设施 | ✅ | 后端选择 + fallback + A/B 骨架 |
| P1 轻量 kernel | 🔄 8/13 | gather、gather_input_ids_zos、gather_engram_tokens_zos、expand_swa_index、kv_index_cat、convert_kv_index、load_kv_index、store_mtp_info |
| P2 KV cache | ⬜ | |
| P3 MTP 链路 | ⬜ | |
| P4 采样 | ⬜ | |
| P5 端到端 | ⬜ | |
A/B 现状:**45 个用例全过**(`python scripts/h20_ops_ab.py`)。
### 过程中踩到的 Triton 语义坑(都已修,可复用)
1. **`tl.where` 两侧都会求值**。`tl.where(i == 0, 0, LOAD(i - 1))` 在 `i == 0` 时真的会读
`LOAD[-1]` → illegal memory access。凡是「边界索引用 where 兜住」的写法都要先
`tl.maximum(idx, 0)` 钳制。
2. **标量指针不能配 block mask**。`tl.load(PTR + scalar, mask=<block mask>)` 报
`Mask argument cannot be block type if pointer argument is not a block`。要
`PTR + scalar + tl.zeros((BLOCK,), tl.int32)` 广播成 block。
3. **一个 kernel 里两个分支用不同 tile 形状**编不过。`kv_index_cat` 拆成两个 kernel 分别
launch(同一 stream,语义不变)。
4. **视图的 storage offset 已含在 `data_ptr()` 里**。给 kernel 传
`out[:, w:w+k]` 后再加一次 `w` 就重复偏移了 —— 表现为后半段全是 `0x01010101` 之类的
uninitialized 内存。
5. **测试用例的容量也要算对**:`store_mtp_info` 的 pool 是 `slot * nextn + step` 寻址,
行数必须是 `(max_slot + 1) * nextn`,否则两个后端一起写越界,表现成互相不一致。

View File

@ -0,0 +1,69 @@
"""Logical top-k token indices to physical cache slots, in place, in Triton."""
from typing import Any
import torch
import triton
import triton.language as tl
from deepinfer.model_executor.layers.tilelang_ops.triton.common import next_power_of_2
@triton.jit
def _convert_kv_index_kernel(
TOPK_IDXS,
CONTEXT_LENS,
BLOCK_TABLES,
s,
k: tl.constexpr,
n: tl.constexpr,
block_size: tl.constexpr,
BLOCK_K: tl.constexpr,
):
sid = tl.program_id(0)
bid = tl.program_id(1)
base = (bid * s + sid) * k
offs = tl.arange(0, BLOCK_K)
context_len = tl.load(CONTEXT_LENS + bid * s + sid)
idx = tl.load(TOPK_IDXS + base + offs, mask=offs < k, other=-1)
# Entries past the context length are padding; the reference marks them -1 rather than
# translating them, so they must not go through the block-table lookup.
live = (offs < k) & (offs < context_len)
page = tl.load(BLOCK_TABLES + bid * n + idx // block_size, mask=live, other=0)
physical = page * block_size + idx % block_size
tl.store(TOPK_IDXS + base + offs, tl.where(live, physical, -1), mask=offs < k)
def convert_kv_index_tl(
s: int,
k: int,
n: int,
block_size: int,
) -> Any:
"""Build the kernel for a fixed query length, top-k and page size."""
block_k = next_power_of_2(k)
def launch(
topk_idxs: torch.Tensor,
context_lens: torch.Tensor,
block_tables: torch.Tensor,
) -> None:
b = topk_idxs.shape[0]
if b == 0 or s == 0:
return
_convert_kv_index_kernel[(s, b)](
topk_idxs,
context_lens,
block_tables,
s,
k=k,
n=n,
block_size=block_size,
BLOCK_K=block_k,
num_warps=4,
)
return launch

View File

@ -0,0 +1,134 @@
"""Per-request n-gram token windows for engram lookup, in Triton."""
from typing import Any
import torch
import triton
import triton.language as tl
from deepinfer.model_executor.layers.tilelang_ops.triton.common import next_power_of_2
@triton.jit
def _gather_engram_tokens_zos_kernel(
WIN_COMPRESSED,
CURR_FRESH,
PREV_SAMPLED,
PREV_PREDICT,
PREV_NUM_ACCEPTED,
REMAP,
LOOKUP_TABLE,
SEQLENS_Q,
CU_SEQLENS_Q,
OUT,
max_seqlen_q: tl.constexpr,
max_ngram_size: tl.constexpr,
window_size: tl.constexpr,
engram_compressed_pad_token: tl.constexpr,
image_sentinel: tl.constexpr,
BLOCK_Q: tl.constexpr,
):
seq_idx = tl.program_id(0)
sq = tl.load(SEQLENS_Q + seq_idx)
# `tl.where` evaluates both arms, so the index is clamped: at seq_idx == 0 the other arm
# would read CU_SEQLENS_Q[-1].
q_base = tl.where(seq_idx == 0, 0, tl.load(CU_SEQLENS_Q + tl.maximum(seq_idx - 1, 0)))
pidx = tl.load(REMAP + seq_idx)
# A remapped request reuses `nextn - accepted` tokens it already produced, so its window starts
# that much later; a fresh request starts at the beginning of the window.
nr = tl.where(pidx >= 0, (max_seqlen_q - 1) - tl.load(PREV_NUM_ACCEPTED + pidx), max_seqlen_q - 1)
base = window_size - nr
offs = tl.arange(0, BLOCK_Q)
active = offs < sq
# Once an image sentinel is seen the rest of the window is padding, so the r loop carries a
# per-lane `hit` flag forward instead of re-scanning.
hit = tl.zeros((BLOCK_Q,), dtype=tl.int1)
for r in tl.static_range(max_ngram_size):
from_current = offs >= r
c = offs - r
token_cur = tl.load(
PREV_SAMPLED + pidx + tl.zeros((BLOCK_Q,), tl.int32),
mask=active & from_current & (pidx >= 0),
other=0,
)
token_draft = tl.load(
PREV_PREDICT + pidx * (max_seqlen_q - 1) + (c - 1),
mask=active & from_current & (pidx >= 0) & (c > 0),
other=0,
)
token_fresh = tl.load(
CURR_FRESH + seq_idx * max_seqlen_q + c,
mask=active & from_current & (pidx < 0),
other=0,
)
token_window = tl.load(
WIN_COMPRESSED + seq_idx * window_size + base + offs - r,
mask=active & ~from_current,
other=0,
)
raw = tl.where(
from_current,
tl.where(pidx < 0, token_fresh, tl.where(c == 0, token_cur, token_draft)),
token_window,
)
# Fresh tokens are already vocabulary ids; replayed ones are compressed and need the map.
mapped = tl.load(LOOKUP_TABLE + raw, mask=active & from_current & (pidx >= 0), other=0)
token = tl.where(from_current & (pidx >= 0), mapped, raw)
is_image = token == image_sentinel
value = tl.where(hit | is_image, engram_compressed_pad_token, token)
tl.store(OUT + (q_base + offs) * max_ngram_size + r, value, mask=active)
hit = hit | is_image
def gather_engram_tokens_zos_tl(
max_seqlen_q: int,
max_ngram_size: int,
engram_compressed_pad_token: int,
image_sentinel: int,
) -> Any:
"""Build the kernel for a fixed draft depth and n-gram size."""
block_q = next_power_of_2(max_seqlen_q)
nextn = max_seqlen_q - 1
window_size = nextn + max_ngram_size - 1
def launch(
win_compressed: torch.Tensor,
curr_fresh: torch.Tensor,
prev_sampled_token_ids: torch.Tensor,
prev_predict_token_ids: torch.Tensor,
prev_num_accepted_tokens: torch.Tensor,
remap: torch.Tensor,
lookup_table: torch.Tensor,
seqlens_q: torch.Tensor,
cu_seqlens_q: torch.Tensor,
out_ngram_token_ids: torch.Tensor,
) -> None:
n_seq = curr_fresh.shape[0]
if n_seq == 0:
return
_gather_engram_tokens_zos_kernel[(n_seq,)](
win_compressed,
curr_fresh,
prev_sampled_token_ids,
prev_predict_token_ids,
prev_num_accepted_tokens,
remap,
lookup_table,
seqlens_q,
cu_seqlens_q,
out_ngram_token_ids,
max_seqlen_q=max_seqlen_q,
max_ngram_size=max_ngram_size,
window_size=window_size,
engram_compressed_pad_token=engram_compressed_pad_token,
image_sentinel=image_sentinel,
BLOCK_Q=block_q,
num_warps=4,
)
return launch

View File

@ -0,0 +1,79 @@
"""Per-request row expansion of previous-step token ids, in Triton."""
from typing import Any
import torch
import triton
import triton.language as tl
from deepinfer.model_executor.layers.tilelang_ops.triton.common import next_power_of_2
@triton.jit
def _gather_input_ids_zos_kernel(
FRESH_TOKENS,
PREV_SAMPLED,
PREV_PREDICT,
REMAP,
SEQLENS_Q,
CU_SEQLENS_Q,
OUT,
max_seqlen_q: tl.constexpr,
BLOCK_Q: tl.constexpr,
):
seq_idx = tl.program_id(0)
sq = tl.load(SEQLENS_Q + seq_idx)
# `tl.where` evaluates both arms, so the index is clamped: at seq_idx == 0 the other arm
# would read CU_SEQLENS_Q[-1].
q_base = tl.where(seq_idx == 0, 0, tl.load(CU_SEQLENS_Q + tl.maximum(seq_idx - 1, 0)))
pidx = tl.load(REMAP + seq_idx)
offs = tl.arange(0, BLOCK_Q)
active = offs < sq
# Fresh prompt tokens come from this request's own row; a remapped request instead replays the
# previous step's sampled token for position 0 and its draft predictions after that.
fresh = tl.load(FRESH_TOKENS + seq_idx * max_seqlen_q + offs, mask=active, other=0)
# `pidx` is a scalar, so the pointer has to be broadcast to a block before it can carry a
# block-shaped mask.
prev_sampled = tl.load(PREV_SAMPLED + pidx + tl.zeros((BLOCK_Q,), tl.int32), mask=active & (pidx >= 0) & (offs == 0), other=0)
prev_predict = tl.load(
PREV_PREDICT + pidx * (max_seqlen_q - 1) + (offs - 1),
mask=active & (pidx >= 0) & (offs > 0),
other=0,
)
value = tl.where(pidx < 0, fresh, tl.where(offs == 0, prev_sampled, prev_predict))
tl.store(OUT + q_base + offs, value, mask=active)
def gather_input_ids_zos_tl(max_seqlen_q: int) -> Any:
"""Build the kernel for a fixed draft depth; the request count stays a runtime length."""
block_q = next_power_of_2(max_seqlen_q)
def launch(
fresh_tokens: torch.Tensor,
prev_sampled_token_ids: torch.Tensor,
prev_predict_token_ids: torch.Tensor,
remap: torch.Tensor,
seqlens_q: torch.Tensor,
cu_seqlens_q: torch.Tensor,
out_input_ids: torch.Tensor,
) -> None:
n_seq = fresh_tokens.shape[0]
if n_seq == 0:
return
_gather_input_ids_zos_kernel[(n_seq,)](
fresh_tokens,
prev_sampled_token_ids,
prev_predict_token_ids,
remap,
seqlens_q,
cu_seqlens_q,
out_input_ids,
max_seqlen_q=max_seqlen_q,
BLOCK_Q=block_q,
num_warps=4,
)
return launch

View File

@ -0,0 +1,80 @@
"""Expand per-row (offset, length) into cache index rows, in Triton."""
from typing import Any
import torch
import triton
import triton.language as tl
from deepinfer.model_executor.layers.tilelang_ops.triton.common import cdiv, next_power_of_2
@triton.jit
def _load_kv_index_kernel(
OFFSET_LEN,
KV_INDEX,
FLATTEN_BLOCK_TABLES,
index_length: tl.constexpr,
block_size: tl.constexpr,
has_block_tables: tl.constexpr,
BLOCK: tl.constexpr,
):
row = tl.program_id(0)
col_block = tl.program_id(1)
offset = tl.load(OFFSET_LEN + row * 2 + 0)
length = tl.load(OFFSET_LEN + row * 2 + 1)
cols = col_block * BLOCK + tl.arange(0, BLOCK)
live = cols < length
if has_block_tables:
# Contiguous positions, then mapped through the page table into a physical slot.
pos = offset + cols
page = tl.load(FLATTEN_BLOCK_TABLES + pos // block_size, mask=live, other=0)
index = page * block_size + pos % block_size
elif block_size == 0:
index = offset + cols
else:
# Ring buffer confined to the block that `offset` falls in.
index = offset // block_size * block_size + (offset + cols) % block_size
tl.store(
KV_INDEX + row * index_length + cols,
tl.where(live, index, -1),
mask=cols < index_length,
)
def load_kv_index_tl(
index_length: int,
block_size: int,
has_block_tables: bool,
) -> Any:
"""Build the kernel for a fixed row length and addressing mode."""
block = min(2048, next_power_of_2(index_length))
def launch(
kv_index_offset_len: torch.Tensor,
kv_index: torch.Tensor,
flatten_block_tables: torch.Tensor,
) -> None:
n = kv_index_offset_len.shape[0]
if n == 0 or index_length == 0:
return
if flatten_block_tables is None:
# The contiguous mode takes `None` here, and Triton needs a real pointer even when the
# branch that would read it is compiled out.
flatten_block_tables = kv_index_offset_len
_load_kv_index_kernel[(n, cdiv(index_length, block))](
kv_index_offset_len,
kv_index,
flatten_block_tables,
index_length=index_length,
block_size=block_size,
has_block_tables=has_block_tables,
BLOCK=block,
num_warps=4,
)
return launch

View File

@ -0,0 +1,112 @@
"""Concatenate the window index and the shifted compressed index, in Triton.
Two launches rather than one fused kernel: the two halves have different widths and different
element rules (verbatim copy vs. conditional rebase), so keeping them separate avoids a
compile-time branch whose two arms would need the same tile shape.
"""
from typing import Any
import torch
import triton
import triton.language as tl
from deepinfer.model_executor.layers.tilelang_ops.triton.common import cdiv, next_power_of_2
@triton.jit
def _copy_window_kernel(
SRC,
DST,
total_q,
window_size: tl.constexpr,
out_stride: tl.constexpr,
BLOCK_M: tl.constexpr,
BLOCK_W: tl.constexpr,
):
row_block = tl.program_id(0)
col_block = tl.program_id(1)
rows = row_block * BLOCK_M + tl.arange(0, BLOCK_M)
cols = col_block * BLOCK_W + tl.arange(0, BLOCK_W)
mask = (rows[:, None] < total_q) & (cols[None, :] < window_size)
value = tl.load(SRC + rows[:, None] * window_size + cols[None, :], mask=mask, other=0)
tl.store(DST + rows[:, None] * out_stride + cols[None, :], value, mask=mask)
@triton.jit
def _shift_compressed_kernel(
SRC,
DST,
total_q,
topk_tokens: tl.constexpr,
out_stride: tl.constexpr,
shift: tl.constexpr,
BLOCK_M: tl.constexpr,
BLOCK_K: tl.constexpr,
):
row_block = tl.program_id(0)
col_block = tl.program_id(1)
rows = row_block * BLOCK_M + tl.arange(0, BLOCK_M)
cols = col_block * BLOCK_K + tl.arange(0, BLOCK_K)
mask = (rows[:, None] < total_q) & (cols[None, :] < topk_tokens)
value = tl.load(SRC + rows[:, None] * topk_tokens + cols[None, :], mask=mask, other=-1)
# `kv_ca_index_out` is already a view whose base sits at column `window_size`, so only the
# row stride is applied here. Padding stays -1; shifting it would make it a plausible slot.
tl.store(
DST + rows[:, None] * out_stride + cols[None, :],
tl.where(value == -1, -1, value + shift),
mask=mask,
)
def kv_index_cat_tl(
dtype: str,
window_size: int,
topk_tokens: int,
out_stride: int,
) -> Any:
"""Build the kernel for a fixed window width, top-k and output row stride."""
del dtype # the interface layer only ever passes int32
block_m = 8
block_w = min(1024, next_power_of_2(window_size))
block_k = min(1024, next_power_of_2(topk_tokens))
def launch(
kv_index: torch.Tensor,
kv_ca_index: torch.Tensor,
kv_index_out: torch.Tensor,
kv_ca_index_out: torch.Tensor,
total_k_swa: int,
total_k: int,
) -> None:
total_q = kv_index.shape[0]
if total_q == 0:
return
_copy_window_kernel[(cdiv(total_q, block_m), cdiv(window_size, block_w))](
kv_index,
kv_index_out,
total_q,
window_size=window_size,
out_stride=out_stride,
BLOCK_M=block_m,
BLOCK_W=block_w,
num_warps=4,
)
_shift_compressed_kernel[(cdiv(total_q, block_m), cdiv(topk_tokens, block_k))](
kv_ca_index,
kv_ca_index_out,
total_q,
topk_tokens=topk_tokens,
out_stride=out_stride,
shift=total_k + total_k_swa,
BLOCK_M=block_m,
BLOCK_K=block_k,
num_warps=4,
)
return launch

View File

@ -0,0 +1,95 @@
"""Copy the draft head's per-step outputs into the MTP pool, in Triton."""
from typing import Any
import torch
import triton
import triton.language as tl
from deepinfer.model_executor.layers.tilelang_ops.triton.common import cdiv, next_power_of_2
@triton.jit
def _store_mtp_info_kernel(
WRITE_SLOTS,
TOKEN_IDS,
LOGPROBS,
LOGPROBS_INDEX,
CONFIDENCE,
TOKEN_IDS_POOL,
LOGPROBS_POOL,
LOGPROBS_INDEX_POOL,
CONFIDENCE_POOL,
nextn: tl.constexpr,
vocab_size: tl.constexpr,
has_index: tl.constexpr,
has_confidence: tl.constexpr,
BLOCK_V: tl.constexpr,
):
pid = tl.program_id(0)
col_block = tl.program_id(1)
req = pid // nextn
step = pid % nextn
# Slot 0 means "nothing to write" -- the pool row is pre-seeded by the caller.
slot = tl.load(WRITE_SLOTS + req)
if slot != 0:
cols = col_block * BLOCK_V + tl.arange(0, BLOCK_V)
mask = cols < vocab_size
src = (req * nextn + step) * vocab_size + cols
dst = (slot * nextn + step) * vocab_size + cols
logprobs = tl.load(LOGPROBS + src, mask=mask, other=0.0)
tl.store(LOGPROBS_POOL + dst, logprobs, mask=mask)
if has_index:
logprobs_index = tl.load(LOGPROBS_INDEX + src, mask=mask, other=0)
tl.store(LOGPROBS_INDEX_POOL + dst, logprobs_index, mask=mask)
# The token ids and confidences are per (request, step), not per vocabulary entry, so only
# the first column block writes them.
if (col_block == 0) & (step == 0):
for i in tl.static_range(nextn):
token = tl.load(TOKEN_IDS + req * nextn + i)
tl.store(TOKEN_IDS_POOL + slot * nextn + i, token)
if has_confidence:
conf = tl.load(CONFIDENCE + req * nextn + i)
tl.store(CONFIDENCE_POOL + slot * nextn + i, conf)
def store_mtp_info_tl(nextn: int, vocab_size: int, has_index: bool, has_confidence: bool) -> Any:
"""Build the kernel for a fixed draft depth and vocabulary size."""
block_v = min(next_power_of_2(vocab_size), 4096)
def launch(
write_slots: torch.Tensor,
token_ids: torch.Tensor,
logprobs: torch.Tensor,
logprobs_index: torch.Tensor,
confidence: torch.Tensor,
token_ids_pool: torch.Tensor,
logprobs_pool: torch.Tensor,
logprobs_index_pool: torch.Tensor,
confidence_pool: torch.Tensor,
) -> None:
req_cnt = write_slots.shape[0]
if req_cnt == 0:
return
_store_mtp_info_kernel[(req_cnt * nextn, cdiv(vocab_size, block_v))](
write_slots,
token_ids,
logprobs,
logprobs_index,
confidence,
token_ids_pool,
logprobs_pool,
logprobs_index_pool,
confidence_pool,
nextn=nextn,
vocab_size=vocab_size,
has_index=has_index,
has_confidence=has_confidence,
BLOCK_V=block_v,
num_warps=8,
)
return launch

View File

@ -0,0 +1,58 @@
"""Expand four per-row pivots into a full sliding-window index row, in Triton."""
from typing import Any
import torch
import triton
import triton.language as tl
from deepinfer.model_executor.layers.tilelang_ops.triton.common import cdiv, next_power_of_2
@triton.jit
def _expand_swa_index_kernel(
PIVOTS,
OUTPUT,
n,
width: tl.constexpr,
BLOCK_W: tl.constexpr,
):
row = tl.program_id(0)
col_block = tl.program_id(1)
if row < n:
offset1 = tl.load(PIVOTS + row * 4 + 0)
len1 = tl.load(PIVOTS + row * 4 + 1)
offset2 = tl.load(PIVOTS + row * 4 + 2)
len2 = tl.load(PIVOTS + row * 4 + 3)
cols = col_block * BLOCK_W + tl.arange(0, BLOCK_W)
# Both spans are contiguous ranges; past the second one the column is padding.
value = tl.where(
cols < len1,
offset1 + cols,
tl.where(cols < len1 + len2, offset2 - len1 + cols, -1),
)
tl.store(OUTPUT + row * width + cols, value, mask=cols < width)
def expand_swa_index_tl(width: int) -> Any:
"""Build the kernel for a fixed row width."""
# A row is written by BLOCK_W-wide blocks; 1024 int32 per block is one coalesced pass with a
# comfortable register budget, and narrower rows simply use the whole width in one block.
block_w = min(1024, next_power_of_2(width))
def launch(pivots: torch.Tensor, output: torch.Tensor) -> None:
n = pivots.shape[0]
if n == 0:
return
_expand_swa_index_kernel[(n, cdiv(width, block_w))](
pivots,
output,
n,
width=width,
BLOCK_W=block_w,
num_warps=4,
)
return launch

View File

@ -15,7 +15,14 @@ from typing import Any
import torch
from deepinfer.model_executor.layers.tilelang_ops.flash_mla_utils import convert_kv_index
from deepinfer.model_executor.layers.tilelang_ops.gather import gather
from deepinfer.model_executor.layers.tilelang_ops.gather_engram_tokens_zos import gather_engram_tokens_zos
from deepinfer.model_executor.layers.tilelang_ops.gather_input_ids_zos import gather_input_ids_zos
from deepinfer.model_executor.layers.tilelang_ops.get_kv_index import get_kv_ca_index_decode, get_kv_ca_index_prefill
from deepinfer.model_executor.layers.tilelang_ops.kv_index_cat import kv_index_cat
from deepinfer.model_executor.layers.tilelang_ops.store_mtp_info import store_mtp_info
from deepinfer.model_executor.layers.tilelang_ops.swa_index import expand_swa_index
def _cases_gather() -> list[tuple[str, bool, Callable[[], Any]]]:
@ -48,6 +55,249 @@ def _cases_gather() -> list[tuple[str, bool, Callable[[], Any]]]:
]
def _cases_gather_input_ids() -> list[tuple[str, bool, Callable[[], Any]]]:
"""Draft-token replay: remapped requests replay previous ids, fresh ones use their own row."""
def make(n_seq: int, max_seqlen_q: int, prev_n_seq: int, mix: bool):
def run() -> Any:
torch.manual_seed(0)
nextn = max_seqlen_q - 1
fresh = torch.randint(0, 1000, (n_seq, max_seqlen_q), device="cuda", dtype=torch.int32)
prev_sampled = torch.randint(0, 1000, (prev_n_seq,), device="cuda", dtype=torch.int32)
prev_predict = torch.randint(0, 1000, (prev_n_seq, nextn), device="cuda", dtype=torch.int32)
if mix:
remap = torch.tensor([-1 if i % 2 == 0 else i % prev_n_seq for i in range(n_seq)], device="cuda", dtype=torch.int32)
else:
remap = torch.full((n_seq,), -1, device="cuda", dtype=torch.int32)
seqlens = torch.randint(1, max_seqlen_q + 1, (n_seq,), device="cuda", dtype=torch.int32)
cu = torch.cumsum(seqlens, 0, dtype=torch.int32)
out = torch.full((int(cu[-1].item()),), -7, device="cuda", dtype=torch.int32)
gather_input_ids_zos(fresh, prev_sampled, prev_predict, remap, seqlens, cu, out, max_seqlen_q=max_seqlen_q)
return out
return run
return [
("n8_nextn5_mixed", True, make(8, 6, 8, True)),
("n8_nextn5_all_fresh", True, make(8, 6, 8, False)),
("n1_nextn1_mixed", True, make(1, 2, 3, True)),
("n64_nextn5_mixed", True, make(64, 6, 64, True)),
("n5_nextn2_full_len", True, make(5, 3, 5, True)),
]
def _cases_gather_engram() -> list[tuple[str, bool, Callable[[], Any]]]:
"""Engram n-gram windows, including the image-sentinel padding rule."""
def make(n_seq: int, max_seqlen_q: int, max_ngram: int, vocab: int, with_image: bool):
def run() -> Any:
torch.manual_seed(0)
nextn = max_seqlen_q - 1
window = nextn + max_ngram - 1
pad_token = 7
sentinel = -1
win = torch.randint(0, 50, (n_seq, window), device="cuda", dtype=torch.int32)
if with_image:
win[:, window // 2] = sentinel
fresh = torch.randint(0, 50, (n_seq, max_seqlen_q), device="cuda", dtype=torch.int32)
if with_image:
fresh[:, -1] = sentinel
prev_sampled = torch.randint(0, vocab, (n_seq,), device="cuda", dtype=torch.int32)
prev_predict = torch.randint(0, vocab, (n_seq, nextn), device="cuda", dtype=torch.int32)
prev_accepted = torch.randint(0, nextn + 1, (n_seq,), device="cuda", dtype=torch.int32)
remap = torch.tensor([-1 if i % 2 == 0 else i for i in range(n_seq)], device="cuda", dtype=torch.int32)
lookup = torch.arange(vocab, device="cuda", dtype=torch.int32) % 13
seqlens = torch.randint(1, max_seqlen_q + 1, (n_seq,), device="cuda", dtype=torch.int32)
cu = torch.cumsum(seqlens, 0, dtype=torch.int32)
out = torch.full((int(cu[-1].item()), max_ngram), -7, device="cuda", dtype=torch.int32)
gather_engram_tokens_zos(
win,
fresh,
prev_sampled,
prev_predict,
prev_accepted,
remap,
lookup,
seqlens,
cu,
out,
max_seqlen_q=max_seqlen_q,
max_ngram_size=max_ngram,
engram_compressed_pad_token=pad_token,
image_sentinel=sentinel,
)
return out
return run
return [
("n8_nextn5_ngram3", True, make(8, 6, 3, 64, False)),
("n8_nextn5_ngram1", True, make(8, 6, 1, 64, False)),
("n8_nextn5_ngram3_image", True, make(8, 6, 3, 64, True)),
("n1_nextn1_ngram2", True, make(1, 2, 2, 64, False)),
]
def _cases_swa_index() -> list[tuple[str, bool, Callable[[], Any]]]:
"""Sliding-window index expansion for the widths the engine uses."""
def make(n: int, width: int, overflow: bool):
def run() -> Any:
torch.manual_seed(0)
pivots = torch.zeros((n, 4), device="cuda", dtype=torch.int32)
pivots[:, 0] = torch.randint(0, 4096, (n,), device="cuda", dtype=torch.int32)
if overflow:
# len1 + len2 can exceed width; the third branch must still win past len1 + len2.
pivots[:, 1] = torch.randint(0, width + 1, (n,), device="cuda", dtype=torch.int32)
pivots[:, 2] = torch.randint(0, 4096, (n,), device="cuda", dtype=torch.int32)
pivots[:, 3] = torch.randint(0, width + 1, (n,), device="cuda", dtype=torch.int32)
else:
total = torch.randint(0, width + 1, (n,), device="cuda", dtype=torch.int32)
pivots[:, 1] = total // 2
pivots[:, 2] = torch.randint(0, 4096, (n,), device="cuda", dtype=torch.int32)
pivots[:, 3] = total - total // 2
return expand_swa_index(pivots, width)
return run
return [
("n32_w512", True, make(32, 512, False)),
("n32_w513_overflow", True, make(32, 513, True)),
("n1_w1", True, make(1, 1, False)),
("n37_w2048_overflow", True, make(37, 2048, True)),
("n8_w64", True, make(8, 64, False)),
]
def _cases_kv_index_cat() -> list[tuple[str, bool, Callable[[], Any]]]:
"""Window + compressed index concatenation with the -1 padding preserved."""
def make(total_q: int, window: int, topk: int):
def run() -> Any:
torch.manual_seed(0)
kv_index = torch.randint(-1, 4000, (total_q, window), device="cuda", dtype=torch.int32)
kv_ca = torch.randint(-1, 300, (total_q, topk), device="cuda", dtype=torch.int32)
return kv_index_cat(kv_index, kv_ca, 512, 2000)
return run
return [
("q8_w512_k128", True, make(8, 512, 128)),
("q1_w64_k32", True, make(1, 64, 32)),
("q5_w2048_k1024", True, make(5, 2048, 1024)),
]
def _cases_convert_kv_index() -> list[tuple[str, bool, Callable[[], Any]]]:
"""Logical to physical index translation through a page table."""
def make(b: int, s: int, k: int, block_size: int, max_blocks: int):
def run() -> Any:
torch.manual_seed(0)
topk = torch.randint(0, max_blocks * block_size, (b, s, k), device="cuda", dtype=torch.int32)
ctx = torch.randint(0, k + 1, (b, s), device="cuda", dtype=torch.int32)
table = torch.randint(0, 50, (b, max_blocks), device="cuda", dtype=torch.int32)
convert_kv_index(topk, block_size, ctx, table)
return topk
return run
return [
("b2_s4_k64_bs64", True, make(2, 4, 64, 64, 32)),
("b1_s1_k128_bs128", True, make(1, 1, 128, 128, 16)),
("b3_s2_k16_bs8", True, make(3, 2, 16, 8, 64)),
]
def _cases_load_kv_index() -> list[tuple[str, bool, Callable[[], Any]]]:
"""Prefill (contiguous) and decode (paged) index expansion."""
def make_prefill(n: int, length: int):
def run() -> Any:
torch.manual_seed(0)
off_len = torch.zeros((n, 2), device="cuda", dtype=torch.int32)
off_len[:, 0] = torch.randint(0, 500, (n,), device="cuda", dtype=torch.int32)
off_len[:, 1] = torch.randint(0, length + 1, (n,), device="cuda", dtype=torch.int32)
return get_kv_ca_index_prefill(off_len, length)
return run
def make_decode(n: int, length: int, tpb: int, num_blocks: int):
def run() -> Any:
torch.manual_seed(0)
off_len = torch.zeros((n, 2), device="cuda", dtype=torch.int32)
off_len[:, 0] = torch.randint(0, 500, (n,), device="cuda", dtype=torch.int32)
off_len[:, 1] = torch.randint(0, length + 1, (n,), device="cuda", dtype=torch.int32)
table = torch.randint(0, num_blocks, (num_blocks,), device="cuda", dtype=torch.int32)
return get_kv_ca_index_decode(off_len, length, tpb, table)
return run
return [
("prefill_n8_len512", True, make_prefill(8, 512)),
("prefill_n1_len1024", True, make_prefill(1, 1024)),
("prefill_n33_len100", True, make_prefill(33, 100)),
("decode_n8_len512_tpb64", True, make_decode(8, 512, 64, 128)),
("decode_n5_len128_tpb128", True, make_decode(5, 128, 128, 64)),
]
def _cases_store_mtp_info() -> list[tuple[str, bool, Callable[[], Any]]]:
"""Draft logprob pooling, including the slot-0 no-write path."""
def make(req_cnt: int, nextn: int, vocab: int, has_index: bool, has_confidence: bool, zero_slots: bool):
def run() -> Any:
torch.manual_seed(0)
# The pool is addressed as slot * nextn + step and slots run 0..req_cnt, so it needs
# room for every slot including the reserved one.
pool_rows = (req_cnt + 1) * nextn
write_slots = torch.arange(1, req_cnt + 1, device="cuda", dtype=torch.int32)
if zero_slots:
write_slots[0] = 0
token_ids = torch.randint(0, vocab, (req_cnt, nextn), device="cuda", dtype=torch.int32)
logprobs = torch.randn((req_cnt, nextn, vocab), device="cuda", dtype=torch.float32)
logprobs_index = torch.randint(0, vocab, (req_cnt, nextn, vocab), device="cuda", dtype=torch.int32)
confidence = torch.rand((req_cnt, nextn), device="cuda", dtype=torch.float32)
token_ids_pool = torch.full((pool_rows,), -7, device="cuda", dtype=torch.int32)
logprobs_pool = torch.full((pool_rows, vocab), -7.0, device="cuda", dtype=torch.float32)
logprobs_index_pool = torch.full((pool_rows, vocab), -7, device="cuda", dtype=torch.int32)
confidence_pool = torch.full((pool_rows,), -7.0, device="cuda", dtype=torch.float32)
store_mtp_info(
write_slots,
token_ids,
logprobs,
logprobs_index,
confidence,
token_ids_pool,
logprobs_pool,
logprobs_index_pool,
confidence_pool,
)
return (
token_ids_pool.clone(),
logprobs_pool.clone(),
logprobs_index_pool.clone(),
confidence_pool.clone(),
)
return run
return [
("n4_nextn5_v1k_full", True, make(4, 5, 1024, True, True, False)),
("n4_nextn5_v1k_no_index", True, make(4, 5, 1024, False, True, False)),
("n4_nextn5_v1k_no_conf", True, make(4, 5, 1024, True, False, False)),
("n4_nextn5_v1k_slot0", True, make(4, 5, 1024, True, True, True)),
("n2_nextn1_v129280", True, make(2, 1, 129280, True, True, False)),
]
CASES: dict[str, Callable[[], list[tuple[str, bool, Callable[[], Any]]]]] = {
"gather_tl": _cases_gather,
"gather_input_ids_zos_tl": _cases_gather_input_ids,
"gather_engram_tokens_zos_tl": _cases_gather_engram,
"expand_swa_index_tl": _cases_swa_index,
"kv_index_cat_tl": _cases_kv_index_cat,
"convert_kv_index_tl": _cases_convert_kv_index,
"load_kv_index_tl": _cases_load_kv_index,
"store_mtp_info_tl": _cases_store_mtp_info,
}

View File

@ -81,6 +81,17 @@ def compare(name: str, expected: Any, actual: Any, *, exact: bool, rtol: float =
print(f" FAIL {name}: max abs {diff.max().item():.3e}, max rel {(diff / scale).max().item():.3e}")
return bool(close)
if isinstance(expected, (tuple, list)):
# A case that returns several tensors is only right if every element is; recurse so each
# one gets its own report instead of collapsing into one boolean.
if len(expected) != len(actual):
print(f" FAIL {name}: {len(expected)} outputs vs {len(actual)}")
return False
ok = True
for i, (e, a) in enumerate(zip(expected, actual, strict=True)):
ok &= compare(f"{name}[{i}]", e, a, exact=exact, rtol=rtol, atol=atol)
return ok
same = expected == actual
if not same:
print(f" FAIL {name}: {expected!r} vs {actual!r}")
@ -113,7 +124,8 @@ def main() -> None:
try:
expected, actual = run_both(kernel, fn)
except Exception as exc: # noqa: BLE001 - a raise on one backend is itself a result
print(f" FAIL {case_name}: {type(exc).__name__}: {str(exc).splitlines()[0][:160]}")
lines = str(exc).splitlines()
print(f" FAIL {case_name}: {type(exc).__name__}: {lines[0][:160] if lines else ''}")
failures += 1
continue
if not compare(case_name, expected, actual, exact=exact):