|
Download README.md from gatilin/llama.cpp-youtu-moe: direct link, hf CLI and curl.
- Browser
- Download file 10.8 kB
-
https://huggingface.co/gatilin/llama.cpp-youtu-moe/resolve/main/README.md
- Command line
-
hf download hf://gatilin/llama.cpp-youtu-moe/README.md
-
curl -L -o README.md https://huggingface.co/gatilin/llama.cpp-youtu-moe/resolve/main/README.md
10.8 kB
| license: other | |
| license_name: youtu-llm | |
| base_model: tencent/Youtu-LLM-A7B-Base | |
| tags: | |
| - llama.cpp | |
| - gguf | |
| - patch | |
| # llama.cpp support for Youtu-LLM-A7B-Base | |
| Patch that adds `YoutuVLMoEForConditionalGeneration` (`youtu-moe` arch) to | |
| [llama.cpp](https://github.com/ggml-org/llama.cpp) — text **and** vision. | |
| | Item | Value | | |
| |---|---| | |
| | Upstream model | [`tencent/Youtu-LLM-A7B-Base`](https://huggingface.co/tencent/Youtu-LLM-A7B-Base) @ `6b0d02b2912c2975806db5a4922a1e091fa2f5d3` | | |
| | llama.cpp base commit | `e0f142d5ddc1461ce721644d92af33db23545105` | | |
| | GGUF weights | [`gatilin/Youtu-LLM-A7B-Base-GGUF`](https://huggingface.co/gatilin/Youtu-LLM-A7B-Base-GGUF) | | |
| | Status | text + vision verified end-to-end on real 120B weights (macOS / Metal) | | |
| **中文摘要**:本仓库提供一个 llama.cpp 补丁,为 Youtu-LLM-A7B-Base 增加 | |
| `youtu-moe` 架构支持(KDA 线性注意力 + GatedMLA 混合、256 专家 QB 路由 MoE、 | |
| Qwen3-VL 视觉塔)。已验证文本与图文推理,可导出 mmproj、可量化。下面第 6 节记录了 | |
| 几个踩过的坑(RoPE 布局、mrope sections、模态感知专家偏置、`</think>` 残留)。 | |
| --- | |
| ## 1. Quick start | |
| ```bash | |
| git clone https://github.com/ggml-org/llama.cpp | |
| cd llama.cpp | |
| git checkout e0f142d5ddc1461ce721644d92af33db23545105 | |
| git apply ../llama.cpp-youtu-moe.patch | |
| cmake -B build -DGGML_METAL=ON -DLLAMA_BUILD_SERVER=ON -DCMAKE_BUILD_TYPE=Release | |
| cmake --build build -j --target llama-cli llama-server llama-mtmd-cli llama-quantize | |
| ``` | |
| > On macOS, `cmake` / `ninja` are often only inside a Python venv (`pip install cmake ninja`). | |
| Text inference: | |
| ```bash | |
| ./build/bin/llama-cli -m youtu-moe-a7b-Q4_K_M.gguf --jinja \ | |
| -p "用一句话解释什么是光合作用。" -st --simple-io -ngl 99 -n 128 | |
| ``` | |
| Image inference: | |
| ```bash | |
| ./build/bin/llama-mtmd-cli -m youtu-moe-a7b-Q4_K_M.gguf \ | |
| --mmproj youtu-vl-mmproj-f16.gguf --image test.jpeg \ | |
| -p "<|vision_start|><__media__><|vision_end|>描述这张图片的内容。" -n 200 -ngl 99 | |
| ``` | |
| The `<|vision_start|>…<|vision_end|>` wrapper is **required**. `chat_template.jinja` | |
| in the upstream repo has no image branch, so `--jinja` cannot inject the image | |
| placeholder — feed the raw prompt instead. `<__media__>` is mtmd's media marker; | |
| do **not** also write `<|image_pad|>`. | |
| ## 2. What's in the patch | |
| | File | Change | | |
| |---|---| | |
| | `src/models/youtu-moe.cpp` | **new** — the arch: graph, KDA layer, MLA layer, QB-MoE, modality input | | |
| | `src/models/models.h` | **new** — `llama_model_youtu_moe` class + graph members | | |
| | `conversion/youtu_moe.py` | **new** — HF → GGUF converter (`YoutuVLMoEForConditionalGeneration`) | | |
| | `conversion/youtu_vl_vision.py` | **new** — visual tower key mapping for `--mmproj` | | |
| | `src/llama-arch.cpp` / `.h` | register `LLM_ARCH_YOUTU_MOE` and its metadata keys | | |
| | `src/llama-model.cpp` | register the arch; route it to the IMROPE group | | |
| | `src/llama-graph.cpp` | `build_inp_mem_hybrid_k()` helper (KDA + MLA hybrid KV cache) | | |
| | `src/llama-context.cpp` | hybrid recurrent state bookkeeping | | |
| | `gguf-py/gguf/constants.py` | tensor/arch/metadata key names | | |
| | `conversion/__init__.py` | register the converter module | | |
| ## 3. Conversion | |
| ```bash | |
| # put the 24 shards under models/Youtu-LLM-A7B-Base/ first | |
| bash convert_youtu.sh # 7 steps, idempotent | |
| ``` | |
| `convert_youtu.sh` runs: weight integrity check → export mmproj → BF16 GGUF → | |
| metadata self-check → Q4_K_M quantize → text smoke test → vision smoke test. | |
| Two flags matter: | |
| * `--use-temp-file` is **mandatory** for BF16. Without it the converter keeps every | |
| tensor in RAM and a 220 GiB model OOMs on a 128 GiB machine. | |
| * Quantize is done **from the BF16 GGUF**, not from safetensors. | |
| Produced artifacts: | |
| | File | Size | | |
| |---|---| | |
| | `youtu-moe-a7b-bf16.gguf` | 220 GiB (1090 tensors) | | |
| | `youtu-moe-a7b-Q4_K_M.gguf` | 66 GiB | | |
| | `youtu-vl-mmproj-f16.gguf` | 867 MiB | | |
| ## 4. Architecture notes | |
| Youtu-LLM-A7B: 48 layers, hidden 3072, ~120B total / ~7B active, vocab 129280, | |
| context 262144, 262144-token training window over 11.5T multimodal tokens. | |
| **Hybrid attention.** 3 KDA (linear attention) layers followed by 1 GatedMLA layer, | |
| repeated 12 times. KDA reuses llama.cpp's gated-delta-net recurrent path | |
| (`llm_build_delta_net_base`, `build_recurrent_attn`, conv state + ssm state, | |
| L2-normalised q/k, gated lower bound −5.0). MLA is the DeepSeek-style low-rank | |
| path with `q_lora_rank=1024`, `kv_lora_rank=512`, `qk_nope_head_dim=128`, | |
| `qk_rope_head_dim=64`, `v_head_dim=128`, plus a **head-wise output gate** | |
| (`[32, 3072]`) applied after the attention output projection. | |
| **MoE.** 256 routed experts, top-8, SwiGLU, width 1024, plus 1 shared expert. | |
| Routing is *quantile-balancing* with a **modality-aware expert bias** | |
| (`qb_beta`, shape `[2, 256]`: row 0 = text, row 1 = image/video): | |
| ``` | |
| selection = router_logits - qb_beta[modality] | |
| top8 = argmax8(selection) | |
| weights = sigmoid(router_logits[top8]) / (sum + 1e-20) * 2.83 | |
| ``` | |
| Layer 0 is a dense FFN (12288). | |
| **Vision.** Qwen3-VL-MoE-style ViT + merger (`clip.projector_type=qwen3vl_merger`), | |
| exported as a separate mmproj. | |
| ## 5. Gotchas that cost real debugging time | |
| ### 5.1 RoPE layout: ggml IMROPE is NEOX, the official model is not | |
| `ggml`'s `MROPE` / `IMROPE` **force NEOX rotation** (first half paired with second | |
| half), while the official MLA implementation does `cat(q[0::2], q[1::2])` + | |
| `rotate_half` — i.e. **adjacent pairs**. The two must be reconciled by | |
| **permuting the weights at conversion time**: | |
| ```python | |
| # wq_b: per-head [q_nope(128) | q_pe(64)] | |
| q = w.reshape(32, 128 + 64, -1) | |
| pe = q[:, 128:] | |
| w = torch.cat([q[:, :128], pe[:, 0::2], pe[:, 1::2]], dim=1).reshape(32 * 192, -1) | |
| # wkv_a_proj_with_mqa: [kv_lora(512) | k_pe(64)] | |
| pe = w[512:] | |
| w = torch.cat([w[:512], pe[0::2], pe[1::2]], dim=0) | |
| ``` | |
| Verified numerically: after the permutation the per-element error vs. the official | |
| implementation is **1e-16**; without it the relative error is **1.33** (garbage). | |
| ### 5.2 mrope sections must be `[11, 10, 10, 0]`, not `[12, 10, 10]` | |
| The official config says `mrope_section=[12, 10, 10]` (sum = 32 = `qk_rope_head_dim/2`). | |
| But ggml computes `sector = (freq_index / 2) % sum(sections)`. With `sum = 32`, | |
| `freq 31` lands in the **extra** slot and reads `pos[3]` — which is always 0 for text | |
| tokens, silently corrupting one frequency. Setting `sections[0] = 11` (sum = 31) maps | |
| `freq 31 → sector 0 → T` and leaves every other frequency unchanged, matching the | |
| official `_apply_interleaved_mrope` frequency-by-frequency (verified by exhaustive check). | |
| ### 5.3 Modality routing is decided by "is this row an embedding", not by token id | |
| mtmd injects image embeddings via `llama_batch_ext_add_embd`, so for a pure-vision | |
| batch `ubatch->token == nullptr` while `ubatch->type` / embeddings are set. The | |
| graph input `llm_graph_input_youtu_modality` therefore emits `1` for every token of | |
| an embedding-only batch, `1` where `ubatch->type != 0` otherwise, and `0` for plain | |
| text. Two rows of `qb_beta` differ substantially (mean |Δ| = 0.387, max 2.63, | |
| 100 % non-zero), so getting this wrong visibly changes expert selection. | |
| Also: when the last layer trims `inpL` with `inp_out_ids`, the modality ids must be | |
| trimmed with the same `ggml_get_rows(...)` call, or the shapes stop matching. | |
| ### 5.4 The model always emits a leading `</think>` | |
| This is a **base** model — the upstream README says raw text continuation is the | |
| recommended mode and `chat_template.jinja` is only provided "for further purpose". | |
| It nonetheless emits `</think>` (token **128014**) as its very first assistant-side | |
| token, closing the think block the template pre-opened: | |
| ``` | |
| prompt: <|Assistant|><think> <- template | |
| model: </think>光合作用是… <- generated | |
| ``` | |
| It does this even with a bare `<|User|>…<|Assistant|>` prompt that contains no | |
| think markers at all, and even if the prompt already ends with `</think>`. | |
| `</think>` is token type **USER_DEFINED (4)**, so llama.cpp prints it as ordinary text. | |
| llama.cpp's auto parser (`common/chat-diff-analyzer.cpp`) cannot classify it: the | |
| official template renders `<think>` in thinking mode and `</think>` otherwise, which | |
| is a **replacement** diff, and `compare_thinking_enabled()` only recognises | |
| **append-style** diffs (its 64-char tail anchor never matches on the other side) → | |
| `reasoning_mode` is judged `NONE` and the marker stays in `message.content`. | |
| `conversion/youtu_moe.py` therefore rewrites the non-thinking branch to emit | |
| `<think></think>` (semantically identical — an empty think block), which turns the | |
| diff into an append and makes the analyzer report `reasoning_mode: TAG_BASED` | |
| (`-lv 5`). The parse itself still does not strip the token in current llama.cpp | |
| (`p.prefix(generation_prompt, reasoning_start)` anchors on a prompt literal that is | |
| not present in the generated text), so `strip_think.py` removes it on the output side. | |
| Tried and **not** recommended: banning token 128014 via `logit_bias`. The `</think>` | |
| disappears, but the model then believes it is still inside the think block and | |
| starts emitting quasi-reasoning prose — strictly worse. | |
| ### 5.5 Small mtmd-CLI annoyances | |
| * Log timestamps have **four** dot-separated groups (`0.05.132.418 I …`); a | |
| three-group regex will not filter them. | |
| * `llama-mtmd-cli` prints the generated text with `LOG()`, so `--log-disable` | |
| swallows the answer too (symptom: empty output). | |
| ## 6. Known limitations | |
| * No reasoning extraction into `reasoning_content` (see 5.4) — use `strip_think.py`. | |
| * Only Q4_K_M and BF16 are published; other quant types are untested. | |
| * KDA recurrent path has only been validated on Metal (macOS). CUDA/CPU are expected | |
| to work but have not been run. | |
| * Small-text OCR is limited by the default 300 image tokens; pass | |
| `--image-min-tokens 1024` for dense documents. | |
| ## 7. Verified output | |
| Text (Q4_K_M, M3 Max 128 GiB, prefill ≈ 38 t/s, decode ≈ 14 t/s): | |
| ``` | |
| > 用一句话解释什么是光合作用。 | |
| 光合作用是植物、藻类和某些细菌利用光能将二氧化碳和水转化为有机物(如葡萄糖)并释放氧气的过程。 | |
| ``` | |
| Vision — the 1969-07-21 New York Times front page (mtmd's bundled `test-1.jpeg`): | |
| masthead, date `MONDAY, JULY 21, 1969`, `LATE CITY EDITION`, `10 CENTS`, | |
| `VOL. CXVIII. NO. 46,731`, headline `MEN WALK ON MOON`, subhead | |
| `ASTRONAUTS LAND ON PLAIN; COLLECT ROCKS, PLANT FLAG` — all correct. | |
| ## 8. License | |
| The patch follows llama.cpp's MIT license. The weights and the model architecture | |
| follow the upstream **Youtu-LLM license** (`license: other`); see | |
| [`tencent/Youtu-LLM-A7B-Base/LICENSE.txt`](https://huggingface.co/tencent/Youtu-LLM-A7B-Base/blob/main/LICENSE.txt). | |