ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ONNX Runtime 推理加速:8 个降低 Python 延迟的硬核技巧

ONNX Runtime 推理加速:8 个降低 Python 延迟的硬核技巧 在深度学习往实际应用中落实的进程里, 存在着一个较为普遍的错误认知, 一旦推理的速度没有达到所要求的标准, 众人的第一种反应常常是着手对模型进行操作, 像是: 开展剪枝工作、实施蒸馏手段、甚至是以牺牲模型精度为代价来换取更小的模型。事实上, 生产环境里的推理链路, 潜藏着庞大的“工程红利”。许多时候, 你的模型自身并不迟缓, 迟缓的是低效的数据搬运, 是混乱的线程争用, 以及不合理的默认配置。在不改变模型精度的情形下, 仅凭借ONNX(ORT)的工程特性, 往往便能从现有的技术栈当中“抠”出令人惊叹的性能提升。以下存在着 8 个, 是那种经过了实战加以验证的, 属于低延迟范畴的优化策略, 专门用来整治各种各样“莫名其妙的慢。”。1、 明确指定 及其顺序ORT 会依据你传进去的列表顺序来严格开展尝试, 将速度最快的置于首位, 而且要用尽全力去防止它悄然回退到 CPU。要是没有进行明确指定, ORT 偶尔会表现出“犹豫”, 殊不知这些情况全都会耗费时间。import onnxruntime as ort providers [ (TensorrtExecutionProvider, {trt_fp16_enable: True}), # if supported CUDAExecutionProvider, CPUExecutionProvider, ] sess ort.InferenceSession(model.onnx, providersproviders) print(sess.get_providers()) # verify what you actually got是存在成本的, 要是环境当中有, 那就优先予以使用, 要是不存在, 那就降级至CUDA, 最终才是CPU, 将这个路径固定下来, 另外在边缘设备之上, 或者的性能通常会远超普通CPU推理要是是平台带有集成显卡, 那也是一个容易被忽略的加速选择。2.、像做手术一样控制线程数不要超配线程方面存在着被称作intra - op意味着算子内并行以及inter - op意味着算子间并行的两个核心参数情况, 对于二者参数相关设置做法而言, 必然是要参照机器所具备的物理核心数量以及你自身负载所呈现出的特性状况才行的。import os, multiprocessing as mp, onnxruntime as ort cores mp.cpu_count() // 2 or 1 # conservative default so ort.SessionOptions() so.intra_op_num_threads cores so.inter_op_num_threads 1 # start low for consistent latency so.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL sess ort.InferenceSession(model.onnx, sess_optionsso, providers[CPUExecutionProvider])那默认的线程策略, 时不时就会跟 NumPy 争抢资源, 和 BLAS 库也会有资源争夺情况, 甚至对你的 Web 同样会有抢占资源状况, 进而造成严重的线程争用现象, 还会引发长尾延迟。建议你把它设为 1, 一般这样能获取到更稳定的延迟, 之后再进行遍历测试, 测试范围是从 1 起到物理核数, 一边盯着 p50 和 p95 指标, 一边去寻找最佳平衡点, 可千万别只单纯看平均速度。3、使用 IO 规避内存拷贝GPU 必选项要是在GPU上面跑推理, 然而每一次run()的时候, 都把张量从那个地方拷回到Host, 接着又拷回去, 借助IO把输入以及输出直接绑定在内存当中, 重复使用这块内存。import onnxruntime as ort import numpy as np sess ort.InferenceSession(model.onnx, providers[CUDAExecutionProvider]) io sess.io_binding() # Example: preallocate on device via OrtValue (CUDA) import onnxruntime as ort x np.random.rand(1, 3, 224, 224).astype(np.float32) x_ort ort.OrtValue.ortvalue_from_numpy(x, device_typecuda, device_id0) io.bind_input(namesess.get_inputs()[0].name, device_typecuda, device_id0, element_typenp.float32, shapex.shape, buffer_ptrx_ort.data_ptr()) io.bind_output(namesess.get_outputs()[0].name, device_typecuda, device_id0) sess.run_with_iobinding(io) y_ort io.get_outputs()[0] # still on device对于高频请求而言, 这显得极为重要, 哪怕单次拷贝仅仅不过耗费几毫秒, 然而累积起来却会是巨大的开销, 因此要让热数据停留在它应当所在的地方。4、锁定 Shape 或采用分桶策略动态的Shape, 看上去富有灵动性, 然而其会对ORT开展激进的算子融合以及优选形成阻碍, 于导出ONNX之际, 倘若能够将Shape固定下来, 那就尽可能地予以固定。如果业务场景确实需要变长输入可以采用分桶策略# pseudo: choose session by input shape def get_session_for_shape(h, w): if h 256 and w 256: return sess_256 if h 384 and w 384: return sess_384 return sess_fallback举例来说, 于视觉任务当中, 将输入设定在224、256、384这些档位上, 去创建相应的。哪怕仅仅划分两三个桶, 其性能表现相较于完全动态的Shape也强大许多。5、开启全图优化并验证这一步虽说简单, 然而却极易被忽视。开启它, 能让ORT替你去做算子融合, 还能做常量折叠, 以及进行内存规划。import onnxruntime as ort so ort.SessionOptions() so.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL # optional: serialize the optimized model for inspection so.optimized_model_filepath model.optimized.onnx sess ort.InferenceSession(model.onnx, sess_optionsso, providers[CPUExecutionProvider])更低数量的算子意味着更低的开销以及更低的内存带宽压力, 推荐导出一条路径, 通过打开来加以查看, 以此确认Conv加上BN再加上ReLU这样的经典组合是否确实被融合成为一个节点, 要是没有融合, 那么就是优化链路方面存在问题。6、CPU 推理直接上量化要是仅能够运用CPU, INT8量化或者动态量化则变成提速的神奇工具。与CPU的向量指令集相结合, 这能够极大程度地削减掉矩阵乘法所产生的开销。from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( model_inputmodel.onnx, model_outputmodel.int8.onnx, weight_typeQuantType.QInt8, # try QInt8 or QUInt8 extra_options{MatMulConstBOnly: True} )然后加载量化后的模型import onnxruntime as ort sess ort.InferenceSession(model.int8.onnx, providers[CPUExecutionProvider])对于类模型而言, 动态量化一般能够带来1.5至3倍的加速效果, 并且精度损失极小。然而, 需要先在真实数据之上进行验证, 要是精度下降幅度较大, 那就尝试Per-量化, 或者仅仅对计算最为密集的算子进行量化。7、预热、复用与 Micro-它的初始化开销极为巨大, 是那种属于重资源类型的对象。一定要确保在全局范围里面仅仅开展一次创建的动作, 另外在此基础之上还需要在启动之后先去运行几次虚拟数据来进行预热操作, 从而能够将缓存以及内存池都填充完善。# app startup sess ort.InferenceSession(model.onnx, providers[CUDAExecutionProvider]) dummy {sess.get_inputs()[0].name: np.zeros((1, 3, 224, 224), np.float32)} for _ in range(3): sess.run(None, dummy) # warms kernels, caches, memory arenas要是处于高并发情形之下, 别让一个个请求独自运作, 而是积攒成一个 Micro-batch, 把诸如 2 到 8 个样本那样的攒起来一同送进去, 如此便能显著提升 GPU 的利用率, 。def infer_batch(batch): inputs np.stack(batch, axis0).astype(np.float32, copyFalse) return sess.run(None, {sess.get_inputs()[0].name: inputs})[0]在对Batch Size作出调整之际, 目光紧紧注视着p95延迟以及吞吐量, 从中寻觅到那个处于最佳状态的点。8、优化前后处理拒绝 循环许多情况下众人埋怨模型迟缓, 实际上瓶颈处于预处理以及后处理 , 进行像素处理的for循环是绝对的性能杀手 , 因而维持数组内存连续 , 防止不必要的转换并尽可能全部向量化。import numpy as np # Bad: repeated copies/conversions # x np.array(img).astype(np.float32) # realloc every time # Better: reuse buffers and normalize in-place buf np.empty((1, 3, 224, 224), dtypenp.float32) def preprocess(img, outbuf): # assume img is already CHW float32 normalized upstream np.copyto(out, img, castingno) # no implicit cast return out # Post-process with NumPy ops, not Python loops def topk(logits, k5): idx np.argpartition(logits, -k, axis1)[:, -k:] vals np.take_along_axis(logits, idx, axis1) order np.argsort(-vals, axis1) return np.take_along_axis(idx, order, axis1), np.take_along_axis(vals, order, axis1)好几毫秒会被几个多余的.( )给吃掉, 在低延迟场景内里, 这可是非常地致命。基准测试模板这是个简易的脚本, 略微修改之后就能投入使用, 切莫倚靠感觉加以优化, 而是得运用数据去开展对比。import time, statistics as stats import numpy as np, onnxruntime as ort def bench(sess, x, iters100, warmup5): name sess.get_inputs()[0].name for _ in range(warmup): sess.run(None, {name: x}) times [] for _ in range(iters): t0 time.perf_counter() sess.run(None, {name: x}) times.append((time.perf_counter() - t0) * 1e3) return { p50_ms: stats.median(times), p95_ms: sorted(times)[int(0.95 * len(times)) - 1], min_ms: min(times), max_ms: max(times) } # Example usage providers [CUDAExecutionProvider, CPUExecutionProvider] so ort.SessionOptions(); so.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL sess ort.InferenceSession(model.onnx, sess_optionsso, providersproviders) x np.random.rand(1, 3, 224, 224).astype(np.float32) print(bench(sess, x))总结进行低延迟推理不存在什么神秘莫测的黑科技, 通通都是些细微之处。要做出正确选择, 千万别随意开启线程, 减少内存之间的拷贝动作, 让形状保持固定, 大胆地去做图形融合, 最后把代码整理得干干净净。哪怕仅仅只是切实做到其中的两三点, 性能的提升也是能够明显看得到的。
返回列表