推理框架性能比较全解析:从算子融合到延迟与吞吐选型
1. 从一次模型上线事故说起推理框架选型为什么是个难题我印象很深的一次经历是某团队把训练好的检测模型部署到线上服务器选了一个当时看起来很稳妥的推理框架。结果上线当天单路延迟直接飙到 80 毫秒以上GPU 利用率却只有 20% 出头显存占用倒是拉满了。业务方催着优化算法团队说“模型在训练机上测过没问题”运维团队说“框架参数全按默认来的没动过”。两边都没犯错但整体性能就是上不去。最后折腾了两天换了一个推理框架同一份模型、同一台机器延迟降到了 20 毫秒以内。从那以后我就养成了一个习惯任何模型要上线之前先做一轮推理框架的性能比较分析。这不是可做可不做的优化而是部署环节里最容易被低估、又最容易翻车的一步。很多人在模型训练阶段反复调精度觉得模型准了上线就稳了但推理框架选得不对模型再准也会因为延迟超标、吞吐不足被否掉。这篇文章我想把推理框架性能比较这件事拆开讲透。核心讨论几个问题推理框架的性能差异到底来自哪里怎么设计一套公平可复现的基准测试流程框架选型时要盯哪些关键指标以及我在实际部署中踩过哪些坑、后来怎么排查的。适合三类人看一是刚接触模型部署、想系统了解推理框架差异的算法工程师二是负责推理服务性能优化、需要选型依据的工程团队三是正在做项目方案评审、需要对推理性能有客观判断的技术负责人。2. 性能差异的本质推理框架到底在优化什么很多人的第一反应是推理框架不就是把模型文件加载进来然后跑一遍前向计算吗同样的模型、同样的输入不同框架跑出来的结果能差多少实测下来差异大得惊人。同一份模型文件在 CPU 上跑最快的框架和最慢的框架可以差出两到三倍在 GPU 上跑差距也经常超过 50%。这个差距不是玄学而是推理框架在以下几个层面的实现差异决定的。2.1 算子融合少吃几趟内存就能快一大截推理计算有个特别反直觉的特点真正做数学运算的时间其实不多大头都花在数据搬运上。数据要从内存搬到寄存器算完再搬出去这中间的内存访问开销在深度学习早期是没人管的每个算子独立完成“读数据、算、写结果”哪怕两个算子是前后接着的也各自走一遍完整流程。推理框架做的第一件大事就是把相邻的算子融合成一个整体。以卷积后面接激活函数为例传统的执行方式是卷积层算完把结果写回内存激活函数再把结果读出来。算子融合之后卷积算完一个元素立刻就在寄存器里做激活根本不需要写回再读出来。省掉的这趟内存读写在几十层上百层的模型里累计起来就是几倍的性能差距。我在测试中发现算子融合对轻量级模型的影响尤其明显。比如那种只有几十层的小模型本来计算量就不大内存搬运占的时间比例更高融合优化做得好不好直接决定框架排第几。重型模型因为计算本身时间长把内存搬运的时间摊薄了融合优化的优势就不那么显眼。2.2 内存布局与线程调度硬件底子要用对第二个层面的差异是框架对底层硬件的利用方式。先拿内存布局来说NCHW 和 NHWC 这两种常见的张量排布方式在不同硬件上的表现差异很大有的框架默认只支持其中一种如果模型传进去的格式恰好跟硬件擅长的不匹配性能就会打折扣。有些框架会在加载模型时做格式重排有些框架干脆让用户自己负责选错了帧格式性能差距立刻就能体现出来。线程调度这块就更微妙了。CPU 推理的极限性能很大程度上取决于线程绑定方式。默认情况下操作系统会自由调度线程线程可能会被分配到不同的物理核心上缓存利用率就很差。好的推理框架会自己管理线程池做 CPU 亲和性绑定让核心线程固定在某几个物理内核上。我做过一组对照测试同一台机器开启了线程绑定的框架和不开启的相比多核推理的吞吐量稳定高出 30% 上下。GPU 那边的差异集中在算子的实现方式上。有没有用对 CUDA 核函数来启动配置张量核心用了没有显存是怎么复用的这些都直接影响实际吞吐。很多框架虽然标称支持 GPU但实际用的还是老旧的计算路径跑出来的性能自然是天差地别。2.3 量化策略与内存复用同一个精度标准的差距也很大量化的逻辑也不仅仅是“INT8 一定比 FP32 快”这么简单。同样是 INT8 量化校准数据集选得不一样对每层激活值的动态范围估算就不一样推理时为了保精度框架内部可能插入了更多的重量化节点。稍微上一点规模就会发现不同框架对量化模型的支持程度不同有的框架只能做简单的逐张量量化有的框架能做逐通道量化后者的精度损失更小但内部实现更复杂推理速度未必更快。这里面的取舍就落在“围绕你的实际部署场景去测试”这句话上了。内存复用也是一个容易被忽视的环节。推理框架在申请显存和内存的时候像仓库管理员管货架有的框架每批数据来了就重新堆货有的框架提前规划好每层的临时缓冲区。重新申请的成本在单次推理里看不出来但并发量上来之后内存分分配释放就会变成明显的瓶颈。3. 先跑通一套公平的基准测试流程再谈比较结论框架之间的性能比较最怕的就是“不公平”。我在实际操作中见过太多种不公平的测法有的只在某一款特定的硬件上做了单帧测试有的训练时用的优化手段和推理时不一致最普遍的一个问题是测的时候用了不同的输入尺寸和批处理大小。不同框架对不同输入尺寸的敏感度不一样A框架在224224输入下表现好换到512512可能就掉队了B框架在单请求下表现平平并发50路之后反而反超。这种情况下得出的“谁快谁慢”的结论是没法用的。3.1 先把“测什么”和“怎么测”固化下来我的习惯是做框架比较前先写出一份测试方案把下面这些变量固定住只留“框架”作为唯一的变量。第一是硬件环境。CPU 型号、核数、是否开启超线程需要固定GPU 型号、驱动版本、CUDA 版本、深度学习库的版本都要记录清楚。这里有个很容易忽略的点驱动版本不同同一个框架在同一个 GPU 上的推理性能能差出 10%。别小看这个数字在性能比较的场景里这个误差级别足以颠倒排名。第二是模型集合。不要只拿一个模型测意义太小。我建议起码准备三个不同风格的模型一个轻量级的分类模型比如 MobileNet 这类用来测框架对小型网络的优化能力一个中等规模的检测模型这类模型算子类型比较丰富能反映出框架对各种算子的支持程度再一个就是 Transformer 类的模型这类模型结构跟传统卷积网络差异大对框架的算子融合策略是个很好的考验。3.2 用可重复的脚本跑测试手动操作不靠谱比较测试必须是脚本化的不能靠人手动点。手动操作的问题不仅仅是效率低更关键的是你很难保证每次的输入数据一致、预热轮数一致、测试轮数一致。我习惯用一个脚本把整个流程串起来基本逻辑是这样的# 伪代码示例只是说明测试流程骨架 for framework in [框架A, 框架B, 框架C, 框架D]: for input_size in [224, 512, 1024]: for batch_size in [1, 4, 16]: warmup(framework, input_size, batch_size) # 预热让缓存和显存都热起来 latency, throughput benchmark(framework, input_size, batch_size) 记录结果到 CSV 文件预热这一步特别关键。模型第一次推理时框架内部要做很多初始化工作内存池建立、模型解析、算子选择、缓存生成。这些工作本质上是运行时初始化的一部分不是真正的推理计算。如果不预热直接测测出来的延迟会明显偏高而且不同框架的初始化开销还不一样有的框架第一次推理要花好几秒有时候甚至感觉像“卡死”了预热后就能恢复正常水平。我的建议是正式测试前先跑 10 到 20 轮预热让框架完成所有初始化工作然后再开始测正式的延迟数据。每轮测试之间还应该做一些清理工作比如清空 GPU 显存缓存避免上一轮测试的残留数据影响下一轮。3.3 指标不是越多越好要抓住关键的两个维度推理性能的指标不管框架怎么宣传最终落到应用中本质上就是两个维度延迟和吞吐。延迟看的是输入端到输出端的时间跟用户请求的响应速度直接相关。互联网产品的接口调用场景对延迟高度敏感举个例子如果业务要求 P99 延迟低于 30 毫秒那你测试时要关注的就不只是平均延迟P90、P95、P99 这几个分位数的稳定性比平均值更重要。有些框架平均延迟很好看但偶尔会有几个特别慢的请求这种抖动在线上服务里是非常难处理的。吞吐看的是单位时间能处理多少请求跟系统整体的服务能力直接相关对离线批处理场景至关重要。比如你要做一批图片的批量识别单张慢一点没关系但总处理时间必须可控此时吞吐量就是主要指标。不同框架在不同指标上的强弱可能完全不同。我见过很典型的对比结果框架 A 单请求延迟很低但一旦并发上来吞吐就遇到瓶颈框架 B 单请求延迟稍高但并发压上去之后吞吐反超。这个差异本质上是线程调度策略和内存管理策略不同带来的。所以做对比分析时延迟和吞吐这对维度必须同时测同时看缺失任何一个维度的比较都是不完整的。4. 主流推理框架的定位差异没有绝对的好只有适不适合现在市面上的推理框架数量相当可观风格和定位也很不一样。如果让我按部署场景去给它们分类的话大致可以分成这几路。4.1 跨平台移动端框架核心优势就在小而快跑在手机和嵌入式设备上的框架性能追求有两个方向一是模型体积和内存占用要足够小小到不会挤占应用自身的内存空间二是推理速度要足够快快到用户无感知。这些框架通常对 ARM 架构做了深度优化能在不牺牲太多精度的前提下把延迟压到很低。这一派的典型代表有几个。TNN 在移动端的算子优化做得相当深入对 NPU 等异构硬件的适配也做得比较完善在手机芯片上的性能表现稳定。MNN 的优点是架构清晰、内置了大量对轻量模型的优化模型转换工具链也比较完整对 ARM 平台的支持力度很大。NCNN 则是最早一批开源的移动端推理框架之一至今仍在持续迭代特色是极致轻量、精简单文件即可集成。我拿同一个检测模型在这几个框架上做过对比在骁龙平台上TNN 的延迟最低MNN 紧随其后NCNN 也维持在可用范围内。但换到麒麟平台排序就发生了变化MNN 反超成了第一。这个例子想说明的很简单移动端框架的实测性能跟硬件平台强绑定脱离具体芯片讨论“哪个框架更快”是没有意义的。如果要做移动端项目最好拿着你的目标机型清单挨个测不要只看框架官网上的宣传数据。4.2 云端服务框架吃的是延迟和吞吐跑在服务器上的框架优化目标是 CPU 上的高吞吐、GPU 上的低延迟、以及批处理下的高利用率。这类框架通常对 NVIDIA GPU 做了深度适配有的直接调用深度学习加速库把底层优化做到了极致在服务端场景下有着传统架构不可替代的地位。这一派里绕不开的一个是 TensorRT它的核心技术思路是“构建引擎”而非“逐算子解释执行”加载模型后会针对当前硬件环境做层级的融合、精度校准、显存复用规划最终产出一个高度定制的序列化引擎文件。这个引擎天生就跟“推理框架”这个字面含义不完全一样——它更接近于“把模型编译成专属硬件的机器码”。代价是不够灵活模型结构一旦变化就得重新构建引擎。另一个重要代表是 OpenVINO它跟 TensorRT 的思路有相似之处同样强调在特定硬件上的极致优化。OpenVINO 对 Intel 平台的优化尤其深入如果你的服务器是 Intel CPU加速效果非常突出。但它也面临类似的灵活性问题模型支持列表之外的结构就需要额外的转换工程。还有一类新兴的“下一代”推理框架比如 Rust 语言写的 WasmEdge 这类重点关注云原生和边缘结合的场景引入了多线程调度和显存优化方面的现代设计。这类框架的优势是部署形态轻便不用为每一类硬件单独编译但生态成熟度和算子覆盖度跟老牌框架相比还有差距落地前需要做更严格的验证。4.3 传统框架与自研框架稳定性与灵活性的取舍除了专门为推理设计的框架还有一种选择是继续用训练时那套深度学习库直接做推理。比如 PyTorch 也提供了推理部署的路径配合 TorchScript 可以做一定程度的性能优化。这种路线的优势在于成本低、上手快很多时候不需要额外做模型格式转换训练完就能直接部署。但代价是性能上限不高在极致的优化上跟专门的推理框架有明显差距。我做过测试同一个模型PyTorch 直接推理和 ONNX Runtime 推理性能差距可以达到 30%再跟 TensorRT 比差距更大。所以训练框架直接部署更适合原型验证、流量较小的场景不适合作为大规模线上服务的基础方案。有些大厂会选择在自己的业务场景基础上自研推理框架这种做法通常是因为通用框架解决不了特定业务里那些极端复杂的模型结构或性能需求。自研推理框架的好处是能跟业务深度绑定把优化做到极致代价是持续的研发维护成本很高如果不是团队规模大到可以长期养一个基础架构小组一般团队选择成熟的开放方案更稳妥。5. 实操对比测试实录一个具体案例的拆解说这么多我想给一个我实际做过的对比测试案例方便你看清楚完整的测试流程和结论推导过程。这次测试选的场景是一个图像分类服务目标平台是服务器 CPU没有配置 GPU模型规模在轻量到中等之间对延迟和吞吐都有要求。5.1 测试环境和模型配置硬件环境是双路 CPU 服务器每颗 CPU 16 核 32 线程内存 64GB。操作系统是 Ubuntu 20.04所有框架都用了当时的稳定版本。为了排除编译环境的干扰所有框架都预编译好二进制版本不现场进行编译。模型方面我选了三个有代表性的MobileNetV2 作为轻量模型的代表ResNet50 作为中等模型的代表以及一个小型 Transformer 分类模型用来考察框架对新结构的适配能力。数据全部用固定随机种子生成的模拟图片数据不涉及真实样本图床保证输入完全一致。这里有两个容易被忽视的细节。一是所有框架的输入预处理必须保持一致Resize 的参数、归一化的均值和方差、数据排布的格式有一处不一致测出来的性能就不能直接横向对比因为你实际上测的是“框架预处理”的组合。二是所有框架使用相同的线程数设置比如统一配置为 8 线程避免因为线程数不同导致成绩差异那样测出来的数据根本无法归因。5.2 延迟测试的结果与分析单请求延迟测试的结论是在 MobileNetV2 和 ResNet50 这两个卷积网络上TensorRT 的优化效果最明显比 ONNX Runtime 快约 40%OpenVINO 紧随其后差距不大。这符合预期因为 TensorRT 对卷积结构的融合策略非常成熟。但值得注意的是在 Transformer 分类模型上TensorRT 的优势幅度收窄了。原因也很简单Transformer 的核心结构是矩阵乘法和层归一化这两类算子的融合空间不如卷积网络那么大TensorRT 的“杀手锏”在这个模型上发挥不出来。这个案例说明了一个容易被忽略的问题框架的优化优势往往和模型结构强相关。你不能因为某个框架在你的检测模型上表现好就默认它在另一个 Transformer 模型上也一样好。5.3 吞吐测试的结果与分析并发场景下的测试则呈现了完全不同的格局。在并发 16 路的压力下OpenVINO 的吞吐表现超过了 TensorRT 和 ONNX Runtime。原因在于 OpenVINO 在多核 CPU 的负载均衡和线程调度上做得很到位并发上来之后多个请求能在多个核心之间比较均匀地分配。TensorRT 在这个环节的表现则比较有意思单请求延迟低但并发吞吐没有拉开绝对优势。这说明它的优化重心更偏向“把单个请求跑得快”而不是“把一批请求安排得明明白白”。如果业务的主要模式是串行执行大量离线任务这个特点没有问题但如果是高并发在线服务就需要仔细评估这个短板。这一轮完整的测试跑完我最后给出的建议是根据业务场景来区分是“低延迟优先”还是“高吞吐优先”再决定框架的选型而不是笼统地说“哪个框架性能最好”。6. 影响模型部署实战体验的几个隐藏变量按上一节的对比测试选定框架之后真正把服务上线时还有一些隐藏变量会在实际运行中显著影响性能。这些变量在基准测试阶段不容易暴露但到了线上往往是性能瓶颈的根源。6.1 动态形状与输入尺寸的统一是性能杀手很多现成框架默认支持动态输入尺寸也就是说你不用提前固定模型输入的高度和宽度用户发来一张任意尺寸的图都能跑。这种灵活性看起来很美好但代价是框架无法提前规划内存池、无法充分编译器结构优化。我处理过一个线上问题服务调用的图片尺寸不统一有 320x320 的、有 640x640 的模型本身是动态输入。因为输入尺寸一直变化框架的内存池频繁重建GPU 利用率一直上不去。改成固定输入尺寸 640x640图片先做等比缩放与填充吞吐提升了 60%。这个改动本身也不复杂损失的一点灵活性换来的性能提升非常可观。提示线上服务如果对输入尺寸没有强需求强烈建议固定输入尺寸这是性价比最高的优化项之一。6.2 CPU 核心绑定与线程池参数需要手动调推理框架的默认线程配置通常是为了“通用性”设计的它要兼容各种机器、各种负载模型。但默认参数往往不是你这个场景的最佳参数。线程开得太少CPU 多核浪费线程开得太多线程切换开销会拖慢整体速度。我建议在性能测试阶段就把线程数作为一个变量跑一遍曲线而不是直接用默认值。CPU 亲和性绑定这个操作也值得重视。操作系统默认的线程调度是“谁空闲谁上”这会导致线程在不同核心之间迁移缓存命中率下降。手动把工作线程绑定到固定的物理核心上能明显减少缓存失效带来的性能损耗。在 Linux 上这类的操作也很成熟工具链支持程度很高调配起来也比较方便。6.3 GPU 推理还有两个容易被忽视的细节用 GPU 推理时只关注计算时间是不够的。CPU 和 GPU 之间的数据拷贝往往是真正的时间瓶颈。如果你在每次请求时都走一遍“CPU 预处理 - 传到 GPU - 推理 - 传回 CPU - 后处理”的过程数据传输开销占比会非常高。解决思路有两个方向。一是隐藏传输延迟用 CUDA 流把数据拷贝和计算重叠起来CPU 在准备下一批输入数据的同时GPU 在算当前这批流水线跑起来之后有效吞吐能提升一大截。二是减少传输次数把前处理也尽量放到 GPU 上去做比如图像的 Resize 和归一化直接用 GPU 算子完成CPU 只需要把原始图像数据一次性传给 GPU。另外一个容易踩的坑是框架的显存分配策略。有些框架会默认在加载模型时把所有显存都占满看起来“更主动”但在显存小的卡上会直接导致申请失败或者在多模型共存的场景下互相挤占。实际部署时需要手动限制显存占用比例让框架和业务其他模块共存。7. 常见问题排查实录上线前这五个问题一定要提前想清楚在实际部署过程中我整理了一些出现频率特别高的问题和对应的排查思路。这些问题看着都不复杂但每个都可能导致性能波动甚至服务不可用。7.1 同一个模型为什么不同框架的推理结果不完全一致这是刚接触推理框架的人最常问的问题。同一个 checkpoint 转换到不同框架输出结果经常有极小的差异比如某个分类概率从一个 0.8671 变成了 0.8672。这是因为不同框架的算子实现细节不同浮点运算的舍入顺序不一样可能是算子融合改变了计算顺序也可能是底层数学库的算法不同。这属于正常现象。只要最终的输出分布没有明显偏移分类阈值判定没有产生实质性的不同结果就可以接受。如果差异明显优先检查模型转换过程比如算子是否被错误映射以及输入预处理是否保持一致。7.2 预热之后还是慢先查 CPU 频率而不是框架我遇到过一种非常迷惑的现象同一个框架同一个模型在性能测试机上跑得好好的部署到生产机器上性能直接减半。折腾了半天最后发现问题是生产服务器的 CPU 频率被限制在了一个较低的档位可能是 BIOS 设置或系统节能策略导致的。排查这个问题的方法很简单看运行推理时的 CPU 实时频率。如果频率明显低于 CPU 标称的主频需要检查电源管理策略、散热状态。很多机器的默认策略是“平衡模式”推理负载上来不会主动拉高频率改成“高性能模式”之后性能立竿见影地提升。7.3 并发一上来性能就崩先确认瓶颈是计算还是 I/O并发场景下性能下降明显很多人的第一反应是“框架不行”但框架经常替 I/O 背锅。并发请求达到一定规模之后瓶颈可能出现在网络传输、磁盘读取或数据序列化逻辑上推理计算只占了很小的时间比例。排查方法是拆解耗时分布把请求处理的各个阶段分别打点看看时间究竟消耗在哪个环节。如果推理计算只占 30%网络 I/O 占 50%那换什么推理框架都解决不了根本问题优化应该优先做网络层的改造、批量读取、数据缓存。7.4 显存占用异常增高大多数情况是内存池没有复用加载模型之后观察显存占用发现比模型文件大小高出很多倍很多人以为模型解析出了问题。其实多数情况是框架在运行时建立了大量内存池为各种中间计算结果预留空间这是一种“空间换时间”的策略。要控制显存占用可以限制框架的内存池上限或者调整内存分配策略。7.5 模型转换失败不一定是框架的问题模型从训练框架转换到推理框架时经常出现算子不支持的报错。这种时候先不要怀疑模型结构先用框架自带的算子支持列表逐一比对看报错的算子是否有替代方案。这些年框架的算子覆盖度已经越来越全面大多数模型结构都能找到映射路径关键是要多看转换日志分析具体是哪个算子出了问题。有些复杂算子可能需要用等价算子的组合去替代这也就需要你对模型结构有足够的理解。8. 聊点实际的我做推理框架选型的体会框架性能要放在具体场景里去评估。脱离业务谈框架优劣就像脱离路况谈汽车性能一样不现实。在 GPU 上不发力的框架可能在 CPU 上表现特别好在移动端延迟极低的框架在服务端可能就没有优势可言。认清这一点是做好推理框架性能比较的前提。我自己的选型流程已经基本固化成了一套组合拳第一步明确业务的核心指标是延迟还是吞吐定好量化目标第二步准备包含不同结构特性的模型集合第三步在固定硬件环境上跑基准测试覆盖单请求和并发两种模式第四步不仅测端到端延迟还要看分位数和资源利用率第五步结合团队维护能力和模型更新频率做最终选型。整个流程大概需要一到两周但换来的是上线后的稳定性能和可控的资源成本。最后分享两个小技巧。第一个任何框架的官方 benchmark 都只能当参考不能当决策依据因为他们的测试环境和你的生产环境差异太大拿到的数字对你有多少参考价值要客观判断。第二个测框架性能时不要只看平均延迟一定要看 P99 延迟和抖动幅度。很多框架的平均数很漂亮但线上请求延迟的尾巴很长观感很差这类框架在服务稳定性要求高的场景就不是最优选择。推理框架的生态还在快速演进今天的最优解半年后可能就被新的方案取代。保持用数据说话的习惯比追逐任何“最好”的框架都更可靠。