From a4d2651c21ad4c6749fc0b5683e007945e2278be Mon Sep 17 00:00:00 2001 From: Yihui Lu <269394165+YihuiLu512@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:44:21 +0800 Subject: [PATCH 1/4] [TRTLLM-15863][fix] Raise ImportError with the cause when a lazily imported submodule raises AttributeError An AttributeError raised while __getattr__ imports a submodule escaped as is, so hasattr() and getattr() with a default hid it, and from-import ran the submodule twice and, for non-module names, reported "cannot import name" without the cause. Re-raise it as ImportError chained to the original in tensorrt_llm, _torch.models, visual_gen, _torch.pyexecutor.sampler and _torch.visual_gen.modules, as #20606 did for _torch.attention, and add test_lazy_getattr_errors.py. Signed-off-by: Yihui Lu <269394165+YihuiLu512@users.noreply.github.com> --- tensorrt_llm/__init__.py | 18 ++- tensorrt_llm/_torch/models/__init__.py | 17 ++- .../_torch/pyexecutor/sampler/__init__.py | 19 ++- .../_torch/visual_gen/modules/__init__.py | 12 +- tensorrt_llm/visual_gen/__init__.py | 19 ++- .../others/test_lazy_getattr_errors.py | 134 ++++++++++++++++++ 6 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 tests/unittest/others/test_lazy_getattr_errors.py diff --git a/tensorrt_llm/__init__.py b/tensorrt_llm/__init__.py index e7c6b2b626d2..d783bb8ad856 100644 --- a/tensorrt_llm/__init__.py +++ b/tensorrt_llm/__init__.py @@ -110,7 +110,17 @@ def __getattr__(name): entry = _LAZY_ATTRS.get(name) if entry is not None: module_name, attr = entry - module = importlib.import_module(module_name) + try: + module = importlib.import_module(module_name) + except AttributeError as e: + # An AttributeError escaping the module is a bug inside it, not a + # missing name here. Raised as is, hasattr() and getattr() with a + # default would hide it, and ``from ... import`` would run the + # module twice (losing the cause unless the name is a submodule). + raise ImportError( + f"cannot import {name!r} from {__name__!r}: importing " + f"{module_name} raised {e!r}", + name=__name__) from e value = module if attr is None else getattr(module, attr) globals()[name] = value # cache: subsequent access skips __getattr__ return value @@ -126,6 +136,12 @@ def __getattr__(name): raise raise AttributeError( f"module {__name__!r} has no attribute {name!r}") from None + except AttributeError as e: + # Same as above: a bug inside the submodule, not a missing name. + raise ImportError( + f"cannot import {name!r} from {__name__!r}: importing " + f"{__name__}.{name} raised {e!r}", + name=__name__) from e def __dir__(): diff --git a/tensorrt_llm/_torch/models/__init__.py b/tensorrt_llm/_torch/models/__init__.py index 30b30c8d2f68..e862b19e1326 100644 --- a/tensorrt_llm/_torch/models/__init__.py +++ b/tensorrt_llm/_torch/models/__init__.py @@ -119,7 +119,22 @@ def __getattr__(name: str): raise raise AttributeError( f"module {__name__!r} has no attribute {name!r}") from None - module = importlib.import_module(f".{module_name}", __name__) + except AttributeError as e: + raise ImportError( + f"cannot import {name!r} from {__name__!r}: importing " + f"{__name__}.{name} raised {e!r}", + name=__name__) from e + try: + module = importlib.import_module(f".{module_name}", __name__) + except AttributeError as e: + # An AttributeError escaping the submodule is a bug inside it, not a + # missing name here. Raised as is, hasattr() and ``from ... import`` + # would swallow it, and from-import would run the submodule again, + # hiding the cause. + raise ImportError( + f"cannot import {name!r} from {__name__!r}: importing " + f"{__name__}.{module_name} raised {e!r}", + name=__name__) from e attr = getattr(module, name) globals()[name] = attr # cache: subsequent access skips __getattr__ return attr diff --git a/tensorrt_llm/_torch/pyexecutor/sampler/__init__.py b/tensorrt_llm/_torch/pyexecutor/sampler/__init__.py index 32779aae154a..fb7ce239eed3 100644 --- a/tensorrt_llm/_torch/pyexecutor/sampler/__init__.py +++ b/tensorrt_llm/_torch/pyexecutor/sampler/__init__.py @@ -68,11 +68,26 @@ _SUBMODULES = frozenset({"sampler", "sampler_common", "sampling_resolution", "ops"}) +def _import_submodule(name: str, submodule: str): + try: + return importlib.import_module(f"{__name__}.{submodule}") + except AttributeError as e: + # An AttributeError escaping the submodule is a bug inside it, not a + # missing name here. Raised as is, hasattr() and getattr() with a + # default would hide it, and ``from ... import`` would run the + # submodule twice (losing the cause unless the name is a submodule). + raise ImportError( + f"cannot import {name!r} from {__name__!r}: importing " + f"{__name__}.{submodule} raised {e!r}", + name=__name__, + ) from e + + def __getattr__(name: str): if name in _SUBMODULES: - return importlib.import_module(f"{__name__}.{name}") + return _import_submodule(name, name) # Lazily forward everything else to the sampler orchestration module. - _sampler = importlib.import_module(f"{__name__}.sampler") + _sampler = _import_submodule(name, "sampler") try: return getattr(_sampler, name) except AttributeError: diff --git a/tensorrt_llm/_torch/visual_gen/modules/__init__.py b/tensorrt_llm/_torch/visual_gen/modules/__init__.py index a3a58f9ad2ab..4624fa909cc3 100644 --- a/tensorrt_llm/_torch/visual_gen/modules/__init__.py +++ b/tensorrt_llm/_torch/visual_gen/modules/__init__.py @@ -34,7 +34,17 @@ def __getattr__(name): if name in __all__: - from ..attention import attention + try: + from ..attention import attention + except AttributeError as e: + # An AttributeError escaping ``attention`` is a bug inside it, not a + # missing name here. Raised as is, hasattr() and ``from ... import`` + # would swallow it, and from-import would run it again, hiding the cause. + raise ImportError( + f"cannot import {name!r} from {__name__!r}: importing " + f"tensorrt_llm._torch.visual_gen.attention.attention raised {e!r}", + name=__name__, + ) from e return getattr(attention, name) raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/tensorrt_llm/visual_gen/__init__.py b/tensorrt_llm/visual_gen/__init__.py index 3b1ef26b8d98..c2dfccc3a5b0 100644 --- a/tensorrt_llm/visual_gen/__init__.py +++ b/tensorrt_llm/visual_gen/__init__.py @@ -94,7 +94,18 @@ def __getattr__(name): module_name = _LAZY_ATTRS.get(name) if module_name is not None: - value = getattr(importlib.import_module(module_name), name) + try: + module = importlib.import_module(module_name) + except AttributeError as e: + # An AttributeError escaping the module is a bug inside it, not a + # missing name here. Raised as is, hasattr() and ``from ... import`` + # would swallow it, and from-import would run the module again, + # hiding the cause. + raise ImportError( + f"cannot import {name!r} from {__name__!r}: importing {module_name} raised {e!r}", + name=__name__, + ) from e + value = getattr(module, name) globals()[name] = value # cache: subsequent access skips __getattr__ return value # Fall back to plain submodules (tensorrt_llm.visual_gen.sparse_attention, @@ -108,6 +119,12 @@ def __getattr__(name): if e.name != f"{__name__}.{name}": raise raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None + except AttributeError as e: + # Same as above: a bug inside the submodule, not a missing name. + raise ImportError( + f"cannot import {name!r} from {__name__!r}: importing {__name__}.{name} raised {e!r}", + name=__name__, + ) from e def __dir__(): diff --git a/tests/unittest/others/test_lazy_getattr_errors.py b/tests/unittest/others/test_lazy_getattr_errors.py new file mode 100644 index 000000000000..0dc72447bba7 --- /dev/null +++ b/tests/unittest/others/test_lazy_getattr_errors.py @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Guards how the PEP 562 ``__getattr__`` of the lazily loaded packages reports a broken submodule. + +An ``AttributeError`` raised while a submodule is being imported is a bug inside that submodule. +``hasattr()``, ``getattr(..., default)`` and ``from package import name`` all treat an +``AttributeError`` from ``__getattr__`` as "no such name", so it must leave ``__getattr__`` as an +``ImportError`` chained to the original error. Otherwise ``hasattr`` returns False, +``getattr(..., default)`` returns the default, and ``from ... import`` imports the submodule a +second time and, for a name that is not itself a submodule, reports ``cannot import name`` +without the cause. + +The fault is injected with a ``sys.meta_path`` finder that raises when the target submodule is +imported, so it covers both ``importlib.import_module`` and ``from . import`` inside +``__getattr__``. +""" + +import importlib +import importlib.abc +import sys + +import pytest + +pytestmark = pytest.mark.cpu_only + +_MARKER = "injected-by-test_lazy_getattr_errors" + +# (package, name accessed on it, submodule that the access imports) +CASES = [ + ("tensorrt_llm", "AutoConfig", "tensorrt_llm.models.automodel"), + ("tensorrt_llm", "profiler", "tensorrt_llm.profiler"), + ( + "tensorrt_llm._torch.models", + "Starcoder2ForCausalLM", + "tensorrt_llm._torch.models.modeling_starcoder2", + ), + ( + "tensorrt_llm._torch.models", + "modeling_starcoder2", + "tensorrt_llm._torch.models.modeling_starcoder2", + ), + ("tensorrt_llm.visual_gen", "VisualGenParams", "tensorrt_llm.visual_gen.params"), + ("tensorrt_llm.visual_gen", "sparse_attention", "tensorrt_llm.visual_gen.sparse_attention"), + ( + "tensorrt_llm._torch.pyexecutor.sampler", + "TorchSampler", + "tensorrt_llm._torch.pyexecutor.sampler.sampler", + ), + ( + "tensorrt_llm._torch.pyexecutor.sampler", + "sampler_common", + "tensorrt_llm._torch.pyexecutor.sampler.sampler_common", + ), + ( + "tensorrt_llm._torch.visual_gen.modules", + "Attention", + "tensorrt_llm._torch.visual_gen.attention.attention", + ), +] +_IDS = [f"{pkg}.{name}" for pkg, name, _ in CASES] + + +class _RaiseOnImport(importlib.abc.MetaPathFinder): + def __init__(self, fullname: str): + self.fullname = fullname + self.attempts = 0 + + def find_spec(self, name, path=None, target=None): + if name != self.fullname: + return None + self.attempts += 1 + raise AttributeError(f"{_MARKER}: importing {name}") + + +@pytest.fixture +def broken_submodule(monkeypatch): + """Make importing ``target`` raise AttributeError until the test ends; return (package, finder).""" + + def install(pkg_name: str, name: str, target: str): + pkg = importlib.import_module(pkg_name) + parent_name, _, leaf = target.rpartition(".") + parent = importlib.import_module(parent_name) + finder = _RaiseOnImport(target) + monkeypatch.setattr(sys, "meta_path", [finder, *sys.meta_path]) + # Edit the namespaces directly: hasattr() (used by monkeypatch.delattr) would call the + # package's __getattr__ and re-import the target before the test starts. + monkeypatch.delitem(sys.modules, target, raising=False) + monkeypatch.delitem(vars(parent), leaf, raising=False) + monkeypatch.delitem(vars(pkg), name, raising=False) + assert target not in sys.modules and leaf not in vars(parent) and name not in vars(pkg) + assert finder.attempts == 0 + return pkg, finder + + return install + + +def _assert_chained(excinfo, finder): + assert isinstance(excinfo.value.__cause__, AttributeError), repr(excinfo.value) + assert _MARKER in str(excinfo.value.__cause__) + assert finder.attempts == 1 + + +@pytest.mark.parametrize("pkg_name,name,target", CASES, ids=_IDS) +def test_from_import_reports_cause_once(broken_submodule, pkg_name, name, target): + _, finder = broken_submodule(pkg_name, name, target) + with pytest.raises(ImportError) as excinfo: + exec(f"from {pkg_name} import {name}", {}) + _assert_chained(excinfo, finder) + + +@pytest.mark.parametrize("pkg_name,name,target", CASES, ids=_IDS) +@pytest.mark.parametrize("probe", ["attr", "hasattr", "getattr_default"]) +def test_probe_does_not_hide_broken_submodule(broken_submodule, probe, pkg_name, name, target): + pkg, finder = broken_submodule(pkg_name, name, target) + with pytest.raises(ImportError) as excinfo: + if probe == "attr": + getattr(pkg, name) + elif probe == "hasattr": + hasattr(pkg, name) + else: + getattr(pkg, name, None) + _assert_chained(excinfo, finder) From db8971667c0828f698b5f17e4f8fb8aa15bd31ea Mon Sep 17 00:00:00 2001 From: Yihui Lu <269394165+YihuiLu512@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:49:16 +0800 Subject: [PATCH 2/4] [TRTLLM-15863][fix] Order the _torch.models __getattr__ branches like the other lazy packages Handle MODEL_CLASS_TO_MODULE names first, with the full comment on the AttributeError re-raise, and the bare-submodule fallback second, with "Same as above", matching tensorrt_llm and visual_gen. The fallback branch had no comment, so the two identical re-raises looked different. Behavior is unchanged. Signed-off-by: Yihui Lu <269394165+YihuiLu512@users.noreply.github.com> --- tensorrt_llm/_torch/models/__init__.py | 47 +++++++++++++------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/tensorrt_llm/_torch/models/__init__.py b/tensorrt_llm/_torch/models/__init__.py index e862b19e1326..87ea4c400e35 100644 --- a/tensorrt_llm/_torch/models/__init__.py +++ b/tensorrt_llm/_torch/models/__init__.py @@ -105,39 +105,40 @@ def __getattr__(name: str): module_name = MODEL_CLASS_TO_MODULE.get(name) - if module_name is None: - # Also resolve bare submodule access (models.modeling_llama, - # models.checkpoints, models.hf_parameter_utils, ...) so callers that - # relied on the previously-eager submodule attributes keep working. + if module_name is not None: try: - return importlib.import_module(f".{name}", __name__) - except ModuleNotFoundError as e: - # Only translate "no such submodule" into AttributeError; a - # ModuleNotFoundError raised *inside* an existing submodule - # (missing dependency) must propagate unchanged. - if e.name != f"{__name__}.{name}": - raise - raise AttributeError( - f"module {__name__!r} has no attribute {name!r}") from None + module = importlib.import_module(f".{module_name}", __name__) except AttributeError as e: + # An AttributeError escaping the submodule is a bug inside it, not + # a missing name here. Raised as is, hasattr() and + # ``from ... import`` would swallow it, and from-import would run + # the submodule again, hiding the cause. raise ImportError( f"cannot import {name!r} from {__name__!r}: importing " - f"{__name__}.{name} raised {e!r}", + f"{__name__}.{module_name} raised {e!r}", name=__name__) from e + attr = getattr(module, name) + globals()[name] = attr # cache: subsequent access skips __getattr__ + return attr + # Also resolve bare submodule access (models.modeling_llama, + # models.checkpoints, models.hf_parameter_utils, ...) so callers that + # relied on the previously-eager submodule attributes keep working. try: - module = importlib.import_module(f".{module_name}", __name__) + return importlib.import_module(f".{name}", __name__) + except ModuleNotFoundError as e: + # Only translate "no such submodule" into AttributeError; a + # ModuleNotFoundError raised *inside* an existing submodule + # (missing dependency) must propagate unchanged. + if e.name != f"{__name__}.{name}": + raise + raise AttributeError( + f"module {__name__!r} has no attribute {name!r}") from None except AttributeError as e: - # An AttributeError escaping the submodule is a bug inside it, not a - # missing name here. Raised as is, hasattr() and ``from ... import`` - # would swallow it, and from-import would run the submodule again, - # hiding the cause. + # Same as above: a bug inside the submodule, not a missing name. raise ImportError( f"cannot import {name!r} from {__name__!r}: importing " - f"{__name__}.{module_name} raised {e!r}", + f"{__name__}.{name} raised {e!r}", name=__name__) from e - attr = getattr(module, name) - globals()[name] = attr # cache: subsequent access skips __getattr__ - return attr def __dir__(): From aa9c86c8afc474e7466171598126827d2a81613f Mon Sep 17 00:00:00 2001 From: Yihui Lu <269394165+YihuiLu512@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:06:05 +0800 Subject: [PATCH 3/4] [TRTLLM-15863][fix] Use the top-level comment wording in _torch.models and visual_gen __getattr__ The full comment above the "Same as above" fallback said from-import hides the cause, which does not hold when the name is a submodule: from-import then keeps the cause and only runs the submodule twice. Use the qualified wording from tensorrt_llm/__init__.py so the comment is right for both branches and the three packages read the same. Comments only. Signed-off-by: Yihui Lu <269394165+YihuiLu512@users.noreply.github.com> --- tensorrt_llm/_torch/models/__init__.py | 8 ++++---- tensorrt_llm/visual_gen/__init__.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tensorrt_llm/_torch/models/__init__.py b/tensorrt_llm/_torch/models/__init__.py index 87ea4c400e35..a9976df81177 100644 --- a/tensorrt_llm/_torch/models/__init__.py +++ b/tensorrt_llm/_torch/models/__init__.py @@ -109,10 +109,10 @@ def __getattr__(name: str): try: module = importlib.import_module(f".{module_name}", __name__) except AttributeError as e: - # An AttributeError escaping the submodule is a bug inside it, not - # a missing name here. Raised as is, hasattr() and - # ``from ... import`` would swallow it, and from-import would run - # the submodule again, hiding the cause. + # An AttributeError escaping the module is a bug inside it, not a + # missing name here. Raised as is, hasattr() and getattr() with a + # default would hide it, and ``from ... import`` would run the + # module twice (losing the cause unless the name is a submodule). raise ImportError( f"cannot import {name!r} from {__name__!r}: importing " f"{__name__}.{module_name} raised {e!r}", diff --git a/tensorrt_llm/visual_gen/__init__.py b/tensorrt_llm/visual_gen/__init__.py index c2dfccc3a5b0..a6181fee6cb4 100644 --- a/tensorrt_llm/visual_gen/__init__.py +++ b/tensorrt_llm/visual_gen/__init__.py @@ -98,9 +98,9 @@ def __getattr__(name): module = importlib.import_module(module_name) except AttributeError as e: # An AttributeError escaping the module is a bug inside it, not a - # missing name here. Raised as is, hasattr() and ``from ... import`` - # would swallow it, and from-import would run the module again, - # hiding the cause. + # missing name here. Raised as is, hasattr() and getattr() with a + # default would hide it, and ``from ... import`` would run the + # module twice (losing the cause unless the name is a submodule). raise ImportError( f"cannot import {name!r} from {__name__!r}: importing {module_name} raised {e!r}", name=__name__, From 1e8c9627ca6d8e38dba8a815f239f25b898fdcd7 Mon Sep 17 00:00:00 2001 From: Yihui Lu <269394165+YihuiLu512@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:02:14 +0800 Subject: [PATCH 4/4] [TRTLLM-15863][fix] Re-raise an AttributeError-caused ImportError from the deep_ep / deep_gemm import guards The lazy __getattr__ now turns an AttributeError raised while importing a submodule into an ImportError chained to it. The `except ImportError` guards around `from tensorrt_llm import deep_ep` (deep_ep_utils) and `from tensorrt_llm import deep_gemm` (_import_deep_gemm and test_dsa_fp4_indexer) would take that for a missing module; re-raise when the cause is an AttributeError. The two deep_gemm guards are not reached in a normal run, because `import tensorrt_llm` already imports deep_gemm without a guard; they follow the same rule for consistency. Also use the shared lazy __getattr__ comment in _torch.attention and _torch.visual_gen.modules. Signed-off-by: Yihui Lu <269394165+YihuiLu512@users.noreply.github.com> --- tensorrt_llm/_torch/attention/__init__.py | 6 +++--- tensorrt_llm/_torch/moe/fused_moe/deep_ep_utils.py | 7 ++++++- tensorrt_llm/_torch/moe/fused_moe/quantization.py | 5 +++++ tensorrt_llm/_torch/visual_gen/modules/__init__.py | 7 ++++--- .../_torch/attention/sparse/dsa/test_dsa_fp4_indexer.py | 9 ++++++--- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/tensorrt_llm/_torch/attention/__init__.py b/tensorrt_llm/_torch/attention/__init__.py index 20226c35c596..a90996fc4daa 100644 --- a/tensorrt_llm/_torch/attention/__init__.py +++ b/tensorrt_llm/_torch/attention/__init__.py @@ -81,9 +81,9 @@ def __getattr__(name: str): module = importlib.import_module(f".{module_name}", __name__) except AttributeError as e: # An AttributeError escaping the submodule is a bug inside it, not a - # missing name here. Raised as is, hasattr() and ``from ... import`` - # would swallow it, and from-import would run the submodule again, - # hiding the cause. + # missing name here. Raised as is, hasattr() and getattr() with a + # default would hide it, and ``from ... import`` would run the + # submodule twice (losing the cause unless the name is a submodule). raise ImportError( f"cannot import {name!r} from {__name__!r}: importing " f"{__name__}.{module_name} raised {e!r}", diff --git a/tensorrt_llm/_torch/moe/fused_moe/deep_ep_utils.py b/tensorrt_llm/_torch/moe/fused_moe/deep_ep_utils.py index 702b1c3de7a9..63779c610ee9 100644 --- a/tensorrt_llm/_torch/moe/fused_moe/deep_ep_utils.py +++ b/tensorrt_llm/_torch/moe/fused_moe/deep_ep_utils.py @@ -15,7 +15,12 @@ from tensorrt_llm import deep_ep from tensorrt_llm.deep_ep import Buffer deep_ep_installed = True -except ImportError: +except ImportError as e: + # An ImportError chained to an AttributeError (from + # tensorrt_llm.__getattr__) means deep_ep raised while importing; surface + # it instead of silently disabling DeepEP. + if isinstance(e.__cause__, AttributeError): + raise deep_ep_installed = False diff --git a/tensorrt_llm/_torch/moe/fused_moe/quantization.py b/tensorrt_llm/_torch/moe/fused_moe/quantization.py index 021abd760120..9efe3a72ad1b 100644 --- a/tensorrt_llm/_torch/moe/fused_moe/quantization.py +++ b/tensorrt_llm/_torch/moe/fused_moe/quantization.py @@ -7248,6 +7248,11 @@ def _import_deep_gemm(): try: from tensorrt_llm import deep_gemm as _dg except ImportError as e: + # An ImportError chained to an AttributeError (from + # tensorrt_llm.__getattr__) means deep_gemm raised while importing; + # surface it instead of turning it into _MegaMoEUnavailable. + if isinstance(e.__cause__, AttributeError): + raise raise _MegaMoEUnavailable( f"tensorrt_llm.deep_gemm not importable: {e}") from e diff --git a/tensorrt_llm/_torch/visual_gen/modules/__init__.py b/tensorrt_llm/_torch/visual_gen/modules/__init__.py index 4624fa909cc3..e65edbe989d4 100644 --- a/tensorrt_llm/_torch/visual_gen/modules/__init__.py +++ b/tensorrt_llm/_torch/visual_gen/modules/__init__.py @@ -37,9 +37,10 @@ def __getattr__(name): try: from ..attention import attention except AttributeError as e: - # An AttributeError escaping ``attention`` is a bug inside it, not a - # missing name here. Raised as is, hasattr() and ``from ... import`` - # would swallow it, and from-import would run it again, hiding the cause. + # An AttributeError escaping the module is a bug inside it, not a + # missing name here. Raised as is, hasattr() and getattr() with a + # default would hide it, and ``from ... import`` would run the + # module twice (losing the cause unless the name is a submodule). raise ImportError( f"cannot import {name!r} from {__name__!r}: importing " f"tensorrt_llm._torch.visual_gen.attention.attention raised {e!r}", diff --git a/tests/unittest/_torch/attention/sparse/dsa/test_dsa_fp4_indexer.py b/tests/unittest/_torch/attention/sparse/dsa/test_dsa_fp4_indexer.py index 31fc078f3f80..be5157a1608a 100644 --- a/tests/unittest/_torch/attention/sparse/dsa/test_dsa_fp4_indexer.py +++ b/tests/unittest/_torch/attention/sparse/dsa/test_dsa_fp4_indexer.py @@ -28,9 +28,12 @@ try: from tensorrt_llm import deep_gemm -except ImportError: - # Only skip on actual module-missing failures — any other error should - # surface rather than silently turn into a test-wide skip. +except ImportError as e: + # An ImportError chained to an AttributeError (from + # tensorrt_llm.__getattr__) means deep_gemm raised while importing; surface + # it instead of skipping the tests gated on HAS_DEEP_GEMM. + if isinstance(e.__cause__, AttributeError): + raise HAS_DEEP_GEMM = False else: HAS_DEEP_GEMM = hasattr(deep_gemm, "fp8_fp4_mqa_logits")