@@ -98,10 +98,13 @@ def forward(
9898 },
9999 )
100100
101- # Setup precision and autocast settings
102- # TODO(will): make the precision configurable for inference
103- # target_dtype = PRECISION_TO_TYPE[fastvideo_args.precision]
104- target_dtype = torch .bfloat16
101+ # Setup precision and autocast settings. Honor
102+ # pipeline_config.dit_precision so callers can opt into fp32
103+ # (e.g. numerical-precision-floor diagnostics for batched-CFG).
104+ # Default bf16 preserves pre-PR behaviour.
105+ from fastvideo .utils import PRECISION_TO_TYPE as _PRECISION_TO_TYPE
106+ _dit_precision = getattr (fastvideo_args .pipeline_config , "dit_precision" , "bf16" )
107+ target_dtype = _PRECISION_TO_TYPE .get (_dit_precision , torch .bfloat16 )
105108 autocast_enabled = (target_dtype != torch .float32 ) and not fastvideo_args .disable_autocast
106109
107110 # Get timesteps and calculate warmup steps
@@ -229,6 +232,82 @@ def forward(
229232 # written to, so we allocate once.
230233 v2v_zero_pad = torch .zeros_like (latents ) if batch .video_latent is not None else None
231234
235+ # Batched classifier-free guidance precomputation.
236+ # When enabled, the per-step cond + uncond pair runs as a single
237+ # batch=2 DiT forward instead of two sequential batch=1 forwards.
238+ # Disabled (sequential fallback) when V2V/I2V/TI2V or action /
239+ # camera conditioning is present: those carry batch=1 conditioning
240+ # tensors that aren't covered by this PR's batching path.
241+ _cfg_conditioning_present = (batch .video_latent is not None or batch .image_latent is not None
242+ or batch .pil_image is not None or len (batch .image_embeds ) > 0
243+ or batch .mouse_cond is not None or batch .keyboard_cond is not None
244+ or batch .c2ws_plucker_emb is not None or batch .camera_states is not None )
245+ use_batched_cfg = (fastvideo_args .use_batched_cfg and batch .do_classifier_free_guidance
246+ and not _cfg_conditioning_present )
247+
248+ if use_batched_cfg :
249+ assert neg_prompt_embeds is not None
250+
251+ # Shape compatibility check. Batched-CFG cats pos+neg along
252+ # dim 0, which requires matched seq lengths. The
253+ # TextEncodingStage's diffusers-style trim+pad-to-zero
254+ # (gated on `use_batched_cfg`) gives a common length
255+ # upstream — mirrors diffusers' canonical WanPipeline so the
256+ # padded zero positions are exactly what the model was
257+ # trained on. If shapes still differ here (user constructed
258+ # the batch by hand, or a pipeline doesn't go through our
259+ # TextEncodingStage), fall back to sequential to avoid
260+ # silently wrong output.
261+ def _shapes_match (neg_list : list [torch .Tensor ] | None , pos_list : list [torch .Tensor ] | None ) -> bool :
262+ if neg_list is None and pos_list is None :
263+ return True
264+ if neg_list is None or pos_list is None :
265+ return False
266+ if len (neg_list ) != len (pos_list ):
267+ return False
268+ return all (n .shape [1 :] == p .shape [1 :] for n , p in zip (neg_list , pos_list , strict = True ))
269+
270+ if not (_shapes_match (neg_prompt_embeds , prompt_embeds )
271+ and _shapes_match (batch .clip_embedding_neg , batch .clip_embedding_pos )
272+ and _shapes_match (batch .negative_attention_mask , batch .prompt_attention_mask )):
273+ logger .info ("use_batched_cfg disabled for this generation: pos/neg conditioning "
274+ "shapes differ. Falling back to sequential CFG. (TextEncodingStage "
275+ "pads to a common length when use_batched_cfg is set — this fallback "
276+ "fires when that upstream padding didn't run.)" )
277+ use_batched_cfg = False
278+
279+ if use_batched_cfg :
280+ # Element-wise cat to preserve the list-of-tensors call
281+ # convention (Wan's DiT picks element [0]; other DiTs may
282+ # use auxiliary entries). neg first to match the
283+ # chunk(2) -> (uncond, text) split inside the loop.
284+ assert neg_prompt_embeds is not None
285+ prompt_embeds_combined : list [torch .Tensor ] = [
286+ torch .cat ([n , p ], dim = 0 ) for n , p in zip (neg_prompt_embeds , prompt_embeds , strict = True )
287+ ]
288+
289+ def _cat_list_or_none (neg_val : list [torch .Tensor ] | None ,
290+ pos_val : list [torch .Tensor ] | None ) -> list [torch .Tensor ] | None :
291+ if neg_val is None and pos_val is None :
292+ return None
293+ assert neg_val is not None and pos_val is not None , (
294+ "batched CFG requires matched pos/neg conditioning tensors" )
295+ return [torch .cat ([n , p ], dim = 0 ) for n , p in zip (neg_val , pos_val , strict = True )]
296+
297+ combined_cond_kwargs = self .prepare_extra_func_kwargs (
298+ self .transformer .forward ,
299+ {
300+ "encoder_hidden_states_2" : _cat_list_or_none (batch .clip_embedding_neg , batch .clip_embedding_pos ),
301+ "encoder_attention_mask" : _cat_list_or_none (batch .negative_attention_mask ,
302+ batch .prompt_attention_mask ),
303+ },
304+ )
305+ guidance_expand_cfg = guidance_expand .repeat (2 ) if guidance_expand is not None else None
306+ else :
307+ prompt_embeds_combined = None # type: ignore[assignment]
308+ combined_cond_kwargs = None # type: ignore[assignment]
309+ guidance_expand_cfg = None
310+
232311 # Run denoising loop
233312 with self .progress_bar (total = num_inference_steps ) as progress_bar :
234313 for i , t in enumerate (timesteps ):
@@ -341,56 +420,87 @@ def forward(
341420 # support torch dynamo compilation. They pass in
342421 # attn_metadata, vllm_config, and num_tokens. We can pass in
343422 # fastvideo_args or training_args, and attn_metadata.
344- batch .is_cfg_negative = False
345- with set_forward_context (
346- current_timestep = i ,
347- attn_metadata = attn_metadata ,
348- forward_batch = batch ,
349- # fastvideo_args=fastvideo_args
350- ):
351- # Run transformer
352- noise_pred = current_model (
353- latent_model_input ,
354- prompt_embeds ,
355- t_expand ,
356- guidance = guidance_expand ,
357- ** image_kwargs ,
358- ** pos_cond_kwargs ,
359- ** action_kwargs ,
360- ** camera_kwargs ,
361- ** timesteps_r_kwarg ,
362- )
363-
364- if batch .do_classifier_free_guidance :
365- batch .is_cfg_negative = True
423+ if use_batched_cfg :
424+ # Single batch=2 forward: [uncond, cond] stacked
425+ # along batch dim. is_cfg_negative left False
426+ # (flag is write-only in this codebase; no
427+ # consumer branches on it).
428+ batch .is_cfg_negative = False
429+ latent_model_input_cfg = torch .cat ([latent_model_input , latent_model_input ], dim = 0 )
430+ t_expand_cfg = t_expand .repeat (2 ) if t_expand .dim () == 1 else t_expand .repeat (2 , 1 )
431+ timesteps_r_kwarg_cfg = timesteps_r_kwarg
432+ if "timestep_r" in timesteps_r_kwarg and timesteps_r_kwarg ["timestep_r" ] is not None :
433+ timesteps_r_kwarg_cfg = {"timestep_r" : timesteps_r_kwarg ["timestep_r" ].repeat (2 )}
434+ with set_forward_context (
435+ current_timestep = i ,
436+ attn_metadata = attn_metadata ,
437+ forward_batch = batch ,
438+ ):
439+ noise_pred_cfg = current_model (
440+ latent_model_input_cfg ,
441+ prompt_embeds_combined ,
442+ t_expand_cfg ,
443+ guidance = guidance_expand_cfg ,
444+ ** image_kwargs ,
445+ ** combined_cond_kwargs ,
446+ ** action_kwargs ,
447+ ** camera_kwargs ,
448+ ** timesteps_r_kwarg_cfg ,
449+ )
450+ noise_pred_uncond , noise_pred_text = noise_pred_cfg .chunk (2 , dim = 0 )
451+ noise_pred = noise_pred_uncond + current_guidance_scale * (noise_pred_text - noise_pred_uncond )
452+ else :
453+ batch .is_cfg_negative = False
366454 with set_forward_context (
367455 current_timestep = i ,
368456 attn_metadata = attn_metadata ,
369457 forward_batch = batch ,
458+ # fastvideo_args=fastvideo_args
370459 ):
371- noise_pred_uncond = current_model (
460+ # Run transformer
461+ noise_pred = current_model (
372462 latent_model_input ,
373- neg_prompt_embeds ,
463+ prompt_embeds ,
374464 t_expand ,
375465 guidance = guidance_expand ,
376466 ** image_kwargs ,
377- ** neg_cond_kwargs ,
467+ ** pos_cond_kwargs ,
378468 ** action_kwargs ,
379469 ** camera_kwargs ,
380470 ** timesteps_r_kwarg ,
381471 )
382472
383- noise_pred_text = noise_pred
384- noise_pred = noise_pred_uncond + current_guidance_scale * (noise_pred_text - noise_pred_uncond )
385-
386- # Apply guidance rescale if needed
387- if batch .guidance_rescale > 0.0 :
388- # Based on 3.4. in https://arxiv.org/pdf/2305.08891.pdf
389- noise_pred = self .rescale_noise_cfg (
390- noise_pred ,
391- noise_pred_text ,
392- guidance_rescale = batch .guidance_rescale ,
393- )
473+ if batch .do_classifier_free_guidance :
474+ batch .is_cfg_negative = True
475+ with set_forward_context (
476+ current_timestep = i ,
477+ attn_metadata = attn_metadata ,
478+ forward_batch = batch ,
479+ ):
480+ noise_pred_uncond = current_model (
481+ latent_model_input ,
482+ neg_prompt_embeds ,
483+ t_expand ,
484+ guidance = guidance_expand ,
485+ ** image_kwargs ,
486+ ** neg_cond_kwargs ,
487+ ** action_kwargs ,
488+ ** camera_kwargs ,
489+ ** timesteps_r_kwarg ,
490+ )
491+
492+ noise_pred_text = noise_pred
493+ noise_pred = noise_pred_uncond + current_guidance_scale * (noise_pred_text -
494+ noise_pred_uncond )
495+
496+ # Apply guidance rescale if needed (CFG-only path)
497+ if batch .do_classifier_free_guidance and batch .guidance_rescale > 0.0 :
498+ # Based on 3.4. in https://arxiv.org/pdf/2305.08891.pdf
499+ noise_pred = self .rescale_noise_cfg (
500+ noise_pred ,
501+ noise_pred_text ,
502+ guidance_rescale = batch .guidance_rescale ,
503+ )
394504 # Compute the previous noisy sample
395505 latents = self .scheduler .step (noise_pred , t , latents , ** extra_step_kwargs , return_dict = False )[0 ]
396506 if fastvideo_args .pipeline_config .ti2v_task and batch .pil_image is not None :
0 commit comments