unsupported vjp AD rule for tenferro-fft.rfft.v1
主线位置:traced 主线的
grad,经过tenferro-fft。FFT 的两个 CPU 后端和它的 AD 规则见 FFT 的「AD 规则:只有复到复」;规则怎么注册见 semantic AD 与扩展。
flowchart TB
x["实数 x"] --> r["rfft"]
r --> lr["linearize"]
lr --> chk{"is_c2c"}
chk -->|否| err["unsupported vjp"]
x --> cv["convert C64"]
cv --> f["fft 复到复"]
f --> chk
chk -->|是| ok["梯度"]
实验 crate exp-tenferro8/pit2-exp,bin p26_rfft,CPU(CpuEngine::serial()),debug,M2 Max、macOS 27、rustc 1.99.0。
现象
8 个实数,损失是 rfft 各频点模长的平方和。AdContext 里已经放了 linalg 和 FFT 的规则,Runtime 也装了 FFT 的执行模块:
let mut rules = tenferro_linalg::semantic_ad_rules()?;
rules.merge(tenferro_fft::semantic_ad_rules()?)?;
let ad = AdContext::builder().with_semantic_extension_rules(rules)?.build()?;
let t = TracedTensor::from_vec_col_major(vec![8], x0.clone())?;
let m = t.rfft(None, 0, FftNorm::Backward)?.abs()?;
let l = (&m * &m)?.reduce_sum(None)?;
match ad.grad(&l, &t) { /* 打印 */ }
rfft loss = [18.543607973750376]
rfft grad: Err(unsupported vjp AD rule for tenferro-fft.rfft.v1)
UnsupportedAdRule { transform: "vjp", op: "tenferro-fft.rfft.v1" }
fft of real input grad: Err(unsupported vjp AD rule for tenferro-fft.rfft.v1)
irfft loss = [11.375]
irfft grad: Err(unsupported vjp AD rule for tenferro-fft.irfft.v1)
- 和 规则没注册 时的报错一模一样,可这里规则是注册了的。区别只在 family 名:注册时用的是
tenferro-fft.fft.v1,报错里按操作写成了tenferro-fft.rfft.v1。 - 对实数张量直接调
fft(第二行)也是rfft.v1:实数输入的整长变换走的是实到复的路径。 - 反方向的
irfft(5 个复数的叶子,irfft(Some(8))后求平方和)同样不行。
出处
FFT 的规则三种角色都注册了,但 linearize 一开头就拒绝非复到复的变换:
if !fft_op.operation.is_c2c() {
return Err(semantic_fft_unsupported(
fft_op.operation,
SemanticAdRuleKind::Linearize,
));
}
转置和直接 VJP 共用的伴随也有同样的判断。semantic_fft_unsupported 返回 SemanticAdError::Unsupported,family 名按具体操作写;traced 一侧把它和「规则缺失」改写成同一个 Error::UnsupportedAdRule,所以两种情况分不出来。复到复 FFT 的伴随还是一个 FFT;rfft 只输出 n/2 + 1 个频点,伴随要按频点修正权重(直流和偶数长度时的 Nyquist 点只算一次,其余算两次),0.8.0 没写这一步(FFT 的「没有蠢问题」)。
修法
先 convert(DType::C64),用复到复的 fft。复到复的结果有 8 个频点,前 5 个就是 rfft 的输出,用一个 0/1 掩码只留它们,损失和原来相同:
let t = TracedTensor::from_vec_col_major(vec![8], x.to_vec())?;
let m = t.convert(DType::C64)?.fft(None, 0, FftNorm::Backward)?.abs()?;
let keep = TracedTensor::from_vec_col_major(vec![8], vec![1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0])?;
let l = (&(&m * &m)? * &keep)?.reduce_sum(None)?;
let g = cpu.eval(&ad.grad(&l, &t)?)?;
c2c loss = [18.543607973750373]
c2c grad = [9.021465, 5.986081, -0.796152, -6.219376, -6.152603, -0.673165, 6.099008, 9.012070]
max |grad - fd| = 4.87e-9
- 损失和
rfft版只差最后一位(复到复多算了一半频点,舍入顺序不同)。 - 梯度和中心差分(步长 1e-6)差 4.9e-9,而且是实数(按
f64读出来),可以直接用来更新实数参数。 - 代价是算了两倍的频点,内存也翻倍,用不上 realfft 的半长变换。
怎么确认没中招
- 搜
rfft、irfft,以及对实数张量调用的fft:只要它们在求导路径上,就要改写。 - 看到
unsupported vjp AD rule for tenferro-fft.*,先看.后面的操作名:是rfft/irfft就是这一条,是fft则多半是用了TracedTensorAdExt::grad、规则集是空的。 - 改写后用中心差分核对一次梯度。
小结
tenferro-fft的规则只支持复到复变换;rfft、实数输入的fft求导报unsupported vjp AD rule for tenferro-fft.rfft.v1,irfft报...irfft.v1,规则注册了也一样。- 报错和「规则没注册」是同一个变体,看 family 里的操作名区分。
- 先
convert(DType::C64)再用复到复fft,用掩码留下前n/2 + 1个频点;梯度与差分差 4.9e-9。
💬 关联讨论
发起讨论加载中...