File size: 10,764 Bytes
7e0a8c6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 | ---
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).
|