FlexGen 完全拆解:用线性规划编排 GPU、CPU 与 SSD 上的大模型推理

8400 字
42 分钟
FlexGen 完全拆解:用线性规划编排 GPU、CPU 与 SSD 上的大模型推理

问题:显存放不下时,为什么“能跑”仍不等于“跑得动”

FlexGen 由 Stanford、UC Berkeley、ETH Zurich 等机构的研究者在 2023 年发表于 ICML。它研究的不是“如何让显存足够时的在线聊天再快一点”,而是一个更受约束的问题:当模型连 CPU 内存都未必放得下、只能借助 SSD 时,怎样用一张 16 GB 的普通 GPU 完成高吞吐生成式推理。

这一定义里有两个容易被忽略的限定。

第一,FlexGen 面向的是吞吐优先、延迟不敏感的离线任务,例如批量评测、文档信息抽取和数据清洗。单个请求等几分钟甚至更久并不理想,但只要整批数据完成得更快,就仍有价值。它不适合要求秒级首 token 的交互式服务。第二,“把张量移到 CPU 或 SSD”只解决容量问题,并不自动解决性能问题。若每生成一个 token 都把数百 GB 权重重新读一遍,系统虽然不会 OOM,却会被 PCIe 或 SSD 带宽彻底锁死。

可以用最简单的下界看清这个矛盾。设一层需要从慢速存储读入的权重为 WW 字节,链路有效带宽为 β\beta 字节每秒,一次只服务 BB 个序列,那么忽略计算后,每个序列分摊的权重读取时间至少是

tweight per sequence≥WBβ.t_{\text{weight per sequence}} \geq \frac{W}{B\beta}.

BB 越大,同一份权重被更多序列复用,单位序列承担的 I/O 越少。但批量一大,激活与 KV Cache 又会迅速吃掉内存。FlexGen 真正解决的是这个闭环:在 GPU、CPU、SSD 的容量和带宽约束下,联合选择批量大小、执行顺序、张量放置位置和计算位置,而不是孤立地做一次 offload。

FlexGen 统一调度权重、激活与 KV Cache
FlexGen 统一调度权重、激活与 KV Cache

图 1 来自 FlexGen 论文。三类张量进入统一调度器后,可以分别放在 GPU 显存、CPU DRAM 或硬盘;KV Cache 还可以先压缩再存储。图很简洁,但它表达了论文最重要的系统观:权重、激活和缓存的生命周期不同,不能再沿用“只卸载权重”的单一策略。

论文以 OPT-175B 为例说明容量压力。FP16 权重约占 325 GB,而 T4 只有 16 GB 显存。更反直觉的是,在批量 b=512b=512、提示长度 s=512s=512、生成长度 n=32n=32 时,KV Cache 峰值约为 1.2 TB,达到权重体积的 3.8 倍。大批量一方面摊薄权重 I/O,另一方面却把缓存推成新的容量瓶颈。因此,批量并不是越大越好,它必须与放置策略一起求解。

FlexGen 给出的答案可以压缩成四点:

  1. 用之字形块调度让已加载的一层权重连续服务多个 GPU 小批次;
  2. 分别决定权重、激活和 KV Cache 在三级存储中的比例;
  3. 把 I/O 与 GPU 计算重叠,并在特定情况下把注意力计算交给 CPU;
  4. 用代价模型和线性规划寻找高吞吐策略,再用 4-bit 压缩进一步减少 I/O。

这些设计的共同目标不是减少算术量,而是提高每次慢速数据搬运所支撑的有效计算量。

从 Transformer 推理的数据流重新定义卸载

理解调度前,先把一次生成拆成张量生命周期。设批量大小为 bb,提示长度为 ss,生成 token 数为 nn,Transformer 层数为 ll,隐藏维度为 h1h_1,MLP 中间维度为 h2h_2。对第 ii 层,输入为 XiX^i,注意力投影权重为 WQi,WKi,WVi,WOiW_Q^i,W_K^i,W_V^i,W_O^i。

在 Prefill 阶段,一次处理全部提示 token:

Ki=XiWKi,Vi=XiWVi,Qi=XiWQi.K^i=X^iW_K^i,\qquad V^i=X^iW_V^i,\qquad Q^i=X^iW_Q^i.

随后计算注意力与 MLP。这里 Ki,ViK^i,V^i 不会随这一层计算结束而释放,因为 Decode 的每一步都要读取历史键和值。到生成第 tt 个 token 时,新隐藏状态记作 X~ti\widetilde X_t^i,缓存会执行追加:

K1:s+ti=Concat⁡(K1:s+t−1i,X~tiWKi),K^i_{1:s+t}=\operatorname{Concat}\left(K^i_{1:s+t-1},\widetilde X_t^iW_K^i\right),V1:s+ti=Concat⁡(V1:s+t−1i,X~tiWVi).V^i_{1:s+t}=\operatorname{Concat}\left(V^i_{1:s+t-1},\widetilde X_t^iW_V^i\right).

当前 query 只有一个位置,却要与长为 s+ts+t 的全部历史 Key 做内积。这造成三类数据截然不同的复用方式。

张量大致生命周期主要复用方向卸载难点
权重整个任务始终存在同一层、不同序列与 token体积固定且很大,反复读取最昂贵
激活相邻层之间只被下一层消费生命周期短,但大 Prefill 激活仍可观
KV Cache从 Prefill 持续到序列结束同一序列后续所有 Decode 步随批量和上下文长度线性增长

按论文采用的 FP16 口径,忽略较小的嵌入层,权重字节数近似为

MW=l(8h12+4h1h2).M_W=l\left(8h_1^2+4h_1h_2\right).

其中 8h128h_1^2 来自注意力中的四个 h1×h1h_1\times h_1 矩阵,每个 FP16 元素占 2 字节;4h1h24h_1h_2 来自 MLP 的两个矩阵。KV Cache 峰值近似为

MKV=4blh1(s+n).M_{KV}=4blh_1(s+n).

系数 4 等于 Key 与 Value 两份张量乘以每个 FP16 元素 2 字节。这个式子揭示了系统设计的矛盾:增大 bb 能提高权重复用,却按相同比例增大 MKVM_{KV};延长上下文也不会增加权重,却会持续放大缓存。

LLM 推理按批次、层和 token 展开的计算图
LLM 推理按批次、层和 token 展开的计算图

图 2 同样来自论文。横向依次展开生成 token,纵向是近似无限的数据集,每个彩色方块代表某个 GPU batch 在某一层上的计算,相同颜色表示共享同一层权重。图中一条合法执行路径必须满足四个约束:左侧依赖先完成;权重、激活和缓存与计算必须位于同一设备;激活保留到下一层消费,KV Cache 保留到序列生成结束;任一时刻的驻留张量不得超过设备容量。

于是,卸载可形式化为受容量约束的计算图遍历:选择以什么顺序访问方块,同时决定访问每个方块前后要搬哪些张量。目标函数是计算时间与数据移动时间共同构成的总执行时间。

FlexGen 的核心:计算图分块与张量生命周期

最直觉的执行顺序是逐行调度:取一个 batch,完成第一个 token 的所有层,再完成下一个 token,直到该 batch 结束,然后换下一批。这种顺序的优点是单批完成得快,序列结束后也能立即释放 KV Cache;它天然偏向低延迟。

问题在于相邻方块通常属于不同层。假设模型权重放在 CPU 或 SSD,每跨过一层就要读入下一层权重;生成下一 token 时,又从第一层重新开始。若生成 nn 个 token,整套权重至少被轮转读取 nn 次,而且一次加载只服务当前 GPU batch。对 OPT-175B 这类模型,链路输送的字节数比 GPU 能执行的 FLOP 更早成为瓶颈。

另一个极端是逐列调度:加载第 jj 层权重后,让数据集中所有序列都通过这一层。它能最大化权重复用,但真实数据集可以无限大,产生的中间激活和 KV Cache 无处存放,系统很快耗尽 CPU 内存与 SSD 空间。

FlexGen 在两个极端之间取一个有限矩形块。一个块包含若干 GPU batch;系统加载某层权重,让块内所有小批次依次完成该层,再转到下一层。抵达 token 边界时,遍历方向反转,形成之字形路径。反转不是数学必需,而是一种减少控制复杂度、便于连续访问相邻边界数据的工程安排。

逐行调度与之字形块调度的计算顺序
逐行调度与之字形块调度的计算顺序

图 3 来自论文。上半部分的逐行调度沿每一行横向走完;下半部分的红色箭头在有限高度的块内纵向扫过,使同色方块共享一次权重驻留。这里需要区分两个批量概念:

  • GPU batch size(gbsgbs)是单次矩阵运算真正送入 GPU 的序列数,决定单个 Kernel 的形状和显存临时空间;
  • block size(blsbls)是一次权重驻留期间累计处理的序列数,等于 gbsgbs 与块内 GPU batch 数的乘积,也称有效批量。

例如论文在 OPT-175B 的无压缩策略中使用 gbs=32gbs=32、块内 8 个 GPU batch,因此 bls=256bls=256。GPU 并不需要同时容纳 256 条序列的全部工作集,却能让一次权重加载为 256 条序列服务。它把“计算批量”和“权重复用批量”解耦了。

块也不能无限加高。生成过程中的 KV Cache 长期存活,块内序列越多,缓存占用越大。用 ww 表示一层权重大小,只考虑 CPU 交换时,论文给出的峰值近似为

Mpeak=w+2h1 bls+4h1 bls l(s+n).M_{\text{peak}}=w+2h_1\,bls+4h_1\,bls\,l(s+n).

三项分别对应当前层权重、活动激活和整个块累积的 KV Cache。若可用 CPU 内存为 MCM_C,则块大小受到

bls≤MC−w2h1+4h1l(s+n)bls\leq \frac{M_C-w}{2h_1+4h_1l(s+n)}

约束。这解释了为什么 FlexGen 还必须卸载缓存、压缩缓存,并搜索张量放置比例:块调度只是创造了权重复用机会,容量规划才决定这个机会能放大到多少。

论文附录还构造了理论上 I/O 最优的对角块调度,但其状态管理与实现更复杂。最终系统采用之字形块调度,并证明其 I/O 复杂度不超过最优解的 2 倍。这个保证不是说运行时间最多慢 2 倍,因为实际还有计算、并发和碎片等因素;它只限定了理想化模型中的数据移动复杂度。

用线性规划搜索卸载策略

有了块调度,仍然存在一个很大的策略空间。FlexGen 用 11 个变量描述一项策略:blsbls、gbsgbs,以及三类张量在三个设备上的放置比例。

p=(bls,gbs,wg,wc,wd,hg,hc,hd,cg,cc,cd).\begin{aligned} p={}&(bls,gbs,\\ &w_g,w_c,w_d,\\ &h_g,h_c,h_d,\\ &c_g,c_c,c_d). \end{aligned}

下标 g,c,dg,c,d 分别代表 GPU、CPU 和 disk;ww 表示权重,hh 表示隐藏状态或激活,cc 表示 KV Cache。每类张量的比例必须守恒:

wg+wc+wd=1,w_g+w_c+w_d=1,hg+hc+hd=1,h_g+h_c+h_d=1,cg+cc+cd=1.c_g+c_c+c_d=1.

权重采用层粒度分区,激活和 KV Cache 采用张量粒度分区。若权重也切到元素粒度,放置更灵活,却会引入大量小传输、元数据与拼接开销;若激活和缓存只按整层放置,又很难精确填满设备空余容量。这是一个典型的“控制开销与搜索自由度”折中。

从硬件画像得到每层时间

FlexGen 先在目标机器上测量 PCIe、SSD 读写带宽,以及 GPU GEMM、GPU BMM 和 CPU 计算吞吐。对一个块的一层 Prefill,系统把时间分成五条可能并行的路径:

Tpre=max⁡(tc→gp,tg→cp,td→cp,tc→dp,tcompp).T_{pre}=\max\left( t_{c\rightarrow g}^{p}, t_{g\rightarrow c}^{p}, t_{d\rightarrow c}^{p}, t_{c\rightarrow d}^{p}, t_{comp}^{p} \right).

Decode 的平均每层时间同理:

Tgen=max⁡(tc→gg,tg→cg,td→cg,tc→dg,tcompg).T_{gen}=\max\left( t_{c\rightarrow g}^{g}, t_{g\rightarrow c}^{g}, t_{d\rightarrow c}^{g}, t_{c\rightarrow d}^{g}, t_{comp}^{g} \right).

上标 pp 与 gg 分别表示 Prefill 和 generation。这里取最大值而不是求和,是因为模型假设各方向 I/O 与计算能够理想重叠;最慢的流水阶段决定稳态时间。整个块经过 ll 层并生成 nn 个 token,总时间估计为

T=lTpre+l(n−1)Tgen.T=lT_{pre}+l(n-1)T_{gen}.

为什么是 n−1n-1?论文把产生第一个输出 token 的提示处理计入 Prefill,剩余输出才走逐 token Decode。不同系统对 Prefill/Decode 边界的计时定义可能不同,复现实验时必须统一这一口径。

以 Decode 阶段从 SSD 到 CPU 的读取为例,时间近似为

td→cg=1βd→c[wd(8h12+4h1h2)+4cd bls(s+n2)h1+2hd bls h1].\begin{aligned} t_{d\rightarrow c}^{g}=\frac{1}{\beta_{d\rightarrow c}} \bigg[&w_d(8h_1^2+4h_1h_2)\\ &+4c_d\,bls\left(s+\frac{n}{2}\right)h_1\\ &+2h_d\,bls\,h_1\bigg]. \end{aligned}

βd→c\beta_{d\rightarrow c} 是 SSD 到 CPU 的有效带宽。方括号三项分别是该层权重、平均长度下的 KV Cache 和激活字节数。s+n/2s+n/2 用生成期间的平均缓存长度近似从 ss 增长到 s+ns+n 的过程。这个公式清楚展示了不同决策的代价:降低 wdw_d 能少读权重,降低 cdc_d 能少读随批量和长度增长的缓存,降低 hdh_d 能少搬短生命周期激活。

为什么固定批量后能变成线性规划

FlexGen 并未把所有变量一起交给一个通用非线性求解器。它先枚举少量 (bls,gbs)(bls,gbs) 组合;固定二者后,传输字节数、计算拆分和设备峰值内存都能写成放置比例的线性函数。max 也可通过辅助变量线性化:令 zz 不小于五条路径的每一条,再最小化 zz。

优化目标是最小化每条序列的时间,也就是最大化吞吐:

min⁡pTblss.t.Mgpeak(p)≤Cg,Mcpeak(p)≤Cc,Mdpeak(p)≤Cd,wg+wc+wd=1,hg+hc+hd=1,cg+cc+cd=1.\begin{aligned} \min_p\quad &\frac{T}{bls}\\ \text{s.t.}\quad &M_g^{peak}(p)\leq C_g,\\ &M_c^{peak}(p)\leq C_c,\\ &M_d^{peak}(p)\leq C_d,\\ &w_g+w_c+w_d=1,\\ &h_g+h_c+h_d=1,\\ &c_g+c_c+c_d=1. \end{aligned}

Cg,Cc,CdC_g,C_c,C_d 是三级存储容量。注意,优化器最小化的是 T/blsT/bls,并非单个请求的 TT。某个策略让整块延迟变长,却把块大小扩大得更多,吞吐仍可能上升。这正是 FlexGen 与在线服务调度目标最本质的区别。

线性模型也有明确误差来源。真实 PCIe 和 SSD 带宽随请求大小变化;页锁定内存、CUDA 工作区、文件映射和碎片会挤占理论容量;多条 DMA 通路也未必能完全并行。论文用分段拟合与正则项改善硬件画像,求解后仍可能因 OOM 手工微调。更重要的是,归档的官方仓库说明自动策略优化器并未发布,命令行中的放置比例通常要参考基准策略后人工试验。因此,“论文有 LP”不等于开源代码会自动为任意机器找到最优参数。

三条关键数据通路如何重叠

块调度把一次权重加载摊给多个小批次,但若所有动作仍串行执行,SSD、PCIe、CPU 和 GPU 会轮流空闲。FlexGen 用多个 CPU 线程和 CUDA stream 维护并发流水。对当前层 jj、当前小批次 kk,稳态迭代可以抽象为:

并行发起:
预取下一层 j+1 的权重
写回上一批 k-1 的激活与 KV Cache
读入下一批 k+1 的激活与 KV Cache
在当前层 j 上计算当前批 k
等待本轮依赖完成,然后推进流水

这些动作访问不同方向或设备资源,所以有重叠机会:磁盘把下一层权重读入 CPU,PCIe 把当前需要的数据传入 GPU,GPU 同时计算已经就绪的 batch,另一路再把已消费的激活写回。稳态吞吐由最慢阶段决定;但流水的启动和排空仍有气泡,小块或短生成时尤其明显。

为什么有时应该在 CPU 上算注意力

通常 GPU 比 CPU 快得多,把计算移到 CPU 看似反常。关键在于 Decode 注意力常常受数据移动而非 FLOP 限制。若 KV Cache 已放在 CPU,把它搬到 GPU 只为做一次单 query 注意力,需要传输的缓存量近似为

MKV,move=4bsh1,M_{KV,move}=4bsh_1,

而把当前隐藏状态送到 CPU 只需近似

Mact,move=4bh1.M_{act,move}=4bh_1.

两者相差约 ss 倍。对于 s=512s=512 的长提示,避免搬运整段 KV Cache 可能比使用更快的 GPU 算术单元重要。因此,当某部分缓存驻留在 CPU 时,FlexGen 允许 CPU 直接计算相应注意力,再把小得多的结果送回 GPU。

这不是“CPU 注意力总比 GPU 好”。它只在缓存本来就在 CPU、PCIe 搬运成为瓶颈、CPU 还有可用算力时成立。若 KV Cache 已驻留 GPU,或现代 GPU 端注意力 Kernel 能直接利用高带宽显存,委托 CPU 反而会增加同步和低效计算。启用 4-bit 压缩后,论文还关闭了 CPU delegation,因为 CPU 上的量化与反量化开销会抵消省下的传输。

SSD 不是更慢的显存

论文测试机器的 SSD 读带宽约 2 GB/s、写带宽约 1 GB/s。它与 GPU HBM 相差数个数量级,因此 SSD 的正确角色是“容量最后防线”,而不是频繁随机访问的缓存。FlexGen 为磁盘张量创建文件并映射到虚拟内存,依靠大块顺序访问与预取获得有效带宽。

若 325 GB 权重每个 Decode 步都完整从 2 GB/s SSD 读取,仅理想顺序读就需要约 162.5 秒;任何 Kernel 优化都无法补回这个数量级。块调度的意义正在于让这 162.5 秒对应数百条序列,而不是一条序列。4-bit 压缩若还能让权重完全进入 208 GB CPU 内存,就会直接消灭最慢的 SSD 权重通路,收益远大于“字节数减少四倍”这一表面描述。

权重压缩与 KV Cache 压缩

FlexGen 的量化目标与常见低比特 GEMM 不同。GPTQ、AWQ 或专用 W4A16 Kernel 关注的是用低比特权重直接加速矩阵乘;FlexGen 首先关注存储容量和 I/O 字节数,计算前会把数据还原为 FP16。关于直接低比特计算的路线,可参考 Marlin 的 W4A16 Kernel 拆解;两者优化的瓶颈并不相同。

它采用细粒度、分组非对称量化。对一组连续的 gg 个值,先取组内最小值 xminx_{min} 与最大值 xmaxx_{max},再映射到 bb 位无符号整数:

q(x)=round⁡(x−xminxmax−xmin(2b−1)).q(x)=\operatorname{round}\left( \frac{x-x_{min}}{x_{max}-x_{min}}(2^b-1) \right).

反量化为

x^=xmin+q(x)2b−1(xmax−xmin).\widehat{x}=x_{min}+\frac{q(x)}{2^b-1}(x_{max}-x_{min}).

这里 xminx_{min} 相当于零点对应的偏移,(xmax−xmin)/(2b−1)(x_{max}-x_{min})/(2^b-1) 是缩放因子。分组越小,每组动态范围越贴近局部分布,量化误差通常越低;代价是每组都要保存尺度与最小值,元数据比例和量化 Kernel 开销更高。

论文对权重与 KV Cache 都使用 4-bit、组大小 64,不需要重新训练或校准。权重沿输出通道维分组,KV Cache 沿隐藏维分组。这一选择兼顾局部数值范围与运行时连续访问。量化后的张量在 CPU 或磁盘中存储,使用前反量化成 FP16,因此其主要收益是:

  • 权重更可能从 SSD 层提升到 CPU 层,跨越一次数量级更大的带宽差;
  • 相同 CPU/SSD 容量能容纳更大的有效批量;
  • KV Cache 的传输字节减少,使长上下文 Decode 的 I/O 流水更容易被计算覆盖。

论文还实验了 Top-K 稀疏注意力:先根据 Key 计算注意力分数,只加载得分最高的 10% Value。它减少了 Value Cache I/O,但仍需访问 Key 才能选出 Top-K,而且“10%”是固定实验策略,并不保证对其他模型、层和任务都安全。FlexGen 的核心贡献并不依赖这项近似;把它视为系统可插入的一种推理近似更准确。

准确率实验使用 LAMBADA 与 WikiText。OPT-175B 的 LAMBADA 准确率从 FP16 的 0.758 变为 4-bit 的 0.756;WikiText 困惑度从 10.82 变为 10.94。加入 10% Value 稀疏后,这两项仍为 0.756 与 10.94。论文也报告 3-bit 无法维持准确率。这里的结论应限定为论文所测 OPT 模型和任务,不能外推为所有模型都能无校准地使用相同 4-bit KV 格式。后来 KIVI 等工作进一步研究了 Key 与 Value 的分布差异,可参考 KIVI 的非对称 2-bit KV Cache 量化。

从算法到工程实现

论文中的比例变量落到开源实现后,是一个明确的 Policy 对象。命令行参数 --percent 接收六个数字,依次表示权重在 GPU、CPU 的百分比,KV Cache 在 GPU、CPU 的百分比,以及激活在 GPU、CPU 的百分比;每一类剩余的比例自动归入磁盘。例如:

Terminal window
python3 -m flexllmgen.flex_opt \
--model facebook/opt-30b \
--percent 20 80 0 100 0 100 \
--gpu-batch-size 48 \
--num-gpu-batches 3

这个策略表示:20% 权重放 GPU、80% 放 CPU;KV Cache 全放 CPU;激活也全放 CPU;有效批量是 48×3=14448\times3=144。这里只用于解释参数结构,并非对任意 30B 模型与机器都适用的通用配置。模型结构、上下文长度、CPU 内存、PCIe 代际和实际可用带宽改变后,安全比例也会改变。

设备抽象与异步复制

运行时把 GPU、CPU 和磁盘包装成统一设备,并让张量知道自己的 home device。磁盘张量不是某种 CUDA 可直接计算的对象,而是由 NumPy open_memmap 创建的文件映射;真正计算前仍要经过 SSD 到 CPU,再经过 CPU 到 GPU。分层抽象的价值是把张量放置与算子逻辑分开,代价则是必须显式管理异步复制、缓冲区所有权与生命周期。

源码为权重加载、缓存加载和缓存写回分别创建 CUDA stream。理想流水中,当前 stream 执行层计算时,其他 stream 可以预取下一层或搬运相邻 batch 的状态。但正确性依赖事件同步:下一层不能在权重复制完成前读取;一个缓存缓冲区也不能在前一计算仍使用它时被覆盖。FlexGen 的 block 循环实质上既是调度顺序,也是环形缓冲区的安全协议。

CPU 侧还要处理两种内存。普通 pageable 内存容量压力较低,但 CUDA 传输通常需要额外 staging;pinned memory 能提高异步 PCIe 复制效率,却不能被操作系统换出,而且锁页本身带来显著额外占用。官方说明在 OOM 时可关闭权重 pinning,能减少约 20% 或更多 CPU 内存,但速度会下降。这说明“CPU 内存还有 10 GB 空余”不代表策略一定可运行,页锁定副本和框架工作区也必须计入余量。

一个层级流水的伪代码

下面的伪代码省略了事件与边界条件,但能展示控制结构:

for token_step in generation:
for layer in transformer_layers:
for micro_batch in block:
async_load(weight[layer + 1])
async_store(activation[layer, micro_batch - 1])
async_store(kv[layer, micro_batch - 1])
async_load(activation[layer, micro_batch + 1])
async_load(kv[layer, micro_batch + 1])
compute(layer, micro_batch)
synchronize_required_events()

外层 token 循环决定自回归依赖,中层 layer 循环保持 Transformer 拓扑正确,内层 micro-batch 循环提供权重复用与流水并发。把 micro-batch 循环放到最内层很关键:同一层权重在它期间保持热驻留。逐请求卸载通常把 layer 或 token 作为最内层,因而失去这次复用。

多 GPU 为什么可能出现超线性 Decode 扩展

FlexGen 选择流水并行,把 ll 层平均切给 mm 个 GPU,每个 stage 只负责约 l/ml/m 层,再在 stage 间传激活。相比张量并行频繁进行 collective,流水并行通信粒度较粗,更符合吞吐优先目标。

论文报告四张分布式 T4 上,OPT-175B Decode 吞吐从单卡 0.83 token/s 提升到 3.86 token/s,略高于线性四倍。原因并不是 GPU 凭空多出算力,而是每个节点分到的权重和缓存压力降低后,策略可能从 SSD 卸载跨越到 CPU 卸载,或使用更大的 batch。资源翻倍触发了离散的存储层级变化,所以性能曲线不是线性的。不过包含 Prefill 的总生成吞吐只从 0.69 提升到 2.33 token/s,因为短输出下 Prefill 流水气泡占比仍高。

实验结果该怎样解读

论文主实验在 Google Cloud 的单张 NVIDIA T4 16 GB、208 GB CPU DRAM 和 1.5 TB NVMe SSD 上完成,SSD 读写带宽约为 2 GB/s 与 1 GB/s。模型为 OPT-6.7B、30B 和 175B;合成输入统一 padding 到 512 或 1024 token,每条提示生成 32 token。指标定义为

generation throughput=bntprefill+tdecode.\text{generation throughput} =\frac{bn}{t_{prefill}+t_{decode}}.

这个指标把整个 batch 的 Prefill 与 Decode 都计入分母,不能与只统计 Decode token/s、只报单请求速度或排除排队时间的结果直接比较。

最大吞吐不是单请求速度

输入长度 512 时,论文报告如下结果:

系统OPT-6.7BOPT-30BOPT-175B
Hugging Face Accelerate25.120.620.01
DeepSpeed ZeRO-Inference9.280.600.01
Petals8.252.840.08
FlexGen25.267.320.69
FlexGen,4-bit 压缩29.128.701.12

单位均为生成 token/s。OPT-175B 的 0.69 token/s 来自 32×8=25632\times8=256 的有效批量,基线最大批量只有 1 或 2;压缩策略用有效批量 144 达到 1.12 token/s,并因压缩后权重可以驻留 CPU 而绕开 SSD。这里批量 144 反而小于无压缩的 256,却更快,说明决定性能的不是批量单变量,而是是否跨过了存储层级边界。

论文在吞吐基准中使用 dummy weights,只在准确率实验中使用真实权重。这不会改变同尺寸矩阵运算和数据移动的字节数,但意味着吞吐表并未同时验证模型输出正确性;正确解读是“系统执行路径的性能测量”加上独立的“量化准确率测量”,而不是一项端到端业务质量基准。

FlexGen 在 OPT-175B 与 OPT-30B 上的延迟—吞吐 Pareto 曲线
FlexGen 在 OPT-175B 与 OPT-30B 上的延迟—吞吐 Pareto 曲线

图 4 来自论文。横轴是完成整个块的总延迟,纵轴是生成吞吐,两者都采用对数刻度。橙色 FlexGen 与蓝色压缩版通过增加有效批量向右上移动:单块更慢,但单位时间完成的 token 更多。绿色 DeepSpeed 与红色 Accelerate 很快受 OOM 限制,无法继续通过扩大批量换吞吐。蓝色虚线并非在所有延迟点都胜过橙线;压缩有反量化开销,只有当减少 I/O 或跨越容量边界的收益更大时才占优。

消融实验揭示了真正瓶颈

在提示长度 512 的 OPT-175B 上,完整无压缩系统为 0.69 token/s。取消策略搜索后降到 0.27;取消 I/O 重叠后为 0.59;取消 CPU 计算后为 0.62;完全禁用磁盘则 OOM;把 DeepSpeed 的放置策略放进 FlexGen 运行时只有 0.01。由此可见,最大的增益不是某一个 CUDA Kernel,而是让有效批量、缓存放置和权重复用形成一致策略。

OPT-30B 的情况不同:完整系统为 7.32 token/s,取消 CPU 计算后降到 4.03,影响比 OPT-175B 更显著。原因是 30B 策略把权重放在 GPU 与 CPU、缓存放 CPU,CPU 就地注意力能省下大量 PCIe 传输;175B 还受到磁盘权重通路支配,优化其中一条次要路径的相对收益更小。消融结果不能脱离具体策略和瓶颈层级来比较。

实验数字为何不能直接套到今天的模型

T4、OPT 和 2023 年软件栈提供了清晰的受限资源案例,却与现代部署有多处差异:

  • GQA 或 MQA 会显著减少 KV Cache 头数,改变 MKVM_{KV} 与权重 I/O 的平衡;
  • MoE 的总权重很大,但每 token 只激活部分专家,卸载策略还要考虑路由与专家热度;
  • PCIe 5.0、CXL、更快 NVMe 与更大主存会移动各层级的临界点;
  • FlashAttention、分页 KV 与融合低比特 Kernel 会改变 GPU 计算时间和临时内存;
  • 真实请求长度不齐,padding、提前结束和到达时间波动都会破坏静态大块的整齐流水。

因此,FlexGen 最耐久的价值是调度方法与代价建模思路,而不是表中某个固定 token/s。

与其他推理内存技术的边界

FlexGen 常与 PagedAttention、统一内存或权重量化混在一起讨论,但它们解决的不是同一层问题。

技术主要问题核心动作典型目标
FlexGen总工作集超过 GPU,甚至超过 CPU跨 GPU/CPU/SSD 放置并重排计算离线大批量吞吐
PagedAttentionGPU 内 KV Cache 的碎片与动态增长用逻辑块表映射物理 KV block在线动态批处理
vAttention分页寻址给 Kernel 带来侵入性预留连续虚拟地址,按需映射物理页保留连续 Kernel 接口
权重量化 Kernel权重带宽与 Tensor Core 利用低比特存储并在计算路径解码GPU 内直接加速 GEMM
P/D 分离Prefill 与 Decode 资源特征冲突将两阶段放到不同实例并传 KV集群级 SLO 与 goodput

PagedAttention 假设 KV Cache 主要仍由 GPU 管理,重点消除预留和碎片浪费;FlexGen 则允许缓存整体落到 CPU 或 SSD,重点减少跨层级数据移动。两者可以组合:上层策略决定哪些缓存页在哪种设备,下层分页器管理某一设备内部的物理分配,但组合后代价模型必须纳入页粒度传输和命中率。

vAttention 用 CUDA 虚拟内存把逻辑连续与物理按需分配结合,目标仍是让高性能连续注意力 Kernel 易于使用。它不解决权重比整机 DRAM 还大的情形,也不会自动安排 SSD 预取。

与 DistServe 的 Prefill/Decode 分离 相比,FlexGen 在单机或少量 GPU 内把慢速存储带宽摊给离线大批量;DistServe 在集群中隔离两阶段干扰,优化满足延迟 SLO 的 goodput。前者愿意牺牲单请求延迟,后者恰恰需要控制 TTFT 与 TPOT。

它也不同于操作系统透明统一内存。透明迁移按缺页发生,系统很难保证大模型层级访问与预取顺序,错误的逐页抖动会毁掉带宽。FlexGen 知道下一层、下一批和下一 token 的依赖,能显式发起大块预取并构造稳定流水。这体现了领域专用运行时相对通用虚拟内存的优势。

局限、适用场景与部署判断

FlexGen 最适合同时满足以下条件的任务:模型无法放入 GPU;输入可以积累成大批量;单条结果无需立即返回;CPU DRAM 或本地 NVMe 容量充足;工作负载的长度分布相对可预测。批量评测、离线标注、固定语料抽取和夜间数据处理都符合这一画像。

若是低并发聊天、流式 Agent 或严格 TPOT 服务,FlexGen 通常不是合适选择。权重与缓存跨 PCIe/SSD 往返会把单请求延迟拉得很高,之字形块还必须等待足够多输入才能形成有效复用。论文官方仓库也明确说明,小 batch 时它会显著慢于权重完整驻留高性能 GPU 的系统。

部署前可以按以下顺序判断,而不是直接复制论文参数。

  1. 先算容量账。按真实模型精度计算权重,按层数、KV 头数、head dimension、上下文和并发计算 KV Cache;为 CUDA workspace、页锁定缓冲和碎片预留空间。
  2. 再量带宽。分别实测 SSD 顺序读写、CPU 内存带宽和双向 PCIe,不能用厂商峰值替代端到端有效值。
  3. 找主导字节。若权重搬运占主导,增大 block size 或压缩权重;若缓存搬运占主导,减少并发、量化 KV 或让 CPU 就地算注意力;若计算占主导,卸载调度不会制造额外 FLOP。
  4. 检查延迟预算。通过 T/blsT/bls 优化出的吞吐策略可能让单块 TT 非常大,应先设定可接受的最大完成时间,再搜索可行 Pareto 点。
  5. 用真实长度回放。长度不齐会导致 padding 浪费和块内提前结束,静态合成序列的最优策略可能失效。

还要注意四个系统性局限。

其一,论文的线性模型假设较理想的并发。SSD、CPU 内存与 PCIe 往往共享 NUMA、IOMMU 或内存控制器资源,多个方向同时工作时的带宽之和并不独立。其二,磁盘耐久性与热节流未进入目标函数,长时间大流量写回会影响消费级 SSD。其三,公开实现已在 2024 年归档,主要围绕 OPT,不能视为持续适配新架构的生产框架。其四,压缩准确率只覆盖有限模型和数据集,现代长上下文、代码或多模态任务需要重新校验。

FlexGen 仍给出一个很有价值的架构原则:当性能由数据移动主导时,应优化“每搬一个字节完成多少有用 token”,而不是只盯着某个算子的 TFLOPS。大 batch、块调度、张量放置、计算委托和量化都是这个原则在不同层面的实现。

小结

FlexGen 把显存不足问题提升成一个异构内存调度问题。它先把自回归推理展开为 batch、layer 与 token 三维计算图,再用之字形块调度让同一层权重服务多个 GPU 小批次;随后分别规划权重、激活和 KV Cache 在 GPU、CPU、SSD 的位置,并用线性代价模型在容量约束下优化每序列时间。

它最关键的三个洞察是:

  • 权重 I/O 必须靠有效批量摊薄,计算 batch 与复用 block 应当解耦;
  • 数据在哪里,有时比哪种处理器算得快更重要,CPU 就地注意力就是典型例子;
  • 压缩的最大收益可能不是低比特算术,而是让工作集从 SSD 跨越到 CPU 或从 CPU 跨越到 GPU。

FlexGen 并没有把廉价单卡变成低延迟的高端集群。它用更长的单块延迟换取更高的离线吞吐,并清楚标出了这项交换成立的条件。也正因如此,它是理解大模型推理中“计算、容量、带宽、批量”四者如何耦合的一篇经典系统工作。

参考资料

  1. FlexGen: High-Throughput Generative Inference of Large Language Models with a Single GPU(ICML 2023)
  2. FlexGen arXiv 论文与 HTML 全文
  3. FlexLLMGen 官方开源仓库
  4. DeepSpeed Inference: Enabling Efficient Inference of Transformer Models at Unprecedented Scale
  5. OPT: Open Pre-trained Transformer Language Models
  6. Petals: Collaborative Inference and Fine-tuning of Large Models
  7. Hugging Face Accelerate:Big Model Inference 官方文档
  8. KIVI: A Tuning-Free Asymmetric 2bit Quantization for KV Cache

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

FlexGen 完全拆解:用线性规划编排 GPU、CPU 与 SSD 上的大模型推理
https://pinghaoyang.com.cn/articles/insights/posts/flexgen-offloading/
作者
平昊阳
发布于
2026-10-17
许可协议
CC BY-NC-SA 4.0

评论区

访客信息
--°天气加载中

正在获取所在地区…

💨 风力--
💧 湿度--

正在读取天气

Profile Image of the Author
平昊阳
乘长风,破巨浪, 展鸿图于未央!
公告
欢迎来到我的个人博客!欢迎关注交流吖!
更多相关公告,见
「社交-留言」。
音乐
封面

音乐

暂未播放

0:000:00
暂无歌词
站点统计
文章
448
分类
29
标签
451
总字数
6,178,652
运行时长
0 天
最后活动
0 天前

文章目录