Skip to main content

fandhe_ai/nn/
module.rs

1//! facade 独自の `nn::Module` trait(イシュー #2395・親 #2338)。
2//!
3//! 役割: facade 利用者が `Var` 演算を自由に組み合わせた独自層を、facade だけの依存で
4//! 定義するための土台である(`fandhe_ai_autodiff::nn::Module::forward` は生の
5//! `fandhe_ai_autodiff::Tape` を引数に取るため facade 利用者は名指しできない。
6//! `docs/facade-nn-module-exposure-decision.md` §1.3)。#2338 承認事項 1(案 B)・
7//! 2(required `forward` + defaulted 6 件。#2400 で凍結 API 3 件・#2401 で introspection 4 件・#2400 の葉単位ロールバック用に `children_mut` を追加し 14 件)・4(`forward` 第 1 引数は
8//! [`crate::TapeRef`])に従う。
9//!
10//! REQ-12: 生の `Tape`・`BackendOps`・内部層型(`as_*`・`forward_host`・
11//! `is_pooling` 等の内部フック)は一切載せない。open trait(sealed にしない)で
12//! `dyn Module` が成り立つ。後方互換規則: defaulted メソッドの追加は非破壊、
13//! required メソッドの追加は破壊的変更である。
14//!
15//! `named_parameters`/`set_parameter`/`state_dict`/`load_state_dict`/
16//! `set_training`/`training`/`children`/`named_modules`/`parameter_count`/`type_name`
17//! (後ろ 4 件は #2134 の鏡写し。イシュー #2401)と、凍結 API の `set_requires_grad`/`freeze`/
18//! `requires_grad`(#2137 の鏡写し。#2400)の意味論・命名契約・fail-closed 検証は
19//! `crates/autodiff/src/nn/module.rs` の同名メソッドと同一である。
20//! `load_state_dict` は autodiff 側の単一実装を crate 内アダプタ(`FacadeModuleAdapter`。
21//! autodiff コンテナへ積む橋渡しを兼ねる。#2397)経由で再利用し、
22//! two-pass 検証・キー昇順適用・逆順ロールバックの一致を構造的に保証する
23//! (コピーによるドリフトを避ける)。
24
25use std::collections::{HashMap, HashSet};
26
27use fandhe_ai_autodiff::nn::Module as AutodiffModule;
28use fandhe_ai_autodiff::nn::RequiresGradSnapshot;
29
30use crate::{AutodiffError, TapeRef, Tensor, Var};
31
32/// facade 利用者が独自層を定義するための共通 forward シグネチャ。
33///
34/// required は [`Self::forward`] の 1 件、defaulted は 14 件
35/// ([`Self::named_parameters`]・[`Self::set_parameter`]・[`Self::state_dict`]・
36/// [`Self::load_state_dict`]・[`Self::set_training`]・[`Self::training`]・
37/// [`Self::set_requires_grad`]・[`Self::freeze`]・[`Self::requires_grad`]・
38/// [`Self::children`]・[`Self::children_mut`]・[`Self::named_modules`]・
39/// [`Self::parameter_count`]・[`Self::type_name`]。`children`・`named_modules`・
40/// `parameter_count`・`type_name` の 4 件は autodiff `Module`(#2134)の同名メソッドの
41/// 鏡写し〈イシュー #2401〉、`children_mut` は `set_requires_grad` の葉単位ロールバック用に
42/// `children` と対で実装する facade 独自のメソッド〈#2400〉)。
43/// 出典は `crates/autodiff/src/nn/module.rs` で、`tests/api_surface.rs` が
44/// この集合を機械的に固定する。
45pub trait Module {
46    /// このステップの `tape` 上で 1 回分の forward を計算する。
47    ///
48    /// `tape` は [`crate::TapeRef`](`TapeRef::from(&fandhe_ai::tape())` 等)。
49    /// パラメータは `tape.var(&tensor)` で葉ノード登録してから使う。
50    fn forward<'t>(&self, tape: TapeRef<'t>, input: &Var<'t>) -> Result<Var<'t>, AutodiffError>;
51
52    /// 学習可能パラメータの「名前, 参照」列。名前は struct フィールド名/accessor 名
53    /// を正とし、順序は登録順(weight → bias)。既定は空(無状態層向け)。
54    fn named_parameters(&self) -> Vec<(String, &Tensor<f32>)> {
55        Vec::new()
56    }
57
58    /// [`Self::named_parameters`] が返す名前のパラメータを書き戻す(shape 保存置換のみ)。
59    ///
60    /// 該当名がなければ `AutodiffError::InvalidArgument`(fail-closed)。既定は常に `Err`
61    /// (無状態層向け)。[`Self::named_parameters`] を override する層は必ず本メソッドも
62    /// 対で override すること(片方だけだと [`Self::load_state_dict`] が `Err` になる)。
63    fn set_parameter(&mut self, name: &str, _value: Tensor<f32>) -> Result<(), AutodiffError> {
64        Err(AutodiffError::InvalidArgument(format!(
65            "Module::set_parameter: no parameter named `{name}` (default fail-safe; this \
66             Module does not override set_parameter)"
67        )))
68    }
69
70    /// [`Self::named_parameters`] のキー付きビュー(各値は `clone`)。順序契約は持たない。
71    fn state_dict(&self) -> HashMap<String, Tensor<f32>> {
72        self.named_parameters()
73            .into_iter()
74            .map(|(name, tensor)| (name, tensor.clone()))
75            .collect()
76    }
77
78    /// [`Self::state_dict`] の逆(strict 限定)。
79    ///
80    /// パス 1 でキー集合の完全一致(欠落・余剰を昇順列挙)と shape 完全一致を検証し
81    /// (無変更)、パス 2 でキー名昇順に [`Self::set_parameter`] を適用する。途中で
82    /// 失敗したら適用済みキーを逆順に元の値へ戻す。ロールバック自体が失敗した場合は
83    /// 部分適用の可能性を明示した `Err` を返す(完全な原子性は保証しない)。
84    fn load_state_dict(
85        &mut self,
86        state: HashMap<String, Tensor<f32>>,
87    ) -> Result<(), AutodiffError> {
88        AutodiffModule::load_state_dict(&mut FacadeModuleAdapter(self), state)
89    }
90
91    /// 学習/評価モードの切替。既定は no-op(無状態層向け)。モード依存層は
92    /// [`Self::training`] と必ず両方を override して自層に保持すること。
93    fn set_training(&mut self, _training: bool) {}
94
95    /// 現在のモード。既定は `true`(PyTorch `Module.training` の初期値と同じ)。
96    fn training(&self) -> bool {
97        true
98    }
99
100    /// 層単位の `requires_grad` 凍結(PyTorch `module.requires_grad_(bool)` 相当。
101    /// #2137 の鏡写し・#2400)。
102    ///
103    /// 実装者契約: パラメータを持つ層は `requires_grad: bool` を自層に保持し、`forward`
104    /// 内でパラメータ葉を `if self.requires_grad { tape.var(&p) } else { tape.var_no_grad(&p) }`
105    /// のように切り替えて登録する。本メソッドと [`Self::requires_grad`] は必ず対で
106    /// override すること。
107    ///
108    /// 複合層([`Self::children`] が非空)が自身にも `requires_grad` フラグを持つ場合、
109    /// [`Self::requires_grad`] はそのフラグを返すこと。凍結が失敗したときのロールバックは
110    /// 複合層ごとに「自身へ `set_requires_grad(呼び出し前の自身の `requires_grad()`)` →
111    /// 子を葉単位で再帰復元」の順に呼んで戻す(自身の呼び出しが子へ伝播した値は直後の子の
112    /// 復元で上書きされる)。ただし復元の前に子を照合キー(名前・型・アドレス・サイズ)で照合し、不一致なら自身の
113    /// 呼び出しを省いて部分適用エラーにする([`Self::children_mut`] 参照。検出範囲は同 doc の
114    /// (b)〜(d) のとおりで、取り消しは不可)。不一致が無ければ自身のフラグと各子の状態が共に
115    /// 呼び出し前へ戻る。
116    ///
117    /// - 反映は次の `forward`(葉登録)から。登録済みの `Var` は変わらない。
118    /// - [`Self::set_training`]/[`Self::training`] とは独立の軸(BatchNorm 系の統計は
119    ///   凍結後も training のまま更新される)。
120    /// - [`Self::state_dict`]/[`Self::load_state_dict`]/[`Self::set_parameter`] の対象外で、
121    ///   パラメータを差し替えてもフラグは保持される。
122    /// - 粒度は層単位(パラメータ名指定の個別凍結は範囲外)。
123    ///
124    /// 既定は fail-closed: パラメータ 0 件なら `Ok(())`(無状態層)、1 件以上なら
125    /// `InvalidArgument`。override 忘れで「凍結したつもりが学習が続く」黙った事故を
126    /// 防ぐ(security.md A08。no-op 既定は採らない)。文言は autodiff 側と同一。
127    fn set_requires_grad(&mut self, _requires_grad: bool) -> Result<(), AutodiffError> {
128        let param_count = self.named_parameters().len();
129        if param_count == 0 {
130            Ok(())
131        } else {
132            Err(AutodiffError::InvalidArgument(format!(
133                "Module::set_requires_grad: this Module has {param_count} named parameter(s) \
134                 but does not override set_requires_grad (fail-closed default; freezing would \
135                 silently be a no-op)"
136            )))
137        }
138    }
139
140    /// [`Self::set_requires_grad`]`(false)` の別名(転移学習で backbone を固定する典型呼び出し)。
141    fn freeze(&mut self) -> Result<(), AutodiffError> {
142        self.set_requires_grad(false)
143    }
144
145    /// この層が現在追跡対象か。既定は `true`(パラメータを持たない層は状態を持たない)。
146    /// パラメータを持つ層は [`Self::set_requires_grad`] と対で override する。自身のフラグを
147    /// 持つ複合層はそのフラグを返すこと(ロールバックが自身→子の順に復元するため。
148    /// [`Self::set_requires_grad`] 参照)。
149    fn requires_grad(&self) -> bool {
150        true
151    }
152
153    /// この層が直接内包する子 `Module` の「名前, 参照」列を登録順で返す
154    /// (autodiff `Module::children`・PyTorch `Module.children()` 相当。#2134・#2401)。
155    ///
156    /// 名前は [`Self::named_parameters`] の接頭辞と完全一致させる(`ModuleList`/
157    /// `Sequential` は `"{index}"`)。既定は空(葉モジュール向け)。
158    fn children(&self) -> Vec<(String, &dyn Module)> {
159        Vec::new()
160    }
161
162    /// 子孫を深さ優先(子自身 → その子孫)・登録順で再帰列挙する
163    /// (autodiff `Module::named_modules` の鏡写し。#2134・#2401)。
164    ///
165    /// ルート自身は含めない(`Self: Sized` 境界が object safety を壊すため。PyTorch の
166    /// `""` エントリとは異なる)。パスは `"{parent}.{child}"`。
167    ///
168    /// 循環・重複: 同一性は `(データポインタ, type_name)` の組で判定する。(1) 祖先スタックに
169    /// 既出なら常に打ち切る(循環でも panic しない)、(2) ゼロサイズ型を除き訪問済み集合でも
170    /// dedup する(共有子は最初の経路のみ。PyTorch の memo 相当)。ZST は複数インスタンスが
171    /// dangling address を共有しうるため (2) から除外する。型名をキーに含めるのは、先頭
172    /// フィールドの子がルートと同一アドレスになる(offset 0)場合の誤判定を避けるため。
173    fn named_modules(&self) -> Vec<(String, &dyn Module)> {
174        let mut out = Vec::new();
175        let root_key: NodeKey = (self as *const Self as *const (), self.type_name());
176        let mut ancestors: Vec<NodeKey> = vec![root_key];
177        let mut visited: HashSet<NodeKey> = HashSet::new();
178        for (name, child) in self.children() {
179            collect_named_modules(name, child, &mut ancestors, &mut visited, &mut out);
180        }
181        out
182    }
183
184    /// [`Self::named_parameters`] が公開する学習可能パラメータの総要素数
185    /// (autodiff `Module::parameter_count` の鏡写し。#2134・#2401)。
186    /// `saturating_add` で合計するためオーバーフローでも panic せず `usize::MAX` に飽和する。
187    fn parameter_count(&self) -> usize {
188        self.named_parameters()
189            .into_iter()
190            .fold(0usize, |acc, (_, tensor)| {
191                acc.saturating_add(tensor.numel())
192            })
193    }
194
195    /// 実装型名(`std::any::type_name::<Self>()`)。出力形式は標準ライブラリが安定を
196    /// 保証しないため表示用途に限る(autodiff `Module::type_name` の鏡写し。#2134・#2401)。
197    /// 例外として、凍結ロールバックが子の型の照合キーに使う([`Self::children_mut`] の (b)。
198    /// 異なる型は区別できるが同じ型の別インスタンスは区別できない。偽る override は範囲外)。
199    fn type_name(&self) -> &'static str {
200        std::any::type_name::<Self>()
201    }
202
203    /// [`Self::children`] の可変版(同じ「名前, 参照」列を同じ順序・同じ名前で返す。
204    /// #2400・PR #2426 レビュー是正。2026-09-29 ユーザー承認済みの公開面追加)。
205    ///
206    /// 用途: 凍結([`Self::set_requires_grad`])が失敗したとき、コンテナが子孫を**葉単位**
207    /// (葉 = [`Self::children`] が空の層)で呼び出し前の状態へ戻すための可変アクセス。
208    /// 子を持つ利用者定義の複合層は `children` と対で必ず実装すること(`children` と同じ子への
209    /// 参照を同じ順・同じ名前で返し、名前が同じでも別の子オブジェクトを返してはならない)。
210    ///
211    /// 契約と検出範囲(PR #2426 第 6〜8 回レビュー P1 是正。ライブラリは `Module` 参照から真の
212    /// 同一性を得られないため、検出できる範囲を次のとおり定義する):
213    /// - (a) 実装側の契約: [`Self::children`]/本メソッドは [`Self::set_requires_grad`] の間、
214    ///   同じ子を返し続けること。
215    /// - (b) ライブラリは、件数・名前・型([`Self::type_name`])・データアドレス・サイズで検出
216    ///   できる契約違反を、`ModuleList`/`Sequential`/`ModuleDict` の `set_requires_grad` が
217    ///   状態を変更する前の `InvalidArgument`(事前検査。`children_mut` 未実装で `children` だけ
218    ///   実装した層を含む)、または復元時の部分適用エラー(`InvalidArgument`)として報告する。
219    ///   復元は複合層ごとに、まず本メソッドの子を取得時のキーと照合し、**不一致なら自身の
220    ///   [`Self::set_requires_grad`] は呼ばない**。一致した位置の子だけを葉単位で戻す。
221    /// - (c) 同じ型・同じ格納位置での値の置き換え(同じアドレスでの再確保を含む)は検出できず、
222    ///   ロールバックは位置(格納スロット)単位で、そのスロットを凍結前の値へ戻す。
223    /// - (d) 自身の setter の**実行中**の伝播先は利用者の実装の責任で、ライブラリは防げない
224    ///   (事後の再照合で検出できる範囲だけ部分適用エラーとして報告し、取り消しはしない)。
225    ///   `type_name` を偽って override する実装は信頼境界の外。
226    /// - 全一致のときは自身へ `set_requires_grad` を呼んでから本メソッドを取り直して再照合する。
227    ///   取得時は葉(`children` が空)だった層が復元時に子を持っていれば、setter を呼ばず部分適用
228    ///   エラーにする。
229    ///
230    /// 既定は空(葉モジュール向け)。
231    fn children_mut(&mut self) -> Vec<(String, &mut dyn Module)> {
232        Vec::new()
233    }
234}
235
236/// `named_modules` 用のノード同一性キー(データポインタ, 型名)。非公開。
237pub(super) type NodeKey = (*const (), &'static str);
238
239/// `Module::named_modules` の再帰本体(autodiff 側 `collect_named_modules` と同一意味論)。
240/// 祖先スタックで循環を常に打ち切り、非 ZST のみ訪問済み集合で共有子を dedup する。
241fn collect_named_modules<'a>(
242    name: String,
243    child: &'a dyn Module,
244    ancestors: &mut Vec<NodeKey>,
245    visited: &mut HashSet<NodeKey>,
246    out: &mut Vec<(String, &'a dyn Module)>,
247) {
248    let key: NodeKey = (child as *const dyn Module as *const (), child.type_name());
249    if ancestors.contains(&key) {
250        return;
251    }
252    let is_zst = std::mem::size_of_val(child) == 0;
253    if !is_zst && !visited.insert(key) {
254        return;
255    }
256    out.push((name.clone(), child));
257    ancestors.push(key);
258    for (descendant_name, descendant) in child.children() {
259        collect_named_modules(
260            format!("{name}.{descendant_name}"),
261            descendant,
262            ancestors,
263            visited,
264            out,
265        );
266    }
267    ancestors.pop();
268}
269
270/// facade の [`Module`] を autodiff 側 `Module` として扱う crate 内専用アダプタ
271/// (イシュー #2397・親 #2338 承認事項 4「借用ハンドル型による橋渡し」。
272/// `docs/facade-nn-module-exposure-decision.md` §6・§10・§14)。
273///
274/// 役割は 2 つ。`P = &mut M`(`M: Module + ?Sized`)では facade `Module::load_state_dict`
275/// 既定実装が autodiff 側の単一実装を再利用するための内部ブリッジ、`P = Box<dyn Module>`
276/// では autodiff コンテナ(`Box<dyn fandhe_ai_autodiff::nn::Module>`。`'static` 必須)へ
277/// facade 層を積むアダプタ(公開入口は #2398 で `compat::Sequential` に足す。それまで
278/// `nn/mod.rs` からは再エクスポートせず、compat からは到達しない)。
279///
280/// `pub(crate)` で公開面(REQ-12)には出ない。`unsafe` は使わず、autodiff の
281/// `&Tape` は `TapeRef::from_autodiff` の安全な借用変換で facade 層へ渡す。
282///
283/// 委譲: `forward`・`named_parameters`・`set_parameter`・`set_training`・`training`・
284/// `set_requires_grad`・`requires_grad`(#2400)・葉単位の凍結スナップショット
285/// `requires_grad_snapshot`/`restore_requires_grad_snapshot`(autodiff コンテナの
286/// ロールバックで内側の複合層の混在状態を `children`/`children_mut` 経由で葉単位に保つ。PR #2426 P1)。`freeze` は autodiff 既定が
287/// `set_requires_grad(false)` を経由するため委譲せず(facade 層が独自に `freeze` を
288/// override していても本アダプタ経由では迂回される)、
289/// `state_dict`/`load_state_dict` は autodiff 既定のまま(委譲済みの
290/// `named_parameters`/`set_parameter` の上で動く。facade 層が独自に
291/// `load_state_dict` を override していても本アダプタ経由では迂回される。
292/// `ModuleList` が子を扱うのと同じ意味論)。`as_*`/`is_pooling` は既定(`None`/`false`)。
293/// `type_name` も委譲する(autodiff 側の表示に利用者の層の型名を出すため。#2401)。
294/// `children` は委譲しない(facade の `&dyn Module` を借用のまま autodiff の
295/// `&dyn Module` へ変換できず、autodiff の `named_modules` はアダプタ内へ降りない)。
296///
297/// ホスト推論経路は非対応: `supports_forward_host` を `false` へ override し
298/// (autodiff 既定は `true`。既定のままだと `compat::Sequential::predict` の事前判定を
299/// 通過し、途中層の `Err` で手前層の副作用〈Dropout の RNG 消費・BatchNorm の running
300/// stats 更新〉が tape 経路再実行と二重化する)、`forward_host` は
301/// `InvalidArgument` を返す(`BackendError::Unsupported` は「フォールバックの合図」で
302/// `predict_recorded` が捕捉して再実行するため使わない。前例は `ModuleList::forward_host`。
303/// `Embedding` は `Unsupported` と `supports_forward_host() == false` の組だが、本アダプタは
304/// 直接呼び出しやネスト経由でも黙ってフォールバックさせない方針を優先する。security.md A04)。
305pub(crate) struct FacadeModuleAdapter<P>(pub(crate) P);
306
307impl<P> AutodiffModule for FacadeModuleAdapter<P>
308where
309    P: std::ops::DerefMut,
310    P::Target: Module,
311{
312    fn forward<'t>(
313        &self,
314        tape: &'t fandhe_ai_autodiff::Tape,
315        input: &Var<'t>,
316    ) -> Result<Var<'t>, AutodiffError> {
317        Module::forward(&*self.0, TapeRef::from_autodiff(tape), input)
318    }
319
320    fn named_parameters(&self) -> Vec<(String, &Tensor<f32>)> {
321        Module::named_parameters(&*self.0)
322    }
323
324    fn set_parameter(&mut self, name: &str, value: Tensor<f32>) -> Result<(), AutodiffError> {
325        Module::set_parameter(&mut *self.0, name, value)
326    }
327
328    fn set_training(&mut self, training: bool) {
329        Module::set_training(&mut *self.0, training);
330    }
331
332    fn training(&self) -> bool {
333        Module::training(&*self.0)
334    }
335
336    fn set_requires_grad(&mut self, requires_grad: bool) -> Result<(), AutodiffError> {
337        Module::set_requires_grad(&mut *self.0, requires_grad)
338    }
339
340    fn requires_grad(&self) -> bool {
341        Module::requires_grad(&*self.0)
342    }
343
344    /// facade 側の葉単位スナップショット(`nn::container::snapshot_requires_grad`。
345    /// `children`/`children_mut` 経由)を `Opaque` に包んで返す。autodiff のコンテナが
346    /// アダプタを単一の葉として集約値で保存すると、内側の複合層の混在状態が復元時に
347    /// 均一化されるため(PR #2426 P1)。件数・名前・型・アドレス・サイズで検出できる
348    /// `children`/`children_mut` の不整合は状態変更前に `InvalidArgument` で報告する
349    /// (検出範囲は [`Module::children_mut`] の (b)〜(d))。
350    fn requires_grad_snapshot(&mut self) -> Result<RequiresGradSnapshot, AutodiffError> {
351        crate::nn::container::snapshot_requires_grad(&mut *self.0)
352            .map(|snap| RequiresGradSnapshot::Opaque(Box::new(snap)))
353    }
354
355    /// `Opaque` を facade 側のスナップショット型へ戻して葉単位に復元する。`Opaque` 以外・
356    /// 型違いは fail-closed の `InvalidArgument`。
357    fn restore_requires_grad_snapshot(
358        &mut self,
359        snapshot: &RequiresGradSnapshot,
360    ) -> Result<(), AutodiffError> {
361        let inner = match snapshot {
362            RequiresGradSnapshot::Opaque(any) => {
363                any.downcast_ref::<crate::nn::container::RequiresGradSnapshot>()
364            }
365            _ => None,
366        };
367        match inner {
368            Some(snap) => crate::nn::container::restore_requires_grad(&mut *self.0, snap),
369            None => Err(AutodiffError::InvalidArgument(
370                "FacadeModuleAdapter::restore_requires_grad_snapshot: snapshot was not \
371                 produced by a FacadeModuleAdapter (structure changed during rollback)"
372                    .to_string(),
373            )),
374        }
375    }
376
377    fn type_name(&self) -> &'static str {
378        Module::type_name(&*self.0)
379    }
380
381    fn supports_forward_host(&self) -> bool {
382        false
383    }
384
385    fn forward_host(
386        &self,
387        _ops: &dyn fandhe_ai_tensor_core::BackendOps,
388        _input: &Tensor<f32>,
389    ) -> Result<Tensor<f32>, AutodiffError> {
390        Err(AutodiffError::InvalidArgument(
391            "FacadeModuleAdapter::forward_host: facade nn::Module does not support the host \
392             (tape-free) inference path; supports_forward_host() is false and must be honored"
393                .to_string(),
394        ))
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    /// 2 パラメータ層。`fail_b` が真なら `b` の `set_parameter` は常に失敗する。
403    struct TwoParam {
404        a: Tensor<f32>,
405        b: Tensor<f32>,
406        fail_b: bool,
407    }
408
409    impl Module for TwoParam {
410        fn forward<'t>(
411            &self,
412            tape: TapeRef<'t>,
413            input: &Var<'t>,
414        ) -> Result<Var<'t>, AutodiffError> {
415            let a = tape.var(&self.a);
416            input.add(&a)
417        }
418        fn named_parameters(&self) -> Vec<(String, &Tensor<f32>)> {
419            vec![("a".into(), &self.a), ("b".into(), &self.b)]
420        }
421        fn set_parameter(&mut self, name: &str, value: Tensor<f32>) -> Result<(), AutodiffError> {
422            match name {
423                "a" => self.a = value,
424                "b" if self.fail_b => {
425                    return Err(AutodiffError::InvalidArgument("b failed".into()));
426                }
427                "b" => self.b = value,
428                _ => return Err(AutodiffError::InvalidArgument(format!("unknown {name}"))),
429            }
430            Ok(())
431        }
432    }
433
434    struct Stateless;
435    impl Module for Stateless {
436        fn forward<'t>(
437            &self,
438            _tape: TapeRef<'t>,
439            input: &Var<'t>,
440        ) -> Result<Var<'t>, AutodiffError> {
441            Ok(*input)
442        }
443    }
444
445    fn t(v: f32) -> Tensor<f32> {
446        Tensor::from_slice(&[v, v], &[2]).expect("tensor")
447    }
448
449    fn bits(x: &Tensor<f32>) -> Vec<u32> {
450        x.as_slice()
451            .expect("host tensor")
452            .iter()
453            .map(|v| v.to_bits())
454            .collect()
455    }
456
457    #[test]
458    fn bridge_forward_matches_direct_forward() {
459        let mut layer = TwoParam {
460            a: t(1.0),
461            b: t(2.0),
462            fail_b: false,
463        };
464        let ta = crate::tape();
465        let xa = ta.var(&t(3.0));
466        let direct = layer.forward(TapeRef::from(&ta), &xa).expect("direct");
467
468        let tb = crate::tape();
469        let xb = tb.var(&t(3.0));
470        let bridge = FacadeModuleAdapter(&mut layer);
471        let via = AutodiffModule::forward(&bridge, &tb.0, &xb).expect("bridge");
472        assert_eq!(bits(&direct.value()), bits(&via.value()));
473    }
474
475    #[test]
476    fn default_set_parameter_message_matches_autodiff() {
477        let mine = Stateless
478            .set_parameter("w", t(0.0))
479            .expect_err("default is Err")
480            .to_string();
481        let mut relu = fandhe_ai_autodiff::nn::activation::Relu;
482        let theirs = AutodiffModule::set_parameter(&mut relu, "w", t(0.0))
483            .expect_err("default is Err")
484            .to_string();
485        assert_eq!(mine, theirs);
486    }
487
488    #[test]
489    fn load_state_dict_errors_and_rollback_via_bridge() {
490        let mut layer = TwoParam {
491            a: t(1.0),
492            b: t(2.0),
493            fail_b: true,
494        };
495        let mut good = layer.state_dict();
496        good.insert("a".into(), t(9.0));
497        let err = layer.load_state_dict(good).expect_err("b fails");
498        assert!(err.to_string().contains("b failed"), "{err}");
499        assert_eq!(bits(&layer.a), bits(&t(1.0)), "a is rolled back");
500
501        let mut missing = layer.state_dict();
502        missing.remove("b");
503        let e = layer.load_state_dict(missing).expect_err("missing");
504        assert!(e.to_string().contains("missing keys"), "{e}");
505    }
506
507    // ---- #2397: autodiff コンテナへ積むアダプタ(P = Box<dyn Module>)の検証 ----
508
509    use fandhe_ai_autodiff::nn::{Linear, Sequential};
510
511    /// パラメータ `a`(`[3]`)と train/eval フラグを実保持する facade 層。
512    struct Scale {
513        a: Tensor<f32>,
514        training: bool,
515    }
516
517    impl Scale {
518        fn new() -> Self {
519            Self {
520                a: Tensor::from_slice(&[0.5, -1.5, 2.0], &[3]).expect("tensor"),
521                training: true,
522            }
523        }
524    }
525
526    impl Module for Scale {
527        fn forward<'t>(
528            &self,
529            tape: TapeRef<'t>,
530            input: &Var<'t>,
531        ) -> Result<Var<'t>, AutodiffError> {
532            let a = tape.var(&self.a);
533            input.mul(&a)
534        }
535        fn named_parameters(&self) -> Vec<(String, &Tensor<f32>)> {
536            vec![("a".into(), &self.a)]
537        }
538        fn set_parameter(&mut self, name: &str, value: Tensor<f32>) -> Result<(), AutodiffError> {
539            if name != "a" {
540                return Err(AutodiffError::InvalidArgument(format!("unknown {name}")));
541            }
542            self.a = value;
543            Ok(())
544        }
545        fn set_training(&mut self, training: bool) {
546            self.training = training;
547        }
548        fn training(&self) -> bool {
549            self.training
550        }
551    }
552
553    fn input() -> Tensor<f32> {
554        Tensor::from_slice(&[0.1, 0.2, -0.3, 0.4, -0.5, 0.6], &[2, 3]).expect("tensor")
555    }
556
557    fn adapter() -> FacadeModuleAdapter<Box<dyn Module>> {
558        FacadeModuleAdapter(Box::new(Scale::new()) as Box<dyn Module>)
559    }
560
561    /// forward 値・入力勾配・葉勾配・ノード数のスナップショット(bit 比較用)。
562    type Snapshot = (Vec<u32>, Option<Vec<u32>>, Vec<Option<Vec<u32>>>, usize);
563
564    fn snapshot<'t>(tape: &'t crate::Tape, x: &Var<'t>, out: &Var<'t>) -> Snapshot {
565        let loss = out.sum(None).expect("sum");
566        let grads = tape.backward(&loss).expect("backward");
567        let leaves = (0..tape.leaf_count())
568            .map(|i| {
569                let v = tape.leaf(i).expect("leaf");
570                grads.get(&v).expect("get").map(bits)
571            })
572            .collect();
573        (
574            bits(&out.value()),
575            grads.get(x).expect("get").map(bits),
576            leaves,
577            tape.0.len(),
578        )
579    }
580
581    #[test]
582    fn adapter_in_autodiff_sequential_matches_manual_chain() {
583        for linear_first in [true, false] {
584            let linear = Linear::new(3, 3, true, 7).expect("linear");
585            let reference = Scale::new();
586
587            let tape = crate::tape();
588            let x = tape.var(&input());
589            let mut h = x;
590            if linear_first {
591                h = linear.bind(&tape.0).forward(&h).expect("linear");
592                h = Module::forward(&reference, TapeRef::from(&tape), &h).expect("scale");
593            } else {
594                h = Module::forward(&reference, TapeRef::from(&tape), &h).expect("scale");
595                h = linear.bind(&tape.0).forward(&h).expect("linear");
596            }
597            let expected = snapshot(&tape, &x, &h);
598
599            let mut seq = Sequential::new();
600            let linear2 = Linear::new(3, 3, true, 7).expect("linear");
601            if linear_first {
602                seq.push(Box::new(linear2));
603                seq.push(Box::new(adapter()));
604            } else {
605                seq.push(Box::new(adapter()));
606                seq.push(Box::new(linear2));
607            }
608            let tape2 = crate::tape();
609            let x2 = tape2.var(&input());
610            let out = AutodiffModule::forward(&seq, &tape2.0, &x2).expect("seq forward");
611            let actual = snapshot(&tape2, &x2, &out);
612            assert_eq!(expected, actual, "linear_first={linear_first}");
613        }
614    }
615
616    #[test]
617    fn adapter_state_dict_semantics_match_facade_layer() {
618        let mut ad = adapter();
619        let direct = Scale::new();
620        let via = AutodiffModule::state_dict(&ad);
621        let want = Module::state_dict(&direct);
622        assert_eq!(via.len(), want.len());
623        assert_eq!(bits(&via["a"]), bits(&want["a"]));
624
625        let mut seq = Sequential::new();
626        seq.push(Box::new(Linear::new(3, 3, false, 1).expect("linear")));
627        seq.push(Box::new(adapter()));
628        let mut sd = AutodiffModule::state_dict(&seq);
629        let new_a = Tensor::from_slice(&[9.0, 8.0, 7.0], &[3]).expect("tensor");
630        sd.insert("1.a".into(), new_a.clone());
631        AutodiffModule::load_state_dict(&mut seq, sd).expect("load");
632        assert_eq!(bits(&AutodiffModule::state_dict(&seq)["1.a"]), bits(&new_a));
633
634        // 欠落キーの Err 文言は facade 層直接と同一。
635        let mut direct_mut = Scale::new();
636        let e_direct = Module::load_state_dict(&mut direct_mut, HashMap::new())
637            .expect_err("missing")
638            .to_string();
639        let e_adapter = AutodiffModule::load_state_dict(&mut ad, HashMap::new())
640            .expect_err("missing")
641            .to_string();
642        assert_eq!(e_direct, e_adapter);
643    }
644
645    #[test]
646    fn adapter_propagates_training_flag() {
647        let mut seq = Sequential::new();
648        seq.push(Box::new(adapter()));
649        assert!(AutodiffModule::training(&seq));
650        AutodiffModule::set_training(&mut seq, false);
651        assert!(!AutodiffModule::training(&seq));
652        assert!(!seq.layers()[0].training());
653        AutodiffModule::set_training(&mut seq, true);
654        assert!(seq.layers()[0].training());
655    }
656
657    #[test]
658    fn default_set_requires_grad_message_matches_autodiff() {
659        // autodiff 側は named_parameters だけを override した同数パラメータの型で比較する。
660        struct AdParams(Tensor<f32>);
661        impl AutodiffModule for AdParams {
662            fn forward<'t>(
663                &self,
664                _tape: &'t fandhe_ai_autodiff::Tape,
665                input: &Var<'t>,
666            ) -> Result<Var<'t>, AutodiffError> {
667                Ok(*input)
668            }
669            fn named_parameters(&self) -> Vec<(String, &Tensor<f32>)> {
670                vec![("p".into(), &self.0)]
671            }
672        }
673        struct FacParams(Tensor<f32>);
674        impl Module for FacParams {
675            fn forward<'t>(
676                &self,
677                _tape: TapeRef<'t>,
678                input: &Var<'t>,
679            ) -> Result<Var<'t>, AutodiffError> {
680                Ok(*input)
681            }
682            fn named_parameters(&self) -> Vec<(String, &Tensor<f32>)> {
683                vec![("p".into(), &self.0)]
684            }
685        }
686        let mine = FacParams(t(1.0))
687            .set_requires_grad(false)
688            .expect_err("fail-closed")
689            .to_string();
690        let theirs = AutodiffModule::set_requires_grad(&mut AdParams(t(1.0)), false)
691            .expect_err("fail-closed")
692            .to_string();
693        assert_eq!(mine, theirs);
694    }
695
696    #[test]
697    fn default_requires_grad_for_stateless_layer() {
698        let mut s = Stateless;
699        assert!(s.set_requires_grad(false).is_ok());
700        assert!(s.freeze().is_ok());
701        assert!(s.requires_grad());
702        let mut p = TwoParam {
703            a: t(1.0),
704            b: t(2.0),
705            fail_b: false,
706        };
707        assert!(matches!(p.freeze(), Err(AutodiffError::InvalidArgument(_))));
708    }
709
710    /// `requires_grad` を実保持する facade 層(アダプタ委譲の検証用)。
711    struct Freezable {
712        a: Tensor<f32>,
713        rg: bool,
714    }
715    impl Module for Freezable {
716        fn forward<'t>(
717            &self,
718            tape: TapeRef<'t>,
719            input: &Var<'t>,
720        ) -> Result<Var<'t>, AutodiffError> {
721            let a = if self.rg {
722                tape.var(&self.a)
723            } else {
724                tape.var_no_grad(&self.a)
725            };
726            input.mul(&a)
727        }
728        fn named_parameters(&self) -> Vec<(String, &Tensor<f32>)> {
729            vec![("a".into(), &self.a)]
730        }
731        fn set_requires_grad(&mut self, v: bool) -> Result<(), AutodiffError> {
732            self.rg = v;
733            Ok(())
734        }
735        fn requires_grad(&self) -> bool {
736            self.rg
737        }
738    }
739
740    #[test]
741    fn adapter_delegates_set_requires_grad_and_requires_grad() {
742        let mut seq = Sequential::new();
743        seq.push(Box::new(FacadeModuleAdapter(Box::new(Freezable {
744            a: Tensor::from_slice(&[1.0, 2.0, 3.0], &[3]).expect("tensor"),
745            rg: true,
746        }) as Box<dyn Module>)));
747        assert!(AutodiffModule::requires_grad(&seq));
748        AutodiffModule::freeze(&mut seq).expect("freeze");
749        assert!(!AutodiffModule::requires_grad(&seq));
750        assert!(!seq.layers()[0].requires_grad());
751        AutodiffModule::set_requires_grad(&mut seq, true).expect("unfreeze");
752        assert!(AutodiffModule::requires_grad(&seq));
753
754        // 未 override のパラメータ持ち facade 層は fail-closed で Err。
755        let mut bad = Sequential::new();
756        bad.push(Box::new(adapter()));
757        assert!(AutodiffModule::freeze(&mut bad).is_err());
758    }
759    #[test]
760    fn adapter_delegates_type_name_but_not_children() {
761        let ad = adapter();
762        assert_eq!(
763            AutodiffModule::type_name(&ad),
764            Module::type_name(&Scale::new())
765        );
766        assert!(AutodiffModule::children(&ad).is_empty());
767        assert_eq!(
768            AutodiffModule::parameter_count(&ad),
769            Module::parameter_count(&Scale::new())
770        );
771
772        let mut seq = Sequential::new();
773        seq.push(Box::new(adapter()));
774        seq.push(Box::new(adapter()));
775        let names: Vec<String> = AutodiffModule::named_modules(&seq)
776            .into_iter()
777            .map(|(n, _)| n)
778            .collect();
779        assert_eq!(names, vec!["0".to_string(), "1".to_string()]);
780    }
781
782    #[test]
783    fn adapter_host_inference_is_fail_closed() {
784        let ad = adapter();
785        assert!(!AutodiffModule::supports_forward_host(&ad));
786        let ops = fandhe_ai_backend_cpu::CpuBackendOps::new();
787        let e = AutodiffModule::forward_host(&ad, &ops, &input()).expect_err("must be Err");
788        assert!(matches!(e, AutodiffError::InvalidArgument(_)), "{e}");
789
790        let mut seq = Sequential::new();
791        seq.push(Box::new(adapter()));
792        let e = AutodiffModule::forward_host(&seq, &ops, &input()).expect_err("must be Err");
793        assert!(matches!(e, AutodiffError::InvalidArgument(_)), "{e}");
794
795        assert!(AutodiffModule::as_linear(&ad).is_none());
796        assert!(!AutodiffModule::is_pooling(&ad));
797    }
798}