[Bug已解决] CUDA Graph 重放深度展开 RNN 训练时报非法地址(cudaErrorIllegalAddress / SIGSEGV)解决方案
一、现象长什么样
当你用 CUDA Graph 把「多个训练迭代 + 深度展开(deeply unrolled)的循环神经网络(RNN / LSTM)」录制下来,然后重放(replay)时,可能遇到:
cudaErrorIllegalAddress严重时甚至进程直接段错误:
SIGSEGV (segmentation fault)这就是官方描述的场景:
SIGSEGV / cudaErrorIllegalAddress when replaying a CUDA graph that captured multiple training iterations of two trainers over deeply unrolled recurrent modules核心问题:CUDA Graph 被设计成「录制一段固定形状、固定内存布局的操作序列」,而深度展开的 RNN 训练循环里,内存访问模式复杂、迭代间有依赖、且可能隐含动态形状,一旦被「录制—重放」,很容易在重放时访问到录制期不存在 / 已失效的内存,触发非法地址甚至段错误。
本文讲清楚 CUDA Graph 的捕获约束、为什么深度展开 RNN 训练特别容易踩、以及如何安全地用图捕获加速。
二、CUDA Graph 捕获的硬性约束(回顾)
CUDA Graph 把一串操作「录制」成图,之后整体重放。捕获期间有严格限制:
- 内存地址固定:录制时分配 / 引用的每一块显存,重放时必须还有效且在原位;
- 形状固定:不能出现录制时没出现的动态形状;
- 不能有 host 端回调 / 跨流全局同步;
- 不能有动态分配(malloc/free);
- 不能有依赖「录制期外部状态」的操作。
违反任意一条,重放时就会访问非法地址。
三、为什么「深度展开 RNN + 多训练迭代」特别危险
3.1 深度展开本身内存复杂
一个「深度展开(deeply unrolled)」的 RNN,会把时间步T个副本全部摊平成一张大图:
h_0 -> h_1 -> h_2 -> ... -> h_T每个时间步都有权重、隐藏态、梯度。展开越深,图越大、中间 buffer 越多。这些 buffer 在录制期分配,重放期必须全部有效。
3.2 「多个训练迭代」叠加
如果你把「多个训练迭代(forward + backward + step)」都录进一个图,那图里包含:多次 forward 的激活、多次 backward 的梯度、优化器状态更新。这些 buffer 数量爆炸,且迭代之间共享的优化器状态 / 梯度累加器在录制期就固定了地址——但如果某次重放时这些状态被外部改动(比如另一个 trainer 共享了参数),地址就对不上了。
3.3 「两个 trainer」共享参数
报错里提到「two trainers」。两个 trainer 若共享同一份参数 / 优化器状态,而 CUDA Graph 捕获时把地址「钉死」在录制瞬间,另一个 trainer 更新了参数,图重放仍访问旧地址 → 非法访问 →cudaErrorIllegalAddress/SIGSEGV。
四、可运行:正确的 CUDA Graph 捕获范式(单迭代、固定形状)
下面给出一个真实可运行的、安全的 CUDA Graph 捕获示例(仅捕获单个推理/单步,避开「多迭代 + 深度展开」的雷区):
import torch import torch.nn as nn def safe_graph_capture(): if not torch.cuda.is_available(): print("无 GPU,跳过演示。") return model = nn.Linear(128, 128).cuda() x = torch.randn(16, 128, device="cuda") out = [None] # 先 warmup(让 allocator / autotune 稳定) with torch.no_grad(): for _ in range(3): out[0] = model(x) # 用 torch.cuda.graph 捕获(推荐的高层 API) g = torch.cuda.CUDAGraph() with torch.cuda.graph(g): out[0] = model(x) # 捕获单步,x 地址固定 # 重放:只改 x 内容,不改 x 地址 with torch.no_grad(): for _ in range(5): x.copy_(torch.randn(16, 128, device="cuda")) g.replay() # out[0] 已更新 print("图重放完成,输出形状:", out[0].shape) if __name__ == "__main__": safe_graph_capture()关键点:捕获的是「单步 + 固定输入地址」的图,重放时只改x的内容(x.copy_),不换x这个张量对象(地址不变)。这是 CUDA Graph 的安全用法。
五、解决方案一:不要捕获「多训练迭代」,只捕获单步
针对报错的「captured multiple training iterations」,最稳的做法是把图的范围缩小到单个 forward(或单个 step),而不是把整个训练循环录进去:
# ❌ 危险:把多个迭代 forward+backward 都录进一个图 # with torch.cuda.graph(g): # for _ in range(N): # loss = model(batch[i]); loss.backward(); opt.step() # ✅ 安全:只捕获单步 forward g = torch.cuda.CUDAGraph() static_input = torch.randn(16, 128, device="cuda") with torch.cuda.graph(g): static_out = model(static_input) # 重放时只 copy_ 新数据训练循环仍在外面跑(每个迭代g.replay()拿 forward 结果,再单独 backward),而不是把 backward 也塞进图。
六、解决方案二:深度展开 RNN 用torch.utils.checkpoint(梯度检查点)替代全展开
深度展开 RNN 的内存爆炸,正是非法地址的温床。改用梯度检查点(gradient checkpointing),只保存部分时间步的激活,反向时重算其余:
import torch from torch.utils.checkpoint import checkpoint def rnn_cell(x, h, w_ih, w_hh, b): return torch.tanh(x @ w_ih.t() + h @ w_hh.t() + b) def unrolled_rnn(x_seq, h0, params, checkpoint_seg=4): h = h0 for t in range(0, x_seq.size(0), checkpoint_seg): # 把一段放进 checkpoint,不存全部中间激活 seg = x_seq[t:t+checkpoint_seg] h = checkpoint(lambda s, h0: _run_seg(s, h0, params), seg, h) return h def _run_seg(seg, h, params): for i in range(seg.size(0)): h = rnn_cell(seg[i], h, params["w_ih"], params["w_hh"], params["b"]) return h梯度检查点减少了「被捕获/固定」的中间 buffer 数量,从而降低 CUDA Graph 捕获时的内存风险。注意:用checkpoint时通常不把整段包进 CUDA Graph,二者本就冲突(checkpoint 有重算,动态性强)。
七、解决方案三:两个 trainer 不要共享被图捕获的参数地址
如果有两个 trainer 共享参数,且其中一个用 CUDA Graph 捕获了参数地址,另一个更新参数后图就失效。解法:
- 让 CUDA Graph 只捕获「不跨 trainer 共享」的局部 buffer;
- 或在图捕获前确保参数地址已稳定,且捕获期间不被另一个 trainer 改动;
- 或干脆两个 trainer 各自独立参数 / 各自图。
# 每个 trainer 用独立的静态输入缓冲,地址互不干扰 static_a = torch.randn(16, 128, device="cuda") static_b = torch.randn(16, 128, device="cuda") graph_a = torch.cuda.CUDAGraph() with torch.cuda.graph(graph_a): out_a = trainer_a(static_a) # trainer_b 用 static_b / graph_b,不共享 static_a 的地址八、解决方案四:捕获前 warmup,避免 allocator 抖动
CUDA Graph 捕获前务必 warmup,让 PyTorch 的 allocator 稳定、避免捕获期发生意料外的分配:
model.cuda() for _ in range(3): # warmup with torch.no_grad(): model(static_input) torch.cuda.synchronize() # 之后再 capture否则 warmup 没做,捕获期第一次见到某形状才分配,重放时该分配不在图里 → 非法访问。
九、诊断:确认是图重放非法访问
- 报错是
cudaErrorIllegalAddress或进程SIGSEGV; - 发生在
g.replay()阶段,而非 capture 阶段; - 代码捕获了「多训练迭代 / 深度展开 RNN / 共享参数」;
- 关掉 CUDA Graph(退回 eager)后不报错。
全中即说明是图重放期的非法内存访问,按上述方案缩小捕获范围、固定地址、避免共享。
十、小结
CUDA Graph 重放深度展开 RNN 训练报cudaErrorIllegalAddress/SIGSEGV,根因是图捕获了过多、过复杂的状态(多迭代、深展开、共享参数),重放时内存地址对不上。应对:
- 只捕获单步,不要把多个训练迭代塞进一个图(第五节);
- 深度展开 RNN 用梯度检查点减少被固定的 buffer(第六节);
- 两个 trainer不要共享被图捕获的参数地址(第七节);
- 捕获前充分warmup(第八节);
- 用
torch.cuda.graph高层 API,只copy_改数据、不换张量对象。
CUDA Graph 是双刃剑:它把「固定、简单、可重放」的操作加速到极致,但只要你让它捕获「动态、共享、跨迭代」的状态,非法地址就来了。捕获范围越小、地址越固定,图越安全。