[ROCm] Defer `tilelang` import through its import `from vllm.tilelang_utils import tilelang` and relaxed `has_tilelang` (#51159)
Signed-off-by: Felix Marty <Felix.Marty@amd.com> Co-authored-by: Andreas Karatzas <akaratza@amd.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
fxmarty-amd committed
Aug 13, 2026 at 20:12 UTC
c5b7c069a43e88dc1fb67e91286887cc664e2af9
8 files changed
+313
-52
docs/assets/contributing/dockerfile-stages-dependency.png
Binary files a/docs/assets/contributing/dockerfile-stages-dependency.png and b/docs/assets/contributing/dockerfile-stages-dependency.png differ
tests/jit_monitor/test_hooks.py
+21
@@ -12,6 +12,7 @@ from unittest import mock
12
13
import pytest
14
15
+from vllm.platforms import current_platform
16
from vllm.utils import jit_monitor
17
18
pytestmark = pytest.mark.cpu_test
@@ -322,6 +323,10 @@ def test_cutedsl_subscripted_compile_is_monitored():
323
# ------------------------------------------------------------------
324
325
326
+@pytest.mark.skipif(
327
+ current_platform.is_rocm(),
328
+ reason="TileLang JIT monitoring is disabled on ROCm",
329
+)
330
def test_tilelang_jit_kernel_logs_warning():
331
with _patch_jit_modules(_make_fake_knobs()):
332
from tilelang.jit.kernel import JITKernel
@@ -337,6 +342,10 @@ def test_tilelang_jit_kernel_logs_warning():
342
assert "tl_kernel" in msg
343
344
345
+@pytest.mark.skipif(
346
+ current_platform.is_rocm(),
347
+ reason="TileLang JIT monitoring is disabled on ROCm",
348
+)
349
def test_tilelang_jit_impl_logs_warning():
350
with _patch_jit_modules(_make_fake_knobs()):
351
from tilelang.jit import JITImpl
@@ -386,6 +395,10 @@ def test_tilelang_jit_impl_logs_warning():
395
assert "tilelang_fn" in msg
396
397
398
+@pytest.mark.skipif(
399
+ current_platform.is_rocm(),
400
+ reason="TileLang JIT monitoring is disabled on ROCm",
401
+)
402
def test_tilelang_jit_impl_does_not_log_on_cache_hit():
403
with _patch_jit_modules(_make_fake_knobs()):
404
from tilelang.jit import JITImpl
@@ -413,6 +426,10 @@ def test_tilelang_jit_impl_does_not_log_on_cache_hit():
426
warning_once.assert_called_once()
427
428
429
+@pytest.mark.skipif(
430
+ current_platform.is_rocm(),
431
+ reason="TileLang JIT monitoring is disabled on ROCm",
432
+)
433
def test_tilelang_from_database_does_not_log():
434
with _patch_jit_modules(_make_fake_knobs()):
435
from tilelang.jit.kernel import JITKernel
@@ -425,6 +442,10 @@ def test_tilelang_from_database_does_not_log():
442
warning_once.assert_not_called()
443
444
445
+@pytest.mark.skipif(
446
+ current_platform.is_rocm(),
447
+ reason="TileLang JIT monitoring is disabled on ROCm",
448
+)
449
def test_tilelang_error_mode_raises():
450
with _patch_jit_modules(_make_fake_knobs()):
451
from tilelang.jit.kernel import JITKernel
tests/kernels/test_mhc_tilelang_jit.py
new
+128
@@ -0,0 +1,128 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+import ctypes
5
+import importlib
6
+import importlib.util
7
+import sys
8
+from types import ModuleType
9
+from typing import Any
10
+
11
+import pytest
12
+
13
+from vllm.platforms import current_platform
14
+from vllm.utils import import_utils
15
+
16
+
17
+class _PassConfigKey:
18
+ TL_DISABLE_WARP_SPECIALIZED = "disable_warp_specialized"
19
+ TL_DISABLE_TMA_LOWER = "disable_tma_lower"
20
+ TL_PTXAS_REGISTER_USAGE_LEVEL = "ptxas_register_usage_level"
21
+
22
+
23
+def _install_tilelang_stub(
24
+ monkeypatch: pytest.MonkeyPatch,
25
+) -> dict[str, int]:
26
+ calls = {"jit_decorate": 0, "compiled_call": 0}
27
+
28
+ tilelang: Any = ModuleType("tilelang")
29
+
30
+ def jit(**kwargs: Any) -> Any:
31
+ def decorate(func: Any) -> Any:
32
+ calls["jit_decorate"] += 1
33
+
34
+ def compiled(*args: Any, **kw: Any) -> Any:
35
+ calls["compiled_call"] += 1
36
+ return func.__name__
37
+
38
+ return compiled
39
+
40
+ return decorate
41
+
42
+ tilelang.PassConfigKey = _PassConfigKey
43
+ tilelang.jit = jit
44
+
45
+ monkeypatch.setattr(import_utils, "has_tilelang", lambda: True)
46
+ monkeypatch.setitem(sys.modules, "tilelang", tilelang)
47
+ monkeypatch.setitem(
48
+ sys.modules, "tilelang.language", ModuleType("tilelang.language")
49
+ )
50
+ monkeypatch.delitem(sys.modules, "vllm.tilelang_utils", raising=False)
51
+
52
+ return calls
53
+
54
+
55
+def test_tilelang_jit_decorator_is_lazy_only_on_rocm(
56
+ monkeypatch: pytest.MonkeyPatch,
57
+) -> None:
58
+ if not (current_platform.is_cuda() or current_platform.is_rocm()):
59
+ pytest.skip("Test requires CUDA or ROCm")
60
+
61
+ calls = _install_tilelang_stub(monkeypatch)
62
+ module_name = "vllm.model_executor.kernels.mhc.tilelang_kernels"
63
+ monkeypatch.delitem(sys.modules, module_name, raising=False)
64
+ module = importlib.import_module(module_name)
65
+
66
+ if current_platform.is_rocm():
67
+ assert calls["jit_decorate"] == 0
68
+ else:
69
+ assert calls["jit_decorate"] > 0
70
+
71
+ decorated_calls = calls["jit_decorate"]
72
+ assert module.mhc_post_tilelang() == "mhc_post_tilelang"
73
+ if current_platform.is_rocm():
74
+ assert calls["jit_decorate"] == 1
75
+ else:
76
+ assert calls["jit_decorate"] == decorated_calls
77
+ assert calls["compiled_call"] == 1
78
+
79
+
80
+@pytest.mark.skipif(not current_platform.is_rocm(), reason="Test requires ROCm")
81
+def test_deepseek_v4_import_and_jit_monitor_do_not_hijack_hip_symbols(
82
+ monkeypatch: pytest.MonkeyPatch,
83
+) -> None:
84
+ if importlib.util.find_spec("tilelang") is None:
85
+ pytest.skip("Test requires TileLang to be installed")
86
+
87
+ class DlInfo(ctypes.Structure):
88
+ _fields_ = [
89
+ ("dli_fname", ctypes.c_char_p),
90
+ ("dli_fbase", ctypes.c_void_p),
91
+ ("dli_sname", ctypes.c_char_p),
92
+ ("dli_saddr", ctypes.c_void_p),
93
+ ]
94
+
95
+ libdl = ctypes.CDLL("libdl.so.2")
96
+ dlsym = libdl.dlsym
97
+ dlsym.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
98
+ dlsym.restype = ctypes.c_void_p
99
+ dladdr = libdl.dladdr
100
+ dladdr.argtypes = [ctypes.c_void_p, ctypes.POINTER(DlInfo)]
101
+ dladdr.restype = ctypes.c_int
102
+
103
+ from vllm.model_executor.layers import mhc # noqa: F401
104
+ from vllm.models import deepseek_v4 # noqa: F401
105
+ from vllm.utils import jit_monitor
106
+
107
+ monkeypatch.setattr(jit_monitor, "_active", False)
108
+ monkeypatch.setattr(jit_monitor, "_setup_triton_autotuning_print", lambda: None)
109
+ monkeypatch.setattr(jit_monitor, "_setup_triton_jit_hook", lambda: None)
110
+ monkeypatch.setattr(jit_monitor, "_setup_cutedsl_jit_hook", lambda: None)
111
+ monkeypatch.setattr(
112
+ jit_monitor,
113
+ "_setup_tilelang_jit_hook",
114
+ lambda: pytest.fail("TileLang JIT monitor must not run on ROCm"),
115
+ )
116
+ jit_monitor.activate()
117
+
118
+ assert not any(
119
+ name == "tilelang" or name.startswith("tilelang.") for name in sys.modules
120
+ )
121
+
122
+ address = dlsym(None, b"hipFree")
123
+ assert address is not None, "hipFree is not available in the global symbol table"
124
+ info = DlInfo()
125
+ assert dladdr(address, ctypes.byref(info))
126
+ source = info.dli_fname.decode() if info.dli_fname else "<unknown>"
127
+ assert "libhip_stub.so" not in source, source
128
+ assert "libamdhip64.so" in source, source
tools/pre_commit/check_forbidden_imports.py
+32
@@ -92,6 +92,15 @@ CHECK_IMPORTS = {
92
),
93
allowed_files={"vllm/triton_utils/importing.py"},
94
),
95
+ "tilelang": ForbiddenImport(
96
+ pattern=r"^(from|import)\s+tilelang(\s|\.|$)",
97
+ tip="Use 'from vllm.tilelang_utils import tilelang, T' instead.",
98
+ allowed_pattern=re.compile(
99
+ r"from\s+vllm\.tilelang_utils\s+import\s+"
100
+ r"(tilelang|T|T, tilelang|tilelang, T)\b"
101
+ ),
102
+ allowed_files={"vllm/tilelang_utils/__init__.py"},
103
+ ),
104
"huggingface_hub repo API": ForbiddenImport(
105
# Catch `from huggingface_hub import <fn>`, including parenthesized,
106
# multi-line imports.
@@ -182,6 +191,29 @@ def test_regex():
191
f"(expected {should_match}, got {result})"
192
)
193
194
+ tilelang_cases = [
195
+ # Should match
196
+ ("import tilelang", True),
197
+ ("import tilelang.language as T", True),
198
+ ("from tilelang.jit import JITImpl", True),
199
+ ("from tilelang.jit.kernel import JITKernel", True),
200
+ # Should not match: indented (local) imports are allowed, mirroring
201
+ # the "triton" rule, so mocked-module test imports are not flagged.
202
+ (" import tilelang", False),
203
+ (" from tilelang.jit import JITImpl", False),
204
+ ("from vllm.tilelang_utils import tilelang", False),
205
+ ("from vllm.tilelang_utils import T", False),
206
+ ("from vllm.tilelang_utils import T, tilelang", False),
207
+ ("from vllm.tilelang_utils import tilelang, T", False),
208
+ ("import tilelang_kernels", False),
209
+ ]
210
+ for i, (content, should_match) in enumerate(tilelang_cases):
211
+ result = matches("tilelang", content)
212
+ assert result == should_match, (
213
+ f"tilelang case {i} failed: {content!r} "
214
+ f"(expected {should_match}, got {result})"
215
+ )
216
+
217
hf_cases = [
218
# Should match
219
("from huggingface_hub import snapshot_download", True),
vllm/model_executor/kernels/mhc/tilelang_kernels.py
+12
-49
@@ -1,29 +1,17 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from __future__ import annotations
5
+
6
import math
7
from functools import cache
5
-from typing import TYPE_CHECKING, Any
8
9
import torch
10
11
from vllm.platforms import current_platform
10
-from vllm.utils.import_utils import has_tilelang
12
+from vllm.tilelang_utils import T, tilelang, tilelang_jit
13
from vllm.utils.math_utils import cdiv
14
13
-# TileLang is used for MHC on CUDA and ROCm. Keep non-GPU imports cheap so
14
-# registering the Python wrapper modules does not require TileLang everywhere.
15
-if TYPE_CHECKING or current_platform.is_cuda_alike():
16
- if not has_tilelang():
17
- raise ImportError(
18
- "tilelang is required for mhc but is not installed. Install it with "
19
- "`pip install tilelang`."
20
- )
21
- import tilelang
22
- import tilelang.language as T
23
-else:
24
- tilelang = None # type: ignore[assignment]
25
- T = None # type: ignore[assignment]
26
-
15
ENABLE_PDL = current_platform.is_arch_support_pdl() and current_platform.is_cuda()
16
17
@@ -40,18 +28,7 @@ def compute_num_split(block_k: int, k: int | None, grid_size: int) -> int:
28
return split_k
29
30
43
-pass_configs: dict[tilelang.PassConfigKey, Any] = {
44
- tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
45
- tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
46
-}
47
-
48
-if current_platform.is_cuda():
49
- pass_configs[tilelang.PassConfigKey.TL_PTXAS_REGISTER_USAGE_LEVEL] = 10
50
-
51
-
52
-@tilelang.jit(
53
- pass_configs=pass_configs,
54
-)
31
+@tilelang_jit
32
def mhc_pre_big_fuse_tilelang(
33
gemm_out_mul,
34
gemm_out_sqrsum,
@@ -191,9 +168,7 @@ def mhc_pre_big_fuse_tilelang(
168
# Copied from https://github.com/sgl-project/sglang/blob/main/python/sglang/srt/layers/mhc.py#L478
169
170
194
-@tilelang.jit(
195
- pass_configs=pass_configs,
196
-)
171
+@tilelang_jit
172
def mhc_pre_big_fuse_with_norm_tilelang(
173
gemm_out_mul,
174
gemm_out_sqrsum,
@@ -353,9 +328,7 @@ def mhc_pre_big_fuse_with_norm_tilelang(
328
T.pdl_trigger()
329
330
356
-@tilelang.jit(
357
- pass_configs=pass_configs,
358
-)
331
+@tilelang_jit
332
def mhc_pre_big_fuse_broadcast_with_norm_tilelang(
333
gemm_out_mul,
334
gemm_out_sqrsum,
@@ -517,9 +490,7 @@ def mhc_pre_big_fuse_broadcast_with_norm_tilelang(
490
T.pdl_trigger()
491
492
520
-@tilelang.jit(
521
- pass_configs=pass_configs,
522
-)
493
+@tilelang_jit
494
def mhc_fused_tilelang(
495
comb_mix,
496
residual_in,
@@ -640,9 +611,7 @@ def mhc_fused_tilelang(
611
T.pdl_trigger()
612
613
643
-@tilelang.jit(
644
- pass_configs=pass_configs,
645
-)
614
+@tilelang_jit
615
def mhc_post_tilelang(
616
a,
617
b,
@@ -695,9 +664,7 @@ def mhc_post_tilelang(
664
T.pdl_trigger()
665
666
698
-@tilelang.jit(
699
- pass_configs=pass_configs,
700
-)
667
+@tilelang_jit
668
def hc_prenorm_gemm_tilelang(
669
x,
670
fn,
@@ -781,9 +748,7 @@ def hc_prenorm_gemm_tilelang(
748
T.pdl_trigger()
749
750
784
-@tilelang.jit(
785
- pass_configs=pass_configs,
786
-)
751
+@tilelang_jit
752
def hc_prenorm_gemm_block_m_tilelang(
753
x,
754
fn,
@@ -878,9 +843,7 @@ def hc_prenorm_gemm_block_m_tilelang(
843
T.pdl_trigger()
844
845
881
-@tilelang.jit(
882
- pass_configs=pass_configs,
883
-)
846
+@tilelang_jit
847
def hc_head_fuse_tilelang(
848
residual,
849
fn,
vllm/tilelang_utils/__init__.py
new
+91
@@ -0,0 +1,91 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+from __future__ import annotations
5
+
6
+import functools
7
+from collections.abc import Callable
8
+from functools import cache
9
+from typing import TYPE_CHECKING, Any
10
+
11
+from vllm.platforms import current_platform
12
+from vllm.utils.import_utils import has_tilelang
13
+
14
+if TYPE_CHECKING or current_platform.is_cuda():
15
+ if not has_tilelang():
16
+ raise ImportError(
17
+ "tilelang is required for mhc but is not installed. Install it with "
18
+ "`pip install tilelang`."
19
+ )
20
+ import tilelang
21
+ import tilelang.language as T
22
+else:
23
+ tilelang = None # type: ignore[assignment]
24
+ T = None # type: ignore[assignment]
25
+
26
+
27
+def _ensure_tilelang_imported() -> None:
28
+ """Bind the `tilelang` and `T` module globals, importing them if needed.
29
+
30
+ On ROCm, this runs on the first kernel call instead of at import time.
31
+
32
+ Raises:
33
+ ImportError: If TileLang is not installed.
34
+ """
35
+ global T, tilelang
36
+
37
+ if tilelang is not None:
38
+ return
39
+ if not has_tilelang():
40
+ raise ImportError(
41
+ "tilelang is required for mhc but is not installed. Install it with "
42
+ "`pip install tilelang`."
43
+ )
44
+ import tilelang as tilelang_module
45
+ import tilelang.language as tilelang_language
46
+
47
+ tilelang = tilelang_module
48
+ T = tilelang_language
49
+
50
+
51
+@cache
52
+def _get_pass_configs() -> dict[Any, Any]:
53
+ _ensure_tilelang_imported()
54
+ pass_configs: dict[Any, Any] = {
55
+ tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
56
+ tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
57
+ }
58
+ if current_platform.is_cuda():
59
+ pass_configs[tilelang.PassConfigKey.TL_PTXAS_REGISTER_USAGE_LEVEL] = 10
60
+ return pass_configs
61
+
62
+
63
+def tilelang_jit(kernel_function: Callable[..., Any]) -> Callable[..., Any]:
64
+ """Apply `tilelang.jit`, deferring until first call on ROCm.
65
+
66
+ ROCm defers JIT decoration so importing the caller's module does not
67
+ require TileLang immediately. CUDA keeps the eager decoration behavior.
68
+
69
+ The kernel body parsed by TileLang references `T` as an unqualified
70
+ global, so on the deferred ROCm path this rebinds `T`/`tilelang` in the
71
+ decorated function's own module globals once they become available.
72
+ """
73
+ if not current_platform.is_rocm():
74
+ _ensure_tilelang_imported()
75
+ return tilelang.jit(pass_configs=_get_pass_configs())(kernel_function)
76
+
77
+ compiled_kernel: Callable[..., Any] | None = None
78
+
79
+ @functools.wraps(kernel_function)
80
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
81
+ nonlocal compiled_kernel
82
+ if compiled_kernel is None:
83
+ _ensure_tilelang_imported()
84
+ kernel_function.__globals__["tilelang"] = tilelang
85
+ kernel_function.__globals__["T"] = T
86
+ compiled_kernel = tilelang.jit(pass_configs=_get_pass_configs())(
87
+ kernel_function
88
+ )
89
+ return compiled_kernel(*args, **kwargs)
90
+
91
+ return wrapper
vllm/utils/import_utils.py
+22
-2
@@ -412,6 +412,21 @@ def _has_module(module_name: str) -> bool:
412
return True
413
414
415
+@cache
416
+def _has_module_spec(module_name: str) -> bool:
417
+ """Return True if *module_name* is installed, without importing it.
418
+
419
+ Unlike [`_has_module`][vllm.utils.import_utils._has_module], this only
420
+ resolves the import spec. It therefore does not pay the import cost of
421
+ heavyweight modules, at the price of not verifying that native
422
+ dependencies (shared libraries, etc.) are satisfied. The result is cached.
423
+ """
424
+ try:
425
+ return importlib.util.find_spec(module_name) is not None
426
+ except Exception:
427
+ return False
428
+
429
+
430
def has_deep_ep() -> bool:
431
"""Whether the optional `deep_ep` package is available."""
432
return _has_module("deep_ep")
@@ -504,8 +519,13 @@ def has_triton_kernels() -> bool:
519
520
@cache
521
def has_tilelang() -> bool:
507
- """Whether the optional `tilelang` package is available."""
508
- if not _has_module("tilelang"):
522
+ """Whether the optional `tilelang` package is available.
523
+
524
+ Only the import spec is checked: importing `tilelang` is expensive, so
525
+ callers must import it lazily at their point of use rather than relying
526
+ on this function to have imported it already.
527
+ """
528
+ if not _has_module_spec("tilelang"):
529
return False
530
# ROCm-only guard, imported lazily to avoid loading rocm on CUDA.
531
from vllm.platforms import current_platform
vllm/utils/jit_monitor.py
+7
-1
@@ -30,6 +30,7 @@ from contextlib import suppress
30
from typing import Any, Literal, cast
31
32
from vllm.logger import init_logger
33
+from vllm.platforms import current_platform
34
from vllm.triton_utils.importing import HAS_TRITON
35
36
logger = init_logger(__name__)
@@ -74,7 +75,12 @@ def activate(*, mode: JitMonitorMode = "warn", verbose: bool = False) -> None:
75
_setup_triton_autotuning_print()
76
_setup_triton_jit_hook()
77
_setup_cutedsl_jit_hook()
77
- _setup_tilelang_jit_hook()
78
+
79
+ # Refer to #51159. tilelang ships broken symbols
80
+ # on rocm.
81
+ # TODO: Remove the guard once tilelang upstream is fixed.
82
+ if not current_platform.is_rocm():
83
+ _setup_tilelang_jit_hook()
84
85
logger.info(
86
"Kernel JIT monitor activated; monitored JIT compilations during "