1
0
mirror of https://github.com/huggingface/diffusers.git synced 2026-10-24 20:59:27 +03:00
Files
diffusers/tests/models/testing_utils/quantization.py
Sayak Paul 7ecb1e1b4f Flux3 image (#15046)
* Bump the actions group with 17 updates (#12)

Bumps the actions group with 17 updates:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `6` | `7` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `4.6.2` | `7.0.1` |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `3.12.0` | `4.2.0` |
| [docker/login-action](https://github.com/docker/login-action) | `3.7.0` | `4.6.0` |
| [docker/build-push-action](https://github.com/docker/build-push-action) | `6.19.2` | `7.3.0` |
| [huggingface/doc-builder/.github/workflows/build_main_documentation.yml](https://github.com/huggingface/doc-builder) | `e60a538eea9817ab312196d0d233604b01697265` | `af49b8dc3859c3d80c7220c8c0ba146431132e49` |
| [actions/setup-python](https://github.com/actions/setup-python) | `5.6.0` | `7.0.0` |
| [huggingface/doc-builder/.github/workflows/build_pr_documentation.yml](https://github.com/huggingface/doc-builder) | `e60a538eea9817ab312196d0d233604b01697265` | `af49b8dc3859c3d80c7220c8c0ba146431132e49` |
| [anthropics/claude-code-action](https://github.com/anthropics/claude-code-action) | `1.0.155` | `1.0.193` |
| [huggingface/security-workflows/.github/workflows/codeql-reusable.yml](https://github.com/huggingface/security-workflows) | `1.1.0` | `1.2.0` |
| [actions/download-artifact](https://github.com/actions/download-artifact) | `4.3.0` | `8.0.1` |
| [actions/labeler](https://github.com/actions/labeler) | `5.0.0` | `7.0.0` |
| [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) | `1.13.0` | `1.14.2` |
| [huggingface/tailscale-action](https://github.com/huggingface/tailscale-action) | `7d53c9737e53934c30290b5524d1c9b4a7c98c8a` | `07d36e4c3b68707bd3fe070dbb96f03f993a6efd` |
| [trufflesecurity/trufflehog](https://github.com/trufflesecurity/trufflehog) | `3.94.2` | `3.97.0` |
| [crate-ci/typos](https://github.com/crate-ci/typos) | `1.42.1` | `1.49.0` |
| [huggingface/doc-builder/.github/workflows/upload_pr_documentation.yml](https://github.com/huggingface/doc-builder) | `9ad2de8582b56c017cb530c1165116d40433f1c6` | `af49b8dc3859c3d80c7220c8c0ba146431132e49` |


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

Updates `actions/upload-artifact` from 4.6.2 to 7.0.1
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.6.2...v7.0.1)

Updates `docker/setup-buildx-action` from 3.12.0 to 4.2.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](8d2750c68a...bb05f3f551)

Updates `docker/login-action` from 3.7.0 to 4.6.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](c94ce9fb46...dbcb813823)

Updates `docker/build-push-action` from 6.19.2 to 7.3.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](10e90e3645...53b7df96c9)

Updates `huggingface/doc-builder/.github/workflows/build_main_documentation.yml` from e60a538eea9817ab312196d0d233604b01697265 to af49b8dc3859c3d80c7220c8c0ba146431132e49
- [Release notes](https://github.com/huggingface/doc-builder/releases)
- [Commits](e60a538eea...af49b8dc38)

Updates `actions/setup-python` from 5.6.0 to 7.0.0
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5.6.0...v7)

Updates `huggingface/doc-builder/.github/workflows/build_pr_documentation.yml` from e60a538eea9817ab312196d0d233604b01697265 to af49b8dc3859c3d80c7220c8c0ba146431132e49
- [Release notes](https://github.com/huggingface/doc-builder/releases)
- [Commits](e60a538eea...af49b8dc38)

Updates `anthropics/claude-code-action` from 1.0.155 to 1.0.193
- [Release notes](https://github.com/anthropics/claude-code-action/releases)
- [Commits](80b3182633...9d7150bc8a)

Updates `huggingface/security-workflows/.github/workflows/codeql-reusable.yml` from 1.1.0 to 1.2.0
- [Release notes](https://github.com/huggingface/security-workflows/releases)
- [Commits](dc6ca34688...e0d40aaf3c)

Updates `actions/download-artifact` from 4.3.0 to 8.0.1
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4.3.0...3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c)

Updates `actions/labeler` from 5.0.0 to 7.0.0
- [Release notes](https://github.com/actions/labeler/releases)
- [Commits](8558fd7429...bf12e9b00b)

Updates `pypa/gh-action-pypi-publish` from 1.13.0 to 1.14.2
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](ed0c53931b...dc37677b2e)

Updates `huggingface/tailscale-action` from 7d53c9737e53934c30290b5524d1c9b4a7c98c8a to 07d36e4c3b68707bd3fe070dbb96f03f993a6efd
- [Release notes](https://github.com/huggingface/tailscale-action/releases)
- [Commits](7d53c9737e...07d36e4c3b)

Updates `trufflesecurity/trufflehog` from 3.94.2 to 3.97.0
- [Release notes](https://github.com/trufflesecurity/trufflehog/releases)
- [Commits](6bd2d14f7a...bcfcf73aaf)

Updates `crate-ci/typos` from 1.42.1 to 1.49.0
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](65120634e7...8a48f81b6c)

Updates `huggingface/doc-builder/.github/workflows/upload_pr_documentation.yml` from 9ad2de8582b56c017cb530c1165116d40433f1c6 to af49b8dc3859c3d80c7220c8c0ba146431132e49
- [Release notes](https://github.com/huggingface/doc-builder/releases)
- [Commits](9ad2de8582...af49b8dc38)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: docker/login-action
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: docker/build-push-action
  dependency-version: 7.3.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: huggingface/doc-builder/.github/workflows/build_main_documentation.yml
  dependency-version: af49b8dc3859c3d80c7220c8c0ba146431132e49
  dependency-type: direct:production
  dependency-group: actions
- dependency-name: actions/setup-python
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml
  dependency-version: af49b8dc3859c3d80c7220c8c0ba146431132e49
  dependency-type: direct:production
  dependency-group: actions
- dependency-name: anthropics/claude-code-action
  dependency-version: 1.0.193
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: huggingface/security-workflows/.github/workflows/codeql-reusable.yml
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: actions/download-artifact
  dependency-version: 8.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/labeler
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: pypa/gh-action-pypi-publish
  dependency-version: 1.14.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: huggingface/tailscale-action
  dependency-version: 07d36e4c3b68707bd3fe070dbb96f03f993a6efd
  dependency-type: direct:production
  dependency-group: actions
- dependency-name: trufflesecurity/trufflehog
  dependency-version: 3.97.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: crate-ci/typos
  dependency-version: 1.49.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: huggingface/doc-builder/.github/workflows/upload_pr_documentation.yml
  dependency-version: af49b8dc3859c3d80c7220c8c0ba146431132e49
  dependency-type: direct:production
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Scope GITHUB_TOKEN permissions per job (#16)

A job with no `permissions:` block inherits whatever the repository
hands out. Each block added here grants what that job's own steps
need and nothing more.

Co-authored-by: hf-security-analysis[bot] <265538906+hf-security-analysis[bot]@users.noreply.github.com>

* Pin GitHub Actions to commit SHAs (#17)

A tag is mutable: whoever controls the action's repository can move
`v4` to different code, and the next run picks it up with no diff to
review. Pinning to the commit SHA freezes the code that runs today.

Co-authored-by: hf-security-analysis[bot] <265538906+hf-security-analysis[bot]@users.noreply.github.com>

* Bump doc-builder workflow pin (#22)

* Bump doc-builder pin to 9fc8a41 in build_documentation.yml

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Bump doc-builder pin to 9fc8a41 in build_pr_documentation.yml

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Bump doc-builder pin to 9fc8a41 in upload_pr_documentation.yml

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* Add FLUX.3 image: Flux3Transformer2DModel and modular text-to-image / image-to-image pipeline

Adds the FLUX.3 image transformer, the modular pipeline (text-to-image and image-to-image with one
prompt per call and one or more reference images), Flux3ImageProcessor, the converter for BFL's
model package, tests and docs. Verified against BFL's reference on the real checkpoint
(integration-notes/parity_bf16.py, temporary folder).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* integration-notes: ship the knight example image with the doc examples

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* integration-notes: drop the tokenizer config workaround (not needed with transformers 5.x)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Address review: rename the mode channel dict and the final norm, name the reference workflow
image_conditioned, document the guidance scale

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Add the step-distilled FLUX.3 image model (flow map)

- transformer: `flow_map` config and `target_timestep_embedder`; forward takes
  `target_timestep` (the timestep each step jumps to), added to the modulation
  of every mode and the final layer
- convert script: map `ref_time_in.*` and read `flow_map` from the package config
- modular: dedicated `Flux3Distilled*` blocks (text encoder, text input, text
  RoPE inputs, denoiser) with no guider and no negative prompt, 16 steps by
  default; `Flux3DistilledAutoBlocks` and `Flux3DistilledModularPipeline`
- tests for the flow-map transformer path and the distilled modular pipeline
- docs: step-distilled section and autodoc entries

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Add the step-distilled examples script

Same four cases as flux3_examples.py, run against the distilled Hub repo.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* feat: add a single file support in Flux3 image. (#32)

* Warn about the denoiser input fields that are not passed to the transformer

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Document reproducible generation with user-provided latents

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Run the QK norm and RoPE in fp32 with a single cast back, matching the BFL implementation

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* parity script: run against the Hub repos and print the imported diffusers source

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* Wrap a forward docstring line for the doc-builder style check

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* support lora loading (including ostris) in flux3 image pipeline. (#25)

* feat: support loading and performing inference with single-file formatted FP8 and NVFP4 checkpoints (#24)

* feat: support loading siungle file format checkpoints including quants.

* remove updates.patch

* remove fluff.

* feat: add a single file support in Flux3 image.

* bfl prefix

* version guard.

* prpoagate changes.

* add tests

* doc mods and remove integration notes

* remove script for quant

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

* add LoRA training for Flux 3 image small

* tests: clarify FLUX3_TINY_REPO comment

The tiny repo is uploaded to hf-internal-testing and the env var is a
local-dev escape hatch, not a pre-PR TODO.

* examples/flux3: drop unused args and dead class

Remove the text-encoder weight-decay arg from both trainers (neither
trains the text encoder) and, from the img2img trainer, the
prior-preservation leftovers (--class_data_dir, --sample_batch_size,
PromptDataset) that it never uses.

* bump yarn example rank to 32 in flux3 small readme

* Document <ref_image_n> references and layout prompts for FLUX.3

Matches the prompt grammar in BFL's reference inference code (bfl-hf-flux3-confidential#3): references are addressed as <ref_image_n> in the order passed to image, and regions can be placed with a trailing JSON list of id / bbox / desc (from / src_bbox / tgt_bbox when editing).

* examples/flux3: set klein model id, document ref image prompts, clean leftover refs

* point flux3 image docs at the klein release model ids

* fix: TestFlux3DistilledModularPipelineWorkflow::test_workflow_defaults test (#35)

* fix checkpoint ids in the flux3 docs. (#38)

* [docs] update pipe loading with vae fp32 dtype (#39)

* update pipe loading with vae fp32 dtype

* updating `load_componenets`

* use actual ckpts. (#42)

* flux3 klein example: rename from "small" and fix caption dropout for long prompts (#41)

* examples/flux3: rename small trainer, tests and readme files to klein

* examples/flux3: fix caption dropout shape mismatch for prompts longer than the pad window

* examples/flux3: add caption dropout to the i2i trainer

* examples/flux3: complete the klein rename (test class names, example output dirs)

* examples/flux3: guard against train_batch_size>1 with variable-length captions

* update flux3.md (#43)

* examples/flux3: support train_batch_size>1 and prior preservation with variable-length captions (#44)

* doc

* autodocstrings.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: hf-security-analysis[bot] <265538906+hf-security-analysis[bot]@users.noreply.github.com>
Co-authored-by: Pauline Bailly-Masson <155966238+paulinebm@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: yiyixuxu <yixu310@gmail.com>
Co-authored-by: linoytsaban <linoy.tsaban@gmail.com>
Co-authored-by: apolinário <joaopaulo.passos@gmail.com>
Co-authored-by: Linoy Tsaban <57615435+linoytsaban@users.noreply.github.com>
Co-authored-by: Aritra Roy Gosthipaty <aritra.born2fly@gmail.com>
Co-authored-by: Yumeng Li <109459218+YumengLi007@users.noreply.github.com>
2026-10-15 22:00:50 +05:30

2113 lines
90 KiB
Python

# coding=utf-8
# Copyright 2026 HuggingFace 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.
import gc
import re
import tempfile
import pytest
import safetensors.torch
import torch
from diffusers import (
AutoRoundConfig,
BFLQuantizationConfig,
BitsAndBytesConfig,
GGUFQuantizationConfig,
NunchakuLiteQuantizationConfig,
NVIDIAModelOptConfig,
SDNQConfig,
TorchAoConfig,
)
from diffusers.loaders.single_file_model import SINGLE_FILE_LOADABLE_CLASSES, _get_single_file_loadable_mapping_class
from diffusers.utils.import_utils import (
is_bitsandbytes_available,
is_flashinfer_available,
is_gguf_available,
is_kernels_available,
is_nvidia_modelopt_available,
is_peft_available,
is_torchao_available,
)
from ...testing_utils import (
assert_tensors_close,
backend_empty_cache,
is_autoround,
is_bfl,
is_bitsandbytes,
is_gguf,
is_modelopt,
is_quantization,
is_sdnq,
is_torch_compile,
is_torchao,
require_accelerate,
require_accelerator,
require_auto_round_version_greater_or_equal,
require_bitsandbytes_version_greater,
require_gguf_version_greater_or_equal,
require_modelopt_version_greater_or_equal,
require_sdnq,
require_torchao_version_greater_or_equal,
torch_device,
)
from .single_file import fetch_checkpoint_metadata
if is_peft_available():
from peft import LoraConfig
from diffusers.loaders.peft import PeftAdapterMixin
if is_nvidia_modelopt_available():
import modelopt.torch.quantization as mtq
if is_bitsandbytes_available():
import bitsandbytes as bnb
if is_gguf_available():
pass
if is_torchao_available():
import torchao.quantization as _torchao_quantization
class LoRALayer(torch.nn.Module):
"""Wraps a linear layer with LoRA-like adapter - Used for testing purposes only.
Taken from
https://github.com/huggingface/transformers/blob/566302686a71de14125717dea9a6a45b24d42b37/tests/quantization/bnb/test_4bit.py#L62C5-L78C77
"""
def __init__(self, module: torch.nn.Module, rank: int):
super().__init__()
self.module = module
self.adapter = torch.nn.Sequential(
torch.nn.Linear(module.in_features, rank, bias=False),
torch.nn.Linear(rank, module.out_features, bias=False),
)
small_std = (2.0 / (5 * min(module.in_features, module.out_features))) ** 0.5
torch.nn.init.normal_(self.adapter[0].weight, std=small_std)
torch.nn.init.zeros_(self.adapter[1].weight)
self.adapter.to(module.weight.device)
def forward(self, input, *args, **kwargs):
return self.module(input, *args, **kwargs) + self.adapter(input)
@is_quantization
@require_accelerator
class QuantizationTesterMixin:
"""
Base mixin class providing common test implementations for quantization testing.
Backend-specific mixins should:
1. Implement _create_quantized_model(config_kwargs)
2. Implement _verify_if_layer_quantized(name, module, config_kwargs)
3. Define their config dict (e.g., BNB_CONFIGS, TORCHAO_CONFIGS, etc.)
4. Use @pytest.mark.parametrize to create tests that call the common test methods below
Expected class attributes:
- model_class: The model class to test
- pretrained_model_name_or_path: Hub repository ID for the pretrained model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained (e.g., {"subfolder": "transformer"})
Expected methods in test classes:
- get_dummy_inputs(): Returns dict of inputs to pass to the model forward pass
"""
def setup_method(self):
gc.collect()
backend_empty_cache(torch_device)
def teardown_method(self):
gc.collect()
backend_empty_cache(torch_device)
def _create_quantized_model(self, config_kwargs, **extra_kwargs):
"""
Create a quantized model with the given config kwargs.
Args:
config_kwargs: Quantization config parameters
**extra_kwargs: Additional kwargs to pass to from_pretrained (e.g., device_map, offload_folder)
"""
raise NotImplementedError("Subclass must implement _create_quantized_model")
def _verify_if_layer_quantized(self, name, module, config_kwargs):
raise NotImplementedError("Subclass must implement _verify_if_layer_quantized")
def _is_module_quantized(self, module, config_kwargs=None):
"""
Check if a module is quantized. Returns True if quantized, False otherwise.
Default implementation tries _verify_if_layer_quantized and catches exceptions. Backends whose
verifier depends on the quantization config (e.g. bnb's 4-bit/8-bit split) need config_kwargs.
Subclasses can override for more efficient checking.
"""
try:
self._verify_if_layer_quantized("", module, config_kwargs or {})
return True
except (AssertionError, AttributeError):
return False
def _load_unquantized_model(self):
kwargs = getattr(self, "pretrained_model_kwargs", {})
return self.model_class.from_pretrained(self.pretrained_model_name_or_path, **kwargs)
def _test_quantization_num_parameters(self, config_kwargs):
model = self._load_unquantized_model()
num_params = model.num_parameters()
model_quantized = self._create_quantized_model(config_kwargs)
num_params_quantized = model_quantized.num_parameters()
assert num_params == num_params_quantized, (
f"Parameter count mismatch: unquantized={num_params}, quantized={num_params_quantized}"
)
def _test_quantization_memory_footprint(self, config_kwargs, expected_memory_reduction=1.2):
model = self._load_unquantized_model()
mem = model.get_memory_footprint()
model_quantized = self._create_quantized_model(config_kwargs)
mem_quantized = model_quantized.get_memory_footprint()
ratio = mem / mem_quantized
assert ratio >= expected_memory_reduction, (
f"Memory ratio {ratio:.2f} is less than expected ({expected_memory_reduction}x). unquantized={mem}, quantized={mem_quantized}"
)
@torch.no_grad()
def _test_quantization_inference(self, config_kwargs):
model_quantized = self._create_quantized_model(config_kwargs)
model_quantized.to(torch_device)
inputs = self.get_dummy_inputs()
output = model_quantized(**inputs, return_dict=False)[0]
assert output is not None, "Model output is None"
assert not torch.isnan(output).any(), "Model output contains NaN"
def _test_quantization_dtype_assignment(self, config_kwargs):
model = self._create_quantized_model(config_kwargs)
with pytest.raises(ValueError):
model.to(torch.float16)
with pytest.raises(ValueError):
device_0 = f"{torch_device}:0"
model.to(device=device_0, dtype=torch.float16)
with pytest.raises(ValueError):
model.float()
with pytest.raises(ValueError):
model.half()
model.to(torch_device)
@torch.no_grad()
def _test_quantization_lora_inference(self, config_kwargs):
if not is_peft_available():
pytest.skip("peft is not available")
if not issubclass(self.model_class, PeftAdapterMixin):
pytest.skip(f"PEFT is not supported for this model ({self.model_class.__name__})")
model = self._create_quantized_model(config_kwargs)
lora_config = LoraConfig(
r=4,
lora_alpha=4,
target_modules=["to_q", "to_k", "to_v", "to_out.0"],
init_lora_weights=False,
)
model.add_adapter(lora_config)
# Move LoRA adapter weights to device (they default to CPU)
model.to(torch_device)
inputs = self.get_dummy_inputs()
output = model(**inputs, return_dict=False)[0]
assert output is not None, "Model output is None with LoRA"
assert not torch.isnan(output).any(), "Model output contains NaN with LoRA"
# Backends opt into the sharded-serialization test by setting this to a quantization config dict.
sharded_serialization_config = None
def test_quantization_sharded_serialization(self, tmp_path):
if self.sharded_serialization_config is None:
pytest.skip("sharded_serialization_config not defined for this backend")
self._test_quantization_serialization(self.sharded_serialization_config, tmp_path, max_shard_size="auto")
@torch.no_grad()
def _test_quantization_serialization(self, config_kwargs, tmp_path, max_shard_size=None):
"""
Test that a quantized model can be saved and reloaded without changing its outputs.
Args:
config_kwargs: Quantization config parameters
tmp_path: Directory the model is serialized into
max_shard_size: When set, the checkpoint is sharded and the shard/index files are checked.
"auto" derives a size from the model footprint that yields a handful of shards; sizes
far below the largest tensor can split a weight from its quantization components
(e.g. bnb's SCB), which the shard-by-shard loader does not support.
"""
model = self._create_quantized_model(config_kwargs)
model.to(torch_device)
inputs = self.get_dummy_inputs()
expected_output = model(**inputs, return_dict=False)[0].detach().cpu()
if max_shard_size == "auto":
max_shard_size = max(int(model.get_memory_footprint() // 2), 1)
save_kwargs = {"safe_serialization": True}
if max_shard_size is not None:
save_kwargs["max_shard_size"] = max_shard_size
model.save_pretrained(str(tmp_path), **save_kwargs)
del model
gc.collect()
backend_empty_cache(torch_device)
if max_shard_size is not None:
assert len(list(tmp_path.glob("*.safetensors"))) > 1, "Expected a sharded safe-serialization checkpoint."
assert any(path.name.endswith(".index.json") for path in tmp_path.iterdir()), (
"Expected an index file for sharded safe checkpoint."
)
model_loaded = self.model_class.from_pretrained(str(tmp_path), device_map=str(torch_device))
output = model_loaded(**inputs, return_dict=False)[0].detach().cpu()
assert_tensors_close(output, expected_output, rtol=1e-3, atol=1e-3)
def _test_quantization_config_serialization(self, config_kwargs):
"""
Test that the quantization config attached to a quantized model is serializable.
Args:
config_kwargs: Quantization config parameters
"""
model = self._create_quantized_model(config_kwargs)
assert "quantization_config" in model.config, "Missing quantization_config"
_ = model.config["quantization_config"].to_dict()
_ = model.config["quantization_config"].to_diff_dict()
_ = model.config["quantization_config"].to_json_string()
def _test_original_dtype(self, config_kwargs):
"""
Test that the dtype the model had before quantization is recorded on its config.
Args:
config_kwargs: Quantization config parameters
"""
model = self._create_quantized_model(config_kwargs)
assert "_pre_quantization_dtype" in model.config, "Missing _pre_quantization_dtype"
assert model.config["_pre_quantization_dtype"] in [
torch.float16,
torch.float32,
torch.bfloat16,
], f"Unexpected dtype: {model.config['_pre_quantization_dtype']}"
def _test_quantized_layers(self, config_kwargs):
model_fp = self._load_unquantized_model()
num_linear_layers = sum(1 for module in model_fp.modules() if isinstance(module, torch.nn.Linear))
model_quantized = self._create_quantized_model(config_kwargs)
num_fp32_modules = 0
if hasattr(model_quantized, "_keep_in_fp32_modules") and model_quantized._keep_in_fp32_modules:
for name, module in model_quantized.named_modules():
if isinstance(module, torch.nn.Linear):
if any(fp32_name in name for fp32_name in model_quantized._keep_in_fp32_modules):
num_fp32_modules += 1
expected_quantized_layers = num_linear_layers - num_fp32_modules
num_quantized_layers = 0
for name, module in model_quantized.named_modules():
if isinstance(module, torch.nn.Linear):
if hasattr(model_quantized, "_keep_in_fp32_modules") and model_quantized._keep_in_fp32_modules:
if any(fp32_name in name for fp32_name in model_quantized._keep_in_fp32_modules):
continue
self._verify_if_layer_quantized(name, module, config_kwargs)
num_quantized_layers += 1
assert num_quantized_layers > 0, (
f"No quantized layers found in model (expected {expected_quantized_layers} linear layers, {num_fp32_modules} kept in FP32)"
)
assert num_quantized_layers == expected_quantized_layers, (
f"Quantized layer count mismatch: expected {expected_quantized_layers}, got {num_quantized_layers} (total linear layers: {num_linear_layers}, FP32 modules: {num_fp32_modules})"
)
def _test_keep_modules_in_fp32(self, config_kwargs):
"""
Test that modules listed in the model's `_keep_in_fp32_modules` stay in FP32 after quantization.
Args:
config_kwargs: Quantization config parameters
"""
fp32_modules = getattr(self.model_class, "_keep_in_fp32_modules", None)
if not fp32_modules:
pytest.skip(f"{self.model_class.__name__} does not declare _keep_in_fp32_modules")
model = self._create_quantized_model(config_kwargs)
model.to(torch_device)
for name, module in model.named_modules():
if isinstance(module, torch.nn.Linear) and any(fp32_name in name for fp32_name in fp32_modules):
assert module.weight.dtype == torch.float32, (
f"Module {name} should be FP32 but is {module.weight.dtype}"
)
@torch.no_grad()
def _test_quantization_modules_to_not_convert(
self, config_kwargs, modules_to_not_convert, exclusion_key="modules_to_not_convert"
):
"""
Test that modules specified in modules_to_not_convert are not quantized.
Args:
config_kwargs: Base quantization config kwargs
modules_to_not_convert: List of module names to exclude from quantization
exclusion_key: Name of the config parameter carrying the exclusion list
(BitsAndBytesConfig calls it `llm_int8_skip_modules`)
"""
# Create config with the exclusion list
config_kwargs_with_exclusion = config_kwargs.copy()
config_kwargs_with_exclusion[exclusion_key] = modules_to_not_convert
model_with_exclusion = self._create_quantized_model(config_kwargs_with_exclusion)
# Find a module that should NOT be quantized
found_excluded = False
for name, module in model_with_exclusion.named_modules():
if isinstance(module, torch.nn.Linear):
# Check if this module is in the exclusion list
if any(excluded in name for excluded in modules_to_not_convert):
found_excluded = True
# This module should NOT be quantized
assert not self._is_module_quantized(module, config_kwargs), (
f"Module {name} should not be quantized but was found to be quantized"
)
assert found_excluded, f"No linear layers found in excluded modules: {modules_to_not_convert}"
# Find a module that SHOULD be quantized (not in exclusion list)
found_quantized = False
for name, module in model_with_exclusion.named_modules():
if isinstance(module, torch.nn.Linear):
# Check if this module is NOT in the exclusion list
if not any(excluded in name for excluded in modules_to_not_convert):
if self._is_module_quantized(module, config_kwargs):
found_quantized = True
break
assert found_quantized, "No quantized layers found outside of excluded modules"
# Inference must work on the mixed model: excluded modules run in the compute dtype next to
# quantized ones (excluded linears do strict-dtype matmuls).
model_with_exclusion.to(torch_device)
output = model_with_exclusion(**self.get_dummy_inputs(), return_dict=False)[0]
assert output is not None, "Model output is None"
assert not torch.isnan(output).any(), "Model output contains NaN"
# Compare memory footprint with fully quantized model
model_fully_quantized = self._create_quantized_model(config_kwargs)
mem_with_exclusion = model_with_exclusion.get_memory_footprint()
mem_fully_quantized = model_fully_quantized.get_memory_footprint()
assert mem_with_exclusion > mem_fully_quantized, (
f"Model with exclusions should be larger. With exclusion: {mem_with_exclusion}, fully quantized: {mem_fully_quantized}"
)
@torch.no_grad()
def _test_quantization_device_map(self, config_kwargs):
"""
Test that quantized models work correctly with device_map="auto".
Args:
config_kwargs: Base quantization config kwargs
"""
model = self._create_quantized_model(config_kwargs, device_map="auto")
assert hasattr(model, "hf_device_map"), "Model should have hf_device_map attribute"
assert model.hf_device_map is not None, "hf_device_map should not be None"
map_devices = {torch.device(d).type for d in model.hf_device_map.values()}
for kind, named_tensors in (("parameter", model.named_parameters()), ("buffer", model.named_buffers())):
for name, tensor in named_tensors:
assert tensor.device.type != "meta", f"{kind} {name} was left on the meta device"
if len(map_devices) == 1:
assert tensor.device.type == next(iter(map_devices)), (
f"Expected device {next(iter(map_devices))} for {kind} {name}, got {tensor.device}"
)
inputs = self.get_dummy_inputs()
output = model(**inputs, return_dict=False)[0]
assert output is not None, "Model output is None"
assert not torch.isnan(output).any(), "Model output contains NaN"
def _test_quantization_cpu_device_map(self, config_kwargs):
"""
Test that quantized models are placed on the CPU with device_map="cpu".
Args:
config_kwargs: Base quantization config kwargs
"""
model_quantized = self._create_quantized_model(config_kwargs, device_map="cpu")
assert hasattr(model_quantized, "hf_device_map"), "Model should have hf_device_map attribute"
assert model_quantized.hf_device_map is not None, "hf_device_map should not be None"
assert model_quantized.device == torch.device("cpu"), (
f"Model should be on CPU, but is on {model_quantized.device}"
)
@torch.no_grad()
def _test_dequantize(self, config_kwargs):
"""
Test that dequantize() converts quantized model back to standard linear layers.
Args:
config_kwargs: Quantization config parameters
"""
model = self._create_quantized_model(config_kwargs)
model.to(torch_device)
if not hasattr(model, "dequantize"):
pytest.skip("Model does not have dequantize method")
model.dequantize()
for name, module in model.named_modules():
if isinstance(module, torch.nn.Linear):
assert not self._is_module_quantized(module, config_kwargs), (
f"Module {name} is still quantized after dequantize()"
)
inputs = self.get_dummy_inputs()
output = model(**inputs, return_dict=False)[0]
assert output is not None, "Model output is None after dequantization"
assert not torch.isnan(output).any(), "Model output contains NaN after dequantization"
def _test_quantization_training(self, config_kwargs):
"""
Test that quantized models can be used for training with LoRA-like adapters.
This test:
1. Freezes all model parameters
2. Casts small parameters (e.g., layernorm) to fp32 for stability
3. Adds LoRA adapters to attention layers
4. Runs forward and backward passes
5. Verifies gradients are computed correctly
Args:
config_kwargs: Quantization config parameters
"""
model = self._create_quantized_model(config_kwargs)
# Step 1: freeze all parameters
for param in model.parameters():
param.requires_grad = False
if param.ndim == 1:
# cast small parameters (e.g. layernorm) to fp32 for stability
param.data = param.data.to(torch.float32)
# Step 2: add adapters to attention layers
adapter_count = 0
for _, module in model.named_modules():
if "Attention" in repr(type(module)):
if hasattr(module, "to_k"):
module.to_k = LoRALayer(module.to_k, rank=4)
adapter_count += 1
if hasattr(module, "to_q"):
module.to_q = LoRALayer(module.to_q, rank=4)
adapter_count += 1
if hasattr(module, "to_v"):
module.to_v = LoRALayer(module.to_v, rank=4)
adapter_count += 1
if adapter_count == 0:
pytest.skip("No attention layers found in model for adapter training test")
# Step 3: run forward and backward pass
inputs = self.get_dummy_inputs()
# Use bfloat16 on XPU and for bfloat16 models to avoid gradient underflow with quantized layers
use_bf16 = torch_device == "xpu" or getattr(self, "torch_dtype", None) == torch.bfloat16
autocast_dtype = torch.bfloat16 if use_bf16 else torch.float16
with torch.amp.autocast(torch_device, dtype=autocast_dtype):
out = model(**inputs, return_dict=False)[0]
out.norm().backward()
# Step 4: verify gradients are computed
for module in model.modules():
if isinstance(module, LoRALayer):
assert module.adapter[1].weight.grad is not None, "LoRA adapter gradient is None"
assert module.adapter[1].weight.grad.norm().item() > 0, "LoRA adapter gradient norm is zero"
@is_quantization
@is_bitsandbytes
@require_accelerator
@require_bitsandbytes_version_greater("0.43.2")
@require_accelerate
class BitsAndBytesConfigMixin:
"""
Base mixin providing BitsAndBytes quantization config and model creation.
Expected class attributes:
- model_class: The model class to test
- pretrained_model_name_or_path: Hub repository ID for the pretrained model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained
"""
BNB_CONFIGS = {
"4bit_nf4": {
"load_in_4bit": True,
"bnb_4bit_quant_type": "nf4",
"bnb_4bit_compute_dtype": torch.float16,
},
"4bit_fp4": {
"load_in_4bit": True,
"bnb_4bit_quant_type": "fp4",
"bnb_4bit_compute_dtype": torch.float16,
},
"8bit": {
"load_in_8bit": True,
},
}
BNB_EXPECTED_MEMORY_REDUCTIONS = {
"4bit_nf4": 3.0,
"4bit_fp4": 3.0,
"8bit": 1.5,
}
sharded_serialization_config = BNB_CONFIGS["8bit"]
def _create_quantized_model(self, config_kwargs, **extra_kwargs):
config = BitsAndBytesConfig(**config_kwargs)
kwargs = getattr(self, "pretrained_model_kwargs", {}).copy()
kwargs["quantization_config"] = config
kwargs.update(extra_kwargs)
return self.model_class.from_pretrained(self.pretrained_model_name_or_path, **kwargs)
def _verify_if_layer_quantized(self, name, module, config_kwargs):
expected_weight_class = bnb.nn.Params4bit if config_kwargs.get("load_in_4bit") else bnb.nn.Int8Params
assert module.weight.__class__ == expected_weight_class, (
f"Layer {name} has weight type {module.weight.__class__}, expected {expected_weight_class}"
)
@is_bitsandbytes
@require_accelerator
@require_bitsandbytes_version_greater("0.43.2")
@require_accelerate
class BitsAndBytesTesterMixin(BitsAndBytesConfigMixin, QuantizationTesterMixin):
"""
Mixin class for testing BitsAndBytes quantization on models.
Expected class attributes:
- model_class: The model class to test
- pretrained_model_name_or_path: Hub repository ID for the pretrained model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained (e.g., {"subfolder": "transformer"})
Expected methods to be implemented by subclasses:
- get_dummy_inputs(): Returns dict of inputs to pass to the model forward pass
Optional class attributes:
- BNB_CONFIGS: Dict of config name -> BitsAndBytesConfig kwargs to test
Pytest mark: bitsandbytes
Use `pytest -m "not bitsandbytes"` to skip these tests
"""
@pytest.mark.parametrize(
"config_name",
list(BitsAndBytesConfigMixin.BNB_CONFIGS.keys()),
ids=list(BitsAndBytesConfigMixin.BNB_CONFIGS.keys()),
)
def test_bnb_quantization_num_parameters(self, config_name):
self._test_quantization_num_parameters(BitsAndBytesConfigMixin.BNB_CONFIGS[config_name])
@pytest.mark.parametrize(
"config_name",
list(BitsAndBytesConfigMixin.BNB_CONFIGS.keys()),
ids=list(BitsAndBytesConfigMixin.BNB_CONFIGS.keys()),
)
def test_bnb_quantization_memory_footprint(self, config_name):
expected = self.BNB_EXPECTED_MEMORY_REDUCTIONS.get(config_name, 1.2)
self._test_quantization_memory_footprint(
BitsAndBytesConfigMixin.BNB_CONFIGS[config_name], expected_memory_reduction=expected
)
@pytest.mark.parametrize(
"config_name",
list(BitsAndBytesConfigMixin.BNB_CONFIGS.keys()),
ids=list(BitsAndBytesConfigMixin.BNB_CONFIGS.keys()),
)
def test_bnb_quantization_inference(self, config_name):
self._test_quantization_inference(BitsAndBytesConfigMixin.BNB_CONFIGS[config_name])
@pytest.mark.parametrize("config_name", ["4bit_nf4", "8bit"], ids=["4bit_nf4", "8bit"])
def test_bnb_quantization_dtype_assignment(self, config_name):
self._test_quantization_dtype_assignment(BitsAndBytesConfigMixin.BNB_CONFIGS[config_name])
def test_bnb_device_assignment(self):
"""Test that a 4-bit model moves between CPU and accelerator without changing its memory footprint."""
model = self._create_quantized_model(BitsAndBytesConfigMixin.BNB_CONFIGS["4bit_nf4"])
mem_before = model.get_memory_footprint()
model.to("cpu")
assert model.device.type == "cpu"
assert model.get_memory_footprint() == pytest.approx(mem_before)
model.to(torch_device)
assert model.device.type == torch.device(torch_device).type
assert model.get_memory_footprint() == pytest.approx(mem_before)
@pytest.mark.parametrize("config_name", ["4bit_nf4"], ids=["4bit_nf4"])
def test_bnb_quantization_lora_inference(self, config_name):
self._test_quantization_lora_inference(BitsAndBytesConfigMixin.BNB_CONFIGS[config_name])
@pytest.mark.parametrize(
"config_name",
list(BitsAndBytesConfigMixin.BNB_CONFIGS.keys()),
ids=list(BitsAndBytesConfigMixin.BNB_CONFIGS.keys()),
)
def test_bnb_quantization_serialization(self, config_name, tmp_path):
self._test_quantization_serialization(BitsAndBytesConfigMixin.BNB_CONFIGS[config_name], tmp_path)
@pytest.mark.parametrize(
"config_name",
list(BitsAndBytesConfigMixin.BNB_CONFIGS.keys()),
ids=list(BitsAndBytesConfigMixin.BNB_CONFIGS.keys()),
)
def test_bnb_quantized_layers(self, config_name):
self._test_quantized_layers(BitsAndBytesConfigMixin.BNB_CONFIGS[config_name])
@pytest.mark.parametrize(
"config_name",
list(BitsAndBytesConfigMixin.BNB_CONFIGS.keys()),
ids=list(BitsAndBytesConfigMixin.BNB_CONFIGS.keys()),
)
def test_bnb_quantization_config_serialization(self, config_name):
self._test_quantization_config_serialization(BitsAndBytesConfigMixin.BNB_CONFIGS[config_name])
def test_bnb_original_dtype(self):
self._test_original_dtype(BitsAndBytesConfigMixin.BNB_CONFIGS["4bit_nf4"])
def test_bnb_keep_modules_in_fp32(self):
self._test_keep_modules_in_fp32(BitsAndBytesConfigMixin.BNB_CONFIGS["4bit_nf4"])
@pytest.mark.parametrize("config_name", ["4bit_nf4", "8bit"], ids=["4bit_nf4", "8bit"])
def test_bnb_modules_to_not_convert(self, config_name):
"""Test module exclusion, which BitsAndBytesConfig exposes as `llm_int8_skip_modules` (despite the
name, it also applies to 4-bit quantization)."""
modules_to_exclude = getattr(self, "modules_to_not_convert_for_test", None)
if modules_to_exclude is None:
pytest.skip("modules_to_not_convert_for_test not defined for this model")
self._test_quantization_modules_to_not_convert(
BitsAndBytesConfigMixin.BNB_CONFIGS[config_name],
modules_to_exclude,
exclusion_key="llm_int8_skip_modules",
)
def test_bnb_errors_loading_incorrect_state_dict(self, tmp_path):
"""Test that loading a checkpoint with a corrupted quantized weight raises a helpful error."""
model = self._create_quantized_model(BitsAndBytesConfigMixin.BNB_CONFIGS["4bit_nf4"])
model.save_pretrained(str(tmp_path))
del model
gc.collect()
backend_empty_cache(torch_device)
weights_file = tmp_path / "diffusion_pytorch_model.safetensors"
state_dict = safetensors.torch.load_file(str(weights_file))
key_to_target = next(k for k in state_dict if k.endswith(".weight") and state_dict[k].dtype == torch.uint8)
corrupted_param = torch.randn(state_dict[key_to_target].shape[0] - 1, 1)
state_dict[key_to_target] = bnb.nn.Params4bit(corrupted_param, requires_grad=False)
safetensors.torch.save_file(state_dict, str(weights_file))
with pytest.raises(ValueError) as err_context:
_ = self.model_class.from_pretrained(str(tmp_path))
assert key_to_target in str(err_context.value)
@pytest.mark.parametrize("config_name", ["4bit_nf4", "8bit"], ids=["4bit_nf4", "8bit"])
def test_bnb_device_map(self, config_name):
"""Test that device_map='auto' works correctly with quantization."""
self._test_quantization_device_map(BitsAndBytesConfigMixin.BNB_CONFIGS[config_name])
def test_bnb_dequantize(self):
"""Test that dequantize() works correctly."""
self._test_dequantize(BitsAndBytesConfigMixin.BNB_CONFIGS["4bit_nf4"])
@pytest.mark.parametrize("config_name", ["4bit_nf4", "8bit"], ids=["4bit_nf4", "8bit"])
def test_bnb_training(self, config_name):
"""Test that quantized models can be used for training with adapters."""
self._test_quantization_training(BitsAndBytesConfigMixin.BNB_CONFIGS[config_name])
@pytest.mark.parametrize(
"config_name",
list(BitsAndBytesConfigMixin.BNB_CONFIGS.keys()),
ids=list(BitsAndBytesConfigMixin.BNB_CONFIGS.keys()),
)
def test_bnb_cpu_device_map(self, config_name):
"""Test that device_map='cpu' works correctly with quantization."""
self._test_quantization_cpu_device_map(BitsAndBytesConfigMixin.BNB_CONFIGS[config_name])
@is_quantization
@is_torchao
@require_accelerator
@require_torchao_version_greater_or_equal("0.7.0")
class TorchAoConfigMixin:
"""
Base mixin providing TorchAO quantization config and model creation.
Expected class attributes:
- model_class: The model class to test
- pretrained_model_name_or_path: Hub repository ID for the pretrained model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained
"""
TORCHAO_QUANT_TYPES = {
"int4wo": "Int4WeightOnlyConfig",
"int8wo": "Int8WeightOnlyConfig",
"int8dq": "Int8DynamicActivationInt8WeightConfig",
}
TORCHAO_EXPECTED_MEMORY_REDUCTIONS = {
"int4wo": 1.8,
"int8wo": 1.5,
"int8dq": 1.5,
}
@staticmethod
def _get_quant_config(config_name, modules_to_not_convert=None):
# Quant types that need constructor arguments (e.g. `FqnToConfig`) are passed in already built.
if not isinstance(config_name, str):
return TorchAoConfig(config_name, modules_to_not_convert=modules_to_not_convert)
config_cls = getattr(_torchao_quantization, config_name)
config_kwargs = {"version": 2}
# version=2 int4 defaults to the "plain" packing format, which routes through the
# fbgemm/mslk Int4Tensor kernels. Pin the packing format to the tinygemm
# (_convert_weight_to_int4pack) path on CUDA and plain_int32 on Intel XPU so the tests
# don't require those extra kernels to be installed.
if config_name == "Int4WeightOnlyConfig":
if torch_device == "xpu":
config_kwargs["int4_packing_format"] = "plain_int32"
elif torch_device == "cuda":
config_kwargs["int4_packing_format"] = "tile_packed_to_4d"
return TorchAoConfig(config_cls(**config_kwargs), modules_to_not_convert=modules_to_not_convert)
def _create_quantized_model(self, config_name, modules_to_not_convert=None, **extra_kwargs):
config = self._get_quant_config(config_name, modules_to_not_convert=modules_to_not_convert)
kwargs = getattr(self, "pretrained_model_kwargs", {}).copy()
kwargs["quantization_config"] = config
kwargs["device_map"] = str(torch_device)
kwargs.update(extra_kwargs)
return self.model_class.from_pretrained(self.pretrained_model_name_or_path, **kwargs)
def _verify_if_layer_quantized(self, name, module, config_kwargs):
from torchao.utils import TorchAOBaseTensor
assert isinstance(module, torch.nn.Linear), f"Layer {name} is not Linear, got {type(module)}"
assert isinstance(module.weight, TorchAOBaseTensor), (
f"Layer {name} weight is {type(module.weight)}, expected TorchAOBaseTensor"
)
# int4wo requires CUDA or XPU ops (_convert_weight_to_int4pack)
_int4wo_skip = pytest.mark.skipif(
torch_device not in ["cuda", "xpu"], reason="int4wo quantization requires CUDA or XPU"
)
# The CUDA int4 tinygemm path (Int4TilePackedTo4dTensor) does not implement aten.dequantize.
_int4wo_dequantize_skip = pytest.mark.skip(
reason="int4wo tinygemm packing (Int4TilePackedTo4dTensor) does not support dequantize"
)
@is_torchao
@require_accelerator
@require_torchao_version_greater_or_equal("0.7.0")
class TorchAoTesterMixin(TorchAoConfigMixin, QuantizationTesterMixin):
"""
Mixin class for testing TorchAO quantization on models.
Expected class attributes:
- model_class: The model class to test
- pretrained_model_name_or_path: Hub repository ID for the pretrained model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained (e.g., {"subfolder": "transformer"})
Expected methods to be implemented by subclasses:
- get_dummy_inputs(): Returns dict of inputs to pass to the model forward pass
Optional class attributes:
- TORCHAO_QUANT_TYPES: Dict of quantization type strings to test
Pytest mark: torchao
Use `pytest -m "not torchao"` to skip these tests
"""
@pytest.mark.parametrize(
"quant_type",
[
pytest.param("int4wo", marks=_int4wo_skip),
"int8wo",
"int8dq",
],
ids=["int4wo", "int8wo", "int8dq"],
)
def test_torchao_quantization_num_parameters(self, quant_type):
self._test_quantization_num_parameters(TorchAoConfigMixin.TORCHAO_QUANT_TYPES[quant_type])
@pytest.mark.parametrize(
"quant_type",
[
pytest.param("int4wo", marks=_int4wo_skip),
"int8wo",
"int8dq",
],
ids=["int4wo", "int8wo", "int8dq"],
)
def test_torchao_quantization_memory_footprint(self, quant_type):
expected = TorchAoConfigMixin.TORCHAO_EXPECTED_MEMORY_REDUCTIONS.get(quant_type, 1.2)
self._test_quantization_memory_footprint(
TorchAoConfigMixin.TORCHAO_QUANT_TYPES[quant_type], expected_memory_reduction=expected
)
@pytest.mark.parametrize(
"quant_type",
[
pytest.param("int4wo", marks=[_int4wo_skip, _int4wo_dequantize_skip]),
"int8wo",
"int8dq",
],
ids=["int4wo", "int8wo", "int8dq"],
)
def test_torchao_quantization_inference(self, quant_type):
self._test_quantization_inference(TorchAoConfigMixin.TORCHAO_QUANT_TYPES[quant_type])
@pytest.mark.parametrize("quant_type", ["int8wo"], ids=["int8wo"])
def test_torchao_quantized_layers(self, quant_type):
self._test_quantized_layers(TorchAoConfigMixin.TORCHAO_QUANT_TYPES[quant_type])
@pytest.mark.parametrize("quant_type", ["int8wo"], ids=["int8wo"])
def test_torchao_quantization_lora_inference(self, quant_type):
self._test_quantization_lora_inference(TorchAoConfigMixin.TORCHAO_QUANT_TYPES[quant_type])
@pytest.mark.parametrize("quant_type", ["int8wo"], ids=["int8wo"])
@require_torchao_version_greater_or_equal("0.16.0")
def test_torchao_quantization_serialization(self, quant_type, tmp_path):
self._test_quantization_serialization(TorchAoConfigMixin.TORCHAO_QUANT_TYPES[quant_type], tmp_path)
@pytest.mark.parametrize("quant_type", ["int8dq"], ids=["int8dq"])
@require_torchao_version_greater_or_equal("0.16.0")
def test_torchao_quantization_sharded_serialization(self, quant_type, tmp_path):
self._test_quantization_serialization(
TorchAoConfigMixin.TORCHAO_QUANT_TYPES[quant_type], tmp_path, max_shard_size="16KB"
)
def test_torchao_modules_to_not_convert(self):
"""Test that modules_to_not_convert parameter works correctly."""
modules_to_exclude = getattr(self, "modules_to_not_convert_for_test", None)
if modules_to_exclude is None:
pytest.skip("modules_to_not_convert_for_test not defined for this model")
# TorchAoConfig takes modules_to_not_convert directly (not inside the quant_type config),
# so this can't reuse the dict-based QuantizationTesterMixin helper.
model = self._create_quantized_model(
TorchAoConfigMixin.TORCHAO_QUANT_TYPES["int8wo"], modules_to_not_convert=modules_to_exclude
)
found_excluded = False
for name, module in model.named_modules():
if isinstance(module, torch.nn.Linear) and any(excluded in name for excluded in modules_to_exclude):
found_excluded = True
assert not self._is_module_quantized(module), (
f"Module {name} should not be quantized but was found to be quantized"
)
assert found_excluded, f"No linear layers found in excluded modules: {modules_to_exclude}"
# Inference must work on the mixed model (see _test_quantization_modules_to_not_convert).
with torch.no_grad():
output = model(**self.get_dummy_inputs(), return_dict=False)[0]
assert output is not None, "Model output is None"
assert not torch.isnan(output).any(), "Model output contains NaN"
def test_torchao_device_map(self):
"""Test that device_map='auto' works correctly with quantization."""
self._test_quantization_device_map(TorchAoConfigMixin.TORCHAO_QUANT_TYPES["int8wo"])
@torch.no_grad()
def test_torchao_cpu_disk_offload_device_map(self, tmp_path):
"""Test custom device maps with cpu/disk offload: offloaded modules stay unquantized, inference works."""
from torchao.utils import TorchAOBaseTensor
model = self._create_quantized_model(TorchAoConfigMixin.TORCHAO_QUANT_TYPES["int8wo"])
# Offload the first two linear-bearing top-level modules to cpu and disk, keep the rest on the
# accelerator. Root-level parameters and buffers (e.g. Wan's `scale_shift_table`) need their own
# device-map entries since they belong to no child module.
device_map = {}
offload_targets = []
for name, child in model.named_children():
if len(offload_targets) < 2 and any(isinstance(m, torch.nn.Linear) for m in child.modules()):
device_map[name] = "disk" if offload_targets else "cpu"
offload_targets.append(name)
else:
device_map[name] = str(torch_device)
for name, _ in list(model.named_parameters(recurse=False)) + list(model.named_buffers(recurse=False)):
device_map[name] = str(torch_device)
del model
gc.collect()
backend_empty_cache(torch_device)
if len(offload_targets) < 2:
pytest.skip("Model does not have enough linear-bearing top-level modules for offload testing")
model = self._create_quantized_model(
TorchAoConfigMixin.TORCHAO_QUANT_TYPES["int8wo"], device_map=device_map, offload_folder=str(tmp_path)
)
# Weights offloaded to cpu/disk are not quantized, only (some of) the weights on the accelerator
# are. Not every accelerator module is necessarily quantized: the offload exclusion matches
# module names by substring, so an offloaded `blocks` also excludes e.g. Wan's `vace_blocks`.
found_quantized = False
for name, module in model.named_modules():
if isinstance(module, torch.nn.Linear):
if name.split(".")[0] in offload_targets:
assert not isinstance(module.weight, TorchAOBaseTensor), (
f"Offloaded module {name} should not be quantized"
)
elif isinstance(module.weight, TorchAOBaseTensor):
found_quantized = True
assert found_quantized, "No quantized layers found outside the offloaded modules"
inputs = self.get_dummy_inputs()
output = model(**inputs, return_dict=False)[0]
assert output is not None, "Model output is None"
assert not torch.isnan(output).any(), "Model output contains NaN"
def test_torchao_dequantize(self):
"""Test that dequantize() works correctly."""
self._test_dequantize(TorchAoConfigMixin.TORCHAO_QUANT_TYPES["int8wo"])
def test_torchao_training(self):
"""Test that quantized models can be used for training with adapters."""
self._test_quantization_training(TorchAoConfigMixin.TORCHAO_QUANT_TYPES["int8wo"])
def test_torchao_keep_modules_in_fp32(self):
self._test_keep_modules_in_fp32(TorchAoConfigMixin.TORCHAO_QUANT_TYPES["int8wo"])
def test_torchao_fqn_to_config(self):
"""An `FqnToConfig` quant type must reach `quantize_` instead of tripping its `filter_fn` guard.
See https://github.com/huggingface/diffusers/issues/14667.
"""
quant_type = _torchao_quantization.FqnToConfig(
{"_default": _torchao_quantization.Int8WeightOnlyConfig(version=2)}
)
self._test_quantized_layers(quant_type)
def test_torchao_fqn_to_config_targets_named_layers(self):
"""Selective `FqnToConfig` targeting must land on the same layers as a plain `quantize_` call.
The quantizer resolves fqns one `nn.Linear` at a time, so `quantization_config=` is checked against
torchao's own whole-model pass. See https://github.com/huggingface/diffusers/issues/14667.
"""
keep_in_fp32 = getattr(self.model_class, "_keep_in_fp32_modules", None) or []
reference = self._load_unquantized_model().to(torch_device)
linear_fqns = [
name
for name, module in reference.named_modules()
if isinstance(module, torch.nn.Linear) and not any(fp32_name in name for fp32_name in keep_in_fp32)
]
if len(linear_fqns) < 2:
pytest.skip("Model does not have enough linear layers to test selective fqn quantization")
# One layer selected by its exact fqn, a set of them by a wildcard regex over the model's own fqns
# (the shape from the issue https://github.com/huggingface/diffusers/issues/14667), everything else
# left alone by `_default: None`. On Flux2, this gives
# exact_fqn="time_guidance_embed.timestep_embedder.linear_1" and
# pattern=".*proj_out" (matching "proj_out").
exact_fqn = linear_fqns[0]
pattern = f".*{re.escape(linear_fqns[-1].split('.')[-1])}"
fqn_to_config = {
exact_fqn: _torchao_quantization.Int8WeightOnlyConfig(version=2),
f"re:{pattern}": _torchao_quantization.Int8WeightOnlyConfig(version=2),
"_default": None,
}
# `quantize_` on the whole model is the reference: `quantization_config=` must place the same tensors.
_torchao_quantization.quantize_(reference, _torchao_quantization.FqnToConfig(fqn_to_config), filter_fn=None)
expected = {name: type(reference.get_submodule(name).weight) for name in linear_fqns}
del reference
assert set(expected.values()) != {torch.nn.Parameter}, "`quantize_` did not quantize any of the named layers"
model = self._create_quantized_model(_torchao_quantization.FqnToConfig(fqn_to_config))
quantized = {name: type(model.get_submodule(name).weight) for name in linear_fqns}
mismatched = {
name: (quantized[name], expected[name]) for name in linear_fqns if quantized[name] != expected[name]
}
assert not mismatched, f"Layers quantized through `quantization_config=` differ from `quantize_`: {mismatched}"
@is_quantization
@is_gguf
@require_accelerate
@require_accelerator
@require_gguf_version_greater_or_equal("0.10.0")
class GGUFConfigMixin:
"""
Base mixin providing GGUF quantization config and model creation.
Expected from config mixin:
- model_class: The model class to test
Required properties (must be implemented by subclasses):
- gguf_filename: URL or path to the GGUF file
"""
@property
def gguf_filename(self):
"""URL or path to the GGUF file. Must be implemented by subclasses."""
raise NotImplementedError("Subclasses must implement the `gguf_filename` property.")
def _create_quantized_model(self, config_kwargs=None, **extra_kwargs):
if config_kwargs is None:
config_kwargs = {"compute_dtype": torch.bfloat16}
config = GGUFQuantizationConfig(**config_kwargs)
kwargs = {
"quantization_config": config,
"torch_dtype": config_kwargs.get("compute_dtype", torch.bfloat16),
"device_map": str(torch_device),
}
kwargs.update(extra_kwargs)
return self.model_class.from_single_file(self.gguf_filename, **kwargs)
def _verify_if_layer_quantized(self, name, module, config_kwargs=None):
from diffusers.quantizers.gguf.utils import GGUFParameter
assert isinstance(module.weight, GGUFParameter), f"{name} weight is not GGUFParameter"
assert hasattr(module.weight, "quant_type"), f"{name} weight missing quant_type"
assert module.weight.dtype == torch.uint8, f"{name} weight dtype should be uint8"
@is_gguf
@require_accelerate
@require_accelerator
@require_gguf_version_greater_or_equal("0.10.0")
class GGUFTesterMixin(GGUFConfigMixin, QuantizationTesterMixin):
"""
Mixin class for testing GGUF quantization on models.
Expected from config mixin:
- model_class: The model class to test
Required properties (must be implemented by subclasses):
- gguf_filename: URL or path to the GGUF file
Expected methods from config mixin:
- get_dummy_inputs(): Returns dict of inputs to pass to the model forward pass
Pytest mark: gguf
Use `pytest -m "not gguf"` to skip these tests
"""
def test_gguf_quantization_inference(self):
self._test_quantization_inference({"compute_dtype": torch.bfloat16})
def test_gguf_keep_modules_in_fp32(self):
if not hasattr(self.model_class, "_keep_in_fp32_modules"):
pytest.skip(f"{self.model_class.__name__} does not have _keep_in_fp32_modules")
_keep_in_fp32_modules = self.model_class._keep_in_fp32_modules
self.model_class._keep_in_fp32_modules = ["proj_out"]
try:
self._test_keep_modules_in_fp32({"compute_dtype": torch.bfloat16})
finally:
self.model_class._keep_in_fp32_modules = _keep_in_fp32_modules
def test_gguf_quantization_dtype_assignment(self):
self._test_quantization_dtype_assignment({"compute_dtype": torch.bfloat16})
def test_gguf_quantization_lora_inference(self):
self._test_quantization_lora_inference({"compute_dtype": torch.bfloat16})
def test_gguf_dequantize(self):
"""Test that dequantize() works correctly."""
self._test_dequantize({"compute_dtype": torch.bfloat16})
@is_quantization
@is_modelopt
@require_accelerator
@require_accelerate
@require_modelopt_version_greater_or_equal("0.33.1")
class ModelOptConfigMixin:
"""
Base mixin providing NVIDIA ModelOpt quantization config and model creation.
Expected class attributes:
- model_class: The model class to test
- pretrained_model_name_or_path: Hub repository ID for the pretrained model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained
"""
MODELOPT_CONFIGS = {
"fp8": {"quant_type": "FP8"},
"int8": {"quant_type": "INT8"},
"int4": {"quant_type": "INT4"},
}
MODELOPT_EXPECTED_MEMORY_REDUCTIONS = {
"fp8": 1.5,
"int8": 1.5,
"int4": 3.0,
}
def _create_quantized_model(self, config_kwargs, **extra_kwargs):
config = NVIDIAModelOptConfig(**config_kwargs)
kwargs = getattr(self, "pretrained_model_kwargs", {}).copy()
kwargs["quantization_config"] = config
kwargs["device_map"] = str(torch_device)
kwargs.update(extra_kwargs)
return self.model_class.from_pretrained(self.pretrained_model_name_or_path, **kwargs)
def _verify_if_layer_quantized(self, name, module, config_kwargs):
assert mtq.utils.is_quantized(module), f"Layer {name} does not have weight_quantizer attribute (not quantized)"
@is_modelopt
@require_accelerator
@require_accelerate
@require_modelopt_version_greater_or_equal("0.33.1")
class ModelOptTesterMixin(ModelOptConfigMixin, QuantizationTesterMixin):
"""
Mixin class for testing NVIDIA ModelOpt quantization on models.
Expected class attributes:
- model_class: The model class to test
- pretrained_model_name_or_path: Hub repository ID for the pretrained model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained (e.g., {"subfolder": "transformer"})
Expected methods to be implemented by subclasses:
- get_dummy_inputs(): Returns dict of inputs to pass to the model forward pass
Optional class attributes:
- MODELOPT_CONFIGS: Dict of config name -> NVIDIAModelOptConfig kwargs to test
Pytest mark: modelopt
Use `pytest -m "not modelopt"` to skip these tests
"""
@pytest.mark.parametrize("config_name", ["fp8"], ids=["fp8"])
def test_modelopt_quantization_num_parameters(self, config_name):
self._test_quantization_num_parameters(ModelOptConfigMixin.MODELOPT_CONFIGS[config_name])
@pytest.mark.parametrize(
"config_name",
list(ModelOptConfigMixin.MODELOPT_CONFIGS.keys()),
ids=list(ModelOptConfigMixin.MODELOPT_CONFIGS.keys()),
)
def test_modelopt_quantization_memory_footprint(self, config_name):
expected = ModelOptConfigMixin.MODELOPT_EXPECTED_MEMORY_REDUCTIONS.get(config_name, 1.2)
self._test_quantization_memory_footprint(
ModelOptConfigMixin.MODELOPT_CONFIGS[config_name], expected_memory_reduction=expected
)
@pytest.mark.parametrize(
"config_name",
list(ModelOptConfigMixin.MODELOPT_CONFIGS.keys()),
ids=list(ModelOptConfigMixin.MODELOPT_CONFIGS.keys()),
)
def test_modelopt_quantization_inference(self, config_name):
self._test_quantization_inference(ModelOptConfigMixin.MODELOPT_CONFIGS[config_name])
@pytest.mark.parametrize("config_name", ["fp8"], ids=["fp8"])
def test_modelopt_quantization_dtype_assignment(self, config_name):
self._test_quantization_dtype_assignment(ModelOptConfigMixin.MODELOPT_CONFIGS[config_name])
@pytest.mark.parametrize("config_name", ["fp8"], ids=["fp8"])
def test_modelopt_quantization_lora_inference(self, config_name):
self._test_quantization_lora_inference(ModelOptConfigMixin.MODELOPT_CONFIGS[config_name])
@pytest.mark.parametrize("config_name", ["fp8"], ids=["fp8"])
def test_modelopt_quantization_serialization(self, config_name, tmp_path):
self._test_quantization_serialization(ModelOptConfigMixin.MODELOPT_CONFIGS[config_name], tmp_path)
@pytest.mark.parametrize("config_name", ["fp8"], ids=["fp8"])
def test_modelopt_quantized_layers(self, config_name):
self._test_quantized_layers(ModelOptConfigMixin.MODELOPT_CONFIGS[config_name])
def test_modelopt_modules_to_not_convert(self):
"""Test that modules_to_not_convert parameter works correctly."""
modules_to_exclude = getattr(self, "modules_to_not_convert_for_test", None)
if modules_to_exclude is None:
pytest.skip("modules_to_not_convert_for_test not defined for this model")
self._test_quantization_modules_to_not_convert(ModelOptConfigMixin.MODELOPT_CONFIGS["fp8"], modules_to_exclude)
def test_modelopt_device_map(self):
"""Test that device_map='auto' works correctly with quantization."""
self._test_quantization_device_map(ModelOptConfigMixin.MODELOPT_CONFIGS["fp8"])
def test_modelopt_dequantize(self):
"""Test that dequantize() works correctly."""
self._test_dequantize(ModelOptConfigMixin.MODELOPT_CONFIGS["fp8"])
@is_quantization
@is_sdnq
@require_sdnq
@require_accelerate
@require_accelerator
class SDNQConfigMixin:
"""
Base mixin providing SDNQ quantization config and model creation.
Expected class attributes:
- model_class: The model class to test
- pretrained_model_name_or_path: Hub repository ID for the pretrained model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained
"""
SDNQ_CONFIGS = {
"int8": {"weights_dtype": "int8"},
"uint4_svd": {"weights_dtype": "uint4", "use_svd": True},
}
SDNQ_EXPECTED_MEMORY_REDUCTIONS = {
"int8": 1.5,
"uint4_svd": 1.5,
}
def _create_quantized_model(self, config_kwargs, **extra_kwargs):
config = SDNQConfig(**config_kwargs)
kwargs = getattr(self, "pretrained_model_kwargs", {}).copy()
kwargs["quantization_config"] = config
# SDNQ leaves some modules (embedders, norms) unquantized, so the compute dtype must be set at load
# time to keep them consistent with the quantized layers.
kwargs.setdefault("dtype", getattr(self, "torch_dtype", torch.bfloat16))
kwargs.update(extra_kwargs)
return self.model_class.from_pretrained(self.pretrained_model_name_or_path, **kwargs)
def _verify_if_layer_quantized(self, name, module, config_kwargs):
assert hasattr(module, "sdnq_dequantizer"), f"Layer {name} is not an SDNQ quantized layer"
@is_sdnq
@require_sdnq
@require_accelerate
@require_accelerator
class SDNQTesterMixin(SDNQConfigMixin, QuantizationTesterMixin):
"""
Mixin class for testing SDNQ quantization on models.
Expected class attributes:
- model_class: The model class to test
- pretrained_model_name_or_path: Hub repository ID for the pretrained model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained (e.g., {"subfolder": "transformer"})
Expected methods to be implemented by subclasses:
- get_dummy_inputs(): Returns dict of inputs to pass to the model forward pass
Optional class attributes:
- SDNQ_CONFIGS: Dict of config name -> SDNQConfig kwargs to test
Pytest mark: sdnq
Use `pytest -m "not sdnq"` to skip these tests
"""
@pytest.mark.parametrize(
"config_name",
list(SDNQConfigMixin.SDNQ_CONFIGS.keys()),
ids=list(SDNQConfigMixin.SDNQ_CONFIGS.keys()),
)
def test_sdnq_quantization_inference(self, config_name):
self._test_quantization_inference(SDNQConfigMixin.SDNQ_CONFIGS[config_name])
@pytest.mark.parametrize(
"config_name",
list(SDNQConfigMixin.SDNQ_CONFIGS.keys()),
ids=list(SDNQConfigMixin.SDNQ_CONFIGS.keys()),
)
def test_sdnq_quantization_memory_footprint(self, config_name):
expected = SDNQConfigMixin.SDNQ_EXPECTED_MEMORY_REDUCTIONS.get(config_name, 1.2)
self._test_quantization_memory_footprint(
SDNQConfigMixin.SDNQ_CONFIGS[config_name], expected_memory_reduction=expected
)
@torch.no_grad()
def test_sdnq_quantization_serialization(self, tmp_path):
# Unlike the base helper, reload onto the accelerator: SDNQ reloads to CPU, while dummy inputs live on device.
model = self._create_quantized_model(SDNQConfigMixin.SDNQ_CONFIGS["int8"])
model.save_pretrained(str(tmp_path), safe_serialization=True)
# No quantization_config passed, so it must be picked up from the saved config.json.
model_loaded = self.model_class.from_pretrained(str(tmp_path)).to(torch_device)
inputs = self.get_dummy_inputs()
output = model_loaded(**inputs, return_dict=False)[0]
assert not torch.isnan(output).any(), "Loaded model output contains NaN"
@pytest.mark.parametrize("config_name", ["int8"], ids=["int8"])
def test_sdnq_quantization_dtype_assignment(self, config_name):
self._test_quantization_dtype_assignment(SDNQConfigMixin.SDNQ_CONFIGS[config_name])
def test_sdnq_modules_to_not_convert(self):
"""Test that modules_to_not_convert parameter works correctly."""
modules_to_exclude = getattr(self, "modules_to_not_convert_for_test", None)
if modules_to_exclude is None:
pytest.skip("modules_to_not_convert_for_test not defined for this model")
self._test_quantization_modules_to_not_convert(SDNQConfigMixin.SDNQ_CONFIGS["int8"], modules_to_exclude)
def test_sdnq_dequantize(self):
"""Test that dequantize() works correctly."""
self._test_dequantize(SDNQConfigMixin.SDNQ_CONFIGS["int8"])
@is_quantization
@is_torch_compile
class QuantizationCompileTesterMixin:
"""
Base mixin class providing common test implementations for torch.compile with quantized models.
Backend-specific compile mixins should:
1. Inherit from their respective config mixin (e.g., BitsAndBytesConfigMixin)
2. Inherit from this mixin
3. Define the config to use for compile tests
Expected class attributes:
- model_class: The model class to test
- pretrained_model_name_or_path: Hub repository ID for the pretrained model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained
Expected methods in test classes:
- get_dummy_inputs(): Returns dict of inputs to pass to the model forward pass
"""
def setup_method(self):
gc.collect()
backend_empty_cache(torch_device)
torch.compiler.reset()
def teardown_method(self):
gc.collect()
backend_empty_cache(torch_device)
torch.compiler.reset()
@torch.no_grad()
def _test_torch_compile(self, config_kwargs, fullgraph=True, error_on_recompile=True):
"""
Test that torch.compile works correctly with a quantized model.
Args:
config_kwargs: Quantization config parameters
fullgraph: Whether the compiled model is required to compile without graph breaks
error_on_recompile: Whether a recompilation during the forward pass should fail the test
"""
model = self._create_quantized_model(config_kwargs)
model.to(torch_device)
model.eval()
model.compile(fullgraph=fullgraph)
with torch._dynamo.config.patch(error_on_recompile=error_on_recompile):
inputs = self.get_dummy_inputs()
output = model(**inputs, return_dict=False)[0]
assert output is not None, "Model output is None"
assert not torch.isnan(output).any(), "Model output contains NaN"
@torch.no_grad()
def _test_torch_compile_with_group_offload(self, config_kwargs, use_stream=False):
"""
Test that torch.compile works correctly with a quantized model and group offloading.
Args:
config_kwargs: Quantization config parameters
use_stream: Whether to use CUDA streams for offloading
"""
torch._dynamo.config.cache_size_limit = 1000
model = self._create_quantized_model(config_kwargs)
model.eval()
if not hasattr(model, "enable_group_offload"):
pytest.skip("Model does not support group offloading")
group_offload_kwargs = {
"onload_device": torch.device(torch_device),
"offload_device": torch.device("cpu"),
"offload_type": "leaf_level",
"use_stream": use_stream,
}
model.enable_group_offload(**group_offload_kwargs)
model.compile()
inputs = self.get_dummy_inputs()
output = model(**inputs, return_dict=False)[0]
assert output is not None, "Model output is None"
assert not torch.isnan(output).any(), "Model output contains NaN"
@is_bitsandbytes
@require_accelerator
@require_bitsandbytes_version_greater("0.43.2")
@require_accelerate
class BitsAndBytesCompileTesterMixin(BitsAndBytesConfigMixin, QuantizationCompileTesterMixin):
"""
Mixin class for testing torch.compile with BitsAndBytes quantized models.
Expected class attributes:
- model_class: The model class to test
- pretrained_model_name_or_path: Hub repository ID for the pretrained model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained
Expected methods to be implemented by subclasses:
- get_dummy_inputs(): Returns dict of inputs to pass to the model forward pass
Pytest mark: bitsandbytes
Use `pytest -m "not bitsandbytes"` to skip these tests
"""
@pytest.mark.parametrize("config_name", ["4bit_nf4"], ids=["4bit_nf4"])
def test_bnb_torch_compile(self, config_name):
self._test_torch_compile(BitsAndBytesConfigMixin.BNB_CONFIGS[config_name])
@pytest.mark.parametrize("config_name", ["4bit_nf4"], ids=["4bit_nf4"])
def test_bnb_torch_compile_with_group_offload(self, config_name):
self._test_torch_compile_with_group_offload(BitsAndBytesConfigMixin.BNB_CONFIGS[config_name])
@is_torchao
@require_accelerator
@require_torchao_version_greater_or_equal("0.7.0")
class TorchAoCompileTesterMixin(TorchAoConfigMixin, QuantizationCompileTesterMixin):
"""
Mixin class for testing torch.compile with TorchAO quantized models.
Expected class attributes:
- model_class: The model class to test
- pretrained_model_name_or_path: Hub repository ID for the pretrained model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained
Expected methods to be implemented by subclasses:
- get_dummy_inputs(): Returns dict of inputs to pass to the model forward pass
Pytest mark: torchao
Use `pytest -m "not torchao"` to skip these tests
"""
@pytest.mark.parametrize("quant_type", ["int8wo"], ids=["int8wo"])
def test_torchao_torch_compile(self, quant_type):
self._test_torch_compile(TorchAoConfigMixin.TORCHAO_QUANT_TYPES[quant_type])
@pytest.mark.parametrize("quant_type", ["int8wo"], ids=["int8wo"])
def test_torchao_torch_compile_with_group_offload(self, quant_type):
self._test_torch_compile_with_group_offload(TorchAoConfigMixin.TORCHAO_QUANT_TYPES[quant_type])
@is_gguf
@require_accelerate
@require_accelerator
@require_gguf_version_greater_or_equal("0.10.0")
class GGUFCompileTesterMixin(GGUFConfigMixin, QuantizationCompileTesterMixin):
"""
Mixin class for testing torch.compile with GGUF quantized models.
Expected from config mixin:
- model_class: The model class to test
Required properties (must be implemented by subclasses):
- gguf_filename: URL or path to the GGUF file
Expected methods from config mixin:
- get_dummy_inputs(): Returns dict of inputs to pass to the model forward pass
Pytest mark: gguf
Use `pytest -m "not gguf"` to skip these tests
"""
def test_gguf_torch_compile(self):
self._test_torch_compile({"compute_dtype": torch.bfloat16})
def test_gguf_torch_compile_with_group_offload(self):
self._test_torch_compile_with_group_offload({"compute_dtype": torch.bfloat16})
@pytest.mark.skipif(not is_kernels_available(), reason="`kernels` is not available.")
@require_accelerate
@require_accelerator
class NunchakuLiteConfigMixin:
"""
Base mixin providing Nunchaku Lite quantization config and model creation.
Expected class attributes:
- model_class: The model class to test
- quantized_model_name_or_path: Hub repository ID or local path for the quantized model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained
"""
config_dict = None
def _create_quantized_model(self, config_kwargs=None, **extra_kwargs):
kwargs = getattr(self, "pretrained_model_kwargs", {}).copy()
if config_kwargs is not None:
kwargs["quantization_config"] = NunchakuLiteQuantizationConfig(**config_kwargs)
kwargs.update(extra_kwargs)
return self.model_class.from_pretrained(self.quantized_model_name_or_path, **kwargs)
def _verify_if_layer_quantized(self, name, module, config_kwargs):
from diffusers.quantizers.nunchaku.utils import AWQW4A16Linear, SVDQW4A4Linear
assert isinstance(module, (SVDQW4A4Linear, AWQW4A16Linear)), (
f"Layer {name} is not a Nunchaku Lite layer, got {type(module)}"
)
@pytest.mark.skipif(not is_kernels_available(), reason="`kernels` is not available.")
@require_accelerate
@require_accelerator
class NunchakuLiteTesterMixin(NunchakuLiteConfigMixin, QuantizationTesterMixin):
"""
Mixin class for testing Nunchaku Lite quantization on models.
Expected class attributes:
- model_class: The model class to test
- quantized_model_name_or_path: Hub repository ID or local path for the quantized model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained
Expected methods to be implemented by subclasses:
- get_dummy_inputs(): Returns dict of inputs to pass to the model forward pass
"""
def test_nunchaku_lite_quantization_inference(self):
self._test_quantization_inference(self.config_dict)
def _is_module_quantized(self, module, config_kwargs=None):
from diffusers.quantizers.nunchaku.utils import AWQW4A16Linear, SVDQW4A4Linear
return isinstance(module, (SVDQW4A4Linear, AWQW4A16Linear))
def _test_quantized_layers(self, config_kwargs):
model = self._create_quantized_model(config_kwargs)
num_quantized_layers = 0
for name, module in model.named_modules():
if self._is_module_quantized(module):
self._verify_if_layer_quantized(name, module, config_kwargs)
num_quantized_layers += 1
expected_quantized_layers = sum(
len(config_kwargs.get(section, {}).get("targets", [])) for section in ("svdq_w4a4", "awq_w4a16")
)
assert num_quantized_layers > 0, (
f"No quantized layers found in model (expected {expected_quantized_layers} Nunchaku Lite layers)"
)
assert num_quantized_layers == expected_quantized_layers, (
f"Quantized layer count mismatch: expected {expected_quantized_layers}, got {num_quantized_layers} "
f"(configured Nunchaku Lite targets: {expected_quantized_layers})"
)
def test_nunchaku_lite_quantized_layers(self):
self._test_quantized_layers(self.config_dict)
@pytest.mark.skipif(not is_kernels_available(), reason="`kernels` is not available.")
@require_accelerate
@require_accelerator
class NunchakuLiteCompileTesterMixin(NunchakuLiteConfigMixin, QuantizationCompileTesterMixin):
"""
Mixin class for testing torch.compile with Nunchaku Lite quantized models.
Expected class attributes:
- model_class: The model class to test
- quantized_model_name_or_path: Hub repository ID or local path for the quantized model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained
Expected methods to be implemented by subclasses:
- get_dummy_inputs(): Returns dict of inputs to pass to the model forward pass
"""
def test_nunchaku_lite_torch_compile(self):
self._test_torch_compile(self.config_dict)
def test_nunchaku_lite_torch_compile_with_group_offload(self):
self._test_torch_compile_with_group_offload(self.config_dict)
@is_modelopt
@require_accelerator
@require_accelerate
@require_modelopt_version_greater_or_equal("0.33.1")
class ModelOptCompileTesterMixin(ModelOptConfigMixin, QuantizationCompileTesterMixin):
"""
Mixin class for testing torch.compile with NVIDIA ModelOpt quantized models.
Expected class attributes:
- model_class: The model class to test
- pretrained_model_name_or_path: Hub repository ID for the pretrained model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained
Expected methods to be implemented by subclasses:
- get_dummy_inputs(): Returns dict of inputs to pass to the model forward pass
Pytest mark: modelopt
Use `pytest -m "not modelopt"` to skip these tests
"""
@pytest.mark.parametrize("config_name", ["fp8"], ids=["fp8"])
def test_modelopt_torch_compile(self, config_name):
self._test_torch_compile(ModelOptConfigMixin.MODELOPT_CONFIGS[config_name])
@pytest.mark.parametrize("config_name", ["fp8"], ids=["fp8"])
def test_modelopt_torch_compile_with_group_offload(self, config_name):
self._test_torch_compile_with_group_offload(ModelOptConfigMixin.MODELOPT_CONFIGS[config_name])
@is_sdnq
@require_sdnq
@require_accelerate
@require_accelerator
class SDNQCompileTesterMixin(SDNQConfigMixin, QuantizationCompileTesterMixin):
"""
Mixin class for testing torch.compile with SDNQ quantized models.
Expected class attributes:
- model_class: The model class to test
- pretrained_model_name_or_path: Hub repository ID for the pretrained model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained
Expected methods to be implemented by subclasses:
- get_dummy_inputs(): Returns dict of inputs to pass to the model forward pass
Pytest mark: sdnq
Use `pytest -m "not sdnq"` to skip these tests
"""
@pytest.mark.parametrize("config_name", ["int8"], ids=["int8"])
def test_sdnq_torch_compile(self, config_name):
self._test_torch_compile(SDNQConfigMixin.SDNQ_CONFIGS[config_name])
@pytest.mark.parametrize("config_name", ["int8"], ids=["int8"])
def test_sdnq_torch_compile_with_group_offload(self, config_name):
self._test_torch_compile_with_group_offload(SDNQConfigMixin.SDNQ_CONFIGS[config_name])
@is_quantization
@is_autoround
@require_accelerator
@require_accelerate
@require_auto_round_version_greater_or_equal("0.13.0")
class AutoRoundConfigMixin:
"""
Base mixin providing AutoRound quantization config and model creation.
AutoRound is a weight-only quantization method (W4A16). It supports multiple inference
When `backend="auto"`, AutoRound selects the best available backend automatically.
Expected class attributes:
- model_class: The model class to test
- pretrained_model_name_or_path: Hub repository ID for the pretrained model
- quantized_model_name_or_path: Hub repository ID for the quantized model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained
"""
config_dict = {"backend": "auto"}
def _create_quantized_model(self, config_kwargs, **extra_kwargs):
config = AutoRoundConfig(**config_kwargs)
kwargs = getattr(self, "pretrained_model_kwargs", {}).copy()
kwargs["quantization_config"] = config
kwargs["torch_dtype"] = torch.bfloat16
if "device_map" not in kwargs:
kwargs["device_map"] = torch_device
kwargs.update(extra_kwargs)
return self.model_class.from_pretrained(self.quantized_model_name_or_path, **kwargs)
def _verify_if_layer_quantized(self, name, module, config_kwargs):
# AutoRound replaces linear layers with quantized linear layers
assert isinstance(module, torch.nn.Linear), f"Layer {name} is not Linear, got {type(module)}"
@is_autoround
@require_accelerator
@require_accelerate
@require_auto_round_version_greater_or_equal("0.13.0")
class AutoRoundTesterMixin(AutoRoundConfigMixin, QuantizationTesterMixin):
"""
Mixin class for testing AutoRound quantization on models.
Expected class attributes:
- model_class: The model class to test
- pretrained_model_name_or_path: Hub repository ID for the pretrained model
- quantized_model_name_or_path: Hub repository ID for the quantized model
- pretrained_model_kwargs: (Optional) Dict of kwargs to pass to from_pretrained (e.g., {"subfolder": "transformer"})
Expected methods to be implemented by subclasses:
- get_dummy_inputs(): Returns dict of inputs to pass to the model forward pass
Optional class attributes:
- AUTOROUND_CONFIGS: Dict of config name -> AutoRoundConfig kwargs to test
Pytest mark: autoround
Use `pytest -m "not autoround"` to skip these tests
"""
config_dict = {"backend": "auto"}
def test_autoround_quantization_memory_footprint(self):
expected = 1.5 # AutoRound is a W4A16 method, so we expect around 1.5x memory reduction
self._test_quantization_memory_footprint(self.config_dict, expected_memory_reduction=expected)
def test_autoround_quantization_inference(self):
self._test_quantization_inference(self.config_dict)
def test_autoround_device_map(self):
"""Test that device_map='auto' works correctly with quantization."""
self._test_quantization_device_map(self.config_dict)
@is_autoround
@require_accelerator
@require_accelerate
@require_auto_round_version_greater_or_equal("0.13.0")
class AutoRoundCompileTesterMixin(AutoRoundConfigMixin, QuantizationCompileTesterMixin):
"""
Mixin class for testing `torch.compile` with AutoRound-quantized models.
This mixin provides tests that verify `torch.compile` works correctly with models
quantized using AutoRound. Subclasses are expected to inherit from
`AutoRoundConfigMixin` (which defines `config_dict`) and to provide the
following class attributes: `model_class`, `pretrained_model_name_or_path`, and
`quantized_model_name_or_path`.
The mixin uses `config_dict` (defaults to {"backend": "auto"}) as the
quantization configuration passed into `_create_quantized_model` when
invoking the compile-related tests.
Provided tests:
- `test_autoround_torch_compile`: Ensures `torch.compile` runs and produces
valid, non-NaN outputs for an AutoRound-quantized model.
- `test_autoround_torch_compile_with_group_offload`: Ensures `torch.compile`
works together with group offloading when supported by the quantized
model implementation.
"""
def test_autoround_torch_compile(self):
self._test_torch_compile(self.config_dict, fullgraph=False, error_on_recompile=False)
def test_autoround_torch_compile_with_group_offload(self):
self._test_torch_compile_with_group_offload(self.config_dict)
class BFLConfigMixin:
"""
Base mixin providing BFL (`fp8r` / `nvfp4`) quantization config and model creation.
BFL checkpoints are prequantized single files, one per scheme, and the tests never download their weights. The
safetensors header of each checkpoint says which linears the exporter quantized; the tests quantize those same
linears of the small test model with the library's own `quantize_fp8r` / `quantize_nvfp4` and load the result
through [`~FromOriginalModelMixin.from_single_file`], exactly as the real checkpoint is loaded.
Expected class attributes:
- model_class: The model class to test
Expected methods to be implemented by subclasses:
- get_init_dict(): Returns the init kwargs of the small test model
- get_dummy_inputs(): Returns dict of inputs to pass to the model forward pass
Required properties (must be implemented by subclasses):
- bfl_checkpoints: Dict mapping a scheme (`"fp8r"`, `"nvfp4"`) to the URL or path of its single-file checkpoint
"""
BFL_CONFIGS = {
"fp8r": {"scheme": "fp8r", "compute_dtype": torch.bfloat16},
"nvfp4": {"scheme": "nvfp4", "compute_dtype": torch.bfloat16},
}
@property
def bfl_checkpoints(self) -> dict[str, str]:
"""Scheme -> single-file checkpoint. Must be implemented by subclasses."""
raise NotImplementedError("Subclasses must implement the `bfl_checkpoints` property.")
@staticmethod
def _bfl_linear_cls(scheme):
from diffusers.quantizers.bfl.utils import BFLFP8RLinear, BFLNVFP4Linear
return {"fp8r": BFLFP8RLinear, "nvfp4": BFLNVFP4Linear}[scheme]
def _bfl_quantized_modules(self, scheme) -> set[str]:
"""The diffusers names of the linears the checkpoint quantizes, read from its safetensors header."""
metadata = fetch_checkpoint_metadata(self.bfl_checkpoints[scheme])
if metadata is None:
pytest.skip(f"{self.bfl_checkpoints[scheme]} is not a safetensors checkpoint")
# one `weight_scale` per quantized linear; the model's single-file mapping renames them to diffusers keys
weights = {
f"{name[: -len('.weight_scale')]}.weight" for name in metadata.tensors if name.endswith(".weight_scale")
}
checkpoint = {
name: torch.empty(tuple(metadata.tensors[name].shape), device="meta") for name in sorted(weights)
}
mapping_fn = SINGLE_FILE_LOADABLE_CLASSES[_get_single_file_loadable_mapping_class(self.model_class)][
"checkpoint_mapping_fn"
]
converted = mapping_fn(checkpoint=checkpoint, config=None)
return {name[: -len(".weight")] for name in converted}
def _bfl_quantized_linears(self, model, scheme) -> set[str]:
"""The checkpoint's quantized linears that the small model has (it has fewer blocks than the checkpoint)."""
linears = {name for name, module in model.named_modules() if isinstance(module, torch.nn.Linear)}
return self._bfl_quantized_modules(scheme) & linears
def _load_unquantized_model(self):
torch.manual_seed(0)
return self.model_class(**self.get_init_dict()).eval()
@torch.no_grad()
def _bfl_quantized_state_dict(self, model, scheme):
from diffusers.quantizers.bfl.utils import E2M1_MAX, FP8_MAX, quantize_fp8r, quantize_nvfp4
quantized_modules = self._bfl_quantized_linears(model, scheme)
assert quantized_modules, "the small model has none of the linears the checkpoint quantizes"
# nvfp4 stores a calibrated activation scale per linear: record the input amax on the dummy inputs
amax = {}
if scheme == "nvfp4":
handles = [
model.get_submodule(name).register_forward_pre_hook(
lambda module, inputs, name=name: amax.__setitem__(name, inputs[0].abs().amax().item())
)
for name in quantized_modules
]
model.to(torch_device)(**self.get_dummy_inputs(), return_dict=False)
model.cpu()
for handle in handles:
handle.remove()
state_dict = {}
for name, tensor in model.state_dict().items():
module_name = name[: -len(".weight")] if name.endswith(".weight") else None
if module_name not in quantized_modules:
state_dict[name] = tensor
elif scheme == "fp8r":
state_dict[name], state_dict[f"{module_name}.weight_scale"] = quantize_fp8r(tensor)
else:
weight_q, weight_scale, weight_scale_2 = quantize_nvfp4(tensor)
state_dict[name] = weight_q
state_dict[f"{module_name}.weight_scale"] = weight_scale
state_dict[f"{module_name}.weight_scale_2"] = weight_scale_2
# linears the dummy inputs do not reach (e.g. the reference-image blocks) take the largest scale seen
input_amax = amax.get(module_name, max(amax.values(), default=1.0))
state_dict[f"{module_name}.input_scale"] = torch.tensor(input_amax / (FP8_MAX * E2M1_MAX))
return state_dict
def _create_quantized_model(self, config_kwargs, **extra_kwargs):
config_kwargs = dict(config_kwargs)
scheme = config_kwargs.pop("scheme")
compute_dtype = config_kwargs.get("compute_dtype", torch.bfloat16)
model = self._load_unquantized_model().to(compute_dtype)
state_dict = self._bfl_quantized_state_dict(model, scheme)
kwargs = {
"quantization_config": BFLQuantizationConfig(**config_kwargs),
"dtype": compute_dtype,
"device": str(torch_device),
}
kwargs.update(extra_kwargs)
with tempfile.TemporaryDirectory() as config_dir:
model.save_config(config_dir)
return self.model_class.from_single_file(state_dict, config=config_dir, **kwargs)
def _verify_if_layer_quantized(self, name, module, config_kwargs):
cls = self._bfl_linear_cls(config_kwargs["scheme"])
assert isinstance(module, cls), f"Layer {name} is not a {cls.__name__}, got {type(module)}"
if config_kwargs["scheme"] == "fp8r":
assert module.weight.dtype == torch.float8_e4m3fn, f"{name} weight dtype should be float8_e4m3fn"
assert module.weight_scale.shape == (module.out_features,), f"{name} weight_scale is not per row"
else:
assert module.weight.dtype == torch.uint8, f"{name} weight dtype should be uint8 (packed E2M1)"
assert module.weight.shape == (module.out_features, module.in_features // 2), (
f"{name} weight is not packed"
)
assert module.weight_scale.dtype == torch.float8_e4m3fn, f"{name} block scales should be float8_e4m3fn"
assert module.weight_scale_2.ndim == 0 and module.input_scale.ndim == 0, f"{name} global scales"
def _is_module_quantized(self, module, config_kwargs=None):
from diffusers.quantizers.bfl.utils import BFLFP8RLinear, BFLNVFP4Linear
return isinstance(module, (BFLFP8RLinear, BFLNVFP4Linear))
@is_quantization
@is_bfl
@require_accelerator
@require_accelerate
class BFLTesterMixin(BFLConfigMixin, QuantizationTesterMixin):
"""
Mixin class for testing BFL (`fp8r` / `nvfp4`) quantization on models.
Expected class attributes:
- model_class: The model class to test
Expected methods to be implemented by subclasses:
- get_init_dict(): Returns the init kwargs of the small test model
- get_dummy_inputs(): Returns dict of inputs to pass to the model forward pass
Required properties (must be implemented by subclasses):
- bfl_checkpoints: Dict mapping a scheme (`"fp8r"`, `"nvfp4"`) to its single-file checkpoint; only the
safetensors header is fetched
Pytest mark: bfl
Use `pytest -m "not bfl"` to skip these tests
"""
@pytest.mark.parametrize("config_name", list(BFLConfigMixin.BFL_CONFIGS), ids=list(BFLConfigMixin.BFL_CONFIGS))
def test_bfl_quantization_inference(self, config_name):
self._test_quantization_inference(BFLConfigMixin.BFL_CONFIGS[config_name])
@pytest.mark.parametrize("config_name", list(BFLConfigMixin.BFL_CONFIGS), ids=list(BFLConfigMixin.BFL_CONFIGS))
def test_bfl_quantization_memory_footprint(self, config_name):
self._test_quantization_memory_footprint(BFLConfigMixin.BFL_CONFIGS[config_name])
def _test_quantized_layers(self, config_kwargs):
# The exporter quantizes a subset of the linears (embedders, output layers and some blocks stay in bf16),
# so the expected set is the one the checkpoint header describes, restricted to the small model's layers.
model = self._create_quantized_model(config_kwargs)
expected = self._bfl_quantized_linears(model, config_kwargs["scheme"])
quantized = set()
for name, module in model.named_modules():
if self._is_module_quantized(module):
self._verify_if_layer_quantized(name, module, config_kwargs)
quantized.add(name)
elif isinstance(module, torch.nn.Linear):
assert module.weight.dtype == config_kwargs["compute_dtype"], f"{name} is neither quantized nor bf16"
assert quantized, "No quantized layers found in model"
assert quantized == expected, f"Quantized layers differ from the checkpoint: {quantized ^ expected}"
@pytest.mark.parametrize("config_name", list(BFLConfigMixin.BFL_CONFIGS), ids=list(BFLConfigMixin.BFL_CONFIGS))
def test_bfl_quantized_layers(self, config_name):
self._test_quantized_layers(BFLConfigMixin.BFL_CONFIGS[config_name])
@pytest.mark.parametrize("config_name", list(BFLConfigMixin.BFL_CONFIGS), ids=list(BFLConfigMixin.BFL_CONFIGS))
@torch.no_grad()
def test_bfl_quantization_matches_unquantized(self, config_name):
"""The quantized model tracks the model it was quantized from, on the fused kernel or the fallback."""
config_kwargs = BFLConfigMixin.BFL_CONFIGS[config_name]
reference = self._load_unquantized_model().to(torch_device, config_kwargs["compute_dtype"])
expected = reference(**self.get_dummy_inputs(), return_dict=False)[0]
model = self._create_quantized_model(config_kwargs)
model.to(torch_device)
output = model(**self.get_dummy_inputs(), return_dict=False)[0]
assert output.shape == expected.shape and not torch.isnan(output).any()
cosine = torch.nn.functional.cosine_similarity(output.float().flatten(), expected.float().flatten(), dim=0)
assert cosine > {"fp8r": 0.99, "nvfp4": 0.9}[config_name], f"cosine similarity {cosine.item():.4f}"
@pytest.mark.parametrize("config_name", ["fp8r"], ids=["fp8r"])
def test_bfl_quantization_dtype_assignment(self, config_name):
self._test_quantization_dtype_assignment(BFLConfigMixin.BFL_CONFIGS[config_name])
@pytest.mark.parametrize("config_name", ["fp8r"], ids=["fp8r"])
def test_bfl_quantization_lora_inference(self, config_name):
self._test_quantization_lora_inference(BFLConfigMixin.BFL_CONFIGS[config_name])
@pytest.mark.parametrize("config_name", list(BFLConfigMixin.BFL_CONFIGS), ids=list(BFLConfigMixin.BFL_CONFIGS))
def test_bfl_keep_modules_in_fp32(self, config_name):
# `to_q` is quantized in the checkpoint, so this exercises the quantizer's fp32 exclusion path rather than
# a layer the exporter already left in bf16.
_keep_in_fp32_modules = self.model_class._keep_in_fp32_modules
self.model_class._keep_in_fp32_modules = ["to_q"]
try:
self._test_keep_modules_in_fp32(BFLConfigMixin.BFL_CONFIGS[config_name])
finally:
self.model_class._keep_in_fp32_modules = _keep_in_fp32_modules
@pytest.mark.parametrize("config_name", list(BFLConfigMixin.BFL_CONFIGS), ids=list(BFLConfigMixin.BFL_CONFIGS))
def test_bfl_dequantize(self, config_name):
self._test_dequantize(BFLConfigMixin.BFL_CONFIGS[config_name])
def test_bfl_rejects_unquantized_checkpoint(self):
model = self._load_unquantized_model().to(torch.bfloat16)
with tempfile.TemporaryDirectory() as config_dir:
model.save_config(config_dir)
with pytest.raises(ValueError, match="weight_scale"):
self.model_class.from_single_file(
model.state_dict(), config=config_dir, quantization_config=BFLQuantizationConfig()
)
@is_quantization
@is_bfl
@is_torch_compile
@require_accelerator
@require_accelerate
class BFLCompileTesterMixin(BFLConfigMixin, QuantizationCompileTesterMixin):
"""
Mixin class for testing torch.compile with BFL (`fp8r` / `nvfp4`) quantized models.
Expected class attributes:
- model_class: The model class to test
Expected methods to be implemented by subclasses:
- get_init_dict(): Returns the init kwargs of the small test model
- get_dummy_inputs(): Returns dict of inputs to pass to the model forward pass
Required properties (must be implemented by subclasses):
- bfl_checkpoints: Dict mapping a scheme (`"fp8r"`, `"nvfp4"`) to its single-file checkpoint; only the
safetensors header is fetched
Pytest mark: bfl
Use `pytest -m "not bfl"` to skip these tests
"""
@pytest.mark.parametrize("config_name", list(BFLConfigMixin.BFL_CONFIGS), ids=list(BFLConfigMixin.BFL_CONFIGS))
def test_bfl_torch_compile(self, config_name):
if (
config_name == "nvfp4"
and torch_device == "cuda"
and torch.cuda.get_device_capability()[0] >= 10
and is_flashinfer_available()
):
# Dynamo cannot trace FlashInfer's Python wrappers (they take a thread lock), and with graph breaks around
# them the remaining fragments recompile between calls. Expected to pass once the fused nvfp4 path wraps
# the FlashInfer calls as custom ops.
pytest.xfail("the fused nvfp4 path calls FlashInfer, which torch.compile cannot trace")
self._test_torch_compile(BFLConfigMixin.BFL_CONFIGS[config_name])
@pytest.mark.parametrize("config_name", ["fp8r"], ids=["fp8r"])
def test_bfl_torch_compile_with_group_offload(self, config_name):
self._test_torch_compile_with_group_offload(BFLConfigMixin.BFL_CONFIGS[config_name])