Skip to content

Commit 106f909

Browse files
committed
feat(v4/parallelism): PSpec Stage D — auto_shard heuristic proposer
Adds cppmega_v4.parallelism.auto_shard (ParallelismSpec.md §3.5, §6 D). Heuristic-driven proposer that produces 3-5 ranked ShardingProposals informed by the production patterns the team uses in ../cppmega (Megatron EP=4/8) and ../nanochat (FSDP2 + Megatron TP + regional compile). ShardingProposal frozen dataclass: strategy_name + sharding + human-readable reason + estimated_per_rank_bytes + fits + tuple of fired gotchas + .num_errors helper. Five candidate builders: - _propose_single_device: DP-only baseline; always present - _propose_fsdp2: ZeRO-3 across dp axis; multi-device only - _propose_megatron_ep: cppmega EP=4/8 pattern; MoE + ep>=2 axis only - _propose_fsdp2_plus_tp: 3D parallel base; dp>=2 + tp>=2 axes only - _propose_fsdp2_plus_ep: ZeRO-3 backbone + EP experts; MoE + dp>=2 + ep>=2 only Scoring (lower = better): - any ERROR-severity gotcha → score += 10^15 (pushed to bottom but kept visible — the GUI shows "this would crash" instead of hiding the option) - doesn't fit on topology → score += 10^14 - otherwise → estimated_per_rank_bytes Every emitted proposal uses compile_mode='regional' (the only safe mode per the gotcha checker — avoids FSDP2 / Megatron whole-compile footguns) and master_weights_fp32=False (Muon handles bf16 loss scaling, saves duplication overhead). Tests (tests/v4/test_pspec_stage_d.py, 31 cases): - shape/ranking: ≥1 proposal returned; sorted by score ascending; top pick fits OR has no ERRORs - heuristic: single-device topology → only single_device proposal; MoE → EP-family appears when ep axis available; dense → no EP; 3D mesh → fsdp2_plus_tp appears; no-tp mesh skips it - multi-device always proposes fsdp2_only - compile-mode safety: every proposal uses 'regional'; no proposal enables master_weights_fp32 by default - reason text non-empty; gotcha tuple carried; num_errors helper matches actual count - fits flag consistent with per_rank_bytes vs HBM headroom - system: every preset (7 of 12) yields ≥1 proposal on both {gb10_quarter, h100_8x}; at least one clean (no-error) proposal exists Full v4 regression: 1055 passed / 5 skipped / 0 failed.
1 parent cf50d9b commit 106f909

3 files changed

Lines changed: 554 additions & 0 deletions

File tree

cppmega_v4/parallelism/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313

1414
from __future__ import annotations
1515

16+
from cppmega_v4.parallelism.auto_shard import (
17+
ShardingProposal,
18+
suggest_sharding,
19+
)
1620
from cppmega_v4.parallelism.distributed_memory import (
1721
DistributedMemoryReport,
1822
PerRankMemory,
@@ -59,12 +63,14 @@
5963
"GotchaSeverity",
6064
"ParallelismKind",
6165
"PerRankMemory",
66+
"ShardingProposal",
6267
"ShardingSpec",
6368
"TOPOLOGY_BUILTINS",
6469
"a100_8x",
6570
"b100_8x",
6671
"check_gotchas",
6772
"estimate_distributed_memory",
73+
"suggest_sharding",
6874
"fsdp2_only",
6975
"fsdp2_plus_tp",
7076
"gb10_quarter",
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
"""Heuristic sharding-strategy proposer.
2+
3+
Given a :class:`ModelBuildSpec` and a :class:`DeviceTopology`, produces
4+
3-5 ranked :class:`ShardingProposal`s. Heuristics are informed by the
5+
production patterns the team uses in ``../cppmega`` and ``../nanochat``:
6+
7+
- **MoE present → EP first** (degree = sqrt(num_experts) clamped to the
8+
available mesh) — the cppmega EP=4/8 pattern.
9+
- **>70B params on ≤80 GB device → FSDP2 mandatory** — single-rank
10+
weights don't fit otherwise.
11+
- **Attention with H ≥ 4096 → TP=2 reduces activation memory** — the
12+
nanochat Megatron TP+SP recipe.
13+
- **<10B params + single device → no-shard / DP-only**.
14+
- **Always compile_mode="regional"** — avoids the FSDP2/Megatron
15+
whole-model-compile footguns (see GotchaChecker).
16+
- **master_weights_fp32=False unless explicitly requested** — Muon
17+
handles bf16 loss scaling fine; saves the duplication overhead.
18+
19+
Each proposal is scored by a fitness metric (lower per-rank memory →
20+
better) and gotcha penalty (any ERROR severity gotcha pushes score to
21+
worst). Proposals that would never fit on the topology are filtered out.
22+
"""
23+
24+
from __future__ import annotations
25+
26+
from dataclasses import dataclass, field
27+
from typing import Iterable
28+
29+
from cppmega_v4.buildspec import ModelBuildSpec
30+
from cppmega_v4.parallelism.distributed_memory import (
31+
DistributedMemoryReport,
32+
estimate_distributed_memory,
33+
)
34+
from cppmega_v4.parallelism.gotcha_checker import (
35+
Gotcha,
36+
GotchaSeverity,
37+
check_gotchas,
38+
)
39+
from cppmega_v4.parallelism.sharding_spec import (
40+
AxisAssignment,
41+
ParallelismKind,
42+
ShardingSpec,
43+
fsdp2_only,
44+
fsdp2_plus_tp,
45+
megatron_ep_only,
46+
single_device,
47+
)
48+
from cppmega_v4.parallelism.topology import DeviceTopology
49+
50+
51+
@dataclass(frozen=True)
52+
class ShardingProposal:
53+
"""One ranked proposal."""
54+
55+
strategy_name: str
56+
sharding: ShardingSpec
57+
reason: str
58+
estimated_per_rank_bytes: int
59+
fits: bool
60+
gotchas: tuple[Gotcha, ...] = field(default_factory=tuple)
61+
62+
@property
63+
def num_errors(self) -> int:
64+
return sum(
65+
1 for g in self.gotchas if g.severity is GotchaSeverity.ERROR
66+
)
67+
68+
69+
# ---------------------------------------------------------------------------
70+
# Heuristic helpers
71+
# ---------------------------------------------------------------------------
72+
73+
74+
def _moe_present(spec: ModelBuildSpec) -> bool:
75+
return any(n.kind in {"moe", "bailing_moe"} for n in spec.graph.nodes)
76+
77+
78+
def _largest_attention_h(spec: ModelBuildSpec) -> int:
79+
env = spec.dim_env or {}
80+
if any(n.kind in {
81+
"gated_attention", "attention", "mla", "mla_absorb",
82+
"gqa_sliding", "cca_attention", "mistral4_mla",
83+
} for n in spec.graph.nodes):
84+
return int(env.get("H", 0))
85+
return 0
86+
87+
88+
def _estimate_total_param_bytes(spec: ModelBuildSpec) -> int:
89+
"""Use the single-device memory report as the baseline param-bytes
90+
proxy (sums brick params_elems × bf16 width)."""
91+
from cppmega_v4.spec import estimate_memory, resolve_shapes
92+
env = spec.dim_env or {"B": 1, "S": 1, "H": 1}
93+
resolved = resolve_shapes(spec.graph, env, strict=False,
94+
available_side_channels=frozenset({
95+
"doc_ids", "token_ids",
96+
}))
97+
base = estimate_memory(resolved, training=False, dtype_bytes=2)
98+
return base.weights_bytes
99+
100+
101+
def _smallest_device_hbm(topology: DeviceTopology) -> int:
102+
return min(d.hbm_bytes for d in topology.devices)
103+
104+
105+
def _propose_single_device(
106+
spec: ModelBuildSpec, topology: DeviceTopology,
107+
) -> ShardingProposal:
108+
sharding = single_device(topology)
109+
report = estimate_distributed_memory(spec, sharding)
110+
return ShardingProposal(
111+
strategy_name="single_device",
112+
sharding=sharding,
113+
reason=(
114+
"DP-only / replicate everything. Fits when each device's HBM "
115+
"holds full model + optimizer."
116+
),
117+
estimated_per_rank_bytes=report.worst_rank.total_bytes,
118+
fits=report.fits_on_topology(),
119+
gotchas=check_gotchas(sharding, spec),
120+
)
121+
122+
123+
def _propose_fsdp2(
124+
spec: ModelBuildSpec, topology: DeviceTopology,
125+
) -> ShardingProposal | None:
126+
if topology.num_devices < 2:
127+
return None
128+
sharding = fsdp2_only(topology)
129+
report = estimate_distributed_memory(spec, sharding)
130+
return ShardingProposal(
131+
strategy_name="fsdp2_only",
132+
sharding=sharding,
133+
reason=(
134+
"FSDP2 (ZeRO-3) sharding across the dp axis. Optimizer + "
135+
"grads divided by dp_degree. Peak weights = unsharded "
136+
"during forward all-gather. Recommended for >10B models on "
137+
"80 GB devices."
138+
),
139+
estimated_per_rank_bytes=report.worst_rank.total_bytes,
140+
fits=report.fits_on_topology(),
141+
gotchas=check_gotchas(sharding, spec),
142+
)
143+
144+
145+
def _propose_megatron_ep(
146+
spec: ModelBuildSpec, topology: DeviceTopology,
147+
) -> ShardingProposal | None:
148+
if not _moe_present(spec):
149+
return None
150+
if "ep" not in topology.mesh_axes or topology.mesh_axes["ep"] < 2:
151+
return None
152+
sharding = megatron_ep_only(topology)
153+
report = estimate_distributed_memory(spec, sharding)
154+
return ShardingProposal(
155+
strategy_name="megatron_ep_only",
156+
sharding=sharding,
157+
reason=(
158+
f"MoE present + EP={topology.mesh_axes['ep']} axis available. "
159+
"Cppmega-style production pattern (Megatron EP=4/8). "
160+
"Each rank holds num_experts/ep_degree experts."
161+
),
162+
estimated_per_rank_bytes=report.worst_rank.total_bytes,
163+
fits=report.fits_on_topology(),
164+
gotchas=check_gotchas(sharding, spec),
165+
)
166+
167+
168+
def _propose_fsdp2_plus_tp(
169+
spec: ModelBuildSpec, topology: DeviceTopology,
170+
) -> ShardingProposal | None:
171+
if "tp" not in topology.mesh_axes or topology.mesh_axes["tp"] < 2:
172+
return None
173+
if "dp" not in topology.mesh_axes or topology.mesh_axes["dp"] < 2:
174+
return None
175+
sharding = fsdp2_plus_tp(topology)
176+
report = estimate_distributed_memory(spec, sharding)
177+
return ShardingProposal(
178+
strategy_name="fsdp2_plus_tp",
179+
sharding=sharding,
180+
reason=(
181+
"3D parallelism: FSDP2 across dp + Megatron TP across tp. "
182+
"TP halves per-layer matmul activation memory. Use when "
183+
"attention has H ≥ 4096 or very deep stacks. "
184+
"compile_mode='regional' is mandatory."
185+
),
186+
estimated_per_rank_bytes=report.worst_rank.total_bytes,
187+
fits=report.fits_on_topology(),
188+
gotchas=check_gotchas(sharding, spec),
189+
)
190+
191+
192+
def _propose_fsdp2_plus_ep(
193+
spec: ModelBuildSpec, topology: DeviceTopology,
194+
) -> ShardingProposal | None:
195+
"""FSDP2 across dp + EP across ep — for MoE models on 3D meshes."""
196+
if not _moe_present(spec):
197+
return None
198+
if "ep" not in topology.mesh_axes or topology.mesh_axes["ep"] < 2:
199+
return None
200+
if "dp" not in topology.mesh_axes or topology.mesh_axes["dp"] < 2:
201+
return None
202+
sharding = ShardingSpec(
203+
topology=topology,
204+
axis_assignments=(
205+
AxisAssignment("dp", ParallelismKind.FSDP2,
206+
topology.mesh_axes["dp"]),
207+
AxisAssignment("ep", ParallelismKind.EP,
208+
topology.mesh_axes["ep"]),
209+
),
210+
compile_mode="regional",
211+
)
212+
report = estimate_distributed_memory(spec, sharding)
213+
return ShardingProposal(
214+
strategy_name="fsdp2_plus_ep",
215+
sharding=sharding,
216+
reason=(
217+
"FSDP2 backbone + EP for MoE experts. The killer combo for "
218+
"100B+ MoE models on multi-node clusters. Each rank's "
219+
"memory = (backbone params / dp) + (expert params / ep)."
220+
),
221+
estimated_per_rank_bytes=report.worst_rank.total_bytes,
222+
fits=report.fits_on_topology(),
223+
gotchas=check_gotchas(sharding, spec),
224+
)
225+
226+
227+
# ---------------------------------------------------------------------------
228+
# Scoring + ranking
229+
# ---------------------------------------------------------------------------
230+
231+
232+
_ERROR_PENALTY: int = 10 ** 15 # any ERROR gotcha pushes score to worst
233+
234+
235+
def _score(p: ShardingProposal) -> int:
236+
"""Lower is better. ERROR gotchas dominate; otherwise fits + per-rank."""
237+
if p.num_errors > 0:
238+
return _ERROR_PENALTY + p.estimated_per_rank_bytes
239+
if not p.fits:
240+
# Doesn't fit → ranks below any fitting strategy.
241+
return (_ERROR_PENALTY // 10) + p.estimated_per_rank_bytes
242+
return p.estimated_per_rank_bytes
243+
244+
245+
def suggest_sharding(
246+
build_spec: ModelBuildSpec,
247+
topology: DeviceTopology,
248+
) -> list[ShardingProposal]:
249+
"""Produce 3-5 ranked proposals.
250+
251+
Returns a list sorted by :func:`_score` ascending. The first entry
252+
is the recommended choice; the rest are alternatives the user can
253+
pick from. Proposals that emit ERROR-severity gotchas are kept in
254+
the list but ranked last (so the GUI can show "this would crash —
255+
here's a safer alternative" instead of hiding the option).
256+
"""
257+
proposals: list[ShardingProposal] = []
258+
for builder in (
259+
_propose_single_device,
260+
_propose_fsdp2,
261+
_propose_megatron_ep,
262+
_propose_fsdp2_plus_tp,
263+
_propose_fsdp2_plus_ep,
264+
):
265+
p = builder(build_spec, topology)
266+
if p is not None:
267+
proposals.append(p)
268+
proposals.sort(key=_score)
269+
return proposals
270+
271+
272+
__all__ = [
273+
"ShardingProposal",
274+
"suggest_sharding",
275+
]

0 commit comments

Comments
 (0)