DeepInfer/scripts/tile_kernels_patch/patch_quant_common.py
loopyt cbb28a8d65 feat(h20): make the pure-torch kernel references usable without TileLang
The bring-up plan for a card that cannot build TileLang is to run the model on
`tile_kernels.torch` -- pure-torch implementations of nearly every kernel the forward pass
calls -- and replace the hot ones with Triton afterwards. That plan was blocked at the
import: `import tile_kernels.torch` dragged in the whole compiled stack.

Three eager imports were responsible, and each is now lazy or optional:

* the top-level `__init__` re-exported every compiled kernel; it resolves them on access
* `quant/__init__` did the same for 13 modules, and `import quant.common` runs it first
* `quant/common.py` mixed host-side config dataclasses with `@T.macro` fragments; the
  dataclasses now import without TileLang, the macros are behind a guard, and its three
  dtype-handle members resolve TileLang when called rather than when imported

`config.py` also only needed TileLang for three constants, now literals.

One thing that looks like it should work and does not: a module-wide
`from __future__ import annotations`. It also stringifies the annotations inside `@T.macro`
bodies, which TileLang's macro parser evaluates itself, and it fails there. Only the four
host-side annotations are quoted, individually.

The patches are idempotent and reversible, and `verify_tile_kernels_patch.py` proves the
surface is unchanged where TileLang is installed -- the kernel modules' wildcard imports of
`quant.common` still see the macros, `from pkg import sub` still works, config knobs still
take effect.

The other half of the question, whether the references are numerically trustworthy, is
answered by `compare_with_cuda.py`: on the recipe the engine actually uses on Hopper,
per_token_cast, cast_back, swiglu_forward_and_per_token_cast and engram_hash are bitwise
identical, and apply_rotary differs by one bf16 ULP on one element in 65536. The quantisation
chain being bitwise equal is what makes a torch-first bring-up safe rather than approximate;
a cast that were merely close would change tokens downstream.

Not yet done, and recorded in the doc: MHC and the MoE gate still need their own comparison
before anyone assumes they match, and the routing layer that points deepinfer's 25 call sites
at these references does not exist yet.
2026-10-22 16:08:46 +08:00

174 lines
6.3 KiB
Python
Executable File

"""Patch tile_kernels.quant.common so its host-side half imports without TileLang.
The module mixes two things: configuration dataclasses and shape helpers (pure Python, needed
by `tile_kernels.torch`), and TileLang `@T.macro` kernel fragments (only used by the compiled
kernels). An eager `from tilelang import language as T` plus module-level `@T.macro` decorators
made the whole file unimportable without TileLang, which in turn blocked `tile_kernels.torch`.
Edits, each asserted:
1. The TileLang import becomes optional.
2. The four host-visible annotations that mention `T.dtype` are quoted, so they are not
evaluated at definition time. Quoting them locally is deliberate: a module-wide
`from __future__ import annotations` also stringifies the annotations *inside* the
`@T.macro` bodies, and TileLang's macro parser resolves those itself -- it then fails with
`NameError: name 'float32' is not defined`.
3. The three members that return TileLang dtype handles resolve `T` on call instead. They
describe device-side formats, so they still need TileLang -- but only if called.
4. The `@T.macro` block is guarded, since decorators run at import. A guarded block still
assigns module globals, so `from .common import *` in the kernel modules is unaffected
wherever TileLang is installed.
python patch_quant_common.py <site-packages/tile_kernels> [--revert]
"""
import argparse
import shutil
import subprocess
import sys
from pathlib import Path
OLD_IMPORT = "from tilelang import language as T\n"
NEW_IMPORT = """try:
from tilelang import language as T
except ImportError: # torch reference path only: no TileLang, so no macros below
T = None
"""
# Annotations evaluated at definition time. Quoted so a missing TileLang does not matter.
ANNOTATIONS = [
(
"def get_best_vectorize_size(dtype: T.dtype) -> int:",
'def get_best_vectorize_size(dtype: "T.dtype") -> int:',
),
(
" def dtype(self) -> T.dtype:",
' def dtype(self) -> "T.dtype":',
),
(
" def sf_dtype(self) -> T.dtype:",
' def sf_dtype(self) -> "T.dtype":',
),
(
"def get_sf_inv_dtype(value_dtype: T.dtype, out_config: CastOutputConfig) -> T.dtype:",
'def get_sf_inv_dtype(value_dtype: "T.dtype", out_config: CastOutputConfig) -> "T.dtype":',
),
]
OLD_DTYPE = """ @property
def dtype(self) -> "T.dtype":
return T.dtype(self.torch_dtype) if self.torch_dtype != torch.int8 else T.float4_e2m1fn
"""
NEW_DTYPE = """ @property
def dtype(self) -> "T.dtype":
# A TileLang dtype handle describes a device-side format, so it resolves on call rather
# than at import: the torch reference path never needs it and must not need TileLang.
from tilelang import language as T
return T.dtype(self.torch_dtype) if self.torch_dtype != torch.int8 else T.float4_e2m1fn
"""
OLD_SF_DTYPE = """ @property
def sf_dtype(self) -> "T.dtype":
return T.dtype(self.sf_torch_dtype)
"""
NEW_SF_DTYPE = """ @property
def sf_dtype(self) -> "T.dtype":
from tilelang import language as T
return T.dtype(self.sf_torch_dtype)
"""
OLD_SF_INV = """ if value_dtype == T.bfloat16 and out_config.round_sf:
"""
NEW_SF_INV = """ from tilelang import language as T
if value_dtype == T.bfloat16 and out_config.round_sf:
"""
MACRO_START = "@T.macro\n"
MACRO_END = " tensor[row, col] = sf\n"
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("target", type=Path, help="the installed tile_kernels package directory")
parser.add_argument("--revert", action="store_true")
args = parser.parse_args()
path = args.target / "quant" / "common.py"
backup = path.with_suffix(".py.orig2")
if not path.is_file():
print(f"missing: {path}", file=sys.stderr)
return 2
if args.revert:
if not backup.is_file():
print(f"no backup: {backup}", file=sys.stderr)
return 1
shutil.move(str(backup), str(path))
print("reverted")
return 0
if not backup.is_file():
shutil.copy2(path, backup)
text = path.read_text()
# Idempotent: `T = None` only appears in the patched form, so seeing it means the whole patch
# already applied and a re-run should be a no-op rather than an assertion error.
if " T = None\n" in text:
print("already patched; nothing to do")
return 0
steps = []
def apply(name: str, old: str, new: str) -> None:
nonlocal text
found = text.count(old)
if found != 1:
raise SystemExit(f"{name}: expected 1 occurrence, found {found}")
text = text.replace(old, new, 1)
steps.append(name)
apply("optional tilelang import", OLD_IMPORT, NEW_IMPORT)
for old, new in ANNOTATIONS:
apply(f"quoted annotation: {old.strip()[:48]}", old, new)
apply("BaseCastConfig.dtype", OLD_DTYPE, NEW_DTYPE)
apply("BaseCastConfig.sf_dtype", OLD_SF_DTYPE, NEW_SF_DTYPE)
apply("get_sf_inv_dtype", OLD_SF_INV, NEW_SF_INV)
# Guard the macro block: keep the definitions out of import time without changing what the
# kernel modules see, because a guarded block still sets module globals.
start = text.index(MACRO_START)
end = text.index(MACRO_END) + len(MACRO_END)
block = text[start:end]
indented = "".join(f" {line}" if line.strip() else line for line in block.splitlines(True))
text = text[:start] + "if T is not None:\n" + indented + text[end:]
steps.append("guarded @T.macro block")
path.write_text(text)
compile(path.read_text(), str(path), "exec")
for step in steps:
print(f" applied: {step}")
# The macros must still exist when TileLang is installed, otherwise the kernel modules break.
root = str(args.target.parent)
probe_code = (
"import sys\n"
f"sys.path.insert(0, {root!r})\n"
"from tile_kernels.quant.common import (\n"
" BaseCastConfig, get_sf_inv_dtype, get_sf_and_inv,\n"
" load_sf, store_sf, transform_sf, get_sf_index)\n"
"print('macros + host api present')\n"
)
probe = subprocess.run(
[sys.executable, "-c", probe_code],
capture_output=True,
text=True,
)
print(probe.stdout.strip() or "\n".join(probe.stderr.strip().splitlines()[-3:]))
return probe.returncode
if __name__ == "__main__":
sys.exit(main())