Debugging

This section presents strategies for debugging Dr.Jit-based programs that do not behave as expected.

Suppressing undefined behavior

Several operations elide bounds checks for performance reasons, which can lead to undefined behavior.

For example, calling drjit.gather() with an incorrect index could cause the operation to read beyond the end of an array, producing bogus results or even crashing the Python session due to a CPU or GPU page fault. Similarly, incorrect indices passed to drjit.scatter(), drjit.scatter_reduce(), drjit.scatter_add(), etc., might cause these operations to write beyond the end of an array and crash Python or, worse, introduce data corruption elsewhere that shows up much later.

To track down such issues, enable debug mode (drjit.JitFlag.Debug). Debug mode instruments compiled kernels with additional checks that suppress and report all undefined behavior along with the responsible Python source code location.

To enable it, set the associated flag at the beginning of your program.

dr.set_flag(drjit.JitFlag.Debug)

Alternatively, you can enable debug mode locally for a block of code.

with dr.scoped_flag(drjit.JitFlag.Debug):

    # .. code goes here

(Due to how this instrumentation works internally, Python source code locations will be tracked following the next function call)

Debug mode comes at a significant additional cost and is not a good default setting. We recommend enabling it occasionally to flush out errors.

In general, it should not be possible to crash Dr.Jit or encounter undefined behavior when debug mode is enabled. If you can break things with this flag set, then you have likely found a bug within Dr.Jit (see the next section).

Debug assertions

Dr.Jit offers the following assertion helper functions that perform additional check when the program runs in the debug mode explained above. Otherwise, they are optimized away.

A useful feature of these functions is that they also work in a symbolic context, in which case they report errors asynchronously when code eventually runs on the device.

Stepping through programs

If debug mode did not change the behavior of the program, then it may be helpful to isolate the issue using traditional debugging techniques (visualizing variable contents, setting breakpoints, and single-stepping through the program using the built-in Python debugger or an IDE such as VS Code.

Dr.Jit’s symbolic loops, conditionals, and calls can sometimes interfere with this kind of debugging methodology because they prevent access to symbolic variable contents. In this case, you can temporarily disable all symbolic program features by setting drjit.JitFlag.SymbolicLoops, drjit.JitFlag.SymbolicCalls, and drjit.JitFlag.SymbolicConditionals to False. This will switch control flow to the less efficient but functionally equivalent evaluated mode that is compatible with interactive debugging.

Localizing bugs within Dr.Jit

To debug Dr.Jit, begin making a debug build (i.e., manually compile it with -DCMAKE_BUILD_TYPE=Debug). Furthermore, you may want to enable some of the following sanitization flags:

  • DRJIT_SANITIZE_ASAN: Enable the Address Sanitizer.

  • DRJIT_SANITIZE_UBSAN: Enable the Undefined Behavior Sanitizer.

  • DRJIT_SANITIZE_INTENSE: Insert sanitization “checkpoints” into Dr.Jit that aggressively flush out undefined behavior involving its internal variable data structures. This setting only makes sense combined with ASan and/or UBSan.

Sanitizing Python sessions

Getting the sanitizers to play well with Python requires a few extra steps. First, unless you have manually compiled Python with sanitization, you will need to preload libasan using LD_PRELOAD (Linux)` or DYLD_INSERT_LIBRARIES (macOS). The precise path will depend on the details of your development environment. For example, I use the following on macOS and Linux.

# macOS
DYLD_INSERT_LIBRARIES=/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/15.0.0/lib/darwin/libclang_rt.asan_osx_dynamic.dylib python <...>

# Linux
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libasan.so.6:/usr/lib/x86_64-linux-gnu/libstdc++.so.6

On Linux, both libasan and libstdc++ or libc++ need to be preloaded at the same time (be careful to use the right version of libasan in case multiple ones are installed on your system).

On macOS, the DYLD_INSERT_LIBRARIES environment variable isn’t enough: libasan needs to be preloaded into the actual Python binary, and the python3 binary is generally just a thin wrapper. To determine the path of the actual Python executable, run whoami.py by Jonas Devlieghere.

import ctypes
dyld = ctypes.cdll.LoadLibrary('/usr/lib/system/libdyld.dylib')
namelen = ctypes.c_ulong(1024)
name = ctypes.create_string_buffer(b'\000', namelen.value)
dyld._NSGetExecutablePath(ctypes.byref(name), ctypes.byref(namelen))
print(name.value)

On my machine, this, e.g., prints b'/opt/homebrew/Cellar/python@3.12/3.12.2_1/Frameworks/Python.framework/Versions/3.12/Resources/Python.app/Contents/MacOS/Python'.

Putting both together, we can then, e.g., run the Python test suite via pytest. (Don’t forget to specify --capture no to ensure that the sanitizer messages are visible).

DYLD_INSERT_LIBRARIES=/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/15.0.0/lib/darwin/libclang_rt.asan_osx_dynamic.dylib

/opt/homebrew/Cellar/python@3.12/3.12.1/Frameworks/Python.framework/Versions/3.12/Resources/Python.app/Contents/MacOS/Python -m pytest –capture no

On Linux, ASAN conflicts with CUDA because both very aggressively map the entire virtual memory space and cause each other to run out of memory. A workaround seems to be to set the environment variable

ASAN_OPTIONS=protect_shadow_gap=0:replace_intrin=0:detect_leaks=0