refactor: unify kernel module loading and packaging

- loader.py: lazy/cached import; is_available defers the actual load; get_module raises on unavailable
- ops/{attention,rotary,fp8}: use get_module instead of touching private _modules or their own _mod() cache
- package-data: ship astrai.extension.lib *.so in built wheels (non-editable installs previously lost every kernel)
This commit is contained in:
2026-08-23 15:57:04 +08:00
parent 4244df2785
commit 2bc4d2b8a8
5 changed files with 65 additions and 63 deletions
+43 -14
View File
@@ -5,9 +5,15 @@ Each kernel is built by the CMake build in ``csrc/CMakeLists.txt`` into a
``.so`` name equals the pybind name (e.g. ``attn_decode``, defined via
``TORCH_EXTENSION_NAME``). ``KERNEL_NAMES`` is discovered automatically from
the ``.so`` files present, so adding a kernel to the CMake ``KERNELS``
registry needs no change here. On import we try to load each one; kernels
that failed to build (or are running on a CPU-only machine) are marked
unavailable so the wrapper functions can fall back to ``torch`` SDPA.
registry needs no change here.
Loading is **lazy and centralized**: module names are discovered eagerly
(cheap glob), but each ``.so`` is imported on first use via the single
``get_module`` accessor, then cached. The wrapper modules (``ops/*.py``) never
touch the internals or keep their own caches — they call ``get_module(name)``
(or ``is_available(name)`` when a torch fallback is acceptable). A kernel that
failed to build (or is running on a CPU-only machine) is ``None`` in the cache,
so ``is_available`` returns ``False`` and ``get_module`` raises a clear error.
"""
import glob
@@ -34,21 +40,44 @@ KERNEL_NAMES = _discover_kernel_names()
_available: dict[str, bool] = {}
_modules: dict[str, object] = {}
for _name in KERNEL_NAMES:
try:
_mod = importlib.import_module(f".lib.{_name}", package=__package__)
_available[_name] = True
_modules[_name] = _mod
except ImportError:
_available[_name] = False
_modules[_name] = None
def _try_load(name: str) -> object:
"""Import and cache the ``name`` kernel module (lazy, one attempt).
Returns the module, or ``None`` if it is unavailable. Cached so each
``.so`` is imported at most once per process.
"""
if name not in _modules:
try:
_modules[name] = importlib.import_module(
f".lib.{name}", package=__package__
)
_available[name] = True
except ImportError:
logger.warning("kernel '%s' failed to import; marking unavailable", name)
_modules[name] = None
_available[name] = False
return _modules[name]
def is_available(name: str) -> bool:
"""Return ``True`` if the compiled kernel ``name`` was loaded."""
"""Return ``True`` if the compiled kernel ``name`` could be loaded."""
if name not in _available:
_try_load(name)
return _available.get(name, False)
def get_module(name: str) -> object:
"""Return the loaded kernel module for ``name``, or ``None`` if unavailable."""
return _modules.get(name)
"""Return the loaded kernel module for ``name``, importing it on first use.
Raises ``RuntimeError`` if the kernel is unavailable (not built, or failed
to import) — callers that can tolerate a torch fallback should check
``is_available(name)`` first instead.
"""
mod = _try_load(name)
if mod is None:
raise RuntimeError(
f"CUDA kernel '{name}' is not available. "
f"Build with CSRC_KERNELS=true (or use the torch-native fallback)."
)
return mod