Spaces:
Running on L40S
Running on L40S
File size: 28,137 Bytes
9f818c5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 | # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: OpenMDW-1.1
"""Self-contained regression test for the SFT smoke launch flow.
Re-runs the same ``torchrun`` invocation that ``launch_sft_llava_ov.sh``
executes (limited to 10 iterations, ``--deterministic`` mode) and asserts that
the rank-0 ``loss`` and global ``clip_grad_norm`` reproduce the inline goldens
at the bottom of this file. The launch goes through
``cosmos_framework.scripts.train --sft-toml=examples/toml/sft_config/<recipe>.toml``
β the only training entrypoint after the structured-TOML refactor.
Per-GPU goldens
---------------
Goldens are keyed by detected GPU architecture (``torch.cuda.get_device_name``):
* ``gb200`` β original values captured 2026-05-18 against the legacy
``cosmos_framework.scripts.train`` pipeline. The inputs and VLM backbone
used at the time are not part of the OSS layout. The entries stay inline
as a documented historical reference; don't re-run the GB200 path locally.
* ``h100`` β captured on 8Γ H100 (4-GPU subset). The VLM backbone is
``Qwen/Qwen3-VL-8B-Instruct``. Input paths come from env vars matching the
names in ``docs/training.md``::
MODEL_PATH VLM backbone (Qwen/Qwen3-VL-8B-Instruct local snapshot)
Use ``tests/_stage_h100_inputs.sh`` to download/convert this and emit an
``env.sh`` that ``source``s ``MODEL_PATH`` before invoking pytest.
This file is intentionally the only deliverable β the goldens are embedded as a
Python constant and the ``torchrun`` command line is reproduced here, so the
upstream launch shell stays untouched and there is no separate JSON file to
commit.
Invocation (on a 4-GPU node, inside the training container, from the repo
root)::
pytest -s tests/launch_regression_test.py --num-gpus=4 --levels=2 -o addopts=
* ``--num-gpus=4 --levels=2`` matches the markers on the test below and lets
the conftest's per-test setup pin ``CUDA_VISIBLE_DEVICES=0,1,2,3`` for
torchrun. (``4`` is in ``ALL_NUM_GPUS`` in
``cosmos_framework/inference/fixtures/args.py``.)
* ``-o addopts=`` clears the ``addopts`` line in the repo's ``.pytest.toml``
which references ``--suppress-no-test-exit-code`` from the optional
``pytest-custom-exit-code`` plugin (not installed in the training image).
Determinism notes:
* ``llava_ov_datapacker`` runs **without** ``--deterministic`` on H100 AND
overrides ``model.config.deterministic=false``: the Qwen3-VL text
path uses an attention backend whose Hopper FMHA backward kernel has no
deterministic mode (raises ``NotImplementedError`` under PyTorch's
deterministic context). ``VLMModel.__init__`` honors the config-level
flag via ``init_flash_attn_meta`` independently of the launcher arg, so
both must be off. It streams ``lmms-lab/LLaVA-OneVision-Data`` from the
HuggingFace Hub with ``dataloader_train.num_workers=0`` so the data order is
fully deterministic (single process); the only run-to-run noise left is the
FMHA backward kernel. iter-0 is bit-exact (forward only) but iters 1+ drift
(the Hopper FMHA backward has no deterministic mode β confirmed: forcing
``deterministic=true`` raises ``NotImplementedError``, and a 2-run
``num_workers=0`` check still drifts β€0.006 on iters 1-9). All 10 iters are
asserted with a tiered tolerance (``loss_tol_bands``): iter-0 at
1e-3, iters 1-2 at 1e-2, iters 3-9 at 2e-2.
Refreshing the goldens (after an intentional numerical change)::
COSMOS_REGRESSION_UPDATE_GOLDENS=1 pytest -s launch_regression_test.py ...
That prints the captured series for each spec; copy them into the matching
``_GOLDENS[<arch>]`` entry below.
"""
from __future__ import annotations
import os
import re
import shutil
import socket
import subprocess
import sys
from dataclasses import dataclass, field
from pathlib import Path
import pytest
from cosmos_framework.inference.fixtures.args import MAX_GPUS
THIS_DIR = Path(__file__).resolve().parent
# ``cosmos_framework.scripts.train`` and the ``--sft-toml=...`` paths are relative to
# the repo root; we always invoke torchrun from there.
REPO_ROOT = THIS_DIR.parent
def _free_port() -> int:
"""Return a currently-free TCP port for torchrun's rendezvous, instead of a
hardcoded ``master_port`` that ``EADDRINUSE``s when a prior run lingers."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("", 0))
return s.getsockname()[1]
# --- per-arch input paths ----------------------------------------------------
#
# GB200: the original input snapshot lived on an internal read-only filesystem
# that is not in the OSS layout, so the GB200 path is not runnable here. The
# GB200 goldens dict is kept as a historical reference; ``_resolve_paths``
# below skips the GB200 arch instead of re-running it.
def _hf_download(args: list[str]) -> str:
"""``uvx hf download <args> --quiet`` -> the local path it prints (from the HF cache)."""
result = subprocess.run(
["uvx", "hf@latest", "download", *args, "--quiet"],
cwd=str(REPO_ROOT),
capture_output=True,
text=True,
)
if result.returncode != 0:
pytest.fail(f"hf download failed for {args} (exit {result.returncode}):\n{result.stdout}\n{result.stderr}")
lines = [ln.strip() for ln in result.stdout.splitlines() if ln.strip()]
if not lines:
pytest.fail(f"hf download for {args} printed no path:\n{result.stdout}\n{result.stderr}")
return lines[-1]
def _convert_nano_dcp(dest: Path) -> None:
"""Convert the Cosmos3-Nano checkpoint to DCP at ``dest`` (Step 2 of docs/training.md)."""
env = os.environ.copy()
env["PYTHONPATH"] = f".:{env.get('PYTHONPATH', '')}"
result = subprocess.run(
[
sys.executable, "-m", "cosmos_framework.scripts.convert_model_to_dcp",
"-o", str(dest), "--checkpoint-path", "Cosmos3-Nano",
],
cwd=str(REPO_ROOT),
env=env,
)
if result.returncode != 0:
pytest.fail(f"convert_model_to_dcp (Cosmos3-Nano) failed with exit code {result.returncode}")
def _detect_arch() -> str:
"""Map ``torch.cuda.get_device_name(0)`` to a goldens key."""
import torch # local import keeps module import side-effects light
if not torch.cuda.is_available():
return "unknown"
name = torch.cuda.get_device_name(0).upper()
if "GB200" in name:
return "gb200"
# H200 shares the Hopper kernels with H100 and is treated identically here:
# both map to the ``h100`` goldens key (the GitHub GPU CI runs on 8ΓH200).
if "H100" in name or "H200" in name:
return "h100"
return "unknown"
# Pinned revisions mirror tests/_stage_h100_inputs.sh so prepared inputs match
# the captured h100 goldens.
_BRIDGE_REVISION = "46468e12ac0dd36901e9e3240d4fc7620942b5d7"
_QWEN_VL_REVISION = "0c351dd01ed87e9c1b53cbc748cba10e6187ff3b"
# Tolerances for ``pytest.approx``. The launch passes ``--deterministic`` and
# ``PYTHONHASHSEED=42``; the tolerance only absorbs minor noise from
# non-deterministic NCCL reductions.
_DEFAULT_RTOL = 1e-3
_DEFAULT_ATOL = 1e-3
# --- log parsers -------------------------------------------------------------
#
# VLM (``pre_exp012_llava_ov_datapacker``) logs the DP-reduced loss on rank 0::
#
# train/loss_avg: 1.32225 (iteration 0)
#
# ``GradClip`` emits the global grad-norm via every rank, prefixed with
# ``[RANK X]``. Key is ``clip_grad_norm/global`` for VLM.
_VLM_LOSS_RE = re.compile(r"train/loss_avg:\s+([0-9.eE+-]+)\s+\(iteration\s+\d+\)")
# VFM logs per-rank loss via the IterSpeed callback's on_training_step_end:
# [RANK 0] Iteration 1: Hit counter: 1/50 | Loss: 0.2515 | Time: 120.42s
_VFM_LOSS_RE = re.compile(
r"\[RANK\s+0\]\s+Iteration\s+\d+:\s+Hit counter:[^|]+\|\s+Loss:\s+([0-9.eE+-]+)"
)
_GRAD_NORM_RE = re.compile(
r"\[RANK\s+0\][^\n]*clip_grad_norm/(?:[^/]+/)?global:\s+([0-9.eE+-]+)\s+\(iteration\s+\d+\)"
)
@dataclass(frozen=True)
class LaunchSpec:
"""A single launch flow under regression β mirrors the launcher shell."""
key: str # goldens key + pytest parametrize id source
sft_toml: str # ``--sft-toml=...`` value, relative to REPO_ROOT
extra_hydra_args: tuple[str, ...]
loss_re: re.Pattern[str]
deterministic_iters: int # how many leading iters are bit-exact deterministic
extra_env: dict[str, str] = field(default_factory=dict)
nproc_per_node: int = 4
# Some specs can't run under ``--deterministic`` on H100: the Qwen3-VL text
# attention's Hopper FMHA backward kernel has no deterministic mode and
# raises NotImplementedError. For those specs we drop the flag and accept
# the tighter goldens tolerance only on the iters that still reproduce in
# practice (see ``deterministic_iters``).
deterministic: bool = True
# Per-spec goldens tolerance for ``pytest.approx``. Deterministic specs use
# the tight default uniformly across all asserted iters.
loss_rtol: float = _DEFAULT_RTOL
loss_atol: float = _DEFAULT_ATOL
# Optional tiered tolerance: each ``(count, rtol, atol)`` applies to the next
# ``count`` iters in order, and the counts must sum to ``deterministic_iters``.
# Lets the reasoner tighten its bit-exact iter-0 while loosening the
# non-deterministic tail. When empty, all iters use ``loss_rtol/loss_atol``.
loss_tol_bands: tuple[tuple[int, float, float], ...] = ()
# 4-GPU specs run by ``test_launch_regression``; 8-GPU specs run by
# ``test_launch_regression_8gpu`` (the ``gpus`` marker carries only one value,
# so the test functions are split).
_SPEC_KEYS = (
"llava_ov_datapacker",
"vision_sft_nano",
)
_SPEC_KEYS_8GPU = ("vision_sft_super",)
def _build_specs(paths: dict[str, str]) -> dict[str, LaunchSpec]:
"""Build the per-arch ``LaunchSpec`` list using the resolved input paths."""
# vision_sft_super needs a Cosmos3-Super DCP; the default staging script
# only produces Cosmos3-Nano. If BASE_CHECKPOINT_PATH_SUPER is set,
# redirect BASE_CHECKPOINT_PATH for this spec via extra_env.
super_extra_env: dict[str, str] = {}
if super_ckpt := os.environ.get("BASE_CHECKPOINT_PATH_SUPER"):
super_extra_env["BASE_CHECKPOINT_PATH"] = super_ckpt
return {
"llava_ov_datapacker": LaunchSpec(
# Replicates launch_sft_llava_ov.sh, capped to 10 iters.
key="llava_ov_datapacker",
sft_toml="examples/toml/sft_config/llava_ov_datapacker.toml",
extra_hydra_args=(
# TAIL_OVERRIDES from launch_sft_llava_ov.sh β fields not modeled
# by SFTExperimentConfig.
f"model.config.policy.backbone.model_name={paths['vlm_model_path']}",
"data_setting.max_tokens=16000",
# 4-GPU subset for the test (TOML pins dp_shard=8 for the 8-GPU
# launch shell).
"model.config.parallelism.data_parallel_shard_degree=4",
# The Hopper FMHA backward raises under PyTorch
# deterministic mode, so both the config default and the
# launcher's --deterministic flag must be off (see the
# determinism notes in the module docstring).
"model.config.deterministic=false",
# num_workers=0: fully-ordered single-process streaming, so the
# only run-to-run noise is the FMHA backward kernel, not data
# order. prefetch_factor/persistent_workers must be unset for
# num_workers=0 (torch DataLoader rejects them otherwise).
"dataloader_train.num_workers=0",
"dataloader_train.prefetch_factor=null",
"dataloader_train.persistent_workers=false",
# Regression-specific tweaks.
"trainer.max_iter=10",
"trainer.logging_iter=1",
"job.wandb_mode=disabled",
"ckpt_type=dummy",
"checkpoint.load_from_object_store.enabled=false",
"checkpoint.save_to_object_store.enabled=false",
"upload_reproducible_setup=false",
),
loss_re=_VLM_LOSS_RE,
deterministic_iters=10,
deterministic=False,
# Tiered tolerance: iter-0 is bit-exact (forward only) β 1e-3; iters
# 1+ carry the FMHA-backward noise (β€0.006 across two num_workers=0
# runs) β 1e-2 for the early iters 1-2, 2e-2 for the tail 3-9.
loss_tol_bands=(
(1, 1e-3, 1e-3), # iter 0
(2, 1e-2, 1e-2), # iters 1-2
(7, 2e-2, 2e-2), # iters 3-9
),
),
"vision_sft_nano": LaunchSpec(
# Replicates launch_sft_vision_nano.sh, capped to 10 iters.
# ``DATASET_PATH`` / ``WAN_VAE_PATH`` / ``BASE_CHECKPOINT_PATH`` flow
# in via the TOML's ``${oc.env:...}`` interpolation; no Hydra plumbing
# needed beyond the regression-cap overrides below.
key="vision_sft_nano",
sft_toml="examples/toml/sft_config/vision_sft_nano.toml",
extra_hydra_args=(
"model.config.parallelism.data_parallel_shard_degree=4",
"model.config.compile.enabled=true",
"trainer.max_iter=10",
"trainer.logging_iter=1",
"job.wandb_mode=disabled",
"upload_reproducible_setup=false",
"checkpoint.save_iter=999999",
),
loss_re=_VFM_LOSS_RE,
deterministic_iters=10,
),
"vision_sft_super": LaunchSpec(
# Replicates launch_sft_vision_super.sh on 8 GPUs (dp_shard=4 Γ cp=2),
# capped to 10 iters. ``compile.enabled=false`` because the Super
# backbone's compile path is not bit-exact across runs on H100.
key="vision_sft_super",
sft_toml="examples/toml/sft_config/vision_sft_super.toml",
nproc_per_node=8,
extra_hydra_args=(
"model.config.parallelism.data_parallel_shard_degree=4",
"model.config.parallelism.context_parallel_shard_degree=2",
"model.config.compile.enabled=false",
"trainer.max_iter=10",
"trainer.logging_iter=1",
"job.wandb_mode=disabled",
"upload_reproducible_setup=false",
"checkpoint.save_iter=999999",
),
loss_re=_VFM_LOSS_RE,
deterministic_iters=10,
extra_env=super_extra_env,
),
}
# --- helpers -----------------------------------------------------------------
def _parse_series(log_text: str, loss_re: re.Pattern[str]) -> tuple[list[float], list[float]]:
"""Extract per-iteration rank-0 loss and global grad-norm series, in order."""
losses = [float(m.group(1)) for m in loss_re.finditer(log_text)]
grad_norms = [float(m.group(1)) for m in _GRAD_NORM_RE.finditer(log_text)]
assert losses and grad_norms, (
f"No loss/grad-norm pairs found in log (losses={len(losses)}, grads={len(grad_norms)})"
)
assert len(losses) == len(grad_norms), (
f"loss vs grad-norm length mismatch ({len(losses)} vs {len(grad_norms)}): "
"the log must contain one rank-0 entry of each per training step."
)
return losses, grad_norms
def _run_torchrun(spec: LaunchSpec, run_dir: Path) -> Path:
"""Invoke the same ``torchrun`` command that the launcher shell runs.
Returns the path of the captured combined stdout+stderr log.
"""
run_dir.mkdir(parents=True, exist_ok=True)
log_file = run_dir / "training.log"
cmd = [
"torchrun",
f"--nproc_per_node={spec.nproc_per_node}",
f"--master_port={_free_port()}",
"-m",
"cosmos_framework.scripts.train",
f"--sft-toml={spec.sft_toml}",
]
if spec.deterministic:
cmd.append("--deterministic")
cmd += ["--", *spec.extra_hydra_args]
env = os.environ.copy()
# HF env mirrors what the launcher shell sets up; ``HF_TOKEN`` must already
# be exported in the caller's environment if the experiment hits gated Hub
# endpoints (e.g. the LLaVA-OneVision-Data streaming dataset).
env.setdefault("HF_HOME", "/tmp/hf_cache")
Path(env["HF_HOME"]).mkdir(parents=True, exist_ok=True)
env.setdefault("HF_HUB_DISABLE_XET", "1")
env["PYTHONHASHSEED"] = "42" # must be set before interpreter starts
env["PYTHONPATH"] = f".:{env.get('PYTHONPATH', '')}"
env["IMAGINAIRE_OUTPUT_ROOT"] = str(run_dir / "output")
env.update(spec.extra_env)
# Tee: stream the torchrun output live to stdout (so CI shows training
# progress under ``pytest -s``) while capturing it into the log file.
with log_file.open("w") as fp:
proc = subprocess.Popen(
cmd,
env=env,
cwd=str(REPO_ROOT),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
assert proc.stdout is not None
for line in proc.stdout:
sys.stdout.write(line)
sys.stdout.flush()
fp.write(line)
returncode = proc.wait()
if returncode != 0:
# Tolerate harmless PyGIL teardown warnings if training did complete.
text = log_file.read_text(errors="replace")
if "Done with training" not in text:
pytest.fail(
f"{spec.key}: torchrun failed with exit code {returncode} "
"and log does not contain 'Done with training'.\n"
f"Log tail:\n{text[-2000:]}"
)
return log_file
# --- fixtures ----------------------------------------------------------------
@pytest.fixture(scope="module", autouse=True)
def _require_4_gpus() -> None:
"""Skip the whole module unless we can launch 4-GPU training here."""
if shutil.which("torchrun") is None:
pytest.skip("torchrun not on PATH β must run inside the training container")
try:
import torch
except Exception as exc: # pragma: no cover β surfaces during dev only
pytest.skip(f"torch unavailable ({exc!r})")
if not torch.cuda.is_available() or torch.cuda.device_count() < 4:
pytest.skip(f"requires 4 visible CUDA devices, found {torch.cuda.device_count()}")
@pytest.fixture(scope="module")
def h100_inputs(tmp_path_factory: pytest.TempPathFactory):
"""Provide the regression input paths, preparing any not already set in env.
Mirrors the download/convert steps of ``tests/_stage_h100_inputs.sh`` (it
does NOT set up the environment -- ``uv sync`` and the ``transformers``
pin still belong to that script / the caller). Honors pre-set env vars (so
``source env.sh`` still works); anything prepared here goes under a temp
stage dir that is removed on teardown. The four vars are exported because
the SFT TOMLs interpolate ``DATASET_PATH`` / ``WAN_VAE_PATH`` /
``BASE_CHECKPOINT_PATH`` at load time and the VLM spec passes ``MODEL_PATH``
as a Hydra backbone override.
"""
arch = _detect_arch()
if arch not in ("h100", "gb200"):
pytest.skip(f"no regression goldens for GPU arch {arch!r}; only h100/gb200 supported")
if shutil.which("uvx") is None:
pytest.skip("uvx not on PATH -- required to prepare regression inputs")
stage = tmp_path_factory.mktemp("h100_stage")
set_vars: list[str] = []
def _ensure(var: str, value_fn) -> None:
if not os.environ.get(var):
os.environ[var] = str(value_fn())
set_vars.append(var)
_ensure(
"DATASET_PATH",
lambda: Path(
_hf_download(
["--repo-type", "dataset", "nvidia/bridge-v2-subset-synthetic-captions",
"--revision", _BRIDGE_REVISION]
)
) / "sft_dataset_bridge",
)
_ensure("WAN_VAE_PATH", lambda: _hf_download(["Wan-AI/Wan2.2-TI2V-5B", "Wan2.2_VAE.pth"]))
_ensure("MODEL_PATH", lambda: _hf_download(["Qwen/Qwen3-VL-8B-Instruct", "--revision", _QWEN_VL_REVISION]))
def _make_dcp() -> Path:
dest = stage / "Cosmos3-Nano-DCP"
_convert_nano_dcp(dest)
return dest
_ensure("BASE_CHECKPOINT_PATH", _make_dcp)
try:
yield {"vlm_model_path": os.environ.get("MODEL_PATH", "")}
finally:
for var in set_vars:
os.environ.pop(var, None)
shutil.rmtree(stage, ignore_errors=True)
# --- tests -------------------------------------------------------------------
def _assert_spec_matches_goldens(spec_key: str, tmp_path: Path, paths: dict[str, str]) -> None:
"""Re-run ``spec``'s torchrun command and check loss / grad-norm against goldens."""
arch = _detect_arch()
spec = _build_specs(paths)[spec_key]
log_path = _run_torchrun(spec, tmp_path)
log_text = log_path.read_text(errors="replace")
loss, grad_norm = _parse_series(log_text, spec.loss_re)
# The run log also streamed live under ``pytest -s``; include its tail in any
# failure message so the run detail is attached to the failure report too.
run_detail = f"\n--- {spec.key} run log (last 4000 chars) ---\n{log_text[-4000:]}"
assert len(loss) == 10, f"expected 10 iterations, parsed {len(loss)} (loss={loss}){run_detail}"
# Refresh path: print captured values for manual copy into ``_GOLDENS``.
if os.environ.get("COSMOS_REGRESSION_UPDATE_GOLDENS") == "1":
print(f"\n# --- goldens for arch={arch!r} key={spec.key!r} ---")
print(f'"{spec.key}": {{')
print(f' "loss": {loss},')
print(f' "grad_norm": {grad_norm},')
print("},")
pytest.skip(
f"captured fresh series for arch={arch!r} key={spec.key!r}; copy the printed "
f"dict into _GOLDENS[{arch!r}] at the bottom of launch_regression_test.py, "
"then rerun without COSMOS_REGRESSION_UPDATE_GOLDENS to assert."
)
arch_goldens = _GOLDENS.get(arch)
assert arch_goldens is not None, (
f"no goldens table for arch {arch!r}; capture with COSMOS_REGRESSION_UPDATE_GOLDENS=1"
)
expected = arch_goldens.get(spec.key)
assert expected is not None, (
f"no goldens for arch={arch!r} key={spec.key!r}; capture with COSMOS_REGRESSION_UPDATE_GOLDENS=1"
)
n = spec.deterministic_iters
# Build the per-iter tolerance segments: either the spec's tiered bands or a
# single uniform band spanning all asserted iters.
if spec.loss_tol_bands:
assert sum(c for c, _, _ in spec.loss_tol_bands) == n, (
f"{spec.key}: loss_tol_bands counts {[c for c, _, _ in spec.loss_tol_bands]} "
f"must sum to deterministic_iters={n}"
)
bands = spec.loss_tol_bands
else:
bands = ((n, spec.loss_rtol, spec.loss_atol),)
start = 0
for count, rtol, atol in bands:
end = start + count
assert loss[start:end] == pytest.approx(
expected["loss"][start:end], rel=rtol, abs=atol
), (
f"{spec.key} ({arch}): rank-0 loss[{start}:{end}] (rel/abs={rtol}) "
f"does not match goldens\n"
f" got : {loss[start:end]}\n"
f" expected: {expected['loss'][start:end]}{run_detail}"
)
start = end
# ``grad_norm`` is optional: ``None`` skips the check when the FSDP
# global-norm all-reduce isn't bit-exact on this arch.
if expected["grad_norm"] is None:
return
assert grad_norm[:n] == pytest.approx(
expected["grad_norm"][:n], rel=spec.loss_rtol, abs=spec.loss_atol
), (
f"{spec.key} ({arch}): global grad-norm[:{n}] does not match goldens\n"
f" got : {grad_norm[:n]}\n"
f" expected: {expected['grad_norm'][:n]}{run_detail}"
)
# Define only the test function matching MAX_GPUS β the conftest rejects
# ``gpus(N)`` markers outside the active ``ALL_NUM_GPUS = (0, 1, MAX_GPUS)``.
if MAX_GPUS == 4:
@pytest.mark.level(2)
@pytest.mark.gpus(4)
@pytest.mark.parametrize("spec_key", _SPEC_KEYS, ids=lambda k: k.removeprefix("launch_"))
def test_launch_regression(spec_key: str, tmp_path: Path, h100_inputs: dict[str, str]) -> None:
"""Re-run ``spec``'s torchrun command and check loss / grad-norm against goldens."""
_assert_spec_matches_goldens(spec_key, tmp_path, h100_inputs)
if MAX_GPUS == 8:
@pytest.mark.skip(reason="vision_sft_super spec disabled")
@pytest.mark.level(2)
@pytest.mark.gpus(8)
@pytest.mark.parametrize(
"spec_key", _SPEC_KEYS_8GPU, ids=lambda k: k.removeprefix("launch_")
)
def test_launch_regression_8gpu(spec_key: str, tmp_path: Path, h100_inputs: dict[str, str]) -> None:
"""8-GPU variant for ``vision_sft_super`` (dp_shard=4 Γ cp=2)."""
_assert_spec_matches_goldens(spec_key, tmp_path, h100_inputs)
# Goldens keyed by GPU arch then ``LaunchSpec.key``. Refresh with
# ``COSMOS_REGRESSION_UPDATE_GOLDENS=1``.
_GOLDENS: dict[str, dict[str, dict[str, list[float] | None]]] = {
# Captured 2026-05-18 on a 4 Γ NVIDIA GB200 node with ``--deterministic``
# and seed 42 against the legacy training pipeline. VLM backbone is not
# part of the OSS layout.
"gb200": {
"llava_ov_datapacker": {
"loss": [1.32208, 1.20886, 1.39254, 1.40460, 1.16652, 1.24852, 1.38463, 1.22766, 0.96263, 1.14468],
"grad_norm": [
38.62454, 23.61477, 30.53218, 36.46255, 25.06240,
39.70305, 48.52226, 52.18334, 22.77521, 25.06970,
],
},
# Captured 2026-06-09 on a 4 Γ NVIDIA GB200 node with seed 42 against the
# current TOML-config pipeline (inputs prepared in-test by ``h100_inputs``,
# which now also serves gb200). Runs under ``--deterministic`` so loss
# reproduces bit-exact across all 10 iters; loss matches the h100 nano
# series within ~1e-3. grad_norm is non-det because ``compile.enabled=true``
# makes the all-rank reduction not bit-exact, so None (same as h100).
"vision_sft_nano": {
"loss": [0.2269, 0.2181, 0.2026, 0.2309, 0.2178, 0.273, 0.2871, 0.2164, 0.2059, 0.264],
"grad_norm": None,
},
},
# Recaptured 2026-06-03 on a 4 Γ NVIDIA H100 80GB HBM3 node with seed 42 and
# transformers==4.57.6. VLM model is ``Qwen/Qwen3-VL-8B-Instruct``; inputs are
# prepared in-test by the ``h100_inputs`` fixture (or via
# ``tests/_stage_h100_inputs.sh`` if its env vars are pre-set).
"h100": {
# num_workers=0, deterministic mode off (see the spec's hydra overrides
# and the loss_tol_bands tiers). Centered on the midpoint of two H200 CI
# runs (CI runs on H200) so the tiered bands keep maximum margin; iter-0
# is bit-exact across H100/H200 runs. grad-norm is non-det, so None.
"llava_ov_datapacker": {
"loss": [1.06924, 0.88399, 1.09293, 1.16314, 1.03592, 0.99041, 1.11041, 0.97001, 0.81246, 0.98548],
"grad_norm": None,
},
# Recaptured 2026-06-03 after the TOML-config rewrite shifted some
# defaults. Runs under ``--deterministic`` so loss reproduces bit-exact
# across all 10 iters, but grad_norm is non-det because
# ``compile.enabled=true`` makes the all-rank reduction not bit-exact
# on H100.
"vision_sft_nano": {
"loss": [0.2272, 0.2181, 0.2028, 0.2306, 0.218, 0.2734, 0.2865, 0.2162, 0.2055, 0.2643],
"grad_norm": None,
},
"vision_sft_super": {
"loss": [0.2133, 0.2028, 0.1992, 0.2373, 0.2539, 0.2645, 0.2679, 0.2182, 0.1959, 0.2457],
"grad_norm": [0.00403, 0.00255, 0.00412, 0.00485, 0.00305, 0.00331, 0.00375, 0.00371, 0.00313, 0.00276],
},
},
}
if __name__ == "__main__": # pragma: no cover β manual driver
sys.exit(pytest.main([__file__, "-v", "-s", "-o", "addopts="]))
|