Skip to content

Commit adbdb92

Browse files
- run_inference_2_2: MagCache skip path with per-phase retention (forced-compute)
zones and an explicit residual reset at the high->low transformer boundary, driven by a single interleaved mag_ratios_base curve spanning both phases - generate_wan.py: pass use_magcache / magcache_thresh / magcache_K / retention_ratio through to the 2.2 pipeline - base_wan_27b.yml: default flow_shift=12.0 (official A14B sampling shift; sets the high->low boundary the ratios are aligned to) + MagCache params and the official mag_ratios_base - README: document MagCache for Wan2.2 (flow_shift requirement, ~1.82x speedup, SSIM/PSNR vs dense) - tests: wan_mag_cache_test.py (host-side validation/schedule/core tests + a TPU-only end-to-end smoke test)
1 parent 5e884cf commit adbdb92

5 files changed

Lines changed: 563 additions & 8 deletions

File tree

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,13 @@ 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). |
611+
612+
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.
613+
614+
> **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.
615+
616+
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.
610617

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

@@ -622,6 +629,14 @@ To generate images, run the following command:
622629
src/maxdiffusion/configs/base_wan_i2v_27b.yml \
623630
use_cfg_cache=True \
624631
...
632+
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+
...
625640
```
626641

627642
### Ring Attention
@@ -819,4 +834,4 @@ This script will automatically format your code with `pyink` and help you identi
819834
The full suite of -end-to end tests is in `tests` and `src/maxdiffusion/tests`. We run them with a nightly cadance.
820835
821836
## Profiling
822-
To learn how to enable ML Diagnostics and XProf profiling for your runs, please see our [ML Diagnostics Guide](docs/profiling.md).
837+
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
@@ -336,7 +336,11 @@ do_classifier_free_guidance: True
336336
height: 720
337337
width: 1280
338338
num_frames: 81
339-
flow_shift: 5.0
339+
# Official Wan2.2 T2V A14B sampling shift (wan_t2v_A14B.py: sample_shift=12.0). This sets where the
340+
# high->low transformer boundary falls in the step schedule (~step 26 of 40 at boundary_ratio 0.875),
341+
# which must match the schedule the seeded mag_ratios_base was calibrated at. (A smaller shift moves
342+
# the boundary earlier, which both degrades the base sample and misaligns MagCache's skip schedule.)
343+
flow_shift: 12.0
340344

341345
# Reference for below guidance scale and boundary values: https://github.com/Wan-Video/Wan2.2/blob/main/wan/configs/wan_t2v_A14B.py
342346
# guidance scale factor for low noise transformer
@@ -361,6 +365,20 @@ use_kv_cache: False
361365
# when predicted output change (based on accumulated latent/timestep drift) is small
362366
use_sen_cache: False
363367

368+
# MagCache (https://github.com/Zehong-Ma/MagCache) — skip transformer blocks when
369+
# the accumulated magnitude-ratio error stays below `magcache_thresh`, reusing the
370+
# cached block residual. `magcache_K` caps consecutive skips; `retention_ratio` is
371+
# the fraction at the start of each phase that always computes in full.
372+
use_magcache: False
373+
magcache_thresh: 0.04
374+
magcache_K: 2
375+
retention_ratio: 0.2
376+
# Calibrated average magnitude ratios, interleaved [cond, uncond, ...], length
377+
# num_inference_steps*2 (= 80 for 40 steps). Single curve spanning both phases;
378+
# the dip near the middle marks the high->low boundary. Seeded from the official
379+
# WAN 2.2 T2V ratios — recalibrate for this setup to tune the speedup/quality.
380+
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]
381+
364382
# Based on 3.4. in https://arxiv.org/pdf/2305.08891.pdf
365383
guidance_rescale: 0.0
366384
num_inference_steps: 40
@@ -422,4 +440,4 @@ enable_ssim: False
422440
# ML Diagnostics settings
423441
enable_ml_diagnostics: False
424442
profiler_gcs_path: ""
425-
enable_ondemand_xprof: False
443+
enable_ondemand_xprof: False

src/maxdiffusion/generate_wan.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ def call_pipeline(config, pipeline, prompt, negative_prompt):
157157
use_cfg_cache=config.use_cfg_cache,
158158
use_sen_cache=config.use_sen_cache,
159159
use_kv_cache=config.use_kv_cache,
160+
use_magcache=config.use_magcache,
161+
magcache_thresh=config.magcache_thresh,
162+
magcache_K=config.magcache_K,
163+
retention_ratio=config.retention_ratio,
160164
)
161165
else:
162166
raise ValueError(f"Unsupported model_name for T2V in config: {model_key}")
@@ -384,4 +388,4 @@ def main(argv: Sequence[str]) -> None:
384388

385389
if __name__ == "__main__":
386390
with transformer_engine_context():
387-
app.run(main)
391+
app.run(main)

src/maxdiffusion/pipelines/wan/wan_pipeline_2_2.py

Lines changed: 133 additions & 4 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,109 @@ 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+
cache_count = 0
342+
for step in range(num_inference_steps):
343+
t = jnp.array(scheduler_state.timesteps, dtype=jnp.int32)[step]
344+
345+
# Select transformer + guidance for this phase.
346+
if step_uses_high[step]:
347+
graphdef, state, rest = high_noise_graphdef, high_noise_state, high_noise_rest
348+
guidance_scale = guidance_scale_high
349+
kv_cache = kv_cache_high
350+
encoder_attention_mask = encoder_attention_mask_high
351+
else:
352+
graphdef, state, rest = low_noise_graphdef, low_noise_state, low_noise_rest
353+
guidance_scale = guidance_scale_low
354+
kv_cache = kv_cache_low
355+
encoder_attention_mask = encoder_attention_mask_low
356+
357+
# Boundary reset: the high-noise transformer's residual is meaningless to
358+
# the low-noise transformer, so start the low phase with a fresh cache.
359+
is_boundary = step > 0 and step_uses_high[step] != step_uses_high[step - 1]
360+
if is_boundary:
361+
cached_residual = None
362+
accumulated_state = init_magcache(num_inference_steps, retention_ratio, mag_ratios_base)[:6]
363+
364+
# Force a full compute inside either phase's warmup zone, at the boundary,
365+
# or until we have a residual to reuse.
366+
in_warmup = step < high_warmup_end or (high_noise_steps <= step < low_warmup_end)
367+
force_compute = in_warmup or is_boundary or cached_residual is None
368+
369+
skip_blocks, accumulated_state = magcache_step(
370+
step,
371+
mag_ratios,
372+
accumulated_state,
373+
magcache_thresh,
374+
magcache_K,
375+
use_magcache=(not force_compute),
376+
)
377+
378+
latents_doubled = jnp.concatenate([latents] * 2)
379+
timestep = jnp.broadcast_to(t, bsz * 2)
380+
noise_pred, _, residual_x_cur = transformer_forward_pass(
381+
graphdef,
382+
state,
383+
rest,
384+
latents_doubled,
385+
timestep,
386+
prompt_embeds_combined,
387+
do_classifier_free_guidance=True,
388+
guidance_scale=guidance_scale,
389+
skip_blocks=bool(skip_blocks),
390+
cached_residual=cached_residual,
391+
return_residual=True,
392+
kv_cache=kv_cache,
393+
rotary_emb=rotary_emb,
394+
encoder_attention_mask=encoder_attention_mask,
395+
)
396+
397+
if skip_blocks:
398+
cache_count += 1
399+
else:
400+
cached_residual = residual_x_cur
401+
402+
latents, scheduler_state = scheduler.step(scheduler_state, noise_pred, t, latents).to_tuple()
403+
404+
max_logging.log(
405+
f"[MagCache] Cached {cache_count}/{num_inference_steps} steps "
406+
f"({100*cache_count/num_inference_steps:.1f}% cache ratio), "
407+
f"high_noise_steps={high_noise_steps}, thresh={magcache_thresh}, K={magcache_K}"
408+
)
409+
return latents
410+
282411
# ── SenCache path (arXiv:2602.24208) ──
283412
if use_sen_cache and do_classifier_free_guidance:
284413
timesteps_np = np.array(scheduler_state.timesteps, dtype=np.int32)
@@ -711,4 +840,4 @@ def scan_body(carry, t):
711840
if profiler:
712841
latents.block_until_ready()
713842
profiler.stop()
714-
return latents
843+
return latents

0 commit comments

Comments
 (0)