Skip to content

Commit 0956041

Browse files
Merge pull request #433 from HadarIngonyama:magcache_wan22_integration
PiperOrigin-RevId: 943962464
2 parents 9b48bf3 + bf91020 commit 0956041

7 files changed

Lines changed: 743 additions & 13 deletions

File tree

README.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,18 @@ To generate images, run the following command:
627627
| --- | --- | --- | --- | --- |
628628
| **CFG Cache** | `use_cfg_cache: True` | Wan 2.1 T2V, Wan 2.2 T2V/I2V | ~1.2x | FasterCache-style: caches the unconditional branch and applies FFT frequency-domain compensation on skipped steps. |
629629
| **SenCache** | `use_sen_cache: True` | Wan 2.2 T2V/I2V | ~1.4x | Sensitivity-Aware Caching ([arXiv:2602.24208](https://arxiv.org/abs/2602.24208)): predicts output change via first-order sensitivity S = α_x·‖Δx‖ + α_t·\|Δt\|. Skips the full CFG forward pass when predicted change is below tolerance ε. |
630+
| **MagCache** | `use_magcache: True` | Wan 2.1 T2V, Wan 2.2 T2V/I2V | ~1.75–1.9x | [MagCache](https://github.com/Zehong-Ma/MagCache): skips the transformer blocks and reuses the cached block residual when the accumulated magnitude-ratio error stays below `magcache_thresh`, capped at `magcache_K` consecutive skips. Uses a precalibrated per-step `mag_ratios_base` curve, so the skip schedule is deterministic (no data-dependent control flow). |
631+
632+
For Wan 2.2 (dual-transformer), MagCache uses a single `mag_ratios_base` curve across both phases, forces a full recompute for the first `retention_ratio` fraction of each phase, and resets the cached residual at the high→low boundary. The shipped curves are seeded from the official Wan2.2 values (`base_wan_27b.yml` for T2V, `base_wan_i2v_27b.yml` for I2V); recalibrate for your dtype/attention kernel to tighten the quality gap.
633+
634+
> **Wan 2.2 T2V requires `flow_shift=12.0`** — it sets where the high→low boundary lands, which is what `mag_ratios_base` is calibrated against. A lower shift (e.g. `5.0`) moves the boundary out of phase, so MagCache skips at the wrong steps and quality drops.
635+
636+
Benchmarks (7x, A14B, 720×1280, 81 frames, 40 steps, vs dense — SSIM/PSNR largely reflect trajectory divergence, not visible degradation):
637+
638+
| Variant | Settings | Speedup | SSIM / PSNR |
639+
| --- | --- | --- | --- |
640+
| T2V | `flow_shift=12.0`, `magcache_thresh=0.04`, `magcache_K=2` | ~1.82× (18/40 skipped) | 0.72 / 21.8 dB |
641+
| I2V | `flow_shift=5.0`, `boundary_ratio=0.900`, `magcache_thresh=0.06`, `magcache_K=2` | ~1.75× (17/40 skipped) | 0.91 / 25.4 dB |
630642

631643
To enable a caching mechanism, set the corresponding flag in your config YAML or pass it as a command-line override:
632644

@@ -642,7 +654,23 @@ To generate images, run the following command:
642654
src/maxdiffusion/configs/base_wan_i2v_27b.yml \
643655
use_cfg_cache=True \
644656
...
645-
```
657+
658+
# Example: enable MagCache for Wan 2.2 T2V
659+
python src/maxdiffusion/generate_wan.py \
660+
src/maxdiffusion/configs/base_wan_27b.yml \
661+
use_magcache=True \
662+
magcache_thresh=0.04 \
663+
magcache_K=2 \
664+
...
665+
666+
# Example: enable MagCache for Wan 2.2 I2V
667+
python src/maxdiffusion/generate_wan.py \
668+
src/maxdiffusion/configs/base_wan_i2v_27b.yml \
669+
use_magcache=True \
670+
magcache_thresh=0.06 \
671+
magcache_K=2 \
672+
...
673+
```
646674

647675
### Ring Attention
648676
We added ring attention support for Wan models. Below are the stats for one `720p` (81 frames) video generation (with CFG DP):
@@ -839,4 +867,4 @@ This script will automatically format your code with `pyink` and help you identi
839867
The full suite of -end-to end tests is in `tests` and `src/maxdiffusion/tests`. We run them with a nightly cadance.
840868
841869
## Profiling
842-
To learn how to enable ML Diagnostics and XProf profiling for your runs, please see our [ML Diagnostics Guide](docs/profiling.md).
870+
To learn how to enable ML Diagnostics and XProf profiling for your runs, please see our [ML Diagnostics Guide](docs/profiling.md).

src/maxdiffusion/configs/base_wan_27b.yml

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,11 @@ do_classifier_free_guidance: True
342342
height: 720
343343
width: 1280
344344
num_frames: 81
345-
flow_shift: 5.0
345+
# Official Wan2.2 T2V A14B sampling shift (wan_t2v_A14B.py: sample_shift=12.0). This sets where the
346+
# high->low transformer boundary falls in the step schedule (~step 26 of 40 at boundary_ratio 0.875),
347+
# which must match the schedule the seeded mag_ratios_base was calibrated at. (A smaller shift moves
348+
# the boundary earlier, which both degrades the base sample and misaligns MagCache's skip schedule.)
349+
flow_shift: 12.0
346350

347351
# Reference for below guidance scale and boundary values: https://github.com/Wan-Video/Wan2.2/blob/main/wan/configs/wan_t2v_A14B.py
348352
# guidance scale factor for low noise transformer
@@ -367,6 +371,20 @@ use_kv_cache: False
367371
# when predicted output change (based on accumulated latent/timestep drift) is small
368372
use_sen_cache: False
369373

374+
# MagCache (https://github.com/Zehong-Ma/MagCache) — skip transformer blocks when
375+
# the accumulated magnitude-ratio error stays below `magcache_thresh`, reusing the
376+
# cached block residual. `magcache_K` caps consecutive skips; `retention_ratio` is
377+
# the fraction at the start of each phase that always computes in full.
378+
use_magcache: False
379+
magcache_thresh: 0.04
380+
magcache_K: 2
381+
retention_ratio: 0.2
382+
# Calibrated average magnitude ratios, interleaved [cond, uncond, ...], length
383+
# num_inference_steps*2 (= 80 for 40 steps). Single curve spanning both phases;
384+
# the dip near the middle marks the high->low boundary. Seeded from the official
385+
# WAN 2.2 T2V ratios — recalibrate for this setup to tune the speedup/quality.
386+
mag_ratios_base: [1.0, 1.0, 1.00124, 1.00155, 0.99822, 0.99851, 0.99696, 0.99687, 0.99703, 0.99732, 0.9966, 0.99679, 0.99602, 0.99658, 0.99578, 0.99664, 0.99484, 0.9949, 0.99633, 0.996, 0.99659, 0.99683, 0.99534, 0.99549, 0.99584, 0.99577, 0.99681, 0.99694, 0.99563, 0.99554, 0.9944, 0.99473, 0.99594, 0.9964, 0.99466, 0.99461, 0.99453, 0.99481, 0.99389, 0.99365, 0.99391, 0.99406, 0.99354, 0.99361, 0.99283, 0.99278, 0.99268, 0.99263, 0.99057, 0.99091, 0.99125, 0.99126, 0.65523, 0.65252, 0.98808, 0.98852, 0.98765, 0.98736, 0.9851, 0.98535, 0.98311, 0.98339, 0.9805, 0.9806, 0.97776, 0.97771, 0.97278, 0.97286, 0.96731, 0.96728, 0.95857, 0.95855, 0.94385, 0.94385, 0.92118, 0.921, 0.88108, 0.88076, 0.80263, 0.80181]
387+
370388
# Based on 3.4. in https://arxiv.org/pdf/2305.08891.pdf
371389
guidance_rescale: 0.0
372390
num_inference_steps: 40
@@ -428,4 +446,4 @@ enable_ssim: False
428446
# ML Diagnostics settings
429447
enable_ml_diagnostics: False
430448
profiler_gcs_path: ""
431-
enable_ondemand_xprof: False
449+
enable_ondemand_xprof: False

src/maxdiffusion/configs/base_wan_i2v_27b.yml

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ width: 1280
337337
num_frames: 81
338338
flow_shift: 5.0
339339

340-
# Reference for below guidance scale and boundary values: https://github.com/Wan-Video/Wan2.2/blob/main/wan/configs/wan_t2v_A14B.py
340+
# Reference for below guidance scale and boundary values: https://github.com/Wan-Video/Wan2.2/blob/main/wan/configs/wan_i2v_A14B.py
341341
# guidance scale factor for low noise transformer
342342
guidance_scale_low: 3.0
343343

@@ -346,8 +346,10 @@ guidance_scale_high: 4.0
346346

347347
# The timestep threshold. If `t` is at or above this value,
348348
# the `high_noise_model` is considered as the required model.
349-
# timestep to switch between low noise and high noise transformer
350-
boundary_ratio: 0.875
349+
# timestep to switch between low noise and high noise transformer.
350+
# Official Wan2.2 I2V-A14B uses boundary=0.900 (vs 0.875 for T2V); this sets
351+
# the high->low expert switch the MagCache ratios below are calibrated against.
352+
boundary_ratio: 0.900
351353

352354
# Diffusion CFG cache (FasterCache-style)
353355
use_cfg_cache: False
@@ -359,6 +361,20 @@ use_kv_cache: False
359361
# SenCache: Sensitivity-Aware Caching (arXiv:2602.24208)
360362
use_sen_cache: False
361363

364+
# ── MagCache (Ma et al., https://github.com/Zehong-Ma/MagCache) ──
365+
# Skips the transformer blocks on steps whose accumulated magnitude-ratio error
366+
# stays below `magcache_thresh`, reusing the cached block residual. Official
367+
# Wan2.2 I2V recommendation: thresh 0.06, K 2, retention 0.1-0.2.
368+
use_magcache: False
369+
magcache_thresh: 0.06
370+
magcache_K: 2
371+
retention_ratio: 0.2
372+
# Average magnitude ratios, interleaved [cond, uncond, ...], length
373+
# num_inference_steps*2 (= 80 for 40 steps). Single curve spanning both phases;
374+
# the dip near step 15 marks the high->low boundary (boundary=0.900, flow_shift=5).
375+
# Official Wan2.2 I2V-A14B ratios (upstream MagCache, WanI2V branch).
376+
mag_ratios_base: [1.0, 1.0, 0.99191, 0.99144, 0.99356, 0.99337, 0.99326, 0.99285, 0.99251, 0.99264, 0.99393, 0.99366, 0.9943, 0.9943, 0.99276, 0.99288, 0.99389, 0.99393, 0.99274, 0.99289, 0.99316, 0.9931, 0.99379, 0.99377, 0.99268, 0.99271, 0.99222, 0.99227, 0.99175, 0.9916, 0.91076, 0.91046, 0.98931, 0.98933, 0.99087, 0.99088, 0.98852, 0.98855, 0.98895, 0.98896, 0.98806, 0.98808, 0.9871, 0.98711, 0.98613, 0.98618, 0.98434, 0.98435, 0.983, 0.98307, 0.98185, 0.98187, 0.98131, 0.98131, 0.9783, 0.97835, 0.97619, 0.9762, 0.97264, 0.9727, 0.97088, 0.97098, 0.96568, 0.9658, 0.96045, 0.96055, 0.95322, 0.95335, 0.94579, 0.94594, 0.93297, 0.93311, 0.91699, 0.9172, 0.89174, 0.89202, 0.8541, 0.85446, 0.79823, 0.79902]
377+
362378
# Based on 3.4. in https://arxiv.org/pdf/2305.08891.pdf
363379
guidance_rescale: 0.0
364380
num_inference_steps: 40

src/maxdiffusion/generate_wan.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ def call_pipeline(config, pipeline, prompt, negative_prompt):
124124
use_cfg_cache=config.use_cfg_cache,
125125
use_sen_cache=config.use_sen_cache,
126126
use_kv_cache=config.use_kv_cache,
127+
use_magcache=config.use_magcache,
128+
magcache_thresh=config.magcache_thresh,
129+
magcache_K=config.magcache_K,
130+
retention_ratio=config.retention_ratio,
127131
)
128132
else:
129133
raise ValueError(f"Unsupported model_name for I2V in config: {model_key}")
@@ -157,6 +161,10 @@ def call_pipeline(config, pipeline, prompt, negative_prompt):
157161
use_cfg_cache=config.use_cfg_cache,
158162
use_sen_cache=config.use_sen_cache,
159163
use_kv_cache=config.use_kv_cache,
164+
use_magcache=config.use_magcache,
165+
magcache_thresh=config.magcache_thresh,
166+
magcache_K=config.magcache_K,
167+
retention_ratio=config.retention_ratio,
160168
)
161169
else:
162170
raise ValueError(f"Unsupported model_name for T2V in config: {model_key}")

src/maxdiffusion/pipelines/wan/wan_pipeline_2_2.py

Lines changed: 134 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,14 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from .wan_pipeline import WanPipeline, transformer_forward_pass, transformer_forward_pass_full_cfg, transformer_forward_pass_cfg_cache
15+
from .wan_pipeline import (
16+
WanPipeline,
17+
transformer_forward_pass,
18+
transformer_forward_pass_full_cfg,
19+
transformer_forward_pass_cfg_cache,
20+
init_magcache,
21+
magcache_step,
22+
)
1623
from ...models.wan.transformers.transformer_wan import WanModel
1724
from typing import List, Union, Optional
1825
from ...pyconfig import HyperParameters
@@ -118,13 +125,24 @@ def __call__(
118125
use_cfg_cache: bool = False,
119126
use_sen_cache: bool = False,
120127
use_kv_cache: bool = False,
128+
use_magcache: bool = False,
129+
magcache_thresh: float = 0.04,
130+
magcache_K: int = 2,
131+
retention_ratio: float = 0.2,
121132
):
122133
config = getattr(self, "config", None)
123134
if max_sequence_length is None:
124135
max_sequence_length = getattr(config, "max_sequence_length", 512)
125136

126-
if use_cfg_cache and use_sen_cache:
127-
raise ValueError("use_cfg_cache and use_sen_cache are mutually exclusive. Enable only one.")
137+
if sum([use_cfg_cache, use_sen_cache, use_magcache]) > 1:
138+
raise ValueError("use_cfg_cache, use_sen_cache and use_magcache are mutually exclusive. Enable only one.")
139+
140+
if use_magcache and (guidance_scale_low <= 1.0 or guidance_scale_high <= 1.0):
141+
raise ValueError(
142+
f"use_magcache=True requires both guidance_scale_low > 1.0 and guidance_scale_high > 1.0 "
143+
f"(got {guidance_scale_low}, {guidance_scale_high}). "
144+
"MagCache reuses the cached residual across the doubled CFG batch, which must be enabled for both phases."
145+
)
128146

129147
if use_cfg_cache and (guidance_scale_low <= 1.0 or guidance_scale_high <= 1.0):
130148
raise ValueError(
@@ -183,6 +201,10 @@ def __call__(
183201
use_sen_cache=use_sen_cache,
184202
height=height,
185203
use_kv_cache=use_kv_cache,
204+
use_magcache=use_magcache,
205+
magcache_thresh=magcache_thresh,
206+
magcache_K=magcache_K,
207+
retention_ratio=retention_ratio,
186208
)
187209

188210
t_denoise_start = time.perf_counter()
@@ -233,6 +255,10 @@ def run_inference_2_2(
233255
height: int = 480,
234256
config=None,
235257
use_kv_cache: bool = False,
258+
use_magcache: bool = False,
259+
magcache_thresh: float = 0.04,
260+
magcache_K: int = 2,
261+
retention_ratio: float = 0.2,
236262
):
237263
"""Denoising loop for WAN 2.2 T2V with optional caching acceleration.
238264
@@ -279,6 +305,111 @@ def run_inference_2_2(
279305
high_transformer = nnx.merge(high_noise_graphdef, high_noise_state, high_noise_rest)
280306
kv_cache_high, encoder_attention_mask_high = high_transformer.compute_kv_cache(prompt_embeds_combined)
281307

308+
# ── MagCache path (Ma et al., https://github.com/Zehong-Ma/MagCache) ──
309+
# Skips the transformer blocks on steps whose accumulated magnitude-ratio error
310+
# stays below `magcache_thresh`, reusing the cached block residual instead.
311+
# The skip schedule is fully static (the ratios are calibration constants), so
312+
# the decision is made host-side and only a static `skip_blocks` bool crosses
313+
# into the forward pass. Dual-transformer handling: one calibrated curve spans
314+
# both phases (the official `mag_ratios` layout), with a forced-compute zone at
315+
# the start of each phase and an explicit cache reset at the high→low boundary.
316+
if use_magcache and do_classifier_free_guidance:
317+
timesteps_np = np.array(scheduler_state.timesteps, dtype=np.int32)
318+
step_uses_high = [bool(timesteps_np[s] >= boundary) for s in range(num_inference_steps)]
319+
high_noise_steps = sum(step_uses_high)
320+
321+
mag_ratios_base = getattr(config, "mag_ratios_base", None) if config else None
322+
if mag_ratios_base is None:
323+
raise ValueError(
324+
"use_magcache=True requires config.mag_ratios_base — the calibrated magnitude ratios "
325+
"(interleaved cond/uncond, length num_inference_steps*2). Run the calibration pass or "
326+
"use the published WAN 2.2 ratios."
327+
)
328+
329+
# Single state + single ratio curve spanning both phases (official layout).
330+
magcache_init = init_magcache(num_inference_steps, retention_ratio, mag_ratios_base)
331+
accumulated_state = magcache_init[:6]
332+
cached_residual = magcache_init[6]
333+
mag_ratios = magcache_init[8]
334+
335+
# Forced-compute ("retention") zones, in step units: the first
336+
# `retention_ratio` fraction of each phase always computes in full. This is
337+
# also what flushes the stale residual right after the boundary.
338+
high_warmup_end = int(high_noise_steps * retention_ratio)
339+
low_warmup_end = high_noise_steps + int((num_inference_steps - high_noise_steps) * retention_ratio)
340+
341+
# timesteps are fixed for the whole schedule; convert once outside the loop.
342+
timesteps = jnp.array(scheduler_state.timesteps, dtype=jnp.int32)
343+
cache_count = 0
344+
for step in range(num_inference_steps):
345+
t = timesteps[step]
346+
347+
# Select transformer + guidance for this phase.
348+
if step_uses_high[step]:
349+
graphdef, state, rest = high_noise_graphdef, high_noise_state, high_noise_rest
350+
guidance_scale = guidance_scale_high
351+
kv_cache = kv_cache_high
352+
encoder_attention_mask = encoder_attention_mask_high
353+
else:
354+
graphdef, state, rest = low_noise_graphdef, low_noise_state, low_noise_rest
355+
guidance_scale = guidance_scale_low
356+
kv_cache = kv_cache_low
357+
encoder_attention_mask = encoder_attention_mask_low
358+
359+
# Boundary reset: the high-noise transformer's residual is meaningless to
360+
# the low-noise transformer, so start the low phase with a fresh cache.
361+
is_boundary = step > 0 and step_uses_high[step] != step_uses_high[step - 1]
362+
if is_boundary:
363+
cached_residual = None
364+
accumulated_state = init_magcache(num_inference_steps, retention_ratio, mag_ratios_base)[:6]
365+
366+
# Force a full compute inside either phase's warmup zone, at the boundary,
367+
# or until we have a residual to reuse.
368+
in_warmup = step < high_warmup_end or (high_noise_steps <= step < low_warmup_end)
369+
force_compute = in_warmup or is_boundary or cached_residual is None
370+
371+
skip_blocks, accumulated_state = magcache_step(
372+
step,
373+
mag_ratios,
374+
accumulated_state,
375+
magcache_thresh,
376+
magcache_K,
377+
use_magcache=(not force_compute),
378+
)
379+
380+
latents_doubled = jnp.concatenate([latents] * 2)
381+
timestep = jnp.broadcast_to(t, bsz * 2)
382+
noise_pred, _, residual_x_cur = transformer_forward_pass(
383+
graphdef,
384+
state,
385+
rest,
386+
latents_doubled,
387+
timestep,
388+
prompt_embeds_combined,
389+
do_classifier_free_guidance=True,
390+
guidance_scale=guidance_scale,
391+
skip_blocks=bool(skip_blocks),
392+
cached_residual=cached_residual,
393+
return_residual=True,
394+
kv_cache=kv_cache,
395+
rotary_emb=rotary_emb,
396+
encoder_attention_mask=encoder_attention_mask,
397+
)
398+
399+
if skip_blocks:
400+
cache_count += 1
401+
else:
402+
cached_residual = residual_x_cur
403+
404+
latents, scheduler_state = scheduler.step(scheduler_state, noise_pred, t, latents).to_tuple()
405+
406+
max_logging.log(
407+
f"[MagCache] Cached {cache_count}/{num_inference_steps} steps "
408+
f"({100*cache_count/num_inference_steps:.1f}% cache ratio), "
409+
f"high_noise_steps={high_noise_steps}, thresh={magcache_thresh}, K={magcache_K}"
410+
)
411+
return latents
412+
282413
# ── SenCache path (arXiv:2602.24208) ──
283414
if use_sen_cache and do_classifier_free_guidance:
284415
timesteps_np = np.array(scheduler_state.timesteps, dtype=np.int32)

0 commit comments

Comments
 (0)