Skip to content

Commit f515b2f

Browse files
Han Wangclaude
andcommitted
test(loss): xfail-track force_mag MAE frame-normalization debt
Both reviewers asked for the known force_mag MAE grad-accum discrepancy (sum over frames -> 2x factor for nf=2) to be tracked by CI rather than only documented in a comment. Add strict xfail tests in the pt and dpmodel loss-padding suites; strict mode self-heals (XPASS -> failure) once force_mag MAE is switched to a frame-wise mean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 25cedda commit f515b2f

2 files changed

Lines changed: 208 additions & 0 deletions

File tree

source/tests/common/dpmodel/test_loss_padding.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""
1515

1616
import numpy as np
17+
import pytest
1718

1819
from deepmd.dpmodel.loss.dos import (
1920
DOSLoss,
@@ -2124,3 +2125,118 @@ def make_padded():
21242125
make_B,
21252126
make_padded,
21262127
)
2128+
2129+
2130+
class TestDPModelEnerSpinLossForceMagMAEGradAccum:
2131+
"""force_mag MAE (dpmodel) FAILS the grad-accum invariant by a known 2x factor.
2132+
2133+
Unlike the MSE path, ``force_mag`` MAE reduces with ``xp.sum`` over frames
2134+
instead of a frame-wise mean, so a padded ``[A+B]`` batch (nf=2) yields twice
2135+
the mean-of-frames reference. This is a pre-existing frame-normalization
2136+
artifact, NOT a ghost-atom padding bug (padding is correctly excluded via
2137+
``mask_mag``); see the module NOTE above. The test is ``xfail(strict=True)``
2138+
so the debt is tracked in CI and self-heals: if ``force_mag`` MAE is switched
2139+
to a frame-wise mean, this test XPASSes and strict mode turns that into a
2140+
failure that flags the marker for removal.
2141+
"""
2142+
2143+
def _make_loss(self):
2144+
return EnergySpinLossDPModel(
2145+
starter_learning_rate=1.0,
2146+
start_pref_e=0.0,
2147+
limit_pref_e=0.0,
2148+
start_pref_fr=0.0,
2149+
limit_pref_fr=0.0,
2150+
start_pref_fm=1.0,
2151+
limit_pref_fm=1.0,
2152+
start_pref_v=0.0,
2153+
limit_pref_v=0.0,
2154+
loss_func="mae",
2155+
)
2156+
2157+
def _loss_fn(self, loss_obj, model_pred, label, natoms):
2158+
loss, _ = loss_obj.call(1.0, natoms, model_pred, label)
2159+
return float(loss)
2160+
2161+
@pytest.mark.xfail(
2162+
strict=True,
2163+
reason="force_mag MAE sums over frames (2x factor for nf=2); "
2164+
"pre-existing frame-normalization inconsistency tracked for follow-up.",
2165+
)
2166+
def test_mae_grad_accum(self):
2167+
"""force_mag MAE violates the frame-average invariant (documented follow-up)."""
2168+
fm_A = _rnd(NA, 3)
2169+
fm_A_hat = _rnd(NA, 3)
2170+
fm_B = _rnd(NB, 3)
2171+
fm_B_hat = _rnd(NB, 3)
2172+
2173+
def make_A():
2174+
fm_A_full = np.zeros((NA, 3), dtype=np.float64)
2175+
fm_A_full[:_NM] = fm_A[:_NM]
2176+
fm_A_hat_full = np.zeros((NA, 3), dtype=np.float64)
2177+
fm_A_hat_full[:_NM] = fm_A_hat[:_NM]
2178+
pred = {
2179+
"energy": np.zeros((1, 1), dtype=np.float64),
2180+
"force_mag": fm_A_full[None], # [1, NA, 3]
2181+
"mask_mag": _MASK_MAG_A,
2182+
}
2183+
lbl = {
2184+
"force_mag": fm_A_hat_full[None],
2185+
"find_force_mag": 1.0,
2186+
"find_energy": 0.0,
2187+
"find_force": 0.0,
2188+
"find_virial": 0.0,
2189+
}
2190+
return pred, lbl, NA
2191+
2192+
def make_B():
2193+
fm_B_full = np.zeros((NB, 3), dtype=np.float64)
2194+
fm_B_full[:_NM] = fm_B[:_NM]
2195+
fm_B_hat_full = np.zeros((NB, 3), dtype=np.float64)
2196+
fm_B_hat_full[:_NM] = fm_B_hat[:_NM]
2197+
pred = {
2198+
"energy": np.zeros((1, 1), dtype=np.float64),
2199+
"force_mag": fm_B_full[None], # [1, NB, 3]
2200+
"mask_mag": _MASK_MAG_B,
2201+
}
2202+
lbl = {
2203+
"force_mag": fm_B_hat_full[None],
2204+
"find_force_mag": 1.0,
2205+
"find_energy": 0.0,
2206+
"find_force": 0.0,
2207+
"find_virial": 0.0,
2208+
}
2209+
return pred, lbl, NB
2210+
2211+
def make_padded():
2212+
fm_A_pad = np.zeros((NP, 3), dtype=np.float64)
2213+
fm_A_pad[:_NM] = fm_A[:_NM]
2214+
fm_A_hat_pad = np.zeros((NP, 3), dtype=np.float64)
2215+
fm_A_hat_pad[:_NM] = fm_A_hat[:_NM]
2216+
fm_B_pad = np.zeros((NP, 3), dtype=np.float64)
2217+
fm_B_pad[:_NM] = fm_B[:_NM]
2218+
fm_B_hat_pad = np.zeros((NP, 3), dtype=np.float64)
2219+
fm_B_hat_pad[:_NM] = fm_B_hat[:_NM]
2220+
fm_pad = np.stack([fm_A_pad, fm_B_pad], axis=0) # [2, NP, 3]
2221+
fm_hat_pad = np.stack([fm_A_hat_pad, fm_B_hat_pad], axis=0)
2222+
pred = {
2223+
"energy": np.zeros((2, 1), dtype=np.float64),
2224+
"force_mag": fm_pad,
2225+
"mask_mag": _MASK_MAG_PAD_SPIN, # ghost atoms have mask_mag=False
2226+
"mask": _MASK_PAD_SPIN,
2227+
}
2228+
lbl = {
2229+
"force_mag": fm_hat_pad,
2230+
"find_force_mag": 1.0,
2231+
"find_energy": 0.0,
2232+
"find_force": 0.0,
2233+
"find_virial": 0.0,
2234+
}
2235+
return pred, lbl, NP
2236+
2237+
assert_grad_accum_invariant(
2238+
lambda mp, lb, na: self._loss_fn(self._make_loss(), mp, lb, na),
2239+
make_A,
2240+
make_B,
2241+
make_padded,
2242+
)

source/tests/pt/test_loss_padding.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"""
1414

1515
import numpy as np
16+
import pytest
1617
import torch
1718

1819
from deepmd.pt.loss.dos import (
@@ -2061,3 +2062,94 @@ def make_padded():
20612062
make_B,
20622063
make_padded,
20632064
)
2065+
2066+
2067+
class TestPTEnerSpinLossForceMagMAEGradAccum:
2068+
"""force_mag MAE (pt) FAILS the grad-accum invariant by a known 2x factor.
2069+
2070+
Unlike the MSE path, ``force_mag`` MAE reduces with ``.sum()`` over frames
2071+
instead of a frame-wise mean, so a padded ``[A+B]`` batch (nf=2) yields twice
2072+
the mean-of-frames reference. This is a pre-existing frame-normalization
2073+
artifact, NOT a ghost-atom padding bug (padding is correctly excluded via
2074+
``mask_mag``); see the module NOTE above. The test is ``xfail(strict=True)``
2075+
so the debt is tracked in CI and self-heals: if ``force_mag`` MAE is switched
2076+
to a frame-wise mean, this test XPASSes and strict mode turns that into a
2077+
failure that flags the marker for removal.
2078+
"""
2079+
2080+
def _make_loss(self):
2081+
return EnergySpinLossPT(
2082+
starter_learning_rate=1.0,
2083+
start_pref_e=0.0,
2084+
limit_pref_e=0.0,
2085+
start_pref_fr=0.0,
2086+
limit_pref_fr=0.0,
2087+
start_pref_fm=1.0,
2088+
limit_pref_fm=1.0,
2089+
start_pref_v=0.0,
2090+
limit_pref_v=0.0,
2091+
loss_func="mae",
2092+
)
2093+
2094+
@pytest.mark.xfail(
2095+
strict=True,
2096+
reason="force_mag MAE sums over frames (2x factor for nf=2); "
2097+
"pre-existing frame-normalization inconsistency tracked for follow-up.",
2098+
)
2099+
def test_mae_grad_accum(self):
2100+
"""force_mag MAE violates the frame-average invariant (documented follow-up)."""
2101+
fm_A = _rnd_t(_NM_PT, 3)
2102+
fm_A_hat = _rnd_t(_NM_PT, 3)
2103+
fm_B = _rnd_t(_NM_PT, 3)
2104+
fm_B_hat = _rnd_t(_NM_PT, 3)
2105+
2106+
def make_A():
2107+
fm_A_full = torch.zeros(NA, 3, dtype=torch.float64, device="cpu")
2108+
fm_A_full[:_NM_PT] = fm_A
2109+
fm_A_hat_full = torch.zeros(NA, 3, dtype=torch.float64, device="cpu")
2110+
fm_A_hat_full[:_NM_PT] = fm_A_hat
2111+
mp = {
2112+
"force_mag": fm_A_full.unsqueeze(0), # [1, NA, 3]
2113+
"mask_mag": _MASK_MAG_A_PT,
2114+
}
2115+
lb = {"force_mag": fm_A_hat_full.unsqueeze(0), "find_force_mag": 1.0}
2116+
return mp, lb, NA
2117+
2118+
def make_B():
2119+
fm_B_full = torch.zeros(NB, 3, dtype=torch.float64, device="cpu")
2120+
fm_B_full[:_NM_PT] = fm_B
2121+
fm_B_hat_full = torch.zeros(NB, 3, dtype=torch.float64, device="cpu")
2122+
fm_B_hat_full[:_NM_PT] = fm_B_hat
2123+
mp = {
2124+
"force_mag": fm_B_full.unsqueeze(0), # [1, NB, 3]
2125+
"mask_mag": _MASK_MAG_B_PT,
2126+
}
2127+
lb = {"force_mag": fm_B_hat_full.unsqueeze(0), "find_force_mag": 1.0}
2128+
return mp, lb, NB
2129+
2130+
def make_padded():
2131+
fm_A_pad = torch.zeros(NP, 3, dtype=torch.float64, device="cpu")
2132+
fm_A_pad[:_NM_PT] = fm_A
2133+
fm_A_hat_pad = torch.zeros(NP, 3, dtype=torch.float64, device="cpu")
2134+
fm_A_hat_pad[:_NM_PT] = fm_A_hat
2135+
fm_B_pad = torch.zeros(NP, 3, dtype=torch.float64, device="cpu")
2136+
fm_B_pad[:_NM_PT] = fm_B
2137+
fm_B_hat_pad = torch.zeros(NP, 3, dtype=torch.float64, device="cpu")
2138+
fm_B_hat_pad[:_NM_PT] = fm_B_hat
2139+
mp = {
2140+
"force_mag": torch.stack([fm_A_pad, fm_B_pad], dim=0), # [2, NP, 3]
2141+
"mask_mag": _MASK_MAG_PAD_SPIN_PT,
2142+
"mask": _MASK_PAD_SPIN_PT,
2143+
}
2144+
lb = {
2145+
"force_mag": torch.stack([fm_A_hat_pad, fm_B_hat_pad], dim=0),
2146+
"find_force_mag": 1.0,
2147+
}
2148+
return mp, lb, NP
2149+
2150+
assert_grad_accum_invariant(
2151+
lambda mp, lb, na: _spin_loss_fn(self._make_loss(), mp, lb, na),
2152+
make_A,
2153+
make_B,
2154+
make_padded,
2155+
)

0 commit comments

Comments
 (0)