--- 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、模态感知专家偏置、`` 残留)。 --- ## 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 `` 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 `` (token **128014**) as its very first assistant-side token, closing the think block the template pre-opened: ``` prompt: <|Assistant|> <- template model: 光合作用是… <- 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 ``. `` 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 `` in thinking mode and `` 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 `` (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 `` 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).