Skip to content

Commit 738cf8b

Browse files
committed
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 NVIDIA#671
1 parent 572015c commit 738cf8b

2 files changed

Lines changed: 69 additions & 113 deletions

File tree

‎cuda_core/cuda/core/utils/_logging.py‎

Lines changed: 44 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,8 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5-
"""Bridge the CUDA driver's ``cuLogs*`` error-log-management callback into
6-
the standard :mod:`logging` module.
7-
8-
The CUDA driver (CUDA 12.9+) maintains an internal, human-readable error/
9-
warning log stream (see the *Error Log Management* section of the CUDA
10-
driver API docs) that is otherwise only observable via the ``CUDA_LOG_FILE``
11-
environment variable or stderr. This module lets a caller route that stream
12-
into any :class:`logging.Logger`, so it can be filtered, captured (e.g. via
13-
pytest's ``caplog``), and forwarded using ordinary Python logging tooling.
14-
15-
xref: https://github.com/NVIDIA/cuda-python/issues/671
5+
"""Bridge CUDA driver error logs into Python's :mod:`logging` module via
6+
the ``cuLogs*`` API.
167
"""
178

189
from __future__ import annotations
@@ -27,64 +18,51 @@
2718

2819
__all__ = ["register_cuda_error_log", "unregister_cuda_error_log"]
2920

30-
# C callback signature expected by cuLogsRegisterCallback:
21+
# C callback signature for cuLogsRegisterCallback:
3122
# void callback(void *userData, CUlogLevel logLevel, char *message, size_t length)
32-
#
33-
# Note: cuLogsRegisterCallback requires an actual C function pointer. The
34-
# cuda.bindings wrapper does *not* accept a plain Python callable directly
35-
# (it only ever converts the `callbackFunc` argument to an integer address),
36-
# so we build a real function pointer via ctypes and pass its address.
23+
# TODO: support a user-supplied userData.
3724
_CUlogsCallback_functype = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_size_t)
3825

39-
# CUlogLevel currently only defines two levels (CUDA 12.9 cuLogs* API).
40-
# Anything unrecognized is conservatively mapped to ERROR.
26+
# CUlogLevel only defines two levels today (CUDA 12.9 cuLogs* API).
4127
_CUDA_LEVEL_TO_PY_LEVEL = {
4228
int(_driver.CUlogLevel.CU_LOG_LEVEL_ERROR): logging.ERROR,
4329
int(_driver.CUlogLevel.CU_LOG_LEVEL_WARNING): logging.WARNING,
4430
}
4531

46-
# User-supplied hook that customizes how each driver log message is
47-
# displayed/handled; see the ``callback`` parameter of
48-
# :func:`register_cuda_error_log`.
49-
#
50-
# callback(logger, level, message) -> None
51-
#
52-
# ``logger`` is the same :class:`logging.Logger` passed to (or defaulted by)
53-
# :func:`register_cuda_error_log`, ``level`` is the already-mapped
54-
# ``logging.ERROR`` / ``logging.WARNING`` level, and ``message`` is the
55-
# decoded driver log text (without the ``[CUDA Driver]`` prefix added by the
56-
# default formatter).
57-
LogCallback = Callable[[logging.Logger, int, str], None]
32+
# User callback for `register_cuda_error_log`; signature matches the C
33+
# callback 1:1: callback(user_data, log_level, message, length) -> None.
34+
# user_data is the raw void* (None/0 today); log_level is the raw,
35+
# unmapped CUlogLevel; message is raw undecoded bytes; length is its byte
36+
# count. Decoding/level-mapping is the callback's job (see
37+
# `_default_callback`). Needs `logger`? Close over it.
38+
LogCallback = Callable[[Optional[int], int, bytes, int], None]
5839

5940
_lock = threading.Lock()
60-
# Must hold a reference to the ctypes callback for as long as it is
61-
# registered: once it is garbage collected, the driver would be left
62-
# holding a dangling function pointer and crash the process on the next
63-
# log message.
41+
# Keep a reference while registered, or GC'ing it leaves the driver with
42+
# a dangling function pointer -> crash on the next log message.
6443
_c_callback: ctypes._FuncPointer | None = None
6544
_callback_handle = None # CUlogsCallbackHandle, needed to unregister
6645

6746

68-
def _default_callback(logger: logging.Logger, level: int, message: str) -> None:
69-
"""Default display/forwarding behavior: plain forward into ``logger``."""
70-
logger.log(level, "[CUDA Driver] %s", message)
47+
def _make_c_callback(logger: logging.Logger, callback: Optional[LogCallback]) -> ctypes._FuncPointer:
48+
"""Build the ctypes C callback passed to cuLogsRegisterCallback."""
7149

50+
def _default_callback(user_data: Optional[int], log_level: int, message: bytes, length: int) -> None:
51+
"""Decode, map the level, and forward into ``logger`` (closed over)."""
52+
del user_data, length
53+
msg = message.decode("utf-8", errors="replace").rstrip("\n")
54+
py_level = _CUDA_LEVEL_TO_PY_LEVEL.get(log_level, logging.ERROR)
55+
logger.log(py_level, "[CUDA Driver] %s", msg)
7256

73-
def _make_c_callback(logger: logging.Logger, callback: Optional[LogCallback]) -> ctypes._FuncPointer:
74-
"""Build a ctypes C function pointer that forwards each driver log
75-
record to ``logger``, optionally via a user-supplied ``callback`` that
76-
customizes how the message gets displayed.
77-
"""
7857
emit = callback if callback is not None else _default_callback
7958

80-
def _on_log(_user_data, log_level, message_ptr, length):
81-
# This runs inside a ctypes callback trampoline, potentially from a
82-
# CUDA-driver-internal thread. It must never raise or block.
59+
def _on_log(user_data, log_level, message_ptr, length):
60+
# Driver-internal thread; must never raise or block.
8361
try:
84-
raw = ctypes.string_at(message_ptr, length) if message_ptr else b""
85-
msg = raw.decode("utf-8", errors="replace").rstrip("\n")
86-
py_level = _CUDA_LEVEL_TO_PY_LEVEL.get(log_level, logging.ERROR)
87-
emit(logger, py_level, msg)
62+
# message_ptr is only valid during this call, so copy it out
63+
# now. Pass args through undecoded/unmapped -- that's `emit`'s job.
64+
message = ctypes.string_at(message_ptr, length) if message_ptr else b""
65+
emit(user_data, log_level, message, length)
8866
except Exception:
8967
logger.exception("cuda_error_log: error while forwarding driver log")
9068

@@ -98,32 +76,28 @@ def register_cuda_error_log(
9876
"""Register a bridge that forwards CUDA driver ``cuLogs*`` messages to
9977
a Python :class:`logging.Logger`.
10078
101-
This is a process-wide, idempotent operation: the driver only supports
102-
a global log stream (not scoped to a context/stream/thread), so calling
103-
this again while a bridge is already registered is a no-op -- the
104-
logger (and callback) passed to the first call remain in effect.
79+
Process-wide and idempotent: the driver only has one global log
80+
stream, so calling this again while already registered is a no-op.
10581
10682
Parameters
10783
----------
10884
logger : logging.Logger, optional
109-
The logger that driver messages should be forwarded to. Defaults
110-
to ``logging.getLogger("cuda.driver")``.
111-
callback : Callable[[logging.Logger, int, str], None], optional
112-
Customizes how each driver log message is displayed, instead of
113-
being limited to plain forwarding into ``logger``. Called as
114-
``callback(logger, level, message)`` for every driver log record,
115-
where ``level`` is the mapped ``logging.ERROR``/``logging.WARNING``
116-
level and ``message`` is the decoded driver log text. Defaults to
117-
``None``, which plainly forwards every message via
118-
``logger.log(level, "[CUDA Driver] %s", message)``.
85+
Target logger. Defaults to ``logging.getLogger("cuda.driver")``.
86+
callback : Callable[[Optional[int], int, bytes, int], None], optional
87+
Overrides the default forwarding. Called as
88+
``callback(user_data, log_level, message, length)``, matching the
89+
C callback 1:1 (undecoded ``message``, unmapped ``log_level``;
90+
decoding/mapping is the callback's job). Close over ``logger`` if
91+
needed. Defaults to ``None`` (decode, map, and forward via
92+
``logger.log(level, "[CUDA Driver] %s", message)``).
11993
12094
Raises
12195
------
12296
TypeError
12397
If ``callback`` is not ``None`` and not callable.
12498
cuda.core._utils.cuda_utils.CUDAError
125-
If the underlying ``cuLogsRegisterCallback`` call fails, e.g.
126-
because the loaded CUDA driver predates the 12.9 cuLogs* API.
99+
If ``cuLogsRegisterCallback`` fails, e.g. the driver predates
100+
CUDA 12.9.
127101
"""
128102
global _c_callback, _callback_handle
129103

@@ -142,22 +116,20 @@ def register_cuda_error_log(
142116

143117
handle = handle_return(_driver.cuLogsRegisterCallback(addr, None))
144118

145-
# Keep the ctypes callback object alive for as long as it is
146-
# registered (see the module-level comment on _c_callback above).
119+
# Keep the ctypes callback alive (see _c_callback comment above).
147120
_c_callback = c_callback
148121
_callback_handle = handle
149122

150123

151124
def unregister_cuda_error_log() -> None:
152-
"""Unregister the bridge previously installed by
153-
:func:`register_cuda_error_log`.
125+
"""Unregister the bridge installed by :func:`register_cuda_error_log`.
154126
155-
Idempotent: safe to call even if no bridge is currently registered.
127+
Idempotent: safe to call even if nothing is registered.
156128
157129
Raises
158130
------
159131
cuda.core._utils.cuda_utils.CUDAError
160-
If the underlying ``cuLogsUnregisterCallback`` call fails.
132+
If ``cuLogsUnregisterCallback`` fails.
161133
"""
162134
global _c_callback, _callback_handle
163135

‎cuda_core/tests/test_cuda_log_bridge.py‎

Lines changed: 25 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,22 @@
1717
@pytest.fixture(autouse=True)
1818
def teardown_bridge():
1919
yield
20-
# Guarantee the callback is unregistered after every test, so a leaked
21-
# bridge/slot from one test can't affect later ones.
20+
# Unregister after every test so a leaked bridge can't leak into others.
2221
unregister_cuda_error_log()
2322

2423

25-
# 使用方式1: 调用 register(logger), 通过 caplog 捕获 driver log
24+
# 使用方式1: register(logger) 后用 caplog 捕获 driver log
2625
def test_cuda_driver_log_captured_via_caplog(caplog):
27-
# 1. 初始化 CUDA Driver
2826
driver.cuInit(0)
2927

30-
# 2. 注册桥接,将驱动日志路由到用户提供的 logger
3128
logger_name = "cuda.driver.caplog_test"
3229
logger_obj = logging.getLogger(logger_name)
3330
register_cuda_error_log(logger_obj)
3431

35-
# 3. 触发驱动错误:ordinal 9999 必然越界,驱动会写入两条 [E] 日志
32+
# cuDeviceGet(9999) 越界,驱动写入两条 ERROR 日志
3633
with caplog.at_level(logging.ERROR, logger=logger_name):
3734
driver.cuDeviceGet(9999)
3835

39-
# 4. 验证 caplog 中包含两条驱动错误日志
4036
messages = [r.message for r in caplog.records if r.name == logger_name]
4137

4238
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):
4844
)
4945

5046

51-
# 使用方式2: 通过 logging handler 把 driver log 倒入到文件
47+
# 使用方式2: 通过 FileHandler 把 driver log 写到文件
5248
def test_cuda_driver_log_written_to_file(tmp_path):
53-
# 1. 初始化 CUDA Driver
5449
driver.cuInit(0)
5550

56-
# 2. 准备一个独立的 logger,挂上写文件的 FileHandler
57-
# (用独立 logger name,避免污染 root logger / 影响其他用例)
51+
# 独立 logger name,避免污染 root logger / 影响其他用例
5852
log_file = tmp_path / "cuda_driver.log"
5953
logger_name = "cuda.driver.file_test"
6054
logger_obj = logging.getLogger(logger_name)
@@ -66,19 +60,13 @@ def test_cuda_driver_log_written_to_file(tmp_path):
6660
logger_obj.addHandler(file_handler)
6761

6862
try:
69-
# 3. 注册桥接,将驱动日志路由到该 logger
7063
register_cuda_error_log(logger_obj)
71-
72-
# 4. 触发驱动错误:ordinal 9999 必然越界,驱动会写入两条 [E] 日志
7364
driver.cuDeviceGet(9999)
7465
finally:
75-
# 5. 显式移除并关闭 handler:
76-
# - close() 会 flush 缓冲区,确保日志落盘再读取
77-
# - removeHandler 避免 handler 残留导致文件句柄泄漏 / 影响后续用例
66+
# close() flush 落盘;removeHandler 防止跨用例累加 handler
7867
logger_obj.removeHandler(file_handler)
7968
file_handler.close()
8069

81-
# 6. 校验文件内容
8270
content = log_file.read_text(encoding="utf-8")
8371

8472
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):
9078
)
9179

9280

93-
# 使用方式3: 动态绑定解绑 register -> unregister -> 再register
81+
# 使用方式3: register -> unregister -> 换绑另一个 logger
9482
def test_cuda_driver_log_rebind_logger(caplog):
9583
driver.cuInit(0)
9684

9785
name_a, name_b = "cuda.driver.rebind_a", "cuda.driver.rebind_b"
9886

99-
# 先绑定 logger_a,确认能收到日志
10087
register_cuda_error_log(logging.getLogger(name_a))
10188
with caplog.at_level(logging.ERROR, logger=name_a):
10289
driver.cuDeviceGet(9999)
10390
assert any(r.name == name_a for r in caplog.records)
10491

105-
# 换绑到 logger_b:必须先 unregister 释放驱动侧 slot,再用新 logger register
92+
# 必须先 unregister 释放驱动侧 slot,再绑新 logger
10693
unregister_cuda_error_log()
10794
caplog.clear()
10895
register_cuda_error_log(logging.getLogger(name_b))
10996
with caplog.at_level(logging.ERROR, logger=name_b):
11097
driver.cuDeviceGet(9999)
11198

112-
# 旧 logger 不应再收到任何消息,新 logger 应正常收到
11399
assert not any(r.name == name_a for r in caplog.records)
114100
assert any(r.name == name_b for r in caplog.records)
115101

116102

117-
# 使用方式4: 日志过滤 —— logger 的 level 阈值对驱动日志同样生效
103+
# 使用方式4: logger 的 level 阈值对驱动日志同样生效
118104
def test_cuda_driver_log_level_filtering(caplog):
119105
driver.cuInit(0)
120106

121107
logger_name = "cuda.driver.level_test"
122108
register_cuda_error_log(logging.getLogger(logger_name))
123109

124-
# 阈值设为 CRITICAL:ERROR 级别的驱动日志应被过滤,caplog 收不到任何记录
110+
# CRITICAL 阈值过滤掉 ERROR 级别的驱动日志
125111
with caplog.at_level(logging.CRITICAL, logger=logger_name):
126112
driver.cuDeviceGet(9999)
127113
assert not any(r.name == logger_name for r in caplog.records)
128114

129-
# 放低阈值到 ERROR:同样的错误应该能被捕获到
115+
# 降到 ERROR 阈值就能捕获到
130116
with caplog.at_level(logging.ERROR, logger=logger_name):
131117
driver.cuDeviceGet(9999)
132118
assert any(r.name == logger_name for r in caplog.records)
133119

134120

135-
# 使用方式5: 监控/告警集成 —— 用自定义 Handler 统计驱动错误次数,
136-
# 可据此接入 Prometheus/Datadog 等监控系统做告警
121+
# 使用方式5: 自定义 Handler 统计错误次数,接入监控/告警
137122
def test_cuda_driver_log_alert_counter():
138123
driver.cuInit(0)
139124

@@ -158,25 +143,24 @@ def emit(self, record):
158143
assert counter_handler.count >= 1, "Expected at least one ERROR-level driver log to be counted"
159144

160145

161-
# 使用方式6: integrate driver logs into existing logger through callback
146+
# 使用方式6: 用 callback 把 driver log 接进已有的 logger
162147
def test_cuda_driver_log_custom_callback_overrides_default(caplog):
163148
driver.cuInit(0)
164149

165-
# (a) 创建一个叫做 "my_library.cuda-driver" 的 logger
166150
logger_name = "my_library.cuda-driver"
167151
logger_obj = logging.getLogger(logger_name)
168152

169-
# (b) 自定义 my_callback:不管驱动原始级别是 ERROR 还是 WARNING,
170-
# 统一记成 DEBUG;并在打印的消息前面加上 logger name 前缀
171-
def my_callback(logger, level, message):
172-
logger.debug("%s: %s", logger.name, message)
153+
# 按 raw CUlogLevel 分发;ERROR 记成 DEBUG
154+
def my_callback(user_data, cu_log_level, message, length):
155+
msg = message.decode("utf-8", errors="replace").rstrip("\n")
156+
if cu_log_level == driver.CUlogLevel.CU_LOG_LEVEL_WARNING:
157+
logger_obj.warning("%s: %s", logger_obj.name, msg)
158+
elif cu_log_level == driver.CUlogLevel.CU_LOG_LEVEL_ERROR:
159+
logger_obj.debug("%s: %s", logger_obj.name, msg)
173160

174-
# (c) register 时传入 callback,覆盖掉内部默认的
175-
# logger.log(level, "[CUDA Driver] %s", message) 转发逻辑
176161
register_cuda_error_log(logger_obj, callback=my_callback)
177162

178-
# 1. logging 默认的 WARNING 级别阈值下,DEBUG 级别的驱动日志会被过滤掉,
179-
# caplog 收不到任何记录
163+
# 默认 WARNING 阈值下,DEBUG 级别的驱动日志被过滤掉
180164
with caplog.at_level(logging.WARNING, logger=logger_name):
181165
driver.cuDeviceGet(9999)
182166
assert not any(r.name == logger_name for r in caplog.records), (
@@ -185,19 +169,19 @@ def my_callback(logger, level, message):
185169

186170
caplog.clear()
187171

188-
# 2. 把 logger 的阈值调到 DEBUG,才能收到 my_callback 记录的驱动日志
172+
# 调到 DEBUG 阈值才能收到
189173
with caplog.at_level(logging.DEBUG, logger=logger_name):
190174
driver.cuDeviceGet(9999)
191175

192176
records = [r for r in caplog.records if r.name == logger_name]
193177
assert records, "Expected driver log records forwarded via my_callback once the threshold is DEBUG"
194178

195-
# 验证 my_callback 生效:级别被统一改写成了 DEBUG(而不是默认的 ERROR/WARNING)
179+
# my_callback 生效:级别统一改成了 DEBUG
196180
assert all(r.levelno == logging.DEBUG for r in records), (
197181
f"Expected every record logged as DEBUG via my_callback, got levels: {[r.levelname for r in records]}"
198182
)
199183

200-
# 验证消息前缀是 logger name(而不是 register_cuda_error_log 默认的 "[CUDA Driver] " 前缀)
184+
# 消息前缀是 logger name,不是默认的 "[CUDA Driver] "
201185
messages = [r.message for r in records]
202186
assert any(m.startswith(f"{logger_name}: ") for m in messages), (
203187
f"Expected messages prefixed with '{logger_name}: ', got:\n{messages}"
@@ -207,7 +191,7 @@ def my_callback(logger, level, message):
207191
)
208192

209193

210-
# callback 必须是可调用对象或 None,否则应尽早报错
194+
# callback 必须可调用或 None,否则应尽早报错
211195
def test_cuda_driver_log_callback_must_be_callable_or_none():
212196
with pytest.raises(TypeError):
213197
register_cuda_error_log(logging.getLogger("cuda.driver.bad_callback_test"), callback="not-callable")

0 commit comments

Comments
 (0)