Skip to content

[Neuron] Add tensor parallel support for Neuron backend - #13718

Open
JingyaHuang wants to merge 89 commits into
huggingface:mainfrom
JingyaHuang:support-neuron-tp
Open

[Neuron] Add tensor parallel support for Neuron backend#13718
JingyaHuang wants to merge 89 commits into
huggingface:mainfrom
JingyaHuang:support-neuron-tp

Conversation

@JingyaHuang

@JingyaHuang JingyaHuang commented May 11, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds tensor-parallel (TP) inference for diffusers models on AWS Neuron (Trainium/Inferentia).
The implementation is:

  • model-agnostic, it shards from a flat _tp_plan
  • the TP support is generic, easy to extend to other backends (CUDA, TPU, and more). It is exposed through the same public API used for CP: model.enable_parallelism(config=TensorParallelConfig(...)).

Now validated on 3 pipelines on Neuron (trn2, TP=8), in both eager and torch.compile mode: FLUX.1-dev, FLUX.2 (Klein), and Qwen-Image.

Key changes:

  • A model-agnostic apply_tensor_parallel that shards from a flat _tp_plan (Neuron pre-shard path works around the NRT consecutive-reduce_scatter bug; the default parallelize_module path is used on other backends).
  • _tp_plan added to the FLUX.1, FLUX.2 and Qwen-Image transformers.
  • Qwen-Image: the attention processor reshape is made TP-agnostic (reshape by a fixed head_dim), and its RoPE is ported from complex torch.polar/view_as_complex to real cos/sin. The RoPE change is numerically identical and unconditional — required for XLA backends (Neuron/TPU) and cleaner under torch.compile. The same real-RoPE change is applied to NucleusMoE, which shared the code.

Example scripts

Runnable torchrun --nproc_per_node=8 scripts live under examples_tp/ for each pipeline (e.g. test_neuron_flux1_dev_tp.py, test_neuron_flux2_dev_tp.py, test_qwenimage_tp.py).

Quick test — Flux2 TP on Neuron (For future release)

run with torchrun --nproc_per_node=8 flux2_tp8_neuron.py

import torch
import torch.distributed as dist
from torch.distributed.device_mesh import DeviceMesh
import torch_neuronx  # noqa: F401 — registers torch.neuron

from diffusers import Flux2KleinPipeline, TensorParallelConfig

MODEL = "black-forest-labs/FLUX.2-klein-9B"
PROMPT = "a golden retriever surfing a wave, photorealistic"

dist.init_process_group(backend="neuron")
device = torch.neuron.current_device()
rank = dist.get_rank()
tp_size = dist.get_world_size()
tp_mesh = DeviceMesh("neuron", list(range(tp_size)))

pipe = Flux2KleinPipeline.from_pretrained(MODEL, torch_dtype=torch.bfloat16)

# Text encoder + VAE: replicated on every rank (no TP).
pipe.text_encoder = pipe.text_encoder.to(device)
pipe.vae = pipe.vae.to(device)

# Transformer: shard across all ranks while still on CPU, then move to device.
pipe.transformer.enable_parallelism(config=TensorParallelConfig(mesh=tp_mesh))
pipe.transformer = pipe.transformer.to(device)
torch.neuron.synchronize()

image = pipe(
    prompt=PROMPT, height=1024, width=1024,
    num_inference_steps=4, guidance_scale=1.0,
  ).images[0]

if rank == 0:
    image.save("flux2_tp8.png")
    print("Saved flux2_tp8.png")

dist.destroy_process_group()

Who can review?

Anyone in the community is free to review the PR once the tests have passed. Feel free to tag
members/contributors who may be interested in your PR.

@sayakpaul sayakpaul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for the iteration, @JingyaHuang! Left some further comments.

I am also running the tests and trying out examples. Will keep this PR updated with findings from that.

Edit: Here's the report of my findings https://gist.github.com/sayakpaul/954b5d64aad648aca091f36c35f36397


## Tensor parallelism

[Tensor parallelism](https://huggingface.co/spaces/nanotron/ultrascale-playbook?section=tensor_parallelism) shards the weight matrices of a model across devices. Each device holds a column-wise (`"colwise"`) or row-wise (`"rowwise"`) slice of each layer, computes a partial result, and an `AllReduce`/`AllGather` at the layer boundary reconstructs the full output. Unlike context parallelism, it reduces the per-device *weight* memory, which is useful for models that do not fit on a single device.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also provide a short section after the TP section, showing which parallelism should be preferred in what circumstances?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, should there be guidance for model authors on how they should write the _tp_plan for their model? That seems non-trivial to me and some useful hints could be nice to have there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For guidance on which parallelism config to use, I think each section already explains briefly the trade-offs , eg. CP -> long sequences, TP -> large weights. I just added a section to summarize i bit, If we want more precise/actionable guide, we need to run experiments, since the right choice also depends on the specific model.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will add more explanation on contributing tp plan tho!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all of that makes sense and should be documented (for example, the right choices depending on the model size, input problem space, etc.).

Comment thread src/diffusers/hooks/tensor_parallel_neuron.py Outdated
to the given query or key 'x' tensors using the provided frequency tensor 'freqs_cis'. The input tensors are
reshaped as complex numbers, and the frequency tensor is reshaped for broadcasting compatibility. The resulting
tensors contain rotary embeddings and are returned as real tensors.
def apply_rotary_emb_qwen(x: torch.Tensor, freqs: torch.Tensor) -> torch.Tensor:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this change required?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a tp required change, cuda runs the complex path fine. But for neuron, rope_params did torch.polar, so pos_freqs/neg_freqs are complex64, the Neuron compiler has no lowering for complex tensors so far. It's a workaround.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a breaking change. So, we should perhaps have a pattern like:

ROPE_PER_DEVICE = {
    "cuda": apply_rotary_emb_qwen,
    "neuron": apply_rotary_emb_qwen_neuron,
}

And then fetch from ROPE_PER_DEVICE in the caller site. @DN6 WDYT?

Comment thread src/diffusers/models/transformers/transformer_flux.py
Comment thread src/diffusers/models/transformers/transformer_flux.py
Comment thread src/diffusers/models/_modeling_parallel.py
Comment thread src/diffusers/models/modeling_utils.py
Comment thread docs/source/en/training/distributed_inference.md Outdated
Comment thread docs/source/en/training/distributed_inference.md
@@ -63,7 +63,12 @@
from ..utils.distributed_utils import is_torch_dist_rank_zero

@sayakpaul sayakpaul Jul 28, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the following aren't implemented at the moment (which is fine; just flagging).

  • Sharded loading. from_pretrained should stream shards straight to each rank's DTensor rather than materializing the full checkpoint then slicing — otherwise TP saves you nothing at load time. And check save_pretrained / state_dict calls .full_tensor() or uses DCP. I think we should at least raise when save_pretrained() is called in case TP is enabled?
  • LoRA loading. for a colwise base layer, lora_A replicated + lora_B colwise; for rowwise, lora_A rowwise + lora_B replicated. If the plan doesn't cover PEFT layers, loading an adapter onto a TP model will either error or be wrong. I think we should detect if the model has peft layers injected and raise if TP is requested?
  • Quantization, offloading. We should probably also raise when these are requested?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JingyaHuang this doesn't seem to have been resolved?

@DN6 DN6 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good 👍🏽 I think we can merge quite soon. My comments are minor.
@sayakpaul Perhpas we can handle sharded TP loading/saving + LoRA and Quant support follow ups so that the scope is manageable here.

Comment thread src/diffusers/models/_modeling_parallel.py Outdated
Comment thread src/diffusers/models/modeling_utils.py Outdated
Comment on lines +1172 to +1179
# When text encoders are offloaded to CPU while the denoising backbone
# (transformer, unet, vae) runs on an accelerator, self.device returns CPU
# (first component). Prefer any non-CPU, non-meta component so that
# latent tensors land on the accelerator. This covers CUDA, XPU, NPU, HPU,
# and any other backend, including TP-sharded models via DTensor.
for name, model in self.components.items():
if isinstance(model, torch.nn.Module) and model.device.type not in ("cpu", "meta"):
return model.device

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fix is okay with me, but is there a specific reason to include it in this PR? Does it affect TP?

@sayakpaul

Copy link
Copy Markdown
Member

Perhpas we can handle sharded TP loading/saving + LoRA and Quant support follow ups so that the scope is manageable here.

200 percent.

@tengomucho tengomucho left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just few comments

Comment thread src/diffusers/hooks/tensor_parallel.py
Comment thread src/diffusers/hooks/tensor_parallel.py
Comment thread src/diffusers/models/_modeling_parallel.py Outdated

`tp_degree` is taken from `world_size` above, so `--nproc-per-node 4` shards the transformer across 4 devices.

### Writing a _tp_plan

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stevhliu can this first _ be escaped so that it renders properly? If so, how?

Comment thread docs/source/en/training/distributed_inference.md
# in the model's _tp_plan
"single_transformer_blocks.*.attn.to_out": PackedRowwiseParallel(),
```

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(nit): Perhaps provide a reference implementation example of Flux2 (by linking the path to the modeling file)?

@sayakpaul sayakpaul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for iterating. Just a few remaining nits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation hooks models pipelines size/L PR with diff > 200 LOC tests utils

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants