onnx runtime文档学习3-CUDA provider

网上充斥着ONNX Runtime的简单科普,却没有一个系统介绍ONNX Runtime的博客,因此本博客旨在基于官方文档进行翻译与进一步的解释。ONNX runtime的官方文档:https://onnxruntime.ai/docs/

如果尚不熟悉ONNX格式,可以参照该博客专栏,本专栏对onnx 1.16文档进行翻译与进一步解释,
ONNX 1.16学习笔记专栏:https://blog.csdn.net/qq_33345365/category_12581965.html
如果觉得有收获,点赞收藏关注,目前仅在CSDN发布,本博客会分为多个章节,目前尚在连载中。

开始编辑时间:2024/3/11;最后编辑时间:2024/3/11

所有资料均来自书写时的最新官方文档内容。


本专栏链接如下所示,所有相关内容均会在此处收录。

https://blog.csdn.net/qq_33345365/category_12589378.html


介绍

参考:https://onnxruntime.ai/docs/get-started/with-python.html

本教程第一篇:介绍ONNX Runtime(ORT)的基本概念。

本教程第二篇:是一个快速指南,包括安装ONNX Runtime;安装ONNX进行模型输出;Pytorch, TensorFlow和SciKit的快速开始例子

本教程第三篇(本博客):CUDA Provider


CUDA Provider

延续上一篇教程,已经在data目录下有一个模型,我们使用Pytorch CV模型进行介绍:

在开始本教程前,需要在目录下有data/model.onnx文件

本文使用的版本如下所示,更多版本适配问题,见https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html

  • onnx runtime 1.17 # 使用pip安装: pip install
  • cudn 8.9.2.26 # 使用conda安装: conda install cudnn=8.9.2.26, 查看现有版本conda search cudnn
  • CUDA 11.8 # 安装在系统上 见cuda安装

首先创建如下代码:

import torch
import onnxruntime as ort

from torchvision import datasets
from torchvision.transforms import ToTensor

onnx_model = "data/model.onnx"

test_data = datasets.FashionMNIST(
    root="data", train=False, download=True, transform=ToTensor()
)

classes = [
    "T-shirt/top",
    "Trouser",
    "Pullover",
    "Dress",
    "Coat",
    "Sandal",
    "Shirt",
    "Sneaker",
    "Bag",
    "Ankle boot",
]
x, y = test_data[0][0], test_data[0][1]


session = ort.InferenceSession(onnx_model, None)
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name

result = session.run([output_name], {input_name: x.numpy()})
predicted, actual = classes[result[0][0].argmax(0)], classes[y]
print(f'Predicted: "{predicted}", Actual: "{actual}"')

# GPU run

providers = [
    (
        "CUDAExecutionProvider",
        {
            "device_id": torch.cuda.current_device(),
        },
    )
]
sess_options = ort.SessionOptions()
session = ort.InferenceSession(
    onnx_model, sess_options=sess_options, providers=providers
)
input_name = session.get_inputs()[0].name
output_name = session.get_outputs()[0].name

result = session.run([output_name], {input_name: x.numpy()})
predicted, actual = classes[result[0][0].argmax(0)], classes[y]
print(f'Predicted: "{predicted}", Actual: "{actual}"')

输出:

Predicted: "Ankle boot", Actual: "Ankle boot"
Predicted: "Ankle boot", Actual: "Ankle boot"

如果可以正确输出,则证明环境正确,输出第二行的代码是使用CUDA运行产生的。

示例

import onnxruntime as ort

model_path = '<path to model>'

providers = [
    (
        "CUDAExecutionProvider",
        {
            "device_id": torch.cuda.current_device(),
            # "user_compute_stream": str(torch.cuda.current_stream().cuda_stream),
            "do_copy_in_default_stream": True,
            "use_ep_level_unified_stream": False,
            "gpu_mem_limit": 2 * 1024 * 1024 * 1024,
            "arena_extend_strategy": "kNextPowerOfTwo",
            "cudnn_conv_algo_search": "EXHAUSTIVE",
            "cudnn_conv_use_max_workspace": "1",
            "cudnn_conv1d_pad_to_nc1d": "0",
            "enable_cuda_graph": "0",
            "enable_skip_layer_norm_strict_mode": "0",
            # "use_tf32": 0,
            "prefer_nhwc": "0",
        },
    )
]

session = ort.InferenceSession(model_path, providers=providers)

这些参数的含义如下所示。

更多语言的示例参照https://onnxruntime.ai/docs/execution-providers/CUDA-ExecutionProvider.html

配置选项

CUDA Execution provider提供的配置选项如下所示,以下所有参数的使用示例均体现在上面的代码中。

  1. device_id

    1. 含义:GPU的设备id

    2. 默认值:0

  2. user_compute_stream

    1. 含义:定义了推理运行的计算流。它隐含地设置了has_user_compute_stream选项。它不能通过UpdateCUDAProviderOptions设置,而是要通过UpdateCUDAProviderOptionsWithValue设置。这不能与外部分配器一起使用。为了充分利用用户计算流,建议使用I/O绑定将输入和输出绑定到设备中的张量。
    2. 默认值:未知

    这个参数在本文环境中不可使用,或未找到正确使用方法

  3. do_copy_in_default_stream

    1. 含义:是否在默认流中进行复制,还是使用单独的流。建议设置为 true。如果设置为 false,可能会出现竞争条件,也可能性能会更好。
    2. 默认值:true
  4. use_ep_level_unified_stream

    1. 含义:所有CUDA EP(Execution Provider)线程都使用相同的CUDA流。这是通过has_user_compute_streamenable_cuda_graph或者使用外部分配器时隐式启用的。
    2. 默认值:false
  5. gpu_mem_limit

    1. 设备内存区域的大小限制,以字节为单位。这个大小限制仅适用于执行提供程序的区域。总设备内存使用量可能会更高。s:C++ size_t 类型的最大值(实际上是无限制的)。会被default_memory_arena_cfg的内容覆盖(如果指定)。
    2. 默认值:不确定,可能不指定为无限大
  6. arena_extend_strategy

    1. 含义:扩展设备内存领域(memory arena)的策略。会被default_memory_arena_cfg的内容覆盖(如果指定)。

    2. 可用值

      1. kNextPowerOfTwo (0):乘以二的幂次来扩展
      2. kSameAsRequested (1):按照请求量扩展
    3. 默认值:kNextPowerOfTwo

  7. cudnn_conv_algo_search

    1. 含义:对cuDNN卷积算法所做的搜索类型。

    2. 可用值:

      1. EXHAUSTIVE (0):使用cudnnFindConvolutionForwardAlgorithmEx进行昂贵的穷举基准测试
      2. HEURISTIC (1):基于cudnnGetConvolutionForwardAlgorithm_v7的轻量级启发式搜索
      3. DEFAULT (2):使用CUDNN_CONVOLUTION_FWD_ALGO_IMPLICIT_PRECOMP_GEMM的默认算法
    3. 默认值:EXHAUSTIVE

  8. cudnn_conv_use_max_workspace

    1. 含义:在下面性能调优部分的卷积密集型模型中了解此标志的作用详情。当使用 C API 时,此标志仅在使用 V2 版本的提供程序选项结构时受支持。
    2. 默认值:1.14和更高的版本为1,之前的版本为0
  9. cudnn_conv1d_pad_to_nc1d

    1. 含义:在下面性能调优部分的卷积输入填充中了解此标志的作用详情。当使用 C API 时,此标志仅在使用 V2 版本的提供程序选项结构时受支持。
    2. 默认值:0
  10. enable_cuda_graph

    1. 含义:在下面性能调优部分的使用CUDA图中了解此标志的作用详情。当使用 C API 时,此标志仅在使用 V2 版本的提供程序选项结构时受支持。
    2. 默认值:0
  11. enable_skip_layer_norm_strict_mode

    1. 含义:是否在SkipLayerNormalization的cuda实现中使用严格模式。默认和推荐的设置是false。如果启用,可以期望提高准确性但性能下降。仅当使用C API时,此标志才受V2版本的提供者选项结构支持。
    2. 默认值:0
  12. use_tf32

    1. 含义:TF32 是自 Ampere 以来在 NVIDIA GPU 上可用的一种数学模式。它允许某些 float32 矩阵乘法和卷积在张量核上以 TensorFloat-32 降低精度运行得更快:float32 输入被四舍五入为 10 位尾数,并且结果使用 float32 精度累积。TensorFloat-32 默认已启用。从 ONNX Runtime 1.18 开始,您可以使用此标志禁用它以进行推理会话。
    2. 默认值:1

    这个参数在本文环境中不可使用,或未找到正确使用方法

  13. gpu_external_[alloc|free|empty_cache]

    1. gpu_external_* 用于传递外部分配器。示例Python用法:

      from onnxruntime.training.ortmodule.torch_cpp_extensions import torch_gpu_allocator
      
      provider_option_map["gpu_external_alloc"] = str(torch_gpu_allocator.gpu_caching_allocator_raw_alloc_address())
      provider_option_map["gpu_external_free"] = str(torch_gpu_allocator.gpu_caching_allocator_raw_delete_address())
      provider_option_map["gpu_external_empty_cache"] = str(torch_gpu_allocator.gpu_caching_allocator_empty_cache_address())
      
    2. 默认值:0

      这个参数在本文环境中不可使用,包都找不到

  14. prefer_nhwc

    1. 含义:该选项在默认构建中不可用!必须使用onnxruntime_USE_CUDA_NHWC_OPS=ON编译ONNX Runtime。如果启用了此选项,EP将优先使用NHWC运算符而不是NCHW。所需的转换将被添加到模型中。由于NVIDIA张量核只能在NHWC布局上工作,如果模型由许多支持的运算符组成并且不需要太多的新转置节点,则可以提高性能。未来计划扩展更广泛的运算符支持。此标志仅在使用C API时从V2版本的提供程序选项结构中受支持。可以使用CreateCUDAProviderOptions创建V2提供程序选项结构,并使用UpdateCUDAProviderOptions进行更新。
    2. 默认值:0

性能调优

I/O绑定特征应该被利用以避免由于输入和输出的复制而产生的开销。理想情况下,输入的上载和下载可以在推理之后隐藏起来。这可以通过在运行推断时进行异步复制来实现。这在这个PR中有所体现。

Ort::RunOptions run_options;
run_options.AddConfigEntry("disable_synchronize_execution_providers", "1");
session->Run(run_options, io_binding);

通过禁用推理中的同步,用户必须在执行后负责同步计算流。此功能只应与设备本地内存或在pinned memory中分配的ORT值一起使用,否则发出的下载将是阻塞的,并且不会表现出预期的行为。

卷积密集型模型(Convolution-heavy models)

ORT利用CuDNN进行卷积操作,这个过程的第一步是确定在每个Conv节点中执行给定输入配置(输入形状、过滤器形状等)的卷积操作时使用哪个“最佳”卷积算法。这个子步骤涉及向CuDNN查询“工作空间”内存大小,并分配这个内存,以便CuDNN在确定要使用的“最佳”卷积算法时可以使用这个辅助内存。

cudnn_conv_use_max_workspace的默认值为1,对于版本1.14或更高版本,对于之前的版本,默认值为0。当其值为0时,ORT将工作空间大小限制为32 MB,这可能导致CuDNN选择一个次优的卷积算法。为了允许ORT分配由CuDNN确定的最大可能的工作空间,需要设置一个名为cudnn_conv_use_max_workspace的提供者选项(如下所示)。

请注意,使用此标志可能会将峰值内存使用量增加数倍(有时几个GB),但这确实帮助CuDNN为给定的输入选择最佳的卷积算法。我们发现,在使用fp16模型时使用这个标志是很重要的,因为这允许CuDNN选择用于卷积操作的张量核算法(如果硬件支持张量核操作)。对于其他数据类型(float和double),该标志可能会或可能不会导致性能增益。

仅提供Python实现

  providers = [("CUDAExecutionProvider", {"cudnn_conv_use_max_workspace": '1'})]
  sess_options = ort.SessionOptions()
  sess = ort.InferenceSession("my_conv_heavy_fp16_model.onnx", sess_options=sess_options, providers=providers)

卷积输入填充

ORT利用CuDNN进行卷积操作。虽然CuDNN仅接受4维或5维张量作为卷积操作的输入,但如果输入是3维张量,则需要进行维度填充。给定形状为[N,C,D]的输入张量,可以填充为[N,C,D,1]或[N,C,1,D]。虽然这两种填充方式产生相同的输出,但性能可能会有很大差异,因为会选择不同的卷积算法,特别是在某些设备上,如A100。默认情况下,输入被填充为[N,C,D,1]。如果更倾向于[N,C,1,D],则需要设置一个名为cudnn_conv1d_pad_to_nc1d的提供者选项(如下所示)。

仅提供Python实现

  providers = [("CUDAExecutionProvider", {"cudnn_conv1d_pad_to_nc1d": '1'})]
  sess_options = ort.SessionOptions()
  sess = ort.InferenceSession("my_conv_model.onnx", sess_options=sess_options, providers=providers)

使用CUDA图(预览) Using CUDA Graphs (Preview)

在使用CUDA EP时,ORT支持使用CUDA Graphs来消除与顺序启动CUDA内核相关的CPU开销。要启用CUDA Graph的使用,请使用以下示例中显示的提供程序选项。通过将用户指定的gpu_graph_id传递给运行选项,ORT支持多图形捕获功能。当会话使用一个CUDA Graph时,gpu_graph_id是可选的。如果未设置,则默认值为0。如果gpu_graph_id设置为-1,则在该运行中禁用CUDA Graph捕获/回放。

目前,使用CUDA Graph功能存在一些限制:

  • 不支持具有控制流操作(即If、Loop和Scan操作)的模型。
  • CUDA Graph的使用仅限于可以将所有模型操作(图节点)分区到CUDA EP的模型。
  • 模型的输入/输出类型必须是张量。
  • 对于相同的图注释ID,输入/输出的形状和地址在推断调用之间不能更改。用于回放的输入张量将被复制到图形捕获中使用的输入张量的地址。
  • 在多图形捕获模式下,捕获的图形将保留在会话的生命周期中,并且当前不支持捕获的图形删除功能。
  • 根据设计,CUDA Graph被设计为在图形重放步骤期间从/写入相同的CUDA虚拟内存地址读取/写入,正如它在图形捕获步骤期间所做的那样。由于这个要求,使用这个特性需要使用IOBinding来绑定将被用作CUDA Graph机制读取/写入输入/输出的内存(请参见下面的示例)。
  • 在更新后续推断调用的输入时,需要将新的输入复制到绑定的OrtValue输入的相应CUDA内存位置(请参见下面的示例,了解如何实现此目标)。这是因为“图形重播”将需要从相同的CUDA虚拟内存地址读取输入。
  • 目前不支持多线程使用,即在使用CUDA Graph时,不可以从多个线程在同一InferenceSession对象上调用Run()。

注意:第一个Run()在幕后执行各种任务,如进行CUDA内存分配、捕获模型的CUDA Graph,然后执行图形重放以确保图形运行。由于这个原因,与第一个Run()相关联的延迟必定会很高。后续的Run()只执行在第一个Run()中捕获和缓存的图形的图形重放。

仅提供Python代码

  providers = [("CUDAExecutionProvider", {"enable_cuda_graph": '1'})]
  sess_options = ort.SessionOptions()
  sess = ort.InferenceSession("my_model.onnx", sess_options=sess_options, providers=providers)

  providers = [("CUDAExecutionProvider", {'enable_cuda_graph': True})]
  x = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]], dtype=np.float32)
  y = np.array([[0.0], [0.0], [0.0]], dtype=np.float32)
  x_ortvalue = onnxrt.OrtValue.ortvalue_from_numpy(x, 'cuda', 0)
  y_ortvalue = onnxrt.OrtValue.ortvalue_from_numpy(y, 'cuda', 0)

  session = onnxrt.InferenceSession("matmul_2.onnx", providers=providers)
  io_binding = session.io_binding()

  # Pass gpu_graph_id to RunOptions through RunConfigs
  ro = onnxrt.RunOptions()
  # gpu_graph_id is optional if the session uses only one cuda graph
  ro.add_run_config_entry("gpu_graph_id", "1")

  # Bind the input and output
  io_binding.bind_ortvalue_input('X', x_ortvalue)
  io_binding.bind_ortvalue_output('Y', y_ortvalue)

  # One regular run for the necessary memory allocation and cuda graph capturing
  session.run_with_iobinding(io_binding, ro)
  expected_y = np.array([[5.0], [11.0], [17.0]], dtype=np.float32)
  np.testing.assert_allclose(expected_y, y_ortvalue.numpy(), rtol=1e-05, atol=1e-05)

  # After capturing, CUDA graph replay happens from this Run onwards
  session.run_with_iobinding(io_binding, ro)
  np.testing.assert_allclose(expected_y, y_ortvalue.numpy(), rtol=1e-05, atol=1e-05)

  # Update input and then replay CUDA graph with the updated input
  x_ortvalue.update_inplace(np.array([[10.0, 20.0], [30.0, 40.0], [50.0, 60.0]], dtype=np.float32))
  session.run_with_iobinding(io_binding, ro)
class SR_net { public: SR_net(string path, vector<int> input_size, bool fp32, bool cuda = true); private: vector<int64_t> Gdims; int Gfp32; Env env = Env(ORT_LOGGING_LEVEL_ERROR, "RRDB"); SessionOptions session_options = SessionOptions(); Session* Gsession = nullptr; vector<const char*> Ginput_names; vector<const char*> Goutput_names; vector<int> Ginput_size = {}; }; SR_net::SR_net(string path, vector<int> input_size, bool fp32, bool cuda) { this->Ginput_size = input_size; this->Gfp32 = fp32; clock_t startTime_, endTime_; startTime_ = clock(); session_options.SetIntraOpNumThreads(6); if (cuda) { OrtCUDAProviderOptions cuda_option; cuda_option.device_id = 0; cuda_option.arena_extend_strategy = 0; cuda_option.cudnn_conv_algo_search = OrtCudnnConvAlgoSearchExhaustive; cuda_option.gpu_mem_limit = SIZE_MAX; cuda_option.do_copy_in_default_stream = 1; session_options.AppendExecutionProvider_CUDA(cuda_option); } wstring widestr = wstring(path.begin(), path.end()); this->Gsession = new Session(env, widestr.c_str(), this->session_options); this->session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); AllocatorWithDefaultOptions allocator; this->Ginput_names = { "input" }; this->Goutput_names = { "output" }; endTime_ = clock(); cout << " The model loading time is:" << (double)(endTime_ - startTime_) / CLOCKS_PER_SEC << "s" << endl; } int main() { vector<int> input_shape = {}; SR_net net("E:/prj/SR_C/onnx_file/rrdb_full.onnx", input_shape, true, true); },在这段代码中,我如何把SR_net net("E:/prj/SR_C/onnx_file/rrdb_full.onnx", input_shape, true, true);这一行写到主函数的外面?
06-02
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

whyte王

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值