Skip to main content
Glama

SkyVision.cpp

docker-mcp

用 ggml 在本地跑图像/视频 VQA —— 一个多模型推理引擎: 每个视觉语言模型是一个后端(实现 vqa::Model),统一前端按 GGUF 里的 general.architecture 自动选择后端,图像与视频帧序列共用同一套输入抽象。 运行时不需要 Python、不需要 CUDA Toolkit、不依赖任何厂商运行时。

English

也可以用 MCP(Model Context Protocol)服务器的方式跑:vqa-cli --mcp 把同一套引擎包成 stdio JSON-RPC 服务,暴露 9 个工具(看图问答 / 图像描述 / 目标检测 / 定位 / 注视点 / 视频问答 / 视频分类 / 模型管理),仓库自带 Dockerfile,镜像内置 403MB 的 SmolVLM,不挂载任何权重也能 开箱回答"这张图里有什么"。见 MCP 服务器 一节。

支持的后端

按 GGUF 架构标签 10 秒选型(此矩阵由 capability spec 表生成——vqa-cli --specs / GET /api/specs 是同一份 JSON,README 内嵌块用 scripts/gen_backend_table.py 重新生成,勿手改); 各后端的详细结构说明见下方人工维护的表格:

架构标签

别名

输入

任务

流式

标记

moondream3

moondream

图像 + 文本

ask caption detect point gaze

—

需 *-vision.gguf

videochat3

—

图像 + 视频 + 文本

ask

—

—

qwen3_5

—

图像 + 视频 + 文本

ask caption

—

—

marlin

—

图像 + 视频 + 文本

ask caption

—

—

videojev

—

视频 + 文本

score

—

—

minicpmv47

minicpm_v4_7

图像 + 视频 + 文本

ask

—

—

qwen3vl

qwen3_vl

图像 + 视频 + 文本

ask

✅

—

moss_vl

—

图像 + 视频 + 文本

ask

✅

—

smolvlm

idefics3

图像 + 文本

ask

—

—

qwen2vl

qwen2_5_vl

图像 + 文本

ask

—

—

videomae

vivit

图像 + 视频

classify embed

—

纯视觉

vjepa2

vjepa

图像 + 视频

classify embed

—

纯视觉

mage_vl

—

图像 + 视频 + 文本

ask gate

✅

—

qwen25omni

qwen2_5_omni

图像 + 音频 + 文本

ask

—

语音输出

embgemma2

—

图像 + 视频 + 音频 + 文本

embed

—

—

gemma4

—

图像 + 视频 + 音频 + 文本

ask

—

—

siglip

—

图像

classify embed

—

纯视觉

keye-vl2

keye_vl2

图像 + 视频 + 文本

ask

—

—

qwen3omni

qwen3_omni

图像 + 音频 + 文本

ask

—

语音输出

janus

janus_pro

图像 + 文本

ask

—

—

nemotron_parse

nemotron-parse

图像 + 文本

ask

—

—

nemotronomni

—

图像 + 音频 + 文本

ask

—

—

minicpmo

minicpm_o

图像 + 音频 + 文本

ask

—

语音输出

diffusiongemma

diffusion_gemma

图像 + 文本

ask

—

—

后端

架构标签

输入

说明

moondream3.1-9B-A2B

moondream3

图像

MoE 文本 + SigLIP 视觉塔;五个官方技能(caption / query / detect / point / gaze)、Web UI、批量推理

VideoChat3-4B

videochat3

图像 + 视频

Qwen3 文本主干(36 层 GQA + per-head qk-norm)+ VideoChat3 自研 27 层 ViT(2D RoPE + 2×2 merge)、2×2→4608 merger、multimodal projector

Qwen3.5 系(含 Marlin-2B)

qwen3_5(旧 GGUF 标签 marlin 兼容)

图像 + 视频

Qwen3.5 混合主干(18 层 gated-delta 线性注意力 + 6 层 full attention,interleaved MRoPE)+ Qwen3-VL 式视觉塔(学习式绝对位置嵌入 + 2D RoPE + 逐时间步注意力)。官方 Qwen/Qwen3.5-* 检查点直接转换即用(结构同构、逐级 parity 已验证);Marlin-2B 为其视频字幕特化版,额外带官方训练技能 caption(带时间戳事件)/ find(时序定位)

VideoJev-4B(打分式)

videojev

视频

Qwen3.5-4B 主干(32 层:24 线性 + 8 full,线性注意力 16 键 / 32 值头 GQA)+ 同款视觉塔,但末层被裁成 attention-only(无 MLP/post-norm),并用训练好的 cosine 决策头替代 lm head:一次前向对候选列表打分(时序定位=逐时间片 sigmoid,单选=VQA softmax),不生成文本。走 --score <qs.json>,见"候选打分"

Mage-VL-4B

mage_vl

图像 + 视频

微软亚研院 codec 原生 VLM:Mage-ViT Codec-ViT(24 层、16×16 patch、逐维 4:6:6 T/H/W RoPE、按帧分块注意力、2×2 block 布局直折 merger)+ Qwen3-4B-Instruct-2507 文本主干(36 层 GQA + qk-norm,与 VideoChat3 同构)。官方 microsoft/Mage-VL 检查点转换即用;图像单块、视频逐帧 <X.X seconds> 时间戳标签(帧采样后端)。社区首个 ggml 实现

MOSS-VL-Realtime(11B)

moss_vl

图像 + 视频

OpenMOSS cross-attention 架构:Qwen3 文本主干(36 self 层 + 12 纯 cross-attn 层,tanh 双门控)+ SigLIP 塔(deepstack 8/16/24 + merger)+ 每帧可学习 separator。视觉 KV 走独立 cross 注意力,interleaved MRoPE(视觉三轴平面位置)。支持多轮会话(?session=)

VideoMAE(base,video tower)

videomae

视频(图像=静帧)

纯视觉塔,没有文本侧:Conv3d tubelet patch 嵌入 + 固定 sin-cos 位置表(无 cls token)+ 12 层全时空注意力,一次前向出分类标签(*-finetuned-* 检查点,如 Kinetics-400)或 pooled 嵌入(MCG-NJU/videomae-base 是预训练版,无分类头)。单 clip 1568 token,单图;--mode embed 直接吐 768 维特征

ViViT(b-16x2,video tower)

vivit

视频(图像=静帧)

同一条视频塔(复用 videomae 后端):差别只是有 cls token、位置表是学出来的参数、gelu_fast(tanh 形式)、LN eps 1e-6、qkv 三路都有 bias、预处理短边 256→裁 224 且双线性重采样 + 零中心化。32 帧 → 3136+1 token,一次前向出 Kinetics-400 标签

V-JEPA 2(ViT-L/16,video tower)

vjepa2

视频(图像=静帧)

Meta FAIR 的世界模型编码器:没有位置表,改用三维分解 RoPE(head_dim 64 = 20 时间 + 20 高 + 20 宽 + 4 不动,位置取自 (t', h', w') 网格);24 层全时空注意力,q/k/v 三路都带 bias,GELU(erf)。64 帧 × 256² → 8192 token。三个头都支持:attentive pooler(fine-tune 用它池化)+ 线性分类器(--mode N 出 top-N 标签,如 SSv2/Diving48)+ masked-latent predictor(--mode predict 做掩码潜变量预测)。发布版只有编码器 + predictor

Qwen3-VL(4B)/ OneStreamer-4B

qwen3vl

图像 + 视频(逐帧)

独立实现的 Qwen3-VL dense 家族后端(OneStreamer-4B 就是 Qwen3-VL-4B-Instruct 的流式 SFT):36 层全注意力、GQA 32:8、head_dim 128、hidden 2560、IMROPE 24/20/20 旋转全部 128 维(theta 5e6)、tie_word_embeddings、无 q|gate 与输出门控;视觉塔是共享 vqa::vit::block 的 24 层 ViT(patch16、temporal2、2×2 merge、learned 48×48 位置表、2D rope),末端 merger 之外还有 DeepStack——视觉第 5/11/17 层的输出各过一套(post-fold LayerNorm,宽度 4×hidden)merger MLP,加到文本第 0/1/2 层残差流的视觉 token 位置上。视频按参考的流式协议逐帧独立编码(temporal patch 复制该帧),prompt 里每帧前置 <0.000s-0.250s> 区间标签;ask_session 做 PHCM 式窗口重放(视觉特征按帧缓存,历史整轮回填 assistant,滑窗按轮淘汰,QWEN3VL_SESSION_FRAMES 调窗宽;prompt 是上一轮 KV 的严格延长时只 prefill 新增尾巴,日志出现 KV splice),配合 </Silence>/</Standby>/</Response> 三态与 </Observe>/</Summary> 记忆标记做流式主动响应(/api/model/<id>/ask?rt=1&sys=1&session=<id> 会带上后端声明的 OVO 三态 system prompt)。转换 convert/convert_onestreamer.py,对拍 scripts/parity_onestreamer.py

MiniCPM-V 4.7(1B)

minicpmv47

图像 + 视频

openbmb 端侧 VLM,文本主干就是 Qwen3.5(text_config.model_type = qwen3_5_text,24 层 3:1 gated-delta/全注意力、IMROPE 11/11/10、q|gate 交织、untied lm_head),因此直接跑在 qwen3_5 后端的 minicpm 模式下,不重复一份文本图。新增部分只有:SigLIP canvas 视觉塔(27 层、d=1152、patch14、GELU-tanh、无 rope——learned 70×70 位置表按最近桶取值,整塔即共享 vqa::vit::block)+ 16× 两级合并(第 6 层后插入窗口注意力 merger:2×2 窗口内注意力 + 4×d 展平后 LN/Linear/GELU/Linear,再 2×2 投影 MLP 到文本宽度)+ canvas M-RoPE(视觉 token 铺 2-D 画布,<image>/</image> 钉在画布光晕/远端角,文本计数从 max(h,w)+1 续;图像块后面那个模板换行也钉在 canvas 原点,官方行为)。slice_mode 走官方切片规则(长边 >448 起切,最多 9 块 <slice> 明细 + 缩略图,每块独立过一次塔),视频按帧推进同一套 canvas。转换 convert/convert_minicpmv47.py。开关:MINICPM_DOWNSAMPLE=4x(4x 保 token)、MINICPM_NO_SLICES=1、MINICPM_SCALE_RES=<n>(缩略图长边,默认 448)。预置权重:hf download SkyContact/MiniCPM-V-4.7-1B-GGUF(含 tokenizer,魔搭同名 modelscope download --model SkyContact/MiniCPM-V-4.7-1B-GGUF)

Qwen2.5-VL(3B)

qwen2vl

图像(视频待接)

Qwen2 文本主干(36 层 GQA 16:2、qkv 带 bias、section MRoPE 16/24/24、SwiGLU)+ Qwen2.5 ViT(32 层、patch14、temporal2、2×2 merge、窗口注意力:每 8 层一次全局)+ merger。动态分辨率走 smart_resize(28 的倍数),预处理是 CLIP mean/std、bicubic 缩放、patch 按 2×2 分组序打包

Qwen2.5-Omni(3B)

qwen25omni

图像 + 音频输入,文本 + 语音输出

全功能 thinker-talker:Qwen2.5 文本主干(untied lm_head)+ Qwen2.5-VL 视觉塔(与 qwen2vl 同款窗口注意力图)+ Whisper 系音频塔(128-bin log-mel、n_window=100 分块卷积+分块注意力、25 token/s)→ Talker(24 层双轨 LM:文本轨吃 thinker 隐状态,输出 25Hz codec token)→ Token2Wav(flow-matching DiT,RK4×10 步 + CFG + sway 采样;BigVGAN v2 抗锯齿激活,纯 C++ 宿主实现)。音频输入走 --audio(ffmpeg 解码 16k mono)。参考实现:llama.cpp PR #13784(thinker)+ HF transformers 5.20 官方源码(全部语义基准),mel/音频塔与官方逐位对齐

Qwen3-Omni(30B-A3B-Instruct)

qwen3omni

图像 + 音频输入,文本 + 语音输出

首个 ggml 实现(llama.cpp 无此架构):thinker=标准 Qwen3 全注意力 + MoE 文本主干(48 层、128 专家激活 8、interleaved MRoPE 24/20/20 逐轴整支 128 维)+ Qwen3-VL 视觉塔(deepstack 三抽头,与 qwen3vl 共享 qwen3vl_tower 积木)+ Qwen3-ASR 同族音频塔(128 mel、chunk 500、窗 800)→ Talker(20 层 MoE + sigmoid 门控共享专家;用户段只喂 role==user token;step 输入=16 个嵌入行求和)→ code predictor(5 层 dense,15 组独立嵌入表/头——头未 tie,checkpoint 独立权重,与 codec_head 同理)→ code2wav(8 层窗注意力 transformer + 上采样 ConvNeXt + snake/transconv 解码器,整条声码器是单张 ggml 图:F32 im2col+mul_mat 卷积、conv_transpose_1d、sin/sqr snake,比宿主循环快 50-100×)。语音出 --speak(24 kHz,`--speaker chelsie

Janus-Pro-1B

janus(janus_pro)

图像

DeepSeek 解耦式理解/生成 VLM 的理解侧(VQ 生成解码器不属于本引擎):timm 风味 SigLIP-large-384 视觉塔(24 层、d=1024、patch16、fused qkv+全 bias、GELU(erf)、无 cls token、select_layer=-1 全层 + 终端 LN)+ 2 层 MLP aligner(1024→2048 GELU(erf) →2048)+ 纯 llama 1.5B 文本主干(24 层、16:16 头、theta 1e4、untied 102400 vocab)。预处理官方口径:bicubic 保比缩到长边 384 → expand2square 填充 (127,127,127) → /255 归一化;一图 576 个 <image_placeholder> token,deepseek sft 模板(<|User|>: 开头,问题后换行接 576 个占位符,再空行后以 <|Assistant|>: 收尾)。分词器新增 DeepSeekBpe 模式(isolated Split 序:拉丁/标点各带一个前导空格、CJK 大块连写、数字逐位——与 HF 官方逐 id 一致)。f16 3.9 GB / q8_0 ≈ 2.1 GB;对拍:与 fp32 官方栈 logits corr 0.9999、输出逐字一致。转换 convert/convert_janus.py。预置权重:modelscope download --model SkyContact/Janus-Pro-1B-GGUF(f16/q8_0/q4_0 + tokenizer,HF 同名 hf download SkyContact/Janus-Pro-1B-GGUF)

Nemotron-3.5-Parse(0.63B)

nemotron_parse(nemotron-parse)

图像(文档页)

NVIDIA 文档解析模型:C-RADIOv4-SO400M 视觉塔(27 层、patch16、d=1152、10 个 class token 排在最前、学出来的 128×128 位置表按内容区插值、无 rope、GELU(erf)、塔末无 norm)+ 卷积 neck(1×1 Conv1d → LN → 宽度 4 合 1 的 Conv2d → LN;前两个 class token 投影成 1 个 summary token 追加在末尾)+ 10 层 mBART 式前缀解码器(d=1024、16/8 GQA、pre-LN、√d 嵌入缩放、tied lm_head、没有任何位置编码,视觉特征即 KV 前缀)。输出是带版面类别与归一化框的控制 token 流,--mode markdown/json 渲染成 markdown(LaTeX tabular→HTML 表)或带 bbox 的 blocks。对拍:位置场 2.7e-7、整页 159 步贪心与官方逐 token 相同。转换 convert/convert_nemotron_parse.py,MCP 工具 parse_document

Nemotron-3-Nano-Omni-30B-A3B(Reasoning)

nemotronomni

图像 + 音频

NVIDIA 30B-A3B 全模态 MoE 推理模型:52 层 Mamba2/Attention/MoE 混合主干(23 Mamba2 + 6 NoPE attention + 23 MoE;sigmoid ×2.5 路由带 e_score_correction_bias、relu² 非门控专家 1856、1 个 3712 共享专家)+ C-RADIOv4-H 视觉塔(32 层 d=1280、10 个 register token 前置后整块丢弃、学出的 128×128 位置表、T=2 tubelet 视频嵌入已转换)+ Parakeet/FastConformer 音频塔(BatchNorm 引擎侧折叠)。英文 only、thinking 默认开、256k ctx、纯文本输出(无 talker)。对拍:文本贪心 argmax 与官方栈 100% 一致、音频转写逐字一致、RoPE 判决=NoPE(partial84 媒体路径 step0 即 cos 0.85 语义损坏)。逐张量混合量化:%256 宽的塔/主干 Q6_K + 专家 Q5_0 + 嵌入 Q8_0(行宽 2688/1856/3712 非 256 倍数,裸 k-quant 档只能覆盖 5% 字节,故无纯 Q4_K/Q6_K 档)。转换 convert/convert_nemotronomni.py。预置权重:modelscope download --model SkyContact/Nemotron3-Nano-Omni-30B-A3B-GGUF(HF 同名,含 tokenizer)

MiniCPM-o 4.5

minicpmo(minicpm_o)

图像 + 音频

OpenBMB 8B 级全模态(官方 Qwen3-8B 稠密底座):36 层 d=4096 GQA 32:8 主干(词表 151748、ctx 40960、q/k 逐 head RMSNorm)+ SigLIP NaFiT 视觉塔(27 层 d=1152 @448px,学出的 70×70 位置表按图动态多瓦片切片)+ 64-query Perceiver 重采样器 + Whisper-medium 音频塔(24 层、80 mel、1s 块对角掩码)+ Llama-768×20L TTS 副塔(6562 码本)+ CosyVoice2 token2wav 声码器(兄弟 GGUF,同目录自动发现);文本出 + --speak 24kHz 语音出。TTS 必须官方采样参数(贪心会码塌缩)。对拍:文本贪心 "Paris" 与官方一致、视觉重采样 cos 0.99994、音频投影 cos 0.9999980、同噪声声码器门 mel cos 0.999999986。量化两档:q8_0 10.09GB(仅视觉 fc2×27 保 F16)+ q6k_mixed 8.19GB(1152 系×136+fc2 F16)。转换 convert/convert_minicpmo45.py。预置权重:modelscope download --model SkyContact/MiniCPM-o-4.5-GGUF(HF 同名,含 tokenizer 与声码器)

DiffusionGemma 26B-A4B-it

diffusiongemma(diffusion_gemma)

图像 + 文本

Google 的块扩散(block-diffusion)解码器,不是自回归:同一套 30 层稀疏 MoE 主干(hidden 2816、128 专家 top-8 + 并行的 dense MLP 双 FFN、SWA 8×256/窗 1024 与 full 2×512/theta 1e6 proportional 0.25 交织、三明治 post-norm、logit softcap 30、tied embedding ×sqrt(2816))被跑两遍——encoder 因果 prefill 写 KV,canvas decoder 对 256 token 的块做双向注意力去噪(只读同一份 KV);每块最多 48 步,EntropyBound 采样(熵界 0.1、温度 0.8→0.4 线性退火、未接受 token 重加噪)+ "稳定且自信"自适应停止,self-conditioning 吃上一步 logits 的嵌入期望;块定稿后 argmax canvas 追加进序列并重新按因果编码覆盖 KV 暂存槽。视觉走 Gemma4 塔(27 层 d=1152、patch16、学习式 2D 位置表、轴向 2D RoPE、3×3 空间池化 → 280 soft token)。转换 convert/convert_diffusiongemma.py;DG_STEPS / DG_SC_TOPK 可调去噪步数与 self-conditioning 截断

SmolVLM(256M,Idefics3)

smolvlm

图像

目前最小的可用后端:SigLIP-base 视觉塔(12 层、patch16、512²、learned pos、LN/注意力/MLP 全带 bias、gelu_tanh)+ 4×4 pixel shuffle 连接器(12288→576)+ SmolLM2-135M 文本主干(30 层、无 bias、GQA 9:3、rope 1e5)。预处理按官方 Idefics3:长边缩到 2048 → 补齐到 512 的倍数 → 切 512² 瓦片(行优先)+ 一张全局视图,每块 64 token。f16 515 MB / q8 ≈ 270 MB

EmbeddingGemma-2(740M)

embgemma2

图像 + 视频 + 音频 + 文本(嵌入)

多模态嵌入模型(不生成文本):Gemma3 文本塔(24 层 d=512、GQA 4:1、SWA/full 交错、逐层 PLE)+ Gemma4 ViT(16 层 patch16、轴向 2D RoPE、3×3 位置池化)+ Gemma4 音频塔(USM conformer:双 3×3 stride-2 卷积子采样 + 12 层 macaron FFN×2 / LightConv1d / 分块局部注意力 + Transformer-XL 相对位置偏置,ClippableLinear 全程带 clamp)。--embed 输出 768 维(Matryoshka 可截 512/256/128/64)、--embed-preset 官方任务前缀、--embed-file 批量 JSONL。四模态对 fp32 HF 参考 0.99999+

Keye-VL-2.0(30B-A3B)

keye-vl2

图像 + 视频

快手 Keye-VL-2.0-30B-A3B:Qwen3 式 MoE 文本主干(48 层、128 路由专家激活 8、norm-topk softmax 路由、mRoPE 16/24/24 theta 1e7、per-head qk-norm)+ 每层 DSA lightning indexer 稀疏注意力(16 头×64 维、Walsh-Hadamard 旋转 MQA indexer、top-2048 KV 稀疏掩码、独立 indexer K 缓存)+ SigLIP-so400m 系视觉塔(27 层 patch14、ViT 内 2D mRoPE、729 学习式位置表双线性插值、post-LN、2×2 merge + GELU 投影)。视频=官方默认逐帧路径(mRoPE 官方 get_rope_index 逐位对齐;官方 packing 表在该路径为死代码;opt-in fast 双流未实现)+ `<

| Gemma 4(12B 统一版) | gemma4 | 图像 + 音频 + 文本 | Google Gemma4 统一多模态:encoder-free——没有视觉/音频塔,图像 patch(48×48×3=6912 维 im2col → LN1 → patch_dense → LN2 → [1120,2,3840] x/y 位置表相加 → pos_norm → RMSNorm → 3840×3840 投影)与音频(原始 16 kHz 波形每 640 样本一帧补零 → RMSNorm → [3840,640] 投影)直接 splice 进文本流。文本主干 48 层 dense:SWA(窗1024, 16×256, 8KV×256, rope 1e4) 与 full(16×512, 1KV×512, theta 1e6 proportional + partial rotary 0.25) 5:1 交错、k_eq_v、三明治 post-norm、logit softcap 30、tied embeddings(×sqrt(3840) 只乘 token 路径)、vocab 262144。混合形状 KV(KvCacheMixed 逐层维度),视觉 token 双向注意力(SWA 层块内双向、full 层保持因果)。分词坑:聊天模板与媒体标记全部按普通 piece 编码(<|turn>/<|image> 碎片化,<turn\|> 反而是单 token 106),特殊 added id 绝不能直接喂;思考模板 <|turn>system <|think> <turn\|> 开局,输出剥离 <|channel>thought 块。CUDA fattn 需 KV 视图与 mask 宽度按 FATTN_KQ_STRIDE(256) 对齐(mask 补列 -inf 惰性)。f16 24 GB / q6_K 9.8 GB(12GB 卡 decode 36 tok/s)。转换 convert/convert_gemma4.py(f16/q8_0/q6_K/q4_0),对拍裁判=llama.cpp 上游 mtmd-cli(文本/图像)+ 官方 Gemma4UnifiedProcessor 5.21.0.dev0(视频逐位)

DiffusionGemma:放置、性能与限制

放置。q8_0 档权重 28.89 GB,12 GB 级显卡容纳不下,因此 CUDA 构建默认使用两段式放置:视觉塔(1.19 GB)及其计算图在 CUDA0 上,语言主干(27.70 GB)留在 host mmap 由 CPU 后端计算。每张计算图只属于一个后端,两个阶段之间没有跨后端张量拷贝(encoder / decoder 只在 CPU,vision / projection 只在卡上)。

开关

放置

--cpu

全部 CPU

默认(CUDA 构建)

视觉塔 → 显卡,语言主干 → host mmap

DG_GPU=1

全部上卡(约需 30 GB VRAM)

DG_TEXT_GPU=1

语言主干也上卡(仅大显存机器有意义)

DG_ALL_HOST=1

参考路径:忽略设备后端,全部 CPU

性能(i7-14700K 20C/28T、RTX 4070 SUPER 12 GB、q8_0 档):

场景

默认(两段式)

--cpu

文本 prefill,26 token

1.7 s

1.8 s

单个去噪步

4.0 s

4.0 s

一个 canvas(256 槽,7–21 步)

28–85 s

28–79 s

视觉塔,260 soft token

0.24 s

7.03 s

端到端图像问答(单图、一句话)

≈90 s

≈91 s

这里的成本单元是去噪步而非 token:一个 canvas 一次性产出 256 个槽位,其中只有少数是有效答案 token,所以约 0.27–0.39 的 tok/s 不能与自回归后端横向比较。端到端时间几乎全部来自 CPU 上的主干(每步 256 token × 约 4B 激活参数);视觉塔上卡快约 29×,但单图只省下约 7 s。要提高吞吐需要把主干也放上卡(DG_GPU=1 / DG_TEXT_GPU=1)。引擎会打印 [perf] vision … / prefill … / decode … 三段分解。

量化:dense 权重与专家表 Q8_0;视觉塔与 tied embedding 保持 F16(t.embed 同时充当 lm_head,F16 可避免 logits 的量化噪声)。

验证:视觉塔与官方 Gemma4VisionModel 对拍 corr = 0.999991(scripts/parity_diffusiongemma_vision.py);端到端 test_shapes.png → "This image features a red circle, a blue square, and a black triangle against a light gray background."

已知限制

  • 视频按单帧处理(取第 0 帧),checkpoint 只有一条图像路径。

  • 输出以 canvas(256 槽)为粒度,短回答也要付一次完整去噪。

  • 主干留在 CPU 时约 4 s/步,长回答的端到端时间以分钟计。

调试开关:DG_VISION_LAYERS=N、DG_VISION_SKIP=mlp|attn|all|pos、DG_VISION_CONST=1、DG_VISION_GRID="w h"、DG_VISION_ECHO=1、DG_VISION_ONLY=1、DG_DUMP_WEIGHTS=<dir>、DG_DUMP_DIR=<dir>(配合 scripts/ref_diffusiongemma.py 与 scripts/parity_diffusiongemma_vision.py 使用)。

Related MCP server: llm-vision

当前状态

moondream3.1 全部功能已就绪:文本 + 视觉 + 五个官方技能 + Web UI + 批量推理 + 流式输出, CPU / CUDA 双后端,且和其它后端一样只通过统一的 vqa-cli 驱动(没有专属二进制/接口)。 VideoChat3-4B 后端可用的部分见下方标注。

文本 decode(24 层)

✅ 质量对齐官方(逐张量 diff ~1e-3),贪心输出无 reserved token

GPU 后端(CUDA via ggml)

✅ 单序列 ~106 tok/s,8 路批量 ~241 tok/s(5070 Ti Laptop,Q6_K;flash-attn + F16 KV + CUDA graph 回放)

CPU 后端

✅ 12.8 tok/s(16 线程,与 CUDA 数值一致)

SigLIP 视觉编码器

✅ 官方多 crop Lanczos 预处理 + 批量编码,~0.56s/图(10 crop),parity < 0.04

五个官方技能

✅ caption(short/normal/long)/ query / detect / point / gaze,模板与官方逐 token 一致

Web UI(--serve,可绑 0.0.0.0 部署局域网)

✅ 移动端友好:拖拽/选图、NDJSON 流式逐字输出(UTF-8 边界安全)、视觉编码逐 clip 进度、detect 绿框 / point 十字 / gaze;同图同视频追问走两级缓存(上传解码 + 视觉特征),二次提问 ~26ms

moondream 思考模式

✅ mode=ask-think:官方 reasoning 协议(thinking_id → grounded 坐标 token → answer),推理段与回答流式分段输出

FP8 专家权重 → Q8_0 / Q6_K / Q4_K

✅ 转换器已就绪(Q6_K ≈ 7.6GB,12GB 显卡首选)

批量推理(--batch)/ 流式输出

✅ prefill 注意力按 prompt 长度开窗(不再扫满 n_ctx);8 路批量 decode ~241 tok/s

VideoChat3-4B 图像问答

✅ 端到端 + 逐级 parity 已验证:patch 嵌入 rel 4e-4、27 层 ViT + merger + projector rel 0.026(F16 权重噪声级),贪心输出与官方 bf16 逐字一致

VideoChat3-4B 视频输入

✅ 内置 --video(ffmpeg 管道,FFMPEG=<path> 可指定):抽帧 + 时间戳提示词 + 4 帧时序 clip 联合注意力;视觉塔逐 clip 分批执行(激活峰值降至单 clip),同上下文预算内自动钳制超大图/视频

Marlin-2B 图像/视频问答

✅ 端到端 + 逐级 parity 已验证(官方 transformers 5.20 做 oracle):pixel_values 与引擎 patchify 逐位一致、pos 表 2.7e-8、patch 卷积 2.6e-3、block0 5.3e-3;整塔漂移 6.6e-2 优于官方 bf16 自身的 1.1e-1;末 token logits 1.5e-2 < 官方 bf16 2.8e-2;文本纯输入 gated-delta 层 1.4e-2

VideoJev-4B 候选打分

✅ 全链路打通 + 对拍官方 transformers 5.14(scripts/parity_videojev.py,prompt token 序列逐位一致)。以 CPU float32 参考为真值:引擎 f16 的最大概率偏差 6.8e-3(q8_0 5.9e-3),而官方仓库默认的 bf16 跑法偏差 5.7e-2 —— 引擎比官方参考更贴近 fp32。choice 端偏差 6.6e-6(官方 bf16 为 5.1e-5)。见 scripts/parity_videojev.py / scripts/gen_fake_videojev.py

Marlin-2B 视频结构化技能

✅ --mode caption(Scene + 带秒级时间戳的 Events 列表)/ --mode find(自然语言事件 → From X.X to Y.X.),训练对齐的官方提示词

视觉预处理 token 预算

✅ 图片/视频的视觉 token 上限 = n_ctx − max_tokens − 文本开销,编码前钳制分辨率或提前报错(不再白等几秒后 context overflow)

MOSS-VL-Realtime 图像/视频问答

✅ 端到端 + 官方栈对拍:图像 prefill token 流 24/24、视频 78/78 逐位一致;layer2 cross-attn 中间量 corr ≥ 0.999;KV cache 回读 vs 官方 roped K corr 0.9997。CUDA(5070 Ti Laptop,Q4_K):视觉 213ms/图、decode 67 tok/s;CPU 16 线程 decode 8.9 tok/s

MOSS-VL 多轮会话

✅ --serve 的 /ask?session=<id>(reset=1 清空):server 维护最近 12 轮文本历史,引擎 prompt 重建(媒体缓存命中时视觉塔免重跑)

MOSS-VL 实时流会话

✅ /ask?session=<id>&rt=1:官方 real_time_generate 的引擎侧实现——新帧/新问题增量追加到活跌 KV(文本 KV 续写 + cross-KV 行追加,不重建全上下文),位置按官方增量 MRoPE 公式;`<

MiniCPM-V 4.7 图像/切片/视频

✅ 端到端 + 逐级数值消融(scripts/ref_minicpmv47.py 出 oracle,scripts/parity_minicpmv47.py 逐级对拍):patches corr 0.999999、patch embed 0.999997、窗口 merger 0.999908、塔输出 0.999981、投影特征 0.999839、末 token logits 0.999820(top-8 完全相同),贪婪 24 token 与官方逐字一致("The image displays three simple geometric shapes…");MINICPM_DOWNSAMPLE=4x 同套消融 corr 0.9997~0.99999;切片路径(1400×900 → 缩略图 + 6 切片,444 vision token):canvas M-RoPE 位置 id 与官方逐元素相同(484 token)、7 个 crop 的 patch ≤2 灰阶、合并特征 corr 0.999458、prefill logits corr 0.999518(top-8 相同);视频(6 帧 420 token)帧级 canvas 位置与官方逐元素相同(delta -343),按帧顺序叙述正确("Frame 1: red square on the left, frame 2: slightly moved right…");切片开/关 OCR 消融:MINCPM SLICE TEST12345 vs 幻觉 MARGIN ACT SETTING

Keye-VL-2.0 文本/图像/视频

✅ 端到端 + 官方 llama.cpp fork 对拍(KEYE_DUMP_DIR 真值):视觉投影嵌入与官方逐位一致(corr 1.0,meandiff 0.0045);文本自报 Keye;测试卡图像描述与官方一致(彩条 + 时间码 00:00:05.000);12 帧视频描述正确(竖色条/移动彩带/漂浮色块);KEYE_EXPERTCACHE=1 与 VQA_SPLIT=1 贪婪 120 token 逐 byte 一致

CPU vs CUDA(RTX 4070 SUPER 12GB,7 crop 切片图 444 vision token)

视觉 7704 ms → 244 ms;prefill 484 tok 1680 ms → 32 ms(15.3k tok/s);decode 27 → 185 tok/s;视频 6 帧视觉 7765 ms → 187 ms。两条路径答案逐字相同

Mage-VL 图像问答

✅ 端到端 + 视觉塔逐级 parity(scripts/parity_mage_vision.py,1GB 视觉塔-only oracle):patches 5e-3、v_patch_out 1.5e-2、block0 qkv 7e-3;e2e 与官方 transformers(bf16 CPU/GPU oracle)贪心输出语义一致(llamas 测试图:数量/着装/颜色三问全对)。CUDA(3060 6GB,Q6_K):视觉 162ms/图、prefill 2557 tok/s、decode 63 tok/s;q6_k 权重 4.28GB + 0.6GB KV 正好放进 6GB 显存

Mage-VL System 1(主动流式门控)

✅ streammind_gate 认知门控接入:PreNet→Mamba-1 逐窗口递推→PostNet→vocab-2 Qwen3 分类头(纯 C++ host 实现,与 torch 参考逐级对拍 rel≈0)。--mode gate 逐帧输出 p(speak);--mode gated[:τ] 门控生成;rt 流(rt/start)的纯观察轮自动走门控,MAGE_GATE_TAU 可调阈值(默认 0.5)。rt 会话为有界重放滑窗:静默轮把 gate 视觉编码产出的逐帧特征免费存入滑动窗口(MAGE_SESSION_FRAMES,并按 n_ctx token 预算自动收紧),发言/提问轮只重放窗口内(system+历史轮+本轮帧)一次 prefill,长流成本 O(窗口) 而非 O(累计)。门控权重:convert/convert_mage_gate.py → mage-gate-f16.gguf(放到模型同目录自动加载)

Mage-VL 视频问答

✅ --video(ffmpeg 帧采样后端,逐帧 <X.X seconds> 标签 + 每帧独立注意力块):双图案拼接测试视频的前/后半段区分描述正确;1800 视觉 token prefill 2416 tok/s

MOSS-VL 常驻推理流

✅ rt/start?session=&src=&interval=(src=文件/URL/video=摄像头):ffmpeg 采集线程 + 常驻推理循环,模型按轮观察新帧并主动发言(`<

VideoMAE 视频分类 / 特征

✅ 一个 GGUF 装全部权重(94M 参数,f16 约 180MB):16 帧均匀采样 → 中心裁剪 224 → 一次全时空注意力前向 → top-k 标签;无 tokenizer、无 KV cache。逐级对拍已验证(numpy 从 GGUF 权重复算整条前向):12 层全长相对差 ~1e-3、patch 嵌入 3e-3、block0 注意力 3e-4、Kinetics-400 的 400 个类别概率差 <1e-4。预训练版(无分类头)输出 pooled 嵌入,--mode embed 直出 768 维 JSON。CPU 单 clip(1568 token×12 层)约 0.85 s(q8_0 权重 0.59 s)

EmbeddingGemma-2 四模态嵌入

✅ 文本 0.9999999 / 图像 0.99999 / 音频 0.99999 / 视频 0.9999998(fp32 HF 参考,f16 权重)。音频塔逐层 hook 对拍修复 5 个图实现 bug(漏乘 attention 输出投影、content 项与 rel-bias 布局互为转置、rel-bias 行视图跨度、头合并 cont_2d 乱序、GLU 切分/残差/clamp);视频逐帧跑 B=1 视觉图(注意力不跨帧,视频预处理用 video 预算 140 soft/帧)

V-JEPA 2 特征 / 分类 / 掩码预测

✅ ViT-L(326M + predictor 25M,f16 653MB):64 帧 → 短边 292(双线性)→ 裁 256 → 8192 token 全时空注意力 ×24 层 → 1024 维特征;SSv2 微调版(16 帧)再走 3 层 attentive pooler + 174 类线性头,--mode 5 直出标签;--mode predict 跑 12 层 masked-latent predictor。对拍已验证(transformers 5.x):同输入下 24 层全长 cos 1.000000(max 0.065);端到端(各自预处理)pooled cos 0.9994、SSv2 top-1 标签一致(0.916 vs 0.921,概率最大差 4.5e-3)、predictor cos 0.998。CPU 单 clip(8192 token)约 25 s,SSv2(2048 token)约 5 s

Nemotron-3.5-Parse 文档解析

✅ 端到端 + 官方 torch 参考逐级对拍(scripts/ref_nemotron_parse.py → convert/compare_dumps.py):位置场 2.7e-7、patch 嵌入 2.8e-3、首 token logits top8 8/8、整页 159 步贪心输出与官方逐 token 相同。CPU(19 线程)一页 ≈ 19 s(视觉 16.3 s + prefill 0.8 s + 158 token decode),CUDA(4070 SUPER)≈ 0.9 s(0.64 s + 0.019 s + 0.25 s);CUDA 上 7/7 block 正文一致,坐标末位有 f16 舍入漂移

Nemotron-3-Nano-Omni-30B-A3B 文本/图像/音频

✅ 端到端 + 官方栈对拍(BF16 17 分片做 oracle):文本 prompt A/B 贪心 argmax 100% 一致、音频转写与官方逐字一致("The sound of putting the knife on the chopping board.");RoPE 判决=NoPE(partial84 媒体路径 step0 即 cos 0.85、丢失转写内容);Mamba2 数值双锚(逐步递推 vs HF chunk-SSD 交叉验证)、EC 专家驻留 XCHK logits maxdiff 0.00000、104/104 层输出位级一致。量化两档(Q8_0 35.2GB / q6k_mixed 23.3GB)贪心流与 F16 基线一致(见下)。F16 性能:CPU 16 线程 decode 0.33 tok/s / prefill 7.8 tok/s(66GB mmap 缺页墙 ~350MB/s)、CUDA(5070 Ti Laptop 12GB)EC split 0.14-0.19 tok/s(swap-wait 占 95%)——量化是可用性必需,量化后数字见"模型转换"章节

快速开始

1. 构建

唯一第三方依赖是 ggml 本体(它同时提供 gguf):

git clone https://github.com/SkYContact/SkyVision.cpp
cd SkyVision.cpp
git clone --depth 1 https://github.com/ggml-org/ggml

# Windows(MSVC + Ninja)
scripts\build-cuda.bat        # CUDA 后端(默认 sm_89=Ada,可用 CUDA_ARCH 覆盖)
scripts\build-vqa.bat         # 仅 CPU
scripts\build.bat             # 仅 CPU,改用 Visual Studio 生成器(产物在 build\Release\)
# 手工 CMake:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DGGML_NATIVE=ON
cmake --build build --config Release --target vqa-cli

vqa-cli 是按架构分派的统一前端——包括 moondream3.1:它没有独立二进制,也没有自带 HTTP server,全部能力(看图问答、caption/detect/point/gaze/ask-think、批量、Web UI)都通过 vqa::Model 接口由同一个 vqa-cli 驱动。 CUDA 构建下引擎默认优先使用 GPU,--cpu(或 moondream 的 MD_CPU=1)可强制 CPU。

可选:把 JPEG 解码换成 libjpeg-turbo(不 clone 也能编,走 stb_image,代价是与 PIL/libjpeg 的 IDCT 差 1-3 灰阶;换了之后与 PIL 逐位一致,omni 视觉塔 parity 闭环):

git clone --depth 1 https://github.com/libjpeg-turbo/libjpeg-turbo vendor/libjpeg-turbo
# 上游禁止未修改的树被 add_subdirectory:把其自保护守卫改成自比较(该补丁已进 Dockerfile)
sed -i 's/CMAKE_SOURCE_DIR/CMAKE_CURRENT_SOURCE_DIR/g' vendor/libjpeg-turbo/CMakeLists.txt
# 有 nasm 自动开 SIMD(输出与不开逐位一致,只是快 2-4 倍);MSVC 下自动对齐 /MD 运行时

2. 取权重

预转好的 GGUF 免转换直接下:魔搭 ModelScope · Hugging Face

想自己转,或者要别的量化档位:

pip install -r convert/requirements.txt

# moondream3.1-9B-A2B(官方 FP8 检查点)
hf download moondream/moondream3.1-9B-A2B --local-dir models/moondream3.1-9B-A2B

# VideoChat3-4B
hf download MCG-NJU/VideoChat3-4B --local-dir models/VideoChat3-4B

# MOSS-VL-Realtime(图像/视频 + 多轮会话)
vqa-cli --model Moss-VL-Realtime-q4k.gguf --image photo.jpg --ask "What is in this image?"
vqa-cli --model Moss-VL-Realtime-q4k.gguf --video clip.mp4 --max-frames 8 --ask "What happens?"   # 视频:逐帧时间戳提示词
# 服务端多轮会话:POST /api/model/Moss-VL-Realtime-q4k/ask?q=...&session=abc(reset=1 清空)

# Marlin-2B(魔搭镜像,无需登录)
modelscope download NemoStation/Marlin-2B --local_dir models/Marlin-2B

# Qwen3.5-2B(官方,通用图文/视频问答)
modelscope download Qwen/Qwen3.5-2B --local_dir models/Qwen3.5-2B

# VideoJev-4B(打分式,不生成文本;用 --score 驱动)
hf download ZhuYuhan/VideoJev-4B --local-dir models/VideoJev-4B
python convert/convert_videojev.py --src models/VideoJev-4B -o videojev-4b-q8_0.gguf

# VideoMAE:预训练版(只有嵌入)或带 Kinetics-400 分类头的 fine-tune
hf download MCG-NJU/videomae-base --local-dir models/videomae-base
hf download MCG-NJU/videomae-base-finetuned-kinetics --local-dir models/videomae-base-ft

# V-JEPA 2:ViT-L 视频编码器(64 帧 256²,只出特征)
hf download facebook/vjepa2-vitl-fpc64-256 --local-dir models/vjepa2-vitl-fpc64-256

# Nemotron-3.5-Parse:文档解析(整页 → 版面类别 + 归一化框)
hf download nvidia/Nemotron-3.5-Parse --local-dir models/Nemotron-3.5-Parse
python convert/convert_nemotron_parse.py

转换器会自己找检查点:--src <目录|文件> → 该模型的 $MOONDREAM_SRC / $VIDEOCHAT3_SRC → HF 缓存($HF_HOME 或 ~/.cache/huggingface/hub)→ 仓库下的 models/; 找不到时会打印下载命令。详见 模型转换。

3. 运行

# 统一前端:后端按 GGUF 的 general.architecture 自动选择
vqa-cli --model models/VideoChat3-4B/videochat3-4b-q8_0.gguf \
        --tokenizer models/VideoChat3-4B \
        --image photo.jpg --ask "What is in this image?" --max-tokens 256

# Marlin-2B:视频结构化信息提取
vqa-cli --model marlin-2b-q8_0.gguf --tokenizer models/Marlin-2B         --video clip.mp4 --fps 2 --max-frames 16         --ask x --mode caption --max-tokens 512          # Scene: + 带时间戳的事件列表
vqa-cli --model marlin-2b-q8_0.gguf --tokenizer models/Marlin-2B         --video clip.mp4 --fps 2         --ask "a person enters the room" --mode find     # From X.X to Y.Y.

vqa-cli --model moondream3.1-q6k.gguf --tokenizer tokenizer.json \
        --vision moondream3.1-vision.gguf --region moondream3.1-region.gguf \
        --image photo.jpg --ask "What is in this image?" --n-ctx 1024

# VideoJev-4B:候选打分(不生成文本)。grounding 逐时间片 sigmoid → 区间;
# choice 对选项 softmax → answer_index。--threshold 覆盖模型自带阈值(默认 0.25)
cat > qs.json <<'JSON'
{"questions": [{"id": "q1", "task": "grounding", "query": "when does the person sit down"},
               {"id": "q2", "task": "choice", "query": "where does it happen",
                "options": ["A. indoors", "B. outdoors"]}]}
JSON
vqa-cli --model videojev-4b-q8_0.gguf --video clip.mp4 --fps 2 --score qs.json

# VideoMAE:视频 → 动作标签(分类头版)/ 768 维嵌入(预训练版)
vqa-cli --model videomae-base-ft.gguf --video clip.mp4 --ask x --mode 5
vqa-cli --model videomae-base.gguf    --video clip.mp4 --ask x --mode embed

# V-JEPA 2:视频 → 1024 维特征 / SSv2 标签 / 掩码潜变量预测
vqa-cli --model vjepa2-f16.gguf --video clip.mp4 --ask x --mode embed
vqa-cli --model vjepa2-f16.gguf --video clip.mp4 --embed ""     # L2 归一化向量
vqa-cli --model vjepa2-ssv2-f16.gguf --video clip.mp4 --ask x --mode 5
vqa-cli --model vjepa2-f16.gguf --video clip.mp4 --mode predict \
        --ask "ctx=0:4096,tgt=4096:8192"                        # 预测后一半 token 的潜变量

# EmbeddingGemma-2:多模态嵌入(图像/视频/音频/文本 → 768 维,不生成文本)
vqa-cli --model embeddinggemma2-f16.gguf --tokenizer models/embeddinggemma-2 \
        --image photo.jpg --embed "describe" --embed-preset query          # JSON 输出嵌入向量
vqa-cli --model embeddinggemma2-f16.gguf --tokenizer models/embeddinggemma-2 \
        --video clip.mp4 --embed "summarize"                               # 1 fps 均匀采样 ≤32 帧
vqa-cli --model embeddinggemma2-f16.gguf --tokenizer models/embeddinggemma-2 \
        --audio clip.wav --embed "what is said" --embed-dim 256            # Matryoshka 截断
vqa-cli --model embeddinggemma2-f16.gguf --tokenizer models/embeddinggemma-2 \
        --embed-file docs.txt                                              # 批量:一行一条 → JSONL

# moondream3.1 就是一个普通后端:技能、批量、Web UI 全在 vqa-cli 上
vqa-cli --model models/moondream3.1-q6k.gguf --image photo.jpg --ask "What is in this image?"
vqa-cli --model models/moondream3.1-q6k.gguf --image photo.jpg --ask person  --mode detect
vqa-cli --model models/moondream3.1-q6k.gguf --image photo.jpg --ask normal  --mode caption
vqa-cli --model models/moondream3.1-q6k.gguf --image photo.jpg --ask "0.5,0.5" --mode gaze
vqa-cli --model models/moondream3.1-q6k.gguf --batch prompts.txt      # 纯文本批量
vqa-cli --serve 8080                                                  # 统一 Web UI
# --vision/--region 省略时按模型所在目录找 moondream3.1-vision/-region.gguf

vqa-cli 常用开关:--video <file> --fps 2 --max-frames 32(内置视频:ffmpeg 管道解码 + qwen_vl_utils 式采样 n = min(duration×fps, max_frames),超限自动全片均匀重采样)、--frames <dir>(预抽帧目录)、--temp/--top-k/--top-p(采样,默认贪心)、 --max-tokens、--n-ctx、--threads、--cpu、--dump-ids(只跑分词并打印 token id)、 --help 看全部。

moondream Web UI 覆盖官方全部五个技能:图像描述(简短/标准/详细)、提问(VQA)、 目标检测(绿框)、定位(黄色十字)、注视点预测(点选起点→预测注视点)。 对同一张图连续提问时自动复用视觉编码与 KV 前缀(~1.2s → ~66ms)。 HTTP API:POST /api/vision,mode=ask|caption|detect|point|gaze (caption 带 len=short|normal|long,gaze 带 x=&y= 起点坐标),body 为图片二进制。

双模型同驻:单槽 registry 一次只驻留一个模型,换载已异步化——POST /api/model/<id>/load 立即返回 {"loading":true},轮询 GET /api/model/<id>/status(loading 进度字节/ready/error), 换载期间 /ask 返回 409,加载前有显存守卫(预估 权重×1.1+KV,不足时给行动建议; SKYVISION_FORCE_LOAD=1 可越过)。两个大模型要同时在线时用双进程: scripts\serve-dual.bat——GPU 实例 --serve 8080 跑 MOSS-VL,第二实例 --serve 8081 --cpu 跑 Qwen2.5-Omni(CPU 全功能含 --speak),端口/绑定地址可用 SKYVISION_*_PORT/_HOST 环境变量改。

4. MCP 服务器(给 agent 用)

vqa-cli --mcp 把同一个引擎包装成 MCP(Model Context Protocol) 服务器,走标准 stdio + JSON-RPC(换行分隔),任何支持 MCP 的客户端(CodeBuddy / Claude Desktop / 自研 agent) 都能直接让模型"看图/看视频",不需要写粘合代码:

# 从仓库根目录启动;--models-dir 省略时用 ./models(不存在则用 .)
vqa-cli --mcp --models-dir models
# 也可以固定一个默认模型(工具调用里不写 model 时用它)
vqa-cli --mcp --models-dir models --model moondream3.1-q6k.gguf

客户端配置示例(mcpServers 条目)——用仓库自带的启动脚本最省事,它会切到仓库根目录、 挑一个真正支持 --mcp 的构建目录、把 ggml*.dll 所在目录加进 PATH 再启动:

{
  "mcpServers": {
    "skyvision": {
      "command": "cmd",
      "args": ["/c", "C:\\moondream\\scripts\\mcp-server.bat"],
      "env": { "FFMPEG": "ffmpeg" }
    }
  }
}

也可以直接指向二进制(这时要自己保证 DLL 能找到):

{
  "mcpServers": {
    "skyvision": {
      "command": "C:/moondream/build-ninja/vqa-cli.exe",
      "args": ["--mcp", "--models-dir", "C:/moondream/models"],
      "env": { "PATH": "C:/moondream/build-ninja/bin;%PATH%" }
    }
  }
}

共享库版构建把 ggml*.dll 放在 <build>/bin/,客户端从任意工作目录启动时不会自动找到, 所以要么走启动脚本,要么把该目录加进 PATH/把三个 DLL 拷到 vqa-cli.exe 旁边。 scripts/mcp-server.bat(Windows)与 scripts/mcp-server.sh(Linux/macOS/WSL)都支持 SKYVISION_BUILD / SKYVISION_MODELS 覆盖。视频工具另外需要 ffmpeg(FFMPEG 可指定 路径),纯图片工具不需要。

Docker(MCP 目录/沙箱跑的就是这个)

仓库自带 Dockerfile:多阶段构建出 CPU 版二进制,并内置 403MB 的 SmolVLM-256M q8, 所以镜像开箱就是个能真答话的 MCP 服务器:

docker build -t skyvision-mcp .                       # 内置 403MB smolvlm
docker build --build-arg MODEL_REPO= -t skyvision-mcp:bare .   # 不带权重(CI 用)
docker run --rm -i skyvision-mcp                      # 内置 smolvlm,直接可用
docker run --rm -i -v ~/models:/models skyvision-mcp   # 换成自己的 GGUF

入口是 vqa-cli --mcp --models-dir /models;内置模型就是 describe_image / ask_image 的兜底,挂进去的每个 *.gguf 都会出现在 list_models 里。镜像预设 SMOLVLM_MAX_EDGE=1024(官方预处理会放大到 2048,见 SmolVLM 那节:同样答案但慢 2.3 倍)。

完全没有权重时它也不会崩:initialize / tools/list / list_models(返回空列表) 都正常应答 —— 这正是目录爬虫、CI 冒烟测试和 MCP 平台审核所做的事。

内置权重默认从 Hugging Face 的 SkyContact/SmolVLM-256M-GGUF 拉取(魔搭有同款镜像), 换源或固定版本用 --build-arg MODEL_BASE=... --build-arg MODEL_REV=...; --build-arg MODEL_REPO= 则完全不下载(得到一个只有二进制、靠 -v 挂权重的镜像)。

.github/workflows/docker-mcp.yml 在 Linux 上真的构建 这个镜像并跑冒烟,所以 README 顶部的徽章是"真构建过",不是宣称。构建成本按三层摊开:

层

何时失效

说明

toolchain

只改 apt 包列表

apt lists 走 cache mount

ggml

只改 GGML_REF

ggml 独立成 stage 单独编译(-DGGML_PREBUILT_DIR 复用),改 src/ 不会碰它

build

改源码

COPY . + ccache(本地重复构建有效;CI 跨 runner 不复用 ccache,靠上面两层的层缓存)

CI 的推送构建默认不带权重(MODEL_REPO=):BuildKit 的 --mount=type=cache 在新 runner 上 是空的,带权重等于每次多传 400MB。完整镜像(含 describe_image 冒烟)走每周定时 + 手动 workflow_dispatch,缓存用 ghcr registry(mode=max,无 10GB 配额限制)。

工具

说明

list_models

列出可运行的 GGUF(架构/量化/体积/是否常驻)+ 已加载模型的能力位

load_model

显式加载(同一时刻只常驻一个模型;换模型会卸载旧的),可设 n_ctx / threads / gpu

describe_image

图像描述,length=short|normal|long

ask_image

自由提问(mode=ask-think 可开 moondream 的 grounded 推理),支持 session 多轮

detect_objects

目标检测,返回 JSON(归一化 0..1 + 像素坐标),需 moondream3 + *-region.gguf

point_objects

目标中心点,同上

gaze_point

由起点预测注视点,同上

parse_document

文档页解析(nemotron_parse):format=json(默认,markdown + 带归一化/像素框的 blocks)/ markdown / raw,LaTeX 表自动转 HTML

analyze_video

视频问答(ffmpeg 抽帧,fps / max_frames),Marlin 支持 mode=caption|find

classify_video

纯视频塔(VideoMAE / ViViT / V-JEPA 2)→ top-k 标签或 pooled 嵌入

约定:媒体参数既可以是本地路径,也可以是 data:image/...;base64,<data> / base64:<data>; 静止图片可以直接喂给 classify_video / analyze_video(当作单帧 clip,不需要 ffmpeg); 坐标类工具返回两层坐标(x1/y1… 归一化、x1_px/… 像素),方便直接画框。 工具失败以 isError: true + 可读原因返回(不占用 JSON-RPC 错误码),协议层错误才用 JSON-RPC error。

模型转换

所有转换器都在 convert/,共用 hf_source.py 定位检查点、_lib.py 提供 GGUF/量化工具, 日志统一 [convert] 前缀,写出后会重新打开文件校验张量表。

脚本

产出

convert_hf_to_gguf.py

moondream3.1-q8_0.gguf / -q6k / -q4k(文本主干,--q6 / --q4 / --q8-dense)

convert_vision.py

moondream3.1-vision.gguf(SigLIP 塔,~0.84GB)

convert_region.py

moondream3.1-region.gguf(detect/point/gaze 头,~30MB)

convert_videochat3.py

videochat3-4b-<outtype>.gguf(Qwen3 + VideoChat3 ViT,单文件,--outtype f16|q8_0|q6_k|q4_0)

convert_qwen3_5.py

Qwen3.5 / Marlin-2B 通用:<检查点>-<outtype>.gguf(混合主干 + Qwen3-VL 视觉塔,单文件,--outtype f16|q8_0|q6_k|q4_0,默认 q8_0;Marlin 检查点自动写入 marlin.skills 技能位)

convert_videojev.py

videojev-4b-<outtype>.gguf(cosine 打分头与特殊 token 一起写进 GGUF,--outtype 默认 q8_0)

convert_mage_vl.py

mage-vl-4b-<outtype>.gguf(Codec-ViT + Qwen3 文本主干,单文件,--outtype f16|q8_0|q6_k|q4_0)

convert_minicpmv47.py

minicpm-v-4.7-1b-<outtype>.gguf(SigLIP canvas 塔 + 窗口/投影 merger + Qwen3.5 文本主干,单文件,--outtype f16|q8_0|q6_k)

convert_moss_vl.py

Moss-VL-Realtime-<nf4-out>.gguf(官方 NF4 self 层权重反量化后重量化,默认 q4_k;--nf4-out / --bf16-text-out / --vision-out 分层选档)

convert_videomae.py

videomae-<outtype>.gguf(VideoMAE 视频塔,单文件,--outtype f16|q8_0|q4_0;分类头与 id2label 一起写进 GGUF)

convert_vjepa2.py

vjepa2-<outtype>.gguf(V-JEPA 2 编码器 + 可选 attentive pooler/分类器/predictor,单文件,--outtype f16|f32|q8_0|q4_0,f16 约 653MB;--no-predictor / --no-pooler 裁头;q/k 行按 RoPE 布局重排)

convert_qwen2vl.py

qwen2vl-<outtype>.gguf(单文件,--outtype f16|q8_0|q4_0,默认 f16)

convert_qwen3omni.py + convert_qwen3omni_talker.py

qwen3omni-30b-thinker-<outtype>.gguf + qwen3omni-talker-<outtype>.gguf(thinker 与 talker 分两文件,qwen25omni 先例;音频/视觉塔永久 F16,--quant-audio 已按精度策略撤除)

convert_qwen25omni.py

qwen25omni-<outtype>.gguf(thinker)+ qwen25omni-talker-<outtype>.gguf(talker+token2wav),--outtype f16|q8_0|q4_0、--vision-f32、--no-talker

convert_janus.py

janus-pro-1b-<outtype>.gguf(timm SigLIP 塔 + aligner + llama 主干,理解侧单文件,--outtype f16|q8_0|q4_0;生成侧 VQ 解码器不导出)

convert_nemotron_parse.py

nemotron-parse-<outtype>.gguf(C-RADIOv4-SO400M 塔 + 卷积 neck + 10 层 mBART 解码器,单文件,--outtype f16|f32;1152/4304 这类维度不是 256 的倍数,量化只对可分张量生效,默认 f16 ≈ 1.3GB)

convert_smolvlm.py

smolvlm-<outtype>.gguf(单文件,--outtype f16|q8_0|q4_0,默认 f16)

convert_embedgemma2.py

embeddinggemma2-<outtype>.gguf(四模态嵌入,单文件,--outtype f16|q8_0|q4_0;f16 约 1.5GB)

moondream3.1

读取官方 FP8 检查点 moondream/moondream3.1-9B-A2B:

  • FP8 专家张量逐位精确解码(与 PyTorch 的 e4m3 解码 bit 级一致)后重量化为 Q8_0, --q6 可选 Q6_K(约 7.6GB,12GB 显存首选)、--q4 可选 Q4_K

  • BF16 张量转 F16

  • FP8 的逐通道 scale 直接折叠进量化块

python convert/convert_hf_to_gguf.py          # → moondream3.1-q8_0.gguf(约 10GB)
python convert/convert_hf_to_gguf.py --q6     # → moondream3.1-q6k.gguf(约 7.6GB)
python convert/convert_vision.py              # → moondream3.1-vision.gguf
python convert/convert_region.py              # → moondream3.1-region.gguf

输出默认写当前目录(-o 可改,moondream 侧的 -o 里 %s 会替换成量化标签)。 Q6_K 量化器是本仓库自带的(gguf-py 至今只实现了 Q6_K 的解码,quants.quantize 会抛 NotImplementedError),python scripts/test_q6k.py 可自检块布局与量化误差。

VideoChat3-4B

单个 GGUF 装下全部张量:文本解码器(--outtype 量化,默认 q8_0)、 27 层 ViT 与 multimodal projector(保持 F16)、学习式位置编码表(F32,供双三次插值)。 分词器不打包进 GGUF,运行时从检查点目录的 tokenizer.json 读取(--tokenizer <dir>)。

python convert/convert_videochat3.py --outtype q8_0   # → videochat3-4b-q8_0.gguf(4.83GB)
python convert/convert_qwen3_5.py --src models/Qwen3.5-2B   # → qwen3.5-2b-q8_0.gguf(2.67GB,Qwen3.5/Marlin 通用)
python convert/convert_videochat3.py --outtype q6_k   # → 3.92GB
python convert/convert_videochat3.py --outtype q4_0   # → 2.95GB

q4_k / q5_k / q2_k 目前不可用(上游 gguf-py 只实现了它们的解码), q6_k 走本仓库的 convert/q6k.py。Q8_0 及以上精度更高,Q6_K/Q4_0 更省显存与带宽。

VideoMAE

读取任意 transformers videomae 检查点(MCG-NJU/videomae-base、MCG-NJU/videomae-base-finetuned-kinetics 等):

python convert/convert_videomae.py --src models/videomae-base-ft   # → videomae-f16.gguf(约 190MB)
  • Conv3d patch 嵌入折叠成一次 matmul(权重 [dim, C*tubelet*patch*patch]), token 顺序 (t', h', w')、元素顺序 (c, dt, dh, dw),与卷积权重展平顺序一致

  • 位置表不是权重:VideoMAE 用的是原始实现里的固定 sin-cos 表(get_sinusoid_encoding_table, 一维序号 0..1567),转换器现场生成后以 F32 存入 GGUF

  • 没有 cls token:pooled 取 token 0(use_mean_pooling 的 fine-tune 才做均值 + fc_norm)

  • q/k/v 三块权重在转换期拼成 v.blk.{i}.wqkv.weight,bias 是 q_bias | 0 | v_bias (key 无 bias,这是 VideoMAE 特有的写法)

  • 池化分两种,跟着检查点走:预训练版 use_mean_pooling=false → 取 token 0 且有末尾 LayerNorm(v.norm);*-finetuned-kinetics 这类 use_mean_pooling=true 的微调版 → 对全部 token 求均值、没有末尾 LayerNorm,改用 fc_norm(eps 1e-5)。转换器按张量 实际存在与否写 videomae.has_final_norm / videomae.use_mean_pooling

  • 分类头的 id2label 写成 videomae.labels 字符串数组,引擎按它打印标签名; 没有 classifier.* 的预训练检查点转换后只出嵌入

  • --mode N 打印 top-N(默认 5),--mode embed 输出 768 维 pooled 嵌入(JSON 数组); --ask 对这类模型没有意义,会被忽略

  • 图只建一次:clip 形状固定(16 帧 → 1568 token),load() 里建好静态图后每次只回填输入 张量。这不只是省掉 per-clip 的建图/分配(实测 <0.5 ms,可忽略),更是 CUDA graph capture 的前提——ggml 的 capture key 是 nodes[0],图对象必须跨调用存活

  • CPU 上想更快就量化权重:--outtype q8_0(~100 MB,i7-14700K 上 850 ms → 590 ms, Kinetics-400 的 top-1 概率只差 4e-4)。f16 更省事、GPU 更快,所以仍是默认

  • 实测(i7-14700K,19 物理核,单 clip 1568 token × 12 层):f16 约 0.85 s、q8_0 约 0.59 s

对拍:python scripts/ref_videomae.py --model <hf_dir> --video clip.mp4 --out ref_videomae 导出官方实现的 v_patch_out / v_blk0_out / v_final / v_pooled / v_logits / v_probs, 再用 vqa-cli --dump-dir ref_videomae 跑同一段视频,逐级比 cpp_*.bin。 本仓库提交前已用另一条独立通道(numpy 从 GGUF 权重复算整条前向,preprocess 用引擎自己的 patches 导出)验证:12 层全长 maxdiff 8e-3(值域 ~13),patch 嵌入 3e-3,block0 注意力 3e-4,Kinetics-400 分类头 400 个概率与引擎最大差 8.7e-6——都在 F16 权重噪声量级。

ViViT

google/vivit-b-16x2 在 HF 里就是"ViT + tubelet 卷积 + cls token",所以直接复用 videomae 后端,差别全部走 GGUF 元数据:

python convert/convert_vivit.py --src models/vivit-b-16x2      # → vivit-f16.gguf(约 183MB)
  • cls token:学出来的参数(v.cls_token.weight),在 patch 嵌入之后、位置相加之前 concat 到序列最前面;位置表本身有 3137 行(含 cls)

  • 位置表是学出来的参数(vivit.embeddings.position_embeddings),与 VideoMAE 的固定 sin-cos 不同

  • 激活是 gelu_fast:注意 transformers 的 FastGELUActivation 是tanh 形式,不是 x·sigmoid(1.702x)(后者叫 quick_gelu)——一开始按 sigmoid 理解导致 block0 就对不上, 用 numpy 复算三种激活才定位到

  • LN eps 1e-6;q/k/v 三路都有 bias(VideoMAE 的 key 没有)

  • 预处理:短边 256 → center crop 224,双线性重采样(preprocessor_config.json 里 resample: 2 是 PIL 的 BILINEAR,不是 BICUBIC),零中心化(等价 mean=std=0.5)

  • 32 帧 → 3136 patch token + 1 cls;CPU(19 核)单 clip 约 2.2 s

对拍:python scripts/ref_vivit.py --model models/vivit-b-16x2 --image photo.jpg --out ref_vivit, 引擎 --dump-dir ref_vivit 跑同一张图,实测 v_patch_out mean|d| 3.5e-3、block0 1.0e-2、 12 层全长 1.4e-2(值域 22,corr 0.99977)、400 类概率最大差 6.0e-3(top-5 标签顺序一致), 残差全部来自重采样舍入(像素级差 ~1.8 灰阶)。

V-JEPA 2

hf download facebook/vjepa2-vitl-fpc64-256 --local-dir models/vjepa2-vitl-fpc64-256
python convert/convert_vjepa2.py --src models/vjepa2-vitl-fpc64-256 -o vjepa2-f16.gguf   # 608MB
vqa-cli --model vjepa2-f16.gguf --video clip.mp4 --ask x --mode embed   # 1024 维特征 JSON

预转好的权重(免转换直接下): hf download SkyContact/VJEPA2-ViT-L-fpc64-256-GGUF(编码器 + masked-latent predictor,魔搭同名)· hf download SkyContact/VJEPA2-ViT-L-fpc16-256-SSv2-GGUF(SSv2 174 类分类微调,魔搭同名)。

这是纯编码器(世界模型的一半):输出 1024 维 mean-pooled 视频特征,不生成文本、也没有分类头。

  • Conv3d(kernel=stride=(tubelet,patch,patch))折成一次 matmul,token 顺序 (t', h', w')、 元素顺序 (c, dt, dh, dw),与 videomae 共用同一份 clip 打包代码(videomae_preprocess)

  • 没有位置表,位置在层内的 RoPE 里:head_dim 64 切成 20(时间) + 20(高) + 20(宽) + 4(不动), 规则是 seg = 2*((head_dim//3)//2);token 的 (t', h', w') 网格下标就是三趟旋转的 position id

  • RoPE 有一个必须照抄的上游 bug:rotate_queries_or_keys 里 repeat(1,1,1,2) 是平铺 而不是交织,于是相邻维对 (2u, 2u+1) 的偶分量用频率 2u、奇分量用 2u+1(不是正交旋转)。 Meta 源码注释明确写了"修正会破坏与预训练权重的兼容性"(vjepa2 PR #15),所以必须复现。 转换器把 q/k 的每个旋转段重排成 [偶数维 | 奇数维],引擎就能用 mul/add 直接算, q 和 k 用同一重排 → 注意力打分不受影响(GGUF 里写 vjepa2.rope_layout = "even-odd", 引擎加载时校验,不匹配直接报错而不是静默出错)

  • 4096 宽的 MLP 是 hidden_size * mlp_ratio,hidden_act = gelu 是 erf 精确版;LN eps 1e-6; q/k/v 与输出投影全部带 bias

  • 预处理跟 videomae 不同:短边缩到 292(video_preprocessor_config.json 的 size.shortest_edge,resample: 2 双线性)再中心裁 256;default_to_square 在 size 是 dict 时不生效(实测 HF 仍按短边等比缩放,已按此实现)

  • 64 帧 × 256² = 8192 token,全时空注意力单层就是 16 头 × 8192²,CPU 上单 clip 25-30 s (19 核);GPU 上建议直接跑 f16

  • 张量:v.patch_embd.* / v.blk.{i}.{ln1,wqkv,wo,ln2,fc1,fc2} / v.norm.*,共 292 个

  • predictor.*(12 层、pred_hidden_size=384,含 mask token 与 1024↔384 投影)只服务预训练的 masked latent prediction,转换时整体丢弃(199 个张量),需要 rollout/规划再接

三个头(转换器按权重自动识别,--no-predictor / --no-pooler 可裁):

  • attentive pooler(*-ssv2 这类 VJEPA2ForVideoClassification 才有):先 N 层 普通 pre-norm 自注意力(无 RoPE、无 mask)跑完整 token 流,再用一个学出来的 query token 对整段做 cross attention——注意这个 cross attention 没有输出投影 (上游注释点明),query 本身也不做 LN,只有被查询的流走 LN;随后 query 残差 + MLP

  • 分类器:pooler 输出过一个线性层 → --mode N 打印 top-N 标签,id2label 写进 vjepa2.labels;嵌入口径也跟着变:有 pooler 时 --mode embed/--embed 给 pooler 输出 (官方 get_vision_features 同样这么选)

  • masked-latent predictor(12 层 d=384,含 mask token 与 1024↔384 投影,f16 约 45MB): --mode predict --ask "ctx=0:4096,tgt=4096:8192[,mask=1]"(区间写 a:b 半开、 a-b 闭区间、多项用 / 分隔)。流程与官方一致:取编码器流在 ctx token 上的特征 → 1024→384 投影 → 拼上 mask token → 按位置排序 → 12 层窄栈(自己的 RoPE 表)→ LN → 反排序取 target token → 384→1024 投影。predictor 图按 (n_ctx, n_tgt) 缓存,换掩码形状才重建 (~10 ms),每次只回填索引与表

  • 坑:transformers 的 VJEPA2Predictor.forward 不会把 mask_index 传下去,永远用索引 1 (发布权重里 1..9 号 mask token 全零,只有 0 号有范数 0.769)——引擎默认也用 1 以对齐, mask=0 才用那颗真训过的 token

  • 第二个坑:predictor 的 head_dim 是 32 → 每段只有 seg/2 = 5 个频率,频率表长度是奇数, 奇分量频率 (2u+1) % 5 会回绕(不是 偶分量 + 1)。编码器 seg=20 时 seg/2=10 为偶数, 这个差别看不出来,只有 predictor 会错

对拍:python scripts/ref_vjepa2.py --model <hf_dir> --video clip.mp4 --out ref_vjepa2 导出 v_patch_out / v_blk0_out / v_final / v_pooled(微调版再加 v_logits / v_probs), 以及 ref_p_latents.npy + predict_spec.txt(直接喂 --mode predict --ask "$(cat ...)"); vqa-cli --dump-dir ref_vjepa2 跑同一段视频逐级比 cpp_*.bin(按 ggml 的 ne 顺序读, 二维张量是 [dim, tokens] 列主序)。实测(ViT-L/fpc64-256,本机 19 核):

对照

结果

同输入(把引擎的 patches 灌回官方实现)24 层全长

cos 1.000000、max 0.065 —— 图实现本身没有偏差

端到端 v_patch_out / block0

相对差 6.9e-4 / 7.1e-4

端到端 24 层全长

逐 token cos 0.91(8192 token 全注意力对亚灰阶输入差极敏感,均值池化后收敛)

mean-pooled 特征

cos 0.9994~0.9998

SSv2 pooler + 174 类头

top-1 标签一致(0.916 vs 0.921),概率最大差 4.5e-3,top-4 集合一致

predictor(4096 / 1024 target)

cos 0.998,max 0.5

端到端残差全部来自重采样:引擎走 PIL 语义,HF 的 video processor 走 torch 抗锯齿, 像素级 mean 差 1.7e-4(max 0.0175 归一化 ≈ 半个灰阶)。想要更硬的对照就用 --outtype f32(1.2GB)+ 同输入法。调试:VJEPA2_MAX_LAYERS=N 只跑前 N 层, --dump-dir 额外导出 v_blk0_qkv / v_blk0_q_rope / v_blk0_k_rope / v_blk0_attn / v_logits / v_probs。

复用了什么:block 体、flash-attn 接线、成对旋转 RoPE 全部来自 core/vit_block.hpp (moondream / VideoChat3 / VideoMAE / ViViT 同一份,本后端只补了「第二条角度表」和 「无 rope」两个开关);patch 打包直接用 videomae 的 pack_clip;卷积折 matmul、ggml 的 flash_attn_ext / norm / gelu_erf / mul_mat / get_rows / concat、GGUF 读写 (gguf + _lib)都与其它后端同源。真正手写的只有本架构特有的图接线(三维 RoPE 表、 pooler 的 cross attention、predictor 的排序/反排序)——ggml 的 ggml_rope_* 表达不了上游那个 「频率平铺」的 bug,所以旋转只能落成 mul/add。

Qwen2.5-VL

python convert/convert_qwen2vl.py --src models/qwen2.5-vl-3b     # → qwen2vl-f16.gguf(7.5GB)
vqa-cli --model qwen2vl-f16.gguf --tokenizer models/qwen2.5-vl-3b \
        --image photo.jpg --ask "How many people are in the image?"

视觉塔(Qwen2.5 ViT)和文本主干(Qwen2)各自的图都照 llama.cpp / HF 的结构实现,踩到并修掉的 三个坑值得记下来,它们都属于"数值看着像、结果全错"的类型:

  1. patch 排布是 2×2 分组优先。HF 的 Qwen2VLImageProcessor 会 reshape(grid_t, gh/2, 2, gw/2, 2, C, T, P, P) 再 transpose(0,3,6,4,7,2,1,5,8), 所以 pixel_values 的行序是 (t, gh/2, gw/2, 2, 2),不是光栅序。按光栅序打包会让 图像"看起来还在",但特征全错。

  2. merger 的 RMSNorm 是逐 token 1280 维,归一化完才 reshape 成 5120 喂 fc1。先 reshape 再 norm(5200 维上归一化)会静默毁掉特征——这一条让我在"塔输出 corr 0.98 但答案错"上卡了很久。

  3. KV cache 的槽位 ≠ MRoPE 的 t 轴。图像 token 的 t 坐标全是同一个值(st),拿它当 ggml_set_rows 的索引会把 612 个 token 全写进同一格。槽位必须单独用 0..T-1。

另外:预处理用 CLIP mean/std(不是 0.5)、bicubic 缩放、smart_resize 到 28 的倍数; 文本是 section MRoPE(16/24/24),图像 token 位置 (st, st+h, st+w),图像结束后 st += max(gh, gw), 解码时下一个位置取 max(pos)+1 而不是 T。

对拍:python scripts/ref_qwen2vl.py --model models/qwen2.5-vl-3b --image photo.jpg --out ref_q2v, 引擎 --dump-dir ref_q2v 跑同一输入。实测:patch 逐元素一致、block0 maxdiff 0.16(值域 15)、 塔输出 corr 0.982(值域 2.3e4)、merged 特征 corr 0.9935、首 token 命中官方 argmax; 问答结果与官方一致("There are five people in the image.")。调试开关:Q2V_MAX_VLAYERS=N 截断视觉层数、Q2V_FEATS=<file> 直接用磁盘上的特征(隔离塔与文本侧)、Q2V_DEBUG=1 打印 token 与首 token top5。

SmolVLM(最小可用后端)

256M 的 Idefics3 模型,量化后 ~270 MB,适合塞进容器/CI 里当"永远能跑"的 demo:

python convert/convert_smolvlm.py --src models/smolvlm-256m        # → smolvlm-256m-f16.gguf(515MB)
python convert/convert_smolvlm.py --src models/smolvlm-256m --outtype q8_0   # ~270MB
vqa-cli --model smolvlm-256m-f16.gguf --tokenizer models/smolvlm-256m \
        --image photo.jpg --ask "How many people are in the image?"

结构(都是实测的,不是照文档推的):

  • 视觉塔是 SigLIP-base:Conv2d(3,768,16,16) 折成一次 matmul(带 bias)、学出来的 32×32 位置表(无 cls)、12 层 LayerNorm 带 bias + 注意力 qkv/out 全带 bias(没有 rope)+ 普通 MLP(gelu_pytorch_tanh,不是 SwiGLU)

  • 连接器是 4×4 pixel shuffle → 一次 Linear(12288→576),所以每张 512² 瓦片恰好 64 个文本 token

  • 文本主干是 SmolLM2-135M:30 层、d=576、9/3 heads、所有投影都没有 bias、RMSNorm eps 1e-5、rope theta 1e5、lm_head 不共享

预处理照官方 Idefics3(这条最容易做错):先把长边缩到 2048(不是 512),再把两边补齐到 512 的倍数(第二次缩放会拉伸、不保比例),切成 512² 瓦片(行优先),最后追加一张整图的全局视图;<image> 会展开成 <fake><row_R_col_C><image>×64(每块)与结尾的 <fake><global-img><image>×64<fake>。960×504 的样图是 4×3 瓦片 + 全局 = 13 块 / 832 token,与官方处理器逐项一致。

分词器是 SmolLM2 的 Digits + ByteLevel/GPT-2 regex,为此给 core/tokenizer 加了第三种模式(Gpt2Regex)——和 Qwen 的差别在"标点能否前缀字母"和标点后是否吞 CR/LF;三种测试串(特殊 token / 缩略语 / 数字 / 制表符 / 多空格)与官方分词器逐 id 相同。

对拍:python scripts/ref_smolvlm.py --model models/smolvlm-256m --image photo.jpg --out ref_smolvlm, 引擎 --dump-dir ref_smolvlm 跑同一输入。实测像素 corr 0.99997(maxdiff 0.046,来自第二次缩放我们把中间结果取整回 u8)、last-token logits corr 0.99991(maxdiff 0.23),生成文本与官方逐字相同(" There are five persons in the image.")。

性能(CPU 19 线程,f16 权重,960×504 样图):

配置

图像块

端到端

SMOLVLM_MAX_EDGE=2048(官方预处理原样)

4×3 瓦片 + 全局 = 13

8.6 s

默认(长边上限 1024)

2×2 + 全局 = 5

3.8 s

SMOLVLM_MAX_EDGE=512

1×1 + 全局 = 2

1.6 s

三档答案完全相同。开销几乎全在视觉塔的全注意力:一块 1024 token = 2·1024²·64·12 ≈ 1.6 GFLOP/层 × 12 层 ≈ 19 GFLOP,所以"块数"就是时间,--dump-dir 那种逐块模式并不会更慢。

顺带记录一个反直觉的结论:把所有瓦片批合并成一次前向(batch = 块数)在 CPU 上几乎没有收益(8.7 s → 8.7 s)——瓶颈是 FLOP 不是调度,批合并只是少几次 launch;代码保留了批处理形式(GPU 上才有意义),并且 --dump-dir 时自动退化成单块以保证可与官方逐级对比。

模型权重遵循各自的许可证(moondream 为 Moondream Model License 1.0),不在本仓库分发。 该许可明确允许量化、转换、再分发、自托管与商用集成;仅对公众开放的通用 moondream API 转售需要单独授权——那是部署者的义务,与本引擎无关。

Qwen2.5-Omni

python convert/convert_qwen25omni.py --src models/qwen25omni/hf --outtype f16 --vision-f32
# → qwen25omni-f16.gguf(thinker)+ qwen25omni-talker-f16.gguf(talker+token2wav+音色)
vqa-cli --model qwen25omni-f16.gguf --tokenizer models/qwen25omni/hf \
        --image photo.jpg --audio clip.wav --ask "What is shown?" \
        --speak reply.wav          # 语音输出(24kHz wav,默认音色 Chelsie)

thinker 侧:文本主干走 QWEN2VL 同构图(注意 lm_head 不 tied,转换器写 t.output.weight); 音频塔与原味 whisper 的三点差异都按 HF 5.20 官方实现复刻——mel 按 n_window×2=200 帧分块后 逐块独立卷积(ggml 侧用 4 零列 + conv1 后零化边界两列精确复现)、块内注意力、帧对 (2i, 2i+1) 平均池化(floor 语义,奇尾帧丢弃)。ggml_conv_1d 的三要素(kernel 字节 = torch [OC,IC,K] 原样、 数据通道连续、输出 [T,C] 且 bias 在转置后加)在 qwen25omni_audio.cpp 顶部有完整注释。

talker 侧:预填只有 3 个位置([h0+e0, bos+codec_pad, h1+e1+codec_bos]),prompt 只通过 rope 绝对 偏移起作用;codec 采样 temp 0.9 / top-k 40 / top-p 0.8 / 重复惩罚 1.05。Token2Wav 的 DiT 走 ggml 图 (AdaLN-zero、block=24 分块注意力 mask、RoPE),BigVGAN 与 ECAPA 说话人塔是宿主 C++(抗锯齿激活的 分组 FIR 没有 ggml 算子;kaiser 滤波器按 HF 公式现算)。已知差距:语音数值尚未与 HF 对齐(HF 的 token2wav 在 CPU bf16 下有 dtype 问题,参考输出待补),音量偏小,BigVGAN 的 snake/fir 语义值得用 官方 fp32 权重再核对一遍。

量化版:

精度

thinker / talker

验证(多模态消融,test-1.jpeg + 4 s wav vs HF bf16 ref)

用法

f16

11.2 GB / 1.66 GB (q8)

logits0 corr 0.9946;音频塔 corr 0.9997;贪心答案与 HF 一致

12G 卡可跑(配 q8 talker)

q8_0

5.64 GB / 1.66 GB

与 ggml-org 官方 Q8_0 逐张量 bit 级一致;logits corr 0.9928(CPU/GPU 均 0.993);音频塔 corr 0.9992;答案与 f16 相同

CPU 全功能(含 --speak);12G 卡全功能

q4_0

3.63 GB / 1.50 GB

logits corr 0.9475;音频塔 corr 0.9714;答案为同一事实的更短表述

6G 卡文本→语音整链;12G 卡全功能

语音输出(全部量化档):token2wav 修复后 wav rms 0.16-0.31(HF 量级 0.08-0.10), 贪心解码对 talker 双方(HF 与本引擎)都是退化解,采样(temp 0.9/top-k 40/top-p 0.8) 下 EOS 正常。e2e 多模态 QA+7.9 s 语音在 12G 卡 54 s(q8,宿主声码器已并行化)。

GPU 上所有"宿主直读权重"的位置(mel 滤波器、BigVGAN/ECAPA/spk)都必须走 ggml_backend_tensor_get——GPU 上 t->data 是空指针,tensor_to_f32 直读必崩。

Qwen3-Omni

python convert/convert_qwen3omni.py --src models/qwen3-omni/hf --outtype q6_k
python convert/convert_qwen3omni_talker.py --src models/qwen3-omni/hf --outtype q8_0
vqa-cli --model qwen3omni-30b-thinker-q6_k.gguf --tokenizer models/qwen3-omni/hf \
        --image photo.jpg --audio clip.wav --ask "What do you see and hear?" \
        --speak reply.wav --speaker ethan --max-codes 400

thinker 侧三模态与官方逐段对拍(图像 1200 patch→300 token、音频 400 mel→52 token、文本 1 层 parity corr 0.9997+)。talker 侧全部协议坑都在 qwen3omni_talker.hpp 头注释里:双轨轨迹(embedding + 第 24 层 隐层)→ ResizeMLP 投影 → 预填九行(3 文本 + 4×tts_pad + bos + 首生成 token,错位加 6 个 codec 特殊嵌入); codec_head 与 predictor 的 15 个头都不 tie(checkpoint 两侧都在且数值不同,HF tie_weights 明确不 tie——当初按"embedding wins"强 tie 导致引擎与 oracle 双双输出无意义音节,是该后端最后一处也是最大一处 协议坑)。声码器为单张 ggml 图(code2wav_graph),QOMNI_TRAJ_DUMP 可导出轨迹供 parity/qwen3omni/hf_speak_oracle.py 做 HF 全链 A/B。CPU(Q6_K)参考速度:decode 6+ tok/s、 --speak 全程(30 帧)约 15-25 s,其中声码器仅约 3 s。

EmbeddingGemma-2(四模态嵌入)

modelscope download google/embeddinggemma-2 --local_dir models/embeddinggemma-2   # 或 hf download
python convert/convert_embedgemma2.py --src models/embeddinggemma-2 -o embeddinggemma2-f16.gguf

740M(文本 270M + 视觉 170M + 音频 300M),不生成文本,只出嵌入。要点(都是对拍实测的):

  • 文本塔是 Gemma3 风格编码器:24 层 d=512、GQA 4:1、SWA(1024)/full 交错、逐层 PLE (per-layer embedding:12288→512 投影 + RMSNorm,每层一份加到隐状态上)、末尾均值池化 + 投影到 768

  • 视觉塔 Gemma4 ViT:16 层 patch16、轴向 2D RoPE(x/y 各 32 维,theta 100)、3×3 位置池化; 图像预算 280 soft、视频预算 140/帧(vision.video_max_soft_tokens);帧间注意力严格不跨帧

  • 音频塔 USM conformer:128-bin log-mel → 双 3×3 stride-2 卷积子采样 → 12 层 (macaron FFN×2 + LightConv1d(GLU + 因果深度卷积) + chunk=12 局部注意力 + softcap 50), 相对位置偏置是 Transformer-XL 式 rel-shift(每头独立的 rel-k 投影), 所有线性层都是 ClippableLinear(检查点里带 finite input/output clamp 标量,转换器逐个导出)

  • 占位块:<|image|>/<|video|>/<|audio|> 展开成 <|image> + N×soft + <image|>, prompt 里没写标记时媒体块追加在文本后

  • 四模态 parity(fp32 HF 参考,f16 权重):文本 0.9999999 / 图像 0.99999 / 音频 0.99999 / 视频 0.9999998。音频塔曾长期停在 0.84——公式(全矩阵 + 13 条对角线 mask 等价 rel-shift)先用 numpy 验证过是对的,真凶是 5 个 ggml 图实现 bug,逐层 hook dump 定位 (详见提交 7af56f7);视频的坑是 rope 位置轴、预处理预算、占位块展开与跨帧注意力(1b8680a)

  • Matryoshka:--embed-dim 512|256|128|64 截断;--embed-preset query|document|sts| classification|clustering 套官方 sentence-transformers 任务前缀

  • 性能(f16,3060 Laptop 6GB / i7 8 线程,含 ~1.2s 权重上传):GPU 文本 1.2s、图像 1.4s、 30s 音频 1.5s、32 帧视频 3.9s;CPU 图像 4.0s / 32 帧视频 39s——多模态走 GPU 收益 ~10×。 全路径 flash attention:CUDA fattn 要求 head_dim 256/512 的 KV 长度是 256 的倍数,序列按 标准做法补齐到 256 倍数、mask 屏蔽补齐位(均值池化只算真实 token),GPU 与 CPU 数值一致 0.999996+。文本 n_ctx 默认 8192(32 帧视频 ≈3.9k token)。CPU 上 q8_0 权重只快 ~8% (瓶颈在 f32 attention,不在权重带宽)

Nemotron-3.5-Parse(文档解析)

0.63B 的文档解析模型,一个 1.3 GB 单文件 GGUF 把整页扫描件/截图/PDF 页变成带版面类别与坐标的 结构化结果。它只有一套任务、提示词固定(6 个控制 token),不接受自由问答;一次一页。

# 预转好的:hf download SkyContact/Nemotron-3.5-Parse-GGUF --local-dir models/Nemotron-3.5-Parse-GGUF
#            (含 tokenizer;魔搭同名 modelscope download --model SkyContact/Nemotron-3.5-Parse-GGUF)
pip install -r convert/requirements.txt
hf download nvidia/Nemotron-3.5-Parse --local-dir models/Nemotron-3.5-Parse   # 权重 + tokenizer
python convert/convert_nemotron_parse.py                      # → nemotron-parse-f16.gguf(1.3GB)
python convert/convert_nemotron_parse.py --outtype q8_0       # → 866MB

vqa-cli --model nemotron-parse-f16.gguf --tokenizer models/Nemotron-3.5-Parse \
        --image page.png --ask parse --n-ctx 8192 --max-tokens 4000       # 原始标记
vqa-cli ...  --mode markdown      # markdown,LaTeX 表格转成 HTML 表
vqa-cli ...  --mode json          # blocks + 归一化/像素 bbox

结构(都是照官方检查点核过的):

  • 视觉塔是 C-RADIOv4-SO400M:27 层 ViT、patch16、d=1152,10 个 class token 排在最前,位置是学出来的 128×128 表按内容区插值(NaViT 变分辨率),没有 rope、GELU 用 erf 形式、塔末没有 norm

  • neck:1×1 Conv1d(1152→1024) → LN → 按列 4 合 1(折成 matmul)→ LN;前两个 class token 投影成 1 个 summary token 追加在末尾

  • 解码器是 10 层 mBART 式前缀解码:d=1024、16/8 GQA、pre-LN、√d 嵌入缩放、tied lm_head, 全程没有位置编码也没有 rope——次序只由因果掩码给出,视觉特征就是 KV 前缀

  • 输出是控制 token 流:<x_0.0586><y_0.0539>Quarterly Report 2026<x_0.1621><y_0.0633><class_Text>, 坐标是内容区归一化值;表格以 LaTeX tabular 出现,由 --mode markdown/json 转换(Title→#、 Section-header→##、表头按「首行以文字为主」提升为 <th>)

对拍:python scripts/ref_nemotron_parse.py --model models/Nemotron-3.5-Parse --image page.png --out ref_np 出官方栈的逐级 dump,引擎 --dump-dir 跑同一输入。位置场逐元素一致(2.7e-7)、patch 嵌入 2.8e-3 (f16 权重噪声)、首 token logits top8 8/8,整页 159 步贪心与官方逐 token 相同。

性能(1000×1300 一页,1313 visual token / 158 输出 token):

视觉塔

prefill

decode

CPU(19 线程)

16.3 s

0.83 s

11.0 ms/token(91 tok/s)

CUDA(RTX 4070 SUPER)

0.64 s

0.019 s

1.6 ms/token(620 tok/s)

整页端到端 CPU ≈ 19 s、GPU ≈ 0.9 s。视觉塔开销随视觉 token 数平方增长,NEMOTRON_PARSE_MAX_EDGE=1024 能把长边降下来换速度;--max-tokens 要给够(整页 2–8k,截断前会告警)。q8_0 权重在这里比 f16 慢 (CPU 视觉塔受 flash-attention 限制、GPU decode 受 kernel 延迟限制,都吃不到权重带宽),只建议用来省体积。

图表块按官方口径渲染:<chart …> 里的 markdown 管道表转成 <table>,title: 变成它的 <caption>、 subtitle: 追加进同一个 caption;紧跟图表上方的单行 Caption 会按官方规则(水平重叠 ≥80%、垂直间隙 ≤0.03、唯一最近)并入该图表的标题;List-item 的行首符号归一成 - 、文本角色里的软连字符会去掉。 阅读顺序就是模型给的顺序(上游 merge.py 是另一件事——把 direct 预测和 detector/crop 预测两路合并, 需要第二个模型)。一次一页(多页由调用方循环)。

Nemotron-3-Nano-Omni-30B-A3B(全模态 Reasoning)

33B 总参 / 每 token 激活 ~3B 的全模态推理模型:图/音频/文本进、纯文本出(无 talker)。本引擎的 一条龙:52 层 Mamba2/Attention/MoE 混合主干 + C-RADIOv4-H 视觉塔 + Parakeet 音频塔 + EC 专家 驻留(NEMOTRONOMNI_EXPERTCACHE=1),transformers 5.21 官方栈做 oracle。RoPE 判决实验的结论是 NoPE(config 与 HF 代码一致,llama.cpp GGUF 头里的 rope 84/128 元数据是家族模板残留; partial84 替代路径在媒体注入 step0 即 cos 0.85、完全丢失转写内容)。Mamba2 的 dt 走 inverse-softplus 预补偿(ggml_ssm_scan 内部自带 softplus 无 clamp,包一层把 HF 的 clamp(min=1e-3) 语义精确搬进 kernel)。

# 预转好的:modelscope download --model SkyContact/Nemotron3-Nano-Omni-30B-A3B-GGUF
#          --local-dir models/Nemotron3-Omni-GGUF(HF 同名;含 tokenizer.json)
pip install -r convert/requirements.txt
hf download nvidia/Nemotron-3-Nano-Omni-30B-A3B-Reasoning-BF16 --local-dir models/nemotron3omni
python convert/convert_nemotronomni.py --out-type f16         # 66 GB(对拍基线)
python convert/convert_nemotronomni.py --out-type q8_0        # 35.2 GB(全兼容基准档)
python convert/convert_nemotronomni.py --out-type q6k_mixed   # 23.3 GB(混合档,见下)
python convert/check_nemotronomni.py --gguf <产物> [--hash]    # 转换后自检(不加载模型)

vqa-cli --model nemotronomni-q8_0.gguf --tokenizer models/nemotron3omni \
        --image photo.jpg --ask "What is in this picture?"
vqa-cli --model nemotronomni-q8_0.gguf --tokenizer models/nemotron3omni \
        --audio clip.wav --ask "Transcribe this audio."

量化(为什么没有纯 Q4_K/Q6_K 档):本模型行宽 2688/1856/3712 都不是 256 的倍数,k-quant (%256)只能覆盖 5% 的字节,裸 Q4_K_M/Q6_K 档无意义;所以是逐张量混合:

档

大小

逐张量类型

q8_0

35.2 GB

全部 %32 matmul 操作数 Q8_0(全兼容基准)

q6k_mixed

23.3 GB

token_embd/lm_head Q8_0 + 专家堆叠 Q5_0 + %256 宽(塔/主干/out_proj 等)Q6_K + 其余 %32 宽 Q5_0;4-tap ssm conv、4D 音频子采样卷积等宿主直读尾巴保持 F16(fold 路径硬约束,引擎有类型守卫)

对拍(oracle = transformers 5.21 官方栈 BF16):文本 prompt A/B 贪心 argmax 100% 一致; 音频转写与官方逐字一致(F16);EC 专家驻留 NEMO_XCHK logits maxdiff 0.00000、104/104 层输出 位级一致。量化档在两 prompt 上的贪心流与 F16 基线一致性见下表。

性能(CPU 16 线程 / CUDA 5070 Ti Laptop 12GB EC split,156 slot):

F16

q8_0

q6k_mixed

CPU decode

0.33 tok/s

0.95 tok/s

3.39 tok/s

CUDA EC split decode

0.14-0.19 tok/s

2.3 tok/s

3.1 tok/s

F16 下 66GB 权重对 31GB 内存全靠 mmap 缺页(墙 ~350MB/s,CUDA EC split 的 swap-wait 占 95%); 量化把每步专家流从 ~2GB 压到 ~1GB(Q8_0)/ ~0.7GB(q6k_mixed),是可用性的分水岭。CUDA 档注意 slot 预算:默认 2GB 在 Q8_0 下只剩 236/519 slot 会抖动(14s/step),NEMO_EC_RESERVE=4e9 让 276 个逐步 slot 全驻留后 warmup 一次即 427ms/步(2.3 tok/s)。

性能提示:EC 路径设 VQA_POLL=100(CPU 线程 spin 轮询,免掉每 op 唤醒同步)可再提速——NVMe 上实测 1.86×,NEMO_XCHK 逐位一致;权重大于内存时权重放 NVMe 本地盘(同一份权重 USB 外接盘 vs NVMe 实测差 4.8×)。单步耗时可用 NEMOTRONOMNI_STEP_TIME=1 在进程内打点(stderr);专家内核吞吐上限可用 tools/moe_cpu_bench 免模型单独测。

MiniCPM-o 4.5(全模态 + 语音输出)

OpenBMB 的 8B 级全模态模型(图/音/文进、文本出 + 可选语音出):Qwen3-8B 稠密主干 (36 层 d=4096、GQA 32:8、head_dim 128、theta 1e6、untied lm_head、词表 151748、ctx 40960, q/k 逐 head RMSNorm)+ SigLIP-SO400M NaFiT 视觉塔(27 层 d=1152 @448px patch14,学出的 70×70 位置表按图动态切片,slice_image 多瓦片)+ 64-query Perceiver 重采样器(视觉→LLM 投影)

  • Whisper-medium 音频塔(24 层 d=1024、80 mel、1s 块对角注意力掩码、AvgPool1d(5))+ Llama-768×20L TTS 副塔(6562 码本 speech token,weight_norm head 重组合)+ CosyVoice2 token2wav 声码器(独立兄弟 GGUF,arch token2wav,引擎自动在主模型同目录发现, MINICPMO_VOCODER 可改路径)。transformers 4.51 官方栈做 oracle。

# 预转好的:modelscope download --model SkyContact/MiniCPM-o-4.5-GGUF --local-dir models/MiniCPM-o-GGUF
#          (HF 同名;含 tokenizer.json + 声码器 GGUF)
pip install -r convert/requirements.txt
hf download openbmb/MiniCPM-o-4_5 --local-dir models/minicpmo45      # 4 分片 BF16 + tokenizer
python convert/convert_minicpmo45.py --out-type f16                  # 18.7 GB(对拍基线)
python convert/convert_minicpmo45.py --out-type q8_0                 # 10.09 GB(全兼容基准档)
python convert/convert_minicpmo45.py --out-type q6_k -o minicpmo45-q6k_mixed.gguf   # 8.19 GB(混合档)
python convert/convert_minicpmo45_vocoder.py                         # -> minicpmo-vocoder.gguf(377 MB,--speak 需要)
python convert/check_minicpmo45.py --gguf <产物> [--hash]            # 转换后自检(不加载模型)

vqa-cli --model minicpmo45-q8_0.gguf --tokenizer models/minicpmo45 \
        --image photo.jpg --ask "What is in this picture?"
vqa-cli --model minicpmo45-q8_0.gguf --tokenizer models/minicpmo45 \
        --audio clip.wav --ask "Transcribe this audio."
vqa-cli --model minicpmo45-q8_0.gguf --tokenizer models/minicpmo45 \
        --ask "用一句话介绍你自己" --speak reply.wav        # TTS 副塔 + 声码器 -> 24kHz wav

能力与口径(转换器/引擎实测):

  • 输入:图像(多瓦片动态分辨率)+ 音频(16k mono)+ 文本;输出文本,--speak 追加语音 wav

  • chat 模板 Qwen 风格(im_start/im_end),模板自带空 think 块;eos=151645、bos/pad=151643; TTS 副塔专用 token:audio_bos=151687、text_eos=151692

  • TTS 不能贪心采样:temperature→0 且无重复惩罚时 AR 码塌缩成单 token 循环(82 秒噪声); 引擎复刻官方默认采样 temp 0.8 / top_p 0.85 / top_k 25 / rep_pen 1.05

  • 量化:语言/音频/TTS 主干行宽 4096/12288/1024/3072/768 全是 256 倍数,唯视觉塔 d=1152 不是(fc2 入宽 4304 连 32 都不整除)——q8_0 档仅 fc2×27 回退 F16,q6k_mixed 档 1152 系 ×136 + fc2×27 回退 F16(见下表),norms/biases/位置表/查询向量恒 F32

量化档(转换后自检键数/差集/抽样 byte-exact 全过):

档

大小

逐张量类型

f16

18.71 GB

全部 2-D F16(对拍基线)

q8_0

10.09 GB

686 个 %32 宽 matmul 操作数 Q8_0;视觉 fc2(4304 宽)×27 张 F16

q6k_mixed

8.19 GB

550 个 %256 宽 Q6_K;1152 宽×136 + fc2×27 张 F16

对拍(oracle = transformers 4.51 官方栈 BF16,numpy 独立重算判卷):文本 "What is the capital of France?" 贪心输出 "Paris",与官方 chat() 一致(prefill 末位 top1 p=0.9999);视觉 3 瓦片 问答语义正确,重采样器输出 cos_mean 0.99994;音频投影 cos_mean 0.9999980 (max_abs_diff 0.057);TTS seed 0 官方采样 102 个语音码 → "你好,我是MiniCPM 3 Omni。很高兴为您服务。", 声码器 wav 4.08s @24kHz;同噪声门(同一高斯噪声 teacher-force)mel cos 0.999999986 / wav SNR 12.6dB(AR 采样 RNG 对齐不可行,parity 口径=同噪声门)。

引擎量化档四门(vs F16 基线,文本 prompt 贪心):

门

q8_0

q6k_mixed

文本 prefill argmax

25/26(=F16 基线,gen0/gen1 "Paris"+im_end 全对)

23/26(gen0/gen1 仍全对)

视觉重采样器 cos

0.999877

0.999961

音频投影 cos

0.999654

0.998163

TTS 语音码

AR 采样从码 1 起分叉(量化噪声;逐 step logit cos 0.985-0.998),语音正常产出

同左(模型自称 Qwen——openbmb 底座即 Qwen3,官方模型同款行为)

性能(CPU 16 线程 / CUDA 5070 Ti Laptop 12GB 全卡,两档均可整个进卡):

q8_0

q6k_mixed

CPU decode

7.25 tok/s

9.17 tok/s

CUDA decode

62.5 tok/s

52.6 tok/s

视觉 prefill 已优化:重采样器 x @ proj 从宿主单线程循环移入计算图(载入期做 F32 [in,out] 转置副本),3 tile 图像端到端 prefill 12972 -> 11586 ms(−10.7%),~10 tile 图像约省 3.4 s。

为什么做独立引擎?

moondream3.1 是 Moondream 团队推出的 90 亿参数混合专家(MoE)视觉语言模型(每 token 激活 2B)。 目前本地运行它只有一个途径:Moondream 官方的 Photon 引擎 (Python + 专有 CUDA 内核,需要 24GB+ 显存)。llama.cpp 生态对 moondream 的支持从 2025 年起 就只有一份挂起的 feature request——它的架构 和 llama.cpp 支持的任何模型都不一样:

  • tau attention —— 逐 head、逐 token 的可学习 Q/V 缩放: q *= tanh(Wq·gelu(qkv)) + 1 + (sigmoid(alpha·log(pos+1)) − 0.5)

  • 并行残差 block —— x = x + attn(LN(x)) + mlp(LN(x)),GPT-J 风格

  • 处处带 bias 的 LayerNorm(attention 前、FFN 前、最后的 norm),lm_head 也带 bias

  • GeGLU 变体 —— MoE 专家内是 gelu(h) · (g + 1)

  • dense 前缀 + MoE 主体 —— 第 03 层是 dense MLP,第 423 层才是 MoE

  • 官方检查点直接发 FP8 (e4m3) 专家权重,带逐通道 scale

  • SigLIP 视觉塔:588 列 patch embedding + 多 crop 切图

与其把这些硬塞进 llama.cpp 的模型注册表,不如整条栈自己掌控:权重转换、计算图、KV cache、 采样全部自研,只为 kernel 复用 ggml——就像 whisper.cpp 和 audio.cpp 那样。VideoChat3-4B 是同一套框架下的第二个后端, 它的视觉塔、位置编码与多模态投影同样不在 ggml 的既有模型里。

架构速览

moondream3.1(逆向自官方检查点与 vikhyat/moondream)

层数

24(0–3 dense,4–23 MoE)

dim / heads

2048 / 32 heads,无 GQA,head_dim 64

RoPE

半旋转,仅前 32 维,theta 1.5e6

MoE

64 专家选 8,专家 inter dim 1024,router 带 bias

词表

51200(moondream/starmie-v1)

上下文

32768

VideoChat3-4B

文本主干

Qwen3:36 层,dim 2560,32 heads / 8 KV heads,head_dim 128,per-head qk-norm,RoPE theta 5e6,tied lm_head

视觉塔

27 层 ViT,dim 1152,16 heads,patch 14,2D RoPE,2×2 merge,GELU(tanh) MLP,学习式 64×64 位置表

融合

merger 把 4 个 patch 拼成 4608 维 → LayerNorm → 4608→4608 GELU(erf) → 2560

词表 / 上下文

151936(Qwen2 BPE);文本上下文 262144,引擎 KV 默认 8192

提示词

`<

性能

CPU 上 Q8_0 的 VideoChat3-4B 受内存带宽限制;换成更小的量化或 CUDA 构建提升明显 (下表为实测,37 token prompt、贪心解码,具体数值随硬件与负载浮动):

模型 / 量化

后端

硬件

prefill

decode

VideoChat3-4B Q8_0(4.83GB)

CPU 14 线程

i7-14700K

~71 tok/s

~10.4 tok/s

VideoChat3-4B Q8_0(4.83GB)

CUDA

RTX 4070 SUPER

~401 tok/s

~49.8 tok/s

Marlin-2B Q8_0(2.67GB)

CUDA

RTX 5070 Ti Laptop

~9.7k tok/s

~121 tok/s(视觉编码 112ms/300tok)

DiffusionGemma 26B-A4B Q8_0(28.89GB)

两段式(塔 CUDA / 主干 CPU 28 线程)

i7-14700K + RTX 4070 SUPER

26 tok 1.7 s

4.0 s/去噪步(≈0.27–0.39 tok/s)

每次推理都会在 stderr 打印一行 [perf] prefill ... | decode ...,便于自行核对。 moondream3.1 的数字见当前状态(官方标注来自 5070 Ti Laptop)。

仓库结构

src/
  core/                共享层:vqa::Model 接口、mmap/UTF-8/JSON 工具、字节级 BPE 分词器、
                       图像与视频帧解码(stb_image 实现所在)、按架构发现后端、
                       模型发现与单实例驻留(model_registry,Web UI 与 MCP 共用)
  models/moondream/    moondream3.1 后端(引擎 + 视觉塔 + 多 crop 预处理 + region 头 + 服务端/批量)
  models/videochat3/   VideoChat3-4B 后端(视觉预处理 + 计算图)
  models/videomae/     VideoMAE 视频塔后端(clip 预处理 + 全时空注意力前向 + 分类/嵌入)
  models/vjepa2/       V-JEPA 2 后端(三维 RoPE + 全时空注意力 + pooler/分类器/predictor)
  models/qwen3_5/      Qwen3.5 / Marlin / VideoJev 后端(gated-delta + full attention 混合图、caption/find 技能、打分头)
  models/moss_vl/      MOSS-VL-Realtime 后端(self+cross 双路注意力图、多轮/实时流会话)
  models/mage_vl/      Mage-VL 后端(Codec-ViT 视觉塔 + System 1 门控)
  models/qwen2vl/      Qwen2.5-VL 后端(窗口注意力 ViT + 2×2 分组 patch 预处理)
  models/qwen25omni/   Qwen2.5-Omni 后端(thinker/talker 双 GGUF + 宿主 Token2Wav)
  models/smolvlm/      SmolVLM-256M 后端(Idefics3 瓦片预处理)
  models/embedgemma2/  EmbeddingGemma-2 后端(四模态嵌入)
  cli/                 统一前端 vqa-cli(推理 / --serve Web UI / --mcp MCP 服务器)
convert/               HF 检查点 → GGUF 转换器
scripts/               构建脚本、parity 与自检脚本
vendor/stb/            自带的单头图像解码器
ggml/                  唯一第三方依赖(clone 得到,不入库)

开发

  • parity 脚本:scripts/ref_videochat3.py 用 transformers 跑官方实现并导出中间张量, scripts/compare_vc3.py 与 C++ 侧的 dump 逐级比对(预处理 / patch 嵌入 / 每层输出 / logits)。

  • 调试开关:VC3_DUMP_DIR=<dir> 导出中间张量,VC3_MAX_LAYERS=N 截断层数做分层对照, VC3_NO_FUSION=1 退回未融合的 SwiGLU, --dump-ids 只跑分词;VideoMAE 用 VIDEOMAE_DUMP_DIR=<dir>(或 --dump-dir)导出 patches / v_patch_out / v_blk0_qkv / v_blk0_attn_out / v_blk0_out / v_final / v_pooled / v_logits / v_probs, VIDEOMAE_MAX_LAYERS=N 只跑前 N 层做逐层漂移对照(本机实测 12 层全长相对差 ~1e-3, 约为 F16 权重噪声 + 线程归约顺序噪声);V-JEPA 2 用 --dump-dir 导出 patches / v_patch_out / v_blk0_qkv / v_blk0_q_rope / v_blk0_attn / v_blk0_attn_out / v_blk0_out / v_final / v_pooled,VJEPA2_MAX_LAYERS=N 同理截断层数。Nemotron-Parse 用 NEMOTRON_PARSE_DUMP_DIR=<dir>(或 --dump-dir)导出 v_patches / v_positions / v_patch_out / v_final / v_neck / v_feats / t_emb / t_l0_out / t_final_norm / logits,NEMOTRON_PARSE_PROFILE=1 打印图规模与 compute/建图拆分。

  • 共享层(src/core):后端之间不放重复实现,目录按职责分层,索引见 src/core/README.md;include 一律从 src/ 根写 (#include "core/engine/weight_load.hpp"),所以再加子目录也不用改 include 路径:

    • vqa.hpp — 唯一放在根上的头:vqa::Model 接口与能力表,前端只认它

    • engine/ — ggml 侧公共件:backend.hpp(CUDA 优先 + CPU 回退 + 线程数按物理核, SMT 对这类模型的 GEMV 是负收益)、weight_load.hpp(mmap 绑定、1D 张量转 host F32、 CPU 零拷贝 / GPU 上传的 buffer 归属,以及加载期的归一化折叠 LnFoldPlan——把 norm(x)·γ+β 折进后续 matmul,现有 GGUF 无需重转;device 后端默认开、CPU 默认关, VQA_FOLD_LN=1/0 强制)、gguf_util.hpp(元数据读取 / 张量转 F32)、 ggml_dump.hpp(对拍导出)、attention.hpp(KV cache + mask)、stage_buf.hpp(device 侧 跨图暂存)、sampler.hpp、decode_graph.hpp(decode 图按 KV 窗口分桶复用,只在跨桶时重建, VQA_DECODE_BUCKET=N 覆盖桶宽;逐 token 重建既白烧 ~0.5 ms 也会让 ggml-cuda 的 CUDA graph 缓存失配——它按 cgraph->nodes[0] 做键)

    • blocks/ — 可复用图体:vit_block.hpp(所有 ViT 塔共用的 block 体 + 通用成对旋转 RoPE, moondream / VideoChat3 / VideoMAE / ViViT / V-JEPA 2 都用它,差异走 hooks)、 llm_block.hpp(Qwen 系因果解码层,VideoChat3 / SmolVLM / MOSS-VL 已入住,权重字段为空即跳过 对应 op)、qwen2vl_tower.hpp(Qwen2.5-VL 视觉塔整塔共享,qwen2vl 与 qwen25omni 逐 op 一致)

    • media/ — resample.hpp(smart_resize、PIL 语义 antialias bicubic、位置表插值; 原来寄居在 videochat3 目录被三个模型反向 include)、media_io.hpp(图像 / 视频帧解码)、 jpeg_decode.hpp(libjpeg-turbo,与 PIL 逐像素一致)、audio_feats.hpp(FFT / Hann / mel / log-mel:Qwen2.5-Omni 的 whisper 系与 EmbedGemma-2 的 Gemma4 系两套数值不可互换,禁止跨族 "统一"浮点行为)、video_pipe.cpp(ffmpeg 抽帧)

    • text/ — tokenizer(byte-level BPE + Qwen2 正则预切分)、utf8(UTF-8 编解码 + GPT-2 byte↔unicode 字符表)、json 与 json_scan(前者递归下降解析器,后者只做 HF 配置的键扫描)、 unicode_ranges.inc(生成文件)

    • runtime/ — model_registry(能跑哪些 GGUF / tokenizer 解析 / 单实例驻留)、vqa_registry (架构名 → 工厂表,唯一知道后端清单的文件)、backend_factories、load_util(LoadOptions 落地

      • 各后端 <PREFIX>_DUMP_DIR + 特殊 token id)、vram_guard、http_server(内嵌 Web UI)

    • util/ — mmap(只读映射 + 整文件读取)、aligned_buf(64 字节对齐缓冲)

  • 加/改共享层前先读:src/core/README.md 里有"新代码该放哪"的判定表和"把重复实现收敛进来" 的四步流程(两份实现先各自数值一致 → 合并前 diff 掉漂移的守卫 → 新行为默认关闭 → 跑 scripts/ref_*.py + scripts/smoke.sh)。

  • 改分词器数据:scripts/gen_unicode_ranges.py 重新生成 src/core/text/unicode_ranges.inc。

  • 回归冒烟:scripts/smoke.sh 跑跨后端金标准电池(smolvlm/vc3/moss/qwen3.5 各一条贪婪,CPU

    • 钉死 --threads 1 保证可复现);--golden FILE 对既有金标准逐字节比对。改共享层/做迁移后先跑它。

  • 纯 C++ 自检:cmake --build build --target np_markup_test 跑 Nemotron-Parse 的答案渲染断言 (图表表格/标题/字幕、caption 上移、列表符号与软连字符、JSON),不依赖 ggml 与权重,CI 每次都跑。

License

MIT,见 LICENSE。ggml 为 MIT(ggml-org)。模型权重与 tokenizer 遵循其各自条款,单独下载。

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    MCP server that provides a 'borrowed eye' for text-only LLMs, enabling them to identify and describe local images via the Qwen VL vision model, including face recognition, scene description, OCR, and targeted visual questioning.
    3 npm
    Apache 2.0
  • A
    license
    A
    quality
    B
    maintenance
    Provides vision understanding capabilities such as image analysis, OCR, object localization, and video frame analysis, plus optional image generation and editing, to coding agents via OpenAI-compatible multimodal models. Runs as a local MCP server with HTTP and stdio transports, configurable for clients like Codex, Claude Code, Kimi, and Cursor.
    3
    25 npm
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    MCP server for multimodal understanding and object grounding (bounding boxes) across images, videos, and documents, with support for multiple AI providers (Zhipu GLM-V, OpenAI GPT-4o, Anthropic Claude, or any OpenAI-compatible endpoint).
    1
    MIT