Image-to-Video
Diffusers
Safetensors
Wan2.2
English
Chinese
WanPipeline
video-generation
visual-reasoning
reinforcement-learning
rlvr
vbvr-pro
Instructions to use Video-Reason/VBVR-Pro-Wan2.2-TI2V-5B-RLVR with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use Video-Reason/VBVR-Pro-Wan2.2-TI2V-5B-RLVR with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline from diffusers.utils import load_image, export_to_video # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("Video-Reason/VBVR-Pro-Wan2.2-TI2V-5B-RLVR", dtype=torch.bfloat16, device_map="cuda") pipe.to("cuda") prompt = "A man with short gray hair plays a red electric guitar." image = load_image( "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/guitar-man.png" ) output = pipe(image=image, prompt=prompt).frames[0] export_to_video(output, "output.mp4") - Wan2.2
How to use Video-Reason/VBVR-Pro-Wan2.2-TI2V-5B-RLVR with Wan2.2:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
Add Flow-CPS and matched sampler pipeline
Browse filesAdd a Diffusers custom image-to-video pipeline for CPS 0.1/0.3/0.7/0.9, FlowMatch Euler, and UniPC. Update the model card with runnable loading examples, paper-matched settings, and sampler results.
- README.md +81 -13
- pipeline.py +384 -0
README.md
CHANGED
|
@@ -22,7 +22,7 @@ tags:
|
|
| 22 |
|
| 23 |
# VBVR-Pro Wan2.2 TI2V-5B — Rule-RL
|
| 24 |
|
| 25 |
-
This repository contains a complete Diffusers
|
| 26 |
Wan2.2 TI2V-5B model optimized with task-specific rule-based reinforcement
|
| 27 |
learning. It is derived from
|
| 28 |
[`Wan-AI/Wan2.2-TI2V-5B-Diffusers`](https://huggingface.co/Wan-AI/Wan2.2-TI2V-5B-Diffusers)
|
|
@@ -30,7 +30,27 @@ and is intended for research on image-conditioned video generation and visual
|
|
| 30 |
reasoning.
|
| 31 |
|
| 32 |
The repository includes the transformer, text encoder, tokenizer, VAE, and
|
| 33 |
-
scheduler
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
## Recommended evaluation settings
|
| 36 |
|
|
@@ -40,32 +60,35 @@ scheduler required by `WanPipeline.from_pretrained`.
|
|
| 40 |
- Inference steps: 30
|
| 41 |
- Guidance scale: 1.0
|
| 42 |
|
| 43 |
-
|
| 44 |
-
Flow-CPS samplers used in the VBVR-Pro experiments are available in
|
| 45 |
-
[`pufanyi/vbvr-rl`](https://github.com/pufanyi/vbvr-rl).
|
| 46 |
|
| 47 |
-
|
|
|
|
|
|
|
| 48 |
|
| 49 |
```python
|
| 50 |
import torch
|
| 51 |
-
from diffusers import AutoencoderKLWan,
|
| 52 |
from diffusers.utils import export_to_video, load_image
|
| 53 |
|
| 54 |
model_id = "pufanyi/VBVR-Pro-Wan2.2-TI2V-5B-Rule-RL"
|
| 55 |
|
|
|
|
| 56 |
vae = AutoencoderKLWan.from_pretrained(
|
| 57 |
model_id,
|
| 58 |
subfolder="vae",
|
| 59 |
torch_dtype=torch.float32,
|
| 60 |
)
|
| 61 |
-
pipe =
|
| 62 |
model_id,
|
|
|
|
|
|
|
| 63 |
vae=vae,
|
| 64 |
torch_dtype=torch.bfloat16,
|
| 65 |
)
|
| 66 |
pipe.enable_model_cpu_offload()
|
| 67 |
|
| 68 |
-
image = load_image("input.png")
|
| 69 |
frames = pipe(
|
| 70 |
image=image,
|
| 71 |
prompt="Move the marked object to the matching target.",
|
|
@@ -74,15 +97,60 @@ frames = pipe(
|
|
| 74 |
num_frames=81,
|
| 75 |
num_inference_steps=30,
|
| 76 |
guidance_scale=1.0,
|
| 77 |
-
|
|
|
|
| 78 |
).frames[0]
|
| 79 |
|
| 80 |
export_to_video(frames, "output.mp4", fps=16)
|
| 81 |
```
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
## Training summary
|
| 88 |
|
|
|
|
| 22 |
|
| 23 |
# VBVR-Pro Wan2.2 TI2V-5B — Rule-RL
|
| 24 |
|
| 25 |
+
This repository contains a complete Diffusers checkpoint for the VBVR-Pro
|
| 26 |
Wan2.2 TI2V-5B model optimized with task-specific rule-based reinforcement
|
| 27 |
learning. It is derived from
|
| 28 |
[`Wan-AI/Wan2.2-TI2V-5B-Diffusers`](https://huggingface.co/Wan-AI/Wan2.2-TI2V-5B-Diffusers)
|
|
|
|
| 30 |
reasoning.
|
| 31 |
|
| 32 |
The repository includes the transformer, text encoder, tokenizer, VAE, and
|
| 33 |
+
scheduler. It now also includes [`pipeline.py`](./pipeline.py), a custom
|
| 34 |
+
image-to-video pipeline exposing all six inference configurations evaluated in
|
| 35 |
+
the VBVR-Pro paper.
|
| 36 |
+
|
| 37 |
+
## Inference samplers
|
| 38 |
+
|
| 39 |
+
The sampler is selected per call; the model weights do not change.
|
| 40 |
+
|
| 41 |
+
| `sampler` | Inference method | CPS coefficient | Paper overall score |
|
| 42 |
+
|---|---|---:|---:|
|
| 43 |
+
| `cps-0.1` | Flow-CPS | 0.1 | 0.509 |
|
| 44 |
+
| `cps-0.3` | Flow-CPS | 0.3 | 0.526 |
|
| 45 |
+
| `cps-0.7` | Flow-CPS | 0.7 | **0.548** |
|
| 46 |
+
| `cps-0.9` | Flow-CPS | 0.9 | 0.539 |
|
| 47 |
+
| `euler` | FlowMatch Euler ODE | — | 0.522 |
|
| 48 |
+
| `unipc` | UniPC ODE | — | 0.522 |
|
| 49 |
+
|
| 50 |
+
These are the aggregate VBVR-Pro-Bench results reported in Table 8 under the
|
| 51 |
+
matched settings below. The model was trained with Flow-CPS coefficient 0.7.
|
| 52 |
+
Reported scores are evaluation results, not guarantees for other prompts or
|
| 53 |
+
runtime configurations.
|
| 54 |
|
| 55 |
## Recommended evaluation settings
|
| 56 |
|
|
|
|
| 60 |
- Inference steps: 30
|
| 61 |
- Guidance scale: 1.0
|
| 62 |
|
| 63 |
+
## Usage with all six samplers
|
|
|
|
|
|
|
| 64 |
|
| 65 |
+
Use Diffusers 0.37.1 or newer. Because this loads Python code from the model
|
| 66 |
+
repository, review `pipeline.py`, pass `trust_remote_code=True`, and pin a
|
| 67 |
+
reviewed `revision` in production.
|
| 68 |
|
| 69 |
```python
|
| 70 |
import torch
|
| 71 |
+
from diffusers import AutoencoderKLWan, DiffusionPipeline
|
| 72 |
from diffusers.utils import export_to_video, load_image
|
| 73 |
|
| 74 |
model_id = "pufanyi/VBVR-Pro-Wan2.2-TI2V-5B-Rule-RL"
|
| 75 |
|
| 76 |
+
# Wan's VAE is kept in float32 for stable decoding.
|
| 77 |
vae = AutoencoderKLWan.from_pretrained(
|
| 78 |
model_id,
|
| 79 |
subfolder="vae",
|
| 80 |
torch_dtype=torch.float32,
|
| 81 |
)
|
| 82 |
+
pipe = DiffusionPipeline.from_pretrained(
|
| 83 |
model_id,
|
| 84 |
+
custom_pipeline="pipeline",
|
| 85 |
+
trust_remote_code=True,
|
| 86 |
vae=vae,
|
| 87 |
torch_dtype=torch.bfloat16,
|
| 88 |
)
|
| 89 |
pipe.enable_model_cpu_offload()
|
| 90 |
|
| 91 |
+
image = load_image("input.png").convert("RGB")
|
| 92 |
frames = pipe(
|
| 93 |
image=image,
|
| 94 |
prompt="Move the marked object to the matching target.",
|
|
|
|
| 97 |
num_frames=81,
|
| 98 |
num_inference_steps=30,
|
| 99 |
guidance_scale=1.0,
|
| 100 |
+
sampler="cps-0.7", # cps-0.1, cps-0.3, cps-0.7, cps-0.9, euler, or unipc
|
| 101 |
+
generator=torch.Generator(device="cuda").manual_seed(0),
|
| 102 |
).frames[0]
|
| 103 |
|
| 104 |
export_to_video(frames, "output.mp4", fps=16)
|
| 105 |
```
|
| 106 |
|
| 107 |
+
The generic form `sampler="cps", cps_eta=<value>` accepts any finite
|
| 108 |
+
coefficient from 0 to 1. `generator` controls the initial latent and, by
|
| 109 |
+
default, the fresh Flow-CPS transition noise. Pass a separate
|
| 110 |
+
`cps_generator` when the two random streams must be controlled independently.
|
| 111 |
+
|
| 112 |
+
Loading the complete pipeline requires substantial CPU and accelerator
|
| 113 |
+
memory. CPU offloading is recommended on smaller GPUs.
|
| 114 |
+
|
| 115 |
+
## Standard Diffusers compatibility
|
| 116 |
+
|
| 117 |
+
The bundled scheduler remains UniPC and `model_index.json` is unchanged. Users
|
| 118 |
+
who only need the standard deterministic path can load the checkpoint without
|
| 119 |
+
remote custom code:
|
| 120 |
+
|
| 121 |
+
```python
|
| 122 |
+
import torch
|
| 123 |
+
from diffusers import AutoencoderKLWan, WanImageToVideoPipeline
|
| 124 |
+
|
| 125 |
+
model_id = "pufanyi/VBVR-Pro-Wan2.2-TI2V-5B-Rule-RL"
|
| 126 |
+
|
| 127 |
+
vae = AutoencoderKLWan.from_pretrained(
|
| 128 |
+
model_id,
|
| 129 |
+
subfolder="vae",
|
| 130 |
+
torch_dtype=torch.float32,
|
| 131 |
+
)
|
| 132 |
+
pipe = WanImageToVideoPipeline.from_pretrained(
|
| 133 |
+
model_id,
|
| 134 |
+
vae=vae,
|
| 135 |
+
torch_dtype=torch.bfloat16,
|
| 136 |
+
)
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
Use `WanImageToVideoPipeline`, not the text-to-video `WanPipeline`: the latter
|
| 140 |
+
does not accept the first-frame `image` argument in Diffusers 0.37.1.
|
| 141 |
+
|
| 142 |
+
## Implementation and reproducibility notes
|
| 143 |
+
|
| 144 |
+
- Flow-CPS uses the training-time shifted `linspace(1, 0, T + 1)` sigma grid
|
| 145 |
+
and preserves the released scheduler's `flow_shift: 5.0`.
|
| 146 |
+
- CPS updates are evaluated in float32 and cast back to the transformer latent
|
| 147 |
+
dtype. Euler and UniPC retain their native Diffusers latent precision and
|
| 148 |
+
solver grids.
|
| 149 |
+
- The custom class subclasses `WanImageToVideoPipeline`, preserving the
|
| 150 |
+
official first-frame VAE conditioning and TI2V-5B expanded-timestep mask.
|
| 151 |
+
- The release training/evaluation repository remains the source of truth for
|
| 152 |
+
formal score provenance. Exact output bytes can vary with PyTorch,
|
| 153 |
+
Diffusers, attention backend, dtype, and device.
|
| 154 |
|
| 155 |
## Training summary
|
| 156 |
|
pipeline.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Diffusers pipeline for the six VBVR-Pro RL inference configurations.
|
| 2 |
+
|
| 3 |
+
The released TI2V-5B checkpoints contain ordinary flow-prediction weights.
|
| 4 |
+
They are not tied to the bundled UniPC scheduler. This module keeps the
|
| 5 |
+
official Diffusers image-conditioning path and adds the Flow-CPS transition
|
| 6 |
+
used for RL rollouts, alongside matched FlowMatch Euler and UniPC ODE modes.
|
| 7 |
+
|
| 8 |
+
The file is intentionally self-contained so it can also be copied to a model
|
| 9 |
+
repository as a Diffusers custom pipeline.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import math
|
| 15 |
+
from typing import Any
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
from diffusers import FlowMatchEulerDiscreteScheduler, UniPCMultistepScheduler, WanImageToVideoPipeline
|
| 19 |
+
from diffusers.schedulers.scheduling_flow_match_euler_discrete import FlowMatchEulerDiscreteSchedulerOutput
|
| 20 |
+
from diffusers.utils.torch_utils import randn_tensor
|
| 21 |
+
|
| 22 |
+
PAPER_SAMPLER_PRESETS = (
|
| 23 |
+
"cps-0.1",
|
| 24 |
+
"cps-0.3",
|
| 25 |
+
"cps-0.7",
|
| 26 |
+
"cps-0.9",
|
| 27 |
+
"euler",
|
| 28 |
+
"unipc",
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
_CPS_PRESETS = {
|
| 32 |
+
"cps-0.1": 0.1,
|
| 33 |
+
"cps-0.3": 0.3,
|
| 34 |
+
"cps-0.7": 0.7,
|
| 35 |
+
"cps-0.9": 0.9,
|
| 36 |
+
"cps0p1": 0.1,
|
| 37 |
+
"cps0p3": 0.3,
|
| 38 |
+
"cps0p7": 0.7,
|
| 39 |
+
"cps0p9": 0.9,
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
_SAMPLER_ALIASES = {
|
| 43 |
+
"cps": "cps",
|
| 44 |
+
"flow-cps": "cps",
|
| 45 |
+
"flowcps": "cps",
|
| 46 |
+
"euler": "euler",
|
| 47 |
+
"euler-ode": "euler",
|
| 48 |
+
"flow-match-euler": "euler",
|
| 49 |
+
"flowmatch-euler": "euler",
|
| 50 |
+
"unipc": "unipc",
|
| 51 |
+
"unipc-ode": "unipc",
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _config_value(config: Any, name: str, default: Any = None) -> Any:
|
| 56 |
+
if hasattr(config, "get"):
|
| 57 |
+
return config.get(name, default)
|
| 58 |
+
return getattr(config, name, default)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _normalize_sampler_name(sampler: str) -> str:
|
| 62 |
+
if not isinstance(sampler, str) or not sampler.strip():
|
| 63 |
+
raise ValueError(f"sampler must be a non-empty string, got {sampler!r}")
|
| 64 |
+
return sampler.strip().lower().replace("_", "-").replace(" ", "-")
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def resolve_vbvr_sampler(sampler: str, cps_eta: float | None = None) -> tuple[str, float | None]:
|
| 68 |
+
"""Resolve a public sampler name to ``(kind, eta)``.
|
| 69 |
+
|
| 70 |
+
``kind`` is one of ``cps``, ``euler``, and ``unipc``. The four CPS names
|
| 71 |
+
in :data:`PAPER_SAMPLER_PRESETS` carry their paper coefficient, while the
|
| 72 |
+
generic ``cps`` name defaults to the training coefficient 0.7.
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
normalized = _normalize_sampler_name(sampler)
|
| 76 |
+
preset_key = normalized.replace("cps-0p", "cps0p")
|
| 77 |
+
if normalized in _CPS_PRESETS or preset_key in _CPS_PRESETS:
|
| 78 |
+
preset_eta = _CPS_PRESETS.get(normalized, _CPS_PRESETS[preset_key])
|
| 79 |
+
if cps_eta is not None and float(cps_eta) != preset_eta:
|
| 80 |
+
raise ValueError(f"sampler={sampler!r} fixes cps_eta={preset_eta}, but cps_eta={cps_eta} was also passed")
|
| 81 |
+
return "cps", preset_eta
|
| 82 |
+
|
| 83 |
+
try:
|
| 84 |
+
kind = _SAMPLER_ALIASES[normalized]
|
| 85 |
+
except KeyError as exc:
|
| 86 |
+
choices = ", ".join(PAPER_SAMPLER_PRESETS)
|
| 87 |
+
raise ValueError(f"Unknown sampler {sampler!r}; paper presets are: {choices}") from exc
|
| 88 |
+
|
| 89 |
+
if kind != "cps":
|
| 90 |
+
if cps_eta is not None:
|
| 91 |
+
raise ValueError(f"cps_eta only applies to Flow-CPS, not sampler={sampler!r}")
|
| 92 |
+
return kind, None
|
| 93 |
+
|
| 94 |
+
if cps_eta is None:
|
| 95 |
+
cps_eta = 0.7
|
| 96 |
+
if isinstance(cps_eta, bool):
|
| 97 |
+
raise ValueError(f"cps_eta must be a finite number in [0, 1], got {cps_eta!r}")
|
| 98 |
+
eta = float(cps_eta)
|
| 99 |
+
if not math.isfinite(eta) or not 0.0 <= eta <= 1.0:
|
| 100 |
+
raise ValueError(f"cps_eta must be a finite number in [0, 1], got {cps_eta!r}")
|
| 101 |
+
return kind, eta
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _flow_shift(config: Any) -> float:
|
| 105 |
+
value = _config_value(config, "flow_shift", _config_value(config, "shift", 1.0))
|
| 106 |
+
shift = float(value)
|
| 107 |
+
if not math.isfinite(shift) or shift <= 0.0:
|
| 108 |
+
raise ValueError(f"The flow scheduler shift must be positive and finite, got {value!r}")
|
| 109 |
+
return shift
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _new_flow_scheduler(scheduler_cls: type[FlowMatchEulerDiscreteScheduler], config: Any):
|
| 113 |
+
"""Build a FlowMatch scheduler without silently dropping UniPC's ``flow_shift``."""
|
| 114 |
+
|
| 115 |
+
return scheduler_cls(
|
| 116 |
+
num_train_timesteps=int(_config_value(config, "num_train_timesteps", 1000)),
|
| 117 |
+
shift=_flow_shift(config),
|
| 118 |
+
use_dynamic_shifting=bool(_config_value(config, "use_dynamic_shifting", False)),
|
| 119 |
+
base_shift=_config_value(config, "base_shift", 0.5),
|
| 120 |
+
max_shift=_config_value(config, "max_shift", 1.15),
|
| 121 |
+
base_image_seq_len=int(_config_value(config, "base_image_seq_len", 256)),
|
| 122 |
+
max_image_seq_len=int(_config_value(config, "max_image_seq_len", 4096)),
|
| 123 |
+
invert_sigmas=bool(_config_value(config, "invert_sigmas", False)),
|
| 124 |
+
shift_terminal=_config_value(config, "shift_terminal"),
|
| 125 |
+
use_karras_sigmas=bool(_config_value(config, "use_karras_sigmas", False)),
|
| 126 |
+
use_exponential_sigmas=bool(_config_value(config, "use_exponential_sigmas", False)),
|
| 127 |
+
use_beta_sigmas=bool(_config_value(config, "use_beta_sigmas", False)),
|
| 128 |
+
time_shift_type=str(_config_value(config, "time_shift_type", "exponential")),
|
| 129 |
+
stochastic_sampling=False,
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
class FlowMatchCPSScheduler(FlowMatchEulerDiscreteScheduler):
|
| 134 |
+
"""FlowMatch sigma schedule with the coefficient-preserving transition.
|
| 135 |
+
|
| 136 |
+
At ``cps_eta=0`` this transition is the first-order rectified-flow Euler
|
| 137 |
+
update. Positive values rotate part of the scheduler-prescribed noise
|
| 138 |
+
coefficient into freshly sampled Gaussian noise while preserving its total
|
| 139 |
+
magnitude.
|
| 140 |
+
"""
|
| 141 |
+
|
| 142 |
+
_cps_eta = 0.7
|
| 143 |
+
_cps_generator: torch.Generator | list[torch.Generator] | None = None
|
| 144 |
+
|
| 145 |
+
@property
|
| 146 |
+
def cps_eta(self) -> float:
|
| 147 |
+
return self._cps_eta
|
| 148 |
+
|
| 149 |
+
def set_cps_eta(self, value: float) -> None:
|
| 150 |
+
_, eta = resolve_vbvr_sampler("cps", value)
|
| 151 |
+
assert eta is not None
|
| 152 |
+
self._cps_eta = eta
|
| 153 |
+
|
| 154 |
+
def set_cps_generator(self, generator: torch.Generator | list[torch.Generator] | None) -> None:
|
| 155 |
+
self._cps_generator = generator
|
| 156 |
+
|
| 157 |
+
def set_timesteps(
|
| 158 |
+
self,
|
| 159 |
+
num_inference_steps: int | None = None,
|
| 160 |
+
device: str | torch.device | None = None,
|
| 161 |
+
sigmas: list[float] | None = None,
|
| 162 |
+
mu: float | None = None,
|
| 163 |
+
timesteps: list[float] | None = None,
|
| 164 |
+
) -> None:
|
| 165 |
+
"""Install the exact training-time ``T+1`` sigma grid.
|
| 166 |
+
|
| 167 |
+
Diffusers' FlowMatch Euler scheduler spaces ``T`` source sigmas from
|
| 168 |
+
raw time 1 to ``1 / num_train_timesteps`` and then appends zero. The
|
| 169 |
+
Flow-CPS rollout used for VBVR-Pro instead shifts ``linspace(1, 0,
|
| 170 |
+
T+1)``. Keeping this distinction is required to reproduce the CPS
|
| 171 |
+
inference contract.
|
| 172 |
+
"""
|
| 173 |
+
|
| 174 |
+
if sigmas is not None or timesteps is not None:
|
| 175 |
+
raise ValueError("FlowMatchCPSScheduler only supports its training-time automatic sigma grid")
|
| 176 |
+
if isinstance(num_inference_steps, bool) or not isinstance(num_inference_steps, int):
|
| 177 |
+
raise ValueError(f"num_inference_steps must be a positive integer, got {num_inference_steps!r}")
|
| 178 |
+
if num_inference_steps < 1:
|
| 179 |
+
raise ValueError(f"num_inference_steps must be positive, got {num_inference_steps}")
|
| 180 |
+
if self.config.use_dynamic_shifting and mu is None:
|
| 181 |
+
raise ValueError("mu is required when use_dynamic_shifting=True")
|
| 182 |
+
if any(
|
| 183 |
+
(
|
| 184 |
+
self.config.invert_sigmas,
|
| 185 |
+
self.config.use_karras_sigmas,
|
| 186 |
+
self.config.use_exponential_sigmas,
|
| 187 |
+
self.config.use_beta_sigmas,
|
| 188 |
+
)
|
| 189 |
+
):
|
| 190 |
+
raise ValueError("FlowMatchCPSScheduler requires the standard non-inverted linear flow sigma family")
|
| 191 |
+
|
| 192 |
+
raw_sigmas = torch.linspace(1.0, 0.0, num_inference_steps + 1, device=device, dtype=torch.float32)
|
| 193 |
+
if self.config.use_dynamic_shifting:
|
| 194 |
+
shifted_sigmas = self.time_shift(mu, 1.0, raw_sigmas)
|
| 195 |
+
else:
|
| 196 |
+
shifted_sigmas = self.shift * raw_sigmas / (1.0 + (self.shift - 1.0) * raw_sigmas)
|
| 197 |
+
if self.config.shift_terminal:
|
| 198 |
+
shifted_sigmas = self.stretch_shift_to_terminal(shifted_sigmas)
|
| 199 |
+
|
| 200 |
+
self.num_inference_steps = num_inference_steps
|
| 201 |
+
self.sigmas = shifted_sigmas
|
| 202 |
+
self.timesteps = shifted_sigmas[:-1] * self.config.num_train_timesteps
|
| 203 |
+
self._step_index = None
|
| 204 |
+
self._begin_index = None
|
| 205 |
+
|
| 206 |
+
def step(
|
| 207 |
+
self,
|
| 208 |
+
model_output: torch.FloatTensor,
|
| 209 |
+
timestep: float | torch.FloatTensor,
|
| 210 |
+
sample: torch.FloatTensor,
|
| 211 |
+
s_churn: float = 0.0,
|
| 212 |
+
s_tmin: float = 0.0,
|
| 213 |
+
s_tmax: float = float("inf"),
|
| 214 |
+
s_noise: float = 1.0,
|
| 215 |
+
generator: torch.Generator | list[torch.Generator] | None = None,
|
| 216 |
+
per_token_timesteps: torch.Tensor | None = None,
|
| 217 |
+
return_dict: bool = True,
|
| 218 |
+
) -> FlowMatchEulerDiscreteSchedulerOutput | tuple[torch.Tensor]:
|
| 219 |
+
"""Advance one Flow-CPS step using the scheduler's current sigma pair."""
|
| 220 |
+
|
| 221 |
+
if per_token_timesteps is not None:
|
| 222 |
+
raise ValueError("FlowMatchCPSScheduler does not support per_token_timesteps")
|
| 223 |
+
if s_churn != 0.0 or s_tmin != 0.0 or s_tmax != float("inf") or s_noise != 1.0:
|
| 224 |
+
raise ValueError("FlowMatchCPSScheduler does not support Euler churn parameters")
|
| 225 |
+
if isinstance(timestep, (int, torch.IntTensor, torch.LongTensor)):
|
| 226 |
+
raise ValueError("Pass a value from scheduler.timesteps, not an integer step index")
|
| 227 |
+
|
| 228 |
+
if self.step_index is None:
|
| 229 |
+
self._init_step_index(timestep)
|
| 230 |
+
|
| 231 |
+
sigma = self.sigmas[self.step_index].to(device=sample.device, dtype=torch.float32)
|
| 232 |
+
sigma_next = self.sigmas[self.step_index + 1].to(device=sample.device, dtype=torch.float32)
|
| 233 |
+
sample_fp32 = sample.to(torch.float32)
|
| 234 |
+
model_output_fp32 = model_output.to(torch.float32)
|
| 235 |
+
|
| 236 |
+
predicted_clean = sample_fp32 - sigma * model_output_fp32
|
| 237 |
+
predicted_noise = sample_fp32 + (1.0 - sigma) * model_output_fp32
|
| 238 |
+
angle = self.cps_eta * math.pi / 2.0
|
| 239 |
+
prev_sample = (1.0 - sigma_next) * predicted_clean + sigma_next * math.cos(angle) * predicted_noise
|
| 240 |
+
|
| 241 |
+
noise_std = sigma_next * math.sin(angle)
|
| 242 |
+
if self.cps_eta > 0.0 and bool(noise_std > 0.0):
|
| 243 |
+
noise = randn_tensor(
|
| 244 |
+
sample.shape,
|
| 245 |
+
generator=generator if generator is not None else self._cps_generator,
|
| 246 |
+
device=sample.device,
|
| 247 |
+
dtype=torch.float32,
|
| 248 |
+
)
|
| 249 |
+
prev_sample = prev_sample + noise_std * noise
|
| 250 |
+
|
| 251 |
+
self._step_index += 1
|
| 252 |
+
prev_sample = prev_sample.to(sample.dtype)
|
| 253 |
+
if not return_dict:
|
| 254 |
+
return (prev_sample,)
|
| 255 |
+
return FlowMatchEulerDiscreteSchedulerOutput(prev_sample=prev_sample)
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def create_vbvr_scheduler(config: Any, sampler: str, cps_eta: float | None = None):
|
| 259 |
+
"""Create one of the schedulers used in the paper's matched sampler matrix."""
|
| 260 |
+
|
| 261 |
+
kind, eta = resolve_vbvr_sampler(sampler, cps_eta)
|
| 262 |
+
if kind == "cps":
|
| 263 |
+
scheduler = _new_flow_scheduler(FlowMatchCPSScheduler, config)
|
| 264 |
+
assert eta is not None
|
| 265 |
+
scheduler.set_cps_eta(eta)
|
| 266 |
+
return scheduler
|
| 267 |
+
if kind == "euler":
|
| 268 |
+
return _new_flow_scheduler(FlowMatchEulerDiscreteScheduler, config)
|
| 269 |
+
|
| 270 |
+
return UniPCMultistepScheduler.from_config(
|
| 271 |
+
config,
|
| 272 |
+
solver_order=2,
|
| 273 |
+
solver_type="bh2",
|
| 274 |
+
prediction_type="flow_prediction",
|
| 275 |
+
use_flow_sigmas=True,
|
| 276 |
+
flow_shift=_flow_shift(config),
|
| 277 |
+
predict_x0=True,
|
| 278 |
+
lower_order_final=True,
|
| 279 |
+
final_sigmas_type="zero",
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
class VBVRWanPipeline(WanImageToVideoPipeline):
|
| 284 |
+
"""Wan TI2V/I2V pipeline with the six VBVR-Pro paper samplers.
|
| 285 |
+
|
| 286 |
+
``sampler`` accepts the values in :data:`PAPER_SAMPLER_PRESETS`, plus the
|
| 287 |
+
generic ``cps`` name with an arbitrary ``cps_eta`` in ``[0, 1]``.
|
| 288 |
+
"""
|
| 289 |
+
|
| 290 |
+
@property
|
| 291 |
+
def active_sampler(self) -> str | None:
|
| 292 |
+
return getattr(self, "_vbvr_active_sampler", None)
|
| 293 |
+
|
| 294 |
+
@property
|
| 295 |
+
def active_cps_eta(self) -> float | None:
|
| 296 |
+
return getattr(self, "_vbvr_active_cps_eta", None)
|
| 297 |
+
|
| 298 |
+
def _original_scheduler_config(self) -> dict[str, Any]:
|
| 299 |
+
config = getattr(self, "_vbvr_original_scheduler_config", None)
|
| 300 |
+
if config is None:
|
| 301 |
+
config = dict(self.scheduler.config)
|
| 302 |
+
self._vbvr_original_scheduler_config = config
|
| 303 |
+
return config
|
| 304 |
+
|
| 305 |
+
def set_sampler(self, sampler: str, *, cps_eta: float | None = None):
|
| 306 |
+
"""Install a fresh scheduler for one inference call and return it."""
|
| 307 |
+
|
| 308 |
+
kind, eta = resolve_vbvr_sampler(sampler, cps_eta)
|
| 309 |
+
scheduler = create_vbvr_scheduler(self._original_scheduler_config(), sampler, cps_eta)
|
| 310 |
+
self.scheduler = scheduler
|
| 311 |
+
self._vbvr_active_sampler = kind
|
| 312 |
+
self._vbvr_active_cps_eta = eta
|
| 313 |
+
return scheduler
|
| 314 |
+
|
| 315 |
+
def prepare_latents(
|
| 316 |
+
self,
|
| 317 |
+
image: Any,
|
| 318 |
+
batch_size: int,
|
| 319 |
+
num_channels_latents: int = 16,
|
| 320 |
+
height: int = 480,
|
| 321 |
+
width: int = 832,
|
| 322 |
+
num_frames: int = 81,
|
| 323 |
+
dtype: torch.dtype | None = None,
|
| 324 |
+
device: torch.device | None = None,
|
| 325 |
+
generator: torch.Generator | list[torch.Generator] | None = None,
|
| 326 |
+
latents: torch.Tensor | None = None,
|
| 327 |
+
last_image: torch.Tensor | None = None,
|
| 328 |
+
):
|
| 329 |
+
# The training-time Flow-CPS rollout starts and transitions in the
|
| 330 |
+
# transformer dtype. Keep that path while leaving standard Diffusers
|
| 331 |
+
# UniPC/Euler latent precision untouched.
|
| 332 |
+
if self.active_sampler == "cps":
|
| 333 |
+
transformer = self.transformer if self.transformer is not None else self.transformer_2
|
| 334 |
+
dtype = transformer.dtype
|
| 335 |
+
return super().prepare_latents(
|
| 336 |
+
image=image,
|
| 337 |
+
batch_size=batch_size,
|
| 338 |
+
num_channels_latents=num_channels_latents,
|
| 339 |
+
height=height,
|
| 340 |
+
width=width,
|
| 341 |
+
num_frames=num_frames,
|
| 342 |
+
dtype=dtype,
|
| 343 |
+
device=device,
|
| 344 |
+
generator=generator,
|
| 345 |
+
latents=latents,
|
| 346 |
+
last_image=last_image,
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
def __call__(
|
| 350 |
+
self,
|
| 351 |
+
*args: Any,
|
| 352 |
+
sampler: str = "cps",
|
| 353 |
+
cps_eta: float | None = None,
|
| 354 |
+
cps_generator: torch.Generator | list[torch.Generator] | None = None,
|
| 355 |
+
generator: torch.Generator | list[torch.Generator] | None = None,
|
| 356 |
+
**kwargs: Any,
|
| 357 |
+
):
|
| 358 |
+
"""Run the official I2V pipeline with a selected VBVR-Pro sampler.
|
| 359 |
+
|
| 360 |
+
``generator`` controls the initial latent. ``cps_generator`` can be
|
| 361 |
+
supplied separately to control per-step CPS noise, which is useful for
|
| 362 |
+
exact experiment seed contracts. If omitted, CPS reuses ``generator``.
|
| 363 |
+
"""
|
| 364 |
+
|
| 365 |
+
scheduler = self.set_sampler(sampler, cps_eta=cps_eta)
|
| 366 |
+
if not isinstance(scheduler, FlowMatchCPSScheduler):
|
| 367 |
+
if cps_generator is not None:
|
| 368 |
+
raise ValueError("cps_generator only applies to a Flow-CPS sampler")
|
| 369 |
+
return super().__call__(*args, generator=generator, **kwargs)
|
| 370 |
+
|
| 371 |
+
scheduler.set_cps_generator(cps_generator if cps_generator is not None else generator)
|
| 372 |
+
try:
|
| 373 |
+
return super().__call__(*args, generator=generator, **kwargs)
|
| 374 |
+
finally:
|
| 375 |
+
scheduler.set_cps_generator(None)
|
| 376 |
+
|
| 377 |
+
|
| 378 |
+
__all__ = [
|
| 379 |
+
"FlowMatchCPSScheduler",
|
| 380 |
+
"PAPER_SAMPLER_PRESETS",
|
| 381 |
+
"VBVRWanPipeline",
|
| 382 |
+
"create_vbvr_scheduler",
|
| 383 |
+
"resolve_vbvr_sampler",
|
| 384 |
+
]
|