PyTorch Asynchronous Assert
Introduction
When using PyTorch, sometimes we would like to check if certain variable conditions are met during the forward pass. If a PyTorch program runs on GPU, we would like to have this check to be performed asynchronously on the GPU device stream, without blocking the CPU thread. Otherwise, it is a graph break operation which can disturb the optimization of the computation graph from a neural network compiler, such as torch.compile. In PyTorch, such asynchronous assertion can be performed using the torch._assert_async API.
In this blog post, I would like to quickly discuss how to use the torch._assert_async API and how it is implemented in PyTorch.
PyTorch Asynchronous Assert
The torch._assert_async is not well documented. What’s different from the torch._assert API is that torch._assert_async accepts a boolean tensor whereas torch._assert accepts a Python boolean value. When the boolean tensor is on GPU, the assertion will be performed asynchronously on the GPU device stream. Since the assertion is performed asynchronously, if the assertion fails, the error will be reported at a later time only when the GPU stream is synchronized with the CPU thread.
In the following example, we inserted a torch._assert_async assertion in a PyTorch model. We will test what happens when the assertion fails and how it is reported asynchronously when torch.compile is used or not used.
1 | import argparse |
To profile the PyTorch program with and without using torch.compile, we could run the following commands, which will generate Perfetto profiling traces for both cases.
1 | $ python assert_async.py --profile --trace-path assert_async_trace.json |
In the profiling trace of the run that does not use torch.compile, we could see that the assertion is performed asynchronously on the GPU stream.
In the profiling trace of the run that uses torch.compile, we could see that the assertion is fused into the compiled graph with other operations, which is friendly to the optimization of the computation graph.
If an assertion fails, the error will be reported asynchronously when the GPU stream is synchronized with the CPU thread. For example, if we run the following command to trigger an assertion failure.
1 | $ python assert_async.py --trigger-fail |
On caveat is that an assertion failure will cause CUDA context to be poisoned, which will cause all subsequent CUDA calls to fail. Without restarting the CUDA context, the PyTorch program will not be able to continue on GPU. That is why it is an assertion rather than an exception which can be caught and handled.
PyTorch Asynchronous Assert Implementation
Because the purpose of assertion failure is to terminate the program, the implementation of torch._assert_async is designed to poison the CUDA context. In the CUDA operation, the __trap instruction, which translates to asm volatile("trap;") I believe, is used to terminate the program when an assertion fails. In the Triton compilation, the tl.device_assert instruction is used to terminate the program when an assertion fails.
Conclusions
The torch._assert_async calls are not completely free and it can poison the CUDA context when an assertion fails. Therefore, ideally it should be used in development, and should not be used in production because assertion should be expected to always pass. In C++, similarly, the assert will be optimized away by the compiler in release or production builds when the macro NDEBUG is defined.
PyTorch Asynchronous Assert