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