Skip to content

Commit 4a20b42

Browse files
committed
Finalize integration and configs
Signed-off-by: Julius Berner <mail@jberner.info>
1 parent 957b62f commit 4a20b42

9 files changed

Lines changed: 135 additions & 135 deletions

File tree

fastgen/configs/experiments/WanT2V/config_anyflow.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def create_config():
6767
# learns the guided flow directly. guidance_scale stays None — MeanFlow's
6868
# target-side eq. 19 fusion is a different mechanism.
6969
config.model.guidance_scale = None
70-
config.model.loss_config.guidance_fuse_scale = 3.0
70+
config.model.guidance_fuse_scale = 3.0
7171
config.model.cond_dropout_prob = 0.1
7272

7373
# ------ (t, r) sampling: shifted uniform pairs + AnyFlow buckets ------

fastgen/configs/experiments/WanT2V/config_anyflow_onpolicy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def create_config():
112112
config.model.loss_config.use_jvp_finite_diff = True
113113
config.model.loss_config.jvp_finite_diff_eps = 5e-3
114114
config.model.loss_config.rebalance_to_flow_matching = True
115-
config.model.loss_config.guidance_fuse_scale = 3.0
115+
config.model.guidance_fuse_scale = 3.0
116116
config.model.cond_dropout_prob = 0.1
117117
config.model.precision_amp_jvp = "float32"
118118
config.model.sample_t_cfg.flow_matching_ratio = 0.5

fastgen/configs/methods/config_anyflow.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,24 @@ class ModelConfig(DMD2ModelConfig):
5959
# the fixed student_sample_steps.
6060
student_sample_steps_list: Optional[List[int]] = None
6161

62+
# Prediction-side guidance fusion for the co-trained flow-map loss; see
63+
# `config_mean_flow.ModelConfig`.
64+
guidance_fuse_scale: Optional[float] = None
65+
6266
# Text dropout for the co-trained flow-map loss (reference drop_text_ratio).
6367
cond_dropout_prob: Optional[float] = None
6468
cond_keys_no_dropout: List[str] = attrs.field(factory=list)
6569

6670
# Precision for autocast in the co-trained loss JVP (None = training precision).
6771
precision_amp_jvp: str | None = None
6872

73+
# MeanFlow's target-side guidance knobs. AnyFlow typixcally guides on the
74+
# PREDICTION side (`guidance_fuse_scale`), so these stay at their
75+
# no-op defaults
76+
guidance_mixture_ratio: Optional[float] = None
77+
guidance_t_start: float = 0.0
78+
guidance_t_end: float = 1.0
79+
6980

7081
@attrs.define(slots=False)
7182
class Config(BaseConfig):

fastgen/configs/methods/config_mean_flow.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,6 @@ class LossConfig:
8686
# ("beta08", "gaussian", "uniform"). Multiplies the adaptive norm_method
8787
# weight above; None disables it.
8888
weight_type: Optional[str] = None
89-
# prediction-side guidance fusion scale (AnyFlow guidance distillation):
90-
# the conditional output is trained to be the guided flow directly via
91-
# (u_cond + (g-1) * u_uncond) / g against the raw data velocity. None
92-
# keeps MeanFlow's target-side guidance (guidance_scale).
93-
guidance_fuse_scale: Optional[float] = None
9489
# rebalance the flow-map / consistency (r < t) sample losses to the global
9590
# flow-matching (r = t) loss mean via a detached per-sample factor
9691
rebalance_to_flow_matching: bool = False
@@ -116,6 +111,12 @@ class ModelConfig(BaseModelConfig):
116111
# optimizer
117112
net_optimizer: DictConfig = attrs.field(factory=lambda: copy.deepcopy(RAdamOptimizerConfig))
118113

114+
# prediction-side guidance fusion scale (AnyFlow guidance distillation):
115+
# the conditional output is trained to be the guided flow directly via
116+
# (u_cond + (g-1) * u_uncond) / g against the raw data velocity. None
117+
# keeps MeanFlow's target-side guidance (guidance_scale).
118+
guidance_fuse_scale: Optional[float] = None
119+
119120
# condition dropout probability
120121
cond_dropout_prob: Optional[float] = None
121122

fastgen/methods/consistency_model/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ Two-stage training: frozen Stage-1 CM for `t < transition_t`, trainable student
7373

7474
## MeanFlow
7575

76-
**File:** [`mean_flow.py`](mean_flow.py) | **Reference:** [Geng et al., 2025](https://arxiv.org/abs/2505.13447)
76+
**File:** [`mean_flow.py`](mean_flow.py) | **References:** [Geng et al., 2025](https://arxiv.org/abs/2505.13447), [Sabour et al., 2025](https://arxiv.org/abs/2506.14603)
7777

7878
Learns average velocity between trajectory points: `x_r = x_t - (t-r) · u(x_t, t, r)`.
7979

@@ -82,6 +82,7 @@ Learns average velocity between trajectory points: `x_r = x_t - (t-r) · u(x_t,
8282
- `loss_config.use_jvp_finite_diff`: Use finite difference for JVP (e.g., for compatibility with Flash Attention and FSDP)
8383
- `sample_t_cfg`, `sample_r_cfg`: Configs of the distributions for sampling `t` and `r`
8484
- `sample_t_cfg.flow_matching_ratio`: Fraction of the batch with `r = t` (flow matching loss)
85+
- `guidance_scale`, `guidance_fuse_scale`: If `guidance_fuse_scale` is `None`, use target-side guidance (`guidance_scale`, 3-way mixture via `guidance_mixture_ratio`, t-window via `guidance_t_start` / `guidance_t_end`), otherwise use prediction-side guidance (conditional output learns the guided flow directly)
8586

8687

8788
**Configs:** [`EDM/config_mf_cifar10.py`](../../configs/experiments/EDM/config_mf_cifar10.py), [`DiT/config_mf_b.py`](../../configs/experiments/DiT/config_mf_b.py), [`DiT/config_mf_xl.py`](../../configs/experiments/DiT/config_mf_xl.py), [`WanT2V/config_mf.py`](../../configs/experiments/WanT2V/config_mf.py), [`WanT2V/config_anyflow.py`](../../configs/experiments/WanT2V/config_anyflow.py) (Stage 1 of [AnyFlow](../distribution_matching/README.md#anyflow))

fastgen/methods/consistency_model/mean_flow.py

Lines changed: 109 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,90 @@ def _drop_condition(self, condition: Any, neg_condition: Any) -> Tuple[Any, Opti
125125
return condition, keep
126126
raise TypeError(f"Unsupported condition type: {type(condition)}")
127127

128+
@torch.no_grad()
129+
def _get_velocity(
130+
self,
131+
x: torch.Tensor,
132+
z: torch.Tensor,
133+
t: torch.Tensor,
134+
condition: Optional[Any] = None,
135+
neg_condition: Optional[Any] = None,
136+
) -> Tuple[Any, torch.Tensor]:
137+
"""Regression target for the flow-map loss, plus the condition it was built from.
138+
139+
Two independent choices:
140+
141+
* ``loss_config.use_cd`` picks the target SOURCE -- the teacher, or the
142+
conditional data velocity.
143+
* ``guidance_fuse_scale`` guides the *prediction* in
144+
``_compute_mf_loss``, so the target needs no guidance of its own and we only
145+
drop the condition. Otherwise we guide the target too: through the teacher, or
146+
-- without one -- by the net's own cond/uncond pass.
147+
"""
148+
fuse_scale = self.config.guidance_fuse_scale
149+
if fuse_scale is not None:
150+
assert fuse_scale > 0, f"guidance_fuse_scale must be > 0, got {fuse_scale} (None disables fusion)"
151+
condition, _ = self._drop_condition(condition, neg_condition)
152+
153+
x_t = self.net.noise_scheduler.forward_process(x, z, t)
154+
155+
if self.loss_config.use_cd:
156+
dxt_dt = self.teacher(x_t, t, condition=condition, fwd_pred_type="flow")
157+
# Under fusion the target stays unguided, or it would be guided twice.
158+
if self.config.guidance_scale is not None and fuse_scale is None:
159+
guidance_scale = torch.where(
160+
((t >= self.config.guidance_t_start) & (t <= self.config.guidance_t_end)),
161+
self.config.guidance_scale,
162+
1.0,
163+
)
164+
guidance_scale = expand_like(guidance_scale, x_t).to(dtype=x_t.dtype)
165+
neg_dxt_dt = self.teacher(x_t, t, condition=neg_condition, fwd_pred_type="flow")
166+
dxt_dt = dxt_dt + (guidance_scale - 1.0) * (dxt_dt - neg_dxt_dt)
167+
else:
168+
dxt_dt = self.net.noise_scheduler.cond_velocity(x=x, eps=z, t=t)
169+
170+
# unconditional score estimation from meanflow eq (19). Skipped under
171+
# prediction-side fusion: that is this same guidance, moved onto the
172+
# prediction, and the dropout it needs already ran above.
173+
if fuse_scale is None and (
174+
self.config.guidance_scale is not None or self.config.guidance_mixture_ratio is not None
175+
):
176+
# Turn off dropout
177+
self.net.eval()
178+
neg_dxt_dt = self.net(x_t, t, r=t, condition=neg_condition, fwd_pred_type="flow")
179+
guidance_scale = self.config.guidance_scale or 1.0
180+
guidance_scale = torch.where(
181+
((t >= self.config.guidance_t_start) & (t <= self.config.guidance_t_end)),
182+
guidance_scale,
183+
1.0,
184+
)
185+
guidance_scale = expand_like(guidance_scale, x_t).to(dtype=x_t.dtype)
186+
187+
if self.config.guidance_mixture_ratio is None:
188+
guided_dxt_dt = neg_dxt_dt + guidance_scale * (dxt_dt - neg_dxt_dt)
189+
else:
190+
guidance_mixture_ratio = torch.where(
191+
((t >= self.config.guidance_t_start) & (t <= self.config.guidance_t_end)),
192+
self.config.guidance_mixture_ratio,
193+
0.0,
194+
)
195+
guidance_mixture_ratio = expand_like(guidance_mixture_ratio, x_t).to(dtype=x_t.dtype)
196+
cond_dxt_dt = self.net(x_t, t, r=t, condition=condition, fwd_pred_type="flow")
197+
guided_dxt_dt = (
198+
guidance_scale * dxt_dt
199+
+ (1.0 - guidance_scale - guidance_mixture_ratio) * neg_dxt_dt
200+
+ guidance_mixture_ratio * cond_dxt_dt
201+
)
202+
203+
self.net.train()
204+
condition, keep = self._drop_condition(condition, neg_condition)
205+
if keep is not None:
206+
# Same subset: a kept sample is conditional + guided, a
207+
# dropped one unconditional + unguided.
208+
dxt_dt = torch.where(expand_like(keep, dxt_dt), guided_dxt_dt, dxt_dt)
209+
210+
return condition, dxt_dt
211+
128212
def _estimate_jvp_finite_difference(
129213
self,
130214
net_wrapper: Callable[[torch.Tensor, torch.Tensor, torch.Tensor], torch.Tensor],
@@ -489,50 +573,34 @@ def _compute_mf_loss(
489573
z = torch.randn_like(real_data)
490574
x_t = self.net.noise_scheduler.forward_process(real_data, z, t)
491575

492-
guidance_fuse_scale = getattr(self.loss_config, "guidance_fuse_scale", None)
576+
condition, dxt_dt = self._get_velocity(real_data, z, t, condition=condition, neg_condition=neg_condition)
577+
# prevent JVP to use cached conversions (which can break the computational graph) that were created in the no_grad context of _get_velocity
578+
torch.clear_autocast_cache()
579+
u_theta_jvp = self._jvp(x_t, t, r, dxt_dt, condition=condition)
580+
assert not u_theta_jvp.requires_grad, "u_theta_jvp should not require gradients"
581+
582+
# additional forward pass to get u_theta with gradient; see also https://github.com/Gsunshine/py-meanflow?tab=readme-ov-file#note-on-jvp
583+
assert x_t.dtype == real_data.dtype, f"x_t.dtype: {x_t.dtype}, real_data.dtype: {real_data.dtype}"
584+
u_theta = self.net(
585+
x_t,
586+
t,
587+
r=r,
588+
condition=condition,
589+
fwd_pred_type="flow",
590+
)
591+
592+
guidance_fuse_scale = self.config.guidance_fuse_scale
493593
if guidance_fuse_scale is not None:
494-
# Guidance distillation fused on the PREDICTION side: the
495-
# conditional output is trained to be the guided flow directly.
496-
# The regression target stays the raw data velocity, the
497-
# unconditional branch is queried at the SAME (t, r) flow-map
498-
# slice, and the effective prediction is
499-
# (u_cond + (g - 1) * u_uncond) / g. Text dropout replaces the
500-
# condition with neg_condition for a random subset beforehand.
501-
g = float(guidance_fuse_scale)
502-
assert g > 0, f"guidance_fuse_scale must be > 0, got {g} (set it to None to disable guidance fusion)"
503-
assert (
504-
neg_condition is not None
505-
), "guidance_fuse_scale requires neg_condition: the unconditional branch is queried at the same (t, r)"
506-
condition, _ = self._drop_condition(condition, neg_condition)
507-
dxt_dt = self.net.noise_scheduler.cond_velocity(x=real_data, eps=z, t=t)
508-
torch.clear_autocast_cache()
509-
# dF/dt of the fused prediction: finite difference of the
510-
# conditional output divided by g (the unconditional derivative is
511-
# dropped).
512-
u_theta_jvp = self._jvp(x_t, t, r, dxt_dt, condition=condition) / g
513-
assert not u_theta_jvp.requires_grad, "u_theta_jvp should not require gradients"
514-
515-
assert x_t.dtype == real_data.dtype, f"x_t.dtype: {x_t.dtype}, real_data.dtype: {real_data.dtype}"
516-
u_theta = self.net(x_t, t, r=r, condition=condition, fwd_pred_type="flow")
594+
# Guidance distillation on the PREDICTION side (see `_get_velocity`): the
595+
# conditional output learns the guided flow directly, so only the prediction
596+
# changes. The uncond branch is queried at the SAME (t, r) flow-map slice,
597+
# giving (u_cond + (g - 1) * u_uncond) / g; dF/dt is then the conditional
598+
# finite difference over g, with the unconditional derivative dropped.
599+
u_theta_jvp = u_theta_jvp / guidance_fuse_scale
517600
with torch.no_grad():
518601
u_uncond = self.net(x_t, t, r=r, condition=neg_condition, fwd_pred_type="flow")
519-
u_theta = (u_theta + (g - 1.0) * u_uncond) / g
520-
else:
521-
condition, dxt_dt = self._get_velocity(real_data, z, t, condition=condition, neg_condition=neg_condition)
522-
# prevent JVP to use cached conversions (which can break the computational graph) that were created in the no_grad context of _get_velocity
523-
torch.clear_autocast_cache()
524-
u_theta_jvp = self._jvp(x_t, t, r, dxt_dt, condition=condition)
525-
assert not u_theta_jvp.requires_grad, "u_theta_jvp should not require gradients"
526-
527-
# additional forward pass to get u_theta with gradient; see also https://github.com/Gsunshine/py-meanflow?tab=readme-ov-file#note-on-jvp
528-
assert x_t.dtype == real_data.dtype, f"x_t.dtype: {x_t.dtype}, real_data.dtype: {real_data.dtype}"
529-
u_theta = self.net(
530-
x_t,
531-
t,
532-
r=r,
533-
condition=condition,
534-
fwd_pred_type="flow",
535-
)
602+
u_theta = (u_theta + (guidance_fuse_scale - 1.0) * u_uncond) / guidance_fuse_scale
603+
536604
mf_loss, tangent, loss_weight, warmup_weight = self._mf_pred_to_loss(
537605
u_theta=u_theta, u_theta_jvp=u_theta_jvp, x_t=x_t, dxt_dt=dxt_dt, t=t, r=r, iteration=iteration
538606
)
@@ -564,69 +632,6 @@ def __init__(self, config: ModelConfig):
564632
self.config = config
565633
self._init_flow_map_loss()
566634

567-
@torch.no_grad()
568-
def _get_velocity(
569-
self,
570-
x: torch.Tensor,
571-
z: torch.Tensor,
572-
t: torch.Tensor,
573-
condition: Optional[torch.Tensor] = None,
574-
neg_condition: Optional[torch.Tensor] = None,
575-
) -> Tuple[torch.Tensor, torch.Tensor]:
576-
x_t = self.net.noise_scheduler.forward_process(x, z, t)
577-
578-
if self.loss_config.use_cd:
579-
dxt_dt = self.teacher(x_t, t, condition=condition, fwd_pred_type="flow")
580-
if self.config.guidance_scale is not None:
581-
guidance_scale = torch.where(
582-
((t >= self.config.guidance_t_start) & (t <= self.config.guidance_t_end)),
583-
self.config.guidance_scale,
584-
1.0,
585-
)
586-
guidance_scale = expand_like(guidance_scale, x_t).to(dtype=x_t.dtype)
587-
neg_dxt_dt = self.teacher(x_t, t, condition=neg_condition, fwd_pred_type="flow")
588-
dxt_dt = dxt_dt + (guidance_scale - 1.0) * (dxt_dt - neg_dxt_dt)
589-
else:
590-
dxt_dt = self.net.noise_scheduler.cond_velocity(x=x, eps=z, t=t)
591-
592-
# unconditional score estimation from meanflow eq (19)
593-
if self.config.guidance_scale is not None or self.config.guidance_mixture_ratio is not None:
594-
# Turn off dropout
595-
self.net.eval()
596-
neg_dxt_dt = self.net(x_t, t, r=t, condition=neg_condition, fwd_pred_type="flow")
597-
guidance_scale = self.config.guidance_scale or 1.0
598-
guidance_scale = torch.where(
599-
((t >= self.config.guidance_t_start) & (t <= self.config.guidance_t_end)),
600-
guidance_scale,
601-
1.0,
602-
)
603-
guidance_scale = expand_like(guidance_scale, x_t).to(dtype=x_t.dtype)
604-
605-
if self.config.guidance_mixture_ratio is None:
606-
guided_dxt_dt = neg_dxt_dt + guidance_scale * (dxt_dt - neg_dxt_dt)
607-
else:
608-
guidance_mixture_ratio = torch.where(
609-
((t >= self.config.guidance_t_start) & (t <= self.config.guidance_t_end)),
610-
self.config.guidance_mixture_ratio,
611-
0.0,
612-
)
613-
guidance_mixture_ratio = expand_like(guidance_mixture_ratio, x_t).to(dtype=x_t.dtype)
614-
cond_dxt_dt = self.net(x_t, t, r=t, condition=condition, fwd_pred_type="flow")
615-
guided_dxt_dt = (
616-
guidance_scale * dxt_dt
617-
+ (1.0 - guidance_scale - guidance_mixture_ratio) * neg_dxt_dt
618-
+ guidance_mixture_ratio * cond_dxt_dt
619-
)
620-
621-
self.net.train()
622-
condition, keep = self._drop_condition(condition, neg_condition)
623-
if keep is not None:
624-
# Same subset: a kept sample is conditional + guided, a
625-
# dropped one unconditional + unguided.
626-
dxt_dt = torch.where(expand_like(keep, dxt_dt), guided_dxt_dt, dxt_dt)
627-
628-
return condition, dxt_dt
629-
630635
def single_train_step(
631636
self, data: Dict[str, Any], iteration: int
632637
) -> tuple[dict[str, torch.Tensor], dict[str, torch.Tensor | Callable]]:

0 commit comments

Comments
 (0)