Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
98f6c8c
draft:add neuron as a legit backend
JingyaHuang Mar 18, 2026
c58b8b8
Merge branch 'huggingface:main' into add-neuron-backend
JingyaHuang Mar 18, 2026
3367409
Merge branch 'huggingface:main' into add-neuron-backend
JingyaHuang Mar 19, 2026
0c51734
Merge branch 'main' into add-neuron-backend
JingyaHuang Mar 25, 2026
a76953c
feat: neuron-specific changes in the pipeline
JingyaHuang Mar 26, 2026
2480388
tests: eager tests
JingyaHuang Mar 27, 2026
929ab72
fix: style
JingyaHuang Apr 9, 2026
52cac76
Merge branch 'huggingface:main' into add-neuron-backend
JingyaHuang Apr 9, 2026
28a5086
Merge branch 'add-neuron-backend' of github.com:JingyaHuang/diffusers…
JingyaHuang Apr 9, 2026
68689e5
Merge branch 'huggingface:main' into add-neuron-backend
JingyaHuang Apr 10, 2026
da79308
Merge branch 'main' into add-neuron-backend
JingyaHuang Apr 10, 2026
3bb9c7c
fix:apr_02 beta
JingyaHuang Apr 10, 2026
c4facab
Merge branch 'add-neuron-backend' of github.com:JingyaHuang/diffusers…
JingyaHuang Apr 10, 2026
1eb5ff9
Merge branch 'huggingface:main' into add-neuron-backend
JingyaHuang Apr 13, 2026
0d927b3
Merge branch 'main' into add-neuron-backend
JingyaHuang Apr 22, 2026
d1a8911
Merge branch 'huggingface:main' into add-neuron-backend
JingyaHuang May 4, 2026
c9e55ae
Merge branch 'main' into add-neuron-backend
JingyaHuang May 11, 2026
291171b
cleanup: remove tp part, for another pr
JingyaHuang May 11, 2026
f1caec0
fix: restore ring_anything to ContextParallelConfig after over-aggres…
JingyaHuang May 11, 2026
510a914
removal: style fix
JingyaHuang May 11, 2026
c312167
tests:sdxl + flux2
JingyaHuang May 12, 2026
1165f9f
tests: simplify
JingyaHuang May 13, 2026
5d13779
tests: simplify
JingyaHuang May 13, 2026
c007709
fix style
JingyaHuang May 13, 2026
c608bbb
Merge branch 'main' into add-neuron-backend
JingyaHuang May 13, 2026
fb5ea94
fix style
JingyaHuang May 13, 2026
9d93c68
fix style for doc-builder
JingyaHuang May 13, 2026
332b5a6
Merge branch 'main' into add-neuron-backend
JingyaHuang May 18, 2026
e8bf642
review: address comments
JingyaHuang May 18, 2026
1e25f3e
Merge branch 'main' into add-neuron-backend
JingyaHuang May 18, 2026
a3b6ccb
review:apply suggestion for the fix of index_for_timtestep
JingyaHuang May 20, 2026
86e550a
review: stronger guard on image slice
JingyaHuang May 20, 2026
c8e9716
Merge branch 'add-neuron-backend' of github.com:JingyaHuang/diffusers…
JingyaHuang May 20, 2026
45a8812
Apply suggestions from code review
JingyaHuang May 20, 2026
6c025c2
Merge branch 'main' into add-neuron-backend
JingyaHuang May 20, 2026
a18bfc5
Merge branch 'main' into add-neuron-backend
JingyaHuang May 21, 2026
1bf6b56
Merge branch 'main' into add-neuron-backend
JingyaHuang May 22, 2026
3144cfe
Merge branch 'main' into add-neuron-backend
JingyaHuang May 26, 2026
d5ee083
fix: when set_begin_index not implemented for scheduler
JingyaHuang May 26, 2026
688df52
Merge branch 'add-neuron-backend' of github.com:JingyaHuang/diffusers…
JingyaHuang May 26, 2026
4b3ff51
review:add maybe_adjust_dtype_for_device and apply to all models with…
JingyaHuang Jun 1, 2026
888a936
fix: dependency
JingyaHuang Jun 1, 2026
1c25326
Merge branch 'main' into add-neuron-backend
JingyaHuang Jun 1, 2026
2443920
Merge branch 'main' into add-neuron-backend
yiyixuxu Jun 2, 2026
e98f17e
Update src/diffusers/pipelines/stable_diffusion_xl/pipeline_stable_di…
JingyaHuang Jun 2, 2026
19702b2
Merge branch 'main' into add-neuron-backend
JingyaHuang Jun 2, 2026
ab82699
review: apply maybe_adjust_dtype_for_device in pixart pipe
JingyaHuang Jun 2, 2026
20af97b
review: update .ai/models.md
JingyaHuang Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/diffusers/models/transformers/transformer_flux2.py
Original file line number Diff line number Diff line change
Expand Up @@ -961,7 +961,8 @@ def forward(self, ids: torch.Tensor) -> torch.Tensor:
pos = ids.float()
is_mps = ids.device.type == "mps"
is_npu = ids.device.type == "npu"
freqs_dtype = torch.float32 if (is_mps or is_npu) else torch.float64
Comment thread
JingyaHuang marked this conversation as resolved.
is_neuron = ids.device.type == "neuron"
freqs_dtype = torch.float32 if (is_mps or is_npu or is_neuron) else torch.float64
# Unlike Flux 1, loop over len(self.axes_dim) rather than ids.shape[-1]
for i in range(len(self.axes_dim)):
cos, sin = get_1d_rotary_pos_embed(
Expand Down
5 changes: 3 additions & 2 deletions src/diffusers/models/unets/unet_2d_condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -855,10 +855,11 @@ def get_time_embed(self, sample: torch.Tensor, timestep: torch.Tensor | float |
# This would be a good case for the `match` statement (Python 3.10+)
is_mps = sample.device.type == "mps"
is_npu = sample.device.type == "npu"
is_neuron = sample.device.type == "neuron"
if isinstance(timestep, float):
dtype = torch.float32 if (is_mps or is_npu) else torch.float64
dtype = torch.float32 if (is_mps or is_npu or is_neuron) else torch.float64
else:
dtype = torch.int32 if (is_mps or is_npu) else torch.int64
dtype = torch.int32 if (is_mps or is_npu or is_neuron) else torch.int64
timesteps = torch.tensor([timesteps], dtype=dtype, device=sample.device)
elif len(timesteps.shape) == 0:
timesteps = timesteps[None].to(sample.device)
Expand Down
8 changes: 5 additions & 3 deletions src/diffusers/pipelines/pixart_alpha/pipeline_pixart_alpha.py
Original file line number Diff line number Diff line change
Expand Up @@ -862,7 +862,8 @@ def __call__(
prompt_attention_mask = torch.cat([negative_prompt_attention_mask, prompt_attention_mask], dim=0)

# 4. Prepare timesteps
if XLA_AVAILABLE:
is_neuron_device = device.type == "neuron"
if XLA_AVAILABLE or is_neuron_device:
Comment thread
JingyaHuang marked this conversation as resolved.
timestep_device = "cpu"
else:
timestep_device = device
Expand Down Expand Up @@ -914,10 +915,11 @@ def __call__(
# This would be a good case for the `match` statement (Python 3.10+)
is_mps = latent_model_input.device.type == "mps"
is_npu = latent_model_input.device.type == "npu"
is_neuron = latent_model_input.device.type == "neuron"
if isinstance(current_timestep, float):
dtype = torch.float32 if (is_mps or is_npu) else torch.float64
dtype = torch.float32 if (is_mps or is_npu or is_neuron) else torch.float64
Comment thread
JingyaHuang marked this conversation as resolved.
Outdated
else:
dtype = torch.int32 if (is_mps or is_npu) else torch.int64
dtype = torch.int32 if (is_mps or is_npu or is_neuron) else torch.int64
current_timestep = torch.tensor([current_timestep], dtype=dtype, device=latent_model_input.device)
elif len(current_timestep.shape) == 0:
current_timestep = current_timestep[None].to(latent_model_input.device)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1092,7 +1092,8 @@ def __call__(
)

# 4. Prepare timesteps
if XLA_AVAILABLE:
is_neuron_device = hasattr(device, "type") and device.type == "neuron"
Comment thread
JingyaHuang marked this conversation as resolved.
Outdated
if XLA_AVAILABLE or is_neuron_device:
timestep_device = "cpu"
else:
timestep_device = device
Expand Down Expand Up @@ -1187,23 +1188,26 @@ def __call__(
).to(device=device, dtype=latents.dtype)

self._num_timesteps = len(timesteps)
self.scheduler.set_begin_index(0)
with self.progress_bar(total=num_inference_steps) as progress_bar:
for i, t in enumerate(timesteps):
if self.interrupt:
continue

# expand the latents if we are doing classifier free guidance
latent_model_input = torch.cat([latents] * 2) if self.do_classifier_free_guidance else latents

latent_model_input = self.scheduler.scale_model_input(latent_model_input, t)

# predict the noise residual
added_cond_kwargs = {"text_embeds": add_text_embeds, "time_ids": add_time_ids}
if ip_adapter_image is not None or ip_adapter_image_embeds is not None:
added_cond_kwargs["image_embeds"] = image_embeds
# [Neuron] pre-cast timestep to float32 on device. Neuron XLA does not support
# int64 ops; the compiled UNet graph requires a float32 timestep input on-device.
t_unet = t.to(torch.float32).to(device) if is_neuron_device else t
noise_pred = self.unet(
latent_model_input,
t,
t_unet,
encoder_hidden_states=prompt_embeds,
timestep_cond=timestep_cond,
cross_attention_kwargs=self.cross_attention_kwargs,
Expand Down
1 change: 1 addition & 0 deletions src/diffusers/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
is_timm_available,
is_torch_available,
is_torch_mlu_available,
is_torch_neuronx_available,
is_torch_npu_available,
is_torch_version,
is_torch_xla_available,
Expand Down
11 changes: 11 additions & 0 deletions src/diffusers/utils/import_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ def _is_package_available(pkg_name: str, get_dist_name: bool = False) -> tuple[b
_torch_xla_available, _torch_xla_version = _is_package_available("torch_xla")
_torch_npu_available, _torch_npu_version = _is_package_available("torch_npu")
_torch_mlu_available, _torch_mlu_version = _is_package_available("torch_mlu")
_torch_neuronx_available, _torch_neuronx_version = _is_package_available("torch_neuronx")
_transformers_available, _transformers_version = _is_package_available("transformers")
_hf_hub_available, _hf_hub_version = _is_package_available("huggingface_hub")
_kernels_available, _kernels_version = _is_package_available("kernels")
Expand Down Expand Up @@ -250,6 +251,10 @@ def is_torch_mlu_available():
return _torch_mlu_available


def is_torch_neuronx_available():
return _torch_neuronx_available


def is_flax_available():
return _flax_available

Expand Down Expand Up @@ -600,6 +605,11 @@ def is_av_available():
"""


TORCH_NEURONX_IMPORT_ERROR = """
{0} requires the torch_neuronx library (AWS Neuron SDK) but it was not found in your environment. Please install it
following the AWS Neuron documentation: https://awsdocs-neuron.readthedocs-hosted.com/en/latest/
"""

BACKENDS_MAPPING = OrderedDict(
[
("bs4", (is_bs4_available, BS4_IMPORT_ERROR)),
Expand Down Expand Up @@ -630,6 +640,7 @@ def is_av_available():
("pytorch_retinaface", (is_pytorch_retinaface_available, PYTORCH_RETINAFACE_IMPORT_ERROR)),
("better_profanity", (is_better_profanity_available, BETTER_PROFANITY_IMPORT_ERROR)),
("nltk", (is_nltk_available, NLTK_IMPORT_ERROR)),
("torch_neuronx", (is_torch_neuronx_available, TORCH_NEURONX_IMPORT_ERROR)),
]
)

Expand Down
3 changes: 3 additions & 0 deletions src/diffusers/utils/testing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
is_peft_available,
is_timm_available,
is_torch_available,
is_torch_neuronx_available,
is_torch_version,
is_torchao_available,
is_torchsde_available,
Expand Down Expand Up @@ -113,6 +114,8 @@
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(
Expand Down
32 changes: 29 additions & 3 deletions src/diffusers/utils/torch_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@
from typing import Callable, ParamSpec, TypeVar

from . import logging
from .import_utils import is_torch_available, is_torch_mlu_available, is_torch_npu_available, is_torch_version
from .import_utils import (
is_torch_available,
is_torch_mlu_available,
is_torch_neuronx_available,
is_torch_npu_available,
is_torch_version,
)


T = TypeVar("T")
Expand All @@ -33,54 +39,68 @@
import torch
from torch.fft import fftn, fftshift, ifftn, ifftshift

BACKEND_SUPPORTS_TRAINING = {"cuda": True, "xpu": True, "cpu": True, "mps": False, "default": True}
BACKEND_SUPPORTS_TRAINING = {
"cuda": True,
"xpu": True,
"cpu": True,
"mps": False,
"neuron": False,
"default": True,
}
BACKEND_EMPTY_CACHE = {
"cuda": torch.cuda.empty_cache,
"xpu": torch.xpu.empty_cache,
"cpu": None,
"mps": torch.mps.empty_cache,
"neuron": None,
"default": None,
}
BACKEND_DEVICE_COUNT = {
"cuda": torch.cuda.device_count,
"xpu": torch.xpu.device_count,
"cpu": lambda: 0,
"mps": lambda: 0,
"neuron": lambda: getattr(getattr(torch, "neuron", None), "device_count", lambda: 0)(),
"default": 0,
}
BACKEND_MANUAL_SEED = {
"cuda": torch.cuda.manual_seed,
"xpu": torch.xpu.manual_seed,
"cpu": torch.manual_seed,
"mps": torch.mps.manual_seed,
"neuron": torch.manual_seed,
"default": torch.manual_seed,
}
BACKEND_RESET_PEAK_MEMORY_STATS = {
"cuda": torch.cuda.reset_peak_memory_stats,
"xpu": getattr(torch.xpu, "reset_peak_memory_stats", None),
"cpu": None,
"mps": None,
"neuron": None,
"default": None,
}
BACKEND_RESET_MAX_MEMORY_ALLOCATED = {
"cuda": torch.cuda.reset_max_memory_allocated,
"xpu": getattr(torch.xpu, "reset_peak_memory_stats", None),
"cpu": None,
"mps": None,
"neuron": None,
"default": None,
}
BACKEND_MAX_MEMORY_ALLOCATED = {
"cuda": torch.cuda.max_memory_allocated,
"xpu": getattr(torch.xpu, "max_memory_allocated", None),
"cpu": 0,
"mps": 0,
"neuron": 0,
"default": 0,
}
BACKEND_SYNCHRONIZE = {
"cuda": torch.cuda.synchronize,
"xpu": getattr(torch.xpu, "synchronize", None),
"cpu": None,
"mps": None,
"neuron": getattr(getattr(torch, "neuron", None), "synchronize", None),
"default": None,
}
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
Expand Down Expand Up @@ -169,11 +189,15 @@ def randn_tensor(
layout = layout or torch.strided
device = device or torch.device("cpu")

# Neuron (XLA) does not support creating random tensors directly on device; always use CPU
if device.type == "neuron":
rand_device = torch.device("cpu")

if generator is not None:
gen_device_type = generator.device.type if not isinstance(generator, list) else generator[0].device.type
if gen_device_type != device.type and gen_device_type == "cpu":
rand_device = "cpu"
if device.type != "mps":
if device.type not in ("mps", "neuron"):
logger.info(
f"The passed generator was created on 'cpu' even though a tensor on {device} was expected."
f" Tensors will be created on 'cpu' and then moved to {device}. Note that one can probably"
Expand Down Expand Up @@ -296,6 +320,8 @@ def get_device():
return "mps"
elif is_torch_mlu_available():
return "mlu"
elif is_torch_neuronx_available() and hasattr(torch, "neuron") and torch.neuron.is_available():
return "neuron"
else:
return "cpu"

Expand Down
63 changes: 62 additions & 1 deletion tests/pipelines/flux2/test_pipeline_flux2_klein.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import gc
import os
import unittest

import numpy as np
Expand All @@ -11,8 +13,13 @@
Flux2KleinPipeline,
Flux2Transformer2DModel,
)
from diffusers.utils.import_utils import is_torch_neuronx_available

from ...testing_utils import torch_device
from ...testing_utils import (
backend_empty_cache,
require_torch_neuron,
torch_device,
)
from ..test_pipelines_common import PipelineTesterMixin, check_qkv_fused_layers_exist


Expand Down Expand Up @@ -181,3 +188,57 @@ def test_image_input(self):
@unittest.skip("Needs to be revisited")
def test_encode_prompt_works_in_isolation(self):
pass


@require_torch_neuron
class Flux2KleinPipelineIntegrationTests(unittest.TestCase):
ckpt_id = "black-forest-labs/FLUX.2-klein-4B"
prompt = "A small cactus with a happy face in the Sahara desert."

def setUp(self):
super().setUp()
self._saved_env = {}
if is_torch_neuronx_available():
neff_cache_dir = "/tmp/neff_cache"
os.makedirs(neff_cache_dir, exist_ok=True)
for key in ("TORCH_NEURONX_NEFF_CACHE_DIR", "TORCH_NEURONX_ENABLE_NKI_SDPA"):
self._saved_env[key] = os.environ.get(key)
os.environ["TORCH_NEURONX_NEFF_CACHE_DIR"] = neff_cache_dir
Comment thread
JingyaHuang marked this conversation as resolved.
os.environ.setdefault("TORCH_NEURONX_ENABLE_NKI_SDPA", "0")
gc.collect()
backend_empty_cache(torch_device)

def tearDown(self):
super().tearDown()
for key, original in self._saved_env.items():
if original is None:
os.environ.pop(key, None)
else:
os.environ[key] = original
gc.collect()
backend_empty_cache(torch_device)

def test_flux2_klein_inference_512(self):
generator = torch.Generator("cpu").manual_seed(0)

pipe = Flux2KleinPipeline.from_pretrained(self.ckpt_id, torch_dtype=torch.bfloat16)
pipe.to(torch_device)
if is_torch_neuronx_available():
torch.neuron.synchronize()
pipe.set_progress_bar_config(disable=None)

image = pipe(
prompt=self.prompt,
height=512,
width=512,
num_inference_steps=4,
guidance_scale=1.0,
generator=generator,
output_type="np",
).images

image_slice = image[0, -3:, -3:, -1]
Comment thread
JingyaHuang marked this conversation as resolved.
self.assertEqual(image.shape, (1, 512, 512, 3))
self.assertTrue(np.all((image >= 0.0) & (image <= 1.0)), "Pixel values must be in [0, 1]")
expected_slice = np.array([0.3652, 0.3574, 0.3633, 0.4102, 0.4062, 0.4043, 0.4453, 0.4355, 0.4570])
self.assertLess(np.abs(image_slice.flatten() - expected_slice).max(), 5e-2)
9 changes: 7 additions & 2 deletions tests/pipelines/pixart_alpha/test_pixart.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
PixArtAlphaPipeline,
PixArtTransformer2DModel,
)
from diffusers.utils.import_utils import is_torch_neuronx_available

from ...testing_utils import (
backend_empty_cache,
Expand Down Expand Up @@ -291,7 +292,9 @@ def test_pixart_1024(self):
expected_slice = np.array([0.0742, 0.0835, 0.2114, 0.0295, 0.0784, 0.2361, 0.1738, 0.2251, 0.3589])

max_diff = numpy_cosine_similarity_distance(image_slice.flatten(), expected_slice)
self.assertLessEqual(max_diff, 1e-4)
# Neuron uses bfloat16 internally which has lower precision than float16 on CUDA
atol = 1e-2 if is_torch_neuronx_available() else 1e-4
self.assertLessEqual(max_diff, atol)

def test_pixart_512(self):
generator = torch.Generator("cpu").manual_seed(0)
Expand All @@ -307,7 +310,9 @@ def test_pixart_512(self):
expected_slice = np.array([0.3477, 0.3882, 0.4541, 0.3413, 0.3821, 0.4463, 0.4001, 0.4409, 0.4958])

max_diff = numpy_cosine_similarity_distance(image_slice.flatten(), expected_slice)
self.assertLessEqual(max_diff, 1e-4)
# Neuron uses bfloat16 internally which has lower precision than float16 on CUDA
atol = 1e-2 if is_torch_neuronx_available() else 1e-4
self.assertLessEqual(max_diff, atol)

def test_pixart_1024_without_resolution_binning(self):
generator = torch.manual_seed(0)
Expand Down
Loading
Loading