Skip to content

Commit 9d52996

Browse files
sayakpauldg845
andauthored
[tests] implement base model output caching in model-level tests (huggingface#14059)
* implement base model output caching in model-level tests * single quotes * memory --------- Co-authored-by: dg845 <58458699+dg845@users.noreply.github.com>
1 parent d8d3f90 commit 9d52996

5 files changed

Lines changed: 89 additions & 65 deletions

File tree

tests/models/testing_utils/common.py

Lines changed: 67 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,13 @@
2424
from accelerate.utils.modeling import _get_proper_dtype, compute_module_sizes, dtype_byte_size
2525

2626
from 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

3236
def 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
)

tests/models/testing_utils/memory.py

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
require_accelerator,
3838
torch_device,
3939
)
40-
from .common import cast_inputs_to_dtype, check_device_map_is_respected
40+
from .common import BaseModelOutputMixin, cast_inputs_to_dtype, check_device_map_is_respected
4141

4242

4343
def require_offload_support(func):
@@ -69,7 +69,7 @@ def wrapper(self, *args, **kwargs):
6969

7070

7171
@is_cpu_offload
72-
class CPUOffloadTesterMixin:
72+
class CPUOffloadTesterMixin(BaseModelOutputMixin):
7373
"""
7474
Mixin class for testing CPU offloading functionality.
7575
@@ -94,16 +94,14 @@ def model_split_percents(self) -> list[float]:
9494

9595
@require_offload_support
9696
@torch.no_grad()
97-
def test_cpu_offload(self, tmp_path, atol=1e-5, rtol=0):
97+
def test_cpu_offload(self, base_model_output, tmp_path, atol=1e-5, rtol=0):
98+
torch.manual_seed(0)
9899
config = self.get_init_dict()
99100
inputs_dict = self.get_dummy_inputs()
100101
model = self.model_class(**config).eval()
101102

102103
model = model.to(torch_device)
103104

104-
torch.manual_seed(0)
105-
base_output = model(**inputs_dict)
106-
107105
model_size = compute_module_sizes(model)[""]
108106
# We test several splits of sizes to make sure it works
109107
max_gpu_sizes = [int(p * model_size) for p in self.model_split_percents]
@@ -120,21 +118,19 @@ def test_cpu_offload(self, tmp_path, atol=1e-5, rtol=0):
120118
new_output = new_model(**inputs_dict)
121119

122120
assert_tensors_close(
123-
base_output[0], new_output[0], atol=atol, rtol=rtol, msg="Output should match with CPU offloading"
121+
base_model_output, new_output[0], atol=atol, rtol=rtol, msg="Output should match with CPU offloading"
124122
)
125123

126124
@require_offload_support
127125
@torch.no_grad()
128-
def test_disk_offload_without_safetensors(self, tmp_path, atol=1e-5, rtol=0):
126+
def test_disk_offload_without_safetensors(self, base_model_output, tmp_path, atol=1e-5, rtol=0):
127+
torch.manual_seed(0)
129128
config = self.get_init_dict()
130129
inputs_dict = self.get_dummy_inputs()
131130
model = self.model_class(**config).eval()
132131

133132
model = model.to(torch_device)
134133

135-
torch.manual_seed(0)
136-
base_output = model(**inputs_dict)
137-
138134
model_size = compute_module_sizes(model)[""]
139135
max_size = int(self.model_split_percents[0] * model_size)
140136
# Force disk offload by setting very small CPU memory
@@ -154,21 +150,19 @@ def test_disk_offload_without_safetensors(self, tmp_path, atol=1e-5, rtol=0):
154150
new_output = new_model(**inputs_dict)
155151

156152
assert_tensors_close(
157-
base_output[0], new_output[0], atol=atol, rtol=rtol, msg="Output should match with disk offloading"
153+
base_model_output, new_output[0], atol=atol, rtol=rtol, msg="Output should match with disk offloading"
158154
)
159155

160156
@require_offload_support
161157
@torch.no_grad()
162-
def test_disk_offload_with_safetensors(self, tmp_path, atol=1e-5, rtol=0):
158+
def test_disk_offload_with_safetensors(self, base_model_output, tmp_path, atol=1e-5, rtol=0):
159+
torch.manual_seed(0)
163160
config = self.get_init_dict()
164161
inputs_dict = self.get_dummy_inputs()
165162
model = self.model_class(**config).eval()
166163

167164
model = model.to(torch_device)
168165

169-
torch.manual_seed(0)
170-
base_output = model(**inputs_dict)
171-
172166
model_size = compute_module_sizes(model)[""]
173167
model.cpu().save_pretrained(str(tmp_path))
174168

@@ -183,7 +177,7 @@ def test_disk_offload_with_safetensors(self, tmp_path, atol=1e-5, rtol=0):
183177
new_output = new_model(**inputs_dict)
184178

185179
assert_tensors_close(
186-
base_output[0],
180+
base_model_output,
187181
new_output[0],
188182
atol=atol,
189183
rtol=rtol,
@@ -192,7 +186,7 @@ def test_disk_offload_with_safetensors(self, tmp_path, atol=1e-5, rtol=0):
192186

193187

194188
@is_group_offload
195-
class GroupOffloadTesterMixin:
189+
class GroupOffloadTesterMixin(BaseModelOutputMixin):
196190
"""
197191
Mixin class for testing group offloading functionality.
198192
@@ -209,10 +203,9 @@ class GroupOffloadTesterMixin:
209203

210204
@require_group_offload_support
211205
@pytest.mark.parametrize("record_stream", [False, True])
212-
def test_group_offloading(self, record_stream, atol=1e-5, rtol=0):
206+
def test_group_offloading(self, base_model_output, record_stream, atol=1e-5, rtol=0):
213207
init_dict = self.get_init_dict()
214208
inputs_dict = self.get_dummy_inputs()
215-
torch.manual_seed(0)
216209

217210
@torch.no_grad()
218211
def run_forward(model):
@@ -224,10 +217,7 @@ def run_forward(model):
224217
model.eval()
225218
return model(**inputs_dict)[0]
226219

227-
model = self.model_class(**init_dict)
228-
229-
model.to(torch_device)
230-
output_without_group_offloading = run_forward(model)
220+
output_without_group_offloading = base_model_output
231221

232222
torch.manual_seed(0)
233223
model = self.model_class(**init_dict)

tests/models/transformers/test_models_transformer_hunyuan_dit.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,9 @@ def get_dummy_inputs(self, batch_size: int = 2) -> dict[str, torch.Tensor]:
120120

121121

122122
class TestHunyuanDiT(HunyuanDiTTesterConfig, ModelTesterMixin):
123-
def test_output(self):
123+
def test_output(self, base_model_output):
124124
batch_size = self.get_dummy_inputs()[self.main_input_name].shape[0]
125-
super().test_output(expected_output_shape=(batch_size,) + self.output_shape)
125+
super().test_output(base_model_output, expected_output_shape=(batch_size,) + self.output_shape)
126126

127127

128128
class TestHunyuanDiTTraining(HunyuanDiTTesterConfig, TrainingTesterMixin):

tests/models/transformers/test_models_transformer_hunyuan_video.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,8 @@ def get_dummy_inputs(self, batch_size: int = 1) -> dict[str, torch.Tensor]:
223223

224224

225225
class TestHunyuanVideoI2VTransformer(HunyuanVideoI2VTransformerTesterConfig, ModelTesterMixin):
226-
def test_output(self):
227-
super().test_output(expected_output_shape=(1, *self.output_shape))
226+
def test_output(self, base_model_output):
227+
super().test_output(base_model_output, expected_output_shape=(1, *self.output_shape))
228228

229229

230230
# ======================== HunyuanVideo Token Replace Image-to-Video ========================
@@ -299,5 +299,5 @@ def get_dummy_inputs(self, batch_size: int = 1) -> dict[str, torch.Tensor]:
299299

300300

301301
class TestHunyuanVideoTokenReplaceTransformer(HunyuanVideoTokenReplaceTransformerTesterConfig, ModelTesterMixin):
302-
def test_output(self):
303-
super().test_output(expected_output_shape=(1, *self.output_shape))
302+
def test_output(self, base_model_output):
303+
super().test_output(base_model_output, expected_output_shape=(1, *self.output_shape))

tests/models/transformers/test_models_transformer_wan_animate.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,11 @@ def get_dummy_inputs(self) -> dict[str, torch.Tensor]:
146146
class TestWanAnimateTransformer3D(WanAnimateTransformer3DTesterConfig, ModelTesterMixin):
147147
"""Core model tests for Wan Animate Transformer 3D."""
148148

149-
def test_output(self):
149+
def test_output(self, base_model_output):
150150
# Override test_output because the transformer output is expected to have less channels
151151
# than the main transformer input.
152152
expected_output_shape = (1, 4, 21, 16, 16)
153-
super().test_output(expected_output_shape=expected_output_shape)
153+
super().test_output(base_model_output, expected_output_shape=expected_output_shape)
154154

155155
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16], ids=["fp16", "bf16"])
156156
def test_from_save_pretrained_dtype_inference(self, tmp_path, dtype):

0 commit comments

Comments
 (0)