Skip to content

Commit 47eed48

Browse files
committed
feat(flux2klein): optimize weight loading speed and fix pipeline static jit dimensions
1 parent 001ea85 commit 47eed48

21 files changed

Lines changed: 1350 additions & 518 deletions

src/maxdiffusion/configs/base_flux2klein_9B.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ gcs_metrics: False
2727
save_config_to_gcs: False
2828
log_period: 100
2929

30-
pretrained_model_name_or_path: 'black-forest-labs/FLUX.2-klein-9B'
30+
pretrained_model_name_or_path: '/mnt/hyperdisk_weights/hub/flux2klein-9b'
3131
clip_model_name_or_path: 'ariG23498/clip-vit-large-patch14-text-flax'
3232
t5xxl_model_name_or_path: 'ariG23498/t5-v1-1-xxl-flax'
3333

@@ -43,7 +43,7 @@ max_shift: 1.15
4343

4444

4545
unet_checkpoint: ''
46-
revision: 'refs/pr/95'
46+
revision: ''
4747
# This will convert the weights to this dtype.
4848
# When running inference on TPUv5e, use weights_dtype: 'bfloat16'
4949
weights_dtype: 'bfloat16'

src/maxdiffusion/configs/base_wan_14b.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -274,10 +274,6 @@ load_tfrecord_cached: True
274274
train_data_dir: ''
275275
dataset_config_name: ''
276276
jax_cache_dir: ''
277-
# Directory for per-shape AOT serialized executables ('' = disabled).
278-
aot_cache_dir: ''
279-
# Directory for memoized torch->flax converted weights ('' = disabled).
280-
converted_weights_dir: ''
281277
hf_data_dir: ''
282278
hf_train_files: ''
283279
hf_access_token: ''

src/maxdiffusion/configs/base_wan_1_3b.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -227,10 +227,6 @@ load_tfrecord_cached: True
227227
train_data_dir: ''
228228
dataset_config_name: ''
229229
jax_cache_dir: ''
230-
# Directory for per-shape AOT serialized executables ('' = disabled).
231-
aot_cache_dir: ''
232-
# Directory for memoized torch->flax converted weights ('' = disabled).
233-
converted_weights_dir: ''
234230
hf_data_dir: ''
235231
hf_train_files: ''
236232
hf_access_token: ''

src/maxdiffusion/configs/base_wan_27b.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -248,10 +248,6 @@ load_tfrecord_cached: True
248248
train_data_dir: ''
249249
dataset_config_name: ''
250250
jax_cache_dir: ''
251-
# Directory for per-shape AOT serialized executables ('' = disabled).
252-
aot_cache_dir: ''
253-
# Directory for memoized torch->flax converted weights ('' = disabled).
254-
converted_weights_dir: ''
255251
hf_data_dir: ''
256252
hf_train_files: ''
257253
hf_access_token: ''

src/maxdiffusion/configs/base_wan_animate.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,6 @@ load_tfrecord_cached: True
237237
train_data_dir: ''
238238
dataset_config_name: ''
239239
jax_cache_dir: '.jax_cache'
240-
# Directory for per-shape AOT serialized executables ('' = disabled).
241-
aot_cache_dir: ''
242-
# Directory for memoized torch->flax converted weights ('' = disabled).
243-
converted_weights_dir: ''
244240
hf_data_dir: ''
245241
hf_train_files: ''
246242
hf_access_token: ''

src/maxdiffusion/configs/base_wan_i2v_14b.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,6 @@ load_tfrecord_cached: True
240240
train_data_dir: ''
241241
dataset_config_name: ''
242242
jax_cache_dir: ''
243-
# Directory for per-shape AOT serialized executables ('' = disabled).
244-
aot_cache_dir: ''
245-
# Directory for memoized torch->flax converted weights ('' = disabled).
246-
converted_weights_dir: ''
247243
hf_data_dir: ''
248244
hf_train_files: ''
249245
hf_access_token: ''

src/maxdiffusion/configs/base_wan_i2v_27b.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,6 @@ load_tfrecord_cached: True
241241
train_data_dir: ''
242242
dataset_config_name: ''
243243
jax_cache_dir: ''
244-
# Directory for per-shape AOT serialized executables ('' = disabled).
245-
aot_cache_dir: ''
246-
# Directory for memoized torch->flax converted weights ('' = disabled).
247-
converted_weights_dir: ''
248244
hf_data_dir: ''
249245
hf_train_files: ''
250246
hf_access_token: ''

src/maxdiffusion/generate_flux2klein.py

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def partition_prompts(prompt_str: str, batch_size: int) -> List[str]:
6262
max_logging.log(
6363
f"⚠️ Warning: Found {num_prompts} prompts, but batch_size is {batch_size}. Truncating to the first {batch_size}."
6464
)
65-
return raw_prompts[:batch_size]
65+
6666

6767

6868
def encode_prompt(prompt: str, snapshot_dir: str = None, repo_id: str = "black-forest-labs/FLUX.2-klein-4B"):
@@ -78,8 +78,18 @@ def encode_prompt(prompt: str, snapshot_dir: str = None, repo_id: str = "black-f
7878

7979
text_encoder_path = os.path.join(snapshot_dir, "text_encoder")
8080
tokenizer_path = os.path.join(snapshot_dir, "tokenizer")
81-
if not os.path.exists(tokenizer_path):
82-
tokenizer_path = text_encoder_path
81+
82+
if not os.path.exists(os.path.join(text_encoder_path, "config.json")) or not os.path.exists(tokenizer_path):
83+
try:
84+
fb_dir = snapshot_download(repo_id=repo_id, local_files_only=True)
85+
if not os.path.exists(os.path.join(text_encoder_path, "config.json")):
86+
text_encoder_path = os.path.join(fb_dir, "text_encoder")
87+
if not os.path.exists(tokenizer_path):
88+
tokenizer_path = os.path.join(fb_dir, "tokenizer") if os.path.exists(os.path.join(fb_dir, "tokenizer")) else os.path.join(fb_dir, "text_encoder")
89+
except Exception as e:
90+
if not os.path.exists(tokenizer_path):
91+
tokenizer_path = text_encoder_path
92+
8393
tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)
8494
text_encoder = AutoModelForCausalLM.from_pretrained(text_encoder_path, torch_dtype=torch.float32)
8595
text_encoder.eval()
@@ -183,8 +193,13 @@ def main(argv):
183193
else:
184194
from huggingface_hub import snapshot_download
185195

186-
max_logging.log(f"Resolving snapshot directory for model '{repo_id}' from HF Hub...")
187-
snapshot_dir = snapshot_download(repo_id=repo_id)
196+
rev = getattr(config, "revision", None)
197+
if not rev or rev == "refs/pr/95":
198+
rev = "main"
199+
try:
200+
snapshot_dir = snapshot_download(repo_id=repo_id, revision=rev, local_files_only=True)
201+
except Exception:
202+
snapshot_dir = snapshot_download(repo_id=repo_id, local_files_only=True)
188203

189204
max_logging.log(f"Host {jax.process_index()} using HF snapshot directory: {snapshot_dir}")
190205
safetensors_path = os.path.join(snapshot_dir, "transformer")
@@ -194,8 +209,13 @@ def main(argv):
194209
# 4. Load Qwen3 Config & Setup model layout
195210
from transformers import AutoConfig
196211

197-
max_logging.log(f"Loading Qwen3 config from text_encoder path: {text_encoder_path}...")
198-
pt_config = AutoConfig.from_pretrained(text_encoder_path, local_files_only=True)
212+
try:
213+
pt_config = AutoConfig.from_pretrained(text_encoder_path, local_files_only=True)
214+
except Exception as e:
215+
depth_val = getattr(config, "depth", 24)
216+
hf_repo = "black-forest-labs/FLUX.2-klein-9B" if depth_val in (24, -1) else "black-forest-labs/FLUX.2-klein-4B"
217+
max_logging.log(f"ℹ️ Config not found in {text_encoder_path}. Resolving from HF cache: {hf_repo}")
218+
pt_config = AutoConfig.from_pretrained(hf_repo, subfolder="text_encoder", local_files_only=True)
199219

200220
qwen3_config = FlaxQwen3Config(
201221
vocab_size=pt_config.vocab_size,
@@ -216,9 +236,25 @@ def main(argv):
216236

217237
transformer_config_json = os.path.join(safetensors_path, "config.json")
218238
transformer_pt_cfg = {}
239+
loaded_cfg = False
219240
if os.path.exists(transformer_config_json):
220-
with open(transformer_config_json, "r") as f:
221-
transformer_pt_cfg = json.load(f)
241+
try:
242+
with open(transformer_config_json, "r") as f:
243+
transformer_pt_cfg = json.load(f)
244+
loaded_cfg = True
245+
except Exception as e:
246+
max_logging.log(f"ℹ️ Could not parse {transformer_config_json}: {e}. Falling back to HF cache...")
247+
248+
if not loaded_cfg:
249+
depth_val = getattr(config, "depth", 24)
250+
hf_repo = "black-forest-labs/FLUX.2-klein-9B" if depth_val in (24, -1) else "black-forest-labs/FLUX.2-klein-4B"
251+
try:
252+
from huggingface_hub import hf_hub_download
253+
cfg_file = hf_hub_download(repo_id=hf_repo, filename="transformer/config.json", local_files_only=True)
254+
with open(cfg_file, "r") as f:
255+
transformer_pt_cfg = json.load(f)
256+
except Exception as e:
257+
max_logging.log(f"⚠️ Warning resolving transformer config fallback: {e}")
222258

223259
num_double_layers = getattr(config, "num_double_layers", -1)
224260
if num_double_layers is None or num_double_layers <= 0:
@@ -481,9 +517,22 @@ def unbox_fn(x):
481517
max_logging.log(f" -> Custom latents shape: {latents_to_use.shape} | sum: {latents_to_use.sum():.6f}")
482518

483519
max_logging.log("\n" + "=" * 80)
484-
max_logging.log("🚀 Running initial dry run (Warmup Pass) to compile XLA graphs...")
520+
max_logging.log("🚀 Pre-compiling XLA graphs concurrently (AOT Compilation)...")
521+
max_logging.log("=" * 80)
522+
aot_time = pipeline.compile_aot_async(
523+
params=params,
524+
vae_params=vae_params,
525+
qwen3_params=qwen3_params,
526+
vae_bn_mean=vae_bn_mean,
527+
vae_bn_std=vae_bn_std,
528+
batch_size=config.batch_size,
529+
height=config.height,
530+
width=config.width,
531+
)
532+
533+
max_logging.log("\n" + "=" * 80)
534+
max_logging.log("🚀 Running initial dry run (Warmup Pass) to verify compiled graph execution...")
485535
max_logging.log("=" * 80)
486-
pipeline.compile_aot_async(params, vae_params, qwen3_params, batch_size=config.batch_size, height=config.height, width=config.width)
487536
_, warmup_trace = pipeline(
488537
prompt=active_prompts,
489538
params=params,
@@ -500,7 +549,6 @@ def unbox_fn(x):
500549
batch_size=config.batch_size,
501550
use_latents=use_latents_flag,
502551
latents=latents_to_use,
503-
warmup=True,
504552
output_dir=config.output_dir,
505553
output_name="flux2klein_warmup.png",
506554
)
@@ -536,15 +584,19 @@ def unbox_fn(x):
536584
main_trace.get("prompt_encoding", 0.0) + main_trace.get("denoise_loop", 0.0) + main_trace.get("vae_decode", 0.0)
537585
)
538586

587+
total_cold_start = load_time + aot_time + warmup_time
588+
539589
max_logging.log("\n" + "=" * 80)
540-
max_logging.log("📊 FLUX.2-KLEIN LATENCY & TIMING BREAKDOWN (PURE MODEL INFERENCE)")
590+
max_logging.log("📊 FLUX.2-KLEIN COMPLETE LATENCY & TIMING BREAKDOWN")
541591
max_logging.log("=" * 80)
542-
max_logging.log(f"1) Total Model Loading & Placement Time: {load_time:.2f} seconds ⏱️")
543-
max_logging.log(f"2) Cold-Start / Warmup Pass (XLA Compilation): {warmup_time:.2f} seconds ⏱️")
592+
max_logging.log(f"1) Model Loading & Placement Time: {load_time:.2f} seconds ⏱️")
593+
max_logging.log(f"2) Concurrent AOT XLA Compilation Time: {aot_time:.2f} seconds ⚡")
594+
max_logging.log(f"3) Warmup Pass Execution Time: {warmup_time:.2f} seconds ⏱️")
544595
max_logging.log(f" - Qwen3 Encoding: {warmup_trace.get('prompt_encoding', 0.0):.2f}s")
545596
max_logging.log(f" - Flux Denoising: {warmup_trace.get('denoise_loop', 0.0):.2f}s")
546597
max_logging.log(f" - VAE Decoding: {warmup_trace.get('vae_decode', 0.0):.2f}s")
547-
max_logging.log(f"3) Main Warmed-Up Pass (Pure Model Inference): {main_time:.2f} seconds ⏱️")
598+
max_logging.log(f"👉 TOTAL COLD-START TIME (Loading + AOT + Warmup): {total_cold_start:.2f} seconds 🎯")
599+
max_logging.log(f"4) Main Warmed-Up Pass (Pure Inference Latency): {main_time:.2f} seconds ⏱️")
548600
max_logging.log(f" - Qwen3 Encoding: {main_trace.get('prompt_encoding', 0.0):.2f}s")
549601
max_logging.log(f" - Flux Denoising: {main_trace.get('denoise_loop', 0.0):.2f}s")
550602
max_logging.log(f" - VAE Decoding: {main_trace.get('vae_decode', 0.0):.2f}s")

src/maxdiffusion/generate_wan.py

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from maxdiffusion.checkpointing.wan_checkpointer_2_2 import WanCheckpointer2_2
2222
from maxdiffusion.checkpointing.wan_checkpointer_i2v_2p1 import WanCheckpointerI2V_2_1
2323
from maxdiffusion.checkpointing.wan_checkpointer_i2v_2p2 import WanCheckpointerI2V_2_2
24-
from maxdiffusion import aot_cache, pyconfig, max_logging, max_utils
24+
from maxdiffusion import pyconfig, max_logging, max_utils
2525
from absl import app
2626
from maxdiffusion.train_utils import transformer_engine_context
2727
from maxdiffusion.utils import export_to_video
@@ -279,30 +279,6 @@ def run(config, pipeline=None, filename_prefix="", commit_hash=None):
279279
dtype=config.weights_dtype,
280280
)
281281

282-
# Per-shape AOT executable cache: deserialization starts on background
283-
# threads now and overlaps the remaining setup; unknown shapes silently
284-
# fall back to jit and are serialized by save_pending() after warmup.
285-
aot_cache.install(
286-
getattr(config, "aot_cache_dir", ""),
287-
meta={
288-
"model": config.pretrained_model_name_or_path,
289-
"attention": config.attention,
290-
# Kernel block sizes change the lowered graph, not the input
291-
# shapes — they must key the executable or a re-tuned config
292-
# would silently hit stale binaries.
293-
"flash_block_sizes": str(config.flash_block_sizes),
294-
"mesh_shape": str(pipeline.mesh.shape),
295-
"weights_dtype": str(config.weights_dtype),
296-
"activations_dtype": str(config.activations_dtype),
297-
"scan_layers": str(config.scan_layers),
298-
"jax": jax.__version__,
299-
},
300-
mesh=pipeline.mesh,
301-
)
302-
# Deserialization is seconds and warmup must see the loaded executables
303-
# to hit them; without this the first call races the loader threads.
304-
aot_cache.wait_for_loads()
305-
306282
s0 = time.perf_counter()
307283

308284
# Disable profiler for the first two runs to avoid duplicate uploads
@@ -323,21 +299,11 @@ def run(config, pipeline=None, filename_prefix="", commit_hash=None):
323299
# step count only changes the Python loop trip count, not traced shapes.
324300
warmup_steps = min(2, config.num_inference_steps)
325301
max_logging.log(f"Compile warmup: {warmup_steps} denoising steps")
326-
# Zero-execution warmup: wrapped transformer passes lower+compile (or
327-
# reuse the deserialized AOT executable) and return sharded zeros, so
328-
# the warmup pays compile time only, never real denoise compute. The
329-
# returned videos are garbage by design and are discarded below.
330-
with aot_cache.warmup_mode():
331-
videos = call_pipeline(config, pipeline, prompt, negative_prompt, num_inference_steps=warmup_steps)
302+
videos = call_pipeline(config, pipeline, prompt, negative_prompt, num_inference_steps=warmup_steps)
332303
if isinstance(videos, tuple):
333304
videos, warmup_trace = videos
334305
max_logging.log("Warmup breakdown: " + ", ".join(f"{stage}={seconds:.1f}s" for stage, seconds in warmup_trace.items()))
335306

336-
# Serialize any newly-compiled shapes synchronously while still inside
337-
# warmup-accounted time; a background save would compete with the first
338-
# real generation (DiffusionServing PR#39 first-generation-stall lesson).
339-
aot_cache.save_pending()
340-
341307
max_logging.log("===================== Model details =======================")
342308
max_logging.log(f"model name: {config.model_name}")
343309
max_logging.log(f"model path: {config.pretrained_model_name_or_path}")

src/maxdiffusion/models/flux/util.py

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -300,17 +300,17 @@ def unpack_latents(latents, batch_size, num_channels_latents, height, width):
300300
Unpacks packed sequence of shape (batch_size, (height//16)*(width//16), channels*4)
301301
back to the unpacked spatial grid shape (batch_size, channels, height//8, width//8).
302302
"""
303-
import numpy as np
303+
import jax.numpy as jnp
304304

305305
h_latent = height // 8
306306
w_latent = width // 8
307307

308308
# 1. Reshape to split spatial grid and packed channel blocks
309-
latents = np.reshape(latents, (batch_size, h_latent // 2, w_latent // 2, num_channels_latents, 2, 2))
309+
latents = jnp.reshape(latents, (batch_size, h_latent // 2, w_latent // 2, num_channels_latents, 2, 2))
310310
# 2. Permute dimensions back to unpacked order
311-
latents = np.transpose(latents, (0, 3, 1, 4, 2, 5))
311+
latents = jnp.transpose(latents, (0, 3, 1, 4, 2, 5))
312312
# 3. Flatten back to 4D unpacked latent shape
313-
latents = np.reshape(latents, (batch_size, num_channels_latents, h_latent, w_latent))
313+
latents = jnp.reshape(latents, (batch_size, num_channels_latents, h_latent, w_latent))
314314
return latents
315315

316316

@@ -411,7 +411,7 @@ def cast_dict_to_bfloat16_inplace(d, device=None, exclude_keywords=None, parent_
411411
# -----------------------------------------------------------------------------
412412

413413

414-
def load_and_convert_flux_klein_weights(safetensors_path, params, num_double_layers, num_single_layers, dtype=None):
414+
def load_and_convert_flux_klein_weights(safetensors_path, params, num_double_layers, num_single_layers, dtype=None, pt_state_dict=None):
415415
"""
416416
Loads weights from safetensors via zero-copy safetensors.numpy and converts them to JAX parameter dictionary.
417417
Supports dynamic layer counts (double and single stream blocks) and sharded safetensors directories.
@@ -423,16 +423,17 @@ def load_and_convert_flux_klein_weights(safetensors_path, params, num_double_lay
423423
import os
424424
import gc
425425

426-
pt_state_dict = {}
427-
if os.path.isdir(safetensors_path):
428-
shards = glob.glob(os.path.join(safetensors_path, "*.safetensors"))
429-
max_logging.log(f"Loading sharded weights from directory: {safetensors_path} (Found {len(shards)} shards)...")
430-
for shard in sorted(shards):
431-
max_logging.log(f"Loading shard: {shard}...")
432-
pt_state_dict.update(load_file(shard))
433-
else:
434-
max_logging.log(f"Loading weights from: {safetensors_path}")
435-
pt_state_dict = load_file(safetensors_path)
426+
if pt_state_dict is None:
427+
pt_state_dict = {}
428+
if os.path.isdir(safetensors_path):
429+
shards = glob.glob(os.path.join(safetensors_path, "*.safetensors"))
430+
max_logging.log(f"Loading sharded weights from directory: {safetensors_path} (Found {len(shards)} shards)...")
431+
for shard in sorted(shards):
432+
max_logging.log(f"Loading shard: {shard}...")
433+
pt_state_dict.update(load_file(shard))
434+
else:
435+
max_logging.log(f"Loading weights from: {safetensors_path}")
436+
pt_state_dict = load_file(safetensors_path)
436437

437438
max_logging.log("Mapping weights to JAX parameters...")
438439

@@ -564,14 +565,15 @@ def convert_and_transpose_tensor(tensor, transpose=False, is_norm=False):
564565
return params
565566

566567

567-
def load_and_convert_vae_weights(safetensors_path, jax_params, dtype=None):
568+
def load_and_convert_vae_weights(safetensors_path, jax_params, dtype=None, pt_state_dict=None):
568569
"""Loads VAE weights from safetensors via zero-copy safetensors.numpy, maps them to JAX, and extracts BN stats."""
569570
from safetensors.numpy import load_file
570571
import flax
571572
import jax.numpy as jnp
572573

573-
max_logging.log(f"Loading VAE weights from: {safetensors_path}")
574-
pt_state_dict = load_file(safetensors_path)
574+
if pt_state_dict is None:
575+
max_logging.log(f"Loading VAE weights from: {safetensors_path}")
576+
pt_state_dict = load_file(safetensors_path)
575577

576578
# Unfreeze JAX params so we can load the weights
577579
jax_params = flax.core.unfreeze(jax_params)

0 commit comments

Comments
 (0)