pufanyi commited on
Commit
003373e
·
verified ·
1 Parent(s): 2b2487e

Add Flow-CPS and matched sampler pipeline

Browse files

Add 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.

Files changed (2) hide show
  1. README.md +81 -13
  2. 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 pipeline 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,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 required by `WanPipeline.from_pretrained`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- The bundled scheduler is UniPC for direct Diffusers compatibility. The
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
- ## Usage
 
 
48
 
49
  ```python
50
  import torch
51
- from diffusers import AutoencoderKLWan, WanPipeline
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 = WanPipeline.from_pretrained(
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
- generator=torch.Generator(device="cpu").manual_seed(0),
 
78
  ).frames[0]
79
 
80
  export_to_video(frames, "output.mp4", fps=16)
81
  ```
82
 
83
- Use Diffusers 0.37.1 or newer. Loading the complete pipeline requires
84
- substantial CPU and accelerator memory; CPU offloading is recommended on
85
- smaller GPUs.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ]