跳转至

FlexGen: 利用单 GPU 实现大语言模型的高吞吐量生成推理

Key Idea

将推理中的内存需求下放到 CPU/DISK,用延迟换吞吐量和内存需求。

背景与现状

当前的问题

大模型需要巨量的显存来完成推理。例如,OPT-175B 模型通常需要 5 张 A100 (80GB) 的显卡来加载权重。这限制了个人开发者或小型机构的使用。

主流的解决方法

  • Model Compression (压缩模型):通过优化模型的存储和加载。

  • Quantization (量化):将模型参数从高精度的浮点数(如 FP32 或 FP16)转换为低精度的数值(如 INT8, INT4 甚至 1.5-bit)。

  • Pruning (剪枝):结构“剔骨”,删除对最终输出贡献小的分支和参数。

  • Distillation (知识蒸馏)。

  • Collaborative Inference (协作推理)。

  • Offloading (卸载):空间换时间(本文所用方法)。

Offloading 当前面临的挑战

  • 挑战一:高效的规划算法

    如何设计一个高效的规划算法,对三种 Tensors (Weights, Activations, Key-value (KV) Cache) 哪些需要被卸载、卸载在哪儿、什么时候卸载,进行合理的规划。

    关于 Dependency Graph 的疑问

    原文: The batch-by-batch, token-by-token, and layer-by-layer structure of the computation forms a complex dependency graph.

    这里的 Dependency Graph 是什么意思?类似于“寄存器之间的依赖关系”吗?Token-by-token、layer-by-layer 之间的计算依赖大概能够理解,但是为什么 Batch-by-batch 之间也存在依赖关系?

    已有的方法: 继承训练的策略,但是都是次优点,I/O 过多,吞吐量远低于理论值。

  • 挑战二:有效的压缩策略

    如何设计有效的压缩策略,在不显著降低模型精度的前提下,进一步减少显存占用。

核心机制:Zig-zag 块调度

FlexGen 提出了一种不同于传统按行调度的策略——Zig-zag Block Schedule。

我们将推理过程抽象为一个方格阵列,一个方格代表一个 batch 中一个 Transformer Layer 的计算流程。其遵循以下规则:

  • 水平依赖:一个方格只有在它所在行的所有左侧方格都计算完成后才能计算。

  • 加载约束:要在设备上计算一个方格,必须将它的所有输入(权重、激活值、缓存)加载到同一个设备上。

  • 存储逻辑:计算完成后,一个方格会产生两个输出:激活值和 KV 缓存。

  • 激活值:应存储直到它的右侧相邻方格计算完成。

  • KV 缓存:应存储直到同一行的最右侧方格计算完成。

  • 容量限制:任何时候,设备上存储的张量的总大小不能超过它的内存容量。

文章中提到的 Zig-zag 调度策略如下:

并行优化 (Overlapping)

由于以下操作是不相干的,所以下一层权重的加载、下一批次的激活/缓存加载、前一批次的激活/缓存存储,它们可以并行进行;在这种模式下,只有两个关键变量:GPU batch size 和 number of GPU batches in a block。

张量放置比例

系统根据硬件配比,动态调整张量在 GPU (\(w_g\))、CPU (\(w_c\)) 和 Disk (\(w_d\)) 中的存放比例。

对于激活值,使用比例 \(h_g, h_c, h_d\)

对于 KV 缓存,使用比例 \(c_g, c_c, c_d\)

张量粒度

这里还额外提到了张量的粒度的问题,我们可以考虑一个Block级别的张量如何放置,或者一个Transformer Layer级别的张量如何放置,或者一个Batch级别的张量如何放置。不同粒度的张量放置会有不同的开销和性能表现。(类似于数据库中的粒度的Trade-Off?)

计算委托

有的计算我们可以委托给 CPU 进行。

成本核算

  • 将 KV 缓存移动到 GPU 的成本是 \(b \times s \times h \times 4\)
  • 反方向将激活移动到 CPU 的成本是 \(b \times h \times 4\)

如果我们面对一个较长的序列,I/O 成本将会超出计算成本。在这种情况下,将 KV 缓存保留在 CPU 端计算反而更具效益。

线性规划求解

那么我们可以建模计算的模型如下:

对于\(T_{pre}\)用于Prefilling 一层的时间,\(T_{gen}\)用于decoding中一层的时间,假设我们的I/O层及中的各个传输可以并行(也存在overlapping),那么 \(T_{pre} = max{(ctog^p , gtoc^p, dtoc^p,ctod^p,comp^p)}\)这些部分的最大值(对于T_gen也是类似的)。对于 dtoc (代表从磁盘到CPU的latency)这样的我们都可以估计其大小(在附录中,相对较长)。那么总的时间是:\(T_{pre} \times l + T_{gen} \times (n-1) \times l\)

总而言之,这一步分的策略中,总体的变量为: 块大小 bls , batch的大小端gbs , 9个如何放置张量的百分比。有如下的约束:一般而言 gbs 是4的倍数,要求bls小于20,那么我们可以得到一个线性规划问题。

这样的问题可以通过线性规划求解器来求解,得到最优的调度策略。

拓展

多个GPU的场景,如果LLM可以分为m个GPU来使用流水线的模型并行,并且GPU都遵循同样的模式,那么这上面的Offloading等价于对于 n/m 的Transformer 上进行 Single GPU的优化。

此外,FluxGen 还采用了量化的方式来进一步的减小I/O的开销;同时还可以利用稀疏注意力的机制,只取最大的 K 个注意力权重的索引。这样就可以进一步的减少 I/O 的开销。

Question

这样的工作与DeepSpeed,Llama.cpp这些框架的关系是?