colibrì 架构概述
1. colibrì 是什么
colibrì 是一个单文件、零依赖的纯 C 推理引擎,目标是在消费级机器(25 GB 内存、12 核 CPU,甚至没有 GPU)上运行 GLM-5.2 —— 一个 744B 参数的 MoE 大模型。整个运行时引擎是一个 C 文件 glm.c(约 2500 行)加几个 header-only 头文件,没有 BLAS、没有 Python、不强制要求 GPU。
它要解决的根本矛盾是:
GLM-5.2 (744B MoE):
FP8 权重在磁盘上 ≈ 756 GB
常规推理需要的显存 数百 GB → 一整机柜 GPU
普通开发者能拿出的内存 ≈ 25 GB
colibrì 的答案:
常驻内存(稠密, int4) ≈ 9.9 GB ← 装得下
磁盘上的路由专家 ≈ 370 GB ← 按需流式读
每 token 真正读的专家 ≈ 11 GB ← 磁盘 I/O 是唯一瓶颈
它的哲学是 mechanical sympathy(贴合硬件):既然显存不够,就把显存瓶颈换成磁盘瓶颈,再用量化、SIMD 整数点积、KV 压缩、投机解码、页缓存把磁盘瓶颈一层层抹平。
2. 核心思想:为什么”流式专家”可行
一个 744B 的 MoE 模型,每个 token 只激活约 40B 参数。关键观察是把参数分成两类:
┌─────────────────────────────────────────────────────────────┐
│ 稠密部分(每个 token 都用,约 17B 参数) │
│ ├─ 注意力(MLA 的 q/kv-LoRA 投影、输出投影) │
│ ├─ 共享专家(shared expert,每个 token 都过) │
│ ├─ 路由器(router)与各种 norm │
│ └─ embedding + lm_head │
│ → int4 量化后 ≈ 9.9 GB,一次性【常驻内存】 │
├─────────────────────────────────────────────────────────────┤
│ 路由专家(每个 token 只选 8 个,约 11 GB/token 变化) │
│ ├─ 75 个 MoE 层 × 256 专家 + MTP 头 = 21,504 个专家 │
│ ├─ 每个专家 int4 后 ≈ 19 MB │
│ └─ 全部 ≈ 370 GB,【留在磁盘上,按需流式读】 │
└─────────────────────────────────────────────────────────────┘
decode 一个 token 时,每层的路由器只挑出 8 个专家。75 层 × 8 专家 × 19 MB ≈ 11 GB 的磁盘读。这就是 colibrì 的成本模型——decode 是磁盘受限的,token 速度基本等于”磁盘随机读带宽 ÷ 11 GB”。
围绕这个成本模型,colibrì 建立了一整套缓存层级来减少真正落到磁盘的读:
专家的存储层级(越靠上越快、越小)
┌──────────────────────────────────┐
│ VRAM 固定专家 (可选 CUDA 层) │ ← 最热的专家常驻显存
├──────────────────────────────────┤
│ RAM 固定热存储 (pinned hot-store) │ ← .coli_usage 学到的热专家
├──────────────────────────────────┤
│ 每层 LRU 缓存 (ecache) │ ← 最近用过的专家
├──────────────────────────────────┤
│ OS 页缓存 (免费的 L2) │ ← 读过的磁盘页还在内存里
├──────────────────────────────────┤
│ 磁盘 (NVMe, ~370 GB) │ ← 冷读,19 MB/专家
└──────────────────────────────────┘
用得越多,热专家越准,落盘的读就越少——README 里那句”colibrì literally gets faster the more you use it”就来自 .coli_usage 这个会学习的缓存。
3. 三项让它跑得动的关键技术
3.1 极致量化(int4 / int8 / int2)
所有权重都以极低精度存储,推理时用到哪一行才反量化(dequant-on-use),从不把整个反量化矩阵物化到内存:
- int4 packed(默认专家精度):两个 4-bit 数打包进一个字节,每行一个 f32 缩放系数 —— 见 pack_int4。
- int8(稠密与 MTP 头精度):一个字节一个数 —— 见 quantize_rows。
- int2 packed(实验,四个 2-bit 数一个字节)—— 见 pack_int2。
decode 单 token 时还有一条 IDOT 整数点积快路径:把激活也量化成 int8,用 SIMD 的 maddubs/vpdpbusd/vdotq 指令做整数点积,量化矩阵乘快 2–3 倍,只带来约 0.3% 的 RMS 误差 —— 见 dot_i8i8、dot_i4i8。
3.2 MLA 压缩 KV-cache(57 倍)
GLM-5.2 有 64 个注意力头且不用 GQA,朴素 KV-cache 每 token 要存 32,768 个 float。colibrì 忠实实现了 MLA(Multi-head Latent Attention):只缓存一个压缩的 latent(kv_lora 维)加一小段旋转过的 key(qk_rope 维),每 token 只存 576 个 float(57 倍更小)。k_nope 和 value 在用时通过 kv_b 现场重建 —— 见 attention 与 Model 结构注释。
进一步地,decode 时用 DeepSeek 的权重吸收(weight absorption)技巧:不再逐 token 重建 k/v,而是让 query 吸收 kv_b,注意力之后再投影出上下文,避免每 token 的 k/v 重建开销 —— 见 attention 吸收路径。
3.3 原生 MTP 投机解码
GLM-5.2 自带一个 多 token 预测(MTP)头(位于 layer 78)。colibrì 用它来”起草”若干后续 token,再让主模型在一次批量前向里同时验证——接受的部分就免费多产出了几个 token。community 实测在 int8 头下接受率 39–59%,2.2–2.8 tokens/forward —— 见 mtp_draft 与 spec_decode。
关键工程细节:MTP 头必须是 int8。在 int4 下 draft 几乎总是猜错(接受率 0–4%),投机永远无法启动。转换器默认就把这个头转成 int8 —— 见 convert_fp8_to_int4.py 的注释。而且投机在温度采样下依然无损:靠拒绝采样(rejection sampling)保证输出分布与不投机时逐 token 一致 —— 见 spec_decode 的接受/拒绝逻辑。
4. 分层架构
colibrì 的运行时全部在 glm.c 一个文件里,但逻辑上分为清晰的几层:
┌──────────────────────────────────────────────────────────────┐
│ 入口 / 运行模式 (main, run_text, run_serve, run_score ...) │
│ env-var 驱动:SNAP / PROMPT / SERVE / SCORE / REPLAY / TF │
├──────────────────────────────────────────────────────────────┤
│ 生成与采样层 │
│ ├─ spec_decode: draft + verify 投机解码主循环 │
│ ├─ pick_tok / dist_build: 温度 + nucleus 采样、拒绝采样 │
│ └─ mtp_draft / ngram_draft: 起草 │
├──────────────────────────────────────────────────────────────┤
│ 前向计算层 │
│ ├─ step / step_all / layers_forward / layer_forward │
│ ├─ attention: MLA + DSA 稀疏注意力 + 权重吸收 │
│ ├─ moe: sigmoid 路由 + 专家计算 + 共享专家 │
│ └─ dense_mlp: 前 3 层稠密 MLP │
├──────────────────────────────────────────────────────────────┤
│ 内核层 (纯 C + SIMD, 无 BLAS) │
│ ├─ matmul / matmul_q / matmul_i4 / matmul_i2: dequant-on-use │
│ ├─ dot_i8i8 / dot_i4i8: IDOT 整数点积 (AVX512-VNNI/AVX2/NEON) │
│ └─ rmsnorm / softmax / rope_interleave │
├──────────────────────────────────────────────────────────────┤
│ 专家缓存与调度层 │
│ ├─ expert_load: 合并 pread 读一个专家 (~19 MB) │
│ ├─ ecache (每层 LRU) / pin (RAM 热存储) / ws (working set) │
│ ├─ tier.h: 热度驱动的 repin 换入换出 │
│ └─ pilot_prefetch: router-lookahead 磁盘预取 │
├──────────────────────────────────────────────────────────────┤
│ 存储层 (st.h, 纯 pread + posix_fadvise, 无 mmap) │
│ ├─ st_init: 索引所有 *.safetensors,建哈希表 │
│ ├─ st_read_raw / st_read_f32: 定位读 + 反量化 │
│ └─ O_DIRECT 双 fd + posix_fadvise(WILLNEED/DONTNEED) │
├──────────────────────────────────────────────────────────────┤
│ 可选 CUDA 层 (backend_cuda.cu, -DCOLI_CUDA) │
│ └─ 把最热的固定专家/稠密张量上传到 GPU 常驻 │
└──────────────────────────────────────────────────────────────┘
一个重要的实现选择:存储层完全基于 pread + posix_fadvise,不用 mmap。st.h 的头注释明确说这是为了修复”mmap-rss-bug”——用 posix_fadvise(DONTNEED) 在读完后立即驱逐页,让常驻内存(RSS)保持稠密、可控,而不是被 mmap 的页无限撑大。
5. 一个 token 的完整生命周期
结合后续各文档,decode 一个 token 从头到尾的路径:
启动阶段(进程启动时一次):
1. model_init: 读 config.json,索引所有 safetensors 分片 (st_init)
2. 加载【稠密】权重常驻内存:注意力投影、共享专家、router、embed/lm_head
—— 专家权重【不加载】,只留在磁盘
3. 检测并加载 MTP 头 (int8) 和 DSA indexer 权重
4. 从 MemAvailable 自动推算安全的专家缓存上限 (cap_for_ram)
5. 从 .coli_usage 学到的热专家固定进 RAM 热存储 (autopin)
6. 从 .coli_kv 恢复上次会话的压缩 KV(对话"暖开机",零重新 prefill)
decode 每个 token:
spec_decode 主循环
├─ mtp_draft: 用 MTP 头起草 G 个候选 token(自回归)
├─ step_all: 一次批量前向,同时算出 [真 token + G 个 draft] 的 logits
│ 对每层 layer_forward:
│ ├─ attention (MLA):
│ │ ├─ q/kv down/up 投影 + 交错部分 RoPE
│ │ ├─ 写压缩 KV (Lc/Rc, 576 floats/token)
│ │ ├─ DSA lightning indexer: 选 top-2048 因果 key(长上下文)
│ │ └─ 吸收路径算注意力,o 投影输出
│ └─ moe (sparse 层) 或 dense_mlp (前 3 层):
│ ├─ sigmoid 路由,选 top-8 专家
│ ├─ 查 pin → ecache → 缺失则 expert_load 从磁盘读
│ │ (合并 pread ~19 MB/专家,可选 O_DIRECT)
│ ├─ gate/up/SiLU/down 专家矩阵乘(IDOT 快路径)
│ └─ 共享专家 + LRU 提升
├─ 验证:逐个比对 draft 与主模型输出(贪心 argmax 或拒绝采样)
├─ 接受连续匹配的前缀,mtp_absorb 把接受的 token 吸收进 MTP 层 KV
└─ pick_tok: 对第一个不匹配位置做温度 + nucleus 采样
持续优化(随会话进行):
├─ pilot_prefetch: 用 L+1 层的路由器预测下层专家,后台 I/O 线程预读
├─ repin_pass: 在安全的回合边界,用热度图把冷的固定专家换成热的
└─ kv_disk_append: 每回合把新增的压缩 KV 追加到 .coli_kv(崩溃安全)
6. 关键设计决策
| 决策 | 说明 | 代价 |
|---|---|---|
| 专家流式读磁盘 | 用磁盘瓶颈换显存瓶颈,让 744B 模型在 25 GB 内存上跑得动 | decode 磁盘受限(冷读 ~11 GB/token),慢 |
| pread + posix_fadvise,不用 mmap | RSS 稠密可控,读完即驱逐页,避免 mmap-rss-bug | 需要手动管理预取/驱逐 |
| int4 权重 + dequant-on-use | 内存/磁盘占用减到 1/4,从不物化反量化矩阵 | 量化误差;采样默认收紧到 0.7/0.90 抵消尾部噪声 |
| IDOT 整数点积 | 量化矩阵乘快 2–3 倍 | 约 0.3% RMS 误差;按 shape 逐个测量决定是否启用 |
| MLA 压缩 KV(576 floats) | KV-cache 小 57 倍,长上下文才装得下 | 需现场重建 k/v,或用权重吸收规避 |
| MTP 投机解码(头必须 int8) | 一次前向多产出 2+ token | int4 头接受率崩到 0–4%;冷缓存下反而更慢(有自适应关闭) |
| 会学习的缓存 + 固定热专家 | 用得越多越快,热专家命中免磁盘读 | 需要 .coli_usage 积累历史 |
| 单文件、零依赖、env-var 驱动 | 极致可移植,gcc glm.c 即可 | 配置分散在环境变量里,不如子命令直观 |
7. 各子系统文档入口
| 子系统 | 文档 | 核心函数 / 文件 |
|---|---|---|
| 代码结构与构建 | 02-代码结构与构建 | glm.c 分区、Makefile、st.h/json.h/tok.h/tier.h |
| 量化与矩阵乘内核 | 03-量化与矩阵乘内核 | QT, pack_int4, matmul_qt, dot_i8i8, dot_i4i8 |
| 初始化与权重加载 | 04-初始化与权重加载 | load_cfg, model_init, qt_from_disk, st_init |
| 推理主流程 | 05-推理主流程 | step, layers_forward, spec_decode, pick_tok |
| MLA 注意力与 KV 缓存 | 06-MLA注意力与KV缓存 | attention, rope_interleave, kv_alloc, kv_disk_append |
| MoE 路由与专家流式加载 | 07-MoE路由与专家流式加载 | moe, expert_load, ecache, tier.h, repin_pass |
| MTP 投机解码与预取 | 08-MTP投机解码与预取 | mtp_draft, mtp_absorb, spec_decode, pilot_prefetch |
| CUDA 后端 | 09-CUDA后端 | backend_cuda.cu, qt_cuda_upload, coli_cuda_matmul |
| 服务与工具链 | 10-服务与工具链 | run_serve, openai_server.py, resource_plan.py, web/ |