跳至正文
Eric®
作品与手记EN

← 文章  /  AI 工程

模型装得下,为什么还是跑不快?

缓存省了 26%,完整生成步骤却只快了一小截。GAAC 把本地推理里最容易被混淆的三件事摆在了一起:容量、延迟和并发。

本地部署大模型时,显存容量往往是第一道门槛。BF16 放不下,就换成 8-bit,再不行就用 4-bit。等权重终于加载成功,我们很容易以为最困难的部分已经过去了。

可一旦接入长文档、代码仓库,或者让几个任务同时运行,生成速度又可能慢下来。模型没有溢出内存,GPU 的理论算力也不低,时间究竟花在哪里?

Notre Dame 的 Guo、George 和 Joshi 在 2026 年 10 月 28 日发布的预印本 GAAC提供了一组值得拆开读的结果:驻留 KV 内存减少约 26%,DGX Spark 上 64K 上下文的注意力内核加速 1.19–1.27 倍,完整 W4A16 decode step 加速 1.04–1.15 倍,固定 KV 预算下的聚合吞吐提高 13%–29%。

把模型塞进内存,只解决了第一道门槛。接下来限制速度的,往往是数据每一步怎么被搬进计算单元。GAAC 最有意思的地方,就是同一份容量节省,到了内核、模型和服务三个层面,收益一路变样。

权重放下了,历史 token 还在占内存

权重只是推理时驻留数据的一部分。Transformer 还要保存历史 token 的 Key 和 Value,避免生成新 token 时重新计算历史 K、V。这份 KV Cache 随上下文增长,也会随并发序列数增加。

对常见的全注意力结构,单条序列的理论存储量可以近似写成:

KV bytes = 2 × layers × KV heads × head dim
           × context tokens × bytes per element

最前面的 2 代表 K 和 V。用一个假设的全注意力架构算一遍:32 层、8 个 KV head、head dimension 为 128,使用 BF16,每个 token 的 KV 就是 128 KiB。上下文达到 65,536 token,一条序列占 8 GiB;四条相同长度、没有共享前缀的序列,单是 KV 就要 32 GiB。分配器和临时工作区还得另算。

权重量化不会自动缩小这份缓存。一个 8B 模型的纯权重,BF16 粗算约 16 GB,4-bit 约 4 GB,但 KV 可能仍用 BF16。原来被权重掩盖的成本,就会显得越来越突出。GQA、MQA、滑动窗口和混合注意力能改变增长方式,因此参数量相近的模型,也不一定需要相同的 KV 容量。

缓存还藏着另一笔账。标准全注意力每生成一个 token,当前 Query 都要访问历史 Key,再结合 Value 得到输出。此前算好的东西保住了,读取这些东西的时间却要在每一步重新支付。

这时,TFLOPS 再高也可能使不上劲。Prefill 能把许多输入 token 组织成较大的矩阵运算;单请求、小 batch 的 decode 则要反复读取权重和历史 KV。计算单元等着吃数据,内存带宽已经忙满了。

假设一段路径每步都要从设备内存读取 8 GiB,可用带宽是十进制的 1 TB/s,理想传输下限就有约 8.6 ms。缓存命中、数据重用和实际带宽利用率会改变这笔账,但读取本身始终有成本。长上下文生成,要一遍遍付这笔钱。

压缩以后,在哪里解开?

FlashAttention已经展示过重新组织计算与存储访问的价值。GAAC 把关注点进一步放到 KV 的表示上:既然历史缓存要被反复读取,能不能少搬一些字节,又不改变保存的数值?

BF16 有 1 位符号、8 位指数和 7 位尾数。GAAC 保留符号和尾数,只编码指数。模型产生的指数并非均匀分布,很多 head-channel 的常见指数集中在短区间内。校准时,为每层 K、V 的各 head-channel 选出长度为 8 和 16 的高覆盖区间;实际编码保存相对区间基值的偏移。

数据按 8 个元素组成一组,共享一个 2-bit 模式标签:全部落在较窄窗口,用 3-bit 偏移;否则尝试 4-bit;仍有越界值,整组就保存原始 8-bit 指数。异常值走回退路径,校准失配的代价由压缩率承担,原始数值照样留下。

在常见的 3-bit 模式里,每个元素摊到 8 + 3 + 2/8 = 11.25 bit。五个测试模型中,约 88%–90% 的组采用这种模式,完整编码体积的压缩比为 1.405–1.407 倍。以 1.406 计算,编码体积节省 1 - 1/1.406 ≈ 28.9%;固定槽位、块表和回退又吃掉一部分,最后落到约 26% 的驻留 KV 节省。权重和其他运行时内存仍各算各的。

不过,只把 KV 压小,还不足以让推理更快。最直接的实现是读取压缩数据,完整解压到设备内存,再交给普通注意力内核。这样多了一次完整缓存的写出,后续又要把它读回来,压缩省下的流量可能被重新花掉。

GAAC 将重建融合进注意力内核。内核读取压缩 tile 和元数据,恢复当前要用的 BF16 数值,随即计算注意力分数和 Value 加权和,不在外部内存中物化一整份解压缓存。新 token 则先写入未满的 BF16 尾块,块满后只压缩一次;后续生成不反复重打包历史块。压缩块超出固定槽容量时,直接保留原始 BF16。

这套调度决定了压缩能否进入实际推理。KV 在整个请求里不断追加、读取、释放,历史块还会被后续 token 反复消费。GAAC 保存原始 KV 位模式,把轻量重建放到消费现场;集成时,继续对照基线检查浮点内核的数值行为和任务质量。这比单独追一个更漂亮的压缩率扎实得多。

多做一点计算,反而少花了时间

论文 Table 5把 64K 上下文的 decode-attention 内核计时拆成了几项,单位为微秒:

耗时项目 BF16 / FA2 先解压再计算 GAAC 融合路径
KV 读取 1117.8 1117.8 823.5
独立指数解码 — 930.8 —
计算(GAAC 含融合重建) 241.5 241.5 290.7
总耗时 1359.3 2290.1 1114.2

GAAC 少花了 294.3 μs 读取 KV,多花了 49.2 μs 计算,净省 245.1 μs。内核加速约 1.22 倍,对应耗时下降约 18%。它主动多做了一点计算,把更大的一笔访存开销换掉了。

先解压再计算的路径反而达到 2290.1 μs,比未压缩的基线更慢。相同的压缩数据,因为还原位置和后续访问方式不同,最后得到相反的性能结果。

Table 4 的独立 codec 消融更能说明取舍。加入 2-bit 模式,压缩比从 1.406 提高到 1.439 倍,约改善 2.3%;解码耗时却从 187.1 μs 涨到 526.7 μs,约为原来的 2.8 倍。省下那一点空间,换来了大得多的读取负担。

在带宽受限的路径上,让 GPU 多干一点活,可能正是提速的方法。关键在于这些操作换掉了多少数据搬运,以及重建位置、访问粒度和布局能否配合。把计算量和访存量拆开优化,很容易省错地方。

内核的 1.22 倍,传不到整个请求上

注意力之外,decode step 还包含线性层、MLP、归一化、权重访问以及运行时开销。假设注意力原本占总时间的 40%,把它加速到 1.22 倍,其余部分不变:

overall speedup = 1 / (0.6 + 0.4 / 1.22)
                ≈ 1.078

整体速率只提高约 7.8%。Amdahl 定律把局部优化的上限写得很清楚:收益由这部分原本占了多少时间决定。先画出完整步骤的耗时分布,再谈内核提速,才知道工程资源该往哪里投。

GAAC 的同 batch 实验把这件事测了出来。DGX Spark 上,batch 1、64K 上下文的注意力内核加速 1.19–1.27 倍;完整 W4A16 decode step 在所测模型和上下文配置中加速 1.04–1.15 倍。两个混合/MoE 模型只有约四分之一的层使用全注意力,完整步骤收益约 1.04–1.06 倍。其余层和权重访问仍占着大头。

对长文档尤其如此。Prefill 要先处理输入,GAAC 的融合路径主要加速 decode,prefill 仍走常规注意力,必要时还需临时展开缓存。如果输入 64K token 后只生成几十个 token,大量等待可能已经发生在第一个 token 之前;若在这份上下文上持续输出几千个 token,逐步节省才更容易累计。

内核倍率本身也有起伏。Qwen3-8B 在 32K、64K、128K 上下文的注意力加速分别为 1.31、1.27、1.25 倍;完整步骤则会随 KV 读取占比变化,呈现另一条收益曲线。选择一个最大的倍率做标题很容易,找出自己的工作负载落在哪段曲线上才有用。

多接一条请求,算不算更快?

另一组实验固定 KV 内存预算,再分别把并发数加到各自能容纳的上限。Table 2中,Qwen3-8B 在 64K 上下文、约 20 GB KV 预算下,BF16 能放 2 条序列,聚合吞吐 19.0 tok/s;GAAC 能放 3 条,达到 22.8 tok/s,增加 20%。

把总量粗略摊给活跃序列,前者每条约 9.5 tok/s,后者约 7.6 tok/s。比较条件已经从两条并发换成三条:设备每秒吐出的 token 更多,摊到每条上的速率却更低。看聚合吞吐时,我会同时要逐请求的 ITL 和完成时间,弄清楚新增容量究竟换来了什么。

对一个人的聊天助手,多放一条序列价值有限。换成三个各自维护长上下文的任务,第三个原本要排队,如今可以一起驻留,容量就可能改变整组任务的完成时间。拿这样的任务轨迹做对照,才看得出调度和依赖关系怎样影响用户体验。

论文中有个很有说服力的反差:混合和 MoE 模型在相同 batch 下加速较小,固定 KV 预算后的吞吐收益却达到 27% 和 29%。更多收益来自容纳额外并发,而不是单个计算步骤突然快了同样的幅度。

在线服务还得算排队和尾延迟。更大的 batch 可以让设备产出更多 token,也可以把部分用户晾得更久。我会把 goodput 直接绑到产品目标上:例如只统计同时满足首 token 和持续输出延迟要求的请求。聊天助手要及时回应,离线文档处理更看重批量完成;各自该优化什么,产品已经给出了答案。

已经用了 FP8,还能照搬这些收益吗?

PagedAttention已经证明过缓存管理的分量:分页分配与共享减少了碎片和重复占用,其论文中的 vLLM 在当时的基线、模型和负载下获得 2–4 倍吞吐。GAAC 又往里走了一层,处理数值怎么存、注意力怎样直接消费压缩数据。推理服务的提速,很多就藏在这些矩阵计算之外的地方。

FP8 把 BF16 KV 的存储位宽减半,同时引入数值近似。vLLM 2026 年 4 月的报告里,H100 上 Llama-3.1-8B 的 150 请求、并发 8、约 20K 输入/2K 输出测试,聚合吞吐从 450.3 提高到 517.5 tok/s,约 14.9%;跳过滑动窗口层的 gpt-oss-20b 约提高 4.8%。位宽节省怎样传到系统吞吐,仍由模型结构和执行路径决定。

两条路线的精度账也不同。GAAC 在已经量化的 FP8 E4M3 上还能额外无损压缩 1.09–1.14 倍,在 E5M2 上为 1.25–1.30 倍,保存的是量化后的值。前一步 FP8 的误差、后续内核的累积行为,都应纳入具体任务的质量测试。

选型就从自己的基线出发。已经用上成熟 FP8 内核的系统,测继续压缩的增量收益;需要保存原始 BF16 KV 的系统,测无损路径的成本。硬件、模型结构、精度、batch 和工作负载对齐之后,比较才真正开始。

接进推理系统,才是费工夫的地方

GAAC 的指数窗口依赖校准。附录 C用 Qwen3-8B 的一层做位置漂移实验:短于或等于 8K 的校准窗口用于 32K 输入后,后段 Key 的溢出存储明显增多;用 32K 校准则较稳定。RoPE 会让 Key 的指数分布随位置变化。结果不会因此有损,但压缩率和回退开销会变。

跨任务测试给了一个不错的起点:WikiText 上选的窗口,在若干代码、数学和知识测试上仍维持接近的压缩率。接下来该把上下文拉长、请求跑久,同时记录槽位浪费、回退比例、前缀共享和缓存周转。线上请求会不断换形状,缓存也得跟着经受折腾。

Apple M4 上,作者用 Metal 实现压缩注意力,与同结构 BF16 KV 内核比较。64K 上下文单序列约加速 1.10 倍,8 条序列约 1.25 倍。这个内核已经跨过了平台门槛;再把它放进完整 Mac 推理框架,测首 token、连续生成和任务完成时间,才走到了本地应用真正关心的地方。

个人设备还要算温度和电池这笔账。论文给出了内存和时间数据,功率与能耗留待测量。额外计算与减少搬运究竟怎样影响续航,需要功率曲线、运行时长和持续负载测试。把每 token 能耗也放进对照表,本地部署的判断才完整。

真要部署,我会怎样做对照

先固定一条能复现的基线:模型及量化版本、推理框架和内核、上下文与输出长度、KV 精度、batch、请求到达方式。容量分别记录权重、驻留 KV 和工作区,瓶颈才有地方可找。

然后分开跑两组对照:固定并发,测完整 decode step 和请求延迟;固定内存预算,放开并发,测聚合吞吐、排队与 goodput。前者回答同一个用户能快多少,后者回答同一台设备能多接多少活。

延迟记录也要分层。vLLM 的指标文档区分首 token 时间 TTFT、输出间隔 ITL 和每输出 token 的平均耗时 TPOT。报告时应写清是否包含排队、怎样聚合,以及输出 token 数的分母,避免平均 tokens/s 把短请求的等待和长请求的卡顿都遮住。再配合 P50/P95/P99、任务质量、持续运行的内存和能耗,才比较接近实际使用。

对于 Agent,我还会保存包含工具等待和重新 prefill 的任务轨迹。若一次任务大部分时间在等外部工具,把 decode 改善 10% 并不会让任务总时长也缩短 10%。先确认时间花在哪里,才能判断下一个优化该落在哪一层。

本地推理最容易省错的,就是以为模型装下便万事大吉。权重变小以后,历史缓存、数据搬运和并发调度会接着要账。GAAC 用额外计算换掉了部分访存,收益沿着整条执行路径逐层兑现。下一次看到“提速 20%”,我会先找那张完整的耗时表,再决定这个优化值不值得接。