Skip to content

Commit 304ef1a

Browse files
committed
Optimize WAN pipeline: hoist timesteps, batched text encoding, I2V concat/transpose churn reduction, replace prints with max_logging, and fix CFG cache multi-host sharding crashes
1 parent 5e884cf commit 304ef1a

8 files changed

Lines changed: 132 additions & 75 deletions

File tree

src/maxdiffusion/loaders/ltx2_lora_nnx_loader.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,6 @@ def translate_fn(nnx_path_str):
7676
# the merge_fn warns about unmatched keys in each dict, so we only warn about any leftovers
7777
unmatched_keys = set(h_state_dict) - set(transformer_state_dict) - set(connector_state_dict)
7878
if unmatched_keys:
79-
max_logging.log(
80-
f"{len(unmatched_keys)} key(s) in LoRA dictionary routed to no merge target: {unmatched_keys}"
81-
)
79+
max_logging.log(f"{len(unmatched_keys)} key(s) in LoRA dictionary routed to no merge target: {unmatched_keys}")
8280

8381
return pipeline

src/maxdiffusion/models/quantizations.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import jax.numpy as jnp
2626
from jax.tree_util import tree_flatten_with_path, tree_unflatten
2727
from typing import Tuple, Sequence
28+
from maxdiffusion import max_logging
2829

2930
# Params used to define mixed precision quantization configs
3031
DEFAULT = "__default__" # default config
@@ -139,7 +140,7 @@ def _get_quant_config(config):
139140
else:
140141
drhs_bits = 8
141142
drhs_accumulator_dtype = jnp.int32
142-
print(config.quantization_local_shard_count) # -1
143+
max_logging.log(config.quantization_local_shard_count) # -1
143144
drhs_local_aqt = aqt_config.LocalAqt(contraction_axis_shard_count=config.quantization_local_shard_count)
144145
return aqt_config.config_v4(
145146
fwd_bits=8,

src/maxdiffusion/pipelines/wan/wan_pipeline.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -997,7 +997,8 @@ def _prepare_model_inputs_i2v(
997997

998998
prompt_embeds = jax.device_put(prompt_embeds, data_sharding)
999999
negative_prompt_embeds = jax.device_put(negative_prompt_embeds, data_sharding)
1000-
image_embeds = jax.device_put(image_embeds, data_sharding)
1000+
if image_embeds is not None:
1001+
image_embeds = jax.device_put(image_embeds, data_sharding)
10011002

10021003
return prompt_embeds, negative_prompt_embeds, image_embeds, effective_batch_size
10031004

src/maxdiffusion/pipelines/wan/wan_pipeline_2_1.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ def __call__(
101101
magcache_K: Optional[int] = None,
102102
retention_ratio: Optional[float] = None,
103103
use_kv_cache: bool = False,
104+
output_type: str = "pil",
104105
):
105106
config = getattr(self, "config", None)
106107
if max_sequence_length is None:
@@ -170,6 +171,9 @@ def __call__(
170171
latents.block_until_ready()
171172
trace["denoise_total"] = time.perf_counter() - t_denoise_start
172173

174+
if output_type == "latent":
175+
return latents, trace
176+
173177
t_decode_start = time.perf_counter()
174178
video = self._decode_latents_to_video(latents, trace=trace)
175179
if hasattr(video, "block_until_ready"):
@@ -222,6 +226,21 @@ def run_inference_2_1(
222226
do_cfg = guidance_scale > 1.0
223227
bsz = latents.shape[0]
224228

229+
data_shards = 1
230+
try:
231+
if hasattr(latents, "sharding") and hasattr(latents.sharding, "mesh"):
232+
data_shards = latents.sharding.mesh.shape["data"] * latents.sharding.mesh.shape.get("fsdp", 1)
233+
except Exception:
234+
pass
235+
236+
if use_cfg_cache and do_cfg and bsz % data_shards != 0:
237+
from maxdiffusion.max_logging import MaxLogging
238+
239+
max_logging = MaxLogging()
240+
max_logging.log(
241+
f"Warning: Disabling CFG cache because batch size {bsz} is not divisible by data shards {data_shards}. This often happens with data_parallelism > 1 and per_device_batch_size = 1."
242+
)
243+
use_cfg_cache = False
225244
# Resolution-dependent CFG cache config (FasterCache / MixCache guidance)
226245
if height >= 720:
227246
# 720p: conservative — protect last 40%, interval=5
@@ -306,10 +325,9 @@ def run_inference_2_1(
306325
)
307326

308327
scan_diffusion_loop = getattr(config, "scan_diffusion_loop", False) if config else False
328+
timesteps = jnp.array(scheduler_state.timesteps, dtype=jnp.int32)
309329

310330
if scan_diffusion_loop and not use_magcache and not use_cfg_cache:
311-
timesteps = jnp.array(scheduler_state.timesteps, dtype=jnp.int32)
312-
313331
scheduler_state = scheduler_state.replace(last_sample=jnp.zeros_like(latents), step_index=jnp.array(0, dtype=jnp.int32))
314332

315333
def scan_body(carry, t):
@@ -365,7 +383,7 @@ def scan_body(carry, t):
365383
profiler = max_utils.Profiler(config)
366384
profiler.start()
367385

368-
t = jnp.array(scheduler_state.timesteps, dtype=jnp.int32)[step]
386+
t = timesteps[step]
369387

370388
if use_magcache and do_cfg:
371389
timestep = jnp.broadcast_to(t, bsz * 2 if do_cfg else bsz)

src/maxdiffusion/pipelines/wan/wan_pipeline_2_2.py

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import numpy as np
2626
from ...schedulers.scheduling_unipc_multistep_flax import FlaxUniPCMultistepScheduler
2727
from ... import max_utils
28-
from maxdiffusion import max_logging
2928

3029

3130
class WanPipeline2_2(WanPipeline):
@@ -118,6 +117,7 @@ def __call__(
118117
use_cfg_cache: bool = False,
119118
use_sen_cache: bool = False,
120119
use_kv_cache: bool = False,
120+
output_type: str = "pil",
121121
):
122122
config = getattr(self, "config", None)
123123
if max_sequence_length is None:
@@ -203,6 +203,9 @@ def __call__(
203203
latents.block_until_ready()
204204
trace["denoise_total"] = time.perf_counter() - t_denoise_start
205205

206+
if output_type == "latent":
207+
return latents, trace
208+
206209
t_decode_start = time.perf_counter()
207210
video = self._decode_latents_to_video(latents, trace=trace)
208211
if hasattr(video, "block_until_ready"):
@@ -252,6 +255,22 @@ def run_inference_2_2(
252255
do_classifier_free_guidance = guidance_scale_low > 1.0 or guidance_scale_high > 1.0
253256
bsz = latents.shape[0]
254257

258+
data_shards = 1
259+
try:
260+
if hasattr(latents, "sharding") and hasattr(latents.sharding, "mesh"):
261+
data_shards = latents.sharding.mesh.shape["data"] * latents.sharding.mesh.shape.get("fsdp", 1)
262+
except Exception:
263+
pass
264+
265+
if use_cfg_cache and do_classifier_free_guidance and bsz % data_shards != 0:
266+
from maxdiffusion.max_logging import MaxLogging
267+
268+
max_logging = MaxLogging()
269+
max_logging.log(
270+
f"Warning: Disabling CFG cache because batch size {bsz} is not divisible by data shards {data_shards}. This often happens with data_parallelism > 1 and per_device_batch_size = 1."
271+
)
272+
use_cfg_cache = False
273+
255274
prompt_embeds_combined = (
256275
jnp.concatenate([prompt_embeds, negative_prompt_embeds], axis=0) if do_classifier_free_guidance else prompt_embeds
257276
)
@@ -279,6 +298,8 @@ def run_inference_2_2(
279298
high_transformer = nnx.merge(high_noise_graphdef, high_noise_state, high_noise_rest)
280299
kv_cache_high, encoder_attention_mask_high = high_transformer.compute_kv_cache(prompt_embeds_combined)
281300

301+
timesteps = jnp.array(scheduler_state.timesteps, dtype=jnp.int32)
302+
282303
# ── SenCache path (arXiv:2602.24208) ──
283304
if use_sen_cache and do_classifier_free_guidance:
284305
timesteps_np = np.array(scheduler_state.timesteps, dtype=np.int32)
@@ -303,16 +324,18 @@ def run_inference_2_2(
303324
num_train_timesteps = float(scheduler.config.num_train_timesteps)
304325

305326
# SenCache state
306-
ref_noise_pred = None # y^r: cached denoiser output
307-
ref_latent = None # x^r: latent at last cache refresh
308-
ref_timestep = 0.0 # t^r: timestep (normalized to [0,1]) at last cache refresh
309-
accum_dx = 0.0 # accumulated ||Δx|| since last refresh
310-
accum_dt = 0.0 # accumulated |Δt| since last refresh
311-
reuse_count = 0 # consecutive cache reuses
312-
cache_count = 0
327+
ref_noise_pred = jnp.zeros(
328+
(bsz * 2, latents.shape[1], latents.shape[2], latents.shape[3], latents.shape[4]), dtype=latents.dtype
329+
)
330+
ref_latent = jnp.zeros_like(latents)
331+
ref_timestep = jnp.array(0.0, dtype=jnp.float32)
332+
accum_dx = jnp.array(0.0, dtype=jnp.float32)
333+
accum_dt = jnp.array(0.0, dtype=jnp.float32)
334+
reuse_count = jnp.array(0, dtype=jnp.int32)
335+
cache_count = jnp.array(0, dtype=jnp.int32)
313336

314337
for step in range(num_inference_steps):
315-
t = jnp.array(scheduler_state.timesteps, dtype=jnp.int32)[step]
338+
t = timesteps[step]
316339
t_float = float(timesteps_np[step]) / num_train_timesteps # normalize to [0, 1]
317340

318341
# Select transformer and guidance scale
@@ -358,10 +381,10 @@ def run_inference_2_2(
358381
)
359382
ref_noise_pred = noise_pred
360383
ref_latent = latents
361-
ref_timestep = t_float
362-
accum_dx = 0.0
363-
accum_dt = 0.0
364-
reuse_count = 0
384+
ref_timestep = jnp.array(t_float, dtype=jnp.float32)
385+
accum_dx = jnp.array(0.0, dtype=jnp.float32)
386+
accum_dt = jnp.array(0.0, dtype=jnp.float32)
387+
reuse_count = jnp.array(0, dtype=jnp.int32)
365388
latents, scheduler_state = scheduler.step(scheduler_state, noise_pred, t, latents).to_tuple()
366389
continue
367390

@@ -375,12 +398,10 @@ def run_inference_2_2(
375398
score = alpha_x * accum_dx + alpha_t * accum_dt
376399

377400
if score <= sen_epsilon and reuse_count < max_reuse:
378-
# Cache hit: reuse previous output
379401
noise_pred = ref_noise_pred
380402
reuse_count += 1
381403
cache_count += 1
382404
else:
383-
# Cache miss: full CFG forward pass
384405
latents_doubled = jnp.concatenate([latents] * 2)
385406
timestep = jnp.broadcast_to(t, bsz * 2)
386407
noise_pred, _, _ = transformer_forward_pass_full_cfg(
@@ -470,7 +491,7 @@ def run_inference_2_2(
470491
cached_noise_uncond = None
471492

472493
for step in range(num_inference_steps):
473-
t = jnp.array(scheduler_state.timesteps, dtype=jnp.int32)[step]
494+
t = timesteps[step]
474495
is_cache_step = step_is_cache[step]
475496

476497
# Select transformer and guidance scale based on precomputed schedule
@@ -607,8 +628,6 @@ def low_noise_branch(operands):
607628
)
608629

609630
if scan_diffusion_loop:
610-
timesteps = jnp.array(scheduler_state.timesteps, dtype=jnp.int32)
611-
612631
scheduler_state = scheduler_state.replace(last_sample=jnp.zeros_like(latents), step_index=jnp.array(0, dtype=jnp.int32))
613632

614633
def scan_body(carry, t):
@@ -657,7 +676,7 @@ def scan_body(carry, t):
657676
profiler = max_utils.Profiler(config)
658677
profiler.start()
659678

660-
t = jnp.array(scheduler_state.timesteps, dtype=jnp.int32)[step]
679+
t = timesteps[step]
661680

662681
if step_uses_high[step]:
663682
graphdef, state, rest = (

src/maxdiffusion/pipelines/wan/wan_pipeline_i2v_2p1.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -213,19 +213,17 @@ def __call__(
213213
last_image,
214214
)
215215

216-
def _process_image_input(img_input, height, width, num_videos_per_prompt):
216+
def _process_image_input(img_input, height, width):
217217
if img_input is None:
218218
return None
219219
tensor = self.video_processor.preprocess(img_input, height=height, width=width)
220220
jax_array = jnp.array(tensor.cpu().numpy())
221221
if jax_array.ndim == 3:
222222
jax_array = jax_array[None, ...] # Add batch dimension
223-
if num_videos_per_prompt > 1:
224-
jax_array = jnp.repeat(jax_array, num_videos_per_prompt, axis=0)
225223
return jax_array
226224

227-
image_tensor = _process_image_input(image, height, width, effective_batch_size)
228-
last_image_tensor = _process_image_input(last_image, height, width, effective_batch_size)
225+
image_tensor = _process_image_input(image, height, width)
226+
last_image_tensor = _process_image_input(last_image, height, width)
229227

230228
if rng is None:
231229
rng = jax.random.key(self.config.seed)
@@ -352,6 +350,8 @@ def run_inference_2_1_i2v(
352350
image_embeds_combined = image_embeds
353351
condition_combined = condition
354352

353+
condition_combined = jnp.transpose(condition_combined, (0, 4, 1, 2, 3))
354+
355355
transformer_obj = nnx.merge(graphdef, sharded_state, rest_of_state)
356356

357357
# Compute RoPE once as it only depends on shape
@@ -373,10 +373,9 @@ def run_inference_2_1_i2v(
373373
)
374374

375375
scan_diffusion_loop = getattr(config, "scan_diffusion_loop", False) if config else False
376+
timesteps = jnp.array(scheduler_state.timesteps, dtype=jnp.int32)
376377

377378
if scan_diffusion_loop and not use_magcache:
378-
timesteps = jnp.array(scheduler_state.timesteps, dtype=jnp.int32)
379-
380379
scheduler_state = scheduler_state.replace(last_sample=jnp.zeros_like(latents), step_index=jnp.array(0, dtype=jnp.int32))
381380

382381
def scan_body(carry, t):
@@ -386,9 +385,9 @@ def scan_body(carry, t):
386385
if do_cfg:
387386
latents_input = jnp.concatenate([current_latents, current_latents], axis=0)
388387

389-
latent_model_input = jnp.concatenate([latents_input, condition_combined], axis=-1)
388+
latents_input = jnp.transpose(latents_input, (0, 4, 1, 2, 3))
389+
latent_model_input = jnp.concatenate([latents_input, condition_combined], axis=1)
390390
timestep = jnp.broadcast_to(t, latents_input.shape[0])
391-
latent_model_input = jnp.transpose(latent_model_input, (0, 4, 1, 2, 3))
392391

393392
outputs = transformer_forward_pass(
394393
graphdef,
@@ -429,7 +428,7 @@ def scan_body(carry, t):
429428
profiler = max_utils.Profiler(config)
430429
profiler.start()
431430

432-
t = jnp.array(scheduler_state.timesteps, dtype=jnp.int32)[step]
431+
t = timesteps[step]
433432

434433
skip_blocks = False
435434
if use_magcache and do_cfg:
@@ -446,9 +445,9 @@ def scan_body(carry, t):
446445
if do_cfg:
447446
latents_input = jnp.concatenate([latents, latents], axis=0)
448447

449-
latent_model_input = jnp.concatenate([latents_input, condition_combined], axis=-1)
448+
latents_input = jnp.transpose(latents_input, (0, 4, 1, 2, 3))
449+
latent_model_input = jnp.concatenate([latents_input, condition_combined], axis=1)
450450
timestep = jnp.broadcast_to(t, latents_input.shape[0])
451-
latent_model_input = jnp.transpose(latent_model_input, (0, 4, 1, 2, 3))
452451

453452
outputs = transformer_forward_pass(
454453
graphdef,

0 commit comments

Comments
 (0)