Compare commits

..
Author SHA1 Message Date
zhisbug e537ff8aeb [docs] Update README with realtime demo announcement
Made-with: Cursor
2026-03-13 15:48:46 -07:00
132 changed files with 2854 additions and 6627 deletions
+58 -97
View File
@@ -2,46 +2,20 @@ env:
IMAGE_VERSION: "py3.12-latest"
BUILDKITE_CLEAN_CHECKOUT: true
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"
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"
- label: "pre-commit"
command: ".buildkite/scripts/pre_commit.sh"
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
limit: 3
- exit_status: -1
limit: 2
plugins:
- monorepo-diff#v1.4.0:
diff: 'git fetch origin "${BUILDKITE_PULL_REQUEST_BASE_BRANCH:-main}" && git diff --name-only "origin/${BUILDKITE_PULL_REQUEST_BASE_BRANCH:-main}...HEAD"'
watch:
- path:
- wait
- label: "Trigger Tests"
plugins:
- monorepo-diff#v1.4.0:
diff: 'git fetch origin "$BUILDKITE_PULL_REQUEST_BASE_BRANCH" && git diff --name-only origin/"$BUILDKITE_PULL_REQUEST_BASE_BRANCH"...HEAD'
watch:
- path:
- "fastvideo/models/encoders/**"
- "fastvideo/models/loader/**"
- "fastvideo/tests/encoders/**"
@@ -49,7 +23,7 @@ steps:
- "docker/Dockerfile.python3.12"
config:
command: "timeout 20m .buildkite/scripts/pr_test.sh"
label: ":microscope: Encoder Tests"
label: "Encoder Tests"
env:
- TEST_TYPE=encoder
agents:
@@ -62,7 +36,7 @@ steps:
- "docker/Dockerfile.python3.12"
config:
command: "timeout 20m .buildkite/scripts/pr_test.sh"
label: ":microscope: VAE Tests"
label: "VAE Tests"
env:
- TEST_TYPE=vae
agents:
@@ -77,62 +51,18 @@ steps:
- "docker/Dockerfile.python3.12"
config:
command: "timeout 15m .buildkite/scripts/pr_test.sh"
label: ":microscope: Transformer Tests"
label: "Transformer Tests"
env:
- TEST_TYPE=transformer
agents:
queue: "default"
- path:
- "fastvideo-kernel/**"
- "pyproject.toml"
- "docker/Dockerfile.python3.12"
config:
command: "timeout 15m .buildkite/scripts/pr_test.sh"
label: ":microscope: Kernel Tests"
env:
- TEST_TYPE=kernel_tests
agents:
queue: "default"
- path:
- "fastvideo/**"
- ".buildkite/**"
- ".github/**"
- "pyproject.toml"
- "docker/Dockerfile.python3.12"
config:
command: "timeout 15m .buildkite/scripts/pr_test.sh"
label: ":microscope: Unit Tests"
env:
- TEST_TYPE=unit_test
agents:
queue: "default"
# ============================================================
# Full Suite: Runs when TEST_SCOPE=full
# 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"
retry:
automatic:
- exit_status: 128
limit: 3
- exit_status: -1
limit: 2
plugins:
- monorepo-diff#v1.4.0:
diff: 'git fetch origin "${BUILDKITE_PULL_REQUEST_BASE_BRANCH:-main}" && git diff --name-only "origin/${BUILDKITE_PULL_REQUEST_BASE_BRANCH:-main}...HEAD"'
watch:
- path:
- path:
- "fastvideo/**/*.py"
- "pyproject.toml"
- "docker/Dockerfile.python3.12"
config:
command: "timeout 90m .buildkite/scripts/pr_test.sh"
label: ":bar_chart: SSIM Tests"
label: "SSIM Tests"
env:
- TEST_TYPE=ssim
agents:
@@ -147,7 +77,7 @@ steps:
- "docker/Dockerfile.python3.12"
config:
command: "timeout 20m .buildkite/scripts/pr_test.sh"
label: ":test_tube: LoRA Inference Tests"
label: "LoRA Inference Tests"
env:
- TEST_TYPE=inference_lora
agents:
@@ -158,7 +88,7 @@ steps:
- "docker/Dockerfile.python3.12"
config:
command: "timeout 15m .buildkite/scripts/pr_test.sh"
label: ":test_tube: Training Tests"
label: "Training Tests"
env:
- TEST_TYPE=training
agents:
@@ -169,7 +99,7 @@ steps:
- "docker/Dockerfile.python3.12"
config:
command: "timeout 15m .buildkite/scripts/pr_test.sh"
label: ":test_tube: Distillation DMD Tests"
label: "Distillation DMDTests"
env:
- TEST_TYPE=distillation_dmd
agents:
@@ -181,7 +111,7 @@ steps:
- "docker/Dockerfile.python3.12"
config:
command: "timeout 15m .buildkite/scripts/pr_test.sh"
label: ":test_tube: Self-Forcing Tests"
label: "Self-Forcing Tests"
env:
- TEST_TYPE=self_forcing
agents:
@@ -192,7 +122,7 @@ steps:
- "docker/Dockerfile.python3.12"
config:
command: "timeout 15m .buildkite/scripts/pr_test.sh"
label: ":test_tube: LoRA Training Tests"
label: "LoRA Training Tests"
env:
- TEST_TYPE=training_lora
agents:
@@ -204,11 +134,20 @@ steps:
- "docker/Dockerfile.python3.12"
config:
command: "timeout 15m .buildkite/scripts/pr_test.sh"
label: ":test_tube: Training Tests VSA"
label: "Training Tests VSA"
env:
- TEST_TYPE=training_vsa
agents:
queue: "default"
- path:
- "fastvideo-kernel/**"
- "pyproject.toml"
- "docker/Dockerfile.python3.12"
config:
command: "timeout 15m .buildkite/scripts/pr_test.sh"
label: "Kernel Tests"
env:
- TEST_TYPE=kernel_tests
- path:
- "fastvideo-kernel/**"
- "fastvideo/attention/backends/vmoba.py"
@@ -216,11 +155,22 @@ steps:
- "docker/Dockerfile.python3.12"
config:
command: "timeout 15m .buildkite/scripts/pr_test.sh"
label: ":test_tube: Inference Tests VMoBA"
env:
label: "Inference Tests VMoBA"
env:
- TEST_TYPE=inference_vmoba
agents:
queue: "default"
- path:
- "fastvideo/**"
- "pyproject.toml"
- "docker/Dockerfile.python3.12"
config:
command: "timeout 15m .buildkite/scripts/pr_test.sh"
label: "Unit Tests"
env:
- TEST_TYPE=unit_test
agents:
queue: "default"
- path:
- "fastvideo/models/dits/**"
- "fastvideo/pipelines/**"
@@ -234,7 +184,7 @@ steps:
- "docker/Dockerfile.python3.12"
config:
command: "timeout 30m .buildkite/scripts/pr_test.sh"
label: ":test_tube: Performance Tests"
label: "Performance Tests"
env:
- TEST_TYPE=performance
agents:
@@ -247,8 +197,19 @@ steps:
- "docker/Dockerfile.python3.12"
config:
command: "timeout 30m .buildkite/scripts/pr_test.sh"
label: ":test_tube: API Server Tests"
label: "API Server Tests"
env:
- TEST_TYPE=api_server
agents:
queue: "default"
# - path:
# - "scripts/lora_extraction/**"
# - "pyproject.toml"
# - "docker/Dockerfile.python3.12"
# config:
# command: "timeout 90m .buildkite/scripts/pr_test.sh"
# label: "LoRA Extraction Tests"
# env:
# - TEST_TYPE=lora_extraction
# agents:
# queue: "default"
+1 -5
View File
@@ -59,11 +59,7 @@ if [ -z "${TEST_TYPE:-}" ]; then
fi
log "Test type: $TEST_TYPE"
EFFECTIVE_PR=${BUILDKITE_PULL_REQUEST:-false}
if [ "$EFFECTIVE_PR" = "false" ] && [ -n "${PR_NUMBER:-}" ]; then
EFFECTIVE_PR=$PR_NUMBER
fi
MODAL_ENV="BUILDKITE_REPO=$BUILDKITE_REPO BUILDKITE_COMMIT=$BUILDKITE_COMMIT BUILDKITE_PULL_REQUEST=$EFFECTIVE_PR IMAGE_VERSION=$IMAGE_VERSION"
MODAL_ENV="BUILDKITE_REPO=$BUILDKITE_REPO BUILDKITE_COMMIT=$BUILDKITE_COMMIT BUILDKITE_PULL_REQUEST=$BUILDKITE_PULL_REQUEST IMAGE_VERSION=$IMAGE_VERSION"
case "$TEST_TYPE" in
"encoder")
-19
View File
@@ -1,22 +1,3 @@
<!--
PR TITLE: Must start with a type tag, e.g.:
[feat] Add new model [bugfix] Fix VAE tiling [refactor] Restructure pipeline
[perf] Optimize kernel [ci] Update tests [docs] Add guide
[misc] Cleanup configs [new-model] Port Flux2
MERGE WORKFLOW:
1. Ensure pre-commit passes and you have at least 1 approval
2. Comment /merge (or add the "ready" label) to enter the Merge Queue
3. Full Test Suite runs automatically on a staging branch → auto-merge on success
ON-DEMAND TESTING (write access required):
/test full — Full Test Suite /test ssim — SSIM regression
/test training — Training pipeline /test encoder — Encoder tests
/test transformer — Transformer tests /test vae — VAE tests
/test kernel — CUDA kernel tests /test unit — Unit tests
See docs/contributing/pull_requests.md for all 17 test commands
-->
## Purpose
<!-- What does this PR do? Link the related issue if applicable. -->
-321
View File
@@ -1,321 +0,0 @@
merge_protections:
- name: PR merge requirements
if:
- base = main
success_conditions:
- "title~=(?i)^\\[(feat|feature|bugfix|fix|refactor|perf|ci|doc|docs|misc|chore|kernel|new.?model)\\]"
- check-success~=pre-commit
- check-success=fastcheck-passed
pull_request_rules:
# ============================================================
# Type labels (from PR title prefix)
# ============================================================
- name: "label type: feat"
conditions:
- "title~=(?i)^\\[(feat|feature)\\]"
- -closed
actions:
label:
add: ["type: feat"]
- name: "label type: bugfix"
conditions:
- "title~=(?i)^\\[(bug)?fix\\]"
- -closed
actions:
label:
add: ["type: bugfix"]
- name: "label type: refactor"
conditions:
- "title~=(?i)^\\[refactor\\]"
- -closed
actions:
label:
add: ["type: refactor"]
- name: "label type: perf"
conditions:
- "title~=(?i)^\\[perf\\]"
- -closed
actions:
label:
add: ["type: perf"]
- name: "label type: ci"
conditions:
- "title~=(?i)^\\[ci\\]"
- -closed
actions:
label:
add: ["type: ci"]
- name: "label type: docs"
conditions:
- "title~=(?i)^\\[(doc|docs)\\]"
- -closed
actions:
label:
add: ["type: docs"]
- name: "label type: misc"
conditions:
- "title~=(?i)^\\[(misc|chore)\\]"
- -closed
actions:
label:
add: ["type: misc"]
- name: "label type: new-model"
conditions:
- "title~=(?i)^\\[new.?model\\]"
- -closed
actions:
label:
add: ["type: new-model"]
# ============================================================
# Scope labels (from changed files)
# ============================================================
- name: "label scope: training"
conditions:
- or:
- files~=^fastvideo/train/
- files~=^fastvideo/training/
- files~=^fastvideo/distillation/
- files~=^examples/train/
- files~=^examples/training/
- files~=^examples/distill/
- -closed
actions:
label:
add: ["scope: training"]
- 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/
- -closed
actions:
label:
add: ["scope: inference"]
- name: "label scope: attention"
conditions:
- files~=^fastvideo/attention/
- -closed
actions:
label:
add: ["scope: attention"]
- name: "label scope: kernel"
conditions:
- or:
- files~=^fastvideo-kernel/
- files~=^csrc/
- -closed
actions:
label:
add: ["scope: kernel"]
- name: "label scope: data"
conditions:
- or:
- files~=^fastvideo/dataset/
- files~=^fastvideo/pipelines/preprocess/
- files~=^examples/preprocessing/
- -closed
actions:
label:
add: ["scope: data"]
- name: "label scope: infra"
conditions:
- or:
- files~=^\.github/
- files~=^\.buildkite/
- files~=^fastvideo/tests/
- files~=^docker/
- -closed
actions:
label:
add: ["scope: infra"]
- name: "label scope: distributed"
conditions:
- files~=^fastvideo/distributed/
- -closed
actions:
label:
add: ["scope: distributed"]
- name: "label scope: docs"
conditions:
- files~=^docs/
- -closed
actions:
label:
add: ["scope: docs"]
- name: "label scope: ui"
conditions:
- files~=^ui/
- -closed
actions:
label:
add: ["scope: ui"]
- name: "label scope: model"
conditions:
- or:
- files~=^fastvideo/models/
- files~=^fastvideo/layers/
- files~=^fastvideo/configs/models/
- -closed
actions:
label:
add: ["scope: model"]
# ============================================================
# Pre-commit failure help comment
# ============================================================
- name: comment on pre-commit failure
conditions:
- check-failure~=pre-commit
- -closed
actions:
comment:
message: |
## Pre-commit checks failed
Hi @{{author}}, the pre-commit checks have failed. To fix them locally:
```bash
# Install pre-commit if you haven't already
uv pip install pre-commit
pre-commit install
# Run all checks and auto-fix what's possible
pre-commit run --all-files
```
Common fixes:
- **yapf**: `yapf -i <file>` (formatting)
- **ruff**: `ruff check --fix <file>` (linting)
- **codespell**: `codespell --write-changes <file>` (spelling)
After fixing, commit and push the changes. The checks will re-run automatically.
For future commits, `pre-commit` will run automatically on changed files before each commit.
# ============================================================
# Merge conflict detection
# ============================================================
- name: label conflicting PRs
conditions:
- conflict
- -closed
- label!=stale
actions:
label:
add: [needs-rebase]
comment:
message: |
This PR has merge conflicts with the base branch. Please rebase:
```bash
git fetch origin main
git rebase origin/main
# Resolve any conflicts, then:
git push --force-with-lease
```
- name: remove conflict label when resolved
conditions:
- -conflict
- -closed
- label=needs-rebase
actions:
label:
remove: [needs-rebase]
# ============================================================
# Auto-merge and auto-rebase
# ============================================================
- name: auto-merge when ready and all checks pass
conditions:
- label=ready
- "title~=(?i)^\\[(feat|feature|bugfix|fix|refactor|perf|ci|doc|docs|misc|chore|kernel|new.?model)\\]"
- "#approved-reviews-by>=1"
- check-success~=pre-commit
- check-success=fastcheck-passed
- check-success=full-suite-passed
- -conflict
- -closed
- -draft
actions:
merge:
method: squash
- name: auto-rebase when ready and Full Suite passed
conditions:
- label=ready
- "#approved-reviews-by>=1"
- check-success=full-suite-passed
- -conflict
- -closed
- -draft
actions:
rebase: {}
- name: remove ready label on Full Suite failure
conditions:
- label=ready
- check-failure=full-suite-passed
actions:
label:
remove: [ready]
# ============================================================
# PR title format help
# ============================================================
- name: comment on invalid PR title format
conditions:
- -closed
- -draft
- "-title~=(?i)^\\[(feat|feature|bugfix|fix|refactor|perf|ci|doc|docs|misc|chore|kernel|new.?model)\\]"
actions:
comment:
message: |
## ⚠️ PR title format required
Your PR title must start with a type tag in brackets. Examples:
- `[feat] Add new model support`
- `[bugfix] Fix VAE tiling corruption`
- `[refactor] Restructure training pipeline`
- `[perf] Optimize attention kernel`
- `[ci] Update test infrastructure`
- `[docs] Add inference guide`
- `[misc] Clean up configs`
- `[new-model] Port Flux2 to FastVideo`
Valid tags: `feat`, `feature`, `bugfix`, `fix`, `refactor`, `perf`, `ci`, `doc`, `docs`, `misc`, `chore`, `kernel`, `new-model`
Please update your PR title and the merge protection check will pass automatically.
+249
View File
@@ -0,0 +1,249 @@
import argparse
import json
import os
import subprocess
import sys
import time
import requests
def parse_arguments():
"""Parse command line arguments"""
parser = argparse.ArgumentParser(description='Run tests on RunPod GPU')
parser.add_argument('--gpu-type', type=str, help='GPU type to use')
parser.add_argument('--gpu-count',
type=int,
help='Number of GPUs to use',
default=1)
parser.add_argument('--test-command', type=str, help='Test command to run')
parser.add_argument('--disk-size',
type=int,
default=20,
help='Container disk size in GB (default: 20)')
parser.add_argument('--volume-size',
type=int,
default=20,
help='Persistent volume size in GB (default: 20)')
parser.add_argument(
'--image',
type=str,
required=True,
help='Docker image to use')
return parser.parse_args()
args = parse_arguments()
API_KEY = os.environ['RUNPOD_API_KEY']
RUN_ID = os.environ['GITHUB_RUN_ID']
JOB_ID = os.environ['JOB_ID']
PODS_API = "https://rest.runpod.io/v1/pods"
HEADERS = {
"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}"
}
def create_pod():
"""Create a RunPod instance"""
# Ensure image name is lowercase (Docker requirement)
image_name = args.image.lower()
print(f"Using specified image: {image_name}")
docker_start_cmd = [
"bash",
"-c",
"apt update;DEBIAN_FRONTEND=noninteractive apt-get install openssh-server -y;mkdir -p ~/.ssh;cd $_;chmod 700 ~/.ssh;echo \"$PUBLIC_KEY\" >> authorized_keys;chmod 700 authorized_keys;service ssh start;sleep infinity"
]
print(f"Creating RunPod instance with GPU: {args.gpu_type}...")
payload = {
"name": f"fastvideo-{JOB_ID}-{RUN_ID}",
"containerDiskInGb": args.disk_size,
"volumeInGb": args.volume_size,
"gpuTypeIds": [args.gpu_type],
"gpuCount": args.gpu_count,
"imageName": image_name,
"allowedCudaVersions": ["12.4"],
"dockerStartCmd": docker_start_cmd
}
response = requests.post(PODS_API, headers=HEADERS, json=payload)
response_data = response.json()
print(f"Response: {json.dumps(response_data, indent=2)}")
return response_data["id"]
def wait_for_pod(pod_id):
"""Wait for pod to be in RUNNING state and fully ready with SSH access"""
print("Waiting for RunPod to be ready...")
# First wait for RUNNING status
max_attempts = 10
attempts = 0
while attempts < max_attempts:
response = requests.get(f"{PODS_API}/{pod_id}", headers=HEADERS)
pod_data = response.json()
status = pod_data["desiredStatus"]
if status == "RUNNING":
print("RunPod is running! Now waiting for ports to be assigned...")
break
print(
f"Current status: {status}, waiting... (attempt {attempts+1}/{max_attempts})"
)
time.sleep(2)
attempts += 1
if attempts >= max_attempts:
raise TimeoutError(
"Timed out waiting for RunPod to reach RUNNING state")
# Wait for ports to be assigned
max_attempts = 50
attempts = 0
while attempts < max_attempts:
response = requests.get(f"{PODS_API}/{pod_id}", headers=HEADERS)
pod_data = response.json()
port_mappings = pod_data.get("portMappings")
if (port_mappings is not None and "22" in port_mappings
and pod_data.get("publicIp", "") != ""):
print("RunPod is ready with SSH access!")
print(f"SSH IP: {pod_data['publicIp']}")
print(f"SSH Port: {port_mappings['22']}")
break
print(
f"Waiting for SSH port and public IP to be available... (attempt {attempts+1}/{max_attempts})"
)
time.sleep(20)
attempts += 1
if attempts >= max_attempts:
raise TimeoutError("Timed out waiting for RunPod SSH access")
def execute_command(pod_id):
"""Execute command on the pod via SSH using system SSH client"""
print(f"Running command: {args.test_command}")
response = requests.get(f"{PODS_API}/{pod_id}", headers=HEADERS)
pod_data = response.json()
ssh_ip = pod_data["publicIp"]
ssh_port = pod_data["portMappings"]["22"]
# Copy the repository to the pod using scp
repo_dir = os.path.abspath(os.getcwd())
repo_name = os.path.basename(repo_dir)
print(f"Copying repository from {repo_dir} to RunPod...")
tar_command = [
"tar", "-czf", "/tmp/repo.tar.gz", "-C",
os.path.dirname(repo_dir), repo_name
]
subprocess.run(tar_command, check=True)
# Copy the tarball to the pod
scp_command = [
"scp", "-o", "StrictHostKeyChecking=no", "-o",
"UserKnownHostsFile=/dev/null", "-o", "ServerAliveInterval=60", "-o",
"ServerAliveCountMax=10", "-P",
str(ssh_port), "/tmp/repo.tar.gz", f"root@{ssh_ip}:/tmp/"
]
subprocess.run(scp_command, check=True)
# For custom image, we can use the pre-configured environment
setup_steps = [
"tar -xzf /tmp/repo.tar.gz --no-same-owner -C /workspace/",
f"cd /workspace/{repo_name}",
"source $HOME/.local/bin/env && source /opt/venv/bin/activate",
args.test_command
]
remote_command = " && ".join(setup_steps)
ssh_command = [
"ssh", "-o", "StrictHostKeyChecking=no", "-o",
"UserKnownHostsFile=/dev/null", "-o", "ServerAliveInterval=60", "-o",
"ServerAliveCountMax=10", "-p",
str(ssh_port), f"root@{ssh_ip}", remote_command
]
print(f"Connecting to {ssh_ip}:{ssh_port}...")
try:
process = subprocess.Popen(ssh_command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
bufsize=0)
stdout_lines = []
print("Command output:")
for line in iter(process.stdout.readline, ''):
print(line.strip())
stdout_lines.append(line)
process.wait()
return_code = process.returncode
success = return_code == 0
stdout_str = "".join(stdout_lines)
if success:
print("Command executed successfully")
else:
print(f"Command failed with exit code {return_code}")
result = {
"success": success,
"return_code": return_code,
"stdout": stdout_str,
"stderr": ""
}
return result
except Exception as e:
print(f"Error executing SSH command: {str(e)}")
result = {"success": False, "error": str(e), "stdout": "", "stderr": ""}
return result
def terminate_pod(pod_id):
"""Terminate the pod"""
print("Terminating RunPod...")
requests.delete(f"{PODS_API}/{pod_id}", headers=HEADERS)
print(f"Terminated pod {pod_id}")
def main():
pod_id = None
try:
pod_id = create_pod()
wait_for_pod(pod_id)
result = execute_command(pod_id)
if result.get("error") is not None:
print(f"Error executing command: {result['error']}")
sys.exit(1)
if not result.get("success", False):
print(
"Tests failed - check the output above for details on which tests failed"
)
sys.exit(1)
finally:
if pod_id:
terminate_pod(pod_id)
if __name__ == "__main__":
main()
+90
View File
@@ -0,0 +1,90 @@
import json
import os
import sys
import uuid
import requests
API_KEY = os.environ['RUNPOD_API_KEY']
RUN_ID = os.environ.get('GITHUB_RUN_ID', str(uuid.uuid4()))
PODS_API = "https://rest.runpod.io/v1/pods"
HEADERS = {
"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}"
}
def get_job_ids():
"""Parse job IDs from environment variable"""
job_ids_str = os.environ.get('JOB_IDS')
try:
job_ids = json.loads(job_ids_str)
if not isinstance(job_ids, list):
print("Error: JOB_IDS is not a list.")
sys.exit(1)
return job_ids
except json.JSONDecodeError as e:
print(f"Error parsing JOB_IDS: {e}")
sys.exit(1)
def cleanup_pods():
"""Find and terminate RunPod instances"""
print(f"Run ID: {RUN_ID}")
single_job_id = os.environ.get('JOB_ID')
if single_job_id:
job_ids = [single_job_id]
print(f"Job ID: {single_job_id}")
else:
job_ids = get_job_ids()
print(f"Job IDs: {job_ids}")
# Get all pods associated with RunPod API_KEY
try:
response = requests.get(PODS_API, headers=HEADERS)
response.raise_for_status()
pods = response.json()
except requests.exceptions.RequestException as e:
print(f"Error getting pods: {e}")
sys.exit(1)
# Find and terminate pods created by this workflow run
terminated_pods = []
for pod in pods:
pod_name = pod.get("name", "")
pod_id = pod.get("id")
# Check if this pod was created by one of our jobs
if any(f"{job_id}-{RUN_ID}" in pod_name for job_id in job_ids):
print(f"Found pod: {pod_id} ({pod_name})")
try:
print(f"Terminating pod {pod_id}...")
term_response = requests.delete(f"{PODS_API}/{pod_id}",
headers=HEADERS)
term_response.raise_for_status()
terminated_pods.append(pod_id)
print(f"Successfully terminated pod {pod_id}")
except requests.exceptions.RequestException as e:
print(f"Error terminating pod {pod_id}: {e}")
sys.exit(1)
if terminated_pods:
if single_job_id:
print(f"Terminated pod: {terminated_pods[0]}")
else:
print(f"Terminated {len(terminated_pods)} pods: {terminated_pods}")
else:
if single_job_id:
print(f"No pod found matching pattern: {single_job_id}-{RUN_ID}")
else:
print("No pods found to terminate.")
def main():
cleanup_pods()
if __name__ == "__main__":
main()
@@ -32,7 +32,7 @@ permissions:
jobs:
build-python-3-10:
if: ${{ github.event.inputs.python_3_10 == 'true' }}
uses: ./.github/workflows/_template-build-image.yml
uses: ./.github/workflows/build-image-template.yml
with:
python_version: '3.10'
dockerfile_path: docker/Dockerfile.python3.10
@@ -41,7 +41,7 @@ jobs:
build-python-3-11:
if: ${{ github.event.inputs.python_3_11 == 'true' }}
uses: ./.github/workflows/_template-build-image.yml
uses: ./.github/workflows/build-image-template.yml
with:
python_version: '3.11'
dockerfile_path: docker/Dockerfile.python3.11
@@ -50,7 +50,7 @@ jobs:
build-python-3-12:
if: ${{ github.event.inputs.python_3_12 == 'true' }}
uses: ./.github/workflows/_template-build-image.yml
uses: ./.github/workflows/build-image-template.yml
with:
python_version: '3.12'
dockerfile_path: docker/Dockerfile.python3.12
@@ -59,9 +59,9 @@ jobs:
build-python-3-12-cuda-12-9:
if: ${{ github.event.inputs.python_3_12_cuda_12_9 == 'true' }}
uses: ./.github/workflows/_template-build-image.yml
uses: ./.github/workflows/build-image-template.yml
with:
python_version: '3.12'
dockerfile_path: docker/Dockerfile.python3.12.cuda12.9.1
tag_suffix: py3.12-cuda12.9.1
secrets: inherit
secrets: inherit
-29
View File
@@ -1,29 +0,0 @@
name: pre-commit
on:
pull_request:
branches: [main]
workflow_call:
concurrency:
group: pre-commit-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
jobs:
pre-commit:
if: github.event_name == 'workflow_call' || github.event.pull_request.draft != true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: echo "::add-matcher::.github/workflows/matchers/actionlint.json"
- run: echo "::add-matcher::.github/workflows/matchers/mypy.json"
- run: echo "::add-matcher::.github/workflows/matchers/ruff.json"
- uses: pre-commit/action@v3.0.1
with:
extra_args: --all-files --hook-stage manual
-230
View File
@@ -1,230 +0,0 @@
name: Slash Commands
on:
issue_comment:
types: [created]
permissions:
contents: read
pull-requests: write
statuses: write
jobs:
handle-merge:
if: >-
github.event.issue.pull_request != null
&& startsWith(github.event.comment.body, '/merge')
runs-on: ubuntu-latest
steps:
- name: Check write permission
id: perm
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: context.payload.comment.user.login,
});
const hasWrite = ['admin', 'write'].includes(perm.permission);
if (!hasWrite) {
core.setFailed(`User ${context.payload.comment.user.login} lacks write permission (has: ${perm.permission}).`);
}
core.setOutput('has_write', String(hasWrite));
- name: Add ready label and react
if: steps.perm.outputs.has_write == 'true'
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const prNumber = context.payload.issue.number;
// Remove ready first to allow re-trigger (labeled event fires on add, not if already present)
try { await github.rest.issues.removeLabel({ owner, repo, issue_number: prNumber, name: 'ready' }); } catch {}
await github.rest.issues.addLabels({ owner, repo, issue_number: prNumber, labels: ['ready'] });
await github.rest.reactions.createForIssueComment({
owner, repo,
comment_id: context.payload.comment.id,
content: 'rocket',
});
parse-command:
if: >-
github.event.issue.pull_request != null
&& startsWith(github.event.comment.body, '/test')
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 }}
has_write: ${{ steps.perm.outputs.has_write }}
steps:
- name: Check write permission
id: perm
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner,
repo: context.repo.repo,
username: context.payload.comment.user.login,
});
const hasWrite = ['admin', 'write'].includes(perm.permission);
core.setOutput('has_write', String(hasWrite));
if (!hasWrite) {
core.info(`User ${context.payload.comment.user.login} lacks write permission — ignoring.`);
}
- name: Parse /test command
id: parse
if: steps.perm.outputs.has_write == 'true'
shell: bash
env:
COMMENT: ${{ github.event.comment.body }}
run: |
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"
if [ -z "$TEST_NAME" ] || ! echo "$VALID" | grep -qw "$TEST_NAME"; then
echo "Unknown test: '$TEST_NAME'. Valid: $VALID"
exit 1
fi
declare -A MAP=(
[encoder]=encoder [vae]=vae [transformer]=transformer
[kernel]=kernel_tests [unit]=unit_test
[ssim]=ssim [training]=training
[lora-inference]=inference_lora [lora-training]=training_lora
[distillation]=distillation_dmd [self-forcing]=self_forcing
[vsa]=training_vsa [vmoba]=inference_vmoba
[performance]=performance [api]=api_server
)
if [ "$TEST_NAME" = "full" ]; then
{
echo "test_type=all"
echo "test_scope=full"
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"
else
{
echo "test_type=${MAP[$TEST_NAME]}"
echo "test_scope=direct"
echo "full_suite=false"
} >> "$GITHUB_OUTPUT"
fi
- name: Get PR details
id: pr
if: steps.perm.outputs.has_write == 'true'
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.issue.number,
});
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:
- 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: >-
needs.parse-command.outputs.has_write == 'true'
&& needs.parse-command.outputs.test_type != ''
runs-on: ubuntu-latest
steps:
- name: React to comment
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'rocket',
});
- 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
}
}')"
@@ -1,83 +0,0 @@
name: Trigger Full Suite
on:
pull_request:
types: [labeled, synchronize]
permissions:
contents: read
pull-requests: read
concurrency:
group: full-suite-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
trigger:
if: >-
(github.event.action == 'labeled' && github.event.label.name == 'ready')
|| github.event.action == 'synchronize'
runs-on: ubuntu-latest
steps:
- name: Check ready label
id: check
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
});
const hasReady = pr.labels.some(l => l.name === 'ready');
core.setOutput('has_ready', String(hasReady));
if (!hasReady) core.info('No ready label — skipping Full Suite trigger.');
- name: Cancel previous Buildkite builds
if: steps.check.outputs.has_ready == 'true'
env:
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
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')
for build_num in $builds; do
echo "Cancelling Buildkite build #$build_num"
curl -sS -X PUT -H "Authorization: Bearer $BUILDKITE_API_TOKEN" \
"https://api.buildkite.com/v2/organizations/${{ vars.BUILDKITE_ORG_SLUG }}/pipelines/${{ vars.BUILDKITE_PIPELINE_SLUG }}/builds/${build_num}/cancel"
done
- 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)
}
}')"
@@ -1,65 +0,0 @@
name: Auto-Label Issues
on:
issues:
types: [opened, edited]
permissions:
issues: write
jobs:
label-issues:
if: github.repository == 'hao-ai-lab/FastVideo'
runs-on: ubuntu-latest
steps:
- name: Label by keywords
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const title = context.payload.issue.title.toLowerCase();
const body = (context.payload.issue.body || '').toLowerCase();
const text = title + ' ' + body;
const labels = [];
const rules = [
// scope labels (shared with PR labeling via Mergify)
// Mapping: label → repo directories
// scope: training → fastvideo/train/, fastvideo/training/, fastvideo/distillation/
// scope: inference → fastvideo/pipelines/, fastvideo/entrypoints/, fastvideo/worker/
// scope: attention → fastvideo/attention/
// scope: kernel → fastvideo-kernel/, csrc/
// scope: model → fastvideo/models/, fastvideo/layers/, fastvideo/configs/models/
// scope: data → fastvideo/dataset/, fastvideo/pipelines/preprocess/
// scope: distributed → fastvideo/distributed/
// scope: docs → docs/
{ keywords: ['training', 'finetune', 'fine-tune', 'lora', 'fsdp', 'distill'], label: 'scope: training' },
{ keywords: ['inference', 'generate', 'pipeline', 'slow', 'latency'], label: 'scope: inference' },
{ keywords: ['attention', 'vsa', 'flash', 'sta', 'vmoba', 'sparse attn'], label: 'scope: attention' },
{ keywords: ['kernel', 'csrc', 'cuda kernel', 'thunderkittens'], label: 'scope: kernel' },
{ keywords: ['wan', 'hunyuan', 'mochi', 'ltx', 'cogvideo', 'flux', 'sd3', 'cosmos'], label: 'scope: model' },
{ keywords: ['dataset', 'dataloader', 'preprocessing', 'preprocess'], label: 'scope: data' },
{ keywords: ['distributed', 'sequence parallel', 'fsdp', 'tensor parallel', 'multi-node', 'multi-gpu'], label: 'scope: distributed' },
{ keywords: ['docs', 'documentation', 'tutorial', 'example'], label: 'scope: docs' },
// issue-only labels (cross-module, no single repo directory)
{ keywords: ['install', 'setup', 'pip', 'cuda', 'uv ', 'import error', 'modulenotfound'], label: 'installation' },
{ keywords: ['memory', 'oom', 'out of memory', 'gpu memory', 'vram'], label: 'performance' },
{ keywords: ['windows', 'macos', 'mac os', 'apple', 'mps', 'rocm', 'amd', 'npu'], label: 'platform' },
];
for (const rule of rules) {
if (rule.keywords.some(kw => text.includes(kw))) {
labels.push(rule.label);
}
}
if (labels.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
labels: labels,
});
console.log(`Added labels: ${labels.join(', ')}`);
} else {
console.log('No keyword matches found');
}
-51
View File
@@ -1,51 +0,0 @@
name: Close Stale Issues and PRs
on:
schedule:
# Daily at 1:30 AM UTC
- cron: '30 1 * * *'
jobs:
stale:
if: github.repository == 'hao-ai-lab/FastVideo'
permissions:
issues: write
pull-requests: write
actions: write
runs-on: ubuntu-latest
steps:
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
with:
operations-per-run: 500
exempt-draft-pr: true
exempt-issue-labels: 'keep-open,pinned,security,Bug,RFC'
exempt-pr-labels: 'keep-open,pinned'
labels-to-add-when-unstale: 'unstale'
labels-to-remove-when-stale: 'unstale'
days-before-issue-stale: 90
days-before-issue-close: 30
stale-issue-label: 'stale'
stale-issue-message: >
This issue has been automatically marked as stale because it has not
had any activity within 90 days. It will be automatically closed if
no further activity occurs within 30 days. Leave a comment if you
feel this issue should remain open. Thank you!
close-issue-message: >
This issue has been automatically closed due to inactivity. Please
feel free to reopen if you feel it is still relevant. Thank you!
days-before-pr-stale: 60
days-before-pr-close: 14
stale-pr-label: 'stale'
stale-pr-message: >
This pull request has been automatically marked as stale because it
has not had any activity within 60 days. It will be automatically
closed if no further activity occurs within 14 days. Leave a comment
if you feel this pull request should remain open. Thank you!
close-pr-message: >
This pull request has been automatically closed due to inactivity.
Please feel free to reopen if you intend to continue working on it.
Thank you!
-56
View File
@@ -1,56 +0,0 @@
name: Welcome First-Time Contributors
on:
issues:
types: [opened]
pull_request_target:
types: [opened]
permissions:
issues: write
pull-requests: write
jobs:
welcome:
if: github.repository == 'hao-ai-lab/FastVideo'
runs-on: ubuntu-latest
steps:
- uses: actions/first-interaction@34f15e814fe48ac9312ccf29db4e74fa767cbab7 # v1.3.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: |
Welcome to FastVideo! Thanks for opening your first issue.
To help us investigate, please include:
- **FastVideo version**: `pip show fastvideo`
- **GPU**: `nvidia-smi` output (GPU model, driver, CUDA version)
- **Python version**: `python --version`
- **OS**: e.g., Ubuntu 22.04
If this is a bug, a minimal reproduction script helps us fix it faster.
Useful links:
- [Documentation](https://hao-ai-lab.github.io/FastVideo)
- [Contributing Guide](https://hao-ai-lab.github.io/FastVideo/contributing/overview/)
- [Slack](https://join.slack.com/t/fastvideo/shared_invite/zt-3f4lao1uq-u~Ipx6Lt4J27AlD2y~IdLQ)
pr-message: |
Welcome to FastVideo! Thanks for your first pull request.
**How our CI works:**
PRs run a two-tier CI system:
1. **Pre-commit** — formatting (yapf), linting (ruff), type checking (mypy). Runs immediately on every PR.
2. **Fastcheck** — core GPU tests (encoders, VAEs, transformers, kernels, unit tests). Runs automatically via Buildkite on relevant file changes (~10-15 min).
3. **Full Suite** — integration tests, training pipelines, SSIM regression. Runs only when a reviewer adds the `ready` label.
**Before your PR is reviewed:**
- [ ] `pre-commit run --all-files` passes locally
- [ ] You've added or updated tests for your changes
- [ ] The PR description explains what and why
If pre-commit fails, a bot comment will explain how to fix it. Fastcheck and Full Suite results appear in the Checks section below.
**Useful links:**
- [Contributing Guide](https://hao-ai-lab.github.io/FastVideo/contributing/overview/)
- [Development Roadmap](https://github.com/hao-ai-lab/FastVideo/issues/899)
- [Slack](https://join.slack.com/t/fastvideo/shared_invite/zt-3f4lao1uq-u~Ipx6Lt4J27AlD2y~IdLQ)
@@ -7,14 +7,14 @@ on:
- 'docs/**'
- 'mkdocs.yml'
- 'requirements-mkdocs.txt'
- '.github/workflows/infra-docs.yml'
- '.github/workflows/docs.yml'
pull_request:
branches: [ main ]
paths:
- 'docs/**'
- 'mkdocs.yml'
- 'requirements-mkdocs.txt'
- '.github/workflows/infra-docs.yml'
- '.github/workflows/docs.yml'
permissions:
contents: read
@@ -36,11 +36,11 @@ jobs:
if [ "$NEW_VERSION" != "$OLD_VERSION" ]; then
echo "Version changed from $OLD_VERSION to $NEW_VERSION"
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "new-version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
echo "changed=true" >> $GITHUB_OUTPUT
echo "new-version=$NEW_VERSION" >> $GITHUB_OUTPUT
else
echo "Version did not change"
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "changed=false" >> $GITHUB_OUTPUT
fi
build_wheels:
-17
View File
@@ -1,17 +0,0 @@
{
"problemMatcher": [
{
"owner": "ruff",
"pattern": [
{
"regexp": "^(.+):(\\d+):(\\d+): (\\w+) (.+)$",
"file": 1,
"line": 2,
"column": 3,
"code": 4,
"message": 5
}
]
}
]
}
+338
View File
@@ -0,0 +1,338 @@
name: PR Test
on:
push:
branches: [main]
paths:
- "fastvideo/**/*.py"
- ".github/workflows/pr-test.yml"
pull_request:
branches: [main]
types: [opened, ready_for_review, synchronize, reopened]
paths:
- "fastvideo/**/*.py"
- ".github/workflows/pr-test.yml"
- "pyproject.toml"
- "docker/Dockerfile.python3.12"
- "csrc/**"
workflow_dispatch:
inputs:
run_encoder_test:
description: "Run encoder-test"
required: false
default: false
type: boolean
run_vae_test:
description: "Run vae-test"
required: false
default: false
type: boolean
run_transformer_test:
description: "Run transformer-test"
required: false
default: false
type: boolean
run_ssim_test:
description: "Run ssim-test"
required: false
default: false
type: boolean
run_training_test:
description: "Run training-test"
required: false
default: false
type: boolean
run_training_test_VSA:
description: "Run training-test-VSA"
required: false
default: false
type: boolean
run_precision_test_VSA:
description: "Run precision-test-VSA"
required: false
default: false
type: boolean
run_unit_test:
description: "Run unit-test"
required: false
default: false
type: boolean
env:
PYTHONUNBUFFERED: "1"
concurrency:
group: pr-test-${{ github.ref }}
cancel-in-progress: true
jobs:
pre-commit:
uses: ./.github/workflows/pre-commit.yml
change-filter:
runs-on: ubuntu-latest
needs: pre-commit
if: ${{ github.event.pull_request.draft == false || github.event_name == 'workflow_dispatch' }}
outputs:
encoder-test: ${{ steps.filter.outputs.encoder-test }}
vae-test: ${{ steps.filter.outputs.vae-test }}
transformer-test: ${{ steps.filter.outputs.transformer-test }}
training-test: ${{ steps.filter.outputs.training-test }}
training-test-VSA: ${{ steps.filter.outputs.training-test-VSA }}
precision-test-VSA: ${{ steps.filter.outputs.precision-test-VSA }}
unit-test: ${{ steps.filter.outputs.unit-test }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
# Define reusable path patterns
common-paths: &common-paths
- 'pyproject.toml'
- 'docker/Dockerfile.python3.10'
- 'docker/Dockerfile.python3.11'
- 'docker/Dockerfile.python3.12'
vsa-kernel-paths: &vsa-kernel-paths
- 'csrc/attn/video_sparse_attn/**'
- 'csrc/attn/video_sparse_attn/tk/**'
- 'csrc/attn/video_sparse_attn/setup.py'
- 'csrc/attn/video_sparse_attn/config_vsa.py'
- 'csrc/attn/video_sparse_attn/vsa.cpp'
vsa-paths: &vsa-paths
- 'fastvideo/**'
- *common-paths
- *vsa-kernel-paths
# Actual tests
encoder-test:
- 'fastvideo/models/encoders/**'
- 'fastvideo/models/loader/**'
- 'fastvideo/tests/encoders/**'
- *common-paths
vae-test:
- 'fastvideo/models/vaes/**'
- 'fastvideo/models/loader/**'
- 'fastvideo/tests/vaes/**'
- *common-paths
transformer-test:
- 'fastvideo/models/dits/**'
- 'fastvideo/models/loader/**'
- 'fastvideo/tests/transformers/**'
- 'fastvideo/layers/**'
- 'fastvideo/attention/**'
- *common-paths
training-test:
- 'fastvideo/**'
- *common-paths
training-test-VSA:
- 'fastvideo/**'
- *common-paths
- *vsa-kernel-paths
precision-test-VSA:
- *common-paths
- *vsa-kernel-paths
unit-test:
- 'fastvideo/**'
- *common-paths
encoder-test:
needs: change-filter
if: >-
(github.event_name != 'workflow_dispatch' && needs.change-filter.outputs.encoder-test == 'true') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_encoder_test == 'true')
uses: ./.github/workflows/runpod-test.yml
with:
job_id: "encoder-test"
gpu_type: "NVIDIA A40"
gpu_count: 1
volume_size: 100
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest"
test_command: "uv pip install -e .[test] && pytest ./fastvideo/tests/encoders -s"
timeout_minutes: 30
secrets:
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
vae-test:
needs: change-filter
if: >-
(github.event_name != 'workflow_dispatch' && needs.change-filter.outputs.vae-test == 'true') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_vae_test == 'true')
uses: ./.github/workflows/runpod-test.yml
with:
job_id: "vae-test"
gpu_type: "NVIDIA A40"
gpu_count: 1
volume_size: 100
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest"
test_command: "uv pip install -e .[test] && pytest ./fastvideo/tests/vaes -s"
timeout_minutes: 30
secrets:
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
transformer-test:
needs: change-filter
if: >-
(github.event_name != 'workflow_dispatch' && needs.change-filter.outputs.transformer-test == 'true') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_transformer_test == 'true')
uses: ./.github/workflows/runpod-test.yml
with:
job_id: "transformer-test"
gpu_type: "NVIDIA L40S"
gpu_count: 1
volume_size: 100
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest"
test_command: "uv pip install -e .[test] && pytest ./fastvideo/tests/transformers -s"
timeout_minutes: 30
secrets:
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
ssim-test:
needs: change-filter
if: >-
github.event_name != 'workflow_dispatch' || (github.event_name == 'workflow_dispatch' && github.event.inputs.run_ssim_test == 'true')
strategy:
fail-fast: false
matrix:
python-version: [
# {version: "3.10", tag: "latest"},
# {version: "3.11", tag: "py3.11-latest"},
{version: "3.12", tag: "py3.12-latest"}
]
uses: ./.github/workflows/runpod-test.yml
with:
job_id: "ssim-test-py${{ matrix.python-version.version }}"
gpu_type: "NVIDIA A40"
gpu_count: 2
volume_size: 200
disk_size: 200
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:${{ matrix.python-version.tag }}"
test_command: "uv pip install -e .[test] && pytest ./fastvideo/tests/ssim -vs"
timeout_minutes: 60
secrets:
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
training-test:
needs: change-filter
if: >-
(github.event_name != 'workflow_dispatch' && needs.change-filter.outputs.training-test == 'true') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_training_test == 'true')
uses: ./.github/workflows/runpod-test.yml
with:
job_id: "training-test"
gpu_type: "NVIDIA A40"
gpu_count: 4
volume_size: 100
disk_size: 100
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest"
test_command: "wandb login $WANDB_API_KEY && uv pip install -e .[test] && pytest ./fastvideo/tests/training/Vanilla -srP"
timeout_minutes: 30
secrets:
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }}
training-test-VSA:
needs: change-filter
if: >-
(github.event_name != 'workflow_dispatch' && needs.change-filter.outputs.training-test-VSA == 'true') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_training_test_VSA == 'true')
uses: ./.github/workflows/runpod-test.yml
with:
job_id: "training-test-VSA"
gpu_type: "NVIDIA H100 NVL"
gpu_count: 2
volume_size: 100
disk_size: 100
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest"
test_command: "wandb login $WANDB_API_KEY && uv pip install -e .[test] && pytest ./fastvideo/tests/training/VSA -srP"
timeout_minutes: 30
secrets:
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }}
precision-test-VSA:
needs: change-filter
if: >-
(github.event_name != 'workflow_dispatch' && needs.change-filter.outputs.precision-test-VSA == 'true') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_precision_test_VSA == 'true')
uses: ./.github/workflows/runpod-test.yml
with:
job_id: "precision-test-VSA"
gpu_type: "NVIDIA H100 NVL"
gpu_count: 1
volume_size: 100
disk_size: 100
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest"
test_command: "uv pip install -e .[test] && python csrc/attn/tests/test_vsa.py"
timeout_minutes: 30
secrets:
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
unit-test:
needs: change-filter
if: >-
(github.event_name != 'workflow_dispatch' && needs.change-filter.outputs.unit-test == 'true') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_unit_test == 'true')
uses: ./.github/workflows/runpod-test.yml
with:
job_id: "unit-test"
gpu_type: "NVIDIA L40S"
gpu_count: 1
volume_size: 100
disk_size: 100
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest"
test_command: "uv pip install -e .[test] && pytest ./fastvideo/dataset/ -vs && pytest ./fastvideo/workflow/ -vs && pytest ./fastvideo/entrypoints/ -vs"
timeout_minutes: 30
secrets:
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
# nightly-test:
# if: >-
# (github.event_name == 'workflow_dispatch' && github.event.inputs.run_nightly_test == 'true')
# uses: ./.github/workflows/runpod-test.yml
# with:
# job_id: "nightly-test"
# gpu_type: "NVIDIA A40"
# gpu_count: 4
# volume_size: 100
# disk_size: 100
# image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest"
# test_command: "wandb login $WANDB_API_KEY && uv pip install -e .[test] && pytest ./fastvideo/tests/nightly/test_e2e_overfit_single_sample.py -vs"
# timeout_minutes: 30
# secrets:
# RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
# RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
# WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }}
runpod-cleanup:
# Add other jobs to this list as you create them
needs: [encoder-test, vae-test, transformer-test, ssim-test, training-test, training-test-VSA, precision-test-VSA]
if: ${{ always() && ((github.event_name != 'workflow_dispatch' && github.event.pull_request.draft == false) || github.event_name == 'workflow_dispatch') }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install dependencies
run: pip install requests
- name: Cleanup all RunPod instances
env:
JOB_IDS: '["encoder-test", "vae-test", "transformer-test", "ssim-test-py3.10", "ssim-test-py3.11", "ssim-test-py3.12", "training-test", "training-test-VSA", "precision-test-VSA"]'
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
GITHUB_RUN_ID: ${{ github.run_id }}
run: python .github/scripts/runpod_cleanup.py
+18
View File
@@ -0,0 +1,18 @@
name: pre-commit
on:
workflow_call:
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: echo "::add-matcher::.github/workflows/matchers/actionlint.json"
- run: echo "::add-matcher::.github/workflows/matchers/mypy.json"
- uses: pre-commit/action@v3.0.1
with:
extra_args: --all-files --hook-stage manual
+94
View File
@@ -0,0 +1,94 @@
name: RunPod Test
on:
workflow_call:
inputs:
job_id:
required: true
type: string
description: "Unique identifier for this test job"
gpu_type:
required: true
type: string
description: "GPU type to use (e.g. NVIDIA A40, NVIDIA L40S)"
gpu_count:
required: true
type: number
description: "Number of GPUs to use"
volume_size:
required: false
type: number
default: 20
description: "Volume size in GB"
disk_size:
required: false
type: number
default: 20
description: "Disk size in GB"
image:
required: true
type: string
description: "Docker image to use"
test_command:
required: true
type: string
description: "Command to run tests"
timeout_minutes:
required: false
type: number
default: 30
description: "Timeout in minutes"
secrets:
RUNPOD_API_KEY:
required: true
RUNPOD_PRIVATE_KEY:
required: true
WANDB_API_KEY:
required: false
jobs:
run-test:
runs-on: ubuntu-latest
environment: runpod-runners
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Set up SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.RUNPOD_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keygen -y -f ~/.ssh/id_rsa > ~/.ssh/id_rsa.pub
- name: Install dependencies
run: pip install requests
- name: Run tests on RunPod
env:
JOB_ID: ${{ inputs.job_id }}
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
GITHUB_RUN_ID: ${{ github.run_id }}
WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }}
timeout-minutes: ${{ inputs.timeout_minutes }}
run: >-
python .github/scripts/runpod_api.py
--gpu-type "${{ inputs.gpu_type }}"
--gpu-count ${{ inputs.gpu_count }}
--volume-size ${{ inputs.volume_size }}
--disk-size ${{ inputs.disk_size }}
--image "${{ inputs.image }}"
--test-command "${{ inputs.test_command }}"
- name: Terminate RunPod Instances
if: ${{ always() }}
env:
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
GITHUB_RUN_ID: ${{ github.run_id }}
JOB_ID: ${{ inputs.job_id }}
run: python .github/scripts/runpod_cleanup.py
+31
View File
@@ -0,0 +1,31 @@
name: Run Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12' # or any version you need
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools wheel
pip install torch
pip install packaging ninja
pip install -e .
pip install pytest
- name: Run Pytest
run: |
pytest --ignore csrc/attn/test
+257
View File
@@ -0,0 +1,257 @@
name: Publish Video Sparse Attention Kernel to PyPI on Version Change
on:
push:
branches:
- main
paths:
- "csrc/attn/video_sparse_attn/setup.py"
workflow_dispatch:
jobs:
check-version-change:
runs-on: ubuntu-latest
outputs:
version-changed: ${{ steps.check-version.outputs.changed }}
new-version: ${{ steps.check-version.outputs.new-version }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Check if version changed
id: check-version
run: |
cd csrc/attn/video_sparse_attn
# Get current commit's version
NEW_VERSION=$(grep -oP 'VERSION\s*=\s*"\K[^"]+' setup.py)
echo "New version: $NEW_VERSION"
# Get previous version from git history
OLD_VERSION=$(git show HEAD~1:./setup.py | grep -oP 'VERSION\s*=\s*"\K[^"]+' || echo "0.0.0")
echo "Old version: $OLD_VERSION"
if [ "$NEW_VERSION" != "$OLD_VERSION" ]; then
echo "Version changed from $OLD_VERSION to $NEW_VERSION"
echo "changed=true" >> $GITHUB_OUTPUT
echo "new-version=$NEW_VERSION" >> $GITHUB_OUTPUT
else
echo "Version did not change"
echo "changed=false" >> $GITHUB_OUTPUT
fi
build_wheels:
name: Build Wheel
needs: check-version-change
if: ${{ needs.check-version-change.outputs.version-changed == 'true' || github.event_name == 'workflow_dispatch' }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
# Using ubuntu-20.04 instead of 22.04 for more compatibility (glibc). Ideally we'd use the
# manylinux docker image, but I haven't figured out how to install CUDA on manylinux.
os: [ubuntu-22.04]
python-version: ['3.10', '3.11', '3.12', '3.13']
# For version reference https://pytorch.org/get-started/previous-versions/
torch-cuda:
- torch-version: '2.5.1'
cuda-version: '12.4.1'
torch-cuda-short: 'cu124'
- torch-version: '2.6.0'
cuda-version: '12.6.3'
torch-cuda-short: 'cu126'
- torch-version: '2.7.1'
cuda-version: '12.8.0'
torch-cuda-short: 'cu128'
steps:
- name: Free up disk space
run: |
echo "Initial disk space:"
df -h
# Remove large directories
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/share/boost
sudo rm -rf /usr/share/swift
sudo rm -rf /usr/local/lib/node_modules
sudo rm -rf /usr/local/share/powershell
sudo rm -rf /usr/share/rust
sudo rm -rf /usr/local/.ghcup
# Remove cached files
sudo rm -rf /var/lib/apt/lists/*
sudo rm -rf /var/cache/apt/archives/*
echo "Disk space after cleanup:"
df -h
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install CUDA ${{ matrix.torch-cuda.cuda-version }}
uses: Jimver/cuda-toolkit@v0.2.21
id: cuda-toolkit
with:
cuda: ${{ matrix.torch-cuda.cuda-version }}
linux-local-args: '["--toolkit"]'
method: 'network'
- name: Install dependencies (GCC, Clang, CUDA Paths, Git)
run: |
sudo apt update
sudo apt install -y git patchelf gcc-11 g++-11 clang-11
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 100 --slave /usr/bin/g++ g++ /usr/bin/g++-11
# Allow Git to Access Safe Directory
git config --global --add safe.directory /__w/FastVideo/FastVideo
# Set CUDA environment variables
export CUDA_HOME=/usr/local/cuda-${{ matrix.torch-cuda.cuda-version }}
export PATH=${CUDA_HOME}/bin:${PATH}
export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:$LD_LIBRARY_PATH
# Verify installation
gcc --version
g++ --version
clang-11 --version
nvcc --version
- name: Install PyTorch ${{ matrix.torch-cuda.torch-version }}+cu${{ matrix.torch-cuda.cuda-version }}
run: |
pip install --upgrade pip
# With python 3.13 and torch 2.5.1, unless we update typing-extensions, we get error
# AttributeError: attribute '__default__' of 'typing.ParamSpec' objects is not writable
pip install typing-extensions==4.12.2
# We want to figure out the CUDA version to download pytorch
# e.g. we can have system CUDA version being 11.7 but if torch==1.12 then we need to download the wheel from cu116
# see https://github.com/pytorch/pytorch/blob/main/RELEASE.md#release-compatibility-matrix
pip install --no-cache-dir torch==${{ matrix.torch-cuda.torch-version }} --index-url https://download.pytorch.org/whl/${{matrix.torch-cuda.torch-cuda-short}}
nvcc --version
python --version
python -c "import torch; print('PyTorch:', torch.__version__)"
python -c "import torch; print('CUDA:', torch.version.cuda)"
python -c "from torch.utils import cpp_extension; print (cpp_extension.CUDA_HOME)"
- name: Build wheel
run: |
export PYTHONPATH=$GITHUB_WORKSPACE:$PYTHONPATH
# We want setuptools >= 49.6.0 otherwise we can't compile the extension if system CUDA version is 11.7 and pytorch cuda version is 11.6
# https://github.com/pytorch/pytorch/blob/664058fa83f1d8eede5d66418abff6e20bd76ca8/torch/utils/cpp_extension.py#L810
# However this still fails so I'm using a newer version of setuptools
pip install setuptools
pip install ninja packaging wheel
cd csrc/attn/video_sparse_attn # Move into the correct folder
git submodule update --init --recursive # Ensure ThunderKittens submodule is initialized
python setup.py bdist_wheel --dist-dir=dist
- name: Rename wheel file
run: |
cd csrc/attn/video_sparse_attn
CUDA_SHORT_VERSION=$(echo ${{ matrix.torch-cuda.cuda-version }} | cut -d. -f1,2 | sed 's/\.//g')
TORCH_SHORT_VERSION=$(echo ${{ matrix.torch-cuda.torch-version }} | cut -d. -f1,2)
# Get the correct version format
tmpname=cu${CUDA_SHORT_VERSION}torch${TORCH_SHORT_VERSION}
wheel_name=$(ls dist/*whl | xargs -n 1 basename | sed "s/-/+$tmpname-/2")
# Rename with version information
ls dist/*whl |xargs -I {} mv {} dist/${wheel_name}
echo "wheel_name=${wheel_name}" >> $GITHUB_ENV
- name: Upload wheel artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.wheel_name }}
path: csrc/attn/video_sparse_attn/dist/*.whl
retention-days: 90
publish_package:
name: Publish package
needs: [build_wheels, check-version-change]
if: ${{ needs.check-version-change.outputs.version-changed == 'true' || github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-22.04
permissions:
id-token: write # Needed for OIDC Trusted Publishing
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install CUDA 12.4.1
uses: Jimver/cuda-toolkit@v0.2.21
id: cuda-toolkit
with:
cuda: 12.4.1
linux-local-args: '["--toolkit"]'
method: 'network'
sub-packages: '["nvcc"]'
- name: Install dependencies (GCC, Clang, CUDA Paths, Git)
run: |
sudo apt update
sudo apt install -y git patchelf gcc-11 g++-11 clang-11
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 100 --slave /usr/bin/g++ g++ /usr/bin/g++-11
# Allow Git to Access Safe Directory
git config --global --add safe.directory /__w/FastVideo/FastVideo
# Set CUDA environment variables
export CUDA_HOME=/usr/local/cuda-12.4.1
export PATH=${CUDA_HOME}/bin:${PATH}
export LD_LIBRARY_PATH=${CUDA_HOME}/lib64:$LD_LIBRARY_PATH
# Verify installation
gcc --version
g++ --version
clang-11 --version
nvcc --version
- name: Install PyTorch 2.5.1+cu12.4.1
run: |
pip install --upgrade pip
# With python 3.13 and torch 2.5.1, unless we update typing-extensions, we get error
# AttributeError: attribute '__default__' of 'typing.ParamSpec' objects is not writable
pip install typing-extensions==4.12.2
# We want to figure out the CUDA version to download pytorch
# e.g. we can have system CUDA version being 11.7 but if torch==1.12 then we need to download the wheel from cu116
# see https://github.com/pytorch/pytorch/blob/main/RELEASE.md#release-compatibility-matrix
export TORCH_CUDA_VERSION=124
pip install --no-cache-dir torch==2.5.1 --index-url https://download.pytorch.org/whl/cu${TORCH_CUDA_VERSION}
nvcc --version
python --version
python -c "import torch; print('PyTorch:', torch.__version__)"
python -c "import torch; print('CUDA:', torch.version.cuda)"
python -c "from torch.utils import cpp_extension; print (cpp_extension.CUDA_HOME)"
- name: Build source distribution
run: |
export PYTHONPATH=$GITHUB_WORKSPACE:$PYTHONPATH
# We want setuptools >= 49.6.0 otherwise we can't compile the extension if system CUDA version is 11.7 and pytorch cuda version is 11.6
# https://github.com/pytorch/pytorch/blob/664058fa83f1d8eede5d66418abff6e20bd76ca8/torch/utils/cpp_extension.py#L810
# However this still fails so I'm using a newer version of setuptools
pip install setuptools
pip install ninja packaging wheel
cd csrc/attn/video_sparse_attn # Move into the correct folder
git submodule update --init --recursive # Ensure ThunderKittens submodule is initialized
python setup.py sdist --dist-dir=dist
- name: Publish release distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: csrc/attn/video_sparse_attn/dist/
-3
View File
@@ -85,9 +85,6 @@ docs/distillation/examples/
dmd_t2v_output/
preprocess_output_text/
.claude/
.codex/
.sisyphus/
openspec/
fastvideo/tests/ssim/reference_videos/**
+4 -2
View File
@@ -18,8 +18,10 @@ exclude: |
fastvideo/train\.py|
fastvideo/utils/.*|
examples/.*|
.github/workflows/publish-fastvideo.yml|
.github/workflows/_template-build-image.yml|
.github/workflows/fastvideo-publish.yml|
.github/workflows/sta-publish.yml|
.github/workflows/vsa-publish.yml|
.github/workflows/build-image-template.yml|
docs/source/inference/support_matrix.md
)
repos:
+1 -1
View File
@@ -9,7 +9,7 @@
**FastVideo is a unified post-training and real-time inference framework for accelerated video generation.**
## NEWS
- `2026/03/17`: Release Live demo: [Into the Dreamverse: Vibe Directing in FastVideo](https://dreamverse.fastvideo.org/), check out the [Blog](https://haoailab.com/blogs/dreamverse/).
- `2026/03/13`: Release Live demo: [Create a 5s 1080p Video in 4.5s with FastVideo on a Single GPU](https://1080p.fastvideo.org/), check out the [Blog](https://haoailab.com/blogs/fastvideo_realtime_1080p/).
- `2025/11/19`: Release [CausalWan2.2 I2V A14B Preview](https://huggingface.co/FastVideo/CausalWan2.2-I2V-A14B-Preview-Diffusers) models, [Blog](https://hao-ai-lab.github.io/blogs/fastvideo_causalwan_preview/) and [Inference Code!](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_self_forcing_causal_wan2_2_i2v.py).
- `2025/08/04`: Release [FastWan](https://hao-ai-lab.github.io/FastVideo/distillation/dmd) models and [Sparse-Distillation](https://hao-ai-lab.github.io/blogs/fastvideo_post_training/).
+1 -1
View File
@@ -36,4 +36,4 @@ Then open your browser to: http://localhost:8000
## Automatic Deployment
Documentation is automatically built and deployed to GitHub Pages when changes are pushed to the `main` branch via the `.github/workflows/infra-docs.yml` workflow.
Documentation is automatically built and deployed to GitHub Pages when changes are pushed to the `main` branch via the `.github/workflows/docs.yml` workflow.
-306
View File
@@ -1,306 +0,0 @@
# CI Architecture
## Overview
FastVideo uses a three-tier CI pipeline designed to keep feedback fast on every push while
protecting `main` through a full GPU regression suite before any merge.
```
PR push
│
├─► Tier 1: Pre-commit (~2 min)
│ GitHub Actions / ubuntu-latest
│ yapf, ruff, mypy, codespell, pymarkdown, actionlint, check-filenames
│
└─► Tier 2: Fastcheck (~10-20 min, path-filtered)
Buildkite / Modal GPU instances
Only runs tests for paths you changed
│ (developer comments /merge or maintainer adds 'ready' label)
▼
Tier 3: Full Suite (~60-90 min)
Buildkite / Modal GPU instances
All integration, SSIM, training, and performance tests
Runs on the PR branch directly
│
pass ──► Mergify auto-squash-merges to main, branch deleted
fail ──► Mergify removes 'ready' label; fix and /merge again
```
---
## CI Tiers
### Tier 1: Pre-commit (every PR push)
| Attribute | Value |
|-----------|-------|
| Triggered by | Every push to any PR branch, plus pushes to `main` |
| Runs on | GitHub Actions, `ubuntu-latest` |
| Duration | ~2 minutes |
**Checks run** (from `.pre-commit-config.yaml`, stage: `manual`):
| Hook | What it checks |
|------|---------------|
| `yapf` | Python code formatting |
| `ruff` | Python linting and auto-fixable style issues |
| `codespell` | Spelling errors in code and docs |
| `pymarkdown` | Markdown formatting |
| `actionlint` | GitHub Actions workflow syntax |
| `mypy` | Static type checking (Python 3.10 target) |
| `check-filenames` | No spaces in tracked filenames |
A failure here means code style or type issues. Run `pre-commit run --all-files` locally to
replicate CI results before pushing.
---
### Tier 2: Fastcheck (path-filtered, every PR push)
| Attribute | Value |
|-----------|-------|
| Triggered by | Every push; the monorepo-diff plugin skips tests for unchanged paths |
| Runs on | Buildkite, Modal GPU instances |
| Duration | ~10-20 minutes per test (run in parallel) |
**Tests and their path triggers:**
| Buildkite label | `TEST_TYPE` | Triggers when you change |
|-----------------|-------------|--------------------------|
| Encoder Tests | `encoder` | `fastvideo/models/encoders/**`, `fastvideo/models/loader/**`, `fastvideo/tests/encoders/**`, `pyproject.toml`, `docker/Dockerfile.python3.12` |
| VAE Tests | `vae` | `fastvideo/models/vaes/**`, `fastvideo/models/loader/**`, `fastvideo/tests/vaes/**`, `pyproject.toml`, `docker/Dockerfile.python3.12` |
| Transformer Tests | `transformer` | `fastvideo/models/dits/**`, `fastvideo/models/loader/**`, `fastvideo/tests/transformers/**`, `fastvideo/layers/**`, `fastvideo/attention/**`, `pyproject.toml`, `docker/Dockerfile.python3.12` |
| Kernel Tests | `kernel_tests` | `fastvideo-kernel/**`, `pyproject.toml`, `docker/Dockerfile.python3.12` |
| Unit Tests | `unit_test` | `fastvideo/**`, `.buildkite/**`, `.github/**`, `pyproject.toml`, `docker/Dockerfile.python3.12` |
A Fastcheck failure means a component-level regression. Check the Buildkite build log for the
failing test's output.
---
### Tier 3: Full Test Suite (triggered by `ready` label)
| Attribute | Value |
|-----------|-------|
| Triggered by | Adding the `ready` label to the PR (via `/merge` command), or a `/test full` command |
| Runs on | Buildkite, Modal GPU instances |
| Duration | 60-90 minutes total (tests run in parallel, path-filtered) |
**Tests:**
| Buildkite label | `TEST_TYPE` | Timeout |
|-----------------|-------------|---------|
| SSIM Tests | `ssim` | 90 min |
| LoRA Inference Tests | `inference_lora` | 20 min |
| Training Tests | `training` | 15 min |
| Distillation DMD Tests | `distillation_dmd` | 15 min |
| Self-Forcing Tests | `self_forcing` | 15 min |
| LoRA Training Tests | `training_lora` | 15 min |
| Training Tests VSA | `training_vsa` | 15 min |
| Inference Tests VMoBA | `inference_vmoba` | 15 min |
| Performance Tests | `performance` | 30 min |
| API Server Tests | `api_server` | 30 min |
A Full Suite failure removes the `ready` label automatically. A Mergify comment links to
the Buildkite build. Fix the regression, push, and comment `/merge` again.
---
## Auto-merge Flow
Mergify prevents untested code from landing on `main` by gating squash-merge on the Full
Suite passing directly on the PR branch.
**How it works:**
1. A developer comments `/merge` on an approved PR (or a maintainer adds the `ready` label).
2. The `ready` label triggers `ci-trigger-full-suite.yml`, which calls the Buildkite API to
run the Full Suite on the PR branch itself.
3. While the Full Suite runs, Mergify also auto-rebases the PR branch against `main` if it
is behind and has no conflicts.
4. Once the Full Suite posts `full-suite-passed`, Mergify checks all **merge conditions**:
- `pre-commit` check is green
- `fastcheck-passed` check is green
- `full-suite-passed` check is green
- At least 1 approved review (`#approved-reviews-by>=1`)
- PR title starts with a valid `[type]` tag
- PR is not a draft
- No merge conflicts
5. If all conditions pass, Mergify squash-merges to `main` automatically. The branch is
deleted after merge.
6. If the Full Suite fails, Mergify removes the `ready` label and posts a comment linking to
the Buildkite build. The developer fixes the issue, pushes, and comments `/merge` again.
**Merge conditions summary:**
| Condition | Meaning |
|-----------|---------|
| `check-success~=pre-commit` | Tier 1 pre-commit must be green |
| `check-success=fastcheck-passed` | Tier 2 Fastcheck must be green |
| `check-success=full-suite-passed` | Tier 3 Full Suite must be green |
| `#approved-reviews-by>=1` | At least one approved review |
| `title~=(?i)^\[(feat|bugfix|...)` | PR title has a valid type tag |
| `-draft` | PR is not in draft state |
| `-conflict` | No merge conflicts with base branch |
| `-closed` | PR is still open |
---
## Label System
Labels are applied automatically. You don't need to set them manually.
### Type Labels (from PR title prefix)
Applied by Mergify based on the `[tag]` at the start of the PR title.
| Label | Matched title prefix | Meaning |
|-------|---------------------|---------|
| `type: feat` | `[feat]` or `[feature]` | New feature or capability |
| `type: bugfix` | `[bugfix]` or `[fix]` | Bug fix |
| `type: refactor` | `[refactor]` | Code restructuring, no behavior change |
| `type: perf` | `[perf]` | Performance improvement |
| `type: ci` | `[ci]` | CI/CD or tooling changes |
| `type: docs` | `[doc]` or `[docs]` | Documentation only |
| `type: misc` | `[misc]` or `[chore]` | Housekeeping, dependency bumps |
| `type: new-model` | `[new-model]` | Adding a new model |
### Scope Labels (from changed files)
Applied by Mergify based on which paths you modified. Multiple scope labels can be added.
| Label | File paths that trigger it |
|-------|---------------------------|
| `scope: training` | `fastvideo/train/`, `fastvideo/training/`, `fastvideo/distillation/`, `examples/train/`, `examples/training/`, `examples/distill/` |
| `scope: inference` | `fastvideo/pipelines/basic/`, `fastvideo/pipelines/stages/`, `fastvideo/pipelines/samplers/`, `fastvideo/entrypoints/`, `fastvideo/worker/`, `fastvideo/configs/sample/`, `fastvideo/configs/pipelines/`, `examples/inference/` |
| `scope: attention` | `fastvideo/attention/` |
| `scope: kernel` | `fastvideo-kernel/`, `csrc/` |
| `scope: data` | `fastvideo/dataset/`, `fastvideo/pipelines/preprocess/`, `examples/preprocessing/` |
| `scope: infra` | `.github/`, `.buildkite/`, `fastvideo/tests/`, `docker/` |
| `scope: distributed` | `fastvideo/distributed/` |
| `scope: docs` | `docs/` |
| `scope: ui` | `ui/` |
| `scope: model` | `fastvideo/models/`, `fastvideo/layers/`, `fastvideo/configs/models/` |
### Process Labels
| Label | Who sets it | Meaning |
|-------|-------------|---------|
| `ready` | Developer (`/merge` command) or maintainer | Triggers Full Suite and enables auto-merge |
| `needs-rebase` | Mergify (automatic) | PR has merge conflicts; rebase needed |
| `do-not-merge` | Maintainer | Blocks queue entry regardless of other conditions |
---
## PR Title Format
All PR titles targeting `main` must start with a bracketed type tag. This is enforced by a
Mergify merge protection rule and is required before a PR can be squash-merged.
**Format:**
```
[type] Short description
```
**Valid type tags:**
`feat`, `feature`, `bugfix`, `fix`, `refactor`, `perf`, `ci`, `doc`, `docs`, `misc`, `chore`,
`kernel`, `new-model`
**Valid examples:**
```
[feat] Add causal Wan pipeline
[bugfix] Fix VAE temporal tiling corruption
[refactor] Restructure training framework
[perf] Optimize FlashAttention kernel dispatch
[docs] Add inference guide for LoRA
[new-model] Port HunyuanVideo 1.5 to FastVideo
```
**Invalid examples (will block merge):**
```
Add causal Wan pipeline ← missing type tag
FEAT: Add pipeline ← wrong format
feat: Add pipeline ← square brackets required
```
If your title is invalid, Mergify posts a comment explaining the required format and the
merge protection check will remain failed until you update the title.
---
## Slash Commands
Slash commands let contributors and maintainers trigger CI actions directly from PR comments.
**Write permission to the repository is required.**
The command is recognized within a few seconds. The workflow reacts with a 🚀 emoji to confirm.
### `/merge`
```
/merge
```
Adds the `ready` label to the PR, which triggers the Full Suite on your PR branch and
enables Mergify to auto-squash-merge once all conditions pass.
The command first removes the `ready` label if it is already present, then re-adds it. This
ensures the `labeled` event fires and a fresh Full Suite build is started even on a re-try.
### `/test <name>`
```
/test <name>
```
Triggers a specific Buildkite test or suite on the current PR branch.
| Command | Runs | Maps to `TEST_TYPE` |
|---------|------|---------------------|
| `/test encoder` | Encoder Tests (Fastcheck) | `encoder` |
| `/test vae` | VAE Tests (Fastcheck) | `vae` |
| `/test transformer` | Transformer Tests (Fastcheck) | `transformer` |
| `/test kernel` | Kernel Tests (Fastcheck) | `kernel_tests` |
| `/test unit` | Unit Tests (Fastcheck) | `unit_test` |
| `/test ssim` | SSIM regression tests | `ssim` |
| `/test training` | Training pipeline tests | `training` |
| `/test lora-inference` | LoRA inference tests | `inference_lora` |
| `/test lora-training` | LoRA training tests | `training_lora` |
| `/test distillation` | DMD distillation tests | `distillation_dmd` |
| `/test self-forcing` | Self-Forcing tests | `self_forcing` |
| `/test vsa` | VSA training tests | `training_vsa` |
| `/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`) |
---
## Auto Branch Cleanup
After a PR is squash-merged to `main`, Mergify automatically deletes the head branch.
Protected branches (`main`, `master`, `release/*`) are never deleted.
---
## Workflow File Reference
| Filename | Trigger | What it does |
|----------|---------|-------------|
| `ci-precommit.yml` | Every push / PR against `main` | Runs pre-commit hooks (yapf, ruff, mypy, codespell, pymarkdown, actionlint, check-filenames) |
| `ci-trigger-full-suite.yml` | `ready` label added to a PR | Calls Buildkite API to run Full Suite on the PR branch |
| `ci-slash-commands.yml` | PR comment starting with `/merge` or `/test` | Handles slash commands; adds `ready` label or triggers Buildkite |
| `community-issue-labeler.yml` | Issue opened or edited | Auto-labels issues by keyword matching against title and body |
| `community-welcome.yml` | First contribution | Posts a welcome comment for first-time contributors |
| `community-stale.yml` | Scheduled | Marks and closes stale issues and PRs |
| `infra-build-image.yml` | Manual (`workflow_dispatch`) | Builds Docker images for CI |
| `infra-docs.yml` | Changes to `docs/` merged to `main` | Builds and deploys documentation to GitHub Pages |
| `publish-fastvideo.yml` | Version bump | Publishes `fastvideo` package to PyPI |
| `publish-kernel.yml` | Version bump | Publishes `fastvideo-kernel` package to PyPI |
| `publish-comfyui.yml` | Version bump | Publishes ComfyUI node package to PyPI |
+33 -163
View File
@@ -1,195 +1,65 @@
# RunPod Development Environment
RunPod gives you on-demand cloud GPUs for FastVideo development. It's useful when you need a beefy GPU to test training runs, benchmark inference, or reproduce results without waiting for shared cluster time.
# 📦 Developing FastVideo on RunPod
## Prerequisites
You can easily use the FastVideo Pod Template on [RunPod](https://www.runpod.io) for development or experimentation.
- A [RunPod](https://www.runpod.io) account with billing configured
- An SSH key pair. If you don't have one, generate it with `ssh-keygen -t ed25519`
- Your public key (`~/.ssh/id_ed25519.pub`) ready to paste into RunPod
## Step 1: Create a Pod
**1. Verify your account**
Make sure you're logged into the right RunPod account before spending credits.
## Creating a new pod
- Make sure you are using the correct RunPod account.
![RunPod Account Selection](../../assets/images/runpod_account.png)
**2. Filter by CUDA version**
Use "Additional Filters" to select CUDA 12.8.
- Use "Additional Filters" to select CUDA 12.8.
![RunPod CUDA selection](../../assets/images/runpod_cuda.png)
**3. Select a GPU**
Click "Deploy" and pick a GPU. See [GPU Recommendations](#gpu-recommendations) below for guidance on which GPU to choose.
- Click "Deploy" and Pick a single A40 or RTX 4090 GPU.
![RunPod GPU Selection](../../assets/images/runpod_deploy.png)
**4. Pick the FastVideo template**
Select the "FastVideo" or "fastvideo-dev" Pod Template. This pulls the pre-built image that includes all dependencies, Flash Attention, and a ready-to-use `uv` environment.
- Select the "FastVideo" or "fastvideo-dev" Pod Template.
![RunPod Pod Template Selection](../../assets/images/runpod_create.png)
**5. Name your pod**
Use a memorable name like `yourname-fastvideo-2026-03-28`. This helps if you have multiple pods running.
**6. Add a persistent volume (recommended)**
Attach a network volume to `/root/.cache` or `/models` for storing downloaded model weights. Models can be 10-50 GB each, and re-downloading them every session wastes time and bandwidth.
**7. Deploy**
Click Deploy. The pod takes a few minutes to start while the image pulls. You'll see it transition to "Running" in your dashboard.
## Step 2: Connect via SSH
Once the pod is running, find the "SSH over exposed TCP" connection string in the pod dashboard.
- Set the Pod name to "`<name>-<FastVideo>-<date>`".
- Finally, once the pod is deployed (will take a few minutes as the image is being pulled), you can SSH into it using "SSH exposed over TCP". You'll need to use the matching private ssh key you provided.
![RunPod SSH](../../assets/images/runpod_ssh.png)
Connect with:
## Working with the pod
After SSH'ing into your pod, you'll find the correct `uv` environment already activated and you should be in /FastVideo/ directory. Make sure to use /FastVideo/ for all your work.
To pull in the latest changes from the GitHub repo:
```bash
ssh root@<pod-ip> -p <port> -i ~/.ssh/id_ed25519
cd /FastVideo
git pull
```
RunPod also supports VS Code Remote SSH if you prefer an IDE.
Run your development workflows as usual:
### Custom template (advanced)
```bash
# Run linters
pre-commit run --all-files
If you're setting up a pod from scratch instead of the FastVideo template, use this image:
# Run tests
pytest tests/
```
Make sure to push your changes back to the GitHub repo as nothing will be saved to the pod when it is terminated.
After you are done with your work, you can terminate the pod by clicking the "Terminate" and "Delete" buttons. Remember if the pod is not completely deleted, Runpod will keep charging you for it.
## Extra Information:
If you need to customize the pod template this section has some useful information. For the most part you can leave the defaults of the FastVideo Pod Template.
When creating your pod template, use this image:
```
ghcr.io/hao-ai-lab/fastvideo/fastvideo-dev:py3.12-latest
```
And paste this as the Container Start Command to enable SSH ([RunPod docs](https://docs.runpod.io/pods/configuration/use-ssh)):
Paste Container Start Command to support SSH ([RunPod Docs](https://docs.runpod.io/pods/configuration/use-ssh)):
```bash
bash -c "apt update;DEBIAN_FRONTEND=noninteractive apt-get install openssh-server -y;mkdir -p ~/.ssh;cd $_;chmod 700 ~/.ssh;echo \"$PUBLIC_KEY\" >> authorized_keys;chmod 700 authorized_keys;service ssh start;sleep infinity"
```
![RunPod template configuration](../../assets/images/runpod_template.png)
## Step 3: Set Up FastVideo
After SSH'ing in, the `uv` virtual environment at `/opt/venv` is already activated (configured in `.bashrc` and `.profile`). You land in the `/FastVideo` directory.
**Clone or pull the repo**
If the pod already has the FastVideo repo:
```bash
cd /FastVideo
git pull
```
If starting from a blank pod:
```bash
git clone https://github.com/hao-ai-lab/FastVideo.git /FastVideo
cd /FastVideo
```
**Install the package**
```bash
uv pip install -e .[dev]
```
The Docker image already includes Flash Attention and most heavy dependencies, so this is fast.
**Build the custom kernels (optional)**
VSA and STA attention kernels aren't in the Docker image by default. Build them if you're working on attention backends or need maximum inference performance:
```bash
cd /FastVideo/fastvideo-kernel
./build.sh
```
The build script detects your GPU architecture automatically. An A100 or H100 takes about 5-10 minutes.
**Verify the setup**
```bash
cd /FastVideo
python -c "import fastvideo; print('OK')"
pytest tests/ -q --no-header
```
## Development Workflow
### Editing code on RunPod
Two common approaches:
**Option A: Edit on RunPod directly**
Use VS Code Remote SSH or `vim`/`nano` on the pod. Commit and push when you're ready:
```bash
cd /FastVideo
git add .
git commit -m "your change"
git push
```
**Option B: Edit locally, sync to RunPod**
Work in your local repo, then pull on the pod:
```bash
# On RunPod:
cd /FastVideo
git pull
```
This keeps your local tools (editor, linters) intact while running GPU workloads on the pod.
### Running linters and tests
```bash
# Lint
pre-commit run --all-files
# Full test suite
pytest tests/
# Just package tests
pytest fastvideo/tests/ -v
```
### Storing models
If you attached a persistent volume, point your model downloads there:
```bash
export HF_HOME=/models/huggingface
export TRANSFORMERS_CACHE=/models/huggingface
```
Add these to `/root/.bashrc` so they persist across SSH sessions. The volume survives pod termination, so you only download models once.
### Terminating the pod
When you're done, push any commits you want to keep. RunPod does not save pod storage after termination.
Go to your RunPod dashboard, click "Terminate", then "Delete". A pod that's stopped but not deleted still charges you for storage. Fully delete it to stop all charges.
## GPU Recommendations
| GPU | VRAM | Good for |
|-----|------|----------|
| RTX 4090 | 24 GB | Inference testing, small model fine-tuning, quick iteration |
| A40 | 48 GB | Mid-size training runs, 480p video generation |
| A100 (40 GB) | 40 GB | Multi-GPU inference, training with sequence parallelism |
| A100 (80 GB) | 80 GB | Large model training, 720p+ video generation |
| H100 | 80 GB | Heavy training, benchmarking, kernel development |
For most development work, a single RTX 4090 or A40 is sufficient and cost-effective. Use A100/H100 when you need to reproduce training results at scale or test multi-GPU features.
-218
View File
@@ -1,218 +0,0 @@
# Contributing via Pull Requests
This guide walks through the PR workflow: title format, labels, CI pipeline, and getting
your changes merged.
---
## PR Title Format (Required)
Every PR targeting `main` must start with a type tag in square brackets. This is checked by
Mergify before any merge is allowed.
**Format:**
```
[type] Short description of the change
```
**Valid type tags:**
| Tag | When to use |
|-----|-------------|
| `[feat]` or `[feature]` | New feature or capability |
| `[bugfix]` or `[fix]` | Bug fix |
| `[refactor]` | Code restructuring with no behavior change |
| `[perf]` | Performance improvement |
| `[ci]` | CI/CD or build tooling changes |
| `[doc]` or `[docs]` | Documentation only |
| `[misc]` or `[chore]` | Housekeeping, dependency bumps, minor cleanup |
| `[kernel]` | CUDA kernel changes in `fastvideo-kernel/` |
| `[new-model]` | Adding a new model or pipeline |
**Examples:**
```
[feat] Add causal Wan 2.2 I2V pipeline
[bugfix] Fix VAE temporal tiling corruption on H100
[refactor] Restructure distributed attention dispatch
[docs] Add LoRA finetuning guide
[new-model] Port HunyuanVideo 1.5 to FastVideo
```
If your title is missing the tag, Mergify will post a comment listing the valid formats.
Update the title and the check will re-evaluate automatically.
---
## Labels
Labels are applied automatically based on your PR title and the files you changed. You don't
need to set them manually.
**Type label** — set from the `[tag]` in your title:
`type: feat`, `type: bugfix`, `type: refactor`, `type: perf`, `type: ci`, `type: docs`,
`type: misc`, `type: new-model`
**Scope labels** — set from which files you modified (multiple labels can apply):
`scope: training`, `scope: inference`, `scope: attention`, `scope: kernel`, `scope: data`,
`scope: infra`, `scope: distributed`, `scope: docs`, `scope: ui`, `scope: model`
**Process labels** — set during review and merge:
| Label | Who sets it | Meaning |
|-------|-------------|---------|
| `ready` | You (`/merge` comment) or a maintainer | Triggers Full Suite and enables auto-merge |
| `needs-rebase` | Mergify (automatic) | Your PR has conflicts; rebase against `main` |
| `do-not-merge` | Maintainer | Blocks merge regardless of CI status |
---
## CI Pipeline
Three tiers run automatically on every PR.
**Tier 1: Pre-commit (~2 min) — runs on every push**
GitHub Actions checks formatting, linting, type correctness, and spelling using pre-commit
hooks: yapf, ruff, mypy, codespell, pymarkdown, actionlint, and check-filenames.
**Tier 2: Fastcheck (~10-20 min) — runs on every push, path-filtered**
Buildkite runs GPU tests only for the components you changed. If you only modified
`fastvideo/models/vaes/`, only VAE Tests run. Tests run in parallel.
**Tier 3: Full Suite (~60-90 min) — triggered by the `ready` label**
When you comment `/merge` (or a maintainer adds the `ready` label), Buildkite runs the
complete test suite on your PR branch: SSIM regression, LoRA inference and training,
distillation, self-forcing, VSA, VMoBA, performance benchmarks, and API server tests.
---
## Getting Your PR Merged
**Step-by-step:**
1. Open a PR with a title that starts with a valid `[type]` tag.
2. Push your changes. Pre-commit and Fastcheck run automatically.
3. Fix any pre-commit failures locally (`pre-commit run --all-files`) and push again.
4. Wait for at least one approving review.
5. Once approved and pre-commit is green, comment `/merge` on the PR.
6. The `ready` label is added, which triggers the Full Suite on your PR branch.
7. Mergify also auto-rebases your branch against `main` if it is behind and conflict-free.
8. If all Full Suite tests pass and all merge conditions are met (approval, valid title,
pre-commit green, fastcheck green, no draft, no conflicts), Mergify squash-merges to
`main` automatically. Your branch is deleted.
9. If a Full Suite test fails, Mergify removes the `ready` label and posts a comment with a
link to the Buildkite build. Fix the issue, push, and comment `/merge` again.
!!! note
Only contributors with write permission to the repository can trigger slash commands.
If you're an external contributor, ask a maintainer to run `/merge` or add the `ready`
label for you.
---
## Running Tests On Demand
Comment on your PR to trigger specific tests independently of the auto-merge flow.
**Trigger the entire Full Suite:**
```
/test full
```
**Trigger the Fastcheck suite:**
```
/test fastcheck
```
**Trigger individual tests:**
```
/test encoder # Encoder component tests
/test vae # VAE component tests
/test transformer # Transformer / DiT tests
/test kernel # CUDA kernel tests
/test unit # Unit tests
/test ssim # SSIM regression tests
/test training # Training pipeline tests
/test lora-inference # LoRA inference tests
/test lora-training # LoRA training tests
/test distillation # DMD distillation tests
/test self-forcing # Self-Forcing distillation tests
/test vsa # VSA training tests
/test vmoba # VMoBA inference tests
/test performance # Performance benchmarks
/test api # API server integration tests
```
The workflow reacts with a 🚀 emoji to confirm the command was received.
---
## Troubleshooting
### Pre-commit fails
Run locally to reproduce and auto-fix:
```bash
# Install pre-commit if needed
uv pip install pre-commit
pre-commit install
# Run all checks on all files
pre-commit run --all-files
```
Common quick fixes:
- **yapf**: `yapf -i <file>` (Python formatting)
- **ruff**: `ruff check --fix <file>` (linting)
- **codespell**: `codespell --write-changes <file>` (spelling)
### PR title format check fails
Update your title to start with a valid type tag. The Mergify merge protection check
re-evaluates automatically after you save the title.
Valid tags: `feat`, `feature`, `bugfix`, `fix`, `refactor`, `perf`, `ci`, `doc`, `docs`,
`misc`, `chore`, `kernel`, `new-model`
### My PR has merge conflicts (`needs-rebase` label)
Rebase against `main` and force-push:
```bash
git fetch origin main
git rebase origin/main
# Resolve any conflicts, then:
git push --force-with-lease
```
Mergify removes the `needs-rebase` label automatically once conflicts are resolved.
### Full Suite failed after `/merge`
The Full Suite found a regression. Mergify removes the `ready` label and posts a comment
linking to the Buildkite build. Check the failing step's output for assertion errors or
tracebacks.
Common causes:
- Test failures caused by your code changes
- Missing dependency in `pyproject.toml`
- GPU memory issue (some tests require specific hardware like L40S or H100)
- Kernel build failure (if you changed `fastvideo-kernel/`)
After fixing, push and comment `/merge` again.
### I'm an external contributor without write permission
You can't use slash commands directly. After your PR is approved, ask a maintainer to
comment `/merge` or add the `ready` label.
+22 -109
View File
@@ -23,18 +23,13 @@ SSIM tests are located in `fastvideo/tests/ssim`. These tests generate videos us
```
fastvideo/tests/ssim/
├── reference_videos/
│ ├── default/
│ │ └── <GPU>_reference_videos/
│ │ ├── <Model_Name>/
│ │ │ ├── <Backend>/ # e.g., FLASH_ATTN, TORCH_SDPA
│ │ │ │ └── <Video_File>
│ └── full_quality/
│ └── <GPU>_reference_videos/
├── <GPU>_reference_videos/ # Reference videos organized by GPU type (e.g., L40S_reference_videos)
│ ├── <Model_Name>/
│ │ ├── <Backend>/ # e.g., FLASH_ATTN, TORCH_SDPA
│ │ │ └── <Video_File>
├── test_causal_similarity.py
├── test_wan_t2v_similarity.py
├── test_wan_i2v_similarity.py
├── reference_videos_cli.py
├── test_inference_similarity.py
├── update_reference_videos.sh
└── ...
```
@@ -42,7 +37,7 @@ fastvideo/tests/ssim/
To add a new SSIM test, follow these steps:
1. **Create or Update a Test File**: Prefer model-specific files (for example `test_wan_t2v_similarity.py`) and create a new one when testing a distinct model or pipeline.
1. **Create or Update a Test File**: You can add a new test function to an existing file (like `test_inference_similarity.py`) or create a new one if testing a distinct category of models.
2. **Define Model Parameters**: Define the configuration for the model you want to test. This includes model path, dimensions, inference steps, and other generation parameters. **Note:** Consider using lower `num_inference_steps` or reduced resolution (e.g., 480p instead of 720p) to keep test execution time reasonable, provided it doesn't compromise the test's ability to detect regression.
@@ -86,15 +81,10 @@ To add a new SSIM test, follow these steps:
```
4. **Reference Videos**:
* When running the test for the first time (or when updating the reference), the test will fail because the reference video is missing. The generated video will be saved in `fastvideo/tests/ssim/generated_videos/<quality-tier>/<GPU>_reference_videos`.
* When running the test for the first time (or when updating the reference), the test will fail because the reference video is missing. The generated video will be saved in `fastvideo/tests/ssim/generated_videos`.
* Inspect the generated video to ensure it meets quality expectations.
* Move the generated video to the appropriate quality/GPU reference folder:
`fastvideo/tests/ssim/reference_videos/<quality-tier>/<GPU>_reference_videos/<Model>/<Backend>/`.
* You can use the helper CLI to copy generated videos into a reference folder:
`python fastvideo/tests/ssim/reference_videos_cli.py copy-local --quality-tier default --reference-dir fastvideo/tests/ssim/reference_videos/default/L40S_reference_videos`
* Upload/download can target both quality tiers and specific GPU folders:
`python fastvideo/tests/ssim/reference_videos_cli.py upload --quality-tier all`
`python fastvideo/tests/ssim/reference_videos_cli.py download --quality-tier full_quality --device-folder H200_reference_videos`
* Move the generated video to the appropriate reference folder: `fastvideo/tests/ssim/<GPU>_reference_videos/<Model>/<Backend>/`.
* You can use the helper script `update_reference_videos.sh` to automate copying videos from `generated_videos` to `L40S_reference_videos`. Note: Check the script to ensure paths match your environment (it defaults to `L40S_reference_videos`).
### Running Tests Locally
@@ -106,113 +96,36 @@ pytest fastvideo/tests/ssim/ -vs
Ensure you have the necessary GPUs available as defined in your test parameters.
## CI Integration
## Modal Workflow
FastVideo uses [Modal](https://modal.com/) for running tests in a CI environment. The
workflow scripts are located in `fastvideo/tests/modal/`.
### Buildkite Pipeline
Tests are orchestrated by Buildkite (`.buildkite/pipeline.yml`) and executed on Modal GPU
instances. The pipeline runs in two modes:
**Fastcheck** — runs on every PR push, path-filtered. Only tests for the components you
changed are triggered. Tests run in parallel.
**Full Suite** — runs when a PR enters the Merge Queue (or when triggered manually via
`/test full`). Covers SSIM regression, training, distillation, inference, and performance.
FastVideo uses [Modal](https://modal.com/) for running tests in a CI environment. The workflow scripts are located in `fastvideo/tests/modal/`.
### `pr_test.py`
The main entry point for CI tests is `fastvideo/tests/modal/pr_test.py`. This script defines
Modal functions that execute the pytest suites on specific hardware (e.g., L40S, H100).
The main entry point for CI tests is `fastvideo/tests/modal/pr_test.py`. This script defines Modal functions that execute the pytest suites on specific hardware (e.g., L40S, H100).
### Updating Modal Configuration
If you add a new test that requires:
* **Different GPU Hardware**: You may need to change the `@app.function(gpu=...)` decorator.
* **Longer Execution Time**: Increase the `timeout` parameter.
* **New Environment Variables/Secrets**: Add them to `secrets=[...]` or the image
environment. For example, if your model is gated on Hugging Face, ensure `HF_API_KEY`
is passed.
* **New Environment Variables/Secrets**: Add them to `secrets=[...]` or the image environment. For example, if your model is gated on Hugging Face, ensure `HF_API_KEY` is passed.
For SSIM tests, use `fastvideo/tests/modal/ssim_test.py`:
For SSIM tests, the `run_ssim_tests` function in `pr_test.py` currently runs:
```bash
python -m modal run fastvideo/tests/modal/ssim_test.py::run_ssim_tests
```python
@app.function(gpu="L40S:2", image=image, timeout=2700, secrets=[modal.Secret.from_dict({"HF_API_KEY": os.environ.get("HF_API_KEY", "")})])
def run_ssim_tests():
run_test("hf auth login --token $HF_API_KEY && pytest ./fastvideo/tests/ssim -vs")
```
Target specific SSIM files/models:
```bash
python -m modal run fastvideo/tests/modal/ssim_test.py::run_ssim_tests \
--test-files test_wan_t2v_similarity.py \
--model-ids Wan2.1-T2V-1.3B-Diffusers
```
If HF token env vars are not set (`HF_API_KEY` / `HUGGINGFACE_HUB_TOKEN` /
`HF_TOKEN`), the local entrypoint fails fast. To export raw `generated_videos`
from Modal to the shared volume:
```bash
python -m modal run fastvideo/tests/modal/ssim_test.py::run_ssim_tests \
--sync-generated-to-volume
```
The raw export path is quality-tiered:
* default params: `ssim_generated_videos/default/<subdir>/generated_videos`
* full-quality params: `ssim_generated_videos/full_quality/<subdir>/generated_videos`
The printed `modal volume get` command also downloads into a quality-specific
local directory under `./generated_videos_modal/<quality-tier>`.
To turn downloaded Modal outputs into local reference videos, use the matching
quality tier with `copy-local`, for example:
```bash
python fastvideo/tests/ssim/reference_videos_cli.py copy-local \
--quality-tier full_quality \
--generated-dir ./generated_videos_modal/full_quality/L40S_reference_videos \
--device-folder L40S_reference_videos
```
If your new test file is inside `fastvideo/tests/ssim`, it will automatically be picked up by this command. However, ensure that the `gpu="L40S:2"` configuration is sufficient for your model. If your model requires more GPUs (e.g., 4 or 8), you might need to create a separate Modal function or update the existing one.
### Workflow Scripts
The shell script that triggers tests in CI is `.buildkite/scripts/pr_test.sh`. If you add
a new test category (e.g., a new folder outside of `ssim`), you will need to:
The shell script that triggers these tests in the CI pipeline is located at `.buildkite/scripts/pr_test.sh`. If you add a new test category (e.g., a new folder outside of `ssim`), you will need to:
1. Add a new function in `fastvideo/tests/modal/pr_test.py`.
2. Add a new case in `.buildkite/scripts/pr_test.sh` to handle the new test type.
!!! note
If you are a maintainer, update the workflow script in Buildkite after merging. Otherwise,
ask a maintainer for help.
## Triggering Tests via Slash Commands
Maintainers and contributors with write access can trigger individual test suites directly
from a PR comment. The workflow reacts with a 🚀 emoji to confirm the command was received.
```
/test ssim # SSIM regression tests
/test training # Training pipeline tests
/test lora-training # LoRA training tests
/test lora-inference # LoRA inference tests
/test distillation # DMD distillation tests
/test self-forcing # Self-Forcing tests
/test vsa # VSA training tests
/test vmoba # VMoBA inference tests
/test performance # Performance benchmarks
/test api # API server integration tests
/test encoder # Encoder component tests (Fastcheck)
/test vae # VAE component tests (Fastcheck)
/test transformer # Transformer / DiT tests (Fastcheck)
/test kernel # CUDA kernel tests (Fastcheck)
/test unit # Unit tests (Fastcheck)
/test full # Entire Full Suite
/test fastcheck # Entire Fastcheck suite
```
See [CI Architecture](ci_architecture.md) for the complete reference.
If you are a maintainer, you'll need to finally manually update the workflow script in Buildkite. Otherwise, a maintainer will help you update.
-93
View File
@@ -1,93 +0,0 @@
# Training
## Quick Start
### Single-node
```bash
bash examples/train/run.sh <config.yaml> [--dotted.key value ...]
```
The script auto-detects available GPUs. Override with `NUM_GPUS`:
```bash
NUM_GPUS=4 bash examples/train/run.sh \
examples/train/configs/fine_tuning/wan/t2v.yaml
```
### Multi-node (Slurm)
```bash
bash examples/train/run_slurm.sh <config.yaml> <num_nodes> [--dotted.key value ...]
```
```bash
bash examples/train/run_slurm.sh \
examples/train/configs/fine_tuning/wan/t2v.yaml 4 \
--training.distributed.hsdp_shard_dim 8 \
--training.distributed.hsdp_replicate_dim 4
```
Slurm environment variables:
| Variable | Default | Description |
|---|---|---|
| `PARTITION` | `main` | Slurm partition |
| `NUM_GPUS` | `8` | GPUs per node |
| `CPUS_PER_TASK` | `128` | CPUs per task |
| `MEM` | `1440G` | Memory per node |
| `EXCLUDE` | `""` | Nodes to exclude |
### Overriding config values
Any config field can be overridden from the command line using dotted keys:
```bash
bash examples/train/run.sh examples/train/configs/fine_tuning/wan/t2v.yaml \
--training.optimizer.learning_rate 1e-5 \
--training.loop.max_train_steps 1000 \
--training.checkpoint.output_dir outputs/my_experiment
```
### Resuming from a checkpoint
```bash
bash examples/train/run.sh examples/train/configs/fine_tuning/wan/t2v.yaml \
--training.checkpoint.resume_from_checkpoint outputs/my_experiment/checkpoint-500
```
## W&B Logging
Training metrics and validation videos are logged to
[Weights & Biases](https://wandb.ai). Set your API key before launching:
```bash
export WANDB_API_KEY=your_key_here
```
To disable W&B (e.g. for local debugging):
```bash
export WANDB_MODE=offline
```
The project name and run name are set in the config under `training.tracker`:
```yaml
training:
tracker:
project_name: my_project
run_name: my_run
```
## Directory Layout
```
examples/train/
├── configs/ # Single-step training configs (by method and model)
├── scenario/ # Multi-step end-to-end training pipelines
├── run.sh # Single-node launcher
└── run_slurm.sh # Multi-node Slurm launcher
```
See `configs/README.md` and `scenario/README.md` for details.
-21
View File
@@ -1,21 +0,0 @@
# Training Configs
Single-step training configurations organized by method and model.
```
configs/
├── fine_tuning/ # Standard finetuning and DFSFT
├── distribution_matching/ # DMD2 and Self-Forcing
├── knowledge_distillation/ # KD from teacher to student
└── example.yaml # Annotated reference config with all fields
```
Each method directory contains per-model subdirectories (e.g. `wan/`, `hunyuan/`).
Launch any config with:
```bash
bash examples/train/run.sh examples/train/configs/<method>/<model>/<config>.yaml
```
For multi-step training pipelines, see `examples/train/scenario/`.
@@ -1,72 +0,0 @@
# HunyuanVideo T2V finetune config.
#
# Data must be preprocessed with Hunyuan VAE + dual text encoders
# (LLaMA + CLIP) into parquet format before training.
# See scripts/preprocess/ for preprocessing pipeline.
models:
student:
_target_: fastvideo.train.models.hunyuan.HunyuanModel
init_from: hunyuanvideo-community/HunyuanVideo
trainable: true
method:
_target_: fastvideo.train.methods.fine_tuning.finetune.FineTuneMethod
training:
distributed:
num_gpus: 8
sp_size: 1
tp_size: 1
hsdp_replicate_dim: 1
hsdp_shard_dim: 8
data:
data_path: data/hunyuan_training_data
dataloader_num_workers: 4
train_batch_size: 1
training_cfg_rate: 0.0
seed: 1000
num_latent_t: 32
num_height: 720
num_width: 1280
num_frames: 125
optimizer:
learning_rate: 1.0e-5
betas: [0.9, 0.999]
weight_decay: 0.01
lr_scheduler: constant
lr_warmup_steps: 0
loop:
max_train_steps: 5000
gradient_accumulation_steps: 1
checkpoint:
output_dir: outputs/hunyuan_finetune
training_state_checkpointing_steps: 500
checkpoints_total_limit: 3
resume_from_checkpoint: latest
tracker:
project_name: fastvideo_hunyuan
run_name: hunyuan_finetune
model:
enable_gradient_checkpointing_type: full
callbacks:
grad_clip:
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback
max_grad_norm: 1.0
validation:
_target_: fastvideo.train.callbacks.validation.ValidationCallback
pipeline_target: fastvideo.pipelines.basic.hunyuan.hunyuan_pipeline.HunyuanVideoPipeline
dataset_file: data/hunyuan_training_data/validation_prompts.json
every_steps: 100
sampling_steps: [50]
guidance_scale: 1.0
pipeline:
flow_shift: 7
@@ -1,68 +0,0 @@
# Wan 2.1 T2V 1.3B finetune.
models:
student:
_target_: fastvideo.train.models.wan.WanModel
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
trainable: true
method:
_target_: fastvideo.train.methods.fine_tuning.finetune.FineTuneMethod
training:
distributed:
num_gpus: 8
sp_size: 1
tp_size: 1
hsdp_replicate_dim: 8
hsdp_shard_dim: 1
data:
data_path: data/preprocessed
dataloader_num_workers: 4
train_batch_size: 1
training_cfg_rate: 0.0
seed: 1000
num_latent_t: 20
num_height: 448
num_width: 832
num_frames: 77
optimizer:
learning_rate: 1.0e-6
betas: [0.9, 0.999]
weight_decay: 0.01
lr_scheduler: constant
lr_warmup_steps: 0
loop:
max_train_steps: 4000
gradient_accumulation_steps: 1
checkpoint:
output_dir: outputs/wan_finetune
training_state_checkpointing_steps: 100
checkpoints_total_limit: 3
resume_from_checkpoint: latest
tracker:
project_name: fastvideo
run_name: wan_finetune
model:
enable_gradient_checkpointing_type: full
callbacks:
grad_clip:
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback
max_grad_norm: 1.0
validation:
_target_: fastvideo.train.callbacks.validation.ValidationCallback
pipeline_target: fastvideo.pipelines.basic.wan.wan_pipeline.WanPipeline
dataset_file: data/validation_prompts.json
every_steps: 100
sampling_steps: [50]
guidance_scale: 5.0
pipeline:
flow_shift: 3
@@ -4,7 +4,7 @@ Uses fastvideo's AutoencoderKLWan and its built-in denormalization
(latent * std + mean, matching normalize_dit_input's inverse).
Usage:
python examples/train/scenario/ode_init_self_forcing_wan_causal/decode_kd_cache.py \
python examples/train/configs/kdtest/decode_kd_cache.py \
--cache_dir data/kd_test_cache_small \
--out_dir data/kd_test_cache_small/video \
--fps 16
@@ -5,7 +5,7 @@
#
# Step 1 — Run KD training:
# torchrun --nproc_per_node=1 -m fastvideo.train.entrypoint.train \
# --config examples/train/scenario/ode_init_self_forcing_wan_causal/step1_kd.yaml
# --config examples/train/configs/kdtest/kd_causal.yaml
#
# Step 2 — Export ode_init checkpoint:
# python -m fastvideo.train.entrypoint.dcp_to_diffusers \
@@ -15,7 +15,7 @@
#
# Step 3 — Run Self-Forcing with the exported ode_init:
# torchrun --nproc_per_node=1 -m fastvideo.train.entrypoint.train \
# --config examples/train/scenario/ode_init_self_forcing_wan_causal/step3_self_forcing.yaml
# --config examples/train/configs/kdtest/self_forcing_causal.yaml
#
# Success criteria:
# - Cache generates 16 .pt files under data/kd_test_cache/samples/
@@ -1,7 +1,7 @@
# Self-Forcing Test: Causal Wan 1.3B
#
# Smoke-test for SelfForcingMethod on a tiny 16-sample dataset.
# Run this AFTER step1_kd.yaml + dcp_to_diffusers export.
# Run this AFTER kd_causal.yaml + dcp_to_diffusers export.
#
# Prerequisite — export ode_init from the KD run:
# python -m fastvideo.train.entrypoint.dcp_to_diffusers \
@@ -11,7 +11,7 @@
#
# Then run this config:
# torchrun --nproc_per_node=1 -m fastvideo.train.entrypoint.train \
# --config examples/train/scenario/ode_init_self_forcing_wan_causal/step3_self_forcing.yaml
# --config examples/train/configs/kdtest/self_forcing_causal.yaml
#
# Success criteria:
# - generator_loss and critic_loss both finite at step 0
@@ -100,7 +100,7 @@ callbacks:
max_grad_norm: 1.0
validation:
pipeline_target: fastvideo.pipelines.basic.wan.wan_causal_dmd_pipeline.WanCausalDMDPipeline
dataset_file: examples/train/scenario/ode_init_self_forcing_wan_causal/validation.json
dataset_file: examples/train/configs/kdtest/validation.json
every_steps: 20
sampling_steps: [4]
sampling_timesteps: [1000, 750, 500, 250]
+8
View File
@@ -0,0 +1,8 @@
# Step 1 — KD training
bash examples/train/run.sh examples/train/configs/kdtest/kd_causal.yaml
# Step 2 — Export ode_init
python -m fastvideo.train.entrypoint.dcp_to_diffusers --role student --checkpoint outputs/kdtest/kd_causal/checkpoint-300 --output-dir outputs/kdtest/kd_ode_init
# Step 3 — Self-Forcing
bash examples/train/run.sh examples/train/configs/kdtest/self_forcing_causal.yaml
@@ -1,78 +0,0 @@
# HunyuanVideo T2V overfitting test config.
#
# Overfits on 3 short videos (480x832, 77 frames) to verify the
# training pipeline works end-to-end.
#
# Preprocess data first:
# CUDA_VISIBLE_DEVICES=0 python scripts/preprocess_hunyuan_overfit.py
#
# Run:
# bash examples/train/run.sh examples/train/configs/overfit_hunyuan_t2v.yaml
models:
student:
_target_: fastvideo.train.models.hunyuan.HunyuanModel
init_from: hunyuanvideo-community/HunyuanVideo
trainable: true
enable_gradient_checkpointing_type: full
method:
_target_: fastvideo.train.methods.fine_tuning.finetune.FineTuneMethod
training:
distributed:
num_gpus: 8
sp_size: 1
tp_size: 1
hsdp_replicate_dim: 1
hsdp_shard_dim: 8
data:
data_path: data/hunyuan_overfit_preprocessed
dataloader_num_workers: 0
train_batch_size: 1
training_cfg_rate: 0.0
seed: 42
num_latent_t: 20
num_height: 480
num_width: 832
num_frames: 77
optimizer:
learning_rate: 5.0e-5
betas: [0.9, 0.999]
weight_decay: 0.0
lr_scheduler: constant
lr_warmup_steps: 0
loop:
max_train_steps: 300
gradient_accumulation_steps: 1
checkpoint:
output_dir: outputs/hunyuan_overfit
training_state_checkpointing_steps: 50
checkpoints_total_limit: 2
tracker:
project_name: fastvideo_hunyuan
run_name: hunyuan_overfit
model:
precondition_outputs: true
enable_gradient_checkpointing_type: full
callbacks:
grad_clip:
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback
max_grad_norm: 1.0
validation:
_target_: fastvideo.train.callbacks.validation.ValidationCallback
pipeline_target: fastvideo.pipelines.basic.hunyuan.hunyuan_pipeline.HunyuanVideoPipeline
dataset_file: data/hunyuan_overfit_preprocessed/validation_prompts.json
every_steps: 50
sampling_steps: [50]
guidance_scale: 1.0
pipeline:
flow_shift: 7
+4 -4
View File
@@ -5,12 +5,12 @@
# bash examples/train/run.sh <config.yaml> [--dotted.key value ...]
#
# Examples:
# bash examples/train/run.sh examples/train/configs/fine_tuning/wan/t2v.yaml
# bash examples/train/run.sh examples/train/configs/distribution_matching/wan/dmd2_t2v.yaml --dry-run
# bash examples/train/run.sh examples/train/configs/distribution_matching/wan/dmd2_t2v.yaml \
# bash examples/train/run.sh examples/train/finetune_wan2.1_t2v_1.3B_vsa_phase3.4_0.9sparsity.yaml
# bash examples/train/run.sh examples/train/distill_wan2.1_t2v_1.3B_dmd2.yaml --dry-run
# bash examples/train/run.sh examples/train/distill_wan2.1_t2v_1.3B_dmd2.yaml \
# --training.distributed.num_gpus 4 \
# --training.optimizer.learning_rate 1e-5
# bash examples/train/run.sh examples/train/configs/distribution_matching/wan/dmd2_t2v.yaml \
# bash examples/train/run.sh examples/train/distill_wan2.1_t2v_1.3B_dmd2.yaml \
# --training.checkpoint.resume_from_checkpoint outputs/my_run/checkpoint-1000
#
# Logs are written to logs/<config_name>_<timestamp>.log (and also printed to stdout).
+1 -1
View File
@@ -6,7 +6,7 @@
#
# Examples:
# bash examples/train/run_slurm.sh examples/train/configs/example.yaml 2
# bash examples/train/run_slurm.sh examples/train/configs/distribution_matching/wan/dmd2_t2v.yaml 4 \
# bash examples/train/run_slurm.sh examples/train/configs/distill_wan2.1_t2v_1.3B_dmd2.yaml 4 \
# --training.optimizer.learning_rate 1e-5
# bash examples/train/run_slurm.sh examples/train/configs/example.yaml 8 \
# --training.checkpoint.resume_from_checkpoint outputs/my_run/checkpoint-1000
-13
View File
@@ -1,13 +0,0 @@
# Training Scenarios
End-to-end multi-step training pipelines. Each scenario directory contains
all configs, scripts, and data needed to run a complete workflow.
```
scenario/
└── ode_init_self_forcing_wan_causal/ # KD → export → Self-Forcing
```
See the `usage.md` inside each scenario for step-by-step instructions.
For single-step configs, see `examples/train/configs/`.
@@ -1,21 +0,0 @@
#!/usr/bin/env bash
# Step 2 — Export ODE-init weights from KD checkpoint to diffusers format.
#
# Usage:
# bash examples/train/scenario/ode_init_self_forcing_wan_causal/step2_export.sh
set -euo pipefail
CHECKPOINT_DIR="${1:-outputs/kdtest/kd_causal/checkpoint-300}"
OUTPUT_DIR="${2:-outputs/kdtest/kd_ode_init}"
echo "Exporting ODE-init weights..."
echo " checkpoint: ${CHECKPOINT_DIR}"
echo " output: ${OUTPUT_DIR}"
python -m fastvideo.train.entrypoint.dcp_to_diffusers \
--role student \
--checkpoint "${CHECKPOINT_DIR}" \
--output-dir "${OUTPUT_DIR}"
echo "Done. Use the exported weights in step3_self_forcing.yaml."
@@ -1,19 +0,0 @@
# ODE-Init Self-Forcing: Wan 2.1 Causal 1.3B
End-to-end scenario that trains a causal Wan 1.3B model via
ODE-init knowledge distillation followed by self-forcing.
## Steps
```bash
# Step 1 — KD training (produces ODE-init checkpoint)
bash examples/train/run.sh \
examples/train/scenario/ode_init_self_forcing_wan_causal/step1_kd.yaml
# Step 2 — Export ODE-init weights to diffusers format
bash examples/train/scenario/ode_init_self_forcing_wan_causal/step2_export.sh
# Step 3 — Self-forcing with the exported ODE-init
bash examples/train/run.sh \
examples/train/scenario/ode_init_self_forcing_wan_causal/step3_self_forcing.yaml
```
+1 -2
View File
@@ -7,9 +7,8 @@ from fastvideo.configs.models.dits.longcat import LongCatVideoConfig
from fastvideo.configs.models.dits.ltx2 import LTX2VideoConfig
from fastvideo.configs.models.dits.wanvideo import WanVideoConfig
from fastvideo.configs.models.dits.hyworld import HYWorldConfig
from fastvideo.configs.models.dits.kandinsky5 import Kandinsky5VideoConfig
__all__ = [
"HunyuanVideoConfig", "HunyuanVideo15Config", "HunyuanGameCraftConfig", "WanVideoConfig", "CosmosVideoConfig",
"Cosmos25VideoConfig", "LongCatVideoConfig", "LTX2VideoConfig", "HYWorldConfig", "Kandinsky5VideoConfig"
"Cosmos25VideoConfig", "LongCatVideoConfig", "LTX2VideoConfig", "HYWorldConfig"
]
@@ -1,65 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass, field
from fastvideo.configs.models.dits.base import DiTArchConfig, DiTConfig
@dataclass
class Kandinsky5ArchConfig(DiTArchConfig):
_fsdp_shard_conditions: list = field(default_factory=lambda: [
lambda n, m:
("text_transformer_blocks" in n or "visual_transformer_blocks" in n) and n.split(".")[-1].isdigit()
])
# Native FastVideo implementation uses the same parameter names as diffusers
# except FFN internals: Diffusers FFN uses `in_layer/out_layer`, while
# FastVideo uses MLP `fc_in/fc_out`.
param_names_mapping: dict = field(
default_factory=lambda: {
r"^(.*feed_forward)\.in_layer\.(weight|bias)$": r"\1.mlp.fc_in.\2",
r"^(.*feed_forward)\.out_layer\.(weight|bias)$": r"\1.mlp.fc_out.\2",
})
reverse_param_names_mapping: dict = field(default_factory=lambda: {})
# Diffusers Kandinsky5Transformer3DModel config fields.
in_visual_dim: int = 4
in_text_dim: int = 3584
in_text_dim2: int = 768
time_dim: int = 512
out_visual_dim: int = 4
patch_size: tuple[int, int, int] = (1, 2, 2)
model_dim: int = 2048
ff_dim: int = 5120
num_text_blocks: int = 2
num_visual_blocks: int = 32
axes_dims: tuple[int, int, int] = (16, 24, 24)
visual_cond: bool = False
attention_type: str = "regular"
attention_causal: bool | None = None
attention_local: bool | None = None
attention_glob: bool | None = None
attention_window: int | None = None
attention_P: float | None = None
attention_wT: int | None = None
attention_wW: int | None = None
attention_wH: int | None = None
attention_add_sta: bool | None = None
attention_method: str | None = None
def __post_init__(self):
super().__post_init__()
head_dim = sum(self.axes_dims)
if self.model_dim % head_dim != 0:
raise ValueError(f"model_dim ({self.model_dim}) must be divisible by head_dim ({head_dim})")
self.hidden_size = self.model_dim
self.num_attention_heads = self.model_dim // head_dim
self.in_channels = self.in_visual_dim
self.out_channels = self.out_visual_dim
self.num_channels_latents = self.in_visual_dim
@dataclass
class Kandinsky5VideoConfig(DiTConfig):
arch_config: DiTArchConfig = field(default_factory=Kandinsky5ArchConfig)
prefix: str = "Kandinsky5"
@@ -72,12 +72,9 @@ def i2v_record_creator(batch: PreprocessBatch) -> list[dict[str, Any]]:
"""Create a record for the Parquet dataset with CLIP features."""
records = basic_t2v_record_creator(batch)
if len(batch.image_embeds) > 0:
assert len(batch.image_embeds
) == 1, "image embedding should be a single tensor"
image_embeds = batch.image_embeds[0]
else:
image_embeds = None
assert len(
batch.image_embeds) == 1, "image embedding should be a single tensor"
image_embeds = batch.image_embeds[0]
image_latent = batch.image_latent
pil_image = batch.pil_image
-32
View File
@@ -4,7 +4,6 @@ import os
import pathlib
import datasets
import numpy as np
from torch.utils.data import IterableDataset
from fastvideo.distributed import (get_sp_world_size, get_world_rank,
@@ -161,36 +160,5 @@ class ValidationDataset(IterableDataset):
else:
sample["control_video"] = load_video(control_video_path)
if sample.get("action_path", None) is not None:
action_path = sample["action_path"]
action_path = os.path.join(self.dir, action_path)
sample["action_path"] = action_path
if not pathlib.Path(action_path).is_file():
logger.warning("Action file %s does not exist.",
action_path)
else:
action_data = np.load(action_path, allow_pickle=True)
num_frames = sample["num_frames"]
if isinstance(action_data,
np.ndarray) and action_data.dtype == object:
action_data = action_data.item()
if isinstance(action_data, dict):
kb_length = len(action_data["keyboard"])
ms_length = len(action_data["mouse"])
assert kb_length >= num_frames, (
f"keyboard length {kb_length} is smaller than "
f"num_frames {num_frames} for {action_path}.")
assert ms_length >= num_frames, (
f"mouse length {ms_length} is smaller than "
f"num_frames {num_frames} for {action_path}.")
sample["keyboard_cond"] = action_data["keyboard"][:num_frames]
sample["mouse_cond"] = action_data["mouse"][:num_frames]
else:
kb_length = len(action_data)
assert kb_length >= num_frames, (
f"keyboard length {kb_length} is smaller than "
f"num_frames {num_frames} for {action_path}.")
sample["keyboard_cond"] = action_data[:num_frames]
sample = {k: v for k, v in sample.items() if v is not None}
yield sample
-761
View File
@@ -1,761 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import math
from dataclasses import dataclass
from typing import Any
import torch
import torch.nn as nn
import torch.nn.functional as F
from fastvideo.attention import LocalAttention
from fastvideo.configs.models.dits import Kandinsky5VideoConfig
from fastvideo.layers.layernorm import LayerNormScaleShift
from fastvideo.layers.linear import ReplicatedLinear
from fastvideo.layers.mlp import MLP
from fastvideo.models.dits.base import BaseDiT
from fastvideo.platforms import AttentionBackendEnum
FRACTAL_PIXEL_SIZE = 8
_ARCH_CONFIG_DEFAULTS = Kandinsky5VideoConfig().arch_config
def _build_rotary_freqs(dim: int, max_period: float) -> torch.Tensor:
return torch.exp(-math.log(max_period) * torch.arange(
start=0, end=dim, dtype=torch.float32) / dim)
def local_patching(x: torch.Tensor, shape: tuple[int, int, int, int],
group_size: tuple[int, int, int],
dim: int = 0) -> torch.Tensor:
batch_size, duration, height, width = shape
g1, g2, g3 = group_size
x = x.reshape(
*x.shape[:dim],
duration // g1,
g1,
height // g2,
g2,
width // g3,
g3,
*x.shape[dim + 3:],
)
x = x.permute(
*range(len(x.shape[:dim])),
dim,
dim + 2,
dim + 4,
dim + 1,
dim + 3,
dim + 5,
*range(dim + 6, len(x.shape)),
)
x = x.flatten(dim, dim + 2).flatten(dim + 1, dim + 3)
return x
def local_merge(x: torch.Tensor, shape: tuple[int, int, int, int],
group_size: tuple[int, int, int],
dim: int = 0) -> torch.Tensor:
batch_size, duration, height, width = shape
g1, g2, g3 = group_size
x = x.reshape(
*x.shape[:dim],
duration // g1,
height // g2,
width // g3,
g1,
g2,
g3,
*x.shape[dim + 2:],
)
x = x.permute(
*range(len(x.shape[:dim])),
dim,
dim + 3,
dim + 1,
dim + 4,
dim + 2,
dim + 5,
*range(dim + 6, len(x.shape)),
)
x = x.flatten(dim, dim + 1).flatten(dim + 1, dim + 2).flatten(
dim + 2, dim + 3)
return x
def fractal_flatten(x: torch.Tensor,
rope: torch.Tensor,
shape: tuple[int, int, int, int],
block_mask: bool = False):
if block_mask:
pixel_size = FRACTAL_PIXEL_SIZE
x = local_patching(x, shape, (1, pixel_size, pixel_size), dim=1)
rope = local_patching(rope, shape, (1, pixel_size, pixel_size), dim=1)
x = x.flatten(1, 2)
rope = rope.flatten(1, 2)
else:
x = x.flatten(1, 3)
rope = rope.flatten(1, 3)
return x, rope
def fractal_unflatten(x: torch.Tensor,
shape: tuple[int, int, int, int],
block_mask: bool = False):
if block_mask:
pixel_size = FRACTAL_PIXEL_SIZE
x = x.reshape(x.shape[0], -1, pixel_size**2, *x.shape[2:])
x = local_merge(x, shape, (1, pixel_size, pixel_size), dim=1)
else:
x = x.reshape(*shape, *x.shape[2:])
return x
class Kandinsky5TimeEmbeddings(nn.Module):
def __init__(self, model_dim: int, time_dim: int,
max_period: float = 10000.0):
super().__init__()
assert model_dim % 2 == 0
self.model_dim = model_dim
self.max_period = max_period
self.freqs = _build_rotary_freqs(self.model_dim // 2, self.max_period)
self.in_layer = ReplicatedLinear(model_dim, time_dim, bias=True)
self.activation = nn.SiLU()
self.out_layer = ReplicatedLinear(time_dim, time_dim, bias=True)
@torch.autocast(device_type="cuda", dtype=torch.float32)
def forward(self, time: torch.Tensor) -> torch.Tensor:
args = torch.outer(time, self.freqs.to(device=time.device))
time_embed = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
time_embed, _ = self.in_layer(time_embed)
time_embed = self.activation(time_embed)
time_embed, _ = self.out_layer(time_embed)
return time_embed
class Kandinsky5TextEmbeddings(nn.Module):
def __init__(self, text_dim: int, model_dim: int):
super().__init__()
self.in_layer = ReplicatedLinear(text_dim, model_dim, bias=True)
self.norm = nn.LayerNorm(model_dim, elementwise_affine=True)
def forward(self, text_embed: torch.Tensor) -> torch.Tensor:
text_embed, _ = self.in_layer(text_embed)
return self.norm(text_embed).type_as(text_embed)
class Kandinsky5VisualEmbeddings(nn.Module):
def __init__(self, visual_dim: int, model_dim: int,
patch_size: tuple[int, int, int]):
super().__init__()
self.patch_size = patch_size
self.in_layer = ReplicatedLinear(math.prod(patch_size) * visual_dim,
model_dim)
def forward(self, x: torch.Tensor) -> torch.Tensor:
batch_size, duration, height, width, dim = x.shape
x = (x.view(
batch_size,
duration // self.patch_size[0],
self.patch_size[0],
height // self.patch_size[1],
self.patch_size[1],
width // self.patch_size[2],
self.patch_size[2],
dim,
).permute(0, 1, 3, 5, 2, 4, 6, 7).flatten(4, 7))
x, _ = self.in_layer(x)
return x
class Kandinsky5RoPE1D(nn.Module):
def __init__(self,
dim: int,
max_pos: int = 1024,
max_period: float = 10000.0):
super().__init__()
self.max_period = max_period
self.dim = dim
self.max_pos = max_pos
freq = _build_rotary_freqs(dim // 2, max_period)
pos = torch.arange(max_pos, dtype=freq.dtype)
self.register_buffer("args", torch.outer(pos, freq), persistent=False)
def forward(self, pos: torch.Tensor) -> torch.Tensor:
args = self.args[pos]
cosine = torch.cos(args)
sine = torch.sin(args)
rope = torch.stack([cosine, -sine, sine, cosine], dim=-1)
rope = rope.view(*rope.shape[:-1], 2, 2)
return rope.unsqueeze(-4)
class Kandinsky5RoPE3D(nn.Module):
def __init__(self,
axes_dims: tuple[int, int, int],
max_pos: tuple[int, int, int] = (128, 128, 128),
max_period: float = 10000.0):
super().__init__()
self.axes_dims = axes_dims
self.max_pos = max_pos
self.max_period = max_period
for i, (axes_dim, ax_max_pos) in enumerate(
zip(axes_dims, max_pos, strict=True)):
freq = _build_rotary_freqs(axes_dim // 2, max_period)
pos = torch.arange(ax_max_pos, dtype=freq.dtype)
self.register_buffer(f"args_{i}",
torch.outer(pos, freq),
persistent=False)
def forward(self,
shape: tuple[int, int, int, int],
pos: tuple[torch.Tensor, torch.Tensor, torch.Tensor]
| list[torch.Tensor],
scale_factor: tuple[float, float,
float] = (1.0, 1.0, 1.0)):
batch_size, duration, height, width = shape
args_t = self.args_0[pos[0]] / scale_factor[0]
args_h = self.args_1[pos[1]] / scale_factor[1]
args_w = self.args_2[pos[2]] / scale_factor[2]
args = torch.cat(
[
args_t.view(1, duration, 1, 1, -1).repeat(
batch_size, 1, height, width, 1),
args_h.view(1, 1, height, 1, -1).repeat(
batch_size, duration, 1, width, 1),
args_w.view(1, 1, 1, width, -1).repeat(
batch_size, duration, height, 1, 1),
],
dim=-1,
)
cosine = torch.cos(args)
sine = torch.sin(args)
rope = torch.stack([cosine, -sine, sine, cosine], dim=-1)
rope = rope.view(*rope.shape[:-1], 2, 2)
return rope.unsqueeze(-4)
class Kandinsky5Modulation(nn.Module):
def __init__(self, time_dim: int, model_dim: int, num_params: int):
super().__init__()
self.activation = nn.SiLU()
self.out_layer = ReplicatedLinear(time_dim,
num_params * model_dim,
bias=True)
self.out_layer.weight.data.zero_()
self.out_layer.bias.data.zero_()
@torch.autocast(device_type="cuda", dtype=torch.float32)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.activation(x)
x, _ = self.out_layer(x)
return x
def _apply_rotary(x: torch.Tensor, rope: torch.Tensor) -> torch.Tensor:
orig_dtype = x.dtype
x_ = x.reshape(*x.shape[:-1], -1, 1, 2).to(torch.float32)
x_out = (rope * x_).sum(dim=-1)
return x_out.reshape(*x.shape).to(orig_dtype)
class Kandinsky5Attention(nn.Module):
def __init__(
self,
num_channels: int,
head_dim: int,
supported_attention_backends: tuple[AttentionBackendEnum, ...] | None,
prefix: str = "",
):
super().__init__()
assert num_channels % head_dim == 0
self.num_heads = num_channels // head_dim
self.to_query = ReplicatedLinear(num_channels,
num_channels,
bias=True,
prefix=f"{prefix}.to_query")
self.to_key = ReplicatedLinear(num_channels,
num_channels,
bias=True,
prefix=f"{prefix}.to_key")
self.to_value = ReplicatedLinear(num_channels,
num_channels,
bias=True,
prefix=f"{prefix}.to_value")
self.query_norm = nn.RMSNorm(head_dim)
self.key_norm = nn.RMSNorm(head_dim)
self.out_layer = ReplicatedLinear(num_channels,
num_channels,
bias=True,
prefix=f"{prefix}.out_layer")
self.local_attention = LocalAttention(
num_heads=self.num_heads,
head_size=head_dim,
causal=False,
supported_attention_backends=supported_attention_backends,
)
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor | None = None,
sparse_params: dict[str, Any] | None = None,
rotary_emb: torch.Tensor | None = None,
) -> torch.Tensor:
query, _ = self.to_query(hidden_states)
if encoder_hidden_states is not None:
key, _ = self.to_key(encoder_hidden_states)
value, _ = self.to_value(encoder_hidden_states)
shape, cond_shape = query.shape[:-1], key.shape[:-1]
query = query.reshape(*shape, self.num_heads, -1)
key = key.reshape(*cond_shape, self.num_heads, -1)
value = value.reshape(*cond_shape, self.num_heads, -1)
else:
key, _ = self.to_key(hidden_states)
value, _ = self.to_value(hidden_states)
shape = query.shape[:-1]
query = query.reshape(*shape, self.num_heads, -1)
key = key.reshape(*shape, self.num_heads, -1)
value = value.reshape(*shape, self.num_heads, -1)
query = self.query_norm(query.float()).type_as(query)
key = self.key_norm(key.float()).type_as(key)
if rotary_emb is not None:
query = _apply_rotary(query, rotary_emb).type_as(query)
key = _apply_rotary(key, rotary_emb).type_as(key)
if sparse_params is not None:
raise NotImplementedError(
"Sparse attention is not yet supported for Kandinsky5 in FastVideo."
)
try:
hidden_states = self.local_attention(query, key, value)
except AssertionError as exc:
# LocalAttention requires pipeline forward context. Standalone
# parity tests call the model directly, so fallback to Torch SDPA.
if "Forward context is not set" not in str(exc):
raise
query = query.transpose(1, 2)
key = key.transpose(1, 2)
value = value.transpose(1, 2)
hidden_states = F.scaled_dot_product_attention(query,
key,
value,
attn_mask=None,
is_causal=False)
hidden_states = hidden_states.transpose(1, 2)
hidden_states = hidden_states.flatten(2)
hidden_states, _ = self.out_layer(hidden_states)
return hidden_states
class Kandinsky5FeedForward(nn.Module):
def __init__(self, dim: int, ff_dim: int):
super().__init__()
self.mlp = MLP(dim, ff_dim, bias=False, act_type="gelu")
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.mlp(x)
class Kandinsky5OutLayer(nn.Module):
def __init__(self, model_dim: int, time_dim: int, visual_dim: int,
patch_size: tuple[int, int, int]):
super().__init__()
self.patch_size = patch_size
self.modulation = Kandinsky5Modulation(time_dim, model_dim, 2)
self.norm = nn.LayerNorm(model_dim,
eps=1e-5,
elementwise_affine=False)
self.out_layer = ReplicatedLinear(model_dim,
math.prod(patch_size) * visual_dim,
bias=True)
def forward(self, visual_embed: torch.Tensor,
time_embed: torch.Tensor) -> torch.Tensor:
shift, scale = torch.chunk(
self.modulation(time_embed).unsqueeze(dim=1), 2, dim=-1)
visual_embed = (
self.norm(visual_embed.float()) *
(scale.float()[:, None, None] + 1.0) +
shift.float()[:, None, None]).type_as(visual_embed)
x, _ = self.out_layer(visual_embed)
batch_size, duration, height, width, _ = x.shape
x = (x.view(
batch_size,
duration,
height,
width,
-1,
self.patch_size[0],
self.patch_size[1],
self.patch_size[2],
).permute(0, 1, 5, 2, 6, 3, 7, 4).flatten(1, 2).flatten(
2, 3).flatten(3, 4))
return x
class Kandinsky5TransformerEncoderBlock(nn.Module):
def __init__(self, model_dim: int, time_dim: int, ff_dim: int,
head_dim: int,
supported_attention_backends: tuple[AttentionBackendEnum, ...]
| None = None,
prefix: str = ""):
super().__init__()
self.text_modulation = Kandinsky5Modulation(time_dim, model_dim, 6)
self.self_attention_norm = LayerNormScaleShift(
model_dim,
norm_type="layer",
eps=1e-5,
elementwise_affine=False,
dtype=torch.float32,
compute_dtype=torch.float32)
self.self_attention = Kandinsky5Attention(
model_dim,
head_dim,
supported_attention_backends=supported_attention_backends,
prefix=f"{prefix}.self_attention")
self.feed_forward_norm = LayerNormScaleShift(
model_dim,
norm_type="layer",
eps=1e-5,
elementwise_affine=False,
dtype=torch.float32,
compute_dtype=torch.float32)
self.feed_forward = Kandinsky5FeedForward(model_dim, ff_dim)
def forward(self, x: torch.Tensor, time_embed: torch.Tensor,
rope: torch.Tensor) -> torch.Tensor:
self_attn_params, ff_params = torch.chunk(
self.text_modulation(time_embed).unsqueeze(dim=1), 2, dim=-1)
shift, scale, gate = torch.chunk(self_attn_params, 3, dim=-1)
out = self.self_attention_norm(x.float(),
shift=shift,
scale=scale,
convert_modulation_dtype=True).type_as(x)
out = self.self_attention(out, rotary_emb=rope)
x = (x.float() + gate.float() * out.float()).type_as(x)
ff_shift, ff_scale, ff_gate = torch.chunk(ff_params, 3, dim=-1)
out = self.feed_forward_norm(x.float(),
shift=ff_shift,
scale=ff_scale,
convert_modulation_dtype=True).type_as(x)
out = self.feed_forward(out)
x = (x.float() + ff_gate.float() * out.float()).type_as(x)
return x
class Kandinsky5TransformerDecoderBlock(nn.Module):
def __init__(self, model_dim: int, time_dim: int, ff_dim: int,
head_dim: int,
supported_attention_backends: tuple[AttentionBackendEnum, ...]
| None = None,
prefix: str = ""):
super().__init__()
self.visual_modulation = Kandinsky5Modulation(time_dim, model_dim, 9)
self.self_attention_norm = LayerNormScaleShift(
model_dim,
norm_type="layer",
eps=1e-5,
elementwise_affine=False,
dtype=torch.float32,
compute_dtype=torch.float32)
self.self_attention = Kandinsky5Attention(
model_dim,
head_dim,
supported_attention_backends=supported_attention_backends,
prefix=f"{prefix}.self_attention")
self.cross_attention_norm = LayerNormScaleShift(
model_dim,
norm_type="layer",
eps=1e-5,
elementwise_affine=False,
dtype=torch.float32,
compute_dtype=torch.float32)
self.cross_attention = Kandinsky5Attention(
model_dim,
head_dim,
supported_attention_backends=supported_attention_backends,
prefix=f"{prefix}.cross_attention")
self.feed_forward_norm = LayerNormScaleShift(
model_dim,
norm_type="layer",
eps=1e-5,
elementwise_affine=False,
dtype=torch.float32,
compute_dtype=torch.float32)
self.feed_forward = Kandinsky5FeedForward(model_dim, ff_dim)
def forward(self, visual_embed: torch.Tensor, text_embed: torch.Tensor,
time_embed: torch.Tensor, rope: torch.Tensor,
sparse_params: dict[str, Any] | None) -> torch.Tensor:
self_attn_params, cross_attn_params, ff_params = torch.chunk(
self.visual_modulation(time_embed).unsqueeze(dim=1), 3, dim=-1)
self_shift, self_scale, self_gate = torch.chunk(self_attn_params,
3,
dim=-1)
visual_out = self.self_attention_norm(
visual_embed.float(),
shift=self_shift,
scale=self_scale,
convert_modulation_dtype=True,
).type_as(visual_embed)
visual_out = self.self_attention(visual_out,
rotary_emb=rope,
sparse_params=sparse_params)
visual_embed = (visual_embed.float() +
self_gate.float() * visual_out.float()).type_as(
visual_embed)
cross_shift, cross_scale, cross_gate = torch.chunk(cross_attn_params,
3,
dim=-1)
visual_out = self.cross_attention_norm(
visual_embed.float(),
shift=cross_shift,
scale=cross_scale,
convert_modulation_dtype=True,
).type_as(visual_embed)
visual_out = self.cross_attention(
visual_out, encoder_hidden_states=text_embed)
visual_embed = (visual_embed.float() +
cross_gate.float() * visual_out.float()).type_as(
visual_embed)
ff_shift, ff_scale, ff_gate = torch.chunk(ff_params, 3, dim=-1)
visual_out = self.feed_forward_norm(
visual_embed.float(),
shift=ff_shift,
scale=ff_scale,
convert_modulation_dtype=True,
).type_as(visual_embed)
visual_out = self.feed_forward(visual_out)
visual_embed = (visual_embed.float() +
ff_gate.float() * visual_out.float()).type_as(
visual_embed)
return visual_embed
@dataclass
class Kandinsky5TransformerOutput:
sample: torch.Tensor
class Kandinsky5Transformer3DModel(BaseDiT):
"""
Native FastVideo implementation of Kandinsky5 Transformer.
"""
_fsdp_shard_conditions = _ARCH_CONFIG_DEFAULTS._fsdp_shard_conditions
_compile_conditions = _ARCH_CONFIG_DEFAULTS._compile_conditions
param_names_mapping = _ARCH_CONFIG_DEFAULTS.param_names_mapping
reverse_param_names_mapping = _ARCH_CONFIG_DEFAULTS.reverse_param_names_mapping
lora_param_names_mapping = _ARCH_CONFIG_DEFAULTS.lora_param_names_mapping
_supported_attention_backends = _ARCH_CONFIG_DEFAULTS._supported_attention_backends
def __init__(self, config: Kandinsky5VideoConfig,
hf_config: dict[str, Any]) -> None:
super().__init__(config=config, hf_config=hf_config)
arch = config.arch_config
head_dim = sum(arch.axes_dims)
self.in_visual_dim = arch.in_visual_dim
self.model_dim = arch.model_dim
self.patch_size = arch.patch_size
self.visual_cond = arch.visual_cond
self.attention_type = arch.attention_type
visual_embed_dim = (2 * arch.in_visual_dim +
1) if arch.visual_cond else arch.in_visual_dim
self.time_embeddings = Kandinsky5TimeEmbeddings(
arch.model_dim, arch.time_dim)
self.text_embeddings = Kandinsky5TextEmbeddings(
arch.in_text_dim, arch.model_dim)
self.pooled_text_embeddings = Kandinsky5TextEmbeddings(
arch.in_text_dim2, arch.time_dim)
self.visual_embeddings = Kandinsky5VisualEmbeddings(
visual_embed_dim, arch.model_dim, arch.patch_size)
self.text_rope_embeddings = Kandinsky5RoPE1D(head_dim)
self.visual_rope_embeddings = Kandinsky5RoPE3D(arch.axes_dims)
self.text_transformer_blocks = nn.ModuleList([
Kandinsky5TransformerEncoderBlock(arch.model_dim, arch.time_dim,
arch.ff_dim,
head_dim,
self._supported_attention_backends,
prefix=f"{config.prefix}.text_transformer_blocks.{i}")
for i in range(arch.num_text_blocks)
])
self.visual_transformer_blocks = nn.ModuleList([
Kandinsky5TransformerDecoderBlock(arch.model_dim, arch.time_dim,
arch.ff_dim,
head_dim,
self._supported_attention_backends,
prefix=f"{config.prefix}.visual_transformer_blocks.{i}")
for i in range(arch.num_visual_blocks)
])
self.out_layer = Kandinsky5OutLayer(arch.model_dim, arch.time_dim,
arch.out_visual_dim,
arch.patch_size)
self.gradient_checkpointing = False
self.hidden_size = arch.hidden_size
self.num_attention_heads = arch.num_attention_heads
self.num_channels_latents = arch.num_channels_latents
self.__post_init__()
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor | list[torch.Tensor],
timestep: torch.Tensor,
encoder_hidden_states_image: torch.Tensor | list[torch.Tensor]
| None = None,
pooled_projections: torch.Tensor | None = None,
visual_rope_pos: tuple[torch.Tensor, torch.Tensor,
torch.Tensor] | list[torch.Tensor] | None = None,
text_rope_pos: torch.Tensor | None = None,
scale_factor: tuple[float, float, float] = (1.0, 1.0, 1.0),
sparse_params: dict[str, Any] | None = None,
return_dict: bool = False,
**kwargs,
) -> torch.Tensor | Kandinsky5TransformerOutput:
if isinstance(encoder_hidden_states, list):
encoder_hidden_states = encoder_hidden_states[0]
if pooled_projections is None:
if encoder_hidden_states_image is None:
raise ValueError(
"pooled_projections must be provided for Kandinsky5.")
pooled_projections = encoder_hidden_states_image
if isinstance(pooled_projections, list):
pooled_projections = pooled_projections[0]
if visual_rope_pos is None or text_rope_pos is None:
raise ValueError(
"visual_rope_pos and text_rope_pos are required for Kandinsky5."
)
x = hidden_states
text_embed = self.text_embeddings(encoder_hidden_states)
time_embed = self.time_embeddings(timestep)
time_embed = time_embed + self.pooled_text_embeddings(pooled_projections)
visual_embed = self.visual_embeddings(x)
text_rope = self.text_rope_embeddings(text_rope_pos).unsqueeze(dim=0)
for text_transformer_block in self.text_transformer_blocks:
if torch.is_grad_enabled() and self.gradient_checkpointing:
text_embed = torch.utils.checkpoint.checkpoint(
text_transformer_block,
text_embed,
time_embed,
text_rope,
use_reentrant=False)
else:
text_embed = text_transformer_block(text_embed, time_embed,
text_rope)
visual_shape = visual_embed.shape[:-1]
visual_rope = self.visual_rope_embeddings(visual_shape, visual_rope_pos,
scale_factor)
to_fractal = sparse_params[
"to_fractal"] if sparse_params is not None else False
visual_embed, visual_rope = fractal_flatten(visual_embed, visual_rope,
visual_shape,
block_mask=to_fractal)
for visual_transformer_block in self.visual_transformer_blocks:
if torch.is_grad_enabled() and self.gradient_checkpointing:
visual_embed = torch.utils.checkpoint.checkpoint(
visual_transformer_block,
visual_embed,
text_embed,
time_embed,
visual_rope,
sparse_params,
use_reentrant=False)
else:
visual_embed = visual_transformer_block(
visual_embed,
text_embed,
time_embed,
visual_rope,
sparse_params,
)
visual_embed = fractal_unflatten(visual_embed,
visual_shape,
block_mask=to_fractal)
x = self.out_layer(visual_embed, time_embed)
if return_dict:
return Kandinsky5TransformerOutput(sample=x)
return x
def materialize_non_persistent_buffers(self, device: torch.device,
dtype: torch.dtype | None = None
) -> None:
if isinstance(self.time_embeddings.freqs,
torch.Tensor) and self.time_embeddings.freqs.is_meta:
self.time_embeddings.freqs = _build_rotary_freqs(
self.time_embeddings.model_dim // 2,
self.time_embeddings.max_period).to(device=device)
if isinstance(self.text_rope_embeddings.args,
torch.Tensor) and self.text_rope_embeddings.args.is_meta:
freq = _build_rotary_freqs(self.text_rope_embeddings.dim // 2,
self.text_rope_embeddings.max_period).to(
device=device)
pos = torch.arange(self.text_rope_embeddings.max_pos,
dtype=freq.dtype,
device=device)
self.text_rope_embeddings._buffers["args"] = torch.outer(pos, freq)
for i, (axes_dim,
ax_max_pos) in enumerate(
zip(self.visual_rope_embeddings.axes_dims,
self.visual_rope_embeddings.max_pos,
strict=True)):
name = f"args_{i}"
buf = getattr(self.visual_rope_embeddings, name, None)
if isinstance(buf, torch.Tensor) and buf.is_meta:
freq = _build_rotary_freqs(
axes_dim // 2, self.visual_rope_embeddings.max_period).to(
device=device)
pos = torch.arange(ax_max_pos, dtype=freq.dtype, device=device)
self.visual_rope_embeddings._buffers[name] = torch.outer(
pos, freq)
-3
View File
@@ -147,9 +147,6 @@ def maybe_load_fsdp_model(
cpu_offload=cpu_offload,
param_names_mapping=param_names_mapping_fn,
)
if hasattr(model, "materialize_non_persistent_buffers"):
model.materialize_non_persistent_buffers(
device=device, dtype=default_dtype)
for n, p in chain(model.named_parameters(), model.named_buffers()):
if p.is_meta:
raise RuntimeError(
-2
View File
@@ -40,8 +40,6 @@ _TEXT_TO_VIDEO_DIT_MODELS = {
"LTX2Transformer3DModel": ("dits", "ltx2", "LTX2Transformer3DModel"),
"SD3Transformer2DModel": ("dits", "sd3", "SD3Transformer2DModel"),
"LingBotWorldTransformer3DModel": ("dits", "lingbotworld", "LingBotWorldTransformer3DModel"),
"Kandinsky5Transformer3DModel":
("dits", "kandinsky5", "Kandinsky5Transformer3DModel"),
}
_IMAGE_TO_VIDEO_DIT_MODELS = {
+1 -6
View File
@@ -364,15 +364,10 @@ class ParallelTiledVAE(ABC):
if i > 0:
tile = tile[:, :, 1:, :, :]
row.append(tile)
blend_latent_num_frames = max(
1,
self.blend_num_frames // self.temporal_compression_ratio,
)
result_row = []
for i, tile in enumerate(row):
if i > 0:
tile = self.blend_t(row[i - 1], tile,
blend_latent_num_frames)
tile = self.blend_t(row[i - 1], tile, self.blend_num_frames)
result_row.append(
tile[:, :, :tile_latent_stride_num_frames, :, :])
else:
@@ -117,26 +117,56 @@ class PreprocessPipeline_MatrixGame(BasePreprocessPipeline):
if "action_path" in valid_data and valid_data["action_path"]:
keyboard_cond_list = []
mouse_cond_list = []
num_bits = 6
for action_path in valid_data["action_path"]:
if action_path:
action_data = np.load(action_path, allow_pickle=True)
if isinstance(action_data, np.ndarray) and action_data.dtype == np.dtype('O'):
action_dict = action_data.item()
if "keyboard" in action_dict:
kb_length = len(action_dict["keyboard"])
assert kb_length >= num_frames, (f"keyboard length {kb_length} is smaller than "
f"num_frames {num_frames} for {action_path}.")
keyboard_cond_list.append(action_dict["keyboard"][:num_frames].astype(np.float32))
keyboard_raw = action_dict["keyboard"]
# Convert 1D bit-flag values to 2D multi-hot encoding
if isinstance(keyboard_raw, np.ndarray):
if keyboard_raw.ndim == 1:
# [T] -> [T, num_bits]
T = len(keyboard_raw)
multi_hot = np.zeros((T, num_bits), dtype=np.float32)
action_values = keyboard_raw.astype(int)
for bit_idx in range(num_bits):
target_idx = (2 - (bit_idx % 3)) + 3 * (bit_idx // 3)
if target_idx < num_bits:
multi_hot[:, target_idx] = ((action_values >> bit_idx)
& 1).astype(np.float32)
keyboard_cond_list.append(multi_hot)
else:
# If already 2D, pad to num_bits if necessary
k_data = keyboard_raw.astype(np.float32)
if k_data.ndim == 2 and k_data.shape[-1] < num_bits:
padding = np.zeros((k_data.shape[0], num_bits - k_data.shape[-1]),
dtype=np.float32)
k_data = np.concatenate([k_data, padding], axis=-1)
keyboard_cond_list.append(k_data)
else:
keyboard_cond_list.append(keyboard_raw)
if "mouse" in action_dict:
ms_length = len(action_dict["mouse"])
assert ms_length >= num_frames, (f"mouse length {ms_length} is smaller than "
f"num_frames {num_frames} for {action_path}.")
mouse_cond_list.append(action_dict["mouse"][:num_frames])
mouse_cond_list.append(action_dict["mouse"])
else:
kb_length = len(action_data)
assert kb_length >= num_frames, (f"keyboard length {kb_length} is smaller than "
f"num_frames {num_frames} for {action_path}.")
keyboard_cond_list.append(action_data.astype(np.float32)[:num_frames])
if isinstance(action_data, np.ndarray) and action_data.ndim == 1:
T = len(action_data)
multi_hot = np.zeros((T, num_bits), dtype=np.float32)
action_values = action_data.astype(int)
for bit_idx in range(num_bits):
target_idx = (2 - (bit_idx % 3)) + 3 * (bit_idx // 3)
if target_idx < num_bits:
multi_hot[:, target_idx] = ((action_values >> bit_idx) & 1).astype(np.float32)
keyboard_cond_list.append(multi_hot)
else:
# If already 2D, pad to num_bits if necessary
k_data = action_data.astype(np.float32)
if k_data.ndim == 2 and k_data.shape[-1] < num_bits:
padding = np.zeros((k_data.shape[0], num_bits - k_data.shape[-1]), dtype=np.float32)
k_data = np.concatenate([k_data, padding], axis=-1)
keyboard_cond_list.append(k_data)
if keyboard_cond_list:
features["keyboard_cond"] = keyboard_cond_list
if mouse_cond_list:
@@ -1,206 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
"""Preprocess HunyuanVideo overfit data into parquet format.
Encodes videos with HunyuanVideo VAE and captions with dual text
encoders (LLaMA + CLIP) into the t2v parquet schema expected by
the training framework.
Usage:
CUDA_VISIBLE_DEVICES=0 python scripts/preprocess_hunyuan_overfit.py
"""
import glob
import json
import os
import cv2
import numpy as np
import pyarrow as pa
import pyarrow.parquet as pq
import torch
from safetensors.torch import load_file as safetensors_load_file
from fastvideo.configs.models.vaes import HunyuanVAEConfig
from fastvideo.configs.pipelines.hunyuan import (
clip_preprocess_text,
clip_postprocess_text,
llama_preprocess_text,
llama_postprocess_text,
)
from fastvideo.dataset.dataloader.schema import pyarrow_schema_t2v
from fastvideo.models.vaes.hunyuanvae import AutoencoderKLHunyuanVideo
from fastvideo.utils import maybe_download_model
# --- Config ---
NUM_FRAMES = 77 # 4k+1 for temporal compression ratio 4
MAX_HEIGHT = 480
MAX_WIDTH = 832
TRAIN_FPS = 16.0
DATA_DIR = "data/hunyuan_overfit"
OUTPUT_DIR = "data/hunyuan_overfit_preprocessed"
def load_video(path: str, num_frames: int) -> torch.Tensor:
"""Load video as [1, C, T, H, W] in [-1, 1]."""
cap = cv2.VideoCapture(path)
frames: list[np.ndarray] = []
while len(frames) < num_frames:
ret, frame = cap.read()
if not ret:
break
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frames.append(frame)
cap.release()
if len(frames) < num_frames:
# Repeat last frame to fill
while len(frames) < num_frames:
frames.append(frames[-1])
frames = frames[:num_frames]
video = np.stack(frames, axis=0)
video = torch.from_numpy(video).float()
video = video / 127.5 - 1.0 # [0,255] -> [-1,1]
video = video.permute(3, 0, 1, 2).unsqueeze(0) # [1,C,T,H,W]
return video
def main() -> None:
device = torch.device("cuda:0")
model_path = maybe_download_model("hunyuanvideo-community/HunyuanVideo")
vae_path = os.path.join(model_path, "vae")
os.makedirs(OUTPUT_DIR, exist_ok=True)
# Load captions
with open(os.path.join(DATA_DIR, "videos2caption.json")) as f:
caption_data = json.load(f)
# --- Load VAE ---
print("Loading HunyuanVideo VAE...")
vae_config = HunyuanVAEConfig()
vae_config.load_encoder = True
vae_config.load_decoder = False
vae = AutoencoderKLHunyuanVideo(vae_config)
sf_files = glob.glob(os.path.join(vae_path, "*.safetensors"))
loaded = {}
for sf_file in sf_files:
loaded.update(safetensors_load_file(sf_file))
vae.load_state_dict(loaded, strict=False)
vae = vae.to(device=device, dtype=torch.float16).eval()
vae.use_parallel_tiling = False
print(f"VAE loaded ({sum(p.numel() for p in vae.parameters())/1e6:.0f}M)")
# --- Load text encoders ---
print("Loading LLaMA text encoder...")
from transformers import AutoTokenizer, CLIPTextModel, LlamaModel
from fastvideo.configs.pipelines.hunyuan import (LlamaConfig, CLIPTextConfig)
llama_cfg = LlamaConfig()
llama_tok = AutoTokenizer.from_pretrained(os.path.join(model_path, "tokenizer"))
llama_enc = LlamaModel.from_pretrained(
os.path.join(model_path, "text_encoder"),
torch_dtype=torch.float16,
).to(device).eval()
llama_tok_kwargs = dict(llama_cfg.tokenizer_kwargs)
print("Loading CLIP text encoder...")
clip_cfg = CLIPTextConfig()
clip_tok = AutoTokenizer.from_pretrained(os.path.join(model_path, "tokenizer_2"))
clip_enc = CLIPTextModel.from_pretrained(
os.path.join(model_path, "text_encoder_2"),
torch_dtype=torch.float16,
).to(device).eval()
clip_tok_kwargs = dict(clip_cfg.tokenizer_kwargs)
# --- Process each video ---
records = []
for item in caption_data:
video_name = item["path"]
caption = item["cap"][0]
video_path = os.path.join(DATA_DIR, "videos", video_name)
print(f"\nProcessing: {video_name}")
print(f" Caption: {caption[:80]}...")
# Encode video
video = load_video(video_path, NUM_FRAMES).to(device=device, dtype=torch.float16)
print(f" Video shape: {video.shape}")
with torch.no_grad():
latent_dist = vae.encode(video)
# Cast to fp32 — dataloader hardcodes np.float32
latent = latent_dist.mean.squeeze(0).float().cpu()
print(f" Latent shape: {latent.shape}")
# Encode text with LLaMA
llama_text = llama_preprocess_text(caption)
with torch.no_grad():
llama_inputs = llama_tok(llama_text, **llama_tok_kwargs).to(device)
llama_out = llama_enc(**llama_inputs, output_hidden_states=True)
llama_embeds = llama_postprocess_text(llama_out).squeeze(0) # [seq, dim]
# Encode text with CLIP
clip_text = clip_preprocess_text(caption)
with torch.no_grad():
clip_inputs = clip_tok(clip_text, **clip_tok_kwargs).to(device)
clip_out = clip_enc(**clip_inputs)
clip_pooled = clip_postprocess_text(clip_out).squeeze(0) # [dim]
# Combine: [pooled_clip_row, llama_embeds]
llama_dim = llama_embeds.shape[-1]
pooled_row = torch.zeros(llama_dim, device=device, dtype=torch.float16)
pooled_row[:clip_pooled.shape[-1]] = clip_pooled
text_embedding = torch.cat(
[pooled_row.unsqueeze(0), llama_embeds],
dim=0,
).float().cpu() # [seq+1, dim]
print(f" Text embedding shape: {text_embedding.shape}")
record = {
"id": video_name,
"vae_latent_bytes": latent.numpy().tobytes(),
"vae_latent_shape": list(latent.shape),
"vae_latent_dtype": str(latent.dtype).replace("torch.", ""),
"text_embedding_bytes": text_embedding.numpy().tobytes(),
"text_embedding_shape": list(text_embedding.shape),
"text_embedding_dtype": str(text_embedding.dtype).replace("torch.", ""),
"file_name": video_name,
"caption": caption,
"media_type": "video",
"width": MAX_WIDTH,
"height": MAX_HEIGHT,
"num_frames": NUM_FRAMES,
"duration_sec": NUM_FRAMES / TRAIN_FPS,
"fps": TRAIN_FPS,
}
records.append(record)
# Clean up encoders
del llama_enc, llama_tok, clip_enc, clip_tok, vae
# Write parquet
table = pa.table(
{k: [r[k] for r in records]
for k in records[0]},
schema=pyarrow_schema_t2v,
)
output_path = os.path.join(OUTPUT_DIR, "data_00000.parquet")
pq.write_table(table, output_path)
print(f"\nWrote {len(records)} records to {output_path}")
# Write validation prompts for callback
# Wrap in "data" key — ValidationDataset expects field="data"
# Use "caption" field — ValidationDataset aliases it to "prompt"
val_prompts = {"data": [{"caption": item["cap"][0]} for item in caption_data]}
val_path = os.path.join(OUTPUT_DIR, "validation_prompts.json")
with open(val_path, "w") as f:
json.dump(val_prompts, f, indent=2)
print(f"Wrote validation prompts to {val_path}")
print("\nDone! Use data_path: " + OUTPUT_DIR + " in training config.")
if __name__ == "__main__":
main()
@@ -31,12 +31,11 @@ class PreprocessPipeline_I2V(BasePreprocessPipeline):
tokenizers=[self.get_module("tokenizer")],
))
if (self.get_module("image_encoder") is not None and self.get_module("image_processor") is not None):
self.add_stage(stage_name="image_encoding_stage",
stage=ImageEncodingStage(
image_encoder=self.get_module("image_encoder"),
image_processor=self.get_module("image_processor"),
))
self.add_stage(stage_name="image_encoding_stage",
stage=ImageEncodingStage(
image_encoder=self.get_module("image_encoder"),
image_processor=self.get_module("image_processor"),
))
def get_pyarrow_schema(self):
"""Return the PyArrow schema for I2V pipeline."""
@@ -45,34 +44,35 @@ class PreprocessPipeline_I2V(BasePreprocessPipeline):
def get_extra_features(self, valid_data: dict[str, Any], fastvideo_args: FastVideoArgs) -> dict[str, Any]:
# TODO(will): move these to cpu at some point
self.get_module("image_encoder").to(get_local_torch_device())
self.get_module("vae").to(get_local_torch_device())
features = {}
"""Get CLIP features from the first frame of each video."""
first_frame = valid_data["pixel_values"][:, :, 0, :, :].permute(0, 2, 3, 1) # (B, C, T, H, W) -> (B, H, W, C)
_, _, num_frames, height, width = valid_data["pixel_values"].shape
# latent_height = height // self.get_module(
# "vae").spatial_compression_ratio
# latent_width = width // self.get_module("vae").spatial_compression_ratio
# Get CLIP features if image_encoder is available
if (self.get_module("image_encoder") is not None and self.get_module("image_processor") is not None):
self.get_module("image_encoder").to(get_local_torch_device())
processed_images = []
# Frame has values between -1 and 1
for frame in first_frame:
frame = (frame + 1) * 127.5
frame_pil = Image.fromarray(frame.cpu().numpy().astype(np.uint8))
processed_img = self.get_module("image_processor")(images=frame_pil, return_tensors="pt")
processed_images.append(processed_img)
processed_images = []
for frame in first_frame:
frame = (frame + 1) * 127.5
frame_pil = Image.fromarray(frame.cpu().numpy().astype(np.uint8))
processed_img = self.get_module("image_processor")(images=frame_pil, return_tensors="pt")
processed_images.append(processed_img)
# Get CLIP features
pixel_values = torch.cat([img['pixel_values'] for img in processed_images], dim=0).to(get_local_torch_device())
with torch.no_grad():
image_inputs = {'pixel_values': pixel_values}
with set_forward_context(current_timestep=0, attn_metadata=None):
clip_features = self.get_module("image_encoder")(**image_inputs)
clip_features = clip_features.last_hidden_state
pixel_values = torch.cat([img['pixel_values'] for img in processed_images],
dim=0).to(get_local_torch_device())
with torch.no_grad():
image_inputs = {'pixel_values': pixel_values}
with set_forward_context(current_timestep=0, attn_metadata=None):
clip_features = self.get_module("image_encoder")(**image_inputs)
clip_features = clip_features.last_hidden_state
features["clip_feature"] = clip_features
# Get VAE features from the first frame of each video
features["clip_feature"] = clip_features
"""Get VAE features from the first frame of each video"""
video_conditions = []
for frame in first_frame:
processed_img = frame.to(device="cpu", dtype=torch.float32)
@@ -105,6 +105,25 @@ class PreprocessPipeline_I2V(BasePreprocessPipeline):
else:
latent_condition = latent_condition * self.get_module("vae").scaling_factor
# mask_lat_size = torch.ones(batch_size, 1, num_frames, latent_height,
# latent_width)
# mask_lat_size[:, :, list(range(1, num_frames))] = 0
# first_frame_mask = mask_lat_size[:, :, 0:1]
# first_frame_mask = torch.repeat_interleave(
# first_frame_mask,
# dim=2,
# repeats=self.get_module("vae").temporal_compression_ratio)
# mask_lat_size = torch.concat(
# [first_frame_mask, mask_lat_size[:, :, 1:, :]], dim=2)
# mask_lat_size = mask_lat_size.view(
# batch_size, -1,
# self.get_module("vae").temporal_compression_ratio, latent_height,
# latent_width)
# mask_lat_size = mask_lat_size.transpose(1, 2)
# mask_lat_size = mask_lat_size.to(latent_condition.device)
# image_latent = torch.concat([mask_lat_size, latent_condition], dim=1)
features["first_frame_latent"] = latent_condition
return features
@@ -178,6 +178,8 @@ class InputValidationStage(PipelineStage):
if batch.keyboard_cond.dim() != 3:
raise ValueError(f"keyboard_cond must have 3 dimensions (B, T, K), but got {batch.keyboard_cond.dim()}")
keyboard_dim = batch.keyboard_cond.shape[-1]
if keyboard_dim not in {2, 3, 4, 6, 7}:
raise ValueError(f"keyboard_cond last dimension must be 2, 3, 4, 6, or 7, but got {keyboard_dim}")
logger.info("Action control: keyboard_cond validated - shape %s (dim=%d)", batch.keyboard_cond.shape,
keyboard_dim)
-12
View File
@@ -348,18 +348,6 @@ def _register_configs() -> None:
model_detectors=[lambda path: ("lingbotworld" in path.lower() or "lingbot-world" in path.lower())],
)
# Kandinsky5 Lite T2V
register_configs(
sampling_param_cls=None,
pipeline_config_cls=PipelineConfig,
hf_model_paths=[
"kandinskylab/Kandinsky-5.0-T2V-Lite-sft-5s-Diffusers",
],
model_detectors=[
lambda path: any(token in path.lower() for token in ("kandinsky5", "kandinsky-5")),
],
)
# LongCat
register_configs(
sampling_param_cls=None,
+24 -17
View File
@@ -21,7 +21,9 @@ os.environ["MASTER_ADDR"] = "localhost"
os.environ["MASTER_PORT"] = "29503"
BASE_MODEL_PATH = "hunyuanvideo-community/HunyuanVideo"
MODEL_PATH = maybe_download_model(BASE_MODEL_PATH, local_dir=os.path.join("data", BASE_MODEL_PATH))
MODEL_PATH = maybe_download_model(BASE_MODEL_PATH,
local_dir=os.path.join("data", BASE_MODEL_PATH) # store in the large /workspace disk on Runpod
)
TEXT_ENCODER_PATH = os.path.join(MODEL_PATH, "text_encoder_2")
TOKENIZER_PATH = os.path.join(MODEL_PATH, "tokenizer_2")
@@ -32,12 +34,13 @@ def test_clip_encoder():
Tests compatibility between two different implementations for loading text encoders:
1. load_text_encoder from fastvideo.models.hunyuan.text_encoder
2. TextEncoderLoader from fastvideo.models.loader
The test verifies that both implementations:
- Load models with the same weights and parameters
- Produce nearly identical outputs for the same input prompts
"""
args = FastVideoArgs(model_path="openai/clip-vit-large-patch14", pipeline_config=HunyuanConfig())
args = FastVideoArgs(model_path="openai/clip-vit-large-patch14",
pipeline_config=HunyuanConfig())
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
logger.info("Loading models from %s", args.model_path)
@@ -52,7 +55,6 @@ def test_clip_encoder():
model1 = CLIPTextModel.from_pretrained(TEXT_ENCODER_PATH).to(torch.float16).to(device).eval()
from fastvideo.models.loader.component_loader import TextEncoderLoader
loader = TextEncoderLoader()
model2 = loader.load(TEXT_ENCODER_PATH, args)
@@ -88,7 +90,10 @@ def test_clip_encoder():
tokenizer = AutoTokenizer.from_pretrained(TOKENIZER_PATH)
# Test with some sample prompts
prompts = ["a photo of a cat", "a beautiful landscape with mountains", "an astronaut riding a horse on the moon"]
prompts = [
"a photo of a cat", "a beautiful landscape with mountains",
"an astronaut riding a horse on the moon"
]
logger.info("Testing CLIP text encoder with sample prompts")
@@ -97,11 +102,14 @@ def test_clip_encoder():
logger.info("Testing prompt: '%s'", prompt)
# Tokenize the prompt
tokens = tokenizer(prompt, padding="max_length", max_length=77, truncation=True, return_tensors="pt").to(
device
)
tokens = tokenizer(prompt,
padding="max_length",
max_length=77,
truncation=True,
return_tensors="pt").to(device)
# Get embeddings from our implementation
outputs1 = model1(input_ids=tokens.input_ids, output_hidden_states=True)
outputs1 = model1(input_ids=tokens.input_ids,
output_hidden_states=True)
logger.info("Testing model2")
print("--------------------------------")
@@ -110,25 +118,24 @@ def test_clip_encoder():
outputs2 = model2(
input_ids=tokens.input_ids,
# attention_mask=tokens.attention_mask,
output_hidden_states=True,
)
output_hidden_states=True)
# Compare last hidden states
last_hidden_state1 = outputs1.last_hidden_state[tokens.attention_mask == 1]
last_hidden_state2 = outputs2.last_hidden_state[tokens.attention_mask == 1]
last_hidden_state1 = outputs1.last_hidden_state[
tokens.attention_mask == 1]
last_hidden_state2 = outputs2.last_hidden_state[
tokens.attention_mask == 1]
# print("last_hidden_state1", last_hidden_state1)
# print("last_hidden_state2", last_hidden_state2)
assert last_hidden_state1.shape == last_hidden_state2.shape, (
assert last_hidden_state1.shape == last_hidden_state2.shape, \
f"Hidden state shapes don't match: {last_hidden_state1.shape} vs {last_hidden_state2.shape}"
)
# Compare pooler outputs
pooler_output1 = outputs1.pooler_output
pooler_output2 = outputs2.pooler_output
assert pooler_output1.shape == pooler_output2.shape, (
assert pooler_output1.shape == pooler_output2.shape, \
f"Pooler output shapes don't match: {pooler_output1.shape} vs {pooler_output2.shape}"
)
assert_close(pooler_output1, pooler_output2, atol=1e-2, rtol=1e-3)
assert_close(last_hidden_state1, last_hidden_state2, atol=1e-2, rtol=1e-3)
+28 -22
View File
@@ -15,14 +15,15 @@ from fastvideo.utils import maybe_download_model
from fastvideo.configs.models.encoders import LlamaConfig
from torch.distributed.tensor import DTensor
from torch.testing import assert_close
logger = init_logger(__name__)
os.environ["MASTER_ADDR"] = "localhost"
os.environ["MASTER_PORT"] = "29503"
BASE_MODEL_PATH = "hunyuanvideo-community/HunyuanVideo"
MODEL_PATH = maybe_download_model(BASE_MODEL_PATH, local_dir=os.path.join("data", BASE_MODEL_PATH))
MODEL_PATH = maybe_download_model(BASE_MODEL_PATH,
local_dir=os.path.join("data", BASE_MODEL_PATH) # store in the large /workspace disk on Runpod
)
TEXT_ENCODER_PATH = os.path.join(MODEL_PATH, "text_encoder")
TOKENIZER_PATH = os.path.join(MODEL_PATH, "tokenizer")
@@ -33,12 +34,13 @@ def test_llama_encoder():
Tests compatibility between two different implementations for loading text encoders:
1. load_text_encoder from fastvideo.models.hunyuan.text_encoder
2. TextEncoderLoader from fastvideo.models.loader
The test verifies that both implementations:
- Load models with the same weights and parameters
- Produce nearly identical outputs for the same input prompts
"""
args = FastVideoArgs(model_path="meta-llama/Llama-2-7b-hf", pipeline_config=HunyuanConfig())
args = FastVideoArgs(model_path="meta-llama/Llama-2-7b-hf",
pipeline_config=HunyuanConfig())
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
@@ -66,15 +68,15 @@ def test_llama_encoder():
logger.info("Model1 has %d parameters", len(params1))
logger.info("Model2 has %d parameters", len(params2))
# check if embed_tokens are the same
device = model1.embed_tokens.weight.device
assert torch.allclose(
model1.embed_tokens.weight,
model2.embed_tokens.weight.to_local().to(device)
if isinstance(model2.embed_tokens.weight, DTensor)
else model2.embed_tokens.weight.to(device),
)
weights = ["layers.{}.input_layernorm.weight", "layers.{}.post_attention_layernorm.weight"]
assert torch.allclose(model1.embed_tokens.weight,
model2.embed_tokens.weight.to_local().to(device) if isinstance(model2.embed_tokens.weight, DTensor) else model2.embed_tokens.weight.to(device))
weights = [
"layers.{}.input_layernorm.weight",
"layers.{}.post_attention_layernorm.weight"
]
for layer_idx in range(hf_config.num_hidden_layers):
for w in weights:
name1 = w.format(layer_idx)
@@ -119,29 +121,33 @@ def test_llama_encoder():
logger.info("Testing prompt: '%s'", prompt)
# Tokenize the prompt
tokens = tokenizer(prompt, padding="max_length", max_length=512, truncation=True, return_tensors="pt").to(
device
)
tokens = tokenizer(prompt,
padding="max_length",
max_length=512,
truncation=True,
return_tensors="pt").to(device)
# Get outputs from our implementation
# filter out padding input_ids
# tokens.input_ids = tokens.input_ids[tokens.attention_mask==1]
# tokens.attention_mask = tokens.attention_mask[tokens.attention_mask==1]
outputs1 = model1(input_ids=tokens.input_ids, output_hidden_states=True)
outputs1 = model1(input_ids=tokens.input_ids,
output_hidden_states=True)
print("--------------------------------")
logger.info("Testing model2")
# Get outputs from HuggingFace implementation
with set_forward_context(current_timestep=0, attn_metadata=None):
outputs2 = model2(
input_ids=tokens.input_ids, attention_mask=tokens.attention_mask, output_hidden_states=True
)
outputs2 = model2(input_ids=tokens.input_ids,
attention_mask=tokens.attention_mask,
output_hidden_states=True)
# Compare last hidden states
last_hidden_state1 = outputs1.last_hidden_state[tokens.attention_mask == 1]
last_hidden_state2 = outputs2.last_hidden_state[tokens.attention_mask == 1]
last_hidden_state1 = outputs1.last_hidden_state[
tokens.attention_mask == 1]
last_hidden_state2 = outputs2.last_hidden_state[
tokens.attention_mask == 1]
assert last_hidden_state1.shape == last_hidden_state2.shape, (
assert last_hidden_state1.shape == last_hidden_state2.shape, \
f"Hidden state shapes don't match: {last_hidden_state1.shape} vs {last_hidden_state2.shape}"
)
assert_close(last_hidden_state1, last_hidden_state2, atol=1e-1, rtol=1e-4)
+152 -474
View File
@@ -1,11 +1,7 @@
import ast
import datetime
import glob
import os
import re
import shutil
import subprocess
import sys
from dataclasses import dataclass
from typing import Any
@@ -14,7 +10,7 @@ import modal
app = modal.App()
model_vol = modal.Volume.from_name("hf-model-weights")
image_version = os.getenv("IMAGE_VERSION", "latest")
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}")
@@ -28,29 +24,30 @@ image = (
"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", ""),
}
.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", ""),
})
)
SSIM_NUM_GPUS = 4
SSIM_TERMINATE_TIMEOUT_S = 30
HF_TOKEN_ENV_KEYS = ("HF_API_KEY", "HUGGINGFACE_HUB_TOKEN", "HF_TOKEN")
RAW_GENERATED_VOLUME_ROOT = "ssim_generated_videos"
DEFAULT_OUTPUT_QUALITY_TIER = "default"
FULL_OUTPUT_QUALITY_TIER = "full_quality"
MODAL_DEVICE_REFERENCE_FOLDER = "L40S_reference_videos"
SSIM_COMMON_KWARGS = dict(
image=image,
timeout=5400,
secrets=[modal.Secret.from_dict(
{"HF_API_KEY": os.environ.get("HF_API_KEY", "")}
)],
volumes={"/root/data": model_vol},
)
@@ -108,194 +105,6 @@ class _PartitionResult:
exit_code: int
def _split_csv_values(csv_values: str) -> set[str]:
return {value.strip() for value in csv_values.split(",") if value.strip()}
def _run_git_command(args: list[str]) -> str:
try:
result = subprocess.run(
["git", *args],
check=True,
capture_output=True,
text=True,
)
except subprocess.CalledProcessError as error:
stderr = (error.stderr or "").strip()
raise RuntimeError(f"Failed to run git {' '.join(args)}: {stderr or error}") from error
return result.stdout.strip()
def _normalize_git_repo_url(git_repo: str) -> str:
if git_repo.startswith("git@github.com:"):
return "https://github.com/" + git_repo[len("git@github.com:") :]
if git_repo.startswith("ssh://git@github.com/"):
return "https://github.com/" + git_repo[len("ssh://git@github.com/") :]
return git_repo
def _resolve_git_repo(git_repo: str) -> str:
if git_repo.strip():
return _normalize_git_repo_url(git_repo.strip())
env_repo = os.environ.get("BUILDKITE_REPO", "").strip()
if env_repo:
return _normalize_git_repo_url(env_repo)
discovered_repo = _run_git_command(["config", "--get", "remote.origin.url"])
if discovered_repo:
return _normalize_git_repo_url(discovered_repo)
raise RuntimeError("Could not resolve git repo URL. Pass --git-repo or set BUILDKITE_REPO.")
def _resolve_git_commit(git_commit: str) -> str:
if git_commit.strip():
return git_commit.strip()
env_commit = os.environ.get("BUILDKITE_COMMIT", "").strip()
if env_commit:
return env_commit
discovered_commit = _run_git_command(["rev-parse", "HEAD"])
if discovered_commit:
return discovered_commit
raise RuntimeError("Could not resolve git commit. Pass --git-commit or set BUILDKITE_COMMIT.")
def _resolve_pull_request(pr_number: str) -> str:
if pr_number.strip():
return pr_number.strip()
env_pr = os.environ.get("BUILDKITE_PULL_REQUEST", "").strip()
if env_pr:
return env_pr
return "false"
def _resolve_hf_api_key(
hf_api_key: str,
) -> str:
if hf_api_key.strip():
return hf_api_key.strip()
for key in HF_TOKEN_ENV_KEYS:
value = os.environ.get(key, "").strip()
if value:
return value
raise RuntimeError(
"Hugging Face token is required. Set HF_API_KEY, HUGGINGFACE_HUB_TOKEN, or HF_TOKEN; or pass --hf-api-key."
)
def _sanitize_path_fragment(value: str) -> str:
cleaned = re.sub(r"[^A-Za-z0-9_.-]+", "_", value.strip())
return cleaned.strip("._-")
def _resolve_generated_volume_subdir(
requested_subdir: str,
git_commit: str,
) -> str:
if requested_subdir.strip():
normalized = requested_subdir.strip().strip("/")
if not normalized:
raise RuntimeError("generated_volume_subdir must not be empty.")
return normalized
short_commit = _sanitize_path_fragment(git_commit[:12]) or "unknown"
timestamp = datetime.datetime.utcnow().strftime("%Y%m%d_%H%M%S")
return f"{timestamp}_{short_commit}"
def _resolve_output_quality_tier(ssim_full_quality: bool) -> str:
if ssim_full_quality:
return FULL_OUTPUT_QUALITY_TIER
return DEFAULT_OUTPUT_QUALITY_TIER
def _build_generated_volume_relative_path(
*,
generated_volume_subdir: str,
quality_tier: str,
) -> str:
return os.path.join(
RAW_GENERATED_VOLUME_ROOT,
quality_tier,
generated_volume_subdir,
"generated_videos",
)
def _build_local_generated_download_dir(quality_tier: str) -> str:
return os.path.join(".", "generated_videos_modal", quality_tier)
def _print_local_reference_copy_command(quality_tier: str) -> None:
generated_dir = os.path.join(
_build_local_generated_download_dir(quality_tier),
MODAL_DEVICE_REFERENCE_FOLDER,
)
print(
"To update local references from the downloaded Modal outputs, run:\n"
" python fastvideo/tests/ssim/reference_videos_cli.py copy-local "
f"--quality-tier {quality_tier} "
f"--generated-dir {generated_dir} "
f"--device-folder {MODAL_DEVICE_REFERENCE_FOLDER}"
)
def _count_video_files(root: str) -> int:
count = 0
for current_root, _, filenames in os.walk(root):
for filename in filenames:
if filename.lower().endswith((".mp4", ".avi", ".mov", ".mkv", ".webm", ".flv")):
count += 1
return count
def _sync_generated_videos_to_volume(
repo_root: str,
generated_volume_subdir: str,
quality_tier: str,
) -> str | None:
generated_root = os.path.join(
repo_root,
"fastvideo",
"tests",
"ssim",
"generated_videos",
quality_tier,
)
if not os.path.isdir(generated_root):
print(
f"No generated_videos directory found for quality tier {quality_tier}; skipping raw generated video export."
)
return None
relative_dst = _build_generated_volume_relative_path(
generated_volume_subdir=generated_volume_subdir,
quality_tier=quality_tier,
)
absolute_dst = os.path.join("/root/data", relative_dst)
if os.path.exists(absolute_dst):
shutil.rmtree(absolute_dst)
os.makedirs(os.path.dirname(absolute_dst), exist_ok=True)
shutil.copytree(generated_root, absolute_dst)
model_vol.commit()
num_videos = _count_video_files(absolute_dst)
print(f"Raw generated videos exported to Modal volume path: {relative_dst} ({num_videos} video files).")
print(
"Download command:\n"
f" modal volume get hf-model-weights {relative_dst} "
f"{_build_local_generated_download_dir(quality_tier)}"
)
_print_local_reference_copy_command(quality_tier)
return relative_dst
def _extract_required_gpus(filepath: str) -> int:
"""Read REQUIRED_GPUS from a test file. Defaults to 1."""
with open(filepath, encoding="utf-8") as file:
@@ -317,7 +126,10 @@ def _extract_model_ids(filepath: str) -> list[str]:
value_node = None
if isinstance(node, ast.Assign):
target_names = [target.id for target in node.targets if isinstance(target, ast.Name)]
target_names = [
target.id for target in node.targets
if isinstance(target, ast.Name)
]
value_node = node.value
elif isinstance(node, ast.AnnAssign):
if isinstance(node.target, ast.Name):
@@ -326,7 +138,8 @@ def _extract_model_ids(filepath: str) -> list[str]:
if not target_names or value_node is None:
continue
if not any(name.endswith("MODEL_TO_PARAMS") for name in target_names):
if not any(name.endswith("MODEL_TO_PARAMS")
for name in target_names):
continue
if not isinstance(value_node, ast.Dict):
continue
@@ -346,70 +159,35 @@ def _extract_model_ids(filepath: str) -> list[str]:
return unique_model_ids
def _discover_ssim_tasks(
ssim_dir: str,
*,
selected_test_files: set[str] | None = None,
selected_model_ids: set[str] | None = None,
) -> list[SSIMTask]:
def _discover_ssim_tasks(ssim_dir: str) -> list[SSIMTask]:
tasks = []
task_id = 0
selected_test_files = selected_test_files or set()
selected_test_file_names = {os.path.basename(test_file) for test_file in selected_test_files}
selected_model_ids = selected_model_ids or set()
test_files = sorted(glob.glob(os.path.join(ssim_dir, "test_*.py")))
matched_test_files = set()
matched_model_ids = set()
for filepath in test_files:
file_name = os.path.basename(filepath)
rel_path = f"./fastvideo/tests/ssim/{file_name}"
if selected_test_files and (
filepath not in selected_test_files
and rel_path not in selected_test_files
and file_name not in selected_test_file_names
):
continue
matched_test_files.add(file_name)
required_gpus = _extract_required_gpus(filepath)
if required_gpus < 1 or required_gpus > SSIM_NUM_GPUS:
raise ValueError(f"{filepath} requires {required_gpus} GPUs, but scheduler supports up to {SSIM_NUM_GPUS}.")
raise ValueError(
f"{filepath} requires {required_gpus} GPUs, "
f"but scheduler supports up to {SSIM_NUM_GPUS}."
)
rel_path = f"./fastvideo/tests/ssim/{os.path.basename(filepath)}"
model_ids = _extract_model_ids(filepath)
if model_ids:
for model_id in model_ids:
if selected_model_ids and model_id not in selected_model_ids:
continue
matched_model_ids.add(model_id)
tasks.append(
SSIMTask(
task_id=task_id,
test_file=rel_path,
required_gpus=required_gpus,
model_id=model_id,
)
)
task_id += 1
else:
if selected_model_ids:
continue
tasks.append(
SSIMTask(
tasks.append(SSIMTask(
task_id=task_id,
test_file=rel_path,
required_gpus=required_gpus,
)
)
model_id=model_id,
))
task_id += 1
else:
tasks.append(SSIMTask(
task_id=task_id,
test_file=rel_path,
required_gpus=required_gpus,
))
task_id += 1
if selected_test_files:
unmatched_files = sorted(selected_test_file_names - matched_test_files)
if unmatched_files:
raise RuntimeError("Requested SSIM test file(s) not found: " + ", ".join(unmatched_files))
if selected_model_ids:
unmatched_model_ids = sorted(selected_model_ids - matched_model_ids)
if unmatched_model_ids:
raise RuntimeError("Requested SSIM model_id(s) not found: " + ", ".join(unmatched_model_ids))
return sorted(tasks, key=lambda task: task.sort_key)
@@ -429,24 +207,26 @@ def _build_checkout_command(git_commit: str, pr_number: str | None) -> str:
try:
pr_id = int(pr_number)
except ValueError as error:
raise RuntimeError(f"Invalid BUILDKITE_PULL_REQUEST value: {pr_number}") from error
return f"git fetch --prune origin refs/pull/{pr_id}/head && git checkout FETCH_HEAD"
raise RuntimeError(
f"Invalid BUILDKITE_PULL_REQUEST value: {pr_number}"
) from error
return (
"git fetch --prune origin "
f"refs/pull/{pr_id}/head && "
"git checkout FETCH_HEAD"
)
return f"git checkout {shlex.quote(git_commit)}"
def _prepare_ssim_workspace(
*,
git_repo: str,
git_commit: str,
pr_number: str,
hf_api_key: str,
selected_test_files: set[str] | None = None,
selected_model_ids: set[str] | None = None,
) -> tuple[str, list[SSIMTask]]:
def _prepare_ssim_workspace() -> tuple[str, list[SSIMTask]]:
import shlex
import subprocess
if not hf_api_key.strip():
raise RuntimeError("HF API key is required to prepare SSIM workspace.")
git_repo = os.environ.get("BUILDKITE_REPO")
git_commit = os.environ.get("BUILDKITE_COMMIT")
pr_number = os.environ.get("BUILDKITE_PULL_REQUEST")
if not git_repo or not git_commit:
raise RuntimeError("BUILDKITE_REPO and BUILDKITE_COMMIT must be set.")
checkout_command = _build_checkout_command(git_commit, pr_number)
repo_root = "/FastVideo"
@@ -455,14 +235,7 @@ def _prepare_ssim_workspace(
set -euo pipefail
source $HOME/.local/bin/env
source /opt/venv/bin/activate
if [ -d {shlex.quote(repo_root)}/.git ]; then
cd {shlex.quote(repo_root)}
git remote set-url origin {shlex.quote(git_repo)} || true
git fetch --prune origin
else
git clone {shlex.quote(git_repo)} {shlex.quote(repo_root)}
cd {shlex.quote(repo_root)}
fi
cd {shlex.quote(repo_root)}
{checkout_command}
git submodule update --init --recursive
cd fastvideo-kernel
@@ -476,22 +249,17 @@ def _prepare_ssim_workspace(
["/bin/bash", "-lc", command],
capture_output=True,
text=True,
env={
**os.environ,
"HF_API_KEY": hf_api_key,
},
)
if result.returncode != 0:
print(result.stdout)
print(result.stderr)
raise RuntimeError(f"Workspace setup failed with exit code {result.returncode}")
raise RuntimeError(
"Workspace setup failed with "
f"exit code {result.returncode}"
)
ssim_dir = os.path.join(repo_root, "fastvideo", "tests", "ssim")
tasks = _discover_ssim_tasks(
ssim_dir,
selected_test_files=selected_test_files,
selected_model_ids=selected_model_ids,
)
tasks = _discover_ssim_tasks(ssim_dir)
if not tasks:
raise RuntimeError("No SSIM test files found.")
return repo_root, tasks
@@ -503,14 +271,22 @@ def _spawn_ssim_task(
assigned_gpu_ids: list[str],
log_dir: str,
task_index: int,
pytest_extra_args: list[str],
) -> _RunningTask:
import shlex
import subprocess
safe_test_name = re.sub(r"[^A-Za-z0-9_.-]+", "_", task.test_name)
log_path = os.path.join(log_dir, f"{task_index:03d}_{safe_test_name}.log")
pytest_command = shlex.join(["pytest", task.test_file, "-vs", *pytest_extra_args])
command = f"set -euo pipefail && source $HOME/.local/bin/env && source /opt/venv/bin/activate && {pytest_command}"
safe_test_name = re.sub(
r"[^A-Za-z0-9_.-]+", "_", task.test_name
)
log_path = os.path.join(
log_dir, f"{task_index:03d}_{safe_test_name}.log"
)
command = (
"set -euo pipefail && "
"source $HOME/.local/bin/env && "
"source /opt/venv/bin/activate && "
f"pytest {shlex.quote(task.test_file)} -vs"
)
env = os.environ.copy()
env["HF_HOME"] = "/root/data/.cache"
# MultiprocExecutor returns CUDA tensors through mp pipes (CUDA IPC).
@@ -551,7 +327,9 @@ def _finalize_running_task(
) -> None:
running_task.log_handle.close()
available_gpu_ids.extend(running_task.gpu_ids)
available_gpu_ids.sort(key=lambda gpu_id: gpu_order.get(gpu_id, len(gpu_order)))
available_gpu_ids.sort(
key=lambda gpu_id: gpu_order.get(gpu_id, len(gpu_order))
)
results[running_task.task.task_id] = _TaskResult(
task=running_task.task,
status=status,
@@ -624,30 +402,9 @@ def _get_visible_gpu_ids() -> list[str]:
return gpu_ids
def _build_pytest_extra_args(
*,
ssim_full_quality: bool,
ssim_reference_repo: str,
skip_ssim_reference_download: bool,
pytest_k: str,
) -> list[str]:
args = []
if ssim_full_quality:
args.append("--ssim-full-quality")
if ssim_reference_repo.strip():
args.extend(["--ssim-reference-repo", ssim_reference_repo.strip()])
if skip_ssim_reference_download:
args.append("--skip-ssim-reference-download")
if pytest_k.strip():
args.extend(["-k", pytest_k.strip()])
return args
def _schedule_ssim_tasks(
repo_root: str,
tasks: list[SSIMTask],
pytest_extra_args: list[str],
fail_fast: bool = True,
) -> dict[int, _TaskResult]:
import tempfile
import time
@@ -655,10 +412,16 @@ def _schedule_ssim_tasks(
pending_tasks = list(tasks)
running_tasks = []
available_gpu_ids = _get_visible_gpu_ids()
gpu_order = {gpu_id: index for index, gpu_id in enumerate(available_gpu_ids)}
max_required_gpus = max((task.required_gpus for task in pending_tasks), default=0)
gpu_order = {
gpu_id: index for index, gpu_id in enumerate(available_gpu_ids)
}
max_required_gpus = max(
(task.required_gpus for task in pending_tasks), default=0
)
if max_required_gpus > len(available_gpu_ids):
cuda_visible_devices = os.environ.get("CUDA_VISIBLE_DEVICES", "<unset>")
cuda_visible_devices = os.environ.get(
"CUDA_VISIBLE_DEVICES", "<unset>"
)
raise RuntimeError(
"SSIM task requires "
f"{max_required_gpus} GPUs but only "
@@ -680,17 +443,19 @@ def _schedule_ssim_tasks(
break
task = pending_tasks.pop(next_task_index)
assigned_gpu_ids = available_gpu_ids[: task.required_gpus]
del available_gpu_ids[: task.required_gpus]
assigned_gpu_ids = available_gpu_ids[:task.required_gpus]
del available_gpu_ids[:task.required_gpus]
running_task = _spawn_ssim_task(
task=task,
repo_root=repo_root,
assigned_gpu_ids=assigned_gpu_ids,
log_dir=log_dir,
task_index=task.task_id,
pytest_extra_args=pytest_extra_args,
)
print(f"Started {task.test_name} on GPUs {','.join(assigned_gpu_ids)}")
print(
f"Started {task.test_name} on GPUs "
f"{','.join(assigned_gpu_ids)}"
)
running_tasks.append(running_task)
completed_tasks = []
@@ -709,8 +474,11 @@ def _schedule_ssim_tasks(
gpu_order=gpu_order,
)
running_tasks.remove(running_task)
print(f"Finished {running_task.task.test_name} with exit code {returncode}")
if returncode != 0 and fail_fast and not fail_fast_triggered:
print(
f"Finished {running_task.task.test_name} with "
f"exit code {returncode}"
)
if returncode != 0 and not fail_fast_triggered:
fail_fast_triggered = True
if fail_fast_triggered and running_tasks:
@@ -749,18 +517,22 @@ def _collect_task_summaries(
for task in tasks:
result = results[task.task_id]
log_content = None
if result.status == "failed" and result.log_path and os.path.exists(result.log_path):
with open(result.log_path, encoding="utf-8") as f:
if (
result.status == "failed"
and result.log_path
and os.path.exists(result.log_path)
):
with open(
result.log_path, encoding="utf-8"
) as f:
log_content = f.read()
summaries.append(
_TaskSummary(
test_name=task.test_name,
required_gpus=task.required_gpus,
status=result.status,
returncode=result.returncode,
log_content=log_content,
)
)
summaries.append(_TaskSummary(
test_name=task.test_name,
required_gpus=task.required_gpus,
status=result.status,
returncode=result.returncode,
log_content=log_content,
))
return summaries
@@ -778,13 +550,18 @@ def _print_combined_results(
if result is None:
continue
for s in result.task_summaries:
label = f"[P{result.partition_index}] {s.test_name}"
label = (
f"[P{result.partition_index}] "
f"{s.test_name}"
)
if s.status == "passed":
passed.append(label)
elif s.status == "failed":
failed.append(label)
if first_failure is None:
first_failure = (result.partition_index, s)
first_failure = (
result.partition_index, s
)
elif s.status == "terminated":
terminated.append(label)
elif s.status == "skipped":
@@ -808,8 +585,14 @@ def _print_combined_results(
status = "cancelled"
else:
count = len(result.task_summaries)
status = "passed" if result.exit_code == 0 else "failed"
print(f" Partition {i}: {status} ({count} tasks)")
status = (
"passed" if result.exit_code == 0
else "failed"
)
print(
f" Partition {i}: {status} "
f"({count} tasks)"
)
print(f" passed: {len(passed)}")
print(f" failed: {len(failed)}")
print(f" terminated: {len(terminated)}")
@@ -822,72 +605,40 @@ def _print_combined_results(
if skipped:
print(f"Skipped: {', '.join(skipped)}")
has_failures = bool(failed or terminated or skipped or any(r is None for r in partition_results))
has_failures = bool(
failed or terminated or skipped
or any(r is None for r in partition_results)
)
return 1 if has_failures else 0
@app.function(gpu=f"L40S:{SSIM_NUM_GPUS}", **SSIM_COMMON_KWARGS)
def run_ssim_partition(
partition_index: int,
num_partitions: int,
git_repo: str,
git_commit: str,
pr_number: str = "false",
hf_api_key: str = "",
test_files_csv: str = "",
model_ids_csv: str = "",
ssim_full_quality: bool = False,
ssim_reference_repo: str = "",
skip_ssim_reference_download: bool = False,
pytest_k: str = "",
sync_generated_to_volume: bool = False,
generated_volume_subdir: str = "",
fail_fast: bool = True,
) -> _PartitionResult:
selected_test_files = _split_csv_values(test_files_csv)
selected_model_ids = _split_csv_values(model_ids_csv)
repo_root, tasks = _prepare_ssim_workspace(
git_repo=git_repo,
git_commit=git_commit,
pr_number=pr_number,
hf_api_key=hf_api_key,
selected_test_files=selected_test_files,
selected_model_ids=selected_model_ids,
)
partition = _partition_tasks(tasks, partition_index, num_partitions)
repo_root, tasks = _prepare_ssim_workspace()
partition = _partition_tasks(tasks, partition_index)
if not partition:
print(f"Partition {partition_index}: no tasks assigned.")
print(
f"Partition {partition_index}: "
"no tasks assigned."
)
return _PartitionResult(
partition_index=partition_index,
task_summaries=[],
exit_code=0,
)
print(f"Partition {partition_index}: running {len(partition)}/{len(tasks)} tasks")
pytest_extra_args = _build_pytest_extra_args(
ssim_full_quality=ssim_full_quality,
ssim_reference_repo=ssim_reference_repo,
skip_ssim_reference_download=skip_ssim_reference_download,
pytest_k=pytest_k,
print(
f"Partition {partition_index}: running "
f"{len(partition)}/{len(tasks)} tasks"
)
results = _schedule_ssim_tasks(
repo_root,
partition,
pytest_extra_args=pytest_extra_args,
fail_fast=fail_fast,
results = _schedule_ssim_tasks(repo_root, partition)
summaries = _collect_task_summaries(
partition, results
)
has_failures = any(
s.status != "passed" for s in summaries
)
summaries = _collect_task_summaries(partition, results)
has_failures = any(s.status != "passed" for s in summaries)
if sync_generated_to_volume:
quality_tier = _resolve_output_quality_tier(ssim_full_quality)
resolved_subdir = _resolve_generated_volume_subdir(
generated_volume_subdir,
git_commit,
)
_sync_generated_videos_to_volume(
repo_root,
resolved_subdir,
quality_tier,
)
return _PartitionResult(
partition_index=partition_index,
task_summaries=summaries,
@@ -895,91 +646,18 @@ def run_ssim_partition(
)
NUM_PARTITIONS = 2
@app.local_entrypoint()
def run_ssim_tests(
git_repo: str = "",
git_commit: str = "",
pr_number: str = "",
hf_api_key: str = "",
test_files: str = "",
model_ids: str = "",
full_quality: bool = False,
reference_repo: str = "",
skip_reference_download: bool = False,
pytest_k: str = "",
sync_generated_to_volume: bool = False,
generated_volume_subdir: str = "",
no_fail_fast: bool = False,
):
resolved_git_repo = _resolve_git_repo(git_repo)
resolved_git_commit = _resolve_git_commit(git_commit)
resolved_pr_number = _resolve_pull_request(pr_number)
resolved_hf_api_key = _resolve_hf_api_key(hf_api_key)
def run_ssim_tests():
import sys
print(f"Running SSIM on repo: {resolved_git_repo}")
print(f"Using commit: {resolved_git_commit}")
if resolved_pr_number and resolved_pr_number != "false":
print(f"Using PR ref: {resolved_pr_number}")
if test_files.strip():
print(f"Selected test files: {test_files}")
if model_ids.strip():
print(f"Selected model ids: {model_ids}")
if pytest_k.strip():
print(f"Using pytest -k filter: {pytest_k}")
quality_tier = _resolve_output_quality_tier(full_quality)
if sync_generated_to_volume:
resolved_subdir = _resolve_generated_volume_subdir(
generated_volume_subdir,
resolved_git_commit,
)
print(
"Raw generated videos will be saved to Modal volume path: "
f"{_build_generated_volume_relative_path(generated_volume_subdir=resolved_subdir, quality_tier=quality_tier)}"
)
else:
resolved_subdir = ""
common_kwargs = dict(
git_repo=resolved_git_repo,
git_commit=resolved_git_commit,
pr_number=resolved_pr_number,
hf_api_key=resolved_hf_api_key,
test_files_csv=test_files,
model_ids_csv=model_ids,
ssim_full_quality=full_quality,
ssim_reference_repo=reference_repo,
skip_ssim_reference_download=skip_reference_download,
pytest_k=pytest_k,
sync_generated_to_volume=sync_generated_to_volume,
generated_volume_subdir=resolved_subdir,
fail_fast=not no_fail_fast,
)
futures = [
run_ssim_partition.spawn(
partition_index=i,
num_partitions=NUM_PARTITIONS,
**common_kwargs,
)
for i in range(NUM_PARTITIONS)
future_0 = run_ssim_partition.spawn(partition_index=0)
future_1 = run_ssim_partition.spawn(partition_index=1)
results: list[_PartitionResult | None] = [
future_0.get(),
future_1.get(),
]
results: list[_PartitionResult | None] = [f.get() for f in futures]
exit_code = _print_combined_results(results)
if sync_generated_to_volume:
download_src = _build_generated_volume_relative_path(
generated_volume_subdir=resolved_subdir,
quality_tier=quality_tier,
)
local_download_dir = _build_local_generated_download_dir(quality_tier)
print(
"To download raw generated videos locally, run:\n"
f" modal volume get hf-model-weights {download_src} "
f"{local_download_dir}"
)
_print_local_reference_copy_command(quality_tier)
if exit_code != 0:
sys.exit(exit_code)
print("All SSIM tasks passed.")
@@ -3,8 +3,8 @@ from pathlib import Path
from huggingface_hub import snapshot_download
import subprocess
import sys
from fastvideo.tests.ssim.test_inference_similarity import compute_video_ssim_torchvision
import shutil
from fastvideo.tests.utils import compute_video_ssim_torchvision
# Import the training pipeline
sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent))
@@ -181,4 +181,4 @@ def test_e2e_overfit_single_sample():
if __name__ == "__main__":
test_e2e_overfit_single_sample()
test_e2e_overfit_single_sample()
@@ -4,7 +4,7 @@ from huggingface_hub import snapshot_download
import shutil
import subprocess
import sys
from fastvideo.tests.utils import compute_video_ssim_torchvision
from fastvideo.tests.ssim.test_inference_similarity import compute_video_ssim_torchvision
# Import the training pipeline
sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent))
@@ -170,4 +170,4 @@ def test_e2e_overfit_single_sample():
if __name__ == "__main__":
test_e2e_overfit_single_sample()
test_e2e_overfit_single_sample()
@@ -4,7 +4,7 @@ from huggingface_hub import snapshot_download
import shutil
import subprocess
import sys
from fastvideo.tests.utils import compute_video_ssim_torchvision
from fastvideo.tests.ssim.test_inference_similarity import compute_video_ssim_torchvision
# Import the training pipeline
sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent))
@@ -175,4 +175,4 @@ def test_e2e_overfit_single_sample():
if __name__ == "__main__":
test_e2e_overfit_single_sample()
test_e2e_overfit_single_sample()
-55
View File
@@ -1,55 +0,0 @@
# SSIM Directory Guidelines
## Scope
These instructions apply to everything under `fastvideo/tests/ssim/`.
## Purpose
SSIM tests are GPU-backed end-to-end quality regression checks. They generate
videos and compare them against device-specific reference videos.
## Key Files
- `test_*.py`: SSIM regression tests by model/task.
- `conftest.py`: optional filtering via `FASTVIDEO_SSIM_MODEL_ID`.
- `reference_videos/<quality-tier>/<GPU>_reference_videos/`: local cache of
references split by quality tier and GPU type.
- `generated_videos/<quality-tier>/<GPU>_reference_videos/`: local outputs and
`*_ssim.json` artifacts (git-ignored).
- `reference_videos_cli.py`: copy/download/upload/ensure reference videos
(including HF sync).
## Run Commands
- Full SSIM suite: `pytest fastvideo/tests/ssim/ -vs`
- Full SSIM with full-quality params:
`pytest fastvideo/tests/ssim/ -vs --ssim-full-quality`
- Single test file:
`pytest fastvideo/tests/ssim/test_wan_t2v_similarity.py -vs`
- Single model split:
`FASTVIDEO_SSIM_MODEL_ID=<model_id> pytest fastvideo/tests/ssim/test_wan_t2v_similarity.py -vs`
- Modal orchestrator (CI-style scheduling):
`modal run fastvideo/tests/modal/ssim_test.py`
## Authoring Rules
- Name files `test_<feature>_similarity.py`.
- Set `REQUIRED_GPUS = <int>` near the top of each test module.
- For multi-model suites, keep configs in `*_MODEL_TO_PARAMS` dictionaries.
The Modal scheduler auto-discovers these keys and runs one subprocess per
model id.
- Keep runs deterministic when possible (fixed prompts/seeds/frames/backend).
- Persist metrics with `write_ssim_results(...)`.
- Use `pytest.skip(...)` for unsupported hardware, missing assets, or
insufficient GPU count.
## Updating Reference Videos
1. Run the target SSIM test and inspect generated outputs + scores.
2. Update references only for intentional behavior changes.
3. Copy local generated outputs into the target reference folder:
`python fastvideo/tests/ssim/reference_videos_cli.py copy-local ...`
4. Upload updated reference folders to HF:
`python fastvideo/tests/ssim/reference_videos_cli.py upload ...`
5. In the PR, explain why references changed and which GPU type generated
them.
## PR Expectations
- Include exact test commands and pass/fail evidence.
- If thresholds change, include before/after SSIM numbers and rationale.
- If references change, call out the source commit/model/backend.
@@ -0,0 +1,11 @@
{
"mean_ssim": 0.9805062346988254,
"min_ssim": 0.9636241793632507,
"max_ssim": 0.9951534271240234,
"reference_video": "/mnt/fast-disks/hao_lab/loay/FastVideo/fastvideo/tests/ssim/L40S_reference_videos/TurboWan2.2-I2V-A14B-Diffusers/SLA_ATTN/An astronaut hatching from an egg, on the surface of the moon, the darkness and depth of space reali.mp4",
"generated_video": "/mnt/fast-disks/hao_lab/loay/FastVideo/fastvideo/tests/ssim/generated_videos/TurboWan2.2-I2V-A14B-Diffusers/SLA_ATTN/An astronaut hatching from an egg, on the surface of the moon, the darkness and depth of space reali.mp4",
"parameters": {
"num_inference_steps": 4,
"prompt": "An astronaut hatching from an egg, on the surface of the moon, the darkness and depth of space realised in the background. High quality, ultrarealistic detail and breath-taking movie-like camera shot."
}
}

Some files were not shown because too many files have changed in this diff Show More