Repository navigation
[Feat] support CFG Parallel for FLUX kontext #2281
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4cf10d0
31dd7ce
cc4d7f6
60f1055
a2f0c51
21e493f
8eda48d
6021012
54738de
432e2eb
fe880da
9630151
69901fa
1370d2d
1328ec7
353013b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,6 +24,7 @@ | |
| from vllm.model_executor.models.utils import AutoWeightsLoader | ||
|
|
||
| from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig | ||
| from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin | ||
| from vllm_omni.diffusion.distributed.utils import get_local_device | ||
| from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader | ||
| from vllm_omni.diffusion.models.flux import ( | ||
|
|
@@ -72,6 +73,7 @@ def post_process_func(images: torch.Tensor) -> list[PIL.Image.Image]: | |
| class FluxKontextPipeline( | ||
| nn.Module, | ||
| FluxPipelineMixin, | ||
| CFGParallelMixin, | ||
| SupportImageInput, | ||
| ProgressBarMixin, | ||
| DiffusionPipelineProfilerMixin, | ||
|
|
@@ -157,6 +159,7 @@ def __init__( | |
| self._attention_kwargs = None | ||
| self._joint_attention_kwargs = None | ||
| self._num_timesteps = None | ||
| self._current_timestep = None | ||
| self._interrupt = False | ||
| self._callback_tensor_inputs = ["latents", "prompt_embeds"] | ||
| self.latent_channels = self.vae.config.latent_channels if hasattr(self.vae, "config") else 16 | ||
|
|
@@ -465,6 +468,113 @@ def joint_attention_kwargs(self): | |
| def num_timesteps(self): | ||
| return self._num_timesteps | ||
|
|
||
| @property | ||
| def current_timestep(self) -> torch.Tensor | None: | ||
| return self._current_timestep | ||
|
|
||
| def predict_noise( | ||
| self, | ||
| hidden_states: torch.Tensor, | ||
| timestep: torch.Tensor, | ||
| guidance: torch.Tensor | None, | ||
| pooled_projections: torch.Tensor, | ||
| encoder_hidden_states: torch.Tensor, | ||
| txt_ids: torch.Tensor, | ||
| img_ids: torch.Tensor, | ||
| joint_attention_kwargs: dict[str, Any] | None, | ||
| return_dict: bool = False, | ||
| ) -> torch.Tensor: | ||
| return self.transformer( | ||
| hidden_states=hidden_states, | ||
| timestep=timestep, | ||
| guidance=guidance, | ||
| pooled_projections=pooled_projections, | ||
| encoder_hidden_states=encoder_hidden_states, | ||
| txt_ids=txt_ids, | ||
| img_ids=img_ids, | ||
| joint_attention_kwargs=joint_attention_kwargs, | ||
| return_dict=return_dict, | ||
| )[0] | ||
|
|
||
| def diffuse( | ||
| self, | ||
| prompt_embeds: torch.Tensor, | ||
| pooled_prompt_embeds: torch.Tensor, | ||
| negative_prompt_embeds: torch.Tensor | None, | ||
| negative_pooled_prompt_embeds: torch.Tensor | None, | ||
| latents: torch.Tensor, | ||
| image_latents: torch.Tensor | None, | ||
| latent_ids: torch.Tensor, | ||
| text_ids: torch.Tensor, | ||
| negative_text_ids: torch.Tensor | None, | ||
| timesteps: torch.Tensor, | ||
| do_true_cfg: bool, | ||
| guidance: torch.Tensor | None, | ||
| true_cfg_scale: float, | ||
| callback_on_step_end: Callable[[int, int, dict], None] | None = None, | ||
| callback_on_step_end_tensor_inputs: list[str] = ["latents"], | ||
| ) -> torch.Tensor: | ||
| self.scheduler.set_begin_index(0) | ||
| self.transformer.do_true_cfg = do_true_cfg | ||
| with self.progress_bar(total=len(timesteps)) as pbar: | ||
| for i, t in enumerate(timesteps): | ||
| if self.interrupt: | ||
| continue | ||
|
|
||
| self._current_timestep = t | ||
| latent_model_input = latents | ||
| if image_latents is not None: | ||
| latent_model_input = torch.cat([latents, image_latents], dim=1) | ||
| timestep = t.expand(latents.shape[0]).to(device=latents.device, dtype=latents.dtype) | ||
|
|
||
| positive_kwargs = { | ||
| "hidden_states": latent_model_input, | ||
| "timestep": timestep / 1000, | ||
| "guidance": guidance, | ||
| "pooled_projections": pooled_prompt_embeds, | ||
| "encoder_hidden_states": prompt_embeds, | ||
| "txt_ids": text_ids, | ||
| "img_ids": latent_ids, | ||
| "joint_attention_kwargs": self.joint_attention_kwargs, | ||
| "return_dict": False, | ||
| } | ||
| negative_kwargs = None | ||
| if do_true_cfg: | ||
| negative_kwargs = { | ||
| "hidden_states": latent_model_input, | ||
| "timestep": timestep / 1000, | ||
| "guidance": guidance, | ||
| "pooled_projections": negative_pooled_prompt_embeds, | ||
| "encoder_hidden_states": negative_prompt_embeds, | ||
| "txt_ids": negative_text_ids, | ||
| "img_ids": latent_ids, | ||
| "joint_attention_kwargs": self.joint_attention_kwargs, | ||
| "return_dict": False, | ||
| } | ||
|
|
||
| noise_pred = self.predict_noise_maybe_with_cfg( | ||
| do_true_cfg, | ||
| true_cfg_scale, | ||
| positive_kwargs, | ||
| negative_kwargs, | ||
| cfg_normalize=False, | ||
| output_slice=latents.size(1), | ||
| ) | ||
|
|
||
| latents = self.scheduler_step_maybe_with_cfg(noise_pred, t, latents, do_true_cfg) | ||
|
|
||
| if callback_on_step_end is not None: | ||
| callback_kwargs = {} | ||
| for k in callback_on_step_end_tensor_inputs: | ||
| callback_kwargs[k] = locals()[k] | ||
| callback_outputs = callback_on_step_end(self, i, t, callback_kwargs) | ||
|
|
||
| latents = callback_outputs.pop("latents", latents) | ||
| prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds) | ||
|
|
||
| pbar.update() | ||
| return latents | ||
|
|
||
| @torch.no_grad() | ||
| def forward( | ||
| self, | ||
|
|
@@ -503,8 +613,13 @@ def forward( | |
|
|
||
| if isinstance(first_prompt, str): | ||
| prompt = first_prompt | ||
| # Keep negative_prompt from function parameter (passed via API) | ||
| elif first_prompt: | ||
| prompt = first_prompt.get("prompt") or "" | ||
| # Prefer negative_prompt from prompt dict, fallback to function parameter | ||
| np_from_dict = first_prompt.get("negative_prompt") | ||
| if np_from_dict is not None: | ||
| negative_prompt = np_from_dict | ||
| # else: keep original prompt unchanged | ||
|
|
||
| # Handle image from prompt data | ||
|
|
@@ -524,8 +639,17 @@ def forward( | |
| height = req.sampling_params.height or height | ||
| width = req.sampling_params.width or width | ||
| num_inference_steps = req.sampling_params.num_inference_steps or num_inference_steps | ||
| sigmas = req.sampling_params.sigmas or sigmas | ||
| guidance_scale = req.sampling_params.guidance_scale or guidance_scale | ||
| true_cfg_scale = ( | ||
| req.sampling_params.true_cfg_scale if req.sampling_params.true_cfg_scale is not None else true_cfg_scale | ||
| ) | ||
| generator = req.sampling_params.generator or generator | ||
| num_images_per_prompt = ( | ||
| req.sampling_params.num_outputs_per_prompt | ||
| if req.sampling_params.num_outputs_per_prompt > 0 | ||
| else num_images_per_prompt | ||
| ) | ||
| latents = ( | ||
| req.sampling_params.extra_args.get("latents") | ||
| if req.sampling_params.extra_args.get("latents") is not None | ||
|
|
@@ -568,6 +692,8 @@ def forward( | |
| ) | ||
| do_true_cfg = true_cfg_scale > 1.0 and has_neg_prompt | ||
|
|
||
| do_true_cfg = do_true_cfg and self.check_cfg_parallel_validity(true_cfg_scale, has_neg_prompt) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. About the CFG scale, I notice in FLUX.1-Kontext-dev HF webpage, diffusers use
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FLUX.1-dev HF webpage uses |
||
|
|
||
| # 1. Prepare text embeddings | ||
| prompt_embeds, pooled_prompt_embeds, text_ids = self.encode_prompt( | ||
| prompt=prompt, | ||
|
|
@@ -651,57 +777,23 @@ def forward( | |
| self._joint_attention_kwargs = {} | ||
|
|
||
| # 5. Denoising loop | ||
| self.scheduler.set_begin_index(0) | ||
| with self.progress_bar(total=len(timesteps)) as pbar: | ||
| for i, t in enumerate(timesteps): | ||
| if self.interrupt: | ||
| continue | ||
|
|
||
| latent_model_input = latents | ||
| if image_latents is not None: | ||
| latent_model_input = torch.cat([latents, image_latents], dim=1) | ||
| timestep = t.expand(latents.shape[0]).to(latents.dtype) | ||
|
|
||
| noise_pred = self.transformer( | ||
| hidden_states=latent_model_input, | ||
| timestep=timestep / 1000, | ||
| guidance=guidance, | ||
| pooled_projections=pooled_prompt_embeds, | ||
| encoder_hidden_states=prompt_embeds, | ||
| txt_ids=text_ids, | ||
| img_ids=latent_ids, | ||
| joint_attention_kwargs=self.joint_attention_kwargs, | ||
| return_dict=False, | ||
| )[0] | ||
| noise_pred = noise_pred[:, : latents.size(1)] | ||
|
|
||
| if do_true_cfg: | ||
| neg_noise_pred = self.transformer( | ||
| hidden_states=latent_model_input, | ||
| timestep=timestep / 1000, | ||
| guidance=guidance, | ||
| pooled_projections=negative_pooled_prompt_embeds, | ||
| encoder_hidden_states=negative_prompt_embeds, | ||
| txt_ids=negative_text_ids, | ||
| img_ids=latent_ids, | ||
| joint_attention_kwargs=self.joint_attention_kwargs, | ||
| return_dict=False, | ||
| )[0] | ||
| neg_noise_pred = neg_noise_pred[:, : latents.size(1)] | ||
| noise_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred) | ||
|
|
||
| latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0] | ||
|
|
||
| if callback_on_step_end is not None: | ||
| callback_kwargs = {} | ||
| for k in callback_on_step_end_tensor_inputs: | ||
| callback_kwargs[k] = locals()[k] | ||
| callback_outputs = callback_on_step_end(self, i, t, callback_kwargs) | ||
|
|
||
| latents = callback_outputs.pop("latents", latents) | ||
| prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds) | ||
|
|
||
| pbar.update() | ||
| latents = self.diffuse( | ||
| prompt_embeds=prompt_embeds, | ||
| pooled_prompt_embeds=pooled_prompt_embeds, | ||
| negative_prompt_embeds=negative_prompt_embeds, | ||
| negative_pooled_prompt_embeds=negative_pooled_prompt_embeds, | ||
| latents=latents, | ||
| image_latents=image_latents, | ||
| latent_ids=latent_ids, | ||
| text_ids=text_ids, | ||
| negative_text_ids=negative_text_ids if do_true_cfg else None, | ||
| timesteps=timesteps, | ||
| do_true_cfg=do_true_cfg, | ||
| guidance=guidance, | ||
| true_cfg_scale=true_cfg_scale, | ||
| callback_on_step_end=callback_on_step_end, | ||
| callback_on_step_end_tensor_inputs=callback_on_step_end_tensor_inputs, | ||
| ) | ||
| if output_type == "latent": | ||
| image = latents | ||
| else: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: the mixin already imports from
vllm_omni.diffusion.distributed.parallel_state— is there a project-wide logger convention (e.g. from vllm) you could use instead ofimport logging+getLogger? Minor.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thx, fixed