Skip to content

Commit 29d37fd

Browse files
committed
Standardize logging to max_logging.log and update AGENTS.md workflow
1 parent 56e6a4d commit 29d37fd

4 files changed

Lines changed: 602 additions & 39 deletions

File tree

.agents/AGENTS.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Developer Guidelines & Workflow Rules — Flux.2-klein-4B
2+
3+
## Project Goal & Context
4+
* **Objective**: Onboard the `Flux.2-klein-4B` model (currently implemented in PyTorch's `diffusers` library) into this `maxdiffusion` repository so it can run with JAX+TPU support.
5+
* **Reference Implementation**: We are porting and improving upon the PyTorch reference implementation: [pipeline_flux2_klein.py](https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/flux2/pipeline_flux2_klein.py).
6+
7+
### Key Development Files
8+
* **Pipeline Entry Point**: `src/maxdiffusion/generate_flux2klein.py`. This is where we load configurations, load weights, stitch together all model blocks, run the image generation pipeline, apply VAE Batch Normalization, and run the JAX VAE Decoder.
9+
* **Component Testing**: `src/maxdiffusion/tests/generate_flux2klein_test.py`. This is where we write granular unit tests verifying the scheduler, RoPE position grids, packing/unpacking, individual attention blocks, E2E transformer, and the VAE decoder.
10+
11+
### Completed Milestones (⚠️ LOST CHECKPOINT - ACTIVE RECOVERY IN PROGRESS)
12+
> [!IMPORTANT]
13+
> The parity milestones listed below were achieved on a previous repository checkpoint that is no longer available. We are currently working to recover the codebase to this state from scratch.
14+
1. **4-Step Denoising Parity**: [In Progress] Recovering parity between the JAX transformer + FlowMatch scheduler loop and the PyTorch reference.
15+
2. **VAE Decoder Port**: [In Progress] Verification of `FlaxAutoencoderKL` decoder mapping.
16+
3. **VAE Batch Normalization**: [In Progress] Restoring channel-wise latent scaling.
17+
4. **End-to-End Image Generation**: [In Progress] Achieving high SSIM/PSNR visual match against the PyTorch reference.
18+
5. **Multi-Prompt Parity Validation (1024x1024)**: [In Progress] Sequential multi-prompt verification under bfloat16.
19+
20+
---
21+
22+
## 🚀 Concrete Next Action Items (The Roadmap)
23+
24+
If you are picking up this project, your immediate priority tasks are:
25+
26+
1. **Task 1: Batched Generation ($B > 1$)**
27+
* Verify the pipeline under `batch_size > 1` (parallel prompt and image generation).
28+
* Ensure coordinate ID replication (`jnp.repeat`) and parallel latent unpacking loop work flawlessly without collapsing dimensions.
29+
* *Note*: Flux was **not** trained with attention masks (verified by Hugging Face `diffusers` codebase search), so do **not** attempt to implement an attention mask; let the model attend to the padded sequence naturally, as this is how it maintains its learned representations!
30+
2. **Task 2: Dynamic Resolutions & Static Bucketing**
31+
* Verify JAX generation for non-square aspect ratios (e.g. 1024x768).
32+
* To prevent slow JAX/XLA re-compilation runs in production when the resolution changes, implement a **static shape bucketing strategy** (e.g. compiling for a few standard sizes like 512x512, 768x512, 1024x768, and padding inputs).
33+
3. **Task 3: VAE Encoder Porting (Img2Img support)**
34+
* Port the VAE Encoder block (`FlaxEncoder` inside `vae_flax.py`) to JAX and write its weights mapping function.
35+
* Implement the inverse Batch Normalization step on encoded latents and verify the entire roundtrip (image $\rightarrow$ latents $\rightarrow$ image) is mathematically lossless.
36+
4. **Task 4: ControlNet & LoRA Adapter Merging**
37+
* Verify how to dynamically load LoRA weights and merge them into the main parameter PyTrees (`vae_params` and `transformer_params`) efficiently without triggering XLA re-compilations.
38+
39+
---
40+
41+
## ⚠️ CRITICAL WORKFLOW RULES
42+
43+
### 1. The Continuous Rsync Race Condition Warning
44+
* **The Problem**: The user has a continuous `rsync` script running from their local workspace to the remote TPU VM (`local -> VM`).
45+
* **The Risk**: If you generate any large file on the VM (like a new 1.8GB diagnostic `.npz` bundle, or a generated image), the local workspace won't have it. During the next `rsync` pass, the user's script will see the mismatch and **overwrite or delete** your newly generated file on the VM with the old/missing local one!
46+
* **The Rule**: As soon as you generate a new diagnostic bundle or output on the VM, you **must immediately run a `gcloud compute tpus tpu-vm scp` command to download it to your local workspace**. Once it is local, the `rsync` script will see both environments are identical and won't touch it.
47+
48+
### 2. Testing & Execution Environment
49+
* **Target VM**: Run all code and tests exclusively on the remote TPU VM `ameypasarkar-tpu` in zone `southamerica-west1-a` (Project: `cloud-tpu-inference-test`).
50+
* **No Local Runs**: Do not attempt to run the model or tests in your local workspace environment.
51+
* **Permissions**: Always ask the user for explicit permission before running any tests or pipeline scripts on the TPU VM.
52+
* **gcloud SSH Command Format**:
53+
```bash
54+
gcloud compute tpus tpu-vm ssh ameypasarkar-tpu \
55+
--zone southamerica-west1-a \
56+
--project cloud-tpu-inference-test \
57+
--command "source ~/.bashrc && source /mnt/data/mdiff_venv/bin/activate && cd /mnt/data/maxdiffusion/ && <command>"
58+
```
59+
*(Note: Always prepend `source ~/.bashrc` to ensure user-specific environment variables like `HF_HOME` and Hugging Face authentication tokens are correctly loaded).*
60+
61+
### 3. Git Workflow
62+
* **Branch Constraint**: Work exclusively on the `flux2klein-onboarding` branch.
63+
* **Commit History**: Create individual, focused commits per task/fix. Do NOT squash commits into 1 unless explicitly requested by the user.
64+
* **Code Push**: Never push code to the remote repository without explicit user permission.
65+
66+
### 4. Precision Policy: Debugging vs Production
67+
* **For Parity Verification**: Set `jax_default_matmul_precision = "highest"` and load weights in `float32`. This removes TPU hardware-level `bfloat16` rounding noise, allowing you to isolate structural bugs by achieving clean $10^{-6}$ differences. (Note: Only feasible for resolutions up to 512x512 due to memory limits).
68+
* **For Production/Inference & High Resolutions (e.g. 1024x1024)**: Set `weights_dtype: 'bfloat16'` and `activations_dtype: 'bfloat16'` in the config, and let JAX use default matmul precision. This activates the TPU's hardware matrix units, giving maximum speed and efficiency.
69+
* **TPU HBM Memory Optimization**: Running 1024x1024 generation in float32 completely saturates the 32GB HBM of a single TPU v6e. You **must** run in `bfloat16` and use a recursive, leaf-by-leaf in-place parameter casting strategy (`cast_dict_to_bfloat16_inplace` with active Python `gc.collect()`) during weight conversion to avoid `RESOURCE_EXHAUSTED` OOM crashes.

src/maxdiffusion/generate_flux2klein.py

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -444,14 +444,14 @@ def unbox_fn(x):
444444
latents_to_use = None
445445
use_latents_flag = False
446446
if getattr(config, "latents_path", ""):
447-
print(f"Loading custom starting noise latents from: {config.latents_path}...")
447+
max_logging.log(f"Loading custom starting noise latents from: {config.latents_path}...")
448448
latents_to_use = np.load(config.latents_path)
449449
use_latents_flag = True
450-
print(f" -> Custom latents shape: {latents_to_use.shape} | sum: {latents_to_use.sum():.6f}")
450+
max_logging.log(f" -> Custom latents shape: {latents_to_use.shape} | sum: {latents_to_use.sum():.6f}")
451451

452-
print("\n" + "=" * 80)
453-
print("🚀 Running initial dry run (Warmup Pass) to compile XLA graphs...")
454-
print("=" * 80)
452+
max_logging.log("\n" + "=" * 80)
453+
max_logging.log("🚀 Running initial dry run (Warmup Pass) to compile XLA graphs...")
454+
max_logging.log("=" * 80)
455455
_, warmup_trace = pipeline(
456456
prompt=active_prompts,
457457
params=params,
@@ -477,9 +477,9 @@ def unbox_fn(x):
477477
+ warmup_trace.get("vae_decode", 0.0)
478478
)
479479

480-
print("\n" + "=" * 80)
481-
print("⏱️ Running timed pass at full TPU speed...")
482-
print("=" * 80)
480+
max_logging.log("\n" + "=" * 80)
481+
max_logging.log("⏱️ Running timed pass at full TPU speed...")
482+
max_logging.log("=" * 80)
483483
_, main_trace = pipeline(
484484
prompt=active_prompts,
485485
params=params,
@@ -503,23 +503,23 @@ def unbox_fn(x):
503503
main_trace.get("prompt_encoding", 0.0) + main_trace.get("denoise_loop", 0.0) + main_trace.get("vae_decode", 0.0)
504504
)
505505

506-
print("\n" + "=" * 80)
507-
print("📊 FLUX.2-KLEIN LATENCY & TIMING BREAKDOWN (PURE MODEL INFERENCE)")
508-
print("=" * 80)
509-
print(f"1) Total Model Loading & Placement Time: {load_time:.2f} seconds ⏱️")
510-
print(f"2) Cold-Start / Warmup Pass (XLA Compilation): {warmup_time:.2f} seconds ⏱️")
511-
print(f" - Qwen3 Encoding: {warmup_trace.get('prompt_encoding', 0.0):.2f}s")
512-
print(f" - Flux Denoising: {warmup_trace.get('denoise_loop', 0.0):.2f}s")
513-
print(f" - VAE Decoding: {warmup_trace.get('vae_decode', 0.0):.2f}s")
514-
print(f"3) Main Warmed-Up Pass (Pure Model Inference): {main_time:.2f} seconds ⏱️")
515-
print(f" - Qwen3 Encoding: {main_trace.get('prompt_encoding', 0.0):.2f}s")
516-
print(f" - Flux Denoising: {main_trace.get('denoise_loop', 0.0):.2f}s")
517-
print(f" - VAE Decoding: {main_trace.get('vae_decode', 0.0):.2f}s")
518-
print("=" * 80)
519-
520-
print("\n=======================================================")
521-
print(f"SUCCESS! Batched generation complete for {config.batch_size} images! 🎨🎉")
522-
print("=======================================================\n")
506+
max_logging.log("\n" + "=" * 80)
507+
max_logging.log("📊 FLUX.2-KLEIN LATENCY & TIMING BREAKDOWN (PURE MODEL INFERENCE)")
508+
max_logging.log("=" * 80)
509+
max_logging.log(f"1) Total Model Loading & Placement Time: {load_time:.2f} seconds ⏱️")
510+
max_logging.log(f"2) Cold-Start / Warmup Pass (XLA Compilation): {warmup_time:.2f} seconds ⏱️")
511+
max_logging.log(f" - Qwen3 Encoding: {warmup_trace.get('prompt_encoding', 0.0):.2f}s")
512+
max_logging.log(f" - Flux Denoising: {warmup_trace.get('denoise_loop', 0.0):.2f}s")
513+
max_logging.log(f" - VAE Decoding: {warmup_trace.get('vae_decode', 0.0):.2f}s")
514+
max_logging.log(f"3) Main Warmed-Up Pass (Pure Model Inference): {main_time:.2f} seconds ⏱️")
515+
max_logging.log(f" - Qwen3 Encoding: {main_trace.get('prompt_encoding', 0.0):.2f}s")
516+
max_logging.log(f" - Flux Denoising: {main_trace.get('denoise_loop', 0.0):.2f}s")
517+
max_logging.log(f" - VAE Decoding: {main_trace.get('vae_decode', 0.0):.2f}s")
518+
max_logging.log("=" * 80)
519+
520+
max_logging.log("\n=======================================================")
521+
max_logging.log(f"SUCCESS! Batched generation complete for {config.batch_size} images! 🎨🎉")
522+
max_logging.log("=======================================================\n")
523523

524524

525525
if __name__ == "__main__":

src/maxdiffusion/pipelines/flux/flux2klein_pipeline.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import numpy as np
2525
from flax.linen import partitioning as nn_partitioning
2626

27+
from maxdiffusion import max_logging
2728
from ..pipeline_flax_utils import FlaxDiffusionPipeline
2829
from ...models.flux.transformers.transformer_flux_flax import Flux2KleinTransformer2DModel
2930
from ...models.vae_flax import FlaxAutoencoderKL
@@ -163,13 +164,13 @@ def __call__(
163164
if latents_jax.ndim == 4:
164165
B, C, H, W = latents_jax.shape
165166
if C == 32:
166-
print(" [PIPELINE] Unpacked 32-channel latents detected. Packing using pack_latents...")
167+
max_logging.log(" [PIPELINE] Unpacked 32-channel latents detected. Packing using pack_latents...")
167168
latents_jax = pack_latents(latents_jax)
168169
else:
169170
latents_jax = jnp.transpose(jnp.reshape(latents_jax, (B, C, H * W)), (0, 2, 1))
170171
else:
171172
latents_numpy = self._prepare_latents(self._config, batch_size, height, width)
172-
print(f"DEBUG PIPELINE: latents_numpy sum = {latents_numpy.sum():.6f} | mean = {latents_numpy.mean():.6f}")
173+
max_logging.log(f"DEBUG PIPELINE: latents_numpy sum = {latents_numpy.sum():.6f} | mean = {latents_numpy.mean():.6f}")
173174
B, C, H, W = latents_numpy.shape
174175
latents_jax = jnp.transpose(jnp.reshape(latents_numpy, (B, C, H * W)), (0, 2, 1))
175176

@@ -194,7 +195,7 @@ def __call__(
194195
# ---------------------------------------------------------------------
195196
# PHASE A: Encode Prompt (Qwen3)
196197
# ---------------------------------------------------------------------
197-
print(f"[PHASE A] Encoding {len(prompts)} prompt(s) using JAX Qwen3 on TPU...")
198+
max_logging.log(f"[PHASE A] Encoding {len(prompts)} prompt(s) using JAX Qwen3 on TPU...")
198199
t0 = time.perf_counter()
199200

200201
# Resolve tokenizer path from config
@@ -215,11 +216,11 @@ def __call__(
215216

216217
# Tokenize using deterministic explicit template string (version-agnostic across transformers versions)
217218
templated_texts = [f"<|im_start|>user\n{p}<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n" for p in prompts]
218-
print(f"DEBUG: templated_texts[0] = {repr(templated_texts[0])}")
219+
max_logging.log(f"DEBUG: templated_texts[0] = {repr(templated_texts[0])}")
219220
inputs = tokenizer(templated_texts, return_tensors="np", padding="max_length", truncation=True, max_length=seq_len_txt)
220221
prompt_ids = jnp.array(inputs["input_ids"])
221222
prompt_mask = jnp.array(inputs["attention_mask"])
222-
print(f"DEBUG: prompt_ids[0][:15] = {prompt_ids[0][:15]}")
223+
max_logging.log(f"DEBUG: prompt_ids[0][:15] = {prompt_ids[0][:15]}")
223224

224225
# Run Text Encoding
225226
hidden_states, all_hidden_states = self._jitted_qwen3_forward(qwen3_params, prompt_ids, prompt_mask)
@@ -232,17 +233,19 @@ def __call__(
232233
# Transpose shape to [B, seq_len, 3*hidden_size]
233234
prompt_embeds_jax = jnp.transpose(out, (0, 2, 1, 3)).reshape((batch_size, seq_len_txt, -1))
234235
prompt_embeds_jax.block_until_ready()
235-
print(
236+
max_logging.log(
236237
f"DEBUG: prompt_embeds_jax min={float(prompt_embeds_jax.min()):.4f}, max={float(prompt_embeds_jax.max()):.4f}, mean={float(prompt_embeds_jax.mean()):.4f}, sum={float(prompt_embeds_jax.sum()):.4f}"
237238
)
238239

239240
trace["prompt_encoding"] = time.perf_counter() - t0
240-
print(f" -> [TIMING] Prompt Encoding (Qwen3): {trace['prompt_encoding']:.4f} seconds ⏱️")
241+
max_logging.log(f" -> [TIMING] Prompt Encoding (Qwen3): {trace['prompt_encoding']:.4f} seconds ⏱️")
241242

242243
# ---------------------------------------------------------------------
243244
# PHASE B: Denoising Loop (Flux Transformer)
244245
# ---------------------------------------------------------------------
245-
print(f"[PHASE B] Running {num_inference_steps}-step E2E Denoising Loop on a batch of {batch_size} images...")
246+
max_logging.log(
247+
f"[PHASE B] Running {num_inference_steps}-step E2E Denoising Loop on a batch of {batch_size} images..."
248+
)
246249
t0 = time.perf_counter()
247250

248251
guidance_vec_val = None
@@ -267,17 +270,19 @@ def __call__(
267270

268271
# Print progress
269272
sigma_val = scheduler_state.sigmas[step_idx]
270-
print(f" -> Step {step_idx}: Timestep = {scheduler_state.timesteps[step_idx]:.4f}, Sigma = {sigma_val:.4f}")
273+
max_logging.log(
274+
f" -> Step {step_idx}: Timestep = {scheduler_state.timesteps[step_idx]:.4f}, Sigma = {sigma_val:.4f}"
275+
)
271276

272277
latents_jax.block_until_ready()
273278

274279
trace["denoise_loop"] = time.perf_counter() - t0
275-
print(f" -> [TIMING] Denoising Loop (Flux): {trace['denoise_loop']:.4f} seconds ⏱️")
280+
max_logging.log(f" -> [TIMING] Denoising Loop (Flux): {trace['denoise_loop']:.4f} seconds ⏱️")
276281

277282
# ---------------------------------------------------------------------
278283
# PHASE C: Decode Latents (VAE Decoder)
279284
# ---------------------------------------------------------------------
280-
print("[PHASE C] Decoding final latents to RGB image using JAX VAE decoder on TPU...")
285+
max_logging.log("[PHASE C] Decoding final latents to RGB image using JAX VAE decoder on TPU...")
281286
t0 = time.perf_counter()
282287

283288
# Apply Channel-wise Batch Normalization Scaling in packed sequence format (denormalize)
@@ -295,12 +300,12 @@ def __call__(
295300
images_rgb.block_until_ready()
296301

297302
trace["vae_decode"] = time.perf_counter() - t0
298-
print(f" -> [TIMING] VAE Decoding: {trace['vae_decode']:.4f} seconds ⏱️")
303+
max_logging.log(f" -> [TIMING] VAE Decoding: {trace['vae_decode']:.4f} seconds ⏱️")
299304

300305
# ---------------------------------------------------------------------
301306
# POST-PROCESS: Format and Save Outputs
302307
# ---------------------------------------------------------------------
303-
print("Postprocessing and saving generated images...")
308+
max_logging.log("Postprocessing and saving generated images...")
304309
saved_paths = []
305310
# Clamp pixels and scale to [0, 255]
306311
images_rgb = jnp.clip((images_rgb + 1.0) / 2.0, 0.0, 1.0)
@@ -322,7 +327,7 @@ def __call__(
322327

323328
output_png_path = os.path.join(output_dir, batch_output_name)
324329
img.save(output_png_path)
325-
print(f" -> Saved image: {output_png_path} | Prompt: '{prompts[b_idx]}'")
330+
max_logging.log(f" -> Saved image: {output_png_path} | Prompt: '{prompts[b_idx]}'")
326331
saved_paths.append(output_png_path)
327332

328333
return saved_paths, trace

0 commit comments

Comments
 (0)