|
14 | 14 |
|
15 | 15 | from maxdiffusion.image_processor import PipelineImageInput |
16 | 16 | 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 | +) |
18 | 25 | from ...models.wan.transformers.transformer_wan import WanModel |
19 | 26 | from typing import List, Union, Optional, Tuple |
20 | 27 | from ...pyconfig import HyperParameters |
@@ -188,14 +195,35 @@ def __call__( |
188 | 195 | use_cfg_cache: bool = False, |
189 | 196 | use_sen_cache: bool = False, |
190 | 197 | 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, |
191 | 202 | ): |
192 | 203 | config = getattr(self, "config", None) |
193 | 204 | if max_sequence_length is None: |
194 | 205 | max_sequence_length = getattr(config, "max_sequence_length", 512) |
195 | 206 |
|
| 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 | + |
196 | 217 | if use_cfg_cache and use_sen_cache: |
197 | 218 | raise ValueError("use_cfg_cache and use_sen_cache are mutually exclusive. Enable only one.") |
198 | 219 |
|
| 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 | + |
199 | 227 | if use_cfg_cache and (guidance_scale_low <= 1.0 or guidance_scale_high <= 1.0): |
200 | 228 | raise ValueError( |
201 | 229 | 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): |
309 | 337 | image_embeds=image_embeds, |
310 | 338 | use_cfg_cache=use_cfg_cache, |
311 | 339 | 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, |
312 | 344 | height=height, |
313 | 345 | config=self.config, |
314 | 346 | use_kv_cache=use_kv_cache, |
@@ -366,6 +398,10 @@ def run_inference_2_2_i2v( |
366 | 398 | scheduler_state, |
367 | 399 | use_cfg_cache: bool = False, |
368 | 400 | 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, |
369 | 405 | height: int = 480, |
370 | 406 | config=None, |
371 | 407 | use_kv_cache: bool = False, |
@@ -404,6 +440,117 @@ def run_inference_2_2_i2v( |
404 | 440 | prompt_embeds_combined, image_embeds_combined |
405 | 441 | ) |
406 | 442 |
|
| 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 | + |
407 | 554 | # ── SenCache path (arXiv:2602.24208) ── |
408 | 555 | if use_sen_cache and do_classifier_free_guidance: |
409 | 556 | timesteps_np = np.array(scheduler_state.timesteps, dtype=np.int32) |
|
0 commit comments