diff --git a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py index 2b0761be1a6..e4a101e163d 100644 --- a/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py +++ b/cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py @@ -35,6 +35,7 @@ parse_dynamic_lib_subprocess_payload, ) from cuda.pathfinder._dynamic_libs.supported_nvidia_libs import ALL_AVAILABLE_LIBNAMES +from cuda.pathfinder._utils.diagnostic_log import LOGGER from cuda.pathfinder._utils.platform_aware import IS_WINDOWS if TYPE_CHECKING: @@ -312,6 +313,9 @@ def load_nvidia_dynamic_lib(libname: str) -> LoadedDL: The search is performed **per library**. There is currently no mechanism to guarantee that multiple libraries are all resolved from the same location. + Set ``CUDA_PATHFINDER_LOG_LEVEL=INFO`` to log where each library was + found (``abs_path`` and ``found_via``) to the ``cuda.pathfinder`` logger. + """ pointer_size_bits = struct.calcsize("P") * 8 if pointer_size_bits != 64: @@ -327,4 +331,7 @@ def load_nvidia_dynamic_lib(libname: str) -> LoadedDL: f"Library name {libname!r} is known but not available on {_PLATFORM_NAME}. " f"Supported names on {_PLATFORM_NAME}: {sorted(ALL_AVAILABLE_LIBNAMES)}" ) - return _load_lib_no_cache(libname) + loaded = _load_lib_no_cache(libname) + if LOGGER is not None: + LOGGER.info("loaded %s from %s (found via %s)", libname, loaded.abs_path, loaded.found_via) + return loaded diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/diagnostic_log.py b/cuda_pathfinder/cuda/pathfinder/_utils/diagnostic_log.py new file mode 100644 index 00000000000..7c9a362a338 --- /dev/null +++ b/cuda_pathfinder/cuda/pathfinder/_utils/diagnostic_log.py @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Opt-in logging of where :func:`load_nvidia_dynamic_lib` found each library. + +Disabled by default. Set ``CUDA_PATHFINDER_LOG_LEVEL`` to a level name (e.g. +``INFO``) or number to enable it. ``logging`` is only imported when the variable +is set, and the root logger is never configured. +""" + +from __future__ import annotations + +import os +import warnings +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import logging + +ENV_VAR_NAME = "CUDA_PATHFINDER_LOG_LEVEL" +LOGGER_NAME = "cuda.pathfinder" +_LEVEL_NAMES = ("CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG") + + +def _make_logger() -> logging.Logger | None: + raw = os.environ.get(ENV_VAR_NAME, "").strip() + if not raw: + return None + + import logging + + name = raw.upper() + level = int(raw) if raw.isdecimal() else getattr(logging, name) if name in _LEVEL_NAMES else None + if level is None: + warnings.warn(f"{ENV_VAR_NAME}={raw!r} is not a valid logging level; logging stays disabled.", stacklevel=2) + return None + + logger = logging.getLogger(LOGGER_NAME) + logger.addHandler(logging.NullHandler()) + logger.setLevel(level) + return logger + + +#: ``None`` when logging is disabled, so call sites can skip all work. +LOGGER: logging.Logger | None = _make_logger() diff --git a/cuda_pathfinder/tests/test_diagnostic_log.py b/cuda_pathfinder/tests/test_diagnostic_log.py new file mode 100644 index 00000000000..58ee11cd4af --- /dev/null +++ b/cuda_pathfinder/tests/test_diagnostic_log.py @@ -0,0 +1,127 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for opt-in logging in load_nvidia_dynamic_lib. No GPU or NVIDIA library needed.""" + +import importlib +import logging +import os +from unittest.mock import patch + +import pytest + +from cuda.pathfinder._dynamic_libs import load_nvidia_dynamic_lib as mod +from cuda.pathfinder._dynamic_libs.load_dl_common import DynamicLibNotFoundError, LoadedDL +from cuda.pathfinder._utils import diagnostic_log + +pytestmark = pytest.mark.thread_unsafe(reason="reloads diagnostic_log under a patched os.environ") + +ENV_VAR = diagnostic_log.ENV_VAR_NAME +LOGGER_NAME = diagnostic_log.LOGGER_NAME +LOADED = LoadedDL("/opt/cuda/lib64/libcudart.so.12", False, 1234, "CUDA_PATH") + + +def reload_with_env(value): + """Reimport diagnostic_log with ENV_VAR set to *value* (None to unset).""" + env = {k: v for k, v in os.environ.items() if k != ENV_VAR} + if value is not None: + env[ENV_VAR] = value + with patch.dict(os.environ, env, clear=True): + return importlib.reload(diagnostic_log) + + +@pytest.fixture(autouse=True) +def restore_diagnostic_log(): + logger = logging.getLogger(LOGGER_NAME) + saved = (logger.level, list(logger.handlers)) + yield + logger.setLevel(saved[0]) + logger.handlers[:] = saved[1] + reload_with_env(None) + + +@pytest.mark.parametrize("value", [None, "", " "]) +@pytest.mark.agent_authored(model="claude-opus-5") +def test_disabled_by_default(value): + assert reload_with_env(value).LOGGER is None + + +@pytest.mark.parametrize(("value", "level"), [("INFO", logging.INFO), (" debug ", logging.DEBUG), ("10", 10)]) +@pytest.mark.agent_authored(model="claude-opus-5") +def test_env_var_enables_logger(value, level): + logger = reload_with_env(value).LOGGER + assert logger is not None + assert logger.name == LOGGER_NAME + assert logger.level == level + assert all(isinstance(h, logging.NullHandler) for h in logger.handlers) + + +@pytest.mark.parametrize("value", ["VERBOSE", "\u00b2"]) # superscript two: isdigit() but not int()-parsable +@pytest.mark.agent_authored(model="claude-opus-5") +def test_invalid_value_warns_and_stays_disabled(value): + with pytest.warns(UserWarning, match=ENV_VAR): + assert reload_with_env(value).LOGGER is None + + +@pytest.mark.agent_authored(model="claude-opus-5") +def test_does_not_configure_root_logger(): + root = logging.getLogger() + before = (root.level, list(root.handlers)) + reload_with_env("DEBUG") + assert (root.level, root.handlers) == before + + +@pytest.mark.agent_authored(model="claude-opus-5") +def test_logging_not_imported_at_module_scope(): + with open(diagnostic_log.__file__) as f: + lines = f.read().splitlines() + assert not [line for line in lines if line.startswith(("import logging", "from logging"))] + + +@pytest.fixture +def load_cudart(monkeypatch): + """Call load_nvidia_dynamic_lib('cudart') with the search stubbed out.""" + + def call(logger, result=LOADED): + monkeypatch.setattr(mod, "LOGGER", logger) + + def fake_load(_libname): + if isinstance(result, Exception): + raise result + return result + + monkeypatch.setattr(mod, "_load_lib_no_cache", fake_load) + mod.load_nvidia_dynamic_lib.cache_clear() + try: + return mod.load_nvidia_dynamic_lib("cudart") + finally: + mod.load_nvidia_dynamic_lib.cache_clear() + + return call + + +@pytest.mark.agent_authored(model="claude-opus-5") +def test_successful_load_logs_one_info_record(load_cudart, caplog): + logger = reload_with_env("INFO").LOGGER + with caplog.at_level(logging.INFO, logger=LOGGER_NAME): + assert load_cudart(logger) is LOADED + + records = [r for r in caplog.records if r.name == LOGGER_NAME] + assert len(records) == 1 + assert records[0].levelno == logging.INFO + assert records[0].getMessage() == "loaded cudart from /opt/cuda/lib64/libcudart.so.12 (found via CUDA_PATH)" + + +@pytest.mark.agent_authored(model="claude-opus-5") +def test_failed_load_logs_nothing(load_cudart, caplog): + logger = reload_with_env("DEBUG").LOGGER + with caplog.at_level(logging.DEBUG, logger=LOGGER_NAME), pytest.raises(DynamicLibNotFoundError): + load_cudart(logger, DynamicLibNotFoundError("not found")) + assert [r for r in caplog.records if r.name == LOGGER_NAME] == [] + + +@pytest.mark.agent_authored(model="claude-opus-5") +def test_silent_when_disabled(load_cudart, caplog): + with caplog.at_level(logging.DEBUG, logger=LOGGER_NAME): + assert load_cudart(None) is LOADED + assert [r for r in caplog.records if r.name == LOGGER_NAME] == []