mirror of
https://github.com/deepseek-ai/DeepInfer.git
synced 2026-10-31 08:46:21 +08:00
The references were importable and numerically validated, but nothing pointed the model at them: the forward pass still called the compiled kernels, which raise ModuleNotFoundError on a host without TileLang. This adds the routing layer and swaps the call sites. `deepinfer/tk_compat/` exposes the *compiled* signatures -- out-buffers, in-place conventions, argument order -- because that is what the 25 call sites are written against, and adapts to the reference's shape internally. Selection is `DEEPINFER_KERNELS=auto|cuda|ref`; `auto` follows whether TileLang imports. The adapters are not pass-throughs. Each difference was found by running both backends: * `per_token_cast` takes `num_per_channels=n`, the reference takes `block_size=(1, n)`. * `apply_rotary` takes a concatenated cos|sin cache and rotates in place returning None; the reference takes cos and sin separately and returns a new tensor. The adapter splits, calls, and copies back, because the call site relies on the in-place contract. * `engram_gate` takes the pre-multiplied weight and `(eps, clamp_value)`; the reference needs the two weights separately and `(clamp_value, eps)`. Since a product cannot be un-multiplied, `fused_weight` returns the pair on the reference path and the gate unpacks it. * `mhc_pre_big_fuse_fwd` has no single reference: its five steps are composed from the reference's own components. * `mhc_reduce_partials_and_rmsnorm_fwd` likewise; rebuilt from the reference's formula. `moe_topk_gate(force_random=...)` raises rather than silently ignoring the flag. The reference has no equivalent, and dropping it would route tokens differently while looking healthy. Verification, all on the real call signatures: 18 cases compared between `cuda` and `ref` modes, `ALL_MATCH`, 12 bitwise identical including every quantised byte and every routing index; plus a smoke run that executes all 23 APIs with TileLang blocked, `ALL_CALLS_OK`. End to end still has not completed, and the reason is not this change: six GPUs on the host are held by other containers (82 GiB of 139 each), so the 8-way EP run OOMs in allocate_userbuffer and the surviving ranks wait at a barrier. Recorded in the doc along with the command to rerun it.
373 lines
13 KiB
Python
Executable File
373 lines
13 KiB
Python
Executable File
"""Compare the compat layer's two backends on identical inputs, one per process.
|
|
|
|
The per-kernel A/B scripts compare `tile_kernels` against `tile_kernels.torch` directly. This
|
|
compares `deepinfer.tk_compat` against itself in both modes, which is a different question: it
|
|
exercises the adapters -- the `num_per_channels` -> `block_size` translation, the out-buffer
|
|
copies, the in-place rotary, the reordered engram-gate arguments, the composed big_fuse -- on the
|
|
signatures deepinfer actually calls.
|
|
|
|
Two processes because the backend is chosen once, at import.
|
|
|
|
DEEPINFER_KERNELS=cuda python compat_ab.py --dump /tmp/cuda.pt
|
|
DEEPINFER_KERNELS=ref python compat_ab.py --compare /tmp/cuda.pt
|
|
|
|
Every case seeds from scratch, so both runs build byte-identical inputs.
|
|
"""
|
|
|
|
import argparse
|
|
import sys
|
|
import traceback
|
|
from collections.abc import Callable
|
|
from typing import TypeVar
|
|
|
|
import torch
|
|
|
|
from deepinfer.tk_compat import config, engram, mhc, moe, quant, transform
|
|
|
|
DEV = "cuda"
|
|
F = TypeVar("F", bound=Callable[[], dict])
|
|
CASES: dict[str, Callable[[], dict]] = {}
|
|
|
|
|
|
def case(name: str) -> Callable[[F], F]:
|
|
"""Register a function returning a dict of tensors for one operator."""
|
|
|
|
def wrap(fn: F) -> F:
|
|
CASES[name] = fn
|
|
return fn
|
|
|
|
return wrap
|
|
|
|
|
|
def _inp(*shape: int, dtype: torch.dtype = torch.float32, scale: float = 1.0) -> torch.Tensor:
|
|
"""Deterministic input: the seed is set per case, so the order of calls does not matter."""
|
|
return (torch.rand(shape, device=DEV, dtype=torch.float32) - 0.5) * 2 * scale
|
|
|
|
|
|
# ------------------------------------------------------------------------------ quant
|
|
|
|
|
|
@case("per_token_cast")
|
|
def _c_per_token_cast() -> dict:
|
|
torch.manual_seed(0)
|
|
x = _inp(128, 2048, dtype=torch.bfloat16).to(torch.bfloat16)
|
|
q, sf = quant.per_token_cast(x, fmt="e4m3", num_per_channels=128, round_sf=True, use_tma_aligned_col_major_sf=True)
|
|
return {"q": q.view(torch.uint8), "sf": sf}
|
|
|
|
|
|
@case("per_token_cast_out")
|
|
def _c_per_token_cast_out() -> dict:
|
|
torch.manual_seed(1)
|
|
x = _inp(64, 512, dtype=torch.bfloat16).to(torch.bfloat16)
|
|
kwargs = dict(fmt="e4m3", num_per_channels=128, round_sf=True, use_tma_aligned_col_major_sf=False)
|
|
# Take the layout the interface itself allocates: the compiled kernel asserts specific strides
|
|
# on the sf buffer, and `zeros_like` does not reproduce them (it fails with "strides[1]
|
|
# expected 64, got 1"). Cloning the allocating result's storage gives a valid out buffer.
|
|
ref_q, ref_sf = quant.per_token_cast(x, **kwargs)
|
|
out_q = ref_q.clone()
|
|
out_sf = ref_sf.clone()
|
|
out_q.zero_()
|
|
out_sf.zero_()
|
|
quant.per_token_cast(x, out=(out_q, out_sf), **kwargs)
|
|
return {"q": out_q.view(torch.uint8), "sf": out_sf}
|
|
|
|
|
|
@case("cast_back")
|
|
def _c_cast_back() -> dict:
|
|
torch.manual_seed(2)
|
|
x = _inp(64, 512, dtype=torch.bfloat16).to(torch.bfloat16)
|
|
q = quant.per_token_cast(x, fmt="e4m3", num_per_channels=128, round_sf=True, use_tma_aligned_col_major_sf=True)
|
|
return {"deq": quant.cast_back(tuple(q), "bf16", (1, 128))}
|
|
|
|
|
|
@case("swiglu_cast")
|
|
def _c_swiglu() -> dict:
|
|
torch.manual_seed(3)
|
|
x = _inp(64, 1024, dtype=torch.bfloat16).to(torch.bfloat16)
|
|
q, sf = quant.swiglu_forward_and_per_token_cast(x, fmt="e4m3", num_per_channels=128, round_sf=True, use_tma_aligned_col_major_sf=True)
|
|
return {"q": q.view(torch.uint8), "sf": sf}
|
|
|
|
|
|
# -------------------------------------------------------------------------------- moe
|
|
|
|
|
|
@case("moe_gate")
|
|
def _c_moe_gate() -> dict:
|
|
torch.manual_seed(4)
|
|
logits = _inp(128, 384, scale=2.0)
|
|
bias = _inp(384, scale=0.1)
|
|
idx, w = moe.moe_topk_gate(
|
|
logits,
|
|
num_topk=6,
|
|
use_shared_as_routed=False,
|
|
num_shared_experts=0,
|
|
routed_scaling_factor=1.5,
|
|
ep_rank=0,
|
|
scoring_func="sqrtsoftplus",
|
|
bias=bias,
|
|
)
|
|
return {"idx": idx, "w": w}
|
|
|
|
|
|
@case("moe_gate_mask")
|
|
def _c_moe_gate_mask() -> dict:
|
|
torch.manual_seed(5)
|
|
logits = _inp(64, 384, scale=2.0)
|
|
bias = _inp(384, scale=0.1)
|
|
mask = torch.ones((64,), device=DEV, dtype=torch.bool)
|
|
mask[::5] = False
|
|
idx, w = moe.moe_topk_gate(
|
|
logits,
|
|
num_topk=6,
|
|
use_shared_as_routed=False,
|
|
num_shared_experts=0,
|
|
routed_scaling_factor=1.5,
|
|
ep_rank=0,
|
|
mask=mask,
|
|
bias=bias,
|
|
)
|
|
return {"idx": idx, "w": w}
|
|
|
|
|
|
# --------------------------------------------------------------------------- transform
|
|
|
|
|
|
@case("apply_rotary")
|
|
def _c_rotary() -> dict:
|
|
torch.manual_seed(6)
|
|
tokens, heads, rot_dim = 32, 8, 128
|
|
q = _inp(tokens, heads, rot_dim, dtype=torch.bfloat16).to(torch.bfloat16)
|
|
k = _inp(tokens, heads, rot_dim, dtype=torch.bfloat16).to(torch.bfloat16)
|
|
positions = torch.arange(tokens, device=DEV, dtype=torch.int64)
|
|
angles = _inp(tokens, rot_dim // 2) * 3.0
|
|
cache = torch.cat([angles.cos(), angles.sin()], dim=-1).contiguous()
|
|
transform.apply_rotary(q, cache, k, positions, interleaved=False)
|
|
return {"q": q, "k": k}
|
|
|
|
|
|
# ----------------------------------------------------------------------------- engram
|
|
|
|
|
|
@case("engram_hash")
|
|
def _c_engram_hash() -> dict:
|
|
torch.manual_seed(7)
|
|
layers, ngram, tables, vocab = 2, 4, 3, 1000
|
|
ids = torch.randint(0, vocab, (128, ngram), device=DEV, dtype=torch.int32)
|
|
mult = torch.randint(1, 2**31, (layers, ngram), device=DEV, dtype=torch.int64)
|
|
vocab_sizes = torch.full((layers, ngram - 1, tables), vocab, device=DEV, dtype=torch.int32)
|
|
offsets = torch.zeros((layers, (ngram - 1) * tables), device=DEV, dtype=torch.int32)
|
|
return {"hash": engram.engram_hash(ids, mult, vocab_sizes, offsets, None)}
|
|
|
|
|
|
@case("engram_gate")
|
|
def _c_engram_gate() -> dict:
|
|
torch.manual_seed(8)
|
|
tokens, hc_mult, hidden = 64, 4, 512
|
|
hs = _inp(tokens, hc_mult, hidden, dtype=torch.bfloat16, scale=0.5).to(torch.bfloat16)
|
|
kv = _inp(tokens, hc_mult + 1, hidden, dtype=torch.bfloat16, scale=0.5).to(torch.bfloat16)
|
|
wh = (_inp(hc_mult, hidden) * 0.5 + 0.5).to(torch.bfloat16)
|
|
we = (_inp(hc_mult, hidden) * 0.5 + 0.5).to(torch.bfloat16)
|
|
fused = engram.fused_weight(wh, we)
|
|
out, *_ = engram.engram_gate_fwd(hs, kv, fused, 1e-6, 10.0, save_for_backward=False, image_token_mask=None, inplace=False)
|
|
return {"out": out}
|
|
|
|
|
|
# -------------------------------------------------------------------------------- mhc
|
|
|
|
|
|
@case("mhc_expand")
|
|
def _c_expand() -> dict:
|
|
torch.manual_seed(9)
|
|
x = _inp(64, 128, dtype=torch.bfloat16).to(torch.bfloat16)
|
|
o = torch.empty((64, 4, 128), device=DEV, dtype=torch.bfloat16)
|
|
mhc.expand_to_mhc_fwd(x, o)
|
|
return {"o": o}
|
|
|
|
|
|
@case("mhc_normw_merge")
|
|
def _c_normw() -> dict:
|
|
torch.manual_seed(10)
|
|
fn = _inp(12, 256)
|
|
normw = _inp(256) * 0.5 + 0.5
|
|
out = torch.empty_like(fn)
|
|
mhc.mhc_fn_normw_merge_fwd(fn, normw, out)
|
|
return {"out": out}
|
|
|
|
|
|
@case("mhc_head_mix")
|
|
def _c_head_mix() -> dict:
|
|
torch.manual_seed(11)
|
|
mix = _inp(64, 12)
|
|
scale = _inp(1) * 2
|
|
base = _inp(12)
|
|
out = torch.empty_like(mix)
|
|
mhc.mhc_head_compute_mix_fwd(mix, scale, base, out, 1e-6)
|
|
return {"out": out}
|
|
|
|
|
|
@case("mhc_post")
|
|
def _c_post() -> dict:
|
|
torch.manual_seed(12)
|
|
seqs, tokens, mult, hidden = 2, 16, 4, 128
|
|
x = _inp(seqs, tokens, hidden, dtype=torch.bfloat16).to(torch.bfloat16)
|
|
residual = _inp(seqs, tokens, mult, hidden, dtype=torch.bfloat16).to(torch.bfloat16)
|
|
post_mix = _inp(seqs, tokens, mult, 1) * 0.5 + 0.5
|
|
comb = _inp(seqs, tokens, mult, mult) * 0.5 + 0.5
|
|
return {"out": mhc.mhc_post_fwd(x, residual, post_mix, comb)}
|
|
|
|
|
|
@case("mhc_apply_mix")
|
|
def _c_apply_mix() -> dict:
|
|
torch.manual_seed(13)
|
|
x = _inp(64, 4, 128, dtype=torch.bfloat16).to(torch.bfloat16)
|
|
mix = _inp(64, 4) * 0.5 + 0.5
|
|
o = torch.empty((64, 128), device=DEV, dtype=torch.bfloat16)
|
|
mhc.mhc_pre_apply_mix_fwd(x, mix, o)
|
|
return {"o": o}
|
|
|
|
|
|
@case("mhc_split_mixes")
|
|
def _c_split_mixes() -> dict:
|
|
torch.manual_seed(14)
|
|
tokens, mult = 64, 4
|
|
mult3 = mult * 2 + mult * mult
|
|
mixes = _inp(tokens, mult3)
|
|
scale = _inp(3) * 2
|
|
base = _inp(mult3)
|
|
pre = torch.empty((tokens, mult), device=DEV, dtype=torch.float32)
|
|
post = torch.empty((tokens, mult), device=DEV, dtype=torch.float32)
|
|
comb = torch.empty((tokens, mult * mult), device=DEV, dtype=torch.float32)
|
|
mhc.mhc_pre_split_mixes_fwd(mixes, scale, base, pre, post, comb, 0.5, 1e-6)
|
|
return {"pre": pre, "post": post, "comb": comb}
|
|
|
|
|
|
@case("mhc_reduce_rmsnorm")
|
|
def _c_reduce() -> dict:
|
|
torch.manual_seed(15)
|
|
tokens, mult3, groups, n_splits = 32, 12, 512, 16
|
|
mul = _inp(n_splits, tokens, 1, mult3)
|
|
sqr = _inp(n_splits, tokens, 1) * 0.5 + 1.0
|
|
o_mul = torch.empty_like(mul[0])
|
|
o_sqr = torch.empty_like(sqr[0])
|
|
out = torch.empty((tokens, mult3), device=DEV, dtype=torch.float32)
|
|
mhc.mhc_reduce_partials_and_rmsnorm_fwd(mul, sqr, o_mul, o_sqr, out, rms_group_size=groups, rms_eps=1e-6, n_splits=n_splits)
|
|
return {"o_mul": o_mul, "o_sqr": o_sqr, "out": out}
|
|
|
|
|
|
@case("mhc_sinkhorn")
|
|
def _c_sinkhorn() -> dict:
|
|
torch.manual_seed(16)
|
|
x = _inp(64, 4, 4)
|
|
out = torch.empty_like(x)
|
|
mhc.mhc_sinkhorn_fwd(x, out, 10, 1e-6)
|
|
return {"out": out}
|
|
|
|
|
|
@case("mhc_big_fuse")
|
|
def _c_big_fuse() -> dict:
|
|
torch.manual_seed(17)
|
|
tokens, mult, hidden, n_splits = 32, 4, 128, 16
|
|
mult3 = mult * 2 + mult * mult
|
|
residual = _inp(tokens, mult, hidden, dtype=torch.bfloat16).to(torch.bfloat16)
|
|
mul = _inp(n_splits, tokens, mult3)
|
|
sqr = _inp(n_splits, tokens) * 0.5 + float(hidden)
|
|
scale = _inp(3) * 2
|
|
base = _inp(mult3)
|
|
post_mix = torch.empty((tokens, mult), device=DEV, dtype=torch.float32)
|
|
comb = torch.empty((tokens, mult * mult), device=DEV, dtype=torch.float32)
|
|
layer_in = torch.empty((tokens, hidden), device=DEV, dtype=torch.bfloat16)
|
|
mhc.mhc_pre_big_fuse_fwd(
|
|
mul,
|
|
sqr,
|
|
scale,
|
|
base,
|
|
residual,
|
|
post_mix,
|
|
comb,
|
|
layer_in,
|
|
1e-6,
|
|
1e-6,
|
|
1e-6,
|
|
0.5,
|
|
10,
|
|
n_splits=n_splits,
|
|
)
|
|
return {"post": post_mix, "comb": comb, "layer_in": layer_in}
|
|
|
|
|
|
# -------------------------------------------------------------------------------- main
|
|
|
|
|
|
def _tolerance(kernel: str, key: str, ref: torch.Tensor) -> tuple[float, str]:
|
|
"""How close the two backends must be, and why.
|
|
|
|
Integer outputs and the quantised bytes must match exactly: they are indices and encoded
|
|
values, so "close" would be a different token. Floats get a tolerance scaled by the tensor's
|
|
own magnitude, because several of these tensors sit near zero by construction.
|
|
"""
|
|
if ref.dtype in (torch.int32, torch.int64, torch.uint8, torch.bool) or ref.dtype.is_complex:
|
|
return 0.0, "exact"
|
|
if ref.dtype == torch.bfloat16:
|
|
return 2.0**-8 * ref.float().abs().max().clamp_min(1e-6).item() * 2, "2 bf16 ULP at magnitude"
|
|
return ref.float().abs().max().clamp_min(1e-6).item() * 1e-5, "1e-5 of magnitude"
|
|
|
|
|
|
def run(compare_path: str | None, dump_path: str | None) -> int:
|
|
print(f"backend = {config.backend()}")
|
|
results = {}
|
|
failed = []
|
|
for name, fn in CASES.items():
|
|
try:
|
|
results[name] = fn()
|
|
except Exception:
|
|
print(f" ERROR {name}")
|
|
traceback.print_exc()
|
|
failed.append(name)
|
|
|
|
if dump_path:
|
|
torch.save(results, dump_path)
|
|
print(f"dumped {len(results)} case(s) to {dump_path}")
|
|
return 0 if not failed else 1
|
|
|
|
expected = torch.load(compare_path, weights_only=False)
|
|
missing = sorted(set(expected) - set(results))
|
|
if missing:
|
|
print(f"cases present in the dump but not produced here: {missing}")
|
|
for name in CASES:
|
|
if name not in expected or name not in results:
|
|
continue
|
|
exp, got = expected[name], results[name]
|
|
for key in exp:
|
|
a, b = exp[key], got[key]
|
|
if a.shape != b.shape:
|
|
print(f" FAIL {name}.{key}: shape {tuple(a.shape)} vs {tuple(b.shape)}")
|
|
failed.append(f"{name}.{key}")
|
|
continue
|
|
if a.dtype != b.dtype:
|
|
print(f" FAIL {name}.{key}: dtype {a.dtype} vs {b.dtype}")
|
|
failed.append(f"{name}.{key}")
|
|
continue
|
|
tol, why = _tolerance(name, key, a)
|
|
diff = (a.float() - b.float()).abs().max().item()
|
|
ok = diff <= tol
|
|
print(f" {'ok ' if ok else 'FAIL'} {name}.{key}: max diff {diff:.3e} (tol {tol:.3e}, {why})")
|
|
if not ok:
|
|
failed.append(f"{name}.{key}")
|
|
|
|
print()
|
|
if failed:
|
|
print(f"MISMATCH {len(failed)}: {sorted(set(failed))}")
|
|
return 1
|
|
print(f"ALL_MATCH ({len(results)} case(s))")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--dump", metavar="PATH")
|
|
parser.add_argument("--compare", metavar="PATH")
|
|
args = parser.parse_args()
|
|
if bool(args.dump) == bool(args.compare):
|
|
parser.error("pass exactly one of --dump or --compare")
|
|
sys.exit(run(args.compare, args.dump))
|