Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / compile / examples / automodel_tp / train.py: 0%

135 statements  

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

1#!/usr/bin/env python3 

2# Copyright 2026 Huawei Technologies Co., Ltd 

3# 

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

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

6# You may obtain a copy of the License at 

7# 

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

9# 

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

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

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

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

14# limitations under the License. 

15# ============================================================================ 

16"""TP + FSDP graph-mode demo. 

17 

18Combines automodel's TP sharding with the graph-mode FSDP pass: 

19 

20 1. automodel ``MeshContext.build_meshs`` builds a ``(dp, cp, tp)`` mesh. 

21 The TP group is created here and the boundary forwards will hold the 

22 group object directly. 

23 2. ``ShardingPlanner.plan`` derives a TP ``ShardingPlan``. 

24 3. ``apply_sharding_plan(production)`` turns parameters into plain TP 

25 shards (``to_local``) and wraps module forwards with boundary 

26 redistribution — TP collectives are inserted on **activations** 

27 (colwise all-reduce on exit, rowwise input gather, etc.). 

28 4. FSDP2Manager is NOT used (no ``fully_shard`` hooks). Instead the 

29 TP-sharded model is handed to ``GraphTrainer``: 

30 - the automodel mesh is reused — only the dp sub-mesh is registered 

31 under ``"fsdp"`` (the TP group is already wired into the boundary 

32 forwards); 

33 - ``trace_model_graph`` traces a joint fwd+bwd FX graph; parameters 

34 are static inputs whose shape is the TP shard; 

35 - ``FSDPPass`` shards each already-TP-sharded parameter along dim 0 

36 (FSDP axis, orthogonal to the TP axis), inserts all_gather on the 

37 parameter placeholders (recovers the TP shard) and reduce_scatter 

38 on the gradient outputs; 

39 - ``AutoOverlapPass`` moves wait_tensor for comm/compute overlap. 

40 

41The two communication domains are orthogonal: FSDP talks on parameters/grads 

42(the "fsdp" group), TP talks on activations (the "tp" group baked into the 

43boundary forwards). FSDPPass only sees the FX graph — it is TP-agnostic. 

44""" 

45 

46import argparse 

47import logging 

48import sys 

49from pathlib import Path 

50 

51import torch 

52import torch.distributed as dist 

53import torch.nn.functional as F 

54import yaml 

55 

56_REPO_ROOT = str(Path(__file__).resolve().parent.parent.parent.parent.parent) 

57if _REPO_ROOT not in sys.path: 

58 sys.path.append(_REPO_ROOT) 

59 

60from model import ( # pylint: disable=C0413 

61 build_model as build_model_from_config, 

62 DataSampler, 

63) 

64from hyper_parallel.compile import ( # pylint: disable=C0413 

65 GraphParallelPlan, 

66 GraphTrainer, 

67 PassConfig, 

68) 

69from hyper_parallel.distributed.mesh import MeshContext # pylint: disable=C0413 

70from hyper_parallel.distributed import ( # pylint: disable=C0413 

71 ShardingPlanner, 

72 apply_sharding_plan, 

73) 

74 

75_LOG = logging.getLogger(__name__) 

76 

77 

78def parse_args(): 

79 """Parse example CLI arguments.""" 

80 parser = argparse.ArgumentParser( 

81 description="TP + FSDP graph-mode demo (automodel TP-shard + FSDPPass)" 

82 ) 

83 parser.add_argument("--config", type=str, default="config.yaml") 

84 return parser.parse_args() 

85 

86 

87def load_config(path: str) -> dict: 

88 """Load the example YAML config.""" 

89 with open(path, encoding="utf-8") as f: 

90 return yaml.safe_load(f) 

91 

92 

93def setup_distributed() -> str: 

94 """Init the process group; return the device type to build meshes on.""" 

95 if not dist.is_initialized(): 

96 if hasattr(torch, "npu") and torch.npu.is_available(): 

97 dist.init_process_group(backend="hccl") 

98 torch.npu.set_device(dist.get_rank() % torch.npu.device_count()) 

99 if dist.get_rank() == 0: 

100 _LOG.info( 

101 "[setup] backend=hccl, world_size=%s, npu_device=%s", 

102 dist.get_world_size(), 

103 torch.npu.current_device(), 

104 ) 

105 return "npu" 

106 dist.init_process_group(backend="gloo") 

107 if dist.get_rank() == 0: 

108 _LOG.info("[setup] backend=gloo, world_size=%s", dist.get_world_size()) 

109 return "cpu" 

110 return "cpu" 

111 

112 

113def build_model(cfg: dict, device: torch.device) -> torch.nn.Module: 

114 """Build the model on ``device``. 

115 

116 Delegates to ``build_model`` from ``model.py``, which creates an 

117 ``AutoModelAdapterForCausalLM`` — a structurally correct model where 

118 ``rotary_emb`` lives inside ``Attention.forward`` (not in the inner 

119 model's forward). This means cos/sin are computed *after* the SP 

120 boundary all-gather, so they naturally have the full sequence length 

121 — no runtime patch needed. 

122 """ 

123 return build_model_from_config(cfg["model"], device) 

124 

125 

126def build_mesh_context(parallel_cfg: dict, device_type: str) -> MeshContext: 

127 """Build the automodel (dp, cp, tp) mesh + the dense FSDP sub-mesh.""" 

128 tp = parallel_cfg["tp_size"] 

129 dp = parallel_cfg["dp_size"] 

130 sequence_parallel = parallel_cfg.get("sequence_parallel", False) 

131 loss_parallel = parallel_cfg.get("loss_parallel", False) 

132 world_size = dist.get_world_size() 

133 if tp * dp != world_size: 

134 raise ValueError( 

135 f"tp_size({tp}) * dp_size({dp}) != world_size({world_size}) — this " 

136 "demo assumes tp*dp == world_size (cp=pp=ep=1)." 

137 ) 

138 ctx = MeshContext( 

139 tp_size=tp, 

140 dp_size=dp, 

141 cp_size=1, 

142 pp_size=1, 

143 ep_size=1, 

144 dp_replicate_size=1, 

145 dp_shard_size=dp, 

146 edp_shard_size=1, 

147 sequence_parallel=sequence_parallel, 

148 loss_parallel=loss_parallel, 

149 ) 

150 ctx.build_meshs(device_type, world_size) 

151 ctx.tp_rank = dist.get_rank() % tp 

152 ctx.dp_rank = dist.get_rank() // tp 

153 return ctx 

154 

155 

156def train_fn(model, *, input_ids, labels): 

157 """Standard CE loss on the boundary-wrapped model forward. 

158 

159 In SP mode the lm_head boundary all-gathers hidden_states to full 

160 sequence, so logits and labels both have the full seq_len. The 

161 min_seq alignment is a safety no-op. 

162 """ 

163 outputs = model(input_ids) 

164 if hasattr(outputs, "logits"): 

165 logits = outputs.logits 

166 elif isinstance(outputs, dict) and "logits" in outputs: 

167 logits = outputs["logits"] 

168 else: 

169 logits = outputs[0] 

170 # Align sequence dimension: logits may be shorter than labels in SP mode 

171 min_seq = min(logits.shape[1], labels.shape[1]) 

172 logits = logits[:, :min_seq, :] 

173 labels = labels[:, :min_seq] 

174 shift_logits = logits[..., :-1, :].contiguous() 

175 shift_labels = labels[..., 1:].contiguous() 

176 loss = F.cross_entropy( 

177 shift_logits.view(-1, shift_logits.size(-1)), 

178 shift_labels.view(-1), 

179 ignore_index=-100, 

180 ) 

181 return loss 

182 

183 

184def inspect_graph(trainer: GraphTrainer) -> None: 

185 """Print FSDP and TP comm nodes in the compiled joint graph. 

186 

187 FSDPPass tags its nodes with ``meta['comm_type']`` starting with ``fsdp_``. 

188 TP collectives live inside the boundary forwards and are plain 

189 ``call_function`` nodes whose op name mentions all_gather/all_reduce/ 

190 reduce_scatter. 

191 """ 

192 gm = trainer._compiler._joint_graph.graph_module # pylint: disable=W0212 

193 fsdp, tp_nodes = [], [] 

194 for node in gm.graph.nodes: 

195 if node.op != "call_function": 

196 continue 

197 comm = node.meta.get("comm_type") 

198 target = getattr(node.target, "_qualified_op_name", None) or str(node.target) 

199 if comm and "fsdp" in str(comm): 

200 fsdp.append((node.name, comm)) 

201 elif any(k in target for k in ("all_gather", "reduce_scatter", "all_reduce")): 

202 tp_nodes.append((node.name, target)) 

203 rank = dist.get_rank() if dist.is_initialized() else 0 

204 if rank == 0: 

205 _LOG.info("=" * 70) 

206 _LOG.info("[graph] FSDP comm nodes: %s", len(fsdp)) 

207 for n, c in fsdp[:6]: 

208 _LOG.info(" %s: %s", n, c) 

209 _LOG.info("[graph] TP(boundary) comm nodes: %s", len(tp_nodes)) 

210 for n, t in tp_nodes[:6]: 

211 _LOG.info(" %s: %s", n, t) 

212 _LOG.info("=" * 70) 

213 

214 

215def main(): # pylint: disable=too-many-locals 

216 """Run the TP + FSDP graph-mode demo end to end.""" 

217 logging.basicConfig(level=logging.INFO, format="%(message)s") 

218 args = parse_args() 

219 cfg = load_config(args.config) 

220 

221 device_type = setup_distributed() 

222 device = ( 

223 torch.device(device_type) 

224 if device_type == "cpu" 

225 else torch.device(device_type, dist.get_rank() % torch.npu.device_count()) 

226 ) 

227 rank = dist.get_rank() 

228 world_size = dist.get_world_size() 

229 

230 if rank == 0: 

231 _LOG.info("=" * 70) 

232 _LOG.info("TP + FSDP graph-mode demo") 

233 _LOG.info(" world_size=%s, device=%s", world_size, device_type) 

234 _LOG.info("=" * 70) 

235 

236 # 1. Model on a real device (distribute_tensor needs real params). 

237 tp_size = cfg["parallel"]["tp_size"] 

238 sequence_parallel = cfg["parallel"].get("sequence_parallel", False) 

239 loss_parallel = cfg["parallel"].get("loss_parallel", False) 

240 model = build_model(cfg, device) 

241 

242 # 2. automodel mesh: (dp, cp, tp) + dense FSDP sub-mesh. TP group ready. 

243 mesh_ctx = build_mesh_context(cfg["parallel"], device_type) 

244 

245 # 3. TP sharding plan + production apply. Parameters become plain TP 

246 # shards; module forwards are wrapped with boundary TP collectives on 

247 # activations. When sequence_parallel=True the boundary I/O contract 

248 # changes: attention/mlp boundaries use Shard(1)->Replicate on entry 

249 # and Partial->Shard(1) (reduce_scatter) on exit. 

250 planner = ShardingPlanner() 

251 plan = planner.plan( 

252 model, 

253 mesh_ctx.device_mesh, 

254 tp_size=cfg["parallel"]["tp_size"], 

255 sequence_parallel=sequence_parallel, 

256 loss_parallel=loss_parallel, 

257 ) 

258 model, _ = apply_sharding_plan(model, plan, mesh_ctx, validate_mode=False) 

259 if rank == 0: 

260 _LOG.info( 

261 "[automodel] TP sharding applied — params are now plain TP shards, " 

262 "boundary forwards wrap TP collectives on activations" 

263 ) 

264 if sequence_parallel: 

265 _LOG.info("[SP] sequence_parallel=True, tp_size=%s", tp_size) 

266 

267 # 3a. No rotary_emb patch needed — the model places 

268 # rotary_emb inside Attention.forward (after boundary all-gather), 

269 # so cos/sin naturally have the full sequence length. 

270 if sequence_parallel and rank == 0: 

271 _LOG.info( 

272 "[SP] Using AutoModelAdapterForCausalLM — rotary_emb inside " 

273 "attention, no patch needed" 

274 ) 

275 

276 # 4. FSDP plan for the graph-mode FSDPPass (mark everything). 

277 fsdp_plan = GraphParallelPlan() 

278 fsdp_plan.fsdp_mark_pattern("*") 

279 

280 # 5. GraphTrainer reuses the automodel mesh (tp group already created); 

281 # it registers the dp sub-mesh as "fsdp" and back-fills fsdp_degree. 

282 tcfg = cfg["train"] 

283 trainer = GraphTrainer( 

284 model=model, 

285 train_fn=train_fn, 

286 pass_config=PassConfig( 

287 enable_overlap=cfg["parallel"].get("enable_overlap", True), 

288 fsdp_degree=cfg["parallel"]["dp_size"], 

289 tp_size=tp_size, 

290 sequence_parallel=sequence_parallel, 

291 loss_parallel=loss_parallel, 

292 ), 

293 parallel_plan=fsdp_plan, 

294 optimizer_config={ 

295 "lr": tcfg.get("lr", 1e-4), 

296 "grad_clip": tcfg.get("grad_clip", 1.0), 

297 }, 

298 mesh_context=mesh_ctx, 

299 device=device, 

300 ) 

301 

302 # 6. Dummy data + explicit compile so we can inspect the graph. 

303 # DataSampler yields full-sequence data; SP sharding is handled by 

304 # the embedding boundary's reduce-scatter on hidden_states. 

305 mcfg = cfg["model"] 

306 sampler = DataSampler( 

307 vocab_size=mcfg["vocab_size"], 

308 batch_size=tcfg["batch_size"], 

309 seq_len=tcfg["seq_len"], 

310 max_steps=tcfg["max_steps"], 

311 tp_size=tp_size, 

312 tp_rank=mesh_ctx.tp_rank, 

313 sequence_parallel=sequence_parallel, 

314 device=device, 

315 ) 

316 

317 # Sample one batch for compilation (shape must match runtime batches) 

318 input_batch, label_batch = sampler.sample() 

319 if sequence_parallel and rank == 0: 

320 _LOG.info( 

321 "[SP] sample input full seq_len=%s (SP sharding via embedding " 

322 "reduce-scatter)", 

323 input_batch.shape[1], 

324 ) 

325 trainer.compile(input_ids=input_batch, labels=label_batch) 

326 inspect_graph(trainer) 

327 

328 # 7. Training data iterator (DataSampler yields full-sequence batches) 

329 _LOG.info("\nStarting training...") 

330 trainer.train( 

331 iter(sampler), 

332 max_steps=tcfg["max_steps"], 

333 log_interval=cfg["logging"]["log_interval"], 

334 ) 

335 if rank == 0: 

336 _LOG.info("=" * 70) 

337 _LOG.info("Training completed!") 

338 _LOG.info("=" * 70) 

339 

340 if dist.is_initialized(): 

341 dist.destroy_process_group() 

342 

343 

344if __name__ == "__main__": 

345 main()