|
12 | 12 | # See the License for the specific language governing permissions and |
13 | 13 | # limitations under the License. |
14 | 14 |
|
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 | +) |
16 | 23 | from ...models.wan.transformers.transformer_wan import WanModel |
17 | 24 | from typing import List, Union, Optional |
18 | 25 | from ...pyconfig import HyperParameters |
@@ -118,13 +125,24 @@ def __call__( |
118 | 125 | use_cfg_cache: bool = False, |
119 | 126 | use_sen_cache: bool = False, |
120 | 127 | 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, |
121 | 132 | ): |
122 | 133 | config = getattr(self, "config", None) |
123 | 134 | if max_sequence_length is None: |
124 | 135 | max_sequence_length = getattr(config, "max_sequence_length", 512) |
125 | 136 |
|
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 | + ) |
128 | 146 |
|
129 | 147 | if use_cfg_cache and (guidance_scale_low <= 1.0 or guidance_scale_high <= 1.0): |
130 | 148 | raise ValueError( |
@@ -183,6 +201,10 @@ def __call__( |
183 | 201 | use_sen_cache=use_sen_cache, |
184 | 202 | height=height, |
185 | 203 | 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, |
186 | 208 | ) |
187 | 209 |
|
188 | 210 | t_denoise_start = time.perf_counter() |
@@ -233,6 +255,10 @@ def run_inference_2_2( |
233 | 255 | height: int = 480, |
234 | 256 | config=None, |
235 | 257 | 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, |
236 | 262 | ): |
237 | 263 | """Denoising loop for WAN 2.2 T2V with optional caching acceleration. |
238 | 264 |
|
@@ -279,6 +305,111 @@ def run_inference_2_2( |
279 | 305 | high_transformer = nnx.merge(high_noise_graphdef, high_noise_state, high_noise_rest) |
280 | 306 | kv_cache_high, encoder_attention_mask_high = high_transformer.compute_kv_cache(prompt_embeds_combined) |
281 | 307 |
|
| 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 | + |
282 | 413 | # ── SenCache path (arXiv:2602.24208) ── |
283 | 414 | if use_sen_cache and do_classifier_free_guidance: |
284 | 415 | timesteps_np = np.array(scheduler_state.timesteps, dtype=np.int32) |
|
0 commit comments