Bc-AI commited on
Commit
9bc1632
Β·
verified Β·
1 Parent(s): 876852f

Update nova10b_dense.py

Browse files
Files changed (1) hide show
  1. nova10b_dense.py +197 -210
nova10b_dense.py CHANGED
@@ -1,25 +1,30 @@
1
  #!/usr/bin/env python3
2
  """
3
- Nova-10B-Dense Β· HuggingFace Jobs Β· 4-8Γ— RTX PRO 6000 Blackwell
4
  ~10B fully-dense transformer (no MoE, no routing, no MoD)
5
 
6
  Architecture:
7
- D1 Fused QKV – single Linear for Q+K+V, split after projection
8
- D2 Differential Attn – dual-stream diff attn + QK-Norm + head-gate (GQA/RoPE)
9
- D3 SwiGLU FFN – dense gated MLP, no routing
10
- D4 RMSNorm – pre-norm on attn + FFN
11
- D5 LayerScale – per-layer learnable residual scale (0.1 init)
12
- D6 RoPE – extended-theta (500k) rotary embeddings
13
- D7 Logit soft-cap – tanh(x/cap)*cap on final logits
14
- D8 FP8 – torchao rowwise (Blackwell sm_100)
15
 
16
  Training stack:
17
- O1 Muon – Newton-Schulz orthogonalized momentum for 2-D weights
18
- O2 AdamW-8bit – bitsandbytes, for embed/norms/1-D params
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
@@ -28,7 +33,6 @@ Launch:
28
 
29
  # ── stdlib ──────────────────────────────────────────────────────────────────
30
  import os
31
- import gc
32
  import re
33
  import json
34
  import time
@@ -38,7 +42,7 @@ import logging
38
  import itertools
39
  import functools
40
  from dataclasses import dataclass, asdict, field
41
- from typing import Optional, List, Iterator
42
 
43
  # ── third-party ─────────────────────────────────────────────────────────────
44
  import numpy as np
@@ -86,8 +90,8 @@ torch.backends.cuda.matmul.allow_tf32 = True
86
  torch.backends.cudnn.allow_tf32 = True
87
  torch.backends.cudnn.benchmark = True
88
 
89
- # ── logging β€” filter must be on handlers, not just the logger,
90
- # so %(rank)s is available from the very first message ────────────────────
91
  class _RankFilter(logging.Filter):
92
  def filter(self, record):
93
  record.rank = dist.get_rank() if dist.is_initialized() else 0
@@ -95,30 +99,40 @@ class _RankFilter(logging.Filter):
95
 
96
  _rank_filter = _RankFilter()
97
 
 
 
 
98
  logging.basicConfig(
99
- level=logging.INFO,
100
  format="%(asctime)s | R%(rank)s | %(message)s",
101
  datefmt="%H:%M:%S",
102
  )
103
- # Attach to every handler so %(rank)s is always resolved
104
  for _h in logging.root.handlers:
105
  _h.addFilter(_rank_filter)
106
 
107
  log = logging.getLogger("nova10b")
108
  log.addFilter(_rank_filter)
109
 
 
 
 
 
 
 
110
  # ══════════════════════════════════════════════════════════════════════════════
111
  # DISTRIBUTED HELPERS
112
  # ═════════════════��════════════════════════════════════════════════════════════
113
 
114
  def setup_dist() -> int:
115
- dist.init_process_group(backend="nccl")
116
  local_rank = int(os.environ.get("LOCAL_RANK", 0))
117
  torch.cuda.set_device(local_rank)
 
118
  return local_rank
119
 
120
  def cleanup_dist():
121
- dist.destroy_process_group()
 
122
 
123
  def is_main() -> bool: return rank() == 0
124
  def rank() -> int: return dist.get_rank() if dist.is_initialized() else 0
@@ -129,7 +143,7 @@ def barrier():
129
  dist.barrier()
130
 
131
  def all_reduce_mean(tensor: torch.Tensor) -> torch.Tensor:
132
- """All-reduce mean β€” uses SUM + div for universal NCCL compatibility."""
133
  dist.all_reduce(tensor, op=dist.ReduceOp.SUM)
134
  tensor.div_(world_size())
135
  return tensor
@@ -176,7 +190,7 @@ def hf_upload(local: str, remote: str) -> bool:
176
  token=HF_TOKEN,
177
  )
178
  if is_main():
179
- log.info(f"[hf] ↑ {remote}")
180
  return True
181
  except Exception as e:
182
  if is_main():
@@ -229,7 +243,7 @@ def list_shards(domain: str) -> List[str]:
229
 
230
 
231
  def _ckpt_step(fname: str) -> int:
232
- """Extract step number from checkpoint filename robustly."""
233
  m = re.search(r"_s(\d+)\.", fname)
234
  return int(m.group(1)) if m else 0
235
 
@@ -259,7 +273,7 @@ def rotate_ckpts(tag: str, keep: int = 2):
259
  )
260
  except Exception:
261
  pass
262
- log.info(f"[hf] rotated {victim}")
263
  except Exception as e:
264
  log.warning(f"[hf] rotate failed: {e}")
265
 
@@ -278,11 +292,11 @@ class NovaConfig:
278
  max_len: int = 8192
279
  ffn_hidden: int = 14336
280
  diff_lambda_init: float = 0.8
281
- logit_softcap: float = 50.0 # loosened from 30 β€” less aggressive at 10B scale
282
  rope_theta: float = 500_000.0
283
 
284
  # ── FP8 ────────────────────────────────────────────────────────────────
285
- use_fp8: str = "auto" # "auto" | "on" | "off"
286
 
287
  # ── optimiser ──────────────────────────────────────────────────────────
288
  lr: float = 2e-4
@@ -357,17 +371,16 @@ def resolve_fp8(cfg: NovaConfig) -> bool:
357
  return False
358
  ok = HAS_TORCHAO and _is_blackwell()
359
  if cfg.use_fp8 == "on" and not ok:
360
- log.warning("[fp8] forced ON but torchao missing or GPU < sm_100 β€” bf16 fallback")
361
  return False
362
  if cfg.use_fp8 == "auto" and is_main():
363
  log.info(
364
- f"[fp8] auto β†’ torchao={HAS_TORCHAO} "
365
- f"blackwell={_is_blackwell()} β†’ {'ON' if ok else 'OFF'}"
366
  )
367
  return ok
368
 
369
 
370
- # Layers to keep in bf16: embeddings, lm_head, norms, diff scalars, gates
371
  _FP8_SKIP = ("embed", "lm_head", "norm", "diff_lambda", "head_gate",
372
  "ls_a", "ls_f", "rope")
373
 
@@ -380,10 +393,7 @@ def _fp8_filter(module: nn.Module, fqn: str) -> bool:
380
 
381
 
382
  def apply_fp8(model: nn.Module, cfg: NovaConfig) -> nn.Module:
383
- """
384
- Apply FP8 training to eligible Linear layers.
385
- Must be called BEFORE weight tying so embed and lm_head stay bf16.
386
- """
387
  if not resolve_fp8(cfg):
388
  return model
389
  fp8_cfg = Float8LinearConfig.from_recipe_name("rowwise")
@@ -402,10 +412,8 @@ def apply_fp8(model: nn.Module, cfg: NovaConfig) -> nn.Module:
402
 
403
  class ShardStream:
404
  """
405
- Per-domain streaming iterator.
406
- Each DataLoader worker constructs its own ShardStream, so there is no
407
- shared state and no race conditions between workers.
408
- Re-scans the Hub every `refresh_every` rows to pick up new shards.
409
  """
410
 
411
  def __init__(self, domain: str, refresh_every: int = 100_000):
@@ -434,7 +442,6 @@ class ShardStream:
434
  def __iter__(self) -> Iterator[np.ndarray]:
435
  while True:
436
  if not self._queue:
437
- # Restart from full seen list and rescan for new shards
438
  q = list(self._seen)
439
  random.shuffle(q)
440
  self._queue = q
@@ -450,9 +457,8 @@ class ShardStream:
450
 
451
  class BalancedIterableDataset(IterableDataset):
452
  """
453
- Each DataLoader worker gets its own independent ShardStream per domain.
454
- No shared mutable state β€” fully safe with num_workers > 0.
455
- Yields equal numbers of tokens from each domain per buffer cycle.
456
  """
457
 
458
  def __init__(
@@ -469,7 +475,6 @@ class BalancedIterableDataset(IterableDataset):
469
  self.refresh_every = refresh_every
470
 
471
  def __iter__(self) -> Iterator[torch.Tensor]:
472
- # Each worker (or main process) builds its own streams independently
473
  streams = {
474
  d: iter(ShardStream(d, self.refresh_every))
475
  for d in self.domains
@@ -484,7 +489,6 @@ class BalancedIterableDataset(IterableDataset):
484
  random.shuffle(buf)
485
  for row in buf:
486
  tokens = row[: self.seq_len].astype(np.int64)
487
- # pad if shard rows are shorter than seq_len
488
  if len(tokens) < self.seq_len:
489
  tokens = np.pad(
490
  tokens,
@@ -507,15 +511,8 @@ def make_loaders(cfg: NovaConfig, local_rank: int, ws: int):
507
  val_ds = BalancedIterableDataset(
508
  cfg.domains, cfg.max_len, cfg.data_buf_size // 4, cfg.shard_refresh_rows
509
  )
510
- # IterableDataset does not use a sampler β€” worker splitting is handled
511
- # inside __iter__ via get_worker_info() if needed (currently each worker
512
- # draws independently, giving statistical balance across ranks).
513
- train_loader = DataLoader(
514
- train_ds, batch_size=cfg.micro_batch, **kw
515
- )
516
- val_loader = DataLoader(
517
- val_ds, batch_size=min(cfg.micro_batch, 4), **kw
518
- )
519
  return train_loader, val_loader
520
 
521
  # ══════════════════════════════════════════════════════════════════════════════
@@ -537,13 +534,8 @@ def precompute_rope(
537
  max_len: int,
538
  theta: float,
539
  device=None,
540
- ):
541
- """
542
- Extended-theta RoPE.
543
- Returns (cos, sin) each of shape (max_len, head_dim // 2).
544
- The caller is responsible for expanding to full head_dim via
545
- torch.cat([cos, cos], dim=-1).
546
- """
547
  inv_freq = 1.0 / (
548
  theta ** (
549
  torch.arange(0, head_dim, 2, dtype=torch.float32, device=device)
@@ -551,7 +543,7 @@ def precompute_rope(
551
  )
552
  )
553
  t = torch.arange(max_len, dtype=torch.float32, device=device)
554
- freqs = torch.outer(t, inv_freq) # (max_len, head_dim // 2)
555
  return torch.cos(freqs), torch.sin(freqs)
556
 
557
 
@@ -560,37 +552,25 @@ def apply_rope(
560
  k: torch.Tensor,
561
  cos: torch.Tensor,
562
  sin: torch.Tensor,
563
- ) -> tuple[torch.Tensor, torch.Tensor]:
564
  """
565
- Standard paired RoPE rotation.
566
-
567
- Args:
568
- q, k : (B, L, n_heads, head_dim)
569
- cos, sin: (L, head_dim // 2) β€” from precompute_rope
570
-
571
- The rotation maps [x0, x1, ..., x_{hd-2}, x_{hd-1}] to
572
- [-x_{hd/2}, ..., -x_{hd-1}, x_0, ..., x_{hd/2 - 1}].
573
- This is the standard "split-half" formulation used by LLaMA/Mistral.
574
- Works for any even head_dim, including non-powers-of-two.
575
  """
576
- L = q.shape[1]
577
- hd = q.shape[-1]
578
 
579
- # Expand half-dim cos/sin to full head_dim: (L, hd)
580
  cos_full = torch.cat([cos[:L], cos[:L]], dim=-1) # (L, hd)
581
  sin_full = torch.cat([sin[:L], sin[:L]], dim=-1) # (L, hd)
582
-
583
- # Broadcast over batch and heads: (1, L, 1, hd)
584
- cos_full = cos_full.unsqueeze(0).unsqueeze(2)
585
  sin_full = sin_full.unsqueeze(0).unsqueeze(2)
586
 
587
  def _rotate_half(x: torch.Tensor) -> torch.Tensor:
588
- """[-x2, x1] split at hd // 2."""
589
  half = x.shape[-1] // 2
590
  x1, x2 = x[..., :half], x[..., half:]
591
  return torch.cat([-x2, x1], dim=-1)
592
 
593
- # Upcast to float32 for numerical stability, then cast back
594
  qf = q.float()
595
  kf = k.float()
596
  q_rot = (qf * cos_full + _rotate_half(qf) * sin_full).to(q.dtype)
@@ -602,12 +582,11 @@ def apply_rope(
602
 
603
  class NovaAttention(nn.Module):
604
  """
605
- D1 β€” Fused QKV: single matmul for Q, K, V; split after projection.
606
- D2 οΏ½οΏ½ Differential Attention:
607
- Each head's head_dim is split into two half-heads (half = head_dim // 2).
608
- Two independent SDPA operations are run; their outputs are differenced
609
- with a learned per-head scalar lambda, then normed and gated.
610
- v is also split so each SDPA sees (q_half, k_half, v_half) β€” shapes match.
611
  """
612
 
613
  def __init__(self, cfg: NovaConfig):
@@ -615,24 +594,19 @@ class NovaAttention(nn.Module):
615
  self.nh = cfg.n_heads
616
  self.nkv = cfg.n_kv_heads
617
  self.hd = cfg.head_dim
618
- self.ng = cfg.n_heads // cfg.n_kv_heads # query groups per KV head
619
- self.half = cfg.head_dim // 2 # per-stream head dim
620
 
621
  q_dim = cfg.n_heads * cfg.head_dim
622
  kv_dim = cfg.n_kv_heads * cfg.head_dim
623
 
624
- # D1: fused QKV projection
625
  self.qkv = nn.Linear(cfg.d_model, q_dim + 2 * kv_dim, bias=False)
626
  self.o_proj = nn.Linear(q_dim, cfg.d_model, bias=False)
627
 
628
- # QK-Norm: stabilises attention logits at scale (applied per head)
629
  self.q_norm = RMSNorm(cfg.head_dim)
630
  self.k_norm = RMSNorm(cfg.head_dim)
631
-
632
- # Post-differential norm: operates on the recombined full head_dim output
633
  self.h_norm = RMSNorm(cfg.head_dim)
634
 
635
- # D2: per-head differential scalar and gate
636
  self.diff_lambda = nn.Parameter(
637
  torch.full((cfg.n_heads,), cfg.diff_lambda_init)
638
  )
@@ -652,51 +626,38 @@ class NovaAttention(nn.Module):
652
  ) -> torch.Tensor:
653
  B, L, D = x.shape
654
 
655
- # ── D1: fused QKV ────────────────────────────────────────────────
656
  qkv = self.qkv(x)
657
  q_dim = self.nh * self.hd
658
  kv_d = self.nkv * self.hd
659
  q, k, v = qkv.split([q_dim, kv_d, kv_d], dim=-1)
660
 
661
- # Reshape to (B, L, n_heads, head_dim)
662
  q = q.view(B, L, self.nh, self.hd)
663
  k = k.view(B, L, self.nkv, self.hd)
664
  v = v.view(B, L, self.nkv, self.hd)
665
 
666
- # QK-Norm before RoPE (over head_dim dimension)
667
  q = self.q_norm(q)
668
  k = self.k_norm(k)
669
 
670
- # RoPE
671
  q, k = apply_rope(q, k, cos, sin)
672
 
673
- # Transpose to (B, n_heads, L, head_dim) for SDPA
674
  q = q.transpose(1, 2)
675
- # GQA: repeat KV heads to match query head count
676
  k = k.transpose(1, 2).repeat_interleave(self.ng, dim=1)
677
  v = v.transpose(1, 2).repeat_interleave(self.ng, dim=1)
678
 
679
- # ── D2: differential attention ───────────────────────────────────
680
- # Split Q, K, V along head_dim into two equal streams.
681
- # Each SDPA sees (B, nh, L, half) for all three tensors β€” shapes match.
682
  q1, q2 = q[..., :self.half], q[..., self.half:]
683
  k1, k2 = k[..., :self.half], k[..., self.half:]
684
  v1, v2 = v[..., :self.half], v[..., self.half:]
685
 
686
- # Two independent causal attention computations
687
- out1 = F.scaled_dot_product_attention(q1, k1, v1, is_causal=True) # (B,nh,L,half)
688
- out2 = F.scaled_dot_product_attention(q2, k2, v2, is_causal=True) # (B,nh,L,half)
689
 
690
- # Learned per-head differential: out1 - lambda * out2
691
  lam = self.diff_lambda.view(1, self.nh, 1, 1).to(out1.dtype)
692
 
693
- # Concatenate the two streams back to full head_dim, then norm + gate
694
- # Shape: (B, nh, L, half) cat (B, nh, L, half) -> (B, nh, L, hd)
695
- out = torch.cat([out1, lam * out2], dim=-1)
696
  out = self.h_norm(out) * (1.0 - self.lambda_init)
697
  out = out * self.head_gate.view(1, self.nh, 1, 1).to(out.dtype)
698
 
699
- # Merge heads and project
700
  out = out.transpose(1, 2).contiguous().view(B, L, -1)
701
  return self.o_proj(out)
702
 
@@ -724,15 +685,12 @@ class SwiGLU(nn.Module):
724
  # ── D4 + D5: Nova Transformer Block ──────────────────────────────────────────
725
 
726
  class NovaBlock(nn.Module):
727
- """Pre-norm block with LayerScale (D5). ls_a / ls_f init = 0.1."""
728
-
729
  def __init__(self, cfg: NovaConfig):
730
  super().__init__()
731
  self.attn_norm = RMSNorm(cfg.d_model)
732
  self.ffn_norm = RMSNorm(cfg.d_model)
733
  self.attn = NovaAttention(cfg)
734
  self.ffn = SwiGLU(cfg)
735
- # D5: LayerScale β€” small init for deep-network stability
736
  self.ls_a = nn.Parameter(torch.full((cfg.d_model,), 0.1))
737
  self.ls_f = nn.Parameter(torch.full((cfg.d_model,), 0.1))
738
 
@@ -754,19 +712,14 @@ class Nova10BDense(nn.Module):
754
  super().__init__()
755
  self.cfg = cfg
756
 
757
- # Embedding and lm_head stay bf16 (excluded from FP8 by _fp8_filter)
758
  self.embed = nn.Embedding(cfg.vocab_size, cfg.d_model)
759
  self.layers = nn.ModuleList([NovaBlock(cfg) for _ in range(cfg.n_layers)])
760
  self.norm = RMSNorm(cfg.d_model)
761
  self.lm_head = nn.Linear(cfg.d_model, cfg.vocab_size, bias=False)
762
 
763
- # Weight tying is applied AFTER FP8 conversion (see build_model())
764
- # so both tensors remain nn.Parameter / bf16.
765
  self.softcap = cfg.logit_softcap
766
-
767
  nn.init.normal_(self.embed.weight, std=0.02)
768
 
769
- # RoPE buffers β€” recomputed from config, not saved in checkpoint
770
  cos, sin = precompute_rope(cfg.head_dim, cfg.max_len, cfg.rope_theta)
771
  self.register_buffer("rope_cos", cos, persistent=False)
772
  self.register_buffer("rope_sin", sin, persistent=False)
@@ -794,7 +747,6 @@ class Nova10BDense(nn.Module):
794
  x = layer(x, cos, sin)
795
 
796
  logits = self.lm_head(self.norm(x))
797
- # D7: logit soft-cap
798
  logits = self.softcap * torch.tanh(logits / self.softcap)
799
 
800
  if targets is None:
@@ -818,14 +770,15 @@ class Nova10BDense(nn.Module):
818
 
819
  def build_model(cfg: NovaConfig) -> nn.Module:
820
  """
821
- Correct build order:
822
  1. Create model on CPU
823
- 2. Apply FP8 (replaces eligible nn.Linear layers in-place)
824
- 3. Tie weights AFTER FP8 so both embed and lm_head stay bf16
 
 
825
  """
826
  model = Nova10BDense(cfg).to(cfg.dtype)
827
  model = apply_fp8(model, cfg)
828
- # Tie weights after FP8 β€” both remain nn.Linear / bf16 (skipped by _fp8_filter)
829
  model.lm_head.weight = model.embed.weight
830
  return model
831
 
@@ -833,9 +786,6 @@ def build_model(cfg: NovaConfig) -> nn.Module:
833
  # FSDP WRAPPING
834
  # ══════════════════════════════════════════════════════════════════════════════
835
 
836
- # State-dict configs:
837
- # SAVE: rank0_only=True β€” only rank-0 holds the full dict (saves memory)
838
- # LOAD: rank0_only=False β€” all ranks must participate in scatter
839
  _SAVE_SD_CFG = FullStateDictConfig(offload_to_cpu=True, rank0_only=True)
840
  _LOAD_SD_CFG = FullStateDictConfig(offload_to_cpu=True, rank0_only=False)
841
 
@@ -845,6 +795,27 @@ def wrap_fsdp(
845
  cfg: NovaConfig,
846
  device: torch.device,
847
  ) -> FSDP:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
848
  mp_policy = MixedPrecision(
849
  param_dtype=torch.bfloat16,
850
  reduce_dtype=torch.bfloat16,
@@ -856,18 +827,19 @@ def wrap_fsdp(
856
  )
857
  fsdp_model = FSDP(
858
  model,
859
- auto_wrap_policy = wrap_policy,
860
- mixed_precision = mp_policy,
861
- sharding_strategy = ShardingStrategy.FULL_SHARD,
862
- backward_prefetch = BackwardPrefetch.BACKWARD_PRE,
863
- cpu_offload = CPUOffload(offload_params=cfg.cpu_offload),
864
- device_id = device,
865
- use_orig_params = True, # required: Muon needs .grad on original params
866
  )
867
  if is_main():
868
  log.info(
869
- f"[fsdp] FULL_SHARD Γ— {world_size()} GPUs | "
870
- f"cpu_offload={cfg.cpu_offload} | bf16 mixed-precision"
 
871
  )
872
  return fsdp_model
873
 
@@ -893,11 +865,13 @@ def _newtonschulz5(G: torch.Tensor, steps: int = 5) -> torch.Tensor:
893
 
894
  class Muon(torch.optim.Optimizer):
895
  """
896
- Muon: SGD with Newton-Schulz orthogonalised momentum.
897
- Applied only to 2-D weight matrices.
898
- With FSDP use_orig_params=True, original param tensors are exposed but
899
- their .grad may be a sharded flat tensor during backward.
900
- We handle non-2-D grads gracefully with a plain momentum SGD fallback.
 
 
901
  """
902
 
903
  def __init__(
@@ -908,9 +882,19 @@ class Muon(torch.optim.Optimizer):
908
  weight_decay: float = 0.0,
909
  ns_steps: int = 5,
910
  ):
 
 
 
 
 
 
 
 
 
 
911
  defaults = dict(lr=lr, momentum=momentum,
912
  weight_decay=weight_decay, ns_steps=ns_steps)
913
- super().__init__(list(params), defaults)
914
 
915
  @torch.no_grad()
916
  def step(self):
@@ -926,10 +910,9 @@ class Muon(torch.optim.Optimizer):
926
  grad = p.grad
927
  state = self.state[p]
928
 
929
- # ── FSDP may expose sharded / flat grads during backward.
930
- # If grad is not 2-D, fall back to plain momentum SGD
931
- # so we never crash on the ndim==2 assertion. ────────
932
  if grad.ndim != 2:
 
 
933
  if "buf" not in state:
934
  state["buf"] = torch.zeros_like(grad)
935
  buf = state["buf"]
@@ -939,12 +922,11 @@ class Muon(torch.optim.Optimizer):
939
  p.add_(buf, alpha=-lr)
940
  continue
941
 
942
- # ── normal Muon path ─────────────────────────────────
943
  if "buf" not in state:
944
  state["buf"] = torch.zeros_like(grad)
945
  buf = state["buf"]
946
  buf.mul_(mom).add_(grad)
947
- grad_ns = grad.add(buf, alpha=mom) # Nesterov look-ahead
948
  u = _newtonschulz5(grad_ns, steps=ns)
949
  if wd > 0:
950
  p.mul_(1.0 - lr * wd)
@@ -952,30 +934,62 @@ class Muon(torch.optim.Optimizer):
952
  p.add_(u.to(p.dtype), alpha=-lr * scale)
953
 
954
 
 
 
 
 
 
 
 
 
 
 
 
 
955
  def build_optimizers(model: nn.Module, cfg: NovaConfig):
956
  """
957
- Split parameters:
958
- 2-D hidden matrices (qkv, o_proj, gate, up, down) -> Muon
959
- embed / lm_head (tied, 2-D but special) -> AdamW with decay
960
- 1-D (RMSNorm scales, ls_*, diff_lambda, head_gate) -> AdamW no-decay
 
 
 
 
 
 
 
 
 
961
  """
962
- muon_p = []
963
- aw_decay = []
964
- aw_nodecay = []
965
- seen: set = set()
966
 
967
  for name, p in model.named_parameters():
968
  if id(p) in seen or not p.requires_grad:
969
  continue
970
  seen.add(id(p))
 
971
  is_embed_or_head = ("embed" in name) or ("lm_head" in name)
972
- if p.ndim == 2 and not is_embed_or_head:
 
 
 
 
 
973
  muon_p.append(p)
974
- elif p.ndim >= 2:
975
  aw_decay.append(p)
976
  else:
977
  aw_nodecay.append(p)
978
 
 
 
 
 
 
 
 
 
979
  muon_opt = Muon(
980
  muon_p,
981
  lr=cfg.muon_lr,
@@ -1005,12 +1019,15 @@ def build_optimizers(model: nn.Module, cfg: NovaConfig):
1005
  fused=True,
1006
  )
1007
  if is_main():
1008
- log.warning("[optim] bitsandbytes not found β€” fused fp32 AdamW")
1009
 
1010
  if is_main():
1011
  nm = sum(p.numel() for p in muon_p)
1012
  na = sum(p.numel() for p in aw_decay + aw_nodecay)
1013
- log.info(f"[optim] Muon {nm / 1e6:.0f}M params | AdamW {na / 1e6:.0f}M params")
 
 
 
1014
 
1015
  return muon_opt, aw_opt
1016
 
@@ -1034,7 +1051,6 @@ def cosine_lr(
1034
  # ══════════════════════════════════════════════════════════════════════════════
1035
 
1036
  def _full_state_dict_save(model: FSDP) -> dict:
1037
- """Gather full state dict to rank-0 CPU only."""
1038
  with FSDP.state_dict_type(model, StateDictType.FULL_STATE_DICT, _SAVE_SD_CFG):
1039
  return model.state_dict()
1040
 
@@ -1049,8 +1065,8 @@ def save_ckpt(
1049
  val_loss: float,
1050
  tag: str,
1051
  ) -> bool:
1052
- barrier() # all ranks finish backward first
1053
- sd = _full_state_dict_save(model) # non-empty only on rank-0
1054
  if not is_main():
1055
  return True
1056
 
@@ -1130,12 +1146,8 @@ def load_ckpt(
1130
  aw_opt,
1131
  device: torch.device,
1132
  cfg: NovaConfig,
1133
- ) -> tuple[int, int, float]:
1134
- """
1135
- Resume from the latest checkpoint on the Hub.
1136
- All ranks must call this β€” FSDP scatters the state dict across GPUs.
1137
- Returns (step, epoch, best_val_loss).
1138
- """
1139
  try:
1140
  files = hf_api.list_repo_files(
1141
  repo_id=HF_MODEL_REPO,
@@ -1158,13 +1170,12 @@ def load_ckpt(
1158
 
1159
  sd = torch.load(mpath, map_location="cpu", weights_only=True)
1160
 
1161
- # LOAD config: rank0_only=False so all ranks receive weights
1162
  with FSDP.state_dict_type(
1163
  model, StateDictType.FULL_STATE_DICT, _LOAD_SD_CFG
1164
  ):
1165
  model.load_state_dict(sd, strict=False)
1166
 
1167
- barrier() # ensure all ranks finish scatter before optimizer load
1168
 
1169
  meta = torch.load(xpath, map_location="cpu", weights_only=False)
1170
  for opt, key in [(muon_opt, "muon"), (aw_opt, "adamw")]:
@@ -1174,7 +1185,6 @@ def load_ckpt(
1174
  except Exception as e:
1175
  log.warning(f"[ckpt] {key} optimizer not restored: {e}")
1176
 
1177
- # Restore schedule params from saved config
1178
  saved_cfg = meta.get("config", {})
1179
  for field_name in [
1180
  "total_steps", "warmup_steps",
@@ -1190,7 +1200,7 @@ def load_ckpt(
1190
 
1191
  except Exception as ex:
1192
  if is_main():
1193
- log.error(f"[ckpt] resume failed: {ex} β€” fresh start")
1194
  return 0, 0, float("inf")
1195
 
1196
  # ══════════════════════════════════════════════════════════════════════════════
@@ -1235,7 +1245,6 @@ def vram_str(dev: int = 0) -> str:
1235
  # ══════════════════════════════════════════════════════════════════════════════
1236
 
1237
  def train():
1238
- # ── distributed init ────────────────────────────────────────────────────
1239
  local_rank = setup_dist()
1240
  device = torch.device(f"cuda:{local_rank}")
1241
  ws = world_size()
@@ -1244,14 +1253,11 @@ def train():
1244
  np.random.seed(42 + rank())
1245
  random.seed(42 + rank())
1246
 
1247
- # HF login on all ranks (data loading happens on every rank)
1248
  init_hf()
1249
  barrier()
1250
 
1251
- # ── config ──────────────────────────────────────────────────────────────
1252
  cfg = NovaConfig()
1253
 
1254
- # Scale total_steps to target ~200B tokens regardless of GPU count
1255
  target_tokens = 200_000_000_000
1256
  tokens_per_step = cfg.effective_batch_tokens(ws)
1257
  cfg.total_steps = max(
@@ -1261,11 +1267,10 @@ def train():
1261
  if is_main():
1262
  log.info(
1263
  f"[schedule] {ws} GPUs | {tokens_per_step / 1e6:.1f}M tok/step | "
1264
- f"{cfg.total_steps} steps β‰ˆ "
1265
  f"{cfg.total_steps * tokens_per_step / 1e9:.0f}B tokens"
1266
  )
1267
 
1268
- # Pull vocab_size from dataset metadata if available (rank-0 only)
1269
  if is_main():
1270
  try:
1271
  meta_p = hf_download_dataset("metadata.json")
@@ -1275,23 +1280,20 @@ def train():
1275
  cfg.vocab_size = (actual + 63) // 64 * 64
1276
  log.info(f"[data] vocab {actual} -> {cfg.vocab_size}")
1277
  except Exception as e:
1278
- log.info(f"[data] no metadata.json ({e}) β€” default vocab={cfg.vocab_size}")
1279
 
1280
- # Broadcast vocab_size to all ranks
1281
  vs = torch.tensor(cfg.vocab_size, device=device)
1282
  dist.broadcast(vs, src=0)
1283
  cfg.vocab_size = int(vs.item())
1284
 
1285
- # ── model ────────────────────────────────────────────────────────────────
1286
  if is_main():
1287
- log.info("[model] building Nova-10B-Dense …")
1288
 
1289
  with torch.device("cpu"):
1290
- model = build_model(cfg) # FP8 applied before weight tying
1291
 
1292
  model = wrap_fsdp(model, cfg, device)
1293
 
1294
- # Optional torch.compile β€” substantial throughput gain on Blackwell
1295
  if cfg.use_compile and torch.__version__ >= "2.7.0":
1296
  model = torch.compile(model, mode="max-autotune", fullgraph=False)
1297
  if is_main():
@@ -1300,10 +1302,8 @@ def train():
1300
  if is_main():
1301
  log.info(f"[model] VRAM after wrap: {vram_str(local_rank)}")
1302
 
1303
- # ── optimizers ──────────────────────────────────────────────────────────
1304
  muon_opt, aw_opt = build_optimizers(model, cfg)
1305
 
1306
- # ── resume ──────────────────────────────────────────────────────────────
1307
  start_step, start_epoch, best_val = 0, 0, float("inf")
1308
  try:
1309
  start_step, start_epoch, best_val = load_ckpt(
@@ -1311,23 +1311,20 @@ def train():
1311
  )
1312
  except Exception as e:
1313
  if is_main():
1314
- log.error(f"[ckpt] resume error: {e} β€” fresh start")
1315
 
1316
- # Broadcast step / epoch so all ranks agree
1317
  info = torch.tensor([start_step, start_epoch], dtype=torch.long, device=device)
1318
  dist.broadcast(info, src=0)
1319
  start_step, start_epoch = int(info[0]), int(info[1])
1320
 
1321
- # ── data ────────────────────────────────────────────────────────────────
1322
  train_loader, val_loader = make_loaders(cfg, rank(), ws)
1323
  train_iter = itertools.cycle(train_loader)
1324
 
1325
- # ── summary ─────────────────────────────────────────────────────────────
1326
  if is_main():
1327
  log.info("=" * 72)
1328
  log.info(
1329
- f"Nova-10B-Dense | Blackwell Γ— {ws} | "
1330
- f"FP8={resolve_fp8(cfg)} | FSDP FULL_SHARD | "
1331
  f"Muon + {'AdamW-8bit' if HAS_BNB else 'AdamW-fp32'}"
1332
  )
1333
  log.info(
@@ -1336,13 +1333,13 @@ def train():
1336
  f"ffn_h={cfg.ffn_hidden} max_len={cfg.max_len}"
1337
  )
1338
  log.info(
1339
- f" micro={cfg.micro_batch} Γ— accum={cfg.grad_accum} Γ— "
1340
- f"{ws} GPUs Γ— seq={cfg.max_len} = "
1341
  f"{tokens_per_step / 1e6:.1f}M tok/step"
1342
  )
1343
  log.info(
1344
  f" {cfg.total_steps} steps -> "
1345
- f"β‰ˆ{cfg.total_steps * tokens_per_step / 1e9:.0f}B tokens"
1346
  )
1347
  log.info(
1348
  f" muon_lr={cfg.muon_lr} aw_lr={cfg.lr} "
@@ -1368,13 +1365,11 @@ def train():
1368
 
1369
  while step < cfg.total_steps:
1370
 
1371
- # ── budget guard ────────────────────────────────────────────────────
1372
  if elapsed_h() >= cfg.max_hours:
1373
  if is_main():
1374
- log.info(f"[time] {cfg.max_hours}h budget exhausted β€” stopping")
1375
  break
1376
 
1377
- # ── fetch batch ─────────────────────────────────────────────────────
1378
  try:
1379
  batch = next(train_iter)
1380
  except Exception as e:
@@ -1384,7 +1379,6 @@ def train():
1384
 
1385
  batch = batch.to(device, non_blocking=True)
1386
 
1387
- # ── ctx-warmup: broadcast length from rank-0 so all ranks agree ────
1388
  ctx_raw = cfg.current_ctx_len(step)
1389
  ctx_t = torch.tensor(ctx_raw, device=device)
1390
  dist.broadcast(ctx_t, src=0)
@@ -1392,7 +1386,6 @@ def train():
1392
  if ctx < batch.shape[1]:
1393
  batch = batch[:, :ctx]
1394
 
1395
- # ── forward + backward ──────────────────────────────────────────────
1396
  try:
1397
  with torch.amp.autocast("cuda", dtype=cfg.dtype):
1398
  _, loss = model(batch, targets=batch)
@@ -1405,13 +1398,12 @@ def train():
1405
  aw_opt.zero_grad(set_to_none=True)
1406
  accum_loss = accum_n = 0
1407
  if is_main():
1408
- log.error("[oom] step skipped β€” consider reducing micro_batch")
1409
  continue
1410
 
1411
  if accum_n < cfg.grad_accum:
1412
  continue
1413
 
1414
- # ── optimizer step ──────────────────────────────────────────────────
1415
  mlr = cosine_lr(step, cfg.warmup_steps, cfg.total_steps,
1416
  cfg.muon_lr, cfg.muon_min_lr)
1417
  alr = cosine_lr(step, cfg.warmup_steps, cfg.total_steps,
@@ -1434,7 +1426,6 @@ def train():
1434
  accum_loss = accum_n = 0
1435
  step += 1
1436
 
1437
- # ── logging ─────────────────────────────────────────────────────────
1438
  if is_main() and step % cfg.log_every == 0:
1439
  sps = (step - start_step) / max(time.time() - t_start, 1e-6)
1440
  tps = sps * tokens_per_step
@@ -1452,7 +1443,6 @@ def train():
1452
  f" vram {vram_str(local_rank)}"
1453
  )
1454
 
1455
- # ── validation ──────────────────────────────────────────────────────
1456
  if step % cfg.eval_every == 0:
1457
  vl = run_val(model, val_loader, device, n=80)
1458
  vpp = math.exp(min(vl, 20))
@@ -1466,7 +1456,6 @@ def train():
1466
  )
1467
  log.info(" >> new best checkpoint saved")
1468
 
1469
- # ── periodic checkpoint ─────────────────────────────────────────────
1470
  if step % cfg.ckpt_every == 0:
1471
  save_ckpt(
1472
  model, muon_opt, aw_opt,
@@ -1475,7 +1464,6 @@ def train():
1475
  tag="ckpt",
1476
  )
1477
 
1478
- # ── 30-min heartbeat checkpoint ─────────────────────────────────────
1479
  if time.time() - t_last_ckpt > 1800:
1480
  save_ckpt(
1481
  model, muon_opt, aw_opt,
@@ -1485,10 +1473,9 @@ def train():
1485
  )
1486
  t_last_ckpt = time.time()
1487
 
1488
- # ── final ────────────────────────────────────────────────────────────────
1489
  if is_main():
1490
  log.info("=" * 72)
1491
- log.info("TRAINING COMPLETE β€” saving final checkpoint …")
1492
 
1493
  fval = run_val(model, val_loader, device, n=200)
1494
  barrier()
 
1
  #!/usr/bin/env python3
2
  """
3
+ Nova-10B-Dense Β· HuggingFace Jobs Β· 4-8x RTX PRO 6000 Blackwell
4
  ~10B fully-dense transformer (no MoE, no routing, no MoD)
5
 
6
  Architecture:
7
+ D1 Fused QKV - single Linear for Q+K+V, split after projection
8
+ D2 Differential Attn - dual-stream diff attn + QK-Norm + head-gate (GQA/RoPE)
9
+ D3 SwiGLU FFN - dense gated MLP, no routing
10
+ D4 RMSNorm - pre-norm on attn + FFN
11
+ D5 LayerScale - per-layer learnable residual scale (0.1 init)
12
+ D6 RoPE - extended-theta (500k) rotary embeddings
13
+ D7 Logit soft-cap - tanh(x/cap)*cap on final logits
14
+ D8 FP8 - torchao rowwise (Blackwell sm_100)
15
 
16
  Training stack:
17
+ O1 Muon - Newton-Schulz orthogonalized momentum for 2-D weights
18
+ O2 AdamW-8bit - bitsandbytes, for embed/norms/1-D params
19
+ O3 FSDP2 SHARD_GRAD_OP (ZeRO-2) - grads + optimizer state sharded,
20
+ FULL params replicated per GPU. Required because
21
+ Muon's Newton-Schulz needs genuine full 2-D weight
22
+ matrices at optimizer-step time; FULL_SHARD
23
+ (ZeRO-3) only exposes 1-D local shards and
24
+ silently disables Muon.
25
+ O4 Cosine LR - separate schedules for Muon vs AdamW
26
+ D9 Balanced streaming - equal tokens per domain, new shards auto-detected
27
+ D10 ctx-warmup - sequence-length curriculum (1024 -> 8192)
28
 
29
  Launch:
30
  torchrun --nproc_per_node=4 nova10b_dense.py
 
33
 
34
  # ── stdlib ──────────────────────────────────────────────────────────────────
35
  import os
 
36
  import re
37
  import json
38
  import time
 
42
  import itertools
43
  import functools
44
  from dataclasses import dataclass, asdict, field
45
+ from typing import Optional, List, Iterator, Tuple
46
 
47
  # ── third-party ─────────────────────────────────────────────────────────────
48
  import numpy as np
 
90
  torch.backends.cudnn.allow_tf32 = True
91
  torch.backends.cudnn.benchmark = True
92
 
93
+ # ── logging β€” rank-0 gets INFO, workers get ERROR only (prevents spam) ──────
94
+
95
  class _RankFilter(logging.Filter):
96
  def filter(self, record):
97
  record.rank = dist.get_rank() if dist.is_initialized() else 0
 
99
 
100
  _rank_filter = _RankFilter()
101
 
102
+ _local_rank_for_log = int(os.environ.get("LOCAL_RANK", "0"))
103
+ _log_level = logging.INFO if _local_rank_for_log == 0 else logging.ERROR
104
+
105
  logging.basicConfig(
106
+ level=_log_level,
107
  format="%(asctime)s | R%(rank)s | %(message)s",
108
  datefmt="%H:%M:%S",
109
  )
 
110
  for _h in logging.root.handlers:
111
  _h.addFilter(_rank_filter)
112
 
113
  log = logging.getLogger("nova10b")
114
  log.addFilter(_rank_filter)
115
 
116
+ if _local_rank_for_log != 0:
117
+ logging.getLogger("huggingface_hub").setLevel(logging.WARNING)
118
+ logging.getLogger("filelock").setLevel(logging.WARNING)
119
+ logging.getLogger("urllib3").setLevel(logging.WARNING)
120
+ logging.getLogger("requests").setLevel(logging.WARNING)
121
+
122
  # ══════════════════════════════════════════════════════════════════════════════
123
  # DISTRIBUTED HELPERS
124
  # ═════════════════��════════════════════════════════════════════════════════════
125
 
126
  def setup_dist() -> int:
127
+ """set_device BEFORE init_process_group avoids the 'device unknown' NCCL warning."""
128
  local_rank = int(os.environ.get("LOCAL_RANK", 0))
129
  torch.cuda.set_device(local_rank)
130
+ dist.init_process_group(backend="nccl")
131
  return local_rank
132
 
133
  def cleanup_dist():
134
+ if dist.is_initialized():
135
+ dist.destroy_process_group()
136
 
137
  def is_main() -> bool: return rank() == 0
138
  def rank() -> int: return dist.get_rank() if dist.is_initialized() else 0
 
143
  dist.barrier()
144
 
145
  def all_reduce_mean(tensor: torch.Tensor) -> torch.Tensor:
146
+ """SUM + div for universal NCCL compatibility (ReduceOp.AVG not always supported)."""
147
  dist.all_reduce(tensor, op=dist.ReduceOp.SUM)
148
  tensor.div_(world_size())
149
  return tensor
 
190
  token=HF_TOKEN,
191
  )
192
  if is_main():
193
+ log.info(f"[hf] uploaded {remote}")
194
  return True
195
  except Exception as e:
196
  if is_main():
 
243
 
244
 
245
  def _ckpt_step(fname: str) -> int:
246
+ """Extract step number from checkpoint filename robustly via regex."""
247
  m = re.search(r"_s(\d+)\.", fname)
248
  return int(m.group(1)) if m else 0
249
 
 
273
  )
274
  except Exception:
275
  pass
276
+ log.info(f"[hf] rotated out {victim}")
277
  except Exception as e:
278
  log.warning(f"[hf] rotate failed: {e}")
279
 
 
292
  max_len: int = 8192
293
  ffn_hidden: int = 14336
294
  diff_lambda_init: float = 0.8
295
+ logit_softcap: float = 50.0
296
  rope_theta: float = 500_000.0
297
 
298
  # ── FP8 ────────────────────────────────────────────────────────────────
299
+ use_fp8: str = "auto" # "auto" | "on" | "off"
300
 
301
  # ── optimiser ──────────────────────────────────────────────────────────
302
  lr: float = 2e-4
 
371
  return False
372
  ok = HAS_TORCHAO and _is_blackwell()
373
  if cfg.use_fp8 == "on" and not ok:
374
+ log.warning("[fp8] forced ON but torchao missing or GPU < sm_100 - bf16 fallback")
375
  return False
376
  if cfg.use_fp8 == "auto" and is_main():
377
  log.info(
378
+ f"[fp8] auto -> torchao={HAS_TORCHAO} "
379
+ f"blackwell={_is_blackwell()} -> {'ON' if ok else 'OFF'}"
380
  )
381
  return ok
382
 
383
 
 
384
  _FP8_SKIP = ("embed", "lm_head", "norm", "diff_lambda", "head_gate",
385
  "ls_a", "ls_f", "rope")
386
 
 
393
 
394
 
395
  def apply_fp8(model: nn.Module, cfg: NovaConfig) -> nn.Module:
396
+ """Must be called BEFORE weight tying so embed/lm_head stay untouched bf16."""
 
 
 
397
  if not resolve_fp8(cfg):
398
  return model
399
  fp8_cfg = Float8LinearConfig.from_recipe_name("rowwise")
 
412
 
413
  class ShardStream:
414
  """
415
+ Per-domain streaming iterator. Each DataLoader worker constructs its own
416
+ instance, so there is no shared mutable state between workers.
 
 
417
  """
418
 
419
  def __init__(self, domain: str, refresh_every: int = 100_000):
 
442
  def __iter__(self) -> Iterator[np.ndarray]:
443
  while True:
444
  if not self._queue:
 
445
  q = list(self._seen)
446
  random.shuffle(q)
447
  self._queue = q
 
457
 
458
  class BalancedIterableDataset(IterableDataset):
459
  """
460
+ Each DataLoader worker builds its own independent ShardStream per domain
461
+ inside __iter__ β€” no shared state, no race conditions with num_workers>0.
 
462
  """
463
 
464
  def __init__(
 
475
  self.refresh_every = refresh_every
476
 
477
  def __iter__(self) -> Iterator[torch.Tensor]:
 
478
  streams = {
479
  d: iter(ShardStream(d, self.refresh_every))
480
  for d in self.domains
 
489
  random.shuffle(buf)
490
  for row in buf:
491
  tokens = row[: self.seq_len].astype(np.int64)
 
492
  if len(tokens) < self.seq_len:
493
  tokens = np.pad(
494
  tokens,
 
511
  val_ds = BalancedIterableDataset(
512
  cfg.domains, cfg.max_len, cfg.data_buf_size // 4, cfg.shard_refresh_rows
513
  )
514
+ train_loader = DataLoader(train_ds, batch_size=cfg.micro_batch, **kw)
515
+ val_loader = DataLoader(val_ds, batch_size=min(cfg.micro_batch, 4), **kw)
 
 
 
 
 
 
 
516
  return train_loader, val_loader
517
 
518
  # ══════════════════════════════════════════════════════════════════════════════
 
534
  max_len: int,
535
  theta: float,
536
  device=None,
537
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
538
+ """Returns (cos, sin), each shape (max_len, head_dim // 2)."""
 
 
 
 
 
539
  inv_freq = 1.0 / (
540
  theta ** (
541
  torch.arange(0, head_dim, 2, dtype=torch.float32, device=device)
 
543
  )
544
  )
545
  t = torch.arange(max_len, dtype=torch.float32, device=device)
546
+ freqs = torch.outer(t, inv_freq)
547
  return torch.cos(freqs), torch.sin(freqs)
548
 
549
 
 
552
  k: torch.Tensor,
553
  cos: torch.Tensor,
554
  sin: torch.Tensor,
555
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
556
  """
557
+ Standard split-half RoPE (LLaMA/Mistral style).
558
+ q, k : (B, L, n_heads, head_dim)
559
+ cos, sin: (L, head_dim // 2)
560
+ Works correctly for any even head_dim.
 
 
 
 
 
 
561
  """
562
+ L = q.shape[1]
 
563
 
 
564
  cos_full = torch.cat([cos[:L], cos[:L]], dim=-1) # (L, hd)
565
  sin_full = torch.cat([sin[:L], sin[:L]], dim=-1) # (L, hd)
566
+ cos_full = cos_full.unsqueeze(0).unsqueeze(2) # (1, L, 1, hd)
 
 
567
  sin_full = sin_full.unsqueeze(0).unsqueeze(2)
568
 
569
  def _rotate_half(x: torch.Tensor) -> torch.Tensor:
 
570
  half = x.shape[-1] // 2
571
  x1, x2 = x[..., :half], x[..., half:]
572
  return torch.cat([-x2, x1], dim=-1)
573
 
 
574
  qf = q.float()
575
  kf = k.float()
576
  q_rot = (qf * cos_full + _rotate_half(qf) * sin_full).to(q.dtype)
 
582
 
583
  class NovaAttention(nn.Module):
584
  """
585
+ D1 - Fused QKV: single matmul for Q, K, V; split after projection.
586
+ D2 - Differential Attention: head_dim split into two half-streams;
587
+ Q, K, AND V are all split so each SDPA call sees matching shapes.
588
+ Outputs recombined via concat (not subtraction) after per-head
589
+ lambda scaling of the second stream, then normed and gated.
 
590
  """
591
 
592
  def __init__(self, cfg: NovaConfig):
 
594
  self.nh = cfg.n_heads
595
  self.nkv = cfg.n_kv_heads
596
  self.hd = cfg.head_dim
597
+ self.ng = cfg.n_heads // cfg.n_kv_heads
598
+ self.half = cfg.head_dim // 2
599
 
600
  q_dim = cfg.n_heads * cfg.head_dim
601
  kv_dim = cfg.n_kv_heads * cfg.head_dim
602
 
 
603
  self.qkv = nn.Linear(cfg.d_model, q_dim + 2 * kv_dim, bias=False)
604
  self.o_proj = nn.Linear(q_dim, cfg.d_model, bias=False)
605
 
 
606
  self.q_norm = RMSNorm(cfg.head_dim)
607
  self.k_norm = RMSNorm(cfg.head_dim)
 
 
608
  self.h_norm = RMSNorm(cfg.head_dim)
609
 
 
610
  self.diff_lambda = nn.Parameter(
611
  torch.full((cfg.n_heads,), cfg.diff_lambda_init)
612
  )
 
626
  ) -> torch.Tensor:
627
  B, L, D = x.shape
628
 
 
629
  qkv = self.qkv(x)
630
  q_dim = self.nh * self.hd
631
  kv_d = self.nkv * self.hd
632
  q, k, v = qkv.split([q_dim, kv_d, kv_d], dim=-1)
633
 
 
634
  q = q.view(B, L, self.nh, self.hd)
635
  k = k.view(B, L, self.nkv, self.hd)
636
  v = v.view(B, L, self.nkv, self.hd)
637
 
 
638
  q = self.q_norm(q)
639
  k = self.k_norm(k)
640
 
 
641
  q, k = apply_rope(q, k, cos, sin)
642
 
 
643
  q = q.transpose(1, 2)
 
644
  k = k.transpose(1, 2).repeat_interleave(self.ng, dim=1)
645
  v = v.transpose(1, 2).repeat_interleave(self.ng, dim=1)
646
 
647
+ # Split Q, K, V along head_dim -> two matching-shape streams
 
 
648
  q1, q2 = q[..., :self.half], q[..., self.half:]
649
  k1, k2 = k[..., :self.half], k[..., self.half:]
650
  v1, v2 = v[..., :self.half], v[..., self.half:]
651
 
652
+ out1 = F.scaled_dot_product_attention(q1, k1, v1, is_causal=True)
653
+ out2 = F.scaled_dot_product_attention(q2, k2, v2, is_causal=True)
 
654
 
 
655
  lam = self.diff_lambda.view(1, self.nh, 1, 1).to(out1.dtype)
656
 
657
+ out = torch.cat([out1, lam * out2], dim=-1) # (B, nh, L, hd)
 
 
658
  out = self.h_norm(out) * (1.0 - self.lambda_init)
659
  out = out * self.head_gate.view(1, self.nh, 1, 1).to(out.dtype)
660
 
 
661
  out = out.transpose(1, 2).contiguous().view(B, L, -1)
662
  return self.o_proj(out)
663
 
 
685
  # ── D4 + D5: Nova Transformer Block ──────────────────────────────────────────
686
 
687
  class NovaBlock(nn.Module):
 
 
688
  def __init__(self, cfg: NovaConfig):
689
  super().__init__()
690
  self.attn_norm = RMSNorm(cfg.d_model)
691
  self.ffn_norm = RMSNorm(cfg.d_model)
692
  self.attn = NovaAttention(cfg)
693
  self.ffn = SwiGLU(cfg)
 
694
  self.ls_a = nn.Parameter(torch.full((cfg.d_model,), 0.1))
695
  self.ls_f = nn.Parameter(torch.full((cfg.d_model,), 0.1))
696
 
 
712
  super().__init__()
713
  self.cfg = cfg
714
 
 
715
  self.embed = nn.Embedding(cfg.vocab_size, cfg.d_model)
716
  self.layers = nn.ModuleList([NovaBlock(cfg) for _ in range(cfg.n_layers)])
717
  self.norm = RMSNorm(cfg.d_model)
718
  self.lm_head = nn.Linear(cfg.d_model, cfg.vocab_size, bias=False)
719
 
 
 
720
  self.softcap = cfg.logit_softcap
 
721
  nn.init.normal_(self.embed.weight, std=0.02)
722
 
 
723
  cos, sin = precompute_rope(cfg.head_dim, cfg.max_len, cfg.rope_theta)
724
  self.register_buffer("rope_cos", cos, persistent=False)
725
  self.register_buffer("rope_sin", sin, persistent=False)
 
747
  x = layer(x, cos, sin)
748
 
749
  logits = self.lm_head(self.norm(x))
 
750
  logits = self.softcap * torch.tanh(logits / self.softcap)
751
 
752
  if targets is None:
 
770
 
771
  def build_model(cfg: NovaConfig) -> nn.Module:
772
  """
773
+ Build order matters:
774
  1. Create model on CPU
775
+ 2. Apply FP8 (replaces eligible nn.Linear in-place)
776
+ 3. Tie lm_head.weight = embed.weight AFTER FP8 conversion, so both
777
+ stay plain bf16 nn.Linear/Embedding (both are in _FP8_SKIP anyway,
778
+ but this order also guards against future skip-list edits).
779
  """
780
  model = Nova10BDense(cfg).to(cfg.dtype)
781
  model = apply_fp8(model, cfg)
 
782
  model.lm_head.weight = model.embed.weight
783
  return model
784
 
 
786
  # FSDP WRAPPING
787
  # ══════════════════════════════════════════════════════════════════════════════
788
 
 
 
 
789
  _SAVE_SD_CFG = FullStateDictConfig(offload_to_cpu=True, rank0_only=True)
790
  _LOAD_SD_CFG = FullStateDictConfig(offload_to_cpu=True, rank0_only=False)
791
 
 
795
  cfg: NovaConfig,
796
  device: torch.device,
797
  ) -> FSDP:
798
+ """
799
+ Uses SHARD_GRAD_OP (ZeRO-2), NOT FULL_SHARD (ZeRO-3).
800
+
801
+ WHY THIS MATTERS: Muon's Newton-Schulz orthogonalization operates on
802
+ genuine 2-D weight matrices. Under FULL_SHARD, each rank only ever
803
+ holds a 1-D flat LOCAL SHARD of every parameter except during the
804
+ brief window of an active forward/backward all-gather. Since
805
+ build_optimizers() runs once, before any forward pass, AND
806
+ Muon.step() runs after backward (when params have been re-sharded
807
+ back to 1-D), FULL_SHARD would either:
808
+ (a) crash immediately β€” optimizer sees ndim==1 params and empty
809
+ muon_p list at construction time, or
810
+ (b) silently reduce Muon to plain momentum-SGD every step (if using
811
+ the ndim-fallback safety net), defeating the entire point of
812
+ using Muon.
813
+
814
+ SHARD_GRAD_OP keeps full-shape parameters replicated on every GPU
815
+ (only gradients + optimizer state are sharded), so Muon always sees
816
+ real 2-D matrices. Memory cost: ~18GB/GPU for a 9B-param bf16 model,
817
+ which is trivial on 96GB RTX PRO 6000 cards.
818
+ """
819
  mp_policy = MixedPrecision(
820
  param_dtype=torch.bfloat16,
821
  reduce_dtype=torch.bfloat16,
 
827
  )
828
  fsdp_model = FSDP(
829
  model,
830
+ auto_wrap_policy = wrap_policy,
831
+ mixed_precision = mp_policy,
832
+ sharding_strategy = ShardingStrategy.SHARD_GRAD_OP, # ZeRO-2, not ZeRO-3
833
+ backward_prefetch = BackwardPrefetch.BACKWARD_PRE,
834
+ cpu_offload = CPUOffload(offload_params=cfg.cpu_offload),
835
+ device_id = device,
836
+ use_orig_params = True,
837
  )
838
  if is_main():
839
  log.info(
840
+ f"[fsdp] SHARD_GRAD_OP (ZeRO-2) x {world_size()} GPUs | "
841
+ f"cpu_offload={cfg.cpu_offload} | bf16 mixed-precision | "
842
+ f"(full params replicated - required by Muon)"
843
  )
844
  return fsdp_model
845
 
 
865
 
866
  class Muon(torch.optim.Optimizer):
867
  """
868
+ Muon: SGD with Newton-Schulz orthogonalised momentum, for 2-D weights.
869
+
870
+ With SHARD_GRAD_OP + use_orig_params=True, gradients on 2-D params
871
+ should also be genuine 2-D tensors (matching the replicated full
872
+ parameter shape). We still keep a defensive fallback to plain
873
+ momentum-SGD for any unexpected non-2-D grad, so a shape surprise
874
+ degrades gracefully instead of crashing training.
875
  """
876
 
877
  def __init__(
 
882
  weight_decay: float = 0.0,
883
  ns_steps: int = 5,
884
  ):
885
+ params = list(params)
886
+ if len(params) == 0:
887
+ raise ValueError(
888
+ "Muon received an empty parameter list. This almost always "
889
+ "means parameter classification ran on a model whose "
890
+ "params were not yet in their true (full) shape β€” check "
891
+ "that build_optimizers() classifies by NAME, not by "
892
+ "p.ndim, and that FSDP sharding_strategy is SHARD_GRAD_OP "
893
+ "(not FULL_SHARD)."
894
+ )
895
  defaults = dict(lr=lr, momentum=momentum,
896
  weight_decay=weight_decay, ns_steps=ns_steps)
897
+ super().__init__(params, defaults)
898
 
899
  @torch.no_grad()
900
  def step(self):
 
910
  grad = p.grad
911
  state = self.state[p]
912
 
 
 
 
913
  if grad.ndim != 2:
914
+ # Defensive fallback - should not normally trigger
915
+ # under SHARD_GRAD_OP, but keeps training alive if it does.
916
  if "buf" not in state:
917
  state["buf"] = torch.zeros_like(grad)
918
  buf = state["buf"]
 
922
  p.add_(buf, alpha=-lr)
923
  continue
924
 
 
925
  if "buf" not in state:
926
  state["buf"] = torch.zeros_like(grad)
927
  buf = state["buf"]
928
  buf.mul_(mom).add_(grad)
929
+ grad_ns = grad.add(buf, alpha=mom)
930
  u = _newtonschulz5(grad_ns, steps=ns)
931
  if wd > 0:
932
  p.mul_(1.0 - lr * wd)
 
934
  p.add_(u.to(p.dtype), alpha=-lr * scale)
935
 
936
 
937
+ # Name substrings identifying the 2-D hidden weight matrices that should
938
+ # receive Muon updates. Everything else (embed, lm_head, norms, gates,
939
+ # LayerScale) goes to AdamW.
940
+ _MUON_NAME_PATTERNS = (
941
+ "qkv.weight",
942
+ "o_proj.weight",
943
+ "gate.weight",
944
+ "up.weight",
945
+ "down.weight",
946
+ )
947
+
948
+
949
  def build_optimizers(model: nn.Module, cfg: NovaConfig):
950
  """
951
+ Parameter classification is done PURELY BY NAME, never by p.ndim.
952
+
953
+ WHY: this function is called once, right after wrap_fsdp(), BEFORE any
954
+ forward/backward pass has run. Under FSDP (any sharding strategy that
955
+ shards parameters), a parameter's exposed .shape / .ndim at this point
956
+ may not reflect its true logical shape - e.g. under FULL_SHARD every
957
+ parameter reports ndim==1 (flat local shard) until the first unshard().
958
+ Relying on p.ndim here would silently misclassify every 2-D weight
959
+ matrix as 1-D, starving Muon of all its parameters and crashing with
960
+ "optimizer got an empty parameter list".
961
+
962
+ Classifying by parameter NAME (which is stable regardless of sharding
963
+ state) avoids this entirely.
964
  """
965
+ muon_p, aw_decay, aw_nodecay, seen = [], [], [], set()
 
 
 
966
 
967
  for name, p in model.named_parameters():
968
  if id(p) in seen or not p.requires_grad:
969
  continue
970
  seen.add(id(p))
971
+
972
  is_embed_or_head = ("embed" in name) or ("lm_head" in name)
973
+ is_muon_target = (
974
+ any(pat in name for pat in _MUON_NAME_PATTERNS)
975
+ and not is_embed_or_head
976
+ )
977
+
978
+ if is_muon_target:
979
  muon_p.append(p)
980
+ elif is_embed_or_head:
981
  aw_decay.append(p)
982
  else:
983
  aw_nodecay.append(p)
984
 
985
+ if not muon_p:
986
+ raise RuntimeError(
987
+ "[optim] Muon parameter list is empty after NAME-based "
988
+ "classification. This means NovaAttention/SwiGLU submodule "
989
+ "attribute names no longer match _MUON_NAME_PATTERNS "
990
+ f"{_MUON_NAME_PATTERNS}. Check for renamed layers."
991
+ )
992
+
993
  muon_opt = Muon(
994
  muon_p,
995
  lr=cfg.muon_lr,
 
1019
  fused=True,
1020
  )
1021
  if is_main():
1022
+ log.warning("[optim] bitsandbytes not found - fused fp32 AdamW")
1023
 
1024
  if is_main():
1025
  nm = sum(p.numel() for p in muon_p)
1026
  na = sum(p.numel() for p in aw_decay + aw_nodecay)
1027
+ log.info(
1028
+ f"[optim] Muon {nm / 1e6:.0f}M params ({len(muon_p)} tensors) | "
1029
+ f"AdamW {na / 1e6:.0f}M params ({len(aw_decay) + len(aw_nodecay)} tensors)"
1030
+ )
1031
 
1032
  return muon_opt, aw_opt
1033
 
 
1051
  # ══════════════════════════════════════════════════════════════════════════════
1052
 
1053
  def _full_state_dict_save(model: FSDP) -> dict:
 
1054
  with FSDP.state_dict_type(model, StateDictType.FULL_STATE_DICT, _SAVE_SD_CFG):
1055
  return model.state_dict()
1056
 
 
1065
  val_loss: float,
1066
  tag: str,
1067
  ) -> bool:
1068
+ barrier()
1069
+ sd = _full_state_dict_save(model)
1070
  if not is_main():
1071
  return True
1072
 
 
1146
  aw_opt,
1147
  device: torch.device,
1148
  cfg: NovaConfig,
1149
+ ) -> Tuple[int, int, float]:
1150
+ """All ranks must call this β€” FSDP scatters the full state dict."""
 
 
 
 
1151
  try:
1152
  files = hf_api.list_repo_files(
1153
  repo_id=HF_MODEL_REPO,
 
1170
 
1171
  sd = torch.load(mpath, map_location="cpu", weights_only=True)
1172
 
 
1173
  with FSDP.state_dict_type(
1174
  model, StateDictType.FULL_STATE_DICT, _LOAD_SD_CFG
1175
  ):
1176
  model.load_state_dict(sd, strict=False)
1177
 
1178
+ barrier()
1179
 
1180
  meta = torch.load(xpath, map_location="cpu", weights_only=False)
1181
  for opt, key in [(muon_opt, "muon"), (aw_opt, "adamw")]:
 
1185
  except Exception as e:
1186
  log.warning(f"[ckpt] {key} optimizer not restored: {e}")
1187
 
 
1188
  saved_cfg = meta.get("config", {})
1189
  for field_name in [
1190
  "total_steps", "warmup_steps",
 
1200
 
1201
  except Exception as ex:
1202
  if is_main():
1203
+ log.error(f"[ckpt] resume failed: {ex} - fresh start")
1204
  return 0, 0, float("inf")
1205
 
1206
  # ══════════════════════════════════════════════════════════════════════════════
 
1245
  # ══════════════════════════════════════════════════════════════════════════════
1246
 
1247
  def train():
 
1248
  local_rank = setup_dist()
1249
  device = torch.device(f"cuda:{local_rank}")
1250
  ws = world_size()
 
1253
  np.random.seed(42 + rank())
1254
  random.seed(42 + rank())
1255
 
 
1256
  init_hf()
1257
  barrier()
1258
 
 
1259
  cfg = NovaConfig()
1260
 
 
1261
  target_tokens = 200_000_000_000
1262
  tokens_per_step = cfg.effective_batch_tokens(ws)
1263
  cfg.total_steps = max(
 
1267
  if is_main():
1268
  log.info(
1269
  f"[schedule] {ws} GPUs | {tokens_per_step / 1e6:.1f}M tok/step | "
1270
+ f"{cfg.total_steps} steps ~= "
1271
  f"{cfg.total_steps * tokens_per_step / 1e9:.0f}B tokens"
1272
  )
1273
 
 
1274
  if is_main():
1275
  try:
1276
  meta_p = hf_download_dataset("metadata.json")
 
1280
  cfg.vocab_size = (actual + 63) // 64 * 64
1281
  log.info(f"[data] vocab {actual} -> {cfg.vocab_size}")
1282
  except Exception as e:
1283
+ log.info(f"[data] no metadata.json ({e}) - default vocab={cfg.vocab_size}")
1284
 
 
1285
  vs = torch.tensor(cfg.vocab_size, device=device)
1286
  dist.broadcast(vs, src=0)
1287
  cfg.vocab_size = int(vs.item())
1288
 
 
1289
  if is_main():
1290
+ log.info("[model] building Nova-10B-Dense ...")
1291
 
1292
  with torch.device("cpu"):
1293
+ model = build_model(cfg)
1294
 
1295
  model = wrap_fsdp(model, cfg, device)
1296
 
 
1297
  if cfg.use_compile and torch.__version__ >= "2.7.0":
1298
  model = torch.compile(model, mode="max-autotune", fullgraph=False)
1299
  if is_main():
 
1302
  if is_main():
1303
  log.info(f"[model] VRAM after wrap: {vram_str(local_rank)}")
1304
 
 
1305
  muon_opt, aw_opt = build_optimizers(model, cfg)
1306
 
 
1307
  start_step, start_epoch, best_val = 0, 0, float("inf")
1308
  try:
1309
  start_step, start_epoch, best_val = load_ckpt(
 
1311
  )
1312
  except Exception as e:
1313
  if is_main():
1314
+ log.error(f"[ckpt] resume error: {e} - fresh start")
1315
 
 
1316
  info = torch.tensor([start_step, start_epoch], dtype=torch.long, device=device)
1317
  dist.broadcast(info, src=0)
1318
  start_step, start_epoch = int(info[0]), int(info[1])
1319
 
 
1320
  train_loader, val_loader = make_loaders(cfg, rank(), ws)
1321
  train_iter = itertools.cycle(train_loader)
1322
 
 
1323
  if is_main():
1324
  log.info("=" * 72)
1325
  log.info(
1326
+ f"Nova-10B-Dense | Blackwell x {ws} | "
1327
+ f"FP8={resolve_fp8(cfg)} | FSDP SHARD_GRAD_OP | "
1328
  f"Muon + {'AdamW-8bit' if HAS_BNB else 'AdamW-fp32'}"
1329
  )
1330
  log.info(
 
1333
  f"ffn_h={cfg.ffn_hidden} max_len={cfg.max_len}"
1334
  )
1335
  log.info(
1336
+ f" micro={cfg.micro_batch} x accum={cfg.grad_accum} x "
1337
+ f"{ws} GPUs x seq={cfg.max_len} = "
1338
  f"{tokens_per_step / 1e6:.1f}M tok/step"
1339
  )
1340
  log.info(
1341
  f" {cfg.total_steps} steps -> "
1342
+ f"~={cfg.total_steps * tokens_per_step / 1e9:.0f}B tokens"
1343
  )
1344
  log.info(
1345
  f" muon_lr={cfg.muon_lr} aw_lr={cfg.lr} "
 
1365
 
1366
  while step < cfg.total_steps:
1367
 
 
1368
  if elapsed_h() >= cfg.max_hours:
1369
  if is_main():
1370
+ log.info(f"[time] {cfg.max_hours}h budget exhausted - stopping")
1371
  break
1372
 
 
1373
  try:
1374
  batch = next(train_iter)
1375
  except Exception as e:
 
1379
 
1380
  batch = batch.to(device, non_blocking=True)
1381
 
 
1382
  ctx_raw = cfg.current_ctx_len(step)
1383
  ctx_t = torch.tensor(ctx_raw, device=device)
1384
  dist.broadcast(ctx_t, src=0)
 
1386
  if ctx < batch.shape[1]:
1387
  batch = batch[:, :ctx]
1388
 
 
1389
  try:
1390
  with torch.amp.autocast("cuda", dtype=cfg.dtype):
1391
  _, loss = model(batch, targets=batch)
 
1398
  aw_opt.zero_grad(set_to_none=True)
1399
  accum_loss = accum_n = 0
1400
  if is_main():
1401
+ log.error("[oom] step skipped - consider reducing micro_batch")
1402
  continue
1403
 
1404
  if accum_n < cfg.grad_accum:
1405
  continue
1406
 
 
1407
  mlr = cosine_lr(step, cfg.warmup_steps, cfg.total_steps,
1408
  cfg.muon_lr, cfg.muon_min_lr)
1409
  alr = cosine_lr(step, cfg.warmup_steps, cfg.total_steps,
 
1426
  accum_loss = accum_n = 0
1427
  step += 1
1428
 
 
1429
  if is_main() and step % cfg.log_every == 0:
1430
  sps = (step - start_step) / max(time.time() - t_start, 1e-6)
1431
  tps = sps * tokens_per_step
 
1443
  f" vram {vram_str(local_rank)}"
1444
  )
1445
 
 
1446
  if step % cfg.eval_every == 0:
1447
  vl = run_val(model, val_loader, device, n=80)
1448
  vpp = math.exp(min(vl, 20))
 
1456
  )
1457
  log.info(" >> new best checkpoint saved")
1458
 
 
1459
  if step % cfg.ckpt_every == 0:
1460
  save_ckpt(
1461
  model, muon_opt, aw_opt,
 
1464
  tag="ckpt",
1465
  )
1466
 
 
1467
  if time.time() - t_last_ckpt > 1800:
1468
  save_ckpt(
1469
  model, muon_opt, aw_opt,
 
1473
  )
1474
  t_last_ckpt = time.time()
1475
 
 
1476
  if is_main():
1477
  log.info("=" * 72)
1478
+ log.info("TRAINING COMPLETE - saving final checkpoint ...")
1479
 
1480
  fval = run_val(model, val_loader, device, n=200)
1481
  barrier()