Skip to content

Commit f2d2bd1

Browse files
author
sayak@huggingface.co
committed
refactor pipeline-level quantization tests
1 parent ceaa6b3 commit f2d2bd1

19 files changed

Lines changed: 1920 additions & 3783 deletions

.github/workflows/nightly_tests.yml

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -343,19 +343,19 @@ jobs:
343343
matrix:
344344
config:
345345
- backend: "bitsandbytes"
346-
test_location: "bnb"
346+
marker: "bitsandbytes"
347347
additional_deps: ["peft"]
348348
- backend: "gguf"
349-
test_location: "gguf"
349+
marker: "gguf"
350350
additional_deps: ["peft", "kernels"]
351351
- backend: "torchao"
352-
test_location: "torchao"
353-
additional_deps: []
352+
marker: "torchao"
353+
additional_deps: ["mslk"]
354354
- backend: "optimum_quanto"
355-
test_location: "quanto"
355+
marker: "quanto"
356356
additional_deps: []
357357
- backend: "nvidia_modelopt"
358-
test_location: "modelopt"
358+
marker: "modelopt"
359359
additional_deps: []
360360
runs-on:
361361
group: aws-g6e-xlarge-plus
@@ -390,9 +390,12 @@ jobs:
390390
BIG_GPU_MEMORY: 40
391391
run: |
392392
pytest -n 1 --max-worker-restart=0 --dist=loadfile \
393+
-m "${{ matrix.config.marker }}" \
393394
--make-reports=tests_${{ matrix.config.backend }}_torch_cuda \
394395
--report-log=tests_${{ matrix.config.backend }}_torch_cuda.log \
395-
tests/quantization/${{ matrix.config.test_location }}
396+
tests/models \
397+
tests/quantization \
398+
tests/pipelines/testing_utils/quantization.py
396399
- name: Failure short reports
397400
if: ${{ failure() }}
398401
run: |
@@ -440,9 +443,10 @@ jobs:
440443
BIG_GPU_MEMORY: 40
441444
run: |
442445
pytest -n 1 --max-worker-restart=0 --dist=loadfile \
446+
-k "TestPipelineQuantization" \
443447
--make-reports=tests_pipeline_level_quant_torch_cuda \
444448
--report-log=tests_pipeline_level_quant_torch_cuda.log \
445-
tests/quantization/test_pipeline_level_quantization.py
449+
tests/pipelines/testing_utils/quantization.py
446450
- name: Failure short reports
447451
if: ${{ failure() }}
448452
run: |

tests/models/testing_utils/quantization.py

Lines changed: 147 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import gc
1717

1818
import pytest
19+
import safetensors.torch
1920
import torch
2021

2122
from diffusers import (
@@ -423,6 +424,14 @@ def _test_quantization_device_map(self, config_kwargs):
423424
assert hasattr(model, "hf_device_map"), "Model should have hf_device_map attribute"
424425
assert model.hf_device_map is not None, "hf_device_map should not be None"
425426

427+
map_devices = {torch.device(d).type for d in model.hf_device_map.values()}
428+
for name, buffer in model.named_buffers():
429+
assert buffer.device.type != "meta", f"Buffer {name} was left on the meta device"
430+
if len(map_devices) == 1:
431+
assert buffer.device.type == next(iter(map_devices)), (
432+
f"Expected device {next(iter(map_devices))} for buffer {name}, got {buffer.device}"
433+
)
434+
426435
inputs = self.get_dummy_inputs()
427436
output = model(**inputs, return_dict=False)[0]
428437
assert output is not None, "Model output is None"
@@ -625,15 +634,32 @@ def test_bnb_quantization_memory_footprint(self, config_name):
625634
def test_bnb_quantization_inference(self, config_name):
626635
self._test_quantization_inference(BitsAndBytesConfigMixin.BNB_CONFIGS[config_name])
627636

628-
@pytest.mark.parametrize("config_name", ["4bit_nf4"], ids=["4bit_nf4"])
637+
@pytest.mark.parametrize("config_name", ["4bit_nf4", "8bit"], ids=["4bit_nf4", "8bit"])
629638
def test_bnb_quantization_dtype_assignment(self, config_name):
630639
self._test_quantization_dtype_assignment(BitsAndBytesConfigMixin.BNB_CONFIGS[config_name])
631640

641+
def test_bnb_device_assignment(self):
642+
"""Test that a 4-bit model moves between CPU and accelerator without changing its memory footprint."""
643+
model = self._create_quantized_model(BitsAndBytesConfigMixin.BNB_CONFIGS["4bit_nf4"])
644+
mem_before = model.get_memory_footprint()
645+
646+
model.to("cpu")
647+
assert model.device.type == "cpu"
648+
assert model.get_memory_footprint() == pytest.approx(mem_before)
649+
650+
model.to(torch_device)
651+
assert model.device.type == torch.device(torch_device).type
652+
assert model.get_memory_footprint() == pytest.approx(mem_before)
653+
632654
@pytest.mark.parametrize("config_name", ["4bit_nf4"], ids=["4bit_nf4"])
633655
def test_bnb_quantization_lora_inference(self, config_name):
634656
self._test_quantization_lora_inference(BitsAndBytesConfigMixin.BNB_CONFIGS[config_name])
635657

636-
@pytest.mark.parametrize("config_name", ["4bit_nf4"], ids=["4bit_nf4"])
658+
@pytest.mark.parametrize(
659+
"config_name",
660+
list(BitsAndBytesConfigMixin.BNB_CONFIGS.keys()),
661+
ids=list(BitsAndBytesConfigMixin.BNB_CONFIGS.keys()),
662+
)
637663
def test_bnb_quantization_serialization(self, config_name, tmp_path):
638664
self._test_quantization_serialization(BitsAndBytesConfigMixin.BNB_CONFIGS[config_name], tmp_path)
639665

@@ -660,15 +686,51 @@ def test_bnb_keep_modules_in_fp32(self):
660686
self._test_keep_modules_in_fp32(BitsAndBytesConfigMixin.BNB_CONFIGS["4bit_nf4"])
661687

662688
def test_bnb_modules_to_not_convert(self):
663-
"""Test that modules_to_not_convert parameter works correctly."""
689+
"""Test that `llm_int8_skip_modules` (the BitsAndBytesConfig module-exclusion option) works correctly."""
664690
modules_to_exclude = getattr(self, "modules_to_not_convert_for_test", None)
665691
if modules_to_exclude is None:
666692
pytest.skip("modules_to_not_convert_for_test not defined for this model")
667693

668-
self._test_quantization_modules_to_not_convert(
669-
BitsAndBytesConfigMixin.BNB_CONFIGS["4bit_nf4"], modules_to_exclude
694+
config_kwargs = {**BitsAndBytesConfigMixin.BNB_CONFIGS["8bit"], "llm_int8_skip_modules": modules_to_exclude}
695+
model = self._create_quantized_model(config_kwargs)
696+
697+
found_excluded = False
698+
for name, module in model.named_modules():
699+
if isinstance(module, torch.nn.Linear):
700+
if any(excluded in name for excluded in modules_to_exclude):
701+
found_excluded = True
702+
assert module.weight.dtype != torch.int8, f"Module {name} should not be quantized"
703+
else:
704+
assert isinstance(module, bnb.nn.Linear8bitLt), f"Module {name} should be quantized"
705+
assert module.weight.dtype == torch.int8, f"Module {name} weight should be int8"
706+
707+
assert found_excluded, f"No linear layers found in excluded modules: {modules_to_exclude}"
708+
709+
@pytest.mark.parametrize("config_name", ["8bit"], ids=["8bit"])
710+
def test_bnb_quantization_sharded_serialization(self, config_name, tmp_path):
711+
self._test_quantization_serialization(
712+
BitsAndBytesConfigMixin.BNB_CONFIGS[config_name], tmp_path, max_shard_size="16KB"
670713
)
671714

715+
def test_bnb_errors_loading_incorrect_state_dict(self, tmp_path):
716+
"""Test that loading a checkpoint with a corrupted quantized weight raises a helpful error."""
717+
model = self._create_quantized_model(BitsAndBytesConfigMixin.BNB_CONFIGS["4bit_nf4"])
718+
model.save_pretrained(str(tmp_path))
719+
del model
720+
gc.collect()
721+
backend_empty_cache(torch_device)
722+
723+
weights_file = tmp_path / "diffusion_pytorch_model.safetensors"
724+
state_dict = safetensors.torch.load_file(str(weights_file))
725+
key_to_target = next(k for k in state_dict if k.endswith(".weight") and state_dict[k].dtype == torch.uint8)
726+
corrupted_param = torch.randn(state_dict[key_to_target].shape[0] - 1, 1)
727+
state_dict[key_to_target] = bnb.nn.Params4bit(corrupted_param, requires_grad=False)
728+
safetensors.torch.save_file(state_dict, str(weights_file))
729+
730+
with pytest.raises(ValueError) as err_context:
731+
_ = self.model_class.from_pretrained(str(tmp_path))
732+
assert key_to_target in str(err_context.value)
733+
672734
@pytest.mark.parametrize("config_name", ["4bit_nf4", "8bit"], ids=["4bit_nf4", "8bit"])
673735
def test_bnb_device_map(self, config_name):
674736
"""Test that device_map='auto' works correctly with quantization."""
@@ -678,9 +740,10 @@ def test_bnb_dequantize(self):
678740
"""Test that dequantize() works correctly."""
679741
self._test_dequantize(BitsAndBytesConfigMixin.BNB_CONFIGS["4bit_nf4"])
680742

681-
def test_bnb_training(self):
743+
@pytest.mark.parametrize("config_name", ["4bit_nf4", "8bit"], ids=["4bit_nf4", "8bit"])
744+
def test_bnb_training(self, config_name):
682745
"""Test that quantized models can be used for training with adapters."""
683-
self._test_quantization_training(BitsAndBytesConfigMixin.BNB_CONFIGS["4bit_nf4"])
746+
self._test_quantization_training(BitsAndBytesConfigMixin.BNB_CONFIGS[config_name])
684747

685748
@pytest.mark.parametrize(
686749
"config_name",
@@ -812,6 +875,10 @@ def test_quanto_quantization_inference(self, weight_type_name):
812875
def test_quanto_quantized_layers(self, weight_type_name):
813876
self._test_quantized_layers(QuantoConfigMixin.QUANTO_WEIGHT_TYPES[weight_type_name])
814877

878+
@pytest.mark.parametrize("weight_type_name", ["int8"], ids=["int8"])
879+
def test_quanto_quantization_dtype_assignment(self, weight_type_name):
880+
self._test_quantization_dtype_assignment(QuantoConfigMixin.QUANTO_WEIGHT_TYPES[weight_type_name])
881+
815882
@pytest.mark.parametrize("weight_type_name", ["int8"], ids=["int8"])
816883
def test_quanto_quantization_lora_inference(self, weight_type_name):
817884
self._test_quantization_lora_inference(QuantoConfigMixin.QUANTO_WEIGHT_TYPES[weight_type_name])
@@ -1016,6 +1083,47 @@ def test_torchao_device_map(self):
10161083
"""Test that device_map='auto' works correctly with quantization."""
10171084
self._test_quantization_device_map(TorchAoConfigMixin.TORCHAO_QUANT_TYPES["int8wo"])
10181085

1086+
@torch.no_grad()
1087+
def test_torchao_cpu_disk_offload_device_map(self, tmp_path):
1088+
"""Test custom device maps with cpu/disk offload: offloaded modules stay unquantized, inference works."""
1089+
from torchao.utils import TorchAOBaseTensor
1090+
1091+
model = self._create_quantized_model(TorchAoConfigMixin.TORCHAO_QUANT_TYPES["int8wo"])
1092+
1093+
# Offload the first two linear-bearing top-level modules to cpu and disk, keep the rest on the accelerator.
1094+
device_map = {}
1095+
offload_targets = []
1096+
for name, child in model.named_children():
1097+
if len(offload_targets) < 2 and any(isinstance(m, torch.nn.Linear) for m in child.modules()):
1098+
device_map[name] = "disk" if offload_targets else "cpu"
1099+
offload_targets.append(name)
1100+
else:
1101+
device_map[name] = str(torch_device)
1102+
del model
1103+
gc.collect()
1104+
backend_empty_cache(torch_device)
1105+
if len(offload_targets) < 2:
1106+
pytest.skip("Model does not have enough linear-bearing top-level modules for offload testing")
1107+
1108+
model = self._create_quantized_model(
1109+
TorchAoConfigMixin.TORCHAO_QUANT_TYPES["int8wo"], device_map=device_map, offload_folder=str(tmp_path)
1110+
)
1111+
1112+
# Weights offloaded to cpu/disk are not quantized, only the weights on the accelerator are.
1113+
for name, module in model.named_modules():
1114+
if isinstance(module, torch.nn.Linear):
1115+
if name.split(".")[0] in offload_targets:
1116+
assert not isinstance(module.weight, TorchAOBaseTensor), (
1117+
f"Offloaded module {name} should not be quantized"
1118+
)
1119+
else:
1120+
assert isinstance(module.weight, TorchAOBaseTensor), f"Module {name} should be quantized"
1121+
1122+
inputs = self.get_dummy_inputs()
1123+
output = model(**inputs, return_dict=False)[0]
1124+
assert output is not None, "Model output is None"
1125+
assert not torch.isnan(output).any(), "Model output contains NaN"
1126+
10191127
@pytest.mark.parametrize(
10201128
"quant_type",
10211129
[
@@ -1103,6 +1211,38 @@ class GGUFTesterMixin(GGUFConfigMixin, QuantizationTesterMixin):
11031211
def test_gguf_quantization_inference(self):
11041212
self._test_quantization_inference({"compute_dtype": torch.bfloat16})
11051213

1214+
def test_gguf_quantized_layers(self):
1215+
compute_dtype = getattr(self, "torch_dtype", torch.bfloat16)
1216+
model = self._create_quantized_model({"compute_dtype": compute_dtype})
1217+
1218+
num_quantized = 0
1219+
for name, module in model.named_modules():
1220+
if isinstance(module, torch.nn.Linear) and hasattr(module.weight, "quant_type"):
1221+
self._verify_if_layer_quantized(name, module)
1222+
if module.bias is not None:
1223+
assert module.bias.dtype == compute_dtype, f"{name} bias should be {compute_dtype}"
1224+
num_quantized += 1
1225+
1226+
assert num_quantized > 0, "No quantized linear layers found in model"
1227+
1228+
@torch.no_grad()
1229+
def test_gguf_memory_usage(self):
1230+
expected_gb = getattr(self, "expected_memory_use_in_gb", None)
1231+
if expected_gb is None:
1232+
pytest.skip("expected_memory_use_in_gb not defined for this model")
1233+
1234+
compute_dtype = getattr(self, "torch_dtype", torch.bfloat16)
1235+
model = self._create_quantized_model({"compute_dtype": compute_dtype})
1236+
model.to(torch_device)
1237+
assert (model.get_memory_footprint() / 1024**3) < expected_gb
1238+
1239+
inputs = self.get_dummy_inputs()
1240+
backend_reset_peak_memory_stats(torch_device)
1241+
backend_empty_cache(torch_device)
1242+
model(**inputs)
1243+
max_memory = backend_max_memory_allocated(torch_device)
1244+
assert (max_memory / 1024**3) < expected_gb
1245+
11061246
def test_gguf_keep_modules_in_fp32(self):
11071247
if not hasattr(self.model_class, "_keep_in_fp32_modules"):
11081248
pytest.skip(f"{self.model_class.__name__} does not have _keep_in_fp32_modules")

tests/models/transformers/test_models_transformer_flux.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import pytest
2020
import torch
2121

22-
from diffusers import BitsAndBytesConfig, FluxTransformer2DModel
22+
from diffusers import BitsAndBytesConfig, FluxTransformer2DModel, GGUFQuantizationConfig
2323
from diffusers.models.embeddings import ImageProjection
2424
from diffusers.models.transformers.transformer_flux import FluxIPAdapterAttnProcessor
2525
from diffusers.utils.torch_utils import randn_tensor
@@ -356,6 +356,8 @@ def torch_dtype(self):
356356
class TestFluxTransformerBitsAndBytes(FluxTransformerTesterConfig, BitsAndBytesTesterMixin):
357357
"""BitsAndBytes quantization tests for Flux Transformer."""
358358

359+
modules_to_not_convert_for_test = ["proj_out"]
360+
359361
@property
360362
def torch_dtype(self):
361363
return torch.float16
@@ -384,6 +386,8 @@ def torch_dtype(self):
384386

385387

386388
class TestFluxTransformerGGUF(FluxTransformerTesterConfig, GGUFTesterMixin):
389+
expected_memory_use_in_gb = 5
390+
387391
@property
388392
def gguf_filename(self):
389393
return "https://huggingface.co/city96/FLUX.1-dev-gguf/blob/main/flux1-dev-Q2_K.gguf"
@@ -410,6 +414,17 @@ def get_dummy_inputs(self):
410414
"guidance": torch.tensor([3.5]).to(torch_device, self.torch_dtype),
411415
}
412416

417+
@torch.no_grad()
418+
def test_loading_gguf_diffusers_format(self):
419+
model = self.model_class.from_single_file(
420+
"https://huggingface.co/sayakpaul/flux-diffusers-gguf/blob/main/model-Q4_0.gguf",
421+
subfolder="transformer",
422+
quantization_config=GGUFQuantizationConfig(compute_dtype=self.torch_dtype),
423+
config="black-forest-labs/FLUX.1-dev",
424+
)
425+
model.to(torch_device)
426+
model(**self.get_dummy_inputs())
427+
413428

414429
class TestFluxTransformerQuantoCompile(FluxTransformerTesterConfig, QuantoCompileTesterMixin):
415430
"""Quanto + compile tests for Flux Transformer."""

tests/models/transformers/test_models_transformer_wan_animate.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ def get_dummy_inputs(self):
239239
class TestWanAnimateTransformer3DGGUF(WanAnimateTransformer3DTesterConfig, GGUFTesterMixin):
240240
"""GGUF quantization tests for Wan Animate Transformer 3D."""
241241

242+
expected_memory_use_in_gb = 9
243+
242244
@property
243245
def gguf_filename(self):
244246
return "https://huggingface.co/QuantStack/Wan2.2-Animate-14B-GGUF/blob/main/Wan2.2-Animate-14B-Q2_K.gguf"

tests/models/transformers/test_models_transformer_wan_vace.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ def get_dummy_inputs(self):
196196
class TestWanVACETransformer3DGGUF(WanVACETransformer3DTesterConfig, GGUFTesterMixin):
197197
"""GGUF quantization tests for Wan VACE Transformer 3D."""
198198

199+
expected_memory_use_in_gb = 9
200+
199201
@property
200202
def gguf_filename(self):
201203
return "https://huggingface.co/QuantStack/Wan2.1_14B_VACE-GGUF/blob/main/Wan2.1_14B_VACE-Q3_K_S.gguf"

0 commit comments

Comments
 (0)