@@ -60,7 +60,11 @@ def setUp(self) -> None:
6060 [[0 , 0 , 0 , 1 , 1 ]], dtype = torch .int64 , device = self .device
6161 )
6262
63- def _make_model (self ) -> EnergyModel :
63+ def _make_model (
64+ self ,
65+ numb_fparam : int = 0 ,
66+ numb_aparam : int = 0 ,
67+ ) -> EnergyModel :
6468 ds = DescrptSeA (
6569 self .rcut ,
6670 self .rcut_smth ,
@@ -71,6 +75,8 @@ def _make_model(self) -> EnergyModel:
7175 self .nt ,
7276 ds .get_dim_out (),
7377 1 ,
78+ numb_fparam = numb_fparam ,
79+ numb_aparam = numb_aparam ,
7480 mixed_types = ds .mixed_types (),
7581 seed = GLOBAL_SEED ,
7682 ).to (self .device )
@@ -98,17 +104,8 @@ def test_output_shapes(self) -> None:
98104 self .assertEqual (ret ["force" ].shape , (1 , self .natoms , 3 ))
99105 self .assertEqual (ret ["virial" ].shape , (1 , 9 ))
100106
101- def test_forward_lower_exportable (self ) -> None :
102- """Test that EnergyModel.forward_lower returns an exportable module.
103-
104- forward_lower() uses make_fx to trace through torch.autograd.grad,
105- decomposing the backward pass into primitive ops. The returned module
106- can be passed directly to torch.export.export.
107- """
108- md = self ._make_model ()
109- md .eval ()
110-
111- # Prepare extended coords and neighbor list using dpmodel utilities
107+ def _prepare_lower_inputs (self ):
108+ """Build extended coords, atype, nlist, mapping as torch tensors."""
112109 coord_np = self .coord .detach ().cpu ().numpy ()
113110 atype_np = self .atype .detach ().cpu ().numpy ()
114111 cell_np = self .cell .reshape (1 , 9 ).detach ().cpu ().numpy ()
@@ -131,8 +128,6 @@ def test_forward_lower_exportable(self) -> None:
131128 distinguish_types = True ,
132129 )
133130 extended_coord = extended_coord .reshape (1 , - 1 , 3 )
134-
135- # Convert to torch tensors
136131 ext_coord = torch .tensor (
137132 extended_coord ,
138133 dtype = torch .float64 ,
@@ -145,63 +140,195 @@ def test_forward_lower_exportable(self) -> None:
145140 )
146141 nlist_t = torch .tensor (nlist , dtype = torch .int64 , device = self .device )
147142 mapping_t = torch .tensor (mapping , dtype = torch .int64 , device = self .device )
143+ return ext_coord , ext_atype , nlist_t , mapping_t
144+
145+ def test_forward_lower_exportable (self ) -> None :
146+ """Test that EnergyModel.forward_lower returns an exportable module.
147+
148+ forward_lower() uses make_fx to trace through torch.autograd.grad,
149+ decomposing the backward pass into primitive ops. The returned module
150+ can be passed directly to torch.export.export.
151+
152+ The test builds a model with numb_fparam > 0 and numb_aparam > 0 and
153+ verifies that:
154+ 1. The traced / exported module reproduces eager results (zero params).
155+ 2. The traced / exported module reproduces eager results with non-zero
156+ fparam and aparam (ruling out baked-in constants).
157+ 3. Changing fparam or aparam at runtime actually changes the output.
158+ """
159+ numb_fparam = 2
160+ numb_aparam = 3
161+ md = self ._make_model (
162+ numb_fparam = numb_fparam ,
163+ numb_aparam = numb_aparam ,
164+ )
165+ md .eval ()
148166
149- # Eager reference via _forward_lower
150- ret0 = md ._forward_lower (
167+ ext_coord , ext_atype , nlist_t , mapping_t = self ._prepare_lower_inputs ()
168+ nframes = ext_coord .shape [0 ]
169+ nloc = self .natoms
170+ output_keys = ("energy" , "extended_force" , "virial" , "extended_virial" )
171+
172+ fparam_zero = torch .zeros (
173+ nframes ,
174+ numb_fparam ,
175+ dtype = torch .float64 ,
176+ device = self .device ,
177+ )
178+ aparam_zero = torch .zeros (
179+ nframes ,
180+ nloc ,
181+ numb_aparam ,
182+ dtype = torch .float64 ,
183+ device = self .device ,
184+ )
185+
186+ # --- eager reference with zero params ---
187+ ret_eager_zero = md ._forward_lower (
151188 ext_coord .requires_grad_ (True ),
152189 ext_atype ,
153190 nlist_t ,
154191 mapping_t ,
192+ fparam = fparam_zero ,
193+ aparam = aparam_zero ,
155194 do_atomic_virial = True ,
156195 )
157- self .assertIn ("energy" , ret0 )
158- self .assertIn ("extended_force" , ret0 )
159- self .assertIn ("virial" , ret0 )
160- self .assertIn ("extended_virial" , ret0 )
196+ for key in output_keys :
197+ self .assertIn (key , ret_eager_zero )
161198
162- # forward_lower returns a traced module
199+ # --- trace and export ---
163200 traced = md .forward_lower (
164201 ext_coord ,
165202 ext_atype ,
166203 nlist_t ,
167204 mapping_t ,
205+ fparam = fparam_zero ,
206+ aparam = aparam_zero ,
168207 do_atomic_virial = True ,
169208 )
170209 self .assertIsInstance (traced , torch .nn .Module )
171210
172- # The traced module should be directly exportable
173211 exported = torch .export .export (
174212 traced ,
175- (ext_coord , ext_atype , nlist_t , mapping_t ),
213+ (ext_coord , ext_atype , nlist_t , mapping_t , fparam_zero , aparam_zero ),
176214 strict = False ,
177215 )
178216 self .assertIsNotNone (exported )
179217
180- # Verify exported model produces same output
181- ret1 = exported .module ()(ext_coord , ext_atype , nlist_t , mapping_t )
182- np .testing .assert_allclose (
183- ret0 ["energy" ].detach ().cpu ().numpy (),
184- ret1 ["energy" ].detach ().cpu ().numpy (),
185- rtol = 1e-10 ,
186- atol = 1e-10 ,
218+ # --- verify traced/exported match eager (zero params) ---
219+ ret_traced_zero = traced (
220+ ext_coord ,
221+ ext_atype ,
222+ nlist_t ,
223+ mapping_t ,
224+ fparam_zero ,
225+ aparam_zero ,
187226 )
188- np .testing .assert_allclose (
189- ret0 ["extended_force" ].detach ().cpu ().numpy (),
190- ret1 ["extended_force" ].detach ().cpu ().numpy (),
191- rtol = 1e-10 ,
192- atol = 1e-10 ,
227+ ret_exported_zero = exported .module ()(
228+ ext_coord ,
229+ ext_atype ,
230+ nlist_t ,
231+ mapping_t ,
232+ fparam_zero ,
233+ aparam_zero ,
193234 )
194- np .testing .assert_allclose (
195- ret0 ["virial" ].detach ().cpu ().numpy (),
196- ret1 ["virial" ].detach ().cpu ().numpy (),
197- rtol = 1e-10 ,
198- atol = 1e-10 ,
235+ for key in output_keys :
236+ np .testing .assert_allclose (
237+ ret_eager_zero [key ].detach ().cpu ().numpy (),
238+ ret_traced_zero [key ].detach ().cpu ().numpy (),
239+ rtol = 1e-10 ,
240+ atol = 1e-10 ,
241+ err_msg = f"traced vs eager (zero params): { key } " ,
242+ )
243+ np .testing .assert_allclose (
244+ ret_eager_zero [key ].detach ().cpu ().numpy (),
245+ ret_exported_zero [key ].detach ().cpu ().numpy (),
246+ rtol = 1e-10 ,
247+ atol = 1e-10 ,
248+ err_msg = f"exported vs eager (zero params): { key } " ,
249+ )
250+
251+ # --- verify traced/exported match eager (non-zero params) ---
252+ fparam_nz = torch .ones (
253+ nframes ,
254+ numb_fparam ,
255+ dtype = torch .float64 ,
256+ device = self .device ,
199257 )
200- np .testing .assert_allclose (
201- ret0 ["extended_virial" ].detach ().cpu ().numpy (),
202- ret1 ["extended_virial" ].detach ().cpu ().numpy (),
203- rtol = 1e-10 ,
204- atol = 1e-10 ,
258+ aparam_nz = torch .ones (
259+ nframes ,
260+ nloc ,
261+ numb_aparam ,
262+ dtype = torch .float64 ,
263+ device = self .device ,
264+ )
265+ ret_eager_nz = md ._forward_lower (
266+ ext_coord .requires_grad_ (True ),
267+ ext_atype ,
268+ nlist_t ,
269+ mapping_t ,
270+ fparam = fparam_nz ,
271+ aparam = aparam_nz ,
272+ do_atomic_virial = True ,
273+ )
274+ ret_traced_nz = traced (
275+ ext_coord ,
276+ ext_atype ,
277+ nlist_t ,
278+ mapping_t ,
279+ fparam_nz ,
280+ aparam_nz ,
281+ )
282+ ret_exported_nz = exported .module ()(
283+ ext_coord ,
284+ ext_atype ,
285+ nlist_t ,
286+ mapping_t ,
287+ fparam_nz ,
288+ aparam_nz ,
289+ )
290+ for key in output_keys :
291+ np .testing .assert_allclose (
292+ ret_eager_nz [key ].detach ().cpu ().numpy (),
293+ ret_traced_nz [key ].detach ().cpu ().numpy (),
294+ rtol = 1e-10 ,
295+ atol = 1e-10 ,
296+ err_msg = f"traced vs eager (non-zero params): { key } " ,
297+ )
298+ np .testing .assert_allclose (
299+ ret_eager_nz [key ].detach ().cpu ().numpy (),
300+ ret_exported_nz [key ].detach ().cpu ().numpy (),
301+ rtol = 1e-10 ,
302+ atol = 1e-10 ,
303+ err_msg = f"exported vs eager (non-zero params): { key } " ,
304+ )
305+
306+ # --- verify fparam is dynamic (changing it changes the output) ---
307+ self .assertFalse (
308+ np .allclose (
309+ ret_traced_zero ["energy" ].detach ().cpu ().numpy (),
310+ ret_traced_nz ["energy" ].detach ().cpu ().numpy (),
311+ ),
312+ "Changing fparam did not change output — "
313+ "fparam may be baked in as a constant" ,
314+ )
315+
316+ # --- verify aparam is dynamic (changing it changes the output) ---
317+ ret_traced_ap = traced (
318+ ext_coord ,
319+ ext_atype ,
320+ nlist_t ,
321+ mapping_t ,
322+ fparam_zero ,
323+ aparam_nz ,
324+ )
325+ self .assertFalse (
326+ np .allclose (
327+ ret_traced_zero ["energy" ].detach ().cpu ().numpy (),
328+ ret_traced_ap ["energy" ].detach ().cpu ().numpy (),
329+ ),
330+ "Changing aparam did not change output — "
331+ "aparam may be baked in as a constant" ,
205332 )
206333
207334 def test_dp_consistency (self ) -> None :
0 commit comments