1. 推理框架与 AI 编译栈到底在解决什么问题
模型训练完之后,真正让它“跑起来”的那一层,才是决定用户体验的生死线。你手里有一个训练好的模型,可能是一个 LightGBM 回归模型、一个 DeBERTa 结构的中文分类器、一个 LSTM 时序预测网络,甚至是一个 CLIP 微调后的多模态模型——这些东西在实验室里跑通不代表能上线。上线意味着它得在某个具体设备上、在可接受的延迟和内存范围内、稳定地完成推理。推理框架和 AI 编译栈就是干这个的。
我见过太多团队在模型训练阶段投入大量精力,到了部署阶段才发现:模型在服务器上跑得好好的,一到瑞芯微 RK3568 这类边缘设备上就崩了,或者延迟从 50ms 飙到 800ms。问题往往不在模型本身,而在于中间这层“翻译”没做好。推理框架负责调度算子和内存,AI 编译栈负责把高层计算图逐步 lowering 到目标设备的指令集。两者配合,才能让模型高效映射到设备并真正跑起来。
这一层涉及的核心角色包括:推理引擎(如 ONNX Runtime、TensorRT、OpenVINO、LocalAI 推理引擎等)、图编译器(如 TVM、XLA、MLIR)、硬件后端(CPU、GPU、NPU、DSP)。不同设备的算力特性差异巨大,编译栈的策略也完全不同。下面我会从整体设计思路开始,逐步拆到实操细节。
1.1 为什么不能直接把训练框架搬上设备
很多人第一反应是:我训练用的是 PyTorch,部署也用 PyTorch 不就行了?理论上可以,但实际几乎不可行。训练框架的设计目标是灵活性和可微分性,它保留了完整的自动求导图、大量的动态调度逻辑、以及为反向传播准备的各种缓存。这些东西在推理阶段全是累赘。
举个例子,PyTorch 的 eager 模式每次执行算子都要经过 Python 解释器层,这在服务器上可能只是几毫秒的开销,但在嵌入式设备上,Python 运行时本身就可能占掉几十兆内存。更关键的是,训练框架不会针对特定硬件做算子融合、内存复用、量化压缩这些优化。你拿一个 FP32 的 BERT 模型直接往 RK3568 上塞,光模型权重就 400 多兆,NPU 的 SRAM 根本放不下,只能反复从 DDR 搬运,带宽直接成为瓶颈。
所以推理框架的第一个核心价值就是“瘦身”和“提速”:去掉训练相关的所有冗余,把计算图冻结成静态形式,然后针对目标硬件做专项优化。AI 编译栈则更进一步,它把模型的计算图当作“源代码”,通过多级中间表示逐步翻译成目标设备的机器码或专用指令。
1.2 推理框架和编译栈的分工边界
这两者的边界在实际工程中经常模糊,但理解它们的职责划分对选型很关键。推理框架更偏“运行时”,它管的是:算子库调用、内存池管理、多线程调度、批处理策略、动态 shape 处理。AI 编译栈更偏“编译期”,它管的是:计算图优化(常量折叠、算子融合、死代码消除)、布局转换(NCHW 转 NHWC 等)、量化校准、代码生成。
打个比方:推理框架像是一个餐厅的前厅经理,负责安排客人入座、点菜、上菜的顺序;AI 编译栈像是后厨的菜谱优化师,负责把复杂的菜品拆解成最高效的烹饪流程,甚至提前把食材切好配好。两者配合不好,就会出现“前厅催菜、后厨手忙脚乱”的局面。
在实际项目中,常见组合是:ONNX 作为模型交换格式,ONNX Runtime 或 TensorRT 作为推理引擎,TVM 或厂商自带的编译器做底层代码生成。比如你在 PC 上训练了一个 LightGBM 回归模型,想部署到 ARM 边缘盒子上,典型路径是先把模型转成 ONNX,然后用 ONNX Runtime 的 ARM 后端跑,或者用 TVM 编译成 ARM 汇编。如果是深度学习模型要上 NPU,那基本得走厂商提供的编译工具链,比如瑞芯微的 RKNN 工具链。
1.3 不同设备场景下的核心矛盾
设备类型决定了优化重点。我大致分三类来说:
服务器/桌面 GPU 场景:算力充足,瓶颈通常在显存带宽和批处理效率。优化重点是算子融合、混合精度(FP16/INT8)、动态批处理。TensorRT 在这个场景下几乎是默认选择,它能把卷积、BN、ReLU 融合成一个 kernel,减少显存读写。
移动端/边缘 NPU 场景:算力有限但能效比要求高,瓶颈在 SRAM 容量和算子支持度。优化重点是量化(INT8 甚至 INT4)、算子拆分与重组、内存复用。这里最头疼的是 NPU 往往只支持有限的算子集,遇到不支持的算子就得回退到 CPU,一回退性能就断崖式下跌。
MCU/超低功耗场景:内存以 KB 计,算力以 MOPS 计。这时候连推理框架都跑不动,需要专门的微型推理库,比如 TFLite Micro 或 CMSIS-NN。模型本身也得极度精简,可能只有几层全连接。
理解这些矛盾之后,才能有针对性地选择推理框架和编译策略。下面进入核心细节的拆解。
2. 核心细节解析与实操要点
2.1 模型导出与图冻结的关键操作
不管用什么推理框架,第一步都是把训练好的模型导出成一个中间格式。PyTorch 用torch.onnx.export,TensorFlow 用tf.saved_model.save或转 TFLite,LightGBM 和 XGBoost 这类树模型则通过onnxmltools或skl2onnx转换。
这里有几个容易踩的坑。第一,动态 shape 的处理。导出时如果没指定dynamic_axes,模型会被固定成某个输入尺寸,后续换 batch size 或换输入长度就会报错。第二,算子版本兼容性。PyTorch 不同版本导出的 ONNX op set 版本不同,目标推理引擎可能不支持太新的 op set。我一般建议导出时显式指定opset_version=11或13,这两个版本兼容性最好。
第三,也是最容易被忽略的:导出后的模型必须做数值一致性验证。具体做法是拿同一批输入数据,分别用原始训练框架和导出后的模型跑一遍,逐层对比输出差异。如果最大绝对误差超过 1e-4,说明导出过程有问题,可能是某个算子被近似了,或者精度从 FP32 掉到了 FP16。这个验证步骤绝对不能省,我见过太多“导出成功但结果全错”的案例。
对于树模型如 LightGBM 回归模型,导出时要注意:ONNX 对树模型的表示方式是把每棵树展开成 If-Else 节点,模型文件会比较大,但推理时可以用 ONNX Runtime 的 TreeEnsemble 优化器加速。如果目标设备不支持 ONNX,也可以直接把 LightGBM 的模型文件用 C++ 接口加载,省去转换环节。
2.2 量化策略的选择与校准
量化是推理优化的核武器,但也是最容易翻车的地方。核心思路是把 FP32 的权重和激活值映射到 INT8 甚至 INT4 的整数域,从而减少内存占用和计算量。但量化会引入精度损失,关键在于如何校准。
主流的量化方式分两种:训练后量化(PTQ)和量化感知训练(QAT)。PTQ 不需要重新训练,只需要一小批校准数据来统计激活值的分布范围,然后计算 scale 和 zero_point。QAT 则在训练阶段就模拟量化误差,让模型学会适应,精度通常更好但成本高。
我一般建议:如果模型本身对精度不敏感(比如图像分类),优先用 PTQ,校准数据 100-500 条就够了。如果是检测、分割、或者对数值精度要求高的回归任务,QAT 更稳妥。校准数据的分布必须和实际推理数据一致,否则 scale 会偏,导致某些激活值被截断。
具体操作上,ONNX Runtime 提供了quantize_static接口,TensorRT 有IInt8Calibrator,TVM 有relay.quantize。以 ONNX Runtime 为例,典型流程是:
from onnxruntime.quantization import quantize_static, CalibrationDataReader class MyCalibReader(CalibrationDataReader): def __init__(self, data): self.data = iter(data) def get_next(self): return next(self.data, None) quantize_static( model_input="model.onnx", model_output="model_int8.onnx", calibration_data_reader=MyCalibReader(calib_data), quant_format=QuantFormat.QDQ, per_channel=True )per_channel=True表示每个通道单独计算 scale,比全局 scale 精度更好,但模型会稍大。QuantFormat.QDQ是量化-反量化格式,兼容性最好。
注意:量化后的模型必须重新做精度验证。我通常要求量化后精度下降不超过 1%,如果超过就得调整校准集或改用 QAT。
2.3 算子融合与内存布局优化
算子融合是编译栈最核心的优化手段之一。原理很简单:把多个连续的小算子合并成一个大的 kernel,减少中间结果的读写。比如 Conv + BatchNorm + ReLU 这三个算子,如果不融合,需要把 Conv 的输出写回内存,再读出来做 BN,再写回,再读出来做 ReLU。融合之后,中间结果直接在寄存器或共享内存里传递,省了两次全局内存读写。
在 TVM 里,算子融合通过relay.optimize自动完成。在 TensorRT 里,构建 engine 时会自动做融合。但自动融合不是万能的,有些情况下需要手动干预。比如遇到 NPU 不支持的算子,可能需要把它拆成多个支持的算子组合,或者用 CPU 回退。
内存布局也很关键。CPU 上通常用 NCHW,GPU 上 NHWC 更友好,NPU 则各有各的偏好。布局转换本身有开销,所以最好在编译期就确定好,避免运行时反复转换。ONNX Runtime 提供了layout_optimization选项,TensorRT 会自动处理,TVM 则通过ConvertLayoutpass 来做。
2.4 设备树与驱动层的配合
模型要跑在具体设备上,离不开驱动和系统层的支持。特别是嵌入式设备,设备树(Device Tree)配置不对,NPU 或 GPU 根本识别不了。比如瑞芯微 RK3568 的设备树里,需要正确配置 NPU 的时钟、电源域、中断号,否则推理时会报“设备离线”或“设备繁忙”。
Petalinux 环境下,设备树文件通常在system-user.dtsi里修改。关键节点包括 NPU 的 compatible 字符串、reg 地址范围、interrupts 配置。改完之后要重新编译设备树并更新启动镜像。这一步如果出错,表现往往是驱动加载失败,dmesg里能看到“probe failed”之类的错误。
对于 USB 设备或外接加速棒,还要注意 USB 控制器的配置。STM32 做 USB 设备时,描述符和端点配置必须和主机端驱动匹配,否则会出现“未知设备”或“设备描述符请求失败”。这类问题排查起来很费时间,建议先用 USB 分析仪抓包确认枚举过程。
3. 实操过程与核心环节实现
3.1 从 PyTorch 到 ONNX 再到 TensorRT 的完整链路
我拿一个实际的 DeBERTa 中文分类模型举例,走一遍完整流程。这个模型有 12 层 Transformer,隐藏维度 768,参数量约 1.4 亿。目标设备是带 RTX 3060 的工控机,要求单条推理延迟低于 20ms。
第一步,导出 ONNX。关键代码如下:
import torch from transformers import AutoModelForSequenceClassification model = AutoModelForSequenceClassification.from_pretrained("deberta-chinese") model.eval() dummy_input = torch.randint(0, 30000, (1, 128)) torch.onnx.export( model, (dummy_input, torch.ones(1, 128, dtype=torch.long)), "deberta.onnx", opset_version=13, input_names=["input_ids", "attention_mask"], output_names=["logits"], dynamic_axes={ "input_ids": {0: "batch", 1: "seq"}, "attention_mask": {0: "batch", 1: "seq"}, "logits": {0: "batch"} } )导出后先用 ONNX Runtime 验证数值一致性,确认误差在 1e-5 以内。
第二步,转 TensorRT。用trtexec命令行工具:
trtexec --onnx=deberta.onnx \ --saveEngine=deberta.trt \ --fp16 \ --minShapes=input_ids:1x32,attention_mask:1x32 \ --optShapes=input_ids:8x128,attention_mask:8x128 \ --maxShapes=input_ids:32x256,attention_mask:32x256 \ --workspace=4096这里--fp16开启半精度,--workspace指定显存工作区大小(单位 MB)。min/opt/max shapes 定义了动态 shape 的范围,TensorRT 会针对 opt shape 做最优优化。
第三步,实测性能。用 TensorRT 的 Python API 加载 engine 并跑 benchmark:
import tensorrt as trt import pycuda.driver as cuda import numpy as np logger = trt.Logger(trt.Logger.WARNING) with open("deberta.trt", "rb") as f, trt.Runtime(logger) as runtime: engine = runtime.deserialize_cuda_engine(f.read()) context = engine.create_execution_context() # 分配输入输出显存,绑定,然后循环推理计时实测下来,FP32 下延迟约 45ms,FP16 下约 18ms,INT8 下约 11ms。但 INT8 的精度掉了 2.3%,超过了我的容忍阈值,所以最终选了 FP16。
3.2 边缘 NPU 部署:RK3568 + RKNN 工具链
RK3568 的 NPU 算力是 0.8 TOPS,支持 INT8 推理。部署流程和 GPU 完全不同。首先得用 RKNN-Toolkit2 把 ONNX 模型转成 RKNN 格式:
from rknn.api import RKNN rknn = RKNN() rknn.config( mean_values=[[123.675, 116.28, 103.53]], std_values=[[58.395, 57.12, 57.375]], target_platform="rk3568", quantized_dtype="asymmetric_quantized-8" ) rknn.load_onnx(model="model.onnx") rknn.build(do_quantization=True, dataset="calib.txt") rknn.export_rknn("model.rknn")calib.txt里每行是一条校准图片的路径。量化校准对精度影响极大,我一般会准备 200-500 张覆盖各种场景的图片。
转完之后在板子上用 RKNN Runtime 加载推理。这里有个关键点:RK3568 的 NPU 只支持有限的算子,如果模型里有不支持的算子(比如某些自定义的 Attention 变体),工具链会报错或者自动回退到 CPU。回退到 CPU 的算子会成为性能瓶颈,所以最好在模型设计阶段就避开这些算子。
实测一个 MobileNetV3 分类模型,RK3568 上 INT8 推理单帧约 8ms,功耗约 1.5W。如果换成 FP32 在 CPU 上跑,单帧要 60ms 以上,功耗还更高。
3.3 树模型与轻量级模型的特殊处理
不是所有模型都是深度神经网络。LightGBM 回归模型、XGBoost、随机森林这些树模型在实际业务中占比很大,它们的部署策略完全不同。
树模型的计算特点是分支多、访存随机、并行度低。ONNX Runtime 对树模型有专门的优化,叫TreeEnsemble,它把多棵树的判断逻辑展开成向量化操作。实测一个 500 棵树的 LightGBM 模型,ONNX Runtime 比原生 LightGBM 预测快 2-3 倍,因为避免了 Python 调用开销。
如果目标设备连 ONNX Runtime 都跑不动,可以直接把 LightGBM 模型编译成 C 代码。LightGBM 提供了convert_model接口,能导出成 C++ 或 Python 代码,然后交叉编译到目标平台。这种方式没有运行时依赖,但灵活性差,模型更新就得重新编译。
对于 LSTM 这类时序模型,推理时的瓶颈往往在序列长度上。如果序列很长,可以考虑用滑动窗口滤波的思路做分段推理,或者用 Longformer 这类稀疏注意力结构降低计算量。但要注意,模型结构改了之后必须重新训练或至少做微调,不能直接拿原始权重套用。
3.4 推理服务的封装与接口设计
模型跑起来之后,还得封装成服务供业务调用。最简单的做法是用 FastAPI 或 Flask 包一层 HTTP 接口,但这种方式延迟较高,不适合高并发场景。更高效的做法是用 gRPC 或者直接共享内存。
LocalAI 推理引擎提供了一个不错的参考:它把模型加载、请求队列、批处理调度都封装好了,对外暴露 OpenAI 兼容的 API。你可以直接用它加载本地模型,省去自己写服务层的功夫。但 LocalAI 对自定义模型的支持有限,如果模型结构特殊,还是得自己写。
我一般建议:如果是内部服务,用 gRPC + Protobuf,延迟能控制在 1ms 以内。如果是对外 API,用 HTTP + JSON,方便调试和集成。批处理策略上,如果请求量不大,单条推理就行;如果 QPS 高,一定要做动态批处理,把多个请求合并成一个 batch 送进模型,吞吐量能提升 3-5 倍。
4. 常见问题与排查技巧实录
4.1 模型转换失败与算子不兼容
这是最高频的问题。表现是转换工具报错,提示某个算子不支持。解决思路分三步:第一,查目标推理引擎的算子支持列表,确认是不是真的不支持。第二,如果确实不支持,看能不能用其他算子组合替代。第三,如果替代不了,考虑把这一部分留在 CPU 上执行。
比如 TensorRT 对NonMaxSuppression的支持就有限,检测模型里的 NMS 层经常需要单独处理。常见做法是把 NMS 拿出来用 CPU 实现,或者用 TensorRT 的插件机制自定义算子。
ONNX 转换时还经常遇到Unsupported op set version的错误。这时候要么降低 opset_version 重新导出,要么升级推理引擎版本。我一般优先降 opset,因为升级引擎可能引入新的兼容性问题。
4.2 精度下降与数值不一致
量化后精度下降是最常见的问题。排查步骤:首先确认校准数据分布是否和实际数据一致,这是最常见的原因。其次检查是否有某些层的激活值范围特别大,导致其他层被压缩得太厉害。可以用逐层敏感度分析找出对精度影响最大的层,对这些层保持 FP32,其余层量化。
还有一种情况是导出 ONNX 时精度就丢了。比如 PyTorch 的某些操作在 ONNX 里没有完全等价的实现,转换时会被近似。这时候需要逐层对比输出,定位到具体是哪一层出的问题。
实操心得:我习惯在导出 ONNX 后立刻跑一遍数值对比,用
np.testing.assert_allclose检查每个输出,误差超过 1e-4 就报警。这个习惯帮我省了无数次返工。
4.3 设备识别失败与驱动问题
嵌入式设备上跑推理,驱动问题占故障的一半以上。常见表现包括:NPU 设备节点不存在、驱动加载失败、推理时提示“设备离线”或“设备繁忙”。
排查顺序:先用ls /dev/确认设备节点是否创建,再用dmesg | grep -i npu看驱动加载日志。如果设备树配置不对,驱动 probe 会失败,日志里会有“failed to get resource”之类的提示。这时候需要检查设备树里的 reg、clocks、power-domains 配置。
Windows 上跑推理时,可能遇到“无法验证此设备所需的驱动程序的数字签名”的提示。这是因为驱动签名验证没通过,需要在测试模式下禁用签名强制,或者用 WHQL 签名的正式驱动。
4.4 内存不足与性能抖动
内存不足在边缘设备上非常常见。表现是推理跑到一半崩溃,或者系统开始频繁 swap 导致延迟飙升。解决方法:第一,量化模型减少内存占用。第二,用内存池预分配,避免运行时动态分配。第三,如果模型太大,考虑模型切分,把不同层放到不同设备上。
性能抖动通常和温度 throttling 有关。边缘设备散热差,跑一段时间后 CPU/GPU 降频,延迟就上去了。我一般会在设备上跑一个长时间稳定性测试,记录延迟随时间的曲线,如果发现周期性升高,基本就是散热问题。
下面整理一个常见问题速查表:
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 转换报错算子不支持 | 推理引擎算子集有限 | 查官方支持列表 | 替换算子或 CPU 回退 |
| 量化后精度掉太多 | 校准数据分布不对 | 对比校准集与实际数据 | 重新校准或改 QAT |
| 推理结果全错 | 导出时数值不一致 | 逐层对比输出 | 修正导出参数 |
| 设备节点不存在 | 设备树配置错误 | dmesg 看驱动日志 | 修正设备树 |
| 延迟周期性升高 | 温度 throttling | 监控频率与温度 | 改善散热或降频 |
| 内存不足崩溃 | 模型太大或内存泄漏 | 监控内存曲线 | 量化或内存池 |
4.5 多模型切换与资源竞争
实际业务中往往需要同时跑多个模型,比如一个检测模型加一个分类模型。这时候资源竞争就成了问题。GPU 显存有限,多个模型同时加载可能爆显存。NPU 通常不支持多模型并行,只能串行执行。
解决方案:第一,用模型池管理,按需加载和卸载。第二,如果模型之间有依赖关系,考虑融合成一个端到端模型。第三,用优先级队列调度,高优先级请求先处理。
我踩过的一个坑是:两个模型同时加载到 GPU 上,显存刚好够,但推理时因为显存碎片化导致 OOM。后来改成串行加载,用完一个卸载一个,问题就解决了。显存碎片化在长时间运行的服务里很常见,建议定期重启推理进程或者用显存池管理。
4.6 模型更新与版本管理
模型上线后总得更新。更新策略分两种:热更新和冷更新。热更新是不停服务直接替换模型,冷更新是停服务再换。热更新对用户体验好,但实现复杂,需要处理新旧模型共存、请求路由、内存回收等问题。
我的做法是:用双缓冲机制,新模型加载到另一块内存,加载完成后原子切换指针,旧模型等所有进行中的请求处理完再释放。这样切换过程对用户无感。版本管理上,每个模型文件带版本号和哈希值,加载时校验,避免加载到损坏的文件。
5. 工具选型与性能对比
5.1 主流推理框架横向对比
选型没有绝对的好坏,关键看场景匹配。下面是我实际用过的几个框架的对比:
| 框架 | 适用场景 | 优势 | 劣势 |
|---|---|---|---|
| ONNX Runtime | 跨平台通用 | 兼容性好,支持 CPU/GPU/NPU | 极致性能不如专用引擎 |
| TensorRT | NVIDIA GPU | 性能最强,融合优化好 | 只支持 NVIDIA,转换有门槛 |
| OpenVINO | Intel CPU/GPU | Intel 平台优化好 | 非 Intel 平台支持弱 |
| TVM | 自定义硬件 | 可编译到任意后端 | 学习曲线陡,调试难 |
| TFLite | 移动端/微控制器 | 轻量,生态好 | 算子支持有限 |
| RKNN | 瑞芯微 NPU | 厂商官方支持 | 只支持瑞芯微芯片 |
我的建议:如果目标设备是 NVIDIA GPU,无脑选 TensorRT。如果是 Intel 平台,OpenVINO。如果是 ARM 边缘设备,优先用厂商自带的工具链。如果都不满足,ONNX Runtime 是保底选择。
5.2 编译栈的选型考量
TVM 是最通用的编译栈,支持从多种前端(ONNX、TensorFlow、PyTorch)编译到多种后端(LLVM、CUDA、OpenCL、各种 NPU)。但 TVM 的自动调度(AutoTVM、Ansor)需要大量调优时间,对于小项目可能不划算。
MLIR 是更现代的编译基础设施,它的多层 dialect 设计让编译流程更清晰。但 MLIR 本身只是框架,要落地还得自己写 pass 和 lowering。适合有编译团队的大厂。
XLA 主要用于 TensorFlow 生态,JAX 也用它。如果你用 TensorFlow 或 JAX,XLA 是自然选择。但 XLA 对 PyTorch 的支持还在完善中。
实际项目中,我大多数时候用的是厂商工具链加 ONNX Runtime 的组合。厂商工具链负责把模型编译到 NPU,ONNX Runtime 负责 CPU 上的算子回退和整体调度。这个组合稳定、可控,出问题也容易定位。
5.3 性能调优的优先级排序
调优要有优先级,不能眉毛胡子一把抓。我的经验排序是:
第一优先:量化。INT8 相比 FP32 通常有 2-4 倍加速,而且内存占用减半。这是性价比最高的优化。
第二优先:算子融合。编译栈自动做的融合通常能带来 20-50% 的提升。如果自动融合不生效,手动调整模型结构。
第三优先:批处理。如果 QPS 高,动态批处理能把吞吐量提升数倍。但要注意延迟和吞吐的权衡。
第四优先:内存布局和线程调度。这些优化收益相对小,但在瓶颈明确时值得做。
第五优先:模型结构优化。比如用 MobileNet 替代 ResNet,用蒸馏后的学生模型替代大模型。这属于“换模型”级别的优化,成本高但收益也大。
注意:每次调优后都要重新验证精度。我见过量化后精度没掉但融合后精度掉了的情况,原因是融合改变了数值计算顺序,累积误差变了。
6. 从工程视角看推理栈的长期维护
6.1 监控与可观测性
推理服务上线后,监控是必须的。核心指标包括:延迟(P50/P95/P99)、吞吐量、错误率、内存占用、GPU/NPU 利用率。这些指标要能实时查看,并且有告警。
延迟监控要分位数,不能只看平均值。平均值 20ms 但 P99 是 500ms 的服务,用户体验是灾难性的。我一般要求 P99 不超过平均值的 3 倍。
内存监控要关注泄漏。长时间运行的服务,内存缓慢增长是常见问题。可以用定期快照对比的方式定位泄漏点。
6.2 版本兼容性与回滚
推理栈涉及多个组件:模型文件、推理引擎、驱动、固件。任何一个升级都可能引入不兼容。我的做法是:所有组件版本号记录在案,升级前先在测试环境验证,确认无误再上生产。生产环境保留上一个版本的完整备份,出问题能快速回滚。
模型文件要带元数据:训练框架版本、导出工具版本、量化参数、输入输出规格。这样出问题时能快速定位是哪个环节的问题。
6.3 成本与性能的平衡
最后说一个容易被忽略的点:成本。推理优化的目标不是无限追求低延迟,而是在满足业务需求的前提下降低成本。如果业务能接受 100ms 延迟,你花大力气优化到 10ms 就是浪费。
我一般先和业务方确认延迟和吞吐的硬性要求,然后倒推需要什么级别的硬件和优化。能用 CPU 解决的就不上 GPU,能用 INT8 的就不上 FP16。省下来的成本是实打实的。
另外,云上和边缘的成本结构不同。云上按算力付费,优化重点是降低资源占用。边缘设备是一次性投入,优化重点是降低硬件规格。理解这个差异,才能做出合理的优化决策。
这个领域变化很快,新的推理框架和编译技术层出不穷。但核心逻辑不变:理解模型的计算特性,理解设备的硬件特性,然后在两者之间找到最优的映射方式。把这条主线抓住,具体工具的选择和调优就有了方向。