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
189from __future__ import annotations
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
151124def 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
0 commit comments