From 645c86a3c876d5f2cc7f5b52ce348a1c08ab43ed Mon Sep 17 00:00:00 2001 From: Jinfeng Date: Fri, 25 Sep 2026 07:06:16 +0000 Subject: [PATCH 01/12] feat(cuda.core): bridge CUDA driver logs into Python logging Add register_cuda_log_bridge()/unregister_cuda_log_bridge() to cuda.core.utils, forwarding cuLogs* driver logs to a Python logging.Logger via cuLogsRegisterCallback/cuLogsUnregisterCallback. Refs: https://github.com/NVIDIA/cuda-python/issues/671 --- cuda_core/cuda/core/utils/__init__.py | 6 ++ cuda_core/cuda/core/utils/_logging.py | 134 ++++++++++++++++++++++++ cuda_core/tests/test_cuda_log_bridge.py | 90 ++++++++++++++++ 3 files changed, 230 insertions(+) create mode 100644 cuda_core/cuda/core/utils/_logging.py create mode 100644 cuda_core/tests/test_cuda_log_bridge.py diff --git a/cuda_core/cuda/core/utils/__init__.py b/cuda_core/cuda/core/utils/__init__.py index bc0a38f2b40..e60ada08ae5 100644 --- a/cuda_core/cuda/core/utils/__init__.py +++ b/cuda_core/cuda/core/utils/__init__.py @@ -17,6 +17,10 @@ StridedMemoryView, args_viewable_as_strided_memory, ) +from cuda.core.utils._logging import ( + register_cuda_log_bridge, + unregister_cuda_log_bridge, +) from cuda.core.utils._program_cache import ( FileStreamProgramCache, InMemoryProgramCache, @@ -38,4 +42,6 @@ "discard_prefetch_batch", "make_program_cache_key", "prefetch_batch", + "register_cuda_log_bridge", + "unregister_cuda_log_bridge", ] diff --git a/cuda_core/cuda/core/utils/_logging.py b/cuda_core/cuda/core/utils/_logging.py new file mode 100644 index 00000000000..8f3261f2828 --- /dev/null +++ b/cuda_core/cuda/core/utils/_logging.py @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Bridge the CUDA driver's ``cuLogs*`` error-log-management callback into +the standard :mod:`logging` module. + +The CUDA driver (CUDA 12.9+) maintains an internal, human-readable error/ +warning log stream (see the *Error Log Management* section of the CUDA +driver API docs) that is otherwise only observable via the ``CUDA_LOG_FILE`` +environment variable or stderr. This module lets a caller route that stream +into any :class:`logging.Logger`, so it can be filtered, captured (e.g. via +pytest's ``caplog``), and forwarded using ordinary Python logging tooling. + +xref: https://github.com/NVIDIA/cuda-python/issues/671 +""" + +from __future__ import annotations + +import ctypes +import logging +import threading + +from cuda.bindings import driver as _driver +from cuda.core._utils.cuda_utils import handle_return + +__all__ = ["register_cuda_log_bridge", "unregister_cuda_log_bridge"] + +# C callback signature expected by cuLogsRegisterCallback: +# void callback(void *userData, CUlogLevel logLevel, char *message, size_t length) +# +# Note: cuLogsRegisterCallback requires an actual C function pointer. The +# cuda.bindings wrapper does *not* accept a plain Python callable directly +# (it only ever converts the `callbackFunc` argument to an integer address), +# so we build a real function pointer via ctypes and pass its address. +_CUlogsCallback_functype = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_size_t) + +# CUlogLevel currently only defines two levels (CUDA 12.9 cuLogs* API). +# Anything unrecognized is conservatively mapped to ERROR. +_CUDA_LEVEL_TO_PY_LEVEL = { + int(_driver.CUlogLevel.CU_LOG_LEVEL_ERROR): logging.ERROR, + int(_driver.CUlogLevel.CU_LOG_LEVEL_WARNING): logging.WARNING, +} + +_lock = threading.Lock() +# Must hold a reference to the ctypes callback for as long as it is +# registered: once it is garbage collected, the driver would be left +# holding a dangling function pointer and crash the process on the next +# log message. +_c_callback: ctypes._FuncPointer | None = None +_callback_handle = None # CUlogsCallbackHandle, needed to unregister + + +def _make_c_callback(logger: logging.Logger) -> ctypes._FuncPointer: + """Build a ctypes C function pointer that forwards each driver log + record to ``logger``. + """ + + def _on_log(_user_data, log_level, message_ptr, length): + # This runs inside a ctypes callback trampoline, potentially from a + # CUDA-driver-internal thread. It must never raise or block. + try: + raw = ctypes.string_at(message_ptr, length) if message_ptr else b"" + msg = raw.decode("utf-8", errors="replace").rstrip("\n") + py_level = _CUDA_LEVEL_TO_PY_LEVEL.get(log_level, logging.ERROR) + logger.log(py_level, "[CUDA Driver] %s", msg) + except Exception: + logger.exception("cuda_log_bridge: error while forwarding driver log") + + return _CUlogsCallback_functype(_on_log) + + +def register_cuda_log_bridge(logger: logging.Logger | None = None) -> None: + """Register a bridge that forwards CUDA driver ``cuLogs*`` messages to + a Python :class:`logging.Logger`. + + This is a process-wide, idempotent operation: the driver only supports + a global log stream (not scoped to a context/stream/thread), so calling + this again while a bridge is already registered is a no-op -- the + logger passed to the first call remains in effect. + + Parameters + ---------- + logger : logging.Logger, optional + The logger that driver messages should be forwarded to. Defaults + to ``logging.getLogger("cuda.driver")``. + + Raises + ------ + cuda.core._utils.cuda_utils.CUDAError + If the underlying ``cuLogsRegisterCallback`` call fails, e.g. + because the loaded CUDA driver predates the 12.9 cuLogs* API. + """ + global _c_callback, _callback_handle + + if logger is None: + logger = logging.getLogger("cuda.driver") + + with _lock: + if _callback_handle is not None: + return # already registered; idempotent + + c_callback = _make_c_callback(logger) + addr = ctypes.cast(c_callback, ctypes.c_void_p).value + + handle = handle_return(_driver.cuLogsRegisterCallback(addr, None)) + + # Keep the ctypes callback object alive for as long as it is + # registered (see the module-level comment on _c_callback above). + _c_callback = c_callback + _callback_handle = handle + + +def unregister_cuda_log_bridge() -> None: + """Unregister the bridge previously installed by + :func:`register_cuda_log_bridge`. + + Idempotent: safe to call even if no bridge is currently registered. + + Raises + ------ + cuda.core._utils.cuda_utils.CUDAError + If the underlying ``cuLogsUnregisterCallback`` call fails. + """ + global _c_callback, _callback_handle + + with _lock: + if _callback_handle is None: + return # nothing registered; idempotent + + handle_return(_driver.cuLogsUnregisterCallback(_callback_handle)) + + _c_callback = None + _callback_handle = None diff --git a/cuda_core/tests/test_cuda_log_bridge.py b/cuda_core/tests/test_cuda_log_bridge.py new file mode 100644 index 00000000000..9bd0eb96176 --- /dev/null +++ b/cuda_core/tests/test_cuda_log_bridge.py @@ -0,0 +1,90 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for bridging CUDA driver ``cuLogs*`` messages into Python +``logging`` (see design doc / GitHub issue #671). +""" + +import logging + +import pytest + +from cuda.bindings import driver +from cuda.core.utils import register_cuda_log_bridge, unregister_cuda_log_bridge + + +@pytest.fixture(autouse=True) +def teardown_bridge(): + yield + # Guarantee the callback is unregistered after every test, so a leaked + # bridge/slot from one test can't affect later ones. + unregister_cuda_log_bridge() + + +# 使用方式1: 调用 register(logger), 通过 caplog 捕获 driver log +def test_cuda_driver_log_captured_via_caplog(caplog): + # 1. 初始化 CUDA Driver + driver.cuInit(0) + + # 2. 注册桥接,将驱动日志路由到用户提供的 logger + logger_name = "cuda.driver.caplog_test" + logger_obj = logging.getLogger(logger_name) + register_cuda_log_bridge(logger_obj) + + # 3. 触发驱动错误:ordinal 9999 必然越界,驱动会写入两条 [E] 日志 + with caplog.at_level(logging.ERROR, logger=logger_name): + driver.cuDeviceGet(9999) + + # 4. 验证 caplog 中包含两条驱动错误日志 + messages = [r.message for r in caplog.records if r.name == logger_name] + + assert any("Parameter ordinal must be between 0 and 1" in m for m in messages), ( + f"Expected ordinal error in caplog, got:\n{messages}" + ) + + assert any("CUDA_ERROR_INVALID_DEVICE" in m and "cuDeviceGet" in m for m in messages), ( + f"Expected cuDeviceGet return error in caplog, got:\n{messages}" + ) + + +# 使用方式2: 通过 logging handler 把 driver log 倒入到文件 +def test_cuda_driver_log_written_to_file(tmp_path): + # 1. 初始化 CUDA Driver + driver.cuInit(0) + + # 2. 准备一个独立的 logger,挂上写文件的 FileHandler + # (用独立 logger name,避免污染 root logger / 影响其他用例) + log_file = tmp_path / "cuda_driver.log" + logger_name = "cuda.driver.file_test" + logger_obj = logging.getLogger(logger_name) + logger_obj.setLevel(logging.WARNING) + + file_handler = logging.FileHandler(log_file, encoding="utf-8") + file_handler.setLevel(logging.WARNING) + file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")) + logger_obj.addHandler(file_handler) + + try: + # 3. 注册桥接,将驱动日志路由到该 logger + register_cuda_log_bridge(logger_obj) + + # 4. 触发驱动错误:ordinal 9999 必然越界,驱动会写入两条 [E] 日志 + driver.cuDeviceGet(9999) + finally: + # 5. 显式移除并关闭 handler: + # - close() 会 flush 缓冲区,确保日志落盘再读取 + # - removeHandler 避免 handler 残留导致文件句柄泄漏 / 影响后续用例 + logger_obj.removeHandler(file_handler) + file_handler.close() + + # 6. 校验文件内容 + content = log_file.read_text(encoding="utf-8") + + assert "Parameter ordinal must be between 0 and 1" in content, ( + f"Expected ordinal error in log file, got:\n{content}" + ) + + assert "CUDA_ERROR_INVALID_DEVICE" in content and "cuDeviceGet" in content, ( + f"Expected cuDeviceGet return error in log file, got:\n{content}" + ) From 916247df49d423953420c1ee17b6fdb451862160 Mon Sep 17 00:00:00 2001 From: Jinfeng Date: Wed, 30 Sep 2026 19:52:09 +0000 Subject: [PATCH 02/12] test(cuda.core): cover rebind, level filtering, and alert counting Refs: https://github.com/NVIDIA/cuda-python/issues/671 --- cuda_core/tests/test_cuda_log_bridge.py | 68 +++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/cuda_core/tests/test_cuda_log_bridge.py b/cuda_core/tests/test_cuda_log_bridge.py index 9bd0eb96176..13226957871 100644 --- a/cuda_core/tests/test_cuda_log_bridge.py +++ b/cuda_core/tests/test_cuda_log_bridge.py @@ -88,3 +88,71 @@ def test_cuda_driver_log_written_to_file(tmp_path): assert "CUDA_ERROR_INVALID_DEVICE" in content and "cuDeviceGet" in content, ( f"Expected cuDeviceGet return error in log file, got:\n{content}" ) + + +# 使用方式3: 动态绑定解绑 register -> unregister -> 再register +def test_cuda_driver_log_rebind_logger(caplog): + driver.cuInit(0) + + name_a, name_b = "cuda.driver.rebind_a", "cuda.driver.rebind_b" + + # 先绑定 logger_a,确认能收到日志 + register_cuda_log_bridge(logging.getLogger(name_a)) + with caplog.at_level(logging.ERROR, logger=name_a): + driver.cuDeviceGet(9999) + assert any(r.name == name_a for r in caplog.records) + + # 换绑到 logger_b:必须先 unregister 释放驱动侧 slot,再用新 logger register + unregister_cuda_log_bridge() + caplog.clear() + register_cuda_log_bridge(logging.getLogger(name_b)) + with caplog.at_level(logging.ERROR, logger=name_b): + driver.cuDeviceGet(9999) + + # 旧 logger 不应再收到任何消息,新 logger 应正常收到 + assert not any(r.name == name_a for r in caplog.records) + assert any(r.name == name_b for r in caplog.records) + + +# 使用方式4: 日志过滤 —— logger 的 level 阈值对驱动日志同样生效 +def test_cuda_driver_log_level_filtering(caplog): + driver.cuInit(0) + + logger_name = "cuda.driver.level_test" + register_cuda_log_bridge(logging.getLogger(logger_name)) + + # 阈值设为 CRITICAL:ERROR 级别的驱动日志应被过滤,caplog 收不到任何记录 + with caplog.at_level(logging.CRITICAL, logger=logger_name): + driver.cuDeviceGet(9999) + assert not any(r.name == logger_name for r in caplog.records) + + # 放低阈值到 ERROR:同样的错误应该能被捕获到 + with caplog.at_level(logging.ERROR, logger=logger_name): + driver.cuDeviceGet(9999) + assert any(r.name == logger_name for r in caplog.records) + + +# 使用方式5: 监控/告警集成 —— 用自定义 Handler 统计驱动错误次数, +# 可据此接入 Prometheus/Datadog 等监控系统做告警 +def test_cuda_driver_log_alert_counter(): + driver.cuInit(0) + + class ErrorCounterHandler(logging.Handler): + def __init__(self): + super().__init__(level=logging.ERROR) + self.count = 0 + + def emit(self, record): + self.count += 1 + + logger_obj = logging.getLogger("cuda.driver.alert_test") + counter_handler = ErrorCounterHandler() + logger_obj.addHandler(counter_handler) + + try: + register_cuda_log_bridge(logger_obj) + driver.cuDeviceGet(9999) + finally: + logger_obj.removeHandler(counter_handler) + + assert counter_handler.count >= 1, "Expected at least one ERROR-level driver log to be counted" From a84e6c0c1ba1ae41686f4b8cea27326485e32c0b Mon Sep 17 00:00:00 2001 From: Jinfeng Date: Tue, 6 Oct 2026 00:19:57 +0000 Subject: [PATCH 03/12] feat(cuda.core): add callback param to register_cuda_log_bridge register_cuda_log_bridge() now accepts an optional callback(logger, level, message) that overrides the default "[CUDA Driver] %s" forwarding. Non-callable callbacks raise TypeError. Add tests for callback override, level filtering, and validation. Refs: https://github.com/NVIDIA/cuda-python/issues/671 --- cuda_core/cuda/core/utils/_logging.py | 49 +++++++++++++++++++--- cuda_core/tests/test_cuda_log_bridge.py | 55 +++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 6 deletions(-) diff --git a/cuda_core/cuda/core/utils/_logging.py b/cuda_core/cuda/core/utils/_logging.py index 8f3261f2828..313cc9cab87 100644 --- a/cuda_core/cuda/core/utils/_logging.py +++ b/cuda_core/cuda/core/utils/_logging.py @@ -20,6 +20,7 @@ import ctypes import logging import threading +from typing import Callable, Optional from cuda.bindings import driver as _driver from cuda.core._utils.cuda_utils import handle_return @@ -42,6 +43,19 @@ int(_driver.CUlogLevel.CU_LOG_LEVEL_WARNING): logging.WARNING, } +# User-supplied hook that customizes how each driver log message is +# displayed/handled; see the ``callback`` parameter of +# :func:`register_cuda_log_bridge`. +# +# callback(logger, level, message) -> None +# +# ``logger`` is the same :class:`logging.Logger` passed to (or defaulted by) +# :func:`register_cuda_log_bridge`, ``level`` is the already-mapped +# ``logging.ERROR`` / ``logging.WARNING`` level, and ``message`` is the +# decoded driver log text (without the ``[CUDA Driver]`` prefix added by the +# default formatter). +LogCallback = Callable[[logging.Logger, int, str], None] + _lock = threading.Lock() # Must hold a reference to the ctypes callback for as long as it is # registered: once it is garbage collected, the driver would be left @@ -51,10 +65,17 @@ _callback_handle = None # CUlogsCallbackHandle, needed to unregister -def _make_c_callback(logger: logging.Logger) -> ctypes._FuncPointer: +def _default_callback(logger: logging.Logger, level: int, message: str) -> None: + """Default display/forwarding behavior: plain forward into ``logger``.""" + logger.log(level, "[CUDA Driver] %s", message) + + +def _make_c_callback(logger: logging.Logger, callback: Optional[LogCallback]) -> ctypes._FuncPointer: """Build a ctypes C function pointer that forwards each driver log - record to ``logger``. + record to ``logger``, optionally via a user-supplied ``callback`` that + customizes how the message gets displayed. """ + emit = callback if callback is not None else _default_callback def _on_log(_user_data, log_level, message_ptr, length): # This runs inside a ctypes callback trampoline, potentially from a @@ -63,30 +84,43 @@ def _on_log(_user_data, log_level, message_ptr, length): raw = ctypes.string_at(message_ptr, length) if message_ptr else b"" msg = raw.decode("utf-8", errors="replace").rstrip("\n") py_level = _CUDA_LEVEL_TO_PY_LEVEL.get(log_level, logging.ERROR) - logger.log(py_level, "[CUDA Driver] %s", msg) + emit(logger, py_level, msg) except Exception: logger.exception("cuda_log_bridge: error while forwarding driver log") return _CUlogsCallback_functype(_on_log) -def register_cuda_log_bridge(logger: logging.Logger | None = None) -> None: +def register_cuda_log_bridge( + logger: logging.Logger | None = None, + callback: Optional[LogCallback] = None, +) -> None: """Register a bridge that forwards CUDA driver ``cuLogs*`` messages to a Python :class:`logging.Logger`. This is a process-wide, idempotent operation: the driver only supports a global log stream (not scoped to a context/stream/thread), so calling this again while a bridge is already registered is a no-op -- the - logger passed to the first call remains in effect. + logger (and callback) passed to the first call remain in effect. Parameters ---------- logger : logging.Logger, optional The logger that driver messages should be forwarded to. Defaults to ``logging.getLogger("cuda.driver")``. + callback : Callable[[logging.Logger, int, str], None], optional + Customizes how each driver log message is displayed, instead of + being limited to plain forwarding into ``logger``. Called as + ``callback(logger, level, message)`` for every driver log record, + where ``level`` is the mapped ``logging.ERROR``/``logging.WARNING`` + level and ``message`` is the decoded driver log text. Defaults to + ``None``, which plainly forwards every message via + ``logger.log(level, "[CUDA Driver] %s", message)``. Raises ------ + TypeError + If ``callback`` is not ``None`` and not callable. cuda.core._utils.cuda_utils.CUDAError If the underlying ``cuLogsRegisterCallback`` call fails, e.g. because the loaded CUDA driver predates the 12.9 cuLogs* API. @@ -96,11 +130,14 @@ def register_cuda_log_bridge(logger: logging.Logger | None = None) -> None: if logger is None: logger = logging.getLogger("cuda.driver") + if callback is not None and not callable(callback): + raise TypeError(f"callback must be callable or None, got {type(callback)!r}") + with _lock: if _callback_handle is not None: return # already registered; idempotent - c_callback = _make_c_callback(logger) + c_callback = _make_c_callback(logger, callback) addr = ctypes.cast(c_callback, ctypes.c_void_p).value handle = handle_return(_driver.cuLogsRegisterCallback(addr, None)) diff --git a/cuda_core/tests/test_cuda_log_bridge.py b/cuda_core/tests/test_cuda_log_bridge.py index 13226957871..e5d7f2419f8 100644 --- a/cuda_core/tests/test_cuda_log_bridge.py +++ b/cuda_core/tests/test_cuda_log_bridge.py @@ -156,3 +156,58 @@ def emit(self, record): logger_obj.removeHandler(counter_handler) assert counter_handler.count >= 1, "Expected at least one ERROR-level driver log to be counted" + + +# 使用方式6: integrate driver logs into existing logger through callback +def test_cuda_driver_log_custom_callback_overrides_default(caplog): + driver.cuInit(0) + + # (a) 创建一个叫做 "my_library.cuda-driver" 的 logger + logger_name = "my_library.cuda-driver" + logger_obj = logging.getLogger(logger_name) + + # (b) 自定义 my_callback:不管驱动原始级别是 ERROR 还是 WARNING, + # 统一记成 DEBUG;并在打印的消息前面加上 logger name 前缀 + def my_callback(logger, level, message): + logger.debug("%s: %s", logger.name, message) + + # (c) register 时传入 callback,覆盖掉内部默认的 + # logger.log(level, "[CUDA Driver] %s", message) 转发逻辑 + register_cuda_log_bridge(logger_obj, callback=my_callback) + + # 1. logging 默认的 WARNING 级别阈值下,DEBUG 级别的驱动日志会被过滤掉, + # caplog 收不到任何记录 + with caplog.at_level(logging.WARNING, logger=logger_name): + driver.cuDeviceGet(9999) + assert not any(r.name == logger_name for r in caplog.records), ( + "DEBUG-level driver log should be filtered out at the default WARNING level" + ) + + caplog.clear() + + # 2. 把 logger 的阈值调到 DEBUG,才能收到 my_callback 记录的驱动日志 + with caplog.at_level(logging.DEBUG, logger=logger_name): + driver.cuDeviceGet(9999) + + records = [r for r in caplog.records if r.name == logger_name] + assert records, "Expected driver log records forwarded via my_callback once the threshold is DEBUG" + + # 验证 my_callback 生效:级别被统一改写成了 DEBUG(而不是默认的 ERROR/WARNING) + assert all(r.levelno == logging.DEBUG for r in records), ( + f"Expected every record logged as DEBUG via my_callback, got levels: {[r.levelname for r in records]}" + ) + + # 验证消息前缀是 logger name(而不是 register_cuda_log_bridge 默认的 "[CUDA Driver] " 前缀) + messages = [r.message for r in records] + assert any(m.startswith(f"{logger_name}: ") for m in messages), ( + f"Expected messages prefixed with '{logger_name}: ', got:\n{messages}" + ) + assert not any(m.startswith("[CUDA Driver] ") for m in messages), ( + f"Default '[CUDA Driver] ' forwarding should be overridden by my_callback, got:\n{messages}" + ) + + +# callback 必须是可调用对象或 None,否则应尽早报错 +def test_cuda_driver_log_callback_must_be_callable_or_none(): + with pytest.raises(TypeError): + register_cuda_log_bridge(logging.getLogger("cuda.driver.bad_callback_test"), callback="not-callable") From 572015c617dfc0fa0cd58a3f5917d4b047495ab5 Mon Sep 17 00:00:00 2001 From: Jinfeng Date: Tue, 6 Oct 2026 00:43:13 +0000 Subject: [PATCH 04/12] refactor(cuda.core): rename register_cuda_log_bridge to register_cuda_error_log Matches CUDA's own Error Log Management terminology (cuLogs* API). Refs: https://github.com/NVIDIA/cuda-python/issues/671 --- cuda_core/cuda/core/utils/__init__.py | 8 ++++---- cuda_core/cuda/core/utils/_logging.py | 14 +++++++------- cuda_core/tests/test_cuda_log_bridge.py | 24 ++++++++++++------------ 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/cuda_core/cuda/core/utils/__init__.py b/cuda_core/cuda/core/utils/__init__.py index e60ada08ae5..7fc0b277159 100644 --- a/cuda_core/cuda/core/utils/__init__.py +++ b/cuda_core/cuda/core/utils/__init__.py @@ -18,8 +18,8 @@ args_viewable_as_strided_memory, ) from cuda.core.utils._logging import ( - register_cuda_log_bridge, - unregister_cuda_log_bridge, + register_cuda_error_log, + unregister_cuda_error_log, ) from cuda.core.utils._program_cache import ( FileStreamProgramCache, @@ -42,6 +42,6 @@ "discard_prefetch_batch", "make_program_cache_key", "prefetch_batch", - "register_cuda_log_bridge", - "unregister_cuda_log_bridge", + "register_cuda_error_log", + "unregister_cuda_error_log", ] diff --git a/cuda_core/cuda/core/utils/_logging.py b/cuda_core/cuda/core/utils/_logging.py index 313cc9cab87..037b5034186 100644 --- a/cuda_core/cuda/core/utils/_logging.py +++ b/cuda_core/cuda/core/utils/_logging.py @@ -25,7 +25,7 @@ from cuda.bindings import driver as _driver from cuda.core._utils.cuda_utils import handle_return -__all__ = ["register_cuda_log_bridge", "unregister_cuda_log_bridge"] +__all__ = ["register_cuda_error_log", "unregister_cuda_error_log"] # C callback signature expected by cuLogsRegisterCallback: # void callback(void *userData, CUlogLevel logLevel, char *message, size_t length) @@ -45,12 +45,12 @@ # User-supplied hook that customizes how each driver log message is # displayed/handled; see the ``callback`` parameter of -# :func:`register_cuda_log_bridge`. +# :func:`register_cuda_error_log`. # # callback(logger, level, message) -> None # # ``logger`` is the same :class:`logging.Logger` passed to (or defaulted by) -# :func:`register_cuda_log_bridge`, ``level`` is the already-mapped +# :func:`register_cuda_error_log`, ``level`` is the already-mapped # ``logging.ERROR`` / ``logging.WARNING`` level, and ``message`` is the # decoded driver log text (without the ``[CUDA Driver]`` prefix added by the # default formatter). @@ -86,12 +86,12 @@ def _on_log(_user_data, log_level, message_ptr, length): py_level = _CUDA_LEVEL_TO_PY_LEVEL.get(log_level, logging.ERROR) emit(logger, py_level, msg) except Exception: - logger.exception("cuda_log_bridge: error while forwarding driver log") + logger.exception("cuda_error_log: error while forwarding driver log") return _CUlogsCallback_functype(_on_log) -def register_cuda_log_bridge( +def register_cuda_error_log( logger: logging.Logger | None = None, callback: Optional[LogCallback] = None, ) -> None: @@ -148,9 +148,9 @@ def register_cuda_log_bridge( _callback_handle = handle -def unregister_cuda_log_bridge() -> None: +def unregister_cuda_error_log() -> None: """Unregister the bridge previously installed by - :func:`register_cuda_log_bridge`. + :func:`register_cuda_error_log`. Idempotent: safe to call even if no bridge is currently registered. diff --git a/cuda_core/tests/test_cuda_log_bridge.py b/cuda_core/tests/test_cuda_log_bridge.py index e5d7f2419f8..fab84dd49de 100644 --- a/cuda_core/tests/test_cuda_log_bridge.py +++ b/cuda_core/tests/test_cuda_log_bridge.py @@ -11,7 +11,7 @@ import pytest from cuda.bindings import driver -from cuda.core.utils import register_cuda_log_bridge, unregister_cuda_log_bridge +from cuda.core.utils import register_cuda_error_log, unregister_cuda_error_log @pytest.fixture(autouse=True) @@ -19,7 +19,7 @@ def teardown_bridge(): yield # Guarantee the callback is unregistered after every test, so a leaked # bridge/slot from one test can't affect later ones. - unregister_cuda_log_bridge() + unregister_cuda_error_log() # 使用方式1: 调用 register(logger), 通过 caplog 捕获 driver log @@ -30,7 +30,7 @@ def test_cuda_driver_log_captured_via_caplog(caplog): # 2. 注册桥接,将驱动日志路由到用户提供的 logger logger_name = "cuda.driver.caplog_test" logger_obj = logging.getLogger(logger_name) - register_cuda_log_bridge(logger_obj) + register_cuda_error_log(logger_obj) # 3. 触发驱动错误:ordinal 9999 必然越界,驱动会写入两条 [E] 日志 with caplog.at_level(logging.ERROR, logger=logger_name): @@ -67,7 +67,7 @@ def test_cuda_driver_log_written_to_file(tmp_path): try: # 3. 注册桥接,将驱动日志路由到该 logger - register_cuda_log_bridge(logger_obj) + register_cuda_error_log(logger_obj) # 4. 触发驱动错误:ordinal 9999 必然越界,驱动会写入两条 [E] 日志 driver.cuDeviceGet(9999) @@ -97,15 +97,15 @@ def test_cuda_driver_log_rebind_logger(caplog): name_a, name_b = "cuda.driver.rebind_a", "cuda.driver.rebind_b" # 先绑定 logger_a,确认能收到日志 - register_cuda_log_bridge(logging.getLogger(name_a)) + register_cuda_error_log(logging.getLogger(name_a)) with caplog.at_level(logging.ERROR, logger=name_a): driver.cuDeviceGet(9999) assert any(r.name == name_a for r in caplog.records) # 换绑到 logger_b:必须先 unregister 释放驱动侧 slot,再用新 logger register - unregister_cuda_log_bridge() + unregister_cuda_error_log() caplog.clear() - register_cuda_log_bridge(logging.getLogger(name_b)) + register_cuda_error_log(logging.getLogger(name_b)) with caplog.at_level(logging.ERROR, logger=name_b): driver.cuDeviceGet(9999) @@ -119,7 +119,7 @@ def test_cuda_driver_log_level_filtering(caplog): driver.cuInit(0) logger_name = "cuda.driver.level_test" - register_cuda_log_bridge(logging.getLogger(logger_name)) + register_cuda_error_log(logging.getLogger(logger_name)) # 阈值设为 CRITICAL:ERROR 级别的驱动日志应被过滤,caplog 收不到任何记录 with caplog.at_level(logging.CRITICAL, logger=logger_name): @@ -150,7 +150,7 @@ def emit(self, record): logger_obj.addHandler(counter_handler) try: - register_cuda_log_bridge(logger_obj) + register_cuda_error_log(logger_obj) driver.cuDeviceGet(9999) finally: logger_obj.removeHandler(counter_handler) @@ -173,7 +173,7 @@ def my_callback(logger, level, message): # (c) register 时传入 callback,覆盖掉内部默认的 # logger.log(level, "[CUDA Driver] %s", message) 转发逻辑 - register_cuda_log_bridge(logger_obj, callback=my_callback) + register_cuda_error_log(logger_obj, callback=my_callback) # 1. logging 默认的 WARNING 级别阈值下,DEBUG 级别的驱动日志会被过滤掉, # caplog 收不到任何记录 @@ -197,7 +197,7 @@ def my_callback(logger, level, message): f"Expected every record logged as DEBUG via my_callback, got levels: {[r.levelname for r in records]}" ) - # 验证消息前缀是 logger name(而不是 register_cuda_log_bridge 默认的 "[CUDA Driver] " 前缀) + # 验证消息前缀是 logger name(而不是 register_cuda_error_log 默认的 "[CUDA Driver] " 前缀) messages = [r.message for r in records] assert any(m.startswith(f"{logger_name}: ") for m in messages), ( f"Expected messages prefixed with '{logger_name}: ', got:\n{messages}" @@ -210,4 +210,4 @@ def my_callback(logger, level, message): # callback 必须是可调用对象或 None,否则应尽早报错 def test_cuda_driver_log_callback_must_be_callable_or_none(): with pytest.raises(TypeError): - register_cuda_log_bridge(logging.getLogger("cuda.driver.bad_callback_test"), callback="not-callable") + register_cuda_error_log(logging.getLogger("cuda.driver.bad_callback_test"), callback="not-callable") From 738cf8b1dfd5424db4d6498b95509333aea37317 Mon Sep 17 00:00:00 2001 From: Jinfeng Date: Tue, 6 Oct 2026 18:20:12 +0000 Subject: [PATCH 05/12] refactor(cuda.core): trim comments in log bridge and its tests Also fix custom-callback test: CUlogLevel enum was referenced via a nonexistent driver.CULOG_LEVEL_* attribute instead of driver.CUlogLevel.CU_LOG_LEVEL_*. Refs #671 --- cuda_core/cuda/core/utils/_logging.py | 116 +++++++++--------------- cuda_core/tests/test_cuda_log_bridge.py | 66 +++++--------- 2 files changed, 69 insertions(+), 113 deletions(-) diff --git a/cuda_core/cuda/core/utils/_logging.py b/cuda_core/cuda/core/utils/_logging.py index 037b5034186..a539e32deef 100644 --- a/cuda_core/cuda/core/utils/_logging.py +++ b/cuda_core/cuda/core/utils/_logging.py @@ -2,17 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 -"""Bridge the CUDA driver's ``cuLogs*`` error-log-management callback into -the standard :mod:`logging` module. - -The CUDA driver (CUDA 12.9+) maintains an internal, human-readable error/ -warning log stream (see the *Error Log Management* section of the CUDA -driver API docs) that is otherwise only observable via the ``CUDA_LOG_FILE`` -environment variable or stderr. This module lets a caller route that stream -into any :class:`logging.Logger`, so it can be filtered, captured (e.g. via -pytest's ``caplog``), and forwarded using ordinary Python logging tooling. - -xref: https://github.com/NVIDIA/cuda-python/issues/671 +"""Bridge CUDA driver error logs into Python's :mod:`logging` module via +the ``cuLogs*`` API. """ from __future__ import annotations @@ -27,64 +18,51 @@ __all__ = ["register_cuda_error_log", "unregister_cuda_error_log"] -# C callback signature expected by cuLogsRegisterCallback: +# C callback signature for cuLogsRegisterCallback: # void callback(void *userData, CUlogLevel logLevel, char *message, size_t length) -# -# Note: cuLogsRegisterCallback requires an actual C function pointer. The -# cuda.bindings wrapper does *not* accept a plain Python callable directly -# (it only ever converts the `callbackFunc` argument to an integer address), -# so we build a real function pointer via ctypes and pass its address. +# TODO: support a user-supplied userData. _CUlogsCallback_functype = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_size_t) -# CUlogLevel currently only defines two levels (CUDA 12.9 cuLogs* API). -# Anything unrecognized is conservatively mapped to ERROR. +# CUlogLevel only defines two levels today (CUDA 12.9 cuLogs* API). _CUDA_LEVEL_TO_PY_LEVEL = { int(_driver.CUlogLevel.CU_LOG_LEVEL_ERROR): logging.ERROR, int(_driver.CUlogLevel.CU_LOG_LEVEL_WARNING): logging.WARNING, } -# User-supplied hook that customizes how each driver log message is -# displayed/handled; see the ``callback`` parameter of -# :func:`register_cuda_error_log`. -# -# callback(logger, level, message) -> None -# -# ``logger`` is the same :class:`logging.Logger` passed to (or defaulted by) -# :func:`register_cuda_error_log`, ``level`` is the already-mapped -# ``logging.ERROR`` / ``logging.WARNING`` level, and ``message`` is the -# decoded driver log text (without the ``[CUDA Driver]`` prefix added by the -# default formatter). -LogCallback = Callable[[logging.Logger, int, str], None] +# User callback for `register_cuda_error_log`; signature matches the C +# callback 1:1: callback(user_data, log_level, message, length) -> None. +# user_data is the raw void* (None/0 today); log_level is the raw, +# unmapped CUlogLevel; message is raw undecoded bytes; length is its byte +# count. Decoding/level-mapping is the callback's job (see +# `_default_callback`). Needs `logger`? Close over it. +LogCallback = Callable[[Optional[int], int, bytes, int], None] _lock = threading.Lock() -# Must hold a reference to the ctypes callback for as long as it is -# registered: once it is garbage collected, the driver would be left -# holding a dangling function pointer and crash the process on the next -# log message. +# Keep a reference while registered, or GC'ing it leaves the driver with +# a dangling function pointer -> crash on the next log message. _c_callback: ctypes._FuncPointer | None = None _callback_handle = None # CUlogsCallbackHandle, needed to unregister -def _default_callback(logger: logging.Logger, level: int, message: str) -> None: - """Default display/forwarding behavior: plain forward into ``logger``.""" - logger.log(level, "[CUDA Driver] %s", message) +def _make_c_callback(logger: logging.Logger, callback: Optional[LogCallback]) -> ctypes._FuncPointer: + """Build the ctypes C callback passed to cuLogsRegisterCallback.""" + def _default_callback(user_data: Optional[int], log_level: int, message: bytes, length: int) -> None: + """Decode, map the level, and forward into ``logger`` (closed over).""" + del user_data, length + msg = message.decode("utf-8", errors="replace").rstrip("\n") + py_level = _CUDA_LEVEL_TO_PY_LEVEL.get(log_level, logging.ERROR) + logger.log(py_level, "[CUDA Driver] %s", msg) -def _make_c_callback(logger: logging.Logger, callback: Optional[LogCallback]) -> ctypes._FuncPointer: - """Build a ctypes C function pointer that forwards each driver log - record to ``logger``, optionally via a user-supplied ``callback`` that - customizes how the message gets displayed. - """ emit = callback if callback is not None else _default_callback - def _on_log(_user_data, log_level, message_ptr, length): - # This runs inside a ctypes callback trampoline, potentially from a - # CUDA-driver-internal thread. It must never raise or block. + def _on_log(user_data, log_level, message_ptr, length): + # Driver-internal thread; must never raise or block. try: - raw = ctypes.string_at(message_ptr, length) if message_ptr else b"" - msg = raw.decode("utf-8", errors="replace").rstrip("\n") - py_level = _CUDA_LEVEL_TO_PY_LEVEL.get(log_level, logging.ERROR) - emit(logger, py_level, msg) + # message_ptr is only valid during this call, so copy it out + # now. Pass args through undecoded/unmapped -- that's `emit`'s job. + message = ctypes.string_at(message_ptr, length) if message_ptr else b"" + emit(user_data, log_level, message, length) except Exception: logger.exception("cuda_error_log: error while forwarding driver log") @@ -98,32 +76,28 @@ def register_cuda_error_log( """Register a bridge that forwards CUDA driver ``cuLogs*`` messages to a Python :class:`logging.Logger`. - This is a process-wide, idempotent operation: the driver only supports - a global log stream (not scoped to a context/stream/thread), so calling - this again while a bridge is already registered is a no-op -- the - logger (and callback) passed to the first call remain in effect. + Process-wide and idempotent: the driver only has one global log + stream, so calling this again while already registered is a no-op. Parameters ---------- logger : logging.Logger, optional - The logger that driver messages should be forwarded to. Defaults - to ``logging.getLogger("cuda.driver")``. - callback : Callable[[logging.Logger, int, str], None], optional - Customizes how each driver log message is displayed, instead of - being limited to plain forwarding into ``logger``. Called as - ``callback(logger, level, message)`` for every driver log record, - where ``level`` is the mapped ``logging.ERROR``/``logging.WARNING`` - level and ``message`` is the decoded driver log text. Defaults to - ``None``, which plainly forwards every message via - ``logger.log(level, "[CUDA Driver] %s", message)``. + Target logger. Defaults to ``logging.getLogger("cuda.driver")``. + callback : Callable[[Optional[int], int, bytes, int], None], optional + Overrides the default forwarding. Called as + ``callback(user_data, log_level, message, length)``, matching the + C callback 1:1 (undecoded ``message``, unmapped ``log_level``; + decoding/mapping is the callback's job). Close over ``logger`` if + needed. Defaults to ``None`` (decode, map, and forward via + ``logger.log(level, "[CUDA Driver] %s", message)``). Raises ------ TypeError If ``callback`` is not ``None`` and not callable. cuda.core._utils.cuda_utils.CUDAError - If the underlying ``cuLogsRegisterCallback`` call fails, e.g. - because the loaded CUDA driver predates the 12.9 cuLogs* API. + If ``cuLogsRegisterCallback`` fails, e.g. the driver predates + CUDA 12.9. """ global _c_callback, _callback_handle @@ -142,22 +116,20 @@ def register_cuda_error_log( handle = handle_return(_driver.cuLogsRegisterCallback(addr, None)) - # Keep the ctypes callback object alive for as long as it is - # registered (see the module-level comment on _c_callback above). + # Keep the ctypes callback alive (see _c_callback comment above). _c_callback = c_callback _callback_handle = handle def unregister_cuda_error_log() -> None: - """Unregister the bridge previously installed by - :func:`register_cuda_error_log`. + """Unregister the bridge installed by :func:`register_cuda_error_log`. - Idempotent: safe to call even if no bridge is currently registered. + Idempotent: safe to call even if nothing is registered. Raises ------ cuda.core._utils.cuda_utils.CUDAError - If the underlying ``cuLogsUnregisterCallback`` call fails. + If ``cuLogsUnregisterCallback`` fails. """ global _c_callback, _callback_handle diff --git a/cuda_core/tests/test_cuda_log_bridge.py b/cuda_core/tests/test_cuda_log_bridge.py index fab84dd49de..525430da9ea 100644 --- a/cuda_core/tests/test_cuda_log_bridge.py +++ b/cuda_core/tests/test_cuda_log_bridge.py @@ -17,26 +17,22 @@ @pytest.fixture(autouse=True) def teardown_bridge(): yield - # Guarantee the callback is unregistered after every test, so a leaked - # bridge/slot from one test can't affect later ones. + # Unregister after every test so a leaked bridge can't leak into others. unregister_cuda_error_log() -# 使用方式1: 调用 register(logger), 通过 caplog 捕获 driver log +# 使用方式1: register(logger) 后用 caplog 捕获 driver log def test_cuda_driver_log_captured_via_caplog(caplog): - # 1. 初始化 CUDA Driver driver.cuInit(0) - # 2. 注册桥接,将驱动日志路由到用户提供的 logger logger_name = "cuda.driver.caplog_test" logger_obj = logging.getLogger(logger_name) register_cuda_error_log(logger_obj) - # 3. 触发驱动错误:ordinal 9999 必然越界,驱动会写入两条 [E] 日志 + # cuDeviceGet(9999) 越界,驱动写入两条 ERROR 日志 with caplog.at_level(logging.ERROR, logger=logger_name): driver.cuDeviceGet(9999) - # 4. 验证 caplog 中包含两条驱动错误日志 messages = [r.message for r in caplog.records if r.name == logger_name] assert any("Parameter ordinal must be between 0 and 1" in m for m in messages), ( @@ -48,13 +44,11 @@ def test_cuda_driver_log_captured_via_caplog(caplog): ) -# 使用方式2: 通过 logging handler 把 driver log 倒入到文件 +# 使用方式2: 通过 FileHandler 把 driver log 写到文件 def test_cuda_driver_log_written_to_file(tmp_path): - # 1. 初始化 CUDA Driver driver.cuInit(0) - # 2. 准备一个独立的 logger,挂上写文件的 FileHandler - # (用独立 logger name,避免污染 root logger / 影响其他用例) + # 独立 logger name,避免污染 root logger / 影响其他用例 log_file = tmp_path / "cuda_driver.log" logger_name = "cuda.driver.file_test" logger_obj = logging.getLogger(logger_name) @@ -66,19 +60,13 @@ def test_cuda_driver_log_written_to_file(tmp_path): logger_obj.addHandler(file_handler) try: - # 3. 注册桥接,将驱动日志路由到该 logger register_cuda_error_log(logger_obj) - - # 4. 触发驱动错误:ordinal 9999 必然越界,驱动会写入两条 [E] 日志 driver.cuDeviceGet(9999) finally: - # 5. 显式移除并关闭 handler: - # - close() 会 flush 缓冲区,确保日志落盘再读取 - # - removeHandler 避免 handler 残留导致文件句柄泄漏 / 影响后续用例 + # close() flush 落盘;removeHandler 防止跨用例累加 handler logger_obj.removeHandler(file_handler) file_handler.close() - # 6. 校验文件内容 content = log_file.read_text(encoding="utf-8") assert "Parameter ordinal must be between 0 and 1" in content, ( @@ -90,50 +78,47 @@ def test_cuda_driver_log_written_to_file(tmp_path): ) -# 使用方式3: 动态绑定解绑 register -> unregister -> 再register +# 使用方式3: register -> unregister -> 换绑另一个 logger def test_cuda_driver_log_rebind_logger(caplog): driver.cuInit(0) name_a, name_b = "cuda.driver.rebind_a", "cuda.driver.rebind_b" - # 先绑定 logger_a,确认能收到日志 register_cuda_error_log(logging.getLogger(name_a)) with caplog.at_level(logging.ERROR, logger=name_a): driver.cuDeviceGet(9999) assert any(r.name == name_a for r in caplog.records) - # 换绑到 logger_b:必须先 unregister 释放驱动侧 slot,再用新 logger register + # 必须先 unregister 释放驱动侧 slot,再绑新 logger unregister_cuda_error_log() caplog.clear() register_cuda_error_log(logging.getLogger(name_b)) with caplog.at_level(logging.ERROR, logger=name_b): driver.cuDeviceGet(9999) - # 旧 logger 不应再收到任何消息,新 logger 应正常收到 assert not any(r.name == name_a for r in caplog.records) assert any(r.name == name_b for r in caplog.records) -# 使用方式4: 日志过滤 —— logger 的 level 阈值对驱动日志同样生效 +# 使用方式4: logger 的 level 阈值对驱动日志同样生效 def test_cuda_driver_log_level_filtering(caplog): driver.cuInit(0) logger_name = "cuda.driver.level_test" register_cuda_error_log(logging.getLogger(logger_name)) - # 阈值设为 CRITICAL:ERROR 级别的驱动日志应被过滤,caplog 收不到任何记录 + # CRITICAL 阈值过滤掉 ERROR 级别的驱动日志 with caplog.at_level(logging.CRITICAL, logger=logger_name): driver.cuDeviceGet(9999) assert not any(r.name == logger_name for r in caplog.records) - # 放低阈值到 ERROR:同样的错误应该能被捕获到 + # 降到 ERROR 阈值就能捕获到 with caplog.at_level(logging.ERROR, logger=logger_name): driver.cuDeviceGet(9999) assert any(r.name == logger_name for r in caplog.records) -# 使用方式5: 监控/告警集成 —— 用自定义 Handler 统计驱动错误次数, -# 可据此接入 Prometheus/Datadog 等监控系统做告警 +# 使用方式5: 自定义 Handler 统计错误次数,接入监控/告警 def test_cuda_driver_log_alert_counter(): driver.cuInit(0) @@ -158,25 +143,24 @@ def emit(self, record): assert counter_handler.count >= 1, "Expected at least one ERROR-level driver log to be counted" -# 使用方式6: integrate driver logs into existing logger through callback +# 使用方式6: 用 callback 把 driver log 接进已有的 logger def test_cuda_driver_log_custom_callback_overrides_default(caplog): driver.cuInit(0) - # (a) 创建一个叫做 "my_library.cuda-driver" 的 logger logger_name = "my_library.cuda-driver" logger_obj = logging.getLogger(logger_name) - # (b) 自定义 my_callback:不管驱动原始级别是 ERROR 还是 WARNING, - # 统一记成 DEBUG;并在打印的消息前面加上 logger name 前缀 - def my_callback(logger, level, message): - logger.debug("%s: %s", logger.name, message) + # 按 raw CUlogLevel 分发;ERROR 记成 DEBUG + def my_callback(user_data, cu_log_level, message, length): + msg = message.decode("utf-8", errors="replace").rstrip("\n") + if cu_log_level == driver.CUlogLevel.CU_LOG_LEVEL_WARNING: + logger_obj.warning("%s: %s", logger_obj.name, msg) + elif cu_log_level == driver.CUlogLevel.CU_LOG_LEVEL_ERROR: + logger_obj.debug("%s: %s", logger_obj.name, msg) - # (c) register 时传入 callback,覆盖掉内部默认的 - # logger.log(level, "[CUDA Driver] %s", message) 转发逻辑 register_cuda_error_log(logger_obj, callback=my_callback) - # 1. logging 默认的 WARNING 级别阈值下,DEBUG 级别的驱动日志会被过滤掉, - # caplog 收不到任何记录 + # 默认 WARNING 阈值下,DEBUG 级别的驱动日志被过滤掉 with caplog.at_level(logging.WARNING, logger=logger_name): driver.cuDeviceGet(9999) assert not any(r.name == logger_name for r in caplog.records), ( @@ -185,19 +169,19 @@ def my_callback(logger, level, message): caplog.clear() - # 2. 把 logger 的阈值调到 DEBUG,才能收到 my_callback 记录的驱动日志 + # 调到 DEBUG 阈值才能收到 with caplog.at_level(logging.DEBUG, logger=logger_name): driver.cuDeviceGet(9999) records = [r for r in caplog.records if r.name == logger_name] assert records, "Expected driver log records forwarded via my_callback once the threshold is DEBUG" - # 验证 my_callback 生效:级别被统一改写成了 DEBUG(而不是默认的 ERROR/WARNING) + # my_callback 生效:级别统一改成了 DEBUG assert all(r.levelno == logging.DEBUG for r in records), ( f"Expected every record logged as DEBUG via my_callback, got levels: {[r.levelname for r in records]}" ) - # 验证消息前缀是 logger name(而不是 register_cuda_error_log 默认的 "[CUDA Driver] " 前缀) + # 消息前缀是 logger name,不是默认的 "[CUDA Driver] " messages = [r.message for r in records] assert any(m.startswith(f"{logger_name}: ") for m in messages), ( f"Expected messages prefixed with '{logger_name}: ', got:\n{messages}" @@ -207,7 +191,7 @@ def my_callback(logger, level, message): ) -# callback 必须是可调用对象或 None,否则应尽早报错 +# callback 必须可调用或 None,否则应尽早报错 def test_cuda_driver_log_callback_must_be_callable_or_none(): with pytest.raises(TypeError): register_cuda_error_log(logging.getLogger("cuda.driver.bad_callback_test"), callback="not-callable") From 851f5dd5d39e8e07ac9543926a06657fbb347a5c Mon Sep 17 00:00:00 2001 From: Jinfeng Date: Tue, 6 Oct 2026 18:43:28 +0000 Subject: [PATCH 06/12] refactor(cuda.core): rename LogCallback to PyLogCallback Also drop the stale userData TODO comment, move the CUlogLevel->logging level map into _default_callback (its only user), and rename its log_level param to cu_log_level for clarity. Refs #671 --- cuda_core/cuda/core/utils/_logging.py | 33 ++++++++------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/cuda_core/cuda/core/utils/_logging.py b/cuda_core/cuda/core/utils/_logging.py index a539e32deef..d7794441309 100644 --- a/cuda_core/cuda/core/utils/_logging.py +++ b/cuda_core/cuda/core/utils/_logging.py @@ -20,38 +20,25 @@ # C callback signature for cuLogsRegisterCallback: # void callback(void *userData, CUlogLevel logLevel, char *message, size_t length) -# TODO: support a user-supplied userData. _CUlogsCallback_functype = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_size_t) +PyLogCallback = Callable[[Optional[int], int, bytes, int], None] -# CUlogLevel only defines two levels today (CUDA 12.9 cuLogs* API). -_CUDA_LEVEL_TO_PY_LEVEL = { - int(_driver.CUlogLevel.CU_LOG_LEVEL_ERROR): logging.ERROR, - int(_driver.CUlogLevel.CU_LOG_LEVEL_WARNING): logging.WARNING, -} - -# User callback for `register_cuda_error_log`; signature matches the C -# callback 1:1: callback(user_data, log_level, message, length) -> None. -# user_data is the raw void* (None/0 today); log_level is the raw, -# unmapped CUlogLevel; message is raw undecoded bytes; length is its byte -# count. Decoding/level-mapping is the callback's job (see -# `_default_callback`). Needs `logger`? Close over it. -LogCallback = Callable[[Optional[int], int, bytes, int], None] - -_lock = threading.Lock() # Keep a reference while registered, or GC'ing it leaves the driver with # a dangling function pointer -> crash on the next log message. +_lock = threading.Lock() _c_callback: ctypes._FuncPointer | None = None _callback_handle = None # CUlogsCallbackHandle, needed to unregister - -def _make_c_callback(logger: logging.Logger, callback: Optional[LogCallback]) -> ctypes._FuncPointer: +def _make_c_callback(logger: logging.Logger, callback: Optional[PyLogCallback]) -> ctypes._FuncPointer: """Build the ctypes C callback passed to cuLogsRegisterCallback.""" - def _default_callback(user_data: Optional[int], log_level: int, message: bytes, length: int) -> None: - """Decode, map the level, and forward into ``logger`` (closed over).""" - del user_data, length + def _default_callback(user_data: Optional[int], cu_log_level: int, message: bytes, length: int) -> None: + cuda_level_to_py_level = { + int(_driver.CUlogLevel.CU_LOG_LEVEL_ERROR): logging.ERROR, + int(_driver.CUlogLevel.CU_LOG_LEVEL_WARNING): logging.WARNING, + } msg = message.decode("utf-8", errors="replace").rstrip("\n") - py_level = _CUDA_LEVEL_TO_PY_LEVEL.get(log_level, logging.ERROR) + py_level = cuda_level_to_py_level.get(cu_log_level, logging.ERROR) logger.log(py_level, "[CUDA Driver] %s", msg) emit = callback if callback is not None else _default_callback @@ -71,7 +58,7 @@ def _on_log(user_data, log_level, message_ptr, length): def register_cuda_error_log( logger: logging.Logger | None = None, - callback: Optional[LogCallback] = None, + callback: Optional[PyLogCallback] = None, ) -> None: """Register a bridge that forwards CUDA driver ``cuLogs*`` messages to a Python :class:`logging.Logger`. From b00e97f8ca942a300a7f822902fd9b2b6cbe8d9d Mon Sep 17 00:00:00 2001 From: Jinfeng Date: Tue, 6 Oct 2026 20:44:30 +0000 Subject: [PATCH 07/12] refactor(cuda.core): drop redundant length param from PyLogCallback bytes already carries its own length via len(), so length added no information beyond what message already has. Updated the custom callback test to match the new 3-arg signature. Refs #671 --- cuda_core/cuda/core/utils/_logging.py | 23 ++++++++++------------- cuda_core/tests/test_cuda_log_bridge.py | 2 +- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/cuda_core/cuda/core/utils/_logging.py b/cuda_core/cuda/core/utils/_logging.py index d7794441309..006d925da00 100644 --- a/cuda_core/cuda/core/utils/_logging.py +++ b/cuda_core/cuda/core/utils/_logging.py @@ -21,10 +21,10 @@ # C callback signature for cuLogsRegisterCallback: # void callback(void *userData, CUlogLevel logLevel, char *message, size_t length) _CUlogsCallback_functype = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_size_t) -PyLogCallback = Callable[[Optional[int], int, bytes, int], None] +PyLogCallback = Callable[[Optional[int], int, bytes], None] # Keep a reference while registered, or GC'ing it leaves the driver with -# a dangling function pointer -> crash on the next log message. +# a dangling function pointer which will crash on the next log message. _lock = threading.Lock() _c_callback: ctypes._FuncPointer | None = None _callback_handle = None # CUlogsCallbackHandle, needed to unregister @@ -32,7 +32,7 @@ def _make_c_callback(logger: logging.Logger, callback: Optional[PyLogCallback]) -> ctypes._FuncPointer: """Build the ctypes C callback passed to cuLogsRegisterCallback.""" - def _default_callback(user_data: Optional[int], cu_log_level: int, message: bytes, length: int) -> None: + def _default_callback(user_data: Optional[int], cu_log_level: int, message: bytes) -> None: cuda_level_to_py_level = { int(_driver.CUlogLevel.CU_LOG_LEVEL_ERROR): logging.ERROR, int(_driver.CUlogLevel.CU_LOG_LEVEL_WARNING): logging.WARNING, @@ -44,12 +44,9 @@ def _default_callback(user_data: Optional[int], cu_log_level: int, message: byte emit = callback if callback is not None else _default_callback def _on_log(user_data, log_level, message_ptr, length): - # Driver-internal thread; must never raise or block. try: - # message_ptr is only valid during this call, so copy it out - # now. Pass args through undecoded/unmapped -- that's `emit`'s job. message = ctypes.string_at(message_ptr, length) if message_ptr else b"" - emit(user_data, log_level, message, length) + emit(user_data, log_level, message) except Exception: logger.exception("cuda_error_log: error while forwarding driver log") @@ -70,13 +67,13 @@ def register_cuda_error_log( ---------- logger : logging.Logger, optional Target logger. Defaults to ``logging.getLogger("cuda.driver")``. - callback : Callable[[Optional[int], int, bytes, int], None], optional + callback : Callable[[Optional[int], int, bytes], None], optional Overrides the default forwarding. Called as - ``callback(user_data, log_level, message, length)``, matching the - C callback 1:1 (undecoded ``message``, unmapped ``log_level``; - decoding/mapping is the callback's job). Close over ``logger`` if - needed. Defaults to ``None`` (decode, map, and forward via - ``logger.log(level, "[CUDA Driver] %s", message)``). + ``callback(user_data, log_level, message)`` with undecoded + ``message`` and unmapped ``log_level``; decoding/mapping is the + callback's job (``len(message)`` gives its size). Close over + ``logger`` if needed. Defaults to ``None`` (decode, map, and + forward via ``logger.log(level, "[CUDA Driver] %s", message)``). Raises ------ diff --git a/cuda_core/tests/test_cuda_log_bridge.py b/cuda_core/tests/test_cuda_log_bridge.py index 525430da9ea..2fe3f1b2c60 100644 --- a/cuda_core/tests/test_cuda_log_bridge.py +++ b/cuda_core/tests/test_cuda_log_bridge.py @@ -151,7 +151,7 @@ def test_cuda_driver_log_custom_callback_overrides_default(caplog): logger_obj = logging.getLogger(logger_name) # 按 raw CUlogLevel 分发;ERROR 记成 DEBUG - def my_callback(user_data, cu_log_level, message, length): + def my_callback(user_data, cu_log_level, message): msg = message.decode("utf-8", errors="replace").rstrip("\n") if cu_log_level == driver.CUlogLevel.CU_LOG_LEVEL_WARNING: logger_obj.warning("%s: %s", logger_obj.name, msg) From 3879839014b35b050ab7b5c44a26fc87944a1e93 Mon Sep 17 00:00:00 2001 From: Jinfeng Date: Tue, 6 Oct 2026 21:06:22 +0000 Subject: [PATCH 08/12] refactor(cuda.core): drop redundant user_data param from PyLogCallback Python closures already give callbacks access to enclosing state (e.g. logger), and cuLogsRegisterCallback is always registered with userData as None today, so it carried no information. Updated the custom callback test to match the new 2-arg signature. Refs #671 --- cuda_core/cuda/core/utils/_logging.py | 18 +++++++++--------- cuda_core/tests/test_cuda_log_bridge.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cuda_core/cuda/core/utils/_logging.py b/cuda_core/cuda/core/utils/_logging.py index 006d925da00..ac72966e4f8 100644 --- a/cuda_core/cuda/core/utils/_logging.py +++ b/cuda_core/cuda/core/utils/_logging.py @@ -21,7 +21,7 @@ # C callback signature for cuLogsRegisterCallback: # void callback(void *userData, CUlogLevel logLevel, char *message, size_t length) _CUlogsCallback_functype = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_size_t) -PyLogCallback = Callable[[Optional[int], int, bytes], None] +PyLogCallback = Callable[[int, bytes], None] # Keep a reference while registered, or GC'ing it leaves the driver with # a dangling function pointer which will crash on the next log message. @@ -32,7 +32,7 @@ def _make_c_callback(logger: logging.Logger, callback: Optional[PyLogCallback]) -> ctypes._FuncPointer: """Build the ctypes C callback passed to cuLogsRegisterCallback.""" - def _default_callback(user_data: Optional[int], cu_log_level: int, message: bytes) -> None: + def _default_callback(cu_log_level: int, message: bytes) -> None: cuda_level_to_py_level = { int(_driver.CUlogLevel.CU_LOG_LEVEL_ERROR): logging.ERROR, int(_driver.CUlogLevel.CU_LOG_LEVEL_WARNING): logging.WARNING, @@ -46,7 +46,7 @@ def _default_callback(user_data: Optional[int], cu_log_level: int, message: byte def _on_log(user_data, log_level, message_ptr, length): try: message = ctypes.string_at(message_ptr, length) if message_ptr else b"" - emit(user_data, log_level, message) + emit(log_level, message) except Exception: logger.exception("cuda_error_log: error while forwarding driver log") @@ -67,13 +67,13 @@ def register_cuda_error_log( ---------- logger : logging.Logger, optional Target logger. Defaults to ``logging.getLogger("cuda.driver")``. - callback : Callable[[Optional[int], int, bytes], None], optional + callback : Callable[[int, bytes], None], optional Overrides the default forwarding. Called as - ``callback(user_data, log_level, message)`` with undecoded - ``message`` and unmapped ``log_level``; decoding/mapping is the - callback's job (``len(message)`` gives its size). Close over - ``logger`` if needed. Defaults to ``None`` (decode, map, and - forward via ``logger.log(level, "[CUDA Driver] %s", message)``). + ``callback(log_level, message)`` with undecoded ``message`` and + unmapped ``log_level``; decoding/mapping is the callback's job + (``len(message)`` gives its size). Close over ``logger`` if + needed. Defaults to ``None`` (decode, map, and forward via + ``logger.log(level, "[CUDA Driver] %s", message)``). Raises ------ diff --git a/cuda_core/tests/test_cuda_log_bridge.py b/cuda_core/tests/test_cuda_log_bridge.py index 2fe3f1b2c60..07d78d9b01f 100644 --- a/cuda_core/tests/test_cuda_log_bridge.py +++ b/cuda_core/tests/test_cuda_log_bridge.py @@ -151,7 +151,7 @@ def test_cuda_driver_log_custom_callback_overrides_default(caplog): logger_obj = logging.getLogger(logger_name) # 按 raw CUlogLevel 分发;ERROR 记成 DEBUG - def my_callback(user_data, cu_log_level, message): + def my_callback(cu_log_level, message): msg = message.decode("utf-8", errors="replace").rstrip("\n") if cu_log_level == driver.CUlogLevel.CU_LOG_LEVEL_WARNING: logger_obj.warning("%s: %s", logger_obj.name, msg) From a878e78a3dca54638a153f9e17bed45b70bb9879 Mon Sep 17 00:00:00 2001 From: Jinfeng Date: Tue, 6 Oct 2026 23:31:54 +0000 Subject: [PATCH 09/12] return logger and define behaviors --- cuda_core/cuda/core/utils/_logging.py | 58 ++++++++++++--------------- 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/cuda_core/cuda/core/utils/_logging.py b/cuda_core/cuda/core/utils/_logging.py index ac72966e4f8..e45647d070e 100644 --- a/cuda_core/cuda/core/utils/_logging.py +++ b/cuda_core/cuda/core/utils/_logging.py @@ -11,7 +11,7 @@ import ctypes import logging import threading -from typing import Callable, Optional +from typing import Callable from cuda.bindings import driver as _driver from cuda.core._utils.cuda_utils import handle_return @@ -29,7 +29,8 @@ _c_callback: ctypes._FuncPointer | None = None _callback_handle = None # CUlogsCallbackHandle, needed to unregister -def _make_c_callback(logger: logging.Logger, callback: Optional[PyLogCallback]) -> ctypes._FuncPointer: + +def _make_c_callback(logger: logging.Logger, callback: PyLogCallback | None) -> ctypes._FuncPointer: """Build the ctypes C callback passed to cuLogsRegisterCallback.""" def _default_callback(cu_log_level: int, message: bytes) -> None: @@ -55,33 +56,28 @@ def _on_log(user_data, log_level, message_ptr, length): def register_cuda_error_log( logger: logging.Logger | None = None, - callback: Optional[PyLogCallback] = None, -) -> None: + callback: PyLogCallback | None = None, +) -> logging.Logger: """Register a bridge that forwards CUDA driver ``cuLogs*`` messages to a Python :class:`logging.Logger`. - Process-wide and idempotent: the driver only has one global log - stream, so calling this again while already registered is a no-op. - Parameters ---------- logger : logging.Logger, optional - Target logger. Defaults to ``logging.getLogger("cuda.driver")``. + Target logger. If not provided, a default is created via + ``logging.getLogger("cuda.driver")``. callback : Callable[[int, bytes], None], optional Overrides the default forwarding. Called as - ``callback(log_level, message)`` with undecoded ``message`` and - unmapped ``log_level``; decoding/mapping is the callback's job - (``len(message)`` gives its size). Close over ``logger`` if - needed. Defaults to ``None`` (decode, map, and forward via - ``logger.log(level, "[CUDA Driver] %s", message)``). - - Raises - ------ - TypeError - If ``callback`` is not ``None`` and not callable. - cuda.core._utils.cuda_utils.CUDAError - If ``cuLogsRegisterCallback`` fails, e.g. the driver predates - CUDA 12.9. + ``callback(cu_log_level, message)`` with undecoded ``message`` and + unmapped ``cu_log_level``; decoding/mapping is the callback's job. + + Returns + ------- + logging.Logger + The ``logger`` argument above (or the default logger it resolved + to). If a bridge was already registered, it is unregistered first, + so this logger/callback replaces it. + """ global _c_callback, _callback_handle @@ -93,7 +89,11 @@ def register_cuda_error_log( with _lock: if _callback_handle is not None: - return # already registered; idempotent + # Already registered: unregister the old callback/handle first, + # then fall through to register the new logger/callback below. + handle_return(_driver.cuLogsUnregisterCallback(_callback_handle)) + _c_callback = None + _callback_handle = None c_callback = _make_c_callback(logger, callback) addr = ctypes.cast(c_callback, ctypes.c_void_p).value @@ -104,22 +104,16 @@ def register_cuda_error_log( _c_callback = c_callback _callback_handle = handle + return logger -def unregister_cuda_error_log() -> None: - """Unregister the bridge installed by :func:`register_cuda_error_log`. - - Idempotent: safe to call even if nothing is registered. - Raises - ------ - cuda.core._utils.cuda_utils.CUDAError - If ``cuLogsUnregisterCallback`` fails. - """ +def unregister_cuda_error_log() -> None: + """Unregister the bridge installed by :func:`register_cuda_error_log`.""" global _c_callback, _callback_handle with _lock: if _callback_handle is None: - return # nothing registered; idempotent + return handle_return(_driver.cuLogsUnregisterCallback(_callback_handle)) From 1e12518fbd0d9247b02444a06ff2a0d0316e4cce Mon Sep 17 00:00:00 2001 From: Jinfeng Date: Tue, 6 Oct 2026 23:34:48 +0000 Subject: [PATCH 10/12] rename unsed user_data to _user_data to get pre-commit ruff passed --- cuda_core/cuda/core/utils/_logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cuda_core/cuda/core/utils/_logging.py b/cuda_core/cuda/core/utils/_logging.py index e45647d070e..3c411b5d9db 100644 --- a/cuda_core/cuda/core/utils/_logging.py +++ b/cuda_core/cuda/core/utils/_logging.py @@ -44,7 +44,7 @@ def _default_callback(cu_log_level: int, message: bytes) -> None: emit = callback if callback is not None else _default_callback - def _on_log(user_data, log_level, message_ptr, length): + def _on_log(_user_data, log_level, message_ptr, length): try: message = ctypes.string_at(message_ptr, length) if message_ptr else b"" emit(log_level, message) From 4e823c9fa263b0c817941be0e94887b71137abaf Mon Sep 17 00:00:00 2001 From: Jinfeng Date: Tue, 6 Oct 2026 23:54:07 +0000 Subject: [PATCH 11/12] test(cuda.core): translate comments to English, tighten assertions - Replace Chinese usage-comments with English equivalents. - Assert on exact message count/content instead of loose any() checks. - Add coverage for unregister() stopping further log forwarding. - Simplify the custom-callback test: single unconditional DEBUG rewrite instead of dead WARNING/ERROR branching. Refs #671 --- cuda_core/tests/test_cuda_log_bridge.py | 85 ++++++++++--------------- 1 file changed, 32 insertions(+), 53 deletions(-) diff --git a/cuda_core/tests/test_cuda_log_bridge.py b/cuda_core/tests/test_cuda_log_bridge.py index 07d78d9b01f..f7244da225e 100644 --- a/cuda_core/tests/test_cuda_log_bridge.py +++ b/cuda_core/tests/test_cuda_log_bridge.py @@ -21,7 +21,7 @@ def teardown_bridge(): unregister_cuda_error_log() -# 使用方式1: register(logger) 后用 caplog 捕获 driver log +# Usage 1: show driver error log interactively def test_cuda_driver_log_captured_via_caplog(caplog): driver.cuInit(0) @@ -29,26 +29,29 @@ def test_cuda_driver_log_captured_via_caplog(caplog): logger_obj = logging.getLogger(logger_name) register_cuda_error_log(logger_obj) - # cuDeviceGet(9999) 越界,驱动写入两条 ERROR 日志 + # cuDeviceGet(9999) is out of range; driver writes two ERROR logs with caplog.at_level(logging.ERROR, logger=logger_name): driver.cuDeviceGet(9999) messages = [r.message for r in caplog.records if r.name == logger_name] - assert any("Parameter ordinal must be between 0 and 1" in m for m in messages), ( - f"Expected ordinal error in caplog, got:\n{messages}" - ) + assert len(messages) == 2, f"Expected exactly 2 driver log messages, got:\n{messages}" + assert "Parameter ordinal must be between 0 and 1" in messages[0] + assert "CUDA_ERROR_INVALID_DEVICE" in messages[1] and "cuDeviceGet" in messages[1] - assert any("CUDA_ERROR_INVALID_DEVICE" in m and "cuDeviceGet" in m for m in messages), ( - f"Expected cuDeviceGet return error in caplog, got:\n{messages}" - ) + # After unregistering, driver logs are no longer forwarded + unregister_cuda_error_log() + caplog.clear() + with caplog.at_level(logging.ERROR, logger=logger_name): + driver.cuDeviceGet(9999) + assert not any(r.name == logger_name for r in caplog.records) + assert "Parameter ordinal must be between 0 and 1" not in caplog.text -# 使用方式2: 通过 FileHandler 把 driver log 写到文件 +# Usage 2: redirect driver log to a file def test_cuda_driver_log_written_to_file(tmp_path): driver.cuInit(0) - # 独立 logger name,避免污染 root logger / 影响其他用例 log_file = tmp_path / "cuda_driver.log" logger_name = "cuda.driver.file_test" logger_obj = logging.getLogger(logger_name) @@ -63,22 +66,16 @@ def test_cuda_driver_log_written_to_file(tmp_path): register_cuda_error_log(logger_obj) driver.cuDeviceGet(9999) finally: - # close() flush 落盘;removeHandler 防止跨用例累加 handler logger_obj.removeHandler(file_handler) file_handler.close() content = log_file.read_text(encoding="utf-8") - assert "Parameter ordinal must be between 0 and 1" in content, ( - f"Expected ordinal error in log file, got:\n{content}" - ) - - assert "CUDA_ERROR_INVALID_DEVICE" in content and "cuDeviceGet" in content, ( - f"Expected cuDeviceGet return error in log file, got:\n{content}" - ) + assert "Parameter ordinal must be between 0 and 1" in content + assert "CUDA_ERROR_INVALID_DEVICE" in content and "cuDeviceGet" in content -# 使用方式3: register -> unregister -> 换绑另一个 logger +# Usage 3: register -> unregister -> rebind to another logger def test_cuda_driver_log_rebind_logger(caplog): driver.cuInit(0) @@ -89,7 +86,7 @@ def test_cuda_driver_log_rebind_logger(caplog): driver.cuDeviceGet(9999) assert any(r.name == name_a for r in caplog.records) - # 必须先 unregister 释放驱动侧 slot,再绑新 logger + # Must unregister first to free the driver-side slot, then bind the new logger unregister_cuda_error_log() caplog.clear() register_cuda_error_log(logging.getLogger(name_b)) @@ -100,25 +97,25 @@ def test_cuda_driver_log_rebind_logger(caplog): assert any(r.name == name_b for r in caplog.records) -# 使用方式4: logger 的 level 阈值对驱动日志同样生效 +# Usage 4: the logger's level threshold applies to driver logs too def test_cuda_driver_log_level_filtering(caplog): driver.cuInit(0) logger_name = "cuda.driver.level_test" register_cuda_error_log(logging.getLogger(logger_name)) - # CRITICAL 阈值过滤掉 ERROR 级别的驱动日志 + # CRITICAL threshold filters out ERROR-level driver logs with caplog.at_level(logging.CRITICAL, logger=logger_name): driver.cuDeviceGet(9999) assert not any(r.name == logger_name for r in caplog.records) - # 降到 ERROR 阈值就能捕获到 + # Lowering to ERROR threshold lets them through with caplog.at_level(logging.ERROR, logger=logger_name): driver.cuDeviceGet(9999) assert any(r.name == logger_name for r in caplog.records) -# 使用方式5: 自定义 Handler 统计错误次数,接入监控/告警 +# Usage 5: custom Handler counts errors, for monitoring/alerting integration def test_cuda_driver_log_alert_counter(): driver.cuInit(0) @@ -140,58 +137,40 @@ def emit(self, record): finally: logger_obj.removeHandler(counter_handler) - assert counter_handler.count >= 1, "Expected at least one ERROR-level driver log to be counted" + assert counter_handler.count == 2, "Expected exactly 2 ERROR-level driver logs to be counted" -# 使用方式6: 用 callback 把 driver log 接进已有的 logger +# Usage 6: use a custom python callback function to change log level and log format def test_cuda_driver_log_custom_callback_overrides_default(caplog): driver.cuInit(0) logger_name = "my_library.cuda-driver" logger_obj = logging.getLogger(logger_name) - # 按 raw CUlogLevel 分发;ERROR 记成 DEBUG + # Log everything as DEBUG, prefixed with the logger name instead of "[CUDA Driver] " def my_callback(cu_log_level, message): msg = message.decode("utf-8", errors="replace").rstrip("\n") - if cu_log_level == driver.CUlogLevel.CU_LOG_LEVEL_WARNING: - logger_obj.warning("%s: %s", logger_obj.name, msg) - elif cu_log_level == driver.CUlogLevel.CU_LOG_LEVEL_ERROR: - logger_obj.debug("%s: %s", logger_obj.name, msg) + logger_obj.debug("%s: %s", logger_obj.name, msg) register_cuda_error_log(logger_obj, callback=my_callback) - # 默认 WARNING 阈值下,DEBUG 级别的驱动日志被过滤掉 + # Default WARNING threshold filters out the DEBUG-level driver logs with caplog.at_level(logging.WARNING, logger=logger_name): driver.cuDeviceGet(9999) - assert not any(r.name == logger_name for r in caplog.records), ( - "DEBUG-level driver log should be filtered out at the default WARNING level" - ) + assert not caplog.records caplog.clear() - # 调到 DEBUG 阈值才能收到 + # Lowering the threshold to DEBUG lets them through, with the custom level/format with caplog.at_level(logging.DEBUG, logger=logger_name): driver.cuDeviceGet(9999) - records = [r for r in caplog.records if r.name == logger_name] - assert records, "Expected driver log records forwarded via my_callback once the threshold is DEBUG" - - # my_callback 生效:级别统一改成了 DEBUG - assert all(r.levelno == logging.DEBUG for r in records), ( - f"Expected every record logged as DEBUG via my_callback, got levels: {[r.levelname for r in records]}" - ) - - # 消息前缀是 logger name,不是默认的 "[CUDA Driver] " - messages = [r.message for r in records] - assert any(m.startswith(f"{logger_name}: ") for m in messages), ( - f"Expected messages prefixed with '{logger_name}: ', got:\n{messages}" - ) - assert not any(m.startswith("[CUDA Driver] ") for m in messages), ( - f"Default '[CUDA Driver] ' forwarding should be overridden by my_callback, got:\n{messages}" - ) + assert len(caplog.records) == 2 + assert all(r.levelno == logging.DEBUG for r in caplog.records) + assert all(r.message.startswith(f"{logger_name}: ") for r in caplog.records) -# callback 必须可调用或 None,否则应尽早报错 +# callback must be callable or None, else fail fast def test_cuda_driver_log_callback_must_be_callable_or_none(): with pytest.raises(TypeError): register_cuda_error_log(logging.getLogger("cuda.driver.bad_callback_test"), callback="not-callable") From 1f7e59a732899853cbc796c3c3caa6d5b20211a6 Mon Sep 17 00:00:00 2001 From: Jinfeng Date: Mon, 12 Oct 2026 22:25:52 +0000 Subject: [PATCH 12/12] docs(cuda.core): document driver log bridge in utils API --- cuda_core/docs/source/api.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cuda_core/docs/source/api.rst b/cuda_core/docs/source/api.rst index d9c4acf206c..9d01008e0c9 100644 --- a/cuda_core/docs/source/api.rst +++ b/cuda_core/docs/source/api.rst @@ -126,6 +126,21 @@ avoid recompiling identical source + options + target without writing the make_program_cache_key +Driver logging +`````````````` + +:func:`~cuda.core.utils.register_cuda_error_log` bridges CUDA driver +``cuLogs*`` messages into Python's standard :mod:`logging` module. + +.. currentmodule:: cuda.core.utils + +.. autosummary:: + :toctree: generated/ + + register_cuda_error_log + unregister_cuda_error_log + + CUDA graphs -----------