Newshot
Account

我正让 LLM 能在小型老旧设备上运行

Translation(model translation, not the source's own text)

🤏 smolmaxxing

🤏 smolmaxxing:在你拥有的最小硬件上运行你能跑的最大模型
🤏 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)。

电视盒子上 llama-server 提供的实时 token 流页面
电视盒子上 llama-server 提供的实时 token 流页面

设备

设备状态
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 设备都可以使用流式加载引擎。

两台 1 GB 设备上各尺寸级别的最佳生成速度:2B 级别电视 7.7(Qwen3.5-2B)对 iPad 23.7(Bonsai-1.7B,1-bit);4B 4.3 对 9.4;8B 稠密 5.2 对 0.08;8B MoE 7.2 对 3.5;16B MoE 5.1 对 4.5,且缓存偏置相同
两台 1 GB 设备上各尺寸级别的最佳生成速度:2B 级别电视 7.7(Qwen3.5-2B)对 iPad 23.7(Bonsai-1.7B,1-bit);4B 4.3 对 9.4;8B 稠密 5.2 对 0.08;8B MoE 7.2 对 3.5;16B MoE 5.1 对 4.5,且缓存偏置相同

记分板

各尺寸级别的最佳模型,从电视盒子上的首次运行到现在:0.8B 7.5 到 16.8 tok/s,2B 0.1 到 7.7,4B 2.2 到 4.3,8B 稠密 1.21 到 5.2,8B MoE 1.87 到 7.2,9B 0.16 到 0.27,16B MoE 4.1 到 5.1;以及一个 Moon-Beam token 的 144 ms 花在了哪里
各尺寸级别的最佳模型,从电视盒子上的首次运行到现在:0.8B 7.5 到 16.8 tok/s,2B 0.1 到 7.7,4B 2.2 到 4.3,8B 稠密 1.21 到 5.2,8B MoE 1.87 到 7.2,9B 0.16 到 0.27,16B MoE 4.1 到 5.1;以及一个 Moon-Beam token 的 144 ms 花在了哪里
Class最佳模型首次运行 → 现在(tok/s)提升Bits/byte靠什么做到的下载
0.8BQwen3.5-0.8B7.5 → 16.8 bench×2.2≈1.00全 Q4_K,补齐到 65K 的 head,fp16 DeltaNet 状态HF
2BQwen3.5-2B0.1 → 7.7 chat×770.909全 Q4_K,补齐到 65K 的 head,融合 DeltaNetHF
4BTernary-Bonsai-4B2.2 → 4.3 chat×2.0≈0.965三值权重放在 2-bit 字段里,合并 scalesHF
8B denseBonsai-8B (1-bit)1.21 → 5.2 chat×4.30.9741-bit LUT kernel,headless 模式下完全驻留内存HF
8B MoEMoon-Beam-8B-A1B1.87 → 7.2 chat×3.90.996Q2_K experts 从 flash 分页,缓存感知路由,预取HF
9BQwen3.5-9B0.16 → 0.27 chat×1.70.885权重流式加载,加上它自己的 MTP headrebuild ( q9 )
16B MoELing-mini-2.04.1 → 5.1 chat×1.21.054Moon-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_M253 MB不适用22 tok/s73 tok/s
Qwen3.5-0.8B Q4_K_M(原版)497 MB不适用7.5 bench28 tok/s
Qwen3.5-0.8B,全 Q4_K + 65K head442 MiB(566 MB RSS)≈1.00*16.8 bench33 tok/s目前同质量下速度最快(§15-17)
Qwen3.5-2B,全 Q4_K + 65K head0.99 GB0.9097.7 chat17.8默认选择:综合最佳(§11、§16、§17)
Ternary-Bonsai-4B,合并 scales + 2-bit 字段1.03 GB≈0.9654.3 chat(4.8 bench)9.6最佳 4B:同一构建上此前为 3.2 chat / 7.0 prompt(§18)
Moon-Beam-8B-A1B(MoE),Q2_K experts + 60K head0.48 GB GGUF + 2.6 GB repack 缓存在 flash 上,~1.1 GB RSS0.9967.2 chat不适用最快 8B:128 个 experts,用 6 个;expert 分页器、缓存感知路由、预门控预取、针对长回答循环的重复惩罚(§19,无头模式)
Ling-mini-2.0(MoE,16.3 B),Q2_K experts + 60K head0.47 GB GGUF + 5.3 GB repack 缓存在 flash 上,~1.13 GB RSS1.0545.1 chat不适用可用速度下最大的模型:256 个 1 MB 的 experts,用 8 个,每层池 43 个,bias 0.3(§20)
Bonsai-8B,1-bit,LUT kernel0.96 GB,全部常驻0.991(head 上为 0.974)5.2 chat(5.1-5.4 bench),~900 tokens 上下文时 4.05.8-6.1最佳 dense 8B:无头模式,Q8_0 attention(§19)
Bonsai-8B,1-bit + 紧凑 scales0.84 GB + 0.12 GB 流式读取0.9911.21 chat(2.2 bench)4.9§14,无头模式之前
Ternary-Bonsai-8B0.89 GB + 0.80 GB 流式读取0.9190.31 bench3.45§14
Qwen3.5-9B UD-IQ2_XXS + MTP0.63 GB + 1.6 GB 流式读取0.8850.16 chat,用 MTP 时 0.270.8质量最佳;受限于 flash(§14)
  • 在剪枝后 head 的位置上测得(1.3 % 的 tokens 落在其外),因此不完全可比;原版全词表 Q4_0 文件得分为 1.024。

其他小模型,原版文件(筛选,§15 E54):

模型生成(bench)提示词处理比特/字节
LFM2.5-350M (QAD Q4_0)23.7 tok/s104 tok/s1.805
LFM2-700M (Q4_0)11.5 tok/s52.8 tok/s1.169
Qwen3-0.6B (Q8_0)9.35 tok/s50.6 tok/s1.059
LFM2.5-1.2B (QAD Q4_0)8.3 tok/s31.6 tok/s1.133
Llama 3.2 1B (Q4_0)7.0 tok/s29.5 tok/s0.998
Ternary-Bonsai-2-2B (Q2_0)5.9 tok/s15.8 tok/s1.124
Gemma 3 1B (Q4_K_M)5.7 tok/s11.1 tok/s1.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。

ModelFileGeneration (tok/s)Peak RSSFixed three-question test
Bonsai-1.7B Q1_0 (1-bit)237 MiB23.7-25.8,长对话中 18-22605 MB三题全对,还能写出正确代码:就用这个
Bonsai-4B Q1_0 (1-bit),-c 512 -b 64546 MiB9.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 642.6 GiB(映射 474 MB)较长回答 3.5-3.6;加 --experts 4 为 4.9-5.1(困惑度 +27 %)~600-625 MB三题全对;适合会话的模型,对常开服务器来说太大
SmolLM2-360M-Instruct Q4_K_M258 MiB10.9-12.5269-316 MB三题全对;最轻的好选择
Gemma 3 270M-it Q4_K_M241 MiB15.4-19.0248-294 MB潮汐题答错,召回题空回复
SmolLM2-135M-Instruct Q4_K_M101 MB18.6-29.3141 MB法国和召回题答对,潮汐题较弱
Qwen3.5-0.8B Q4_K_M508 MiB约 0.2506 MB反复颠簸 page cache,手动停止

只有 Bonsai 和 Moon-Beam 两行是在最新构建上测得的。其余各行早于下文的 attention 和 sampler 工作。

iPad Air 上各尺寸级别的最佳模型,从首次运行到现在:1.7B 2.75 到 23.7-25.8 tok/s;4B 9.4-11.4;8B dense 0.08;8B MoE 用原版 llama.cpp 无法加载,之后 1.6 到 3.5(4 个专家时 5.0);16B MoE 无法加载,现在 2.6(bias 0.3 时 4.5);以及一个 Moon-Beam token 的约 0.3 s 花在哪里
iPad Air 上各尺寸级别的最佳模型,从首次运行到现在:1.7B 2.75 到 23.7-25.8 tok/s;4B 9.4-11.4;8B dense 0.08;8B MoE 用原版 llama.cpp 无法加载,之后 1.6 到 3.5(4 个专家时 5.0);16B MoE 无法加载,现在 2.6(bias 0.3 时 4.5);以及一个 Moon-Beam token 的约 0.3 s 花在哪里
ClassBest modelFirst run → now (tok/s)GainWhat got it there
1.7BBonsai-1.7B (1-bit)2.75 → 23.7-25.8×8.6无乘法、查表式且不带 dotprod 的 1-bit kernel,LM head 走快速 kernel,带剪枝的行序 attention,单遍 sampler
4BBonsai-4B (1-bit)9.4-11.41.7B 的 kernel,每个 K head 4 个 q head 的 attention,-c 512 -b 64 以保持在约 700 MB 上限之下
8B denseBonsai-8B (1-bit)0.08放不进内存;每个 token 都要从 flash 重读文件的大部分
8B MoEMoon-Beam-8B-A1B无法加载 → 1.6 → 3.5(4 个专家时 5.0,困惑度 +27 %)×2.2expert pager,Q2_K 查表 kernel,预取,cache bias 0.03,基于频率的淘汰
16B MoELing-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::filesystem8B 和 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-22151,669 行不是 32 的倍数,因此绑定头从未进入该内核:每 token 72 ms 中的 45 ms
带剪枝的行序注意力,以及不对词表排序的采样器23.7-25.8KV 缓存将一行中的所有头并排存储,因此一次处理一个头会不断错过缓存

在长聊天中(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 权重/sE38
三值 Q2_0 → 在 RAM 中无损压缩到 1.875 比特/权重(三进制打包,保留每 64 个权重的 scale)Ternary-Bonsai-4B 从“抖动”变为 3.2 tok/sE39、E42
Q1_0 位平面:vtst 直接把一个位平面变成 vdot 的掩码,无需查表;8 位逐行仿射 scale(−5.6 %,0.07 % 误差)Bonsai-4B 3.02 → 4.22 tok/s;Bonsai-8B 可装入 0.96 GBE44、E52
融合的 Gated-DeltaNet 单步,直接写入状态(Qwen3.5)2B 6.0 → 6.3-6.7 tok/sE28
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.6E76、E77
1 位查表内核(Four-Russians / T-MAC 风格):每组 32 行,每 4 个激活值一张 16 项的表,直接由 float 激活值构建,每 128 次乘加约 8 个 NEON 操作 + 2 次加载,对比 16 + 4Bonsai-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/shyperionaidev/Qwen3.5-0.8B-tvbox-GGUFq08-q4k-h65p8.gguf
Qwen3.5-2B, 7.7 tok/shyperionaidev/Qwen3.5-2B-tvbox-GGUFw-q4k-h65p8.gguf
Ternary-Bonsai-4B, 4.3 tok/shyperionaidev/Ternary-Bonsai-4B-tvbox-GGUFtb4-q20-hml.gguf
Moon-Beam-8B-A1B (MoE), 7.2 tok/shyperionaidev/Moon-Beam-8B-A1B-tvbox-GGUFmb-q2-h60.gguf + moe-warm-moonbeam.txt
Bonsai-8B (1-bit), 5.2 tok/shyperionaidev/Bonsai-8B-tvbox-GGUFb8-q1-hml.gguf
Ling-mini-2.0 (16B MoE), 5.1 tok/shyperionaidev/Ling-mini-2.0-tvbox-GGUFlm2-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.2556-576 MB4 (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 MB19 (E138-E156)
Bonsai-8B (1-bit, dense)prism-ml/Bonsai-8B-gguf0.08:放不进 RAM,从闪存读取629 MB4 (E134-E137,与最初的 Moon-Beam 尝试一起)
Bonsai-4B (1-bit), -c 512 -b 64prism-ml/Bonsai-4B-gguf9.4-11.4 ;长对话中 9.2-10.3615-682 MB5 (E129-E133)
Bonsai-1.7B (1-bit), 默认prism-ml/Bonsai-1.7B-gguf23.7-25.8 ;长对话中 18-22605 MB11 (E118-E128)
SmolLM2-360M-Instruct Q4_K_Mbartowski/SmolLM2-360M-Instruct-GGUF10.9-12.5269-316 MB10 (E108-E117:最初的运行、小模型和网页聊天)
Gemma 3 270M-it Q4_K_Munsloth/gemma-3-270m-it-GGUF15.4-19.0248-294 MB
SmolLM2-135M-Instruct Q4_K_M原版 Q4_K_M18.6-29.3141 MB
Qwen3.5-0.8B Q4_K_Munsloth/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)2bunsloth/Qwen3.5-2B-GGUF BF16 + imatrix_unsloth,MTP 块来自 Qwen/Qwen3.5-2B,在 TV 上量化,头部文件为 data/head-ids-ml60k.txt16057be3…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)08unsloth/Qwen3.5-0.8B-GGUF BF16 + 其 imatrix_unsloth,纯 Q4_K,头部 data/head-ids-ml60k.txt,在 Mac 上量化901f3838…8523a6b
q08-q4k-h65p8.gguf08上一个文件在 Mac 上用 data/head-ids-ml60k-p8.txt 重新切分(§17)7dd2aac5…52fc232
tb4-q20-hml.gguf4bprism-ml/Ternary-Bonsai-4B-gguf Q2_0_g64,头部 data/head-ids-qwen3-ml64k.txta02d805f…3fe24
b8-q1-hml.ggufb8prism-ml/Bonsai-8B-gguf Q1_0,同一头部fc297cb3…0d42a
tb8-q20-hml.gguftb8prism-ml/Ternary-Bonsai-8B-gguf Q2_0_g64,同一头部01b1e0ef…93e06
mb-q2-h60.ggufmbtablinum/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.gguflmbartowski/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.ggufq9unsloth/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)。

The translation is machine-generated and individual terms may be off. Nothing is rewritten or cut; the original is authoritative.

Published by Hacker News
This site only summarises and aggregates; copyright stays with the original author. Headlines and summaries follow the source — where they differ, the original wins.
Read the original ↗