mirror of
https://github.com/deepseek-ai/DeepInfer.git
synced 2026-10-31 08:46:21 +08:00
The point of the Triton backend is to run where TileLang cannot, but importing `deepinfer` pulled the whole TileLang stack in regardless. Three separate leaks: 1. `ops.py` imported the platform default backend even when another one was selected, purely to have fallbacks available. That default is TileLang, so it dragged in `tile_kernels`. `DEEPINFER_OPS_STRICT=1` now skips it: no default import, no fallback, a missing kernel raises. That is the right trade when the default is not installed, or when a silent fallback would hide an incomplete port. 2. `tile_kernels` sat at module scope in `norm`, `cache_common`, `fused_transform` and `load_paged_kv_cache`, but only the reference implementations and the unit tests use it. The kernels never do. It now lives inside those functions. 3. `tilelang` itself: `compress` set a TileLang pass flag, and `norm` needed a TileLang dtype handle to pass along a parameter the Triton factory discards. Both now degrade to a no-op or a plain string when TileLang is absent. Verified on a FlagTree image (torch 2.11, triton 3.6, no TileLang): `import` succeeds, 19 kernels build and run, and the outputs are element-wise identical to the same kernels built by triton 3.5.1 and 3.7.1. Full A/B on the reference image is still green, so nothing changed for the paths that do have TileLang.
176 lines
7.4 KiB
Python
176 lines
7.4 KiB
Python
"""Kernel dispatcher: interface modules do
|
|
``from deepinfer.model_executor.layers.tilelang_ops import ops`` and call
|
|
``ops.xxx_tl``; backend selection happens only in this file.
|
|
|
|
Re-exports all ``xxx_tl`` factories of the selected backend.
|
|
|
|
Backend resolution:
|
|
|
|
* ``DEEPINFER_OPS_BACKEND`` picks the backend package for every kernel. Unset means the platform
|
|
default, which is what the engine has always used, so nothing changes unless asked for.
|
|
* ``DEEPINFER_OPS_FALLBACK`` is a comma-separated list of kernel names that must come from the
|
|
platform default even when another backend is selected. It exists so a partially ported backend
|
|
can still run end to end: the ported kernels exercise the new code, the rest keep working, and
|
|
the comparison stays meaningful throughout the port.
|
|
* ``DEEPINFER_OPS_STRICT`` disables the fallback path entirely: the default backend is never
|
|
imported, and a kernel the selected backend lacks raises. Use it where the default backend is
|
|
not installed (only Triton available) or where a silent fallback would hide an incomplete port.
|
|
|
|
When the selected backend does not export a kernel at all, the default backend's implementation
|
|
is used and a line naming it is printed once, so a partial port is never mistaken for a complete
|
|
one.
|
|
"""
|
|
|
|
import importlib
|
|
import os
|
|
import pkgutil
|
|
import sys
|
|
from pathlib import Path
|
|
from types import ModuleType
|
|
from typing import Any
|
|
|
|
from deepinfer import PLATFORM
|
|
|
|
_TRACE = bool(os.getenv("DEEPINFER_TRACE_KERNELS"))
|
|
|
|
|
|
def _record(name: str, args: tuple = (), kwargs: dict | None = None) -> None:
|
|
"""Log one use of a backend kernel factory, so the real surface can be measured.
|
|
|
|
Called once per kernel *construction* rather than per launch: TileLang caches compiled
|
|
kernels, so construction happens at warmup and the count stays meaningful.
|
|
|
|
With `DEEPINFER_TRACE_KERNEL_ARGS=1` the construction arguments are logged too. Several
|
|
factories take a dozen flags (norm, fused_transform, fused_sample) and only a handful of
|
|
combinations are ever used, so the arguments say which code paths a port actually needs.
|
|
"""
|
|
path = Path(os.environ["DEEPINFER_TRACE_KERNELS"])
|
|
with path.open("a") as f:
|
|
if os.getenv("DEEPINFER_TRACE_KERNEL_ARGS"):
|
|
f.write(f"{name}|args={args!r}|kwargs={sorted((kwargs or {}).items())!r}\n")
|
|
else:
|
|
f.write(f"{name}\n")
|
|
|
|
|
|
class _KernelTrace:
|
|
"""Proxy a `*_tl` factory, recording its first use.
|
|
|
|
A plain function wrapper is not enough: callers read attributes off the factory (the TileLang
|
|
`@tilelang.jit` decorator attaches `pass_configs`, for one), so attribute access has to keep
|
|
working. Everything unknown is forwarded to the real object.
|
|
"""
|
|
|
|
def __init__(self, name: str, factory: Any) -> None:
|
|
self._name = name
|
|
self._factory = factory
|
|
self._seen = False
|
|
|
|
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
if not self._seen:
|
|
self._seen = True
|
|
_record(self._name, args, kwargs)
|
|
return self._factory(*args, **kwargs)
|
|
|
|
def __getattr__(self, item: str) -> Any:
|
|
return getattr(self._factory, item)
|
|
|
|
def __repr__(self) -> str:
|
|
return f"<traced {self._name}>"
|
|
|
|
|
|
class _FallbackNotice:
|
|
"""Report the first *use* of a kernel the selected backend does not implement.
|
|
|
|
The import-time summary says how many names are missing, which is a static count: it says
|
|
nothing about whether the running code actually reaches any of them. This records that, so an
|
|
end-to-end run proves which kernels still came from the default backend instead of leaving it
|
|
to be inferred from a name list.
|
|
"""
|
|
|
|
def __init__(self, name: str, factory: Any, selected: str, default: str) -> None:
|
|
self._name = name
|
|
self._factory = factory
|
|
self._selected = selected
|
|
self._default = default
|
|
self._seen = False
|
|
|
|
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
if not self._seen:
|
|
self._seen = True
|
|
print(
|
|
f"[ops] kernel {self._name!r} used from {self._default!r}: {self._selected!r} does not implement it",
|
|
file=sys.stderr,
|
|
flush=True,
|
|
)
|
|
return self._factory(*args, **kwargs)
|
|
|
|
def __getattr__(self, item: str) -> Any:
|
|
return getattr(self._factory, item)
|
|
|
|
|
|
def _collect(backend: str) -> dict[str, Any]:
|
|
"""Collect every ``*_tl`` factory exported by a backend package, by name."""
|
|
pkg = importlib.import_module(f"{__package__}.{backend}")
|
|
found: dict[str, Any] = {}
|
|
for info in pkgutil.iter_modules(pkg.__path__):
|
|
mod = importlib.import_module(f"{pkg.__name__}.{info.name}")
|
|
for name, obj in vars(mod).items():
|
|
if name.startswith("_") or isinstance(obj, ModuleType) or not name.endswith("_tl"):
|
|
continue
|
|
if name in found:
|
|
raise RuntimeError(f"backend {backend!r} exports {name!r} twice")
|
|
found[name] = obj
|
|
return found
|
|
|
|
|
|
_DEFAULT_BACKEND = PLATFORM.tilelang_ops_backend
|
|
_BACKEND = os.getenv("DEEPINFER_OPS_BACKEND") or _DEFAULT_BACKEND
|
|
# Strict mode never imports the platform default backend. That backend is the TileLang one, and
|
|
# importing it drags in `tile_kernels` and `tilelang`, so on a host where only Triton is installed
|
|
# the fallback machinery would make the whole package unimportable. With this set, a missing kernel
|
|
# raises instead of silently borrowing an implementation: the right trade for CI and for measuring
|
|
# how complete a port is, and the wrong one for a run that must produce tokens.
|
|
_STRICT = bool(os.getenv("DEEPINFER_OPS_STRICT"))
|
|
_FALLBACK = (
|
|
{name.strip() for name in os.getenv("DEEPINFER_OPS_FALLBACK", "").split(",") if name.strip()} if _BACKEND != _DEFAULT_BACKEND else set()
|
|
)
|
|
if _STRICT and _FALLBACK:
|
|
raise RuntimeError("DEEPINFER_OPS_STRICT forbids DEEPINFER_OPS_FALLBACK: strict means no fallback of any kind")
|
|
|
|
_SELECTED = _collect(_BACKEND)
|
|
_DEFAULT: dict[str, Any] = _SELECTED if (_BACKEND == _DEFAULT_BACKEND or _STRICT) else _collect(_DEFAULT_BACKEND)
|
|
|
|
_unknown = sorted(_FALLBACK - set(_SELECTED))
|
|
if _unknown:
|
|
raise RuntimeError(f"DEEPINFER_OPS_FALLBACK names kernel(s) backend {_BACKEND!r} does not export: {_unknown}")
|
|
|
|
for _name, _obj in _SELECTED.items():
|
|
globals()[_name] = _KernelTrace(_name, _obj) if _TRACE else _obj
|
|
|
|
for _name in _FALLBACK:
|
|
globals()[_name] = _DEFAULT[_name]
|
|
|
|
_INHERITED = sorted(set(_DEFAULT) - set(_SELECTED) - _FALLBACK)
|
|
for _name in _INHERITED:
|
|
globals()[_name] = _FallbackNotice(_name, _DEFAULT[_name], _BACKEND, _DEFAULT_BACKEND)
|
|
|
|
# Kept short: with data parallelism this prints once per rank, and the full list is dozens of
|
|
# names that are not on the caller's path. `DEEPINFER_OPS_VERBOSE=1` prints them all.
|
|
if _INHERITED:
|
|
print(
|
|
f"[ops] backend {_BACKEND!r} lacks {len(_INHERITED)} kernel(s), falling back to "
|
|
f"{_DEFAULT_BACKEND!r} ({_INHERITED if os.getenv('DEEPINFER_OPS_VERBOSE') else 'first: ' + _INHERITED[0]})",
|
|
file=sys.stderr,
|
|
flush=True,
|
|
)
|
|
|
|
|
|
class KernelUnavailableError(AttributeError):
|
|
"""Selected backend lacks this kernel factory; conftest.py turns it into
|
|
a test skip on non-cuda backends. Subclasses AttributeError to keep the
|
|
module ``__getattr__`` / ``hasattr`` protocol intact."""
|
|
|
|
|
|
def __getattr__(name: str) -> Any: # for static analyzers
|
|
raise KernelUnavailableError(f"tilelang_ops backend {_BACKEND!r} has no kernel {name!r}")
|