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

目录
相关文章
|
1月前
|
人工智能 测试技术 Python
AI也有“智商”吗?我们到底该用什么标准来评估它?
AI也有“智商”吗?我们到底该用什么标准来评估它?
178 8
|
4月前
|
机器学习/深度学习 算法 测试技术
NSA稀疏注意力深度解析:DeepSeek如何将Transformer复杂度从O(N²)降至线性,实现9倍训练加速
本文将深入分析NSA的架构设计,通过详细的示例、可视化展示和数学推导,构建对其工作机制的全面理解,从高层策略到底层硬件实现均有涉及。
395 0
NSA稀疏注意力深度解析:DeepSeek如何将Transformer复杂度从O(N²)降至线性,实现9倍训练加速
|
存储 计算机视觉 开发者
【mobileSam】使用大模型推理赋能标注工作,让标注工作不再困难
【mobileSam】使用大模型推理赋能标注工作,让标注工作不再困难
880 1
|
算法 数据库 计算机视觉
Dataset之COCO数据集:COCO数据集的简介、下载、使用方法之详细攻略
Dataset之COCO数据集:COCO数据集的简介、下载、使用方法之详细攻略
|
存储 SQL NoSQL
HarmonyOS学习路之开发篇—数据管理(分布式数据服务)
分布式数据服务(Distributed Data Service,DDS) 为应用程序提供不同设备间数据库数据分布式的能力。通过调用分布式数据接口,应用程序将数据保存到分布式数据库中。通过结合帐号、应用和数据库三元组,分布式数据服务对属于不同应用的数据进行隔离,保证不同应用之间的数据不能通过分布式数据服务互相访问。在通过可信认证的设备间,分布式数据服务支持应用数据相互同步,为用户提供在多种终端设备上最终一致的数据访问体验。
|
2月前
|
机器学习/深度学习 缓存 PyTorch
131_推理加速:ONNX与TensorRT深度技术解析与LLM模型转换优化实践
在大语言模型(LLM)时代,高效的推理加速已成为部署高性能AI应用的关键挑战。随着模型规模的不断扩大(从BERT的数亿参数到GPT-4的数千亿参数),推理过程的计算成本和延迟问题日益突出。ONNX(开放神经网络交换格式)和TensorRT作为业界领先的推理优化框架,为LLM的高效部署提供了强大的技术支持。本文将深入探讨LLM推理加速的核心原理,详细讲解PyTorch模型转换为ONNX和TensorRT的完整流程,并结合2025年最新优化技术,提供可落地的代码实现与性能调优方案。
|
2月前
|
机器学习/深度学习 缓存 并行计算
90_推理优化:性能调优技术
随着大型语言模型(LLM)规模的不断扩大和应用场景的日益复杂,推理性能已成为制约模型实际部署和应用的关键因素。尽管大模型在各项任务上展现出了令人惊艳的能力,但其庞大的参数量和计算需求也带来了严峻的性能挑战。在资源受限的环境中,如何在保持模型效果的同时,最大化推理性能,成为了研究人员和工程师们亟待解决的核心问题。
|
9月前
|
机器学习/深度学习 编解码 自然语言处理
SigLIP 2:多语言语义理解、定位和密集特征的视觉语言编码器
SigLIP 2 是一种改进的多语言视觉-语言编码器系列,通过字幕预训练、自监督学习和在线数据管理优化性能。它在零样本分类、图像-文本检索及视觉表示提取中表现卓越,支持多分辨率处理并保持图像纵横比。模型提供 ViT-B 至 g 四种规格,采用 WebLI 数据集训练,结合 Sigmoid 损失与自蒸馏等技术提升效果。实验表明,SigLIP 2 在密集预测、定位任务及多模态应用中显著优于前代和其他基线模型。
805 9
SigLIP 2:多语言语义理解、定位和密集特征的视觉语言编码器
|
机器学习/深度学习 并行计算 PyTorch
PyTorch中的多进程并行处理
这篇文章我们将介绍如何利用torch.multiprocessing模块,在PyTorch中实现高效的多进程处理。
502 1
|
11月前
|
人工智能 机器人
LeCun 的世界模型初步实现!基于预训练视觉特征,看一眼任务就能零样本规划
纽约大学Gaoyue Zhou等人提出DINO World Model(DINO-WM),利用预训练视觉特征构建世界模型,实现零样本规划。该方法具备离线训练、测试时行为优化和任务无关性三大特性,通过预测未来补丁特征学习离线行为轨迹。实验表明,DINO-WM在迷宫导航、桌面推动等任务中表现出强大的泛化能力,无需依赖专家演示或奖励建模。论文地址:https://arxiv.org/pdf/2411.04983v1。
325 21

热门文章

最新文章