Files
wehub-resource-sync e9a2f726c9
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:29:51 +08:00

512 lines
19 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Tests for omlx._torch_stub.
The stub is load-bearing for the DMG flow: it satisfies xgrammar /
tvm_ffi's import-time torch references without the real ~500 MB torch
wheel. Direct tests here catch the realistic regression where a future
xgrammar / tvm_ffi version starts touching a new torch attribute at
import.
"""
from __future__ import annotations
import importlib
import os
import subprocess
import sys
import textwrap
import threading
import types
import unittest.mock as mock
import pytest
# Save modules touched by install() so each test starts clean.
_TOUCHED = (
"torch",
"torch.cuda",
"torch.cuda.amp",
"torch.cuda.amp.common",
"torch.backends",
"torch.backends.mps",
"torch.backends.cudnn",
"torch.version",
"torch.nn",
"torch.nn.functional",
"torch.utils",
"torch.utils.dlpack",
)
@pytest.fixture(autouse=True)
def _restore_sys_modules():
saved = {k: sys.modules[k] for k in _TOUCHED if k in sys.modules}
# Clear any leftover stub state from a previous test so each starts clean.
for k in _TOUCHED:
sys.modules.pop(k, None)
yield
for k in _TOUCHED:
sys.modules.pop(k, None)
sys.modules.update(saved)
@pytest.fixture
def stub_module():
"""Import a fresh copy of the stub module so its module-level state
doesn't leak between tests."""
if "omlx._torch_stub" in sys.modules:
importlib.reload(sys.modules["omlx._torch_stub"])
return sys.modules["omlx._torch_stub"]
import omlx._torch_stub as m
return m
def test_install_returns_true_and_populates_sys_modules(stub_module):
# Force "no real torch": remove any existing torch import.
for k in _TOUCHED:
sys.modules.pop(k, None)
with mock.patch(
"importlib.util.find_spec", side_effect=lambda name: None
):
applied = stub_module.install()
assert applied is True
for k in _TOUCHED:
assert k in sys.modules, f"{k} not installed in sys.modules"
torch = sys.modules["torch"]
assert torch.__version__.endswith("+omlx-stub")
# The dtype set xgrammar/tvm_ffi look up at import time.
for dt in (
"int8", "int16", "int32", "int", "int64", "long", "uint8",
"float16", "half", "float32", "float", "float64", "double",
"bfloat16", "bool", "short",
):
assert hasattr(torch, dt), f"torch.{dt} missing"
# Tensor aliases that xgrammar's contrib/hf.py uses in annotations.
for alias in ("Tensor", "LongTensor", "FloatTensor", "IntTensor"):
assert hasattr(torch, alias)
# Submodules tvm_ffi reaches into.
assert sys.modules["torch.cuda"].is_available() is False
assert sys.modules["torch.cuda"].device_count() == 0
assert (
sys.modules["torch.cuda.amp.common"].amp_definitely_not_available() is True
)
assert sys.modules["torch.backends.mps"].is_available() is False
assert sys.modules["torch.backends.mps"].is_built() is False
assert sys.modules["torch.version"].cuda is None
def test_install_is_idempotent(stub_module):
for k in _TOUCHED:
sys.modules.pop(k, None)
with mock.patch("importlib.util.find_spec", side_effect=lambda name: None):
first = stub_module.install()
second = stub_module.install()
assert first is True
# Second call sees the stub already in sys.modules and reports it.
assert second is True
def test_install_no_op_when_real_torch_present(stub_module):
# Simulate a previously-imported real torch module.
real = types.ModuleType("torch")
real.__version__ = "2.4.0"
real.__spec__ = importlib.machinery.ModuleSpec("torch", loader=None)
sys.modules["torch"] = real
applied = stub_module.install()
assert applied is False
# We must not have replaced the real torch.
assert sys.modules["torch"] is real
# And we must not have added stub submodules on top of real torch.
assert "torch.cuda" not in sys.modules
def test_install_no_op_when_torch_findable_via_spec(stub_module):
# No torch in sys.modules, but importlib can find a spec for it.
for k in _TOUCHED:
sys.modules.pop(k, None)
fake_spec = importlib.machinery.ModuleSpec("torch", loader=None)
with mock.patch(
"importlib.util.find_spec",
side_effect=lambda name: fake_spec if name == "torch" else None,
):
applied = stub_module.install()
assert applied is False
assert "torch" not in sys.modules
def test_stub_dtype_works_as_dict_key(stub_module):
"""tvm_ffi.cython.dtype.pxi builds a dict keyed by torch.int8,
torch.bfloat16, etc. — verify the stub dtypes are hashable and
distinct."""
for k in _TOUCHED:
sys.modules.pop(k, None)
with mock.patch("importlib.util.find_spec", side_effect=lambda name: None):
stub_module.install()
torch = sys.modules["torch"]
table = {
torch.int8: 1,
torch.short: 2,
torch.int32: 3,
torch.int64: 4,
torch.bfloat16: 5,
torch.bool: 6,
torch.float32: 7,
}
# All distinct keys.
assert len(table) == 7
assert table[torch.int32] == 3
def test_stub_tensor_isinstance_check(stub_module):
"""xgrammar/tvm_ffi use isinstance(value, torch.Tensor) to gate
torch-specific paths. Our values (numpy arrays, mx.array) must
correctly fail that check."""
for k in _TOUCHED:
sys.modules.pop(k, None)
with mock.patch("importlib.util.find_spec", side_effect=lambda name: None):
stub_module.install()
torch = sys.modules["torch"]
assert isinstance(torch.Tensor(), torch.Tensor) # stub instance is its own tensor
# Non-stub values cleanly fail.
assert not isinstance(42, torch.Tensor)
assert not isinstance([1, 2, 3], torch.Tensor)
assert not isinstance("hello", torch.Tensor)
# torch.dtype is also a class for isinstance checks.
assert isinstance(torch.int32, torch.dtype)
assert not isinstance(42, torch.dtype)
def test_unsupported_helpers_raise_runtime_error(stub_module):
"""torch.full / torch.zeros / torch.nn.functional.pad are stubbed to
raise RuntimeError so a future caller gets a clear error instead of
a cryptic None-attribute traceback."""
for k in _TOUCHED:
sys.modules.pop(k, None)
with mock.patch("importlib.util.find_spec", side_effect=lambda name: None):
stub_module.install()
torch = sys.modules["torch"]
with pytest.raises(RuntimeError, match="torch.full"):
torch.full((1,), 0)
with pytest.raises(RuntimeError, match="torch.zeros"):
torch.zeros((1,))
with pytest.raises(RuntimeError, match="nn.functional.pad"):
torch.nn.functional.pad(None, (0, 1))
def test_torch_tensor_returns_stub_instance_with_loud_method_failure(
stub_module,
):
"""torch.tensor(...) returns a _StubTensor instance so module-globals
like ``_FULL_MASK = torch.tensor(-1, dtype=...)`` survive import time.
Subsequent method calls (e.g. ``.fill_()``) raise a clear RuntimeError
rather than the prior silent-None path.
"""
for k in _TOUCHED:
sys.modules.pop(k, None)
with mock.patch("importlib.util.find_spec", side_effect=lambda name: None):
stub_module.install()
torch = sys.modules["torch"]
t = torch.tensor(-1, dtype=torch.int32)
assert isinstance(t, torch.Tensor)
with pytest.raises(RuntimeError, match="_StubTensor.fill_"):
t.fill_(0)
def test_dtype_aliases_share_identity(stub_module):
"""Real torch has ``torch.int is torch.int32`` — preserve that identity
so code doing ``assert x.dtype is torch.int32`` against ``torch.int``
works identically against the stub."""
for k in _TOUCHED:
sys.modules.pop(k, None)
with mock.patch("importlib.util.find_spec", side_effect=lambda name: None):
stub_module.install()
torch = sys.modules["torch"]
assert torch.int is torch.int32
assert torch.long is torch.int64
assert torch.short is torch.int16
assert torch.half is torch.float16
assert torch.float is torch.float32
assert torch.double is torch.float64
def test_dtype_str_returns_torch_prefix(stub_module):
"""tvm_ffi.cpp.dtype.to_cpp_dtype calls ``str(dtype)`` and strips
a ``torch.`` prefix; our dtypes must serialize that way."""
for k in _TOUCHED:
sys.modules.pop(k, None)
with mock.patch("importlib.util.find_spec", side_effect=lambda name: None):
stub_module.install()
torch = sys.modules["torch"]
assert str(torch.int32) == "torch.int32"
assert str(torch.bfloat16) == "torch.bfloat16"
def test_install_sets_tvm_ffi_dlpack_env_var(stub_module):
"""install() must set TVM_FFI_DISABLE_TORCH_C_DLPACK so tvm-ffi skips
the doomed JIT extension build that otherwise spawns a Python
subprocess and surfaces a misleading warning at every cold start.
"""
for k in _TOUCHED:
sys.modules.pop(k, None)
os.environ.pop("TVM_FFI_DISABLE_TORCH_C_DLPACK", None)
try:
with mock.patch(
"importlib.util.find_spec", side_effect=lambda name: None
):
stub_module.install()
assert os.environ.get("TVM_FFI_DISABLE_TORCH_C_DLPACK") == "1"
finally:
os.environ.pop("TVM_FFI_DISABLE_TORCH_C_DLPACK", None)
def test_install_does_not_touch_env_var_when_real_torch_present(stub_module):
"""The opposite of the previous test: when real torch is detected via
find_spec, install() must NOT mutate TVM_FFI_DISABLE_TORCH_C_DLPACK.
A user with real torch installed may want the tvm-ffi/torch-C-DLPack
fast path; the stub should not silently disable it.
"""
for k in _TOUCHED:
sys.modules.pop(k, None)
os.environ.pop("TVM_FFI_DISABLE_TORCH_C_DLPACK", None)
try:
fake_spec = importlib.util.spec_from_loader("torch", loader=None)
with mock.patch(
"importlib.util.find_spec",
side_effect=lambda name: fake_spec if name == "torch" else None,
):
result = stub_module.install()
assert result is False
assert "TVM_FFI_DISABLE_TORCH_C_DLPACK" not in os.environ, (
"real-torch path must leave the env var alone"
)
finally:
os.environ.pop("TVM_FFI_DISABLE_TORCH_C_DLPACK", None)
def test_missing_top_level_attribute_raises_attributeerror_and_logs(
stub_module, caplog
):
"""``torch.<unknown>`` must raise ``AttributeError`` (so ``hasattr``
consumers behave correctly) AND log a one-shot WARNING that names
the missing attribute. The log is the operator-facing diagnostic
when a future xgrammar / tvm-ffi release reaches for a torch
surface the stub doesn't cover; without it, the AttributeError
surfaces only if the caller logs it themselves.
"""
for k in _TOUCHED:
sys.modules.pop(k, None)
with mock.patch("importlib.util.find_spec", side_effect=lambda name: None):
stub_module.install()
torch = sys.modules["torch"]
with caplog.at_level("WARNING", logger="omlx._torch_stub"):
with pytest.raises(AttributeError, match="torch.compile"):
torch.compile # noqa: B018
assert any(
"missing attribute: torch.compile" in rec.message
for rec in caplog.records
), caplog.records
# ``hasattr`` must continue to return False (i.e. the AttributeError
# path is reachable) — regression for replacing the raise with a
# log-and-return.
assert not hasattr(torch, "another_missing_attr")
def test_known_probe_names_log_at_debug_not_warning(stub_module, caplog):
"""xgrammar / tvm_ffi probe a fixed set of dtype names via
``getattr(torch, name)`` for feature detection. They catch the
AttributeError and fall back, so a per-probe WARNING is pure noise.
Known-probed names log at DEBUG instead.
Regression for #1453 review feedback (fry69): 9 WARNING entries per
model load flagged as actionable when they aren't.
"""
for k in _TOUCHED:
sys.modules.pop(k, None)
with mock.patch("importlib.util.find_spec", side_effect=lambda name: None):
stub_module.install()
torch = sys.modules["torch"]
# Probe one known dtype + one genuinely-missing attribute. Capture at
# DEBUG so both log calls land in caplog.records and we can compare
# their levels.
with caplog.at_level("DEBUG", logger="omlx._torch_stub"):
with pytest.raises(AttributeError):
torch.float8_e4m3fn # noqa: B018
with pytest.raises(AttributeError):
torch.totally_unknown_attr # noqa: B018
dtype_records = [
rec for rec in caplog.records
if "torch.float8_e4m3fn" in rec.message
]
unknown_records = [
rec for rec in caplog.records
if "torch.totally_unknown_attr" in rec.message
]
assert dtype_records, "known-probe name should still log at DEBUG"
assert unknown_records, "unknown name should still log"
assert all(rec.levelname == "DEBUG" for rec in dtype_records), (
f"known probe must log at DEBUG, got {[r.levelname for r in dtype_records]}"
)
assert all(rec.levelname == "WARNING" for rec in unknown_records), (
f"unknown attr must log at WARNING, got {[r.levelname for r in unknown_records]}"
)
def test_stub_modules_have_real_spec_and_loader(stub_module):
"""Every stub module in sys.modules must have a real ``__spec__``
(a ``ModuleSpec`` instance, not ``None``) so ``importlib.util.
find_spec`` succeeds for downstream consumers — transformers /
accelerate / huggingface_hub all probe torch via find_spec at
import time, and ``None`` here trips their fallback paths into
incorrect behavior.
"""
for k in _TOUCHED:
sys.modules.pop(k, None)
with mock.patch("importlib.util.find_spec", side_effect=lambda name: None):
stub_module.install()
for name in (
"torch",
"torch.cuda",
"torch.cuda.amp",
"torch.cuda.amp.common",
"torch.backends",
"torch.backends.mps",
"torch.backends.cudnn",
"torch.version",
"torch.nn",
"torch.nn.functional",
"torch.utils",
"torch.utils.dlpack",
):
mod = sys.modules[name]
assert mod.__spec__ is not None, f"{name} missing __spec__"
assert isinstance(mod.__spec__, importlib.machinery.ModuleSpec), (
f"{name}.__spec__ wrong type: {type(mod.__spec__)}"
)
assert mod.__spec__.name == name
def test_utils_dlpack_to_dlpack_raises(stub_module):
"""``torch.utils.dlpack.to_dlpack`` is a separately-exposed helper
(not in ``torch.nn.functional``). If a future tvm-ffi reaches for
it under the stub it must raise loudly rather than silently return
None — calls into this path mean the caller assumed real torch and
will produce wrong results downstream.
"""
for k in _TOUCHED:
sys.modules.pop(k, None)
with mock.patch("importlib.util.find_spec", side_effect=lambda name: None):
stub_module.install()
import torch # type: ignore
with pytest.raises(RuntimeError, match="utils.dlpack.to_dlpack"):
torch.utils.dlpack.to_dlpack(object())
def test_install_is_thread_safe(stub_module):
"""Concurrent install() calls must serialize and produce a single
consistent stub. Regression for a race where two threads both passed
the ``"torch" in sys.modules`` check, both built modules, and
overwrote each other in sys.modules — leaving threads with stale
references to the loser's module objects.
"""
for k in _TOUCHED:
sys.modules.pop(k, None)
results: list[bool] = []
barrier = threading.Barrier(8)
errors: list[Exception] = []
def worker():
try:
barrier.wait(timeout=2.0)
with mock.patch(
"importlib.util.find_spec", side_effect=lambda name: None
):
results.append(stub_module.install())
except Exception as e:
errors.append(e)
threads = [threading.Thread(target=worker) for _ in range(8)]
for t in threads:
t.start()
for t in threads:
t.join(timeout=5.0)
assert not errors, errors
assert len(results) == 8
assert all(r is True for r in results)
# All threads see the same single torch module instance.
torch = sys.modules["torch"]
assert torch.__version__.endswith("+omlx-stub")
@pytest.mark.skipif(
not (importlib.util.find_spec("xgrammar") and importlib.util.find_spec("tvm_ffi")),
reason="xgrammar / tvm_ffi not installed",
)
def test_xgrammar_imports_against_stub_only(stub_module, tmp_path):
"""Realistic regression: spawn a subprocess that blocks real torch and
asserts ``import xgrammar`` and the modules oMLX touches still load
against the stub. This is the test that gates xgrammar / tvm-ffi
version bumps — if a new release reaches for a torch attribute the
stub doesn't cover, this fails loudly at the import step.
"""
script = tmp_path / "probe.py"
script.write_text(textwrap.dedent("""
import sys
# Block real torch end-to-end without touching sys.path (which
# would also strip xgrammar in the common pip layout where both
# live in the same site-packages). A meta-path finder that
# returns None just delegates to the next finder; raising
# ImportError aborts the import before PathFinder runs.
for k in list(sys.modules):
if k == "torch" or k.startswith("torch."):
del sys.modules[k]
import importlib.abc
class _BlockTorch(importlib.abc.MetaPathFinder):
def find_spec(self, fullname, path, target=None):
if fullname == "torch" or fullname.startswith("torch."):
raise ImportError(
f"{fullname} blocked by test probe to force "
"the stub-only path"
)
return None
sys.meta_path.insert(0, _BlockTorch())
# install()'s own `importlib.util.find_spec('torch')` check
# also needs to see no torch.
import importlib.util
_orig_find_spec = importlib.util.find_spec
def _no_torch(name, *args, **kwargs):
if name == "torch" or name.startswith("torch."):
return None
return _orig_find_spec(name, *args, **kwargs)
importlib.util.find_spec = _no_torch
from omlx._torch_stub import install
assert install() is True, (
"stub install returned False — real torch was reachable "
"despite meta-path / find_spec blocking"
)
import xgrammar
from xgrammar import contrib # noqa: F401
from xgrammar.kernels.apply_token_bitmask_mlx import ( # noqa: F401
apply_token_bitmask_mlx,
)
print("OK")
"""))
env = dict(os.environ)
env.pop("TVM_FFI_DISABLE_TORCH_C_DLPACK", None)
out = subprocess.check_output(
[sys.executable, str(script)],
stderr=subprocess.STDOUT,
env=env,
timeout=30,
)
assert b"OK" in out, out