Dynamic control flow
A Python if on a runtime value picks one branch at capture. Every replay repeats that branch.
def func(idx, x, y):
if idx % 2 == 0: # ❌ Dynamic control flow!
z = x + y
else:
z = x - y
return z * x
for i in range(6):
if i < 3: z = func(i, x, y) # warmup
elif i == 3: # capture: idx=3 is odd
with torch.cuda.graph(graph):
z = func(i, x, y)
graph.replay()
else: graph.replay()def func(idx_tensor, x, y):
condition = (idx_tensor % 2 == 0) # GPU tensor
add_result = x + y
sub_result = x - y
z = torch.where(condition, add_result, sub_result) # ✅
return z * x
idx_tensor = torch.zeros(1, device="cuda", dtype=torch.int32)
for i in range(6):
idx_tensor.fill_(i) # update the GPU value every iteration
...Bug, measured. Same as the page.
All four fixes (torch.where, a branching CUDA kernel, two graphs chosen on the CPU, partial capture) gave the same correct strip.
idx % 2is evaluated by Python, once, at capture. The graph holds a subtract node and a multiply node, andidxis not an argument of either.torch.whereworks because the condition becomes a tensor. The select kernel reads it through a pointer at replay time, sofill_(i)before each replay changes the result. The cost is that both branches always run.- The custom kernel reads
idx[0]from device memory and branches per thread, so only one path does work. It must be launched ongetCurrentCUDAStream(), otherwise it is not captured (see library streams). It compiled withload_inlinefor sm_121 and ran correctly. - Two graphs (one per branch) and partial capture (branch in eager code, then a graph for the static rest) keep the decision on the CPU, outside the recording.
- CUDA 12.4 added conditional graph nodes, but the page notes that PyTorch's graph API does not expose them yet.
cases: control-flow/bug · fix-where · fix-kernel · fix-multi-graph · fix-partial