colibri Vulkan 计算后端完全指南:在任意 Vulkan 1.2 GPU 上运行 GLM 解码
【免费下载链接】colibriRun frontier MoE models on hardware you already own — pure C, zero deps, experts streamed from disk. Tiny engine, immense model. 🐦项目地址: https://gitcode.com/GitHub_Trending/colibri3/colibri
colibri 内置了可选的 Vulkan compute 后端,能够在任何被 Vulkan 驱动可见的 GPU 上运行完整的 GLM 解码计算路径——不需要 CUDA,也不需要 ROCm。本文以 docs/vulkan.md 为主体,结合仓库源码与测试,系统讲解该后端的构建方法、环境变量、GPU 上执行的部件划分、正确性验证、实测性能、跨后端对比陷阱与已知限制,帮助你在被厂商堆栈放弃的旧卡、AMD 新卡或 APU 上榨出可用的推理性能。
快速上手:构建与运行
Vulkan 后端是**可选(opt-in)**特性,默认构建完全不受影响。构建命令在c/目录下执行:
cd c make glm VK=1 # needs libvulkan + glslc (shaderc) for the shaders在 c/Makefile 中,VK=1会追加-DCOLI_VULKAN编译宏与-lvulkan链接选项,并把四份 GLSL 着色器编译产物纳入构建:
VK ?= 0 GLSLC ?= glslc ifeq ($(VK),1) CFLAGS += -DCOLI_VULKAN LDFLAGS += -lvulkan VK_OBJ = backend_vulkan.o VK_SPV = shaders/qmatmul.spv shaders/qmatmul_gate_up.spv shaders/attention_absorb.spv shaders/rmsnorm.spv endif其中.spv由glslc --target-env=vulkan1.2从同目录的.comp源码生成(Makefile 规则),构建时若找不到glslc会直接报错提示安装 shaderc。
运行一个最简单的例子:
COLI_VULKAN=1 COLI_VK_DENSE=1 COLI_VK_ATTN=1 \ PIN=<model>/.coli_usage PIN_GB=0 COLI_NO_OMP_TUNE=1 \ ./coli run "Hello" --topp 0.7环境要求
- 运行时:
libvulkan以及一块提供 Vulkan1.2ICD 的 GPU,驱动需支持GL_KHR_shader_subgroup_arithmetic扩展(近几年的任意 Mesa RADV、AMDVLK、NVIDIA 或 Intel ANV 驱动均满足)。 - 构建时:
glslc(来自 shaderc)。 - 后端会挑选能力最强的物理设备(独立显卡优先于核显),并且任何失败都会降级到 CPU 路径——一块卡死(wedged)的 GPU 最多拖慢运行速度,绝不会损坏结果。
别忘了 COLI_NO_OMP_TUNE=1
在多核机器上必须设置COLI_NO_OMP_TUNE=1:引擎的 OMP 自调优(自旋等待)在COLI_CUDA/COLI_METAL下会被跳过,但在 Vulkan 下不会。自旋的工作线程会饿死异步 I/O 池——实测 CPU 专家带宽会从 28 GB/s 跌到 5 GB/s。
独立显卡必须先开 Resizable BAR
权重分层分配的是HOST_VISIBLE|DEVICE_LOCAL内存;如果关闭 ReBAR,这种组合只存在于约 256 MB 的 BAR 窗口内,驱动会把超出部分静默放到系统内存里——于是分层报告专家已驻留,但每次访问都在穿越 PCIe,比 CPU 路径还慢。在 RX 9070 XT 上实测,BIOS 开关两侧分别是 0.11 与 0.24 tok/s。引擎在初始化时如果发现 VRAM 的可主机访问切片偏小会打印警告;看到该警告就去 BIOS 里开启 Resizable BAR / Smart Access Memory。统一内存(unified-memory)的 APU 不受影响。
着色器查找路径
编译好的着色器通过COLI_VK_SHADERS定位,可以是qmatmul.spv文件本身,也可以是存放整套.spv的目录;未设置时引擎先找二进制旁的shaders/,再找相对当前工作目录(CWD)的位置。注意 docs/ENVIRONMENT.md 的说明:给出目录时,其余着色器会在同一目录下按文件名被发现。
GPU 上跑什么:三块部件
后端把解码路径按“驻留频率”划分成三块,用三个环境变量独立开关:
| 部件 | 环境变量 | 机制 |
|---|---|---|
| 路由专家(热集) | COLI_VK_EXPERTS=N(默认 320) | 按.coli_usage热度取 Top-N 专家,启动时一次性上传到 VRAM 注册表;解码时直接从 VRAM 提供,不占 RAM 槽、不读磁盘、不预取,以一次异步融合批次(gate+up+silu→down,隐藏层留在设备上)与 CPU 计算其余专家并行。命中率行中显示为vk桶。 |
| 稠密投影 | COLI_VK_DENSE=1 | q_a+kv_a 融合为一次提交,q_b、o 各自成批;共享专家作为单个融合专家组提交。驻留的 int4/int8 权重只上传一次。 |
| MLA 注意力核心 | COLI_VK_ATTN=1 | 每层一次 dispatch:吸收式 query(absorbed query)、KV 窗口上的分数、softmax、加权潜变量、值行,与 o 投影融合(上下文向量从不离开 GPU)。潜变量/rope KV 存放在持久化的逐层设备镜像中,每 token 每层追加约 2.3 KB,失效点与 CUDA KV 影子完全一致。 |
示例命令中的PIN_GB=0(但保留PIN)是有意为之:VRAM 注册表里已经装着与 RAM pin 相同的热专家,pin 的 RAM 应该留给自适应 LRU 缓存。保留PIN是为了防止 AUTOPIN 从历史记录里重新 pin。
这些开关背后是 c/backend_vulkan.h 中的一组 API 契约:
coli_vk_matmul:单个量化 GEMV,fmt=1为 int8、fmt=2为 int4,首次调用上传权重与 scale,之后复用驻留副本;coli_vk_gate_up:专家 MLP 前半段(silu(gate(x))*up(x))在一次 dispatch内完成,x 只读一次;coli_vk_expert_group/_issue/_take:整批专家的融合 MLP 一次提交(同步或异步,隐藏层全程在设备上);异步形态先下发再 join 读回,CPU 与 GPU 重叠计算;coli_vk_attention_absorb/_project:吸收式 MLA 注意力核心,以及融合 o 投影的变体;coli_vk_attn_qprep:q 预备链(q_a+kv_a 对 → q 潜变量 rmsnorm → q_b)在一次提交内完成,需要rmsnorm.spv;coli_vk_alloc_priority/coli_vk_mem_budget:借助VK_EXT_memory_priority/VK_EXT_memory_budget做 VRAM 压力防护——引擎把批量专家层填充的优先级压到 0.4,使过载的堆优先逐出冷专家,绝不动逐 token 的注意力工作集。
着色器层面的实现细节
c/shaders/qmatmul.comp 是解码 GEMV 的核心,注释明确列出了借自 llama.cppmul_mat_vec的三项技术:
x[s,:]只加载一次到共享内存,供工作组内每个输出行复用(x 位于 host-visible 暂存区,避免每次跨 BAR 重读);- 一个 subgroup 对应一个输出行:lane 以完全合并(coalesced)的方式跨过权重字,用一次
subgroupAdd取代屏障密集的共享内存树归约; - 按行网格步进(grid-stride),固定且利于占用率的工作组数量可覆盖任意 O 与任意 subgroup 宽度(RADV wave32/64)。
着色器还实现了多种量化格式:int8、int4(每字节 2 个、nibble−8)、int3-g64(每 64 输入一组、双平面打包、每组一个 f32 scale)、分组 int4,以及 Kimi K3 专家使用的MXFP4(e2m1 nibble、bit3 为符号位,scale 由宿主预展开为 f32,每 32 输入一组)。隐藏维上限 6144(24 KB LDS);超过暂存数组长度的行(如o_proj的I = H*vh = 16384)会跳过共享内存暂存,直接从存储缓冲读取。
c/shaders/attention_absorb.comp 则演示了“单次 dispatch 跑完整注意力核”:每个(query 行 s, head h)一个工作组,吸收式 query、因果分数、softmax、潜变量上下文、值行读取全部在一次提交内完成,L/R 直接读设备端持久缓存。共享内存数组的硬上限为 Q≤256、R≤64、K≤512。
内存类型的两个写合并规则
实测中能让这套设计成立的是两条内存规则(见 c/backend_vulkan.c 中memtype/memtype_cached的选择):
- CPU 要读回的缓冲必须是HOST_CACHED(否则 ReBAR VRAM 读取只有约 40 MB/s);
- 其余一切使用HOST_VISIBLE|DEVICE_LOCAL——在 Strix Halo 这类统一内存平台上,所谓“上传”写进的正是 iGPU 读取的同一块物理 RAM,根本没有 PCIe 拷贝,这正是流式专家在这里能盈利而离散 CUDA 路径刻意回避的原因。
第二设备(COLI_VK_DEV2)
后端还支持把专家层放到第二块 Vulkan GPU 上:COLI_VK_DEV2可设为设备枚举索引或auto(自动选择非 0 号设备的最强真实 GPU),其上下文只承载层专家、只跑异步专家组路径,可与 0 号设备同时在途(backend_vulkan.h)。tensor 会记住自己所在的设备,free/bytes对两者皆可用。
正确性:如何在两个实现之间建立信任
Vulkan 路径与 CPU 路径是同一乘法的两套独立实现,唯一重要的是给出相同的 token:一个解错 nibble、搞反分组方向或打乱 scale 顺序的 kernel 不会报错,它只会给出一个变差的模型。因此验证策略是“比 token,不比 logit”。
1. 原语级精确性测试(文档给出的命令):
gcc -O3 -DVK_TEST backend_vulkan.c -o test_vk -lvulkan -lm && ./test_vk shaders/qmatmul.spv该测试覆盖每个原语:不同形状下的 GEMV int4/int8(含长行 o 投影)、融合 gate+up、完整专家组的同步与异步路径、matmul 对,以及吸收式注意力核心(含因果 S=2、kv_start 窗口、int8 与长上下文)。典型 maxrel 约 1e-5 到 2e-3(fp32 归约顺序差异)。
2. 引擎级一致性:贪婪解码在验证提示词上与纯 CPU 引擎逐 token 完全一致。
3. 权重布局零重打包:int4 权重以偏移二进制(nibble−8)解码,与 CPU 路径的字节级布局完全一致,无需重新打包。
仓库里还有自动化回归测试 c/tests/test_glm53_vulkan.py,它用同一 fixture 分别以纯 CPU 与COLI_VULKAN=1运行引擎各 4 个贪婪 token,比较两者的输出;当二进制未以VK=1构建或没有可用设备时测试声明为SKIP而非假绿色。运行方式:
make VK=1 glm53 python3 tests/test_glm53_vulkan.py --binary ./glm53 --fixture ~/glm53_mm_tiny实测性能(AMD RX 9070,RDNA4,RADV/Mesa 26.1)
文档给出的数据全部来自同一张 RX 9070 卡(RADV/Mesa 26.1):
| 指标 | Vulkan | 对比对象 |
|---|---|---|
| 专家 MLP 原语(K 个专家,int4 6144→2048→6144,含回读) | 0.11–0.13 ms/专家 | 生产 ROCm/HIP 专家组0.179 ms/专家,快约 35% |
| 解码 MLA 注意力核心 | 3.7×快于同卡 HIP kernel | — |
| 端到端 GLM-5.2(744B int4,NVMe 流式)12 核 Zen2 + RX 9070 | 1.7–1.8 tok/s(64-token)/ 1.6(256-token)/1.58 持续(512-token) | HIP 后端同配置 1.5–1.55 |
其中“每调用含回读”意味着 0.11–0.13 ms 已经算上数据从设备回到 CPU 的开销。需要强调的是,端到端 tok/s 取决于整机还有什么在拖后腿(磁盘带宽、CPU 侧专家计算等),原语快 35% 不等于端到端同比例提升。
与其他后端对比时的两个坑
文档特别警告:有两个默认行为会静默扭曲任何 Vulkan 对 CUDA/HIP 的对比:
- MTP 投机解码:CUDA/HIP 构建默认禁用模型草稿(
DRAFT在COLI_CUDA=1下自动解析为 0,见 issue #163),而 CPU 与 Vulkan 运行保持DRAFT=3。于是两组对照跑的是不同的解码循环——投机分支每个产出 token 要路由约 2× 的专家位置(被拒绝的草稿位置仍然付出专家 I/O),这在存储受限的机器上起主导作用。两边输出其实完全一致(贪婪验证无损),所以看起来没有任何异常。必须在两个分支上显式钉住DRAFT=0(或DRAFT=3 COLI_CUDA_MTP=1)。 - GPU 时钟:解码 dispatch 是微秒级突发,本身不足以让 DPM 提频,显存时钟可能整场运行都停在低位。两个分支都要钉住
power_dpm_force_performance_level=high,或者在结果中如实披露时钟状态。
另外注意:运行统计里的experts loaded/token统计的是路由位置数(含被拒绝的投机位置),在咨询任何缓存/分层之前计数——VK 层命中时它不会下降;命中率行里的vk桶才是衡量分层有效性的数字。
环境变量速查
除文档正文外,docs/ENVIRONMENT.md 收录了完整的 Vulkan 相关变量,补充若干重要项:
COLI_VK_DEV:指定主 Vulkan 物理设备枚举索引;未设置时优先独立显卡。COLI_VK_QPREP(默认开):把 Q 预备步(RMSNorm + rope + compress)融合为一次 GPU dispatch 而不是拆开(拆开会付出三个 fence 的代价);2额外保留 CPU 参考副本用于 A/B 对比。COLI_VK_RESERVE_GB(默认 3.0):为惰性分配的稠密权重、KV 镜像与暂存缓冲预留的 VRAM(4k 上下文实测约 1.7 GB,随max_t增长)。仅在驱动报告VK_EXT_memory_budget时有意义,否则只受COLI_VK_EXPERTS数量上限约束。COLI_VK_SPIN_US(默认 300):轮询 fence 的微秒数;0表示总是阻塞(空闲时延迟更低,代价是一个核心空转)。COLI_VK_EXPERTS2/COLI_VK_RESERVE2_GB:dev2 层的专家数上限与保留 VRAM。VK_PROF:设置后计时并上报 Vulkan 专家组路径。- Kimi K3 模型另有两个变量:
K3_VK_GB(K3 专家层的 VRAM 上限,默认取驱动预算)与K3_VK_UP(填层时每步允许的专家上传数,默认 8)。 COLI_VK_TEST_BALLAST:分配 N 个哑 Vulkan 缓冲以复现“VRAM 空闲时解码注意力仍随专家层规模退化”的现象——这是VK_EXT_memory_priority存在的理由(实测 2.6k 缓冲对象 7.9s → 4.3k 时 15.6s,且仍有 2.9 GB 空闲)。
从源码结构看(backend_vulkan.c),后端还维护了一个重提交缓存:当绑定的 tensor/形状/暂存缓冲与上次调用一致时,跳过vkUpdateDescriptorSets与命令重录——这正是热专家被反复调用的典型模式;由于每次调用都会同步等待 fence,不会有提交在途,因此这种“仅在变化时重绑”是安全的。同一段代码也记录了注意力工作集(KV 镜像、暂存)借助 memory-priority 高于批量专家权重的设计依据:实测驻留 7.6 GB 时解码注意力耗时 7.8s,涨到 15.2 GB 时变成 17.8s。
已知限制与未来工作
- 解码聚焦:专家层与注意力核心服务于
S<=4;prefill 使用 CPU/批处理路径(稠密投影在 prefill 阶段可在 VK 上运行)。 - 回退到 CPU 注意力路径:DSA top-k 选择、参差不齐(ragged)的多槽服务、以及量化 KV 缓存。
- 尚未完成:cooperative-matrix(coopmat)prefill kernel、全驻留层流水线、在真实硬件上验证 Polaris/gfx803(着色器使用动态 subgroup 大小,按构造即 wave64 安全)。
小结
Vulkan 后端让 colibri 的“tiny engine, immense model”理念第一次与硬件厂商无关:无论是 ROCm 已放弃的 Polaris(RX 580 经 RADV 可跑),还是 RDNA4 新卡(实测快于同卡 ROCm/HIP),只要驱动提供 Vulkan 1.2 与 subgroup 算术扩展就能接入。配合COLI_VK_EXPERTS的驻留专家层、融合的注意力核与异步专家组,它把“从磁盘流式拉专家”的瓶颈从 I/O 转移到了可并行的 GPU 计算上。动手前请务必确认三件事:构建机有glslc、运行时设置COLI_NO_OMP_TUNE=1、独立显卡开启 Resizable BAR。
【免费下载链接】colibriRun frontier MoE models on hardware you already own — pure C, zero deps, experts streamed from disk. Tiny engine, immense model. 🐦项目地址: https://gitcode.com/GitHub_Trending/colibri3/colibri
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考