Repository navigation
feat(cuda.core): bridge CUDA driver logs into Python logging #3081
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
645c86a
916247d
a84e6c0
572015c
738cf8b
851f5dd
b00e97f
3879839
a878e78
1e12518
4e823c9
1f7e59a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Bridge CUDA driver error logs into Python's :mod:`logging` module via | ||
| the ``cuLogs*`` API. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import ctypes | ||
| import logging | ||
| import threading | ||
| from typing import Callable | ||
|
|
||
| from cuda.bindings import driver as _driver | ||
| from cuda.core._utils.cuda_utils import handle_return | ||
|
|
||
| __all__ = ["register_cuda_error_log", "unregister_cuda_error_log"] | ||
|
|
||
| # 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[[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. | ||
| _lock = threading.Lock() | ||
| _c_callback: ctypes._FuncPointer | None = None | ||
| _callback_handle = None # CUlogsCallbackHandle, needed to unregister | ||
|
|
||
|
|
||
| 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: | ||
| 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(cu_log_level, logging.ERROR) | ||
| logger.log(py_level, "[CUDA Driver] %s", msg) | ||
|
|
||
| emit = callback if callback is not None else _default_callback | ||
|
|
||
| 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) | ||
| except Exception: | ||
| logger.exception("cuda_error_log: error while forwarding driver log") | ||
|
|
||
| return _CUlogsCallback_functype(_on_log) | ||
|
|
||
|
|
||
| def register_cuda_error_log( | ||
| logger: logging.Logger | None = None, | ||
| callback: PyLogCallback | None = None, | ||
| ) -> logging.Logger: | ||
| """Register a bridge that forwards CUDA driver ``cuLogs*`` messages to | ||
| a Python :class:`logging.Logger`. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| logger : logging.Logger, optional | ||
| 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(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 | ||
|
|
||
| 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: | ||
| # 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 | ||
|
|
||
| handle = handle_return(_driver.cuLogsRegisterCallback(addr, None)) | ||
|
|
||
| # Keep the ctypes callback alive (see _c_callback comment above). | ||
| _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`.""" | ||
| global _c_callback, _callback_handle | ||
|
|
||
| with _lock: | ||
| if _callback_handle is None: | ||
| return | ||
|
|
||
| handle_return(_driver.cuLogsUnregisterCallback(_callback_handle)) | ||
|
|
||
| _c_callback = None | ||
| _callback_handle = None |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,176 @@ | ||||||||||
| # 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_error_log, unregister_cuda_error_log | ||||||||||
|
|
||||||||||
|
|
||||||||||
| @pytest.fixture(autouse=True) | ||||||||||
| def teardown_bridge(): | ||||||||||
| yield | ||||||||||
| # Unregister after every test so a leaked bridge can't leak into others. | ||||||||||
| unregister_cuda_error_log() | ||||||||||
|
Comment on lines
+17
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win important: Nothing in this module skips the tests when the driver is older than CUDA 12.9, where |
||||||||||
|
|
||||||||||
|
|
||||||||||
| # Usage 1: show driver error log interactively | ||||||||||
| def test_cuda_driver_log_captured_via_caplog(caplog): | ||||||||||
| driver.cuInit(0) | ||||||||||
|
|
||||||||||
| logger_name = "cuda.driver.caplog_test" | ||||||||||
| logger_obj = logging.getLogger(logger_name) | ||||||||||
| register_cuda_error_log(logger_obj) | ||||||||||
|
Check failure on line 30 in cuda_core/tests/test_cuda_log_bridge.py
|
||||||||||
|
|
||||||||||
| # cuDeviceGet(9999) is out of range; driver writes two ERROR logs | ||||||||||
| with caplog.at_level(logging.ERROR, logger=logger_name): | ||||||||||
| driver.cuDeviceGet(9999) | ||||||||||
|
Comment on lines
+32
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: printf '%s\n' '--- cuda_core/tests/AGENTS.md ---'
nl -ba cuda_core/tests/AGENTS.md
printf '%s\n' '--- test module ---'
nl -ba cuda_core/tests/test_cuda_log_bridge.py
printf '%s\n' '--- relevant conftest fixtures ---'
nl -ba cuda_core/tests/conftest.py | sed -n '210,245p'
printf '%s\n' '--- changed test diff ---'
git diff 0271e3ad33d56bd34530110a1decb8bebfdb0cb4 4e823c9fa263b0c817941be0e94887b71137abaf -- cuda_core/tests/test_cuda_log_bridge.pyRepository: NVIDIA/cuda-python Length of output: 29840 important: Establish a current CUDA context for these driver tests.
Suggested fix-def test_cuda_driver_log_captured_via_caplog(caplog):
+def test_cuda_driver_log_captured_via_caplog(caplog, init_cuda):
-def test_cuda_driver_log_written_to_file(tmp_path):
+def test_cuda_driver_log_written_to_file(tmp_path, init_cuda):
-def test_cuda_driver_log_rebind_logger(caplog):
+def test_cuda_driver_log_rebind_logger(caplog, init_cuda):
-def test_cuda_driver_log_level_filtering(caplog):
+def test_cuda_driver_log_level_filtering(caplog, init_cuda):
-def test_cuda_driver_log_alert_counter():
+def test_cuda_driver_log_alert_counter(init_cuda):
-def test_cuda_driver_log_custom_callback_overrides_default(caplog):
+def test_cuda_driver_log_custom_callback_overrides_default(caplog, init_cuda): |
||||||||||
|
|
||||||||||
| messages = [r.message for r in caplog.records if r.name == logger_name] | ||||||||||
|
|
||||||||||
| assert len(messages) == 2, f"Expected exactly 2 driver log messages, got:\n{messages}" | ||||||||||
|
Check failure on line 38 in cuda_core/tests/test_cuda_log_bridge.py
|
||||||||||
| assert "Parameter ordinal must be between 0 and 1" in messages[0] | ||||||||||
|
Check failure on line 39 in cuda_core/tests/test_cuda_log_bridge.py
|
||||||||||
| assert "CUDA_ERROR_INVALID_DEVICE" in messages[1] and "cuDeviceGet" in messages[1] | ||||||||||
|
Comment on lines
+38
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '24,85p' cuda_core/tests/test_cuda_log_bridge.pyRepository: NVIDIA/cuda-python Length of output: 2482 🏁 Script executed: rg -n -F -- 'unregister_cuda_error_log' cuda_core/tests/test_cuda_log_bridge.py
sed -n '1,210p' cuda_core/tests/test_cuda_log_bridge.pyRepository: NVIDIA/cuda-python Length of output: 6606 important: Remove driver-specific log wording and count assumptions. The driver does not guarantee diagnostic text or callback count. These assertions can fail without a bridge failure. The post-unregister block already checks forwarding independently of message text with Suggested fix- # cuDeviceGet(9999) is out of range; driver writes two ERROR logs
+ # cuDeviceGet(9999) is out of range; the driver emits error logs
...
- 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 messages, f"Expected forwarded driver log messages, got:\n{messages}"
...
- assert "Parameter ordinal must be between 0 and 1" not in caplog.textApply the same rule to the file, handler-count, and custom-callback checks: assert non-empty forwarded output or 📝 Committable suggestion
Suggested change
|
||||||||||
|
|
||||||||||
| # 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 | ||||||||||
|
|
||||||||||
|
|
||||||||||
| # Usage 2: redirect driver log to a file | ||||||||||
| def test_cuda_driver_log_written_to_file(tmp_path): | ||||||||||
| driver.cuInit(0) | ||||||||||
|
|
||||||||||
| 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: | ||||||||||
| register_cuda_error_log(logger_obj) | ||||||||||
|
Check failure on line 66 in cuda_core/tests/test_cuda_log_bridge.py
|
||||||||||
| driver.cuDeviceGet(9999) | ||||||||||
| finally: | ||||||||||
| 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 | ||||||||||
|
Check failure on line 74 in cuda_core/tests/test_cuda_log_bridge.py
|
||||||||||
| assert "CUDA_ERROR_INVALID_DEVICE" in content and "cuDeviceGet" in content | ||||||||||
|
|
||||||||||
|
|
||||||||||
| # Usage 3: register -> unregister -> rebind to another logger | ||||||||||
| def test_cuda_driver_log_rebind_logger(caplog): | ||||||||||
| driver.cuInit(0) | ||||||||||
|
|
||||||||||
| name_a, name_b = "cuda.driver.rebind_a", "cuda.driver.rebind_b" | ||||||||||
|
|
||||||||||
| register_cuda_error_log(logging.getLogger(name_a)) | ||||||||||
|
Check failure on line 84 in cuda_core/tests/test_cuda_log_bridge.py
|
||||||||||
| with caplog.at_level(logging.ERROR, logger=name_a): | ||||||||||
| driver.cuDeviceGet(9999) | ||||||||||
| assert any(r.name == name_a for r in caplog.records) | ||||||||||
|
Check failure on line 87 in cuda_core/tests/test_cuda_log_bridge.py
|
||||||||||
|
|
||||||||||
| # 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)) | ||||||||||
| with caplog.at_level(logging.ERROR, logger=name_b): | ||||||||||
| driver.cuDeviceGet(9999) | ||||||||||
|
|
||||||||||
| assert not any(r.name == name_a for r in caplog.records) | ||||||||||
| assert any(r.name == name_b for r in caplog.records) | ||||||||||
|
|
||||||||||
|
|
||||||||||
| # 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)) | ||||||||||
|
Check failure on line 105 in cuda_core/tests/test_cuda_log_bridge.py
|
||||||||||
|
|
||||||||||
| # 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) | ||||||||||
|
Check failure on line 110 in cuda_core/tests/test_cuda_log_bridge.py
|
||||||||||
|
|
||||||||||
| # 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) | ||||||||||
|
Check failure on line 115 in cuda_core/tests/test_cuda_log_bridge.py
|
||||||||||
|
|
||||||||||
|
|
||||||||||
| # Usage 5: custom Handler counts errors, for monitoring/alerting integration | ||||||||||
| 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_error_log(logger_obj) | ||||||||||
|
Check failure on line 135 in cuda_core/tests/test_cuda_log_bridge.py
|
||||||||||
| driver.cuDeviceGet(9999) | ||||||||||
| finally: | ||||||||||
| logger_obj.removeHandler(counter_handler) | ||||||||||
|
|
||||||||||
| assert counter_handler.count == 2, "Expected exactly 2 ERROR-level driver logs to be counted" | ||||||||||
|
Check failure on line 140 in cuda_core/tests/test_cuda_log_bridge.py
|
||||||||||
|
|
||||||||||
|
|
||||||||||
| # 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) | ||||||||||
|
|
||||||||||
| # 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") | ||||||||||
| logger_obj.debug("%s: %s", logger_obj.name, msg) | ||||||||||
|
|
||||||||||
| register_cuda_error_log(logger_obj, callback=my_callback) | ||||||||||
|
Check failure on line 155 in cuda_core/tests/test_cuda_log_bridge.py
|
||||||||||
|
|
||||||||||
| # Default WARNING threshold filters out the DEBUG-level driver logs | ||||||||||
| with caplog.at_level(logging.WARNING, logger=logger_name): | ||||||||||
| driver.cuDeviceGet(9999) | ||||||||||
| assert not caplog.records | ||||||||||
|
Check failure on line 160 in cuda_core/tests/test_cuda_log_bridge.py
|
||||||||||
|
|
||||||||||
| caplog.clear() | ||||||||||
|
|
||||||||||
| # 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) | ||||||||||
|
|
||||||||||
| assert len(caplog.records) == 2 | ||||||||||
|
Check failure on line 168 in cuda_core/tests/test_cuda_log_bridge.py
|
||||||||||
| 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 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") | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: NVIDIA/cuda-python
Length of output: 156
🏁 Script executed:
Repository: NVIDIA/cuda-python
Length of output: 7273
important: Add a release-note entry for
register_cuda_error_logandunregister_cuda_error_log. The release notes contain no entry for this new public API or CUDA driver logging.Source: Path instructions