loopyt 39b086d781 feat(h20): route tile_kernels calls through a compat layer, so the model runs without TileLang
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.
2026-10-22 18:10:14 +08:00

204 lines
6.8 KiB
Python

"""`tile_kernels.quant` -- FP8/FP4 casting, fused SwiGLU, scale-factor layout.
The compiled functions take `num_per_channels` and write into caller-provided `out` buffers; the
references take `block_size` and return fresh tensors. The signatures here are the compiled ones,
because that is what the 4 call sites in the model are written against.
The adapters are thin but not vacuous -- three things genuinely differ:
* `num_per_channels=n` means a per-token block of `(1, n)`, not `(n, n)`.
* `swiglu_forward_and_per_token_cast` fuses activation and cast; the reference has them separate,
so it is activation followed by cast.
* `cast_back`'s `fmt` is the *output* dtype (`'bf16'`/`'fp32'`), which is easy to misread as the
quantised format.
Verified against the compiled kernels: `per_token_cast`, `cast_back` and
`swiglu_forward_and_per_token_cast` are bitwise identical on the SM90 recipe
(`scripts/tile_kernels_patch/compare_with_cuda.py`).
"""
from __future__ import annotations
from collections.abc import Sequence
from typing import Any
import torch
from deepinfer.tk_compat import use_references
if not use_references():
from tile_kernels import quant as _compiled
from tile_kernels import torch as _ref
def per_token_cast(
x: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
fmt: str,
num_per_channels: int,
x_block_size: tuple[int, int] | None = None,
use_tma_aligned_col_major_sf: bool = False,
round_sf: bool = False,
use_packed_ue8m0: bool = False,
use_e4m3_sf: bool = False,
stochastic_cast: bool = False,
out: tuple[torch.Tensor, torch.Tensor] | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Cast to FP8/FP4 with per-token scaling factors."""
if not use_references():
return _compiled.per_token_cast(
x,
fmt,
num_per_channels,
x_block_size,
use_tma_aligned_col_major_sf,
round_sf,
use_packed_ue8m0,
use_e4m3_sf,
stochastic_cast,
out,
)
if stochastic_cast:
raise NotImplementedError(
"stochastic_cast has no torch reference; it changes rounding, so silently using the "
"deterministic path would produce plausible but different numbers"
)
result = _ref.cast(
x,
fmt,
block_size=(1, num_per_channels),
x_block_size=x_block_size,
round_sf=round_sf,
use_tma_aligned_col_major_sf=use_tma_aligned_col_major_sf,
use_packed_ue8m0=use_packed_ue8m0,
use_e4m3_sf=use_e4m3_sf,
)
if out is not None:
# Honour the out-buffer contract: the call sites allocate these and may hold views.
out[0].copy_(result[0])
out[1].copy_(result[1])
return out[0], out[1]
return result
def cast_back(
x: tuple[torch.Tensor, torch.Tensor],
fmt: str,
x_block_size: tuple[int, int],
x_special_fmt: str | None = None,
) -> torch.Tensor:
"""Dequantise an FP8/FP4 tensor. `fmt` is the output dtype, not the quantised format."""
if not use_references():
return _compiled.cast_back(x, fmt, x_block_size, x_special_fmt)
if x_special_fmt is not None:
raise NotImplementedError(f"x_special_fmt={x_special_fmt!r} has no torch reference")
return _ref.cast_back(x, fmt, block_size=x_block_size)
def swiglu_forward(
x: torch.Tensor,
fmt: str,
psum_num_tokens_per_expert: torch.Tensor | None = None,
topk_weights: torch.Tensor | None = None,
routed_scaling_factor: float | None = None,
clamp_value: float | None = None,
clamped_count: torch.Tensor | None = None,
) -> torch.Tensor:
"""SwiGLU activation. `fmt` is unused by the compiled path here; kept for signature parity."""
if not use_references():
return _compiled.swiglu_forward(
x,
fmt,
psum_num_tokens_per_expert,
topk_weights,
routed_scaling_factor,
clamp_value,
clamped_count,
)
return _ref.swiglu_forward(
x,
psum_num_tokens_per_expert,
topk_weights,
routed_scaling_factor,
clamp_value,
clamped_count,
)
def swiglu_forward_and_per_token_cast(
x: torch.Tensor,
fmt: str,
num_per_channels: int,
psum_num_tokens_per_expert: torch.Tensor | None = None,
topk_weights: torch.Tensor | None = None,
routed_scaling_factor: float | None = None,
use_tma_aligned_col_major_sf: bool = False,
round_sf: bool = False,
use_packed_ue8m0: bool = False,
clamp_value: float | None = None,
clamped_count: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Fused SwiGLU + per-token cast."""
if not use_references():
return _compiled.swiglu_forward_and_per_token_cast(
x,
fmt,
num_per_channels,
psum_num_tokens_per_expert,
topk_weights,
routed_scaling_factor,
use_tma_aligned_col_major_sf,
round_sf,
use_packed_ue8m0,
clamp_value,
clamped_count,
)
activated = _ref.swiglu_forward(
x,
psum_num_tokens_per_expert,
topk_weights,
routed_scaling_factor,
clamp_value,
clamped_count,
)
return _ref.cast(
activated,
fmt,
block_size=(1, num_per_channels),
round_sf=round_sf,
use_tma_aligned_col_major_sf=use_tma_aligned_col_major_sf,
use_packed_ue8m0=use_packed_ue8m0,
)
def batched_transpose_weight_sf(
sf: Sequence[torch.Tensor],
block_size: int,
transpose_sf1s: Sequence[bool],
outs: Sequence[tuple[torch.Tensor, torch.Tensor] | None],
) -> list[tuple[torch.Tensor, torch.Tensor]]:
"""Transpose the scale factors of several weights in one launch."""
if not use_references():
return _compiled.batched_transpose_weight_sf(sf, block_size, transpose_sf1s, outs)
# The reference allocates its outputs, so the `outs` slots cannot be filled in place. Check
# rather than ignore: a caller that pre-allocated would read stale memory.
if any(o is not None for o in outs):
raise NotImplementedError("the torch reference allocates its own outputs; pre-allocated `outs` cannot be filled")
return _ref.batched_transpose_weight_sf(sf, block_size, transpose_sf1s)
def unpack_from_e2m1fn_x2(x: torch.Tensor, out_dtype: torch.dtype = torch.float32) -> torch.Tensor:
"""Decode packed FP4 pairs. Same on both paths -- it is host-side bit manipulation."""
return _ref.unpack_from_e2m1fn_x2(x, out_dtype)
def backend() -> str:
return "ref" if use_references() else "cuda"
def _passthrough(name: str) -> Any:
"""Resolve a name this module does not adapt from whichever backend is active."""
if not use_references():
return getattr(_compiled, name)
return getattr(_ref, name)