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