🤏 smolmaxxing

你身边的每一台设备都比它看起来更有能力。电视盒子、旧手机、单板计算机和路由器都配有 CPU、RAM 和闪存,而这些大部分时间都处于闲置状态。smolmaxxing 就是要释放这种能力:找出每台设备真正能运行的最大语言模型,然后通过自定义 kernel、更聪明的内存使用和权重流式加载把它推得更远。每一个结论都在硬件本身上实测。
设备 #1 是一台 Xiaomi TV Box S(第 3 代):Amlogic S7D,4× Cortex-A55 @ 2.5 GHz,1.9 GB RAM(约 1 GB 可用),eMMC 闪存,Android 14,未 root。在这个设备上,本仓库运行从 260 K 到 16 B 参数的模型:
- Qwen3.5-0.8B:16.8 tok/s(是原版 7.5 的 2.2 倍;于 11-05 晚间测量,见 §17)。
- Qwen3.5-2B:真实聊天中 7.7 tok/s。这台盒子的速度在一天内会漂移约 10 %,所以每一步都是背靠背测量:§16 带来 +11 %,§17 又带来 +7 %。
- 一个 8B 混合专家模型(Moon-Beam-8B-A1B):真实聊天中 7.2 tok/s,其专家按需从闪存分页,并提前一层预取(§19)。
- 一个稠密 1-bit 8B 模型(Bonsai-8B):真实聊天中 5.2 tok/s,从 1.2 提升而来:以“无头”模式完全常驻内存,外加一个查找表 kernel(§19)。
- 一个 16B 混合专家模型(Ling-mini-2.0):真实聊天中 5.1 tok/s,在 600 词的回答中保持连贯,同样的专家分页器原样套用(§20)。体积是 Moon-Beam 的两倍,但每字节并不更好:在这台盒子上,更大只有在模型同时更好的时候才有帮助。
- 一个稠密 9B 模型:0.27 tok/s,大部分权重从闪存流式加载。
设备 #2 是一台第一代 iPad Air(2013):Apple A7,2 核,带 NEON 但不带 dotprod,1 GB RAM,iOS 12.5.7,已越狱,仅用 CPU。一台 13 年前的平板以 24-26 tok/s 运行一个 1.7B 参数模型(长对话中 18-22),以约 10 tok/s 运行一个 4B 模型,以约 3.5 tok/s 运行一个 8B 混合专家模型(用一半专家时是 5,但有质量代价),以 2.6-4.5 tok/s 运行一个 16B 模型,它们的专家都从闪存分页,且都来自它自己 Safari 里的一个聊天页面。见设备 #2 上的结果。
每一步都按实验逐个记录在 EXPERIMENT.md 中(E1-E160)。

设备
| 设备 | 状态 |
|---|---|
| Xiaomi TV Box S(第 3 代):Amlogic S7D,Cortex-A55,2 GB | ✅ 完成:260K → 16B 参数,8B 为 5-7 tok/s,16B 为 5 tok/s |
| iPad Air(2013):Apple A7,2 核,1 GB,iOS 12.5.7,已越狱( ipad-air/ ) | ✅ 运行中:Bonsai-1.7B(1-bit)为 24-26 tok/s,长对话中 18-22;Bonsai-4B 约 10 tok/s;Moon-Beam-8B-A1B(MoE)约 3.5 tok/s;Ling-mini-2.0(16B MoE)为 2.6-4.5 tok/s;仅用 CPU |
| 更多电视盒子、手机、单板计算机、路由器…… | 🔜 即将推出 |
这些技术并不绑定于某一台盒子。1-bit 查找表 kernel 只需要基础 NEON,正如 iPad 所展示的;任何带 dotprod 的 ARM 芯片都能用上其他 kernel;任何模型大于 RAM 的 Linux 或 Android 设备都可以使用流式加载引擎。
记分板
| Class | 最佳模型 | 首次运行 → 现在(tok/s) | 提升 | Bits/byte | 靠什么做到的 | 下载 |
|---|---|---|---|---|---|---|
| 0.8B | Qwen3.5-0.8B | 7.5 → 16.8 bench | ×2.2 | ≈1.00 | 全 Q4_K,补齐到 65K 的 head,fp16 DeltaNet 状态 | HF |
| 2B | Qwen3.5-2B | 0.1 → 7.7 chat | ×77 | 0.909 | 全 Q4_K,补齐到 65K 的 head,融合 DeltaNet | HF |
| 4B | Ternary-Bonsai-4B | 2.2 → 4.3 chat | ×2.0 | ≈0.965 | 三值权重放在 2-bit 字段里,合并 scales | HF |
| 8B dense | Bonsai-8B (1-bit) | 1.21 → 5.2 chat | ×4.3 | 0.974 | 1-bit LUT kernel,headless 模式下完全驻留内存 | HF |
| 8B MoE | Moon-Beam-8B-A1B | 1.87 → 7.2 chat | ×3.9 | 0.996 | Q2_K experts 从 flash 分页,缓存感知路由,预取 | HF |
| 9B | Qwen3.5-9B | 0.16 → 0.27 chat | ×1.7 | 0.885 | 权重流式加载,加上它自己的 MTP head | rebuild ( q9 ) |
| 16B MoE | Ling-mini-2.0 | 4.1 → 5.1 chat | ×1.2 | 1.054 | Moon-Beam 那套做法,原样照搬 | HF |
“首次运行”指每个模型第一次在这台机器上跑起来时的表现:
- 0.8B:原版 Q4_K_M 文件。
- 2B:原版文件,因为塞不进 RAM,所以从 flash 流式加载。它第一个能放得下的构建跑到了 4.8,所以从那之后算起的提升是 ×1.6。
- 4B:原版文件,频繁抖动(thrash)。
- 8B dense:最后 0.1 GB 从 flash 流式加载。
- 8B MoE:第一次运行 expert pager,没有路由偏置。
- 9B:流式加载。
- 16B:一开始就用 Moon-Beam 的做法,所以提升很小。
在这台机器上,2B 是速度与质量之间最好的平衡。Ling-mini-2.0 说明更大并不自动更好:它的体积是 Moon-Beam 的两倍,但每字节得分更差,跑得也更慢。
我们做的东西,按它所攻克的瓶颈分组(细节和数字见 What's in here):
| 瓶颈 | 创新 | 最大收益 |
|---|---|---|
| 计算(顺序执行的 A55,10 项) | 面向 vdotq_laneq 的 Q2_K 与三值重打包,1-bit bit planes,1-bit 查表 kernel,三值放在 2-bit 字段中,带跳过阈值的 Q8_0 attention,融合 Gated-DeltaNet,按 id / fp16 读取 DeltaNet 状态,LM head 走快 kernel,受引导的 matmul 调度 | Q2_K 7.8 → 13.9 G weights/s;Bonsai-8B 3.77 → 5.1-5.4;4B 3.2 → 4.3;长回答不再把速度砍半 |
| RAM(约 1 GB,5 项) | 剪枝的 LM head,无损 1.875-bit 三值,headless 模式,无需 fork 的内存保护,flash 上的重打包缓存 | 每个模型 −0.2 到 −0.5 GB;空闲 RAM ~1,000 → ~1,300 MB;Bonsai-8B 在任何 kernel 工作之前 1.2 → 3.6 |
| Flash(278 MB/s,5 项) | 使用 O_DIRECT 的权重流式加载,受 flash 限制时的 MTP 推测,expert pager,缓存感知路由,预门控预取 | 9B +69 %;expert 未命中 13 % → ~2 %;Moon-Beam 4.4 → 7.2 |
| 输出质量(2 项) | 长回答退化测试,覆盖 256 个 token 的 1.15 重复惩罚 | 退化的长回答 12/13 → 1/13 |
设备 #1 上的结果
质量是在一份固定的 22 KB 文本上测得的英文 perplexity,换算成 bits per byte,以便让分词器不同的模型之间公平比较(越低越好)。速度是在电视上生成时的速度,通过 llama-server 发送真实聊天请求(“chat”)或使用 llama-bench(“bench”)测得。
| 模型 | 包装上标称的大小 | 质量(比特/字节) | 生成 | 提示词处理 | 备注 |
|---|---|---|---|---|---|
| stories260K (llama2.c) | 1 MB | 不适用 | ~650 tok/s | 不适用 | 第一个跑起来的东西(§1-8) |
| Gemma 3 270M Q4_K_M | 253 MB | 不适用 | 22 tok/s | 73 tok/s | |
| Qwen3.5-0.8B Q4_K_M(原版) | 497 MB | 不适用 | 7.5 bench | 28 tok/s | |
| Qwen3.5-0.8B,全 Q4_K + 65K head | 442 MiB(566 MB RSS) | ≈1.00* | 16.8 bench | 33 tok/s | 目前同质量下速度最快(§15-17) |
| Qwen3.5-2B,全 Q4_K + 65K head | 0.99 GB | 0.909 | 7.7 chat | 17.8 | 默认选择:综合最佳(§11、§16、§17) |
| Ternary-Bonsai-4B,合并 scales + 2-bit 字段 | 1.03 GB | ≈0.965 | 4.3 chat(4.8 bench) | 9.6 | 最佳 4B:同一构建上此前为 3.2 chat / 7.0 prompt(§18) |
| Moon-Beam-8B-A1B(MoE),Q2_K experts + 60K head | 0.48 GB GGUF + 2.6 GB repack 缓存在 flash 上,~1.1 GB RSS | 0.996 | 7.2 chat | 不适用 | 最快 8B:128 个 experts,用 6 个;expert 分页器、缓存感知路由、预门控预取、针对长回答循环的重复惩罚(§19,无头模式) |
| Ling-mini-2.0(MoE,16.3 B),Q2_K experts + 60K head | 0.47 GB GGUF + 5.3 GB repack 缓存在 flash 上,~1.13 GB RSS | 1.054 | 5.1 chat | 不适用 | 可用速度下最大的模型:256 个 1 MB 的 experts,用 8 个,每层池 43 个,bias 0.3(§20) |
| Bonsai-8B,1-bit,LUT kernel | 0.96 GB,全部常驻 | 0.991(head 上为 0.974) | 5.2 chat(5.1-5.4 bench),~900 tokens 上下文时 4.0 | 5.8-6.1 | 最佳 dense 8B:无头模式,Q8_0 attention(§19) |
| Bonsai-8B,1-bit + 紧凑 scales | 0.84 GB + 0.12 GB 流式读取 | 0.991 | 1.21 chat(2.2 bench) | 4.9 | §14,无头模式之前 |
| Ternary-Bonsai-8B | 0.89 GB + 0.80 GB 流式读取 | 0.919 | 0.31 bench | 3.45 | §14 |
| Qwen3.5-9B UD-IQ2_XXS + MTP | 0.63 GB + 1.6 GB 流式读取 | 0.885 | 0.16 chat,用 MTP 时 0.27 | 0.8 | 质量最佳;受限于 flash(§14) |
- 在剪枝后 head 的位置上测得(1.3 % 的 tokens 落在其外),因此不完全可比;原版全词表 Q4_0 文件得分为 1.024。
其他小模型,原版文件(筛选,§15 E54):
| 模型 | 生成(bench) | 提示词处理 | 比特/字节 |
|---|---|---|---|
| LFM2.5-350M (QAD Q4_0) | 23.7 tok/s | 104 tok/s | 1.805 |
| LFM2-700M (Q4_0) | 11.5 tok/s | 52.8 tok/s | 1.169 |
| Qwen3-0.6B (Q8_0) | 9.35 tok/s | 50.6 tok/s | 1.059 |
| LFM2.5-1.2B (QAD Q4_0) | 8.3 tok/s | 31.6 tok/s | 1.133 |
| Llama 3.2 1B (Q4_0) | 7.0 tok/s | 29.5 tok/s | 0.998 |
| Ternary-Bonsai-2-2B (Q2_0) | 5.9 tok/s | 15.8 tok/s | 1.124 |
| Gemma 3 1B (Q4_K_M) | 5.7 tok/s | 11.1 tok/s | 1.260 |
| Granite 4.0 H 1B (Q4_0,~1.5 B) | 原版太大跑不动 | 0.916 | |
| ↳ 重建版:全 Q4_K,Q8_0 embedding,48K head (E62) | 5.29 tok/s(仅在释放 RAM 后才放得下,950 MB RSS) | 0.919 | |
| LFM3-1.7B (QAD Q4_0) | 放不下 |
Granite 4.0 H 1B 是唯一接近 2B 质量的模型。按我们的方式重建后(E62),它以 5.29 tok/s 运行,低于 2B 的 6.55,质量也略低:它每个 token 携带的权重字节数约为 0.8B 的两倍。
这台机器的物理限制:在 Android 开始杀进程之前,一个进程大约能占 ~0.96 GB(无头模式下约 ~1.15 GB,前提是强制停止启动器和 Google 服务),而 flash 读取最高 278 MB/s,单次 ~1 MB 的随机读取耗时约 ~6 ms。一个每 token 需要流式读取 X MB 的模型,速度永远不可能超过 278 / X tokens 每秒。限制 2B 以上一切的就是这一点,而不是 CPU,这也是为什么混合专家模型——每个 token 只触及 8 B 权重中的 ~1 B——是突破它的途径。
8 B 跑到 10 tok/s?实测,不是猜测(E98):Moon-Beam 在每 token 2 个 experts 时达到 10.4 tok/s,但文本变成乱码;最佳连贯设置约为 ~7-8 tok/s。Dense Qwen3-8B 就是 1 bit 的 Bonsai-8B,而最小的 Qwen MoE(35B-A3B)每 token 触及的权重是 Moon-Beam 的 3 倍。
设备 #2 上的结果
文章标题:我在让 LLM 在小旧设备上运行
正文: 一台 2013 款 iPad Air:Apple A7,2 核,不带 dotprod 的 NEON,1 GB 内存,iOS 12.5.7,用 Amethyst 越狱。一切都在 CPU 上用 2 个线程、1,024 token 上下文运行,从 Mac 交叉编译。详细记录是 EXPERIMENT.md 的 §21,构建步骤在 ipad-air/README.md。
| Model | File | Generation (tok/s) | Peak RSS | Fixed three-question test |
|---|---|---|---|---|
| Bonsai-1.7B Q1_0 (1-bit) | 237 MiB | 23.7-25.8,长对话中 18-22 | 605 MB | 三题全对,还能写出正确代码:就用这个 |
| Bonsai-4B Q1_0 (1-bit),-c 512 -b 64 | 546 MiB | 9.4-11.4,长对话中 9.2-10.3;作为服务器时,iOS 几分钟内就会把它重置 | 615-682 MB | 法国那题答对,潮汐基本正确,召回每次失败;每字节更好(1.073 对 1.158 bits/byte) |
| Moon-Beam-8B-A1B(MoE,Q2_K experts),expert pager,-c 512 -b 64 | 2.6 GiB(映射 474 MB) | 较长回答 3.5-3.6;加 --experts 4 为 4.9-5.1(困惑度 +27 %) | ~600-625 MB | 三题全对;适合会话的模型,对常开服务器来说太大 |
| SmolLM2-360M-Instruct Q4_K_M | 258 MiB | 10.9-12.5 | 269-316 MB | 三题全对;最轻的好选择 |
| Gemma 3 270M-it Q4_K_M | 241 MiB | 15.4-19.0 | 248-294 MB | 潮汐题答错,召回题空回复 |
| SmolLM2-135M-Instruct Q4_K_M | 101 MB | 18.6-29.3 | 141 MB | 法国和召回题答对,潮汐题较弱 |
| Qwen3.5-0.8B Q4_K_M | 508 MiB | 约 0.2 | 506 MB | 反复颠簸 page cache,手动停止 |
只有 Bonsai 和 Moon-Beam 两行是在最新构建上测得的。其余各行早于下文的 attention 和 sampler 工作。
| Class | Best model | First run → now (tok/s) | Gain | What got it there |
|---|---|---|---|---|
| 1.7B | Bonsai-1.7B (1-bit) | 2.75 → 23.7-25.8 | ×8.6 | 无乘法、查表式且不带 dotprod 的 1-bit kernel,LM head 走快速 kernel,带剪枝的行序 attention,单遍 sampler |
| 4B | Bonsai-4B (1-bit) | 9.4-11.4 | 1.7B 的 kernel,每个 K head 4 个 q head 的 attention,-c 512 -b 64 以保持在约 700 MB 上限之下 | |
| 8B dense | Bonsai-8B (1-bit) | 0.08 | 放不进内存;每个 token 都要从 flash 重读文件的大部分 | |
| 8B MoE | Moon-Beam-8B-A1B | 无法加载 → 1.6 → 3.5(4 个专家时 5.0,困惑度 +27 %) | ×2.2 | expert pager,Q2_K 查表 kernel,预取,cache bias 0.03,基于频率的淘汰 |
| 16B MoE | Ling-mini-2.0 | 无法加载 → 2.6(+2.6 % 时 3.3,bias 0.3 时 4.5) | 新增 | Moon-Beam 的方案,以及一个通过 HTTP 组装并直接流式传输到 iPad 的模型文件 |
“无法加载”:iOS 12 允许一个进程最多映射约 1.7 GB,所以原版 llama.cpp 在 2.6 和 5.5 GB 的文件上还没跑一个 token 就失败了。Moon-Beam 的“首次运行”是第一次 expert-pager 运行(pool 8,无 bias)。
我们在 iPad 上构建的东西,按所攻克的瓶颈分组:
| 瓶颈 | 创新 | 最大收益 |
|---|---|---|
| 计算(2 核 A7,无 dotprod,6 项) | 无乘法 1-bit 内核、将电视的 1-bit 查找表内核移植到基础 NEON、绑定 LM head 的查找表副本、带剪枝的行序注意力(每个 K head 对应 2 或 4 个 q head)、带 40 项堆的一次性采样器、用于专家的 Q2_K 查找表内核 | Bonsai-1.7B 2.75 → 3.45 → ~11 → 23.7-25.8 tok/s;采样每 token 3 → 0.23 ms;仅靠 Q2_K 内核,Moon-Beam 2.94 → 3.06 |
| 地址空间和 RAM(约 1.7 GB 可映射,约 700 MB 可用,4 项) | 仅映射非专家部分的专家分页器(experts-last 文件)、为 bailingmoe2 和 bailingmoe3 剪枝 LM head、小批量( -c 512 -b 64 )、为 iOS 12 编译时移除 std::filesystem | 8B 和 16B MoE 模型终于能加载,占用 556-625 MB;Bonsai-4B 可容纳 |
| Flash(约 260 MB/s,5 项) | 带 4 个读取器的无缓存 pread、按需要时间排队的读取、预门控预取、缓存感知路由偏置、衰减频率驱逐 | Moon-Beam 1.6 → 3.5 tok/s,且无质量损失;仅靠驱逐,未命中率 42 % → 38 % |
| 工具(5 项) | 带缺页计数的算子分析器、专家跟踪和缓存模拟器、设备端困惑度( tinyppl )、通过 HTTP range 请求进行模型组装、可杀死孤立运行的基准测试 | 找到从未进入快速内核的 LM head(72 ms 中的 45 ms);排除了软缺页理论;用 1.4 GB 的 Mac 磁盘构建了 5.5 GB 模型 |
Bonsai-1.7B 如何从 2.75 提升到约 25 tok/s(全程使用同一个模型文件):
| 步骤 | tok/s | 变化 |
|---|---|---|
| 原版 llama.cpp,2 线程 | 2.75 | 用扩展乘法模拟 dotprod |
| 无乘法 NEON 内核 | 3.45 | 权重是 ±1,因此无需乘法;输出完全相同 |
| 来自设备 #1 的查找表内核,移植到基础 NEON | 约 11 | 其 dotprod 要求原来只是一个假设 |
| 该内核上的输出头 | 18-22 | 151,669 行不是 32 的倍数,因此绑定头从未进入该内核:每 token 72 ms 中的 45 ms |
| 带剪枝的行序注意力,以及不对词表排序的采样器 | 23.7-25.8 | KV 缓存将一行中的所有头并排存储,因此一次处理一个头会不断错过缓存 |
在长聊天中(400 token 的回答),仅通过注意力相关工作,三轮就从 15.4 / 7.6 / 8.2 提升到 21.8 / 17.9 / 18.4 tok/s。让长聊天保持在 17 以上的剪枝阈值,在 1,024 token 上下文时带来 +0.42 % 的困惑度,接近 ±0.3 % 的噪声下限;GGML_FA_EPS=0.0001 以约 15 tok/s 换回这一点。
一个 8B 模型,通过专家分页器运行。iOS 12 将进程的映射地址空间上限设为约 1.7 GB(Web 服务器的用户为 1.3 GB),因此 Moon-Beam-8B 的 2.6 GB 文件甚至无法被映射,而保留更少的专家以使其适配,会让其困惑度增加一倍以上(§21.7)。Dense Bonsai-8B 从闪存运行的速度为 0.08 tok/s。因此,模型文件被重写,将其专家放在最后,只映射它们之前的 474 MB,每层在 RAM 中保留 14 个专家,当路由器选中其他专家时,用 pread 从闪存读取它们。结果与未分页模型完全一致。一个偏向已在 RAM 中的专家的较小路由偏置(+0.3% 困惑度,在噪声范围内)使其在约 600 MB 下达到 2.4-2.8 tok/s(§21.8)。第 2 轮(§21.9)按需要时间对闪存读取排队,预取下一层可能用到的专家,并通过针对 Q2_K 的查找表内核运行这些专家:3.4 tok/s,是稠密 8B 的 40 倍,在完整评估文本上困惑度 +0.7%。闪存仍然是瓶颈:每个 token 以 266 MB/s 读取约 53 MB。将权重接入 RAM 导致 iPad 内核恐慌,就像之前的 16 池那样。第 3 轮(§21.10)按衰减后的使用频率而不是最近使用时间来驱逐,达到 3.5 tok/s 且无质量损失。5 tok/s 需要每个 token 使用 4 个专家而不是 8 个(困惑度 +27%);按 token 自适应选择数量并不比固定数量更好。一个 16B 模型(§21.11):Ling-mini-2.0,16.3B 参数,每层 256 个小专家,通过同一个分页器以 2.6 tok/s 运行且路由不变;在困惑度 +2.6% 时为 3.3 tok/s,使用强缓存偏置时为 4.5 tok/s,占用不到 580 MB。它的 5.5 GB 文件通过 HTTP 范围请求从两个已发布的量化版本组装而成,并直接流式传输到 iPad,因为 Mac 上没有空间存放它。它的规模是 Moon-Beam 的两倍,速度相同,但并不是更好的模型(困惑度 +3%)。
![iPad 上 Moon-Beam-8B-A1B 和 Ling-mini-2.0 的速度与困惑度代价对比:Moon-Beam 在无代价下为 3.5
| 内核 | 之前 → 之后 | 位置 |
|---|---|---|
| Q2_K,为 vdotq_laneq 重新打包成 8 行 × 4 字节(上游仅在 x86 / RISC-V 上对 Q2_K 重新打包) | 7.8 → 13.9 G 权重/s;生成 +41 %,prompt 2.4× | E34 |
| TQ1_0 / TQ2_0 三值,重新打包 | 9.5 → 14.4 / 11.9 → 19.1 G 权重/s | E38 |
| 三值 Q2_0 → 在 RAM 中无损压缩到 1.875 比特/权重(三进制打包,保留每 64 个权重的 scale) | Ternary-Bonsai-4B 从“抖动”变为 3.2 tok/s | E39、E42 |
| Q1_0 位平面:vtst 直接把一个位平面变成 vdot 的掩码,无需查表;8 位逐行仿射 scale(−5.6 %,0.07 % 误差) | Bonsai-4B 3.02 → 4.22 tok/s;Bonsai-8B 可装入 0.96 GB | E44、E52 |
| 融合的 Gated-DeltaNet 单步,直接写入状态(Qwen3.5) | 2B 6.0 → 6.3-6.7 tok/s | E28 |
| DeltaNet 状态按 id 就地读取,而不是每个 token 都做 1 MB/层的 gather;fp16 状态( LLAMA_STATE_F16=1 ,UI 中默认开启) | 0.8B 11.9 → 12.9 tok/s;2B +2.5 %,困惑度 +0.15 % | E56、E61、E65 |
| LM head 跑在快速内核上:65,668 行的 head 从未被重新打包(ggml 只对能被 8 整除的行数重新打包),因此额外加了 4 个 token 行;embedding 行随后从重新打包后的 head 重建(8 行交错的逆操作),而不是从 flash 读取;单 token 的 DeltaNet conv 单步由 4 个节点融合为一个 NEON 操作 | 2B +7 %(基准测试和聊天);0.8B +9.5 % | E69-E72 |
| 三值权重放进 2 位字段,scale 每 128 个合并一次:在顺序执行的 A55 上,一个三进制位每 16 个权重约需 5 个 NEON 操作,而一个 2 位字段只需 2 个,所以多花 13 % 的 RAM 换来速度。合并相邻 scale 的代价是困惑度 +0.18 % | Ternary-Bonsai-4B 聊天中 3.2 → 4.3 tok/s,prompt 7.0 → 9.6 | E76、E77 |
| 1 位查表内核(Four-Russians / T-MAC 风格):每组 32 行,每 4 个激活值一张 16 项的表,直接由 float 激活值构建,每 128 次乘加约 8 个 NEON 操作 + 2 次加载,对比 16 + 4 | Bonsai-8B 3.77 → 5.1-5.4 tok/s(配合 E82、E88) | E81 |
| 带阈值的 Q8_0 注意力:先用 sdot 算完所有 K 点积,这样 softmax 的最大值一开始就已知,随后对低于 max + ln(1e-4) 的行跳过 expf 和 V 的计算 | 一个 1,000 token 的回答不再速度减半(结束时 2.50 → 约 4 tok/s) | E86 |
| 引导式 matmul 调度:每个线程取 remaining / (2 × threads)行,这样慢的或被抢占的线程只留下很短的尾巴,而不是每个 token 约 28 ms 的空闲核心;单行逐元素操作按列切分(ggml 把它们全留在线程 0 上) | 2B 6.26 → 6.54 tok/s;0.8B +1.4 % | E63、E64 |
内存技巧:
- 剪枝 LM head:只保留约 49-66 K 个最有用的词表行,按英文、代码和另外 7 种语言的 token 频率挑选。其余 logits 散射到 −inf。省下 0.2-0.5 GB,漏掉约 1 % 的英文 token。
- flash 上的重打包缓存(GGML_REPACK_CACHE):重新打包的权重写入文件一次,之后直接映射。模型几秒内加载完成。
无头模式(tools/tv-llm-mode.sh on|off):先启动一个很小的前台 activity,然后强制停止启动器、Google 服务和其他重量级应用,并用 svc power stayon 让动态屏保(占一个核心的 17 %)不出现。除了保持唤醒之外没有任何持久改动,关掉即恢复。MemAvailable 从约 1000 MB 提升到约 1300 MB。一个不 fork 的守护进程(tools/tv-watch.c)取代了原先每秒启动约 25 个进程、吃掉 10 % 速度的 shell 循环。
面向混合专家模型的 expert pager(ggml-cpu/moe-pager-tv.inc):
- 专家存放在 flash 上:每个 expert tensor 都叠加在一段空的匿名内存上,某一层完成路由之后,立刻用并行 O_DIRECT 把缺失的专家直接读入各自的地址;每层一个 LRU 池(128 个专家中的 43 个)保留其余部分。内核看到的仍是普通的 tensor。一旦重打包缓存中保存了这些数据,GGUF 自带的专家字节就被 hole-punch 掉,因此模型在 flash 上只占用一份权重。
- Cache-aware routing(GGML_MOE_BIAS):给已经在 RAM 里的专家一点小加成,能把未命中率从 13 % 降到约 2 %,代价是可测量的质量损失(arXiv 2412.00099 在其他模型上有这个思路)。每个 token 用更少的专家(LLAMA_MOE_USED=6)是另一个可调项。
- 从每层最常用专家的列表热启动,加上预判式预取:把第 L+2 层的 router 作用在第 L 层的 hidden state 上,预测 router 实际会选什么(sigmoid、模型自身的选择偏置和缓存加成),在约 60 % 的未命中被需要之前就把它们加载进来。
- 答案为什么会循环,以及修复办法:2-bit 专家在约 250-300 词后破坏长文本连贯性,而 routing bias 会放大它。DRY sampling 只是把它藏起来(模型会换个说法重复自己的循环);在 256 个 token 上施加 1.15 的重复惩罚,能把电视上退化的长回答从 12/13 降到 1/13(E99、E100)。
面向大于 RAM 的模型的权重流式加载(ggml-cpu/stream-tv.inc):
- 常驻 RAM 的部分:选定的一部分权重保持常驻。其余的存在 stream zones 里。
- 预取:每个 matmul 上挂一个 hook,记录权重被使用的顺序。读取线程随后用并行的 8 MB O_DIRECT 读提前把 zone 权重直接读进权重自己的地址,达到 flash 的完整 278 MB/s。
- 正确性:每个计算线程都会等到自己的权重完全加载完成,所以预测错误只花时间,绝不会影响正确性。
- 用后即弃:每个 zone 权重在用完后立即释放。
- 校验模式:GGML_STREAM_VERIFY=1 会把每个流式张量与文件比对。它发现了两个静默损坏 bug,已修复并记录在 E48 和 E50 中。
- 投机解码:借助模型自己的 MTP head,当这一遍受 flash 带宽限制时,验证 2 个 token 大约只花一遍遍历权重的成本,所以是划算的(+69 %)。在内存中的 2B 上则是亏的(E31)。
性能分析(补丁里的环境变量开关):GGML_OPPROF=1 给出逐算子表,GGML_OPPROF=2 给出单个 graph 的逐节点、逐线程 trace(tools/optrace.py),LLAMA_UBATCH_PROF / GGML_CPU_PROF 给出各阶段耗时。它们揭示了小模型的时间到底花在哪(E57):约 71 % 在 Q4_K matmul,而 barrier 和调度每 token 只有约 2 ms。
- 流式 GGUF 重写器:在一次顺序遍历中加上剪枝后的 head、额外的张量和元数据,这样 Hugging Face → Mac → TV 的流水线从不需要把模型存在 Mac 上。
- MTP fetcher:只从 safetensors 中按范围读取 MTP 张量。
- 内存守卫:包住电视上每一次重负载运行。
- 基准测试和 perplexity harness、一个能把任意 Hugging Face GGUF 流式传到电视并打分的模型筛选器(tools/screen-small.sh),以及一个实时 token 流 web UI。
快速开始
你需要:带网络 ADB 的电视盒子(或其他 ARMv8.2 Android 设备)、adb、zig(用于静态 aarch64 musl 构建)、cmake + ninja、python3,以及 uv(用于 numpy)。
export ADB_SERIAL=<tv-ip>:5555 # every script talks to the box through this
adb connect $ADB_SERIAL
# 1. llama.cpp v0.8.0 + 补丁,为电视交叉编译
git clone https://github.com/ggml-org/llama.cpp third_party/llama.cpp cd third_party/llama.cpp && git checkout 8642541 && git apply ../../patches/llama.cpp-v0.8.0-tvbox.patch cmake -S . -B build-tv -G Ninja -DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=aarch64 \ -DCMAKE_C_COMPILER=$PWD/../../zigcc/cc -DCMAKE_CXX_COMPILER=$PWD/../../zigcc/c++ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_EXE_LINKER_FLAGS=-static -DBUILD_SHARED_LIBS=OFF -DGGML_NATIVE=OFF \ -DGGML_CPU_ARM_ARCH=armv8.2-a+dotprod+fp16 -DGGML_OPENMP=OFF -DLLAMA_OPENSSL=OFF -DLLAMA_BUILD_TESTS=OFF ninja -C build-tv llama-server llama-cli llama-bench llama-perplexity llama-quantize cd ../.. adb -s $ADB_SERIAL shell mkdir -p /data/local/tmp/llm/patched25 /data/local/tmp/llm/patched adb -s $ADB_SERIAL push third_party/llama.cpp/build-tv/bin/llama-{server,cli,bench,perplexity} /data/local/tmp/llm/patched25/ adb -s $ADB_SERIAL push third_party/llama.cpp/build-tv/bin/llama-quantize /data/local/tmp/llm/patched/ # used by tools/tv-quant.sh zig cc -target aarch64-linux-musl -O2 -static -o gguf_recut tools/gguf-recut/gguf_recut.c && adb -s $ADB_SERIAL push gguf_recut /data/local/tmp/llm/
# 2. 模型:直接从它们的 Hugging Face 源重建到电视上(见下文)
tools/rebuild-models.sh 2b # also: 08 4b b8 tb8 q9 mb lm caches
#
现成可下载的文件。每个体量级别中最好的文件都在 Hugging Face 上,与电视上运行的完全一致。它们需要上面打过补丁的 llama.cpp;原版 llama.cpp 无法加载剪枝后的 head。每张卡片都附有运行命令和实测数据:
| 模型 | Hugging Face | 文件 |
|---|---|---|
| Qwen3.5-0.8B, 16.8 tok/s | hyperionaidev/Qwen3.5-0.8B-tvbox-GGUF | q08-q4k-h65p8.gguf |
| Qwen3.5-2B, 7.7 tok/s | hyperionaidev/Qwen3.5-2B-tvbox-GGUF | w-q4k-h65p8.gguf |
| Ternary-Bonsai-4B, 4.3 tok/s | hyperionaidev/Ternary-Bonsai-4B-tvbox-GGUF | tb4-q20-hml.gguf |
| Moon-Beam-8B-A1B (MoE), 7.2 tok/s | hyperionaidev/Moon-Beam-8B-A1B-tvbox-GGUF | mb-q2-h60.gguf + moe-warm-moonbeam.txt |
| Bonsai-8B (1-bit), 5.2 tok/s | hyperionaidev/Bonsai-8B-tvbox-GGUF | b8-q1-hml.gguf |
| Ling-mini-2.0 (16B MoE), 5.1 tok/s | hyperionaidev/Ling-mini-2.0-tvbox-GGUF | lm2-q2-h60.gguf + moe-warm-ling.txt |
用 hf download hyperionaidev/<repo> --local-dir models/ 下载,然后通过 adb push 把文件推到 /data/local/tmp/llm/,并把 moe-warm-*.txt 放在它们旁边。这样就替换了 08 / 2b / 4b / mb / b8 / lm 这些目标。对于 8B 和 16B 模型,首次启动会写入重打包缓存。
iPad Air 下载。每个模型都在 iPad 上运行过,这里列出它们的获取位置和在那里的速度。三个 MoE 文件是我们自己构建的,针对 iPad 的专家分页器和查找表内核进行了布局:它们需要 iOS 12 补丁(ipad-air/),其他任何 llama.cpp 虽然能加载它们,但专家会被打乱。其余模型完全按发布版本使用;Bonsai 模型的快速布局在加载时于 RAM 中构建。共 53 项实验(E108-E160,§21):
| 模型 | 下载 | iPad 速度 (tok/s) | 峰值 RSS | 实验 |
|---|---|---|---|---|
| Ling-mini-2.0 (16B MoE) | hyperionaidev/Ling-mini-2.0-ipad-GGUF : lm2-q2-h60-lut.gguf (SHA-256 3e464310…4e50bd ) | 2.6 无偏置;加偏置 0.1 时 3.3(困惑度 +2.6 %);加偏置 0.3 时 4.5;加 --experts 6 时 5.2 | 556-576 MB | 4 (E157-E160) |
| Moon-Beam-8B-A1B (MoE) | hyperionaidev/Moon-Beam-8B-A1B-ipad-GGUF : mb-q2-h60-lut.gguf (SHA-256 acf1cb56…94b976 );未经重打包的 experts-last 文件约慢 4 %,为 hyperionaidev/Moon-Beam-8B-A1B-ipad-xl-GGUF : mb-q2-h60-xl.gguf ( aa919400…871d7e ) | 3.5-3.6 ;7 个专家时 3.7-4.0(+0.4-1.9 %);4 个专家时 5.0(+27 %) | 600-625 MB | 19 (E138-E156) |
| Bonsai-8B (1-bit, dense) | prism-ml/Bonsai-8B-gguf | 0.08:放不进 RAM,从闪存读取 | 629 MB | 4 (E134-E137,与最初的 Moon-Beam 尝试一起) |
| Bonsai-4B (1-bit), -c 512 -b 64 | prism-ml/Bonsai-4B-gguf | 9.4-11.4 ;长对话中 9.2-10.3 | 615-682 MB | 5 (E129-E133) |
| Bonsai-1.7B (1-bit), 默认 | prism-ml/Bonsai-1.7B-gguf | 23.7-25.8 ;长对话中 18-22 | 605 MB | 11 (E118-E128) |
| SmolLM2-360M-Instruct Q4_K_M | bartowski/SmolLM2-360M-Instruct-GGUF | 10.9-12.5 | 269-316 MB | 10 (E108-E117:最初的运行、小模型和网页聊天) |
| Gemma 3 270M-it Q4_K_M | unsloth/gemma-3-270m-it-GGUF | 15.4-19.0 | 248-294 MB | |
| SmolLM2-135M-Instruct Q4_K_M | 原版 Q4_K_M | 18.6-29.3 | 141 MB | |
| Qwen3.5-0.8B Q4_K_M | unsloth/Qwen3.5-0.8B-GGUF | 约 0.2:频繁换页 | 506 MB | |
| stories15M q4_0 | 原版 | 在 llama-simple 中约 290 |
| TV 上的文件 | 目标 | 构建来源 | SHA-256 |
|---|---|---|---|
| w-q4k-h65.gguf (Qwen3.5-2B) | 2b | unsloth/Qwen3.5-2B-GGUF BF16 + imatrix_unsloth,MTP 块来自 Qwen/Qwen3.5-2B,在 TV 上量化,头部文件为 data/head-ids-ml60k.txt | 16057be3…3285b |
| w-q4k-h65p8.gguf (Qwen3.5-2B,默认) | 2b | 上一个文件在 TV 上用 data/head-ids-ml60k-p8.txt 重新切分(65,672 行,因此头部被重新打包,§17) | 631a27ae…54fd11 |
| w-q4k-h45.gguf (可选) | 2b45 | 同上,45,654 行的头部 data/head-ids-ml40k.txt(与 E29 的 H45 文件逐字节相同) | a50eadc5…664d5b |
| q08-q4k-h65.gguf (Qwen3.5-0.8B) | 08 | unsloth/Qwen3.5-0.8B-GGUF BF16 + 其 imatrix_unsloth,纯 Q4_K,头部 data/head-ids-ml60k.txt,在 Mac 上量化 | 901f3838…8523a6b |
| q08-q4k-h65p8.gguf | 08 | 上一个文件在 Mac 上用 data/head-ids-ml60k-p8.txt 重新切分(§17) | 7dd2aac5…52fc232 |
| tb4-q20-hml.gguf | 4b | prism-ml/Ternary-Bonsai-4B-gguf Q2_0_g64,头部 data/head-ids-qwen3-ml64k.txt | a02d805f…3fe24 |
| b8-q1-hml.gguf | b8 | prism-ml/Bonsai-8B-gguf Q1_0,同一头部 | fc297cb3…0d42a |
| tb8-q20-hml.gguf | tb8 | prism-ml/Ternary-Bonsai-8B-gguf Q2_0_g64,同一头部 | 01b1e0ef…93e06 |
| mb-q2-h60.gguf | mb | tablinum/Moon-Beam-8B-A1B-GGUF-Preview Q8_0,在 Mac 上重新量化:专家部分 Q2_K,其余 Q4_K,自有 imatrix 来自 data/corpus,头部 data/head-ids-ling-60k.txt;在 repack 缓存(cache/mbq2.*)写入后,于 TV 上对专家字节做挖孔处理 | 2d78fcdd…1c34ee |
| lm2-q2-h60.gguf | lm | bartowski/inclusionAI_Ling-mini-2.0-GGUF Q4_K_M + 其 imatrix:专家部分在 Mac 上重新量化为 Q2_K,其余 Q4_K,头部 data/head-ids-ling-60k.txt;在 repack 缓存(cache/lmq2.*)写入后于 TV 上做挖孔处理 | c089aeb0…e8d7 |
| q9-iq2-h65-mtp.gguf | q9 | unsloth/Qwen3.5-9B-GGUF UD-IQ2_XXS,MTP 块来自 Qwen/Qwen3.5-9B(Q4_0),65K 头部 | 7766478e…8dcbc |
| cache/b8c.,cache/tb8. | 缓存 | 由首次带 GGML_REPACK_CACHE 的运行写入 |
data/ 中的 head-id 列表由 tools/vocab_select.py(Qwen3.5 分词器)和 tools/head_select_tok.py(任意分词器,经 llama-tokenize)生成,语料来自 tools/fetch_corpus.py 下载的内容(Wikipedia + Project Gutenberg)。
布局
| 路径 | 内容 |
|---|---|
| EXPERIMENT.md | 完整记录:§1-8 第一个微型模型,§9-10 llama.cpp 与首次 flash streaming,§11 2B 冲刺,§13 4B 冲刺,§14 9B 冲刺,§15 0.8B 速度冲刺,§16-17 2B 的第 3-4 轮,§18 4B 第 2 轮(arXiv / GitHub 扫描,2-bit ternary 布局),§19 8B 冲刺(headless 模式、LUT kernel、带 expert pager 的 Moon-Beam MoE、长回答为何循环),§20 大于 9B(Ling-mini-2.0 16B,以及 LFM2.5-24B 与 Qwen3.6-35B 为何放不下) |
| patches/llama.cpp-v0.8.0-tvbox.patch | 对 llama.cpp 的每一处改动:针对 Qwen3 / Qwen3.5 / Granite 4.0 H 的剪枝 head( output_ids ),上述各 kernel( ggml-cpu/repack-*-tv.inc ),融合 DeltaNet(按 id 读取 state、可选 fp16 state、单 token conv 步),1-bit LUT kernel,Q8_0 attention,带 routing bias 与 prefetch 的 MoE expert pager( ggml-cpu/moe-pager-tv.inc ),针对 bailingmoe3 的剪枝 head,从常驻 head 取 embedding 行,repack 缓存与 streaming 引擎( ggml-cpu/stream-tv.inc 、loader 钩子),小批量 padding,op profiler,能识别剪枝 head 的 perplexity,针对 Mali 的 Vulkan 调优 |
| tools/ | GGUF 重写器 / head 剪枝器 / 重新切分器,词表筛选,语料与 MTP 抓取器,模型重建脚本,TV 内存守护与 RAM 辅助工具,基准扫描,kernel 基准与正确性检查( kbench/ ),flash 基准 |
| tv-llm-ui/ | llama-server 的实时 token 流页面(置信度配色、前 3 候选、速度、box 统计),及其启动器和基准测试框架 |
| tiny-llm-web/ | 第一个实验中的 llama2.c 故事服务器 + TV 页面 |
| bench/ | results.jsonl (每次对话基准:prompts、输出、速度)和 raw/ (每次实验的日志) |
| data/ | head-id 列表和两个评测文本 |
| zigcc/ | 用于交叉编译静态 aarch64 二进制的 zig 封装 |
| ipad-air/ | 设备 #2:为 iOS 12 交叉编译,smolchat(终端聊天与 Safari 网页),部署与基准脚本( README ,§21) |
| screenshots/ | 在 TV 上运行的网页 |
经验教训
- 在这台机器上,4 bit 的 2B 胜过所有 1-2 bit 的更大模型。低于约 2.5 bit 的训练后量化会破坏正常模型(E22、E23、E35)。为 1-bit 或 ternary 训练的模型能撑住,但在 4-8 B 规模下,不靠 streaming 就放不下。
- 过了 RAM 这道坎,flash 带宽决定速度。kernel 的 LRU 对于大于缓存的循环扫描是最糟糕的策略:它每个 token 都要重读一切。保持一个固定的常驻集合,其余的自己流式读取。
- 当你受限于 flash 时,batching 是免费的。Prompts 和投机验证大约只花一次权重遍历的代价,无论其大小。
- 先测量,再验证。两个 streaming bug 给出了看似合理但错误的速度数字;只有把张量与文件对比、并用 Mac 参考做 perplexity 校验,才把它们抓出来。
- 小模型主要就是词表。Qwen3.5-0.8B 的 248K 行 head 占其权重的三分之一;把它剪到 65K 行并全部改用 Q4_K,速度从 7.5 提升到 12.1 tok/s(E55)。小模型在 4 bit 下损失也更大(perplexity +7 %,而 2B 为 +2.3 %)。
- 写 kernel 之前先模拟。激活稀疏性(E58)和低秩 head(E60)在纸面上都很有前景;在 Mac 上做一次质量模拟,不到一小时就各自被否决。
- 在顺序执行核心上,除了字节数还要数 NEON 操作。更紧的打包省下 20 % 的字节,却付出 2.5 倍的操作数;撤销它让 4B 在聊天中快了 33 %(E76、E77)。4-bit kernel 是相反的情况,受限于 RAM(E27),所以这个技巧对 2B 或 0.8B 没有帮助。
- 检查每个大矩阵是否都走到了快速 kernel。2B 的 head(65,668 行,约占每个 token 权重的 10 %)在通用 kernel 上悄悄跑了数周,因为 ggml 只对行数能被 8 整除的矩阵做 repack;多出四行就带来 +5.8 %(E71、E72)。加载日志里的 repack buffer 大小会暴露这一点。
我正让 LLM 在小型和老旧设备上运行
- 大部分 RAM 属于 Android,而不是模型。在此期间强制停止启动器和 Google 服务(不持久化任何内容),让 Bonsai-8B 完全驻留在 RAM 中:在没有任何内核工作之前,速度从 1.2 提升到 3.6 tok/s(E80)。一个动画屏保和一个每秒 fork 25 次的守护进程各自又带来 10-15% 的开销(E82、E88)。
- 对于混合专家模型,flash 是一次缓存未命中,而不是一次流式读取。每个专家约 6 ms 的未命中意味着池命中率决定速度;将路由导向已缓存的专家,并预取下一个路由器会选择的专家,把速度从 4.4 提升到 7.2 tok/s。
- 困惑度看不到循环。它在干净文本上一次只对一个 token 打分;那个 2-bit 模型得分不错,却仍在 13 个长回答中的 12 个里陷入循环。也要对长时间自由生成的文本打分(tools/loop_stats.py),并且用每字节比特数来比较使用不同分词器的模型,绝不要用困惑度。
- 超过 9B 之后,要数驻留字节,而不是参数。这里混合专家模型的上限,是必须留在 RAM 中的非专家权重加上一个有用的专家池。Ling-mini-2.0(16B)保持 279 MB 驻留,运行速度为 5 tok/s;Qwen3.6-35B-A3B 在缓存单个专家之前就需要约 870 MB(§20)。
- 保护设备。在两次早期内核崩溃之后,每次重负载运行都要经过一个包装器,在可用内存低于 180 MB 时将其终止。
许可证
MIT。llama.cpp 补丁是 llama.cpp(MIT)的衍生作品。模型权重保留各自的许可证,评估文本是 Wikipedia 摘录(CC BY-SA 4.0)。
致谢
构建于 llama.cpp 和 llama2.c 之上。模型来自 Qwen、unsloth(GGUF 和重要性矩阵)、PrismML(Bonsai)、Google(Gemma)和 stories260K。评估文本是 Wikipedia 的摘录(CC BY-SA 4.0)。🤏 标志是 Google Noto Emoji 中的捏手 emoji(Apache 2.0)。