From 6e3554bac76f96852361bc1226dd89f8ac5f1562 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:14:58 +0800 Subject: [PATCH 1/9] fix(model): fit residual bias changes for preset outputs --- .../dpmodel/atomic_model/base_atomic_model.py | 12 +++++---- .../model/atomic_model/base_atomic_model.py | 12 +++++---- .../model/atomic_model/base_atomic_model.py | 12 +++++---- .../dpmodel/test_atomic_model_global_stat.py | 23 +++++++++++----- .../pd/model/test_atomic_model_global_stat.py | 26 ++++++++++++++----- .../pt/model/test_atomic_model_global_stat.py | 26 ++++++++++++++----- 6 files changed, 78 insertions(+), 33 deletions(-) diff --git a/deepmd/dpmodel/atomic_model/base_atomic_model.py b/deepmd/dpmodel/atomic_model/base_atomic_model.py index 3a7922e8bd..a665be29ae 100644 --- a/deepmd/dpmodel/atomic_model/base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/base_atomic_model.py @@ -817,12 +817,14 @@ def change_out_bias( 'change-by-statistic' : perform predictions on labels of target dataset, and do least square on the errors to obtain the target shift as bias. 'set-by-statistic' : directly use the statistic output bias in the target dataset. - An output assigned in `preset_out_bias` is fixed by the preset in both modes: + In 'set-by-statistic' mode, an output assigned in `preset_out_bias` + is fixed by the preset: every element in the data must be assigned, the assigned types take the - preset value, and the types absent from the data keep zero - ('set-by-statistic') or their stored bias ('change-by-statistic'); no + preset value, and the types absent from the data keep zero; no statistics are computed, read or written for such an output, whose output std keeps its stored value. + In 'change-by-statistic' mode, presets are treated as part of the + current stored bias and the residual statistics are fitted normally. stat_file_path : Optional[DPPath] The path to the stat file. observed_type : list[str], optional @@ -842,7 +844,7 @@ def change_out_bias( sampled = sample_merged # === Step 1. Outputs fixed by the preset === fitted_keys = self.bias_keys - if self.preset_out_bias: + if self.preset_out_bias and not change: if observed_type is None: sampled = sample_merged() if callable(sample_merged) else sample_merged observed_type = collect_observed_types(sampled, self.type_map) @@ -853,7 +855,7 @@ def change_out_bias( to_numpy_array(self.out_bias), self.bias_keys, [self.atomic_output_def()[kk].size for kk in self.bias_keys], - keep_unassigned=change, + keep_unassigned=False, excluded_types=self.atom_exclude_types, ) self._store_out_stat(rows) diff --git a/deepmd/pd/model/atomic_model/base_atomic_model.py b/deepmd/pd/model/atomic_model/base_atomic_model.py index 8a8aaefd48..5ce71605b8 100644 --- a/deepmd/pd/model/atomic_model/base_atomic_model.py +++ b/deepmd/pd/model/atomic_model/base_atomic_model.py @@ -518,12 +518,14 @@ def change_out_bias( 'change-by-statistic' : perform predictions on labels of target dataset, and do least square on the errors to obtain the target shift as bias. 'set-by-statistic' : directly use the statistic output bias in the target dataset. - An output assigned in `preset_out_bias` is fixed by the preset in both modes: + In 'set-by-statistic' mode, an output assigned in `preset_out_bias` + is fixed by the preset: every element in the data must be assigned, the assigned types take the - preset value, and the types absent from the data keep zero - ('set-by-statistic') or their stored bias ('change-by-statistic'); no + preset value, and the types absent from the data keep zero; no statistics are computed, read or written for such an output, whose output std keeps its stored value. + In 'change-by-statistic' mode, presets are treated as part of the + current stored bias and the residual statistics are fitted normally. stat_file_path : Optional[DPPath] The path to the stat file. observed_type : list[str], optional @@ -538,7 +540,7 @@ def change_out_bias( sampled = sample_merged # === Step 1. Outputs fixed by the preset === fitted_keys = self.bias_keys - if self.preset_out_bias: + if self.preset_out_bias and not change: if observed_type is None: sampled = sample_merged() if callable(sample_merged) else sample_merged observed_type = collect_observed_types(sampled, self.type_map) @@ -549,7 +551,7 @@ def change_out_bias( to_numpy_array(self.out_bias), self.bias_keys, [self.atomic_output_def()[kk].size for kk in self.bias_keys], - keep_unassigned=change, + keep_unassigned=False, excluded_types=self.atom_exclude_types, ) self._store_out_stat({kk: to_paddle_tensor(vv) for kk, vv in rows.items()}) diff --git a/deepmd/pt/model/atomic_model/base_atomic_model.py b/deepmd/pt/model/atomic_model/base_atomic_model.py index d18e780cd3..91c251098f 100644 --- a/deepmd/pt/model/atomic_model/base_atomic_model.py +++ b/deepmd/pt/model/atomic_model/base_atomic_model.py @@ -648,12 +648,14 @@ def change_out_bias( 'change-by-statistic' : perform predictions on labels of target dataset, and do least square on the errors to obtain the target shift as bias. 'set-by-statistic' : directly use the statistic output bias in the target dataset. - An output assigned in `preset_out_bias` is fixed by the preset in both modes: + In 'set-by-statistic' mode, an output assigned in `preset_out_bias` + is fixed by the preset: every element in the data must be assigned, the assigned types take the - preset value, and the types absent from the data keep zero - ('set-by-statistic') or their stored bias ('change-by-statistic'); no + preset value, and the types absent from the data keep zero; no statistics are computed, read or written for such an output, whose output std keeps its stored value. + In 'change-by-statistic' mode, presets are treated as part of the + current stored bias and the residual statistics are fitted normally. model_forward : Callable[..., dict[str, torch.Tensor]], optional Predictor of the complete atomic outputs used by 'change-by-statistic'. Defaults to the forward of this atomic model; a model that adds @@ -670,7 +672,7 @@ def change_out_bias( sampled = sample_merged # === Step 1. Outputs fixed by the preset === fitted_keys = self.bias_keys - if self.preset_out_bias: + if self.preset_out_bias and not change: if observed_type is None: sampled = sample_merged() if callable(sample_merged) else sample_merged observed_type = collect_observed_types(sampled, self.type_map) @@ -681,7 +683,7 @@ def change_out_bias( to_numpy_array(self.out_bias), self.bias_keys, [self.atomic_output_def()[kk].size for kk in self.bias_keys], - keep_unassigned=change, + keep_unassigned=False, excluded_types=self.atom_exclude_types, ) self._store_out_stat({kk: to_torch_tensor(vv) for kk, vv in rows.items()}) diff --git a/source/tests/common/dpmodel/test_atomic_model_global_stat.py b/source/tests/common/dpmodel/test_atomic_model_global_stat.py index 0059374e0a..54a3fa792f 100644 --- a/source/tests/common/dpmodel/test_atomic_model_global_stat.py +++ b/source/tests/common/dpmodel/test_atomic_model_global_stat.py @@ -319,16 +319,27 @@ def test_preset_bias(self) -> None: for kk in ["foo", "pix", "bar"]: np.testing.assert_almost_equal(ret1[kk], expected_ret1[kk]) - # 3. change-by-statistic keeps every assigned type at its preset + # 3. change-by-statistic treats the preset as the stored starting bias + # and fits a downstream residual shift. md0.change_out_bias( self.merged_output_stat, bias_adjust_mode="change-by-statistic" ) - ret3 = md0.forward_common_atomic(*args) - for kk in ["foo", "pix", "bar"]: - np.testing.assert_almost_equal(ret3[kk], ret1[kk]) + args_stat = [ + self.coord_ext, + np.array(self.merged_output_stat[0]["atype_ext"], dtype=np.int64), + self.nlist, + ] + ret3 = md0.forward_common_atomic(*args_stat) + expected_ret3 = {} + expected_ret3["foo"] = np.array([[3, 4, -2], [6, 0, 1]]).reshape(2, 3, 1) + expected_ret3["pix"] = ret0["pix"] + for kk in ["foo", "pix"]: + np.testing.assert_almost_equal(ret3[kk], expected_ret3[kk]) out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) - np.testing.assert_almost_equal(out_bias["foo"], foo_bias) - np.testing.assert_almost_equal(out_bias["bar"], bar_bias) + np.testing.assert_almost_equal( + out_bias["foo"], np.array([2.0, -5.0]).reshape(2, 1) + ) + assert not np.allclose(out_bias["bar"], bar_bias) # 4. a preset that leaves an observed type unassigned is rejected md1 = DPDPAtomicModel( diff --git a/source/tests/pd/model/test_atomic_model_global_stat.py b/source/tests/pd/model/test_atomic_model_global_stat.py index bacec7e682..c8ba1e176f 100644 --- a/source/tests/pd/model/test_atomic_model_global_stat.py +++ b/source/tests/pd/model/test_atomic_model_global_stat.py @@ -349,17 +349,31 @@ def cvt_ret(x): expected_ret1["bar"] = ret0["bar"] + bar_bias[at] for kk in ["foo", "pix", "bar"]: np.testing.assert_almost_equal(ret1[kk], expected_ret1[kk]) - # 3. change-by-statistic keeps every assigned type at its preset + # 3. change-by-statistic treats the preset as the stored starting bias + # and fits a downstream residual shift. BaseAtomicModel.change_out_bias( md0, self.merged_output_stat, bias_adjust_mode="change-by-statistic" ) - ret3 = md0.forward_common_atomic(*args) + args_stat = [ + to_paddle_tensor(ii) + for ii in [ + self.coord_ext, + np.array(self.merged_output_stat[0]["atype_ext"], dtype=np.int64), + self.nlist, + ] + ] + ret3 = md0.forward_common_atomic(*args_stat) ret3 = cvt_ret(ret3) - for kk in ["foo", "pix", "bar"]: - np.testing.assert_almost_equal(ret3[kk], ret1[kk]) + expected_ret3 = {} + expected_ret3["foo"] = np.array([[3, 4, -2], [6, 0, 1]]).reshape(2, 3, 1) + expected_ret3["pix"] = ret0["pix"] + for kk in ["foo", "pix"]: + np.testing.assert_almost_equal(ret3[kk], expected_ret3[kk]) out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) - np.testing.assert_almost_equal(to_numpy_array(out_bias["foo"]), foo_bias) - np.testing.assert_almost_equal(to_numpy_array(out_bias["bar"]), bar_bias) + np.testing.assert_almost_equal( + to_numpy_array(out_bias["foo"]), np.array([2.0, -5.0]).reshape(2, 1) + ) + assert not np.allclose(to_numpy_array(out_bias["bar"]), bar_bias) # 4. a preset that leaves an observed type unassigned is rejected md1 = DPAtomicModel( ds, diff --git a/source/tests/pt/model/test_atomic_model_global_stat.py b/source/tests/pt/model/test_atomic_model_global_stat.py index a4bca815b9..1388d3f16b 100644 --- a/source/tests/pt/model/test_atomic_model_global_stat.py +++ b/source/tests/pt/model/test_atomic_model_global_stat.py @@ -375,17 +375,31 @@ def cvt_ret(x): expected_ret1["bar"] = ret0["bar"] + bar_bias[at] for kk in ["foo", "pix", "bar"]: np.testing.assert_almost_equal(ret1[kk], expected_ret1[kk]) - # 3. change-by-statistic keeps every assigned type at its preset + # 3. change-by-statistic treats the preset as the stored starting bias + # and fits a downstream residual shift. BaseAtomicModel.change_out_bias( md0, self.merged_output_stat, bias_adjust_mode="change-by-statistic" ) - ret3 = md0.forward_common_atomic(*args, fparam=self.fparam) + args_stat = [ + to_torch_tensor(ii) + for ii in [ + self.coord_ext, + np.array(self.merged_output_stat[0]["atype_ext"], dtype=np.int64), + self.nlist, + ] + ] + ret3 = md0.forward_common_atomic(*args_stat, fparam=self.fparam) ret3 = cvt_ret(ret3) - for kk in ["foo", "pix", "bar"]: - np.testing.assert_almost_equal(ret3[kk], ret1[kk]) + expected_ret3 = {} + expected_ret3["foo"] = np.array([[3, 4, -2], [6, 0, 1]]).reshape(2, 3, 1) + expected_ret3["pix"] = ret0["pix"] + for kk in ["foo", "pix"]: + np.testing.assert_almost_equal(ret3[kk], expected_ret3[kk]) out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) - np.testing.assert_almost_equal(to_numpy_array(out_bias["foo"]), foo_bias) - np.testing.assert_almost_equal(to_numpy_array(out_bias["bar"]), bar_bias) + np.testing.assert_almost_equal( + to_numpy_array(out_bias["foo"]), np.array([2.0, -5.0]).reshape(2, 1) + ) + assert not np.allclose(to_numpy_array(out_bias["bar"]), bar_bias) # 4. a preset that leaves an observed type unassigned is rejected md1 = DPAtomicModel( ds, From d085730967d38e2658bc7cd5399c57fcd196c21f Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:34:42 +0800 Subject: [PATCH 2/9] fix(finetune): honor explicit downstream output bias presets --- deepmd/dpmodel/atomic_model/base_atomic_model.py | 12 ++++++++---- deepmd/dpmodel/model/make_model.py | 5 ++++- deepmd/dpmodel/model/spin_model.py | 2 ++ deepmd/dpmodel/train/trainer.py | 2 ++ deepmd/jax/train/trainer.py | 1 + deepmd/pd/model/atomic_model/base_atomic_model.py | 12 ++++++++---- deepmd/pd/model/model/make_model.py | 2 ++ deepmd/pd/train/training.py | 3 +++ deepmd/pt/model/atomic_model/base_atomic_model.py | 12 ++++++++---- deepmd/pt/model/model/make_model.py | 2 ++ deepmd/pt/model/model/sezm_spin_model.py | 2 ++ deepmd/pt/model/model/spin_model.py | 2 ++ deepmd/pt/train/training.py | 3 +++ deepmd/utils/argcheck.py | 7 +++++-- .../dpmodel/test_atomic_model_global_stat.py | 12 ++++++++++++ source/tests/common/test_dpmodel_train.py | 14 +++++++++++++- .../pd/model/test_atomic_model_global_stat.py | 13 +++++++++++++ .../pt/model/test_atomic_model_global_stat.py | 13 +++++++++++++ 18 files changed, 103 insertions(+), 16 deletions(-) diff --git a/deepmd/dpmodel/atomic_model/base_atomic_model.py b/deepmd/dpmodel/atomic_model/base_atomic_model.py index a665be29ae..ecad9d08b7 100644 --- a/deepmd/dpmodel/atomic_model/base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/base_atomic_model.py @@ -800,6 +800,7 @@ def change_out_bias( stat_file_path: DPPath | None = None, bias_adjust_mode: str = "change-by-statistic", observed_type: list[str] | None = None, + apply_preset: bool = False, ) -> None: """Change the output bias according to the input data and the pretrained model. @@ -823,8 +824,11 @@ def change_out_bias( preset value, and the types absent from the data keep zero; no statistics are computed, read or written for such an output, whose output std keeps its stored value. - In 'change-by-statistic' mode, presets are treated as part of the - current stored bias and the residual statistics are fitted normally. + In 'change-by-statistic' mode, stored presets are ignored unless + apply_preset is True for an explicitly provided downstream preset. + apply_preset : bool + Apply the current configuration's preset during residual fitting. + Enable only when the downstream operation explicitly supplies it. stat_file_path : Optional[DPPath] The path to the stat file. observed_type : list[str], optional @@ -844,7 +848,7 @@ def change_out_bias( sampled = sample_merged # === Step 1. Outputs fixed by the preset === fitted_keys = self.bias_keys - if self.preset_out_bias and not change: + if self.preset_out_bias and (not change or apply_preset): if observed_type is None: sampled = sample_merged() if callable(sample_merged) else sample_merged observed_type = collect_observed_types(sampled, self.type_map) @@ -855,7 +859,7 @@ def change_out_bias( to_numpy_array(self.out_bias), self.bias_keys, [self.atomic_output_def()[kk].size for kk in self.bias_keys], - keep_unassigned=False, + keep_unassigned=change, excluded_types=self.atom_exclude_types, ) self._store_out_stat(rows) diff --git a/deepmd/dpmodel/model/make_model.py b/deepmd/dpmodel/model/make_model.py index b74fb6b697..3cf8430470 100644 --- a/deepmd/dpmodel/model/make_model.py +++ b/deepmd/dpmodel/model/make_model.py @@ -926,6 +926,7 @@ def change_out_bias( self, merged: Any, bias_adjust_mode: str = "change-by-statistic", + apply_preset: bool = False, ) -> None: """Change the output bias according to the input data and the pretrained model. @@ -937,7 +938,9 @@ def change_out_bias( The mode for changing output bias: 'change-by-statistic' or 'set-by-statistic'. """ - self.atomic_model.change_out_bias(merged, bias_adjust_mode=bias_adjust_mode) + self.atomic_model.change_out_bias( + merged, bias_adjust_mode=bias_adjust_mode, apply_preset=apply_preset + ) def _input_type_cast( self, diff --git a/deepmd/dpmodel/model/spin_model.py b/deepmd/dpmodel/model/spin_model.py index 8678e51398..21c83de014 100644 --- a/deepmd/dpmodel/model/spin_model.py +++ b/deepmd/dpmodel/model/spin_model.py @@ -603,6 +603,7 @@ def change_out_bias( self, merged: Callable[[], list[dict]] | list[dict], bias_adjust_mode: str = "change-by-statistic", + apply_preset: bool = False, ) -> None: """Change the output bias of atomic model according to the input data and the pretrained model. @@ -627,6 +628,7 @@ def change_out_bias( self.backbone_model.change_out_bias( spin_sampled_func, bias_adjust_mode=bias_adjust_mode, + apply_preset=apply_preset, ) def change_type_map( diff --git a/deepmd/dpmodel/train/trainer.py b/deepmd/dpmodel/train/trainer.py index 531bc7d07c..5c6544518b 100644 --- a/deepmd/dpmodel/train/trainer.py +++ b/deepmd/dpmodel/train/trainer.py @@ -74,12 +74,14 @@ def change_model_out_bias( *, bias_adjust_mode: str = "change-by-statistic", recompute_input_stats: bool = False, + apply_preset: bool = False, ) -> Any: """Change one model's output bias and log the before/after values.""" old_bias = deepcopy(model.get_out_bias()) model.change_out_bias( sample_func, bias_adjust_mode=bias_adjust_mode, + apply_preset=apply_preset, ) new_bias = deepcopy(model.get_out_bias()) diff --git a/deepmd/jax/train/trainer.py b/deepmd/jax/train/trainer.py index a35de2a49b..3194398ded 100644 --- a/deepmd/jax/train/trainer.py +++ b/deepmd/jax/train/trainer.py @@ -616,6 +616,7 @@ def _apply_finetune(self) -> None: self.models[model_key].change_out_bias( self._sample_funcs[model_key], bias_adjust_mode=bias_mode, + apply_preset=True, ) def _initialize_stat(self, model_key: str) -> None: diff --git a/deepmd/pd/model/atomic_model/base_atomic_model.py b/deepmd/pd/model/atomic_model/base_atomic_model.py index 5ce71605b8..88ff961fbb 100644 --- a/deepmd/pd/model/atomic_model/base_atomic_model.py +++ b/deepmd/pd/model/atomic_model/base_atomic_model.py @@ -501,6 +501,7 @@ def change_out_bias( stat_file_path: DPPath | None = None, bias_adjust_mode: str = "change-by-statistic", observed_type: list[str] | None = None, + apply_preset: bool = False, ) -> None: """Change the output bias according to the input data and the pretrained model. @@ -524,8 +525,11 @@ def change_out_bias( preset value, and the types absent from the data keep zero; no statistics are computed, read or written for such an output, whose output std keeps its stored value. - In 'change-by-statistic' mode, presets are treated as part of the - current stored bias and the residual statistics are fitted normally. + In 'change-by-statistic' mode, stored presets are ignored unless + apply_preset is True for an explicitly provided downstream preset. + apply_preset : bool + Apply the current configuration's preset during residual fitting. + Enable only when the downstream operation explicitly supplies it. stat_file_path : Optional[DPPath] The path to the stat file. observed_type : list[str], optional @@ -540,7 +544,7 @@ def change_out_bias( sampled = sample_merged # === Step 1. Outputs fixed by the preset === fitted_keys = self.bias_keys - if self.preset_out_bias and not change: + if self.preset_out_bias and (not change or apply_preset): if observed_type is None: sampled = sample_merged() if callable(sample_merged) else sample_merged observed_type = collect_observed_types(sampled, self.type_map) @@ -551,7 +555,7 @@ def change_out_bias( to_numpy_array(self.out_bias), self.bias_keys, [self.atomic_output_def()[kk].size for kk in self.bias_keys], - keep_unassigned=False, + keep_unassigned=change, excluded_types=self.atom_exclude_types, ) self._store_out_stat({kk: to_paddle_tensor(vv) for kk, vv in rows.items()}) diff --git a/deepmd/pd/model/model/make_model.py b/deepmd/pd/model/model/make_model.py index f5beb501fc..294d395e51 100644 --- a/deepmd/pd/model/model/make_model.py +++ b/deepmd/pd/model/model/make_model.py @@ -231,6 +231,7 @@ def change_out_bias( self, merged: list[dict] | Callable[[], list[dict]], bias_adjust_mode: str = "change-by-statistic", + apply_preset: bool = False, ) -> None: """Change the output bias of atomic model according to the input data and the pretrained model. @@ -252,6 +253,7 @@ def change_out_bias( self.atomic_model.change_out_bias( merged, bias_adjust_mode=bias_adjust_mode, + apply_preset=apply_preset, ) def forward_common_lower( diff --git a/deepmd/pd/train/training.py b/deepmd/pd/train/training.py index a33908c632..906225235b 100644 --- a/deepmd/pd/train/training.py +++ b/deepmd/pd/train/training.py @@ -647,6 +647,7 @@ def single_model_finetune( _bias_adjust_mode="change-by-statistic" if not _finetune_rule_single.get_random_fitting() else "set-by-statistic", + apply_preset=True, ) return _model @@ -1455,11 +1456,13 @@ def model_change_out_bias( _model: Any, _sample_func: Any, _bias_adjust_mode: str = "change-by-statistic", + apply_preset: bool = False, ) -> None: old_bias = deepcopy(_model.get_out_bias()) _model.change_out_bias( _sample_func, bias_adjust_mode=_bias_adjust_mode, + apply_preset=apply_preset, ) new_bias = deepcopy(_model.get_out_bias()) diff --git a/deepmd/pt/model/atomic_model/base_atomic_model.py b/deepmd/pt/model/atomic_model/base_atomic_model.py index 91c251098f..01a95dad85 100644 --- a/deepmd/pt/model/atomic_model/base_atomic_model.py +++ b/deepmd/pt/model/atomic_model/base_atomic_model.py @@ -629,6 +629,7 @@ def change_out_bias( bias_adjust_mode: str = "change-by-statistic", model_forward: Callable[..., dict[str, torch.Tensor]] | None = None, observed_type: list[str] | None = None, + apply_preset: bool = False, ) -> None: """Change the output bias according to the input data and the pretrained model. @@ -654,8 +655,11 @@ def change_out_bias( preset value, and the types absent from the data keep zero; no statistics are computed, read or written for such an output, whose output std keeps its stored value. - In 'change-by-statistic' mode, presets are treated as part of the - current stored bias and the residual statistics are fitted normally. + In 'change-by-statistic' mode, stored presets are ignored unless + apply_preset is True for an explicitly provided downstream preset. + apply_preset : bool + Apply the current configuration's preset during residual fitting. + Enable only when the downstream operation explicitly supplies it. model_forward : Callable[..., dict[str, torch.Tensor]], optional Predictor of the complete atomic outputs used by 'change-by-statistic'. Defaults to the forward of this atomic model; a model that adds @@ -672,7 +676,7 @@ def change_out_bias( sampled = sample_merged # === Step 1. Outputs fixed by the preset === fitted_keys = self.bias_keys - if self.preset_out_bias and not change: + if self.preset_out_bias and (not change or apply_preset): if observed_type is None: sampled = sample_merged() if callable(sample_merged) else sample_merged observed_type = collect_observed_types(sampled, self.type_map) @@ -683,7 +687,7 @@ def change_out_bias( to_numpy_array(self.out_bias), self.bias_keys, [self.atomic_output_def()[kk].size for kk in self.bias_keys], - keep_unassigned=False, + keep_unassigned=change, excluded_types=self.atom_exclude_types, ) self._store_out_stat({kk: to_torch_tensor(vv) for kk, vv in rows.items()}) diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index 185122349d..523f93b3cf 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -335,6 +335,7 @@ def change_out_bias( self, merged: Any, bias_adjust_mode: str = "change-by-statistic", + apply_preset: bool = False, ) -> None: """Change the output bias of atomic model according to the input data and the pretrained model. @@ -359,6 +360,7 @@ def change_out_bias( self.atomic_model.change_out_bias( merged, bias_adjust_mode=bias_adjust_mode, + apply_preset=apply_preset, model_forward=self.predict_atomic_outputs_for_stat, ) diff --git a/deepmd/pt/model/model/sezm_spin_model.py b/deepmd/pt/model/model/sezm_spin_model.py index ace082b02b..7cbe965302 100644 --- a/deepmd/pt/model/model/sezm_spin_model.py +++ b/deepmd/pt/model/model/sezm_spin_model.py @@ -432,6 +432,7 @@ def change_out_bias( self, merged: Callable[[], list[dict[str, Any]]] | list[dict[str, Any]], bias_adjust_mode: str = "change-by-statistic", + apply_preset: bool = False, ) -> None: """Change output bias using spin-expanded sampled data.""" spin_sampled_func = self._get_spin_sampled_func( @@ -440,6 +441,7 @@ def change_out_bias( super().change_out_bias( spin_sampled_func, bias_adjust_mode=bias_adjust_mode, + apply_preset=apply_preset, ) def change_type_map( diff --git a/deepmd/pt/model/model/spin_model.py b/deepmd/pt/model/model/spin_model.py index 550de0bfa4..cb8c303ce0 100644 --- a/deepmd/pt/model/model/spin_model.py +++ b/deepmd/pt/model/model/spin_model.py @@ -518,6 +518,7 @@ def change_out_bias( self, merged: Callable[[], list[dict]] | list[dict], bias_adjust_mode: str = "change-by-statistic", + apply_preset: bool = False, ) -> None: """Change the output bias of atomic model according to the input data and the pretrained model. @@ -542,6 +543,7 @@ def change_out_bias( self.backbone_model.change_out_bias( spin_sampled_func, bias_adjust_mode=bias_adjust_mode, + apply_preset=apply_preset, ) def change_type_map( diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 7a7210962e..2681344fd6 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -975,6 +975,7 @@ def update_finetune_bias( _bias_adjust_mode="change-by-statistic" if not _finetune_rule.get_random_fitting() else "set-by-statistic", + apply_preset=True, ) if self.multi_task: self.model[_model_key] = model @@ -2600,6 +2601,7 @@ def model_change_out_bias( _model: Any, _sample_func: Callable[[], Any], _bias_adjust_mode: str = "change-by-statistic", + apply_preset: bool = False, ) -> Any: from deepmd.pt.model.model.dp_model import ( DPModelCommon, @@ -2610,4 +2612,5 @@ def model_change_out_bias( _sample_func, bias_adjust_mode=_bias_adjust_mode, recompute_input_stats=isinstance(_model, DPModelCommon), + apply_preset=apply_preset, ) diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index 982c5d2a26..1457bb4ca4 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -3646,8 +3646,11 @@ def model_args( "for tensor outputs, e.g. `{'dipole': [null, [0., 1., 2.]]}`. " "Elements outside the `type_map` are ignored and every element that occurs in the data must be assigned. " "A table is resolved once when the input is processed and its values are stored in the model. An assigned " - "output is taken from the preset without statistics, both when a model is initialized and when its bias is " - "changed by fine-tuning or `dp change-bias`. Dipole models apply no output bias and take no preset." + "output is taken from the preset without statistics when a model is initialized. During fine-tuning, " + "a preset explicitly provided in the downstream input overrides the loaded bias and skips fitting for " + "that output; without a downstream preset, the loaded bias is adjusted by fitting residuals. " + "`dp change-bias` in change mode fits residuals without reapplying the model's stored preset. " + "Dipole models apply no output bias and take no preset." ) doc_finetune_head = ( "The chosen fitting net to fine-tune on, when doing multi-task fine-tuning. " diff --git a/source/tests/common/dpmodel/test_atomic_model_global_stat.py b/source/tests/common/dpmodel/test_atomic_model_global_stat.py index 54a3fa792f..5313e9f57d 100644 --- a/source/tests/common/dpmodel/test_atomic_model_global_stat.py +++ b/source/tests/common/dpmodel/test_atomic_model_global_stat.py @@ -341,6 +341,18 @@ def test_preset_bias(self) -> None: ) assert not np.allclose(out_bias["bar"], bar_bias) + # An explicit downstream preset overrides the fitted bias. + new_foo_bias = foo_bias + 7.0 + md0.preset_out_bias["foo"] = new_foo_bias.tolist() + md0.change_out_bias( + self.merged_output_stat, + bias_adjust_mode="change-by-statistic", + apply_preset=True, + ) + out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) + np.testing.assert_almost_equal(out_bias["foo"], new_foo_bias) + np.testing.assert_almost_equal(out_bias["bar"], bar_bias) + # 4. a preset that leaves an observed type unassigned is rejected md1 = DPDPAtomicModel( ds, diff --git a/source/tests/common/test_dpmodel_train.py b/source/tests/common/test_dpmodel_train.py index 22149f8e99..884346d710 100644 --- a/source/tests/common/test_dpmodel_train.py +++ b/source/tests/common/test_dpmodel_train.py @@ -26,7 +26,8 @@ def __init__(self, bias: float = 0.0) -> None: def get_out_bias(self): return self.out_bias - def change_out_bias(self, sample_func, bias_adjust_mode): + def change_out_bias(self, sample_func, bias_adjust_mode, apply_preset=False): + self.apply_preset = apply_preset self.change_calls.append((sample_func, bias_adjust_mode)) self.out_bias = self.out_bias + 1.0 @@ -38,6 +39,17 @@ def get_fitting_net(self): class TestChangeModelOutBias(unittest.TestCase): + def test_change_model_out_bias_explicit_preset(self): + model = FakeModel() + + def sample_func(): + return [] + + change_model_out_bias(model, sample_func) + self.assertFalse(model.apply_preset) + change_model_out_bias(model, sample_func, apply_preset=True) + self.assertTrue(model.apply_preset) + def test_change_model_out_bias_recomputes_input_stats_when_requested(self): model = FakeModel() diff --git a/source/tests/pd/model/test_atomic_model_global_stat.py b/source/tests/pd/model/test_atomic_model_global_stat.py index c8ba1e176f..93c3096f27 100644 --- a/source/tests/pd/model/test_atomic_model_global_stat.py +++ b/source/tests/pd/model/test_atomic_model_global_stat.py @@ -374,6 +374,19 @@ def cvt_ret(x): to_numpy_array(out_bias["foo"]), np.array([2.0, -5.0]).reshape(2, 1) ) assert not np.allclose(to_numpy_array(out_bias["bar"]), bar_bias) + + # An explicit downstream preset overrides the fitted bias. + new_foo_bias = foo_bias + 7.0 + md0.preset_out_bias["foo"] = new_foo_bias.tolist() + BaseAtomicModel.change_out_bias( + md0, + self.merged_output_stat, + bias_adjust_mode="change-by-statistic", + apply_preset=True, + ) + out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) + np.testing.assert_almost_equal(to_numpy_array(out_bias["foo"]), new_foo_bias) + np.testing.assert_almost_equal(to_numpy_array(out_bias["bar"]), bar_bias) # 4. a preset that leaves an observed type unassigned is rejected md1 = DPAtomicModel( ds, diff --git a/source/tests/pt/model/test_atomic_model_global_stat.py b/source/tests/pt/model/test_atomic_model_global_stat.py index 1388d3f16b..81ceec0577 100644 --- a/source/tests/pt/model/test_atomic_model_global_stat.py +++ b/source/tests/pt/model/test_atomic_model_global_stat.py @@ -400,6 +400,19 @@ def cvt_ret(x): to_numpy_array(out_bias["foo"]), np.array([2.0, -5.0]).reshape(2, 1) ) assert not np.allclose(to_numpy_array(out_bias["bar"]), bar_bias) + + # An explicit downstream preset overrides the fitted bias. + new_foo_bias = foo_bias + 7.0 + md0.preset_out_bias["foo"] = new_foo_bias.tolist() + BaseAtomicModel.change_out_bias( + md0, + self.merged_output_stat, + bias_adjust_mode="change-by-statistic", + apply_preset=True, + ) + out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) + np.testing.assert_almost_equal(to_numpy_array(out_bias["foo"]), new_foo_bias) + np.testing.assert_almost_equal(to_numpy_array(out_bias["bar"]), bar_bias) # 4. a preset that leaves an observed type unassigned is rejected md1 = DPAtomicModel( ds, From 8cffa358336cdbfdf3b2afff8e4f182038a8b84e Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Tue, 13 Oct 2026 12:40:18 +0800 Subject: [PATCH 3/9] fix(model): unify downstream preset bias updates --- .../dpmodel/atomic_model/base_atomic_model.py | 23 +++++++++++-------- deepmd/dpmodel/model/make_model.py | 6 +++-- deepmd/dpmodel/model/spin_model.py | 4 ++-- deepmd/dpmodel/train/trainer.py | 10 ++++++-- deepmd/jax/train/trainer.py | 8 ++++++- .../model/atomic_model/base_atomic_model.py | 23 +++++++++++-------- deepmd/pd/model/model/make_model.py | 4 ++-- deepmd/pd/train/training.py | 17 ++++++++++---- .../model/atomic_model/base_atomic_model.py | 23 +++++++++++-------- deepmd/pt/model/model/make_model.py | 4 ++-- deepmd/pt/model/model/sezm_spin_model.py | 4 ++-- deepmd/pt/model/model/spin_model.py | 4 ++-- deepmd/pt/train/training.py | 14 ++++++++--- deepmd/utils/argcheck.py | 1 + .../dpmodel/test_atomic_model_global_stat.py | 17 ++++++++++++-- source/tests/common/test_dpmodel_train.py | 14 +++++++---- source/tests/jax/test_training.py | 6 +++++ .../pd/model/test_atomic_model_global_stat.py | 18 +++++++++++++-- .../pt/model/test_atomic_model_global_stat.py | 18 +++++++++++++-- source/tests/pt/test_training.py | 4 +++- 20 files changed, 161 insertions(+), 61 deletions(-) diff --git a/deepmd/dpmodel/atomic_model/base_atomic_model.py b/deepmd/dpmodel/atomic_model/base_atomic_model.py index ecad9d08b7..871cf15efb 100644 --- a/deepmd/dpmodel/atomic_model/base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/base_atomic_model.py @@ -800,7 +800,7 @@ def change_out_bias( stat_file_path: DPPath | None = None, bias_adjust_mode: str = "change-by-statistic", observed_type: list[str] | None = None, - apply_preset: bool = False, + preset_out_bias: dict | None = None, ) -> None: """Change the output bias according to the input data and the pretrained model. @@ -824,11 +824,11 @@ def change_out_bias( preset value, and the types absent from the data keep zero; no statistics are computed, read or written for such an output, whose output std keeps its stored value. - In 'change-by-statistic' mode, stored presets are ignored unless - apply_preset is True for an explicitly provided downstream preset. - apply_preset : bool - Apply the current configuration's preset during residual fitting. - Enable only when the downstream operation explicitly supplies it. + In 'change-by-statistic' mode, stored presets are ignored. + preset_out_bias : dict, optional + An explicit downstream preset. Assigned outputs use this bias and + skip fitting; other outputs fit residuals against the stored bias. + In 'set-by-statistic' mode, the model preset is used when omitted. stat_file_path : Optional[DPPath] The path to the stat file. observed_type : list[str], optional @@ -843,17 +843,22 @@ def change_out_bias( if bias_adjust_mode not in ("change-by-statistic", "set-by-statistic"): raise RuntimeError("Unknown bias_adjust_mode mode: " + bias_adjust_mode) change = bias_adjust_mode == "change-by-statistic" + preset = ( + normalize_preset_out_bias(preset_out_bias, self.type_map) + if preset_out_bias is not None + else (None if change else self.preset_out_bias) + ) distinguish_types = self.get_compute_stats_distinguish_types() - check_preset_out_bias(self.preset_out_bias, self.bias_keys, distinguish_types) + check_preset_out_bias(preset, self.bias_keys, distinguish_types) sampled = sample_merged # === Step 1. Outputs fixed by the preset === fitted_keys = self.bias_keys - if self.preset_out_bias and (not change or apply_preset): + if preset: if observed_type is None: sampled = sample_merged() if callable(sample_merged) else sample_merged observed_type = collect_observed_types(sampled, self.type_map) rows = preset_out_bias_rows( - self.preset_out_bias, + preset, self.type_map, observed_type, to_numpy_array(self.out_bias), diff --git a/deepmd/dpmodel/model/make_model.py b/deepmd/dpmodel/model/make_model.py index 3cf8430470..a7cbd0d357 100644 --- a/deepmd/dpmodel/model/make_model.py +++ b/deepmd/dpmodel/model/make_model.py @@ -926,7 +926,7 @@ def change_out_bias( self, merged: Any, bias_adjust_mode: str = "change-by-statistic", - apply_preset: bool = False, + preset_out_bias: dict | None = None, ) -> None: """Change the output bias according to the input data and the pretrained model. @@ -939,7 +939,9 @@ def change_out_bias( 'change-by-statistic' or 'set-by-statistic'. """ self.atomic_model.change_out_bias( - merged, bias_adjust_mode=bias_adjust_mode, apply_preset=apply_preset + merged, + bias_adjust_mode=bias_adjust_mode, + preset_out_bias=preset_out_bias, ) def _input_type_cast( diff --git a/deepmd/dpmodel/model/spin_model.py b/deepmd/dpmodel/model/spin_model.py index 21c83de014..8930e84e35 100644 --- a/deepmd/dpmodel/model/spin_model.py +++ b/deepmd/dpmodel/model/spin_model.py @@ -603,7 +603,7 @@ def change_out_bias( self, merged: Callable[[], list[dict]] | list[dict], bias_adjust_mode: str = "change-by-statistic", - apply_preset: bool = False, + preset_out_bias: dict | None = None, ) -> None: """Change the output bias of atomic model according to the input data and the pretrained model. @@ -628,7 +628,7 @@ def change_out_bias( self.backbone_model.change_out_bias( spin_sampled_func, bias_adjust_mode=bias_adjust_mode, - apply_preset=apply_preset, + preset_out_bias=preset_out_bias, ) def change_type_map( diff --git a/deepmd/dpmodel/train/trainer.py b/deepmd/dpmodel/train/trainer.py index 5c6544518b..89c0203571 100644 --- a/deepmd/dpmodel/train/trainer.py +++ b/deepmd/dpmodel/train/trainer.py @@ -74,14 +74,14 @@ def change_model_out_bias( *, bias_adjust_mode: str = "change-by-statistic", recompute_input_stats: bool = False, - apply_preset: bool = False, + preset_out_bias: dict | None = None, ) -> Any: """Change one model's output bias and log the before/after values.""" old_bias = deepcopy(model.get_out_bias()) model.change_out_bias( sample_func, bias_adjust_mode=bias_adjust_mode, - apply_preset=apply_preset, + preset_out_bias=preset_out_bias, ) new_bias = deepcopy(model.get_out_bias()) @@ -104,6 +104,7 @@ def change_model_out_bias_by_task( *, bias_adjust_mode: str = "change-by-statistic", recompute_input_stats: bool = False, + preset_out_bias_by_task: Mapping[str, dict | None] | None = None, ) -> MutableMapping[str, Any]: """Change output bias for all requested training-task models.""" log.info("Changing output bias after training.") @@ -113,6 +114,11 @@ def change_model_out_bias_by_task( sample_funcs[model_key], bias_adjust_mode=bias_adjust_mode, recompute_input_stats=recompute_input_stats, + preset_out_bias=( + preset_out_bias_by_task.get(model_key) + if preset_out_bias_by_task is not None + else None + ), ) return models diff --git a/deepmd/jax/train/trainer.py b/deepmd/jax/train/trainer.py index 3194398ded..f906d82eb3 100644 --- a/deepmd/jax/train/trainer.py +++ b/deepmd/jax/train/trainer.py @@ -616,7 +616,9 @@ def _apply_finetune(self) -> None: self.models[model_key].change_out_bias( self._sample_funcs[model_key], bias_adjust_mode=bias_mode, - apply_preset=True, + preset_out_bias=self.model_params_by_task[model_key].get( + "preset_out_bias" + ), ) def _initialize_stat(self, model_key: str) -> None: @@ -929,6 +931,10 @@ def _change_bias_after_training(self) -> None: self._sample_funcs, self.model_keys, bias_adjust_mode="change-by-statistic", + preset_out_bias_by_task={ + key: self.model_params_by_task[key].get("preset_out_bias") + for key in self.model_keys + }, ) self._broadcast_model_states() diff --git a/deepmd/pd/model/atomic_model/base_atomic_model.py b/deepmd/pd/model/atomic_model/base_atomic_model.py index 88ff961fbb..5ae71b42c7 100644 --- a/deepmd/pd/model/atomic_model/base_atomic_model.py +++ b/deepmd/pd/model/atomic_model/base_atomic_model.py @@ -501,7 +501,7 @@ def change_out_bias( stat_file_path: DPPath | None = None, bias_adjust_mode: str = "change-by-statistic", observed_type: list[str] | None = None, - apply_preset: bool = False, + preset_out_bias: dict | None = None, ) -> None: """Change the output bias according to the input data and the pretrained model. @@ -525,11 +525,11 @@ def change_out_bias( preset value, and the types absent from the data keep zero; no statistics are computed, read or written for such an output, whose output std keeps its stored value. - In 'change-by-statistic' mode, stored presets are ignored unless - apply_preset is True for an explicitly provided downstream preset. - apply_preset : bool - Apply the current configuration's preset during residual fitting. - Enable only when the downstream operation explicitly supplies it. + In 'change-by-statistic' mode, stored presets are ignored. + preset_out_bias : dict, optional + An explicit downstream preset. Assigned outputs use this bias and + skip fitting; other outputs fit residuals against the stored bias. + In 'set-by-statistic' mode, the model preset is used when omitted. stat_file_path : Optional[DPPath] The path to the stat file. observed_type : list[str], optional @@ -539,17 +539,22 @@ def change_out_bias( if bias_adjust_mode not in ("change-by-statistic", "set-by-statistic"): raise RuntimeError("Unknown bias_adjust_mode mode: " + bias_adjust_mode) change = bias_adjust_mode == "change-by-statistic" + preset = ( + normalize_preset_out_bias(preset_out_bias, self.type_map) + if preset_out_bias is not None + else (None if change else self.preset_out_bias) + ) distinguish_types = self.get_compute_stats_distinguish_types() - check_preset_out_bias(self.preset_out_bias, self.bias_keys, distinguish_types) + check_preset_out_bias(preset, self.bias_keys, distinguish_types) sampled = sample_merged # === Step 1. Outputs fixed by the preset === fitted_keys = self.bias_keys - if self.preset_out_bias and (not change or apply_preset): + if preset: if observed_type is None: sampled = sample_merged() if callable(sample_merged) else sample_merged observed_type = collect_observed_types(sampled, self.type_map) rows = preset_out_bias_rows( - self.preset_out_bias, + preset, self.type_map, observed_type, to_numpy_array(self.out_bias), diff --git a/deepmd/pd/model/model/make_model.py b/deepmd/pd/model/model/make_model.py index 294d395e51..2d0a288754 100644 --- a/deepmd/pd/model/model/make_model.py +++ b/deepmd/pd/model/model/make_model.py @@ -231,7 +231,7 @@ def change_out_bias( self, merged: list[dict] | Callable[[], list[dict]], bias_adjust_mode: str = "change-by-statistic", - apply_preset: bool = False, + preset_out_bias: dict | None = None, ) -> None: """Change the output bias of atomic model according to the input data and the pretrained model. @@ -253,7 +253,7 @@ def change_out_bias( self.atomic_model.change_out_bias( merged, bias_adjust_mode=bias_adjust_mode, - apply_preset=apply_preset, + preset_out_bias=preset_out_bias, ) def forward_common_lower( diff --git a/deepmd/pd/train/training.py b/deepmd/pd/train/training.py index 906225235b..ccce989229 100644 --- a/deepmd/pd/train/training.py +++ b/deepmd/pd/train/training.py @@ -640,6 +640,7 @@ def single_model_finetune( _model: Any, _finetune_rule_single: Any, _sample_func: Any, + _model_params: dict, ) -> Any: _model = model_change_out_bias( _model, @@ -647,14 +648,17 @@ def single_model_finetune( _bias_adjust_mode="change-by-statistic" if not _finetune_rule_single.get_random_fitting() else "set-by-statistic", - apply_preset=True, + preset_out_bias=_model_params.get("preset_out_bias"), ) return _model if not self.multi_task: finetune_rule_single = self.finetune_links["Default"] self.model = single_model_finetune( - self.model, finetune_rule_single, self.get_sample_func + self.model, + finetune_rule_single, + self.get_sample_func, + model_params, ) else: for model_key in self.model_keys: @@ -667,6 +671,7 @@ def single_model_finetune( self.model[model_key], finetune_rule_single, self.get_sample_func[model_key], + model_params["model_dict"][model_key], ) else: log.info( @@ -1088,6 +1093,7 @@ def log_loss_valid(_task_key: str = "Default") -> dict: self.model, self.get_sample_func, _bias_adjust_mode="change-by-statistic", + preset_out_bias=self.wrapper.model_params.get("preset_out_bias"), ) else: for model_key in self.model_keys: @@ -1095,6 +1101,9 @@ def log_loss_valid(_task_key: str = "Default") -> dict: self.model[model_key], self.get_sample_func[model_key], _bias_adjust_mode="change-by-statistic", + preset_out_bias=self.wrapper.model_params["model_dict"][ + model_key + ].get("preset_out_bias"), ) self.latest_model = Path(self.save_ckpt + f"-{self.num_steps}.pd") cur_lr = self.lr_schedule.value(self.num_steps - 1) @@ -1456,13 +1465,13 @@ def model_change_out_bias( _model: Any, _sample_func: Any, _bias_adjust_mode: str = "change-by-statistic", - apply_preset: bool = False, + preset_out_bias: dict | None = None, ) -> None: old_bias = deepcopy(_model.get_out_bias()) _model.change_out_bias( _sample_func, bias_adjust_mode=_bias_adjust_mode, - apply_preset=apply_preset, + preset_out_bias=preset_out_bias, ) new_bias = deepcopy(_model.get_out_bias()) diff --git a/deepmd/pt/model/atomic_model/base_atomic_model.py b/deepmd/pt/model/atomic_model/base_atomic_model.py index 01a95dad85..82cc7829f9 100644 --- a/deepmd/pt/model/atomic_model/base_atomic_model.py +++ b/deepmd/pt/model/atomic_model/base_atomic_model.py @@ -629,7 +629,7 @@ def change_out_bias( bias_adjust_mode: str = "change-by-statistic", model_forward: Callable[..., dict[str, torch.Tensor]] | None = None, observed_type: list[str] | None = None, - apply_preset: bool = False, + preset_out_bias: dict | None = None, ) -> None: """Change the output bias according to the input data and the pretrained model. @@ -655,11 +655,11 @@ def change_out_bias( preset value, and the types absent from the data keep zero; no statistics are computed, read or written for such an output, whose output std keeps its stored value. - In 'change-by-statistic' mode, stored presets are ignored unless - apply_preset is True for an explicitly provided downstream preset. - apply_preset : bool - Apply the current configuration's preset during residual fitting. - Enable only when the downstream operation explicitly supplies it. + In 'change-by-statistic' mode, stored presets are ignored. + preset_out_bias : dict, optional + An explicit downstream preset. Assigned outputs use this bias and + skip fitting; other outputs fit residuals against the stored bias. + In 'set-by-statistic' mode, the model preset is used when omitted. model_forward : Callable[..., dict[str, torch.Tensor]], optional Predictor of the complete atomic outputs used by 'change-by-statistic'. Defaults to the forward of this atomic model; a model that adds @@ -671,17 +671,22 @@ def change_out_bias( if bias_adjust_mode not in ("change-by-statistic", "set-by-statistic"): raise RuntimeError("Unknown bias_adjust_mode mode: " + bias_adjust_mode) change = bias_adjust_mode == "change-by-statistic" + preset = ( + normalize_preset_out_bias(preset_out_bias, self.type_map) + if preset_out_bias is not None + else (None if change else self.preset_out_bias) + ) distinguish_types = self.get_compute_stats_distinguish_types() - check_preset_out_bias(self.preset_out_bias, self.bias_keys, distinguish_types) + check_preset_out_bias(preset, self.bias_keys, distinguish_types) sampled = sample_merged # === Step 1. Outputs fixed by the preset === fitted_keys = self.bias_keys - if self.preset_out_bias and (not change or apply_preset): + if preset: if observed_type is None: sampled = sample_merged() if callable(sample_merged) else sample_merged observed_type = collect_observed_types(sampled, self.type_map) rows = preset_out_bias_rows( - self.preset_out_bias, + preset, self.type_map, observed_type, to_numpy_array(self.out_bias), diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index 523f93b3cf..5bf5af1967 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -335,7 +335,7 @@ def change_out_bias( self, merged: Any, bias_adjust_mode: str = "change-by-statistic", - apply_preset: bool = False, + preset_out_bias: dict | None = None, ) -> None: """Change the output bias of atomic model according to the input data and the pretrained model. @@ -360,7 +360,7 @@ def change_out_bias( self.atomic_model.change_out_bias( merged, bias_adjust_mode=bias_adjust_mode, - apply_preset=apply_preset, + preset_out_bias=preset_out_bias, model_forward=self.predict_atomic_outputs_for_stat, ) diff --git a/deepmd/pt/model/model/sezm_spin_model.py b/deepmd/pt/model/model/sezm_spin_model.py index 7cbe965302..1a5389e3f9 100644 --- a/deepmd/pt/model/model/sezm_spin_model.py +++ b/deepmd/pt/model/model/sezm_spin_model.py @@ -432,7 +432,7 @@ def change_out_bias( self, merged: Callable[[], list[dict[str, Any]]] | list[dict[str, Any]], bias_adjust_mode: str = "change-by-statistic", - apply_preset: bool = False, + preset_out_bias: dict | None = None, ) -> None: """Change output bias using spin-expanded sampled data.""" spin_sampled_func = self._get_spin_sampled_func( @@ -441,7 +441,7 @@ def change_out_bias( super().change_out_bias( spin_sampled_func, bias_adjust_mode=bias_adjust_mode, - apply_preset=apply_preset, + preset_out_bias=preset_out_bias, ) def change_type_map( diff --git a/deepmd/pt/model/model/spin_model.py b/deepmd/pt/model/model/spin_model.py index cb8c303ce0..5197f6d16b 100644 --- a/deepmd/pt/model/model/spin_model.py +++ b/deepmd/pt/model/model/spin_model.py @@ -518,7 +518,7 @@ def change_out_bias( self, merged: Callable[[], list[dict]] | list[dict], bias_adjust_mode: str = "change-by-statistic", - apply_preset: bool = False, + preset_out_bias: dict | None = None, ) -> None: """Change the output bias of atomic model according to the input data and the pretrained model. @@ -543,7 +543,7 @@ def change_out_bias( self.backbone_model.change_out_bias( spin_sampled_func, bias_adjust_mode=bias_adjust_mode, - apply_preset=apply_preset, + preset_out_bias=preset_out_bias, ) def change_type_map( diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 2681344fd6..7b73165a38 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -975,7 +975,11 @@ def update_finetune_bias( _bias_adjust_mode="change-by-statistic" if not _finetune_rule.get_random_fitting() else "set-by-statistic", - apply_preset=True, + preset_out_bias=( + model_params["model_dict"][_model_key] + if self.multi_task + else model_params + ).get("preset_out_bias"), ) if self.multi_task: self.model[_model_key] = model @@ -1872,6 +1876,7 @@ def log_loss_valid(_task_key: str = "Default") -> dict: self.model, self.get_sample_func, _bias_adjust_mode="change-by-statistic", + preset_out_bias=self.wrapper.model_params.get("preset_out_bias"), ) else: for model_key in self.model_keys: @@ -1879,6 +1884,9 @@ def log_loss_valid(_task_key: str = "Default") -> dict: self.model[model_key], self.get_sample_func[model_key], _bias_adjust_mode="change-by-statistic", + preset_out_bias=self.wrapper.model_params["model_dict"][ + model_key + ].get("preset_out_bias"), ) self.latest_model = self.ckpt_store.path_for(self.num_steps) cur_lr = self.lr_schedule.value(self.num_steps - 1) @@ -2601,7 +2609,7 @@ def model_change_out_bias( _model: Any, _sample_func: Callable[[], Any], _bias_adjust_mode: str = "change-by-statistic", - apply_preset: bool = False, + preset_out_bias: dict | None = None, ) -> Any: from deepmd.pt.model.model.dp_model import ( DPModelCommon, @@ -2612,5 +2620,5 @@ def model_change_out_bias( _sample_func, bias_adjust_mode=_bias_adjust_mode, recompute_input_stats=isinstance(_model, DPModelCommon), - apply_preset=apply_preset, + preset_out_bias=preset_out_bias, ) diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index 1457bb4ca4..4632c2bff8 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -3649,6 +3649,7 @@ def model_args( "output is taken from the preset without statistics when a model is initialized. During fine-tuning, " "a preset explicitly provided in the downstream input overrides the loaded bias and skips fitting for " "that output; without a downstream preset, the loaded bias is adjusted by fitting residuals. " + "Post-training bias updates use the same preset from the current training input. " "`dp change-bias` in change mode fits residuals without reapplying the model's stored preset. " "Dipole models apply no output bias and take no preset." ) diff --git a/source/tests/common/dpmodel/test_atomic_model_global_stat.py b/source/tests/common/dpmodel/test_atomic_model_global_stat.py index 5313e9f57d..3f0b4ae875 100644 --- a/source/tests/common/dpmodel/test_atomic_model_global_stat.py +++ b/source/tests/common/dpmodel/test_atomic_model_global_stat.py @@ -340,19 +340,32 @@ def test_preset_bias(self) -> None: out_bias["foo"], np.array([2.0, -5.0]).reshape(2, 1) ) assert not np.allclose(out_bias["bar"], bar_bias) + fitted_bar_bias = out_bias["bar"].copy() # An explicit downstream preset overrides the fitted bias. new_foo_bias = foo_bias + 7.0 - md0.preset_out_bias["foo"] = new_foo_bias.tolist() md0.change_out_bias( self.merged_output_stat, bias_adjust_mode="change-by-statistic", - apply_preset=True, + preset_out_bias={ + "foo": new_foo_bias.tolist(), + "bar": bar_bias.tolist(), + }, ) out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) np.testing.assert_almost_equal(out_bias["foo"], new_foo_bias) np.testing.assert_almost_equal(out_bias["bar"], bar_bias) + # Presets for other outputs in the checkpoint are not downstream inputs. + md0.change_out_bias( + self.merged_output_stat, + preset_out_bias={"foo": new_foo_bias.tolist()}, + ) + out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) + np.testing.assert_almost_equal(out_bias["foo"], new_foo_bias) + np.testing.assert_almost_equal(out_bias["bar"], fitted_bar_bias) + np.testing.assert_almost_equal(md0.preset_out_bias["foo"], foo_bias) + # 4. a preset that leaves an observed type unassigned is rejected md1 = DPDPAtomicModel( ds, diff --git a/source/tests/common/test_dpmodel_train.py b/source/tests/common/test_dpmodel_train.py index 884346d710..d97bc1665b 100644 --- a/source/tests/common/test_dpmodel_train.py +++ b/source/tests/common/test_dpmodel_train.py @@ -26,8 +26,8 @@ def __init__(self, bias: float = 0.0) -> None: def get_out_bias(self): return self.out_bias - def change_out_bias(self, sample_func, bias_adjust_mode, apply_preset=False): - self.apply_preset = apply_preset + def change_out_bias(self, sample_func, bias_adjust_mode, preset_out_bias=None): + self.preset_out_bias = preset_out_bias self.change_calls.append((sample_func, bias_adjust_mode)) self.out_bias = self.out_bias + 1.0 @@ -46,9 +46,10 @@ def sample_func(): return [] change_model_out_bias(model, sample_func) - self.assertFalse(model.apply_preset) - change_model_out_bias(model, sample_func, apply_preset=True) - self.assertTrue(model.apply_preset) + self.assertIsNone(model.preset_out_bias) + preset = {"energy": [1.0, 2.0]} + change_model_out_bias(model, sample_func, preset_out_bias=preset) + self.assertEqual(model.preset_out_bias, preset) def test_change_model_out_bias_recomputes_input_stats_when_requested(self): model = FakeModel() @@ -82,9 +83,12 @@ def test_change_model_out_bias_by_task_updates_all_models(self): models, sample_funcs, ["task_a", "task_b"], + preset_out_bias_by_task={"task_a": {"energy": [1.0, 2.0]}}, ) self.assertIs(returned, models) + self.assertEqual(models["task_a"].preset_out_bias, {"energy": [1.0, 2.0]}) + self.assertIsNone(models["task_b"].preset_out_bias) np.testing.assert_allclose(models["task_a"].get_out_bias(), np.ones((1, 2, 1))) np.testing.assert_allclose( models["task_b"].get_out_bias(), diff --git a/source/tests/jax/test_training.py b/source/tests/jax/test_training.py index 913a851fe3..76eb252bbe 100644 --- a/source/tests/jax/test_training.py +++ b/source/tests/jax/test_training.py @@ -753,6 +753,9 @@ def _bias_sync_trainer(rank: int) -> DPTrainer: trainer.models = {DEFAULT_TASK_KEY: _BiasModel(0.0)} trainer._sample_funcs = {DEFAULT_TASK_KEY: object()} trainer.model_keys = [DEFAULT_TASK_KEY] + trainer.model_params_by_task = { + DEFAULT_TASK_KEY: {"preset_out_bias": {"energy": [1.0]}} + } return trainer @@ -777,6 +780,9 @@ def change_bias(models, *args, **kwargs) -> None: trainer._change_bias_after_training() change_model_out_bias_by_task.assert_called_once() + assert change_model_out_bias_by_task.call_args.kwargs[ + "preset_out_bias_by_task" + ] == {DEFAULT_TASK_KEY: {"energy": [1.0]}} broadcast_one_to_all.assert_called_once() assert broadcast_one_to_all.call_args.kwargs["is_source"] is True np.testing.assert_allclose( diff --git a/source/tests/pd/model/test_atomic_model_global_stat.py b/source/tests/pd/model/test_atomic_model_global_stat.py index 93c3096f27..0cca5e2025 100644 --- a/source/tests/pd/model/test_atomic_model_global_stat.py +++ b/source/tests/pd/model/test_atomic_model_global_stat.py @@ -374,19 +374,33 @@ def cvt_ret(x): to_numpy_array(out_bias["foo"]), np.array([2.0, -5.0]).reshape(2, 1) ) assert not np.allclose(to_numpy_array(out_bias["bar"]), bar_bias) + fitted_bar_bias = to_numpy_array(out_bias["bar"]).copy() # An explicit downstream preset overrides the fitted bias. new_foo_bias = foo_bias + 7.0 - md0.preset_out_bias["foo"] = new_foo_bias.tolist() BaseAtomicModel.change_out_bias( md0, self.merged_output_stat, bias_adjust_mode="change-by-statistic", - apply_preset=True, + preset_out_bias={ + "foo": new_foo_bias.tolist(), + "bar": bar_bias.tolist(), + }, ) out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) np.testing.assert_almost_equal(to_numpy_array(out_bias["foo"]), new_foo_bias) np.testing.assert_almost_equal(to_numpy_array(out_bias["bar"]), bar_bias) + + # Presets for other outputs in the checkpoint are not downstream inputs. + BaseAtomicModel.change_out_bias( + md0, + self.merged_output_stat, + preset_out_bias={"foo": new_foo_bias.tolist()}, + ) + out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) + np.testing.assert_almost_equal(to_numpy_array(out_bias["foo"]), new_foo_bias) + np.testing.assert_almost_equal(to_numpy_array(out_bias["bar"]), fitted_bar_bias) + np.testing.assert_almost_equal(md0.preset_out_bias["foo"], foo_bias) # 4. a preset that leaves an observed type unassigned is rejected md1 = DPAtomicModel( ds, diff --git a/source/tests/pt/model/test_atomic_model_global_stat.py b/source/tests/pt/model/test_atomic_model_global_stat.py index 81ceec0577..57962c8a1d 100644 --- a/source/tests/pt/model/test_atomic_model_global_stat.py +++ b/source/tests/pt/model/test_atomic_model_global_stat.py @@ -400,19 +400,33 @@ def cvt_ret(x): to_numpy_array(out_bias["foo"]), np.array([2.0, -5.0]).reshape(2, 1) ) assert not np.allclose(to_numpy_array(out_bias["bar"]), bar_bias) + fitted_bar_bias = to_numpy_array(out_bias["bar"]).copy() # An explicit downstream preset overrides the fitted bias. new_foo_bias = foo_bias + 7.0 - md0.preset_out_bias["foo"] = new_foo_bias.tolist() BaseAtomicModel.change_out_bias( md0, self.merged_output_stat, bias_adjust_mode="change-by-statistic", - apply_preset=True, + preset_out_bias={ + "foo": new_foo_bias.tolist(), + "bar": bar_bias.tolist(), + }, ) out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) np.testing.assert_almost_equal(to_numpy_array(out_bias["foo"]), new_foo_bias) np.testing.assert_almost_equal(to_numpy_array(out_bias["bar"]), bar_bias) + + # Presets for other outputs in the checkpoint are not downstream inputs. + BaseAtomicModel.change_out_bias( + md0, + self.merged_output_stat, + preset_out_bias={"foo": new_foo_bias.tolist()}, + ) + out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) + np.testing.assert_almost_equal(to_numpy_array(out_bias["foo"]), new_foo_bias) + np.testing.assert_almost_equal(to_numpy_array(out_bias["bar"]), fitted_bar_bias) + np.testing.assert_almost_equal(md0.preset_out_bias["foo"], foo_bias) # 4. a preset that leaves an observed type unassigned is rejected md1 = DPAtomicModel( ds, diff --git a/source/tests/pt/test_training.py b/source/tests/pt/test_training.py index 9cce8c02aa..b7dcba84c2 100644 --- a/source/tests/pt/test_training.py +++ b/source/tests/pt/test_training.py @@ -1388,7 +1388,9 @@ def test_ema_checkpoint_cleanup_removes_future_steps(self) -> None: def test_ema_checkpoint_keeps_changed_out_bias( self, mocked_change_out_bias ) -> None: - def change_out_bias(model, sample_func, _bias_adjust_mode): + def change_out_bias( + model, sample_func, _bias_adjust_mode, preset_out_bias=None + ): model.set_out_bias(model.get_out_bias() + 1.0) return model From f682c8d10438bb10c69467ec0951bf1cba79c2b2 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Tue, 13 Oct 2026 13:13:51 +0800 Subject: [PATCH 4/9] fix(cli): scope preset bias override to standalone change-bias --- .../dpmodel/atomic_model/base_atomic_model.py | 13 ++--- deepmd/dpmodel/train/trainer.py | 11 ++-- deepmd/jax/train/trainer.py | 7 --- deepmd/pd/entrypoints/main.py | 5 +- .../model/atomic_model/base_atomic_model.py | 13 ++--- deepmd/pd/train/training.py | 17 ++---- deepmd/pt/entrypoints/main.py | 5 +- .../model/atomic_model/base_atomic_model.py | 13 ++--- deepmd/pt/train/training.py | 9 ---- deepmd/utils/argcheck.py | 8 ++- .../dpmodel/test_atomic_model_global_stat.py | 49 ++++++----------- source/tests/common/test_dpmodel_train.py | 18 +------ source/tests/jax/test_training.py | 6 --- .../pd/model/test_atomic_model_global_stat.py | 53 ++++++------------- .../pt/model/test_atomic_model_global_stat.py | 53 ++++++------------- source/tests/pt/test_change_bias.py | 26 +++++++++ source/tests/pt/test_training.py | 4 +- 17 files changed, 120 insertions(+), 190 deletions(-) diff --git a/deepmd/dpmodel/atomic_model/base_atomic_model.py b/deepmd/dpmodel/atomic_model/base_atomic_model.py index 871cf15efb..b7243ea4ea 100644 --- a/deepmd/dpmodel/atomic_model/base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/base_atomic_model.py @@ -818,17 +818,18 @@ def change_out_bias( 'change-by-statistic' : perform predictions on labels of target dataset, and do least square on the errors to obtain the target shift as bias. 'set-by-statistic' : directly use the statistic output bias in the target dataset. - In 'set-by-statistic' mode, an output assigned in `preset_out_bias` - is fixed by the preset: + An output assigned in `preset_out_bias` is fixed by the preset in both modes: every element in the data must be assigned, the assigned types take the - preset value, and the types absent from the data keep zero; no + preset value, and the types absent from the data keep zero + ('set-by-statistic') or their stored bias ('change-by-statistic'); no statistics are computed, read or written for such an output, whose output std keeps its stored value. - In 'change-by-statistic' mode, stored presets are ignored. preset_out_bias : dict, optional An explicit downstream preset. Assigned outputs use this bias and skip fitting; other outputs fit residuals against the stored bias. - In 'set-by-statistic' mode, the model preset is used when omitted. + The model preset is used when omitted, preserving training and + fine-tuning behavior. Pass an empty dict to fit from the data + without reapplying the model's stored preset. stat_file_path : Optional[DPPath] The path to the stat file. observed_type : list[str], optional @@ -846,7 +847,7 @@ def change_out_bias( preset = ( normalize_preset_out_bias(preset_out_bias, self.type_map) if preset_out_bias is not None - else (None if change else self.preset_out_bias) + else self.preset_out_bias ) distinguish_types = self.get_compute_stats_distinguish_types() check_preset_out_bias(preset, self.bias_keys, distinguish_types) diff --git a/deepmd/dpmodel/train/trainer.py b/deepmd/dpmodel/train/trainer.py index 89c0203571..d2dd7703a5 100644 --- a/deepmd/dpmodel/train/trainer.py +++ b/deepmd/dpmodel/train/trainer.py @@ -78,10 +78,13 @@ def change_model_out_bias( ) -> Any: """Change one model's output bias and log the before/after values.""" old_bias = deepcopy(model.get_out_bias()) + preset_kwargs = ( + {"preset_out_bias": preset_out_bias} if preset_out_bias is not None else {} + ) model.change_out_bias( sample_func, bias_adjust_mode=bias_adjust_mode, - preset_out_bias=preset_out_bias, + **preset_kwargs, ) new_bias = deepcopy(model.get_out_bias()) @@ -104,7 +107,6 @@ def change_model_out_bias_by_task( *, bias_adjust_mode: str = "change-by-statistic", recompute_input_stats: bool = False, - preset_out_bias_by_task: Mapping[str, dict | None] | None = None, ) -> MutableMapping[str, Any]: """Change output bias for all requested training-task models.""" log.info("Changing output bias after training.") @@ -114,11 +116,6 @@ def change_model_out_bias_by_task( sample_funcs[model_key], bias_adjust_mode=bias_adjust_mode, recompute_input_stats=recompute_input_stats, - preset_out_bias=( - preset_out_bias_by_task.get(model_key) - if preset_out_bias_by_task is not None - else None - ), ) return models diff --git a/deepmd/jax/train/trainer.py b/deepmd/jax/train/trainer.py index f906d82eb3..a35de2a49b 100644 --- a/deepmd/jax/train/trainer.py +++ b/deepmd/jax/train/trainer.py @@ -616,9 +616,6 @@ def _apply_finetune(self) -> None: self.models[model_key].change_out_bias( self._sample_funcs[model_key], bias_adjust_mode=bias_mode, - preset_out_bias=self.model_params_by_task[model_key].get( - "preset_out_bias" - ), ) def _initialize_stat(self, model_key: str) -> None: @@ -931,10 +928,6 @@ def _change_bias_after_training(self) -> None: self._sample_funcs, self.model_keys, bias_adjust_mode="change-by-statistic", - preset_out_bias_by_task={ - key: self.model_params_by_task[key].get("preset_out_bias") - for key in self.model_keys - }, ) self._broadcast_model_states() diff --git a/deepmd/pd/entrypoints/main.py b/deepmd/pd/entrypoints/main.py index cf15e99192..237c860125 100644 --- a/deepmd/pd/entrypoints/main.py +++ b/deepmd/pd/entrypoints/main.py @@ -552,7 +552,10 @@ def change_bias( nbatches, ) updated_model = training.model_change_out_bias( - model_to_change, sampled_data, _bias_adjust_mode=bias_adjust_mode + model_to_change, + sampled_data, + _bias_adjust_mode=bias_adjust_mode, + preset_out_bias={}, ) if not multi_task: diff --git a/deepmd/pd/model/atomic_model/base_atomic_model.py b/deepmd/pd/model/atomic_model/base_atomic_model.py index 5ae71b42c7..7618ce7ee3 100644 --- a/deepmd/pd/model/atomic_model/base_atomic_model.py +++ b/deepmd/pd/model/atomic_model/base_atomic_model.py @@ -519,17 +519,18 @@ def change_out_bias( 'change-by-statistic' : perform predictions on labels of target dataset, and do least square on the errors to obtain the target shift as bias. 'set-by-statistic' : directly use the statistic output bias in the target dataset. - In 'set-by-statistic' mode, an output assigned in `preset_out_bias` - is fixed by the preset: + An output assigned in `preset_out_bias` is fixed by the preset in both modes: every element in the data must be assigned, the assigned types take the - preset value, and the types absent from the data keep zero; no + preset value, and the types absent from the data keep zero + ('set-by-statistic') or their stored bias ('change-by-statistic'); no statistics are computed, read or written for such an output, whose output std keeps its stored value. - In 'change-by-statistic' mode, stored presets are ignored. preset_out_bias : dict, optional An explicit downstream preset. Assigned outputs use this bias and skip fitting; other outputs fit residuals against the stored bias. - In 'set-by-statistic' mode, the model preset is used when omitted. + The model preset is used when omitted, preserving training and + fine-tuning behavior. Pass an empty dict to fit from the data + without reapplying the model's stored preset. stat_file_path : Optional[DPPath] The path to the stat file. observed_type : list[str], optional @@ -542,7 +543,7 @@ def change_out_bias( preset = ( normalize_preset_out_bias(preset_out_bias, self.type_map) if preset_out_bias is not None - else (None if change else self.preset_out_bias) + else self.preset_out_bias ) distinguish_types = self.get_compute_stats_distinguish_types() check_preset_out_bias(preset, self.bias_keys, distinguish_types) diff --git a/deepmd/pd/train/training.py b/deepmd/pd/train/training.py index ccce989229..7db7d89895 100644 --- a/deepmd/pd/train/training.py +++ b/deepmd/pd/train/training.py @@ -640,7 +640,6 @@ def single_model_finetune( _model: Any, _finetune_rule_single: Any, _sample_func: Any, - _model_params: dict, ) -> Any: _model = model_change_out_bias( _model, @@ -648,17 +647,13 @@ def single_model_finetune( _bias_adjust_mode="change-by-statistic" if not _finetune_rule_single.get_random_fitting() else "set-by-statistic", - preset_out_bias=_model_params.get("preset_out_bias"), ) return _model if not self.multi_task: finetune_rule_single = self.finetune_links["Default"] self.model = single_model_finetune( - self.model, - finetune_rule_single, - self.get_sample_func, - model_params, + self.model, finetune_rule_single, self.get_sample_func ) else: for model_key in self.model_keys: @@ -671,7 +666,6 @@ def single_model_finetune( self.model[model_key], finetune_rule_single, self.get_sample_func[model_key], - model_params["model_dict"][model_key], ) else: log.info( @@ -1093,7 +1087,6 @@ def log_loss_valid(_task_key: str = "Default") -> dict: self.model, self.get_sample_func, _bias_adjust_mode="change-by-statistic", - preset_out_bias=self.wrapper.model_params.get("preset_out_bias"), ) else: for model_key in self.model_keys: @@ -1101,9 +1094,6 @@ def log_loss_valid(_task_key: str = "Default") -> dict: self.model[model_key], self.get_sample_func[model_key], _bias_adjust_mode="change-by-statistic", - preset_out_bias=self.wrapper.model_params["model_dict"][ - model_key - ].get("preset_out_bias"), ) self.latest_model = Path(self.save_ckpt + f"-{self.num_steps}.pd") cur_lr = self.lr_schedule.value(self.num_steps - 1) @@ -1468,10 +1458,13 @@ def model_change_out_bias( preset_out_bias: dict | None = None, ) -> None: old_bias = deepcopy(_model.get_out_bias()) + preset_kwargs = ( + {"preset_out_bias": preset_out_bias} if preset_out_bias is not None else {} + ) _model.change_out_bias( _sample_func, bias_adjust_mode=_bias_adjust_mode, - preset_out_bias=preset_out_bias, + **preset_kwargs, ) new_bias = deepcopy(_model.get_out_bias()) diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index da6b890d13..33b90ade7f 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -623,7 +623,10 @@ def change_bias( nbatches, ) updated_model = training.model_change_out_bias( - model_to_change, sampled_data, _bias_adjust_mode=bias_adjust_mode + model_to_change, + sampled_data, + _bias_adjust_mode=bias_adjust_mode, + preset_out_bias={}, ) if not multi_task: diff --git a/deepmd/pt/model/atomic_model/base_atomic_model.py b/deepmd/pt/model/atomic_model/base_atomic_model.py index 82cc7829f9..48d26b73ba 100644 --- a/deepmd/pt/model/atomic_model/base_atomic_model.py +++ b/deepmd/pt/model/atomic_model/base_atomic_model.py @@ -649,17 +649,18 @@ def change_out_bias( 'change-by-statistic' : perform predictions on labels of target dataset, and do least square on the errors to obtain the target shift as bias. 'set-by-statistic' : directly use the statistic output bias in the target dataset. - In 'set-by-statistic' mode, an output assigned in `preset_out_bias` - is fixed by the preset: + An output assigned in `preset_out_bias` is fixed by the preset in both modes: every element in the data must be assigned, the assigned types take the - preset value, and the types absent from the data keep zero; no + preset value, and the types absent from the data keep zero + ('set-by-statistic') or their stored bias ('change-by-statistic'); no statistics are computed, read or written for such an output, whose output std keeps its stored value. - In 'change-by-statistic' mode, stored presets are ignored. preset_out_bias : dict, optional An explicit downstream preset. Assigned outputs use this bias and skip fitting; other outputs fit residuals against the stored bias. - In 'set-by-statistic' mode, the model preset is used when omitted. + The model preset is used when omitted, preserving training and + fine-tuning behavior. Pass an empty dict to fit from the data + without reapplying the model's stored preset. model_forward : Callable[..., dict[str, torch.Tensor]], optional Predictor of the complete atomic outputs used by 'change-by-statistic'. Defaults to the forward of this atomic model; a model that adds @@ -674,7 +675,7 @@ def change_out_bias( preset = ( normalize_preset_out_bias(preset_out_bias, self.type_map) if preset_out_bias is not None - else (None if change else self.preset_out_bias) + else self.preset_out_bias ) distinguish_types = self.get_compute_stats_distinguish_types() check_preset_out_bias(preset, self.bias_keys, distinguish_types) diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 7b73165a38..205203e865 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -975,11 +975,6 @@ def update_finetune_bias( _bias_adjust_mode="change-by-statistic" if not _finetune_rule.get_random_fitting() else "set-by-statistic", - preset_out_bias=( - model_params["model_dict"][_model_key] - if self.multi_task - else model_params - ).get("preset_out_bias"), ) if self.multi_task: self.model[_model_key] = model @@ -1876,7 +1871,6 @@ def log_loss_valid(_task_key: str = "Default") -> dict: self.model, self.get_sample_func, _bias_adjust_mode="change-by-statistic", - preset_out_bias=self.wrapper.model_params.get("preset_out_bias"), ) else: for model_key in self.model_keys: @@ -1884,9 +1878,6 @@ def log_loss_valid(_task_key: str = "Default") -> dict: self.model[model_key], self.get_sample_func[model_key], _bias_adjust_mode="change-by-statistic", - preset_out_bias=self.wrapper.model_params["model_dict"][ - model_key - ].get("preset_out_bias"), ) self.latest_model = self.ckpt_store.path_for(self.num_steps) cur_lr = self.lr_schedule.value(self.num_steps - 1) diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index 4632c2bff8..2b7698fde2 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -3646,11 +3646,9 @@ def model_args( "for tensor outputs, e.g. `{'dipole': [null, [0., 1., 2.]]}`. " "Elements outside the `type_map` are ignored and every element that occurs in the data must be assigned. " "A table is resolved once when the input is processed and its values are stored in the model. An assigned " - "output is taken from the preset without statistics when a model is initialized. During fine-tuning, " - "a preset explicitly provided in the downstream input overrides the loaded bias and skips fitting for " - "that output; without a downstream preset, the loaded bias is adjusted by fitting residuals. " - "Post-training bias updates use the same preset from the current training input. " - "`dp change-bias` in change mode fits residuals without reapplying the model's stored preset. " + "output is taken from the preset without statistics, both when a model is initialized and when its bias is " + "changed by fine-tuning. `dp change-bias` fits from its input data without reapplying the model's stored " + "preset; explicit `--bias-value` values override the bias directly. " "Dipole models apply no output bias and take no preset." ) doc_finetune_head = ( diff --git a/source/tests/common/dpmodel/test_atomic_model_global_stat.py b/source/tests/common/dpmodel/test_atomic_model_global_stat.py index 3f0b4ae875..7760cce9cd 100644 --- a/source/tests/common/dpmodel/test_atomic_model_global_stat.py +++ b/source/tests/common/dpmodel/test_atomic_model_global_stat.py @@ -319,52 +319,37 @@ def test_preset_bias(self) -> None: for kk in ["foo", "pix", "bar"]: np.testing.assert_almost_equal(ret1[kk], expected_ret1[kk]) - # 3. change-by-statistic treats the preset as the stored starting bias - # and fits a downstream residual shift. + # 3. change-by-statistic keeps every assigned type at its preset md0.change_out_bias( self.merged_output_stat, bias_adjust_mode="change-by-statistic" ) - args_stat = [ - self.coord_ext, - np.array(self.merged_output_stat[0]["atype_ext"], dtype=np.int64), - self.nlist, - ] - ret3 = md0.forward_common_atomic(*args_stat) - expected_ret3 = {} - expected_ret3["foo"] = np.array([[3, 4, -2], [6, 0, 1]]).reshape(2, 3, 1) - expected_ret3["pix"] = ret0["pix"] - for kk in ["foo", "pix"]: - np.testing.assert_almost_equal(ret3[kk], expected_ret3[kk]) + ret3 = md0.forward_common_atomic(*args) + for kk in ["foo", "pix", "bar"]: + np.testing.assert_almost_equal(ret3[kk], ret1[kk]) out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) - np.testing.assert_almost_equal( - out_bias["foo"], np.array([2.0, -5.0]).reshape(2, 1) - ) - assert not np.allclose(out_bias["bar"], bar_bias) - fitted_bar_bias = out_bias["bar"].copy() + np.testing.assert_almost_equal(out_bias["foo"], foo_bias) + np.testing.assert_almost_equal(out_bias["bar"], bar_bias) - # An explicit downstream preset overrides the fitted bias. - new_foo_bias = foo_bias + 7.0 + # Standalone change-bias explicitly ignores the checkpoint preset. md0.change_out_bias( self.merged_output_stat, bias_adjust_mode="change-by-statistic", - preset_out_bias={ - "foo": new_foo_bias.tolist(), - "bar": bar_bias.tolist(), - }, + preset_out_bias={}, ) out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) - np.testing.assert_almost_equal(out_bias["foo"], new_foo_bias) - np.testing.assert_almost_equal(out_bias["bar"], bar_bias) + np.testing.assert_almost_equal( + out_bias["foo"], np.array([2.0, -5.0]).reshape(2, 1) + ) + assert not np.allclose(out_bias["bar"], bar_bias) - # Presets for other outputs in the checkpoint are not downstream inputs. + # The override does not remove the model's configured preset. + np.testing.assert_almost_equal(md0.preset_out_bias["foo"], foo_bias) md0.change_out_bias( - self.merged_output_stat, - preset_out_bias={"foo": new_foo_bias.tolist()}, + self.merged_output_stat, bias_adjust_mode="change-by-statistic" ) out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) - np.testing.assert_almost_equal(out_bias["foo"], new_foo_bias) - np.testing.assert_almost_equal(out_bias["bar"], fitted_bar_bias) - np.testing.assert_almost_equal(md0.preset_out_bias["foo"], foo_bias) + np.testing.assert_almost_equal(out_bias["foo"], foo_bias) + np.testing.assert_almost_equal(out_bias["bar"], bar_bias) # 4. a preset that leaves an observed type unassigned is rejected md1 = DPDPAtomicModel( diff --git a/source/tests/common/test_dpmodel_train.py b/source/tests/common/test_dpmodel_train.py index d97bc1665b..22149f8e99 100644 --- a/source/tests/common/test_dpmodel_train.py +++ b/source/tests/common/test_dpmodel_train.py @@ -26,8 +26,7 @@ def __init__(self, bias: float = 0.0) -> None: def get_out_bias(self): return self.out_bias - def change_out_bias(self, sample_func, bias_adjust_mode, preset_out_bias=None): - self.preset_out_bias = preset_out_bias + def change_out_bias(self, sample_func, bias_adjust_mode): self.change_calls.append((sample_func, bias_adjust_mode)) self.out_bias = self.out_bias + 1.0 @@ -39,18 +38,6 @@ def get_fitting_net(self): class TestChangeModelOutBias(unittest.TestCase): - def test_change_model_out_bias_explicit_preset(self): - model = FakeModel() - - def sample_func(): - return [] - - change_model_out_bias(model, sample_func) - self.assertIsNone(model.preset_out_bias) - preset = {"energy": [1.0, 2.0]} - change_model_out_bias(model, sample_func, preset_out_bias=preset) - self.assertEqual(model.preset_out_bias, preset) - def test_change_model_out_bias_recomputes_input_stats_when_requested(self): model = FakeModel() @@ -83,12 +70,9 @@ def test_change_model_out_bias_by_task_updates_all_models(self): models, sample_funcs, ["task_a", "task_b"], - preset_out_bias_by_task={"task_a": {"energy": [1.0, 2.0]}}, ) self.assertIs(returned, models) - self.assertEqual(models["task_a"].preset_out_bias, {"energy": [1.0, 2.0]}) - self.assertIsNone(models["task_b"].preset_out_bias) np.testing.assert_allclose(models["task_a"].get_out_bias(), np.ones((1, 2, 1))) np.testing.assert_allclose( models["task_b"].get_out_bias(), diff --git a/source/tests/jax/test_training.py b/source/tests/jax/test_training.py index 76eb252bbe..913a851fe3 100644 --- a/source/tests/jax/test_training.py +++ b/source/tests/jax/test_training.py @@ -753,9 +753,6 @@ def _bias_sync_trainer(rank: int) -> DPTrainer: trainer.models = {DEFAULT_TASK_KEY: _BiasModel(0.0)} trainer._sample_funcs = {DEFAULT_TASK_KEY: object()} trainer.model_keys = [DEFAULT_TASK_KEY] - trainer.model_params_by_task = { - DEFAULT_TASK_KEY: {"preset_out_bias": {"energy": [1.0]}} - } return trainer @@ -780,9 +777,6 @@ def change_bias(models, *args, **kwargs) -> None: trainer._change_bias_after_training() change_model_out_bias_by_task.assert_called_once() - assert change_model_out_bias_by_task.call_args.kwargs[ - "preset_out_bias_by_task" - ] == {DEFAULT_TASK_KEY: {"energy": [1.0]}} broadcast_one_to_all.assert_called_once() assert broadcast_one_to_all.call_args.kwargs["is_source"] is True np.testing.assert_allclose( diff --git a/source/tests/pd/model/test_atomic_model_global_stat.py b/source/tests/pd/model/test_atomic_model_global_stat.py index 0cca5e2025..ff747eb518 100644 --- a/source/tests/pd/model/test_atomic_model_global_stat.py +++ b/source/tests/pd/model/test_atomic_model_global_stat.py @@ -349,58 +349,39 @@ def cvt_ret(x): expected_ret1["bar"] = ret0["bar"] + bar_bias[at] for kk in ["foo", "pix", "bar"]: np.testing.assert_almost_equal(ret1[kk], expected_ret1[kk]) - # 3. change-by-statistic treats the preset as the stored starting bias - # and fits a downstream residual shift. + # 3. change-by-statistic keeps every assigned type at its preset BaseAtomicModel.change_out_bias( md0, self.merged_output_stat, bias_adjust_mode="change-by-statistic" ) - args_stat = [ - to_paddle_tensor(ii) - for ii in [ - self.coord_ext, - np.array(self.merged_output_stat[0]["atype_ext"], dtype=np.int64), - self.nlist, - ] - ] - ret3 = md0.forward_common_atomic(*args_stat) + ret3 = md0.forward_common_atomic(*args) ret3 = cvt_ret(ret3) - expected_ret3 = {} - expected_ret3["foo"] = np.array([[3, 4, -2], [6, 0, 1]]).reshape(2, 3, 1) - expected_ret3["pix"] = ret0["pix"] - for kk in ["foo", "pix"]: - np.testing.assert_almost_equal(ret3[kk], expected_ret3[kk]) + for kk in ["foo", "pix", "bar"]: + np.testing.assert_almost_equal(ret3[kk], ret1[kk]) out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) - np.testing.assert_almost_equal( - to_numpy_array(out_bias["foo"]), np.array([2.0, -5.0]).reshape(2, 1) - ) - assert not np.allclose(to_numpy_array(out_bias["bar"]), bar_bias) - fitted_bar_bias = to_numpy_array(out_bias["bar"]).copy() + np.testing.assert_almost_equal(to_numpy_array(out_bias["foo"]), foo_bias) + np.testing.assert_almost_equal(to_numpy_array(out_bias["bar"]), bar_bias) - # An explicit downstream preset overrides the fitted bias. - new_foo_bias = foo_bias + 7.0 + # Standalone change-bias explicitly ignores the checkpoint preset. BaseAtomicModel.change_out_bias( md0, self.merged_output_stat, bias_adjust_mode="change-by-statistic", - preset_out_bias={ - "foo": new_foo_bias.tolist(), - "bar": bar_bias.tolist(), - }, + preset_out_bias={}, ) out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) - np.testing.assert_almost_equal(to_numpy_array(out_bias["foo"]), new_foo_bias) - np.testing.assert_almost_equal(to_numpy_array(out_bias["bar"]), bar_bias) + np.testing.assert_almost_equal( + to_numpy_array(out_bias["foo"]), np.array([2.0, -5.0]).reshape(2, 1) + ) + assert not np.allclose(to_numpy_array(out_bias["bar"]), bar_bias) - # Presets for other outputs in the checkpoint are not downstream inputs. + # The override does not remove the model's configured preset. + np.testing.assert_almost_equal(md0.preset_out_bias["foo"], foo_bias) BaseAtomicModel.change_out_bias( - md0, - self.merged_output_stat, - preset_out_bias={"foo": new_foo_bias.tolist()}, + md0, self.merged_output_stat, bias_adjust_mode="change-by-statistic" ) out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) - np.testing.assert_almost_equal(to_numpy_array(out_bias["foo"]), new_foo_bias) - np.testing.assert_almost_equal(to_numpy_array(out_bias["bar"]), fitted_bar_bias) - np.testing.assert_almost_equal(md0.preset_out_bias["foo"], foo_bias) + np.testing.assert_almost_equal(to_numpy_array(out_bias["foo"]), foo_bias) + np.testing.assert_almost_equal(to_numpy_array(out_bias["bar"]), bar_bias) # 4. a preset that leaves an observed type unassigned is rejected md1 = DPAtomicModel( ds, diff --git a/source/tests/pt/model/test_atomic_model_global_stat.py b/source/tests/pt/model/test_atomic_model_global_stat.py index 57962c8a1d..3e304ee1d7 100644 --- a/source/tests/pt/model/test_atomic_model_global_stat.py +++ b/source/tests/pt/model/test_atomic_model_global_stat.py @@ -375,58 +375,39 @@ def cvt_ret(x): expected_ret1["bar"] = ret0["bar"] + bar_bias[at] for kk in ["foo", "pix", "bar"]: np.testing.assert_almost_equal(ret1[kk], expected_ret1[kk]) - # 3. change-by-statistic treats the preset as the stored starting bias - # and fits a downstream residual shift. + # 3. change-by-statistic keeps every assigned type at its preset BaseAtomicModel.change_out_bias( md0, self.merged_output_stat, bias_adjust_mode="change-by-statistic" ) - args_stat = [ - to_torch_tensor(ii) - for ii in [ - self.coord_ext, - np.array(self.merged_output_stat[0]["atype_ext"], dtype=np.int64), - self.nlist, - ] - ] - ret3 = md0.forward_common_atomic(*args_stat, fparam=self.fparam) + ret3 = md0.forward_common_atomic(*args, fparam=self.fparam) ret3 = cvt_ret(ret3) - expected_ret3 = {} - expected_ret3["foo"] = np.array([[3, 4, -2], [6, 0, 1]]).reshape(2, 3, 1) - expected_ret3["pix"] = ret0["pix"] - for kk in ["foo", "pix"]: - np.testing.assert_almost_equal(ret3[kk], expected_ret3[kk]) + for kk in ["foo", "pix", "bar"]: + np.testing.assert_almost_equal(ret3[kk], ret1[kk]) out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) - np.testing.assert_almost_equal( - to_numpy_array(out_bias["foo"]), np.array([2.0, -5.0]).reshape(2, 1) - ) - assert not np.allclose(to_numpy_array(out_bias["bar"]), bar_bias) - fitted_bar_bias = to_numpy_array(out_bias["bar"]).copy() + np.testing.assert_almost_equal(to_numpy_array(out_bias["foo"]), foo_bias) + np.testing.assert_almost_equal(to_numpy_array(out_bias["bar"]), bar_bias) - # An explicit downstream preset overrides the fitted bias. - new_foo_bias = foo_bias + 7.0 + # Standalone change-bias explicitly ignores the checkpoint preset. BaseAtomicModel.change_out_bias( md0, self.merged_output_stat, bias_adjust_mode="change-by-statistic", - preset_out_bias={ - "foo": new_foo_bias.tolist(), - "bar": bar_bias.tolist(), - }, + preset_out_bias={}, ) out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) - np.testing.assert_almost_equal(to_numpy_array(out_bias["foo"]), new_foo_bias) - np.testing.assert_almost_equal(to_numpy_array(out_bias["bar"]), bar_bias) + np.testing.assert_almost_equal( + to_numpy_array(out_bias["foo"]), np.array([2.0, -5.0]).reshape(2, 1) + ) + assert not np.allclose(to_numpy_array(out_bias["bar"]), bar_bias) - # Presets for other outputs in the checkpoint are not downstream inputs. + # The override does not remove the model's configured preset. + np.testing.assert_almost_equal(md0.preset_out_bias["foo"], foo_bias) BaseAtomicModel.change_out_bias( - md0, - self.merged_output_stat, - preset_out_bias={"foo": new_foo_bias.tolist()}, + md0, self.merged_output_stat, bias_adjust_mode="change-by-statistic" ) out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) - np.testing.assert_almost_equal(to_numpy_array(out_bias["foo"]), new_foo_bias) - np.testing.assert_almost_equal(to_numpy_array(out_bias["bar"]), fitted_bar_bias) - np.testing.assert_almost_equal(md0.preset_out_bias["foo"], foo_bias) + np.testing.assert_almost_equal(to_numpy_array(out_bias["foo"]), foo_bias) + np.testing.assert_almost_equal(to_numpy_array(out_bias["bar"]), bar_bias) # 4. a preset that leaves an observed type unassigned is rejected md1 = DPAtomicModel( ds, diff --git a/source/tests/pt/test_change_bias.py b/source/tests/pt/test_change_bias.py index 746656ac8c..f7e3060293 100644 --- a/source/tests/pt/test_change_bias.py +++ b/source/tests/pt/test_change_bias.py @@ -144,6 +144,32 @@ def test_change_bias_with_data_sys_file(self) -> None: expected_bias = expected_model.get_out_bias() torch.testing.assert_close(updated_bias, expected_bias) + def test_change_bias_ignores_checkpoint_preset(self) -> None: + checkpoint = torch.load( + str(self.model_path), map_location=DEVICE, weights_only=True + ) + preset = {"energy": [1.0e6] * len(self.config["model"]["type_map"])} + checkpoint["model"]["_extra_state"]["model_params"]["preset_out_bias"] = preset + torch.save(checkpoint, self.model_path) + run_dp( + f"dp --pt change-bias {self.model_path!s} -s {self.data_file[0]} -o {self.model_path_data_bias!s}" + ) + updated = torch.load( + str(self.model_path_data_bias), map_location=DEVICE, weights_only=True + ) + model_params = updated["model"]["_extra_state"]["model_params"] + self.assertEqual(model_params["preset_out_bias"], preset) + wrapper = ModelWrapper(get_model_for_wrapper(model_params)) + wrapper.load_state_dict(updated["model"]) + expected_model = model_change_out_bias( + self.trainer.wrapper.model["Default"], + self.sampled, + _bias_adjust_mode="change-by-statistic", + ) + torch.testing.assert_close( + wrapper.model["Default"].get_out_bias(), expected_model.get_out_bias() + ) + def test_change_bias_with_user_defined(self) -> None: user_bias = [0.1, 3.2, -0.5] run_dp( diff --git a/source/tests/pt/test_training.py b/source/tests/pt/test_training.py index b7dcba84c2..9cce8c02aa 100644 --- a/source/tests/pt/test_training.py +++ b/source/tests/pt/test_training.py @@ -1388,9 +1388,7 @@ def test_ema_checkpoint_cleanup_removes_future_steps(self) -> None: def test_ema_checkpoint_keeps_changed_out_bias( self, mocked_change_out_bias ) -> None: - def change_out_bias( - model, sample_func, _bias_adjust_mode, preset_out_bias=None - ): + def change_out_bias(model, sample_func, _bias_adjust_mode): model.set_out_bias(model.get_out_bias() + 1.0) return model From 79beec01e8369cb7f514f9b39cafc6a800b9d456 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:27:28 +0800 Subject: [PATCH 5/9] fix(pt_expt): ignore checkpoint presets in standalone change-bias --- deepmd/pt_expt/entrypoints/main.py | 5 +- deepmd/pt_expt/train/training.py | 5 ++ source/tests/pt_expt/test_change_bias.py | 73 +++++++++++++++++++++++- 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/deepmd/pt_expt/entrypoints/main.py b/deepmd/pt_expt/entrypoints/main.py index 7a381b4938..1e4ae0b0ad 100644 --- a/deepmd/pt_expt/entrypoints/main.py +++ b/deepmd/pt_expt/entrypoints/main.py @@ -825,7 +825,10 @@ def change_bias( nbatches = min(data.get_nbatches()) sampled_data = make_stat_input(data, nbatches) model_to_change = model_change_out_bias( - model_to_change, sampled_data, _bias_adjust_mode=bias_adjust_mode + model_to_change, + sampled_data, + _bias_adjust_mode=bias_adjust_mode, + preset_out_bias={}, ) if input_file.endswith(".pt"): diff --git a/deepmd/pt_expt/train/training.py b/deepmd/pt_expt/train/training.py index 9130f5753b..e98d724970 100644 --- a/deepmd/pt_expt/train/training.py +++ b/deepmd/pt_expt/train/training.py @@ -3441,6 +3441,7 @@ def model_change_out_bias( _model: Any, _sample_func: Any, _bias_adjust_mode: str = "change-by-statistic", + preset_out_bias: dict | None = None, ) -> Any: """Change the output bias of a model based on sampled data. @@ -3452,6 +3453,9 @@ def model_change_out_bias( Callable that returns sampled data for bias computation. _bias_adjust_mode ``"change-by-statistic"`` or ``"set-by-statistic"``. + preset_out_bias + Optional preset override. An empty mapping ignores checkpoint presets; + omitting it preserves training and fine-tuning behavior. Returns ------- @@ -3466,6 +3470,7 @@ def model_change_out_bias( _sample_func, bias_adjust_mode=_bias_adjust_mode, recompute_input_stats=isinstance(_model, DPModelCommon), + preset_out_bias=preset_out_bias, ) diff --git a/source/tests/pt_expt/test_change_bias.py b/source/tests/pt_expt/test_change_bias.py index 2401aa59c5..719ada14b3 100644 --- a/source/tests/pt_expt/test_change_bias.py +++ b/source/tests/pt_expt/test_change_bias.py @@ -19,18 +19,27 @@ from deepmd.pt_expt.model.get_model import ( get_model, ) +from deepmd.pt_expt.train.training import ( + model_change_out_bias, +) from deepmd.pt_expt.train.wrapper import ( ModelWrapper, ) from deepmd.pt_expt.utils.env import ( DEVICE, ) +from deepmd.pt_expt.utils.stat import ( + make_stat_input, +) from deepmd.utils.argcheck import ( normalize, ) from deepmd.utils.compat import ( update_deepmd_input, ) +from deepmd.utils.data_system import ( + DeepmdDataSystem, +) def to_numpy(x): @@ -184,6 +193,7 @@ def setUpClass(cls) -> None: # Build & train 1-step model config = _make_config(data_dir) + config["model"]["preset_out_bias"] = {"energy": [-10.0, -13.6]} config = update_deepmd_input(config, warning=False) config = normalize(config) config["training"]["save_ckpt"] = "model.ckpt" @@ -193,8 +203,18 @@ def setUpClass(cls) -> None: cls.model_path = os.path.join(cls.tmpdir, "model.ckpt.pt") - # Record original bias - cls.original_bias = to_numpy(trainer.wrapper.model["Default"].get_out_bias()) + # A checkpoint may already carry a downstream-adjusted finite bias. + model = trainer.wrapper.model["Default"] + old_bias = model.get_out_bias() + model.set_out_bias( + torch.as_tensor( + [7.0, 8.0], dtype=old_bias.dtype, device=old_bias.device + ).reshape(old_bias.shape) + ) + checkpoint = torch.load(cls.model_path, map_location=DEVICE, weights_only=True) + checkpoint["model"] = trainer.wrapper.state_dict() + torch.save(checkpoint, cls.model_path) + cls.original_bias = to_numpy(model.get_out_bias()).copy() # Pre-freeze shared .pte and .pt2 files so individual tests don't # each pay the AOTInductor compilation cost (~82s per .pt2). @@ -231,6 +251,44 @@ def _load_model_from_ckpt(self, ckpt_path: str): wrapper.load_state_dict(model_state) return model + def _expected_fitted_bias(self): + model = self._load_model_from_ckpt(self.model_path) + # Compare against the established no-preset statistics path with the + # same network and adjusted bias, rather than starting from the preset. + model.atomic_model.preset_out_bias = None + data = DeepmdDataSystem( + systems=self.data_file, + batch_size=1, + test_size=1, + rcut=model.get_rcut(), + type_map=model.get_type_map(), + ) + from deepmd.pt_expt.train.training import ( + get_loss, + ) + + loss = get_loss({"inference": True}, 1.0, len(model.get_type_map()), model) + data.add_data_requirements(loss.label_requirement) + sampled = make_stat_input(data, min(data.get_nbatches())) + return to_numpy(model_change_out_bias(model, sampled).get_out_bias()) + + def _assert_preset_bias_update(self, model) -> None: + np.testing.assert_allclose( + to_numpy(model.get_out_bias()), self._expected_fitted_bias(), atol=1e-10 + ) + self.assertEqual( + model.atomic_model.preset_out_bias, {"energy": [[-10.0], [-13.6]]} + ) + + def test_default_bias_helper_preserves_preset_behavior(self) -> None: + model = self._load_model_from_ckpt(self.model_path) + np.testing.assert_allclose(to_numpy(model.get_out_bias()), self.original_bias) + sampled = [{"atype": np.array([[0, 1]], dtype=np.int32)}] + model_change_out_bias(model, sampled) + np.testing.assert_allclose( + to_numpy(model.get_out_bias()).reshape(-1), [-10.0, -13.6] + ) + def test_change_bias_with_data(self) -> None: output_path = os.path.join(self.tmpdir, "model_data_bias.pt") run_dp( @@ -239,6 +297,7 @@ def test_change_bias_with_data(self) -> None: ) updated_model = self._load_model_from_ckpt(output_path) updated_bias = to_numpy(updated_model.get_out_bias()) + self._assert_preset_bias_update(updated_model) original_bias = np.array(self.original_bias) # Bias should have changed from the original self.assertFalse( @@ -333,6 +392,11 @@ def test_change_bias_frozen_pte(self) -> None: updated_data = serialize_from_file(output_pte) updated_model = BaseModel.deserialize(updated_data["model"]) updated_bias = to_numpy(updated_model.get_out_bias()) + self._assert_preset_bias_update(updated_model) + self.assertEqual( + updated_data["model_def_script"]["preset_out_bias"], + self.config["model"]["preset_out_bias"], + ) # Bias should have changed self.assertFalse( @@ -362,6 +426,11 @@ def test_change_bias_frozen_pt2(self) -> None: updated_data = serialize_from_file(output_pt2) updated_model = BaseModel.deserialize(updated_data["model"]) updated_bias = to_numpy(updated_model.get_out_bias()) + self._assert_preset_bias_update(updated_model) + self.assertEqual( + updated_data["model_def_script"]["preset_out_bias"], + self.config["model"]["preset_out_bias"], + ) self.assertFalse( np.allclose(original_bias, updated_bias), From b72a481f1e123344c8f0b9f36962ea95e966684d Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:20:47 +0800 Subject: [PATCH 6/9] fix(spin): map downstream bias presets to virtual type layout --- deepmd/dpmodel/model/spin_model.py | 8 +++++++ deepmd/pt/model/model/sezm_spin_model.py | 8 +++++++ deepmd/pt/model/model/spin_model.py | 8 +++++++ .../dpmodel/test_spin_model_virtual_types.py | 12 ++++++++++ source/tests/pd/test_change_bias.py | 23 +++++++++++++++++++ source/tests/pt/model/test_sezm_spin_model.py | 15 ++++++++++++ .../pt/model/test_spin_model_virtual_types.py | 13 +++++++++++ 7 files changed, 87 insertions(+) diff --git a/deepmd/dpmodel/model/spin_model.py b/deepmd/dpmodel/model/spin_model.py index 8930e84e35..ffebdf7870 100644 --- a/deepmd/dpmodel/model/spin_model.py +++ b/deepmd/dpmodel/model/spin_model.py @@ -34,6 +34,10 @@ from deepmd.utils.spin import ( Spin, ) +from deepmd.utils.preset_out_bias import ( + normalize_preset_out_bias, + remap_preset_out_bias, +) _NATOMS_VEC_KEYS = ("natoms", "real_natoms_vec") _SPIN_STAT_RESERVED_KEYS = frozenset( @@ -622,6 +626,10 @@ def change_out_bias( and do least square on the errors to obtain the target shift as bias. 'set-by-statistic' : directly use the statistic output bias in the target dataset. """ + preset_out_bias = remap_preset_out_bias( + normalize_preset_out_bias(preset_out_bias, self.get_type_map()), + list(range(self.get_ntypes())) + [-1] * self.get_ntypes(), + ) spin_sampled_func = self._get_spin_sampled_func( merged if callable(merged) else lambda: merged ) diff --git a/deepmd/pt/model/model/sezm_spin_model.py b/deepmd/pt/model/model/sezm_spin_model.py index 1a5389e3f9..83e4489d87 100644 --- a/deepmd/pt/model/model/sezm_spin_model.py +++ b/deepmd/pt/model/model/sezm_spin_model.py @@ -47,6 +47,10 @@ from deepmd.utils.spin import ( Spin, ) +from deepmd.utils.preset_out_bias import ( + normalize_preset_out_bias, + remap_preset_out_bias, +) from deepmd.utils.version import ( check_version_compatibility, ) @@ -435,6 +439,10 @@ def change_out_bias( preset_out_bias: dict | None = None, ) -> None: """Change output bias using spin-expanded sampled data.""" + preset_out_bias = remap_preset_out_bias( + normalize_preset_out_bias(preset_out_bias, self.get_type_map()), + list(range(self.get_ntypes())) + [-1] * self.get_ntypes(), + ) spin_sampled_func = self._get_spin_sampled_func( merged if callable(merged) else lambda: merged ) diff --git a/deepmd/pt/model/model/spin_model.py b/deepmd/pt/model/model/spin_model.py index 5197f6d16b..ab5a93d9e4 100644 --- a/deepmd/pt/model/model/spin_model.py +++ b/deepmd/pt/model/model/spin_model.py @@ -30,6 +30,10 @@ from deepmd.utils.spin import ( Spin, ) +from deepmd.utils.preset_out_bias import ( + normalize_preset_out_bias, + remap_preset_out_bias, +) from .make_model import ( make_model, @@ -537,6 +541,10 @@ def change_out_bias( and do least square on the errors to obtain the target shift as bias. 'set-by-statistic' : directly use the statistic output bias in the target dataset. """ + preset_out_bias = remap_preset_out_bias( + normalize_preset_out_bias(preset_out_bias, self.get_type_map()), + list(range(self.get_ntypes())) + [-1] * self.get_ntypes(), + ) spin_sampled_func = self._get_spin_sampled_func( merged if callable(merged) else lambda: merged ) diff --git a/source/tests/common/dpmodel/test_spin_model_virtual_types.py b/source/tests/common/dpmodel/test_spin_model_virtual_types.py index 727eac3b53..4795dfb971 100644 --- a/source/tests/common/dpmodel/test_spin_model_virtual_types.py +++ b/source/tests/common/dpmodel/test_spin_model_virtual_types.py @@ -80,6 +80,18 @@ def test_dense_spin_expansion_preserves_virtual_types(model) -> None: ) +def test_real_type_sequence_bias_override(model) -> None: + sample = { + "coord": np.arange(9, dtype=np.float64).reshape(1, 3, 3), + "atype": np.array([[0, 1, 2]], dtype=np.int32), + "spin": np.ones((1, 3, 3), dtype=np.float64), + } + model.change_out_bias([sample], preset_out_bias={"energy": [1.0, 2.0, 3.0]}) + np.testing.assert_allclose( + to_numpy_array(model.get_out_bias()).reshape(-1), [1.0, 2.0, 3.0, 0.0, 0.0, 0.0] + ) + + def test_lower_spin_expansion_preserves_virtual_types(model) -> None: """Local and ghost placeholders remain negative in the switched layout.""" extended_coord = np.arange(12, dtype=np.float64).reshape(1, 4, 3) diff --git a/source/tests/pd/test_change_bias.py b/source/tests/pd/test_change_bias.py index 2d87b739ff..36c2f0290c 100644 --- a/source/tests/pd/test_change_bias.py +++ b/source/tests/pd/test_change_bias.py @@ -140,6 +140,29 @@ def test_change_bias_with_user_defined(self): ) np.testing.assert_allclose(updated_bias.numpy(), expected_bias.numpy()) + def test_change_bias_ignores_checkpoint_preset(self): + checkpoint = paddle.load(str(self.model_path)) + preset = {"energy": [1.0e6] * len(self.config["model"]["type_map"])} + checkpoint["model"]["_extra_state"]["model_params"]["preset_out_bias"] = preset + paddle.save(checkpoint, str(self.model_path)) + run_dp( + f"dp --pd change-bias {self.model_path!s} -s {self.data_file[0]} -o {self.model_path_data_bias!s}" + ) + updated = paddle.load(str(self.model_path_data_bias)) + model_params = updated["model"]["_extra_state"]["model_params"] + self.assertEqual(model_params["preset_out_bias"], preset) + wrapper = ModelWrapper(get_model_for_wrapper(model_params)) + wrapper.set_state_dict(updated["model"]) + expected_model = model_change_out_bias( + self.trainer.wrapper.model["Default"], + self.sampled, + _bias_adjust_mode="change-by-statistic", + ) + np.testing.assert_allclose( + wrapper.model["Default"].get_out_bias().numpy(), + expected_model.get_out_bias().numpy(), + ) + def tearDown(self): for f in os.listdir("."): if f.startswith("change-bias-model") and f.endswith(".pd"): diff --git a/source/tests/pt/model/test_sezm_spin_model.py b/source/tests/pt/model/test_sezm_spin_model.py index 314a715364..dcfa932228 100644 --- a/source/tests/pt/model/test_sezm_spin_model.py +++ b/source/tests/pt/model/test_sezm_spin_model.py @@ -438,6 +438,21 @@ def test_zbl_change_out_bias_is_invariant_for_self_labels(self) -> None: rtol=1.0e-12, ) + def test_real_type_sequence_bias_override(self) -> None: + model = get_model(self._build_model_params()).to(self.device) + sample = { + "coord": self.coord, + "atype": self.atype, + "spin": self.spin, + "box": self.box, + } + model.change_out_bias([sample], preset_out_bias={"energy": [1.0, 2.0]}) + bias = model.get_out_bias() + torch.testing.assert_close( + bias.reshape(-1), + torch.tensor([1.0, 2.0, 0.0, 0.0], dtype=bias.dtype, device=bias.device), + ) + def test_zbl_statistics_use_physical_spin_neighbor_topology(self) -> None: """Statistics preserve real-nlist expansion when `sel` is saturated.""" params = self._build_model_params(bridging_method="ZBL") diff --git a/source/tests/pt/model/test_spin_model_virtual_types.py b/source/tests/pt/model/test_spin_model_virtual_types.py index b8d8dbf772..8193751b06 100644 --- a/source/tests/pt/model/test_spin_model_virtual_types.py +++ b/source/tests/pt/model/test_spin_model_virtual_types.py @@ -61,6 +61,19 @@ def _tensor(array: np.ndarray) -> torch.Tensor: class TestPtSpinModelVirtualTypes(unittest.TestCase): """Every pt lookup must mask ``atype < 0`` exactly as dpmodel does.""" + def test_real_type_sequence_bias_override(self) -> None: + model = get_pt_model(MODEL_CONFIG) + sample = { + "coord": _tensor(np.arange(9, dtype=np.float64).reshape(1, 3, 3)), + "atype": _tensor(np.array([[0, 1, 2]], dtype=np.int64)), + "spin": _tensor(np.ones((1, 3, 3), dtype=np.float64)), + } + model.change_out_bias([sample], preset_out_bias={"energy": [1.0, 2.0, 3.0]}) + np.testing.assert_allclose( + to_numpy_array(model.get_out_bias()).reshape(-1), + [1.0, 2.0, 3.0, 0.0, 0.0, 0.0], + ) + def test_dense_spin_expansion_preserves_virtual_types(self) -> None: model = get_pt_model(MODEL_CONFIG) coord = np.arange(9, dtype=np.float64).reshape(1, 3, 3) From 4f5af649bb88751a236b5c11634383df8714bcf3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:21:59 +0000 Subject: [PATCH 7/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- deepmd/dpmodel/model/spin_model.py | 6 +++--- deepmd/pt/model/model/sezm_spin_model.py | 6 +++--- deepmd/pt/model/model/spin_model.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/deepmd/dpmodel/model/spin_model.py b/deepmd/dpmodel/model/spin_model.py index ffebdf7870..f97a1d60ea 100644 --- a/deepmd/dpmodel/model/spin_model.py +++ b/deepmd/dpmodel/model/spin_model.py @@ -31,13 +31,13 @@ from deepmd.dpmodel.output_def import ( ModelOutputDef, ) -from deepmd.utils.spin import ( - Spin, -) from deepmd.utils.preset_out_bias import ( normalize_preset_out_bias, remap_preset_out_bias, ) +from deepmd.utils.spin import ( + Spin, +) _NATOMS_VEC_KEYS = ("natoms", "real_natoms_vec") _SPIN_STAT_RESERVED_KEYS = frozenset( diff --git a/deepmd/pt/model/model/sezm_spin_model.py b/deepmd/pt/model/model/sezm_spin_model.py index 83e4489d87..779d07376d 100644 --- a/deepmd/pt/model/model/sezm_spin_model.py +++ b/deepmd/pt/model/model/sezm_spin_model.py @@ -44,13 +44,13 @@ from deepmd.utils.path import ( DPPath, ) -from deepmd.utils.spin import ( - Spin, -) from deepmd.utils.preset_out_bias import ( normalize_preset_out_bias, remap_preset_out_bias, ) +from deepmd.utils.spin import ( + Spin, +) from deepmd.utils.version import ( check_version_compatibility, ) diff --git a/deepmd/pt/model/model/spin_model.py b/deepmd/pt/model/model/spin_model.py index ab5a93d9e4..3ba467b851 100644 --- a/deepmd/pt/model/model/spin_model.py +++ b/deepmd/pt/model/model/spin_model.py @@ -27,13 +27,13 @@ from deepmd.utils.path import ( DPPath, ) -from deepmd.utils.spin import ( - Spin, -) from deepmd.utils.preset_out_bias import ( normalize_preset_out_bias, remap_preset_out_bias, ) +from deepmd.utils.spin import ( + Spin, +) from .make_model import ( make_model, From 0be5abf17d51941cc28b8092ee9851a27ce9e83f Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:32:49 +0800 Subject: [PATCH 8/9] test(model): verify adjusted bias baseline during residual fitting --- .../dpmodel/test_atomic_model_global_stat.py | 27 +++++++++++++---- .../pd/model/test_atomic_model_global_stat.py | 29 +++++++++++++++---- .../pt/model/test_atomic_model_global_stat.py | 29 +++++++++++++++---- 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/source/tests/common/dpmodel/test_atomic_model_global_stat.py b/source/tests/common/dpmodel/test_atomic_model_global_stat.py index 7760cce9cd..28873494d3 100644 --- a/source/tests/common/dpmodel/test_atomic_model_global_stat.py +++ b/source/tests/common/dpmodel/test_atomic_model_global_stat.py @@ -1,6 +1,9 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import tempfile import unittest +from unittest.mock import ( + patch, +) from pathlib import ( Path, ) @@ -331,11 +334,25 @@ def test_preset_bias(self) -> None: np.testing.assert_almost_equal(out_bias["bar"], bar_bias) # Standalone change-bias explicitly ignores the checkpoint preset. - md0.change_out_bias( - self.merged_output_stat, - bias_adjust_mode="change-by-statistic", - preset_out_bias={}, - ) + adjusted_bias = foo_bias + 7.0 + md0._store_out_stat({"foo": adjusted_bias}) + forward = md0._get_forward_wrapper_func() + seen_biases = [] + + def check_forward(*args, **kwargs): + bias, _ = md0._fetch_out_stat(["foo"]) + seen_biases.append(bias["foo"].copy()) + return forward(*args, **kwargs) + + with patch.object(md0, "_get_forward_wrapper_func", return_value=check_forward): + md0.change_out_bias( + self.merged_output_stat, + bias_adjust_mode="change-by-statistic", + preset_out_bias={}, + ) + self.assertTrue(seen_biases) + for seen_bias in seen_biases: + np.testing.assert_almost_equal(seen_bias, adjusted_bias) out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) np.testing.assert_almost_equal( out_bias["foo"], np.array([2.0, -5.0]).reshape(2, 1) diff --git a/source/tests/pd/model/test_atomic_model_global_stat.py b/source/tests/pd/model/test_atomic_model_global_stat.py index ff747eb518..557c8d1ea2 100644 --- a/source/tests/pd/model/test_atomic_model_global_stat.py +++ b/source/tests/pd/model/test_atomic_model_global_stat.py @@ -1,6 +1,9 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import tempfile import unittest +from unittest.mock import ( + patch, +) from pathlib import ( Path, ) @@ -362,12 +365,26 @@ def cvt_ret(x): np.testing.assert_almost_equal(to_numpy_array(out_bias["bar"]), bar_bias) # Standalone change-bias explicitly ignores the checkpoint preset. - BaseAtomicModel.change_out_bias( - md0, - self.merged_output_stat, - bias_adjust_mode="change-by-statistic", - preset_out_bias={}, - ) + adjusted_bias = foo_bias + 7.0 + md0._store_out_stat({"foo": to_paddle_tensor(adjusted_bias)}) + forward = md0._get_forward_wrapper_func() + seen_biases = [] + + def check_forward(*args, **kwargs): + bias, _ = md0._fetch_out_stat(["foo"]) + seen_biases.append(to_numpy_array(bias["foo"]).copy()) + return forward(*args, **kwargs) + + with patch.object(md0, "_get_forward_wrapper_func", return_value=check_forward): + BaseAtomicModel.change_out_bias( + md0, + self.merged_output_stat, + bias_adjust_mode="change-by-statistic", + preset_out_bias={}, + ) + self.assertTrue(seen_biases) + for seen_bias in seen_biases: + np.testing.assert_almost_equal(seen_bias, adjusted_bias) out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) np.testing.assert_almost_equal( to_numpy_array(out_bias["foo"]), np.array([2.0, -5.0]).reshape(2, 1) diff --git a/source/tests/pt/model/test_atomic_model_global_stat.py b/source/tests/pt/model/test_atomic_model_global_stat.py index 3e304ee1d7..388b2458e9 100644 --- a/source/tests/pt/model/test_atomic_model_global_stat.py +++ b/source/tests/pt/model/test_atomic_model_global_stat.py @@ -1,6 +1,9 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import tempfile import unittest +from unittest.mock import ( + patch, +) from pathlib import ( Path, ) @@ -388,12 +391,26 @@ def cvt_ret(x): np.testing.assert_almost_equal(to_numpy_array(out_bias["bar"]), bar_bias) # Standalone change-bias explicitly ignores the checkpoint preset. - BaseAtomicModel.change_out_bias( - md0, - self.merged_output_stat, - bias_adjust_mode="change-by-statistic", - preset_out_bias={}, - ) + adjusted_bias = foo_bias + 7.0 + md0._store_out_stat({"foo": to_torch_tensor(adjusted_bias)}) + forward = md0._get_forward_wrapper_func() + seen_biases = [] + + def check_forward(*args, **kwargs): + bias, _ = md0._fetch_out_stat(["foo"]) + seen_biases.append(to_numpy_array(bias["foo"]).copy()) + return forward(*args, **kwargs) + + with patch.object(md0, "_get_forward_wrapper_func", return_value=check_forward): + BaseAtomicModel.change_out_bias( + md0, + self.merged_output_stat, + bias_adjust_mode="change-by-statistic", + preset_out_bias={}, + ) + self.assertTrue(seen_biases) + for seen_bias in seen_biases: + np.testing.assert_almost_equal(seen_bias, adjusted_bias) out_bias, _ = md0._fetch_out_stat(["foo", "bar"]) np.testing.assert_almost_equal( to_numpy_array(out_bias["foo"]), np.array([2.0, -5.0]).reshape(2, 1) From 8a13bce37efa0314a43227daeccb8631419f6d5a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:33:56 +0000 Subject: [PATCH 9/9] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../tests/common/dpmodel/test_atomic_model_global_stat.py | 6 +++--- source/tests/pd/model/test_atomic_model_global_stat.py | 6 +++--- source/tests/pt/model/test_atomic_model_global_stat.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/source/tests/common/dpmodel/test_atomic_model_global_stat.py b/source/tests/common/dpmodel/test_atomic_model_global_stat.py index 28873494d3..1ddc7d1acc 100644 --- a/source/tests/common/dpmodel/test_atomic_model_global_stat.py +++ b/source/tests/common/dpmodel/test_atomic_model_global_stat.py @@ -1,15 +1,15 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import tempfile import unittest -from unittest.mock import ( - patch, -) from pathlib import ( Path, ) from typing import ( NoReturn, ) +from unittest.mock import ( + patch, +) import h5py import numpy as np diff --git a/source/tests/pd/model/test_atomic_model_global_stat.py b/source/tests/pd/model/test_atomic_model_global_stat.py index 557c8d1ea2..023bafb277 100644 --- a/source/tests/pd/model/test_atomic_model_global_stat.py +++ b/source/tests/pd/model/test_atomic_model_global_stat.py @@ -1,12 +1,12 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import tempfile import unittest -from unittest.mock import ( - patch, -) from pathlib import ( Path, ) +from unittest.mock import ( + patch, +) import h5py import numpy as np diff --git a/source/tests/pt/model/test_atomic_model_global_stat.py b/source/tests/pt/model/test_atomic_model_global_stat.py index 388b2458e9..dc12dd23b6 100644 --- a/source/tests/pt/model/test_atomic_model_global_stat.py +++ b/source/tests/pt/model/test_atomic_model_global_stat.py @@ -1,15 +1,15 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import tempfile import unittest -from unittest.mock import ( - patch, -) from pathlib import ( Path, ) from typing import ( NoReturn, ) +from unittest.mock import ( + patch, +) import h5py import numpy as np