☰
推理框架性能比较全解析:从算子融合到延迟与吞吐选型
2026/10/20 18:02:05 网站建设 项目流程

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 延迟和抖动幅度。很多框架的平均数很漂亮,但线上请求延迟的尾巴很长,观感很差,这类框架在服务稳定性要求高的场景就不是最优选择。

推理框架的生态还在快速演进,今天的最优解,半年后可能就被新的方案取代。保持用数据说话的习惯,比追逐任何“最好”的框架都更可靠。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询