Bc-AI commited on
Commit
aeec779
Β·
verified Β·
1 Parent(s): 873bc6b

Update nova10b_dense.py

Browse files
Files changed (1) hide show
  1. nova10b_dense.py +176 -106
nova10b_dense.py CHANGED
@@ -19,16 +19,25 @@ Training stack:
19
  O3 FSDP2 – PyTorch FullyShardedDataParallel
20
  O4 Cosine LR – separate schedules for Muon vs AdamW
21
  D9 Balanced streaming – equal tokens per domain, new shards auto-detected
22
- D10 ctx-warmup – sequence-length curriculum (1024 -> 8192)
 
 
 
 
 
 
 
 
 
23
 
24
  Launch:
25
  torchrun --nproc_per_node=4 nova10b_dense.py
26
  torchrun --nproc_per_node=8 nova10b_dense.py
27
 
28
- Debug launch (verbose CUDA/NCCL diagnostics, slower):
29
  NOVA_DEBUG=1 torchrun --nproc_per_node=8 nova10b_dense.py
30
 
31
- NCCL-safe fallback launch (disables P2P/SHM fast paths):
32
  NOVA_NCCL_SAFE=1 torchrun --nproc_per_node=8 nova10b_dense.py
33
  """
34
 
@@ -116,7 +125,7 @@ torch.backends.cuda.matmul.allow_tf32 = True
116
  torch.backends.cudnn.allow_tf32 = True
117
  torch.backends.cudnn.benchmark = True
118
 
119
- # ── logging ───────────────────────────────────────────────────────────────
120
  class _RankFilter(logging.Filter):
121
  def filter(self, record):
122
  record.rank = dist.get_rank() if dist.is_initialized() else 0
@@ -174,7 +183,7 @@ def seed_for_model_init(seed: int):
174
  """
175
  random.seed(seed)
176
  np.random.seed(seed)
177
- torch.manual_seed(seed) # CPU generator only β€” no CUDA touch
178
 
179
 
180
  def seed_for_runtime(seed: int, device: torch.device):
@@ -197,14 +206,16 @@ def seed_for_runtime(seed: int, device: torch.device):
197
  def _stage(name: str):
198
  """
199
  Context manager: logs entry, syncs CUDA before AND after, barriers all
200
- ranks, and on exception logs rank/device/stage/traceback before re-raising.
201
  """
202
  class _StageCtx:
203
  def __enter__(self):
204
  self.t0 = time.time()
205
  dev = torch.cuda.current_device() if torch.cuda.is_available() else -1
206
- log.info(f"[stage:{name}] enter | rank={rank()} device=cuda:{dev} "
207
- f"host={socket.gethostname()}")
 
 
208
  if torch.cuda.is_available():
209
  torch.cuda.synchronize()
210
  return self
@@ -216,9 +227,10 @@ def _stage(name: str):
216
  f"[stage:{name}] FAILED | rank={rank()} device=cuda:{dev} | "
217
  f"{exc_type.__name__}: {exc_val}"
218
  )
219
- log.error("".join(traceback.format_exception(exc_type, exc_val, exc_tb)))
 
 
220
  return False
221
-
222
  if torch.cuda.is_available():
223
  torch.cuda.synchronize()
224
  dt = time.time() - self.t0
@@ -234,8 +246,13 @@ def log_cuda_environment(local_rank: int):
234
  try:
235
  props = torch.cuda.get_device_properties(dev)
236
  cap_major, cap_minor = torch.cuda.get_device_capability(dev)
237
- nccl_ver = torch.cuda.nccl.version() if hasattr(torch.cuda, "nccl") else "unknown"
238
- drv = torch.cuda.driver_version() if hasattr(torch.cuda, "driver_version") else "n/a"
 
 
 
 
 
239
  log.info(
240
  f"[env] rank={rank()} local_rank={local_rank} "
241
  f"gpu={props.name} sm_{cap_major}{cap_minor} "
@@ -251,12 +268,9 @@ def log_cuda_environment(local_rank: int):
251
 
252
  def log_p2p_matrix(local_rank: int, ws: int):
253
  """
254
- Logs pairwise P2P access capability between this rank's device and every
255
- other visible device. H100/H200 SXM nodes typically ship with NVLink +
256
- NVSwitch, so expect P2P access = Y across ALL pairs. If P2P reports False
257
- anywhere on this hardware, that is a red flag worth escalating β€” it means
258
- you are not getting the NVLink fabric you are paying for. Unlike PCIe-only
259
- workstation cards, a False here is unexpected and should be investigated.
260
  """
261
  if not is_main() or not torch.cuda.is_available():
262
  return
@@ -272,8 +286,7 @@ def log_p2p_matrix(local_rank: int, ws: int):
272
  can = torch.cuda.can_device_access_peer(i, j)
273
  row.append("Y" if can else "N")
274
  rows.append(" ".join(row))
275
- log.info("[p2p] pairwise CUDA P2P access matrix (Y=can access, N=cannot):")
276
- log.info("[p2p] expect all-Y on H200 SXM NVLink/NVSwitch nodes:")
277
  for i, r in enumerate(rows):
278
  log.info(f"[p2p] gpu{i}: {r}")
279
  except Exception as e:
@@ -291,23 +304,27 @@ def cuda_health_check(device: torch.device):
291
  p = m @ n
292
  torch.cuda.synchronize(device)
293
  checksum = float(z.sum().item()) + float(p.sum().item())
294
- log.info(f"[health] rank={rank()} device={device} basic op OK "
295
- f"(checksum={checksum:.1f})")
 
 
296
  except Exception as e:
297
- log.error(f"[health] rank={rank()} device={device} FAILED basic CUDA op: {e}")
 
 
298
  raise
299
 
300
 
301
  def nccl_collective_selftest(device: torch.device, ws: int):
302
  """
303
- Isolated NCCL collective stress test, run BEFORE FSDP wrap.
304
- Tests progressively larger all_gather and broadcast ops β€” same primitive
305
- classes used by FSDP unshard() and sync_module_states.
306
 
307
- If this fails β†’ fault is environmental (NCCL/driver), not model code.
308
- If this passes β†’ fault is specific to FSDP/model collective patterns.
309
 
310
- On H200 SXM with NVLink this should pass all sizes easily and quickly.
311
  """
312
  sizes_mb = [1, 16, 64, 256, 1024, 4096]
313
  for mb in sizes_mb:
@@ -317,12 +334,10 @@ def nccl_collective_selftest(device: torch.device, ws: int):
317
  (n_elem,), float(rank()), device=device, dtype=torch.bfloat16
318
  )
319
 
320
- # all_gather β€” same primitive as FSDP unshard()
321
  gathered = [torch.zeros_like(local) for _ in range(ws)]
322
  dist.all_gather(gathered, local)
323
  torch.cuda.synchronize(device)
324
 
325
- # broadcast β€” same primitive as sync_module_states
326
  bcast = local.clone()
327
  dist.broadcast(bcast, src=0)
328
  torch.cuda.synchronize(device)
@@ -352,7 +367,7 @@ def nccl_collective_selftest(device: torch.device, ws: int):
352
  if is_main():
353
  log.info(
354
  f"[nccl_selftest] ALL sizes up to {sizes_mb[-1]}MB passed "
355
- f"on all {ws} ranks β€” NCCL/NVLink healthy"
356
  )
357
 
358
 
@@ -504,30 +519,30 @@ class NovaConfig:
504
 
505
  use_fp8: str = "auto"
506
 
507
- lr: float = 2e-4
508
- min_lr: float = 2e-5
509
- muon_lr: float = 6e-3
510
- muon_min_lr: float = 6e-4
511
- weight_decay: float = 0.1
512
- beta1: float = 0.9
513
- beta2: float = 0.95
514
- muon_momentum: float = 0.95
515
- grad_clip: float = 1.0
516
- warmup_steps: int = 4_000
517
 
518
  total_steps: int = 130_000
519
  ctx_warmup_steps: int = 3_000
520
  ctx_start_len: int = 2048
521
  ctx_end_len: int = 2048
522
 
523
- micro_batch: int = 6
524
- grad_accum: int = 8
525
- log_every: int = 10
526
- ckpt_every: int = 1_000
527
- eval_every: int = 500
528
- ckpt_keep: int = 2
529
- seed: int = 42
530
- max_hours: float = 47.5
531
 
532
  domains: List[str] = field(default_factory=lambda: list(DOMAINS))
533
  shard_refresh_rows: int = 100_000
@@ -550,23 +565,23 @@ class NovaConfig:
550
  def current_ctx_len(self, step: int) -> int:
551
  if self.ctx_warmup_steps <= 0 or step >= self.ctx_warmup_steps:
552
  return self.ctx_end_len
553
- t = step / self.ctx_warmup_steps
554
  raw = int(self.ctx_start_len + t * (self.ctx_end_len - self.ctx_start_len))
555
  return ((raw + 63) // 64) * 64
556
 
557
 
558
  # ══════════════════════════════════════════════════════════════════════════════
559
- # FP8 HELPERS β€” Hopper (sm_90, H100/H200) + Blackwell (sm_100+)
 
 
560
  # ══════════════════════════════════════════════════════════════════════════════
561
 
562
  def _has_fp8_support() -> bool:
563
  """
564
- Native fp8 tensor cores + torchao rowwise scaling are supported on:
565
- - Hopper sm_90 (H100, H200) ← primary target here
566
- - Blackwell sm_100+ (RTX PRO 6000) ← also supported
567
- Ampere sm_80 (A100) and below do NOT have fp8 tensor cores at all.
568
- The old _is_blackwell() checked major >= 10 which silently returned
569
- False on H100/H200 (sm_90) β€” this is the fixed version.
570
  """
571
  if not torch.cuda.is_available():
572
  return False
@@ -581,14 +596,14 @@ def resolve_fp8(cfg: NovaConfig) -> bool:
581
  ok = HAS_TORCHAO and _has_fp8_support()
582
  if cfg.use_fp8 == "on" and not ok:
583
  log.warning(
584
- "[fp8] forced ON but torchao missing or GPU < sm_90 (Hopper) "
585
- "β€” bf16 fallback"
586
  )
587
  return False
588
  if cfg.use_fp8 == "auto" and is_main():
589
- dev = torch.cuda.current_device() if torch.cuda.is_available() else -1
590
  if torch.cuda.is_available():
591
- major, minor = torch.cuda.get_device_capability(dev)
 
 
592
  else:
593
  major, minor = 0, 0
594
  log.info(
@@ -600,8 +615,10 @@ def resolve_fp8(cfg: NovaConfig) -> bool:
600
  return ok
601
 
602
 
603
- _FP8_SKIP = ("embed", "lm_head", "norm", "diff_lambda", "head_gate",
604
- "ls_a", "ls_f", "rope")
 
 
605
 
606
 
607
  def _fp8_filter(module: nn.Module, fqn: str) -> bool:
@@ -633,19 +650,24 @@ def apply_fp8(model: nn.Module, cfg: NovaConfig) -> nn.Module:
633
  # ══════════════════════════════════════════════════════════════════════════════
634
 
635
  class ShardStream:
 
 
 
 
 
636
  def __init__(self, domain: str, refresh_every: int = 100_000):
637
  self.domain = domain
638
  self.refresh_every = refresh_every
639
  self._seen: List[str] = []
640
  self._queue: List[str] = []
641
- self._since_refresh = 0
642
  self._refresh()
643
 
644
  def _refresh(self):
645
  all_shards = list_shards(self.domain)
646
  new = [s for s in all_shards if s not in self._seen]
647
  if new and is_main():
648
- log.info(f"[data] {self.domain}: +{len(new)} new shards")
649
  random.shuffle(new)
650
  self._queue.extend(new)
651
  self._seen.extend(new)
@@ -687,10 +709,7 @@ class BalancedIterableDataset(IterableDataset):
687
  self.refresh_every = refresh_every
688
 
689
  def __iter__(self) -> Iterator[torch.Tensor]:
690
- streams = {
691
- d: iter(ShardStream(d, self.refresh_every))
692
- for d in self.domains
693
- }
694
  per_domain = max(1, self.buf_size // len(self.domains))
695
 
696
  while True:
@@ -755,11 +774,10 @@ def precompute_rope(head_dim: int, max_len: int, theta: float, device=None):
755
 
756
 
757
  def apply_rope(q, k, cos, sin):
758
- L = q.shape[1]
759
 
760
  cos_full = torch.cat([cos[:L], cos[:L]], dim=-1)
761
  sin_full = torch.cat([sin[:L], sin[:L]], dim=-1)
762
-
763
  cos_full = cos_full.unsqueeze(0).unsqueeze(2)
764
  sin_full = sin_full.unsqueeze(0).unsqueeze(2)
765
 
@@ -819,7 +837,6 @@ class NovaAttention(nn.Module):
819
 
820
  q = self.q_norm(q)
821
  k = self.k_norm(k)
822
-
823
  q, k = apply_rope(q, k, cos, sin)
824
 
825
  q = q.transpose(1, 2)
@@ -944,8 +961,8 @@ class Nova10BDense(nn.Module):
944
  def weight_checksum(self) -> float:
945
  """
946
  Cheap deterministic checksum over a sample of parameters.
947
- Used to verify all ranks built bit-identical models after
948
- seed_for_model_init β€” cheaper than a full sync_module_states broadcast.
949
  """
950
  with torch.no_grad():
951
  s = 0.0
@@ -960,8 +977,8 @@ class Nova10BDense(nn.Module):
960
 
961
  def build_model(cfg: NovaConfig) -> nn.Module:
962
  """
963
- Build on CPU β€” caller must have called seed_for_model_init(same_seed)
964
- on ALL ranks immediately before this, so every rank's random init is
965
  bit-identical β€” no FSDP broadcast needed afterward.
966
  """
967
  model = Nova10BDense(cfg).to(cfg.dtype)
@@ -1007,10 +1024,11 @@ def wrap_fsdp(model: nn.Module, cfg: NovaConfig, device: torch.device) -> FSDP:
1007
  cpu_offload = CPUOffload(offload_params=cfg.cpu_offload),
1008
  device_id = device,
1009
  use_orig_params = True,
1010
- # sync_module_states=False: correctness guaranteed by identical CPU
1011
- # seeding via seed_for_model_init() on all ranks. Verified cheaply
1012
- # by verify_model_sync() checksum all_reduce instead of a giant
1013
- # parameter broadcast.
 
1014
  sync_module_states = False,
1015
  )
1016
  if is_main():
@@ -1043,8 +1061,9 @@ def _newtonschulz5(G: torch.Tensor, steps: int = 5) -> torch.Tensor:
1043
 
1044
  class Muon(torch.optim.Optimizer):
1045
  def __init__(self, params, lr=0.02, momentum=0.95, weight_decay=0.0, ns_steps=5):
1046
- defaults = dict(lr=lr, momentum=momentum,
1047
- weight_decay=weight_decay, ns_steps=ns_steps)
 
1048
  super().__init__(list(params), defaults)
1049
 
1050
  @torch.no_grad()
@@ -1084,6 +1103,19 @@ class Muon(torch.optim.Optimizer):
1084
 
1085
 
1086
  def build_optimizers(model: nn.Module, cfg: NovaConfig):
 
 
 
 
 
 
 
 
 
 
 
 
 
1087
  muon_p = []
1088
  aw_decay = []
1089
  aw_nodecay = []
@@ -1093,7 +1125,9 @@ def build_optimizers(model: nn.Module, cfg: NovaConfig):
1093
  if id(p) in seen or not p.requires_grad:
1094
  continue
1095
  seen.add(id(p))
 
1096
  is_embed_or_head = ("embed" in name) or ("lm_head" in name)
 
1097
  if p.ndim == 2 and not is_embed_or_head:
1098
  muon_p.append(p)
1099
  elif p.ndim >= 2:
@@ -1101,8 +1135,26 @@ def build_optimizers(model: nn.Module, cfg: NovaConfig):
1101
  else:
1102
  aw_nodecay.append(p)
1103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1104
  muon_opt = Muon(
1105
- muon_p, lr=cfg.muon_lr, momentum=cfg.muon_momentum,
 
 
1106
  weight_decay=cfg.weight_decay,
1107
  )
1108
 
@@ -1166,10 +1218,12 @@ def save_ckpt(model, muon_opt, aw_opt, step, epoch, cfg, val_loss, tag) -> bool:
1166
  torch.save({k: v.contiguous() for k, v in sd.items()}, mpath)
1167
  torch.save(
1168
  {
1169
- "step": step, "epoch": epoch, "val_loss": val_loss,
1170
- "config": asdict(cfg),
1171
- "muon": muon_opt.state_dict(),
1172
- "adamw": aw_opt.state_dict(),
 
 
1173
  },
1174
  xpath,
1175
  )
@@ -1314,6 +1368,20 @@ def vram_str(dev: Optional[int] = None) -> str:
1314
 
1315
  # ══════════════════════════════════════════════════════════════════════════════
1316
  # TRAIN
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1317
  # ══════════════════════════════════════════════════════════════════════════════
1318
 
1319
  def train():
@@ -1378,12 +1446,10 @@ def train():
1378
 
1379
  with _stage("verify_model_sync"):
1380
  # Cheap correctness check replacing sync_module_states broadcast.
1381
- # Each rank computes a sampled parameter checksum, we all_reduce,
1382
- # and compare. If seed_for_model_init worked correctly every rank's
1383
- # checksum is numerically identical.
1384
  local_checksum = (
1385
- model.weight_checksum()
1386
- if hasattr(model, "weight_checksum") else 0.0
1387
  )
1388
  checksum_t = torch.tensor(
1389
  local_checksum, dtype=torch.float64, device=device
@@ -1402,10 +1468,17 @@ def train():
1402
  log.error(
1403
  f"[verify] model checksums DIVERGE across ranks: {vals} "
1404
  f"(max_diff={max_diff:.6f}) β€” ranks built DIFFERENT models! "
1405
- f"seed_for_model_init may not have produced identical init. "
1406
- f"Investigate before continuing β€” training would silently corrupt."
1407
  )
1408
 
 
 
 
 
 
 
 
 
1409
  with _stage("fsdp_wrap"):
1410
  model = wrap_fsdp(model, cfg, device)
1411
  enable_weight_tying(model)
@@ -1437,9 +1510,6 @@ def train():
1437
  with _stage("seed_runtime"):
1438
  seed_for_runtime(42 + rank(), device)
1439
 
1440
- with _stage("optimizer_build"):
1441
- muon_opt, aw_opt = build_optimizers(model, cfg)
1442
-
1443
  with _stage("checkpoint_resume"):
1444
  start_step, start_epoch, best_val = 0, 0, float("inf")
1445
  try:
@@ -1561,9 +1631,8 @@ def train():
1561
  accum_loss = accum_n = 0
1562
  if is_main():
1563
  log.error(
1564
- "[oom] step skipped β€” consider reducing micro_batch. "
1565
- "Note: 141GB HBM3e per H200 means this is unlikely "
1566
- "unless micro_batch is very large."
1567
  )
1568
  continue
1569
  except RuntimeError as e:
@@ -1611,8 +1680,9 @@ def train():
1611
  f" mlr {mlr:.2e} alr {alr:.2e}"
1612
  )
1613
  log.info(
1614
- f" {tps / 1e3:6.0f}K tok/s {1 / max(sps, 1e-9):.2f}s/step"
1615
- f" vram {vram_str()}"
 
1616
  )
1617
 
1618
  if step % cfg.eval_every == 0:
@@ -1624,7 +1694,7 @@ def train():
1624
  best_val = vl
1625
  save_ckpt(
1626
  model, muon_opt, aw_opt,
1627
- step, epoch, cfg, vl, tag="best"
1628
  )
1629
  log.info(" >> new best checkpoint saved")
1630
 
@@ -1632,14 +1702,14 @@ def train():
1632
  save_ckpt(
1633
  model, muon_opt, aw_opt,
1634
  step, epoch, cfg,
1635
- losses[-1] if losses else 0.0, tag="ckpt"
1636
  )
1637
 
1638
  if time.time() - t_last_ckpt > 1800:
1639
  save_ckpt(
1640
  model, muon_opt, aw_opt,
1641
  step, epoch, cfg,
1642
- losses[-1] if losses else 0.0, tag="latest"
1643
  )
1644
  t_last_ckpt = time.time()
1645
 
 
19
  O3 FSDP2 – PyTorch FullyShardedDataParallel
20
  O4 Cosine LR – separate schedules for Muon vs AdamW
21
  D9 Balanced streaming – equal tokens per domain, new shards auto-detected
22
+ D10 ctx-warmup – sequence-length curriculum
23
+
24
+ Fixes applied vs original:
25
+ FIX1 _has_fp8_support() checks major >= 9 (Hopper sm_90 = H100/H200)
26
+ old _is_blackwell() checked >= 10, silently disabled fp8 on H200
27
+ FIX2 build_optimizers() called BEFORE wrap_fsdp() on the raw CPU model
28
+ after FSDP wrap, named_parameters() walks flat-param structure and
29
+ the 2D-weight classifier sees nothing -> empty muon_p -> ValueError
30
+ FIX3 sync_module_states=False + identical CPU seeding replaces the giant
31
+ NCCL broadcast that crashed on both Blackwell and early Hopper runs
32
 
33
  Launch:
34
  torchrun --nproc_per_node=4 nova10b_dense.py
35
  torchrun --nproc_per_node=8 nova10b_dense.py
36
 
37
+ Debug:
38
  NOVA_DEBUG=1 torchrun --nproc_per_node=8 nova10b_dense.py
39
 
40
+ NCCL safe fallback:
41
  NOVA_NCCL_SAFE=1 torchrun --nproc_per_node=8 nova10b_dense.py
42
  """
43
 
 
125
  torch.backends.cudnn.allow_tf32 = True
126
  torch.backends.cudnn.benchmark = True
127
 
128
+ # ── logging ──────────────────────────────────────────────────────────────────
129
  class _RankFilter(logging.Filter):
130
  def filter(self, record):
131
  record.rank = dist.get_rank() if dist.is_initialized() else 0
 
183
  """
184
  random.seed(seed)
185
  np.random.seed(seed)
186
+ torch.manual_seed(seed) # CPU generator only β€” no CUDA touch
187
 
188
 
189
  def seed_for_runtime(seed: int, device: torch.device):
 
206
  def _stage(name: str):
207
  """
208
  Context manager: logs entry, syncs CUDA before AND after, barriers all
209
+ ranks, on exception logs rank/device/stage/traceback before re-raising.
210
  """
211
  class _StageCtx:
212
  def __enter__(self):
213
  self.t0 = time.time()
214
  dev = torch.cuda.current_device() if torch.cuda.is_available() else -1
215
+ log.info(
216
+ f"[stage:{name}] enter | rank={rank()} device=cuda:{dev} "
217
+ f"host={socket.gethostname()}"
218
+ )
219
  if torch.cuda.is_available():
220
  torch.cuda.synchronize()
221
  return self
 
227
  f"[stage:{name}] FAILED | rank={rank()} device=cuda:{dev} | "
228
  f"{exc_type.__name__}: {exc_val}"
229
  )
230
+ log.error(
231
+ "".join(traceback.format_exception(exc_type, exc_val, exc_tb))
232
+ )
233
  return False
 
234
  if torch.cuda.is_available():
235
  torch.cuda.synchronize()
236
  dt = time.time() - self.t0
 
246
  try:
247
  props = torch.cuda.get_device_properties(dev)
248
  cap_major, cap_minor = torch.cuda.get_device_capability(dev)
249
+ nccl_ver = (
250
+ torch.cuda.nccl.version() if hasattr(torch.cuda, "nccl") else "unknown"
251
+ )
252
+ drv = (
253
+ torch.cuda.driver_version()
254
+ if hasattr(torch.cuda, "driver_version") else "n/a"
255
+ )
256
  log.info(
257
  f"[env] rank={rank()} local_rank={local_rank} "
258
  f"gpu={props.name} sm_{cap_major}{cap_minor} "
 
268
 
269
  def log_p2p_matrix(local_rank: int, ws: int):
270
  """
271
+ Logs pairwise P2P access capability. H200 SXM nodes have NVLink +
272
+ NVSwitch so expect all-Y. Any N on H200 is a red flag β€” you are not
273
+ getting the fabric you are paying for, escalate to infra.
 
 
 
274
  """
275
  if not is_main() or not torch.cuda.is_available():
276
  return
 
286
  can = torch.cuda.can_device_access_peer(i, j)
287
  row.append("Y" if can else "N")
288
  rows.append(" ".join(row))
289
+ log.info("[p2p] pairwise CUDA P2P access matrix (expect all-Y on H200 NVLink):")
 
290
  for i, r in enumerate(rows):
291
  log.info(f"[p2p] gpu{i}: {r}")
292
  except Exception as e:
 
304
  p = m @ n
305
  torch.cuda.synchronize(device)
306
  checksum = float(z.sum().item()) + float(p.sum().item())
307
+ log.info(
308
+ f"[health] rank={rank()} device={device} basic op OK "
309
+ f"(checksum={checksum:.1f})"
310
+ )
311
  except Exception as e:
312
+ log.error(
313
+ f"[health] rank={rank()} device={device} FAILED basic CUDA op: {e}"
314
+ )
315
  raise
316
 
317
 
318
  def nccl_collective_selftest(device: torch.device, ws: int):
319
  """
320
+ Isolated NCCL collective stress test run BEFORE FSDP wrap or any model
321
+ code. Tests the exact same primitive classes (all_gather, broadcast) that
322
+ FSDP uses internally.
323
 
324
+ If this fails -> fault is environmental (NCCL/driver), not model code.
325
+ If this passes -> fault is specific to FSDP/model collective patterns.
326
 
327
+ On H200 SXM with NVLink this should pass all sizes quickly.
328
  """
329
  sizes_mb = [1, 16, 64, 256, 1024, 4096]
330
  for mb in sizes_mb:
 
334
  (n_elem,), float(rank()), device=device, dtype=torch.bfloat16
335
  )
336
 
 
337
  gathered = [torch.zeros_like(local) for _ in range(ws)]
338
  dist.all_gather(gathered, local)
339
  torch.cuda.synchronize(device)
340
 
 
341
  bcast = local.clone()
342
  dist.broadcast(bcast, src=0)
343
  torch.cuda.synchronize(device)
 
367
  if is_main():
368
  log.info(
369
  f"[nccl_selftest] ALL sizes up to {sizes_mb[-1]}MB passed "
370
+ f"on all {ws} ranks β€” NCCL/NVLink healthy βœ“"
371
  )
372
 
373
 
 
519
 
520
  use_fp8: str = "auto"
521
 
522
+ lr: float = 2e-4
523
+ min_lr: float = 2e-5
524
+ muon_lr: float = 6e-3
525
+ muon_min_lr: float = 6e-4
526
+ weight_decay: float = 0.1
527
+ beta1: float = 0.9
528
+ beta2: float = 0.95
529
+ muon_momentum: float = 0.95
530
+ grad_clip: float = 1.0
531
+ warmup_steps: int = 4_000
532
 
533
  total_steps: int = 130_000
534
  ctx_warmup_steps: int = 3_000
535
  ctx_start_len: int = 2048
536
  ctx_end_len: int = 2048
537
 
538
+ micro_batch: int = 6
539
+ grad_accum: int = 8
540
+ log_every: int = 10
541
+ ckpt_every: int = 1_000
542
+ eval_every: int = 500
543
+ ckpt_keep: int = 2
544
+ seed: int = 42
545
+ max_hours: float = 47.5
546
 
547
  domains: List[str] = field(default_factory=lambda: list(DOMAINS))
548
  shard_refresh_rows: int = 100_000
 
565
  def current_ctx_len(self, step: int) -> int:
566
  if self.ctx_warmup_steps <= 0 or step >= self.ctx_warmup_steps:
567
  return self.ctx_end_len
568
+ t = step / self.ctx_warmup_steps
569
  raw = int(self.ctx_start_len + t * (self.ctx_end_len - self.ctx_start_len))
570
  return ((raw + 63) // 64) * 64
571
 
572
 
573
  # ══════════════════════════════════════════════════════════════════════════════
574
+ # FP8 HELPERS
575
+ # FIX1: _has_fp8_support() checks major >= 9 so H100/H200 (sm_90) get fp8.
576
+ # Old _is_blackwell() checked >= 10, silently killed fp8 on Hopper.
577
  # ══════════════════════════════════════════════════════════════════════════════
578
 
579
  def _has_fp8_support() -> bool:
580
  """
581
+ FP8 tensor cores + torchao rowwise scaling are available on:
582
+ Hopper sm_90 (H100, H200) <- primary target
583
+ Blackwell sm_100+ (RTX PRO 6000) <- also supported
584
+ Ampere sm_80 (A100) and below: NO fp8 tensor cores.
 
 
585
  """
586
  if not torch.cuda.is_available():
587
  return False
 
596
  ok = HAS_TORCHAO and _has_fp8_support()
597
  if cfg.use_fp8 == "on" and not ok:
598
  log.warning(
599
+ "[fp8] forced ON but torchao missing or GPU < sm_90 β€” bf16 fallback"
 
600
  )
601
  return False
602
  if cfg.use_fp8 == "auto" and is_main():
 
603
  if torch.cuda.is_available():
604
+ major, minor = torch.cuda.get_device_capability(
605
+ torch.cuda.current_device()
606
+ )
607
  else:
608
  major, minor = 0, 0
609
  log.info(
 
615
  return ok
616
 
617
 
618
+ _FP8_SKIP = (
619
+ "embed", "lm_head", "norm", "diff_lambda",
620
+ "head_gate", "ls_a", "ls_f", "rope",
621
+ )
622
 
623
 
624
  def _fp8_filter(module: nn.Module, fqn: str) -> bool:
 
650
  # ══════════════════════════════════════════════════════════════════════════════
651
 
652
  class ShardStream:
653
+ """
654
+ Per-domain infinite shard iterator. Periodically re-queries HF for new
655
+ shards so training automatically picks up newly uploaded data files
656
+ without restart β€” general and code shards trickling in are handled.
657
+ """
658
  def __init__(self, domain: str, refresh_every: int = 100_000):
659
  self.domain = domain
660
  self.refresh_every = refresh_every
661
  self._seen: List[str] = []
662
  self._queue: List[str] = []
663
+ self._since_refresh = 0
664
  self._refresh()
665
 
666
  def _refresh(self):
667
  all_shards = list_shards(self.domain)
668
  new = [s for s in all_shards if s not in self._seen]
669
  if new and is_main():
670
+ log.info(f"[data] {self.domain}: +{len(new)} new shards discovered")
671
  random.shuffle(new)
672
  self._queue.extend(new)
673
  self._seen.extend(new)
 
709
  self.refresh_every = refresh_every
710
 
711
  def __iter__(self) -> Iterator[torch.Tensor]:
712
+ streams = {d: iter(ShardStream(d, self.refresh_every)) for d in self.domains}
 
 
 
713
  per_domain = max(1, self.buf_size // len(self.domains))
714
 
715
  while True:
 
774
 
775
 
776
  def apply_rope(q, k, cos, sin):
777
+ L = q.shape[1]
778
 
779
  cos_full = torch.cat([cos[:L], cos[:L]], dim=-1)
780
  sin_full = torch.cat([sin[:L], sin[:L]], dim=-1)
 
781
  cos_full = cos_full.unsqueeze(0).unsqueeze(2)
782
  sin_full = sin_full.unsqueeze(0).unsqueeze(2)
783
 
 
837
 
838
  q = self.q_norm(q)
839
  k = self.k_norm(k)
 
840
  q, k = apply_rope(q, k, cos, sin)
841
 
842
  q = q.transpose(1, 2)
 
961
  def weight_checksum(self) -> float:
962
  """
963
  Cheap deterministic checksum over a sample of parameters.
964
+ Verifies all ranks built bit-identical models after seed_for_model_init
965
+ without needing a full sync_module_states broadcast.
966
  """
967
  with torch.no_grad():
968
  s = 0.0
 
977
 
978
  def build_model(cfg: NovaConfig) -> nn.Module:
979
  """
980
+ Build on CPU. Caller must have called seed_for_model_init(same_seed) on
981
+ ALL ranks immediately before this so every rank's random init is
982
  bit-identical β€” no FSDP broadcast needed afterward.
983
  """
984
  model = Nova10BDense(cfg).to(cfg.dtype)
 
1024
  cpu_offload = CPUOffload(offload_params=cfg.cpu_offload),
1025
  device_id = device,
1026
  use_orig_params = True,
1027
+ # FIX3: sync_module_states=False β€” correctness guaranteed by identical
1028
+ # CPU seeding in seed_for_model_init(). Verified cheaply via
1029
+ # verify_model_sync() checksum all_reduce. The old True value caused
1030
+ # a giant NCCL broadcast at construction time that crashed on both
1031
+ # Blackwell and early Hopper runs.
1032
  sync_module_states = False,
1033
  )
1034
  if is_main():
 
1061
 
1062
  class Muon(torch.optim.Optimizer):
1063
  def __init__(self, params, lr=0.02, momentum=0.95, weight_decay=0.0, ns_steps=5):
1064
+ defaults = dict(
1065
+ lr=lr, momentum=momentum, weight_decay=weight_decay, ns_steps=ns_steps
1066
+ )
1067
  super().__init__(list(params), defaults)
1068
 
1069
  @torch.no_grad()
 
1103
 
1104
 
1105
  def build_optimizers(model: nn.Module, cfg: NovaConfig):
1106
+ """
1107
+ FIX2: MUST be called on the raw unwrapped CPU model BEFORE wrap_fsdp().
1108
+
1109
+ After FSDP wraps the model, named_parameters() walks FSDP's internal
1110
+ flat-param structure. Parameter names look different and the 2D-weight
1111
+ classification logic can see an empty set, producing:
1112
+ ValueError: optimizer got an empty parameter list
1113
+
1114
+ With use_orig_params=True FSDP keeps references to the original param
1115
+ objects alive, so optimizer references built pre-wrap remain valid and
1116
+ correct after wrapping. Always call: build_optimizers() -> wrap_fsdp(),
1117
+ never the other way around.
1118
+ """
1119
  muon_p = []
1120
  aw_decay = []
1121
  aw_nodecay = []
 
1125
  if id(p) in seen or not p.requires_grad:
1126
  continue
1127
  seen.add(id(p))
1128
+
1129
  is_embed_or_head = ("embed" in name) or ("lm_head" in name)
1130
+
1131
  if p.ndim == 2 and not is_embed_or_head:
1132
  muon_p.append(p)
1133
  elif p.ndim >= 2:
 
1135
  else:
1136
  aw_nodecay.append(p)
1137
 
1138
+ if is_main():
1139
+ log.info(
1140
+ f"[optim] param classification: "
1141
+ f"muon={len(muon_p)} tensors | "
1142
+ f"aw_decay={len(aw_decay)} | "
1143
+ f"aw_nodecay={len(aw_nodecay)}"
1144
+ )
1145
+
1146
+ # Hard guard β€” if this fires, call order is wrong
1147
+ if len(muon_p) == 0:
1148
+ raise RuntimeError(
1149
+ "[optim] muon_p is empty β€” no 2D non-embed/lm_head params found.\n"
1150
+ "build_optimizers() MUST be called on the unwrapped model BEFORE "
1151
+ "wrap_fsdp(). Check call order in train()."
1152
+ )
1153
+
1154
  muon_opt = Muon(
1155
+ muon_p,
1156
+ lr=cfg.muon_lr,
1157
+ momentum=cfg.muon_momentum,
1158
  weight_decay=cfg.weight_decay,
1159
  )
1160
 
 
1218
  torch.save({k: v.contiguous() for k, v in sd.items()}, mpath)
1219
  torch.save(
1220
  {
1221
+ "step": step,
1222
+ "epoch": epoch,
1223
+ "val_loss": val_loss,
1224
+ "config": asdict(cfg),
1225
+ "muon": muon_opt.state_dict(),
1226
+ "adamw": aw_opt.state_dict(),
1227
  },
1228
  xpath,
1229
  )
 
1368
 
1369
  # ══════════════════════════════════════════════════════════════════════════════
1370
  # TRAIN
1371
+ #
1372
+ # Stage order (IMPORTANT β€” do not reorder without reading FIX2 comment):
1373
+ # seed_model_init
1374
+ # config_build
1375
+ # vocab_metadata
1376
+ # model_build_cpu
1377
+ # verify_model_sync
1378
+ # optimizer_build <- BEFORE fsdp_wrap (FIX2)
1379
+ # fsdp_wrap <- AFTER optimizer_build (FIX2)
1380
+ # compile
1381
+ # seed_runtime
1382
+ # checkpoint_resume
1383
+ # data_loaders
1384
+ # first_step_smoke_test
1385
  # ══════════════════════════════════════════════════════════════════════════════
1386
 
1387
  def train():
 
1446
 
1447
  with _stage("verify_model_sync"):
1448
  # Cheap correctness check replacing sync_module_states broadcast.
1449
+ # Sampled param checksum all_reduce β€” if all ranks seeded identically
1450
+ # every checksum is bit-identical.
 
1451
  local_checksum = (
1452
+ model.weight_checksum() if hasattr(model, "weight_checksum") else 0.0
 
1453
  )
1454
  checksum_t = torch.tensor(
1455
  local_checksum, dtype=torch.float64, device=device
 
1468
  log.error(
1469
  f"[verify] model checksums DIVERGE across ranks: {vals} "
1470
  f"(max_diff={max_diff:.6f}) β€” ranks built DIFFERENT models! "
1471
+ f"Investigate seed_for_model_init before continuing."
 
1472
  )
1473
 
1474
+ # ── FIX2: optimizer built HERE on raw CPU model, BEFORE fsdp_wrap ────────
1475
+ # After wrap_fsdp(), named_parameters() walks FSDP's flat-param structure
1476
+ # and the 2D-weight classifier finds nothing -> empty muon_p -> ValueError.
1477
+ # With use_orig_params=True the optimizer's param references stay valid
1478
+ # after wrapping, so this ordering is both correct and safe.
1479
+ with _stage("optimizer_build"):
1480
+ muon_opt, aw_opt = build_optimizers(model, cfg)
1481
+
1482
  with _stage("fsdp_wrap"):
1483
  model = wrap_fsdp(model, cfg, device)
1484
  enable_weight_tying(model)
 
1510
  with _stage("seed_runtime"):
1511
  seed_for_runtime(42 + rank(), device)
1512
 
 
 
 
1513
  with _stage("checkpoint_resume"):
1514
  start_step, start_epoch, best_val = 0, 0, float("inf")
1515
  try:
 
1631
  accum_loss = accum_n = 0
1632
  if is_main():
1633
  log.error(
1634
+ "[oom] step skipped β€” reduce micro_batch. "
1635
+ "(Unlikely on 141GB H200 HBM3e unless micro_batch is huge.)"
 
1636
  )
1637
  continue
1638
  except RuntimeError as e:
 
1680
  f" mlr {mlr:.2e} alr {alr:.2e}"
1681
  )
1682
  log.info(
1683
+ f" {tps / 1e3:6.0f}K tok/s "
1684
+ f"{1 / max(sps, 1e-9):.2f}s/step "
1685
+ f"vram {vram_str()}"
1686
  )
1687
 
1688
  if step % cfg.eval_every == 0:
 
1694
  best_val = vl
1695
  save_ckpt(
1696
  model, muon_opt, aw_opt,
1697
+ step, epoch, cfg, vl, tag="best",
1698
  )
1699
  log.info(" >> new best checkpoint saved")
1700
 
 
1702
  save_ckpt(
1703
  model, muon_opt, aw_opt,
1704
  step, epoch, cfg,
1705
+ losses[-1] if losses else 0.0, tag="ckpt",
1706
  )
1707
 
1708
  if time.time() - t_last_ckpt > 1800:
1709
  save_ckpt(
1710
  model, muon_opt, aw_opt,
1711
  step, epoch, cfg,
1712
+ losses[-1] if losses else 0.0, tag="latest",
1713
  )
1714
  t_last_ckpt = time.time()
1715