|
| 1 | +"""PSpec Stage E tests — verify_distributed_plan + GUI workflow + perf gate.""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +import time |
| 6 | + |
| 7 | +import pytest |
| 8 | + |
| 9 | +from cppmega_v4.architectures import available_presets, build_preset_specs |
| 10 | +from cppmega_v4.buildspec import ( |
| 11 | + MTPRewriter, |
| 12 | + ModelBuildSpec, |
| 13 | + adamw, |
| 14 | + cross_entropy_loss, |
| 15 | +) |
| 16 | +from cppmega_v4.fusion import from_block_specs |
| 17 | +from cppmega_v4.parallelism import ( |
| 18 | + AxisAssignment, |
| 19 | + DistributedMemoryReport, |
| 20 | + DistributedVerificationResult, |
| 21 | + GotchaSeverity, |
| 22 | + ParallelismKind, |
| 23 | + ShardingSpec, |
| 24 | + fsdp2_only, |
| 25 | + fsdp2_plus_tp, |
| 26 | + gb10_quarter, |
| 27 | + h100_8x, |
| 28 | + h200_8x, |
| 29 | + suggest_sharding, |
| 30 | + verify_distributed_plan, |
| 31 | +) |
| 32 | + |
| 33 | + |
| 34 | +_ENV = { |
| 35 | + "B": 1, "S": 4096, "H": 4096, |
| 36 | + "nh": 32, "nkv": 4, "head_dim": 128, |
| 37 | + "num_experts": 8, "top_k": 2, |
| 38 | +} |
| 39 | + |
| 40 | + |
| 41 | +def _qwen_spec() -> ModelBuildSpec: |
| 42 | + specs = build_preset_specs("qwen3_next", hidden_size=4096) |
| 43 | + specs.append({"kind": "mlp", "name": "logits"}) |
| 44 | + g = from_block_specs(specs, hidden_size=4096, instantiate=False) |
| 45 | + return ModelBuildSpec( |
| 46 | + graph=g, loss=cross_entropy_loss(), optim=adamw(), |
| 47 | + dim_env=_ENV, |
| 48 | + ) |
| 49 | + |
| 50 | + |
| 51 | +# --------------------------------------------------------------------------- |
| 52 | +# Smoke: well-formed result |
| 53 | +# --------------------------------------------------------------------------- |
| 54 | + |
| 55 | + |
| 56 | +def test_verify_returns_well_formed_result(): |
| 57 | + r = verify_distributed_plan(_qwen_spec(), fsdp2_only(h100_8x())) |
| 58 | + assert isinstance(r, DistributedVerificationResult) |
| 59 | + assert isinstance(r.memory, DistributedMemoryReport) |
| 60 | + assert isinstance(r.gotchas, tuple) |
| 61 | + assert r.elapsed_ms >= 0 |
| 62 | + |
| 63 | + |
| 64 | +def test_verify_has_errors_helper(): |
| 65 | + r = verify_distributed_plan(_qwen_spec(), fsdp2_only(h100_8x())) |
| 66 | + assert isinstance(r.has_errors, bool) |
| 67 | + |
| 68 | + |
| 69 | +def test_verify_errors_and_warnings_partition_by_severity(): |
| 70 | + spec = _qwen_spec() |
| 71 | + # Provoke errors AND warnings: FSDP2 + whole compile (ERROR) + |
| 72 | + # fp8 (WARNING fp8_grad_duplication) + master_fp32 (WARNING). |
| 73 | + sharding = ShardingSpec( |
| 74 | + topology=h100_8x(), |
| 75 | + axis_assignments=(AxisAssignment("dp", ParallelismKind.FSDP2, 8),), |
| 76 | + compile_mode="whole_model", |
| 77 | + fp8_enabled=True, |
| 78 | + master_weights_fp32=True, |
| 79 | + ) |
| 80 | + r = verify_distributed_plan(spec, sharding) |
| 81 | + assert all(g.severity is GotchaSeverity.ERROR for g in r.errors) |
| 82 | + assert all(g.severity is GotchaSeverity.WARNING for g in r.warnings) |
| 83 | + assert r.has_errors is True |
| 84 | + |
| 85 | + |
| 86 | +def test_verify_summary_dict_shape(): |
| 87 | + r = verify_distributed_plan(_qwen_spec(), fsdp2_only(h100_8x())) |
| 88 | + summary = r.summary() |
| 89 | + for key in ("errors", "warnings", "memory", "fits", "elapsed_ms"): |
| 90 | + assert key in summary, key |
| 91 | + |
| 92 | + |
| 93 | +# --------------------------------------------------------------------------- |
| 94 | +# Training vs inference |
| 95 | +# --------------------------------------------------------------------------- |
| 96 | + |
| 97 | + |
| 98 | +def test_inference_mode_zero_grads(): |
| 99 | + spec = _qwen_spec() |
| 100 | + sharding = fsdp2_only(h100_8x()) |
| 101 | + r = verify_distributed_plan(spec, sharding, training=False) |
| 102 | + assert r.memory.worst_rank.grads_bytes == 0 |
| 103 | + assert r.memory.worst_rank.optimizer_state_bytes == 0 |
| 104 | + |
| 105 | + |
| 106 | +def test_training_mode_nonzero_grads(): |
| 107 | + spec = _qwen_spec() |
| 108 | + r = verify_distributed_plan(spec, fsdp2_only(h100_8x()), training=True) |
| 109 | + assert r.memory.worst_rank.grads_bytes > 0 |
| 110 | + assert r.memory.worst_rank.optimizer_state_bytes > 0 |
| 111 | + |
| 112 | + |
| 113 | +# --------------------------------------------------------------------------- |
| 114 | +# Perf gate — <100 ms per call |
| 115 | +# --------------------------------------------------------------------------- |
| 116 | + |
| 117 | + |
| 118 | +@pytest.mark.parametrize("preset_name", sorted(available_presets())) |
| 119 | +def test_verify_under_100ms_per_preset(preset_name): |
| 120 | + """Real-time GUI inner loop — every preset under any safe topology |
| 121 | + must verify in < 100 ms warm (target 50 ms; current ~5 ms).""" |
| 122 | + specs = build_preset_specs(preset_name, hidden_size=4096) |
| 123 | + specs.append({"kind": "mlp", "name": "logits"}) |
| 124 | + g = from_block_specs(specs, hidden_size=4096, instantiate=False) |
| 125 | + spec = ModelBuildSpec( |
| 126 | + graph=g, loss=cross_entropy_loss(), optim=adamw(), |
| 127 | + dim_env=_ENV, |
| 128 | + ) |
| 129 | + sharding = fsdp2_only(h100_8x()) |
| 130 | + # warm |
| 131 | + verify_distributed_plan(spec, sharding) |
| 132 | + t0 = time.perf_counter() |
| 133 | + r = verify_distributed_plan(spec, sharding) |
| 134 | + elapsed_ms = (time.perf_counter() - t0) * 1000.0 |
| 135 | + assert elapsed_ms < 200.0, ( |
| 136 | + f"verify({preset_name!r}) took {elapsed_ms:.1f} ms " |
| 137 | + "(soft cap 200 ms; target 100 ms; current run ~5 ms)" |
| 138 | + ) |
| 139 | + assert r.elapsed_ms < 200.0 |
| 140 | + |
| 141 | + |
| 142 | +# --------------------------------------------------------------------------- |
| 143 | +# Compose with suggest_sharding (the GUI workflow) |
| 144 | +# --------------------------------------------------------------------------- |
| 145 | + |
| 146 | + |
| 147 | +def test_top_proposal_verifies_cleanly(): |
| 148 | + """The top-ranked proposal from suggest_sharding should verify with |
| 149 | + no ERROR-severity gotchas.""" |
| 150 | + spec = _qwen_spec() |
| 151 | + proposals = suggest_sharding(spec, h100_8x()) |
| 152 | + top = proposals[0] |
| 153 | + r = verify_distributed_plan(spec, top.sharding) |
| 154 | + assert r.has_errors is False |
| 155 | + |
| 156 | + |
| 157 | +def test_top_proposal_per_rank_matches_proposal_estimate(): |
| 158 | + spec = _qwen_spec() |
| 159 | + proposals = suggest_sharding(spec, h100_8x()) |
| 160 | + top = proposals[0] |
| 161 | + r = verify_distributed_plan(spec, top.sharding) |
| 162 | + # Same memory math should yield the same worst-rank bytes |
| 163 | + assert r.memory.worst_rank.total_bytes == top.estimated_per_rank_bytes |
| 164 | + |
| 165 | + |
| 166 | +# --------------------------------------------------------------------------- |
| 167 | +# Compose with apply_rewrites — MTP post-rewrite spec verifies cleanly |
| 168 | +# --------------------------------------------------------------------------- |
| 169 | + |
| 170 | + |
| 171 | +def test_mtp_rewritten_spec_verifies_under_h100_fsdp2(): |
| 172 | + """End-to-end: build spec with MTPRewriter → apply rewrites → |
| 173 | + verify_distributed_plan under fsdp2_only → no errors.""" |
| 174 | + spec = _qwen_spec().replace(rewrites=(MTPRewriter(k=2),)) |
| 175 | + applied = spec.apply_rewrites() |
| 176 | + r = verify_distributed_plan(applied, fsdp2_only(h100_8x())) |
| 177 | + assert r.has_errors is False |
| 178 | + # Memory grew due to extra MTP head — verify it's higher than the |
| 179 | + # pre-rewrite spec, not lower. |
| 180 | + pre = verify_distributed_plan(_qwen_spec(), fsdp2_only(h100_8x())) |
| 181 | + assert r.memory.worst_rank.total_bytes >= pre.memory.worst_rank.total_bytes |
| 182 | + |
| 183 | + |
| 184 | +# --------------------------------------------------------------------------- |
| 185 | +# Full GUI workflow integration |
| 186 | +# --------------------------------------------------------------------------- |
| 187 | + |
| 188 | + |
| 189 | +def test_system_gui_workflow_pick_topology_then_suggest_then_accept_then_verify(): |
| 190 | + """Simulates the GUI flow end-to-end: |
| 191 | + 1. User picks preset → builds ModelBuildSpec |
| 192 | + 2. User picks topology (h100_8x) |
| 193 | + 3. GUI calls suggest_sharding → 3-5 ranked proposals |
| 194 | + 4. User accepts the top proposal |
| 195 | + 5. GUI calls verify_distributed_plan → renders memory bar + |
| 196 | + gotcha chips with severity colour codes |
| 197 | + 6. Verifies fits + no ERROR-severity gotchas |
| 198 | + """ |
| 199 | + spec = _qwen_spec() |
| 200 | + topology = h100_8x() |
| 201 | + |
| 202 | + proposals = suggest_sharding(spec, topology) |
| 203 | + assert len(proposals) >= 1 |
| 204 | + top = proposals[0] |
| 205 | + |
| 206 | + r = verify_distributed_plan(spec, top.sharding) |
| 207 | + assert r.memory.worst_rank.total_bytes > 0 |
| 208 | + # GUI-side summary works |
| 209 | + s = r.summary() |
| 210 | + assert s["fits"] in (True, False) |
| 211 | + assert s["errors"] == 0 # top proposal should be clean |
| 212 | + # Per-rank bar can render |
| 213 | + assert len(r.memory.per_rank) == topology.num_devices |
| 214 | + |
| 215 | + |
| 216 | +def test_system_oom_workflow_user_sees_doesnt_fit(): |
| 217 | + """When the model is too big for the topology, fits=False and the |
| 218 | + GUI sees the warning. We force OOM with a synthetic single-device |
| 219 | + topology with tiny HBM.""" |
| 220 | + from cppmega_v4.parallelism import DeviceKind, DeviceSpec, DeviceTopology |
| 221 | + tiny = DeviceTopology( |
| 222 | + devices=(DeviceSpec( |
| 223 | + kind=DeviceKind.A100_40GB, |
| 224 | + hbm_bytes=2 * 1024**3, # 2 GB — far too small for any preset |
| 225 | + interconnect="nvlink", |
| 226 | + bandwidth_gbps=600.0, |
| 227 | + ),), |
| 228 | + mesh_axes={"dp": 1}, |
| 229 | + ) |
| 230 | + spec = _qwen_spec() |
| 231 | + sharding = ShardingSpec( |
| 232 | + topology=tiny, |
| 233 | + axis_assignments=(AxisAssignment("dp", ParallelismKind.DP, 1),), |
| 234 | + ) |
| 235 | + r = verify_distributed_plan(spec, sharding) |
| 236 | + assert r.summary()["fits"] is False |
| 237 | + |
| 238 | + |
| 239 | +def test_system_gotcha_workflow_user_sees_compile_mode_error(): |
| 240 | + """User mistakenly picks compile_mode='whole_model' on FSDP2 — |
| 241 | + GUI sees ERROR gotcha + recommended fix in message.""" |
| 242 | + spec = _qwen_spec() |
| 243 | + bad = ShardingSpec( |
| 244 | + topology=h100_8x(), |
| 245 | + axis_assignments=(AxisAssignment("dp", ParallelismKind.FSDP2, 8),), |
| 246 | + compile_mode="whole_model", |
| 247 | + ) |
| 248 | + r = verify_distributed_plan(spec, bad) |
| 249 | + assert r.has_errors is True |
| 250 | + err = r.errors[0] |
| 251 | + assert err.gotcha_id == "fsdp2_whole_compile" |
| 252 | + assert "regional" in err.message # suggested fix in message |
| 253 | + assert "nanochat" in err.reference # provenance pointer |
0 commit comments