loopyt 32d3d2c6bf docs(h20): plan the FlagGems integration, with the first operator already proven
Opens `h20-sm90-flaggems` to replace `tile_kernels`'s torch reference with FlagGems' Triton kernels.
The point is not "runs without TileLang" -- that landed already. It is speed: the compat layer is
plain torch today, so every activation in the hot path is going through eager PyTorch.

Wrote the plan down because the tempting framing is wrong. `tile_kernels` and `flag_gems` are not
substitutes; they overlap in three layers and only the first is a clean fit:

* generic ops (quant, rotary, rms_norm, topk) -- FlagGems' home turf, 251 ops
* DeepSeek-specific fused ops (mhc_*, engram_*, the five-way swiglu+quant) -- partial overlap, and
  almost none of the signatures match 1:1
* the engine kernel backend (tilelang_ops) -- already done, unrelated

De-risked the highest-value one first, since per-token quantisation runs on every activation.
`ab_quant.py`, 5 shapes: `per_token_group_quant_fp8(scale_ue8m0=True, column_major_scales=True)`
against `per_token_cast(fmt="e4m3", 128, round_sf=True, use_packed_ue8m0=False,
use_tma_aligned_col_major_sf=True)` -- **quantised values bitwise identical, scales and dequantised
values exactly zero difference**. The two `round_sf` spellings are the same function: DeepInfer does
`(int + 0x7FFFFF) & 0x7F800000`, which carries into the exponent field, i.e. ceiling to a power of
two; FlagGems does `exp2(ceil(log2(s)))`. Same function, so the test demands exactness rather than
tolerance and reports any difference.

`ab_import_side_effects.py` pins the mechanism that makes this safe: import is inert,
`only_enable(include=[...])` touches exactly what it names. Recorded also that the package root
pulls Triton TLE -> libflagcx, so selective imports or vendoring stay the default posture rather
than reaching for `enable()`.

Two questions the plan puts ahead of the work, because without answers the payoff is unknowable:
nothing on this line has ever been measured for speed, and `batched_transpose_weight_sf` is still
the only path operator never A/B'd -- it rewrites every weight's scale factor and is the leading
suspect for the token divergence. Fixing that could be worth more than any op swap here.

Also names what is deliberately off the table: `enable()` with no arguments, swapping tilelang_ops
again, and churning broadcast-only micro-ops that were never the bottleneck.
2026-10-25 14:38:14 +08:00

117 lines
4.3 KiB
Python

"""Is FlagGems' per-token FP8 quant a drop-in for `tile_kernels`'s, on the SM90 recipe?
This is the highest-value candidate in the FlagGems inventory: per-token quantisation runs on every
activation, in attention and in the MLP, so it is the one where replacing a torch reference with a
Triton kernel actually buys something.
Recipe on SM90 (`get_fp8_cast_params`):
fmt=e4m3, num_per_channels=128, round_sf=True,
use_packed_ue8m0=False, use_tma_aligned_col_major_sf=True
The two `round_sf` implementations look different but should be the same function. DeepInfer does
`(int_bits + 0x7FFFFF) & 0x7F800000` -- add the maximum mantissa and mask to the exponent field,
which carries into the next exponent unless the mantissa was already zero, i.e. ceiling to a power of
two. FlagGems does `exp2(ceil(log2(s)))`, the same thing spelled arithmetically. Both then divide by
that scale. So this should match bit for bit, not merely closely -- which is why the comparison
insists on exactness and reports any difference rather than tolerating it.
Run: PYTHONPATH=. python scripts/flaggems_port/ab_quant.py
"""
from __future__ import annotations
import sys
import torch
from deepinfer.tk_compat.quant import per_token_cast
DEV = "cuda"
GROUP = 128
FAILED: list[str] = []
def flag_gems_quant(x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
"""Call FlagGems' op without importing the package root.
`import flag_gems` star-imports every op module and loads Triton TLE (which needs libflagcx).
Pulling the single module in through the package still runs the package `__init__`, so this goes
through the already-installed package but touches only what it must -- see the note in
`ab_import_side_effects.py` for why importing the root is safe anyway.
"""
from flag_gems.ops.per_token_group_quant_fp8 import per_token_group_quant_fp8
return per_token_group_quant_fp8(x, GROUP, scale_ue8m0=True, column_major_scales=True)
def reference_quant(x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
return per_token_cast(
x,
"e4m3",
GROUP,
round_sf=True,
use_packed_ue8m0=False,
use_tma_aligned_col_major_sf=True,
)
def compare(tag: str, a: torch.Tensor, b: torch.Tensor, exact: bool) -> None:
if a.shape != b.shape:
print(f" DIFF {tag}: shape {tuple(a.shape)} vs {tuple(b.shape)}")
FAILED.append(f"{tag}:shape")
return
if a.dtype != b.dtype:
print(f" DIFF {tag}: dtype {a.dtype} vs {b.dtype}")
FAILED.append(f"{tag}:dtype")
return
if exact:
same = torch.equal(a.view(torch.uint8) if a.dtype.itemsize == 1 else a, b.view(torch.uint8) if b.dtype.itemsize == 1 else b)
print(f" {'ok ' if same else 'DIFF'} {tag}: bitwise identical = {same}")
if not same:
FAILED.append(tag)
return
diff = (a.float() - b.float()).abs()
mx = diff.max().item()
scale = a.float().abs().max().item()
print(f" {'ok ' if mx == 0 else 'DIFF'} {tag}: max abs diff {mx:.3e} (max|a| {scale:.4f})")
if mx != 0:
FAILED.append(tag)
def case(tag: str, tokens: int, dim: int, seed: int) -> None:
torch.manual_seed(seed)
x = (torch.randn((tokens, dim), device=DEV, dtype=torch.bfloat16) * 0.5).contiguous()
print(f"\n[{tag}] x {tuple(x.shape)} {x.dtype}")
q_ref, s_ref = reference_quant(x)
q_fg, s_fg = flag_gems_quant(x)
torch.cuda.synchronize()
compare("quantised values", q_ref, q_fg, exact=True)
compare("scales", s_ref, s_fg, exact=False)
# Dequantising both ways is the property that matters downstream: a matching scale with
# mismatched values would still be wrong after the GEMM.
deq_ref = q_ref.to(torch.float32) * s_ref.float().repeat_interleave(GROUP, dim=-1)
deq_fg = q_fg.to(torch.float32) * s_fg.float().repeat_interleave(GROUP, dim=-1)
compare("dequantised", deq_ref, deq_fg, exact=False)
CASES = [
("typical activation", 512, 4096, 0),
("single token", 1, 4096, 1),
("narrow", 64, 128, 2),
("odd token count", 7, 2048, 3),
("with small values", 128, 1024, 4),
]
if __name__ == "__main__":
for c in CASES:
case(*c)
print()
if FAILED:
print(f"MISMATCH {len(FAILED)}: {FAILED}")
sys.exit(1)
print(f"ALL_MATCH ({len(CASES)} cases)")