Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / distributed / _builder / tp_collective_lowering.py: 87%

78 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-25 05:45 +0800

1# Copyright 2026 Huawei Technologies Co., Ltd 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14# ============================================================================ 

15"""Lower TP placement transitions to differentiable local-tensor collectives.""" 

16 

17import logging 

18from dataclasses import dataclass 

19from typing import Any, Optional, Sequence 

20 

21import torch 

22import torch.distributed as dist 

23 

24from hyper_parallel.core.dtensor.placement_types import Partial, Placement, Replicate, Shard 

25from hyper_parallel.core.utils import communication 

26 

27logger = logging.getLogger(__name__) 

28 

29 

30def classify_tp_transition( 

31 src: Sequence[Placement], 

32 dst: Sequence[Placement], 

33 mesh_dim_names: Sequence[str], 

34) -> Optional[dict]: 

35 """Classify one placement transition for the TP collective path. 

36 

37 Single source of truth shared by the runtime lowerer 

38 (``TPCollectiveLowerer``) and the codegen boundary-form classifier 

39 (``hyper_parallel.codegen.plan.boundary_forms``), so the emit-time form 

40 decision and the install-time lowering can never disagree about what a 

41 transition means. 

42 

43 Args: 

44 src: Source placements aligned to ``mesh_dim_names``. 

45 dst: Target placements aligned to ``mesh_dim_names``. 

46 mesh_dim_names: Axis names the placements resolve against. 

47 

48 Returns: 

49 ``{"kind": "identity"}`` when no communication is needed; 

50 ``{"kind": "all_gather", "tensor_dim": d}`` for Shard -> Replicate on 

51 the tp axis; ``{"kind": "all_reduce", "reduce_op": "sum"}`` for 

52 Partial -> Replicate; ``{"kind": "reduce_scatter", "tensor_dim": d, 

53 "reduce_op": "sum"}`` for Partial -> Shard; ``None`` when the 

54 transition is not lowerable on this path (a non-tp axis differs, the 

55 placement pair is unsupported, or the reduce op is not ``sum``). 

56 """ 

57 if tuple(src) == tuple(dst): 

58 return {"kind": "identity"} 

59 if "tp" not in mesh_dim_names: 

60 return None 

61 tp_axis = tuple(mesh_dim_names).index("tp") 

62 if any( 

63 axis != tp_axis and src_placement != dst_placement 

64 for axis, (src_placement, dst_placement) in enumerate(zip(src, dst)) 

65 ): 

66 return None 

67 

68 src_tp = src[tp_axis] 

69 dst_tp = dst[tp_axis] 

70 if isinstance(src_tp, Shard) and isinstance(dst_tp, Replicate): 

71 return {"kind": "all_gather", "tensor_dim": src_tp.dim} 

72 if isinstance(src_tp, Partial) and isinstance(dst_tp, Replicate): 

73 if src_tp.reduce_op != "sum": 

74 return None 

75 return {"kind": "all_reduce", "reduce_op": "sum"} 

76 if isinstance(src_tp, Partial) and isinstance(dst_tp, Shard): 

77 if src_tp.reduce_op != "sum": 

78 return None 

79 return { 

80 "kind": "reduce_scatter", 

81 "tensor_dim": dst_tp.dim, 

82 "reduce_op": "sum", 

83 } 

84 return None 

85 

86 

87@dataclass 

88class TPExecutionOp: 

89 """Executable local-tensor operation for one TP placement transition.""" 

90 

91 kind: str 

92 group: object 

93 group_size: int 

94 group_rank: int 

95 tensor_dim: Optional[int] = None 

96 reduce_op: str = "sum" 

97 

98 def execute(self, tensor: Any) -> Any: 

99 """Execute the differentiable collective selected during lowering.""" 

100 if self.kind == "all_gather": 

101 return communication.differentiable_all_gather_concat( 

102 tensor, 

103 self.group, 

104 self.group_size, 

105 self.tensor_dim, 

106 ) 

107 if self.kind == "all_reduce": 

108 return communication.differentiable_all_reduce( 

109 tensor, 

110 self.reduce_op, 

111 self.group, 

112 ) 

113 if self.kind == "reduce_scatter": 

114 return communication.differentiable_reduce_scatter( 

115 tensor, 

116 self.group_size, 

117 self.tensor_dim, 

118 self.reduce_op, 

119 self.group, 

120 ) 

121 if self.kind == "all_reduce_shard": 

122 reduced = communication.differentiable_all_reduce( 

123 tensor, 

124 self.reduce_op, 

125 self.group, 

126 ) 

127 return torch.chunk( 

128 reduced, 

129 self.group_size, 

130 dim=self.tensor_dim, 

131 )[self.group_rank] 

132 raise ValueError(f"Unsupported TP execution operation: {self.kind!r}") 

133 

134 

135@dataclass(frozen=True) 

136class TPCollectiveLowerer: 

137 """Lower supported TP-only placement differences to execution operations.""" 

138 

139 mesh_dim_names: tuple[str, ...] 

140 group: object 

141 group_size: int 

142 group_rank: int 

143 backend: str 

144 

145 def __call__( 

146 self, 

147 src: Sequence[Placement], 

148 dst: Sequence[Placement], 

149 ) -> Optional[TPExecutionOp]: 

150 """Return an execution operation or ``None`` for the generic fallback.""" 

151 result = classify_tp_transition(src, dst, self.mesh_dim_names) 

152 if result is None or result["kind"] == "identity": 

153 return None 

154 return self.execution_op( 

155 result["kind"], 

156 tensor_dim=result.get("tensor_dim"), 

157 reduce_op=result.get("reduce_op", "sum"), 

158 ) 

159 

160 def execution_op( 

161 self, 

162 kind: str, 

163 tensor_dim: Optional[int] = None, 

164 reduce_op: str = "sum", 

165 ) -> TPExecutionOp: 

166 """Build an execution op for one bare-operator call by kind. 

167 

168 The codegen bare-operator runtime (``runtime.TPOperators``) calls this 

169 with the kind the generated forward named, so a statically emitted 

170 ``reduce_scatter`` runs the exact same gloo remap (all_reduce + local 

171 chunk) the placement-driven ``__call__`` path applies. 

172 """ 

173 # Gloo has no reduce_scatter: lower to all_reduce + local chunk, the 

174 # same remap the bare-operator runtime (``TPOperators``) applies. 

175 if kind == "reduce_scatter" and "gloo" in self.backend: 

176 kind = "all_reduce_shard" 

177 return TPExecutionOp( 

178 kind=kind, 

179 group=self.group, 

180 group_size=self.group_size, 

181 group_rank=self.group_rank, 

182 tensor_dim=tensor_dim, 

183 reduce_op=reduce_op, 

184 ) 

185 

186 

187def create_tp_collective_lowerer( 

188 mesh: Any, 

189 mesh_dim_names: Sequence[str], 

190 *, 

191 collective_backend: Optional[str] = None, 

192) -> Optional[TPCollectiveLowerer]: 

193 """Resolve immutable TP runtime metadata and create the boundary lowerer.""" 

194 mesh_dim_names = tuple(mesh_dim_names) 

195 if "tp" not in mesh_dim_names: 

196 return None 

197 

198 tp_mesh = mesh if mesh_dim_names == ("tp",) else mesh["tp"] 

199 group = tp_mesh.get_group() 

200 mesh_ranks = tuple(tp_mesh.rank_list) 

201 group_ranks = tuple(dist.get_process_group_ranks(group)) 

202 if mesh_ranks != group_ranks: 

203 logger.warning( 

204 "TP mesh rank order %s differs from process group rank order %s; " 

205 "falling back to DTensor redistribution.", 

206 mesh_ranks, 

207 group_ranks, 

208 ) 

209 return None 

210 

211 backend = collective_backend or dist.get_backend(group) 

212 return TPCollectiveLowerer( 

213 mesh_dim_names=mesh_dim_names, 

214 group=group, 

215 group_size=tp_mesh.size(), 

216 group_rank=tp_mesh.get_local_rank(), 

217 backend=str(backend).lower(), 

218 ) 

219 

220 

221__all__ = [ 

222 "TPCollectiveLowerer", 

223 "TPExecutionOp", 

224 "classify_tp_transition", 

225 "create_tp_collective_lowerer", 

226]