Skip to content

Commit cf50d9b

Browse files
committed
feat(v4/parallelism): PSpec Stage C — gotcha_checker table (15 known footguns)
Adds cppmega_v4.parallelism.gotcha_checker (ParallelismSpec.md §3.4, §6 C). Table-driven catalog of every distributed-training footgun the team has hit in production. Each Gotcha row carries a reference back to the exact nanochat / cppmega file or doc where the lesson was learned. Gotcha dataclass: gotcha_id + severity (ERROR/WARNING/INFO mirroring cppmega_v4.spec.DiagnosticSeverity) + condition callable + message + reference. GotchaSeverity Enum. GOTCHAS table (15 entries, all with verifiable provenance): ERROR: - fsdp2_whole_compile — FSDP2 + whole-model compile = flat loss (PyTorch #144376; nanochat CLAUDE.md) - megatron_tp_whole_compile — Megatron TP + whole-model compile = NaN step 1 (#118435; nanochat/megatron_tp.py) WARNING: - fp8_grad_duplication — FP8 fwd forces bf16/fp32 grad copies - master_fp32_duplication — fp32 master copy doubles param+optim - ep_more_than_16_experts_xla — TPU 4 GB single-tensor crash - tp_master_weights_double_state — TP shard bf16 AND fp32 per rank - grad_reduce_fp32_doubles_grad_buffer — bf16 default; fp32 only for stability - ep_without_moe — dead EP axis when no MoE bricks - checkpointing_off_with_large_seq — likely OOM at S≥4096 INFO: - pp_comm_stream_broken — torch.dist.pipelining serialises P2P - megatron_row_parallel_boundary — AllReduce materialises (B,S,H) - fsdp_allgather_peak_unsharded — peak = full unsharded - sp_replicated_param_allreduce_overhead — norms all-reduce no-overlap - dp_no_optim_sharding — replicate optim on every rank - fp8_with_sgd_loses_precision — SGD + FP8 typically diverges check_gotchas(sharding, build_spec) → tuple[Gotcha]. Pure function, GUI-inner-loop safe. Predicate exceptions swallowed defensively (missing dim_env keys won't break the check). Tests (tests/v4/test_pspec_stage_c.py, 25 cases): - dataclass / table invariants: every gotcha has id+severity+message+ reference; ids unique; severities span all three levels - check_gotchas returns tuple - 15 per-gotcha fire/no-fire tests covering every condition predicate: * fsdp2_whole_compile fires ONLY when both FSDP and whole compile * megatron_tp_whole_compile fires under TP+whole; not regional * fp8/master/tp_master/grad_fp32/ep_without_moe/checkpointing/ pp/row_parallel/fsdp_allgather/sp/dp_no_optim/ep_xla_4gb/ fp8_sgd — each tested both for fire and no-fire branches - clean Qwen3-Next + fsdp2_only(h100_8x) regional compile = no errors - defensive: monkey-patched throwing predicate doesn't crash the check Full v4 regression: 1024 passed / 5 skipped / 0 failed.
1 parent 72b6d9f commit cf50d9b

3 files changed

Lines changed: 699 additions & 0 deletions

File tree

cppmega_v4/parallelism/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
PerRankMemory,
1919
estimate_distributed_memory,
2020
)
21+
from cppmega_v4.parallelism.gotcha_checker import (
22+
GOTCHAS,
23+
Gotcha,
24+
GotchaSeverity,
25+
check_gotchas,
26+
)
2127
from cppmega_v4.parallelism.sharding_spec import (
2228
AxisAssignment,
2329
ParallelismKind,
@@ -48,12 +54,16 @@
4854
"DeviceSpec",
4955
"DeviceTopology",
5056
"DistributedMemoryReport",
57+
"GOTCHAS",
58+
"Gotcha",
59+
"GotchaSeverity",
5160
"ParallelismKind",
5261
"PerRankMemory",
5362
"ShardingSpec",
5463
"TOPOLOGY_BUILTINS",
5564
"a100_8x",
5665
"b100_8x",
66+
"check_gotchas",
5767
"estimate_distributed_memory",
5868
"fsdp2_only",
5969
"fsdp2_plus_tp",
Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
"""Known-gotcha table — catches distributed-training footguns at planning
2+
time so the GUI can flag them BEFORE training launches.
3+
4+
Every Gotcha record encodes one lesson learned by the team. The
5+
``reference`` field points back to the file or doc in ``../nanochat``
6+
or ``../cppmega`` where the original investigation lives, so future
7+
engineers can audit / update the entry when the upstream gotcha is
8+
fixed (or shifts).
9+
10+
The check is purely a function of (ShardingSpec, ModelBuildSpec) — no
11+
runtime; suitable for the real-time GUI inner loop.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
from collections.abc import Callable
17+
from dataclasses import dataclass
18+
from enum import Enum
19+
20+
from cppmega_v4.buildspec import ModelBuildSpec
21+
from cppmega_v4.parallelism.sharding_spec import (
22+
ParallelismKind,
23+
ShardingSpec,
24+
)
25+
26+
27+
class GotchaSeverity(str, Enum):
28+
"""Mirrors cppmega_v4.spec.DiagnosticSeverity so GUIs share one legend."""
29+
30+
INFO = "info"
31+
WARNING = "warning"
32+
ERROR = "error"
33+
34+
35+
@dataclass(frozen=True)
36+
class Gotcha:
37+
"""One row in the gotcha table."""
38+
39+
gotcha_id: str
40+
severity: GotchaSeverity
41+
condition: Callable[[ShardingSpec, ModelBuildSpec], bool]
42+
message: str
43+
reference: str # file:line / doc anchor in nanochat / cppmega
44+
45+
46+
# ---------------------------------------------------------------------------
47+
# Helpers used in trigger predicates
48+
# ---------------------------------------------------------------------------
49+
50+
51+
def _kinds(s: ShardingSpec) -> frozenset[ParallelismKind]:
52+
return s.axis_kinds()
53+
54+
55+
def _has(s: ShardingSpec, *kinds: ParallelismKind) -> bool:
56+
return any(k in _kinds(s) for k in kinds)
57+
58+
59+
def _is_tpu(s: ShardingSpec) -> bool:
60+
return s.topology.devices[0].kind.value.startswith("tpu")
61+
62+
63+
def _has_moe(b: ModelBuildSpec) -> bool:
64+
return any(n.kind in {"moe", "bailing_moe"} for n in b.graph.nodes)
65+
66+
67+
def _has_attention(b: ModelBuildSpec) -> bool:
68+
return any(
69+
n.kind in {
70+
"attention", "gated_attention", "gqa_sliding", "cca_attention",
71+
"mla", "mla_absorb", "mistral4_mla", "bailing_mla",
72+
"dsv4_attention", "nsa", "csa_hca",
73+
}
74+
for n in b.graph.nodes
75+
)
76+
77+
78+
def _num_experts(b: ModelBuildSpec) -> int:
79+
return int(b.dim_env.get("num_experts", 0))
80+
81+
82+
# ---------------------------------------------------------------------------
83+
# The gotcha table — every entry has a real upstream provenance.
84+
# ---------------------------------------------------------------------------
85+
86+
87+
GOTCHAS: tuple[Gotcha, ...] = (
88+
Gotcha(
89+
gotcha_id="fsdp2_whole_compile",
90+
severity=GotchaSeverity.ERROR,
91+
condition=lambda s, b: (
92+
_has(s, ParallelismKind.FSDP2, ParallelismKind.FSDP1)
93+
and s.compile_mode == "whole_model"
94+
),
95+
message=(
96+
"FSDP2 + whole-model torch.compile produces flat loss "
97+
"(gradients never sync; PyTorch #144376). Use "
98+
"compile_mode='regional' — compile each TransformerBlock "
99+
"BEFORE FSDP2-wrapping."
100+
),
101+
reference="nanochat/CLAUDE.md (fsdp2_compile_section); "
102+
"nanochat/fsdp_cuda.py",
103+
),
104+
Gotcha(
105+
gotcha_id="megatron_tp_whole_compile",
106+
severity=GotchaSeverity.ERROR,
107+
condition=lambda s, b: (
108+
ParallelismKind.TP in _kinds(s)
109+
and s.compile_mode == "whole_model"
110+
),
111+
message=(
112+
"Megatron TP + whole-model compile = NaN step 1 (hooks "
113+
"reorder, param.grad=None triggers recompile mid-backward, "
114+
"PyTorch #118435). Use compile_mode='regional'."
115+
),
116+
reference="nanochat/scripts/base_train.py (regional_compile flag); "
117+
"nanochat/megatron_tp.py:69-108",
118+
),
119+
Gotcha(
120+
gotcha_id="fp8_grad_duplication",
121+
severity=GotchaSeverity.WARNING,
122+
condition=lambda s, b: s.fp8_enabled,
123+
message=(
124+
"FP8 forward forces bf16/fp32 grad copies — no FP8 backward "
125+
"kernel. Effective weight+grad ≈ 3× param-elements (fp8 weight "
126+
"+ bf16 grad), not the naive 0.5× FP8 promises. Use Muon "
127+
"(2 B/param momentum) to mitigate AdamW (8 B/param) overhead."
128+
),
129+
reference="nanochat/fp8_training.py:1-32; "
130+
"nanochat/CLAUDE.md (fp8_grad_section)",
131+
),
132+
Gotcha(
133+
gotcha_id="master_fp32_duplication",
134+
severity=GotchaSeverity.WARNING,
135+
condition=lambda s, b: s.master_weights_fp32,
136+
message=(
137+
"master_weights_fp32=True keeps an fp32 master copy alongside "
138+
"bf16/fp8 compute weights. Doubles param memory + doubles "
139+
"optimizer state. Use Muon + bf16 loss scaling instead "
140+
"(nanochat production pattern)."
141+
),
142+
reference="nanochat/megatron_optimizer.py (master_weight section)",
143+
),
144+
Gotcha(
145+
gotcha_id="ep_more_than_16_experts_xla",
146+
severity=GotchaSeverity.WARNING,
147+
condition=lambda s, b: (
148+
_is_tpu(s)
149+
and _num_experts(b) > 16
150+
and ParallelismKind.EP in _kinds(s)
151+
),
152+
message=(
153+
"TPU XLA has a 4 GB single-tensor limit. With >16 fused MoE "
154+
"experts the [N_experts, C_e_fused, D] tensor can exceed it "
155+
"and crash. Set --xla_tpu_rwb_fusion=false OR keep "
156+
"experts/rank ≤ 16."
157+
),
158+
reference="nanochat/memory_estimator.py:LLO_4GB_section",
159+
),
160+
Gotcha(
161+
gotcha_id="pp_comm_stream_broken",
162+
severity=GotchaSeverity.INFO,
163+
condition=lambda s, b: _has(s, ParallelismKind.PP, ParallelismKind.PP_VPP),
164+
message=(
165+
"PP comm-stream separation patch is BROKEN in torch.distributed."
166+
"pipelining (the NANOCHAT_PP_COMM_STREAM env var exists but is "
167+
"disabled by default). P2P comm serialises on the default "
168+
"stream — no overlap with compute. Expect ~10-20% throughput "
169+
"hit vs Megatron GPipe."
170+
),
171+
reference="nanochat/pipeline_parallel.py:1-50",
172+
),
173+
Gotcha(
174+
gotcha_id="megatron_row_parallel_boundary",
175+
severity=GotchaSeverity.INFO,
176+
condition=lambda s, b: ParallelismKind.TP in _kinds(s),
177+
message=(
178+
"Megatron RowParallelLinear AllReduce materialises the full "
179+
"(B,S,H) tensor on every rank at the layer's backward boundary. "
180+
"Gradient checkpointing CAN'T elide this — it must be saved "
181+
"for the backward computation."
182+
),
183+
reference="nanochat/megatron_tp.py (RowParallel section); "
184+
"cppmega/cppmega/megatron/memory_debug.py:258-302",
185+
),
186+
Gotcha(
187+
gotcha_id="fsdp_allgather_peak_unsharded",
188+
severity=GotchaSeverity.INFO,
189+
condition=lambda s, b: _has(s, ParallelismKind.FSDP2, ParallelismKind.FSDP1),
190+
message=(
191+
"FSDP all-gather peak == unsharded parameter size. Steady-state "
192+
"memory is divided by dp_degree, but during the forward pass "
193+
"each block briefly materialises its full unsharded weight on "
194+
"every rank. Plan worst-case peak around one fully-gathered block."
195+
),
196+
reference="nanochat/fsdp_cuda.py; "
197+
"nanochat/memory_estimator.py (FSDP all-gather section)",
198+
),
199+
Gotcha(
200+
gotcha_id="sp_replicated_param_allreduce_overhead",
201+
severity=GotchaSeverity.INFO,
202+
condition=lambda s, b: ParallelismKind.SP in _kinds(s),
203+
message=(
204+
"Sequence-parallel with TP: small replicated params (norms, "
205+
"qk-norms) all-reduce separately AFTER backward fills the grad. "
206+
"No overlap with GEMM. Use TE's communicate_next_backward to "
207+
"fuse with the next layer's GEMM."
208+
),
209+
reference="nanochat/megatron_tp.py (_install_sequence_parallel_hooks)",
210+
),
211+
Gotcha(
212+
gotcha_id="tp_master_weights_double_state",
213+
severity=GotchaSeverity.WARNING,
214+
condition=lambda s, b: (
215+
ParallelismKind.TP in _kinds(s)
216+
and s.master_weights_fp32
217+
),
218+
message=(
219+
"TP + master_weights_fp32: each rank holds the local TP weight "
220+
"shard in BOTH bf16 (compute) AND fp32 (optimizer state). The "
221+
"duplication isn't solved at the library level. Drop master "
222+
"weights or accept the per-rank doubling."
223+
),
224+
reference="nanochat/megatron_optimizer.py (TP master section)",
225+
),
226+
Gotcha(
227+
gotcha_id="dp_no_optim_sharding",
228+
severity=GotchaSeverity.INFO,
229+
condition=lambda s, b: (
230+
ParallelismKind.DP in _kinds(s)
231+
and not _has(s, ParallelismKind.FSDP2, ParallelismKind.FSDP1,
232+
ParallelismKind.ZERO1, ParallelismKind.ZERO2)
233+
and s.topology.num_devices > 1
234+
),
235+
message=(
236+
"DP-only (no FSDP / no ZeRO) replicates the full optimizer state "
237+
"on every rank. For >5B-param models on 80 GB devices this is "
238+
"usually the bottleneck. Consider FSDP2 (ZeRO-3) — optim sharded "
239+
"by dp_degree at near-zero throughput cost."
240+
),
241+
reference="cppmega/docs/memory_dtype_audit_2026_04_25.md",
242+
),
243+
Gotcha(
244+
gotcha_id="grad_reduce_fp32_doubles_grad_buffer",
245+
severity=GotchaSeverity.WARNING,
246+
condition=lambda s, b: s.grad_reduce_dtype == "fp32",
247+
message=(
248+
"grad_reduce_dtype='fp32' doubles the gradient-buffer cost vs "
249+
"bf16 reduction. Use bf16 unless you're chasing the last 0.5% "
250+
"of numerical stability on very deep stacks."
251+
),
252+
reference="cppmega/cppmega/megatron/memory_debug.py:258-302 "
253+
"(--local-ddp-disable-contiguous-grad-buffer notes)",
254+
),
255+
Gotcha(
256+
gotcha_id="ep_without_moe",
257+
severity=GotchaSeverity.WARNING,
258+
condition=lambda s, b: (
259+
ParallelismKind.EP in _kinds(s)
260+
and not _has_moe(b)
261+
),
262+
message=(
263+
"EP axis declared but no MoE bricks in the model. EP has nothing "
264+
"to shard — wastes the mesh axis. Drop EP or add a 'moe' brick."
265+
),
266+
reference="cppmega/cppmega/recipes/megatron_args.py (EP guard)",
267+
),
268+
Gotcha(
269+
gotcha_id="checkpointing_off_with_large_seq",
270+
severity=GotchaSeverity.WARNING,
271+
condition=lambda s, b: (
272+
s.activation_checkpointing == "off"
273+
and int(b.dim_env.get("S", 0)) >= 4096
274+
),
275+
message=(
276+
"activation_checkpointing='off' at S ≥ 4096: activations dominate "
277+
"memory and likely OOM. Use 'full' (only block boundaries) or "
278+
"'selective' (per-layer cherry-pick — Mamba-style)."
279+
),
280+
reference="nanochat/memory_estimator.py:activations_section",
281+
),
282+
Gotcha(
283+
gotcha_id="fp8_with_sgd_loses_precision",
284+
severity=GotchaSeverity.INFO,
285+
condition=lambda s, b: (
286+
s.fp8_enabled
287+
and b.optim.kind.value == "sgd"
288+
),
289+
message=(
290+
"FP8 forward + SGD: no momentum to smooth out quantisation noise. "
291+
"Use AdamW (or Muon — preferred for bf16-state optimizer) when "
292+
"FP8 is enabled. SGD + FP8 typically diverges by step ~200."
293+
),
294+
reference="cppmega/docs/memory_dtype_audit_2026_04_25.md "
295+
"(precision-aware storage ladder)",
296+
),
297+
)
298+
299+
300+
# ---------------------------------------------------------------------------
301+
# Public API
302+
# ---------------------------------------------------------------------------
303+
304+
305+
def check_gotchas(
306+
sharding: ShardingSpec,
307+
build_spec: ModelBuildSpec,
308+
) -> tuple[Gotcha, ...]:
309+
"""Run every gotcha trigger; return all that fire.
310+
311+
Pure function — no runtime, suitable for the GUI inner loop on every
312+
sharding-spec / model-spec mutation.
313+
"""
314+
fired: list[Gotcha] = []
315+
for g in GOTCHAS:
316+
try:
317+
if g.condition(sharding, build_spec):
318+
fired.append(g)
319+
except Exception:
320+
# Defensive: a trigger predicate should never raise; if it
321+
# does (e.g. a missing dim_env key) we treat as not-fired
322+
# rather than break the whole check.
323+
continue
324+
return tuple(fired)
325+
326+
327+
__all__ = [
328+
"GOTCHAS",
329+
"Gotcha",
330+
"GotchaSeverity",
331+
"check_gotchas",
332+
]

0 commit comments

Comments
 (0)