TensorRT 和 ONNX Runtime 推理优化实战:10 个降低延迟的工程技巧

简介: 模型性能优化关键在于细节:固定输入形状、预热、I/O绑定、精度量化、图优化与CUDA Graph等小技巧,无需重构代码即可显著降低延迟。结合ONNX Runtime与TensorRT最佳实践,每个环节节省几毫秒,累积提升用户体验。生产环境实测有效,低延迟从此有据可依。

模型速度的瓶颈往往不在算法本身。几毫秒的优化累积起来就能让用户感受到明显的性能提升。下面这些技术都是在生产环境跑出来的经验,不需要重构代码实施起来也相对简单并且效果显著。

固定输入形状,越早告诉运行时越好

动态形状用起来方便但对性能不友好。TensorRT 和 ONNX Runtime 在处理固定形状时能做更激进的优化。

TensorRT 这边,构建引擎时最好围绕实际使用的 min/opt/max 设置 optimization profile,生产环境尽量让所有请求都落在 opt 范围。ONNX Runtime 可以直接导出固定维度的模型,比如 1×3×224×224。确实需要动态性的话,也要把范围控制得足够紧。

 # TensorRT: build with a tight optimization profile  
 profile = builder.create_optimization_profile()  
 profile.set_shape("input", (1,3,224,224), (1,3,224,224), (1,3,224,224))  
 config.add_optimization_profile(profile)

这样kernel 选择、内存规划、算子融合在形状确定的情况下都能做得更彻底。

启动前把该热的都热一遍

冷启动的开销藏在各个角落:驱动初始化、page fault、lazy allocation。服务启动和重启后跑几轮 warmup,把这些一次性成本提前消化掉。

 # ONNX Runtime warmup + pinned buffers  
import onnxruntime as ort, numpy as np  

so = ort.SessionOptions()  
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL  
sess = ort.InferenceSession("model.onnx", sess_options=so, providers=["CUDAExecutionProvider"])  

x = np.random.randn(1,3,224,224).astype(np.float32)  
for _ in range(8):  # small loop to populate caches/contexts  
     sess.run(None, {"input": x})

warmup 的形状一定要和线上一模一样。如果服务多种 batch size,每个都得过一遍。

I/O binding 配合 pinned memory 减少拷贝

Host 和 device 之间来回搬数据是 tail latency 的大敌。buffer 绑定一次,后面反复用就行了。

 # ONNX Runtime I/O binding example  
io = sess.io_binding()  
x = np.random.randn(1,3,224,224).astype(np.float32)  

# Upload once & bind  
io.bind_cpu_input("input", x)       # or bind to CUDA device via OrtValue  
io.bind_output("logits", device_type="cuda")  

sess.run_with_iobinding(io)  
 out = io.copy_outputs_to_cpu()[0]   # pull back only when you must

GPU 流量大的场景,host 端内存用 page-locked(pinned)能让 H2D/D2H 传输快不少。本质上是把多次小开销合并成一次前置成本,allocator 也不用频繁介入。

精度降低不一定掉点

现在的 GPU 对 FP16 支持很好,服务器 CPU 和 NPU 上 INT8 的收益也越来越明显。只要精度守得住,延迟的改善非常直接。

TensorRT 开 FP16 就是一个 flag 的设置:

config.set_flag(trt.BuilderFlag.FP16)

。但是INT8 需要校准,要拿代表性数据跑一遍生成 per-channel scale。ONNX Runtime 可以用 TensorRT EP 或者直接加载量化后的模型。

 # TensorRT FP16 (and INT8 if you have a calibrator)  
 config.set_flag(trt.BuilderFlag.FP16)  
 # config.set_flag(trt.BuilderFlag.INT8)  
 # config.int8_calibrator = calibrator

这里可以先量化最慢的几个子图,比如 embedding 层或者 attention block,不用一上来就全模型量化。

图优化可以开到最高档,但要验证数值

让运行时自己去融合算子、选更优的 kernel,这个收益基本是白来的。

 # ONNX Runtime optimizations + EPs  
so = ort.SessionOptions()  
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL  
providers = [  
  ("TensorrtExecutionProvider", {"trt_fp16_enable": True}),  
  "CUDAExecutionProvider",  
  "CPUExecutionProvider"  
]  
 sess = ort.InferenceSession("model.onnx", sess_options=so, providers=providers)

但是需要注意的是融合会改变计算顺序,数值可能有细微漂移。开启前后要跑个 tolerance check,确保输出没问题。

micro-batch 在 GPU 上效果明显

单条请求跑推理简单,但硬件利用率往往上不去。打包成 4-8 个请求一起跑,能在保持低延迟的同时提升吞吐。

关键是 batching window 要够小,比如 2-5ms,不然 p95 会飙。micro-batch 的大小最好和前面 optimization profile 设置的尺寸对齐。

不过如果 SLA 本身就很紧(p50 要求 5ms 以内),micro-batch 带来的收益可能不如下面要说的 CUDA Graph。

CUDA Graph 消除 kernel launch 开销

小模型或者调用频繁的 graph,kernel launch 的开销会很明显。CUDA Graph 能把整个推理过程录制下来,replay 时几乎没有 CPU 开销。

TensorRT 在形状固定的情况下可以直接启用,只需要warmup 一次,后面就一直跑 captured graph。

这里可以理解成在 GPU driver 层面把推理编译成一个可重放的宏。

ONNX Runtime 线程设置有讲究

ONNX Runtime 暴露了几个线程相关的参数,对 CPU 和混合负载的 tail latency 影响挺大。

 so = ort.SessionOptions()  
 so.intra_op_num_threads = 1   # one thread per operator often stabilizes latency  
 so.inter_op_num_threads = 1   # avoid oversubscription; raise carefully if parallel graphs

Execution Provider 的选择也很重要:

GPU 场景优先级是 TensorRT EP → CUDA EP → CPU EP fallback。纯 CPU 跑的话 OpenMP 或者 DNNL/MKL build 配合合理的线程池设置效果最好。边缘设备上 Intel 的盒子可以试试 OpenVINO EP。

把预处理后处理从 GIL 里挪出去

Python 的胶水代码经常成为隐藏的性能杀手。能用 NumPy 向量化就别写循环,能用 Numba 或者推到 CUDA/CuPy 上更好。热路径里的 transform 最好提前编译好。如果要并发处理请求,worker pool 的规模要和运行时的线程数配合好。

 # Example: pre-allocate and reuse buffers to dodge Python overheads  
import numpy as np  

class Preprocessor:  
    def __init__(self, shape=(1,3,224,224)):  
        self.buf = np.empty(shape, dtype=np.float32)  

    def __call__(self, img):  
        # write into self.buf in-place; no fresh allocations  
        np.copyto(self.buf, img)  
        self.buf /= 255.0  
         return self.buf

这里的判断标准很简单,每个请求都会跑的代码,问问能不能预分配、向量化或者缓存起来。

Session、Engine、Buffer 都只建一次

每个请求新建一个

trt.ICudaEngine

onnxruntime.InferenceSession

基本等于自杀。output array 每次重新分配也一样。

正确做法是进程启动时就加载好 singleton session/engine,每个 worker 维护一两个 CUDA stream,buffer pool 按 shape 和 dtype 索引。

 # Simple singleton-ish loader  
class Model:  
    _sess = None  
    _io = None  
    @classmethod  
    def get(cls):  
        if cls._sess is None:  
            so = ort.SessionOptions()  
            so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL  
            cls._sess = ort.InferenceSession("model.onnx", sess_options=so,  
                          providers=["CUDAExecutionProvider"])  
            cls._io = cls._sess.io_binding()  
         return cls._sess, cls._io

这样做的好处是稳定,p95 不会因为 allocator 和 initializer 出现在热路径而突然炸掉。

一个完整的 GPU 推理骨架

下面的代码是把前面几个关键技术串起来:

 import onnxruntime as ort, numpy as np  

def make_session(path):  
    so = ort.SessionOptions()  
    so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL  
    providers = [("TensorrtExecutionProvider", {"trt_fp16_enable": True}),  
                 "CUDAExecutionProvider", "CPUExecutionProvider"]  
    return ort.InferenceSession(path, sess_options=so, providers=providers)  

class Runner:  
    def __init__(self, model_path, shape=(1,3,224,224)):  
        self.sess = make_session(model_path)  
        self.shape = shape  
        self.io = self.sess.io_binding()  
        self._warmup()  

    def _warmup(self, iters=8):  
        x = np.random.randn(*self.shape).astype(np.float32)  
        self.io.bind_cpu_input("input", x)  
        self.io.bind_output("logits", device_type="cuda")  
        for _ in range(iters):  
            self.sess.run_with_iobinding(self.io)  
        self.io.clear_binding_inputs()  # ready for real runs  

    def run(self, x_np: np.ndarray):  
        # assumes x_np matches self.shape; in production, validate or clamp  
        self.io.bind_cpu_input("input", x_np)  
        self.io.bind_output("logits", device_type="cuda")  
        self.sess.run_with_iobinding(self.io)  
        return self.io.copy_outputs_to_cpu()[0]  

# usage  
runner = Runner("model.onnx")  
batch = np.random.randn(1,3,224,224).astype(np.float32)  
 probs = runner.run(batch)

这个代码已经包含了图优化、I/O binding 和 warmup。后面再加上 CUDA Graph、micro-batch 和固定 shape,能把延迟压到很低,基本上拿来就可以用了

几个容易踩的坑

延迟指标一定要看 p50/p90/p95,别只盯平均值。真正的问题都藏在 tail 里。API 层面最好把 shape 和 dtype 固定下来,或者至少让调用方知道优化过的范围。这样生产请求才能稳定落在最优路径上。

开了融合或量化之后,精度的自动化回归测试必不可少。

总结

低延迟不靠黑科技就是一堆小优化叠起来:形状固定、减少拷贝、更好的 kernel、graph capture、运行时零意外。每个单拎出来可能只省几毫秒,但加起来用户就能感受到"快"。

https://avoid.overfit.cn/post/494ca93b9c184407936ef7b6bd16e15e

作者:Syntal

目录
相关文章
|
5月前
|
机器学习/深度学习 缓存 PyTorch
131_推理加速:ONNX与TensorRT深度技术解析与LLM模型转换优化实践
在大语言模型(LLM)时代,高效的推理加速已成为部署高性能AI应用的关键挑战。随着模型规模的不断扩大(从BERT的数亿参数到GPT-4的数千亿参数),推理过程的计算成本和延迟问题日益突出。ONNX(开放神经网络交换格式)和TensorRT作为业界领先的推理优化框架,为LLM的高效部署提供了强大的技术支持。本文将深入探讨LLM推理加速的核心原理,详细讲解PyTorch模型转换为ONNX和TensorRT的完整流程,并结合2025年最新优化技术,提供可落地的代码实现与性能调优方案。
1578 4
|
4月前
|
人工智能 测试技术 Python
AI也有“智商”吗?我们到底该用什么标准来评估它?
AI也有“智商”吗?我们到底该用什么标准来评估它?
819 8
|
算法 数据库 计算机视觉
Dataset之COCO数据集:COCO数据集的简介、下载、使用方法之详细攻略
Dataset之COCO数据集:COCO数据集的简介、下载、使用方法之详细攻略
|
1月前
|
缓存 人工智能 自然语言处理
Prompt 缓存的四种策略:从精确匹配到语义检索
本文详解Prompt缓存四大策略(精确匹配、规范化、语义相似、分层架构),直击LLM应用成本痛点——重复调用导致API费用飙升。代码示例+架构图,助你低成本提升命中率,降本30%–90%,延迟同步优化。
236 11
Prompt 缓存的四种策略:从精确匹配到语义检索
|
3月前
|
人工智能 自然语言处理 安全
Lux 上手指南:让 AI 直接操作你的电脑
Lux 是一款能直接操作计算机的AI基础模型,通过视觉理解与动作预测,实现自然语言指令下的自动化任务。它无需依赖API,可像真人一样点击、输入、滚动,完成浏览器操作等复杂工作,准确率超越主流模型,是迈向“意图即执行”的重要突破。(238字)
1110 13
Lux 上手指南:让 AI 直接操作你的电脑
|
3月前
|
机器学习/深度学习 存储 安全
别只会One-Hot了!20种分类编码技巧让你的特征工程更专业
分类变量需编码为数字才能被模型处理。本文详解20种编码方法,从基础的独热、序数编码到高级的目标编码、CatBoost、WOE等,涵盖适用场景与代码示例,助你提升模型性能,避免泄露与过拟合,是特征工程中不可或缺的实用指南。
273 14
别只会One-Hot了!20种分类编码技巧让你的特征工程更专业
|
3月前
|
人工智能 JavaScript 机器人
Coze vs Dify vs n8n:三大AI智能体开发平台全面对比
2025年三大AI智能体平台深度对比:Coze零代码快速搭建,适合个人与轻量应用;Dify专注企业级大模型应用,平衡易用与灵活;n8n强在自动化集成,支持高度定制。根据需求选型,助力高效开发。
Jetson学习笔记(二):TensorRT 查看模型的输入输出
这篇博客介绍了如何使用TensorRT查看模型的输入输出,并通过代码示例展示了如何获取和验证模型的输入输出信息。
721 5
|
机器学习/深度学习 算法 数据可视化
【从零开始学习深度学习】46. 目标检测中锚框的概念、计算方法、样本锚框标注方式及如何选取预测边界框
【从零开始学习深度学习】46. 目标检测中锚框的概念、计算方法、样本锚框标注方式及如何选取预测边界框