第 5 章 · 03 ODIRECT 与读写算重叠 本节摘要:ODIRECT 绕过页缓存做零拷贝直接 DMA,在带 DRAM 缓存的 NVMe 上常是大幅胜出(Blackwell/Windows +34% decode,GB10 iobench 4.25→9.69 GB/s),但 drive-dependent(QLC/无 DRAM/虚拟盘可能中性甚至负向)。readahead/PILOT 预取和批量专家联合读取让一层多个专家合并一次 I/O。OMP 并行 pin/warmup 从两块盘流式拉满。整套 I/O 工程的哲学是:attack the streaming path,而不是假装存储延迟免费——I/O 是引擎的一部分。
本节摘要:O_DIRECT 绕过页缓存做零拷贝直接 DMA,在带 DRAM 缓存的 NVMe 上常是大幅胜出(Blackwell/Windows +34% decode,GB10 iobench 4.25→9.69 GB/s),但 drive-dependent(QLC/无 DRAM/虚拟盘可能中性甚至负向)。readahead/PILOT 预取和批量专家联合读取让一层多个专家合并一次 I/O。OMP 并行 pin/warmup 从两块盘流式拉满。整套 I/O 工程的哲学是:attack the streaming path,而不是假装存储延迟免费——I/O 是引擎的一部分。
内容来源:原项目源码
README.md(L231-253)、c/uring.h
⚠️ 注意:本节是第 5 章的收口节,把 O_DIRECT、预取、批量读、并行加载这四个机制串成一条完整的"I/O 工程"主线。这些机制单独看都是常识,组合起来才是 Colibrì 的杀手锏——承认 I/O 是解码循环的一部分,把每一毫秒的磁盘延迟都和计算重叠掉。
默认的 buffered read 会把磁盘内容先拷到内核页缓存,再从页缓存拷到用户缓冲。对于一次性读取的大块权重(每个专家的三个矩阵),页缓存命中率为零,反而白白多一次内存拷贝,还挤占页缓存空间。O_DIRECT 让 read 直接做 DMA 到用户缓冲,零拷贝。
Colibrì 用 DIRECT=1 开关 O_DIRECT。README 明确指出效果是 drive-dependent:
On real NVMe, measure
DIRECT=1. O_DIRECT bypasses the page cache and is often a large win on drives with DRAM cache and bandwidth headroom (+34% decode measured withPIPE=1on a Blackwell/Windows box; 4.25→9.69 GB/s in iobench on a GB10) — but it is drive-dependent: QLC/DRAM-less or virtualised disks can be neutral to negative. Try it first; keep what your hardware rewards.
实测数据非常亮眼:Blackwell/Windows + PIPE=1 测到 +34% decode 加速;GB10 上 iobench 带宽从 4.25 飙到 9.69 GB/s。但也有反例:QLC 闪存(没有独立 DRAM 缓存)和虚拟化磁盘上,O_DIRECT 可能中性甚至负向,因为这类盘依赖页缓存做合并/预读。所以官方建议永远是"measure first",而不是无脑开启。
O_DIRECT 和 io_uring 是天然搭档:io_uring 的 IORING_OP_READ 直接支持 O_DIRECT 打开的 fd,DMA 完成后内核把 CQE 推到 CQ ring,用户态收割。整条链路从磁盘到用户缓冲没有任何多余拷贝。
需要强调 O_DIRECT 的对齐要求:用户缓冲地址、读偏移、读长度三者都必须按块大小(通常 512 字节或 4KB)对齐,否则 read 直接返回 EINVAL。Colibrì 在分配专家权重缓冲时就按这个对齐分配(典型是 posix_memalign),保证 O_DIRECT 路径不会因为对齐失败。这是"零拷贝"换来的额外约束——buffered read 内核会帮你处理对齐,O_DIRECT 把责任交还给用户态。
预取是降低 miss 延迟的最直接手段。Colibrì 有两条预取路径。
readahead(预读):每次需要某个专家时,把它附近"很可能接下来也要"的专家一起读到内存。这是利用空间局部性的经典手段。
PILOT 路由预取(PILOT=1):一个专门的 router-lookahead 线程,提前一层预测下一层会路由到哪些专家,在当前层计算的同时把下一层的专家权重预取到热存。这是 Colibrì 最聪明的预取——README 给出关键数字:routing is measurably 71.6% predictable one layer ahead。
71.6% 这个数字意味着,下一层的路由结果有 71.6% 的概率能从当前层的路由猜对,所以提前一层的预取命中率很高,大部分 miss 在计算开始前就被填上了。预取和 demand read 都走确定性哈希命中同一块盘(上一节讲过),所以双 SSD 镜像下预取照样享受带宽叠加。
PILOT 的工作方式值得深究:它跑一个轻量的 router-lookahead 线程,在主计算线程处理第 N 层时,这个预取线程已经拿到第 N+1 层的输入(residual stream),跑一次路由 gate,预测 N+1 层会选哪些专家,然后提前发起 io_uring 读。等主线程走到 N+1 层时,那些专家大概率已经在热存里——miss 变成 hit,磁盘延迟被藏在了第 N 层的计算时间后面。这是经典的"预取+计算重叠",但前提是路由可预测,否则预取全是浪费——这正是 Colibrì 用 71.6% 这个数字量化暴露的边界。
MoE 的一个 batch 里,同一层会被多个 token 路由到多个专家。如果每个 token 各自读自己路由的专家,会出现"同一专家被同一层读多次"的浪费。Colibrì 用 batch-union 解决:
batch 内一层所有 token 的路由结果 ↓ 取并集 {expert_A, expert_C, expert_F, expert_G} ← 一层只读这 4 个 ↓ 每个专家的三矩阵相邻存储 一次 pread 把三个矩阵一起读进来
两层合并:第一,batch 内每个 unique expert 只读一次(batch-union);第二,每个 expert 的三个矩阵(gate_up、down 等)在磁盘上相邻存储,合并成一次 pread。这两层合并把 I/O 次数压到最低,syscall 开销和寻道开销都最小化。
这也解释了为什么 io_uring 的批量提交设计如此契合:一个 batch 收集完一层所有 unique expert 的读请求,一次性 submit 一批 SQE,内核 io-wq 并发执行,用户态逐个收割 CQE,全程异步。
启动时的 pin/warmup(把热点专家预加载到热存)也是一个 I/O 密集阶段。Colibrì 用 OpenMP 并行(#pragma omp parallel)把不同专家的加载分发到多线程,每个线程走自己的读路径。
双 SSD 镜像开启时,这些并行加载线程会被确定性哈希分流到两块盘,把两块盘的带宽都吃满。所以 pin/warmup 阶段的加速也享受双 SSD 叠加——9+3 GB/s 这对组合在启动时把热点专家拉进热存的速度,比单 9 GB/s 那块快约 33%。
这是一个很好的"机制协同"例子:O_DIRECT(零拷贝)、io_uring(异步批量)、双 SSD 镜像(带宽叠加)、batch-union(I/O 次数最小化)、OMP 并行(多核压榨)、确定性哈希(预取/需求命中同盘)——这六个机制互不冲突,层层叠加,最终把"磁盘层"的性能逼近"内存层"。
OMP 并行还有一个常被忽略的细节:线程数选择。pin/warmup 阶段是 I/O 密集,线程数过多会让磁盘寻道抖动反而变慢,线程数过少又压不满带宽。Colibrì 默认按"磁盘并发数 × 适度系数"选线程数,而不是简单地用满所有 CPU 核。这种"I/O 阶段不过度并行"的克制,是经验性优化——一味并行反而杀性能。
把整章串起来,Colibrì 的 I/O 工程哲学可以浓缩成一句:attack the streaming path。
Misses are expensive, so the engine spends most of its cleverness avoiding and overlapping them.
很多引擎默认权重都在显存,把磁盘 I/O 当成"启动时的一次性加载成本",推理循环里假装存储延迟免费。Colibrì 反过来:承认 I/O 是 decode 循环里真实存在的一环,把大部分工程聪明才智都花在"避免 miss"和"重叠 miss 延迟"上。
具体手段清单:
PIPE=1(默认开启),缺失专家加载和驻留专家计算重叠。COLI_CUDA_PIPE=2 让 residual stream 留在显存,CPU 专家循环不被打断。每一个机制单独看都是常识,但组合起来构成了一条极致优化的流式路径。这就是 Colibrì 能在 25GB 笔记本上跑 370GB 专家模型的根本原因——它没有"装不下就报错",而是把磁盘层的每一毫秒都榨干。
💡 深潜要点:O_DIRECT、io_uring、双 SSD、batch-union、PILOT、OMP 并行——这六个机制是正交且可叠加的。没有任何一个依赖另一个开启,但开启得越多,流式路径越快。这就是"attack the streaming path"的可操作性含义:不寄望于单一银弹,而是把整条路径上的每一处延迟都重叠或消除掉。
下一节:第 5 章的 I/O 工程讲完了。第 6 章我们深潜
route_trace.h——engine-agnostic 的路由遥测设计,看清 Colibrì 如何让所有引擎发出同样的字节,以及.coli_usage学习型缓存为什么越用越快。