Compare commits

...
Author SHA1 Message Date
SolitaryThinker 757e024dc7 [misc]: address review — stale STG docstring, typos, rename np-less freq-grid helper, add compile-conditions contract test
- ltx2.py: the keep-mask closure docstring still claimed only
  stg_block_idx is perturbed; that gate moved to the eager caller, so
  document the caller contract instead. Note that
  LTX2_PIPELINE_DEBUG_LOG re-specializes compiled graphs per block.
- rename generate_ltx_freq_grid_np -> generate_ltx_freq_grid_float64
  (it no longer uses numpy); update the gemma.py consumer.
- flash_attn_cute.py: fix 'nstead' typo; flash_attn.py: wrap comment
  to 120 cols; launch script: grammar fix.
- new fastvideo/tests/contract/test_ltx2_compile_conditions.py pins
  the class-level _compile_conditions wiring and the is_ltx2_blocks
  matcher so the whole-model fullgraph fallback can't silently return
  (CPU-only, no model instantiation).
2026-07-20 13:14:08 -07:00
Adriel Fung d3ff912ac0 [perf]: enable per-block torch.compile for LTX2 with persistent cache
Cache LTX2 graphs and allow all 48 blocks to share one compiled graph. Also added compile to the dreamverse backend launch.
2026-07-20 13:14:08 -07:00
William Lin 229419208e [ci]: opt in to fork-PR head checkout after actions/checkout guard change (#1625) 2026-07-20 13:12:22 -07:00
William Lin 65f3b946b9 [feat] Add LTX-2 and LTX-2.3 fine-tuning to the modular trainer (#1624) 2026-07-20 11:58:39 -07:00
Zhang Peiyuan 191fcbf46c [feat] add qat docs (#1621) 2026-07-19 18:37:47 -07:00
Junda Su 755a4e4470 [new-model] Add LingBot-Video Dense and MoE/refiner T2V inference (#1595) 2026-07-18 20:18:16 -07:00
9709b7513b [feat] Port NVFP4 QAT/QAD to modular train framework (#1619)
Co-authored-by: Peiyuan Zhang <email>

Co-authored-by: Peiyuan <a>
2026-07-18 15:01:09 -07:00
William Lin 32cd603515 Revert docs trusted-branch-only workflow (#1618) 2026-07-17 00:22:56 -07:00
Junda Su d4bdd3621a [new-model] Port LingBot-World-v2 (#1579) 2026-07-16 19:51:53 -07:00
William Lin e2f8322842 [ci]: skip unused Buildkite submodule checkout (#1614) 2026-07-16 18:37:57 -07:00
6966f9e0bc [fix] Z-Image (#1236) draft port: rebase + strict-load contract + bf16 encoder parity + PORT_STATUS (#1339)
Co-authored-by: Mrinaal Dogra <mdogra@ucsd.edu>
Co-authored-by: SolitaryThinker <wlsaidhi@gmail.com>
2026-07-16 18:18:25 -07:00
163 changed files with 16785 additions and 428 deletions
+2
View File
@@ -1,6 +1,8 @@
env:
IMAGE_VERSION: "py3.12-latest"
BUILDKITE_CLEAN_CHECKOUT: true
# Buildkite only launches Modal; remote jobs initialize their own submodules.
BUILDKITE_GIT_SUBMODULES: false
notify:
- github_commit_status:
+17 -4
View File
@@ -27,14 +27,25 @@ jobs:
ref: ${{ inputs.ref || '' }}
# For PR events, lint the PR head — but keep the hook definitions from
# the base branch so an untrusted PR cannot alter what gets executed.
- name: Save trusted hook config
# The gate scripts are saved too: the self-test step below executes them,
# so it must run the base-branch copies, not the PR head's.
- name: Save trusted hook config and gate scripts
if: github.event_name == 'pull_request_target'
run: cp .pre-commit-config.yaml "$RUNNER_TEMP/trusted-pre-commit-config.yaml"
- uses: actions/checkout@v4
run: |
cp .pre-commit-config.yaml "$RUNNER_TEMP/trusted-pre-commit-config.yaml"
cp -a .github/scripts "$RUNNER_TEMP/trusted-scripts"
echo "GATE_SCRIPTS_DIR=$RUNNER_TEMP/trusted-scripts" >> "$GITHUB_ENV"
# allow-unsafe-pr-checkout acknowledges checkout's pull_request_target
# guard: the head is data for the trusted hooks to lint; nothing from it
# is executed (config and gate scripts are pinned to the base branch
# above) and credentials are not persisted. SHA-pinned to v4.4.0 because
# actionlint's action schema does not know the new input yet.
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
if: github.event_name == 'pull_request_target'
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
allow-unsafe-pr-checkout: true
- name: Restore trusted hook config
if: github.event_name == 'pull_request_target'
run: cp "$RUNNER_TEMP/trusted-pre-commit-config.yaml" .pre-commit-config.yaml
@@ -48,5 +59,7 @@ jobs:
with:
extra_args: --all-files --hook-stage manual
# After pre-commit so a self-test failure cannot mask lint failures.
# GATE_SCRIPTS_DIR points at the base-branch copy on fork PRs (set above);
# push / workflow_call runs use the checked-out tree directly.
- name: Full-suite gate self-test
run: bash .github/scripts/test_gate_full_suite.sh
run: bash "${GATE_SCRIPTS_DIR:-.github/scripts}/test_gate_full_suite.sh"
+8 -14
View File
@@ -11,9 +11,7 @@ on:
- 'requirements-mkdocs.txt'
- 'scripts/check_docs_links.py'
- '.github/workflows/infra-docs.yml'
# Run the trusted base-branch workflow so fork PRs can be skipped without
# waiting for maintainer approval.
pull_request_target:
pull_request:
branches: [ main ]
paths:
- 'docs/**'
@@ -26,19 +24,21 @@ on:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
# MkDocs executes repository code; only trusted same-repository PRs run it.
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@v5
@@ -52,7 +52,6 @@ jobs:
run: uv pip install --system -r requirements-mkdocs.txt
- name: Setup Pages
if: github.event_name == 'push'
uses: actions/configure-pages@v4
- name: Build documentation
@@ -62,22 +61,17 @@ jobs:
run: python scripts/check_docs_links.py
- name: Upload artifact
if: github.event_name == 'push'
uses: actions/upload-pages-artifact@v3
with:
path: ./site
deploy:
permissions:
pages: write
id-token: write
concurrency: pages
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'push'
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to GitHub Pages
id: deployment
+2
View File
@@ -6,6 +6,7 @@ results/
wandb/
*.ipynb
*.jpg
!examples/dataset/lingbotworld2/image.jpg
*.safetensors
*.mp4
*.png
@@ -34,6 +35,7 @@ env
*.log
weights/
logs/
/Z-Image/
official_weights/
converted_weights/
+1 -1
View File
@@ -9,7 +9,7 @@
**FastVideo is a unified post-training and real-time inference framework for accelerated video generation.**
## NEWS
- `2026/06/23`: Release FastWan-QAD: 5s of Video generated in 1.8s E2E. [FastWan-QAD models](https://huggingface.co/FastVideo/FastWan-QAD-FP8-1.3B), check out the [Blog](https://haoailab.com/blogs/fastwan-qad/).
- `2026/06/23`: Release FastWan-QAD: 5s of Video generated in 1.8s E2E. See the [FastWan-QAD models](https://huggingface.co/FastVideo/FastWan-QAD-FP8-1.3B), [Attn-QAT training guide](https://haoailab.com/FastVideo/training/attn_qat/), and [blog](https://haoailab.com/blogs/fastwan-qad/).
- `2026/03/17`: Release demo: Into the Dreamverse: Vibe Directing in FastVideo, check out the [Blog](https://haoailab.com/blogs/dreamverse/).
- `2026/03/13`: Release demo: Create a 5s 1080p Video in 4.5s with FastVideo on a Single GPU, check out the [Blog](https://haoailab.com/blogs/fastvideo_realtime_1080p/).
- `2025/11/19`: Release [CausalWan2.2 I2V A14B Preview](https://huggingface.co/FastVideo/CausalWan2.2-I2V-A14B-Preview-Diffusers) models, [Blog](https://hao-ai-lab.github.io/blogs/fastvideo_causalwan_preview/) and [Inference Code!](https://github.com/hao-ai-lab/FastVideo/blob/main/examples/inference/basic/basic_self_forcing_causal_wan2_2_i2v.py).
@@ -39,6 +39,16 @@ export FASTVIDEO_GENERATION_SEGMENT_CAP="${FASTVIDEO_GENERATION_SEGMENT_CAP:-6}"
export FASTVIDEO_PROMPT_AUTO_SLEEP_MS="${FASTVIDEO_PROMPT_AUTO_SLEEP_MS:-120}"
export FASTVIDEO_PROMPT_AUTO_TIMEOUT_MS="${FASTVIDEO_PROMPT_AUTO_TIMEOUT_MS:-1800}"
# Persistent torch.compile cache so warmup pays the full autotune/codegen cost only on the first launch.
# Later launches on the same box reload the Inductor FX-graph + AOTAutograd + Triton artifacts from disk.
export DREAMVERSE_TORCH_COMPILE_CACHE_ROOT="${DREAMVERSE_TORCH_COMPILE_CACHE_ROOT:-${HOME}/.cache/dreamverse/torch_compile}"
export TORCHINDUCTOR_CACHE_DIR="${TORCHINDUCTOR_CACHE_DIR:-${DREAMVERSE_TORCH_COMPILE_CACHE_ROOT}/inductor}"
export TRITON_CACHE_DIR="${TRITON_CACHE_DIR:-${DREAMVERSE_TORCH_COMPILE_CACHE_ROOT}/triton}"
export TORCHINDUCTOR_FX_GRAPH_CACHE="${TORCHINDUCTOR_FX_GRAPH_CACHE:-1}"
export TORCHINDUCTOR_AUTOGRAD_CACHE="${TORCHINDUCTOR_AUTOGRAD_CACHE:-1}"
mkdir -p "${TORCHINDUCTOR_CACHE_DIR}" "${TRITON_CACHE_DIR}"
echo "[launch-demo] torch.compile cache: ${DREAMVERSE_TORCH_COMPILE_CACHE_ROOT} (first launch builds it, later launches reload it)"
cd "${DREAMVERSE_ROOT}"
if ! command -v dreamverse-server >/dev/null 2>&1; then
+3 -1
View File
@@ -8,13 +8,15 @@ FastVideo provides highly optimized custom attention kernels to accelerate video
* **[Sliding Tile Attention (STA)](sta/index.md)**: STA kernel support is kept in
`fastvideo-kernel`; full FastVideo STA pipeline workflow is archived in
`sta_do_not_delete`.
* **[Attn-QAT Training](../training/attn_qat.md)**: Runtime-JIT Triton forward
and backward kernels for role-local quantization-aware training.
* **Backend development guide**: See the developer guide at
[Attention Backend Development](../contributing/attention_backend.md).
## General Build Instructions
These instructions apply to building the `fastvideo-kernel` package from
source, which includes both STA and VSA kernels.
source, which includes STA, VSA, and Attn-QAT kernels.
### Prerequisites
+3 -4
View File
@@ -255,10 +255,9 @@ If you add a new CI test category:
### Documentation
`.github/workflows/infra-docs.yml` builds documentation for same-repository PRs
that touch `docs/**`, `mkdocs.yml`, `requirements-mkdocs.txt`, or the workflow
itself. Fork PRs skip this executable build instead of waiting for maintainer
approval. On pushes to `main`, it also deploys the built site to GitHub Pages.
`.github/workflows/infra-docs.yml` builds documentation for PRs that touch
`docs/**`, `mkdocs.yml`, `requirements-mkdocs.txt`, or the workflow itself. On
pushes to `main`, it also deploys the built site to GitHub Pages.
The docs job:
@@ -333,15 +333,25 @@ surfaces:
use_distill:
sources: [fastvideo.configs.pipelines.longcat.LongCatT2V480PConfig, fastvideo.configs.pipelines.longcat.LongCatT2V704PConfig]
scheduler_arch:
sources: [fastvideo.configs.pipelines.sd35.SD35Config]
sources:
- fastvideo.configs.pipelines.sd35.SD35Config
- fastvideo.configs.pipelines.zimage.ZImagePipelineConfig
text_encoder_archs:
sources: [fastvideo.configs.pipelines.sd35.SD35Config]
sources:
- fastvideo.configs.pipelines.sd35.SD35Config
- fastvideo.configs.pipelines.zimage.ZImagePipelineConfig
tokenizer_archs:
sources: [fastvideo.configs.pipelines.sd35.SD35Config]
sources:
- fastvideo.configs.pipelines.sd35.SD35Config
- fastvideo.configs.pipelines.zimage.ZImagePipelineConfig
transformer_arch:
sources: [fastvideo.configs.pipelines.sd35.SD35Config]
sources:
- fastvideo.configs.pipelines.sd35.SD35Config
- fastvideo.configs.pipelines.zimage.ZImagePipelineConfig
vae_arch:
sources: [fastvideo.configs.pipelines.sd35.SD35Config]
sources:
- fastvideo.configs.pipelines.sd35.SD35Config
- fastvideo.configs.pipelines.zimage.ZImagePipelineConfig
expand_timesteps:
sources:
- fastvideo.configs.pipelines.wan.FastWan2_2_TI2V_5B_Config
@@ -421,6 +431,8 @@ surfaces:
frame_receptive_field: "MagiHuman internal data-proxy receptive-field setting."
image_conditioning: "MagiHuman preset variant marker for reference-image conditioning."
ref_audio_offset: "MagiHuman internal data-proxy audio alignment offset."
scheduler_sigma_min: "Z-Image scheduler parity invariant; not part of the public typed inference API."
scheduler_use_reference_discrete_timesteps: "Z-Image scheduler parity invariant; not part of the public typed inference API."
sr_local_attn_layers: "MagiHuman SR internal sparse-attention layer selection."
text_offset: "MagiHuman internal data-proxy text alignment offset."
vae_stride: "MagiHuman internal VAE/data-proxy stride setting."
@@ -438,6 +450,7 @@ surfaces:
grid_sizes: request.inputs.grid_sizes
pose: request.inputs.pose
c2ws_plucker_emb: request.inputs.c2ws_plucker_emb
action_path: request.inputs.action_path
refine_from: request.inputs.refine_from
stage1_video: request.inputs.stage1_video
prompt: request.prompt
@@ -447,6 +460,7 @@ surfaces:
output_video_name: request.output.output_video_name
num_videos_per_prompt: request.sampling.num_videos_per_prompt
seed: request.sampling.seed
max_sequence_length: request.sampling.max_sequence_length
num_frames: request.sampling.num_frames
height: request.sampling.height
width: request.sampling.width
@@ -456,7 +470,10 @@ surfaces:
num_inference_steps: request.sampling.num_inference_steps
num_inference_steps_sr: request.sampling.num_inference_steps_sr
guidance_scale: request.sampling.guidance_scale
batch_cfg: request.sampling.batch_cfg
guidance_scale_2: request.sampling.guidance_scale_2
cfg_normalization: request.sampling.cfg_normalization
cfg_truncation: request.sampling.cfg_truncation
guidance_rescale: request.sampling.guidance_rescale
use_embedded_guidance: request.sampling.use_embedded_guidance
true_cfg_scale: request.sampling.true_cfg_scale
@@ -508,7 +525,6 @@ surfaces:
internal_only:
data_type: "Derived from the request shape and not a public input."
latents: "Pre-generated diffusion latents supplied by parity/debug harnesses; not a public input."
max_sequence_length: "Model-specific text-encoder sequence cap; not part of the public typed inference API."
sampling_param_extensions: {}
+6
View File
@@ -2,6 +2,12 @@
We introduce a new finetuning strategy - **Sparse-distill**, which jointly integrates **[DMD](https://arxiv.org/abs/2405.14867)** and **[VSA](https://arxiv.org/abs/2505.13389)** in a single training process. This approach combines the benefits of both distillation to shorten diffusion steps and sparse attention to reduce attention computation, enabling much faster video generation.
!!! tip "Attn-QAT DMD2 workflow"
The modular trainer also provides a Wan2.1 MixKit recipe that first
fine-tunes with fake-quantized attention, then distills the student to
timesteps `[1000, 757, 522]` while teacher and critic remain on Flash
Attention. See [Attn-QAT Training](../training/attn_qat.md).
## 📊 Model Overview
We provide two distilled models:
+147
View File
@@ -0,0 +1,147 @@
# Attn-QAT Training
Attn-QAT simulates low-bit attention during training while keeping the rest of
the training method unchanged. In the modular `fastvideo/train` framework it is
a per-role model option, not a separate training method: supervised fine-tuning
and DMD2 still own their losses and optimizer cadence.
This guide covers the QAD Wan2.1-T2V-1.3B MixKit workflow:
1. run a 4,000-step supervised Attn-QAT fine-tune;
2. export the stage-1 DCP checkpoint to Diffusers format; and
3. distill the student to three denoising steps with DMD2.
The ready-to-run configs and wrappers are in
`examples/train/scenario/qad_wan2_1_mixkit/`.
## Role-local attention backends
A DMD2 run owns three independent model roles. Configure the attention backend
on each role so fake quantization is applied only to the student:
```yaml
models:
student:
attention_backend: ATTN_QAT_TRAIN
teacher:
attention_backend: FLASH_ATTN
critic:
attention_backend: FLASH_ATTN
```
The override is active only while that role's transformer is constructed, then
the previous process-wide backend is restored. This lets student, teacher, and
critic use different implementations in one process. Invalid role-level names
fail during configuration instead of silently selecting another backend.
See [Training Infrastructure](train_infra.md) for the complete model-role
configuration reference.
## Prerequisites
- Install FastVideo and make the `fastvideo-kernel` Python package importable.
`ATTN_QAT_TRAIN` intentionally fails instead of falling back to dense
attention when its kernel cannot be loaded.
- Prepare the precomputed MixKit VAE latents and text embeddings.
- Run the commands below from the repository root. The supplied recipe expects
four GPUs by default; set `NUM_GPUS` to override it.
Download the published preprocessed dataset:
```bash
bash examples/training/finetune/wan_t2v_1.3B/mixkit/download_mixkit_data.sh
```
## Stage 1: supervised Attn-QAT fine-tuning
The stage-1 config uses `ATTN_QAT_TRAIN` on the student, sequence parallelism
across four GPUs, FP32 master weights, and 4,000 optimizer steps:
```bash
NUM_GPUS=4 \
bash examples/train/scenario/qad_wan2_1_mixkit/run_stage1.sh
```
Pass a dataset directory as the first positional argument when it differs from
the default:
```bash
NUM_GPUS=4 \
bash examples/train/scenario/qad_wan2_1_mixkit/run_stage1.sh \
/path/to/combined_parquet_dataset
```
The wrapper calls `examples/train/run.sh`; the YAML file remains the source of
truth for optimizer, validation, checkpointing, and distributed settings.
## Export the stage-1 checkpoint
Modular training checkpoints use Distributed Checkpoint (DCP) format. Export
the student before using it to initialize stage 2:
```bash
bash examples/train/scenario/qad_wan2_1_mixkit/export_stage1.sh \
checkpoints/wan_t2v_qat_finetune/checkpoint-4000 \
checkpoints/wan_t2v_qat_finetune/diffusers
```
Both arguments are optional; the command above shows their defaults.
## Stage 2: three-step DMD2 distillation
Stage 2 loads the exported student weights, keeps Attn-QAT on the student, and
uses Flash Attention for the teacher and critic:
```bash
NUM_GPUS=4 \
bash examples/train/scenario/qad_wan2_1_mixkit/run_stage2.sh \
data/HD-Mixkit-Finetune-Wan/combined_parquet_dataset \
checkpoints/wan_t2v_qat_finetune/diffusers/transformer/model.safetensors
```
The migrated recipe preserves these behaviors:
| Behavior | Modular configuration |
|---|---|
| Student fake-quantized attention | `models.student.attention_backend: ATTN_QAT_TRAIN` |
| Teacher and critic full-precision attention | Role-local `FLASH_ATTN` |
| Generator update every five critic steps | `method.generator_update_interval: 5` |
| Three-step rollout | `method.dmd_denoising_steps: [1000, 757, 522]` |
| Score timestep range | `method.min_timestep_ratio: 0.02`, `max_timestep_ratio: 0.98` |
| Legacy guidance `cond + 2(cond - uncond)` | Standard CFG scale `3.0` |
| Stage handoff | DCP checkpoint to Diffusers export to student override weights |
The timestep ratios apply to randomly sampled teacher and critic score
timesteps; `dmd_denoising_steps` separately controls the student rollout. See
[DMD Distillation](../distillation/dmd.md) for general DMD concepts.
## Architecture-specific Triton routing
The training kernel is runtime-JIT-compiled Triton code and selects its route on
every call. It supports different query and key/value sequence lengths for
cross-attention; key and value must have the same sequence length.
| Hardware/configuration | Route |
|---|---|
| SM100, validated non-causal BF16 QAT configuration with head dimension 128 | Large-tile forward and split 64x64 backward; optimized backward requires a 16-aligned KV length |
| SM120, including RTX 5090 | Previous forward tiling with joined quantized/STE P@V operations and a shallower backward pipeline for long sequences |
| Unsupported configurations | Previous Triton implementation |
Warp specialization is disabled automatically on SM100 and SM120 because the
Triton 3.7 NVWS compiler pass aborts for this kernel on Blackwell. No user
setting is required.
The available tuning and comparison controls are:
| Environment variable | Default | Effect |
|---|---|---|
| `FASTVIDEO_ATTN_QAT_FWD_MODE` | `fast` | Selects `fast`, `balanced`, or `reference` forward tiling on the SM100 optimized route |
| `FASTVIDEO_ATTN_QAT_FWD_EXACT_M` | `0` | Set to `1` to recompute reference-order softmax statistics and keep `dV` bitwise-compatible on the SM100 optimized route |
| `FASTVIDEO_ATTN_QAT_SM100_OPTIMIZED` | `1` | Set to `0` to force the previous SM100 forward and backward for comparison |
| `FASTVIDEO_ATTN_QAT_SM120_JOIN_QAT_PV` | `1` | Set to `0` to compare SM120 against the split P@V path |
The first invocation JIT-compiles the selected configuration; later calls reuse
the Triton cache. To measure the production shape, run
`python benchmarks/benchmark_attn_qat_train.py` from `fastvideo-kernel/`.
For import and backend-selection failures, see [Debugging](../utilities/debugging.md).
+13
View File
@@ -62,6 +62,18 @@ bash examples/training/finetune/wan_t2v_1.3B/crush_smol/finetune_t2v.sh
- Gradient checkpointing: `--enable_gradient_checkpointing_type "full"`
- Memory scaling: Increase `--sp_size` or reduce `--num_latent_t` to fit in memory
## Attention Quantization-Aware Training
Attn-QAT fine-tunes a model while simulating low-bit attention in the forward
and backward passes. The modular trainer can select the backend per model role,
so a later DMD2 stage can keep fake quantization on the student while the
teacher and critic use Flash Attention.
The ready-to-run Wan2.1 MixKit workflow includes supervised fine-tuning,
checkpoint export, and three-step DMD2 distillation:
**→ [Follow the Attn-QAT training guide](attn_qat.md)**
## LoRA Finetuning
LoRA (Low-Rank Adaptation) trains lightweight adapters while keeping the base model frozen. This significantly reduces memory usage and training time.
@@ -166,6 +178,7 @@ Ready-to-run training scripts are available for multiple models:
| Wan2.1 I2V 14B | I2V | `examples/training/finetune/wan_i2v_14B_480p/crush_smol/` |
| Wan2.1-Fun 1.3B InP | I2V | `examples/training/finetune/Wan2.1-Fun-1.3B-InP/crush_smol/` |
| Wan2.1 VSA | T2V/I2V | `examples/training/finetune/Wan2.1-VSA/Wan-Syn-Data/` |
| Wan2.1 T2V 1.3B Attn-QAT | QAT SFT + DMD2 | `examples/train/scenario/qad_wan2_1_mixkit/` |
Each example includes:
+6 -1
View File
@@ -43,6 +43,9 @@ Ready-to-run examples with preprocessing scripts, training launchers, and valida
**→ [Browse all training examples](examples/examples_training_index.md)**
For the complete two-stage Wan2.1 MixKit quantization-aware workflow, see
**[Attn-QAT Training](attn_qat.md)**.
Each example includes:
- `download_dataset.sh` — download sample data
@@ -59,9 +62,11 @@ FastVideo supports several training approaches:
| **Full finetune** | Adapt entire model to a new domain or style |
| **LoRA finetune** | Lightweight adaptation with frozen base weights |
| **VSA finetune** | Finetune with Variable Sparse Attention for efficiency |
| **Attn-QAT** | Train with fake-quantized attention, optionally followed by DMD2 distillation |
## Next Steps
1. **Get started**: Pick an example from the [training examples index](examples/examples_training_index.md)
2. **Prepare data**: Follow [data preprocessing](data_preprocess.md) for your own dataset
3. **Run inference**: After training, see [inference examples](../inference/examples/examples_inference_index.md)
3. **Train with quantized attention**: Follow the [Attn-QAT two-stage recipe](attn_qat.md)
4. **Run inference**: After training, see [inference examples](../inference/examples/examples_inference_index.md)
+3
View File
@@ -81,6 +81,7 @@ Common model parameters:
| `disable_custom_init_weights` | `false` | Skip custom weight initialization (use for teacher/critic) |
| `flow_shift` | `3.0` | Timestep shifting factor |
| `enable_gradient_checkpointing_type` | `null` | Gradient checkpointing (`"full"` or `null`) |
| `attention_backend` | `null` | Optional role-local backend for Wan models (for example `ATTN_QAT_TRAIN`); overrides the process default only while this role's transformer is built |
Which roles are needed depends on the training method:
@@ -298,6 +299,8 @@ method:
| `dmd_denoising_steps` | *(required)* | Timestep schedule for student rollout |
| `generator_update_interval` | `1` | Update student every N critic steps |
| `real_score_guidance_scale` | `1.0` | CFG scale for teacher predictions |
| `min_timestep_ratio` | `0.0` | Lower bound for randomly sampled teacher/critic score timesteps |
| `max_timestep_ratio` | `1.0` | Upper bound for randomly sampled teacher/critic score timesteps |
| `fake_score_learning_rate` | *(required)* | Critic optimizer learning rate |
| `fake_score_betas` | *(required)* | Critic optimizer Adam betas |
| `fake_score_lr_scheduler` | *(required)* | Critic LR scheduler type |
+4 -1
View File
@@ -78,7 +78,10 @@ If forcing a backend fails, verify optional dependencies are installed:
- `SAGE_ATTN_THREE`: upstream `sageattn3` package
- `ATTN_QAT_INFER`: `fastvideo-kernel` checkout/source install that exposes
`attn_qat_infer`
- `ATTN_QAT_TRAIN`: `fastvideo-kernel` install exposing `fastvideo_kernel`
- `ATTN_QAT_TRAIN`: `fastvideo-kernel`; its runtime-JIT Triton implementation
selects an optimized route on SM100, joins the quantized and STE P@V paths on
SM120, and retains the previous route for unsupported configurations. See
[Attn-QAT Training](../training/attn_qat.md) for architecture controls.
As a fallback, use:
+17
View File
@@ -0,0 +1,17 @@
# LingBot World 2 Example Dataset
These files were copied unchanged from the LingBot World 2 repository for the
FastVideo causal-fast inference example.
- Repository: `https://github.com/Robbyant/lingbot-world-v2.git`
- Source commit: `94f43115de8d4a4f9f282126528c300a0b232c5f`
- Source directory: `examples/03`
## Files
- `image.jpg`: source image for image-to-video generation. SHA-256:
`6ee3dacfef32cfef504dd698adb8a660cf15f686535c52fed4903fef27c0edd0`
- `poses.npy`: camera-to-world trajectory matrices. SHA-256:
`bd0a23a696e184b0b43e7767eb432bfe644690560fe327fa96961affc941c404`
- `intrinsics.npy`: camera intrinsic parameters. SHA-256:
`821fca6cf957ae8fbb1181307f02479efb1705e04c9e05734cd02fb43462e082`
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.
Binary file not shown.
@@ -0,0 +1,52 @@
"""Generate a five-second Dense LingBot-Video clip with the official defaults."""
import argparse
from pathlib import Path
from fastvideo import VideoGenerator
def parse_args() -> argparse.Namespace:
"""Parse the converted checkpoint and output paths for the sample."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--model-path",
type=Path,
required=True,
help="Path to a converted Dense LingBot-Video checkpoint.",
)
parser.add_argument(
"--output-path",
type=Path,
default=Path("outputs/lingbot-video/dense-t2v"),
help="Directory for the generated video.",
)
return parser.parse_args()
def main() -> None:
"""Load the converted Dense checkpoint and generate the default T2V sample."""
args = parse_args()
generator = VideoGenerator.from_pretrained(
str(args.model_path),
num_gpus=1,
use_fsdp_inference=False,
text_encoder_cpu_offload=True,
vae_cpu_offload=False,
pin_cpu_memory=True,
)
try:
generator.generate({
"prompt": "A red fox runs through fresh snow at sunrise.",
"output": {
"output_path": str(args.output_path),
"save_video": True,
"return_frames": False,
},
})
finally:
generator.shutdown()
if __name__ == "__main__":
main()
@@ -0,0 +1,52 @@
# SPDX-License-Identifier: Apache-2.0
"""Run LingBot World 2 14B causal-fast I2V generation with FastVideo."""
import os
from pathlib import Path
from fastvideo import VideoGenerator
REPO_ROOT = Path(__file__).resolve().parents[3]
DATASET_DIR = REPO_ROOT / "examples" / "dataset" / "lingbotworld2"
OUTPUT_PATH = REPO_ROOT / "outputs" / "lingbotworld2_causal_fast.mp4"
def main() -> None:
"""Load the native FastVideo LingBot World 2 causal-fast pipeline and generate one video."""
generator = VideoGenerator.from_pretrained(
os.environ["LINGBOTWORLD2_MODEL_PATH"],
num_gpus=8,
sp_size=8,
hsdp_shard_dim=8,
use_fsdp_inference=True,
dit_layerwise_offload=False,
dit_cpu_offload=False,
vae_cpu_offload=False,
text_encoder_cpu_offload=False,
pin_cpu_memory=True,
override_pipeline_cls_name="LingBotWorld2CausalFastPipeline",
)
try:
generator.generate_video(
"A serene lakeside scene with a lone tree standing in calm water, surrounded by distant snow-capped mountains under a bright blue sky with drifting white clouds; gentle ripples reflect the tree and sky, creating a tranquil, meditative atmosphere.",
image_path=str(DATASET_DIR / "image.jpg"),
action_path=str(DATASET_DIR),
output_path=str(OUTPUT_PATH),
save_video=True,
height=480,
width=832,
num_frames=65,
num_inference_steps=4,
guidance_scale=1.0,
negative_prompt="",
fps=16,
seed=42,
)
finally:
generator.shutdown()
if __name__ == "__main__":
main()
+96
View File
@@ -0,0 +1,96 @@
# SPDX-License-Identifier: Apache-2.0
"""Run Z-Image-Turbo text-to-image generation through FastVideo.
User story:
"I want the official Z-Image-Turbo defaults and a deterministic PNG from
a local or Hugging Face checkpoint."
"""
import argparse
from pathlib import Path
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
OutputConfig,
ParallelismConfig,
PipelineSelection,
SamplingConfig,
)
DEFAULT_PROMPT = (
"Young Chinese woman in red Hanfu, intricate embroidery. Impeccable makeup, red floral forehead pattern. "
"Elaborate high bun, golden phoenix headdress, red flowers, beads. Holds round folding fan with lady, trees, bird. "
"Neon lightning-bolt lamp (⚡️), bright yellow glow, above extended left palm. Soft-lit outdoor night background, "
"silhouetted tiered pagoda (西安大雁塔), blurred colorful distant lights."
)
DEFAULT_REVISION = "f332072aa78be7aecdf3ee76d5c247082da564a6"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run Z-Image-Turbo text-to-image generation.")
parser.add_argument("--model-path", default="Tongyi-MAI/Z-Image-Turbo")
parser.add_argument("--revision", default=DEFAULT_REVISION)
parser.add_argument("--output", default="outputs/zimage/zimage_turbo.png")
parser.add_argument("--prompt", default=DEFAULT_PROMPT)
parser.add_argument("--negative-prompt", default="")
parser.add_argument("--height", type=int, default=1024)
parser.add_argument("--width", type=int, default=1024)
parser.add_argument("--steps", type=int, default=8)
parser.add_argument("--guidance-scale", type=float, default=0.0)
parser.add_argument("--max-sequence-length", type=int, default=512)
parser.add_argument("--cfg-normalization", action=argparse.BooleanOptionalAction, default=False)
parser.add_argument("--cfg-truncation", type=float, default=1.0)
parser.add_argument("--seed", type=int, default=42)
return parser.parse_args()
def main() -> None:
args = parse_args()
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=args.model_path,
revision=args.revision,
engine=EngineConfig(
num_gpus=1,
parallelism=ParallelismConfig(tp_size=1, sp_size=1),
use_fsdp_inference=False,
),
# The model registry selects the native zimage_turbo preset.
pipeline=PipelineSelection(workload_type="t2i"),
))
try:
generator.generate(
GenerationRequest(
prompt=args.prompt,
negative_prompt=args.negative_prompt,
sampling=SamplingConfig(
height=args.height,
width=args.width,
num_frames=1,
fps=1,
num_inference_steps=args.steps,
guidance_scale=args.guidance_scale,
max_sequence_length=args.max_sequence_length,
cfg_normalization=args.cfg_normalization,
cfg_truncation=args.cfg_truncation,
seed=args.seed,
),
output=OutputConfig(
output_path=str(output),
save_video=True,
return_frames=False,
),
))
finally:
generator.shutdown()
if __name__ == "__main__":
main()
+4
View File
@@ -91,3 +91,7 @@ examples/train/
```
See `configs/README.md` and `scenario/README.md` for details.
The featured QAD Wan2.1 MixKit scenario runs Attn-QAT supervised fine-tuning,
checkpoint export, and three-step DMD2. See the
[Attn-QAT training guide](https://haoailab.com/FastVideo/training/attn_qat/).
+3
View File
@@ -25,6 +25,7 @@ models:
disable_custom_init_weights: false # default: false
flow_shift: 3.0 # default: 3.0
enable_gradient_checkpointing_type: null # default: null (falls back to training.model)
attention_backend: null # default: null (global/default); role-local when set
teacher:
_target_: fastvideo.train.models.wan.WanModel
@@ -52,6 +53,8 @@ method:
rollout_mode: simulate # required: "simulate" or "data_latent"
generator_update_interval: 5 # default: 1
dmd_denoising_steps: [1000, 750, 500, 250] # SDE timestep schedule
min_timestep_ratio: 0.0 # score-model timestep lower bound
max_timestep_ratio: 1.0 # score-model timestep upper bound
# Critic optimizer (all required — no fallback)
fake_score_learning_rate: 8.0e-6
@@ -0,0 +1,95 @@
# LTX-2.3 T2V overfitting test config.
#
# Overfits on a single short video (480x832, 81 frames @ 24fps) to
# verify the LTX-2 training plugin works end-to-end on an LTX-2.3
# checkpoint (gated attention, cross-attention AdaLN, 4096-d
# post-connector text embeddings, no in-DiT caption projection).
# Uses the distilled checkpoint (validation is 8 sampling steps).
#
# Preprocess data first (writes data/ltx2_3_overfit_preprocessed):
# CUDA_VISIBLE_DEVICES=0 \
# LTX2_OVERFIT_MODEL=FastVideo/LTX-2.3-Distilled-Diffusers \
# LTX2_OVERFIT_OUTPUT_DIR=data/ltx2_3_overfit_preprocessed \
# python fastvideo/pipelines/preprocess/preprocess_ltx2_overfit.py
#
# Run:
# NUM_GPUS=4 bash examples/train/run.sh examples/train/configs/overfit_ltx2_3_t2v.yaml
models:
student:
_target_: fastvideo.train.models.ltx2.LTX2Model
init_from: FastVideo/LTX-2.3-Distilled-Diffusers
trainable: true
enable_gradient_checkpointing_type: full
method:
_target_: fastvideo.train.methods.fine_tuning.finetune.FineTuneMethod
training:
distributed:
num_gpus: 4
sp_size: 1
tp_size: 1
hsdp_replicate_dim: 1
hsdp_shard_dim: 4
data:
data_path: data/ltx2_3_overfit_preprocessed
dataloader_num_workers: 0
train_batch_size: 1
# LTX2Model requires 0.0: CFG dropout would zero post-connector
# embeddings, which is not the model's unconditional input.
training_cfg_rate: 0.0
seed: 42
num_latent_t: 11 # (81 - 1) / 8 + 1
num_height: 480
num_width: 832
num_frames: 81
optimizer:
learning_rate: 5.0e-5
betas: [0.9, 0.999]
weight_decay: 0.0
lr_scheduler: constant
lr_warmup_steps: 0
loop:
max_train_steps: 300
gradient_accumulation_steps: 1
checkpoint:
output_dir: outputs/ltx2_3_overfit
# A full training-state checkpoint is ~150GB for the 13B trainable
# video branch; disable saves for the overfit smoke run.
training_state_checkpointing_steps: 0
checkpoints_total_limit: 1
tracker:
trackers: [wandb]
project_name: fastvideo_ltx2
run_name: ltx2_3_overfit
model:
# LTX2Model.predict_noise converts the DiT's x0 output to velocity,
# so the default noise-minus-clean target reproduces the official
# unweighted masked-MSE (mask is all-ones for plain T2V).
precondition_outputs: false
enable_gradient_checkpointing_type: full
callbacks:
grad_clip:
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback
max_grad_norm: 1.0
validation:
_target_: fastvideo.train.callbacks.validation.ValidationCallback
pipeline_target: fastvideo.pipelines.basic.ltx2.ltx2_pipeline.LTX2Pipeline
dataset_file: data/ltx2_3_overfit_preprocessed/validation_prompts.json
every_steps: 50
sampling_steps: [8]
guidance_scale: 1.0
num_frames: 81
# Required so the LTX2T2VConfig pipeline config is resolved from
# init_from (without a `pipeline:` key the loader falls back to a
# generic PipelineConfig and the LTX-2 DiT cannot be constructed).
pipeline: {}
@@ -0,0 +1,90 @@
# LTX-2 T2V overfitting test config.
#
# Overfits on a single short video (480x832, 81 frames @ 24fps) to
# verify the LTX-2 training plugin works end-to-end. Uses the
# distilled checkpoint (validation is 8 sampling steps, single pass).
#
# Preprocess data first (writes data/ltx2_overfit_preprocessed):
# CUDA_VISIBLE_DEVICES=0 python fastvideo/pipelines/preprocess/preprocess_ltx2_overfit.py
#
# Run:
# NUM_GPUS=4 bash examples/train/run.sh examples/train/configs/overfit_ltx2_t2v.yaml
models:
student:
_target_: fastvideo.train.models.ltx2.LTX2Model
init_from: FastVideo/LTX2-Distilled-Diffusers
trainable: true
enable_gradient_checkpointing_type: full
method:
_target_: fastvideo.train.methods.fine_tuning.finetune.FineTuneMethod
training:
distributed:
num_gpus: 4
sp_size: 1
tp_size: 1
hsdp_replicate_dim: 1
hsdp_shard_dim: 4
data:
data_path: data/ltx2_overfit_preprocessed
dataloader_num_workers: 0
train_batch_size: 1
# LTX2Model requires 0.0: CFG dropout would zero post-connector
# embeddings, which is not the model's unconditional input.
training_cfg_rate: 0.0
seed: 42
num_latent_t: 11 # (81 - 1) / 8 + 1
num_height: 480
num_width: 832
num_frames: 81
optimizer:
learning_rate: 5.0e-5
betas: [0.9, 0.999]
weight_decay: 0.0
lr_scheduler: constant
lr_warmup_steps: 0
loop:
max_train_steps: 300
gradient_accumulation_steps: 1
checkpoint:
output_dir: outputs/ltx2_overfit
# A full training-state checkpoint is ~150GB for the 13B trainable
# video branch; disable saves for the overfit smoke run.
training_state_checkpointing_steps: 0
checkpoints_total_limit: 1
tracker:
trackers: [wandb]
project_name: fastvideo_ltx2
run_name: ltx2_overfit
model:
# LTX2Model.predict_noise converts the DiT's x0 output to velocity,
# so the default noise-minus-clean target reproduces the official
# unweighted masked-MSE (mask is all-ones for plain T2V).
precondition_outputs: false
enable_gradient_checkpointing_type: full
callbacks:
grad_clip:
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback
max_grad_norm: 1.0
validation:
_target_: fastvideo.train.callbacks.validation.ValidationCallback
pipeline_target: fastvideo.pipelines.basic.ltx2.ltx2_pipeline.LTX2Pipeline
dataset_file: data/ltx2_overfit_preprocessed/validation_prompts.json
every_steps: 50
sampling_steps: [8]
guidance_scale: 1.0
num_frames: 81
# Required so the LTX2T2VConfig pipeline config is resolved from
# init_from (without a `pipeline:` key the loader falls back to a
# generic PipelineConfig and the LTX-2 DiT cannot be constructed).
pipeline: {}
+5 -2
View File
@@ -5,9 +5,12 @@ all configs, scripts, and data needed to run a complete workflow.
```
scenario/
└── ode_init_self_forcing_wan_causal/ # KD → export → Self-Forcing
├── ode_init_self_forcing_wan_causal/ # KD → export → Self-Forcing
└── qad_wan2_1_mixkit/ # Attn-QAT SFT → export → DMD2
```
See the `usage.md` inside each scenario for step-by-step instructions.
See the `usage.md` inside each scenario for step-by-step instructions. The QAD
workflow is also documented in the website-visible
[Attn-QAT training guide](https://haoailab.com/FastVideo/training/attn_qat/).
For single-step configs, see `examples/train/configs/`.
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Export a modular-trainer DCP checkpoint for stage-2 initialization.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../../.." && pwd)"
cd "${REPO_ROOT}"
CHECKPOINT_DIR=${1:-checkpoints/wan_t2v_qat_finetune/checkpoint-4000}
OUTPUT_DIR=${2:-checkpoints/wan_t2v_qat_finetune/diffusers}
python -m fastvideo.train.entrypoint.dcp_to_diffusers \
--role student \
--checkpoint "${CHECKPOINT_DIR}" \
--output-dir "${OUTPUT_DIR}"
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Run the modular Attn-QAT finetune recipe.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../../.." && pwd)"
cd "${REPO_ROOT}"
DATA_DIR=${1:-data/HD-Mixkit-Finetune-Wan/combined_parquet_dataset}
NUM_GPUS=${NUM_GPUS:-4}
export NUM_GPUS
export FASTVIDEO_ATTN_QAT_FWD_EXACT_M=${FASTVIDEO_ATTN_QAT_FWD_EXACT_M:-0}
bash "${REPO_ROOT}/examples/train/run.sh" \
"${SCRIPT_DIR}/stage1_attn_qat_finetune.yaml" \
--training.data.data_path "${DATA_DIR}" \
--training.distributed.num_gpus "${NUM_GPUS}" \
--training.distributed.sp_size "${NUM_GPUS}" \
--training.distributed.hsdp_replicate_dim 1 \
--training.distributed.hsdp_shard_dim "${NUM_GPUS}"
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Run modular DMD2 with Attn-QAT on the student only.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../../../.." && pwd)"
cd "${REPO_ROOT}"
DATA_DIR=${1:-data/HD-Mixkit-Finetune-Wan/combined_parquet_dataset}
INIT_WEIGHTS=${2:-checkpoints/wan_t2v_qat_finetune/diffusers/transformer/model.safetensors}
NUM_GPUS=${NUM_GPUS:-4}
export NUM_GPUS
if [[ ! -f "${INIT_WEIGHTS}" ]]; then
echo "Missing exported stage-1 weights: ${INIT_WEIGHTS}" >&2
echo "Run export_stage1.sh before stage 2." >&2
exit 1
fi
bash "${REPO_ROOT}/examples/train/run.sh" \
"${SCRIPT_DIR}/stage2_attn_qat_dmd.yaml" \
--models.student.transformer_override_safetensor "${INIT_WEIGHTS}" \
--training.data.data_path "${DATA_DIR}" \
--training.distributed.num_gpus "${NUM_GPUS}" \
--training.distributed.sp_size 1 \
--training.distributed.hsdp_replicate_dim "${NUM_GPUS}" \
--training.distributed.hsdp_shard_dim 1
@@ -0,0 +1,72 @@
# QAD stage 1: Attn-QAT finetune of Wan2.1-T2V-1.3B on MixKit.
models:
student:
_target_: fastvideo.train.models.wan.WanModel
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
trainable: true
# Role-local: does not change the backend used by other models loaded in
# this process.
attention_backend: ATTN_QAT_TRAIN
method:
_target_: fastvideo.train.methods.fine_tuning.finetune.FineTuneMethod
training:
distributed:
num_gpus: 4
sp_size: 4
tp_size: 1
hsdp_replicate_dim: 1
hsdp_shard_dim: 4
data:
data_path: data/HD-Mixkit-Finetune-Wan/combined_parquet_dataset
dataloader_num_workers: 1
train_batch_size: 1
training_cfg_rate: 0.1
seed: 1000
num_latent_t: 20
num_height: 480
num_width: 832
num_frames: 77
optimizer:
learning_rate: 1.0e-6
betas: [0.9, 0.999]
weight_decay: 0.01
lr_scheduler: constant
lr_warmup_steps: 0
loop:
max_train_steps: 4000
gradient_accumulation_steps: 1
checkpoint:
output_dir: checkpoints/wan_t2v_qat_finetune
training_state_checkpointing_steps: 500
checkpoints_total_limit: 0
tracker:
project_name: wan_t2v_qat_finetune
run_name: wan_t2v_qat_finetune
model:
enable_gradient_checkpointing_type: full
dit_precision: fp32
callbacks:
grad_clip:
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback
max_grad_norm: 1.0
validation:
_target_: fastvideo.train.callbacks.validation.ValidationCallback
pipeline_target: fastvideo.pipelines.basic.wan.wan_pipeline.WanPipeline
dataset_file: examples/training/finetune/wan_t2v_1.3B/crush_smol/validation.json
every_steps: 50
sampling_steps: [50]
guidance_scale: 5.0
pipeline:
flow_shift: 1
@@ -0,0 +1,101 @@
# QAD stage 2: distill the Attn-QAT student to three sampling steps.
#
# Export the stage-1 DCP checkpoint first, then point
# models.student.transformer_override_safetensor at the exported weight file.
models:
student:
_target_: fastvideo.train.models.wan.WanModel
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
transformer_override_safetensor: checkpoints/wan_t2v_qat_finetune/diffusers/transformer/model.safetensors
trainable: true
attention_backend: ATTN_QAT_TRAIN
teacher:
_target_: fastvideo.train.models.wan.WanModel
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
trainable: false
disable_custom_init_weights: true
attention_backend: FLASH_ATTN
critic:
_target_: fastvideo.train.models.wan.WanModel
init_from: Wan-AI/Wan2.1-T2V-1.3B-Diffusers
trainable: true
disable_custom_init_weights: true
attention_backend: FLASH_ATTN
method:
_target_: fastvideo.train.methods.distribution_matching.dmd2.DMD2Method
rollout_mode: data_latent
generator_update_interval: 5
dmd_denoising_steps: [1000, 757, 522]
min_timestep_ratio: 0.02
max_timestep_ratio: 0.98
# The modular DMD method uses standard CFG:
# uncond + scale * (cond - uncond). This is equivalent to the legacy
# recipe's cond + 2.0 * (cond - uncond).
real_score_guidance_scale: 3.0
# The legacy recipe inherited these values from its global optimizer.
fake_score_learning_rate: 2.0e-6
fake_score_betas: [0.9, 0.999]
fake_score_lr_scheduler: constant
training:
distributed:
num_gpus: 4
sp_size: 1
tp_size: 1
hsdp_replicate_dim: 4
hsdp_shard_dim: 1
data:
data_path: data/HD-Mixkit-Finetune-Wan/combined_parquet_dataset
dataloader_num_workers: 4
train_batch_size: 1
training_cfg_rate: 0.0
seed: 1000
num_latent_t: 20
num_height: 480
num_width: 832
num_frames: 77
optimizer:
learning_rate: 2.0e-6
betas: [0.9, 0.999]
weight_decay: 0.01
lr_scheduler: constant
lr_warmup_steps: 0
loop:
max_train_steps: 2000
gradient_accumulation_steps: 1
checkpoint:
output_dir: checkpoints/wan_t2v_distill_dmd_qat
training_state_checkpointing_steps: 500
checkpoints_total_limit: 3
tracker:
project_name: wan_t2v_distill_dmd_qat
run_name: wan_t2v_distill_dmd_qat
model:
enable_gradient_checkpointing_type: full
dit_precision: fp32
callbacks:
grad_clip:
_target_: fastvideo.train.callbacks.grad_clip.GradNormClipCallback
max_grad_norm: 1.0
validation:
_target_: fastvideo.train.callbacks.validation.ValidationCallback
pipeline_target: fastvideo.pipelines.basic.wan.wan_dmd_pipeline.WanDMDPipeline
dataset_file: examples/training/finetune/wan_t2v_1.3B/crush_smol/validation.json
every_steps: 200
sampling_steps: [3]
sampling_timesteps: [1000, 757, 522]
guidance_scale: 6.0
pipeline:
flow_shift: 3
@@ -0,0 +1,26 @@
# QAD Wan2.1 MixKit Attn-QAT
This scenario runs entirely on the modular `fastvideo/train` stack. The
student's attention backend is configured per role, so DMD2 can keep the
teacher and critic on Flash Attention while the student uses the fake-quantized
Attn-QAT kernel.
From the repository root:
```bash
# 1. Download the preprocessed MixKit data.
bash examples/training/finetune/wan_t2v_1.3B/mixkit/download_mixkit_data.sh
# 2. Stage 1: Attn-QAT supervised finetune.
NUM_GPUS=4 bash examples/train/scenario/qad_wan2_1_mixkit/run_stage1.sh
# 3. Export the stage-1 DCP checkpoint to a Diffusers weight file.
bash examples/train/scenario/qad_wan2_1_mixkit/export_stage1.sh
# 4. Stage 2: three-step Attn-QAT DMD2 distillation.
NUM_GPUS=4 bash examples/train/scenario/qad_wan2_1_mixkit/run_stage2.sh
```
The two YAML configs are also directly runnable through `examples/train/run.sh`.
The wrapper scripts only provide dataset/checkpoint paths and derive distributed
dimensions from `NUM_GPUS`.
@@ -52,7 +52,7 @@ for the full parameter reference.
## Train (QAT finetune)
With the data in place, run the quantization-aware finetune. The 4-bit attention
path is **config-driven** — selected purely by an env var, no monkey-patching:
path is **config-driven** and selected by an environment variable:
```bash
bash examples/training/finetune/wan_t2v_1.3B/mixkit/finetune_qat.sh
@@ -60,10 +60,19 @@ bash examples/training/finetune/wan_t2v_1.3B/mixkit/finetune_qat.sh
NUM_GPUS=4 bash .../mixkit/finetune_qat.sh data/HD-Mixkit-Finetune-Wan/combined_parquet_dataset/
```
`FASTVIDEO_ATTENTION_BACKEND=ATTN_QAT_TRAIN` routes attention through the
fake-quantized Triton kernel (straight-through estimator), so the DiT learns to
absorb FP4 attention error. This kernel is Triton, so it runs on both `sm_100`
(B200/GB200) and `sm_120` (RTX 5090).
`FASTVIDEO_ATTENTION_BACKEND=ATTN_QAT_TRAIN` keeps the fake-quantized Triton
forward and backward (straight-through estimator). Both kernels ship in
`fastvideo-kernel`: SM100 automatically uses the optimized Triton path for the
production non-causal, head-dimension-128 configuration, while SM120 GPUs such
as RTX 5090 join the quantized and STE P@V operations and use a shallower
backward pipeline for long sequences. Set
`FASTVIDEO_ATTN_QAT_SM120_JOIN_QAT_PV=0` to compare against the split P@V path.
The script defaults
`FASTVIDEO_ATTN_QAT_FWD_EXACT_M=0` for throughput; set it to `1` to reproduce
the previous forward softmax statistic and bitwise-compatible `dV`.
For the website-visible modular SFT-to-DMD2 workflow, see the
[Attn-QAT training guide](https://haoailab.com/FastVideo/training/attn_qat/).
## Train stage 2 (QAT DMD distillation to 3 steps)
@@ -71,8 +80,8 @@ Distill the QAT-finetuned generator down to **3 sampling steps**. Only the
generator is quantized (Attn-QAT); the teacher (`real_score`) and critic
(`fake_score`) stay full precision. This is enforced in the loader
(`component_loader.py`, via the `_loading_teacher_critic_model` flag), so the
same global `ATTN_QAT_TRAIN` env reaches **only** the generator — no per-model
flags or monkey-patching.
same global `ATTN_QAT_TRAIN` env reaches **only** the generator, with no
per-model flags.
```bash
# generator init = the stage-1 finetune checkpoint
@@ -2,20 +2,23 @@
# QAD recipe — quantization-aware finetune of Wan2.1-T2V-1.3B with fake-quant
# (Attn-QAT) attention.
#
# The 4-bit attention path is selected purely by env var (config-driven, no
# monkey-patching): FASTVIDEO_ATTENTION_BACKEND=ATTN_QAT_TRAIN routes attention
# through the fake-quantized Triton kernel (straight-through estimator), so the
# DiT learns to absorb FP4 attention error instead of fighting it.
# The 4-bit attention path is selected by env var:
# FASTVIDEO_ATTENTION_BACKEND=ATTN_QAT_TRAIN keeps the fake-quantized Triton
# forward and backward, so the DiT learns to absorb FP4 attention error instead
# of fighting it. SM100 selects the optimized kernels; RTX 5090 keeps the
# previous Triton implementation.
#
# Data: run download_mixkit_data.sh first (preprocessed Parquet).
#
# Verified end-to-end on Blackwell (GB200/sm_100): the ATTN_QAT_TRAIN backend is
# selected (not a fallback), forward+backward run, loss/grad are healthy, and
# validation generates videos. The kernel is Triton so it runs on sm_100 and
# sm_120 alike (the FP4 inference kernel, by contrast, is sm_120-only).
# validation generates videos.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../../../.." && pwd)"
export PYTHONPATH="${REPO_ROOT}/fastvideo-kernel/python${PYTHONPATH:+:${PYTHONPATH}}"
export FASTVIDEO_ATTENTION_BACKEND=ATTN_QAT_TRAIN # <-- enables Attn-QAT training
export FASTVIDEO_ATTN_QAT_FWD_EXACT_M=${FASTVIDEO_ATTN_QAT_FWD_EXACT_M:-0}
export WANDB_MODE=${WANDB_MODE:-online}
export TOKENIZERS_PARALLELISM=false
@@ -23,6 +26,8 @@ MODEL_PATH="Wan-AI/Wan2.1-T2V-1.3B-Diffusers"
DATA_DIR=${1:-"data/HD-Mixkit-Finetune-Wan/combined_parquet_dataset/"}
VALIDATION_FILE="$(dirname "$0")/../crush_smol/validation.json"
NUM_GPUS=${NUM_GPUS:-4}
MAX_TRAIN_STEPS=${MAX_TRAIN_STEPS:-4000}
VALIDATION_SAMPLING_STEPS=${VALIDATION_SAMPLING_STEPS:-50}
torchrun --nnodes 1 --nproc_per_node "${NUM_GPUS}" \
fastvideo/training/wan_training_pipeline.py \
@@ -30,15 +35,15 @@ torchrun --nnodes 1 --nproc_per_node "${NUM_GPUS}" \
--hsdp_replicate_dim 1 --hsdp_shard_dim "${NUM_GPUS}" \
--model_path "${MODEL_PATH}" --pretrained_model_name_or_path "${MODEL_PATH}" \
--data_path "${DATA_DIR}" --dataloader_num_workers 1 \
--max_train_steps 2000 --train_batch_size 1 --train_sp_batch_size 1 \
--max_train_steps "${MAX_TRAIN_STEPS}" --train_batch_size 1 --train_sp_batch_size 1 \
--gradient_accumulation_steps 1 \
--num_latent_t 20 --num_height 480 --num_width 832 --num_frames 77 \
--enable_gradient_checkpointing_type full \
--log_validation --validation_dataset_file "${VALIDATION_FILE}" \
--validation_steps 200 --validation_sampling_steps 50 --validation_guidance_scale 3.0 \
--learning_rate 5e-5 --mixed_precision bf16 --weight_decay 1e-4 --max_grad_norm 1.0 \
--validation_steps 50 --validation_sampling_steps "${VALIDATION_SAMPLING_STEPS}" --validation_guidance_scale 5.0 \
--learning_rate 1e-6 --mixed_precision bf16 --weight_decay 0.01 --max_grad_norm 1.0 \
--weight_only_checkpointing_steps 500 --training_state_checkpointing_steps 500 \
--tracker_project_name wan_t2v_qat_finetune --output_dir checkpoints/wan_t2v_qat_finetune \
--inference_mode False --training_cfg_rate 0.1 --not_apply_cfg_solver \
--dit_precision fp32 --num_euler_timesteps 50 --ema_start_step 0 \
--dit_precision fp32 --num_euler_timesteps 50 --ema_start_step 0 --flow_shift 1 \
--multi_phased_distill_schedule "4000-1"
+27
View File
@@ -108,6 +108,33 @@ out = moba_attn_varlen(q, k, v, cu_seqlens_q, cu_seqlens_k, ...)
## Benchmark
### Attn-QAT training
The default shape matches one sequence-parallel rank of the 4-GPU
Wan2.1-T2V-1.3B MixKit recipe (`B=1, H=3, L=31200, D=128`):
```bash
cd fastvideo-kernel
python benchmarks/benchmark_attn_qat_train.py
```
The benchmark reports both conventional attention FLOPs and the extra matrix
multiplications executed by the QAT straight-through path. Override
`--peak-tflops` when running on a GPU other than RTX 5090.
The QAT kernel is entirely Triton and routes by architecture at runtime. SM100
uses a large-tile forward and split 64x64 backward for the production
non-causal, head-dimension-128 configuration with a 16-aligned KV length. SM120
(including RTX 5090) keeps the previous tiling but joins the quantized and STE
P@V operations and uses a shallower backward pipeline for long sequences. Set
`FASTVIDEO_ATTN_QAT_SM120_JOIN_QAT_PV=0` to compare against the split P@V path.
Unsupported configurations retain the previous implementation. Set
`FASTVIDEO_ATTN_QAT_SM100_OPTIMIZED=0` to benchmark that previous path on SM100. Forward tuning is available through
`FASTVIDEO_ATTN_QAT_FWD_MODE=fast|balanced|reference`; exact reference-order
softmax statistics are controlled by `FASTVIDEO_ATTN_QAT_FWD_EXACT_M` and are
disabled by default for maximum throughput. Set it to `1` for reference-order
statistics and bitwise-compatible `dV`.
### VSA (block-sparse) TFLOPs
After building/installing `fastvideo-kernel`, run:
@@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""Benchmark the Attn-QAT training kernel on a single GPU.
Defaults model one rank of the 4-GPU Wan2.1-T2V-1.3B MixKit recipe:
``B=1, H=12/4, L=20*30*52, D=128``.
"""
from __future__ import annotations
import argparse
import statistics
import time
from collections.abc import Callable
import torch
from fastvideo_kernel.triton_kernels.attn_qat_train import attention
RTX_5090_DENSE_BF16_TFLOPS = 209.5
def _qat_attention(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor) -> torch.Tensor:
consumer_blackwell = torch.cuda.get_device_capability()[0] == 12
return attention(
q,
k,
v,
False,
q.shape[-1]**-0.5,
True, # use_qat_qkv_backward
False, # smooth_k
not consumer_blackwell, # warp_specialize
True, # IS_QAT
False, # two_level_quant_P
True, # fake_quant_P
True, # use_high_prec_o
False, # smooth_q
False, # use_global_sf_P
False, # use_global_sf_QKV
)
def _measure_ms(fn: Callable[[], object], warmup: int, repeat: int) -> tuple[float, float, float]:
for _ in range(warmup):
fn()
torch.cuda.synchronize()
samples = []
for _ in range(repeat):
start = torch.cuda.Event(enable_timing=True)
end = torch.cuda.Event(enable_timing=True)
start.record()
fn()
end.record()
end.synchronize()
samples.append(start.elapsed_time(end))
return statistics.median(samples), min(samples), max(samples)
def _format_result(
label: str,
timing_ms: tuple[float, float, float],
algorithmic_flops: int,
executed_matmul_flops: int,
peak_tflops: float,
) -> str:
median_ms, min_ms, max_ms = timing_ms
algorithmic_tflops = algorithmic_flops / (median_ms * 1e9)
executed_tflops = executed_matmul_flops / (median_ms * 1e9)
return (
f"{label}: {median_ms:.3f} ms (min={min_ms:.3f}, max={max_ms:.3f}), "
f"algorithmic={algorithmic_tflops:.2f} TFLOPS/{100 * algorithmic_tflops / peak_tflops:.2f}% MFU, "
f"executed_matmul={executed_tflops:.2f} TFLOPS/{100 * executed_tflops / peak_tflops:.2f}% MFU"
)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--batch-size", type=int, default=1)
parser.add_argument("--heads", type=int, default=3, help="Heads per SP rank; Wan 1.3B has 12 total.")
parser.add_argument("--query-length", type=int, default=31_200)
parser.add_argument("--kv-length", type=int, help="Defaults to --query-length.")
parser.add_argument("--head-dim", type=int, default=128)
parser.add_argument("--warmup", type=int, default=3)
parser.add_argument("--repeat", type=int, default=10)
parser.add_argument(
"--peak-tflops",
type=float,
default=RTX_5090_DENSE_BF16_TFLOPS,
help="Dense BF16 Tensor TFLOPS with FP32 accumulation; default is RTX 5090 boost-clock peak.",
)
args = parser.parse_args()
kv_length = args.kv_length or args.query_length
torch.manual_seed(0)
q_shape = (args.batch_size, args.heads, args.query_length, args.head_dim)
kv_shape = (args.batch_size, args.heads, kv_length, args.head_dim)
q = torch.randn(q_shape, device="cuda", dtype=torch.bfloat16, requires_grad=True)
k = torch.randn(kv_shape, device="cuda", dtype=torch.bfloat16, requires_grad=True)
v = torch.randn(kv_shape, device="cuda", dtype=torch.bfloat16, requires_grad=True)
grad_out = torch.randn_like(q)
compile_start = time.perf_counter()
output = _qat_attention(q, k, v)
torch.cuda.synchronize()
compile_seconds = time.perf_counter() - compile_start
forward_ms = _measure_ms(lambda: _qat_attention(q, k, v), args.warmup, args.repeat)
backward_ms = _measure_ms(
lambda: torch.autograd.grad(output, (q, k, v), grad_out, retain_graph=True),
args.warmup,
args.repeat,
)
base_flops = args.batch_size * args.heads * args.query_length * kv_length * args.head_dim
# Conventional attention FLOPs are 4*base forward and 10*base backward.
# QAT additionally computes the STE high-precision P@V path in forward and
# the quantized-P dV path in backward, for 6*base and 14*base matmul FLOPs.
print(f"device: {torch.cuda.get_device_name()}")
print(f"q: {q_shape}; k/v: {kv_shape}; compile+first-forward: {compile_seconds:.3f} s")
print(_format_result("forward", forward_ms, 4 * base_flops, 6 * base_flops, args.peak_tflops))
print(_format_result("backward", backward_ms, 10 * base_flops, 14 * base_flops, args.peak_tflops))
if __name__ == "__main__":
main()
@@ -20,14 +20,68 @@ def supports_host_descriptor():
return is_cuda() and torch.cuda.get_device_capability()[0] >= 9
def is_sm100(device=None):
return is_cuda() and torch.cuda.get_device_capability(device) == (10, 0)
def is_blackwell():
return is_cuda() and torch.cuda.get_device_capability()[0] == 10
def is_consumer_blackwell():
return is_cuda() and torch.cuda.get_device_capability()[0] == 12
def is_hopper():
return is_cuda() and torch.cuda.get_device_capability()[0] == 9
def _sm100_optimization_enabled():
return os.environ.get("FASTVIDEO_ATTN_QAT_SM100_OPTIMIZED", "1") != "0"
def _sm100_exact_m_enabled():
return os.environ.get("FASTVIDEO_ATTN_QAT_FWD_EXACT_M", "0") != "0"
def _consumer_blackwell_join_qat_pv_enabled():
"""Return whether SM120 uses the joined quantized/STE P@V path."""
return os.environ.get("FASTVIDEO_ATTN_QAT_SM120_JOIN_QAT_PV", "1") != "0"
def _use_sm100_optimized_qat(
device,
head_dim: int,
causal: bool,
is_qat: bool,
fake_quant_p: bool,
two_level_quant_p: bool,
use_global_sf_p: bool,
) -> bool:
"""Return whether this call matches the validated SM100 fast path."""
return (
_sm100_optimization_enabled()
and is_sm100(device)
and head_dim == 128
and not causal
and is_qat
and fake_quant_p
and not two_level_quant_p
and not use_global_sf_p
)
def _select_sm100_forward_config(n_ctx_q: int, n_ctx_kv: int, mode: str):
n_ctx = max(n_ctx_q, n_ctx_kv)
if mode == "reference":
return 32, 32, 4, 4 if n_ctx >= 16_384 else 5
if n_ctx <= 2_048:
return 32, 32, 4, 5
if mode == "balanced":
return 64, 32, 4, 4
return 128, 128, 8, 3
@triton.jit
def _mul_alpha(acc, alpha, BM: tl.constexpr, BN: tl.constexpr):
acc0, acc1 = acc.reshape([BM, 2, BN // 2]).permute(0, 2, 1).split()
@@ -47,7 +101,8 @@ def _attn_fwd_inner(acc, high_prec_acc, l_i, m_i, q, q_valid,
IS_QAT: tl.constexpr,
fake_quant_P: tl.constexpr = True,
two_level_quant_P: tl.constexpr = False,
use_global_sf_P: tl.constexpr = True):
use_global_sf_P: tl.constexpr = True,
JOIN_QAT_PV: tl.constexpr = False):
# range of values handled by this stage (kv blocks)
if STAGE == 1:
lo, hi = 0, start_m * BLOCK_M
@@ -113,10 +168,19 @@ def _attn_fwd_inner(acc, high_prec_acc, l_i, m_i, q, q_valid,
v = desc_v.load([offsetv_y, 0])
v = tl.where(kv_valid[:, None], v, 0.0)
p = p.to(dtype)
# note that this non transposed v for FP8 is only supported on Blackwell
acc = tl.dot(p, v.to(dtype), acc)
if IS_QAT:
high_prec_acc = tl.dot(high_prec_p, v, high_prec_acc)
# Keep the quantized and STE paths in one tensor-core operation. They
# share V, so joining along M avoids issuing two small, independent
# dot operations for every KV tile.
if IS_QAT and JOIN_QAT_PV:
joined_p = tl.join(p, high_prec_p).permute(2, 0, 1).reshape([2 * BLOCK_M, BLOCK_N])
joined_acc = tl.join(acc, high_prec_acc).permute(2, 0, 1).reshape([2 * BLOCK_M, HEAD_DIM])
joined_acc = tl.dot(joined_p, v.to(dtype), joined_acc)
acc, high_prec_acc = joined_acc.reshape([2, BLOCK_M, HEAD_DIM]).permute(1, 2, 0).split()
else:
# note that this non transposed v for FP8 is only supported on Blackwell
acc = tl.dot(p, v.to(dtype), acc)
if IS_QAT:
high_prec_acc = tl.dot(high_prec_p, v, high_prec_acc)
# update m_i and l_i
# place this at the end of the loop to reduce register pressure
l_i = l_i * alpha + l_ij
@@ -204,6 +268,7 @@ def _attn_fwd(sm_scale, M,
fake_quant_P: tl.constexpr = True,
two_level_quant_P: tl.constexpr = False,
use_global_sf_P: tl.constexpr = True,
JOIN_QAT_PV: tl.constexpr = False,
):
dtype = tl.float8e5 if FP8_OUTPUT else tl.bfloat16
tl.static_assert(BLOCK_N <= HEAD_DIM)
@@ -268,7 +333,7 @@ def _attn_fwd(sm_scale, M,
offset_y_kv, dtype, start_m, qk_scale,
BLOCK_M, HEAD_DIM, BLOCK_N,
4 - STAGE, offs_m, offs_n, N_CTX_KV,
warp_specialize, IS_HOPPER, IS_QAT, fake_quant_P, two_level_quant_P, use_global_sf_P
warp_specialize, IS_HOPPER, IS_QAT, fake_quant_P, two_level_quant_P, use_global_sf_P, JOIN_QAT_PV
)
# stage 2: on-band
if STAGE & 2:
@@ -278,7 +343,7 @@ def _attn_fwd(sm_scale, M,
offset_y_kv, dtype, start_m, qk_scale,
BLOCK_M, HEAD_DIM, BLOCK_N,
2, offs_m, offs_n, N_CTX_KV,
warp_specialize, IS_HOPPER, IS_QAT, fake_quant_P, two_level_quant_P, use_global_sf_P
warp_specialize, IS_HOPPER, IS_QAT, fake_quant_P, two_level_quant_P, use_global_sf_P, JOIN_QAT_PV
)
# epilogue
m_i += tl.math.log2(l_i)
@@ -292,6 +357,52 @@ def _attn_fwd(sm_scale, M,
desc_high_prec_o.store([off_hz, start_m * BLOCK_M, 0], high_prec_acc[None, :, :])
@triton.jit
def _attn_fwd_exact_m(
desc_q,
desc_k,
M,
sm_scale,
N_CTX_Q,
N_CTX_KV,
HEAD_DIM: tl.constexpr,
BLOCK_M: tl.constexpr,
BLOCK_N: tl.constexpr,
):
"""Reproduce the legacy 32x32 forward softmax statistic exactly."""
start_m = tl.program_id(0) * BLOCK_M
off_hz = tl.program_id(1)
offs_m = start_m + tl.arange(0, BLOCK_M)
offs_n = tl.arange(0, BLOCK_N)
q_valid = offs_m < N_CTX_Q
q_base = off_hz * N_CTX_Q
kv_base = off_hz * N_CTX_KV
q = desc_q.load([q_base + start_m, 0])
q = tl.where(q_valid[:, None], q, 0.0)
m_i = tl.full([BLOCK_M], -float("inf"), tl.float32)
l_i = tl.full([BLOCK_M], 1.0, tl.float32)
qk_scale = sm_scale * 1.44269504
for start_n in tl.range(0, N_CTX_KV, BLOCK_N):
start_n = tl.multiple_of(start_n, BLOCK_N)
kv_valid = start_n + offs_n < N_CTX_KV
k = desc_k.load([kv_base + start_n, 0])
k = tl.where(kv_valid[:, None], k, 0.0)
qk = tl.dot(q, tl.trans(k))
qk = tl.where(kv_valid[None, :], qk, -1.0e6)
m_ij = tl.maximum(m_i, tl.max(qk, axis=1) * qk_scale)
p = tl.math.exp2(qk * qk_scale - m_ij[:, None])
l_ij = tl.sum(p.to(tl.bfloat16), axis=1)
alpha = tl.math.exp2(m_i - m_ij)
l_i = l_i * alpha + l_ij
m_i = m_ij
m_i += tl.math.log2(l_i)
tl.store(M + off_hz * N_CTX_Q + offs_m, m_i, mask=q_valid)
@triton.jit
def _attn_bwd_preprocess(O, DO,
Delta,
@@ -835,10 +946,33 @@ class _attention(torch.autograd.Function):
assert HEAD_DIM_Q == HEAD_DIM_K and HEAD_DIM_K == HEAD_DIM_V
assert HEAD_DIM_K in {16, 32, 64, 128, 256}
# Triton 3.7's NVWS pass aborts for this kernel on Blackwell. Keep the
# architecture guard next to the kernel so direct callers and the
# FastVideo backend follow the same supported path on sm_100/sm_120.
consumer_blackwell = is_consumer_blackwell()
blackwell = is_blackwell() or consumer_blackwell
warp_specialize = warp_specialize and not blackwell
# Support different sequence lengths for q and k/v (needed for cross attention)
N_CTX_Q = q.shape[2] # Query sequence length
N_CTX_KV = k.shape[2] # Key/Value sequence length (may differ from query)
assert k.shape[2] == v.shape[2], "k and v must have the same sequence length"
sm100_optimized = (
q.dtype == torch.bfloat16
and k.dtype == q.dtype
and v.dtype == q.dtype
and k.device == q.device
and v.device == q.device
and _use_sm100_optimized_qat(
q.device,
HEAD_DIM_K,
causal,
IS_QAT,
fake_quant_P,
two_level_quant_P,
use_global_sf_P,
)
)
# smoothing k from SageAttn
ctx.k_mean = None
@@ -924,7 +1058,20 @@ class _attention(torch.autograd.Function):
else:
extra_kern_args["maxnreg"] = 80
BLOCK_M, BLOCK_N = 32, 32
qkv_block_m, qkv_block_n = 32, 32
fwd_block_m, fwd_block_n = 32, 32
fwd_num_warps, fwd_num_stages = 4, 2
fwd_mode = "legacy"
if sm100_optimized:
fwd_mode = os.environ.get("FASTVIDEO_ATTN_QAT_FWD_MODE", "fast").lower()
if fwd_mode not in {"fast", "balanced", "reference"}:
raise ValueError(
f"FASTVIDEO_ATTN_QAT_FWD_MODE={fwd_mode!r} "
"(want fast|balanced|reference)"
)
fwd_block_m, fwd_block_n, fwd_num_warps, fwd_num_stages = _select_sm100_forward_config(
N_CTX_Q, N_CTX_KV, fwd_mode
)
if IS_QAT:
fake_q = torch.empty_like(q)
fake_k = torch.empty_like(k)
@@ -942,8 +1089,8 @@ class _attention(torch.autograd.Function):
desc_v = fake_v
H = q.shape[1]
grid_1 = (triton.cdiv(q.shape[2], BLOCK_M), q.shape[0] * q.shape[1], 1)
grid_2 = (triton.cdiv(k.shape[2], BLOCK_N), q.shape[0] * q.shape[1], 1)
grid_1 = (triton.cdiv(q.shape[2], qkv_block_m), q.shape[0] * q.shape[1], 1)
grid_2 = (triton.cdiv(k.shape[2], qkv_block_n), q.shape[0] * q.shape[1], 1)
fake_quantize_q[grid_1](
q, fake_q,
@@ -952,7 +1099,7 @@ class _attention(torch.autograd.Function):
fake_q.stride(0), fake_q.stride(1),
fake_q.stride(2), fake_q.stride(3),
H, N_CTX_Q,
BLOCK_M=BLOCK_M, HEAD_DIM=HEAD_DIM_K,
BLOCK_M=qkv_block_m, HEAD_DIM=HEAD_DIM_K,
use_global_sf=use_global_sf_QKV,
)
fake_quantize_kv[grid_2](
@@ -962,14 +1109,14 @@ class _attention(torch.autograd.Function):
fake_k.stride(0), fake_k.stride(1),
fake_k.stride(2), fake_k.stride(3),
H, N_CTX_KV,
BLOCK_N=BLOCK_N, HEAD_DIM=HEAD_DIM_K,
BLOCK_N=qkv_block_n, HEAD_DIM=HEAD_DIM_K,
use_global_sf=use_global_sf_QKV,
)
# Apply pre-hook to set block shapes on tensor descriptors
_host_descriptor_pre_hook({
"BLOCK_M": BLOCK_M,
"BLOCK_N": BLOCK_N,
"BLOCK_M": fwd_block_m,
"BLOCK_N": fwd_block_n,
"HEAD_DIM": HEAD_DIM_K,
"desc_q": desc_q,
"desc_k": desc_k,
@@ -986,7 +1133,7 @@ class _attention(torch.autograd.Function):
N_CTX_Q=N_CTX_Q,
N_CTX_KV=N_CTX_KV,
HEAD_DIM=HEAD_DIM_K,
BLOCK_M=BLOCK_M, BLOCK_N=BLOCK_N,
BLOCK_M=fwd_block_m, BLOCK_N=fwd_block_n,
FP8_OUTPUT=q.dtype == torch.float8_e5m2,
STAGE=stage,
warp_specialize=warp_specialize,
@@ -995,10 +1142,40 @@ class _attention(torch.autograd.Function):
fake_quant_P=fake_quant_P,
two_level_quant_P=two_level_quant_P,
use_global_sf_P=use_global_sf_P,
num_warps=4,
num_stages=2,
JOIN_QAT_PV=(consumer_blackwell and _consumer_blackwell_join_qat_pv_enabled()),
num_warps=fwd_num_warps,
num_stages=fwd_num_stages,
**extra_kern_args
)
exact_m = _sm100_exact_m_enabled()
if (
sm100_optimized
and fwd_mode != "reference"
and exact_m
and (fwd_block_m != 32 or fwd_block_n != 32)
):
# The large forward tile changes a legal reduction order. Restore
# the legacy statistic so dV remains bitwise-compatible while the
# two output paths retain the faster large-tile PV computation.
assert isinstance(desc_q, TensorDescriptor)
assert isinstance(desc_k, TensorDescriptor)
desc_q.block_shape = [32, HEAD_DIM_K]
desc_k.block_shape = [32, HEAD_DIM_K]
stats_grid = (triton.cdiv(N_CTX_Q, 32), q.shape[0] * q.shape[1])
_attn_fwd_exact_m[stats_grid](
desc_q,
desc_k,
M,
sm_scale,
N_CTX_Q,
N_CTX_KV,
HEAD_DIM=HEAD_DIM_K,
BLOCK_M=32,
BLOCK_N=32,
num_warps=8,
num_stages=4,
)
o_for_bwd = high_prec_o if IS_QAT and use_high_prec_o else o
if IS_QAT:
@@ -1018,6 +1195,7 @@ class _attention(torch.autograd.Function):
ctx.smooth_q = smooth_q
ctx.use_global_sf_P = use_global_sf_P
ctx.warp_specialize = warp_specialize
ctx.sm100_optimized = sm100_optimized
return o
@staticmethod
@@ -1032,7 +1210,10 @@ class _attention(torch.autograd.Function):
N_CTX_KV = k.shape[2]
assert k.shape[2] == v.shape[2], "k and v must have the same sequence length"
PRE_BLOCK = 128
NUM_STAGES = 3
# Long video sequences are occupancy-bound on consumer Blackwell: a
# third software-pipeline stage consumes shared memory without hiding
# additional latency. Shorter sequences retain the deeper pipeline.
NUM_STAGES = 2 if is_consumer_blackwell() and max(N_CTX_Q, N_CTX_KV) >= 8192 else 3
NUM_WARPS = 4
BLOCK_M1, BLOCK_N1, BLOCK_M2, BLOCK_N2 = 32, 32, 32, 32
if not ctx.use_qat_qkv_backward:
@@ -1057,7 +1238,85 @@ class _attention(torch.autograd.Function):
# _, q_m = triton_group_mean(q)
q_m = q_m.repeat_interleave(q.shape[2] // q_m.shape[2], dim=2) # B,H,L,D
if N_CTX_Q == N_CTX_KV:
sm100_optimized_backward = (
getattr(ctx, "sm100_optimized", False)
and ctx.use_qat_qkv_backward
and not ctx.smooth_k
and not ctx.smooth_q
and N_CTX_KV % 16 == 0
)
if sm100_optimized_backward:
# Keeping dQ and dK/dV in separate programs allows 64x64 tiles
# without carrying all three fp32 accumulators at once. On SM100
# this is substantially faster than the legacy 32x32 combined
# self-attention program with the same math and BF16 parity bounds.
block_m, block_n = 64, 64
grid_dq = ((N_CTX_Q + block_m - 1) // block_m, 1, BATCH * N_HEAD)
_attn_bwd_dq_cross[grid_dq](
q,
arg_k,
v,
ctx.sm_scale,
do,
dq,
M,
delta,
q.stride(0),
k.stride(0),
q.stride(1),
k.stride(1),
q.stride(2),
k.stride(2),
q.stride(3),
k.stride(3),
N_HEAD,
N_CTX_Q,
N_CTX_KV,
ctx.k_mean,
BLOCK_M2=block_m,
BLOCK_N2=block_n,
HEAD_DIM=ctx.HEAD_DIM,
SMOOTH_K=False,
warp_specialize=False,
num_warps=8,
num_stages=2,
)
grid_dkdv = ((N_CTX_KV + block_n - 1) // block_n, 1, BATCH * N_HEAD)
_attn_bwd_dkdv_cross[grid_dkdv](
q,
arg_k,
v,
ctx.sm_scale,
do,
dk,
dv,
M,
delta,
q_m,
q.stride(0),
k.stride(0),
q.stride(1),
k.stride(1),
q.stride(2),
k.stride(2),
q.stride(3),
k.stride(3),
N_HEAD,
N_CTX_Q,
N_CTX_KV,
BLOCK_M1=block_m,
BLOCK_N1=block_n,
HEAD_DIM=ctx.HEAD_DIM,
IS_QAT=True,
two_level_quant_P=False,
fake_quant_P=True,
SMOOTH_Q=False,
use_global_sf_P=False,
warp_specialize=False,
num_warps=8,
num_stages=3,
)
elif N_CTX_Q == N_CTX_KV:
# Use existing kernel for self-attention (same sequence lengths)
grid = ((N_CTX_KV + BLOCK_N1 - 1) // BLOCK_N1, 1, BATCH * N_HEAD)
_attn_bwd[grid](
@@ -1074,10 +1333,10 @@ class _attention(torch.autograd.Function):
IS_QAT=ctx.IS_QAT,
SMOOTH_K=ctx.smooth_k,
two_level_quant_P=ctx.two_level_quant_P,
fake_quant_P=ctx.fake_quant_P,
SMOOTH_Q=ctx.smooth_q,
use_global_sf_P=ctx.use_global_sf_P,
warp_specialize=ctx.warp_specialize,
fake_quant_P=ctx.fake_quant_P,
SMOOTH_Q=ctx.smooth_q,
use_global_sf_P=ctx.use_global_sf_P,
warp_specialize=ctx.warp_specialize,
num_warps=NUM_WARPS,
num_stages=NUM_STAGES
)
@@ -0,0 +1,190 @@
# SPDX-License-Identifier: Apache-2.0
import math
import pytest
import torch
from fastvideo_kernel.triton_kernels import attn_qat_train as kernel
def _production_route_kwargs():
return {
"device": torch.device("cuda"),
"head_dim": 128,
"causal": False,
"is_qat": True,
"fake_quant_p": True,
"two_level_quant_p": False,
"use_global_sf_p": False,
}
def test_sm100_production_configuration_uses_optimized_route(monkeypatch):
monkeypatch.setattr(kernel, "is_sm100", lambda device=None: True)
monkeypatch.delenv("FASTVIDEO_ATTN_QAT_SM100_OPTIMIZED", raising=False)
assert kernel._use_sm100_optimized_qat(**_production_route_kwargs())
@pytest.mark.parametrize(
("override", "value"),
[
("head_dim", 64),
("causal", True),
("is_qat", False),
("fake_quant_p", False),
("two_level_quant_p", True),
("use_global_sf_p", True),
],
)
def test_unsupported_configuration_keeps_legacy_route(monkeypatch, override, value):
monkeypatch.setattr(kernel, "is_sm100", lambda device=None: True)
kwargs = _production_route_kwargs()
kwargs[override] = value
assert not kernel._use_sm100_optimized_qat(**kwargs)
def test_non_sm100_and_debug_switch_keep_legacy_route(monkeypatch):
kwargs = _production_route_kwargs()
monkeypatch.setattr(kernel, "is_sm100", lambda device=None: False)
assert not kernel._use_sm100_optimized_qat(**kwargs)
monkeypatch.setattr(kernel, "is_sm100", lambda device=None: True)
monkeypatch.setenv("FASTVIDEO_ATTN_QAT_SM100_OPTIMIZED", "0")
assert not kernel._use_sm100_optimized_qat(**kwargs)
def test_exact_m_is_opt_in(monkeypatch):
monkeypatch.delenv("FASTVIDEO_ATTN_QAT_FWD_EXACT_M", raising=False)
assert not kernel._sm100_exact_m_enabled()
monkeypatch.setenv("FASTVIDEO_ATTN_QAT_FWD_EXACT_M", "1")
assert kernel._sm100_exact_m_enabled()
def test_sm120_joined_pv_is_enabled_by_default_and_can_be_disabled(monkeypatch):
monkeypatch.delenv("FASTVIDEO_ATTN_QAT_SM120_JOIN_QAT_PV", raising=False)
assert kernel._consumer_blackwell_join_qat_pv_enabled()
monkeypatch.setenv("FASTVIDEO_ATTN_QAT_SM120_JOIN_QAT_PV", "0")
assert not kernel._consumer_blackwell_join_qat_pv_enabled()
@pytest.mark.parametrize(
("n_ctx", "mode", "expected"),
[
(2_048, "fast", (32, 32, 4, 5)),
(4_096, "fast", (128, 128, 8, 3)),
(4_096, "balanced", (64, 32, 4, 4)),
(4_096, "reference", (32, 32, 4, 5)),
(31_200, "reference", (32, 32, 4, 4)),
],
)
def test_sm100_forward_config_selection(n_ctx, mode, expected):
assert kernel._select_sm100_forward_config(n_ctx, n_ctx, mode) == expected
@pytest.mark.skipif(
not torch.cuda.is_available() or torch.cuda.get_device_capability() != (10, 0),
reason="SM100 parity test",
)
@pytest.mark.parametrize(("q_length", "kv_length"), [(2_112, 2_112), (2_112, 2_080)])
def test_sm100_optimized_forward_backward_matches_legacy(monkeypatch, q_length, kv_length):
torch.manual_seed(7)
q_shape = (1, 1, q_length, 128)
kv_shape = (1, 1, kv_length, 128)
inputs = [
torch.randn(q_shape, device="cuda", dtype=torch.bfloat16),
torch.randn(kv_shape, device="cuda", dtype=torch.bfloat16),
torch.randn(kv_shape, device="cuda", dtype=torch.bfloat16),
]
grad_out = torch.randn(q_shape, device="cuda", dtype=torch.bfloat16)
flags = (
True, # use_qat_qkv_backward
False, # smooth_k
True, # warp_specialize (disabled internally on Blackwell)
True, # IS_QAT
False, # two_level_quant_P
True, # fake_quant_P
True, # use_high_prec_o
False, # smooth_q
False, # use_global_sf_P
False, # use_global_sf_QKV
)
def run(optimized: bool):
monkeypatch.setenv("FASTVIDEO_ATTN_QAT_SM100_OPTIMIZED", "1" if optimized else "0")
monkeypatch.setenv("FASTVIDEO_ATTN_QAT_FWD_MODE", "fast")
monkeypatch.setenv("FASTVIDEO_ATTN_QAT_FWD_EXACT_M", "1")
q, k, v = [tensor.clone().requires_grad_(True) for tensor in inputs]
output = kernel.attention(
q,
k,
v,
False,
1.0 / math.sqrt(q_shape[-1]),
*flags,
)
output.backward(grad_out)
return output.detach(), q.grad, k.grad, v.grad
legacy = run(False)
optimized = run(True)
assert (optimized[0].float() - legacy[0].float()).abs().max().item() <= 1e-2
assert (optimized[1].float() - legacy[1].float()).abs().max().item() <= 4e-3
assert (optimized[2].float() - legacy[2].float()).abs().max().item() <= 4e-3
assert torch.equal(optimized[3], legacy[3])
@pytest.mark.skipif(
not torch.cuda.is_available() or torch.cuda.get_device_capability()[0] != 12,
reason="SM120 parity test",
)
@pytest.mark.parametrize(("q_length", "kv_length"), [(2_112, 2_112), (2_112, 2_080)])
def test_sm120_joined_pv_forward_backward_matches_split_path(monkeypatch, q_length, kv_length):
torch.manual_seed(11)
q_shape = (1, 1, q_length, 128)
kv_shape = (1, 1, kv_length, 128)
inputs = [
torch.randn(q_shape, device="cuda", dtype=torch.bfloat16),
torch.randn(kv_shape, device="cuda", dtype=torch.bfloat16),
torch.randn(kv_shape, device="cuda", dtype=torch.bfloat16),
]
grad_out = torch.randn(q_shape, device="cuda", dtype=torch.bfloat16)
flags = (
True, # use_qat_qkv_backward
False, # smooth_k
True, # warp_specialize (disabled internally on Blackwell)
True, # IS_QAT
False, # two_level_quant_P
True, # fake_quant_P
True, # use_high_prec_o
False, # smooth_q
False, # use_global_sf_P
False, # use_global_sf_QKV
)
def run(joined_pv: bool):
monkeypatch.setenv("FASTVIDEO_ATTN_QAT_SM120_JOIN_QAT_PV", "1" if joined_pv else "0")
q, k, v = [tensor.clone().requires_grad_(True) for tensor in inputs]
output = kernel.attention(
q,
k,
v,
False,
1.0 / math.sqrt(q_shape[-1]),
*flags,
)
output.backward(grad_out)
return output.detach(), q.grad, k.grad, v.grad
split = run(False)
joined = run(True)
assert torch.equal(joined[0], split[0])
assert torch.equal(joined[1], split[1])
assert torch.equal(joined[2], split[2])
assert torch.equal(joined[3], split[3])
+2
View File
@@ -304,6 +304,8 @@ def generator_config_to_fastvideo_args(config: GeneratorConfig | Mapping[str, An
for key in _LTX2_REFINE_FLAT_KEYS:
if key in refine:
kwargs[f"ltx2_refine_{key}"] = refine[key]
if "enabled" in refine:
kwargs["refine_enabled"] = refine["enabled"]
kwargs.update(preset_overrides)
kwargs.update(deepcopy(normalized.pipeline.experimental))
return FastVideoArgs.from_kwargs(**kwargs)
+26 -1
View File
@@ -51,8 +51,9 @@ class SamplingParam:
gt_latents: Any | None = None # Ground truth latents [B, 16, T, H, W]
conditioning_mask: Any | None = None # Mask [B, 1, T, H, W]
# Camera control inputs (LingBotWorld)
# Camera control inputs (LingBotWorld and LingBotWorld2)
c2ws_plucker_emb: Any | None = None # Plucker embedding: [B, C, F_lat, H_lat, W_lat]
action_path: str | None = None # Directory containing poses.npy and intrinsics.npy
# Refine inputs (LongCat 480p->720p upscaling)
# Path-based refine (load stage1 video from disk, e.g. MP4)
@@ -89,7 +90,13 @@ class SamplingParam:
num_inference_steps: int = 50
num_inference_steps_sr: int = 50
guidance_scale: float = 1.0
batch_cfg: bool = False
guidance_scale_2: float | None = None
# Z-Image CFG controls. ``cfg_normalization=True`` caps the guided
# prediction norm at the positive-prediction norm; ``cfg_truncation``
# disables CFG above the normalized-noise threshold.
cfg_normalization: bool = False
cfg_truncation: float | None = 1.0
# Embedded guidance (FLUX): do not treat ``guidance_scale > 1`` as classic CFG.
use_embedded_guidance: bool = False
# Diffusers-style true CFG for FLUX when > 1 (requires negative prompt encoding).
@@ -323,6 +330,24 @@ class SamplingParam:
default=SamplingParam.guidance_scale,
help="Classifier-free guidance scale",
)
parser.add_argument(
"--cfg-normalization",
action=StoreBoolean,
default=SamplingParam.cfg_normalization,
help="Cap Z-Image CFG prediction norm to the positive-prediction norm",
)
parser.add_argument(
"--cfg-truncation",
type=float,
default=SamplingParam.cfg_truncation,
help="Disable Z-Image CFG above this normalized-noise threshold",
)
parser.add_argument(
"--batch-cfg",
action=StoreBoolean,
default=SamplingParam.batch_cfg,
help="Evaluate conditional and unconditional CFG branches in one batch",
)
parser.add_argument(
"--guidance-rescale",
type=float,
+5
View File
@@ -130,6 +130,7 @@ class InputConfig:
keyboard_cond: Any | None = None
grid_sizes: Any | None = None
c2ws_plucker_emb: Any | None = None
action_path: str | None = None
refine_from: str | None = None
stage1_video: Any | None = None
@@ -138,6 +139,7 @@ class InputConfig:
class SamplingConfig:
num_videos_per_prompt: int = 1
seed: int = 1024
max_sequence_length: int | None = None
num_frames: int = 125
height: int = 720
width: int = 1280
@@ -147,7 +149,10 @@ class SamplingConfig:
num_inference_steps: int = 50
num_inference_steps_sr: int = 50
guidance_scale: float = 1.0
batch_cfg: bool = False
guidance_scale_2: float | None = None
cfg_normalization: bool = False
cfg_truncation: float | None = 1.0
guidance_rescale: float = 0.0
true_cfg_scale: float | None = None
use_embedded_guidance: bool | None = None
+18 -9
View File
@@ -18,14 +18,14 @@ from fastvideo.logger import init_logger
logger = init_logger(__name__)
_project_root = Path(__file__).resolve().parent.parent.parent.parent
_kernel_root = _project_root / "fastvideo-kernel"
_kernel_python_root = _kernel_root / "python"
_kernel_python_root = _project_root / "fastvideo-kernel" / "python"
_attn_qat_train_attention: Callable[..., torch.Tensor] | None = None
_attn_qat_train_import_attempted = False
_attn_qat_train_import_error: ImportError | None = None
def _ensure_kernel_paths() -> None:
for path in (_project_root, _kernel_root, _kernel_python_root):
for path in (_project_root, _kernel_python_root):
path_str = str(path)
if path_str not in sys.path:
sys.path.insert(0, path_str)
@@ -34,6 +34,7 @@ def _ensure_kernel_paths() -> None:
def _get_attn_qat_train_attention() -> Callable[..., torch.Tensor] | None:
global _attn_qat_train_attention
global _attn_qat_train_import_attempted
global _attn_qat_train_import_error
if _attn_qat_train_import_attempted:
return _attn_qat_train_attention
@@ -42,8 +43,11 @@ def _get_attn_qat_train_attention() -> Callable[..., torch.Tensor] | None:
_ensure_kernel_paths()
try:
_attn_qat_train_attention = importlib.import_module("fastvideo_kernel.triton_kernels.attn_qat_train").attention
except ImportError:
triton_qat = importlib.import_module("fastvideo_kernel.triton_kernels.attn_qat_train")
_attn_qat_train_attention = triton_qat.attention
logger.info("ATTN_QAT_TRAIN loaded FastVideo's architecture-optimized Triton kernel")
except ImportError as exc:
_attn_qat_train_import_error = exc
_attn_qat_train_attention = None
return _attn_qat_train_attention
@@ -60,8 +64,9 @@ def attn_qat_train(q_BLHD: torch.Tensor,
sm_scale: float | None = None) -> torch.Tensor:
attention = _get_attn_qat_train_attention()
if attention is None:
raise ImportError("fastvideo_kernel.triton_kernels.attn_qat_train is not available. "
"Please ensure the FastVideo kernel package is installed.")
detail = f" Original import error: {_attn_qat_train_import_error}" if _attn_qat_train_import_error else ""
raise ImportError("ATTN_QAT_TRAIN requires FastVideo's fastvideo-kernel package. Install it or make "
f"fastvideo-kernel/python importable.{detail}")
q_BHLD = q_BLHD.permute(0, 2, 1, 3).contiguous()
k_BHLD = k_BLHD.permute(0, 2, 1, 3).contiguous()
@@ -69,7 +74,11 @@ def attn_qat_train(q_BLHD: torch.Tensor,
use_qat_qkv_backward = True
smooth_k = False
warp_specialize = True
# Triton 3.7's NVWS pass aborts while compiling this kernel on Blackwell.
# The kernel has a supported non-warp-specialized path, so use it on both
# datacenter (sm_100) and consumer (sm_120) Blackwell GPUs.
capability_major = torch.cuda.get_device_capability()[0]
warp_specialize = capability_major not in (10, 12)
is_qat = True
two_level_quant_p_sage3 = False
fake_quant_p_bwd = True
@@ -106,7 +115,7 @@ class AttnQatTrainBackend(AttentionBackend):
@staticmethod
def get_supported_head_sizes() -> list[int]:
return [64, 96, 128, 160, 192, 224, 256]
return [128]
@staticmethod
def get_name() -> str:
+5 -2
View File
@@ -180,8 +180,11 @@ class FlashAttentionImpl(AttentionImpl):
# SP). Cast through bf16 and restore, matching TORCH_SDPA's tolerance.
orig_dtype = query.dtype
if orig_dtype not in (torch.float16, torch.bfloat16):
logger.warning_once(f"FLASH_ATTN received {orig_dtype} inputs; casting to "
f"bfloat16 for the kernel and restoring on output.")
# Keep logging (and its Python lru_cache) out of compiled traces
# so that the AOTAutograd cache can serialize.
if not torch.compiler.is_compiling():
logger.warning_once(f"FLASH_ATTN received {orig_dtype} inputs; casting to "
f"bfloat16 for the kernel and restoring on output.")
query = query.to(torch.bfloat16)
key = key.to(torch.bfloat16)
value = value.to(torch.bfloat16)
+26
View File
@@ -32,6 +32,27 @@ def backend_name_to_enum(backend_name: str) -> AttentionBackendEnum | None:
None
def coerce_attn_backend(attn_backend: AttentionBackendEnum | str | None, ) -> AttentionBackendEnum | None:
"""Normalize an explicit backend selection.
Environment-variable parsing remains permissive via
:func:`backend_name_to_enum`, but typed/config-driven call sites should
fail fast on typos instead of silently falling back to another backend.
"""
if attn_backend is None or isinstance(attn_backend, AttentionBackendEnum):
return attn_backend
if not isinstance(attn_backend, str) or not attn_backend.strip():
raise ValueError("attention backend must be a non-empty string, "
f"an AttentionBackendEnum, or None; got {attn_backend!r}")
backend_name = attn_backend.strip().upper()
backend = backend_name_to_enum(backend_name)
if backend is None:
raise ValueError(f"Unknown attention backend {attn_backend!r}. "
f"Expected one of {sorted(AttentionBackendEnum.__members__)}")
return backend
def get_env_variable_attn_backend() -> AttentionBackendEnum | None:
'''
Get the backend override specified by the FastVideo attention
@@ -69,6 +90,11 @@ def global_force_attn_backend(attn_backend: AttentionBackendEnum | None) -> None
'''
global forced_attn_backend
forced_attn_backend = attn_backend
# Backend selection is cached by tensor shape/dtype, while the global
# override is intentionally not part of that cache key. Invalidate cached
# resolutions whenever the override changes so independently constructed
# role models can bind different attention implementations.
_cached_get_attn_backend.cache_clear()
def get_global_forced_attn_backend() -> AttentionBackendEnum | None:
+4 -3
View File
@@ -1,6 +1,5 @@
from __future__ import annotations
import functools
from collections.abc import Callable
import torch
@@ -43,15 +42,17 @@ except ImportError:
_flash_attn_2_func = None
_flash_attn_2_varlen_func = None
_IS_SM90_OR_NEWER = current_platform.has_device_capability(90)
def _check_dropout(dropout_p: float) -> None:
if dropout_p != 0.0:
raise NotImplementedError(f"flash_attn.cute does not support dropout (got dropout_p={dropout_p})")
@functools.cache
def _sm90_or_newer() -> bool:
return current_platform.has_device_capability(90)
# Compiled callers only read a boolean instead of tracing through a memoized platform query.
return _IS_SM90_OR_NEWER
def _use_fa2(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor) -> bool:
+5 -1
View File
@@ -12,12 +12,16 @@ from fastvideo.configs.models.dits.ltx2 import LTX2VideoConfig
from fastvideo.configs.models.dits.magi_human import MagiHumanVideoConfig
from fastvideo.configs.models.dits.stable_audio import StableAudioConfig
from fastvideo.configs.models.dits.wanvideo import WanVideoConfig
from fastvideo.configs.models.dits.zimage import ZImageDiTConfig
from fastvideo.configs.models.dits.hyworld import HYWorldConfig
from fastvideo.configs.models.dits.kandinsky5 import Kandinsky5VideoConfig
from fastvideo.configs.models.dits.lingbotworld2 import LingBotWorld2CausalFastVideoConfig
from fastvideo.configs.models.dits.lingbot_video import LingBotVideoConfig
__all__ = [
"HunyuanVideoConfig", "HunyuanVideo15Config", "HunyuanGameCraftConfig", "WanVideoConfig", "DreamXWorldConfig",
"DreamXWorldARConfig", "CosmosVideoConfig", "Cosmos25VideoConfig", "FluxDiTConfig", "Flux2Config",
"LongCatVideoConfig", "LTX2VideoConfig", "HYWorldConfig", "Kandinsky5VideoConfig", "MagiHumanVideoConfig",
"StableAudioConfig", "GlmImageDiTConfig"
"StableAudioConfig", "GlmImageDiTConfig", "LingBotWorld2CausalFastVideoConfig", "LingBotVideoConfig",
"ZImageDiTConfig"
]
@@ -0,0 +1,70 @@
# SPDX-License-Identifier: Apache-2.0
"""Architecture configuration for LingBot-Video Dense and MoE DiTs."""
from __future__ import annotations
from dataclasses import dataclass, field
from fastvideo.configs.models.dits.base import DiTArchConfig, DiTConfig
from fastvideo.platforms import AttentionBackendEnum
def _is_lingbot_video_block(name: str, module: object) -> bool:
"""Select top-level transformer blocks for FSDP and compilation."""
del module
parts = name.split(".")
return len(parts) == 2 and parts[0] == "blocks" and parts[1].isdigit()
@dataclass
class LingBotVideoArchConfig(DiTArchConfig):
"""One-to-one representation of the released transformer config JSON."""
_fsdp_shard_conditions: list = field(default_factory=lambda: [_is_lingbot_video_block])
_supported_attention_backends: tuple[AttentionBackendEnum, ...] = (
AttentionBackendEnum.TORCH_SDPA,
AttentionBackendEnum.FLASH_ATTN,
)
param_names_mapping: dict = field(default_factory=lambda: {r"^(.*)$": r"\1"})
patch_size: tuple[int, int, int] = (1, 2, 2)
in_channels: int = 16
out_channels: int = 16
hidden_size: int = 2048
num_attention_heads: int = 16
depth: int = 24
intermediate_size: int = 6144
text_dim: int = 2560
freq_dim: int = 256
norm_eps: float = 1e-6
rope_theta: float = 256.0
axes_dims: tuple[int, int, int] = (32, 48, 48)
axes_lens: tuple[int, int, int] = (8192, 1024, 1024)
qkv_bias: bool = False
out_bias: bool = True
patch_embed_bias: bool = True
timestep_mlp_bias: bool = True
num_experts: int = 0
num_experts_per_tok: int = 8
moe_intermediate_size: int = 512
decoder_sparse_step: int = 1
mlp_only_layers: tuple[int, ...] = ()
n_shared_experts: int | None = None
score_func: str = "sigmoid"
norm_topk_prob: bool = True
n_group: int | None = None
topk_group: int | None = None
routed_scaling_factor: float = 1.0
def __post_init__(self) -> None:
"""Populate FastVideo loader fields from the released architecture."""
super().__post_init__()
self.num_channels_latents = self.in_channels
self.attention_head_dim = self.hidden_size // self.num_attention_heads
@dataclass
class LingBotVideoConfig(DiTConfig):
"""FastVideo component configuration for LingBot-Video transformers."""
arch_config: DiTArchConfig = field(default_factory=LingBotVideoArchConfig)
@@ -0,0 +1,55 @@
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass, field
from fastvideo.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
class LingBotWorld2CausalFastArchConfig(DiTArchConfig):
_fsdp_shard_conditions: list = field(default_factory=lambda: [is_blocks])
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)
model_type: str = "i2v"
patch_size: tuple[int, int, int] = (1, 2, 2)
text_len: int = 512
in_dim: int = 36
dim: int = 5120
ffn_dim: int = 13824
freq_dim: int = 256
text_dim: int = 4096
out_dim: int = 16
num_heads: int = 40
num_layers: int = 40
qk_norm: bool = True
cross_attn_norm: bool = True
eps: float = 1e-6
local_attn_size: int = 18
sink_size: int = 6
chunk_size: int = 4
sample_shift: float = 10.0
num_train_timesteps: int = 1000
timesteps_index: tuple[int, int, int, int] = (0, 250, 500, 750)
max_area: int = 480 * 832
def __post_init__(self):
super().__post_init__()
self.hidden_size = self.dim
self.num_attention_heads = self.num_heads
self.attention_head_dim = self.dim // self.num_heads
self.in_channels = self.in_dim
self.out_channels = self.out_dim
self.num_channels_latents = self.out_dim
@dataclass
class LingBotWorld2CausalFastVideoConfig(DiTConfig):
arch_config: DiTArchConfig = field(default_factory=LingBotWorld2CausalFastArchConfig)
prefix: str = "Wan"
+60
View File
@@ -0,0 +1,60 @@
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass, field
from fastvideo.configs.models.dits.base import DiTArchConfig, DiTConfig
from fastvideo.platforms import AttentionBackendEnum
def is_zimage_block(name: str, module) -> bool:
parts = name.split(".")
return len(parts) >= 2 and parts[-2] in {"noise_refiner", "context_refiner", "layers"} and parts[-1].isdigit()
@dataclass
class ZImageDiTArchConfig(DiTArchConfig):
_fsdp_shard_conditions: list = field(default_factory=lambda: [is_zimage_block])
_supported_attention_backends: tuple[AttentionBackendEnum, ...] = (AttentionBackendEnum.TORCH_SDPA, )
all_patch_size: tuple[int, ...] = (2, )
all_f_patch_size: tuple[int, ...] = (1, )
in_channels: int = 16
dim: int = 3840
n_layers: int = 30
n_refiner_layers: int = 2
n_heads: int = 30
n_kv_heads: int = 30
norm_eps: float = 1e-5
qk_norm: bool = True
cap_feat_dim: int = 2560
rope_theta: float = 256.0
t_scale: float = 1000.0
axes_dims: tuple[int, ...] = (32, 48, 48)
axes_lens: tuple[int, ...] = (1536, 512, 512)
adaln_embed_dim: int = 256
frequency_embedding_size: int = 256
timestep_mid_size: int = 1024
max_period: int = 10000
seq_multi_of: int = 32
def __post_init__(self) -> None:
super().__post_init__()
if len(self.all_patch_size) != len(self.all_f_patch_size):
raise ValueError("all_patch_size and all_f_patch_size must have equal length")
if self.dim % self.n_heads:
raise ValueError("dim must be divisible by n_heads")
if self.dim // self.n_heads != sum(self.axes_dims):
raise ValueError("attention head dimension must equal sum(axes_dims)")
if len(self.axes_dims) != len(self.axes_lens) or any(dim % 2 for dim in self.axes_dims):
raise ValueError("RoPE axes require matching lengths and even dimensions")
self.hidden_size = self.dim
self.num_attention_heads = self.n_heads
self.num_channels_latents = self.in_channels
self.out_channels = self.in_channels
@dataclass
class ZImageDiTConfig(DiTConfig):
arch_config: DiTArchConfig = field(default_factory=ZImageDiTArchConfig)
prefix: str = "ZImage"
@@ -2,6 +2,7 @@ from fastvideo.configs.models.encoders.base import (BaseEncoderOutput, EncoderCo
TextEncoderConfig)
from fastvideo.configs.models.encoders.clip import (CLIPTextConfig, CLIPVisionConfig, WAN2_1ControlCLIPVisionConfig)
from fastvideo.configs.models.encoders.llama import LlamaConfig
from fastvideo.configs.models.encoders.lingbotworld2_t5 import LingBotWorld2UMT5ArchConfig, LingBotWorld2UMT5Config
from fastvideo.configs.models.encoders.t5 import T5Config, T5LargeConfig
from fastvideo.configs.models.encoders.qwen2_5 import Qwen2_5_VLConfig
from fastvideo.configs.models.encoders.siglip import SiglipVisionConfig
@@ -9,6 +10,7 @@ from fastvideo.configs.models.encoders.reason1 import Reason1ArchConfig, Reason1
from fastvideo.configs.models.encoders.gemma import LTX2GemmaConfig
from fastvideo.configs.models.encoders.mistral3 import Mistral3TextConfig
from fastvideo.configs.models.encoders.qwen3 import Qwen3TextConfig
from fastvideo.configs.models.encoders.lingbot_video import LingBotVideoQwen3VLTextConfig
from fastvideo.configs.models.encoders.stable_audio_conditioner import (StableAudioConditionerArchConfig,
StableAudioConditionerConfig)
from fastvideo.configs.models.encoders.t5gemma import T5GemmaEncoderConfig
@@ -17,5 +19,6 @@ __all__ = [
"EncoderConfig", "TextEncoderConfig", "ImageEncoderConfig", "BaseEncoderOutput", "CLIPTextConfig",
"CLIPVisionConfig", "WAN2_1ControlCLIPVisionConfig", "LlamaConfig", "T5Config", "T5LargeConfig", "Qwen2_5_VLConfig",
"Reason1ArchConfig", "Reason1Config", "LTX2GemmaConfig", "SiglipVisionConfig", "StableAudioConditionerArchConfig",
"StableAudioConditionerConfig", "T5GemmaEncoderConfig", "Qwen3TextConfig", "Mistral3TextConfig"
"StableAudioConditionerConfig", "T5GemmaEncoderConfig", "Qwen3TextConfig", "Mistral3TextConfig",
"LingBotWorld2UMT5ArchConfig", "LingBotWorld2UMT5Config", "LingBotVideoQwen3VLTextConfig"
]
@@ -83,6 +83,7 @@ class TextEncoderConfig(EncoderConfig):
arch_config: ArchConfig = field(default_factory=TextEncoderArchConfig)
is_chat_model: bool = False
treat_empty_as_dot: bool = False
chat_template_enable_thinking: bool = field(default=False, kw_only=True)
@dataclass
@@ -0,0 +1,50 @@
# SPDX-License-Identifier: Apache-2.0
"""Qwen3-VL text-only encoder configuration used by LingBot-Video."""
from dataclasses import dataclass, field
from fastvideo.configs.models.encoders.base import TextEncoderArchConfig
from fastvideo.configs.models.encoders.qwen3 import Qwen3TextArchConfig, Qwen3TextConfig
@dataclass
class LingBotVideoQwen3VLTextArchConfig(Qwen3TextArchConfig):
"""Exact Qwen3-VL language-model architecture released with LingBot-Video."""
architectures: list[str] = field(default_factory=lambda: ["LingBotVideoQwen3VLTextModel"])
vocab_size: int = 151936
hidden_size: int = 2560
intermediate_size: int = 9728
num_hidden_layers: int = 36
num_attention_heads: int = 32
num_key_value_heads: int = 8
max_position_embeddings: int = 262144
rms_norm_eps: float = 1e-6
rope_theta: float = 5000000.0
rope_scaling: dict | None = None
mrope_interleaved: bool = True
mrope_section: tuple[int, int, int] = (24, 20, 20)
bos_token_id: int = 151643
eos_token_id: int = 151645
pad_token_id: int = 151643
text_len: int = 37698
output_hidden_states: bool = True
require_processor: bool = True
def __post_init__(self) -> None:
"""Match the official processor call used by LingBotVideoPipeline."""
self.tokenizer_kwargs = {
"truncation": True,
"max_length": self.text_len,
"padding": "longest",
"return_tensors": "pt",
}
@dataclass
class LingBotVideoQwen3VLTextConfig(Qwen3TextConfig):
"""FastVideo loader config for the LingBot-Video text-only Qwen3-VL path."""
arch_config: TextEncoderArchConfig = field(default_factory=LingBotVideoQwen3VLTextArchConfig)
prefix: str = "language_model"
is_chat_model: bool = False
@@ -0,0 +1,40 @@
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass, field
from fastvideo.configs.models.encoders.base import (
TextEncoderArchConfig,
TextEncoderConfig,
)
@dataclass
class LingBotWorld2UMT5ArchConfig(TextEncoderArchConfig):
architectures: list[str] = field(default_factory=lambda: ["LingBotWorld2T5EncoderModel"])
vocab_size: int = 256384
dim: int = 4096
dim_attn: int = 4096
dim_ffn: int = 10240
num_heads: int = 64
num_layers: int = 24
num_buckets: int = 32
text_len: int = 512
hidden_size: int = 4096
dropout: float = 0.1
def __post_init__(self) -> None:
super().__post_init__()
self.tokenizer_kwargs = {
"padding": "max_length",
"truncation": True,
"max_length": self.text_len,
"add_special_tokens": True,
"return_attention_mask": True,
"return_tensors": "pt",
}
@dataclass
class LingBotWorld2UMT5Config(TextEncoderConfig):
arch_config: TextEncoderArchConfig = field(default_factory=LingBotWorld2UMT5ArchConfig)
prefix: str = "text_encoder"
+4 -1
View File
@@ -7,6 +7,8 @@ from fastvideo.configs.pipelines.hunyuan15 import Hunyuan15T2V480PConfig, Hunyua
from fastvideo.configs.pipelines.hunyuangamecraft import HunyuanGameCraftPipelineConfig
from fastvideo.configs.pipelines.hyworld import HYWorldConfig
from fastvideo.configs.pipelines.kandinsky5 import Kandinsky5I2VConfig, Kandinsky5T2VConfig
from fastvideo.configs.pipelines.lingbotworld2 import LingBotWorld2CausalFastI2V480PConfig
from fastvideo.configs.pipelines.lingbot_video import LingBotVideoT2VConfig
from fastvideo.configs.pipelines.matrixgame2 import MatrixGame2I2V480PConfig
from fastvideo.configs.pipelines.matrixgame3 import MatrixGame3I2V720PConfig
from fastvideo.pipelines.basic.ltx2.pipeline_configs import LTX2T2VConfig
@@ -19,5 +21,6 @@ __all__ = [
"Hunyuan15T2V720PConfig", "WanT2V480PConfig", "WanI2V480PConfig", "WanT2V720PConfig", "WanI2V720PConfig",
"SelfForcingWanT2V480PConfig", "LucyEditDevConfig", "CosmosConfig", "Cosmos25Config", "LTX2T2VConfig",
"DreamXWorld5BCamPipelineConfig", "DreamXWorld5BARPipelineConfig", "HYWorldConfig", "Kandinsky5T2VConfig",
"Kandinsky5I2VConfig", "MatrixGame2I2V480PConfig", "MatrixGame3I2V720PConfig", "get_pipeline_config_cls_from_name"
"Kandinsky5I2VConfig", "LingBotWorld2CausalFastI2V480PConfig", "LingBotVideoT2VConfig", "MatrixGame2I2V480PConfig",
"MatrixGame3I2V720PConfig", "get_pipeline_config_cls_from_name"
]
@@ -0,0 +1,73 @@
# SPDX-License-Identifier: Apache-2.0
"""Dense LingBot-Video T2V pipeline configuration."""
from collections.abc import Callable
from dataclasses import dataclass, field
import torch
from fastvideo.configs.models import DiTConfig, EncoderConfig, VAEConfig
from fastvideo.configs.models.dits.lingbot_video import LingBotVideoConfig
from fastvideo.configs.models.encoders import BaseEncoderOutput
from fastvideo.configs.models.encoders.lingbot_video import LingBotVideoQwen3VLTextConfig
from fastvideo.configs.models.vaes import WanVAEConfig
from fastvideo.configs.pipelines.base import PipelineConfig
PROMPT_CROP_START = 140
PROMPT_TEMPLATE = ("<|im_start|>system\nGiven a user input that may include a text prompt alone, "
"a text prompt with an image reference, or a text prompt with a video reference "
'or a video reference alone, generate an "Enhanced prompt" that provides detailed '
"visual descriptions suitable for video generation. Evaluate the level of detail "
"in the user's input: if it is simple, enrich it by adding specifics about colors, "
"shapes, sizes, textures, lighting, motion dynamics, camera movement, temporal "
"progression, and spatial relationships to create vivid, concrete, and temporally "
"coherent scenes to create vivid and concrete scenes. Please generate only the "
"enhanced description for the prompt below and avoid including any additional "
"commentary or evaluations:<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n"
"<|im_start|>assistant\n")
def preprocess_lingbot_video_prompt(prompt: str) -> str:
"""Apply the released T2V system/user/assistant prompt template."""
return PROMPT_TEMPLATE.format(prompt)
def postprocess_lingbot_video_text(
outputs: BaseEncoderOutput,
attention_mask: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Select the final hidden state, crop the template, and trim batch-one padding."""
if outputs.hidden_states is None:
raise ValueError("LingBot-Video requires text-encoder hidden states")
prompt_embeds = outputs.hidden_states[-1][:, PROMPT_CROP_START:]
prompt_mask = attention_mask[:, PROMPT_CROP_START:]
if prompt_embeds.shape[0] == 1:
true_length = int(prompt_mask[0].sum().item())
prompt_embeds = prompt_embeds[:, :true_length]
prompt_mask = prompt_mask[:, :true_length]
return prompt_embeds, prompt_mask
@dataclass
class LingBotVideoT2VConfig(PipelineConfig):
"""Released Dense T2V component wiring and numerical precision policy."""
dit_config: DiTConfig = field(default_factory=LingBotVideoConfig)
vae_config: VAEConfig = field(default_factory=WanVAEConfig)
text_encoder_configs: tuple[EncoderConfig, ...] = field(default_factory=lambda: (LingBotVideoQwen3VLTextConfig(), ))
preprocess_text_funcs: tuple[Callable, ...] = field(default_factory=lambda: (preprocess_lingbot_video_prompt, ))
postprocess_text_funcs: tuple[Callable, ...] = field(default_factory=lambda: (postprocess_lingbot_video_text, ))
text_encoder_precisions: tuple[str, ...] = field(default_factory=lambda: ("bf16", ))
dit_precision: str = "bf16"
vae_precision: str = "fp32"
vae_decode_precision: str | None = "fp32"
vae_tiling: bool = False
vae_sp: bool = False
flow_shift: float | None = 3.0
embedded_cfg_scale: float | None = None
scheduler_step_in_fp32: bool = True
def __post_init__(self) -> None:
"""Load only the VAE decoder for the T2V workload."""
self.vae_config.load_encoder = False
self.vae_config.load_decoder = True
@@ -0,0 +1,45 @@
# SPDX-License-Identifier: Apache-2.0
import html
from dataclasses import dataclass, field
import ftfy
import torch
from fastvideo.configs.models import DiTConfig
from fastvideo.configs.models.dits.lingbotworld2 import LingBotWorld2CausalFastVideoConfig
from fastvideo.configs.models.encoders import BaseEncoderOutput, LingBotWorld2UMT5Config
from fastvideo.configs.models.vaes import WanVAEConfig
from fastvideo.configs.pipelines.wan import Wan2_2_I2V_A14B_Config
def lingbotworld2_whitespace_preprocess(prompt: str) -> str:
text = ftfy.fix_text(prompt)
text = html.unescape(html.unescape(text))
return " ".join(text.strip().split())
def lingbotworld2_t5_postprocess_text(outputs: BaseEncoderOutput) -> torch.Tensor:
assert outputs.last_hidden_state is not None
return outputs.last_hidden_state
@dataclass
class LingBotWorld2CausalFastI2V480PConfig(Wan2_2_I2V_A14B_Config):
dit_config: DiTConfig = field(default_factory=LingBotWorld2CausalFastVideoConfig)
vae_config: WanVAEConfig = field(default_factory=WanVAEConfig)
text_encoder_configs: tuple = field(default_factory=lambda: (LingBotWorld2UMT5Config(), ))
preprocess_text_funcs: tuple = field(default_factory=lambda: (lingbotworld2_whitespace_preprocess, ))
postprocess_text_funcs: tuple = field(default_factory=lambda: (lingbotworld2_t5_postprocess_text, ))
text_encoder_precisions: tuple[str, ...] = field(default_factory=lambda: ("bf16", ))
flow_shift: float | None = 10.0
boundary_ratio: float | None = 0.947
vae_precision: str = "fp32"
vae_decode_precision: str | None = "fp32"
vae_tiling: bool = False
vae_sp: bool = False
dit_precision: str = "bf16"
is_causal: bool = False
def __post_init__(self) -> None:
self.vae_config.load_encoder = True
self.vae_config.load_decoder = True
+52
View File
@@ -0,0 +1,52 @@
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
import torch
from fastvideo.configs.models import EncoderConfig
from fastvideo.configs.models.dits.zimage import ZImageDiTConfig
from fastvideo.configs.models.encoders import BaseEncoderOutput
from fastvideo.configs.models.encoders.qwen3 import Qwen3TextConfig
from fastvideo.configs.models.vaes.autoencoder_kl import AutoencoderKLVAEConfig
from fastvideo.configs.pipelines.base import PipelineConfig, preprocess_text
def _zimage_text_postprocess(outputs: BaseEncoderOutput) -> torch.Tensor:
if outputs.hidden_states is None:
raise RuntimeError("Z-Image requires Qwen3 hidden states")
return outputs.hidden_states[-2]
@dataclass
class ZImagePipelineConfig(PipelineConfig):
"""Configuration for the native Z-Image text-to-image pipeline."""
scheduler_arch: str = "FlowMatchEulerDiscreteScheduler"
transformer_arch: str = "ZImageTransformer2DModel"
vae_arch: str = "AutoencoderKL"
text_encoder_archs: tuple[str, ...] = ("Qwen3Model", )
tokenizer_archs: tuple[str, ...] = ("Qwen2Tokenizer", )
dit_config: ZImageDiTConfig = field(default_factory=ZImageDiTConfig)
vae_config: AutoencoderKLVAEConfig = field(default_factory=AutoencoderKLVAEConfig)
text_encoder_configs: tuple[EncoderConfig, ...] = field(
default_factory=lambda: (Qwen3TextConfig(chat_template_enable_thinking=True), ))
preprocess_text_funcs: tuple[Callable[[str], str], ...] = field(default_factory=lambda: (preprocess_text, ))
postprocess_text_funcs: tuple[Callable[[BaseEncoderOutput], torch.Tensor],
...] = field(default_factory=lambda: (_zimage_text_postprocess, ))
dit_precision: str = "bf16"
vae_precision: str = "fp32"
text_encoder_precisions: tuple[str, ...] = field(default_factory=lambda: ("bf16", ))
vae_tiling: bool = False
vae_sp: bool = False
embedded_cfg_scale: float = 0.0
flow_shift: float | None = 3.0
scheduler_step_in_fp32: bool = True
scheduler_sigma_min: float = 0.0
scheduler_use_reference_discrete_timesteps: bool = True
+45 -1
View File
@@ -112,6 +112,40 @@ def _infer_latent_batch_size(batch: ForwardBatch) -> int:
return latent_batch_size
def _resolve_output_size(
samples: torch.Tensor,
fallback: tuple[int, int, int],
*,
pixel_output: bool,
) -> tuple[int, int, int]:
"""Report the final decoded video's `(height, width, frames)`.
Refiner stages can produce a different resolution from the base request, so
pixel outputs use the final `[batch, channels, frames, height, width]` tensor.
Latent and audio outputs keep the requested fallback because their tensor
dimensions do not describe decoded pixels.
"""
if pixel_output and samples.ndim == 5:
return (int(samples.shape[-2]), int(samples.shape[-1]), int(samples.shape[-3]))
return fallback
def _validate_request_stage_overrides(model_path: str, request: GenerationRequest) -> None:
"""Validate typed stage overrides against the model's registered preset."""
if not request.stage_overrides:
return
from fastvideo.api.presets import validate_preset_selection
from fastvideo.registry import get_preset_selection
preset_name, model_family = get_preset_selection(model_path)
if preset_name is None or model_family is None:
raise ValueError(f"Model {model_path!r} has no preset for stage override validation")
validate_preset_selection(
preset_name,
model_family,
stage_overrides=request.stage_overrides,
)
class VideoGenerator:
"""
A unified class for generating videos using diffusion models.
@@ -443,6 +477,7 @@ class VideoGenerator:
self,
request: GenerationRequest,
) -> GenerationResult | list[GenerationResult]:
_validate_request_stage_overrides(self.fastvideo_args.model_path, request)
if isinstance(request.prompt, list):
if request.inputs.prompt_path is not None:
raise ValueError("request.prompt list cannot be combined with request.inputs.prompt_path")
@@ -808,6 +843,15 @@ class VideoGenerator:
# 2. Audio-only workload — `samples` is a 1×3×1×8×8 placeholder
# no caller will use; skip the grid loop and save a `.wav`.
# 3. Pixel video / image — the historical happy path.
# `GenerationResult.size` describes the produced media, not only the
# base-stage request. Refiner pipelines can change the final pixel
# dimensions, so derive this result metadata from the decoded output.
output_size = _resolve_output_size(
samples,
(target_height, target_width, batch.num_frames),
pixel_output=not is_latent_output and not audio_only,
)
postprocess_start = time.perf_counter()
frames: list[np.ndarray] | None
if is_latent_output or audio_only:
@@ -916,7 +960,7 @@ class VideoGenerator:
"audio": output_batch.extra.get("audio"),
"audio_sample_rate": output_batch.extra.get("audio_sample_rate"),
"ltx2_audio_latents": output_batch.extra.get("ltx2_audio_latents"),
"size": (target_height, target_width, batch.num_frames),
"size": output_size,
"generation_time": gen_time,
"e2e_latency": e2e_time,
"logging_info": logging_info,
+811
View File
@@ -0,0 +1,811 @@
# SPDX-License-Identifier: Apache-2.0
"""FastVideo-native LingBot-Video Dense and MoE diffusion transformers."""
from __future__ import annotations
import math
from dataclasses import dataclass
from typing import Any
import torch
import torch.nn as nn
import torch.nn.functional as F
from fastvideo.configs.models.dits.lingbot_video import LingBotVideoConfig, _is_lingbot_video_block
from fastvideo.distributed.communication_op import (
sequence_model_parallel_all_gather_with_unpad,
sequence_model_parallel_all_to_all_4D,
sequence_model_parallel_shard,
)
from fastvideo.distributed.parallel_state import get_sp_world_size, model_parallel_is_initialized
from fastvideo.layers.linear import ReplicatedLinear
from fastvideo.layers.visual_embedding import Timesteps
from fastvideo.models.dits.base import BaseDiT
from fastvideo.platforms import AttentionBackendEnum
@dataclass
class LingBotVideoTransformerOutput:
"""Output container matching the released transformer contract."""
sample: torch.Tensor
_FP32_MODULE_NAMES = (
"time_embedder",
"time_modulation",
"scale_shift_table",
"norm",
"norm1",
"norm2",
"norm_q",
"norm_k",
"norm_post_attn",
"norm_post_ffn",
"norm_out",
"norm_out_modulation",
"router",
)
def _keep_in_fp32(name: str) -> bool:
"""Return whether a released checkpoint module keeps fp32 parameters."""
return any(module_name in name.split(".") for module_name in _FP32_MODULE_NAMES)
def _sequence_parallel_world_size() -> int:
"""Use standalone single-rank behavior before distributed initialization."""
return get_sp_world_size() if model_parallel_is_initialized() else 1
class LingBotVideoLinear(ReplicatedLinear):
"""Replicated FastVideo linear with the tensor-only official call contract."""
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
"""Return the projected tensor while preserving the normal weight surface."""
output, _ = super().forward(hidden_states)
return output
class LingBotVideoRMSNorm(nn.Module):
"""RMSNorm with fp32 accumulation and input-dtype output."""
def __init__(self, dim: int, eps: float = 1e-6) -> None:
super().__init__()
self.weight = nn.Parameter(torch.ones(dim))
self.variance_epsilon = eps
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
"""Normalize the last dimension using the official accumulation order."""
input_dtype = hidden_states.dtype
hidden_states = hidden_states.to(torch.float32)
variance = hidden_states.pow(2).mean(-1, keepdim=True)
hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
return (self.weight * hidden_states).to(input_dtype)
def _apply_rotary_emb(x: torch.Tensor, freqs_cis: torch.Tensor) -> torch.Tensor:
"""Apply complex 3D rotary embeddings to `(B, S, H, D)` tensors."""
with torch.amp.autocast("cuda", enabled=False):
x_complex = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2))
output = torch.view_as_real(x_complex * freqs_cis.unsqueeze(2)).flatten(3)
return output.type_as(x)
class LingBotVideoRotaryEmbedding(nn.Module):
"""Complex64 rotary table indexed by temporal and spatial positions."""
def __init__(self, axes_dims: tuple[int, ...], axes_lens: tuple[int, ...], theta: float) -> None:
super().__init__()
self.axes_dims = tuple(axes_dims)
self.axes_lens = list(axes_lens)
self.theta = theta
self.freqs_cis: list[torch.Tensor] | None = None
@staticmethod
def _precompute(dims: tuple[int, ...], lengths: tuple[int, ...], theta: float) -> list[torch.Tensor]:
"""Build the per-axis complex frequency tables on CPU."""
tables: list[torch.Tensor] = []
for dim, length in zip(dims, lengths, strict=True):
frequencies = 1.0 / (theta**(torch.arange(0, dim, 2, dtype=torch.float64, device="cpu") / dim))
positions = torch.arange(length, device=frequencies.device, dtype=torch.float64)
phases = torch.outer(positions, frequencies).float()
tables.append(torch.polar(torch.ones_like(phases), phases).to(torch.complex64))
return tables
def forward(
self,
position_ids: torch.Tensor,
maxima: tuple[int, ...] | None = None,
) -> torch.Tensor:
"""Gather and concatenate rotary frequencies for `(S, 3)` positions."""
device = position_ids.device
if maxima is None:
maxima = tuple(int(value) for value in position_ids.max(dim=0).values.tolist())
rebuild = self.freqs_cis is None or any(maximum >= length
for maximum, length in zip(maxima, self.axes_lens, strict=True))
if rebuild:
for index, maximum in enumerate(maxima):
if maximum >= self.axes_lens[index]:
self.axes_lens[index] = int(maximum * 1.5) + 1
self.freqs_cis = self._precompute(self.axes_dims, tuple(self.axes_lens), self.theta)
self.freqs_cis = [table.to(device) for table in self.freqs_cis]
elif self.freqs_cis[0].device != device:
self.freqs_cis = [table.to(device) for table in self.freqs_cis]
return torch.cat(
[self.freqs_cis[index][position_ids[:, index]] for index in range(len(self.axes_dims))],
dim=-1,
)
def _make_joint_position_ids(
text_len: int,
grid_t: int,
grid_h: int,
grid_w: int,
device: torch.device,
) -> torch.Tensor:
"""Create official `[video; text]` 3D positions for one sample."""
temporal = torch.arange(grid_t, device=device, dtype=torch.int32) + text_len + 1
height = torch.arange(grid_h, device=device, dtype=torch.int32)
width = torch.arange(grid_w, device=device, dtype=torch.int32)
video_positions = torch.stack(torch.meshgrid(temporal, height, width, indexing="ij"), dim=-1).flatten(0, 2)
text_temporal = torch.arange(text_len, device=device, dtype=torch.int32) + 1
text_positions = torch.stack(
[text_temporal, torch.zeros_like(text_temporal),
torch.zeros_like(text_temporal)], dim=-1)
return torch.cat([video_positions, text_positions], dim=0)
class LingBotVideoTextEmbedder(nn.Module):
"""Project Qwen3-VL hidden states into the DiT hidden dimension."""
def __init__(self, text_dim: int, hidden_size: int) -> None:
super().__init__()
self.norm = LingBotVideoRMSNorm(text_dim, eps=1e-6)
self.linear_1 = LingBotVideoLinear(text_dim, hidden_size, bias=True)
self.linear_2 = LingBotVideoLinear(hidden_size, hidden_size, bias=True)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
"""Apply RMSNorm followed by the released two-layer SiLU projection."""
hidden_states = self.norm(hidden_states)
return self.linear_2(F.silu(self.linear_1(hidden_states)))
class LingBotVideoAttention(nn.Module):
"""Joint video-text attention shared by the Dense and MoE variants."""
def __init__(
self,
hidden_size: int,
num_heads: int,
norm_eps: float,
qkv_bias: bool,
out_bias: bool,
) -> None:
"""Create released QKV projections, per-head norms, and output projection."""
super().__init__()
self.num_heads = num_heads
self.head_dim = hidden_size // num_heads
self.to_q = LingBotVideoLinear(hidden_size, hidden_size, bias=qkv_bias)
self.to_k = LingBotVideoLinear(hidden_size, hidden_size, bias=qkv_bias)
self.to_v = LingBotVideoLinear(hidden_size, hidden_size, bias=qkv_bias)
self.norm_q = LingBotVideoRMSNorm(self.head_dim, norm_eps)
self.norm_k = LingBotVideoRMSNorm(self.head_dim, norm_eps)
self.to_out = LingBotVideoLinear(hidden_size, hidden_size, bias=out_bias)
def forward(
self,
hidden_states: torch.Tensor,
rotary_emb: torch.Tensor,
attention_mask: torch.Tensor | None = None,
original_seq_len: int | None = None,
) -> torch.Tensor:
"""Project QKV, apply rotary embeddings, and run non-causal SDPA."""
batch, sequence, _ = hidden_states.shape
query = self.to_q(hidden_states).unflatten(2, (self.num_heads, self.head_dim))
key = self.to_k(hidden_states).unflatten(2, (self.num_heads, self.head_dim))
value = self.to_v(hidden_states).unflatten(2, (self.num_heads, self.head_dim))
query = _apply_rotary_emb(self.norm_q(query), rotary_emb)
key = _apply_rotary_emb(self.norm_k(key), rotary_emb)
if original_seq_len is not None:
# Attention needs the full unpadded joint sequence while projections
# and residual blocks stay sharded over tokens.
qkv = torch.cat([query, key, value], dim=0)
qkv = sequence_model_parallel_all_to_all_4D(qkv, scatter_dim=2, gather_dim=1)
padded_seq_len = qkv.shape[1]
query, key, value = qkv[:, :original_seq_len].chunk(3, dim=0)
output = F.scaled_dot_product_attention(
query.transpose(1, 2),
key.transpose(1, 2),
value.transpose(1, 2),
attn_mask=attention_mask,
dropout_p=0.0,
is_causal=False,
).transpose(1, 2)
if original_seq_len is not None:
output = F.pad(output, (0, 0, 0, 0, 0, padded_seq_len - original_seq_len))
output = sequence_model_parallel_all_to_all_4D(output, scatter_dim=1, gather_dim=2)
return self.to_out(output.reshape(batch, sequence, -1).type_as(hidden_states))
class LingBotVideoMLP(nn.Module):
"""Dense SwiGLU feed-forward network."""
def __init__(self, hidden_size: int, intermediate_size: int) -> None:
super().__init__()
self.gate_proj = LingBotVideoLinear(hidden_size, intermediate_size, bias=False)
self.up_proj = LingBotVideoLinear(hidden_size, intermediate_size, bias=False)
self.down_proj = LingBotVideoLinear(intermediate_size, hidden_size, bias=False)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
"""Apply the released SiLU-gated MLP ordering."""
return self.down_proj(F.silu(self.gate_proj(hidden_states)) * self.up_proj(hidden_states))
class LingBotVideoRouter(nn.Module):
"""Released token-choice router with bias-only expert selection correction."""
def __init__(
self,
hidden_size: int,
num_experts: int,
top_k: int,
score_func: str,
norm_topk_prob: bool,
n_group: int | None,
topk_group: int | None,
route_scale: float,
) -> None:
"""Create fp32-routed expert scores with the released persistent bias."""
super().__init__()
self.num_experts = num_experts
self.top_k = top_k
self.score_func = score_func
self.norm_topk_prob = norm_topk_prob
self.n_group = n_group
self.topk_group = topk_group
self.route_scale = route_scale
self.weight = nn.Parameter(torch.empty(num_experts, hidden_size))
self.register_buffer("e_score_correction_bias", torch.zeros(num_experts), persistent=True)
def _group_limited_topk(self, scores_for_choice: torch.Tensor) -> torch.Tensor:
"""Restrict token choices to groups with the two strongest expert scores."""
sequence_length = scores_for_choice.shape[0]
experts_per_group = self.num_experts // self.n_group
grouped = scores_for_choice.view(sequence_length, self.n_group, experts_per_group)
group_scores = grouped.topk(2, dim=-1)[0].sum(dim=-1)
group_indices = torch.topk(group_scores, k=self.topk_group, dim=-1, sorted=False)[1]
group_mask = torch.zeros_like(group_scores)
group_mask.scatter_(1, group_indices, 1)
score_mask = (group_mask.unsqueeze(-1).expand(sequence_length, self.n_group,
experts_per_group).reshape(sequence_length, -1))
masked_scores = scores_for_choice.masked_fill(~score_mask.bool(), float("-inf"))
return torch.topk(masked_scores, k=self.top_k, dim=-1, sorted=False)[1]
def forward(self,
tokens: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""Score in fp32, select with correction bias, and weight without it."""
with torch.amp.autocast(tokens.device.type, enabled=False):
logits = F.linear(tokens.float(), self.weight.float())
scores = F.softmax(logits, dim=-1) if self.score_func == "softmax" else logits.sigmoid()
scores_for_choice = scores + self.e_score_correction_bias.unsqueeze(0)
if self.n_group is not None and self.n_group > 1:
top_indices = self._group_limited_topk(scores_for_choice)
else:
top_indices = torch.topk(scores_for_choice, k=self.top_k, dim=-1, sorted=False)[1]
top_scores = scores.gather(1, top_indices)
if self.top_k > 1 and self.norm_topk_prob:
top_scores = top_scores / (top_scores.sum(dim=-1, keepdim=True) + 1e-20)
top_scores = top_scores * self.route_scale
return top_indices, top_scores.to(tokens.dtype), logits, scores, scores_for_choice
class LingBotVideoGroupedExperts(nn.Module):
"""Released grouped-expert parameter layout: w1/w3 `[E,I,H]`, w2 `[E,H,I]`."""
def __init__(self, num_experts: int, hidden_size: int, intermediate_size: int) -> None:
super().__init__()
self.num_experts = num_experts
self.w1 = nn.Parameter(torch.empty(num_experts, intermediate_size, hidden_size))
self.w2 = nn.Parameter(torch.empty(num_experts, hidden_size, intermediate_size))
self.w3 = nn.Parameter(torch.empty(num_experts, intermediate_size, hidden_size))
def _round_up_to_multiple(value: int, multiple: int) -> int:
"""Round an integer up to the next multiple."""
return ((value + multiple - 1) // multiple) * multiple
class LingBotVideoSparseMoeBlock(nn.Module):
"""Token-choice sparse feed-forward block matching the released state surface."""
def __init__(
self,
hidden_size: int,
intermediate_size: int,
num_experts: int,
top_k: int,
moe_intermediate_size: int,
score_func: str,
norm_topk_prob: bool,
n_group: int | None,
topk_group: int | None,
routed_scaling_factor: float,
n_shared_experts: int | None,
) -> None:
"""Create routed and optional shared experts with released parameter names."""
super().__init__()
del intermediate_size
self.hidden_size = hidden_size
self.num_experts = num_experts
self.router = LingBotVideoRouter(
hidden_size,
num_experts,
top_k,
score_func,
norm_topk_prob,
n_group,
topk_group,
routed_scaling_factor,
)
self.experts = LingBotVideoGroupedExperts(num_experts, hidden_size, moe_intermediate_size)
self.shared_experts = None
if n_shared_experts is not None and n_shared_experts > 0:
self.shared_experts = LingBotVideoMLP(hidden_size, moe_intermediate_size * n_shared_experts)
@staticmethod
def _reorder_tokens(
tokens: torch.Tensor,
top_scores: torch.Tensor,
top_indices: torch.Tensor,
num_experts: int,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, int, int]:
"""Pack active token choices into stable expert-major order."""
num_tokens = tokens.shape[0]
top_k = top_indices.shape[1]
flat_scores = top_scores.reshape(-1)
flat_indices = top_indices.reshape(-1)
active_positions = torch.where(flat_scores != 0)[0]
active_experts = flat_indices[active_positions]
counts = torch.zeros(num_experts, device=tokens.device, dtype=torch.int64)
counts.scatter_add_(0, active_experts, torch.ones_like(active_experts, dtype=torch.int64))
sort_order = torch.argsort(active_experts, stable=True)
sorted_positions = active_positions[sort_order]
sorted_scores = flat_scores[sorted_positions]
original_token_indices = sorted_positions // top_k
permuted_tokens = tokens[original_token_indices]
return permuted_tokens, counts, sorted_positions, sorted_scores, num_tokens, top_k
@staticmethod
def _pad_grouped_tokens(tokens: torch.Tensor,
counts: torch.Tensor,
align: int = 8) -> tuple[torch.Size, torch.Tensor, torch.Tensor, torch.Tensor]:
"""Align each expert segment for `torch._grouped_mm` and retain unpad indices."""
num_tokens = tokens.shape[0]
num_experts = int(counts.shape[0])
max_length = _round_up_to_multiple(num_tokens + num_experts * align, align)
# Build the small padding index on CPU after one synchronization instead
# of reading three CUDA scalars for every expert.
counts_cpu = counts.to(device="cpu", dtype=torch.int64)
total_per_expert = torch.clamp_min(counts_cpu, align)
aligned_counts_cpu = ((total_per_expert + align - 1) // align * align).to(torch.int32)
write_offsets = torch.cumsum(aligned_counts_cpu, dim=0) - aligned_counts_cpu
start_indices = torch.cumsum(counts_cpu, dim=0) - counts_cpu
permuted_indices_cpu = torch.full((max_length, ), num_tokens, dtype=torch.int64, device="cpu")
for expert_index in range(num_experts):
length = int(counts_cpu[expert_index])
if length == 0:
continue
write_start = int(write_offsets[expert_index])
start = int(start_indices[expert_index])
permuted_indices_cpu[write_start:write_start + length] = torch.arange(start,
start + length,
device="cpu",
dtype=torch.int64)
permuted_indices = permuted_indices_cpu.to(tokens.device)
aligned_counts = aligned_counts_cpu.to(tokens.device)
tokens_with_pad = torch.vstack((tokens, tokens.new_zeros((tokens.shape[-1], ))))
input_shape = tokens_with_pad.shape
return input_shape, tokens_with_pad[permuted_indices], permuted_indices, aligned_counts
@staticmethod
def _unpad_grouped_tokens(output: torch.Tensor, input_shape: torch.Size,
permuted_indices: torch.Tensor) -> torch.Tensor:
"""Undo per-expert alignment while dropping the shared padding row."""
unpermuted = output.new_empty(input_shape)
unpermuted[permuted_indices, :] = output
return unpermuted[:-1]
def _run_grouped_experts(self, tokens: torch.Tensor, counts: torch.Tensor) -> torch.Tensor:
"""Use the released bf16 grouped matmuls on CUDA and an eager CPU fallback."""
if tokens.device.type == "cpu" or not hasattr(torch, "_grouped_mm"):
return self._run_experts_for_loop(tokens, counts)
input_shape, padded_tokens, permuted_indices, aligned_counts = self._pad_grouped_tokens(tokens, counts)
offsets = torch.cumsum(aligned_counts, dim=0, dtype=torch.int32)
hidden = F.silu(
torch._grouped_mm(
padded_tokens.bfloat16(),
self.experts.w1.bfloat16().transpose(-2, -1),
offs=offsets,
))
hidden = hidden * torch._grouped_mm(
padded_tokens.bfloat16(),
self.experts.w3.bfloat16().transpose(-2, -1),
offs=offsets,
)
output = torch._grouped_mm(
hidden,
self.experts.w2.bfloat16().transpose(-2, -1),
offs=offsets,
).type_as(padded_tokens)
return self._unpad_grouped_tokens(output, input_shape, permuted_indices)
def _run_experts_for_loop(self, tokens: torch.Tensor, counts: torch.Tensor) -> torch.Tensor:
"""Evaluate contiguous expert segments eagerly for CPU correctness tests."""
splits = torch.split(tokens, counts.tolist(), dim=0)
outputs: list[torch.Tensor] = []
for expert_index, expert_tokens in enumerate(splits):
if expert_tokens.numel() == 0:
continue
hidden = F.silu(expert_tokens @ self.experts.w1[expert_index].transpose(-2, -1))
hidden = hidden * (expert_tokens @ self.experts.w3[expert_index].transpose(-2, -1))
outputs.append(hidden @ self.experts.w2[expert_index].transpose(-2, -1))
if not outputs:
return tokens.new_zeros(tokens.shape)
return torch.cat(outputs, dim=0)
@staticmethod
def _restore_tokens(
expert_output: torch.Tensor,
sorted_positions: torch.Tensor,
sorted_scores: torch.Tensor,
num_tokens: int,
top_k: int,
) -> torch.Tensor:
"""Restore token order and combine expert outputs with fp32 weighted sums."""
hidden_size = expert_output.shape[-1]
unsorted = torch.zeros(
(num_tokens * top_k, hidden_size),
dtype=expert_output.dtype,
device=expert_output.device,
)
unsorted[sorted_positions] = expert_output
unsorted = unsorted.reshape(num_tokens, top_k, hidden_size)
scores_unsorted = torch.zeros(
num_tokens * top_k,
dtype=sorted_scores.dtype,
device=sorted_scores.device,
)
scores_unsorted[sorted_positions] = sorted_scores
scores_unsorted = scores_unsorted.reshape(num_tokens, top_k, 1)
return (unsorted.float() * scores_unsorted).sum(dim=1).to(expert_output.dtype)
def _run_selected_experts(
self,
tokens: torch.Tensor,
top_scores: torch.Tensor,
top_indices: torch.Tensor,
) -> torch.Tensor:
"""Dispatch routed choices, execute experts, and restore token-major order."""
permuted_tokens, counts, sorted_positions, sorted_scores, num_tokens, top_k = self._reorder_tokens(
tokens, top_scores, top_indices, self.router.num_experts)
expert_output = self._run_grouped_experts(permuted_tokens, counts)
return self._restore_tokens(expert_output, sorted_positions, sorted_scores, num_tokens, top_k)
def forward(self, hidden_states: torch.Tensor, padding_mask: torch.Tensor | None = None) -> torch.Tensor:
"""Route token choices, zero padded routes, and add optional shared experts."""
batch = hidden_states.shape[0]
tokens = hidden_states.view(-1, self.hidden_size)
top_indices, top_scores, logits, scores, scores_for_choice = self.router(tokens)
del logits, scores, scores_for_choice
if padding_mask is not None:
mask = padding_mask.unsqueeze(-1).to(top_scores.dtype)
top_scores = top_scores * mask
top_scores = top_scores / (top_scores.sum(dim=-1, keepdim=True) + 1e-9)
top_scores = top_scores * self.router.route_scale
output = self._run_selected_experts(tokens, top_scores, top_indices)
output = output.view(batch, -1, self.hidden_size)
if self.shared_experts is not None:
output = output + self.shared_experts(hidden_states)
return output
class LingBotVideoBlock(nn.Module):
"""One Dense or sparse LingBot-Video transformer block."""
def __init__(
self,
hidden_size: int,
num_attention_heads: int,
intermediate_size: int,
norm_eps: float,
qkv_bias: bool,
out_bias: bool,
num_experts: int,
num_experts_per_tok: int,
moe_intermediate_size: int,
decoder_sparse_step: int,
mlp_only_layers: tuple[int, ...] | list[int],
n_shared_experts: int | None,
score_func: str,
norm_topk_prob: bool,
n_group: int | None,
topk_group: int | None,
routed_scaling_factor: float,
layer_idx: int,
) -> None:
"""Select the released Dense or sparse feed-forward structure for one layer."""
super().__init__()
self.layer_idx = layer_idx
self.scale_shift_table = nn.Parameter(torch.zeros(1, 6 * hidden_size))
self.norm1 = LingBotVideoRMSNorm(hidden_size, norm_eps)
self.attn = LingBotVideoAttention(hidden_size, num_attention_heads, norm_eps, qkv_bias, out_bias)
self.norm_post_attn = LingBotVideoRMSNorm(hidden_size, norm_eps)
self.norm2 = LingBotVideoRMSNorm(hidden_size, norm_eps)
if layer_idx not in mlp_only_layers and (num_experts > 0 and (layer_idx + 1) % decoder_sparse_step == 0):
self.ffn = LingBotVideoSparseMoeBlock(
hidden_size,
intermediate_size,
num_experts,
num_experts_per_tok,
moe_intermediate_size,
score_func,
norm_topk_prob,
n_group,
topk_group,
routed_scaling_factor,
n_shared_experts,
)
else:
self.ffn = LingBotVideoMLP(hidden_size, intermediate_size)
self.norm_post_ffn = LingBotVideoRMSNorm(hidden_size, norm_eps)
def forward(
self,
hidden_states: torch.Tensor,
temb6: torch.Tensor,
rotary_emb: torch.Tensor,
attention_mask: torch.Tensor | None = None,
moe_padding_mask: torch.Tensor | None = None,
original_seq_len: int | None = None,
) -> torch.Tensor:
"""Run attention and configured feed-forward residual branches with fp32 AdaLN."""
expected_tokens = hidden_states.shape[0] * hidden_states.shape[1]
if temb6.ndim != 2 or temb6.shape[0] != expected_tokens:
raise ValueError("LingBotVideoBlock expects token-level temb6 with shape "
f"(B*S, 6D); got {tuple(temb6.shape)} for {tuple(hidden_states.shape)}.")
modulation = temb6.view(*hidden_states.shape[:2], -1) + self.scale_shift_table.unsqueeze(0)
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = modulation.chunk(6, dim=-1)
gate_msa, gate_mlp = gate_msa.tanh(), gate_mlp.tanh()
scale_msa, scale_mlp = 1.0 + scale_msa, 1.0 + scale_mlp
bulk_dtype = self.attn.to_q.weight.dtype
attention_input = (self.norm1(hidden_states) * scale_msa + shift_msa).to(bulk_dtype)
attention_output = self.attn(attention_input, rotary_emb, attention_mask, original_seq_len)
hidden_states = hidden_states + (gate_msa * self.norm_post_attn(attention_output)).to(hidden_states.dtype)
mlp_input = (self.norm2(hidden_states) * scale_mlp + shift_mlp).to(bulk_dtype)
if isinstance(self.ffn, LingBotVideoSparseMoeBlock):
mlp_output = self.ffn(mlp_input, padding_mask=moe_padding_mask)
else:
mlp_output = self.ffn(mlp_input)
mlp_output = self.norm_post_ffn(mlp_output)
return hidden_states + (gate_mlp * mlp_output).to(hidden_states.dtype)
class LingBotVideoTimestepEmbedding(nn.Module):
"""Two-layer timestep embedding with released parameter names."""
def __init__(self, input_dim: int, hidden_size: int, bias: bool) -> None:
super().__init__()
self.linear_1 = LingBotVideoLinear(input_dim, hidden_size, bias=bias)
self.linear_2 = LingBotVideoLinear(hidden_size, hidden_size, bias=bias)
def forward(self, sample: torch.Tensor) -> torch.Tensor:
"""Apply the official linear-SiLU-linear timestep projection."""
return self.linear_2(F.silu(self.linear_1(sample)))
class LingBotVideoTransformer3DModel(BaseDiT):
"""LingBot-Video DiT with a source-compatible Dense or MoE state surface."""
_fsdp_shard_conditions = [_is_lingbot_video_block]
_compile_conditions = [_is_lingbot_video_block]
_supported_attention_backends = (
AttentionBackendEnum.TORCH_SDPA,
AttentionBackendEnum.FLASH_ATTN,
)
param_names_mapping = {r"^(.*)$": r"\1"}
reverse_param_names_mapping = {r"^(.*)$": r"\1"}
def _get_parameter_dtype(self, name: str, default_dtype: torch.dtype) -> torch.dtype:
"""Select the released mixed-precision dtype while loading each parameter."""
return torch.float32 if _keep_in_fp32(name) else default_dtype
def __init__(self, config: LingBotVideoConfig, hf_config: dict[str, Any]) -> None:
"""Construct the Dense or MoE variant from its released transformer config."""
config.update_model_arch(hf_config)
super().__init__(config, hf_config)
head_dim = config.hidden_size // config.num_attention_heads
if head_dim != sum(config.axes_dims):
raise ValueError(f"head_dim {head_dim} != sum(axes_dims) {sum(config.axes_dims)}")
sp_world_size = _sequence_parallel_world_size()
assert config.num_attention_heads % sp_world_size == 0, (
f"The number of attention heads ({config.num_attention_heads}) must be divisible by "
f"the sequence parallel size ({sp_world_size})")
self.hidden_size = config.hidden_size
self.num_attention_heads = config.num_attention_heads
self.num_channels_latents = config.in_channels
self.patch_embedder = LingBotVideoLinear(
config.in_channels * math.prod(config.patch_size),
config.hidden_size,
bias=config.patch_embed_bias,
)
self.time_proj = Timesteps(config.freq_dim, flip_sin_to_cos=True, downscale_freq_shift=0)
self.time_embedder = LingBotVideoTimestepEmbedding(config.freq_dim, config.hidden_size,
config.timestep_mlp_bias)
self.time_modulation = nn.Sequential(nn.SiLU(), LingBotVideoLinear(config.hidden_size, 6 * config.hidden_size))
self.text_embedder = LingBotVideoTextEmbedder(config.text_dim, config.hidden_size)
self.rope = LingBotVideoRotaryEmbedding(tuple(config.axes_dims), tuple(config.axes_lens), config.rope_theta)
self.blocks = nn.ModuleList([
LingBotVideoBlock(
config.hidden_size,
config.num_attention_heads,
config.intermediate_size,
config.norm_eps,
config.qkv_bias,
config.out_bias,
config.num_experts,
config.num_experts_per_tok,
config.moe_intermediate_size,
config.decoder_sparse_step,
config.mlp_only_layers,
config.n_shared_experts,
config.score_func,
config.norm_topk_prob,
config.n_group,
config.topk_group,
config.routed_scaling_factor,
layer_index,
) for layer_index in range(config.depth)
])
self.norm_out = nn.LayerNorm(config.hidden_size, elementwise_affine=False, eps=config.norm_eps)
self.norm_out_modulation = nn.Sequential(nn.SiLU(),
LingBotVideoLinear(config.hidden_size, 2 * config.hidden_size))
self.proj_out = LingBotVideoLinear(config.hidden_size, math.prod(config.patch_size) * config.out_channels)
self.__post_init__()
def to(self, *args: Any, **kwargs: Any) -> LingBotVideoTransformer3DModel:
"""Cast bulk weights while retaining the released fp32-sensitive modules."""
device, dtype, non_blocking, _ = torch._C._nn._parse_to(*args, **kwargs)
if dtype is None or dtype == torch.float32:
return super().to(*args, **kwargs)
if not torch.is_floating_point(torch.empty((), dtype=dtype)):
return super().to(*args, **kwargs)
if device is not None:
super().to(device=device, non_blocking=non_blocking)
for name, parameter in self.named_parameters():
if torch.is_floating_point(parameter):
target_dtype = torch.float32 if _keep_in_fp32(name) else dtype
parameter.data = parameter.data.to(target_dtype, non_blocking=non_blocking)
if parameter.grad is not None:
parameter.grad.data = parameter.grad.data.to(target_dtype, non_blocking=non_blocking)
for name, buffer in self.named_buffers():
if torch.is_floating_point(buffer):
target_dtype = torch.float32 if _keep_in_fp32(name) else dtype
buffer.data = buffer.data.to(target_dtype, non_blocking=non_blocking)
return self
def forward(
self,
hidden_states: torch.Tensor,
timestep: torch.Tensor,
encoder_hidden_states: torch.Tensor | list[torch.Tensor],
encoder_hidden_states_image: torch.Tensor | list[torch.Tensor] | None = None,
guidance: torch.Tensor | None = None,
encoder_attention_mask: torch.Tensor | None = None,
return_dict: bool = True,
**kwargs: Any,
) -> LingBotVideoTransformerOutput | tuple[torch.Tensor]:
"""Denoise video latents with joint video-text attention."""
del encoder_hidden_states_image, guidance, kwargs
if isinstance(encoder_hidden_states, list):
if len(encoder_hidden_states) != 1:
raise ValueError("LingBot-Video expects one text-encoder output tensor.")
encoder_hidden_states = encoder_hidden_states[0]
batch, channels, frames, height, width = hidden_states.shape
patch_t, patch_h, patch_w = self.config.patch_size
grid_t, grid_h, grid_w = frames // patch_t, height // patch_h, width // patch_w
video_tokens = grid_t * grid_h * grid_w
text_tokens = encoder_hidden_states.shape[1]
device = hidden_states.device
if encoder_attention_mask is None:
encoder_attention_mask = torch.ones((batch, text_tokens), device=device, dtype=torch.bool)
text_lengths = encoder_attention_mask.sum(dim=-1).long()
patches = hidden_states.reshape(batch, channels, grid_t, patch_t, grid_h, patch_h, grid_w, patch_w)
patches = patches.permute(0, 2, 4, 6, 3, 5, 7, 1).reshape(batch, video_tokens,
patch_t * patch_h * patch_w * channels)
video_hidden = self.patch_embedder(patches)
text_hidden = self.text_embedder(encoder_hidden_states)
joint = torch.cat([video_hidden, text_hidden], dim=1)
rotary_parts: list[torch.Tensor] = []
for index in range(batch):
real_text_length = int(text_lengths[index].item())
positions = _make_joint_position_ids(real_text_length, grid_t, grid_h, grid_w, device)
maxima = (real_text_length + grid_t, grid_h - 1, grid_w - 1)
rotary = self.rope(positions, maxima=maxima)
if real_text_length < text_tokens:
padding = torch.zeros(
text_tokens - real_text_length,
rotary.shape[-1],
device=device,
dtype=rotary.dtype,
)
rotary = torch.cat([rotary, padding], dim=0)
rotary_parts.append(rotary)
rotary_emb = torch.stack(rotary_parts, dim=0)
attention_mask = None
moe_padding_mask = None
if bool((text_lengths < text_tokens).any()):
key_mask = torch.cat(
[
torch.ones(batch, video_tokens, dtype=torch.bool, device=device),
encoder_attention_mask.bool(),
],
dim=1,
)
attention_mask = key_mask[:, None, None, :]
moe_padding_mask = key_mask
timestep_projection = self.time_proj(timestep.float())
timestep_embedding = self.time_embedder(timestep_projection)
token_embedding = timestep_embedding.unsqueeze(1).expand(batch, joint.shape[1], -1)
original_joint_length: int | None = None
if _sequence_parallel_world_size() > 1:
# Match the official CP order: project token modulation before
# placing every token-aligned tensor on the same padded shard.
temb6 = self.time_modulation(token_embedding.reshape(-1,
self.hidden_size)).reshape(batch, joint.shape[1], -1)
joint, original_joint_length = sequence_model_parallel_shard(joint, dim=1)
rotary_emb, _ = sequence_model_parallel_shard(rotary_emb, dim=1)
token_embedding, _ = sequence_model_parallel_shard(token_embedding, dim=1)
temb6, _ = sequence_model_parallel_shard(temb6, dim=1)
if moe_padding_mask is None:
moe_padding_mask = torch.ones(batch, original_joint_length, dtype=torch.bool, device=device)
moe_padding_mask, _ = sequence_model_parallel_shard(moe_padding_mask, dim=1)
moe_padding_mask = moe_padding_mask.reshape(-1)
temb6 = temb6.reshape(-1, 6 * self.hidden_size)
else:
temb6 = self.time_modulation(token_embedding.reshape(-1, self.hidden_size))
if moe_padding_mask is not None:
moe_padding_mask = moe_padding_mask.reshape(-1)
for block in self.blocks:
joint = block(
joint,
temb6,
rotary_emb,
attention_mask,
moe_padding_mask,
original_joint_length,
)
final_modulation = self.norm_out_modulation(token_embedding.reshape(-1, self.hidden_size))
shift, scale = final_modulation.reshape(*joint.shape[:2], -1).chunk(2, dim=-1)
final_hidden = self.norm_out(joint) * (1.0 + scale) + shift
projected = self.proj_out(final_hidden.to(self.proj_out.weight.dtype))
if original_joint_length is not None:
projected = sequence_model_parallel_all_gather_with_unpad(projected, original_joint_length, dim=1)
projected = projected[:, :video_tokens]
output_channels = self.config.out_channels
output = projected.reshape(batch, grid_t, grid_h, grid_w, patch_t, patch_h, patch_w, output_channels)
output = output.permute(0, 7, 1, 4, 2, 5, 3, 6).reshape(batch, output_channels, frames, height, width)
if not return_dict:
return (output, )
return LingBotVideoTransformerOutput(sample=output)
EntryClass = LingBotVideoTransformer3DModel
@@ -0,0 +1,5 @@
from .causal_fast import LingBotWorld2CausalFastTransformer3DModel
__all__ = ["LingBotWorld2CausalFastTransformer3DModel"]
EntryClass = LingBotWorld2CausalFastTransformer3DModel
@@ -0,0 +1,204 @@
# SPDX-License-Identifier: Apache-2.0
import numpy as np
import os
import torch
from scipy.interpolate import interp1d
from scipy.spatial.transform import Rotation, Slerp
# --- Official Code (Leave Unchanged) ---
def interpolate_camera_poses(
src_indices: np.ndarray,
src_rot_mat: np.ndarray,
src_trans_vec: np.ndarray,
tgt_indices: np.ndarray,
) -> torch.Tensor:
# interpolate translation
interp_func_trans = interp1d(
src_indices,
src_trans_vec,
axis=0,
kind='linear',
bounds_error=False,
fill_value="extrapolate",
)
interpolated_trans_vec = interp_func_trans(tgt_indices)
# interpolate rotation
src_quat_vec = Rotation.from_matrix(src_rot_mat)
# ensure there is no sudden change in qw
quats = src_quat_vec.as_quat().copy() # [N, 4]
for i in range(1, len(quats)):
if np.dot(quats[i], quats[i-1]) < 0:
quats[i] = -quats[i]
src_quat_vec = Rotation.from_quat(quats)
slerp_func_rot = Slerp(src_indices, src_quat_vec)
interpolated_rot_quat = slerp_func_rot(tgt_indices)
interpolated_rot_mat = interpolated_rot_quat.as_matrix()
poses = np.zeros((len(tgt_indices), 4, 4))
poses[:, :3, :3] = interpolated_rot_mat
poses[:, :3, 3] = interpolated_trans_vec
poses[:, 3, 3] = 1.0
return torch.from_numpy(poses).float()
def SE3_inverse(T: torch.Tensor) -> torch.Tensor:
Rot = T[:, :3, :3] # [B,3,3]
trans = T[:, :3, 3:] # [B,3,1]
R_inv = Rot.transpose(-1, -2)
t_inv = -torch.bmm(R_inv, trans)
T_inv = torch.eye(4, device=T.device, dtype=T.dtype)[None, :, :].repeat(T.shape[0], 1, 1)
T_inv[:, :3, :3] = R_inv
T_inv[:, :3, 3:] = t_inv
return T_inv
def compute_relative_poses(
c2ws_mat: torch.Tensor,
framewise: bool = False,
normalize_trans: bool = True,
) -> torch.Tensor:
ref_w2cs = SE3_inverse(c2ws_mat[0:1])
relative_poses = torch.matmul(ref_w2cs, c2ws_mat)
# ensure identity matrix for 1st frame
relative_poses[0] = torch.eye(4, device=c2ws_mat.device, dtype=c2ws_mat.dtype)
if framewise:
# compute pose between i and i+1
relative_poses_framewise = torch.bmm(SE3_inverse(relative_poses[:-1]), relative_poses[1:])
relative_poses[1:] = relative_poses_framewise
if normalize_trans: # note refer to camctrl2: "we scale the coordinate inputs to roughly 1 standard deviation to simplify model learning."
translations = relative_poses[:, :3, 3] # [f, 3]
max_norm = torch.norm(translations, dim=-1).max()
# only normlaize when moving
if max_norm > 0:
relative_poses[:, :3, 3] = translations / max_norm
return relative_poses
@torch.no_grad()
def create_meshgrid(n_frames: int, height: int, width: int, bias: float = 0.5, device='cuda', dtype=torch.float32) -> torch.Tensor:
x_range = torch.arange(width, device=device, dtype=dtype)
y_range = torch.arange(height, device=device, dtype=dtype)
grid_y, grid_x = torch.meshgrid(y_range, x_range, indexing='ij')
grid_xy = torch.stack([grid_x, grid_y], dim=-1).view([-1, 2]) + bias # [h*w, 2]
grid_xy = grid_xy[None, ...].repeat(n_frames, 1, 1) # [f, h*w, 2]
return grid_xy
def get_plucker_embeddings(
c2ws_mat: torch.Tensor,
Ks: torch.Tensor,
height: int,
width: int,
):
n_frames = c2ws_mat.shape[0]
device = c2ws_mat.device
dtype = c2ws_mat.dtype
grid_y, grid_x = torch.meshgrid(
torch.arange(height, device=device, dtype=dtype) + 0.5,
torch.arange(width, device=device, dtype=dtype) + 0.5,
indexing='ij',
)
x_flat = grid_x.reshape(-1)
y_flat = grid_y.reshape(-1)
fx, fy, cx, cy = Ks[0, 0], Ks[0, 1], Ks[0, 2], Ks[0, 3]
dirs = torch.stack([
(x_flat - cx) / fx,
(y_flat - cy) / fy,
torch.ones_like(x_flat),
], dim=-1)
dirs = dirs / dirs.norm(dim=-1, keepdim=True)
rays_d = (c2ws_mat[:, :3, :3] @ dirs.T).transpose(1, 2)
rays_o = c2ws_mat[:, :3, 3].unsqueeze(1).expand_as(rays_d)
plucker_embeddings = torch.cat([rays_o, rays_d], dim=-1)
return plucker_embeddings.view(n_frames, height, width, 6)
def get_Ks_transformed(
Ks: torch.Tensor,
height_org: int,
width_org: int,
height_resize: int,
width_resize: int,
height_final: int,
width_final: int,
):
fx, fy, cx, cy = Ks.chunk(4, dim=-1) # [f, 1]
scale_x = width_resize / width_org
scale_y = height_resize / height_org
fx_resize = fx * scale_x
fy_resize = fy * scale_y
cx_resize = cx * scale_x
cy_resize = cy * scale_y
crop_offset_x = (width_resize - width_final) / 2
crop_offset_y = (height_resize - height_final) / 2
cx_final = cx_resize - crop_offset_x
cy_final = cy_resize - crop_offset_y
Ks_transformed = torch.zeros_like(Ks)
Ks_transformed[:, 0:1] = fx_resize
Ks_transformed[:, 1:2] = fy_resize
Ks_transformed[:, 2:3] = cx_final
Ks_transformed[:, 3:4] = cy_final
return Ks_transformed
# --- Custom ---
def prepare_camera_embedding(
action_path: str,
num_frames: int,
height: int,
width: int,
spatial_scale: int = 8,
) -> tuple[torch.Tensor, int]:
c2ws = np.load(os.path.join(action_path, "poses.npy"))
len_c2ws = ((len(c2ws) - 1) // 4) * 4 + 1
num_frames = min(num_frames, len_c2ws)
c2ws = c2ws[:num_frames]
Ks = torch.from_numpy(
np.load(os.path.join(action_path, "intrinsics.npy"))
).float()
Ks = get_Ks_transformed(
Ks,
height_org=480,
width_org=832,
height_resize=height,
width_resize=width,
height_final=height,
width_final=width,
)
Ks = Ks[0] # use first frame
len_c2ws = len(c2ws)
num_latent_frames = (len_c2ws - 1) // 4 + 1
c2ws_infer = interpolate_camera_poses(
src_indices=np.linspace(0, len_c2ws - 1, len_c2ws),
src_rot_mat=c2ws[:, :3, :3],
src_trans_vec=c2ws[:, :3, 3],
tgt_indices=np.linspace(0, len_c2ws - 1, num_latent_frames),
)
c2ws_infer = compute_relative_poses(c2ws_infer, framewise=True)
Ks = Ks.repeat(num_latent_frames, 1)
plucker = get_plucker_embeddings(c2ws_infer, Ks, height, width) # [F, H, W, 6]
# reshpae
latent_height = height // spatial_scale
latent_width = width // spatial_scale
plucker = plucker.view(num_latent_frames, latent_height, spatial_scale, latent_width, spatial_scale, 6)
plucker = plucker.permute(0, 1, 3, 5, 2, 4).contiguous()
plucker = plucker.view(num_latent_frames, latent_height, latent_width, 6 * spatial_scale * spatial_scale)
c2ws_plucker_emb = plucker.permute(3, 0, 1, 2).contiguous().unsqueeze(0)
return c2ws_plucker_emb, num_frames
@@ -0,0 +1,776 @@
# SPDX-License-Identifier: Apache-2.0
"""LingBot World 2 causal-fast DiT implemented inside FastVideo."""
import math
import warnings
from typing import Any
from einops import rearrange
import torch
import torch.nn as nn
import torch.nn.functional as F
from fastvideo.configs.models.dits.lingbotworld2 import (
LingBotWorld2CausalFastVideoConfig,
)
from fastvideo.distributed.communication_op import (
sequence_model_parallel_all_gather,
sequence_model_parallel_all_to_all_4D,
)
from fastvideo.distributed.parallel_state import get_sp_parallel_rank, get_sp_world_size
from fastvideo.models.dits.base import BaseDiT
from fastvideo.platforms import AttentionBackendEnum
try:
import flash_attn_interface
FLASH_ATTN_3_AVAILABLE = True
except ModuleNotFoundError:
FLASH_ATTN_3_AVAILABLE = False
try:
import flash_attn
FLASH_ATTN_2_AVAILABLE = True
except ModuleNotFoundError:
FLASH_ATTN_2_AVAILABLE = False
def is_blocks(n: str, m) -> bool:
return "blocks" in n and str.isdigit(n.split(".")[-1])
def sinusoidal_embedding_1d(dim: int, position: torch.Tensor) -> torch.Tensor:
"""Build Wan/LingBot World 2 sinusoidal timestep embeddings."""
assert dim % 2 == 0
half = dim // 2
position = position.type(torch.float64)
sinusoid = torch.outer(
position,
torch.pow(10000, -torch.arange(half, device=position.device).to(position).div(half)),
)
return torch.cat([torch.cos(sinusoid), torch.sin(sinusoid)], dim=1)
@torch.amp.autocast("cuda", enabled=False)
def rope_params(max_seq_len: int, dim: int, theta: int = 10000) -> torch.Tensor:
"""Return complex RoPE frequencies used by the released LingBot World 2 model."""
assert dim % 2 == 0
freqs = torch.outer(
torch.arange(max_seq_len),
1.0 / torch.pow(theta, torch.arange(0, dim, 2).to(torch.float64).div(dim)),
)
return torch.polar(torch.ones_like(freqs), freqs)
def flash_attention(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
q_lens: torch.Tensor | None = None,
k_lens: torch.Tensor | None = None,
dropout_p: float = 0.0,
softmax_scale: float | None = None,
q_scale: float | None = None,
causal: bool = False,
window_size: tuple[int, int] = (-1, -1),
deterministic: bool = False,
dtype: torch.dtype = torch.bfloat16,
version: int | None = None,
) -> torch.Tensor:
"""Run LingBot World 2-compatible FlashAttention on packed varlen inputs."""
half_dtypes = (torch.float16, torch.bfloat16)
assert dtype in half_dtypes
assert q.device.type == "cuda" and q.size(-1) <= 256
b, lq, lk, out_dtype = q.size(0), q.size(1), k.size(1), q.dtype
def half(x: torch.Tensor) -> torch.Tensor:
return x if x.dtype in half_dtypes else x.to(dtype)
if q_lens is None:
q = half(q.flatten(0, 1))
q_lens = torch.tensor([lq] * b, dtype=torch.int32, device=q.device)
else:
q = half(torch.cat([u[:v] for u, v in zip(q, q_lens, strict=True)]))
if k_lens is None:
k = half(k.flatten(0, 1))
v = half(v.flatten(0, 1))
k_lens = torch.tensor([lk] * b, dtype=torch.int32, device=k.device)
else:
k = half(torch.cat([u[:v] for u, v in zip(k, k_lens, strict=True)]))
v = half(torch.cat([u[:v] for u, v in zip(v, k_lens, strict=True)]))
q = q.to(v.dtype)
k = k.to(v.dtype)
if q_scale is not None:
q = q * q_scale
if version == 3 and not FLASH_ATTN_3_AVAILABLE:
warnings.warn("FlashAttention 3 is not available; using FlashAttention 2.")
cu_q = torch.cat([q_lens.new_zeros([1]), q_lens]).cumsum(
0, dtype=torch.int32
).to(q.device, non_blocking=True)
cu_k = torch.cat([k_lens.new_zeros([1]), k_lens]).cumsum(
0, dtype=torch.int32
).to(k.device, non_blocking=True)
if (version is None or version == 3) and FLASH_ATTN_3_AVAILABLE:
x = flash_attn_interface.flash_attn_varlen_func(
q=q,
k=k,
v=v,
cu_seqlens_q=cu_q,
cu_seqlens_k=cu_k,
seqused_q=None,
seqused_k=None,
max_seqlen_q=lq,
max_seqlen_k=lk,
softmax_scale=softmax_scale,
causal=causal,
deterministic=deterministic,
).unflatten(0, (b, lq))
else:
assert FLASH_ATTN_2_AVAILABLE
x = flash_attn.flash_attn_varlen_func(
q=q,
k=k,
v=v,
cu_seqlens_q=cu_q,
cu_seqlens_k=cu_k,
max_seqlen_q=lq,
max_seqlen_k=lk,
dropout_p=dropout_p,
softmax_scale=softmax_scale,
causal=causal,
window_size=window_size,
deterministic=deterministic,
).unflatten(0, (b, lq))
return x.type(out_dtype)
def attention(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
q_lens: torch.Tensor | None = None,
k_lens: torch.Tensor | None = None,
dropout_p: float = 0.0,
softmax_scale: float | None = None,
q_scale: float | None = None,
causal: bool = False,
window_size: tuple[int, int] = (-1, -1),
deterministic: bool = False,
dtype: torch.dtype = torch.bfloat16,
fa_version: int | None = None,
) -> torch.Tensor:
"""Dispatch LingBot World 2 attention to FlashAttention when available."""
if FLASH_ATTN_2_AVAILABLE or FLASH_ATTN_3_AVAILABLE:
return flash_attention(
q=q,
k=k,
v=v,
q_lens=q_lens,
k_lens=k_lens,
dropout_p=dropout_p,
softmax_scale=softmax_scale,
q_scale=q_scale,
causal=causal,
window_size=window_size,
deterministic=deterministic,
dtype=dtype,
version=fa_version,
)
if q_lens is not None or k_lens is not None:
warnings.warn("Padding masks are disabled without FlashAttention.")
q = q.transpose(1, 2).to(dtype)
k = k.transpose(1, 2).to(dtype)
v = v.transpose(1, 2).to(dtype)
out = F.scaled_dot_product_attention(
q, k, v, attn_mask=None, is_causal=causal, dropout_p=dropout_p
)
return out.transpose(1, 2).contiguous()
@torch.amp.autocast("cuda", enabled=False)
def causal_rope_apply(
x: torch.Tensor,
grid_sizes: torch.Tensor,
freqs: torch.Tensor,
start_frame: int = 0,
) -> torch.Tensor:
"""Apply LingBot World 2 causal RoPE with the current chunk frame offset."""
n, c = x.size(2), x.size(3) // 2
freqs = freqs.split([c - 2 * (c // 3), c // 3, c // 3], dim=1)
output = []
for i, (f, h, w) in enumerate(grid_sizes.tolist()):
seq_len = f * h * w
x_i = torch.view_as_complex(x[i, :seq_len].to(torch.float64).reshape(seq_len, n, -1, 2))
freqs_i = torch.cat(
[
freqs[0][start_frame : start_frame + f].view(f, 1, 1, -1).expand(f, h, w, -1),
freqs[1][:h].view(1, h, 1, -1).expand(f, h, w, -1),
freqs[2][:w].view(1, 1, w, -1).expand(f, h, w, -1),
],
dim=-1,
).reshape(seq_len, 1, -1)
x_i = torch.view_as_real(x_i * freqs_i).flatten(2)
x_i = torch.cat([x_i, x[i, seq_len:]])
output.append(x_i)
return torch.stack(output).type_as(x)
class WanRMSNorm(nn.Module):
"""RMSNorm used by Wan/LingBot World 2 attention projections."""
def __init__(self, dim: int, eps: float = 1e-5):
super().__init__()
self.dim = dim
self.eps = eps
self.weight = nn.Parameter(torch.ones(dim))
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Normalize the last dimension in fp32 and restore input dtype."""
return self._norm(x.float()).type_as(x) * self.weight
def _norm(self, x: torch.Tensor) -> torch.Tensor:
return x * torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps)
class WanLayerNorm(nn.LayerNorm):
"""LayerNorm variant that computes in fp32 and returns the input dtype."""
def __init__(self, dim: int, eps: float = 1e-6, elementwise_affine: bool = False):
super().__init__(dim, elementwise_affine=elementwise_affine, eps=eps)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Apply layer norm in fp32 for LingBot World 2 numerical parity."""
return super().forward(x.float()).type_as(x)
class CausalWanSelfAttention(nn.Module):
"""LingBot World 2 causal self-attention with rolling KV cache."""
def __init__(
self,
dim: int,
num_heads: int,
local_attn_size: int = -1,
sink_size: int = 0,
qk_norm: bool = True,
eps: float = 1e-6,
):
super().__init__()
assert dim % num_heads == 0
self.dim = dim
self.num_heads = num_heads
self.head_dim = dim // num_heads
self.local_attn_size = local_attn_size
self.sink_size = sink_size
self.qk_norm = qk_norm
self.eps = eps
self.q = nn.Linear(dim, dim)
self.k = nn.Linear(dim, dim)
self.v = nn.Linear(dim, dim)
self.o = nn.Linear(dim, dim)
self.norm_q = WanRMSNorm(dim, eps=eps) if qk_norm else nn.Identity()
self.norm_k = WanRMSNorm(dim, eps=eps) if qk_norm else nn.Identity()
def forward(
self,
x: torch.Tensor,
seq_lens: torch.Tensor,
grid_sizes: torch.Tensor,
freqs: torch.Tensor,
kv_cache: dict,
current_start: int = 0,
max_attention_size: int = 1_000_000,
frame_seqlen: int | None = None,
seq_lens_int: int | None = None,
) -> torch.Tensor:
"""Project QKV, update the rolling cache, and attend to its active window."""
b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim
q = self.norm_q(self.q(x)).view(b, s, n, d)
k = self.norm_k(self.k(x)).view(b, s, n, d)
v = self.v(x).view(b, s, n, d)
if frame_seqlen is None:
frame_seqlen = math.prod(grid_sizes[0][1:]).item()
current_start_frame = current_start // frame_seqlen
if seq_lens_int is None:
seq_lens_int = int(seq_lens[0].item() if seq_lens.dim() > 0 else seq_lens.item())
sp_size = get_sp_world_size()
if sp_size > 1:
q = sequence_model_parallel_all_to_all_4D(q, scatter_dim=2, gather_dim=1)
k = sequence_model_parallel_all_to_all_4D(k, scatter_dim=2, gather_dim=1)
v = sequence_model_parallel_all_to_all_4D(v, scatter_dim=2, gather_dim=1)
padded_seq_len = s * sp_size
roped_query = causal_rope_apply(q, grid_sizes, freqs, start_frame=current_start_frame).type_as(v)
roped_key = causal_rope_apply(k, grid_sizes, freqs, start_frame=current_start_frame).type_as(v)
roped_query = roped_query[:, :seq_lens_int]
roped_key = roped_key[:, :seq_lens_int]
v = v[:, :seq_lens_int]
num_new_tokens = seq_lens_int
else:
padded_seq_len = s
roped_query = causal_rope_apply(q, grid_sizes, freqs, start_frame=current_start_frame).type_as(v)
roped_key = causal_rope_apply(k, grid_sizes, freqs, start_frame=current_start_frame).type_as(v)
num_new_tokens = roped_query.shape[1]
current_end = current_start + num_new_tokens
sink_tokens = self.sink_size * frame_seqlen
kv_cache_size = kv_cache["k"].shape[1]
if self.local_attn_size == -1:
local_end_index = current_start + num_new_tokens
local_start_index = current_start
kv_cache["k"][:, local_start_index:local_end_index] = roped_key
kv_cache["v"][:, local_start_index:local_end_index] = v
elif (current_end > kv_cache["global_end_index"].item()) and (
num_new_tokens + kv_cache["local_end_index"].item() > kv_cache_size
):
num_evicted_tokens = num_new_tokens + kv_cache["local_end_index"].item() - kv_cache_size
num_rolled_tokens = kv_cache["local_end_index"].item() - num_evicted_tokens - sink_tokens
kv_cache["k"][:, sink_tokens : sink_tokens + num_rolled_tokens] = kv_cache["k"][
:, sink_tokens + num_evicted_tokens : sink_tokens + num_evicted_tokens + num_rolled_tokens
].clone()
kv_cache["v"][:, sink_tokens : sink_tokens + num_rolled_tokens] = kv_cache["v"][
:, sink_tokens + num_evicted_tokens : sink_tokens + num_evicted_tokens + num_rolled_tokens
].clone()
local_end_index = kv_cache["local_end_index"].item() + current_end - kv_cache["global_end_index"].item() - num_evicted_tokens
local_start_index = local_end_index - num_new_tokens
kv_cache["k"][:, local_start_index:local_end_index] = roped_key
kv_cache["v"][:, local_start_index:local_end_index] = v
else:
local_end_index = kv_cache["local_end_index"].item() + current_end - kv_cache["global_end_index"].item()
local_start_index = local_end_index - num_new_tokens
kv_cache["k"][:, local_start_index:local_end_index] = roped_key
kv_cache["v"][:, local_start_index:local_end_index] = v
k_cache = kv_cache["k"][:, max(0, local_end_index - max_attention_size) : local_end_index]
v_cache = kv_cache["v"][:, max(0, local_end_index - max_attention_size) : local_end_index]
x = attention(roped_query, k_cache, v_cache)
kv_cache["global_end_index"].fill_(current_end)
kv_cache["local_end_index"].fill_(local_end_index)
if sp_size > 1:
sp_pad = padded_seq_len - seq_lens_int
if sp_pad > 0:
x = torch.cat([x, x.new_zeros(b, sp_pad, x.size(2), d)], dim=1)
x = sequence_model_parallel_all_to_all_4D(x, scatter_dim=1, gather_dim=2)
return self.o(x.flatten(2))
class WanCrossAttention(CausalWanSelfAttention):
"""LingBot World 2 cross-attention with reusable text K/V cache."""
def forward(
self,
x: torch.Tensor,
context: torch.Tensor,
context_lens: torch.Tensor | None,
crossattn_cache: dict | None = None,
cross_attn_first_call: bool | None = None,
) -> torch.Tensor:
"""Attend hidden states to text context, populating cache on first use."""
b, n, d = x.size(0), self.num_heads, self.head_dim
q = self.norm_q(self.q(x)).view(b, -1, n, d)
if crossattn_cache is not None:
is_first = crossattn_cache["is_init"].item() == 0 if cross_attn_first_call is None else cross_attn_first_call
if is_first:
crossattn_cache["is_init"].fill_(1)
k = self.norm_k(self.k(context)).view(b, -1, n, d)
v = self.v(context).view(b, -1, n, d)
crossattn_cache["k"].copy_(k)
crossattn_cache["v"].copy_(v)
else:
k = crossattn_cache["k"]
v = crossattn_cache["v"]
else:
k = self.norm_k(self.k(context)).view(b, -1, n, d)
v = self.v(context).view(b, -1, n, d)
x = flash_attention(q, k, v, k_lens=context_lens)
return self.o(x.flatten(2))
class CausalWanAttentionBlock(nn.Module):
"""One LingBot World 2 causal transformer block including camera injection."""
def __init__(
self,
dim: int,
ffn_dim: int,
num_heads: int,
local_attn_size: int = -1,
sink_size: int = 0,
qk_norm: bool = True,
cross_attn_norm: bool = False,
eps: float = 1e-6,
):
super().__init__()
self.dim = dim
self.ffn_dim = ffn_dim
self.num_heads = num_heads
self.local_attn_size = local_attn_size
self.qk_norm = qk_norm
self.cross_attn_norm = cross_attn_norm
self.eps = eps
self.norm1 = WanLayerNorm(dim, eps)
self.self_attn = CausalWanSelfAttention(dim, num_heads, local_attn_size, sink_size, qk_norm, eps)
self.norm3 = WanLayerNorm(dim, eps, elementwise_affine=True) if cross_attn_norm else nn.Identity()
self.cross_attn = WanCrossAttention(dim, num_heads, qk_norm=qk_norm, eps=eps)
self.norm2 = WanLayerNorm(dim, eps)
self.ffn = nn.Sequential(nn.Linear(dim, ffn_dim), nn.GELU(approximate="tanh"), nn.Linear(ffn_dim, dim))
self.modulation = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
self.cam_injector_layer1 = nn.Linear(dim, dim)
self.cam_injector_layer2 = nn.Linear(dim, dim)
self.cam_scale_layer = nn.Linear(dim, dim)
self.cam_shift_layer = nn.Linear(dim, dim)
def forward(
self,
x: torch.Tensor,
e: torch.Tensor,
seq_lens: torch.Tensor,
grid_sizes: torch.Tensor,
freqs: torch.Tensor,
context: torch.Tensor,
context_lens: torch.Tensor | None,
dit_cond_dict: dict[str, Any] | None = None,
kv_cache: dict | None = None,
crossattn_cache: dict | None = None,
current_start: int = 0,
max_attention_size: int = 1_000_000,
frame_seqlen: int | None = None,
cross_attn_first_call: bool | None = None,
seq_lens_int: int | None = None,
) -> torch.Tensor:
"""Apply self-attention, camera modulation, cross-attention, and FFN."""
assert kv_cache is not None
assert e.dtype == torch.float32
with torch.amp.autocast("cuda", dtype=torch.float32):
e = (self.modulation.unsqueeze(0) + e).chunk(6, dim=2)
y = self.self_attn(
self.norm1(x).float() * (1 + e[1].squeeze(2)) + e[0].squeeze(2),
seq_lens,
grid_sizes,
freqs,
kv_cache,
current_start,
max_attention_size,
frame_seqlen=frame_seqlen,
seq_lens_int=seq_lens_int,
)
with torch.amp.autocast("cuda", dtype=torch.float32):
x = x + y * e[2].squeeze(2)
if dit_cond_dict is not None and "c2ws_plucker_emb" in dit_cond_dict:
c2ws_plucker_emb = dit_cond_dict["c2ws_plucker_emb"]
c2ws_hidden_states = self.cam_injector_layer2(
F.silu(self.cam_injector_layer1(c2ws_plucker_emb))
)
c2ws_hidden_states = c2ws_hidden_states + c2ws_plucker_emb
x = (1.0 + self.cam_scale_layer(c2ws_hidden_states)) * x + self.cam_shift_layer(c2ws_hidden_states)
x = x + self.cross_attn(
self.norm3(x),
context,
context_lens,
crossattn_cache=crossattn_cache,
cross_attn_first_call=cross_attn_first_call,
)
y = self.ffn(self.norm2(x).float() * (1 + e[4].squeeze(2)) + e[3].squeeze(2))
with torch.amp.autocast("cuda", dtype=torch.float32):
x = x + y * e[5].squeeze(2)
return x
class CausalHead(nn.Module):
"""Output projection head for LingBot World 2 causal-fast DiT."""
def __init__(self, dim: int, out_dim: int, patch_size: tuple[int, int, int], eps: float = 1e-6):
super().__init__()
self.dim = dim
self.out_dim = out_dim
self.patch_size = patch_size
self.eps = eps
self.norm = WanLayerNorm(dim, eps)
self.head = nn.Linear(dim, math.prod(patch_size) * out_dim)
self.modulation = nn.Parameter(torch.randn(1, 2, dim) / dim**0.5)
def forward(self, x: torch.Tensor, e: torch.Tensor) -> torch.Tensor:
"""Normalize, modulate, and project hidden states to latent patches."""
assert e.dtype == torch.float32
with torch.amp.autocast("cuda", dtype=torch.float32):
e = (self.modulation.unsqueeze(0) + e.unsqueeze(2)).chunk(2, dim=2)
x = self.head(self.norm(x) * (1 + e[1].squeeze(2)) + e[0].squeeze(2))
return x
class LingBotWorld2CausalFastTransformer3DModel(BaseDiT):
"""Released LingBot World 2 14B causal-fast model with native FastVideo loading."""
_fsdp_shard_conditions = [is_blocks]
_compile_conditions: list = []
_supported_attention_backends = (
AttentionBackendEnum.FLASH_ATTN,
AttentionBackendEnum.TORCH_SDPA,
)
param_names_mapping: dict = {}
reverse_param_names_mapping: dict = {}
lora_param_names_mapping: dict = {}
def __init__(self, config: LingBotWorld2CausalFastVideoConfig, hf_config: dict[str, Any]) -> None:
super().__init__(config=config, hf_config=hf_config)
self.model_type = config.model_type
self.patch_size = tuple(config.patch_size)
self.text_len = config.text_len
self.in_dim = config.in_dim
self.dim = config.dim
self.hidden_size = config.dim
self.ffn_dim = config.ffn_dim
self.freq_dim = config.freq_dim
self.text_dim = config.text_dim
self.out_dim = config.out_dim
self.out_channels = config.out_dim
self.num_heads = config.num_heads
self.num_attention_heads = config.num_heads
self.attention_head_dim = config.dim // config.num_heads
self.num_layers = config.num_layers
self.local_attn_size = config.local_attn_size
self.sink_size = config.sink_size
self.qk_norm = config.qk_norm
self.cross_attn_norm = config.cross_attn_norm
self.eps = config.eps
self.num_channels_latents = config.out_dim
control_dim = 6
self.patch_embedding = nn.Conv3d(self.in_dim, self.dim, kernel_size=self.patch_size, stride=self.patch_size)
self.patch_embedding_wancamctrl = nn.Linear(
control_dim * 64 * self.patch_size[0] * self.patch_size[1] * self.patch_size[2],
self.dim,
)
self.c2ws_hidden_states_layer1 = nn.Linear(self.dim, self.dim)
self.c2ws_hidden_states_layer2 = nn.Linear(self.dim, self.dim)
self.text_embedding = nn.Sequential(
nn.Linear(self.text_dim, self.dim),
nn.GELU(approximate="tanh"),
nn.Linear(self.dim, self.dim),
)
self.time_embedding = nn.Sequential(
nn.Linear(self.freq_dim, self.dim),
nn.SiLU(),
nn.Linear(self.dim, self.dim),
)
self.time_projection = nn.Sequential(nn.SiLU(), nn.Linear(self.dim, self.dim * 6))
self.blocks = nn.ModuleList(
[
CausalWanAttentionBlock(
self.dim,
self.ffn_dim,
self.num_heads,
self.local_attn_size,
self.sink_size,
self.qk_norm,
self.cross_attn_norm,
self.eps,
)
for _ in range(self.num_layers)
]
)
self.head = CausalHead(self.dim, self.out_dim, self.patch_size, self.eps)
self.freqs: torch.Tensor | None = None
self.init_weights()
self.__post_init__()
def _get_freqs(self, device: torch.device) -> torch.Tensor:
"""Materialize the non-persistent RoPE frequency table outside meta init."""
if self.freqs is None or self.freqs.is_meta or self.freqs.device != device:
d = self.dim // self.num_heads
self.freqs = torch.cat(
[
rope_params(1024, d - 4 * (d // 6)),
rope_params(1024, 2 * (d // 6)),
rope_params(1024, 2 * (d // 6)),
],
dim=1,
).to(device)
return self.freqs
def forward(
self,
hidden_states: torch.Tensor | list[torch.Tensor] | None = None,
encoder_hidden_states: torch.Tensor | list[torch.Tensor] | None = None,
timestep: torch.Tensor | None = None,
encoder_hidden_states_image: torch.Tensor | list[torch.Tensor] | None = None,
guidance=None,
*,
x: list[torch.Tensor] | None = None,
t: torch.Tensor | None = None,
context: list[torch.Tensor] | torch.Tensor | None = None,
seq_len: int | None = None,
y: list[torch.Tensor] | None = None,
dit_cond_dict: dict[str, Any] | None = None,
kv_cache: list[dict] | None = None,
crossattn_cache: list[dict] | None = None,
current_start: int = 0,
max_attention_size: int = 1_000_000,
frame_seqlen: int | None = None,
cross_attn_first_call: bool | None = None,
**kwargs,
) -> list[torch.Tensor]:
"""Run one cached causal-fast DiT forward using the released LingBot World 2 ABI."""
del encoder_hidden_states_image, guidance, kwargs
if x is None:
assert isinstance(hidden_states, torch.Tensor)
x = [hidden_states[0]]
if t is None:
assert timestep is not None
t = timestep
if context is None:
context = encoder_hidden_states
if isinstance(context, torch.Tensor):
context = [u for u in context]
assert context is not None
assert seq_len is not None
assert kv_cache is not None
assert crossattn_cache is not None
if self.model_type == "i2v":
assert y is not None
device = self.patch_embedding.weight.device
freqs = self._get_freqs(device)
if y is not None:
x = [torch.cat([u, v], dim=0) for u, v in zip(x, y, strict=True)]
x = [self.patch_embedding(u.unsqueeze(0)) for u in x]
grid_sizes = torch.stack([torch.tensor(u.shape[2:], dtype=torch.long, device=u.device) for u in x])
x = [u.flatten(2).transpose(1, 2) for u in x]
seq_lens = torch.tensor([u.size(1) for u in x], dtype=torch.long, device=device)
assert seq_lens.max() <= seq_len
x = torch.cat(x)
seq_lens_int = int(seq_lens[0].item())
sp_size = get_sp_world_size()
sp_rank = get_sp_parallel_rank()
padded_seq_len = ((seq_lens_int + sp_size - 1) // sp_size) * sp_size
sp_pad_len = padded_seq_len - seq_lens_int
if sp_pad_len > 0:
x = torch.cat([x, x.new_zeros(x.size(0), sp_pad_len, x.size(2))], dim=1)
if t.dim() == 1:
t = t.expand(t.size(0), padded_seq_len)
with torch.amp.autocast("cuda", dtype=torch.float32):
bt = t.size(0)
t = t.flatten()
e = self.time_embedding(
sinusoidal_embedding_1d(self.freq_dim, t).unflatten(0, (bt, padded_seq_len)).float()
)
e0 = self.time_projection(e).unflatten(2, (6, self.dim))
context_lens = None
context = self.text_embedding(
torch.stack([torch.cat([u, u.new_zeros(self.text_len - u.size(0), u.size(1))]) for u in context])
)
if dit_cond_dict is not None and "c2ws_plucker_emb" in dit_cond_dict:
c2ws_plucker_emb = dit_cond_dict["c2ws_plucker_emb"]
c2ws_plucker_emb = [
rearrange(
i,
"1 c (f c1) (h c2) (w c3) -> 1 (f h w) (c c1 c2 c3)",
c1=self.patch_size[0],
c2=self.patch_size[1],
c3=self.patch_size[2],
)
for i in c2ws_plucker_emb
]
c2ws_plucker_emb = torch.cat(c2ws_plucker_emb, dim=1)
c2ws_plucker_emb = self.patch_embedding_wancamctrl(c2ws_plucker_emb)
c2ws_hidden_states = self.c2ws_hidden_states_layer2(
F.silu(self.c2ws_hidden_states_layer1(c2ws_plucker_emb))
)
c2ws_plucker_emb = c2ws_plucker_emb + c2ws_hidden_states
cam_len = c2ws_plucker_emb.size(1)
if cam_len < padded_seq_len:
c2ws_plucker_emb = torch.cat(
[
c2ws_plucker_emb,
c2ws_plucker_emb.new_zeros(
c2ws_plucker_emb.size(0),
padded_seq_len - cam_len,
c2ws_plucker_emb.size(2),
),
],
dim=1,
)
elif cam_len > padded_seq_len:
c2ws_plucker_emb = c2ws_plucker_emb[:, :padded_seq_len, :]
if sp_size > 1:
c2ws_plucker_emb = torch.chunk(c2ws_plucker_emb, sp_size, dim=1)[sp_rank]
dit_cond_dict = dict(dit_cond_dict)
dit_cond_dict["c2ws_plucker_emb"] = c2ws_plucker_emb
if sp_size > 1:
x = torch.chunk(x, sp_size, dim=1)[sp_rank]
e = torch.chunk(e, sp_size, dim=1)[sp_rank]
e0 = torch.chunk(e0, sp_size, dim=1)[sp_rank]
for block_index, block in enumerate(self.blocks):
x = block(
x,
e=e0,
seq_lens=seq_lens,
grid_sizes=grid_sizes,
freqs=freqs,
context=context,
context_lens=context_lens,
dit_cond_dict=dit_cond_dict,
kv_cache=kv_cache[block_index],
crossattn_cache=crossattn_cache[block_index],
current_start=current_start,
max_attention_size=max_attention_size,
frame_seqlen=frame_seqlen,
cross_attn_first_call=cross_attn_first_call,
seq_lens_int=seq_lens_int,
)
x = self.head(x, e)
if sp_size > 1:
x = sequence_model_parallel_all_gather(x, dim=1)
return [u.float() for u in self.unpatchify(x, grid_sizes)]
def unpatchify(self, x: torch.Tensor, grid_sizes: torch.Tensor) -> list[torch.Tensor]:
"""Reconstruct latent videos from flattened patch tokens."""
c = self.out_dim
out = []
for u, v in zip(x, grid_sizes.tolist(), strict=True):
u = u[: math.prod(v)].view(*v, *self.patch_size, c)
u = torch.einsum("fhwpqrc->cfphqwr", u)
u = u.reshape(c, *[i * j for i, j in zip(v, self.patch_size, strict=True)])
out.append(u)
return out
def init_weights(self) -> None:
"""Initialize modules for non-meta construction; checkpoint load overwrites them."""
if self.patch_embedding.weight.is_meta:
return
for m in self.modules():
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)
if m.bias is not None:
nn.init.zeros_(m.bias)
nn.init.xavier_uniform_(self.patch_embedding.weight.flatten(1))
for m in self.text_embedding.modules():
if isinstance(m, nn.Linear):
nn.init.normal_(m.weight, std=0.02)
for m in self.time_embedding.modules():
if isinstance(m, nn.Linear):
nn.init.normal_(m.weight, std=0.02)
nn.init.zeros_(self.head.head.weight)
EntryClass = LingBotWorld2CausalFastTransformer3DModel
+44 -31
View File
@@ -5,14 +5,11 @@ LTX-2 transformer implementation
from dataclasses import dataclass, replace
from enum import Enum
import functools
import math
import os
from pathlib import Path
from typing import Any, Optional, Tuple, Callable
import numpy as np
import torch
import torch.nn as nn
from einops import rearrange, repeat
@@ -770,28 +767,30 @@ def _apply_ltx_split_rotary_emb(
return output
@functools.lru_cache(maxsize=5)
def generate_ltx_freq_grid_np(
def generate_ltx_freq_grid_float64(
positional_embedding_theta: float, positional_embedding_max_pos_count: int, inner_dim: int
) -> torch.Tensor:
"""Generate LTX-2 rotary frequencies with high-precision numpy."""
"""Generate LTX-2 rotary frequencies with float64 precision.
Implemented in pure torch and free of Python memoization so it is safe
for callers that may be captured and reused by torch.compile.
"""
theta = positional_embedding_theta
start = 1
end = theta
n_elem = 2 * positional_embedding_max_pos_count
pow_indices = np.power(
pow_indices = torch.pow(
theta,
np.linspace(
np.log(start) / np.log(theta),
np.log(end) / np.log(theta),
torch.linspace(
math.log(start) / math.log(theta),
math.log(end) / math.log(theta),
inner_dim // n_elem,
dtype=np.float64,
dtype=torch.float64,
),
)
return torch.tensor(pow_indices * math.pi / 2, dtype=torch.float32)
return (pow_indices * math.pi / 2).to(dtype=torch.float32)
@functools.lru_cache(maxsize=5)
def generate_ltx_freq_grid_pytorch(
positional_embedding_theta: float, positional_embedding_max_pos_count: int, inner_dim: int
) -> torch.Tensor:
@@ -1012,7 +1011,7 @@ class TransformerArgsPreprocessor:
x_dtype: torch.dtype,
) -> torch.Tensor:
if self.double_precision_rope:
freq_grid_generator = generate_ltx_freq_grid_np
freq_grid_generator = generate_ltx_freq_grid_float64
else:
freq_grid_generator = generate_ltx_freq_grid_pytorch
return precompute_ltx_freqs_cis(
@@ -2058,27 +2057,35 @@ class BasicAVTransformerBlock(torch.nn.Module):
"""STG keep-mask for self-attention.
Returns a per-sample multiplier (1 keep, 0 perturb) broadcastable
over ``values``. Only the configured ``stg_block_idx`` is perturbed.
over ``values``. Applied unconditionally: the eager caller
(``_process_transformer_blocks``) enforces that only the
configured ``stg_block_idx`` block receives a truthy flag.
Supports both the LTX-2.0 bool flag and an LTX-2.3 per-sample
tensor ``[B]`` (1/True == perturb). When skip_flag is False/0 the
multiplier is all ones, reproducing the un-skipped LTX-2.0 path.
"""
bsz = values.shape[0]
keep = torch.ones((bsz, ), device=values.device, dtype=values.dtype)
if self.idx == self.stg_block_idx:
if torch.is_tensor(skip_flag):
if skip_flag.ndim == 0:
perturb = skip_flag.reshape(1).expand(bsz)
else:
if skip_flag.shape[0] != bsz:
raise ValueError(
"Per-sample STG mask batch size mismatch: "
f"got {skip_flag.shape[0]}, expected {bsz}")
perturb = skip_flag.reshape(bsz)
keep = 1.0 - perturb.to(device=values.device,
dtype=values.dtype)
elif bool(skip_flag):
keep.zero_()
# The ``idx == stg_block_idx`` gate lives in the eager
# ``_process_transformer_blocks`` caller, not here: reading the
# per-instance ``self.idx`` inside this compiled forward forces
# torch.compile to specialize a distinct graph per block (48x),
# blowing the Dynamo recompile limit and defeating regional
# compilation. The caller only passes a truthy ``skip_flag`` to the
# configured STG block, so applying it unconditionally here is
# equivalent while keeping the graph identical across all 48 blocks.
if torch.is_tensor(skip_flag):
if skip_flag.ndim == 0:
perturb = skip_flag.reshape(1).expand(bsz)
else:
if skip_flag.shape[0] != bsz:
raise ValueError(
"Per-sample STG mask batch size mismatch: "
f"got {skip_flag.shape[0]}, expected {bsz}")
perturb = skip_flag.reshape(bsz)
keep = 1.0 - perturb.to(device=values.device, dtype=values.dtype)
elif bool(skip_flag):
keep.zero_()
return keep.view(bsz, *([1] * (values.ndim - 1)))
if run_vx:
@@ -2277,6 +2284,9 @@ class BasicAVTransformerBlock(torch.nn.Module):
1 + ascale_mlp) + ashift_mlp
ax = ax + self.audio_ff(ax_scaled) * agate_mlp
# Debug-only: reading ``self.idx`` (and .item() syncs) here means
# enabling this env var re-specializes the compiled graph per block,
# defeating the shared-graph regional compilation.
if os.getenv("LTX2_PIPELINE_DEBUG_LOG", "0") == "1":
video_sum = vx.float().sum().item() if vx is not None else 0.0
audio_sum = ax.float().sum().item() if ax is not None else 0.0
@@ -2656,8 +2666,10 @@ class LTXModel(torch.nn.Module):
skip_audio_self_attn_block_set = set(skip_audio_self_attn_blocks or [])
for idx, block in enumerate(self.transformer_blocks):
skip_v_sa = idx in skip_video_self_attn_block_set
skip_a_sa = idx in skip_audio_self_attn_block_set
# Preserve the original STG block condition in eager code so it does not introduce a block index guard in the compiled forward
is_stg_block = idx == block.stg_block_idx
skip_v_sa = is_stg_block and idx in skip_video_self_attn_block_set
skip_a_sa = is_stg_block and idx in skip_audio_self_attn_block_set
video, audio = block(
video=video,
audio=audio,
@@ -2768,6 +2780,7 @@ class LTX2Transformer3DModel(BaseDiT):
reverse_param_names_mapping = LTX2VideoConfig().reverse_param_names_mapping
lora_param_names_mapping = LTX2VideoConfig().lora_param_names_mapping
_fsdp_shard_conditions = LTX2VideoConfig()._fsdp_shard_conditions
_compile_conditions = LTX2VideoConfig()._compile_conditions
def __init__(self, config: LTX2VideoConfig, hf_config: dict[str, Any]):
super().__init__(config=config, hf_config=hf_config)
+570
View File
@@ -0,0 +1,570 @@
# SPDX-License-Identifier: Apache-2.0
"""FastVideo-native Z-Image transformer.
Z-Image attends over padded variable-length image/text streams and requires a
key-padding mask. FastVideo's distributed attention wrappers do not expose that
mask contract yet, so this implementation uses torch SDPA and is SP=1 only.
"""
from __future__ import annotations
import math
from typing import Any
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.nn.utils.rnn import pad_sequence
from fastvideo.configs.models.dits.zimage import ZImageDiTConfig
from fastvideo.distributed.parallel_state import get_sp_world_size, model_parallel_is_initialized
from fastvideo.layers.linear import ReplicatedLinear
from fastvideo.models.dits.base import BaseDiT
from fastvideo.platforms import AttentionBackendEnum
def _linear(layer: ReplicatedLinear, x: torch.Tensor) -> torch.Tensor:
return layer(x)[0]
def _prepare_attention_mask(attention_mask: torch.Tensor | None, dtype: torch.dtype) -> torch.Tensor | None:
if attention_mask is None:
return None
if attention_mask.ndim == 2:
attention_mask = attention_mask[:, None, None, :]
if attention_mask.dtype == torch.bool:
additive_mask = torch.zeros_like(attention_mask, dtype=dtype)
additive_mask.masked_fill_(~attention_mask, float("-inf"))
return additive_mask
return attention_mask
class TimestepEmbedder(nn.Module):
def __init__(
self,
out_size: int,
mid_size: int,
frequency_embedding_size: int,
max_period: int,
) -> None:
super().__init__()
self.mlp = nn.ModuleList([
ReplicatedLinear(frequency_embedding_size, mid_size, bias=True),
nn.SiLU(),
ReplicatedLinear(mid_size, out_size, bias=True),
])
self.frequency_embedding_size = frequency_embedding_size
self.max_period = max_period
@staticmethod
def timestep_embedding(t: torch.Tensor, dim: int, max_period: int) -> torch.Tensor:
with torch.amp.autocast("cuda", enabled=False):
half = dim // 2
freqs = torch.exp(
-math.log(max_period) * torch.arange(half, dtype=torch.float32, device=t.device) / half)
args = t[:, None].float() * freqs[None]
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
if dim % 2:
embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
return embedding
def forward(self, t: torch.Tensor) -> torch.Tensor:
t_freq = self.timestep_embedding(t, self.frequency_embedding_size, self.max_period)
weight_dtype = self.mlp[0].weight.dtype
if weight_dtype.is_floating_point:
t_freq = t_freq.to(weight_dtype)
return _linear(self.mlp[2], self.mlp[1](_linear(self.mlp[0], t_freq)))
class RMSNorm(nn.Module):
def __init__(self, dim: int, eps: float = 1e-5) -> None:
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.ones(dim))
def forward(self, x: torch.Tensor) -> torch.Tensor:
output = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
return output * self.weight
class FeedForward(nn.Module):
def __init__(self, dim: int, hidden_dim: int) -> None:
super().__init__()
self.w1 = ReplicatedLinear(dim, hidden_dim, bias=False)
self.w2 = ReplicatedLinear(hidden_dim, dim, bias=False)
self.w3 = ReplicatedLinear(dim, hidden_dim, bias=False)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return _linear(self.w2, F.silu(_linear(self.w1, x)) * _linear(self.w3, x))
def apply_rotary_emb(x_in: torch.Tensor, freqs_cis: torch.Tensor) -> torch.Tensor:
with torch.amp.autocast("cuda", enabled=False):
x = torch.view_as_complex(x_in.float().reshape(*x_in.shape[:-1], -1, 2))
x_out = torch.view_as_real(x * freqs_cis.unsqueeze(2)).flatten(3)
return x_out.type_as(x_in)
class ZImageAttention(nn.Module):
def __init__(self, dim: int, n_heads: int, n_kv_heads: int, qk_norm: bool = True, eps: float = 1e-5) -> None:
super().__init__()
self.n_heads = n_heads
self.n_kv_heads = n_kv_heads
self.head_dim = dim // n_heads
self.to_q = ReplicatedLinear(dim, n_heads * self.head_dim, bias=False)
self.to_k = ReplicatedLinear(dim, n_kv_heads * self.head_dim, bias=False)
self.to_v = ReplicatedLinear(dim, n_kv_heads * self.head_dim, bias=False)
self.to_out = nn.ModuleList([ReplicatedLinear(n_heads * self.head_dim, dim, bias=False)])
self.norm_q = RMSNorm(self.head_dim, eps=eps) if qk_norm else None
self.norm_k = RMSNorm(self.head_dim, eps=eps) if qk_norm else None
def forward(
self,
hidden_states: torch.Tensor,
attention_mask: torch.Tensor | None = None,
freqs_cis: torch.Tensor | None = None,
) -> torch.Tensor:
query = _linear(self.to_q, hidden_states).unflatten(-1, (self.n_heads, -1))
key = _linear(self.to_k, hidden_states).unflatten(-1, (self.n_kv_heads, -1))
value = _linear(self.to_v, hidden_states).unflatten(-1, (self.n_kv_heads, -1))
if self.norm_q is not None:
query = self.norm_q(query)
if self.norm_k is not None:
key = self.norm_k(key)
if freqs_cis is not None:
query = apply_rotary_emb(query, freqs_cis)
key = apply_rotary_emb(key, freqs_cis)
mask = _prepare_attention_mask(attention_mask, query.dtype)
hidden_states = F.scaled_dot_product_attention(
query.transpose(1, 2),
key.transpose(1, 2),
value.transpose(1, 2),
attn_mask=mask,
dropout_p=0.0,
is_causal=False,
).transpose(1, 2).contiguous()
return _linear(self.to_out[0], hidden_states.flatten(2, 3).to(query.dtype))
class ZImageTransformerBlock(nn.Module):
def __init__(
self,
layer_id: int,
dim: int,
n_heads: int,
n_kv_heads: int,
norm_eps: float,
qk_norm: bool,
adaln_embed_dim: int,
modulation: bool = True,
) -> None:
super().__init__()
self.dim = dim
self.head_dim = dim // n_heads
self.layer_id = layer_id
self.modulation = modulation
self.attention = ZImageAttention(dim, n_heads, n_kv_heads, qk_norm, norm_eps)
self.feed_forward = FeedForward(dim=dim, hidden_dim=int(dim / 3 * 8))
self.attention_norm1 = RMSNorm(dim, eps=norm_eps)
self.ffn_norm1 = RMSNorm(dim, eps=norm_eps)
self.attention_norm2 = RMSNorm(dim, eps=norm_eps)
self.ffn_norm2 = RMSNorm(dim, eps=norm_eps)
if modulation:
self.adaLN_modulation = nn.ModuleList(
[ReplicatedLinear(min(dim, adaln_embed_dim), 4 * dim, bias=True)])
def forward(
self,
x: torch.Tensor,
attn_mask: torch.Tensor,
freqs_cis: torch.Tensor,
adaln_input: torch.Tensor | None = None,
) -> torch.Tensor:
if self.modulation:
assert adaln_input is not None
scale_msa, gate_msa, scale_mlp, gate_mlp = _linear(
self.adaLN_modulation[0], adaln_input).unsqueeze(1).chunk(4, dim=2)
gate_msa, gate_mlp = gate_msa.tanh(), gate_mlp.tanh()
scale_msa, scale_mlp = 1.0 + scale_msa, 1.0 + scale_mlp
attn_out = self.attention(
self.attention_norm1(x) * scale_msa,
attention_mask=attn_mask,
freqs_cis=freqs_cis,
)
x = x + gate_msa * self.attention_norm2(attn_out)
x = x + gate_mlp * self.ffn_norm2(self.feed_forward(self.ffn_norm1(x) * scale_mlp))
else:
attn_out = self.attention(
self.attention_norm1(x),
attention_mask=attn_mask,
freqs_cis=freqs_cis,
)
x = x + self.attention_norm2(attn_out)
x = x + self.ffn_norm2(self.feed_forward(self.ffn_norm1(x)))
return x
class FinalLayer(nn.Module):
def __init__(self, hidden_size: int, out_channels: int, adaln_embed_dim: int) -> None:
super().__init__()
self.norm_final = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
self.linear = ReplicatedLinear(hidden_size, out_channels, bias=True)
self.adaLN_modulation = nn.ModuleList([
nn.SiLU(),
ReplicatedLinear(min(hidden_size, adaln_embed_dim), hidden_size, bias=True),
])
def forward(self, x: torch.Tensor, c: torch.Tensor) -> torch.Tensor:
scale = 1.0 + _linear(self.adaLN_modulation[1], self.adaLN_modulation[0](c))
return _linear(self.linear, self.norm_final(x) * scale.unsqueeze(1))
class RopeEmbedder:
def __init__(self, theta: float, axes_dims: tuple[int, ...], axes_lens: tuple[int, ...]) -> None:
if len(axes_dims) != len(axes_lens):
raise ValueError("RoPE axes require matching dimensions and lengths")
self.theta = theta
self.axes_dims = axes_dims
self.axes_lens = axes_lens
self.freqs_cis: list[torch.Tensor] | None = None
@staticmethod
def precompute_freqs_cis(dim: tuple[int, ...], end: tuple[int, ...], theta: float) -> list[torch.Tensor]:
with torch.device("cpu"):
freqs_cis = []
for axis_dim, axis_end in zip(dim, end):
freqs = 1.0 / (theta**(torch.arange(0, axis_dim, 2, dtype=torch.float64) / axis_dim))
timestep = torch.arange(axis_end, dtype=torch.float64)
angles = torch.outer(timestep, freqs).float()
freqs_cis.append(torch.polar(torch.ones_like(angles), angles).to(torch.complex64))
return freqs_cis
def __call__(self, ids: torch.Tensor) -> torch.Tensor:
if ids.ndim != 2 or ids.shape[-1] != len(self.axes_dims):
raise ValueError("RoPE ids must have shape [sequence, number_of_axes]")
if self.freqs_cis is None:
self.freqs_cis = [
freqs.to(ids.device)
for freqs in self.precompute_freqs_cis(self.axes_dims, self.axes_lens, self.theta)
]
elif self.freqs_cis[0].device != ids.device:
self.freqs_cis = [freqs.to(ids.device) for freqs in self.freqs_cis]
return torch.cat([self.freqs_cis[i][ids[:, i]] for i in range(len(self.axes_dims))], dim=-1)
class ZImageTransformer2DModel(BaseDiT):
_default_config = ZImageDiTConfig()
_fsdp_shard_conditions = _default_config.arch_config._fsdp_shard_conditions
_compile_conditions = _default_config.arch_config._compile_conditions
_supported_attention_backends = (AttentionBackendEnum.TORCH_SDPA, )
param_names_mapping = _default_config.arch_config.param_names_mapping
reverse_param_names_mapping = _default_config.arch_config.reverse_param_names_mapping
def __init__(self, config: ZImageDiTConfig, hf_config: dict[str, Any]) -> None:
super().__init__(config=config, hf_config=hf_config)
arch = config.arch_config
self.in_channels = arch.in_channels
self.out_channels = arch.in_channels
self.all_patch_size = tuple(arch.all_patch_size)
self.all_f_patch_size = tuple(arch.all_f_patch_size)
self.dim = arch.dim
self.n_heads = arch.n_heads
self.rope_theta = arch.rope_theta
self.t_scale = arch.t_scale
self.seq_multi_of = arch.seq_multi_of
self.hidden_size = arch.dim
self.num_attention_heads = arch.n_heads
self.num_channels_latents = arch.in_channels
self.all_x_embedder = nn.ModuleDict({
f"{patch_size}-{f_patch_size}": ReplicatedLinear(
f_patch_size * patch_size * patch_size * arch.in_channels, arch.dim, bias=True)
for patch_size, f_patch_size in zip(self.all_patch_size, self.all_f_patch_size)
})
self.all_final_layer = nn.ModuleDict({
f"{patch_size}-{f_patch_size}": FinalLayer(
arch.dim,
patch_size * patch_size * f_patch_size * self.out_channels,
arch.adaln_embed_dim,
)
for patch_size, f_patch_size in zip(self.all_patch_size, self.all_f_patch_size)
})
block_kwargs = {
"dim": arch.dim,
"n_heads": arch.n_heads,
"n_kv_heads": arch.n_kv_heads,
"norm_eps": arch.norm_eps,
"qk_norm": arch.qk_norm,
"adaln_embed_dim": arch.adaln_embed_dim,
}
self.noise_refiner = nn.ModuleList([
ZImageTransformerBlock(1000 + layer_id, modulation=True, **block_kwargs)
for layer_id in range(arch.n_refiner_layers)
])
self.context_refiner = nn.ModuleList([
ZImageTransformerBlock(layer_id, modulation=False, **block_kwargs)
for layer_id in range(arch.n_refiner_layers)
])
self.t_embedder = TimestepEmbedder(
min(arch.dim, arch.adaln_embed_dim),
mid_size=arch.timestep_mid_size,
frequency_embedding_size=arch.frequency_embedding_size,
max_period=arch.max_period,
)
self.cap_embedder = nn.ModuleList([
RMSNorm(arch.cap_feat_dim, eps=arch.norm_eps),
ReplicatedLinear(arch.cap_feat_dim, arch.dim, bias=True),
])
self.x_pad_token = nn.Parameter(torch.empty((1, arch.dim)))
self.cap_pad_token = nn.Parameter(torch.empty((1, arch.dim)))
self.layers = nn.ModuleList([
ZImageTransformerBlock(layer_id, modulation=True, **block_kwargs) for layer_id in range(arch.n_layers)
])
self.axes_dims = tuple(arch.axes_dims)
self.axes_lens = tuple(arch.axes_lens)
self.rope_embedder = RopeEmbedder(arch.rope_theta, self.axes_dims, self.axes_lens)
self.__post_init__()
def unpatchify(
self,
x: list[torch.Tensor],
size: list[tuple[int, int, int]],
patch_size: int,
f_patch_size: int,
) -> list[torch.Tensor]:
patch_height = patch_width = patch_size
patch_frames = f_patch_size
if len(x) != len(size):
raise ValueError("output batch and original sizes must have equal length")
for i, (frames, height, width) in enumerate(size):
original_length = (frames // patch_frames) * (height // patch_height) * (width // patch_width)
x[i] = (x[i][:original_length].view(
frames // patch_frames,
height // patch_height,
width // patch_width,
patch_frames,
patch_height,
patch_width,
self.out_channels,
).permute(6, 0, 3, 1, 4, 2, 5).reshape(self.out_channels, frames, height, width))
return x
@staticmethod
def create_coordinate_grid(
size: tuple[int, int, int],
start: tuple[int, int, int] | None = None,
device: torch.device | None = None,
) -> torch.Tensor:
start = start or (0, ) * len(size)
axes = [
torch.arange(axis_start, axis_start + span, dtype=torch.int32, device=device)
for axis_start, span in zip(start, size)
]
return torch.stack(torch.meshgrid(axes, indexing="ij"), dim=-1)
def patchify_and_embed(
self,
all_image: list[torch.Tensor],
all_cap_feats: list[torch.Tensor],
patch_size: int,
f_patch_size: int,
) -> tuple[
list[torch.Tensor],
list[torch.Tensor],
list[tuple[int, int, int]],
list[torch.Tensor],
list[torch.Tensor],
list[torch.Tensor],
list[torch.Tensor],
]:
patch_height = patch_width = patch_size
patch_frames = f_patch_size
device = all_image[0].device
image_out = []
image_sizes = []
image_pos_ids = []
image_pad_masks = []
cap_pos_ids = []
cap_pad_masks = []
cap_feats_out = []
for image, cap_feat in zip(all_image, all_cap_feats):
cap_length = len(cap_feat)
cap_padding = (-cap_length) % self.seq_multi_of
cap_pos_ids.append(
self.create_coordinate_grid(
(cap_length + cap_padding, 1, 1),
start=(1, 0, 0),
device=device,
).flatten(0, 2))
cap_pad_masks.append(
torch.cat([
torch.zeros(cap_length, dtype=torch.bool, device=device),
torch.ones(cap_padding, dtype=torch.bool, device=device),
]) if cap_padding else torch.zeros(cap_length, dtype=torch.bool, device=device))
cap_feats_out.append(
torch.cat([cap_feat, cap_feat[-1:].repeat(cap_padding, 1)]) if cap_padding else cap_feat)
channels, frames, height, width = image.size()
image_sizes.append((frames, height, width))
frame_tokens, height_tokens, width_tokens = (
frames // patch_frames,
height // patch_height,
width // patch_width,
)
image = image.view(
channels,
frame_tokens,
patch_frames,
height_tokens,
patch_height,
width_tokens,
patch_width,
)
image = image.permute(1, 3, 5, 2, 4, 6, 0).reshape(
frame_tokens * height_tokens * width_tokens,
patch_frames * patch_height * patch_width * channels,
)
image_length = len(image)
image_padding = (-image_length) % self.seq_multi_of
original_pos_ids = self.create_coordinate_grid(
(frame_tokens, height_tokens, width_tokens),
start=(cap_length + cap_padding + 1, 0, 0),
device=device,
).flatten(0, 2)
if image_padding:
padding_pos_ids = self.create_coordinate_grid((1, 1, 1), device=device).flatten(0, 2).repeat(
image_padding, 1)
image_pos_ids.append(torch.cat([original_pos_ids, padding_pos_ids]))
else:
image_pos_ids.append(original_pos_ids)
image_pad_masks.append(
torch.cat([
torch.zeros(image_length, dtype=torch.bool, device=device),
torch.ones(image_padding, dtype=torch.bool, device=device),
]) if image_padding else torch.zeros(image_length, dtype=torch.bool, device=device))
image_out.append(
torch.cat([image, image[-1:].repeat(image_padding, 1)]) if image_padding else image)
return (
image_out,
cap_feats_out,
image_sizes,
image_pos_ids,
cap_pos_ids,
image_pad_masks,
cap_pad_masks,
)
@staticmethod
def _attention_mask(lengths: list[int], device: torch.device) -> torch.Tensor:
mask = torch.zeros((len(lengths), max(lengths)), dtype=torch.bool, device=device)
for i, length in enumerate(lengths):
mask[i, :length] = True
return mask
def forward(
self,
hidden_states: torch.Tensor | list[torch.Tensor],
encoder_hidden_states: torch.Tensor | list[torch.Tensor],
timestep: torch.LongTensor,
encoder_hidden_states_image: torch.Tensor | list[torch.Tensor] | None = None,
guidance=None,
patch_size: int = 2,
f_patch_size: int = 1,
**kwargs,
) -> tuple[list[torch.Tensor], dict]:
del encoder_hidden_states_image, guidance, kwargs
if model_parallel_is_initialized() and get_sp_world_size() != 1:
raise NotImplementedError(
"Z-Image masked SDPA does not support sequence parallelism; run with sp_size=1")
if patch_size not in self.all_patch_size or f_patch_size not in self.all_f_patch_size:
raise ValueError(f"unsupported patch sizes: spatial={patch_size}, temporal={f_patch_size}")
if isinstance(hidden_states, torch.Tensor):
hidden_states = list(hidden_states.unbind(0))
if isinstance(encoder_hidden_states, torch.Tensor):
encoder_hidden_states = list(encoder_hidden_states.unbind(0))
device = hidden_states[0].device
timestep_embedding = self.t_embedder(timestep * self.t_scale)
(
hidden_states,
encoder_hidden_states,
image_sizes,
image_pos_ids,
cap_pos_ids,
image_inner_pad_masks,
cap_inner_pad_masks,
) = self.patchify_and_embed(hidden_states, encoder_hidden_states, patch_size, f_patch_size)
image_lengths = [len(item) for item in hidden_states]
if not all(length % self.seq_multi_of == 0 for length in image_lengths):
raise ValueError("padded image sequence lengths must be aligned")
hidden_states = torch.cat(hidden_states)
hidden_states = _linear(self.all_x_embedder[f"{patch_size}-{f_patch_size}"], hidden_states)
adaln_input = timestep_embedding.type_as(hidden_states)
hidden_states[torch.cat(image_inner_pad_masks)] = self.x_pad_token
hidden_states = list(hidden_states.split(image_lengths))
image_freqs_cis = list(
self.rope_embedder(torch.cat(image_pos_ids)).split([len(item) for item in image_pos_ids]))
hidden_states = pad_sequence(hidden_states, batch_first=True, padding_value=0.0)
image_freqs_cis = pad_sequence(image_freqs_cis, batch_first=True, padding_value=0.0)
image_freqs_cis = image_freqs_cis[:, :hidden_states.shape[1]]
image_attn_mask = self._attention_mask(image_lengths, device)
for layer in self.noise_refiner:
hidden_states = layer(hidden_states, image_attn_mask, image_freqs_cis, adaln_input)
cap_lengths = [len(item) for item in encoder_hidden_states]
if not all(length % self.seq_multi_of == 0 for length in cap_lengths):
raise ValueError("padded caption sequence lengths must be aligned")
encoder_hidden_states = torch.cat(encoder_hidden_states)
encoder_hidden_states = _linear(self.cap_embedder[1], self.cap_embedder[0](encoder_hidden_states))
encoder_hidden_states[torch.cat(cap_inner_pad_masks)] = self.cap_pad_token
encoder_hidden_states = list(encoder_hidden_states.split(cap_lengths))
cap_freqs_cis = list(self.rope_embedder(torch.cat(cap_pos_ids)).split([len(item) for item in cap_pos_ids]))
encoder_hidden_states = pad_sequence(encoder_hidden_states, batch_first=True, padding_value=0.0)
cap_freqs_cis = pad_sequence(cap_freqs_cis, batch_first=True, padding_value=0.0)
cap_freqs_cis = cap_freqs_cis[:, :encoder_hidden_states.shape[1]]
cap_attn_mask = self._attention_mask(cap_lengths, device)
for layer in self.context_refiner:
encoder_hidden_states = layer(encoder_hidden_states, cap_attn_mask, cap_freqs_cis)
unified = []
unified_freqs_cis = []
for i, (image_length, cap_length) in enumerate(zip(image_lengths, cap_lengths)):
unified.append(
torch.cat([hidden_states[i][:image_length], encoder_hidden_states[i][:cap_length]]))
unified_freqs_cis.append(
torch.cat([image_freqs_cis[i][:image_length], cap_freqs_cis[i][:cap_length]]))
unified_lengths = [image_length + cap_length for image_length, cap_length in zip(image_lengths, cap_lengths)]
unified = pad_sequence(unified, batch_first=True, padding_value=0.0)
unified_freqs_cis = pad_sequence(unified_freqs_cis, batch_first=True, padding_value=0.0)
unified_attn_mask = self._attention_mask(unified_lengths, device)
for layer in self.layers:
unified = layer(unified, unified_attn_mask, unified_freqs_cis, adaln_input)
unified = self.all_final_layer[f"{patch_size}-{f_patch_size}"](unified, adaln_input)
outputs = self.unpatchify(list(unified.unbind(0)), image_sizes, patch_size, f_patch_size)
return outputs, {}
EntryClass = ZImageTransformer2DModel
+2 -2
View File
@@ -15,7 +15,7 @@ from fastvideo.models.dits.ltx2 import (
FeedForward,
LTXRopeType,
apply_ltx_rotary_emb,
generate_ltx_freq_grid_np,
generate_ltx_freq_grid_float64,
generate_ltx_freq_grid_pytorch,
precompute_ltx_freqs_cis,
)
@@ -330,7 +330,7 @@ class Embeddings1DConnector(nn.Module):
)
indices_grid = indices_grid[None, None, :]
freq_grid_generator = (
generate_ltx_freq_grid_np
generate_ltx_freq_grid_float64
if self.double_precision_rope
else generate_ltx_freq_grid_pytorch
)
+221
View File
@@ -0,0 +1,221 @@
# SPDX-License-Identifier: Apache-2.0
"""Native LingBot-Video Qwen3-VL language model for text-only conditioning."""
from collections.abc import Iterable
from typing import Any
import torch
from torch import nn
from fastvideo.configs.models.encoders import BaseEncoderOutput
from fastvideo.layers.layernorm import RMSNorm
from fastvideo.layers.vocab_parallel_embedding import VocabParallelEmbedding
from fastvideo.models.encoders.base import TextEncoder
from fastvideo.models.encoders.qwen3 import (
Qwen3Attention,
Qwen3DecoderLayer,
Qwen3ForCausalLM,
Qwen3MLP,
)
class LingBotVideoQwen3VLAttention(Qwen3Attention):
"""Qwen3-VL attention with the official masked repeat-K/V SDPA path."""
def _apply_qwen3_vl_rope(
self,
positions: torch.Tensor,
query: torch.Tensor,
key: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Apply NeoX RoPE with Qwen3-VL's input-dtype multiply-add ordering."""
flat_positions = positions.flatten()
cos_sin = self.rotary_emb.cos_sin_cache.index_select(0, flat_positions)
cos_half, sin_half = cos_sin.chunk(2, dim=-1)
cos = torch.cat((cos_half, cos_half), dim=-1).to(query.dtype)
sin = torch.cat((sin_half, sin_half), dim=-1).to(query.dtype)
if flat_positions.numel() == query.shape[1]:
cos = cos.view(1, query.shape[1], 1, self.head_dim)
sin = sin.view(1, query.shape[1], 1, self.head_dim)
else:
cos = cos.view(*query.shape[:2], 1, self.head_dim)
sin = sin.view(*query.shape[:2], 1, self.head_dim)
def rotate(tensor: torch.Tensor) -> torch.Tensor:
first, second = tensor.chunk(2, dim=-1)
rotated = torch.cat((-second, first), dim=-1)
return tensor * cos + rotated * sin
return rotate(query), rotate(key)
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
attention_mask: torch.Tensor | None = None,
) -> torch.Tensor:
"""Apply fused projections, QK norm, RoPE, and causal grouped attention."""
qkv, _ = self.qkv_proj(hidden_states)
query, key, value = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
batch_size, sequence_length = query.shape[:2]
query = query.reshape(batch_size, sequence_length, self.num_heads, self.head_dim)
key = key.reshape(batch_size, sequence_length, self.num_kv_heads, self.head_dim)
value = value.reshape(batch_size, sequence_length, self.num_kv_heads, self.head_dim)
query = self.q_norm(query)
key = self.k_norm(key)
query, key = self._apply_qwen3_vl_rope(positions, query, key)
no_padding = attention_mask is None
if no_padding:
attention_output = torch.nn.functional.scaled_dot_product_attention(
query.transpose(1, 2),
key.transpose(1, 2),
value.transpose(1, 2),
dropout_p=0.0,
is_causal=sequence_length > 1,
scale=self.scaling,
enable_gqa=self.num_heads != self.num_kv_heads,
).transpose(1, 2)
else:
groups = self.num_heads // self.num_kv_heads
key = (key[:, :, :, None, :].expand(-1, -1, -1, groups, -1).reshape(batch_size, sequence_length,
self.num_heads, self.head_dim))
value = (value[:, :, :, None, :].expand(-1, -1, -1, groups, -1).reshape(batch_size, sequence_length,
self.num_heads, self.head_dim))
causal_mask = torch.ones(sequence_length, sequence_length, device=query.device, dtype=torch.bool).tril()
key_mask = attention_mask.to(device=query.device, dtype=torch.bool)
sdpa_mask = causal_mask[None, None, :, :] & key_mask[:, None, None, :]
attention_output = torch.nn.functional.scaled_dot_product_attention(
query.transpose(1, 2),
key.transpose(1, 2),
value.transpose(1, 2),
attn_mask=sdpa_mask,
dropout_p=0.0,
is_causal=False,
scale=self.scaling,
).transpose(1, 2)
output, _ = self.o_proj(attention_output.reshape(batch_size, sequence_length, -1))
return output
class LingBotVideoQwen3VLDecoderLayer(Qwen3DecoderLayer):
"""Qwen3-VL decoder layer with explicit official residual rounding order."""
def __init__(self, config: Any, prefix: str) -> None:
"""Build the final Qwen3-VL attention once to avoid orphan parameters."""
nn.Module.__init__(self)
self.hidden_size = config.hidden_size
quant_config = getattr(config, "quant_config", None)
self.self_attn = LingBotVideoQwen3VLAttention(
config=config,
hidden_size=self.hidden_size,
num_heads=config.num_attention_heads,
num_kv_heads=config.num_key_value_heads,
rope_theta=config.rope_theta,
rope_scaling=config.rope_scaling,
max_position_embeddings=config.max_position_embeddings,
quant_config=quant_config,
bias=config.attention_bias,
prefix=f"{prefix}.self_attn",
)
self.mlp = Qwen3MLP(
hidden_size=self.hidden_size,
intermediate_size=config.intermediate_size,
hidden_act=config.hidden_act,
quant_config=quant_config,
bias=getattr(config, "mlp_bias", False),
prefix=f"{prefix}.mlp",
)
self.input_layernorm = RMSNorm(self.hidden_size, eps=config.rms_norm_eps)
self.post_attention_layernorm = RMSNorm(self.hidden_size, eps=config.rms_norm_eps)
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
attention_mask: torch.Tensor | None = None,
) -> torch.Tensor:
"""Run attention and MLP with each residual sum rounded before normalization."""
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
hidden_states = self.self_attn(positions, hidden_states, attention_mask)
hidden_states = residual + hidden_states
residual = hidden_states
hidden_states = self.post_attention_layernorm(hidden_states)
hidden_states = self.mlp(hidden_states)
return residual + hidden_states
class LingBotVideoQwen3VLTextModel(Qwen3ForCausalLM):
"""Load the Qwen3-VL language-model subset without its vision tower or LM head."""
supports_hf_from_pretrained = False
def __init__(self, config) -> None:
"""Construct the exact Qwen3-VL module graph without replacing base layers."""
TextEncoder.__init__(self, config)
self.quant_config = getattr(config, "quant_config", None)
if getattr(config, "lora_config", None) is not None:
max_loras = getattr(config.lora_config, "max_loras", 1)
lora_vocab_size = getattr(config.lora_config, "lora_extra_vocab_size", 1)
lora_vocab = lora_vocab_size * max_loras
else:
lora_vocab = 0
self.vocab_size = config.vocab_size + lora_vocab
self.org_vocab_size = config.vocab_size
self.embed_tokens = VocabParallelEmbedding(
self.vocab_size,
config.hidden_size,
org_num_embeddings=config.vocab_size,
quant_config=self.quant_config,
)
self.layers = nn.ModuleList(
LingBotVideoQwen3VLDecoderLayer(config, prefix=f"{config.prefix}.layers.{index}")
for index in range(config.num_hidden_layers))
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
def forward(
self,
input_ids: torch.Tensor | None = None,
position_ids: torch.Tensor | None = None,
attention_mask: torch.Tensor | None = None,
inputs_embeds: torch.Tensor | None = None,
output_hidden_states: bool | None = None,
**kwargs: Any,
) -> BaseEncoderOutput:
"""Run explicit Qwen3-VL layers and return the requested hidden-state tuple."""
del kwargs
output_hidden_states = (output_hidden_states
if output_hidden_states is not None else self.config.output_hidden_states)
if inputs_embeds is None:
if input_ids is None:
raise ValueError("input_ids or inputs_embeds is required")
hidden_states = self.get_input_embeddings(input_ids)
else:
hidden_states = inputs_embeds
if position_ids is None:
position_ids = torch.arange(hidden_states.shape[1], device=hidden_states.device).unsqueeze(0)
if attention_mask is not None and bool(attention_mask.to(torch.bool).all()):
attention_mask = None
all_hidden_states: tuple[torch.Tensor, ...] | None = () if output_hidden_states else None
for layer in self.layers:
if all_hidden_states is not None:
all_hidden_states += (hidden_states, )
hidden_states = layer(position_ids, hidden_states, attention_mask)
hidden_states = self.norm(hidden_states)
if all_hidden_states is not None:
all_hidden_states += (hidden_states, )
return BaseEncoderOutput(
last_hidden_state=hidden_states,
hidden_states=all_hidden_states,
)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
"""Accept either official compound keys or converted native keys."""
prefix = "model.language_model."
language_weights = ((name[len(prefix):] if name.startswith(prefix) else name, tensor)
for name, tensor in weights
if name.startswith(prefix) or not name.startswith(("model.", "lm_head.")))
return super().load_weights(language_weights)
EntryClass = LingBotVideoQwen3VLTextModel
@@ -0,0 +1,269 @@
# SPDX-License-Identifier: Apache-2.0
"""LingBot World 2 UMT5 encoder with the released checkpoint's module names."""
from collections.abc import Iterable
import html
import math
import string
import ftfy
import torch
import torch.nn as nn
import torch.nn.functional as F
from fastvideo.configs.models.encoders import BaseEncoderOutput
from fastvideo.configs.models.encoders.lingbotworld2_t5 import LingBotWorld2UMT5Config
from fastvideo.models.encoders.base import TextEncoder
from fastvideo.models.loader.weight_utils import default_weight_loader
def basic_clean(text: str) -> str:
"""Apply LingBot World 2's ftfy/html cleanup before tokenization."""
text = ftfy.fix_text(text)
text = html.unescape(html.unescape(text))
return text.strip()
def whitespace_clean(text: str) -> str:
"""Collapse all whitespace runs to single spaces."""
return " ".join(text.split())
def canonicalize(text: str, keep_punctuation_exact_string: str | None = None) -> str:
"""Normalize prompts with LingBot World 2's optional punctuation handling."""
text = text.replace("_", " ")
if keep_punctuation_exact_string:
text = keep_punctuation_exact_string.join(
part.translate(str.maketrans("", "", string.punctuation))
for part in text.split(keep_punctuation_exact_string)
)
else:
text = text.translate(str.maketrans("", "", string.punctuation))
return " ".join(text.lower().split())
def lingbotworld2_whitespace_preprocess(prompt: str) -> str:
"""Match the LingBot World 2 source tokenizer's `clean='whitespace'` behavior."""
return whitespace_clean(basic_clean(prompt))
class GELU(nn.Module):
"""T5 gated GELU approximation used by the source checkpoint."""
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Apply the tanh GELU approximation."""
return 0.5 * x * (1.0 + torch.tanh(math.sqrt(2.0 / math.pi) * (x + 0.044715 * torch.pow(x, 3.0))))
class T5LayerNorm(nn.Module):
"""T5 RMS-style layer norm with source-compatible parameter name."""
def __init__(self, dim: int, eps: float = 1e-6):
super().__init__()
self.dim = dim
self.eps = eps
self.weight = nn.Parameter(torch.ones(dim))
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Normalize in fp32 and apply the learned scale."""
x = x * torch.rsqrt(x.float().pow(2).mean(dim=-1, keepdim=True) + self.eps)
if self.weight.dtype in (torch.float16, torch.bfloat16):
x = x.type_as(self.weight)
return self.weight * x
class T5Attention(nn.Module):
"""LingBot World 2 source T5 attention block."""
def __init__(self, dim: int, dim_attn: int, num_heads: int, dropout: float = 0.1):
super().__init__()
assert dim_attn % num_heads == 0
self.dim = dim
self.dim_attn = dim_attn
self.num_heads = num_heads
self.head_dim = dim_attn // num_heads
self.q = nn.Linear(dim, dim_attn, bias=False)
self.k = nn.Linear(dim, dim_attn, bias=False)
self.v = nn.Linear(dim, dim_attn, bias=False)
self.o = nn.Linear(dim_attn, dim, bias=False)
self.dropout = nn.Dropout(dropout)
def forward(
self,
x: torch.Tensor,
context: torch.Tensor | None = None,
mask: torch.Tensor | None = None,
pos_bias: torch.Tensor | None = None,
) -> torch.Tensor:
"""Project QKV, add relative bias/mask, and return attended states."""
context = x if context is None else context
b, n, c = x.size(0), self.num_heads, self.head_dim
q = self.q(x).view(b, -1, n, c)
k = self.k(context).view(b, -1, n, c)
v = self.v(context).view(b, -1, n, c)
attn_bias = x.new_zeros(b, n, q.size(1), k.size(1))
if pos_bias is not None:
attn_bias += pos_bias
if mask is not None:
assert mask.ndim in (2, 3)
mask = mask.view(b, 1, 1, -1) if mask.ndim == 2 else mask.unsqueeze(1)
attn_bias.masked_fill_(mask == 0, torch.finfo(x.dtype).min)
attn = torch.einsum("binc,bjnc->bnij", q, k) + attn_bias
attn = F.softmax(attn.float(), dim=-1).type_as(attn)
x = torch.einsum("bnij,bjnc->binc", attn, v)
return self.dropout(self.o(x.reshape(b, -1, n * c)))
class T5FeedForward(nn.Module):
"""LingBot World 2 source T5 gated feed-forward block."""
def __init__(self, dim: int, dim_ffn: int, dropout: float = 0.1):
super().__init__()
self.dim = dim
self.dim_ffn = dim_ffn
self.gate = nn.Sequential(nn.Linear(dim, dim_ffn, bias=False), GELU())
self.fc1 = nn.Linear(dim, dim_ffn, bias=False)
self.fc2 = nn.Linear(dim_ffn, dim, bias=False)
self.dropout = nn.Dropout(dropout)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Apply the gated feed-forward projection."""
x = self.fc1(x) * self.gate(x)
x = self.dropout(x)
x = self.fc2(x)
return self.dropout(x)
class T5RelativeEmbedding(nn.Module):
"""Per-block relative position embedding used by LingBot World 2 UMT5."""
def __init__(self, num_buckets: int, num_heads: int, bidirectional: bool, max_dist: int = 128):
super().__init__()
self.num_buckets = num_buckets
self.num_heads = num_heads
self.bidirectional = bidirectional
self.max_dist = max_dist
self.embedding = nn.Embedding(num_buckets, num_heads)
def forward(self, lq: int, lk: int) -> torch.Tensor:
"""Build a relative-position bias tensor for attention logits."""
device = self.embedding.weight.device
rel_pos = torch.arange(lk, device=device).unsqueeze(0) - torch.arange(lq, device=device).unsqueeze(1)
rel_pos = self._relative_position_bucket(rel_pos)
rel_pos_embeds = self.embedding(rel_pos)
return rel_pos_embeds.permute(2, 0, 1).unsqueeze(0).contiguous()
def _relative_position_bucket(self, rel_pos: torch.Tensor) -> torch.Tensor:
"""Map token offsets to T5 relative-position buckets."""
if self.bidirectional:
num_buckets = self.num_buckets // 2
rel_buckets = (rel_pos > 0).long() * num_buckets
rel_pos = torch.abs(rel_pos)
else:
num_buckets = self.num_buckets
rel_buckets = 0
rel_pos = -torch.min(rel_pos, torch.zeros_like(rel_pos))
max_exact = num_buckets // 2
rel_pos_large = max_exact + (
torch.log(rel_pos.float() / max_exact) / math.log(self.max_dist / max_exact) * (num_buckets - max_exact)
).long()
rel_pos_large = torch.min(rel_pos_large, torch.full_like(rel_pos_large, num_buckets - 1))
rel_buckets += torch.where(rel_pos < max_exact, rel_pos, rel_pos_large)
return rel_buckets
class T5SelfAttention(nn.Module):
"""One source-compatible UMT5 encoder block."""
def __init__(
self,
dim: int,
dim_attn: int,
dim_ffn: int,
num_heads: int,
num_buckets: int,
shared_pos: bool = False,
dropout: float = 0.1,
):
super().__init__()
self.norm1 = T5LayerNorm(dim)
self.attn = T5Attention(dim, dim_attn, num_heads, dropout)
self.norm2 = T5LayerNorm(dim)
self.ffn = T5FeedForward(dim, dim_ffn, dropout)
self.pos_embedding = None if shared_pos else T5RelativeEmbedding(num_buckets, num_heads, bidirectional=True)
def forward(self, x: torch.Tensor, mask: torch.Tensor | None = None, pos_bias: torch.Tensor | None = None) -> torch.Tensor:
"""Run self-attention and feed-forward residual updates."""
e = pos_bias if self.pos_embedding is None else self.pos_embedding(x.size(1), x.size(1))
x = self._fp16_clamp(x + self.attn(self.norm1(x), mask=mask, pos_bias=e))
return self._fp16_clamp(x + self.ffn(self.norm2(x)))
@staticmethod
def _fp16_clamp(x: torch.Tensor) -> torch.Tensor:
if x.dtype == torch.float16 and torch.isinf(x).any():
clamp = torch.finfo(x.dtype).max - 1000
x = torch.clamp(x, min=-clamp, max=clamp)
return x
class LingBotWorld2T5EncoderModel(TextEncoder):
"""FastVideo-native LingBot World 2 UMT5 encoder for the released `.pth` weights."""
fall_back_to_pt_during_load = True
allow_patterns_overrides = ["*.pt"]
def __init__(self, config: LingBotWorld2UMT5Config, prefix: str = ""):
super().__init__(config)
del prefix
arch = config.arch_config
self.token_embedding = nn.Embedding(arch.vocab_size, arch.dim)
self.dropout = nn.Dropout(arch.dropout)
self.blocks = nn.ModuleList(
[
T5SelfAttention(
arch.dim,
arch.dim_attn,
arch.dim_ffn,
arch.num_heads,
arch.num_buckets,
shared_pos=False,
dropout=arch.dropout,
)
for _ in range(arch.num_layers)
]
)
self.norm = T5LayerNorm(arch.dim)
def forward(
self,
input_ids: torch.Tensor | None,
position_ids: torch.Tensor | None = None,
attention_mask: torch.Tensor | None = None,
inputs_embeds: torch.Tensor | None = None,
output_hidden_states: bool | None = None,
**kwargs,
) -> BaseEncoderOutput:
"""Encode token ids and return source-compatible hidden states."""
del position_ids, inputs_embeds, output_hidden_states, kwargs
assert input_ids is not None
x = self.dropout(self.token_embedding(input_ids))
for block in self.blocks:
x = block(x, attention_mask)
x = self.dropout(self.norm(x))
return BaseEncoderOutput(last_hidden_state=x, attention_mask=attention_mask)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
"""Load source `.pth` weights whose names already match this module."""
params_dict = dict(self.named_parameters())
loaded_params: set[str] = set()
for name, loaded_weight in weights:
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
loaded_params.add(name)
return loaded_params
EntryClass = LingBotWorld2T5EncoderModel
+79 -29
View File
@@ -327,17 +327,15 @@ class Qwen3ForCausalLM(TextEncoder):
dtype: torch.dtype,
device: torch.device,
) -> nn.Module:
from transformers import AutoModelForCausalLM
from transformers import AutoModel
if device.type == "cpu" and torch.cuda.is_available():
from fastvideo.distributed import get_local_torch_device
device = get_local_torch_device()
return AutoModelForCausalLM.from_pretrained(
# FastVideo uses Qwen3 only as a text encoder. Loading the body avoids
# materializing an unused LM head and full-vocabulary logits, including
# for checkpoints whose metadata names Qwen3ForCausalLM.
return AutoModel.from_pretrained(
model_path,
local_files_only=True,
torch_dtype=dtype,
dtype=dtype,
low_cpu_mem_usage=True,
).eval().to(device)
@@ -368,9 +366,18 @@ class Qwen3ForCausalLM(TextEncoder):
residual = None
if position_ids is None:
# Expand to [batch_size, seq_len]: the rotary layer flattens
# positions to ``num_tokens`` and reshapes q/k to
# ``(num_tokens, -1, head_dim)``. A bare [1, seq_len] only matches
# ``num_tokens`` when batch_size == 1; for batched inputs it folds
# the batch dim into the head dim and misaligns RoPE. Expanding to
# ``batch_size * seq_len`` tokens keeps the layout correct.
position_ids = torch.arange(
0, hidden_states.shape[1], device=hidden_states.device
).unsqueeze(0)
0,
hidden_states.shape[1],
device=hidden_states.device,
dtype=torch.long,
).unsqueeze(0).expand(hidden_states.shape[0], -1)
all_hidden_states: tuple[Any, ...] | None = (
() if output_hidden_states else None
@@ -405,6 +412,20 @@ class Qwen3ForCausalLM(TextEncoder):
) -> set[str]:
params_dict = dict(self.named_parameters())
loaded_params: set[str] = set()
stacked_params_mapping = self.config.arch_config.stacked_params_mapping
# A fused destination is initialized either by one already-fused tensor
# or after every split source projection has been loaded. Include
# auxiliary quantization parameters (for example scale_weight) rather
# than limiting completeness checks to weight/bias tensors.
expected_stacked_shards = {
(name, shard_id)
for name in params_dict
for param_name, _, shard_id in stacked_params_mapping
if param_name in name
}
fused_param_names = {name for name, _ in expected_stacked_shards}
loaded_stacked_shards: set[tuple[str, str | int]] = set()
loaded_fused_params: set[str] = set()
for name, loaded_weight in weights:
if name.startswith("model."):
@@ -423,37 +444,66 @@ class Qwen3ForCausalLM(TextEncoder):
continue
name = kv_scale_name
for (
param_name,
weight_name,
shard_id,
) in self.config.arch_config.stacked_params_mapping:
matched_stacked_param = False
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name not in name:
continue
name = name.replace(weight_name, param_name)
matched_stacked_param = True
target_name = name.replace(weight_name, param_name)
if name.endswith(".bias") and name not in params_dict:
continue
if target_name.endswith(".bias") and target_name not in params_dict:
break
if name not in params_dict:
continue
if target_name not in params_dict:
break
param = params_dict[name]
param = params_dict[target_name]
weight_loader = param.weight_loader
weight_loader(param, loaded_weight, shard_id)
loaded_params.add(target_name)
loaded_stacked_shards.add((target_name, shard_id))
break
if matched_stacked_param:
continue
if name.endswith(".bias") and name not in params_dict:
continue
if name not in params_dict:
continue
param = params_dict[name]
if name in fused_param_names and name.endswith(".scale_weight"):
# Merged scale loaders interpret a missing shard id as shard 0.
# An exact fused key is already a complete vector, so copy it
# atomically and retain the default loader's shape validation.
default_weight_loader(param, loaded_weight)
else:
if name.endswith(".bias") and name not in params_dict:
continue
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
loaded_params.add(name)
if name in fused_param_names:
loaded_fused_params.add(name)
required_split_shards = {
(name, shard_id)
for name, shard_id in expected_stacked_shards
if name not in loaded_fused_params
}
missing_stacked_shards = required_split_shards - loaded_stacked_shards
if missing_stacked_shards:
formatted_missing = ", ".join(
f"{name}[{shard_id}]"
for name, shard_id in sorted(
missing_stacked_shards,
key=lambda item: (item[0], str(item[1])),
)
)
raise ValueError(
"Missing required stacked checkpoint shards: "
f"{formatted_missing}"
)
return loaded_params
+33 -11
View File
@@ -391,18 +391,22 @@ class TextEncoderLoader(ComponentLoader):
model_config.quant_config = quant_cls()
with set_default_torch_dtype(PRECISION_TO_TYPE[dtype]):
with target_device:
architectures = getattr(model_config, "architectures", [])
model_cls, _ = ModelRegistry.resolve_model_cls(architectures)
if getattr(model_cls, "supports_hf_from_pretrained", False):
model = model_cls.from_pretrained_local( # type: ignore[attr-defined]
model_path,
model_config, # type: ignore[arg-type]
dtype=PRECISION_TO_TYPE[dtype],
device=target_device,
)
return model.eval()
architectures = getattr(model_config, "architectures", [])
model_cls, _ = ModelRegistry.resolve_model_cls(architectures)
if getattr(model_cls, "supports_hf_from_pretrained", False):
model = model_cls.from_pretrained_local( # type: ignore[attr-defined]
model_path,
model_config, # type: ignore[arg-type]
dtype=PRECISION_TO_TYPE[dtype],
device=target_device,
)
# HF passthrough encoders return before FastVideo's FSDP
# wrapping path, so the text stage needs their placement to
# put token tensors on the same device.
model._fastvideo_input_device = target_device
return model.eval()
with target_device:
model = model_cls(model_config) # type: ignore
weights_to_load = {name for name, _ in model.named_parameters()}
@@ -820,6 +824,24 @@ class VAELoader(ComponentLoader):
vae.load_state_dict(sd, strict=False)
return vae.eval()
if class_name == "LingBotWorld2WanVAE":
dtype = PRECISION_TO_TYPE[fastvideo_args.pipeline_config.vae_precision]
config.pop("_class_name", None)
vae_config = fastvideo_args.pipeline_config.vae_config
vae_config.update_model_arch(config)
vae_cls, _ = ModelRegistry.resolve_model_cls(class_name)
weight_path = os.path.join(model_path, "Wan2.1_VAE.pth")
if not os.path.exists(weight_path):
raise FileNotFoundError(
f"Missing LingBot World 2 VAE weights: {weight_path}"
)
vae = vae_cls(
vae_config,
checkpoint_path=weight_path,
dtype=dtype,
).to(target_device)
return vae.eval()
# LTX-2 uses CausalVideoAutoencoder with nested "vae" config
if class_name == "CausalVideoAutoencoder" and "vae" in config:
vae_cls, _ = ModelRegistry.resolve_model_cls(class_name)
+94 -68
View File
@@ -15,13 +15,11 @@ import torch
from torch import nn
from torch.distributed import DeviceMesh, init_device_mesh
from torch.distributed._tensor import distribute_tensor
from torch.distributed.fsdp import (CPUOffloadPolicy, FSDPModule,
MixedPrecisionPolicy, fully_shard)
from torch.distributed.fsdp import (CPUOffloadPolicy, FSDPModule, MixedPrecisionPolicy, fully_shard)
from torch.nn.modules.module import _IncompatibleKeys
from fastvideo.logger import init_logger
from fastvideo.models.loader.utils import (get_param_names_mapping,
hf_to_custom_state_dict)
from fastvideo.models.loader.utils import (get_param_names_mapping, hf_to_custom_state_dict)
from fastvideo.models.loader.weight_utils import safetensors_weights_iterator
from fastvideo.utils import set_mixed_precision_policy, is_pin_memory_available
@@ -43,13 +41,16 @@ def _maybe_quantize_model(model: nn.Module) -> None:
"""
# Defer imports: these modules pull in heavy symbols at module-load time.
from fastvideo.layers.quantization.nvfp4_config import (
NVFP4QuantizeMethod, convert_model_to_nvfp4,
NVFP4QuantizeMethod,
convert_model_to_nvfp4,
)
from fastvideo.layers.quantization.nvfp4_qat_config import (
NVFP4QATQuantizeMethod, convert_model_to_fp4,
NVFP4QATQuantizeMethod,
convert_model_to_fp4,
)
from fastvideo.layers.quantization.fp8_config import (
FP8QuantizeMethod, convert_model_to_fp8,
FP8QuantizeMethod,
convert_model_to_fp8,
)
for mod in model.modules():
@@ -121,10 +122,7 @@ def maybe_load_fsdp_model(
"""
# NOTE(will): cast_forward_inputs=True shouldn't be needed as we are
# manually casting the inputs to the model
mp_policy = MixedPrecisionPolicy(param_dtype,
reduce_dtype,
output_dtype,
cast_forward_inputs=False)
mp_policy = MixedPrecisionPolicy(param_dtype, reduce_dtype, output_dtype, cast_forward_inputs=False)
set_mixed_precision_policy(
param_dtype=param_dtype,
@@ -137,6 +135,13 @@ def maybe_load_fsdp_model(
with set_default_dtype(default_dtype), torch.device("meta"):
model = model_cls(**init_params)
dtype_selector = getattr(model, "_get_parameter_dtype", None)
has_mixed_parameter_dtypes = callable(dtype_selector) and any(
dtype_selector(name, param_dtype) != param_dtype for name, _ in model.named_parameters())
if training_mode and has_mixed_parameter_dtypes:
raise NotImplementedError("FSDP training with model-selected mixed parameter dtypes requires "
"separate gradient synchronization for replicated parameters.")
# Check if we should use FSDP
use_fsdp = training_mode or fsdp_inference
@@ -152,7 +157,7 @@ def maybe_load_fsdp_model(
if not training_mode and not fsdp_inference:
hsdp_replicate_dim = world_size
hsdp_shard_dim = 1
if current_platform.is_npu():
with torch.device("cpu"):
device_mesh = init_device_mesh(
@@ -163,11 +168,11 @@ def maybe_load_fsdp_model(
)
else:
device_mesh = init_device_mesh(
"cuda",
# (Replicate(), Shard(dim=0))
mesh_shape=(hsdp_replicate_dim, hsdp_shard_dim),
mesh_dim_names=("replicate", "shard"),
)
"cuda",
# (Replicate(), Shard(dim=0))
mesh_shape=(hsdp_replicate_dim, hsdp_shard_dim),
mesh_dim_names=("replicate", "shard"),
)
shard_model(model,
cpu_offload=cpu_offload,
reshard_after_forward=True,
@@ -188,12 +193,10 @@ def maybe_load_fsdp_model(
param_names_mapping=param_names_mapping_fn,
)
if hasattr(model, "materialize_non_persistent_buffers"):
model.materialize_non_persistent_buffers(
device=device, dtype=default_dtype)
model.materialize_non_persistent_buffers(device=device, dtype=default_dtype)
for n, p in chain(model.named_parameters(), model.named_buffers()):
if p.is_meta:
raise RuntimeError(
f"Unexpected param or buffer {n} on meta device.")
raise RuntimeError(f"Unexpected param or buffer {n} on meta device.")
# Avoid unintended computation graph accumulation during inference
if isinstance(p, torch.nn.Parameter):
p.requires_grad = False
@@ -209,8 +212,7 @@ def maybe_load_fsdp_model(
compile_in_loader = enable_torch_compile and training_mode
if compile_in_loader:
compile_kwargs = torch_compile_kwargs or {}
logger.info("Enabling torch.compile for FSDP training module with kwargs=%s",
compile_kwargs)
logger.info("Enabling torch.compile for FSDP training module with kwargs=%s", compile_kwargs)
model = torch.compile(model, **compile_kwargs)
logger.info("torch.compile enabled for %s", type(model).__name__)
return model
@@ -254,58 +256,81 @@ def shard_model(
"""
# Check if we should use size-based filtering
use_size_filtering = os.environ.get("FASTVIDEO_FSDP2_AUTOWRAP", "0") == "1"
if not fsdp_shard_conditions:
logger.warning("No FSDP shard conditions provided; nothing will be sharded.")
return
default_param_dtype = getattr(mp_policy, "param_dtype", None)
dtype_selector = getattr(model, "_get_parameter_dtype", None)
ignored_params: set[nn.Parameter] = set()
if callable(dtype_selector) and default_param_dtype is not None:
ignored_params = {
parameter
for name, parameter in model.named_parameters()
if dtype_selector(name, default_param_dtype) != default_param_dtype
}
named_modules = list(model.named_modules())
ignored_params_by_module = {
id(module): ignored_params.intersection(set(module.parameters()))
for _, module in named_modules
}
fsdp_kwargs = {
"reshard_after_forward": reshard_after_forward,
"mesh": mesh,
"mp_policy": mp_policy,
}
if cpu_offload:
fsdp_kwargs["offload_policy"] = CPUOffloadPolicy(
pin_memory=pin_cpu_memory)
fsdp_kwargs["offload_policy"] = CPUOffloadPolicy(pin_memory=pin_cpu_memory)
# iterating in reverse to start with
# lowest-level modules first
num_layers_sharded = 0
if use_size_filtering:
# Size-based filtering mode
min_params = int(os.environ.get("FASTVIDEO_FSDP2_MIN_PARAMS", "10000000"))
logger.info("Using size-based filtering with threshold: %.2fM", min_params / 1e6)
for n, m in reversed(list(model.named_modules())):
for n, m in reversed(named_modules):
if any([shard_condition(n, m) for shard_condition in fsdp_shard_conditions]):
# Count all parameters
param_count = sum(p.numel() for p in m.parameters(recurse=True))
# Skip small modules
if param_count < min_params:
logger.info("Skipping module %s (%.2fM params < %.2fM threshold)",
n, param_count / 1e6, min_params / 1e6)
logger.info("Skipping module %s (%.2fM params < %.2fM threshold)", n, param_count / 1e6,
min_params / 1e6)
continue
# Shard this module
logger.info("Sharding module %s (%.2fM params)", n, param_count / 1e6)
fully_shard(m, **fsdp_kwargs)
module_kwargs = fsdp_kwargs
local_ignored_params = ignored_params_by_module[id(m)]
if local_ignored_params:
module_kwargs = {**fsdp_kwargs, "ignored_params": local_ignored_params}
fully_shard(m, **module_kwargs)
num_layers_sharded += 1
else:
# Shard all modules matching conditions
for n, m in reversed(list(model.named_modules())):
# Shard all modules matching conditions
for n, m in reversed(named_modules):
if any([shard_condition(n, m) for shard_condition in fsdp_shard_conditions]):
fully_shard(m, **fsdp_kwargs)
module_kwargs = fsdp_kwargs
local_ignored_params = ignored_params_by_module[id(m)]
if local_ignored_params:
module_kwargs = {**fsdp_kwargs, "ignored_params": local_ignored_params}
fully_shard(m, **module_kwargs)
num_layers_sharded += 1
if num_layers_sharded == 0:
raise ValueError(
"No layer modules were sharded. Please check if shard conditions are working as expected."
)
raise ValueError("No layer modules were sharded. Please check if shard conditions are working as expected.")
# Finally shard the entire model to account for any stragglers
fully_shard(model, **fsdp_kwargs)
root_kwargs = fsdp_kwargs
if ignored_params:
root_kwargs = {**fsdp_kwargs, "ignored_params": ignored_params}
fully_shard(model, **root_kwargs)
# TODO(PY): device mesh for cfg parallel
@@ -341,17 +366,17 @@ def load_model_from_full_model_state_dict(
"""
meta_sd = model.state_dict()
named_parameters = dict(model.named_parameters())
named_buffers = dict(model.named_buffers())
sharded_sd = {}
custom_param_sd, reverse_param_names_mapping = hf_to_custom_state_dict(
full_sd_iterator, param_names_mapping) # type: ignore
custom_param_sd, reverse_param_names_mapping = hf_to_custom_state_dict(full_sd_iterator,
param_names_mapping) # type: ignore
for target_param_name, full_tensor in custom_param_sd.items():
meta_sharded_param = meta_sd.get(target_param_name)
if meta_sharded_param is None:
# Some checkpoints include extra entries that are not part of the
# instantiated model's state_dict (e.g. `_extra_state` keys from
# some FSDP checkpoint formats). These can be safely skipped.
if (target_param_name.endswith("._extra_state")
or target_param_name.endswith("_extra_state")):
if (target_param_name.endswith("._extra_state") or target_param_name.endswith("_extra_state")):
logger.warning(
"Skipping non-parameter checkpoint key: %s",
target_param_name,
@@ -370,8 +395,12 @@ def load_model_from_full_model_state_dict(
raise ValueError(
f"Parameter {target_param_name} not found in custom model state dict. The hf to custom mapping may be incorrect."
)
target_dtype = param_dtype
dtype_selector = getattr(model, "_get_parameter_dtype", None)
if callable(dtype_selector):
target_dtype = dtype_selector(target_param_name, param_dtype)
if not hasattr(meta_sharded_param, "device_mesh"):
full_tensor = full_tensor.to(device=device, dtype=param_dtype)
full_tensor = full_tensor.to(device=device, dtype=target_dtype)
target_param = named_parameters.get(target_param_name)
weight_loader = getattr(target_param, "weight_loader", None)
# Gated on a shape mismatch: only fused/stacked params with a custom
@@ -380,9 +409,7 @@ def load_model_from_full_model_state_dict(
# fall through to the original `sharded_tensor = full_tensor` below.
if target_param is not None and callable(weight_loader) and tuple(target_param.shape) != tuple(
full_tensor.shape):
loaded_param = nn.Parameter(torch.empty(tuple(target_param.shape),
device=device,
dtype=param_dtype),
loaded_param = nn.Parameter(torch.empty(tuple(target_param.shape), device=device, dtype=target_dtype),
requires_grad=False)
for attr_name, attr_value in vars(target_param).items():
setattr(loaded_param, attr_name, attr_value)
@@ -392,7 +419,7 @@ def load_model_from_full_model_state_dict(
# In cases where parts of the model aren't sharded, some parameters will be plain tensors.
sharded_tensor = full_tensor
else:
full_tensor = full_tensor.to(device=device, dtype=param_dtype)
full_tensor = full_tensor.to(device=device, dtype=target_dtype)
sharded_tensor = distribute_tensor(
full_tensor,
meta_sharded_param.device_mesh,
@@ -400,36 +427,35 @@ def load_model_from_full_model_state_dict(
)
if cpu_offload:
sharded_tensor = sharded_tensor.cpu()
sharded_sd[target_param_name] = nn.Parameter(sharded_tensor)
if target_param_name in named_buffers:
sharded_sd[target_param_name] = sharded_tensor
else:
sharded_sd[target_param_name] = nn.Parameter(sharded_tensor)
model.reverse_param_names_mapping = reverse_param_names_mapping
unused_keys = set(meta_sd.keys()) - set(sharded_sd.keys())
if unused_keys:
logger.warning("Found unloaded parameters in meta state dict: %s",
unused_keys)
logger.warning("Found unloaded parameters in meta state dict: %s", unused_keys)
# List of allowed parameter name patterns
ALLOWED_NEW_PARAM_PATTERNS = ["gate_compress", "proj_l"] # Can be extended as needed
for new_param_name in unused_keys:
if not any(pattern in new_param_name
for pattern in ALLOWED_NEW_PARAM_PATTERNS):
logger.error("Unsupported new parameter: %s. Allowed patterns: %s",
new_param_name, ALLOWED_NEW_PARAM_PATTERNS)
raise ValueError(
f"New parameter '{new_param_name}' is not supported. "
f"Currently only parameters containing {ALLOWED_NEW_PARAM_PATTERNS} are allowed."
)
if not any(pattern in new_param_name for pattern in ALLOWED_NEW_PARAM_PATTERNS):
logger.error("Unsupported new parameter: %s. Allowed patterns: %s", new_param_name,
ALLOWED_NEW_PARAM_PATTERNS)
raise ValueError(f"New parameter '{new_param_name}' is not supported. "
f"Currently only parameters containing {ALLOWED_NEW_PARAM_PATTERNS} are allowed.")
meta_sharded_param = meta_sd.get(new_param_name)
target_dtype = param_dtype
dtype_selector = getattr(model, "_get_parameter_dtype", None)
if callable(dtype_selector):
target_dtype = dtype_selector(new_param_name, param_dtype)
if not hasattr(meta_sharded_param, "device_mesh"):
# Initialize with zeros
sharded_tensor = torch.zeros_like(meta_sharded_param,
device=device,
dtype=param_dtype)
sharded_tensor = torch.zeros_like(meta_sharded_param, device=device, dtype=target_dtype)
else:
# Initialize with zeros and distribute
full_tensor = torch.zeros_like(meta_sharded_param,
device=device,
dtype=param_dtype)
full_tensor = torch.zeros_like(meta_sharded_param, device=device, dtype=target_dtype)
sharded_tensor = distribute_tensor(
full_tensor,
meta_sharded_param.device_mesh,
+4 -2
View File
@@ -18,8 +18,10 @@ def set_default_torch_dtype(dtype: torch.dtype):
"""Sets the default torch dtype to the given dtype."""
old_dtype = torch.get_default_dtype()
torch.set_default_dtype(dtype)
yield
torch.set_default_dtype(old_dtype)
try:
yield
finally:
torch.set_default_dtype(old_dtype)
def get_param_names_mapping(
+16
View File
@@ -42,6 +42,11 @@ _TEXT_TO_VIDEO_DIT_MODELS = {
"LTX2Transformer3DModel": ("dits", "ltx2", "LTX2Transformer3DModel"),
"SD3Transformer2DModel": ("dits", "sd3", "SD3Transformer2DModel"),
"LingBotWorldTransformer3DModel": ("dits", "lingbotworld", "LingBotWorldTransformer3DModel"),
"LingBotWorld2CausalFastTransformer3DModel": (
"dits",
"lingbotworld2",
"LingBotWorld2CausalFastTransformer3DModel",
),
"Gen3CTransformer3DModel": ("dits", "gen3c", "Gen3CTransformer3DModel"),
"Kandinsky5Transformer3DModel": ("dits", "kandinsky5", "Kandinsky5Transformer3DModel"),
"Flux2Transformer2DModel": ("dits", "flux_2", "Flux2Transformer2DModel"),
@@ -53,6 +58,11 @@ _IMAGE_TO_VIDEO_DIT_MODELS = {
"DreamXWorldTransformer3DModel": ("dits", "dreamx_world", "DreamXWorldTransformer3DModel"),
"DreamXWorldARTransformer3DModel": ("dits", "dreamx_world_ar", "DreamXWorldARTransformer3DModel"),
"CausalWanTransformer3DModel": ("dits", "causal_wanvideo", "CausalWanTransformer3DModel"),
"LingBotWorld2CausalFastTransformer3DModel": (
"dits",
"lingbotworld2",
"LingBotWorld2CausalFastTransformer3DModel",
),
"MatrixGame2WanModel": ("dits", "matrixgame2", "MatrixGame2WanModel"),
"CausalMatrixGame2WanModel": ("dits", "matrixgame2", "CausalMatrixGame2WanModel"),
# Legacy aliases for older HF model_index.json files
@@ -64,6 +74,7 @@ _IMAGE_TO_VIDEO_DIT_MODELS = {
# Text-to-image DiT models (2D image generation)
_TEXT_TO_IMAGE_DIT_MODELS = {
"GlmImageTransformer2DModel": ("dits", "glm_image", "GlmImageTransformer2DModel"),
"ZImageTransformer2DModel": ("dits", "zimage", "ZImageTransformer2DModel"),
}
_TEXT_ENCODER_MODELS = {
@@ -72,12 +83,16 @@ _TEXT_ENCODER_MODELS = {
("encoders", "clip", "CLIPTextModelWithProjection"),
"LlamaModel": ("encoders", "llama", "LlamaModel"),
"UMT5EncoderModel": ("encoders", "t5", "UMT5EncoderModel"),
"LingBotWorld2T5EncoderModel": ("encoders", "lingbotworld2_t5", "LingBotWorld2T5EncoderModel"),
"T5EncoderModel": ("encoders", "t5_hf", "T5EncoderModel"),
"BertModel": ("encoders", "clip", "CLIPTextModel"),
"Qwen2_5_VLTextModel": ("encoders", "qwen2_5", "Qwen2_5_VLTextModel"),
"Reason1TextEncoder": ("encoders", "reason1", "Reason1TextEncoder"),
"Qwen2_5_VLForConditionalGeneration":
("encoders", "reason1", "Reason1TextEncoder"),
# Z-Image-Turbo's text_encoder/config.json declares architecture
# "Qwen3Model"; route it to the shared Qwen3 encoder (added for Flux2 Klein).
"Qwen3Model": ("encoders", "qwen3", "Qwen3ForCausalLM"),
"LTX2GemmaTextEncoderModel": ("encoders", "gemma", "LTX2GemmaTextEncoderModel"),
"Qwen3ForCausalLM": ("encoders", "qwen3", "Qwen3ForCausalLM"),
"Mistral3ForConditionalGeneration":
@@ -98,6 +113,7 @@ _VAE_MODELS = {
"AutoencoderKLHYWorld": ("vaes", "hyworldvae", "AutoencoderKLHYWorld"),
"AutoencoderKLHunyuanVideo15": ("vaes", "hunyuan15vae", "AutoencoderKLHunyuanVideo15"),
"AutoencoderKLWan": ("vaes", "wanvae", "AutoencoderKLWan"),
"LingBotWorld2WanVAE": ("vaes", "lingbotworld2_wanvae", "LingBotWorld2WanVAE"),
"AutoencoderKL": ("vaes", "autoencoder_kl", "AutoencoderKL"),
"AutoencoderKLGen3CTokenizer":
("vaes", "gen3c_tokenizer_vae", "AutoencoderKLGen3CTokenizer"),
@@ -96,6 +96,14 @@ class FlowMatchEulerDiscreteScheduler(SchedulerMixin, ConfigMixin,
The minimum sigma value for the noise schedule.
sigma_data (`float`, *optional*):
The sigma data value for scaling.
use_reference_discrete_timesteps (`bool`, defaults to False):
Some reference schedulers (e.g. Z-Image) construct the timestep
schedule by linspacing `num_inference_steps + 1` points from
`t_max` to `t_min` and dropping the terminal point. Default
(`False`) preserves the original `np.linspace(t_max, t_min,
num_inference_steps)` (float64) behaviour used by every existing
model. Enable this flag only when matching a reference scheduler
that expects the +1 + drop-terminal construction.
"""
_compatibles: list[Any] = []
@@ -122,6 +130,7 @@ class FlowMatchEulerDiscreteScheduler(SchedulerMixin, ConfigMixin,
sigma_max: float | None = None,
sigma_min: float | None = None,
sigma_data: float | None = None,
use_reference_discrete_timesteps: bool = False,
):
if sum([
self.config.use_beta_sigmas, self.config.use_exponential_sigmas,
@@ -155,7 +164,7 @@ class FlowMatchEulerDiscreteScheduler(SchedulerMixin, ConfigMixin,
self.sigmas = sigmas.to(
"cpu") # to avoid too much CPU/GPU communication
self.sigma_min = self.sigmas[-1].item()
self.sigma_min = sigma_min if sigma_min is not None else self.sigmas[-1].item()
self.sigma_max = self.sigmas[0].item()
BaseScheduler.__init__(self)
@@ -350,7 +359,19 @@ class FlowMatchEulerDiscreteScheduler(SchedulerMixin, ConfigMixin,
if timesteps_array is None:
t_max = self._sigma_to_t(self.sigma_max)
t_min = self._sigma_to_t(self.sigma_min)
timesteps_array = np.linspace(t_max, t_min, num_inference_steps)
if self.config.use_reference_discrete_timesteps:
# Some reference schedulers (for example Z-Image) build a
# float64 num_steps+1 linspace and drop the terminal point.
timesteps_array = np.linspace(
t_max,
t_min,
num_inference_steps + 1,
)[:-1]
else:
# Preserve the original numpy default (float64) here —
# casting to float32 silently shifts rounded timestep
# values for every existing model that uses this branch.
timesteps_array = np.linspace(t_max, t_min, num_inference_steps)
sigmas_array = timesteps_array / self.config.num_train_timesteps
else:
sigmas_array = np.array(sigmas).astype(np.float32)
@@ -0,0 +1,722 @@
import logging
from types import SimpleNamespace
import torch
import torch.cuda.amp as amp
import torch.nn as nn
import torch.nn.functional as F
from einops import rearrange
__all__ = [
'Wan2_1_VAE',
'LingBotWorld2WanVAE',
]
CACHE_T = 2
class CausalConv3d(nn.Conv3d):
"""
Causal 3d convolusion.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._padding = (self.padding[2], self.padding[2], self.padding[1],
self.padding[1], 2 * self.padding[0], 0)
self.padding = (0, 0, 0)
def forward(self, x, cache_x=None):
padding = list(self._padding)
if cache_x is not None and self._padding[4] > 0:
cache_x = cache_x.to(x.device)
x = torch.cat([cache_x, x], dim=2)
padding[4] -= cache_x.shape[2]
x = F.pad(x, padding)
return super().forward(x)
class RMS_norm(nn.Module):
def __init__(self, dim, channel_first=True, images=True, bias=False):
super().__init__()
broadcastable_dims = (1, 1, 1) if not images else (1, 1)
shape = (dim, *broadcastable_dims) if channel_first else (dim,)
self.channel_first = channel_first
self.scale = dim**0.5
self.gamma = nn.Parameter(torch.ones(shape))
self.bias = nn.Parameter(torch.zeros(shape)) if bias else 0.
def forward(self, x):
return F.normalize(
x, dim=(1 if self.channel_first else
-1)) * self.scale * self.gamma + self.bias
class Upsample(nn.Upsample):
def forward(self, x):
"""
Fix bfloat16 support for nearest neighbor interpolation.
"""
return super().forward(x.float()).type_as(x)
class Resample(nn.Module):
def __init__(self, dim, mode):
assert mode in ('none', 'upsample2d', 'upsample3d', 'downsample2d',
'downsample3d')
super().__init__()
self.dim = dim
self.mode = mode
# layers
if mode == 'upsample2d':
self.resample = nn.Sequential(
Upsample(scale_factor=(2., 2.), mode='nearest-exact'),
nn.Conv2d(dim, dim // 2, 3, padding=1))
elif mode == 'upsample3d':
self.resample = nn.Sequential(
Upsample(scale_factor=(2., 2.), mode='nearest-exact'),
nn.Conv2d(dim, dim // 2, 3, padding=1))
self.time_conv = CausalConv3d(
dim, dim * 2, (3, 1, 1), padding=(1, 0, 0))
elif mode == 'downsample2d':
self.resample = nn.Sequential(
nn.ZeroPad2d((0, 1, 0, 1)),
nn.Conv2d(dim, dim, 3, stride=(2, 2)))
elif mode == 'downsample3d':
self.resample = nn.Sequential(
nn.ZeroPad2d((0, 1, 0, 1)),
nn.Conv2d(dim, dim, 3, stride=(2, 2)))
self.time_conv = CausalConv3d(
dim, dim, (3, 1, 1), stride=(2, 1, 1), padding=(0, 0, 0))
else:
self.resample = nn.Identity()
def forward(self, x, feat_cache=None, feat_idx=[0]):
b, c, t, h, w = x.size()
if self.mode == 'upsample3d':
if feat_cache is not None:
idx = feat_idx[0]
if feat_cache[idx] is None:
feat_cache[idx] = 'Rep'
feat_idx[0] += 1
else:
cache_x = x[:, :, -CACHE_T:, :, :].clone()
if cache_x.shape[2] < 2 and feat_cache[
idx] is not None and feat_cache[idx] != 'Rep':
# cache last frame of last two chunk
cache_x = torch.cat([
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
cache_x.device), cache_x
],
dim=2)
if cache_x.shape[2] < 2 and feat_cache[
idx] is not None and feat_cache[idx] == 'Rep':
cache_x = torch.cat([
torch.zeros_like(cache_x).to(cache_x.device),
cache_x
],
dim=2)
if feat_cache[idx] == 'Rep':
x = self.time_conv(x)
else:
x = self.time_conv(x, feat_cache[idx])
feat_cache[idx] = cache_x
feat_idx[0] += 1
x = x.reshape(b, 2, c, t, h, w)
x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]),
3)
x = x.reshape(b, c, t * 2, h, w)
t = x.shape[2]
x = rearrange(x, 'b c t h w -> (b t) c h w')
x = self.resample(x)
x = rearrange(x, '(b t) c h w -> b c t h w', t=t)
if self.mode == 'downsample3d':
if feat_cache is not None:
idx = feat_idx[0]
if feat_cache[idx] is None:
feat_cache[idx] = x.clone()
feat_idx[0] += 1
else:
cache_x = x[:, :, -1:, :, :].clone()
# if cache_x.shape[2] < 2 and feat_cache[idx] is not None and feat_cache[idx]!='Rep':
# # cache last frame of last two chunk
# cache_x = torch.cat([feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(cache_x.device), cache_x], dim=2)
x = self.time_conv(
torch.cat([feat_cache[idx][:, :, -1:, :, :], x], 2))
feat_cache[idx] = cache_x
feat_idx[0] += 1
return x
def init_weight(self, conv):
conv_weight = conv.weight
nn.init.zeros_(conv_weight)
c1, c2, t, h, w = conv_weight.size()
one_matrix = torch.eye(c1, c2)
init_matrix = one_matrix
nn.init.zeros_(conv_weight)
#conv_weight.data[:,:,-1,1,1] = init_matrix * 0.5
conv_weight.data[:, :, 1, 0, 0] = init_matrix #* 0.5
conv.weight.data.copy_(conv_weight)
nn.init.zeros_(conv.bias.data)
def init_weight2(self, conv):
conv_weight = conv.weight.data
nn.init.zeros_(conv_weight)
c1, c2, t, h, w = conv_weight.size()
init_matrix = torch.eye(c1 // 2, c2)
#init_matrix = repeat(init_matrix, 'o ... -> (o 2) ...').permute(1,0,2).contiguous().reshape(c1,c2)
conv_weight[:c1 // 2, :, -1, 0, 0] = init_matrix
conv_weight[c1 // 2:, :, -1, 0, 0] = init_matrix
conv.weight.data.copy_(conv_weight)
nn.init.zeros_(conv.bias.data)
class ResidualBlock(nn.Module):
def __init__(self, in_dim, out_dim, dropout=0.0):
super().__init__()
self.in_dim = in_dim
self.out_dim = out_dim
# layers
self.residual = nn.Sequential(
RMS_norm(in_dim, images=False), nn.SiLU(),
CausalConv3d(in_dim, out_dim, 3, padding=1),
RMS_norm(out_dim, images=False), nn.SiLU(), nn.Dropout(dropout),
CausalConv3d(out_dim, out_dim, 3, padding=1))
self.shortcut = CausalConv3d(in_dim, out_dim, 1) \
if in_dim != out_dim else nn.Identity()
def forward(self, x, feat_cache=None, feat_idx=[0]):
h = self.shortcut(x)
for layer in self.residual:
if isinstance(layer, CausalConv3d) and feat_cache is not None:
idx = feat_idx[0]
cache_x = x[:, :, -CACHE_T:, :, :].clone()
if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
# cache last frame of last two chunk
cache_x = torch.cat([
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
cache_x.device), cache_x
],
dim=2)
x = layer(x, feat_cache[idx])
feat_cache[idx] = cache_x
feat_idx[0] += 1
else:
x = layer(x)
return x + h
class AttentionBlock(nn.Module):
"""
Causal self-attention with a single head.
"""
def __init__(self, dim):
super().__init__()
self.dim = dim
# layers
self.norm = RMS_norm(dim)
self.to_qkv = nn.Conv2d(dim, dim * 3, 1)
self.proj = nn.Conv2d(dim, dim, 1)
# zero out the last layer params
nn.init.zeros_(self.proj.weight)
def forward(self, x):
identity = x
b, c, t, h, w = x.size()
x = rearrange(x, 'b c t h w -> (b t) c h w')
x = self.norm(x)
# compute query, key, value
q, k, v = self.to_qkv(x).reshape(b * t, 1, c * 3,
-1).permute(0, 1, 3,
2).contiguous().chunk(
3, dim=-1)
# apply attention
x = F.scaled_dot_product_attention(
q,
k,
v,
)
x = x.squeeze(1).permute(0, 2, 1).reshape(b * t, c, h, w)
# output
x = self.proj(x)
x = rearrange(x, '(b t) c h w-> b c t h w', t=t)
return x + identity
class Encoder3d(nn.Module):
def __init__(self,
dim=128,
z_dim=4,
dim_mult=[1, 2, 4, 4],
num_res_blocks=2,
attn_scales=[],
temperal_downsample=[True, True, False],
dropout=0.0):
super().__init__()
self.dim = dim
self.z_dim = z_dim
self.dim_mult = dim_mult
self.num_res_blocks = num_res_blocks
self.attn_scales = attn_scales
self.temperal_downsample = temperal_downsample
# dimensions
dims = [dim * u for u in [1] + dim_mult]
scale = 1.0
# init block
self.conv1 = CausalConv3d(3, dims[0], 3, padding=1)
# downsample blocks
downsamples = []
for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
# residual (+attention) blocks
for _ in range(num_res_blocks):
downsamples.append(ResidualBlock(in_dim, out_dim, dropout))
if scale in attn_scales:
downsamples.append(AttentionBlock(out_dim))
in_dim = out_dim
# downsample block
if i != len(dim_mult) - 1:
mode = 'downsample3d' if temperal_downsample[
i] else 'downsample2d'
downsamples.append(Resample(out_dim, mode=mode))
scale /= 2.0
self.downsamples = nn.Sequential(*downsamples)
# middle blocks
self.middle = nn.Sequential(
ResidualBlock(out_dim, out_dim, dropout), AttentionBlock(out_dim),
ResidualBlock(out_dim, out_dim, dropout))
# output blocks
self.head = nn.Sequential(
RMS_norm(out_dim, images=False), nn.SiLU(),
CausalConv3d(out_dim, z_dim, 3, padding=1))
def forward(self, x, feat_cache=None, feat_idx=[0]):
if feat_cache is not None:
idx = feat_idx[0]
cache_x = x[:, :, -CACHE_T:, :, :].clone()
if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
# cache last frame of last two chunk
cache_x = torch.cat([
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
cache_x.device), cache_x
],
dim=2)
x = self.conv1(x, feat_cache[idx])
feat_cache[idx] = cache_x
feat_idx[0] += 1
else:
x = self.conv1(x)
## downsamples
for layer in self.downsamples:
if feat_cache is not None:
x = layer(x, feat_cache, feat_idx)
else:
x = layer(x)
## middle
for layer in self.middle:
if isinstance(layer, ResidualBlock) and feat_cache is not None:
x = layer(x, feat_cache, feat_idx)
else:
x = layer(x)
## head
for layer in self.head:
if isinstance(layer, CausalConv3d) and feat_cache is not None:
idx = feat_idx[0]
cache_x = x[:, :, -CACHE_T:, :, :].clone()
if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
# cache last frame of last two chunk
cache_x = torch.cat([
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
cache_x.device), cache_x
],
dim=2)
x = layer(x, feat_cache[idx])
feat_cache[idx] = cache_x
feat_idx[0] += 1
else:
x = layer(x)
return x
class Decoder3d(nn.Module):
def __init__(self,
dim=128,
z_dim=4,
dim_mult=[1, 2, 4, 4],
num_res_blocks=2,
attn_scales=[],
temperal_upsample=[False, True, True],
dropout=0.0):
super().__init__()
self.dim = dim
self.z_dim = z_dim
self.dim_mult = dim_mult
self.num_res_blocks = num_res_blocks
self.attn_scales = attn_scales
self.temperal_upsample = temperal_upsample
# dimensions
dims = [dim * u for u in [dim_mult[-1]] + dim_mult[::-1]]
scale = 1.0 / 2**(len(dim_mult) - 2)
# init block
self.conv1 = CausalConv3d(z_dim, dims[0], 3, padding=1)
# middle blocks
self.middle = nn.Sequential(
ResidualBlock(dims[0], dims[0], dropout), AttentionBlock(dims[0]),
ResidualBlock(dims[0], dims[0], dropout))
# upsample blocks
upsamples = []
for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
# residual (+attention) blocks
if i == 1 or i == 2 or i == 3:
in_dim = in_dim // 2
for _ in range(num_res_blocks + 1):
upsamples.append(ResidualBlock(in_dim, out_dim, dropout))
if scale in attn_scales:
upsamples.append(AttentionBlock(out_dim))
in_dim = out_dim
# upsample block
if i != len(dim_mult) - 1:
mode = 'upsample3d' if temperal_upsample[i] else 'upsample2d'
upsamples.append(Resample(out_dim, mode=mode))
scale *= 2.0
self.upsamples = nn.Sequential(*upsamples)
# output blocks
self.head = nn.Sequential(
RMS_norm(out_dim, images=False), nn.SiLU(),
CausalConv3d(out_dim, 3, 3, padding=1))
def forward(self, x, feat_cache=None, feat_idx=[0]):
## conv1
if feat_cache is not None:
idx = feat_idx[0]
cache_x = x[:, :, -CACHE_T:, :, :].clone()
if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
# cache last frame of last two chunk
cache_x = torch.cat([
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
cache_x.device), cache_x
],
dim=2)
x = self.conv1(x, feat_cache[idx])
feat_cache[idx] = cache_x
feat_idx[0] += 1
else:
x = self.conv1(x)
## middle
for layer in self.middle:
if isinstance(layer, ResidualBlock) and feat_cache is not None:
x = layer(x, feat_cache, feat_idx)
else:
x = layer(x)
## upsamples
for layer in self.upsamples:
if feat_cache is not None:
x = layer(x, feat_cache, feat_idx)
else:
x = layer(x)
## head
for layer in self.head:
if isinstance(layer, CausalConv3d) and feat_cache is not None:
idx = feat_idx[0]
cache_x = x[:, :, -CACHE_T:, :, :].clone()
if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
# cache last frame of last two chunk
cache_x = torch.cat([
feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
cache_x.device), cache_x
],
dim=2)
x = layer(x, feat_cache[idx])
feat_cache[idx] = cache_x
feat_idx[0] += 1
else:
x = layer(x)
return x
def count_conv3d(model):
count = 0
for m in model.modules():
if isinstance(m, CausalConv3d):
count += 1
return count
class WanVAE_(nn.Module):
def __init__(self,
dim=128,
z_dim=4,
dim_mult=[1, 2, 4, 4],
num_res_blocks=2,
attn_scales=[],
temperal_downsample=[True, True, False],
dropout=0.0):
super().__init__()
self.dim = dim
self.z_dim = z_dim
self.dim_mult = dim_mult
self.num_res_blocks = num_res_blocks
self.attn_scales = attn_scales
self.temperal_downsample = temperal_downsample
self.temperal_upsample = temperal_downsample[::-1]
# modules
self.encoder = Encoder3d(dim, z_dim * 2, dim_mult, num_res_blocks,
attn_scales, self.temperal_downsample, dropout)
self.conv1 = CausalConv3d(z_dim * 2, z_dim * 2, 1)
self.conv2 = CausalConv3d(z_dim, z_dim, 1)
self.decoder = Decoder3d(dim, z_dim, dim_mult, num_res_blocks,
attn_scales, self.temperal_upsample, dropout)
def forward(self, x):
mu, log_var = self.encode(x)
z = self.reparameterize(mu, log_var)
x_recon = self.decode(z)
return x_recon, mu, log_var
def encode(self, x, scale):
self.clear_cache()
## cache
t = x.shape[2]
iter_ = 1 + (t - 1) // 4
for i in range(iter_):
self._enc_conv_idx = [0]
if i == 0:
out = self.encoder(
x[:, :, :1, :, :],
feat_cache=self._enc_feat_map,
feat_idx=self._enc_conv_idx)
else:
out_ = self.encoder(
x[:, :, 1 + 4 * (i - 1):1 + 4 * i, :, :],
feat_cache=self._enc_feat_map,
feat_idx=self._enc_conv_idx)
out = torch.cat([out, out_], 2)
mu, log_var = self.conv1(out).chunk(2, dim=1)
if isinstance(scale[0], torch.Tensor):
mu = (mu - scale[0].view(1, self.z_dim, 1, 1, 1)) * scale[1].view(
1, self.z_dim, 1, 1, 1)
else:
mu = (mu - scale[0]) * scale[1]
self.clear_cache()
return mu
def decode(self, z, scale):
self.clear_cache()
# z: [b,c,t,h,w]
if isinstance(scale[0], torch.Tensor):
z = z / scale[1].view(1, self.z_dim, 1, 1, 1) + scale[0].view(
1, self.z_dim, 1, 1, 1)
else:
z = z / scale[1] + scale[0]
iter_ = z.shape[2]
x = self.conv2(z)
for i in range(iter_):
self._conv_idx = [0]
if i == 0:
out = self.decoder(
x[:, :, i:i + 1, :, :],
feat_cache=self._feat_map,
feat_idx=self._conv_idx)
else:
out_ = self.decoder(
x[:, :, i:i + 1, :, :],
feat_cache=self._feat_map,
feat_idx=self._conv_idx)
out = torch.cat([out, out_], 2)
self.clear_cache()
return out
def reparameterize(self, mu, log_var):
std = torch.exp(0.5 * log_var)
eps = torch.randn_like(std)
return eps * std + mu
def sample(self, imgs, deterministic=False):
mu, log_var = self.encode(imgs)
if deterministic:
return mu
std = torch.exp(0.5 * log_var.clamp(-30.0, 20.0))
return mu + std * torch.randn_like(std)
def clear_cache(self):
self._conv_num = count_conv3d(self.decoder)
self._conv_idx = [0]
self._feat_map = [None] * self._conv_num
#cache encode
self._enc_conv_num = count_conv3d(self.encoder)
self._enc_conv_idx = [0]
self._enc_feat_map = [None] * self._enc_conv_num
def _video_vae(pretrained_path=None, z_dim=None, device='cpu', **kwargs):
"""
Autoencoder3d adapted from Stable Diffusion 1.x, 2.x and XL.
"""
# params
cfg = dict(
dim=96,
z_dim=z_dim,
dim_mult=[1, 2, 4, 4],
num_res_blocks=2,
attn_scales=[],
temperal_downsample=[False, True, True],
dropout=0.0)
cfg.update(**kwargs)
# init model
with torch.device('meta'):
model = WanVAE_(**cfg)
# load checkpoint
logging.info(f'loading {pretrained_path}')
model.load_state_dict(
torch.load(pretrained_path, map_location=device), assign=True)
return model
class Wan2_1_VAE:
def __init__(self,
z_dim=16,
vae_pth='cache/vae_step_411000.pth',
dtype=torch.float,
device="cuda"):
self.dtype = dtype
self.device = device
mean = [
-0.7571, -0.7089, -0.9113, 0.1075, -0.1745, 0.9653, -0.1517, 1.5508,
0.4134, -0.0715, 0.5517, -0.3632, -0.1922, -0.9497, 0.2503, -0.2921
]
std = [
2.8184, 1.4541, 2.3275, 2.6558, 1.2196, 1.7708, 2.6052, 2.0743,
3.2687, 2.1526, 2.8652, 1.5579, 1.6382, 1.1253, 2.8251, 1.9160
]
self.mean = torch.tensor(mean, dtype=dtype, device=device)
self.std = torch.tensor(std, dtype=dtype, device=device)
self.scale = [self.mean, 1.0 / self.std]
# init model
self.model = _video_vae(
pretrained_path=vae_pth,
z_dim=z_dim,
).eval().requires_grad_(False).to(device)
def encode(self, videos):
"""
videos: A list of videos each with shape [C, T, H, W].
"""
with amp.autocast(dtype=self.dtype):
return [
self.model.encode(u.unsqueeze(0), self.scale).float().squeeze(0)
for u in videos
]
def decode(self, zs):
with amp.autocast(dtype=self.dtype):
return [
self.model.decode(u.unsqueeze(0),
self.scale).float().clamp_(-1, 1).squeeze(0)
for u in zs
]
class LingBotWorld2WanVAE(nn.Module):
"""FastVideo-facing wrapper around the exact LingBot World 2 Wan2.1 VAE computation."""
handles_latent_denorm = True
def __init__(self, config, checkpoint_path=None, dtype=torch.float):
"""Load the official LingBot World 2 VAE weights and expose FastVideo VAE APIs."""
super().__init__()
self.config = config
self.dtype = dtype
z_dim = int(getattr(config, "z_dim", 16))
mean = torch.tensor(getattr(config, "latents_mean"), dtype=dtype)
std = torch.tensor(getattr(config, "latents_std"), dtype=dtype)
self.register_buffer("shift_factor", mean.view(1, z_dim, 1, 1, 1), persistent=False)
self.register_buffer("scaling_factor", (1.0 / std).view(1, z_dim, 1, 1, 1), persistent=False)
self.scale = [mean, 1.0 / std]
if checkpoint_path is None:
self.model = WanVAE_(dim=96, z_dim=z_dim, dim_mult=[1, 2, 4, 4],
num_res_blocks=2, attn_scales=[],
temperal_downsample=[False, True, True],
dropout=0.0)
else:
self.model = _video_vae(pretrained_path=checkpoint_path, z_dim=z_dim)
self.model.eval().requires_grad_(False)
def _scale_for(self, device: torch.device) -> list[torch.Tensor]:
"""Return source VAE scale tensors on the active device."""
return [u.to(device) for u in self.scale]
def encode(self, videos: torch.Tensor):
"""Encode `[B,C,T,H,W]` videos and return a FastVideo-style mean tensor."""
if videos.ndim != 5:
raise ValueError(f"LingBotWorld2WanVAE.encode expects 5D input, got {videos.shape}")
scale = self._scale_for(videos.device)
latents = []
with amp.autocast(dtype=self.dtype):
for video in videos:
latent = self.model.encode(video.unsqueeze(0), scale).float().squeeze(0)
latents.append(latent)
normalized = torch.stack(latents, dim=0)
return SimpleNamespace(mean=normalized)
def decode(self, latents: torch.Tensor) -> torch.Tensor:
"""Decode normalized LingBot World 2 latents to clamped `[-1,1]` video tensors."""
if latents.ndim != 5:
raise ValueError(f"LingBotWorld2WanVAE.decode expects 5D input, got {latents.shape}")
scale = self._scale_for(latents.device)
videos = []
with amp.autocast(dtype=self.dtype):
for latent in latents:
video = self.model.decode(latent.unsqueeze(0), scale).float().clamp_(-1, 1).squeeze(0)
videos.append(video)
return torch.stack(videos, dim=0)
EntryClass = LingBotWorld2WanVAE
+1 -1
View File
@@ -35,7 +35,7 @@ def build_pipeline(fastvideo_args: FastVideoArgs,
"""
# Get pipeline type
model_path = fastvideo_args.model_path
model_path = maybe_download_model(model_path)
model_path = maybe_download_model(model_path, revision=fastvideo_args.revision)
# fastvideo_args.downloaded_model_path = model_path
logger.info("Model path: %s", model_path)
@@ -19,7 +19,7 @@ class GlmImageDecodingStage(DecodingStage):
vae_dtype = PRECISION_TO_TYPE[fastvideo_args.pipeline_config.vae_precision]
vae_autocast = (vae_dtype != torch.float32 and not fastvideo_args.disable_autocast)
latents = self._denormalize_latents(latents)
latents = self._denormalize_latents(latents, fastvideo_args)
if latents.dim() == 5:
latents = latents.squeeze(2)
@@ -0,0 +1,5 @@
"""Dense LingBot-Video inference pipeline."""
from fastvideo.pipelines.basic.lingbot_video.lingbot_video_pipeline import LingBotVideoPipeline
__all__ = ["LingBotVideoPipeline"]
@@ -0,0 +1,106 @@
# SPDX-License-Identifier: Apache-2.0
"""Stage-composed LingBot-Video Dense and MoE/refiner T2V pipeline."""
from typing import Any
from fastvideo.fastvideo_args import FastVideoArgs
from fastvideo.pipelines import ComposedPipelineBase, LoRAPipeline
from fastvideo.pipelines.basic.lingbot_video.stages import (
LingBotVideoDenoisingStage,
LingBotVideoInputValidationStage,
LingBotVideoLatentPreparationStage,
LingBotVideoRefinerPreparationStage,
)
from fastvideo.pipelines.stages import (
ConditioningStage,
DecodingStage,
TextEncodingStage,
TimestepPreparationStage,
)
class LingBotVideoPipeline(LoRAPipeline, ComposedPipelineBase):
"""T2V pipeline with optional released MoE pixel-space refinement."""
is_video_pipeline = True
_required_config_modules = ["text_encoder", "tokenizer", "vae", "transformer", "scheduler"]
def load_modules(
self,
fastvideo_args: FastVideoArgs,
loaded_modules: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Load the optional refiner DiT and the VAE encoder only when declared."""
model_index = self._load_config(self.model_path)
required = list(type(self)._required_config_modules)
load_refiner = "transformer_2" in model_index and getattr(fastvideo_args, "refine_enabled", None) is not False
if load_refiner:
required.append("transformer_2")
fastvideo_args.pipeline_config.vae_config.load_encoder = True
self._required_config_modules = required
return super().load_modules(fastvideo_args, loaded_modules)
def initialize_pipeline(self, fastvideo_args: FastVideoArgs) -> None:
"""Apply the released runtime flow shift to the loaded scheduler."""
shift = fastvideo_args.pipeline_config.flow_shift
if shift is None:
raise ValueError("LingBot-Video requires a flow shift")
self.get_module("scheduler").set_shift(float(shift))
def create_pipeline_stages(self, fastvideo_args: FastVideoArgs) -> None:
"""Create base generation and the optional decoded-video refiner stages."""
refiner = self.get_module("transformer_2")
self.add_stage(
"input_validation_stage",
LingBotVideoInputValidationStage(refiner_enabled=refiner is not None),
)
self.add_stage(
"prompt_encoding_stage",
TextEncodingStage(
text_encoders=[self.get_module("text_encoder")],
tokenizers=[self.get_module("tokenizer")],
),
)
self.add_stage("conditioning_stage", ConditioningStage())
self.add_stage(
"timestep_preparation_stage",
TimestepPreparationStage(scheduler=self.get_module("scheduler")),
)
self.add_stage(
"latent_preparation_stage",
LingBotVideoLatentPreparationStage(transformer=self.get_module("transformer")),
)
self.add_stage(
"denoising_stage",
LingBotVideoDenoisingStage(
transformer=self.get_module("transformer"),
scheduler=self.get_module("scheduler"),
),
)
self.add_stage(
"decoding_stage",
DecodingStage(vae=self.get_module("vae"), pipeline=self),
)
if refiner is not None:
self.add_stage(
"refiner_preparation_stage",
LingBotVideoRefinerPreparationStage(
vae=self.get_module("vae"),
scheduler=self.get_module("scheduler"),
),
)
self.add_stage(
"refiner_denoising_stage",
LingBotVideoDenoisingStage(
transformer=refiner,
scheduler=self.get_module("scheduler"),
refiner=True,
),
)
self.add_stage(
"refiner_decoding_stage",
DecodingStage(vae=self.get_module("vae"), pipeline=self),
)
EntryClass = LingBotVideoPipeline
@@ -0,0 +1,92 @@
# SPDX-License-Identifier: Apache-2.0
"""Official LingBot-Video T2V inference presets."""
from fastvideo.api.presets import InferencePreset, PresetStageSpec
DEFAULT_NEGATIVE_PROMPT = ('{"universal_negative": {"visual_quality": ["low quality", "worst quality", "blurry", '
'"pixelated", "jpeg artifacts", "low resolution", "unstable color", "color flicker", '
'"underexposed", "overexposed", "invisible subject", "subject hidden in darkness"], '
'"artistic_style": ["painting", "illustration", "drawing", "cartoon", "3d render", '
'"cgi", "sketch", "digital art"], "composition_and_content": ["text", "watermark", '
'"signature", "logo", "subtitles", "pillarboxed", "side bars", "portrait image in '
'landscape frame"], "temporal_and_motion_stability": ["flickering", "jittery", '
'"motion blur", "temporal inconsistency", "warping", "morphing", "incoherent motion", '
'"unnatural movement", "static object with sudden jump", "frame-to-frame inconsistency"], '
'"material_and_structure": ["plastic-like glass", "unrealistic texture", "deformed '
'bottle", "liquid freezing improperly", "distorted reflections"]}}')
_DENOISE_STAGE = PresetStageSpec(
name="denoise",
kind="denoising",
description="LingBot-Video batched-CFG denoising",
allowed_overrides=frozenset({"num_inference_steps", "guidance_scale"}),
)
_REFINE_STAGE = PresetStageSpec(
name="refine",
kind="refinement",
description="LingBot-Video pixel-space resize, VAE re-encode, and refiner denoising",
allowed_overrides=frozenset({
"height_sr",
"width_sr",
"num_inference_steps_sr",
"guidance_scale_2",
"t_thresh",
}),
)
LINGBOT_VIDEO_DENSE_T2V = InferencePreset(
name="lingbot_video_dense_t2v",
version=1,
model_family="lingbot_video",
description="LingBot-Video Dense 1.3B text-to-video at 480p",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 480,
"width": 832,
"num_frames": 121,
"fps": 24,
"num_inference_steps": 40,
"guidance_scale": 3.0,
"batch_cfg": True,
"seed": 42,
"negative_prompt": DEFAULT_NEGATIVE_PROMPT,
},
)
LINGBOT_VIDEO_MOE_REFINER_T2V = InferencePreset(
name="lingbot_video_moe_refiner_t2v",
version=1,
model_family="lingbot_video",
description="LingBot-Video MoE 30B-A3B T2V with 1080p refiner",
workload_type="t2v",
stage_schemas=(_DENOISE_STAGE, _REFINE_STAGE),
defaults={
"height": 480,
"width": 832,
"height_sr": 1088,
"width_sr": 1920,
"num_frames": 121,
"fps": 24,
"num_inference_steps": 40,
"num_inference_steps_sr": 8,
"guidance_scale": 3.0,
"guidance_scale_2": 3.0,
"batch_cfg": True,
"t_thresh": 0.85,
"seed": 42,
"negative_prompt": DEFAULT_NEGATIVE_PROMPT,
},
stage_defaults={
"refine": {
"height_sr": 1088,
"width_sr": 1920,
"num_inference_steps_sr": 8,
"guidance_scale_2": 3.0,
"t_thresh": 0.85,
},
},
)
ALL_PRESETS = (LINGBOT_VIDEO_DENSE_T2V, LINGBOT_VIDEO_MOE_REFINER_T2V)
@@ -0,0 +1,345 @@
# SPDX-License-Identifier: Apache-2.0
"""LingBot-Video stages whose contracts differ from shared Wan behavior."""
from __future__ import annotations
import numpy as np
import torch
import torch.nn.functional as F
from fastvideo.distributed import get_local_torch_device
from fastvideo.fastvideo_args import FastVideoArgs
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.pipelines.stages.base import PipelineStage
from fastvideo.pipelines.stages.input_validation import InputValidationStage
LINGBOT_VIDEO_REFINER_TAIL_STEPS = 2
def _compute_refiner_sigmas(
sigma_max: float,
sigma_min: float,
num_inference_steps: int,
shift: float,
t_thresh: float,
) -> np.ndarray:
"""Build the released truncated schedule plus its two-step low-noise tail."""
if not 0.0 < t_thresh <= 1.0:
raise ValueError(f"LingBot-Video refiner t_thresh must be in (0, 1], got {t_thresh}")
if num_inference_steps < 1:
raise ValueError("LingBot-Video refiner requires at least one inference step")
base = np.linspace(sigma_max, sigma_min, num_inference_steps + 1).copy()[:-1]
shifted = shift * base / (1.0 + (shift - 1.0) * base)
sigmas = shifted[shifted <= t_thresh + 1e-6]
if sigmas.size == 0 or abs(float(sigmas[0]) - t_thresh) > 1e-6:
sigmas = np.concatenate(([t_thresh], sigmas))
tail = np.linspace(
float(sigmas[-1]),
min(sigma_min, float(sigmas[-1])),
LINGBOT_VIDEO_REFINER_TAIL_STEPS + 2,
)[1:-1]
sigmas = np.concatenate((sigmas, tail))
if sigmas.size > 1 and not np.all(np.diff(sigmas) < 0.0):
raise ValueError(f"LingBot-Video refiner sigmas must descend strictly, got {sigmas.tolist()}")
return sigmas.astype(np.float32)
class LingBotVideoInputValidationStage(InputValidationStage):
"""Validate released shape constraints and construct the official CUDA RNG."""
def __init__(self, refiner_enabled: bool = False) -> None:
self.refiner_enabled = refiner_enabled
def _generate_seeds(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> None:
"""Use one device-local generator, matching the official production runner."""
del fastvideo_args
if batch.seed is None:
raise ValueError("LingBot-Video requires a seed")
if batch.num_videos_per_prompt != 1:
raise ValueError("LingBot-Video currently supports one video per prompt")
batch.seeds = [batch.seed]
batch.generator = torch.Generator(device=get_local_torch_device()).manual_seed(batch.seed)
def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> ForwardBatch:
"""Run shared validation, then enforce LingBot temporal and spatial geometry."""
batch = super().forward(batch, fastvideo_args)
if not isinstance(batch.num_frames, int):
raise TypeError("LingBot-Video num_frames must be an integer")
if batch.num_frames != 1 and (batch.num_frames - 1) % 4 != 0:
raise ValueError(f"num_frames must be 1 or 4n+1, got {batch.num_frames}")
if not isinstance(batch.height, int) or not isinstance(batch.width, int):
raise TypeError("LingBot-Video height and width must be integers")
if batch.height % 16 != 0 or batch.width % 16 != 0:
raise ValueError(f"height and width must be divisible by 16, got {batch.height}x{batch.width}")
if isinstance(batch.prompt, list) and len(batch.prompt) != 1:
raise ValueError("LingBot-Video currently supports prompt batch size one")
if self.refiner_enabled and fastvideo_args.output_type == "latent":
raise ValueError("LingBot-Video refinement requires decoded pixel output")
return batch
class LingBotVideoLatentPreparationStage(PipelineStage):
"""Prepare fp32 latents in the released 4x temporal and 8x spatial geometry."""
def __init__(self, transformer) -> None:
self.transformer = transformer
def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> ForwardBatch:
"""Generate or validate one normalized fp32 latent video."""
del fastvideo_args
if not all(isinstance(value, int) for value in (batch.num_frames, batch.height, batch.width)):
raise TypeError("latent geometry must contain integer frames, height, and width")
shape = (
1,
self.transformer.num_channels_latents,
(batch.num_frames - 1) // 4 + 1,
batch.height // 8,
batch.width // 8,
)
device = get_local_torch_device()
if batch.latents is None:
batch.latents = torch.randn(
shape,
generator=batch.generator,
device=device,
dtype=torch.float32,
)
else:
if tuple(batch.latents.shape) != shape:
raise ValueError(f"supplied latent shape {tuple(batch.latents.shape)} does not match {shape}")
batch.latents = batch.latents.to(device=device, dtype=torch.float32)
batch.raw_latent_shape = shape
return batch
class LingBotVideoRefinerPreparationStage(PipelineStage):
"""Resize and encode the base video, then initialize the released refiner state."""
performance_component_metric = "vae_encode_time_s"
def __init__(self, vae, scheduler) -> None:
self.vae = vae
self.scheduler = scheduler
@staticmethod
def _resize_video(video: torch.Tensor, height: int, width: int) -> torch.Tensor:
"""Bicubic-resize every decoded frame using the released tensor layout."""
batch, channels, frames, source_height, source_width = video.shape
flat = video.permute(0, 2, 1, 3, 4).reshape(batch * frames, channels, source_height, source_width)
resized = F.interpolate(flat, size=(height, width), mode="bicubic", align_corners=False).clamp(0.0, 1.0)
return resized.reshape(batch, frames, channels, height, width).permute(0, 2, 1, 3, 4).contiguous()
def _encode_video(
self,
video: torch.Tensor,
generator: torch.Generator,
device: torch.device,
) -> torch.Tensor:
"""Encode `[0,1]` pixels and convert Wan VAE latents to normalized DiT space."""
video = video.to(device=device, dtype=torch.float32).mul(2.0).sub(1.0)
with torch.autocast(device_type=device.type, dtype=torch.bfloat16, enabled=device.type == "cuda"):
encoded = self.vae.encode(video)
if hasattr(encoded, "latent_dist"):
latents = encoded.latent_dist.sample(generator)
elif hasattr(encoded, "sample") and callable(encoded.sample):
latents = encoded.sample(generator)
elif isinstance(encoded, tuple | list):
latents = encoded[0]
else:
latents = encoded
mean = torch.tensor(self.vae.config.latents_mean, device=device, dtype=torch.float32).view(1, -1, 1, 1, 1)
std = torch.tensor(self.vae.config.latents_std, device=device, dtype=torch.float32).view(1, -1, 1, 1, 1)
return ((latents.float() - mean) / std).to(latents)
def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> ForwardBatch:
"""Prepare high-resolution refiner latents and its exact truncated sigma schedule."""
if batch.output is None or batch.output.ndim != 5:
raise ValueError("LingBot-Video refinement requires a decoded base video")
if not isinstance(batch.height_sr, int) or not isinstance(batch.width_sr, int):
raise TypeError("LingBot-Video refinement requires integer height_sr and width_sr")
if batch.height_sr % 16 != 0 or batch.width_sr % 16 != 0:
raise ValueError("LingBot-Video refiner height_sr and width_sr must be divisible by 16")
if batch.seed is None:
raise ValueError("LingBot-Video refinement requires a seed")
device = get_local_torch_device()
if isinstance(self.vae, torch.nn.Module):
self.vae.to(device)
generator = torch.Generator(device=device).manual_seed(batch.seed)
resized = self._resize_video(batch.output, batch.height_sr, batch.width_sr)
encoded = self._encode_video(resized, generator, device)
noise = torch.randn(encoded.shape, generator=generator, device=device, dtype=encoded.dtype)
batch.latents = ((1.0 - batch.t_thresh) * encoded + batch.t_thresh * noise).float()
batch.generator = generator
batch.height = batch.height_sr
batch.width = batch.width_sr
batch.raw_latent_shape = tuple(batch.latents.shape)
batch.extra["lingbot_video_base_shape"] = tuple(batch.output.shape)
batch.output = None
shift = fastvideo_args.pipeline_config.flow_shift
if shift is None:
raise ValueError("LingBot-Video refinement requires a flow shift")
sigmas = _compute_refiner_sigmas(
float(self.scheduler.sigma_max),
float(self.scheduler.sigma_min),
batch.num_inference_steps_sr,
float(shift),
float(batch.t_thresh),
)
self.scheduler.set_timesteps(len(sigmas), device=device, sigmas=sigmas, shift=1.0)
batch.timesteps = self.scheduler.timesteps
if getattr(fastvideo_args, "vae_cpu_offload", False):
self.vae.to("cpu")
return batch
class LingBotVideoDenoisingStage(PipelineStage):
"""Run the released batched-CFG bf16 DiT loop with fp32 scheduler state."""
performance_component_metric = "dit_time_s"
def __init__(self, transformer, scheduler, refiner: bool = False) -> None:
self.transformer = transformer
self.scheduler = scheduler
self.refiner = refiner
@staticmethod
def _pad_condition(
embeds: torch.Tensor,
mask: torch.Tensor,
length: int,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Right-pad one condition stream to a shared batched-CFG length."""
pad_length = length - embeds.shape[1]
if pad_length < 0 or embeds.shape[:2] != mask.shape:
raise ValueError("invalid LingBot-Video prompt embedding/mask shapes")
if pad_length == 0:
return embeds, mask
embed_padding = embeds.new_zeros(embeds.shape[0], pad_length, embeds.shape[2])
mask_padding = mask.new_zeros(mask.shape[0], pad_length)
return (
torch.cat((embeds, embed_padding), dim=1),
torch.cat((mask, mask_padding), dim=1),
)
@staticmethod
def _transformer_timestep(timestep: torch.Tensor, dtype: torch.dtype) -> torch.Tensor:
"""Reproduce the official divide-cast-multiply timestep rounding."""
sigma = timestep.float() / 1000.0
if dtype in (torch.bfloat16, torch.float16):
sigma = sigma.to(dtype)
return (sigma * 1000.0).float()
def _prepare_conditions(
self,
batch: ForwardBatch,
dtype: torch.dtype,
device: torch.device,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Pack conditional then unconditional text streams for one batched CFG call."""
prompt = batch.prompt_embeds[0].to(device=device, dtype=dtype)
if batch.prompt_attention_mask is None or not batch.prompt_attention_mask:
raise ValueError("LingBot-Video requires a prompt attention mask")
prompt_mask = batch.prompt_attention_mask[0].to(device=device)
if not self._uses_cfg(batch) or not batch.batch_cfg:
return prompt, prompt_mask
negative, negative_mask = self._negative_condition(batch, prompt, prompt_mask, dtype, device)
target_length = max(prompt.shape[1], negative.shape[1])
prompt, prompt_mask = self._pad_condition(prompt, prompt_mask, target_length)
negative, negative_mask = self._pad_condition(negative, negative_mask, target_length)
return (
torch.cat((prompt, negative), dim=0),
torch.cat((prompt_mask, negative_mask), dim=0),
)
def _negative_condition(
self,
batch: ForwardBatch,
prompt: torch.Tensor,
prompt_mask: torch.Tensor,
dtype: torch.dtype,
device: torch.device,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Use the refiner's zero-cloned null condition or the encoded negative prompt."""
if self.refiner:
return torch.zeros_like(prompt), prompt_mask.clone()
if batch.negative_prompt_embeds is None or not batch.negative_prompt_embeds:
raise ValueError("LingBot-Video CFG requires negative prompt embeddings")
if batch.negative_attention_mask is None or not batch.negative_attention_mask:
raise ValueError("LingBot-Video CFG requires a negative prompt mask")
return (
batch.negative_prompt_embeds[0].to(device=device, dtype=dtype),
batch.negative_attention_mask[0].to(device=device),
)
def _uses_cfg(self, batch: ForwardBatch) -> bool:
"""Enable guidance independently for the base or refiner scale."""
scale = batch.guidance_scale_2 if self.refiner else batch.guidance_scale
return scale is not None and scale > 1.0
def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> ForwardBatch:
"""Denoise latents while keeping scheduler samples and predictions in fp32."""
if batch.latents is None or batch.timesteps is None:
raise ValueError("LingBot-Video denoising requires latents and timesteps")
device = get_local_torch_device()
transformer_dtype = next(self.transformer.parameters()).dtype
condition, condition_mask = self._prepare_conditions(batch, transformer_dtype, device)
latents = batch.latents.to(device=device, dtype=torch.float32)
do_cfg = self._uses_cfg(batch)
negative = negative_mask = None
if do_cfg and not batch.batch_cfg:
negative, negative_mask = self._negative_condition(batch, condition, condition_mask, transformer_dtype,
device)
trajectory: list[torch.Tensor] = []
trajectory_timesteps: list[torch.Tensor] = []
for timestep in batch.timesteps:
timestep_batch = self._transformer_timestep(timestep, transformer_dtype).expand(1).to(device)
latent_input = latents
if do_cfg and batch.batch_cfg:
latent_input = torch.cat((latents, latents), dim=0)
timestep_batch = torch.cat((timestep_batch, timestep_batch), dim=0)
autocast_enabled = device.type == "cuda" and transformer_dtype != torch.float32
with torch.autocast(device_type=device.type, dtype=transformer_dtype, enabled=autocast_enabled):
prediction = self.transformer(
latent_input,
timestep_batch,
condition,
encoder_attention_mask=condition_mask,
return_dict=False,
)[0].float()
if do_cfg:
if batch.batch_cfg:
conditional, unconditional = prediction.chunk(2, dim=0)
else:
with torch.autocast(
device_type=device.type,
dtype=transformer_dtype,
enabled=autocast_enabled,
):
unconditional = self.transformer(
latents,
timestep_batch,
negative,
encoder_attention_mask=negative_mask,
return_dict=False,
)[0].float()
conditional = prediction
guidance_scale = batch.guidance_scale_2 if self.refiner else batch.guidance_scale
if guidance_scale is None:
raise ValueError("LingBot-Video CFG requires a guidance scale")
prediction = unconditional + guidance_scale * (conditional - unconditional)
latents = self.scheduler.step(
prediction,
timestep,
latents,
return_dict=False,
generator=batch.generator,
)[0].float()
if batch.return_trajectory_latents:
trajectory.append(latents.detach().cpu())
trajectory_timesteps.append(timestep.detach().cpu())
batch.latents = latents
if trajectory:
batch.trajectory_latents = torch.stack(trajectory, dim=1)
batch.trajectory_timesteps = trajectory_timesteps
return batch
@@ -0,0 +1,5 @@
from .causal_fast_pipeline import LingBotWorld2CausalFastPipeline
__all__ = ["LingBotWorld2CausalFastPipeline"]
EntryClass = LingBotWorld2CausalFastPipeline
@@ -0,0 +1,365 @@
# SPDX-License-Identifier: Apache-2.0
"""LingBot World 2 causal-fast image-to-video pipeline."""
import math
import os
from einops import rearrange
import numpy as np
import torch
import torchvision.transforms.functional as TF
from fastvideo.distributed import get_local_torch_device
from fastvideo.distributed.parallel_state import get_sp_world_size
from fastvideo.fastvideo_args import FastVideoArgs
from fastvideo.logger import init_logger
from fastvideo.models.dits.lingbotworld2.cam_utils import (
compute_relative_poses,
get_Ks_transformed,
get_plucker_embeddings,
interpolate_camera_poses,
)
from fastvideo.models.schedulers.scheduling_flow_unipc_multistep import (
FlowUniPCMultistepScheduler, )
from fastvideo.pipelines import ComposedPipelineBase, LoRAPipeline
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.pipelines.stages import (
ConditioningStage,
DecodingStage,
InputValidationStage,
TextEncodingStage,
)
from fastvideo.pipelines.stages.base import PipelineStage
logger = init_logger(__name__)
class LingBotWorld2TextEncodingStage(TextEncodingStage):
"""Keep LingBot World 2 T5 attention masks so DiT context matches the source pipeline."""
def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> ForwardBatch:
"""Initialize mask storage before running the shared text-encoding stage."""
if batch.prompt_attention_mask is None:
batch.prompt_attention_mask = []
return super().forward(batch, fastvideo_args)
class LingBotWorld2CausalFastGenerationStage(PipelineStage):
"""Prepare LingBot World 2 conditions and run the released causal-fast sampling loop."""
def __init__(self, transformer, scheduler, vae) -> None:
super().__init__()
self.transformer = transformer
self.scheduler = scheduler
self.vae = vae
self._cross_attn_initialized = False
def _convert_flow_pred_to_x0(
self,
flow_pred: torch.Tensor,
xt: torch.Tensor,
timestep: torch.Tensor,
) -> torch.Tensor:
"""Convert LingBot World 2 flow prediction to x0 using the scheduler sigma."""
original_dtype = flow_pred.dtype
flow_pred, xt, sigmas, timesteps = map(
lambda x: x.double().to(flow_pred.device),
[flow_pred, xt, self.scheduler.sigmas, self.scheduler.timesteps],
)
timestep_id = torch.argmin((timesteps - timestep).abs())
sigma_t = sigmas[timestep_id].reshape(-1, 1, 1, 1)
return (xt - sigma_t * flow_pred).to(original_dtype)
def _initialize_self_kv_cache(
self,
batch_size: int,
kv_size: int,
dtype: torch.dtype,
device: torch.device,
) -> list[dict]:
"""Allocate per-block self-attention KV cache tensors."""
head_dim = self.transformer.dim // self.transformer.num_heads
num_heads = self.transformer.num_heads // get_sp_world_size()
shape = [batch_size, kv_size, num_heads, head_dim]
return [{
"k": torch.zeros(shape, dtype=dtype, device=device),
"v": torch.zeros(shape, dtype=dtype, device=device),
"global_end_index": torch.tensor([0], dtype=torch.long, device=device),
"local_end_index": torch.tensor([0], dtype=torch.long, device=device),
} for _ in range(self.transformer.num_layers)]
def _initialize_crossattn_cache(
self,
batch_size: int,
max_sequence_length: int,
dtype: torch.dtype,
device: torch.device,
) -> list[dict]:
"""Allocate per-block text cross-attention KV cache tensors."""
head_dim = self.transformer.dim // self.transformer.num_heads
shape = [batch_size, max_sequence_length, self.transformer.num_heads, head_dim]
return [{
"k": torch.zeros(shape, dtype=dtype, device=device),
"v": torch.zeros(shape, dtype=dtype, device=device),
"is_init": torch.tensor([0], dtype=torch.bool, device=device),
} for _ in range(self.transformer.num_layers)]
@staticmethod
def _prompt_context(batch: ForwardBatch, device: torch.device) -> list[torch.Tensor]:
"""Slice padded text encoder states back to LingBot World 2's unpadded context list."""
assert batch.prompt_embeds
context_tensor = batch.prompt_embeds[0].to(device)
if batch.prompt_attention_mask:
mask = batch.prompt_attention_mask[0].to(device)
seq_lens = mask.gt(0).sum(dim=1).long()
return [u[:v] for u, v in zip(context_tensor, seq_lens, strict=True)]
return [u for u in context_tensor]
def _prepare_image_tensor(self, batch: ForwardBatch, device: torch.device) -> torch.Tensor:
"""Return the source-style normalized image tensor `[C,H,W]`."""
image = batch.pil_image
if image is None:
raise ValueError("LingBot World 2 causal-fast requires `image_path` or `pil_image`.")
if isinstance(image, torch.Tensor):
if image.ndim == 5:
return image[0, :, 0].to(device)
if image.ndim == 4:
return image[0].to(device)
return image.to(device)
return TF.to_tensor(image).sub_(0.5).div_(0.5).to(device)
def _prepare_camera(
self,
action_path: str,
c2ws: np.ndarray,
h: int,
w: int,
lat_f: int,
lat_h: int,
lat_w: int,
chunk_size: int,
dtype: torch.dtype,
device: torch.device,
) -> torch.Tensor:
"""Build the LingBot World 2 camera Plucker tensor for latent chunks."""
Ks = torch.from_numpy(np.load(os.path.join(action_path, "intrinsics.npy"))).float()
Ks = get_Ks_transformed(
Ks,
height_org=480,
width_org=832,
height_resize=h,
width_resize=w,
height_final=h,
width_final=w,
)
Ks = Ks[0]
len_c2ws = len(c2ws)
len_c2ws_ = int((len_c2ws - 1) // 4) + 1
len_c2ws_ = int(len_c2ws_ - (len_c2ws_ % chunk_size))
c2ws_infer = interpolate_camera_poses(
src_indices=np.linspace(0, len_c2ws - 1, len_c2ws),
src_rot_mat=c2ws[:, :3, :3],
src_trans_vec=c2ws[:, :3, 3],
tgt_indices=np.linspace(0, len_c2ws - 1, len_c2ws_),
)
c2ws_infer = compute_relative_poses(c2ws_infer, framewise=True)
Ks = Ks.repeat(len(c2ws_infer), 1)
c2ws_plucker_emb = get_plucker_embeddings(c2ws_infer.to(device), Ks.to(device), h, w)
c2ws_plucker_emb = rearrange(
c2ws_plucker_emb,
"f (h c1) (w c2) c -> (f h w) (c c1 c2)",
c1=int(h // lat_h),
c2=int(w // lat_w),
)
c2ws_plucker_emb = c2ws_plucker_emb[None, ...]
return rearrange(
c2ws_plucker_emb,
"b (f h w) c -> b c f h w",
f=lat_f,
h=lat_h,
w=lat_w,
).to(device=device, dtype=dtype)
def _encode_condition_video(
self,
img: torch.Tensor,
h: int,
w: int,
frames: int,
mask: torch.Tensor,
fastvideo_args: FastVideoArgs,
) -> torch.Tensor:
"""Encode the first-frame conditioning video and prepend mask channels."""
device = get_local_torch_device()
self.vae = self.vae.to(device)
video_condition = torch.concat(
[
torch.nn.functional.interpolate(
img[None].cpu(),
size=(h, w),
mode="bicubic",
).transpose(0, 1),
torch.zeros(3, frames - 1, h, w),
],
dim=1,
).to(device)
vae_dtype = torch.float32
with torch.autocast(device_type="cuda", dtype=vae_dtype, enabled=False):
encoder_output = self.vae.encode(video_condition.unsqueeze(0).to(torch.float32))
latent_condition = encoder_output.mean
if not bool(getattr(self.vae, "handles_latent_denorm", False)):
if hasattr(self.vae, "shift_factor") and self.vae.shift_factor is not None:
latent_condition -= self.vae.shift_factor.to(latent_condition.device, latent_condition.dtype)
latent_condition = latent_condition * self.vae.scaling_factor.to(latent_condition.device,
latent_condition.dtype)
if fastvideo_args.vae_cpu_offload:
self.vae.to("cpu")
return torch.concat([mask, latent_condition[0]], dim=0)
@torch.no_grad()
def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> ForwardBatch:
"""Execute LingBot World 2 causal-fast generation and store final latents on the batch."""
device = get_local_torch_device()
cfg = fastvideo_args.pipeline_config.dit_config.arch_config
chunk_size = int(cfg.chunk_size)
max_sequence_length = int(batch.max_sequence_length or cfg.text_len)
action_path = batch.action_path
if action_path is None:
raise ValueError("LingBot World 2 causal-fast requires `action_path`.")
c2ws = np.load(os.path.join(action_path, "poses.npy"))
len_c2ws = ((len(c2ws) - 1) // 4) * 4 + 1
frame_num = ((int(batch.num_frames) - 1) // 4) * 4 + 1
frame_num = min(frame_num, len_c2ws)
c2ws = c2ws[:frame_num]
img = self._prepare_image_tensor(batch, device)
h0, w0 = img.shape[1:]
aspect_ratio = h0 / w0
lat_h = round(np.sqrt(cfg.max_area * aspect_ratio) // 8 // cfg.patch_size[1] * cfg.patch_size[1])
lat_w = round(np.sqrt(cfg.max_area / aspect_ratio) // 8 // cfg.patch_size[2] * cfg.patch_size[2])
h = lat_h * 8
w = lat_w * 8
lat_f = (frame_num - 1) // 4 + 1
lat_f = int(lat_f - (lat_f % chunk_size))
frames = (lat_f - 1) * 4 + 1
batch.height = h
batch.width = w
batch.num_frames = frames
seed = int(batch.seed if batch.seed is not None else 42)
seed_g = torch.Generator(device=device)
seed_g.manual_seed(seed)
noise = torch.randn(16, lat_f, lat_h, lat_w, dtype=torch.float32, generator=seed_g, device=device)
mask = torch.ones(1, frames, lat_h, lat_w, device=device)
mask[:, 1:] = 0
mask = torch.concat([torch.repeat_interleave(mask[:, 0:1], repeats=4, dim=1), mask[:, 1:]], dim=1)
mask = mask.view(1, mask.shape[1] // 4, 4, lat_h, lat_w).transpose(1, 2)[0]
self.scheduler.set_timesteps(cfg.num_train_timesteps, shift=cfg.sample_shift)
timesteps = self.scheduler.timesteps[list(cfg.timesteps_index)].to(device)
context = self._prompt_context(batch, device)
c2ws_plucker_emb = self._prepare_camera(
action_path,
c2ws,
h,
w,
lat_f,
lat_h,
lat_w,
chunk_size,
torch.bfloat16,
device,
)
y = self._encode_condition_video(img, h, w, frames, mask, fastvideo_args).to(device=device,
dtype=torch.bfloat16)
transformer_dtype = torch.bfloat16
frame_seqlen = int(noise.shape[-2] * noise.shape[-1] // 4)
kv_size = frame_seqlen * cfg.local_attn_size if cfg.local_attn_size > -1 else frame_seqlen * lat_f
self_kv_cache = self._initialize_self_kv_cache(1, kv_size, transformer_dtype, device)
cross_kv_cache = self._initialize_crossattn_cache(1, max_sequence_length, transformer_dtype, device)
self.transformer = self.transformer.to(device)
self._cross_attn_initialized = False
pred_latent_chunks = []
latents_chunk = noise.split(chunk_size, dim=1)
condition_chunk = y.split(chunk_size, dim=1)
c2ws_plucker_emb_chunk = c2ws_plucker_emb.split(chunk_size, dim=2)
max_seq_len = int(math.ceil(chunk_size * lat_h * lat_w // 4))
with torch.amp.autocast("cuda", dtype=transformer_dtype):
for chunk_id, current_latent in enumerate(latents_chunk):
current_condition = condition_chunk[chunk_id]
current_c2ws_plucker_emb = c2ws_plucker_emb_chunk[chunk_id]
dit_cond_dict = {"c2ws_plucker_emb": current_c2ws_plucker_emb.chunk(1, dim=0)}
kwargs = {
"context": [context[0]],
"seq_len": max_seq_len,
"y": [current_condition],
"dit_cond_dict": dit_cond_dict,
"kv_cache": self_kv_cache,
"crossattn_cache": cross_kv_cache,
"current_start": chunk_id * chunk_size * frame_seqlen,
"max_attention_size": kv_size,
"frame_seqlen": frame_seqlen,
}
x0 = current_latent
for timestep_idx, timestep_value in enumerate(timesteps):
timestep = torch.stack([timestep_value]).to(device)
noise_pred = self.transformer(
x=[current_latent.to(device)],
t=timestep,
cross_attn_first_call=not self._cross_attn_initialized,
**kwargs,
)[0]
self._cross_attn_initialized = True
x0 = self._convert_flow_pred_to_x0(noise_pred, current_latent, timestep_value)
if timestep_idx < len(timesteps) - 1:
next_timestep = timesteps[timestep_idx + 1].reshape(1)
current_latent = self.scheduler.add_noise(
x0,
torch.randn(x0.shape, generator=seed_g, device=x0.device, dtype=x0.dtype),
next_timestep,
)
pred_latent_chunks.append(x0)
context_timestep = torch.stack([timesteps[-1] * 0.0]).to(device)
self.transformer(x=[x0], t=context_timestep, cross_attn_first_call=False, **kwargs)
batch.latents = torch.cat(pred_latent_chunks, dim=1).unsqueeze(0)
return batch
class LingBotWorld2CausalFastPipeline(LoRAPipeline, ComposedPipelineBase):
"""FastVideo pipeline for LingBot World 2 14B causal-fast I2V generation."""
_required_config_modules = ["text_encoder", "tokenizer", "vae", "transformer", "scheduler"]
def initialize_pipeline(self, fastvideo_args: FastVideoArgs):
if "scheduler" not in self.modules:
self.modules["scheduler"] = FlowUniPCMultistepScheduler(shift=1.0, use_dynamic_shifting=False)
def create_pipeline_stages(self, fastvideo_args: FastVideoArgs) -> None:
"""Set up the LingBot World 2 causal-fast pipeline stages."""
self.add_stage(stage_name="input_validation_stage", stage=InputValidationStage())
self.add_stage(
stage_name="prompt_encoding_stage",
stage=LingBotWorld2TextEncodingStage(
text_encoders=[self.get_module("text_encoder")],
tokenizers=[self.get_module("tokenizer")],
),
)
self.add_stage(stage_name="conditioning_stage", stage=ConditioningStage())
self.add_stage(
stage_name="lingbotworld2_causal_fast_generation_stage",
stage=LingBotWorld2CausalFastGenerationStage(
transformer=self.get_module("transformer"),
scheduler=self.get_module("scheduler"),
vae=self.get_module("vae"),
),
)
self.add_stage(stage_name="decoding_stage", stage=DecodingStage(vae=self.get_module("vae")))
EntryClass = LingBotWorld2CausalFastPipeline
@@ -0,0 +1,35 @@
# SPDX-License-Identifier: Apache-2.0
"""LingBotWorld2 causal-fast pipeline preset."""
from fastvideo.api.presets import InferencePreset, PresetStageSpec
_DENOISE_STAGE = PresetStageSpec(
name="denoise",
kind="denoising",
description="Causal-fast denoising pass",
allowed_overrides=frozenset({
"num_inference_steps",
"guidance_scale",
}),
)
LINGBOTWORLD2_CAUSAL_FAST_I2V = InferencePreset(
name="lingbotworld2_causal_fast_i2v",
version=1,
model_family="lingbotworld2",
description="LingBot World 2 14B causal-fast I2V",
workload_type="i2v",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"guidance_scale": 1.0,
"num_inference_steps": 4,
"fps": 16,
"seed": 42,
"num_frames": 65,
"height": 480,
"width": 832,
"negative_prompt": "",
},
)
ALL_PRESETS = (LINGBOTWORLD2_CAUSAL_FAST_I2V, )
@@ -0,0 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
"""Z-Image pipeline."""
from fastvideo.pipelines.basic.zimage.zimage_pipeline import ZImagePipeline
__all__ = ["ZImagePipeline"]
@@ -0,0 +1,40 @@
# SPDX-License-Identifier: Apache-2.0
"""Z-Image inference presets."""
from fastvideo.api.presets import InferencePreset, PresetStageSpec
_DENOISE_STAGE = PresetStageSpec(
name="denoise",
kind="denoising",
description="Z-Image denoising pass",
allowed_overrides=frozenset({
"num_inference_steps",
"guidance_scale",
"cfg_normalization",
"cfg_truncation",
}),
)
ZIMAGE_TURBO = InferencePreset(
name="zimage_turbo",
version=1,
model_family="zimage",
description="Z-Image-Turbo text-to-image generation",
workload_type="t2i",
stage_schemas=(_DENOISE_STAGE, ),
defaults={
"height": 1024,
"width": 1024,
"num_frames": 1,
"fps": 1,
"seed": 42,
"guidance_scale": 0.0,
"num_inference_steps": 8,
"negative_prompt": "",
"max_sequence_length": 512,
"cfg_normalization": False,
"cfg_truncation": 1.0,
},
)
ALL_PRESETS = (ZIMAGE_TURBO, )
+337
View File
@@ -0,0 +1,337 @@
# SPDX-License-Identifier: Apache-2.0
"""Pipeline stages for the native Z-Image text-to-image path."""
from __future__ import annotations
import inspect
import torch
from fastvideo.distributed import get_local_torch_device
from fastvideo.fastvideo_args import FastVideoArgs
from fastvideo.forward_context import set_forward_context
from fastvideo.hooks.activation_trace import trace_step
from fastvideo.pipelines.pipeline_batch_info import ForwardBatch
from fastvideo.pipelines.stages.base import PipelineStage
from fastvideo.pipelines.stages.input_validation import InputValidationStage
from fastvideo.utils import PRECISION_TO_TYPE
class ZImageInputValidationStage(InputValidationStage):
"""Validate the image geometry and reproduce the official device RNG."""
def _generate_seeds(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> None:
del fastvideo_args
assert batch.seed is not None
batch.seeds = [batch.seed]
device = get_local_torch_device()
batch.generator = torch.Generator(device=device).manual_seed(batch.seed)
def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> ForwardBatch:
if batch.do_classifier_free_guidance and batch.negative_prompt is None and not batch.negative_prompt_embeds:
batch.negative_prompt = ""
batch = super().forward(batch, fastvideo_args)
if batch.num_frames != 1:
raise ValueError(f"Z-Image is text-to-image and requires num_frames=1, got {batch.num_frames}")
if batch.height is None or batch.width is None:
raise ValueError("Z-Image requires height and width")
if batch.height % 16 or batch.width % 16:
raise ValueError("Z-Image height and width must be divisible by 16; "
f"got {batch.height}x{batch.width}")
return batch
class ZImageConditioningStage(PipelineStage):
"""Trim padded Qwen states and materialize variable-length CFG streams."""
@staticmethod
def _trim_embeddings(
embeds: torch.Tensor,
attention_mask: torch.Tensor | None,
) -> list[torch.Tensor]:
if attention_mask is None:
return list(embeds.unbind(0))
return [
sample[mask.to(device=sample.device, dtype=torch.bool)]
for sample, mask in zip(embeds, attention_mask, strict=True)
]
@staticmethod
def _repeat(items: list[torch.Tensor], count: int) -> list[torch.Tensor]:
return [item for item in items for _ in range(count)]
@torch.no_grad()
def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> ForwardBatch:
del fastvideo_args
if len(batch.prompt_embeds) != 1:
raise ValueError(f"Z-Image expects one text encoder, got {len(batch.prompt_embeds)}")
prompt_mask = batch.prompt_attention_mask[0] if batch.prompt_attention_mask else None
prompt_embeds = self._trim_embeddings(batch.prompt_embeds[0], prompt_mask)
batch.extra["zimage_prompt_embeds"] = self._repeat(prompt_embeds, batch.num_videos_per_prompt)
if batch.do_classifier_free_guidance:
if not batch.negative_prompt_embeds:
raise ValueError("Z-Image CFG requires negative prompt embeddings")
negative_mask = batch.negative_attention_mask[0] if batch.negative_attention_mask else None
negative_embeds = self._trim_embeddings(batch.negative_prompt_embeds[0], negative_mask)
batch.extra["zimage_negative_prompt_embeds"] = self._repeat(
negative_embeds,
batch.num_videos_per_prompt,
)
else:
batch.extra["zimage_negative_prompt_embeds"] = []
return batch
class ZImageLatentPreparationStage(PipelineStage):
"""Create the official fp32 image latents on the transformer device."""
def __init__(self, transformer) -> None:
self.transformer = transformer
@staticmethod
def _randn(
shape: tuple[int, ...],
generators: torch.Generator | list[torch.Generator] | None,
device: torch.device,
) -> torch.Tensor:
if isinstance(generators, list):
if len(generators) != shape[0]:
raise ValueError(f"generator list length {len(generators)} does not match batch size {shape[0]}")
sample_shape = (1, *shape[1:])
return torch.cat([
torch.randn(sample_shape, generator=generator, device=device, dtype=torch.float32)
for generator in generators
])
return torch.randn(shape, generator=generators, device=device, dtype=torch.float32)
@torch.no_grad()
def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> ForwardBatch:
if batch.height is None or batch.width is None:
raise ValueError("Z-Image requires height and width before latent preparation")
prompt_embeds = batch.extra.get("zimage_prompt_embeds")
if not isinstance(prompt_embeds, list) or not prompt_embeds:
raise ValueError("Z-Image conditioning must run before latent preparation")
channels = int(getattr(self.transformer, "in_channels", 16))
spatial_ratio = int(fastvideo_args.pipeline_config.vae_config.arch_config.spatial_compression_ratio)
shape = (
len(prompt_embeds),
channels,
1,
batch.height // spatial_ratio,
batch.width // spatial_ratio,
)
device = get_local_torch_device()
if batch.latents is None:
latents = self._randn(shape, batch.generator, device)
else:
latents = batch.latents
if latents.ndim == 4:
latents = latents.unsqueeze(2)
if tuple(latents.shape) != shape:
raise ValueError(f"Expected Z-Image latents with shape {shape}, got {tuple(latents.shape)}")
latents = latents.to(device=device, dtype=torch.float32)
batch.latents = latents
batch.raw_latent_shape = shape
return batch
class ZImageTimestepPreparationStage(PipelineStage):
"""Apply the native scheduler's zero endpoint and discrete schedule."""
def __init__(self, scheduler) -> None:
self.scheduler = scheduler
@staticmethod
def _calculate_shift(
image_seq_len: int,
base_seq_len: int,
max_seq_len: int,
base_shift: float,
max_shift: float,
) -> float:
slope = (max_shift - base_shift) / (max_seq_len - base_seq_len)
return image_seq_len * slope + base_shift - slope * base_seq_len
def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> ForwardBatch:
if batch.latents is None:
raise ValueError("Z-Image latents must be prepared before timesteps")
if batch.timesteps is not None and batch.sigmas is not None:
raise ValueError("Only one of timesteps or sigmas may be supplied")
scheduler = self.scheduler
sigma_min = float(fastvideo_args.pipeline_config.scheduler_sigma_min)
use_reference_timesteps = bool(fastvideo_args.pipeline_config.scheduler_use_reference_discrete_timesteps)
scheduler.sigma_min = sigma_min
scheduler.register_to_config(
sigma_min=sigma_min,
use_reference_discrete_timesteps=use_reference_timesteps,
)
config = scheduler.config
image_seq_len = (batch.latents.shape[-2] // 2) * (batch.latents.shape[-1] // 2)
mu = self._calculate_shift(
image_seq_len,
int(config.get("base_image_seq_len", 256)),
int(config.get("max_image_seq_len", 4096)),
float(config.get("base_shift", 0.5)),
float(config.get("max_shift", 1.15)),
)
device = get_local_torch_device()
if batch.timesteps is not None:
if "timesteps" not in inspect.signature(scheduler.set_timesteps).parameters:
raise ValueError(f"{type(scheduler).__name__} does not accept custom timesteps")
scheduler.set_timesteps(timesteps=batch.timesteps, device=device, mu=mu)
elif batch.sigmas is not None:
if "sigmas" not in inspect.signature(scheduler.set_timesteps).parameters:
raise ValueError(f"{type(scheduler).__name__} does not accept custom sigmas")
scheduler.set_timesteps(sigmas=batch.sigmas, device=device, mu=mu)
else:
scheduler.set_timesteps(batch.num_inference_steps, device=device, mu=mu)
batch.timesteps = scheduler.timesteps
batch.num_inference_steps = len(batch.timesteps)
return batch
class ZImageDenoisingStage(PipelineStage):
"""Run the native Z-Image flow-matching loop."""
performance_component_metric = "transformer_time_s"
def __init__(self, transformer, scheduler) -> None:
self.transformer = transformer
self.scheduler = scheduler
@torch.no_grad()
def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> ForwardBatch:
if batch.latents is None or batch.timesteps is None:
raise ValueError("Z-Image denoising requires latents and timesteps")
latents = batch.latents.float()
positive = batch.extra.get("zimage_prompt_embeds")
negative = batch.extra.get("zimage_negative_prompt_embeds", [])
if not isinstance(positive, list) or not positive:
raise ValueError("Z-Image denoising requires prompt embeddings")
device = get_local_torch_device()
target_dtype = PRECISION_TO_TYPE[fastvideo_args.pipeline_config.dit_precision]
batch_size = latents.shape[0]
for index, timestep_value in enumerate(batch.timesteps):
if timestep_value.item() == 0 and index == len(batch.timesteps) - 1:
continue
timestep = timestep_value.expand(batch_size).to(device=device, dtype=torch.float32)
timestep = (1000.0 - timestep) / 1000.0
current_guidance_scale = float(batch.guidance_scale)
if (batch.do_classifier_free_guidance and batch.cfg_truncation is not None and batch.cfg_truncation <= 1.0
and timestep[0].item() > batch.cfg_truncation):
current_guidance_scale = 0.0
apply_cfg = batch.do_classifier_free_guidance and current_guidance_scale > 0.0
if apply_cfg:
if not isinstance(negative, list) or len(negative) != batch_size:
raise ValueError("Z-Image CFG requires one negative embedding per image")
model_latents = latents.to(target_dtype).repeat(2, 1, 1, 1, 1)
model_embeddings = positive + negative
model_timestep = timestep.repeat(2)
else:
model_latents = latents.to(target_dtype)
model_embeddings = positive
model_timestep = timestep
with (
torch.autocast(
device_type=device.type,
enabled=False,
),
trace_step(index),
set_forward_context(
current_timestep=index,
attn_metadata=None,
forward_batch=batch,
),
):
model_outputs = self.transformer(
hidden_states=model_latents,
encoder_hidden_states=model_embeddings,
timestep=model_timestep,
)[0]
if apply_cfg:
positive_outputs = model_outputs[:batch_size]
negative_outputs = model_outputs[batch_size:]
guided_outputs = []
for positive_output, negative_output in zip(
positive_outputs,
negative_outputs,
strict=True,
):
positive_fp32 = positive_output.float()
prediction = positive_fp32 + current_guidance_scale * (positive_fp32 - negative_output.float())
if batch.cfg_normalization:
positive_norm = torch.linalg.vector_norm(positive_fp32)
prediction_norm = torch.linalg.vector_norm(prediction)
if prediction_norm > positive_norm:
prediction = prediction * (positive_norm / prediction_norm)
guided_outputs.append(prediction)
noise_pred = torch.stack(guided_outputs)
else:
noise_pred = torch.stack([output.float() for output in model_outputs])
noise_pred = -noise_pred
latents = self.scheduler.step(
noise_pred,
timestep_value,
latents,
return_dict=False,
)[0].float()
batch.latents = latents
return batch
class ZImageDecodingStage(PipelineStage):
"""Apply the official latent transform and decode one image frame."""
performance_component_metric = "vae_decode_time_s"
def __init__(self, vae) -> None:
self.vae = vae
@torch.no_grad()
def forward(self, batch: ForwardBatch, fastvideo_args: FastVideoArgs) -> ForwardBatch:
if batch.latents is None:
raise ValueError("Z-Image decoding requires latents")
if fastvideo_args.output_type == "latent":
# FastVideo standardizes image and video latents as [B,C,T,H,W].
# Tongyi's image-only API returns the equivalent tensor with T squeezed.
batch.output = batch.latents
return batch
latents = batch.latents
if latents.ndim != 5 or latents.shape[2] != 1:
raise ValueError(f"Expected Z-Image latents [B,C,1,H,W], got {tuple(latents.shape)}")
device = get_local_torch_device()
self.vae = self.vae.to(device)
vae_dtype = getattr(self.vae, "dtype", None)
if vae_dtype is None:
vae_dtype = next(self.vae.parameters()).dtype
config = self.vae.config
scaling_factor = float(config.scaling_factor)
shift_factor = float(config.shift_factor or 0.0)
latents_2d = latents.squeeze(2).to(device=device, dtype=vae_dtype)
latents_2d = latents_2d / scaling_factor + shift_factor
decoded = self.vae.decode(latents_2d, return_dict=False)[0]
decoded = (decoded / 2 + 0.5).clamp(0, 1)
batch.output = decoded.unsqueeze(2).float()
if fastvideo_args.vae_cpu_offload:
self.vae.to("cpu")
return batch
@@ -0,0 +1,63 @@
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
from fastvideo.configs.pipelines.zimage import ZImagePipelineConfig
from fastvideo.fastvideo_args import FastVideoArgs
from fastvideo.pipelines.composed_pipeline_base import ComposedPipelineBase
from fastvideo.pipelines.stages.text_encoding import TextEncodingStage
from .stages import (
ZImageConditioningStage,
ZImageDecodingStage,
ZImageDenoisingStage,
ZImageInputValidationStage,
ZImageLatentPreparationStage,
ZImageTimestepPreparationStage,
)
class ZImagePipeline(ComposedPipelineBase):
"""Native Z-Image text-to-image pipeline."""
pipeline_config_cls: type[ZImagePipelineConfig] = ZImagePipelineConfig
_required_config_modules = [
"scheduler",
"text_encoder",
"tokenizer",
"transformer",
"vae",
]
def create_pipeline_stages(self, fastvideo_args: FastVideoArgs) -> None:
scheduler = self.get_module("scheduler")
transformer = self.get_module("transformer")
self.add_stage("input_validation_stage", ZImageInputValidationStage())
self.add_stage(
"text_encoding_stage",
TextEncodingStage(
text_encoders=[self.get_module("text_encoder")],
tokenizers=[self.get_module("tokenizer")],
),
)
self.add_stage("zimage_conditioning_stage", ZImageConditioningStage())
self.add_stage(
"latent_preparation_stage",
ZImageLatentPreparationStage(transformer=transformer),
)
self.add_stage(
"timestep_preparation_stage",
ZImageTimestepPreparationStage(scheduler=scheduler),
)
self.add_stage(
"denoising_stage",
ZImageDenoisingStage(transformer=transformer, scheduler=scheduler),
)
self.add_stage(
"decoding_stage",
ZImageDecodingStage(vae=self.get_module("vae")),
)
EntryClass = ZImagePipeline
@@ -303,7 +303,8 @@ class ComposedPipelineBase(ABC):
self.modules[module_name] = module
def _load_config(self, model_path: str) -> dict[str, Any]:
model_path = maybe_download_model(self.model_path)
revision = getattr(self.fastvideo_args, "revision", None)
model_path = maybe_download_model(self.model_path, revision=revision)
self.model_path = model_path
# fastvideo_args.downloaded_model_path = model_path
logger.info("Model path: %s", model_path)
+5 -1
View File
@@ -147,8 +147,9 @@ class ForwardBatch:
camera_trajectory: str | None = None # Camera trajectory file/identifier
action_list: list[str] | None = None # List of actions (e.g., ['forward', 'left'])
action_speed_list: list[float] | None = None # Speed for each action
# Camera control inputs (LingBotWorld)
# Camera control inputs (LingBotWorld and LingBotWorld2)
c2ws_plucker_emb: torch.Tensor | None = None # Plucker embedding: [B, C, F_lat, H_lat, W_lat]
action_path: str | None = None # Directory containing poses.npy and intrinsics.npy
# Camera control inputs (GEN3C)
trajectory_type: str | None = None
@@ -177,7 +178,10 @@ class ForwardBatch:
num_inference_steps: int = 50
num_inference_steps_sr: int = 50
guidance_scale: float = 1.0
batch_cfg: bool = False
guidance_scale_2: float | None = None
cfg_normalization: bool = False
cfg_truncation: float | None = 1.0
guidance_rescale: float = 0.0
eta: float = 0.0
sigmas: list[float] | None = None
@@ -0,0 +1,258 @@
# SPDX-License-Identifier: Apache-2.0
"""Preprocess LTX-2 overfit data into parquet format.
Encodes videos with the LTX-2 causal video VAE and captions with the
Gemma text encoder (feature extractor + embedding connector) into the
t2v parquet schema expected by the training framework.
The stored text embeddings are POST-connector: the connector replaces
pad positions with learnable registers and returns an all-valid mask,
so the parquet collate's ones/zeros mask stays semantically correct
and training needs no text encoder at all. Captions are encoded via
the encoder's forward() (the exact inference path), which handles both
LTX-2.0 (shared 3840-d features) and LTX-2.3 (separate 4096-d video /
2048-d audio feature extractors).
Videos are resampled to TRAIN_FPS and the preprocessed clip is also
saved as an mp4 next to the parquet so overfit tests can use it as
the SSIM reference.
Usage:
CUDA_VISIBLE_DEVICES=0 python fastvideo/pipelines/preprocess/preprocess_ltx2_overfit.py
"""
import json
import os
import shutil
from typing import Any
import av
import numpy as np
import pyarrow as pa
import pyarrow.parquet as pq
import torch
from fastvideo.dataset.dataloader.schema import pyarrow_schema_t2v
from fastvideo.utils import maybe_download_model, verify_model_config_and_directory
# --- Config ---
NUM_FRAMES = 81 # 8k+1 for temporal compression ratio 8
MAX_HEIGHT = 480 # divisible by 32 (spatial compression)
MAX_WIDTH = 832
TRAIN_FPS = 24.0 # matches the LTX-2 preset fps used at validation
DATA_DIR = os.environ.get("LTX2_OVERFIT_DATA_DIR", "data/cats")
CAPTION_JSON = os.environ.get("LTX2_OVERFIT_CAPTION_JSON", "videos2caption_1_sample.json")
VIDEO_SUBDIR = os.environ.get("LTX2_OVERFIT_VIDEO_SUBDIR", "video")
OUTPUT_DIR = os.environ.get("LTX2_OVERFIT_OUTPUT_DIR", "data/ltx2_overfit_preprocessed")
MODEL_REPO = os.environ.get("LTX2_OVERFIT_MODEL", "FastVideo/LTX2-Distilled-Diffusers")
# The train dataloader samples with drop_last=True across data-parallel
# groups, so the dataset must hold at least num_sp_groups * batch_size
# rows or every rank gets zero batches. Replicate the overfit sample so
# a 4-GPU FSDP run still sees one batch per rank.
NUM_COPIES = int(os.environ.get("LTX2_OVERFIT_NUM_COPIES", "4"))
def _init_single_process_distributed() -> None:
"""FastVideo component loaders expect an initialized distributed env."""
os.environ.setdefault("MASTER_ADDR", "127.0.0.1")
os.environ.setdefault("MASTER_PORT", "29511")
os.environ.setdefault("RANK", "0")
os.environ.setdefault("WORLD_SIZE", "1")
os.environ.setdefault("LOCAL_RANK", "0")
from fastvideo.distributed import (
maybe_init_distributed_environment_and_model_parallel, )
maybe_init_distributed_environment_and_model_parallel(1, 1)
def load_video(path: str, num_frames: int, target_fps: float, height: int,
width: int) -> tuple[torch.Tensor, np.ndarray]:
"""Load a video as [1, C, T, H, W] in [-1, 1], resampled to target_fps.
Also returns the uint8 RGB frames [T, H, W, C] for reference-video export.
"""
with av.open(path) as container:
if not container.streams.video:
raise RuntimeError(f"No video stream found in {path}")
stream = container.streams.video[0]
native_fps = float(stream.average_rate or target_fps)
decoded = [torch.from_numpy(frame.to_ndarray(format="rgb24")) for frame in container.decode(video=0)]
if not decoded:
raise RuntimeError(f"Could not read any frames from {path}")
raw = torch.stack(decoded).permute(0, 3, 1, 2) # [T, C, H, W] uint8
step = native_fps / target_fps
wanted = [min(int(round(i * step)), raw.shape[0] - 1) for i in range(num_frames)]
frames = raw[wanted].float() # [T, C, H, W] in [0, 255]
src_h, src_w = frames.shape[2], frames.shape[3]
scale = max(height / src_h, width / src_w)
new_h, new_w = int(round(src_h * scale)), int(round(src_w * scale))
frames = torch.nn.functional.interpolate(frames, size=(new_h, new_w), mode="bilinear", antialias=True)
top = (new_h - height) // 2
left = (new_w - width) // 2
frames = frames[:, :, top:top + height, left:left + width]
frames_np = (frames.permute(0, 2, 3, 1).clamp(0, 255).round().to(torch.uint8).numpy())
video = frames / 127.5 - 1.0 # [0,255] -> [-1,1]
video = video.permute(1, 0, 2, 3).unsqueeze(0) # [1,C,T,H,W]
return video, frames_np
def main() -> None:
_init_single_process_distributed()
from fastvideo.fastvideo_args import FastVideoArgs
from fastvideo.models.loader.component_loader import (
PipelineComponentLoader, )
from fastvideo.pipelines.basic.ltx2.pipeline_configs import LTX2T2VConfig
device = torch.device("cuda:0")
model_path = maybe_download_model(MODEL_REPO)
model_index = verify_model_config_and_directory(model_path)
os.makedirs(OUTPUT_DIR, exist_ok=True)
# The map-style dataset caches parquet file metadata; a stale cache
# next to a regenerated parquet can crash or serve old rows.
shutil.rmtree(os.path.join(OUTPUT_DIR, "map_style_cache"), ignore_errors=True)
with open(os.path.join(DATA_DIR, CAPTION_JSON)) as f:
caption_data = json.load(f)
pipeline_config = LTX2T2VConfig()
fastvideo_args = FastVideoArgs(
model_path=model_path,
pipeline_config=pipeline_config,
num_gpus=1,
tp_size=1,
sp_size=1,
hsdp_shard_dim=1,
vae_cpu_offload=False,
text_encoder_cpu_offload=False,
)
def load_component(name: str) -> Any:
transformers_or_diffusers, _ = model_index[name]
return PipelineComponentLoader.load_module(
module_name=name,
component_model_path=os.path.join(model_path, name),
transformers_or_diffusers=transformers_or_diffusers,
fastvideo_args=fastvideo_args,
)
print("Loading LTX-2 VAE...")
vae = load_component("vae")
vae_dtype = next(vae.parameters()).dtype
print(f"VAE loaded ({sum(p.numel() for p in vae.parameters())/1e6:.0f}M, {vae_dtype})")
print("Loading Gemma text encoder + tokenizer...")
text_encoder = load_component("text_encoder")
tokenizer = load_component("tokenizer")
tokenizer.padding_side = "left"
if tokenizer.pad_token is None and tokenizer.eos_token is not None:
tokenizer.pad_token = tokenizer.eos_token
encoder_config = pipeline_config.text_encoder_configs[0]
tokenizer_kwargs = dict(encoder_config.tokenizer_kwargs)
if "max_length" not in tokenizer_kwargs:
tokenizer_kwargs["max_length"] = encoder_config.arch_config.text_len
preprocess_text = pipeline_config.preprocess_text_funcs[0]
# --- Process each video ---
records = []
for idx, item in enumerate(caption_data):
video_name = item["path"]
record_id = f"{idx:04d}_{video_name}"
caption = item["cap"][0] if isinstance(item["cap"], list) else item["cap"]
video_path = os.path.join(DATA_DIR, VIDEO_SUBDIR, video_name)
print(f"\nProcessing: {video_name}")
print(f" Caption: {caption[:80]}...")
video, frames_np = load_video(video_path, NUM_FRAMES, TRAIN_FPS, MAX_HEIGHT, MAX_WIDTH)
video = video.to(device=device, dtype=vae_dtype)
print(f" Video shape: {video.shape}")
with torch.no_grad():
# LTX-2 encode() returns a deterministic distribution whose
# mean is already per-channel normalized; store as-is.
latent = vae.encode(video).mean.squeeze(0).float().cpu()
print(f" Latent shape: {latent.shape}")
with torch.no_grad():
# Encode through forward() — the inference text path. The
# two-step preprocess_text_embeddings + run_connectors route
# breaks on LTX-2.3, whose separate audio feature extractor
# is narrower than the video one.
text_inputs = tokenizer([preprocess_text(caption)], **tokenizer_kwargs)
encoder_out = text_encoder(
input_ids=text_inputs["input_ids"].to(device),
attention_mask=text_inputs["attention_mask"].to(device),
)
text_embedding = encoder_out.last_hidden_state.squeeze(0).float().cpu()
print(f" Text embedding shape: {text_embedding.shape}")
record = {
"id": record_id,
"vae_latent_bytes": latent.numpy().tobytes(),
"vae_latent_shape": list(latent.shape),
"vae_latent_dtype": str(latent.dtype).replace("torch.", ""),
"text_embedding_bytes": (text_embedding.numpy().tobytes()),
"text_embedding_shape": list(text_embedding.shape),
"text_embedding_dtype": str(text_embedding.dtype).replace("torch.", ""),
"file_name": video_name,
"caption": caption,
"media_type": "video",
"width": MAX_WIDTH,
"height": MAX_HEIGHT,
"num_frames": NUM_FRAMES,
"duration_sec": NUM_FRAMES / TRAIN_FPS,
"fps": TRAIN_FPS,
}
records.append(record)
# Save the preprocessed clip so overfit tests can compare
# validation output against the memorization target.
import imageio
ref_path = os.path.join(OUTPUT_DIR, f"training_sample_{idx}.mp4")
with imageio.get_writer(ref_path, fps=TRAIN_FPS) as writer:
for frame in frames_np:
writer.append_data(frame)
print(f" Wrote reference clip to {ref_path}")
# Clean up
del text_encoder, tokenizer, vae
torch.cuda.empty_cache()
# Write parquet (replicated NUM_COPIES times; see comment at top)
replicated = []
for copy_idx in range(max(1, NUM_COPIES)):
for r in records:
row = dict(r)
row["id"] = f"{r['id']}_copy{copy_idx}"
replicated.append(row)
table = pa.table(
{k: [r[k] for r in replicated]
for k in replicated[0]},
schema=pyarrow_schema_t2v,
)
output_path = os.path.join(OUTPUT_DIR, "data_00000.parquet")
pq.write_table(table, output_path)
print(f"\nWrote {len(replicated)} records "
f"({len(records)} unique x {max(1, NUM_COPIES)} copies) to {output_path}")
# Write validation prompts for the validation callback
val_prompts = {
"data": [{
"caption": (item["cap"][0] if isinstance(item["cap"], list) else item["cap"]),
} for item in caption_data],
}
val_path = os.path.join(OUTPUT_DIR, "validation_prompts.json")
with open(val_path, "w") as f:
json.dump(val_prompts, f, indent=2)
print(f"Wrote validation prompts to {val_path}")
print("\nDone! Use data_path: " + OUTPUT_DIR + " in training config.")
if __name__ == "__main__":
main()
+45 -16
View File
@@ -65,7 +65,11 @@ class DecodingStage(PipelineStage):
ch_dim = 1 if latents.ndim >= 4 else -1
return latents.shape[ch_dim] == packed_ch
def _denormalize_latents(self, latents: torch.Tensor) -> torch.Tensor:
def _denormalize_latents(
self,
latents: torch.Tensor,
fastvideo_args: FastVideoArgs,
) -> torch.Tensor:
"""Convert normalized latents into the VAE's expected latent space."""
# Some VAEs handle latent (de)normalization internally.
if bool(getattr(self.vae, "handles_latent_denorm", False)):
@@ -73,25 +77,45 @@ class DecodingStage(PipelineStage):
cfg = getattr(self.vae, "config", None)
# Matrix-Game 2.0-style: z = z * std + mean
if (cfg is not None and hasattr(cfg, "latents_mean") and hasattr(cfg, "latents_std")):
latents_mean = torch.tensor(cfg.latents_mean, device=latents.device,
# Matrix-Game 2.0-style: z = z * std + mean. Some configs declare these
# fields as None, so test the values rather than mere attribute presence
# -- otherwise torch.tensor(None) raises instead of falling through.
latents_mean_value = getattr(cfg, "latents_mean", None) if cfg is not None else None
latents_std_value = getattr(cfg, "latents_std", None) if cfg is not None else None
if latents_mean_value is not None and latents_std_value is not None:
# Preserve the released LingBot reciprocal arithmetic and its float32 rounding for numeric alignment
if type(fastvideo_args.pipeline_config).__name__.startswith("LingBotVideo"):
latents_mean = torch.tensor(latents_mean_value, device=latents.device,
dtype=torch.float32).view(1, -1, 1, 1, 1)
latents_std_inv = 1.0 / torch.tensor(latents_std_value, device=latents.device,
dtype=torch.float32).view(1, -1, 1, 1, 1)
# See Lingbot: https://github.com/Robbyant/lingbot-video/blob/a638721cf2271804d02738b69f2ad788c4a559fc/lingbot_video/pipeline_lingbot_video.py#L282
return latents.float() / latents_std_inv + latents_mean
latents_mean = torch.tensor(latents_mean_value, device=latents.device,
dtype=latents.dtype).view(1, -1, 1, 1, 1)
latents_std = torch.tensor(cfg.latents_std, device=latents.device, dtype=latents.dtype).view(1, -1, 1, 1, 1)
latents_std = torch.tensor(latents_std_value, device=latents.device,
dtype=latents.dtype).view(1, -1, 1, 1, 1)
return latents * latents_std + latents_mean
# Diffusers-style: scaling_factor (+ optional shift_factor)
if hasattr(self.vae, "scaling_factor"):
if isinstance(self.vae.scaling_factor, torch.Tensor):
latents = latents / self.vae.scaling_factor.to(latents.device, latents.dtype)
# Diffusers-style: scaling_factor (+ optional shift_factor), read from
# the module only. Nearly every VAE *config* also carries a
# scaling_factor whose decode() already accounts for it, so sourcing it
# from cfg here would newly divide latents for VAEs that have always
# passed through untouched (flux2, hunyuanvideo, hunyuanvideo15, ltx2).
scaling_factor = getattr(self.vae, "scaling_factor", None)
if scaling_factor is not None:
if isinstance(scaling_factor, torch.Tensor):
latents = latents / scaling_factor.to(latents.device, latents.dtype)
else:
latents = latents / self.vae.scaling_factor
latents = latents / scaling_factor
if hasattr(self.vae, "shift_factor") and self.vae.shift_factor is not None:
if isinstance(self.vae.shift_factor, torch.Tensor):
latents = latents + self.vae.shift_factor.to(latents.device, latents.dtype)
shift_factor = getattr(self.vae, "shift_factor", None)
if shift_factor is not None:
if isinstance(shift_factor, torch.Tensor):
latents = latents + shift_factor.to(latents.device, latents.dtype)
else:
latents = latents + self.vae.shift_factor
latents = latents + shift_factor
return latents
@@ -145,7 +169,12 @@ class DecodingStage(PipelineStage):
# Flux2: skip denormalize on packed latents; BN denorm runs below instead
if not (latents.ndim == 5 and self._is_flux2_packed(latents)):
latents = self._denormalize_latents(latents)
latents = self._denormalize_latents(latents, fastvideo_args)
# The released LingBot decoder enters Wan VAE in channels-last-3d
# layout; matching that boundary avoids selecting a different Conv3d path.
if (type(fastvideo_args.pipeline_config).__name__.startswith("LingBotVideo") and latents.ndim == 5):
latents = latents.contiguous(memory_format=torch.channels_last_3d)
# Decode latents
with torch.autocast(device_type="cuda", dtype=vae_dtype, enabled=vae_autocast_enabled):
@@ -210,7 +239,7 @@ class DecodingStage(PipelineStage):
vae_dtype = PRECISION_TO_TYPE[decode_precision]
vae_autocast_enabled = (vae_dtype != torch.float32) and not fastvideo_args.disable_autocast
latents = self._denormalize_latents(latents)
latents = self._denormalize_latents(latents, fastvideo_args)
# Initialize cache if needed
if cache is None:
+20 -4
View File
@@ -73,6 +73,7 @@ class TextEncodingStage(PipelineStage):
fastvideo_args,
encoder_index=all_indices,
return_attention_mask=True,
max_length=batch.max_sequence_length,
)
if self._last_audio_embeds is not None:
batch.extra["ltx2_audio_prompt_embeds"] = self._last_audio_embeds
@@ -91,6 +92,7 @@ class TextEncodingStage(PipelineStage):
fastvideo_args,
encoder_index=all_indices,
return_attention_mask=True,
max_length=batch.max_sequence_length,
)
if self._last_audio_embeds is not None:
batch.extra["ltx2_audio_negative_embeds"] = self._last_audio_embeds
@@ -221,6 +223,20 @@ class TextEncodingStage(PipelineStage):
encoder_device = torch.device(target_device)
moved_for_forward = True
# An explicit `device=` wins. Otherwise follow the encoder's real
# param device. Once it has been moved for the forward that is the
# target device, and an HF-passthrough encoder's
# _fastvideo_input_device (stamped at load, e.g. "cpu" under
# text_encoder_cpu_offload) is stale -- honouring it would feed cpu
# token ids to cuda weights. The marker only speaks when nothing
# moved and the module has no parameters to speak for it.
if device is not None:
input_device = torch.device(target_device)
elif moved_for_forward:
input_device = encoder_device
else:
input_device = getattr(text_encoder, "_fastvideo_input_device", encoder_device)
tok_kwargs = dict(encoder_config.tokenizer_kwargs)
if max_length is not None:
tok_kwargs["max_length"] = max_length
@@ -264,7 +280,7 @@ class TextEncodingStage(PipelineStage):
# pre-format prompts into message lists upstream and rely on
# the inner tokenizer + full tokenizer_kwargs (which include
# add_generation_prompt). Preserve that original path exactly.
text_inputs = tok.apply_chat_template(processed_texts, **tok_kwargs).to(encoder_device)
text_inputs = tok.apply_chat_template(processed_texts, **tok_kwargs).to(input_device)
else:
# Two-step approach matching Diffusers: format with chat
# template first, then tokenize the resulting strings.
@@ -275,12 +291,12 @@ class TextEncodingStage(PipelineStage):
messages,
tokenize=False,
add_generation_prompt=True,
enable_thinking=False,
enable_thinking=encoder_config.chat_template_enable_thinking,
)
formatted_texts.append(formatted)
text_inputs = tokenizer(formatted_texts, **tok_kwargs).to(encoder_device)
text_inputs = tokenizer(formatted_texts, **tok_kwargs).to(input_device)
else:
text_inputs = tok(processed_texts, **tok_kwargs).to(encoder_device)
text_inputs = tok(processed_texts, **tok_kwargs).to(input_device)
input_ids = text_inputs["input_ids"]
attention_mask = text_inputs["attention_mask"]
+67 -1
View File
@@ -29,7 +29,9 @@ from fastvideo.configs.pipelines.hunyuan15 import (Hunyuan15T2V480PConfig, Hunyu
Hunyuan15SR1080PConfig)
from fastvideo.configs.pipelines.hyworld import HYWorldConfig
from fastvideo.configs.pipelines.kandinsky5 import Kandinsky5I2VConfig, Kandinsky5T2VConfig
from fastvideo.configs.pipelines.lingbot_video import LingBotVideoT2VConfig
from fastvideo.configs.pipelines.lingbotworld import LingBotWorldI2V480PConfig
from fastvideo.configs.pipelines.lingbotworld2 import LingBotWorld2CausalFastI2V480PConfig
from fastvideo.configs.pipelines.longcat import LongCatT2V480PConfig
from fastvideo.pipelines.basic.ltx2.pipeline_configs import LTX2T2VConfig
from fastvideo.configs.pipelines.flux_2 import (
@@ -62,6 +64,7 @@ from fastvideo.configs.pipelines.glm_image import GlmImageConfig
from fastvideo.configs.pipelines.flux import FluxPipelineConfig
from fastvideo.configs.pipelines.sd35 import SD35Config
from fastvideo.configs.pipelines.stable_audio import (StableAudioOpenSmallConfig, StableAudioT2AConfig)
from fastvideo.configs.pipelines.zimage import ZImagePipelineConfig
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.api.matrixgame2 import MatrixGame2SamplingParam
from fastvideo.api.matrixgame3 import MatrixGame3SamplingParam
@@ -488,6 +491,22 @@ def _register_configs() -> None:
model_family="gamecraft",
default_preset="gamecraft_i2v",
)
# LingBotWorld2 causal-fast
register_configs(
sampling_param_cls=None,
pipeline_config_cls=LingBotWorld2CausalFastI2V480PConfig,
workload_types=(WorkloadType.I2V, ),
hf_model_paths=[
"robbyant/lingbot-world-v2-14b-causal-fast",
],
model_detectors=[
lambda path:
("lingbot-world-v2-14b-causal-fast" in path.lower() or "lingbotworld2causalfastpipeline" in path.lower())
],
model_family="lingbotworld2",
default_preset="lingbotworld2_causal_fast_i2v",
)
# LingBotWorld
register_configs(
sampling_param_cls=None,
@@ -496,10 +515,35 @@ def _register_configs() -> None:
hf_model_paths=[
"FastVideo/LingBot-World-Base-Cam-Diffusers",
],
model_detectors=[lambda path: ("lingbotworld" in path.lower() or "lingbot-world" in path.lower())],
model_detectors=[
lambda path: (("lingbotworld" in path.lower() or "lingbot-world" in path.lower()) and "causal-fast" not in
path.lower() and "causalfast" not in path.lower())
],
model_family="lingbotworld",
default_preset="lingbotworld_i2v",
)
# LingBot-Video MoE T2V with the released second-stage refiner.
register_configs(
sampling_param_cls=None,
pipeline_config_cls=LingBotVideoT2VConfig,
workload_types=(WorkloadType.T2V, ),
hf_model_paths=["FastVideo/LingBot-Video-MoE-30B-A3B-Diffusers"],
model_detectors=[lambda path: "lingbotvideomoepipeline" in path.lower()],
model_family="lingbot_video",
default_preset="lingbot_video_moe_refiner_t2v",
pipeline_cls_name="LingBotVideoPipeline",
)
# LingBot-Video Dense T2V
register_configs(
sampling_param_cls=None,
pipeline_config_cls=LingBotVideoT2VConfig,
workload_types=(WorkloadType.T2V, ),
hf_model_paths=["FastVideo/LingBot-Video-Dense-1.3B-Diffusers"],
model_detectors=[lambda path: "lingbotvideodensepipeline" in path.lower()],
model_family="lingbot_video",
default_preset="lingbot_video_dense_t2v",
pipeline_cls_name="LingBotVideoPipeline",
)
def _kandinsky5_detector(require: tuple[str, ...] = (), exclude: tuple[str, ...] = ()) -> Callable[[str], bool]:
@@ -1104,6 +1148,19 @@ def _register_configs() -> None:
],
)
# Z-Image-Turbo
register_configs(
sampling_param_cls=None,
pipeline_config_cls=ZImagePipelineConfig,
workload_types=(WorkloadType.T2I, ),
hf_model_paths=["Tongyi-MAI/Z-Image-Turbo"],
model_detectors=[
lambda path: "zimagepipeline" in path or "z-image" in path or "z_image" in path,
],
model_family="zimage",
default_preset="zimage_turbo",
)
# --- Part 3: Main Resolver ---
@@ -1210,6 +1267,10 @@ def _register_presets() -> None:
ALL_PRESETS as KANDINSKY5_PRESETS, )
from fastvideo.pipelines.basic.lingbotworld.presets import (
ALL_PRESETS as LINGBOTWORLD_PRESETS, )
from fastvideo.pipelines.basic.lingbotworld2.presets import (
ALL_PRESETS as LINGBOTWORLD2_PRESETS, )
from fastvideo.pipelines.basic.lingbot_video.presets import (
ALL_PRESETS as LINGBOT_VIDEO_PRESETS, )
from fastvideo.pipelines.basic.longcat.presets import (
ALL_PRESETS as LONGCAT_PRESETS, )
from fastvideo.pipelines.basic.ltx2.presets import (
@@ -1226,6 +1287,8 @@ def _register_presets() -> None:
ALL_PRESETS as TURBODIFFUSION_PRESETS, )
from fastvideo.pipelines.basic.wan.presets import (
ALL_PRESETS as WAN_PRESETS, )
from fastvideo.pipelines.basic.zimage.presets import (
ALL_PRESETS as ZIMAGE_PRESETS, )
from fastvideo.pipelines.basic.flux_2.presets import (
ALL_PRESETS as FLUX2_PRESETS, )
@@ -1239,7 +1302,9 @@ def _register_presets() -> None:
HUNYUAN15_PRESETS,
HYWORLD_PRESETS,
KANDINSKY5_PRESETS,
LINGBOT_VIDEO_PRESETS,
LINGBOTWORLD_PRESETS,
LINGBOTWORLD2_PRESETS,
LONGCAT_PRESETS,
LTX2_PRESETS,
MATRIXGAME2_PRESETS,
@@ -1248,6 +1313,7 @@ def _register_presets() -> None:
STABLE_AUDIO_PRESETS,
TURBODIFFUSION_PRESETS,
WAN_PRESETS,
ZIMAGE_PRESETS,
)
for group in all_preset_groups:
for preset in group:
+32 -7
View File
@@ -8,8 +8,11 @@ from __future__ import annotations
from fastvideo.api.compat import (
generator_config_to_fastvideo_args,
legacy_from_pretrained_to_config,
request_to_sampling_param,
)
from fastvideo.api.schema import CompileConfig, GeneratorConfig
from fastvideo.api.parser import parse_config
from fastvideo.api.schema import CompileConfig, GenerationRequest, GeneratorConfig
from fastvideo.api.sampling_param import SamplingParam
class TestLegacyTorchCompileKwargsTranslation:
@@ -35,8 +38,7 @@ class TestCompileConfigRoundTrip:
_stub_fastvideo_args_from_kwargs(monkeypatch)
config = GeneratorConfig(
model_path="/models/ltx2",
engine=_engine_with_compile(
CompileConfig(enabled=True, backend="inductor", fullgraph=True)),
engine=_engine_with_compile(CompileConfig(enabled=True, backend="inductor", fullgraph=True)),
)
args = generator_config_to_fastvideo_args(config)
assert args.kwargs["enable_torch_compile"] is True
@@ -53,13 +55,17 @@ class TestCompileConfigRoundTrip:
CompileConfig(
enabled=True,
mode="reduce-overhead",
extras={"options": {"triton.cudagraphs": False}},
extras={"options": {
"triton.cudagraphs": False
}},
)),
)
args = generator_config_to_fastvideo_args(config)
assert args.kwargs["torch_compile_kwargs"] == {
"mode": "reduce-overhead",
"options": {"triton.cudagraphs": False},
"options": {
"triton.cudagraphs": False
},
}
def test_none_fields_suppressed(self, monkeypatch) -> None:
@@ -137,8 +143,7 @@ class TestLegacyTextEncoderCompileTranslation:
_stub_fastvideo_args_from_kwargs(monkeypatch)
config = GeneratorConfig(
model_path="/models/ltx2",
engine=_engine_with_compile(
CompileConfig(text_encoder_enabled=True)),
engine=_engine_with_compile(CompileConfig(text_encoder_enabled=True)),
)
args = generator_config_to_fastvideo_args(config)
assert args.kwargs["enable_torch_compile_text_encoder"] is True
@@ -153,6 +158,26 @@ class TestLegacyTextEncoderCompileTranslation:
assert "enable_torch_compile_text_encoder" not in args.kwargs
def test_batch_cfg_typed_request_reaches_sampling_param(monkeypatch) -> None:
"""Preserve explicit batched CFG through the canonical request adapter."""
monkeypatch.setattr(
SamplingParam,
"from_pretrained",
classmethod(lambda cls, model_path: cls()),
)
request = parse_config(
GenerationRequest,
{
"prompt": "fox",
"sampling": {
"batch_cfg": True
}
},
)
sampling_param = request_to_sampling_param(request, model_path="test-model")
assert sampling_param.batch_cfg is True
# -------------------------------------------------------------------
# Helpers
# -------------------------------------------------------------------
+44 -21
View File
@@ -41,15 +41,15 @@ def test_parse_config_builds_nested_typed_config() -> None:
},
"state": {
"kind": "ltx2_continuation",
"payload": {"segment_index": 1},
"payload": {
"segment_index": 1
},
},
"plan": {
"stages": [
{
"name": "base",
"kind": "sample",
}
]
"stages": [{
"name": "base",
"kind": "sample",
}]
},
},
}
@@ -77,10 +77,14 @@ def test_parse_config_accepts_existing_typed_instance() -> None:
def test_load_run_config_supports_yaml_roundtrip(tmp_path) -> None:
raw = {
"generator": {"model_path": "/models/wan"},
"generator": {
"model_path": "/models/wan"
},
"request": {
"prompt": "hello",
"sampling": {"num_frames": 16},
"sampling": {
"num_frames": 16
},
},
}
path = tmp_path / "run.yaml"
@@ -165,12 +169,14 @@ def test_load_run_config_supports_yaml_roundtrip(tmp_path) -> None:
"keyboard_cond": None,
"grid_sizes": None,
"c2ws_plucker_emb": None,
"action_path": None,
"refine_from": None,
"stage1_video": None,
},
"sampling": {
"num_videos_per_prompt": 1,
"seed": 1024,
"max_sequence_length": None,
"num_frames": 16,
"height": 720,
"width": 1280,
@@ -180,7 +186,10 @@ def test_load_run_config_supports_yaml_roundtrip(tmp_path) -> None:
"num_inference_steps": 50,
"num_inference_steps_sr": 50,
"guidance_scale": 1.0,
"batch_cfg": False,
"guidance_scale_2": None,
"cfg_normalization": False,
"cfg_truncation": 1.0,
"guidance_rescale": 0.0,
"true_cfg_scale": None,
"use_embedded_guidance": None,
@@ -209,9 +218,15 @@ def test_load_run_config_supports_yaml_roundtrip(tmp_path) -> None:
def test_load_serve_config_supports_json_roundtrip(tmp_path) -> None:
raw = {
"generator": {"model_path": "/models/server"},
"server": {"port": 9000},
"default_request": {"prompt": "serve default"},
"generator": {
"model_path": "/models/server"
},
"server": {
"port": 9000
},
"default_request": {
"prompt": "serve default"
},
}
path = tmp_path / "serve.json"
path.write_text(json.dumps(raw), encoding="utf-8")
@@ -231,7 +246,9 @@ def test_serve_config_streaming_defaults_to_none() -> None:
def test_serve_config_parses_streaming_block() -> None:
raw = {
"generator": {"model_path": "/models/server"},
"generator": {
"model_path": "/models/server"
},
"streaming": {
"session_timeout_seconds": 120,
"generation_segment_cap": 4,
@@ -266,8 +283,7 @@ def test_serve_config_parses_streaming_block() -> None:
assert loaded.streaming.session_timeout_seconds == 120
assert loaded.streaming.generation_segment_cap == 4
assert loaded.streaming.stream_mode == "legacy_jpeg"
assert loaded.streaming.warmup == WarmupConfig(
enabled=False, prompt="warmup prompt", timeout_seconds=600)
assert loaded.streaming.warmup == WarmupConfig(enabled=False, prompt="warmup prompt", timeout_seconds=600)
assert loaded.streaming.pool == GpuPoolConfig(
num_workers=2,
enable_audio_reencode=False,
@@ -280,14 +296,17 @@ def test_serve_config_parses_streaming_block() -> None:
timeout_ms=10000,
system_prompt_dir="/opt/prompts",
)
assert loaded.streaming.safety == PromptSafetyConfig(
enabled=True, classifier_path="/opt/safety.pt")
assert loaded.streaming.safety == PromptSafetyConfig(enabled=True, classifier_path="/opt/safety.pt")
def test_serve_config_streaming_round_trip_through_config_to_dict() -> None:
raw = {
"generator": {"model_path": "/models/server"},
"streaming": {"session_timeout_seconds": 600},
"generator": {
"model_path": "/models/server"
},
"streaming": {
"session_timeout_seconds": 600
},
}
loaded = parse_config(ServeConfig, raw)
dumped = config_to_dict(loaded)
@@ -300,10 +319,14 @@ def test_serve_config_streaming_round_trip_through_config_to_dict() -> None:
def test_load_serve_config_with_streaming_from_yaml(tmp_path) -> None:
raw = {
"generator": {"model_path": "/models/server"},
"generator": {
"model_path": "/models/server"
},
"streaming": {
"stream_mode": "av_fmp4",
"pool": {"num_workers": 4},
"pool": {
"num_workers": 4
},
},
}
path = tmp_path / "serve.yaml"
+173 -73
View File
@@ -1,4 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
import json
import pytest
from fastvideo.api.errors import ConfigValidationError
@@ -14,11 +16,11 @@ from fastvideo.api.presets import (
validate_stage_overrides,
)
# -------------------------------------------------------------------
# Fixtures
# -------------------------------------------------------------------
@pytest.fixture()
def _isolated_registry():
"""Run each test with an empty preset registry, restoring after."""
@@ -49,11 +51,11 @@ _NO_OVERRIDES_STAGE = PresetStageSpec(
def _make_preset(
name: str = "test_preset",
version: int = 1,
model_family: str = "test",
stage_schemas: tuple[PresetStageSpec, ...] = (_SIMPLE_STAGE, ),
**kwargs,
name: str = "test_preset",
version: int = 1,
model_family: str = "test",
stage_schemas: tuple[PresetStageSpec, ...] = (_SIMPLE_STAGE, ),
**kwargs,
) -> InferencePreset:
return InferencePreset(
name=name,
@@ -112,8 +114,7 @@ class TestRegistration:
def test_get_presets_for_family(self) -> None:
register_preset(_make_preset(name="a"))
register_preset(_make_preset(name="b"))
register_preset(_make_preset(
name="c", model_family="other"))
register_preset(_make_preset(name="c", model_family="other"))
result = get_presets_for_family("test")
assert {p.name for p in result} == {"a", "b"}
@@ -133,14 +134,11 @@ class TestStageNameValidation:
def test_valid_stage_name_passes(self) -> None:
preset = _make_preset(stage_schemas=(_SIMPLE_STAGE, _SR_STAGE))
validate_stage_names(
preset, {"denoise": {}, "sr": {}})
validate_stage_names(preset, {"denoise": {}, "sr": {}})
def test_unknown_stage_name_raises(self) -> None:
preset = _make_preset(stage_schemas=(_SIMPLE_STAGE, ))
with pytest.raises(
ConfigValidationError, match="stage_overrides.bogus"
):
with pytest.raises(ConfigValidationError, match="stage_overrides.bogus"):
validate_stage_names(preset, {"bogus": {}})
def test_empty_overrides_passes(self) -> None:
@@ -148,11 +146,8 @@ class TestStageNameValidation:
validate_stage_names(preset, {})
def test_error_lists_valid_stages(self) -> None:
preset = _make_preset(
stage_schemas=(_SIMPLE_STAGE, _SR_STAGE))
with pytest.raises(
ConfigValidationError, match="'denoise'"
):
preset = _make_preset(stage_schemas=(_SIMPLE_STAGE, _SR_STAGE))
with pytest.raises(ConfigValidationError, match="'denoise'"):
validate_stage_names(preset, {"nope": {}})
@@ -167,29 +162,35 @@ class TestStageOverrideValidation:
preset = _make_preset(stage_schemas=(_SIMPLE_STAGE, ))
validate_stage_overrides(
preset,
{"denoise": {"num_inference_steps": 25}},
{"denoise": {
"num_inference_steps": 25
}},
)
def test_disallowed_override_raises(self) -> None:
preset = _make_preset(stage_schemas=(_SIMPLE_STAGE, ))
with pytest.raises(
ConfigValidationError,
match="stage_overrides.denoise.height",
ConfigValidationError,
match="stage_overrides.denoise.height",
):
validate_stage_overrides(
preset,
{"denoise": {"height": 720}},
{"denoise": {
"height": 720
}},
)
def test_override_on_stage_with_no_allowed_raises(self) -> None:
preset = _make_preset(stage_schemas=(_NO_OVERRIDES_STAGE, ))
with pytest.raises(
ConfigValidationError,
match="does not accept overrides",
ConfigValidationError,
match="does not accept overrides",
):
validate_stage_overrides(
preset,
{"encode": {"some_key": 1}},
{"encode": {
"some_key": 1
}},
)
def test_empty_override_on_no_allowed_passes(self) -> None:
@@ -199,23 +200,21 @@ class TestStageOverrideValidation:
def test_non_mapping_override_raises(self) -> None:
preset = _make_preset(stage_schemas=(_SIMPLE_STAGE, ))
with pytest.raises(ConfigValidationError, match="mapping"):
validate_stage_overrides(
preset, {"denoise": "not a dict"})
validate_stage_overrides(preset, {"denoise": "not a dict"})
def test_unknown_stage_still_caught(self) -> None:
preset = _make_preset(stage_schemas=(_SIMPLE_STAGE, ))
with pytest.raises(ConfigValidationError, match="unknown"):
validate_stage_overrides(
preset, {"missing_stage": {"a": 1}})
validate_stage_overrides(preset, {"missing_stage": {"a": 1}})
def test_error_lists_allowed_overrides(self) -> None:
preset = _make_preset(stage_schemas=(_SIMPLE_STAGE, ))
with pytest.raises(
ConfigValidationError, match="guidance_scale"
):
with pytest.raises(ConfigValidationError, match="guidance_scale"):
validate_stage_overrides(
preset,
{"denoise": {"bad_key": 1}},
{"denoise": {
"bad_key": 1
}},
)
@@ -228,15 +227,13 @@ class TestValidatePresetSelection:
@pytest.mark.usefixtures("_isolated_registry")
def test_none_preset_returns_none(self) -> None:
assert validate_preset_selection(
None, "test") is None
assert validate_preset_selection(None, "test") is None
@pytest.mark.usefixtures("_isolated_registry")
def test_valid_preset_resolves(self) -> None:
p = _make_preset()
register_preset(p)
result = validate_preset_selection(
"test_preset", "test")
result = validate_preset_selection("test_preset", "test")
assert result is p
@pytest.mark.usefixtures("_isolated_registry")
@@ -246,7 +243,9 @@ class TestValidatePresetSelection:
result = validate_preset_selection(
"test_preset",
"test",
stage_overrides={"denoise": {"guidance_scale": 2.0}},
stage_overrides={"denoise": {
"guidance_scale": 2.0
}},
)
assert result is p
@@ -262,7 +261,9 @@ class TestValidatePresetSelection:
validate_preset_selection(
"test_preset",
"test",
stage_overrides={"denoise": {"bad": 1}},
stage_overrides={"denoise": {
"bad": 1
}},
)
@@ -307,19 +308,22 @@ class TestWanPresets:
# Valid override.
validate_stage_overrides(
preset,
{"denoise": {"num_inference_steps": 25}},
{"denoise": {
"num_inference_steps": 25
}},
)
# Invalid override key.
with pytest.raises(ConfigValidationError):
validate_stage_overrides(
preset,
{"denoise": {"height": 1080}},
{"denoise": {
"height": 1080
}},
)
def test_wan_model_family_in_registry(self) -> None:
from fastvideo.registry import get_model_family
family = get_model_family(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers")
family = get_model_family("Wan-AI/Wan2.1-T2V-1.3B-Diffusers")
assert family == "wan"
@@ -369,22 +373,17 @@ class TestLtx2Presets:
def test_ltx2_two_stage_refine_overrides_valid(self) -> None:
import fastvideo.registry # noqa: F401
p = get_preset("ltx2_two_stage", "ltx2")
validate_stage_overrides(
p, {"refine": {"num_inference_steps": 3}})
validate_stage_overrides(
p, {"refine": {"guidance_scale": 1.0}})
validate_stage_overrides(
p, {"refine": {"image_crf": 18}})
validate_stage_overrides(
p, {"refine": {"video_position_offset_sec": 2.5}})
validate_stage_overrides(p, {"refine": {"num_inference_steps": 3}})
validate_stage_overrides(p, {"refine": {"guidance_scale": 1.0}})
validate_stage_overrides(p, {"refine": {"image_crf": 18}})
validate_stage_overrides(p, {"refine": {"video_position_offset_sec": 2.5}})
def test_ltx2_two_stage_rejects_unknown_refine_override(self) -> None:
import fastvideo.registry # noqa: F401
from fastvideo.api.errors import ConfigValidationError
p = get_preset("ltx2_two_stage", "ltx2")
with pytest.raises(ConfigValidationError):
validate_stage_overrides(
p, {"refine": {"bogus_field": 1}})
validate_stage_overrides(p, {"refine": {"bogus_field": 1}})
# -------------------------------------------------------------------
@@ -437,12 +436,10 @@ class TestHunyuan15Presets:
import fastvideo.registry # noqa: F401
p = get_preset("hunyuan15_sr_1080p", "hunyuan15")
# Valid: override sr num_inference_steps.
validate_stage_overrides(
p, {"sr": {"num_inference_steps": 12}})
validate_stage_overrides(p, {"sr": {"num_inference_steps": 12}})
# Invalid: height not in sr allowed_overrides.
with pytest.raises(ConfigValidationError):
validate_stage_overrides(
p, {"sr": {"height": 1080}})
validate_stage_overrides(p, {"sr": {"height": 1080}})
# -------------------------------------------------------------------
@@ -471,8 +468,7 @@ class TestCosmosPresets:
"""
import fastvideo.registry # noqa: F401
from fastvideo.api.sampling_param import SamplingParam
sp = SamplingParam.from_pretrained(
"KyleShao/Cosmos-Predict2.5-2B-Diffusers")
sp = SamplingParam.from_pretrained("KyleShao/Cosmos-Predict2.5-2B-Diffusers")
assert sp.guidance_scale == 7.0
assert sp.num_inference_steps == 35
@@ -523,6 +519,116 @@ class TestSD35Presets:
assert p.defaults["num_frames"] == 1
# -------------------------------------------------------------------
# LingBot-Video preset integration (Dense and MoE/refiner)
# -------------------------------------------------------------------
class TestLingBotVideoPresets:
@pytest.mark.parametrize(
("directory_name", "pipeline_class", "expected_preset"),
(
("arbitrary-layout-a", "LingBotVideoDensePipeline", "lingbot_video_dense_t2v"),
("arbitrary-layout-b", "LingBotVideoMoePipeline", "lingbot_video_moe_refiner_t2v"),
),
)
def test_local_layouts_select_one_variant(
self,
tmp_path,
caplog,
directory_name: str,
pipeline_class: str,
expected_preset: str,
) -> None:
"""Resolve canonical local Dense and MoE paths without detector overlap."""
from fastvideo.registry import get_preset_selection
model_dir = tmp_path / directory_name
model_dir.mkdir()
(model_dir / "transformer").mkdir()
(model_dir / "model_index.json").write_text(
json.dumps({
"_class_name": pipeline_class,
"_diffusers_version": "0.39.0"
}),
encoding="utf-8",
)
assert get_preset_selection(str(model_dir)) == (expected_preset, "lingbot_video")
assert "Multiple models matched" not in caplog.text
def test_lingbot_video_presets_registered(self) -> None:
"""Register both released LingBot-Video inference layouts."""
import fastvideo.registry # noqa: F401
from fastvideo.registry import get_preset_selection
presets = get_presets_for_family("lingbot_video")
assert {preset.name
for preset in presets} == {
"lingbot_video_dense_t2v",
"lingbot_video_moe_refiner_t2v",
}
assert get_preset_selection("FastVideo/LingBot-Video-Dense-1.3B-Diffusers") == (
"lingbot_video_dense_t2v",
"lingbot_video",
)
assert get_preset_selection("FastVideo/LingBot-Video-MoE-30B-A3B-Diffusers") == (
"lingbot_video_moe_refiner_t2v",
"lingbot_video",
)
def test_moe_refiner_defaults(self) -> None:
"""Expose the official base and 1080p refiner sampling defaults."""
import fastvideo.registry # noqa: F401
preset = get_preset("lingbot_video_moe_refiner_t2v", "lingbot_video")
expected = {
"height": 480,
"width": 832,
"height_sr": 1088,
"width_sr": 1920,
"num_frames": 121,
"num_inference_steps": 40,
"num_inference_steps_sr": 8,
"guidance_scale": 3.0,
"guidance_scale_2": 3.0,
"batch_cfg": True,
"t_thresh": 0.85,
"seed": 42,
}
assert {key: preset.defaults[key] for key in expected} == expected
assert preset.stage_defaults["refine"] == {
key: expected[key]
for key in ("height_sr", "width_sr", "num_inference_steps_sr", "guidance_scale_2", "t_thresh")
}
def test_moe_refiner_stage_schema_uses_sampling_fields(self) -> None:
"""Accept only declared ``SamplingParam`` fields as stage overrides."""
import fastvideo.registry # noqa: F401
from fastvideo.api.sampling_param import SamplingParam
preset = get_preset("lingbot_video_moe_refiner_t2v", "lingbot_video")
sampling_fields = set(SamplingParam.__dataclass_fields__)
assert [(stage.name, stage.kind) for stage in preset.stage_schemas] == [
("denoise", "denoising"),
("refine", "refinement"),
]
assert set(preset.defaults) <= sampling_fields
assert all(stage.allowed_overrides <= sampling_fields for stage in preset.stage_schemas)
validate_stage_overrides(
preset,
{
"denoise": {
"num_inference_steps": 32,
"guidance_scale": 2.5
},
"refine": {
"height_sr": 1088,
"t_thresh": 0.8
},
},
)
with pytest.raises(ConfigValidationError):
validate_stage_overrides(preset, {"refine": {"refiner_steps": 8}})
# -------------------------------------------------------------------
# LingBotWorld preset integration (dual guidance)
# -------------------------------------------------------------------
@@ -540,11 +646,9 @@ class TestLingBotWorldPresets:
def test_lingbotworld_override_validation(self) -> None:
import fastvideo.registry # noqa: F401
p = get_preset("lingbotworld_i2v", "lingbotworld")
validate_stage_overrides(
p, {"denoise": {"boundary_ratio": 0.95}})
validate_stage_overrides(p, {"denoise": {"boundary_ratio": 0.95}})
with pytest.raises(ConfigValidationError):
validate_stage_overrides(
p, {"denoise": {"height": 720}})
validate_stage_overrides(p, {"denoise": {"height": 720}})
# -------------------------------------------------------------------
@@ -583,9 +687,7 @@ class TestSinglePresetFamilies:
import fastvideo.registry # noqa: F401
presets = get_presets_for_family("longcat")
names = {p.name for p in presets}
assert names == {
"longcat_t2v", "longcat_i2v", "longcat_vc"
}
assert names == {"longcat_t2v", "longcat_i2v", "longcat_vc"}
# -------------------------------------------------------------------
@@ -628,12 +730,10 @@ class TestPresetDefaultTypes:
import fastvideo.registry # noqa: F401
from fastvideo.api.presets import _PRESET_REGISTRY
offenders = [
f"{preset.model_family}/{preset.name}"
for preset in _PRESET_REGISTRY.values()
f"{preset.model_family}/{preset.name}" for preset in _PRESET_REGISTRY.values()
if preset.defaults.get("negative_prompt", "") is None
]
assert not offenders, (
"These presets set negative_prompt=None, which violates "
"SamplingParam.negative_prompt's typed str contract and "
"crashes the CFG path in text_encoding. Use \"\" instead:\n"
+ "\n".join(f" - {p}" for p in offenders))
assert not offenders, ("These presets set negative_prompt=None, which violates "
"SamplingParam.negative_prompt's typed str contract and "
"crashes the CFG path in text_encoding. Use \"\" instead:\n" +
"\n".join(f" - {p}" for p in offenders))
@@ -0,0 +1,50 @@
# SPDX-License-Identifier: Apache-2.0
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from fastvideo.attention.backends import attn_qat_train
@pytest.fixture(autouse=True)
def reset_attn_qat_train_import_cache(monkeypatch):
monkeypatch.setattr(attn_qat_train, "_attn_qat_train_attention", None)
monkeypatch.setattr(attn_qat_train, "_attn_qat_train_import_attempted", False)
monkeypatch.setattr(attn_qat_train, "_attn_qat_train_import_error", None)
def test_fastvideo_triton_kernel_is_loaded_directly(monkeypatch):
attention = Mock()
import_module = Mock(return_value=SimpleNamespace(attention=attention))
monkeypatch.setattr(attn_qat_train.importlib, "import_module", import_module)
assert attn_qat_train._get_attn_qat_train_attention() is attention
assert attn_qat_train._get_attn_qat_train_attention() is attention
import_module.assert_called_once_with("fastvideo_kernel.triton_kernels.attn_qat_train")
def test_local_kernel_source_is_added_to_import_path(monkeypatch):
monkeypatch.setattr(attn_qat_train.sys, "path", [])
attn_qat_train._ensure_kernel_paths()
assert str(attn_qat_train._project_root) in attn_qat_train.sys.path
assert str(attn_qat_train._kernel_python_root) in attn_qat_train.sys.path
def test_missing_fastvideo_kernel_has_actionable_error(monkeypatch):
missing = ImportError("No module named 'fastvideo_kernel'")
monkeypatch.setattr(
attn_qat_train.importlib,
"import_module",
Mock(side_effect=missing),
)
with pytest.raises(ImportError, match="requires FastVideo's fastvideo-kernel"):
attn_qat_train.attn_qat_train(Mock(), Mock(), Mock())
def test_backend_advertises_supported_head_size():
assert attn_qat_train.AttnQatTrainBackend.get_supported_head_sizes() == [128]
@@ -0,0 +1,63 @@
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import torch
import fastvideo.attention.selector as selector
import fastvideo.platforms as platforms
from fastvideo.platforms import AttentionBackendEnum
class _FakePlatform:
device_name = "fake"
@classmethod
def get_attn_backend_cls(
cls,
selected_backend: AttentionBackendEnum | None,
head_size: int,
dtype: torch.dtype,
) -> str:
del cls, head_size, dtype
assert selected_backend is not None
return selected_backend.name
def test_role_override_invalidates_cached_backend(monkeypatch) -> None:
monkeypatch.setattr(platforms, "_current_platform", _FakePlatform())
monkeypatch.setattr(selector, "resolve_obj_by_qualname", lambda name: name)
selector.global_force_attn_backend(None)
supported = (
AttentionBackendEnum.FLASH_ATTN,
AttentionBackendEnum.TORCH_SDPA,
)
kwargs = {
"head_size": 64,
"dtype": torch.bfloat16,
"supported_attention_backends": supported,
"default_backend": AttentionBackendEnum.FLASH_ATTN,
}
try:
assert selector.get_attn_backend(**kwargs) == "FLASH_ATTN"
with selector.global_force_attn_backend_context_manager(
AttentionBackendEnum.TORCH_SDPA):
# Same cache key as above; the role-local override must still win.
assert selector.get_attn_backend(**kwargs) == "TORCH_SDPA"
# Exiting the role scope restores the previous resolution as well.
assert selector.get_attn_backend(**kwargs) == "FLASH_ATTN"
finally:
# Do not leave fake-platform resolutions cached for later tests.
selector.global_force_attn_backend(None)
def test_explicit_backend_config_rejects_typos() -> None:
try:
selector.coerce_attn_backend("attn_qat_typo")
except ValueError as exc:
assert "Unknown attention backend" in str(exc)
else:
raise AssertionError("invalid backend name was accepted")
@@ -0,0 +1,35 @@
# SPDX-License-Identifier: Apache-2.0
"""Contract test: LTX2's per-block torch.compile wiring stays intact.
``ComposedPipelineBase._compile_with_conditions`` compiles the submodules
matched by ``model._compile_conditions`` and falls back to compiling the
whole model as one fullgraph when the list is empty. LTX2 shipped with the
conditions declared on its arch config but never exposed on the model
class, so it silently took the whole-model fallback (enormous unportable
graphs, 10+ minute cold compiles). These assertions pin the class-level
wiring and the block matcher so a refactor can't reintroduce the fallback
unnoticed. CPU-only; no model instantiation.
"""
from fastvideo.configs.models.dits.ltx2 import LTX2VideoConfig, is_ltx2_blocks
def test_ltx2_model_exposes_compile_conditions():
from fastvideo.models.dits.ltx2 import LTX2Transformer3DModel
conditions = LTX2Transformer3DModel._compile_conditions
assert conditions, (
"LTX2Transformer3DModel._compile_conditions is empty: "
"torch.compile falls back to a whole-model fullgraph")
assert is_ltx2_blocks in conditions
assert LTX2VideoConfig()._compile_conditions == conditions
def test_is_ltx2_blocks_matches_only_top_level_blocks():
assert is_ltx2_blocks("transformer_blocks.0", None)
assert is_ltx2_blocks("model.transformer_blocks.47", None)
# Submodules and lookalike names must not match — compiling them would
# nest compiled regions inside the per-block graphs.
assert not is_ltx2_blocks("transformer_blocks", None)
assert not is_ltx2_blocks("transformer_blocks.0.attn1", None)
assert not is_ltx2_blocks("audio_transformer_blocks.0", None)

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