Update nova10b_dense.py
Browse files- 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-
|
| 4 |
~10B fully-dense transformer (no MoE, no routing, no MoD)
|
| 5 |
|
| 6 |
Architecture:
|
| 7 |
-
D1 Fused QKV
|
| 8 |
-
D2 Differential Attn
|
| 9 |
-
D3 SwiGLU FFN
|
| 10 |
-
D4 RMSNorm
|
| 11 |
-
D5 LayerScale
|
| 12 |
-
D6 RoPE
|
| 13 |
-
D7 Logit soft-cap
|
| 14 |
-
D8 FP8
|
| 15 |
|
| 16 |
Training stack:
|
| 17 |
-
O1 Muon
|
| 18 |
-
O2 AdamW-8bit
|
| 19 |
-
O3 FSDP2
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 β
|
| 90 |
-
|
| 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=
|
| 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 |
-
|
| 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.
|
|
|
|
| 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 |
-
"""
|
| 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]
|
| 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
|
| 282 |
rope_theta: float = 500_000.0
|
| 283 |
|
| 284 |
# ββ FP8 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 285 |
-
use_fp8: str = "auto"
|
| 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
|
| 361 |
return False
|
| 362 |
if cfg.use_fp8 == "auto" and is_main():
|
| 363 |
log.info(
|
| 364 |
-
f"[fp8] auto
|
| 365 |
-
f"blackwell={_is_blackwell()}
|
| 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 |
-
|
| 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
|
| 454 |
-
|
| 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 |
-
|
| 511 |
-
|
| 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)
|
| 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 |
-
) ->
|
| 564 |
"""
|
| 565 |
-
Standard
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 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
|
| 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
|
| 606 |
-
D2
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 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
|
| 619 |
-
self.half = cfg.head_dim // 2
|
| 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 |
-
#
|
| 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 |
-
|
| 687 |
-
|
| 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 |
-
|
| 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 |
-
|
| 822 |
1. Create model on CPU
|
| 823 |
-
2. Apply FP8 (replaces eligible nn.Linear
|
| 824 |
-
3. Tie
|
|
|
|
|
|
|
| 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
|
| 860 |
-
mixed_precision
|
| 861 |
-
sharding_strategy
|
| 862 |
-
backward_prefetch
|
| 863 |
-
cpu_offload
|
| 864 |
-
device_id
|
| 865 |
-
use_orig_params
|
| 866 |
)
|
| 867 |
if is_main():
|
| 868 |
log.info(
|
| 869 |
-
f"[fsdp]
|
| 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 |
-
|
| 898 |
-
With
|
| 899 |
-
|
| 900 |
-
|
|
|
|
|
|
|
| 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__(
|
| 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)
|
| 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 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 973 |
muon_p.append(p)
|
| 974 |
-
elif
|
| 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
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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()
|
| 1053 |
-
sd = _full_state_dict_save(model)
|
| 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 |
-
) ->
|
| 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()
|
| 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}
|
| 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})
|
| 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)
|
| 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}
|
| 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
|
| 1330 |
-
f"FP8={resolve_fp8(cfg)} | FSDP
|
| 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}
|
| 1340 |
-
f"{ws} GPUs
|
| 1341 |
f"{tokens_per_step / 1e6:.1f}M tok/step"
|
| 1342 |
)
|
| 1343 |
log.info(
|
| 1344 |
f" {cfg.total_steps} steps -> "
|
| 1345 |
-
f"
|
| 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
|
| 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
|
| 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
|
| 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()
|