Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / compile / graph_parallel_plan.py: 94%

85 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""" 

16Graph Parallel Plan - declarative module selection for graph-mode passes. 

17 

18Declares which modules the graph-mode ``FSDPPass`` should shard and which 

19modules each pipeline stage owns. ``FSDPPass`` itself owns all the actual 

20sharding logic (all_gather on parameter placeholders, reduce_scatter on 

21gradient outputs, in-place live-model sharding), so this is purely a *which 

22modules* lookup. 

23 

24Note: 

25 Earlier revisions stored an ``FSDPModuleConfig`` dataclass per entry and 

26 exposed ``get_fsdp_config`` / ``merge``. The dataclass carried no field 

27 beyond the FQN that already keys it, and neither helper had a production 

28 consumer. They were removed as dead surface; when a real per-module 

29 option (reshard, CPU offload) lands it can re-add a value type with a 

30 real consumer. 

31""" 

32 

33__all__ = [ 

34 "GraphParallelPlan", 

35 "create_plan_from_yaml", 

36 "create_all_fsdp_plan", 

37] 

38 

39import fnmatch 

40from dataclasses import dataclass, field 

41from pathlib import Path 

42from typing import List, Optional, Set 

43 

44import yaml 

45 

46DEFAULT_CONFIG_DIR = Path(__file__).parent / "examples" 

47 

48 

49@dataclass 

50class GraphParallelPlan: 

51 """Declare the graph-mode parallel assignment of modules. 

52 

53 ``fsdp_modules`` holds exact module FQNs to FSDP-shard and 

54 ``fsdp_patterns`` holds ``fnmatch`` wildcards; :meth:`is_marked_for_fsdp` 

55 checks the exact set first, then the patterns. 

56 

57 Pipeline-parallel stage assignment lives in 

58 ``pp_module_fqns_per_stage``: a list whose i-th entry is the list of 

59 module FQNs (exact, no wildcards) assigned to stage ``i``. When it is 

60 ``None`` the ``PpPass`` falls back to an automatic even split of the 

61 model's layer-like children (see ``pp_pass._auto_stage_split``). 

62 

63 Example: 

64 plan = GraphParallelPlan() 

65 plan.fsdp_mark("tok_embeddings") 

66 plan.fsdp_mark_pattern("layers.*") 

67 plan.pp_stage(0, ["tok_embeddings", "layers.0"]) 

68 plan.pp_stage(1, ["layers.1", "norm", "lm_head"]) 

69 """ 

70 

71 fsdp_modules: Set[str] = field(default_factory=set) 

72 fsdp_patterns: Set[str] = field(default_factory=set) 

73 pp_module_fqns_per_stage: Optional[List[List[str]]] = None 

74 

75 def fsdp_mark(self, module_fqn: str) -> "GraphParallelPlan": 

76 """Mark a specific module for FSDP sharding (exact match). 

77 

78 Args: 

79 module_fqn: Module fully qualified name. 

80 

81 Returns: 

82 ``self`` (chainable). 

83 

84 Example: 

85 plan.fsdp_mark("tok_embeddings") 

86 """ 

87 self.fsdp_modules.add(module_fqn) 

88 return self 

89 

90 def fsdp_mark_pattern(self, pattern: str) -> "GraphParallelPlan": 

91 """Mark modules for FSDP sharding (wildcard match). 

92 

93 Args: 

94 pattern: Module FQN pattern (``fnmatch`` wildcards, e.g. ``*``, 

95 ``layers.*``). 

96 

97 Returns: 

98 ``self`` (chainable). 

99 

100 Example: 

101 plan.fsdp_mark_pattern("layers.*") 

102 """ 

103 self.fsdp_patterns.add(pattern) 

104 return self 

105 

106 def is_marked_for_fsdp(self, module_fqn: str) -> bool: 

107 """Whether ``module_fqn`` is marked for FSDP sharding. 

108 

109 Pure exact/pattern lookup: an exact mark or any matching pattern 

110 returns ``True``. Ancestor matching (a mark on ``layers.0`` covering 

111 ``layers.0.attention.weight``) is NOT done here — callers such as 

112 ``FSDPPass`` walk a parameter's ancestors and query each level. 

113 """ 

114 if module_fqn in self.fsdp_modules: 

115 return True 

116 

117 return any( 

118 fnmatch.fnmatch(module_fqn, pattern) for pattern in self.fsdp_patterns 

119 ) 

120 

121 def pp_stage(self, stage_idx: int, module_fqns: List[str]) -> "GraphParallelPlan": 

122 """Declare the module FQNs of one pipeline stage (exact match). 

123 

124 Args: 

125 stage_idx: Zero-based stage index, ``stage_idx >= 0``. Stages 

126 may be declared in any order / sparsely; ``PpPass`` 

127 validates completeness (every stage ``0..pp_degree-1`` 

128 declared, no FQN assigned twice) before splitting. 

129 module_fqns: Module FQNs owned by this stage, in model order. 

130 Exact FQNs only — wildcards are rejected because a stage 

131 cut must be unambiguous. 

132 

133 Returns: 

134 ``self`` (chainable). 

135 

136 Raises: 

137 ValueError: If ``stage_idx`` is negative or a module FQN 

138 contains wildcard characters. 

139 

140 Example: 

141 plan.pp_stage(0, ["tok_embeddings", "layers.0"]) 

142 plan.pp_stage(1, ["layers.1", "norm", "lm_head"]) 

143 """ 

144 if stage_idx < 0: 

145 raise ValueError(f"stage_idx must be >= 0, got {stage_idx}") 

146 for fqn in module_fqns: 

147 if any(ch in fqn for ch in "*?["): 

148 raise ValueError( 

149 f"pp_stage() takes exact module FQNs, got wildcard pattern " 

150 f"'{fqn}' — a stage cut must be unambiguous" 

151 ) 

152 if self.pp_module_fqns_per_stage is None: 

153 self.pp_module_fqns_per_stage = [] 

154 while len(self.pp_module_fqns_per_stage) <= stage_idx: 

155 self.pp_module_fqns_per_stage.append([]) 

156 self.pp_module_fqns_per_stage[stage_idx] = list(module_fqns) 

157 return self 

158 

159 

160def create_plan_from_yaml( 

161 config_path: Optional[str] = None, 

162 model_name: Optional[str] = None, 

163) -> GraphParallelPlan: 

164 """Create a ``GraphParallelPlan`` from a YAML configuration file. 

165 

166 Args: 

167 config_path: Path to YAML config file. 

168 model_name: Model name (looks up in ``examples/{model_name}/config.yaml``). 

169 

170 Returns: 

171 GraphParallelPlan object. 

172 

173 Raises: 

174 ValueError: When neither argument is given, ``model_name`` is empty 

175 or contains path separators, or the YAML is not a mapping. 

176 FileNotFoundError: When the resolved config file does not exist. 

177 

178 Example: 

179 plan = create_plan_from_yaml(model_name="llama3") 

180 plan = create_plan_from_yaml(config_path="path/to/config.yaml") 

181 """ 

182 if config_path is None and model_name is None: 

183 raise ValueError("Must provide either config_path or model_name") 

184 

185 if config_path is None: 

186 if not model_name or not isinstance(model_name, str): 

187 raise ValueError("model_name must be a non-empty string") 

188 if ".." in model_name or "/" in model_name or "\\" in model_name: 

189 raise ValueError( 

190 f"Invalid model_name '{model_name}': must not contain path " 

191 "separators or parent directory references" 

192 ) 

193 config_path = DEFAULT_CONFIG_DIR / model_name / "config.yaml" 

194 else: 

195 config_path = Path(config_path) 

196 

197 if not config_path.exists(): 

198 raise FileNotFoundError(f"Config file not found: {config_path}") 

199 

200 with open(config_path, "r", encoding="utf-8") as f: 

201 config = yaml.safe_load(f) 

202 

203 if config is None: 

204 raise ValueError(f"YAML config file is empty: {config_path}") 

205 if not isinstance(config, dict): 

206 raise ValueError( 

207 f"YAML config must be a mapping (dict), " 

208 f"got {type(config).__name__}: {config_path}" 

209 ) 

210 

211 plan = GraphParallelPlan() 

212 

213 fsdp_config = _yaml_section(config, "fsdp", config_path) 

214 _maybe_process_fsdp(plan, fsdp_config) 

215 

216 pp_config = _yaml_section(config, "pp", config_path) 

217 if pp_config.get("stages"): 

218 _process_pp(plan, pp_config) 

219 

220 return plan 

221 

222 

223def _yaml_section(config: dict, key: str, config_path: Path) -> dict: 

224 """Return YAML ``key`` as a mapping; empty/``None`` becomes ``{}``. 

225 

226 ``or {}``: a YAML key present but empty (``fsdp:`` with only comments 

227 under it) parses to None, and ``dict.get(key, {})`` then returns 

228 None instead of the default. 

229 """ 

230 section = config.get(key) or {} 

231 if not isinstance(section, dict): 

232 # A present-but-empty section parses to None and is normalized to {} 

233 # above, so anything landing here is a real scalar/sequence typo. 

234 raise ValueError( 

235 f"YAML '{key}' section must be a mapping (e.g. nested keys or an " 

236 f"empty section); got {type(section).__name__} in {config_path}" 

237 ) 

238 return section 

239 

240 

241def _maybe_process_fsdp(plan: GraphParallelPlan, fsdp_config: dict) -> None: 

242 """Run the FSDP processor when the section is enabled. 

243 

244 Enabled defaults to True when explicit modules/patterns are declared. 

245 """ 

246 has_explicit = bool(fsdp_config.get("modules")) or bool(fsdp_config.get("patterns")) 

247 if fsdp_config.get("enabled", has_explicit): 

248 _process_fsdp(plan, fsdp_config) 

249 

250 

251def _process_fsdp(plan: GraphParallelPlan, fsdp_config: dict) -> None: 

252 """Process FSDP configuration (modules + patterns) into the plan.""" 

253 for module_config in fsdp_config.get("modules", []): 

254 plan.fsdp_mark(module_config["name"]) 

255 

256 for pattern_config in fsdp_config.get("patterns", []): 

257 plan.fsdp_mark_pattern(pattern_config["pattern"]) 

258 

259 

260def _process_pp(plan: GraphParallelPlan, pp_config: dict) -> None: 

261 """Process PP configuration (explicit per-stage FQN lists) into the plan. 

262 

263 YAML shape:: 

264 

265 pp: 

266 stages: 

267 - [tok_embeddings, layers.0] 

268 - [layers.1, norm, lm_head] 

269 

270 An entry may also be a mapping with ``stage`` / ``modules`` keys for 

271 readability:: 

272 

273 pp: 

274 stages: 

275 - stage: 0 

276 modules: [tok_embeddings, layers.0] 

277 """ 

278 for idx, stage in enumerate(pp_config["stages"]): 

279 if isinstance(stage, dict): 

280 stage_idx = stage.get("stage", idx) 

281 module_fqns = list(stage.get("modules", [])) 

282 else: 

283 stage_idx = idx 

284 module_fqns = list(stage) 

285 plan.pp_stage(stage_idx, module_fqns) 

286 

287 

288def create_all_fsdp_plan() -> GraphParallelPlan: 

289 """Create a plan that FSDP-marks every module (``*`` pattern). 

290 

291 Convenience for tests / quick demos. 

292 """ 

293 plan = GraphParallelPlan() 

294 plan.fsdp_mark_pattern("*") 

295 return plan