不一定有什么用, 不经常用还会忘掉.
之前被人问到一次,脑子卡壳回答不出要点, 非常尴尬!
在此记录一下使用心得, 偶尔回来翻看!
一 并行方式
1.1 数据并行 (Data Parallelism)
主要用于模型训练阶段, 即将多个完整的模型副本分布到多个gpu上, 每个gpu运行一部分数据数据, 每个设备独立地计算前向传播和反向传播,然后在每一轮迭代结束时,设备之间通过通信(比如nccl, gloo等), 将计算结果收集起来, 并通过某种归约运算(比如求和、取平均、最大值、最小值等)合并成一个全局结果, 之后广播机制向所有节点gpu发这个全局结果,同步梯度并更新模型参数, 从而确保每个节点都拥有相同的最终数据.
这是一种扩展batch_size的手段, 例如共有16个gpu, 每个gpu运行2个batch_size的数据, 那么总共的batch_size=2*16=32.
DP(Data Parallelism)与DDP(Distributed Data Parallelism)原理都是如此, 只是它们的通讯方式不同.
这种方式在模型规模小时有优势, 不过大模型时代已经被淘汰了, 因为常见显卡通常只要24G显存, A100也不过80G显存, 根本无法训练72B及以上规模的模型.
另外缺点也比较明显:通信开销较大,特别是在节点数量增加时。
我测试过在4张rtx titanx全量微调qwen2.5-vl-7B模型, 1000条多模态数据用了5天才完成2000+ step, 被deepspeed等新型训练方式吊打.
1.2 模型并行(Model Parallelism)
主要用于模型训练阶段, 适用于超大规模的模型. 没用过, 也不推荐使用. 因为实现复杂,通信开销大,效率较低.
处理思路是模型按层分割成若干块,每块都交给一个gpu, 前一个模块计算完成后, 计算结果传递给下一个gpu, 依次进行前向,反向传播. 过程中涉及多次通信传递, 更重要的是, 同一时间只有一个gpu在工作, 极大浪费计算资源, 效率低下.
1.3 流水线并行 (Pipeline Parallelism)
流水线并行可以认为是数据并行与模型并行的混合体.
根据前面知识可知, 数据并行是在每个gpu上独立运行一份数据, 模型并行是将模型切分到多个gpu上依次执行数据.
流水线并行则是将模型按层或模块顺序切分成多个阶段,每个阶段分配到不同的计算节点上,形成流水线。
看起来挺复杂, 不过看下面的流程图就非常容易理解了.
模型被切分到4个device上
如上图所示,原来的一个batch,拆分成了8个微批次.
- ① 首先在device1上运行第一份数据, 执行完成后传递给device2, 直至传递给device4完成计算.
- ② device1计算完成第一份数据后, 紧接着执行第二份数据, 完成后再传递给device2, 依次类推完成剩余数据计算.
- ③ 都计算完成后从device4依次执行反向传播计算, 更新模型梯度.
通过以上描述可知, 这种方法结合了数据并行和模型并行的有点
另外图中灰色区域是正向/反向计算间隔区域, 是gpu空闲时间, 被称为空泡率,减少空泡区域可以提高并行效率. 通常都是对反向传播
部分进行优化. 想了解细节可以参考以下文章:
https://zhuanlan.zhihu.com/p/32724741626
1.4 张量并行 (Tensor Parallelism)
张量并行将模型的张量(如权重矩阵)按维度切分到不同的计算节点上。
通常是将大型张量按行或列切分,每个节点处理切分后的子张量。需要通过集合通信操作(如AllGather或AllReduce)来合并结果
如大模型的attention通常由多个head组成, 此时就可以将多个head计算分配到不同gpu上.
例如某个attention由16个head组成, 分配到8张gpu上, 则每张gpu只负责计算2个head attention, 可以极大加速计算效率.
另外矩阵的行切分和列切分对最后结果合并有影响.
列切分的结构直接cat拼接在一起. 行切分则是结果相加.
1.5 3D并行
3D并行是一种特定的混合并行实现方式,由微软在DeepSpeed框架中提出,专指同时使用以下三种并行策略的组合:
- 数据并行(Data Parallelism, DP):拆分批次数据到不同设备,独立计算梯度后同步。
- 流水线并行(Pipeline Parallelism, PP):将模型按层切分到不同设备,按流水线方式分阶段计算。
- 张量并行(Tensor Parallelism, TP):将单个层的矩阵运算(如注意力头、FFN层)拆分到多设备。
3D并行常用于训练超大规模的大模型, 在部署推理时很少使用, 因为DP通信开销大, 还会出现DP冗余显存占用的情况.
目前主流推理架构, 比如vllm, tensorrt-LLM等都只实现PP+TP的方式, 使用其他方式来优化对数据的出来,比如PagedAttention好Continuous Batching等技术.
二 常见的推理技术
在(一)中提到了各种并行技术, 实际也是一种推理优化方案, 更详细的推理优化技术, 这里谈下自己的使用体验.
2.1 并发优化
并发优化的目的是提高吞吐量, 提高硬件利用率以及降低请求延迟
比较有代表性的技术有以下三个:
- dynamic batching(动态批处理) : 是指允许将一个或多个推理请求组合成单个批次(必须动态创建)以最大化吞吐量的功能. 从定义可知, 整个批次必须全部完成后才能返回结果, 对实时响应时间有很大影响, 这种技术在 Triton Inference Server有使用. 我没用过, 不做评价, 但从描述可知, 不适合做流式输出, 有很大的应用限制.
- continuous batching(连续批处理): 以token/迭代为粒度进行批处理,动态插入新请求到正在运行的批次中, 已完成请求可立即返回,无需等待整个批次.简单地说, 就是消除了padding, 让每个batch都装满, 极大提升了计算效率. Continuous Batching已成为现代大模型推理服务(如vLLM、TGI, Tensorrt-LLM等)的标准配置,能显著提高GPU利用率和系统吞吐量
- Async Serving(异步服务): 目的是减少I/O等待浪费, 优化高并发连接管理. 代表性技术如FastAPI, 与vllm等推理架构的异步引擎AsyncLLMEngine组合使用, 有互补作用, 我在工程化实践证明效果很好.
2.2 显存优化
目的是提升显存利用率, 增大推理吞吐量,提升推理并发能力.
关于显存优化,我接触到的只有flash-attn和PagedAttention
2.21 flash-attn
flash-attn, 准确地说, 这是GPU硬件对attention的优化技术, 原理是通过利用更高速的上层存储计算单元,减少对低速更下层存储器的访问次数,来提升模型的训练,推理性能.
flash-attn核心是对Self-Attention的计算进行分块计算. 开始阶段, flash-attn将输入矩阵分割成多个小块,然后对这些小块进行多次遍历。在每次遍历中,它计算当前块的softmax归一化值,并将其存储在内存中。通过逐步累加这些归一化值,最终得到整个注意力矩阵的结果,而无需在内存中存储整个注意力矩阵.
在前向传播中计算每个块的softmax归一化值,并将这些值存储在内存中。在后向传播中,它只需要利用这些存储的归一化因子和输入矩阵Q,K,V来重新计算注意力矩阵,而不需要重新计算整个注意力矩阵。这种方法显著减少了后向传播的计算量, 提升计算效率
更多细节参考这里:
https://www.zhihu.com/question/611236756/answer/86383686403
另外, flash-attn对硬件有要求, 需要 Ampere架构及以上, 2080ti, rtx titianx不支持, 非常可惜.
flash-attn的安装也是一个问题, 从源码编译很很多坑, 推荐直接使用别人已经编译好的版本:
https://github.com/Dao-AILab/flash-attention/releases
2.22 PagedAttention
PagedAttention是vllm中应用的技术, 这是一个很棒的推理技术, 其他推理架构也融入PagedAttention, 比如TensorRT-LLM, llama.cpp等架构, 反正好的技术大家都会相互借鉴. 该技术主要针对KV Cache 显存瓶颈 进行优化, 核心思想是动态分配, 这个过程有点类似C/C++中的指针操作, 指针只存储数据地址, 具体数据在单独的数据块中:
- 按需申请显存块,避免预分配浪费
- 非连续存储:允许一个请求的KV Cache分散在不同物理块中
- 共享机制:多个请求可共享相同前缀的KV Cache(如提示词相同)
在我之前的文章有更完整描述, 这里不展开了:
vllm源码解析(一):整体架构与推理代码
此时,不得不吐槽一下vllm中对PagedAttention的实现, 改了很多版本, 我最初读的是0.5.4, 现在的版本是0.8.2, 完全重构了主体逻辑架构, 不敢再理解源码了, 怕再被知识清零,白费工夫.
2.3 算子优化
原理是将多个连续操作合并为一个内核(Kernel),减少内存读写和内核启动开销, 例如在attention计算中,针对多查询注意力结构的QKV通用矩阵乘法(GEMM)横向算子融合,以及多层感知机(MLP)中的全连接层(FC)+激活融合.
这种优化方案太过底层, 与硬件设备强相关, 通常都是针对当前硬件的定制性优化.
工作中几乎用不到这项技术, 因为投入的技术/时间成本与回报完全不对等.
印象中, 只有一些推理/计算架构在搞,比如CUDA Kernel, FlashAttention, TensorRT-LLM, pytorch等, 距离我们太过遥远了.
2.4 分布式优化
分布式优化技术 是解决单机资源不足、提升吞吐量和降低延迟的关键手段, 现在有商用价值的大模型至少7B, 好一点的有32B,72B甚至更高, 单个gpu很难加载, 分布式技术已经训练/推理的必选项了.
这项技术主要分两部分: 通信优化和并行技术
通信技术比如NCCL和GLOO等, 使用时常从这二者选择, 对它们的优化没搞过, 不做评价.
并行技术在(一)中有详细介绍, 在使用时最常用的方案是流水线并行+张量并行, 目前所有推理都支持这两种并行技术,比如vllm ,tensorrt-llm, TGI, llama.cpp等. 这里分享下这两种技术组合使用的心得.
根据我的使用经验和pipeline-parallel-size自身原理, pipeline-parallel-size越大, 产生的空泡率越高, 好处是对微batch的处理速度变快, 但不应超过输入数据batch的大小.
tensor-parallel-size是将单个矩阵运算(如GEMM)在多个设备之间并行计算, 如tensor-parallel-size=8, 相当于每个gpu计算2个head, 充分利用gpu计算性能, 但代价也有, 就是每个attention的计算结果需要8卡直接同步, 通信效率较低.
所以具体工作中, 把所有可用的组合全部尝试一遍才是挑选针对当前设备最靠谱的方案, 这里给出部分配置的历史经验
假设一个大模型有32层layers, attention 有16个head. 目前有2台机器, 每台机器8张显卡, 设置不同的pipeline-parallel-size和tensor-parallel-size对推理性能有什么影响, 有以下三组参数:
- pipeline-parallel-size=2和tensor-parallel-size=8
- pipeline-parallel-size=4和tensor-parallel-size=4
- pipeline-parallel-size=8和tensor-parallel-size=2
2.5 低比特量化
就是常说的量化技术, 目前常用方案有autogptq, autoawq等, 还有直接的int4,Int8量化,不过这种方式效果与量化前差异很大, 目前用的较少了. 量化只是减小模型尺寸, 但推理速度不一定变快, 因为经过反量化操作再参与矩阵计算.
gptq是训练后量化, 逐层量化, 原理是先量化第一层, 调整后面所有层参数以最小化损失. 之后量化第二层, 固定第一,第二层再次调整后面所有层参数以最小化损失. 通过描述可知这中需要校准数据集的参与, 校准集选取和数据对量化效果有很大影响. 我操作过gptq的量化, 100张和300张校准集效果差不多, 当把数据增加到1000张,效果反而变差了.
个人更喜欢使用awq技术, 是一种感知量化, 认为模型权重只有1%的参数重要, 不参与量化, 其他不重要的权重参数才参会量化, 也需要校准集参与, 但对数据没有gptq那么敏感, 实践证明awq比gptq好用, 目前, 新出的大模型都使用awq作为量化手段, 已经很能说明问题.
kv-cache是推理阶段的量化, 将推理过程中产生kv-cache处理为int4/int8类型, 减少显存占用, 这种优化手段已经集成到各种推理架构中了,如vllm ,tensorrt-llm等.
2.6 其他优化技术
这类算法都可归类为投机算法
使用一个同类型的尺寸更小的模型模型进行推理, 称为草稿模型
- 先把当前query输入草稿模型,让其快速生成“草稿”
- 让目标模型并行地检查草稿,判断每个token是否可以被接受
例如可以选择qwen2-3B作为草稿模型, 以qwen-72B作为目标模型, 检查的速度可比生成的速度快多了, 虽然不一定每次都准确, 但只有中一个token就大赚.
简单了解下Medusa(多头美杜莎)的工作机制.
美杜莎在原始大模型(如LLaMA、Qwen)的基础上,在最后的输出层添加多个轻量级多头预测模块(Medusa Heads),通常由几层小型FFN(前馈网络)组成,用于并行生成候选Token
推理流程:
- 首Token生成:主干模型生成第1个Token yt。
- 候选Token预测:美杜莎头基于 yt 并行预测 k 个候选Token {yt+1,yt+2,…,yt+k}。
- 验证候选Token:主干模型对候选序列进行验证,保留正确部分。
- 接受或回退:若候选Token全部验证正确,则直接跳过后续计算,继续预测下一组。若部分错误,则回退到最后一个正确Token的位置重新生成。
多头美杜莎使用了【树形Attention】来一次性地生成每条路径的概率,极大地提高了验证草稿token的速度, 当头数为8时, 看报告说有4倍的推理加速, 不过需要训练模型, 有机会要验证一下.
三 推理架构参数解析
前面提到的各种推理优化方法, 都已经集成到推理架构中, 这里以vllm为例, 说下部分重要参数意义:
- –gpu-memory-utilization: 默认设置为 0.9,但如果显存较小,或多个显卡分布式时,有显卡在运行其他程序,可能会导致 Out of Memory 错误, 根据可用显存进行设置
- –tensor-parallel-size, pipeline-parallel-size: 根据上面介绍可知,分别是张量并行和流水线并行数量
- dtype: 加载权重的类型, huggingface下载的模型默认使用bfloat16, 如果当前显卡不支持, 需要手动改为float16
- max-model-len: 模型在处理输入时能够考虑的历史信息的总长度。通过max-model-len控制,不添加此参数时,系统将尝试使用最大可能的序列长度. 这个设置非常占用显存, 同时, 若进行多轮对话, 还需要把这个值设的比较大, 不然中途会报错.
- max-num-seqs: 每个 batch 中最多同时处理的序列(请求)数量. 它指定了 每次连续推理(batch)时并发处理的最大请求数。这个值越大,吞吐量(QPS)可能越高,所以在并发场景下可以适当设大这个值, 但同时也会占用更多的显存和系统资源.
- enable-prefix-caching: 单轮对话中system prompt是相同的,多轮对话中每一轮对话要依赖历史对话,对它们的KV Cache进行缓存,就不用每次重新计算. 现在vllm对前缀缓存时block级别, 即使有prompt不同, 也能使用部分kv-cache缓存.
- enforce-eager: 用于控制vLLM是否始终使用PyTorch的eager模式(即时执行模式),默认为False,vLLM会默认使用eager模式和CUDA图的混合模式来执行操作,这种混合模式旨在提供最大的性能和灵活性. 当enforce-eager为True时, CUDA图, 推理性能变差, 但会极大减小显存消耗, 有些时候这个参数很有用.
其他参数还有很多, 但影响性能的主要就这么多了
最后, 不得不感慨, 想在工作中单独探索某项优化手段对性能的提升, 那个时代已经不在了, 现在是各种推理架构的天下, 如vllm, tensorrt-llm, IMdeploy,llama.cpp等, 优化方案也是在这些架构上二次开发.