资讯动态

大模型推理加速实战:FasterTransformer优化原理与踩坑经验

发布时间:2026/10/20 3:32:03 来源:尧图企业网站定制
前阵子给一个对话式大模型做服务化部署白天把推理脚本调通晚上一看压测结果差点没睡着平均生成一个token要几十毫秒并发一上去GPU利用率倒是拉满了可延迟跟着爆炸QPS根本没法看。我先后试过换更大显存的卡、调整batch大小效果都非常有限。后来才意识到问题不在硬件规模而在整个推理链路的执行方式——同样的权重换一个更懂GPU的推理引擎差距立刻就不是一星半点。这也是我今天想重点聊聊 FasterTransformer 的原因。这篇内容不是官方文档的翻译而是我实际接入 FasterTransformer 做大模型推理加速时的完整经验整理。适合已经把模型跑通、正在被推理延迟和吞吐困扰的开发者也适合想理解推理加速到底在加速什么的入门读者。我会先拆解推理慢的根源再讲清楚 FasterTransformer 的核心优化手段然后给出从编译到接入的完整实操过程最后把我踩过的坑和精度对齐的方法一并交底。1. 推理慢的根源多数时候不是显卡不够强1.1 先分清两个阶段预填充与生成Transformer 模型在推理时其实分为两个完全不同的阶段。第一个是预填充prefill也就是用户把一整段输入丢给模型模型并行计算所有的输入token得到首轮输出的过程。这个阶段矩阵乘法规模大、并行度高计算量可以吃满GPU。第二个是生成decode也就是从第一个输出token开始模型一个token一个token地往后蹦。这个阶段每次只处理一个或一小批token上一轮的输出作为下一轮的输入。整个过程本质上是串行的而且每一步都要读取整个模型的参数权重跑一遍完整的Transformer层。很多人以为推理慢是显存不够或者显卡太老但真正的问题往往出在decode阶段每一步的计算量不大却需要反复搬运权重、启动大量小kernel、写中间结果。GPU的算力没有被充分利用反而被内存带宽和调度开销卡住了脖子。一次decode的延迟里真正花在矩阵乘法上的时间可能连一半都不到。1.2 常规PyTorch推理的低效点在哪里用原生PyTorch跑推理时一个Transformer层会被拆成几十个CUDA kernel。做一次attention需要分别调用多个算子QKV投影、reshape、矩阵乘法、softmax、mask、矩阵乘法、输出投影。每一步之间中间结果都要写到显存里下一步再读回来。这个写出去再读进来的代价在高带宽显存上也依然存在。更隐蔽的开销是kernel launch。每启动一次CUDA kernelGPU都要经历一次调度这个开销在微秒级别。单个kernel看起来不起眼但一个模型几十层每层几十个kernel累积起来就是毫秒级的纯调度开销。decode阶段每次只处理一两个token矩阵本身就很小kernel数量却一点不少调度开销的占比就尤其刺眼。内存分配也是个大问题。PyTorch在推理过程中会不断创建中间张量、释放再分配。虽然PyTorch有自己的缓存分配器但面对Transformer这种固定结构的计算图依然有不少浪费。CUDA的显存分配本身是重操作频繁分配和释放不仅慢还会加剧显存碎片化。1.3 正确的优化方向看清楚了瓶颈之后方向就明确了推理加速的核心不是把GPU换成更贵的型号而是减少kernel数量、减少显存中间交换、降低调度开销、针对decode阶段的小矩阵做专门优化。这些恰恰是 FasterTransformer 这类专用推理引擎在做的事。它把PyTorch时代灵活但昂贵的执行方式换成了固定但高效的专用实现。2. FasterTransformer 快在哪里融合、复用、调度的组合拳2.1 算子融合把几十个小kernel合并成几个大kernelFasterTransformer 最核心的手段是算子融合。通俗地说就是把原来散落在不同kernel里的计算步骤合并到一起让中间结果留在GPU寄存器或共享内存里不再反复写回显存。以多头注意力为例。常规实现里QKV投影之后会得到三个独立的张量分别做reshape得到多头表示。FasterTransformer 会把QKV投影放在同一个kernel里完成投影结果直接拆成Q、K、V三份不产生额外显存写入。计算 attention 分数时Q和K的矩阵乘法结果不会整体写回显存再取出做softmax而是在kernel内部直接把softmax和乘以V的步骤接在后面。LayerNorm也属于典型的融合对象。常规做法是先算均值和方差再对向量做归一化最后做缩放和偏移这通常要拆成多个kernel。FasterTransformer 把均值计算、归一化、缩放、偏移以及残差连接全部塞进同一个kernel一步到位。可以这样理解普通实现像一单快递要在不同分拣站之间来回转运每到一个站就要重新卸货装货算子融合是把整个链路放进一个分拣中心里货物在传送带上直接走完所有工序。省下的不只是运输时间还有每个站点的交接成本。2.2 显存复用让KV Cache和中间buffer活起来Transformer 模型在生成阶段有个固有需求每一步计算attention时都需要用到之前所有token的K和V向量。如果不做缓存每生成一个token都要重新计算前面所有token的历史K、V计算量会随序列长度线性增长非常浪费。FasterTransformer 会提前分配一大块显存做 KV Cache每生成一个token就把新的K、V追加进去。decode阶段只计算当前token的Q然后和缓存的K、V做attention计算不需要重新处理历史信息。这个缓存池在beam search这类多个候选同时生成的场景下也能复用一次分配、多次写入。中间buffer的复用同样关键。Transformer 层的结构高度固定每一层需要的临时显存空间形状都差不多。FasterTransformer 为这些中间结果维护了一个buffer池反复循环利用同一块显存而不是每层都重新申请。这一步不仅减少了显存分配的系统调用次数也让显存占用降下来不少。2.3 面向实际shape的GEMM调度Transformer 推理非常依赖矩阵乘法尤其是decode阶段的矩阵乘法形状和训练时很不一样batch小、序列长度短但隐藏层维度不小像[batch, hidden] × [hidden, 4*hidden]这种形状。这种小矩阵在通用矩阵乘法库里不一定能跑出最优性能因为通用库的更侧重于大矩阵的并行调度。FasterTransformer 针对这些形状做了专门的调优内部维护了不同尺寸下的GEMM实现选择逻辑包括使用不同的线程块尺寸、流水线排布方式。在decode阶段它还能把同一批次多个请求的矩阵运算合并处理减少kernel数量提升GPU占用率。这种针对特定形状做定制的思路和传统深度学习框架通用优先的设计哲学很不一样。框架要服务的是千万种可能的模型结构而推理引擎只需要服务一个固定图结构所以可以做大量激进的假设和优化。3. 环境准备与编译最容易翻车的一步3.1 依赖清单和版本组合FasterTransformer 的构建依赖不多但版本组合需要稍微上点心。我用的环境是常见的 Linux 发行版配了 CUDA 工具链、CMake 和编译器等基础依赖。如果计划用PyTorch接口调用还需要装好对应版本的PyTorch。依赖大体上包括这几个CUDA Toolkit编译和运行都需要CMake3.18以上会比较省心支持 C14 的 GCC/GPyTorch如果走Python调用路径版本匹配上我的建议是不要用太旧的CUDA也不要刻意追最新的。编译器版本和CUDA版本之间有兼容性要求如果编译过程中遇到一堆莫名其妙的模板报错多半就是编译器太新或太旧。源码包里通常有说明文档按说明里推荐的组合来最稳妥。3.2 编译参数架构参数必须写对编译时最关键的参数是目标GPU的架构代码。这个参数告诉编译器要为哪一代GPU生成计算代码。架构参数写错了编译出来的库在目标显卡上根本跑不起来典型报错是找不到可用的kernel image。获取架构参数的方式很简单在Linux终端里执行nvidia-smi看一下GPU型号再去查对应的计算能力版本换成对应的架构码。在使用CMake构建时通过CMAKE_CUDA_ARCHITECTURES指定架构码。比如你的卡是计算能力8.6就填86是9.0就填90。一张卡可以同时填多个架构码用分号隔开编译出的库就能在多代GPU上运行。构建命令大概长这样cmake -DCMAKE_BUILD_TYPERelease -DCMAKE_CUDA_ARCHITECTURES86 .. make -j这个过程会根据项目规模消耗不少时间。如果编译过程中内存被占满导致进程被杀可以把-j后面的并行数调小一点比如make -j4慢一点但稳定。3.3 编译中可能遇到的问题我见过最多的问题有两个。第一个是子模块没有拉全导致编译时找不到依赖的头文件。解决方式是确保拉取源码时使用递归参数如果已经漏了可以补拉子模块。第二个就是架构参数没写对编译倒是过了运行时报内核镜像错误。这个在换卡部署时尤其常见所以建议编译时把目标部署环境的全部架构码都填上。还有一个容易被忽略的点编译产物和PyTorch的python接口是分开的。即便C这块编出来了python接口也未必可用需要确认python的包装库有被正确构建或安装。路径没对上Python里调用时会提示找不到共享库。4. 从PyTorch权重到FasterTransformer映射与首个推理Demo4.1 为什么不能直接load原权重如果你直接用PyTorch保存的模型权重文件丢给FasterTransformer加载大概率会报错或者产生完全错误的输出。原因有两个层面。第一是参数名字不一样。PyTorch的模型里每个参数都有自己的层级路径FasterTransformer作为一个独立的C实现内部持有的是按结构组织的权重数组两者的命名规则完全对不上。第二是权重排布不一样。有些线性层的权重PyTorch里存储的顺序可能和FasterTransformer期望的顺序存在转置关系不同模块的切分方式也可能不同。权重本身没有错但读取时没有按照正确的顺序和维度去解释结果就是错乱。所以接入时需要写一个权重映射脚本把PyTorch的checkpoint读取出来逐层映射到FasterTransformer需要的结构里再按它的方式组织起来。4.2 以自回归模型为例的加载流程这里用一个典型的自回归模型结构来演示思路。整个流程分三步读取checkpoint、按映射表提取张量、灌注给FasterTransformer的模型对象。第一步从checkpoint里读出所有参数。第二步按照结构对照表逐层提取。注意提取时部分矩阵需要先做一个转置操作。第三步调用FasterTransformer接口把权重写入。import torch import numpy as np # 假设 checkpoint 已经加载好参数都在 state_dict 里 for layer_idx in range(num_layers): # PyTorch 侧 QKV 投影权重 qkv_weight state_dict[flayers.{layer_idx}.attn.qkv.weight] qkv_bias state_dict[flayers.{layer_idx}.attn.qkv.bias] # 部分结构要求转置后再传入 qkv_weight_t qkv_weight.transpose(0, 1).contiguous().numpy() # ft_model 是对应模型的FasterTransformer封装 ft_model.attention.qkv.weight[layer_idx] qkv_weight_t ft_model.attention.qkv.bias[layer_idx] qkv_bias.numpy()这段代码只是示意不同模型、不同版本的FasterTransformer接口细节会有出入。但整体思路是一致的逐个模块对齐、确认维度、必要时做转置最后再用随机输入做一次输出对拍来验证映射是否正确。4.3 跑通后的第一件事精度对拍权重重映射完千万不能直接上线。我自己的经验是先构造几个固定输入分别用原始PyTorch模型和FasterTransformer跑一遍推理比较两者的输出分布。比较的方式不只是看输出的文本是否一样。因为decode阶段的采样有随机性文本一致不能完全证明权重没问题。更可靠的方式是看logits层面的数值差异让两个模型都走贪心解码关闭所有随机采样对完全相同的输入做前向计算然后对比每个token的logits。我习惯用两个指标最大绝对误差和余弦相似度。最大绝对误差用来发现问题余弦相似度用来判断整体分布是否一致。在FP16计算下logits存在少量误差是正常的但如果余弦相似度低于0.99或者在某个位置上出现明显的数值漂移基本可以断定权重映射有地方没对齐。这时候要用二分法缩小范围先对拍某一层的中间输出定位到具体是哪一层、哪个张量发生了错位。5. 性能数据怎么看吞吐、延迟、显存与真实收益5.1 该测哪些指标才能说明问题接入完成之后做性能对比时不要只看一个数字。官方宣传里经常只给一个加速N倍但实际业务关注的点不一样如果是聊天机器人首token延迟比平均吞吐更重要如果是批量处理任务每秒生成的token数才是核心如果是长文本生成显存峰值的约束可能比延迟更致命。我自己会固定测三个指标吞吐量、平均生成延迟、显存峰值。吞吐量用每秒生成多少个token来度量代表系统在满载时的处理能力。平均生成延迟用单个token的平均耗时来度量代表用户感受到的生成速度。显存峰值决定了你在给定硬件上能开多大的batch、多长的序列。测试条件要控制变量相同的模型权重、相同的输入长度、相同的batch大小、相同的采样参数。否则任何性能对比都没有说服力。5.2 实测对比加速倍数的合理预期在我的一台普通双卡机器上用7B规模的模型对比原生PyTorch和FasterTransformerbatch为1、输入序列128、输出序列256的条件下FasterTransformer的吞吐大概是原生实现的2到3倍batch加大到8之后收益更明显吞吐可以达到原生实现的3倍以上。显存峰值也有下降因为中间buffer被复用KV Cache的分配更紧凑整体显存占用大约能降低二到四成。要特别说明这个倍数是会浮动的。模型越大、batch越大、序列越长FasterTransformer相对原生实现的优势就越明显。反过来如果batch为1、序列很短、模型又小那两者的差距可能没有想象中那么大——因为此时调度开销和显存带宽的限制没那么严重。还有一个容易被忽略的点是预热。FasterTransformer首次推理时可能会做初始化、kernel选择等准备工作第一次调用会比较慢。性能测试时一定要先跑几轮预热等稳定后再采集数据否则会把启动开销算进平均延迟里。5.3 采样参数对性能的影响很多人测性能时忽略解码参数这其实会影响对比结论。贪心解码是最快的方式每一步只选概率最大的token不需要维护多个候选。beam search需要同时维护多条候选路径计算量和显存占用会明显上升。相同batch下beam width越大KV Cache的开销就越大吞吐自然下降。top-k和top-p这类采样方式本身不改变模型的token概率分布计算流程对性能的影响很小。真正影响性能的是生成长度输出序列越长需要的decode步数越多生成同样多的请求总耗时自然越长。在性能测试结果里最好注明输入长度和输出长度否则数据没法横向对比。6. 实战踩坑FP16精度、并发调用与模型适配6.1 FP16下的精度漂移和应对FasterTransformer 默认普遍使用FP16来跑计算这是加速的重要来源之一。FP16在推理上的收益很直接显存带宽占用减半、计算吞吐翻倍。但它也有代价——数值表示范围比FP32窄。在attention计算里Q和K的点积结果可能非常大如果不对中间结果做缩放很容易超出FP16能表示的范围导致softmax之后出现非正常数值。FasterTransformer内部有缩放逻辑但在某些极端情况下还是会出现logits漂移。我的建议是接入初期先跑一轮精度对拍重点关注长序列、大数值输入的场景对比logits分布。如果发现精度偏差明显可以在权重映射时把某些敏感层保留为FP32计算或者检查是否存在数值溢出。大部分情况下只要缩放逻辑正确FP16的精度损失是可以接受的少数精度敏感的场景才需要对特定层做混合精度处理。检查精度时不要只看输出文本。两个模型输出同一个词不代表logits分布一致。用上面提到的最大绝对误差和余弦相似度两个指标才能判断数值层面是否真正对齐。6.2 并发服务化多进程和显存碎片FasterTransformer本身提供的是单模型实例的推理能力不直接解决并发服务的问题。把它接到线上服务时通常的做法是起多个进程每个进程加载一份模型权重各自持有自己的GPU上下文。但这样做显存开销会成倍增加部署时要根据显存的容量规划并发进程数。多进程方案虽然简单但收益有限。GPU上有多个计算流可以在同一个进程里用多流让不同请求在时间上重叠执行。不过这种方式的调度复杂度会上升需要自己处理队列、批管理和优先级。在长稳运行中显存碎片是个隐性问题。如果线上服务长期承载不同长度的请求显存分配回收的频率又高碎片会逐渐累积最终出现明明显存没满却分配不出大块内存的情况。频繁创建和销毁模型实例会加重这个问题。我在实际部署时的一个有效做法是把模型实例常驻进程内用请求队列做缓冲避免频繁的模型重载。6.3 新模型结构不支持时的兜底方案FasterTransformer覆盖面已经挺广了主流的一些Transformer结构都在支持列表里。但新的模型结构层出不穷总会有某些自定义结构不在原生支持范围内。遇到这种情况有两条路可以走。一条是在FasterTransformer框架里注册自定义算子把PyTorch里的自定义模块用CUDA或C重新实现。这条路工作量不小适合结构固定、改动不大的情况。另一条是对模型做结构替换把不兼容的模块替换成FasterTransformer支持的等价结构然后重新做一遍权重映射。比如把某类归一化替换成支持范围内的变体精度需要重新验证。还有一条更省力的路评估是否真的需要为了一个自定义模块牺牲整个推理引擎。如果模型整体只有一两个小模块不兼容可以试试只替换大部分主流结构再在不兼容的层上用精度对拍评估影响。如果影响在可接受范围内就不必强行造轮子。我在实际使用中的一个习惯是能不动模型结构就不动。宁可在权重映射时多花点时间也要保证模型的计算图结构和训练时完全一致。模型层面的任何改动都可能引入精度问题而这个问题的排查成本往往比写一个自定义算子的成本还高。跑了一段时间之后我的体会是FasterTransformer并不能把所有模型都无脑加速但只要你把模型结构固定、权重映射对拍做扎实它在长序列、大batch、服务化场景下的收益是非常可观的。我个人现在做新模型推理服务时会从一开始就规划好权重映射脚本的目录结构把它当成和训练脚本一样需要维护的核心代码。后续新增模型也好、换卡部署也好都能少走很多弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑