Skip to content

Commit 007e041

Browse files
Add MagCache support for Wan2.2 I2V
- wan_pipeline_i2v_2p2.py: MagCache skip path for the dual-transformer I2V pipeline, mirroring the T2V 2.2 logic (per-phase retention/forced-compute zones, residual reset at the high->low boundary, single interleaved mag_ratios_base curve) with I2V-specific handling for the image condition (concat with latents + BFHWC<->BCFHW transposes) - generate_wan.py: pass use_magcache / magcache_thresh / magcache_K / retention_ratio through to the 2.2 I2V pipeline - base_wan_i2v_27b.yml: MagCache params + the official I2V-A14B mag_ratios_base, and boundary_ratio=0.900 to align the high->low switch with the curve (flow_shift stays at the I2V default of 5.0) - README: document MagCache for Wan2.2 I2V (settings + ~1.75x speedup, SSIM/PSNR vs dense) and add it to the caching support matrix
1 parent adbdb92 commit 007e041

4 files changed

Lines changed: 190 additions & 13 deletions

File tree

README.md

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -607,14 +607,16 @@ To generate images, run the following command:
607607
| --- | --- | --- | --- | --- |
608608
| **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. |
609609
| **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 ε. |
610-
| **MagCache** | `use_magcache: True` | Wan 2.1 T2V, Wan 2.2 T2V | ~1.8–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). |
610+
| **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). |
611611

612612
For Wan 2.2 (dual-transformer), MagCache keeps a single `mag_ratios_base` curve spanning both the high-noise and low-noise 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 `mag_ratios_base` in `base_wan_27b.yml` are seeded from the official Wan2.2 values; recalibrating them for your exact setup (model dtype / attention kernel) improves the speedup/quality trade-off.
613613

614614
> **Wan 2.2 T2V requires `flow_shift=12.0`** (the official A14B sampling shift; `base_wan_27b.yml` now defaults to it). `flow_shift` controls where the high→low noise boundary lands, which is the boundary the `mag_ratios_base` curve is calibrated against — a lower shift (e.g. the old `5.0`) moves the boundary several steps out of phase, so MagCache skips at the wrong steps and quality drops. This also corrects the off-spec dense baseline.
615615
616616
Measured on a v7x (Wan 2.2 A14B T2V, 720×1280, 81 frames, 40 steps, `flow_shift=12.0`, seeded ratios at `magcache_thresh=0.04`, `magcache_K=2`): **~1.82× speedup** (18/40 steps skipped, denoise 360s → 198s) at **SSIM ≈ 0.72 / PSNR ≈ 21.8 dB** versus the dense (`use_magcache=False`) render with the same seed/config. These reference-based metrics mostly reflect *trajectory divergence* — caching nudges the sampler onto a different but equally plausible sample — rather than visible degradation; the cached clips are visually hard to tell apart from dense. Recalibrating `mag_ratios_base` for your exact dtype/attention kernel tightens the metric gap further.
617617

618+
Wan 2.2 I2V (`base_wan_i2v_27b.yml`) uses the official I2V-A14B `mag_ratios_base` together with the I2V sampling settings (`flow_shift=5.0`, `boundary_ratio=0.900`) that the curve is aligned to. Measured on a v7x (Wan 2.2 A14B I2V, 720×1280, 81 frames, 40 steps, `magcache_thresh=0.06`, `magcache_K=2`): **~1.75× speedup** (17/40 steps skipped, 6.30s → 3.61s per step) at **SSIM ≈ 0.91 / PSNR ≈ 25.4 dB** versus the dense render. Fidelity is higher than T2V because the image conditioning anchors the sampling trajectory, leaving less room for divergence.
619+
618620
To enable a caching mechanism, set the corresponding flag in your config YAML or pass it as a command-line override:
619621

620622
```bash
@@ -630,14 +632,22 @@ To generate images, run the following command:
630632
use_cfg_cache=True \
631633
...
632634

633-
# Example: enable MagCache for Wan 2.2 T2V
634-
python src/maxdiffusion/generate_wan.py \
635-
src/maxdiffusion/configs/base_wan_27b.yml \
636-
use_magcache=True \
637-
magcache_thresh=0.04 \
638-
magcache_K=2 \
639-
...
640-
```
635+
# Example: enable MagCache for Wan 2.2 T2V
636+
python src/maxdiffusion/generate_wan.py \
637+
src/maxdiffusion/configs/base_wan_27b.yml \
638+
use_magcache=True \
639+
magcache_thresh=0.04 \
640+
magcache_K=2 \
641+
...
642+
643+
# Example: enable MagCache for Wan 2.2 I2V
644+
python src/maxdiffusion/generate_wan.py \
645+
src/maxdiffusion/configs/base_wan_i2v_27b.yml \
646+
use_magcache=True \
647+
magcache_thresh=0.06 \
648+
magcache_K=2 \
649+
...
650+
```
641651

642652
### Ring Attention
643653
We added ring attention support for Wan models. Below are the stats for one `720p` (81 frames) video generation (with CFG DP):

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: 4 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}")

src/maxdiffusion/pipelines/wan/wan_pipeline_i2v_2p2.py

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,14 @@
1414

1515
from maxdiffusion.image_processor import PipelineImageInput
1616
from maxdiffusion import max_logging
17-
from .wan_pipeline import WanPipeline, transformer_forward_pass, transformer_forward_pass_full_cfg, transformer_forward_pass_cfg_cache
17+
from .wan_pipeline import (
18+
WanPipeline,
19+
transformer_forward_pass,
20+
transformer_forward_pass_full_cfg,
21+
transformer_forward_pass_cfg_cache,
22+
init_magcache,
23+
magcache_step,
24+
)
1825
from ...models.wan.transformers.transformer_wan import WanModel
1926
from typing import List, Union, Optional, Tuple
2027
from ...pyconfig import HyperParameters
@@ -188,14 +195,35 @@ def __call__(
188195
use_cfg_cache: bool = False,
189196
use_sen_cache: bool = False,
190197
use_kv_cache: bool = False,
198+
use_magcache: bool = False,
199+
magcache_thresh: Optional[float] = None,
200+
magcache_K: Optional[int] = None,
201+
retention_ratio: Optional[float] = None,
191202
):
192203
config = getattr(self, "config", None)
193204
if max_sequence_length is None:
194205
max_sequence_length = getattr(config, "max_sequence_length", 512)
195206

207+
if magcache_thresh is None:
208+
magcache_thresh = getattr(config, "magcache_thresh", 0.06)
209+
if magcache_K is None:
210+
magcache_K = getattr(config, "magcache_K", 2)
211+
if retention_ratio is None:
212+
retention_ratio = getattr(config, "retention_ratio", 0.2)
213+
214+
if sum([use_cfg_cache, use_sen_cache, use_magcache]) > 1:
215+
raise ValueError("use_cfg_cache, use_sen_cache and use_magcache are mutually exclusive. Enable only one.")
216+
196217
if use_cfg_cache and use_sen_cache:
197218
raise ValueError("use_cfg_cache and use_sen_cache are mutually exclusive. Enable only one.")
198219

220+
if use_magcache and (guidance_scale_low <= 1.0 or guidance_scale_high <= 1.0):
221+
raise ValueError(
222+
f"use_magcache=True requires both guidance_scale_low > 1.0 and guidance_scale_high > 1.0 "
223+
f"(got {guidance_scale_low}, {guidance_scale_high}). "
224+
"MagCache requires classifier-free guidance to be enabled for both transformer phases."
225+
)
226+
199227
if use_cfg_cache and (guidance_scale_low <= 1.0 or guidance_scale_high <= 1.0):
200228
raise ValueError(
201229
f"use_cfg_cache=True requires both guidance_scale_low > 1.0 and guidance_scale_high > 1.0 "
@@ -309,6 +337,10 @@ def _process_image_input(img_input, height, width, num_videos_per_prompt):
309337
image_embeds=image_embeds,
310338
use_cfg_cache=use_cfg_cache,
311339
use_sen_cache=use_sen_cache,
340+
use_magcache=use_magcache,
341+
magcache_thresh=magcache_thresh,
342+
magcache_K=magcache_K,
343+
retention_ratio=retention_ratio,
312344
height=height,
313345
config=self.config,
314346
use_kv_cache=use_kv_cache,
@@ -366,6 +398,10 @@ def run_inference_2_2_i2v(
366398
scheduler_state,
367399
use_cfg_cache: bool = False,
368400
use_sen_cache: bool = False,
401+
use_magcache: bool = False,
402+
magcache_thresh: float = 0.06,
403+
magcache_K: int = 2,
404+
retention_ratio: float = 0.2,
369405
height: int = 480,
370406
config=None,
371407
use_kv_cache: bool = False,
@@ -404,6 +440,117 @@ def run_inference_2_2_i2v(
404440
prompt_embeds_combined, image_embeds_combined
405441
)
406442

443+
# ── MagCache path (Ma et al., https://github.com/Zehong-Ma/MagCache) ──
444+
# Skips the transformer blocks on steps whose accumulated magnitude-ratio error
445+
# stays below `magcache_thresh`, reusing the cached block residual instead. The
446+
# skip schedule is fully static (the ratios are calibration constants), so the
447+
# decision is made host-side and only a static `skip_blocks` bool crosses into
448+
# the forward pass. Dual-transformer handling: one calibrated curve spans both
449+
# phases (official `mag_ratios` layout), with a forced-compute zone at the start
450+
# of each phase and an explicit cache reset at the high->low boundary.
451+
if use_magcache and do_classifier_free_guidance:
452+
timesteps_np = np.array(scheduler_state.timesteps, dtype=np.int32)
453+
step_uses_high = [bool(timesteps_np[s] >= boundary) for s in range(num_inference_steps)]
454+
high_noise_steps = sum(step_uses_high)
455+
456+
mag_ratios_base = getattr(config, "mag_ratios_base", None) if config else None
457+
if mag_ratios_base is None:
458+
raise ValueError(
459+
"use_magcache=True requires config.mag_ratios_base — the magnitude ratios "
460+
"(interleaved cond/uncond, length num_inference_steps*2). Run the calibration "
461+
"pass or use the published WAN 2.2 I2V ratios."
462+
)
463+
464+
# Single state + single ratio curve spanning both phases (official layout).
465+
magcache_init = init_magcache(num_inference_steps, retention_ratio, mag_ratios_base)
466+
accumulated_state = magcache_init[:6]
467+
cached_residual = magcache_init[6]
468+
mag_ratios = magcache_init[8]
469+
470+
# Forced-compute ("retention") zones, in step units: the first
471+
# `retention_ratio` fraction of each phase always computes in full. This also
472+
# flushes the stale residual right after the boundary.
473+
high_warmup_end = int(high_noise_steps * retention_ratio)
474+
low_warmup_end = high_noise_steps + int((num_inference_steps - high_noise_steps) * retention_ratio)
475+
476+
condition_doubled = jnp.concatenate([condition] * 2)
477+
478+
cache_count = 0
479+
for step in range(num_inference_steps):
480+
t = jnp.array(scheduler_state.timesteps, dtype=jnp.int32)[step]
481+
482+
# Select transformer + guidance for this phase.
483+
if step_uses_high[step]:
484+
graphdef, state, rest = high_noise_graphdef, high_noise_state, high_noise_rest
485+
guidance_scale = guidance_scale_high
486+
kv_cache = kv_cache_high
487+
encoder_attention_mask = encoder_attention_mask_high
488+
else:
489+
graphdef, state, rest = low_noise_graphdef, low_noise_state, low_noise_rest
490+
guidance_scale = guidance_scale_low
491+
kv_cache = kv_cache_low
492+
encoder_attention_mask = encoder_attention_mask_low
493+
494+
# Boundary reset: the high-noise transformer's residual is meaningless to
495+
# the low-noise transformer, so start the low phase with a fresh cache.
496+
is_boundary = step > 0 and step_uses_high[step] != step_uses_high[step - 1]
497+
if is_boundary:
498+
cached_residual = None
499+
accumulated_state = init_magcache(num_inference_steps, retention_ratio, mag_ratios_base)[:6]
500+
501+
# Force a full compute inside either phase's warmup zone, at the boundary,
502+
# or until we have a residual to reuse.
503+
in_warmup = step < high_warmup_end or (high_noise_steps <= step < low_warmup_end)
504+
force_compute = in_warmup or is_boundary or cached_residual is None
505+
506+
skip_blocks, accumulated_state = magcache_step(
507+
step,
508+
mag_ratios,
509+
accumulated_state,
510+
magcache_thresh,
511+
magcache_K,
512+
use_magcache=(not force_compute),
513+
)
514+
515+
# I2V input: concat the image-conditioning latents on the channel axis, then
516+
# BFHWC -> BCFHW for the transformer (mirrors the dense/SenCache paths).
517+
latents_doubled = jnp.concatenate([latents, latents], axis=0)
518+
latent_model_input = jnp.concatenate([latents_doubled, condition_doubled], axis=-1)
519+
latent_model_input = jnp.transpose(latent_model_input, (0, 4, 1, 2, 3))
520+
timestep = jnp.broadcast_to(t, bsz * 2)
521+
noise_pred, _, residual_x_cur = transformer_forward_pass(
522+
graphdef,
523+
state,
524+
rest,
525+
latent_model_input,
526+
timestep,
527+
prompt_embeds_combined,
528+
do_classifier_free_guidance=True,
529+
guidance_scale=guidance_scale,
530+
encoder_hidden_states_image=image_embeds_combined,
531+
skip_blocks=bool(skip_blocks),
532+
cached_residual=cached_residual,
533+
return_residual=True,
534+
kv_cache=kv_cache,
535+
rotary_emb=rotary_emb,
536+
encoder_attention_mask=encoder_attention_mask,
537+
)
538+
noise_pred = jnp.transpose(noise_pred, (0, 2, 3, 4, 1))
539+
540+
if skip_blocks:
541+
cache_count += 1
542+
else:
543+
cached_residual = residual_x_cur
544+
545+
latents, scheduler_state = scheduler.step(scheduler_state, noise_pred, t, latents).to_tuple()
546+
547+
max_logging.log(
548+
f"[MagCache] Cached {cache_count}/{num_inference_steps} steps "
549+
f"({100*cache_count/num_inference_steps:.1f}% cache ratio), "
550+
f"high_noise_steps={high_noise_steps}, thresh={magcache_thresh}, K={magcache_K}"
551+
)
552+
return latents
553+
407554
# ── SenCache path (arXiv:2602.24208) ──
408555
if use_sen_cache and do_classifier_free_guidance:
409556
timesteps_np = np.array(scheduler_state.timesteps, dtype=np.int32)

0 commit comments

Comments
 (0)