|
37 | 37 | layout — the FP8 all-gather. Skipped when the checkpoint is not reachable |
38 | 38 | (see ``_download_fp8_checkpoint``). |
39 | 39 |
|
| 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 | +
|
40 | 47 | Smoke-level only (output validity, not numeric goldens). The checkpoint + its |
41 | 48 | tokenizers download from the HF Hub on first run and are reused afterward. |
42 | 49 |
|
|
216 | 223 | ), |
217 | 224 | } |
218 | 225 |
|
| 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 | + |
219 | 242 | # Audio sanity thresholds for the muxed sound track. |
220 | 243 | _RMS_SILENCE_FLOOR = 1e-4 # below this the track is effectively silence |
221 | 244 | _PEAK_SANITY_CEIL = 1.5 # decoded float audio should sit within ~[-1, 1] |
@@ -354,22 +377,32 @@ def _download_fp8_checkpoint() -> Path: |
354 | 377 | nvidia/Cosmos3-Experimental), so a fork PR without the runner secret does not |
355 | 378 | go red. Any other failure (a deleted revision, a broken download) still fails |
356 | 379 | 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. |
357 | 385 | """ |
358 | 386 | from huggingface_hub import snapshot_download |
359 | 387 | from huggingface_hub.errors import GatedRepoError, HfHubHTTPError, RepositoryNotFoundError |
360 | 388 |
|
| 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 | + |
361 | 394 | try: |
362 | 395 | repo_root = snapshot_download( |
363 | 396 | repo_id=_FP8_REPOSITORY, |
364 | 397 | revision=_FP8_REVISION, |
365 | 398 | allow_patterns=[f"{_FP8_SUBDIRECTORY}/*"], |
366 | 399 | ) |
367 | 400 | 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}") |
369 | 402 | except HfHubHTTPError as error: |
370 | 403 | status_code = getattr(error.response, "status_code", None) |
371 | 404 | 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}") |
373 | 406 | raise |
374 | 407 |
|
375 | 408 | checkpoint_path = Path(repo_root) / _FP8_SUBDIRECTORY |
@@ -627,3 +660,76 @@ def test_nano_fp8_inference(tmp_path: Path, layout: str) -> None: |
627 | 660 | video = so.parent / "vision.mp4" |
628 | 661 | assert video.is_file(), f"FP8 {layout} run produced no vision.mp4 ({so})" |
629 | 662 | _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