Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
300eab2b8d | ||
|
|
88a5a933ab | ||
|
|
c591d6d2a6 | ||
|
|
65dff806a8 | ||
|
|
b85f0f4c2a | ||
|
|
76c62d7a00 | ||
|
|
f6e65ff668 | ||
|
|
c220aa8000 | ||
|
|
4713fc17ed | ||
|
|
5789955bbe | ||
|
|
2ad84a3b78 | ||
|
|
12d699cd78 | ||
|
|
34f14ded21 | ||
|
|
71d1ab411f | ||
|
|
805e487773 | ||
|
|
8803b4547e | ||
|
|
3b3806b3f6 | ||
|
|
38d962e89d | ||
|
|
3966a365d0 | ||
|
|
d73fd14af0 | ||
|
|
a87cc89916 | ||
|
|
81fd80c8ee | ||
|
|
ff22439f28 | ||
|
|
de0de04212 | ||
|
|
7f2c3e1f64 | ||
|
|
ab55e57c22 | ||
|
|
46f6b43a53 | ||
|
|
833a33b663 | ||
|
|
9ea1307cd4 | ||
|
|
be35003cb1 | ||
|
|
26bd4db253 | ||
|
|
e294ca011c | ||
|
|
2085a4fc4a | ||
|
|
c0c8e39c04 | ||
|
|
f72618dafb | ||
|
|
b3edfacdd8 | ||
|
|
30129a3350 | ||
|
|
4d49f7b0aa | ||
|
|
71bfc13d75 | ||
|
|
74db6e18d1 | ||
|
|
7d263c6a36 | ||
|
|
454c32d1d1 | ||
|
|
d1240b9238 |
+280
-57
@@ -2,20 +2,218 @@ 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"
|
||||
- github_commit_status:
|
||||
context: "direct-test-completed"
|
||||
if: build.env("TEST_SCOPE") == "direct"
|
||||
|
||||
steps:
|
||||
- label: "pre-commit"
|
||||
command: ".buildkite/scripts/pre_commit.sh"
|
||||
agents:
|
||||
queue: "default"
|
||||
# ============================================================
|
||||
# Direct test: triggered by /test <name> slash command.
|
||||
# Labels match fastcheck/full-suite counterparts so the GitHub
|
||||
# check status overwrites the original failed check.
|
||||
# Only ONE step executes per build (gated by TEST_TYPE).
|
||||
# ============================================================
|
||||
|
||||
- wait
|
||||
# --- Fastcheck-scope direct tests ---
|
||||
- label: ":microscope: Encoder Tests"
|
||||
if: build.env("TEST_SCOPE") == "direct" && build.env("TEST_TYPE") == "encoder"
|
||||
command: "timeout 90m .buildkite/scripts/pr_test.sh"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "default"
|
||||
- label: ":microscope: VAE Tests"
|
||||
if: build.env("TEST_SCOPE") == "direct" && build.env("TEST_TYPE") == "vae"
|
||||
command: "timeout 90m .buildkite/scripts/pr_test.sh"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "default"
|
||||
- label: ":microscope: Transformer Tests"
|
||||
if: build.env("TEST_SCOPE") == "direct" && build.env("TEST_TYPE") == "transformer"
|
||||
command: "timeout 90m .buildkite/scripts/pr_test.sh"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "default"
|
||||
- label: ":microscope: Kernel Tests"
|
||||
if: build.env("TEST_SCOPE") == "direct" && build.env("TEST_TYPE") == "kernel_tests"
|
||||
command: "timeout 90m .buildkite/scripts/pr_test.sh"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "default"
|
||||
- label: ":microscope: Unit Tests"
|
||||
if: build.env("TEST_SCOPE") == "direct" && build.env("TEST_TYPE") == "unit_test"
|
||||
command: "timeout 90m .buildkite/scripts/pr_test.sh"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "default"
|
||||
|
||||
- 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:
|
||||
# --- Full-suite-scope direct tests ---
|
||||
- label: ":bar_chart: SSIM Tests"
|
||||
if: build.env("TEST_SCOPE") == "direct" && build.env("TEST_TYPE") == "ssim"
|
||||
command: "timeout 90m .buildkite/scripts/pr_test.sh"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
- exit_status: 1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "default"
|
||||
- label: ":test_tube: LoRA Inference Tests"
|
||||
if: build.env("TEST_SCOPE") == "direct" && build.env("TEST_TYPE") == "inference_lora"
|
||||
command: "timeout 90m .buildkite/scripts/pr_test.sh"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "default"
|
||||
- label: ":test_tube: Training Tests"
|
||||
if: build.env("TEST_SCOPE") == "direct" && build.env("TEST_TYPE") == "training"
|
||||
command: "timeout 90m .buildkite/scripts/pr_test.sh"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "default"
|
||||
- label: ":test_tube: Distillation DMD Tests"
|
||||
if: build.env("TEST_SCOPE") == "direct" && build.env("TEST_TYPE") == "distillation_dmd"
|
||||
command: "timeout 90m .buildkite/scripts/pr_test.sh"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "default"
|
||||
- label: ":test_tube: Self-Forcing Tests"
|
||||
if: build.env("TEST_SCOPE") == "direct" && build.env("TEST_TYPE") == "self_forcing"
|
||||
command: "timeout 90m .buildkite/scripts/pr_test.sh"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "default"
|
||||
- label: ":test_tube: LoRA Training Tests"
|
||||
if: build.env("TEST_SCOPE") == "direct" && build.env("TEST_TYPE") == "training_lora"
|
||||
command: "timeout 90m .buildkite/scripts/pr_test.sh"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
- exit_status: 1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "default"
|
||||
- label: ":test_tube: Training Tests VSA"
|
||||
if: build.env("TEST_SCOPE") == "direct" && build.env("TEST_TYPE") == "training_vsa"
|
||||
command: "timeout 90m .buildkite/scripts/pr_test.sh"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
- exit_status: 1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "default"
|
||||
- label: ":test_tube: Inference Tests VMoBA"
|
||||
if: build.env("TEST_SCOPE") == "direct" && build.env("TEST_TYPE") == "inference_vmoba"
|
||||
command: "timeout 90m .buildkite/scripts/pr_test.sh"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "default"
|
||||
- label: ":test_tube: Performance Tests"
|
||||
if: build.env("TEST_SCOPE") == "direct" && build.env("TEST_TYPE") == "performance"
|
||||
command: "timeout 90m .buildkite/scripts/pr_test.sh"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "default"
|
||||
- label: ":test_tube: API Server Tests"
|
||||
if: build.env("TEST_SCOPE") == "direct" && build.env("TEST_TYPE") == "api_server"
|
||||
command: "timeout 90m .buildkite/scripts/pr_test.sh"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
limit: 3
|
||||
- exit_status: -1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "default"
|
||||
|
||||
# ============================================================
|
||||
# Fastcheck: Runs on every PR (~10-15 min parallel)
|
||||
# Core component validation: encoders, VAEs, transformers,
|
||||
# CUDA kernels, and unit tests.
|
||||
# ============================================================
|
||||
- label: "Trigger Fastcheck"
|
||||
if: build.env("TEST_SCOPE") == "fastcheck" || build.env("TEST_SCOPE") == null
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 128
|
||||
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:
|
||||
- "fastvideo/models/encoders/**"
|
||||
- "fastvideo/models/loader/**"
|
||||
- "fastvideo/tests/encoders/**"
|
||||
@@ -23,7 +221,7 @@ steps:
|
||||
- "docker/Dockerfile.python3.12"
|
||||
config:
|
||||
command: "timeout 20m .buildkite/scripts/pr_test.sh"
|
||||
label: "Encoder Tests"
|
||||
label: ":microscope: Encoder Tests"
|
||||
env:
|
||||
- TEST_TYPE=encoder
|
||||
agents:
|
||||
@@ -36,7 +234,7 @@ steps:
|
||||
- "docker/Dockerfile.python3.12"
|
||||
config:
|
||||
command: "timeout 20m .buildkite/scripts/pr_test.sh"
|
||||
label: "VAE Tests"
|
||||
label: ":microscope: VAE Tests"
|
||||
env:
|
||||
- TEST_TYPE=vae
|
||||
agents:
|
||||
@@ -51,20 +249,68 @@ steps:
|
||||
- "docker/Dockerfile.python3.12"
|
||||
config:
|
||||
command: "timeout 15m .buildkite/scripts/pr_test.sh"
|
||||
label: "Transformer Tests"
|
||||
label: ":microscope: Transformer Tests"
|
||||
env:
|
||||
- TEST_TYPE=transformer
|
||||
agents:
|
||||
queue: "default"
|
||||
- path:
|
||||
- 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:
|
||||
- "fastvideo/**/*.py"
|
||||
- "pyproject.toml"
|
||||
- "docker/Dockerfile.python3.12"
|
||||
config:
|
||||
command: "timeout 90m .buildkite/scripts/pr_test.sh"
|
||||
label: "SSIM Tests"
|
||||
label: ":bar_chart: SSIM Tests"
|
||||
env:
|
||||
- TEST_TYPE=ssim
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "default"
|
||||
- path:
|
||||
@@ -77,7 +323,7 @@ steps:
|
||||
- "docker/Dockerfile.python3.12"
|
||||
config:
|
||||
command: "timeout 20m .buildkite/scripts/pr_test.sh"
|
||||
label: "LoRA Inference Tests"
|
||||
label: ":test_tube: LoRA Inference Tests"
|
||||
env:
|
||||
- TEST_TYPE=inference_lora
|
||||
agents:
|
||||
@@ -88,7 +334,7 @@ steps:
|
||||
- "docker/Dockerfile.python3.12"
|
||||
config:
|
||||
command: "timeout 15m .buildkite/scripts/pr_test.sh"
|
||||
label: "Training Tests"
|
||||
label: ":test_tube: Training Tests"
|
||||
env:
|
||||
- TEST_TYPE=training
|
||||
agents:
|
||||
@@ -99,7 +345,7 @@ steps:
|
||||
- "docker/Dockerfile.python3.12"
|
||||
config:
|
||||
command: "timeout 15m .buildkite/scripts/pr_test.sh"
|
||||
label: "Distillation DMDTests"
|
||||
label: ":test_tube: Distillation DMD Tests"
|
||||
env:
|
||||
- TEST_TYPE=distillation_dmd
|
||||
agents:
|
||||
@@ -111,7 +357,7 @@ steps:
|
||||
- "docker/Dockerfile.python3.12"
|
||||
config:
|
||||
command: "timeout 15m .buildkite/scripts/pr_test.sh"
|
||||
label: "Self-Forcing Tests"
|
||||
label: ":test_tube: Self-Forcing Tests"
|
||||
env:
|
||||
- TEST_TYPE=self_forcing
|
||||
agents:
|
||||
@@ -122,9 +368,13 @@ steps:
|
||||
- "docker/Dockerfile.python3.12"
|
||||
config:
|
||||
command: "timeout 15m .buildkite/scripts/pr_test.sh"
|
||||
label: "LoRA Training Tests"
|
||||
label: ":test_tube: LoRA Training Tests"
|
||||
env:
|
||||
- TEST_TYPE=training_lora
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 1
|
||||
limit: 2
|
||||
agents:
|
||||
queue: "default"
|
||||
- path:
|
||||
@@ -134,20 +384,15 @@ steps:
|
||||
- "docker/Dockerfile.python3.12"
|
||||
config:
|
||||
command: "timeout 15m .buildkite/scripts/pr_test.sh"
|
||||
label: "Training Tests VSA"
|
||||
label: ":test_tube: Training Tests VSA"
|
||||
env:
|
||||
- TEST_TYPE=training_vsa
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 1
|
||||
limit: 2
|
||||
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"
|
||||
@@ -155,20 +400,9 @@ steps:
|
||||
- "docker/Dockerfile.python3.12"
|
||||
config:
|
||||
command: "timeout 15m .buildkite/scripts/pr_test.sh"
|
||||
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"
|
||||
label: ":test_tube: Inference Tests VMoBA"
|
||||
env:
|
||||
- TEST_TYPE=unit_test
|
||||
- TEST_TYPE=inference_vmoba
|
||||
agents:
|
||||
queue: "default"
|
||||
- path:
|
||||
@@ -184,7 +418,7 @@ steps:
|
||||
- "docker/Dockerfile.python3.12"
|
||||
config:
|
||||
command: "timeout 30m .buildkite/scripts/pr_test.sh"
|
||||
label: "Performance Tests"
|
||||
label: ":test_tube: Performance Tests"
|
||||
env:
|
||||
- TEST_TYPE=performance
|
||||
agents:
|
||||
@@ -197,19 +431,8 @@ steps:
|
||||
- "docker/Dockerfile.python3.12"
|
||||
config:
|
||||
command: "timeout 30m .buildkite/scripts/pr_test.sh"
|
||||
label: "API Server Tests"
|
||||
label: ":test_tube: 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"
|
||||
|
||||
@@ -59,7 +59,11 @@ if [ -z "${TEST_TYPE:-}" ]; then
|
||||
fi
|
||||
log "Test type: $TEST_TYPE"
|
||||
|
||||
MODAL_ENV="BUILDKITE_REPO=$BUILDKITE_REPO BUILDKITE_COMMIT=$BUILDKITE_COMMIT BUILDKITE_PULL_REQUEST=$BUILDKITE_PULL_REQUEST IMAGE_VERSION=$IMAGE_VERSION"
|
||||
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"
|
||||
|
||||
case "$TEST_TYPE" in
|
||||
"encoder")
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
<!--
|
||||
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. -->
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
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)\\]"
|
||||
- "#approved-reviews-by>=1"
|
||||
- check-success~=pre-commit
|
||||
- check-success=fastcheck-passed
|
||||
- check-success=full-suite-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-update when ready
|
||||
conditions:
|
||||
- label=ready
|
||||
- "#approved-reviews-by>=1"
|
||||
- -conflict
|
||||
- -closed
|
||||
- -draft
|
||||
actions:
|
||||
update: {}
|
||||
|
||||
# ============================================================
|
||||
# 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.
|
||||
|
||||
merge_protections_settings:
|
||||
reporting_method: check-runs
|
||||
@@ -1,249 +0,0 @@
|
||||
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()
|
||||
@@ -1,90 +0,0 @@
|
||||
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()
|
||||
@@ -0,0 +1,80 @@
|
||||
name: Aggregate Test Status
|
||||
|
||||
on:
|
||||
status:
|
||||
|
||||
permissions:
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
aggregate:
|
||||
if: >-
|
||||
github.event.context == 'direct-test-completed'
|
||||
&& github.event.state == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check and update aggregate status
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const sha = context.payload.sha;
|
||||
|
||||
const { data } = await github.rest.repos.getCombinedStatusForRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: sha,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const bkStatuses = data.statuses.filter(
|
||||
s => s.context.startsWith('buildkite/ci/')
|
||||
);
|
||||
|
||||
const FASTCHECK_PREFIX = 'buildkite/ci/microscope-';
|
||||
const FULL_SUITE_PREFIXES = [
|
||||
'buildkite/ci/test-tube-',
|
||||
'buildkite/ci/bar-chart-',
|
||||
];
|
||||
|
||||
const fastcheck = bkStatuses.filter(
|
||||
s => s.context.startsWith(FASTCHECK_PREFIX)
|
||||
);
|
||||
const fullSuite = bkStatuses.filter(
|
||||
s => FULL_SUITE_PREFIXES.some(p => s.context.startsWith(p))
|
||||
);
|
||||
|
||||
if (
|
||||
fastcheck.length > 0
|
||||
&& fastcheck.every(s => s.state === 'success')
|
||||
) {
|
||||
core.info(
|
||||
`All ${fastcheck.length} fastcheck tests passed — updating fastcheck-passed`
|
||||
);
|
||||
await github.rest.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha,
|
||||
state: 'success',
|
||||
context: 'fastcheck-passed',
|
||||
description:
|
||||
`All ${fastcheck.length} fastcheck tests passed`,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
fullSuite.length > 0
|
||||
&& fullSuite.every(s => s.state === 'success')
|
||||
) {
|
||||
core.info(
|
||||
`All ${fullSuite.length} full suite tests passed — updating full-suite-passed`
|
||||
);
|
||||
await github.rest.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha,
|
||||
state: 'success',
|
||||
context: 'full-suite-passed',
|
||||
description:
|
||||
`All ${fullSuite.length} full suite tests passed`,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
name: pre-commit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'Git ref to checkout (defaults to github.ref)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
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
|
||||
with:
|
||||
ref: ${{ inputs.ref || '' }}
|
||||
- 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
|
||||
@@ -0,0 +1,271 @@
|
||||
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
|
||||
id: label
|
||||
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;
|
||||
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',
|
||||
});
|
||||
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
||||
core.setOutput('pr_sha', pr.head.sha);
|
||||
core.setOutput('pr_branch', pr.head.ref);
|
||||
core.setOutput('pr_number', String(prNumber));
|
||||
|
||||
- name: Trigger Full Suite
|
||||
if: steps.perm.outputs.has_write == 'true'
|
||||
env:
|
||||
BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_API_TOKEN }}
|
||||
PR_SHA: ${{ steps.label.outputs.pr_sha }}
|
||||
PR_BRANCH: ${{ steps.label.outputs.pr_branch }}
|
||||
PR_NUMBER: ${{ steps.label.outputs.pr_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} (via /merge)" \
|
||||
--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)
|
||||
}
|
||||
}')"
|
||||
|
||||
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);
|
||||
|
||||
- name: React to comment
|
||||
if: steps.perm.outputs.has_write == 'true'
|
||||
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',
|
||||
});
|
||||
|
||||
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
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.issue.number }}/merge
|
||||
|
||||
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: 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
|
||||
}
|
||||
}')"
|
||||
@@ -0,0 +1,83 @@
|
||||
name: Trigger Full Suite
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [labeled, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: full-suite-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
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(try (.env.TEST_SCOPE == "full") catch false) | .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)
|
||||
}
|
||||
}')"
|
||||
@@ -0,0 +1,65 @@
|
||||
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');
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
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!
|
||||
@@ -0,0 +1,56 @@
|
||||
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)
|
||||
@@ -32,7 +32,7 @@ permissions:
|
||||
jobs:
|
||||
build-python-3-10:
|
||||
if: ${{ github.event.inputs.python_3_10 == 'true' }}
|
||||
uses: ./.github/workflows/build-image-template.yml
|
||||
uses: ./.github/workflows/_template-build-image.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/build-image-template.yml
|
||||
uses: ./.github/workflows/_template-build-image.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/build-image-template.yml
|
||||
uses: ./.github/workflows/_template-build-image.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/build-image-template.yml
|
||||
uses: ./.github/workflows/_template-build-image.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
|
||||
@@ -7,14 +7,14 @@ on:
|
||||
- 'docs/**'
|
||||
- 'mkdocs.yml'
|
||||
- 'requirements-mkdocs.txt'
|
||||
- '.github/workflows/docs.yml'
|
||||
- '.github/workflows/infra-docs.yml'
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'mkdocs.yml'
|
||||
- 'requirements-mkdocs.txt'
|
||||
- '.github/workflows/docs.yml'
|
||||
- '.github/workflows/infra-docs.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "ruff",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(.+):(\\d+):(\\d+): (\\w+) (.+)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"code": 4,
|
||||
"message": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,338 +0,0 @@
|
||||
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
|
||||
@@ -1,18 +0,0 @@
|
||||
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
|
||||
+3
-3
@@ -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:
|
||||
@@ -1,94 +0,0 @@
|
||||
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
|
||||
@@ -1,31 +0,0 @@
|
||||
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
|
||||
@@ -1,257 +0,0 @@
|
||||
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/
|
||||
@@ -85,6 +85,10 @@ docs/distillation/examples/
|
||||
dmd_t2v_output/
|
||||
preprocess_output_text/
|
||||
|
||||
# Next.js / Node artifacts under ui/: see ui/.gitignore
|
||||
|
||||
.claude/
|
||||
.codex/
|
||||
.sisyphus/
|
||||
openspec/
|
||||
fastvideo/tests/ssim/reference_videos/**
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
WRN 2026-03-26T13:46:33.469 ?.19646 server_start:193: Failed to start server: operation not permitted: /var/folders/z_/h_6myyk14d1b7z87z3vy4mjh0000gn/T/nvim.dsynkd/iSe0el/nvim.19646.0
|
||||
@@ -18,10 +18,8 @@ exclude: |
|
||||
fastvideo/train\.py|
|
||||
fastvideo/utils/.*|
|
||||
examples/.*|
|
||||
.github/workflows/fastvideo-publish.yml|
|
||||
.github/workflows/sta-publish.yml|
|
||||
.github/workflows/vsa-publish.yml|
|
||||
.github/workflows/build-image-template.yml|
|
||||
.github/workflows/publish-fastvideo.yml|
|
||||
.github/workflows/_template-build-image.yml|
|
||||
docs/source/inference/support_matrix.md
|
||||
)
|
||||
repos:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
3.12
|
||||
@@ -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
@@ -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/docs.yml` workflow.
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
# 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 ──► fix the regression, push, 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 |
|
||||
|
||||
If a Full Suite test fails, check the Buildkite build log for the failing step's output.
|
||||
Fix the regression, push, and comment `/merge` again to re-trigger.
|
||||
|
||||
---
|
||||
|
||||
## 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, the developer fixes the issue, pushes, and comments `/merge`
|
||||
again to re-trigger.
|
||||
|
||||
**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`) |
|
||||
| `/test pre-commit` | Pre-commit checks on PR code | — (runs `ci-precommit.yml` via `workflow_call`) |
|
||||
|
||||
**Re-running failed tests:** When you use `/test <name>` to re-run a specific failed test,
|
||||
the resulting Buildkite check uses the same name as the original (e.g., `/test encoder`
|
||||
creates `buildkite/ci/microscope-encoder-tests`). This overwrites the failed check status.
|
||||
Once all tests in a tier pass, the aggregate status (`fastcheck-passed` or
|
||||
`full-suite-passed`) is automatically updated to `success` by the `ci-aggregate-status.yml`
|
||||
workflow.
|
||||
|
||||
**How aggregate status refresh works:**
|
||||
|
||||
1. `/test <name>` triggers a Buildkite build with `TEST_SCOPE=direct`. The test step uses
|
||||
the same label as its fastcheck/full-suite counterpart, so the resulting GitHub check
|
||||
overwrites the original.
|
||||
2. When the build completes, Buildkite's `notify` posts a `direct-test-completed` commit
|
||||
status. This is the only signal that triggers the aggregate workflow — intermediate step
|
||||
status updates do not trigger it.
|
||||
3. `ci-aggregate-status.yml` fires, calls `getCombinedStatusForRef` to fetch the latest
|
||||
status for every context on that commit (each context returns only its most recent
|
||||
state), groups them by prefix (`microscope-*` → fastcheck, `test-tube-*`/`bar-chart-*`
|
||||
→ full suite), and posts `fastcheck-passed: success` or `full-suite-passed: success` if
|
||||
all entries in the group are `success`.
|
||||
4. Tests that were never triggered (skipped by monorepo-diff) have no status entry and do
|
||||
not block the aggregate.
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
| `ci-aggregate-status.yml` | Any Buildkite commit status update | Checks if all tests in a tier passed; updates `fastcheck-passed` or `full-suite-passed` |
|
||||
| `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 |
|
||||
@@ -1,65 +1,195 @@
|
||||
# RunPod Development Environment
|
||||
|
||||
# 📦 Developing FastVideo on RunPod
|
||||
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.
|
||||
|
||||
You can easily use the FastVideo Pod Template on [RunPod](https://www.runpod.io) for development or experimentation.
|
||||
## Prerequisites
|
||||
|
||||
## Creating a new pod
|
||||
- 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.
|
||||
|
||||
- Make sure you are using the correct RunPod account.
|
||||

|
||||
|
||||
- Use "Additional Filters" to select CUDA 12.8.
|
||||
**2. Filter by CUDA version**
|
||||
|
||||
Use "Additional Filters" to select CUDA 12.8.
|
||||
|
||||

|
||||
|
||||
- Click "Deploy" and Pick a single A40 or RTX 4090 GPU.
|
||||
**3. Select a GPU**
|
||||
|
||||
Click "Deploy" and pick a GPU. See [GPU Recommendations](#gpu-recommendations) below for guidance on which GPU to choose.
|
||||
|
||||

|
||||
|
||||
- Select the "FastVideo" or "fastvideo-dev" Pod Template.
|
||||
**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.
|
||||
|
||||

|
||||
|
||||
- Set the Pod name to "`<name>-<FastVideo>-<date>`".
|
||||
**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.
|
||||
|
||||
- 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.
|
||||

|
||||
|
||||
## 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:
|
||||
Connect with:
|
||||
|
||||
```bash
|
||||
cd /FastVideo
|
||||
git pull
|
||||
ssh root@<pod-ip> -p <port> -i ~/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
Run your development workflows as usual:
|
||||
RunPod also supports VS Code Remote SSH if you prefer an IDE.
|
||||
|
||||
```bash
|
||||
# Run linters
|
||||
pre-commit run --all-files
|
||||
### Custom template (advanced)
|
||||
|
||||
# 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:
|
||||
If you're setting up a pod from scratch instead of the FastVideo template, use this image:
|
||||
|
||||
```
|
||||
ghcr.io/hao-ai-lab/fastvideo/fastvideo-dev:py3.12-latest
|
||||
```
|
||||
|
||||
Paste Container Start Command to support SSH ([RunPod Docs](https://docs.runpod.io/pods/configuration/use-ssh)):
|
||||
And paste this as the Container Start Command to enable 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"
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
# 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, check the Buildkite build log for the failing step. Fix the
|
||||
issue, push, and comment `/merge` again. You can also re-run individual failed tests
|
||||
with `/test <name>` — see below.
|
||||
|
||||
!!! 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
|
||||
/test pre-commit # Pre-commit checks on PR code
|
||||
```
|
||||
|
||||
The workflow reacts with a 🚀 emoji to confirm the command was received.
|
||||
|
||||
When you re-run an individual test with `/test <name>`, the new result overwrites the
|
||||
original failed check (same Buildkite check name). Once all tests in a tier pass, the
|
||||
`fastcheck-passed` or `full-suite-passed` status is automatically updated.
|
||||
|
||||
---
|
||||
|
||||
## 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. Check the failing Buildkite 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.
|
||||
+109
-22
@@ -23,13 +23,18 @@ SSIM tests are located in `fastvideo/tests/ssim`. These tests generate videos us
|
||||
|
||||
```
|
||||
fastvideo/tests/ssim/
|
||||
├── <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>
|
||||
├── reference_videos/
|
||||
│ ├── default/
|
||||
│ │ └── <GPU>_reference_videos/
|
||||
│ │ ├── <Model_Name>/
|
||||
│ │ │ ├── <Backend>/ # e.g., FLASH_ATTN, TORCH_SDPA
|
||||
│ │ │ │ └── <Video_File>
|
||||
│ └── full_quality/
|
||||
│ └── <GPU>_reference_videos/
|
||||
├── test_causal_similarity.py
|
||||
├── test_inference_similarity.py
|
||||
├── update_reference_videos.sh
|
||||
├── test_wan_t2v_similarity.py
|
||||
├── test_wan_i2v_similarity.py
|
||||
├── reference_videos_cli.py
|
||||
└── ...
|
||||
```
|
||||
|
||||
@@ -37,7 +42,7 @@ fastvideo/tests/ssim/
|
||||
|
||||
To add a new SSIM test, follow these steps:
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -81,10 +86,15 @@ 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`.
|
||||
* 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`.
|
||||
* Inspect the generated video to ensure it meets quality expectations.
|
||||
* 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`).
|
||||
* 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`
|
||||
|
||||
### Running Tests Locally
|
||||
|
||||
@@ -96,36 +106,113 @@ pytest fastvideo/tests/ssim/ -vs
|
||||
|
||||
Ensure you have the necessary GPUs available as defined in your test parameters.
|
||||
|
||||
## Modal Workflow
|
||||
## CI Integration
|
||||
|
||||
FastVideo uses [Modal](https://modal.com/) for running tests in a CI environment. The workflow scripts are located in `fastvideo/tests/modal/`.
|
||||
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.
|
||||
|
||||
### `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, the `run_ssim_tests` function in `pr_test.py` currently runs:
|
||||
For SSIM tests, use `fastvideo/tests/modal/ssim_test.py`:
|
||||
|
||||
```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")
|
||||
```bash
|
||||
python -m modal run fastvideo/tests/modal/ssim_test.py::run_ssim_tests
|
||||
```
|
||||
|
||||
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.
|
||||
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
|
||||
```
|
||||
|
||||
### Workflow Scripts
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
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, you'll need to finally manually update the workflow script in Buildkite. Otherwise, a maintainer will help you update.
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,508 @@
|
||||
status_definitions:
|
||||
kept: "Public field remains on a public adapter surface with the same meaning."
|
||||
moved: "Public field remains supported but normalizes into a different nested path."
|
||||
profile_owned: "Public field remains supported only through a model/profile-specific surface."
|
||||
compatibility_only: "Legacy public field remains adapter-only during migration and is not part of the canonical typed schema."
|
||||
private_only: "Field should only be handled by private adapters and is not a public FastVideo compatibility promise."
|
||||
internal_only: "Field is runtime/config plumbing and should not be part of the new public typed inference API."
|
||||
|
||||
surfaces:
|
||||
fastvideo_args:
|
||||
moved:
|
||||
model_path: generator.model_path
|
||||
workload_type: generator.pipeline.workload_type
|
||||
distributed_executor_backend: generator.engine.execution_backend
|
||||
trust_remote_code: generator.trust_remote_code
|
||||
revision: generator.revision
|
||||
num_gpus: generator.engine.num_gpus
|
||||
tp_size: generator.engine.parallelism.tp_size
|
||||
sp_size: generator.engine.parallelism.sp_size
|
||||
hsdp_replicate_dim: generator.engine.parallelism.hsdp_replicate_dim
|
||||
hsdp_shard_dim: generator.engine.parallelism.hsdp_shard_dim
|
||||
dist_timeout: generator.engine.parallelism.dist_timeout
|
||||
lora_path: generator.pipeline.components.lora_path
|
||||
dit_cpu_offload: generator.engine.offload.dit
|
||||
use_fsdp_inference: generator.engine.use_fsdp_inference
|
||||
dit_layerwise_offload: generator.engine.offload.dit_layerwise
|
||||
text_encoder_cpu_offload: generator.engine.offload.text_encoder
|
||||
image_encoder_cpu_offload: generator.engine.offload.image_encoder
|
||||
vae_cpu_offload: generator.engine.offload.vae
|
||||
pin_cpu_memory: generator.engine.offload.pin_cpu_memory
|
||||
enable_torch_compile: generator.engine.compile.enabled
|
||||
torch_compile_kwargs: generator.engine.compile.kwargs
|
||||
disable_autocast: generator.engine.disable_autocast
|
||||
enable_stage_verification: generator.engine.enable_stage_verification
|
||||
prompt_txt: request.inputs.prompt_path
|
||||
override_text_encoder_safetensors: generator.pipeline.components.text_encoder_weights
|
||||
override_text_encoder_quant: generator.engine.quantization.text_encoder_quant
|
||||
override_transformer_cls_name: generator.pipeline.components.override_transformer_cls_name
|
||||
init_weights_from_safetensors: generator.pipeline.components.transformer_weights
|
||||
init_weights_from_safetensors_2: generator.pipeline.components.transformer_2_weights
|
||||
override_pipeline_cls_name: generator.pipeline.components.override_pipeline_cls_name
|
||||
boundary_ratio: request.sampling.boundary_ratio
|
||||
profile_owned:
|
||||
ltx2_vae_tiling: generator.pipeline.profile_overrides.ltx2.vae_tiling
|
||||
ltx2_vae_spatial_tile_size_in_pixels: generator.pipeline.profile_overrides.ltx2.vae.spatial_tile_size_in_pixels
|
||||
ltx2_vae_spatial_tile_overlap_in_pixels: generator.pipeline.profile_overrides.ltx2.vae.spatial_tile_overlap_in_pixels
|
||||
ltx2_vae_temporal_tile_size_in_frames: generator.pipeline.profile_overrides.ltx2.vae.temporal_tile_size_in_frames
|
||||
ltx2_vae_temporal_tile_overlap_in_frames: generator.pipeline.profile_overrides.ltx2.vae.temporal_tile_overlap_in_frames
|
||||
ltx2_initial_latent_path: request.extensions.ltx2.initial_latent_path
|
||||
compatibility_only:
|
||||
mode: "Legacy multi-mode FastVideoArgs switch; typed inference config should not expose execution mode."
|
||||
inference_mode: "Legacy boolean mirror of mode; kept only through adapters while FastVideoArgs remains."
|
||||
lora_nickname: "Legacy adapter-selection surface pending LoRA API cleanup."
|
||||
lora_target_modules: "Legacy LoRA configuration surface pending dedicated component API."
|
||||
output_type: "Legacy output formatting surface pending GenerationResult cleanup."
|
||||
VSA_sparsity: "Model-specific inference optimization not yet represented in the typed public schema."
|
||||
moba_config_path: "Model-specific MoBA optimization surface not yet represented in the typed public schema."
|
||||
master_port: "Executor/bootstrap compatibility field; not part of the canonical inference schema."
|
||||
private_only:
|
||||
ray_placement_group: "Ray deployment-only field."
|
||||
ray_runtime_env: "Ray deployment-only field."
|
||||
internal_only:
|
||||
pipeline_config: "Legacy internal carrier object."
|
||||
preprocess_config: "Legacy preprocess carrier object."
|
||||
moba_config: "Derived runtime config loaded from moba_config_path."
|
||||
model_paths: "Runtime bookkeeping."
|
||||
model_loaded: "Runtime bookkeeping."
|
||||
|
||||
pipeline_config_base:
|
||||
moved:
|
||||
pipeline_config_path: generator.pipeline.components.pipeline_config_path
|
||||
profile_owned:
|
||||
embedded_cfg_scale: generator.pipeline.profile_overrides.embedded_cfg_scale
|
||||
flow_shift: generator.pipeline.profile_overrides.flow_shift
|
||||
flow_shift_sr: generator.pipeline.profile_overrides.flow_shift_sr
|
||||
is_causal: generator.pipeline.profile_overrides.is_causal
|
||||
vae_tiling: generator.pipeline.profile_overrides.vae_tiling
|
||||
vae_sp: generator.pipeline.profile_overrides.vae_sp
|
||||
dmd_denoising_steps: generator.pipeline.profile_overrides.dmd_denoising_steps
|
||||
ti2v_task: generator.pipeline.profile_overrides.ti2v_task
|
||||
boundary_ratio: generator.pipeline.profile_overrides.boundary_ratio
|
||||
compatibility_only:
|
||||
model_path: "Redundant with generator.model_path."
|
||||
disable_autocast: "Duplicated by generator.engine.disable_autocast during migration."
|
||||
dit_precision: "Precision override pending dedicated typed component precision design."
|
||||
upsampler_precision: "Precision override pending dedicated typed component precision design."
|
||||
vae_precision: "Precision override pending dedicated typed component precision design."
|
||||
image_encoder_precision: "Precision override pending dedicated typed component precision design."
|
||||
text_encoder_precisions: "Precision override pending dedicated typed component precision design."
|
||||
internal_only:
|
||||
dit_config: "Legacy internal component config object."
|
||||
upsampler_config: "Legacy internal component config object."
|
||||
vae_config: "Legacy internal component config object."
|
||||
image_encoder_config: "Legacy internal component config object."
|
||||
text_encoder_configs: "Legacy internal component config object."
|
||||
preprocess_text_funcs: "Internal text preprocessing hooks."
|
||||
postprocess_text_funcs: "Internal text postprocessing hooks."
|
||||
|
||||
pipeline_config_extensions:
|
||||
profile_owned:
|
||||
conditioning_strategy:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.cosmos.CosmosConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
max_num_conditional_frames:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.cosmos.CosmosConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
min_num_conditional_frames:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.cosmos.CosmosConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
sigma_conditional:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.cosmos.CosmosConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
sigma_data:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.cosmos.CosmosConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
state_ch:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.cosmos.CosmosConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
state_t:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.cosmos.CosmosConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
text_encoder_class:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.cosmos.CosmosConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
autoregressive_chunk_frames:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
autoregressive_overlap_frames:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
cfg_behavior:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
default_camera_rotation:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
default_movement_distance:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
default_negative_prompt:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
default_trajectory_type:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
filter_points_threshold:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
fps:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
frame_buffer_max:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
moge_model_name:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
noise_aug_strength:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
num_frames:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
offload_moge_after_depth:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
use_moge_depth:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
video_resolution:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CConfig
|
||||
- fastvideo.configs.pipelines.gen3c.Gen3CInferenceConfig
|
||||
text_encoder_crop_start:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.hunyuan15.Hunyuan15I2V480PStepDistilledConfig
|
||||
- fastvideo.configs.pipelines.hunyuan15.Hunyuan15I2V720PConfig
|
||||
- fastvideo.configs.pipelines.hunyuan15.Hunyuan15SR1080PConfig
|
||||
- fastvideo.configs.pipelines.hunyuan15.Hunyuan15T2V480PConfig
|
||||
- fastvideo.configs.pipelines.hunyuan15.Hunyuan15T2V720PConfig
|
||||
- fastvideo.configs.pipelines.hyworld.HYWorldConfig
|
||||
- fastvideo.configs.pipelines.hyworld.Hunyuan15T2V480PConfig
|
||||
text_encoder_max_lengths:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.hunyuan15.Hunyuan15I2V480PStepDistilledConfig
|
||||
- fastvideo.configs.pipelines.hunyuan15.Hunyuan15I2V720PConfig
|
||||
- fastvideo.configs.pipelines.hunyuan15.Hunyuan15SR1080PConfig
|
||||
- fastvideo.configs.pipelines.hunyuan15.Hunyuan15T2V480PConfig
|
||||
- fastvideo.configs.pipelines.hunyuan15.Hunyuan15T2V720PConfig
|
||||
- fastvideo.configs.pipelines.hyworld.HYWorldConfig
|
||||
- fastvideo.configs.pipelines.hyworld.Hunyuan15T2V480PConfig
|
||||
precision:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.lingbotworld.LingBotWorldI2V480PConfig
|
||||
- fastvideo.configs.pipelines.lingbotworld.Wan2_2_I2V_A14B_Config
|
||||
- fastvideo.configs.pipelines.turbodiffusion.TurboDiffusionI2VConfig
|
||||
- fastvideo.configs.pipelines.turbodiffusion.TurboDiffusionI2V_A14B_Config
|
||||
- fastvideo.configs.pipelines.turbodiffusion.TurboDiffusionT2VConfig
|
||||
- fastvideo.configs.pipelines.turbodiffusion.TurboDiffusionT2V_14B_Config
|
||||
- fastvideo.configs.pipelines.turbodiffusion.TurboDiffusionT2V_1_3B_Config
|
||||
- fastvideo.configs.pipelines.wan.FastWan2_1_T2V_480P_Config
|
||||
- fastvideo.configs.pipelines.wan.FastWan2_2_TI2V_5B_Config
|
||||
- fastvideo.configs.pipelines.wan.MatrixGameBaseI2V480PConfig
|
||||
- fastvideo.configs.pipelines.wan.MatrixGameI2V480PConfig
|
||||
- fastvideo.configs.pipelines.wan.SelfForcingWan2_2_T2V480PConfig
|
||||
- fastvideo.configs.pipelines.wan.SelfForcingWanT2V480PConfig
|
||||
- fastvideo.configs.pipelines.wan.WANV2VConfig
|
||||
- fastvideo.configs.pipelines.wan.Wan2_2_I2V_A14B_Config
|
||||
- fastvideo.configs.pipelines.wan.Wan2_2_T2V_A14B_Config
|
||||
- fastvideo.configs.pipelines.wan.Wan2_2_TI2V_5B_Config
|
||||
- fastvideo.configs.pipelines.wan.WanI2V480PConfig
|
||||
- fastvideo.configs.pipelines.wan.WanI2V720PConfig
|
||||
- fastvideo.configs.pipelines.wan.WanT2V480PConfig
|
||||
- fastvideo.configs.pipelines.wan.WanT2V720PConfig
|
||||
warp_denoising_step:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.lingbotworld.LingBotWorldI2V480PConfig
|
||||
- fastvideo.configs.pipelines.lingbotworld.Wan2_2_I2V_A14B_Config
|
||||
- fastvideo.configs.pipelines.turbodiffusion.TurboDiffusionI2VConfig
|
||||
- fastvideo.configs.pipelines.turbodiffusion.TurboDiffusionI2V_A14B_Config
|
||||
- fastvideo.configs.pipelines.turbodiffusion.TurboDiffusionT2VConfig
|
||||
- fastvideo.configs.pipelines.turbodiffusion.TurboDiffusionT2V_14B_Config
|
||||
- fastvideo.configs.pipelines.turbodiffusion.TurboDiffusionT2V_1_3B_Config
|
||||
- fastvideo.configs.pipelines.wan.FastWan2_1_T2V_480P_Config
|
||||
- fastvideo.configs.pipelines.wan.FastWan2_2_TI2V_5B_Config
|
||||
- fastvideo.configs.pipelines.wan.MatrixGameBaseI2V480PConfig
|
||||
- fastvideo.configs.pipelines.wan.MatrixGameI2V480PConfig
|
||||
- fastvideo.configs.pipelines.wan.SelfForcingWan2_2_T2V480PConfig
|
||||
- fastvideo.configs.pipelines.wan.SelfForcingWanT2V480PConfig
|
||||
- fastvideo.configs.pipelines.wan.WANV2VConfig
|
||||
- fastvideo.configs.pipelines.wan.Wan2_2_I2V_A14B_Config
|
||||
- fastvideo.configs.pipelines.wan.Wan2_2_T2V_A14B_Config
|
||||
- fastvideo.configs.pipelines.wan.Wan2_2_TI2V_5B_Config
|
||||
- fastvideo.configs.pipelines.wan.WanI2V480PConfig
|
||||
- fastvideo.configs.pipelines.wan.WanI2V720PConfig
|
||||
- fastvideo.configs.pipelines.wan.WanT2V480PConfig
|
||||
- fastvideo.configs.pipelines.wan.WanT2V720PConfig
|
||||
bsa_cdf_threshold:
|
||||
sources: [fastvideo.configs.pipelines.longcat.LongCatT2V480PConfig, fastvideo.configs.pipelines.longcat.LongCatT2V704PConfig]
|
||||
bsa_chunk_k:
|
||||
sources: [fastvideo.configs.pipelines.longcat.LongCatT2V480PConfig, fastvideo.configs.pipelines.longcat.LongCatT2V704PConfig]
|
||||
bsa_chunk_q:
|
||||
sources: [fastvideo.configs.pipelines.longcat.LongCatT2V480PConfig, fastvideo.configs.pipelines.longcat.LongCatT2V704PConfig]
|
||||
bsa_params:
|
||||
sources: [fastvideo.configs.pipelines.longcat.LongCatT2V480PConfig, fastvideo.configs.pipelines.longcat.LongCatT2V704PConfig]
|
||||
bsa_sparsity:
|
||||
sources: [fastvideo.configs.pipelines.longcat.LongCatT2V480PConfig, fastvideo.configs.pipelines.longcat.LongCatT2V704PConfig]
|
||||
enable_bsa:
|
||||
sources: [fastvideo.configs.pipelines.longcat.LongCatT2V480PConfig, fastvideo.configs.pipelines.longcat.LongCatT2V704PConfig]
|
||||
enable_kv_cache:
|
||||
sources: [fastvideo.configs.pipelines.longcat.LongCatT2V480PConfig, fastvideo.configs.pipelines.longcat.LongCatT2V704PConfig]
|
||||
enhance_hf:
|
||||
sources: [fastvideo.configs.pipelines.longcat.LongCatT2V480PConfig, fastvideo.configs.pipelines.longcat.LongCatT2V704PConfig]
|
||||
offload_kv_cache:
|
||||
sources: [fastvideo.configs.pipelines.longcat.LongCatT2V480PConfig, fastvideo.configs.pipelines.longcat.LongCatT2V704PConfig]
|
||||
t_thresh:
|
||||
sources: [fastvideo.configs.pipelines.longcat.LongCatT2V480PConfig, fastvideo.configs.pipelines.longcat.LongCatT2V704PConfig]
|
||||
use_distill:
|
||||
sources: [fastvideo.configs.pipelines.longcat.LongCatT2V480PConfig, fastvideo.configs.pipelines.longcat.LongCatT2V704PConfig]
|
||||
scheduler_arch:
|
||||
sources: [fastvideo.configs.pipelines.sd35.SD35Config]
|
||||
text_encoder_archs:
|
||||
sources: [fastvideo.configs.pipelines.sd35.SD35Config]
|
||||
tokenizer_archs:
|
||||
sources: [fastvideo.configs.pipelines.sd35.SD35Config]
|
||||
transformer_arch:
|
||||
sources: [fastvideo.configs.pipelines.sd35.SD35Config]
|
||||
vae_arch:
|
||||
sources: [fastvideo.configs.pipelines.sd35.SD35Config]
|
||||
expand_timesteps:
|
||||
sources:
|
||||
- fastvideo.configs.pipelines.wan.FastWan2_2_TI2V_5B_Config
|
||||
- fastvideo.configs.pipelines.wan.Wan2_2_TI2V_5B_Config
|
||||
context_noise:
|
||||
sources: [fastvideo.configs.pipelines.wan.MatrixGameI2V480PConfig]
|
||||
num_frames_per_block:
|
||||
sources: [fastvideo.configs.pipelines.wan.MatrixGameI2V480PConfig]
|
||||
compatibility_only:
|
||||
batch_size: "Gen3C inference-only tuning field pending typed batching design."
|
||||
gradient_checkpointing: "Gen3C inference-only compatibility field pending typed batching design."
|
||||
guidance_scale: "Gen3C pipeline-level default pending profile/default-request cleanup."
|
||||
num_inference_steps: "Gen3C pipeline-level default pending profile/default-request cleanup."
|
||||
internal_only:
|
||||
audio_decoder_config: "Legacy internal component config object."
|
||||
audio_decoder_precision: "Precision override pending dedicated component precision design."
|
||||
vocoder_config: "Legacy internal component config object."
|
||||
vocoder_precision: "Precision override pending dedicated component precision design."
|
||||
|
||||
sampling_param_base:
|
||||
moved:
|
||||
image_path: request.inputs.image_path
|
||||
pil_image: request.inputs.pil_image
|
||||
video_path: request.inputs.video_path
|
||||
mouse_cond: request.inputs.mouse_cond
|
||||
keyboard_cond: request.inputs.keyboard_cond
|
||||
grid_sizes: request.inputs.grid_sizes
|
||||
pose: request.inputs.pose
|
||||
c2ws_plucker_emb: request.inputs.c2ws_plucker_emb
|
||||
refine_from: request.inputs.refine_from
|
||||
stage1_video: request.inputs.stage1_video
|
||||
prompt: request.prompt
|
||||
negative_prompt: request.negative_prompt
|
||||
prompt_path: request.inputs.prompt_path
|
||||
output_path: request.output.output_path
|
||||
output_video_name: request.output.output_video_name
|
||||
num_videos_per_prompt: request.sampling.num_videos_per_prompt
|
||||
seed: request.sampling.seed
|
||||
num_frames: request.sampling.num_frames
|
||||
height: request.sampling.height
|
||||
width: request.sampling.width
|
||||
height_sr: request.sampling.height_sr
|
||||
width_sr: request.sampling.width_sr
|
||||
fps: request.sampling.fps
|
||||
num_inference_steps: request.sampling.num_inference_steps
|
||||
num_inference_steps_sr: request.sampling.num_inference_steps_sr
|
||||
guidance_scale: request.sampling.guidance_scale
|
||||
guidance_rescale: request.sampling.guidance_rescale
|
||||
boundary_ratio: request.sampling.boundary_ratio
|
||||
sigmas: request.sampling.sigmas
|
||||
enable_teacache: request.runtime.enable_teacache
|
||||
save_video: request.output.save_video
|
||||
return_frames: request.output.return_frames
|
||||
return_trajectory_latents: request.runtime.return_trajectory_latents
|
||||
return_trajectory_decoded: request.runtime.return_trajectory_decoded
|
||||
profile_owned:
|
||||
t_thresh: request.stage_overrides.refine.t_thresh
|
||||
spatial_refine_only: request.stage_overrides.refine.spatial_refine_only
|
||||
num_cond_frames: request.stage_overrides.refine.num_cond_frames
|
||||
trajectory_type: request.extensions.gen3c.trajectory_type
|
||||
movement_distance: request.extensions.gen3c.movement_distance
|
||||
camera_rotation: request.extensions.gen3c.camera_rotation
|
||||
internal_only:
|
||||
data_type: "Derived from the request shape and not a public input."
|
||||
|
||||
sampling_param_extensions:
|
||||
moved:
|
||||
guidance_scale_2:
|
||||
target: request.sampling.guidance_scale_2
|
||||
sources:
|
||||
- fastvideo.configs.sample.lingbotworld.LingBotWorld_SamplingParam
|
||||
- fastvideo.configs.sample.lingbotworld.Wan2_2_I2V_A14B_SamplingParam
|
||||
- fastvideo.configs.sample.wan.SelfForcingWan2_2_T2V_A14B_480P_SamplingParam
|
||||
- fastvideo.configs.sample.wan.Wan2_2_I2V_A14B_SamplingParam
|
||||
- fastvideo.configs.sample.wan.Wan2_2_T2V_A14B_SamplingParam
|
||||
profile_owned:
|
||||
action_list:
|
||||
target: request.extensions.hunyuangamecraft.action_list
|
||||
sources:
|
||||
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraftSamplingParam
|
||||
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft65FrameSamplingParam
|
||||
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft129FrameSamplingParam
|
||||
action_speed_list:
|
||||
target: request.extensions.hunyuangamecraft.action_speed_list
|
||||
sources:
|
||||
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraftSamplingParam
|
||||
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft65FrameSamplingParam
|
||||
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft129FrameSamplingParam
|
||||
camera_states:
|
||||
target: request.extensions.hunyuangamecraft.camera_states
|
||||
sources:
|
||||
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraftSamplingParam
|
||||
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft65FrameSamplingParam
|
||||
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft129FrameSamplingParam
|
||||
camera_trajectory:
|
||||
target: request.extensions.hunyuangamecraft.camera_trajectory
|
||||
sources:
|
||||
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraftSamplingParam
|
||||
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft65FrameSamplingParam
|
||||
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft129FrameSamplingParam
|
||||
conditioning_mask:
|
||||
target: request.extensions.hunyuangamecraft.conditioning_mask
|
||||
sources:
|
||||
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraftSamplingParam
|
||||
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft65FrameSamplingParam
|
||||
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft129FrameSamplingParam
|
||||
gt_latents:
|
||||
target: request.extensions.hunyuangamecraft.gt_latents
|
||||
sources:
|
||||
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraftSamplingParam
|
||||
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft65FrameSamplingParam
|
||||
- fastvideo.configs.sample.hunyuangamecraft.HunyuanGameCraft129FrameSamplingParam
|
||||
prompt_attention_mask:
|
||||
target: request.extensions.hyworld.prompt_attention_mask
|
||||
sources: [fastvideo.configs.sample.hyworld.HYWorld_SamplingParam]
|
||||
negative_attention_mask:
|
||||
target: request.extensions.hyworld.negative_attention_mask
|
||||
sources: [fastvideo.configs.sample.hyworld.HYWorld_SamplingParam]
|
||||
ltx2_cfg_scale_audio:
|
||||
target: request.extensions.ltx2.cfg_scale_audio
|
||||
sources: [fastvideo.configs.sample.ltx2.LTX2BaseSamplingParam]
|
||||
ltx2_cfg_scale_video:
|
||||
target: request.extensions.ltx2.cfg_scale_video
|
||||
sources: [fastvideo.configs.sample.ltx2.LTX2BaseSamplingParam]
|
||||
ltx2_modality_scale_audio:
|
||||
target: request.extensions.ltx2.modality_scale_audio
|
||||
sources: [fastvideo.configs.sample.ltx2.LTX2BaseSamplingParam]
|
||||
ltx2_modality_scale_video:
|
||||
target: request.extensions.ltx2.modality_scale_video
|
||||
sources: [fastvideo.configs.sample.ltx2.LTX2BaseSamplingParam]
|
||||
ltx2_rescale_scale:
|
||||
target: request.extensions.ltx2.rescale_scale
|
||||
sources: [fastvideo.configs.sample.ltx2.LTX2BaseSamplingParam]
|
||||
ltx2_stg_blocks_audio:
|
||||
target: request.extensions.ltx2.stg_blocks_audio
|
||||
sources: [fastvideo.configs.sample.ltx2.LTX2BaseSamplingParam]
|
||||
ltx2_stg_blocks_video:
|
||||
target: request.extensions.ltx2.stg_blocks_video
|
||||
sources: [fastvideo.configs.sample.ltx2.LTX2BaseSamplingParam]
|
||||
ltx2_stg_scale_audio:
|
||||
target: request.extensions.ltx2.stg_scale_audio
|
||||
sources: [fastvideo.configs.sample.ltx2.LTX2BaseSamplingParam]
|
||||
ltx2_stg_scale_video:
|
||||
target: request.extensions.ltx2.stg_scale_video
|
||||
sources: [fastvideo.configs.sample.ltx2.LTX2BaseSamplingParam]
|
||||
|
||||
openai_image_request:
|
||||
kept:
|
||||
model: "HTTP adapter model-routing field."
|
||||
response_format: "HTTP adapter response formatting field."
|
||||
output_format: "HTTP adapter output-format field."
|
||||
background: "HTTP adapter output-format field."
|
||||
quality: "Compatibility field currently accepted by the adapter."
|
||||
style: "Compatibility field currently accepted by the adapter."
|
||||
user: "Compatibility field currently accepted by the adapter."
|
||||
moved:
|
||||
prompt: request.prompt
|
||||
n: request.sampling.num_videos_per_prompt
|
||||
size:
|
||||
target: request.sampling.width,height
|
||||
note: "Adapter parses OpenAI size strings as WIDTHxHEIGHT and forwards width then height."
|
||||
num_inference_steps: request.sampling.num_inference_steps
|
||||
guidance_scale: request.sampling.guidance_scale
|
||||
true_cfg_scale: request.sampling.true_cfg_scale
|
||||
seed: request.sampling.seed
|
||||
negative_prompt: request.negative_prompt
|
||||
enable_teacache: request.runtime.enable_teacache
|
||||
|
||||
openai_video_request:
|
||||
kept:
|
||||
model: "HTTP adapter model-routing field."
|
||||
moved:
|
||||
prompt: request.prompt
|
||||
input_reference: request.inputs.image_path
|
||||
reference_url: request.inputs.image_path
|
||||
size:
|
||||
target: request.sampling.width,height
|
||||
note: "Adapter parses OpenAI size strings as WIDTHxHEIGHT and forwards width then height."
|
||||
fps: request.sampling.fps
|
||||
num_frames: request.sampling.num_frames
|
||||
seed: request.sampling.seed
|
||||
num_inference_steps: request.sampling.num_inference_steps
|
||||
guidance_scale: request.sampling.guidance_scale
|
||||
guidance_scale_2: request.sampling.guidance_scale_2
|
||||
true_cfg_scale: request.sampling.true_cfg_scale
|
||||
negative_prompt: request.negative_prompt
|
||||
enable_teacache: request.runtime.enable_teacache
|
||||
output_path: request.output.output_path
|
||||
compatibility_only:
|
||||
seconds:
|
||||
target: request.sampling.num_frames
|
||||
note: "HTTP adapter duration convenience field. If num_frames is omitted, the adapter computes num_frames = fps * seconds."
|
||||
|
||||
cli:
|
||||
notes:
|
||||
- "CLI parity is checked against the actual generate/serve parser dest sets."
|
||||
- "The inventory tracks parser dest names, excluding argparse's implicit help action."
|
||||
- "The refactored inference CLI is config-only: subcommands expose only --config, and any additional CLI input must use dotted override paths."
|
||||
generate:
|
||||
explicit_local_fields:
|
||||
- config
|
||||
expected_dests:
|
||||
- config
|
||||
serve:
|
||||
explicit_local_fields:
|
||||
- config
|
||||
expected_dests:
|
||||
- config
|
||||
@@ -16,7 +16,8 @@ Both models are trained on **61×448×832** resolution but support generating vi
|
||||
First install [VSA](../attention/vsa/index.md). Set `MODEL_BASE` to your own model path and run:
|
||||
|
||||
```bash
|
||||
bash scripts/inference/v1_inference_wan_dmd.sh
|
||||
FASTVIDEO_ATTENTION_BACKEND=VIDEO_SPARSE_ATTN \
|
||||
fastvideo generate --config scripts/inference/inference_wan_VSA_DMD_1_3B.yaml
|
||||
```
|
||||
|
||||
## 🗂️ Dataset
|
||||
|
||||
@@ -455,6 +455,6 @@ User: generator.generate_video(prompt, ...)
|
||||
`fastvideo/pipelines/stages/`, implement `forward()`, optionally
|
||||
implement `verify_input()`/`verify_output()`.
|
||||
|
||||
7. **Verify** — Run `fastvideo generate --model-path <path> --prompt
|
||||
"test" --num-inference-steps 2` to confirm the pipeline loads and
|
||||
generates output.
|
||||
7. **Verify** — Run `fastvideo generate --config <config.yaml>` with a
|
||||
minimal nested config to confirm the pipeline loads and generates
|
||||
output.
|
||||
|
||||
+42
-81
@@ -1,71 +1,29 @@
|
||||
# FastVideo CLI Inference
|
||||
|
||||
The FastVideo CLI exposes the same core inference controls as the Python API.
|
||||
The FastVideo CLI is config-first. Inference runs are driven by a nested JSON or
|
||||
YAML config, with optional dotted-path overrides on the command line. The
|
||||
contract matches training: use an explicit subcommand plus `--config`, then add
|
||||
any dotted overrides you need.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Use either:
|
||||
|
||||
1. `--model-path` + `--prompt`
|
||||
2. `--model-path` + `--prompt-txt` (batch prompts, one line per prompt)
|
||||
3. `--config` (JSON/YAML)
|
||||
|
||||
```bash
|
||||
fastvideo generate --model-path Wan-AI/Wan2.1-T2V-1.3B-Diffusers \
|
||||
--prompt "A cat playing with a ball of yarn"
|
||||
fastvideo generate --config config.yaml
|
||||
fastvideo serve --config serve.yaml
|
||||
```
|
||||
|
||||
```bash
|
||||
fastvideo generate --model-path Wan-AI/Wan2.1-T2V-1.3B-Diffusers \
|
||||
--prompt-txt prompts.txt
|
||||
```
|
||||
|
||||
You cannot provide both `--prompt` and `--prompt-txt` in the same run.
|
||||
|
||||
## View All Arguments
|
||||
|
||||
```bash
|
||||
fastvideo generate --help
|
||||
```
|
||||
|
||||
Arguments come from:
|
||||
The subcommands intentionally expose only `--config`. Any per-run CLI changes
|
||||
must use dotted override paths such as:
|
||||
|
||||
- FastVideo runtime args (`FastVideoArgs`)
|
||||
- Sampling args (`SamplingParam`)
|
||||
- Pipeline config args (`PipelineConfig`)
|
||||
|
||||
## Common Arguments
|
||||
|
||||
### Parallelism
|
||||
|
||||
- `--num-gpus`
|
||||
- `--sp-size`
|
||||
- `--tp-size`
|
||||
|
||||
### Sampling
|
||||
|
||||
- `--num-frames`
|
||||
- `--height` / `--width`
|
||||
- `--num-inference-steps`
|
||||
- `--guidance-scale`
|
||||
- `--seed`
|
||||
- `--negative-prompt`
|
||||
|
||||
### Output
|
||||
|
||||
- `--output-path`
|
||||
- `--save-video` / `--no-save-video`
|
||||
- `--return-frames`
|
||||
|
||||
### Offloading and Performance
|
||||
|
||||
- `--dit-layerwise-offload`
|
||||
- `--use-fsdp-inference`
|
||||
- `--text-encoder-cpu-offload`
|
||||
- `--image-encoder-cpu-offload`
|
||||
- `--vae-cpu-offload`
|
||||
- `--enable-torch-compile`
|
||||
- `--torch-compile-kwargs`
|
||||
- `--generator.engine.num_gpus 2`
|
||||
- `--request.sampling.seed 42`
|
||||
- `--server.port 9000`
|
||||
|
||||
## Using Config Files
|
||||
|
||||
@@ -73,50 +31,53 @@ Arguments come from:
|
||||
fastvideo generate --config config.yaml
|
||||
```
|
||||
|
||||
Config files can be JSON or YAML. CLI flags override config-file values.
|
||||
Config files can be JSON or YAML. Dotted CLI overrides take precedence over
|
||||
config-file values.
|
||||
|
||||
Example `config.yaml`:
|
||||
|
||||
```yaml
|
||||
model_path: "FastVideo/FastHunyuan-diffusers"
|
||||
prompt: "A capybara lounging in a hammock"
|
||||
output_path: "outputs/"
|
||||
num_gpus: 2
|
||||
sp_size: 2
|
||||
tp_size: 1
|
||||
num_frames: 45
|
||||
height: 720
|
||||
width: 1280
|
||||
num_inference_steps: 6
|
||||
seed: 1024
|
||||
dit_precision: "bf16"
|
||||
vae_precision: "fp16"
|
||||
vae_tiling: true
|
||||
vae_sp: true
|
||||
enable_torch_compile: false
|
||||
generator:
|
||||
model_path: FastVideo/FastHunyuan-diffusers
|
||||
engine:
|
||||
num_gpus: 2
|
||||
parallelism:
|
||||
sp_size: 2
|
||||
tp_size: 1
|
||||
request:
|
||||
prompt: A capybara lounging in a hammock
|
||||
sampling:
|
||||
num_frames: 45
|
||||
height: 720
|
||||
width: 1280
|
||||
num_inference_steps: 6
|
||||
seed: 1024
|
||||
output:
|
||||
output_path: outputs/
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Use `dit_precision` / `vae_precision` (not `precision`).
|
||||
- Nested config objects are supported, for example `vae_config` and
|
||||
`dit_config`.
|
||||
- `generator` and `request` are the top-level keys for generation configs.
|
||||
- `serve` configs use `generator`, `server`, and optional `default_request`.
|
||||
- Prompt text files belong under `request.inputs.prompt_path`.
|
||||
|
||||
## Examples
|
||||
|
||||
Simple generation:
|
||||
|
||||
```bash
|
||||
fastvideo generate \
|
||||
--model-path FastVideo/FastHunyuan-diffusers \
|
||||
--prompt "A cat playing with a ball of yarn" \
|
||||
--num-frames 45 --height 720 --width 1280 \
|
||||
--num-inference-steps 6 --seed 1024 \
|
||||
--output-path outputs/
|
||||
fastvideo generate --config config.yaml
|
||||
```
|
||||
|
||||
Config + CLI override:
|
||||
Config + dotted override:
|
||||
|
||||
```bash
|
||||
fastvideo generate --config config.yaml --prompt "A panda skiing at sunset"
|
||||
fastvideo generate --config config.yaml --request.prompt "A panda skiing at sunset"
|
||||
```
|
||||
|
||||
Helper wrapper with positional config path:
|
||||
|
||||
```bash
|
||||
bash scripts/inference/run.sh scripts/inference/inference_wan.yaml
|
||||
```
|
||||
|
||||
@@ -73,32 +73,40 @@ if __name__ == '__main__':
|
||||
|
||||
## JSON/YAML Config Files (CLI)
|
||||
|
||||
The CLI supports `--config` with JSON or YAML. Command-line arguments override
|
||||
config file values.
|
||||
By default, `fastvideo generate` uses `return_frames=false` unless you set
|
||||
`--return-frames` (or `return_frames: true` in config).
|
||||
The inference CLI is config-first. Use an explicit subcommand with `--config`,
|
||||
then apply optional dotted overrides on top, matching the training CLI style.
|
||||
By default, CLI generation uses `return_frames=false` unless you set
|
||||
`request.output.return_frames: true` in config or via a dotted override.
|
||||
|
||||
```bash
|
||||
fastvideo generate --config config.yaml
|
||||
```
|
||||
|
||||
Use CLI argument names as keys (underscore or hyphen is accepted). Example:
|
||||
Example nested config:
|
||||
|
||||
```yaml
|
||||
model_path: "FastVideo/FastHunyuan-diffusers"
|
||||
prompt: "A capybara relaxing in a hammock"
|
||||
num_gpus: 2
|
||||
sp_size: 2
|
||||
num_frames: 45
|
||||
height: 720
|
||||
width: 1280
|
||||
num_inference_steps: 6
|
||||
seed: 1024
|
||||
dit_precision: "bf16"
|
||||
vae_precision: "fp16"
|
||||
vae_tiling: true
|
||||
vae_sp: true
|
||||
enable_torch_compile: false
|
||||
generator:
|
||||
model_path: FastVideo/FastHunyuan-diffusers
|
||||
engine:
|
||||
num_gpus: 2
|
||||
parallelism:
|
||||
sp_size: 2
|
||||
request:
|
||||
prompt: A capybara relaxing in a hammock
|
||||
sampling:
|
||||
num_frames: 45
|
||||
height: 720
|
||||
width: 1280
|
||||
num_inference_steps: 6
|
||||
seed: 1024
|
||||
output:
|
||||
output_path: outputs/
|
||||
```
|
||||
|
||||
Override individual values from the CLI with dotted paths:
|
||||
|
||||
```bash
|
||||
fastvideo generate --config config.yaml --request.sampling.seed 42
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# GEN3C: 3D-Informed Camera-Controlled Video Generation
|
||||
|
||||
[GEN3C](https://arxiv.org/abs/2503.03751) is NVIDIA's Cosmos-7B-based video model for camera-controlled generation from a single image. The FastVideo integration supports the GEN3C I2V workflow, including 3D cache conditioning and tokenizer-based conditioning latents.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Camera trajectory control**: `left/right/up/down/zoom_in/zoom_out/clockwise/counterclockwise`
|
||||
- **3D cache conditioning**: depth prediction -> point cloud cache -> forward warping -> latent conditioning
|
||||
- **Single-image to video generation**: 121-frame generation with camera motion
|
||||
- **Official raw checkpoint conversion**: `model.pt` -> Diffusers/FastVideo layout
|
||||
|
||||
## Model Sources
|
||||
|
||||
- Official raw checkpoint (not Diffusers): `nvidia/GEN3C-Cosmos-7B`
|
||||
- Diffusers-format checkpoint: `FastVideo/GEN3C-Cosmos-7B-Diffusers`
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Install MoGe:
|
||||
|
||||
```bash
|
||||
pip install git+https://github.com/microsoft/MoGe.git
|
||||
```
|
||||
|
||||
- If you hit `ImportError: libGL.so.1` (common on Ubuntu/headless nodes), you can try installing OpenCV runtime libs:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgl1 libglib2.0-0 libsm6 libxext6 libxrender1
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option A: Use Diffusers-format weights directly
|
||||
|
||||
```bash
|
||||
python examples/inference/basic/basic_gen3c.py \
|
||||
--model_path FastVideo/GEN3C-Cosmos-7B-Diffusers \
|
||||
--image_path /path/to/input.png \
|
||||
--prompt "" \
|
||||
--trajectory left \
|
||||
--movement_distance 0.3 \
|
||||
--camera_rotation center_facing \
|
||||
--num_inference_steps 35 \
|
||||
--guidance_scale 1.0 \
|
||||
--output_path outputs_video/gen3c_output.mp4
|
||||
```
|
||||
|
||||
### Option B: Convert official raw checkpoint locally
|
||||
|
||||
1. Download:
|
||||
|
||||
```bash
|
||||
huggingface-cli download nvidia/GEN3C-Cosmos-7B --local-dir official_weights/GEN3C-Cosmos-7B
|
||||
```
|
||||
|
||||
1. Convert:
|
||||
|
||||
```bash
|
||||
python scripts/checkpoint_conversion/convert_gen3c_to_fastvideo.py \
|
||||
--source official_weights/GEN3C-Cosmos-7B/model.pt \
|
||||
--output converted_weights/GEN3C-Cosmos-7B
|
||||
```
|
||||
|
||||
1. Run:
|
||||
|
||||
```bash
|
||||
python examples/inference/basic/basic_gen3c.py \
|
||||
--model_path converted_weights/GEN3C-Cosmos-7B \
|
||||
--image_path /path/to/input.png \
|
||||
--prompt "" \
|
||||
--trajectory left \
|
||||
--movement_distance 0.3 \
|
||||
--camera_rotation center_facing \
|
||||
--num_inference_steps 35 \
|
||||
--guidance_scale 1.0 \
|
||||
--output_path outputs_video/gen3c_output.mp4
|
||||
```
|
||||
|
||||
## FastVideo Defaults
|
||||
|
||||
GEN3C defaults in FastVideo:
|
||||
|
||||
- `height=704`, `width=1280`
|
||||
- `num_frames=121`
|
||||
- `num_inference_steps=35`
|
||||
- `guidance_scale=1.0`
|
||||
- `fps=24`
|
||||
|
||||
These values are defined in:
|
||||
|
||||
- `fastvideo/configs/sample/gen3c.py`
|
||||
- `fastvideo/configs/pipelines/gen3c.py`
|
||||
|
||||
and align with the official GEN3C inference defaults in:
|
||||
|
||||
- `tmp/GEN3C/cosmos_predict1/diffusion/inference/inference_utils.py`
|
||||
|
||||
## Scheduler Note
|
||||
|
||||
The converted GEN3C Diffusers layout may include a FlowMatch scheduler config, but GEN3C denoising uses EDM preconditioning behavior. FastVideo's GEN3C pipeline enforces an EDM scheduler at runtime for parity with official inference behavior.
|
||||
|
||||
Implementation path:
|
||||
|
||||
- `fastvideo/pipelines/basic/gen3c/gen3c_pipeline.py`
|
||||
|
||||
## 3D Cache Conditioning Path
|
||||
|
||||
FastVideo GEN3C conditioning stage performs:
|
||||
|
||||
1. MoGe depth estimation from input image
|
||||
2. 3D cache initialization
|
||||
3. Camera trajectory generation
|
||||
4. Forward rendering of warped frames + masks
|
||||
5. VAE/tokenizer encoding of conditioning buffers
|
||||
6. Denoising with condition mask + condition pose channels
|
||||
|
||||
Main implementation:
|
||||
|
||||
- `fastvideo/pipelines/basic/gen3c/gen3c_pipeline.py`
|
||||
- `fastvideo/pipelines/basic/gen3c/cache_3d.py`
|
||||
- `fastvideo/pipelines/basic/gen3c/depth_estimation.py`
|
||||
- `fastvideo/models/vaes/gen3c_tokenizer_vae.py`
|
||||
|
||||
## References
|
||||
|
||||
- [GEN3C Paper](https://arxiv.org/abs/2503.03751)
|
||||
- [Official Repository](https://github.com/nv-tlabs/GEN3C)
|
||||
- [Official Checkpoint (raw)](https://huggingface.co/nvidia/GEN3C-Cosmos-7B)
|
||||
@@ -73,6 +73,7 @@ pipeline initialization and sampling.
|
||||
| Matrix Game 2.0 Base | `FastVideo/Matrix-Game-2.0-Base-Diffusers` | 352x640 | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ |
|
||||
| Matrix Game 2.0 GTA | `FastVideo/Matrix-Game-2.0-GTA-Diffusers` | 352x640 | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ |
|
||||
| Matrix Game 2.0 TempleRun | `FastVideo/Matrix-Game-2.0-TempleRun-Diffusers` | 352x640 | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ |
|
||||
| GEN3C Cosmos 7B | `FastVideo/GEN3C-Cosmos-7B-Diffusers` | 704px1280p | ❌ | ❌ | ❌ | ⭕ | ⭕ |
|
||||
|
||||
**Note**: Wan2.2 TI2V 5B has some quality issues when performing I2V generation. We are working on fixing this issue.
|
||||
|
||||
@@ -85,6 +86,11 @@ The authoritative source for model-ID recognition is
|
||||
`fastvideo/registry.py`. If a model ID is registered there, FastVideo can
|
||||
resolve default pipeline and sampling configuration for it.
|
||||
|
||||
**Note (GEN3C)**: The official `nvidia/GEN3C-Cosmos-7B` repo provides a raw
|
||||
`model.pt` checkpoint. Use a Diffusers-format repo (for example,
|
||||
`FastVideo/GEN3C-Cosmos-7B-Diffusers`) or convert locally with
|
||||
`scripts/checkpoint_conversion/convert_gen3c_to_fastvideo.py`.
|
||||
|
||||
## Special requirements
|
||||
|
||||
### Sliding Tile Attention
|
||||
|
||||
@@ -28,6 +28,11 @@ For an example running DMD+VSA inference:
|
||||
python examples/inference/basic/basic_dmd.py
|
||||
```
|
||||
|
||||
For the typed config/request path added during the inference API refactor:
|
||||
```
|
||||
python examples/inference/basic/basic_dmd_new_api.py
|
||||
```
|
||||
|
||||
## Basic Walkthrough
|
||||
|
||||
All you need to generate videos using multi-gpus from state-of-the-art diffusion pipelines is the following few lines!
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import os
|
||||
import time
|
||||
|
||||
from fastvideo import VideoGenerator
|
||||
from fastvideo.api import (
|
||||
EngineConfig,
|
||||
GenerationRequest,
|
||||
GeneratorConfig,
|
||||
OffloadConfig,
|
||||
OutputConfig,
|
||||
PipelineSelection,
|
||||
)
|
||||
|
||||
OUTPUT_PATH = "video_samples_dmd2_typed"
|
||||
|
||||
|
||||
def main():
|
||||
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "VIDEO_SPARSE_ATTN"
|
||||
|
||||
model_name = "FastVideo/FastWan2.1-T2V-1.3B-Diffusers"
|
||||
generator_config = GeneratorConfig(
|
||||
model_path=model_name,
|
||||
engine=EngineConfig(
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=False,
|
||||
offload=OffloadConfig(
|
||||
text_encoder=True,
|
||||
pin_cpu_memory=True,
|
||||
dit=False,
|
||||
vae=False,
|
||||
),
|
||||
),
|
||||
# PR 2 still routes a few advanced inference knobs through the
|
||||
# compatibility bridge until they get first-class typed fields.
|
||||
pipeline=PipelineSelection(
|
||||
experimental={
|
||||
"VSA_sparsity": 0.8,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
load_start_time = time.perf_counter()
|
||||
generator = VideoGenerator.from_config(generator_config)
|
||||
load_end_time = time.perf_counter()
|
||||
load_time = load_end_time - load_start_time
|
||||
|
||||
prompt = (
|
||||
"A neon-lit alley in futuristic Tokyo during a heavy rainstorm at night. "
|
||||
"The puddles reflect glowing signs in kanji, advertising ramen, karaoke, "
|
||||
"and VR arcades. A woman in a translucent raincoat walks briskly with an "
|
||||
"LED umbrella. Steam rises from a street food cart, and a cat darts "
|
||||
"across the screen. Raindrops are visible on the camera lens, creating "
|
||||
"a cinematic bokeh effect."
|
||||
)
|
||||
request = GenerationRequest(
|
||||
prompt=prompt,
|
||||
output=OutputConfig(
|
||||
output_path=OUTPUT_PATH,
|
||||
save_video=True,
|
||||
return_frames=False,
|
||||
),
|
||||
)
|
||||
|
||||
start_time = time.perf_counter()
|
||||
result = generator.generate(request)
|
||||
end_time = time.perf_counter()
|
||||
gen_time = end_time - start_time
|
||||
|
||||
prompt2 = (
|
||||
"A majestic lion strides across the golden savanna, its powerful frame "
|
||||
"glistening under the warm afternoon sun. The tall grass ripples gently "
|
||||
"in the breeze, enhancing the lion's commanding presence. The tone is "
|
||||
"vibrant, embodying the raw energy of the wild. Low angle, steady "
|
||||
"tracking shot, cinematic."
|
||||
)
|
||||
request2 = GenerationRequest(
|
||||
prompt=prompt2,
|
||||
output=OutputConfig(
|
||||
output_path=OUTPUT_PATH,
|
||||
save_video=True,
|
||||
return_frames=False,
|
||||
),
|
||||
)
|
||||
|
||||
start_time = time.perf_counter()
|
||||
result2 = generator.generate(request2)
|
||||
end_time = time.perf_counter()
|
||||
gen_time2 = end_time - start_time
|
||||
|
||||
print(f"Time taken to load model: {load_time} seconds")
|
||||
print(f"Time taken to generate video: {gen_time} seconds")
|
||||
print(f"First output written to: {result.video_path}")
|
||||
print(f"Time taken to generate video2: {gen_time2} seconds")
|
||||
print(f"Second output written to: {result2.video_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
GEN3C: 3D-aware camera-controlled video generation.
|
||||
|
||||
This example generates a video from a single input image with camera control.
|
||||
The pipeline uses MoGe depth estimation, 3D point cloud forward warping,
|
||||
and the GEN3C diffusion model.
|
||||
|
||||
Requirements:
|
||||
1. Install MoGe:
|
||||
pip install git+https://github.com/microsoft/MoGe.git
|
||||
If you hit `ImportError: libGL.so.1`, install:
|
||||
sudo apt-get update && sudo apt-get install -y libgl1 libglib2.0-0 libsm6 libxext6 libxrender1
|
||||
2. Download and convert weights:
|
||||
huggingface-cli download nvidia/GEN3C-Cosmos-7B --local-dir official_weights/GEN3C-Cosmos-7B
|
||||
python scripts/checkpoint_conversion/convert_gen3c_to_fastvideo.py \
|
||||
--source ./official_weights/GEN3C-Cosmos-7B/model.pt \
|
||||
--output ./converted_weights/GEN3C-Cosmos-7B \
|
||||
--components-source nvidia/Cosmos-Predict2-2B-Video2World
|
||||
3. Provide an input image for 3D-conditioned generation.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
from fastvideo import VideoGenerator
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="GEN3C video generation")
|
||||
parser.add_argument("--model_path",
|
||||
type=str,
|
||||
default="converted_weights/GEN3C-Cosmos-7B")
|
||||
parser.add_argument("--image_path",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Input image for 3D cache conditioning")
|
||||
parser.add_argument("--prompt",
|
||||
type=str,
|
||||
default="A slow camera pan over a sunlit landscape.")
|
||||
parser.add_argument(
|
||||
"--negative_prompt",
|
||||
type=str,
|
||||
default=(
|
||||
"The video captures a series of frames showing ugly scenes, static with no motion, motion blur, "
|
||||
"over-saturation, shaky footage, low resolution, grainy texture, pixelated images, poorly lit areas, "
|
||||
"underexposed and overexposed scenes, poor color balance, washed out colors, choppy sequences, "
|
||||
"jerky movements, low frame rate, artifacting, color banding, unnatural transitions, outdated special "
|
||||
"effects, fake elements, unconvincing visuals, poorly edited content, jump cuts, visual noise, and "
|
||||
"flickering. Overall, the video is of poor quality."
|
||||
),
|
||||
)
|
||||
parser.add_argument("--trajectory",
|
||||
type=str,
|
||||
default="left",
|
||||
choices=[
|
||||
"left", "right", "up", "down", "zoom_in",
|
||||
"zoom_out", "clockwise", "counterclockwise", "none"
|
||||
])
|
||||
parser.add_argument("--movement_distance", type=float, default=0.3)
|
||||
parser.add_argument("--camera_rotation",
|
||||
type=str,
|
||||
default="center_facing",
|
||||
choices=[
|
||||
"center_facing", "no_rotation",
|
||||
"trajectory_aligned"
|
||||
])
|
||||
parser.add_argument("--height", type=int, default=704)
|
||||
parser.add_argument("--width", type=int, default=1280)
|
||||
parser.add_argument("--num_frames", type=int, default=121)
|
||||
parser.add_argument("--num_inference_steps", type=int, default=35)
|
||||
parser.add_argument("--guidance_scale", type=float, default=1.0)
|
||||
parser.add_argument("--output_path",
|
||||
type=str,
|
||||
default="outputs_video/gen3c.mp4")
|
||||
parser.add_argument("--seed", type=int, default=42)
|
||||
args = parser.parse_args()
|
||||
|
||||
generator = VideoGenerator.from_pretrained(
|
||||
args.model_path,
|
||||
num_gpus=1,
|
||||
use_fsdp_inference=False,
|
||||
dit_cpu_offload=False,
|
||||
vae_cpu_offload=True,
|
||||
text_encoder_cpu_offload=True,
|
||||
pin_cpu_memory=True,
|
||||
)
|
||||
|
||||
video = generator.generate_video(
|
||||
args.prompt,
|
||||
negative_prompt=args.negative_prompt,
|
||||
image_path=args.image_path,
|
||||
trajectory_type=args.trajectory,
|
||||
movement_distance=args.movement_distance,
|
||||
camera_rotation=args.camera_rotation,
|
||||
height=args.height,
|
||||
width=args.width,
|
||||
num_frames=args.num_frames,
|
||||
num_inference_steps=args.num_inference_steps,
|
||||
guidance_scale=args.guidance_scale,
|
||||
fps=24,
|
||||
seed=args.seed,
|
||||
output_path=args.output_path,
|
||||
save_video=True,
|
||||
)
|
||||
|
||||
generator.shutdown()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,93 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,21 @@
|
||||
# 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/`.
|
||||
@@ -0,0 +1,72 @@
|
||||
# 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
|
||||
@@ -0,0 +1,68 @@
|
||||
# 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
|
||||
@@ -1,8 +0,0 @@
|
||||
# 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
|
||||
@@ -0,0 +1,78 @@
|
||||
# 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
|
||||
@@ -5,12 +5,12 @@
|
||||
# bash examples/train/run.sh <config.yaml> [--dotted.key value ...]
|
||||
#
|
||||
# Examples:
|
||||
# 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 \
|
||||
# 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 \
|
||||
# --training.distributed.num_gpus 4 \
|
||||
# --training.optimizer.learning_rate 1e-5
|
||||
# bash examples/train/run.sh examples/train/distill_wan2.1_t2v_1.3B_dmd2.yaml \
|
||||
# bash examples/train/run.sh examples/train/configs/distribution_matching/wan/dmd2_t2v.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).
|
||||
|
||||
@@ -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/distill_wan2.1_t2v_1.3B_dmd2.yaml 4 \
|
||||
# bash examples/train/run_slurm.sh examples/train/configs/distribution_matching/wan/dmd2_t2v.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
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# 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
-1
@@ -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/configs/kdtest/decode_kd_cache.py \
|
||||
python examples/train/scenario/ode_init_self_forcing_wan_causal/decode_kd_cache.py \
|
||||
--cache_dir data/kd_test_cache_small \
|
||||
--out_dir data/kd_test_cache_small/video \
|
||||
--fps 16
|
||||
+2
-2
@@ -5,7 +5,7 @@
|
||||
#
|
||||
# Step 1 — Run KD training:
|
||||
# torchrun --nproc_per_node=1 -m fastvideo.train.entrypoint.train \
|
||||
# --config examples/train/configs/kdtest/kd_causal.yaml
|
||||
# --config examples/train/scenario/ode_init_self_forcing_wan_causal/step1_kd.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/configs/kdtest/self_forcing_causal.yaml
|
||||
# --config examples/train/scenario/ode_init_self_forcing_wan_causal/step3_self_forcing.yaml
|
||||
#
|
||||
# Success criteria:
|
||||
# - Cache generates 16 .pt files under data/kd_test_cache/samples/
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/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."
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
# Self-Forcing Test: Causal Wan 1.3B
|
||||
#
|
||||
# Smoke-test for SelfForcingMethod on a tiny 16-sample dataset.
|
||||
# Run this AFTER kd_causal.yaml + dcp_to_diffusers export.
|
||||
# Run this AFTER step1_kd.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/configs/kdtest/self_forcing_causal.yaml
|
||||
# --config examples/train/scenario/ode_init_self_forcing_wan_causal/step3_self_forcing.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/configs/kdtest/validation.json
|
||||
dataset_file: examples/train/scenario/ode_init_self_forcing_wan_causal/validation.json
|
||||
every_steps: 20
|
||||
sampling_steps: [4]
|
||||
sampling_timesteps: [1000, 750, 500, 250]
|
||||
@@ -0,0 +1,19 @@
|
||||
# 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
|
||||
```
|
||||
@@ -10,6 +10,35 @@ set -ex
|
||||
|
||||
echo "Building fastvideo-kernel..."
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Neutralise conda-injected compiler toolchains.
|
||||
#
|
||||
# Conda compiler packages (gcc_linux-aarch64, gxx_linux-64, etc.) set
|
||||
# CMAKE_ARGS, CFLAGS, CXXFLAGS, and LDFLAGS on activation. When multiple
|
||||
# toolchains are installed the variables can reference a *cross*-compiler
|
||||
# that doesn't match the host (e.g. aarch64-conda-linux-gnu-c++ on x86_64).
|
||||
# Even when the correct toolchain is active, the flags it injects
|
||||
# (-march=nocona, -mtune=haswell, …) can conflict with nvcc's host-compiler
|
||||
# expectations. Clear them so CMake discovers the system compiler instead.
|
||||
# ---------------------------------------------------------------------------
|
||||
if [[ -n "${CONDA_PREFIX:-}" ]]; then
|
||||
_need_clean=0
|
||||
# Detect conda cross-compiler that doesn't match the host.
|
||||
_host_arch="$(uname -m)"
|
||||
if [[ "${CXX:-}" == *"conda"* ]] || [[ "${CC:-}" == *"conda"* ]]; then
|
||||
_need_clean=1
|
||||
fi
|
||||
if [[ "${CMAKE_ARGS:-}" == *"conda"* ]]; then
|
||||
_need_clean=1
|
||||
fi
|
||||
if (( _need_clean )); then
|
||||
echo "NOTE: Clearing conda-injected compiler settings (CC/CXX/CMAKE_ARGS/CFLAGS/...)"
|
||||
echo " to use the system compiler for CUDA extension builds."
|
||||
unset CC CXX CMAKE_ARGS CFLAGS CXXFLAGS LDFLAGS
|
||||
fi
|
||||
unset _need_clean _host_arch
|
||||
fi
|
||||
|
||||
# Ensure submodules are initialized if needed (tk)
|
||||
git submodule update --init --recursive
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"torch>=2.5.0",
|
||||
"triton>=2.0.0",
|
||||
"triton>=2.0.0; sys_platform == 'linux'",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from fastvideo.api.schema import (
|
||||
CompileConfig,
|
||||
ComponentConfig,
|
||||
ContinuationState,
|
||||
EngineConfig,
|
||||
GenerationPlan,
|
||||
GenerationRequest,
|
||||
GeneratorConfig,
|
||||
InputConfig,
|
||||
OffloadConfig,
|
||||
OutputConfig,
|
||||
ParallelismConfig,
|
||||
PipelineSelection,
|
||||
PlannedStage,
|
||||
QuantizationConfig,
|
||||
RequestRuntimeConfig,
|
||||
RunConfig,
|
||||
SamplingConfig,
|
||||
ServeConfig,
|
||||
ServerConfig,
|
||||
)
|
||||
from fastvideo.api.errors import ConfigValidationError
|
||||
from fastvideo.api.overrides import apply_overrides, parse_cli_overrides
|
||||
from fastvideo.api.parser import (
|
||||
config_to_dict,
|
||||
load_config,
|
||||
load_raw_config,
|
||||
load_run_config,
|
||||
load_serve_config,
|
||||
parse_config,
|
||||
)
|
||||
from fastvideo.api.results import GenerationResult
|
||||
|
||||
__all__ = [
|
||||
"CompileConfig",
|
||||
"ComponentConfig",
|
||||
"ContinuationState",
|
||||
"ConfigValidationError",
|
||||
"EngineConfig",
|
||||
"GenerationResult",
|
||||
"GenerationPlan",
|
||||
"GenerationRequest",
|
||||
"GeneratorConfig",
|
||||
"InputConfig",
|
||||
"OffloadConfig",
|
||||
"OutputConfig",
|
||||
"ParallelismConfig",
|
||||
"PipelineSelection",
|
||||
"PlannedStage",
|
||||
"QuantizationConfig",
|
||||
"RequestRuntimeConfig",
|
||||
"RunConfig",
|
||||
"SamplingConfig",
|
||||
"ServeConfig",
|
||||
"ServerConfig",
|
||||
"apply_overrides",
|
||||
"config_to_dict",
|
||||
"load_config",
|
||||
"load_raw_config",
|
||||
"load_run_config",
|
||||
"load_serve_config",
|
||||
"parse_cli_overrides",
|
||||
"parse_config",
|
||||
]
|
||||
@@ -0,0 +1,507 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from copy import deepcopy
|
||||
from dataclasses import fields, is_dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastvideo.api.overrides import apply_overrides, parse_cli_overrides
|
||||
from fastvideo.api.parser import config_to_dict, load_raw_config, parse_config
|
||||
from fastvideo.api.request_metadata import (
|
||||
EXPLICIT_REQUEST_ATTR,
|
||||
bind_generation_request_raw,
|
||||
refresh_generation_request_raw,
|
||||
)
|
||||
from fastvideo.api.schema import (
|
||||
GenerationRequest,
|
||||
GeneratorConfig,
|
||||
InputConfig,
|
||||
OutputConfig,
|
||||
RequestRuntimeConfig,
|
||||
SamplingConfig,
|
||||
)
|
||||
from fastvideo.configs.sample import SamplingParam
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.utils import shallow_asdict
|
||||
|
||||
_INPUT_FIELD_NAMES = {field.name for field in fields(InputConfig)}
|
||||
_SAMPLING_FIELD_NAMES = {field.name for field in fields(SamplingConfig)}
|
||||
_RUNTIME_FIELD_NAMES = {field.name for field in fields(RequestRuntimeConfig)}
|
||||
_OUTPUT_FIELD_NAMES = {field.name for field in fields(OutputConfig)}
|
||||
_MISSING = object()
|
||||
_LEGACY_REQUEST_ALIASES = {
|
||||
"neg_prompt": "negative_prompt",
|
||||
}
|
||||
_REQUEST_PIPELINE_OVERRIDE_FIELDS = frozenset({
|
||||
"embedded_cfg_scale",
|
||||
})
|
||||
|
||||
|
||||
def normalize_generator_config(config: GeneratorConfig | Mapping[str, Any], ) -> GeneratorConfig:
|
||||
if isinstance(config, GeneratorConfig):
|
||||
return config
|
||||
return parse_config(GeneratorConfig, config)
|
||||
|
||||
|
||||
def load_generator_config_from_file(
|
||||
path: str | Path,
|
||||
overrides: list[str] | Mapping[str, Any] | None = None,
|
||||
) -> GeneratorConfig:
|
||||
raw = load_raw_config(path)
|
||||
normalized_overrides = _normalize_overrides(overrides)
|
||||
|
||||
if _looks_like_run_or_serve_config(raw):
|
||||
if normalized_overrides:
|
||||
raw = apply_overrides(raw, normalized_overrides)
|
||||
return parse_config(GeneratorConfig, raw["generator"])
|
||||
|
||||
if normalized_overrides:
|
||||
adjusted = normalized_overrides
|
||||
if all(key.startswith("generator.") for key in adjusted):
|
||||
adjusted = {key[len("generator."):]: value for key, value in adjusted.items()}
|
||||
raw = apply_overrides(raw, adjusted)
|
||||
|
||||
return parse_config(GeneratorConfig, raw)
|
||||
|
||||
|
||||
def legacy_from_pretrained_to_config(
|
||||
model_path: str,
|
||||
kwargs: Mapping[str, Any],
|
||||
) -> GeneratorConfig:
|
||||
raw: dict[str, Any] = {"model_path": model_path}
|
||||
engine: dict[str, Any] = {}
|
||||
parallelism: dict[str, Any] = {}
|
||||
offload: dict[str, Any] = {}
|
||||
compile_config: dict[str, Any] = {}
|
||||
pipeline: dict[str, Any] = {}
|
||||
components: dict[str, Any] = {}
|
||||
quantization: dict[str, Any] = {}
|
||||
experimental: dict[str, Any] = {}
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if key == "revision":
|
||||
raw["revision"] = value
|
||||
elif key == "trust_remote_code":
|
||||
raw["trust_remote_code"] = value
|
||||
elif key == "num_gpus":
|
||||
engine["num_gpus"] = value
|
||||
elif key == "distributed_executor_backend":
|
||||
engine["execution_backend"] = value
|
||||
elif key in {"tp_size", "sp_size", "hsdp_replicate_dim", "hsdp_shard_dim", "dist_timeout"}:
|
||||
parallelism[key] = value
|
||||
elif key == "dit_cpu_offload":
|
||||
offload["dit"] = value
|
||||
elif key == "dit_layerwise_offload":
|
||||
offload["dit_layerwise"] = value
|
||||
elif key == "text_encoder_cpu_offload":
|
||||
offload["text_encoder"] = value
|
||||
elif key == "image_encoder_cpu_offload":
|
||||
offload["image_encoder"] = value
|
||||
elif key == "vae_cpu_offload":
|
||||
offload["vae"] = value
|
||||
elif key == "pin_cpu_memory":
|
||||
offload["pin_cpu_memory"] = value
|
||||
elif key == "enable_torch_compile":
|
||||
compile_config["enabled"] = value
|
||||
elif key == "torch_compile_kwargs":
|
||||
compile_config["kwargs"] = deepcopy(value)
|
||||
elif key in {"enable_stage_verification", "use_fsdp_inference", "disable_autocast"}:
|
||||
engine[key] = value
|
||||
elif key == "override_text_encoder_quant":
|
||||
quantization["text_encoder_quant"] = value
|
||||
elif key == "workload_type":
|
||||
pipeline["workload_type"] = value
|
||||
elif key == "lora_path":
|
||||
components["lora_path"] = value
|
||||
elif key == "override_pipeline_cls_name":
|
||||
components["override_pipeline_cls_name"] = value
|
||||
elif key == "override_transformer_cls_name":
|
||||
components["override_transformer_cls_name"] = value
|
||||
elif key == "pipeline_config":
|
||||
if isinstance(value, str):
|
||||
components["pipeline_config_path"] = value
|
||||
else:
|
||||
experimental[key] = deepcopy(value)
|
||||
elif key == "override_text_encoder_safetensors":
|
||||
components["text_encoder_weights"] = value
|
||||
elif key == "init_weights_from_safetensors":
|
||||
components["transformer_weights"] = value
|
||||
elif key == "init_weights_from_safetensors_2":
|
||||
components["transformer_2_weights"] = value
|
||||
else:
|
||||
experimental[key] = deepcopy(value)
|
||||
|
||||
if parallelism:
|
||||
engine["parallelism"] = parallelism
|
||||
if offload:
|
||||
engine["offload"] = offload
|
||||
if compile_config:
|
||||
engine["compile"] = compile_config
|
||||
if quantization:
|
||||
engine["quantization"] = quantization
|
||||
if engine:
|
||||
raw["engine"] = engine
|
||||
|
||||
if components:
|
||||
pipeline["components"] = components
|
||||
if experimental:
|
||||
pipeline["experimental"] = experimental
|
||||
if pipeline:
|
||||
raw["pipeline"] = pipeline
|
||||
|
||||
return parse_config(GeneratorConfig, raw)
|
||||
|
||||
|
||||
def generator_config_to_fastvideo_args(config: GeneratorConfig | Mapping[str, Any], ) -> FastVideoArgs:
|
||||
normalized = normalize_generator_config(config)
|
||||
unsupported = []
|
||||
if normalized.pipeline.profile is not None:
|
||||
unsupported.append("pipeline.profile")
|
||||
if normalized.pipeline.profile_version is not None:
|
||||
unsupported.append("pipeline.profile_version")
|
||||
if normalized.pipeline.components.config_root is not None:
|
||||
unsupported.append("pipeline.components.config_root")
|
||||
if normalized.pipeline.components.vae_weights is not None:
|
||||
unsupported.append("pipeline.components.vae_weights")
|
||||
if normalized.pipeline.components.upsampler_weights is not None:
|
||||
unsupported.append("pipeline.components.upsampler_weights")
|
||||
if unsupported:
|
||||
joined = ", ".join(unsupported)
|
||||
raise NotImplementedError(f"VideoGenerator compatibility adapter does not support {joined} yet")
|
||||
|
||||
engine = normalized.engine
|
||||
kwargs: dict[str, Any] = {
|
||||
"model_path": normalized.model_path,
|
||||
"revision": normalized.revision,
|
||||
"trust_remote_code": normalized.trust_remote_code,
|
||||
"num_gpus": engine.num_gpus,
|
||||
"distributed_executor_backend": engine.execution_backend,
|
||||
"tp_size": engine.parallelism.tp_size,
|
||||
"sp_size": engine.parallelism.sp_size,
|
||||
"hsdp_replicate_dim": engine.parallelism.hsdp_replicate_dim,
|
||||
"hsdp_shard_dim": engine.parallelism.hsdp_shard_dim,
|
||||
"dist_timeout": engine.parallelism.dist_timeout,
|
||||
"dit_cpu_offload": engine.offload.dit,
|
||||
"dit_layerwise_offload": engine.offload.dit_layerwise,
|
||||
"text_encoder_cpu_offload": engine.offload.text_encoder,
|
||||
"image_encoder_cpu_offload": engine.offload.image_encoder,
|
||||
"vae_cpu_offload": engine.offload.vae,
|
||||
"pin_cpu_memory": engine.offload.pin_cpu_memory,
|
||||
"enable_torch_compile": engine.compile.enabled,
|
||||
"torch_compile_kwargs": deepcopy(engine.compile.kwargs),
|
||||
"enable_stage_verification": engine.enable_stage_verification,
|
||||
"use_fsdp_inference": engine.use_fsdp_inference,
|
||||
"disable_autocast": engine.disable_autocast,
|
||||
}
|
||||
if normalized.pipeline.workload_type is not None:
|
||||
kwargs["workload_type"] = normalized.pipeline.workload_type
|
||||
|
||||
quantization = engine.quantization
|
||||
if quantization is not None and quantization.text_encoder_quant is not None:
|
||||
kwargs["override_text_encoder_quant"] = quantization.text_encoder_quant
|
||||
if quantization is not None and quantization.transformer_quant is not None:
|
||||
kwargs["transformer_quant"] = quantization.transformer_quant
|
||||
|
||||
components = normalized.pipeline.components
|
||||
if components.pipeline_config_path is not None:
|
||||
kwargs["pipeline_config"] = components.pipeline_config_path
|
||||
if components.lora_path is not None:
|
||||
kwargs["lora_path"] = components.lora_path
|
||||
if components.override_pipeline_cls_name is not None:
|
||||
kwargs["override_pipeline_cls_name"] = components.override_pipeline_cls_name
|
||||
if components.override_transformer_cls_name is not None:
|
||||
kwargs["override_transformer_cls_name"] = components.override_transformer_cls_name
|
||||
if components.text_encoder_weights is not None:
|
||||
kwargs["override_text_encoder_safetensors"] = components.text_encoder_weights
|
||||
if components.transformer_weights is not None:
|
||||
kwargs["init_weights_from_safetensors"] = components.transformer_weights
|
||||
if components.transformer_2_weights is not None:
|
||||
kwargs["init_weights_from_safetensors_2"] = components.transformer_2_weights
|
||||
|
||||
kwargs.update(deepcopy(normalized.pipeline.profile_overrides))
|
||||
kwargs.update(deepcopy(normalized.pipeline.experimental))
|
||||
return FastVideoArgs.from_kwargs(**kwargs)
|
||||
|
||||
|
||||
def normalize_generation_request(request: GenerationRequest | Mapping[str, Any], ) -> GenerationRequest:
|
||||
normalized = (request if isinstance(request, GenerationRequest) else parse_config(GenerationRequest, request))
|
||||
|
||||
if hasattr(normalized, EXPLICIT_REQUEST_ATTR):
|
||||
refresh_generation_request_raw(normalized)
|
||||
else:
|
||||
bind_generation_request_raw(normalized, _serialize_generation_request(normalized))
|
||||
return normalized
|
||||
|
||||
|
||||
def legacy_generate_call_to_request(
|
||||
prompt: str | None,
|
||||
sampling_param: SamplingParam | None,
|
||||
*,
|
||||
mouse_cond: Any | None = None,
|
||||
keyboard_cond: Any | None = None,
|
||||
grid_sizes: Any | None = None,
|
||||
legacy_kwargs: Mapping[str, Any] | None = None,
|
||||
) -> GenerationRequest:
|
||||
raw = _sampling_param_to_request_raw(sampling_param)
|
||||
if prompt is not None:
|
||||
raw["prompt"] = prompt
|
||||
|
||||
for key, value in (legacy_kwargs or {}).items():
|
||||
_apply_request_field(raw, key, value)
|
||||
|
||||
if mouse_cond is not None:
|
||||
raw.setdefault("inputs", {})["mouse_cond"] = mouse_cond
|
||||
if keyboard_cond is not None:
|
||||
raw.setdefault("inputs", {})["keyboard_cond"] = keyboard_cond
|
||||
if grid_sizes is not None:
|
||||
raw.setdefault("inputs", {})["grid_sizes"] = grid_sizes
|
||||
|
||||
normalized = parse_config(GenerationRequest, raw)
|
||||
bind_generation_request_raw(normalized, raw)
|
||||
return normalized
|
||||
|
||||
|
||||
def request_to_sampling_param(
|
||||
request: GenerationRequest,
|
||||
*,
|
||||
model_path: str,
|
||||
) -> SamplingParam:
|
||||
if request.plan is not None:
|
||||
raise NotImplementedError("GenerationRequest.plan is not wired into VideoGenerator yet")
|
||||
if request.state is not None:
|
||||
raise NotImplementedError("GenerationRequest.state is not wired into VideoGenerator yet")
|
||||
|
||||
sampling_param = SamplingParam.from_pretrained(model_path)
|
||||
updates = _explicit_request_updates(request)
|
||||
|
||||
for key, value in updates.items():
|
||||
if hasattr(sampling_param, key):
|
||||
setattr(sampling_param, key, deepcopy(value))
|
||||
elif key in _REQUEST_PIPELINE_OVERRIDE_FIELDS or _is_supported_as_default_only(key, value):
|
||||
continue
|
||||
else:
|
||||
raise ValueError(f"Request field {key!r} is not supported by sampling params for {model_path}")
|
||||
|
||||
sampling_param.__post_init__()
|
||||
sampling_param.check_sampling_param()
|
||||
return sampling_param
|
||||
|
||||
|
||||
def expand_request_prompt_batch(request: GenerationRequest, ) -> list[GenerationRequest]:
|
||||
if not isinstance(request.prompt, list):
|
||||
return [request]
|
||||
|
||||
requests: list[GenerationRequest] = []
|
||||
for index, prompt in enumerate(request.prompt):
|
||||
single_request = deepcopy(request)
|
||||
single_request.prompt = prompt
|
||||
_fan_out_batched_input_value(request, single_request, "image_path", index)
|
||||
_fan_out_batched_input_value(request, single_request, "video_path", index)
|
||||
_fan_out_explicit_request_metadata(request, single_request, index, prompt)
|
||||
requests.append(single_request)
|
||||
return requests
|
||||
|
||||
|
||||
def _looks_like_run_or_serve_config(raw: Mapping[str, Any]) -> bool:
|
||||
return isinstance(raw.get("generator"), Mapping)
|
||||
|
||||
|
||||
def _normalize_overrides(overrides: list[str] | Mapping[str, Any] | None, ) -> dict[str, Any] | None:
|
||||
if not overrides:
|
||||
return None
|
||||
if isinstance(overrides, list):
|
||||
return parse_cli_overrides(overrides)
|
||||
return dict(overrides)
|
||||
|
||||
|
||||
def _sampling_param_to_request_raw(sampling_param: SamplingParam | None, ) -> dict[str, Any]:
|
||||
if sampling_param is None:
|
||||
return {}
|
||||
|
||||
raw: dict[str, Any] = {}
|
||||
for key, value in shallow_asdict(sampling_param).items():
|
||||
if key == "prompt":
|
||||
continue
|
||||
_apply_request_field(raw, key, deepcopy(value))
|
||||
return raw
|
||||
|
||||
|
||||
def _apply_request_field(
|
||||
raw: dict[str, Any],
|
||||
key: str,
|
||||
value: Any,
|
||||
) -> None:
|
||||
key = _LEGACY_REQUEST_ALIASES.get(key, key)
|
||||
if key == "negative_prompt":
|
||||
raw["negative_prompt"] = value
|
||||
return
|
||||
if key in _INPUT_FIELD_NAMES:
|
||||
raw.setdefault("inputs", {})[key] = value
|
||||
return
|
||||
if key in _SAMPLING_FIELD_NAMES:
|
||||
raw.setdefault("sampling", {})[key] = value
|
||||
return
|
||||
if key in _RUNTIME_FIELD_NAMES:
|
||||
raw.setdefault("runtime", {})[key] = value
|
||||
return
|
||||
if key in _OUTPUT_FIELD_NAMES:
|
||||
raw.setdefault("output", {})[key] = value
|
||||
return
|
||||
raw.setdefault("extensions", {})[key] = value
|
||||
|
||||
|
||||
def request_to_pipeline_overrides(request: GenerationRequest) -> dict[str, Any]:
|
||||
overrides: dict[str, Any] = {}
|
||||
for key, value in _explicit_request_updates(request).items():
|
||||
if key in _REQUEST_PIPELINE_OVERRIDE_FIELDS:
|
||||
overrides[key] = deepcopy(value)
|
||||
return overrides
|
||||
|
||||
|
||||
def _explicit_request_updates(request: GenerationRequest) -> dict[str, Any]:
|
||||
raw = getattr(request, EXPLICIT_REQUEST_ATTR, None)
|
||||
if raw is None:
|
||||
raw = _serialize_generation_request(request)
|
||||
|
||||
return _extract_request_updates(raw)
|
||||
|
||||
|
||||
def _extract_request_updates(raw: Mapping[str, Any]) -> dict[str, Any]:
|
||||
updates: dict[str, Any] = {}
|
||||
if "negative_prompt" in raw:
|
||||
updates["negative_prompt"] = deepcopy(raw["negative_prompt"])
|
||||
|
||||
for section_name in ("inputs", "sampling", "runtime", "output"):
|
||||
section = raw.get(section_name)
|
||||
if not isinstance(section, Mapping):
|
||||
continue
|
||||
for key, value in section.items():
|
||||
updates[key] = deepcopy(value)
|
||||
|
||||
stage_overrides = raw.get("stage_overrides")
|
||||
if stage_overrides:
|
||||
updates.update(_flatten_stage_overrides(stage_overrides))
|
||||
|
||||
extensions = raw.get("extensions")
|
||||
if isinstance(extensions, Mapping):
|
||||
for key, value in extensions.items():
|
||||
updates[key] = deepcopy(value)
|
||||
|
||||
return updates
|
||||
|
||||
|
||||
def _flatten_stage_overrides(stage_overrides: Any) -> dict[str, Any]:
|
||||
if not isinstance(stage_overrides, Mapping):
|
||||
raise ValueError("GenerationRequest.stage_overrides must be a mapping")
|
||||
|
||||
flattened: dict[str, Any] = {}
|
||||
for stage_name, overrides in stage_overrides.items():
|
||||
if not isinstance(overrides, Mapping):
|
||||
raise ValueError(f"GenerationRequest.stage_overrides.{stage_name} must be a mapping")
|
||||
for key, value in overrides.items():
|
||||
if key in flattened and flattened[key] != value:
|
||||
raise ValueError(f"Conflicting stage override for {key!r} across stages")
|
||||
flattened[key] = deepcopy(value)
|
||||
return flattened
|
||||
|
||||
|
||||
def _serialize_generation_request(request: GenerationRequest) -> dict[str, Any]:
|
||||
return deepcopy(config_to_dict(request))
|
||||
|
||||
|
||||
def _fan_out_batched_input_value(
|
||||
source_request: GenerationRequest,
|
||||
target_request: GenerationRequest,
|
||||
field_name: str,
|
||||
index: int,
|
||||
) -> None:
|
||||
value = getattr(source_request.inputs, field_name)
|
||||
if not isinstance(value, list):
|
||||
return
|
||||
_validate_batched_input_length(source_request.prompt, value, field_name)
|
||||
setattr(target_request.inputs, field_name, deepcopy(value[index]))
|
||||
|
||||
|
||||
def _fan_out_explicit_request_metadata(
|
||||
source_request: GenerationRequest,
|
||||
target_request: GenerationRequest,
|
||||
index: int,
|
||||
prompt: str,
|
||||
) -> None:
|
||||
raw = getattr(source_request, EXPLICIT_REQUEST_ATTR, None)
|
||||
if raw is None:
|
||||
return
|
||||
|
||||
raw = deepcopy(raw)
|
||||
raw["prompt"] = prompt
|
||||
inputs = raw.get("inputs")
|
||||
if isinstance(inputs, dict):
|
||||
for field_name in ("image_path", "video_path"):
|
||||
value = inputs.get(field_name)
|
||||
if isinstance(value, list):
|
||||
_validate_batched_input_length(source_request.prompt, value, field_name)
|
||||
inputs[field_name] = deepcopy(value[index])
|
||||
|
||||
setattr(target_request, EXPLICIT_REQUEST_ATTR, raw)
|
||||
|
||||
|
||||
def _validate_batched_input_length(
|
||||
prompts: str | list[str] | None,
|
||||
values: list[Any],
|
||||
field_name: str,
|
||||
) -> None:
|
||||
if not isinstance(prompts, list):
|
||||
return
|
||||
if len(values) != len(prompts):
|
||||
raise ValueError(f"GenerationRequest.inputs.{field_name} must have the same length as request.prompt")
|
||||
|
||||
|
||||
def _is_supported_as_default_only(key: str, value: Any) -> bool:
|
||||
default_value = _DEFAULT_REQUEST_UPDATES.get(key, _MISSING)
|
||||
return default_value is not _MISSING and _values_equal(value, default_value)
|
||||
|
||||
|
||||
def _collect_non_default_fields(
|
||||
value: Any,
|
||||
default: Any,
|
||||
) -> dict[str, Any]:
|
||||
if not (is_dataclass(value) and is_dataclass(default)):
|
||||
return {}
|
||||
|
||||
result: dict[str, Any] = {}
|
||||
for field in fields(value):
|
||||
current = getattr(value, field.name)
|
||||
default_value = getattr(default, field.name)
|
||||
if is_dataclass(current) and is_dataclass(default_value):
|
||||
nested = _collect_non_default_fields(current, default_value)
|
||||
if nested:
|
||||
result[field.name] = nested
|
||||
continue
|
||||
if not _values_equal(current, default_value):
|
||||
result[field.name] = deepcopy(current)
|
||||
return result
|
||||
|
||||
|
||||
def _values_equal(left: Any, right: Any) -> bool:
|
||||
if left is right:
|
||||
return True
|
||||
try:
|
||||
return bool(left == right)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
_DEFAULT_REQUEST_UPDATES = _extract_request_updates(config_to_dict(GenerationRequest()))
|
||||
|
||||
__all__ = [
|
||||
"generator_config_to_fastvideo_args",
|
||||
"legacy_from_pretrained_to_config",
|
||||
"legacy_generate_call_to_request",
|
||||
"load_generator_config_from_file",
|
||||
"normalize_generation_request",
|
||||
"normalize_generator_config",
|
||||
"request_to_pipeline_overrides",
|
||||
"request_to_sampling_param",
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class ConfigValidationError(ValueError):
|
||||
"""Validation error that keeps track of the nested config path."""
|
||||
|
||||
def __init__(self, path: str, message: str):
|
||||
self.path = path
|
||||
self.message = message
|
||||
super().__init__(str(self))
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.path:
|
||||
return f"{self.path}: {self.message}"
|
||||
return self.message
|
||||
@@ -0,0 +1,101 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
from collections.abc import Mapping
|
||||
|
||||
import yaml
|
||||
|
||||
from fastvideo.api.errors import ConfigValidationError
|
||||
|
||||
|
||||
def parse_cli_overrides(overrides: list[str]) -> dict[str, Any]:
|
||||
"""Parse ``--dotted.key value`` style overrides into a flat mapping."""
|
||||
parsed: dict[str, Any] = {}
|
||||
index = 0
|
||||
while index < len(overrides):
|
||||
token = overrides[index]
|
||||
if not token.startswith("--"):
|
||||
raise ValueError(f"Expected --dotted.key, got {token!r}")
|
||||
|
||||
key = token[2:]
|
||||
if not key:
|
||||
raise ValueError("Override key cannot be empty")
|
||||
|
||||
if "=" in key:
|
||||
key, raw_value = key.split("=", 1)
|
||||
else:
|
||||
index += 1
|
||||
if index >= len(overrides):
|
||||
raise ValueError(f"Missing value for override {token!r}")
|
||||
raw_value = overrides[index]
|
||||
|
||||
parsed[_normalize_override_key(key)] = _cast_override_value(raw_value)
|
||||
index += 1
|
||||
|
||||
return parsed
|
||||
|
||||
|
||||
def apply_overrides(config: Mapping[str, Any], overrides: Mapping[str, Any]) -> dict[str, Any]:
|
||||
"""Return a copy of ``config`` with dotted-key overrides applied."""
|
||||
merged = deepcopy(dict(config))
|
||||
for dotted_key, value in overrides.items():
|
||||
_apply_single_override(merged, dotted_key, value)
|
||||
return merged
|
||||
|
||||
|
||||
def _apply_single_override(config: dict[str, Any], dotted_key: str, value: Any) -> None:
|
||||
parts = dotted_key.split(".")
|
||||
if not all(parts):
|
||||
raise ValueError(f"Invalid override path {dotted_key!r}")
|
||||
|
||||
cursor = config
|
||||
for depth, part in enumerate(parts[:-1]):
|
||||
existing = cursor.get(part)
|
||||
if existing is None:
|
||||
existing = {}
|
||||
cursor[part] = existing
|
||||
elif not isinstance(existing, dict):
|
||||
raise ConfigValidationError(
|
||||
".".join(parts[:depth + 1]),
|
||||
"cannot apply nested override through a non-mapping value",
|
||||
)
|
||||
cursor = existing
|
||||
|
||||
cursor[parts[-1]] = value
|
||||
|
||||
|
||||
def _cast_override_value(raw: str) -> Any:
|
||||
lowered = raw.lower()
|
||||
if lowered == "true":
|
||||
return True
|
||||
if lowered == "false":
|
||||
return False
|
||||
if lowered in {"none", "null"}:
|
||||
return None
|
||||
|
||||
try:
|
||||
return int(raw)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return float(raw)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if raw.startswith("[") or raw.startswith("{"):
|
||||
try:
|
||||
return yaml.safe_load(raw)
|
||||
except yaml.YAMLError:
|
||||
pass
|
||||
|
||||
return raw
|
||||
|
||||
|
||||
def _normalize_override_key(key: str) -> str:
|
||||
return key.replace("-", "_")
|
||||
|
||||
|
||||
__all__ = ["apply_overrides", "parse_cli_overrides"]
|
||||
@@ -0,0 +1,332 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import json
|
||||
import types
|
||||
from pathlib import Path
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, Literal, TypeVar, Union, get_args, get_origin, get_type_hints
|
||||
|
||||
import yaml
|
||||
|
||||
from fastvideo.api.errors import ConfigValidationError
|
||||
from fastvideo.api.overrides import apply_overrides, parse_cli_overrides
|
||||
from fastvideo.api.request_metadata import (
|
||||
bind_generation_request_raw,
|
||||
bind_run_config_raw,
|
||||
bind_serve_config_raw,
|
||||
)
|
||||
from fastvideo.api.schema import GenerationRequest, RunConfig, ServeConfig
|
||||
|
||||
T = TypeVar("T")
|
||||
_UNION_ORIGINS = {types.UnionType, Union}
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class _DataclassSpec:
|
||||
cls: type[Any]
|
||||
type_hints: dict[str, Any]
|
||||
fields_by_name: dict[str, dataclasses.Field[Any]]
|
||||
|
||||
|
||||
def parse_config(config_type: type[T], raw: Mapping[str, Any] | T) -> T:
|
||||
"""Parse a nested mapping into a typed inference config object."""
|
||||
if isinstance(raw, config_type):
|
||||
return raw
|
||||
if not isinstance(raw, Mapping):
|
||||
raise ConfigValidationError("", f"expected mapping for {config_type.__name__}")
|
||||
parsed = _SchemaParser().parse_dataclass(config_type, raw, "")
|
||||
if config_type is GenerationRequest:
|
||||
return bind_generation_request_raw(parsed, raw)
|
||||
if config_type is RunConfig:
|
||||
return bind_run_config_raw(parsed, raw)
|
||||
if config_type is ServeConfig:
|
||||
return bind_serve_config_raw(parsed, raw)
|
||||
return parsed
|
||||
|
||||
|
||||
def config_to_dict(config: Any) -> Any:
|
||||
"""Serialize a typed config object into plain Python containers."""
|
||||
if dataclasses.is_dataclass(config) and not isinstance(config, type):
|
||||
return {field.name: config_to_dict(getattr(config, field.name)) for field in dataclasses.fields(config)}
|
||||
if isinstance(config, list):
|
||||
return [config_to_dict(item) for item in config]
|
||||
if isinstance(config, dict):
|
||||
return {key: config_to_dict(value) for key, value in config.items()}
|
||||
return config
|
||||
|
||||
|
||||
def load_config(
|
||||
config_type: type[T],
|
||||
path: str | Path,
|
||||
overrides: list[str] | Mapping[str, Any] | None = None,
|
||||
) -> T:
|
||||
"""Load a typed config object from YAML or JSON."""
|
||||
raw = load_raw_config(path)
|
||||
normalized_overrides = _normalize_overrides(overrides)
|
||||
if normalized_overrides:
|
||||
raw = apply_overrides(raw, normalized_overrides)
|
||||
return parse_config(config_type, raw)
|
||||
|
||||
|
||||
def load_run_config(
|
||||
path: str | Path,
|
||||
overrides: list[str] | Mapping[str, Any] | None = None,
|
||||
) -> RunConfig:
|
||||
return load_config(RunConfig, path, overrides)
|
||||
|
||||
|
||||
def load_serve_config(
|
||||
path: str | Path,
|
||||
overrides: list[str] | Mapping[str, Any] | None = None,
|
||||
) -> ServeConfig:
|
||||
return load_config(ServeConfig, path, overrides)
|
||||
|
||||
|
||||
def load_raw_config(path: str | Path) -> dict[str, Any]:
|
||||
config_path = Path(path)
|
||||
if not config_path.exists():
|
||||
raise FileNotFoundError(f"Config file not found: {config_path}")
|
||||
|
||||
with config_path.open(encoding="utf-8") as handle:
|
||||
raw = _load_raw_mapping(handle, config_path)
|
||||
|
||||
if raw is None:
|
||||
return {}
|
||||
if not isinstance(raw, Mapping):
|
||||
raise ConfigValidationError("", f"{config_path} must contain a top-level mapping")
|
||||
return dict(raw)
|
||||
|
||||
|
||||
def _load_raw_mapping(handle: Any, config_path: Path) -> Any:
|
||||
suffix = config_path.suffix.lower()
|
||||
if suffix in {".yaml", ".yml"}:
|
||||
return yaml.safe_load(handle)
|
||||
if suffix == ".json":
|
||||
return json.load(handle)
|
||||
raise ValueError(f"Unsupported config file format: {config_path}")
|
||||
|
||||
|
||||
def _normalize_overrides(overrides: list[str] | Mapping[str, Any] | None, ) -> dict[str, Any] | None:
|
||||
if not overrides:
|
||||
return None
|
||||
if isinstance(overrides, list):
|
||||
return parse_cli_overrides(overrides)
|
||||
return dict(overrides)
|
||||
|
||||
|
||||
class _SchemaParser:
|
||||
|
||||
def parse_dataclass(
|
||||
self,
|
||||
config_type: type[T],
|
||||
raw: Mapping[str, Any],
|
||||
path: str,
|
||||
) -> T:
|
||||
if not isinstance(raw, Mapping):
|
||||
raise ConfigValidationError(path, f"expected mapping for {config_type.__name__}")
|
||||
|
||||
spec = _get_dataclass_spec(config_type)
|
||||
self._validate_keys(raw, spec, path)
|
||||
|
||||
values: dict[str, Any] = {}
|
||||
for name, field in spec.fields_by_name.items():
|
||||
field_path = _join_path(path, name)
|
||||
if name in raw:
|
||||
values[name] = self.parse_value(spec.type_hints[name], raw[name], field_path)
|
||||
continue
|
||||
if _field_is_required(field):
|
||||
raise ConfigValidationError(field_path, "missing required field")
|
||||
|
||||
return config_type(**values)
|
||||
|
||||
def parse_value(self, annotation: Any, value: Any, path: str) -> Any:
|
||||
if annotation is Any:
|
||||
return value
|
||||
|
||||
origin = get_origin(annotation)
|
||||
if origin in _UNION_ORIGINS:
|
||||
return self._parse_union(annotation, value, path)
|
||||
if origin is Literal:
|
||||
return self._parse_literal(annotation, value, path)
|
||||
if origin is list:
|
||||
return self._parse_list(annotation, value, path)
|
||||
if origin is dict:
|
||||
return self._parse_dict(annotation, value, path)
|
||||
if origin is tuple:
|
||||
return self._parse_tuple(annotation, value, path)
|
||||
if isinstance(annotation, type) and dataclasses.is_dataclass(annotation):
|
||||
return self.parse_dataclass(annotation, value, path)
|
||||
|
||||
scalar_parser = _SCALAR_PARSERS.get(annotation)
|
||||
if scalar_parser is not None:
|
||||
return scalar_parser(value, path)
|
||||
|
||||
return self._parse_instance(annotation, value, path)
|
||||
|
||||
def _validate_keys(
|
||||
self,
|
||||
raw: Mapping[str, Any],
|
||||
spec: _DataclassSpec,
|
||||
path: str,
|
||||
) -> None:
|
||||
for key in raw:
|
||||
if not isinstance(key, str):
|
||||
raise ConfigValidationError(path, "expected mapping keys to be strings")
|
||||
if key not in spec.fields_by_name:
|
||||
raise ConfigValidationError(_join_path(path, key), "unknown field")
|
||||
|
||||
def _parse_union(self, annotation: Any, value: Any, path: str) -> Any:
|
||||
candidates = [candidate for candidate in get_args(annotation) if candidate is not type(None)]
|
||||
if value is None and len(candidates) != len(get_args(annotation)):
|
||||
return None
|
||||
if len(candidates) == 1:
|
||||
return self.parse_value(candidates[0], value, path)
|
||||
|
||||
errors: list[str] = []
|
||||
for candidate in candidates:
|
||||
try:
|
||||
return self.parse_value(candidate, value, path)
|
||||
except ConfigValidationError as exc:
|
||||
errors.append(exc.message)
|
||||
|
||||
expected = ", ".join(_type_name(candidate) for candidate in candidates)
|
||||
detail = errors[0] if errors else f"expected one of ({expected})"
|
||||
raise ConfigValidationError(path, detail)
|
||||
|
||||
def _parse_literal(self, annotation: Any, value: Any, path: str) -> Any:
|
||||
allowed = get_args(annotation)
|
||||
if value not in allowed:
|
||||
raise ConfigValidationError(path, f"expected one of {sorted(allowed)!r}")
|
||||
return value
|
||||
|
||||
def _parse_list(self, annotation: Any, value: Any, path: str) -> list[Any]:
|
||||
if not isinstance(value, list):
|
||||
raise ConfigValidationError(path, "expected list")
|
||||
item_type = get_args(annotation)[0] if get_args(annotation) else Any
|
||||
return [self.parse_value(item_type, item, f"{path}[{index}]") for index, item in enumerate(value)]
|
||||
|
||||
def _parse_dict(self, annotation: Any, value: Any, path: str) -> dict[Any, Any]:
|
||||
if not isinstance(value, Mapping):
|
||||
raise ConfigValidationError(path, "expected mapping")
|
||||
|
||||
key_type, value_type = (get_args(annotation) + (Any, Any))[:2]
|
||||
parsed: dict[Any, Any] = {}
|
||||
for key, item in value.items():
|
||||
parsed_key = self._parse_dict_key(key_type, key, path)
|
||||
item_path = _join_path(path, str(key))
|
||||
parsed[parsed_key] = self.parse_value(value_type, item, item_path)
|
||||
return parsed
|
||||
|
||||
def _parse_tuple(self, annotation: Any, value: Any, path: str) -> tuple[Any, ...]:
|
||||
if not isinstance(value, list | tuple):
|
||||
raise ConfigValidationError(path, "expected tuple")
|
||||
|
||||
item_types = get_args(annotation)
|
||||
if len(item_types) == 2 and item_types[1] is Ellipsis:
|
||||
return tuple(self.parse_value(item_types[0], item, f"{path}[{index}]") for index, item in enumerate(value))
|
||||
|
||||
if len(value) != len(item_types):
|
||||
raise ConfigValidationError(path, f"expected tuple of length {len(item_types)}")
|
||||
|
||||
return tuple(
|
||||
self.parse_value(item_type, item, f"{path}[{index}]")
|
||||
for index, (item_type, item) in enumerate(zip(item_types, value, strict=True)))
|
||||
|
||||
def _parse_dict_key(self, annotation: Any, value: Any, path: str) -> Any:
|
||||
if annotation is Any:
|
||||
return value
|
||||
if annotation is str:
|
||||
if not isinstance(value, str):
|
||||
raise ConfigValidationError(path, "expected string dictionary keys")
|
||||
return value
|
||||
if annotation is int:
|
||||
if not isinstance(value, int) or isinstance(value, bool):
|
||||
raise ConfigValidationError(path, "expected integer dictionary keys")
|
||||
return value
|
||||
return value
|
||||
|
||||
def _parse_instance(self, annotation: Any, value: Any, path: str) -> Any:
|
||||
if isinstance(annotation, type) and not isinstance(value, annotation):
|
||||
raise ConfigValidationError(path, f"expected {annotation.__name__}")
|
||||
return value
|
||||
|
||||
|
||||
def _parse_bool(value: Any, path: str) -> bool:
|
||||
if type(value) is not bool:
|
||||
raise ConfigValidationError(path, "expected bool")
|
||||
return value
|
||||
|
||||
|
||||
def _parse_int(value: Any, path: str) -> int:
|
||||
if not isinstance(value, int) or isinstance(value, bool):
|
||||
raise ConfigValidationError(path, "expected int")
|
||||
return value
|
||||
|
||||
|
||||
def _parse_float(value: Any, path: str) -> float:
|
||||
if not isinstance(value, int | float) or isinstance(value, bool):
|
||||
raise ConfigValidationError(path, "expected float")
|
||||
return float(value)
|
||||
|
||||
|
||||
def _parse_str(value: Any, path: str) -> str:
|
||||
if not isinstance(value, str):
|
||||
raise ConfigValidationError(path, "expected str")
|
||||
return value
|
||||
|
||||
|
||||
_SCALAR_PARSERS: dict[Any, Any] = {
|
||||
bool: _parse_bool,
|
||||
int: _parse_int,
|
||||
float: _parse_float,
|
||||
str: _parse_str,
|
||||
}
|
||||
|
||||
|
||||
def _field_is_required(field: dataclasses.Field[Any]) -> bool:
|
||||
return (field.default is dataclasses.MISSING and field.default_factory is dataclasses.MISSING)
|
||||
|
||||
|
||||
def _get_dataclass_spec(config_type: type[Any]) -> _DataclassSpec:
|
||||
spec = _DATACLASS_SPEC_CACHE.get(config_type)
|
||||
if spec is not None:
|
||||
return spec
|
||||
|
||||
spec = _DataclassSpec(
|
||||
cls=config_type,
|
||||
type_hints=get_type_hints(config_type),
|
||||
fields_by_name={field.name: field
|
||||
for field in dataclasses.fields(config_type)},
|
||||
)
|
||||
_DATACLASS_SPEC_CACHE[config_type] = spec
|
||||
return spec
|
||||
|
||||
|
||||
_DATACLASS_SPEC_CACHE: dict[type[Any], _DataclassSpec] = {}
|
||||
|
||||
|
||||
def _join_path(prefix: str, suffix: str) -> str:
|
||||
if not prefix:
|
||||
return suffix
|
||||
return f"{prefix}.{suffix}"
|
||||
|
||||
|
||||
def _type_name(annotation: Any) -> str:
|
||||
origin = get_origin(annotation)
|
||||
if origin is not None:
|
||||
return str(annotation)
|
||||
if hasattr(annotation, "__name__"):
|
||||
return annotation.__name__
|
||||
return str(annotation)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"config_to_dict",
|
||||
"load_config",
|
||||
"load_raw_config",
|
||||
"load_run_config",
|
||||
"load_serve_config",
|
||||
"parse_config",
|
||||
]
|
||||
@@ -0,0 +1,247 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Track which GenerationRequest fields the user explicitly provided.
|
||||
|
||||
This module solves a specific problem: when translating a GenerationRequest into
|
||||
a legacy SamplingParam, we need to distinguish user-provided values (which
|
||||
should override model defaults) from schema defaults (which should NOT override
|
||||
model defaults).
|
||||
|
||||
The approach:
|
||||
1. At bind time, store the original raw dict and a baseline snapshot.
|
||||
2. Patch __setattr__ on tracked dataclass types to record dirty field paths.
|
||||
3. At access time, do a lazy 3-way merge: raw + baseline + current state,
|
||||
with dirty paths forcing inclusion even when current == baseline.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from copy import deepcopy
|
||||
import dataclasses
|
||||
from typing import Any, cast
|
||||
from collections.abc import Callable
|
||||
|
||||
from fastvideo.api.schema import (
|
||||
ContinuationState,
|
||||
GenerationPlan,
|
||||
GenerationRequest,
|
||||
InputConfig,
|
||||
OutputConfig,
|
||||
PlannedStage,
|
||||
RequestRuntimeConfig,
|
||||
RunConfig,
|
||||
SamplingConfig,
|
||||
ServeConfig,
|
||||
)
|
||||
|
||||
EXPLICIT_REQUEST_ATTR = "_fastvideo_explicit_request"
|
||||
ORIGINAL_REQUEST_STATE_ATTR = "_fastvideo_original_request_state"
|
||||
_TRACKING_ROOT_ATTR = "_fastvideo_request_tracking_root"
|
||||
_TRACKING_PATH_ATTR = "_fastvideo_request_tracking_path"
|
||||
_TRACKING_PATCHED_ATTR = "_fastvideo_request_tracking_patched"
|
||||
_DIRTY_PATHS_ATTR = "_fastvideo_dirty_paths"
|
||||
_TRACKED_REQUEST_TYPES = (
|
||||
GenerationRequest,
|
||||
InputConfig,
|
||||
SamplingConfig,
|
||||
RequestRuntimeConfig,
|
||||
OutputConfig,
|
||||
ContinuationState,
|
||||
PlannedStage,
|
||||
GenerationPlan,
|
||||
)
|
||||
|
||||
|
||||
def bind_generation_request_raw(
|
||||
request: GenerationRequest,
|
||||
raw: Mapping[str, Any] | None,
|
||||
) -> GenerationRequest:
|
||||
_ensure_request_tracking()
|
||||
# Disable dirty tracking during bind so tree walk doesn't record paths.
|
||||
object.__setattr__(request, _DIRTY_PATHS_ATTR, None)
|
||||
object.__setattr__(request, EXPLICIT_REQUEST_ATTR, deepcopy(dict(raw or {})))
|
||||
object.__setattr__(request, ORIGINAL_REQUEST_STATE_ATTR, _serialize_config(request))
|
||||
_set_tracking_roots(request, request, "")
|
||||
# Enable dirty tracking.
|
||||
object.__setattr__(request, _DIRTY_PATHS_ATTR, set())
|
||||
return request
|
||||
|
||||
|
||||
def bind_run_config_raw(
|
||||
config: RunConfig,
|
||||
raw: Mapping[str, Any],
|
||||
) -> RunConfig:
|
||||
request_raw = raw.get("request")
|
||||
if isinstance(request_raw, Mapping):
|
||||
bind_generation_request_raw(config.request, request_raw)
|
||||
return config
|
||||
|
||||
|
||||
def bind_serve_config_raw(
|
||||
config: ServeConfig,
|
||||
raw: Mapping[str, Any],
|
||||
) -> ServeConfig:
|
||||
default_request_raw = raw.get("default_request")
|
||||
if isinstance(default_request_raw, Mapping):
|
||||
bind_generation_request_raw(config.default_request, default_request_raw)
|
||||
elif "default_request" not in raw:
|
||||
bind_generation_request_raw(config.default_request, {})
|
||||
return config
|
||||
|
||||
|
||||
def refresh_generation_request_raw(request: GenerationRequest, ) -> dict[str, Any] | None:
|
||||
raw = getattr(request, EXPLICIT_REQUEST_ATTR, None)
|
||||
baseline = getattr(request, ORIGINAL_REQUEST_STATE_ATTR, None)
|
||||
if not isinstance(raw, Mapping) or not isinstance(baseline, Mapping):
|
||||
return None
|
||||
|
||||
dirty = getattr(request, _DIRTY_PATHS_ATTR, None) or frozenset()
|
||||
current = _serialize_config(request)
|
||||
merged = deepcopy(dict(raw))
|
||||
_merge_request_mutations(merged, dict(baseline), current, dirty)
|
||||
|
||||
object.__setattr__(request, EXPLICIT_REQUEST_ATTR, merged)
|
||||
object.__setattr__(request, ORIGINAL_REQUEST_STATE_ATTR, current)
|
||||
object.__setattr__(request, _DIRTY_PATHS_ATTR, set())
|
||||
return merged
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3-way merge: raw + baseline + current, with dirty-path forcing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_MISSING = object()
|
||||
|
||||
|
||||
def _merge_request_mutations(
|
||||
merged: dict[str, Any],
|
||||
baseline: Mapping[str, Any],
|
||||
current: Mapping[str, Any],
|
||||
dirty: frozenset[str] | set[str],
|
||||
path_prefix: str = "",
|
||||
force_dirty: bool = False,
|
||||
) -> None:
|
||||
# Remove keys that were deleted from the current state.
|
||||
for key in set(merged) | set(baseline):
|
||||
if key not in current:
|
||||
merged.pop(key, None)
|
||||
|
||||
for key in current:
|
||||
current_path = f"{path_prefix}.{key}" if path_prefix else key
|
||||
current_value = current[key]
|
||||
baseline_value = baseline.get(key, _MISSING)
|
||||
merged_value = merged.get(key, _MISSING)
|
||||
|
||||
# If this exact path was dirtied (e.g. whole section replaced),
|
||||
# propagate to all children.
|
||||
child_force = force_dirty or current_path in dirty
|
||||
|
||||
# Recurse into nested mappings.
|
||||
if isinstance(current_value, Mapping) and isinstance(baseline_value, Mapping):
|
||||
nested = (deepcopy(dict(merged_value)) if isinstance(merged_value, Mapping) else {})
|
||||
_merge_request_mutations(
|
||||
nested,
|
||||
baseline_value,
|
||||
current_value,
|
||||
dirty,
|
||||
current_path,
|
||||
child_force,
|
||||
)
|
||||
if nested:
|
||||
merged[key] = nested
|
||||
else:
|
||||
merged.pop(key, None)
|
||||
continue
|
||||
|
||||
# A field is explicitly set if:
|
||||
# - it's new (not in baseline),
|
||||
# - it changed from baseline,
|
||||
# - its path was touched by __setattr__ (dirty), or
|
||||
# - an ancestor path was dirty (whole section replaced).
|
||||
is_dirty = child_force or current_path in dirty
|
||||
if baseline_value is _MISSING or current_value != baseline_value or is_dirty:
|
||||
merged[key] = deepcopy(current_value)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# __setattr__ patching for dirty-path recording
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _ensure_request_tracking() -> None:
|
||||
for config_type in _TRACKED_REQUEST_TYPES:
|
||||
_patch_tracking_setattr(config_type)
|
||||
|
||||
|
||||
def _patch_tracking_setattr(config_type: type[Any]) -> None:
|
||||
if getattr(config_type, _TRACKING_PATCHED_ATTR, False):
|
||||
return
|
||||
|
||||
original_setattr = cast(
|
||||
Callable[[Any, str, Any], None],
|
||||
config_type.__setattr__,
|
||||
)
|
||||
field_names = {field.name for field in dataclasses.fields(config_type)}
|
||||
|
||||
def _tracking_setattr(self: Any, name: str, value: Any) -> None:
|
||||
if name.startswith("_fastvideo_") or name not in field_names:
|
||||
original_setattr(self, name, value)
|
||||
return
|
||||
|
||||
root = getattr(self, _TRACKING_ROOT_ATTR, None)
|
||||
if root is not None:
|
||||
dirty = getattr(root, _DIRTY_PATHS_ATTR, None)
|
||||
if isinstance(dirty, set):
|
||||
prefix = getattr(self, _TRACKING_PATH_ATTR, "")
|
||||
path = f"{prefix}.{name}" if prefix else name
|
||||
dirty.add(path)
|
||||
|
||||
original_setattr(self, name, value)
|
||||
|
||||
type.__setattr__(config_type, "__setattr__", _tracking_setattr)
|
||||
setattr(config_type, _TRACKING_PATCHED_ATTR, True)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tree walk to set tracking root/path on nested dataclasses
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _set_tracking_roots(
|
||||
root: GenerationRequest,
|
||||
obj: Any,
|
||||
prefix: str,
|
||||
) -> None:
|
||||
if not dataclasses.is_dataclass(obj) or isinstance(obj, type):
|
||||
return
|
||||
object.__setattr__(obj, _TRACKING_ROOT_ATTR, root)
|
||||
object.__setattr__(obj, _TRACKING_PATH_ATTR, prefix)
|
||||
for field in dataclasses.fields(obj):
|
||||
child = getattr(obj, field.name)
|
||||
child_path = f"{prefix}.{field.name}" if prefix else field.name
|
||||
if dataclasses.is_dataclass(child) and not isinstance(child, type):
|
||||
_set_tracking_roots(root, child, child_path)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Serialization helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _serialize_config(config: Any) -> Any:
|
||||
if dataclasses.is_dataclass(config) and not isinstance(config, type):
|
||||
return {field.name: _serialize_config(getattr(config, field.name)) for field in dataclasses.fields(config)}
|
||||
if isinstance(config, list):
|
||||
return [_serialize_config(item) for item in config]
|
||||
if isinstance(config, dict):
|
||||
return {key: _serialize_config(value) for key, value in config.items()}
|
||||
return deepcopy(config)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"EXPLICIT_REQUEST_ATTR",
|
||||
"ORIGINAL_REQUEST_STATE_ATTR",
|
||||
"bind_generation_request_raw",
|
||||
"bind_run_config_raw",
|
||||
"bind_serve_config_raw",
|
||||
"refresh_generation_request_raw",
|
||||
]
|
||||
@@ -0,0 +1,101 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
from collections.abc import Mapping
|
||||
|
||||
from fastvideo.api.schema import ContinuationState
|
||||
|
||||
|
||||
@dataclass
|
||||
class GenerationResult:
|
||||
prompt: str | None = None
|
||||
prompt_index: int | None = None
|
||||
samples: Any | None = None
|
||||
frames: Any | None = None
|
||||
audio: Any | None = None
|
||||
size: tuple[int, int, int] | None = None
|
||||
generation_time: float | None = None
|
||||
logging_info: Any | None = None
|
||||
trajectory: Any | None = None
|
||||
trajectory_timesteps: Any | None = None
|
||||
trajectory_decoded: Any | None = None
|
||||
video_path: str | None = None
|
||||
peak_memory_mb: float | None = None
|
||||
state: ContinuationState | None = None
|
||||
extra: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_legacy_result(
|
||||
cls,
|
||||
result: Mapping[str, Any],
|
||||
) -> GenerationResult:
|
||||
prompt = result.get("prompt")
|
||||
if prompt is None:
|
||||
prompt = result.get("prompts")
|
||||
|
||||
extra = {
|
||||
key: value
|
||||
for key, value in result.items() if key not in {
|
||||
"prompt",
|
||||
"prompt_index",
|
||||
"prompts",
|
||||
"samples",
|
||||
"frames",
|
||||
"audio",
|
||||
"size",
|
||||
"generation_time",
|
||||
"logging_info",
|
||||
"trajectory",
|
||||
"trajectory_timesteps",
|
||||
"trajectory_decoded",
|
||||
"video_path",
|
||||
"peak_memory_mb",
|
||||
"state",
|
||||
}
|
||||
}
|
||||
|
||||
return cls(
|
||||
prompt=prompt,
|
||||
prompt_index=result.get("prompt_index"),
|
||||
samples=result.get("samples"),
|
||||
frames=result.get("frames"),
|
||||
audio=result.get("audio"),
|
||||
size=result.get("size"),
|
||||
generation_time=result.get("generation_time"),
|
||||
logging_info=result.get("logging_info"),
|
||||
trajectory=result.get("trajectory"),
|
||||
trajectory_timesteps=result.get("trajectory_timesteps"),
|
||||
trajectory_decoded=result.get("trajectory_decoded"),
|
||||
video_path=result.get("video_path"),
|
||||
peak_memory_mb=result.get("peak_memory_mb"),
|
||||
state=result.get("state"),
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
def to_legacy_dict(self) -> dict[str, Any]:
|
||||
result = {
|
||||
"prompts": self.prompt,
|
||||
"samples": self.samples,
|
||||
"frames": self.frames,
|
||||
"audio": self.audio,
|
||||
"size": self.size,
|
||||
"generation_time": self.generation_time,
|
||||
"logging_info": self.logging_info,
|
||||
"trajectory": self.trajectory,
|
||||
"trajectory_timesteps": self.trajectory_timesteps,
|
||||
"trajectory_decoded": self.trajectory_decoded,
|
||||
"video_path": self.video_path,
|
||||
"peak_memory_mb": self.peak_memory_mb,
|
||||
}
|
||||
if self.prompt_index is not None:
|
||||
result["prompt_index"] = self.prompt_index
|
||||
result["prompt"] = self.prompt
|
||||
if self.state is not None:
|
||||
result["state"] = self.state
|
||||
result.update(self.extra)
|
||||
return result
|
||||
|
||||
|
||||
__all__ = ["GenerationResult"]
|
||||
@@ -0,0 +1,210 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Literal
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServerConfig:
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 8000
|
||||
output_dir: str = "outputs/"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParallelismConfig:
|
||||
tp_size: int = -1
|
||||
sp_size: int = -1
|
||||
hsdp_replicate_dim: int = 1
|
||||
hsdp_shard_dim: int = -1
|
||||
dist_timeout: int | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class OffloadConfig:
|
||||
dit: bool = True
|
||||
dit_layerwise: bool = True
|
||||
text_encoder: bool = True
|
||||
image_encoder: bool = True
|
||||
vae: bool = True
|
||||
pin_cpu_memory: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class CompileConfig:
|
||||
enabled: bool = False
|
||||
kwargs: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class QuantizationConfig:
|
||||
text_encoder_quant: str | None = None
|
||||
transformer_quant: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class EngineConfig:
|
||||
num_gpus: int = 1
|
||||
execution_backend: Literal["mp", "ray"] = "mp"
|
||||
parallelism: ParallelismConfig = field(default_factory=ParallelismConfig)
|
||||
offload: OffloadConfig = field(default_factory=OffloadConfig)
|
||||
compile: CompileConfig = field(default_factory=CompileConfig)
|
||||
enable_stage_verification: bool = True
|
||||
use_fsdp_inference: bool = False
|
||||
disable_autocast: bool = False
|
||||
quantization: QuantizationConfig | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ComponentConfig:
|
||||
config_root: str | None = None
|
||||
pipeline_config_path: str | None = None
|
||||
text_encoder_weights: str | None = None
|
||||
transformer_weights: str | None = None
|
||||
transformer_2_weights: str | None = None
|
||||
vae_weights: str | None = None
|
||||
upsampler_weights: str | None = None
|
||||
lora_path: str | None = None
|
||||
override_pipeline_cls_name: str | None = None
|
||||
override_transformer_cls_name: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class PipelineSelection:
|
||||
workload_type: Literal["t2v", "i2v", "t2i", "i2i"] | None = None
|
||||
profile: str | None = None
|
||||
profile_version: str | None = None
|
||||
components: ComponentConfig = field(default_factory=ComponentConfig)
|
||||
profile_overrides: dict[str, Any] = field(default_factory=dict)
|
||||
experimental: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GeneratorConfig:
|
||||
model_path: str
|
||||
revision: str | None = None
|
||||
trust_remote_code: bool = False
|
||||
engine: EngineConfig = field(default_factory=EngineConfig)
|
||||
pipeline: PipelineSelection = field(default_factory=PipelineSelection)
|
||||
|
||||
|
||||
@dataclass
|
||||
class InputConfig:
|
||||
prompt_path: str | None = None
|
||||
image_path: str | list[str] | None = None
|
||||
video_path: str | list[str] | None = None
|
||||
pil_image: Any | None = None
|
||||
pose: str | None = None
|
||||
mouse_cond: Any | None = None
|
||||
keyboard_cond: Any | None = None
|
||||
grid_sizes: Any | None = None
|
||||
c2ws_plucker_emb: Any | None = None
|
||||
refine_from: str | None = None
|
||||
stage1_video: Any | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SamplingConfig:
|
||||
num_videos_per_prompt: int = 1
|
||||
seed: int = 1024
|
||||
num_frames: int = 125
|
||||
height: int = 720
|
||||
width: int = 1280
|
||||
height_sr: int = 1072
|
||||
width_sr: int = 1920
|
||||
fps: int = 24
|
||||
num_inference_steps: int = 50
|
||||
num_inference_steps_sr: int = 50
|
||||
guidance_scale: float = 1.0
|
||||
guidance_scale_2: float | None = None
|
||||
guidance_rescale: float = 0.0
|
||||
true_cfg_scale: float | None = None
|
||||
boundary_ratio: float | None = None
|
||||
sigmas: list[float] | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class RequestRuntimeConfig:
|
||||
enable_teacache: bool = False
|
||||
return_trajectory_latents: bool = False
|
||||
return_trajectory_decoded: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutputConfig:
|
||||
output_path: str = "outputs/"
|
||||
output_video_name: str | None = None
|
||||
save_video: bool = True
|
||||
return_frames: bool = True
|
||||
return_state: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContinuationState:
|
||||
kind: str
|
||||
payload: dict[str, Any]
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlannedStage:
|
||||
name: str
|
||||
kind: str
|
||||
source: str | None = None
|
||||
overrides: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GenerationPlan:
|
||||
stages: list[PlannedStage]
|
||||
final_stage: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class GenerationRequest:
|
||||
prompt: str | list[str] | None = None
|
||||
negative_prompt: str | None = None
|
||||
inputs: InputConfig = field(default_factory=InputConfig)
|
||||
sampling: SamplingConfig = field(default_factory=SamplingConfig)
|
||||
runtime: RequestRuntimeConfig = field(default_factory=RequestRuntimeConfig)
|
||||
output: OutputConfig = field(default_factory=OutputConfig)
|
||||
stage_overrides: dict[str, Any] = field(default_factory=dict)
|
||||
state: ContinuationState | None = None
|
||||
plan: GenerationPlan | None = None
|
||||
extensions: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RunConfig:
|
||||
generator: GeneratorConfig
|
||||
request: GenerationRequest
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServeConfig:
|
||||
generator: GeneratorConfig
|
||||
server: ServerConfig = field(default_factory=ServerConfig)
|
||||
default_request: GenerationRequest = field(default_factory=GenerationRequest)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CompileConfig",
|
||||
"ComponentConfig",
|
||||
"ContinuationState",
|
||||
"EngineConfig",
|
||||
"GenerationPlan",
|
||||
"GenerationRequest",
|
||||
"GeneratorConfig",
|
||||
"InputConfig",
|
||||
"OffloadConfig",
|
||||
"OutputConfig",
|
||||
"ParallelismConfig",
|
||||
"PipelineSelection",
|
||||
"PlannedStage",
|
||||
"QuantizationConfig",
|
||||
"RequestRuntimeConfig",
|
||||
"RunConfig",
|
||||
"SamplingConfig",
|
||||
"ServeConfig",
|
||||
"ServerConfig",
|
||||
]
|
||||
@@ -0,0 +1,738 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""
|
||||
Bidirectional Sparse Attention (BSA) backend for FastVideo.
|
||||
|
||||
Pure-PyTorch reference implementation from:
|
||||
"Bidirectional Sparse Attention for Faster Video Diffusion Training"
|
||||
(arXiv:2509.01085)
|
||||
|
||||
BSA sparsifies both queries (pruning redundant tokens per block) and
|
||||
key-value pairs (keeping only relevant KV blocks per query block).
|
||||
|
||||
This is a training-free inference backend: it works with any model
|
||||
trained with full attention by applying BSA sparsity at inference time.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from fastvideo.attention.backends.abstract import (
|
||||
AttentionBackend,
|
||||
AttentionImpl,
|
||||
AttentionMetadata,
|
||||
AttentionMetadataBuilder,
|
||||
)
|
||||
from fastvideo.distributed import get_sp_group
|
||||
from fastvideo.logger import init_logger
|
||||
|
||||
try:
|
||||
from fastvideo.attention.utils.flash_attn_no_pad import (
|
||||
flash_attn_varlen_func_impl, )
|
||||
FLASH_ATTN_AVAILABLE = True
|
||||
except ImportError:
|
||||
try:
|
||||
from flash_attn import flash_attn_varlen_func as flash_attn_varlen_func_impl
|
||||
FLASH_ATTN_AVAILABLE = True
|
||||
except ImportError:
|
||||
FLASH_ATTN_AVAILABLE = False
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
BSA_TILE_SIZE = (4, 4, 4)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cached index helpers (same pattern as VSA)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=10)
|
||||
def get_tile_partition_indices(
|
||||
dit_seq_shape: tuple[int, int, int],
|
||||
tile_size: tuple[int, int, int],
|
||||
device: torch.device,
|
||||
) -> torch.LongTensor:
|
||||
"""Map raster-order tokens to tile-contiguous order."""
|
||||
T, H, W = dit_seq_shape
|
||||
ts, hs, ws = tile_size
|
||||
indices = torch.arange(T * H * W, device=device, dtype=torch.long).reshape(T, H, W)
|
||||
ls = []
|
||||
for t in range(math.ceil(T / ts)):
|
||||
for h in range(math.ceil(H / hs)):
|
||||
for w in range(math.ceil(W / ws)):
|
||||
ls.append(indices[
|
||||
t * ts:min(t * ts + ts, T),
|
||||
h * hs:min(h * hs + hs, H),
|
||||
w * ws:min(w * ws + ws, W),
|
||||
].flatten())
|
||||
return torch.cat(ls, dim=0)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=10)
|
||||
def get_reverse_tile_partition_indices(
|
||||
dit_seq_shape: tuple[int, int, int],
|
||||
tile_size: tuple[int, int, int],
|
||||
device: torch.device,
|
||||
) -> torch.LongTensor:
|
||||
"""Inverse mapping: tile-contiguous order back to raster order."""
|
||||
return torch.argsort(get_tile_partition_indices(dit_seq_shape, tile_size, device))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BSA core operations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _prune_queries(
|
||||
q_blocks: torch.Tensor,
|
||||
keep_ratio: float,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, int]:
|
||||
"""
|
||||
Prune redundant query tokens within each block.
|
||||
|
||||
Scores tokens by cosine similarity to the block center.
|
||||
Keeps the LEAST similar (most informative) tokens.
|
||||
|
||||
Args:
|
||||
q_blocks: [B, N_heads, N_blocks, block_size, D]
|
||||
keep_ratio: fraction of tokens to keep
|
||||
|
||||
Returns:
|
||||
sparse_q: [B, N_heads, N_blocks, keep_size, D]
|
||||
keep_indices: [B, N_heads, N_blocks, keep_size]
|
||||
keep_size: int
|
||||
"""
|
||||
B, H, N, S, D = q_blocks.shape
|
||||
keep_size = max(1, int(S * keep_ratio))
|
||||
|
||||
if keep_size >= S:
|
||||
idx = torch.arange(S, device=q_blocks.device)
|
||||
idx = idx.view(1, 1, 1, S).expand(B, H, N, S)
|
||||
return q_blocks, idx, S
|
||||
|
||||
center_idx = S // 2
|
||||
center = q_blocks[:, :, :, center_idx:center_idx + 1, :]
|
||||
|
||||
q_norm = F.normalize(q_blocks, dim=-1)
|
||||
c_norm = F.normalize(center, dim=-1)
|
||||
similarity = (q_norm * c_norm).sum(dim=-1) # [B, H, N, S]
|
||||
|
||||
# lowest similarity = most distinctive = keep
|
||||
_, indices = similarity.topk(keep_size, dim=-1, largest=False)
|
||||
indices, _ = indices.sort(dim=-1)
|
||||
|
||||
idx_expand = indices.unsqueeze(-1).expand(-1, -1, -1, -1, D)
|
||||
sparse_q = torch.gather(q_blocks, 3, idx_expand)
|
||||
|
||||
return sparse_q, indices, keep_size
|
||||
|
||||
|
||||
def _select_kv_blocks(
|
||||
sparse_q: torch.Tensor,
|
||||
k_blocks: torch.Tensor,
|
||||
cumulative_threshold: float,
|
||||
min_kv_blocks: int,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Dynamically select KV blocks for each query block.
|
||||
|
||||
Mean-pools to block level, computes block attention scores,
|
||||
admits blocks in descending order until cumulative mass
|
||||
exceeds threshold.
|
||||
|
||||
Args:
|
||||
sparse_q: [B, H, N, Sq, D]
|
||||
k_blocks: [B, H, N, Sk, D]
|
||||
cumulative_threshold: e.g. 0.9
|
||||
min_kv_blocks: minimum blocks to keep
|
||||
|
||||
Returns:
|
||||
kv_mask: [B, H, N, N] boolean
|
||||
"""
|
||||
B, H, N, _, D = sparse_q.shape
|
||||
|
||||
q_repr = sparse_q.mean(dim=3)
|
||||
k_repr = k_blocks.mean(dim=3)
|
||||
|
||||
scores = torch.matmul(q_repr, k_repr.transpose(-1, -2)) / (D**0.5)
|
||||
block_attn = F.softmax(scores, dim=-1)
|
||||
|
||||
sorted_attn, sorted_idx = block_attn.sort(dim=-1, descending=True)
|
||||
cumsum = sorted_attn.cumsum(dim=-1)
|
||||
|
||||
keep_sorted = torch.ones_like(cumsum, dtype=torch.bool)
|
||||
keep_sorted[..., 1:] = cumsum[..., :-1] < cumulative_threshold
|
||||
|
||||
min_mask = torch.zeros_like(keep_sorted)
|
||||
min_mask[..., :min(min_kv_blocks, N)] = True
|
||||
keep_sorted = keep_sorted | min_mask
|
||||
|
||||
kv_mask = torch.zeros_like(block_attn, dtype=torch.bool)
|
||||
kv_mask.scatter_(-1, sorted_idx, keep_sorted)
|
||||
|
||||
return kv_mask
|
||||
|
||||
|
||||
def _compute_sparse_attention(
|
||||
sparse_q: torch.Tensor,
|
||||
k_blocks: torch.Tensor,
|
||||
v_blocks: torch.Tensor,
|
||||
kv_mask: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Compute attention for each query block against selected KV blocks.
|
||||
|
||||
Handles per-batch and per-head KV masks correctly.
|
||||
Uses flash_attn_varlen_func when available on GPU.
|
||||
Falls back to pure-PyTorch reference on CPU.
|
||||
|
||||
Args:
|
||||
sparse_q: [B, H, N, Sq, D]
|
||||
k_blocks: [B, H, N, Sk, D]
|
||||
v_blocks: [B, H, N, Sk, D]
|
||||
kv_mask: [B, H, N, N] boolean (per-batch, per-head)
|
||||
|
||||
Returns:
|
||||
output: [B, H, N, Sq, D]
|
||||
"""
|
||||
if FLASH_ATTN_AVAILABLE and sparse_q.is_cuda:
|
||||
return _compute_sparse_attention_flash(sparse_q, k_blocks, v_blocks, kv_mask)
|
||||
else:
|
||||
return _compute_sparse_attention_reference(sparse_q, k_blocks, v_blocks, kv_mask)
|
||||
|
||||
|
||||
def _compute_sparse_attention_reference(
|
||||
sparse_q: torch.Tensor,
|
||||
k_blocks: torch.Tensor,
|
||||
v_blocks: torch.Tensor,
|
||||
kv_mask: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""Pure-PyTorch fallback with per-batch, per-head mask support."""
|
||||
B, H, N, Sq, D = sparse_q.shape
|
||||
output = torch.zeros_like(sparse_q)
|
||||
|
||||
for b in range(B):
|
||||
for h in range(H):
|
||||
for qb in range(N):
|
||||
selected = kv_mask[b, h, qb] # [N] boolean
|
||||
sel_idx = selected.nonzero(as_tuple=True)[0]
|
||||
|
||||
if sel_idx.shape[0] == 0:
|
||||
continue
|
||||
|
||||
# [num_sel * Sk, D]
|
||||
sel_k = k_blocks[b, h, sel_idx].reshape(-1, D)
|
||||
sel_v = v_blocks[b, h, sel_idx].reshape(-1, D)
|
||||
|
||||
q = sparse_q[b, h, qb] # [Sq, D]
|
||||
scores = torch.matmul(q, sel_k.transpose(-1, -2)) / (D**0.5)
|
||||
weights = F.softmax(scores, dim=-1)
|
||||
output[b, h, qb] = torch.matmul(weights, sel_v)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def _compute_sparse_attention_flash(
|
||||
sparse_q: torch.Tensor,
|
||||
k_blocks: torch.Tensor,
|
||||
v_blocks: torch.Tensor,
|
||||
kv_mask: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
FlashAttention implementation with per-batch, per-head mask support.
|
||||
|
||||
Strategy: check if all heads share the same mask. If so, use a single
|
||||
FlashAttention call per batch (fast path). If not, process each head
|
||||
separately (correct path).
|
||||
|
||||
Args:
|
||||
sparse_q: [B, H, N, Sq, D]
|
||||
k_blocks: [B, H, N, Sk, D]
|
||||
v_blocks: [B, H, N, Sk, D]
|
||||
kv_mask: [B, H, N, N] boolean
|
||||
|
||||
Returns:
|
||||
output: [B, H, N, Sq, D]
|
||||
"""
|
||||
B, H, N, Sq, D = sparse_q.shape
|
||||
Sk = k_blocks.shape[3]
|
||||
device = sparse_q.device
|
||||
output = torch.zeros_like(sparse_q)
|
||||
|
||||
for b in range(B):
|
||||
# Check if all heads share the same mask for this batch element
|
||||
# Compare each head's mask to head 0's mask
|
||||
head0_mask = kv_mask[b, 0] # [N, N]
|
||||
all_heads_same = all(torch.equal(kv_mask[b, h], head0_mask) for h in range(1, H))
|
||||
|
||||
if all_heads_same:
|
||||
# Fast path: all heads share the same mask, single FA call
|
||||
_flash_attn_single_mask(
|
||||
sparse_q[b],
|
||||
k_blocks[b],
|
||||
v_blocks[b],
|
||||
head0_mask,
|
||||
output[b],
|
||||
H,
|
||||
N,
|
||||
Sq,
|
||||
Sk,
|
||||
D,
|
||||
device,
|
||||
)
|
||||
else:
|
||||
# Per-head path: process each head individually
|
||||
for h in range(H):
|
||||
head_mask = kv_mask[b, h] # [N, N]
|
||||
# Process single head: squeeze head dim, run FA, put back
|
||||
_flash_attn_single_head(
|
||||
sparse_q[b, h],
|
||||
k_blocks[b, h],
|
||||
v_blocks[b, h],
|
||||
head_mask,
|
||||
output,
|
||||
b,
|
||||
h,
|
||||
N,
|
||||
Sq,
|
||||
Sk,
|
||||
D,
|
||||
device,
|
||||
)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def _flash_attn_single_mask(
|
||||
sparse_q_b: torch.Tensor, # [H, N, Sq, D]
|
||||
k_blocks_b: torch.Tensor, # [H, N, Sk, D]
|
||||
v_blocks_b: torch.Tensor, # [H, N, Sk, D]
|
||||
mask: torch.Tensor, # [N, N] boolean
|
||||
output_b: torch.Tensor, # [H, N, Sq, D] (modified in-place)
|
||||
H: int,
|
||||
N: int,
|
||||
Sq: int,
|
||||
Sk: int,
|
||||
D: int,
|
||||
device: torch.device,
|
||||
) -> None:
|
||||
"""Run FlashAttention for all heads sharing the same KV mask."""
|
||||
q_list = []
|
||||
k_list = []
|
||||
v_list = []
|
||||
cu_seqlens_q = [0]
|
||||
cu_seqlens_k = [0]
|
||||
active_blocks = []
|
||||
|
||||
for qb in range(N):
|
||||
selected = mask[qb] # [N] boolean
|
||||
sel_idx = selected.nonzero(as_tuple=True)[0]
|
||||
|
||||
if sel_idx.shape[0] == 0:
|
||||
continue
|
||||
|
||||
active_blocks.append(qb)
|
||||
num_kv_tokens = sel_idx.shape[0] * Sk
|
||||
|
||||
# [H, Sq, D] -> [Sq, H, D]
|
||||
q_block = sparse_q_b[:, qb].permute(1, 0, 2)
|
||||
q_list.append(q_block)
|
||||
|
||||
# [H, num_sel, Sk, D] -> [num_kv_tokens, H, D]
|
||||
sel_k = k_blocks_b[:, sel_idx].permute(1, 2, 0, 3).reshape(num_kv_tokens, H, D)
|
||||
sel_v = v_blocks_b[:, sel_idx].permute(1, 2, 0, 3).reshape(num_kv_tokens, H, D)
|
||||
k_list.append(sel_k)
|
||||
v_list.append(sel_v)
|
||||
|
||||
cu_seqlens_q.append(cu_seqlens_q[-1] + Sq)
|
||||
cu_seqlens_k.append(cu_seqlens_k[-1] + num_kv_tokens)
|
||||
|
||||
if not q_list:
|
||||
return
|
||||
|
||||
flat_q = torch.cat(q_list, dim=0)
|
||||
flat_k = torch.cat(k_list, dim=0)
|
||||
flat_v = torch.cat(v_list, dim=0)
|
||||
|
||||
cu_seqlens_q_t = torch.tensor(cu_seqlens_q, dtype=torch.int32, device=device)
|
||||
cu_seqlens_k_t = torch.tensor(cu_seqlens_k, dtype=torch.int32, device=device)
|
||||
|
||||
max_seqlen_q = Sq
|
||||
max_seqlen_k = int((cu_seqlens_k_t[1:] - cu_seqlens_k_t[:-1]).max().item())
|
||||
|
||||
orig_dtype = flat_q.dtype
|
||||
compute_dtype = orig_dtype
|
||||
if compute_dtype not in (torch.float16, torch.bfloat16):
|
||||
compute_dtype = torch.bfloat16
|
||||
flat_q = flat_q.to(compute_dtype)
|
||||
flat_k = flat_k.to(compute_dtype)
|
||||
flat_v = flat_v.to(compute_dtype)
|
||||
|
||||
flat_out = flash_attn_varlen_func_impl(
|
||||
flat_q,
|
||||
flat_k,
|
||||
flat_v,
|
||||
cu_seqlens_q_t,
|
||||
cu_seqlens_k_t,
|
||||
max_seqlen_q,
|
||||
max_seqlen_k,
|
||||
causal=False,
|
||||
)
|
||||
|
||||
if compute_dtype != orig_dtype:
|
||||
flat_out = flat_out.to(orig_dtype)
|
||||
|
||||
idx = 0
|
||||
for qb in active_blocks:
|
||||
block_out = flat_out[idx:idx + Sq] # [Sq, H, D]
|
||||
output_b[:, qb] = block_out.permute(1, 0, 2) # [H, Sq, D]
|
||||
idx += Sq
|
||||
|
||||
|
||||
def _flash_attn_single_head(
|
||||
sparse_q_bh: torch.Tensor, # [N, Sq, D]
|
||||
k_blocks_bh: torch.Tensor, # [N, Sk, D]
|
||||
v_blocks_bh: torch.Tensor, # [N, Sk, D]
|
||||
mask: torch.Tensor, # [N, N] boolean
|
||||
output: torch.Tensor, # [B, H, N, Sq, D] (modified in-place)
|
||||
b: int,
|
||||
h: int,
|
||||
N: int,
|
||||
Sq: int,
|
||||
Sk: int,
|
||||
D: int,
|
||||
device: torch.device,
|
||||
) -> None:
|
||||
"""Run FlashAttention for a single head with its own KV mask."""
|
||||
q_list = []
|
||||
k_list = []
|
||||
v_list = []
|
||||
cu_seqlens_q = [0]
|
||||
cu_seqlens_k = [0]
|
||||
active_blocks = []
|
||||
|
||||
for qb in range(N):
|
||||
selected = mask[qb]
|
||||
sel_idx = selected.nonzero(as_tuple=True)[0]
|
||||
|
||||
if sel_idx.shape[0] == 0:
|
||||
continue
|
||||
|
||||
active_blocks.append(qb)
|
||||
num_kv_tokens = sel_idx.shape[0] * Sk
|
||||
|
||||
# [Sq, D] -> [Sq, 1, D] (single head)
|
||||
q_block = sparse_q_bh[qb].unsqueeze(1)
|
||||
q_list.append(q_block)
|
||||
|
||||
# [num_sel, Sk, D] -> [num_kv_tokens, 1, D]
|
||||
sel_k = k_blocks_bh[sel_idx].reshape(num_kv_tokens, 1, D)
|
||||
sel_v = v_blocks_bh[sel_idx].reshape(num_kv_tokens, 1, D)
|
||||
k_list.append(sel_k)
|
||||
v_list.append(sel_v)
|
||||
|
||||
cu_seqlens_q.append(cu_seqlens_q[-1] + Sq)
|
||||
cu_seqlens_k.append(cu_seqlens_k[-1] + num_kv_tokens)
|
||||
|
||||
if not q_list:
|
||||
return
|
||||
|
||||
flat_q = torch.cat(q_list, dim=0)
|
||||
flat_k = torch.cat(k_list, dim=0)
|
||||
flat_v = torch.cat(v_list, dim=0)
|
||||
|
||||
cu_seqlens_q_t = torch.tensor(cu_seqlens_q, dtype=torch.int32, device=device)
|
||||
cu_seqlens_k_t = torch.tensor(cu_seqlens_k, dtype=torch.int32, device=device)
|
||||
|
||||
max_seqlen_q = Sq
|
||||
max_seqlen_k = int((cu_seqlens_k_t[1:] - cu_seqlens_k_t[:-1]).max().item())
|
||||
|
||||
orig_dtype = flat_q.dtype
|
||||
compute_dtype = orig_dtype
|
||||
if compute_dtype not in (torch.float16, torch.bfloat16):
|
||||
compute_dtype = torch.bfloat16
|
||||
flat_q = flat_q.to(compute_dtype)
|
||||
flat_k = flat_k.to(compute_dtype)
|
||||
flat_v = flat_v.to(compute_dtype)
|
||||
|
||||
flat_out = flash_attn_varlen_func_impl(
|
||||
flat_q,
|
||||
flat_k,
|
||||
flat_v,
|
||||
cu_seqlens_q_t,
|
||||
cu_seqlens_k_t,
|
||||
max_seqlen_q,
|
||||
max_seqlen_k,
|
||||
causal=False,
|
||||
)
|
||||
|
||||
if compute_dtype != orig_dtype:
|
||||
flat_out = flat_out.to(orig_dtype)
|
||||
|
||||
idx = 0
|
||||
for qb in active_blocks:
|
||||
block_out = flat_out[idx:idx + Sq] # [Sq, 1, D]
|
||||
output[b, h, qb] = block_out.squeeze(1) # [Sq, D]
|
||||
idx += Sq
|
||||
|
||||
|
||||
def _reconstruct_pruned(
|
||||
sparse_output: torch.Tensor,
|
||||
keep_indices: torch.Tensor,
|
||||
block_size: int,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Scatter sparse output back to full block size.
|
||||
Pruned positions get nearest kept token's output.
|
||||
|
||||
Handles per-batch, per-head indices correctly.
|
||||
|
||||
Args:
|
||||
sparse_output: [B, H, N, keep_size, D]
|
||||
keep_indices: [B, H, N, keep_size]
|
||||
block_size: original tokens per block
|
||||
|
||||
Returns:
|
||||
full_output: [B, H, N, block_size, D]
|
||||
"""
|
||||
B, H, N, keep_size, D = sparse_output.shape
|
||||
device = sparse_output.device
|
||||
|
||||
if keep_size >= block_size:
|
||||
return sparse_output
|
||||
|
||||
full_output = torch.zeros(B, H, N, block_size, D, device=device, dtype=sparse_output.dtype)
|
||||
|
||||
# Scatter kept tokens
|
||||
idx_expand = keep_indices.unsqueeze(-1).expand(-1, -1, -1, -1, D)
|
||||
full_output.scatter_(3, idx_expand, sparse_output)
|
||||
|
||||
# Fill pruned positions with nearest kept token (vectorized)
|
||||
all_pos = torch.arange(block_size, device=device)
|
||||
|
||||
for b in range(B):
|
||||
for h in range(H):
|
||||
for n in range(N):
|
||||
kept = keep_indices[b, h, n] # [keep_size]
|
||||
|
||||
# Distance from every position to every kept position
|
||||
dists = (all_pos.view(-1, 1) - kept.view(1, -1)).abs()
|
||||
nearest_local_idx = dists.argmin(dim=1) # [block_size]
|
||||
|
||||
# Identify pruned positions
|
||||
is_pruned = torch.ones(block_size, dtype=torch.bool, device=device)
|
||||
is_pruned[kept] = False
|
||||
pruned_indices = is_pruned.nonzero(as_tuple=True)[0]
|
||||
|
||||
if pruned_indices.numel() > 0:
|
||||
src_indices = nearest_local_idx[pruned_indices]
|
||||
full_output[b, h, n, pruned_indices] = sparse_output[b, h, n, src_indices]
|
||||
|
||||
return full_output
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# FastVideo backend classes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class BSAAttentionBackend(AttentionBackend):
|
||||
|
||||
accept_output_buffer: bool = False
|
||||
|
||||
@staticmethod
|
||||
def get_supported_head_sizes() -> list[int]:
|
||||
return [64, 128]
|
||||
|
||||
@staticmethod
|
||||
def get_name() -> str:
|
||||
return "BSA_ATTN"
|
||||
|
||||
@staticmethod
|
||||
def get_impl_cls() -> type["BSAAttentionImpl"]:
|
||||
return BSAAttentionImpl
|
||||
|
||||
@staticmethod
|
||||
def get_metadata_cls() -> type["BSAAttentionMetadata"]:
|
||||
return BSAAttentionMetadata
|
||||
|
||||
@staticmethod
|
||||
def get_builder_cls() -> type["BSAAttentionMetadataBuilder"]:
|
||||
return BSAAttentionMetadataBuilder
|
||||
|
||||
|
||||
@dataclass
|
||||
class BSAAttentionMetadata(AttentionMetadata):
|
||||
current_timestep: int
|
||||
dit_seq_shape: tuple[int, int, int]
|
||||
total_seq_length: int
|
||||
num_blocks: int
|
||||
block_size: int
|
||||
tile_partition_indices: torch.LongTensor
|
||||
reverse_tile_partition_indices: torch.LongTensor
|
||||
# BSA-specific config
|
||||
query_keep_ratio: float
|
||||
kv_cumulative_threshold: float
|
||||
min_kv_blocks: int
|
||||
|
||||
|
||||
class BSAAttentionMetadataBuilder(AttentionMetadataBuilder):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def prepare(self):
|
||||
pass
|
||||
|
||||
def build(
|
||||
self,
|
||||
current_timestep: int,
|
||||
raw_latent_shape: tuple[int, int, int],
|
||||
patch_size: tuple[int, int, int],
|
||||
device: torch.device,
|
||||
bsa_query_keep_ratio: float = 0.5,
|
||||
bsa_kv_cumulative_threshold: float = 0.9,
|
||||
bsa_min_kv_blocks: int = 4,
|
||||
**kwargs: dict[str, Any],
|
||||
) -> "BSAAttentionMetadata":
|
||||
# Ensure patching does not drop tokens silently.
|
||||
assert all(r % p == 0 for r, p in zip(raw_latent_shape, patch_size, strict=False)), (
|
||||
"raw_latent_shape must be divisible by patch_size for BSA", )
|
||||
|
||||
dit_seq_shape = (
|
||||
raw_latent_shape[0] // patch_size[0],
|
||||
raw_latent_shape[1] // patch_size[1],
|
||||
raw_latent_shape[2] // patch_size[2],
|
||||
)
|
||||
|
||||
total_seq_length = math.prod(dit_seq_shape)
|
||||
block_size = math.prod(BSA_TILE_SIZE)
|
||||
# Require exact tiling to avoid reshape failures later.
|
||||
assert all(d % t == 0 for d, t in zip(dit_seq_shape, BSA_TILE_SIZE, strict=False)), (
|
||||
"dit_seq_shape must be divisible by BSA_TILE_SIZE", )
|
||||
num_blocks = total_seq_length // block_size
|
||||
|
||||
tile_partition_indices = get_tile_partition_indices(dit_seq_shape, BSA_TILE_SIZE, device)
|
||||
reverse_tile_partition_indices = get_reverse_tile_partition_indices(dit_seq_shape, BSA_TILE_SIZE, device)
|
||||
|
||||
return BSAAttentionMetadata(
|
||||
current_timestep=current_timestep,
|
||||
dit_seq_shape=dit_seq_shape,
|
||||
total_seq_length=total_seq_length,
|
||||
num_blocks=num_blocks,
|
||||
block_size=block_size,
|
||||
tile_partition_indices=tile_partition_indices,
|
||||
reverse_tile_partition_indices=reverse_tile_partition_indices,
|
||||
query_keep_ratio=bsa_query_keep_ratio,
|
||||
kv_cumulative_threshold=bsa_kv_cumulative_threshold,
|
||||
min_kv_blocks=bsa_min_kv_blocks,
|
||||
)
|
||||
|
||||
|
||||
class BSAAttentionImpl(AttentionImpl):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_heads: int,
|
||||
head_size: int,
|
||||
causal: bool,
|
||||
softmax_scale: float,
|
||||
num_kv_heads: int | None = None,
|
||||
prefix: str = "",
|
||||
**extra_impl_args,
|
||||
) -> None:
|
||||
self.prefix = prefix
|
||||
self.num_heads = num_heads
|
||||
self.head_size = head_size
|
||||
if num_kv_heads is not None and num_kv_heads != num_heads:
|
||||
raise ValueError("BSA backend does not support grouped-query attention")
|
||||
if causal:
|
||||
raise ValueError("BSA backend is bidirectional; causal=True is unsupported")
|
||||
if softmax_scale is not None:
|
||||
expected_scale = 1.0 / math.sqrt(self.head_size)
|
||||
if not math.isclose(softmax_scale, expected_scale, rel_tol=1e-4, abs_tol=1e-5):
|
||||
raise ValueError("softmax_scale must be default (1/sqrt(d)) for BSA")
|
||||
try:
|
||||
sp_group = get_sp_group()
|
||||
self.sp_size = sp_group.world_size
|
||||
except (AssertionError, RuntimeError):
|
||||
self.sp_size = 1
|
||||
|
||||
def preprocess_qkv(
|
||||
self,
|
||||
qkv: torch.Tensor,
|
||||
attn_metadata: BSAAttentionMetadata,
|
||||
) -> torch.Tensor:
|
||||
"""Reorder tokens from raster order to tile-contiguous order."""
|
||||
# qkv: [B, L, num_heads, D]
|
||||
return qkv[:, attn_metadata.tile_partition_indices]
|
||||
|
||||
def postprocess_output(
|
||||
self,
|
||||
output: torch.Tensor,
|
||||
attn_metadata: BSAAttentionMetadata,
|
||||
) -> torch.Tensor:
|
||||
"""Reorder tokens from tile-contiguous order back to raster order."""
|
||||
return output[:, attn_metadata.reverse_tile_partition_indices]
|
||||
|
||||
def forward(
|
||||
self,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
attn_metadata: BSAAttentionMetadata,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
BSA attention forward pass.
|
||||
|
||||
Input tensors are already in tile-contiguous order from preprocess_qkv.
|
||||
|
||||
Args:
|
||||
query: [B, L, num_heads, D] (tile-ordered)
|
||||
key: [B, L, num_heads, D] (tile-ordered)
|
||||
value: [B, L, num_heads, D] (tile-ordered)
|
||||
attn_metadata: BSA metadata
|
||||
|
||||
Returns:
|
||||
output: [B, L, num_heads, D] (tile-ordered)
|
||||
"""
|
||||
B, L, H, D = query.shape
|
||||
block_size = attn_metadata.block_size
|
||||
num_blocks = attn_metadata.num_blocks
|
||||
assert num_blocks * block_size == L, "Sequence length must match tiling"
|
||||
|
||||
# Reshape to [B, H, L, D] for attention computation
|
||||
q = query.transpose(1, 2).contiguous() # [B, H, L, D]
|
||||
k = key.transpose(1, 2).contiguous()
|
||||
v = value.transpose(1, 2).contiguous()
|
||||
|
||||
# Reshape into blocks: [B, H, num_blocks, block_size, D]
|
||||
q_blocks = q.view(B, H, num_blocks, block_size, D)
|
||||
k_blocks = k.view(B, H, num_blocks, block_size, D)
|
||||
v_blocks = v.view(B, H, num_blocks, block_size, D)
|
||||
|
||||
# --- Query sparsification ---
|
||||
sparse_q, keep_indices, keep_size = _prune_queries(q_blocks, attn_metadata.query_keep_ratio)
|
||||
|
||||
# --- KV block selection ---
|
||||
kv_mask = _select_kv_blocks(
|
||||
sparse_q,
|
||||
k_blocks,
|
||||
attn_metadata.kv_cumulative_threshold,
|
||||
attn_metadata.min_kv_blocks,
|
||||
)
|
||||
|
||||
# --- Sparse attention ---
|
||||
sparse_output = _compute_sparse_attention(sparse_q, k_blocks, v_blocks, kv_mask)
|
||||
|
||||
# --- Reconstruct pruned positions ---
|
||||
full_output = _reconstruct_pruned(sparse_output, keep_indices, block_size)
|
||||
|
||||
# Reshape back: [B, H, num_blocks, block_size, D] -> [B, H, L, D] -> [B, L, H, D]
|
||||
hidden_states = full_output.view(B, H, L, D).transpose(1, 2)
|
||||
|
||||
return hidden_states
|
||||
@@ -7,8 +7,9 @@ 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"
|
||||
"Cosmos25VideoConfig", "LongCatVideoConfig", "LTX2VideoConfig", "HYWorldConfig", "Kandinsky5VideoConfig"
|
||||
]
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from fastvideo.configs.models.dits.base import DiTArchConfig, DiTConfig
|
||||
|
||||
|
||||
def is_transformer_blocks(n: str, m) -> bool:
|
||||
return "transformer_blocks" in n and str.isdigit(n.split(".")[-1])
|
||||
|
||||
|
||||
@dataclass
|
||||
class Gen3CArchConfig(DiTArchConfig):
|
||||
"""Configuration for GEN3C architecture (VideoExtendGeneralDIT)."""
|
||||
|
||||
_fsdp_shard_conditions: list = field(default_factory=lambda: [is_transformer_blocks])
|
||||
|
||||
param_names_mapping: dict = field(
|
||||
default_factory=lambda: {
|
||||
# Official GEN3C checkpoint key naming to FastVideo mapping.
|
||||
# The official checkpoint uses nn.Sequential patterns like attn.to_q.0 (Linear)
|
||||
# and attn.to_q.1 (RMSNorm), and layer1/layer2 for MLP.
|
||||
#
|
||||
# Patch embedding: net.x_embedder.proj.1.weight -> patch_embed.proj.weight
|
||||
r"^net\.x_embedder\.proj\.1\.(.*)$": r"patch_embed.proj.\1",
|
||||
|
||||
# Time embedding: net.t_embedder.1.linear_*.weight -> time_embed.t_embedder.linear_*.weight
|
||||
r"^net\.t_embedder\.0\.(.*)$": r"time_embed.time_proj.\1",
|
||||
r"^net\.t_embedder\.1\.linear_1\.(.*)$": r"time_embed.t_embedder.linear_1.\1",
|
||||
r"^net\.t_embedder\.1\.linear_2\.(.*)$": r"time_embed.t_embedder.linear_2.\1",
|
||||
|
||||
# Augment sigma embedding (GEN3C-specific)
|
||||
r"^net\.augment_sigma_embedder\.0\.(.*)$": r"augment_sigma_embed.time_proj.\1",
|
||||
r"^net\.augment_sigma_embedder\.1\.linear_1\.(.*)$": r"augment_sigma_embed.t_embedder.linear_1.\1",
|
||||
r"^net\.augment_sigma_embedder\.1\.linear_2\.(.*)$": r"augment_sigma_embed.t_embedder.linear_2.\1",
|
||||
|
||||
# Affine embedding norm: net.affline_norm.weight -> affine_norm.weight
|
||||
# Note: "affline" is a typo in the official GEN3C checkpoint (should be "affine")
|
||||
r"^net\.affline_norm\.(.*)$": r"affine_norm.\1",
|
||||
|
||||
# Extra positional embeddings (learnable per-axis)
|
||||
r"^net\.extra_pos_embedder\.pos_emb_t$": r"learnable_pos_embed.pos_emb_t",
|
||||
r"^net\.extra_pos_embedder\.pos_emb_h$": r"learnable_pos_embed.pos_emb_h",
|
||||
r"^net\.extra_pos_embedder\.pos_emb_w$": r"learnable_pos_embed.pos_emb_w",
|
||||
|
||||
# Transformer blocks: net.blocks.blockN -> transformer_blocks.N
|
||||
# Official uses: block.attn.to_q.0 (Linear), block.attn.to_q.1 (QK RMSNorm)
|
||||
#
|
||||
# Self-attention (block index 0)
|
||||
r"^net\.blocks\.block(\d+)\.blocks\.0\.block\.attn\.to_q\.0\.(.*)$": r"transformer_blocks.\1.attn1.to_q.\2",
|
||||
r"^net\.blocks\.block(\d+)\.blocks\.0\.block\.attn\.to_q\.1\.(.*)$":
|
||||
r"transformer_blocks.\1.attn1.norm_q.\2",
|
||||
r"^net\.blocks\.block(\d+)\.blocks\.0\.block\.attn\.to_k\.0\.(.*)$": r"transformer_blocks.\1.attn1.to_k.\2",
|
||||
r"^net\.blocks\.block(\d+)\.blocks\.0\.block\.attn\.to_k\.1\.(.*)$":
|
||||
r"transformer_blocks.\1.attn1.norm_k.\2",
|
||||
r"^net\.blocks\.block(\d+)\.blocks\.0\.block\.attn\.to_v\.0\.(.*)$": r"transformer_blocks.\1.attn1.to_v.\2",
|
||||
r"^net\.blocks\.block(\d+)\.blocks\.0\.block\.attn\.to_out\.0\.(.*)$":
|
||||
r"transformer_blocks.\1.attn1.to_out.\2",
|
||||
# AdaLN modulation for self-attention
|
||||
r"^net\.blocks\.block(\d+)\.blocks\.0\.adaLN_modulation\.(.*)$":
|
||||
r"transformer_blocks.\1.adaln_modulation_self_attn.\2",
|
||||
|
||||
# Cross-attention (block index 1)
|
||||
r"^net\.blocks\.block(\d+)\.blocks\.1\.block\.attn\.to_q\.0\.(.*)$": r"transformer_blocks.\1.attn2.to_q.\2",
|
||||
r"^net\.blocks\.block(\d+)\.blocks\.1\.block\.attn\.to_q\.1\.(.*)$":
|
||||
r"transformer_blocks.\1.attn2.norm_q.\2",
|
||||
r"^net\.blocks\.block(\d+)\.blocks\.1\.block\.attn\.to_k\.0\.(.*)$": r"transformer_blocks.\1.attn2.to_k.\2",
|
||||
r"^net\.blocks\.block(\d+)\.blocks\.1\.block\.attn\.to_k\.1\.(.*)$":
|
||||
r"transformer_blocks.\1.attn2.norm_k.\2",
|
||||
r"^net\.blocks\.block(\d+)\.blocks\.1\.block\.attn\.to_v\.0\.(.*)$": r"transformer_blocks.\1.attn2.to_v.\2",
|
||||
r"^net\.blocks\.block(\d+)\.blocks\.1\.block\.attn\.to_out\.0\.(.*)$":
|
||||
r"transformer_blocks.\1.attn2.to_out.\2",
|
||||
# AdaLN modulation for cross-attention
|
||||
r"^net\.blocks\.block(\d+)\.blocks\.1\.adaLN_modulation\.(.*)$":
|
||||
r"transformer_blocks.\1.adaln_modulation_cross_attn.\2",
|
||||
|
||||
# MLP (block index 2): layer1 -> fc_in, layer2 -> fc_out
|
||||
r"^net\.blocks\.block(\d+)\.blocks\.2\.block\.layer1\.(.*)$": r"transformer_blocks.\1.mlp.fc_in.\2",
|
||||
r"^net\.blocks\.block(\d+)\.blocks\.2\.block\.layer2\.(.*)$": r"transformer_blocks.\1.mlp.fc_out.\2",
|
||||
# AdaLN modulation for MLP
|
||||
r"^net\.blocks\.block(\d+)\.blocks\.2\.adaLN_modulation\.(.*)$":
|
||||
r"transformer_blocks.\1.adaln_modulation_mlp.\2",
|
||||
|
||||
# Final layer: net.final_layer.linear -> final_layer.proj_out
|
||||
r"^net\.final_layer\.linear\.(.*)$": r"final_layer.proj_out.\1",
|
||||
# Final layer AdaLN: net.final_layer.adaLN_modulation -> final_layer.adaln_modulation
|
||||
r"^net\.final_layer\.adaLN_modulation\.(.*)$": r"final_layer.adaln_modulation.\1",
|
||||
|
||||
# Note: The following keys from official checkpoint are NOT mapped and can be safely ignored:
|
||||
# - net.pos_embedder.* (rope position embeddings computed dynamically)
|
||||
# - net.accum_* keys (training metadata)
|
||||
# - logvar.* (training-only module, not used in inference)
|
||||
})
|
||||
|
||||
lora_param_names_mapping: dict = field(
|
||||
default_factory=lambda: {
|
||||
r"^transformer_blocks\.(\d+)\.attn1\.to_q\.(.*)$": r"transformer_blocks.\1.attn1.to_q.\2",
|
||||
r"^transformer_blocks\.(\d+)\.attn1\.to_k\.(.*)$": r"transformer_blocks.\1.attn1.to_k.\2",
|
||||
r"^transformer_blocks\.(\d+)\.attn1\.to_v\.(.*)$": r"transformer_blocks.\1.attn1.to_v.\2",
|
||||
r"^transformer_blocks\.(\d+)\.attn1\.to_out\.(.*)$": r"transformer_blocks.\1.attn1.to_out.\2",
|
||||
r"^transformer_blocks\.(\d+)\.attn2\.to_q\.(.*)$": r"transformer_blocks.\1.attn2.to_q.\2",
|
||||
r"^transformer_blocks\.(\d+)\.attn2\.to_k\.(.*)$": r"transformer_blocks.\1.attn2.to_k.\2",
|
||||
r"^transformer_blocks\.(\d+)\.attn2\.to_v\.(.*)$": r"transformer_blocks.\1.attn2.to_v.\2",
|
||||
r"^transformer_blocks\.(\d+)\.attn2\.to_out\.(.*)$": r"transformer_blocks.\1.attn2.to_out.\2",
|
||||
r"^transformer_blocks\.(\d+)\.mlp\.(.*)$": r"transformer_blocks.\1.mlp.\2",
|
||||
})
|
||||
|
||||
# GEN3C architecture parameters
|
||||
# Base VAE latent channels
|
||||
in_channels: int = 16
|
||||
out_channels: int = 16
|
||||
|
||||
# Channels per 3D cache buffer: 16 (warped frame latent) + 16 (warped mask latent)
|
||||
CHANNELS_PER_BUFFER: int = 32
|
||||
|
||||
# Number of 3D cache buffers
|
||||
frame_buffer_max: int = 2
|
||||
|
||||
# Attention configuration (7B model: 32 heads x 128 dim = 4096 hidden)
|
||||
num_attention_heads: int = 32
|
||||
attention_head_dim: int = 128 # 4096 / 32
|
||||
num_layers: int = 28
|
||||
mlp_ratio: float = 4.0
|
||||
|
||||
# Text encoder configuration
|
||||
text_embed_dim: int = 1024
|
||||
|
||||
# AdaLN-LoRA configuration
|
||||
adaln_lora_dim: int = 256
|
||||
use_adaln_lora: bool = True
|
||||
|
||||
# GEN3C-specific: augment sigma embedding for conditioning noise augmentation
|
||||
# Note: The official GEN3C-Cosmos-7B checkpoint was trained without this
|
||||
add_augment_sigma_embedding: bool = False
|
||||
|
||||
# Position embedding configuration
|
||||
max_size: tuple[int, int, int] = (128, 240, 240) # T, H, W
|
||||
patch_size: tuple[int, int, int] = (1, 2, 2)
|
||||
rope_scale: tuple[float, float, float] = (2.0, 1.0, 1.0) # T, H, W scaling
|
||||
|
||||
# GEN3C uses learnable positional embeddings in addition to RoPE
|
||||
extra_pos_embed_type: str = "learnable"
|
||||
|
||||
# Padding mask handling
|
||||
concat_padding_mask: bool = True
|
||||
|
||||
# Cross-attention projection (not used in GEN3C 7B)
|
||||
use_crossattn_projection: bool = False
|
||||
|
||||
# RoPE FPS modulation
|
||||
rope_enable_fps_modulation: bool = True
|
||||
|
||||
# QK normalization
|
||||
qk_norm: str = "rms_norm"
|
||||
eps: float = 1e-6
|
||||
|
||||
# Affine embedding normalization
|
||||
affine_emb_norm: bool = True
|
||||
|
||||
# Block format (THWBD for GEN3C compatibility)
|
||||
block_x_format: str = "THWBD"
|
||||
|
||||
exclude_lora_layers: list[str] = field(default_factory=lambda: ["embedder"])
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
self.out_channels = self.out_channels or self.in_channels
|
||||
self.hidden_size = self.num_attention_heads * self.attention_head_dim
|
||||
self.num_channels_latents = self.in_channels
|
||||
|
||||
# Calculate total input channels for patch embedding:
|
||||
# - in_channels (16): VAE latent
|
||||
# - condition_video_input_mask (1): Binary mask for conditioning frames
|
||||
# - condition_video_pose (frame_buffer_max * 32): 3D cache buffers
|
||||
# - padding_mask (1 if concat_padding_mask): Padding mask
|
||||
self.buffer_channels = self.frame_buffer_max * self.CHANNELS_PER_BUFFER
|
||||
self.total_input_channels = (
|
||||
self.in_channels + # 16: VAE latent
|
||||
1 + # 1: condition_video_input_mask
|
||||
self.buffer_channels # 64: 3D cache buffers (2 * 32)
|
||||
)
|
||||
# padding_mask is added in build_patch_embed if concat_padding_mask=True
|
||||
|
||||
|
||||
@dataclass
|
||||
class Gen3CVideoConfig(DiTConfig):
|
||||
"""Configuration for GEN3C video generation model."""
|
||||
arch_config: DiTArchConfig = field(default_factory=Gen3CArchConfig)
|
||||
prefix: str = "Gen3C"
|
||||
@@ -0,0 +1,65 @@
|
||||
# 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"
|
||||
@@ -1,6 +1,7 @@
|
||||
from fastvideo.configs.models.vaes.cosmosvae import CosmosVAEConfig
|
||||
from fastvideo.configs.models.vaes.cosmos2_5vae import Cosmos25VAEConfig
|
||||
from fastvideo.configs.models.vaes.gamecraftvae import GameCraftVAEConfig
|
||||
from fastvideo.configs.models.vaes.gen3cvae import Gen3CVAEConfig
|
||||
from fastvideo.configs.models.vaes.hunyuanvae import HunyuanVAEConfig
|
||||
from fastvideo.configs.models.vaes.hunyuan15vae import Hunyuan15VAEConfig
|
||||
from fastvideo.configs.models.vaes.ltx2vae import LTX2VAEConfig
|
||||
@@ -12,6 +13,7 @@ __all__ = [
|
||||
"WanVAEConfig",
|
||||
"CosmosVAEConfig",
|
||||
"Cosmos25VAEConfig",
|
||||
"Gen3CVAEConfig",
|
||||
"Hunyuan15VAEConfig",
|
||||
"LTX2VAEConfig",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastvideo.configs.models.vaes.cosmosvae import CosmosVAEConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
class Gen3CVAEConfig(CosmosVAEConfig):
|
||||
"""
|
||||
GEN3C VAE config placeholder.
|
||||
|
||||
GEN3C uses tokenizer-backed VAE loading logic at runtime, but we keep a
|
||||
model-specific config class so pipeline/model configs stay model-scoped.
|
||||
"""
|
||||
@@ -0,0 +1,171 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import torch
|
||||
|
||||
from fastvideo.configs.models import DiTConfig, EncoderConfig, VAEConfig
|
||||
from fastvideo.configs.models.dits.gen3c import Gen3CVideoConfig
|
||||
from fastvideo.configs.models.encoders import BaseEncoderOutput
|
||||
from fastvideo.configs.models.encoders.base import TextEncoderArchConfig
|
||||
from fastvideo.configs.models.encoders.t5 import (T5LargeArchConfig, T5LargeConfig)
|
||||
from fastvideo.configs.models.vaes import Gen3CVAEConfig
|
||||
from fastvideo.configs.pipelines.base import PipelineConfig
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Gen3CT5LargeArchConfig(T5LargeArchConfig):
|
||||
"""T5 Large arch config that pads inputs to max_length.
|
||||
|
||||
GEN3C requires padded text encoder inputs, while the base
|
||||
T5 config no longer pads by default after the SP mask
|
||||
refactor [PR#1142](https://github.com/hao-ai-lab/FastVideo/pull/1142).
|
||||
"""
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
self.tokenizer_kwargs["padding"] = "max_length"
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Gen3CT5LargeConfig(T5LargeConfig):
|
||||
arch_config: TextEncoderArchConfig = field(default_factory=_Gen3CT5LargeArchConfig)
|
||||
prefix: str = "t5"
|
||||
|
||||
|
||||
def t5_large_postprocess_text(outputs: BaseEncoderOutput) -> torch.Tensor:
|
||||
"""Postprocess T5 Large text encoder outputs for GEN3C pipeline.
|
||||
|
||||
Return raw last_hidden_state without truncation/padding.
|
||||
"""
|
||||
hidden_state = outputs.last_hidden_state
|
||||
|
||||
if hidden_state is None:
|
||||
raise ValueError("T5 Large outputs missing last_hidden_state")
|
||||
|
||||
nan_count = torch.isnan(hidden_state).sum()
|
||||
if nan_count > 0:
|
||||
hidden_state = hidden_state.masked_fill(torch.isnan(hidden_state), 0.0)
|
||||
|
||||
# Zero out embeddings beyond actual sequence length (vectorized)
|
||||
if outputs.attention_mask is not None:
|
||||
attention_mask = outputs.attention_mask
|
||||
lengths = attention_mask.sum(dim=1)
|
||||
max_len = hidden_state.shape[1]
|
||||
mask = torch.arange(max_len, device=hidden_state.device)[None, :] >= lengths[:, None]
|
||||
hidden_state[mask] = 0.0
|
||||
|
||||
return hidden_state
|
||||
|
||||
|
||||
@dataclass
|
||||
class Gen3CConfig(PipelineConfig):
|
||||
"""Configuration for GEN3C Video Generation Pipeline.
|
||||
|
||||
GEN3C extends Cosmos with 3D cache for camera-controlled video generation.
|
||||
Key parameters:
|
||||
- frame_buffer_max: Number of 3D cache buffers (default: 2)
|
||||
- noise_aug_strength: Strength of noise augmentation per buffer
|
||||
- filter_points_threshold: Threshold for filtering unreliable depth points
|
||||
"""
|
||||
|
||||
dit_config: DiTConfig = field(default_factory=Gen3CVideoConfig)
|
||||
|
||||
vae_config: VAEConfig = field(default_factory=Gen3CVAEConfig)
|
||||
|
||||
text_encoder_configs: tuple[EncoderConfig, ...] = field(default_factory=lambda: (_Gen3CT5LargeConfig(), ))
|
||||
postprocess_text_funcs: tuple[Callable[[BaseEncoderOutput], torch.Tensor],
|
||||
...] = field(default_factory=lambda: (t5_large_postprocess_text, ))
|
||||
|
||||
dit_precision: str = "bf16"
|
||||
vae_precision: str = "bf16"
|
||||
text_encoder_precisions: tuple[str, ...] = field(default_factory=lambda: ("bf16", ))
|
||||
|
||||
# GEN3C-specific conditioning parameters
|
||||
conditioning_strategy: str = "frame_replace"
|
||||
min_num_conditional_frames: int = 1
|
||||
max_num_conditional_frames: int = 2
|
||||
# Match official GEN3C/Cosmos inference defaults.
|
||||
sigma_conditional: float = 0.001
|
||||
sigma_data: float = 0.5
|
||||
state_ch: int = 16
|
||||
state_t: int = 16 # GEN3C uses 16 latent frames (121 pixel frames)
|
||||
text_encoder_class: str = "T5"
|
||||
|
||||
# Flow matching parameters
|
||||
embedded_cfg_scale: int = 6
|
||||
flow_shift: float = 1.0
|
||||
|
||||
# GEN3C 3D Cache parameters
|
||||
frame_buffer_max: int = 2
|
||||
noise_aug_strength: float = 0.0
|
||||
filter_points_threshold: float = 0.05
|
||||
|
||||
# Depth estimation settings
|
||||
use_moge_depth: bool = True
|
||||
moge_model_name: str = "Ruicheng/moge-vitl"
|
||||
offload_moge_after_depth: bool = True
|
||||
|
||||
# Camera trajectory settings (matching NVIDIA inference defaults)
|
||||
default_trajectory_type: str = "left"
|
||||
default_movement_distance: float = 0.3
|
||||
default_camera_rotation: str = "center_facing"
|
||||
|
||||
# Video generation settings
|
||||
# Match official GEN3C defaults (height=704, width=1280).
|
||||
video_resolution: tuple[int, int] = (704, 1280) # H, W
|
||||
num_frames: int = 121 # Default number of frames to generate
|
||||
|
||||
# Generation frame rate
|
||||
fps: int = 24
|
||||
|
||||
# Explicit CFG behavior policy:
|
||||
# - "legacy": CFG branch only when guidance_scale > 1.0
|
||||
# - "official_uncond_at_unity": also run uncond branch at guidance_scale == 1.0
|
||||
cfg_behavior: str = "legacy"
|
||||
default_negative_prompt: str = (
|
||||
"The video captures a series of frames showing ugly scenes, static with no motion, motion blur, "
|
||||
"over-saturation, shaky footage, low resolution, grainy texture, pixelated images, poorly lit areas, "
|
||||
"underexposed and overexposed scenes, poor color balance, washed out colors, choppy sequences, "
|
||||
"jerky movements, low frame rate, artifacting, color banding, unnatural transitions, outdated special "
|
||||
"effects, fake elements, unconvincing visuals, poorly edited content, jump cuts, visual noise, and "
|
||||
"flickering. Overall, the video is of poor quality.")
|
||||
|
||||
# Autoregressive generation settings
|
||||
autoregressive_chunk_frames: int = 121 # Frames per chunk
|
||||
autoregressive_overlap_frames: int = 1 # Overlap between chunks
|
||||
|
||||
def __post_init__(self):
|
||||
self.vae_config.load_encoder = True
|
||||
self.vae_config.load_decoder = True
|
||||
|
||||
self._vae_latent_dim = 16
|
||||
|
||||
# Validate frame buffer configuration matches DiT
|
||||
if hasattr(self.dit_config, 'arch_config'):
|
||||
arch_config = self.dit_config.arch_config
|
||||
if (hasattr(arch_config, 'frame_buffer_max') and arch_config.frame_buffer_max != self.frame_buffer_max):
|
||||
raise ValueError(f"frame_buffer_max mismatch: pipeline config has {self.frame_buffer_max}, "
|
||||
f"DiT config has {arch_config.frame_buffer_max}")
|
||||
|
||||
allowed_cfg_behavior = {"legacy", "official_uncond_at_unity"}
|
||||
if self.cfg_behavior not in allowed_cfg_behavior:
|
||||
raise ValueError(f"cfg_behavior must be one of {sorted(allowed_cfg_behavior)}, got {self.cfg_behavior!r}")
|
||||
|
||||
|
||||
@dataclass
|
||||
class Gen3CInferenceConfig(Gen3CConfig):
|
||||
"""Configuration for GEN3C inference with optimized defaults."""
|
||||
|
||||
# Use smaller batch sizes for inference
|
||||
batch_size: int = 1
|
||||
|
||||
# Enable gradient checkpointing for memory efficiency
|
||||
gradient_checkpointing: bool = False
|
||||
|
||||
# Inference-specific parameters
|
||||
guidance_scale: float = 1.0
|
||||
num_inference_steps: int = 35
|
||||
|
||||
# Disable noise augmentation during inference
|
||||
noise_aug_strength: float = 0.0
|
||||
@@ -72,6 +72,14 @@ class SamplingParam:
|
||||
boundary_ratio: float | None = None
|
||||
sigmas: list[float] | None = None
|
||||
|
||||
# TeaCache parameters
|
||||
enable_teacache: bool = False
|
||||
|
||||
# GEN3C camera control
|
||||
trajectory_type: str | None = None
|
||||
movement_distance: float | None = None
|
||||
camera_rotation: str | None = None
|
||||
|
||||
# Misc
|
||||
save_video: bool = True
|
||||
return_frames: bool = True
|
||||
@@ -95,17 +103,55 @@ class SamplingParam:
|
||||
self.__post_init__()
|
||||
|
||||
@classmethod
|
||||
def from_pretrained(cls, model_path: str) -> "SamplingParam":
|
||||
from fastvideo.registry import get_sampling_param_cls_for_name
|
||||
sampling_cls = get_sampling_param_cls_for_name(model_path)
|
||||
if sampling_cls is not None:
|
||||
sampling_param: SamplingParam = sampling_cls()
|
||||
else:
|
||||
logger.warning("Couldn't find an optimal sampling param for %s. Using the default sampling param.",
|
||||
model_path)
|
||||
sampling_param = cls()
|
||||
def _from_profile(
|
||||
cls,
|
||||
profile_name: str,
|
||||
) -> "SamplingParam":
|
||||
"""Create a ``SamplingParam`` with profile defaults applied.
|
||||
|
||||
return sampling_param
|
||||
Looks up *profile_name* in the registered profile tables,
|
||||
creates a base ``SamplingParam()``, then delegates to
|
||||
:meth:`update` so that ``__post_init__`` is called and
|
||||
derived fields stay consistent.
|
||||
"""
|
||||
from fastvideo.pipelines.basic.hunyuan.profiles import (
|
||||
PROFILES as _hunyuan_profiles, )
|
||||
|
||||
# Merged lazily; extend this dict as more model families
|
||||
# migrate to profile-based defaults.
|
||||
all_profiles = _hunyuan_profiles
|
||||
|
||||
if profile_name not in all_profiles:
|
||||
raise ValueError(f"Unknown profile '{profile_name}'. "
|
||||
f"Available: {sorted(all_profiles)}")
|
||||
|
||||
instance = cls()
|
||||
instance.update(all_profiles[profile_name].defaults)
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def from_pretrained(cls, model_path: str) -> "SamplingParam":
|
||||
from fastvideo.registry import _get_config_info
|
||||
|
||||
config_info = _get_config_info(model_path, raise_on_missing=False)
|
||||
|
||||
if config_info is None:
|
||||
logger.warning(
|
||||
"Couldn't find an optimal sampling param "
|
||||
"for %s. Using the default sampling param.",
|
||||
model_path,
|
||||
)
|
||||
return cls()
|
||||
|
||||
# Profile-based path (preferred for new migrations).
|
||||
if config_info.default_profile is not None:
|
||||
return cls._from_profile(config_info.default_profile)
|
||||
|
||||
# Legacy path: use the registered subclass directly.
|
||||
if config_info.sampling_param_cls is not None:
|
||||
return config_info.sampling_param_cls()
|
||||
|
||||
return cls()
|
||||
|
||||
@staticmethod
|
||||
def add_cli_args(parser: Any) -> Any:
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastvideo.configs.sample.base import SamplingParam
|
||||
|
||||
|
||||
@dataclass
|
||||
class Gen3C_Cosmos_7B_SamplingParam(SamplingParam):
|
||||
"""Defaults for GEN3C (Cosmos-7B) camera-controlled video generation."""
|
||||
|
||||
# Video parameters (matching official GEN3C defaults)
|
||||
height: int = 704
|
||||
width: int = 1280
|
||||
num_frames: int = 121
|
||||
fps: int = 24
|
||||
|
||||
# Denoising stage
|
||||
guidance_scale: float = 1.0
|
||||
num_inference_steps: int = 35
|
||||
|
||||
# GEN3C camera control defaults
|
||||
trajectory_type: str = "left"
|
||||
movement_distance: float = 0.3
|
||||
camera_rotation: str = "center_facing"
|
||||
@@ -1,21 +1,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from dataclasses import dataclass
|
||||
"""Hunyuan sampling parameter classes.
|
||||
|
||||
from fastvideo.configs.sample.base import SamplingParam
|
||||
|
||||
|
||||
@dataclass
|
||||
class HunyuanSamplingParam(SamplingParam):
|
||||
num_inference_steps: int = 50
|
||||
|
||||
num_frames: int = 125
|
||||
height: int = 720
|
||||
width: int = 1280
|
||||
fps: int = 24
|
||||
|
||||
guidance_scale: float = 1.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class FastHunyuanSamplingParam(HunyuanSamplingParam):
|
||||
num_inference_steps: int = 6
|
||||
Hunyuan model-specific SamplingParam subclasses have been removed.
|
||||
Defaults are now provided by pipeline profiles in
|
||||
``fastvideo/pipelines/basic/hunyuan/profiles.py``.
|
||||
"""
|
||||
|
||||
@@ -72,9 +72,12 @@ 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)
|
||||
|
||||
assert len(
|
||||
batch.image_embeds) == 1, "image embedding should be a single tensor"
|
||||
image_embeds = batch.image_embeds[0]
|
||||
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
|
||||
image_latent = batch.image_latent
|
||||
pil_image = batch.pil_image
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ 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,
|
||||
@@ -160,5 +161,36 @@ 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
|
||||
|
||||
@@ -7,7 +7,7 @@ Example usage:
|
||||
# launch a server and benchmark on it
|
||||
|
||||
# T2V or T2I or any other multimodal generation model
|
||||
fastvideo serve --model-path Wan-AI/Wan2.1-T2V-1.3B-Diffusers --port 8000
|
||||
fastvideo serve --config serve.yaml
|
||||
|
||||
# benchmark it and make sure the port is the same as the server's port
|
||||
fastvideo bench --dataset vbench --num-prompts 20 --port 8000
|
||||
|
||||
@@ -2,19 +2,17 @@
|
||||
# adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/entrypoints/cli/serve.py
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
import os
|
||||
from typing import cast
|
||||
|
||||
from fastvideo import VideoGenerator
|
||||
from fastvideo.configs.sample.base import SamplingParam
|
||||
from fastvideo.entrypoints.cli.cli_types import CLISubcommand
|
||||
from fastvideo.entrypoints.cli.utils import RaiseNotImplementedAction
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.entrypoints.cli.inference_config import build_generate_run_config
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.utils import FlexibleArgumentParser
|
||||
|
||||
logger = init_logger(__name__)
|
||||
_VALIDATED_RUN_CONFIG_ATTR = "_fastvideo_validated_run_config"
|
||||
|
||||
|
||||
class GenerateSubcommand(CLISubcommand):
|
||||
@@ -23,89 +21,47 @@ class GenerateSubcommand(CLISubcommand):
|
||||
def __init__(self) -> None:
|
||||
self.name = "generate"
|
||||
super().__init__()
|
||||
self.init_arg_names = self._get_init_arg_names()
|
||||
self.generation_arg_names = self._get_generation_arg_names()
|
||||
|
||||
def _get_init_arg_names(self) -> list[str]:
|
||||
"""Get names of arguments for VideoGenerator initialization"""
|
||||
return ["num_gpus", "tp_size", "sp_size", "model_path"]
|
||||
|
||||
def _get_generation_arg_names(self) -> list[str]:
|
||||
"""Get names of arguments for generate_video method"""
|
||||
return [field.name for field in dataclasses.fields(SamplingParam)]
|
||||
|
||||
def cmd(self, args: argparse.Namespace) -> None:
|
||||
excluded_args = ['subparser', 'config', 'dispatch_function']
|
||||
run_config = getattr(args, _VALIDATED_RUN_CONFIG_ATTR, None)
|
||||
if run_config is None:
|
||||
run_config = build_generate_run_config(
|
||||
args,
|
||||
overrides=getattr(args, "_unknown", None),
|
||||
)
|
||||
logger.info("CLI generate config: %s", run_config)
|
||||
|
||||
provided_args = {}
|
||||
for k, v in vars(args).items():
|
||||
if (k not in excluded_args and v is not None and hasattr(args, '_provided') and k in args._provided):
|
||||
provided_args[k] = v
|
||||
|
||||
if 'model_path' in vars(args) and args.model_path is not None:
|
||||
provided_args['model_path'] = args.model_path
|
||||
|
||||
if 'prompt' in vars(args) and args.prompt is not None:
|
||||
provided_args['prompt'] = args.prompt
|
||||
|
||||
merged_args = {**provided_args}
|
||||
|
||||
logger.info('CLI Args: %s', merged_args)
|
||||
|
||||
if 'model_path' not in merged_args or not merged_args['model_path']:
|
||||
raise ValueError("model_path must be provided either in config file or via --model-path")
|
||||
|
||||
# Check if either prompt or prompt_txt is provided
|
||||
has_prompt = 'prompt' in merged_args and merged_args['prompt']
|
||||
has_prompt_txt = 'prompt_txt' in merged_args and merged_args['prompt_txt']
|
||||
|
||||
if not (has_prompt or has_prompt_txt):
|
||||
raise ValueError("Either prompt or prompt_txt must be provided")
|
||||
|
||||
if has_prompt and has_prompt_txt:
|
||||
raise ValueError("Cannot provide both 'prompt' and 'prompt_txt'. Use only one of them.")
|
||||
|
||||
init_args = {k: v for k, v in merged_args.items() if k not in self.generation_arg_names}
|
||||
generation_args = {k: v for k, v in merged_args.items() if k in self.generation_arg_names}
|
||||
generation_args.setdefault("return_frames", False)
|
||||
|
||||
model_path = init_args.pop('model_path')
|
||||
prompt = generation_args.pop('prompt', None)
|
||||
|
||||
generator = VideoGenerator.from_pretrained(model_path=model_path, **init_args)
|
||||
|
||||
# Call generate_video - it handles both single and batch modes
|
||||
generator.generate_video(prompt=prompt, **generation_args)
|
||||
generator = VideoGenerator.from_config(run_config.generator)
|
||||
generator.generate(run_config.request)
|
||||
|
||||
def validate(self, args: argparse.Namespace) -> None:
|
||||
"""Validate the arguments for this command"""
|
||||
if args.num_gpus is not None and args.num_gpus <= 0:
|
||||
raise ValueError("Number of gpus must be positive")
|
||||
|
||||
if args.config and not os.path.exists(args.config):
|
||||
if not args.config:
|
||||
raise ValueError("fastvideo generate requires --config PATH; use a nested "
|
||||
"run config plus optional dotted overrides")
|
||||
if not os.path.exists(args.config):
|
||||
raise ValueError(f"Config file not found: {args.config}")
|
||||
setattr(
|
||||
args,
|
||||
_VALIDATED_RUN_CONFIG_ATTR,
|
||||
build_generate_run_config(
|
||||
args,
|
||||
overrides=getattr(args, "_unknown", None),
|
||||
),
|
||||
)
|
||||
|
||||
def subparser_init(self, subparsers: argparse._SubParsersAction) -> FlexibleArgumentParser:
|
||||
generate_parser = subparsers.add_parser(
|
||||
"generate",
|
||||
help="Run inference on a model",
|
||||
usage="fastvideo generate (--model-path MODEL_PATH_OR_ID --prompt PROMPT) | --config CONFIG_FILE [OPTIONS]")
|
||||
usage="fastvideo generate --config RUN_CONFIG [--dotted.override VALUE]")
|
||||
|
||||
generate_parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
default='',
|
||||
required=False,
|
||||
help="Read CLI options from a config JSON or YAML file. If provided, --model-path and --prompt are optional."
|
||||
)
|
||||
|
||||
generate_parser = FastVideoArgs.add_cli_args(generate_parser)
|
||||
generate_parser = SamplingParam.add_cli_args(generate_parser)
|
||||
|
||||
generate_parser.add_argument(
|
||||
"--text-encoder-configs",
|
||||
action=RaiseNotImplementedAction,
|
||||
help="JSON array of text encoder configurations (NOT YET IMPLEMENTED)",
|
||||
help="Path to a nested run config JSON or YAML file. Required.",
|
||||
)
|
||||
|
||||
return cast(FlexibleArgumentParser, generate_parser)
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from collections.abc import Mapping
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
from fastvideo.api.overrides import apply_overrides, parse_cli_overrides
|
||||
from fastvideo.api.parser import load_raw_config, parse_config
|
||||
from fastvideo.api.schema import RunConfig, ServeConfig
|
||||
|
||||
_GENERATE_OVERRIDE_PREFIXES = ("generator.", "request.")
|
||||
_SERVE_OVERRIDE_PREFIXES = (
|
||||
"generator.",
|
||||
"server.",
|
||||
"default_request.",
|
||||
)
|
||||
|
||||
|
||||
def build_generate_run_config(
|
||||
args: argparse.Namespace,
|
||||
overrides: list[str] | None = None,
|
||||
) -> RunConfig:
|
||||
raw = _load_nested_config(getattr(args, "config", None))
|
||||
raw.setdefault("request", {})
|
||||
raw = _apply_dotted_overrides(
|
||||
raw,
|
||||
overrides,
|
||||
allowed_prefixes=_GENERATE_OVERRIDE_PREFIXES,
|
||||
)
|
||||
_ensure_generate_cli_defaults(raw)
|
||||
config = parse_config(RunConfig, raw)
|
||||
_validate_num_gpus(config.generator.engine.num_gpus)
|
||||
_validate_generate_prompt_sources(config)
|
||||
return config
|
||||
|
||||
|
||||
def build_serve_config(
|
||||
args: argparse.Namespace,
|
||||
overrides: list[str] | None = None,
|
||||
) -> ServeConfig:
|
||||
raw = _load_nested_config(getattr(args, "config", None))
|
||||
raw.setdefault("server", {})
|
||||
raw.setdefault("default_request", {})
|
||||
raw = _apply_dotted_overrides(
|
||||
raw,
|
||||
overrides,
|
||||
allowed_prefixes=_SERVE_OVERRIDE_PREFIXES,
|
||||
)
|
||||
config = parse_config(ServeConfig, raw)
|
||||
_validate_num_gpus(config.generator.engine.num_gpus)
|
||||
return config
|
||||
|
||||
|
||||
def _load_nested_config(path: str | None) -> dict[str, Any]:
|
||||
if not path:
|
||||
raise ValueError("Inference CLI requires --config PATH; use a nested config file "
|
||||
"plus optional dotted overrides")
|
||||
|
||||
raw = load_raw_config(path)
|
||||
if not isinstance(raw.get("generator"), Mapping):
|
||||
raise ValueError("Inference config must use the nested schema with a top-level "
|
||||
"'generator' mapping")
|
||||
return deepcopy(dict(raw))
|
||||
|
||||
|
||||
def _apply_dotted_overrides(
|
||||
raw: Mapping[str, Any],
|
||||
overrides: list[str] | None,
|
||||
*,
|
||||
allowed_prefixes: tuple[str, ...],
|
||||
) -> dict[str, Any]:
|
||||
if not overrides:
|
||||
return deepcopy(dict(raw))
|
||||
|
||||
parsed = parse_cli_overrides(overrides)
|
||||
for key in parsed:
|
||||
if "." not in key:
|
||||
raise ValueError("CLI overrides must use dotted config paths like "
|
||||
"--request.sampling.seed 42")
|
||||
if not key.startswith(allowed_prefixes):
|
||||
allowed = ", ".join(allowed_prefixes)
|
||||
raise ValueError(f"Unsupported override path {key!r}. Allowed prefixes: {allowed}")
|
||||
return apply_overrides(raw, parsed)
|
||||
|
||||
|
||||
def _ensure_generate_cli_defaults(raw: dict[str, Any]) -> None:
|
||||
request = raw.setdefault("request", {})
|
||||
output = request.setdefault("output", {})
|
||||
output.setdefault("return_frames", False)
|
||||
|
||||
|
||||
def _validate_generate_prompt_sources(config: RunConfig) -> None:
|
||||
has_prompt = config.request.prompt is not None
|
||||
has_prompt_path = config.request.inputs.prompt_path is not None
|
||||
if not (has_prompt or has_prompt_path):
|
||||
raise ValueError("Either request.prompt or request.inputs.prompt_path must be provided")
|
||||
if has_prompt and has_prompt_path:
|
||||
raise ValueError("Cannot provide both request.prompt and request.inputs.prompt_path")
|
||||
|
||||
|
||||
def _validate_num_gpus(num_gpus: int) -> None:
|
||||
if num_gpus <= 0:
|
||||
raise ValueError(f"generator.engine.num_gpus must be > 0; got {num_gpus}")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"build_generate_run_config",
|
||||
"build_serve_config",
|
||||
]
|
||||
@@ -1,6 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/entrypoints/cli/main.py
|
||||
|
||||
from fastvideo.entrypoints.cli.cli_types import CLISubcommand
|
||||
from fastvideo.entrypoints.cli.generate import cmd_init as generate_cmd_init
|
||||
from fastvideo.utils import FlexibleArgumentParser
|
||||
@@ -27,14 +26,17 @@ def main() -> None:
|
||||
for cmd in cmd_init():
|
||||
cmd.subparser_init(subparsers).set_defaults(dispatch_function=cmd.cmd)
|
||||
cmds[cmd.name] = cmd
|
||||
args = parser.parse_args()
|
||||
|
||||
args, unknown = parser.parse_known_args()
|
||||
if unknown and args.subparser not in {"generate", "serve"}:
|
||||
parser.error(f"unrecognized arguments: {' '.join(unknown)}")
|
||||
args._unknown = unknown
|
||||
if args.subparser in cmds:
|
||||
cmds[args.subparser].validate(args)
|
||||
|
||||
if hasattr(args, "dispatch_function"):
|
||||
args.dispatch_function(args)
|
||||
else:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
# adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/entrypoints/cli/serve.py
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from typing import cast
|
||||
|
||||
from fastvideo.api.compat import generator_config_to_fastvideo_args
|
||||
from fastvideo.api.request_metadata import EXPLICIT_REQUEST_ATTR
|
||||
from fastvideo.entrypoints.cli.cli_types import CLISubcommand
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.entrypoints.cli.inference_config import build_serve_config
|
||||
from fastvideo.logger import init_logger
|
||||
from fastvideo.utils import FlexibleArgumentParser
|
||||
|
||||
logger = init_logger(__name__)
|
||||
_VALIDATED_SERVE_CONFIG_ATTR = "_fastvideo_validated_serve_config"
|
||||
|
||||
|
||||
class ServeSubcommand(CLISubcommand):
|
||||
@@ -20,94 +24,67 @@ class ServeSubcommand(CLISubcommand):
|
||||
super().__init__()
|
||||
|
||||
def cmd(self, args: argparse.Namespace) -> None:
|
||||
excluded_args = {
|
||||
"subparser",
|
||||
"config",
|
||||
"dispatch_function",
|
||||
"host",
|
||||
"port",
|
||||
"output_dir",
|
||||
}
|
||||
|
||||
provided: set[str] = getattr(args, '_provided', set())
|
||||
cli_kwargs = {}
|
||||
for k, v in vars(args).items():
|
||||
if k in excluded_args:
|
||||
continue
|
||||
if k == '_provided':
|
||||
continue
|
||||
if k in provided and v is not None:
|
||||
cli_kwargs[k] = v
|
||||
|
||||
if 'model_path' not in cli_kwargs and args.model_path is not None:
|
||||
cli_kwargs['model_path'] = args.model_path
|
||||
|
||||
if not cli_kwargs.get('model_path'):
|
||||
raise ValueError("model_path must be provided via --model-path")
|
||||
serve_config = getattr(args, _VALIDATED_SERVE_CONFIG_ATTR, None)
|
||||
if serve_config is None:
|
||||
serve_config = build_serve_config(
|
||||
args,
|
||||
overrides=getattr(args, "_unknown", None),
|
||||
)
|
||||
explicit_raw = getattr(
|
||||
serve_config.default_request,
|
||||
EXPLICIT_REQUEST_ATTR,
|
||||
None,
|
||||
)
|
||||
if explicit_raw:
|
||||
raise NotImplementedError("ServeConfig.default_request is not wired into the OpenAI "
|
||||
"server yet")
|
||||
|
||||
from fastvideo.entrypoints.openai.api_server import (
|
||||
DEFAULT_HOST,
|
||||
DEFAULT_OUTPUT_DIR,
|
||||
DEFAULT_PORT,
|
||||
run_server,
|
||||
run_server, )
|
||||
|
||||
logger.info("CLI serve config: %s", serve_config)
|
||||
logger.info(
|
||||
"Server will listen on %s:%d",
|
||||
serve_config.server.host,
|
||||
serve_config.server.port,
|
||||
)
|
||||
|
||||
host = getattr(args, "host", DEFAULT_HOST)
|
||||
port = getattr(args, "port", DEFAULT_PORT)
|
||||
output_dir = getattr(args, "output_dir", DEFAULT_OUTPUT_DIR)
|
||||
|
||||
logger.info("CLI serve args: %s", cli_kwargs)
|
||||
logger.info("Server will listen on %s:%d", host, port)
|
||||
|
||||
fastvideo_args = FastVideoArgs.from_kwargs(**cli_kwargs)
|
||||
run_server(fastvideo_args, host=host, port=port, output_dir=output_dir)
|
||||
fastvideo_args = generator_config_to_fastvideo_args(serve_config.generator)
|
||||
run_server(
|
||||
fastvideo_args,
|
||||
host=serve_config.server.host,
|
||||
port=serve_config.server.port,
|
||||
output_dir=serve_config.server.output_dir,
|
||||
)
|
||||
|
||||
def validate(self, args: argparse.Namespace) -> None:
|
||||
if args.num_gpus is not None and args.num_gpus <= 0:
|
||||
raise ValueError("Number of gpus must be positive")
|
||||
|
||||
def subparser_init(self, subparsers: argparse._SubParsersAction) -> FlexibleArgumentParser:
|
||||
from fastvideo.entrypoints.openai.api_server import (
|
||||
DEFAULT_HOST,
|
||||
DEFAULT_OUTPUT_DIR,
|
||||
DEFAULT_PORT,
|
||||
if not args.config:
|
||||
raise ValueError("fastvideo serve requires --config PATH; use a nested "
|
||||
"serve config plus optional dotted overrides")
|
||||
if not os.path.exists(args.config):
|
||||
raise ValueError(f"Config file not found: {args.config}")
|
||||
setattr(
|
||||
args,
|
||||
_VALIDATED_SERVE_CONFIG_ATTR,
|
||||
build_serve_config(
|
||||
args,
|
||||
overrides=getattr(args, "_unknown", None),
|
||||
),
|
||||
)
|
||||
|
||||
def subparser_init(self, subparsers: argparse._SubParsersAction) -> FlexibleArgumentParser:
|
||||
serve_parser = subparsers.add_parser(
|
||||
"serve",
|
||||
help="Start an OpenAI-compatible HTTP server",
|
||||
usage=("fastvideo serve --model-path MODEL_PATH_OR_ID "
|
||||
"[--host HOST] [--port PORT] [OPTIONS]"),
|
||||
)
|
||||
|
||||
serve_parser.add_argument(
|
||||
"--host",
|
||||
type=str,
|
||||
default=DEFAULT_HOST,
|
||||
help=f"Host to bind the server to (default: {DEFAULT_HOST})",
|
||||
)
|
||||
serve_parser.add_argument(
|
||||
"--port",
|
||||
type=int,
|
||||
default=DEFAULT_PORT,
|
||||
help=f"Port to listen on (default: {DEFAULT_PORT})",
|
||||
)
|
||||
serve_parser.add_argument(
|
||||
"--output-dir",
|
||||
type=str,
|
||||
default=DEFAULT_OUTPUT_DIR,
|
||||
help=("Directory for generated outputs "
|
||||
f"(default: {DEFAULT_OUTPUT_DIR})"),
|
||||
usage="fastvideo serve --config SERVE_CONFIG [--dotted.override VALUE]",
|
||||
)
|
||||
serve_parser.add_argument(
|
||||
"--config",
|
||||
type=str,
|
||||
default="",
|
||||
required=False,
|
||||
help="Read CLI options from a config JSON or YAML file.",
|
||||
help="Path to a nested config JSON or YAML file. Required.",
|
||||
)
|
||||
|
||||
serve_parser = FastVideoArgs.add_cli_args(serve_parser)
|
||||
return cast(FlexibleArgumentParser, serve_parser)
|
||||
|
||||
|
||||
|
||||
@@ -8,8 +8,12 @@ diffusion models.
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import threading
|
||||
import time
|
||||
import tempfile
|
||||
import warnings
|
||||
from collections.abc import Mapping
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
@@ -18,9 +22,19 @@ import numpy as np
|
||||
import torch
|
||||
import torchvision
|
||||
from einops import rearrange
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from fastvideo.api.compat import (
|
||||
expand_request_prompt_batch,
|
||||
generator_config_to_fastvideo_args,
|
||||
legacy_from_pretrained_to_config,
|
||||
load_generator_config_from_file,
|
||||
normalize_generation_request,
|
||||
normalize_generator_config,
|
||||
request_to_pipeline_overrides,
|
||||
request_to_sampling_param,
|
||||
)
|
||||
from fastvideo.api.results import GenerationResult
|
||||
from fastvideo.api.schema import GenerationRequest, GeneratorConfig
|
||||
from fastvideo.configs.sample import SamplingParam
|
||||
from fastvideo.fastvideo_args import FastVideoArgs
|
||||
from fastvideo.logger import init_logger
|
||||
@@ -30,6 +44,29 @@ from fastvideo.worker.executor import Executor
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
_FROM_PRETRAINED_CONVENIENCE_KWARGS = frozenset({
|
||||
"num_gpus",
|
||||
"revision",
|
||||
"trust_remote_code",
|
||||
"distributed_executor_backend",
|
||||
"tp_size",
|
||||
"sp_size",
|
||||
"hsdp_replicate_dim",
|
||||
"hsdp_shard_dim",
|
||||
"dist_timeout",
|
||||
"use_fsdp_inference",
|
||||
"disable_autocast",
|
||||
"enable_stage_verification",
|
||||
"dit_cpu_offload",
|
||||
"dit_layerwise_offload",
|
||||
"text_encoder_cpu_offload",
|
||||
"image_encoder_cpu_offload",
|
||||
"vae_cpu_offload",
|
||||
"pin_cpu_memory",
|
||||
"enable_torch_compile",
|
||||
"torch_compile_kwargs",
|
||||
})
|
||||
|
||||
|
||||
def _infer_latent_batch_size(batch: ForwardBatch) -> int:
|
||||
if isinstance(batch.prompt, list):
|
||||
@@ -52,19 +89,33 @@ class VideoGenerator:
|
||||
customization options, similar to popular frameworks like HF Diffusers.
|
||||
"""
|
||||
|
||||
def __init__(self, fastvideo_args: FastVideoArgs, executor_class: type[Executor], log_stats: bool):
|
||||
def __init__(
|
||||
self,
|
||||
fastvideo_args: FastVideoArgs,
|
||||
executor_class: type[Executor],
|
||||
log_stats: bool,
|
||||
*,
|
||||
log_queue=None,
|
||||
):
|
||||
"""
|
||||
Initialize the video generator.
|
||||
|
||||
|
||||
Args:
|
||||
fastvideo_args: The inference arguments
|
||||
executor_class: The executor class to use for inference
|
||||
log_stats: Whether to log statistics
|
||||
log_queue: Optional multiprocessing.Queue to forward worker logs to
|
||||
"""
|
||||
self.config: GeneratorConfig | None = None
|
||||
self.fastvideo_args = fastvideo_args
|
||||
self.executor = executor_class(fastvideo_args)
|
||||
self.executor = executor_class(fastvideo_args, log_queue=log_queue)
|
||||
|
||||
@classmethod
|
||||
def from_pretrained(cls, model_path: str, **kwargs) -> "VideoGenerator":
|
||||
def from_pretrained(
|
||||
cls,
|
||||
model_path: str | GeneratorConfig | Mapping[str, Any] | None = None,
|
||||
**kwargs,
|
||||
) -> "VideoGenerator":
|
||||
"""
|
||||
Create a video generator from a pretrained model.
|
||||
|
||||
@@ -77,21 +128,84 @@ class VideoGenerator:
|
||||
The created video generator
|
||||
|
||||
Priority level: Default pipeline config < User's pipeline config < User's kwargs
|
||||
"""
|
||||
# If users also provide some kwargs, it will override the FastVideoArgs and PipelineConfig.
|
||||
kwargs['model_path'] = model_path
|
||||
fastvideo_args = FastVideoArgs.from_kwargs(**kwargs)
|
||||
|
||||
return cls.from_fastvideo_args(fastvideo_args)
|
||||
Stable convenience kwargs remain supported here for common engine and
|
||||
offload settings. Advanced model- or pipeline-specific options should
|
||||
move to VideoGenerator.from_config(...).
|
||||
"""
|
||||
log_queue = kwargs.pop("log_queue", None)
|
||||
typed_config = kwargs.pop("config", None)
|
||||
if typed_config is not None:
|
||||
if model_path is not None:
|
||||
raise TypeError("Pass either model_path or config to from_pretrained, not both")
|
||||
if kwargs:
|
||||
unexpected = ", ".join(sorted(kwargs))
|
||||
raise TypeError(f"Unexpected keyword arguments with config: {unexpected}")
|
||||
return cls.from_config(typed_config, log_queue=log_queue)
|
||||
|
||||
if isinstance(model_path, GeneratorConfig | Mapping):
|
||||
if kwargs:
|
||||
unexpected = ", ".join(sorted(kwargs))
|
||||
raise TypeError(f"Unexpected keyword arguments with typed config: {unexpected}")
|
||||
return cls.from_config(model_path, log_queue=log_queue)
|
||||
|
||||
if model_path is None:
|
||||
raise TypeError("model_path or config is required")
|
||||
|
||||
legacy_only_kwargs = sorted(set(kwargs) - _FROM_PRETRAINED_CONVENIENCE_KWARGS)
|
||||
if legacy_only_kwargs:
|
||||
warnings.warn(
|
||||
"VideoGenerator.from_pretrained(...) received legacy-only kwargs "
|
||||
f"({', '.join(legacy_only_kwargs)}); prefer VideoGenerator.from_config(...) "
|
||||
"for advanced configuration.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return cls.from_config(
|
||||
legacy_from_pretrained_to_config(model_path, kwargs),
|
||||
log_queue=log_queue,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_fastvideo_args(cls, fastvideo_args: FastVideoArgs) -> "VideoGenerator":
|
||||
def from_config(
|
||||
cls,
|
||||
config: GeneratorConfig | Mapping[str, Any],
|
||||
*,
|
||||
log_queue=None,
|
||||
) -> "VideoGenerator":
|
||||
normalized = normalize_generator_config(config)
|
||||
fastvideo_args = generator_config_to_fastvideo_args(normalized)
|
||||
generator = cls.from_fastvideo_args(fastvideo_args, log_queue=log_queue)
|
||||
generator.config = normalized
|
||||
return generator
|
||||
|
||||
@classmethod
|
||||
def from_file(
|
||||
cls,
|
||||
path: str,
|
||||
overrides: list[str] | Mapping[str, Any] | None = None,
|
||||
*,
|
||||
log_queue=None,
|
||||
) -> "VideoGenerator":
|
||||
return cls.from_config(
|
||||
load_generator_config_from_file(path, overrides=overrides),
|
||||
log_queue=log_queue,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_fastvideo_args(
|
||||
cls,
|
||||
fastvideo_args: FastVideoArgs,
|
||||
*,
|
||||
log_queue=None,
|
||||
) -> "VideoGenerator":
|
||||
"""
|
||||
Create a video generator with the specified arguments.
|
||||
|
||||
|
||||
Args:
|
||||
fastvideo_args: The inference arguments
|
||||
|
||||
log_queue: Optional multiprocessing.Queue to forward worker logs to
|
||||
|
||||
Returns:
|
||||
The created video generator
|
||||
"""
|
||||
@@ -103,8 +217,40 @@ class VideoGenerator:
|
||||
fastvideo_args=fastvideo_args,
|
||||
executor_class=executor_class,
|
||||
log_stats=False, # TODO: implement
|
||||
log_queue=log_queue,
|
||||
)
|
||||
|
||||
def generate(
|
||||
self,
|
||||
request: GenerationRequest | Mapping[str, Any],
|
||||
*,
|
||||
log_queue=None,
|
||||
) -> GenerationResult | list[GenerationResult]:
|
||||
"""
|
||||
Generate video or image outputs from a typed inference request.
|
||||
|
||||
Args:
|
||||
request: A `GenerationRequest` instance or a mapping that can be
|
||||
parsed into one. This is the primary public inference
|
||||
entrypoint for the typed API.
|
||||
log_queue: Optional multiprocessing.Queue to forward worker logs to
|
||||
during this request.
|
||||
|
||||
Returns:
|
||||
A `GenerationResult` for single-request generation, or a list of
|
||||
`GenerationResult` objects when the request expands into multiple
|
||||
prompts.
|
||||
"""
|
||||
normalized_request = normalize_generation_request(request)
|
||||
if log_queue:
|
||||
self.executor.set_log_queue(log_queue)
|
||||
|
||||
try:
|
||||
return self._generate_request_impl(normalized_request)
|
||||
finally:
|
||||
if log_queue:
|
||||
self.executor.clear_log_queue()
|
||||
|
||||
def generate_video(
|
||||
self,
|
||||
prompt: str | None = None,
|
||||
@@ -140,9 +286,93 @@ class VideoGenerator:
|
||||
A metadata dictionary for single-prompt generation, or a list of
|
||||
metadata dictionaries for prompt-file batch generation.
|
||||
"""
|
||||
log_queue = kwargs.pop("log_queue", None)
|
||||
warnings.warn(
|
||||
"VideoGenerator.generate_video(...) is deprecated; use "
|
||||
"VideoGenerator.generate(request=...) instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if log_queue:
|
||||
self.executor.set_log_queue(log_queue)
|
||||
|
||||
try:
|
||||
return self._generate_video_impl(
|
||||
prompt=prompt,
|
||||
sampling_param=sampling_param,
|
||||
mouse_cond=mouse_cond,
|
||||
keyboard_cond=keyboard_cond,
|
||||
grid_sizes=grid_sizes,
|
||||
**kwargs,
|
||||
)
|
||||
finally:
|
||||
if log_queue:
|
||||
self.executor.clear_log_queue()
|
||||
|
||||
def _generate_request_impl(
|
||||
self,
|
||||
request: GenerationRequest,
|
||||
) -> GenerationResult | list[GenerationResult]:
|
||||
if isinstance(request.prompt, list):
|
||||
if request.inputs.prompt_path is not None:
|
||||
raise ValueError("request.prompt list cannot be combined with request.inputs.prompt_path")
|
||||
results: list[GenerationResult] = []
|
||||
for index, single_request in enumerate(expand_request_prompt_batch(request)):
|
||||
prompt = single_request.prompt
|
||||
wrapped = self._generate_single_request(single_request)
|
||||
if isinstance(wrapped, list):
|
||||
results.extend(wrapped)
|
||||
continue
|
||||
wrapped.prompt_index = index
|
||||
if wrapped.prompt is None:
|
||||
wrapped.prompt = prompt
|
||||
results.append(wrapped)
|
||||
return results
|
||||
|
||||
return self._generate_single_request(request)
|
||||
|
||||
def _generate_single_request(
|
||||
self,
|
||||
request: GenerationRequest,
|
||||
) -> GenerationResult | list[GenerationResult]:
|
||||
fastvideo_args = self.fastvideo_args
|
||||
pipeline_overrides = request_to_pipeline_overrides(request)
|
||||
if pipeline_overrides:
|
||||
fastvideo_args = deepcopy(self.fastvideo_args)
|
||||
for key, value in pipeline_overrides.items():
|
||||
if not hasattr(fastvideo_args.pipeline_config, key):
|
||||
raise ValueError(f"Request field {key!r} is not supported by pipeline config overrides")
|
||||
setattr(fastvideo_args.pipeline_config, key, deepcopy(value))
|
||||
|
||||
sampling_param = request_to_sampling_param(
|
||||
request,
|
||||
model_path=self.fastvideo_args.model_path,
|
||||
)
|
||||
result = self._generate_video_impl(
|
||||
prompt=request.prompt,
|
||||
sampling_param=sampling_param,
|
||||
fastvideo_args=fastvideo_args,
|
||||
)
|
||||
return self._wrap_legacy_result(result)
|
||||
|
||||
def _generate_video_impl(
|
||||
self,
|
||||
prompt: str | None = None,
|
||||
sampling_param: SamplingParam | None = None,
|
||||
mouse_cond: torch.Tensor | None = None,
|
||||
keyboard_cond: torch.Tensor | None = None,
|
||||
grid_sizes: tuple[int, int, int] | list[int] | torch.Tensor
|
||||
| None = None,
|
||||
fastvideo_args: FastVideoArgs | None = None,
|
||||
**kwargs,
|
||||
) -> dict[str, Any] | list[np.ndarray] | list[dict[str, Any]]:
|
||||
"""Internal implementation of generate_video."""
|
||||
if fastvideo_args is None:
|
||||
fastvideo_args = self.fastvideo_args
|
||||
|
||||
# Handle batch processing from text file
|
||||
if sampling_param is None:
|
||||
sampling_param = SamplingParam.from_pretrained(self.fastvideo_args.model_path)
|
||||
sampling_param = SamplingParam.from_pretrained(fastvideo_args.model_path)
|
||||
|
||||
# Add action control inputs to kwargs if provided
|
||||
if mouse_cond is not None:
|
||||
@@ -154,9 +384,9 @@ class VideoGenerator:
|
||||
|
||||
sampling_param.update(kwargs)
|
||||
|
||||
if self.fastvideo_args.prompt_txt is not None or sampling_param.prompt_path is not None:
|
||||
prompt_txt_path = sampling_param.prompt_path or self.fastvideo_args.prompt_txt
|
||||
if not os.path.exists(prompt_txt_path):
|
||||
if fastvideo_args.prompt_txt is not None or sampling_param.prompt_path is not None:
|
||||
prompt_txt_path = sampling_param.prompt_path or fastvideo_args.prompt_txt
|
||||
if not prompt_txt_path or not os.path.exists(prompt_txt_path):
|
||||
raise FileNotFoundError(f"Prompt text file not found: {prompt_txt_path}")
|
||||
|
||||
# Read prompts from file
|
||||
@@ -175,7 +405,12 @@ class VideoGenerator:
|
||||
# Generate video for this prompt using the same logic below
|
||||
output_path = self._prepare_output_path(sampling_param.output_path, batch_prompt)
|
||||
kwargs["output_path"] = output_path
|
||||
result = self._generate_single_video(prompt=batch_prompt, sampling_param=sampling_param, **kwargs)
|
||||
result = self._generate_single_video(
|
||||
prompt=batch_prompt,
|
||||
sampling_param=sampling_param,
|
||||
fastvideo_args=fastvideo_args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# Add prompt info to result
|
||||
result["prompt_index"] = i
|
||||
@@ -196,7 +431,12 @@ class VideoGenerator:
|
||||
raise ValueError("Either prompt or prompt_txt must be provided")
|
||||
output_path = self._prepare_output_path(sampling_param.output_path, prompt)
|
||||
kwargs["output_path"] = output_path
|
||||
return self._generate_single_video(prompt=prompt, sampling_param=sampling_param, **kwargs)
|
||||
return self._generate_single_video(
|
||||
prompt=prompt,
|
||||
sampling_param=sampling_param,
|
||||
fastvideo_args=fastvideo_args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def _is_image_workload(self) -> bool:
|
||||
"""Return True when the workload produces a single image (t2i, i2i …)."""
|
||||
@@ -280,11 +520,12 @@ class VideoGenerator:
|
||||
self,
|
||||
prompt: str,
|
||||
sampling_param: SamplingParam | None = None,
|
||||
fastvideo_args: FastVideoArgs | None = None,
|
||||
**kwargs,
|
||||
) -> dict[str, Any]:
|
||||
"""Internal method for single video generation"""
|
||||
# Create a copy of inference args to avoid modifying the original
|
||||
fastvideo_args = self.fastvideo_args
|
||||
if fastvideo_args is None:
|
||||
fastvideo_args = self.fastvideo_args
|
||||
|
||||
# Validate inputs
|
||||
if not isinstance(prompt, str):
|
||||
@@ -427,6 +668,20 @@ class VideoGenerator:
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _wrap_legacy_result(
|
||||
result: dict[str, Any] | list[dict[str, Any]], ) -> GenerationResult | list[GenerationResult]:
|
||||
if isinstance(result, list):
|
||||
return [GenerationResult.from_legacy_result(item) for item in result]
|
||||
return GenerationResult.from_legacy_result(result)
|
||||
|
||||
@staticmethod
|
||||
def _unwrap_typed_result(
|
||||
result: GenerationResult | list[GenerationResult], ) -> dict[str, Any] | list[dict[str, Any]]:
|
||||
if isinstance(result, list):
|
||||
return [item.to_legacy_dict() for item in result]
|
||||
return result.to_legacy_dict()
|
||||
|
||||
@staticmethod
|
||||
def _mux_audio(
|
||||
video_path: str,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,761 @@
|
||||
# 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)
|
||||
@@ -588,6 +588,33 @@ class TokenizerLoader(ComponentLoader):
|
||||
class VAELoader(ComponentLoader):
|
||||
"""Loader for VAE."""
|
||||
|
||||
@staticmethod
|
||||
def _find_gen3c_tokenizer_checkpoint(model_path: str) -> str | None:
|
||||
"""Locate tokenizer-backed VAE checkpoint used by GEN3C integration."""
|
||||
candidates = [
|
||||
os.path.join(model_path, "tokenizer.pth"),
|
||||
os.path.join(os.path.dirname(model_path), "tokenizer",
|
||||
"tokenizer.pth"),
|
||||
]
|
||||
for candidate in candidates:
|
||||
if os.path.exists(candidate):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _find_gen3c_jit_tokenizer_dir(model_path: str) -> str | None:
|
||||
"""Locate official tokenizer JIT assets (encoder/decoder/mean_std)."""
|
||||
candidates = [
|
||||
model_path,
|
||||
os.path.join(os.path.dirname(model_path), "tokenizer"),
|
||||
]
|
||||
required = ("encoder.jit", "decoder.jit", "mean_std.pt")
|
||||
for directory in candidates:
|
||||
if all(os.path.exists(os.path.join(directory, name))
|
||||
for name in required):
|
||||
return directory
|
||||
return None
|
||||
|
||||
def load(self, model_path: str, fastvideo_args: FastVideoArgs):
|
||||
"""Load the VAE based on the model path, and inference args."""
|
||||
config = get_diffusers_config(model=model_path)
|
||||
@@ -614,8 +641,68 @@ class VAELoader(ComponentLoader):
|
||||
if fastvideo_args.pipeline_config.vae_precision
|
||||
else torch.bfloat16
|
||||
):
|
||||
pipeline_name = fastvideo_args.pipeline_config.__class__.__name__
|
||||
is_gen3c = pipeline_name.startswith("Gen3C")
|
||||
is_cosmos25 = pipeline_name == "Cosmos25Config"
|
||||
|
||||
# GEN3C: prefer tokenizer-backed VAE checkpoint when available.
|
||||
# This aligns latent conditioning with the GEN3C temporal contract.
|
||||
if is_gen3c and class_name in (
|
||||
"AutoencoderKLWan", "AutoencoderKLGen3CTokenizer"):
|
||||
from fastvideo.models.vaes.gen3c_tokenizer_vae import (
|
||||
AutoencoderKLGen3CTokenizer)
|
||||
|
||||
dtype = PRECISION_TO_TYPE[
|
||||
fastvideo_args.pipeline_config.vae_precision]
|
||||
num_frames = int(
|
||||
getattr(fastvideo_args.pipeline_config, "num_frames", 121))
|
||||
state_t = int(
|
||||
getattr(fastvideo_args.pipeline_config, "state_t", 16))
|
||||
if state_t > 1 and num_frames > 1:
|
||||
target_temporal = max(1,
|
||||
(num_frames - 1) // (state_t - 1))
|
||||
else:
|
||||
target_temporal = 8
|
||||
|
||||
jit_dir = self._find_gen3c_jit_tokenizer_dir(model_path)
|
||||
if jit_dir is not None:
|
||||
vae = AutoencoderKLGen3CTokenizer.from_jit_tokenizer(
|
||||
jit_dir,
|
||||
device=target_device,
|
||||
dtype=dtype,
|
||||
target_temporal_compression=target_temporal,
|
||||
pixel_chunk_duration=num_frames,
|
||||
)
|
||||
logger.info(
|
||||
"Loaded GEN3C tokenizer VAE from JIT assets in %s (target temporal compression=%d)",
|
||||
jit_dir,
|
||||
target_temporal,
|
||||
)
|
||||
return vae.eval()
|
||||
|
||||
tokenizer_ckpt = self._find_gen3c_tokenizer_checkpoint(
|
||||
model_path)
|
||||
if tokenizer_ckpt is not None:
|
||||
vae = AutoencoderKLGen3CTokenizer.from_tokenizer_checkpoint(
|
||||
tokenizer_ckpt,
|
||||
device=target_device,
|
||||
dtype=dtype,
|
||||
target_temporal_compression=target_temporal,
|
||||
pixel_chunk_duration=num_frames,
|
||||
)
|
||||
logger.info(
|
||||
"Loaded GEN3C tokenizer VAE from %s (target temporal compression=%d)",
|
||||
tokenizer_ckpt,
|
||||
target_temporal,
|
||||
)
|
||||
return vae.eval()
|
||||
logger.warning(
|
||||
"GEN3C tokenizer VAE checkpoint not found near %s; falling back to configured class %s.",
|
||||
model_path,
|
||||
class_name,
|
||||
)
|
||||
|
||||
# Cosmos2.5 uses a Wan2.1 VAE stored as `tokenizer.safetensors` under the VAE folder.
|
||||
is_cosmos25 = fastvideo_args.pipeline_config.__class__.__name__ == "Cosmos25Config"
|
||||
if class_name == "AutoencoderKLWan" and is_cosmos25:
|
||||
from fastvideo.models.vaes.cosmos25wanvae import Cosmos25WanVAE
|
||||
|
||||
|
||||
@@ -147,6 +147,9 @@ 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(
|
||||
|
||||
@@ -40,6 +40,8 @@ _TEXT_TO_VIDEO_DIT_MODELS = {
|
||||
"LTX2Transformer3DModel": ("dits", "ltx2", "LTX2Transformer3DModel"),
|
||||
"SD3Transformer2DModel": ("dits", "sd3", "SD3Transformer2DModel"),
|
||||
"LingBotWorldTransformer3DModel": ("dits", "lingbotworld", "LingBotWorldTransformer3DModel"),
|
||||
"Gen3CTransformer3DModel": ("dits", "gen3c", "Gen3CTransformer3DModel"),
|
||||
"Kandinsky5Transformer3DModel": ("dits", "kandinsky5", "Kandinsky5Transformer3DModel"),
|
||||
}
|
||||
|
||||
_IMAGE_TO_VIDEO_DIT_MODELS = {
|
||||
@@ -80,6 +82,9 @@ _VAE_MODELS = {
|
||||
"AutoencoderKLHunyuanVideo15": ("vaes", "hunyuan15vae", "AutoencoderKLHunyuanVideo15"),
|
||||
"AutoencoderKLWan": ("vaes", "wanvae", "AutoencoderKLWan"),
|
||||
"AutoencoderKL": ("vaes", "autoencoder_kl", "AutoencoderKL"),
|
||||
"AutoencoderKLGen3CTokenizer":
|
||||
("vaes", "gen3c_tokenizer_vae", "AutoencoderKLGen3CTokenizer"),
|
||||
"AutoencoderKLStepvideo": ("vaes", "stepvideovae", "AutoencoderKLStepvideo"),
|
||||
"CausalVideoAutoencoder": ("vaes", "ltx2vae", "LTX2CausalVideoAutoencoder"),
|
||||
}
|
||||
|
||||
|
||||
@@ -364,10 +364,15 @@ 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, self.blend_num_frames)
|
||||
tile = self.blend_t(row[i - 1], tile,
|
||||
blend_latent_num_frames)
|
||||
result_row.append(
|
||||
tile[:, :, :tile_latent_stride_num_frames, :, :])
|
||||
else:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user