Skip to content
FastVideo
Esc
↑↓navigate↵open⌘Jpreview
On this page

anyflow

AnyFlow on-policy distillation method.

Stage 2 of the AnyFlow two-stage recipe. Continues from a pretrained flow-map student and refines it via distribution-matching distillation (DMD2) where the student is rolled out for student_sample_steps Euler-flow steps from pure noise. One randomly-chosen step in the rollout is gradient-enabled (and broadcast across ranks so every worker agrees on which step to gradient-enable); the rest run under torch.no_grad.

Inherits DMD2Method for the alternating student / critic update machinery and the existing DMD VSD-with-fake-score loss. Overrides _student_rollout to drive the multi-step Euler-flow rollout with r = t_next (mean-velocity sampling — matches the AnyFlow paper's WanAnyFlowPipeline.training_rollout with use_mean_velocity=True).

Reference: pipeline_wan_anyflow.py::training_rollout in NVlabs/AnyFlow at commit 549236a.

fastvideo.train.methods.distribution_matching.anyflow.DMD2Method

DMD2Method(*, cfg: Any, role_models: dict[str, ModelBase])

DMD2 distillation algorithm (method layer).

Owns role model instances directly: - self.student — trainable student :class:ModelBase - self.teacher — frozen teacher :class:ModelBase - self.critic — trainable critic :class:ModelBase

fastvideo.train.methods.distribution_matching.anyflow.DMD2Method.single_train_step

single_train_step(batch: dict[str, Any], iteration: int) -> tuple[dict[str, torch.Tensor], dict[str, Any], dict[str, LogScalar]]

fastvideo.train.methods.distribution_matching.anyflow.DMD2Method.backward

backward(loss_map: dict[str, torch.Tensor], outputs: dict[str, Any], *, grad_accum_rounds: int = 1) -> None

fastvideo.train.methods.distribution_matching.anyflow.DMD2Method.get_optimizers

get_optimizers(iteration: int) -> list[torch.optim.Optimizer]

fastvideo.train.methods.distribution_matching.anyflow.DMD2Method.get_lr_schedulers

get_lr_schedulers(iteration: int) -> list[Any]

fastvideo.train.methods.distribution_matching.anyflow.DMD2Method.get_grad_clip_targets

get_grad_clip_targets(iteration: int) -> dict[str, torch.nn.Module]

fastvideo.train.methods.distribution_matching.anyflow.get_optional_float

get_optional_float(mapping: dict[str, Any], key: str, *, where: str) -> float | None

fastvideo.train.methods.distribution_matching.anyflow.get_optional_int

get_optional_int(mapping: dict[str, Any], key: str, *, where: str) -> int | None

fastvideo.train.methods.distribution_matching.anyflow.AnyFlowMethod

AnyFlowMethod(*, cfg: Any, role_models: dict[str, Any])

AnyFlow on-policy distillation (multi-step rollout).

Source
class AnyFlowMethod(DMD2Method):
    """AnyFlow on-policy distillation (multi-step rollout)."""
def __init__(
    self,
    *,
    cfg: Any,
    role_models: dict[str, Any],
) -> None:
    super().__init__(cfg=cfg, role_models=role_models)
    mcfg = self.method_config

    student_sample_steps = get_optional_int(mcfg, "student_sample_steps", where="method.student_sample_steps")
    if student_sample_steps is None:
        student_sample_steps = 4
    if int(student_sample_steps) <= 0:
        raise ValueError("method.student_sample_steps must be positive, "
                         f"got {student_sample_steps}")
    self._student_sample_steps = int(student_sample_steps)

    use_mean_velocity_raw = mcfg.get("use_mean_velocity", True)
    if not isinstance(use_mean_velocity_raw, bool):
        raise ValueError("method.use_mean_velocity must be a bool, "
                         f"got {type(use_mean_velocity_raw).__name__}")
    self._use_mean_velocity = bool(use_mean_velocity_raw)

    # Optional pinned rollout schedule (descending, absolute t-units).
    # Falls back to dmd_denoising_steps when absent.
    raw_t_list = mcfg.get("t_list_override", None)
    if raw_t_list is None:
        self._t_list_override: list[float] | None = None
    else:
        if not isinstance(raw_t_list, list) or not raw_t_list:
            raise ValueError("method.t_list_override must be a non-empty list of "
                             f"floats when set, got {raw_t_list!r}")
        t_list = [float(x) for x in raw_t_list]
        for i in range(len(t_list) - 1):
            if t_list[i] < t_list[i + 1]:
                raise ValueError("method.t_list_override must be descending, "
                                 f"got {t_list!r}")
        self._t_list_override = t_list

    # Scoring conditioning: AnyFlow scores against r=0 for the DMD branch.
    score_r_raw = mcfg.get("dmd_score_r_value", 0.0)
    try:
        self._dmd_score_r = float(score_r_raw)
    except (TypeError, ValueError) as exc:
        raise ValueError("method.dmd_score_r_value must be numeric, "
                         f"got {score_r_raw!r}") from exc

    # Optional teacher guidance scale for the DMD loss (carry over from
    # DMD2Method's behavior; default 1.0).
    guidance = get_optional_float(mcfg, "real_score_guidance_scale", where="method.real_score_guidance_scale")
    self._real_score_guidance = float(guidance) if guidance is not None else 1.0

# ------------------------------------------------------------------
# Rollout schedule

def _get_rollout_schedule(self, *, device: torch.device) -> torch.Tensor:
    """Build the descending timestep schedule used by the on-policy
    rollout. Length is ``num_steps + 1`` so ``num_steps`` Euler steps
    consume the full range.

    Order of precedence:
      1. ``method.t_list_override`` — used verbatim (absolute units).
      2. ``method.dmd_denoising_steps`` (inherited from DMD2) appended
         with a final 0 boundary if the last entry isn't already 0.
    """
    if self._t_list_override is not None:
        return torch.tensor(self._t_list_override, device=device, dtype=torch.float32)

    steps = self._get_denoising_step_list(device).to(dtype=torch.float32)
    if float(steps[-1].item()) != 0.0:
        zero = torch.zeros(1, device=device, dtype=torch.float32)
        steps = torch.cat([steps, zero], dim=0)
    return steps

def _broadcast_grad_step_index(
    self,
    num_steps: int,
    *,
    device: torch.device,
) -> int:
    """Pick the rollout step that gets gradient enabled. In distributed
    runs the choice is broadcast from rank 0 so every worker agrees."""
    if num_steps <= 0:
        raise ValueError("num_steps must be positive")
    if dist.is_initialized() and dist.get_rank() != 0:
        idx_tensor = torch.empty((1, ), dtype=torch.long, device=device)
    else:
        idx_tensor = torch.randint(0,
                                   num_steps, (1, ),
                                   device=device,
                                   dtype=torch.long,
                                   generator=self.cuda_generator)
    if dist.is_initialized():
        dist.broadcast(idx_tensor, src=0)
    return int(idx_tensor.item())

# ------------------------------------------------------------------
# Rollout

def _student_rollout(
    self,
    batch: Any,
    *,
    with_grad: bool,
) -> torch.Tensor:
    """Multi-step Euler-flow rollout from pure noise.

    Returns the predicted clean latent ``x_0`` after the chosen
    gradient step (or the final ``x`` after the last step if
    ``with_grad`` is False — used by the critic path).
    """
    latents = batch.latents
    if latents is None or latents.ndim != 5:
        raise RuntimeError("AnyFlow on-policy rollout requires TrainingBatch.latents "
                           "of shape [B, T, C, H, W] for shape templating")
    device = latents.device
    dtype = latents.dtype

    schedule = self._get_rollout_schedule(device=device)
    num_entries = int(schedule.numel())
    num_steps = num_entries - 1
    if num_steps <= 0:
        raise RuntimeError("rollout schedule must have at least two entries "
                           f"(got {num_entries})")
    if num_steps > self._student_sample_steps:
        # Trim to the configured cap, keeping the last (=0) boundary.
        schedule = torch.cat([schedule[:self._student_sample_steps], schedule[-1:]], dim=0)
        num_steps = self._student_sample_steps

    grad_step = self._broadcast_grad_step_index(num_steps, device=device) if with_grad else -1

    attn_kind: Literal["dense", "vsa"] = "vsa"
    n_train = float(self.student.num_train_timesteps)

    x = torch.randn(latents.shape, device=device, dtype=dtype, generator=self.cuda_generator)
    last_pred_x0: torch.Tensor | None = None
    batch_size = int(latents.shape[0])

    for i in range(num_steps):
        t_cur = schedule[i].expand(batch_size)
        t_next = schedule[i + 1].expand(batch_size)
        r = t_next if self._use_mean_velocity else t_cur

        enable_grad = bool(with_grad) and (i == grad_step)
        with torch.set_grad_enabled(enable_grad):
            v = self.student.predict_velocity_with_r(
                x,
                t_cur,
                r,
                batch,
                conditional=True,
                cfg_uncond=self._cfg_uncond,
                attn_kind=attn_kind,
            )
        # Euler step in absolute units: x ← x - ((t_cur - t_next) / N) * v.
        view = [-1] + [1] * (x.ndim - 1)
        dt = ((t_cur - t_next) / n_train).view(*view)
        x = x - dt * v

        if enable_grad:
            # We treat the rollout output (post-step) as a predicted
            # clean latent — AnyFlow's last Euler step lands at t=0.
            last_pred_x0 = x

    if last_pred_x0 is None:
        # No gradient step taken (with_grad=False path).
        last_pred_x0 = x

    if hasattr(batch, "dmd_latent_vis_dict"):
        batch.dmd_latent_vis_dict["generator_timestep"] = (schedule[-1].detach().clone())
    return last_pred_x0</code></pre></details>

Was this page helpful?