|
| 1 | +# Copyright 2026 Google LLC |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +import gc |
| 16 | +import os |
| 17 | +import time |
| 18 | +from typing import List, Union |
| 19 | +from PIL import Image |
| 20 | + |
| 21 | +import jax |
| 22 | +import jax.numpy as jnp |
| 23 | +import numpy as np |
| 24 | +from flax.linen import partitioning as nn_partitioning |
| 25 | + |
| 26 | +from ..pipeline_flax_utils import FlaxDiffusionPipeline |
| 27 | +from ...models.flux.transformers.transformer_flux_flax import FluxTransformer2DModel |
| 28 | +from ...models.vae_flax import FlaxAutoencoderKL |
| 29 | +from ...models.qwen3_flax import FlaxQwen3Model |
| 30 | +from ...schedulers.scheduling_flow_match_flax import FlaxFlowMatchScheduler, compute_empirical_mu |
| 31 | + |
| 32 | +from ...models.flux.util import ( |
| 33 | + pack_latents, |
| 34 | + unpack_latents, |
| 35 | + prepare_latent_image_ids, |
| 36 | + prepare_text_ids, |
| 37 | +) |
| 38 | + |
| 39 | + |
| 40 | +class FlaxFlux2KleinPipeline(FlaxDiffusionPipeline): |
| 41 | + """ |
| 42 | + Unified end-to-end inference pipeline for Flux.2-klein-4B and 9B models on JAX+TPU. |
| 43 | + Supports dynamic parameter offloading to Host CPU to optimize HBM footprint. |
| 44 | + """ |
| 45 | + |
| 46 | + def __init__( |
| 47 | + self, |
| 48 | + transformer: FluxTransformer2DModel, |
| 49 | + vae: FlaxAutoencoderKL, |
| 50 | + text_encoder: FlaxQwen3Model, |
| 51 | + tokenizer, |
| 52 | + scheduler: FlaxFlowMatchScheduler, |
| 53 | + config, |
| 54 | + mesh, |
| 55 | + **kwargs, |
| 56 | + ): |
| 57 | + super().__init__() |
| 58 | + self.register_modules( |
| 59 | + transformer=transformer, |
| 60 | + vae=vae, |
| 61 | + text_encoder=text_encoder, |
| 62 | + tokenizer=tokenizer, |
| 63 | + scheduler=scheduler, |
| 64 | + ) |
| 65 | + self._config = config |
| 66 | + self.mesh = mesh |
| 67 | + |
| 68 | + # JIT compilation cache |
| 69 | + self._jitted_qwen3_forward = None |
| 70 | + self._jitted_transformer_step = None |
| 71 | + self._jitted_vae_decode = None |
| 72 | + |
| 73 | + def _setup_jit_functions(self): |
| 74 | + if self._jitted_qwen3_forward is not None: |
| 75 | + return |
| 76 | + |
| 77 | + @jax.jit |
| 78 | + def qwen3_forward(q_params, ids, mask): |
| 79 | + return self.text_encoder.apply({"params": q_params}, input_ids=ids, attention_mask=mask) |
| 80 | + |
| 81 | + @jax.jit |
| 82 | + def transformer_step(t_params, latents, img_ids, prompt_embeds, txt_ids, vec, timestep, guidance): |
| 83 | + return self.transformer.apply( |
| 84 | + {"params": t_params}, |
| 85 | + hidden_states=latents, |
| 86 | + img_ids=img_ids, |
| 87 | + encoder_hidden_states=prompt_embeds, |
| 88 | + txt_ids=txt_ids, |
| 89 | + pooled_projections=vec, |
| 90 | + timestep=timestep, |
| 91 | + guidance=guidance, |
| 92 | + ) |
| 93 | + |
| 94 | + @jax.jit |
| 95 | + def vae_decode(v_params, latents_unpatched): |
| 96 | + return self.vae.apply({"params": v_params}, latents=latents_unpatched, method=self.vae.decode) |
| 97 | + |
| 98 | + self._jitted_qwen3_forward = qwen3_forward |
| 99 | + self._jitted_transformer_step = transformer_step |
| 100 | + self._jitted_vae_decode = vae_decode |
| 101 | + |
| 102 | + def _prepare_latents(self, config, batch_size, height, width): |
| 103 | + num_channels_latents = 32 |
| 104 | + latent_height = height // 8 |
| 105 | + latent_width = width // 8 |
| 106 | + latent_shape = (batch_size, num_channels_latents, latent_height, latent_width) |
| 107 | + |
| 108 | + if config.use_latents: |
| 109 | + print("use_latents is True. Loading latents from disk...") |
| 110 | + bundle_path = "src/maxdiffusion/tests/flux2_klein_complete_diagnostic_bundle.npz" |
| 111 | + if not os.path.exists(bundle_path): |
| 112 | + raise FileNotFoundError(f"Expected to find {bundle_path} but it was not found.") |
| 113 | + |
| 114 | + bundle = np.load(bundle_path) |
| 115 | + if "initial_pipeline_latents" in bundle: |
| 116 | + packed_latents = bundle["initial_pipeline_latents"] |
| 117 | + elif "step_0_cond_transformer_input_latents" in bundle: |
| 118 | + packed_latents = bundle["step_0_cond_transformer_input_latents"] |
| 119 | + else: |
| 120 | + raise KeyError( |
| 121 | + f"Neither 'initial_pipeline_latents' nor 'step_0_cond_transformer_input_latents' was found in {bundle_path}" |
| 122 | + ) |
| 123 | + |
| 124 | + print(f"Successfully loaded initial latents with shape: {packed_latents.shape}") |
| 125 | + if packed_latents.shape[0] != batch_size: |
| 126 | + packed_latents = np.repeat(packed_latents, batch_size, axis=0) |
| 127 | + latents = unpack_latents(packed_latents, batch_size, num_channels_latents, height, width) |
| 128 | + else: |
| 129 | + print(f"use_latents is False. Generating random gaussian noise with shape: {latent_shape}...") |
| 130 | + np.random.seed(42) # Fixed seed for parity consistency |
| 131 | + latents = np.random.randn(*latent_shape).astype(np.float32) |
| 132 | + |
| 133 | + return latents |
| 134 | + |
| 135 | + def __call__( |
| 136 | + self, |
| 137 | + prompt: Union[str, List[str]], |
| 138 | + params, |
| 139 | + vae_params, |
| 140 | + qwen3_params, |
| 141 | + vae_bn_mean, |
| 142 | + vae_bn_std, |
| 143 | + transformer_shardings, |
| 144 | + vae_shardings, |
| 145 | + qwen3_shardings, |
| 146 | + height: int = 1024, |
| 147 | + width: int = 1024, |
| 148 | + num_inference_steps: int = 4, |
| 149 | + batch_size: int = 1, |
| 150 | + use_latents: bool = False, |
| 151 | + offload_encoders: bool = False, |
| 152 | + measure_time: bool = False, |
| 153 | + output_dir: str = "output/", |
| 154 | + output_name: str = "flux2klein_generated_image.png", |
| 155 | + ): |
| 156 | + # 1. Setup JIT functions |
| 157 | + self._setup_jit_functions() |
| 158 | + |
| 159 | + # 2. Setup prompts and inputs |
| 160 | + if isinstance(prompt, str): |
| 161 | + prompts = [prompt] * batch_size |
| 162 | + else: |
| 163 | + prompts = prompt |
| 164 | + |
| 165 | + seq_len_img = (height // 16) * (width // 16) |
| 166 | + seq_len_txt = self._config.max_sequence_length |
| 167 | + |
| 168 | + # Load or generate latents |
| 169 | + latents_numpy = self._prepare_latents(self._config, batch_size, height, width) |
| 170 | + latents_jax = jnp.array(latents_numpy) |
| 171 | + latents_jax = pack_latents(latents_jax) |
| 172 | + |
| 173 | + # RoPE position IDs |
| 174 | + txt_ids_val = prepare_text_ids(batch_size, seq_len_txt) |
| 175 | + img_ids_val = prepare_latent_image_ids(batch_size, height // 16, width // 16) |
| 176 | + |
| 177 | + # Scheduler |
| 178 | + mu = compute_empirical_mu(seq_len_img, num_inference_steps) |
| 179 | + scheduler_state = self.scheduler.create_state() |
| 180 | + explicit_sigmas = jnp.linspace(1.0, 0.25, num_inference_steps) |
| 181 | + scheduler_state = self.scheduler.set_timesteps_ltx2( |
| 182 | + state=scheduler_state, |
| 183 | + num_inference_steps=num_inference_steps, |
| 184 | + shift=mu, |
| 185 | + sigmas=explicit_sigmas, |
| 186 | + ) |
| 187 | + |
| 188 | + trace = {} |
| 189 | + |
| 190 | + # --------------------------------------------------------------------- |
| 191 | + # PHASE A: Encode Prompt (Qwen3) |
| 192 | + # --------------------------------------------------------------------- |
| 193 | + print(f"[PHASE A] Encoding {len(prompts)} prompt(s) using JAX Qwen3 on TPU...") |
| 194 | + t0 = time.perf_counter() |
| 195 | + |
| 196 | + # Resolve tokenizer path from config |
| 197 | + tokenizer_path = self._config.tokenizer_model_name_or_path |
| 198 | + hf_home = os.environ.get("HF_HOME", os.path.expanduser("~/.cache/huggingface")) |
| 199 | + repo_cache = os.path.join( |
| 200 | + hf_home, "hub", f"models--{self._config.pretrained_model_name_or_path.replace('/', '--')}", "snapshots" |
| 201 | + ) |
| 202 | + if os.path.exists(repo_cache) and os.listdir(repo_cache): |
| 203 | + tokenizer_path = os.path.join(repo_cache, os.listdir(repo_cache)[0]) |
| 204 | + |
| 205 | + from transformers import Qwen2TokenizerFast |
| 206 | + |
| 207 | + try: |
| 208 | + tokenizer = Qwen2TokenizerFast.from_pretrained(tokenizer_path, local_files_only=True) |
| 209 | + except Exception: |
| 210 | + tokenizer = Qwen2TokenizerFast.from_pretrained(tokenizer_path, subfolder="tokenizer", local_files_only=True) |
| 211 | + |
| 212 | + # Tokenize |
| 213 | + messages = [{"role": "user", "content": p} for p in prompts] |
| 214 | + # In batch execution, we format and tokenize prompts |
| 215 | + templated_texts = [ |
| 216 | + tokenizer.apply_chat_template([msg], tokenize=False, add_generation_prompt=True, enable_thinking=False) |
| 217 | + for msg in messages |
| 218 | + ] |
| 219 | + inputs = tokenizer(templated_texts, return_tensors="np", padding="max_length", truncation=True, max_length=seq_len_txt) |
| 220 | + prompt_ids = jnp.array(inputs["input_ids"]) |
| 221 | + prompt_mask = jnp.array(inputs["attention_mask"]) |
| 222 | + |
| 223 | + # Dynamically move Qwen3 parameters to TPU |
| 224 | + if offload_encoders: |
| 225 | + print(" Moving Qwen3 parameters to TPU HBM...") |
| 226 | + qwen3_params_tpu = jax.device_put(qwen3_params, qwen3_shardings) |
| 227 | + else: |
| 228 | + qwen3_params_tpu = qwen3_params |
| 229 | + |
| 230 | + # Run Text Encoding |
| 231 | + hidden_states, all_hidden_states = self._jitted_qwen3_forward(qwen3_params_tpu, prompt_ids, prompt_mask) |
| 232 | + |
| 233 | + # Stack layers 9, 18, 27 to form prompt embeddings |
| 234 | + h_9 = all_hidden_states[9] |
| 235 | + h_18 = all_hidden_states[18] |
| 236 | + h_27 = all_hidden_states[27] |
| 237 | + out = jnp.stack([h_9, h_18, h_27], axis=1) |
| 238 | + # Transpose shape to [B, seq_len, 3*hidden_size] |
| 239 | + prompt_embeds_jax = jnp.transpose(out, (0, 2, 1, 3)).reshape((batch_size, seq_len_txt, -1)) |
| 240 | + prompt_embeds_jax.block_until_ready() |
| 241 | + |
| 242 | + trace["prompt_encoding"] = time.perf_counter() - t0 |
| 243 | + print(f" -> [TIMING] Prompt Encoding (Qwen3): {trace['prompt_encoding']:.4f} seconds ⏱️") |
| 244 | + |
| 245 | + if offload_encoders: |
| 246 | + print(" Releasing Qwen3 parameters from TPU HBM...") |
| 247 | + del qwen3_params_tpu |
| 248 | + gc.collect() |
| 249 | + |
| 250 | + # --------------------------------------------------------------------- |
| 251 | + # PHASE B: Denoising Loop (Flux Transformer) |
| 252 | + # --------------------------------------------------------------------- |
| 253 | + print(f"[PHASE B] Running {num_inference_steps}-step E2E Denoising Loop on a batch of {batch_size} images...") |
| 254 | + t0 = time.perf_counter() |
| 255 | + |
| 256 | + # Dynamically move Flux Transformer parameters to TPU |
| 257 | + if offload_encoders: |
| 258 | + print(" Moving Flux Transformer parameters to TPU HBM...") |
| 259 | + params_tpu = jax.device_put(params, transformer_shardings) |
| 260 | + else: |
| 261 | + params_tpu = params |
| 262 | + |
| 263 | + guidance_vec_val = jnp.array([self._config.guidance_scale] * batch_size) |
| 264 | + vec_val = jnp.zeros((batch_size, 768)) |
| 265 | + |
| 266 | + with self.mesh, nn_partitioning.axis_rules(self._config.logical_axis_rules): |
| 267 | + for step_idx in range(num_inference_steps): |
| 268 | + timestep = scheduler_state.timesteps[step_idx] |
| 269 | + t_vec = jnp.array([timestep] * batch_size) |
| 270 | + |
| 271 | + # Execute transformer forward pass step |
| 272 | + model_output = self._jitted_transformer_step( |
| 273 | + params_tpu, latents_jax, img_ids_val, prompt_embeds_jax, txt_ids_val, vec_val, t_vec, guidance_vec_val |
| 274 | + ) |
| 275 | + |
| 276 | + # Update latents using FlowMatch step |
| 277 | + latents_jax = self.scheduler.step( |
| 278 | + state=scheduler_state, |
| 279 | + model_output=model_output.sample, |
| 280 | + timestep=scheduler_state.timesteps[step_idx], |
| 281 | + sample=latents_jax, |
| 282 | + ).prev_sample |
| 283 | + |
| 284 | + # Print progress |
| 285 | + sigma_val = scheduler_state.sigmas[step_idx] |
| 286 | + print(f" -> Step {step_idx}: Timestep = {scheduler_state.timesteps[step_idx]:.4f}, Sigma = {sigma_val:.4f}") |
| 287 | + |
| 288 | + latents_jax.block_until_ready() |
| 289 | + |
| 290 | + trace["denoise_loop"] = time.perf_counter() - t0 |
| 291 | + print(f" -> [TIMING] Denoising Loop (Flux): {trace['denoise_loop']:.4f} seconds ⏱️") |
| 292 | + |
| 293 | + if offload_encoders: |
| 294 | + print(" Releasing Flux Transformer parameters from TPU HBM...") |
| 295 | + del params_tpu |
| 296 | + gc.collect() |
| 297 | + |
| 298 | + # --------------------------------------------------------------------- |
| 299 | + # PHASE C: Decode Latents (VAE Decoder) |
| 300 | + # --------------------------------------------------------------------- |
| 301 | + print("[PHASE C] Decoding final latents to RGB image using JAX VAE decoder on TPU...") |
| 302 | + t0 = time.perf_counter() |
| 303 | + |
| 304 | + # Apply Channel-wise Batch Normalization Scaling in packed sequence format (denormalize) |
| 305 | + vae_bn_mean_seq = vae_bn_mean.reshape(1, 1, 128) |
| 306 | + vae_bn_std_seq = vae_bn_std.reshape(1, 1, 128) |
| 307 | + latents_bn = latents_jax * vae_bn_std_seq + vae_bn_mean_seq |
| 308 | + |
| 309 | + # Unpack packed latents back to spatial grid |
| 310 | + latents_unpacked = unpack_latents(latents_bn, batch_size, 32, height, width) |
| 311 | + |
| 312 | + # Dynamically move VAE parameters to TPU |
| 313 | + if offload_encoders: |
| 314 | + print(" Moving VAE parameters to TPU HBM...") |
| 315 | + vae_params_tpu = jax.device_put(vae_params, vae_shardings) |
| 316 | + else: |
| 317 | + vae_params_tpu = vae_params |
| 318 | + |
| 319 | + # Decode VAE latents to RGB pixels |
| 320 | + decoded_out = self._jitted_vae_decode(vae_params_tpu, latents_unpacked) |
| 321 | + # VAE output is in decoded_out.sample |
| 322 | + images_rgb = decoded_out.sample |
| 323 | + images_rgb.block_until_ready() |
| 324 | + |
| 325 | + trace["vae_decode"] = time.perf_counter() - t0 |
| 326 | + print(f" -> [TIMING] VAE Decoding: {trace['vae_decode']:.4f} seconds ⏱️") |
| 327 | + |
| 328 | + if offload_encoders: |
| 329 | + print(" Releasing VAE parameters from TPU HBM...") |
| 330 | + del vae_params_tpu |
| 331 | + gc.collect() |
| 332 | + |
| 333 | + # --------------------------------------------------------------------- |
| 334 | + # POST-PROCESS: Format and Save Outputs |
| 335 | + # --------------------------------------------------------------------- |
| 336 | + print("Postprocessing and saving generated images...") |
| 337 | + saved_paths = [] |
| 338 | + # Clamp pixels and scale to [0, 255] |
| 339 | + images_rgb = jnp.clip((images_rgb + 1.0) / 2.0, 0.0, 1.0) |
| 340 | + images_numpy = np.array(images_rgb) |
| 341 | + |
| 342 | + for b_idx in range(batch_size): |
| 343 | + image_np = np.array(images_numpy[b_idx] * 255.0, dtype=np.uint8) |
| 344 | + # Transpose channel dimension if shape is (C, H, W) instead of (H, W, C) |
| 345 | + if image_np.shape[0] == 3: |
| 346 | + image_np = image_np.transpose(1, 2, 0) |
| 347 | + |
| 348 | + img = Image.fromarray(image_np) |
| 349 | + |
| 350 | + # Formulate output filename for this batch index |
| 351 | + if batch_size > 1: |
| 352 | + batch_output_name = output_name.replace(".png", f"_b{b_idx}.png") |
| 353 | + else: |
| 354 | + batch_output_name = output_name |
| 355 | + |
| 356 | + output_png_path = os.path.join(output_dir, batch_output_name) |
| 357 | + img.save(output_png_path) |
| 358 | + print(f" -> Saved image: {output_png_path} | Prompt: '{prompts[b_idx]}'") |
| 359 | + saved_paths.append(output_png_path) |
| 360 | + |
| 361 | + return saved_paths, trace |
0 commit comments