Skip to main content

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}