Compare commits

..
Author SHA1 Message Date
Jinzhe Pan 15c79dda56 [ci]: fix fork PR checkout — pass PR_NUMBER through Buildkite to Modal for ref-based checkout 2026-03-31 01:32:20 -04:00
Jinzhe Pan f7ad5ef7e0 [ci]: remove deprecated delete_head_branch rule 2026-03-30 12:55:44 -04:00
Jinzhe Pan 2cb6bdd0b2 [ci]: remove Buildkite CI failure comment rule 2026-03-30 12:52:25 -04:00
Jinzhe Pan 4df8a7e209 [ci]: cancel old Buildkite builds and re-trigger Full Suite on new commits 2026-03-30 12:45:23 -04:00
Jinzhe Pan 6371f19638 [ci]: prevent auto-rebase from interrupting Full Suite — require full-suite-passed 2026-03-30 12:37:43 -04:00
Jinzhe Pan 78173b6741 [ci]: restore missing Trigger Full Suite label in pipeline.yml 2026-03-30 12:29:34 -04:00
Jinzhe Pan a3e1591f93 [docs]: update CI docs — replace Merge Queue with auto-merge flow 2026-03-30 12:20:39 -04:00
Jinzhe Pan 9225604322 [ci]: remove merge-queue branch exception from pre-commit draft skip 2026-03-30 12:17:11 -04:00
Jinzhe Pan 98f628a49c [ci]: simplify /merge command — remove @mergifyio queue, use label-based trigger 2026-03-30 12:16:45 -04:00
Jinzhe Pan cc4889de98 [ci]: update Buildkite notify and Full Suite gate — remove queue branch detection 2026-03-30 12:16:21 -04:00
Jinzhe Pan 05eb6ccc60 [ci]: recreate Full Suite trigger workflow — labeled event with ready label check 2026-03-30 12:15:42 -04:00
Jinzhe Pan 4da7beccbc [ci]: replace Merge Queue with auto-merge flow — add auto-rebase and CI failure label removal 2026-03-30 12:15:22 -04:00
9 changed files with 244 additions and 349 deletions
+3 -18
View File
@@ -5,32 +5,17 @@ env:
notify:
- github_commit_status:
context: "fastcheck-passed"
if: build.env("TEST_SCOPE") == "fastcheck" || build.env("TEST_SCOPE") == null
- github_commit_status:
context: "full-suite-passed"
if: build.env("TEST_SCOPE") == "full"
if: build.env("FULL_SUITE") == "true"
steps:
# ============================================================
- label: ":dart: Direct Test (${TEST_TYPE})"
if: build.env("TEST_SCOPE") == "direct"
command: "timeout 90m .buildkite/scripts/pr_test.sh"
retry:
automatic:
- exit_status: 128
limit: 3
- exit_status: -1
limit: 2
agents:
queue: "default"
# ============================================================
# Fastcheck: Runs on every PR (~10-15 min parallel)
# Core component validation: encoders, VAEs, transformers,
# CUDA kernels, and unit tests.
# ============================================================
- label: "Trigger Fastcheck"
if: build.env("TEST_SCOPE") == "fastcheck" || build.env("TEST_SCOPE") == null
retry:
automatic:
- exit_status: 128
@@ -108,14 +93,14 @@ steps:
queue: "default"
# ============================================================
# Full Suite: Runs when TEST_SCOPE=full
# Full Suite: Runs when FULL_SUITE=true
# Triggered by adding the 'ready' label (via ci-trigger-full-suite.yml)
# or on-demand via /test full slash command.
# Includes integration tests, SSIM regression, training pipelines,
# and performance benchmarks.
# ============================================================
- label: "Trigger Full Suite"
if: build.env("TEST_SCOPE") == "full"
if: build.env("FULL_SUITE") == "true"
retry:
automatic:
- exit_status: 128
+26 -26
View File
@@ -84,12 +84,12 @@ pull_request_rules:
- name: "label scope: training"
conditions:
- or:
- files~=^fastvideo/train/
- files~=^fastvideo/training/
- files~=^fastvideo/distillation/
- files~=^examples/train/
- files~=^examples/training/
- files~=^examples/distill/
- files~=^fastvideo/train/
- files~=^fastvideo/training/
- files~=^fastvideo/distillation/
- files~=^examples/train/
- files~=^examples/training/
- files~=^examples/distill/
- -closed
actions:
label:
@@ -98,14 +98,14 @@ pull_request_rules:
- name: "label scope: inference"
conditions:
- or:
- files~=^fastvideo/pipelines/basic/
- files~=^fastvideo/pipelines/stages/
- files~=^fastvideo/pipelines/samplers/
- files~=^fastvideo/entrypoints/
- files~=^fastvideo/worker/
- files~=^fastvideo/configs/sample/
- files~=^fastvideo/configs/pipelines/
- files~=^examples/inference/
- files~=^fastvideo/pipelines/basic/
- files~=^fastvideo/pipelines/stages/
- files~=^fastvideo/pipelines/samplers/
- files~=^fastvideo/entrypoints/
- files~=^fastvideo/worker/
- files~=^fastvideo/configs/sample/
- files~=^fastvideo/configs/pipelines/
- files~=^examples/inference/
- -closed
actions:
label:
@@ -122,8 +122,8 @@ pull_request_rules:
- name: "label scope: kernel"
conditions:
- or:
- files~=^fastvideo-kernel/
- files~=^csrc/
- files~=^fastvideo-kernel/
- files~=^csrc/
- -closed
actions:
label:
@@ -132,9 +132,9 @@ pull_request_rules:
- name: "label scope: data"
conditions:
- or:
- files~=^fastvideo/dataset/
- files~=^fastvideo/pipelines/preprocess/
- files~=^examples/preprocessing/
- files~=^fastvideo/dataset/
- files~=^fastvideo/pipelines/preprocess/
- files~=^examples/preprocessing/
- -closed
actions:
label:
@@ -143,10 +143,10 @@ pull_request_rules:
- name: "label scope: infra"
conditions:
- or:
- files~=^\.github/
- files~=^\.buildkite/
- files~=^fastvideo/tests/
- files~=^docker/
- files~=^\.github/
- files~=^\.buildkite/
- files~=^fastvideo/tests/
- files~=^docker/
- -closed
actions:
label:
@@ -179,9 +179,9 @@ pull_request_rules:
- name: "label scope: model"
conditions:
- or:
- files~=^fastvideo/models/
- files~=^fastvideo/layers/
- files~=^fastvideo/configs/models/
- files~=^fastvideo/models/
- files~=^fastvideo/layers/
- files~=^fastvideo/configs/models/
- -closed
actions:
label:
+1 -1
View File
@@ -14,7 +14,7 @@ permissions:
jobs:
pre-commit:
if: github.event_name == 'workflow_call' || github.event.pull_request.draft != true
if: github.event.pull_request.draft != true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
+18 -90
View File
@@ -54,7 +54,6 @@ jobs:
runs-on: ubuntu-latest
outputs:
test_type: ${{ steps.parse.outputs.test_type }}
test_scope: ${{ steps.parse.outputs.test_scope }}
full_suite: ${{ steps.parse.outputs.full_suite }}
pr_sha: ${{ steps.pr.outputs.sha }}
pr_branch: ${{ steps.pr.outputs.branch }}
@@ -86,7 +85,7 @@ jobs:
set -euo pipefail
TEST_NAME=$(echo "$COMMENT" | grep -oP '(?<=/test\s)\S+' | head -1 || true)
VALID="encoder vae transformer kernel unit ssim training lora-inference lora-training distillation self-forcing vsa vmoba performance api full fastcheck pre-commit"
VALID="encoder vae transformer kernel unit ssim training lora-inference lora-training distillation self-forcing vsa vmoba performance api full fastcheck"
if [ -z "$TEST_NAME" ] || ! echo "$VALID" | grep -qw "$TEST_NAME"; then
echo "Unknown test: '$TEST_NAME'. Valid: $VALID"
exit 1
@@ -103,29 +102,14 @@ jobs:
)
if [ "$TEST_NAME" = "full" ]; then
{
echo "test_type=all"
echo "test_scope=full"
echo "full_suite=true"
} >> "$GITHUB_OUTPUT"
echo "test_type=all" >> "$GITHUB_OUTPUT"
echo "full_suite=true" >> "$GITHUB_OUTPUT"
elif [ "$TEST_NAME" = "fastcheck" ]; then
{
echo "test_type=fastcheck"
echo "test_scope=fastcheck"
echo "full_suite=false"
} >> "$GITHUB_OUTPUT"
elif [ "$TEST_NAME" = "pre-commit" ]; then
{
echo "test_type="
echo "test_scope=precommit"
echo "full_suite=false"
} >> "$GITHUB_OUTPUT"
echo "test_type=fastcheck" >> "$GITHUB_OUTPUT"
echo "full_suite=false" >> "$GITHUB_OUTPUT"
else
{
echo "test_type=${MAP[$TEST_NAME]}"
echo "test_scope=direct"
echo "full_suite=false"
} >> "$GITHUB_OUTPUT"
echo "test_type=${MAP[$TEST_NAME]}" >> "$GITHUB_OUTPUT"
echo "full_suite=true" >> "$GITHUB_OUTPUT"
fi
- name: Get PR details
@@ -142,35 +126,6 @@ jobs:
core.setOutput('sha', pr.head.sha);
core.setOutput('branch', pr.head.ref);
pre-commit:
needs: parse-command
if: >-
needs.parse-command.outputs.has_write == 'true'
&& needs.parse-command.outputs.test_scope == 'precommit'
uses: ./.github/workflows/ci-precommit.yml
post-precommit-status:
needs: [parse-command, pre-commit]
if: always() && needs.parse-command.outputs.test_scope == 'precommit'
runs-on: ubuntu-latest
steps:
- name: Post commit status
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
env:
PR_SHA: ${{ needs.parse-command.outputs.pr_sha }}
RESULT: ${{ needs.pre-commit.result }}
with:
script: |
const state = process.env.RESULT === 'success' ? 'success' : 'failure';
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: process.env.PR_SHA,
state,
context: 'pre-commit',
description: `Triggered via /test pre-commit (${state})`,
});
trigger-buildkite:
needs: parse-command
if: >-
@@ -190,41 +145,14 @@ jobs:
});
- name: Trigger Buildkite
env:
BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_API_TOKEN }}
PR_SHA: ${{ needs.parse-command.outputs.pr_sha }}
PR_BRANCH: ${{ needs.parse-command.outputs.pr_branch }}
PR_NUMBER: ${{ github.event.issue.number }}
TEST_SCOPE: ${{ needs.parse-command.outputs.test_scope }}
FULL_SUITE: ${{ needs.parse-command.outputs.full_suite }}
TEST_TYPE: ${{ needs.parse-command.outputs.test_type }}
BK_ORG: ${{ vars.BUILDKITE_ORG_SLUG }}
BK_PIPELINE: ${{ vars.BUILDKITE_PIPELINE_SLUG }}
run: |
curl -sS --fail-with-body -X POST \
"https://api.buildkite.com/v2/organizations/${BK_ORG}/pipelines/${BK_PIPELINE}/builds" \
-H "Authorization: Bearer $BUILDKITE_API_TOKEN" \
-H "Content-Type: application/json" \
--data-raw "$(jq -n \
--arg commit "$PR_SHA" \
--arg branch "$PR_BRANCH" \
--arg message "/test ${TEST_TYPE} on PR #${PR_NUMBER}" \
--argjson pr_id "$PR_NUMBER" \
--arg test_scope "$TEST_SCOPE" \
--arg full_suite "$FULL_SUITE" \
--arg test_type "$TEST_TYPE" \
--arg pr_number "$PR_NUMBER" \
'{
commit: $commit,
branch: $branch,
message: $message,
ignore_pipeline_branch_filters: true,
pull_request_id: $pr_id,
pull_request_base_branch: "main",
env: {
TEST_SCOPE: $test_scope,
FULL_SUITE: $full_suite,
TEST_TYPE: $test_type,
PR_NUMBER: $pr_number
}
}')"
uses: buildkite/trigger-pipeline-action@v2.4.1
with:
buildkite_api_access_token: ${{ secrets.BUILDKITE_API_TOKEN }}
pipeline: "${{ vars.BUILDKITE_ORG_SLUG }}/${{ vars.BUILDKITE_PIPELINE_SLUG }}"
branch: "${{ needs.parse-command.outputs.pr_branch }}"
commit: "${{ needs.parse-command.outputs.pr_sha }}"
message: "/test ${{ needs.parse-command.outputs.test_type }} on PR #${{ github.event.issue.number }}"
build_env_vars: '{"FULL_SUITE": "${{ needs.parse-command.outputs.full_suite }}", "TEST_TYPE": "${{ needs.parse-command.outputs.test_type }}", "PR_NUMBER": "${{ github.event.issue.number }}"}'
send_pull_request: "true"
pull_request_id: "${{ github.event.issue.number }}"
pull_request_base_branch: "main"
+13 -32
View File
@@ -39,10 +39,10 @@ jobs:
BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_API_TOKEN }}
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
run: |
# Find running builds for this branch with TEST_SCOPE=full and cancel them
# Find running builds for this branch with FULL_SUITE=true and cancel them
builds=$(curl -sS -H "Authorization: Bearer $BUILDKITE_API_TOKEN" \
"https://api.buildkite.com/v2/organizations/${{ vars.BUILDKITE_ORG_SLUG }}/pipelines/${{ vars.BUILDKITE_PIPELINE_SLUG }}/builds?branch=${PR_BRANCH}&state=running,scheduled" \
| jq -r '.[] | select(.env.TEST_SCOPE == "full") | .number')
| jq -r '.[] | select(.env.FULL_SUITE == "true") | .number')
for build_num in $builds; do
echo "Cancelling Buildkite build #$build_num"
curl -sS -X PUT -H "Authorization: Bearer $BUILDKITE_API_TOKEN" \
@@ -51,33 +51,14 @@ jobs:
- name: Trigger Buildkite Full Suite
if: steps.check.outputs.has_ready == 'true'
env:
BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_API_TOKEN }}
PR_SHA: ${{ github.event.pull_request.head.sha }}
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BK_ORG: ${{ vars.BUILDKITE_ORG_SLUG }}
BK_PIPELINE: ${{ vars.BUILDKITE_PIPELINE_SLUG }}
run: |
curl -sS --fail-with-body -X POST \
"https://api.buildkite.com/v2/organizations/${BK_ORG}/pipelines/${BK_PIPELINE}/builds" \
-H "Authorization: Bearer $BUILDKITE_API_TOKEN" \
-H "Content-Type: application/json" \
--data-raw "$(jq -n \
--arg commit "$PR_SHA" \
--arg branch "$PR_BRANCH" \
--arg message "Full Suite for PR #${PR_NUMBER}" \
--argjson pr_id "$PR_NUMBER" \
'{
commit: $commit,
branch: $branch,
message: $message,
ignore_pipeline_branch_filters: true,
pull_request_id: $pr_id,
pull_request_base_branch: "main",
env: {
TEST_SCOPE: "full",
FULL_SUITE: "true",
PR_NUMBER: ($pr_id | tostring)
}
}')"
uses: buildkite/trigger-pipeline-action@v2.4.1
with:
buildkite_api_access_token: ${{ secrets.BUILDKITE_API_TOKEN }}
pipeline: "${{ vars.BUILDKITE_ORG_SLUG }}/${{ vars.BUILDKITE_PIPELINE_SLUG }}"
branch: "${{ github.event.pull_request.head.ref }}"
commit: "${{ github.event.pull_request.head.sha }}"
message: "Full Suite for PR #${{ github.event.pull_request.number }}"
build_env_vars: '{"FULL_SUITE": "true", "PR_NUMBER": "${{ github.event.pull_request.number }}"}'
send_pull_request: "true"
pull_request_id: "${{ github.event.pull_request.number }}"
pull_request_base_branch: "main"
+2 -2
View File
@@ -277,8 +277,8 @@ Triggers a specific Buildkite test or suite on the current PR branch.
| `/test vmoba` | VMoBA inference tests | `inference_vmoba` |
| `/test performance` | Performance benchmarks | `performance` |
| `/test api` | API server integration tests | `api_server` |
| `/test full` | Entire Full Suite | all (with `TEST_SCOPE=full`) |
| `/test fastcheck` | Entire Fastcheck suite | fastcheck (with `TEST_SCOPE=fastcheck`) |
| `/test full` | Entire Full Suite | all (with `FULL_SUITE=true`) |
| `/test fastcheck` | Entire Fastcheck suite | fastcheck (with `FULL_SUITE=false`) |
---
+91 -116
View File
@@ -9,24 +9,22 @@ image_version = os.getenv("IMAGE_VERSION")
image_tag = f"ghcr.io/hao-ai-lab/fastvideo/fastvideo-dev:{image_version}"
print(f"Using image: {image_tag}")
image = (modal.Image.from_registry(
image_tag, add_python="3.12"
).run_commands("rm -rf /FastVideo").apt_install(
"cmake", "pkg-config", "build-essential", "curl", "libssl-dev", "ffmpeg"
).run_commands(
"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable"
).run_commands("echo 'source ~/.cargo/env' >> ~/.bashrc").env({
"PATH":
"/root/.cargo/bin:$PATH",
"BUILDKITE_REPO":
os.environ.get("BUILDKITE_REPO", ""),
"BUILDKITE_COMMIT":
os.environ.get("BUILDKITE_COMMIT", ""),
"BUILDKITE_PULL_REQUEST":
os.environ.get("BUILDKITE_PULL_REQUEST", ""),
"IMAGE_VERSION":
os.environ.get("IMAGE_VERSION", ""),
}))
image = (
modal.Image.from_registry(image_tag, add_python="3.12")
.run_commands("rm -rf /FastVideo")
.apt_install("cmake", "pkg-config", "build-essential", "curl", "libssl-dev", "ffmpeg")
.run_commands("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable")
.run_commands("echo 'source ~/.cargo/env' >> ~/.bashrc")
.env(
{
"PATH": "/root/.cargo/bin:$PATH",
"BUILDKITE_REPO": os.environ.get("BUILDKITE_REPO", ""),
"BUILDKITE_COMMIT": os.environ.get("BUILDKITE_COMMIT", ""),
"BUILDKITE_PULL_REQUEST": os.environ.get("BUILDKITE_PULL_REQUEST", ""),
"IMAGE_VERSION": os.environ.get("IMAGE_VERSION", ""),
}
)
)
def run_test(pytest_command: str):
@@ -41,8 +39,7 @@ def run_test(pytest_command: str):
print(f"Cloning repository: {git_repo}")
print(f"Target commit: {git_commit}")
if pr_number:
print(f"PR number: {pr_number}")
print(f"PR number: {pr_number}")
# For PRs (including forks), use GitHub's PR refs to get the correct commit
if pr_number and pr_number != "false":
@@ -66,95 +63,84 @@ def run_test(pytest_command: str):
{pytest_command}
"""
result = subprocess.run(["/bin/bash", "-c", command],
stdout=sys.stdout,
stderr=sys.stderr,
check=False)
result = subprocess.run(["/bin/bash", "-c", command], stdout=sys.stdout, stderr=sys.stderr, check=False)
sys.exit(result.returncode)
@app.function(gpu="H100:1",
image=image,
timeout=1200,
secrets=[
modal.Secret.from_dict(
{"HF_API_KEY": os.environ.get("HF_API_KEY", "")})
],
volumes={"/root/data": model_vol})
@app.function(
gpu="H100:1",
image=image,
timeout=1200,
secrets=[modal.Secret.from_dict({"HF_API_KEY": os.environ.get("HF_API_KEY", "")})],
volumes={"/root/data": model_vol},
)
def run_encoder_tests():
run_test(
"export HF_HOME='/root/data/.cache' && hf auth login --token $HF_API_KEY && pytest ./fastvideo/tests/encoders -vs"
)
@app.function(gpu="L40S:1",
image=image,
timeout=1200,
secrets=[
modal.Secret.from_dict(
{"HF_API_KEY": os.environ.get("HF_API_KEY", "")})
],
volumes={"/root/data": model_vol})
@app.function(
gpu="L40S:1",
image=image,
timeout=1200,
secrets=[modal.Secret.from_dict({"HF_API_KEY": os.environ.get("HF_API_KEY", "")})],
volumes={"/root/data": model_vol},
)
def run_vae_tests():
run_test(
"export HF_HOME='/root/data/.cache' && hf auth login --token $HF_API_KEY && pytest ./fastvideo/tests/vaes -vs"
)
@app.function(gpu="L40S:1",
image=image,
timeout=900,
secrets=[
modal.Secret.from_dict(
{"HF_API_KEY": os.environ.get("HF_API_KEY", "")})
],
volumes={"/root/data": model_vol})
@app.function(
gpu="L40S:1",
image=image,
timeout=900,
secrets=[modal.Secret.from_dict({"HF_API_KEY": os.environ.get("HF_API_KEY", "")})],
volumes={"/root/data": model_vol},
)
def run_transformer_tests():
run_test(
"export HF_HOME='/root/data/.cache' && hf auth login --token $HF_API_KEY && pytest ./fastvideo/tests/transformers -vs"
)
@app.function(gpu="L40S:4",
image=image,
timeout=900,
secrets=[
modal.Secret.from_dict(
{"WANDB_API_KEY": os.environ.get("WANDB_API_KEY", "")})
],
volumes={"/root/data": model_vol})
@app.function(
gpu="L40S:4",
image=image,
timeout=900,
secrets=[modal.Secret.from_dict({"WANDB_API_KEY": os.environ.get("WANDB_API_KEY", "")})],
volumes={"/root/data": model_vol},
)
def run_training_tests():
run_test(
"export HF_HOME='/root/data/.cache' && wandb login $WANDB_API_KEY && pytest ./fastvideo/tests/training/Vanilla -srP"
)
@app.function(gpu="L40S:2",
image=image,
timeout=900,
secrets=[
modal.Secret.from_dict(
{"WANDB_API_KEY": os.environ.get("WANDB_API_KEY", "")})
],
volumes={"/root/data": model_vol})
@app.function(
gpu="L40S:2",
image=image,
timeout=900,
secrets=[modal.Secret.from_dict({"WANDB_API_KEY": os.environ.get("WANDB_API_KEY", "")})],
volumes={"/root/data": model_vol},
)
def run_training_lora_tests():
run_test(
"export HF_HOME='/root/data/.cache' && wandb login $WANDB_API_KEY && pytest ./fastvideo/tests/training/lora/test_lora_training.py -srP"
)
@app.function(gpu="H100:2",
image=image,
timeout=900,
secrets=[
modal.Secret.from_dict(
{"WANDB_API_KEY": os.environ.get("WANDB_API_KEY", "")})
])
@app.function(
gpu="H100:2",
image=image,
timeout=900,
secrets=[modal.Secret.from_dict({"WANDB_API_KEY": os.environ.get("WANDB_API_KEY", "")})],
)
def run_training_tests_VSA():
run_test(
"wandb login $WANDB_API_KEY && pytest ./fastvideo/tests/training/VSA -srP"
)
run_test("wandb login $WANDB_API_KEY && pytest ./fastvideo/tests/training/VSA -srP")
@app.function(gpu="H100:1", image=image, timeout=900)
@@ -174,33 +160,27 @@ def run_kernel_tests():
@app.function(gpu="L40S:1", image=image, timeout=900)
def run_inference_tests_vmoba():
run_test('python fastvideo/tests/inference/vmoba/test_vmoba_inference.py')
run_test("python fastvideo/tests/inference/vmoba/test_vmoba_inference.py")
@app.function(gpu="L40S:1", image=image, timeout=1200)
def run_inference_lora_tests():
run_test(
"pytest ./fastvideo/tests/inference/lora/test_lora_inference_similarity.py -vs"
)
run_test("pytest ./fastvideo/tests/inference/lora/test_lora_inference_similarity.py -vs")
@app.function(gpu="L40S:2", image=image, timeout=900)
def run_distill_dmd_tests():
run_test(
"pytest ./fastvideo/tests/training/distill/test_distill_dmd.py -vs")
run_test("pytest ./fastvideo/tests/training/distill/test_distill_dmd.py -vs")
@app.function(gpu="L40S:2",
image=image,
timeout=900,
secrets=[
modal.Secret.from_dict(
{"WANDB_API_KEY": os.environ.get("WANDB_API_KEY", "")})
])
@app.function(
gpu="L40S:2",
image=image,
timeout=900,
secrets=[modal.Secret.from_dict({"WANDB_API_KEY": os.environ.get("WANDB_API_KEY", "")})],
)
def run_self_forcing_tests():
run_test(
"wandb login $WANDB_API_KEY && pytest ./fastvideo/tests/training/self-forcing/test_self_forcing.py -vs"
)
run_test("wandb login $WANDB_API_KEY && pytest ./fastvideo/tests/training/self-forcing/test_self_forcing.py -vs")
@app.function(gpu="L40S:1", image=image, timeout=900)
@@ -210,41 +190,36 @@ def run_unit_test():
)
@app.function(gpu="L40S:1",
image=image,
timeout=3600,
secrets=[
modal.Secret.from_dict(
{"HF_API_KEY": os.environ.get("HF_API_KEY", "")})
])
@app.function(
gpu="L40S:1",
image=image,
timeout=3600,
secrets=[modal.Secret.from_dict({"HF_API_KEY": os.environ.get("HF_API_KEY", "")})],
)
def run_lora_extraction_tests():
run_test(
"hf auth login --token $HF_API_KEY && pytest ./fastvideo/tests/lora_extraction/test_lora_extraction.py"
)
run_test("hf auth login --token $HF_API_KEY && pytest ./fastvideo/tests/lora_extraction/test_lora_extraction.py")
@app.function(gpu="L40S:2",
image=image,
timeout=1800,
secrets=[
modal.Secret.from_dict(
{"HF_API_KEY": os.environ.get("HF_API_KEY", "")})
],
volumes={"/root/data": model_vol})
@app.function(
gpu="L40S:2",
image=image,
timeout=1800,
secrets=[modal.Secret.from_dict({"HF_API_KEY": os.environ.get("HF_API_KEY", "")})],
volumes={"/root/data": model_vol},
)
def run_performance_tests():
run_test(
"export HF_HOME='/root/data/.cache' && hf auth login --token $HF_API_KEY && pytest ./fastvideo/tests/performance -vs"
)
@app.function(gpu="L40S:1",
image=image,
timeout=1800,
secrets=[
modal.Secret.from_dict(
{"HF_API_KEY": os.environ.get("HF_API_KEY", "")})
],
volumes={"/root/data": model_vol})
@app.function(
gpu="L40S:1",
image=image,
timeout=1800,
secrets=[modal.Secret.from_dict({"HF_API_KEY": os.environ.get("HF_API_KEY", "")})],
volumes={"/root/data": model_vol},
)
def run_api_server_tests():
run_test(
"export HF_HOME='/root/data/.cache' && hf auth login --token $HF_API_KEY && pytest ./fastvideo/tests/entrypoints/test_openai_api_integration.py -vs"
@@ -79,13 +79,8 @@ def _assert_similarity(
attention_backend_name: str,
) -> None:
if not os.path.exists(reference_folder):
logger.error("Reference folder missing: %s", reference_folder)
error_msg = (
f"Reference video folder does not exist: {reference_folder}\n"
f"To download reference videos, run:\n"
f" python fastvideo/tests/ssim/reference_videos_cli.py download"
)
raise FileNotFoundError(error_msg)
logger.error("Reference folder missing")
raise FileNotFoundError(f"Reference video folder does not exist: {reference_folder}")
try:
reference_video_path = _find_reference_video(reference_folder, prompt)
+88 -57
View File
@@ -51,7 +51,11 @@ def _iter_video_files(root: Path) -> Iterable[Path]:
def _discover_reference_dirs(base_dir: Path) -> list[Path]:
if not base_dir.exists():
return []
dirs = [p for p in base_dir.iterdir() if p.is_dir() and p.name.endswith("_reference_videos")]
dirs = [
p
for p in base_dir.iterdir()
if p.is_dir() and p.name.endswith("_reference_videos")
]
return sorted(dirs)
@@ -102,7 +106,10 @@ def _resolve_device_reference_folder(
) -> str:
if explicit_device_folder and explicit_device_folder.strip():
return explicit_device_folder.strip()
if reference_dir is not None and reference_dir.name.endswith("_reference_videos"):
if (
reference_dir is not None
and reference_dir.name.endswith("_reference_videos")
):
return reference_dir.name
return DEFAULT_DEVICE_REFERENCE_FOLDER
@@ -134,11 +141,6 @@ def _discover_reference_dirs_for_tier(
def _has_local_reference_videos(base_dir: Path, quality_tier: str) -> bool:
# Check for completion marker file first
marker_path = base_dir / REFERENCE_VIDEOS_DIRNAME / quality_tier / f".download_complete_{quality_tier}"
if not marker_path.exists():
return False
# Also verify at least one .mp4 exists
for ref_dir in _discover_reference_dirs_for_tier(base_dir, quality_tier):
for _ in _iter_video_files(ref_dir):
return True
@@ -159,7 +161,8 @@ def _load_hf_sdk():
from huggingface_hub import HfApi, snapshot_download
except ImportError as exc:
raise RuntimeError(
"huggingface_hub is required for download/upload.\nInstall with: pip install huggingface_hub"
"huggingface_hub is required for download/upload.\n"
"Install with: pip install huggingface_hub"
) from exc
return HfApi, snapshot_download
@@ -207,7 +210,9 @@ def download_reference_videos(
for quality_tier in tiers:
tier_prefix = f"{REFERENCE_VIDEOS_DIRNAME}/{quality_tier}"
if device_folders:
allow_patterns.extend(f"{tier_prefix}/{folder}/**" for folder in device_folders)
allow_patterns.extend(
f"{tier_prefix}/{folder}/**" for folder in device_folders
)
else:
allow_patterns.append(f"{tier_prefix}/*_reference_videos/**")
@@ -237,12 +242,9 @@ def download_reference_videos(
)
if copied_roots == 0:
raise RuntimeError(
"HF download completed but no reference_videos content was found in downloaded artifacts."
"HF download completed but no reference_videos content "
"was found in downloaded artifacts."
)
for quality_tier in tiers:
marker_path = local_dir / REFERENCE_VIDEOS_DIRNAME / quality_tier / f".download_complete_{quality_tier}"
marker_path.parent.mkdir(parents=True, exist_ok=True)
marker_path.touch()
def upload_reference_videos(
@@ -264,8 +266,12 @@ def upload_reference_videos(
for quality_tier, reference_dir in reference_dirs_by_tier:
if not reference_dir.exists():
raise FileNotFoundError(f"Reference directory not found: {reference_dir}")
path_in_repo = f"{REFERENCE_VIDEOS_DIRNAME}/{quality_tier}/{reference_dir.name}"
raise FileNotFoundError(
f"Reference directory not found: {reference_dir}"
)
path_in_repo = (
f"{REFERENCE_VIDEOS_DIRNAME}/{quality_tier}/{reference_dir.name}"
)
print(f"Uploading {reference_dir.name} ({quality_tier}) to {repo_id} ...")
api.upload_folder(
repo_id=repo_id,
@@ -288,14 +294,20 @@ def _resolve_upload_reference_dirs(
if explicit_reference_dirs:
if len(tiers) != 1:
raise RuntimeError("--reference-dir requires a single --quality-tier (default or full_quality).")
raise RuntimeError(
"--reference-dir requires a single --quality-tier "
"(default or full_quality)."
)
resolved_tier = tiers[0]
return [(resolved_tier, Path(p).resolve()) for p in explicit_reference_dirs]
resolved: list[tuple[str, Path]] = []
for tier in tiers:
for reference_dir in _discover_reference_dirs_for_tier(base_dir, tier):
if selected_device_folders and reference_dir.name not in selected_device_folders:
if (
selected_device_folders
and reference_dir.name not in selected_device_folders
):
continue
resolved.append((tier, reference_dir.resolve()))
return resolved
@@ -315,38 +327,32 @@ def ensure_reference_videos_available(
lock_path = target_dir / ".reference_videos_download.lock"
with _exclusive_download_lock(lock_path):
if _has_local_reference_videos(target_dir, quality_tier):
print(f"Reference videos ({quality_tier}) already available at {target_dir}")
return False
resolved_repo_id = repo_id or _default_repo_id()
resolved_repo_type = repo_type or _default_repo_type()
if not resolved_repo_id:
raise RuntimeError(
f"No local reference videos found and no HF repo configured.\nSet {HF_REPO_ENV_KEY} or pass --repo-id."
"No local reference videos found and no HF repo configured.\n"
f"Set {HF_REPO_ENV_KEY} or pass --repo-id."
)
print(f"Repo ID: {resolved_repo_id}")
print(f"Quality tier: {quality_tier}")
print(f"No local {quality_tier} reference videos found under {target_dir}. Starting download...")
try:
download_reference_videos(
repo_id=resolved_repo_id,
repo_type=resolved_repo_type,
local_dir=target_dir,
quality_tiers=[quality_tier],
)
print(f"Download completed for {quality_tier} reference videos.")
except Exception as exc:
print(f"ERROR: Failed to download {quality_tier} reference videos from {resolved_repo_id}.")
print(
f"Suggested command to retry: "
f"python fastvideo/tests/ssim/reference_videos_cli.py download "
f"--quality-tier {quality_tier}"
)
raise
print(
f"No local {quality_tier} reference videos found under {target_dir}. "
f"Downloading from HF repo {resolved_repo_id} ..."
)
download_reference_videos(
repo_id=resolved_repo_id,
repo_type=resolved_repo_type,
local_dir=target_dir,
quality_tiers=[quality_tier],
)
if not _has_local_reference_videos(target_dir, quality_tier):
raise RuntimeError(f"HF download completed but no {quality_tier} *_reference_videos content found.")
raise RuntimeError(
f"HF download completed but no {quality_tier} *_reference_videos "
"content found."
)
return True
@@ -391,12 +397,18 @@ def _build_parser() -> argparse.ArgumentParser:
"--generated-dir",
type=Path,
default=None,
help=("Source generated directory. Default: <ssim_dir>/generated_videos/<quality-tier>/<device-folder>"),
help=(
"Source generated directory. "
"Default: <ssim_dir>/generated_videos/<quality-tier>/<device-folder>"
),
)
copy_parser.add_argument(
"--device-folder",
default=None,
help=("GPU folder name (e.g., H200_reference_videos) used to build default generated/reference paths."),
help=(
"GPU folder name (e.g., H200_reference_videos) used to build "
"default generated/reference paths."
),
)
copy_parser.add_argument(
"--reference-dir",
@@ -488,13 +500,19 @@ def _build_parser() -> argparse.ArgumentParser:
"--device-folder",
action="append",
default=[],
help=("Specific GPU reference folder to upload (repeatable), e.g. H200_reference_videos."),
help=(
"Specific GPU reference folder to upload (repeatable), "
"e.g. H200_reference_videos."
),
)
upload_parser.add_argument(
"--base-dir",
type=Path,
default=_ssim_dir(),
help=("Base SSIM directory that contains reference_videos/<quality-tier>/<GPU>_reference_videos."),
help=(
"Base SSIM directory that contains "
"reference_videos/<quality-tier>/<GPU>_reference_videos."
),
)
upload_parser.add_argument(
"--private",
@@ -520,7 +538,10 @@ def _build_parser() -> argparse.ArgumentParser:
"--local-dir",
type=Path,
default=_ssim_dir(),
help=("Local SSIM directory that should contain reference_videos/<quality-tier>/<GPU>_reference_videos."),
help=(
"Local SSIM directory that should contain "
"reference_videos/<quality-tier>/<GPU>_reference_videos."
),
)
ensure_parser.add_argument(
"--quality-tier",
@@ -556,20 +577,21 @@ def main(argv: Sequence[str] | None = None) -> int:
reference_dir = (
args.reference_dir
if args.reference_dir is not None
else (_ssim_dir() / REFERENCE_VIDEOS_DIRNAME / args.quality_tier / device_folder)
else (
_ssim_dir()
/ REFERENCE_VIDEOS_DIRNAME
/ args.quality_tier
/ device_folder
)
)
copied = copy_generated_to_reference(
generated_dir=generated_dir,
reference_dir=reference_dir,
dry_run=args.dry_run,
)
print(f"Done. {'Would copy' if args.dry_run else 'Copied'} {copied} video files.")
if not args.dry_run and copied > 0:
marker = (
_ssim_dir() / REFERENCE_VIDEOS_DIRNAME / args.quality_tier / f".download_complete_{args.quality_tier}"
)
marker.parent.mkdir(parents=True, exist_ok=True)
marker.touch()
print(
f"Done. {'Would copy' if args.dry_run else 'Copied'} {copied} video files."
)
return 0
if args.command == "download":
@@ -581,14 +603,19 @@ def main(argv: Sequence[str] | None = None) -> int:
quality_tiers=quality_tiers,
device_folders=args.device_folder or None,
)
print("Download complete for quality tier(s): " + ", ".join(quality_tiers) + ".")
print(
"Download complete for quality tier(s): "
+ ", ".join(quality_tiers)
+ "."
)
return 0
if args.command == "upload":
token = _get_hf_token()
if token is None:
raise RuntimeError(
"Hugging Face API key is required for upload. Set HF_API_KEY, HUGGINGFACE_HUB_TOKEN, or HF_TOKEN."
"Hugging Face API key is required for upload. Set "
"HF_API_KEY, HUGGINGFACE_HUB_TOKEN, or HF_TOKEN."
)
reference_dirs_by_tier = _resolve_upload_reference_dirs(
@@ -599,7 +626,8 @@ def main(argv: Sequence[str] | None = None) -> int:
)
if not reference_dirs_by_tier:
raise RuntimeError(
f"No *_reference_videos directories found for the selected quality tier(s) under {args.base_dir}"
"No *_reference_videos directories found for the selected "
f"quality tier(s) under {args.base_dir}"
)
upload_reference_videos(
@@ -620,7 +648,10 @@ def main(argv: Sequence[str] | None = None) -> int:
quality_tier=args.quality_tier,
)
if downloaded:
print(f"{args.quality_tier} reference videos were missing and have been downloaded.")
print(
f"{args.quality_tier} reference videos were missing and "
"have been downloaded."
)
else:
print(f"{args.quality_tier} reference videos already exist locally.")
return 0