Skip to content

Commit f88d1f3

Browse files
committed
feat(flux2klein): optimize model loading and XLA compilation latency by 2 minutes
1 parent 64450df commit f88d1f3

4 files changed

Lines changed: 86 additions & 14 deletions

File tree

src/maxdiffusion/generate_flux2klein.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ def qwen3_init_fn():
341341
def unbox_fn(x):
342342
return x.unbox() if isinstance(x, flax_spmd.LogicallyPartitioned) else x
343343

344+
t_sub0 = time.time()
344345
params = jax.tree_util.tree_map(
345346
unbox_fn, abstract_transformer_vars["params"], is_leaf=lambda k: isinstance(k, flax_spmd.LogicallyPartitioned)
346347
)
@@ -356,17 +357,23 @@ def unbox_fn(x):
356357
)
357358
qwen3_params = flax.core.unfreeze(qwen3_params)
358359

360+
max_logging.log(f" -> [SUB-TIMING 1/3] PyTree unboxing template setup: {time.time() - t_sub0:.2f}s")
361+
t_sub1 = time.time()
362+
359363
params = load_and_convert_flux_klein_weights(safetensors_path, params, num_double_layers, depth)
360364
vae_params, vae_bn_mean, vae_bn_std = load_and_convert_vae_weights(vae_safetensors_path, vae_params)
361365
qwen3_params = load_and_convert_qwen3_weights(text_encoder_path, qwen3_params, qwen3_config)
366+
max_logging.log(f" -> [SUB-TIMING 2/3] Safetensors loading & key mapping: {time.time() - t_sub1:.2f}s")
362367

368+
t_sub2 = time.time()
363369
if config.weights_dtype == "bfloat16":
364370
max_logging.log("Casting JAX parameters to bfloat16 in-place...")
365371
cast_dict_to_bfloat16_inplace(params, exclude_keywords=("norm",))
366372
cast_dict_to_bfloat16_inplace(vae_params, exclude_keywords=("norm",))
367373
cast_dict_to_bfloat16_inplace(qwen3_params, exclude_keywords=("norm",))
368374
vae_bn_mean = vae_bn_mean.astype(jnp.bfloat16)
369375
vae_bn_std = vae_bn_std.astype(jnp.bfloat16)
376+
max_logging.log(f" -> [SUB-TIMING 2b/3] bfloat16 in-place casting: {time.time() - t_sub2:.2f}s")
370377

371378
params = flax.core.freeze(params)
372379
vae_params = flax.core.freeze(vae_params)
@@ -375,6 +382,7 @@ def unbox_fn(x):
375382
max_logging.log("\n" + "=" * 80)
376383
max_logging.log("🚀 Pinning all parameters to TPU HBM permanently...")
377384
max_logging.log("=" * 80 + "\n")
385+
t_sub3 = time.time()
378386
max_logging.log("Putting params on TPU HBM...")
379387
with mesh, nn_partitioning.axis_rules(config.logical_axis_rules):
380388
try:
@@ -393,6 +401,7 @@ def unbox_fn(x):
393401
vae_params = jax.tree_util.tree_map(max_utils.device_put_replicated, vae_params, vae_shardings)
394402
max_logging.log("Putting qwen3_params on TPU HBM...")
395403
qwen3_params = jax.tree_util.tree_map(max_utils.device_put_replicated, qwen3_params, qwen3_shardings)
404+
max_logging.log(f" -> [SUB-TIMING 3/3] TPU HBM device_put placement: {time.time() - t_sub3:.2f}s")
396405
max_logging.log("All parameters placed on TPU HBM successfully!")
397406
gc.collect()
398407
jax.effects_barrier()
@@ -482,6 +491,7 @@ def unbox_fn(x):
482491
max_logging.log("\n" + "=" * 80)
483492
max_logging.log("🚀 Running initial dry run (Warmup Pass) to compile XLA graphs...")
484493
max_logging.log("=" * 80)
494+
pipeline.compile_aot_async(params, vae_params, qwen3_params, batch_size=config.batch_size, height=config.height, width=config.width)
485495
_, warmup_trace = pipeline(
486496
prompt=active_prompts,
487497
params=params,
@@ -498,6 +508,7 @@ def unbox_fn(x):
498508
batch_size=config.batch_size,
499509
use_latents=use_latents_flag,
500510
latents=latents_to_use,
511+
warmup=True,
501512
output_dir=config.output_dir,
502513
output_name="flux2klein_warmup.png",
503514
)

src/maxdiffusion/models/flux/util.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -398,11 +398,12 @@ def cast_dict_to_bfloat16_inplace(d, device=None, exclude_keywords=None, parent_
398398
is_excluded = exclude_keywords and any(kw.lower() in current_key.lower() for kw in exclude_keywords)
399399
target_dtype = jnp.float32 if is_excluded else jnp.bfloat16
400400

401-
d[k] = v.astype(target_dtype)
402-
if hasattr(d[k], "block_until_ready"):
403-
d[k].block_until_ready()
404-
del v
405-
gc.collect()
401+
if v.dtype != target_dtype:
402+
d[k] = v.astype(target_dtype)
403+
if hasattr(d[k], "block_until_ready"):
404+
d[k].block_until_ready()
405+
del v
406+
gc.collect()
406407

407408

408409
# -----------------------------------------------------------------------------
@@ -571,12 +572,16 @@ def load_and_convert_vae_weights(safetensors_path, jax_params):
571572
max_logging.log(f"Loading VAE weights from: {safetensors_path}")
572573
pt_state_dict = load_file(safetensors_path)
573574

574-
def get_pytorch_weight_tensor(key):
575-
return pt_state_dict[key]
576-
577575
# Unfreeze JAX params so we can load the weights
578576
jax_params = flax.core.unfreeze(jax_params)
579577

578+
first_leaf = jax.tree_util.tree_leaves(jax_params)[0]
579+
target_dtype = first_leaf.dtype
580+
581+
def get_pytorch_weight_tensor(key, dtype=target_dtype):
582+
tensor = pt_state_dict[key]
583+
return jnp.array(tensor, dtype=dtype)
584+
580585
# Map weights
581586
max_logging.log("Mapping VAE decoder weights to JAX parameters...")
582587

src/maxdiffusion/models/qwen3_flax.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -648,15 +648,18 @@ def load_and_convert_qwen3_weights(safetensors_path: str, jax_params: dict, conf
648648
torch_weights = load_file(safetensors_path)
649649
max_logging.log("Safetensors weights loaded successfully. Starting JAX parameter mapping...")
650650

651-
# Helper to transpose and cast weight
652-
def get_w(name: str, transpose: bool = True) -> np.ndarray:
651+
first_leaf = jax.tree_util.tree_leaves(jax_params)[0]
652+
target_dtype = first_leaf.dtype
653+
654+
# Helper to transpose and cast weight directly to target_dtype
655+
def get_w(name: str, transpose: bool = True):
653656
nonlocal torch_weights
654657
if name not in torch_weights:
655658
raise KeyError(f"Weight '{name}' not found in safetensors!")
656659
t = torch_weights[name]
657660
if len(t.shape) == 2 and transpose:
658661
t = t.T
659-
return t
662+
return jnp.array(t, dtype=target_dtype)
660663

661664
# Create mutable copy of JAX params to populate
662665
import flax

src/maxdiffusion/pipelines/flux/flux2klein_pipeline.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,65 @@ def transformer_step(t_params, latents, img_ids, prompt_embeds, txt_ids, vec, ti
9797
guidance=guidance,
9898
)
9999

100-
@jax.jit
100+
@jax.jit(donate_argnums=(1,))
101101
def vae_decode(v_params, latents_unpatched):
102102
return self.vae.apply({"params": v_params}, latents=latents_unpatched, method=self.vae.decode)
103103

104104
self._jitted_qwen3_forward = qwen3_forward
105105
self._jitted_transformer_step = transformer_step
106106
self._jitted_vae_decode = vae_decode
107107

108+
def compile_aot_async(self, params, vae_params, qwen3_params, batch_size=1, height=1024, width=1024):
109+
"""Triggers AOT compilation for Qwen3, Flux Transformer, and VAE concurrently using ThreadPoolExecutor."""
110+
self._setup_jit_functions()
111+
max_logging.log("🚀 Pre-compiling XLA graphs for Qwen3, Flux Transformer, and VAE concurrently...")
112+
from concurrent.futures import ThreadPoolExecutor
113+
114+
seq_len_img = (height // 16) * (width // 16)
115+
seq_len_txt = self._config.max_sequence_length
116+
117+
dummy_ids = jnp.zeros((batch_size, seq_len_txt), dtype=jnp.int32)
118+
dummy_mask = jnp.ones((batch_size, seq_len_txt), dtype=jnp.int32)
119+
120+
dummy_latents = jnp.zeros((batch_size, seq_len_img, 128), dtype=jnp.float32)
121+
dummy_img_ids = jnp.zeros((batch_size, seq_len_img, 4), dtype=jnp.int32)
122+
dummy_prompt_embeds = jnp.zeros((batch_size, seq_len_txt, 12288), dtype=jnp.bfloat16)
123+
dummy_txt_ids = jnp.zeros((batch_size, seq_len_txt, 4), dtype=jnp.float32)
124+
dummy_t_vec = jnp.zeros((batch_size,), dtype=jnp.float32)
125+
126+
dummy_unpacked_latents = jnp.zeros((batch_size, 32, height // 8, width // 8), dtype=jnp.float32)
127+
128+
def compile_qwen3():
129+
t0 = time.perf_counter()
130+
with self.mesh, nn_partitioning.axis_rules(self._config.logical_axis_rules):
131+
self._jitted_qwen3_forward.lower(qwen3_params, dummy_ids, dummy_mask).compile()
132+
max_logging.log(f" -> [AOT COMPILED] Qwen3 Text Encoder in {time.perf_counter() - t0:.2f}s")
133+
134+
def compile_transformer():
135+
t0 = time.perf_counter()
136+
with self.mesh, nn_partitioning.axis_rules(self._config.logical_axis_rules):
137+
self._jitted_transformer_step.lower(
138+
params, dummy_latents, dummy_img_ids, prompt_embeds=dummy_prompt_embeds, txt_ids=dummy_txt_ids, vec=None, timestep=dummy_t_vec, guidance=None
139+
).compile()
140+
max_logging.log(f" -> [AOT COMPILED] Flux Transformer Step in {time.perf_counter() - t0:.2f}s")
141+
142+
def compile_vae():
143+
t0 = time.perf_counter()
144+
with self.mesh, nn_partitioning.axis_rules(self._config.logical_axis_rules):
145+
self._jitted_vae_decode.lower(vae_params, dummy_unpacked_latents).compile()
146+
max_logging.log(f" -> [AOT COMPILED] VAE Decoder in {time.perf_counter() - t0:.2f}s")
147+
148+
t_start = time.perf_counter()
149+
with ThreadPoolExecutor(max_workers=3) as executor:
150+
futures = [
151+
executor.submit(compile_qwen3),
152+
executor.submit(compile_transformer),
153+
executor.submit(compile_vae),
154+
]
155+
for future in futures:
156+
future.result()
157+
max_logging.log(f"⚡ [AOT CONCURRENT COMPILATION COMPLETE] Total AOT compile time: {time.perf_counter() - t_start:.2f}s")
158+
108159
def _prepare_latents(self, config, batch_size, height, width):
109160
num_channels_latents = 32
110161
latent_height = height // 8
@@ -147,6 +198,7 @@ def __call__(
147198
use_latents: bool = False,
148199
latents: Optional[Any] = None,
149200
measure_time: bool = False,
201+
warmup: bool = False,
150202
output_dir: str = "output/",
151203
output_name: str = "flux2klein_generated_image.png",
152204
):
@@ -293,8 +345,9 @@ def put_data_on_devices(x, sharding):
293345
# ---------------------------------------------------------------------
294346
# PHASE B: Denoising Loop (Flux Transformer - Standalone Step JIT)
295347
# ---------------------------------------------------------------------
348+
steps_to_run = 1 if warmup else num_inference_steps
296349
print(
297-
f"{host_prefix} [PHASE B] Running {num_inference_steps}-step E2E Denoising Loop on a batch of {batch_size} images...",
350+
f"{host_prefix} [PHASE B] Running {steps_to_run}-step E2E Denoising Loop on a batch of {batch_size} images (warmup={warmup})...",
298351
flush=True,
299352
)
300353
t0 = time.perf_counter()
@@ -303,7 +356,7 @@ def put_data_on_devices(x, sharding):
303356
guidance_vec_val = None
304357
vec_val = None
305358

306-
for step_idx in range(num_inference_steps):
359+
for step_idx in range(steps_to_run):
307360
timestep = scheduler_state.timesteps[step_idx]
308361
t_vec = jnp.full((batch_size,), timestep / 1000.0, dtype=latents_jax.dtype)
309362

0 commit comments

Comments
 (0)