Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 7.3k
[core] Support tensor parallelism for model inference (CUDA, Neuron)#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
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
98f6c8cc58b8b833674090c51734a76953c24803881469c04929ab7252cac7630cb35328a50867fab0c468689e5da793083bb9c7cc4facabdff1f321c930c41eb5ff9cbe8f2816b96067f13f68a46cb19a354b88931bb859ab6dc348fb75bc350f7b644477a03cb7259da93edd44f7723fc043ee6d20d8e76a2fc034ba9e4907524af2aed7b9b048b915eeb1720dad289cf8b629cd9c3eaab29930a43d5155802cf133732b3d81307ea75f7c73cf09eb58402c3e123cdc33e26491c537c5b6c89310c471909dfcf70212e5c130e00b0b3b7c4f2fea593dae9c4bb881d1ba454fd2733b444eba6e5f661c606f7976911c58eb3901324b7ae6b6d84695c6e0680164681422184533613423e8dcefcd55ac1848ea484fcc2bcb2364649ad5536d737945a686e3795c2549d57365c53c8001431112cb6f3db51a676e7978cad487860a0d38a4bd4f49900df96171ebf9f3b530da4e1a6ee7bff21d7e4cb6a1e180a562596b36699000a70415d5d1c382e9ab320ec6ecd25f3File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
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.
Uh oh!There was an error while loading. Please reload this page. | ||
| 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 tensor parallelism plan | ||
| 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.
Uh oh!There was an error while loading. Please reload this page. | ||
| | 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(), | ||
| ``` | ||
JingyaHuang marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| Both forms appear in one plan in [`transformer_flux2.py`](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/transformers/transformer_flux2.py): the double-stream SwiGLU pair passes `blocks` literally, while the fused single-stream QKV+MLP projection stores its sizes on the `Linear` in `Flux2ParallelSelfAttention.__init__` because they depend on `mlp_ratio` and `mlp_mult_factor`. | ||
| #### 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 — see [`transformer_qwenimage.py`](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/transformers/transformer_qwenimage.py), [`transformer_flux.py`](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/transformers/transformer_flux.py), and [`transformer_flux2.py`](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/transformers/transformer_flux2.py) respectively. | ||
| ## 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. Pick a backend from the [Ulysses/Ring benchmarks](#ulysses-attention) above. | ||
| - **Both weights and sequence are too large.** Combine tensor and context parallelism. [`TensorParallelConfig`] accepts a `mesh` argument so both can share one device mesh. This combination is not yet validated, so treat it as experimental. | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.