DeepInfer/scripts/ab_cases.py
loopyt 4f3edbf604 feat(triton): complete P1 with norm, lookup and metadata kernels
norm, map_lookup, expand_sampling_params, prepare_verification_metadata, copy_block_tables.
Plus the measured parameterisation that made the port tractable: `DEEPINFER_TRACE_KERNEL_ARGS=1`
records each factory's construction arguments, which showed that e.g. `norm_tl` is only ever built
as one combination on this path (d=5120, bf16 source, fp8 out, per-128 fp32 scales, col-major,
round_sf, no residual) rather than the dozen the signature allows.

Three findings worth keeping:

`norm` has to round the fp32 normalised value back to bf16 *before* computing the per-block amax
and the scale. That is what makes its fp8 output bit-identical to quantising the bf16 result
separately, which is how the engine defines the reference. Without it the fp8 bytes scatter by an
ulp wherever a value lands on a rounding boundary -- which looked like a scale bug at first
(224 vs 240) and was not.

With `round_sf=True`, which is the production setting, fp8 output and every scale factor come out
byte-identical. With `round_sf=False` 240 of 327680 bytes differ by one ulp, because `amax/448`
depends on the order of a 5120-element reduction and two compilers will not agree on that order.
For the same reason the bf16 output differs on 10 of 655360 elements by one ulp. Both are
documented, and the A/B harness now picks its tolerance from the dtype instead of one global pair.

`map_lookup` resolves duplicate keys by taking the last match, not the maximum: the TileLang kernel
overwrites on every hit while its own Python reference uses `max`, and the two agree only when keys
are unique. Top-k indices always are, so the Triton version matches the kernel -- and the case
generator now produces unique keys, with a comment saying why.

13 kernels and 53 cases now agree with the baseline.
2026-10-22 00:28:38 +08:00

466 lines
22 KiB
Python

"""Per-kernel A/B cases: run the public interface function once per backend and compare.
Each entry maps a kernel name to a builder returning ``(case_name, exact, callable)`` triples.
``exact=True`` demands bitwise equality, which is right for index and metadata kernels; floating
point kernels are compared with a tolerance and are labelled ``exact=False``.
Shapes and dtypes reuse the ones the engine actually passes, taken from the TileLang test matrices
in ``cuda/*.py`` and from the traced inference run.
"""
from __future__ import annotations
from collections.abc import Callable
from typing import Any
import torch
from deepinfer.model_executor.layers.tilelang_ops import ops
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.norm import norm
from deepinfer.model_executor.layers.tilelang_ops.sampler import map_lookup
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]]]:
"""Row gather across the widths the engine uses: metadata rows, heads, and hidden states."""
def make(n: int, rows: int, d: int, dtype: torch.dtype, index_dtype: torch.dtype, strided: bool):
def run() -> Any:
torch.manual_seed(0)
base = (
torch.randn(n, d, device="cuda").to(dtype)
if dtype.is_floating_point
else torch.randint(0, 100, (n, d), device="cuda", dtype=dtype)
)
x = base[:, ::1] if strided else base
index = torch.randint(-3, n + 3, (rows,), device="cuda", dtype=index_dtype)
return gather(x, index)
return run
return [
("n1k_m233_d128_int32_idx", True, make(1024, 233, 128, torch.int8, torch.int32, False)),
("n1k_m1_d4_int8", True, make(1024, 1, 4, torch.int8, torch.int32, False)),
("n64_m256_d528_bytes", True, make(64, 256, 528, torch.int8, torch.int32, False)),
("n512_m300_d4096_bytes", True, make(512, 300, 4096, torch.int8, torch.int32, False)),
("n512_m300_d7168_int64_idx", True, make(512, 300, 7168, torch.int8, torch.int64, False)),
("n256_m256_d4096_bf16", True, make(256, 256, 4096, torch.bfloat16, torch.int32, False)),
("n256_m0_d128_empty", True, make(256, 0, 128, torch.int8, torch.int32, False)),
("n1_m17_d128_single_row", True, make(1, 17, 128, torch.int8, torch.int32, False)),
("n300_m233_d1025_odd", True, make(300, 233, 1025, torch.int8, torch.int32, False)),
]
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)),
]
def _cases_norm() -> list[tuple[str, bool, Callable[[], Any]]]:
"""RMSNorm in the forms the engine uses: bf16 out, fp8 with fp32 scales, and both at once."""
def make(rows: int, d: int, with_residual: bool, as_fp8: bool, return_bf16: bool, round_sf: bool):
def run() -> Any:
torch.manual_seed(0)
x = torch.randn((rows, d), device="cuda", dtype=torch.bfloat16)
w = torch.randn((d,), device="cuda", dtype=torch.bfloat16)
residual = torch.randn((rows, d), device="cuda", dtype=torch.bfloat16) if with_residual else None
kwargs: dict[str, Any] = {"eps": 1e-20, "out_scale": 1.5}
if as_fp8:
kwargs.update(
fmt="e4m3",
num_per_channels=128,
round_sf=round_sf,
use_tma_aligned_col_major_sf=True,
)
return norm(x, w, residual=residual, return_bf16=return_bf16, **kwargs)
return run
return [
("r128_d5120_bf16", False, make(128, 5120, False, False, False, False)),
("r128_d5120_bf16_residual", False, make(128, 5120, True, False, False, False)),
("r64_d5120_fp8_round", False, make(64, 5120, False, True, False, True)),
("r64_d5120_fp8_no_round", False, make(64, 5120, False, True, False, False)),
("r64_d5120_fp8_round_bf16", False, make(64, 5120, False, True, True, True)),
("r33_d5120_fp8_round_bf16", False, make(33, 5120, False, True, True, True)),
("r16_d7168_fp8_round", False, make(16, 7168, False, True, False, True)),
("r8_d512_fp8_round", False, make(8, 512, False, True, False, True)),
]
def _cases_map_lookup() -> list[tuple[str, bool, Callable[[], Any]]]:
"""Both lookup modes, including out-of-range and missing keys."""
def make_index(rows: int, width: int, dtype: torch.dtype):
def run() -> Any:
torch.manual_seed(0)
value = (
torch.randn((rows, width), device="cuda", dtype=dtype)
if dtype.is_floating_point
else torch.randint(0, 1000, (rows, width), device="cuda", dtype=dtype)
)
query = torch.randint(-width, 2 * width, (rows, 1), device="cuda", dtype=torch.int32)
return map_lookup(None, value, query, mode="by_index")
return run
def make_key(rows: int, width: int):
def run() -> Any:
torch.manual_seed(0)
# Distinct keys per row: that is what the indexer produces, and it is also the only
# case where the kernel (last match wins) and the Python reference (max) agree.
key = torch.randperm(width, device="cuda").to(torch.int32).unsqueeze(0).repeat(rows, 1)
value = torch.randn((rows, width), device="cuda", dtype=torch.float32)
# Half the queries exist in the row, half do not.
present = key.gather(-1, torch.randint(0, width, (rows, 1), device="cuda").long())
missing = torch.full((rows, 1), 10_000, device="cuda", dtype=torch.int32)
keep = torch.randint(0, 2, (rows, 1), device="cuda") == 1
query = torch.where(keep, present, missing)
return map_lookup(key, value, query, mode="by_key")
return run
return [
("index_f32", True, make_index(256, 4096, torch.float32)),
("index_i32", True, make_index(256, 4096, torch.int32)),
("key_f32", False, make_key(128, 4096)),
]
def _cases_expand_sampling_params() -> list[tuple[str, bool, Callable[[], Any]]]:
"""Per-row parameter expansion for the draft rows."""
def make(req_cnt: int, total_q: int):
def run() -> Any:
torch.manual_seed(0)
temps = torch.rand((req_cnt,), device="cuda", dtype=torch.float32)
top_ps = torch.rand((req_cnt,), device="cuda", dtype=torch.float32)
top_ks = torch.randint(0, 4096, (req_cnt,), device="cuda", dtype=torch.int32)
positions = torch.randint(0, 1000, (req_cnt,), device="cuda", dtype=torch.int32)
seeds = torch.randint(0, 2**62, (req_cnt,), device="cuda", dtype=torch.uint64)
row_to_req = torch.randint(0, req_cnt, (total_q,), device="cuda", dtype=torch.int32)
local_step = torch.randint(0, 6, (total_q,), device="cuda", dtype=torch.int32)
outs = [
torch.empty((total_q,), device="cuda", dtype=torch.float32),
torch.empty((total_q,), device="cuda", dtype=torch.float32),
torch.empty((total_q,), device="cuda", dtype=torch.int32),
torch.empty((total_q,), device="cuda", dtype=torch.int32),
torch.empty((total_q,), device="cuda", dtype=torch.uint64),
]
kernel = ops.expand_sampling_params_tl()
kernel(temps, top_ps, top_ks, positions, seeds, row_to_req, local_step, *outs)
return tuple(outs)
return run
return [("n4_q32", True, make(4, 32)), ("n1_q5", True, make(1, 5)), ("n8_q100", True, make(8, 100))]
def _cases_prepare_verification() -> list[tuple[str, bool, Callable[[], Any]]]:
"""Draft-row metadata: request index, local step and the verify flag."""
def make(req_cnt: int, total_q: int, max_len: int):
def run() -> Any:
torch.manual_seed(0)
seqlen_q = torch.randint(1, max_len + 1, (req_cnt,), device="cuda", dtype=torch.int32)
ftp = torch.zeros((req_cnt,), device="cuda", dtype=torch.int32)
ftp[1:] = torch.cumsum(seqlen_q, 0, dtype=torch.int32)[:-1]
row_to_req = torch.full((total_q,), -7, device="cuda", dtype=torch.int32)
local_step = torch.full((total_q,), -7, device="cuda", dtype=torch.int32)
is_verify = torch.zeros((total_q,), device="cuda", dtype=torch.bool)
# Driven through the factory: the interface wrapper allocates its own outputs, which
# would leave nothing to compare.
kernel = ops.prepare_verification_metadata_tl(max_len)
kernel(ftp, seqlen_q, row_to_req, local_step, is_verify)
return row_to_req, local_step, is_verify
return run
return [("n4_q24", True, make(4, 24, 6)), ("n1_q1", True, make(1, 1, 1)), ("n8_q48", True, make(8, 48, 6))]
def _cases_copy_block_tables() -> list[tuple[str, bool, Callable[[], Any]]]:
"""Compact 1D block table expanded to one row per token."""
def make(n_seq: int, max_seqlen_q: int, max_blocks: int):
def run() -> Any:
torch.manual_seed(0)
seqlens = torch.randint(1, max_seqlen_q + 1, (n_seq,), device="cuda", dtype=torch.int32)
cu = torch.cumsum(seqlens, 0, dtype=torch.int32)
total_q = int(cu[-1].item())
counts = torch.randint(1, max_blocks + 1, (n_seq,), device="cuda", dtype=torch.int32)
offsets = torch.zeros((n_seq + 1,), device="cuda", dtype=torch.int32)
offsets[1:] = torch.cumsum(counts, 0, dtype=torch.int32)
flat = torch.randint(0, 512, (int(offsets[-1].item()),), device="cuda", dtype=torch.int32)
out = torch.full((total_q, max_blocks), -7, device="cuda", dtype=torch.int32)
# No standalone interface function: this kernel is only reached from
# `prepare_v4_decode_metadata`, so the case drives the factory directly. The harness
# swaps `ops.copy_block_tables_tl` between backends, which is all that is needed.
kernel = ops.copy_block_tables_tl(max_seqlen_q, max_blocks)
kernel(seqlens, cu, flat, offsets, out)
return out
return run
return [
("n4_q6_b64", True, make(4, 6, 64)),
("n1_q1_b32", True, make(1, 1, 32)),
("n8_q6_b1400_multi_chunk", True, make(8, 6, 1400)),
]
CASES: dict[str, Callable[[], list[tuple[str, bool, Callable[[], Any]]]]] = {
"norm_tl": _cases_norm,
"map_lookup_tl": _cases_map_lookup,
"expand_sampling_params_tl": _cases_expand_sampling_params,
"prepare_verification_metadata_tl": _cases_prepare_verification,
"copy_block_tables_tl": _cases_copy_block_tables,
"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,
}