Skip to content

Commit 0e034bc

Browse files
pengcuoclaude
andauthored
test(inference): smoke-test FP8 mixed-precision diffusion steps (#226)
## What Adds `test_nano_fp8_mixed_precision_inference` to `tests/nano_inference_smoke_test.py`: the same sharded-layout text2video run as `test_nano_fp8_inference`, plus the FP8 mixed-precision diffusion-step flags from #217 (`--mixed-precision-first-steps=2 --mixed-precision-last-steps=2`, default `w8a16-cache=none` — the only mode valid under FSDP sharding, which is exactly the layout used). ## Pass criterion The schedule itself, not just a valid video: the test parses the `MIXED_PRECISION_TRACE` log line and compares it exactly against the expected `2x W8A16 / 6x W8A8 / 2x W8A16` sequence for the 10-step run (`num_steps` is read back from the shared `_FP8_GENERATION_ARGS` so the expectation cannot drift). A run where the flags silently never engaged (all-W8A8 trace or no trace) fails even though its video would look fine. `_assert_video_has_content` still catches the numerically-broken-but-running case, and the existing FP8 swap-count guard confirms the FP8 path engaged at all. ## CI wiring No new job or trigger: the existing `generator-inference-smoke` job runs the whole file, so the case is picked up automatically. Like the other two FP8 cases it SKIPs (not fails) when `HF_TOKEN` cannot read `nvidia/Cosmos3-Experimental`. Job timeout bumped 90 → 105 min for the extra full-width run. ## Verification `pytest --collect-only` inside the inference container collects 5 tests including the new case; the GPU run itself is exercised by this PR's own `generator-inference-smoke` job. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent b02a3af commit 0e034bc

2 files changed

Lines changed: 124 additions & 9 deletions

File tree

.github/workflows/gpu-tests.yml

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,15 +125,22 @@ jobs:
125125
generator-inference-smoke:
126126
needs: pre-commit
127127
runs-on: [self-hosted, gpu, h200]
128-
# 90 (not 60) since the Nano step gained the two ModelOpt FP8 cases: a 20 GB
129-
# checkpoint on a cold cache plus two more full-width runs.
130-
timeout-minutes: 90
128+
# 105 (not 60) since the Nano step gained the three ModelOpt FP8 cases (two
129+
# parallelism layouts + mixed-precision diffusion steps): a 20 GB checkpoint
130+
# on a cold cache plus three more full-width runs.
131+
timeout-minutes: 105
131132
env:
132133
HF_TOKEN: ${{ secrets.HF_TOKEN }}
133134
HF_HUB_DISABLE_XET: "1"
134135
# Both steps use the `throughput` preset, whose layout is derived from
135136
# world_size (cfgp=1, cp=1, dp_shard=world_size), so the width is free.
136137
TEST_MAX_GPUS: "4"
138+
# This is the job whose stated purpose includes FP8 coverage: promote the
139+
# cannot-reach-nvidia/Cosmos3-Experimental skip to a hard failure, so a
140+
# token rotation or a permissions change turns the job red instead of
141+
# silently dropping all three FP8 cases. Forks and local runs (which do
142+
# not set this) keep the skip behaviour.
143+
REQUIRE_FP8: "1"
137144
steps:
138145
- uses: actions/checkout@v6
139146

@@ -144,10 +151,12 @@ jobs:
144151

145152
# One inference call over t2vs (+sound), action policy, and forward_dynamics; checks each output.
146153
# Plus the ModelOpt static-FP8 Nano checkpoint in both parallelism layouts
147-
# (FSDP-sharded and replicated). The FP8 checkpoint lives in the
148-
# access-controlled nvidia/Cosmos3-Experimental repo, so those two cases SKIP
149-
# unless HF_TOKEN can read it — check the log for "no access to
150-
# nvidia/Cosmos3-Experimental" before trusting a green run to have covered FP8.
154+
# (FSDP-sharded and replicated) and once more with FP8 mixed-precision
155+
# diffusion steps (W8A16 edge steps; exact schedule asserted from the
156+
# MIXED_PRECISION_TRACE log). The FP8 checkpoint lives in the
157+
# access-controlled nvidia/Cosmos3-Experimental repo; elsewhere those three
158+
# cases SKIP unless HF_TOKEN can read it, but REQUIRE_FP8=1 above makes the
159+
# skip a hard failure here, so a green run is guaranteed to have covered FP8.
151160
# -s streams the live process log.
152161
# Reuse the same input-asset cache dir as the unittest job.
153162
- name: Nano inference smoke (t2vs + action policy + forward_dynamics + FP8, 4 GPU)

tests/nano_inference_smoke_test.py

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@
3737
layout — the FP8 all-gather. Skipped when the checkpoint is not reachable
3838
(see ``_download_fp8_checkpoint``).
3939
40+
4. One more ``text2video`` call against the same FP8 checkpoint with FP8
41+
mixed-precision diffusion steps enabled (``--mixed-precision-first-steps`` /
42+
``--mixed-precision-last-steps``): the first/last N denoising steps run
43+
W8A16 (dequantized weight + dense GEMM) while middle steps keep the TorchAO
44+
W8A8 path. Asserts the exact per-step precision schedule from the
45+
``MIXED_PRECISION_TRACE`` log line plus a non-degenerate ``vision.mp4``.
46+
4047
Smoke-level only (output validity, not numeric goldens). The checkpoint + its
4148
tokenizers download from the HF Hub on first run and are reused afterward.
4249
@@ -216,6 +223,22 @@
216223
),
217224
}
218225

226+
# Mixed-precision diffusion steps (FP8 W8A16 edge steps) schedule for the
227+
# dedicated smoke case: with the 10-step ``_FP8_GENERATION_ARGS`` run this
228+
# selects 2x W8A16 / 6x W8A8 / 2x W8A16 (``use_w8a16_step``). Small enough to
229+
# stay cheap, large enough that first, middle, and last regions are all
230+
# non-empty.
231+
_MIXED_PRECISION_FIRST_STEPS = 2
232+
_MIXED_PRECISION_LAST_STEPS = 2
233+
234+
# Emitted by ``MixedPrecisionRuntime`` (``cosmos_framework/utils/generator/
235+
# mixed_precision.py``): the install summary at load time and the per-request
236+
# per-step precision trace at request end. The trace is parsed and compared
237+
# exactly — a run that silently ignored the flags would log an all-W8A8 trace
238+
# (or none at all) and still produce a perfectly valid video.
239+
_MIXED_PRECISION_INSTALL_LOG = "Mixed precision installed:"
240+
_MIXED_PRECISION_TRACE_LOG = re.compile(r"MIXED_PRECISION_TRACE steps=([A-Za-z0-9,]+)")
241+
219242
# Audio sanity thresholds for the muxed sound track.
220243
_RMS_SILENCE_FLOOR = 1e-4 # below this the track is effectively silence
221244
_PEAK_SANITY_CEIL = 1.5 # decoded float audio should sit within ~[-1, 1]
@@ -354,22 +377,32 @@ def _download_fp8_checkpoint() -> Path:
354377
nvidia/Cosmos3-Experimental), so a fork PR without the runner secret does not
355378
go red. Any other failure (a deleted revision, a broken download) still fails
356379
loudly: a silently-skipping FP8 job would look green while testing nothing.
380+
381+
``REQUIRE_FP8=1`` promotes that skip to a hard failure. The CI job whose
382+
stated purpose includes FP8 coverage sets it, so a token rotation or a
383+
permissions change on the gated repo turns that job red instead of silently
384+
dropping every FP8 case while still reporting green.
357385
"""
358386
from huggingface_hub import snapshot_download
359387
from huggingface_hub.errors import GatedRepoError, HfHubHTTPError, RepositoryNotFoundError
360388

389+
def _skip_or_fail_no_access(reason: str) -> None:
390+
if os.environ.get("REQUIRE_FP8") == "1":
391+
pytest.fail(f"REQUIRE_FP8=1 but the FP8 checkpoint is unreachable: {reason}")
392+
pytest.skip(reason)
393+
361394
try:
362395
repo_root = snapshot_download(
363396
repo_id=_FP8_REPOSITORY,
364397
revision=_FP8_REVISION,
365398
allow_patterns=[f"{_FP8_SUBDIRECTORY}/*"],
366399
)
367400
except (GatedRepoError, RepositoryNotFoundError) as error:
368-
pytest.skip(f"no access to {_FP8_REPOSITORY} (needs an HF_TOKEN with read access): {error!r}")
401+
_skip_or_fail_no_access(f"no access to {_FP8_REPOSITORY} (needs an HF_TOKEN with read access): {error!r}")
369402
except HfHubHTTPError as error:
370403
status_code = getattr(error.response, "status_code", None)
371404
if status_code in (401, 403):
372-
pytest.skip(f"no access to {_FP8_REPOSITORY} (HTTP {status_code}): {error!r}")
405+
_skip_or_fail_no_access(f"no access to {_FP8_REPOSITORY} (HTTP {status_code}): {error!r}")
373406
raise
374407

375408
checkpoint_path = Path(repo_root) / _FP8_SUBDIRECTORY
@@ -627,3 +660,76 @@ def test_nano_fp8_inference(tmp_path: Path, layout: str) -> None:
627660
video = so.parent / "vision.mp4"
628661
assert video.is_file(), f"FP8 {layout} run produced no vision.mp4 ({so})"
629662
_assert_video_has_content(video)
663+
664+
@pytest.mark.level(2)
665+
@pytest.mark.gpus(MAX_GPUS)
666+
def test_nano_fp8_mixed_precision_inference(tmp_path: Path) -> None:
667+
"""text2video from the FP8 Nano checkpoint with mixed-precision diffusion steps.
668+
669+
Same run as the ``sharded`` case of ``test_nano_fp8_inference`` plus the
670+
``--mixed-precision-first-steps`` / ``--mixed-precision-last-steps`` flags,
671+
so the first/last 2 of the 10 denoising steps run W8A16 (dequantized E4M3
672+
weight + dense GEMM) while the middle 6 keep the TorchAO W8A8 path. The
673+
sharded layout is the one that constrains the feature: FSDP-sharded FP8
674+
weights support only the default ``mixed_precision_w8a16_cache='none'``
675+
(per-step on-the-fly dequant), which is exactly the mode exercised here.
676+
677+
The pass criterion is the schedule itself, not just a valid video: the
678+
``MIXED_PRECISION_TRACE`` line is parsed and compared exactly against the
679+
expected ``2x W8A16 / 6x W8A8 / 2x W8A16`` sequence, so a run where the
680+
flags never engaged (all-W8A8 trace, or no trace at all) fails even though
681+
its output video would look fine. ``_assert_video_has_content`` then
682+
catches the numerically-broken-but-still-running case.
683+
"""
684+
checkpoint_path = _download_fp8_checkpoint()
685+
out_dir = tmp_path / "out_fp8_mixed_precision"
686+
cmd = [
687+
"torchrun",
688+
f"--nproc_per_node={MAX_GPUS}",
689+
f"--master_port={_free_port()}",
690+
"-m",
691+
"cosmos_framework.scripts.inference",
692+
*_FP8_LAYOUTS["sharded"],
693+
"-i",
694+
"inputs/omni/t2v.json",
695+
"-o",
696+
str(out_dir),
697+
"--checkpoint-path",
698+
str(checkpoint_path),
699+
*_FP8_GENERATION_ARGS,
700+
f"--mixed-precision-first-steps={_MIXED_PRECISION_FIRST_STEPS}",
701+
f"--mixed-precision-last-steps={_MIXED_PRECISION_LAST_STEPS}",
702+
]
703+
log = _run(cmd, tmp_path / "inference_fp8_mixed_precision.log")
704+
705+
# The FP8 path itself still engaged (same guard as test_nano_fp8_inference).
706+
swap_match = _FP8_SWAP_LOG.search(log)
707+
assert swap_match is not None and int(swap_match.group(1)) > 0, (
708+
"no ModelOpt FP8 linear swap in the mixed-precision run; FP8 path never engaged"
709+
)
710+
# ... and the mixed-precision runtime was installed on top of it.
711+
assert _MIXED_PRECISION_INSTALL_LOG in log, (
712+
"mixed precision was never installed despite --mixed-precision-first/last-steps"
713+
)
714+
715+
# Exact per-step precision schedule. num_steps is read from
716+
# _FP8_GENERATION_ARGS so the expectation cannot drift from the run.
717+
(num_steps,) = [int(a.split("=")[1]) for a in _FP8_GENERATION_ARGS if a.startswith("--num-steps=")]
718+
expected = (
719+
["W8A16"] * _MIXED_PRECISION_FIRST_STEPS
720+
+ ["W8A8"] * (num_steps - _MIXED_PRECISION_FIRST_STEPS - _MIXED_PRECISION_LAST_STEPS)
721+
+ ["W8A16"] * _MIXED_PRECISION_LAST_STEPS
722+
)
723+
traces = [m.split(",") for m in _MIXED_PRECISION_TRACE_LOG.findall(log)]
724+
assert expected in traces, (
725+
f"expected a MIXED_PRECISION_TRACE of {'/'.join(expected)}, got traces={traces}"
726+
)
727+
728+
results = sorted(out_dir.rglob("sample_outputs.json"))
729+
assert len(results) == 1, f"expected 1 mixed-precision sample_outputs.json, found {[str(p) for p in results]}"
730+
so = results[0]
731+
args = json.loads(so.read_text()).get("args", {})
732+
assert args.get("model_mode") == "text2video", f"expected a text2video sample, got {args.get('model_mode')}"
733+
video = so.parent / "vision.mp4"
734+
assert video.is_file(), f"FP8 mixed-precision run produced no vision.mp4 ({so})"
735+
_assert_video_has_content(video)

0 commit comments

Comments
 (0)