GGUF vision towers (mmproj): survey and plan
Los mantenedores suelen responder en 1 día
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Aptitud para principiantes
- 35/100
Línea de trabajo
Start with the completed shared mmproj work and family implementations in #803, then inspect the model architecture mappings referenced here, such as #721 for GLM-5.3-Flash and #725 for MiniMax-M3. Pick one unchecked family and verify its listed tensor transforms, defaults, and image processor behavior against the payload’s findings. Done means that family loads image input from its separate mmproj and its checks pass; the issue does not name test files.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Sub-issue of #671 (GGUF → Vision towers). It records what we found about loading llama.cpp vision towers (mmproj) for the families FreeToken serves images for. Checked against public main 546c5fe and llama.cpp master 339f3242c.
Where it stands
- #803 (merged): image input from an
mmprojfor Qwen3.5 / 3.6 / 3.8 and Gemma-4 (26B-A4B, 31B, 12B unified); the behavior is under Behavior. - On the same base, all merged: GLM-5.3-Flash (
glm5v) #810, DeepSeek-V4-Flash-Vision-Exp (deepseek4v) #816, Qwen3-VL (qwen3vl_mergerwith DeepStack) #827, MiniMax-M3 (minimax_m3) #829, Muse-Glimmer (muse-glimmer, images only) #830. - Every family that has GGUF text support and a llama.cpp vision tower now takes image input. DeepSeek-V4.1 has no GGUF text support.
- llama.cpp always writes the vision tower to a separate file,
mmproj-*.gguf:general.architecture = clip,general.type = mmproj.- Tensors are named
v.*/mm.*; metadata sits underclip.vision.*. - The projector type is in
clip.projector_typeorclip.vision.projector_type.
- llama.cpp loads it with
--mmproj FILE. When it downloads a model with-hf, it also picks the sibling*mmproj*.ggufwhose quant bits are closest to the text model. - Old Ollama GGUFs put
v.*/mm.*inside the text file. Ollama is migrating them to split files (compatmigrate). None of the HF repos we checked ship that layout.
Per family
Checked by comparing a BF16 mmproj with the HF vision weights after undoing llama.cpp's transforms (CPU only).
| Family | Text GGUF | projector_type |
Tensor transforms | Fixed defaults missing from the mmproj | Weights vs HF |
|---|---|---|---|---|---|
| Qwen3.5 / 3.6 | qwen35, qwen35moe (#701) |
qwen3vl_merger |
stack v.patch_embd.weight + .weight.1 into the Conv3d; qkv is already fused |
min/max pixels 65536 / 16777216 | 333/333 bit-exact (Qwen3.6-27B); served in #803 |
| Qwen3.8-Flash-Next | qwen4exp (#690) |
qwen3vl_merger (no DeepStack, as Qwen3.5) |
same as Qwen3.5 | same as Qwen3.5 | served in #803 |
| Qwen3-VL | qwen3vl, qwen3vlmoe (#807) |
qwen3vl_merger with DeepStack layers |
as Qwen3.5, plus the DeepStack mergers | same as Qwen3.5 | 351/351 (Qwen3-VL-8B). Qwen3.5 and later refuse a DeepStack mmproj; served in #827 |
| Gemma-4 26B-A4B / 31B | gemma4 (#690, #706) |
gemma4v |
patch embedding CHW → HWC | gelu_tanh, pooling 3, vision rope theta 100, max_soft_tokens 280; clip.vision.image_size = 224 is a placeholder |
356/356 (26B-A4B, 31B); served in #803 |
| Gemma-4 12B (unified) | gemma4, the same arch as 26B / 31B; the mmproj tells them apart |
gemma4uv (+ gemma4ua audio, not loaded) |
as gemma4v, plus the position-embedding permute |
as gemma4v |
10/10; served in #803 |
| GLM-5.3-Flash | glm5-next (#721) |
glm5v |
stack the patch embedding halves; qkv is already fused | none (min/max pixels are in the metadata) | 347/347; served in #810 |
| MiniMax-M3 | minimax-m3 (#725) |
minimax_m3 |
stack the patch embedding halves; undo llama.cpp's per-head q/k permute (head_dim 80) | vision rope theta 10000; min/max pixels 3136 / 451584 | 395/395; served in #829 |
| Muse-Glimmer | muse-glimmer (#724) |
muse-glimmer |
undo the q/k rope permute. llama.cpp sums the two temporal slabs of the patch embedding, which cannot be undone, so video cannot be supported. Images are unaffected: #830 writes the f32 sum as bf16 hi + lo into the two slabs (exact), tower output 1e-6 .. 2e-5 relative to HF | layer_types rule, temporal patch 2, gelu, vision rope theta 10000, max_image_tokens 4096 | 808/809 (the patch embedding is the one that differs); served in #830 |
| DeepSeek-V4-Flash-Vision-Exp | deepseek4 (#774) |
deepseek4v |
cat(gate, up); cat(q, k, v) for unsloth's file (ggml-org's is already fused) |
vision rope theta 10000, max_n_token 384, max_wh_ratio 8 | 267/267; served in #816 |
| DeepSeek-V4.1 | no text GGUF support (llama.cpp has no converter) | deepseek41v (unsloth) |
n/a | n/a | not checked |
Notes:
- Where mmprojs are published:
- unsloth, ggml-org, bartowski and lmstudio publish one for every family above except MiniMax-M3.
- For MiniMax-M3, 9 of 26 GGUF repos ship one (bartowski, AesSedai, ...); unsloth does not.
- Weight types:
- Most mmprojs are BF16, F16 or F32.
- ggml-org also publishes Q8_0.
- lmstudio's Muse-Glimmer mmproj is Q4_K_M.
- Image token ids: the mmproj does not store image / boi / eoi token ids. They come from the text GGUF's tokenizer, looked up by token string.
- DeepSeek-V4 specifics:
- The text GGUF has no image tokens. The img_start / pad / end embeddings live in the mmproj.
- ggml-org's chat template renders no image placeholder (unsloth's does), so image requests on GGUFs that carry it fail (see Open).
- Image processors: for Qwen, Gemma-4, GLM-5.3, MiniMax-M3 (20 images) and Muse-Glimmer (19 images, two budgets), an image processor built from the mmproj metadata plus the defaults above gives the same output as
AutoImageProcessoron the HF release. DeepSeek-V4 has its own preprocessing.
Precision
Step 1 does not quantize the vision tower. Every mmproj is dequantized to bf16 at load.
| mmproj type | Result in bf16 |
|---|---|
| BF16 / F32 | exact |
| F16 | loses f16 subnormals only (|x| ≤ 7.6e-6, max error 3e-8) |
| Q8_0 | the loss is already in the file. Per-tensor cos ≥ 0.9999, but on GLM-5.3 the worst-token cos of the tower output drops from 0.74 to 0.50 |
Why not quantize:
- Every FreeToken tower is plain bf16. Qwen's tower takes
config.quant, butGGUFConfigmaps novisual.*names. - The FP8 / NVFP4 / MXFP8 HF releases we checked all keep vision in bf16.
- The towers are 0.1–3.7 GB, and host placement already covers memory pressure.
- vLLM and SGLang:
- Neither has a feature that quantizes ViT weights on its own initiative; the tower follows the checkpoint.
- SGLang hardcodes
quant_config=Nonefor the Qwen3-VL / Qwen3.5 and MiniMax-M3 towers. - vLLM's GGUF plugin handles mmproj for Gemma3 only, and runs it at the file's types.
Behavior
As shipped in #803; the families added later (#810, #816, #827, #829, #830) follow it.
- Only separate llama.cpp
mmprojfiles are read. A text GGUF that carriesv.*/mm.*tensors itself (old Ollama layout) serves text, with a warning that those tensors are not read. --mmproj FILEpicks the file. It must be a llama.cpp vision mmproj and needs a.ggufmodel. A file the model cannot use (another tower type, another output width, image tokens missing from the text tokenizer) stops startup with an error.- Without
--mmproj, FreeToken looks for*mmproj*.ggufin the model file's own folder only, as llama-server does for a--models-direntry. It does not look in parent folders, so a repo that keeps each quant in a subfolder and the mmproj at the top needs--mmproj(17 of the 200 most downloaded Qwen GGUF repos).- It skips a file that cannot be read, whose output width differs from the text model, or that fails the family's checks. Unreadable files and failed checks are logged; a width mismatch is not (see Open).
- When the files belong to several models, it takes the one named like the text model. If none matches, the model serves text, with a warning.
- Among the rest it prefers BF16, then F32, then F16, then quantized files. llama.cpp
-hfpicks the file whose quant bits are closest to the text model instead.
- No usable mmproj: the model serves text and rejects image requests.
--mm-disable visionand--text-model-onlyleave the mmproj unloaded the same way. - Families without GGUF image input do not look for an mmproj, and
--mmprojhas no effect on them. - Log: one line per process names the mmproj, how it was found, its projector type, its file type -> bf16, and its size. Another line lists the files not used. A quantized file gets a warning. Without an mmproj, the log says vision is disabled and why.
- Precision: the tower runs in bf16. A quantized mmproj is dequantized at load and keeps the quantization error of the file.
- Config: comes from the GGUF files alone, with no HF repo. The mmproj metadata, the text GGUF tokenizer (image token ids) and per-projector defaults supply the values. Image tokens, budgets,
--image-min-tokens/--image-max-tokensand--mm-processor-kwargswork as for the HF checkpoint of the same model. - FTW: an FTW converted from a GGUF serves text only.
Open
- FTW converted from a GGUF keeps no vision tower.
- Discovery drops a folder mmproj without a log line when its output width differs from the text model, or when the several-models name rule filters it out (unsloth Muse-Glimmer's
mmproj-kquant.ggufis missing from the "not used" line). - DeepSeek-V4 GGUFs whose chat template renders no image placeholder (ggml-org, vultr, prometheusAIR, huihui-ai and others: 25 of the 35 files checked with a tower in #816; unsloth's template maps image parts to
<|deepseek_image|>). FreeToken passes the message content to the template as a list of parts; these templates print it as text, so the request fails with "prompt renders 0 image placeholders" (a 400, or an error chunk when streaming). llama.cpp turns image parts into<__media__>markers first, and vLLM flattens the content for string-content templates.
Work items
- Shared: mmproj discovery, logging, GGUF config with the vision sections, image processor (#803)
- Qwen3.5 / 3.6 / 3.8 (
qwen3vl_merger) (#803) - GLM-5.3-Flash (
glm5v): #810 - Gemma-4 26B-A4B / 31B (
gemma4v), 12B unified (gemma4uv, vision only) (#803) - MiniMax-M3 (
minimax_m3) (#829) - Muse-Glimmer (
muse-glimmer, images only) (#830) - DeepSeek-V4-Flash-Vision-Exp (
deepseek4v) (#816) - Qwen3-VL (
qwen3vl_mergerwith DeepStack; text archs in #807) (#827)
- Lenguaje dominante
- Python
- Estrellas
- 14.2k
- Forks
- 1.4k
- Merge medio
- 21 h 53 min
- PR fusionados (30 d)
- 100
Preparar el entorno
Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de FlashML-org/FreeToken
-
Desktop feature
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
FlashML-org/FreeToken#800 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
FlashML-org/FreeToken#767 ·
Los mantenedores suelen responder en 1 día
-
test_gemv_path_matches_the_transcription fails on sm_120 (bf16 rounding tie exceeds the tolerance)Posiblemente ocupada @void-mckenzie la tomó hace 17 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 77/100
FlashML-org/FreeToken#649 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
FlashML-org/FreeToken#648 · 1 comentario ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
FlashML-org/FreeToken#646 · 1 comentario ·
Los mantenedores suelen responder en 1 día
Todos los issues de FlashML-org/FreeToken
Issues similares
-
getchaintips leaves out the active tip once a header is staged on it, where Core always lists itAbierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
btclib-org/btclib-node#2158 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 85/100
dbt-msft/dbt-sqlserver#897 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
Gathin23/cleanyourtv#2770 · 2 comentarios ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
vllm-project/vllm#64845 ·
Los mantenedores suelen responder en 1 día
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
dekobon/big-code-analysis#1729 ·
Los mantenedores suelen responder en 1 día