Rust in Everything
基于 tenferro-runtime 0.8.0

SVD 的梯度全是 NaN

主线位置:traced 主线的 grad,经过 tenferro-linalg 的 svd。SVD 的反向规则、F 矩阵的展宽、derivative_eps 和一张简并、零奇异值的实测表见 线性代数 的「SVD 的反向规则」;规则怎么注册见 semantic AD 与扩展。

flowchart TB
  a["4x3 矩阵 s3 恰为 0"] --> svd["svd"]
  svd --> rule["linearize_svd"]
  rule --> f["F 和 1/s 有 eps 展宽"]
  rule --> proj["m 大于 n 的投影项"]
  proj --> div["除以 s 无展宽"]
  div --> nan["0 除 0 得 NaN"]
  nan --> all["整个梯度 NaN"]

实验 crate exp-tenferro8/pit2-exp,bin p23_svd_nan,CPU(CpuEngine::serial()),debug,M2 Max、macOS 27、rustc 1.99.0。

现象

4×3 的对角矩阵 diag(2, 1, 0),损失只依赖前两个左奇异向量:L = Σ W ∘ (U₂ U₂ᵀ),U₂ 是 U 的前两列(用掩码乘出来),W 是固定的权重。这个函数在这一点可微,中心差分给出的梯度是有限的:

let (u, _s, _vt) = x.svd()?;                     // 或 svd_with_options(SvdOptions::default().derivative_eps(1e-6))
let u2 = (&u * &mask)?;                          // 只留前两列
let p = u2.dot_general(&u2, /* 收缩第 1 维 */)?;   // U2 U2^T
let loss = (&p * &w)?.reduce_sum(None)?;
let g = ad.grad(&loss, &x)?;                     // ad 带 linalg 和 fft 的规则
central difference = [0.000000, 0.000000, -0.600000, 0.700000, 0.000000, 0.000000, 1.400000, -0.400000, 0.000000, 0.000000, 0.000000, 0.000000]
4x3 diag(2,1,0): s = [2.0, 1.0, 0.0], 12/12 NaN, grad = [NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN]
4x3 diag(2,1,0) eps=1e-6: s = [2.0, 1.0, 0.0], 12/12 NaN, grad = [NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN]
  • 12 个分量全是 NaN,不报错。
  • 损失根本没用到第三个奇异向量,掩码已经把它乘成了 0。
  • 调大 derivative_eps 不起作用。

转置过来的 3×4(n > m)同样会中招,只是换成了 V 一侧:

3x4 transpose, loss on U: s = [2.0, 1.0, 0.0], 0/12 NaN, grad = [0.000000, 0.000000, 0.200000, 0.000000, 0.000000, -0.600000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000]
3x4 transpose, loss on Vt: 12/12 NaN

对角矩阵、补零的数据、秩亏损的设计矩阵都会让 faer 给出恰好为 0 的奇异值;同一组奇异值经过正交变换后,faer 算出的是 1e-16 量级的数,梯度就是有限的(线性代数 的表)。所以这个坑看起来像是「偶尔出现」。

出处

linearize_svd 里,奇异值两两之间的系数 F 和 1/s 都写成了带 ε² 的形式,间隙或奇异值为 0 时不会除以零;derivative_eps 调的就是这个 ε。例外是 m > n 时 dU 的投影修正 (I − U Uᴴ) dA V S⁻¹,这一行 直接除以原始的 s:

            let proj = linear_sub(builder, d_av, u_ut_d_av);
            let s_broadcast = broadcast_in_dim_fixed(
                builder,
                s.clone(),
                matrix_shape(m, &k, batch_shape),
                vector_to_matrix_broadcast_dims(batch_shape.len()),
            );
            let s_broadcast =
                convert_fixed_ref_to_dtype(builder, ValueRef::Local(s_broadcast), s_dtype, dtype);
            let correction = linear_div_fixed(builder, proj, s_broadcast);

n > m 时 dV 的修正(第 888 行)也一样。s₃ = 0 时第三列是 0/0;掩码在反向里把那一列的余切乘 0,可 0 · NaN 还是 NaN,再经过矩阵乘扩散到梯度的每个元素。

修正项只在用到的那一侧生成:dU 在 u_active 时才建,dV 在 vt_active 时才建。所以 3×4 的情形损失只用 U 时没有问题,用 Vt 时就是 NaN。方阵两项都没有,不受影响。同一份规则在 F 和 1/s 上都做了展宽,这里漏了,是 0.8.0 仍在的上游 bug(0.7.1 一样)。JAX 0.12.0.1 的 SVD 求导规则在这一项上也直接除以 s,同样的零列矩阵在 JAX 里梯度也全是 NaN,所以这不是 tenferro 独有的问题,只是 tenferro 别处都做了展宽,这一项显得不一致。

修法

两种绕法,都让投影项不再除以 0:

// 1. 补一列 0,变成方阵:没有 m > n 的投影项
let mut sq = a.clone();
sq.extend([0.0; 4]);
report(&mut cpu, "4x4 padded", 4, 4, &sq, None)?;

// 2. 把恰好为 0 的那一维扰动成极小的数
let mut j = a.clone();
j[2 + 8] = 1e-12;
report(&mut cpu, "4x3 diag(2,1,1e-12)", 4, 3, &j, None)?;
4x4 padded: s = [2.0, 1.0, 0.0, 0.0], 0/16 NaN, grad = [0.000000, 0.000000, -0.600000, 0.700000, 0.000000, 0.000000, 1.400000, -0.400000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000]
4x3 diag(2,1,1e-12): s = [2.0, 1.0, 1e-12], 0/12 NaN, grad = [0.000000, 0.000000, -0.600000, 0.700000, 0.000000, 0.000000, 1.400000, -0.400000, -0.000000, 0.000000, 0.000000, 0.000000]

两种都和中心差分一致。补零后的梯度是 4×4,前 12 个(列主序的前三列)就是原矩阵的梯度。补零更干净,不改变前向结果;扰动简单,但要保证扰动量远小于数据本身的尺度。

如果损失真的依赖那个零奇异值对应的向量(比如用全部的 U Uᴴ),这个函数在秩亏损处本身不可微,绕过 NaN 之后得到的是 1e15 量级的数,也不能用(线性代数 的表)。重复奇异值不出 NaN,而是 1e9 量级的梯度,原因和对策见同一页的动脑筋答案。

怎么确认没中招

  • 求完梯度检查 g.iter().any(|v| v.is_nan()),再看一眼 s 里有没有恰好等于 0.0 的值。
  • 非方阵的 SVD 求导,测试数据别只用随机矩阵:加一组对角或秩亏损的输入,并且分别测只用 U 和只用 Vt 的损失。
  • 梯度的量级突然到 1e9 以上,打印相邻奇异值的差。

小结

  • 非方阵里恰好为 0 的奇异值让 SVD 的整个梯度变成 NaN:m > n 时 dU、n > m 时 dV 的投影修正项直接除以 s,没有 ε 展宽,derivative_eps 管不到。0.8.0 仍在的上游 bug。
  • 只在损失用到的那一侧出现:4×3 用 U、3×4 用 Vt 时中招。
  • 绕法:补零成方阵,或把零奇异值扰动成 1e-12 这样的小数;两种梯度都和差分一致。

💬 关联讨论

发起讨论
加载中...