FLUX.2 klein fine-tuning is two jobs people keep merging. Training runs on your own GPU against an open-weight Base checkpoint. Serving happens at api.bfl.ai, where the endpoint adds two fields. Each side has its own parameter names, and mixing them is the usual failure.
What fine-tuning actually changes
The training target is the Base variant, never the fast one. The overview calls the Base models undistilled foundation models carrying the full training signal, and the training page adds that a Base-trained adapter still loads on the distilled model. Licence decides which size ships: 4B Base is Apache 2.0, 9B Base is FLUX Non-Commercial, and the page recommends 4B for its lower hardware floor — adapters run 10–200MB and one to three hours on consumer GPUs, against days to weeks for full fine-tuning. Neither Base size is offered on the public API, which is why the loop starts on your own machine.
Dataset rules and minimum hardware
The training page is precise about image quality and silent about quantity: 1024px or higher, consistent quality, no artifacts. Two other official pages disagree: the step-by-step example calls 20–40 images optimal, the Hugging Face walkthrough asks for 15–40.
Captions invert the obvious rule: describe the content, say nothing about the style you are teaching, and open every caption with the same trigger word — the training page writes the pattern as [trigger]. Resolution is two-stage: 512px while you iterate, 1024px or higher for the final run.
The published hardware floors
| Variant | Minimum GPU | System memory | Licence |
|---|---|---|---|
| FLUX.2 klein 4B Base | NVIDIA, 12GB VRAM | 32GB | Apache 2.0 |
| FLUX.2 klein 9B Base | NVIDIA, 22GB VRAM | 64GB | FLUX Non-Commercial |
The walkthrough puts a 4B LoRA run under 24GB of VRAM and about an hour on an RTX 4090. No AMD or Apple Silicon path is documented; the two trainers named are AI-Toolkit and Diffusers.
The fine-tuning loop, step by step

Read the loop as four stages, because the vocabulary changes at each one: a folder of images with matching captions; a training run driven by keys such as trigger_word and train.lr; the .safetensors adapter, uploaded under Customization → Finetunes; then the endpoint, where finetune_id and finetune_strength appear and the trainer keys vanish.
That transition breaks most integrations: train.lr belongs to a finished config file, finetune_strength belongs to a live request, and neither accepts the other’s names.
Writing the training configuration
The example publishes a complete AI-Toolkit job, and the useful part is how little changes per run. Three keys move: config.name, trigger_word and datasets[0].folder_path, which name the output folder, match your captions, and point at the images. Inside network, linear and conv set the adapter rank with linear_alpha and conv_alpha as scaling factors; the default pairs 32/32 with 16/16.
# One folder: each image needs a caption file with the same basename.
mkdir -p ~/klein-lora/dataset
cd ~/klein-lora/dataset
for i in 1 2 3; do
cp ~/raw/shirt-$i.png "SHIRT_$i.png"
printf '%s\n' \
"SHIRTTR1GGER. A cream knit sweater on a plain white background, front view." \
> "SHIRT_$i.txt"
done
ls
The job itself is one YAML file and one command, using the published keys verbatim.
mkdir -p ~/klein-lora/config
cat > ~/klein-lora/config/shirt_lora.yaml <<'YAML'
job: "extension"
config:
name: "shirt_lora_v1"
process:
- type: "diffusion_trainer"
training_folder: "/app/ai-toolkit/output"
device: "cuda"
trigger_word: "SHIRTTR1GGER"
network:
type: "lora"
linear: 32
linear_alpha: 32
conv: 16
conv_alpha: 16
save:
dtype: "bf16"
save_every: 250
datasets:
- folder_path: "/home/you/klein-lora/dataset"
caption_ext: "txt"
resolution:
- 512
- 768
- 1024
train:
batch_size: 1
steps: 3000
lr: 0.0001
optimizer: "adamw8bit"
timestep_type: "weighted"
content_or_style: "balanced"
model:
name_or_path: "black-forest-labs/FLUX.2-klein-base-4B"
quantize: true
meta:
name: "[name]"
version: "1.0"
YAML
cd /app/ai-toolkit && python run.py ~/klein-lora/config
quantize: true is what fits a 4B run on a 12GB card, and the example lists it first among its VRAM optimisations. The documented default targets the 9B checkpoint.
Running the job and reading the run
Learning rate is asymmetric: the training page gives 8e-5 to 1e-4 for LoRA, and the example adds a correction — raise to 2e-4 when loss falls slowly, drop to 5e-5 when it oscillates.
Step counts disagree between official pages. The training page gives 1500–2500 steps for a style LoRA and 1500–3000 for a character LoRA. The example scales by dataset size, 800–1200 up to 2000–3000 for 50 images, and its config asks for 3000 steps with save_every: 150 before reporting that the 1500-step checkpoint was best — you cannot select a checkpoint the run never saved.
Watch the sample outputs. The training page names overfitting as the failure mode: when every sample looks like your dataset regardless of the prompt, that is the signal.
Uploading and calling the endpoint
Uploading is a Dashboard flow. Under Customization → Finetunes, + Add Finetune takes a name, a base model, a precision, an optional trigger phrase and the .safetensors file. The name becomes your finetune_id and decides which endpoint can serve the adapter.
Six endpoints are published. The two Base endpoints, /v1/flux-2-klein-base-4b-finetuned and /v1/flux-2-klein-base-9b-finetuned, serve Base-trained adapters, and the four distilled endpoints accept them too. FP8 is the default precision.
The submit-and-poll shape matches every other FLUX.2 endpoint; only two fields are new.
# The endpoint must match the base model and precision chosen in the Dashboard.
MODEL="flux-2-klein-base-4b-finetuned"
RESPONSE=$(curl -s -X POST "https://api.bfl.ai/v1/${MODEL}" \
-H "x-key: ${BFL_API_KEY}" \
-H 'Content-Type: application/json' \
-d '{
"prompt": "A cream knit sweater on a plain white background, SHIRTTR1GGER product shot.",
"finetune_id": "shirt-lora-v1",
"finetune_strength": 1.0
}')
POLLING_URL=$(echo "$RESPONSE" | jq -r '.polling_url')
while true; do
RESULT=$(curl -s "$POLLING_URL" -H "x-key: ${BFL_API_KEY}")
STATUS=$(echo "$RESULT" | jq -r '.status')
[ "$STATUS" = "Ready" ] && echo "$RESULT" | jq -r '.result.sample' && break
[ "$STATUS" = "Error" ] || [ "$STATUS" = "Failed" ] && echo "$RESULT" && break
sleep 1
done
finetune_id is required; a shared adapter is addressed as {owner_org_id}/{name}. finetune_strength defaults to 1.0, and sweeping 0.7 → 0.9 at a fixed seed is the documented fix when an adapter overpowers the prompt.
The same adapter runs locally through the Diffusers pipeline.
python - <<'PY'
import torch
from diffusers import Flux2KleinPipeline
pipe = Flux2KleinPipeline.from_pretrained(
"black-forest-labs/FLUX.2-klein-base-4B", torch_dtype=torch.bfloat16
)
pipe.load_lora_weights("klein-lora/output/shirt_lora_v1/shirt_lora_v1.safetensors")
pipe.to("cuda")
image = pipe(
"A cream knit sweater on a plain white background, SHIRTTR1GGER product shot.",
num_inference_steps=50,
guidance_scale=4.0,
).images[0]
image.save("shirt-lora-sample.png")
PY
One LoRA per request, so stacking is unsupported, and a strict base-model match, so the 4B endpoint rejects an adapter uploaded for 9B. A fine-tuned endpoint bills at its base endpoint’s rate during public beta, and no price for training is published. The local ComfyUI walkthrough covers sampler settings first, the submit-and-poll pattern is documented elsewhere, how the endpoints are billed has the rates, and the Chinese-language walkthrough covers prompting.
Frequently asked questions
Can I train through the API? No. The Base variants are open weights for local development and are not on the public API.
Which Base size should I train? 4B, unless you need 9B quality and have 22GB of VRAM. It is Apache 2.0 and needs less hardware.
Can a 9B adapter be served from the 4B endpoint? No. The base-model match is strict and a mismatched adapter fails.
How strong should finetune_strength be? Start at the default 1.0, and sweep 0.7 → 0.9 at a fixed seed only if the adapter overwhelms the prompt.