DeepInfer/scripts/ab_cases.py
loopyt afbfdb6c7a feat(triton): port the sampler, bit-exact on the draw and honest about the rest
`fused_sample` for the configuration the engine builds: 4096 columns after the force_topk cut,
sampling on, rejection off. With one tile the baseline's two-level search collapses -- the coarse
offset *is* the token id -- so this implements log-softmax, the top-k/top-p cut as a threshold plus
the tie rule, renormalisation, a fixed-point cumulative distribution and one Philox draw.

The draw matches cuRAND bit for bit (that is what the previous commit established). The rest does
not all match, and the split is measured rather than claimed:

    engine's configuration (top_p=0.99, 2644 kept columns)   tokens identical on every row
    narrow cuts (top_k=128, top_p=0.5)                       ~90% of rows

Kept sets match column for column and log-probabilities agree to ~1e-5 in every case. What differs
is the token, and the cause is a discontinuity in the baseline rather than a defect here: the draw
is `d % total` with `total = sum(floor(p_i * 2^31))`, and the baseline's rejection limit
`(2^32-1) // total * total` steps from `2 * total` to `1 * total` exactly at `total == 2^31` -- which
`total` sits next to, because the probabilities sum to one. On the near side almost no draw is
rejected, on the far side about half are, and each rejection moves to the next draw. A few units of
rounding error in `total`, which is a reduction order that cannot match, therefore change the token
outright. Clamping `total` under 2^31 does not help: the baseline crosses in either direction too,
so it only relocates the disagreement (30 rows instead of 11).

Four bugs found on the way, all worth recording because each looked like something else:

- Triton evaluates `//`, `%` and `<=` on **uint32 operands with signed arithmetic**, so 4294967295
  behaves as -1. That made `limit` come out as `2^32 - total`, flipping the rejection decision.
  Fixed by doing the whole cumulative distribution in int64.
- Nested runtime `if`s produced a threshold that was not the one assigned inside the branch; the
  cuts are now combined with `tl.where` instead.
- The threshold was taken from the probability array and compared against logits. The sort has to be
  of the logits (equivalent, since the map is monotone -- the mass is recomputed from the sorted
  logits rather than sorting twice).
- Renormalising by division rather than through the log domain, as the baseline does, moved the
  token agreement from 76% to 91% before the remaining gap was understood.

Also gives the A/B harness a fourth case element: an agreement floor for outputs whose answer is a
random draw, so the rate is reported rather than compared for equality -- a comparison that can pass
without testing anything would be worse than none.
2026-10-22 08:55:10 +08:00

974 lines
48 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.cache_common import (
FP8_FLOAT32_SF_INDEX_KV_FORMAT,
V41_FORMAT,
)
from deepinfer.model_executor.layers.tilelang_ops.compress import (
compress_and_update_state_decode,
compress_and_update_state_prefill,
)
from deepinfer.model_executor.layers.tilelang_ops.flash_mla_utils import convert_kv_index
from deepinfer.model_executor.layers.tilelang_ops.fused_sample import fused_sample
from deepinfer.model_executor.layers.tilelang_ops.fused_transform import (
CastArgs,
KVCacheOutArgs,
NormArgs,
RopeArgs,
fused_norm_rope_hadamard_cast_store_kvcache,
)
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.load_paged_kv_cache import load_paged_kv_cache
from deepinfer.model_executor.layers.tilelang_ops.norm import norm
from deepinfer.model_executor.layers.tilelang_ops.prepare_v4_decode_metadata import prepare_v4_decode_metadata
from deepinfer.model_executor.layers.tilelang_ops.sampler import map_lookup
from deepinfer.model_executor.layers.tilelang_ops.store_kv_cache import store_kv_cache
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
from deepinfer.model_executor.sampling_inputs import LogitsTransformInputs, SamplingKernelInputs
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, max_len: int, build_steps: int):
def run() -> Any:
torch.manual_seed(0)
seqlen_q = torch.randint(1, max_len + 1, (req_cnt,), device="cuda", dtype=torch.int32)
cu = torch.cumsum(seqlen_q, 0, dtype=torch.int32)
total_q = int(cu[-1].item())
ftp = torch.zeros((req_cnt,), device="cuda", dtype=torch.int32)
ftp[1:] = cu[:-1]
# The sentinel makes "wrote nothing" visible. `row_to_req` is a `torch.empty` buffer in
# the engine, so a kernel that fills fewer than `total_q` rows leaves garbage behind and
# every consumer then indexes with it -- and both backends would still agree with each
# other here, which is why this check has to be explicit rather than a comparison.
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(build_steps)
kernel(ftp, seqlen_q, row_to_req, local_step, is_verify)
assert int((row_to_req == -7).sum()) == 0, "kernel left rows unwritten"
assert int((local_step == -7).sum()) == 0, "kernel left rows unwritten"
return row_to_req, local_step, is_verify
return run
return [
("n4_maxlen6", True, make(4, 6, 6)),
("n1_maxlen1", True, make(1, 1, 1)),
("n8_maxlen6", True, make(8, 6, 6)),
# `num_steps=0` is what the engine builds when speculative decoding is off, so it must
# still write every row.
("n8_zero_steps", True, make(8, 6, 0)),
# A request longer than one tile (32 lanes) exercises the runtime tile loop.
("n2_maxlen130", True, make(2, 130, 130)),
("n64_maxlen6", True, make(64, 6, 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)),
]
def _cases_store_kv_cache() -> list[tuple[str, bool, Callable[[], Any]]]:
"""Quantised cache writes: the whole cache buffer must match byte for byte."""
def make(layout: Any, block_size: int, n: int, with_invalid: bool):
def run() -> Any:
torch.manual_seed(0)
num_blocks = (n + block_size - 1) // block_size + 2
kv = torch.randn((n, layout.num_elements), device="cuda", dtype=torch.bfloat16)
slots = torch.arange(n, dtype=torch.int32, device="cuda") + block_size
if with_invalid:
slots[::7] = -1
cache = torch.zeros((num_blocks, block_size, 1, layout.bytes_per_token), dtype=torch.uint8, device="cuda")
store_kv_cache(kv, cache, slots, layout=layout)
return cache.clone()
return run
return [
("main_fp8_b32", True, make(V41_FORMAT, 32, 100, False)),
("main_fp8_b64", True, make(V41_FORMAT, 64, 233, True)),
("main_fp8_b134", True, make(V41_FORMAT, 134, 500, False)),
("index_fp32sf_b32", True, make(FP8_FLOAT32_SF_INDEX_KV_FORMAT, 32, 64, False)),
("index_fp32sf_b64", True, make(FP8_FLOAT32_SF_INDEX_KV_FORMAT, 64, 200, True)),
]
def _cases_load_paged_kv_cache() -> list[tuple[str, bool, Callable[[], Any]]]:
"""Paged reads, including holes (src_block_id == -1) and partial segments.
The cache is filled by `store_kv_cache` rather than with random bytes: random bytes produce
fp8 NaN patterns and NaN scales, and those compare unequal to themselves, which would report a
mismatch that says nothing about the load kernel.
"""
def make(layout: Any, block_size: int, num_blocks: int, num_segments: int, with_hole: bool):
torch.manual_seed(0)
total = num_blocks * block_size
kv = torch.randn((total, layout.num_elements), device="cuda", dtype=torch.bfloat16)
cache = torch.zeros((num_blocks, block_size, 1, layout.bytes_per_token), dtype=torch.uint8, device="cuda")
store_kv_cache(kv, cache, torch.arange(total, dtype=torch.int32, device="cuda"), layout=layout)
segments = []
dst = 0
for i in range(num_segments):
begin = i % block_size
end = min(block_size, begin + 1 + (i * 7) % block_size)
src = -1 if (with_hole and i == 1) else i % num_blocks
segments.append([src, begin, end, dst])
dst += end - begin
seg = torch.tensor(segments, dtype=torch.int32, device="cuda")
def run() -> Any:
result = load_paged_kv_cache(cache, seg, dst, layout=layout)
if result.bf16_data is not None:
return result.bf16_data
# Byte views for the index layout: both payload and scales are passthrough copies, so
# exact bytes are the right comparison and NaN bit patterns are harmless.
return result.quantized_data.view(torch.uint8), result.sf_data.view(torch.uint8)
return run
return [
("main_fp8_b32", True, make(V41_FORMAT, 32, 8, 6, False)),
("main_fp8_b64_holes", True, make(V41_FORMAT, 64, 8, 6, True)),
("index_fp32sf_b32", True, make(FP8_FLOAT32_SF_INDEX_KV_FORMAT, 32, 8, 6, False)),
("index_fp32sf_b64_holes", True, make(FP8_FLOAT32_SF_INDEX_KV_FORMAT, 64, 8, 6, True)),
]
def _cases_fused_transform() -> list[tuple[str, bool, Callable[[], Any]]]:
"""The norm -> RoPE -> fp8 pipeline, in every form the engine can ask for."""
def make(
n: int,
num_heads: int,
head_dim: int,
rotary: int,
*,
norm: bool,
rope: bool,
cast: bool,
cast_back: bool,
is_neox: bool = False,
conjugate: bool = False,
npc: int = 32,
sf_format: str = "ue8m0",
):
def run() -> Any:
torch.manual_seed(0)
x = torch.randn((n, num_heads, head_dim), device="cuda", dtype=torch.bfloat16)
norm_args = NormArgs(weight=torch.randn((head_dim,), device="cuda", dtype=torch.bfloat16), eps=1e-20) if norm else None
rope_args = None
if rope:
rotary_dim = rotary
half = rotary_dim // 2
angles = torch.randn((4096, half), device="cuda", dtype=torch.float32).cumsum(-1) * 0.01
cache = torch.cat([angles.cos(), angles.sin()], dim=-1)
positions = torch.randint(0, 4096, (n,), device="cuda", dtype=torch.int32)
rope_args = RopeArgs(
positions=positions,
cos_sin_cache=cache,
rotary_dim=rotary_dim,
is_neox=is_neox,
offset=head_dim - rotary_dim,
conjugate=conjugate,
)
cast_args = CastArgs("e4m3", npc, round_sf=True, sf_format=sf_format, cast_back=cast_back) if cast else None
return fused_norm_rope_hadamard_cast_store_kvcache(x, norm_args, rope_args, None, cast_args, None)
return run
def make_index_cache(n: int, num_heads: int, head_dim: int, rotary: int, num_blocks: int):
"""Index-K path: fp32 scales, per-128 groups, into the index KV cache."""
def run() -> Any:
torch.manual_seed(0)
layout = FP8_FLOAT32_SF_INDEX_KV_FORMAT
block_size = 64
x = torch.randn((n, num_heads, head_dim), device="cuda", dtype=torch.bfloat16)
half = rotary // 2
angles = torch.randn((4096, half), device="cuda", dtype=torch.float32).cumsum(-1) * 0.01
cos_sin = torch.cat([angles.cos(), angles.sin()], dim=-1)
positions = torch.randint(0, 4096, (n,), device="cuda", dtype=torch.int32)
kv_cache = torch.zeros((num_blocks, block_size, 1, layout.bytes_per_token), dtype=torch.uint8, device="cuda")
# Distinct slots: two tokens landing on the same slot would race, and the winner
# would depend on CTA scheduling rather than on the kernel under test.
slots = torch.randperm(num_blocks * block_size, device="cuda")[:n].to(torch.int32)
fused_norm_rope_hadamard_cast_store_kvcache(
x,
None,
RopeArgs(positions, cos_sin, rotary, False, offset=head_dim - rotary),
None,
CastArgs.from_cache_format(layout),
KVCacheOutArgs(kv_cache, slots, layout=layout),
)
return kv_cache
return run
def make_cache(n: int, block_size: int, num_blocks: int, with_bf16: bool, with_rope: bool = True):
"""The main-KV path: quantise and write straight into the paged cache."""
def run() -> Any:
torch.manual_seed(0)
head_dim, rotary = 512, 64
layout = V41_FORMAT
x = torch.randn((n, 1, head_dim), device="cuda", dtype=torch.bfloat16)
weight = torch.randn((head_dim,), device="cuda", dtype=torch.bfloat16)
half = rotary // 2
angles = torch.randn((4096, half), device="cuda", dtype=torch.float32).cumsum(-1) * 0.01
cache_cs = torch.cat([angles.cos(), angles.sin()], dim=-1)
positions = torch.randint(0, 4096, (n,), device="cuda", dtype=torch.int32)
rope_args = RopeArgs(positions, cache_cs, rotary, False, offset=head_dim - rotary) if with_rope else None
kv_cache = torch.zeros((num_blocks, block_size, 1, layout.bytes_per_token), dtype=torch.uint8, device="cuda")
# Distinct slots: two tokens landing on the same slot would race, and the winner
# would depend on CTA scheduling rather than on the kernel under test.
slots = torch.randperm(num_blocks * block_size, device="cuda")[:n].to(torch.int32)
out_bf16 = torch.empty((n, 1, head_dim), device="cuda", dtype=torch.bfloat16) if with_bf16 else None
fused_norm_rope_hadamard_cast_store_kvcache(
x,
NormArgs(weight, 1e-20),
rope_args,
None,
CastArgs.from_cache_format(layout),
KVCacheOutArgs(kv_cache, slots, out=out_bf16, out_stage="norm", layout=layout),
)
return (kv_cache, out_bf16) if with_bf16 else kv_cache
return run
return [
# The configuration the engine builds, from DEEPINFER_TRACE_KERNEL_ARGS on a full run.
("engine_cfg_n1_h512", False, make(16, 1, 512, 64, norm=True, rope=True, cast=True, cast_back=True)),
# The engine's other call site: norm + rope + quantise into the paged KV cache, with the
# post-norm row also returned for the indexer.
("engine_cache", False, make_cache(16, 134, 4, True)),
("engine_cache_no_bf16", False, make_cache(16, 134, 4, False)),
("engine_cache_b32", False, make_cache(9, 32, 8, True)),
("engine_cache_no_rope", False, make_cache(8, 134, 2, True, with_rope=False)),
("engine_cfg_n128", False, make(128, 1, 512, 64, norm=True, rope=True, cast=True, cast_back=True)),
("engine_cfg_n1_token", False, make(1, 1, 512, 64, norm=True, rope=True, cast=True, cast_back=True)),
("neox", False, make(16, 1, 512, 64, norm=True, rope=True, cast=True, cast_back=True, is_neox=True)),
("conjugate", False, make(16, 1, 512, 64, norm=True, rope=True, cast=True, cast_back=True, conjugate=True)),
("no_rope", False, make(64, 1, 512, 64, norm=True, rope=False, cast=True, cast_back=True)),
("no_norm", False, make(64, 1, 512, 64, norm=False, rope=True, cast=True, cast_back=True)),
("bf16_only", False, make(64, 1, 512, 64, norm=True, rope=True, cast=False, cast_back=False)),
("return_quant", False, make(64, 1, 512, 64, norm=True, rope=True, cast=True, cast_back=False)),
("multi_head_h128", False, make(32, 4, 128, 128, norm=True, rope=True, cast=True, cast_back=True)),
("h128_rope64", False, make(32, 2, 128, 64, norm=True, rope=True, cast=True, cast_back=True)),
# The indexer's Q quantisation: no norm, per-128 groups, fp32 scales, quantised output.
("index_q_fp32sf", False, make(64, 4, 128, 64, norm=False, rope=True, cast=True, cast_back=False, npc=128, sf_format="fp32")),
(
"index_q_fp32sf_many",
False,
make(256, 32, 128, 64, norm=False, rope=True, cast=True, cast_back=False, npc=128, sf_format="fp32"),
),
# Same, but written straight into the index KV cache.
# The index KV cache holds a single KV head (the indexer is MQA), which the baseline
# asserts too.
("index_k_cache", False, make_index_cache(48, 1, 128, 64, 16)),
]
def _cases_prepare_v4_decode_metadata() -> list[tuple[str, bool, Callable[[], Any]]]:
"""Every output of the decode-metadata kernel, including the ratio-bucketed ones."""
def make(
n_seq: int,
max_seqlen_q: int,
max_num_blocks: int,
window_size: int,
swa_block_size: int,
block_size: int,
ca_ratios: tuple[int, ...],
dense_ratios: tuple[int, ...],
sparse_ratios: tuple[int, ...],
placeholders: bool,
):
def run() -> Any:
torch.manual_seed(0)
seqlens_q = torch.randint(1, max_seqlen_q + 1, (n_seq,), device="cuda", dtype=torch.int32)
seqlens_k = seqlens_q + torch.randint(0, 40, (n_seq,), device="cuda", dtype=torch.int32)
cu = torch.cumsum(seqlens_q, 0, dtype=torch.int32)
total_q = int(cu[-1].item())
per_seq_blocks = torch.randint(1, max_num_blocks + 1, (n_seq,), device="cuda", dtype=torch.int32)
offsets = torch.zeros((n_seq + 1,), device="cuda", dtype=torch.int32)
offsets[1:] = torch.cumsum(per_seq_blocks, 0, dtype=torch.int32)
total_blocks = int(offsets[-1].item())
block_tables_in = torch.randint(0, 40, (total_blocks,), device="cuda", dtype=torch.int32)
state_block_idx = torch.randint(0, 8, (n_seq,), device="cuda", dtype=torch.int32)
is_placeholder = torch.zeros((n_seq,), device="cuda", dtype=torch.bool)
if placeholders:
is_placeholder[::2] = True
n_ca = max(len(ca_ratios), 1)
n_dense = max(len(dense_ratios), 1)
n_sparse = max(len(sparse_ratios), 1)
i32 = {"device": "cuda", "dtype": torch.int32}
outs = [
torch.full((total_q,), -7, **i32), # positions
torch.zeros((total_q,), device="cuda", dtype=torch.bool), # valid_token_mask
torch.zeros((total_q,), device="cuda", dtype=torch.bool), # gate_force_random
torch.ones((total_q,), device="cuda", dtype=torch.bool), # fix_routing_mask
torch.full((total_q,), -7, **i32), # context_lens
torch.full((n_sparse, total_q), -7, **i32), # klen
torch.full((n_dense, total_q, 2), -7, **i32), # kv_ca_index_offset_len
torch.full((total_q, 2), -7, **i32), # kv_swa_index_offset_len
torch.full((total_q,), -7, **i32), # slot_mapping_store_swa
torch.full((total_q,), -7, **i32), # query_indices
torch.full((total_q,), -7, **i32), # state_block_idx_out
torch.full((total_q, max_num_blocks), -7, **i32), # block_tables_out
torch.full((n_ca, total_q), -7, **i32), # positions_c
torch.full((n_ca, total_q), -7, **i32), # slot_mapping_store_ca
]
prepare_v4_decode_metadata(
seqlens_q,
seqlens_k,
cu,
block_tables_in,
offsets,
state_block_idx,
is_placeholder,
*outs,
max_seqlen_q=max_seqlen_q,
max_num_blocks=max_num_blocks,
window_size=window_size,
swa_block_size=swa_block_size,
block_size=block_size,
ca_ratios=ca_ratios,
dense_ratios=dense_ratios,
sparse_ratios=sparse_ratios,
force_load_balance=False,
)
# Nothing should be left at the sentinel: a partially written output is a bug even when
# both backends agree on it.
for name, tensor in (
("positions", outs[0]),
("context_lens", outs[4]),
("kv_swa_index_offset_len", outs[7]),
("slot_mapping_store_swa", outs[8]),
("query_indices", outs[9]),
("positions_c", outs[12]),
("slot_mapping_store_ca", outs[13]),
):
assert int((tensor == -7).sum()) == 0, f"{name} left unwritten rows"
return tuple(outs)
return run
return [
# The engine's own configuration: window 128, SWA block 134, ratios {1, 2} all compressed.
("engine_cfg", False, make(8, 6, 64, 128, 134, 128, (1, 2), (), (1, 2), False)),
("engine_cfg_placeholders", False, make(8, 6, 64, 128, 134, 128, (1, 2), (), (1, 2), True)),
("with_dense", False, make(6, 6, 32, 64, 128, 128, (1, 2, 4), (1, 2), (4,), False)),
("single_seq", False, make(1, 1, 8, 32, 32, 32, (1,), (), (1,), False)),
("prefill_len8", False, make(5, 8, 16, 512, 512, 64, (1, 2, 4), (), (1, 2, 4), False)),
]
def _cases_compress_prefill() -> list[tuple[str, bool, Callable[[], Any]]]:
"""Block compression plus the ring-cache tail write, including a window that spans chunks."""
def make(dim: int, ratio: int, n_seq: int, cache_size: int, slots: int, spans_chunks: bool):
def run() -> Any:
torch.manual_seed(0)
qlens = torch.randint(3, 7, (n_seq,), device="cuda", dtype=torch.int32)
# qstart > 0 means the first compression window reaches into the previous chunk, which
# is the only way the cache-read branch is exercised.
qstart = (
torch.randint(1, 4, (n_seq,), device="cuda", dtype=torch.int32)
if spans_chunks
else torch.zeros((n_seq,), device="cuda", dtype=torch.int32)
)
klen = qstart + qlens
# All four prefix sums are exclusive with a leading zero (`n_seq + 1` entries); the
# kernel's ABI rejects the inclusive form.
zero = torch.zeros((1,), device="cuda", dtype=torch.int32)
cu_q = torch.cat([zero, torch.cumsum(qlens, 0, dtype=torch.int32)])
cu_k = torch.cat([zero, torch.cumsum(klen, 0, dtype=torch.int32)])
total_q = int(cu_q[-1].item())
new_blocks = (klen // ratio - qstart // ratio).tolist()
reserved = [max(1, b) for b in new_blocks]
mapping = torch.cat([torch.full((r,), i, device="cuda", dtype=torch.int32) for i, r in enumerate(reserved)])
# Exclusive prefix sum with a leading zero, i.e. `n_seq + 1` entries: the kernel binds
# its `num_seqs` symbol from this tensor, so a shorter one fails the ABI check.
cu_c = torch.zeros((n_seq + 1,), device="cuda", dtype=torch.int32)
acc = 0
for i, r in enumerate(reserved):
cu_c[i] = acc
acc += r
cu_c[n_seq] = acc
# Distinct slots per sequence, as the engine assigns them. Sharing a slot would let one
# sequence's tail copy overwrite the other's cache, which makes the result depend on
# kernel interleaving rather than on the code under test.
assert n_seq <= slots
state_block_idx = torch.randperm(slots, device="cuda")[:n_seq].to(torch.int32)
score = torch.randn((total_q, dim), device="cuda", dtype=torch.float32)
latent = torch.randn((total_q, dim), device="cuda", dtype=torch.float32)
state_cache = torch.randn((slots, cache_size, 2, dim), device="cuda", dtype=torch.float32)
kv = compress_and_update_state_prefill(
score,
latent,
state_cache,
state_block_idx,
mapping,
cu_q,
cu_k,
cu_c,
ratio,
None,
)
return kv, state_cache
return run
return [
("engine_r2", False, make(512, 2, 3, 16, 4, True)),
("engine_r2_no_prev", False, make(512, 2, 3, 16, 4, False)),
("r1", False, make(512, 1, 3, 16, 4, True)),
("single_seq", False, make(512, 2, 1, 8, 1, True)),
# Tightest legal ring: at least one chunk's worth of tokens, so the tail copy cannot wrap
# onto a slot the compression step is reading. The interface asserts this relation too.
("tight_cache", False, make(512, 2, 2, 6, 2, True)),
]
def _cases_compress_decode() -> list[tuple[str, bool, Callable[[], Any]]]:
"""Per-token cache update and the block-closing compression."""
def make(dim: int, ratio: int, total_q: int, max_seqlen_q: int, slots: int, cache_size: int):
def run() -> Any:
torch.manual_seed(0)
score = torch.randn((total_q, dim), device="cuda", dtype=torch.float32)
latent = torch.randn((total_q, dim), device="cuda", dtype=torch.float32)
state_cache = torch.randn((slots, cache_size, 2, dim), device="cuda", dtype=torch.float32)
# One distinct state slot per token: the engine gives every sequence its own slot, and
# sharing one would make the cache writes overlap and the result order-dependent.
assert total_q <= slots
state_block_idx = torch.randperm(slots, device="cuda")[:total_q].to(torch.int32)
context_lens = torch.randint(1, 40, (total_q,), device="cuda", dtype=torch.int32)
kv = compress_and_update_state_decode(
score,
latent,
state_cache,
state_block_idx,
context_lens,
max_seqlen_q,
ratio,
)
return kv, state_cache
return run
return [
("engine_r2", False, make(512, 2, 8, 6, 8, 8)),
("r1", False, make(512, 1, 8, 6, 8, 8)),
("single_token", False, make(512, 2, 1, 1, 1, 4)),
("many_slots", False, make(512, 2, 16, 6, 16, 16)),
]
def _cases_fused_sample() -> list[tuple[str, bool, Callable[[], Any]]]:
"""Sampler output: log-probabilities and the drawn token.
The comparison is not `exact`: a different reduction tree shifts the row sum by an ulp, which
propagates into the fixed-point cumulative mass, where a column sitting exactly on a draw
boundary can flip. So log-probabilities are compared with a tolerance and the token count is
checked as an agreement rate -- the honest standard for a kernel whose answer is a random draw.
"""
def make(vocab: int, rows: int, top_p: float, top_k: int):
def run() -> Any:
torch.manual_seed(0)
logits = torch.randn((rows, vocab), device="cuda", dtype=torch.float32) * 2.0
temperatures = torch.ones(rows, device="cuda", dtype=torch.float32)
top_ps = torch.full((rows,), top_p, device="cuda", dtype=torch.float32)
top_ks = torch.full((rows,), top_k, device="cuda", dtype=torch.int32)
positions = torch.arange(1000, 1000 + rows, device="cuda", dtype=torch.int32)
# torch has no uint64 arange, so build in int64 and reinterpret: the seeds are a bit
# pattern, and they exceed 2^63 as uint64.
seeds = (torch.arange(rows, device="cuda", dtype=torch.int64) * 7919 + 12345).view(torch.uint64)
logprobs, token, *_ = fused_sample(
logits,
SamplingKernelInputs(
logits=LogitsTransformInputs(temperatures, top_ps, top_ks),
seq_seeds=seeds,
sample_positions=positions,
),
)
return logprobs, token
return run
# The last element is the token agreement floor, and it is a measured number, not a guess.
#
# The log-probabilities are compared with a tolerance and the kept set has to match exactly; the
# token cannot, because the draw is `d % total` and the baseline's rejection limit steps at
# `total == 2^31`, which `total` sits next to. A few units of rounding error in `total` therefore
# move to a different draw. The engine's own configuration is unaffected (its totals stay on one
# side), so it is held at 0.9 and passes with every row matching; the narrow cuts sit either
# side of that step on 10-13% of rows, so their floor is 0.85.
return [
# The engine's own configuration: 4096 columns after the force_topk cut, top_p=0.99, no top_k.
("engine_cfg", False, make(4096, 8, 0.99, -1), 0.9),
("engine_cfg_batch32", False, make(4096, 32, 0.99, -1), 0.9),
# top_k only, and both cuts at once.
# A narrow cut concentrates the mass in few columns, so the fixed-point cumulative values are
# coarse relative to the spacing between them and a boundary lands on a different side more
# often. 128 rows rather than 8, because with 8 a single flip is 12.5% and the rate says
# nothing.
("top_k_128", False, make(4096, 128, 1.0, 128), 0.85),
("top_k_1", False, make(4096, 32, 1.0, 1), 0.85),
("top_p_and_k", False, make(4096, 128, 0.9, 256), 0.85),
("no_cut", False, make(4096, 128, 1.0, -1), 0.85),
# Sharp logits make the distribution peaky, so a small numeric difference cannot move the
# draw; flat ones put many columns on similar mass and are the harder case.
("top_p_0.5", False, make(4096, 128, 0.5, -1), 0.85),
]
CASES: dict[str, Callable[[], list[tuple[str, bool, Callable[[], Any]]]]] = {
"store_kv_cache_tl": _cases_store_kv_cache,
"load_paged_kv_cache_tl": _cases_load_paged_kv_cache,
"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,
"fused_norm_rope_hadamard_cast_store_kvcache_tl": _cases_fused_transform,
"prepare_v4_decode_metadata_tl": _cases_prepare_v4_decode_metadata,
"compress_and_update_state_prefill_tl": _cases_compress_prefill,
"compress_and_update_state_decode_tl": _cases_compress_decode,
"fused_sample_tl": _cases_fused_sample,
"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,
}