Skip to content

feat(gguf): serve qwen3vl and qwen3vlmoe gguf checkpoints text only - #807

Merged
jason-fxz merged 1 commit into
FlashML-org:mainfrom
void-mckenzie:feat/gguf-qwen3vl-text
Oct 23, 2026
Merged

jason-fxz merged 1 commit into
FlashML-org:mainfrom
void-mckenzie:feat/gguf-qwen3vl-text

Conversation

@void-mckenzie

@void-mckenzie void-mckenzie commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Serves the text trunks of Qwen3-VL (qwen3vl, dense) and Qwen3-VL-MoE (qwen3vlmoe)
GGUFs: a qwen3_vl/gguf.py adapter through the family's own parse_config, registry
rows, arch-map and AOT alias entries. Text only, TP 1; vision/mmproj stays out.
Text rope is plain NeoX: llama.cpp's IMROPE keeps full-spectrum theta (ggml.h), so
equal (t, h, w) text positions reduce to standard rope and q/k carry no permute.
Experts stack as ffn_*_exps banks like qwen3moe. Part of #671.

Also fixes a latent GGUF-tokenizer bug on main for the whole Qwen lineage
(pre qwen2 / qwen35, the families of #707 and #701): every Qwen HF
tokenizer.json applies NFC (Unicode Normalization Form C) before the byte-level
BPE, but the GGUF carries no normalizer, so decomposed text encoded differently
from HF.

tok_compare (vs the model's own tokenizer.json)

file pre edge vocab combos
unsloth Qwen3-VL-4B-Thinking UD-Q4_K_XL qwen2 0/13 0/150195 0/20000
unsloth Qwen3-VL-30B-A3B-Thinking UD-Q3_K_XL qwen2 0/13 0/150195 0/20000
unsloth Qwen3-30B-A3B UD-Q6_K_XL (qwen3moe) qwen2 0/13 0/150195 0/20000
Qwen3.6-35B-A3B UD-Q6_K_XL qwen35 0/13 0/247100 0/20000
Qwen3.8-27B UD-Q6_K_XL qwen35 0/13 0/247100 0/20000

Before the NFC fix these were 1/248/114 (Qwen3-VL-4B) and 1/0/0 (Qwen3.6); the
NFC-sensitive strings (combining accents, CJK compat ideographs) mismatch HF without
the normalizer on both lineages.

gguf_check sweep

fetch_headers --search Qwen3-VL (19,823 shards), check.py over the stubs:
519/565 files of served architectures pass.

arch pass
qwen3vl 315/341
qwen3vlmoe 169/188
qwen3 (bundled text files) 35/36
failure class n what it is
IQ1_M 26 no MMQ kernel above 8 tokens; standing refusal
nif0 "heretic H3" L0-49 11 broken uploads: 50 blocks + token_embd, no norm/head
Thireus custom quants 6 ik_llama.cpp types (IQ1_KT, IQ2_KT, IQ2_K_R4, IQ6_K)
Thireus split sets 3 sibling shard stubs not fetched

Not covered

item why
vision / mmproj / image input out of scope; #803's tower loader refuses Qwen3-VL DeepStack mmprojs, and wiring encoders into these rows is follow-up
qwen2vl GGUFs (4 files in the sweep) different architecture
Qwen3-ASR GGUFs share qwen3vl but need the audio tower
NVFP4 token_embd, TQ1_0/TQ2_0, MTP-only drafts standing ignores

Real-file serving

RTX 5090 32 GB (driver 580.95.05), Ryzen 9 7950X3D, 187 GB RAM, torch 2.14.1+cu130,
triton 3.8.0, transformers 5.18.0, python 3.10.19.

model mode prefill decode
4B UD-Q4_K_XL resident 13.8k tok/s 353 tok/s
30B-A3B UD-Q3_K_XL auto -> offload, 11.9 GiB experts in host RAM ~10.5k tok/s 299-316 tok/s
ft serve --model-path <gguf> --host 127.0.0.1 --port 8301

Both answer coherently; thinking parses into reasoning_content with the auto qwen3
reasoning parser. The log's instantaneous "input throughput" swings with its logging
window, so prefill above is from request wall time and per-chunk log lines.

Tests

No per-family GGUF test file, per the #721 review direction; the live-registry
tests/models/test_models_registry.py covers the new rows and passes.

@void-mckenzie
void-mckenzie marked this pull request as draft October 22, 2026 23:56
@void-mckenzie
void-mckenzie force-pushed the feat/gguf-qwen3vl-text branch from 57417ec to 66bc73d Compare October 22, 2026 23:58
@void-mckenzie

Copy link
Copy Markdown
Contributor Author

@jason-fxz: llama.cpp has the same gap (its converter never writes tokenizer.ggml.normalizer.nfc, so it also tokenizes decomposed text differently from HF) , do you think it's fine to fix it here against the HF reference, or keep llama.cpp workflow?

@jason-fxz

Copy link
Copy Markdown
Collaborator

Align with HF, so keep the NFC change in this PR. The model is trained on NFC-normalized text, and tokenizer.json is the reference. llama.cpp skips NFC in its BPE path, but not on purpose: since ggml-org/llama.cpp#30540, gguf-py writes tokenizer.ggml.normalizer.nfc when tokenizer.json has NFC, and only the Unigram (UGM) tokenizer applies it. I checked Qwen3.6-35B-A3B Q4_K_M against its tokenizer.json on decomposed input: main differs on 5/5 cases, and your change matches on 5/5.

LGTM on the rest. Checked on real files: unsloth Qwen3-VL-8B BF16 and 30B-A3B BF16 are bit-exact against the HF safetensors (8B: all 291 tensors; 30B: 339 dense tensors plus experts in layers 0/24/47), and the parsed config matches HF. Served end to end next to the HF checkpoints (8B/30B, BF16 and Qwen's Q4_K_M): prompt token counts are identical on every request, including an NFC-sensitive prompt and an 8k-token recall, and the answers match.

@jason-fxz jason-fxz added the gguf label Oct 23, 2026
@jason-fxz
jason-fxz merged commit ed359ee into FlashML-org:main Oct 23, 2026
1 check passed
KarrAcaRn pushed a commit to KarrAcaRn/FreeToken-ByAI that referenced this pull request Oct 23, 2026
Upstream up to 507a8bf (FlashML-org#641, FlashML-org#811 cache flush, FlashML-org#801, FlashML-org#807, FlashML-org#804, FlashML-org#665, FlashML-org#803, FlashML-org#774).
Conflicts resolved:
- gguf/config.py: keep our rope --hf-overrides next to upstream's mmproj hf_fields
- scheduler/cache.py: take upstream's _evict() helper, keep freeing the host bank slots
- scheduler/scheduler.py: block/idle checks wait for both the kv ladder and a pending flush
- glm_moe_dsa, minimax_m2 moe.py: keep our fp32-bias comments
- api_server.py: import Query and Headers
- tests/moe/test_fused_moe.py: keep our moe_align tests and upstream's sigmoid top-k test
- tests/scheduler/test_cache_rebuild.py: the flush test's busy marker is a 1-token batch,
  next's overlap loop reads last_data[1] for the DFlash drain

Assisted-by: Claude Opus 5.5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants