FreeToken:让消费级电脑跑起 284B MoE,一次把整机当成推理集群的系统设计

FreeToken 桌面端控制台

图片来源:FreeToken 仓库

先说一个数字:在 32GB 显存的游戏台式机上,跑 284B 参数的 DeepSeek-V4-Flash;在单张 96GB 工作站 GPU 上,跑 753B 的 GLM-5.2。

不是量化到 2bit 的玩具版本,是能接 Codex、Claude Code 这类真实 agent 的推理服务。

这件事听起来像营销话术,但它背后有一篇正经的系统论文:FreeToken: Efficient Edge-Native MoE Serving with Bandwidth-Adaptive Execution(arXiv 2608.16157,2026 年 8 月),作者来自 Berkeley、MIT、UT Austin 等机构,包括 Ion Stoica、Matei Zaharia、Song Han、Kurt Keutzer。

开源仓库:FlashML-org/FreeToken,Apache-2.0。

这篇文章想讲清楚一件事:为什么“把模型卸载到 CPU”这种老思路不够用,以及 FreeToken 换了一套什么样的资源观。

一、本地推理的真正瓶颈,从来不只是显存

先把常见误解拆掉。

“跑不动大模型”通常被归因于显存不够。于是直觉方案是:权重放 CPU 内存,用时搬到 GPU。llama.cpp 的 --n-gpu-layers、KTransformers 的专家放置,本质都在这个框架里。

但这个框架有一个隐含假设:瓶颈是容量。

真实的瓶颈是带宽,而且不止一种带宽。

看一组数字就够了:

资源 典型带宽
RTX 4090 / 5090 显存 1–1.8 TB/s
双通道 DDR5 系统内存 80–90 GB/s
双通道 DDR4 系统内存 ~50 GB/s
PCIe 5.0 ×16 ~60 GB/s
PCIe 4.0 ×16 ~25 GB/s
笔记本常见的 ×8 链路 更低

显存和系统内存之间有 10–20 倍的带宽差。这意味着:一旦某个专家的权重不在显存里,它要么走 PCIe 搬过来(慢一个数量级),要么就地用 CPU 算(也慢一个数量级)。

所谓“卸载”,只是把慢的那一步藏起来或者摊薄,并没有消除它。

FreeToken 的出发点不是“怎么卸载得更好”,而是:在给定这台机器的实际带宽组合下,每一步应该把多少工作分给谁。

这是一个调度问题,不是一个容量问题。

二、MoE:机会与麻烦是同一件事

MoE 让前沿模型在消费级硬件上“有可能”运行,原因很简单:总参数量 ≠ 每个 token 的激活参数量。

论文里给了 DeepSeek-V4-Flash 的具体结构:

  • 43 层,每层 256 个 routed experts;
  • 每个 token 只激活其中 6 个;
  • 总参数 284B,单 token 实际参与计算约 13B。

13B 的激活权重,在部署精度下能塞进 RTX 5090 的 32GB 显存。

但紧接着就是那个转折:

稀疏性降低了每个 token 的计算量,却没有按比例降低存储完整专家池所需的内存。

也就是说,显存里放得下“这次要算的”,放不下“可能要用到的”。 剩下那 271B 的专家,必须待在主机内存里,按需进入执行路径。

于是问题的形状变得非常具体:

              Router(GPU 上)
                    │
          ┌─────────┴─────────┐
          ▼                   ▼
   命中显存的专家        未命中的专家
   (直接 GPU 算)            │
                    ┌────────┴────────┐
                    ▼                 ▼
              PCIe 搬到 GPU      留在 CPU 上算
              (占链路带宽)      (占内存带宽)

关键洞察在这里:这两条路不是互斥的,它们消耗的是同一种资源——主机侧内存带宽。

三、三个阶段,三种不同的失败模式

论文把边缘 MoE 服务拆成三个阶段,每个阶段的瓶颈完全不同。这个拆法本身就是全文最有价值的部分之一。

1. Prefill:稀疏性在这里失效

Decode 时每个 token 只碰 6 个专家,但 prefill 一次要处理几千个 token。

几千个 token 的路由并集,几乎覆盖每一层的全部专家。于是:

prefill 阶段实际会把整个专家池流过一遍 CPU–GPU 互联链路。

论文给了一个很具体的账:DeepSeek-V4-Flash 的 FP4 部署,prefill 需要搬运约 140GB 专家权重。

平台 链路带宽 搬运耗时
RTX 5090(PCIe 5.0 ×16) ~60 GB/s ~2 秒
RTX 4090 / 3090(PCIe 4.0 ×16) ~25 GB/s ~5 秒
笔记本常见 ×8 链路 更低 10 秒以上

对一个“边想边调工具”的 agent 来说,每一轮都多等 2–10 秒,体验直接崩塌。

更麻烦的是第二个成本:重算。

现代前沿模型大量使用混合注意力——DeepSeek-V4-Flash 交替使用全注意力和滑动窗口注意力,Qwen3.6-35B-A3B 用了 gated DeltaNet,Kimi-K3 用了 Kimi Delta Attention。

这类层把整段历史压缩成一个状态。好处是省内存,坏处是:这个状态没法部分复用。

而 agent 框架几乎每轮都在改上下文:

  • OpenClaw 会剥掉除最新一轮外的 thinking 块;
  • OpenCode 把窗口之外的工具输出替换成占位符;
  • SWE-agent 只保留最近 n 条观察。

改完之后,改动位置之后的所有 checkpoint 全部失效,引擎只能回退到最后一个有效点,把中间几千个 token 重新 prefill 一遍。

在消费级 GPU 上,这段重算藏不住——RTX 5090 的稠密 BF16 吞吐大约只有 H100 的五分之一、B200 的十分之一。

2. Decode:命中率与 CPU 上限

Decode 是相反的形态:稀疏,但每一步都可能 miss。

现有引擎的做法是静态放置:加载时决定哪些专家放 GPU,其余留 CPU。问题是路由随 token、随任务变化,预填充时定下的布局,很快就与实际流量脱节。

那“全部交给 CPU 算”行不行?也不行。

论文算了一笔很干脆的账:decode 小批量时专家计算是内存带宽受限的,每个 token 要把路由到的专家权重完整读一遍。消费级 CPU 挂的是双通道内存,DDR5 也就 80–90 GB/s,而 GPU 显存是 TB/s 级。

结论:CPU-only 路径的速度上限,与核心数无关,只由内存带宽决定。

3. 资源管理:边缘设备上没有“专用”

数据中心里 GPU 是独占的。个人电脑不是。

浏览器、桌面合成器、游戏,随时可能抢走几个 GB 显存。于是:

  • 可用显存每次启动都不一样;
  • 运行过程中还会动态变化;
  • KV Cache 随上下文增长,专家缓存的需求却基本恒定——启动时算好的划分,几十轮之后必然失衡。

再加上一个容易被忽视的成本:启动。140GB 的专家池,从 7GB/s 的 NVMe 读一遍就要 20 秒,还没算预热。

而边缘用户的行为模式是:用的时候打开,不用就关掉;换个模型也重启一次。

四、FreeToken 的核心结构:两级专家内存

面对这三个问题,FreeToken 的架构选择是:不做“部分卸载”,而是把 GPU 剩余显存做成一个全局共享的弹性专家缓存。

GPU
 ├── 非专家权重(常驻)
 ├── KV Cache(分页)
 └── 全局专家缓存(弹性,所有层共享)
       └── 每个 slot = 一个 (layer, expert)
            ▲
            │  PCIe
            ▼
CPU 内存
 └── 完整专家池(source of truth,常驻且 pin)

两个设计决定值得单独说:

第一,缓存按逻辑 (layer, expert) 寻址,而不是按张量分片。 一个 slot 装的是评估某个 (layer, expert) 所需的全部张量(gate、up、down 投影)。这样驻留、查找、执行都围绕同一个逻辑标识,而不是一堆碎片的拼装。这也直接决定了后面 FTW 格式的设计。

第二,CPU 池是唯一真相源(source of truth)。 这一条是整个弹性内存管理的前提:GPU 显存只影响性能,永远不影响正确性。 缓存可以随时被重建、缩小、清空,模型输出不变。

五、Prefill:把搬运藏在计算背后

既然 prefill 注定要搬完整层专家,那就别按需搬,按层搬。

FreeToken 采用 全层双缓冲(full-layer double buffering):

时间 ────────────────────────────────────────►

Buffer A:  [GPU 计算 layer l  ] [加载 layer l+2]
Buffer B:  [加载 layer l+1   ] [GPU 计算 layer l+1]
                  ▲
             两者交换角色

关键点在于它加载的是完整一层,而不是“这一层被路由到的专家”。

这个选择初看很浪费,但它是刻意的:

  • 加载可以在路由结果出来之前就开始,流水线不会断;
  • 传输流与计算流并行,互相不等待;
  • 缓冲区与 decode 的专家缓存共用同一个 slot 池,没有独立的 prefill 缓存,也没有阶段切换的握手成本。

论文特别提到一个细节:prefill 期间留在缓存里的条目,会直接成为 decode 阶段的热数据。这是一次免费的热启动。

当然有兜底:如果 slot 池腾不出两个整层,FreeToken 退回按需加载,而不是硬撑到显存溢出。

语义锚点:让 recurrent state 熬过上下文编辑

第二个问题——重算——解法更巧妙。

全注意力层的 KV 可以用 radix 前缀树复用,这是 SGLang 一类系统的标准做法。但 recurrent 层不行:它把整个前缀压成一个状态,没法部分复用,只能靠 checkpoint。

而 checkpoint 很贵:一个状态占的内存相当于几百个 token 的 KV,所以只能存很少几个。存在哪里,决定了它们值不值钱。

FreeToken 的答案是:存在语义边界上。

具体说,就是那些特殊 token 标记出来的位置——thinking 段的起止、工具调用、工具输出、对话轮次。理由很直接:agent 框架的编辑,本质上就是整块地删除或替换这些边界之间的内容。

  • OpenClaw 剥 thinking 块;
  • OpenCode 换工具输出;
  • SWE-agent 删旧观察。

它们删的都是“特殊 token 划定的整块”,而编辑点之前的前缀被原样保留。

于是,锚定在语义边界上的 checkpoint,存活概率远高于随机位置。当一次编辑发生后,引擎从“最深的、位置仍然有效的锚点”恢复:

原始上下文:  [轮1][thinking][工具调用][工具输出][轮2]...
编辑后:      [轮1][thinking][工具调用][✂ 截断][新增内容]
                                    ▲
                            从这里的锚点恢复
                            只重算新增的后缀

全注意力层从前缀树复用 KV,recurrent 层从锚点恢复状态,只有真正新增的后缀需要 prefill。

这是全文我认为最“懂 agent”的一个设计:它不是从模型结构出发的优化,而是从agent 框架实际怎么改上下文反推出来的。

六、Decode:q* 策略,一个公式解决所有硬件

现在到了最核心的部分。

假设某一层有 $m$ 个专家未命中。这些专家可以:

  • 走 PCIe 搬进显存,在 GPU 上算,并留在缓存里供以后复用;
  • 留在 CPU 上就地算,不改变缓存状态。

问题是:$m$ 个里面,几个该搬,几个该就地算?

FreeToken 的推导很干净。令 $S$ 为单个专家的字节数,$B_P$ 为实测的 PCIe 专家传输带宽,$B_H$ 为实测的 CPU 侧专家处理带宽。

关键前提:两者读的是同一套主机内存。 所以当 PCIe 跑满时,留给 CPU 的残余带宽是:

\[B_R = \max(B_H - B_P,\ 0)\]

于是两条并行分支的耗时分别是:

\[T_{\text{fill}}(q) \approx \frac{qS}{B_P}, \qquad T_{\text{cpu}}(m-q) \approx \frac{(m-q)S}{B_H - B_P}\]

令两者相等,就得到最优切分:

\[q^{\star} \approx m \cdot \frac{B_P}{B_H}\]

这就是 q* 策略。

它有几个很漂亮的性质:

第一,它自动覆盖所有硬件形态。 当 $B_H$ 接近 $B_P$(比如链路极快、内存极慢),$q^\star \to m$,系统退化为“全部搬进缓存”,不需要单独的执行分支。反过来,当 PCIe 很慢而内存带宽充裕,$q^\star$ 变小,更多专家留给 CPU 就地算。

第二,它把“这台机器上该怎么分工”变成一个可测量的量。 $B_P$ 和 $B_H$ 都是部署时在目标硬件上实测的,用的是实际的张量形状,不是规格书上的理论值。论文特别强调这一点:最优配比读不出来,只能量出来。

第三,两条分支的结果可以精确合并。 CPU 和 GPU 各自算部分和,然后相加。没有近似,没有精度损失。 这一点很重要——很多“异构推理”方案在这里就开始丢数值一致性了。

代价是复杂度:调度逻辑、miss 检测、受害者选择全部要放进 CUDA Graph 里,否则每个 MoE 层都要一次主机同步,性能直接归零。

论文的解法是把所有路由相关的控制流变成 GPU 上的数据:一个 kernel 完成去重、驻留判定、算 $q$、选受害者、把逻辑 ID 重写成物理 slot ID 或“交给 CPU”标记。受害者选择用单趟扫描一次找出 $K$ 个 LRU 候选,避免“每淘汰一个扫一遍全缓存”的经典陷阱。

CPU 分支也被捕获进同一张图:固定的 pinned I/O 缓冲、持久化的任务描述符、host function 提交节点,重放时整条异构流水线一次跑完,不需要每个 token 过一次 Python。

七、弹性内存与 FTW:让“启动”和“调整”不再昂贵

三个机制,对应三个现实约束。

1. 运行时重建缓存。 在调度器的安全点上,FreeToken 可以按新的显存预算重新划分并重建 GPU 专家缓存,不需要重启引擎,也不需要重新加载 CPU 侧的专家池。

因为 CPU 池是真相源,这一步天然安全:缓存没了,性能降一点;缓存回来,性能恢复。正确性始终不受影响。

2. 快速启动。 传统做法是:先 pin 一块空内存,再从磁盘读数据填进去。问题是 pin 之后写入会触发缺页和清零,几个 GB 白白浪费。

FreeToken 反过来:先从磁盘直接读进最终的主机布局,读完再 pin。 再加上“冷缓存直接服务第一个请求”(第一次 miss 走正常 decode 路径,缓存自然热起来),预热阶段被整个删掉。

3. FTW 权重格式。 FTW(FreeToken Weight)不是量化算法,而是一种预排版格式。

模型原始 checkpoint 的专家权重是散落在各处的张量。FreeToken 在转换时就把它们按运行时 bank 布局合并好,用 layer × E + expert 作为首维。加载时不需要再做张量发现和重排,直接按对齐块并行直读进预先算好大小的 bank。

前面说的“逻辑 ID 统一”在这里兑现:GPU kernel 和 CPU 执行器共享同一套专家身份,所以一份索引列表可以同时驱动所有 bank 的传输,一次固定形状的 launch 搞定。

八、实测:数字比架构更能说明问题

论文在 6 台机器上做了评测,从 8GB 的 RTX 4060 笔记本到单张 RTX PRO 6000(96GB),跑了 4 个真实 agent 工作负载,对比 llama.cpp、Ollama、KTransformers 和 MoE-Infinity。

场景 结果
RTX 5090 跑 Qwen3.6-35B-A3B 77–83 tok/s
RTX 5090 跑 DeepSeek-V4-Flash(284B) 22–25 tok/s
相对 SOTA 边缘引擎 1.5–2.3× decode 吞吐
8GB RTX 4060 笔记本跑 35B 39.3 tok/s(超过 Codex 生产流量的中位数 33 tok/s)
32GB 游戏台式机 可交互地服务 284B 模型
单张 RTX PRO 6000 服务 753B GLM-5.2,约为 llama.cpp 的 2 倍
五台消费级机器的整体提升 1.3–2.1×

还有两个数字很能说明工程完成度:

稳定性。 从单轮对话切到多轮 agent,FreeToken 的 decode 速率保持在单轮设置的 12% 以内,而对比系统明显退化。原因不难理解:agent 负载的核心特征是“上下文频繁被编辑”,而这正是语义锚点要解决的问题。

尾延迟。 FreeToken 在所有负载下的最差 TTFT 都低于 44 秒;而每个基线都至少有一个设置超过 150 秒——足够让真实的 agent 客户端触发超时。

这两个数字比平均吞吐更有意义。边缘推理的体验由最差的那一次决定,不是由平均值决定。

九、怎么上手

两条路径,覆盖两种使用习惯。

桌面端。 从 flashml.ai 下载 Windows 或 Linux 版本,安装器会把引擎和依赖配置好,界面里可以直接跑模型、对话、调推理参数。对不想碰命令行的用户,这是主要入口。

命令行。 用 uv 安装(推荐):

uv pip install "freetoken[accel]"

或者从源码构建:

git clone https://github.com/FlashML-org/FreeToken.git
cd FreeToken
uv venv && source .venv/bin/activate
uv pip install -e ".[accel]"

装完之后最关键的一点:FreeToken 对外提供 Anthropic / OpenAI 兼容的 API。 这意味着 Codex、Claude Code、OpenCode、OpenClaw 这类现成的 agent 客户端,改个 base URL 就能接过来用。

模型清单、CLI 参数和故障排查都在仓库 docs/ 下,其中「加载失败」有一份专门的诊断脚本,对第一次跑大模型的人来说挺实用。

十、和现有方案到底差在哪

维度 llama.cpp / Ollama KTransformers FreeToken
专家放置 加载时确定 预热专家 pin 在 GPU 运行时 LRU 跟随路由
Prefill 按需取专家 部分优化 全层双缓冲流水
miss 处理 单一路径 CPU 为主 q* 按带宽切分
显存调整 需重启 需重启 安全点热重建
上下文编辑 回退重算 回退重算 语义锚点 + 前缀树
权重格式 GGUF 自有 FTW(预排版 bank)

公平地说,llama.cpp 的 GGUF 生态和跨平台能力仍然是 FreeToken 短期比不了的;KTransformers 在特定 CPU 平台上做了很深的内核优化。FreeToken 的差异化在于它把“这台机器有多少带宽”当成一等公民,而不是把所有机器都当成“小一号的数据中心 GPU”。

论文自己也承认借鉴了很多:架构上跟随 SGLang/vLLM 的 GPU-centric serving,复用了 FlashInfer、flash-linear-attention 的 kernel,也参考了 mini-sglang 的设计。

十一、边界:这套东西不适合什么

写完优点,得写清楚它不解决什么。

1. “能跑”不等于“跑得快”。 5090 上 22–25 tok/s 的 284B,是“可用”级别,不是“流畅”级别。对比一下:跑 7B 模型能到几百 tok/s。

2. 它对硬件有明确偏好。 RTX 30/40/50 系列,CUDA 环境,足够的 PCIe 带宽。PCIe ×8 的笔记本链路会让 prefill 传输成本翻几倍——q* 能优化 decode,但优化不了物理带宽。

3. 内存容量仍然是硬门槛。 284B 的 FP4 权重约 140GB,需要相应的主机内存。显存小可以靠缓存策略缓解,内存小是没救的。

4. 平台兜底路径会明显变慢。 当系统无法 pin 内存或注册 DMA 时(某些操作系统和驱动配置),FreeToken 退回纯 CPU MoE 后端:专家全在可分页内存里,只有激活值过 PCIe。能用,但放弃了快路径的全部收益。

5. 单机优化,不是分布式。 它的目标是“把这台机器榨干”,不是“把几台机器连起来”。多机场景它没有优势。

6. 前沿模型的适配需要持续投入。 20+ 个 MoE 模型的适配列表听起来不少,但每出一个新架构(新的注意力变体、新的专家路由方式),适配层就要跟进。

十二、总结:边缘推理的重心正在从“容量”转向“带宽”

FreeToken 最值得学的地方,不是某个具体技巧,而是它重新定义了问题。

传统思路问的是:怎么把放不下的权重塞进去? FreeToken 问的是:在给定的带宽组合下,每一步的工作该分给谁?

这个转变带来了一整套连锁设计:

  • prefill 因为“注定要搬完整层”,所以干脆按层流水,把搬运藏进计算;
  • decode 因为“两条路抢同一份内存带宽”,所以用一个 $q^\star = m B_P / B_H$ 的公式精确切分;
  • 上下文因为“agent 是按语义块编辑的”,所以把稀缺的 checkpoint 预算花在语义边界上;
  • 显存因为“随时会被别的应用抢走”,所以把它做成可热重建的弹性资源。

四条设计,对应四个从真实使用场景里长出来的观察。这不是“把数据中心方案缩小”,而是为边缘设备重新推导了一遍服务栈。

如果说前几年本地推理的竞争是“谁的量化更狠”,那么接下来的竞争可能是:谁更懂这台机器。

项目与参考资料

项目采用 Apache License 2.0。本文数据来自论文摘要与正文、仓库 README,性能数字对应论文的测试环境,实际表现取决于 CPU、内存容量与带宽、PCIe 链路、量化格式和上下文长度。




Enjoy Reading This Article?

Here are some more articles you might like to read next: