fandhe_ai_autodiff/functional_ops.rs
1//! 関数型 AD ラッパー `vjp`・`hvp`・`vmap`(イシュー #2874/#2875/#2876・親 #2841。契約の正は
2//! `docs/autodiff-functional-transforms-design.md` §3〜§7・§10)。
3//!
4//! **新規 `Op`・`BackendOps` メソッド・VJP・`AutodiffError` variant はゼロ**:
5//! 勾配追跡なしの余接定数葉 `u` と `output.mul(u)` を 1 本足し、既存の
6//! [`Tape::backward`] を 1 回呼ぶだけの reverse-mode 合成(PyTorch
7//! `torch.autograd.grad(outputs, inputs, grad_outputs=u)` 相当)。`Tape::backward` は
8//! 非スカラー loss に全要素 1 のシードを使い、これが暗黙の総和射影になるため
9//! `sum` ノードは足さない(`jacobian_ops` の `FlatElements::element` と同じ理由)。
10//! 既存の Op ごとの VJP ディスパッチャ(`grad.rs` の `pub(crate) fn vjp`)とは
11//! 別物で、本モジュールは「出力と余接ベクトルの組」を受ける利用者向けの合成。
12//!
13//! **公開状況**: facade では `Tape::vjp`/`Tape::hvp`/`Tape::vmap` として公開済み(イシュー #2931・
14//! 設計記録 `docs/autodiff-functional-transforms-design.md` §23〜§24。本モジュールの自由関数への薄い委譲)。
15//! モジュール自体の再エクスポート・裸の自由関数・`Var`/`Tensor<f32>` 上の同名メソッドは引き続き未承認で、
16//! facade の `FunctionalTransformsHoldDoctestGuard` と `tests/api_surface.rs` のガードが機械固定している。
17//! `hvp`(#2875)は `backward_create_graph` で子テープへ 1 階勾配を
18//! 写し、子テープ上で `g ⊙ v` を `child.backward` する合成(既存の手組み HVP と同じ形)。
19//! ループ版 `vmap`(#2876)は `unbind`→クロージャ適用→`contiguous`→`stack` の合成で、
20//! 検査ヘルパー(`jacobian_ops` の `checked_numel`・`check_on_tape`・`copy_grad_row`)を共用する。
21//!
22//! **double-VJP 法の `jvp`/`jacfwd`(イシュー #2940・親 #2939。設計 §8・§19)**:
23//! forward-mode の新規実装は持たず、`u` を追跡ありの葉とした `s = output ⊙ u` の 1 階勾配
24//! `g = ∂s/∂input = Jᵀu`(`u` について線形)を `backward_create_graph` で子テープへ写し、
25//! 第 2 段の VJP `∂(g ⊙ v)/∂u = J·v` を取る。facade の `Tape::jvp`/`Tape::jacfwd` として
26//! 公開済み(イシュー #2956・設計記録 §28)。facade からは薄い 1 行委譲で呼ばれる。
27//! `Tape` 以外の経路(モジュール再エクスポート・裸の自由関数・`Var`/`Tensor<f32>` 上の同名メソッド)は
28//! 引き続き未承認で、facade の保留ガードが機械固定している。
29//!
30//! 複数入力版 `jvp_multi`(イシュー #3063・設計記録 §30)は同じ前段(`double_vjp_front`)を共用し、
31//! 入力ごとの `g_i` を子テープ上でスカラー和へ畳んで `child.backward` を 1 回だけ呼ぶ。facade の
32//! `Tape::jvp_multi` から 1 行委譲で呼ばれる。
33//!
34//! 微分可能な `jvp_create_graph`(イシュー #3067・設計記録 §31)は同じ前段に加え、2 段目の VJP を
35//! `child.backward_create_graph` で孫テープへ `Var` 演算として記録し、結果 `J·v` を追跡された `Var` で返す。
36//! facade への公開は #3068 で扱う(本モジュールは autodiff 内部と crate ルート再エクスポートまで)。
37//!
38//! **共通の契約**: `jvp_multi` 以外は単一入力・f32 の [`Tape`] のみ(`VarF64` は対象外)。`vjp`/`hvp` の結果は
39//! 非微分のホスト値(`vmap` のみ同じテープ上の微分可能な `Var`。`vmap_grad`〈#3071〉は子テープで勾配を取る非微分の `Tensor<f32>`)。resident・fused 経路・checkpoint・`DeviceMismatch` は既存 `mul`/`backward`
40//! の挙動をそのまま伝播する。
41
42use crate::create_graph::CreateGraphResult;
43use crate::error::AutodiffError;
44use crate::jacobian_ops::{FlatElements, check_on_tape, checked_numel, copy_grad_row};
45use crate::tape::{Tape, TapeId};
46use crate::var::Var;
47use fandhe_ai_tensor_core::{ShapeError, Tensor};
48
49/// ベクトル・ヤコビアン積 `Σ_i cotangent[i] · ∂output[i]/∂input`(転置ヤコビアン積
50/// `Jᵀu`)。戻り値は shape が `input.shape()` の非微分ホスト値。
51///
52/// **入口検査(テープへノードを足す前。順序固定)**:
53/// 1. `output`/`input` が `tape` の現世代に属さない → `Err(TapeMismatch)`。
54/// 2. `input.requires_grad() == false` → `Err(GradientTrackingDisabled)`。
55/// 3. `cotangent.shape() != output.shape()` → `Err(Shape(ShapeMismatch))`
56/// (ブロードキャストは許さない。`[1]` が `[3]` に黙って通るのを防ぐ)。
57/// 4. 要素数を `checked_numel` で検査(オーバーフローは `Err(Shape(ElementCountOverflow))`)。
58/// 5. 要素数 0、または `output` が `input` に構造的に依存しない
59/// (`output.requires_grad() == false`)→ テープに触れず全ゼロを返す
60/// (素の `backward` は追跡なしの loss を `Err` にするため先に分岐する)。
61///
62/// 本体はテープへちょうど 2 ノード(余接の定数葉と `mul`)を足し、`backward` を 1 回呼ぶ。
63/// `backward` 等が途中で `Err` を返してもこの 2 ノードは残る(既存ノードの値は不変)。
64/// 勾配が `input` へ届かない場合は全ゼロ。`cotangent` の非有限値は検査せずそのまま伝播する。
65pub fn vjp(
66 tape: &Tape,
67 output: &Var<'_>,
68 input: &Var<'_>,
69 cotangent: &Tensor<f32>,
70) -> Result<Tensor<f32>, AutodiffError> {
71 check_on_tape(tape, output)?;
72 check_on_tape(tape, input)?;
73 if !input.requires_grad() {
74 return Err(AutodiffError::GradientTrackingDisabled);
75 }
76 let out_shape = output.shape();
77 if cotangent.shape() != out_shape.as_slice() {
78 return Err(AutodiffError::Shape(ShapeError::ShapeMismatch {
79 lhs: out_shape,
80 rhs: cotangent.shape().to_vec(),
81 }));
82 }
83 let in_shape = input.shape();
84 let n = checked_numel(&in_shape)?;
85 let m = checked_numel(&out_shape)?;
86 if n == 0 || m == 0 || !output.requires_grad() {
87 return Tensor::zeros(&in_shape).map_err(AutodiffError::Shape);
88 }
89
90 let u = tape.var_no_grad(cotangent);
91 let weighted = output.mul(&u)?;
92 let grads = tape.backward(&weighted)?;
93 let mut data = vec![0.0f32; n];
94 if let Some(g) = grads.get(input)? {
95 copy_grad_row(g, &mut data)?;
96 }
97 Tensor::new(data, &in_shape).map_err(AutodiffError::Shape)
98}
99
100/// Hessian-vector product `Σ_j v_j · ∂g_j/∂input`(`g = ∂loss/∂input`。C² の範囲では
101/// `H·v`)。戻り値は shape が `input.shape()` の非微分ホスト値。`child` は呼び出し側が
102/// [`Tape::new_with_ops`] 等で構築した空の子テープ([`Tape::backward_create_graph`] と
103/// 同じ契約)。`jacobian_ops::hessian` と `v` の積と REQ-2 統一複合判定で一致する。
104///
105/// **入口検査(`backward_create_graph` を呼ぶ前。失敗時は親・子テープとも無変更。順序固定)**:
106/// 1. `loss`/`input` が `tape` の現世代に属さない → `Err(TapeMismatch)`。
107/// 2. `input.requires_grad() == false` → `Err(GradientTrackingDisabled)`。
108/// 3. `loss` の要素数が 1 でない → `Err(InvalidArgument)`(`[]`・`[1]`・`[1, 1]` は可)。
109/// 4. `vector.shape() != input.shape()` → `Err(Shape(ShapeMismatch))`(ブロードキャスト不可)。
110/// 5. `input` の要素数 0 → 空テンソル(`child` は検査も変更もしない)。
111///
112/// 以降は `backward_create_graph` の既存検査(`supports_create_graph() == false` の Op・
113/// rank 3 以上の `MatMul`・非空の子テープ・デバイス不一致・追跡なし `loss` 等)をそのまま
114/// 伝播する(新しい検査・variant は足さない)。追跡なしの `loss` は `vjp` が全ゼロを返すのと
115/// 異なり `hessian` と同じく `Err` になる。1 階勾配が `input` へ届かない、または定数
116/// (`input` に線形な `loss`)の場合は全ゼロ。
117///
118/// 親テープへノードは足さない。子テープには写し・1 階勾配に加えてちょうど 2 ノード
119/// (`vector` の定数葉と `mul`)が残り、途中で `Err` でもそれまでの分は残る。呼び出し後の
120/// `child` は再利用せず作り直す。`mul` 結果は非スカラーだが `Tape::backward` が全要素 1 の
121/// シードを使うため暗黙に総和され、`sum` ノードは足さない(`vjp` と同じ理由)。
122/// `vector` の非有限値は検査せず伝播する。子テープ上の数値方式は 1 階 VJP と bit 同一を
123/// 主張しない(`create_graph` の既存契約)。
124pub fn hvp(
125 tape: &Tape,
126 loss: &Var<'_>,
127 input: &Var<'_>,
128 vector: &Tensor<f32>,
129 child: &Tape,
130) -> Result<Tensor<f32>, AutodiffError> {
131 check_on_tape(tape, loss)?;
132 check_on_tape(tape, input)?;
133 if !input.requires_grad() {
134 return Err(AutodiffError::GradientTrackingDisabled);
135 }
136 let loss_shape = loss.shape();
137 if checked_numel(&loss_shape)? != 1 {
138 return Err(AutodiffError::InvalidArgument(format!(
139 "hvp: loss の要素数は 1 である必要がある(shape {loss_shape:?})"
140 )));
141 }
142 let in_shape = input.shape();
143 if vector.shape() != in_shape.as_slice() {
144 return Err(AutodiffError::Shape(ShapeError::ShapeMismatch {
145 lhs: in_shape,
146 rhs: vector.shape().to_vec(),
147 }));
148 }
149 let n = checked_numel(&in_shape)?;
150 if n == 0 {
151 return Tensor::zeros(&in_shape).map_err(AutodiffError::Shape);
152 }
153
154 let cg = tape.backward_create_graph(loss, child)?;
155 let (Some(g), Some(cx)) = (cg.grad(input)?, cg.child_var(input)?) else {
156 return Tensor::zeros(&in_shape).map_err(AutodiffError::Shape);
157 };
158 if !g.requires_grad() {
159 return Tensor::zeros(&in_shape).map_err(AutodiffError::Shape);
160 }
161 let vc = child.var_no_grad(vector);
162 let prod = g.mul(&vc)?;
163 let grads = child.backward(&prod)?;
164 let mut data = vec![0.0f32; n];
165 if let Some(h) = grads.get(&cx)? {
166 copy_grad_row(h, &mut data)?;
167 }
168 Tensor::new(data, &in_shape).map_err(AutodiffError::Shape)
169}
170
171/// ループ版 `vmap`(`torch.func.vmap` 相当の意味論)。`input` を `in_dim` 軸で `unbind` し、
172/// 各スライスへクロージャ `f` を順に適用して、結果を先頭軸(dim 0)へ `stack` した
173/// 同じテープ上の微分可能な [`Var`] を返す。新規 `Op`・VJP・variant は足さず、既存の
174/// `unbind`・`contiguous`・`stack` の合成のみ。性能は保証しない(ループ実行)。
175///
176/// 前提: `f` は副作用のない関数であること。バッチなしで実行した結果との一致は REQ-2 の
177/// 統一複合判定で見る(bit 一致は契約にしない。設計 §11-2)。`vmap(grad)`
178/// (クロージャ内で `backward` を呼ぶ per-sample gradient)は本関数の契約外で、子テープで勾配を取る
179/// 別関数 [`vmap_grad`](#3071・設計 §32)を使う。
180///
181/// **入口検査(順序固定)**
182///
183/// Phase A(テープ無変更。失敗しても `tape.len()` は不変):
184/// 1. `input` が `tape` の現世代に属さない → `Err(TapeMismatch)`。
185/// 2. `in_dim >= rank`(rank 0 を含む)→ `Err(Shape(AxisOutOfRange))`。
186/// 3. `shape[in_dim] == 0`(空バッチ)→ `Err(InvalidArgument)`。結果形状は推定しない。
187/// `unbind` は零長軸に `Ok(vec![])` を返すため、`unbind` より前に自前で検査する。
188///
189/// Phase B(ノードが積まれる段階。`vmap` 自身の後処理〈`contiguous`・`stack`〉の前に全検査を終える):
190/// 4. 各スライスについて `f` を呼び、`Err` はそのまま伝播する。
191/// 5. 各戻り値が `tape` の現世代に属さない → `Err(TapeMismatch)`(形状検査より先)。
192/// 6. 戻り値の shape が先頭出力と異なる → `Err(Shape(ShapeMismatch))`。
193/// 7. 結果の要素数 `B × numel(out_0)` を検査(`Err(Shape(ElementCountOverflow))`)。
194///
195/// **テープに残るノード**: Phase A の失敗では何も残らない。Phase B で失敗した場合
196/// (`f` の `Err`・別テープの出力・形状不一致・`contiguous`/`stack` の失敗〈`DeviceMismatch` 等の伝播〉)、
197/// `unbind` が積んだノード(スライスあたり最大 3)、それまでに `f` が積んだノード、
198/// `contiguous` 化の途中までのノードが残る。既存ノードの値は変えない。
199///
200/// 非 contiguous なクロージャ出力は `contiguous` で materialize してから `stack` する
201/// (`stack` は非 contiguous 要素を `NonContiguousReshape` で拒否するため)。
202/// 出力軸は先頭固定で `out_dim`・複数入力は扱わない(設計 §11-1)。
203pub fn vmap<'t, F>(
204 tape: &'t Tape,
205 input: &Var<'t>,
206 in_dim: usize,
207 mut f: F,
208) -> Result<Var<'t>, AutodiffError>
209where
210 F: FnMut(&Var<'t>) -> Result<Var<'t>, AutodiffError>,
211{
212 // Phase A
213 check_on_tape(tape, input)?;
214 let in_shape = input.shape();
215 let rank = in_shape.len();
216 if in_dim >= rank {
217 return Err(AutodiffError::Shape(ShapeError::AxisOutOfRange {
218 axis: in_dim,
219 rank,
220 }));
221 }
222 let batch = in_shape[in_dim];
223 if batch == 0 {
224 return Err(AutodiffError::InvalidArgument(format!(
225 "vmap: in_dim={in_dim} の軸長が 0(空バッチは結果形状を推定できないため拒否。shape {in_shape:?})"
226 )));
227 }
228
229 // Phase B
230 let slices = input.unbind(in_dim)?;
231 let mut outs: Vec<Var<'t>> = Vec::with_capacity(slices.len());
232 for s in &slices {
233 let out = f(s)?;
234 check_on_tape(tape, &out)?;
235 if let Some(first) = outs.first() {
236 let (lhs, rhs) = (first.shape(), out.shape());
237 if lhs != rhs {
238 return Err(AutodiffError::Shape(ShapeError::ShapeMismatch { lhs, rhs }));
239 }
240 }
241 outs.push(out);
242 }
243 let out_numel = checked_numel(&outs[0].shape())?;
244 out_numel
245 .checked_mul(outs.len())
246 .ok_or(AutodiffError::Shape(ShapeError::ElementCountOverflow))?;
247 let contiguous: Vec<Var<'t>> = outs
248 .iter()
249 .map(|o| o.contiguous())
250 .collect::<Result<_, _>>()?;
251 Var::stack(&contiguous, 0)
252}
253
254/// 子テープで勾配を取るクロージャ型の `vmap`(`torch.func` の `vmap(grad(f))` 相当のループ版。
255/// イシュー #3071・設計記録 `docs/autodiff-functional-transforms-design.md` §32)。結果は非微分。
256///
257/// `input` を `in_dim` 軸でサンプルへ分け、サンプルごとに呼び出し側が渡した空の子テープ `child` へ
258/// スライスを追跡ありの葉として登録し、クロージャが返した 1 要素の損失を内部で [`Tape::backward`] して
259/// スライス葉の勾配を dim 0 へ積んだ `Tensor<f32>`(shape は `[B] ++ slice_shape`)を返す。
260/// 新規 `Op`・VJP・variant は足さない。facade への委譲は #3072。
261///
262/// **子テープの契約**: 呼び出し時に空であること。サンプルごとに `reset` のあと入口の葉プレフィックス状態へ
263/// 戻すので、戻り時(成功・失敗とも)の `child` は空で、`reset` はサンプル数(失敗時は開始済みの数)だけ
264/// 世代を進める。ピークメモリはサンプル 1 個分のグラフ。クロージャが panic した場合の状態は契約外
265/// ([`vmap`] と同じ)。HRTB のためクロージャはサンプルをまたいで `Var` を保持できず、定数は
266/// サンプルごとに `t.var_no_grad` 等で登録し直す。
267///
268/// **入口検査(順序固定)**
269///
270/// Phase A(子テープ無変更):
271/// 1. `in_dim >= rank`(rank 0 を含む)→ `Err(Shape(AxisOutOfRange))`。
272/// 2. `shape[in_dim] == 0` → `Err(InvalidArgument)`。
273/// 3. 結果要素数のオーバーフロー → `Err(Shape(ElementCountOverflow))`。
274/// 4. `child` が空でない → `Err(Backward)`。
275///
276/// Phase B(サンプル順。最初の違反で止める):
277/// 5. クロージャの `Err` はそのまま伝播。
278/// 6. 損失が `child` の現世代に属さない → `Err(TapeMismatch)`(要素数検査より先)。
279/// 7. 損失の要素数が 1 でない → `Err(InvalidArgument)`(素の `backward` は非スカラーを暗黙に総和するため先に拒否)。
280/// 8. 損失が追跡なし、またはスライスへ勾配が届かない → その行は全ゼロ。
281/// 9. `backward`・バックエンド・hook の `Err` はそのまま伝播。
282///
283/// 数値は明示ループ参照と REQ-2 統一複合判定で一致する(bit 一致は契約にしない)。経路上の Op は 1 階
284/// `backward` が対応する範囲。非有限値は検査せず伝播する。微分可能版は含めない(§32.5)。共有パラメータに
285/// ついてのサンプルごとの勾配は返さない(§32.8-1)。
286pub fn vmap_grad<F>(
287 input: &Tensor<f32>,
288 in_dim: usize,
289 child: &mut Tape,
290 mut f: F,
291) -> Result<Tensor<f32>, AutodiffError>
292where
293 F: for<'c> FnMut(&'c Tape, &Var<'c>) -> Result<Var<'c>, AutodiffError>,
294{
295 // Phase A
296 let in_shape = input.shape();
297 let rank = in_shape.len();
298 if in_dim >= rank {
299 return Err(AutodiffError::Shape(ShapeError::AxisOutOfRange {
300 axis: in_dim,
301 rank,
302 }));
303 }
304 let batch = in_shape[in_dim];
305 if batch == 0 {
306 return Err(AutodiffError::InvalidArgument(format!(
307 "vmap_grad: in_dim={in_dim} の軸長が 0(空バッチは結果形状を推定できないため拒否。shape {in_shape:?})"
308 )));
309 }
310 let slice_shape: Vec<usize> = in_shape
311 .iter()
312 .enumerate()
313 .filter(|&(a, _)| a != in_dim)
314 .map(|(_, &d)| d)
315 .collect();
316 let slice_numel = checked_numel(&slice_shape)?;
317 let total = slice_numel
318 .checked_mul(batch)
319 .ok_or(AutodiffError::Shape(ShapeError::ElementCountOverflow))?;
320 if !child.is_empty() {
321 return Err(AutodiffError::Backward(format!(
322 "vmap_grad: 子テープは空である必要がある(ノード数 {})",
323 child.len()
324 )));
325 }
326 let start = child.rollback_point();
327
328 // Phase B
329 let mut data = vec![0.0f32; total];
330 for i in 0..batch {
331 let lo = i * slice_numel;
332 let r = match data.get_mut(lo..lo + slice_numel) {
333 Some(row) => vmap_grad_sample(&*child, input, in_dim, i, &slice_shape, &mut f, row),
334 None => Err(AutodiffError::Backward(
335 "vmap_grad: 結果バッファの行範囲が不正".to_string(),
336 )),
337 };
338 // 成否に関わらず次サンプル・呼び出し元へ空の子テープを渡す(hook・世代・葉の持ち越しを断つ)。
339 child.reset();
340 child.rollback_to(start);
341 r?;
342 }
343 let mut out_shape = Vec::with_capacity(slice_shape.len() + 1);
344 out_shape.push(batch);
345 out_shape.extend_from_slice(&slice_shape);
346 Tensor::new(data, &out_shape).map_err(AutodiffError::Shape)
347}
348
349/// `vmap_grad` のサンプル 1 個分(スライス登録→クロージャ→検査→`backward`→行コピー)。戻り値が寿命を
350/// 持たないので、呼び出し側は直後に `child.reset()`(`&mut`)を呼べる。
351fn vmap_grad_sample<F>(
352 child: &Tape,
353 input: &Tensor<f32>,
354 in_dim: usize,
355 i: usize,
356 slice_shape: &[usize],
357 f: &mut F,
358 row: &mut [f32],
359) -> Result<(), AutodiffError>
360where
361 F: for<'c> FnMut(&'c Tape, &Var<'c>) -> Result<Var<'c>, AutodiffError>,
362{
363 let slice = input
364 .narrow(in_dim, i, 1)
365 .map_err(AutodiffError::Shape)?
366 .contiguous()
367 .reshape(slice_shape)
368 .map_err(AutodiffError::Shape)?;
369 let s = child.var(&slice);
370 let loss = f(child, &s)?;
371 check_on_tape(child, &loss)?;
372 let numel = checked_numel(&loss.shape())?;
373 if numel != 1 {
374 return Err(AutodiffError::InvalidArgument(format!(
375 "vmap_grad: 損失は 1 要素である必要がある(shape {:?})",
376 loss.shape()
377 )));
378 }
379 if !loss.requires_grad() {
380 return Ok(());
381 }
382 let grads = child.backward(&loss)?;
383 if let Some(g) = grads.get(&s)? {
384 copy_grad_row(g, row)?;
385 }
386 Ok(())
387}
388
389/// double-VJP 法の前段(`jvp`/`jacfwd` 共通。#2940)。親テープへ追跡ありの葉 `u`(全要素 1)と
390/// `s = output ⊙ u` を足し、`backward_create_graph` で `g = Jᵀu` を子テープへ写して
391/// `(g, child 上の u)` を返す。`g` は `u` について線形なので `u` の値は結果に影響しない
392/// (全要素 1 で固定)。`J ≡ 0`(`input` へ勾配が届かない・`g` が定数)は `Ok(None)`。
393/// `child_var(u)` が `None` になるのは構造上の不変条件違反で、ゼロへ丸めず `Err(Backward)` にする。
394/// 親テープへは `backward_create_graph` の検査より前にちょうど 2 ノードが積まれる(失敗時も残る)。
395fn double_vjp_stage<'c>(
396 tape: &Tape,
397 output: &Var<'_>,
398 input: &Var<'_>,
399 child: &'c Tape,
400) -> Result<Option<(Var<'c>, Var<'c>)>, AutodiffError> {
401 let (cg, cu) = double_vjp_front(tape, output, child)?;
402 match cg.grad(input)? {
403 Some(g) if g.requires_grad() => Ok(Some((g, cu))),
404 _ => Ok(None),
405 }
406}
407
408/// double-VJP 法の前段の本体(`double_vjp_stage`・`jvp_multi` 共通。#3063)。親テープへ追跡ありの葉 `u` と
409/// `s = output ⊙ u` を足して `backward_create_graph` を 1 回だけ呼び、`(CreateGraphResult, child 上の u)` を返す。
410/// 入力ごとの `g_i = J_iᵀu` の取り出しは呼び出し側が行う。親テープへの操作列は単一入力版と同一。
411fn double_vjp_front<'c>(
412 tape: &Tape,
413 output: &Var<'_>,
414 child: &'c Tape,
415) -> Result<(CreateGraphResult<'c>, Var<'c>), AutodiffError> {
416 let u = tape.var(&Tensor::ones(&output.shape()).map_err(AutodiffError::Shape)?);
417 let s = output.mul(&u)?;
418 let cg = tape.backward_create_graph(&s, child)?;
419 let Some(cu) = cg.child_var(&u)? else {
420 return Err(AutodiffError::Backward(
421 "double-VJP: 余接葉 u の子テープ写しが存在しない(不変条件違反)".to_string(),
422 ));
423 };
424 Ok((cg, cu))
425}
426
427/// 列 `col`(長さ `m`)を行優先の結果 `data`(`m × n`)の第 `k` 列へ書く。長さ不一致は型付きエラー。
428fn write_column(
429 data: &mut [f32],
430 n: usize,
431 k: usize,
432 col: &Tensor<f32>,
433) -> Result<(), AutodiffError> {
434 let host = col.host_slice();
435 let rows = data.len().checked_div(n).unwrap_or(0);
436 if host.len() != rows || k >= n {
437 return Err(AutodiffError::Backward(format!(
438 "jacfwd: 列の要素数({})が出力の要素数({rows})と一致しない、または列添字 {k} が範囲外",
439 host.len()
440 )));
441 }
442 for (i, v) in host.iter().enumerate() {
443 data[i * n + k] = *v;
444 }
445 Ok(())
446}
447
448/// ヤコビアン・ベクトル積 `J·v`(`J = ∂output/∂input`)を double-VJP 法で求める
449/// (イシュー #2940。`jacobian_ops::jacobian` の `J·v` と REQ-2 統一複合判定で一致する)。
450/// 戻り値は shape が `output.shape()` の非微分ホスト値。`child` は呼び出し側が
451/// [`Tape::new_with_ops`] 等で構築した空の子テープ([`Tape::backward_create_graph`] と同じ契約)で、
452/// 呼び出し後は再利用しない。
453///
454/// **入口検査(ノードを足す前。順序固定。失敗時は親・子テープとも無変更)**:
455/// 1. `output`/`input` が `tape` の現世代に属さない → `Err(TapeMismatch)`。
456/// 2. `input.requires_grad() == false` → `Err(GradientTrackingDisabled)`。
457/// 3. `tangent.shape() != input.shape()` → `Err(Shape(ShapeMismatch))`(ブロードキャスト不可)。
458/// 4. 要素数を `checked_numel` で検査(`Err(Shape(ElementCountOverflow))`)。
459/// 5. 要素数 0、または `output.requires_grad() == false` → テープに触れず全ゼロ(`child` も検査しない。
460/// `jacobian`/`vjp` と同じ意味論で、追跡なし loss を `Err` にする `hvp` とは非対称)。
461///
462/// 以降は `backward_create_graph` の既存検査(`supports_create_graph() == false` の Op・rank 3 以上の
463/// `MatMul`・非空の子テープ・同一テープ・デバイス不一致・checkpoint 登録済み)をそのまま `Err(Backward)`
464/// 等で伝播する(新しい拒否ロジック・variant は足さない)。拒否時 `child` は空のまま。
465///
466/// **副作用**: 前段以降の失敗でも親テープへちょうど 2 ノード(全要素 1 の追跡あり葉 `u` と `mul`)が残る
467/// (既存ノードの値は不変)。成功時の子テープには写し・1 階勾配・第 2 段の補助ノードが残る。
468/// 子テープ上の数値は 1 階 VJP や `jacobian` と bit 同一を主張しない。非有限値は検査せず伝播する。
469/// 計算量は親 backward 1 回+子 backward 1 回。f32 の [`Tape`] のみ(`VarF64`・f16 は対象外)。
470pub fn jvp(
471 tape: &Tape,
472 output: &Var<'_>,
473 input: &Var<'_>,
474 tangent: &Tensor<f32>,
475 child: &Tape,
476) -> Result<Tensor<f32>, AutodiffError> {
477 check_on_tape(tape, output)?;
478 check_on_tape(tape, input)?;
479 if !input.requires_grad() {
480 return Err(AutodiffError::GradientTrackingDisabled);
481 }
482 let in_shape = input.shape();
483 if tangent.shape() != in_shape.as_slice() {
484 return Err(AutodiffError::Shape(ShapeError::ShapeMismatch {
485 lhs: in_shape,
486 rhs: tangent.shape().to_vec(),
487 }));
488 }
489 let out_shape = output.shape();
490 let n = checked_numel(&in_shape)?;
491 let m = checked_numel(&out_shape)?;
492 if n == 0 || m == 0 || !output.requires_grad() {
493 return Tensor::zeros(&out_shape).map_err(AutodiffError::Shape);
494 }
495
496 let mut data = vec![0.0f32; m];
497 if let Some((g, cu)) = double_vjp_stage(tape, output, input, child)? {
498 let prod = g.mul(&child.var_no_grad(tangent))?;
499 let grads = child.backward(&prod)?;
500 if let Some(h) = grads.get(&cu)? {
501 copy_grad_row(h, &mut data)?;
502 }
503 }
504 Tensor::new(data, &out_shape).map_err(AutodiffError::Shape)
505}
506
507/// ヤコビアン全体 `J = ∂output/∂input` を double-VJP 法で列ごとに求める(イシュー #2940。
508/// `jacobian_ops::jacobian` と REQ-2 統一複合判定で一致する)。戻り値は shape が
509/// `output.shape ++ input.shape` の非微分ホスト値。`child` の契約は [`jvp`] と同じ。
510///
511/// 前段(`g = Jᵀu`)を 1 回だけ作り、子テープ上で `g` の要素 `g_k = ∂(uᵀ output)/∂input_k` ごとに
512/// `child.backward` を回すと `∂g_k/∂u = J[:, k]`(第 2 段の VJP を列ごとに適用)が得られる。
513/// `input` の one-hot 接ベクトルで `jvp` を `n` 回呼ぶ方式と数学的に同じだが、子テープが 1 本で済み
514/// 親テープのノードも 2 個で済むため採らない。計算量は親 backward 1 回+子 backward `n` 回。
515///
516/// **入口検査**は [`jvp`] と同じ(接ベクトルの shape 検査を除く)に加え、`m × n` を検査付き乗算で求める。
517/// 追跡なし `output`・要素数 0 はテープに触れず全ゼロ。副作用・拒否時の契約も [`jvp`] と同じ。
518pub fn jacfwd(
519 tape: &Tape,
520 output: &Var<'_>,
521 input: &Var<'_>,
522 child: &Tape,
523) -> Result<Tensor<f32>, AutodiffError> {
524 check_on_tape(tape, output)?;
525 check_on_tape(tape, input)?;
526 if !input.requires_grad() {
527 return Err(AutodiffError::GradientTrackingDisabled);
528 }
529 let out_shape = output.shape();
530 let in_shape = input.shape();
531 let m = checked_numel(&out_shape)?;
532 let n = checked_numel(&in_shape)?;
533 let total = m
534 .checked_mul(n)
535 .ok_or(AutodiffError::Shape(ShapeError::ElementCountOverflow))?;
536 let mut result_shape = out_shape;
537 result_shape.extend_from_slice(&in_shape);
538 if total == 0 || !output.requires_grad() {
539 return Tensor::zeros(&result_shape).map_err(AutodiffError::Shape);
540 }
541
542 let mut data = vec![0.0f32; total];
543 if let Some((g, cu)) = double_vjp_stage(tape, output, input, child)? {
544 let elements = FlatElements::new(&g)?;
545 for k in 0..n {
546 let g_k = elements.element(k)?;
547 if !g_k.requires_grad() {
548 continue;
549 }
550 let grads = child.backward(&g_k)?;
551 if let Some(col) = grads.get(&cu)? {
552 write_column(&mut data, n, k, col)?;
553 }
554 }
555 }
556 Tensor::new(data, &result_shape).map_err(AutodiffError::Shape)
557}
558
559/// 複数入力のヤコビアン・ベクトル積 `J·v = Σ_i J_i·v_i`(`J_i = ∂output/∂inputs[i]`)を double-VJP 法で求める
560/// (イシュー #3063・親 #3062。設計記録 §30。facade の `Tape::jvp_multi` から 1 行委譲で呼ばれる。
561/// `torch.func.jvp` に複数の primals/tangents を渡す形に相当)。戻り値は shape が `output.shape()` の
562/// 非微分ホスト値。`child` の契約は [`jvp`] と同じ(空の子テープ。呼び出し後は再利用しない)。
563///
564/// **入口検査(ノードを足す前。順序固定。違反が複数あれば最初のものを返す。失敗時は親・子テープとも無変更)**:
565/// 1. `inputs.len() != tangents.len()` → `Err(InvalidArgument)`。
566/// 2. `inputs` が空 → `Err(InvalidArgument)`。
567/// 3. `output`、続いて `inputs` を添字順に現世代検査 → `Err(TapeMismatch)`。
568/// 4. 追跡なしの入力(添字順)→ `Err(GradientTrackingDisabled)`。
569/// 5. 同一 `Var` の重複指定 → `Err(InvalidArgument)`(二重計上の誤用を防ぐ fail-closed)。
570/// 6. `tangents[i].shape() != inputs[i].shape()`(添字順。ブロードキャスト不可)→ `Err(Shape(ShapeMismatch))`。
571/// 7. 要素数を `checked_numel` で検査(`Err(Shape(ElementCountOverflow))`)。
572/// 8. 出力の要素数 0・`output.requires_grad() == false`・全入力の要素数 0 → テープに触れず全ゼロ
573/// (`child` も検査しない)。
574///
575/// 入力が 1 つのときはエラーの種類・順序とも [`jvp`] と一致する。
576///
577/// **算法**: 親 backward は `backward_create_graph` 1 回のみ。`g_i = J_iᵀu` を入力ごとに取り出し、
578/// 子テープ上で `p_i = Σ(g_i ⊙ v_i)`(スカラー)を `add` で畳み込んだ合計を `child.backward` 1 回に渡す
579/// (`∂/∂u = Σ J_i·v_i`)。計算量は入力数に依らず親 1 回+子 1 回。寄与する入力が無ければ全ゼロ。
580///
581/// **副作用**: 入口検査後の失敗でも親テープへちょうど 2 ノード(`u` と `mul`)が残る(入力数に依らない)。
582/// 成功時の子テープには写し・1 階勾配・第 2 段の補助ノードが残る。結果は REQ-2 統一複合判定で
583/// `jacobian` 由来の `Σ J_i·v_i` と一致し、入力 1 つでも [`jvp`] と bit 同一は主張しない。
584/// 非有限値は検査せず伝播する。f32 の [`Tape`] のみ。
585pub fn jvp_multi(
586 tape: &Tape,
587 output: &Var<'_>,
588 inputs: &[&Var<'_>],
589 tangents: &[&Tensor<f32>],
590 child: &Tape,
591) -> Result<Tensor<f32>, AutodiffError> {
592 if inputs.len() != tangents.len() {
593 return Err(AutodiffError::InvalidArgument(format!(
594 "jvp_multi: inputs の数({})と tangents の数({})が一致しない",
595 inputs.len(),
596 tangents.len()
597 )));
598 }
599 if inputs.is_empty() {
600 return Err(AutodiffError::InvalidArgument(
601 "jvp_multi: inputs が空".to_string(),
602 ));
603 }
604 check_on_tape(tape, output)?;
605 for input in inputs {
606 check_on_tape(tape, input)?;
607 }
608 if inputs.iter().any(|v| !v.requires_grad()) {
609 return Err(AutodiffError::GradientTrackingDisabled);
610 }
611 for (i, a) in inputs.iter().enumerate() {
612 if inputs[..i].iter().any(|b| b.node_id() == a.node_id()) {
613 return Err(AutodiffError::InvalidArgument(format!(
614 "jvp_multi: inputs[{i}] が重複指定されている"
615 )));
616 }
617 }
618 for (input, tangent) in inputs.iter().zip(tangents) {
619 let in_shape = input.shape();
620 if tangent.shape() != in_shape.as_slice() {
621 return Err(AutodiffError::Shape(ShapeError::ShapeMismatch {
622 lhs: in_shape,
623 rhs: tangent.shape().to_vec(),
624 }));
625 }
626 }
627 let out_shape = output.shape();
628 let m = checked_numel(&out_shape)?;
629 let mut all_empty = true;
630 for input in inputs {
631 if checked_numel(&input.shape())? != 0 {
632 all_empty = false;
633 }
634 }
635 if m == 0 || all_empty || !output.requires_grad() {
636 return Tensor::zeros(&out_shape).map_err(AutodiffError::Shape);
637 }
638
639 let mut data = vec![0.0f32; m];
640 let (cg, cu) = double_vjp_front(tape, output, child)?;
641 let mut total: Option<Var<'_>> = None;
642 for (input, tangent) in inputs.iter().zip(tangents) {
643 if tangent.shape().iter().product::<usize>() == 0 {
644 continue;
645 }
646 let Some(g) = cg.grad(input)? else { continue };
647 if !g.requires_grad() {
648 continue;
649 }
650 let p = g.mul(&child.var_no_grad(tangent))?.sum(None)?;
651 total = Some(match total {
652 Some(acc) => acc.add(&p)?,
653 None => p,
654 });
655 }
656 if let Some(total) = total {
657 let grads = child.backward(&total)?;
658 if let Some(h) = grads.get(&cu)? {
659 copy_grad_row(h, &mut data)?;
660 }
661 }
662 Tensor::new(data, &out_shape).map_err(AutodiffError::Shape)
663}
664/// [`jvp_create_graph`] の結果(イシュー #3067・設計記録 §31.1)。孫テープ上の追跡された `J·v` と、
665/// 元テープの `Var` から孫テープ上の写し(親→子→孫の 2 段合成)を引く対応表を持つ。
666/// 公開型に出るライフタイムは孫テープの `'g` だけ(1 段目の子テープは関数内に閉じる)。
667#[derive(Debug)]
668pub struct JvpCreateGraphResult<'g> {
669 output: Var<'g>,
670 parent_id: TapeId,
671 parent_epoch: u64,
672 /// 親 `NodeId.0` を添字とする孫テープ上の写し(`J ≡ 0` 等のゼロ経路では空)。
673 mirror: Vec<Option<Var<'g>>>,
674 /// テスト専用: 2 段目の余接葉 `u` の 1 階勾配(`jvp` との bit 同一性検査用。公開しない)。
675 #[cfg(test)]
676 stage2_cotangent_grad: Option<Tensor<f32>>,
677}
678
679impl<'g> JvpCreateGraphResult<'g> {
680 /// `J·v`(shape は元の `output` と同じ)を孫テープ上の `Var` として返す。
681 /// 写しの入力へ `grandchild.backward` で微分できる。出力が入力に依存しない場合
682 /// (線形・`var_no_grad` の係数のみ等)は追跡なし(`requires_grad() == false`)になりうる。
683 pub fn output(&self) -> Var<'g> {
684 self.output
685 }
686
687 /// 元テープ上の `parent_var` の孫テープ上の写しを返す。`output()` を `grandchild.backward` で
688 /// 微分した勾配はこの写しをキーに読む(元の `x` とは直接つながらない。§31.2)。
689 ///
690 /// - テープ・世代の不一致 → `Err(TapeMismatch)`、追跡なし → `Err(GradientTrackingDisabled)`
691 /// ([`CreateGraphResult::child_var`] と同じ検査順)。
692 /// - `Ok(None)` は `J·v` がその入力に恒等的に依存しない(勾配ゼロ)ことを意味する。
693 pub fn child_var(&self, parent_var: &Var<'_>) -> Result<Option<Var<'g>>, AutodiffError> {
694 if parent_var.tape_id() != self.parent_id || parent_var.tape_epoch() != self.parent_epoch {
695 return Err(AutodiffError::TapeMismatch);
696 }
697 if !parent_var.requires_grad() {
698 return Err(AutodiffError::GradientTrackingDisabled);
699 }
700 Ok(self.mirror.get(parent_var.node_id().0).copied().flatten())
701 }
702
703 #[cfg(test)]
704 pub(crate) fn stage2_cotangent_grad_host(&self) -> Option<&Tensor<f32>> {
705 self.stage2_cotangent_grad.as_ref()
706 }
707}
708
709/// 全ゼロの `J·v` を孫テープの追跡なし葉として返すゼロ経路(写しは空)。
710fn zero_jvp_result<'g>(
711 tape: &Tape,
712 grandchild: &'g Tape,
713 out_shape: &[usize],
714) -> Result<JvpCreateGraphResult<'g>, AutodiffError> {
715 let zeros = Tensor::zeros(out_shape).map_err(AutodiffError::Shape)?;
716 Ok(JvpCreateGraphResult {
717 output: grandchild.var_no_grad(&zeros),
718 parent_id: tape.id,
719 parent_epoch: tape.epoch(),
720 mirror: Vec::new(),
721 #[cfg(test)]
722 stage2_cotangent_grad: None,
723 })
724}
725
726/// 追跡された `Var` を返す微分可能な `jvp`(イシュー #3067・設計記録 §31.2〜§31.4・§31.6)。
727/// 前段は [`jvp`] と同じ double-VJP 法で、2 段目の VJP だけ `child.backward_create_graph` で
728/// 孫テープ `grandchild` に記録する。結果 `J·v` は孫テープ上の `Var` で、元の入力への微分は
729/// [`JvpCreateGraphResult::child_var`] の写し(親→子→孫の 2 段合成)を通して得る。
730/// facade の `Tape::jvp_create_graph`(#3068)から 1 行委譲で呼ばれる予定。
731///
732/// 入口検査(ノードを足す前。順序固定): 1〜4 は [`jvp`] と同じ(テープ所属・追跡あり入力・
733/// 接ベクトル shape・要素数)。5 は 3 本のテープ関係(`child`・`grandchild` は互いに別で `tape` とも別、
734/// 同デバイス、どちらも空)で、違反は `Err(Backward)`/`Err(DeviceMismatch)`。
735/// 6 は追跡なし `output`・要素数 0 のゼロ返却(親・`child` は無変更、`grandchild` に全ゼロの葉が 1 個)。
736/// [`jvp`] のゼロ経路は `child` を検査しないが、本関数は 5 をゼロ返却より先に行う。
737///
738/// 以降は [`jvp`] と同じ拒否(非対象 Op・rank 3 以上の `matmul`・checkpoint 等)が 1 段目に、
739/// `backward_create_graph` の既存検査が 2 段目に `Err(Backward)` で伝播する。親テープへは前段で
740/// ちょうど 2 ノードが残る。`J ≡ 0` は全ゼロの追跡なし葉を返す。非有限値は検査せず伝播する。
741/// f32 の [`Tape`] のみ(`VarF64`・f16・複数入力は対象外)。
742pub fn jvp_create_graph<'g>(
743 tape: &Tape,
744 output: &Var<'_>,
745 input: &Var<'_>,
746 tangent: &Tensor<f32>,
747 child: &Tape,
748 grandchild: &'g Tape,
749) -> Result<JvpCreateGraphResult<'g>, AutodiffError> {
750 check_on_tape(tape, output)?;
751 check_on_tape(tape, input)?;
752 if !input.requires_grad() {
753 return Err(AutodiffError::GradientTrackingDisabled);
754 }
755 let in_shape = input.shape();
756 if tangent.shape() != in_shape.as_slice() {
757 return Err(AutodiffError::Shape(ShapeError::ShapeMismatch {
758 lhs: in_shape,
759 rhs: tangent.shape().to_vec(),
760 }));
761 }
762 let out_shape = output.shape();
763 let n = checked_numel(&in_shape)?;
764 let m = checked_numel(&out_shape)?;
765
766 if child.id == tape.id {
767 return Err(AutodiffError::Backward(
768 "jvp_create_graph: 子テープに親と同一の Tape は指定できない".into(),
769 ));
770 }
771 if grandchild.id == tape.id || grandchild.id == child.id {
772 return Err(AutodiffError::Backward(
773 "jvp_create_graph: 孫テープは親・子と異なる Tape である必要がある".into(),
774 ));
775 }
776 for other in [child, grandchild] {
777 if other.device() != tape.device() {
778 return Err(AutodiffError::DeviceMismatch {
779 requested: other.device(),
780 actual: tape.device(),
781 });
782 }
783 }
784 if !child.is_empty() || !grandchild.is_empty() {
785 return Err(AutodiffError::Backward(
786 "jvp_create_graph: 子テープ・孫テープは空である必要がある".into(),
787 ));
788 }
789 if n == 0 || m == 0 || !output.requires_grad() {
790 return zero_jvp_result(tape, grandchild, &out_shape);
791 }
792
793 let (cg1, cu) = double_vjp_front(tape, output, child)?;
794 let g = match cg1.grad(input)? {
795 Some(g) if g.requires_grad() => g,
796 _ => return zero_jvp_result(tape, grandchild, &out_shape),
797 };
798 let prod = g.mul(&child.var_no_grad(tangent))?;
799 let cg2 = child.backward_create_graph(&prod, grandchild)?;
800 let Some(r) = cg2.grad(&cu)? else {
801 return Err(AutodiffError::Backward(
802 "jvp_create_graph: 余接葉の 2 段目の勾配が存在しない(不変条件違反)".to_string(),
803 ));
804 };
805
806 // 追跡なしの親ノードの写しは `var_no_grad` の葉で `child_var` が Err になるため、追跡ありだけ合成する。
807 let mut mirror: Vec<Option<Var<'g>>> = vec![None; cg1.mirror().len()];
808 for (slot, cv) in mirror.iter_mut().zip(cg1.mirror()) {
809 if let Some(cv) = cv
810 && cv.requires_grad()
811 {
812 *slot = cg2.child_var(cv)?;
813 }
814 }
815
816 #[cfg(test)]
817 let stage2 = {
818 let mut data = vec![0.0f32; m];
819 if let Some(h) = cg2.first_order().get(&cu)? {
820 copy_grad_row(h, &mut data)?;
821 }
822 Tensor::new(data, &out_shape).map_err(AutodiffError::Shape)?
823 };
824 Ok(JvpCreateGraphResult {
825 output: r,
826 parent_id: tape.id,
827 parent_epoch: tape.epoch(),
828 mirror,
829 #[cfg(test)]
830 stage2_cotangent_grad: Some(stage2),
831 })
832}