Add FLUX.2-klein-4B text-to-image and image-editing sample (chunked DiT on CompiledModel GPU) - #227
Open
john-rocky wants to merge 3 commits into
Open
Conversation
…l GPU) FLUX.2 [klein] 4B (Apache-2.0) generating images end to end on the LiteRT CompiledModel GPU delegate. The 4B rectified-flow transformer and its 4B Qwen3 text encoder are exported as twelve INTEGER-int8 graphs (6.2 GB total, largest 912 MB) and executed one at a time, so the peak footprint is a single graph. klein is step-wise distilled: 4 steps, no classifier-free guidance, one transformer pass per step. Pixel 8a: 306 s, PSNR 36.8 dB / corr 0.9987 against the fp32 diffusers pipeline. Graphs: huggingface.co/litert-community/FLUX.2-klein-4B-LiteRT The sample encodes three GPU-delegate constraints, two of which are invisible to a desktop op check -- the graphs compile, report zero unsupported operations, and return wrong numbers: - BROADCAST_TO is rejected outright, and Tensor.expand lowers to it. Grouped-query attention's repeat_kv hits this and the 4D tensor limit in one expression, so it is rewritten as a CONCATENATION of per-KV-head slices. - A broadcast ADD whose left operand is a BATCH_MATMUL result is silently miscomputed, which is softmax(q @ k^T * scale + mask[1,1,S,S]) -- every masked attention. The probabilities still sum to 1 and still honour the causal and padding masks, but the logits are wrong. The mask is therefore handed in pre-expanded to [1, num_heads, S, S]: zero extra ops, same formula. - Compute is forced to FP32; the modulated (adaLN) blocks return NaN in fp16. conversion/ reproduces every graph, stages the host inputs, and runs the exact device loop on the host driven only by the exported .tflite files. conversion/probe_enc_layer.py taps one encoder layer at every stage; that is what localized the masked-attention miscompile.
klein is natively an image-editing model: Flux2KleinPipeline takes `image` as its first argument and text-to-image is the `image=None` case. Editing VAE-encodes the reference and appends its latent tokens to the noise tokens before every step, which grows the joint sequence from 768 to 1024. The weights are unchanged, so the kce_* graphs are the same tensors re-exported at the longer shape and the chunk sizes are byte-identical. Device-verified on a Pixel 8a: every chunk fully delegates to LITERT_CL in one partition, PSNR 44.3 dB / SSIM 0.9998 against the fp32 diffusers pipeline, and editing costs about +7% wall-clock over generation. The app adds one graph beyond the twelve: kv_vae_enc.tflite. Its mode() chunks the 64-channel moments in half, which lowers to the banned SPLIT; slicing the first 32 channels directly is bit-exact and GPU-clean.
The sample baked the prompt into the staged tensors, so a text-to-image demo you could not type a prompt into. Only two staged tensors depend on the words - inputs_embeds and enc_mask - so the app now carries a faithful Qwen2Tokenizer port and looks the token rows up in a memory-mapped fp16 copy of the Qwen3 embedding table (a GATHER over 151936 rows is not a GPU op, and the row is the graph's input anyway). Device-verified on a Pixel 8a: a typed 'a blue ceramic teapot on a marble counter, morning light' generates the teapot, not the baked apple. The tokenizer matches Python byte-for-byte on a 20-case fixture (CJK, emoji, contractions, zero-width space); the fp16 embedding table is within 3e-8 of fp32 because the checkpoint is bf16. When klein_tokenizer/ is staged the app shows editable prompt fields; otherwise it falls back to the baked prompt. Adds export_tokenizer_klein.py and the 778 MB embedding table.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
FLUX.2 [klein] 4B (Black Forest Labs, Apache-2.0) generating images end to end on the LiteRT
CompiledModelGPU delegate. Nothing runs on the CPU: the Qwen3-4B text encoder, the 4B rectified-flow transformer and the VAE decoder are allAccelerator.GPUgraphs.The upstream model card describes klein as running "on consumer GPUs, with as little as 13 GB VRAM". This sample runs the same weights on a Pixel 8a's Mali-G610.
diffuserspipelineWhat it demonstrates
Sequential residency.
CompiledModelcannot load a graph over 2 GB, and the GPU budget is well under that anyway. The model is split into chunks resident one at a time, so peak footprint is a single ~912 MB graph. Chunks of roughly 500 MB - 1 GB are the sweet spot; a 1.8 GB chunk sends the shader compiler into a pathological blowup.A distilled sampling loop. klein reports
is_distilled, so the pipeline runs no classifier-free guidance: one transformer pass per step rather than two, and a plain flow-matching Euler update.Host/GPU split. Tokenization,
embed_tokens, the mask, both rotary tables, the scheduler and the two tail permutations are precomputed byconversion/gen_prep_klein.py. The tail (unpack by position id, batch-norm denorm, 2x2 unpatchify) is two pure permutations around one elementwise op, so the permutations ship as int32 gather maps recovered with anarangeprobe through the stock pipeline functions.Two GPU-delegate constraints worth flagging
Neither is visible to a desktop op check: the graphs compile, report zero unsupported operations, and return wrong numbers.
BROADCAST_TOis rejected outright, andTensor.expandlowers to it. Grouped-query attention'srepeat_kvhits this and the 4D tensor limit in one expression, so it is rewritten as aCONCATENATIONof per-KV-head slices — same head order, exact.A broadcast
ADDwhose left operand is aBATCH_MATMULresult is silently miscomputed. That issoftmax(q @ kᵀ * scale + mask[1,1,S,S]), i.e. every masked attention. There is no error and no NaN: the probabilities still sum to 1 and still honour the causal and padding masks, but the logits are wrong and the image comes out as structured garbage. The fix here is to hand the mask in already expanded to[1, num_heads, S, S]— zero extra ops, same formula.The diagnostic signature is that token 0 is bit-exact and every later token is wrong, since token 0 attends only to key 0. A broken RoPE looks identical, because RoPE is the identity at position 0.
conversion/probe_enc_layer.pyexports one encoder layer with a tap at every stage, which is how this was localized; materializing an intermediate as a graph output made the bug disappear, which is what identified the fused pattern.I'm happy to file the second one as a separate delegate issue if that's useful — it affects any encoder-style transformer with a
[1,1,S,S]mask.Verification
conversion/gen_verify_klein.pyruns the exact device loop on the host, driven only by the twelve exported.tflitegraphs, and reports PSNR against the fp32 reference. The Kotlin is a transcription of that loop.Note that the desktop int8 path is a pessimistic proxy: the same graphs score 36.4 dB through the host CPU int8 kernels and 44.1 dB on the GPU delegate.
Directory
Placed at
compiled_model_api/text_to_image/flux2_klein_kotlin_gpu/so it can sit alongside another text-to-image sample. Happy to rename if you'd prefer a different convention.