Bc-AI commited on
Commit
fba043f
·
verified ·
1 Parent(s): 97440ba

Update nova10b_dense.py

Browse files
Files changed (1) hide show
  1. nova10b_dense.py +303 -334
nova10b_dense.py CHANGED
@@ -27,6 +27,10 @@ Launch:
27
 
28
  Debug launch (verbose CUDA/NCCL diagnostics, slower):
29
  NOVA_DEBUG=1 torchrun --nproc_per_node=8 nova10b_dense.py
 
 
 
 
30
  """
31
 
32
  # ══════════════════════════════════════════════════════════════════════════════
@@ -34,19 +38,15 @@ Debug launch (verbose CUDA/NCCL diagnostics, slower):
34
  # ══════════════════════════════════════════════════════════════════════════════
35
  import os
36
 
37
- # FIX: Pin deterministic PCI-bus-order device enumeration.
38
- # Default CUDA_DEVICE_ORDER is FASTEST_FIRST, which requires the CUDA
39
- # runtime to benchmark/query every visible GPU's capability on first CUDA
40
- # call. With N processes (torchrun) starting concurrently, this becomes a
41
- # multi-process race across ALL device contexts simultaneously — a known
42
- # trigger class for illegal memory access on immature driver stacks
43
- # (e.g. new sm_100 Blackwell workstation cards). PCI_BUS_ID ordering is
44
- # deterministic, matches nvidia-smi numbering exactly, and requires no
45
- # cross-device benchmarking at all.
46
  os.environ.setdefault("CUDA_DEVICE_ORDER", "PCI_BUS_ID")
47
 
48
- # Debug toggles — controlled via env so you can flip them on/off per launch
49
- # without editing code. Set NOVA_DEBUG=1 in the job env to enable.
50
  _DEBUG = os.environ.get("NOVA_DEBUG", "0") == "1"
51
  if _DEBUG:
52
  os.environ.setdefault("CUDA_LAUNCH_BLOCKING", "1")
@@ -55,6 +55,17 @@ if _DEBUG:
55
  os.environ.setdefault("NCCL_DEBUG", "INFO")
56
  os.environ.setdefault("NCCL_DEBUG_SUBSYS", "INIT,COLL")
57
 
 
 
 
 
 
 
 
 
 
 
 
58
  # ── stdlib ──────────────────────────────────────────────────────────────────
59
  import gc
60
  import re
@@ -71,7 +82,6 @@ from dataclasses import dataclass, asdict, field
71
  from typing import Optional, List, Iterator
72
 
73
  # ── third-party ─────────────────────────────────────────────────────────────
74
- # NOTE: torch import happens AFTER CUDA_DEVICE_ORDER is set above.
75
  import numpy as np
76
  import torch
77
  import torch.nn as nn
@@ -119,8 +129,7 @@ torch.backends.cuda.matmul.allow_tf32 = True
119
  torch.backends.cudnn.allow_tf32 = True
120
  torch.backends.cudnn.benchmark = True
121
 
122
- # ── logging — filter must be on handlers, not just the logger,
123
- # so %(rank)s is available from the very first message ────────────────────
124
  class _RankFilter(logging.Filter):
125
  def filter(self, record):
126
  record.rank = dist.get_rank() if dist.is_initialized() else 0
@@ -161,35 +170,49 @@ def barrier():
161
  dist.barrier()
162
 
163
  def all_reduce_mean(tensor: torch.Tensor) -> torch.Tensor:
164
- """All-reduce mean — uses SUM + div for universal NCCL compatibility."""
165
  dist.all_reduce(tensor, op=dist.ReduceOp.SUM)
166
  tensor.div_(world_size())
167
  return tensor
168
 
169
 
170
  # ══════════════════════════════════════════════════════════════════════════════
171
- # SEEDING — device-scoped, not cross-device
172
  # ══════════════════════════════════════════════════════════════════════════════
173
 
174
- def seed_everything(seed: int, device: torch.device):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  """
176
- Seeds CPU RNG and ONLY this process's assigned CUDA device.
177
-
178
- torch.manual_seed(seed) internally calls torch.cuda.manual_seed_all(seed),
179
- which loops over EVERY visible CUDA device and touches its RNG state —
180
- from EVERY process. On an N-GPU node with N ranks, that's N*N concurrent
181
- cross-device context touches happening right at startup, immediately
182
- after dist.init_process_group(). On mature driver stacks this is usually
183
- harmless; on immature sm_100 Blackwell driver builds, concurrent
184
- multi-process context manipulation across devices is a known class of
185
- trigger for illegal memory access at the context/event layer.
186
-
187
- Fix: seed CPU normally, but seed CUDA RNG state only for the device
188
- this process actually owns.
189
  """
190
  random.seed(seed)
191
  np.random.seed(seed)
192
- torch.manual_seed(seed) # CPU generator only — safe, no CUDA touch
193
  if torch.cuda.is_available():
194
  with torch.cuda.device(device):
195
  torch.cuda.manual_seed(seed)
@@ -201,17 +224,10 @@ def seed_everything(seed: int, device: torch.device):
201
 
202
  def _stage(name: str):
203
  """
204
- Context manager that:
205
- 1. Logs stage entry with rank/device info
206
- 2. Forces CUDA synchronize BEFORE the stage (flushes any latent async
207
- error from the previous stage so it's attributed correctly)
208
- 3. Runs the stage
209
- 4. Forces CUDA synchronize AFTER (so THIS stage's errors surface here,
210
- not silently deferred to the next unrelated CUDA call)
211
- 5. Barriers all ranks so a hang or crash on one rank is visible
212
- immediately rather than other ranks racing ahead
213
- 6. On exception: logs rank + device + stage + full traceback BEFORE
214
- re-raising, so the crash report is unambiguous
215
  """
216
  class _StageCtx:
217
  def __enter__(self):
@@ -231,7 +247,7 @@ def _stage(name: str):
231
  f"{exc_type.__name__}: {exc_val}"
232
  )
233
  log.error("".join(traceback.format_exception(exc_type, exc_val, exc_tb)))
234
- return False # re-raise
235
 
236
  if torch.cuda.is_available():
237
  torch.cuda.synchronize()
@@ -244,12 +260,6 @@ def _stage(name: str):
244
 
245
 
246
  def log_cuda_environment(local_rank: int):
247
- """
248
- Dumps everything relevant to diagnosing driver/CUDA/NCCL version
249
- mismatches, called once per rank at startup. Cheap, always-on —
250
- this is the log output you want available BEFORE a crash, not
251
- reconstructed after the fact.
252
- """
253
  dev = local_rank
254
  try:
255
  props = torch.cuda.get_device_properties(dev)
@@ -262,22 +272,45 @@ def log_cuda_environment(local_rank: int):
262
  f"mem={props.total_memory/1024**3:.0f}GB "
263
  f"torch={torch.__version__} cuda={torch.version.cuda} "
264
  f"nccl={nccl_ver} driver={drv} "
265
- f"device_order={os.environ.get('CUDA_DEVICE_ORDER','default')}"
 
266
  )
267
  except Exception as e:
268
  log.warning(f"[env] rank={rank()} could not fully query CUDA env: {e}")
269
 
270
 
271
- def cuda_health_check(device: torch.device):
272
  """
273
- Minimal, cheap CUDA sanity check run on EVERY rank immediately after
274
- set_device(), before any real model/data work. Isolates whether a
275
- fault originates from basic context/kernel execution on this specific
276
- device, vs. later in FSDP/model/data code.
277
-
278
- If this fails, the problem is proven to be at the raw CUDA context
279
- level for this specific rank/device — not your training logic.
280
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  try:
282
  x = torch.ones(4096, device=device, dtype=torch.bfloat16)
283
  y = torch.ones(4096, device=device, dtype=torch.bfloat16)
@@ -294,6 +327,65 @@ def cuda_health_check(device: torch.device):
294
  raise
295
 
296
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  # ══════════════════════════════════════════════════════════════════════════════
298
  # HF HUB
299
  # ══════════════════════════════════════════════════════════════════════════════
@@ -389,7 +481,6 @@ def list_shards(domain: str) -> List[str]:
389
 
390
 
391
  def _ckpt_step(fname: str) -> int:
392
- """Extract step number from checkpoint filename robustly."""
393
  m = re.search(r"_s(\d+)\.", fname)
394
  return int(m.group(1)) if m else 0
395
 
@@ -429,7 +520,6 @@ def rotate_ckpts(tag: str, keep: int = 2):
429
 
430
  @dataclass
431
  class NovaConfig:
432
- # ── model ──────────────────────────────────────────────────────────────
433
  vocab_size: int = 65536
434
  d_model: int = 4096
435
  n_heads: int = 32
@@ -441,10 +531,8 @@ class NovaConfig:
441
  logit_softcap: float = 50.0
442
  rope_theta: float = 500_000.0
443
 
444
- # ── FP8 ────────────────────────────────────────────────────────────────
445
- use_fp8: str = "auto" # "auto" | "on" | "off"
446
 
447
- # ── optimiser ──────────────────────────────────────────────────────────
448
  lr: float = 2e-4
449
  min_lr: float = 2e-5
450
  muon_lr: float = 6e-3
@@ -456,13 +544,11 @@ class NovaConfig:
456
  grad_clip: float = 1.0
457
  warmup_steps: int = 4_000
458
 
459
- # ── schedule ───────────────────────────────────────────────────────────
460
  total_steps: int = 130_000
461
  ctx_warmup_steps: int = 3_000
462
  ctx_start_len: int = 2048
463
  ctx_end_len: int = 2048
464
 
465
- # ── training ───────────────────────────────────────────────────────────
466
  micro_batch: int = 6
467
  grad_accum: int = 8
468
  log_every: int = 10
@@ -472,15 +558,11 @@ class NovaConfig:
472
  seed: int = 42
473
  max_hours: float = 47.5
474
 
475
- # ── data ───────────────────────────────────────────────────────────────
476
  domains: List[str] = field(default_factory=lambda: list(DOMAINS))
477
  shard_refresh_rows: int = 100_000
478
  data_buf_size: int = 16_384
479
 
480
- # ── FSDP ───────────────────────────────────────────────────────────────
481
  cpu_offload: bool = False
482
-
483
- # ── torch.compile ──────────────────────────────────────────────────────
484
  use_compile: bool = True
485
 
486
  @property
@@ -508,12 +590,6 @@ class NovaConfig:
508
  def _is_blackwell() -> bool:
509
  if not torch.cuda.is_available():
510
  return False
511
- # FIX: query THIS process's assigned device, never a hardcoded index.
512
- # Previously `get_device_capability(0)` meant every rank — regardless of
513
- # which physical GPU it owned — queried device 0's capability. On an
514
- # 8-process launch this meant 8 concurrent cross-device context touches
515
- # to device 0 alone, right at startup. Using current_device() ensures
516
- # each rank only ever touches the device it actually owns.
517
  dev = torch.cuda.current_device()
518
  major, _ = torch.cuda.get_device_capability(dev)
519
  return major >= 10
@@ -534,7 +610,6 @@ def resolve_fp8(cfg: NovaConfig) -> bool:
534
  return ok
535
 
536
 
537
- # Layers to keep in bf16: embeddings, lm_head, norms, diff scalars, gates
538
  _FP8_SKIP = ("embed", "lm_head", "norm", "diff_lambda", "head_gate",
539
  "ls_a", "ls_f", "rope")
540
 
@@ -547,10 +622,6 @@ def _fp8_filter(module: nn.Module, fqn: str) -> bool:
547
 
548
 
549
  def apply_fp8(model: nn.Module, cfg: NovaConfig) -> nn.Module:
550
- """
551
- Apply FP8 training to eligible Linear layers.
552
- Must be called BEFORE weight tying so embed and lm_head stay bf16.
553
- """
554
  if not resolve_fp8(cfg):
555
  return model
556
  fp8_cfg = Float8LinearConfig.from_recipe_name("rowwise")
@@ -564,17 +635,10 @@ def apply_fp8(model: nn.Module, cfg: NovaConfig) -> nn.Module:
564
  return model
565
 
566
  # ═══════════════════════════════════════════════���══════════════════════════════
567
- # DATA — streaming domain-balanced loader (IterableDataset, worker-safe)
568
  # ══════════════════════════════════════════════════════════════════════════════
569
 
570
  class ShardStream:
571
- """
572
- Per-domain streaming iterator.
573
- Each DataLoader worker constructs its own ShardStream, so there is no
574
- shared state and no race conditions between workers.
575
- Re-scans the Hub every `refresh_every` rows to pick up new shards.
576
- """
577
-
578
  def __init__(self, domain: str, refresh_every: int = 100_000):
579
  self.domain = domain
580
  self.refresh_every = refresh_every
@@ -615,12 +679,6 @@ class ShardStream:
615
 
616
 
617
  class BalancedIterableDataset(IterableDataset):
618
- """
619
- Each DataLoader worker gets its own independent ShardStream per domain.
620
- No shared mutable state — fully safe with num_workers > 0.
621
- Yields equal numbers of tokens from each domain per buffer cycle.
622
- """
623
-
624
  def __init__(
625
  self,
626
  domains: List[str],
@@ -671,12 +729,8 @@ def make_loaders(cfg: NovaConfig, local_rank: int, ws: int):
671
  val_ds = BalancedIterableDataset(
672
  cfg.domains, cfg.max_len, cfg.data_buf_size // 4, cfg.shard_refresh_rows
673
  )
674
- train_loader = DataLoader(
675
- train_ds, batch_size=cfg.micro_batch, **kw
676
- )
677
- val_loader = DataLoader(
678
- val_ds, batch_size=min(cfg.micro_batch, 4), **kw
679
- )
680
  return train_loader, val_loader
681
 
682
  # ══════════════════════════════════════════════════════════════════════════════
@@ -693,16 +747,7 @@ class RMSNorm(nn.Module):
693
  return F.rms_norm(x, self.scale.shape, self.scale, self.eps)
694
 
695
 
696
- def precompute_rope(
697
- head_dim: int,
698
- max_len: int,
699
- theta: float,
700
- device=None,
701
- ):
702
- """
703
- Extended-theta RoPE.
704
- Returns (cos, sin) each of shape (max_len, head_dim // 2).
705
- """
706
  inv_freq = 1.0 / (
707
  theta ** (
708
  torch.arange(0, head_dim, 2, dtype=torch.float32, device=device)
@@ -714,15 +759,7 @@ def precompute_rope(
714
  return torch.cos(freqs), torch.sin(freqs)
715
 
716
 
717
- def apply_rope(
718
- q: torch.Tensor,
719
- k: torch.Tensor,
720
- cos: torch.Tensor,
721
- sin: torch.Tensor,
722
- ) -> tuple[torch.Tensor, torch.Tensor]:
723
- """
724
- Standard paired RoPE rotation. q, k: (B, L, n_heads, head_dim)
725
- """
726
  L = q.shape[1]
727
  hd = q.shape[-1]
728
 
@@ -732,7 +769,7 @@ def apply_rope(
732
  cos_full = cos_full.unsqueeze(0).unsqueeze(2)
733
  sin_full = sin_full.unsqueeze(0).unsqueeze(2)
734
 
735
- def _rotate_half(x: torch.Tensor) -> torch.Tensor:
736
  half = x.shape[-1] // 2
737
  x1, x2 = x[..., :half], x[..., half:]
738
  return torch.cat([-x2, x1], dim=-1)
@@ -744,8 +781,6 @@ def apply_rope(
744
  return q_rot, k_rot
745
 
746
 
747
- # ── D1 + D2: Fused QKV + Differential Attention ─────────────────────────────
748
-
749
  class NovaAttention(nn.Module):
750
  def __init__(self, cfg: NovaConfig):
751
  super().__init__()
@@ -776,12 +811,7 @@ class NovaAttention(nn.Module):
776
  nn.init.normal_(self.qkv.weight, std=std)
777
  nn.init.normal_(self.o_proj.weight, std=dstd)
778
 
779
- def forward(
780
- self,
781
- x: torch.Tensor,
782
- cos: torch.Tensor,
783
- sin: torch.Tensor,
784
- ) -> torch.Tensor:
785
  B, L, D = x.shape
786
 
787
  qkv = self.qkv(x)
@@ -819,8 +849,6 @@ class NovaAttention(nn.Module):
819
  return self.o_proj(out)
820
 
821
 
822
- # ── D3: Dense SwiGLU FFN ─────────────────────────────────────────────────────
823
-
824
  class SwiGLU(nn.Module):
825
  def __init__(self, cfg: NovaConfig):
826
  super().__init__()
@@ -835,12 +863,10 @@ class SwiGLU(nn.Module):
835
  nn.init.normal_(self.up.weight, std=std)
836
  nn.init.normal_(self.down.weight, std=dstd)
837
 
838
- def forward(self, x: torch.Tensor) -> torch.Tensor:
839
  return self.down(F.silu(self.gate(x)) * self.up(x))
840
 
841
 
842
- # ── D4 + D5: Nova Transformer Block ──────────────────────────────────────────
843
-
844
  class NovaBlock(nn.Module):
845
  def __init__(self, cfg: NovaConfig):
846
  super().__init__()
@@ -851,19 +877,12 @@ class NovaBlock(nn.Module):
851
  self.ls_a = nn.Parameter(torch.full((cfg.d_model,), 0.1))
852
  self.ls_f = nn.Parameter(torch.full((cfg.d_model,), 0.1))
853
 
854
- def forward(
855
- self,
856
- x: torch.Tensor,
857
- cos: torch.Tensor,
858
- sin: torch.Tensor,
859
- ) -> torch.Tensor:
860
  x = x + self.ls_a * self.attn(self.attn_norm(x), cos, sin)
861
  x = x + self.ls_f * self.ffn(self.ffn_norm(x))
862
  return x
863
 
864
 
865
- # ── Full Model ────────────────────────────────────────────────────────────────
866
-
867
  class Nova10BDense(nn.Module):
868
  def __init__(self, cfg: NovaConfig):
869
  super().__init__()
@@ -872,22 +891,10 @@ class Nova10BDense(nn.Module):
872
  self.embed = nn.Embedding(cfg.vocab_size, cfg.d_model)
873
  self.layers = nn.ModuleList([NovaBlock(cfg) for _ in range(cfg.n_layers)])
874
  self.norm = RMSNorm(cfg.d_model)
875
-
876
- # FIX: lm_head keeps its own independent nn.Parameter. We do NOT
877
- # alias lm_head.weight = embed.weight here. Sharing a single
878
- # Parameter object across two different FSDP-wrapped submodules
879
- # (embed lives at the top level, lm_head lives at the top level too,
880
- # but under FULL_SHARD auto-wrap the flat-param bookkeeping for a
881
- # Parameter referenced from two registration points can produce
882
- # inconsistent shard metadata) — this was implicated in an
883
- # illegal-memory-access crash during FSDP's all_gather unshard.
884
- # Instead we tie via a runtime flag in forward() AFTER FSDP wrap
885
- # (see enable_weight_tying() below), so FSDP only ever sees each
886
- # Parameter object registered exactly once.
887
  self.lm_head = nn.Linear(cfg.d_model, cfg.vocab_size, bias=False)
888
 
889
  self.softcap = cfg.logit_softcap
890
- self._use_tied_lm_head = False # flipped True post-FSDP-wrap
891
 
892
  nn.init.normal_(self.embed.weight, std=0.02)
893
  nn.init.normal_(self.lm_head.weight, std=0.02)
@@ -905,11 +912,7 @@ class Nova10BDense(nn.Module):
905
  f"ffn_h={cfg.ffn_hidden} | max_len={cfg.max_len}"
906
  )
907
 
908
- def forward(
909
- self,
910
- ids: torch.Tensor,
911
- targets: Optional[torch.Tensor] = None,
912
- ):
913
  B, L = ids.shape
914
  x = self.embed(ids)
915
  cos = self.rope_cos[:L]
@@ -945,14 +948,34 @@ class Nova10BDense(nn.Module):
945
  n += p.numel()
946
  return n
947
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
948
 
949
  def build_model(cfg: NovaConfig) -> nn.Module:
950
  """
951
  Build order:
952
- 1. Create model on CPU — NO weight tying yet
 
 
 
 
953
  2. Apply FP8 (replaces eligible nn.Linear layers in-place)
954
  3. Return model — FSDP wraps it next in the caller
955
- 4. enable_weight_tying() is called AFTER FSDP wrap (see below)
956
  """
957
  model = Nova10BDense(cfg).to(cfg.dtype)
958
  model = apply_fp8(model, cfg)
@@ -960,14 +983,6 @@ def build_model(cfg: NovaConfig) -> nn.Module:
960
 
961
 
962
  def enable_weight_tying(fsdp_model: nn.Module):
963
- """
964
- Called AFTER FSDP wrapping.
965
- Sets a flag so forward() uses embed.weight for the lm_head projection
966
- without ever creating a shared Parameter object that FSDP's flat-param
967
- sharding could misregister. lm_head.weight continues to exist as its
968
- own parameter (kept for checkpoint-format compatibility) but is simply
969
- unused in forward once this flag is set.
970
- """
971
  for module in fsdp_model.modules():
972
  if isinstance(module, Nova10BDense):
973
  module._use_tied_lm_head = True
@@ -985,11 +1000,7 @@ _SAVE_SD_CFG = FullStateDictConfig(offload_to_cpu=True, rank0_only=True)
985
  _LOAD_SD_CFG = FullStateDictConfig(offload_to_cpu=True, rank0_only=False)
986
 
987
 
988
- def wrap_fsdp(
989
- model: nn.Module,
990
- cfg: NovaConfig,
991
- device: torch.device,
992
- ) -> FSDP:
993
  mp_policy = MixedPrecision(
994
  param_dtype=torch.bfloat16,
995
  reduce_dtype=torch.bfloat16,
@@ -1008,20 +1019,47 @@ def wrap_fsdp(
1008
  cpu_offload = CPUOffload(offload_params=cfg.cpu_offload),
1009
  device_id = device,
1010
  use_orig_params = True,
1011
- # FIX: force all ranks to start from bit-identical parameter values.
1012
- # Without this, divergent per-rank initialization state (however
1013
- # slight) can produce inconsistent shard metadata that surfaces as
1014
- # illegal memory access during the very first all_gather in unshard().
1015
- sync_module_states = True,
 
 
 
 
 
 
 
 
 
 
 
 
 
1016
  )
1017
  if is_main():
1018
  log.info(
1019
  f"[fsdp] FULL_SHARD × {world_size()} GPUs | "
1020
  f"cpu_offload={cfg.cpu_offload} | bf16 mixed-precision | "
1021
- f"sync_module_states=True"
1022
  )
1023
  return fsdp_model
1024
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1025
  # ══════════════════════════════════════════════════════════════════════════════
1026
  # MUON OPTIMIZER
1027
  # ══════════════════════════════════════════════════════════════════════════════
@@ -1042,14 +1080,7 @@ def _newtonschulz5(G: torch.Tensor, steps: int = 5) -> torch.Tensor:
1042
 
1043
 
1044
  class Muon(torch.optim.Optimizer):
1045
- def __init__(
1046
- self,
1047
- params,
1048
- lr: float = 0.02,
1049
- momentum: float = 0.95,
1050
- weight_decay: float = 0.0,
1051
- ns_steps: int = 5,
1052
- ):
1053
  defaults = dict(lr=lr, momentum=momentum,
1054
  weight_decay=weight_decay, ns_steps=ns_steps)
1055
  super().__init__(list(params), defaults)
@@ -1109,9 +1140,7 @@ def build_optimizers(model: nn.Module, cfg: NovaConfig):
1109
  aw_nodecay.append(p)
1110
 
1111
  muon_opt = Muon(
1112
- muon_p,
1113
- lr=cfg.muon_lr,
1114
- momentum=cfg.muon_momentum,
1115
  weight_decay=cfg.weight_decay,
1116
  )
1117
 
@@ -1121,20 +1150,13 @@ def build_optimizers(model: nn.Module, cfg: NovaConfig):
1121
  ]
1122
  if HAS_BNB:
1123
  aw_opt = bnb.optim.AdamW8bit(
1124
- aw_groups,
1125
- lr=cfg.lr,
1126
- betas=(cfg.beta1, cfg.beta2),
1127
- eps=1e-8,
1128
  )
1129
  if is_main():
1130
  log.info("[optim] Muon + AdamW-8bit (bitsandbytes)")
1131
  else:
1132
  aw_opt = torch.optim.AdamW(
1133
- aw_groups,
1134
- lr=cfg.lr,
1135
- betas=(cfg.beta1, cfg.beta2),
1136
- eps=1e-8,
1137
- fused=True,
1138
  )
1139
  if is_main():
1140
  log.warning("[optim] bitsandbytes not found — fused fp32 AdamW")
@@ -1147,13 +1169,7 @@ def build_optimizers(model: nn.Module, cfg: NovaConfig):
1147
  return muon_opt, aw_opt
1148
 
1149
 
1150
- def cosine_lr(
1151
- step: int,
1152
- warmup: int,
1153
- total: int,
1154
- peak: float,
1155
- floor: float,
1156
- ) -> float:
1157
  if step < warmup:
1158
  return peak * (step + 1) / warmup
1159
  if step >= total:
@@ -1170,16 +1186,7 @@ def _full_state_dict_save(model: FSDP) -> dict:
1170
  return model.state_dict()
1171
 
1172
 
1173
- def save_ckpt(
1174
- model: FSDP,
1175
- muon_opt,
1176
- aw_opt,
1177
- step: int,
1178
- epoch: int,
1179
- cfg: NovaConfig,
1180
- val_loss: float,
1181
- tag: str,
1182
- ) -> bool:
1183
  barrier()
1184
  sd = _full_state_dict_save(model)
1185
  if not is_main():
@@ -1192,12 +1199,9 @@ def save_ckpt(
1192
  torch.save({k: v.contiguous() for k, v in sd.items()}, mpath)
1193
  torch.save(
1194
  {
1195
- "step": step,
1196
- "epoch": epoch,
1197
- "val_loss": val_loss,
1198
- "config": asdict(cfg),
1199
- "muon": muon_opt.state_dict(),
1200
- "adamw": aw_opt.state_dict(),
1201
  },
1202
  xpath,
1203
  )
@@ -1216,13 +1220,7 @@ def save_ckpt(
1216
  return False
1217
 
1218
 
1219
- def save_final(
1220
- model: FSDP,
1221
- step: int,
1222
- epoch: int,
1223
- cfg: NovaConfig,
1224
- val_loss: float,
1225
- ):
1226
  sd = _full_state_dict_save(model)
1227
  if not is_main():
1228
  return
@@ -1238,10 +1236,8 @@ def save_final(
1238
  torch.save(sd_clean, mpath)
1239
  with open(cpath, "w") as f:
1240
  json.dump(
1241
- {**asdict(cfg), "final_step": step,
1242
- "epoch": epoch, "val_loss": val_loss},
1243
- f,
1244
- indent=2,
1245
  )
1246
  hf_upload(mpath, os.path.basename(mpath))
1247
  hf_upload(cpath, os.path.basename(cpath))
@@ -1255,23 +1251,12 @@ def save_final(
1255
  log.error(f"[final] save failed: {e}")
1256
 
1257
 
1258
- def load_ckpt(
1259
- model: FSDP,
1260
- muon_opt,
1261
- aw_opt,
1262
- device: torch.device,
1263
- cfg: NovaConfig,
1264
- ) -> tuple[int, int, float]:
1265
  try:
1266
  files = hf_api.list_repo_files(
1267
- repo_id=HF_MODEL_REPO,
1268
- repo_type="model",
1269
- token=HF_TOKEN,
1270
- )
1271
- ckpts = sorted(
1272
- [f for f in files if f.endswith(".model.pt")],
1273
- key=_ckpt_step,
1274
  )
 
1275
  if not ckpts:
1276
  return 0, 0, float("inf")
1277
 
@@ -1284,9 +1269,7 @@ def load_ckpt(
1284
 
1285
  sd = torch.load(mpath, map_location="cpu", weights_only=True)
1286
 
1287
- with FSDP.state_dict_type(
1288
- model, StateDictType.FULL_STATE_DICT, _LOAD_SD_CFG
1289
- ):
1290
  model.load_state_dict(sd, strict=False)
1291
 
1292
  barrier()
@@ -1322,12 +1305,7 @@ def load_ckpt(
1322
  # ══════════════════════════════════════════════════════════════════════════════
1323
 
1324
  @torch.no_grad()
1325
- def run_val(
1326
- model: nn.Module,
1327
- loader: DataLoader,
1328
- device: torch.device,
1329
- n: int = 80,
1330
- ) -> float:
1331
  model.eval()
1332
  losses: List[float] = []
1333
  it = iter(loader)
@@ -1350,9 +1328,6 @@ def run_val(
1350
  # ══════════════════════════════════════════════════════════════════════════════
1351
 
1352
  def vram_str(dev: Optional[int] = None) -> str:
1353
- # FIX: default to the CURRENT device for THIS process, not physical GPU 0.
1354
- # `dev: int = 0` previously meant every rank's log line silently reported
1355
- # rank 0's memory stats and was an unnecessary cross-device touch.
1356
  if dev is None:
1357
  dev = torch.cuda.current_device()
1358
  used = torch.cuda.memory_allocated(dev) / 1024 ** 3
@@ -1364,30 +1339,33 @@ def vram_str(dev: Optional[int] = None) -> str:
1364
  # ══════════════════════════════════════════════════════════════════════════════
1365
 
1366
  def train():
1367
- # ── distributed init ────────────────────────────────────────────────────
1368
  local_rank = setup_dist()
1369
  device = torch.device(f"cuda:{local_rank}")
1370
  ws = world_size()
1371
 
1372
- # FIX: log full CUDA/driver/NCCL env per rank immediately — this is the
1373
- # single most useful thing to have BEFORE a crash, not reconstructed
1374
- # after the fact.
1375
  log_cuda_environment(local_rank)
 
1376
 
1377
- # FIX: cheap isolated health check before anything else touches CUDA.
1378
- # If this fails, it proves the fault is raw context/driver level for
1379
- # this specific rank/device, independent of your training code.
1380
  with _stage("cuda_health_check"):
1381
  cuda_health_check(device)
1382
 
1383
- # FIX: device-scoped seeding — does NOT touch every visible GPU from
1384
- # every process (see seed_everything() docstring for why this matters).
1385
- with _stage("seed"):
1386
- seed_everything(42 + rank(), device)
1387
-
1388
- with _stage("hf_login"):
1389
- init_hf()
1390
-
 
 
 
 
 
 
 
 
 
1391
  barrier()
1392
 
1393
  with _stage("config_build"):
@@ -1395,8 +1373,7 @@ def train():
1395
  target_tokens = 200_000_000_000
1396
  tokens_per_step = cfg.effective_batch_tokens(ws)
1397
  cfg.total_steps = max(
1398
- cfg.total_steps,
1399
- math.ceil(target_tokens / tokens_per_step),
1400
  )
1401
  if is_main():
1402
  log.info(
@@ -1425,29 +1402,46 @@ def train():
1425
  if is_main():
1426
  log.info("[model] building Nova-10B-Dense …")
1427
  with torch.device("cpu"):
1428
- model = build_model(cfg) # FP8 applied, no weight tying yet
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1429
 
1430
  with _stage("fsdp_wrap"):
1431
  model = wrap_fsdp(model, cfg, device)
1432
- enable_weight_tying(model) # flag-based, post-wrap
1433
 
1434
  with _stage("compile"):
1435
  if cfg.use_compile:
1436
  try:
1437
- # FIX: compile the INNER Nova10BDense.forward, not the FSDP
1438
- # wrapper itself. Compiling an FSDP module directly can trace
1439
- # through FSDP's internal unshard/reshard machinery on some
1440
- # PyTorch versions, producing stale memory references that
1441
- # survive into the first real forward pass. Compiling only
1442
- # the underlying module's forward avoids this entirely while
1443
- # still capturing nearly all the throughput benefit.
1444
  for m in model.modules():
1445
  if isinstance(m, Nova10BDense):
1446
  m.forward = torch.compile(
1447
- m.forward,
1448
- mode="max-autotune",
1449
- fullgraph=False,
1450
- dynamic=True, # handles variable ctx-warmup lengths
1451
  )
1452
  if is_main():
1453
  log.info("[compile] Nova10BDense.forward compiled "
@@ -1460,6 +1454,12 @@ def train():
1460
  if is_main():
1461
  log.info(f"[model] VRAM after wrap: {vram_str()}")
1462
 
 
 
 
 
 
 
1463
  with _stage("optimizer_build"):
1464
  muon_opt, aw_opt = build_optimizers(model, cfg)
1465
 
@@ -1481,7 +1481,6 @@ def train():
1481
  train_loader, val_loader = make_loaders(cfg, rank(), ws)
1482
  train_iter = itertools.cycle(train_loader)
1483
 
1484
- # ── first real forward/backward, isolated and explicitly logged ─────────
1485
  with _stage("first_step_smoke_test"):
1486
  try:
1487
  probe_batch = next(train_iter).to(device, non_blocking=True)
@@ -1500,7 +1499,6 @@ def train():
1500
  log.error(f"[smoke] rank={rank()} FIRST forward/backward failed: {e}")
1501
  raise
1502
 
1503
- # ── summary ─────────────────────────────────────────────────────────────
1504
  if is_main():
1505
  log.info("=" * 72)
1506
  log.info(
@@ -1545,7 +1543,6 @@ def train():
1545
  def remaining_h() -> float: return cfg.max_hours - elapsed_h()
1546
 
1547
  while step < cfg.total_steps:
1548
-
1549
  if elapsed_h() >= cfg.max_hours:
1550
  if is_main():
1551
  log.info(f"[time] {cfg.max_hours}h budget exhausted — stopping")
@@ -1582,8 +1579,6 @@ def train():
1582
  log.error("[oom] step skipped — consider reducing micro_batch")
1583
  continue
1584
  except RuntimeError as e:
1585
- # FIX: explicit rank/device/step attribution for CUDA-class
1586
- # RuntimeErrors during the main loop, not just a bare traceback.
1587
  log.error(
1588
  f"[train_step] rank={rank()} device={device} step={step} "
1589
  f"CUDA RuntimeError: {e}"
@@ -1593,10 +1588,8 @@ def train():
1593
  if accum_n < cfg.grad_accum:
1594
  continue
1595
 
1596
- mlr = cosine_lr(step, cfg.warmup_steps, cfg.total_steps,
1597
- cfg.muon_lr, cfg.muon_min_lr)
1598
- alr = cosine_lr(step, cfg.warmup_steps, cfg.total_steps,
1599
- cfg.lr, cfg.min_lr)
1600
  for pg in muon_opt.param_groups:
1601
  pg["lr"] = mlr
1602
  for pg in aw_opt.param_groups:
@@ -1620,9 +1613,7 @@ def train():
1620
  tps = sps * tokens_per_step
1621
  a50 = float(np.mean(losses[-50:])) if losses else 0.0
1622
  ppl = math.exp(min(avg, 20))
1623
- log.info(
1624
- f"step {step:06d} | ctx {ctx} | {remaining_h():.2f}h left"
1625
- )
1626
  log.info(
1627
  f" loss {avg:.4f} avg50 {a50:.4f} ppl {ppl:.1f}"
1628
  f" mlr {mlr:.2e} alr {alr:.2e}"
@@ -1639,30 +1630,18 @@ def train():
1639
  log.info(f" >> val {vl:.4f} ppl {vpp:.2f}")
1640
  if vl < best_val:
1641
  best_val = vl
1642
- save_ckpt(
1643
- model, muon_opt, aw_opt,
1644
- step, epoch, cfg, vl, tag="best",
1645
- )
1646
  log.info(" >> new best checkpoint saved")
1647
 
1648
  if step % cfg.ckpt_every == 0:
1649
- save_ckpt(
1650
- model, muon_opt, aw_opt,
1651
- step, epoch, cfg,
1652
- losses[-1] if losses else 0.0,
1653
- tag="ckpt",
1654
- )
1655
 
1656
  if time.time() - t_last_ckpt > 1800:
1657
- save_ckpt(
1658
- model, muon_opt, aw_opt,
1659
- step, epoch, cfg,
1660
- losses[-1] if losses else 0.0,
1661
- tag="latest",
1662
- )
1663
  t_last_ckpt = time.time()
1664
 
1665
- # ── final ─────────────────────────────��──────────────────────────────────
1666
  if is_main():
1667
  log.info("=" * 72)
1668
  log.info("TRAINING COMPLETE — saving final checkpoint …")
@@ -1674,17 +1653,10 @@ def train():
1674
  if is_main():
1675
  avg_t = float(np.mean(losses[-200:])) if losses else 0.0
1676
  report = dict(
1677
- step=step,
1678
- epoch=epoch,
1679
- train_loss=avg_t,
1680
- val_loss=fval,
1681
- val_ppl=math.exp(min(fval, 20)),
1682
- best_val=best_val,
1683
- elapsed_h=elapsed_h(),
1684
- tokens_seen=step * tokens_per_step,
1685
- gpus=ws,
1686
- fp8=resolve_fp8(cfg),
1687
- compile=cfg.use_compile,
1688
  optimizer="Muon+AdamW8bit" if HAS_BNB else "Muon+AdamW",
1689
  model="Nova-10B-Dense",
1690
  )
@@ -1698,10 +1670,7 @@ def train():
1698
  pass
1699
  log.info("=" * 72)
1700
  for k, v in report.items():
1701
- log.info(
1702
- f" {k:<22}: "
1703
- + (f"{v:.4f}" if isinstance(v, float) else str(v))
1704
- )
1705
  log.info("=" * 72)
1706
 
1707
  cleanup_dist()
 
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 — use if you suspect
32
+ a driver bug in NCCL's optimized transport on brand-new hardware):
33
+ NOVA_NCCL_SAFE=1 torchrun --nproc_per_node=8 nova10b_dense.py
34
  """
35
 
36
  # ══════════════════════════════════════════════════════════════════════════════
 
38
  # ══════════════════════════════════════════════════════════════════════════════
39
  import os
40
 
41
+ # Pin deterministic PCI-bus-order device enumeration. Default CUDA_DEVICE_ORDER
42
+ # is FASTEST_FIRST, which requires the CUDA runtime to benchmark/query every
43
+ # visible GPU's capability on first CUDA call — with N processes starting
44
+ # concurrently, this becomes a multi-process race across ALL device contexts
45
+ # simultaneously. PCI_BUS_ID is deterministic and needs no cross-device
46
+ # benchmarking at all.
 
 
 
47
  os.environ.setdefault("CUDA_DEVICE_ORDER", "PCI_BUS_ID")
48
 
49
+ # Debug toggle — verbose CUDA/NCCL diagnostics, slower. Set NOVA_DEBUG=1.
 
50
  _DEBUG = os.environ.get("NOVA_DEBUG", "0") == "1"
51
  if _DEBUG:
52
  os.environ.setdefault("CUDA_LAUNCH_BLOCKING", "1")
 
55
  os.environ.setdefault("NCCL_DEBUG", "INFO")
56
  os.environ.setdefault("NCCL_DEBUG_SUBSYS", "INIT,COLL")
57
 
58
+ # NCCL-safe fallback toggle — forces NCCL onto more conservative transport
59
+ # paths. Brand-new GPU generations occasionally have driver bugs specifically
60
+ # in the optimized P2P/SHM fast paths that only manifest under NCCL collective
61
+ # ops, while falling back to plain network-style transport avoids the bug at
62
+ # a throughput cost. Set NOVA_NCCL_SAFE=1 to enable.
63
+ _NCCL_SAFE = os.environ.get("NOVA_NCCL_SAFE", "0") == "1"
64
+ if _NCCL_SAFE:
65
+ os.environ.setdefault("NCCL_P2P_DISABLE", "1")
66
+ os.environ.setdefault("NCCL_SHM_DISABLE", "1")
67
+ os.environ.setdefault("NCCL_ALGO", "Ring")
68
+
69
  # ── stdlib ──────────────────────────────────────────────────────────────────
70
  import gc
71
  import re
 
82
  from typing import Optional, List, Iterator
83
 
84
  # ── third-party ─────────────────────────────────────────────────────────────
 
85
  import numpy as np
86
  import torch
87
  import torch.nn as nn
 
129
  torch.backends.cudnn.allow_tf32 = True
130
  torch.backends.cudnn.benchmark = True
131
 
132
+ # ── logging ───────────────────────────────────────────────────────────────
 
133
  class _RankFilter(logging.Filter):
134
  def filter(self, record):
135
  record.rank = dist.get_rank() if dist.is_initialized() else 0
 
170
  dist.barrier()
171
 
172
  def all_reduce_mean(tensor: torch.Tensor) -> torch.Tensor:
 
173
  dist.all_reduce(tensor, op=dist.ReduceOp.SUM)
174
  tensor.div_(world_size())
175
  return tensor
176
 
177
 
178
  # ══════════════════════════════════════════════════════════════════════════════
179
+ # SEEDING — model-init seed (SAME across ranks) vs runtime seed (per-rank)
180
  # ══════════════════════════════════════════════════════════════════════════════
181
 
182
+ def seed_for_model_init(seed: int):
183
+ """
184
+ FIX: Seeds CPU RNG identically on EVERY rank, specifically for model
185
+ construction. This is deliberately rank-INDEPENDENT.
186
+
187
+ Previously, seed_everything(42 + rank(), device) seeded each rank's CPU
188
+ RNG DIFFERENTLY. Since the model is built via nn.init.normal_() calls on
189
+ CPU (see build_model()), different per-rank seeds meant every rank
190
+ constructed a DIFFERENT randomly-initialized model. The only way to make
191
+ them agree afterward was an FSDP sync_module_states=True broadcast — a
192
+ single ~16GB NCCL collective at construction time, which is exactly what
193
+ crashed (see incident log: crash moved from first-forward all_gather to
194
+ FSDP-construction broadcast the moment sync_module_states was added).
195
+
196
+ Fix: seed CPU identically across ALL ranks before model construction.
197
+ PyTorch's CPU RNG-based init (normal_, etc.) is deterministic for a given
198
+ seed and a given sequence of calls, so all ranks build BIT-IDENTICAL
199
+ models independently — no broadcast required, no giant collective at
200
+ construction time.
201
+ """
202
+ random.seed(seed)
203
+ np.random.seed(seed)
204
+ torch.manual_seed(seed) # CPU generator only — no CUDA touch
205
+
206
+
207
+ def seed_for_runtime(seed: int, device: torch.device):
208
  """
209
+ Rank-dependent seeding for anything AFTER model construction: data
210
+ shuffling, dropout (if any), sampling, etc. Seeds CUDA RNG state only
211
+ for THIS rank's own device — never touches other visible devices.
 
 
 
 
 
 
 
 
 
 
212
  """
213
  random.seed(seed)
214
  np.random.seed(seed)
215
+ torch.manual_seed(seed)
216
  if torch.cuda.is_available():
217
  with torch.cuda.device(device):
218
  torch.cuda.manual_seed(seed)
 
224
 
225
  def _stage(name: str):
226
  """
227
+ Context manager: logs entry, syncs CUDA before AND after (so errors are
228
+ attributed to the correct stage rather than surfacing later), barriers
229
+ all ranks, and on exception logs rank/device/stage/traceback before
230
+ re-raising.
 
 
 
 
 
 
 
231
  """
232
  class _StageCtx:
233
  def __enter__(self):
 
247
  f"{exc_type.__name__}: {exc_val}"
248
  )
249
  log.error("".join(traceback.format_exception(exc_type, exc_val, exc_tb)))
250
+ return False
251
 
252
  if torch.cuda.is_available():
253
  torch.cuda.synchronize()
 
260
 
261
 
262
  def log_cuda_environment(local_rank: int):
 
 
 
 
 
 
263
  dev = local_rank
264
  try:
265
  props = torch.cuda.get_device_properties(dev)
 
272
  f"mem={props.total_memory/1024**3:.0f}GB "
273
  f"torch={torch.__version__} cuda={torch.version.cuda} "
274
  f"nccl={nccl_ver} driver={drv} "
275
+ f"device_order={os.environ.get('CUDA_DEVICE_ORDER','default')} "
276
+ f"nccl_safe={_NCCL_SAFE}"
277
  )
278
  except Exception as e:
279
  log.warning(f"[env] rank={rank()} could not fully query CUDA env: {e}")
280
 
281
 
282
+ def log_p2p_matrix(local_rank: int, ws: int):
283
  """
284
+ Logs pairwise P2P access capability between this rank's device and every
285
+ other visible device. Cheap (N boolean queries), rank-0 only, purely
286
+ diagnostic. RTX PRO 6000 workstation cards typically lack NVLink, so
287
+ all P2P traffic goes over PCIe — if P2P access reports False for pairs
288
+ that should support it, or if topology is unexpectedly asymmetric, that's
289
+ a strong signal to try NOVA_NCCL_SAFE=1.
 
290
  """
291
+ if not is_main() or not torch.cuda.is_available():
292
+ return
293
+ try:
294
+ n = torch.cuda.device_count()
295
+ rows = []
296
+ for i in range(n):
297
+ row = []
298
+ for j in range(n):
299
+ if i == j:
300
+ row.append("-")
301
+ else:
302
+ can = torch.cuda.can_device_access_peer(i, j)
303
+ row.append("Y" if can else "N")
304
+ rows.append(" ".join(row))
305
+ log.info("[p2p] pairwise CUDA P2P access matrix (Y=can access, N=cannot):")
306
+ for i, r in enumerate(rows):
307
+ log.info(f"[p2p] gpu{i}: {r}")
308
+ except Exception as e:
309
+ log.warning(f"[p2p] matrix query failed: {e}")
310
+
311
+
312
+ def cuda_health_check(device: torch.device):
313
+ """Minimal per-rank CUDA sanity check: basic op + small matmul."""
314
  try:
315
  x = torch.ones(4096, device=device, dtype=torch.bfloat16)
316
  y = torch.ones(4096, device=device, dtype=torch.bfloat16)
 
327
  raise
328
 
329
 
330
+ def nccl_collective_selftest(device: torch.device, ws: int):
331
+ """
332
+ Isolated NCCL collective stress test, run BEFORE FSDP wrap, completely
333
+ independent of the model. Tests progressively larger all_gather and
334
+ broadcast operations — the exact same primitive classes that crashed
335
+ inside FSDP's unshard() and sync_module_states broadcast.
336
+
337
+ If this test crashes, it PROVES the fault is at the raw NCCL/driver
338
+ level for large collective operations on this hardware, fully isolated
339
+ from model complexity, FSDP wrapping, or torch.compile. That is the
340
+ single most valuable diagnostic result available: it tells you to stop
341
+ debugging the training script and escalate to a driver/NCCL/hardware
342
+ issue instead.
343
+
344
+ If it PASSES, the fault is specific to how FSDP or the model triggers
345
+ collectives (e.g. a specific tensor shape or timing pattern), not raw
346
+ NCCL health.
347
+ """
348
+ sizes_mb = [1, 16, 64, 256, 1024, 4096] # up to 4GB — comparable to a
349
+ # single large transformer
350
+ # block's flattened params
351
+ for mb in sizes_mb:
352
+ n_elem = (mb * 1024 * 1024) // 2 # bf16 = 2 bytes/elem
353
+ try:
354
+ local = torch.full((n_elem,), float(rank()), device=device, dtype=torch.bfloat16)
355
+
356
+ # all_gather — same primitive as FSDP unshard()
357
+ gathered = [torch.zeros_like(local) for _ in range(ws)]
358
+ dist.all_gather(gathered, local)
359
+ torch.cuda.synchronize(device)
360
+
361
+ # broadcast — same primitive as sync_module_states
362
+ bcast = local.clone()
363
+ dist.broadcast(bcast, src=0)
364
+ torch.cuda.synchronize(device)
365
+
366
+ expected_sum = sum(range(ws)) * n_elem
367
+ actual_sum = sum(float(g.sum().item()) for g in gathered)
368
+ ok = abs(actual_sum - expected_sum) < max(1.0, expected_sum * 1e-3)
369
+
370
+ log.info(
371
+ f"[nccl_selftest] rank={rank()} size={mb}MB "
372
+ f"all_gather+broadcast OK match={ok}"
373
+ )
374
+ del local, gathered, bcast
375
+ torch.cuda.empty_cache()
376
+ except Exception as e:
377
+ log.error(
378
+ f"[nccl_selftest] rank={rank()} size={mb}MB FAILED: {e} — "
379
+ f"this proves the fault is at the raw NCCL/driver level for "
380
+ f"collectives of this size, independent of FSDP/model code. "
381
+ f"Try relaunching with NOVA_NCCL_SAFE=1, or escalate to "
382
+ f"infra for driver/NCCL version review."
383
+ )
384
+ raise
385
+ if is_main():
386
+ log.info(f"[nccl_selftest] ALL sizes up to {sizes_mb[-1]}MB passed on all ranks")
387
+
388
+
389
  # ══════════════════════════════════════════════════════════════════════════════
390
  # HF HUB
391
  # ══════════════════════════════════════════════════════════════════════════════
 
481
 
482
 
483
  def _ckpt_step(fname: str) -> int:
 
484
  m = re.search(r"_s(\d+)\.", fname)
485
  return int(m.group(1)) if m else 0
486
 
 
520
 
521
  @dataclass
522
  class NovaConfig:
 
523
  vocab_size: int = 65536
524
  d_model: int = 4096
525
  n_heads: int = 32
 
531
  logit_softcap: float = 50.0
532
  rope_theta: float = 500_000.0
533
 
534
+ use_fp8: str = "auto"
 
535
 
 
536
  lr: float = 2e-4
537
  min_lr: float = 2e-5
538
  muon_lr: float = 6e-3
 
544
  grad_clip: float = 1.0
545
  warmup_steps: int = 4_000
546
 
 
547
  total_steps: int = 130_000
548
  ctx_warmup_steps: int = 3_000
549
  ctx_start_len: int = 2048
550
  ctx_end_len: int = 2048
551
 
 
552
  micro_batch: int = 6
553
  grad_accum: int = 8
554
  log_every: int = 10
 
558
  seed: int = 42
559
  max_hours: float = 47.5
560
 
 
561
  domains: List[str] = field(default_factory=lambda: list(DOMAINS))
562
  shard_refresh_rows: int = 100_000
563
  data_buf_size: int = 16_384
564
 
 
565
  cpu_offload: bool = False
 
 
566
  use_compile: bool = True
567
 
568
  @property
 
590
  def _is_blackwell() -> bool:
591
  if not torch.cuda.is_available():
592
  return False
 
 
 
 
 
 
593
  dev = torch.cuda.current_device()
594
  major, _ = torch.cuda.get_device_capability(dev)
595
  return major >= 10
 
610
  return ok
611
 
612
 
 
613
  _FP8_SKIP = ("embed", "lm_head", "norm", "diff_lambda", "head_gate",
614
  "ls_a", "ls_f", "rope")
615
 
 
622
 
623
 
624
  def apply_fp8(model: nn.Module, cfg: NovaConfig) -> nn.Module:
 
 
 
 
625
  if not resolve_fp8(cfg):
626
  return model
627
  fp8_cfg = Float8LinearConfig.from_recipe_name("rowwise")
 
635
  return model
636
 
637
  # ═══════════════════════════════════════════════���══════════════════════════════
638
+ # DATA
639
  # ══════════════════════════════════════════════════════════════════════════════
640
 
641
  class ShardStream:
 
 
 
 
 
 
 
642
  def __init__(self, domain: str, refresh_every: int = 100_000):
643
  self.domain = domain
644
  self.refresh_every = refresh_every
 
679
 
680
 
681
  class BalancedIterableDataset(IterableDataset):
 
 
 
 
 
 
682
  def __init__(
683
  self,
684
  domains: List[str],
 
729
  val_ds = BalancedIterableDataset(
730
  cfg.domains, cfg.max_len, cfg.data_buf_size // 4, cfg.shard_refresh_rows
731
  )
732
+ train_loader = DataLoader(train_ds, batch_size=cfg.micro_batch, **kw)
733
+ val_loader = DataLoader(val_ds, batch_size=min(cfg.micro_batch, 4), **kw)
 
 
 
 
734
  return train_loader, val_loader
735
 
736
  # ══════════════════════════════════════════════════════════════════════════════
 
747
  return F.rms_norm(x, self.scale.shape, self.scale, self.eps)
748
 
749
 
750
+ def precompute_rope(head_dim: int, max_len: int, theta: float, device=None):
 
 
 
 
 
 
 
 
 
751
  inv_freq = 1.0 / (
752
  theta ** (
753
  torch.arange(0, head_dim, 2, dtype=torch.float32, device=device)
 
759
  return torch.cos(freqs), torch.sin(freqs)
760
 
761
 
762
+ def apply_rope(q, k, cos, sin):
 
 
 
 
 
 
 
 
763
  L = q.shape[1]
764
  hd = q.shape[-1]
765
 
 
769
  cos_full = cos_full.unsqueeze(0).unsqueeze(2)
770
  sin_full = sin_full.unsqueeze(0).unsqueeze(2)
771
 
772
+ def _rotate_half(x):
773
  half = x.shape[-1] // 2
774
  x1, x2 = x[..., :half], x[..., half:]
775
  return torch.cat([-x2, x1], dim=-1)
 
781
  return q_rot, k_rot
782
 
783
 
 
 
784
  class NovaAttention(nn.Module):
785
  def __init__(self, cfg: NovaConfig):
786
  super().__init__()
 
811
  nn.init.normal_(self.qkv.weight, std=std)
812
  nn.init.normal_(self.o_proj.weight, std=dstd)
813
 
814
+ def forward(self, x, cos, sin):
 
 
 
 
 
815
  B, L, D = x.shape
816
 
817
  qkv = self.qkv(x)
 
849
  return self.o_proj(out)
850
 
851
 
 
 
852
  class SwiGLU(nn.Module):
853
  def __init__(self, cfg: NovaConfig):
854
  super().__init__()
 
863
  nn.init.normal_(self.up.weight, std=std)
864
  nn.init.normal_(self.down.weight, std=dstd)
865
 
866
+ def forward(self, x):
867
  return self.down(F.silu(self.gate(x)) * self.up(x))
868
 
869
 
 
 
870
  class NovaBlock(nn.Module):
871
  def __init__(self, cfg: NovaConfig):
872
  super().__init__()
 
877
  self.ls_a = nn.Parameter(torch.full((cfg.d_model,), 0.1))
878
  self.ls_f = nn.Parameter(torch.full((cfg.d_model,), 0.1))
879
 
880
+ def forward(self, x, cos, sin):
 
 
 
 
 
881
  x = x + self.ls_a * self.attn(self.attn_norm(x), cos, sin)
882
  x = x + self.ls_f * self.ffn(self.ffn_norm(x))
883
  return x
884
 
885
 
 
 
886
  class Nova10BDense(nn.Module):
887
  def __init__(self, cfg: NovaConfig):
888
  super().__init__()
 
891
  self.embed = nn.Embedding(cfg.vocab_size, cfg.d_model)
892
  self.layers = nn.ModuleList([NovaBlock(cfg) for _ in range(cfg.n_layers)])
893
  self.norm = RMSNorm(cfg.d_model)
 
 
 
 
 
 
 
 
 
 
 
 
894
  self.lm_head = nn.Linear(cfg.d_model, cfg.vocab_size, bias=False)
895
 
896
  self.softcap = cfg.logit_softcap
897
+ self._use_tied_lm_head = False
898
 
899
  nn.init.normal_(self.embed.weight, std=0.02)
900
  nn.init.normal_(self.lm_head.weight, std=0.02)
 
912
  f"ffn_h={cfg.ffn_hidden} | max_len={cfg.max_len}"
913
  )
914
 
915
+ def forward(self, ids, targets=None):
 
 
 
 
916
  B, L = ids.shape
917
  x = self.embed(ids)
918
  cos = self.rope_cos[:L]
 
948
  n += p.numel()
949
  return n
950
 
951
+ def weight_checksum(self) -> float:
952
+ """
953
+ Cheap deterministic checksum of a SAMPLE of parameters, used to
954
+ verify all ranks built bit-identical models after seed_for_model_init.
955
+ Sampling (not full sum) keeps this fast even at 8B+ params.
956
+ """
957
+ with torch.no_grad():
958
+ s = 0.0
959
+ s += float(self.embed.weight[:100, :100].sum().item())
960
+ s += float(self.lm_head.weight[:100, :100].sum().item())
961
+ for i, layer in enumerate(self.layers):
962
+ if i % 5 == 0: # sample every 5th layer
963
+ s += float(layer.attn.qkv.weight[:50, :50].sum().item())
964
+ s += float(layer.ffn.gate.weight[:50, :50].sum().item())
965
+ return s
966
+
967
 
968
  def build_model(cfg: NovaConfig) -> nn.Module:
969
  """
970
  Build order:
971
+ 1. Create model on CPU — model construction uses whatever CPU RNG
972
+ state is active at call time. Caller MUST have called
973
+ seed_for_model_init(same_seed) on ALL ranks immediately before
974
+ this, so every rank's random init is bit-identical — no FSDP
975
+ broadcast needed afterward.
976
  2. Apply FP8 (replaces eligible nn.Linear layers in-place)
977
  3. Return model — FSDP wraps it next in the caller
978
+ 4. enable_weight_tying() is called AFTER FSDP wrap
979
  """
980
  model = Nova10BDense(cfg).to(cfg.dtype)
981
  model = apply_fp8(model, cfg)
 
983
 
984
 
985
  def enable_weight_tying(fsdp_model: nn.Module):
 
 
 
 
 
 
 
 
986
  for module in fsdp_model.modules():
987
  if isinstance(module, Nova10BDense):
988
  module._use_tied_lm_head = True
 
1000
  _LOAD_SD_CFG = FullStateDictConfig(offload_to_cpu=True, rank0_only=False)
1001
 
1002
 
1003
+ def wrap_fsdp(model: nn.Module, cfg: NovaConfig, device: torch.device) -> FSDP:
 
 
 
 
1004
  mp_policy = MixedPrecision(
1005
  param_dtype=torch.bfloat16,
1006
  reduce_dtype=torch.bfloat16,
 
1019
  cpu_offload = CPUOffload(offload_params=cfg.cpu_offload),
1020
  device_id = device,
1021
  use_orig_params = True,
1022
+ # FIX: sync_module_states DELIBERATELY set to False.
1023
+ #
1024
+ # Previously we set this True to force cross-rank agreement on
1025
+ # initial weights, since seeding was rank-dependent. That required
1026
+ # FSDP to perform a single massive (~16GB) NCCL broadcast of the
1027
+ # full unsharded parameter set at construction time — and THAT
1028
+ # broadcast is precisely what crashed (see incident log: the crash
1029
+ # moved from first-forward all_gather to FSDP-construction the
1030
+ # moment sync_module_states=True was added).
1031
+ #
1032
+ # Correctness is now guaranteed differently: seed_for_model_init()
1033
+ # is called identically on ALL ranks before build_model(), so every
1034
+ # rank constructs a bit-identical model independently on CPU — no
1035
+ # broadcast is needed. We verify this explicitly via
1036
+ # verify_model_sync() right after this wrap, using a cheap
1037
+ # all_reduce of a parameter-sample checksum instead of moving
1038
+ # gigabytes of data.
1039
+ sync_module_states = False,
1040
  )
1041
  if is_main():
1042
  log.info(
1043
  f"[fsdp] FULL_SHARD × {world_size()} GPUs | "
1044
  f"cpu_offload={cfg.cpu_offload} | bf16 mixed-precision | "
1045
+ f"sync_module_states=False (models pre-synced via seeding)"
1046
  )
1047
  return fsdp_model
1048
 
1049
+
1050
+ def verify_model_sync(model: nn.Module, device: torch.device):
1051
+ """
1052
+ Cheap correctness check replacing the need for sync_module_states'
1053
+ giant broadcast: each rank computes a small checksum over a SAMPLE of
1054
+ its own local (unsharded-at-this-point... note: post-FSDP-wrap params
1055
+ are already flattened/sharded, so this must run on the underlying
1056
+ module BEFORE wrap, or we accept it's checking post-shard consistency
1057
+ of whatever shard each rank holds, which is a weaker but still useful
1058
+ signal). We call this on the CPU model prior to FSDP wrap for a true
1059
+ pre-shard comparison — see call site in train().
1060
+ """
1061
+ pass # actual invocation happens on the pre-wrap CPU model; see train()
1062
+
1063
  # ══════════════════════════════════════════════════════════════════════════════
1064
  # MUON OPTIMIZER
1065
  # ══════════════════════════════════════════════════════════════════════════════
 
1080
 
1081
 
1082
  class Muon(torch.optim.Optimizer):
1083
+ def __init__(self, params, lr=0.02, momentum=0.95, weight_decay=0.0, ns_steps=5):
 
 
 
 
 
 
 
1084
  defaults = dict(lr=lr, momentum=momentum,
1085
  weight_decay=weight_decay, ns_steps=ns_steps)
1086
  super().__init__(list(params), defaults)
 
1140
  aw_nodecay.append(p)
1141
 
1142
  muon_opt = Muon(
1143
+ muon_p, lr=cfg.muon_lr, momentum=cfg.muon_momentum,
 
 
1144
  weight_decay=cfg.weight_decay,
1145
  )
1146
 
 
1150
  ]
1151
  if HAS_BNB:
1152
  aw_opt = bnb.optim.AdamW8bit(
1153
+ aw_groups, lr=cfg.lr, betas=(cfg.beta1, cfg.beta2), eps=1e-8,
 
 
 
1154
  )
1155
  if is_main():
1156
  log.info("[optim] Muon + AdamW-8bit (bitsandbytes)")
1157
  else:
1158
  aw_opt = torch.optim.AdamW(
1159
+ aw_groups, lr=cfg.lr, betas=(cfg.beta1, cfg.beta2), eps=1e-8, fused=True,
 
 
 
 
1160
  )
1161
  if is_main():
1162
  log.warning("[optim] bitsandbytes not found — fused fp32 AdamW")
 
1169
  return muon_opt, aw_opt
1170
 
1171
 
1172
+ def cosine_lr(step, warmup, total, peak, floor):
 
 
 
 
 
 
1173
  if step < warmup:
1174
  return peak * (step + 1) / warmup
1175
  if step >= total:
 
1186
  return model.state_dict()
1187
 
1188
 
1189
+ def save_ckpt(model, muon_opt, aw_opt, step, epoch, cfg, val_loss, tag) -> bool:
 
 
 
 
 
 
 
 
 
1190
  barrier()
1191
  sd = _full_state_dict_save(model)
1192
  if not is_main():
 
1199
  torch.save({k: v.contiguous() for k, v in sd.items()}, mpath)
1200
  torch.save(
1201
  {
1202
+ "step": step, "epoch": epoch, "val_loss": val_loss,
1203
+ "config": asdict(cfg),
1204
+ "muon": muon_opt.state_dict(), "adamw": aw_opt.state_dict(),
 
 
 
1205
  },
1206
  xpath,
1207
  )
 
1220
  return False
1221
 
1222
 
1223
+ def save_final(model, step, epoch, cfg, val_loss):
 
 
 
 
 
 
1224
  sd = _full_state_dict_save(model)
1225
  if not is_main():
1226
  return
 
1236
  torch.save(sd_clean, mpath)
1237
  with open(cpath, "w") as f:
1238
  json.dump(
1239
+ {**asdict(cfg), "final_step": step, "epoch": epoch, "val_loss": val_loss},
1240
+ f, indent=2,
 
 
1241
  )
1242
  hf_upload(mpath, os.path.basename(mpath))
1243
  hf_upload(cpath, os.path.basename(cpath))
 
1251
  log.error(f"[final] save failed: {e}")
1252
 
1253
 
1254
+ def load_ckpt(model, muon_opt, aw_opt, device, cfg) -> tuple:
 
 
 
 
 
 
1255
  try:
1256
  files = hf_api.list_repo_files(
1257
+ repo_id=HF_MODEL_REPO, repo_type="model", token=HF_TOKEN,
 
 
 
 
 
 
1258
  )
1259
+ ckpts = sorted([f for f in files if f.endswith(".model.pt")], key=_ckpt_step)
1260
  if not ckpts:
1261
  return 0, 0, float("inf")
1262
 
 
1269
 
1270
  sd = torch.load(mpath, map_location="cpu", weights_only=True)
1271
 
1272
+ with FSDP.state_dict_type(model, StateDictType.FULL_STATE_DICT, _LOAD_SD_CFG):
 
 
1273
  model.load_state_dict(sd, strict=False)
1274
 
1275
  barrier()
 
1305
  # ══════════════════════════════════════════════════════════════════════════════
1306
 
1307
  @torch.no_grad()
1308
+ def run_val(model, loader, device, n: int = 80) -> float:
 
 
 
 
 
1309
  model.eval()
1310
  losses: List[float] = []
1311
  it = iter(loader)
 
1328
  # ══════════════════════════════════════════════════════════════════════════════
1329
 
1330
  def vram_str(dev: Optional[int] = None) -> str:
 
 
 
1331
  if dev is None:
1332
  dev = torch.cuda.current_device()
1333
  used = torch.cuda.memory_allocated(dev) / 1024 ** 3
 
1339
  # ══════════════════════════════════════════════════════════════════════════════
1340
 
1341
  def train():
 
1342
  local_rank = setup_dist()
1343
  device = torch.device(f"cuda:{local_rank}")
1344
  ws = world_size()
1345
 
 
 
 
1346
  log_cuda_environment(local_rank)
1347
+ log_p2p_matrix(local_rank, ws)
1348
 
 
 
 
1349
  with _stage("cuda_health_check"):
1350
  cuda_health_check(device)
1351
 
1352
+ # FIX: isolated NCCL collective self-test BEFORE any model/FSDP code.
1353
+ # This proves — completely independent of the training script's own
1354
+ # complexity — whether raw large-tensor NCCL collectives (all_gather,
1355
+ # broadcast) work at all on this hardware/driver stack. If this stage
1356
+ # fails, the problem is proven to be environmental, not code.
1357
+ with _stage("nccl_collective_selftest"):
1358
+ nccl_collective_selftest(device, ws)
1359
+
1360
+ # FIX: model-init seed is IDENTICAL across all ranks (not rank+offset).
1361
+ # This guarantees every rank constructs a bit-identical model on CPU
1362
+ # independently, removing the NEED for FSDP's sync_module_states
1363
+ # broadcast entirely (see wrap_fsdp() docstring for why that broadcast
1364
+ # was implicated in the FSDP-construction-time crash).
1365
+ with _stage("seed_model_init"):
1366
+ seed_for_model_init(42)
1367
+
1368
+ init_hf()
1369
  barrier()
1370
 
1371
  with _stage("config_build"):
 
1373
  target_tokens = 200_000_000_000
1374
  tokens_per_step = cfg.effective_batch_tokens(ws)
1375
  cfg.total_steps = max(
1376
+ cfg.total_steps, math.ceil(target_tokens / tokens_per_step),
 
1377
  )
1378
  if is_main():
1379
  log.info(
 
1402
  if is_main():
1403
  log.info("[model] building Nova-10B-Dense …")
1404
  with torch.device("cpu"):
1405
+ model = build_model(cfg)
1406
+
1407
+ with _stage("verify_model_sync"):
1408
+ # FIX: replaces sync_module_states' giant broadcast with a cheap
1409
+ # correctness check. Computes a sampled parameter checksum on each
1410
+ # rank's independently-constructed CPU model, all_reduces it, and
1411
+ # compares — if seed_for_model_init worked, every rank's checksum
1412
+ # should be numerically identical (bit-identical CPU RNG results).
1413
+ local_checksum = model.weight_checksum() if hasattr(model, "weight_checksum") else 0.0
1414
+ checksum_t = torch.tensor(local_checksum, dtype=torch.float64, device=device)
1415
+ gathered = [torch.zeros_like(checksum_t) for _ in range(ws)]
1416
+ dist.all_gather(gathered, checksum_t)
1417
+ vals = [float(g.item()) for g in gathered]
1418
+ max_diff = max(vals) - min(vals)
1419
+ if is_main():
1420
+ if max_diff < 1e-6:
1421
+ log.info(f"[verify] model checksums MATCH across all {ws} ranks "
1422
+ f"(checksum={vals[0]:.6f}) — no broadcast needed")
1423
+ else:
1424
+ log.error(
1425
+ f"[verify] model checksums DIVERGE across ranks: {vals} "
1426
+ f"(max_diff={max_diff:.6f}) — ranks built DIFFERENT models! "
1427
+ f"This means seed_for_model_init did not produce identical "
1428
+ f"init across ranks (possible CPU non-determinism or a "
1429
+ f"process-specific RNG state leak). Training would silently "
1430
+ f"corrupt without an explicit sync — investigate before continuing."
1431
+ )
1432
 
1433
  with _stage("fsdp_wrap"):
1434
  model = wrap_fsdp(model, cfg, device)
1435
+ enable_weight_tying(model)
1436
 
1437
  with _stage("compile"):
1438
  if cfg.use_compile:
1439
  try:
 
 
 
 
 
 
 
1440
  for m in model.modules():
1441
  if isinstance(m, Nova10BDense):
1442
  m.forward = torch.compile(
1443
+ m.forward, mode="max-autotune",
1444
+ fullgraph=False, dynamic=True,
 
 
1445
  )
1446
  if is_main():
1447
  log.info("[compile] Nova10BDense.forward compiled "
 
1454
  if is_main():
1455
  log.info(f"[model] VRAM after wrap: {vram_str()}")
1456
 
1457
+ # FIX: switch to rank-dependent seeding NOW, after model construction is
1458
+ # done. This is safe — it only affects data shuffling / any subsequent
1459
+ # stochastic ops, not model weights (which are already fixed and synced).
1460
+ with _stage("seed_runtime"):
1461
+ seed_for_runtime(42 + rank(), device)
1462
+
1463
  with _stage("optimizer_build"):
1464
  muon_opt, aw_opt = build_optimizers(model, cfg)
1465
 
 
1481
  train_loader, val_loader = make_loaders(cfg, rank(), ws)
1482
  train_iter = itertools.cycle(train_loader)
1483
 
 
1484
  with _stage("first_step_smoke_test"):
1485
  try:
1486
  probe_batch = next(train_iter).to(device, non_blocking=True)
 
1499
  log.error(f"[smoke] rank={rank()} FIRST forward/backward failed: {e}")
1500
  raise
1501
 
 
1502
  if is_main():
1503
  log.info("=" * 72)
1504
  log.info(
 
1543
  def remaining_h() -> float: return cfg.max_hours - elapsed_h()
1544
 
1545
  while step < cfg.total_steps:
 
1546
  if elapsed_h() >= cfg.max_hours:
1547
  if is_main():
1548
  log.info(f"[time] {cfg.max_hours}h budget exhausted — stopping")
 
1579
  log.error("[oom] step skipped — consider reducing micro_batch")
1580
  continue
1581
  except RuntimeError as e:
 
 
1582
  log.error(
1583
  f"[train_step] rank={rank()} device={device} step={step} "
1584
  f"CUDA RuntimeError: {e}"
 
1588
  if accum_n < cfg.grad_accum:
1589
  continue
1590
 
1591
+ mlr = cosine_lr(step, cfg.warmup_steps, cfg.total_steps, cfg.muon_lr, cfg.muon_min_lr)
1592
+ alr = cosine_lr(step, cfg.warmup_steps, cfg.total_steps, cfg.lr, cfg.min_lr)
 
 
1593
  for pg in muon_opt.param_groups:
1594
  pg["lr"] = mlr
1595
  for pg in aw_opt.param_groups:
 
1613
  tps = sps * tokens_per_step
1614
  a50 = float(np.mean(losses[-50:])) if losses else 0.0
1615
  ppl = math.exp(min(avg, 20))
1616
+ log.info(f"step {step:06d} | ctx {ctx} | {remaining_h():.2f}h left")
 
 
1617
  log.info(
1618
  f" loss {avg:.4f} avg50 {a50:.4f} ppl {ppl:.1f}"
1619
  f" mlr {mlr:.2e} alr {alr:.2e}"
 
1630
  log.info(f" >> val {vl:.4f} ppl {vpp:.2f}")
1631
  if vl < best_val:
1632
  best_val = vl
1633
+ save_ckpt(model, muon_opt, aw_opt, step, epoch, cfg, vl, tag="best")
 
 
 
1634
  log.info(" >> new best checkpoint saved")
1635
 
1636
  if step % cfg.ckpt_every == 0:
1637
+ save_ckpt(model, muon_opt, aw_opt, step, epoch, cfg,
1638
+ losses[-1] if losses else 0.0, tag="ckpt")
 
 
 
 
1639
 
1640
  if time.time() - t_last_ckpt > 1800:
1641
+ save_ckpt(model, muon_opt, aw_opt, step, epoch, cfg,
1642
+ losses[-1] if losses else 0.0, tag="latest")
 
 
 
 
1643
  t_last_ckpt = time.time()
1644
 
 
1645
  if is_main():
1646
  log.info("=" * 72)
1647
  log.info("TRAINING COMPLETE — saving final checkpoint …")
 
1653
  if is_main():
1654
  avg_t = float(np.mean(losses[-200:])) if losses else 0.0
1655
  report = dict(
1656
+ step=step, epoch=epoch, train_loss=avg_t, val_loss=fval,
1657
+ val_ppl=math.exp(min(fval, 20)), best_val=best_val,
1658
+ elapsed_h=elapsed_h(), tokens_seen=step * tokens_per_step,
1659
+ gpus=ws, fp8=resolve_fp8(cfg), compile=cfg.use_compile,
 
 
 
 
 
 
 
1660
  optimizer="Muon+AdamW8bit" if HAS_BNB else "Muon+AdamW",
1661
  model="Nova-10B-Dense",
1662
  )
 
1670
  pass
1671
  log.info("=" * 72)
1672
  for k, v in report.items():
1673
+ log.info(f" {k:<22}: " + (f"{v:.4f}" if isinstance(v, float) else str(v)))
 
 
 
1674
  log.info("=" * 72)
1675
 
1676
  cleanup_dist()