| 名称 | warp-debug-gradients |
| 描述 | >- 用于诊断和修复可微分 Warp 程序中的错误梯度。 任何通过 Warp 内核进行训练、优化、标定或拟合的工作都依赖 wp.Tape 梯度, 因此除非证明不是,否则应将此类工作流的任何异常行为都视为梯度问题 —— 当训练发散或出现 NaN、完全无法训练、损失停滞或高原高于预期、 收敛到错误或有偏的答案、比参考实现更差、在小型规模下正常但在生产规模下失败、 或未通过 QA/验证复查时,请使用本技能。 也适用于显式症状 —— 梯度爆炸、NaN/inf、零或细微错误、怀疑 wp.Tape/反向传播问题、 gradcheck 失败 —— 但用户通常只描述表面症状(“模拟爆炸了”、 “拟合被拖离群点”)而不提梯度:请做出这个跳跃。 不适用于纯前向的 Warp 工作、构建/安装问题,或没有 Warp 的其他框架中的 自动梯度问题。 |
| 开源协议 | Apache-2.0 compatibility: 需要一个可用的 NVIDIA Warp 安装(最低 >= 1.13;推荐 >= 1.17 以获得可靠验证 —— copy 伴随梯度累积、覆盖警告调用点、读标志生命周期以及 gradcheck 的 restore_inputs 在 1.17 中发生变化,并在参考资料中带版本警告;在旧版本上,一个已修复的错误类别仍然存在,某些工具需要变通方法)。诊断会运行用户的复现程序,因此需要一个可用的设备(CPU 或 CUDA)。 metadata: |
| 作者 | “Warp Team warp-python@nvidia.com” |
| 版本 | “0.1.0” tags: - warp - 自动微分 - 梯度 - 可微分仿真 - 调试 upstream: https://github.com/NVIDIA/warp |
调试 Warp 中的梯度
Warp 中的梯度错误几乎从来不是数学错误。前向模拟看起来完全健康,而反向传播却在默默地读取被破坏的值、跳过数组或重复累计伴随值。用户常常花费数日调整物理参数、损失函数和资源,而真正原因是两行记录模式(taping-pattern)的修复。你的工作是用证据而不是直觉找到那个修复。
最重要的纪律:先测量,再假设。对你来说,运行一个缩小规模的复现并把自动微分与有限差分进行比较是很便宜的。梯度的错误方式(其“签名”)能比阅读代码更快地缩减假设空间。不要只凭阅读代码就开始提出修复方案——对可微性错误看似合理的诊断往往是不正确的,而一个未经证实的“修复”如果恰好扰动了数字,会浪费每个人的时间。
何时使用本技能
任何通过 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 安装不包括它):
- “可微分性”指南——尤其是“数组覆盖”、“调试梯度”、“数组覆盖跟踪”以及“限制与变通方法”(原地数学、分量赋值、动态循环)。
- FAQ 的“微分与互操作”部分——磁带保存和不保存哪些状态,以及检查点。
前提条件
执行本技能假定以下所有条件;如果缺少某一条,请向用户说明,而不是绕过去:
- 用户的脚本(或忠实的复现)在工作区中可用、可运行且可修改——诊断会多次执行它,并通过编辑来应用修复。
- 本技能的
references/文件(quick-checks.md, verification.md, custom-gradients.md, case-studies.md)随附,并在引用它们的步骤中查询。
说明
-
首先注意用户的 Warp 版本(
wp.__version__或 Warp 初始化时打印的横幅)。多个验证行为在 Warp 1.17 中发生变化——copy 伴随梯度累积、覆盖警告调用点、读标志生命周期、gradcheck的restore_inputs——参考资料中每个都标注了版本注意事项。在 Warp < 1.17 上,存在一个旧版本修复了的 bug 类(quick-checks §1 的版本注意事项),并且某些工具需要 workaround。 -
复现并缩小规模。先让你的脚本运行,然后裁剪:减少粒子/元素数量、减少时间步数、减少优化器迭代次数,如果模拟允许则使用 CPU 设备。你需要一个能在数秒内运行的复现,因为你要运行很多次。保持结构(内核数量、记录模式、缓冲区复用)不变——那里才是 bug 所在。缩小物理规模是可以的;重构数据流则不行。
如果脚本无法运行(缺少依赖、代码损坏),请将阻塞问题作为交付物报告并停止——不要继续验证一个从未运行的程序。
-
插桩并建立基准(详情和模板见
references/verification.md):- 在模块加载前设置
wp.config.verify_autograd_array_access = True,并在激活的 tape 下重新运行。捕获所有警告。这会几乎零成本地捕获最常见的一类 bug(写后读覆盖)。要了解它的盲点:它需要一个 tape,无法看到存储在 Warp 结构体中的数组,并且会禁用内核缓存(预期内核重建——只发生 JIT 模块重编译,而非本地库重建)。如果跟踪器运行干净但梯度仍然错误,请特别检查对存储在 Warp 结构体内部的数组执行原地修改是否违反了 quick-checks §1 和 Limitations,然后再信任干净的结果。 - 运行一次端到端有限差分检查:将完整前向过程(模拟步骤 + 损失)包装成 Python 可调用对象,交给
wp.autograd.gradcheck并传入真实的优化输入——它会将自动微分梯度与中心差分进行比较,并在两次评估之间恢复数组输入(Warp 1.17+),从而使得会对数组进行原地修改的前向过程也能从初始状态开始检查;在旧版 Warp 上使用references/verification.md中的手动 harness。参照物是用户在整个时段上的实际目标,与优化器实际消费的梯度比较——永远不要用更窄的窗口(见references/verification.md)。这能确认梯度是否真的错误(用户有时会误判——请诚实报告“梯度正确”的发现)并产生错误签名。references/verification.md中的模板固定了 eps/容差选择,以及随机前向所需的种子固定——不要凭肉眼根据浮点或采样噪声判断通过与失败。如果在建立基准时反向传播内存不足,请先应用下面“边界情况:内存不足”中的检查点模式再进行。
- 在模块加载前设置
-
将签名与下表匹配以对假设排序。
-
对照已知模式清单扫描代码(
references/quick-checks.md)。这对你来说很快——在同一轮中完成,但要由签名决定哪些发现是可能的原因,哪些只是附带的气味。 -
如果仍然不明确,则定位。对流程进行二分:截断到 K 步,找到 FD 与 autodiff 首次发散的位置;运行
wp.autograd.gradcheck_tape来单独测试每个被记录的 launch。记住 gradcheck_tape 是单独验证内核的——它在结构上对内核间覆盖 bug 是盲目的,因此每个内核干净通过但端到端梯度错误,指向的是记录模式,而不是内核。它还会静默跳过用enable_backward=False编译的内核(见 Limitations)——如果流程中有任何内核设置了该项,则干净的通过并不说明它;请单独验证它。 -
最小修复,然后用建立失败时完全相同的 FD harness 重新验证。没有前后 FD 比较的梯度修复不算是修复。验证你正在交付的确切程序——即当前状态下的修复文件,每一行都要包括——绝不要在诊断脚本中重新实现它:重新构建出的流程会静默丢弃你认为无关的内容,如果这个想法是错的,验证会通过而交付的代码仍然是坏的。机制上:harness 必须导入修复后的模块(或执行修复后的文件)并调用它——唯一可以放在所交付程序之外的代码只有 FD 驱动本身。同时重新运行覆盖跟踪器,确认警告已消失。“最小”适用于代码差异,而不适用于诊断:当根本原因是结构性的(例如,意外的梯度截断,quick-checks §8)时,最小的正确修复是重构——不要用只会消除表面症状的较小改动来代替。
-
在用户的原始反馈上闭环。重新运行他们实际的工作流(他们的脚本、他们打印的指标)。当用户报告的症状得到解决时,任务就完成了——一个曾经“爆炸”的优化现在应该能真正改进其目标,而不仅仅是避免 NaN。如果梯度在整个时段上都验证正确,但训练仍然失败,那是一个新的签名表条目,而不是胜利;继续诊断(或报告已验证的梯度以及剩余的非梯度原因,如学习率)。
失败签名
| 签名 | 主要假设 |
|---|---|
| 梯度恰为零 | 链中某处缺少 requires_grad=True(注意 wp.zeros 默认是 False;zeros_like/clone 继承自源);模块/内核级别启用 enable_backward=False;损失数组未连接到 tape;在 tape.zero() 之后读取梯度;链中存在分段常数算子(round/floor/sign/cast/threshold)——此时零是正确的,解决方案是替代梯度,例如直通估计器,而不是 bug 搜索(quick-checks §9c);在 Warp < 1.17 上,tape 记录的 copy/clone 的源还可能有其它下游读取者(见 references/quick-checks.md 中的版本注意事项) |
| 梯度在优化器迭代中无限增长 | 迭代之间缺少 tape.zero()/tape.reset();状态对象别名使 tape 内的覆盖跨帧携带(案例研究 1) |
| 精确小因子偏差(2x、Nx) | 双重累积:tape 上重复记录了重复的 launch——注意自 Warp 1.13 起,存储伴随在首次使用时消耗输出梯度,因此裸的重复是惰性的,除非重写的数组设置了 retain_grad=True(quick-checks §7)或 Warp 版本较旧;重叠的 tape 作用域对同一工作记录了两次。此外:反向种子与所述目标不匹配——用 ones 对逐元素损失伴随做反向传播会将 sum 反向传播,恰好是 mean 目标梯度的 N 倍 |
| NaN 或 inf | 反向传播中计算了不可微点(wp.sqrt(0)、wp.length(0)、wp.normalize(0)、除法)——需要自定义梯度(references/custom-gradients.md)或更好的稳定重写;溢出在 wp.where(是选择而不是分支——quick-checks §9b)的未选中分支中计算;动态循环局部变量在重放期间未重新计算(文档会生成 inf) |
| 微妙错误,常随步骤/迭代增多而更严重 | 写后读覆盖:wp.copy 覆盖已被读取的数组、单个 tape 内的 ping-pong 缓冲区、Python 重新绑定使两个“不同”状态成为别名(案例研究);原地 *=//=;向量/矩阵分量重新赋值;动态循环中间变量;在 Warp < 1.17 上,记录的 copy/clone 不是其源的最后消费者(references/quick-checks.md 中的版本注意事项) |
| 每个窗口 FD 一致但全时段 FD 不一致;或梯度“已验证”但优化器停滞或恶化损失 | 意外梯度截断:一个每一步一个 tape、在 tape 之间计算反向传播且状态在 tape 之间传递的循环,优化的目标不同于所报告的目标(见 quick-checks §8)。结构性修复是在整个时段上使用一个 tape 并分配 total_steps + 1 个独立状态缓冲区。求解器空间中的对应情形:在 tape 内进行部分收敛迭代求解会使 FD 和 autodiff 在错误的程序上一致——请在 tape 外收敛它,并用 warm-start 参与 tape 的迭代(quick-checks §8) |
| 梯度仅在稀疏、依赖数据的子集上不一致(相对于参考实现或运行间);前向输出与浮点精度一致 | 非光滑点处的前向选择欠定(quick-checks §9):两个答案都可以是有效的次梯度,FD 无法在拐点处裁决。请先检查不匹配的元素处的离散选择是否不同,再去找损坏的 bug |
| FD 和 autodiff 在整个时段上一致但优化仍然失败 | 不是梯度 bug。直说。看看学习率、损失景观、物理稳定性——并将验证正确的梯度作为发现报告 |
示例
一个端到端的代表性会话。用户报告“我的布料模拟训练一段时间后损失又爬上去——调整学习率没有帮助。”没有提到梯度;我们做这个跳跃是因为该工作流通过 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,在两个缓冲区之间 ping-pong,因此反向传播读取被破坏的状态。 - 最小修复:分配
num_steps + 1个独立状态缓冲区并记录在 tape 上(物理不变;只改数据流)。 - 重新验证:同一 gradcheck harness 现在通过(最大相对误差 3e-4);覆盖警告消失;用户的全尺寸训练现在单调下降。
报告:根本原因(tape 内缓冲区复用)、证据链(警告 + 前后 FD 数字)、两行 diff,以及指向“可微分性”指南的“数组覆盖”章节的指针。
报告
先给出根本原因和证据链:确立失败的 FD-vs-autodiff 数字、找到原因的警告或定位步骤、最小 diff,以及修复后的 FD 数字。指出涵盖该模式的文档章节,让用户阅读规范解释。如果你检查过干净的模板(例如,覆盖跟踪器未发现问题),请说明——这告诉用户已经排除了哪些。
如果用户只是询问他们梯度是否可信,验证后停止并报告;当他们要求修复时再实施修复。
保留证据:将诊断脚本(FD harness、缩小复现)留在工作区,并在报告中列出,而不是删除它们——它们是证据链中可复现的一半,用户或审查者应该能够重跑证明修复合理的确切验证。不要删除不是你创建的文件。
局限性
验证工具存在盲点——通过任何单个工具的干净检查并不代表完全健康(详情见 references/verification.md):
- 覆盖跟踪器需要激活的 tape,无法看到存储在 Warp 结构体内部的数组,并且在启用时会禁用内核缓存。
wp.autograd.gradcheck不接受结构体输入;请将前向包装成底层数组上的可调用对象。在 Warp < 1.17 上,它不会在两次求值之间恢复被修改的数组输入(使用手动 harness)。wp.autograd.gradcheck_tape会单独验证每个记录的 launch——它在结构上对内核间的覆盖 bug 是盲目的,并且会静默跳过以enable_backward=False编译的内核。*=//=不可微警告仅在代码生成时在wp.LOG_DEBUG下发出,因此正常运行时没有该警告不意味着什么。- Warp 没有内置的梯度检查点;长时程内存压力需要下面应用级模式。
- 在非光滑点(平局、拐点、argmin 选择),有限差分无法在有效的次梯度之间进行裁决——那里的 FD-vs-AD 不一致并非自动是 bug(quick-checks §9)。
边界情况:内存不足
如果反向传播无法分配(长时程模拟将每个中间状态都保存在 tape 上),修复方法是梯度检查点:保存周期性状态,在反向传播期间重放它们之间的段。Warp 没有内置工具——应用自行实现。使用 warp/examples/optim/example_fluid_checkpoint.py 作为参考模式,并参见 FAQ 的“微分与互操作”部分。
参考文件
references/quick-checks.md— 已知 bug 模式清单,带有文档指引和使每个模式易被忽略的注意事项。references/verification.md— 工具详情:覆盖跟踪器设置和盲点、端到端 FD harness 模板、wp.autogradgradcheck/jacobian 用法与注意事项、tape 可视化、二分。references/custom-gradients.md—@wp.func_grad、@wp.func_replay、@wp.func_native:何时需要以及如何被误用。references/case-studies.md— 两个真实的调试故事(状态别名;可微 copy 覆盖),显示表面症状有多隐蔽。当清单干净时阅读它们——它们能校准此处“细微”的含义。