-
Notifications
You must be signed in to change notification settings - Fork 7.2k
[Neuron] Add tensor parallel support for Neuron backend #13718
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
98f6c8c
c58b8b8
3367409
0c51734
a76953c
2480388
1469c04
929ab72
52cac76
30cb353
28a5086
7fab0c4
68689e5
da79308
3bb9c7c
c4facab
dff1f32
1c930c4
1eb5ff9
cbe8f28
16b9606
7f13f68
a46cb19
a354b88
931bb85
9ab6dc3
48fb75b
c350f7b
644477a
03cb725
9da93ed
d44f772
3fc043e
e6d20d8
e76a2fc
034ba9e
4907524
af2aed7
b9b048b
915eeb1
720dad2
89cf8b6
29cd9c3
eaab299
30a43d5
155802c
f133732
b3d8130
7ea75f7
c73cf09
eb58402
c3e123c
dc33e26
491c537
c5b6c89
310c471
909dfcf
70212e5
c130e00
b0b3b7c
4f2fea5
93dae9c
4bb881d
1ba454f
d2733b4
44eba6e
5f661c6
06f7976
911c58e
b390132
4b7ae6b
6d84695
c6e0680
1646814
2218453
3613423
e8dcefc
d55ac18
48ea484
fcc2bcb
2364649
ad5536d
737945a
686e379
5c2549d
57365c5
3c80014
31112cb
6f3db51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -431,3 +431,133 @@ pipeline = DiffusionPipeline.from_pretrained( | |
| CKPT_ID, transformer=transformer, dtype=torch.bfloat16, | ||
| ).to(device) | ||
| ``` | ||
|
|
||
| ## 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. | ||
|
|
||
| Pass a [`TensorParallelConfig`] to [`~ModelMixin.enable_parallelism`]. `tp_degree` is the number of devices to shard across and must divide the model's number of attention heads. The model must define a `_tp_plan` (a flat mapping of module-name globs to a `"colwise"`/`"rowwise"` style). | ||
|
|
||
| ```py | ||
| import torch | ||
| from torch import distributed as dist | ||
| from diffusers import DiffusionPipeline, TensorParallelConfig | ||
|
|
||
| def setup_distributed(): | ||
| if not dist.is_initialized(): | ||
| dist.init_process_group(backend="nccl") | ||
| rank = dist.get_rank() | ||
| device = torch.device(f"cuda:{rank}") | ||
| torch.cuda.set_device(device) | ||
| return device | ||
|
|
||
| def main(): | ||
|
JingyaHuang marked this conversation as resolved.
|
||
| device = setup_distributed() | ||
| world_size = dist.get_world_size() | ||
|
|
||
| pipeline = DiffusionPipeline.from_pretrained( | ||
| "black-forest-labs/FLUX.2-dev", torch_dtype=torch.bfloat16 | ||
| ) # weights stay on CPU | ||
|
|
||
| # Shard the transformer first, then move only each rank's slice onto the accelerator. | ||
| pipeline.transformer.enable_parallelism(config=TensorParallelConfig(tp_degree=world_size)) | ||
| pipeline.transformer.to(device) | ||
|
|
||
| # Move the remaining, non-sharded components onto the accelerator individually. | ||
| pipeline.text_encoder.to(device) | ||
| pipeline.vae.to(device) | ||
|
|
||
| generator = torch.Generator().manual_seed(42) | ||
| image = pipeline(prompt="a cat holding a sign that says hello", generator=generator).images[0] | ||
| if dist.get_rank() == 0: | ||
| image.save("output.png") | ||
| if dist.is_initialized(): | ||
| dist.destroy_process_group() | ||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
| ``` | ||
|
|
||
| ```shell | ||
| torchrun --nproc-per-node 4 tensor_parallel_flux.py | ||
| ``` | ||
|
|
||
| `tp_degree` is taken from `world_size` above, so `--nproc-per-node 4` shards the transformer across 4 devices. | ||
|
|
||
| ### Writing a _tp_plan | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @stevhliu can this first |
||
|
|
||
| Tensor parallelism only works on models that define a `_tp_plan`, a flat class attribute mapping module-name globs to a sharding style. Writing one is mostly a matter of pairing each projection that *expands* the hidden dimension with the projection that *contracts* it back. | ||
|
|
||
| Each key may contain **at most one `*`**, and the prefix before it must resolve to an [`nn.ModuleList`](https://pytorch.org/docs/stable/generated/torch.nn.ModuleList.html) so a single entry covers every block. A key without a `*` applies to the model itself. Paths are relative to the model. | ||
|
|
||
| #### Colwise and rowwise | ||
|
JingyaHuang marked this conversation as resolved.
|
||
|
|
||
| | Style | Shards | Each rank | Use for | | ||
| |---|---|---|---| | ||
| | `"colwise"` | output features (`weight` dim 0) | computes a slice of the output | `to_q`, `to_k`, `to_v`, FFN in-projection | | ||
| | `"rowwise"` | input features (`weight` dim 1) | computes a partial sum | `to_out.0`, FFN out-projection | | ||
|
|
||
| Always pair them in that order. A `"colwise"` projection leaves its output sharded, the following `"rowwise"` projection consumes that shard directly, and a single `AllReduce` at the block boundary reconstructs the result. Sharding the pair any other way forces a gather in the middle and communicates far more. | ||
|
|
||
| For attention this means each rank owns a subset of heads, which is why `tp_degree` must divide the head count. Encoder-stream duplicates (`add_q_proj`, `to_add_out`, `ff_context`) follow the same pattern as their image-stream counterparts. | ||
|
|
||
| #### Fused projections | ||
|
|
||
| When one `Linear` packs several logical tensors along the dimension being sharded, plain `"colwise"`/`"rowwise"` slices straight across the concatenation and misaligns the pieces. Use `PackedColwiseParallel`/`PackedRowwiseParallel` instead, which shard each packed block independently. | ||
|
|
||
| ```py | ||
| # in src/diffusers/models/transformers/your_model.py | ||
| from ...hooks.tensor_parallel import PackedColwiseParallel, PackedRowwiseParallel | ||
| ``` | ||
|
|
||
| `blocks` is a list of proportional integers whose sum divides the packed dimension — `[1, 1]` for a SwiGLU gate+up projection of equal halves, or `[1, 1, 1, 3, 3]` for a fused Q+K+V+gate+up projection with `mlp_ratio=3`. | ||
|
|
||
| ```py | ||
| "transformer_blocks.*.ff.linear_in": PackedColwiseParallel([1, 1]), | ||
| ``` | ||
|
|
||
| When the block sizes are only known from the config, omit the argument and store the absolute sizes on the `Linear` during `__init__` instead, as `_tp_packed_col_blocks` or `_tp_packed_row_blocks`. | ||
|
|
||
| ```py | ||
| # in the attention module's __init__ | ||
| self.to_out._tp_packed_row_blocks = [self.inner_dim, self.mlp_hidden_dim] | ||
| ``` | ||
|
|
||
| ```py | ||
| # in the model's _tp_plan | ||
| "single_transformer_blocks.*.attn.to_out": PackedRowwiseParallel(), | ||
| ``` | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)? |
||
| #### What to leave out | ||
|
|
||
| Anything absent from the plan stays replicated on every rank, which is the right choice for normalization layers, AdaLN modulation (`img_mod`/`txt_mod`), patch and text embeddings, and the final `norm_out`/`proj_out`. These are small, so sharding them saves little memory while adding communication. | ||
|
|
||
| #### Constraints and verification | ||
|
|
||
| - `tp_degree` must divide `config.num_attention_heads`. This is validated in [`~ModelMixin.enable_parallelism`]. | ||
| - Every packed block must *individually* be divisible by `tp_degree`, not just their sum. | ||
|
|
||
| Validate a new plan numerically rather than by eye: generate with a fixed seed on a single device, then again under tensor parallelism, and compare the outputs. A misplaced `"colwise"`/`"rowwise"` usually still runs and produces a plausible but wrong image. | ||
|
|
||
| > [!TIP] | ||
| > Start from an existing plan for a similar architecture. [`QwenImageTransformer2DModel`] is fully unfused and every entry is plain `"colwise"`/`"rowwise"`, [`FluxTransformer2DModel`] adds a single packed row-wise projection, and [`Flux2Transformer2DModel`] covers both packed styles. | ||
|
|
||
| ## Choosing a strategy | ||
|
|
||
| The strategies above solve different problems, and the useful question is not which is fastest in the abstract but what you are running out of. | ||
|
|
||
| | Strategy | Splits | Reduces | Latency for one prompt | Best when | | ||
| |---|---|---|---|---| | ||
| | [Accelerate](#accelerate) / [DDP](#pytorch-distributed) | prompts across replicas | nothing — each device holds a full copy | unchanged | the model already fits and you have many prompts | | ||
| | [`device_map`](#device_map) | components across devices | weight memory | slightly worse | the model doesn't fit and the interconnect is slow | | ||
| | [Context parallelism](#context-parallelism) | the input sequence | activation memory | lower | sequences are long — high resolution or video | | ||
| | [Tensor parallelism](#tensor-parallelism) | weight matrices | weight memory | lower | one component's weights don't fit and the interconnect is fast | | ||
|
|
||
| Some practical guidance: | ||
|
|
||
| - **Throughput on many prompts, model already fits.** Use data parallelism. It is the only strategy here that scales throughput linearly without touching the model, and it leaves single-prompt latency alone. | ||
| - **A single component's weights don't fit.** Reach for tensor parallelism first, since it lowers both memory and latency. It communicates at every block boundary, so it wants a fast interconnect like NVLink; over PCIe that per-layer traffic can outweigh the compute it saves, and `device_map` becomes the better choice. `device_map` also handles the case where the components are individually fine but collectively too large. | ||
| - **Activations, not weights, are the problem.** This is the long-sequence regime — large images, many frames — and context parallelism is the direct answer. For picking a backend within it, see the [Ulysses/Ring benchmarks](#ulysses-attention) above; Ulysses gives the best throughput but caps at the attention head count, and unified attention lifts that cap once you have at least 4 devices. | ||
| - **Both weights and sequence are too large.** Combine tensor and context parallelism. [`TensorParallelConfig`] accepts a `mesh` argument so both can share one device mesh. | ||
|
|
||
| Two constraints often decide this before performance does: tensor parallelism requires the model to define a [`_tp_plan`](#writing-a-_tp_plan), and its `tp_degree` must divide the attention head count. Context parallelism has no such per-model requirement and works with most attention backends. | ||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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_planfor their model? That seems non-trivial to me and some useful hints could be nice to have there.There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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.).