Uh oh!
There was an error while loading. Please reload this page.
[core] Support tensor parallelism for model inference (CUDA, Neuron) - #13718
Conversation
… into add-neuron-backend
… into add-neuron-backend
…into support-neuron-tp
sayakpaul
commented
Aug 17, 2026
@zhtmike could you check this PR and provide feedback? Additionally, I think your Slack connection got expired. Could you let me know what is the best email to reach you? |
zhtmike
left a comment
There was a problem hiding this comment.
Hi @sayakpaul, got your ping! I’ve sent it to your email.
My major concern is that this PR changes the QKV layout for five models, and there is no regression test to ensure the model output is the same before and after this change. It may break the results when the attention backend is set or when CP is enabled.
The other changes look fine to me, with minor suggestions.
Uh oh!
There was an error while loading. Please reload this page.
| @is_tensor_parallel | ||
| @require_torch_multi_accelerator | ||
| class TensorParallelTesterMixin: |
There was a problem hiding this comment.
I suggest adding a Mixin to guard the functionality and accuracy when both context parallel and tensor parallel are enabled, since they are orthogonal. Maybe in a future PR?
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
sayakpaul
commented
Aug 17, 2026
Thanks @zhtmike!
Do you have a skeleton for such a test? We can run it quickly and see if this is concerning? |
zhtmike
commented
Aug 17, 2026
Just my opinion: a more conservative approach is to keep the model’s default layout unchanged and perform the permutation in the TP block (in column/row parallel if necessary). Otherwise, I suggest simply running some inference and checking the results by eye. It’s hard to compare the old and new layouts in a simple test — may need a shell script to switch branches for this kind of testing... |
An update, the layout seems not changed. (I overlooked |
Uh oh!
There was an error while loading. Please reload this page.
sayakpaul
commented
Aug 17, 2026
Used the following (Claude generated; but verified by me) script and I think it should address the concern around anything being broken around attention backends: Details<script>Unfold</script>importargparseimportosimportsysp=argparse.ArgumentParser()
p.add_argument("--repo", default=".")
p.add_argument("--backends", nargs="*", default=[])
p.add_argument("--save", default=None)
p.add_argument("--ref", default=None)
args=p.parse_args()
sys.path[:0] = [args.repo, os.path.join(args.repo, "src")]
importtorchfromdiffusers.models.attention_dispatchimport (
_HUB_KERNELS_REGISTRY,
_AttentionBackendRegistry,
AttentionBackendName,
)
# flash-attn2 hub kernel v1 has no torch 2.13 builds; v3 works for the single-GPU pathforbin (AttentionBackendName.FLASH_HUB, AttentionBackendName.FLASH_VARLEN_HUB):
cfg=_HUB_KERNELS_REGISTRY[b]
cfg.version, cfg.wrapped_forward_attr, cfg.wrapped_backward_attr=3, None, Nonefromtests.models.transformers.test_models_transformer_fluximportFluxTransformerTesterConfigfromtests.models.transformers.test_models_transformer_flux2importFlux2TransformerTesterConfigfromtests.models.transformers.test_models_transformer_qwenimageimportQwenImageTransformerTesterConfigtorch.use_deterministic_algorithms(False)
calls= {}
forname, fninlist(_AttentionBackendRegistry._backends.items()):
defwrap(f, n):
definner(*a, **k):
calls[n] =calls.get(n, 0) +1returnf(*a, **k)
returninner_AttentionBackendRegistry._backends[name] =wrap(fn, name.value)
definputs_for(name, cfg):
n=32*32ifname=="flux":
inputs=cfg.get_dummy_inputs()
inputs["hidden_states"] =torch.randn(1, n, 4, generator=cfg.generator)
inputs["img_ids"] =torch.randn(n, 3, generator=cfg.generator)
elifname=="flux2":
inputs=cfg.get_dummy_inputs(height=32, width=32)
else:
inputs=cfg.get_dummy_inputs()
inputs["hidden_states"] =torch.randn(1, n, 16, generator=cfg.generator)
inputs["img_shapes"] = [(1, 32, 32)]
inputs["encoder_hidden_states_mask"] =Nonereturn {k: v.cpu() iftorch.is_tensor(v) elsevfork, vininputs.items()}
defcast(inputs, dtype):
return {
k: v.to("cuda", dtype) iftorch.is_tensor(v) andv.is_floating_point()
elsev.to("cuda") iftorch.is_tensor(v) elsevfork, vininputs.items()
}
CONFIGS= {
"flux": FluxTransformerTesterConfig,
"flux2": Flux2TransformerTesterConfig,
"qwenimage": QwenImageTransformerTesterConfig,
}
forname, ConfiginCONFIGS.items():
tc=Config()
torch.manual_seed(0)
model=tc.model_class(**tc.get_init_dict())
ifargs.ref:
model.load_state_dict(torch.load(f"{args.ref}/{name}_sd.pt", weights_only=True))
inputs=torch.load(f"{args.ref}/{name}_inputs.pt", weights_only=False)
else:
inputs=inputs_for(name, tc)
model=model.to("cuda").eval()
withtorch.no_grad():
out=model(**cast(inputs, torch.float32), return_dict=False)[0].cpu()
ifargs.ref:
d= (out-torch.load(f"{args.ref}/{name}_out.pt", weights_only=True)).abs()
print(f"{name}: vs ref (fp32 native) max={d.max():.3e} mean={d.mean():.3e}")
ifargs.save:
os.makedirs(args.save, exist_ok=True)
torch.save({k: v.cpu() fork, vinmodel.state_dict().items()}, f"{args.save}/{name}_sd.pt")
torch.save(inputs, f"{args.save}/{name}_inputs.pt")
torch.save(out, f"{args.save}/{name}_out.pt")
ifargs.backends:
model=model.to(torch.bfloat16)
binputs=cast(inputs, torch.bfloat16)
withtorch.no_grad():
native=model(**binputs, return_dict=False)[0]
restore, _=_AttentionBackendRegistry.get_active_backend()
forbackendinargs.backends:
model.set_attention_backend(backend)
calls.clear()
withtorch.no_grad():
out_b=model(**binputs, return_dict=False)[0]
model.reset_attention_backend()
_AttentionBackendRegistry.set_active_backend(restore)
d= (out_b-native).abs()
ok=torch.allclose(out_b, native, atol=1e-2, rtol=1e-2)
print(f" {name}/{backend}: max={d.max():.3e} mean={d.mean():.3e} ok={ok} dispatched={dict(calls)}")Compare across different backends against SDPA: python backend_check.py --backends _native_cudnn flash_hub _flash_3_hub |
Uh oh!
There was an error while loading. Please reload this page.
…ng or LoRA Addresses the remaining two items of the review on huggingface#13718: tensor parallelism was rejected alongside quantization and `device_map` only on the `from_pretrained` streaming path, while `enable_parallelism` — which the quantization error message itself recommended — accepted a quantized, offloaded or adapter-injected model and sharded it anyway. - Add `_check_tp_model_state`, called from `apply_tensor_parallel`, the one chokepoint every TP entry point funnels through. It rejects a model that is quantized, group-offloaded, placed by accelerate (`device_map` or CPU offload), or has PEFT layers injected. Placed before the device-type check so the reported reason is the useful one. - Guard the reverse order too: `enable_group_offload`, the two pipeline CPU-offload methods, and `load_lora_adapter` now refuse a tensor-parallel model. - `save_pretrained` refuses a quantized tensor-parallel model. Previously the `dcp=True` branch returned before the quantizer's serialization step, writing shards with no quantization metadata and no error. - The DCP load guard checked the `quantization_config` kwarg only, so a pre-quantized checkpoint directory loaded silently; check the config's own entry too, and add the missing `_tp_plan` check that otherwise surfaced as a raw `AttributeError`. - Correct the `from_pretrained` message and the doc sentence that pointed at `enable_parallelism` as a way to shard a quantized model. The new tests are the first tensor-parallel tests that need neither an accelerator nor more than one rank: every case asserts a raise before any collective, so they run single-process on gloo.
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.