CudaGraphModule#
- class tensordict.nn.CudaGraphModule(module: Callable[[List[Tensor] | TensorDictBase], None], warmup: int = 2, in_keys: List[NestedKey] = None, out_keys: List[NestedKey] = None, device: device | None = None)#
Bases:
objectRecords a function, or a TensorDictModule, as a CUDA graph and replays it.
A replay runs all the recorded kernels with one CPU call, which removes the CPU overhead of launching them one by one. The first
warmup - 1calls run the function on a side stream. The next call records the graph, and later calls copy their inputs into the graph’s buffers and replay it. Each call checks that the inputs match the recorded ones (see the rules below). See CUDA graphs for a worked example.Without CUDA, the constructor warns, and each call runs the function directly.
Warning
CudaGraphModuleis a prototype feature and its API restrictions are likely to change in the future.Warning
The wrapped function must meet the requirements below.
CudaGraphModuledoes not check all of them: a function that breaks one can give wrong results.The function cannot have dynamic control flow. For instance, the following code snippet will fail to be wrapped in
CudaGraphModule:def func(x): if x.norm() > 1: return x + 1 else: return x - 1
Use
torch.where()instead:def func(x): return torch.where(x.norm() > 1, x + 1, x - 1)
The function must execute a code that can be exactly re-run using the same buffers. This means that dynamic shapes (changing shape in the input or during the code execution) is not supported. In other words, the input must have a constant shape. After the capture, a tensor input with a different shape, or a tensordict input with a different batch size or an entry of a different shape, raises a
ValueError, and a tensordict input that lacks one of thein_keysraises aKeyError.The output of the function must be detached. If a call to the optimizers is required, put it in the input function. For instance, the following function is a valid operator:
def func(x, y): optim.zero_grad() loss_val = loss_fn(x, y) loss_val.backward() optim.step() return loss_val.detach()
The inputs must not require gradients. If the function uses
nn.Parameterinstances (or other tensors that require gradients), do not pass them as arguments of the call. Read them from the enclosing scope, or bind them as default values, as indef func(x, w=w):w = nn.Parameter(torch.randn((), device="cuda")) optim = torch.optim.Adam([w], lr=1) def func(x): # right: w is not an argument of the call optim.zero_grad() (x * w).backward() optim.step() def func(x, w): # wrong: w is passed at each call ...
Tensor and tensordict arguments can change between calls, if their device and shape stay the same. Arguments that are not tensors (a
float, astr) must not change after the capture. If the function takes tensors, a changed value raises aValueError. If it takes a tensordict, the replay uses the captured value, ignores the new one and emits aFutureWarning. Starting with TensorDict 0.17, the default will change to raising aValueErrorthere too (pytorch/tensordict#2075). The keyword arguments of a tensordict module are itsin_keys(seedispatch()) andtensordict_out.If the module is a
TensorDictModuleBaseinstance and the output id matches the input id, then this identity will be preserved during a call toCudaGraphModule, and the outputs written into the input are clones of the graph’s output buffers. In all other cases, the output will be cloned, irrespective of whether its elements match or do not match one of the inputs.In-place writes to the input tensors inside the function (e.g.
x.add_(1)) do not reach the caller’s inputs once the graph is captured: the graph reads and writes its own copies of the inputs.The inputs are copied into the graph’s buffers, and the graph is replayed, on the current stream. As for any CUDA operation, inputs written on another stream must be ordered before the call by the caller, e.g. with
torch.cuda.current_stream().wait_stream(producer_stream). Consecutive calls share the graph’s buffers: issue them from one stream, or order each call after the previous one in the same way.The replay repeats the branch that each
ProbabilisticTensorDictModuletook at capture: a graph captured underset_interaction_type("random")keeps sampling underset_interaction_type("deterministic"). The first replay under another interaction type emits aUserWarning. The check covers only theProbabilisticTensorDictModuleinstances inside the wrappedModule: other code that branches oninteraction_type()(for example an exploration module) and wrapped functions that are not modules are not checked; if the wrapped module holds noProbabilisticTensorDictModule, the replay repeats the captured branch without a warning. Capture oneCudaGraphModuleper interaction type whenever the output depends on it.
Warning
CudaGraphModuleis not anModuleby design, to discourage gathering parameters of the input module and passing them to an optimizer.- Parameters:
module (Callable) – a function that takes tensors (or a tensordict) and returns a tensor, a tensordict, or a pytree of them, such as a tuple or a dict. A tensordict module can also be called with keyword arguments (see the example).
warmup (int, optional) – the call that records the graph. The first
warmup - 1calls run the function on a side stream, the next call records the graph, and later calls replay it. A compiled function needs enough calls to finish compiling before the capture, for exampleCudaGraphModule(torch.compile(module), warmup=3). Defaults to2.in_keys (list of NestedKey, optional) – the input keys, if the module takes a tensordict. If
modulehas anin_keysattribute, that attribute is used and this argument is ignored, with aUserWarningif the argument is a non-empty str, list, tuple or set of keys that differs from the attribute. An empty list marks a function that takes a tensordict, without keyword calls. Defaults toNone, which means that the function takes tensors.out_keys (list of NestedKey, optional) – the output keys, if the module returns a tensordict. If
modulehas anout_keysattribute, that attribute is used and this argument is ignored, with aUserWarningif the argument is a non-empty str, list, tuple or set of keys that differs from the attribute. Defaults toNone, which becomes[]whenin_keysis given.device (device, optional) – the device of the CUDA streams that run the warmup and the capture. Defaults to
None, the current CUDA device.
Examples
The example needs a CUDA device, so its lines are skipped when the docs are tested.
>>> import torch >>> from tensordict import TensorDict >>> from tensordict.nn import CudaGraphModule, TensorDictModule >>> # Wrap a simple function >>> def func(x): ... return x + 1 >>> func = CudaGraphModule(func) >>> x = torch.rand((), device='cuda') >>> out = func(x) >>> assert isinstance(out, torch.Tensor) >>> assert out == x+1 >>> # Wrap a tensordict module >>> func = TensorDictModule(lambda x: x+1, in_keys=["x"], out_keys=["y"]) >>> func = CudaGraphModule(func) >>> # This can be called either with a TensorDict or regular keyword arguments alike >>> y = func(x=x) >>> td = TensorDict(x=x) >>> td = func(td)
Note
Tips on debugging CudaGraphModule errors:
Debug errors such as
operation would make the legacy stream depend on a capturing blocking streamon a version of your code that is not compiled first: compiled code hides the part of the code that creates the cross-stream dependency. The cause is often an operation between devices, which makes the capturing stream depend on other streams.Errors such as
Cannot call CUDAGeneratorImpl::current_seed during CUDA graph capture, or other errors that come from the compiler (not the compiled code), usually mean that the graph was captured while the function was recompiling. List the recompiles withTORCH_LOGS="+recompiles" python myscript.pyand remove them, or use more warmup calls.
- load_state_dict(state_dict)#
Loads
state_dictinto the wrapped module. Does nothing if the module has noload_state_dict.
- state_dict()#
Returns the
state_dictof the wrapped module, or an empty dict if it has none.