From 88d8c1ca49018935ca592b873afbb16572434b71 Mon Sep 17 00:00:00 2001 From: Jonathan McCaffrey Date: Fri, 15 May 2026 07:22:04 +0000 Subject: [PATCH 1/3] Add CD HF inference asset verification --- .github/workflows/cd.yml | 49 +++ .../tests/test_readme_hf_inference_assets.py | 296 ++++++++++++++++++ 2 files changed, 345 insertions(+) create mode 100644 .github/workflows/cd.yml create mode 100644 flashdreams/tests/test_readme_hf_inference_assets.py diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 000000000..3a6389327 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,49 @@ +name: CD + +on: + push: + branches: + - main + tags: + - "**" + workflow_dispatch: + +jobs: + hf-readme-inference-assets: + name: HF README inference assets + runs-on: linux-amd64-gpu-rtxpro6000-latest-2 + permissions: + contents: read + env: + FLASHDREAMS_HF_ACCESS_TEST: "1" + FLASHDREAMS_TEST_IMAGE: ghcr.io/nvidia/flashdreams:base-v0.3-20260424-55bd566 + FLASHDREAMS_UV_CACHE_DIR: ~/.cache/flashdreams-hf-access-test/uv + FLASHDREAMS_HF_CACHE_DIR: ~/.cache/flashdreams-hf-access-test/huggingface + FLASHDREAMS_CACHE_DIR: ~/.cache/flashdreams-hf-access-test/flashdreams + FLASHDREAMS_TRITON_CACHE_DIR: ~/.cache/flashdreams-hf-access-test/triton + HF_TOKEN: ${{ secrets.HF_TOKEN }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Restore HF verification caches + uses: actions/cache@v4 + with: + path: | + ~/.cache/flashdreams-hf-access-test/uv + ~/.cache/flashdreams-hf-access-test/huggingface + ~/.cache/flashdreams-hf-access-test/flashdreams + ~/.cache/flashdreams-hf-access-test/triton + key: flashdreams-hf-access-${{ runner.os }}-${{ hashFiles('uv.lock') }} + restore-keys: | + flashdreams-hf-access-${{ runner.os }}- + + - name: Verify HF token is configured + run: | + test -n "${HF_TOKEN}" || { + echo "::error::HF_TOKEN secret is required for CD HF asset verification" + exit 1 + } + + - name: Verify README HF inference assets + run: python3 flashdreams/tests/test_readme_hf_inference_assets.py diff --git a/flashdreams/tests/test_readme_hf_inference_assets.py b/flashdreams/tests/test_readme_hf_inference_assets.py new file mode 100644 index 000000000..7e5317210 --- /dev/null +++ b/flashdreams/tests/test_readme_hf_inference_assets.py @@ -0,0 +1,296 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""CD-only verification for README inference commands that touch HF assets. + +The test intentionally runs through ``docker run`` from the host. It keeps the +network-dependent Hugging Face checks out of regular CI while exercising the +same prebuilt container path users follow from the README. +""" + +from __future__ import annotations + +import os +import shutil +import subprocess +from pathlib import Path + +try: + import pytest +except ModuleNotFoundError: # pragma: no cover - direct script mode in CD. + pytest = None # type: ignore[assignment] + +if pytest is not None: + pytestmark = [pytest.mark.manual, pytest.mark.slow] +else: + pytestmark = () + + +REPO_ROOT = Path(__file__).resolve().parents[2] +DEFAULT_IMAGE = "ghcr.io/nvidia/flashdreams:base-v0.3-20260424-55bd566" +ENABLE_ENV_VAR = "FLASHDREAMS_HF_ACCESS_TEST" +README_COMMANDS = ( + ( + "alpadreams-sv-2steps-chunk2-loc6-lightvae-lighttae-perf", + "uv run flashdreams-run " + "alpadreams-sv-2steps-chunk2-loc6-lightvae-lighttae-perf " + "--example-data True --total-blocks 20 --no-instantiate", + ), + ( + "wan21-i2v-14b-480p", + "uv run flashdreams-run wan21-i2v-14b-480p --no-instantiate", + ), +) + + +def _skip(message: str) -> None: + if pytest is not None: + pytest.skip(message) + print(f"SKIP: {message}") + + +def _require_enabled() -> bool: + if os.getenv(ENABLE_ENV_VAR) == "1": + return True + _skip(f"set {ENABLE_ENV_VAR}=1 to run the HF access verification") + return False + + +def _require_hf_token() -> str: + token = os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN") + if not token: + raise AssertionError("HF_TOKEN or HUGGING_FACE_HUB_TOKEN is required") + return token + + +def _cache_dir(env_var: str, leaf: str) -> Path: + default_root = Path("~/.cache/flashdreams-hf-access-test").expanduser() + return Path(os.getenv(env_var, str(default_root / leaf))).expanduser() + + +def _docker_env(token: str) -> dict[str, str]: + env = os.environ.copy() + env["HF_TOKEN"] = token + env.setdefault("HUGGING_FACE_HUB_TOKEN", token) + return env + + +def _docker_command(image: str, cache_dirs: dict[str, Path]) -> list[str]: + cmd = [ + "docker", + "run", + "--rm", + "-i", + "--gpus", + "all", + "--ipc=host", + "--ulimit", + "memlock=-1", + "--ulimit", + "stack=67108864", + "-v", + f"{REPO_ROOT}:/workspace/flashdreams", + "-v", + f"{cache_dirs['uv']}:/root/.cache/uv", + "-v", + f"{cache_dirs['hf']}:/root/.cache/huggingface", + "-v", + f"{cache_dirs['flashdreams']}:/root/.cache/flashdreams", + "-v", + f"{cache_dirs['triton']}:/root/.cache/triton", + "-e", + "HF_TOKEN", + "-e", + "HUGGING_FACE_HUB_TOKEN", + "-e", + "HF_HOME=/root/.cache/huggingface", + "-e", + "FLASHDREAMS_CACHE_DIR=/root/.cache/flashdreams", + "-e", + "TRITON_CACHE_DIR=/root/.cache/triton", + "-e", + "UV_LINK_MODE=copy", + "-e", + "UV_PROJECT_ENVIRONMENT=/tmp/flashdreams-venv", + "-w", + "/workspace/flashdreams", + ] + netrc = Path.home() / ".netrc" + if netrc.exists(): + cmd.extend(["-v", f"{netrc}:/root/.netrc:ro"]) + cmd.extend([image, "bash", "-lc", _inner_script()]) + return cmd + + +def _inner_script() -> str: + command_block = "\n".join( + f"echo '[hf-access] resolving README command: {name}'\n{command}" + for name, command in README_COMMANDS + ) + return f""" +set -euo pipefail + +uv venv --clear +uv sync --frozen --extra runners + +{command_block} + +uv run python - <<'PY' +from __future__ import annotations + +import json +import os +from pathlib import PurePosixPath +from urllib.parse import unquote, urlparse + +from huggingface_hub import HfApi, hf_hub_download + +from flashdreams.recipes.alpadreams.config import ALPADREAMS_RUNNERS +from flashdreams.recipes.alpadreams.runner import ( + DEFAULT_EXAMPLE_DATA_UUID_1V, + _ensure_hf_single_view_example_data_synced, +) +from wan21.config import RUNNER_WAN21_I2V_14B_480P + + +def parse_hf_file_url(url: str) -> tuple[str, str, str]: + parsed = urlparse(url) + if parsed.netloc.lower().removeprefix("www.") != "huggingface.co": + raise AssertionError(f"expected a Hugging Face URL, got {{url!r}}") + parts = [unquote(part) for part in parsed.path.strip("/").split("/") if part] + if len(parts) < 5 or parts[2] not in ("blob", "resolve"): + raise AssertionError(f"unsupported Hugging Face file URL: {{url}}") + repo_id = f"{{parts[0]}}/{{parts[1]}}" + revision = parts[3] + filename = "/".join(parts[4:]) + return repo_id, filename, revision + + +def visible_repo_files( + api: HfApi, + *, + repo_id: str, + filename: str, + revision: str, + repo_type: str | None = None, +) -> set[str]: + parent = str(PurePosixPath(filename).parent) + path_in_repo = "" if parent == "." else parent + entries = api.list_repo_tree( + repo_id=repo_id, + repo_type=repo_type, + revision=revision, + path_in_repo=path_in_repo, + recursive=False, + ) + return {{ + entry.path + for entry in entries + if entry.__class__.__name__ == "RepoFile" + }} + + +def assert_hf_file_visible(api: HfApi, url: str, *, repo_type: str | None = None): + repo_id, filename, revision = parse_hf_file_url(url) + files = visible_repo_files( + api, + repo_id=repo_id, + filename=filename, + revision=revision, + repo_type=repo_type, + ) + if filename not in files: + raise AssertionError(f"{{filename}} not visible in {{repo_id}}@{{revision}}") + print(f"[hf-access] visible: {{repo_id}}/{{filename}}") + return repo_id, filename, revision + + +token = os.environ.get("HF_TOKEN") or os.environ["HUGGING_FACE_HUB_TOKEN"] +api = HfApi(token=token) +api.whoami(token=token) +print("[hf-access] authenticated to Hugging Face") + +alpa_slug = "alpadreams-sv-2steps-chunk2-loc6-lightvae-lighttae-perf" +alpa = ALPADREAMS_RUNNERS[alpa_slug].pipeline +alpa_paths = [ + alpa.image_encoder.checkpoint_path, + alpa.encoder.checkpoint_path, + alpa.decoder.checkpoint_path, + alpa.diffusion_model.transformer.checkpoint_path, +] +for path in alpa_paths: + assert_hf_file_visible(api, path) + +hdmaps, first_frames = _ensure_hf_single_view_example_data_synced( + DEFAULT_EXAMPLE_DATA_UUID_1V +) +for path in (*hdmaps, *first_frames): + if not path.exists() or path.stat().st_size <= 0: + raise AssertionError(f"downloaded example asset is empty or missing: {{path}}") + print(f"[hf-access] downloaded example asset: {{path.name}}") + +wan = RUNNER_WAN21_I2V_14B_480P.pipeline +wan_index_url = wan.diffusion_model.transformer.checkpoint_path +repo_id, filename, revision = assert_hf_file_visible(api, wan_index_url) +index_parent = str(PurePosixPath(filename).parent) +index_path = hf_hub_download( + repo_id=repo_id, + filename=PurePosixPath(filename).name, + subfolder=None if index_parent == "." else index_parent, + revision=revision, + token=token, +) +with open(index_path) as f: + index = json.load(f) +weight_map = index.get("weight_map") +if not isinstance(weight_map, dict) or not weight_map: + raise AssertionError(f"invalid sharded checkpoint index: {{index_path}}") +print(f"[hf-access] downloaded Wan I2V checkpoint index with {{len(weight_map)}} tensors") + +image_encoder_id = wan.image_encoder.model_id_or_local_path +api.model_info(image_encoder_id, token=token) +print(f"[hf-access] visible: {{image_encoder_id}}") + +print("[hf-access] README HF inference asset verification passed") +PY +""" + + +def test_readme_hf_inference_assets_in_docker() -> None: + if not _require_enabled(): + return + if shutil.which("docker") is None: + raise AssertionError("docker is required for the HF access verification") + + token = _require_hf_token() + image = os.getenv("FLASHDREAMS_TEST_IMAGE", DEFAULT_IMAGE) + cache_dirs = { + "uv": _cache_dir("FLASHDREAMS_UV_CACHE_DIR", "uv"), + "hf": _cache_dir("FLASHDREAMS_HF_CACHE_DIR", "huggingface"), + "flashdreams": _cache_dir("FLASHDREAMS_CACHE_DIR", "flashdreams"), + "triton": _cache_dir("FLASHDREAMS_TRITON_CACHE_DIR", "triton"), + } + for cache_dir in cache_dirs.values(): + cache_dir.mkdir(parents=True, exist_ok=True) + + subprocess.run( + _docker_command(image, cache_dirs), + check=True, + env=_docker_env(token), + ) + + +if __name__ == "__main__": + test_readme_hf_inference_assets_in_docker() From 723144f46d31518655e212de2dc423c4548c52b9 Mon Sep 17 00:00:00 2001 From: Jonathan Mccaffrey Date: Wed, 20 May 2026 22:55:18 -0700 Subject: [PATCH 2/3] Fix ty type check on optional pytest import The optional-import fallback used mypy/pyright's `# type: ignore[assignment]` syntax, which ty does not recognize. Rename the import binding to `_pytest` and use the ty-native `# ty:ignore[invalid-assignment]` directive. Co-Authored-By: Claude Opus 4.7 (1M context) --- flashdreams/tests/test_readme_hf_inference_assets.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flashdreams/tests/test_readme_hf_inference_assets.py b/flashdreams/tests/test_readme_hf_inference_assets.py index 7e5317210..b3340f506 100644 --- a/flashdreams/tests/test_readme_hf_inference_assets.py +++ b/flashdreams/tests/test_readme_hf_inference_assets.py @@ -28,12 +28,12 @@ from pathlib import Path try: - import pytest + import pytest as _pytest except ModuleNotFoundError: # pragma: no cover - direct script mode in CD. - pytest = None # type: ignore[assignment] + _pytest = None # ty:ignore[invalid-assignment] -if pytest is not None: - pytestmark = [pytest.mark.manual, pytest.mark.slow] +if _pytest is not None: + pytestmark = [_pytest.mark.manual, _pytest.mark.slow] else: pytestmark = () @@ -56,8 +56,8 @@ def _skip(message: str) -> None: - if pytest is not None: - pytest.skip(message) + if _pytest is not None: + _pytest.skip(message) print(f"SKIP: {message}") From b1d8fec3c2fa846d94b1e362e01d95e9ec811a04 Mon Sep 17 00:00:00 2001 From: Jonathan Mccaffrey Date: Thu, 21 May 2026 21:02:54 -0700 Subject: [PATCH 3/3] Refactor HF inference asset verification Replace the docker-based test driver with a standalone check script and wire it into both CI and CD via a uniform in-venv invocation. - Extract the previously-embedded "_inner_script" heredoc into `flashdreams/tests/check_hf_inference_assets.py`. The script resolves every HF asset referenced by the README's headline recipes (`omnidreams-sv-2steps-chunk2-loc6-lightvae-lighttae-perf` and `wan21-i2v-14b-480p`) and downloads one small file per unique repo, so gated-access and token-scope regressions surface as failed downloads rather than visibility-only false negatives. - Delete the old docker driver. The prebuilt base image that drove its default never ships publicly; only the Dockerfile recipe on top of a CUDA base will. Pinning that internal image in source-of-truth code was misleading. - CI: add a single step in the existing cpu job that runs the script when HF_TOKEN is available; skip with a warning otherwise so external fork PRs do not block. - CD: replace the docker-run job with the same script, executed inside the public `nvidia/cuda` container. Drops four FLASHDREAMS_* cache-dir env vars and the FLASHDREAMS_HF_ACCESS_TEST enable flag (only HF_TOKEN remains). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/cd.yml | 44 +-- .github/workflows/ci.yml | 13 + .../tests/check_hf_inference_assets.py | 164 ++++++++++ .../tests/test_readme_hf_inference_assets.py | 296 ------------------ 4 files changed, 202 insertions(+), 315 deletions(-) create mode 100644 flashdreams/tests/check_hf_inference_assets.py delete mode 100644 flashdreams/tests/test_readme_hf_inference_assets.py diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 3a6389327..52d6cc580 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -11,32 +11,38 @@ on: jobs: hf-readme-inference-assets: name: HF README inference assets - runs-on: linux-amd64-gpu-rtxpro6000-latest-2 + runs-on: linux-amd64-cpu8 + container: + image: nvidia/cuda:13.2.1-cudnn-devel-ubuntu24.04 permissions: contents: read env: - FLASHDREAMS_HF_ACCESS_TEST: "1" - FLASHDREAMS_TEST_IMAGE: ghcr.io/nvidia/flashdreams:base-v0.3-20260424-55bd566 - FLASHDREAMS_UV_CACHE_DIR: ~/.cache/flashdreams-hf-access-test/uv - FLASHDREAMS_HF_CACHE_DIR: ~/.cache/flashdreams-hf-access-test/huggingface - FLASHDREAMS_CACHE_DIR: ~/.cache/flashdreams-hf-access-test/flashdreams - FLASHDREAMS_TRITON_CACHE_DIR: ~/.cache/flashdreams-hf-access-test/triton + UV_PROJECT_ENVIRONMENT: /tmp/flashdreams-venv + UV_LINK_MODE: copy HF_TOKEN: ${{ secrets.HF_TOKEN }} steps: + - name: Install system dependencies + run: | + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \ + python3 python3-dev python3-venv \ + git curl ca-certificates + - name: Checkout uses: actions/checkout@v4 - - name: Restore HF verification caches - uses: actions/cache@v4 + - name: Setup uv + uses: astral-sh/setup-uv@v6 with: - path: | - ~/.cache/flashdreams-hf-access-test/uv - ~/.cache/flashdreams-hf-access-test/huggingface - ~/.cache/flashdreams-hf-access-test/flashdreams - ~/.cache/flashdreams-hf-access-test/triton - key: flashdreams-hf-access-${{ runner.os }}-${{ hashFiles('uv.lock') }} - restore-keys: | - flashdreams-hf-access-${{ runner.os }}- + enable-cache: true + cache-suffix: "cd-hf-access" + + - name: Install workspace dependencies + env: + BLOCK_SPARSE_ATTN_SKIP_CUDA_BUILD: "TRUE" + run: | + uv venv --clear + uv sync --extra runners --no-install-package transformer-engine-torch - name: Verify HF token is configured run: | @@ -45,5 +51,5 @@ jobs: exit 1 } - - name: Verify README HF inference assets - run: python3 flashdreams/tests/test_readme_hf_inference_assets.py + - name: Verify HF inference assets + run: uv run --no-sync python flashdreams/tests/check_hf_inference_assets.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d7a6753d..23153fd42 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,6 +83,19 @@ jobs: - name: Run CPU unit tests run: uv run --no-sync pytest -m ci_cpu -v + # Forces an authenticated HF round-trip for the README's headline + # recipes. Skipped on event contexts where the secret isn't exposed + # (e.g. external fork PRs). + - name: Verify HF inference assets + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + run: | + if [ -z "${HF_TOKEN}" ]; then + echo "::warning::HF_TOKEN not set; skipping HF inference asset verification" + exit 0 + fi + uv run --no-sync python flashdreams/tests/check_hf_inference_assets.py + # Documentation is built and deployed by `.github/workflows/doc.yml`. gpu: diff --git a/flashdreams/tests/check_hf_inference_assets.py b/flashdreams/tests/check_hf_inference_assets.py new file mode 100644 index 000000000..902d6638f --- /dev/null +++ b/flashdreams/tests/check_hf_inference_assets.py @@ -0,0 +1,164 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Verify Hugging Face access to README inference assets. + +Resolves the asset paths referenced by the README's headline recipes +(``omnidreams-sv-2steps-chunk2-loc6-lightvae-lighttae-perf`` and +``wan21-i2v-14b-480p``) and downloads one small file per HF repo using +``HF_TOKEN``. Forces an authenticated round-trip, so gated-repo and +token-scope regressions surface here rather than at user runtime. + +Usage:: + + HF_TOKEN=hf_... uv run --no-sync python flashdreams/tests/check_hf_inference_assets.py +""" + +from __future__ import annotations + +import os +import sys +from urllib.parse import unquote, urlparse + +from huggingface_hub import hf_hub_download +from huggingface_hub.errors import EntryNotFoundError + +from omnidreams.config import OMNIDREAMS_RUNNERS +from wan21.config import RUNNER_WAN21_I2V_14B_480P + +OMNIDREAMS_SLUG = "omnidreams-sv-2steps-chunk2-loc6-lightvae-lighttae-perf" + +# (component-attr, url-attr) pairs walked off each pipeline. Pipelines use +# recipe-specific subclasses whose abstract bases do not declare these +# attrs, so the script reads them via ``getattr`` instead of typed access. +_COMPONENTS: tuple[tuple[str, str], ...] = ( + ("image_encoder", "checkpoint_path"), + ("image_encoder", "model_id_or_local_path"), + ("encoder", "checkpoint_path"), + ("decoder", "checkpoint_path"), +) + +# Small files commonly present in HF model repos, tried in order. The first +# one that exists is downloaded; if none exist, the repo's referenced file +# is downloaded directly only if it is itself small (``.json``). +_PROBE_FILES = ("config.json", "model_index.json", "README.md") + + +def _parse_hf_url(url: str) -> tuple[str, str, str] | None: + """Return ``(repo_id, filename, revision)`` for HF blob/resolve URLs.""" + parsed = urlparse(url) + if parsed.netloc.lower().removeprefix("www.") != "huggingface.co": + return None + parts = [unquote(p) for p in parsed.path.strip("/").split("/") if p] + if len(parts) < 5 or parts[2] not in ("blob", "resolve"): + return None + return f"{parts[0]}/{parts[1]}", "/".join(parts[4:]), parts[3] + + +def _download(*, repo_id: str, filename: str, revision: str, token: str) -> str: + return hf_hub_download( + repo_id=repo_id, filename=filename, revision=revision, token=token + ) + + +def _probe(repo_id: str, revision: str, *, token: str) -> str: + """Download the first existing ``_PROBE_FILES`` entry; raise otherwise.""" + last: Exception | None = None + for name in _PROBE_FILES: + try: + return _download( + repo_id=repo_id, filename=name, revision=revision, token=token + ) + except EntryNotFoundError as exc: + last = exc + raise AssertionError( + f"no probe file ({', '.join(_PROBE_FILES)}) downloadable " + f"from {repo_id}@{revision}: {last}" + ) + + +def _verify(label: str, url_or_id: str, *, token: str, seen: set[str]) -> None: + """Verify HF access for ``url_or_id``; dedupes via ``seen``.""" + parsed = _parse_hf_url(url_or_id) + if parsed is not None: + repo_id, filename, revision = parsed + key = f"{repo_id}@{revision}" + if key in seen: + print(f" {label}: {key} (already verified)") + return + seen.add(key) + if filename.endswith(".json"): + path = _download( + repo_id=repo_id, filename=filename, revision=revision, token=token + ) + else: + path = _probe(repo_id, revision, token=token) + elif "/" in url_or_id and not url_or_id.startswith(("http", "s3", "/")): + key = f"{url_or_id}@main" + if key in seen: + print(f" {label}: {key} (already verified)") + return + seen.add(key) + path = _probe(url_or_id, "main", token=token) + else: + print(f" {label}: skipped non-HF path {url_or_id!r}") + return + print(f" {label}: ok -> {path}") + + +def _pipeline_urls(pipeline: object) -> list[tuple[str, str]]: + """Collect ``(label, url)`` pairs for every HF asset on ``pipeline``.""" + seen_labels: set[str] = set() + urls: list[tuple[str, str]] = [] + for component, attr in _COMPONENTS: + if component in seen_labels: + continue + cfg = getattr(pipeline, component, None) + if cfg is None: + continue + url = getattr(cfg, attr, None) + if isinstance(url, str) and url: + urls.append((component, url)) + seen_labels.add(component) + diffusion = getattr(pipeline, "diffusion_model", None) + transformer = getattr(diffusion, "transformer", None) if diffusion else None + url = getattr(transformer, "checkpoint_path", None) if transformer else None + if isinstance(url, str) and url: + urls.append(("transformer", url)) + return urls + + +def main() -> int: + token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN") + if not token: + print("HF_TOKEN or HUGGING_FACE_HUB_TOKEN is required", file=sys.stderr) + return 1 + + seen: set[str] = set() + recipes: tuple[tuple[str, object], ...] = ( + (OMNIDREAMS_SLUG, OMNIDREAMS_RUNNERS[OMNIDREAMS_SLUG].pipeline), + ("wan21-i2v-14b-480p", RUNNER_WAN21_I2V_14B_480P.pipeline), + ) + for slug, pipeline in recipes: + print(f"recipe: {slug}") + for label, url in _pipeline_urls(pipeline): + _verify(label, url, token=token, seen=seen) + + print("HF inference asset verification passed") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/flashdreams/tests/test_readme_hf_inference_assets.py b/flashdreams/tests/test_readme_hf_inference_assets.py deleted file mode 100644 index b3340f506..000000000 --- a/flashdreams/tests/test_readme_hf_inference_assets.py +++ /dev/null @@ -1,296 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""CD-only verification for README inference commands that touch HF assets. - -The test intentionally runs through ``docker run`` from the host. It keeps the -network-dependent Hugging Face checks out of regular CI while exercising the -same prebuilt container path users follow from the README. -""" - -from __future__ import annotations - -import os -import shutil -import subprocess -from pathlib import Path - -try: - import pytest as _pytest -except ModuleNotFoundError: # pragma: no cover - direct script mode in CD. - _pytest = None # ty:ignore[invalid-assignment] - -if _pytest is not None: - pytestmark = [_pytest.mark.manual, _pytest.mark.slow] -else: - pytestmark = () - - -REPO_ROOT = Path(__file__).resolve().parents[2] -DEFAULT_IMAGE = "ghcr.io/nvidia/flashdreams:base-v0.3-20260424-55bd566" -ENABLE_ENV_VAR = "FLASHDREAMS_HF_ACCESS_TEST" -README_COMMANDS = ( - ( - "alpadreams-sv-2steps-chunk2-loc6-lightvae-lighttae-perf", - "uv run flashdreams-run " - "alpadreams-sv-2steps-chunk2-loc6-lightvae-lighttae-perf " - "--example-data True --total-blocks 20 --no-instantiate", - ), - ( - "wan21-i2v-14b-480p", - "uv run flashdreams-run wan21-i2v-14b-480p --no-instantiate", - ), -) - - -def _skip(message: str) -> None: - if _pytest is not None: - _pytest.skip(message) - print(f"SKIP: {message}") - - -def _require_enabled() -> bool: - if os.getenv(ENABLE_ENV_VAR) == "1": - return True - _skip(f"set {ENABLE_ENV_VAR}=1 to run the HF access verification") - return False - - -def _require_hf_token() -> str: - token = os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN") - if not token: - raise AssertionError("HF_TOKEN or HUGGING_FACE_HUB_TOKEN is required") - return token - - -def _cache_dir(env_var: str, leaf: str) -> Path: - default_root = Path("~/.cache/flashdreams-hf-access-test").expanduser() - return Path(os.getenv(env_var, str(default_root / leaf))).expanduser() - - -def _docker_env(token: str) -> dict[str, str]: - env = os.environ.copy() - env["HF_TOKEN"] = token - env.setdefault("HUGGING_FACE_HUB_TOKEN", token) - return env - - -def _docker_command(image: str, cache_dirs: dict[str, Path]) -> list[str]: - cmd = [ - "docker", - "run", - "--rm", - "-i", - "--gpus", - "all", - "--ipc=host", - "--ulimit", - "memlock=-1", - "--ulimit", - "stack=67108864", - "-v", - f"{REPO_ROOT}:/workspace/flashdreams", - "-v", - f"{cache_dirs['uv']}:/root/.cache/uv", - "-v", - f"{cache_dirs['hf']}:/root/.cache/huggingface", - "-v", - f"{cache_dirs['flashdreams']}:/root/.cache/flashdreams", - "-v", - f"{cache_dirs['triton']}:/root/.cache/triton", - "-e", - "HF_TOKEN", - "-e", - "HUGGING_FACE_HUB_TOKEN", - "-e", - "HF_HOME=/root/.cache/huggingface", - "-e", - "FLASHDREAMS_CACHE_DIR=/root/.cache/flashdreams", - "-e", - "TRITON_CACHE_DIR=/root/.cache/triton", - "-e", - "UV_LINK_MODE=copy", - "-e", - "UV_PROJECT_ENVIRONMENT=/tmp/flashdreams-venv", - "-w", - "/workspace/flashdreams", - ] - netrc = Path.home() / ".netrc" - if netrc.exists(): - cmd.extend(["-v", f"{netrc}:/root/.netrc:ro"]) - cmd.extend([image, "bash", "-lc", _inner_script()]) - return cmd - - -def _inner_script() -> str: - command_block = "\n".join( - f"echo '[hf-access] resolving README command: {name}'\n{command}" - for name, command in README_COMMANDS - ) - return f""" -set -euo pipefail - -uv venv --clear -uv sync --frozen --extra runners - -{command_block} - -uv run python - <<'PY' -from __future__ import annotations - -import json -import os -from pathlib import PurePosixPath -from urllib.parse import unquote, urlparse - -from huggingface_hub import HfApi, hf_hub_download - -from flashdreams.recipes.alpadreams.config import ALPADREAMS_RUNNERS -from flashdreams.recipes.alpadreams.runner import ( - DEFAULT_EXAMPLE_DATA_UUID_1V, - _ensure_hf_single_view_example_data_synced, -) -from wan21.config import RUNNER_WAN21_I2V_14B_480P - - -def parse_hf_file_url(url: str) -> tuple[str, str, str]: - parsed = urlparse(url) - if parsed.netloc.lower().removeprefix("www.") != "huggingface.co": - raise AssertionError(f"expected a Hugging Face URL, got {{url!r}}") - parts = [unquote(part) for part in parsed.path.strip("/").split("/") if part] - if len(parts) < 5 or parts[2] not in ("blob", "resolve"): - raise AssertionError(f"unsupported Hugging Face file URL: {{url}}") - repo_id = f"{{parts[0]}}/{{parts[1]}}" - revision = parts[3] - filename = "/".join(parts[4:]) - return repo_id, filename, revision - - -def visible_repo_files( - api: HfApi, - *, - repo_id: str, - filename: str, - revision: str, - repo_type: str | None = None, -) -> set[str]: - parent = str(PurePosixPath(filename).parent) - path_in_repo = "" if parent == "." else parent - entries = api.list_repo_tree( - repo_id=repo_id, - repo_type=repo_type, - revision=revision, - path_in_repo=path_in_repo, - recursive=False, - ) - return {{ - entry.path - for entry in entries - if entry.__class__.__name__ == "RepoFile" - }} - - -def assert_hf_file_visible(api: HfApi, url: str, *, repo_type: str | None = None): - repo_id, filename, revision = parse_hf_file_url(url) - files = visible_repo_files( - api, - repo_id=repo_id, - filename=filename, - revision=revision, - repo_type=repo_type, - ) - if filename not in files: - raise AssertionError(f"{{filename}} not visible in {{repo_id}}@{{revision}}") - print(f"[hf-access] visible: {{repo_id}}/{{filename}}") - return repo_id, filename, revision - - -token = os.environ.get("HF_TOKEN") or os.environ["HUGGING_FACE_HUB_TOKEN"] -api = HfApi(token=token) -api.whoami(token=token) -print("[hf-access] authenticated to Hugging Face") - -alpa_slug = "alpadreams-sv-2steps-chunk2-loc6-lightvae-lighttae-perf" -alpa = ALPADREAMS_RUNNERS[alpa_slug].pipeline -alpa_paths = [ - alpa.image_encoder.checkpoint_path, - alpa.encoder.checkpoint_path, - alpa.decoder.checkpoint_path, - alpa.diffusion_model.transformer.checkpoint_path, -] -for path in alpa_paths: - assert_hf_file_visible(api, path) - -hdmaps, first_frames = _ensure_hf_single_view_example_data_synced( - DEFAULT_EXAMPLE_DATA_UUID_1V -) -for path in (*hdmaps, *first_frames): - if not path.exists() or path.stat().st_size <= 0: - raise AssertionError(f"downloaded example asset is empty or missing: {{path}}") - print(f"[hf-access] downloaded example asset: {{path.name}}") - -wan = RUNNER_WAN21_I2V_14B_480P.pipeline -wan_index_url = wan.diffusion_model.transformer.checkpoint_path -repo_id, filename, revision = assert_hf_file_visible(api, wan_index_url) -index_parent = str(PurePosixPath(filename).parent) -index_path = hf_hub_download( - repo_id=repo_id, - filename=PurePosixPath(filename).name, - subfolder=None if index_parent == "." else index_parent, - revision=revision, - token=token, -) -with open(index_path) as f: - index = json.load(f) -weight_map = index.get("weight_map") -if not isinstance(weight_map, dict) or not weight_map: - raise AssertionError(f"invalid sharded checkpoint index: {{index_path}}") -print(f"[hf-access] downloaded Wan I2V checkpoint index with {{len(weight_map)}} tensors") - -image_encoder_id = wan.image_encoder.model_id_or_local_path -api.model_info(image_encoder_id, token=token) -print(f"[hf-access] visible: {{image_encoder_id}}") - -print("[hf-access] README HF inference asset verification passed") -PY -""" - - -def test_readme_hf_inference_assets_in_docker() -> None: - if not _require_enabled(): - return - if shutil.which("docker") is None: - raise AssertionError("docker is required for the HF access verification") - - token = _require_hf_token() - image = os.getenv("FLASHDREAMS_TEST_IMAGE", DEFAULT_IMAGE) - cache_dirs = { - "uv": _cache_dir("FLASHDREAMS_UV_CACHE_DIR", "uv"), - "hf": _cache_dir("FLASHDREAMS_HF_CACHE_DIR", "huggingface"), - "flashdreams": _cache_dir("FLASHDREAMS_CACHE_DIR", "flashdreams"), - "triton": _cache_dir("FLASHDREAMS_TRITON_CACHE_DIR", "triton"), - } - for cache_dir in cache_dirs.values(): - cache_dir.mkdir(parents=True, exist_ok=True) - - subprocess.run( - _docker_command(image, cache_dirs), - check=True, - env=_docker_env(token), - ) - - -if __name__ == "__main__": - test_readme_hf_inference_assets_in_docker()