mirror of
https://github.com/huggingface/diffusers.git
synced 2026-10-23 09:09:01 +03:00
* 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>
1817 lines
66 KiB
Python
1817 lines
66 KiB
Python
import functools
|
|
import glob
|
|
import importlib
|
|
import importlib.metadata
|
|
import inspect
|
|
import io
|
|
import logging
|
|
import multiprocessing
|
|
import os
|
|
import random
|
|
import re
|
|
import struct
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import urllib.parse
|
|
from collections import UserDict
|
|
from contextlib import contextmanager
|
|
from io import BytesIO, StringIO
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, Any, Callable, Tuple, Union
|
|
|
|
import numpy as np
|
|
import PIL.Image
|
|
import PIL.ImageOps
|
|
import pytest
|
|
import requests
|
|
from numpy.linalg import norm
|
|
from packaging import version
|
|
|
|
from diffusers.utils.constants import DIFFUSERS_REQUEST_TIMEOUT, USE_PEFT_BACKEND
|
|
from diffusers.utils.import_utils import (
|
|
BACKENDS_MAPPING,
|
|
is_accelerate_available,
|
|
is_auto_round_available,
|
|
is_bitsandbytes_available,
|
|
is_compel_available,
|
|
is_flashpack_available,
|
|
is_gguf_available,
|
|
is_kernels_available,
|
|
is_note_seq_available,
|
|
is_nvidia_modelopt_version,
|
|
is_opencv_available,
|
|
is_peft_available,
|
|
is_sdnq_available,
|
|
is_timm_available,
|
|
is_torch_available,
|
|
is_torch_neuronx_available,
|
|
is_torch_tpu_available,
|
|
is_torch_version,
|
|
is_torchao_available,
|
|
is_torchsde_available,
|
|
is_transformers_available,
|
|
)
|
|
from diffusers.utils.logging import get_logger
|
|
from diffusers.utils.torch_utils import TorchDeviceBackend
|
|
|
|
|
|
if is_torch_available():
|
|
import torch
|
|
|
|
IS_ROCM_SYSTEM = torch.version.hip is not None
|
|
IS_CUDA_SYSTEM = torch.version.cuda is not None
|
|
IS_XPU_SYSTEM = getattr(torch.version, "xpu", None) is not None
|
|
else:
|
|
IS_ROCM_SYSTEM = False
|
|
IS_CUDA_SYSTEM = False
|
|
IS_XPU_SYSTEM = False
|
|
|
|
IS_GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" and os.getenv("DIFFUSERS_IS_CI") == "yes"
|
|
|
|
global_rng = random.Random()
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
BIG_GPU_MEMORY = int(os.getenv("BIG_GPU_MEMORY", 40))
|
|
|
|
if is_torch_available():
|
|
import torch
|
|
|
|
# Set a backend environment variable for any extra module import required for a custom accelerator
|
|
if "DIFFUSERS_TEST_BACKEND" in os.environ:
|
|
backend = os.environ["DIFFUSERS_TEST_BACKEND"]
|
|
try:
|
|
_ = importlib.import_module(backend)
|
|
except ModuleNotFoundError as e:
|
|
raise ModuleNotFoundError(
|
|
f"Failed to import `DIFFUSERS_TEST_BACKEND` '{backend}'! This should be the name of an installed module \
|
|
to enable a specified backend.):\n{e}"
|
|
) from e
|
|
|
|
if "DIFFUSERS_TEST_DEVICE" in os.environ:
|
|
torch_device = os.environ["DIFFUSERS_TEST_DEVICE"]
|
|
try:
|
|
# try creating device to see if provided device is valid
|
|
_ = torch.device(torch_device)
|
|
except RuntimeError as e:
|
|
raise RuntimeError(
|
|
f"Unknown testing device specified by environment variable `DIFFUSERS_TEST_DEVICE`: {torch_device}"
|
|
) from e
|
|
logger.info(f"torch_device overrode to {torch_device}")
|
|
else:
|
|
if torch.cuda.is_available():
|
|
torch_device = "cuda"
|
|
elif torch.xpu.is_available():
|
|
torch_device = "xpu"
|
|
elif is_torch_neuronx_available() and hasattr(torch, "neuron") and torch.neuron.is_available():
|
|
torch_device = torch.neuron.current_device()
|
|
else:
|
|
torch_device = "cpu"
|
|
is_torch_higher_equal_than_1_12 = version.parse(
|
|
version.parse(torch.__version__).base_version
|
|
) >= version.parse("1.12")
|
|
|
|
if is_torch_higher_equal_than_1_12:
|
|
# Some builds of torch 1.12 don't have the mps backend registered. See #892 for more details
|
|
mps_backend_registered = hasattr(torch.backends, "mps")
|
|
torch_device = "mps" if (mps_backend_registered and torch.backends.mps.is_available()) else torch_device
|
|
|
|
from diffusers.utils.torch_utils import get_torch_cuda_device_capability
|
|
|
|
|
|
def torch_all_close(a, b, *args, **kwargs):
|
|
if not is_torch_available():
|
|
raise ValueError("PyTorch needs to be installed to use this function.")
|
|
if not torch.allclose(a, b, *args, **kwargs):
|
|
assert False, f"Max diff is absolute {(a - b).abs().max()}. Diff tensor is {(a - b).abs()}."
|
|
return True
|
|
|
|
|
|
def assert_tensors_close(
|
|
actual: "torch.Tensor",
|
|
expected: "torch.Tensor",
|
|
atol: float = 1e-5,
|
|
rtol: float = 1e-5,
|
|
msg: str = "",
|
|
) -> None:
|
|
"""
|
|
Assert that two tensors are close within tolerance.
|
|
|
|
Uses the same formula as torch.allclose: |actual - expected| <= atol + rtol * |expected| Provides concise,
|
|
actionable error messages without dumping full tensors.
|
|
|
|
Args:
|
|
actual: The actual tensor from the computation.
|
|
expected: The expected tensor to compare against.
|
|
atol: Absolute tolerance.
|
|
rtol: Relative tolerance.
|
|
msg: Optional message prefix for the assertion error.
|
|
|
|
Raises:
|
|
AssertionError: If tensors have different shapes or values exceed tolerance.
|
|
|
|
Example:
|
|
>>> assert_tensors_close(output, expected_output, atol=1e-5, rtol=1e-5, msg="Forward pass")
|
|
"""
|
|
if not is_torch_available():
|
|
raise ValueError("PyTorch needs to be installed to use this function.")
|
|
|
|
# Some models (e.g. Z-Image, Cosmos ControlNet) return a list/tuple of tensors as their output. Compare these
|
|
# element-wise so the same helper works regardless of whether the output is a single tensor or a sequence.
|
|
if isinstance(actual, (list, tuple)) or isinstance(expected, (list, tuple)):
|
|
if not (isinstance(actual, (list, tuple)) and isinstance(expected, (list, tuple))):
|
|
raise AssertionError(f"{msg} Type mismatch: actual {type(actual)} vs expected {type(expected)}")
|
|
if len(actual) != len(expected):
|
|
raise AssertionError(f"{msg} Length mismatch: actual {len(actual)} vs expected {len(expected)}")
|
|
for i, (a, e) in enumerate(zip(actual, expected)):
|
|
assert_tensors_close(a, e, atol=atol, rtol=rtol, msg=f"{msg} [element {i}]")
|
|
return
|
|
|
|
if actual.shape != expected.shape:
|
|
raise AssertionError(f"{msg} Shape mismatch: actual {actual.shape} vs expected {expected.shape}")
|
|
|
|
if not torch.allclose(actual, expected, atol=atol, rtol=rtol):
|
|
abs_diff = (actual - expected).abs()
|
|
max_diff = abs_diff.max().item()
|
|
|
|
flat_idx = abs_diff.argmax().item()
|
|
max_idx = tuple(idx.item() for idx in torch.unravel_index(torch.tensor(flat_idx), actual.shape))
|
|
|
|
threshold = atol + rtol * expected.abs()
|
|
mismatched = (abs_diff > threshold).sum().item()
|
|
total = actual.numel()
|
|
|
|
raise AssertionError(
|
|
f"{msg}\n"
|
|
f"Tensors not close! Mismatched elements: {mismatched}/{total} ({100 * mismatched / total:.1f}%)\n"
|
|
f" Max diff: {max_diff:.6e} at index {max_idx}\n"
|
|
f" Actual: {actual.flatten()[flat_idx].item():.6e}\n"
|
|
f" Expected: {expected.flatten()[flat_idx].item():.6e}\n"
|
|
f" atol: {atol:.6e}, rtol: {rtol:.6e}"
|
|
)
|
|
|
|
|
|
def numpy_cosine_similarity_distance(a, b):
|
|
similarity = np.dot(a, b) / (norm(a) * norm(b))
|
|
distance = 1.0 - similarity.mean()
|
|
|
|
return distance
|
|
|
|
|
|
def check_if_dicts_are_equal(dict1, dict2):
|
|
dict1, dict2 = dict1.copy(), dict2.copy()
|
|
|
|
for key, value in dict1.items():
|
|
if isinstance(value, set):
|
|
dict1[key] = sorted(value)
|
|
for key, value in dict2.items():
|
|
if isinstance(value, set):
|
|
dict2[key] = sorted(value)
|
|
|
|
for key in dict1:
|
|
if key not in dict2:
|
|
return False
|
|
if dict1[key] != dict2[key]:
|
|
return False
|
|
|
|
for key in dict2:
|
|
if key not in dict1:
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def print_tensor_test(
|
|
tensor,
|
|
limit_to_slices=None,
|
|
max_torch_print=None,
|
|
filename="test_corrections.txt",
|
|
expected_tensor_name="expected_slice",
|
|
):
|
|
if max_torch_print:
|
|
torch.set_printoptions(threshold=10_000)
|
|
|
|
test_name = os.environ.get("PYTEST_CURRENT_TEST")
|
|
if not torch.is_tensor(tensor):
|
|
tensor = torch.from_numpy(tensor)
|
|
if limit_to_slices:
|
|
tensor = tensor[0, -3:, -3:, -1]
|
|
|
|
tensor_str = str(tensor.detach().cpu().flatten().to(torch.float32)).replace("\n", "")
|
|
# format is usually:
|
|
# expected_slice = np.array([-0.5713, -0.3018, -0.9814, 0.04663, -0.879, 0.76, -1.734, 0.1044, 1.161])
|
|
output_str = tensor_str.replace("tensor", f"{expected_tensor_name} = np.array")
|
|
test_file, test_class, test_fn = test_name.split("::")
|
|
test_fn = test_fn.split()[0]
|
|
with open(filename, "a") as f:
|
|
print("::".join([test_file, test_class, test_fn, output_str]), file=f)
|
|
|
|
|
|
def get_tests_dir(append_path=None):
|
|
"""
|
|
Args:
|
|
append_path: optional path to append to the tests dir path
|
|
Return:
|
|
The full path to the `tests` dir, so that the tests can be invoked from anywhere. Optionally `append_path` is
|
|
joined after the `tests` dir the former is provided.
|
|
"""
|
|
# this function caller's __file__
|
|
caller__file__ = inspect.stack()[1][1]
|
|
tests_dir = os.path.abspath(os.path.dirname(caller__file__))
|
|
|
|
while not tests_dir.endswith("tests"):
|
|
tests_dir = os.path.dirname(tests_dir)
|
|
|
|
if append_path:
|
|
return Path(tests_dir, append_path).as_posix()
|
|
else:
|
|
return tests_dir
|
|
|
|
|
|
# Taken from the following PR:
|
|
# https://github.com/huggingface/accelerate/pull/1964
|
|
def str_to_bool(value) -> int:
|
|
"""
|
|
Converts a string representation of truth to `True` (1) or `False` (0). True values are `y`, `yes`, `t`, `true`,
|
|
`on`, and `1`; False value are `n`, `no`, `f`, `false`, `off`, and `0`;
|
|
"""
|
|
value = value.lower()
|
|
if value in ("y", "yes", "t", "true", "on", "1"):
|
|
return 1
|
|
elif value in ("n", "no", "f", "false", "off", "0"):
|
|
return 0
|
|
else:
|
|
raise ValueError(f"invalid truth value {value}")
|
|
|
|
|
|
def parse_flag_from_env(key, default=False):
|
|
try:
|
|
value = os.environ[key]
|
|
except KeyError:
|
|
# KEY isn't set, default to `default`.
|
|
_value = default
|
|
else:
|
|
# KEY is set, convert it to True or False.
|
|
try:
|
|
_value = str_to_bool(value)
|
|
except ValueError:
|
|
# More values are supported, but let's keep the message simple.
|
|
raise ValueError(f"If set, {key} must be yes or no.")
|
|
return _value
|
|
|
|
|
|
_run_slow_tests = parse_flag_from_env("RUN_SLOW", default=False)
|
|
_run_nightly_tests = parse_flag_from_env("RUN_NIGHTLY", default=False)
|
|
|
|
|
|
def floats_tensor(shape, scale=1.0, rng=None, name=None):
|
|
"""Creates a random float32 tensor"""
|
|
if rng is None:
|
|
rng = global_rng
|
|
|
|
total_dims = 1
|
|
for dim in shape:
|
|
total_dims *= dim
|
|
|
|
values = []
|
|
for _ in range(total_dims):
|
|
values.append(rng.random() * scale)
|
|
|
|
return torch.tensor(data=values, dtype=torch.float).view(shape).contiguous()
|
|
|
|
|
|
def slow(test_case):
|
|
"""
|
|
Decorator marking a test as slow.
|
|
|
|
Slow tests are skipped by default. Set the RUN_SLOW environment variable to a truthy value to run them.
|
|
|
|
"""
|
|
return pytest.mark.skipif(not _run_slow_tests, reason="test is slow")(test_case)
|
|
|
|
|
|
def nightly(test_case):
|
|
"""
|
|
Decorator marking a test that runs nightly in the diffusers CI.
|
|
|
|
Slow tests are skipped by default. Set the RUN_NIGHTLY environment variable to a truthy value to run them.
|
|
|
|
"""
|
|
return pytest.mark.skipif(not _run_nightly_tests, reason="test is nightly")(test_case)
|
|
|
|
|
|
def is_torch_compile(test_case):
|
|
"""
|
|
Decorator marking a test as a torch.compile test. These tests can be filtered using:
|
|
pytest -m "not compile" to skip pytest -m compile to run only these tests
|
|
"""
|
|
return pytest.mark.compile(test_case)
|
|
|
|
|
|
def is_single_file(test_case):
|
|
"""
|
|
Decorator marking a test as a single file loading test. These tests can be filtered using:
|
|
pytest -m "not single_file" to skip pytest -m single_file to run only these tests
|
|
"""
|
|
return pytest.mark.single_file(test_case)
|
|
|
|
|
|
def is_lora(test_case):
|
|
"""
|
|
Decorator marking a test as a LoRA test. These tests can be filtered using:
|
|
pytest -m "not lora" to skip pytest -m lora to run only these tests
|
|
"""
|
|
return pytest.mark.lora(test_case)
|
|
|
|
|
|
def is_ip_adapter(test_case):
|
|
"""
|
|
Decorator marking a test as an IP Adapter test. These tests can be filtered using:
|
|
pytest -m "not ip_adapter" to skip pytest -m ip_adapter to run only these tests
|
|
"""
|
|
return pytest.mark.ip_adapter(test_case)
|
|
|
|
|
|
def is_training(test_case):
|
|
"""
|
|
Decorator marking a test as a training test. These tests can be filtered using:
|
|
pytest -m "not training" to skip pytest -m training to run only these tests
|
|
"""
|
|
return pytest.mark.training(test_case)
|
|
|
|
|
|
def is_attention(test_case):
|
|
"""
|
|
Decorator marking a test as an attention test. These tests can be filtered using:
|
|
pytest -m "not attention" to skip pytest -m attention to run only these tests
|
|
"""
|
|
return pytest.mark.attention(test_case)
|
|
|
|
|
|
def is_memory(test_case):
|
|
"""
|
|
Decorator marking a test as a memory optimization test. These tests can be filtered using:
|
|
pytest -m "not memory" to skip pytest -m memory to run only these tests
|
|
"""
|
|
return pytest.mark.memory(test_case)
|
|
|
|
|
|
def is_cpu_offload(test_case):
|
|
"""
|
|
Decorator marking a test as a CPU offload test. These tests can be filtered using:
|
|
pytest -m "not cpu_offload" to skip pytest -m cpu_offload to run only these tests
|
|
"""
|
|
return pytest.mark.cpu_offload(test_case)
|
|
|
|
|
|
def is_group_offload(test_case):
|
|
"""
|
|
Decorator marking a test as a group offload test. These tests can be filtered using:
|
|
pytest -m "not group_offload" to skip pytest -m group_offload to run only these tests
|
|
"""
|
|
return pytest.mark.group_offload(test_case)
|
|
|
|
|
|
def is_quantization(test_case):
|
|
"""
|
|
Decorator marking a test as a quantization test. These tests can be filtered using:
|
|
pytest -m "not quantization" to skip pytest -m quantization to run only these tests
|
|
"""
|
|
return pytest.mark.quantization(test_case)
|
|
|
|
|
|
def is_bitsandbytes(test_case):
|
|
"""
|
|
Decorator marking a test as a BitsAndBytes quantization test. These tests can be filtered using:
|
|
pytest -m "not bitsandbytes" to skip pytest -m bitsandbytes to run only these tests
|
|
"""
|
|
return pytest.mark.bitsandbytes(test_case)
|
|
|
|
|
|
def is_torchao(test_case):
|
|
"""
|
|
Decorator marking a test as a TorchAO quantization test. These tests can be filtered using:
|
|
pytest -m "not torchao" to skip pytest -m torchao to run only these tests
|
|
"""
|
|
return pytest.mark.torchao(test_case)
|
|
|
|
|
|
def is_gguf(test_case):
|
|
"""
|
|
Decorator marking a test as a GGUF quantization test. These tests can be filtered using:
|
|
pytest -m "not gguf" to skip pytest -m gguf to run only these tests
|
|
"""
|
|
return pytest.mark.gguf(test_case)
|
|
|
|
|
|
def is_autoround(test_case):
|
|
"""
|
|
Decorator marking a test as an AutoRound quantization test. These tests can be filtered using:
|
|
pytest -m "not autoround" to skip
|
|
pytest -m autoround to run only these tests
|
|
"""
|
|
return pytest.mark.autoround(test_case)
|
|
|
|
|
|
def is_modelopt(test_case):
|
|
"""
|
|
Decorator marking a test as a NVIDIA ModelOpt quantization test. These tests can be filtered using:
|
|
pytest -m "not modelopt" to skip pytest -m modelopt to run only these tests
|
|
"""
|
|
return pytest.mark.modelopt(test_case)
|
|
|
|
|
|
def is_sdnq(test_case):
|
|
"""
|
|
Decorator marking a test as an SDNQ quantization test. These tests can be filtered using:
|
|
pytest -m "not sdnq" to skip pytest -m sdnq to run only these tests
|
|
"""
|
|
return pytest.mark.sdnq(test_case)
|
|
|
|
|
|
def is_bfl(test_case):
|
|
"""
|
|
Decorator marking a test as a BFL (fp8r / nvfp4) quantization test. These tests can be filtered using:
|
|
pytest -m "not bfl" to skip pytest -m bfl to run only these tests
|
|
"""
|
|
return pytest.mark.bfl(test_case)
|
|
|
|
|
|
def is_context_parallel(test_case):
|
|
"""
|
|
Decorator marking a test as a context parallel inference test. These tests can be filtered using:
|
|
pytest -m "not context_parallel" to skip pytest -m context_parallel to run only these tests
|
|
"""
|
|
return pytest.mark.context_parallel(test_case)
|
|
|
|
|
|
def is_tensor_parallel(test_case):
|
|
"""
|
|
Decorator marking a test as a tensor parallel inference test. These tests can be filtered using:
|
|
pytest -m "not tensor_parallel" to skip pytest -m tensor_parallel to run only these tests
|
|
"""
|
|
return pytest.mark.tensor_parallel(test_case)
|
|
|
|
|
|
def is_cache(test_case):
|
|
"""
|
|
Decorator marking a test as a cache test. These tests can be filtered using:
|
|
pytest -m "not cache" to skip pytest -m cache to run only these tests
|
|
"""
|
|
return pytest.mark.cache(test_case)
|
|
|
|
|
|
def require_torch(test_case):
|
|
"""
|
|
Decorator marking a test that requires PyTorch. These tests are skipped when PyTorch isn't installed.
|
|
"""
|
|
return pytest.mark.skipif(not is_torch_available(), reason="test requires PyTorch")(test_case)
|
|
|
|
|
|
def require_torch_2(test_case):
|
|
"""
|
|
Decorator marking a test that requires PyTorch 2. These tests are skipped when it isn't installed.
|
|
"""
|
|
return pytest.mark.skipif(
|
|
not (is_torch_available() and is_torch_version(">=", "2.0.0")), reason="test requires PyTorch 2"
|
|
)(test_case)
|
|
|
|
|
|
def require_torch_version_greater_equal(torch_version):
|
|
"""Decorator marking a test that requires torch with a specific version or greater."""
|
|
|
|
def decorator(test_case):
|
|
correct_torch_version = is_torch_available() and is_torch_version(">=", torch_version)
|
|
return pytest.mark.skipif(
|
|
not correct_torch_version,
|
|
reason=f"test requires torch with the version greater than or equal to {torch_version}",
|
|
)(test_case)
|
|
|
|
return decorator
|
|
|
|
|
|
def require_torch_version_greater(torch_version):
|
|
"""Decorator marking a test that requires torch with a specific version greater."""
|
|
|
|
def decorator(test_case):
|
|
correct_torch_version = is_torch_available() and is_torch_version(">", torch_version)
|
|
return pytest.mark.skipif(
|
|
not correct_torch_version, reason=f"test requires torch with the version greater than {torch_version}"
|
|
)(test_case)
|
|
|
|
return decorator
|
|
|
|
|
|
def require_torch_gpu(test_case):
|
|
"""Decorator marking a test that requires CUDA and PyTorch."""
|
|
return pytest.mark.skipif(torch_device != "cuda", reason="test requires PyTorch+CUDA")(test_case)
|
|
|
|
|
|
def require_torch_cuda_compatibility(expected_compute_capability):
|
|
def decorator(test_case):
|
|
if torch.cuda.is_available():
|
|
current_compute_capability = get_torch_cuda_device_capability()
|
|
return pytest.mark.skipif(
|
|
float(current_compute_capability) != float(expected_compute_capability),
|
|
reason="Test not supported for this compute capability.",
|
|
)(test_case)
|
|
return test_case
|
|
|
|
return decorator
|
|
|
|
|
|
# These decorators are for accelerator-specific behaviours that are not GPU-specific
|
|
def require_torch_accelerator(test_case):
|
|
"""Decorator marking a test that requires an accelerator backend and PyTorch."""
|
|
return pytest.mark.skipif(torch_device == "cpu", reason="test requires accelerator+PyTorch")(test_case)
|
|
|
|
|
|
def require_torch_neuron(test_case):
|
|
"""Decorator marking a test that requires a Neuron device (Trainium/Inferentia)."""
|
|
return pytest.mark.skipif(
|
|
not (is_torch_neuronx_available() and hasattr(torch, "neuron") and torch.neuron.is_available()),
|
|
reason="test requires Neuron device",
|
|
)(test_case)
|
|
|
|
|
|
def require_torch_tpu(test_case):
|
|
"""Decorator marking a test that requires a TPU device (torch_tpu)."""
|
|
return pytest.mark.skipif(
|
|
not is_torch_tpu_available(),
|
|
reason="test requires TPU device (torch_tpu)",
|
|
)(test_case)
|
|
|
|
|
|
def require_torch_multi_gpu(test_case):
|
|
"""
|
|
Decorator marking a test that requires a multi-GPU setup (in PyTorch). These tests are skipped on a machine without
|
|
multiple GPUs. To run *only* the multi_gpu tests, assuming all test names contain multi_gpu: $ pytest -sv ./tests
|
|
-k "multi_gpu"
|
|
"""
|
|
if not is_torch_available():
|
|
return pytest.mark.skip(reason="test requires PyTorch")(test_case)
|
|
|
|
import torch
|
|
|
|
return pytest.mark.skipif(torch.cuda.device_count() <= 1, reason="test requires multiple GPUs")(test_case)
|
|
|
|
|
|
def require_torch_multi_accelerator(test_case):
|
|
"""
|
|
Decorator marking a test that requires a multi-accelerator setup (in PyTorch). These tests are skipped on a machine
|
|
without multiple hardware accelerators.
|
|
"""
|
|
if not is_torch_available():
|
|
return pytest.mark.skip(reason="test requires PyTorch")(test_case)
|
|
|
|
import torch
|
|
|
|
return pytest.mark.skipif(
|
|
not (torch.cuda.device_count() > 1 or torch.xpu.device_count() > 1),
|
|
reason="test requires multiple hardware accelerators",
|
|
)(test_case)
|
|
|
|
|
|
def require_torch_accelerator_with_fp16(test_case):
|
|
"""Decorator marking a test that requires an accelerator with support for the FP16 data type."""
|
|
return pytest.mark.skipif(
|
|
not _is_torch_fp16_available(torch_device), reason="test requires accelerator with fp16 support"
|
|
)(test_case)
|
|
|
|
|
|
def require_torch_accelerator_with_fp64(test_case):
|
|
"""Decorator marking a test that requires an accelerator with support for the FP64 data type."""
|
|
return pytest.mark.skipif(
|
|
not _is_torch_fp64_available(torch_device), reason="test requires accelerator with fp64 support"
|
|
)(test_case)
|
|
|
|
|
|
def require_big_gpu_with_torch_cuda(test_case):
|
|
"""
|
|
Decorator marking a test that requires a bigger GPU (24GB) for execution. Some example pipelines: Flux, SD3, Cog,
|
|
etc.
|
|
"""
|
|
if not is_torch_available():
|
|
return pytest.mark.skip(reason="test requires PyTorch")(test_case)
|
|
|
|
import torch
|
|
|
|
if not torch.cuda.is_available():
|
|
return pytest.mark.skip(reason="test requires PyTorch CUDA")(test_case)
|
|
|
|
device_properties = torch.cuda.get_device_properties(0)
|
|
total_memory = device_properties.total_memory / (1024**3)
|
|
return pytest.mark.skipif(
|
|
total_memory < BIG_GPU_MEMORY, reason=f"test requires a GPU with at least {BIG_GPU_MEMORY} GB memory"
|
|
)(test_case)
|
|
|
|
|
|
def require_big_accelerator(test_case):
|
|
"""
|
|
Decorator marking a test that requires a bigger hardware accelerator (24GB) for execution. Some example pipelines:
|
|
Flux, SD3, Cog, etc.
|
|
"""
|
|
import pytest
|
|
|
|
test_case = pytest.mark.big_accelerator(test_case)
|
|
|
|
if not is_torch_available():
|
|
return pytest.mark.skip(reason="test requires PyTorch")(test_case)
|
|
|
|
import torch
|
|
|
|
if not (torch.cuda.is_available() or torch.xpu.is_available()):
|
|
return pytest.mark.skip(reason="test requires PyTorch CUDA")(test_case)
|
|
|
|
if torch.xpu.is_available():
|
|
device_properties = torch.xpu.get_device_properties(0)
|
|
else:
|
|
device_properties = torch.cuda.get_device_properties(0)
|
|
|
|
total_memory = device_properties.total_memory / (1024**3)
|
|
return pytest.mark.skipif(
|
|
total_memory < BIG_GPU_MEMORY,
|
|
reason=f"test requires a hardware accelerator with at least {BIG_GPU_MEMORY} GB memory",
|
|
)(test_case)
|
|
|
|
|
|
def require_accelerator_memory(min_memory_gb: int):
|
|
"""
|
|
Decorator marking a test that needs at least `min_memory_gb` GB of memory on the accelerator it runs on. Tests
|
|
running on CPU are not affected.
|
|
"""
|
|
|
|
def decorator(test_case):
|
|
if torch_device == "cuda" and torch.cuda.is_available():
|
|
total_memory = torch.cuda.get_device_properties(0).total_memory / (1024**3)
|
|
elif torch_device == "xpu" and torch.xpu.is_available():
|
|
total_memory = torch.xpu.get_device_properties(0).total_memory / (1024**3)
|
|
else:
|
|
return test_case
|
|
|
|
return pytest.mark.skipif(
|
|
total_memory < min_memory_gb,
|
|
reason=f"test requires an accelerator with at least {min_memory_gb} GB memory",
|
|
)(test_case)
|
|
|
|
return decorator
|
|
|
|
|
|
def require_torch_accelerator_with_training(test_case):
|
|
"""Decorator marking a test that requires an accelerator with support for training."""
|
|
return pytest.mark.skipif(
|
|
not (is_torch_available() and backend_supports_training(torch_device)),
|
|
reason="test requires accelerator with training support",
|
|
)(test_case)
|
|
|
|
|
|
def skip_mps(test_case):
|
|
"""Decorator marking a test to skip if torch_device is 'mps'"""
|
|
return pytest.mark.skipif(torch_device == "mps", reason="test requires non 'mps' device")(test_case)
|
|
|
|
|
|
def require_compel(test_case):
|
|
"""
|
|
Decorator marking a test that requires compel: https://github.com/damian0815/compel. These tests are skipped when
|
|
the library is not installed.
|
|
"""
|
|
return pytest.mark.skipif(not is_compel_available(), reason="test requires compel")(test_case)
|
|
|
|
|
|
def require_note_seq(test_case):
|
|
"""
|
|
Decorator marking a test that requires note_seq. These tests are skipped when note_seq isn't installed.
|
|
"""
|
|
return pytest.mark.skipif(not is_note_seq_available(), reason="test requires note_seq")(test_case)
|
|
|
|
|
|
def require_accelerator(test_case):
|
|
"""
|
|
Decorator marking a test that requires a hardware accelerator backend. These tests are skipped when there are no
|
|
hardware accelerator available.
|
|
"""
|
|
return pytest.mark.skipif(torch_device == "cpu", reason="test requires a hardware accelerator")(test_case)
|
|
|
|
|
|
def require_torchsde(test_case):
|
|
"""
|
|
Decorator marking a test that requires torchsde. These tests are skipped when torchsde isn't installed.
|
|
"""
|
|
return pytest.mark.skipif(not is_torchsde_available(), reason="test requires torchsde")(test_case)
|
|
|
|
|
|
def require_peft_backend(test_case):
|
|
"""
|
|
Decorator marking a test that requires PEFT backend, this would require some specific versions of PEFT and
|
|
transformers.
|
|
"""
|
|
return pytest.mark.skipif(not USE_PEFT_BACKEND, reason="test requires PEFT backend")(test_case)
|
|
|
|
|
|
def require_timm(test_case):
|
|
"""
|
|
Decorator marking a test that requires timm. These tests are skipped when timm isn't installed.
|
|
"""
|
|
return pytest.mark.skipif(not is_timm_available(), reason="test requires timm")(test_case)
|
|
|
|
|
|
def require_bitsandbytes(test_case):
|
|
"""
|
|
Decorator marking a test that requires bitsandbytes. These tests are skipped when bitsandbytes isn't installed.
|
|
"""
|
|
return pytest.mark.skipif(not is_bitsandbytes_available(), reason="test requires bitsandbytes")(test_case)
|
|
|
|
|
|
def require_sdnq(test_case):
|
|
"""
|
|
Decorator marking a test that requires sdnq. These tests are skipped when sdnq isn't installed.
|
|
"""
|
|
return pytest.mark.skipif(not is_sdnq_available(), reason="test requires sdnq")(test_case)
|
|
|
|
|
|
def require_accelerate(test_case):
|
|
"""
|
|
Decorator marking a test that requires accelerate. These tests are skipped when accelerate isn't installed.
|
|
"""
|
|
return pytest.mark.skipif(not is_accelerate_available(), reason="test requires accelerate")(test_case)
|
|
|
|
|
|
def require_flashpack(test_case):
|
|
"""
|
|
Decorator marking a test that requires flashpack. These tests are skipped when flashpack isn't installed.
|
|
"""
|
|
return pytest.mark.skipif(not is_flashpack_available(), reason="test requires flashpack")(test_case)
|
|
|
|
|
|
def require_peft_version_greater(peft_version):
|
|
"""
|
|
Decorator marking a test that requires PEFT backend with a specific version, this would require some specific
|
|
versions of PEFT and transformers.
|
|
"""
|
|
|
|
def decorator(test_case):
|
|
correct_peft_version = is_peft_available() and version.parse(
|
|
version.parse(importlib.metadata.version("peft")).base_version
|
|
) > version.parse(peft_version)
|
|
return pytest.mark.skipif(
|
|
not correct_peft_version, reason=f"test requires PEFT backend with the version greater than {peft_version}"
|
|
)(test_case)
|
|
|
|
return decorator
|
|
|
|
|
|
def require_transformers_version_greater(transformers_version):
|
|
"""
|
|
Decorator marking a test that requires transformers with a specific version, this would require some specific
|
|
versions of PEFT and transformers.
|
|
"""
|
|
|
|
def decorator(test_case):
|
|
correct_transformers_version = is_transformers_available() and version.parse(
|
|
version.parse(importlib.metadata.version("transformers")).base_version
|
|
) > version.parse(transformers_version)
|
|
return pytest.mark.skipif(
|
|
not correct_transformers_version,
|
|
reason=f"test requires transformers with the version greater than {transformers_version}",
|
|
)(test_case)
|
|
|
|
return decorator
|
|
|
|
|
|
def require_accelerate_version_greater(accelerate_version):
|
|
def decorator(test_case):
|
|
correct_accelerate_version = is_accelerate_available() and version.parse(
|
|
version.parse(importlib.metadata.version("accelerate")).base_version
|
|
) > version.parse(accelerate_version)
|
|
return pytest.mark.skipif(
|
|
not correct_accelerate_version,
|
|
reason=f"Test requires accelerate with the version greater than {accelerate_version}.",
|
|
)(test_case)
|
|
|
|
return decorator
|
|
|
|
|
|
def require_bitsandbytes_version_greater(bnb_version):
|
|
def decorator(test_case):
|
|
correct_bnb_version = is_bitsandbytes_available() and version.parse(
|
|
version.parse(importlib.metadata.version("bitsandbytes")).base_version
|
|
) > version.parse(bnb_version)
|
|
return pytest.mark.skipif(
|
|
not correct_bnb_version, reason=f"Test requires bitsandbytes with the version greater than {bnb_version}."
|
|
)(test_case)
|
|
|
|
return decorator
|
|
|
|
|
|
def require_hf_token(test_case):
|
|
"""
|
|
Decorator marking a test that requires a Hugging Face auth token (e.g. to access gated repos). The test is
|
|
skipped when no token is available.
|
|
"""
|
|
from huggingface_hub import get_token
|
|
|
|
return pytest.mark.skipif(get_token() is None, reason="test requires a Hugging Face auth token")(test_case)
|
|
|
|
|
|
def require_hf_hub_version_greater(hf_hub_version):
|
|
def decorator(test_case):
|
|
correct_hf_hub_version = version.parse(
|
|
version.parse(importlib.metadata.version("huggingface_hub")).base_version
|
|
) > version.parse(hf_hub_version)
|
|
return pytest.mark.skipif(
|
|
not correct_hf_hub_version,
|
|
reason=f"Test requires huggingface_hub with the version greater than {hf_hub_version}.",
|
|
)(test_case)
|
|
|
|
return decorator
|
|
|
|
|
|
def require_gguf_version_greater_or_equal(gguf_version):
|
|
def decorator(test_case):
|
|
correct_gguf_version = is_gguf_available() and version.parse(
|
|
version.parse(importlib.metadata.version("gguf")).base_version
|
|
) >= version.parse(gguf_version)
|
|
return pytest.mark.skipif(
|
|
not correct_gguf_version, reason=f"Test requires gguf with the version greater than {gguf_version}."
|
|
)(test_case)
|
|
|
|
return decorator
|
|
|
|
|
|
def require_torchao_version_greater_or_equal(torchao_version):
|
|
def decorator(test_case):
|
|
correct_torchao_version = is_torchao_available() and version.parse(
|
|
version.parse(importlib.metadata.version("torchao")).base_version
|
|
) >= version.parse(torchao_version)
|
|
return pytest.mark.skipif(
|
|
not correct_torchao_version, reason=f"Test requires torchao with version greater than {torchao_version}."
|
|
)(test_case)
|
|
|
|
return decorator
|
|
|
|
|
|
def require_auto_round_version_greater_or_equal(auto_round_version):
|
|
def decorator(test_case):
|
|
correct_auto_round_version = is_auto_round_available() and version.parse(
|
|
version.parse(importlib.metadata.version("auto_round")).base_version
|
|
) >= version.parse(auto_round_version)
|
|
return pytest.mark.skipif(
|
|
not correct_auto_round_version,
|
|
reason=f"Test requires auto-round with version greater than {auto_round_version}.",
|
|
)(test_case)
|
|
|
|
return decorator
|
|
|
|
|
|
def require_kernels_version_greater_or_equal(kernels_version):
|
|
def decorator(test_case):
|
|
correct_kernels_version = is_kernels_available() and version.parse(
|
|
version.parse(importlib.metadata.version("kernels")).base_version
|
|
) >= version.parse(kernels_version)
|
|
return pytest.mark.skipif(
|
|
not correct_kernels_version, reason=f"Test requires kernels with version greater than {kernels_version}."
|
|
)(test_case)
|
|
|
|
return decorator
|
|
|
|
|
|
def require_modelopt_version_greater_or_equal(modelopt_version):
|
|
def decorator(test_case):
|
|
return pytest.mark.skipif(
|
|
not is_nvidia_modelopt_version(">=", modelopt_version),
|
|
reason=f"Test requires modelopt with version greater than {modelopt_version}.",
|
|
)(test_case)
|
|
|
|
return decorator
|
|
|
|
|
|
def deprecate_after_peft_backend(test_case):
|
|
"""
|
|
Decorator marking a test that will be skipped after PEFT backend
|
|
"""
|
|
return pytest.mark.skipif(USE_PEFT_BACKEND, reason="test skipped in favor of PEFT backend")(test_case)
|
|
|
|
|
|
def get_python_version():
|
|
sys_info = sys.version_info
|
|
major, minor = sys_info.major, sys_info.minor
|
|
return major, minor
|
|
|
|
|
|
def load_numpy(arry: str | np.ndarray, local_path: str | None = None) -> np.ndarray:
|
|
if isinstance(arry, str):
|
|
if local_path is not None:
|
|
# local_path can be passed to correct images of tests
|
|
return Path(local_path, arry.split("/")[-5], arry.split("/")[-2], arry.split("/")[-1]).as_posix()
|
|
elif arry.startswith("http://") or arry.startswith("https://"):
|
|
response = requests.get(arry, timeout=DIFFUSERS_REQUEST_TIMEOUT)
|
|
response.raise_for_status()
|
|
arry = np.load(BytesIO(response.content))
|
|
elif os.path.isfile(arry):
|
|
arry = np.load(arry)
|
|
else:
|
|
raise ValueError(
|
|
f"Incorrect path or url, URLs must start with `http://` or `https://`, and {arry} is not a valid path"
|
|
)
|
|
elif isinstance(arry, np.ndarray):
|
|
pass
|
|
else:
|
|
raise ValueError(
|
|
"Incorrect format used for numpy ndarray. Should be an url linking to an image, a local path, or a"
|
|
" ndarray."
|
|
)
|
|
|
|
return arry
|
|
|
|
|
|
def load_pt(url: str, map_location: str | None = None, weights_only: bool = True):
|
|
response = requests.get(url, timeout=DIFFUSERS_REQUEST_TIMEOUT)
|
|
response.raise_for_status()
|
|
arry = torch.load(BytesIO(response.content), map_location=map_location, weights_only=weights_only)
|
|
return arry
|
|
|
|
|
|
def load_image(image: str | PIL.Image.Image) -> PIL.Image.Image:
|
|
"""
|
|
Loads `image` to a PIL Image.
|
|
|
|
Args:
|
|
image (`str` or `PIL.Image.Image`):
|
|
The image to convert to the PIL Image format.
|
|
Returns:
|
|
`PIL.Image.Image`:
|
|
A PIL Image.
|
|
"""
|
|
if isinstance(image, str):
|
|
if image.startswith("http://") or image.startswith("https://"):
|
|
image = PIL.Image.open(requests.get(image, stream=True, timeout=DIFFUSERS_REQUEST_TIMEOUT).raw)
|
|
elif os.path.isfile(image):
|
|
image = PIL.Image.open(image)
|
|
else:
|
|
raise ValueError(
|
|
f"Incorrect path or url, URLs must start with `http://` or `https://`, and {image} is not a valid path"
|
|
)
|
|
elif isinstance(image, PIL.Image.Image):
|
|
image = image
|
|
else:
|
|
raise ValueError(
|
|
"Incorrect format used for image. Should be an url linking to an image, a local path, or a PIL image."
|
|
)
|
|
image = PIL.ImageOps.exif_transpose(image)
|
|
image = image.convert("RGB")
|
|
return image
|
|
|
|
|
|
def preprocess_image(image: PIL.Image, batch_size: int):
|
|
w, h = image.size
|
|
w, h = (x - x % 8 for x in (w, h)) # resize to integer multiple of 8
|
|
image = image.resize((w, h), resample=PIL.Image.LANCZOS)
|
|
image = np.array(image).astype(np.float32) / 255.0
|
|
image = np.vstack([image[None].transpose(0, 3, 1, 2)] * batch_size)
|
|
image = torch.from_numpy(image)
|
|
return 2.0 * image - 1.0
|
|
|
|
|
|
def export_to_gif(image: list[PIL.Image.Image], output_gif_path: str = None) -> str:
|
|
if output_gif_path is None:
|
|
output_gif_path = tempfile.NamedTemporaryFile(suffix=".gif").name
|
|
|
|
image[0].save(
|
|
output_gif_path,
|
|
save_all=True,
|
|
append_images=image[1:],
|
|
optimize=False,
|
|
duration=100,
|
|
loop=0,
|
|
)
|
|
return output_gif_path
|
|
|
|
|
|
@contextmanager
|
|
def skip_if_no_cudnn_engine():
|
|
"""
|
|
Skip the enclosing test when cuDNN has no kernel for an op the pipeline runs.
|
|
|
|
cuDNN does not ship an engine for every (op, dtype, layout) combination, and the set it covers differs between
|
|
cuDNN builds and GPU architectures. When none applies it raises `RuntimeError: GET was unable to find an engine
|
|
to execute this computation` — for instance for Sana's depthwise `Conv2d` in bfloat16. That is a property of the
|
|
runner, not of the code under test, so tests that can hit it are skipped there rather than failed. Any other
|
|
`RuntimeError` propagates untouched.
|
|
"""
|
|
try:
|
|
yield
|
|
except RuntimeError as e:
|
|
if "unable to find an engine" not in str(e):
|
|
raise
|
|
pytest.skip(f"cuDNN has no engine for this computation on {torch_device}: {e}")
|
|
|
|
|
|
@contextmanager
|
|
def buffered_writer(raw_f):
|
|
f = io.BufferedWriter(raw_f)
|
|
yield f
|
|
f.flush()
|
|
|
|
|
|
def export_to_ply(mesh, output_ply_path: str = None):
|
|
"""
|
|
Write a PLY file for a mesh.
|
|
"""
|
|
if output_ply_path is None:
|
|
output_ply_path = tempfile.NamedTemporaryFile(suffix=".ply").name
|
|
|
|
coords = mesh.verts.detach().cpu().numpy()
|
|
faces = mesh.faces.cpu().numpy()
|
|
rgb = np.stack([mesh.vertex_channels[x].detach().cpu().numpy() for x in "RGB"], axis=1)
|
|
|
|
with buffered_writer(open(output_ply_path, "wb")) as f:
|
|
f.write(b"ply\n")
|
|
f.write(b"format binary_little_endian 1.0\n")
|
|
f.write(bytes(f"element vertex {len(coords)}\n", "ascii"))
|
|
f.write(b"property float x\n")
|
|
f.write(b"property float y\n")
|
|
f.write(b"property float z\n")
|
|
if rgb is not None:
|
|
f.write(b"property uchar red\n")
|
|
f.write(b"property uchar green\n")
|
|
f.write(b"property uchar blue\n")
|
|
if faces is not None:
|
|
f.write(bytes(f"element face {len(faces)}\n", "ascii"))
|
|
f.write(b"property list uchar int vertex_index\n")
|
|
f.write(b"end_header\n")
|
|
|
|
if rgb is not None:
|
|
rgb = (rgb * 255.499).round().astype(int)
|
|
vertices = [
|
|
(*coord, *rgb)
|
|
for coord, rgb in zip(
|
|
coords.tolist(),
|
|
rgb.tolist(),
|
|
)
|
|
]
|
|
format = struct.Struct("<3f3B")
|
|
for item in vertices:
|
|
f.write(format.pack(*item))
|
|
else:
|
|
format = struct.Struct("<3f")
|
|
for vertex in coords.tolist():
|
|
f.write(format.pack(*vertex))
|
|
|
|
if faces is not None:
|
|
format = struct.Struct("<B3I")
|
|
for tri in faces.tolist():
|
|
f.write(format.pack(len(tri), *tri))
|
|
|
|
return output_ply_path
|
|
|
|
|
|
def export_to_obj(mesh, output_obj_path: str = None):
|
|
if output_obj_path is None:
|
|
output_obj_path = tempfile.NamedTemporaryFile(suffix=".obj").name
|
|
|
|
verts = mesh.verts.detach().cpu().numpy()
|
|
faces = mesh.faces.cpu().numpy()
|
|
|
|
vertex_colors = np.stack([mesh.vertex_channels[x].detach().cpu().numpy() for x in "RGB"], axis=1)
|
|
vertices = [
|
|
"{} {} {} {} {} {}".format(*coord, *color) for coord, color in zip(verts.tolist(), vertex_colors.tolist())
|
|
]
|
|
|
|
faces = ["f {} {} {}".format(str(tri[0] + 1), str(tri[1] + 1), str(tri[2] + 1)) for tri in faces.tolist()]
|
|
|
|
combined_data = ["v " + vertex for vertex in vertices] + faces
|
|
|
|
with open(output_obj_path, "w") as f:
|
|
f.writelines("\n".join(combined_data))
|
|
|
|
|
|
def export_to_video(video_frames: list[np.ndarray], output_video_path: str = None) -> str:
|
|
if is_opencv_available():
|
|
import cv2
|
|
else:
|
|
raise ImportError(BACKENDS_MAPPING["opencv"][1].format("export_to_video"))
|
|
if output_video_path is None:
|
|
output_video_path = tempfile.NamedTemporaryFile(suffix=".mp4").name
|
|
|
|
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
|
h, w, c = video_frames[0].shape
|
|
video_writer = cv2.VideoWriter(output_video_path, fourcc, fps=8, frameSize=(w, h))
|
|
for i in range(len(video_frames)):
|
|
img = cv2.cvtColor(video_frames[i], cv2.COLOR_RGB2BGR)
|
|
video_writer.write(img)
|
|
return output_video_path
|
|
|
|
|
|
def load_hf_numpy(path) -> np.ndarray:
|
|
base_url = "https://huggingface.co/datasets/fusing/diffusers-testing/resolve/main"
|
|
|
|
if not path.startswith("http://") and not path.startswith("https://"):
|
|
path = os.path.join(base_url, urllib.parse.quote(path))
|
|
|
|
return load_numpy(path)
|
|
|
|
|
|
# --- pytest conf functions --- #
|
|
|
|
# to avoid multiple invocation from tests/conftest.py and examples/conftest.py - make sure it's called only once
|
|
pytest_opt_registered = {}
|
|
|
|
|
|
def pytest_addoption_shared(parser):
|
|
"""
|
|
This function is to be called from `conftest.py` via `pytest_addoption` wrapper that has to be defined there.
|
|
|
|
It allows loading both `conftest.py` files at once without causing a failure due to adding the same `pytest`
|
|
option.
|
|
|
|
"""
|
|
option = "--make-reports"
|
|
if option not in pytest_opt_registered:
|
|
parser.addoption(
|
|
option,
|
|
action="store",
|
|
default=False,
|
|
help="generate report files. The value of this option is used as a prefix to report names",
|
|
)
|
|
pytest_opt_registered[option] = 1
|
|
|
|
|
|
def pytest_terminal_summary_main(tr, id):
|
|
"""
|
|
Generate multiple reports at the end of test suite run - each report goes into a dedicated file in the current
|
|
directory. The report files are prefixed with the test suite name.
|
|
|
|
This function emulates --duration and -rA pytest arguments.
|
|
|
|
This function is to be called from `conftest.py` via `pytest_terminal_summary` wrapper that has to be defined
|
|
there.
|
|
|
|
Args:
|
|
- tr: `terminalreporter` passed from `conftest.py`
|
|
- id: unique id like `tests` or `examples` that will be incorporated into the final reports filenames - this is
|
|
needed as some jobs have multiple runs of pytest, so we can't have them overwrite each other.
|
|
|
|
NB: this functions taps into a private _pytest API and while unlikely, it could break should
|
|
pytest do internal changes - also it calls default internal methods of terminalreporter which
|
|
can be hijacked by various `pytest-` plugins and interfere.
|
|
|
|
"""
|
|
from _pytest.config import create_terminal_writer
|
|
|
|
if not len(id):
|
|
id = "tests"
|
|
|
|
config = tr.config
|
|
orig_writer = config.get_terminal_writer()
|
|
orig_tbstyle = config.option.tbstyle
|
|
orig_reportchars = tr.reportchars
|
|
|
|
dir = "reports"
|
|
Path(dir).mkdir(parents=True, exist_ok=True)
|
|
report_files = {
|
|
k: f"{dir}/{id}_{k}.txt"
|
|
for k in [
|
|
"durations",
|
|
"errors",
|
|
"failures_long",
|
|
"failures_short",
|
|
"failures_line",
|
|
"passes",
|
|
"stats",
|
|
"summary_short",
|
|
"warnings",
|
|
]
|
|
}
|
|
|
|
# custom durations report
|
|
# note: there is no need to call pytest --durations=XX to get this separate report
|
|
# adapted from https://github.com/pytest-dev/pytest/blob/897f151e/src/_pytest/runner.py#L66
|
|
dlist = []
|
|
for replist in tr.stats.values():
|
|
for rep in replist:
|
|
if hasattr(rep, "duration"):
|
|
dlist.append(rep)
|
|
if dlist:
|
|
dlist.sort(key=lambda x: x.duration, reverse=True)
|
|
with open(report_files["durations"], "w") as f:
|
|
durations_min = 0.05 # sec
|
|
f.write("slowest durations\n")
|
|
for i, rep in enumerate(dlist):
|
|
if rep.duration < durations_min:
|
|
f.write(f"{len(dlist) - i} durations < {durations_min} secs were omitted")
|
|
break
|
|
f.write(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}\n")
|
|
|
|
def summary_failures_short(tr):
|
|
# expecting that the reports were --tb=long (default) so we chop them off here to the last frame
|
|
reports = tr.getreports("failed")
|
|
if not reports:
|
|
return
|
|
tr.write_sep("=", "FAILURES SHORT STACK")
|
|
for rep in reports:
|
|
msg = tr._getfailureheadline(rep)
|
|
tr.write_sep("_", msg, red=True, bold=True)
|
|
# chop off the optional leading extra frames, leaving only the last one
|
|
longrepr = re.sub(r".*_ _ _ (_ ){10,}_ _ ", "", rep.longreprtext, 0, re.M | re.S)
|
|
tr._tw.line(longrepr)
|
|
# note: not printing out any rep.sections to keep the report short
|
|
|
|
# use ready-made report funcs, we are just hijacking the filehandle to log to a dedicated file each
|
|
# adapted from https://github.com/pytest-dev/pytest/blob/897f151e/src/_pytest/terminal.py#L814
|
|
# note: some pytest plugins may interfere by hijacking the default `terminalreporter` (e.g.
|
|
# pytest-instafail does that)
|
|
|
|
# report failures with line/short/long styles
|
|
config.option.tbstyle = "auto" # full tb
|
|
with open(report_files["failures_long"], "w") as f:
|
|
tr._tw = create_terminal_writer(config, f)
|
|
tr.summary_failures()
|
|
|
|
# config.option.tbstyle = "short" # short tb
|
|
with open(report_files["failures_short"], "w") as f:
|
|
tr._tw = create_terminal_writer(config, f)
|
|
summary_failures_short(tr)
|
|
|
|
config.option.tbstyle = "line" # one line per error
|
|
with open(report_files["failures_line"], "w") as f:
|
|
tr._tw = create_terminal_writer(config, f)
|
|
tr.summary_failures()
|
|
|
|
with open(report_files["errors"], "w") as f:
|
|
tr._tw = create_terminal_writer(config, f)
|
|
tr.summary_errors()
|
|
|
|
with open(report_files["warnings"], "w") as f:
|
|
tr._tw = create_terminal_writer(config, f)
|
|
tr.summary_warnings() # normal warnings
|
|
tr.summary_warnings() # final warnings
|
|
|
|
tr.reportchars = "wPpsxXEf" # emulate -rA (used in summary_passes() and short_test_summary())
|
|
with open(report_files["passes"], "w") as f:
|
|
tr._tw = create_terminal_writer(config, f)
|
|
tr.summary_passes()
|
|
|
|
with open(report_files["summary_short"], "w") as f:
|
|
tr._tw = create_terminal_writer(config, f)
|
|
tr.short_test_summary()
|
|
|
|
with open(report_files["stats"], "w") as f:
|
|
tr._tw = create_terminal_writer(config, f)
|
|
tr.summary_stats()
|
|
|
|
# restore:
|
|
tr._tw = orig_writer
|
|
tr.reportchars = orig_reportchars
|
|
config.option.tbstyle = orig_tbstyle
|
|
|
|
|
|
# Adapted from https://github.com/huggingface/transformers/blob/000e52aec8850d3fe2f360adc6fd256e5b47fe4c/src/transformers..testing_utils.py#L1905
|
|
def is_flaky(max_attempts: int = 5, wait_before_retry: float | None = None, description: str | None = None):
|
|
"""
|
|
To decorate flaky tests (methods or entire classes). They will be retried on failures.
|
|
|
|
Args:
|
|
max_attempts (`int`, *optional*, defaults to 5):
|
|
The maximum number of attempts to retry the flaky test.
|
|
wait_before_retry (`float`, *optional*):
|
|
If provided, will wait that number of seconds before retrying the test.
|
|
description (`str`, *optional*):
|
|
A string to describe the situation (what / where / why is flaky, link to GH issue/PR comments, errors,
|
|
etc.)
|
|
"""
|
|
|
|
def decorator(obj):
|
|
# If decorating a class, wrap each test method on it
|
|
if inspect.isclass(obj):
|
|
for attr_name, attr_value in list(obj.__dict__.items()):
|
|
if callable(attr_value) and attr_name.startswith("test"):
|
|
# recursively decorate the method
|
|
setattr(obj, attr_name, decorator(attr_value))
|
|
return obj
|
|
|
|
# Otherwise we're decorating a single test function / method
|
|
@functools.wraps(obj)
|
|
def wrapper(*args, **kwargs):
|
|
retry_count = 1
|
|
while retry_count < max_attempts:
|
|
try:
|
|
return obj(*args, **kwargs)
|
|
except Exception as err:
|
|
msg = (
|
|
f"[FLAKY] {description or obj.__name__!r} "
|
|
f"failed on attempt {retry_count}/{max_attempts}: {err}"
|
|
)
|
|
print(msg, file=sys.stderr)
|
|
if wait_before_retry is not None:
|
|
time.sleep(wait_before_retry)
|
|
retry_count += 1
|
|
|
|
return obj(*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
return decorator
|
|
|
|
|
|
# Taken from: https://github.com/huggingface/transformers/blob/3658488ff77ff8d45101293e749263acf437f4d5/src/transformers..testing_utils.py#L1787
|
|
def run_test_in_subprocess(target_func, inputs=None, timeout=None):
|
|
"""
|
|
To run a test in a subprocess. In particular, this can avoid (GPU) memory issue.
|
|
|
|
Args:
|
|
target_func (`Callable`):
|
|
The function implementing the actual testing logic.
|
|
inputs (`dict`, *optional*, defaults to `None`):
|
|
The inputs that will be passed to `target_func` through an (input) queue.
|
|
timeout (`int`, *optional*, defaults to `None`):
|
|
The timeout (in seconds) that will be passed to the input and output queues. If not specified, the env.
|
|
variable `PYTEST_TIMEOUT` will be checked. If still `None`, its value will be set to `600`.
|
|
"""
|
|
if timeout is None:
|
|
timeout = int(os.environ.get("PYTEST_TIMEOUT", 600))
|
|
|
|
start_methohd = "spawn"
|
|
ctx = multiprocessing.get_context(start_methohd)
|
|
|
|
input_queue = ctx.Queue(1)
|
|
output_queue = ctx.JoinableQueue(1)
|
|
|
|
# We can't send test case objects to the child, otherwise we get issues regarding pickle.
|
|
input_queue.put(inputs, timeout=timeout)
|
|
|
|
process = ctx.Process(target=target_func, args=(input_queue, output_queue, timeout))
|
|
process.start()
|
|
# Kill the child process if we can't get outputs from it in time: otherwise, the hanging subprocess prevents
|
|
# the test to exit properly.
|
|
try:
|
|
results = output_queue.get(timeout=timeout)
|
|
output_queue.task_done()
|
|
except Exception as e:
|
|
process.terminate()
|
|
pytest.fail(str(e))
|
|
process.join(timeout=timeout)
|
|
|
|
if results["error"] is not None:
|
|
pytest.fail(f"{results['error']}")
|
|
|
|
|
|
class CaptureLogger:
|
|
"""
|
|
Args:
|
|
Context manager to capture `logging` streams
|
|
logger: 'logging` logger object
|
|
Returns:
|
|
The captured output is available via `self.out`
|
|
Example:
|
|
```python
|
|
>>> from diffusers import logging
|
|
>>> from diffusers.utils.testing_utils import CaptureLogger
|
|
|
|
>>> msg = "Testing 1, 2, 3"
|
|
>>> logging.set_verbosity_info()
|
|
>>> logger = logging.get_logger("diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion.py")
|
|
>>> with CaptureLogger(logger) as cl:
|
|
... logger.info(msg)
|
|
>>> assert cl.out, msg + "\n"
|
|
```
|
|
"""
|
|
|
|
def __init__(self, logger):
|
|
self.logger = logger
|
|
self.io = StringIO()
|
|
self.sh = logging.StreamHandler(self.io)
|
|
self.out = ""
|
|
|
|
def __enter__(self):
|
|
self.logger.addHandler(self.sh)
|
|
return self
|
|
|
|
def __exit__(self, *exc):
|
|
self.logger.removeHandler(self.sh)
|
|
self.out = self.io.getvalue()
|
|
|
|
def __repr__(self):
|
|
return f"captured: {self.out}\n"
|
|
|
|
|
|
def enable_full_determinism():
|
|
"""
|
|
Helper function for reproducible behavior during distributed training. See
|
|
- https://pytorch.org/docs/stable/notes/randomness.html for pytorch
|
|
"""
|
|
# Enable PyTorch deterministic mode. This potentially requires either the environment
|
|
# variable 'CUDA_LAUNCH_BLOCKING' or 'CUBLAS_WORKSPACE_CONFIG' to be set,
|
|
# depending on the CUDA version, so we set them both here
|
|
os.environ["CUDA_LAUNCH_BLOCKING"] = "1"
|
|
# Use larger workspace size for PyTorch 2.10+ to avoid CUBLAS_STATUS_NOT_INITIALIZED errors
|
|
# (catches 2.11 dev versions which report as >= 2.10)
|
|
if is_torch_version(">=", "2.10"):
|
|
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
|
|
else:
|
|
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":16:8"
|
|
torch.use_deterministic_algorithms(True)
|
|
|
|
# Enable CUDNN deterministic mode
|
|
torch.backends.cudnn.deterministic = True
|
|
torch.backends.cudnn.benchmark = False
|
|
torch.backends.cuda.matmul.allow_tf32 = False
|
|
|
|
|
|
def disable_full_determinism():
|
|
os.environ["CUDA_LAUNCH_BLOCKING"] = "0"
|
|
os.environ["CUBLAS_WORKSPACE_CONFIG"] = ""
|
|
torch.use_deterministic_algorithms(False)
|
|
|
|
|
|
# Utils for custom and alternative accelerator devices
|
|
def _is_torch_fp16_available(device):
|
|
if not is_torch_available():
|
|
return False
|
|
|
|
import torch
|
|
|
|
device = torch.device(device)
|
|
|
|
try:
|
|
x = torch.zeros((2, 2), dtype=torch.float16).to(device)
|
|
_ = torch.mul(x, x)
|
|
return True
|
|
|
|
except Exception as e:
|
|
if device.type == "cuda":
|
|
raise ValueError(
|
|
f"You have passed a device of type 'cuda' which should work with 'fp16', but 'cuda' does not seem to be correctly installed on your machine: {e}"
|
|
)
|
|
|
|
return False
|
|
|
|
|
|
def _is_torch_fp64_available(device):
|
|
if not is_torch_available():
|
|
return False
|
|
|
|
import torch
|
|
|
|
device = torch.device(device)
|
|
|
|
try:
|
|
x = torch.zeros((2, 2), dtype=torch.float64).to(device)
|
|
_ = torch.mul(x, x)
|
|
return True
|
|
|
|
except Exception as e:
|
|
if device.type == "cuda":
|
|
raise ValueError(
|
|
f"You have passed a device of type 'cuda' which should work with 'fp64', but 'cuda' does not seem to be correctly installed on your machine: {e}"
|
|
)
|
|
|
|
return False
|
|
|
|
|
|
# Guard these lookups for when Torch is not used - alternative accelerator support is for PyTorch
|
|
if is_torch_available():
|
|
# Behaviour flags
|
|
BACKEND_SUPPORTS_TRAINING = {"cuda": True, "xpu": True, "cpu": True, "mps": False, "default": True}
|
|
|
|
# Neuron device key: torch.neuron.current_device() returns an int (e.g. 0).
|
|
# We capture it once at import time if torch_neuronx is available so we can add it
|
|
# to all dispatch tables using the same key that torch_device is set to.
|
|
_neuron_device = (
|
|
torch.neuron.current_device()
|
|
if (is_torch_neuronx_available() and hasattr(torch, "neuron") and torch.neuron.is_available())
|
|
else None
|
|
)
|
|
|
|
if _neuron_device is not None:
|
|
BACKEND_SUPPORTS_TRAINING[_neuron_device] = False
|
|
|
|
|
|
# Device operations go through `TorchDeviceBackend`.
|
|
def backend_manual_seed(device: str, seed: int):
|
|
TorchDeviceBackend(device).manual_seed(seed)
|
|
|
|
|
|
def backend_synchronize(device: str):
|
|
TorchDeviceBackend(device).synchronize()
|
|
|
|
|
|
def backend_empty_cache(device: str):
|
|
TorchDeviceBackend(device).empty_cache()
|
|
|
|
|
|
def backend_device_count(device: str):
|
|
return TorchDeviceBackend(device).device_count()
|
|
|
|
|
|
def backend_reset_peak_memory_stats(device: str):
|
|
TorchDeviceBackend(device).reset_peak_memory_stats()
|
|
|
|
|
|
def backend_reset_max_memory_allocated(device: str):
|
|
# `reset_max_memory_allocated` is CUDA's deprecated alias of `reset_peak_memory_stats`.
|
|
TorchDeviceBackend(device).reset_peak_memory_stats()
|
|
|
|
|
|
def backend_max_memory_allocated(device: str):
|
|
return TorchDeviceBackend(device).max_memory_allocated()
|
|
|
|
|
|
# These are callables which return boolean behaviour flags and can be used to specify some
|
|
# device agnostic alternative where the feature is unsupported.
|
|
def backend_supports_training(device: str):
|
|
if not is_torch_available():
|
|
return False
|
|
|
|
if device not in BACKEND_SUPPORTS_TRAINING:
|
|
device = "default"
|
|
|
|
return BACKEND_SUPPORTS_TRAINING[device]
|
|
|
|
|
|
# Guard for when Torch is not available
|
|
if is_torch_available():
|
|
# Update device function dict mapping
|
|
def update_mapping_from_spec(device_fn_dict: dict[str, Callable], attribute_name: str):
|
|
try:
|
|
# Try to import the function directly
|
|
spec_fn = getattr(device_spec_module, attribute_name)
|
|
device_fn_dict[torch_device] = spec_fn
|
|
except AttributeError as e:
|
|
# If the function doesn't exist, and there is no default, throw an error
|
|
if "default" not in device_fn_dict:
|
|
raise AttributeError(
|
|
f"`{attribute_name}` not found in '{device_spec_path}' and no default fallback function found."
|
|
) from e
|
|
|
|
if "DIFFUSERS_TEST_DEVICE_SPEC" in os.environ:
|
|
device_spec_path = os.environ["DIFFUSERS_TEST_DEVICE_SPEC"]
|
|
if not Path(device_spec_path).is_file():
|
|
raise ValueError(f"Specified path to device specification file is not found. Received {device_spec_path}")
|
|
|
|
try:
|
|
import_name = device_spec_path[: device_spec_path.index(".py")]
|
|
except ValueError as e:
|
|
raise ValueError(f"Provided device spec file is not a Python file! Received {device_spec_path}") from e
|
|
|
|
device_spec_module = importlib.import_module(import_name)
|
|
|
|
try:
|
|
device_name = device_spec_module.DEVICE_NAME
|
|
except AttributeError:
|
|
raise AttributeError("Device spec file did not contain `DEVICE_NAME`")
|
|
|
|
if "DIFFUSERS_TEST_DEVICE" in os.environ and torch_device != device_name:
|
|
msg = f"Mismatch between environment variable `DIFFUSERS_TEST_DEVICE` '{torch_device}' and device found in spec '{device_name}'\n"
|
|
msg += "Either unset `DIFFUSERS_TEST_DEVICE` or ensure it matches device spec name."
|
|
raise ValueError(msg)
|
|
|
|
torch_device = device_name
|
|
|
|
# `SUPPORTS_TRAINING` is the only per-device table left. Device operations come from the backend's own
|
|
# `torch.<backend>` module through `TorchDeviceBackend`, so a spec file no longer supplies them.
|
|
update_mapping_from_spec(BACKEND_SUPPORTS_TRAINING, "SUPPORTS_TRAINING")
|
|
|
|
|
|
# Modified from https://github.com/huggingface/transformers/blob/cdfb018d0300fef3b07d9220f3efe9c2a9974662/src/transformers..testing_utils.py#L3090
|
|
|
|
# Type definition of key used in `Expectations` class.
|
|
DeviceProperties = Tuple[Union[str, None], Union[int, None]]
|
|
|
|
|
|
@functools.lru_cache
|
|
def get_device_properties() -> DeviceProperties:
|
|
"""
|
|
Get environment device properties.
|
|
"""
|
|
if IS_CUDA_SYSTEM or IS_ROCM_SYSTEM:
|
|
import torch
|
|
|
|
major, _ = torch.cuda.get_device_capability()
|
|
if IS_ROCM_SYSTEM:
|
|
return ("rocm", major)
|
|
else:
|
|
return ("cuda", major)
|
|
elif IS_XPU_SYSTEM:
|
|
import torch
|
|
|
|
# To get more info of the architecture meaning and bit allocation, refer to https://github.com/intel/llvm/blob/sycl/sycl/include/sycl/ext/oneapi/experimental/device_architecture.def
|
|
arch = torch.xpu.get_device_capability()["architecture"]
|
|
gen_mask = 0x000000FF00000000
|
|
gen = (arch & gen_mask) >> 32
|
|
return ("xpu", gen)
|
|
else:
|
|
return (torch_device, None)
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
DevicePropertiesUserDict = UserDict[DeviceProperties, Any]
|
|
else:
|
|
DevicePropertiesUserDict = UserDict
|
|
|
|
if is_torch_available():
|
|
from diffusers.hooks._common import _GO_LC_SUPPORTED_PYTORCH_LAYERS
|
|
from diffusers.hooks.group_offloading import (
|
|
_GROUP_ID_LAZY_LEAF,
|
|
_compute_group_hash,
|
|
_find_parent_module_in_module_dict,
|
|
_gather_buffers_with_no_group_offloading_parent,
|
|
_gather_parameters_with_no_group_offloading_parent,
|
|
)
|
|
|
|
def _get_expected_safetensors_files(
|
|
module: torch.nn.Module,
|
|
offload_to_disk_path: str,
|
|
offload_type: str,
|
|
num_blocks_per_group: int | None = None,
|
|
block_modules: list[str] | None = None,
|
|
module_prefix: str = "",
|
|
) -> set[str]:
|
|
expected_files = set()
|
|
|
|
def get_hashed_filename(group_id: str) -> str:
|
|
short_hash = _compute_group_hash(group_id)
|
|
return os.path.join(offload_to_disk_path, f"group_{short_hash}.safetensors")
|
|
|
|
if offload_type == "block_level":
|
|
if num_blocks_per_group is None:
|
|
raise ValueError("num_blocks_per_group must be provided for 'block_level' offloading.")
|
|
|
|
block_modules_set = set(block_modules) if block_modules is not None else set()
|
|
|
|
modules_with_group_offloading = set()
|
|
unmatched_modules = []
|
|
for name, submodule in module.named_children():
|
|
if name in block_modules_set:
|
|
new_prefix = f"{module_prefix}{name}." if module_prefix else f"{name}."
|
|
submodule_files = _get_expected_safetensors_files(
|
|
submodule, offload_to_disk_path, offload_type, num_blocks_per_group, block_modules, new_prefix
|
|
)
|
|
expected_files.update(submodule_files)
|
|
modules_with_group_offloading.add(name)
|
|
|
|
elif isinstance(submodule, (torch.nn.ModuleList, torch.nn.Sequential)):
|
|
for i in range(0, len(submodule), num_blocks_per_group):
|
|
current_modules = submodule[i : i + num_blocks_per_group]
|
|
if not current_modules:
|
|
continue
|
|
group_id = f"{module_prefix}{name}_{i}_{i + len(current_modules) - 1}"
|
|
expected_files.add(get_hashed_filename(group_id))
|
|
for j in range(i, i + len(current_modules)):
|
|
modules_with_group_offloading.add(f"{name}.{j}")
|
|
else:
|
|
unmatched_modules.append(submodule)
|
|
|
|
parameters = _gather_parameters_with_no_group_offloading_parent(module, modules_with_group_offloading)
|
|
buffers = _gather_buffers_with_no_group_offloading_parent(module, modules_with_group_offloading)
|
|
|
|
if len(unmatched_modules) > 0 or len(parameters) > 0 or len(buffers) > 0:
|
|
expected_files.add(get_hashed_filename(f"{module_prefix}{module.__class__.__name__}_unmatched_group"))
|
|
|
|
elif offload_type == "leaf_level":
|
|
# Handle leaf-level module groups
|
|
for name, submodule in module.named_modules():
|
|
if isinstance(submodule, _GO_LC_SUPPORTED_PYTORCH_LAYERS):
|
|
# These groups will always have parameters, so a file is expected
|
|
expected_files.add(get_hashed_filename(name))
|
|
|
|
# Handle groups for non-leaf parameters/buffers
|
|
modules_with_group_offloading = {
|
|
name for name, sm in module.named_modules() if isinstance(sm, _GO_LC_SUPPORTED_PYTORCH_LAYERS)
|
|
}
|
|
parameters = _gather_parameters_with_no_group_offloading_parent(module, modules_with_group_offloading)
|
|
buffers = _gather_buffers_with_no_group_offloading_parent(module, modules_with_group_offloading)
|
|
|
|
all_orphans = parameters + buffers
|
|
if all_orphans:
|
|
parent_to_tensors = {}
|
|
module_dict = dict(module.named_modules())
|
|
for tensor_name, _ in all_orphans:
|
|
parent_name = _find_parent_module_in_module_dict(tensor_name, module_dict)
|
|
if parent_name not in parent_to_tensors:
|
|
parent_to_tensors[parent_name] = []
|
|
parent_to_tensors[parent_name].append(tensor_name)
|
|
|
|
for parent_name in parent_to_tensors:
|
|
# A file is expected for each parent that gathers orphaned tensors
|
|
expected_files.add(get_hashed_filename(parent_name))
|
|
expected_files.add(get_hashed_filename(_GROUP_ID_LAZY_LEAF))
|
|
|
|
else:
|
|
raise ValueError(f"Unsupported offload_type: {offload_type}")
|
|
|
|
return expected_files
|
|
|
|
def _check_safetensors_serialization(
|
|
module: torch.nn.Module,
|
|
offload_to_disk_path: str,
|
|
offload_type: str,
|
|
num_blocks_per_group: int | None = None,
|
|
block_modules: list[str] | None = None,
|
|
) -> bool:
|
|
if not os.path.isdir(offload_to_disk_path):
|
|
return False, None, None
|
|
|
|
expected_files = _get_expected_safetensors_files(
|
|
module, offload_to_disk_path, offload_type, num_blocks_per_group, block_modules
|
|
)
|
|
actual_files = set(glob.glob(os.path.join(offload_to_disk_path, "*.safetensors")))
|
|
missing_files = expected_files - actual_files
|
|
extra_files = actual_files - expected_files
|
|
|
|
is_correct = not missing_files and not extra_files
|
|
return is_correct, extra_files, missing_files
|
|
|
|
|
|
class Expectations(DevicePropertiesUserDict):
|
|
def get_expectation(self) -> Any:
|
|
"""
|
|
Find best matching expectation based on environment device properties.
|
|
"""
|
|
return self.find_expectation(get_device_properties())
|
|
|
|
@staticmethod
|
|
def is_default(key: DeviceProperties) -> bool:
|
|
return all(p is None for p in key)
|
|
|
|
@staticmethod
|
|
def score(key: DeviceProperties, other: DeviceProperties) -> int:
|
|
"""
|
|
Returns score indicating how similar two instances of the `Properties` tuple are. Points are calculated using
|
|
bits, but documented as int. Rules are as follows:
|
|
* Matching `type` gives 8 points.
|
|
* Semi-matching `type`, for example cuda and rocm, gives 4 points.
|
|
* Matching `major` (compute capability major version) gives 2 points.
|
|
* Default expectation (if present) gives 1 points.
|
|
"""
|
|
(device_type, major) = key
|
|
(other_device_type, other_major) = other
|
|
|
|
score = 0b0
|
|
if device_type == other_device_type:
|
|
score |= 0b1000
|
|
elif device_type in ["cuda", "rocm"] and other_device_type in ["cuda", "rocm"]:
|
|
score |= 0b100
|
|
|
|
if major == other_major and other_major is not None:
|
|
score |= 0b10
|
|
|
|
if Expectations.is_default(other):
|
|
score |= 0b1
|
|
|
|
return int(score)
|
|
|
|
def find_expectation(self, key: DeviceProperties = (None, None)) -> Any:
|
|
"""
|
|
Find best matching expectation based on provided device properties.
|
|
"""
|
|
(result_key, result) = max(self.data.items(), key=lambda x: Expectations.score(key, x[0]))
|
|
|
|
if Expectations.score(key, result_key) == 0:
|
|
raise ValueError(f"No matching expectation found for {key}")
|
|
|
|
return result
|
|
|
|
def __repr__(self):
|
|
return f"{self.data}"
|