Instructions to use litert-community/FLUX.2-klein-4B-LiteRT with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- LiteRT
How to use litert-community/FLUX.2-klein-4B-LiteRT with LiteRT:
# No code snippets available yet for this library. # To use this model, check the repository files and the library's documentation. # Want to help? PRs adding snippets are welcome at: # https://github.com/huggingface/huggingface.js
- Notebooks
- Google Colab
- Kaggle
File size: 11,405 Bytes
ef69644 0f42565 ef69644 0f42565 7631672 ef69644 0f42565 ef69644 0f42565 ef69644 f9b9171 0f42565 ef69644 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | ---
license: apache-2.0
base_model: black-forest-labs/FLUX.2-klein-4B
tags:
- litert
- text-to-image
- on-device
- diffusion-transformer
- int8
pipeline_tag: text-to-image
library_name: litert
---
# FLUX.2-klein-4B β LiteRT (on-device text-to-image and image editing)
Black Forest Labs **[FLUX.2 [klein] 4B](https://huggingface.co/black-forest-labs/FLUX.2-klein-4B)**
(Apache-2.0) converted to **LiteRT `CompiledModel`** int8 graphs, generating **and
editing** images fully on a phone GPU. The upstream model card says klein "runs on consumer GPUs, with
as little as 13 GB VRAM". These graphs run it on a Pixel 8a's Mali-G715, which has no
dedicated VRAM at all.

Prompt: *"a red apple on a wooden table, studio lighting"*. 4 steps, 256Γ256, generated
end-to-end on a Pixel 8a Mali GPU in 306 s. Matches the fp32 `diffusers` pipeline at
**PSNR 36.8 dB / corr 0.9987**.

Editing: source, the fp32 `diffusers` edit, and the same edit on a Pixel 8a. Prompt:
*"turn the apple into a green apple"*. 4 steps, 256Γ256, **PSNR 44.3 dB / SSIM 0.9998**
against the fp32 pipeline, in 328β369 s.
klein is **step-wise distilled**, so the sampling loop is unusually plain: 4 steps, no
classifier-free guidance (one DiT pass per step, not two), no sign flip β just a
flow-matching Euler update `latents += dsigma[step] * noise_pred`.
## What's here
The pipeline (Qwen3-4B text encoder β rectified-flow DiT β VAE) is exported as
**INTEGER-int8** LiteRT graphs β intΓint compute, the path the GPU delegate actually
runs; weight-only-FLOAT quantization hangs the GPU compile. The 4B DiT and the 4B
encoder each exceed both LiteRT's 2 GB flatbuffer load limit and a phone's GPU budget,
so they are **split into chunks that are resident one at a time**: peak footprint is a
single ~912 MB graph rather than the 6.2 GB total.
| Graph | Role | int8 size | I/O (256 px) |
|-------|------|-----------|--------------|
| `ke_enc0` / `ke_enc1` / `ke_enc2` | Qwen3-4B layers 1-9 / 10-18 / 19-27 | 912 MB each | `[1,512,2560]` β `[1,512,2560]` |
| `kc_prep` | image + context embedders, 3 modulation FCs | 166 MB | `img[1,256,128]`, `txt[1,512,7680]`, `temb[1,3072]` β hidden + 3 modulations |
| `kc_double0` / `kc_double1` | 3 + 2 double-stream blocks | 739 / 492 MB | `img[1,256,3072]`, `txt[1,512,3072]` β same |
| `kc_single0..3` | 5 single-stream blocks each (20 total) | 615 MB each | `joint[1,768,3072]` β `[1,768,3072]` |
| `kc_final` | adaLN-continuous norm + projection | 19 MB | `[1,768,3072]` β `[1,256,128]` |
| `kv_vae` | VAE decoder | 50 MB | `latent[1,32,32,32]` β `img[1,3,256,256]` |
| `kce_prep` / `kce_double*` / `kce_single*` / `kce_final` | the same eight graphs re-exported for editing | 166 / 739 / 492 / 615Γ4 / 19 MB | image tokens `[1,512,128]`, joint `[1,1024,3072]` |
| `kv_vae_enc` | VAE encoder (editing only) | 35 MB | `img[1,3,256,256]` β `latent[1,32,32,32]` |
The text encoder is included because klein conditions on Qwen3-4B **hidden states from
layers 9 / 18 / 27**, interleaved to 7680 channels β not on a pooled embedding, so there
is no smaller drop-in replacement.
Tensors are raw `float32`, little-endian, row-major. Tokenization, `embed_tokens`, the
causal+padding mask, both rotary tables, the scheduler and the two tail permutations run
on the host.
## Typing the prompt
The prompt is not baked in β only two of the staged tensors depend on the words
(`inputs_embeds` and `enc_mask`), so the app encodes a typed prompt itself. Stage the
`tokenizer/` folder here (`qwen_vocab.txt`, `qwen_merges.txt`, `qwen_special.txt` and the
778 MB `qwen_embed_fp16.bin`) and the app shows editable prompt fields.
It carries a faithful `Qwen2Tokenizer` port, fixture-tested byte-for-byte against Python
(`tokenizer_fixture.txt`), and looks token rows up in the memory-mapped fp16 embedding
table β a `GATHER` over 151936 rows is not a GPU op, and the gathered row is the graph's
input anyway. The fp16 table is within 3e-8 of the fp32 weights because the checkpoint is
bf16. Tokenizing and embedding a prompt takes about 1 s; a typed "a blue ceramic teapot on
a marble counter, morning light" generates the teapot, not the baked apple.
## Image editing
`Flux2KleinPipeline.__call__` takes `image` as its **first** argument: klein is natively
an editing model 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,
```python
latent_model_input = torch.cat([latents, image_latents], dim=1)
latent_image_ids = torch.cat([latent_ids, image_latent_ids], dim=1)
```
with the reference separated from the noise on the rotary `T` axis (noise `T = 0`, the
i-th reference `T = 10 + 10Β·i`). Afterwards only the noise half is kept:
`noise_pred[:, :latents.size(1)]`.
At 256Γ256 that grows the image sequence 256 β 512 tokens, so the joint sequence goes
768 β 1024. **The weights do not change**, so `kce_*` are the same tensors re-exported at
the longer shape and every chunk is byte-identical in size to its `kc_*` twin. Measured
on a Pixel 8a: peak RSS **+2 %**, per-step GPU time **1.6Γ**, shader compile **1.0β1.2Γ**
β and since compilation dominates, editing costs about **+7 %** end-to-end.
The VAE encoder needed one rewrite: `DiagonalGaussianDistribution.mode()` chunks the
64-channel moments in half, which lowers to the banned `SPLIT`. The mode *is* the mean,
so slicing the first 32 channels directly is bit-exact and GPU-clean.
**Caveats.** 256Γ256, one reference image, and the prompt is baked into the staged host
tensors. More references or a different resolution means a re-export, not a redesign.
Report SSIM next to PSNR: the residual error is *sparse*. On a moonlit-snow edit the
worst 0.5 % of pixels carry 76 % of the squared error (specular speckles flipping between
near-white and dark blue), which drags PSNR to 27.9 dB while SSIM stays at 0.989 and the
image is perceptually identical to the fp32 reference.
## Two things the graphs assume
Both come from the GPU delegate (ML Drift), and neither is visible to a desktop op check.
1. **The attention mask is pre-expanded across heads**: pass `[1, 32, 512, 512]`, not
`[1, 1, 512, 512]`. A broadcast `ADD` whose left operand is a `BATCH_MATMUL` result is
silently miscomputed β the probabilities still sum to 1 and still honour the causal and
padding masks, but the logits are wrong.
2. **Compute must be FP32**: `GpuOptions(precision = FP32)`. The modulated (adaLN) blocks
overflow fp16 and return NaN.
Also: create one `Environment` and share it across every `CompiledModel` (a null
environment leaks the OpenCL context), and close every `TensorBuffer` after each run.
## Usage β Python (reproduces the exact device loop)
```python
import numpy as np
from ai_edge_litert.compiled_model import CompiledModel
def run(path, *inputs):
"""Runs one chunk, then releases it β sequential residency, as on device."""
model = CompiledModel.from_file(path)
signatures = model.get_signature_list()
key = list(signatures)[0]
input_details = model.get_input_tensor_details(key)
output_details = model.get_output_tensor_details(key)
input_buffers = model.create_input_buffers(0)
output_buffers = model.create_output_buffers(0)
for name, buffer, value in zip(signatures[key]["inputs"], input_buffers, inputs):
buffer.write(np.ascontiguousarray(value, np.dtype(input_details[name]["dtype"])))
model.run_by_index(0, input_buffers, output_buffers)
outputs = []
for name, buffer in zip(signatures[key]["outputs"], output_buffers):
detail = output_details[name]
flat = buffer.read(int(np.prod(detail["shape"])), np.dtype(detail["dtype"]))
outputs.append(flat.reshape(detail["shape"]).copy())
return outputs
# Host prep (tokenizer, embed_tokens, mask, rotary tables, sigmas) omitted β see below.
hidden, taps = inputs_embeds, []
for i in range(3):
hidden = run(f"ke_enc{i}.tflite", hidden, mask, enc_cos, enc_sin)[0]
taps.append(hidden)
prompt_embeds = np.stack(taps, 1).transpose(0, 2, 1, 3).reshape(1, 512, 7680)
for step in range(4):
image, text, mod_img, mod_txt, mod_single = run(
"kc_prep.tflite", latents, prompt_embeds, temb[step:step + 1])
for i in range(2):
image, text = run(f"kc_double{i}.tflite", image, text, cos, sin, mod_img, mod_txt)
joint = np.concatenate([text, image], axis=1)
for i in range(4):
joint = run(f"kc_single{i}.tflite", joint, cos, sin, mod_single)[0]
latents = latents + dsigma[step] * run("kc_final.tflite", joint, temb[step:step + 1])[0]
latent = unpatchify(unpack(latents) * bn_std + bn_mean) # two pure permutations
image = run("kv_vae.tflite", latent)[0] # [1,3,256,256] in [-1,1]
```
## Usage β Kotlin (Android, LiteRT GPU)
```kotlin
val environment = Environment.create() // create once, share
fun gpu(name: String, inputs: List<FloatArray>): List<FloatArray> {
val options = CompiledModel.Options(Accelerator.GPU)
options.gpuOptions = CompiledModel.GpuOptions(
precision = CompiledModel.GpuOptions.Precision.FP32)
val model = CompiledModel.create(File(dir, name).absolutePath, options, environment)
val inputBuffers = model.createInputBuffers()
val outputBuffers = model.createOutputBuffers()
inputs.forEachIndexed { index, values -> inputBuffers[index].writeFloat(values) }
model.run(inputBuffers, outputBuffers)
val outputs = outputBuffers.map { it.readFloat() }
inputBuffers.forEach { it.close() }
outputBuffers.forEach { it.close() }
model.close() // one graph resident at a time
return outputs
}
var hidden = inputsEmbeds
val taps = (0 until 3).map { gpu("ke_enc$it.tflite", listOf(hidden, mask, encCos, encSin))[0]
.also { output -> hidden = output } }
// interleave the three taps -> [1, 512, 7680], then the 4-step DiT loop, then kv_vae
```
## Conversion
Quantization is `litert_torch` `full_dynamic_recipe(weight_dtype=INT8, granularity=CHANNELWISE)`.
The conversion scripts (`build_klein_enc.py`, `chunked_export_klein.py`,
`vae_deploy_klein.py`) and the host-prep / verification reference
(`gen_prep_klein.py`, `gen_verify_klein.py`) ship alongside the LiteRT sample app for
this model. Three rewrites are required for a GPU-clean graph, all exact:
- **RoPE without `GATHER_ND`** β bake the even/odd de-interleave into the rows of
`to_q` / `to_k` and the fused `to_qkv_mlp_proj`, turning it into a contiguous
half-split rotation. `q Β· k` is invariant to a permutation applied to both.
- **GQA `repeat_kv` as a `CONCATENATION`** β the stock `expand` is rank-5 *and* lowers
to `BROADCAST_TO`, which the GPU delegate rejects outright.
- **Safe RMSNorm / LayerNorm** (max-normalized) and `ManualGroupNormND` in the VAE.
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 device. Weights are never
redistributed here β the graphs are produced from the original Apache-2.0 checkpoint
with those scripts.
## License
Apache-2.0, inherited from `black-forest-labs/FLUX.2-klein-4B` (weights and text encoder).
|