2424from accelerate .utils .modeling import _get_proper_dtype , compute_module_sizes , dtype_byte_size
2525
2626from diffusers .utils import SAFE_WEIGHTS_INDEX_NAME , _add_variant , logging
27- from diffusers .utils .testing_utils import require_accelerator , require_torch_multi_accelerator
2827
29- from ...testing_utils import assert_tensors_close , torch_device
28+ from ...testing_utils import (
29+ assert_tensors_close ,
30+ require_accelerator ,
31+ require_torch_multi_accelerator ,
32+ torch_device ,
33+ )
3034
3135
3236def named_persistent_module_tensors (
@@ -258,7 +262,39 @@ def get_dummy_inputs(self) -> Dict[str, Any]:
258262 raise NotImplementedError ("Subclasses must implement `get_dummy_inputs()`." )
259263
260264
261- class ModelTesterMixin :
265+ class BaseModelOutputMixin :
266+ """Provides the class-scoped `base_model_output` fixture shared across tester mixins.
267+
268+ Kept separate from `BaseModelTesterConfig` — which only declares the testing contract and performs no
269+ computation — so any mixin that needs the cached reference output (`ModelTesterMixin`, the memory
270+ offload mixins, ...) can inherit it without duplicating the build-and-forward.
271+ """
272+
273+ @pytest .fixture (scope = "class" )
274+ def base_model_output (self ):
275+ """Class-scoped reference forward output, built once and reused across the class.
276+
277+ Building the model and running its forward pass is fully deterministic (`torch.manual_seed(0)`
278+ plus the deterministic `get_dummy_inputs` contract), so the reference ("base") output is
279+ identical for every test in the class. The save/load, parallelism, and memory-offload tests
280+ compare a reloaded/offloaded model against this output; computing it a single time here — instead
281+ of rebuilding the model and re-running the forward in each test — removes that redundant work and
282+ speeds up the suite.
283+
284+ The hardware-gated tests that consume this fixture use `pytest.mark.skipif` (via the `require_*`
285+ decorators), which pytest evaluates before fixture setup, so skipping on a machine without the
286+ required accelerators never triggers this forward.
287+
288+ Tests that still need a live model (e.g. to save or offload it) build their own with the same
289+ seed, so the reloaded model's weights match this cached output.
290+ """
291+ torch .manual_seed (0 )
292+ model = self .model_class (** self .get_init_dict ()).eval ().to (torch_device )
293+ with torch .no_grad ():
294+ return model (** self .get_dummy_inputs (), return_dict = False )[0 ]
295+
296+
297+ class ModelTesterMixin (BaseModelOutputMixin ):
262298 """
263299 Base mixin class for model testing with common test methods.
264300
@@ -279,7 +315,7 @@ class TestMyModel(MyModelTestConfig, ModelTesterMixin):
279315 """
280316
281317 @torch .no_grad ()
282- def test_from_save_pretrained (self , tmp_path , atol = 5e-5 , rtol = 5e-5 ):
318+ def test_from_save_pretrained (self , base_model_output , tmp_path , atol = 5e-5 , rtol = 5e-5 ):
283319 torch .manual_seed (0 )
284320 model = self .model_class (** self .get_init_dict ())
285321 model .to (torch_device )
@@ -296,13 +332,15 @@ def test_from_save_pretrained(self, tmp_path, atol=5e-5, rtol=5e-5):
296332 f"Parameter shape mismatch for { param_name } . Original: { param_1 .shape } , loaded: { param_2 .shape } "
297333 )
298334
299- image = model (** self .get_dummy_inputs (), return_dict = False )[0 ]
300335 new_image = new_model (** self .get_dummy_inputs (), return_dict = False )[0 ]
301336
302- assert_tensors_close (image , new_image , atol = atol , rtol = rtol , msg = "Models give different forward passes." )
337+ assert_tensors_close (
338+ base_model_output , new_image , atol = atol , rtol = rtol , msg = "Models give different forward passes."
339+ )
303340
304341 @torch .no_grad ()
305- def test_from_save_pretrained_variant (self , tmp_path , atol = 5e-5 , rtol = 0 ):
342+ def test_from_save_pretrained_variant (self , base_model_output , tmp_path , atol = 5e-5 , rtol = 0 ):
343+ torch .manual_seed (0 )
306344 model = self .model_class (** self .get_init_dict ())
307345 model .to (torch_device )
308346 model .eval ()
@@ -317,10 +355,11 @@ def test_from_save_pretrained_variant(self, tmp_path, atol=5e-5, rtol=0):
317355
318356 new_model .to (torch_device )
319357
320- image = model (** self .get_dummy_inputs (), return_dict = False )[0 ]
321358 new_image = new_model (** self .get_dummy_inputs (), return_dict = False )[0 ]
322359
323- assert_tensors_close (image , new_image , atol = atol , rtol = rtol , msg = "Models give different forward passes." )
360+ assert_tensors_close (
361+ base_model_output , new_image , atol = atol , rtol = rtol , msg = "Models give different forward passes."
362+ )
324363
325364 @pytest .mark .parametrize ("dtype" , [torch .float32 , torch .float16 , torch .bfloat16 ], ids = ["fp32" , "fp16" , "bf16" ])
326365 def test_from_save_pretrained_dtype (self , tmp_path , dtype ):
@@ -360,13 +399,8 @@ def test_determinism(self, atol=1e-5, rtol=0):
360399 )
361400
362401 @torch .no_grad ()
363- def test_output (self , expected_output_shape = None ):
364- model = self .model_class (** self .get_init_dict ())
365- model .to (torch_device )
366- model .eval ()
367-
368- inputs_dict = self .get_dummy_inputs ()
369- output = model (** inputs_dict , return_dict = False )[0 ]
402+ def test_output (self , base_model_output , expected_output_shape = None ):
403+ output = base_model_output
370404
371405 assert output is not None , "Model output is None"
372406 assert output [0 ].shape == expected_output_shape or self .output_shape , (
@@ -509,14 +543,12 @@ def test_from_save_pretrained_dtype_inference(self, tmp_path, dtype, atol=1e-4,
509543
510544 @require_accelerator
511545 @torch .no_grad ()
512- def test_sharded_checkpoints (self , tmp_path , atol = 1e-5 , rtol = 0 ):
546+ def test_sharded_checkpoints (self , base_model_output , tmp_path , atol = 1e-5 , rtol = 0 ):
513547 torch .manual_seed (0 )
514548 config = self .get_init_dict ()
515549 model = self .model_class (** config ).eval ()
516550 model = model .to (torch_device )
517551
518- base_output = model (** self .get_dummy_inputs (), return_dict = False )[0 ]
519-
520552 model_size = compute_module_persistent_sizes (model )["" ]
521553 max_shard_size = int ((model_size * 0.75 ) / (2 ** 10 )) # Convert to KB as these test models are small
522554
@@ -537,19 +569,17 @@ def test_sharded_checkpoints(self, tmp_path, atol=1e-5, rtol=0):
537569 new_output = new_model (** self .get_dummy_inputs (), return_dict = False )[0 ]
538570
539571 assert_tensors_close (
540- base_output , new_output , atol = atol , rtol = rtol , msg = "Output should match after sharded save/load"
572+ base_model_output , new_output , atol = atol , rtol = rtol , msg = "Output should match after sharded save/load"
541573 )
542574
543575 @require_accelerator
544576 @torch .no_grad ()
545- def test_sharded_checkpoints_with_variant (self , tmp_path , atol = 1e-5 , rtol = 0 ):
577+ def test_sharded_checkpoints_with_variant (self , base_model_output , tmp_path , atol = 1e-5 , rtol = 0 ):
546578 torch .manual_seed (0 )
547579 config = self .get_init_dict ()
548580 model = self .model_class (** config ).eval ()
549581 model = model .to (torch_device )
550582
551- base_output = model (** self .get_dummy_inputs (), return_dict = False )[0 ]
552-
553583 model_size = compute_module_persistent_sizes (model )["" ]
554584 max_shard_size = int ((model_size * 0.75 ) / (2 ** 10 )) # Convert to KB as these test models are small
555585 variant = "fp16"
@@ -575,20 +605,22 @@ def test_sharded_checkpoints_with_variant(self, tmp_path, atol=1e-5, rtol=0):
575605 new_output = new_model (** self .get_dummy_inputs (), return_dict = False )[0 ]
576606
577607 assert_tensors_close (
578- base_output , new_output , atol = atol , rtol = rtol , msg = "Output should match after variant sharded save/load"
608+ base_model_output ,
609+ new_output ,
610+ atol = atol ,
611+ rtol = rtol ,
612+ msg = "Output should match after variant sharded save/load" ,
579613 )
580614
581615 @torch .no_grad ()
582- def test_sharded_checkpoints_with_parallel_loading (self , tmp_path , atol = 1e-5 , rtol = 0 ):
616+ def test_sharded_checkpoints_with_parallel_loading (self , base_model_output , tmp_path , atol = 1e-5 , rtol = 0 ):
583617 from diffusers .utils import constants
584618
585619 torch .manual_seed (0 )
586620 config = self .get_init_dict ()
587621 model = self .model_class (** config ).eval ()
588622 model = model .to (torch_device )
589623
590- base_output = model (** self .get_dummy_inputs (), return_dict = False )[0 ]
591-
592624 model_size = compute_module_persistent_sizes (model )["" ]
593625 max_shard_size = int ((model_size * 0.75 ) / (2 ** 10 )) # Convert to KB as these test models are small
594626
@@ -624,7 +656,11 @@ def test_sharded_checkpoints_with_parallel_loading(self, tmp_path, atol=1e-5, rt
624656 output_parallel = model_parallel (** self .get_dummy_inputs (), return_dict = False )[0 ]
625657
626658 assert_tensors_close (
627- base_output , output_parallel , atol = atol , rtol = rtol , msg = "Output should match with parallel loading"
659+ base_model_output ,
660+ output_parallel ,
661+ atol = atol ,
662+ rtol = rtol ,
663+ msg = "Output should match with parallel loading" ,
628664 )
629665
630666 finally :
@@ -635,19 +671,17 @@ def test_sharded_checkpoints_with_parallel_loading(self, tmp_path, atol=1e-5, rt
635671
636672 @require_torch_multi_accelerator
637673 @torch .no_grad ()
638- def test_model_parallelism (self , tmp_path , atol = 1e-5 , rtol = 0 ):
674+ def test_model_parallelism (self , base_model_output , tmp_path , atol = 1e-5 , rtol = 0 ):
639675 if self .model_class ._no_split_modules is None :
640676 pytest .skip ("Test not supported for this model as `_no_split_modules` is not set." )
641677
678+ torch .manual_seed (0 )
642679 config = self .get_init_dict ()
643680 inputs_dict = self .get_dummy_inputs ()
644681 model = self .model_class (** config ).eval ()
645682
646683 model = model .to (torch_device )
647684
648- torch .manual_seed (0 )
649- base_output = model (** inputs_dict , return_dict = False )[0 ]
650-
651685 model_size = compute_module_sizes (model )["" ]
652686 max_gpu_sizes = [int (p * model_size ) for p in self .model_split_percents ]
653687
@@ -665,5 +699,5 @@ def test_model_parallelism(self, tmp_path, atol=1e-5, rtol=0):
665699 new_output = new_model (** inputs_dict , return_dict = False )[0 ]
666700
667701 assert_tensors_close (
668- base_output , new_output , atol = atol , rtol = rtol , msg = "Output should match with model parallelism"
702+ base_model_output , new_output , atol = atol , rtol = rtol , msg = "Output should match with model parallelism"
669703 )
0 commit comments