1
0
mirror of https://github.com/huggingface/diffusers.git synced 2026-10-24 20:59:27 +03:00
Files
diffusers/tests/pipelines/testing_utils/quantization.py
Sayak Paul daad99a04a [tests] remove quanto tests. (#14254)
* remove quanto tests.

* deprecate quanto and remove tests
2026-08-27 13:59:40 +05:30

1691 lines
64 KiB
Python

# coding=utf-8
# Copyright 2026 The HuggingFace Team Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Pipeline-level quantization tests.
Model-level quantization tests live in `tests/models/testing_utils/quantization.py` and are wired
into the individual model test files. Backend-level tests (config validation, loading error paths,
kernels) live in `tests/quantization/`. This module only covers behavior that needs a pipeline.
The module name intentionally does not match pytest's `test_*.py` discovery pattern: these tests
only run when this file is passed to pytest explicitly, as the nightly quantization CI jobs do.
Every class is marked with the `quantization` marker plus its backend marker (`bitsandbytes`,
`torchao`, `gguf`, `modelopt`) so CI can select per-backend subsets with `pytest -m`.
"""
import gc
import json
import tempfile
import numpy as np
import pytest
import torch
from huggingface_hub import hf_hub_download
from parameterized import parameterized
from PIL import Image
from diffusers import (
AuraFlowPipeline,
AuraFlowTransformer2DModel,
AutoencoderKL,
BitsAndBytesConfig,
DiffusionPipeline,
FlowMatchEulerDiscreteScheduler,
FluxControlPipeline,
FluxPipeline,
FluxTransformer2DModel,
GGUFQuantizationConfig,
NVIDIAModelOptConfig,
SD3Transformer2DModel,
StableDiffusion3Pipeline,
TorchAoConfig,
)
from diffusers.quantizers import PipelineQuantizationConfig
from diffusers.utils import is_accelerate_version, load_image, logging
from ...testing_utils import (
CaptureLogger,
Expectations,
backend_empty_cache,
backend_reset_peak_memory_stats,
backend_synchronize,
enable_full_determinism,
is_bitsandbytes,
is_bitsandbytes_available,
is_gguf,
is_gguf_available,
is_modelopt,
is_quantization,
is_torchao,
is_torchao_available,
is_transformers_available,
nightly,
numpy_cosine_similarity_distance,
require_accelerate,
require_big_accelerator,
require_bitsandbytes_version_greater,
require_gguf_version_greater_or_equal,
require_modelopt_version_greater_or_equal,
require_peft_backend,
require_peft_version_greater,
require_torch,
require_torch_accelerator,
require_torch_version_greater,
require_torch_version_greater_equal,
require_torchao_version_greater_or_equal,
require_transformers_version_greater,
slow,
torch_device,
)
if is_transformers_available():
from transformers import AutoTokenizer, CLIPTextModel, CLIPTokenizer, T5EncoderModel
from transformers import BitsAndBytesConfig as TranBitsAndBytesConfig
else:
TranBitsAndBytesConfig = None
if is_bitsandbytes_available():
pass
if is_torchao_available():
from torchao.quantization import (
Float8WeightOnlyConfig,
Int4WeightOnlyConfig,
Int8DynamicActivationInt8WeightConfig,
Int8DynamicActivationIntxWeightConfig,
Int8Tensor,
Int8WeightOnlyConfig,
IntxWeightOnlyConfig,
)
from torchao.utils import TorchAOBaseTensor
if is_gguf_available():
pass
enable_full_determinism()
# ======================== Shared compile base ========================
@is_quantization
@require_torch_accelerator
@slow
class QuantCompileTests:
@property
def quantization_config(self):
raise NotImplementedError(
"This property should be implemented in the subclass to return the appropriate quantization config."
)
@pytest.fixture(autouse=True)
def _cleanup(self):
gc.collect()
backend_empty_cache(torch_device)
torch.compiler.reset()
yield
gc.collect()
backend_empty_cache(torch_device)
torch.compiler.reset()
def _init_pipeline(self, quantization_config, torch_dtype):
pipe = DiffusionPipeline.from_pretrained(
"stabilityai/stable-diffusion-3-medium-diffusers",
quantization_config=quantization_config,
torch_dtype=torch_dtype,
)
return pipe
def _test_torch_compile_with_cpu_offload(self, torch_dtype=torch.bfloat16):
pipe = self._init_pipeline(self.quantization_config, torch_dtype)
pipe.enable_model_cpu_offload()
# regional compilation is better for offloading.
# see: https://pytorch.org/blog/torch-compile-and-diffusers-a-hands-on-guide-to-peak-performance/
if getattr(pipe.transformer, "_repeated_blocks"):
pipe.transformer.compile_repeated_blocks(fullgraph=True)
else:
pipe.transformer.compile()
# small resolutions to ensure speedy execution.
pipe("a dog", num_inference_steps=2, max_sequence_length=16, height=256, width=256)
def test_torch_compile_with_cpu_offload(self):
self._test_torch_compile_with_cpu_offload()
# ======================== PipelineQuantizationConfig ========================
@is_quantization
@require_bitsandbytes_version_greater("0.43.2")
@require_torchao_version_greater_or_equal("0.16.0")
@require_accelerate
@require_torch
@require_torch_accelerator
@slow
class TestPipelineQuantization:
model_name = "hf-internal-testing/tiny-flux-pipe"
prompt = "a beautiful sunset amidst the mountains."
num_inference_steps = 10
seed = 0
def test_quant_config_set_correctly_through_kwargs(self):
components_to_quantize = ["transformer", "text_encoder_2"]
quant_config = PipelineQuantizationConfig(
quant_backend="bitsandbytes_4bit",
quant_kwargs={
"load_in_4bit": True,
"bnb_4bit_quant_type": "nf4",
"bnb_4bit_compute_dtype": torch.bfloat16,
},
components_to_quantize=components_to_quantize,
)
pipe = DiffusionPipeline.from_pretrained(
self.model_name,
quantization_config=quant_config,
torch_dtype=torch.bfloat16,
).to(torch_device)
for name, component in pipe.components.items():
if name in components_to_quantize:
assert getattr(component.config, "quantization_config", None) is not None
quantization_config = component.config.quantization_config
assert quantization_config.load_in_4bit
assert quantization_config.quant_method == "bitsandbytes"
_ = pipe(self.prompt, num_inference_steps=self.num_inference_steps)
def test_quant_config_set_correctly_through_granular(self):
quant_config = PipelineQuantizationConfig(
quant_mapping={
"transformer": TorchAoConfig(Int8WeightOnlyConfig(version=2)),
"text_encoder_2": TranBitsAndBytesConfig(load_in_4bit=True, compute_dtype=torch.bfloat16),
}
)
components_to_quantize = list(quant_config.quant_mapping.keys())
pipe = DiffusionPipeline.from_pretrained(
self.model_name,
quantization_config=quant_config,
torch_dtype=torch.bfloat16,
).to(torch_device)
for name, component in pipe.components.items():
if name in components_to_quantize:
assert getattr(component.config, "quantization_config", None) is not None
quantization_config = component.config.quantization_config
if name == "text_encoder_2":
assert quantization_config.load_in_4bit
assert quantization_config.quant_method == "bitsandbytes"
else:
assert quantization_config.quant_method == "torchao"
_ = pipe(self.prompt, num_inference_steps=self.num_inference_steps)
def test_raises_error_for_invalid_config(self):
with pytest.raises(ValueError) as err_context:
_ = PipelineQuantizationConfig(
quant_mapping={
"transformer": TorchAoConfig(Int8WeightOnlyConfig(version=2)),
"text_encoder_2": TranBitsAndBytesConfig(load_in_4bit=True, compute_dtype=torch.bfloat16),
},
quant_backend="bitsandbytes_4bit",
)
assert (
str(err_context.value) == "Both `quant_backend` and `quant_mapping` cannot be specified at the same time."
)
def test_validation_for_kwargs(self):
components_to_quantize = ["transformer", "text_encoder_2"]
with pytest.raises(ValueError) as err_context:
_ = PipelineQuantizationConfig(
quant_backend="torchao",
quant_kwargs={"quant_type": Int8WeightOnlyConfig(version=2)},
components_to_quantize=components_to_quantize,
)
assert "The signatures of the __init__ methods of the quantization config classes" in str(err_context.value)
def test_raises_error_for_wrong_config_class(self):
quant_config = {
"transformer": TorchAoConfig(Int8WeightOnlyConfig(version=2)),
"text_encoder_2": TranBitsAndBytesConfig(load_in_4bit=True, compute_dtype=torch.bfloat16),
}
with pytest.raises(ValueError) as err_context:
_ = DiffusionPipeline.from_pretrained(
self.model_name,
quantization_config=quant_config,
torch_dtype=torch.bfloat16,
)
assert str(err_context.value) == "`quantization_config` must be an instance of `PipelineQuantizationConfig`."
def test_validation_for_mapping(self):
with pytest.raises(ValueError) as err_context:
_ = PipelineQuantizationConfig(
quant_mapping={
"transformer": DiffusionPipeline(),
"text_encoder_2": TranBitsAndBytesConfig(load_in_4bit=True, compute_dtype=torch.bfloat16),
}
)
assert "Provided config for module_name=transformer could not be found" in str(err_context.value)
def test_saving_loading(self):
quant_config = PipelineQuantizationConfig(
quant_mapping={
"transformer": TorchAoConfig(Int8WeightOnlyConfig(version=2)),
"text_encoder_2": TranBitsAndBytesConfig(load_in_4bit=True, compute_dtype=torch.bfloat16),
}
)
components_to_quantize = list(quant_config.quant_mapping.keys())
pipe = DiffusionPipeline.from_pretrained(
self.model_name,
quantization_config=quant_config,
torch_dtype=torch.bfloat16,
).to(torch_device)
pipe_inputs = {"prompt": self.prompt, "num_inference_steps": self.num_inference_steps, "output_type": "latent"}
output_1 = pipe(**pipe_inputs, generator=torch.manual_seed(self.seed)).images
with tempfile.TemporaryDirectory() as tmpdir:
pipe.save_pretrained(tmpdir)
loaded_pipe = DiffusionPipeline.from_pretrained(tmpdir, torch_dtype=torch.bfloat16).to(torch_device)
for name, component in loaded_pipe.components.items():
if name in components_to_quantize:
assert getattr(component.config, "quantization_config", None) is not None
quantization_config = component.config.quantization_config
if name == "text_encoder_2":
assert quantization_config.load_in_4bit
assert quantization_config.quant_method == "bitsandbytes"
else:
assert quantization_config.quant_method == "torchao"
output_2 = loaded_pipe(**pipe_inputs, generator=torch.manual_seed(self.seed)).images
assert torch.allclose(output_1, output_2)
@parameterized.expand(["quant_kwargs", "quant_mapping"])
def test_warn_invalid_component(self, method):
invalid_component = "foo"
if method == "quant_kwargs":
components_to_quantize = ["transformer", invalid_component]
quant_config = PipelineQuantizationConfig(
quant_backend="bitsandbytes_8bit",
quant_kwargs={"load_in_8bit": True},
components_to_quantize=components_to_quantize,
)
else:
quant_config = PipelineQuantizationConfig(
quant_mapping={
"transformer": TorchAoConfig(Int8WeightOnlyConfig(version=2)),
invalid_component: TranBitsAndBytesConfig(load_in_8bit=True),
}
)
logger = logging.get_logger("diffusers.pipelines.pipeline_loading_utils")
logger.setLevel(logging.WARNING)
with CaptureLogger(logger) as cap_logger:
_ = DiffusionPipeline.from_pretrained(
self.model_name,
quantization_config=quant_config,
torch_dtype=torch.bfloat16,
)
assert invalid_component in cap_logger.out
@parameterized.expand(["quant_kwargs", "quant_mapping"])
def test_no_quantization_for_all_invalid_components(self, method):
invalid_component = "foo"
if method == "quant_kwargs":
components_to_quantize = [invalid_component]
quant_config = PipelineQuantizationConfig(
quant_backend="bitsandbytes_8bit",
quant_kwargs={"load_in_8bit": True},
components_to_quantize=components_to_quantize,
)
else:
quant_config = PipelineQuantizationConfig(
quant_mapping={invalid_component: TranBitsAndBytesConfig(load_in_8bit=True)}
)
pipe = DiffusionPipeline.from_pretrained(
self.model_name,
quantization_config=quant_config,
torch_dtype=torch.bfloat16,
)
for name, component in pipe.components.items():
if isinstance(component, torch.nn.Module):
assert not hasattr(component.config, "quantization_config")
@parameterized.expand(["quant_kwargs", "quant_mapping"])
def test_quant_config_repr(self, method):
component_name = "transformer"
if method == "quant_kwargs":
components_to_quantize = [component_name]
quant_config = PipelineQuantizationConfig(
quant_backend="bitsandbytes_8bit",
quant_kwargs={"load_in_8bit": True},
components_to_quantize=components_to_quantize,
)
else:
quant_config = PipelineQuantizationConfig(
quant_mapping={component_name: BitsAndBytesConfig(load_in_8bit=True)}
)
pipe = DiffusionPipeline.from_pretrained(
self.model_name,
quantization_config=quant_config,
torch_dtype=torch.bfloat16,
)
assert getattr(pipe, "quantization_config", None) is not None
retrieved_config = pipe.quantization_config
expected_config = """
transformer BitsAndBytesConfig {
"_load_in_4bit": false,
"_load_in_8bit": true,
"bnb_4bit_compute_dtype": "float32",
"bnb_4bit_quant_storage": "uint8",
"bnb_4bit_quant_type": "fp4",
"bnb_4bit_use_double_quant": false,
"llm_int8_enable_fp32_cpu_offload": false,
"llm_int8_has_fp16_weight": false,
"llm_int8_skip_modules": null,
"llm_int8_threshold": 6.0,
"load_in_4bit": false,
"load_in_8bit": true,
"quant_method": "bitsandbytes"
}
"""
expected_data = self._parse_config_string(expected_config)
actual_data = self._parse_config_string(str(retrieved_config))
assert actual_data == expected_data
def _parse_config_string(self, config_string: str) -> tuple[str, dict]:
first_brace = config_string.find("{")
if first_brace == -1:
raise ValueError("Could not find opening brace '{' in the string.")
json_part = config_string[first_brace:]
data = json.loads(json_part)
return data
def test_single_component_to_quantize(self):
component_to_quantize = "transformer"
quant_config = PipelineQuantizationConfig(
quant_backend="bitsandbytes_8bit",
quant_kwargs={"load_in_8bit": True},
components_to_quantize=component_to_quantize,
)
pipe = DiffusionPipeline.from_pretrained(
self.model_name,
quantization_config=quant_config,
torch_dtype=torch.bfloat16,
)
for name, component in pipe.components.items():
if name == component_to_quantize:
assert hasattr(component.config, "quantization_config")
# ======================== BitsAndBytes ========================
# Model-level BitsAndBytes tests live in `tests/models/testing_utils/quantization.py`
# (`BitsAndBytesTesterMixin` / `BitsAndBytesCompileTesterMixin`), wired into model test files via
# concrete classes (e.g. `TestFluxTransformerBitsAndBytes`). Only pipeline-level coverage remains here.
@is_quantization
@is_bitsandbytes
@require_bitsandbytes_version_greater("0.43.2")
@require_accelerate
@require_torch
@require_torch_accelerator
@slow
class Base4bitTests:
# We need to test on relatively large models (aka >1b parameters otherwise the quantiztion may not work as expected)
# Therefore here we use only SD3 to test our module
model_name = "stabilityai/stable-diffusion-3-medium-diffusers"
prompt = "a beautiful sunset amidst the mountains."
num_inference_steps = 10
seed = 0
@pytest.fixture(autouse=True, scope="class")
def _toggle_determinism(self):
was_enabled = torch.are_deterministic_algorithms_enabled()
if not was_enabled:
torch.use_deterministic_algorithms(True)
yield
if not was_enabled:
torch.use_deterministic_algorithms(False)
@require_transformers_version_greater("4.44.0")
class TestSlowBnb4Bit(Base4bitTests):
@pytest.fixture(autouse=True)
def _setup_slow(self):
gc.collect()
backend_empty_cache(torch_device)
nf4_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16,
)
model_4bit = SD3Transformer2DModel.from_pretrained(
self.model_name, subfolder="transformer", quantization_config=nf4_config, device_map=torch_device
)
self.pipeline_4bit = DiffusionPipeline.from_pretrained(
self.model_name, transformer=model_4bit, torch_dtype=torch.float16
)
self.pipeline_4bit.enable_model_cpu_offload()
yield
del self.pipeline_4bit
gc.collect()
backend_empty_cache(torch_device)
def test_quality(self):
output = self.pipeline_4bit(
prompt=self.prompt,
num_inference_steps=self.num_inference_steps,
generator=torch.manual_seed(self.seed),
output_type="np",
).images
out_slice = output[0, -3:, -3:, -1].flatten()
expected_slice = np.array([0.1123, 0.1296, 0.1609, 0.1042, 0.1230, 0.1274, 0.0928, 0.1165, 0.1216])
max_diff = numpy_cosine_similarity_distance(expected_slice, out_slice)
assert max_diff < 1e-2
def test_generate_quality_dequantize(self):
r"""
Test that loading the model and unquantize it produce correct results.
"""
self.pipeline_4bit.transformer.dequantize()
output = self.pipeline_4bit(
prompt=self.prompt,
num_inference_steps=self.num_inference_steps,
generator=torch.manual_seed(self.seed),
output_type="np",
).images
out_slice = output[0, -3:, -3:, -1].flatten()
expected_slice = np.array([0.1216, 0.1387, 0.1584, 0.1152, 0.1318, 0.1282, 0.1062, 0.1226, 0.1228])
max_diff = numpy_cosine_similarity_distance(expected_slice, out_slice)
assert max_diff < 1e-3
# Since we offloaded the `pipeline_4bit.transformer` to CPU (result of `enable_model_cpu_offload()), check
# the following.
assert self.pipeline_4bit.transformer.device.type == "cpu"
# calling it again shouldn't be a problem
_ = self.pipeline_4bit(
prompt=self.prompt,
num_inference_steps=2,
generator=torch.manual_seed(self.seed),
output_type="np",
).images
def test_moving_to_cpu_throws_warning(self):
nf4_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16,
)
model_4bit = SD3Transformer2DModel.from_pretrained(
self.model_name, subfolder="transformer", quantization_config=nf4_config, device_map=torch_device
)
logger = logging.get_logger("diffusers.pipelines.pipeline_utils")
logger.setLevel(30)
with CaptureLogger(logger) as cap_logger:
# Because `model.dtype` will return torch.float16 as SD3 transformer has
# a conv layer as the first layer.
_ = DiffusionPipeline.from_pretrained(
self.model_name, transformer=model_4bit, torch_dtype=torch.float16
).to("cpu")
assert "Pipelines loaded with `dtype=torch.float16`" in cap_logger.out
@pytest.mark.xfail(
condition=is_accelerate_version("<=", "1.1.1"),
reason="Test will pass after https://github.com/huggingface/accelerate/pull/3223 is in a release.",
strict=True,
)
def test_pipeline_cuda_placement_works_with_nf4(self):
transformer_nf4_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16,
)
transformer_4bit = SD3Transformer2DModel.from_pretrained(
self.model_name,
subfolder="transformer",
quantization_config=transformer_nf4_config,
torch_dtype=torch.float16,
device_map=torch_device,
)
text_encoder_3_nf4_config = TranBitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16,
)
text_encoder_3_4bit = T5EncoderModel.from_pretrained(
self.model_name,
subfolder="text_encoder_3",
quantization_config=text_encoder_3_nf4_config,
torch_dtype=torch.float16,
device_map=torch_device,
)
# CUDA device placement works.
pipeline_4bit = DiffusionPipeline.from_pretrained(
self.model_name,
transformer=transformer_4bit,
text_encoder_3=text_encoder_3_4bit,
torch_dtype=torch.float16,
).to(torch_device)
# Check if inference works.
_ = pipeline_4bit(self.prompt, max_sequence_length=20, num_inference_steps=2)
del pipeline_4bit
@require_transformers_version_greater("4.44.0")
class TestSlowBnb4BitFlux(Base4bitTests):
@pytest.fixture(autouse=True)
def _setup_flux(self):
gc.collect()
backend_empty_cache(torch_device)
model_id = "hf-internal-testing/flux.1-dev-nf4-pkg"
t5_4bit = T5EncoderModel.from_pretrained(model_id, subfolder="text_encoder_2")
transformer_4bit = FluxTransformer2DModel.from_pretrained(model_id, subfolder="transformer")
self.pipeline_4bit = DiffusionPipeline.from_pretrained(
"black-forest-labs/FLUX.1-dev",
text_encoder_2=t5_4bit,
transformer=transformer_4bit,
torch_dtype=torch.float16,
)
self.pipeline_4bit.enable_model_cpu_offload()
yield
del self.pipeline_4bit
gc.collect()
backend_empty_cache(torch_device)
def test_quality(self):
# keep the resolution and max tokens to a lower number for faster execution.
output = self.pipeline_4bit(
prompt=self.prompt,
num_inference_steps=self.num_inference_steps,
generator=torch.manual_seed(self.seed),
height=256,
width=256,
max_sequence_length=64,
output_type="np",
).images
out_slice = output[0, -3:, -3:, -1].flatten()
expected_slice = np.array([0.0583, 0.0586, 0.0632, 0.0815, 0.0813, 0.0947, 0.1040, 0.1145, 0.1265])
max_diff = numpy_cosine_similarity_distance(expected_slice, out_slice)
assert max_diff < 1e-3
@require_peft_backend
def test_lora_loading(self):
self.pipeline_4bit.load_lora_weights(
hf_hub_download("ByteDance/Hyper-SD", "Hyper-FLUX.1-dev-8steps-lora.safetensors"), adapter_name="hyper-sd"
)
self.pipeline_4bit.set_adapters("hyper-sd", adapter_weights=0.125)
output = self.pipeline_4bit(
prompt=self.prompt,
height=256,
width=256,
max_sequence_length=64,
output_type="np",
num_inference_steps=8,
generator=torch.Generator().manual_seed(42),
).images
out_slice = output[0, -3:, -3:, -1].flatten()
expected_slice = np.array([0.5347, 0.5342, 0.5283, 0.5093, 0.4988, 0.5093, 0.5044, 0.5015, 0.4946])
max_diff = numpy_cosine_similarity_distance(expected_slice, out_slice)
assert max_diff < 1e-3
@require_transformers_version_greater("4.44.0")
@require_peft_backend
class TestSlowBnb4BitFluxControlWithLora(Base4bitTests):
@pytest.fixture(autouse=True)
def _setup_flux_control(self):
gc.collect()
backend_empty_cache(torch_device)
self.pipeline_4bit = FluxControlPipeline.from_pretrained("eramth/flux-4bit", torch_dtype=torch.float16)
self.pipeline_4bit.enable_model_cpu_offload()
yield
del self.pipeline_4bit
gc.collect()
backend_empty_cache(torch_device)
def test_lora_loading(self):
self.pipeline_4bit.load_lora_weights("black-forest-labs/FLUX.1-Canny-dev-lora")
output = self.pipeline_4bit(
prompt=self.prompt,
control_image=Image.new(mode="RGB", size=(256, 256)),
height=256,
width=256,
max_sequence_length=64,
output_type="np",
num_inference_steps=8,
generator=torch.Generator().manual_seed(42),
).images
out_slice = output[0, -3:, -3:, -1].flatten()
expected_slice = np.array([0.1636, 0.1675, 0.1982, 0.1743, 0.1809, 0.1936, 0.1743, 0.2095, 0.2139])
max_diff = numpy_cosine_similarity_distance(expected_slice, out_slice)
assert max_diff < 1e-3, f"{out_slice=} != {expected_slice=}"
@is_quantization
@is_bitsandbytes
@require_torch_version_greater("2.7.1")
@require_bitsandbytes_version_greater("0.45.5")
class TestBnb4BitCompile(QuantCompileTests):
@property
def quantization_config(self):
return PipelineQuantizationConfig(
quant_backend="bitsandbytes_4bit",
quant_kwargs={
"load_in_4bit": True,
"bnb_4bit_quant_type": "nf4",
"bnb_4bit_compute_dtype": torch.bfloat16,
},
components_to_quantize=["transformer", "text_encoder_2"],
)
@is_quantization
@is_bitsandbytes
@require_bitsandbytes_version_greater("0.43.2")
@require_accelerate
@require_torch
@require_torch_accelerator
@slow
class Base8bitTests:
# We need to test on relatively large models (aka >1b parameters otherwise the quantiztion may not work as expected)
# Therefore here we use only SD3 to test our module
model_name = "stabilityai/stable-diffusion-3-medium-diffusers"
prompt = "a beautiful sunset amidst the mountains."
num_inference_steps = 10
seed = 0
@pytest.fixture(autouse=True, scope="class")
def _toggle_determinism(self):
was_enabled = torch.are_deterministic_algorithms_enabled()
if not was_enabled:
torch.use_deterministic_algorithms(True)
yield
if not was_enabled:
torch.use_deterministic_algorithms(False)
@require_transformers_version_greater("4.44.0")
class TestSlowBnb8bit(Base8bitTests):
@pytest.fixture(autouse=True)
def _setup_slow(self):
gc.collect()
backend_empty_cache(torch_device)
mixed_int8_config = BitsAndBytesConfig(load_in_8bit=True)
model_8bit = SD3Transformer2DModel.from_pretrained(
self.model_name, subfolder="transformer", quantization_config=mixed_int8_config, device_map=torch_device
)
self.pipeline_8bit = DiffusionPipeline.from_pretrained(
self.model_name, transformer=model_8bit, torch_dtype=torch.float16
)
self.pipeline_8bit.enable_model_cpu_offload()
yield
del self.pipeline_8bit
gc.collect()
backend_empty_cache(torch_device)
def test_quality(self):
output = self.pipeline_8bit(
prompt=self.prompt,
num_inference_steps=self.num_inference_steps,
generator=torch.manual_seed(self.seed),
output_type="np",
).images
out_slice = output[0, -3:, -3:, -1].flatten()
expected_slice = np.array([0.0674, 0.0623, 0.0364, 0.0632, 0.0671, 0.0430, 0.0317, 0.0493, 0.0583])
max_diff = numpy_cosine_similarity_distance(expected_slice, out_slice)
assert max_diff < 1e-2
def test_model_cpu_offload_raises_warning(self):
model_8bit = SD3Transformer2DModel.from_pretrained(
self.model_name,
subfolder="transformer",
quantization_config=BitsAndBytesConfig(load_in_8bit=True),
device_map=torch_device,
)
pipeline_8bit = DiffusionPipeline.from_pretrained(
self.model_name, transformer=model_8bit, torch_dtype=torch.float16
)
logger = logging.get_logger("diffusers.pipelines.pipeline_utils")
logger.setLevel(30)
with CaptureLogger(logger) as cap_logger:
pipeline_8bit.enable_model_cpu_offload()
assert "has been loaded in `bitsandbytes` 8bit" in cap_logger.out
def test_moving_to_cpu_throws_warning(self):
model_8bit = SD3Transformer2DModel.from_pretrained(
self.model_name,
subfolder="transformer",
quantization_config=BitsAndBytesConfig(load_in_8bit=True),
device_map=torch_device,
)
logger = logging.get_logger("diffusers.pipelines.pipeline_utils")
logger.setLevel(30)
with CaptureLogger(logger) as cap_logger:
# Because `model.dtype` will return torch.float16 as SD3 transformer has
# a conv layer as the first layer.
_ = DiffusionPipeline.from_pretrained(
self.model_name, transformer=model_8bit, torch_dtype=torch.float16
).to("cpu")
assert "Pipelines loaded with `dtype=torch.float16`" in cap_logger.out
def test_generate_quality_dequantize(self):
r"""
Test that loading the model and unquantize it produce correct results.
"""
self.pipeline_8bit.transformer.dequantize()
output = self.pipeline_8bit(
prompt=self.prompt,
num_inference_steps=self.num_inference_steps,
generator=torch.manual_seed(self.seed),
output_type="np",
).images
out_slice = output[0, -3:, -3:, -1].flatten()
expected_slice = np.array([0.0266, 0.0264, 0.0271, 0.0110, 0.0310, 0.0098, 0.0078, 0.0256, 0.0208])
max_diff = numpy_cosine_similarity_distance(expected_slice, out_slice)
assert max_diff < 1e-2
# 8bit models cannot be offloaded to CPU.
assert self.pipeline_8bit.transformer.device.type == torch_device
# calling it again shouldn't be a problem
_ = self.pipeline_8bit(
prompt=self.prompt,
num_inference_steps=2,
generator=torch.manual_seed(self.seed),
output_type="np",
).images
@pytest.mark.xfail(
condition=is_accelerate_version("<=", "1.1.1"),
reason="Test will pass after https://github.com/huggingface/accelerate/pull/3223 is in a release.",
strict=True,
)
def test_pipeline_cuda_placement_works_with_mixed_int8(self):
transformer_8bit_config = BitsAndBytesConfig(load_in_8bit=True)
transformer_8bit = SD3Transformer2DModel.from_pretrained(
self.model_name,
subfolder="transformer",
quantization_config=transformer_8bit_config,
torch_dtype=torch.float16,
device_map=torch_device,
)
text_encoder_3_8bit_config = TranBitsAndBytesConfig(load_in_8bit=True)
text_encoder_3_8bit = T5EncoderModel.from_pretrained(
self.model_name,
subfolder="text_encoder_3",
quantization_config=text_encoder_3_8bit_config,
torch_dtype=torch.float16,
device_map=torch_device,
)
# CUDA device placement works.
device = torch_device if torch_device != "rocm" else "cuda"
pipeline_8bit = DiffusionPipeline.from_pretrained(
self.model_name,
transformer=transformer_8bit,
text_encoder_3=text_encoder_3_8bit,
torch_dtype=torch.float16,
).to(device)
# Check if inference works.
_ = pipeline_8bit(self.prompt, max_sequence_length=20, num_inference_steps=2)
del pipeline_8bit
@require_transformers_version_greater("4.44.0")
@require_big_accelerator
class TestSlowBnb8bitFlux(Base8bitTests):
@pytest.fixture(autouse=True)
def _setup_slow_flux(self):
gc.collect()
backend_empty_cache(torch_device)
model_id = "hf-internal-testing/flux.1-dev-int8-pkg"
t5_8bit = T5EncoderModel.from_pretrained(model_id, subfolder="text_encoder_2")
transformer_8bit = FluxTransformer2DModel.from_pretrained(model_id, subfolder="transformer")
self.pipeline_8bit = DiffusionPipeline.from_pretrained(
"black-forest-labs/FLUX.1-dev",
text_encoder_2=t5_8bit,
transformer=transformer_8bit,
torch_dtype=torch.float16,
)
self.pipeline_8bit.enable_model_cpu_offload()
yield
del self.pipeline_8bit
gc.collect()
backend_empty_cache(torch_device)
def test_quality(self):
# keep the resolution and max tokens to a lower number for faster execution.
output = self.pipeline_8bit(
prompt=self.prompt,
num_inference_steps=self.num_inference_steps,
generator=torch.manual_seed(self.seed),
height=256,
width=256,
max_sequence_length=64,
output_type="np",
).images
out_slice = output[0, -3:, -3:, -1].flatten()
expected_slice = np.array([0.0574, 0.0554, 0.0581, 0.0686, 0.0676, 0.0759, 0.0757, 0.0803, 0.0930])
max_diff = numpy_cosine_similarity_distance(expected_slice, out_slice)
assert max_diff < 1e-3
@require_peft_version_greater("0.14.0")
def test_lora_loading(self):
self.pipeline_8bit.load_lora_weights(
hf_hub_download("ByteDance/Hyper-SD", "Hyper-FLUX.1-dev-8steps-lora.safetensors"), adapter_name="hyper-sd"
)
self.pipeline_8bit.set_adapters("hyper-sd", adapter_weights=0.125)
output = self.pipeline_8bit(
prompt=self.prompt,
height=256,
width=256,
max_sequence_length=64,
output_type="np",
num_inference_steps=8,
generator=torch.manual_seed(42),
).images
out_slice = output[0, -3:, -3:, -1].flatten()
expected_slice = np.array([0.3916, 0.3916, 0.3887, 0.4243, 0.4155, 0.4233, 0.4570, 0.4531, 0.4248])
max_diff = numpy_cosine_similarity_distance(expected_slice, out_slice)
assert max_diff < 1e-3
@require_transformers_version_greater("4.44.0")
@require_peft_backend
class TestSlowBnb8bitFluxControlWithLora(Base8bitTests):
@pytest.fixture(autouse=True)
def _setup_flux_control_lora(self):
gc.collect()
backend_empty_cache(torch_device)
self.pipeline_8bit = FluxControlPipeline.from_pretrained(
"black-forest-labs/FLUX.1-dev",
quantization_config=PipelineQuantizationConfig(
quant_backend="bitsandbytes_8bit",
quant_kwargs={"load_in_8bit": True},
components_to_quantize=["transformer", "text_encoder_2"],
),
torch_dtype=torch.float16,
)
self.pipeline_8bit.enable_model_cpu_offload()
yield
del self.pipeline_8bit
gc.collect()
backend_empty_cache(torch_device)
def test_lora_loading(self):
self.pipeline_8bit.load_lora_weights("black-forest-labs/FLUX.1-Canny-dev-lora")
output = self.pipeline_8bit(
prompt=self.prompt,
control_image=Image.new(mode="RGB", size=(256, 256)),
height=256,
width=256,
max_sequence_length=64,
output_type="np",
num_inference_steps=8,
generator=torch.Generator().manual_seed(42),
).images
out_slice = output[0, -3:, -3:, -1].flatten()
# Hardware-dependent: the Control LoRA dequantizes and expands `x_embedder`, and the error
# accumulates over the 8 denoising steps enough that even different CUDA GPUs disagree, so
# reference slices are stored per accelerator backend.
expected_slices = Expectations(
{
(None, None): np.array([0.2029, 0.2136, 0.2268, 0.1921, 0.1997, 0.2185, 0.2021, 0.2183, 0.2292]),
("xpu", 5): np.array([0.0955, 0.1223, 0.1509, 0.0872, 0.1155, 0.1890, 0.0754, 0.1028, 0.2178]),
}
)
expected_slice = expected_slices.get_expectation()
max_diff = numpy_cosine_similarity_distance(expected_slice, out_slice)
assert max_diff < 1e-3, f"{out_slice=} != {expected_slice=}"
@is_quantization
@is_bitsandbytes
@require_torch_version_greater_equal("2.6.0")
@require_bitsandbytes_version_greater("0.48.0")
class TestBnb8BitCompile(QuantCompileTests):
@property
def quantization_config(self):
return PipelineQuantizationConfig(
quant_backend="bitsandbytes_8bit",
quant_kwargs={"load_in_8bit": True},
components_to_quantize=["transformer", "text_encoder_2"],
)
def test_torch_compile_with_cpu_offload(self):
super()._test_torch_compile_with_cpu_offload(torch_dtype=torch.float16)
# ======================== TorchAO ========================
def _is_xpu_or_cuda_capability_atleast_8_9() -> bool:
if torch.cuda.is_available():
major, minor = torch.cuda.get_device_capability()
if major == 8:
return minor >= 9
return major >= 9
elif torch.xpu.is_available():
return True
return False
# Model-level TorchAO tests live in `tests/models/testing_utils/quantization.py`
# (`TorchAoTesterMixin` / `TorchAoCompileTesterMixin`), wired into model test files via concrete
# classes (e.g. `TestFluxTransformerTorchAo`). Only pipeline-level coverage remains here.
# Slices for these tests have been obtained on our aws-g6e-xlarge-plus runners
@is_quantization
@is_torchao
@require_torch
@require_torch_accelerator
@require_torchao_version_greater_or_equal("0.15.0")
class TestTorchAo:
@pytest.fixture(autouse=True)
def _setup_torchao(self):
yield
gc.collect()
backend_empty_cache(torch_device)
def get_dummy_components(
self, quantization_config: TorchAoConfig, model_id: str = "hf-internal-testing/tiny-flux-pipe"
):
transformer = FluxTransformer2DModel.from_pretrained(
model_id,
subfolder="transformer",
quantization_config=quantization_config,
torch_dtype=torch.bfloat16,
)
text_encoder = CLIPTextModel.from_pretrained(model_id, subfolder="text_encoder", torch_dtype=torch.bfloat16)
text_encoder_2 = T5EncoderModel.from_pretrained(
model_id, subfolder="text_encoder_2", torch_dtype=torch.bfloat16
)
tokenizer = CLIPTokenizer.from_pretrained(model_id, subfolder="tokenizer")
tokenizer_2 = AutoTokenizer.from_pretrained(model_id, subfolder="tokenizer_2")
vae = AutoencoderKL.from_pretrained(model_id, subfolder="vae", torch_dtype=torch.bfloat16)
scheduler = FlowMatchEulerDiscreteScheduler()
return {
"scheduler": scheduler,
"text_encoder": text_encoder,
"text_encoder_2": text_encoder_2,
"tokenizer": tokenizer,
"tokenizer_2": tokenizer_2,
"transformer": transformer,
"vae": vae,
}
def get_dummy_inputs(self, device: torch.device, seed: int = 0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator().manual_seed(seed)
inputs = {
"prompt": "an astronaut riding a horse in space",
"height": 32,
"width": 32,
"num_inference_steps": 2,
"output_type": "np",
"generator": generator,
}
return inputs
def _test_quant_type(self, quantization_config: TorchAoConfig, expected_slice: list[float], model_id: str):
components = self.get_dummy_components(quantization_config, model_id)
pipe = FluxPipeline(**components)
pipe.to(device=torch_device)
inputs = self.get_dummy_inputs(torch_device)
output = pipe(**inputs)[0]
output_slice = output[-1, -1, -3:, -3:].flatten()
assert np.allclose(output_slice, expected_slice, atol=1e-3, rtol=1e-3)
def test_quantization(self):
for model_id in ["hf-internal-testing/tiny-flux-pipe", "hf-internal-testing/tiny-flux-sharded"]:
# fmt: off
QUANTIZATION_TYPES_TO_TEST = [
(Int4WeightOnlyConfig(version=2), np.array([0.4648, 0.5234, 0.5547, 0.4219, 0.4414, 0.6445, 0.4336, 0.4531, 0.5625])),
(Int8DynamicActivationIntxWeightConfig(version=2), np.array([0.4688, 0.5195, 0.5547, 0.418, 0.4414, 0.6406, 0.4336, 0.4531, 0.5625])),
(Int8WeightOnlyConfig(version=2), np.array([0.4648, 0.5195, 0.5547, 0.4199, 0.4414, 0.6445, 0.4316, 0.4531, 0.5625])),
(Int8DynamicActivationInt8WeightConfig(version=2), np.array([0.4648, 0.5195, 0.5547, 0.4199, 0.4414, 0.6445, 0.4316, 0.4531, 0.5625])),
(IntxWeightOnlyConfig(dtype=torch.uint4, group_size=16, version=2), np.array([0.4609, 0.5234, 0.5508, 0.4199, 0.4336, 0.6406, 0.4316, 0.4531, 0.5625])),
(IntxWeightOnlyConfig(dtype=torch.uint7, group_size=16, version=2), np.array([0.4648, 0.5195, 0.5547, 0.4219, 0.4414, 0.6445, 0.4316, 0.4531, 0.5625])),
]
if _is_xpu_or_cuda_capability_atleast_8_9():
QUANTIZATION_TYPES_TO_TEST.extend([
(Float8WeightOnlyConfig(weight_dtype=torch.float8_e5m2), np.array([0.4590, 0.5273, 0.5547, 0.4219, 0.4375, 0.6406, 0.4316, 0.4512, 0.5625])),
(Float8WeightOnlyConfig(weight_dtype=torch.float8_e4m3fn), np.array([0.4648, 0.5234, 0.5547, 0.4219, 0.4414, 0.6406, 0.4316, 0.4531, 0.5625])),
])
# fmt: on
for quant_config, expected_slice in QUANTIZATION_TYPES_TO_TEST:
quantization_config = TorchAoConfig(quant_type=quant_config, modules_to_not_convert=["x_embedder"])
self._test_quant_type(quantization_config, expected_slice, model_id)
def test_sequential_cpu_offload(self):
r"""
A test that checks if inference runs as expected when sequential cpu offloading is enabled.
"""
quantization_config = TorchAoConfig(Int8WeightOnlyConfig())
components = self.get_dummy_components(quantization_config)
pipe = FluxPipeline(**components)
pipe.enable_sequential_cpu_offload()
inputs = self.get_dummy_inputs(torch_device)
_ = pipe(**inputs)
@require_torchao_version_greater_or_equal("0.15.0")
def test_aobase_config(self):
quantization_config = TorchAoConfig(Int8WeightOnlyConfig())
components = self.get_dummy_components(quantization_config)
pipe = FluxPipeline(**components).to(torch_device)
inputs = self.get_dummy_inputs(torch_device)
_ = pipe(**inputs)
@is_quantization
@is_torchao
@require_torchao_version_greater_or_equal("0.15.0")
class TestTorchAoCompile(QuantCompileTests):
@property
def quantization_config(self):
return PipelineQuantizationConfig(
quant_mapping={"transformer": TorchAoConfig(Int8WeightOnlyConfig())},
)
def test_torch_compile_with_cpu_offload(self):
pipe = self._init_pipeline(self.quantization_config, torch.bfloat16)
pipe.enable_model_cpu_offload()
# No compilation because it fails with:
# RuntimeError: _apply(): Couldn't swap Linear.weight
# small resolutions to ensure speedy execution.
pipe("a dog", num_inference_steps=2, max_sequence_length=16, height=256, width=256)
# Slices for these tests have been obtained on our aws-g6e-xlarge-plus runners
@is_quantization
@is_torchao
@require_torch
@require_torch_accelerator
@require_torchao_version_greater_or_equal("0.15.0")
@slow
@nightly
class TestSlowTorchAo:
@pytest.fixture(autouse=True)
def _setup_slow_torchao(self):
yield
gc.collect()
backend_empty_cache(torch_device)
def get_dummy_components(self, quantization_config: TorchAoConfig):
# This is just for convenience, so that we can modify it at one place for custom environments and locally testing
cache_dir = None
model_id = "black-forest-labs/FLUX.1-dev"
transformer = FluxTransformer2DModel.from_pretrained(
model_id,
subfolder="transformer",
quantization_config=quantization_config,
torch_dtype=torch.bfloat16,
cache_dir=cache_dir,
)
text_encoder = CLIPTextModel.from_pretrained(
model_id, subfolder="text_encoder", torch_dtype=torch.bfloat16, cache_dir=cache_dir
)
text_encoder_2 = T5EncoderModel.from_pretrained(
model_id, subfolder="text_encoder_2", torch_dtype=torch.bfloat16, cache_dir=cache_dir
)
tokenizer = CLIPTokenizer.from_pretrained(model_id, subfolder="tokenizer", cache_dir=cache_dir)
tokenizer_2 = AutoTokenizer.from_pretrained(model_id, subfolder="tokenizer_2", cache_dir=cache_dir)
vae = AutoencoderKL.from_pretrained(model_id, subfolder="vae", torch_dtype=torch.bfloat16, cache_dir=cache_dir)
scheduler = FlowMatchEulerDiscreteScheduler()
return {
"scheduler": scheduler,
"text_encoder": text_encoder,
"text_encoder_2": text_encoder_2,
"tokenizer": tokenizer,
"tokenizer_2": tokenizer_2,
"transformer": transformer,
"vae": vae,
}
def get_dummy_inputs(self, device: torch.device, seed: int = 0):
if str(device).startswith("mps"):
generator = torch.manual_seed(seed)
else:
generator = torch.Generator().manual_seed(seed)
inputs = {
"prompt": "an astronaut riding a horse in space",
"height": 512,
"width": 512,
"num_inference_steps": 20,
"output_type": "np",
"generator": generator,
}
return inputs
def _test_quant_type(self, quantization_config, expected_slice):
components = self.get_dummy_components(quantization_config)
pipe = FluxPipeline(**components)
pipe.enable_model_cpu_offload()
weight = pipe.transformer.transformer_blocks[0].ff.net[2].weight
assert isinstance(weight, TorchAOBaseTensor)
inputs = self.get_dummy_inputs(torch_device)
output = pipe(**inputs)[0].flatten()
output_slice = np.concatenate((output[:16], output[-16:]))
assert np.allclose(output_slice, expected_slice, atol=1e-3, rtol=1e-3)
def test_quantization(self):
# fmt: off
QUANTIZATION_TYPES_TO_TEST = [
(Int8WeightOnlyConfig(), np.array([0.0505, 0.0742, 0.1367, 0.0429, 0.0585, 0.1386, 0.0585, 0.0703, 0.1367, 0.0566, 0.0703, 0.1464, 0.0546, 0.0703, 0.1425, 0.0546, 0.3535, 0.7578, 0.5000, 0.4062, 0.7656, 0.5117, 0.4121, 0.7656, 0.5117, 0.3984, 0.7578, 0.5234, 0.4023, 0.7382, 0.5390, 0.4570])),
(Int8DynamicActivationInt8WeightConfig(), np.array([0.0546, 0.0761, 0.1386, 0.0488, 0.0644, 0.1425, 0.0605, 0.0742, 0.1406, 0.0625, 0.0722, 0.1523, 0.0625, 0.0742, 0.1503, 0.0605, 0.3886, 0.7968, 0.5507, 0.4492, 0.7890, 0.5351, 0.4316, 0.8007, 0.5390, 0.4179, 0.8281, 0.5820, 0.4531, 0.7812, 0.5703, 0.4921])),
]
if _is_xpu_or_cuda_capability_atleast_8_9():
QUANTIZATION_TYPES_TO_TEST.extend([
(Float8WeightOnlyConfig(weight_dtype=torch.float8_e4m3fn), np.array([0.0546, 0.0722, 0.1328, 0.0468, 0.0585, 0.1367, 0.0605, 0.0703, 0.1328, 0.0625, 0.0703, 0.1445, 0.0585, 0.0703, 0.1406, 0.0605, 0.3496, 0.7109, 0.4843, 0.4042, 0.7226, 0.5000, 0.4160, 0.7031, 0.4824, 0.3886, 0.6757, 0.4667, 0.3710, 0.6679, 0.4902, 0.4238])),
])
# fmt: on
for quant_config, expected_slice in QUANTIZATION_TYPES_TO_TEST:
quantization_config = TorchAoConfig(quant_type=quant_config, modules_to_not_convert=["x_embedder"])
self._test_quant_type(quantization_config, expected_slice)
gc.collect()
backend_empty_cache(torch_device)
backend_synchronize(torch_device)
def test_serialization_int8wo(self):
quantization_config = TorchAoConfig(Int8WeightOnlyConfig())
components = self.get_dummy_components(quantization_config)
pipe = FluxPipeline(**components)
pipe.enable_model_cpu_offload()
weight = pipe.transformer.x_embedder.weight
assert isinstance(weight, Int8Tensor)
inputs = self.get_dummy_inputs(torch_device)
output = pipe(**inputs)[0].flatten()[:128]
with tempfile.TemporaryDirectory() as tmp_dir:
pipe.transformer.save_pretrained(tmp_dir, safe_serialization=False)
pipe.remove_all_hooks()
del pipe.transformer
gc.collect()
backend_empty_cache(torch_device)
backend_synchronize(torch_device)
transformer = FluxTransformer2DModel.from_pretrained(
tmp_dir, torch_dtype=torch.bfloat16, use_safetensors=False
)
pipe.transformer = transformer
pipe.enable_model_cpu_offload()
weight = transformer.x_embedder.weight
assert isinstance(weight, Int8Tensor)
loaded_output = pipe(**inputs)[0].flatten()[:128]
# Seems to require higher tolerance depending on which machine it is being run.
# A difference of 0.06 in normalized pixel space (-1 to 1), corresponds to a difference of
# 0.06 / 2 * 255 = 7.65 in pixel space (0 to 255). On our CI runners, the difference is about 0.04,
# on DGX it is 0.06, and on audace it is 0.037. So, we are using a tolerance of 0.06 here.
assert np.allclose(output, loaded_output, atol=0.06)
# ======================== GGUF ========================
# Model-level GGUF tests live in `tests/models/testing_utils/quantization.py`
# (`GGUFTesterMixin` / `GGUFCompileTesterMixin`) and backend-level ones (quantized parameter/layer
# inspection, memory use, CUDA kernels) in `tests/quantization/gguf/`. Only pipeline-level coverage
# remains here.
@is_quantization
@is_gguf
@nightly
@require_big_accelerator
@require_accelerate
@require_gguf_version_greater_or_equal("0.10.0")
class GGUFPipelineTests:
@pytest.fixture(autouse=True)
def _cleanup(self):
gc.collect()
backend_empty_cache(torch_device)
yield
gc.collect()
backend_empty_cache(torch_device)
class TestFluxGGUFPipeline(GGUFPipelineTests):
ckpt_path = "https://huggingface.co/city96/FLUX.1-dev-gguf/blob/main/flux1-dev-Q2_K.gguf"
model_cls = FluxTransformer2DModel
torch_dtype = torch.bfloat16
def test_pipeline_inference(self):
quantization_config = GGUFQuantizationConfig(compute_dtype=self.torch_dtype)
transformer = self.model_cls.from_single_file(
self.ckpt_path, quantization_config=quantization_config, torch_dtype=self.torch_dtype
)
pipe = FluxPipeline.from_pretrained(
"black-forest-labs/FLUX.1-dev", transformer=transformer, torch_dtype=self.torch_dtype
)
pipe.enable_model_cpu_offload()
prompt = "a cat holding a sign that says hello"
output = pipe(
prompt=prompt, num_inference_steps=2, generator=torch.Generator("cpu").manual_seed(0), output_type="np"
).images[0]
output_slice = output[:3, :3, :].flatten()
expected_slice = np.array(
[
0.47265625,
0.43359375,
0.359375,
0.47070312,
0.421875,
0.34375,
0.46875,
0.421875,
0.34765625,
0.46484375,
0.421875,
0.34179688,
0.47070312,
0.42578125,
0.34570312,
0.46875,
0.42578125,
0.3515625,
0.45507812,
0.4140625,
0.33984375,
0.4609375,
0.41796875,
0.34375,
0.45898438,
0.41796875,
0.34375,
]
)
max_diff = numpy_cosine_similarity_distance(expected_slice, output_slice)
assert max_diff < 1e-4
class TestSD35LargeGGUFPipeline(GGUFPipelineTests):
ckpt_path = "https://huggingface.co/city96/stable-diffusion-3.5-large-gguf/blob/main/sd3.5_large-Q4_0.gguf"
model_cls = SD3Transformer2DModel
torch_dtype = torch.bfloat16
def test_pipeline_inference(self):
quantization_config = GGUFQuantizationConfig(compute_dtype=self.torch_dtype)
transformer = self.model_cls.from_single_file(
self.ckpt_path, quantization_config=quantization_config, torch_dtype=self.torch_dtype
)
pipe = StableDiffusion3Pipeline.from_pretrained(
"stabilityai/stable-diffusion-3.5-large", transformer=transformer, torch_dtype=self.torch_dtype
)
pipe.enable_model_cpu_offload()
prompt = "a cat holding a sign that says hello"
output = pipe(
prompt=prompt,
num_inference_steps=2,
generator=torch.Generator("cpu").manual_seed(0),
output_type="np",
).images[0]
output_slice = output[:3, :3, :].flatten()
expected_slices = Expectations(
{
("xpu", 3): np.array(
[
0.16796875,
0.27929688,
0.28320312,
0.11328125,
0.27539062,
0.26171875,
0.10742188,
0.26367188,
0.26171875,
0.1484375,
0.2734375,
0.296875,
0.13476562,
0.2890625,
0.30078125,
0.1171875,
0.28125,
0.28125,
0.16015625,
0.31445312,
0.30078125,
0.15625,
0.32421875,
0.296875,
0.14453125,
0.30859375,
0.2890625,
]
),
("cuda", 7): np.array(
[
0.17578125,
0.27539062,
0.27734375,
0.11914062,
0.26953125,
0.25390625,
0.109375,
0.25390625,
0.25,
0.15039062,
0.26171875,
0.28515625,
0.13671875,
0.27734375,
0.28515625,
0.12109375,
0.26757812,
0.265625,
0.16210938,
0.29882812,
0.28515625,
0.15625,
0.30664062,
0.27734375,
0.14648438,
0.29296875,
0.26953125,
]
),
}
)
expected_slice = expected_slices.get_expectation()
max_diff = numpy_cosine_similarity_distance(expected_slice, output_slice)
assert max_diff < 1e-4
class TestSD35MediumGGUFPipeline(GGUFPipelineTests):
ckpt_path = "https://huggingface.co/city96/stable-diffusion-3.5-medium-gguf/blob/main/sd3.5_medium-Q3_K_M.gguf"
model_cls = SD3Transformer2DModel
torch_dtype = torch.bfloat16
def test_pipeline_inference(self):
quantization_config = GGUFQuantizationConfig(compute_dtype=self.torch_dtype)
transformer = self.model_cls.from_single_file(
self.ckpt_path, quantization_config=quantization_config, torch_dtype=self.torch_dtype
)
pipe = StableDiffusion3Pipeline.from_pretrained(
"stabilityai/stable-diffusion-3.5-medium", transformer=transformer, torch_dtype=self.torch_dtype
)
pipe.enable_model_cpu_offload()
prompt = "a cat holding a sign that says hello"
output = pipe(
prompt=prompt, num_inference_steps=2, generator=torch.Generator("cpu").manual_seed(0), output_type="np"
).images[0]
output_slice = output[:3, :3, :].flatten()
expected_slice = np.array(
[
0.625,
0.6171875,
0.609375,
0.65625,
0.65234375,
0.640625,
0.6484375,
0.640625,
0.625,
0.6484375,
0.63671875,
0.6484375,
0.66796875,
0.65625,
0.65234375,
0.6640625,
0.6484375,
0.6328125,
0.6640625,
0.6484375,
0.640625,
0.67578125,
0.66015625,
0.62109375,
0.671875,
0.65625,
0.62109375,
]
)
max_diff = numpy_cosine_similarity_distance(expected_slice, output_slice)
assert max_diff < 1e-4
class TestAuraFlowGGUFPipeline(GGUFPipelineTests):
ckpt_path = "https://huggingface.co/city96/AuraFlow-v0.3-gguf/blob/main/aura_flow_0.3-Q2_K.gguf"
model_cls = AuraFlowTransformer2DModel
torch_dtype = torch.bfloat16
def test_pipeline_inference(self):
quantization_config = GGUFQuantizationConfig(compute_dtype=self.torch_dtype)
transformer = self.model_cls.from_single_file(
self.ckpt_path, quantization_config=quantization_config, torch_dtype=self.torch_dtype
)
pipe = AuraFlowPipeline.from_pretrained(
"fal/AuraFlow-v0.3", transformer=transformer, torch_dtype=self.torch_dtype
)
pipe.enable_model_cpu_offload()
prompt = "a pony holding a sign that says hello"
output = pipe(
prompt=prompt, num_inference_steps=2, generator=torch.Generator("cpu").manual_seed(0), output_type="np"
).images[0]
output_slice = output[:3, :3, :].flatten()
expected_slice = np.array(
[
0.46484375,
0.546875,
0.64453125,
0.48242188,
0.53515625,
0.59765625,
0.47070312,
0.5078125,
0.5703125,
0.42773438,
0.50390625,
0.5703125,
0.47070312,
0.515625,
0.57421875,
0.45898438,
0.48632812,
0.53515625,
0.4453125,
0.5078125,
0.56640625,
0.47851562,
0.5234375,
0.57421875,
0.48632812,
0.5234375,
0.56640625,
]
)
max_diff = numpy_cosine_similarity_distance(expected_slice, output_slice)
assert max_diff < 1e-4
@is_quantization
@is_gguf
@require_peft_backend
@nightly
@require_big_accelerator
@require_accelerate
@require_gguf_version_greater_or_equal("0.10.0")
class TestFluxControlLoRAGGUF:
def test_lora_loading(self):
ckpt_path = "https://huggingface.co/city96/FLUX.1-dev-gguf/blob/main/flux1-dev-Q2_K.gguf"
transformer = FluxTransformer2DModel.from_single_file(
ckpt_path,
quantization_config=GGUFQuantizationConfig(compute_dtype=torch.bfloat16),
torch_dtype=torch.bfloat16,
)
pipe = FluxControlPipeline.from_pretrained(
"black-forest-labs/FLUX.1-dev",
transformer=transformer,
torch_dtype=torch.bfloat16,
).to(torch_device)
pipe.load_lora_weights("black-forest-labs/FLUX.1-Canny-dev-lora")
prompt = "A robot made of exotic candies and chocolates of different kinds. The background is filled with confetti and celebratory gifts."
control_image = load_image(
"https://huggingface.co/datasets/sayakpaul/sample-datasets/resolve/main/control_image_robot_canny.png"
)
output = pipe(
prompt=prompt,
control_image=control_image,
height=256,
width=256,
num_inference_steps=10,
guidance_scale=30.0,
output_type="np",
generator=torch.manual_seed(0),
).images
out_slice = output[0, -3:, -3:, -1].flatten()
expected_slice = np.array([0.8047, 0.8359, 0.8711, 0.6875, 0.7070, 0.7383, 0.5469, 0.5820, 0.6641])
max_diff = numpy_cosine_similarity_distance(expected_slice, out_slice)
assert max_diff < 1e-3
@is_quantization
@is_gguf
@require_torch_version_greater("2.7.1")
class TestGGUFCompile(QuantCompileTests):
torch_dtype = torch.bfloat16
gguf_ckpt = "https://huggingface.co/city96/FLUX.1-dev-gguf/blob/main/flux1-dev-Q2_K.gguf"
@property
def quantization_config(self):
return GGUFQuantizationConfig(compute_dtype=self.torch_dtype)
def _init_pipeline(self, *args, **kwargs):
transformer = FluxTransformer2DModel.from_single_file(
self.gguf_ckpt, quantization_config=self.quantization_config, torch_dtype=self.torch_dtype
)
pipe = DiffusionPipeline.from_pretrained(
"black-forest-labs/FLUX.1-dev", transformer=transformer, torch_dtype=self.torch_dtype
)
return pipe
# ======================== NVIDIA ModelOpt ========================
# Model-level ModelOpt tests live in `tests/models/testing_utils/quantization.py`
# (`ModelOptTesterMixin` / `ModelOptCompileTesterMixin`), wired into model test files via concrete
# classes (e.g. `TestSD3TransformerModelOpt`). Only pipeline-level coverage remains here.
@is_quantization
@is_modelopt
@nightly
@require_big_accelerator
@require_accelerate
@require_modelopt_version_greater_or_equal("0.33.1")
class TestModelOptFP8:
model_id = "hf-internal-testing/tiny-sd3-pipe"
@pytest.fixture(autouse=True)
def _setup(self):
backend_reset_peak_memory_stats(torch_device)
backend_empty_cache(torch_device)
gc.collect()
yield
backend_reset_peak_memory_stats(torch_device)
backend_empty_cache(torch_device)
gc.collect()
def test_model_cpu_offload(self):
transformer = SD3Transformer2DModel.from_pretrained(
self.model_id,
quantization_config=NVIDIAModelOptConfig(quant_type="FP8"),
subfolder="transformer",
torch_dtype=torch.bfloat16,
)
pipe = StableDiffusion3Pipeline.from_pretrained(
self.model_id, transformer=transformer, torch_dtype=torch.bfloat16
)
pipe.enable_model_cpu_offload(device=torch_device)
_ = pipe("a cat holding a sign that says hello", num_inference_steps=2)