资讯详情

资讯详情

大模型推理加速实战:从FasterTransformer原理到部署调优全指南

大模型推理一直是把“看起来能跑”变成“真正能上线”的最后一公里。模型参数量上去之后训练再快、效果再好推理卡在几秒钟一次生成上也很难落地。我第一次接触FasterTransformer 的时候手头正好有一个 GPT 风格的文本生成服务单条请求前向推理要 1.8 秒换了几轮思路都降不下来。后来把解码、采样、padding 这些逻辑往 FasterTransformer 上迁延迟直接压到 300 毫秒以内顿时觉得以前踩的坑都值了。这篇指南我从原理、编译、API 到调参和排坑完整整理一遍我自己的实操过程尤其适合那些刚接手大模型推理服务、手上有模型却不知道怎么加速的开发者。FasterTransformer 本质上是一个面向 Transformer 结构的推理加速引擎它做了大量 kernel 融合、显存复用和并行化工作支持常见的编码器、解码器架构也能跑 GPT、BERT 这类大模型。它最直接的价值就是把前向计算速度提上去同时把显存占用压下来。如果你正在做大模型推理部署或者准备给现有服务换一个更高效的推理后端又搞不清怎么入手这篇文章会按我自己的推进顺序把关键环节一个一个讲明白。1. 为什么大模型推理这么慢FasterTransformer 的加速思路拆解1.1 大模型推理的性能瓶颈到底在哪很多人以为大模型推理慢只是“计算量大”其实计算量只是一部分。我实际排查下来真正的瓶颈往往藏在几个意料之外的地方。Transformer 的每一层里都有多个矩阵乘、注意力计算、LayerNorm、残差连接和激活函数。每一层都要读一遍权重几十层叠起来权重在显存里被反复搬运这个开销非常大。另一个容易被忽略的点是核函数启动开销如果每个矩阵乘、每个激活都单独起一个 kernel几十层累计下来kernel 启动次数可能高达几千甚至上万次每一次都有固定开销。GPU 上这个开销看着小量一多就惊人。再加上自回归解码是一个 token 一个 token 来的每一步依赖上一步的结果无法直接像训练那样把整个序列并行起来K 矩阵和 V 矩阵缓存得不断更新访存比计算更吃不消。我做过一个粗略对比同样一个模型朴素 PyTorch 实现显存占用 6GB 左右换成 FasterTransformer 之后降到不到 4GB速度提升一倍多。主要原因不是 PyTorch 太慢而是它的通用性牺牲了太多对特定结构做深度融合的机会。FasterTransformer 的做法是把连续几层的计算合并到一个大的融合 kernel 里能省掉的中间张量就不落显存能复用的缓存就反复用。1.2 FasterTransformer 的核心优化手段拆解FasterTransformer 的加速手段归纳起来有四条主线。第一条是算子融合也叫 kernel fusion。典型的融合点是把 QKV 三个投影矩阵合到一起变成一个大矩阵乘这样一次拿到 Q、K、V。同理注意力的输出投影、MLP 的两个全连接层也合并处理。LayerNorm 和残差连接通常都和前面的矩阵乘算子融合在一起减少中间张量的读写。第二条是内存复用。Transformer 推理时存在很多生命周期短的中间结果FasterTransformer 用自定义的显存分配器把这些 scratch space 循环复用。比如解码时多个 batch 的中间张量可以共用同一块缓冲区。加上针对自回归解码的 KV Cache 管理让每步解码时不需要重复分配。第三条是自动调度和多级并行。它内部根据 GPU 的 SM 数量和精度格式来选择不同的 kernel 实现比如矩阵乘就有多种 tile 配置。同时在多头注意力内部把多个 head 分配到不同线程块并行计算。多 GPU 场景下还支持张量并行把矩阵拆到多卡上同步计算。第四条是低精度计算。FP16 和 BF16 是基本能力INT8 量化也在支持下。低精度不仅计算更快显存占用和带宽压力也同步降低。缩放因子scale的摆放方式在 FasterTransformer 内部都调过做量化推理时比“先训练后直接截断”的方式稳得多。1.3 谁该用 FasterTransformer适合用 FasterTransformer 的场景非常明确你的模型是 Transformer 家族的一员而且推理性能已经成了瓶颈。典型场景有几种。一种是用 GPT 风格的自回归模型做文本生成比如对话、摘要、代码补全。另一种是用 BERT 风格的编码器做语义向量提取、句子分类特别是高并发批量请求场景。再一种是在线服务要求低延迟同时还想在有限的 GPU 上塞下更大的 batch。如果你只是偶尔跑一两个模型做实验或者已经习惯用某个框架自带的优化推理接口那不一定非要上 FasterTransformer但如果延迟压不下去、显存卡在临界点它几乎是性价比最高的方案之一。我自己常用的判断标准是如果模型推理时间占总请求时间超过 60%优先考虑换推理后端如果显存占用已经让并发 batch 提不上去也要考虑针对显存优化过的推理引擎。2. 环境准备与编译部署避开我踩过的坑2.1 硬件与软件依赖清单FasterTransformer 本质上是一套 CUDA 程序所以硬件上必须以 NVIDIA GPU 为核心。显卡建议 Compute Capability 7.0 以上简单来说常见的数据中心卡和较新的消费级卡都行。显存大小决定你能跑多大的模型跑 7B 级别模型最好 16GB 起步。软件方面我最终跑通的组合是 CUDA 11.8 cuDNN 8.9 cuBLAS随 CUDA 一起装。编译时需要 CMake3.13 以上、GCC9.4 以上、Python 3.8如果要用 Python API。如果要用 TensorFlow 的插件方式那还需要对应版本的 TensorFlow不过我后来主要用 PyTorch 和自带的 C APITensorFlow 那条路径就没深入它多少有点历史包袱。还有一个容易忽略的依赖是 PyBind11。编译 Python 模块时 FasterTransformer 会通过 FetchContent 自动获取它但网络环境差的时候会卡住。最稳妥的办法是提前把 pybind11 源码准备好或者直接指定已安装的系统路径。2.2 源码编译完整步骤FasterTransformer 的源码编译走的是 CMake 这套流程。先把代码仓库 clone 下来进入根目录然后执行一条比较长的命令。我通常在根目录下做一个 release build具体命令是这样git clone --recursive https://github.com/ai-project/FasterTransformer.git cd FasterTransformer mkdir build cd build cmake -DSM80 -DCMAKE_BUILD_TYPERelease -DBUILD_PYTON -DBUILD_CUDA_EXTON .. make -j$(nproc)这里有几个参数要根据实际卡来设置。SM 是显卡的算力代号比如主流 L 卡可用 80更早一些的 V 卡用 70A 卡用 80/86/90 等多种。反过来说跑在 10 系显卡SM 60上会麻烦一些因为很多融合 kernel 需要较高算力支持。BUILD_PYTON 会生成 Python 扩展BUILD_CUDA_EXTON 是生成 C 扩展库。如果只要 Python API没必要开 C 扩展编译时间能省不少。我一开始把选项全开了光编译就花了二十分钟后来精简成只开 PYT六分钟就完事。编译完成后Python 库文件会生成在 build 下的特定目录里。需要用 pip install 指定目录包名比如pip install ./build/python然后就可以import fastertransformer了。注意直接 import 不一定成功常见坑是libth_transformer.so等动态库路径不对这时候需要设置LD_LIBRARY_PATH指向编译产物目录。2.3 容器部署的快捷方式如果不想把环境折腾一遍直接使用官方容器镜像会快很多。容器里已经预置了 CUDA、cuDNN、编译好的 FasterTransformer 库还有配套的 Python 环境。启动命令大致是这个样子docker run --gpus all -it --shm-size8g \ -v /home/me/models:/models \ fastertransformer:latest bash用容器省掉了我大量环境问题。但要提醒一句容器镜像里的 CUDA 版本和你宿主机驱动不一定兼容先跑一个nvidia-smi看看驱动支持的 CUDA 版本范围再决定拉哪个标签。镜像也不要乱改我的习惯是容器只用来跑推理代码和模型都通过 volume 挂进去避免镜像体积膨胀。如果你的 GPU 卡特别新而容器里的 CUDA 版本偏旧能用但部分新卡的性能特性发挥不出来性能调优时就有点亏。最好先确认驱动够新再使用配套 CUDA 版本较高的镜像。2.4 版本选择经验FasterTransformer 版本更新比较快每个版本可能在内部算子实现和行为上有变化。我的建议是不要盲目追最新而是看两点一是模型结构兼容性你手头的 tokenizer 或模型定义是否符合某个版本支持的层数、头数限制二是已知问题的修复情况。我实际遇到过一次小版本升级后解码束搜索beam search结果和旧版本不一致的情况排查后发现是新版改了某个搜索终止条件。如果你的线上环境跑得好好的除非有明确性能收益或 bug 修复否则不要在生产里随便升级推理库。另外源码里的 README 表格会列出不同模型的支持情况比如 GPT、BERT、BART 等。要先确认你的模型处于支持列表里再开始折腾否则后续要做很多兼容改造。3. 核心 API 与推理实操动手跑通一个 GPT 模型3.1 Python API 快速体验FasterTransformer 的 Python API 设计思路是尽量贴近 PyTorch 使用习惯但不是 100% 模拟。第一件事是确保编译出的 Python 库被正确加载我写了一个最简单的探路脚本import torch import fastertransformer from fastertransformer import TransformerTorch print(fastertransformer 版本, fastertransformer.__version__)如果这个能跑通说明环境没问题。接下来定义一个简单的 GPT 配置。假设我们要跑一个 6 层、8 头、hidden size 512 的小模型作为功能验证config { head_num: 8, size_per_head: 64, inter_size: 2048, num_layer: 6, max_seq_len: 256, vocab_size: 30000, tok_type_size: 1, layernorm_type: 0, activation_type: 2, is_gated_activation: False, has_post_decoder_layernorm: True, prefix_decoder_layernorm: True, } net TransformerTorch.from_pretrained( config, weight_dict, model_typegpt, layer_idxdecoder, )那段weight_dict是把训练好的模型权重从原始 checkpoint 转换成 FasterTransformer 所需键值格式的字典。这一步最难也最容易出错。我的经验是先运行它提供的一键转换脚本如果脚本的 export 逻辑和你的模型不完全一致就自己写一个转换函数把权重名对齐到layers.X.self_attention.query_weight这类格式。推理时输入是一个 token id 张量形状为[batch, seq_len]配合一个记录当前生成长度的数组input_ids torch.tensor([[101, 204, 305, 0, 0]]).cuda() output net(input_ids, input_lengths[3], output_seq_len512) print(output.shape) # [batch, generated_seq_len]如果只是想做个自回归文本生成更高效的方式是调用net.generate()它会一次性完成循环不需要你手动管理缓存。3.2 C API 完整示例生产环境里我更倾向 C API因为可以完全摆脱 Python 运行时降低服务内存占用也更方便和已有的 C 服务集成。FasterTransformer 的 C API 使用起来相对底层一些但逻辑清晰。一个最简 GPT 推理流程分五步第一步定义配置gpt::GptInitParameter param; param.head_num 8; param.size_per_head 64; param.inter_size 2048; param.num_layer 6; param.max_seq_len 256; param.vocab_size 30000; param.is_gpt true;第二步加载权重。权重以std::unordered_mapstd::string, Tensor格式组织key 和 Python 转换后的一致。这里需要把 checkpoint 解析成 float 数组再构建 Tensor 对象。第三步创建推理器auto gpt std::make_uniquegpt::Gpt(param, weight_map, memory_allocator, 0);第四步准备输入输出张量。输入 token id 是int32类型的 Tensor长度也是 int32 数组int input_len 3; int total_output_len 512; int batch_size 1;第五步执行推理auto output gpt-forward(input_ids, input_lengths, batch_size, total_output_len);生成的 token id 在 output 中最后一个维度是生成序列。用 C 跑通一个 6 层模型只要一上午但 debug 起来难度高一些因为很多信息需要自己打印指针和形状。建议先用 Python API 跑出参考结果再用 C API 对拍能省很多排查时间。3.3 输入输出张量与同异步推理FasterTransformer 的输入张量都有严格形状要求。输入 id 必须是[batch_size, input_seq_len]的二维 int32 张量input_lengths 是[batch_size]的 int32 数组。如果批量里各样本长度不同需要把它们 padding 到同一长度padding 部分通常用 0 或 1 补位但 attention mask 位置会被内部逻辑忽略。这里有个细节padding 的值不能等于你词表里的真实 token id不然生成结果可能被污染。FasterTransformer 也支持异步推理模式。它内部会创建独立线程你在调用后立刻拿到 future。对于高并发服务这挺有用。auto future gpt-forward_async(input_ids, ...); auto output future.get();不过异步接口在 Python API 里没有暴露太多C 层相对成熟。我的建议是 先用同步接口调通再改成异步避免一开始把异步并发和模型行为混在一起排查。4. 性能调优与显存优化实战4.1 关键开关batch_size 与 max_seq_lenFasterTransformer 的很多行为都受到 batch size 和序列长度影响。它不是完全动态适应的有些算子会按最大长度预分配显存。比如你把max_seq_len设成 2048就算实际只生成 20 个 token它也按 2048 的长度预留 KV Cache 空间。这个设计对显存很浪费。我跑 7B 模型时最初max_seq_len设为 40968 卡张量并行结果显存爆掉。后来改成动态配置按业务场景限制最大生成长度为 1024显存直接降了快 40%。如果你的服务场景是短文本对话最长只有 512就不要留 2048 的余量。当然如果业务会突发长文本可以设置几档不同的max_seq_len再配合服务端做模型实例分池。batch size 的影响更直接。FasterTransformer 在 batch 较小时会受到 kernel 启动和访存主导batch 增大后计算量增长接近线性。但 batch 太大显存里 KV Cache 的占用也会跟着涨。经验数据是用 INT8 量化 batch 32在一个 24GB 显存的卡上跑 13B 模型能勉强塞下但再大就要考虑多卡拆分或降低生成长度。4.2 量化INT8 与 FP16 怎么选量化是加速里收益最明显的一步。FP16 和 BF16 在显存占用上差不多计算延迟也差不多差别主要在数值范围。FP16 容易在权重绝对值大的层溢出BF16 精度低但指数范围宽。如果你的模型训练时用了混合精度直接用 FP16 推理问题不大如果模型权重 are 分布广优先试 BF16。INT8 能把矩阵乘带宽减半显存占用进一步下降。FasterTransformer 的 INT8 量化走的是 per-channel scale 方式相比简单的对称量化更稳。我实际测过一个 6B 模型INT8 相比 FP16 延迟降低约 30%显存占用减少约 45%生成质量在困惑度上只差不到 0.2。但注意 INT8 不是无脑上。如果你的模型本身就很小精度退化会很明显。特别是有 softmax 和 LayerNorm 的层这些一般是保留 FP32 计算的所以不会完全失真。上 INT8 之前最好用几十条典型业务样本对一下生成结果看看语义是否一致。4.3 张量并行与多卡推理单卡装不下模型时FasterTransformer 支持张量并行把每个 Transformer 层的矩阵按列或行拆到多卡上每张卡只管自己那份权重的计算。它有两种模式一种是层并行把不同层放到不同卡上串行另一种是张量并行每层在多卡上并行算卡间通过点对点通信同步。我推荐用张量并行因为对延迟更友好。比如 8 卡跑 13B 模型通常拆成 8 个通道每卡一份权重。推理时一次矩阵乘法会触发一次近似 all-reduce 的通信所以网络带宽会直接影响性能。用 NVLink 卡间通信没问题但如果是普通 PCIE 桥接延迟会高不少性能提升就不明显。配置张量并行时代码里需要在创建推理器时指定tensor_para_size一般写成卡数。权重需要按 rank 切成多份FasterTransformer 提供了一些切分脚本但对自定义模型你可能要自己理解行切列切规则。一个容易出错的点切分权重后各个 rank 上的权重矩阵 shape 要完全对齐一点偏差就会导致结果错乱或 CRASH。4.4 性能对比数据怎么测才靠谱性能测试最容易犯的错误是只用一次前向就下结论。GPU 推理引擎常常会有 warmup 行为第一次调用会做 CUDA context 初始化、kernel 加载时间非常长。一定要先跑 10 到 20 次 warmup再开始打点计时。我在项目里通常这样测固定输入长度和输出长度循环 100 次取 P50、P90、P99 延迟。统计指标时CPU 端到 GPU 端的调度时间也会算在内但不要混在一起说。 如果只关心纯推理时间就在循环外部启动 CUDA event记录 kernel 层耗时。另外对比 FasterTransformer 和其他推理方案时一定要保证以下几点一致同样的 tokenizer、同样的精度、同样的 batch 大小、同样的生成长度、同样的贪婪搜索还是束搜索。搜索方式对生成耗时影响很大比如 beam size 由 1 调到 4计算量会翻几倍如果对比时没固定数据没意义。我实测过一个小型 GPT约 350M在单卡 FP16 下的表现输入 32 token、输出 128 tokenPyTorch eager 模式大约 450msFasterTransformer 大约 180ms。换到更大模型时差距会更明显因为融合 kernel 的优势在深网络上叠加得更明显。5. 常见问题与排查技巧实录5.1 编译报错速查表编译阶段是最容易卡住新手的地方我把高频问题整理成一个速查表。错误现象根因我的解法cublas相关符号找不到CUDA 与 cuBLAS 版本不匹配或编译时没有链接到正确库清理 build 目录重新执行 cmake指定-DCUDNN_ROOT、-DcuBLAS_ROOT保证 CMake 找到的 CUDA 版本和nvidia-smi一致pybind11拉取失败网络访问不到 Git 仓库提前下载 pybind11 源码放到third_party目录或设置PYBIND11_PYTHON_VERSIONSM算力错误未设置或设置过低用deviceQuery查实际算力不可直接写 90 给旧卡编译 OOM内存不足并行任务太多使用make -j4减少并发修改 CMake 里优化等级为O2有时O3会让某些文件内存暴涨xxx.cuh文件缺失子模块未拉取git submodule update --init --recursive一个额外的心得推荐使用ccache。重编译时改动并不多但完整 rebuild 特别耗时ccache 能把二次构建时间压缩大半。5.2 运行期显存不足怎么处理显存不足是使用大模型推理时最常见的运行时错误不一定是你总显存不够往往是分配策略太粗放。第一步先看nvidia-smi确认显存实际分配情况。如果显存占用稳步上升说明分配器有内存泄漏检查一下你是否循环创建推理器实例。FasterTransformer 的推理器实例在初始化时会分配不少显存如果在线服务里频繁重建积累后必爆。正确做法是常驻实例通过队列控制并发。第二步看max_seq_len是不是过大。前面说的 KV Cache 预分配问题常导致显存比实际需要高出一大截。我遇到过一个案例跑 7B 模型需求是输出 256 token但配置里max_seq_len1024显存一直 22GB 以上。调成 256 后直接降到 14GB。第三步考虑量化。INT8 通常能降低接近一半权重显存如果你的卡刚好差一点可以先从量化入手。如果还是不行再上张量并行隔拆分。5.3 结果精度异常的原因生成结果和原模型不一致通常先排查权重转换。权重转换脚本对不上是最常见原因。比如有些模型的 QKV 矩阵是拼接在一起的转换时拆分的顺序错了表面上看不出什么问题但生成结果完全不对。另一种情况是归一化顺序不同。 不同训练框架的 LayerNorm 实现细节有差异比如 epsilon 的大小或者是否把 bias 合并到权重里。FasterTransformer 是高性能实现默认行为是某些优化后的数学等价变换但当浮点精度不够时微小差异会被放大。解决办法是在转换脚本里显式设置layernorm_type0或1并对齐 epsilon。还有 beam search 的问题。fastertransformer 的 beam search 和原框架的 beam 搜索在分数归一化方式上可能不同导致选择结果不同。如果你做的是测试先用贪婪搜索不用 beam这样最容易对拍。等贪婪搜索结果完全一致再开 beam。5.4 部署到生产环境的注意事项把推理引擎从实验环境搬到生产多了不少坑。首先是 CUDA context 初始化。推理库第一次加载时会做非常耗时初始化如果服务框架是懒加载首批请求会特别慢。我建议服务启动时做一次热身推理让所有模块先跑一遍。其次是并发模型。FasterTransformer 的 C API 本身不线程安全如果你对同一个推理器实例并发调用可能崩溃。要么每个线程创建独立实例但是显存开销成倍增加要么用推理器内部的请求队列配合互斥锁或 channel 把请求串行化吞吐可能损失一些但稳定性更好。我在实际项目里采用后一种方式把一个实例的并发请求串成流水线配合异步接口吞吐也能到每秒几十个请求。最后是权重的热更新。如果模型迭代频繁别在推理器运行中直接替换内存里的权重容易造成未定义行为。正确做法是用备份实例做切换先加载新权重再原子替换旧实例。FasterTransformer 没有内置热更新机制所以服务层要自己管理版本切换。我在实际项目中还发现一个小技巧显存够用但不稳定时可以同时创建两个推理实例一个主用、一个备用运行时把请求轮询到两个实例这样单个实例 OOM 时另一个能顶上不至于把整个服务拖垮。最后再分享一点个人体会。FasterTransformer 不是一个“装上就自动变快”的黑箱真正发挥出它的性能需要你对模型结构、权重布局甚至 GPU 计算特性都有一定了解。第一次调它可能因为权重转换反复折腾好几天但只要有一次完整的接入经验之后再接新模型会顺畅很多。不管是用 Python 快速验证效果还是用 C 做生产加速它都是目前大模型推理加速里非常实在的工具。希望这篇指南能帮你少走我走过的弯路。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →