[Neuron] Add tensor parallel support for Neuron backend#13718
[Neuron] Add tensor parallel support for Neuron backend#13718JingyaHuang wants to merge 80 commits into
Conversation
… into add-neuron-backend
… into add-neuron-backend
This reverts commit 7ea75f7.
…into support-neuron-tp
JingyaHuang
left a comment
There was a problem hiding this comment.
Made some modifications according to your comments @sayakpaul, and added flux1 support to test if your PR can easily adapt to suppot it!
| @property | ||
| def _cp_world_size(self) -> int: | ||
| """Context-parallel world size, or 1 when context parallelism is not enabled. | ||
|
|
||
| Lets attention backends branch on context parallelism without dereferencing a possibly ``None`` | ||
| ``context_parallel_config`` (e.g. when only tensor parallelism is active). | ||
| """ | ||
| cp = self.context_parallel_config | ||
| if cp is None or cp._world_size is None: | ||
| return 1 | ||
| return cp._world_size |
There was a problem hiding this comment.
if grad_enabled or (_parallel_config is not None and _parallel_config.context_parallel_config._world_size > 1):_parallel_config.context_parallel_config would be None when TP enabled, so the property was added to pass the conditional statements
| # On Neuron, run the index-heavy `_unpack_latents_with_ids` on CPU to avoid expensive | ||
| # device<->host syncs from the gather/scatter arithmetic, then move the result back. | ||
| latent_device = latents.device | ||
| on_neuron = get_device() == "neuron" | ||
| if on_neuron: | ||
| latents = latents.cpu() | ||
| latent_ids = latent_ids.cpu() |
There was a problem hiding this comment.
this change is for dynamic index-driven
scatter_ in _unpack_latents_with_ids, neuron compiler can't handle it, haven't tested on TPU yet but likely an issue as well. for cuda, scatter_ is a native on-device op, so it's ok.actually for neuron etc, it would be even better to reformulate unpack_latents_with_ids so it doesn't do a data-dependent scatter to avoid the device-host sync penalty...
Does it? 👀👀 |
| attn, hidden_states, encoder_hidden_states | ||
| ) | ||
|
|
||
| query = query.unflatten(-1, (attn.heads, -1)) |
There was a problem hiding this comment.
To keep Bria and nucleusmoe out, I would need to break the copied from links, eg:
diffusers/src/diffusers/models/transformers/transformer_bria_fibo.py
Lines 67 to 68 in 284419b
| # 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 |
There was a problem hiding this comment.
Interesting. But should self.device not return CPU in this because the entire pipeline isn't on an accelerator?
There was a problem hiding this comment.
true, right now self.device is define by the 1st component in alphabetical order, which is not ideal. But how would you define self.device if the components of the pipeline are in different devices? shall we define it by a certain model in the pipeline?
There was a problem hiding this comment.
perhaps:
@property
def device(self) -> torch.device:
r"""
Returns:
`torch.device`: The torch device on which the pipeline is located. When components are
split across devices (e.g. text encoders on CPU while the denoising backbone runs on an
accelerator), the accelerator device is returned.
"""
module_names, _ = self._get_signature_keys(self)
modules = [getattr(self, n, None) for n in module_names]
modules = [m for m in modules if isinstance(m, torch.nn.Module)]
# Prefer a non-CPU, non-meta component so a split pipeline reports the accelerator
# it computes on, rather than whichever component happens to sort first.
for module in modules:
if module.device.type not in ("cpu", "meta"):
return module.device
for module in modules:
return module.device
return torch.device("cpu")?
There was a problem hiding this comment.
🤗 Serge says:
Solid, well-scoped TP implementation overall — the plan-driven sharding design, the packed colwise/rowwise handling for fused projections, and the real-valued RoPE rewrite (verified numerically identical to the complex formulation) all check out. However there are a few correctness issues that should be fixed before merge.
Correctness
- Unconditional per-step
broadcastinpipeline_flux2_klein.py: it fires whenevertorch.distributedis initialized, not just under TP. Any distributed non-TP use of this pipeline (e.g. data-parallel with different prompts/seeds per rank) will have every rank's latents silently overwritten by rank 0's. It is also self-described as a "safety measure only", which the repo rules explicitly disallow. - Packed-colwise bias is sharded incorrectly in the generic path (
hooks/tensor_parallel.py):_PackedColwiseImplshards non-weight params with a plain contiguousShard(0), but the weight rows are packed per-block. For any packed layer with a bias the bias slice won't line up with the weight shard. The Neuron path (_pre_shard_and_tp) packs the bias correctly — the generic path should match. - Missed attention backend:
_flash_attention_3_varlen_hub(attention_dispatch.py~L3216) still dereferences_parallel_config.context_parallel_config.ring_degreeand checksif _parallel_config is None:unguarded. With TP-only,_parallel_configis set butcontext_parallel_configisNone, so this backend will crash withAttributeError. All sibling backends were updated in this PR; this one was skipped. - No divisibility validation when computing per-rank chunks (
chunk = bs // tp_size,rows = w.shape[0] // tp_sizein both backends): if a block size isn't divisible bytp_degree, rows/columns are silently dropped and the model produces garbage. The head-divisibility check inenable_parallelismdoesn't cover MLP dims. A conciseValueErrorwould be much better than silent truncation.
API / scope concerns
apply_rotary_emb_qwen/_apply_rotary_emb_nucleussignature change (dropsuse_real/use_real_unbind_dim, changesfreqssemantics from complex to angles) is a breaking change to a shipped public-ish function; downstream code importing it will break. Worth an explicit sign-off from maintainers.- The
PipelineMixin.device/_execution_devicechanges alter long-standing semantics for every pipeline (any stray non-CPU component now defines the pipeline device). This is a broad behavioral change smuggled into a TP PR and deserves its own discussion/tests. - The
# Copied from ...apply_rotary_emb_qwenheader on_apply_rotary_emb_nucleuswas removed even though the two functions remain identical — keeping the link (and runningmake fix-copies) would prevent future drift. - Duplicate CP+TP mutual-exclusion check:
ParallelConfig.__post_init__already raises, making the second check inenable_parallelismunreachable for normal construction.
Tests / description
- The PR description references runnable scripts under
examples_tp/, but no such directory exists in the diff — the description should be updated. - The generic
TensorParallelTesterMixinand the Neurontorchrunworker are well structured; tolerances are justified.
serge v0.1.0 · model: claude-fable-5 · 18 LLM turns · 38 tool calls · 1236.4s · 1418671 in / 90227 out tokens
| ) | ||
| else: | ||
| dist_param = nn.Parameter( | ||
| distribute_tensor(param, device_mesh, [Shard(0)], src_data_rank=self.src_data_rank), |
There was a problem hiding this comment.
Bug: for a packed-colwise layer with a bias, this shards the bias with a plain contiguous Shard(0), but the weight rows above are sharded per block. E.g. for blocks=[Q, K, V, gate, up], rank r's weight shard holds the r-th slice of each block, while its bias shard holds a contiguous 1/tp slice of the full fused bias — they don't correspond, so outputs are wrong whenever bias=True.
The Neuron path (_pre_shard_and_tp) packs the bias with the same per-block slicing as the weight (bias_parts loop); this generic path needs the same treatment. Currently only reachable with bias=True fused projections, but Flux2FeedForward/Flux2ParallelSelfAttention both expose a bias config knob, so this is a live footgun.
| # offloaded to CPU the pipeline still reports the accelerator device. This | ||
| # also handles TP-sharded models whose DTensor params are on CUDA/XPU/etc. | ||
| for module in modules: | ||
| if module.device.type not in ("cpu", "meta"): |
There was a problem hiding this comment.
This changes the documented semantics of pipeline.device for every pipeline, not just TP ones: any single non-CPU component (e.g. a VAE left on GPU after enable_model_cpu_offload finished a generation, or a partially-moved pipeline) now redefines the whole pipeline's reported device, where previously the first component won. That's a global behavior change to a core public property riding along in a TP PR, with no tests covering the new precedence. Consider scoping the TP fix to _execution_device only (where latents placement actually matters) and leaving device untouched, or splitting this into its own PR with tests.
stevhliu
left a comment
There was a problem hiding this comment.
very nice docs, thanks for adding!
Co-authored-by: Steven Liu <59462357+stevhliu@users.noreply.github.com>
What does this PR do?
Adds tensor-parallel (TP) inference for diffusers models on AWS Neuron (Trainium/Inferentia).
The implementation is:
_tp_planmodel.enable_parallelism(config=TensorParallelConfig(...)).Now validated on 3 pipelines on Neuron (trn2, TP=8), in both eager and
torch.compilemode: FLUX.1-dev, FLUX.2 (Klein), and Qwen-Image.Key changes:
apply_tensor_parallelthat shards from a flat_tp_plan(Neuron pre-shard path works around the NRT consecutive-reduce_scatterbug; the defaultparallelize_modulepath is used on other backends)._tp_planadded to the FLUX.1, FLUX.2 and Qwen-Image transformers.head_dim), and its RoPE is ported from complextorch.polar/view_as_complexto real cos/sin. The RoPE change is numerically identical and unconditional — required for XLA backends (Neuron/TPU) and cleaner undertorch.compile. The same real-RoPE change is applied to NucleusMoE, which shared the code.Example scripts
Runnable
torchrun --nproc_per_node=8scripts live underexamples_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.pyWho 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.