gguf-trainer
A trainer GUI for pig_clip adapters: small bridge networks that let
pig_clip (native train/fine-tune shipped as a GGUF) stand in for a diffusion
model's original text encoder in the ggk engine. Eight trainer packs ship:
- LLaDA-Image-Turbo — the 16B LLaDA2-MoE text stack is replaced by
pig_clip+ a 256-query resampler adapter (the trainer8 recipe); the adapter is text-only, so it pairs unchanged with the model's SigVQ vision encoder for image editing. - MageFlow-Edit — the Qwen3-VL-4B-Instruct text stack is replaced by
pig_clip+ a token-aligned adapter with a vision extension (the trainer5 recipe); the adapter pairs with the unchangedmmproj-qwen3vl-4b-it-f16.ggufvision encoder for editing and text-to-image. - Qwen-Image 2.1 — the Qwen3-VL-8B-Instruct text stack is replaced by
pig_clip+ the same kind of adapter, distilled from the full Hugging Face model (deepstack, M-RoPE image positions, pre-norm tap); text-to-image needs only the adapter, editing pairs it with the unchangedmmproj-qwen3vl-8b-it-f16.ggufvision encoder. - PixArt — the 4.7B T5-XXL v1.1 encoder (
--t5xxl) is replaced bypig_clip+ a 120-query resampler seeded with the T5 token ids (the originaltrainerrecipe); the T5 tokenizer stays at inference, the encoder is never loaded. - Ming-Image 0.1 (Design and Design-Layer) — the 16B Ling-mini-2.0
Bailing-MoE MLLM is replaced by
pig_clip+ a resampler whose query bank is the 256 caption rows plus one slot per Ling prompt token (seeded with a frozen PCA of the Ling embedding table; the Ling tokenizer rides inside the adapter GGUF). Editing and layer decomposition pair it withpig_ming_vision-f16.gguf, the model's own Qwen2.5-VL tower exported by the pack. - MOSS-Image + FLUX.3 Klein (Qwen3.5-9B) — one adapter for the two
roles Qwen3.5-9B plays: MOSS-Image's text encoder (one 4096-d row per
Qwen3.5 token of the chat template, references through the unchanged
mmproj-qwen3.5-9b-f16.gguftower) and FLUX.3 Klein's text conditioning (a token-aligned bank of rows standing in for the Qwen3-VL-4B states of the prompt, so neither the 9B upsampler nor the 4B encoder is loaded). The Qwen3.5 vocabulary rides inside the adapter GGUF. - FLUX.3 Klein (Qwen3-VL-4B-Instruct) — the 4B text encoder itself is
replaced by
pig_clip+ a token-aligned layerwise adapter emitting the 8-layer stack (layers 4..32, 20480 per token) the DiT conditions on (trainer6's krea2 recipe). Text only: reference images reach the DiT through the VAE, and the Qwen3.5 prompt upsampler stays an independent front end (--prompt-upsampler). - Fibo 2.0 Dev / Edit Turbo (Qwen3-VL-4B-Instruct) — the 4B text encoder
is replaced by
pig_clip+ a token-aligned layerwise adapter with the vision extension: it emits the raw 15-layer stack (layers 5..35, 38400 per token) the engine RMS-normalizes into the DiT's six text bundles, and pairs with the unchangedmmproj-qwen3vl-4b-it-f16.ggufvision encoder for editing (the reference image sits inside the prompt, as with MageFlow).
All packs train against pig_clip-f16.gguf as the student.
pip install gguf-trainer
gguf-trainer # opens http://127.0.0.1:8655/ in the browser
The GUI runs in your browser against a local backend, in the style of the ggk diffuser GUI. Nothing is uploaded: models, datasets and outputs are addressed by filesystem path through the built-in file browser.
What the GUI does
Setup tab — everything a run needs, in one project folder:
- Project: create or open a project, picking its trainer pack. A
project is a directory holding the downloaded materials, corpus,
precomputed shards, checkpoints and
eval.json, plusproject.json(settings) andstate.json(progress). The exported GGUFs land next to the project folder by default (test-trainer/pig_llada_adapter-f16.gguffor the projecttest-trainer/llada_adapter/), so every adapter trained under one folder ends up side by side; the Output section can point them elsewhere. The evaluation lives inside the project folder, so several projects sharing one output folder never overwrite each other'seval.json. - Materials: the pack lists what it needs — the teacher from Hugging
Face (LLaDA:
inclusionAI/LLaDA-Image-Turbotext encoder, QueryFormer, text_projection, tokenizer, ~33 GB; MageFlow:Qwen/Qwen3-VL-4B-Instruct, ~8.3 GB; Qwen-Image 2.1:Qwen/Qwen3-VL-8B-Instruct, ~17.5 GB; PixArt:callgg/t5-v1_1-xxl-encoder-bf16, ~9.5 GB), the student tokenizer/config (callgg/pig-clip-tokenizer, ~11 MB), pack extras (LLaDA: the optional SigVQ vision encoder, ~2.4 GB; MageFlow / Qwen-Image 2.1: the image dataset(s) selected under Corpus), and your localpig_clip-f16.gguf. One Download missing button fetches everything that is not on disk yet; it is disabled (and so is each material's own button) as soon as the files are present, so a second click can never start a duplicate download. Files you already have are found automatically — in the project, in the directory the GUI was started from, in another project'smaterials/, or in a folder named byGGUF_TRAINER_MATERIALS— and linked instead of downloaded again (apig_clip*.ggufnext to those is picked as the student). Downloads run as detached processes (they survive closing the GUI) and resume where they stopped after an interruption or a reboot. Headless:gguf-trainer download --project DIRdoes the same in the foreground. - Corpus: pick public prompt datasets (Stable Diffusion prompts,
Midjourney prompts, DiffusionDB, VidProM) and/or your own
.txt/.jsonlfiles, or point at ready-madetrain.txt/val.txt. ~1% empty prompts are injected so the adapter learns the empty CFG prompt. Image packs add image datasets (Flickr30k, COCO captions — downloaded as materials) and/or local image folders (optional<name>.txtcaption next to each image); edit instructions are synthesized from the captions, and the prompt datasets supply the text-only share on the text-to-image template. - Precompute / Training / Output: the pack's reference hyper-parameters, editable (LLaDA: width 1024, depth 6, 20k steps, batch 32, lr 2e-4 …; MageFlow: width 1024, depth 4, 20k steps, batch 32; PixArt: width 1024, depth 6, 20k steps, batch 32, sigma floor 0.03; the step count can be changed at any time, even mid-run — see below), the device, memory budgets for the teacher, the adapter name, output folder and an optional copy destination (your ggk model folder).
Train tab — start/stop/resume the pipeline, a stage strip (corpus → precompute val → precompute train → train → export → eval) with per-stage progress and ETA, live loss / cosine / val-cosine chart, current metrics, and live GPU/CPU/RAM readings of the machine and the pipeline process.
Hardware tab — GPUs (nvidia-smi + torch), RAM, disk, Python/torch/ transformers versions. Logs tab — the pipeline log, following. Output tab — exported files, the evaluation of the exported GGUF, the mid-run snapshots (step, validation cosine, eval per file), the kept steps any of which exports as a snapshot (Export / Delete per row), and the ggk command that uses it (Copy).
Resuming after a reboot
The pipeline runs as a detached process (setsid / detached process group) and every stage is idempotent and checkpointed:
- corpus files and each shard are written atomically and skipped when present;
- training saves
last.ptevery N steps (and on Stop / SIGTERM), including the optimizer, RNG and the exact position in the shard stream; - export/eval rerun only when the checkpoint is newer than the GGUF.
The Export GGUF button (Output tab) or a click on the Export /
Evaluate stage boxes regenerates them from
best.pton demand, e.g. after the GGUF was deleted (gguf-trainer start --project DIR --only export eval --force).
Open the project (or start the GUI with gguf-trainer --auto-resume, which
relaunches the last project if its process died while running) and press
Start / Resume. The same works headless:
gguf-trainer run --project ~/gguf-trainer/projects/llada_adapter # foreground
gguf-trainer start --project ~/gguf-trainer/projects/llada_adapter # detached
gguf-trainer stop --project ~/gguf-trainer/projects/llada_adapter # saves, then exits
gguf-trainer status --project ~/gguf-trainer/projects/llada_adapter
gguf-trainer download --project ~/gguf-trainer/projects/llada_adapter # fetch missing materials
Changing the number of training steps
The planned step count (train.steps, default 20 000) is a setting like any
other, not a fixed recipe: the Steps field on the Setup tab, the
planned steps box on the Train tab (Apply), or the CLI change it, and the
change is honoured at every point of a project's life:
- before the run — the usual case (a quick 5k pilot, a long 40k run);
- while training runs — the trainer re-reads
project.jsonat every log interval (log_every, default 25 steps). Lower the count to finish now: the current step is validated and saved, then export and eval run as usual. Raise it to keep going past the original plan; - after a finished run — a higher count un-finishes the training stage;
Start / Resume (or
gguf-trainer start) continues fromlast.pt(optimizer, RNG and shard position included) up to the new count, and the GGUF is re-exported ifbest.ptimproves.
gguf-trainer set --project DIR --steps 12000 # also while it runs
gguf-trainer start --project DIR --steps 40k # set, then launch
The cosine learning-rate schedule is computed from the count in force, so
extending a finished run lifts the lr back up mid-schedule (warm restart);
snapshots and the exported GGUF record the planned count that was in force
(adapter.planned_steps).
Snapshots: a usable GGUF at any step
You do not have to wait for the last of the 20 000 steps. Stop pauses the
run after the current step (Start / Resume continues it from last.pt), and
Export snapshot (Train tab, Output tab, or the CLI) writes the adapter as
it is right now — or, with the kept-steps schedule switched on, as it was
at any step the trainer kept (0.0.16):
gguf-trainer snapshot --project DIR # newest step, while training runs or after a stop
gguf-trainer snapshot --project DIR --checkpoint best # best validation score so far
gguf-trainer snapshot --project DIR --eval # + evaluate the GGUF on the val shards
gguf-trainer snapshot --project DIR --checkpoint list # the kept steps on disk
gguf-trainer snapshot --project DIR --step 12000 # any kept step (= --checkpoint step12000), also mid-run
0.0.17 fixes the snapshot export of layerwise adapters on Windows: the read-back check kept the fresh
.gguf.tmpmemory-mapped, so the final rename failed with WinError 32 … used by another process every time (seen withpig_qwen3vl_4b_it_adapter). The rename now also falls back to copying the file when something else (antivirus, an editor) holds the.tmpopen.
Kept steps: export any completed point, not just the one you paused at
last.pt is overwritten by every save, so until 0.0.15 a snapshot could only
be taken at the moment the run was paused or still running. The trainer can
also keep a weights-only copy of the adapter under
checkpoints/history/step<N>.pt (+ a .json sidecar with its validation
cosine). Off by default since 0.0.18 — every kept step costs disk — and
scheduled by the project (Setup › Train › Keep steps for later export, or
train.* in project.json):
| setting | default | meaning |
|---|---|---|
history |
false |
the switch; nothing is kept while it is off |
history_every |
500 | keep every N steps (0 = at every validation) |
history_from / history_to |
0 / 0 | only steps inside this window (0 = start / end of the run) |
history_keep |
0 | hold only the newest N files (0 = all) |
-
when (on): every
history_everysteps inside the window, plus every stop, snapshot request and the final step inside it. With the defaults a finished 20 000-step run has steps 500, 1000, … 20 000 on disk; withhistory_from 15000andhistory_every 1000only the last six. Each can be extracted later with--step N, the Export button next to it on the Output tab › Kept steps, or the A kept step choice of the Export-snapshot dialog. The Setup tab shows how many files the schedule will produce. -
size: no optimizer state, and the tensors the GGUF stores as f16 are already f16 — about a sixth of
last.ptper step (~350 MB for the Qwen3.5 bridge, ~200 MB for LLaDA). The Output tab shows the total, each row has a Delete button, and Reset training removes them all. -
exactness: the f32 tensors and the output heads the standardization is folded into stay f32, so a GGUF exported from
history/step<N>.ptis byte-identical (tensor for tensor) to a snapshot taken fromlast.ptat that step — the history is a real substitute, not an approximation. -
adapter.checkpointin the GGUF andsnapshots.jsonrecordstep<N>as the source; the file name is the same step-tagged<name>-step<N>-f16.gguf. -
While the train stage is running the job drops a
SNAPSHOTfile; the trainer validates and saveslast.ptat the current step, removes the file and keeps training — nothing is interrupted (a few seconds).--no-freshtakes the checkpoint already on disk instead. -
The file is step-tagged,
<name>-step<N>-f16.gguf, next to the final<name>-f16.gguf, so several points of one run can be kept and swapped into the same engine command. The final export is not affected. -
<project>/snapshots.json(Output tab › Snapshots) lists every snapshot with its step, the planned steps, the validation cosine measured when the checkpoint was saved and the optional eval — the steps-vs-quality trade-off is visible instead of hidden behind the final export. -
Provenance lives in the GGUF itself:
adapter.trained_steps,adapter.planned_steps,adapter.checkpoint(last/best/step<N>),adapter.snapshot,adapter.val_cos,adapter.best_val_cos. -
The eval of a snapshot taken while training runs is done on the CPU (the training process owns the GPU);
--deviceoverrides. -
Resetting the training stage also clears the manifest; the snapshot files are removed with the other exports by the "output" reset.
The LLaDA-Image pack
Teacher target per prompt = the 256 QueryFormer rows of cap_feats
([256, 2560]): LLaDA2-MoE over [tokens ; 256 queries] with the text
masked from seeing the queries, then the 6-layer text_projection. QueryFormer
and text_projection are re-implemented in plain torch (bit-exact against the
diffusers originals) so no diffusers install or reference checkout is
needed; the MoE backbone loads through trust_remote_code from the snapshot
and is placed sequentially: the chosen GPU up to its budget, then the
other CUDA devices, then CPU RAM (the Precompute tab can switch to
accelerate's balanced split, which caps the biggest card at an even share of
the model and offloads the rest — roughly half the throughput).
Student = pig_clip final-norm hidden states over the engine's exact template
(<role>HUMAN</role> Generate an image: {text}\n<role>ASSISTANT</role>\n<IMAGE1>),
Qwen BPE without special tokens. The adapter is a seedless Perceiver
resampler (self-attn + cross-attn + GELU MLP, head_dim 64) trained with
whitened MSE + cosine on per-dim standardized targets; the export folds the
standardization into out_proj and writes f16 weights / f32 norms, biases and
query, exactly the layout pig_llada_adapter-f16.gguf shipped with.
Use it in ggk (≥ 0.5.7):
ggk diffuser engine -- --diffusion-model LLaDA-image-turbo-nvfp4.gguf \
--vae pig_flux2_vae_fp32-f16.gguf \
--llm pig_clip-q8_0.gguf --llm-adapter pig_llada_adapter-f16.gguf \
--llm_vision pig_llada_sigvq-f16.gguf \
--ref-image sheep.png -p "a sheep in sunglasses" --cfg-scale 1.0 \
--steps 4 --sampling-method euler --diffusion-fa -o out.png
Text-to-image works with any student quantization; editing wants pig_clip
at q8_0 or better. Judge a run by val centred cosine / rel_mse (0.965 /
0.0024 on the reference 5090 run, ~55 min of training); plain cosine on these
rows is ~0.99 even for a zero prediction.
transformers 5 and the teacher's rotary tables. transformers 5 builds
models on the meta device and does not re-initialize the non-persistent
buffers of remote (trust_remote_code) models, so the LLaDA2-MoE backbone
came up with an uninitialized inv_freq RoPE table. The teacher still
emitted plausible rows (same mean and scale, plain cosine 0.999 to the real
ones) but with the positional signal scrambled, and adapters trained on those
targets ignore the prompt in the engine (0.1–0.2 lower centred cosine against
the true teacher; edits return the reference image). Since 0.0.3 the teacher
repairs the tables after loading and refuses to run with a bad one, and every
shard directory carries a CONTRACT marker: shards written before the fix
(contract llada_image/1 or none) are discarded on the next run, the
checkpoints trained on them are moved to checkpoints.stale-<time>/, and
precompute + training start over. A quick health check of any adapter is its
centred cosine against a teacher-conditioned engine context
(trainer8/dumps/m1_gpu_full/context.bin): ≥ 0.96 is healthy, ~0.87 is the
broken-RoPE signature.
The MageFlow-Edit pack
MageFlow-Edit conditions its DiT on Qwen3-VL-4B-Instruct final-norm
hidden states with the reference image spliced in as mmproj vision tokens.
The pack distills that conditioning into pig_clip + a token-aligned
adapter with a vision extension (trainer5): position i of the student maps
to position i of the teacher, and because ggk splices the 2560-d mmproj
embeds into the LLM input while the 0.6B student embeds at 1024, the adapter
owns the bridge in both directions — a frozen vision_proj (2560 → 1024,
ridge least squares over the shared vocabulary, applied by the engine to
every mmproj embed before the student) and a trained vis_in (2560 → width)
that hands the adapter the raw mmproj embeds, so vision fidelity does not
depend on what survives the student. The 4B mmproj stays exactly as the
teacher uses it; nothing else needs converting.
The engine contract is replicated exactly (verified against ggk
SD_DUMP_COND dumps): the 64-token edit template / 34-token text-to-image
template, the 6-token Image N: <|vision_start|> header, nearest-neighbour
resize to a multiple of 32 with the long side capped at 384, OpenAI-CLIP
normalisation, the vision tower's main merger output only (ggk drops
deepstack), all-equal M-RoPE (= plain rope), final-norm tap. The vision
tower is the one the mmproj was converted from (HF bf16 vs engine f16: cosine
0.998), the text stack matches the engine's q4_k_m teacher at the known
quantisation floor (0.975 on text positions).
Corpus (trainer5 mix): 56k single-image + 3k two-image samples with instructions synthesized from the captions (plain and truncated captions, add / remove / replace / restyle / recolor / background patterns, ~1.5% empty) plus 27k text-only prompts; val = 1024 image + 512 text samples, val images never in train. Default image source is Flickr30k (4.4 GB, 31k photos with 5 captions each — several instructions per photo); the COCO captions preset (17 GB, 113k photos) is the trainer5 source. Every sample stores the teacher and student states of every token plus the raw vision embeds (~2.4 MB per image sample, ~200 GB for the full mix) — pick the corpus size for your disk.
Precompute runs the vision tower, the Qwen3-VL text stack and pig_clip
once per shard. The text stack (~7.5 GB bf16) stays resident when the GPU
budget allows, otherwise it is streamed through the GPU with accelerate's
cpu_offload (≈ 1–2 samples/s on a 6 GB laptop card, ~44 on an RTX 5090
resident); batch/token budgets at 0 are chosen per card. Training uses the
trainer5 recipe: width 1024 (its width gate winner), depth 4, 20k steps,
batch 32, lr 2e-4, warmup 1k, whitened MSE + 0.5·(1−cos) masked to real
tokens, out_proj zero-initialised, vision_proj frozen. The export writes
the same layout as the shipped pig_qwen3vl_4b_adapter-f16.gguf (f16
weights, f32 norms/biases/vision_proj).
Use it in ggk:
ggk diffuser engine -- --diffusion-model mageflow-edit-turbo-nvfp4.gguf \
--vae pig_mageflow_vae_fp32-f16.gguf \
--llm pig_clip-q8_0.gguf --llm-adapter pig_qwen3vl_4b_adapter-f16.gguf \
--llm_vision mmproj-qwen3vl-4b-it-f16.gguf \
--ref-image sheep.png -p "a sheep in sunglasses" --cfg-scale 1.0 \
--steps 4 --sampling-method euler --diffusion-fa -o out.png
Judge a run by cos_slice (cosine over the positions the DiT consumes)
and the cos_vis / cos_txt split: the trainer5 reference reached val cos
0.915 (vision positions 0.80, text 0.98) and its A/B edits were
near-identical to the teacher's — the 4-step DiT forgives far more than the
cosine suggests. A lagging vision cosine means the vis_in path or the
width is the limiter, not the student.
The Qwen-Image 2.1 pack
Qwen-Image 2.1 conditions its DiT on Qwen3-VL-8B-Instruct: the last
decoder layer before the final RMSNorm, on one template for both modes
(<|im_start|>system\nComprehend and analyze the provided prompt.<|im_end|>\n
— 14 tokens, dropped — then the user turn, with
<imageN><|vision_start|>…<|vision_end|> blocks in front of the instruction
when editing). The pack (qwen3vl_qwenimage) is the 8B sibling of the
MageFlow pack — same adapter kind, same student, the same frozen
vision_proj / trained vis_in bridge, here 4096 ↔ 1024 — and exports
pig_qwen3vl_8b_adapter-f16.gguf:
--llm pig_clip-<quant>.gguf --llm-adapter pig_qwen3vl_8b_adapter-f16.gguf
[--llm_vision mmproj-qwen3vl-8b-it-f16.gguf -r ref.png]
Three things differ from MageFlow, all of them the engine contract
(vision_data.QwenImage21Contract) rather than the model size:
- The teacher is the unreduced model. For MageFlow the teacher replicates
what ggk runs (no deepstack, plain 1-D rope, final norm). Here it is
Hugging Face's own forward — deepstack features added into the first
decoder layers, real (t, h, w) M-RoPE positions for the image tokens, and
the pre-norm tap (
qwen3vl_teacher.Qwen3VLFullTeacher; it reproducesQwen3VLModel(pixel_values=…)to 2e-7 on a padded multi-image batch, the rest being the bf16 the shards are stored in). The adapter still only ever receives the mmproj's main output, so what deepstack told the teacher is learned from that — which is also why the adapter route needs no deepstack support in the engine. - Only the rows the DiT reads are trained. The 2.1 DiT substitutes the reference latents for the context rows under the image slots (4 latent tokens per slot) and drops the system turn, so the loss, the validation scores and the target statistics cover the text rows from position 14 on. Those rows sit after the image in a causal LM, so they are where the image's influence on the conditioning lives. Taking the statistics over the same rows matters with a pre-norm tap: its attention-sink rows would otherwise own the whitening. Teacher rows under the slots are not stored.
- The vision token count follows the render size. The engine resizes a
reference to the render area (nearest, both sides rounded to 32, alpha over
white,
2x − 1), one slot per 32×32 px: 144 tokens at 384² up to 1024 at 1024². Each image sample draws its area fromcorpus.ref_areas([[area_px, weight], …], default 384²/512²/768²/1024² at 0.35/0.35/0.20/0.10, deterministic per sample); two-image samples stay at or belowcorpus.two_image_max_areaeach (default 512², for cost). The engine gives every reference the full render area, so two-reference edits above that size are outside what the adapter saw unless you raise it. Shards run ~0.6 MB per text sample and 3–11 MB per image sample — the default corpus (40k text, 30k image, 2k two-image) is about 170 GB.
The 17.5 GB teacher is resident on a 24 GB+ card; below that it is streamed
through the GPU (teacher_mode: offload, RAM must hold it), which is slow at
1024-token references — lower the weights of the large areas, or the image
counts, for a first run on a small card.
Judge a run by cos_slice (exactly the rows the DiT consumes), split into
cos_t2i (text-only samples) and cos_edit (the text rows of image
samples). A pre-norm tap has a few very large dimensions, so plain cosine
runs high; compare runs by rel_mse as well. No reference run exists yet.
On the ggk side this needs 0.6.6+: Qwen-Image 2.1 support, and for editing the 2.1 conditioner path that takes the references through the mmproj when an adapter with a vision extension is loaded (without one ggk refuses 2.1 edits, because it has no deepstack path). The engine and this pack were checked against each other: reference resize (pixel-index exact), template tokens and image slot positions for one and two references.
The Ming-Image pack
Ming-Image 0.1 Design (and its Design-Layer sibling) conditions a Z-Image-style
DiT on two outputs of its 16B Ling-mini-2.0 MLLM: 256 caption tokens
(learned query tokens appended to the prompt, read back after the thinker and
refined by a 28-layer Qwen2 connector, 2560 wide) and one direct row per
prompt token (hidden states 5 / 12 / 20 of every token — the system prefix,
the input-image block, the text and the suffix — concatenated, RMSNorm +
Linear to the DiT width 3840). The pack (ming_image) exports
pig_ming_adapter-f16.gguf:
ggk diffuser engine -- --diffusion-model ming-image-0.1-design-nvfp4.gguf \
--vae pig_ming_image_vae_bf16.gguf --llm pig_clip-f16.gguf \
--llm-adapter pig_ming_adapter-f16.gguf -p "a sheep in sunglasses" \
--steps 12 --diffusion-fa --offload-to-cpu -o out.png
# editing / layers: add --llm_vision pig_ming_vision-f16.gguf --ref-image ref.png
The Ling tokenizer is not the Qwen BPE, so a token-aligned adapter is
impossible; the ming_image adapter kind is a resampler whose query bank is
the 256 caption rows plus one seeded slot per Ling prompt token: the
engine tokenizes the prompt with the Ling tokenizer (embedded in the adapter
GGUF as tokenizer_json, exactly like the text encoder GGUF), so the DiT
sees the token count and identity it was trained on. The seed is a frozen
PCA of the Ling embedding table (seed_table.pt, rank train.seed_rank,
default 512) through a trained linear, so unseen tokens still get a
meaningful query. The student reads the raw prompt; the input image of an
edit goes through the model's own Qwen2.5-VL tower + linear_proj (2048-d
unit vectors) straight into the adapter — as extra key/value rows and as the
query seed of its own <imagePatch> slots — never into the student. Both
heads train on standardized targets; export folds the statistics into
out_cap / out_proj.
Materials: inclusionAI/Ming-Image-0.1-Design mllm/ (~32 GB bf16, the
thinker + the vision tower + the tokenizer), connector/ (~5.8 GB) and
mlp/ (~120 MB). The Hugging Face repo ships weights only; the teacher is a
forward-only PyTorch port of the reference modeling code (ming_teacher.py),
placed either resident over the CUDA devices (~36 GiB) or streamed from RAM.
The companion pig_ming_vision-f16.gguf (Output › "Also export the paired
vision encoder GGUF", export.export_vision; export.vision_quant q8_0
halves the ~1.3 GB) is written from the same snapshot after the adapter —
text-to-image does not need it. Budget the corpus by teacher time: an image
sample costs the thinker ~610 tokens against ~35 for a text prompt, so the
defaults are 8k image + 16k text samples (94 train shards of 256; the first
30k + 40k default came to 274 shards, over 18 h). Judge a run by
cos_centered (mean of the caption and direct heads) and rel_mse.
Needs ggk 0.7.7+.
The PixArt / T5-XXL pack
PixArt's DiT cross-attends to T5-XXL v1.1 encoder output over a 120-slot
caption window (4096-dim rows; pads carry a -10000 attention bias, so only
the real tokens matter). The pack distills that conditioning into
pig_clip + a seeded resampler: 120 learned queries, each seeded with
the T5 sentencepiece embedding of its slot (adapter.t5_embed.weight),
cross-attend over the student's final-norm states and are projected to
4096. The seed is what lets a fixed query window follow T5's own
segmentation over Qwen BPE states — without it the adapter plateaus barely
above the per-position mean baseline (0.515 vs 0.455 measured).
Contract (verified against the engine at cosine 0.999999 in trainer/):
- student ids = the raw prompt through the Qwen BPE, no special tokens, an
empty prompt = the single pad token (
151643); - seed ids = T5 sentencepiece + EOS, pad-0 to 120 — exactly the sequence
ggk's
PixArtT5Embedderbuilds for its mask, so the ids are free at inference;query_i = query[i] + t5_embed[id_i]; - target =
T5EncoderModel.last_hidden_stateat the real slots; the loss is masked to them: MSE whitened by the per-dim sigma (floored at 0.03 — T5-XXL has ~120 near-constant dims that otherwise carry half the loss as bf16 noise) + 0.5·(1−cos), on RAW targets (no standardization fold at export).
The teacher is one dense 9.5 GB bf16 encoder: teacher_mode gpu keeps it
resident (needs a ~12 GiB budget), offload streams the 24 layers from RAM
through the GPU (a 6 GB card runs it at ~0.25 GiB of VRAM), and the batched
teacher matches the original per-prompt full-window path bit for bit. Only
the real slots are stored (~40 rows × 4096 per prompt, ~0.4 MB).
Export: pig_t5_adapter-f16.gguf with the layout of the shipped file
(adapter.query f32, adapter.t5_embed.weight f16 [32128, width],
explicit q/k/v/o linears, norms and biases f32, KV adapter.t5_vocab).
ggk diffuser engine -- --diffusion-model pixart-nvfp4.gguf \
--vae pig_pixart_vae_fp16-f16.gguf \
--llm pig_clip-q8_0.gguf --llm-adapter pig_t5_adapter-f16.gguf \
-p "close-up portrait of a young lady" --diffusion-fa -s 42 -o out.png
Judge a run by cos over the real slots and rel_mse; cos_eos
isolates the EOS slot (the hardest one), cos_rms is the per-row-normalized
view. The reference run reached val cos 0.827 at 12.5k steps on 117k
prompts; both reference runs flattened around 0.80–0.83 — the 0.6B student's
representation is the ceiling, not data. Use pig_clip at f16 or q8_0 in the
engine: the 4-bit student loses ~0.09 cosine before the adapter even runs.
Requirements
Python ≥ 3.10, PyTorch (CUDA strongly recommended), transformers,
accelerate, safetensors, huggingface_hub, datasets, pyarrow, pillow,
gguf-connector, psutil. Running the LLaDA teacher needs ~34 GB of combined
GPU + CPU memory; the reference run used an RTX 5090 with CPU offload
(4.5 prompts/s, ~3.7 h for 60k prompts). The Qwen3-VL teacher needs ~9 GB
of GPU memory resident, or ~9 GB of RAM plus any CUDA card when streamed;
the T5-XXL teacher likewise (~12 GB resident, or ~10 GB of RAM streamed).
The T5 tokenizer ships as spiece.model only, so sentencepiece and
protobuf must be installed (they are dependencies).
Training the adapter itself fits in a few GB of VRAM.
Set GGUF_TRAINER_MOCK_TEACHER=1 (or tick the checkbox under Precompute) to
run the whole pipeline with synthetic targets — a smoke test of the
machinery, never a usable adapter.
The Qwen3.5 bridge pack (MOSS-Image + FLUX.3 Klein)
Two models lean on Qwen3.5-9B in different roles. MOSS-Image (Base / SFT)
conditions its DiT on the 9B's last hidden state over
<|im_start|>user\n[<|vision_start|><|image_pad|>…<|vision_end|>]{text}<|im_end|>\n<|im_start|>assistant\n<think>\n,
a reference image entering the prompt through the Qwen3.5 vision tower
(ggk: --llm qwen3.5-9b-*.gguf --llm_vision mmproj-qwen3.5-9b-f16.gguf).
FLUX.3 Klein never reads Qwen3.5 states: its prompt upsampler (a Qwen3.5-9B
VLM) writes a long caption from the short request and the references, and
Qwen3-VL-4B encodes that caption (layers 4, 8, …, 32 stacked → 20480 per
token, right-padded to a multiple of 80) for the DiT (ggk:
--prompt-upsampler qwen3.5-9b-*.gguf). The pack (qwen35_bridge) exports
pig_qwen3.5_9b_adapter-f16.gguf, which serves both:
# MOSS-Image: the adapter's slot head replaces the 9B text encoder
ggk --diffusion-model moss-image-sft-nvfp4.gguf --vae pig_flux2_vae_fp32-f16.gguf \
--llm pig_clip-f16.gguf --llm-adapter pig_qwen3.5_9b_adapter-f16.gguf \
--llm_vision mmproj-qwen3.5-9b-f16.gguf --ref-image sheep.png \
-p "a sheep in sunglasses" --sampling-method euler --diffusion-fa -o out.png
# FLUX.3 Klein: the adapter's bank head replaces the upsampler AND the 4B encoder
ggk --diffusion-model flux3-image-klein-nvfp4.gguf --vae pig_flux2_vae_fp32-f16.gguf \
--llm qwen3vl-4b-it-q4_k_m.gguf --prompt-upsampler pig_clip-f16.gguf \
--prompt-upsampler-adapter pig_qwen3.5_9b_adapter-f16.gguf \
--prompt-upsampler-vision mmproj-qwen3.5-9b-f16.gguf --ref-image sheep.png \
-p "a sheep in sunglasses" --cfg-scale 1 --steps 4 --sampling-method euler --diffusion-fa -o out.png
The Qwen3.5 tokenizer (248k tokens) is not the student's Qwen BPE, so the
adapter is a resampler in the Ming style: one seeded slot per Qwen3.5
token of the MOSS template (seed = a frozen PCA of the 9B embedding table,
train.seed_rank, default 384; the engine tokenizes the prompt with the
vocabulary embedded in the adapter GGUF as tokenizer.ggml.tokens/merges)
plus an optional bank of train.bank_slots queries (default 240, a
multiple of 80) for FLUX.3. The student reads the raw prompt; the raw
mmproj embeds (4096-d, the Qwen3.5 tower's main output) enter the adapter
as key/value rows and as the query seed of their own <|image_pad|> slots.
Both heads train on standardized targets, folded into out_proj (MOSS) and
out_bank (Klein) at export. train.klein_target picks what the bank
learns:
prompt(default since 0.0.14): the Qwen3-VL-4B states of the prompt as typed through FLUX.3's template, a deterministic function. The bank is token-aligned — rowjstands for template tokenj, and since the 4B and pig_clip share the Qwen BPE, each prompt row starts from the student state of its own token (bank_in) and the engine rebuilds the template from its own tokenizer. Pads (rope position 1, masked as keys) are one identical row per sample, stored once; the loss, the eval and the engine cover the real rows padded to the next multiple of 80, exactly what the full text encoder would hand the DiT. Prompts longer thanbank_slots - 8tokens are truncated. No captions are generated, so the Klein side of precompute is just the 4B forward.caption: the states of the caption the Qwen3.5 upsampler would write (Black Forest Labs' external-VLM prompts, the firstbank_slotsrows). That is a generator's output: a regression head can only learn its average. A 2026-10 run on 6000 captions reached centred cos 0.59 on the bank but 0.47 against other samples' targets — the bank carried almost no prompt, and "a sheep in sunglasses" rendered a duck. Kept for experiments; the full Qwen3.5 GGUF keeps the exact upsampler path.
Switching the target changes the shard contract, so the shards are rebuilt
and the checkpoints archived. train.klein_head off trains the MOSS adapter
only (no Qwen3-VL-4B material, no captions).
Materials: Qwen/Qwen3.5-9B (~19 GB bf16 with its vision tower; a hybrid
Gated-DeltaNet model — install flash-linear-attention for the fast
kernels, transformers falls back to torch ones otherwise) and, for the Klein
head, Qwen/Qwen3-VL-4B-Instruct (~8.3 GB, text stack only). Image samples
draw a reference area from corpus.ref_areas (0 = the file's own size, up to
1024² = 1024 vision tokens, mirroring the engine's smart-resize cap); a
subset of corpus.n_klein train samples (+ n_val_klein) also gets the
FLUX.3 target (~5 MB per sample with klein_target = prompt, 10 MB with
caption). With caption, the caption goes through Black Forest Labs'
external-VLM prompts (greedy, presence penalty 1.5 for text-to-image, 0 for
editing) before its Qwen3-VL-4B encoding; captions are the slow part
(seconds each through Hugging Face generation) — a running gguf-server
with the Qwen3.5 GGUF + mmproj is faster, point precompute.caption_endpoint
at its /v1. Judge a run by cos_centered_moss (the rows MOSS-Image
consumes) and cos_centered_klein (the bank over the rows the DiT sees;
with prompt expect it near the MOSS number).
The FLUX.3 Klein pack (Qwen3-VL-4B-Instruct, text only)
FLUX.3 Image Klein / Klein Base condition their DiT on Qwen3-VL-4B-Instruct
hidden states of layers 4, 8, …, 32 stacked (8 × 2560 = 20480 per token)
over <|im_start|>user\n{text}<|im_end|>\n<|im_start|>assistant\n,
right-padded to a multiple of 80 tokens; the pads sit at rope position 1,
are masked as attention keys and are attended by the DiT. The text
encoder never sees the reference images (they reach the DiT through the
VAE), and the Qwen3.5-9B prompt upsampler is a separate front end that
rewrites -p before encoding. The pack (qwen3vl_flux3) exports
pig_qwen3vl_4b_it_adapter-f16.gguf, which replaces the 4B as --llm-adapter:
ggk --diffusion-model flux3-image-klein-nvfp4.gguf --vae pig_flux2_vae_fp32-f16.gguf \
--llm pig_clip-f16.gguf --llm-adapter pig_qwen3vl_4b_it_adapter-f16.gguf \
--ref-image sheep.png -p "a sheep in sunglasses" \
--cfg-scale 1 --steps 4 --sampling-method euler --diffusion-fa -o out.png
# with the Qwen3.5 prompt upsampler in front (it rewrites -p, then the adapter encodes the caption)
ggk --diffusion-model flux3-image-klein-nvfp4.gguf --vae pig_flux2_vae_fp32-f16.gguf \
--llm pig_clip-f16.gguf --llm-adapter pig_qwen3vl_4b_it_adapter-f16.gguf \
--prompt-upsampler qwen3.5-9b-q4_k_m.gguf --prompt-upsampler-vision mmproj-qwen3.5-9b-f16.gguf \
--ref-image sheep.png -p "a sheep in sunglasses" \
--cfg-scale 1 --steps 4 --sampling-method euler --diffusion-fa -o out.png
The adapter is token-aligned and layerwise (trainer6's krea2 recipe with
8 taps instead of 12): the 4B and pig_clip share the Qwen BPE, so the
student runs the teacher's exact token sequence — template, prompt, pads
at position 1 masked as keys (ggk 0.9.5+ hands the student those positions)
— and a fused out_proj/skip head maps every row's final-norm state to
the 8-layer stack at the same row. The engine selects the variant by the
f32 adapter.layer_map tensor and refuses a layer count that does not match
the model's. Every pad row of a sample is one identical state on both sides,
so a shard stores the real template rows plus one pad row (~2.6 MB for a
50-token prompt; 30k prompts ≈ 80 GB) and the training batches expand it to
the 80-multiple the engine hands the DiT. Loss: MSE + 0.5·(1−cos) on the
per-dim standardized target (mu/sigma folded into out_proj/skip at
export) with the cosine averaged per tap (the deep taps are ~30× larger
and would own a 20480-d cosine) and a sigma floor relative to each tap's
median; the pads of a sample weigh one row together. Standardized rather
than raw (trainer6 was raw) because FLUX.3 keeps the template prefix rows
and the attention-sink row (user) carries massive activations — ~11000 in
a few dims whose sigma is ~2000 against a median of 1 — that a raw-space
head cannot reach in 20k steps at lr 2e-4. Defaults: width
1024, depth 4, 20k steps, batch 32 chunked by train.batch_tokens (the
20480-d loss tensors are what fill a card, not the adapter).
Corpus: text prompts (the presets). Prompts typed as-is are short, while
the upsampler's captions are 150–250 tokens, and an adapter trained on short
prompts alone does not generalize to them (a smoke adapter scored cos 0.15
on a caption against 0.7–0.98 on the short prompt). The corpus stage can
cover them itself: set corpus.caption_endpoint to a running gguf-server
serving the Qwen3.5 GGUF (gguf-server --model qwen3.5-9b-q4_k_m.gguf --port 8888 --parallel 4 --ctx-size 49152 → http://127.0.0.1:8888/v1;
the upsampler system prompt is ~10k tokens, so each server slot needs ~12k
of context and caption_workers must not exceed the server's --parallel) and
corpus.n_caption / n_val_caption (pack default 4000 / 128): that many
train / val prompts are rewritten with FLUX.3's prompt-upsampler recipe
(Black Forest Labs' external-VLM text-to-image system prompt, greedy,
presence penalty 1.5 — the request ggk's pup.py makes), each caption
joining its split as an extra sample. corpus.caption_workers requests run
in parallel (4; ~2–5 s per caption on a 5090), captions are cached in
data/captions.jsonl as they arrive, a stopped corpus stage resumes where
it left off, and a rebuilt corpus never re-asks the server. Without an
endpoint the counts are ignored (a log line says so); a caption file (one
per line) under "extra files" works too. Teacher: Qwen/Qwen3-VL-4B-Instruct
(~8.3 GB; only its text stack runs — the snapshot downloaded for the
MageFlow-Edit or Qwen3.5 bridge packs is found and linked automatically,
also from sibling project folders). Judge a run by cos (per-tap cosine
over the rows the DiT sees, expect ~0.9 like trainer6) and cos_tokens
(the real template rows); cos_dit counts every pad row, cos_full is the
inflated plain 20480-d cosine.
The Fibo 2.0 pack (Qwen3-VL-4B-Instruct + mmproj)
Fibo 2.0 Dev / Edit Turbo (briaai) condition their DiT on six 7680-wide
text bundles, each three Qwen3-VL-4B-Instruct hidden layers RMS-normalized
per token — the fifteen taps {5, 7, 9, 11, 13, 16, 18, 20, 22, 24, 27, 29,
31, 33, 35} over <|im_start|>user\n{body}<|im_end|>\n<|im_start|>assistant\n,
where the body is the prompt as typed (or Bria's structured JSON caption;
the null caption when empty) and, for an edit, the JSON
{"image": <|vision_start|>…<|vision_end|>, "edit_instruction": prompt}
with the reference image inside the user turn through the Qwen3-VL vision
tower (deepstack + M-RoPE on the full 4B). Nothing is padded and every row
is consumed: the DiT's Perceiver squeezes the first bundle into gist tokens
and five injection blocks cross-attend to theirs. The pack (qwen3vl_fibo2)
exports pig_qwen3vl_fibo2_adapter-f16.gguf:
ggk --diffusion-model fibo2-edit-turbo-q3_k_m.gguf --vae pig_flux2_vae_fp32-f16.gguf \
--llm pig_clip-f16.gguf --llm-adapter pig_qwen3vl_fibo2_adapter-f16.gguf \
--llm_vision mmproj-qwen3vl-4b-it-f16.gguf --ref-image sheep.png -p "a sheep in sunglasses" \
--cfg-scale 1 --steps 4 --sampling-method euler --diffusion-fa -o out.png
The adapter is the FLUX.3 pack's layerwise head (15 taps instead of 8,
standardized targets, per-tap cosine, relative per-tap sigma floor,
mu/sigma folded into out_proj/skip at export) on the MageFlow pack's
vision-ext trunk (frozen vision_proj applied by the engine before the
student, trained vis_in handing the adapter the raw mmproj embeds). The
engine RMS-normalizes each tap per token before bundling, so the adapter
emits the raw stack and only each row's per-tap direction matters — judge
a run by cos (the per-tap standardized cosine the loss optimizes) and
cos_dit (the per-tap cosine after that normalization, exactly what the
bundles contain), split into vision slots | text rows and image | text-only
samples. A final-norm adapter (the MageFlow pig_qwen3vl_4b_adapter) cannot
serve this model: the engine refuses it, naming this pack.
Corpus: (image, instruction) samples plus text-only prompts, the MageFlow
mix with smaller defaults (4.5k image, 200 two-image, 10k text) because every
row stores 38400 features. The stack is stored as fp8 (e4m3 with one
scale per row and tap, half of bf16 at a cosine of 0.9998 to the bf16 row —
precompute.fp8_rows, decoded on the device): ~38 KB per token, so a 512²
reference costs ~13 MB, a 768² one ~28 MB, a text prompt ~2.5 MB — about
130 GB at the defaults, roughly 1.5 hours of precompute (~35 s per shard of
96: the reference preprocessing on the CPU, threaded, then the GPU passes)
and the training on a 5090 with the teacher resident. The training batches are
length-bucketed (train.bucket_batches, on for every pack): rows of
similar length share a batch, so a batch is padded by a few percent instead
of the ~70% a random draw of 60-row prompts and 600-row references gave,
and train.batch_tokens (24576 here) is sized so a backward chunk holds
~8k target rows rather than one sample — the first real run with 4096 and
random batches took 2.4 s per step (12.5 h for 20k steps). Each image sample draws its
render area from corpus.ref_areas (512² 0.5, 768² 0.25, 1024² 0.25; the
last two both give 576 slots): the engine scales a single reference to cover the output size
and center crops it, then shows Qwen3-VL at most 768 px a side, so the
adapter sees 256-slot and 576-slot references; two-image samples stay at or
below corpus.two_image_max_area each. The reference preprocessing (Lanczos
cover + crop, antialiased bicubic thumbnail and 32-grid, 8-bit rounding) is
the engine's own resampler reproduced in numpy. Teacher:
Qwen/Qwen3-VL-4B-Instruct with its vision tower (~8.3 GB; the snapshot of
the MageFlow / FLUX.3 packs is found and linked automatically), resident on a
12 GB+ card, else streamed. Defaults: width 1024, depth 4, 20k steps, batch
32 chunked by train.batch_tokens 4096. Needs ggk 0.9.7+.
Adding a pack
Subclass gguf_trainer.packs.base.TrainerPack: declare the adapter kind
(resampler, seeded_resampler, token_aligned_vision, token_aligned_layerwise, token_aligned_layerwise_vision, ming_image or qwen35_bridge), the materials (model or dataset
snapshots, local files), the pack's config defaults and GUI hints, the
prompt template / build_teacher() / build_mock_teacher(), and the
export key/values, then register it in gguf_trainer/packs/__init__.py.
packs/llada_image.py, packs/qwen3vl_mageflow.py,
packs/qwen3vl_qwenimage.py, packs/t5xxl_pixart.py, packs/ming_image.py, packs/qwen35_bridge.py and
packs/qwen3vl_flux3.py and packs/qwen3vl_fibo2.py are the references.
A vision pack whose engine formats or preprocesses differently supplies its
own vision_data.VisionContract from contract() (template, reference
preprocessing, start index, which rows are supervised); the shared stages
talk only to that object.
Metadata
Release files for gguf-trainer 0.0.15
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| gguf_trainer-0.0.15.tar.gz | 276.1 kB | Without provenance | Details for gguf_trainer-0.0.15.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| gguf_trainer-0.0.15-py3-none-any.whl | Python 3 | none | any | Details for gguf_trainer-0.0.15-py3-none-any.whl |
Total release size: 566.2 kB
Release files / gguf_trainer-0.0.15.tar.gz
| Download URL | gguf_trainer-0.0.15.tar.gz |
|---|---|
| Size | 276.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7084b69d71449d6d5abcdf3205a3809cf005af8d1a4c6a6357533a0a8ef8b99b
|
|
BLAKE2b-256 checksum How to use checksums |
774ede21671716ee28d2c0ff2405bafc060a5d1f0bcc6864dcec942f2c8d73ec
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.9
|
Release files / gguf_trainer-0.0.15-py3-none-any.whl
| Download URL | gguf_trainer-0.0.15-py3-none-any.whl |
|---|---|
| Size | 290.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dc25e091f9cbebcd6dc7a41f636a2b5cd7ead5b38d94c98302c00409c9f3c518
|
|
BLAKE2b-256 checksum How to use checksums |
6eb9614cee17af76120d83911153191ff643d69fa8cec0ff7c65a34c38d1f85b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.9
|
Security for gguf-trainer 0.0.15
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.