Skip to content

Commit e42f8b8

Browse files
committed
Part 2: Onboard Flux.2-klein E2E generation pipeline and smoke tests
1 parent d775d42 commit e42f8b8

6 files changed

Lines changed: 1579 additions & 0 deletions

File tree

Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
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

Comments
 (0)