关于 warp-debug-gradients
面向 Warp 开发者的自动微分梯度调试技能,适用于训练和优化异常。它强调用有限差分验证、定位错误来源,并在修复后重新检查。
主要功能
- 01复现并缩小故障工作流
- 02比较自动微分与有限差分
- 03追踪 tape 期间的数组覆盖
- 04定位流水线中的梯度偏差
- 05在交付程序中重新验证修复
适用场景
- 01排查 Warp 训练停滞或发散
- 02诊断 NaN、零值或偏差梯度
- 03检查 gradcheck 失败后的梯度
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nvidia/ skills --skill warp-debug-gradients安装后在 Claude Code 中输入/warp-debug-gradients确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
BENCHMARK.mdevals/EVAL.mdevals-full.jsonevals.jsonevals/files/checkpoint_control.pycontact.pydac.pydeblur.pyemitters.pygain_train.pyheightfit.pyhuber_fit.pylattice.pylens_calibrate.pyevals/files/compound/objective.pyphysics.pystate.pytrain.pyevals/files/compreassign/model.pytrain.pyevals/files/dynloop/model.pytrain.pyevals/files/funcgradassign/model.pytrain.pyevals/files/inplacemul/model.pytrain.pyevals/files/missingzero/model.pytrain.pyevals/files/nangrad/model.pytrain.py详细介绍
在 Warp 中调试梯度
Warp 中的梯度错误几乎从来不是数学错误。正向模拟看起来完全正常,但反向传播会悄悄读取被覆盖的值、跳过数组,或重复累计伴随量。用户常常花上好几天调整物理参数、损失函数和资源,而真正的原因可能只是需要修改两行的记录模式。你的任务是用证据找到这个修复,而不是凭直觉判断。
最重要的一条准则:先测量,再提出假设。运行一个缩小版复现并将自动微分结果与有限差分结果进行比较,成本很低。梯度出错的方式(即其特征)能够比阅读代码更快地缩小假设范围。不要只凭阅读代码就开始提出修复方案——看似合理的可微性错误诊断往往是错的,而未经验证却碰巧扰动了数值的“修复”只会浪费所有人的时间。
何时使用此技能
任何通过 Warp 内核训练、优化、校准或拟合的流程,都会使用 wp.Tape 梯度——因此,当这类工作流出现异常时,即使用户没有提到梯度,首要嫌疑也应是梯度。遇到用户实际报告的症状时启用:训练发散、出现 NaN 或毫无进展;损失停滞,或在高于预期的值处进入平台期;拟合收敛到错误或有偏差的答案,或效果差于参考实现;管线在小规模下正常运行,但在生产规模下失败,或未能通过 QA 复检。也应在出现明确的梯度症状时启用——梯度爆炸、NaN/inf、梯度为零或存在细微错误、wp.autograd.gradcheck 失败、怀疑 wp.Tape/反向传播存在问题——以及用户询问梯度是否可信时启用。
对于仅涉及正向计算的 Warp 工作(编写内核、渲染、性能调优)、Warp 构建或安装问题、与 Warp 无关的其他框架中的自动微分问题,或用户已验证梯度正确、仅针对反向传播进行的纯性能优化,不要启用此技能。
权威背景资料是 Warp 自己的文档——在该领域诊断问题前,请先查阅相关部分(在线文档位于
https://nvidia.github.io/warp/stable/;在 Warp 源码检出版本中,相同内容位于
docs/user_guide/ 下;pip 安装包不包含这些文档):
- “Differentiability”指南——尤其是“Array Overwrites”、“Debugging Gradients”、“Array Overwrite Tracking”和“Limitations and Workarounds” (原地数学运算、分量赋值、动态循环)。
- FAQ 中的“Differentiation and Interoperability”章节——了解 tape 会保留和不会保留哪些状态,以及检查点。
前置条件
执行此技能需要满足以下所有条件;如果缺少任何一项,应告知用户,而不是自行变通:
- 用户的脚本(或忠实复现版本)在工作区中可用、可运行且可修改——诊断过程需要反复执行脚本并修改它以应用修复。
- 此技能的
references/文件(quick-checks.md、verification.md、custom-gradients.md、case-studies.md)随附,并且在引用这些文件的步骤中会查阅它们。
说明
-
首先记录用户的 Warp 版本(
wp.__version__或 Warp 初始化时打印的横幅)。Warp 1.17 中有多项验证行为发生了变化——copy-adjoint 累积、覆盖警告的触发位置、读取标记的生命周期、gradcheck的restore_inputs——参考资料会针对每项标注版本注意事项。在 Warp < 1.17 中,存在一个较大的错误类别,已在后续版本中修复(quick-checks §1 的版本注意事项),并且某些工具需要变通方案。 -
复现并缩小问题。 先让用户的脚本运行起来,再缩小规模: 减少粒子/元素数量、时间步数和优化器迭代次数;如果模拟器支持,则使用 CPU 设备。你需要一个能在几秒内运行的复现用例,因为你要反复运行它。保留原有结构(内核数量、记录模式、缓冲区复用)——问题就出在这些地方。 缩小物理模拟规模没有问题;重构数据流则不行。
如果无法让脚本运行(缺少依赖、代码损坏),请将阻碍问题作为交付结果报告,然后停止——不要继续验证一个从未运行过的程序。
-
添加检测并确立真实基准(详情和模板见
references/verification.md):- 在模块加载前设置
wp.config.verify_autograd_array_access = True,然后在活跃的 tape 下重新运行。捕获每条警告。这几乎不费力就能发现最常见的一类错误(读后写覆盖)。也要了解它的盲区:需要 tape 才能工作;看不到存储在 Warp 结构体中的数组;而且会禁用内核缓存(预计会重新构建内核——这只是 JIT 模块重新编译,不是重新构建原生库)。如果检测器运行正常,但梯度仍然错误,请务必检查 Warp 结构体中数组的原地修改情况(快速检查 §1 和「局限性」),再相信检测结果正常。 - 执行一次端到端有限差分检查:将完整前向传播(模拟步数 + 损失)封装成 Python 可调用对象,并将其交给
wp.autograd.gradcheck,使用真实的优化输入——它会将自动微分梯度与中心差分进行比较,并在每次评估前还原数组输入(Warp 1.17+),因此会从未修改的初始状态检查会进行原地修改的前向传播;旧版 Warp 请使用references/verification.md中的手动测试框架。参考值是用户在整个时间范围内的实际目标函数,比较对象则是优化器实际使用的梯度——绝不能使用更短的时间窗口(见references/verification.md)。这能确认梯度确实错误(用户有时对此判断有误——请如实报告「梯度正确」的发现),并得出误差特征。references/verification.md中的模板规定了 eps/容差的选择,以及随机前向传播所需的种子固定方法——不要凭直觉根据浮点误差或采样噪声判断通过与否。如果在确立真实基准时,反向传播因内存不足而失败,请先应用下文「边缘情况:内存不足」中的检查点模式,再继续。
- 在模块加载前设置
-
将误差特征与下表进行匹配,以确定假设的优先级。
-
根据已知模式检查清单扫描代码 (
references/quick-checks.md)。这很快——扫描时一并完成,但要根据误差特征判断哪些发现可能是原因,哪些只是无关的可疑之处。 -
如仍无法确定,则定位问题。 对流水线进行二分:截断到 K 步,找出有限差分与自动微分首次出现分歧的位置;运行
wp.autograd.gradcheck_tape,逐个测试记录的启动操作。请记住,gradcheck_tape 会逐个验证内核——它在结构上无法检测内核之间的覆盖,因此,单个内核检查正常但端到端梯度错误,说明问题出在记录模式,而非内核本身。它还会静默地跳过使用enable_backward=False编译的内核(见「局限性」)——如果流水线中的任何内核设置了该选项,检查通过也说明不了什么;请单独验证这些内核。 -
只做必要的修复,然后重新验证,使用与确认故障时完全相同的 FD 测试框架。没有修复前后的 FD 对比,就不算修复。验证你实际要交付的程序——当前修复后的文件,包含每一行——绝不要在诊断脚本中重新实现它:重建的流程会悄悄丢掉你认为无关的内容;如果判断有误,验证就会通过,而交付的代码仍然有问题。具体来说:测试框架必须 导入修复后的模块(或执行修复后的文件),并调用其中的代码——交付程序之外唯一可以存在的代码就是 FD 驱动程序。还要重新运行覆盖跟踪器,确认警告已经消失。“只做必要的修复”适用于代码差异,而非诊断过程: 当根本原因是结构性的(例如,意外截断梯度,快速检查 §8)时,最小的正确修复就是重构——不要用更小的改动来掩盖表面症状。
-
针对用户最初的问题闭环验证。 重新运行用户的实际工作流(他们的脚本和打印出的指标)。只有当他们报告的症状得到解决,任务才算完成——如果某个优化过程“爆炸”,现在就应该能够明确证明它改善了目标函数,而不仅仅是不再出现 NaN。如果在完整时间范围内梯度验证正确,但训练仍然失败,那就是签名表中的新条目,并非胜利;继续诊断(或报告已验证的梯度,以及仍然存在的非梯度原因,例如学习率)。
故障特征
| 特征 | 首要假设 |
|---|---|
| 梯度恰好为零 | 链路中某处缺少 requires_grad=True(注意,wp.zeros 默认值为 False;zeros_like/clone 会继承源数组的设置);模块/内核级别设置了 enable_backward=False;损失数组未连接到 tape;在调用 tape.zero() 后读取梯度;链路中包含分段常数运算(round/floor/sign/cast/threshold)——这种情况下,零梯度是正确的,解决方法应是使用直通估计器等替代梯度,而非排查错误(快速检查 §9c);在 Warp < 1.17 中,记录在 tape 上的 copy/clone,其源数组还有其他下游读取者(参见 references/quick-checks.md 中的版本注意事项) |
| 梯度在多次优化器迭代中不断增大且不受限制 | 各次迭代之间缺少 tape.zero()/tape.reset();状态对象别名导致 tape 内的覆盖跨帧延续(案例研究 1) |
| 偏差恰好是一个较小的倍数(2x、Nx) | 重复累积:重复的 launch 被记录在 tape 上——注意,自 Warp 1.13 起,store 的伴随运算会在首次使用时消耗输出梯度,因此单纯重复操作不会生效,除非被重写的数组设置了 retain_grad=True(快速检查 §7),或 Warp 版本较旧;重叠的 tape 作用域将同一操作记录了两次。另外:反向传播种子与声明的目标函数不匹配——为逐元素损失的伴随量设置全 1 种子,会反向传播总和,其梯度恰好是均值目标函数梯度的 N 倍 |
| NaN 或 inf | 在反向传播期间计算了不可微点(wp.sqrt(0)、wp.length(0)、wp.normalize(0)、除法)——需要自定义梯度(references/custom-gradients.md),更好的做法是采用数值稳定的重写方式;在 wp.where 的未选中分支中计算了溢出(这是选择操作而非分支——快速检查 §9b);动态循环中的局部变量在重放期间未重新计算(已知会产生 inf) |
| 结果略有错误,且步数/迭代次数越多通常越差 | 先读后写覆盖:对已读取的数组执行 wp.copy,在单个 tape 内使用乒乓缓冲区,Python 重新绑定导致两个“不同”状态互为别名(案例研究);原地执行 *=//=;重新赋值向量/矩阵分量;动态循环中间值;在 Warp < 1.17 中,记录的 copy/clone 不是其源数组的最后一个使用者(references/quick-checks.md 中的版本注意事项) |
| 分窗口 FD 一致,但完整时域 FD 不一致;或者梯度虽已“验证”,优化器却停滞或损失变大 | 意外的梯度截断:在每步一个 tape 的循环中于循环内执行反向传播,并在多个 tape 之间传递状态,会优化一个与所报告目标不同的目标(参见快速检查 §8)。结构性修复方法是使用一个 tape 覆盖整个时域,并准备 total_steps + 1 个互不相同的状态缓冲区。求解器空间中的类似问题:tape 内未充分收敛的迭代求解,会导致 FD 与自动微分对错误的程序达成一致——在 tape 外使其收敛,并为 tape 内的迭代提供热启动(快速检查 §8) |
| 梯度仅在由数据决定的稀疏子集上不一致(与参考实现或不同运行结果相比);前向输出在浮点精度范围内匹配 | 非光滑点上的前向选择不唯一(快速检查 §9):两个答案都可能是有效的次梯度,且有限差分(FD)无法在尖点处作出判断。排查数据损坏前,先检查不匹配元素的离散选择是否恰好不同 |
| FD 与自动微分在完整时域上达成一致,但优化仍然失败 | 这不是梯度错误。明确说明这一点。检查学习率、损失曲面和物理稳定性——并将已验证正确的梯度作为结论报告 |
示例
一个具有代表性的完整会话。用户报告:“我的布料仿真训练了一段时间后,损失又开始缓慢上升——调整学习率也没用。”用户没有提到梯度;之所以想到这一点,是因为该工作流通过 Warp 内核进行优化。
- 用户的脚本每次迭代运行 512 个粒子、200 个步。缩减为 16 个粒子、10 个步,并在 CPU 上运行——现在复现约需 2 秒,且出现相同的损失上升现象。
- 在 tape 下设置
wp.config.verify_autograd_array_access = True,打印出:array ... was read from kernel integrate and is now being written to by kernel integrate——发生了读后写覆盖。 - 对缩减后的复现用例运行端到端的
wp.autograd.gradcheck:相对误差最大值为 0.4,而有限差分结果作为对照。确认梯度有误,并呈现出“细微错误,且步数越多越严重”的特征。 - 根据特征行和 quick-checks §1,问题指向单个 tape 内的缓冲区复用:仿真步骤按
state_a → state_b → state_a切换,乒乓使用两个缓冲区,因此反向传播会读取已被覆盖的状态。 - 最小修复:分配
num_steps + 1个互不相同、记录在 tape 上的状态缓冲区(不改动物理过程;只改变数据流)。 - 重新验证:相同的 gradcheck 测试框架现在通过(相对误差最大值为 3e-4);覆盖警告消失;用户的完整规模训练运行现在呈单调下降。
报告内容:根本原因(tape 内缓冲区复用)、证据链(警告和修复前后的 FD 数值)、两行差异,以及指向 Differentiability 指南中“Array Overwrites”一节的链接。
报告
先说明根本原因和证据链:用于确认故障的 FD 与自动微分数值、找到原因的警告或定位步骤、最小差异,以及修复后的 FD 数值。注明介绍该问题模式的文档章节,方便用户阅读权威说明。如果检查过的模式没有发现问题(例如,覆盖跟踪器没有发现任何情况),也要说明——这能告诉用户哪些可能性已被排除。
如果用户只是想知道自己的梯度是否可信,请在验证并报告后停止;只有用户要求修复时才应用修复。
保留证据:将诊断脚本(FD 测试框架、缩减后的复现用例)留在工作区,并在报告中列出,而不是删除——它们是证据链中可复现的部分,用户或审查者应能重新运行完全相同的验证,以确认修复的依据。切勿删除自己未创建的文件。
限制
验证工具存在盲区——任何单个工具检查通过,都不能说明一切正常(详情见
references/verification.md):
- 覆盖跟踪器需要 tape 处于活动状态,无法查看存储在 Warp 结构体内的数组,并且启用时会禁用内核缓存。
wp.autograd.gradcheck不接受结构体输入;请将前向计算封装为一个以底层数组为输入的可调用对象。在 Warp < 1.17 中,它不会在每次评估之间恢复被修改的数组输入(请使用手动测试框架)。wp.autograd.gradcheck_tape会逐个验证记录的启动过程——从结构上无法发现内核间覆盖错误,并会静默跳过使用enable_backward=False编译的内核。*=//=不可微警告仅在代码生成时、wp.LOG_DEBUG模式下发出,因此在常规运行中没有警告并不代表任何事情。- Warp 没有内置的梯度检查点功能;长时域运行的内存压力需要使用下面的应用层模式。
- 在非光滑点(平局、尖点、argmin 选择)处,有限差分无法判定多个有效次梯度中的哪一个正确——此时 FD 与 AD 不一致并不一定是缺陷(quick-checks §9)。
边缘情况:内存不足
如果反向传播无法分配内存(长时间模拟会使每个中间状态都保留在磁带上),解决方法是梯度检查点:保存周期性状态,并在反向传播期间重新执行这些状态之间的片段。Warp 没有内置工具——应用程序需要自行实现。请将
warp/examples/optim/example_fluid_checkpoint.py 用作参考模式,并参阅常见问题解答中的“微分与互操作”一节。
参考文件
references/quick-checks.md— 已知错误模式检查清单,附有文档指引和各模式容易被忽略的注意事项。references/verification.md— 工具详情:覆盖追踪器的设置和盲点、端到端有限差分测试框架模板、wp.autograd梯度检查/雅可比矩阵的用法和注意事项、磁带可视化、二分查找。references/custom-gradients.md—@wp.func_grad、@wp.func_replay、@wp.func_native:何时需要使用,以及常见误用方式。references/case-studies.md— 两个真实的调试案例(状态别名;可微复制覆盖),展示表面症状有多么隐蔽。检查清单未发现问题时请阅读这些案例——它们有助于理解这里所说的“隐蔽”程度。