skyvision-mcp
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
SkyVision.cpp
用 ggml 在本地跑图像/视频 VQA —— 一个多模型推理引擎:
每个视觉语言模型是一个后端(实现 vqa::Model),统一前端按 GGUF 里的
general.architecture 自动选择后端,图像与视频帧序列共用同一套输入抽象。
运行时不需要 Python、不需要 CUDA Toolkit、不依赖任何厂商运行时。
也可以用 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 重新生成,勿手改);
各后端的详细结构说明见下方人工维护的表格:
架构标签 | 别名 | 输入 | 任务 | 流式 | 标记 |
|
| 图像 + 文本 | ask caption detect point gaze | — | 需 |
| — | 图像 + 视频 + 文本 | ask | — | — |
| — | 图像 + 视频 + 文本 | ask caption | — | — |
| — | 图像 + 视频 + 文本 | ask caption | — | — |
| — | 视频 + 文本 | score | — | — |
|
| 图像 + 视频 + 文本 | ask | — | — |
|
| 图像 + 视频 + 文本 | ask | ✅ | — |
| — | 图像 + 视频 + 文本 | ask | ✅ | — |
|
| 图像 + 文本 | ask | — | — |
|
| 图像 + 文本 | ask | — | — |
|
| 图像 + 视频 | classify embed | — | 纯视觉 |
|
| 图像 + 视频 | classify embed | — | 纯视觉 |
| — | 图像 + 视频 + 文本 | ask gate | ✅ | — |
|
| 图像 + 音频 + 文本 | ask | — | 语音输出 |
| — | 图像 + 视频 + 音频 + 文本 | embed | — | — |
| — | 图像 + 视频 + 音频 + 文本 | ask | — | — |
| — | 图像 | classify embed | — | 纯视觉 |
|
| 图像 + 视频 + 文本 | ask | — | — |
|
| 图像 + 音频 + 文本 | ask | — | 语音输出 |
|
| 图像 + 文本 | ask | — | — |
|
| 图像 + 文本 | ask | — | — |
| — | 图像 + 音频 + 文本 | ask | — | — |
|
| 图像 + 音频 + 文本 | ask | — | 语音输出 |
|
| 图像 + 文本 | ask | — | — |
后端 | 架构标签 | 输入 | 说明 |
moondream3.1-9B-A2B |
| 图像 | MoE 文本 + SigLIP 视觉塔;五个官方技能(caption / query / detect / point / gaze)、Web UI、批量推理 |
VideoChat3-4B |
| 图像 + 视频 | 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 混合主干(18 层 gated-delta 线性注意力 + 6 层 full attention,interleaved MRoPE)+ Qwen3-VL 式视觉塔(学习式绝对位置嵌入 + 2D RoPE + 逐时间步注意力)。官方 |
VideoJev-4B(打分式) |
| 视频 | Qwen3.5-4B 主干(32 层:24 线性 + 8 full,线性注意力 16 键 / 32 值头 GQA)+ 同款视觉塔,但末层被裁成 attention-only(无 MLP/post-norm),并用训练好的 cosine 决策头替代 lm head:一次前向对候选列表打分(时序定位=逐时间片 sigmoid,单选=VQA softmax),不生成文本。走 |
Mage-VL-4B |
| 图像 + 视频 | 微软亚研院 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 同构)。官方 |
MOSS-VL-Realtime(11B) |
| 图像 + 视频 | OpenMOSS cross-attention 架构:Qwen3 文本主干(36 self 层 + 12 纯 cross-attn 层,tanh 双门控)+ SigLIP 塔(deepstack 8/16/24 + merger)+ 每帧可学习 separator。视觉 KV 走独立 cross 注意力,interleaved MRoPE(视觉三轴平面位置)。支持多轮会话( |
VideoMAE(base,video tower) |
| 视频(图像=静帧) | 纯视觉塔,没有文本侧:Conv3d tubelet patch 嵌入 + 固定 sin-cos 位置表(无 cls token)+ 12 层全时空注意力,一次前向出分类标签( |
ViViT(b-16x2,video tower) |
| 视频(图像=静帧) | 同一条视频塔(复用 |
V-JEPA 2(ViT-L/16,video tower) |
| 视频(图像=静帧) | Meta FAIR 的世界模型编码器:没有位置表,改用三维分解 RoPE(head_dim 64 = 20 时间 + 20 高 + 20 宽 + 4 不动,位置取自 |
Qwen3-VL(4B)/ OneStreamer-4B |
| 图像 + 视频(逐帧) | 独立实现的 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)、 |
MiniCPM-V 4.7(1B) |
| 图像 + 视频 | openbmb 端侧 VLM,文本主干就是 Qwen3.5( |
Qwen2.5-VL(3B) |
| 图像(视频待接) | Qwen2 文本主干(36 层 GQA 16:2、qkv 带 bias、section MRoPE 16/24/24、SwiGLU)+ Qwen2.5 ViT(32 层、patch14、temporal2、2×2 merge、窗口注意力:每 8 层一次全局)+ merger。动态分辨率走 |
Qwen2.5-Omni(3B) |
| 图像 + 音频输入,文本 + 语音输出 | 全功能 thinker-talker:Qwen2.5 文本主干(untied lm_head)+ Qwen2.5-VL 视觉塔(与 |
Qwen3-Omni(30B-A3B-Instruct) |
| 图像 + 音频输入,文本 + 语音输出 | 首个 ggml 实现(llama.cpp 无此架构):thinker=标准 Qwen3 全注意力 + MoE 文本主干(48 层、128 专家激活 8、interleaved MRoPE 24/20/20 逐轴整支 128 维)+ Qwen3-VL 视觉塔(deepstack 三抽头,与 |
Janus-Pro-1B |
| 图像 | 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 个 |
Nemotron-3.5-Parse(0.63B) |
| 图像(文档页) | 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、 |
Nemotron-3-Nano-Omni-30B-A3B(Reasoning) |
| 图像 + 音频 | 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 档)。转换 |
MiniCPM-o 4.5 |
| 图像 + 音频 | 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,同目录自动发现);文本出 + |
DiffusionGemma 26B-A4B-it |
| 图像 + 文本 | 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)。转换 |
SmolVLM(256M,Idefics3) |
| 图像 | 目前最小的可用后端:SigLIP-base 视觉塔(12 层、patch16、512²、learned pos、LN/注意力/MLP 全带 bias、 |
EmbeddingGemma-2(740M) |
| 图像 + 视频 + 音频 + 文本(嵌入) | 多模态嵌入模型(不生成文本):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)。 |
Keye-VL-2.0(30B-A3B) |
| 图像 + 视频 | 快手 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 |
默认(CUDA 构建) | 视觉塔 → 显卡,语言主干 → host mmap |
| 全部上卡(约需 30 GB VRAM) |
| 语言主干也上卡(仅大显存机器有意义) |
| 参考路径:忽略设备后端,全部 CPU |
性能(i7-14700K 20C/28T、RTX 4070 SUPER 12 GB、q8_0 档):
场景 | 默认(两段式) |
|
文本 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( | ✅ 移动端友好:拖拽/选图、NDJSON 流式逐字输出(UTF-8 边界安全)、视觉编码逐 clip 进度、detect 绿框 / point 十字 / gaze;同图同视频追问走两级缓存(上传解码 + 视觉特征),二次提问 ~26ms |
moondream 思考模式 | ✅ |
FP8 专家权重 → Q8_0 / Q6_K / Q4_K | ✅ 转换器已就绪(Q6_K ≈ 7.6GB,12GB 显卡首选) |
批量推理( | ✅ 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 视频输入 | ✅ 内置 |
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( |
Marlin-2B 视频结构化技能 | ✅ |
视觉预处理 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 多轮会话 | ✅ |
MOSS-VL 实时流会话 | ✅ |
MiniCPM-V 4.7 图像/切片/视频 | ✅ 端到端 + 逐级数值消融( |
Keye-VL-2.0 文本/图像/视频 | ✅ 端到端 + 官方 llama.cpp fork 对拍( |
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( |
Mage-VL System 1(主动流式门控) | ✅ |
Mage-VL 视频问答 | ✅ |
MOSS-VL 常驻推理流 | ✅ |
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 嵌入, |
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 类线性头, |
Nemotron-3.5-Parse 文档解析 | ✅ 端到端 + 官方 torch 参考逐级对拍( |
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-clivqa-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.ggufvqa-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 顶部的徽章是"真构建过",不是宣称。构建成本按三层摊开:
层 | 何时失效 | 说明 |
| 只改 apt 包列表 | apt lists 走 cache mount |
| 只改 | ggml 独立成 stage 单独编译( |
| 改源码 |
|
CI 的推送构建默认不带权重(MODEL_REPO=):BuildKit 的 --mount=type=cache 在新 runner 上
是空的,带权重等于每次多传 400MB。完整镜像(含 describe_image 冒烟)走每周定时 +
手动 workflow_dispatch,缓存用 ghcr registry(mode=max,无 10GB 配额限制)。
工具 | 说明 |
| 列出可运行的 GGUF(架构/量化/体积/是否常驻)+ 已加载模型的能力位 |
| 显式加载(同一时刻只常驻一个模型;换模型会卸载旧的),可设 |
| 图像描述, |
| 自由提问( |
| 目标检测,返回 JSON(归一化 0..1 + 像素坐标),需 moondream3 + |
| 目标中心点,同上 |
| 由起点预测注视点,同上 |
| 文档页解析( |
| 视频问答(ffmpeg 抽帧, |
| 纯视频塔(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] 前缀,写出后会重新打开文件校验张量表。
脚本 | 产出 |
|
|
|
|
|
|
|
|
| Qwen3.5 / Marlin-2B 通用: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
moondream3.1
读取官方 FP8 检查点 moondream/moondream3.1-9B-A2B:
FP8 专家张量逐位精确解码(与 PyTorch 的 e4m3 解码 bit 级一致)后重量化为 Q8_0,
--q6可选 Q6_K(约 7.6GB,12GB 显存首选)、--q4可选 Q4_KBF16 张量转 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.95GBq4_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 idRoPE 有一个必须照抄的上游 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 核):
对照 | 结果 |
同输入(把引擎的 | cos 1.000000、max 0.065 —— 图实现本身没有偏差 |
端到端 | 相对差 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 的结构实现,踩到并修掉的 三个坑值得记下来,它们都属于"数值看着像、结果全错"的类型:
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),不是光栅序。按光栅序打包会让 图像"看起来还在",但特征全错。merger 的 RMSNorm 是逐 token 1280 维,归一化完才 reshape 成 5120 喂 fc1。先 reshape 再 norm(5200 维上归一化)会静默毁掉特征——这一条让我在"塔输出 corr 0.98 但答案错"上卡了很久。
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 样图):
配置 | 图像块 | 端到端 |
| 4×3 瓦片 + 全局 = 13 | 8.6 s |
默认(长边上限 1024) | 2×2 + 全局 = 5 | 3.8 s |
| 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 全功能(含 |
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 400thinker 侧三模态与官方逐段对拍(图像 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.gguf740M(文本 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>, 坐标是内容区归一化值;表格以 LaTeXtabular出现,由--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 档无意义;所以是逐张量混合:
档 | 大小 | 逐张量类型 |
| 35.2 GB | 全部 %32 matmul 操作数 Q8_0(全兼容基准) |
| 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追加语音 wavchat 模板 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 全过):
档 | 大小 | 逐张量类型 |
| 18.71 GB | 全部 2-D F16(对拍基线) |
| 10.09 GB | 686 个 %32 宽 matmul 操作数 Q8_0;视觉 fc2(4304 宽)×27 张 F16 |
| 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 主体 —— 第 0
3 层是 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( |
上下文 | 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 遵循其各自条款,单独下载。
This server cannot be deployed
Maintenance
Related MCP Connectors
Chat, image, video, and audio models from many vendors behind one remote MCP server and one key.
MCP server for Clipkit — gives AI agents a video toolbox via the Clipkit schema.
A comprehensive Model Context Protocol (MCP) server that enables AI assistants to interact with yo…
MCP server for MiniMax H3 multimodal video generation
Related MCP Servers
- FlicenseAqualityDmaintenanceLocal multimodal MCP server for vision, document, audio, video, and GUI automation, all processed locally on Apple Silicon for privacy and security.9-
- AlicenseNot gradedqualityDmaintenanceMCP 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 npmApache 2.0
- AlicenseAqualityBmaintenanceProvides 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.325 npmMIT
- AlicenseNot gradedqualityBmaintenanceMCP 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).1MIT