Skip to content

Commit 5efb24a

Browse files
neeldanispikerheado1234sfc-gh-truwasetohtanaPKUWZP
authored
Merging AutoSP into DeepSpeed (#7860)
# AutoSP: Unlocking Long-Context LLM Training Via Compiler-Based Sequence Parallelism ## Overview AutoSP is a compiler optimization pass that shards inputs along the sequence dimension and enables Ulysses styled sequence parallelism while preventing graph breaks during `torch.compile()`. All the passes operate at the Torch IR on the forward graph. ## API Design ### User-Facing Entry Point: `prepare_autosp_inputs()` Users must explicitly call this function to prepare inputs for AutoSP compilation: ```python def prepare_autosp_inputs( input_id: torch.Tensor, label_id: torch.Tensor, position_id: torch.Tensor = None, attention_mask: torch.Tensor = None, seq_dim: int = 1 ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor] ``` **Purpose**: Symbolize sequence dimension and annotate tensors for identification. **Operations**: 1. Mark sequence dimension as dynamic using `torch._dynamo.decorators.mark_dynamic()` 2. Attach metadata tags for tensor identification for auto-sharding: - `input_id.tag = constants.INPUT_ID_KEY` - `label_id.tag = constants.LABEL_ID_KEY` - `position_id.tag = constants.POSITION_ID_KEY` (if provided) **Rationale**: PyTorch's FX graph tracer requires explicit annotation of data-dependent dimensions. Marking the sequence dimension as dynamic prevents symbolic shape propagation from losing dimension information through reshape/view operations. ## Compilation Passes ### Pass 1: `pass_shard_seq_dim()` **Objective**: Propagate sharded sequence dimension to all consumers. **Algorithm**: 1. Extract symbolic sequence dimension from `input_id` shape metadata 2. Locate the symbolic dimension node in the FX graph 3. Create a floor-divide node: `seq_dim / world_size` 4. Perform worklist-based graph traversal to find all direct and indirect consumers of input node, label node and position id node. 5. Replace symbolic dimension references with sharded dimension in consumer nodes **Rationale**: Reshapes and views that consume the sequence dimension as an argument do not get updated during propagation of symbolic shapes. This pass explicitly rewires the computation graph to use sharded dimensions, enabling proper shape inference downstream. ### Pass 2: `pass_shard_input_ids()` / `pass_shard_label_ids()` / `pass_shard_position_ids()` **Objective**: Insert slicing operations after input tensors. **Implementation**: Call `shard_tensor_node()` utility which inserts slice operations. Each rank retains only the portion of the tensor corresponding to its sequence partition and drops the remaining buffer. **Note on `attention_mask`**: Not sharded because it applies to the full sequence length, not the partitioned dimension. ### Pass 3: `pass_insert_attention_all_to_all()` **Objective**: Insert all-to-all collectives around attention (Ulysses styled) to avoid graph breaks during compilation. **Algorithm**: 1. Identify all SDPA (Scaled Dot-Product Attention) nodes in the graph 2. For each SDPA node with inputs Q, K, V, after each of Q, K, V: insert A2A scatter heads (dim=1), gather sequence (dim=2) 3. Insert A2A after thre attention output O: scatter sequence (dim=2), gather heads (dim=1) **Graph Rewrite Example**: ``` Q [B, N, S/P, H] --A2A(scatter_heads,gather_seq)--> [B, N/P, S, H] K [B, N, S/P, H] --A2A(scatter_heads,gather_seq)--> [B, N/P, S, H] V [B, N, S/P, H] --A2A(scatter_heads,gather_seq)--> [B, N/P, S, H] | SDPA | O [B, N/P, S, H] --A2A(scatter_seq,gather_heads)--> [B, N, S/P, H] ``` **Current support**: Currently only supports `torch.nn.functional.scaled_dot_product_attention()`. Composite attention patterns require additional pattern matching logic. ### Pass 4: `pass_propagate_shapes()` **Objective**: Compute static shapes for all nodes using fake tensor execution. **Implementation**: 1. Create `ShapeEnv` for symbolic dimension tracking 2. Construct `FakeTensorMode` with the shape environment 3. Execute `FakeTensorProp.propagate()` to compute shape metadata ### Pass 5: `pass_canonicalize()` **Objective**: Finalize graph representation. **Operations**: 1. `eliminate_dead_code()`: Remove unused operations 2. `lint()`: Validate graph structure 3. `recompile()`: Regenerate compiled representation ## Execution Order ``` prepare_autosp_inputs() ↓ pass_shard_seq_dim ↓ pass_shard_input_ids ↓ pass_shard_label_ids ↓ pass_shard_position_ids ↓ pass_insert_attention_all_to_all ↓ pass_propagate_shapes ↓ pass_canonicalize ↓ pass_selective_activation_checkpointing ``` ## Memory savings AutoSP adds some heuristics to torch.compile's partitioniner which splits the joint graph into the forward and backward graph. Matmul and related ops are not checkpointed since recomputing them is much cheaper compared to the attention op, while reducing the peak active memory. ## Reducing gradients across ranks AutoSP requires an all-reduce to reduce the gradients across ranks. This is automatically called by DeepSpeed's engine [here](https://github.com/deepspeedai/DeepSpeed/blob/93524c8931799a7631a2321d7ef4afaff6b6e54b/deepspeed/runtime/engine.py#L2433) ## Known Limitations 1. **Attention Pattern Matching**: Only `torch.nn.functional.scaled_dot_product_attention()` is supported. Fused attention implementations require pattern-specific handling. 2. **No Graph Break Requirement**: AutoSP will fail if there are graph breaks because use-def chains are lost and it becomes tricky to propagate auto-sharding information across graph modules. ## Example DeepSpeedExample PR: deepspeedai/DeepSpeedExamples#999 --------- Signed-off-by: Neel Dani <neeldani98@gmail.com> Signed-off-by: Ahan Gupta <ahangupta.96@gmail.com> Signed-off-by: Masahiro Tanaka <mtanaka@anyscale.com> Co-authored-by: Ahan Gupta <ahangupta.96@gmail.com> Co-authored-by: Olatunji Ruwase <tunji.ruwase@snowflake.com> Co-authored-by: Masahiro Tanaka <81312776+tohtana@users.noreply.github.com> Co-authored-by: Zhipeng Wang <zhipeng.rainbowserie@gmail.com>
1 parent 2bae360 commit 5efb24a

16 files changed

Lines changed: 1416 additions & 44 deletions

File tree

deepspeed/compile/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33

44
# DeepSpeed Team
55

6+
from typing import List, Optional, Literal
67
from deepspeed.runtime.config_utils import DeepSpeedConfigModel
78

9+
PassName = Literal["z1", "z3", "autosp"]
10+
811

912
class CompileConfig(DeepSpeedConfigModel):
1013
""" Configure compile settings """
@@ -53,3 +56,6 @@ class CompileConfig(DeepSpeedConfigModel):
5356

5457
keep_all_input_tensors: bool = False
5558
""" Keep real values for all input tensors in InputStorage instead of using dummy values """
59+
60+
passes: Optional[List[PassName]] = None
61+
""" Composes different optimizations. """

deepspeed/compile/constants.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# DeepSpeed Team
5+
6+
#########################################
7+
# AUTOSP
8+
#########################################
9+
AUTOSP_INPUT_ID_KEY = "input_id"
10+
AUTOSP_LABEL_ID_KEY = "label_id"
11+
AUTOSP_POSITION_ID_KEY = "position_id"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# DeepSpeed Team
5+
6+
from .all_to_all import all_to_all
7+
from . import sp_dp_registry
8+
9+
__all__ = ["all_to_all", "sp_dp_registry", "sp_compat"]
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# DeepSpeed Team
5+
6+
import torch
7+
import deepspeed.comm as dist
8+
from torch.utils._sympy.functions import FloorDiv
9+
from .sp_dp_registry import get_group, is_setup, sp_size
10+
11+
12+
@torch.library.custom_op("autosp::all_to_all", mutates_args=())
13+
def all_to_all(
14+
input: torch.Tensor,
15+
scatter_idx: int,
16+
gather_idx: int,
17+
name: str,
18+
) -> torch.Tensor:
19+
"""
20+
All-to-all collective for SDPA tensors [B, N, S, H].
21+
22+
For QKV (scatter_idx=1, gather_idx=2):
23+
[B, N, S/P, H] -> [B, N/P, S, H]
24+
For O (scatter_idx=2, gather_idx=1):
25+
[B, N/P, S, H] -> [B, N, S/P, H]
26+
"""
27+
assert is_setup(), 'Incorrect initialization of SP/DP mesh.'
28+
B, dim1, dim2, H = input.shape
29+
gid = dist.get_rank() // sp_size()
30+
group = get_group(gid)
31+
32+
if scatter_idx == 1:
33+
N, local_S = dim1, dim2
34+
input_t = input.reshape(B, sp_size(), N // sp_size(), local_S, H)
35+
input_t = input_t.permute(1, 0, 2, 3, 4).contiguous()
36+
37+
output = torch.empty_like(input_t)
38+
dist.all_to_all_single(output, input_t, group=group)
39+
40+
output = output.permute(1, 2, 0, 3, 4).contiguous()
41+
output = output.reshape(B, N // sp_size(), sp_size() * local_S, H)
42+
else:
43+
local_N, S = dim1, dim2
44+
input_t = input.reshape(B, local_N, sp_size(), S // sp_size(), H)
45+
input_t = input_t.permute(2, 0, 1, 3, 4).contiguous()
46+
47+
output = torch.empty_like(input_t)
48+
dist.all_to_all_single(output, input_t, group=group)
49+
50+
output = output.permute(1, 0, 2, 3, 4).contiguous()
51+
output = output.reshape(B, sp_size() * local_N, S // sp_size(), H)
52+
53+
return output
54+
55+
56+
@torch.library.register_fake("autosp::all_to_all")
57+
def all_to_all_fake(input: torch.Tensor, scatter_idx: int, gather_idx: int, name: str):
58+
59+
def maybe_restore_sharded_dim(dim: torch.SymInt, factor: int):
60+
# Torch 2.9 may keep `P * (s // P)` distinct from the original `s` during
61+
# fake shape propagation. When the local dim is exactly `FloorDiv(s, P)`,
62+
# restore the original symbol so downstream ops see a consistent sequence dim.
63+
node = getattr(dim, "node", None)
64+
if node is None:
65+
return dim * factor
66+
67+
expr = node.expr
68+
if isinstance(expr, FloorDiv) and expr.args[1] == factor:
69+
hint = node.hint * factor if node.has_hint() else None
70+
return node.shape_env.create_symintnode(expr.args[0], hint=hint)
71+
72+
return dim * factor
73+
74+
B, dim1, dim2, H = input.shape
75+
if scatter_idx == 1:
76+
return input.new_empty(B, dim1 // sp_size(), maybe_restore_sharded_dim(dim2, sp_size()), H)
77+
else:
78+
return input.new_empty(B, dim1 * sp_size(), dim2 // sp_size(), H)
79+
80+
81+
def _all_to_all_backward_setup(ctx, inputs, output):
82+
_, scatter_idx, gather_idx, name = inputs
83+
ctx.scatter_idx = gather_idx
84+
ctx.gather_idx = scatter_idx
85+
ctx.name = name + "_grad"
86+
87+
88+
def _all_to_all_backward(ctx, grad):
89+
return (all_to_all(grad, ctx.scatter_idx, ctx.gather_idx, ctx.name), None, None, None)
90+
91+
92+
torch.library.register_autograd("autosp::all_to_all", _all_to_all_backward, setup_context=_all_to_all_backward_setup)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# DeepSpeed Team
5+
6+
import torch
7+
from packaging.version import Version
8+
9+
10+
def _check_autosp_compatibility():
11+
# Strip the local version segment (e.g. +cu128) so CUDA builds don't sort
12+
# above the max bound when using packaging's local-version ordering rules.
13+
torch_version = Version(torch.__version__.split("+")[0])
14+
if torch_version < Version("2.9"):
15+
raise RuntimeError("AutoSP requires PyTorch >= 2.9, found "
16+
f"{torch.__version__}.")
17+
18+
try:
19+
import transformers
20+
if Version(transformers.__version__) > Version("4.50.3"):
21+
raise RuntimeError("AutoSP requires transformers <= 4.50.3, found "
22+
f"{transformers.__version__}.")
23+
except ImportError:
24+
pass # transformers not installed; skip the check
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# DeepSpeed Team
5+
6+
import deepspeed.comm as dist
7+
8+
GROUP_REGISTRY = {} # int -> dist.ProcessGroup
9+
10+
11+
def register_groups(groups):
12+
"""groups: List[List[int]], e.g. [[0,1],[2,3]]"""
13+
for gid, ranks in enumerate(groups):
14+
if gid not in GROUP_REGISTRY:
15+
GROUP_REGISTRY[gid] = dist.new_group(ranks)
16+
17+
18+
def get_group(gid: int):
19+
return GROUP_REGISTRY[gid] if gid is not None else dist.get_world_group()
20+
21+
22+
def get_registry():
23+
return GROUP_REGISTRY
24+
25+
26+
def is_setup():
27+
return GROUP_REGISTRY['is_reg'] if 'is_reg' in GROUP_REGISTRY else False
28+
29+
30+
def extract_mesh_size(param_dict):
31+
sp_size = param_dict.get('sequence_parallel_size', 1)
32+
assert dist.get_world_size() % sp_size == 0, 'World mesh-size should be divisible by SP_SIZE'
33+
dp_size = dist.get_world_size() // sp_size
34+
35+
return sp_size, dp_size
36+
37+
38+
def sp_size():
39+
assert 'SP_SIZE' in GROUP_REGISTRY, 'SP_SIZE not init properly.'
40+
41+
return GROUP_REGISTRY['SP_SIZE']
42+
43+
44+
def dp_size():
45+
assert 'DP_SIZE' in GROUP_REGISTRY, 'DP_SIZE not init properly'
46+
47+
return GROUP_REGISTRY['DP_SIZE']
48+
49+
50+
def populate_registry(SP_SIZE, DP_SIZE):
51+
""" Populate rank to SP/DP mesh index. """
52+
53+
if GROUP_REGISTRY.get('is_reg', False):
54+
return
55+
56+
group_listing = []
57+
offset = 0
58+
for _ in range(DP_SIZE):
59+
group_listing.append([i + offset for i in range(SP_SIZE)])
60+
offset += SP_SIZE
61+
62+
register_groups(group_listing)
63+
64+
## Extraneous metadata required for proper instatiation. ##
65+
GROUP_REGISTRY['SP_SIZE'] = SP_SIZE
66+
GROUP_REGISTRY['DP_SIZE'] = DP_SIZE
67+
GROUP_REGISTRY['is_reg'] = True

deepspeed/compile/fx.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
# DeepSpeed Team
55

6-
from typing import Callable, Any, List, Dict
6+
from typing import Callable, Any, List, Dict, Optional
77
from collections import defaultdict
88

99
import torch
10-
from torch.fx import Node, Graph
10+
from torch.fx import Node, Graph, GraphModule
1111

1212
from .util import get_last_uses
1313

@@ -138,3 +138,32 @@ def free_tensors(tensors: List[torch.Tensor]):
138138

139139
# Python version for debugging
140140
# graph.create_node('call_function', free_tensors, args, {}, name=node_name)
141+
142+
143+
def find_node_by_name(gm: GraphModule, name: str) -> Optional[Node]:
144+
for node in gm.graph.nodes:
145+
if node.name == name:
146+
return node
147+
return None
148+
149+
150+
def get_node_shape_meta(node: Node) -> Optional[torch.Tensor]:
151+
return node.meta.get("val") or node.meta.get("example_value")
152+
153+
154+
def find_node_by_tag(gm: GraphModule, tag: str) -> Optional[Node]:
155+
input_id_node = None
156+
for node in gm.graph.nodes:
157+
# https://github.com/pytorch/pytorch/blob/085b71eab05cbc7d474a173884269c62d2778f77/torch/_dynamo/utils.py#L5048
158+
tensor_dict = node.meta.get('tensor_dict')
159+
if tensor_dict and tensor_dict.get('tag') == tag:
160+
input_id_node = node
161+
break
162+
return input_id_node
163+
164+
165+
def replace_node_users(node: Node, replacement: Node, exclude: Optional[List[Node]] = None):
166+
exclude = exclude or []
167+
to_replace = [u for u in node.users if u not in exclude]
168+
for user in to_replace:
169+
user.replace_input_with(node, replacement)

deepspeed/compile/init_sp.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
# DeepSpeed Team
5+
6+
import torch
7+
from torch.fx import GraphModule
8+
from .passes.sp_compile import apply_autosp
9+
from .passes.long_context_checkpointing import register_long_context_checkpointing
10+
from .custom_ops.sp_dp_registry import extract_mesh_size
11+
from .custom_ops.sp_compat import _check_autosp_compatibility
12+
13+
14+
def init_autosp(config):
15+
_check_autosp_compatibility()
16+
sp_size, dp_size = extract_mesh_size(config._param_dict)
17+
register_long_context_checkpointing()
18+
19+
def backend_fn(gm: GraphModule, real_inputs):
20+
apply_autosp(gm, real_inputs, debug=False, sp_size=sp_size, dp_size=dp_size)
21+
return torch._inductor.compile(gm, real_inputs)
22+
23+
return backend_fn

0 commit comments

Comments
 (0)