Skip to content

Commit dabb2be

Browse files
fix(network): correct skip-layer guidance and preserve old keyword alias
- Apply skip-layer guidance only to the unconditional branch in the Cosmos Predict2 sampler, matching Wan, WanI2V, and dmd2 behavior - Add FlowKarrasUniPCScheduler with the official Cosmos Karras ramp in flow-matching units for diffusers < 0.37 compatibility - Rename skip_layers_start_percent to skip_layers_start_fraction and keep the old keyword as a deprecated **kwargs alias in Wan, WanI2V, and Cosmos Predict2 samplers - Use initial latents over [0, t_init] for conditioning-frame velocity - Derive tqdm total from timesteps instead of a hand-computed value Signed-off-by: Andrew White <andrewh@cdw.com>
1 parent 060b857 commit dabb2be

6 files changed

Lines changed: 80 additions & 23 deletions

File tree

fastgen/networks/VaceWan/network_causal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -924,7 +924,7 @@ def sample(
924924
self.unipc_scheduler.config.flow_shift = shift
925925
self.unipc_scheduler.set_timesteps(num_inference_steps=sample_steps, device=noise.device)
926926
timesteps = self.unipc_scheduler.timesteps
927-
for timestep in tqdm(timesteps, total=sample_steps - 1):
927+
for timestep in tqdm(timesteps):
928928
t = (timestep / time_rescale_factor).expand(batch_size)
929929
x_cur = x_next
930930
flow_pred = self(

fastgen/networks/Wan/network.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -920,7 +920,7 @@ def sample(
920920
num_steps: int = 50,
921921
shift: float = 5.0,
922922
skip_layers: Optional[List[int]] = None,
923-
skip_layers_start_percent: float = 0.0,
923+
skip_layers_start_fraction: float = 0.0,
924924
**kwargs,
925925
) -> torch.Tensor:
926926
"""Multistep sample using the UniPC method
@@ -933,11 +933,17 @@ def sample(
933933
num_steps (int): The number of sampling steps.
934934
shift (float): Noise schedule shift parameter. Affects temporal dynamics.
935935
skip_layers (Optional[List[int]]): List of transformer layers to skip (used by SLG) during sampling.
936-
skip_layers_start_percent (float): The percentage of the sampling steps to start skipping layers.
936+
skip_layers_start_fraction (float): Fraction in [0, 1] of the sampling steps to complete
937+
before skip-layer guidance becomes active.
938+
``skip_layers_start_percent`` is accepted as a deprecated alias.
937939
938940
Returns:
939941
torch.Tensor: The sample output.
940942
"""
943+
# Backward compatibility: the old argument name was skip_layers_start_percent.
944+
if "skip_layers_start_percent" in kwargs:
945+
skip_layers_start_fraction = kwargs.pop("skip_layers_start_percent")
946+
941947
assert self.schedule_type == "rf", f"{self.schedule_type} is not supported"
942948

943949
self.unipc_scheduler.config.flow_shift = shift
@@ -949,7 +955,7 @@ def sample(
949955
latents = self.noise_scheduler.latents(noise=noise, t_init=t_init)
950956

951957
# main sampling loop
952-
for idx, timestep in tqdm(enumerate(timesteps), total=num_steps - 1):
958+
for idx, timestep in enumerate(tqdm(timesteps)):
953959
t = (timestep / self.unipc_scheduler.config.num_train_timesteps).expand(latents.shape[0])
954960
t = self.noise_scheduler.safe_clamp(t, min=self.noise_scheduler.min_t, max=self.noise_scheduler.max_t).to(
955961
latents.dtype
@@ -974,7 +980,7 @@ def sample(
974980
return_features_early=False,
975981
feature_indices={},
976982
return_logvar=False,
977-
skip_layers=skip_layers if idx >= skip_layers_start_percent * num_steps else None,
983+
skip_layers=skip_layers if idx >= skip_layers_start_fraction * num_steps else None,
978984
)
979985
flow_pred = flow_uncond + guidance_scale * (flow_pred - flow_uncond)
980986

fastgen/networks/Wan/network_causal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1231,7 +1231,7 @@ def sample(
12311231
x_next = x[:, :, start:end]
12321232
# Reset scheduler state (model_outputs, lower_order_nums, etc.)
12331233
self.unipc_scheduler.set_timesteps(num_inference_steps=sample_steps, device=noise.device)
1234-
for timestep in tqdm(timesteps, total=sample_steps - 1):
1234+
for timestep in tqdm(timesteps):
12351235
t = (timestep / time_rescale_factor).expand(batch_size)
12361236
x_cur = x_next
12371237
flow_pred = self(

fastgen/networks/WanI2V/network.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,14 +344,18 @@ def sample(
344344
num_steps: int = 40,
345345
shift: float = 3.0,
346346
skip_layers: Optional[List[int]] = None,
347-
skip_layers_start_percent: float = 0.0,
347+
skip_layers_start_fraction: float = 0.0,
348348
**kwargs,
349349
) -> torch.Tensor:
350350
"""Sample from the WanI2V model with proper first-frame conditioning.
351351
352352
For I2V models, the first latent frame must be preserved as the clean
353353
conditioning frame after each scheduler step.
354354
"""
355+
# Backward compatibility: the old argument name was skip_layers_start_percent.
356+
if "skip_layers_start_percent" in kwargs:
357+
skip_layers_start_fraction = kwargs.pop("skip_layers_start_percent")
358+
355359
assert self.schedule_type == "rf", f"{self.schedule_type} is not supported"
356360

357361
# Extract first_frame_cond for replacement after scheduler steps
@@ -368,7 +372,7 @@ def sample(
368372
latents = self.noise_scheduler.latents(noise=noise, t_init=t_init)
369373

370374
# Main sampling loop
371-
for idx, timestep in tqdm(enumerate(timesteps), total=num_steps - 1):
375+
for idx, timestep in enumerate(tqdm(timesteps)):
372376
t = (timestep / self.unipc_scheduler.config.num_train_timesteps).expand(latents.shape[0])
373377
t = self.noise_scheduler.safe_clamp(t, min=self.noise_scheduler.min_t, max=self.noise_scheduler.max_t).to(
374378
latents.dtype
@@ -393,7 +397,7 @@ def sample(
393397
return_features_early=False,
394398
feature_indices={},
395399
return_logvar=False,
396-
skip_layers=skip_layers if idx >= skip_layers_start_percent * num_steps else None,
400+
skip_layers=skip_layers if idx >= skip_layers_start_fraction * num_steps else None,
397401
)
398402
flow_pred = flow_uncond + guidance_scale * (flow_pred - flow_uncond)
399403

fastgen/networks/WanI2V/network_causal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ def sample(
454454
x_next = x[:, :, start:end]
455455
# Reset scheduler state (model_outputs, lower_order_nums, etc.)
456456
self.unipc_scheduler.set_timesteps(num_inference_steps=sample_steps, device=noise.device)
457-
for timestep in tqdm(timesteps, total=sample_steps - 1):
457+
for timestep in tqdm(timesteps):
458458
t = (timestep / time_rescale_factor).expand(batch_size)
459459
x_cur = x_next
460460
flow_pred = self(

fastgen/networks/cosmos_predict2/network.py

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
"""
1414

1515
from typing import Any, Dict, List, Optional, Set, Tuple, Union, Mapping
16+
import inspect
1617
import os
1718
from tqdm.auto import tqdm
1819
from einops import rearrange
1920

21+
import numpy as np
2022
import torch
2123
import torch.nn as nn
2224
import torch.nn.functional as F
@@ -54,6 +56,44 @@
5456
# ---------------------- DiT Network -----------------------
5557

5658

59+
class FlowKarrasUniPCScheduler(UniPCMultistepScheduler):
60+
"""UniPC whose sigmas follow the official Cosmos Predict2.5 Karras ramp.
61+
62+
A Karras schedule over ``[sigma_min, sigma_max]`` mapped into flow-matching
63+
units by ``sigma / (sigma + 1)``. Built here rather than through diffusers'
64+
``use_karras_sigmas``, which only gained that conversion in 0.37.0.
65+
"""
66+
67+
# Hardcoded in diffusers' _convert_to_karras; left unconfigurable to match.
68+
_rho: float = 7.0
69+
70+
def __init__(self, *args, sigma_min: float = 0.01, sigma_max: float = 200.0, **kwargs):
71+
# diffusers >= 0.37 declares these itself. Forward them there so they are not
72+
# recorded in the config's `_use_default_values`, which `from_config` discards;
73+
# register them ourselves on older versions that lack the parameters.
74+
base_params = inspect.signature(UniPCMultistepScheduler.__init__).parameters
75+
forwarded = {k: v for k, v in (("sigma_min", sigma_min), ("sigma_max", sigma_max)) if k in base_params}
76+
super().__init__(*args, **forwarded, **kwargs)
77+
if not forwarded:
78+
self.register_to_config(sigma_min=sigma_min, sigma_max=sigma_max)
79+
80+
def set_timesteps(
81+
self, num_inference_steps: Optional[int] = None, device: Union[str, torch.device] = None, **kwargs
82+
):
83+
if kwargs.get("sigmas") is not None:
84+
raise ValueError("FlowKarrasUniPCScheduler builds its own ramp; an explicit `sigmas` is unsupported.")
85+
assert num_inference_steps is not None, "num_inference_steps is required"
86+
super().set_timesteps(num_inference_steps=num_inference_steps, device=device, **kwargs)
87+
ramp = np.linspace(0.0, 1.0, num_inference_steps)
88+
min_inv_rho = self.config.sigma_min ** (1 / self._rho)
89+
max_inv_rho = self.config.sigma_max ** (1 / self._rho)
90+
sigmas = (max_inv_rho + ramp * (min_inv_rho - max_inv_rho)) ** self._rho
91+
sigmas = sigmas / (sigmas + 1)
92+
# sigmas stay on CPU to avoid per-step host/device traffic, as in diffusers.
93+
self.sigmas = torch.from_numpy(np.concatenate([sigmas, [0.0]]).astype(np.float32))
94+
self.timesteps = torch.from_numpy(sigmas * self.config.num_train_timesteps).to(device=device, dtype=torch.int64)
95+
96+
5797
class CosmosPredict2DiT(nn.Module):
5898
"""
5999
Cosmos Predict2 DiT (Diffusion Transformer) for video generation.
@@ -1100,7 +1140,7 @@ def sample(
11001140
guidance_scale: Optional[float] = 5.0,
11011141
num_steps: int = 50,
11021142
skip_layers: Optional[List[int]] = None,
1103-
skip_layers_start_percent: float = 0.0,
1143+
skip_layers_start_fraction: float = 0.0,
11041144
fps: Optional[torch.Tensor] = None,
11051145
conditioning_latents: Optional[torch.Tensor] = None,
11061146
num_conditioning_frames: int = 1,
@@ -1131,7 +1171,9 @@ def sample(
11311171
num_steps: Number of intervals in the official Karras schedule.
11321172
The sampler evaluates ``num_steps + 1`` timesteps.
11331173
skip_layers: List of transformer layers to skip (for skip-layer guidance).
1134-
skip_layers_start_percent: Percentage of steps before starting to skip layers.
1174+
skip_layers_start_fraction: Fraction in [0, 1] of the sampling steps to complete
1175+
before skip-layer guidance becomes active.
1176+
``skip_layers_start_percent`` is accepted as a deprecated alias.
11351177
fps: Frames per second tensor for temporal conditioning.
11361178
conditioning_latents: Latent frames to condition on for video2world mode,
11371179
shape (B, C, T, H, W). If provided, enables video2world mode.
@@ -1144,17 +1186,20 @@ def sample(
11441186
Returns:
11451187
The denoised sample tensor.
11461188
"""
1189+
# Backward compatibility: the old argument name was skip_layers_start_percent.
1190+
if "skip_layers_start_percent" in kwargs:
1191+
skip_layers_start_fraction = kwargs.pop("skip_layers_start_percent")
1192+
11471193
assert self.schedule_type == "rf", f"{self.schedule_type} is not supported"
11481194

1149-
# Match official Cosmos Predict2.5 inference. Diffusers uses the
1150-
# configured sigma bounds for its Karras conversion; the official
1151-
# `num_steps` denotes intervals, hence `num_steps + 1` ramp points.
1195+
# Match official Cosmos Predict2.5 inference: a Karras ramp over [0.01, 200] in
1196+
# flow-matching units, and `num_steps` denotes intervals, hence `num_steps + 1`
1197+
# ramp points.
11521198
if self.sample_scheduler is None:
1153-
self.sample_scheduler = UniPCMultistepScheduler(
1199+
self.sample_scheduler = FlowKarrasUniPCScheduler(
11541200
num_train_timesteps=1000,
11551201
prediction_type="flow_prediction",
11561202
use_flow_sigmas=True,
1157-
use_karras_sigmas=True,
11581203
sigma_min=0.01,
11591204
sigma_max=200.0,
11601205
)
@@ -1189,7 +1234,7 @@ def sample(
11891234
conditioning_latents_full = None
11901235
condition_mask = None
11911236
condition_mask_C = None
1192-
initial_noise = None
1237+
initial_latents = None
11931238

11941239
if video2world_mode:
11951240
B, C, T, H, W = latents.shape
@@ -1211,10 +1256,10 @@ def sample(
12111256
latents, v2w_condition
12121257
)
12131258

1214-
# Store initial noise for velocity replacement
1215-
initial_noise = latents.clone()
1259+
# Store the initial latents for velocity replacement
1260+
initial_latents = latents.clone()
12161261

1217-
for timestep in tqdm(timesteps, total=len(timesteps), desc="Sampling"):
1262+
for step_idx, timestep in enumerate(tqdm(timesteps, desc="Sampling")):
12181263
# Normalize timestep to [0, 1] range
12191264
t = (timestep / self.sample_scheduler.config.num_train_timesteps).expand(latents.shape[0])
12201265
t = self.noise_scheduler.safe_clamp(t, min=self.noise_scheduler.min_t, max=self.noise_scheduler.max_t).to(
@@ -1259,12 +1304,14 @@ def sample(
12591304
neg_cond_with_mask,
12601305
fps=fps,
12611306
conditional_frame_timestep=conditional_frame_timestep,
1307+
skip_layers=skip_layers if step_idx >= skip_layers_start_fraction * len(timesteps) else None,
12621308
)
12631309
velocity_pred = velocity_uncond + guidance_scale * (velocity_pred - velocity_uncond)
12641310

1265-
# Replace velocity for conditioning frames with analytical velocity: v = noise - x0
1311+
# Replace velocity for conditioning frames with the constant velocity that carries
1312+
# the initial latents onto the conditioning frames over the interval [0, t_init].
12661313
if video2world_mode and denoise_replace_gt_frames:
1267-
gt_velocity = initial_noise - conditioning_latents_full
1314+
gt_velocity = (initial_latents - conditioning_latents_full) / t_init
12681315
velocity_pred = gt_velocity * condition_mask_C + velocity_pred * (1 - condition_mask_C)
12691316

12701317
# Keep clean frames in the DiT input while UniPC evolves raw latents.

0 commit comments

Comments
 (0)