Files
hao-ai-lab-FastVideo/v2/loop/sampler.py
T
SolitaryThinker b3ddf6014d [feat] UniRL/PromptRL joint LM+generator RL stress test + designv4
Stress-tests the v2 Card/Loop/Program design with a UniRL/PromptRL-style
joint RL recipe: a prompt-refiner LM expert and a flow generator expert,
two separate experts driven by two loop types in one request, both updated
simultaneously from a single RL reward.

- loop/sampler.py: flow_sde_step_with_logprob — FlowGRPO SDE rollout sampler
  (per-step Gaussian log-prob), distinct from the deterministic ODE serve step.
- request/params.py: gated sde_rollout/sde_noise_scale on DiffusionParams so
  the serve path stays byte-identical (default ODE).
- models/wan21/loop.py: gated SDE-rollout capture in WanDenoiseLoop.advance.
- models/backend.py: ToyPromptRefiner — a real REINFORCE categorical policy
  (the Qwen role), separate weights from the generator.
- models/unified/: the unified card+program — two disjoint experts (llm +
  transformer) on ar_decode + diffusion_denoise; the topological opposite of
  the Cosmos3 MoT card, same vocabulary.
- training/methods/unified_rl.py: joint GRPO — one reward -> group advantage
  -> LM token policy gradient + DiT FlowGRPO PPO; two LRs; prompt-only/joint
  flag; reuses the shared diffusion loop for rollout.
- training/weight_sync.py: WeightSyncPlan gains a component scope so the two
  experts version + cache-invalidate independently (LM sync never flushes the
  frozen text-encoder feature cache).
- tests/test_unified_rl.py: 9 tests incl. likelihood-based C2 identity, the
  two-loop interleave parity gate, joint vs prompt-only. Full suite 91 passed.

designv4.md: unified design doc reflecting v2 as built+tested, with the joint
RL stress test as the validating case study (the design held — new card +
new method, no new runtime primitive).
2026-07-04 17:15:34 +00:00

69 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Samplers as a small library (design_v3 §5.1: families use samplers as a library).
Flow-match Euler is shared by the Wan and LTX-2 denoise step bodies. On a GPU box the real
UniPC multistep / distilled schedulers are wrapped by the torch component adapters; this
numpy form is the CPU-testable, bit-reproducible reference used by the loop tests.
Flow-match interpolation: ``x_t = (1-σ_t)·x0 + σ_t·ε``; the model predicts velocity
``v = ε - x0``. Then a deterministic Euler step to σ_next is simply ``x_next = x_t + (σ_next-σ_t)·v``
(this is exactly LTX-2's ``velocity=(latents-denoised)/σ; latents += velocity·dt``).
"""
from __future__ import annotations
import numpy as np
def build_flow_sigmas(num_steps: int, shift: float = 1.0, terminal: float = 0.0) -> np.ndarray:
"""σ schedule from 1→terminal over ``num_steps+1`` points, with flow-shift applied.
Flow-shift: ``σ' = shift·σ / (1 + (shift-1)·σ)`` (Wan/LTX). shift=1 is the identity.
"""
base = np.linspace(1.0, terminal, num_steps + 1, dtype=np.float64)
if shift != 1.0:
base = shift * base / (1.0 + (shift - 1.0) * base)
return base
def x0_from_velocity(x_t: np.ndarray, velocity: np.ndarray, sigma_t: float) -> np.ndarray:
"""x0 = x_t - σ_t·v (flow prediction → clean sample; Wan ``x0 = x_t - σ·model_output``)."""
return x_t - sigma_t * velocity
def flow_match_euler_step(x_t: np.ndarray, velocity: np.ndarray,
sigma_t: float, sigma_next: float) -> np.ndarray:
"""One deterministic Euler step along the flow ODE."""
return x_t + (sigma_next - sigma_t) * velocity
def add_noise(x0: np.ndarray, noise: np.ndarray, sigma: float) -> np.ndarray:
"""Forward flow-match interpolation: x_σ = (1-σ)·x0 + σ·noise."""
return (1.0 - sigma) * x0 + sigma * noise
def flow_sde_step_with_logprob(x_t, velocity, sigma_t: float, sigma_next: float, *,
noise=None, prev_sample=None, noise_scale: float = 0.7):
"""FlowGRPO-style stochastic step + Gaussian log-prob (PromptRL/UniRL §6; FlowGRPO lineage).
The RL *rollout* sampler (vs the deterministic ODE ``flow_match_euler_step`` used at serve time).
Injects noise for exploration and returns the log-prob of the realized sample under the step's
Gaussian — the quantity the FlowGRPO PPO ratio uses. ``prev_sample=None`` samples a new step;
passing ``prev_sample`` (the rollout sample) recomputes its log-prob under the *current* velocity
(the ratio's numerator at update time). Returns ``(prev_sample, log_prob, mean, eff_std)``.
"""
x_t = np.asarray(x_t, dtype=np.float64)
velocity = np.asarray(velocity, dtype=np.float64)
s = min(float(sigma_t), 0.9999)
dt = float(sigma_next) - float(sigma_t) # negative (σ decreases)
std = float(np.sqrt(s / (1.0 - s)) * noise_scale)
denom = 2.0 * max(s, 1e-6)
mean = x_t * (1.0 + std ** 2 / denom * dt) + velocity * (1.0 + std ** 2 * (1.0 - s) / denom) * dt
eff_std = max(std * np.sqrt(max(-dt, 1e-12)), 1e-6)
if prev_sample is None:
n = noise if noise is not None else np.zeros_like(x_t)
prev_sample = mean + eff_std * np.asarray(n, dtype=np.float64)
prev_sample = np.asarray(prev_sample, dtype=np.float64)
var = eff_std ** 2
log_prob = float(np.mean(-((prev_sample - mean) ** 2) / (2.0 * var)
- np.log(eff_std) - 0.5 * np.log(2.0 * np.pi)))
return prev_sample.astype("float32"), log_prob, mean.astype("float32"), float(eff_std)