Compare commits

..
Author SHA1 Message Date
SolitaryThinker 48f9690c47 load_video 2025-06-24 10:46:20 -07:00
SolitaryThinker 32df3bcca3 update i2v script 2025-06-24 03:20:09 -07:00
SolitaryThinker b8c81191b6 improve script format 2025-06-24 03:01:55 -07:00
SolitaryThinker e506c4074f remove print and enable first val 2025-06-24 01:55:05 -07:00
SolitaryThinker 1b3914da84 update scripts 2025-06-22 21:09:30 -07:00
SolitaryThinker f471fd3f02 i2v working 2025-06-22 20:59:37 -07:00
SolitaryThinker 2e6d5c5304 t2v working again 2025-06-22 19:45:44 -07:00
SolitaryThinker 5db34184c7 t2v example 2025-06-22 21:53:05 +00:00
SolitaryThinker 37252bf62c f 2025-06-22 11:58:18 +00:00
SolitaryThinker e9263f7d2b update 2025-06-22 04:20:34 -07:00
SolitaryThinker 93afb86c20 update 2025-06-22 04:19:10 -07:00
SolitaryThinker 0d944ba9c1 slrm 2025-06-22 04:09:09 -07:00
SolitaryThinker 3d78604281 update path 2025-06-22 03:54:39 -07:00
SolitaryThinker 0694b0c5eb exmaple scripts 2025-06-22 03:44:57 -07:00
SolitaryThinker b6c5644d40 cleanup 2025-06-22 03:07:37 -07:00
SolitaryThinker a9089fa358 fix pil image 2025-06-22 02:29:51 -07:00
SolitaryThinker 6079a98fd7 i2v preprocess 2025-06-21 19:18:39 -07:00
SolitaryThinker 65c0fcb633 checkpoint 2025-06-21 18:02:43 -07:00
SolitaryThinker 82e3641264 checkpoint 2025-06-21 18:00:29 -07:00
253 changed files with 2639 additions and 9510 deletions
+21 -93
View File
@@ -1,138 +1,66 @@
env: env:
IMAGE_VERSION: "py3.12-latest" IMAGE_VERSION: "py3.12-latest"
BUILDKITE_CLEAN_CHECKOUT: true
steps: steps:
- label: "pre-commit" - block: "Start Build"
command: ".buildkite/scripts/pre_commit.sh" blocked_state: "running"
agents: prompt: "Approve build?"
queue: "default"
- wait
- label: "Trigger Tests" - label: "Trigger Tests"
command: |
echo "Current working directory: $(pwd)"
echo "Current branch:"
git branch --show-current
echo "Full diff:"
git diff --name-only $BUILDKITE_PULL_REQUEST_BASE_BRANCH...HEAD
plugins: plugins:
- monorepo-diff#v1.4.0: - monorepo-diff#v1.4.0:
diff: "git diff --name-only $BUILDKITE_PULL_REQUEST_BASE_BRANCH...HEAD" diff: "git diff --name-only $BUILDKITE_PULL_REQUEST_BASE_BRANCH...HEAD"
watch: watch:
- path: - path:
- "fastvideo/v1/models/encoders/**" - "fastvideo/v1/models/encoders/**"
- "fastvideo/v1/models/loader/**" - "fastvideo/v1/models/loaders/**"
- "fastvideo/v1/tests/encoders/**" - "fastvideo/v1/tests/encoders/**"
- "pyproject.toml"
- "docker/Dockerfile.python3.12"
config: config:
command: "timeout 15m .buildkite/scripts/pr_test.sh" command: "timeout 30m .buildkite/scripts/pr_test.sh"
label: "Encoder Tests" label: "Encoder Tests"
env: env:
- BUILDKITE_CLEAN_CHECKOUT=true
- TEST_TYPE=encoder - TEST_TYPE=encoder
agents: agents:
queue: "default" queue: "default"
- path: - path:
- "fastvideo/v1/models/vaes/**" - "fastvideo/v1/models/vaes/**"
- "fastvideo/v1/models/loader/**" - "fastvideo/v1/models/loaders/**"
- "fastvideo/v1/tests/vaes/**" - "fastvideo/v1/tests/vaes/**"
- "pyproject.toml"
- "docker/Dockerfile.python3.12"
config: config:
command: "timeout 15m .buildkite/scripts/pr_test.sh" command: "timeout 30m .buildkite/scripts/pr_test.sh"
label: "VAE Tests" label: "VAE Tests"
env: env:
- BUILDKITE_CLEAN_CHECKOUT=true
- TEST_TYPE=vae - TEST_TYPE=vae
agents: agents:
queue: "default" queue: "default"
- path: - path:
- "fastvideo/v1/models/dits/**" - "fastvideo/v1/models/dits/**"
- "fastvideo/v1/models/loader/**" - "fastvideo/v1/models/loaders/**"
- "fastvideo/v1/tests/transformers/**" - "fastvideo/v1/tests/transformers/**"
- "fastvideo/v1/layers/**" - "fastvideo/v1/layers/**"
- "fastvideo/v1/attention/**" - "fastvideo/v1/attention/**"
- "pyproject.toml"
- "docker/Dockerfile.python3.12"
config: config:
command: "timeout 15m .buildkite/scripts/pr_test.sh" command: "timeout 30m .buildkite/scripts/pr_test.sh"
label: "Transformer Tests" label: "Transformer Tests"
env: env:
- BUILDKITE_CLEAN_CHECKOUT=true
- TEST_TYPE=transformer - TEST_TYPE=transformer
agents: agents:
queue: "default" queue: "default"
- path: - path: "fastvideo/v1/**/*.py"
- "fastvideo/v1/**/*.py"
config: config:
command: "timeout 30m .buildkite/scripts/pr_test.sh" command: "timeout 60m .buildkite/scripts/pr_test.sh"
label: "SSIM Tests" label: "SSIM Tests"
env: env:
- BUILDKITE_CLEAN_CHECKOUT=true
- TEST_TYPE=ssim - TEST_TYPE=ssim
agents: agents:
queue: "default" queue: "default"
- path:
- "fastvideo/v1/**"
- "pyproject.toml"
- "docker/Dockerfile.python3.12"
config:
command: "timeout 15m .buildkite/scripts/pr_test.sh"
label: "Training Tests"
env:
- TEST_TYPE=training
agents:
queue: "default"
- path:
- "fastvideo/v1/**"
- "csrc/attn/vsa/**"
- "csrc/attn/tk/**"
- "csrc/attn/setup_vsa.py"
- "csrc/attn/config_vsa.py"
- "csrc/attn/vsa.cpp"
- "pyproject.toml"
- "docker/Dockerfile.python3.12"
config:
command: "timeout 15m .buildkite/scripts/pr_test.sh"
label: "Training Tests VSA"
env:
- TEST_TYPE=training_vsa
agents:
queue: "default"
- path:
- "fastvideo/v1/**"
- "csrc/attn/st_attn/**"
- "csrc/attn/setup_sta.py"
- "csrc/attn/config_sta.py"
- "csrc/attn/st_attn.cpp"
- "pyproject.toml"
- "docker/Dockerfile.python3.12"
config:
command: "timeout 15m .buildkite/scripts/pr_test.sh"
label: "Inference Tests STA"
env:
- TEST_TYPE=inference_sta
agents:
queue: "default"
- path:
- "csrc/attn/st_attn/**"
- "csrc/attn/setup_sta.py"
- "csrc/attn/config_sta.py"
- "csrc/attn/st_attn.cpp"
- "pyproject.toml"
- "docker/Dockerfile.python3.12"
config:
command: "timeout 15m .buildkite/scripts/pr_test.sh"
label: "Precision Tests STA"
env:
- TEST_TYPE=precision_sta
agents:
queue: "default"
- path:
- "csrc/attn/vsa/**"
- "csrc/attn/tk/**"
- "csrc/attn/setup_vsa.py"
- "csrc/attn/config_vsa.py"
- "csrc/attn/vsa.cpp"
- "pyproject.toml"
- "docker/Dockerfile.python3.12"
config:
command: "timeout 15m .buildkite/scripts/pr_test.sh"
label: "Precision Tests VSA"
env:
- TEST_TYPE=precision_vsa
agents:
queue: "default"
+4 -30
View File
@@ -31,10 +31,6 @@ log "Setting up Modal authentication from Buildkite secrets..."
MODAL_TOKEN_ID=$(buildkite-agent secret get modal_token_id) MODAL_TOKEN_ID=$(buildkite-agent secret get modal_token_id)
MODAL_TOKEN_SECRET=$(buildkite-agent secret get modal_token_secret) MODAL_TOKEN_SECRET=$(buildkite-agent secret get modal_token_secret)
WANDB_API_KEY=$(buildkite-agent secret get wandb_api_key)
WANDB_API_KEY=$(buildkite-agent secret get wandb_api_key)
if [ -n "$MODAL_TOKEN_ID" ] && [ -n "$MODAL_TOKEN_SECRET" ]; then if [ -n "$MODAL_TOKEN_ID" ] && [ -n "$MODAL_TOKEN_SECRET" ]; then
log "Retrieved Modal credentials from Buildkite secrets" log "Retrieved Modal credentials from Buildkite secrets"
python3 -m modal token set --token-id "$MODAL_TOKEN_ID" --token-secret "$MODAL_TOKEN_SECRET" --profile buildkite-ci --activate --verify python3 -m modal token set --token-id "$MODAL_TOKEN_ID" --token-secret "$MODAL_TOKEN_SECRET" --profile buildkite-ci --activate --verify
@@ -58,44 +54,22 @@ if [ -z "${TEST_TYPE:-}" ]; then
fi fi
log "Test type: $TEST_TYPE" 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"
case "$TEST_TYPE" in case "$TEST_TYPE" in
"encoder") "encoder")
log "Running encoder tests..." log "Running encoder tests..."
MODAL_COMMAND="$MODAL_ENV python3 -m modal run $MODAL_TEST_FILE::run_encoder_tests" MODAL_COMMAND="python3 -m modal run $MODAL_TEST_FILE::run_encoder_tests"
;; ;;
"vae") "vae")
log "Running VAE tests..." log "Running VAE tests..."
MODAL_COMMAND="$MODAL_ENV python3 -m modal run $MODAL_TEST_FILE::run_vae_tests" MODAL_COMMAND="python3 -m modal run $MODAL_TEST_FILE::run_vae_tests"
;; ;;
"transformer") "transformer")
log "Running transformer tests..." log "Running transformer tests..."
MODAL_COMMAND="$MODAL_ENV python3 -m modal run $MODAL_TEST_FILE::run_transformer_tests" MODAL_COMMAND="python3 -m modal run $MODAL_TEST_FILE::run_transformer_tests"
;; ;;
"ssim") "ssim")
log "Running SSIM tests..." log "Running SSIM tests..."
MODAL_COMMAND="$MODAL_ENV python3 -m modal run $MODAL_TEST_FILE::run_ssim_tests" MODAL_COMMAND="python3 -m modal run $MODAL_TEST_FILE::run_ssim_tests"
;;
"training")
log "Running training tests..."
MODAL_COMMAND="$MODAL_ENV WANDB_API_KEY=$WANDB_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_training_tests"
;;
"training_vsa")
log "Running training VSA tests..."
MODAL_COMMAND="$MODAL_ENV WANDB_API_KEY=$WANDB_API_KEY python3 -m modal run $MODAL_TEST_FILE::run_training_tests_VSA"
;;
"inference_sta")
log "Running inference STA tests..."
MODAL_COMMAND="$MODAL_ENV python3 -m modal run $MODAL_TEST_FILE::run_inference_tests_STA"
;;
"precision_sta")
log "Running precision STA tests..."
MODAL_COMMAND="$MODAL_ENV python3 -m modal run $MODAL_TEST_FILE::run_precision_tests_STA"
;;
"precision_vsa")
log "Running precision VSA tests..."
MODAL_COMMAND="$MODAL_ENV python3 -m modal run $MODAL_TEST_FILE::run_precision_tests_VSA"
;; ;;
*) *)
log "Error: Unknown test type: $TEST_TYPE" log "Error: Unknown test type: $TEST_TYPE"
-40
View File
@@ -1,40 +0,0 @@
#!/bin/bash
set -uo pipefail
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
log "=== Starting pre-commit checks ==="
cd "$(dirname "$0")/../.."
PROJECT_ROOT=$(pwd)
log "Project root: $PROJECT_ROOT"
if ! python3 -m pre_commit --version &> /dev/null; then
log "pre-commit not found, installing..."
python3 -m pip install --user pre-commit==4.0.1
if ! python3 -m pre_commit --version &> /dev/null; then
log "Error: Failed to install pre-commit."
exit 1
fi
fi
log "Pre-commit version: $(python3 -m pre_commit --version)"
log "Installing/updating pre-commit hooks..."
python3 -m pre_commit install --install-hooks
log "Running pre-commit checks on all files..."
python3 -m pre_commit run --all-files
PRE_COMMIT_EXIT_CODE=$?
if [ $PRE_COMMIT_EXIT_CODE -eq 0 ]; then
log "Pre-commit checks completed successfully"
else
log "Error: Pre-commit checks failed with exit code: $PRE_COMMIT_EXIT_CODE"
fi
log "=== Pre-commit checks completed with exit code: $PRE_COMMIT_EXIT_CODE ==="
exit $PRE_COMMIT_EXIT_CODE
+36 -106
View File
@@ -14,9 +14,13 @@ on:
- ".github/workflows/pr-test.yml" - ".github/workflows/pr-test.yml"
- "pyproject.toml" - "pyproject.toml"
- "docker/Dockerfile.python3.12" - "docker/Dockerfile.python3.12"
- "csrc/**"
workflow_dispatch: workflow_dispatch:
inputs: inputs:
custom_image:
description: "Custom image from this repository (default: fastvideo-dev:py3.12-latest)"
required: false
default: "fastvideo-dev:py3.12-latest"
type: string
run_encoder_test: run_encoder_test:
description: "Run encoder-test" description: "Run encoder-test"
required: false required: false
@@ -52,16 +56,6 @@ on:
required: false required: false
default: false default: false
type: boolean type: boolean
run_precision_test_STA:
description: "Run precision-test-STA"
required: false
default: false
type: boolean
run_precision_test_VSA:
description: "Run precision-test-VSA"
required: false
default: false
type: boolean
run_nightly_test: run_nightly_test:
description: "Run nightly-test" description: "Run nightly-test"
required: false required: false
@@ -71,7 +65,6 @@ on:
env: env:
PYTHONUNBUFFERED: "1" PYTHONUNBUFFERED: "1"
concurrency: concurrency:
group: pr-test-${{ github.ref }} group: pr-test-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
@@ -91,69 +84,44 @@ jobs:
training-test: ${{ steps.filter.outputs.training-test }} training-test: ${{ steps.filter.outputs.training-test }}
training-test-VSA: ${{ steps.filter.outputs.training-test-VSA }} training-test-VSA: ${{ steps.filter.outputs.training-test-VSA }}
inference-test-STA: ${{ steps.filter.outputs.inference-test-STA }} inference-test-STA: ${{ steps.filter.outputs.inference-test-STA }}
precision-test-STA: ${{ steps.filter.outputs.precision-test-STA }}
precision-test-VSA: ${{ steps.filter.outputs.precision-test-VSA }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dorny/paths-filter@v3 - uses: dorny/paths-filter@v3
id: filter id: filter
with: with:
filters: | filters: |
# Define reusable path patterns
common-paths: &common-paths
- 'pyproject.toml'
- 'docker/Dockerfile.python3.12'
sta-kernel-paths: &sta-kernel-paths
- 'csrc/attn/st_attn/**'
- 'csrc/attn/setup_sta.py'
- 'csrc/attn/config_sta.py'
- 'csrc/attn/st_attn.cpp'
vsa-kernel-paths: &vsa-kernel-paths
- 'csrc/attn/vsa/**'
- 'csrc/attn/tk/**'
- 'csrc/attn/setup_vsa.py'
- 'csrc/attn/config_vsa.py'
- 'csrc/attn/vsa.cpp'
vsa-paths: &vsa-paths
- 'fastvideo/v1/**'
- *common-paths
- *vsa-kernel-paths
# Actual tests
encoder-test: encoder-test:
- 'fastvideo/v1/models/encoders/**' - 'fastvideo/v1/models/encoders/**'
- 'fastvideo/v1/models/loader/**' - 'fastvideo/v1/models/loaders/**'
- 'fastvideo/v1/tests/encoders/**' - 'fastvideo/v1/tests/encoders/**'
- *common-paths - 'pyproject.toml'
- 'docker/Dockerfile.python3.12'
vae-test: vae-test:
- 'fastvideo/v1/models/vaes/**' - 'fastvideo/v1/models/vaes/**'
- 'fastvideo/v1/models/loader/**' - 'fastvideo/v1/models/loaders/**'
- 'fastvideo/v1/tests/vaes/**' - 'fastvideo/v1/tests/vaes/**'
- *common-paths - 'pyproject.toml'
- 'docker/Dockerfile.python3.12'
transformer-test: transformer-test:
- 'fastvideo/v1/models/dits/**' - 'fastvideo/v1/models/dits/**'
- 'fastvideo/v1/models/loader/**' - 'fastvideo/v1/models/loaders/**'
- 'fastvideo/v1/tests/transformers/**' - 'fastvideo/v1/tests/transformers/**'
- 'fastvideo/v1/layers/**' - 'fastvideo/v1/layers/**'
- 'fastvideo/v1/attention/**' - 'fastvideo/v1/attention/**'
- *common-paths - 'pyproject.toml'
- 'docker/Dockerfile.python3.12'
training-test: training-test:
- 'fastvideo/v1/**' - 'fastvideo/v1/**'
- *common-paths - 'pyproject.toml'
- 'docker/Dockerfile.python3.12'
training-test-VSA: training-test-VSA:
- 'fastvideo/v1/**' - 'fastvideo/v1/**'
- *common-paths - 'pyproject.toml'
- *vsa-kernel-paths - 'docker/Dockerfile.python3.12'
inference-test-STA: inference-test-STA:
- 'fastvideo/v1/**' - 'fastvideo/v1/**'
- *common-paths - 'pyproject.toml'
- *sta-kernel-paths - 'docker/Dockerfile.python3.12'
precision-test-STA:
- *common-paths
- *sta-kernel-paths
precision-test-VSA:
- *common-paths
- *vsa-kernel-paths
encoder-test: encoder-test:
needs: change-filter needs: change-filter
@@ -166,7 +134,7 @@ jobs:
gpu_type: "NVIDIA A40" gpu_type: "NVIDIA A40"
gpu_count: 1 gpu_count: 1
volume_size: 100 volume_size: 100
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest" image: "ghcr.io/${{ github.repository }}/${{ github.event.inputs.custom_image || 'fastvideo-dev:py3.12-latest' }}"
test_command: "uv pip install -e .[test] && pytest ./fastvideo/v1/tests/encoders -s" test_command: "uv pip install -e .[test] && pytest ./fastvideo/v1/tests/encoders -s"
timeout_minutes: 30 timeout_minutes: 30
secrets: secrets:
@@ -184,7 +152,7 @@ jobs:
gpu_type: "NVIDIA A40" gpu_type: "NVIDIA A40"
gpu_count: 1 gpu_count: 1
volume_size: 100 volume_size: 100
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest" image: "ghcr.io/${{ github.repository }}/${{ github.event.inputs.custom_image || 'fastvideo-dev:py3.12-latest' }}"
test_command: "uv pip install -e .[test] && pytest ./fastvideo/v1/tests/vaes -s" test_command: "uv pip install -e .[test] && pytest ./fastvideo/v1/tests/vaes -s"
timeout_minutes: 30 timeout_minutes: 30
secrets: secrets:
@@ -202,7 +170,7 @@ jobs:
gpu_type: "NVIDIA L40S" gpu_type: "NVIDIA L40S"
gpu_count: 1 gpu_count: 1
volume_size: 100 volume_size: 100
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest" image: "ghcr.io/${{ github.repository }}/${{ github.event.inputs.custom_image || 'fastvideo-dev:py3.12-latest' }}"
test_command: "uv pip install -e .[test] && pytest ./fastvideo/v1/tests/transformers -s" test_command: "uv pip install -e .[test] && pytest ./fastvideo/v1/tests/transformers -s"
timeout_minutes: 30 timeout_minutes: 30
secrets: secrets:
@@ -212,7 +180,8 @@ jobs:
ssim-test: ssim-test:
needs: change-filter needs: change-filter
if: >- if: >-
github.event_name != 'workflow_dispatch' || (github.event_name == 'workflow_dispatch' && github.event.inputs.run_ssim_test == 'true') (github.event_name != 'workflow_dispatch' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_ssim_test == 'true')
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -238,7 +207,7 @@ jobs:
training-test: training-test:
needs: change-filter needs: change-filter
if: >- if: >-
(github.event_name != 'workflow_dispatch' && needs.change-filter.outputs.training-test == 'true') || (github.event_name != 'workflow_dispatch' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_training_test == 'true') (github.event_name == 'workflow_dispatch' && github.event.inputs.run_training_test == 'true')
uses: ./.github/workflows/runpod-test.yml uses: ./.github/workflows/runpod-test.yml
with: with:
@@ -247,7 +216,7 @@ jobs:
gpu_count: 4 gpu_count: 4
volume_size: 100 volume_size: 100
disk_size: 100 disk_size: 100
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest" image: "ghcr.io/${{ github.repository }}/${{ github.event.inputs.custom_image || 'fastvideo-dev:py3.12-latest' }}"
test_command: "wandb login $WANDB_API_KEY && uv pip install -e .[test] && pytest ./fastvideo/v1/tests/training/Vanilla -srP" test_command: "wandb login $WANDB_API_KEY && uv pip install -e .[test] && pytest ./fastvideo/v1/tests/training/Vanilla -srP"
timeout_minutes: 30 timeout_minutes: 30
secrets: secrets:
@@ -258,16 +227,16 @@ jobs:
training-test-VSA: training-test-VSA:
needs: change-filter needs: change-filter
if: >- if: >-
(github.event_name != 'workflow_dispatch' && needs.change-filter.outputs.training-test-VSA == 'true') || (github.event_name != 'workflow_dispatch' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_training_test_VSA == 'true') (github.event_name == 'workflow_dispatch' && github.event.inputs.run_training_test_VSA == 'true')
uses: ./.github/workflows/runpod-test.yml uses: ./.github/workflows/runpod-test.yml
with: with:
job_id: "training-test-VSA" job_id: "training-test-VSA"
gpu_type: "NVIDIA H100 NVL" gpu_type: "NVIDIA H100 NVL"
gpu_count: 2 gpu_count: 1
volume_size: 100 volume_size: 100
disk_size: 100 disk_size: 100
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest" image: "ghcr.io/${{ github.repository }}/${{ github.event.inputs.custom_image || 'fastvideo-dev:py3.12-latest' }}"
test_command: "wandb login $WANDB_API_KEY && uv pip install -e .[test] && pytest ./fastvideo/v1/tests/training/VSA -srP" test_command: "wandb login $WANDB_API_KEY && uv pip install -e .[test] && pytest ./fastvideo/v1/tests/training/VSA -srP"
timeout_minutes: 30 timeout_minutes: 30
secrets: secrets:
@@ -278,60 +247,22 @@ jobs:
inference-test-STA: inference-test-STA:
needs: change-filter needs: change-filter
if: >- if: >-
(github.event_name != 'workflow_dispatch' && needs.change-filter.outputs.inference-test-STA == 'true') || (github.event_name != 'workflow_dispatch' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_inference_test_STA == 'true') (github.event_name == 'workflow_dispatch' && github.event.inputs.run_inference_test_STA == 'true')
uses: ./.github/workflows/runpod-test.yml uses: ./.github/workflows/runpod-test.yml
with: with:
job_id: "inference-test-STA" job_id: "inference-test-STA"
gpu_type: "NVIDIA H100 NVL" gpu_type: "NVIDIA H100 NVL"
gpu_count: 2 gpu_count: 1
volume_size: 100 volume_size: 100
disk_size: 100 disk_size: 100
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest" image: "ghcr.io/${{ github.repository }}/${{ github.event.inputs.custom_image || 'fastvideo-dev:py3.12-latest' }}"
test_command: "uv pip install -e .[test] && pytest ./fastvideo/v1/tests/inference/STA -srP" test_command: "uv pip install -e .[test] && pytest ./fastvideo/v1/tests/inference/STA -srP"
timeout_minutes: 30 timeout_minutes: 30
secrets: secrets:
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }} RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }} RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
precision-test-STA:
needs: change-filter
if: >-
(github.event_name != 'workflow_dispatch' && needs.change-filter.outputs.precision-test-STA == 'true') ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_precision_test_STA == 'true')
uses: ./.github/workflows/runpod-test.yml
with:
job_id: "precision-test-STA"
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_sta.py"
timeout_minutes: 30
secrets:
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_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_block_sparse.py"
timeout_minutes: 30
secrets:
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
RUNPOD_PRIVATE_KEY: ${{ secrets.RUNPOD_PRIVATE_KEY }}
nightly-test: nightly-test:
if: >- if: >-
(github.event_name == 'workflow_dispatch' && github.event.inputs.run_nightly_test == 'true') (github.event_name == 'workflow_dispatch' && github.event.inputs.run_nightly_test == 'true')
@@ -342,7 +273,7 @@ jobs:
gpu_count: 4 gpu_count: 4
volume_size: 100 volume_size: 100
disk_size: 100 disk_size: 100
image: "ghcr.io/${{ github.repository }}/fastvideo-dev:py3.12-latest" image: "ghcr.io/${{ github.repository }}/${{ github.event.inputs.custom_image || 'fastvideo-dev:py3.12-latest' }}"
test_command: "wandb login $WANDB_API_KEY && uv pip install -e .[test] && pytest ./fastvideo/v1/tests/nightly/test_e2e_overfit_single_sample.py -vs" test_command: "wandb login $WANDB_API_KEY && uv pip install -e .[test] && pytest ./fastvideo/v1/tests/nightly/test_e2e_overfit_single_sample.py -vs"
timeout_minutes: 30 timeout_minutes: 30
secrets: secrets:
@@ -351,8 +282,7 @@ jobs:
WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }} WANDB_API_KEY: ${{ secrets.WANDB_API_KEY }}
runpod-cleanup: runpod-cleanup:
# Add other jobs to this list as you create them needs: [encoder-test, vae-test, transformer-test, ssim-test] # Add other jobs to this list as you create them
needs: [encoder-test, vae-test, transformer-test, ssim-test, training-test, training-test-VSA, inference-test-STA, precision-test-STA, precision-test-VSA]
if: ${{ always() && ((github.event_name != 'workflow_dispatch' && github.event.pull_request.draft == false) || github.event_name == 'workflow_dispatch') }} if: ${{ always() && ((github.event_name != 'workflow_dispatch' && github.event.pull_request.draft == false) || github.event_name == 'workflow_dispatch') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -369,7 +299,7 @@ jobs:
- name: Cleanup all RunPod instances - name: Cleanup all RunPod instances
env: 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", "inference-test-STA", "precision-test-STA", "precision-test-VSA"]' JOB_IDS: '["encoder-test", "vae-test", "transformer-test", "ssim-test-py3.10", "ssim-test-py3.11", "ssim-test-py3.12"]'
RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }} RUNPOD_API_KEY: ${{ secrets.RUNPOD_API_KEY }}
GITHUB_RUN_ID: ${{ github.run_id }} GITHUB_RUN_ID: ${{ github.run_id }}
run: python .github/scripts/runpod_cleanup.py run: python .github/scripts/runpod_cleanup.py
-5
View File
@@ -59,8 +59,3 @@ docs/source/inference/examples/
# Static images # Static images
!docs/source/_static/images/**/*.png !docs/source/_static/images/**/*.png
# Local scripts (keep local but don't track in git)
local_scripts/
!comfyui/assets/**/*.png
!comfyui/assets/**/*.gif
+1 -15
View File
@@ -8,18 +8,13 @@ It features a clean, consistent API that works across popular video models, maki
With FastVideo's optimizations, you can achieve more than 3x inference improvement compared to other systems. With FastVideo's optimizations, you can achieve more than 3x inference improvement compared to other systems.
<p align="center"> <p align="center">
| <a href="https://hao-ai-lab.github.io/FastVideo"><b>Documentation</b></a> | <a href="https://hao-ai-lab.github.io/FastVideo/inference/inference_quick_start.html"><b> Quick Start</b></a> | 🤗 <a href="https://huggingface.co/FastVideo/FastHunyuan" target="_blank"><b>FastHunyuan</b></a> | 🤗 <a href="https://huggingface.co/FastVideo/FastMochi-diffusers" target="_blank"><b>FastMochi</b></a> | 🟣💬 <a href="https://join.slack.com/t/fastvideo/shared_invite/zt-38u6p1jqe-yDI1QJOCEnbtkLoaI5bjZQ" target="_blank"> <b>Slack</b> </a> | | <a href="https://hao-ai-lab.github.io/FastVideo"><b>Documentation</b></a> | <a href="https://hao-ai-lab.github.io/FastVideo/inference/inference_quick_start.html"><b> Quick Start</b></a> | 🤗 <a href="https://huggingface.co/FastVideo/FastHunyuan" target="_blank"><b>FastHunyuan</b></a> | 🤗 <a href="https://huggingface.co/FastVideo/FastMochi-diffusers" target="_blank"><b>FastMochi</b></a> | 🟣💬 <a href="https://join.slack.com/t/fastvideo/shared_invite/zt-2zf6ru791-sRwI9lPIUJQq1mIeB_yjJg" target="_blank"> <b>Slack</b> </a> |
</p> </p>
<div align="center"> <div align="center">
<img src=assets/perf.png width="90%"/> <img src=assets/perf.png width="90%"/>
</div> </div>
## NEWS
- ```2025/06/14```: Release finetuning and inference code for [VSA](https://arxiv.org/pdf/2505.13389)
- ```2025/04/24```: [FastVideo V1](https://hao-ai-lab.github.io/blogs/fastvideo/) is released!
- ```2025/02/18```: Release the inference code for [Sliding Tile Attention](https://hao-ai-lab.github.io/blogs/sta/).
## Key Features ## Key Features
FastVideo has the following features: FastVideo has the following features:
@@ -133,15 +128,6 @@ We thank MBZUAI and [Anyscale](https://www.anyscale.com/) for their support thro
If you use FastVideo for your research, please cite our paper: If you use FastVideo for your research, please cite our paper:
```bibtex ```bibtex
@misc{zhang2025vsafastervideodiffusion,
title={VSA: Faster Video Diffusion with Trainable Sparse Attention},
author={Peiyuan Zhang and Haofeng Huang and Yongqi Chen and Will Lin and Zhengzhong Liu and Ion Stoica and Eric Xing and Hao Zhang},
year={2025},
eprint={2505.13389},
archivePrefix={arXiv},
primaryClass={cs.CV},
url={https://arxiv.org/abs/2505.13389},
}
@misc{zhang2025fastvideogenerationsliding, @misc{zhang2025fastvideogenerationsliding,
title={Fast Video Generation with Sliding Tile Attention}, title={Fast Video Generation with Sliding Tile Attention},
author={Peiyuan Zhang and Yongqi Chen and Runlong Su and Hangliang Ding and Ion Stoica and Zhenghong Liu and Hao Zhang}, author={Peiyuan Zhang and Yongqi Chen and Runlong Su and Hangliang Ding and Ion Stoica and Zhenghong Liu and Hao Zhang},
-138
View File
@@ -1,138 +0,0 @@
# ComfyUI-FastVideo
A custom node suite for ComfyUI that provides accelerated video generation using [FastVideo](https://github.com/hao-ai-labs/FastVideo). See the [blog post](https://hao-ai-lab.github.io/blogs/fastvideo/) about FastVideo V1 to learn more.
## Multi-GPU Parallel Inference
One of the key features ComfyUI-FastVideo brings to ComfyUI is its ability to distribute the generation workload across multiple GPUs, resulting in significantly faster inference times.
![Wan2.1-I2V-14B-480P-Diffusers](./assets/wani2v.gif)
Example of Wan2.1-I2V-14B-480P-Diffusers model running on 4 GPUs.
## Features
- Generate high-quality videos from text prompts and images
- Configurable video parameters (prompt, resolution, frame count, FPS)
- Support for multiple GPUs with tensor and sequence parallelism
- Advanced configuration options for VAE, Text Encoder, and DIT components
- Interruption/cancellation support for long-running generations
## Installation
### Requirements
- [ComfyUI](https://github.com/comfyanonymous/ComfyUI)
- CUDA-capable GPU(s) with sufficient VRAM
### Install using ComfyUI Manager
Coming soon!
### Manual Installation
#### Copy the FastVideo `comfyui` directory into your ComfyUI custom_nodes directory:
```bash
cp -r /path/to/FastVideo/comfyui /path/to/ComfyUI/custom_nodes/FastVideo
```
#### Install dependencies:
Currently, the only dependency is `fastvideo`, which can be installed using pip.
```bash
pip install fastvideo
```
#### Install missing custom nodes:
`ComfyUI-VideoHelperSuite`:
```bash
cd /path/to/ComfyUI/custom_nodes
git clone https://github.com/Kosinkadink/ComfyUI-VideoHelperSuite.git
```
If you're seeing `ImportError: libGL.so.1: cannot open shared object file: No such file or directory`,
you may need to install ffmpeg
```bash
apt-get update && apt-get install ffmpeg
```
## Usage
After installation, the following nodes will be available in the ComfyUI interface under the "fastvideo" category:
- **Video Generator**: The main node for generating videos from prompts
- **Inference Args**: Configure video generation parameters
- **VAE Config**
- **Text Encoder Config**
- **DIT Config**
- **Load Image Path**: Load images for potential conditioning
You may have noticed many arguments on the nodes have 'auto' as the default value. This is because FastVideo will automatically detect the best values for these parameters based on the model and the hardware. However, you can also manually configure these parameters to get the best performance for your specific use case. We plan on releasing more optimized workflow files for different models and hardware configurations in the future.
You can see what some of the default configurations are by looking at the FastVideo repo:
- [Wan2.1-I2V-14B-480P-Diffusers](https://github.com/hao-ai-lab/FastVideo/blob/main/fastvideo/v1/configs/wan_14B_i2v_480p_pipeline.json)
- [FastHunyuan-diffusers](https://github.com/hao-ai-lab/FastVideo/blob/main/fastvideo/v1/configs/fasthunyuan_t2v.json)
### Node Configuration
#### Video Generator
- **prompt**: Text description of the video to generate
- **output_path**: Directory where generated videos will be saved
- **num_gpus**: Number of GPUs to use for generation
- **model_path**: Path to the FastVideo model
- **embedded_cfg_scale**: Classifier-free guidance scale
- **sp_size**: Sequence parallelism size (usually should match num_gpus)
- **tp_size**: Tensor parallelism size (usually should match num_gpus)
- **precision**: Model precision (fp16 or bf16)
`model_path takes either a model id from huggingface or a local path to a model. Models by default will be downloaded to ~/.cache/huggingface/hub/ and cached for subsequent runs.`
#### Inference Args
- **height/width**: Resolution of the output video
- **num_frames**: Number of frames to generate
- **num_inference_steps**: Number of diffusion steps per frame
- **guidance_scale**: Classifier-free guidance scale
- **flow_shift**: Frame flow shift parameter
- **seed**: Random seed for reproducible generation
- **fps**: Frames per second of the output video
- **image_path**: Optional path to input image for conditioning (for i2v models)
## Memory Management
Models will remain loaded in GPU memory between runs when you only change inference arguments (such as prompt, resolution, frame count, FPS, guidance scale, etc.) or the prompt text. This allows for faster subsequent generations since the model doesn't need to be reloaded.
However, if you need to change the following parameters, you will need to restart the ComfyUI server:
- **Number of GPUs** (`num_gpus`)
- **Model path** (`model_path`)
- **Tensor parallelism size** (`tp_size`)
- **Sequence parallelism size** (`sp_size`)
These parameters affect the model's distribution across GPUs and require a complete reinitialization of the model pipeline.
## Example workflows
### Text to Video
FastVideo-FastHunyuan-diffusers
![FastVideo-FastHunyuan-diffusers](./assets/fasthunyuan.png)
- [FastHunyuan-diffusers.json](./examples/FastHunyuan-diffusers.json)
### Image to Video
Wan2.1-I2V-14B-480P-Diffusers
![Wan2.1-I2V-14B-480P-Diffusers](./assets/wani2v.png)
- [Wan2.1-I2V-14B-480P-Diffusers.json](./examples/Wan2.1-I2V-14B-480P-Diffusers.json)
## License
This project is licensed under Apache 2.0.
-5
View File
@@ -1,5 +0,0 @@
from .video_generator.nodes import (NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS)
WEB_DIRECTORY = "./web"
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 769 KiB

-645
View File
@@ -1,645 +0,0 @@
{
"id": "23a1f065-bbba-4a8f-b144-944e1318fcbf",
"revision": 0,
"last_node_id": 8,
"last_link_id": 7,
"nodes": [
{
"id": 4,
"type": "VAEConfig",
"pos": [
374.2159423828125,
554.85888671875
],
"size": [
334.080078125,
322
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "vae_config",
"type": "VAE_CONFIG",
"links": [
2
]
}
],
"properties": {
"Node name for S&R": "VAEConfig"
},
"widgets_values": [
-99999,
-99999,
-99999,
-99999,
-99999,
-99999,
-99999,
-99999,
-99999,
-99999,
-99999,
-99999
],
"auto_widget_states": {
"load_encoder": {
"isAuto": true,
"value": -99999,
"cachedValue": true
},
"load_decoder": {
"isAuto": true,
"value": -99999,
"cachedValue": true
},
"tile_sample_min_height": {
"isAuto": true,
"value": -99999,
"cachedValue": 256
},
"tile_sample_min_width": {
"isAuto": true,
"value": -99999,
"cachedValue": 256
},
"tile_sample_min_num_frames": {
"isAuto": true,
"value": -99999,
"cachedValue": 16
},
"tile_sample_stride_height": {
"isAuto": true,
"value": -99999,
"cachedValue": 192
},
"tile_sample_stride_width": {
"isAuto": true,
"value": -99999,
"cachedValue": 192
},
"tile_sample_stride_num_frames": {
"isAuto": true,
"value": -99999,
"cachedValue": 12
},
"blend_num_frames": {
"isAuto": true,
"value": -99999,
"cachedValue": 0
},
"use_tiling": {
"isAuto": true,
"value": -99999,
"cachedValue": true
},
"use_temporal_tiling": {
"isAuto": true,
"value": -99999,
"cachedValue": true
},
"use_parallel_tiling": {
"isAuto": true,
"value": -99999,
"cachedValue": true
}
}
},
{
"id": 5,
"type": "TextEncoderConfig",
"pos": [
416.4937744140625,
953.6171875
],
"size": [
270,
106
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "text_encoder_config",
"type": "TEXT_ENCODER_CONFIG",
"links": [
7
]
}
],
"properties": {
"Node name for S&R": "TextEncoderConfig"
},
"widgets_values": [
-99999,
-99999,
-99999
],
"auto_widget_states": {
"prefix": {
"isAuto": true,
"value": -99999,
"cachedValue": ""
},
"quant_config": {
"isAuto": true,
"value": -99999,
"cachedValue": ""
},
"lora_config": {
"isAuto": true,
"value": -99999,
"cachedValue": ""
}
}
},
{
"id": 6,
"type": "DITConfig",
"pos": [
415.1928405761719,
1154.1573486328125
],
"size": [
270,
82
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "dit_config",
"type": "DIT_CONFIG",
"links": [
6
]
}
],
"properties": {
"Node name for S&R": "DITConfig"
},
"widgets_values": [
-99999,
-99999
],
"auto_widget_states": {
"prefix": {
"isAuto": true,
"value": -99999,
"cachedValue": ""
},
"quant_config": {
"isAuto": true,
"value": -99999,
"cachedValue": ""
}
}
},
{
"id": 1,
"type": "VideoGenerator",
"pos": [
818.804931640625,
348.9299621582031
],
"size": [
400,
436
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "inference_args",
"shape": 7,
"type": "INFERENCE_ARGS",
"link": 3
},
{
"name": "vae_config",
"shape": 7,
"type": "VAE_CONFIG",
"link": 2
},
{
"name": "text_encoder_config",
"shape": 7,
"type": "TEXT_ENCODER_CONFIG",
"link": 7
},
{
"name": "dit_config",
"shape": 7,
"type": "DIT_CONFIG",
"link": 6
}
],
"outputs": [
{
"name": "video_path",
"type": "STRING",
"links": [
4
]
}
],
"properties": {
"Node name for S&R": "VideoGenerator"
},
"widgets_values": [
"A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes wide with interest. The playful yet serene atmosphere is complemented by soft natural light filtering through the petals. Mid-shot, warm and cheerful tones.",
"/workspace/ComfyUI/outputs_video/",
2,
"FastVideo/FastHunyuan-diffusers",
-99999,
-99999,
-99999,
-99999,
-99999,
-99999,
-99999,
-99999,
-99999
],
"auto_widget_states": {
"embedded_cfg_scale": {
"isAuto": true,
"value": -99999,
"cachedValue": 6
},
"sp_size": {
"isAuto": true,
"value": -99999,
"cachedValue": 2
},
"tp_size": {
"isAuto": true,
"value": -99999,
"cachedValue": 2
},
"vae_precision": {
"isAuto": true,
"value": -99999,
"cachedValue": "fp16"
},
"vae_tiling": {
"isAuto": true,
"value": -99999,
"cachedValue": true
},
"vae_sp": {
"isAuto": true,
"value": -99999,
"cachedValue": true
},
"text_encoder_precision": {
"isAuto": true,
"value": -99999,
"cachedValue": "fp16"
},
"precision": {
"isAuto": true,
"value": -99999,
"cachedValue": "fp16"
},
"use_cpu_offload": {
"isAuto": true,
"value": -99999,
"cachedValue": true
}
}
},
{
"id": 3,
"type": "VHS_LoadVideoPath",
"pos": [
1350.136962890625,
331.20361328125
],
"size": [
231.8896484375,
286
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "meta_batch",
"shape": 7,
"type": "VHS_BatchManager",
"link": null
},
{
"name": "vae",
"shape": 7,
"type": "VAE",
"link": null
},
{
"name": "video",
"type": "STRING",
"widget": {
"name": "video"
},
"link": 4
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
5
]
},
{
"name": "frame_count",
"type": "INT",
"links": null
},
{
"name": "audio",
"type": "AUDIO",
"links": null
},
{
"name": "video_info",
"type": "VHS_VIDEOINFO",
"links": null
}
],
"properties": {
"Node name for S&R": "VHS_LoadVideoPath"
},
"widgets_values": {
"video": "",
"force_rate": 0,
"custom_width": 0,
"custom_height": 0,
"frame_load_cap": 0,
"skip_first_frames": 0,
"select_every_nth": 1,
"format": "Wan",
"videopreview": {
"hidden": false,
"paused": false,
"params": {
"filename": "",
"type": "path",
"format": "video/",
"force_rate": 0,
"custom_width": 0,
"custom_height": 0,
"frame_load_cap": 0,
"skip_first_frames": 0,
"select_every_nth": 1
}
}
}
},
{
"id": 2,
"type": "InferenceArgs",
"pos": [
411.46307373046875,
178.18182373046875
],
"size": [
278.73828125,
298
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "inference_args",
"type": "INFERENCE_ARGS",
"links": [
3
]
}
],
"properties": {
"Node name for S&R": "InferenceArgs"
},
"widgets_values": [
720,
1280,
45,
6,
-99999,
-99999,
1025,
"fixed",
24,
-99999,
-99999
],
"auto_widget_states": {
"height": {
"isAuto": false,
"value": 720,
"cachedValue": 720
},
"width": {
"isAuto": false,
"value": 1280,
"cachedValue": 1280
},
"num_frames": {
"isAuto": false,
"value": 45,
"cachedValue": 45
},
"num_inference_steps": {
"isAuto": false,
"value": 6,
"cachedValue": 6
},
"guidance_scale": {
"isAuto": true,
"value": -99999,
"cachedValue": 1
},
"flow_shift": {
"isAuto": true,
"value": -99999,
"cachedValue": 17
},
"seed": {
"isAuto": false,
"value": 1025,
"cachedValue": 1024
},
"fps": {
"isAuto": false,
"value": 24,
"cachedValue": 24
},
"image_path": {
"isAuto": true,
"value": -99999,
"cachedValue": "X://insert/path/here.mp4"
},
"enable_teacache": {
"isAuto": true,
"value": -99999,
"cachedValue": true
}
}
},
{
"id": 8,
"type": "VHS_VideoCombine",
"pos": [
1668.3499755859375,
328.22625732421875
],
"size": [
507.507080078125,
622.2227172851562
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 5
},
{
"name": "audio",
"shape": 7,
"type": "AUDIO",
"link": null
},
{
"name": "meta_batch",
"shape": 7,
"type": "VHS_BatchManager",
"link": null
},
{
"name": "vae",
"shape": 7,
"type": "VAE",
"link": null
}
],
"outputs": [
{
"name": "Filenames",
"type": "VHS_FILENAMES",
"links": null
}
],
"properties": {
"Node name for S&R": "VHS_VideoCombine"
},
"widgets_values": {
"frame_rate": 24,
"loop_count": 0,
"filename_prefix": "",
"format": "video/h264-mp4",
"pix_fmt": "yuv420p",
"crf": 19,
"save_metadata": true,
"trim_to_audio": false,
"pingpong": false,
"save_output": false,
"videopreview": {
"hidden": false,
"paused": false,
"params": {
"filename": "._00003.mp4",
"subfolder": "",
"type": "temp",
"format": "video/h264-mp4",
"frame_rate": 24,
"workflow": "._00003.png",
"fullpath": "/workspace/ComfyUI/temp/._00003.mp4"
}
}
}
}
],
"links": [
[
2,
4,
0,
1,
1,
"VAE_CONFIG"
],
[
3,
2,
0,
1,
0,
"INFERENCE_ARGS"
],
[
4,
1,
0,
3,
2,
"STRING"
],
[
5,
3,
0,
8,
0,
"IMAGE"
],
[
6,
6,
0,
1,
3,
"DIT_CONFIG"
],
[
7,
5,
0,
1,
2,
"TEXT_ENCODER_CONFIG"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.9090909090909091,
"offset": [
112.86678372727341,
-71.45635903989245
]
},
"frontendVersion": "1.20.4",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true
},
"version": 0.4
}
@@ -1,697 +0,0 @@
{
"id": "23a1f065-bbba-4a8f-b144-944e1318fcbf",
"revision": 0,
"last_node_id": 8,
"last_link_id": 7,
"nodes": [
{
"id": 7,
"type": "LoadImagePath",
"pos": [
33.15385437011719,
191.2037353515625
],
"size": [
270,
334
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "image_path",
"type": "STRING",
"links": [
1
]
},
{
"name": "IMAGE",
"type": "IMAGE",
"links": null
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImagePath"
},
"widgets_values": [
"woman.jpg",
"image"
]
},
{
"id": 3,
"type": "VHS_LoadVideoPath",
"pos": [
1350.136962890625,
331.20361328125
],
"size": [
231.8896484375,
286
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "meta_batch",
"shape": 7,
"type": "VHS_BatchManager",
"link": null
},
{
"name": "vae",
"shape": 7,
"type": "VAE",
"link": null
},
{
"name": "video",
"type": "STRING",
"widget": {
"name": "video"
},
"link": 4
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
5
]
},
{
"name": "frame_count",
"type": "INT",
"links": null
},
{
"name": "audio",
"type": "AUDIO",
"links": null
},
{
"name": "video_info",
"type": "VHS_VIDEOINFO",
"links": null
}
],
"properties": {
"Node name for S&R": "VHS_LoadVideoPath"
},
"widgets_values": {
"video": "",
"force_rate": 0,
"custom_width": 0,
"custom_height": 0,
"frame_load_cap": 0,
"skip_first_frames": 0,
"select_every_nth": 1,
"format": "Wan",
"videopreview": {
"hidden": false,
"paused": false,
"params": {
"filename": "",
"type": "path",
"format": "video/",
"force_rate": 0,
"custom_width": 0,
"custom_height": 0,
"frame_load_cap": 0,
"skip_first_frames": 0,
"select_every_nth": 1
}
}
}
},
{
"id": 8,
"type": "VHS_VideoCombine",
"pos": [
1668.3499755859375,
328.22625732421875
],
"size": [
214.7587890625,
334
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 5
},
{
"name": "audio",
"shape": 7,
"type": "AUDIO",
"link": null
},
{
"name": "meta_batch",
"shape": 7,
"type": "VHS_BatchManager",
"link": null
},
{
"name": "vae",
"shape": 7,
"type": "VAE",
"link": null
}
],
"outputs": [
{
"name": "Filenames",
"type": "VHS_FILENAMES",
"links": null
}
],
"properties": {
"Node name for S&R": "VHS_VideoCombine"
},
"widgets_values": {
"frame_rate": 24,
"loop_count": 0,
"filename_prefix": "",
"format": "video/h264-mp4",
"pix_fmt": "yuv420p",
"crf": 19,
"save_metadata": true,
"trim_to_audio": false,
"pingpong": false,
"save_output": false,
"videopreview": {
"hidden": false,
"paused": false,
"params": {}
}
}
},
{
"id": 4,
"type": "VAEConfig",
"pos": [
374.2159423828125,
554.85888671875
],
"size": [
334.080078125,
322
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "vae_config",
"type": "VAE_CONFIG",
"links": [
2
]
}
],
"properties": {
"Node name for S&R": "VAEConfig"
},
"widgets_values": [
true,
true,
256,
256,
16,
192,
192,
12,
0,
true,
true,
true
],
"auto_widget_states": {
"load_encoder": {
"isAuto": true,
"value": true,
"cachedValue": true
},
"load_decoder": {
"isAuto": true,
"value": true,
"cachedValue": true
},
"tile_sample_min_height": {
"isAuto": true,
"value": 256,
"cachedValue": 256
},
"tile_sample_min_width": {
"isAuto": true,
"value": 256,
"cachedValue": 256
},
"tile_sample_min_num_frames": {
"isAuto": true,
"value": 16,
"cachedValue": 16
},
"tile_sample_stride_height": {
"isAuto": true,
"value": 192,
"cachedValue": 192
},
"tile_sample_stride_width": {
"isAuto": true,
"value": 192,
"cachedValue": 192
},
"tile_sample_stride_num_frames": {
"isAuto": true,
"value": 12,
"cachedValue": 12
},
"blend_num_frames": {
"isAuto": true,
"value": 0,
"cachedValue": 0
},
"use_tiling": {
"isAuto": true,
"value": true,
"cachedValue": true
},
"use_temporal_tiling": {
"isAuto": true,
"value": true,
"cachedValue": true
},
"use_parallel_tiling": {
"isAuto": true,
"value": true,
"cachedValue": true
}
}
},
{
"id": 2,
"type": "InferenceArgs",
"pos": [
411.46307373046875,
178.18182373046875
],
"size": [
278.73828125,
298
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "image_path",
"shape": 7,
"type": "STRING",
"widget": {
"name": "image_path"
},
"link": 1
}
],
"outputs": [
{
"name": "inference_args",
"type": "INFERENCE_ARGS",
"links": [
3
]
}
],
"properties": {
"Node name for S&R": "InferenceArgs"
},
"widgets_values": [
832,
480,
45,
20,
1,
17,
1024,
"fixed",
24,
"X://insert/path/here.mp4",
true
],
"auto_widget_states": {
"height": {
"isAuto": false,
"value": 832,
"cachedValue": 720
},
"width": {
"isAuto": false,
"value": 480,
"cachedValue": 1280
},
"num_frames": {
"isAuto": false,
"value": 45,
"cachedValue": 45
},
"num_inference_steps": {
"isAuto": false,
"value": 20,
"cachedValue": 6
},
"guidance_scale": {
"isAuto": true,
"value": 1,
"cachedValue": 1
},
"flow_shift": {
"isAuto": true,
"value": 17,
"cachedValue": 17
},
"seed": {
"isAuto": false,
"value": 1024,
"cachedValue": 1024
},
"fps": {
"isAuto": false,
"value": 24,
"cachedValue": 24
},
"image_path": {
"isAuto": true,
"value": "X://insert/path/here.mp4",
"cachedValue": "X://insert/path/here.mp4"
},
"enable_teacache": {
"isAuto": true,
"value": true,
"cachedValue": true
}
}
},
{
"id": 1,
"type": "VideoGenerator",
"pos": [
818.804931640625,
348.9299621582031
],
"size": [
400,
436
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "inference_args",
"shape": 7,
"type": "INFERENCE_ARGS",
"link": 3
},
{
"name": "vae_config",
"shape": 7,
"type": "VAE_CONFIG",
"link": 2
},
{
"name": "text_encoder_config",
"shape": 7,
"type": "TEXT_ENCODER_CONFIG",
"link": 7
},
{
"name": "dit_config",
"shape": 7,
"type": "DIT_CONFIG",
"link": 6
}
],
"outputs": [
{
"name": "video_path",
"type": "STRING",
"links": [
4
]
}
],
"properties": {
"Node name for S&R": "VideoGenerator"
},
"widgets_values": [
"A woman crying from laughter.",
"/workspace/ComfyUI/outputs_video/",
4,
"Wan-AI/Wan2.1-I2V-14B-480P-Diffusers",
6,
2,
2,
"fp16",
true,
true,
"fp16",
"fp16",
true
],
"auto_widget_states": {
"embedded_cfg_scale": {
"isAuto": true,
"value": 6,
"cachedValue": 6
},
"sp_size": {
"isAuto": true,
"value": 2,
"cachedValue": 2
},
"tp_size": {
"isAuto": true,
"value": 2,
"cachedValue": 2
},
"vae_precision": {
"isAuto": true,
"value": "fp16",
"cachedValue": "fp16"
},
"vae_tiling": {
"isAuto": true,
"value": true,
"cachedValue": true
},
"vae_sp": {
"isAuto": true,
"value": true,
"cachedValue": true
},
"text_encoder_precision": {
"isAuto": true,
"value": "fp16",
"cachedValue": "fp16"
},
"precision": {
"isAuto": true,
"value": "fp16",
"cachedValue": "fp16"
},
"use_cpu_offload": {
"isAuto": true,
"value": true,
"cachedValue": true
}
}
},
{
"id": 5,
"type": "TextEncoderConfig",
"pos": [
416.4937744140625,
953.6171875
],
"size": [
270,
106
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "text_encoder_config",
"type": "TEXT_ENCODER_CONFIG",
"links": [
7
]
}
],
"properties": {
"Node name for S&R": "TextEncoderConfig"
},
"widgets_values": [
"",
"",
""
],
"auto_widget_states": {
"prefix": {
"isAuto": true,
"value": "",
"cachedValue": ""
},
"quant_config": {
"isAuto": true,
"value": "",
"cachedValue": ""
},
"lora_config": {
"isAuto": true,
"value": "",
"cachedValue": ""
}
}
},
{
"id": 6,
"type": "DITConfig",
"pos": [
415.1928405761719,
1154.1573486328125
],
"size": [
270,
82
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "dit_config",
"type": "DIT_CONFIG",
"links": [
6
]
}
],
"properties": {
"Node name for S&R": "DITConfig"
},
"widgets_values": [
"",
""
],
"auto_widget_states": {
"prefix": {
"isAuto": true,
"value": "",
"cachedValue": ""
},
"quant_config": {
"isAuto": true,
"value": "",
"cachedValue": ""
}
}
}
],
"links": [
[
1,
7,
0,
2,
0,
"STRING"
],
[
2,
4,
0,
1,
1,
"VAE_CONFIG"
],
[
3,
2,
0,
1,
0,
"INFERENCE_ARGS"
],
[
4,
1,
0,
3,
2,
"STRING"
],
[
5,
3,
0,
8,
0,
"IMAGE"
],
[
6,
6,
0,
1,
3,
"DIT_CONFIG"
],
[
7,
5,
0,
1,
2,
"TEXT_ENCODER_CONFIG"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.8264462809917354,
"offset": [
646.7950212991898,
66.17259910028655
]
},
"frontendVersion": "1.20.4",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true
},
"version": 0.4
}
-31
View File
@@ -1,31 +0,0 @@
class DITConfig:
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"prefix": ("STRING", {
"default": ""
}),
"quant_config": ("STRING", {
"default": ""
}),
}
}
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
RETURN_TYPES = ("DIT_CONFIG", )
RETURN_NAMES = ("dit_config", )
FUNCTION = "set_args"
CATEGORY = "fastvideo"
def set_args(self, prefix, quant_config):
raw_args = {"prefix": prefix, "quant_config": quant_config}
# Filter out keys where value is -99999
args = {k: v for k, v in raw_args.items() if str(int(v)) != str(-99999)}
return (args, )
-89
View File
@@ -1,89 +0,0 @@
class InferenceArgs:
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"height": ("INT", {
"default": 720
}),
"width": ("INT", {
"default": 1280
}),
"num_frames": ("INT", {
"default": 45
}),
"num_inference_steps": ("INT", {
"default": 6
}),
"guidance_scale": ("FLOAT", {
"default": 1.0
}),
"flow_shift": ("INT", {
"default": 17
}),
"seed": ("INT", {
"default": 1024
}),
"fps": ("INT", {
"default": 24
}),
"image_path": ("STRING", {
"default": "X://insert/path/here.mp4"
}),
"enable_teacache": ([True, False], {
"default": False
}),
}
}
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
RETURN_TYPES = ("INFERENCE_ARGS", )
RETURN_NAMES = ("inference_args", )
FUNCTION = "set_args"
CATEGORY = "fastvideo"
def set_args(
self,
height,
width,
num_frames,
num_inference_steps,
guidance_scale,
flow_shift,
seed,
fps,
image_path,
enable_teacache,
):
raw_args = {
"height": height,
"width": width,
"num_frames": num_frames,
"num_inference_steps": num_inference_steps,
"guidance_scale": guidance_scale,
"flow_shift": flow_shift,
"seed": seed,
"fps": fps,
"image_path": image_path,
"enable_teacache": enable_teacache,
}
# Filter out keys where value is -99999, handling different types properly
args = {}
for k, v in raw_args.items():
try:
if isinstance(v, str):
if v != "-99999":
args[k] = v
elif v != -99999:
# If it's not a string, compare directly
args[k] = v
except (ValueError, TypeError):
# Include any value that causes an error in comparison
args[k] = v
return (args, )
-103
View File
@@ -1,103 +0,0 @@
import hashlib
import os
import folder_paths
import numpy as np
import torch
from PIL import Image, ImageOps, ImageSequence
from .node_helpers import pillow
class LoadImagePath:
@classmethod
def INPUT_TYPES(s):
input_dir = folder_paths.get_input_directory()
files = [
f for f in os.listdir(input_dir)
if os.path.isfile(os.path.join(input_dir, f))
]
files = folder_paths.filter_files_content_types(files, ["image"])
return {
"required": {
"image": (sorted(files), {
"image_upload": True
})
},
}
CATEGORY = "fastvideo"
RETURN_TYPES = ("STRING", "IMAGE", "MASK")
RETURN_NAMES = ("image_path", "IMAGE", "MASK")
FUNCTION = "load_image"
def load_image(self, image):
image_path = folder_paths.get_annotated_filepath(image)
img = pillow(Image.open, image_path)
output_images: list[torch.Tensor] = []
output_masks: list[torch.Tensor] = []
w, h = None, None
excluded_formats = ['MPO']
for i in ImageSequence.Iterator(img):
processed_image = pillow(ImageOps.exif_transpose, i)
if processed_image is None:
continue
if processed_image.mode == 'I':
processed_image = processed_image.point(lambda i: i * (1 / 255))
image = processed_image.convert("RGB")
if len(output_images) == 0:
w = image.size[0]
h = image.size[1]
if image.size[0] != w or image.size[1] != h:
continue
image = np.array(image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[
None,
]
if 'A' in processed_image.getbands():
mask = np.array(processed_image.getchannel('A')).astype(
np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
elif processed_image.mode == 'P' and 'transparency' in processed_image.info:
mask = np.array(
processed_image.convert('RGBA').getchannel('A')).astype(
np.float32) / 255.0
mask = 1. - torch.from_numpy(mask)
else:
mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")
output_images.append(image)
output_masks.append(mask.unsqueeze(0))
if len(output_images) > 1 and img.format not in excluded_formats:
output_image = torch.cat(output_images, dim=0)
output_mask = torch.cat(output_masks, dim=0)
else:
output_image = output_images[0]
output_mask = output_masks[0]
return (image_path, output_image, output_mask)
@classmethod
def IS_CHANGED(s, image):
image_path = folder_paths.get_annotated_filepath(image)
m = hashlib.sha256()
with open(image_path, 'rb') as f:
m.update(f.read())
return m.digest().hex()
@classmethod
def VALIDATE_INPUTS(s, image):
if not folder_paths.exists_annotated_filepath(image):
return "Invalid image file: {}".format(image)
return True
-68
View File
@@ -1,68 +0,0 @@
import hashlib
from collections.abc import Callable
from typing import Any, TypeVar
import torch
from comfy.cli_args import args
from PIL import ImageFile, UnidentifiedImageError
T = TypeVar('T')
def conditioning_set_values(conditioning: list[Any],
values: dict[str, Any] | None = None) -> list[Any]:
if values is None:
values = {}
c = []
for t in conditioning:
n = [t[0], t[1].copy()]
for k in values:
n[1][k] = values[k]
c.append(n)
return c
def pillow(fn: Callable[[Any], T], arg: Any) -> T:
prev_value = None
try:
x = fn(arg)
except (OSError, UnidentifiedImageError, ValueError
): #PIL issues #4472 and #2445, also fixes ComfyUI issue #3416
prev_value = ImageFile.LOAD_TRUNCATED_IMAGES
ImageFile.LOAD_TRUNCATED_IMAGES = True
x = fn(arg)
finally:
if prev_value is not None:
ImageFile.LOAD_TRUNCATED_IMAGES = prev_value
return x
def hasher() -> Callable[[], Any]:
hashfuncs = {
"md5": hashlib.md5,
"sha1": hashlib.sha1,
"sha256": hashlib.sha256,
"sha512": hashlib.sha512
}
return hashfuncs[args.default_hashing_function]
def string_to_torch_dtype(string: str) -> torch.dtype | None:
if string == "fp32":
return torch.float32
if string == "fp16":
return torch.float16
if string == "bf16":
return torch.bfloat16
return None
def image_alpha_fix(destination: torch.Tensor,
source: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
if destination.shape[-1] < source.shape[-1]:
source = source[..., :destination.shape[-1]]
elif destination.shape[-1] > source.shape[-1]:
destination = torch.nn.functional.pad(destination, (0, 1))
destination[..., -1] = 1.0
return destination, source
-24
View File
@@ -1,24 +0,0 @@
from .dit_config import DITConfig
from .inference_args import InferenceArgs
from .load_image import LoadImagePath
from .text_encoder_config import TextEncoderConfig
from .vae_config import VAEConfig
from .video_generator import VideoGenerator
NODE_CLASS_MAPPINGS = {
"VideoGenerator": VideoGenerator,
"InferenceArgs": InferenceArgs,
"VAEConfig": VAEConfig,
"TextEncoderConfig": TextEncoderConfig,
"DITConfig": DITConfig,
"LoadImagePath": LoadImagePath
}
NODE_DISPLAY_NAME_MAPPINGS = {
"VideoGenerator": "Video Generator",
"InferenceArgs": "Inference Args",
"VAEConfig": "VAE Config",
"TextEncoderConfig": "Text Encoder Config",
"DITConfig": "DIT Config",
"LoadImagePath": "Load Image Path"
}
@@ -1,38 +0,0 @@
class TextEncoderConfig:
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"prefix": ("STRING", {
"default": ""
}),
"quant_config": ("STRING", {
"default": ""
}),
"lora_config": ("STRING", {
"default": ""
}),
}
}
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
RETURN_TYPES = ("TEXT_ENCODER_CONFIG", )
RETURN_NAMES = ("text_encoder_config", )
FUNCTION = "set_args"
CATEGORY = "fastvideo"
def set_args(self, prefix, quant_config, lora_config):
raw_args = {
"prefix": prefix,
"quant_config": quant_config,
"lora_config": lora_config
}
# Filter out keys where value is -99999
args = {k: v for k, v in raw_args.items() if str(int(v)) != str(-99999)}
return (args, )
-88
View File
@@ -1,88 +0,0 @@
class VAEConfig:
@classmethod
def INPUT_TYPES(cls):
return {
"optional": {
"load_encoder": ([True, False], {
"default": True
}),
"load_decoder": ([True, False], {
"default": True
}),
"tile_sample_min_height": ("INT", {
"default": 256
}),
"tile_sample_min_width": ("INT", {
"default": 256
}),
"tile_sample_min_num_frames": ("INT", {
"default": 16
}),
"tile_sample_stride_height": ("INT", {
"default": 192
}),
"tile_sample_stride_width": ("INT", {
"default": 192
}),
"tile_sample_stride_num_frames": ("INT", {
"default": 12
}),
"blend_num_frames": ("INT", {
"default": 0
}),
"use_tiling": ([True, False], {
"default": True
}),
"use_temporal_tiling": ([True, False], {
"default": True
}),
"use_parallel_tiling": ([True, False], {
"default": True
}),
}
}
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
RETURN_TYPES = ("VAE_CONFIG", )
RETURN_NAMES = ("vae_config", )
FUNCTION = "set_args"
CATEGORY = "fastvideo"
def set_args(
self,
load_encoder,
load_decoder,
tile_sample_min_height,
tile_sample_min_width,
tile_sample_min_num_frames,
tile_sample_stride_height,
tile_sample_stride_width,
tile_sample_stride_num_frames,
blend_num_frames,
use_tiling,
use_temporal_tiling,
use_parallel_tiling,
):
raw_args = {
"load_encoder": load_encoder,
"load_decoder": load_decoder,
"tile_sample_min_height": tile_sample_min_height,
"tile_sample_min_width": tile_sample_min_width,
"tile_sample_min_num_frames": tile_sample_min_num_frames,
"tile_sample_stride_height": tile_sample_stride_height,
"tile_sample_stride_width": tile_sample_stride_width,
"tile_sample_stride_num_frames": tile_sample_stride_num_frames,
"blend_num_frames": blend_num_frames,
"use_tiling": use_tiling,
"use_temporal_tiling": use_temporal_tiling,
"use_parallel_tiling": use_parallel_tiling,
}
# Filter out any value explicitly set to -99999
args = {k: v for k, v in raw_args.items() if str(int(v)) != str(-99999)}
return (args, )
-315
View File
@@ -1,315 +0,0 @@
from __future__ import annotations
import glob
import os
import signal
import sys
import threading
import time
from typing import Any
from comfy.model_management import processing_interrupted
from fastvideo import PipelineConfig
from fastvideo import VideoGenerator as FastVideoGenerator
sys.path.insert(
0,
os.path.dirname(
os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
# Custom exception for interruption
class GenerationInterruptedException(Exception):
pass
# Custom exception for interruption that ComfyUI will recognize
class GenerationCancelledException(Exception):
def __init__(self,
message: str = "Generation was cancelled by user") -> None:
self.message = message
super().__init__(self.message)
def update_config_from_args(config: Any, args_dict: dict[str, Any]) -> None:
"""
Update configuration object from arguments dictionary.
Args:
config: The configuration object to update
args_dict: Dictionary containing arguments
"""
for key, value in args_dict.items():
if hasattr(config, key) and value is not None:
if key == "text_encoder_precisions" and isinstance(value, list):
setattr(config, key, tuple(value))
else:
setattr(config, key, value)
class VideoGenerator:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"prompt": ("STRING", {
"multiline":
True,
"default":
"A ripe orange tumbles gently from a tree and lands on the head of a lounging capybara, "
"who blinks slowly in response. The moment is quietly humorous and oddly serene, framed by "
"lush green foliage and dappled sunlight. Mid-shot, warm and whimsical tones."
}),
"output_path": ("STRING", {
"default": "/workspace/ComfyUI/outputs_video/"
}),
"num_gpus": ("INT", {
"default": 2,
"min": 1,
"max": 16
}),
"model_path": ("STRING", {
"default": "FastVideo/FastHunyuan-diffusers"
})
},
"optional": {
"inference_args": ("INFERENCE_ARGS", ),
"embedded_cfg_scale": ("FLOAT", {
"default": 6.0
}),
"sp_size": ("INT", {
"default": 2
}),
"tp_size": ("INT", {
"default": 2
}),
"vae_config": ("VAE_CONFIG", ),
"vae_precision": (["fp16", "bf16"], {
"default": "fp16"
}),
"vae_tiling": ([True, False], {
"default": True
}),
"vae_sp": ([True, False], {
"default": False
}),
"text_encoder_config": ("TEXT_ENCODER_CONFIG", ),
"text_encoder_precision": (["fp16", "bf16"], {
"default": "fp16"
}),
"dit_config": ("DIT_CONFIG", ),
"precision": (["fp16", "bf16"], {
"default": "fp16"
}),
"use_cpu_offload": ([True, False], {
"default": False
}),
}
}
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
RETURN_TYPES = ("STRING", )
RETURN_NAMES = ("video_path", )
FUNCTION = "launch_inference"
CATEGORY = "fastvideo"
generator: FastVideoGenerator | None = None
_interrupt_thread: threading.Thread | None = None
_generation_active: bool = False
_generation_interrupted: bool = False
_interrupt_event: threading.Event = threading.Event()
_generation_thread: threading.Thread | None = None
_generation_result: str | None = None
_generation_exception: Exception | None = None
def _monitor_for_interruption(self):
"""Background thread that monitors for interruption requests"""
time.sleep(2) # Give the generation thread time to send execute_forward
while self._generation_active and not self._interrupt_event.is_set():
if processing_interrupted():
print("Video generation interrupted by user")
self._generation_interrupted = True
# Try to send interrupt signal to worker processes
if self.generator is not None and hasattr(
self.generator, 'executor'):
try:
# The MultiprocExecutor has a workers attribute
if hasattr(self.generator.executor, 'workers'):
for worker in self.generator.executor.workers:
if worker.is_alive():
os.kill(worker.pid, signal.SIGINT)
print("Interrupt signal sent to worker processes")
except Exception as e:
print(f"Error sending interrupt signal: {e}")
# Set the interrupt event to notify other threads
self._interrupt_event.set()
break
time.sleep(0.5)
def _run_generation(self, prompt: str, output_path: str,
inference_args: dict[str, Any]) -> None:
"""Thread function to run the generation"""
try:
if self.generator is not None:
self.generator.generate_video(prompt=prompt,
output_path=output_path,
**inference_args)
self._generation_result = os.path.join(output_path,
f"{prompt[:100]}.mp4")
else:
raise RuntimeError("Generator is not initialized")
except Exception as e:
self._generation_exception = e
self._interrupt_event.set()
def load_output_video(self, output_dir):
video_extensions = ["*.mp4", "*.avi", "*.mov", "*.mkv"]
video_files = []
for ext in video_extensions:
video_files.extend(glob.glob(os.path.join(output_dir, ext)))
if not video_files:
print("No video files found in output directory: %s", output_dir)
return ""
video_files.sort()
return video_files[0]
def launch_inference(
self,
prompt,
output_path,
num_gpus,
model_path,
embedded_cfg_scale,
sp_size,
tp_size,
vae_precision,
vae_tiling,
vae_sp,
text_encoder_precision,
precision,
inference_args=None,
vae_config=None,
text_encoder_config=None,
dit_config=None,
use_cpu_offload=None,
):
print('Running FastVideo inference')
# Reset interruption flag and event
self._generation_interrupted = False
self._interrupt_event.clear()
self._generation_result = None
self._generation_exception = None
# Load pipeline config from model path
pipeline_config = PipelineConfig.from_pretrained(model_path)
print('pipeline_config', pipeline_config)
# Update configs with provided config dictionaries
if dit_config is not None:
update_config_from_args(pipeline_config.dit_config, dit_config)
if vae_config is not None:
update_config_from_args(pipeline_config.vae_config, vae_config)
if text_encoder_config is not None:
update_config_from_args(pipeline_config.text_encoder_configs,
text_encoder_config)
# Update top-level pipeline config with remaining arguments
raw_pipeline_args = {}
if embedded_cfg_scale is not None:
raw_pipeline_args['embedded_cfg_scale'] = embedded_cfg_scale
if precision is not None:
raw_pipeline_args['precision'] = precision
if vae_precision is not None:
raw_pipeline_args['vae_precision'] = vae_precision
if vae_tiling is not None:
raw_pipeline_args['vae_tiling'] = vae_tiling
if vae_sp is not None:
raw_pipeline_args['vae_sp'] = vae_sp
if text_encoder_precision is not None:
raw_pipeline_args['text_encoder_precision'] = text_encoder_precision
# Filter out any value explicitly set to -99999 (auto values)
pipeline_args = {
k: v
for k, v in raw_pipeline_args.items() if str(int(v)) != str(-99999)
}
update_config_from_args(pipeline_config, pipeline_args)
raw_generation_args = {}
if num_gpus is not None:
raw_generation_args['num_gpus'] = num_gpus
if tp_size is not None:
raw_generation_args['tp_size'] = tp_size
if sp_size is not None:
raw_generation_args['sp_size'] = sp_size
if use_cpu_offload is not None:
raw_generation_args['use_cpu_offload'] = use_cpu_offload
generation_args = {
k: v
for k, v in raw_generation_args.items()
if str(int(v)) != str(-99999)
}
if self.generator is None:
print('generation_args', generation_args)
print('pipeline_config', pipeline_config)
self.generator = FastVideoGenerator.from_pretrained(
model_path=model_path,
**generation_args,
pipeline_config=pipeline_config)
print('inference_args', inference_args)
# Start a thread to run the generation
self._generation_thread = threading.Thread(target=self._run_generation,
args=(prompt, output_path,
inference_args),
daemon=True)
self._generation_thread.start()
# Start a background thread to monitor for interruptions
self._generation_active = True
self._interrupt_thread = threading.Thread(
target=self._monitor_for_interruption, daemon=True)
self._interrupt_thread.start()
# Wait for either completion or interruption
while self._generation_thread.is_alive(
) and not self._interrupt_event.is_set():
self._generation_thread.join(timeout=0.5)
self._generation_active = False
if self._interrupt_thread:
self._interrupt_thread.join(timeout=1.0)
self._interrupt_thread = None
if self._generation_interrupted:
print("Video generation was cancelled by user")
raise GenerationCancelledException()
elif self._generation_exception:
# Re-raise the exception from the generation thread
raise self._generation_exception
elif self._generation_result:
return (self._generation_result, )
else:
# This shouldn't happen, but just in case
print("Generation completed but no result was produced")
raise Exception("Generation failed to produce a result")
-593
View File
@@ -1,593 +0,0 @@
import { app } from '../../../scripts/app.js'
function chainCallback(object, property, callback) {
if (object == undefined) {
console.error("Tried to add callback to non-existent object");
return;
}
if (property in object && object[property]) {
const callback_orig = object[property];
object[property] = function () {
const r = callback_orig.apply(this, arguments);
return callback.apply(this, arguments) ?? r;
};
} else {
object[property] = callback;
}
}
function drawAutoAnnotated(ctx, node, widget_width, y, H) {
const litegraph_base = LiteGraph;
const show_text = app.canvas.ds.scale >= 0.5;
const margin = 15;
const autoTextWidth = 30;
const autoTextRightMargin = 5;
ctx.textAlign = 'left';
ctx.strokeStyle = litegraph_base.WIDGET_OUTLINE_COLOR;
ctx.fillStyle = litegraph_base.WIDGET_BGCOLOR;
ctx.beginPath();
if (show_text && ctx.roundRect) {
ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]);
} else {
ctx.rect(margin, y, widget_width - margin * 2, H);
}
ctx.fill();
if (show_text) {
if (!this.disabled) ctx.stroke();
const isAuto = this.isAuto === true;
ctx.save();
if (isAuto) {
ctx.fillStyle = litegraph_base.WIDGET_TEXT_COLOR;
ctx.strokeStyle = litegraph_base.WIDGET_TEXT_COLOR;
} else {
ctx.fillStyle = litegraph_base.WIDGET_SECONDARY_TEXT_COLOR;
ctx.strokeStyle = litegraph_base.WIDGET_SECONDARY_TEXT_COLOR;
}
// Position for the cog
const cogX = widget_width - autoTextRightMargin - autoTextWidth - 6;
const cogY = y + H * 0.5;
const cogRadius = 6;
const toothLength = 2;
const numTeeth = 8;
const holeRadius = 2; // Radius of the center hole
// Draw the cog
ctx.beginPath();
ctx.arc(cogX, cogY, cogRadius - toothLength, 0, Math.PI * 2);
ctx.fill();
// Draw the center hole (by clearing it)
ctx.beginPath();
ctx.arc(cogX, cogY, holeRadius, 0, Math.PI * 2);
ctx.fillStyle = litegraph_base.WIDGET_BGCOLOR;
ctx.fill();
// Reset fill style for the teeth
if (isAuto) {
ctx.fillStyle = litegraph_base.WIDGET_TEXT_COLOR;
} else {
ctx.fillStyle = litegraph_base.WIDGET_SECONDARY_TEXT_COLOR;
}
// Draw teeth
ctx.beginPath();
for (let i = 0; i < numTeeth; i++) {
const angle = (i / numTeeth) * Math.PI * 2;
const innerX = cogX + (cogRadius - toothLength) * Math.cos(angle);
const innerY = cogY + (cogRadius - toothLength) * Math.sin(angle);
const outerX = cogX + cogRadius * Math.cos(angle);
const outerY = cogY + cogRadius * Math.sin(angle);
ctx.moveTo(innerX, innerY);
ctx.lineTo(outerX, outerY);
}
ctx.lineWidth = 2;
ctx.stroke();
ctx.restore();
// Draw label
ctx.fillStyle = litegraph_base.WIDGET_SECONDARY_TEXT_COLOR;
const label = this.label || this.name;
if (label != null) {
ctx.fillText(label, margin * 2 + 5, y + H * 0.7);
}
// Draw value
ctx.textAlign = 'right';
const text = isAuto ? "auto" : this.displayValue();
ctx.fillStyle = isAuto ? litegraph_base.WIDGET_SECONDARY_TEXT_COLOR : litegraph_base.WIDGET_TEXT_COLOR;
ctx.fillText(text, widget_width - autoTextRightMargin - autoTextWidth - 15, y + H * 0.7);
// Draw increment/decrement buttons if not in AUTO mode and not a string widget
if (!isAuto && !this.disabled && this.config[0] !== "FVAUTOSTRING") {
// Draw decrement button (left triangle)
ctx.fillStyle = litegraph_base.WIDGET_TEXT_COLOR;
ctx.beginPath();
ctx.moveTo(margin + 16, y + 5);
ctx.lineTo(margin + 6, y + H * 0.5);
ctx.lineTo(margin + 16, y + H - 5);
ctx.fill();
// Draw increment button (right triangle)
ctx.beginPath();
ctx.moveTo(widget_width - margin - 16, y + 5);
ctx.lineTo(widget_width - margin - 6, y + H * 0.5);
ctx.lineTo(widget_width - margin - 16, y + H - 5);
ctx.fill();
}
}
}
function mouseAutoAnnotated(event, [x, y], node) {
const widget_width = node.size[0];
const margin = 15;
const H = 20; // Widget height
const autoTextWidth = 30;
const autoTextRightMargin = 5;
const cogRadius = 6;
if (this.isAuto) {
if (event.type === "pointerup" || event.type === "mouseup") {
const cogX = widget_width - autoTextRightMargin - autoTextWidth - 6;
const cogLeftEdge = cogX - cogRadius;
const cogRightEdge = cogX + cogRadius;
if (x > cogLeftEdge && x < cogRightEdge) {
this.isAuto = false;
this.value = this.cachedValue !== undefined ? this.cachedValue : (this.options.default || 0);
if (this.callback) {
this.callback(this.value);
}
node.graph.setDirtyCanvas(true, false);
}
}
// Block ALL events in auto mode except cog clicks
event.preventDefault?.();
event.stopPropagation?.();
event.stopImmediatePropagation?.();
return true; // Always return true to indicate event was handled
}
// Determine if clicking on increment/decrement buttons
const delta = this.config[0] === "FVAUTOSTRING" ? 0 :
(x < 40 ? -1 : x > widget_width - 48 ? 1 : 0);
if (event.type === "pointerdown" || event.type === "mousedown") {
// ComfyUI appears to intercept pointerdown events, so this code path is never reached
console.log("pointerdown received (unexpected)");
return false;
} else if (event.type === "pointerup" || event.type === "mouseup") {
// Stop event propagation to prevent double handling
event.preventDefault?.();
event.stopPropagation?.();
event.stopImmediatePropagation?.();
const cogX = widget_width - autoTextRightMargin - autoTextWidth - 6;
const cogLeftEdge = widget_width - autoTextRightMargin - autoTextWidth - 6 - cogRadius;
const cogRightEdge = widget_width - autoTextRightMargin - autoTextWidth - 6 + cogRadius;
if (x > cogLeftEdge && x < cogRightEdge) {
this.isAuto = !this.isAuto;
if (this.isAuto) {
this.cachedValue = this.value;
this.value = -99999;
} else {
this.value = this.cachedValue !== undefined ? this.cachedValue : (this.options.default || 0);
}
if (this.callback) {
this.callback(this.value);
}
node.graph.setDirtyCanvas(true, false);
return true;
}
// If in auto mode and NOT clicking the cog, block all other interactions
if (this.isAuto) {
return true;
}
// Handle increment/decrement buttons if not in auto mode
if (delta !== 0 && !this.isAuto) {
if (this.config[0] === "FVAUTOCOMBO") {
const options = this.options.values || [];
if (options.length === 0) return true;
let currentIndex = -1;
for (let i = 0; i < options.length; i++) {
const optValue = typeof options[i] === 'object' ? options[i].value : options[i];
if (optValue == this.value || String(optValue) === String(this.value)) {
currentIndex = i;
break;
}
}
if (currentIndex === -1) {
currentIndex = 0;
}
let newIndex = currentIndex + delta;
if (newIndex < 0) {
newIndex = options.length - 1;
} else if (newIndex >= options.length) {
newIndex = 0;
}
const newOption = options[newIndex];
this.value = typeof newOption === 'object' ? newOption.value : newOption;
if (this.callback) {
this.callback(this.value);
}
node.graph.setDirtyCanvas(true, false);
return true;
} else {
let v = parseFloat(this.value);
const increment = delta * 0.1 * (this.options.step || 1);
v += increment;
// Apply min/max constraints
if (this.options.min != null) {
v = Math.max(this.options.min, v);
}
if (this.options.max != null) {
v = Math.min(this.options.max, v);
}
// Round to precision or to integer
if (this.config[0] === "FVAUTOINT") {
v = Math.round(v);
} else if (this.options.precision !== undefined) {
const precision = Math.pow(10, this.options.precision);
v = Math.round(v * precision) / precision;
}
this.value = v;
if (this.callback) {
this.callback(this.value);
}
node.graph.setDirtyCanvas(true, false);
return true;
}
}
if (delta === 0 && !this.isAuto) {
if (this.config[0] === "FVAUTOCOMBO") {
const options = this.options.values || [];
// Create menu items
const menuItems = options.map(opt => {
const value = typeof opt === 'object' ? opt.value : opt;
const label = typeof opt === 'object' ? opt.label : opt.toString();
return {
content: label,
callback: () => {
this.value = value;
if (this.callback) {
this.callback(this.value);
}
node.graph.setDirtyCanvas(true, false);
}
};
});
new LiteGraph.ContextMenu(menuItems, {
event: event,
title: null,
callback: null,
extra: node
});
return true;
} else if (this.config[0] === "FVAUTOSTRING") {
const d_callback = (v) => {
this.value = v;
if (this.callback) {
this.callback(this.value);
}
node.graph.setDirtyCanvas(true, false);
};
const dialog = app.canvas.prompt(
'Value',
this.value,
d_callback,
event
);
return true;
} else {
// For numeric widgets, show input dialog
const d_callback = (v) => {
this.value = this.parseValue?.(v) ?? Number(v);
// Apply min/max constraints
if (this.options.min != null) {
this.value = Math.max(this.options.min, this.value);
}
if (this.options.max != null) {
this.value = Math.min(this.options.max, this.value);
}
// Round to precision or to integer
if (this.config[0] === "FVAUTOINT") {
this.value = Math.round(this.value);
} else if (this.options.precision !== undefined) {
const precision = Math.pow(10, this.options.precision);
this.value = Math.round(this.value * precision) / precision;
}
if (this.callback) {
this.callback(this.value);
}
node.graph.setDirtyCanvas(true, false);
};
const dialog = app.canvas.prompt(
'Value',
this.value,
d_callback,
event
);
return true;
}
}
return true;
}
return false;
}
function makeAutoAnnotated(widget, inputData) {
const original = {
callback: widget.callback,
type: widget.type,
value: widget.value
};
// Add AUTO properties to the widget
Object.assign(widget, {
type: "BOOLEAN",
draw: drawAutoAnnotated,
mouse: mouseAutoAnnotated,
onMouse: null, // Explicitly disable original onMouse handler
isAuto: true,
cachedValue: widget.value,
config: inputData,
options: Object.assign({}, inputData[1], widget.options),
original: original, // Store original properties for reference
// Disable other potential mouse handlers with no-op functions
onClick: function () {
return false;
},
onPointerUp: function () {
return false;
},
onPointerDown: function () {
return false;
},
onMouseUp: function () {
return false;
},
onMouseDown: function () {
return false;
},
computeSize(width) {
return [width, 20];
},
displayValue: function () {
if (this.config[0] === "FVAUTOINT") {
return Math.round(this.value).toString();
}
if (this.config[0] === "FVAUTOCOMBO") {
return this.value;
}
if (this.config[0] === "FVAUTOSTRING") {
return this.value;
}
// For FLOAT values, check if it's actually an integer
if (Number.isInteger(this.value)) {
return this.value.toString();
}
return this.value.toFixed(this.options.precision || 2);
},
parseValue: function (v) {
if (this.config[0] === "FVAUTOSTRING") {
return v;
}
if (typeof v === "string") {
return parseFloat(v);
}
return v;
},
serializeValue: function () {
// Return special value for AUTO mode
return this.isAuto ? -99999 : this.value;
},
deserializeValue: function (data) {
if (data === -99999) {
this.isAuto = true;
this.value = -99999;
} else {
this.isAuto = false;
this.value = data;
this.cachedValue = data;
}
}
});
// Override callback to handle AUTO mode
widget.callback = function (v) {
if (this.isAuto) {
return; // Don't call the original callback in AUTO mode
}
const result = original.callback?.call(this, v);
return result;
};
// Override any potential click handlers
const originalOnClick = widget.onClick;
if (originalOnClick) {
widget.onClick = function (...args) {
if (this.isAuto) {
return false;
}
return originalOnClick.call(this, ...args);
};
}
return widget;
}
app.registerExtension({
name: "FastVideo.AutoWidgets",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData?.name == "VideoGenerator" || nodeData?.name === "InferenceArgs" || nodeData?.name === "VAEConfig" ||
nodeData?.name === "TextEncoderConfig" || nodeData?.name === "DITConfig") {
// Add serialization support
chainCallback(nodeType.prototype, "onSerialize", function (info) {
if (!this.widgets) {
return;
}
// Ensure widgets_values exists
if (!info.widgets_values) {
info.widgets_values = {};
}
// Store AUTO widget states in a separate property
if (!info.auto_widget_states) {
info.auto_widget_states = {};
}
// Handle AUTO widgets specially
for (const w of this.widgets) {
if (w.type === "BOOLEAN" && w.isAuto !== undefined) {
// Store the serialized value (for Python node)
info.widgets_values[w.name] = w.serializeValue();
// Store the full state (for UI restoration)
info.auto_widget_states[w.name] = {
isAuto: w.isAuto,
value: w.value,
cachedValue: w.cachedValue
};
}
}
});
// Add deserialization support
chainCallback(nodeType.prototype, "onConfigure", function (info) {
if (!this.widgets) {
return;
}
// First, restore from widgets_values (for backward compatibility)
if (info.widgets_values && Array.isArray(info.widgets_values)) {
for (let i = 0; i < this.widgets.length && i < info.widgets_values.length; i++) {
const w = this.widgets[i];
const value = info.widgets_values[i];
if (w.type === "BOOLEAN" && w.isAuto !== undefined) {
w.deserializeValue(value);
}
}
}
// Then, restore full state if available
if (info.auto_widget_states) {
for (const w of this.widgets) {
if (w.type === "BOOLEAN" && w.isAuto !== undefined && w.name in info.auto_widget_states) {
const state = info.auto_widget_states[w.name];
w.isAuto = state.isAuto;
w.cachedValue = state.cachedValue;
w.value = state.isAuto ? -99999 : state.value;
w.callback?.(w.value);
}
}
}
// Force a redraw
this.graph?.setDirtyCanvas(true, true);
});
// Override addInput to handle AUTO widgets
chainCallback(nodeType.prototype, "onNodeCreated", function () {
// Convert any existing widgets to AUTO widgets if needed
let new_widgets = [];
const intWidgetNames = ["sp_size", "tp_size", "height", "width", "num_frames", "num_inference_steps", "flow_shift", "seed", "fps", "scale_factor",
"tile_sample_min_height", "tile_sample_min_width", "tile_sample_min_num_frames", "tile_sample_stride_height", "tile_sample_stride_width",
"tile_sample_stride_num_frames", "blend_num_frames"
]
const floatWidgetNames = ["embedded_cfg_scale", "guidance_scale"]
const comboWidgetNames = ["vae_tiling", "vae_precision", "vae_sp", "text_encoder_precision", "precision",
"load_encoder", "load_decoder", "use_tiling", "use_temporal_tiling", "use_parallel_tiling", "use_cpu_offload", "enable_teacache"
]
const stringWidgetNames = ["prefix", "quant_config", "lora_config", "image_path"]
if (this.widgets) {
for (let w of this.widgets) {
if (intWidgetNames.includes(w.name)) {
new_widgets.push(makeAutoAnnotated(w, ["FVAUTOINT", { "default": 0 }]));
} else if (floatWidgetNames.includes(w.name)) {
new_widgets.push(makeAutoAnnotated(w, ["FVAUTOFLOAT", { "default": 0 }]));
} else if (comboWidgetNames.includes(w.name)) {
new_widgets.push(makeAutoAnnotated(w, ["FVAUTOCOMBO", { "default": 0 }]));
} else if (stringWidgetNames.includes(w.name)) {
new_widgets.push(makeAutoAnnotated(w, ["FVAUTOSTRING", { "default": "" }]));
} else {
new_widgets.push(w);
}
}
this.widgets = new_widgets;
const autoWidgets = this.widgets.filter(w => w.type === "BOOLEAN" && w.isAuto !== undefined);
}
this.graph?.setDirtyCanvas(true, true);
});
}
},
async init() {
// Force a redraw of all nodes when the extension initializes
if (app.graph) {
setTimeout(() => {
app.graph.setDirtyCanvas(true, true);
}, 1000);
}
}
});
console.log("FastVideo.core.js loaded");
+2 -8
View File
@@ -4,7 +4,7 @@
## Installation ## Installation
We test our code on Pytorch 2.5.0 and CUDA>=12.4. Currently we only support H100/H200, because ThunderKittens uses TMA but doesn't support Blackwell yet. We test our code on Pytorch 2.5.0 and CUDA>=12.4. Currently we only have implementation on H100.
First, install C++20 for ThunderKittens: First, install C++20 for ThunderKittens:
```bash ```bash
sudo apt update sudo apt update
@@ -53,14 +53,8 @@ out = sliding_tile_attention(q, k, v, window_size, 0, False)
## Test ## Test
```bash ```bash
python tests/test_sta.py # test STA python test/test_sta.py
python tests/test_block_sparse.py # test VSA
``` ```
## Benchmark
```bash
python benchmarks/bench_sta.py
```
## How Does STA Work? ## How Does STA Work?
We give a demo for 2D STA with window size (6,6) operating on a (10, 10) image. We give a demo for 2D STA with window size (6,6) operating on a (10, 10) image.
@@ -5,7 +5,6 @@ import matplotlib.pyplot as plt
import numpy as np import numpy as np
import torch import torch
from st_attn import sliding_tile_attention from st_attn import sliding_tile_attention
from triton.testing import do_bench
def flops(batch, seqlen, nheads, headdim, causal, mode="fwd"): def flops(batch, seqlen, nheads, headdim, causal, mode="fwd"):
@@ -14,16 +13,16 @@ def flops(batch, seqlen, nheads, headdim, causal, mode="fwd"):
return f if mode == "fwd" else (2.5 * f if mode == "bwd" else 3.5 * f) return f if mode == "fwd" else (2.5 * f if mode == "bwd" else 3.5 * f)
def compute_TFLOPS(flops, ms): def efficiency(flop, time):
flops = flops / 1e12 flop = flop / 1e12
ms = ms / 1e3 time = time / 1e6
return flops / ms return flop / time
def benchmark_attention(configurations): def benchmark_attention(configurations):
results = {'fwd': defaultdict(list), 'bwd': defaultdict(list)} results = {'fwd': defaultdict(list), 'bwd': defaultdict(list)}
for B, H, N, D, causal, dit_seq_shape, window_size in configurations: for B, H, N, D, causal in configurations:
print("=" * 60) print("=" * 60)
print(f"Timing forward and backward pass for B={B}, H={H}, N={N}, D={D}, causal={causal}") print(f"Timing forward and backward pass for B={B}, H={H}, N={N}, D={D}, causal={causal}")
@@ -31,31 +30,38 @@ def benchmark_attention(configurations):
k = torch.randn(B, H, N, D, dtype=torch.bfloat16, device='cuda', requires_grad=False).contiguous() k = torch.randn(B, H, N, D, dtype=torch.bfloat16, device='cuda', requires_grad=False).contiguous()
v = torch.randn(B, H, N, D, dtype=torch.bfloat16, device='cuda', requires_grad=False).contiguous() v = torch.randn(B, H, N, D, dtype=torch.bfloat16, device='cuda', requires_grad=False).contiguous()
# grad_output = torch.randn_like(q, requires_grad=False).contiguous() grad_output = torch.randn_like(q, requires_grad=False).contiguous()
# qg = torch.zeros_like(q, requires_grad=False, dtype=torch.float).contiguous()
# kg = torch.zeros_like(k, requires_grad=False, dtype=torch.float).contiguous()
# vg = torch.zeros_like(v, requires_grad=False, dtype=torch.float).contiguous()
qg = torch.zeros_like(q, requires_grad=False, dtype=torch.float).contiguous()
kg = torch.zeros_like(k, requires_grad=False, dtype=torch.float).contiguous()
vg = torch.zeros_like(v, requires_grad=False, dtype=torch.float).contiguous()
# # Warmup for forward pass # Prepare for timing forward pass
# for _ in range(10): start_events_fwd = [torch.cuda.Event(enable_timing=True) for _ in range(10)]
# o = sliding_tile_attention(q, k, v, [[3, 6, 10]] * 24, 0, False, dit_seq_shape) end_events_fwd = [torch.cuda.Event(enable_timing=True) for _ in range(10)]
# # Time the forward pass torch.cuda.empty_cache()
# for i in range(10): torch.cuda.synchronize()
# start_events_fwd[i].record()
# o = sliding_tile_attention(q, k, v, [[3, 6, 10]] * 24, 0, False, dit_seq_shape)
# end_events_fwd[i].record()
ms = do_bench(lambda: sliding_tile_attention(q, k, v, [window_size] * 24, 0, False, dit_seq_shape))
# times_fwd = [s.elapsed_time(e) for s, e in zip(start_events_fwd, end_events_fwd)] # Warmup for forward pass
# time_us_fwd = np.mean(times_fwd) * 1000 for _ in range(10):
o = sliding_tile_attention(q, k, v, [[3, 6, 10]] * 24, 0, False, '18x48x80')
tflops_fwd = compute_TFLOPS(flops(B, N, H, D, causal, 'fwd'), ms) # Time the forward pass
for i in range(10):
start_events_fwd[i].record()
o = sliding_tile_attention(q, k, v, [[3, 6, 10]] * 24, 0, False, '18x48x80')
end_events_fwd[i].record()
torch.cuda.synchronize()
times_fwd = [s.elapsed_time(e) for s, e in zip(start_events_fwd, end_events_fwd)]
time_us_fwd = np.mean(times_fwd) * 1000
tflops_fwd = efficiency(flops(B, N, H, D, causal, 'fwd'), time_us_fwd)
results['fwd'][(D, causal)].append((N, tflops_fwd)) results['fwd'][(D, causal)].append((N, tflops_fwd))
print(f"Average time for forward pass (ms): {ms:.2f}") print(f"Average time for forward pass in us: {time_us_fwd:.2f}")
print(f"Average TFLOPS: {tflops_fwd}") print(f"Average efficiency for forward pass in TFLOPS: {tflops_fwd}")
print("-" * 60) print("-" * 60)
# torch.cuda.empty_cache() # torch.cuda.empty_cache()
@@ -79,14 +85,15 @@ def benchmark_attention(configurations):
# times_bwd = [s.elapsed_time(e) for s, e in zip(start_events_bwd, end_events_bwd)] # times_bwd = [s.elapsed_time(e) for s, e in zip(start_events_bwd, end_events_bwd)]
# time_us_bwd = np.mean(times_bwd) * 1000 # time_us_bwd = np.mean(times_bwd) * 1000
# tflops_bwd = compute_TFLOPS(flops(B, N, H, D, causal, 'bwd'), ms) # tflops_bwd = efficiency(flops(B, N, H, D, causal, 'bwd'), time_us_bwd)
# results['bwd'][(D, causal)].append((N, tflops_bwd)) # results['bwd'][(D, causal)].append((N, tflops_bwd))
# print(f"Average time for backward pass(ms): {ms:.2f}") # print(f"Average time for backward pass in us: {time_us_bwd:.2f}")
# print(f"Average TFLOPS: {tflops_bwd}") # print(f"Average efficiency for backward pass in TFLOPS: {tflops_bwd}")
# print("=" * 60) print("=" * 60)
torch.cuda.empty_cache() torch.cuda.empty_cache()
torch.cuda.synchronize()
return results return results
@@ -117,10 +124,7 @@ def plot_results(results):
# Example list of configurations to test # Example list of configurations to test
configurations = [ configurations = [
(2, 24, 69120, 128, False, '18x48x80', [3, 6, 10]), (2, 24, 69120, 128, False),
(2, 24, 69120, 128, True, '18x48x80', [3, 6, 10]),
(2, 24, 82944, 128, False, '36x48x48', [3, 3, 6]), # Stepvideo
(2, 24, 82944, 128, True, '36x48x48', [3, 3, 6]),
# (16, 16, 768*16, 128, False), # (16, 16, 768*16, 128, False),
# (16, 16, 768*2, 128, False), # (16, 16, 768*2, 128, False),
# (16, 16, 768*4, 128, False), # (16, 16, 768*4, 128, False),
+22 -31
View File
@@ -4,17 +4,9 @@
#include <cooperative_groups.h> #include <cooperative_groups.h>
#include <iostream> #include <iostream>
#include <stdio.h> #include <stdio.h>
#include <c10/cuda/CUDAGuard.h>
// #define CLAMP(value, min, max) ((value) < (min) ? (min) : ((value) > (max) ? (max) : (value)))
__device__ __forceinline__ int clamp_int(int value, int min, int max) {
return (value < min) ? min : ((value > max) ? max : value);
}
// #define ABS(x) ((x) < 0 ? -(x) : (x))
__device__ __forceinline__ int abs_int(int value) {
return (value < 0) ? -value : value;
}
#define CLAMP(value, min, max) ((value) < (min) ? (min) : ((value) > (max) ? (max) : (value)))
#define ABS(x) ((x) < 0 ? -(x) : (x))
constexpr int CONSUMER_WARPGROUPS = (3); constexpr int CONSUMER_WARPGROUPS = (3);
constexpr int PRODUCER_WARPGROUPS = (1); constexpr int PRODUCER_WARPGROUPS = (1);
@@ -125,16 +117,16 @@ void fwd_attend_ker(const __grid_constant__ fwd_globals<D> g) {
int qt = seq_idx / 6 / (CH * CW); int qt = seq_idx / 6 / (CH * CW);
int qh = (seq_idx / 6) % (CH * CW) / CW; int qh = (seq_idx / 6) % (CH * CW) / CW;
int qw = (seq_idx / 6) % CW; int qw = (seq_idx / 6) % CW;
qt = clamp_int(qt, DT, CT-DT-1); qt = CLAMP(qt, DT, CT-DT-1);
qh = clamp_int(qh, DH, CH-DH-1); qh = CLAMP(qh, DH, CH-DH-1);
qw = clamp_int(qw, DW, CW-DW-1); qw = CLAMP(qw, DW, CW-DW-1);
int count = 0; int count = 0;
int j = 0; int j = 0;
while (count < K::stages - 1) { while (count < K::stages - 1) {
int kt = j / 3 / (CH * CW); int kt = j / 3 / (CH * CW);
int kh = (j / 3) % (CH * CW) / CW; int kh = (j / 3) % (CH * CW) / CW;
int kw = (j / 3) % CW; int kw = (j / 3) % CW;
bool mask = (abs_int(qt - kt) <= DT) && (abs_int(qh - kh) <= DH) && (abs_int(qw - kw) <= DW); bool mask = (ABS(qt - kt) <= DT) && (ABS(qh - kh) <= DH) && (ABS(qw - kw) <= DW);
if (mask){ if (mask){
coord<k_tile> kv_tile_idx = {blockIdx.z, kv_head_idx, j, 0}; coord<k_tile> kv_tile_idx = {blockIdx.z, kv_head_idx, j, 0};
tma::expect_bytes(k_smem_arrived[count], sizeof(k_tile)); tma::expect_bytes(k_smem_arrived[count], sizeof(k_tile));
@@ -175,15 +167,15 @@ void fwd_attend_ker(const __grid_constant__ fwd_globals<D> g) {
int qt = seq_idx / 6 / (CH * CW); int qt = seq_idx / 6 / (CH * CW);
int qh = (seq_idx / 6) % (CH * CW) / CW; int qh = (seq_idx / 6) % (CH * CW) / CW;
int qw = (seq_idx / 6) % CW; int qw = (seq_idx / 6) % CW;
qt = clamp_int(qt, DT, CT-DT-1); qt = CLAMP(qt, DT, CT-DT-1);
qh = clamp_int(qh, DH, CH-DH-1); qh = CLAMP(qh, DH, CH-DH-1);
qw = clamp_int(qw, DW, CW-DW-1); qw = CLAMP(qw, DW, CW-DW-1);
int k_t_min = clamp_int(qt-DT, 0, CT-1); int k_t_min = CLAMP(qt-DT, 0, CT-1);
int k_t_max = clamp_int(qt+DT, 0, CT-1); int k_t_max = CLAMP(qt+DT, 0, CT-1);
int k_h_min = clamp_int(qh-DH, 0, CH-1); int k_h_min = CLAMP(qh-DH, 0, CH-1);
int k_h_max = clamp_int(qh+DH, 0, CH-1); int k_h_max = CLAMP(qh+DH, 0, CH-1);
int k_w_min = clamp_int(qw-DW, 0, CW-1); int k_w_min = CLAMP(qw-DW, 0, CW-1);
int k_w_max = clamp_int(qw+DW, 0, CW-1); int k_w_max = CLAMP(qw+DW, 0, CW-1);
int count = 0; int count = 0;
for (int kt = k_t_min; kt <= k_t_max; kt++) { for (int kt = k_t_min; kt <= k_t_max; kt++) {
for (int kh = k_h_min; kh <= k_h_max; kh++) { for (int kh = k_h_min; kh <= k_h_max; kh++) {
@@ -242,7 +234,7 @@ void fwd_attend_ker(const __grid_constant__ fwd_globals<D> g) {
// the last three kv blocks are for text, we process them separately // the last three kv blocks are for text, we process them separately
kv_iters = img_kv_blocks - 1; kv_iters = img_kv_blocks - 1;
} else { } else {
kv_iters = clamp_int(DT*2+1, 1, CT) * clamp_int(DH*2+1, 1, CH) * clamp_int(DW*2+1, 1, CW) * 3 - 1 ; kv_iters = CLAMP(DT*2+1, 1, CT) * CLAMP(DH*2+1, 1, CH) * CLAMP(DW*2+1, 1, CW) * 3 - 1 ;
} }
kittens::wait(qsmem_semaphore, 0); kittens::wait(qsmem_semaphore, 0);
@@ -423,9 +415,8 @@ sta_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v, torch::Tensor o,
float* l_ptr = reinterpret_cast<float*>(l_vec.data_ptr<float>()); float* l_ptr = reinterpret_cast<float*>(l_vec.data_ptr<float>());
float* d_l = reinterpret_cast<float*>(l_ptr); float* d_l = reinterpret_cast<float*>(l_ptr);
//cudadevicesynchronize(); cudaDeviceSynchronize();
const c10::cuda::OptionalCUDAGuard device_guard(q.device()); auto stream = at::cuda::getCurrentCUDAStream().stream();
const cudaStream_t stream = at::cuda::getCurrentCUDAStream().stream();
if (head_dim == 128) { if (head_dim == 128) {
@@ -451,8 +442,8 @@ sta_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v, torch::Tensor o,
globals g{qg_arg, kg_arg, vg_arg, lg_arg, og_arg, static_cast<int>(seq_len), static_cast<int>(text_length), static_cast<int>(hr)}; globals g{qg_arg, kg_arg, vg_arg, lg_arg, og_arg, static_cast<int>(seq_len), static_cast<int>(text_length), static_cast<int>(hr)};
constexpr int mem_size = kittens::MAX_SHARED_MEMORY; auto mem_size = kittens::MAX_SHARED_MEMORY;
int threads = NUM_WORKERS * kittens::WARP_THREADS; auto threads = NUM_WORKERS * kittens::WARP_THREADS;
if (has_text) { if (has_text) {
// TORCH_CHECK(seq_len % (CONSUMER_WARPGROUPS*kittens::TILE_DIM*4) == 0, "sequence length must be divisible by 192"); // TORCH_CHECK(seq_len % (CONSUMER_WARPGROUPS*kittens::TILE_DIM*4) == 0, "sequence length must be divisible by 192");
dim3 grid_image(seq_len/(CONSUMER_WARPGROUPS*kittens::TILE_ROW_DIM<bf16>*4)-2, qo_heads, batch); dim3 grid_image(seq_len/(CONSUMER_WARPGROUPS*kittens::TILE_ROW_DIM<bf16>*4)-2, qo_heads, batch);
@@ -832,10 +823,10 @@ sta_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v, torch::Tensor o,
} }
CHECK_CUDA_ERROR(cudaGetLastError()); CHECK_CUDA_ERROR(cudaGetLastError());
// cudaStreamSynchronize(stream); cudaStreamSynchronize(stream);
} }
return o; return o;
//cudadevicesynchronize(); cudaDeviceSynchronize();
} }
@@ -7,7 +7,6 @@ from vsa import BLOCK_M, BLOCK_N
import numpy as np import numpy as np
import random import random
import gc
def set_seed(seed: int = 42): def set_seed(seed: int = 42):
# Python random module # Python random module
@@ -21,6 +20,15 @@ def set_seed(seed: int = 42):
torch.cuda.manual_seed(seed) torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed) # if using multi-GPU torch.cuda.manual_seed_all(seed) # if using multi-GPU
def parse_arguments():
parser = argparse.ArgumentParser(description='Benchmark Block Sparse Attention')
parser.add_argument('--batch_size', type=int, default=4, help='Batch size')
parser.add_argument('--num_heads', type=int, default=6, help='Number of heads')
parser.add_argument('--head_dim', type=int, default=128, help='Head dimension')
parser.add_argument('--topk', type=int, default=64, help='Number of kv blocks each q block attends to')
parser.add_argument('--seq_lengths', type=int, nargs='+', default=[29120], help='Sequence lengths to benchmark')
parser.add_argument('--num_iterations', type=int, default=100, help='Number of test iterations to run')
return parser.parse_args()
@torch.no_grad @torch.no_grad
def precision_metric(quant_o, fa2_o): def precision_metric(quant_o, fa2_o):
@@ -127,7 +135,9 @@ def generate_block_sparse_pattern(bs, h, num_q_blocks, num_kv_blocks, k, device=
return q2k_block_sparse_index, q2k_block_sparse_num, k2q_block_sparse_index, k2q_block_sparse_num, block_sparse_mask return q2k_block_sparse_index, q2k_block_sparse_num, k2q_block_sparse_index, k2q_block_sparse_num, block_sparse_mask
def main(args): def main():
args = parse_arguments()
set_seed(42) set_seed(42)
# Extract parameters # Extract parameters
@@ -181,36 +191,23 @@ def main(args):
block_mask_expanded = block_sparse_mask.unsqueeze(-1).unsqueeze(-2) # [b, h, num_q_blocks, num_kv_blocks, 1, 1] block_mask_expanded = block_sparse_mask.unsqueeze(-1).unsqueeze(-2) # [b, h, num_q_blocks, num_kv_blocks, 1, 1]
block_mask_expanded = block_mask_expanded.expand(-1, -1, -1, -1, BLOCK_M, BLOCK_N) # [b, h, num_q_blocks, num_kv_blocks, BLOCK_M, BLOCK_N] block_mask_expanded = block_mask_expanded.expand(-1, -1, -1, -1, BLOCK_M, BLOCK_N) # [b, h, num_q_blocks, num_kv_blocks, BLOCK_M, BLOCK_N]
full_mask = block_mask_expanded.permute(0, 1, 2, 4, 3, 5).reshape(batch, head, seq_len, seq_len) full_mask = block_mask_expanded.permute(0, 1, 2, 4, 3, 5).reshape(batch, head, seq_len, seq_len)
q_sdpa = q.clone()
k_sdpa = k.clone()
v_sdpa = v.clone()
q.requires_grad = True q.requires_grad = True
k.requires_grad = True k.requires_grad = True
v.requires_grad = True v.requires_grad = True
# testing forward
o = BlockSparseAttentionFunction.apply(q, k, v, q2k_block_sparse_index, q2k_block_sparse_num, k2q_block_sparse_index, k2q_block_sparse_num)
del q2k_block_sparse_index, q2k_block_sparse_num, k2q_block_sparse_index, k2q_block_sparse_num, block_sparse_mask, block_mask_expanded
grad_o = torch.randn_like(o)
o.backward(grad_o)
# clear memory
q_sdpa = q.detach().clone()
k_sdpa = k.detach().clone()
v_sdpa = v.detach().clone()
q_sdpa.requires_grad = True q_sdpa.requires_grad = True
k_sdpa.requires_grad = True k_sdpa.requires_grad = True
v_sdpa.requires_grad = True v_sdpa.requires_grad = True
q.data = torch.empty(0, device=q.device)
k.data = torch.empty(0, device=k.device)
v.data = torch.empty(0, device=v.device)
torch.cuda.empty_cache()
# testing forward
o = BlockSparseAttentionFunction.apply(q, k, v, q2k_block_sparse_index, q2k_block_sparse_num, k2q_block_sparse_index, k2q_block_sparse_num)
o_sdpa = torch.nn.functional.scaled_dot_product_attention(q_sdpa, k_sdpa, v_sdpa, attn_mask=full_mask) o_sdpa = torch.nn.functional.scaled_dot_product_attention(q_sdpa, k_sdpa, v_sdpa, attn_mask=full_mask)
sim, l1, rmse = precision_metric(o, o_sdpa) sim, l1, rmse = precision_metric(o, o_sdpa)
assert sim > 0.9999, f"SSIM too low: {sim}"
assert l1 < 8e-5, f"l1 too large: {l1}"
assert rmse < 2e-5, f"RMSE too large: {rmse}"
forward_metrics['sim'].append(sim) forward_metrics['sim'].append(sim)
forward_metrics['l1'].append(l1) forward_metrics['l1'].append(l1)
forward_metrics['rmse'].append(rmse) forward_metrics['rmse'].append(rmse)
@@ -218,72 +215,52 @@ def main(args):
print(f"block_sparse_attention_fwd vs torch.nn.functional.scaled_dot_product_attention:\nsim: {sim}, l1: {l1}, rmse: {rmse}") print(f"block_sparse_attention_fwd vs torch.nn.functional.scaled_dot_product_attention:\nsim: {sim}, l1: {l1}, rmse: {rmse}")
# test backward # test backward
grad_o = torch.randn_like(o)
o.backward(grad_o)
o_sdpa.backward(grad_o) o_sdpa.backward(grad_o)
sim, l1, rmse = precision_metric(q.grad, q_sdpa.grad) sim, l1, rmse = precision_metric(q.grad, q_sdpa.grad)
# Error bounds collected on H100
assert sim > 0.9999, f"SSIM too low: {sim}"
assert l1 < 4e-3, f"l1 too large: {l1}"
assert rmse < 3e-4, f"RMSE too large: {rmse}"
grad_q_metrics['sim'].append(sim) grad_q_metrics['sim'].append(sim)
grad_q_metrics['l1'].append(l1) grad_q_metrics['l1'].append(l1)
grad_q_metrics['rmse'].append(rmse) grad_q_metrics['rmse'].append(rmse)
print(f"block_sparse_attention_bwd vs torch.nn.functional.scaled_dot_product_attention grad_q:\nsim: {sim}, l1: {l1}, rmse: {rmse}") print(f"block_sparse_attention_bwd vs torch.nn.functional.scaled_dot_product_attention grad_q:\nsim: {sim}, l1: {l1}, rmse: {rmse}")
sim, l1, rmse = precision_metric(k.grad, k_sdpa.grad) sim, l1, rmse = precision_metric(k.grad, k_sdpa.grad)
assert sim > 0.9999, f"SSIM too low: {sim}"
assert l1 < 4e-3, f"l1 too large: {l1}"
assert rmse < 2e-4, f"RMSE too large: {rmse}"
grad_k_metrics['sim'].append(sim) grad_k_metrics['sim'].append(sim)
grad_k_metrics['l1'].append(l1) grad_k_metrics['l1'].append(l1)
grad_k_metrics['rmse'].append(rmse) grad_k_metrics['rmse'].append(rmse)
print(f"block_sparse_attention_bwd vs torch.nn.functional.scaled_dot_product_attention grad_k:\nsim: {sim}, l1: {l1}, rmse: {rmse}") print(f"block_sparse_attention_bwd vs torch.nn.functional.scaled_dot_product_attention grad_k:\nsim: {sim}, l1: {l1}, rmse: {rmse}")
sim, l1, rmse = precision_metric(v.grad, v_sdpa.grad) sim, l1, rmse = precision_metric(v.grad, v_sdpa.grad)
assert sim > 0.9999, f"SSIM too low: {sim}"
assert l1 < 1e-4, f"l1 too large: {l1}"
assert rmse < 2e-5, f"RMSE too large: {rmse}"
grad_v_metrics['sim'].append(sim) grad_v_metrics['sim'].append(sim)
grad_v_metrics['l1'].append(l1) grad_v_metrics['l1'].append(l1)
grad_v_metrics['rmse'].append(rmse) grad_v_metrics['rmse'].append(rmse)
print(f"block_sparse_attention_bwd vs torch.nn.functional.scaled_dot_product_attention grad_v:\nsim: {sim}, l1: {l1}, rmse: {rmse}") print(f"block_sparse_attention_bwd vs torch.nn.functional.scaled_dot_product_attention grad_v:\nsim: {sim}, l1: {l1}, rmse: {rmse}")
del o, o_sdpa, grad_o, q_sdpa, k_sdpa, v_sdpa
gc.collect()
torch.cuda.empty_cache()
# Print summary statistics if multiple iterations were run # Print summary statistics if multiple iterations were run
if num_iterations > 1: if num_iterations > 1:
print("\n" + "="*50) print("\n" + "="*50)
print(f"Summary Statistics (over {num_iterations} iterations):") print(f"Summary Statistics (over {num_iterations} iterations):")
print("\nForward metrics:") print("\nForward metrics:")
print(f"Similarity: mean={np.mean(forward_metrics['sim']):.6f}, std={np.std(forward_metrics['sim']):.6f}, min={np.min(forward_metrics['sim']):.6f}") print(f"Similarity: mean={np.mean(forward_metrics['sim']):.6f}, std={np.std(forward_metrics['sim']):.6f}")
print(f"L1 error: mean={np.mean(forward_metrics['l1']):.6f}, std={np.std(forward_metrics['l1']):.6f}, max={np.max(forward_metrics['l1']):.6f}") print(f"L1 error: mean={np.mean(forward_metrics['l1']):.6f}, std={np.std(forward_metrics['l1']):.6f}")
print(f"RMSE: mean={np.mean(forward_metrics['rmse']):.6f}, std={np.std(forward_metrics['rmse']):.6f}, max={np.max(forward_metrics['rmse']):.6f}") print(f"RMSE: mean={np.mean(forward_metrics['rmse']):.6f}, std={np.std(forward_metrics['rmse']):.6f}")
print("\nGradient Q metrics:") print("\nGradient Q metrics:")
print(f"Similarity: mean={np.mean(grad_q_metrics['sim']):.6f}, std={np.std(grad_q_metrics['sim']):.6f}, min={np.min(grad_q_metrics['sim']):.6f}") print(f"Similarity: mean={np.mean(grad_q_metrics['sim']):.6f}, std={np.std(grad_q_metrics['sim']):.6f}")
print(f"L1 error: mean={np.mean(grad_q_metrics['l1']):.6f}, std={np.std(grad_q_metrics['l1']):.6f}, max={np.max(grad_q_metrics['l1']):.6f}") print(f"L1 error: mean={np.mean(grad_q_metrics['l1']):.6f}, std={np.std(grad_q_metrics['l1']):.6f}")
print(f"RMSE: mean={np.mean(grad_q_metrics['rmse']):.6f}, std={np.std(grad_q_metrics['rmse']):.6f}, max={np.max(grad_q_metrics['rmse']):.6f}") print(f"RMSE: mean={np.mean(grad_q_metrics['rmse']):.6f}, std={np.std(grad_q_metrics['rmse']):.6f}")
print("\nGradient K metrics:") print("\nGradient K metrics:")
print(f"Similarity: mean={np.mean(grad_k_metrics['sim']):.6f}, std={np.std(grad_k_metrics['sim']):.6f}, min={np.min(grad_k_metrics['sim']):.6f}") print(f"Similarity: mean={np.mean(grad_k_metrics['sim']):.6f}, std={np.std(grad_k_metrics['sim']):.6f}")
print(f"L1 error: mean={np.mean(grad_k_metrics['l1']):.6f}, std={np.std(grad_k_metrics['l1']):.6f}, max={np.max(grad_k_metrics['l1']):.6f}") print(f"L1 error: mean={np.mean(grad_k_metrics['l1']):.6f}, std={np.std(grad_k_metrics['l1']):.6f}")
print(f"RMSE: mean={np.mean(grad_k_metrics['rmse']):.6f}, std={np.std(grad_k_metrics['rmse']):.6f}, max={np.max(grad_k_metrics['rmse']):.6f}") print(f"RMSE: mean={np.mean(grad_k_metrics['rmse']):.6f}, std={np.std(grad_k_metrics['rmse']):.6f}")
print("\nGradient V metrics:") print("\nGradient V metrics:")
print(f"Similarity: mean={np.mean(grad_v_metrics['sim']):.6f}, std={np.std(grad_v_metrics['sim']):.6f}, min={np.min(grad_v_metrics['sim']):.6f}") print(f"Similarity: mean={np.mean(grad_v_metrics['sim']):.6f}, std={np.std(grad_v_metrics['sim']):.6f}")
print(f"L1 error: mean={np.mean(grad_v_metrics['l1']):.6f}, std={np.std(grad_v_metrics['l1']):.6f}, max={np.max(grad_v_metrics['l1']):.6f}") print(f"L1 error: mean={np.mean(grad_v_metrics['l1']):.6f}, std={np.std(grad_v_metrics['l1']):.6f}")
print(f"RMSE: mean={np.mean(grad_v_metrics['rmse']):.6f}, std={np.std(grad_v_metrics['rmse']):.6f}, max={np.max(grad_v_metrics['rmse']):.6f}") print(f"RMSE: mean={np.mean(grad_v_metrics['rmse']):.6f}, std={np.std(grad_v_metrics['rmse']):.6f}")
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Benchmark Block Sparse Attention') main()
parser.add_argument('--batch_size', type=int, default=2, help='Batch size')
parser.add_argument('--num_heads', type=int, default=12, help='Number of heads')
parser.add_argument('--head_dim', type=int, default=64, help='Head dimension')
parser.add_argument('--topk', type=int, default=32, help='Number of kv blocks each q block attends to')
parser.add_argument('--seq_lengths', type=int, nargs='+', default=[29120], help='Sequence lengths to benchmark')
parser.add_argument('--num_iterations', type=int, default=100, help='Number of test iterations to run')
args = parser.parse_args()
main(args)
@@ -81,7 +81,5 @@ std = 10
# Run correctness check directly # Run correctness check directly
results = check_correctness(b, h, n, d, causal, mean, std, error_mode='output') results = check_correctness(b, h, n, d, causal, mean, std, error_mode='output')
assert results['TK vs FLEX']['avg_diff'] < 3e-6, f"Average difference: {results['TK vs FLEX']['avg_diff']} is too large"
assert results['TK vs FLEX']['max_diff'] < 4e-2, f"Maximum difference: {results['TK vs FLEX']['max_diff']} is too large"
print(f"Average difference: {results['TK vs FLEX']['avg_diff']}") print(f"Average difference: {results['TK vs FLEX']['avg_diff']}")
print(f"Maximum difference: {results['TK vs FLEX']['max_diff']}") print(f"Maximum difference: {results['TK vs FLEX']['max_diff']}")
+19 -23
View File
@@ -3,8 +3,6 @@
#include "kittens.cuh" #include "kittens.cuh"
#include <cooperative_groups.h> #include <cooperative_groups.h>
#include <iostream> #include <iostream>
#include <c10/cuda/CUDAGuard.h>
using namespace kittens; using namespace kittens;
namespace cg = cooperative_groups; namespace cg = cooperative_groups;
@@ -942,9 +940,8 @@ block_sparse_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v
float* l_ptr = reinterpret_cast<float*>(l_vec.data_ptr<float>()); float* l_ptr = reinterpret_cast<float*>(l_vec.data_ptr<float>());
float* d_l = reinterpret_cast<float*>(l_ptr); float* d_l = reinterpret_cast<float*>(l_ptr);
//cudadevicesynchronize(); cudaDeviceSynchronize();
const c10::cuda::OptionalCUDAGuard device_guard(q.device()); auto stream = at::cuda::getCurrentCUDAStream().stream();
const cudaStream_t stream = at::cuda::getCurrentCUDAStream().stream();
if (head_dim == 64) { if (head_dim == 64) {
using q_tile = st_bf<fwd_attend_ker_tile_dims<64>::qo_height, fwd_attend_ker_tile_dims<64>::tile_width>; using q_tile = st_bf<fwd_attend_ker_tile_dims<64>::qo_height, fwd_attend_ker_tile_dims<64>::tile_width>;
@@ -969,7 +966,7 @@ block_sparse_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v
globals g{qg_arg, kg_arg, vg_arg, lg_arg, og_arg, static_cast<int>(seq_len), static_cast<int>(hr), static_cast<int>(max_kv_blocks_per_q), reinterpret_cast<int32_t*>(q2k_block_sparse_index.data_ptr()), reinterpret_cast<int32_t*>(q2k_block_sparse_num.data_ptr())}; globals g{qg_arg, kg_arg, vg_arg, lg_arg, og_arg, static_cast<int>(seq_len), static_cast<int>(hr), static_cast<int>(max_kv_blocks_per_q), reinterpret_cast<int32_t*>(q2k_block_sparse_index.data_ptr()), reinterpret_cast<int32_t*>(q2k_block_sparse_num.data_ptr())};
constexpr int mem_size = 54000; auto mem_size = 54000;
dim3 grid(seq_len/(64), qo_heads, batch); dim3 grid(seq_len/(64), qo_heads, batch);
@@ -982,7 +979,7 @@ block_sparse_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v
fwd_attend_ker<64><<<grid, (128), mem_size, stream>>>(g); fwd_attend_ker<64><<<grid, (128), mem_size, stream>>>(g);
CHECK_CUDA_ERROR(cudaGetLastError()); CHECK_CUDA_ERROR(cudaGetLastError());
// cudaStreamSynchronize(stream); cudaStreamSynchronize(stream);
} }
if (head_dim == 128) { if (head_dim == 128) {
@@ -1008,7 +1005,7 @@ block_sparse_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v
globals g{qg_arg, kg_arg, vg_arg, lg_arg, og_arg, static_cast<int>(seq_len), static_cast<int>(hr), static_cast<int>(max_kv_blocks_per_q), reinterpret_cast<int32_t*>(q2k_block_sparse_index.data_ptr()), reinterpret_cast<int32_t*>(q2k_block_sparse_num.data_ptr())}; globals g{qg_arg, kg_arg, vg_arg, lg_arg, og_arg, static_cast<int>(seq_len), static_cast<int>(hr), static_cast<int>(max_kv_blocks_per_q), reinterpret_cast<int32_t*>(q2k_block_sparse_index.data_ptr()), reinterpret_cast<int32_t*>(q2k_block_sparse_num.data_ptr())};
constexpr int mem_size = 54000; auto mem_size = 54000;
dim3 grid(seq_len/(64), qo_heads, batch); dim3 grid(seq_len/(64), qo_heads, batch);
@@ -1021,11 +1018,11 @@ block_sparse_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v
fwd_attend_ker<128><<<grid, (128), mem_size, stream>>>(g); fwd_attend_ker<128><<<grid, (128), mem_size, stream>>>(g);
CHECK_CUDA_ERROR(cudaGetLastError()); CHECK_CUDA_ERROR(cudaGetLastError());
// cudaStreamSynchronize(stream); cudaStreamSynchronize(stream);
} }
return {o, l_vec}; return {o, l_vec};
//cudadevicesynchronize(); cudaDeviceSynchronize();
} }
std::vector<torch::Tensor> std::vector<torch::Tensor>
@@ -1135,14 +1132,13 @@ block_sparse_attention_backward(torch::Tensor q,
float* d_kg = reinterpret_cast<float*>(kg_ptr); float* d_kg = reinterpret_cast<float*>(kg_ptr);
float* d_vg = reinterpret_cast<float*>(vg_ptr); float* d_vg = reinterpret_cast<float*>(vg_ptr);
constexpr int mem_size = kittens::MAX_SHARED_MEMORY; auto mem_size = kittens::MAX_SHARED_MEMORY;
int threads = 4 * kittens::WARP_THREADS; auto threads = 4 * kittens::WARP_THREADS;
//cudadevicesynchronize(); cudaDeviceSynchronize();
const c10::cuda::OptionalCUDAGuard device_guard(q.device()); auto stream = at::cuda::getCurrentCUDAStream().stream();
const cudaStream_t stream = at::cuda::getCurrentCUDAStream().stream();
// cudaStreamSynchronize(stream); cudaStreamSynchronize(stream);
// TORCH_CHECK(seq_len % (4*kittens::TILE_DIM*4) == 0, "sequence length must be divisible by 256"); // TORCH_CHECK(seq_len % (4*kittens::TILE_DIM*4) == 0, "sequence length must be divisible by 256");
dim3 grid_bwd(seq_len/(4*kittens::TILE_ROW_DIM<bf16>*4), qo_heads, batch); dim3 grid_bwd(seq_len/(4*kittens::TILE_ROW_DIM<bf16>*4), qo_heads, batch);
@@ -1226,7 +1222,7 @@ block_sparse_attention_backward(torch::Tensor q,
dim3 grid_bwd_2(seq_len/64, qo_heads, batch); dim3 grid_bwd_2(seq_len/64, qo_heads, batch);
threads = 128; threads = 128;
//cudadevicesynchronize(); cudaDeviceSynchronize();
{ {
cudaFuncSetAttribute( cudaFuncSetAttribute(
@@ -1244,8 +1240,8 @@ block_sparse_attention_backward(torch::Tensor q,
} }
// CHECK_CUDA_ERROR(cudaGetLastError()); // CHECK_CUDA_ERROR(cudaGetLastError());
// cudaStreamSynchronize(stream); cudaStreamSynchronize(stream);
//cudadevicesynchronize(); cudaDeviceSynchronize();
// const auto kernel_end = std::chrono::high_resolution_clock::now(); // const auto kernel_end = std::chrono::high_resolution_clock::now();
// std::cout << "Kernel Time: " << std::chrono::duration_cast<std::chrono::microseconds>(kernel_end - start).count() << "us" << std::endl; // std::cout << "Kernel Time: " << std::chrono::duration_cast<std::chrono::microseconds>(kernel_end - start).count() << "us" << std::endl;
// std::cout << "---" << std::endl; // std::cout << "---" << std::endl;
@@ -1330,7 +1326,7 @@ block_sparse_attention_backward(torch::Tensor q,
dim3 grid_bwd_2(seq_len/64, qo_heads, batch); dim3 grid_bwd_2(seq_len/64, qo_heads, batch);
threads = 128; threads = 128;
//cudadevicesynchronize(); cudaDeviceSynchronize();
{ {
cudaFuncSetAttribute( cudaFuncSetAttribute(
@@ -1342,10 +1338,10 @@ block_sparse_attention_backward(torch::Tensor q,
bwd_attend_ker<128><<<grid_bwd_2, threads, 113000, stream>>>(bwd_global); bwd_attend_ker<128><<<grid_bwd_2, threads, 113000, stream>>>(bwd_global);
} }
// cudaStreamSynchronize(stream); cudaStreamSynchronize(stream);
//cudadevicesynchronize(); cudaDeviceSynchronize();
} }
return {qg, kg, vg}; return {qg, kg, vg};
//cudadevicesynchronize(); cudaDeviceSynchronize();
} }
+3 -1
View File
@@ -18,6 +18,7 @@ import os
import re import re
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Optional
import requests import requests
@@ -167,7 +168,8 @@ _cached_base: str = ""
_cached_branch: str = "" _cached_branch: str = ""
def get_repo_base_and_branch(pr_number: str) -> tuple[str | None, str | None]: def get_repo_base_and_branch(
pr_number: str) -> tuple[Optional[str], Optional[str]]:
global _cached_base, _cached_branch global _cached_base, _cached_branch
if _cached_base and _cached_branch: if _cached_base and _cached_branch:
return _cached_base, _cached_branch return _cached_base, _cached_branch
@@ -8,14 +8,12 @@ You can easily use the FastVideo Docker image as a custom container on [RunPod](
Choose a GPU that supports CUDA 12.4 Choose a GPU that supports CUDA 12.4
Pick 1 or 2 L40S GPU(s)
![RunPod CUDA selection](../../_static/images/runpod_cuda.png) ![RunPod CUDA selection](../../_static/images/runpod_cuda.png)
When creating your pod template, use this image: When creating your pod template, use this image:
``` ```
ghcr.io/hao-ai-lab/fastvideo/fastvideo-dev:py3.12-latest ghcr.io/hao-ai-lab/fastvideo/fastvideo-dev:latest
``` ```
Paste Container Start Command to support SSH ([RunPod Docs](https://docs.runpod.io/pods/configuration/use-ssh)): Paste Container Start Command to support SSH ([RunPod Docs](https://docs.runpod.io/pods/configuration/use-ssh)):
+2 -1
View File
@@ -5,6 +5,7 @@ import itertools
import re import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Optional
ROOT_DIR = Path(__file__).parent.parent.parent.resolve() ROOT_DIR = Path(__file__).parent.parent.parent.resolve()
ROOT_DIR_RELATIVE = '../../../..' ROOT_DIR_RELATIVE = '../../../..'
@@ -88,7 +89,7 @@ class Example:
generate() -> str: Generates the documentation content. generate() -> str: Generates the documentation content.
""" # noqa: E501 """ # noqa: E501
path: Path path: Path
category: str | None = None category: Optional[str] = None
main_file: Path = field(init=False) main_file: Path = field(init=False)
other_files: list[Path] = field(init=False) other_files: list[Path] = field(init=False)
title: str = field(init=False) title: str = field(init=False)
+1 -1
View File
@@ -117,4 +117,4 @@ If you're planning to contribute to FastVideo please see the following page:
If you encounter any issues during installation, please open an issue on our [GitHub repository](https://github.com/hao-ai-lab/FastVideo). If you encounter any issues during installation, please open an issue on our [GitHub repository](https://github.com/hao-ai-lab/FastVideo).
You can also join our [Slack community](https://join.slack.com/t/fastvideo/shared_invite/zt-38u6p1jqe-yDI1QJOCEnbtkLoaI5bjZQ) for additional support. You can also join our [Slack community](https://join.slack.com/t/fastvideo/shared_invite/zt-2zf6ru791-sRwI9lPIUJQq1mIeB_yjJg) for additional support.
+1 -1
View File
@@ -12,7 +12,7 @@ This guide explains how to implement a custom diffusion pipeline in FastVideo, l
4. **Register Your Pipeline** - Make it discoverable by the framework 4. **Register Your Pipeline** - Make it discoverable by the framework
5. **Configure Your Pipeline** - (Coming soon) 5. **Configure Your Pipeline** - (Coming soon)
Need help? Join our [Slack community](https://join.slack.com/t/fastvideo/shared_invite/zt-38u6p1jqe-yDI1QJOCEnbtkLoaI5bjZQ). Need help? Join our [Slack community](https://join.slack.com/t/fastvideo/shared_invite/zt-2zf6ru791-sRwI9lPIUJQq1mIeB_yjJg).
## Step 1: Pipeline Modules ## Step 1: Pipeline Modules
+3 -3
View File
@@ -27,7 +27,7 @@ fastvideo generate --help
### Hardware Configuration ### Hardware Configuration
- `--num-gpus {NUM_GPUS}`: Number of GPUs to use - `--num-gpus {NUM_GPUS}`: Number of GPUs to use
- `--tp-size {TP_SIZE}`: Tensor parallelism size (only for the encoder, should not be larger than 1 if text encoder offload is enabled, as layerwise offload + prefetch is faster) - `--tp-size {TP_SIZE}`: Tensor parallelism size (Typically should match the number of GPUs)
- `--sp-size {SP_SIZE}`: Sequence parallelism size (Typically should match the number of GPUs) - `--sp-size {SP_SIZE}`: Sequence parallelism size (Typically should match the number of GPUs)
#### Video Configuration #### Video Configuration
@@ -68,7 +68,7 @@ Example configuration file (config.json):
"output_path": "outputs/", "output_path": "outputs/",
"num_gpus": 2, "num_gpus": 2,
"sp_size": 2, "sp_size": 2,
"tp_size": 1, "tp_size": 2,
"num_frames": 45, "num_frames": 45,
"height": 720, "height": 720,
"width": 1280, "width": 1280,
@@ -102,7 +102,7 @@ prompt: "A beautiful woman in a red dress walking down a street"
output_path: "outputs/" output_path: "outputs/"
num_gpus: 2 num_gpus: 2
sp_size: 2 sp_size: 2
tp_size: 1 tp_size: 2
num_frames: 45 num_frames: 45
height: 720 height: 720
width: 1280 width: 1280
@@ -121,4 +121,4 @@ If the generated video doesn't match your prompt:
- Learn about using [Optimizations](#inference-optimizations) - Learn about using [Optimizations](#inference-optimizations)
- See [Examples](../examples/examples_inference_index.md) for more usage scenarios - See [Examples](../examples/examples_inference_index.md) for more usage scenarios
- Join our [Community Discord](https://discord.gg/JA7cksDz86). - Join our [Community Discord](https://discord.gg/JA7cksDz86).
- Join our [Community Slack](https://join.slack.com/t/fastvideo/shared_invite/zt-38u6p1jqe-yDI1QJOCEnbtkLoaI5bjZQ). - Join our [Community Slack](https://join.slack.com/t/fastvideo/shared_invite/zt-2zf6ru791-sRwI9lPIUJQq1mIeB_yjJg).
+1 -1
View File
@@ -10,7 +10,7 @@ def main():
# attempt to identify the optimal arguments. # attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_pretrained(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers", "Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
# FastVideo will automatically handle distributed setup # if num_gpus > 1, FastVideo will automatically handle distributed setup
num_gpus=2, num_gpus=2,
use_fsdp_inference=True, use_fsdp_inference=True,
use_cpu_offload=False use_cpu_offload=False
@@ -1,10 +0,0 @@
This directory contain e2e examples scripts for finetuning Wan2.1 I2V.
Execute the following commands from `FastVideo/` to run training:
- Download crush-smol dataset:
`bash examples/training/finetune/wan_i2v_14b_480p/crush_smol/download_dataset.sh`
- Preprocess the videos and captions into latents:
`bash examples/training/finetune/wan_i2v_14b_480p/crush_smol/preprocess_wan_data_i2v.sh`
- Edit the following file and run finetuning:
`bash examples/training/finetune/wan_i2v_14b_480p/crush_smol/finetune_i2v.sh`
@@ -1,3 +0,0 @@
#!/bin/bash
python scripts/huggingface/download_hf.py --repo_id "wlsaidhi/crush-smol-merged" --local_dir "data/crush-smol" --repo_type "dataset"
@@ -1,94 +0,0 @@
#!/bin/bash
export WANDB_BASE_URL="https://api.wandb.ai"
export WANDB_MODE=online
export TOKENIZERS_PARALLELISM=false
# export FASTVIDEO_ATTENTION_BACKEND=TORCH_SDPA
MODEL_PATH="weizhou03/Wan2.1-Fun-1.3B-InP-Diffusers"
DATA_DIR="data/crush-smol_processed_i2v_1_3b_inp/combined_parquet_dataset/"
VALIDATION_DATASET_FILE="examples/training/finetune/Wan2.1-Fun-1.3B-InP/crush_smol/validation.json"
NUM_GPUS=8
# export CUDA_VISIBLE_DEVICES=4,5
# IP=[MASTER NODE IP]
# Training arguments
training_args=(
--tracker_project_name "wan_i2v_finetune"
--wandb_run_name "wan_i2v_finetune"
--output_dir "outputs/wan_i2v_finetune"
--max_train_steps 2000
--train_batch_size 1
--train_sp_batch_size 1
--gradient_accumulation_steps 1
--num_latent_t 8
--num_height 480
--num_width 832
--num_frames 77
)
# Parallel arguments
parallel_args=(
--num_gpus $NUM_GPUS
--sp_size 1
--tp_size 1
--hsdp_replicate_dim 8
--hsdp_shard_dim 1
)
# Model arguments
model_args=(
--model_path $MODEL_PATH
--pretrained_model_name_or_path $MODEL_PATH
)
# Dataset arguments
dataset_args=(
--data_path "$DATA_DIR"
--dataloader_num_workers 1
)
# Validation arguments
validation_args=(
--log_validation
--validation_dataset_file "$VALIDATION_DATASET_FILE"
--validation_steps 100
--validation_sampling_steps "50"
--validation_guidance_scale "1.0"
)
# Optimizer arguments
optimizer_args=(
--learning_rate 2e-5
--mixed_precision "bf16"
--checkpointing_steps 2000
--weight_decay 0.1
--max_grad_norm 1.0
)
# Miscellaneous arguments
miscellaneous_args=(
--inference_mode False
--allow_tf32
--checkpoints_total_limit 3
--training_cfg_rate 0.0
--multi_phased_distill_schedule "4000-1"
--not_apply_cfg_solver
--dit_precision "fp32"
--num_euler_timesteps 50
--ema_start_step 0
)
# --enable_gradient_checkpointing_type "full"
# If you do not have 32 GPUs and to fit in memory, you can: 1. increase sp_size. 2. reduce num_latent_t
torchrun \
--nnodes 1 \
--nproc_per_node $NUM_GPUS \
fastvideo/v1/training/wan_i2v_training_pipeline.py \
"${parallel_args[@]}" \
"${model_args[@]}" \
"${dataset_args[@]}" \
"${training_args[@]}" \
"${optimizer_args[@]}" \
"${validation_args[@]}" \
"${miscellaneous_args[@]}"
@@ -1,131 +0,0 @@
#!/bin/bash
#SBATCH --job-name=i2v
#SBATCH --partition=main
#SBATCH --qos=hao
#SBATCH --nodes=4
#SBATCH --ntasks=4
#SBATCH --ntasks-per-node=1
#SBATCH --gres=gpu:8
#SBATCH --cpus-per-task=128
#SBATCH --nodelist=fs-mbz-gpu-[100-850]
#SBATCH --mem=1440G
#SBATCH --output=i2v_output/i2v_%j.out
#SBATCH --error=i2v_output/i2v_%j.err
#SBATCH --exclusive
set -e -x
# Environment Setup
source ~/conda/miniconda/bin/activate
conda activate will-fv
# Basic Info
export WANDB_MODE="online"
export NCCL_P2P_DISABLE=1
export TORCH_NCCL_ENABLE_MONITORING=0
# different cache dir for different processes
export TRITON_CACHE_DIR=/tmp/triton_cache_${SLURM_PROCID}
export MASTER_PORT=29500
export NODE_RANK=$SLURM_PROCID
nodes=( $(scontrol show hostnames $SLURM_JOB_NODELIST) )
export MASTER_ADDR=${nodes[0]}
export CUDA_VISIBLE_DEVICES=$SLURM_LOCALID
export TOKENIZERS_PARALLELISM=false
export WANDB_BASE_URL="https://api.wandb.ai"
export WANDB_MODE=online
# export FASTVIDEO_ATTENTION_BACKEND=TORCH_SDPA
echo "MASTER_ADDR: $MASTER_ADDR"
echo "NODE_RANK: $NODE_RANK"
# Configs
NUM_GPUS=8
MODEL_PATH="weizhou03/Wan2.1-Fun-1.3B-InP-Diffusers"
DATA_DIR="data/crush-smol_processed_i2v_1_3b_inp/combined_parquet_dataset/"
VALIDATION_DATASET_FILE="examples/training/finetune/Wan2.1-Fun-1.3B-InP/crush_smol/validation.json"
# export CUDA_VISIBLE_DEVICES=4,5
# IP=[MASTER NODE IP]
# If you do not have 32 GPUs and to fit in memory, you can: 1. increase sp_size. 2. reduce num_latent_t
# Training arguments
training_args=(
--tracker_project_name wan_i2v_finetune
--output_dir="$DATA_DIR/outputs/wan_i2v_finetune_2n"
--max_train_steps=2000
--train_batch_size=2
--train_sp_batch_size 1
--gradient_accumulation_steps=1
--num_latent_t 8
--num_height 480
--num_width 832
--num_frames 77
)
# Parallel arguments
parallel_args=(
--num_gpus $NUM_GPUS
--sp_size $NUM_GPUS
--tp_size $NUM_GPUS
--hsdp_replicate_dim $SLURM_JOB_NUM_NODES
--hsdp_shard_dim $NUM_GPUS
)
# Model arguments
model_args=(
--model_path $MODEL_PATH
--pretrained_model_name_or_path $MODEL_PATH
)
# Dataset arguments
dataset_args=(
--data_path "$DATA_DIR"
--dataloader_num_workers 10
)
# Validation arguments
validation_args=(
--log_validation
--validation_dataset_file "$VALIDATION_DATASET_FILE"
--validation_steps 100
--validation_sampling_steps "40"
--validation_guidance_scale "1.0"
)
# Optimizer arguments
optimizer_args=(
--learning_rate=1e-5
--mixed_precision="bf16"
--checkpointing_steps=1000
--weight_decay 1e-4
--max_grad_norm 1.0
)
# Miscellaneous arguments
miscellaneous_args=(
--inference_mode False
--allow_tf32
--checkpoints_total_limit 3
--training_cfg_rate 0.1
--multi_phased_distill_schedule "4000-1"
--not_apply_cfg_solver
--dit_precision "fp32"
--num_euler_timesteps 50
--ema_start_step 0
--enable_gradient_checkpointing_type "full"
)
srun torchrun \
--nnodes $SLURM_JOB_NUM_NODES \
--nproc_per_node $NUM_GPUS \
--node_rank $SLURM_PROCID \
--rdzv_backend=c10d \
--rdzv_endpoint="$MASTER_ADDR:$MASTER_PORT" \
fastvideo/v1/training/wan_i2v_training_pipeline.py \
"${parallel_args[@]}" \
"${model_args[@]}" \
"${dataset_args[@]}" \
"${training_args[@]}" \
"${optimizer_args[@]}" \
"${validation_args[@]}" \
"${miscellaneous_args[@]}"
@@ -1,26 +0,0 @@
#!/bin/bash
GPU_NUM=1 # 2,4,8
MODEL_PATH="weizhou03/Wan2.1-Fun-1.3B-InP-Diffusers"
MODEL_TYPE="wan"
DATA_MERGE_PATH="data/crush-smol/merge.txt"
OUTPUT_DIR="data/crush-smol_processed_i2v_1_3b_inp/"
VALIDATION_PATH="examples/training/finetune/Wan2.1-Fun-1.3B-InP/crush_smol/validation.json"
torchrun --nproc_per_node=$GPU_NUM \
fastvideo/v1/pipelines/preprocess/v1_preprocess.py \
--model_path $MODEL_PATH \
--data_merge_path $DATA_MERGE_PATH \
--preprocess_video_batch_size 8 \
--max_height 480 \
--max_width 832 \
--num_frames 77 \
--dataloader_num_workers 0 \
--output_dir=$OUTPUT_DIR \
--model_type $MODEL_TYPE \
--train_fps 16 \
--validation_dataset_file $VALIDATION_PATH \
--samples_per_file 8 \
--flush_frequency 8 \
--video_length_tolerance_range 5 \
--preprocess_task "i2v"
@@ -1,31 +0,0 @@
{
"data": [
{
"caption": "A large metal cylinder is seen pressing down on a pile of Oreo cookies, flattening them as if they were under a hydraulic press.",
"image_path": null,
"video_path": "validation_dataset/yYcK4nANZz4-Scene-034.mp4",
"num_inference_steps": 40,
"height": 480,
"width": 832,
"num_frames": 77
},
{
"caption": "A large metal cylinder is seen compressing colorful clay into a compact shape, demonstrating the power of a hydraulic press.",
"image_path": null,
"video_path": "validation_dataset/yYcK4nANZz4-Scene-027.mp4",
"num_inference_steps": 40,
"height": 480,
"width": 832,
"num_frames": 77
},
{
"caption": "A large metal cylinder is seen pressing down on a pile of colorful candies, flattening them as if they were under a hydraulic press. The candies are crushed and broken into small pieces, creating a mess on the table.",
"image_path": null,
"video_path": "validation_dataset/yYcK4nANZz4-Scene-030.mp4",
"num_inference_steps": 40,
"height": 480,
"width": 832,
"num_frames": 77
}
]
}
@@ -1,10 +0,0 @@
This directory contain e2e examples scripts for finetuning Wan2.1 I2V.
Execute the following commands from `FastVideo/` to run training:
- Download crush-smol dataset:
`bash examples/training/finetune/wan_i2v_14b_480p/crush_smol/download_dataset.sh`
- Preprocess the videos and captions into latents:
`bash examples/training/finetune/wan_i2v_14b_480p/crush_smol/preprocess_wan_data_i2v.sh`
- Edit the following file and run finetuning:
`bash examples/training/finetune/wan_i2v_14b_480p/crush_smol/finetune_i2v.sh`
@@ -1,3 +0,0 @@
#!/bin/bash
python scripts/huggingface/download_hf.py --repo_id "wlsaidhi/crush-smol-merged" --local_dir "data/crush-smol" --repo_type "dataset"
@@ -1,13 +1,9 @@
#!/bin/bash
export WANDB_BASE_URL="https://api.wandb.ai" export WANDB_BASE_URL="https://api.wandb.ai"
export WANDB_MODE=online export WANDB_MODE=online
export TOKENIZERS_PARALLELISM=false
# export FASTVIDEO_ATTENTION_BACKEND=TORCH_SDPA # export FASTVIDEO_ATTENTION_BACKEND=TORCH_SDPA
MODEL_PATH="Wan-AI/Wan2.1-I2V-14B-480P-Diffusers"
DATA_DIR="data/crush-smol_processed_i2v/combined_parquet_dataset/" DATA_DIR="data/crush-smol_processed_i2v/combined_parquet_dataset/"
VALIDATION_DATASET_FILE="examples/training/finetune/wan_i2v_14b_480p/crush_smol/validation.json" VALIDATION_DIR="data/crush-smol_processed_i2v/validation_parquet_dataset/"
NUM_GPUS=8 NUM_GPUS=8
# export CUDA_VISIBLE_DEVICES=4,5 # export CUDA_VISIBLE_DEVICES=4,5
# IP=[MASTER NODE IP] # IP=[MASTER NODE IP]
@@ -16,7 +12,7 @@ NUM_GPUS=8
training_args=( training_args=(
--tracker_project_name "wan_i2v_finetune" --tracker_project_name "wan_i2v_finetune"
--output_dir "$DATA_DIR/outputs/wan_i2v_finetune" --output_dir "$DATA_DIR/outputs/wan_i2v_finetune"
--max_train_steps 2000 --max_train_steps 5000
--train_batch_size 1 --train_batch_size 1
--train_sp_batch_size 1 --train_sp_batch_size 1
--gradient_accumulation_steps 1 --gradient_accumulation_steps 1
@@ -30,15 +26,15 @@ training_args=(
parallel_args=( parallel_args=(
--num_gpus $NUM_GPUS --num_gpus $NUM_GPUS
--sp_size 8 --sp_size 8
--tp_size 1 --tp_size 8
--hsdp_replicate_dim 1 --hsdp_replicate_dim 1
--hsdp_shard_dim 8 --hsdp_shard_dim 8
) )
# Model arguments # Model arguments
model_args=( model_args=(
--model_path $MODEL_PATH --model_path "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers"
--pretrained_model_name_or_path $MODEL_PATH --pretrained_model_name_or_path "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers"
) )
# Dataset arguments # Dataset arguments
@@ -50,7 +46,7 @@ dataset_args=(
# Validation arguments # Validation arguments
validation_args=( validation_args=(
--log_validation --log_validation
--validation_dataset_file "$VALIDATION_DATASET_FILE" --validation_preprocessed_path "$VALIDATION_DIR"
--validation_steps 100 --validation_steps 100
--validation_sampling_steps "40" --validation_sampling_steps "40"
--validation_guidance_scale "1.0" --validation_guidance_scale "1.0"
@@ -60,7 +56,7 @@ validation_args=(
optimizer_args=( optimizer_args=(
--learning_rate 1e-5 --learning_rate 1e-5
--mixed_precision "bf16" --mixed_precision "bf16"
--checkpointing_steps 1000 --checkpointing_steps 6000
--weight_decay 1e-4 --weight_decay 1e-4
--max_grad_norm 1.0 --max_grad_norm 1.0
) )
@@ -70,13 +66,12 @@ miscellaneous_args=(
--inference_mode False --inference_mode False
--allow_tf32 --allow_tf32
--checkpoints_total_limit 3 --checkpoints_total_limit 3
--training_cfg_rate 0.1 --cfg 0.0
--multi_phased_distill_schedule "4000-1" --multi_phased_distill_schedule "4000-1"
--not_apply_cfg_solver --not_apply_cfg_solver
--dit_precision "fp32" --dit_precision "fp32"
--num_euler_timesteps 50 --num_euler_timesteps 50
--ema_start_step 0 --ema_start_step 0
--enable_gradient_checkpointing_type "full"
) )
# If you do not have 32 GPUs and to fit in memory, you can: 1. increase sp_size. 2. reduce num_latent_t # If you do not have 32 GPUs and to fit in memory, you can: 1. increase sp_size. 2. reduce num_latent_t
@@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
#SBATCH --job-name=i2v #SBATCH --job-name=FV_2N_14B
#SBATCH --partition=main #SBATCH --partition=main
#SBATCH --qos=hao #SBATCH --qos=hao
#SBATCH --nodes=4 #SBATCH --nodes=4
@@ -7,10 +7,10 @@
#SBATCH --ntasks-per-node=1 #SBATCH --ntasks-per-node=1
#SBATCH --gres=gpu:8 #SBATCH --gres=gpu:8
#SBATCH --cpus-per-task=128 #SBATCH --cpus-per-task=128
#SBATCH --nodelist=fs-mbz-gpu-[100-850] #SBATCH --nodelist=fs-mbz-gpu-[400-550]
#SBATCH --mem=1440G #SBATCH --mem=1440G
#SBATCH --output=i2v_output/i2v_%j.out #SBATCH --output=4n_i2v/4n_i2v_%j.out
#SBATCH --error=i2v_output/i2v_%j.err #SBATCH --error=4n_i2v/4n_i2v_%j.err
#SBATCH --exclusive #SBATCH --exclusive
set -e -x set -e -x
@@ -30,9 +30,7 @@ nodes=( $(scontrol show hostnames $SLURM_JOB_NODELIST) )
export MASTER_ADDR=${nodes[0]} export MASTER_ADDR=${nodes[0]}
export CUDA_VISIBLE_DEVICES=$SLURM_LOCALID export CUDA_VISIBLE_DEVICES=$SLURM_LOCALID
export TOKENIZERS_PARALLELISM=false export TOKENIZERS_PARALLELISM=false
export WANDB_BASE_URL="https://api.wandb.ai" export WANDB_API_KEY='8d9f4b39abd68eb4e29f6fc010b7ee71a2207cde'
export WANDB_MODE=online
# export FASTVIDEO_ATTENTION_BACKEND=TORCH_SDPA
echo "MASTER_ADDR: $MASTER_ADDR" echo "MASTER_ADDR: $MASTER_ADDR"
echo "NODE_RANK: $NODE_RANK" echo "NODE_RANK: $NODE_RANK"
@@ -40,92 +38,60 @@ echo "NODE_RANK: $NODE_RANK"
# Configs # Configs
NUM_GPUS=8 NUM_GPUS=8
MODEL_PATH="Wan-AI/Wan2.1-I2V-14B-480P-Diffusers" export WANDB_BASE_URL="https://api.wandb.ai"
DATA_DIR="data/crush-smol_processed_i2v/combined_parquet_dataset/" export WANDB_MODE=online
VALIDATION_DATASET_FILE="examples/training/finetune/wan_i2v_14b_480p/crush_smol/validation.json" # export FASTVIDEO_ATTENTION_BACKEND=TORCH_SDPA
DATA_DIR=data/crush-smol_processed_i2v/combined_parquet_dataset
VALIDATION_DIR=data/crush-smol_processed_i2v/validation_parquet_dataset
NUM_GPUS=8
# export CUDA_VISIBLE_DEVICES=4,5 # export CUDA_VISIBLE_DEVICES=4,5
# IP=[MASTER NODE IP] # IP=[MASTER NODE IP]
# If you do not have 32 GPUs and to fit in memory, you can: 1. increase sp_size. 2. reduce num_latent_t # If you do not have 32 GPUs and to fit in memory, you can: 1. increase sp_size. 2. reduce num_latent_t
# Training arguments
training_args=(
--tracker_project_name wan_i2v_finetune
--output_dir="$DATA_DIR/outputs/wan_i2v_finetune_2n"
--max_train_steps=2000
--train_batch_size=2
--train_sp_batch_size 1
--gradient_accumulation_steps=1
--num_latent_t 8
--num_height 480
--num_width 832
--num_frames 77
)
# Parallel arguments
parallel_args=(
--num_gpus $NUM_GPUS
--sp_size $NUM_GPUS
--tp_size 1
--hsdp_replicate_dim $SLURM_JOB_NUM_NODES
--hsdp_shard_dim $NUM_GPUS
)
# Model arguments
model_args=(
--model_path $MODEL_PATH
--pretrained_model_name_or_path $MODEL_PATH
)
# Dataset arguments
dataset_args=(
--data_path "$DATA_DIR"
--dataloader_num_workers 10
)
# Validation arguments
validation_args=(
--log_validation
--validation_dataset_file "$VALIDATION_DATASET_FILE"
--validation_steps 100
--validation_sampling_steps "40"
--validation_guidance_scale "1.0"
)
# Optimizer arguments
optimizer_args=(
--learning_rate=1e-5
--mixed_precision="bf16"
--checkpointing_steps=1000
--weight_decay 1e-4
--max_grad_norm 1.0
)
# Miscellaneous arguments
miscellaneous_args=(
--inference_mode False
--allow_tf32
--checkpoints_total_limit 3
--training_cfg_rate 0.1
--multi_phased_distill_schedule "4000-1"
--not_apply_cfg_solver
--dit_precision "fp32"
--num_euler_timesteps 50
--ema_start_step 0
--enable_gradient_checkpointing_type "full"
)
srun torchrun \ srun torchrun \
--nnodes $SLURM_JOB_NUM_NODES \ --nnodes $SLURM_JOB_NUM_NODES \
--nproc_per_node $NUM_GPUS \ --nproc_per_node $NUM_GPUS \
--node_rank $SLURM_PROCID \ --node_rank $SLURM_PROCID \
--rdzv_backend=c10d \ --rdzv_backend=c10d \
--rdzv_endpoint="$MASTER_ADDR:$MASTER_PORT" \ --rdzv_endpoint="$MASTER_ADDR:$MASTER_PORT" \
fastvideo/v1/training/wan_i2v_training_pipeline.py \ fastvideo/v1/training/wan_i2v_training_pipeline.py\
"${parallel_args[@]}" \ --model_path Wan-AI/Wan2.1-I2V-14B-480P-Diffusers \
"${model_args[@]}" \ --inference_mode False\
"${dataset_args[@]}" \ --pretrained_model_name_or_path Wan-AI/Wan2.1-I2V-14B-480P-Diffusers \
"${training_args[@]}" \ --cache_dir "/home/ray/.cache"\
"${optimizer_args[@]}" \ --data_path "$DATA_DIR"\
"${validation_args[@]}" \ --validation_preprocessed_path "$VALIDATION_DIR"\
"${miscellaneous_args[@]}" --train_batch_size=1\
--num_latent_t 16 \
--num_gpus $NUM_GPUS \
--sp_size $NUM_GPUS \
--tp_size $NUM_GPUS \
--hsdp_replicate_dim $SLURM_JOB_NUM_NODES \
--hsdp_shard_dim $NUM_GPUS \
--train_sp_batch_size 1\
--dataloader_num_workers 10\
--gradient_accumulation_steps=2\
--max_train_steps=10000 \
--learning_rate=5e-5\
--mixed_precision="bf16"\
--checkpointing_steps=11000 \
--validation_steps 100\
--validation_sampling_steps "40" \
--log_validation \
--checkpoints_total_limit 3\
--allow_tf32\
--ema_start_step 0\
--cfg 0.0\
--output_dir="$DATA_DIR/outputs/wan_i2v_finetune_2n"\
--tracker_project_name wan_i2v_finetune \
--num_height 480 \
--num_width 832 \
--num_frames 77 \
--validation_guidance_scale "1.0" \
--num_euler_timesteps 50 \
--multi_phased_distill_schedule "4000-1" \
--weight_decay 1e-4 \
--not_apply_cfg_solver \
--dit_precision "fp32" \
--max_grad_norm 1.0
@@ -1,5 +1,4 @@
#!/bin/bash # export WANDB_MODE="offline"
GPU_NUM=1 # 2,4,8 GPU_NUM=1 # 2,4,8
MODEL_PATH="Wan-AI/Wan2.1-I2V-14B-480P-Diffusers" MODEL_PATH="Wan-AI/Wan2.1-I2V-14B-480P-Diffusers"
MODEL_TYPE="wan" MODEL_TYPE="wan"
@@ -22,5 +21,4 @@ torchrun --nproc_per_node=$GPU_NUM \
--validation_dataset_file $VALIDATION_PATH \ --validation_dataset_file $VALIDATION_PATH \
--samples_per_file 8 \ --samples_per_file 8 \
--flush_frequency 8 \ --flush_frequency 8 \
--video_length_tolerance_range 5 \
--preprocess_task "i2v" --preprocess_task "i2v"
@@ -26,24 +26,6 @@
"height": 480, "height": 480,
"width": 832, "width": 832,
"num_frames": 77 "num_frames": 77
},
{
"caption": "The video shows a close-up of an orange being flattened as if it were under a hydraulic press, with the press moving down and compressing the fruit until it is completely flattened.",
"image_path": null,
"video_path": "validation_dataset/EJqsC21GSBY-Scene-059.mp4",
"num_inference_steps": 40,
"height": 480,
"width": 832,
"num_frames": 98
},
{
"caption": "The video shows a stack of colorful sponges being flattened as if they were under a hydraulic press. The sponges, which are pink, white, blue, and green, are compressed into a smaller size, demonstrating the press's power. The background features a green wall with a yellow and red sign, adding context to the setting.",
"image_path": null,
"video_path": "validation_dataset/EJqsC21GSBY-Scene-013.mp4",
"num_inference_steps": 40,
"height": 480,
"width": 832,
"num_frames": 148
} }
] ]
} }
@@ -1,10 +0,0 @@
This directory contain e2e examples scripts for finetuning Wan2.1 T2v.
Execute the following commands from `FastVideo/` to run training:
- Download crush-smol dataset:
`bash examples/training/finetune/wan_t2v_1_3b/crush_smol/download_dataset.sh`
- Preprocess the videos and captions into latents:
`bash examples/training/finetune/wan_t2v_1_3b/crush_smol/preprocess_wan_data_t2v.sh`
- Edit the following file and run finetuning:
`bash examples/training/finetune/wan_t2v_1_3b/crush_smol/finetune_t2v.sh`
@@ -1,3 +0,0 @@
#!/bin/bash
python scripts/huggingface/download_hf.py --repo_id "wlsaidhi/crush-smol-merged" --local_dir "data/crush-smol" --repo_type "dataset"
@@ -1,13 +1,10 @@
#!/bin/bash
export WANDB_BASE_URL="https://api.wandb.ai" export WANDB_BASE_URL="https://api.wandb.ai"
export WANDB_MODE=online export WANDB_MODE=online
export TOKENIZERS_PARALLELISM=false
# export FASTVIDEO_ATTENTION_BACKEND=TORCH_SDPA # export FASTVIDEO_ATTENTION_BACKEND=TORCH_SDPA
MODEL_PATH="Wan-AI/Wan2.1-T2V-1.3B-Diffusers" MODEL_PATH="Wan-AI/Wan2.1-T2V-1.3B-Diffusers"
DATA_DIR="data/crush-smol_processed_t2v/combined_parquet_dataset/" DATA_DIR="data/crush-smol_processed_t2v/combined_parquet_dataset/"
VALIDATION_DATASET_FILE="examples/training/finetune/wan_t2v_1_3b/crush_smol/validation.json" VALIDATION_DIR="data/crush-smol_processed_t2v/validation_parquet_dataset/"
NUM_GPUS=4 NUM_GPUS=4
# export CUDA_VISIBLE_DEVICES=4,5 # export CUDA_VISIBLE_DEVICES=4,5
@@ -19,7 +16,7 @@ training_args=(
--max_train_steps 5000 --max_train_steps 5000
--train_batch_size 1 --train_batch_size 1
--train_sp_batch_size 1 --train_sp_batch_size 1
--gradient_accumulation_steps 8 --gradient_accumulation_steps 1
--num_latent_t 8 --num_latent_t 8
--num_height 480 --num_height 480
--num_width 832 --num_width 832
@@ -28,11 +25,11 @@ training_args=(
# Parallel arguments # Parallel arguments
parallel_args=( parallel_args=(
--num_gpus $NUM_GPUS --num_gpus $NUM_GPUS \
--sp_size $NUM_GPUS --sp_size $NUM_GPUS \
--tp_size 1 --tp_size $NUM_GPUS \
--hsdp_replicate_dim 1 --hsdp_replicate_dim 1 \
--hsdp_shard_dim $NUM_GPUS --hsdp_shard_dim $NUM_GPUS \
) )
# Model arguments # Model arguments
@@ -50,18 +47,18 @@ dataset_args=(
# Validation arguments # Validation arguments
validation_args=( validation_args=(
--log_validation --log_validation
--validation_dataset_file $VALIDATION_DATASET_FILE --validation_preprocessed_path $VALIDATION_DIR
--validation_steps 50 --validation_steps 100
--validation_sampling_steps "50" --validation_sampling_steps "50"
--validation_guidance_scale "1.0" --validation_guidance_scale "1.0"
) )
# Optimizer arguments # Optimizer arguments
optimizer_args=( optimizer_args=(
--learning_rate 5e-5 --learning_rate 1e-5
--mixed_precision "bf16" --mixed_precision "bf16"
--checkpointing_steps 6000 --checkpointing_steps 6000
--weight_decay 1e-4 --weight_decay 0.01
--max_grad_norm 1.0 --max_grad_norm 1.0
) )
@@ -70,13 +67,12 @@ miscellaneous_args=(
--inference_mode False --inference_mode False
--allow_tf32 --allow_tf32
--checkpoints_total_limit 3 --checkpoints_total_limit 3
--training_cfg_rate 0.1 --cfg 0.0
--multi_phased_distill_schedule "4000-1" --multi_phased_distill_schedule "4000-1"
--not_apply_cfg_solver --not_apply_cfg_solver
--dit_precision "fp32" --dit_precision "fp32"
--num_euler_timesteps 50 --num_euler_timesteps 50
--ema_start_step 0 --ema_start_step 0
--enable_gradient_checkpointing_type "full"
) )
torchrun \ torchrun \
@@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
#SBATCH --job-name=t2v #SBATCH --job-name=FV_2N_14B
#SBATCH --partition=main #SBATCH --partition=main
#SBATCH --qos=hao #SBATCH --qos=hao
#SBATCH --nodes=1 #SBATCH --nodes=1
@@ -7,10 +7,10 @@
#SBATCH --ntasks-per-node=1 #SBATCH --ntasks-per-node=1
#SBATCH --gres=gpu:8 #SBATCH --gres=gpu:8
#SBATCH --cpus-per-task=128 #SBATCH --cpus-per-task=128
#SBATCH --nodelist=fs-mbz-gpu-[100-850] #SBATCH --nodelist=fs-mbz-gpu-[400-550]
#SBATCH --mem=1440G #SBATCH --mem=1440G
#SBATCH --output=t2v_output/t2v_%j.out #SBATCH --output=4n_i2v/4n_i2v_%j.out
#SBATCH --error=t2v_output/t2v_%j.err #SBATCH --error=4n_i2v/4n_i2v_%j.err
#SBATCH --exclusive #SBATCH --exclusive
set -e -x set -e -x
@@ -30,99 +30,69 @@ nodes=( $(scontrol show hostnames $SLURM_JOB_NODELIST) )
export MASTER_ADDR=${nodes[0]} export MASTER_ADDR=${nodes[0]}
export CUDA_VISIBLE_DEVICES=$SLURM_LOCALID export CUDA_VISIBLE_DEVICES=$SLURM_LOCALID
export TOKENIZERS_PARALLELISM=false export TOKENIZERS_PARALLELISM=false
export WANDB_BASE_URL="https://api.wandb.ai" export WANDB_API_KEY='8d9f4b39abd68eb4e29f6fc010b7ee71a2207cde'
export WANDB_MODE=online
# export FASTVIDEO_ATTENTION_BACKEND=TORCH_SDPA
echo "MASTER_ADDR: $MASTER_ADDR" echo "MASTER_ADDR: $MASTER_ADDR"
echo "NODE_RANK: $NODE_RANK" echo "NODE_RANK: $NODE_RANK"
# Configs # Configs
NUM_GPUS=8 NUM_GPUS=8
export WANDB_BASE_URL="https://api.wandb.ai"
export WANDB_MODE=online
# export FASTVIDEO_ATTENTION_BACKEND=TORCH_SDPA
MODEL_PATH="Wan-AI/Wan2.1-T2V-1.3B-Diffusers" MODEL_PATH="Wan-AI/Wan2.1-T2V-1.3B-Diffusers"
DATA_DIR="data/crush-smol_processed_t2v/combined_parquet_dataset/" DATA_DIR="data/crush-smol_processed_t2v/combined_parquet_dataset/"
VALIDATION_DATASET_FILE="examples/training/finetune/wan_t2v_1_3b/crush_smol/validation.json" VALIDATION_DIR="data/crush-smol_processed_t2v/validation_parquet_dataset/"
NUM_GPUS=8
# export CUDA_VISIBLE_DEVICES=4,5 # export CUDA_VISIBLE_DEVICES=4,5
# IP=[MASTER NODE IP] # IP=[MASTER NODE IP]
# Training arguments # If you do not have 32 GPUs and to fit in memory, you can: 1. increase sp_size. 2. reduce num_latent_t
training_args=(
--tracker_project_name wan_t2v_finetune
--output_dir="outputs/wan_t2v_finetune"
--max_train_steps=1000
--train_batch_size=4
--train_sp_batch_size 1
--gradient_accumulation_steps=1
--num_latent_t 8
--num_height 480
--num_width 832
--num_frames 77
)
# Parallel arguments
parallel_args=(
--num_gpus $NUM_GPUS
--sp_size 4
--tp_size 1
--hsdp_replicate_dim 2
--hsdp_shard_dim 4
)
# Model arguments
model_args=(
--model_path $MODEL_PATH
--pretrained_model_name_or_path $MODEL_PATH
)
# Dataset arguments
dataset_args=(
--data_path "$DATA_DIR"
--dataloader_num_workers 10
)
# Validation arguments
validation_args=(
--log_validation
--validation_dataset_file "$VALIDATION_DATASET_FILE"
--validation_steps 100
--validation_sampling_steps "50"
--validation_guidance_scale "1.0"
)
# Optimizer arguments
optimizer_args=(
--learning_rate=5e-5
--mixed_precision="bf16"
--checkpointing_steps=500
--weight_decay 1e-4
--max_grad_norm 1.0
)
# Miscellaneous arguments
miscellaneous_args=(
--inference_mode False
--allow_tf32
--checkpoints_total_limit 3
--training_cfg_rate 0.1
--multi_phased_distill_schedule "4000-1"
--not_apply_cfg_solver
--dit_precision "fp32"
--num_euler_timesteps 50
--ema_start_step 0
--enable_gradient_checkpointing_type "full"
)
srun torchrun \ srun torchrun \
--nnodes $SLURM_JOB_NUM_NODES \ --nnodes $SLURM_JOB_NUM_NODES \
--nproc_per_node $NUM_GPUS \ --nproc_per_node $NUM_GPUS \
--node_rank $SLURM_PROCID \ --node_rank $SLURM_PROCID \
--rdzv_backend=c10d \ --rdzv_backend=c10d \
--rdzv_endpoint="$MASTER_ADDR:$MASTER_PORT" \ --rdzv_endpoint="$MASTER_ADDR:$MASTER_PORT" \
fastvideo/v1/training/wan_training_pipeline.py \ fastvideo/v1/training/wan_training_pipeline.py\
"${parallel_args[@]}" \ --model_path $MODEL_PATH \
"${model_args[@]}" \ --inference_mode False\
"${dataset_args[@]}" \ --pretrained_model_name_or_path $MODEL_PATH \
"${training_args[@]}" \ --cache_dir "/home/ray/.cache"\
"${optimizer_args[@]}" \ --data_path "$DATA_DIR"\
"${validation_args[@]}" \ --validation_preprocessed_path "$VALIDATION_DIR"\
"${miscellaneous_args[@]}" --train_batch_size=1\
--num_latent_t 8 \
--num_gpus $NUM_GPUS \
--sp_size $NUM_GPUS \
--tp_size $NUM_GPUS \
--hsdp_replicate_dim $SLURM_JOB_NUM_NODES \
--hsdp_shard_dim $NUM_GPUS \
--train_sp_batch_size 1\
--dataloader_num_workers 10\
--gradient_accumulation_steps=1\
--max_train_steps=10000 \
--learning_rate=5e-5\
--mixed_precision="bf16"\
--checkpointing_steps=11000 \
--validation_steps 100\
--validation_sampling_steps "40" \
--log_validation \
--checkpoints_total_limit 3\
--allow_tf32\
--ema_start_step 0\
--cfg 0.0\
--output_dir="$DATA_DIR/outputs/wan_i2v_finetune_2n"\
--tracker_project_name wan_i2v_finetune \
--num_height 480 \
--num_width 832 \
--num_frames 77 \
--validation_guidance_scale "1.0" \
--num_euler_timesteps 50 \
--multi_phased_distill_schedule "4000-1" \
--weight_decay 1e-4 \
--not_apply_cfg_solver \
--dit_precision "fp32" \
--max_grad_norm 1.0
@@ -0,0 +1,13 @@
{
"data": [
{
"caption": "A large metal cylinder is seen pressing down on a pile of Oreo cookies, flattening them as if they were under a hydraulic press.",
"image_path": null,
"video_path": "examples/training/finetune/wan_i2v_14b_480p/crush_smol/validation_dataset/yYcK4nANZz4-Scene-034.mp4",
"num_inference_steps": 40,
"height": 480,
"width": 832,
"num_frames": 77
}
]
}
@@ -1,5 +1,4 @@
#!/bin/bash # export WANDB_MODE="offline"
GPU_NUM=1 # 2,4,8 GPU_NUM=1 # 2,4,8
MODEL_PATH="Wan-AI/Wan2.1-T2V-1.3B-Diffusers" MODEL_PATH="Wan-AI/Wan2.1-T2V-1.3B-Diffusers"
MODEL_TYPE="wan" MODEL_TYPE="wan"
@@ -22,5 +21,4 @@ torchrun --nproc_per_node=$GPU_NUM \
--validation_dataset_file $VALIDATION_PATH \ --validation_dataset_file $VALIDATION_PATH \
--samples_per_file 8 \ --samples_per_file 8 \
--flush_frequency 8 \ --flush_frequency 8 \
--video_length_tolerance_range 5 \
--preprocess_task "t2v" --preprocess_task "t2v"
+40 -38
View File
@@ -2,7 +2,7 @@
import json import json
import os import os
from collections import defaultdict from collections import defaultdict
from typing import Any from typing import Any, Dict, List, Optional, Tuple
import numpy as np import numpy as np
@@ -13,7 +13,7 @@ def configure_sta(mode: str = 'STA_searching',
layer_num: int = 40, layer_num: int = 40,
time_step_num: int = 50, time_step_num: int = 50,
head_num: int = 40, head_num: int = 40,
**kwargs) -> list[list[list[Any]]]: **kwargs) -> List[List[List[Any]]]:
""" """
Configure Sliding Tile Attention (STA) parameters based on the specified mode. Configure Sliding Tile Attention (STA) parameters based on the specified mode.
@@ -53,22 +53,22 @@ def configure_sta(mode: str = 'STA_searching',
if mode == 'STA_searching': if mode == 'STA_searching':
# Get parameters with defaults # Get parameters with defaults
mask_candidates: list[str] | None = kwargs.get('mask_candidates') mask_candidates: Optional[List[str]] = kwargs.get('mask_candidates')
if mask_candidates is None: if mask_candidates is None:
raise ValueError( raise ValueError(
"mask_candidates is required for STA_searching mode") "mask_candidates is required for STA_searching mode")
mask_selected: list[int] = kwargs.get('mask_selected', mask_selected: List[int] = kwargs.get('mask_selected',
list(range(len(mask_candidates)))) list(range(len(mask_candidates))))
# Parse selected masks # Parse selected masks
selected_masks: list[list[int]] = [] selected_masks: List[List[int]] = []
for index in mask_selected: for index in mask_selected:
mask = mask_candidates[index] mask = mask_candidates[index]
masks_list = [int(x) for x in mask.split(',')] masks_list = [int(x) for x in mask.split(',')]
selected_masks.append(masks_list) selected_masks.append(masks_list)
# Create 3D mask structure with fixed dimensions (t=50, l=60) # Create 3D mask structure with fixed dimensions (t=50, l=60)
masks_3d: list[list[list[list[int]]]] = [] masks_3d: List[List[List[List[int]]]] = []
for i in range(time_step_num): # Fixed t dimension = 50 for i in range(time_step_num): # Fixed t dimension = 50
row = [] row = []
for j in range(layer_num): # Fixed l dimension = 60 for j in range(layer_num): # Fixed l dimension = 60
@@ -79,23 +79,25 @@ def configure_sta(mode: str = 'STA_searching',
elif mode == 'STA_tuning': elif mode == 'STA_tuning':
# Get required parameters # Get required parameters
mask_search_files_path: str | None = kwargs.get( mask_search_files_path: Optional[str] = kwargs.get(
'mask_search_files_path') 'mask_search_files_path')
if not mask_search_files_path: if not mask_search_files_path:
raise ValueError( raise ValueError(
"mask_search_files_path is required for STA_tuning mode") "mask_search_files_path is required for STA_tuning mode")
# Get optional parameters with defaults # Get optional parameters with defaults
mask_candidates_tuning: list[str] | None = kwargs.get('mask_candidates') mask_candidates_tuning: Optional[List[str]] = kwargs.get(
'mask_candidates')
if mask_candidates_tuning is None: if mask_candidates_tuning is None:
raise ValueError("mask_candidates is required for STA_tuning mode") raise ValueError("mask_candidates is required for STA_tuning mode")
mask_selected_tuning: list[int] = kwargs.get( mask_selected_tuning: List[int] = kwargs.get(
'mask_selected', list(range(len(mask_candidates_tuning)))) 'mask_selected', list(range(len(mask_candidates_tuning))))
skip_time_steps_tuning: int | None = kwargs.get('skip_time_steps') skip_time_steps_tuning: Optional[int] = kwargs.get('skip_time_steps')
save_dir_tuning: str | None = kwargs.get('save_dir', "mask_candidates") save_dir_tuning: Optional[str] = kwargs.get('save_dir',
"mask_candidates")
# Parse selected masks # Parse selected masks
selected_masks_tuning: list[list[int]] = [] selected_masks_tuning: List[List[int]] = []
for index in mask_selected_tuning: for index in mask_selected_tuning:
mask = mask_candidates_tuning[index] mask = mask_candidates_tuning[index]
masks_list = [int(x) for x in mask.split(',')] masks_list = [int(x) for x in mask.split(',')]
@@ -106,7 +108,7 @@ def configure_sta(mode: str = 'STA_searching',
averaged_results = average_head_losses(results, selected_masks_tuning) averaged_results = average_head_losses(results, selected_masks_tuning)
# Add full attention mask for specific cases # Add full attention mask for specific cases
full_attention_mask_tuning: list[int] | None = kwargs.get( full_attention_mask_tuning: Optional[List[int]] = kwargs.get(
'full_attention_mask') 'full_attention_mask')
if full_attention_mask_tuning is not None: if full_attention_mask_tuning is not None:
selected_masks_tuning.append(full_attention_mask_tuning) selected_masks_tuning.append(full_attention_mask_tuning)
@@ -147,11 +149,11 @@ def configure_sta(mode: str = 'STA_searching',
return mask_strategy_3d return mask_strategy_3d
elif mode == 'STA_tuning_cfg': elif mode == 'STA_tuning_cfg':
# Get required parameters for both positive and negative paths # Get required parameters for both positive and negative paths
mask_search_files_path_pos: str | None = kwargs.get( mask_search_files_path_pos: Optional[str] = kwargs.get(
'mask_search_files_path_pos') 'mask_search_files_path_pos')
mask_search_files_path_neg: str | None = kwargs.get( mask_search_files_path_neg: Optional[str] = kwargs.get(
'mask_search_files_path_neg') 'mask_search_files_path_neg')
save_dir_cfg: str | None = kwargs.get('save_dir') save_dir_cfg: Optional[str] = kwargs.get('save_dir')
if not mask_search_files_path_pos or not mask_search_files_path_neg or not save_dir_cfg: if not mask_search_files_path_pos or not mask_search_files_path_neg or not save_dir_cfg:
raise ValueError( raise ValueError(
@@ -159,16 +161,16 @@ def configure_sta(mode: str = 'STA_searching',
) )
# Get optional parameters with defaults # Get optional parameters with defaults
mask_candidates_cfg: list[str] | None = kwargs.get('mask_candidates') mask_candidates_cfg: Optional[List[str]] = kwargs.get('mask_candidates')
if mask_candidates_cfg is None: if mask_candidates_cfg is None:
raise ValueError( raise ValueError(
"mask_candidates is required for STA_tuning_cfg mode") "mask_candidates is required for STA_tuning_cfg mode")
mask_selected_cfg: list[int] = kwargs.get( mask_selected_cfg: List[int] = kwargs.get(
'mask_selected', list(range(len(mask_candidates_cfg)))) 'mask_selected', list(range(len(mask_candidates_cfg))))
skip_time_steps_cfg: int | None = kwargs.get('skip_time_steps') skip_time_steps_cfg: Optional[int] = kwargs.get('skip_time_steps')
# Parse selected masks # Parse selected masks
selected_masks_cfg: list[list[int]] = [] selected_masks_cfg: List[List[int]] = []
for index in mask_selected_cfg: for index in mask_selected_cfg:
mask = mask_candidates_cfg[index] mask = mask_candidates_cfg[index]
masks_list = [int(x) for x in mask.split(',')] masks_list = [int(x) for x in mask.split(',')]
@@ -185,7 +187,7 @@ def configure_sta(mode: str = 'STA_searching',
selected_masks_cfg) selected_masks_cfg)
# Add full attention mask for specific cases # Add full attention mask for specific cases
full_attention_mask_cfg: list[int] | None = kwargs.get( full_attention_mask_cfg: Optional[List[int]] = kwargs.get(
'full_attention_mask') 'full_attention_mask')
if full_attention_mask_cfg is not None: if full_attention_mask_cfg is not None:
selected_masks_cfg.append(full_attention_mask_cfg) selected_masks_cfg.append(full_attention_mask_cfg)
@@ -225,7 +227,7 @@ def configure_sta(mode: str = 'STA_searching',
else: # STA_inference else: # STA_inference
# Get parameters with defaults # Get parameters with defaults
load_path: str | None = kwargs.get( load_path: Optional[str] = kwargs.get(
'load_path', "mask_candidates/mask_strategy.json") 'load_path', "mask_candidates/mask_strategy.json")
if load_path is None: if load_path is None:
raise ValueError("load_path is required for STA_inference mode") raise ValueError("load_path is required for STA_inference mode")
@@ -246,9 +248,9 @@ def configure_sta(mode: str = 'STA_searching',
# Helper functions # Helper functions
def read_specific_json_files(folder_path: str) -> list[dict[str, Any]]: def read_specific_json_files(folder_path: str) -> List[Dict[str, Any]]:
"""Read and parse JSON files containing mask search results.""" """Read and parse JSON files containing mask search results."""
json_contents: list[dict[str, Any]] = [] json_contents: List[Dict[str, Any]] = []
# List files only in the current directory (no walk) # List files only in the current directory (no walk)
files = os.listdir(folder_path) files = os.listdir(folder_path)
@@ -266,11 +268,11 @@ def read_specific_json_files(folder_path: str) -> list[dict[str, Any]]:
def average_head_losses( def average_head_losses(
results: list[dict[str, Any]], results: List[Dict[str, Any]],
selected_masks: list[list[int]]) -> dict[str, dict[str, np.ndarray]]: selected_masks: List[List[int]]) -> Dict[str, Dict[str, np.ndarray]]:
"""Average losses across all prompts for each mask strategy.""" """Average losses across all prompts for each mask strategy."""
# Initialize a dictionary to store the averaged results # Initialize a dictionary to store the averaged results
averaged_losses: dict[str, dict[str, np.ndarray]] = {} averaged_losses: Dict[str, Dict[str, np.ndarray]] = {}
loss_type = 'L2_loss' loss_type = 'L2_loss'
# Get all loss types (e.g., 'L2_loss') # Get all loss types (e.g., 'L2_loss')
averaged_losses[loss_type] = {} averaged_losses[loss_type] = {}
@@ -292,14 +294,14 @@ def average_head_losses(
def select_best_mask_strategy( def select_best_mask_strategy(
averaged_results: dict[str, dict[str, np.ndarray]], averaged_results: Dict[str, Dict[str, np.ndarray]],
selected_masks: list[list[int]], selected_masks: List[List[int]],
skip_time_steps: int = 12, skip_time_steps: int = 12,
timesteps: int = 50, timesteps: int = 50,
head_num: int = 40 head_num: int = 40
) -> tuple[dict[str, list[int]], float, dict[str, int]]: ) -> Tuple[Dict[str, List[int]], float, Dict[str, int]]:
"""Select the best mask strategy for each head based on loss minimization.""" """Select the best mask strategy for each head based on loss minimization."""
best_mask_strategy: dict[str, list[int]] = {} best_mask_strategy: Dict[str, List[int]] = {}
loss_type = 'L2_loss' loss_type = 'L2_loss'
# Get the shape of time steps and layers # Get the shape of time steps and layers
layers = len(averaged_results[loss_type][str(selected_masks[0])][0]) layers = len(averaged_results[loss_type][str(selected_masks[0])][0])
@@ -308,7 +310,7 @@ def select_best_mask_strategy(
total_tokens = 0 # total number of masked tokens total_tokens = 0 # total number of masked tokens
total_length = 0 # total sequence length total_length = 0 # total sequence length
strategy_counts: dict[str, int] = { strategy_counts: Dict[str, int] = {
str(strategy): 0 str(strategy): 0
for strategy in selected_masks for strategy in selected_masks
} }
@@ -350,22 +352,22 @@ def select_best_mask_strategy(
def save_mask_search_results( def save_mask_search_results(
mask_search_final_result: list[dict[str, list[float]]], mask_search_final_result: List[Dict[str, List[float]]],
prompt: str, prompt: str,
mask_strategies: list[str], mask_strategies: List[str],
output_dir: str = 'output/mask_search_result/') -> str | None: output_dir: str = 'output/mask_search_result/') -> Optional[str]:
if not mask_search_final_result: if not mask_search_final_result:
print("No mask search results to save") print("No mask search results to save")
return None return None
# Create result dictionary with defaultdict for nested lists # Create result dictionary with defaultdict for nested lists
mask_search_dict: dict[str, dict[str, list[list[float]]]] = { mask_search_dict: Dict[str, Dict[str, List[List[float]]]] = {
"L2_loss": defaultdict(list), "L2_loss": defaultdict(list),
"L1_loss": defaultdict(list) "L1_loss": defaultdict(list)
} }
mask_selected = list(range(len(mask_strategies))) mask_selected = list(range(len(mask_strategies)))
selected_masks: list[list[int]] = [] selected_masks: List[List[int]] = []
for index in mask_selected: for index in mask_selected:
mask = mask_strategies[index] mask = mask_strategies[index]
masks_list = [int(x) for x in mask.split(',')] masks_list = [int(x) for x in mask.split(',')]
@@ -375,7 +377,7 @@ def save_mask_search_results(
for i, mask_strategy in enumerate(selected_masks): for i, mask_strategy in enumerate(selected_masks):
mask_strategy_str = str(mask_strategy) mask_strategy_str = str(mask_strategy)
# Process L2 loss # Process L2 loss
step_results: list[list[float]] = [] step_results: List[List[float]] = []
for step_data in mask_search_final_result: for step_data in mask_search_final_result:
if isinstance(step_data, dict) and "L2_loss" in step_data: if isinstance(step_data, dict) and "L2_loss" in step_data:
layer_losses = [float(loss) for loss in step_data["L2_loss"]] layer_losses = [float(loss) for loss in step_data["L2_loss"]]
+8 -6
View File
@@ -3,7 +3,8 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass, fields from dataclasses import dataclass, fields
from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeVar from typing import (TYPE_CHECKING, Any, Dict, Generic, Optional, Protocol, Set,
Type, TypeVar)
if TYPE_CHECKING: if TYPE_CHECKING:
from fastvideo.v1.fastvideo_args import FastVideoArgs from fastvideo.v1.fastvideo_args import FastVideoArgs
@@ -26,12 +27,12 @@ class AttentionBackend(ABC):
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def get_impl_cls() -> type["AttentionImpl"]: def get_impl_cls() -> Type["AttentionImpl"]:
raise NotImplementedError raise NotImplementedError
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def get_metadata_cls() -> type["AttentionMetadata"]: def get_metadata_cls() -> Type["AttentionMetadata"]:
raise NotImplementedError raise NotImplementedError
# @staticmethod # @staticmethod
@@ -45,7 +46,7 @@ class AttentionBackend(ABC):
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def get_builder_cls() -> type["AttentionMetadataBuilder"]: def get_builder_cls() -> Type["AttentionMetadataBuilder"]:
raise NotImplementedError raise NotImplementedError
@@ -56,7 +57,8 @@ class AttentionMetadata:
current_timestep: int current_timestep: int
def asdict_zerocopy(self, def asdict_zerocopy(self,
skip_fields: set[str] | None = None) -> dict[str, Any]: skip_fields: Optional[Set[str]] = None
) -> Dict[str, Any]:
"""Similar to dataclasses.asdict, but avoids deepcopying.""" """Similar to dataclasses.asdict, but avoids deepcopying."""
if skip_fields is None: if skip_fields is None:
skip_fields = set() skip_fields = set()
@@ -122,7 +124,7 @@ class AttentionImpl(ABC, Generic[T]):
head_size: int, head_size: int,
softmax_scale: float, softmax_scale: float,
causal: bool = False, causal: bool = False,
num_kv_heads: int | None = None, num_kv_heads: Optional[int] = None,
prefix: str = "", prefix: str = "",
**extra_impl_args, **extra_impl_args,
) -> None: ) -> None:
@@ -1,5 +1,7 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from typing import List, Optional, Type
import torch import torch
from flash_attn import flash_attn_func as flash_attn_2_func from flash_attn import flash_attn_func as flash_attn_2_func
@@ -26,7 +28,7 @@ class FlashAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True accept_output_buffer: bool = True
@staticmethod @staticmethod
def get_supported_head_sizes() -> list[int]: def get_supported_head_sizes() -> List[int]:
return [32, 64, 96, 128, 160, 192, 224, 256] return [32, 64, 96, 128, 160, 192, 224, 256]
@staticmethod @staticmethod
@@ -34,15 +36,15 @@ class FlashAttentionBackend(AttentionBackend):
return "FLASH_ATTN" return "FLASH_ATTN"
@staticmethod @staticmethod
def get_impl_cls() -> type["FlashAttentionImpl"]: def get_impl_cls() -> Type["FlashAttentionImpl"]:
return FlashAttentionImpl return FlashAttentionImpl
@staticmethod @staticmethod
def get_metadata_cls() -> type["AttentionMetadata"]: def get_metadata_cls() -> Type["AttentionMetadata"]:
raise NotImplementedError raise NotImplementedError
@staticmethod @staticmethod
def get_builder_cls() -> type["AttentionMetadataBuilder"]: def get_builder_cls() -> Type["AttentionMetadataBuilder"]:
raise NotImplementedError raise NotImplementedError
@@ -54,7 +56,7 @@ class FlashAttentionImpl(AttentionImpl):
head_size: int, head_size: int,
causal: bool, causal: bool,
softmax_scale: float, softmax_scale: float,
num_kv_heads: int | None = None, num_kv_heads: Optional[int] = None,
prefix: str = "", prefix: str = "",
**extra_impl_args, **extra_impl_args,
) -> None: ) -> None:
+4 -3
View File
@@ -1,4 +1,5 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from typing import List, Optional, Type
import torch import torch
from sageattention import sageattn from sageattention import sageattn
@@ -17,7 +18,7 @@ class SageAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True accept_output_buffer: bool = True
@staticmethod @staticmethod
def get_supported_head_sizes() -> list[int]: def get_supported_head_sizes() -> List[int]:
return [32, 64, 96, 128, 160, 192, 224, 256] return [32, 64, 96, 128, 160, 192, 224, 256]
@staticmethod @staticmethod
@@ -25,7 +26,7 @@ class SageAttentionBackend(AttentionBackend):
return "SAGE_ATTN" return "SAGE_ATTN"
@staticmethod @staticmethod
def get_impl_cls() -> type["SageAttentionImpl"]: def get_impl_cls() -> Type["SageAttentionImpl"]:
return SageAttentionImpl return SageAttentionImpl
# @staticmethod # @staticmethod
@@ -41,7 +42,7 @@ class SageAttentionImpl(AttentionImpl):
head_size: int, head_size: int,
causal: bool, causal: bool,
softmax_scale: float, softmax_scale: float,
num_kv_heads: int | None = None, num_kv_heads: Optional[int] = None,
prefix: str = "", prefix: str = "",
**extra_impl_args, **extra_impl_args,
) -> None: ) -> None:
+4 -3
View File
@@ -1,4 +1,5 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from typing import List, Optional, Type
import torch import torch
@@ -16,7 +17,7 @@ class SDPABackend(AttentionBackend):
accept_output_buffer: bool = True accept_output_buffer: bool = True
@staticmethod @staticmethod
def get_supported_head_sizes() -> list[int]: def get_supported_head_sizes() -> List[int]:
return [32, 64, 96, 128, 160, 192, 224, 256] return [32, 64, 96, 128, 160, 192, 224, 256]
@staticmethod @staticmethod
@@ -24,7 +25,7 @@ class SDPABackend(AttentionBackend):
return "SDPA" return "SDPA"
@staticmethod @staticmethod
def get_impl_cls() -> type["SDPAImpl"]: def get_impl_cls() -> Type["SDPAImpl"]:
return SDPAImpl return SDPAImpl
# @staticmethod # @staticmethod
@@ -40,7 +41,7 @@ class SDPAImpl(AttentionImpl):
head_size: int, head_size: int,
causal: bool, causal: bool,
softmax_scale: float, softmax_scale: float,
num_kv_heads: int | None = None, num_kv_heads: Optional[int] = None,
prefix: str = "", prefix: str = "",
**extra_impl_args, **extra_impl_args,
) -> None: ) -> None:
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
import json import json
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any, List, Optional, Type
import torch import torch
from einops import rearrange from einops import rearrange
@@ -40,7 +40,7 @@ class SlidingTileAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True accept_output_buffer: bool = True
@staticmethod @staticmethod
def get_supported_head_sizes() -> list[int]: def get_supported_head_sizes() -> List[int]:
# TODO(will-refactor): check this # TODO(will-refactor): check this
return [32, 64, 96, 128, 160, 192, 224, 256] return [32, 64, 96, 128, 160, 192, 224, 256]
@@ -49,22 +49,22 @@ class SlidingTileAttentionBackend(AttentionBackend):
return "SLIDING_TILE_ATTN" return "SLIDING_TILE_ATTN"
@staticmethod @staticmethod
def get_impl_cls() -> type["SlidingTileAttentionImpl"]: def get_impl_cls() -> Type["SlidingTileAttentionImpl"]:
return SlidingTileAttentionImpl return SlidingTileAttentionImpl
@staticmethod @staticmethod
def get_metadata_cls() -> type["SlidingTileAttentionMetadata"]: def get_metadata_cls() -> Type["SlidingTileAttentionMetadata"]:
return SlidingTileAttentionMetadata return SlidingTileAttentionMetadata
@staticmethod @staticmethod
def get_builder_cls() -> type["SlidingTileAttentionMetadataBuilder"]: def get_builder_cls() -> Type["SlidingTileAttentionMetadataBuilder"]:
return SlidingTileAttentionMetadataBuilder return SlidingTileAttentionMetadataBuilder
@dataclass @dataclass
class SlidingTileAttentionMetadata(AttentionMetadata): class SlidingTileAttentionMetadata(AttentionMetadata):
current_timestep: int current_timestep: int
STA_param: list[list[ STA_param: List[List[
Any]] # each timestep with one metadata, shape [num_layers, num_heads] Any]] # each timestep with one metadata, shape [num_layers, num_heads]
@@ -98,7 +98,7 @@ class SlidingTileAttentionImpl(AttentionImpl):
head_size: int, head_size: int,
causal: bool, causal: bool,
softmax_scale: float, softmax_scale: float,
num_kv_heads: int | None = None, num_kv_heads: Optional[int] = None,
prefix: str = "", prefix: str = "",
**extra_impl_args, **extra_impl_args,
) -> None: ) -> None:
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
import math import math
from dataclasses import dataclass from dataclasses import dataclass
from typing import List, Optional, Type
import torch import torch
from einops import rearrange from einops import rearrange
@@ -27,7 +28,7 @@ class VideoSparseAttentionBackend(AttentionBackend):
accept_output_buffer: bool = True accept_output_buffer: bool = True
@staticmethod @staticmethod
def get_supported_head_sizes() -> list[int]: def get_supported_head_sizes() -> List[int]:
return [64, 128] return [64, 128]
@staticmethod @staticmethod
@@ -35,22 +36,22 @@ class VideoSparseAttentionBackend(AttentionBackend):
return "VIDEO_SPARSE_ATTN" return "VIDEO_SPARSE_ATTN"
@staticmethod @staticmethod
def get_impl_cls() -> type["VideoSparseAttentionImpl"]: def get_impl_cls() -> Type["VideoSparseAttentionImpl"]:
return VideoSparseAttentionImpl return VideoSparseAttentionImpl
@staticmethod @staticmethod
def get_metadata_cls() -> type["VideoSparseAttentionMetadata"]: def get_metadata_cls() -> Type["VideoSparseAttentionMetadata"]:
return VideoSparseAttentionMetadata return VideoSparseAttentionMetadata
@staticmethod @staticmethod
def get_builder_cls() -> type["VideoSparseAttentionMetadataBuilder"]: def get_builder_cls() -> Type["VideoSparseAttentionMetadataBuilder"]:
return VideoSparseAttentionMetadataBuilder return VideoSparseAttentionMetadataBuilder
@dataclass @dataclass
class VideoSparseAttentionMetadata(AttentionMetadata): class VideoSparseAttentionMetadata(AttentionMetadata):
current_timestep: int current_timestep: int
dit_seq_shape: list[int] dit_seq_shape: List[int]
VSA_sparsity: float VSA_sparsity: float
@@ -96,7 +97,7 @@ class VideoSparseAttentionImpl(AttentionImpl):
head_size: int, head_size: int,
causal: bool, causal: bool,
softmax_scale: float, softmax_scale: float,
num_kv_heads: int | None = None, num_kv_heads: Optional[int] = None,
prefix: str = "", prefix: str = "",
**extra_impl_args, **extra_impl_args,
) -> None: ) -> None:
@@ -104,8 +105,8 @@ class VideoSparseAttentionImpl(AttentionImpl):
sp_group = get_sp_group() sp_group = get_sp_group()
self.sp_size = sp_group.world_size self.sp_size = sp_group.world_size
self.VSA_base_tile_size = [4, 4, 4] self.VSA_base_tile_size = [4, 4, 4]
self.dit_seq_shape: list[int] self.dit_seq_shape: List[int]
self.full_window_size: list[int] self.full_window_size: List[int]
self.img_seq_length: int self.img_seq_length: int
def tile(self, x: torch.Tensor) -> torch.Tensor: def tile(self, x: torch.Tensor) -> torch.Tensor:
+19 -17
View File
@@ -1,5 +1,7 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from typing import Optional, Tuple
import torch import torch
import torch.nn as nn import torch.nn as nn
@@ -21,11 +23,11 @@ class DistributedAttention(nn.Module):
def __init__(self, def __init__(self,
num_heads: int, num_heads: int,
head_size: int, head_size: int,
num_kv_heads: int | None = None, num_kv_heads: Optional[int] = None,
softmax_scale: float | None = None, softmax_scale: Optional[float] = None,
causal: bool = False, causal: bool = False,
supported_attention_backends: tuple[AttentionBackendEnum, ...] supported_attention_backends: Optional[Tuple[
| None = None, AttentionBackendEnum, ...]] = None,
prefix: str = "", prefix: str = "",
**extra_impl_args) -> None: **extra_impl_args) -> None:
super().__init__() super().__init__()
@@ -61,10 +63,10 @@ class DistributedAttention(nn.Module):
q: torch.Tensor, q: torch.Tensor,
k: torch.Tensor, k: torch.Tensor,
v: torch.Tensor, v: torch.Tensor,
replicated_q: torch.Tensor | None = None, replicated_q: Optional[torch.Tensor] = None,
replicated_k: torch.Tensor | None = None, replicated_k: Optional[torch.Tensor] = None,
replicated_v: torch.Tensor | None = None, replicated_v: Optional[torch.Tensor] = None,
) -> tuple[torch.Tensor, torch.Tensor | None]: ) -> tuple[torch.Tensor, Optional[torch.Tensor]]:
"""Forward pass for distributed attention. """Forward pass for distributed attention.
Args: Args:
@@ -142,11 +144,11 @@ class DistributedAttention_VSA(DistributedAttention):
q: torch.Tensor, q: torch.Tensor,
k: torch.Tensor, k: torch.Tensor,
v: torch.Tensor, v: torch.Tensor,
replicated_q: torch.Tensor | None = None, replicated_q: Optional[torch.Tensor] = None,
replicated_k: torch.Tensor | None = None, replicated_k: Optional[torch.Tensor] = None,
replicated_v: torch.Tensor | None = None, replicated_v: Optional[torch.Tensor] = None,
gate_compress: torch.Tensor | None = None, gate_compress: Optional[torch.Tensor] = None,
) -> tuple[torch.Tensor, torch.Tensor | None]: ) -> tuple[torch.Tensor, Optional[torch.Tensor]]:
"""Forward pass for distributed attention. """Forward pass for distributed attention.
Args: Args:
@@ -206,11 +208,11 @@ class LocalAttention(nn.Module):
def __init__(self, def __init__(self,
num_heads: int, num_heads: int,
head_size: int, head_size: int,
num_kv_heads: int | None = None, num_kv_heads: Optional[int] = None,
softmax_scale: float | None = None, softmax_scale: Optional[float] = None,
causal: bool = False, causal: bool = False,
supported_attention_backends: tuple[AttentionBackendEnum, ...] supported_attention_backends: Optional[Tuple[
| None = None, AttentionBackendEnum, ...]] = None,
**extra_impl_args) -> None: **extra_impl_args) -> None:
super().__init__() super().__init__()
if softmax_scale is None: if softmax_scale is None:
+15 -16
View File
@@ -2,10 +2,9 @@
# Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/attention/selector.py # Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/attention/selector.py
import os import os
from collections.abc import Generator
from contextlib import contextmanager from contextlib import contextmanager
from functools import cache from functools import cache
from typing import cast from typing import Generator, Optional, Tuple, Type, cast
import torch import torch
@@ -18,7 +17,7 @@ from fastvideo.v1.utils import STR_BACKEND_ENV_VAR, resolve_obj_by_qualname
logger = init_logger(__name__) logger = init_logger(__name__)
def backend_name_to_enum(backend_name: str) -> AttentionBackendEnum | None: def backend_name_to_enum(backend_name: str) -> Optional[AttentionBackendEnum]:
""" """
Convert a string backend name to a _Backend enum value. Convert a string backend name to a _Backend enum value.
@@ -32,7 +31,7 @@ def backend_name_to_enum(backend_name: str) -> AttentionBackendEnum | None:
None None
def get_env_variable_attn_backend() -> AttentionBackendEnum | None: def get_env_variable_attn_backend() -> Optional[AttentionBackendEnum]:
''' '''
Get the backend override specified by the FastVideo attention Get the backend override specified by the FastVideo attention
backend environment variable, if one is specified. backend environment variable, if one is specified.
@@ -54,11 +53,11 @@ def get_env_variable_attn_backend() -> AttentionBackendEnum | None:
# #
# THIS SELECTION TAKES PRECEDENCE OVER THE # THIS SELECTION TAKES PRECEDENCE OVER THE
# FASTVIDEO ATTENTION BACKEND ENVIRONMENT VARIABLE # FASTVIDEO ATTENTION BACKEND ENVIRONMENT VARIABLE
forced_attn_backend: AttentionBackendEnum | None = None forced_attn_backend: Optional[AttentionBackendEnum] = None
def global_force_attn_backend( def global_force_attn_backend(
attn_backend: AttentionBackendEnum | None) -> None: attn_backend: Optional[AttentionBackendEnum]) -> None:
''' '''
Force all attention operations to use a specified backend. Force all attention operations to use a specified backend.
@@ -73,7 +72,7 @@ def global_force_attn_backend(
forced_attn_backend = attn_backend forced_attn_backend = attn_backend
def get_global_forced_attn_backend() -> AttentionBackendEnum | None: def get_global_forced_attn_backend() -> Optional[AttentionBackendEnum]:
''' '''
Get the currently-forced choice of attention backend, Get the currently-forced choice of attention backend,
or None if auto-selection is currently enabled. or None if auto-selection is currently enabled.
@@ -84,9 +83,9 @@ def get_global_forced_attn_backend() -> AttentionBackendEnum | None:
def get_attn_backend( def get_attn_backend(
head_size: int, head_size: int,
dtype: torch.dtype, dtype: torch.dtype,
supported_attention_backends: tuple[AttentionBackendEnum, ...] supported_attention_backends: Optional[Tuple[AttentionBackendEnum,
| None = None, ...]] = None,
) -> type[AttentionBackend]: ) -> Type[AttentionBackend]:
return _cached_get_attn_backend(head_size, dtype, return _cached_get_attn_backend(head_size, dtype,
supported_attention_backends) supported_attention_backends)
@@ -95,9 +94,9 @@ def get_attn_backend(
def _cached_get_attn_backend( def _cached_get_attn_backend(
head_size: int, head_size: int,
dtype: torch.dtype, dtype: torch.dtype,
supported_attention_backends: tuple[AttentionBackendEnum, ...] supported_attention_backends: Optional[Tuple[AttentionBackendEnum,
| None = None, ...]] = None,
) -> type[AttentionBackend]: ) -> Type[AttentionBackend]:
# Check whether a particular choice of backend was # Check whether a particular choice of backend was
# previously forced. # previously forced.
# #
@@ -106,13 +105,13 @@ def _cached_get_attn_backend(
if not supported_attention_backends: if not supported_attention_backends:
raise ValueError("supported_attention_backends is empty") raise ValueError("supported_attention_backends is empty")
selected_backend = None selected_backend = None
backend_by_global_setting: AttentionBackendEnum | None = ( backend_by_global_setting: Optional[AttentionBackendEnum] = (
get_global_forced_attn_backend()) get_global_forced_attn_backend())
if backend_by_global_setting is not None: if backend_by_global_setting is not None:
selected_backend = backend_by_global_setting selected_backend = backend_by_global_setting
else: else:
# Check the environment variable and override if specified # Check the environment variable and override if specified
backend_by_env_var: str | None = envs.FASTVIDEO_ATTENTION_BACKEND backend_by_env_var: Optional[str] = envs.FASTVIDEO_ATTENTION_BACKEND
if backend_by_env_var is not None: if backend_by_env_var is not None:
selected_backend = backend_name_to_enum(backend_by_env_var) selected_backend = backend_name_to_enum(backend_by_env_var)
@@ -124,7 +123,7 @@ def _cached_get_attn_backend(
if not attention_cls: if not attention_cls:
raise ValueError( raise ValueError(
f"Invalid attention backend for {current_platform.device_name}") f"Invalid attention backend for {current_platform.device_name}")
return cast(type[AttentionBackend], resolve_obj_by_qualname(attention_cls)) return cast(Type[AttentionBackend], resolve_obj_by_qualname(attention_cls))
@contextmanager @contextmanager
+4 -6
View File
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass, field, fields from dataclasses import dataclass, field, fields
from typing import Any from typing import Any, Dict
from fastvideo.v1.logger import init_logger from fastvideo.v1.logger import init_logger
@@ -12,9 +12,7 @@ logger = init_logger(__name__)
# 3. Any field in ArchConfig is fixed upon initialization, and should be hidden away from users # 3. Any field in ArchConfig is fixed upon initialization, and should be hidden away from users
@dataclass @dataclass
class ArchConfig: class ArchConfig:
stacked_params_mapping: list[tuple[str, str, str]] = field( pass
default_factory=list
) # mapping from huggingface weight names to custom names
@dataclass @dataclass
@@ -44,7 +42,7 @@ class ModelConfig:
self.__dict__.update(state) self.__dict__.update(state)
# This should be used only when loading from transformers/diffusers # This should be used only when loading from transformers/diffusers
def update_model_arch(self, source_model_dict: dict[str, Any]) -> None: def update_model_arch(self, source_model_dict: Dict[str, Any]) -> None:
arch_config = self.arch_config arch_config = self.arch_config
valid_fields = {f.name for f in fields(arch_config)} valid_fields = {f.name for f in fields(arch_config)}
@@ -58,7 +56,7 @@ class ModelConfig:
if hasattr(arch_config, "__post_init__"): if hasattr(arch_config, "__post_init__"):
arch_config.__post_init__() arch_config.__post_init__()
def update_model_config(self, source_model_dict: dict[str, Any]) -> None: def update_model_config(self, source_model_dict: Dict[str, Any]) -> None:
assert "arch_config" not in source_model_dict, "Source model config shouldn't contain arch_config." assert "arch_config" not in source_model_dict, "Source model config shouldn't contain arch_config."
valid_fields = {f.name for f in fields(self)} valid_fields = {f.name for f in fields(self)}
+4 -5
View File
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any from typing import Any, List, Optional, Tuple
from fastvideo.v1.configs.models.base import ArchConfig, ModelConfig from fastvideo.v1.configs.models.base import ArchConfig, ModelConfig
from fastvideo.v1.layers.quantization import QuantizationConfig from fastvideo.v1.layers.quantization import QuantizationConfig
@@ -12,9 +12,8 @@ class DiTArchConfig(ArchConfig):
_fsdp_shard_conditions: list = field(default_factory=list) _fsdp_shard_conditions: list = field(default_factory=list)
_compile_conditions: list = field(default_factory=list) _compile_conditions: list = field(default_factory=list)
_param_names_mapping: dict = field(default_factory=dict) _param_names_mapping: dict = field(default_factory=dict)
_reverse_param_names_mapping: dict = field(default_factory=dict)
_lora_param_names_mapping: dict = field(default_factory=dict) _lora_param_names_mapping: dict = field(default_factory=dict)
_supported_attention_backends: tuple[AttentionBackendEnum, ...] = ( _supported_attention_backends: Tuple[AttentionBackendEnum, ...] = (
AttentionBackendEnum.SLIDING_TILE_ATTN, AttentionBackendEnum.SAGE_ATTN, AttentionBackendEnum.SLIDING_TILE_ATTN, AttentionBackendEnum.SAGE_ATTN,
AttentionBackendEnum.FLASH_ATTN, AttentionBackendEnum.TORCH_SDPA, AttentionBackendEnum.FLASH_ATTN, AttentionBackendEnum.TORCH_SDPA,
AttentionBackendEnum.VIDEO_SPARSE_ATTN) AttentionBackendEnum.VIDEO_SPARSE_ATTN)
@@ -22,7 +21,7 @@ class DiTArchConfig(ArchConfig):
hidden_size: int = 0 hidden_size: int = 0
num_attention_heads: int = 0 num_attention_heads: int = 0
num_channels_latents: int = 0 num_channels_latents: int = 0
exclude_lora_layers: list[str] = field(default_factory=list) exclude_lora_layers: List[str] = field(default_factory=list)
def __post_init__(self) -> None: def __post_init__(self) -> None:
if not self._compile_conditions: if not self._compile_conditions:
@@ -35,7 +34,7 @@ class DiTConfig(ModelConfig):
# FastVideoDiT-specific parameters # FastVideoDiT-specific parameters
prefix: str = "" prefix: str = ""
quant_config: QuantizationConfig | None = None quant_config: Optional[QuantizationConfig] = None
@staticmethod @staticmethod
def add_cli_args(parser: Any, prefix: str = "dit-config") -> Any: def add_cli_args(parser: Any, prefix: str = "dit-config") -> Any:
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List, Optional, Tuple
import torch import torch
@@ -146,9 +147,6 @@ class HunyuanVideoArchConfig(DiTArchConfig):
r"final_layer.linear.\1", r"final_layer.linear.\1",
}) })
# Reverse mapping for saving checkpoints: training -> diffusers
_reverse_param_names_mapping: dict = field(default_factory=lambda: {})
patch_size: int = 2 patch_size: int = 2
patch_size_t: int = 1 patch_size_t: int = 1
in_channels: int = 16 in_channels: int = 16
@@ -159,14 +157,14 @@ class HunyuanVideoArchConfig(DiTArchConfig):
num_layers: int = 20 num_layers: int = 20
num_single_layers: int = 40 num_single_layers: int = 40
num_refiner_layers: int = 2 num_refiner_layers: int = 2
rope_axes_dim: tuple[int, int, int] = (16, 56, 56) rope_axes_dim: Tuple[int, int, int] = (16, 56, 56)
guidance_embeds: bool = False guidance_embeds: bool = False
dtype: torch.dtype | None = None dtype: Optional[torch.dtype] = None
text_embed_dim: int = 4096 text_embed_dim: int = 4096
pooled_projection_dim: int = 768 pooled_projection_dim: int = 768
rope_theta: int = 256 rope_theta: int = 256
qk_norm: str = "rms_norm" qk_norm: str = "rms_norm"
exclude_lora_layers: list[str] = field( exclude_lora_layers: List[str] = field(
default_factory=lambda: ["img_in", "txt_in", "time_in", "vector_in"]) default_factory=lambda: ["img_in", "txt_in", "time_in", "vector_in"])
def __post_init__(self): def __post_init__(self):
+11 -8
View File
@@ -1,14 +1,17 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List, Optional, Tuple, Union
from fastvideo.v1.configs.models.dits.base import DiTArchConfig, DiTConfig from fastvideo.v1.configs.models.dits.base import DiTArchConfig, DiTConfig
def is_blocks(n: str, m) -> bool:
return "blocks" in n and str.isdigit(n.split(".")[-1])
@dataclass @dataclass
class StepVideoArchConfig(DiTArchConfig): class StepVideoArchConfig(DiTArchConfig):
_fsdp_shard_conditions: list = field( _fsdp_shard_conditions: list = field(default_factory=lambda: [is_blocks])
default_factory=lambda:
[lambda n, m: "transformer_blocks" in n and n.split(".")[-1].isdigit()])
_param_names_mapping: dict = field( _param_names_mapping: dict = field(
default_factory=lambda: { default_factory=lambda: {
@@ -38,18 +41,18 @@ class StepVideoArchConfig(DiTArchConfig):
num_attention_heads: int = 48 num_attention_heads: int = 48
attention_head_dim: int = 128 attention_head_dim: int = 128
in_channels: int = 64 in_channels: int = 64
out_channels: int | None = 64 out_channels: Optional[int] = 64
num_layers: int = 48 num_layers: int = 48
dropout: float = 0.0 dropout: float = 0.0
patch_size: int = 1 patch_size: int = 1
norm_type: str = "ada_norm_single" norm_type: str = "ada_norm_single"
norm_elementwise_affine: bool = False norm_elementwise_affine: bool = False
norm_eps: float = 1e-6 norm_eps: float = 1e-6
caption_channels: int | list[int] | tuple[int, ...] | None = field( caption_channels: Optional[Union[int, List[int], Tuple[int, ...]]] = field(
default_factory=lambda: [6144, 1024]) default_factory=lambda: [6144, 1024])
attention_type: str | None = "torch" attention_type: Optional[str] = "torch"
use_additional_conditions: bool | None = False use_additional_conditions: Optional[bool] = False
exclude_lora_layers: list[str] = field(default_factory=lambda: []) exclude_lora_layers: List[str] = field(default_factory=lambda: [])
def __post_init__(self): def __post_init__(self):
self.hidden_size = self.num_attention_heads * self.attention_head_dim self.hidden_size = self.num_attention_heads * self.attention_head_dim
+6 -9
View File
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import List, Optional, Tuple
from fastvideo.v1.configs.models.dits.base import DiTArchConfig, DiTConfig from fastvideo.v1.configs.models.dits.base import DiTArchConfig, DiTConfig
@@ -48,13 +49,9 @@ class WanVideoArchConfig(DiTArchConfig):
r"blocks.\1.ffn.fc_in.\2", r"blocks.\1.ffn.fc_in.\2",
r"^blocks\.(\d+)\.ffn\.net\.2\.(.*)$": r"^blocks\.(\d+)\.ffn\.net\.2\.(.*)$":
r"blocks.\1.ffn.fc_out.\2", r"blocks.\1.ffn.fc_out.\2",
r"^blocks\.(\d+)\.norm2\.(.*)$": r"blocks\.(\d+)\.norm2\.(.*)$":
r"blocks.\1.self_attn_residual_norm.norm.\2", r"blocks.\1.self_attn_residual_norm.norm.\2",
}) })
# Reverse mapping for saving checkpoints: training -> diffusers
_reverse_param_names_mapping: dict = field(default_factory=lambda: {})
# Some LoRA adapters use the original official layer names instead of hf layer names, # Some LoRA adapters use the original official layer names instead of hf layer names,
# so apply this before the param_names_mapping # so apply this before the param_names_mapping
_lora_param_names_mapping: dict = field( _lora_param_names_mapping: dict = field(
@@ -73,7 +70,7 @@ class WanVideoArchConfig(DiTArchConfig):
r"^blocks\.(\d+)\.ffn\.2\.(.*)$": r"blocks.\1.ffn.fc_out.\2", r"^blocks\.(\d+)\.ffn\.2\.(.*)$": r"blocks.\1.ffn.fc_out.\2",
}) })
patch_size: tuple[int, int, int] = (1, 2, 2) patch_size: Tuple[int, int, int] = (1, 2, 2)
text_len = 512 text_len = 512
num_attention_heads: int = 40 num_attention_heads: int = 40
attention_head_dim: int = 128 attention_head_dim: int = 128
@@ -86,10 +83,10 @@ class WanVideoArchConfig(DiTArchConfig):
cross_attn_norm: bool = True cross_attn_norm: bool = True
qk_norm: str = "rms_norm_across_heads" qk_norm: str = "rms_norm_across_heads"
eps: float = 1e-6 eps: float = 1e-6
image_dim: int | None = None image_dim: Optional[int] = None
added_kv_proj_dim: int | None = None added_kv_proj_dim: Optional[int] = None
rope_max_seq_len: int = 1024 rope_max_seq_len: int = 1024
exclude_lora_layers: list[str] = field(default_factory=lambda: ["embedder"]) exclude_lora_layers: List[str] = field(default_factory=lambda: ["embedder"])
def __post_init__(self): def __post_init__(self):
super().__post_init__() super().__post_init__()
+12 -15
View File
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any from typing import Any, Dict, List, Optional, Tuple
import torch import torch
@@ -11,8 +11,8 @@ from fastvideo.v1.platforms import AttentionBackendEnum
@dataclass @dataclass
class EncoderArchConfig(ArchConfig): class EncoderArchConfig(ArchConfig):
architectures: list[str] = field(default_factory=lambda: []) architectures: List[str] = field(default_factory=lambda: [])
_supported_attention_backends: tuple[AttentionBackendEnum, ...] = ( _supported_attention_backends: Tuple[AttentionBackendEnum, ...] = (
AttentionBackendEnum.FLASH_ATTN, AttentionBackendEnum.TORCH_SDPA) AttentionBackendEnum.FLASH_ATTN, AttentionBackendEnum.TORCH_SDPA)
output_hidden_states: bool = False output_hidden_states: bool = False
use_return_dict: bool = True use_return_dict: bool = True
@@ -32,11 +32,8 @@ class TextEncoderArchConfig(EncoderArchConfig):
output_past: bool = True output_past: bool = True
scalable_attention: bool = True scalable_attention: bool = True
tie_word_embeddings: bool = False tie_word_embeddings: bool = False
stacked_params_mapping: list[tuple[str, str, str]] = field(
default_factory=list tokenizer_kwargs: Dict[str, Any] = field(default_factory=dict)
) # mapping from huggingface weight names to custom names
tokenizer_kwargs: dict[str, Any] = field(default_factory=dict)
_fsdp_shard_conditions: list = field(default_factory=lambda: [])
def __post_init__(self) -> None: def __post_init__(self) -> None:
self.tokenizer_kwargs = { self.tokenizer_kwargs = {
@@ -53,11 +50,11 @@ class ImageEncoderArchConfig(EncoderArchConfig):
@dataclass @dataclass
class BaseEncoderOutput: class BaseEncoderOutput:
last_hidden_state: torch.FloatTensor | None = None last_hidden_state: Optional[torch.FloatTensor] = None
pooler_output: torch.FloatTensor | None = None pooler_output: Optional[torch.FloatTensor] = None
hidden_states: tuple[torch.FloatTensor, ...] | None = None hidden_states: Optional[Tuple[torch.FloatTensor, ...]] = None
attentions: tuple[torch.FloatTensor, ...] | None = None attentions: Optional[Tuple[torch.FloatTensor, ...]] = None
attention_mask: torch.Tensor | None = None attention_mask: Optional[torch.Tensor] = None
@dataclass @dataclass
@@ -65,8 +62,8 @@ class EncoderConfig(ModelConfig):
arch_config: ArchConfig = field(default_factory=EncoderArchConfig) arch_config: ArchConfig = field(default_factory=EncoderArchConfig)
prefix: str = "" prefix: str = ""
quant_config: QuantizationConfig | None = None quant_config: Optional[QuantizationConfig] = None
lora_config: Any | None = None lora_config: Optional[Any] = None
@dataclass @dataclass
+5 -28
View File
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional
from fastvideo.v1.configs.models.encoders.base import (ImageEncoderArchConfig, from fastvideo.v1.configs.models.encoders.base import (ImageEncoderArchConfig,
ImageEncoderConfig, ImageEncoderConfig,
@@ -7,14 +8,6 @@ from fastvideo.v1.configs.models.encoders.base import (ImageEncoderArchConfig,
TextEncoderConfig) TextEncoderConfig)
def _is_transformer_layer(n: str, m) -> bool:
return "layers" in n and str.isdigit(n.split(".")[-1])
def _is_embeddings(n: str, m) -> bool:
return n.endswith("embeddings")
@dataclass @dataclass
class CLIPTextArchConfig(TextEncoderArchConfig): class CLIPTextArchConfig(TextEncoderArchConfig):
vocab_size: int = 49408 vocab_size: int = 49408
@@ -34,15 +27,6 @@ class CLIPTextArchConfig(TextEncoderArchConfig):
bos_token_id: int = 49406 bos_token_id: int = 49406
eos_token_id: int = 49407 eos_token_id: int = 49407
text_len: int = 77 text_len: int = 77
stacked_params_mapping: list[tuple[str, str,
str]] = field(default_factory=lambda: [
# (param_name, shard_name, shard_id)
("qkv_proj", "q_proj", "q"),
("qkv_proj", "k_proj", "k"),
("qkv_proj", "v_proj", "v"),
])
_fsdp_shard_conditions: list = field(
default_factory=lambda: [_is_transformer_layer, _is_embeddings])
@dataclass @dataclass
@@ -61,13 +45,6 @@ class CLIPVisionArchConfig(ImageEncoderArchConfig):
attention_dropout: float = 0.0 attention_dropout: float = 0.0
initializer_range: float = 0.02 initializer_range: float = 0.02
initializer_factor: float = 1.0 initializer_factor: float = 1.0
stacked_params_mapping: list[tuple[str, str,
str]] = field(default_factory=lambda: [
# (param_name, shard_name, shard_id)
("qkv_proj", "q_proj", "q"),
("qkv_proj", "k_proj", "k"),
("qkv_proj", "v_proj", "v"),
])
@dataclass @dataclass
@@ -75,8 +52,8 @@ class CLIPTextConfig(TextEncoderConfig):
arch_config: TextEncoderArchConfig = field( arch_config: TextEncoderArchConfig = field(
default_factory=CLIPTextArchConfig) default_factory=CLIPTextArchConfig)
num_hidden_layers_override: int | None = None num_hidden_layers_override: Optional[int] = None
require_post_norm: bool | None = None require_post_norm: Optional[bool] = None
prefix: str = "clip" prefix: str = "clip"
@@ -85,6 +62,6 @@ class CLIPVisionConfig(ImageEncoderConfig):
arch_config: ImageEncoderArchConfig = field( arch_config: ImageEncoderArchConfig = field(
default_factory=CLIPVisionArchConfig) default_factory=CLIPVisionArchConfig)
num_hidden_layers_override: int | None = None num_hidden_layers_override: Optional[int] = None
require_post_norm: bool | None = None require_post_norm: Optional[bool] = None
prefix: str = "clip" prefix: str = "clip"
+4 -27
View File
@@ -1,22 +1,11 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional
from fastvideo.v1.configs.models.encoders.base import (TextEncoderArchConfig, from fastvideo.v1.configs.models.encoders.base import (TextEncoderArchConfig,
TextEncoderConfig) TextEncoderConfig)
def _is_transformer_layer(n: str, m) -> bool:
return "layers" in n and str.isdigit(n.split(".")[-1])
def _is_embeddings(n: str, m) -> bool:
return n.endswith("embed_tokens")
def _is_final_norm(n: str, m) -> bool:
return n.endswith("norm")
@dataclass @dataclass
class LlamaArchConfig(TextEncoderArchConfig): class LlamaArchConfig(TextEncoderArchConfig):
vocab_size: int = 32000 vocab_size: int = 32000
@@ -24,7 +13,7 @@ class LlamaArchConfig(TextEncoderArchConfig):
intermediate_size: int = 11008 intermediate_size: int = 11008
num_hidden_layers: int = 32 num_hidden_layers: int = 32
num_attention_heads: int = 32 num_attention_heads: int = 32
num_key_value_heads: int | None = None num_key_value_heads: Optional[int] = None
hidden_act: str = "silu" hidden_act: str = "silu"
max_position_embeddings: int = 2048 max_position_embeddings: int = 2048
initializer_range: float = 0.02 initializer_range: float = 0.02
@@ -36,25 +25,13 @@ class LlamaArchConfig(TextEncoderArchConfig):
pretraining_tp: int = 1 pretraining_tp: int = 1
tie_word_embeddings: bool = False tie_word_embeddings: bool = False
rope_theta: float = 10000.0 rope_theta: float = 10000.0
rope_scaling: float | None = None rope_scaling: Optional[float] = None
attention_bias: bool = False attention_bias: bool = False
attention_dropout: float = 0.0 attention_dropout: float = 0.0
mlp_bias: bool = False mlp_bias: bool = False
head_dim: int | None = None head_dim: Optional[int] = None
hidden_state_skip_layer: int = 2 hidden_state_skip_layer: int = 2
text_len: int = 256 text_len: int = 256
stacked_params_mapping: list[tuple[str, str, str]] = field(
default_factory=lambda: [
# (param_name, shard_name, shard_id)
(".qkv_proj", ".q_proj", "q"),
(".qkv_proj", ".k_proj", "k"),
(".qkv_proj", ".v_proj", "v"),
(".gate_up_proj", ".gate_proj", 0), # type: ignore
(".gate_up_proj", ".up_proj", 1), # type: ignore
])
_fsdp_shard_conditions: list = field(
default_factory=lambda:
[_is_transformer_layer, _is_embeddings, _is_final_norm])
@dataclass @dataclass
+2 -23
View File
@@ -1,22 +1,11 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional
from fastvideo.v1.configs.models.encoders.base import (TextEncoderArchConfig, from fastvideo.v1.configs.models.encoders.base import (TextEncoderArchConfig,
TextEncoderConfig) TextEncoderConfig)
def _is_transformer_layer(n: str, m) -> bool:
return "block" in n and str.isdigit(n.split(".")[-1])
def _is_embeddings(n: str, m) -> bool:
return n.endswith("shared")
def _is_final_layernorm(n: str, m) -> bool:
return n.endswith("final_layer_norm")
@dataclass @dataclass
class T5ArchConfig(TextEncoderArchConfig): class T5ArchConfig(TextEncoderArchConfig):
vocab_size: int = 32128 vocab_size: int = 32128
@@ -24,7 +13,7 @@ class T5ArchConfig(TextEncoderArchConfig):
d_kv: int = 64 d_kv: int = 64
d_ff: int = 2048 d_ff: int = 2048
num_layers: int = 6 num_layers: int = 6
num_decoder_layers: int | None = None num_decoder_layers: Optional[int] = None
num_heads: int = 8 num_heads: int = 8
relative_attention_num_buckets: int = 32 relative_attention_num_buckets: int = 32
relative_attention_max_distance: int = 128 relative_attention_max_distance: int = 128
@@ -40,16 +29,6 @@ class T5ArchConfig(TextEncoderArchConfig):
eos_token_id: int = 1 eos_token_id: int = 1
classifier_dropout: float = 0.0 classifier_dropout: float = 0.0
text_len: int = 512 text_len: int = 512
stacked_params_mapping: list[tuple[str, str,
str]] = field(default_factory=lambda: [
# (param_name, shard_name, shard_id)
(".qkv_proj", ".q", "q"),
(".qkv_proj", ".k", "k"),
(".qkv_proj", ".v", "v"),
])
_fsdp_shard_conditions: list = field(
default_factory=lambda:
[_is_transformer_layer, _is_embeddings, _is_final_layernorm])
# Referenced from https://github.com/huggingface/transformers/blob/main/src/transformers/models/t5/configuration_t5.py # Referenced from https://github.com/huggingface/transformers/blob/main/src/transformers/models/t5/configuration_t5.py
def __post_init__(self): def __post_init__(self):
+2 -2
View File
@@ -2,7 +2,7 @@
import argparse import argparse
import dataclasses import dataclasses
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any from typing import Any, Union
import torch import torch
@@ -12,7 +12,7 @@ from fastvideo.v1.utils import StoreBoolean
@dataclass @dataclass
class VAEArchConfig(ArchConfig): class VAEArchConfig(ArchConfig):
scaling_factor: float | torch.Tensor = 0 scaling_factor: Union[float, torch.tensor] = 0
temporal_compression_ratio: int = 4 temporal_compression_ratio: int = 4
spatial_compression_ratio: int = 8 spatial_compression_ratio: int = 8
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Tuple
from fastvideo.v1.configs.models.vaes.base import VAEArchConfig, VAEConfig from fastvideo.v1.configs.models.vaes.base import VAEArchConfig, VAEConfig
@@ -9,19 +10,19 @@ class HunyuanVAEArchConfig(VAEArchConfig):
in_channels: int = 3 in_channels: int = 3
out_channels: int = 3 out_channels: int = 3
latent_channels: int = 16 latent_channels: int = 16
down_block_types: tuple[str, ...] = ( down_block_types: Tuple[str, ...] = (
"HunyuanVideoDownBlock3D", "HunyuanVideoDownBlock3D",
"HunyuanVideoDownBlock3D", "HunyuanVideoDownBlock3D",
"HunyuanVideoDownBlock3D", "HunyuanVideoDownBlock3D",
"HunyuanVideoDownBlock3D", "HunyuanVideoDownBlock3D",
) )
up_block_types: tuple[str, ...] = ( up_block_types: Tuple[str, ...] = (
"HunyuanVideoUpBlock3D", "HunyuanVideoUpBlock3D",
"HunyuanVideoUpBlock3D", "HunyuanVideoUpBlock3D",
"HunyuanVideoUpBlock3D", "HunyuanVideoUpBlock3D",
"HunyuanVideoUpBlock3D", "HunyuanVideoUpBlock3D",
) )
block_out_channels: tuple[int, ...] = (128, 256, 512, 512) block_out_channels: Tuple[int, ...] = (128, 256, 512, 512)
layers_per_block: int = 2 layers_per_block: int = 2
act_fn: str = "silu" act_fn: str = "silu"
norm_num_groups: int = 32 norm_num_groups: int = 32
+6 -5
View File
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Tuple
import torch import torch
@@ -10,12 +11,12 @@ from fastvideo.v1.configs.models.vaes.base import VAEArchConfig, VAEConfig
class WanVAEArchConfig(VAEArchConfig): class WanVAEArchConfig(VAEArchConfig):
base_dim: int = 96 base_dim: int = 96
z_dim: int = 16 z_dim: int = 16
dim_mult: tuple[int, ...] = (1, 2, 4, 4) dim_mult: Tuple[int, ...] = (1, 2, 4, 4)
num_res_blocks: int = 2 num_res_blocks: int = 2
attn_scales: tuple[float, ...] = () attn_scales: Tuple[float, ...] = ()
temperal_downsample: tuple[bool, ...] = (False, True, True) temperal_downsample: Tuple[bool, ...] = (False, True, True)
dropout: float = 0.0 dropout: float = 0.0
latents_mean: tuple[float, ...] = ( latents_mean: Tuple[float, ...] = (
-0.7571, -0.7571,
-0.7089, -0.7089,
-0.9113, -0.9113,
@@ -33,7 +34,7 @@ class WanVAEArchConfig(VAEArchConfig):
0.2503, 0.2503,
-0.2921, -0.2921,
) )
latents_std: tuple[float, ...] = ( latents_std: Tuple[float, ...] = (
2.8184, 2.8184,
1.4541, 1.4541,
2.3275, 2.3275,
+27 -28
View File
@@ -1,9 +1,8 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
import json import json
from collections.abc import Callable
from dataclasses import asdict, dataclass, field, fields from dataclasses import asdict, dataclass, field, fields
from enum import Enum from enum import Enum
from typing import Any, cast from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
import torch import torch
@@ -40,11 +39,11 @@ def postprocess_text(output: BaseEncoderOutput) -> torch.tensor:
class PipelineConfig: class PipelineConfig:
"""Base configuration for all pipeline architectures.""" """Base configuration for all pipeline architectures."""
model_path: str = "" model_path: str = ""
pipeline_config_path: str | None = None pipeline_config_path: Optional[str] = None
# Video generation parameters # Video generation parameters
embedded_cfg_scale: float = 6.0 embedded_cfg_scale: float = 6.0
flow_shift: float | None = None flow_shift: Optional[float] = None
disable_autocast: bool = False disable_autocast: bool = False
# Model configuration # Model configuration
@@ -53,7 +52,7 @@ class PipelineConfig:
# VAE configuration # VAE configuration
vae_config: VAEConfig = field(default_factory=VAEConfig) vae_config: VAEConfig = field(default_factory=VAEConfig)
vae_precision: str = "fp32" vae_precision: str = "fp16"
vae_tiling: bool = True vae_tiling: bool = True
vae_sp: bool = True vae_sp: bool = True
@@ -63,29 +62,30 @@ class PipelineConfig:
# Text encoder configuration # Text encoder configuration
DEFAULT_TEXT_ENCODER_PRECISIONS = ("fp16", ) DEFAULT_TEXT_ENCODER_PRECISIONS = ("fp16", )
text_encoder_configs: tuple[EncoderConfig, ...] = field( text_encoder_configs: Tuple[EncoderConfig, ...] = field(
default_factory=lambda: (EncoderConfig(), )) default_factory=lambda: (EncoderConfig(), ))
text_encoder_precisions: tuple[str, ...] = field( text_encoder_precisions: Tuple[str, ...] = field(
default_factory=lambda: ("fp16", )) default_factory=lambda: ("fp16", ))
preprocess_text_funcs: tuple[Callable[[str], str], ...] = field( preprocess_text_funcs: Tuple[Callable[[str], str], ...] = field(
default_factory=lambda: (preprocess_text, )) default_factory=lambda: (preprocess_text, ))
postprocess_text_funcs: tuple[Callable[[BaseEncoderOutput], torch.tensor], postprocess_text_funcs: Tuple[Callable[[BaseEncoderOutput], torch.tensor],
...] = field(default_factory=lambda: ...] = field(default_factory=lambda:
(postprocess_text, )) (postprocess_text, ))
# LoRA parameters # LoRA parameters
lora_path: str | None = None lora_path: Optional[str] = None
lora_nickname: str | None = "default" # for swapping adapters in the pipeline lora_nickname: Optional[
lora_target_names: list[ str] = "default" # for swapping adapters in the pipeline
str] | None = None # can restrict list of layers to adapt, e.g. ["q_proj"] lora_target_names: Optional[List[
str]] = None # can restrict list of layers to adapt, e.g. ["q_proj"]
# StepVideo specific parameters # StepVideo specific parameters
pos_magic: str | None = None pos_magic: Optional[str] = None
neg_magic: str | None = None neg_magic: Optional[str] = None
timesteps_scale: bool | None = None timesteps_scale: Optional[bool] = None
# STA (Sliding Tile Attention) parameters # STA (Sliding Tile Attention) parameters
mask_strategy_file_path: str | None = None mask_strategy_file_path: Optional[str] = None
STA_mode: STA_Mode = STA_Mode.STA_INFERENCE STA_mode: STA_Mode = STA_Mode.STA_INFERENCE
skip_time_steps: int = 15 skip_time_steps: int = 15
@@ -217,7 +217,7 @@ class PipelineConfig:
return parser return parser
def update_config_from_dict(self, def update_config_from_dict(self,
args: dict[str, Any], args: Dict[str, Any],
prefix: str = "") -> None: prefix: str = "") -> None:
prefix_with_dot = f"{prefix}." if (prefix.strip() != "") else "" prefix_with_dot = f"{prefix}." if (prefix.strip() != "") else ""
update_config_from_args(self, args, prefix, pop_args=True) update_config_from_args(self, args, prefix, pop_args=True)
@@ -243,7 +243,7 @@ class PipelineConfig:
@classmethod @classmethod
def from_kwargs(cls, def from_kwargs(cls,
kwargs: dict[str, Any], kwargs: Dict[str, Any],
config_cli_prefix: str = "") -> "PipelineConfig": config_cli_prefix: str = "") -> "PipelineConfig":
""" """
Load PipelineConfig from kwargs Dictionary. Load PipelineConfig from kwargs Dictionary.
@@ -255,11 +255,11 @@ class PipelineConfig:
prefix_with_dot = f"{config_cli_prefix}." if (config_cli_prefix.strip() prefix_with_dot = f"{config_cli_prefix}." if (config_cli_prefix.strip()
!= "") else "" != "") else ""
model_path: str | None = kwargs.get(prefix_with_dot + 'model_path', model_path: Optional[str] = kwargs.get(prefix_with_dot + 'model_path',
None) or kwargs.get('model_path') None) or kwargs.get('model_path')
pipeline_config_or_path: str | PipelineConfig | dict[ pipeline_config_or_path: Optional[Union[str, PipelineConfig, Dict[
str, Any] | None = kwargs.get(prefix_with_dot + 'pipeline_config', str, Any]]] = kwargs.get(prefix_with_dot + 'pipeline_config',
None) or kwargs.get('pipeline_config') None) or kwargs.get('pipeline_config')
if model_path is None: if model_path is None:
raise ValueError("model_path is required in kwargs") raise ValueError("model_path is required in kwargs")
@@ -344,7 +344,7 @@ class PipelineConfig:
input_pipeline_dict = json.load(f) input_pipeline_dict = json.load(f)
self.update_pipeline_config(input_pipeline_dict) self.update_pipeline_config(input_pipeline_dict)
def update_pipeline_config(self, source_pipeline_dict: dict[str, def update_pipeline_config(self, source_pipeline_dict: Dict[str,
Any]) -> None: Any]) -> None:
for f in fields(self): for f in fields(self):
key = f.name key = f.name
@@ -360,9 +360,8 @@ class PipelineConfig:
assert len(current_value) == len( assert len(current_value) == len(
new_value new_value
), "Users shouldn't delete or add text encoder config objects in your json" ), "Users shouldn't delete or add text encoder config objects in your json"
for target_config, source_config in zip(current_value, for target_config, source_config in zip(
new_value, current_value, new_value):
strict=True):
target_config.update_model_config(source_config) target_config.update_model_config(source_config)
else: else:
setattr(self, key, new_value) setattr(self, key, new_value)
+6 -7
View File
@@ -1,7 +1,6 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from collections.abc import Callable
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import TypedDict from typing import Callable, Tuple, TypedDict
import torch import torch
@@ -40,7 +39,7 @@ def llama_preprocess_text(prompt: str) -> str:
def llama_postprocess_text(outputs: BaseEncoderOutput) -> torch.tensor: def llama_postprocess_text(outputs: BaseEncoderOutput) -> torch.tensor:
hidden_state_skip_layer = 2 hidden_state_skip_layer = 2
assert outputs.hidden_states is not None assert outputs.hidden_states is not None
hidden_states: tuple[torch.Tensor, ...] = outputs.hidden_states hidden_states: Tuple[torch.Tensor, ...] = outputs.hidden_states
last_hidden_state: torch.tensor = hidden_states[-(hidden_state_skip_layer + last_hidden_state: torch.tensor = hidden_states[-(hidden_state_skip_layer +
1)] 1)]
crop_start = prompt_template_video.get("crop_start", -1) crop_start = prompt_template_video.get("crop_start", -1)
@@ -71,11 +70,11 @@ class HunyuanConfig(PipelineConfig):
flow_shift: int = 7 flow_shift: int = 7
# Text encoding stage # Text encoding stage
text_encoder_configs: tuple[EncoderConfig, ...] = field( text_encoder_configs: Tuple[EncoderConfig, ...] = field(
default_factory=lambda: (LlamaConfig(), CLIPTextConfig())) default_factory=lambda: (LlamaConfig(), CLIPTextConfig()))
preprocess_text_funcs: tuple[Callable[[str], str], ...] = field( preprocess_text_funcs: Tuple[Callable[[str], str], ...] = field(
default_factory=lambda: (llama_preprocess_text, clip_preprocess_text)) default_factory=lambda: (llama_preprocess_text, clip_preprocess_text))
postprocess_text_funcs: tuple[ postprocess_text_funcs: Tuple[
Callable[[BaseEncoderOutput], torch.tensor], Callable[[BaseEncoderOutput], torch.tensor],
...] = field(default_factory=lambda: ...] = field(default_factory=lambda:
(llama_postprocess_text, clip_postprocess_text)) (llama_postprocess_text, clip_postprocess_text))
@@ -83,7 +82,7 @@ class HunyuanConfig(PipelineConfig):
# Precision for each component # Precision for each component
dit_precision: str = "bf16" dit_precision: str = "bf16"
vae_precision: str = "fp16" vae_precision: str = "fp16"
text_encoder_precisions: tuple[str, ...] = field( text_encoder_precisions: Tuple[str, ...] = field(
default_factory=lambda: ("fp16", "fp16")) default_factory=lambda: ("fp16", "fp16"))
def __post_init__(self): def __post_init__(self):
+6 -7
View File
@@ -2,7 +2,7 @@
"""Registry for pipeline weight-specific configurations.""" """Registry for pipeline weight-specific configurations."""
import os import os
from collections.abc import Callable from typing import Callable, Dict, Optional, Type
from fastvideo.v1.configs.pipelines.base import PipelineConfig from fastvideo.v1.configs.pipelines.base import PipelineConfig
from fastvideo.v1.configs.pipelines.hunyuan import (FastHunyuanConfig, from fastvideo.v1.configs.pipelines.hunyuan import (FastHunyuanConfig,
@@ -19,11 +19,10 @@ from fastvideo.v1.utils import (maybe_download_model_index,
logger = init_logger(__name__) logger = init_logger(__name__)
# Registry maps specific model weights to their config classes # Registry maps specific model weights to their config classes
PIPE_NAME_TO_CONFIG: dict[str, type[PipelineConfig]] = { PIPE_NAME_TO_CONFIG: Dict[str, Type[PipelineConfig]] = {
"FastVideo/FastHunyuan-diffusers": FastHunyuanConfig, "FastVideo/FastHunyuan-diffusers": FastHunyuanConfig,
"hunyuanvideo-community/HunyuanVideo": HunyuanConfig, "hunyuanvideo-community/HunyuanVideo": HunyuanConfig,
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers": WanT2V480PConfig, "Wan-AI/Wan2.1-T2V-1.3B-Diffusers": WanT2V480PConfig,
"weizhou03/Wan2.1-Fun-1.3B-InP-Diffusers": WanI2V480PConfig,
"Wan-AI/Wan2.1-I2V-14B-480P-Diffusers": WanI2V480PConfig, "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers": WanI2V480PConfig,
"Wan-AI/Wan2.1-I2V-14B-720P-Diffusers": WanI2V720PConfig, "Wan-AI/Wan2.1-I2V-14B-720P-Diffusers": WanI2V720PConfig,
"Wan-AI/Wan2.1-T2V-14B-Diffusers": WanT2V720PConfig, "Wan-AI/Wan2.1-T2V-14B-Diffusers": WanT2V720PConfig,
@@ -32,7 +31,7 @@ PIPE_NAME_TO_CONFIG: dict[str, type[PipelineConfig]] = {
} }
# For determining pipeline type from model ID # For determining pipeline type from model ID
PIPELINE_DETECTOR: dict[str, Callable[[str], bool]] = { PIPELINE_DETECTOR: Dict[str, Callable[[str], bool]] = {
"hunyuan": lambda id: "hunyuan" in id.lower(), "hunyuan": lambda id: "hunyuan" in id.lower(),
"wanpipeline": lambda id: "wanpipeline" in id.lower(), "wanpipeline": lambda id: "wanpipeline" in id.lower(),
"wanimagetovideo": lambda id: "wanimagetovideo" in id.lower(), "wanimagetovideo": lambda id: "wanimagetovideo" in id.lower(),
@@ -41,7 +40,7 @@ PIPELINE_DETECTOR: dict[str, Callable[[str], bool]] = {
} }
# Fallback configs when exact match isn't found but architecture is detected # Fallback configs when exact match isn't found but architecture is detected
PIPELINE_FALLBACK_CONFIG: dict[str, type[PipelineConfig]] = { PIPELINE_FALLBACK_CONFIG: Dict[str, Type[PipelineConfig]] = {
"hunyuan": "hunyuan":
HunyuanConfig, # Base Hunyuan config as fallback for any Hunyuan variant HunyuanConfig, # Base Hunyuan config as fallback for any Hunyuan variant
"wanpipeline": "wanpipeline":
@@ -53,7 +52,7 @@ PIPELINE_FALLBACK_CONFIG: dict[str, type[PipelineConfig]] = {
def get_pipeline_config_cls_from_name( def get_pipeline_config_cls_from_name(
pipeline_name_or_path: str) -> type[PipelineConfig]: pipeline_name_or_path: str) -> Type[PipelineConfig]:
"""Get the appropriate configuration class for a given pipeline name or path. """Get the appropriate configuration class for a given pipeline name or path.
This function implements a multi-step lookup process to find the most suitable This function implements a multi-step lookup process to find the most suitable
@@ -82,7 +81,7 @@ def get_pipeline_config_cls_from_name(
- Warning messages are logged when falling back to less specific configurations - Warning messages are logged when falling back to less specific configurations
""" """
pipeline_config_cls: type[PipelineConfig] | None = None pipeline_config_cls: Optional[Type[PipelineConfig]] = None
# First try exact match for specific weights # First try exact match for specific weights
if pipeline_name_or_path in PIPE_NAME_TO_CONFIG: if pipeline_name_or_path in PIPE_NAME_TO_CONFIG:
+6 -6
View File
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from collections.abc import Callable
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Callable, Tuple
import torch import torch
@@ -17,7 +17,7 @@ def t5_postprocess_text(outputs: BaseEncoderOutput) -> torch.tensor:
hidden_state: torch.tensor = outputs.last_hidden_state hidden_state: torch.tensor = outputs.last_hidden_state
seq_lens = mask.gt(0).sum(dim=1).long() seq_lens = mask.gt(0).sum(dim=1).long()
assert torch.isnan(hidden_state).sum() == 0 assert torch.isnan(hidden_state).sum() == 0
prompt_embeds = [u[:v] for u, v in zip(hidden_state, seq_lens, strict=True)] prompt_embeds = [u[:v] for u, v in zip(hidden_state, seq_lens)]
prompt_embeds_tensor: torch.tensor = torch.stack([ prompt_embeds_tensor: torch.tensor = torch.stack([
torch.cat([u, u.new_zeros(512 - u.size(0), u.size(1))]) torch.cat([u, u.new_zeros(512 - u.size(0), u.size(1))])
for u in prompt_embeds for u in prompt_embeds
@@ -42,16 +42,16 @@ class WanT2V480PConfig(PipelineConfig):
flow_shift: int = 3 flow_shift: int = 3
# Text encoding stage # Text encoding stage
text_encoder_configs: tuple[EncoderConfig, ...] = field( text_encoder_configs: Tuple[EncoderConfig, ...] = field(
default_factory=lambda: (T5Config(), )) default_factory=lambda: (T5Config(), ))
postprocess_text_funcs: tuple[Callable[[BaseEncoderOutput], torch.tensor], postprocess_text_funcs: Tuple[Callable[[BaseEncoderOutput], torch.tensor],
...] = field(default_factory=lambda: ...] = field(default_factory=lambda:
(t5_postprocess_text, )) (t5_postprocess_text, ))
# Precision for each component # Precision for each component
precision: str = "bf16" precision: str = "bf16"
vae_precision: str = "fp32" vae_precision: str = "fp16"
text_encoder_precisions: tuple[str, ...] = field( text_encoder_precisions: Tuple[str, ...] = field(
default_factory=lambda: ("fp32", )) default_factory=lambda: ("fp32", ))
# WanConfig-specific added parameters # WanConfig-specific added parameters
+6 -6
View File
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any, Dict, List, Optional, Union
from fastvideo.v1.logger import init_logger from fastvideo.v1.logger import init_logger
@@ -16,12 +16,12 @@ class SamplingParam:
data_type: str = "video" data_type: str = "video"
# Image inputs # Image inputs
image_path: str | None = None image_path: Optional[str] = None
# Text inputs # Text inputs
prompt: str | list[str] | None = None prompt: Optional[Union[str, List[str]]] = None
negative_prompt: str | None = None negative_prompt: Optional[str] = None
prompt_path: str | None = None prompt_path: Optional[str] = None
output_path: str = "outputs/" output_path: str = "outputs/"
# Batch info # Batch info
@@ -54,7 +54,7 @@ class SamplingParam:
if self.prompt_path and not self.prompt_path.endswith(".txt"): if self.prompt_path and not self.prompt_path.endswith(".txt"):
raise ValueError("prompt_path must be a txt file") raise ValueError("prompt_path must be a txt file")
def update(self, source_dict: dict[str, Any]) -> None: def update(self, source_dict: Dict[str, Any]) -> None:
for key, value in source_dict.items(): for key, value in source_dict.items():
if hasattr(self, key): if hasattr(self, key):
setattr(self, key, value) setattr(self, key, value)
+7 -9
View File
@@ -1,7 +1,6 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
import os import os
from collections.abc import Callable from typing import Any, Callable, Dict, Optional
from typing import Any
from fastvideo.v1.configs.sample.hunyuan import (FastHunyuanSamplingParam, from fastvideo.v1.configs.sample.hunyuan import (FastHunyuanSamplingParam,
HunyuanSamplingParam) HunyuanSamplingParam)
@@ -9,28 +8,26 @@ from fastvideo.v1.configs.sample.stepvideo import StepVideoT2VSamplingParam
from fastvideo.v1.configs.sample.wan import (WanI2V_14B_480P_SamplingParam, from fastvideo.v1.configs.sample.wan import (WanI2V_14B_480P_SamplingParam,
WanI2V_14B_720P_SamplingParam, WanI2V_14B_720P_SamplingParam,
WanT2V_1_3B_SamplingParam, WanT2V_1_3B_SamplingParam,
WanT2V_14B_SamplingParam, WanT2V_14B_SamplingParam)
Wan2_1_Fun_1_3B_InP_SamplingParam)
from fastvideo.v1.logger import init_logger from fastvideo.v1.logger import init_logger
from fastvideo.v1.utils import (maybe_download_model_index, from fastvideo.v1.utils import (maybe_download_model_index,
verify_model_config_and_directory) verify_model_config_and_directory)
logger = init_logger(__name__) logger = init_logger(__name__)
# Registry maps specific model weights to their config classes # Registry maps specific model weights to their config classes
SAMPLING_PARAM_REGISTRY: dict[str, Any] = { SAMPLING_PARAM_REGISTRY: Dict[str, Any] = {
"FastVideo/FastHunyuan-diffusers": FastHunyuanSamplingParam, "FastVideo/FastHunyuan-diffusers": FastHunyuanSamplingParam,
"hunyuanvideo-community/HunyuanVideo": HunyuanSamplingParam, "hunyuanvideo-community/HunyuanVideo": HunyuanSamplingParam,
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers": WanT2V_1_3B_SamplingParam, "Wan-AI/Wan2.1-T2V-1.3B-Diffusers": WanT2V_1_3B_SamplingParam,
"Wan-AI/Wan2.1-T2V-14B-Diffusers": WanT2V_14B_SamplingParam, "Wan-AI/Wan2.1-T2V-14B-Diffusers": WanT2V_14B_SamplingParam,
"Wan-AI/Wan2.1-I2V-14B-480P-Diffusers": WanI2V_14B_480P_SamplingParam, "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers": WanI2V_14B_480P_SamplingParam,
"Wan-AI/Wan2.1-I2V-14B-720P-Diffusers": WanI2V_14B_720P_SamplingParam, "Wan-AI/Wan2.1-I2V-14B-720P-Diffusers": WanI2V_14B_720P_SamplingParam,
"weizhou03/Wan2.1-Fun-1.3B-InP-Diffusers": Wan2_1_Fun_1_3B_InP_SamplingParam,
"FastVideo/stepvideo-t2v-diffusers": StepVideoT2VSamplingParam, "FastVideo/stepvideo-t2v-diffusers": StepVideoT2VSamplingParam,
# Add other specific weight variants # Add other specific weight variants
} }
# For determining pipeline type from model ID # For determining pipeline type from model ID
SAMPLING_PARAM_DETECTOR: dict[str, Callable[[str], bool]] = { SAMPLING_PARAM_DETECTOR: Dict[str, Callable[[str], bool]] = {
"hunyuan": lambda id: "hunyuan" in id.lower(), "hunyuan": lambda id: "hunyuan" in id.lower(),
"wanpipeline": lambda id: "wanpipeline" in id.lower(), "wanpipeline": lambda id: "wanpipeline" in id.lower(),
"wanimagetovideo": lambda id: "wanimagetovideo" in id.lower(), "wanimagetovideo": lambda id: "wanimagetovideo" in id.lower(),
@@ -39,7 +36,7 @@ SAMPLING_PARAM_DETECTOR: dict[str, Callable[[str], bool]] = {
} }
# Fallback configs when exact match isn't found but architecture is detected # Fallback configs when exact match isn't found but architecture is detected
SAMPLING_FALLBACK_PARAM: dict[str, Any] = { SAMPLING_FALLBACK_PARAM: Dict[str, Any] = {
"hunyuan": "hunyuan":
HunyuanSamplingParam, # Base Hunyuan config as fallback for any Hunyuan variant HunyuanSamplingParam, # Base Hunyuan config as fallback for any Hunyuan variant
"wanpipeline": "wanpipeline":
@@ -50,7 +47,8 @@ SAMPLING_FALLBACK_PARAM: dict[str, Any] = {
} }
def get_sampling_param_cls_for_name(pipeline_name_or_path: str) -> Any | None: def get_sampling_param_cls_for_name(
pipeline_name_or_path: str) -> Optional[Any]:
"""Get the appropriate sampling param for specific pretrained weights.""" """Get the appropriate sampling param for specific pretrained weights."""
if os.path.exists(pipeline_name_or_path): if os.path.exists(pipeline_name_or_path):
-17
View File
@@ -94,20 +94,3 @@ class WanI2V_14B_720P_SamplingParam(WanT2V_14B_SamplingParam):
-5784.54975374, 5449.50911966, -1811.16591783, 256.27178429, -5784.54975374, 5449.50911966, -1811.16591783, 256.27178429,
-13.02252404 -13.02252404
])) ]))
# =============================================
# ============= Wan2.1 Fun Models =============
# =============================================
@dataclass
class Wan2_1_Fun_1_3B_InP_SamplingParam(SamplingParam):
"""Sampling parameters for Wan2.1 Fun 1.3B InP model."""
height: int = 480
width: int = 832
num_frames: int = 81
fps: int = 16
negative_prompt: str | None = "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走"
guidance_scale: float = 6.0
num_inference_steps: int = 50
+2 -2
View File
@@ -1,8 +1,8 @@
from typing import Any from typing import Any, Dict
def update_config_from_args(config: Any, def update_config_from_args(config: Any,
args_dict: dict[str, Any], args_dict: Dict[str, Any],
prefix: str = "", prefix: str = "",
pop_args: bool = False) -> None: pop_args: bool = False) -> None:
""" """
@@ -4,7 +4,7 @@
"use_cpu_offload": true, "use_cpu_offload": true,
"disable_autocast": false, "disable_autocast": false,
"precision": "bf16", "precision": "bf16",
"vae_precision": "fp32", "vae_precision": "fp16",
"vae_tiling": false, "vae_tiling": false,
"vae_sp": false, "vae_sp": false,
"vae_config": { "vae_config": {
@@ -4,7 +4,7 @@
"use_cpu_offload": true, "use_cpu_offload": true,
"disable_autocast": false, "disable_autocast": false,
"precision": "bf16", "precision": "bf16",
"vae_precision": "fp32", "vae_precision": "fp16",
"vae_tiling": false, "vae_tiling": false,
"vae_sp": false, "vae_sp": false,
"vae_config": { "vae_config": {
+1 -4
View File
@@ -12,10 +12,7 @@ from fastvideo.v1.dataset.validation_dataset import ValidationDataset
def getdataset(args) -> VideoCaptionMergedDataset: def getdataset(args) -> VideoCaptionMergedDataset:
if args.do_temporal_sample: temporal_sample = TemporalRandomCrop(args.num_frames) # 16 x
temporal_sample = TemporalRandomCrop(args.num_frames) # 16 x
else:
temporal_sample = None
norm_fun = Lambda(lambda x: 2.0 * x - 1.0) norm_fun = Lambda(lambda x: 2.0 * x - 1.0)
resize_topcrop = [ resize_topcrop = [
CenterCropResizeVideo((args.max_height, args.max_width), top_crop=True), CenterCropResizeVideo((args.max_height, args.max_width), top_crop=True),
@@ -11,7 +11,7 @@ from fastvideo.v1.dataset.parquet_dataset_iterable_style import (
build_parquet_iterable_style_dataloader) build_parquet_iterable_style_dataloader)
from fastvideo.v1.distributed import get_world_rank from fastvideo.v1.distributed import get_world_rank
from fastvideo.v1.distributed.parallel_state import ( from fastvideo.v1.distributed.parallel_state import (
cleanup_dist_env_and_memory, get_local_torch_device, cleanup_dist_env_and_memory, get_torch_device,
maybe_init_distributed_environment_and_model_parallel) maybe_init_distributed_environment_and_model_parallel)
from fastvideo.v1.logger import init_logger from fastvideo.v1.logger import init_logger
@@ -128,8 +128,8 @@ def main() -> None:
# Verify all sums match # Verify all sums match
if all( if all(
abs(a - b) < 1e-6 for a, b in zip( abs(a - b) < 1e-6
first_pass_sums, second_pass_sums, strict=True)): for a, b in zip(first_pass_sums, second_pass_sums)):
logger.info( logger.info(
"All latent sums match between passes - resume verification successful!" "All latent sums match between passes - resume verification successful!"
) )
@@ -148,8 +148,8 @@ def main() -> None:
break break
# Move data to device # Move data to device
latents = latents.to(get_local_torch_device()) latents = latents.to(get_torch_device())
embeddings = embeddings.to(get_local_torch_device()) embeddings = embeddings.to(get_torch_device())
# Calculate actual batch size # Calculate actual batch size
batch_size = latents.size(0) batch_size = latents.size(0)
@@ -8,12 +8,11 @@ import torch
import torch.distributed as dist import torch.distributed as dist
import torch.distributed.checkpoint as dist_cp import torch.distributed.checkpoint as dist_cp
from fastvideo.v1.dataset.dataloader.schema import pyarrow_schema_t2v
from fastvideo.v1.dataset.parquet_dataset_map_style import ( from fastvideo.v1.dataset.parquet_dataset_map_style import (
build_parquet_map_style_dataloader) build_parquet_map_style_dataloader)
from fastvideo.v1.distributed import get_world_rank from fastvideo.v1.distributed import get_world_rank
from fastvideo.v1.distributed.parallel_state import ( from fastvideo.v1.distributed.parallel_state import (
cleanup_dist_env_and_memory, get_local_torch_device, cleanup_dist_env_and_memory, get_torch_device,
maybe_init_distributed_environment_and_model_parallel) maybe_init_distributed_environment_and_model_parallel)
from fastvideo.v1.logger import init_logger from fastvideo.v1.logger import init_logger
@@ -68,18 +67,14 @@ def main() -> None:
# Create DataLoader with proper settings # Create DataLoader with proper settings
dataset, dataloader = build_parquet_map_style_dataloader( dataset, dataloader = build_parquet_map_style_dataloader(
args.path, args.path, args.batch_size, args.num_data_workers)
args.batch_size,
parquet_schema=pyarrow_schema_t2v,
num_data_workers=args.num_data_workers)
logger.info("Initialized dataloader with %d batches", len(dataloader)) logger.info("Initialized dataloader with %d batches", len(dataloader))
if args.verify_resume: if args.verify_resume:
# First pass - record latent sums # First pass - record latent sums
first_pass_sums = [] first_pass_sums = []
for i, batch in enumerate(dataloader): for i, (latents, embeddings, masks,
latents = batch['vae_latent'] caption_text) in enumerate(dataloader):
embeddings = batch['text_embedding']
latent_sum = latents.sum().item() latent_sum = latents.sum().item()
first_pass_sums.append(latent_sum) first_pass_sums.append(latent_sum)
logger.info("Batch %d latent sum: %f", i, latent_sum) logger.info("Batch %d latent sum: %f", i, latent_sum)
@@ -105,18 +100,14 @@ def main() -> None:
# Recreate dataloader and load state # Recreate dataloader and load state
dataset, dataloader = build_parquet_map_style_dataloader( dataset, dataloader = build_parquet_map_style_dataloader(
args.path, args.path, args.batch_size, args.num_data_workers)
args.batch_size,
parquet_schema=pyarrow_schema_t2v,
num_data_workers=args.num_data_workers)
load_states = {"dataloader": dataloader} load_states = {"dataloader": dataloader}
dist_cp.load(load_states, checkpoint_id=checkpoint_dir.as_posix()) dist_cp.load(load_states, checkpoint_id=checkpoint_dir.as_posix())
logger.info("Rank %d: Loaded dataloader state from %s", logger.info("Rank %d: Loaded dataloader state from %s",
get_world_rank(), checkpoint_dir) get_world_rank(), checkpoint_dir)
for i, batch in enumerate(dataloader): for i, (latents, embeddings, masks,
latents = batch['vae_latent'] caption_text) in enumerate(dataloader):
embeddings = batch['text_embedding']
latent_sum = latents.sum().item() latent_sum = latents.sum().item()
first_pass_sums.append(latent_sum) first_pass_sums.append(latent_sum)
logger.info("Batch %d latent sum: %f", logger.info("Batch %d latent sum: %f",
@@ -125,16 +116,11 @@ def main() -> None:
break break
dataset, dataloader = build_parquet_map_style_dataloader( dataset, dataloader = build_parquet_map_style_dataloader(
args.path, args.path, args.batch_size, args.num_data_workers)
args.batch_size,
parquet_schema=pyarrow_schema_t2v,
num_data_workers=args.num_data_workers)
# Second pass - verify latent sums match # Second pass - verify latent sums match
second_pass_sums = [] second_pass_sums = []
for i, batch in enumerate(dataloader): for i, (latents, embeddings, masks) in enumerate(dataloader):
latents = batch['vae_latent']
embeddings = batch['text_embedding']
latent_sum = latents.sum().item() latent_sum = latents.sum().item()
second_pass_sums.append(latent_sum) second_pass_sums.append(latent_sum)
logger.info("Batch %d latent sum: %f (should match first pass: %f)", logger.info("Batch %d latent sum: %f (should match first pass: %f)",
@@ -144,8 +130,8 @@ def main() -> None:
# Verify all sums match # Verify all sums match
if all( if all(
abs(a - b) < 1e-6 for a, b in zip( abs(a - b) < 1e-6
first_pass_sums, second_pass_sums, strict=True)): for a, b in zip(first_pass_sums, second_pass_sums)):
logger.info( logger.info(
"All latent sums match between passes - resume verification successful!" "All latent sums match between passes - resume verification successful!"
) )
@@ -158,15 +144,14 @@ def main() -> None:
total_samples = 0 total_samples = 0
total_batches = 0 total_batches = 0
for _ in range(args.num_epoch): for _ in range(args.num_epoch):
for i, batch in enumerate(dataloader): for i, (latents, embeddings, masks,
latents = batch['vae_latent'] caption_text) in enumerate(dataloader):
embeddings = batch['text_embedding']
if i >= args.num_batches_per_epoch: if i >= args.num_batches_per_epoch:
break break
# Move data to device # Move data to device
latents = latents.to(get_local_torch_device()) latents = latents.to(get_torch_device())
embeddings = embeddings.to(get_local_torch_device()) embeddings = embeddings.to(get_torch_device())
# Calculate actual batch size # Calculate actual batch size
batch_size = latents.size(0) batch_size = latents.size(0)
+1 -1
View File
@@ -76,7 +76,7 @@ def latent_collate_function(batch):
# latent_attn_mask: # b t h w # latent_attn_mask: # b t h w
# text_attn_mask: b 1 l # text_attn_mask: b 1 l
# needs to check if the latent/prompt' size and apply padding & attn mask # needs to check if the latent/prompt' size and apply padding & attn mask
latents, prompt_embeds, prompt_attention_masks = zip(*batch, strict=True) latents, prompt_embeds, prompt_attention_masks = zip(*batch)
# calculate max shape # calculate max shape
max_t = max([latent.shape[1] for latent in latents]) max_t = max([latent.shape[1] for latent in latents])
max_h = max([latent.shape[2] for latent in latents]) max_h = max([latent.shape[2] for latent in latents])
@@ -1,17 +1,19 @@
import os import os
import pickle import pickle
import random import random
from typing import Dict, List, Tuple
import numpy as np import numpy as np
import pyarrow as pa import pyarrow as pa
import pyarrow.parquet as pq import pyarrow.parquet as pq
import torch
import tqdm import tqdm
from torch.utils.data import IterableDataset, get_worker_info from torch.utils.data import IterableDataset, get_worker_info
from torchdata.stateful_dataloader import StatefulDataLoader from torchdata.stateful_dataloader import StatefulDataLoader
from fastvideo.v1.dataset.utils import collate_latents_embs_masks from fastvideo.v1.dataset.utils import collate_latents_embs_masks
from fastvideo.v1.distributed import (get_sp_world_size, get_world_group, from fastvideo.v1.distributed import (get_sp_world_size, get_world_rank,
get_world_rank, get_world_size) get_world_size)
from fastvideo.v1.logger import init_logger from fastvideo.v1.logger import init_logger
logger = init_logger(__name__) logger = init_logger(__name__)
@@ -137,7 +139,7 @@ def shard_parquet_files_across_sp_groups_and_workers(
num_sp_groups: int, num_sp_groups: int,
num_workers: int, num_workers: int,
seed: int = 42, seed: int = 42,
) -> tuple[list[list[str]], list[int], list[dict[str, int]]]: ) -> Tuple[List[List[str]], List[int], List[Dict[str, int]]]:
""" """
Shard parquet files across SP groups and workers in a balanced way. Shard parquet files across SP groups and workers in a balanced way.
@@ -156,123 +158,94 @@ def shard_parquet_files_across_sp_groups_and_workers(
# Check if sharding plan already exists # Check if sharding plan already exists
sharding_info_dir = os.path.join( sharding_info_dir = os.path.join(
path, f"sharding_info_{num_sp_groups}_sp_groups_{num_workers}_workers") path, f"sharding_info_{num_sp_groups}_sp_groups_{num_workers}_workers")
if os.path.exists(sharding_info_dir):
# Only rank 0 handles cache checking and file scanning logger.info("Sharding plan already exists")
if get_world_rank() == 0: logger.info("Loading sharding plan from %s", sharding_info_dir)
cache_loaded = False try:
shard_parquet_files = None
shard_total_samples = None
shard_parquet_lengths = None
# First try to load existing sharding plan
if os.path.exists(sharding_info_dir):
logger.info("Loading sharding plan from %s", sharding_info_dir)
try:
with open(
os.path.join(sharding_info_dir,
"shard_parquet_files.pkl"), "rb") as f:
shard_parquet_files = pickle.load(f)
with open(
os.path.join(sharding_info_dir,
"shard_total_samples.pkl"), "rb") as f:
shard_total_samples = pickle.load(f)
with open(
os.path.join(sharding_info_dir,
"shard_parquet_lengths.pkl"), "rb") as f:
shard_parquet_lengths = pickle.load(f)
cache_loaded = True
logger.info("Successfully loaded sharding plan")
except Exception as e:
logger.error("Error loading sharding plan: %s", str(e))
logger.info("Falling back to creating new sharding plan")
cache_loaded = False
# If cache not loaded (either doesn't exist or failed to load), create sharding plan
if not cache_loaded:
logger.info("Creating new sharding plan")
logger.info("Scanning for parquet files in %s", path)
# Find all parquet files
parquet_files = []
for root, _, files in os.walk(path):
for file in files:
if file.endswith('.parquet'):
parquet_files.append(os.path.join(root, file))
if not parquet_files:
raise ValueError("No parquet files found in %s", path)
# Calculate file lengths efficiently using a single pass
logger.info("Calculating file lengths...")
lengths = []
for file in tqdm.tqdm(parquet_files, desc="Reading parquet files"):
lengths.append(pq.ParquetFile(file).metadata.num_rows)
total_samples = sum(lengths)
logger.info("Found %d files with %d total samples",
len(parquet_files), total_samples)
# Sort files by length for better balancing
sorted_indices = np.argsort(lengths)
sorted_files = [parquet_files[i] for i in sorted_indices]
sorted_lengths = [lengths[i] for i in sorted_indices]
# Create shards
num_shards = num_sp_groups * num_workers
shard_parquet_files = [[] for _ in range(num_shards)]
shard_total_samples = [0] * num_shards
shard_parquet_lengths = [{} for _ in range(num_shards)]
# Distribute files to shards using a greedy approach
logger.info("Distributing files to shards...")
for file, length in zip(reversed(sorted_files),
reversed(sorted_lengths),
strict=True):
# Find shard with minimum current length
target_shard = np.argmin(shard_total_samples)
shard_parquet_files[target_shard].append(file)
shard_total_samples[target_shard] += length
shard_parquet_lengths[target_shard][file] = length
#randomize each shard
for shard in shard_parquet_files:
random.seed(seed)
random.shuffle(shard)
# Save the sharding plan
os.makedirs(sharding_info_dir, exist_ok=True)
with open( with open(
os.path.join(sharding_info_dir, "shard_parquet_files.pkl"), os.path.join(sharding_info_dir, "shard_parquet_files.pkl"),
"wb") as f: "rb") as f:
pickle.dump(shard_parquet_files, f) shard_parquet_files = pickle.load(f)
with open( with open(
os.path.join(sharding_info_dir, "shard_total_samples.pkl"), os.path.join(sharding_info_dir, "shard_total_samples.pkl"),
"wb") as f: "rb") as f:
pickle.dump(shard_total_samples, f) shard_total_samples = pickle.load(f)
with open( with open(
os.path.join(sharding_info_dir, os.path.join(sharding_info_dir,
"shard_parquet_lengths.pkl"), "wb") as f: "shard_parquet_lengths.pkl"), "rb") as f:
pickle.dump(shard_parquet_lengths, f) shard_parquet_lengths = pickle.load(f)
logger.info("Saved sharding info to %s", sharding_info_dir) return shard_parquet_files, shard_total_samples, shard_parquet_lengths
except Exception as e:
logger.error("Error loading sharding plan: %s", str(e))
logger.info("Falling back to creating new sharding plan")
# Wait for rank 0 to finish creating/loading sharding plan if get_world_rank() == 0:
world_group = get_world_group() logger.info("Scanning for parquet files in %s", path)
world_group.barrier()
# Now all ranks load the sharding plan (it should exist and be valid now) # Find all parquet files
logger.info("Loading sharding plan from %s after barrier", parquet_files = []
sharding_info_dir)
with open(os.path.join(sharding_info_dir, "shard_parquet_files.pkl"),
"rb") as f:
shard_parquet_files = pickle.load(f)
with open(os.path.join(sharding_info_dir, "shard_total_samples.pkl"),
"rb") as f:
shard_total_samples = pickle.load(f)
with open(os.path.join(sharding_info_dir, "shard_parquet_lengths.pkl"),
"rb") as f:
shard_parquet_lengths = pickle.load(f)
return shard_parquet_files, shard_total_samples, shard_parquet_lengths for root, _, files in os.walk(path):
for file in files:
if file.endswith('.parquet'):
parquet_files.append(os.path.join(root, file))
if not parquet_files:
raise ValueError("No parquet files found in %s", path)
# Calculate file lengths efficiently using a single pass
logger.info("Calculating file lengths...")
lengths = []
for file in tqdm.tqdm(parquet_files, desc="Reading parquet files"):
lengths.append(pq.ParquetFile(file).metadata.num_rows)
total_samples = sum(lengths)
logger.info("Found %d files with %d total samples", len(parquet_files),
total_samples)
# Sort files by length for better balancing
sorted_indices = np.argsort(lengths)
sorted_files = [parquet_files[i] for i in sorted_indices]
sorted_lengths = [lengths[i] for i in sorted_indices]
# Create shards
num_shards = num_sp_groups * num_workers
shard_parquet_files = [[] for _ in range(num_shards)]
shard_total_samples = [0] * num_shards
shard_parquet_lengths = [{} for _ in range(num_shards)]
# Distribute files to shards using a greedy approach
logger.info("Distributing files to shards...")
for file, length in zip(reversed(sorted_files),
reversed(sorted_lengths)):
# Find shard with minimum current length
target_shard = np.argmin(shard_total_samples)
shard_parquet_files[target_shard].append(file)
shard_total_samples[target_shard] += length
shard_parquet_lengths[target_shard][file] = length
#randomize each shard
for shard in shard_parquet_files:
random.seed(seed)
random.shuffle(shard)
save_dir = os.path.join(
path,
f"sharding_info_{num_sp_groups}_sp_groups_{num_workers}_workers")
os.makedirs(save_dir, exist_ok=True)
with open(os.path.join(save_dir, "shard_parquet_files.pkl"), "wb") as f:
pickle.dump(shard_parquet_files, f)
with open(os.path.join(save_dir, "shard_total_samples.pkl"), "wb") as f:
pickle.dump(shard_total_samples, f)
with open(os.path.join(save_dir, "shard_parquet_lengths.pkl"),
"wb") as f:
pickle.dump(shard_parquet_lengths, f)
logger.info("Saved sharding info to %s", save_dir)
# wait for all ranks to finish
torch.distributed.barrier()
# recursive call
return shard_parquet_files_across_sp_groups_and_workers(
path, num_sp_groups, num_workers, seed)
def build_parquet_iterable_style_dataloader( def build_parquet_iterable_style_dataloader(
@@ -284,7 +257,7 @@ def build_parquet_iterable_style_dataloader(
text_padding_length: int = 512, text_padding_length: int = 512,
seed: int = 42, seed: int = 42,
read_batch_size: int = 32 read_batch_size: int = 32
) -> tuple[LatentsParquetIterStyleDataset, StatefulDataLoader]: ) -> Tuple[LatentsParquetIterStyleDataset, StatefulDataLoader]:
"""Build a dataloader for the LatentsParquetIterStyleDataset.""" """Build a dataloader for the LatentsParquetIterStyleDataset."""
dataset = LatentsParquetIterStyleDataset( dataset = LatentsParquetIterStyleDataset(
path=path, path=path,
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
import os import os
import pickle import pickle
from typing import Any from typing import Any, Dict, List, Tuple
import pyarrow as pa import pyarrow as pa
import pyarrow.parquet as pq import pyarrow.parquet as pq
@@ -13,14 +13,14 @@ from torch.utils.data import Dataset, Sampler
from torchdata.stateful_dataloader import StatefulDataLoader from torchdata.stateful_dataloader import StatefulDataLoader
from fastvideo.v1.dataset.utils import collate_rows_from_parquet_schema from fastvideo.v1.dataset.utils import collate_rows_from_parquet_schema
from fastvideo.v1.distributed import (get_sp_world_size, get_world_group, from fastvideo.v1.distributed import (get_sp_world_size, get_world_rank,
get_world_rank, get_world_size) get_world_size)
from fastvideo.v1.logger import init_logger from fastvideo.v1.logger import init_logger
logger = init_logger(__name__) logger = init_logger(__name__)
class DP_SP_BatchSampler(Sampler[list[int]]): class DP_SP_BatchSampler(Sampler[List[int]]):
""" """
A simple sequential batch sampler that yields batches of indices. A simple sequential batch sampler that yields batches of indices.
""" """
@@ -97,68 +97,49 @@ def get_parquet_files_and_length(path: str):
cache_dir = os.path.join(path, "map_style_cache") cache_dir = os.path.join(path, "map_style_cache")
cache_file = os.path.join(cache_dir, "file_info.pkl") cache_file = os.path.join(cache_dir, "file_info.pkl")
# Only rank 0 checks for cache and scans files if needed if os.path.exists(cache_file):
logger.info("Loading cached file info from %s", cache_file)
try:
with open(cache_file, "rb") as f:
file_names_sorted, lengths_sorted = pickle.load(f)
return file_names_sorted, lengths_sorted
except Exception as e:
logger.error("Error loading cached file info: %s", str(e))
logger.info("Falling back to scanning files")
# If no cache exists or loading failed, scan files
if get_world_rank() == 0: if get_world_rank() == 0:
cache_loaded = False lengths = []
file_names_sorted = None file_names = []
lengths_sorted = None for root, _, files in os.walk(path):
for file in sorted(files):
if file.endswith('.parquet'):
file_path = os.path.join(root, file)
file_names.append(file_path)
for file_path in tqdm.tqdm(file_names,
desc="Reading parquet files to get lengths"):
num_rows = pq.ParquetFile(file_path).metadata.num_rows
lengths.append(num_rows)
# sort according to file name to ensure all rank has the same order (in case os.walk is not sorted)
file_names_sorted, lengths_sorted = zip(
*sorted(zip(file_names, lengths), key=lambda x: x[0]))
assert len(
file_names_sorted) != 0, "No parquet files found in the dataset"
# First try to load existing cache os.makedirs(cache_dir, exist_ok=True)
if os.path.exists(cache_file): with open(cache_file, "wb") as f:
logger.info("Loading cached file info from %s", cache_file) pickle.dump((file_names_sorted, lengths_sorted), f)
try: logger.info("Saved file info to %s", cache_file)
with open(cache_file, "rb") as f:
file_names_sorted, lengths_sorted = pickle.load(f)
cache_loaded = True
logger.info("Successfully loaded cached file info")
except Exception as e:
logger.error("Error loading cached file info: %s", str(e))
logger.info("Falling back to scanning files")
cache_loaded = False
# If cache not loaded (either doesn't exist or failed to load), scan files # Wait for rank 0 to finish saving
if not cache_loaded: if get_world_size() > 1:
logger.info("Scanning parquet files to get lengths") torch.distributed.barrier()
lengths = []
file_names = []
for root, _, files in os.walk(path):
for file in sorted(files):
if file.endswith('.parquet'):
file_path = os.path.join(root, file)
file_names.append(file_path)
for file_path in tqdm.tqdm(
file_names, desc="Reading parquet files to get lengths"):
num_rows = pq.ParquetFile(file_path).metadata.num_rows
lengths.append(num_rows)
# sort according to file name to ensure all rank has the same order
file_names_sorted, lengths_sorted = zip(*sorted(zip(file_names,
lengths,
strict=True),
key=lambda x: x[0]),
strict=True)
assert len(
file_names_sorted) != 0, "No parquet files found in the dataset"
# Save the cache return get_parquet_files_and_length(path)
os.makedirs(cache_dir, exist_ok=True)
with open(cache_file, "wb") as f:
pickle.dump((file_names_sorted, lengths_sorted), f)
logger.info("Saved file info to %s", cache_file)
# Wait for rank 0 to finish creating/loading cache
world_group = get_world_group()
world_group.barrier()
# Now all ranks load the cache (it should exist and be valid now)
logger.info("Loading cached file info from %s after barrier", cache_file)
with open(cache_file, "rb") as f:
file_names_sorted, lengths_sorted = pickle.load(f)
return file_names_sorted, lengths_sorted
def read_row_from_parquet_file(parquet_files: list[str], global_row_idx: int, def read_row_from_parquet_file(parquet_files: List[str], global_row_idx: int,
lengths: list[int]) -> dict[str, Any]: lengths: List[int]) -> Dict[str, Any]:
''' '''
Read a row from a parquet file. Read a row from a parquet file.
Args: Args:
@@ -169,27 +150,17 @@ def read_row_from_parquet_file(parquet_files: list[str], global_row_idx: int,
''' '''
# find the parquet file and local row index # find the parquet file and local row index
cumulative = 0 cumulative = 0
file_index = 0
local_row_idx = 0
for file_index in range(len(lengths)): for file_index in range(len(lengths)):
if cumulative + lengths[file_index] > global_row_idx: if cumulative + lengths[file_index] > global_row_idx:
local_row_idx = global_row_idx - cumulative local_row_idx = global_row_idx - cumulative
break break
cumulative += lengths[file_index] cumulative += lengths[file_index]
else:
# If we reach here, global_row_idx is out of bounds
raise IndexError(
f"global_row_idx {global_row_idx} is out of bounds for dataset")
parquet_file = pq.ParquetFile(parquet_files[file_index]) parquet_file = pq.ParquetFile(parquet_files[file_index])
# Calculate the row group to read into memory and the local idx # Calculate the row group to read into memory and the local idx
# This way we can avoid reading in the entire parquet file # This way we can avoid reading in the entire parquet file
cumulative = 0 cumulative = 0
row_group_index = 0
local_index = 0
for i in range(parquet_file.num_row_groups): for i in range(parquet_file.num_row_groups):
num_rows = parquet_file.metadata.row_group(i).num_rows num_rows = parquet_file.metadata.row_group(i).num_rows
if cumulative + num_rows > local_row_idx: if cumulative + num_rows > local_row_idx:
@@ -197,11 +168,6 @@ def read_row_from_parquet_file(parquet_files: list[str], global_row_idx: int,
local_index = local_row_idx - cumulative local_index = local_row_idx - cumulative
break break
cumulative += num_rows cumulative += num_rows
else:
# If we reach here, local_row_idx is out of bounds for this parquet file
raise IndexError(
f"local_row_idx {local_row_idx} is out of bounds for parquet file {parquet_files[file_index]}"
)
row_group = parquet_file.read_row_group(row_group_index).to_pydict() row_group = parquet_file.read_row_group(row_group_index).to_pydict()
row_dict = {k: v[local_index] for k, v in row_group.items()} row_dict = {k: v[local_index] for k, v in row_group.items()}
@@ -219,6 +185,9 @@ class LatentsParquetMapStyleDataset(Dataset):
Note: Note:
Using parquet for map style dataset is not efficient, we mainly keep it for backward compatibility and debugging. Using parquet for map style dataset is not efficient, we mainly keep it for backward compatibility and debugging.
""" """
# Modify this in the future if we want to add more keys, for example, in image to video.
keys = [("vae_latent", "latent"), "text_embedding", "clip_feature",
"first_frame_latent", "pil_image"]
def __init__( def __init__(
self, self,
@@ -235,6 +204,10 @@ class LatentsParquetMapStyleDataset(Dataset):
self.path = path self.path = path
self.cfg_rate = cfg_rate self.cfg_rate = cfg_rate
self.parquet_schema = parquet_schema self.parquet_schema = parquet_schema
if cfg_rate > 0.0:
raise ValueError(
"cfg_rate > 0.0 is not supported for now because it will trigger bug when num_data_workers > 0"
)
logger.info("Initializing LatentsParquetMapStyleDataset with path: %s", logger.info("Initializing LatentsParquetMapStyleDataset with path: %s",
path) path)
self.parquet_files, self.lengths = get_parquet_files_and_length(path) self.parquet_files, self.lengths = get_parquet_files_and_length(path)
@@ -270,8 +243,7 @@ class LatentsParquetMapStyleDataset(Dataset):
batch = collate_rows_from_parquet_schema([row_dict], batch = collate_rows_from_parquet_schema([row_dict],
self.parquet_schema, self.parquet_schema,
self.text_padding_length, self.text_padding_length)
cfg_rate=0.0)
negative_prompt = batch['info_list'][0]['prompt'] negative_prompt = batch['info_list'][0]['prompt']
negative_prompt_embedding = batch['text_embedding'] negative_prompt_embedding = batch['text_embedding']
negative_prompt_attention_mask = batch['text_attention_mask'] negative_prompt_attention_mask = batch['text_attention_mask']
@@ -284,7 +256,7 @@ class LatentsParquetMapStyleDataset(Dataset):
return negative_prompt_embedding, negative_prompt_attention_mask, negative_prompt return negative_prompt_embedding, negative_prompt_attention_mask, negative_prompt
# PyTorch calls this ONLY because the batch_sampler yields a list # PyTorch calls this ONLY because the batch_sampler yields a list
def __getitems__(self, indices: list[int]) -> dict[str, Any]: def __getitems__(self, indices: List[int]) -> Dict[str, Any]:
""" """
Batch fetch using read_row_from_parquet_file for each index. Batch fetch using read_row_from_parquet_file for each index.
""" """
@@ -293,10 +265,11 @@ class LatentsParquetMapStyleDataset(Dataset):
for idx in indices for idx in indices
] ]
batch = collate_rows_from_parquet_schema(rows, # all_latents, all_embs, all_masks, caption_text, all_extra_latents, all_infos = collate_latents_embs_masks(
self.parquet_schema, # rows, self.text_padding_length, self.keys)
self.text_padding_length, # return all_latents, all_embs, all_masks, caption_text, all_extra_latents, all_infos
cfg_rate=self.cfg_rate) batch = collate_rows_from_parquet_schema(rows, self.parquet_schema,
self.text_padding_length)
return batch return batch
def __len__(self): def __len__(self):
@@ -319,7 +292,7 @@ def build_parquet_map_style_dataloader(
drop_last=True, drop_last=True,
drop_first_row=False, drop_first_row=False,
text_padding_length=512, text_padding_length=512,
seed=42) -> tuple[LatentsParquetMapStyleDataset, StatefulDataLoader]: seed=42) -> Tuple[LatentsParquetMapStyleDataset, StatefulDataLoader]:
dataset = LatentsParquetMapStyleDataset( dataset = LatentsParquetMapStyleDataset(
path, path,
batch_size, batch_size,
+38 -63
View File
@@ -7,7 +7,7 @@ from abc import ABC, abstractmethod
from collections import Counter from collections import Counter
from dataclasses import dataclass from dataclasses import dataclass
from os.path import join as opj from os.path import join as opj
from typing import Any from typing import Any, Dict, List, Optional, Union
import numpy as np import numpy as np
import torch import torch
@@ -31,21 +31,21 @@ class PreprocessBatch:
""" """
# Raw metadata # Raw metadata
path: str path: str
cap: str | list[str] cap: Union[str, List[str]]
resolution: dict | None = None resolution: Optional[Dict] = None
fps: float | None = None fps: Optional[float] = None
duration: float | None = None duration: Optional[float] = None
# Processed metadata # Processed metadata
num_frames: int | None = None num_frames: Optional[int] = None
sample_frame_index: list[int] | None = None sample_frame_index: Optional[List[int]] = None
sample_num_frames: int | None = None sample_num_frames: Optional[int] = None
# Processed data # Processed data
pixel_values: torch.Tensor | None = None pixel_values: Optional[torch.Tensor] = None
text: str | None = None text: Optional[str] = None
input_ids: torch.Tensor | None = None input_ids: Optional[torch.Tensor] = None
cond_mask: torch.Tensor | None = None cond_mask: Optional[torch.Tensor] = None
@property @property
def is_video(self) -> bool: def is_video(self) -> bool:
@@ -281,12 +281,10 @@ class FrameSamplingStage(DatasetFilterStage):
frame_interval).astype(int) frame_interval).astype(int)
# Temporal crop if too long # Temporal crop if too long
if len(frame_indices) > self.num_frames: if len(frame_indices
if temporal_sample_fn is not None: ) > self.num_frames and temporal_sample_fn is not None:
begin_index, end_index = temporal_sample_fn(len(frame_indices)) begin_index, end_index = temporal_sample_fn(len(frame_indices))
frame_indices = frame_indices[begin_index:end_index] frame_indices = frame_indices[begin_index:end_index]
else:
frame_indices = frame_indices[:self.num_frames]
batch.sample_frame_index = frame_indices.tolist() batch.sample_frame_index = frame_indices.tolist()
batch.sample_num_frames = len(frame_indices) batch.sample_num_frames = len(frame_indices)
@@ -413,29 +411,6 @@ class VideoCaptionMergedDataset(torch.utils.data.IterableDataset,
torch.distributed.checkpoint.stateful.Stateful): torch.distributed.checkpoint.stateful.Stateful):
""" """
Merged dataset for video and caption data with stage-based processing. Merged dataset for video and caption data with stage-based processing.
Assumes that data_merge_path is a txt file with the following format:
<folder_path>,<json_file_path>
The folder should contain videos.
The json file should be a list of dictionaries with the following format:
[
{
"path": "1gGQy4nxyUo-Scene-016.mp4",
"resolution": {
"width": 1920,
"height": 1080
},
"size": 2439112,
"fps": 25.0,
"duration": 6.88,
"num_frames": 172,
"cap": [
"A watermelon wearing a helmet is crushed by a hydraulic press, causing it to flatten and burst open."
]
},
...
]
This dataset processes video and image data through a series of stages: This dataset processes video and image data through a series of stages:
- Data validation - Data validation
@@ -486,32 +461,32 @@ class VideoCaptionMergedDataset(torch.utils.data.IterableDataset,
self.text_encoding_stage = TextEncodingStage( self.text_encoding_stage = TextEncodingStage(
tokenizer=tokenizer, tokenizer=tokenizer,
text_max_length=args.text_max_length, text_max_length=args.text_max_length,
cfg_rate=args.training_cfg_rate) cfg_rate=args.cfg)
def _load_raw_data(self) -> list[dict]: def _load_raw_data(self) -> List[Dict]:
"""Load raw data from JSON files.""" """Load raw data from JSON files."""
all_data = []
# Read folder-annotation pairs # Read folder-annotation pairs
with open(self.data_merge_path) as f: with open(self.data_merge_path) as f:
folder_anno_pairs = [ folder_anno_pairs = [
line.strip().split(",") for line in f if line.strip() line.strip().split(",") for line in f if line.strip()
] ]
assert len(
folder_anno_pairs) == 1, "Only support one folder-annotation pair"
assert len(folder_anno_pairs[0]
) == 2, "Folder-annotation pair should have two elements"
folder, annotation_file = folder_anno_pairs[0]
data_items: list[dict] = [] # Process each folder-annotation pair
with open(annotation_file) as f: for folder, annotation_file in folder_anno_pairs:
data_items = json.load(f) with open(annotation_file) as f:
data_items = json.load(f)
# Update paths with folder prefix # Update paths with folder prefix
for item in data_items: for item in data_items:
item["path"] = opj(folder, item["path"]) item["path"] = opj(folder, item["path"])
return data_items all_data.extend(data_items)
def _process_metadata(self) -> list[PreprocessBatch]: return all_data[self.start_idx:]
def _process_metadata(self) -> List[PreprocessBatch]:
"""Process the raw metadata through all filtering stages.""" """Process the raw metadata through all filtering stages."""
raw_data = self._load_raw_data() raw_data = self._load_raw_data()
processed_batches = [] processed_batches = []
@@ -522,7 +497,7 @@ class VideoCaptionMergedDataset(torch.utils.data.IterableDataset,
"resolution_failed": 0, "resolution_failed": 0,
"frame_sampling_failed": 0 "frame_sampling_failed": 0
} }
sample_num_frames: list[int] = [] sample_num_frames: List[int] = []
for item in raw_data: for item in raw_data:
batch = PreprocessBatch(path=item["path"], batch = PreprocessBatch(path=item["path"],
@@ -548,7 +523,7 @@ class VideoCaptionMergedDataset(torch.utils.data.IterableDataset,
return processed_batches return processed_batches
def _apply_filter_stages(self, batch: PreprocessBatch, def _apply_filter_stages(self, batch: PreprocessBatch,
filter_counts: dict[str, int]) -> bool: filter_counts: Dict[str, int]) -> bool:
"""Apply all filter stages and update counters. Returns True if batch should be kept.""" """Apply all filter stages and update counters. Returns True if batch should be kept."""
if not self.validation_stage.should_keep(batch): if not self.validation_stage.should_keep(batch):
filter_counts["validation_failed"] += 1 filter_counts["validation_failed"] += 1
@@ -564,8 +539,8 @@ class VideoCaptionMergedDataset(torch.utils.data.IterableDataset,
return True return True
def _log_filtering_stats(self, filter_counts: dict[str, int], def _log_filtering_stats(self, filter_counts: Dict[str, int],
sample_num_frames: list[int], before_count: int, sample_num_frames: List[int], before_count: int,
after_count: int): after_count: int):
"""Log filtering statistics.""" """Log filtering statistics."""
logger.info( logger.info(
@@ -584,7 +559,7 @@ class VideoCaptionMergedDataset(torch.utils.data.IterableDataset,
def __len__(self): def __len__(self):
return len(self.processed_batches) return len(self.processed_batches)
def _get_item(self, idx: int) -> dict: def _get_item(self, idx: int) -> Dict:
"""Get a single processed data item.""" """Get a single processed data item."""
batch = self.processed_batches[idx] batch = self.processed_batches[idx]
@@ -608,10 +583,10 @@ class VideoCaptionMergedDataset(torch.utils.data.IterableDataset,
return result return result
def state_dict(self) -> dict[str, Any]: def state_dict(self) -> Dict[str, Any]:
"""Return state dict for checkpointing.""" """Return state dict for checkpointing."""
return {"processed_batches": self.processed_batches} return {"processed_batches": self.processed_batches}
def load_state_dict(self, state_dict: dict[str, Any]) -> None: def load_state_dict(self, state_dict: Dict[str, Any]) -> None:
"""Load state dict from checkpoint.""" """Load state dict from checkpoint."""
self.processed_batches = state_dict["processed_batches"] self.processed_batches = state_dict["processed_batches"]

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