Skip to content

Commit 1a69e50

Browse files
authored
Merge branch 'main' into lora-tests-migration-pipelines
2 parents c01ee97 + f83ba3b commit 1a69e50

9 files changed

Lines changed: 219 additions & 187 deletions

File tree

docs/source/en/optimization/attention_backends.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,20 @@ with attention_backend("_flash_3_hub"):
8282
> [!TIP]
8383
> Most attention backends support `torch.compile` without graph breaks and can be used to further speed up inference.
8484
85+
## Trusting remote kernels
86+
87+
Hub backends and other kernel-backed features (such as [GGUF](../quantization/gguf) and [Nunchaku Lite](../quantization/nunchaku)) download compute kernels from the Hub with [`kernels`](https://github.com/huggingface/kernels) and execute their code locally.
88+
89+
By default, `kernels` only loads a kernel when its publisher is a trusted kernel publisher on the Hub. Kernels published under the [`kernels-community`](https://huggingface.co/kernels-community) organization are trusted, so Diffusers loads them without any additional configuration. The `_flash_3_hub`, `flash_hub`, `sage_hub`, and the other Hub attention backends all resolve to `kernels-community` repositories.
90+
91+
Kernels from any other publisher are not vetted. Loading one downloads and runs code that Diffusers cannot vouch for, so Diffusers keeps it disabled unless you explicitly opt in with the `DIFFUSERS_TRUST_REMOTE_KERNELS` environment variable. When set, Diffusers forwards `trust_remote_code=True` to `kernels` so it loads kernels from untrusted publishers too.
92+
93+
```bash
94+
export DIFFUSERS_TRUST_REMOTE_KERNELS=true
95+
```
96+
97+
Only enable this after inspecting the kernel repository, since it grants the downloaded code the ability to run on your machine. Without it, loading a kernel from an untrusted publisher raises an error. Diffusers performs this check itself, so it also applies to `kernels<0.14.0`, which predates the `trust_remote_code` argument. Setting `DIFFUSERS_DISABLE_REMOTE_CODE=true` disables remote code globally and takes precedence over `DIFFUSERS_TRUST_REMOTE_KERNELS`.
98+
8599
## Checks
86100

87101
The attention dispatcher includes debugging checks that catch common errors before they cause problems.

docs/source/en/quantization/gguf.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ pip install -U kernels
6363

6464
Once installed, set `DIFFUSERS_GGUF_CUDA_KERNELS=true` to use optimized kernels when available. Note that CUDA kernels may introduce minor numerical differences compared to the original GGUF implementation, potentially causing subtle visual variations in generated images. To disable CUDA kernel usage, set the environment variable `DIFFUSERS_GGUF_CUDA_KERNELS=false`.
6565

66+
The GGUF kernels are downloaded from the [`Isotr0py/ggml`](https://huggingface.co/Isotr0py/ggml) repository, whose publisher is not a trusted kernel publisher on the Hub. Loading it downloads and executes code from the Hub, so Diffusers requires you to explicitly opt in by setting `DIFFUSERS_TRUST_REMOTE_KERNELS=true`. See [Trusting remote kernels](../optimization/attention_backends#trusting-remote-kernels) for details.
67+
6668
## Supported Quantization Types
6769

6870
- BF16

docs/source/en/quantization/nunchaku.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ The kernels package supplies the optimized CUDA kernels, which load automaticall
2727
pip install -U kernels
2828
```
2929

30+
Nunchaku Lite loads its kernels from the [`rootonchair/nunchaku-lite-kernels`](https://huggingface.co/rootonchair/nunchaku-lite-kernels) repository, whose publisher is not a trusted kernel publisher on the Hub. Loading it downloads and executes code from the Hub, so Diffusers requires you to explicitly opt in by setting `DIFFUSERS_TRUST_REMOTE_KERNELS=true`. See [Trusting remote kernels](../optimization/attention_backends#trusting-remote-kernels) for details.
31+
3032
## Load a quantized pipeline
3133

3234
Load the prequantized pipeline with [`~DiffusionPipeline.from_pretrained`], which reads the quantization

src/diffusers/quantizers/gguf/utils.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
import torch
2121
import torch.nn as nn
2222

23-
from ...utils import is_accelerate_available, is_kernels_available
23+
from ...utils import is_accelerate_available, is_kernels_available, is_kernels_version
24+
from ...utils.constants import DIFFUSERS_TRUST_REMOTE_KERNELS
2425

2526

2627
if is_accelerate_available():
@@ -37,7 +38,15 @@
3738
if can_use_cuda_kernels and is_kernels_available():
3839
from kernels import get_kernel
3940

40-
ops = get_kernel("Isotr0py/ggml")
41+
if not DIFFUSERS_TRUST_REMOTE_KERNELS:
42+
raise ValueError(
43+
"`Isotr0py/ggml` is not published by a trusted kernel publisher on the Hub, so loading it downloads "
44+
"and executes remote code. Set `DIFFUSERS_TRUST_REMOTE_KERNELS=true` to allow it, or set "
45+
"`DIFFUSERS_GGUF_CUDA_KERNELS=false` to run without the CUDA kernels."
46+
)
47+
# `kernels<0.14.0` has no `trust_remote_code` argument and executes the downloaded code unconditionally.
48+
trust_kwargs = {"trust_remote_code": True} if is_kernels_version(">=", "0.14.0") else {}
49+
ops = get_kernel("Isotr0py/ggml", **trust_kwargs)
4150
else:
4251
ops = None
4352

src/diffusers/quantizers/nunchaku/utils.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
import torch
99
import torch.nn as nn
1010

11-
from ...utils import is_accelerate_available, is_kernels_available
11+
from ...utils import is_accelerate_available, is_kernels_available, is_kernels_version
12+
from ...utils.constants import DIFFUSERS_TRUST_REMOTE_KERNELS
1213

1314

1415
if is_accelerate_available():
@@ -22,7 +23,14 @@
2223
if is_kernels_available():
2324
from kernels import get_kernel
2425

25-
ops = get_kernel(_HF_KERNEL_REPO, version=_HF_KERNEL_VERSION, trust_remote_code=True).ops
26+
if not DIFFUSERS_TRUST_REMOTE_KERNELS:
27+
raise ValueError(
28+
f"`{_HF_KERNEL_REPO}` is not published by a trusted kernel publisher on the Hub, so loading it "
29+
"downloads and executes remote code. Set `DIFFUSERS_TRUST_REMOTE_KERNELS=true` to allow it."
30+
)
31+
# `kernels<0.14.0` has no `trust_remote_code` argument and executes the downloaded code unconditionally.
32+
trust_kwargs = {"trust_remote_code": True} if is_kernels_version(">=", "0.14.0") else {}
33+
ops = get_kernel(_HF_KERNEL_REPO, version=_HF_KERNEL_VERSION, **trust_kwargs).ops
2634
else:
2735
raise ImportError(
2836
"Loading Nunchaku checkpoints requires the Hugging Face `kernels` package. "

src/diffusers/utils/constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@
4848
HF_ENABLE_PARALLEL_LOADING = os.environ.get("HF_ENABLE_PARALLEL_LOADING", "").upper() in ENV_VARS_TRUE_VALUES
4949
DIFFUSERS_DISABLE_REMOTE_CODE = os.getenv("DIFFUSERS_DISABLE_REMOTE_CODE", "false").upper() in ENV_VARS_TRUE_VALUES
5050
DIFFUSERS_SDNQ_TRANSFORMERS = os.getenv("DIFFUSERS_SDNQ_TRANSFORMERS", "false").upper() in ENV_VARS_TRUE_VALUES
51+
# Kernels published by untrusted publishers execute remote code, so a globally disabled remote code wins over the opt-in.
52+
DIFFUSERS_TRUST_REMOTE_KERNELS = (
53+
os.getenv("DIFFUSERS_TRUST_REMOTE_KERNELS", "false").upper() in ENV_VARS_TRUE_VALUES
54+
and not DIFFUSERS_DISABLE_REMOTE_CODE
55+
)
5156

5257
# Below should be `True` if the current version of `peft` and `transformers` are compatible with
5358
# PEFT backend. Will automatically fall back to PEFT backend if the correct versions of the libraries are

tests/pipelines/flux2/test_pipeline_flux2_klein.py

Lines changed: 78 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import gc
22
import os
3-
import unittest
43

54
import numpy as np
5+
import pytest
66
import torch
77
from PIL import Image
88
from transformers import Qwen2TokenizerFast, Qwen3Config, Qwen3ForCausalLM
@@ -15,22 +15,26 @@
1515
)
1616

1717
from ...testing_utils import (
18+
assert_tensors_close,
1819
backend_empty_cache,
1920
backend_synchronize,
2021
require_torch_neuron,
2122
torch_device,
2223
)
23-
from ..test_pipelines_common import PipelineTesterMixin, check_qkv_fused_layers_exist
24+
from ..testing_utils import (
25+
BasePipelineTesterConfig,
26+
MemoryTesterMixin,
27+
PipelineTesterMixin,
28+
check_qkv_fused_layers_exist,
29+
)
2430

2531

26-
class Flux2KleinPipelineFastTests(PipelineTesterMixin, unittest.TestCase):
32+
class Flux2KleinPipelineTesterConfig(BasePipelineTesterConfig):
2733
pipeline_class = Flux2KleinPipeline
28-
params = frozenset(["prompt", "height", "width", "guidance_scale", "prompt_embeds"])
29-
batch_params = frozenset(["prompt"])
30-
31-
test_xformers_attention = False
32-
test_layerwise_casting = True
33-
test_group_offloading = True
34+
required_input_params_in_call_signature = frozenset(
35+
["prompt", "height", "width", "guidance_scale", "prompt_embeds"]
36+
)
37+
batch_input_params = frozenset(["prompt"])
3438

3539
def get_dummy_components(self, num_layers: int = 1, num_single_layers: int = 1):
3640
torch.manual_seed(0)
@@ -90,67 +94,70 @@ def get_dummy_components(self, num_layers: int = 1, num_single_layers: int = 1):
9094
"vae": vae,
9195
}
9296

93-
def get_dummy_inputs(self, device, seed=0):
94-
if str(device).startswith("mps"):
95-
generator = torch.manual_seed(seed)
96-
else:
97-
generator = torch.Generator(device="cpu").manual_seed(seed)
98-
97+
def get_dummy_inputs(self):
9998
inputs = {
10099
"prompt": "a dog is dancing",
101-
"generator": generator,
100+
"generator": self.get_generator(0),
102101
"num_inference_steps": 2,
103102
"guidance_scale": 4.0,
104103
"height": 8,
105104
"width": 8,
106105
"max_sequence_length": 64,
107-
"output_type": "np",
106+
# Request torch outputs so tests compare torch tensors directly (see `BasePipelineTesterConfig`).
107+
# Note `"pt"` images are `(batch, channels, height, width)`, unlike `"np"` (`(batch, h, w, c)`).
108+
"output_type": "pt",
108109
"text_encoder_out_layers": (1,),
109110
}
110111
return inputs
111112

113+
114+
class TestFlux2KleinPipeline(Flux2KleinPipelineTesterConfig, PipelineTesterMixin):
112115
def test_fused_qkv_projections(self):
113-
device = "cpu" # ensure determinism for the device-dependent torch.Generator
114-
components = self.get_dummy_components()
115-
pipe = self.pipeline_class(**components)
116-
pipe = pipe.to(device)
117-
pipe.set_progress_bar_config(disable=None)
116+
pipe = self.get_pipeline()
118117

119-
inputs = self.get_dummy_inputs(device)
118+
inputs = self.get_dummy_inputs()
120119
image = pipe(**inputs).images
121-
original_image_slice = image[0, -3:, -3:, -1]
120+
original_image_slice = image[0, -1, -3:, -3:]
122121

123122
pipe.transformer.fuse_qkv_projections()
124-
self.assertTrue(
125-
check_qkv_fused_layers_exist(pipe.transformer, ["to_qkv"]),
126-
("Something wrong with the fused attention layers. Expected all the attention projections to be fused."),
123+
assert check_qkv_fused_layers_exist(pipe.transformer, ["to_qkv"]), (
124+
"Something wrong with the fused attention layers. Expected all the attention projections to be fused."
127125
)
128126

129-
inputs = self.get_dummy_inputs(device)
127+
inputs = self.get_dummy_inputs()
130128
image = pipe(**inputs).images
131-
image_slice_fused = image[0, -3:, -3:, -1]
129+
image_slice_fused = image[0, -1, -3:, -3:]
132130

133131
pipe.transformer.unfuse_qkv_projections()
134-
inputs = self.get_dummy_inputs(device)
132+
inputs = self.get_dummy_inputs()
135133
image = pipe(**inputs).images
136-
image_slice_disabled = image[0, -3:, -3:, -1]
137-
138-
self.assertTrue(
139-
np.allclose(original_image_slice, image_slice_fused, atol=1e-3, rtol=1e-3),
140-
("Fusion of QKV projections shouldn't affect the outputs."),
134+
image_slice_disabled = image[0, -1, -3:, -3:]
135+
136+
assert_tensors_close(
137+
original_image_slice,
138+
image_slice_fused,
139+
atol=1e-3,
140+
rtol=1e-3,
141+
msg="Fusion of QKV projections shouldn't affect the outputs.",
141142
)
142-
self.assertTrue(
143-
np.allclose(image_slice_fused, image_slice_disabled, atol=1e-3, rtol=1e-3),
144-
("Outputs, with QKV projection fusion enabled, shouldn't change when fused QKV projections are disabled."),
143+
assert_tensors_close(
144+
image_slice_fused,
145+
image_slice_disabled,
146+
atol=1e-3,
147+
rtol=1e-3,
148+
msg="Outputs, with QKV projection fusion enabled, shouldn't change when fused QKV projections are disabled.",
145149
)
146-
self.assertTrue(
147-
np.allclose(original_image_slice, image_slice_disabled, atol=1e-2, rtol=1e-2),
148-
("Original outputs should match when fused QKV projections are disabled."),
150+
assert_tensors_close(
151+
original_image_slice,
152+
image_slice_disabled,
153+
atol=1e-2,
154+
rtol=1e-2,
155+
msg="Original outputs should match when fused QKV projections are disabled.",
149156
)
150157

151158
def test_image_output_shape(self):
152-
pipe = self.pipeline_class(**self.get_dummy_components()).to(torch_device)
153-
inputs = self.get_dummy_inputs(torch_device)
159+
pipe = self.get_pipeline().to(torch_device)
160+
inputs = self.get_dummy_inputs()
154161

155162
height_width_pairs = [(32, 32), (72, 57)]
156163
for height, width in height_width_pairs:
@@ -159,55 +166,55 @@ def test_image_output_shape(self):
159166

160167
inputs.update({"height": height, "width": width})
161168
image = pipe(**inputs).images[0]
162-
output_height, output_width, _ = image.shape
163-
self.assertEqual(
164-
(output_height, output_width),
165-
(expected_height, expected_width),
166-
f"Output shape {image.shape} does not match expected shape {(expected_height, expected_width)}",
169+
_, output_height, output_width = image.shape
170+
assert (output_height, output_width) == (expected_height, expected_width), (
171+
f"Output shape {image.shape} does not match expected shape {(expected_height, expected_width)}"
167172
)
168173

169174
def test_image_input(self):
170-
device = "cpu"
171-
pipe = self.pipeline_class(**self.get_dummy_components()).to(device)
172-
inputs = self.get_dummy_inputs(device)
175+
pipe = self.get_pipeline()
176+
inputs = self.get_dummy_inputs()
173177

174178
inputs["image"] = Image.new("RGB", (64, 64))
175-
image = pipe(**inputs).images.flatten()
176-
generated_slice = np.concatenate([image[:8], image[-8:]])
179+
# Permute the `"pt"` output to the `"np"` layout before flattening so the slice matches the recorded values.
180+
image = pipe(**inputs).images.permute(0, 2, 3, 1).flatten()
181+
generated_slice = torch.cat([image[:8], image[-8:]])
177182
# fmt: off
178-
expected_slice = np.array(
183+
expected_slice = torch.tensor(
179184
[
180185
0.8255048 , 0.66054785, 0.6643694 , 0.67462724, 0.5494932 , 0.3480271 , 0.52535003, 0.44510138, 0.23549396, 0.21372932, 0.21166152, 0.63198495, 0.49942136, 0.39147034, 0.49156153, 0.3713916
181186
]
182187
)
183188
# fmt: on
184-
assert np.allclose(expected_slice, generated_slice, atol=1e-4, rtol=1e-4)
189+
assert_tensors_close(generated_slice, expected_slice, atol=1e-4, rtol=1e-4)
185190

186-
@unittest.skip("Needs to be revisited")
191+
@pytest.mark.skip("Needs to be revisited")
187192
def test_encode_prompt_works_in_isolation(self):
188193
pass
189194

190195

196+
class TestFlux2KleinPipelineMemory(Flux2KleinPipelineTesterConfig, MemoryTesterMixin):
197+
"""Memory optimization tests (CPU offload, group offload, layerwise casting) for the Flux2 Klein pipeline."""
198+
199+
191200
@require_torch_neuron
192-
class Flux2KleinPipelineIntegrationTests(unittest.TestCase):
201+
class TestFlux2KleinPipelineIntegration:
193202
ckpt_id = "black-forest-labs/FLUX.2-klein-4B"
194203
prompt = "A small cactus with a happy face in the Sahara desert."
195204

196-
def setUp(self):
197-
super().setUp()
198-
self._saved_env = {}
205+
@pytest.fixture(autouse=True)
206+
def neuron_env(self):
207+
saved_env = {}
199208
neff_cache_dir = "/tmp/neff_cache"
200209
os.makedirs(neff_cache_dir, exist_ok=True)
201210
for key in ("TORCH_NEURONX_NEFF_CACHE_DIR", "TORCH_NEURONX_ENABLE_NKI_SDPA"):
202-
self._saved_env[key] = os.environ.get(key)
211+
saved_env[key] = os.environ.get(key)
203212
os.environ["TORCH_NEURONX_NEFF_CACHE_DIR"] = neff_cache_dir
204213
os.environ.setdefault("TORCH_NEURONX_ENABLE_NKI_SDPA", "0")
205214
gc.collect()
206215
backend_empty_cache(torch_device)
207-
208-
def tearDown(self):
209-
super().tearDown()
210-
for key, original in self._saved_env.items():
216+
yield
217+
for key, original in saved_env.items():
211218
if original is None:
212219
os.environ.pop(key, None)
213220
else:
@@ -234,12 +241,11 @@ def test_flux2_klein_inference_512(self):
234241
).images
235242

236243
image_slice = image[0, -3:, -3:, -1]
237-
self.assertEqual(image.shape, (1, 512, 512, 3))
238-
self.assertTrue(np.all((image >= 0.0) & (image <= 1.0)), "Pixel values must be in [0, 1]")
244+
assert image.shape == (1, 512, 512, 3)
245+
assert np.all((image >= 0.0) & (image <= 1.0)), "Pixel values must be in [0, 1]"
239246
expected_slice = np.array([0.3652, 0.3574, 0.3633, 0.4102, 0.4062, 0.4043, 0.4453, 0.4355, 0.4570])
240-
self.assertLess(np.abs(image_slice.flatten() - expected_slice).max(), 5e-2)
247+
assert np.abs(image_slice.flatten() - expected_slice).max() < 5e-2
241248

242-
@require_torch_neuron
243249
def test_flux2_klein_neuron_compile_128(self):
244250
from torch_neuronx.neuron_dynamo_backend import set_model_name
245251

@@ -273,9 +279,6 @@ def test_flux2_klein_neuron_compile_128(self):
273279
output_type="np",
274280
).images
275281

276-
self.assertEqual(image.shape, (1, 128, 128, 3))
277-
self.assertFalse(np.isnan(image).any(), "Output contains NaN values")
278-
self.assertTrue(
279-
(image >= 0.0).all() and (image <= 1.0).all(),
280-
"Output pixel values outside [0, 1]",
281-
)
282+
assert image.shape == (1, 128, 128, 3)
283+
assert not np.isnan(image).any(), "Output contains NaN values"
284+
assert (image >= 0.0).all() and (image <= 1.0).all(), "Output pixel values outside [0, 1]"

0 commit comments

Comments
 (0)