博客 / 詳情

返回

TensorRtSharp:在 C# 世界中釋放 GPU 推理的極致性能

TensorRtSharp:在 C# 世界中釋放 GPU 推理的極致性能

目錄

  • 一、前言
  • 二、什麼是 TensorRtSharp
  • 三、安裝與配置
  • 四、核心架構設計
  • 五、核心類與 API
  • 六、完整使用示例
  • 七、異常處理
  • 八、日誌系統
  • 九、與其他庫的對比
  • 十、常見問題
  • 十一、總結

一、前言

1.1 為什麼需要 TensorRtSharp?

在深度學習模型部署領域,NVIDIA TensorRT 憑藉其卓越的推理性能已成為 GPU 加速的事實標準。根據 NVIDIA 官方數據,使用 TensorRT 進行模型優化和推理加速,通常可以獲得:

  • 📈 推理速度提升 2-10 倍(相比原生框架)
  • 💾 顯存佔用降低 50% 以上(通過精度優化和層融合)
  • 延遲降低至毫秒級(滿足實時應用需求)

然而,TensorRT 官方僅提供 C++ 和 Python API,這讓廣大 .NET 開發者面臨一個兩難的選擇:

  • 放棄熟悉的 C# 生態,轉向 C++ 或 Python
  • 通過複雜的互操作層進行調用,開發效率低下

TensorRtSharp 應運而生 —— 這是一個純 C# 編寫的 TensorRT 完整封裝庫,為 .NET 開發者提供了:

  • 類型安全的 API 接口 - 強類型系統,編譯時錯誤檢查
  • 易於使用且性能卓越 - 直觀的 API 設計,零性能損失
  • 完整的 TensorRT 功能覆蓋 - 支持所有核心功能
  • 自動資源管理 - 基於 RAII 和 Dispose 模式,無需擔心內存泄漏
  • 開箱即用 - NuGet 一鍵安裝,無需複雜配置
  • 完善的文檔和示例 - 豐富的代碼示例和詳細的使用説明

1.2 TensorRtSharp 的核心優勢

1. 原生 C# 體驗

// 簡潔直觀的 API 設計
using Runtime runtime = new Runtime();
using CudaEngine engine = runtime.deserializeCudaEngineByBlob(data, size);
using ExecutionContext context = engine.createExecutionContext();
context.executeV3(stream);

2. 完整功能覆蓋

  • ✅ 模型構建(ONNX → Engine)
  • ✅ 推理執行(同步/異步)
  • ✅ 動態形狀支持
  • ✅ 多精度推理(FP32/FP16/INT8)
  • ✅ 多 GPU 並行推理

1.3 TensorRtSharp 3.0 的重大改進

在前期開發的 TensorRtSharp 1.0 和 2.0 中,使用者需要下載源碼編譯才能使用,過程繁瑣且容易出錯。

在最新的 3.0 版本中,我們進行了重大改進

一鍵安裝 - 直接將編譯好的原生庫與託管代碼打包至 NuGet 包中
開箱即用 - 無需配置複雜的構建環境
版本一致 - 降低因環境差異導致的潛在錯誤

開發者僅需通過 Visual Studio 的 NuGet 包管理器安裝即可直接使用,顯著提升了開發效率與部署便捷性!

本文將全面介紹 TensorRtSharp 的設計理念、核心功能和使用方法,助力大家快速上手使用。


二、什麼是 TensorRtSharp

2.1 項目簡介

TensorRtSharp 3.0 是作者對 NVIDIA TensorRT 官方庫的完整 C# 接口封裝。通過 P/Invoke 技術,它將 TensorRT 的原生 C++ API 映射為符合 .NET 設計規範的託管代碼,讓 C# 開發者能夠無縫使用 TensorRT 的全部功能。

2.2 核心特性

特性 説明
完整的 API 覆蓋 支持 TensorRT 核心功能,包括模型構建、推理執行、動態形狀等
類型安全 強類型系統,編譯時錯誤檢查,避免運行時類型錯誤
自動資源管理 基於 RAII 和 Dispose 模式的資源管理,防止內存泄漏
跨平台支持 支持 Windows、Linux,兼容 .NET 5.0-10.0、.NET Core 3.1、.NET Framework 4.7.1-4.8.1
高性能異步執行 支持 CUDA Stream、多執行上下文並行推理
開箱即用 NuGet 包含所有依賴,無需複雜配置

2.3 項目信息

項目 信息
版本 目前最新 NuGet 版本為 0.0.5(持續更新中,建議使用最新版本)
GitHub https://github.com/guojin-yan/TensorRT-CSharp-API
接口 NuGet JYPPX.TensorRT.CSharp.API
Runtime NuGet JYPPX.TensorRT.CSharp.API.runtime.win-x64.cuda12JYPPX.TensorRT.CSharp.API.runtime.win-x64.cuda11
編程語言 C# 10

三、安裝與配置

3.1 通過 NuGet 安裝

安裝 TensorRtSharp 非常簡單,只需安裝兩個 NuGet 包:

# 安裝接口包
dotnet add package JYPPX.TensorRT.CSharp.API

# 安裝運行時包(根據您的 CUDA 版本選擇)
# CUDA 12.x 版本
dotnet add package JYPPX.TensorRT.CSharp.API.runtime.win-x64.cuda12

# 或 CUDA 11.x 版本
dotnet add package JYPPX.TensorRT.CSharp.API.runtime.win-x64.cuda11

💡 小貼士:Runtime 包與 CUDA 版本相關,請根據您設備上安裝的 CUDA 版本選擇對應的包。

image-20260109233203782

3.2 系統要求

要求 説明
操作系統 Windows 10+、Linux(Ubuntu 18.04+)、macOS 10.15+
.NET 版本 .NET 5.0-10.0、.NET Core 3.1、.NET Framework 4.7.1+
GPU NVIDIA GPU(支持 CUDA 11.x 或 12.x)
依賴 NVIDIA TensorRT 10.x、CUDA Runtime

3.3 重要版本説明

⚠️ 重要提醒:NVIDIA TensorRT 必須是 10.x 系列!!

TensorRtSharp 3.0 基於 TensorRT 10.x 開發,不支持 TensorRT 8.x 或 9.x 版本。

為了防止出現兼容性問題,建議使用與博主相同的配置:

配置 1(推薦):

  • CUDA 11.6
  • cuDNN 9.2.0
  • TensorRT 10.13.0.35

配置 2:

  • CUDA 12.3
  • cuDNN 9.2.0
  • TensorRT 10.11.0.33

3.4 配置原生庫

TensorRtSharp 依賴 TensorRT 的原生庫(nvinfer.dll)和 CUDA 的原生庫(cudart64_*.dll 等)。有兩種配置方式:

方式一:拷貝 DLL 到應用程序目錄(不推薦)

將 TensorRT 和 CUDA 的所有 DLL 文件拷貝到程序可執行目錄下。

缺點

  • 會導致程序目錄文件龐大
  • 不方便管理與部署
  • 不推薦使用此方式

方式二:設置系統 PATH(推薦)

將 TensorRT 的 lib 目錄和 CUDA 的 bin 目錄路徑添加到系統 PATH 環境變量中。

優點

  • 無需複製大量文件
  • 保持應用目錄整潔
  • 便於版本管理和部署維護

配置步驟

  1. 設置 CUDA_PATH 環境變量

CUDA_PATH 配置

  1. 設置 PATH 環境變量

將以下路徑添加到 PATH:

  • CUDA 的 bin 目錄(如 C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.6\bin
  • TensorRT 的 lib 目錄(如 C:\TensorRT-10.13.0.35\lib

PATH 配置

💡 建議:優先使用環境變量方式配置,避免因文件冗餘導致部署複雜。同時注意不同 CUDA 版本間的兼容性問題。


四、核心架構設計

4.1 三層架構

TensorRtSharp 採用清晰的三層架構設計:

┌─────────────────────────────────────────────────────────┐
│           業務 API 層 (High-Level API)                   │
│  Runtime, Builder, CudaEngine, ExecutionContext         │
└─────────────────────────────────────────────────────────┘
                           ▲
                           │
┌─────────────────────────────────────────────────────────┐
│         資源管理層 (Resource Management)                 │
│  DisposableTrtObject, DisposableObject, IOvPtrHolder    │
└─────────────────────────────────────────────────────────┘
                           ▲
                           │
┌─────────────────────────────────────────────────────────┐
│         P/Invoke 層 (Native Interop)                    │
│  NativeMethodsTensorRt*, NativeMethodsCuda*             │
└─────────────────────────────────────────────────────────┘

4.2 自動資源管理

TensorRtSharp 實現了完善的資源管理機制,所有 TensorRT 對象都繼承自 DisposableTrtObject

// 所有 TensorRT 對象繼承自 DisposableTrtObject
public abstract class DisposableTrtObject : DisposableObject
{
    protected IntPtr ptr;                      // 原生對象指針
    public bool IsDisposed { get; protected set; }

    // 安全訪問原生指針(自動檢查釋放狀態)
    public IntPtr TrtPtr
    {
        get
        {
            ThrowIfDisposed();
            return ptr;
        }
    }

    // 釋放非託管資源
    protected override void DisposeUnmanaged()
    {
        if (ptr != IntPtr.Zero)
        {
            // 調用原生釋放函數
            NativeDestroy(ptr);
            ptr = IntPtr.Zero;
        }
    }
}

// 使用 using 語句自動釋放資源
using Runtime runtime = new Runtime();
using CudaEngine engine = runtime.deserializeCudaEngineByBlob(data, size);
// 離開作用域時自動釋放

設計亮點

  • ✅ 採用標準 Dispose 模式,確保資源正確釋放
  • ✅ 線程安全的資源釋放機制(使用 Interlocked.Exchange
  • ✅ 自動內存壓力通知(GC.AddMemoryPressure
  • ✅ 指針安全訪問(ThrowIfDisposed 檢查)

五、核心類與 API

5.1 命名空間

在使用 TensorRtSharp 之前,首先引入必要的命名空間:

using JYPPX.TensorRtSharp.Cuda;      // CUDA 接口的程序集命名空間
using JYPPX.TensorRtSharp.Nvinfer;   // TensorRT 接口的程序集命名空間

5.2 Runtime(推理運行時)

Runtime 是 TensorRT 推理的入口點,負責從序列化的引擎文件創建推理引擎。

// 創建 Runtime 實例
Runtime runtime = new Runtime();
string filePath = "yolov8s-obb.engine";

// 從字節數組反序列化引擎
byte[] data = File.ReadAllBytes(filePath);
using CudaEngine cudaEngine = runtime.deserializeCudaEngineByBlob(data, (ulong)data.Length);

// 從文件流反序列化
using var reader = new FileStreamReader();
reader.open(filePath);
using CudaEngine cudaEngine = runtime.deserializeCudaEngineByFileStreamReader(reader);

// 配置 DLA(深度學習加速器)
runtime.setDLACore(0);  // 使用 DLA 核心 0
int dlaCores = runtime.getNbDLACores();

// 設置最大線程數
runtime.setMaxThreads(4);

主要用途

  • 反序列化 TensorRT 引擎文件
  • 配置 DLA 加速器
  • 加載插件庫

5.3 Builder(模型構建器)

Builder 用於從 ONNX 模型構建 TensorRT 引擎。

using Builder builder = new Builder();

// 查詢平台能力
bool hasFP16 = builder.platformHasFastFp16();  // 是否支持 FP16
bool hasINT8 = builder.platformHasFastInt8();  // 是否支持 INT8
int maxDLABatch = builder.maxDLABatchSize();   // DLA 最大批大小

// 創建網絡定義(顯式批處理模式)
using NetworkDefinition network = builder.createNetworkV2(
    TrtNetworkDefinitionCreationFlag.kEXPLICIT_BATCH);

// 創建構建器配置
using BuilderConfig config = builder.createBuilderConfig();

// 創建優化配置文件(用於動態形狀)
using OptimizationProfile profile = builder.createOptimizationProfile();

// 構建序列化網絡
using HostMemory serialized = builder.buildSerializedNetwork(network, config);

// 保存引擎文件
using (FileStream fs = new FileStream("model.engine", FileMode.Create, FileAccess.Write))
{
    fs.Write(serialized.getByteData(), 0, (int)serialized.Size);
}

主要用途

  • 創建網絡定義和構建配置
  • 查詢硬件能力(FP16、INT8、DLA)
  • 構建 TensorRT 引擎
  • 註冊自定義插件

5.4 CudaEngine(推理引擎)

CudaEngine 是推理的核心對象,包含優化後的模型計算圖。

// 獲取張量信息
int numTensors = engine.getNbIOTensors();
string inputName = engine.getIOTensorName(0);    // 輸入張量名稱
string outputName = engine.getIOTensorName(1);   // 輸出張量名稱

Dims inputShape = engine.getTensorShape(inputName);
TrtDataType inputType = engine.getTensorDataType(inputName);

// 創建執行上下文
using ExecutionContext context = engine.createExecutionContext();
using ExecutionContext contextStatic = engine.createExecutionContext(
    TrtExecutionContextAllocationStrategy.kSTATIC);

// 序列化引擎
using HostMemory memory = engine.serialize();

// 查詢引擎屬性
int numLayers = engine.getNbLayers();
string name = engine.getName();
long deviceMemory = engine.getDeviceMemorySize();

主要用途

  • 查詢模型輸入輸出信息
  • 創建執行上下文
  • 序列化引擎
  • 性能分析

5.5 ExecutionContext(執行上下文)

ExecutionContext 管理單次推理的執行環境,支持異步推理和動態形狀。

// 綁定張量地址
Cuda1DMemory<float> input = new Cuda1DMemory<float>(3 * 1024 * 1024);
Cuda1DMemory<float> output = new Cuda1DMemory<float>(1 * 20 * 21504);
context.setInputTensorAddress("images", input.get());
context.setOutputTensorAddress("output0", output.get());

// 設置動態形狀
context.setinputShape("images", new Dims(1, 3, 1024, 1024));
Dims shape = context.getTensorShape("images");

// 執行推理(異步,使用 CUDA Stream)
using CudaStream stream = new CudaStream();
context.executeV3(stream);
stream.Synchronize();  // 等待完成

// 設置優化配置文件(動態形狀)
context.setOptimizationProfileAsync(0, stream);

// 調試功能
context.setDebugSync(true);

主要用途

  • 綁定輸入輸出張量
  • 設置動態形狀
  • 執行推理(異步)
  • 性能分析和調試

5.6 OnnxParser(ONNX 解析器)

OnnxParser 將 ONNX 模型轉換為 TensorRT 網絡定義。

// 解析 ONNX 文件
using NetworkDefinition network = build.createNetworkV2(TrtNetworkDefinitionCreationFlag.kEXPLICIT_BATCH);
using OnnxParser parser = new OnnxParser(network);
bool success = parser.parseFromFile("yolov8s-obb.onnx", verbosity: 2);

// 檢查算子支持
bool supportsConv = parser.supportsOperator("Conv");

// 子圖支持
long numSubgraphs = parser.getNbSubgraphs();
bool supported = parser.isSubgraphSupported(0);
long[] nodes = parser.getSubgraphNodes(0);

// 設置解析器標誌
parser.setFlag(TrtOnnxParserFlag.kNATIVE_INSTANCENORM);

主要用途

  • 解析 ONNX 模型
  • 檢查算子支持
  • 處理子圖

5.7 CUDA 內存管理

(1)設備內存(Cuda1DMemory

// 創建設備內存
using Cuda1DMemory<float> input = new Cuda1DMemory<float>(1000);
ulong numElements = input.SizeElements;
ulong numBytes = input.SizeBytes;
IntPtr ptr = input.DevicePointer;

// 同步數據傳輸
float[] hostData = new float[1000];
input.copyFromHost(hostData);   // 主機 → 設備
input.copyToHost(hostData);      // 設備 → 主機

// 異步數據傳輸
using CudaStream stream = new CudaStream();
input.copyFromHostAsync(hostData, stream);
input.copyToHostAsync(hostData, stream);

// 內存操作
input.memset(0);                 // 填充為 0
input.memsetAsync(0, stream);    // 異步填充

(2)CUDA 流(CudaStream)

// 創建流(帶優先級)
using CudaStream stream = new CudaStream();
using CudaStream streamHigh = new CudaStream(0, -1);  // 高優先級

// 同步操作
stream.Synchronize();  // 等待流完成
bool isComplete = stream.Query();  // 查詢是否完成

// 事件依賴
using CudaEvent cudaEvent = new CudaEvent();
stream.WaitEvent(cudaEvent);  // 等待事件

// 添加回調
stream.AddCallback((streamPtr, statue, userData) =>
{
    Console.WriteLine("Stream callback executed");
}, IntPtr.Zero, 0);

// CUDA Graph 捕獲
stream.BeginCapture(CudaStreamCaptureMode.Global);
// ... 執行操作 ...
CudaGraph_t graph = stream.EndCapture();

(3)CUDA 設備(CudaDevice)

// 獲取系統中啓用的 CUDA 兼容設備的數量
int nbDevices = CudaDevice.GetDeviceCount();

// 獲取指定設備的屬性
CudaDeviceProp properties = CudaDevice.GetDeviceProperties(deviceIdx);

// 設置執行設備
CudaDevice.SetDevice(device);

// 獲取有關設備的請求信息
int clockRate = CudaDevice.GetAttribute(CudaDeviceAttr.ClockRate, device);

六、完整使用示例

示例 1:獲取和設置設備信息

下面的代碼可以獲取當前設備的相關信息,同時可以設置推理設備。

using JYPPX.TensorRtSharp.Cuda;
using JYPPX.TensorRtSharp.Nvinfer;

namespace TestDemo
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // 指定默認使用的 GPU 設備索引
            // 在多 GPU 環境下,可以通過修改此變量來選擇特定的顯卡
            int device = 0;

            // 記錄日誌,標記設備信息查詢的開始
            Logger.Instance.INFO("=== Device Information ===");

            // 獲取當前系統中可見的 NVIDIA GPU 數量
            int nbDevices = CudaDevice.GetDeviceCount();

            // 檢查系統中是否存在可用的 GPU 設備
            if (nbDevices <= 0)
            {
                Logger.Instance.ERROR("Cannot find any available devices (GPUs)!");
                Environment.Exit(0);
            }

            // 打印所有可用設備的列表
            Logger.Instance.INFO("Available Devices: ");

            // 遍歷系統中的每一個 GPU
            for (int deviceIdx = 0; deviceIdx < nbDevices; ++deviceIdx)
            {
                // 獲取索引為 deviceIdx 的 GPU 的詳細屬性
                CudaDeviceProp tempProperties = CudaDevice.GetDeviceProperties(deviceIdx);

                // 打印設備 ID、設備名稱以及 UUID (唯一標識符)
                Logger.Instance.INFO($"  Device {deviceIdx}: \"{tempProperties.Name}\" UUID: {GetUuidString(tempProperties.Uuid)}");

                // 如果當前遍歷到的設備 ID 是我們想要使用的目標設備
                // 則將該設備的屬性保存下來,供後續使用
                if (deviceIdx == device)
                {
                    properties = tempProperties;
                }
            }

            // 安全檢查:確保請求的目標設備 ID 在有效範圍內
            if (device < 0 || device >= nbDevices)
            {
                Logger.Instance.ERROR($"Cannot find device ID {device}!");
                Environment.Exit(0);
            }

            // 將 CUDA 上下文設置到指定的 GPU 設備上
            CudaDevice.SetDevice(device);

            // 打印選定設備的詳細信息
            Logger.Instance.INFO($"Selected Device: {properties.Name}");
            Logger.Instance.INFO($"Selected Device ID: {device}");
            Logger.Instance.INFO($"Selected Device UUID: {GetUuidString(properties.Uuid)}");
            Logger.Instance.INFO($"Compute Capability: {properties.Major}.{properties.Minor}");
            Logger.Instance.INFO($"SMs: {properties.MultiProcessorCount}");
            Logger.Instance.INFO($"Device Global Memory: {(properties.TotalGlobalMem + 20)} MiB");
            Logger.Instance.INFO($"Shared Memory per SM: {(properties.SharedMemPerMultiprocessor >> 10)} KiB");
            Logger.Instance.INFO($"Memory Bus Width: {properties.MemoryBusWidth} bits (ECC {(properties.ECCEnabled != 0 ? "enabled" : "disabled")})");

            // 獲取並打印 GPU 核心時鐘頻率和顯存時鐘頻率
            int clockRate = CudaDevice.GetAttribute(CudaDeviceAttr.ClockRate, device);
            int memoryClockRate = CudaDevice.GetAttribute(CudaDeviceAttr.MemoryClockRate, device);

            Logger.Instance.INFO($"Application Compute Clock Rate: {clockRate / 1000000.0F} GHz");
            Logger.Instance.INFO($"Application Memory Clock Rate: {memoryClockRate / 1000000.0F} GHz");
        }

        /// <summary>
        /// 輔助方法:將 CudaUUID 結構體轉換為格式化的 GPU UUID 字符串
        /// 格式通常為:GPU-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
        /// </summary>
        public static string GetUuidString(CudaUUID uuid)
        {
            int kUUID_SIZE = uuid.Bytes.Length;
            var ss = new System.Text.StringBuilder();

            // 定義 UUID 的分段點,用於插入連字符 "-"
            int[] splits = { 0, 4, 6, 8, 10, kUUID_SIZE };

            // 添加固定的 "GPU" 前綴
            ss.Append("GPU");

            // 遍歷分段定義,格式化每一部分的字節
            for (int splitIdx = 0; splitIdx < splits.Length - 1; ++splitIdx)
            {
                ss.Append("-");
                for (int byteIdx = splits[splitIdx]; byteIdx < splits[splitIdx + 1]; ++byteIdx)
                {
                    ss.AppendFormat("{0:x2}", uuid.Bytes[byteIdx]);
                }
            }

            return ss.ToString();
        }
    }
}

程序運行結果:

設備信息輸出

💡 注意:不同的設備輸出會有不同,以具體設備輸出為準。

🔗程序路徑鏈接:完整程序已經上傳到GitHub,請自行下載,鏈接為:

https://github.com/guojin-yan/TensorRT-CSharp-API/tree/TensorRtSharp3.0/samples/SetCudaDeviceInfo

示例 2:ONNX 轉 Engine 模型

下面是按照官方模型轉換代碼編寫的一個簡單的轉換代碼:

using JYPPX.TensorRtSharp.Cuda;
using JYPPX.TensorRtSharp.Nvinfer;

namespace OnnxToEngine
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // === 配置 TensorRT 日誌回調 ===
            // 定義一個委託,用於處理 TensorRT 內部產生的日誌消息
            LogCallbackFunction _callbackDelegate = (message) =>
            {
                Console.WriteLine(message);
            };

            // 將自定義的回調函數註冊給 TensorRT 的全局 Logger 實例
            Logger.Instance.SetCallback(_callbackDelegate);

            // 設置日誌的嚴重性級別閾值
            // LoggerSeverity.kINFO: 打印信息、警告和錯誤
            Logger.Instance.SetThreshold(LoggerSeverity.kINFO);

            // 1. 創建 TensorRT Builder (構建器)
            Builder build = new Builder();

            // 2. 創建網絡定義 (Network Definition)
            // 顯式批處理 標誌表示網絡定義中顯式包含批處理維度
            NetworkDefinition networkDefinition = build.createNetworkV2(TrtNetworkDefinitionCreationFlag.kEXPLICIT_BATCH);

            // 3. 創建構建器配置
            BuilderConfig builderConfig = build.createBuilderConfig();

            // 4. 創建 ONNX 解析器
            OnnxParser onnxParser = new OnnxParser(networkDefinition);

            // 指定待轉換的 ONNX 模型文件路徑
            string modelpath = "yolo11s-obb.onnx";

            // 5. 解析 ONNX 模型文件
            // 參數 2: 日誌級別 (1=ERROR, 2=WARNING, 3=INFO, 4=VERBOSE)
            if (onnxParser.parseFromFile(modelpath, 2) == false)
            {
                Console.WriteLine($"parse onnx model failed");
                return;
            }

            // 6. 設置構建精度標誌
            // kFP16: 啓用半精度 (FP16) 推理模式
            builderConfig.setFlag(TrtBuilderFlag.kFP16);

            // 7. 創建 CUDA 流
            CudaStream cudaStream = new CudaStream();

            // 8. 設置優化配置文件的流
            builderConfig.setProfileStream(cudaStream);

            // 9. 構建並序列化網絡
            // 這是一個耗時較長的過程,因為 TensorRT 會進行內核自動調優、層融合等優化
            HostMemory hostMemory = build.buildSerializedNetwork(networkDefinition, builderConfig);

            // 10. 保存 Engine 到磁盤
            string filePath = "yolo11s-obb.engine";
            using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
            {
                fs.Write(hostMemory.getByteData(), 0, (int)hostMemory.Size);
            }

            Console.WriteLine("Engine saved successfully!");
        }
    }
}

程序運行結果:

ONNX 轉換結果

🔗程序路徑鏈接:完整程序已經上傳到GitHub,請自行下載,鏈接為:

https://github.com/guojin-yan/TensorRT-CSharp-API/tree/TensorRtSharp3.0/samples/OnnxToEngine

💡 使用 trtexec 工具轉換模型(推薦)

當前 ONNX 轉 Engine 代碼由於沒有進行優化,轉換速度會較慢。建議使用 TensorRT SDK 自帶的 trtexec.exe 工具轉換模型

trtexec 使用方式

(1)使用 CMD 切換到工具目錄

該工具存放在下載的 TensorRT 庫中:

trtexec 位置

打開 CMD 並切換到該路徑:

CMD 切換路徑

(2)固定形狀模型轉換指令

對於形狀固定的模型,直接輸入常規指令轉換即可:

trtexec.exe --onnx=yolov8s-obb.onnx --saveEngine=yolov8s-obb.engine --fp16 --workspace=1024

參數説明:

  • --onnx=yolov8s-obb.onnx:指定輸入的 ONNX 模型文件路徑
  • --saveEngine=yolov8s-obb.engine:指定輸出的 Engine 文件保存路徑
  • --fp16:啓用 FP16 精度(可選)
  • --workspace=1024:指定最大工作空間,單位 MB(可選)

trtexec 轉換中

(3)動態形狀模型轉換指令

對於輸入形狀是動態的情況,轉換時要設置形狀參數:

trtexec.exe --onnx=yolov8s-obb_b.onnx --saveEngine=yolov8s-obb_b.engine --fp16 --minShapes=images:1x3x1024x1024 --optShapes=images:8x3x1024x1024 --maxShapes=images:24x3x1024x1024

參數説明:

  • --minShapes=images:1x3x1024x1024:最小輸入形狀
  • --optShapes=images:8x3x1024x1024:最優輸入形狀(Engine 會為此形狀優化)
  • --maxShapes=images:24x3x1024x1024:最大輸入形狀

動態形狀轉換

多輸入模型轉換指令:

trtexec --onnx=model.onnx --minShapes=input1:1x3x224x224,input2:1x256 --optShapes=input1:4x3x224x224,input2:4x256 --maxShapes=input1:8x3x224x224,input2:8x256

示例 3:YOLO 目標檢測

下面是一個完整的 YOLO 目標檢測示例,展示從模型構建到推理的全流程。

⚠️ 由於代碼較長,此處僅展示核心思路。完整代碼請參考項目示例。

using JYPPX.TensorRtSharp.Cuda;
using JYPPX.TensorRtSharp.Nvinfer;
using OpenCvSharp;
using OpenCvSharp.Dnn;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace YoloDetInfer
{
    internal class Program
    {
        // ================= 配置參數 =================
        // 模型輸入尺寸 (寬=高)
        private const int InputSize = 640;


        // 建議根據實際模型動態獲取或使用 Netron 查看
        private const int OutputSize = 8400;

        // 模型類別數 (根據您的具體數據集修改,此處假設為15類)
        private const int CategoryNum = 80;

        // 置信度閾值
        private const float ConfThreshold = 0.25f;

        // NMS IOU 閾值
        private const float NmsThreshold = 0.3f;

        static void Main(string[] args)
        {
            //  ============= 配置 TensorRT 日誌回調 =============
            // 定義一個委託,用於處理 TensorRT 內部產生的日誌消息。
            // 這允許我們將 C++ 層面的日誌輸出到 C# 的控制枱。
            LogCallbackFunction _callbackDelegate = (message) =>
            {
                Console.WriteLine(message);
            };

            // 將自定義的回調函數註冊給 TensorRT 的全局 Logger 實例。
            Logger.Instance.SetCallback(_callbackDelegate);

            // 設置日誌的嚴重性級別閾值。
            // LoggerSeverity.kINFO: 打印信息、警告和錯誤。
            // 開發調試階段通常設為 kINFO 或 kVERBOSE;生產環境可設為 kWARNING 或 kERROR 以減少輸出。
            Logger.Instance.SetThreshold(LoggerSeverity.kINFO);

            string enginePath = "yolov8s.engine";
            string imagePath = "bus.jpg";


            // ================= 1. 加載 TensorRT Engine =================
            // 使用 using 語句確保文件流正確關閉
            byte[] engineData;
            using (FileStream fs = new FileStream(enginePath, FileMode.Open, FileAccess.Read))
            using (BinaryReader br = new BinaryReader(fs))
            {
                engineData = br.ReadBytes((int)fs.Length);
            }

            // 反序列化 Engine
            // Runtime 必須在 Engine 生命週期內保持存活,通常建議設為全局或靜態,或者確保它最後釋放
            Runtime runtime = new Runtime();

            // 創建 CudaEngine (此處使用 using 確保推理完成後引擎被銷燬)
            using (CudaEngine cudaEngine = runtime.deserializeCudaEngineByBlob(engineData, (ulong)engineData.Length))
            {
                // ================= 2. 初始化推理上下文與顯存 =================
                // 創建執行上下文
                using (JYPPX.TensorRtSharp.Nvinfer.ExecutionContext executionContext = cudaEngine.createExecutionContext(TrtExecutionContextAllocationStrategy.kSTATIC))
                using (CudaStream cudaStream = new CudaStream()) // 創建 CUDA 流用於異步執行
                {
                    // 獲取輸入維度信息 (用於校驗)
                    Dims inputDims = executionContext.getTensorShape("images");
                    Logger.Instance.INFO($"Input Shape: {inputDims.d[0]}x{inputDims.d[1]}x{inputDims.d[2]}x{inputDims.d[3]}");

                    // 計算所需顯存大小
                    // 輸入: Batch=1, Channel=3, Height=640, Width=640
                    ulong inputSizeInBytes = 1 * 3 * InputSize * InputSize;
                    // 輸出: Batch=1, Channels=CategoryNum+4(box)+1(angle), Num=8400
                    int outputChannels = CategoryNum + 4; // 4座標 + N類別
                    ulong outputSizeInBytes = (ulong)(1 * outputChannels * OutputSize);

                    Stopwatch sw = new Stopwatch();
                    // 分配 GPU 顯存
                    using (Cuda1DMemory<float> inputGpuMemory = new Cuda1DMemory<float>(inputSizeInBytes))
                    using (Cuda1DMemory<float> outputGpuMemory = new Cuda1DMemory<float>(outputSizeInBytes))
                    {
                        // 綁定顯存地址到 TensorRT 上下文
                        executionContext.setInputTensorAddress("images", inputGpuMemory.get());
                        executionContext.setOutputTensorAddress("output0", outputGpuMemory.get());
                        // 預熱推理 (可選,但推薦,尤其是首次推理時)
                        executionContext.executeV3(cudaStream);
                        cudaStream.Synchronize();
                        // ================= 3. 圖像預處理 =================
                        Mat img = Cv2.ImRead(imagePath);
                        if (img.Empty())
                        {
                            Logger.Instance.INFO("Image not found!");
                            return;
                        }

                        sw.Start();
                        float[] inputData = PreProcess(img, out float scale, out int xOffset, out int yOffset);
                        sw.Stop();
                        Logger.Instance.INFO($"Pre-processing time: {sw.ElapsedMilliseconds} ms");
                        // ================= 4. 推理 =================
                        // 準備主機內存接收結果
                        float[] outputData = new float[outputChannels * OutputSize];


                        sw.Restart();
                        // 將數據從主機 拷貝到設備
                        inputGpuMemory.copyFromHostAsync(inputData, cudaStream);

                        // 執行推理 (enqueueV3 是異步的)
                        executionContext.executeV3(cudaStream);
                        // 等待推理完成
                        cudaStream.Synchronize();

           

                        // 將結果從設備 拷貝回主機
                        // 這裏的拷貝是同步的,會等待 GPU 計算完成
                        outputGpuMemory.copyToHostAsync(outputData, cudaStream);
                        sw.Stop();
                        Logger.Instance.INFO($"Inference time: {sw.ElapsedMilliseconds} ms");
                        // ================= 5. 後處理 =================

                        sw.Restart();
                        List<DetData> results = PostProcess(outputData, scale, xOffset, yOffset);
                        sw.Stop();
                        Logger.Instance.INFO($"Post-processing time: {sw.ElapsedMilliseconds} ms");

                        // ================= 6. 結果可視化 =================
                        Mat resultImg = DrawDetResult(results, img);
                        Cv2.ImShow("YOLO11-DET Result", resultImg);
                        Cv2.WaitKey(0);
                    }
                }
            }
        }

        /// <summary>
        /// 圖像預處理:Letterbox 縮放、歸一化、HWC 轉 CHW
        /// </summary>
        private static float[] PreProcess(Mat img, out float scale, out int xOffset, out int yOffset)
        {
            // 轉換顏色空間 BGR -> RGB
            Mat rgbImg = new Mat();
            Cv2.CvtColor(img, rgbImg, ColorConversionCodes.BGR2RGB);

            // 計算 Letterbox 縮放比例
            int maxDim = Math.Max(rgbImg.Width, rgbImg.Height);
            scale = (float)maxDim / InputSize;

            // 計算縮放後的尺寸
            int newWidth = (int)(rgbImg.Width / scale);
            int newHeight = (int)(rgbImg.Height / scale);

            // Resize 圖像
            Mat resizedImg = new Mat();
            Cv2.Resize(rgbImg, resizedImg, new Size(newWidth, newHeight));

            // 創建黑色背景 Canvas (InputSize x InputSize)
            Mat paddedImg = Mat.Zeros(InputSize, InputSize, MatType.CV_8UC3);

            // 計算粘貼位置 (居中)
            xOffset = (InputSize - newWidth) / 2;
            yOffset = (InputSize - newHeight) / 2;

            // 將圖像拷貝到 Canvas 中央
            Rect roi = new Rect(xOffset, yOffset, newWidth, newHeight);
            resizedImg.CopyTo(new Mat(paddedImg, roi));

            // 歸一化 (0-255 -> 0-1) 並轉為 float 類型
            Mat floatImg = new Mat();
            paddedImg.ConvertTo(floatImg, MatType.CV_32FC3, 1.0 / 255.0);

            // HWC 轉 CHW 並展平為一維數組
            Mat[] channels = Cv2.Split(floatImg);
            float[] chwData = new float[3 * InputSize * InputSize];

            // 拷貝數據:R通道 -> C通道 -> B通道 (OpenCV Split 出來順序是 B, G, R,對應索引 0, 1, 2)
            int channelSize = InputSize * InputSize;
            // 將 R, G, B 依次拷入數組
            Marshal.Copy(channels[0].Data, chwData, 0, channelSize); // R
            Marshal.Copy(channels[1].Data, chwData, channelSize, channelSize); // G
            Marshal.Copy(channels[2].Data, chwData, channelSize * 2, channelSize); // B

            // 釋放臨時 Mat
            rgbImg.Dispose();
            resizedImg.Dispose();
            paddedImg.Dispose();
            floatImg.Dispose();
            foreach (var c in channels) c.Dispose();

            return chwData;
        }

        /// <summary>
        /// 後處理:解析 TensorRT 輸出、NMS 過濾
        /// </summary>
        private static List<DetData> PostProcess(float[] result, float scale, int xOffset, int yOffset)
        {
            List<Rect> boxes = new List<Rect>();
            List<float> confidences = new List<float>();
            List<int> classIds = new List<int>();

            // 遍歷所有預測框 (OutputSize)
            // 數據佈局: [4(box) + 80(classes)] * OutputSize
            // 展平數組中,同一屬性的數據是連續存儲的,例如所有 cx 在一起,所有 cy 在在一起...
            int stride = OutputSize; // 步長,不同屬性在數組中的偏移量

            for (int i = 0; i < OutputSize; i++)
            {
                // 查找最大類別概率及其索引
                float maxConf = 0;
                int maxClassId = -1;

                // 遍歷類別
                for (int c = 0; c < CategoryNum; c++)
                {
                    // 數組索引:(座標/角度偏移量 + 類別偏移) * 框索引
                    // 注意:原始代碼中 result[outputSize * j + i] 這種訪問方式基於 Transposed 數據佈局
                    float conf = result[(4 + c) * stride + i];
                    if (conf > maxConf)
                    {
                        maxConf = conf;
                        maxClassId = c;
                    }
                }

                // 置信度過濾
                if (maxConf > ConfThreshold)
                {
                    // 提取座標 (cx, cy, w, h)
                    float cx = result[0 * stride + i];
                    float cy = result[1 * stride + i];
                    float w = result[2 * stride + i];
                    float h = result[3 * stride + i];
                    // 還原座標到原圖尺寸
                    int rx = (int)((cx - xOffset - 0.5 * w) * scale);
                    int ry = (int)((cy - yOffset - 0.5 * h) * scale);
                    int rw = (int)(w * scale);
                    int rh = (int)(h * scale);

                    boxes.Add(new Rect(rx, ry, rw, rh));
                    confidences.Add(maxConf);
                    classIds.Add(maxClassId);
                }
            }

            // 執行 NMS (旋轉框 NMS)
            // OpenCV 的 NMSBoxes 支持 RotatedRect
            int[] indices;
            CvDnn.NMSBoxes(boxes, confidences, ConfThreshold, NmsThreshold, out indices);

            List<DetData> finalResults = new List<DetData>();
            foreach (int idx in indices)
            {
                finalResults.Add(new DetData
                {
                    index = classIds[idx],
                    score = confidences[idx],
                    box = boxes[idx]
                });
            }

            return finalResults;
        }

        /// <summary>
        /// 繪製檢測結果(水平矩形框)
        /// </summary>
        /// <param name="results">檢測結果列表</param>
        /// <param name="image">原始圖像</param>
        /// <returns>繪製後的圖像</returns>
        public static Mat DrawDetResult(List<DetData> results, Mat image)
        {
            // 克隆圖像以免修改原圖
            Mat mat = image.Clone();

            foreach (var item in results)
            {
                // 1. 繪製矩形框
                // Rect 結構包含 X, Y, Width, Height
                Cv2.Rectangle(mat, item.box, new Scalar(0, 255, 0), thickness: 2);
                // 2. 準備標籤文本 (類別ID - 置信度)
                string label = $"{item.index} - {item.score:F2}";
                // 3. 計算文本的尺寸,用於繪製背景
                int baseLine = 1;
                Size textSize = Cv2.GetTextSize(label, HersheyFonts.HersheySimplex, 0.6, 1, out baseLine);
                // 4. 繪製標籤背景(半透明黑色矩形),防止文字與背景混淆
                // 位置:矩形左上角略微上移,或者直接貼着左上角
                Point labelPosition = new Point(item.box.X, item.box.Y - (int)textSize.Height - 5);

                // 確保標籤不畫出圖像邊界
                if (labelPosition.Y < 0) labelPosition.Y = item.box.Y + (int)textSize.Height + 5;
                Rect labelBgRect = new Rect(labelPosition.X,
                                            labelPosition.Y - (int)textSize.Height, // OpenCV GetTextSize 返回的高度是基線到底部的距離,需調整
                                            (int)textSize.Width,
                                            (int)textSize.Height + (int)baseLine);
                // 如果背景框也在圖像範圍內,則繪製
                // 注意:這裏簡化處理,直接畫在框上方
                Cv2.Rectangle(mat,
                               new Point(item.box.X, item.box.Y - textSize.Height - 5),
                               new Point(item.box.X + textSize.Width, item.box.Y),
                               new Scalar(0, 255, 0),
                               thickness: -1); // -1 表示填充
                // 5. 繪製文本(白色文字)
                Cv2.PutText(mat,
                            label,
                            new Point(item.box.X, item.box.Y - 5),
                            HersheyFonts.HersheySimplex,
                            0.6,
                            new Scalar(0, 0, 0),
                            1);
            }
            return mat;
        }

        public class DetData
        {
            public int index;
            public float score;
            public Rect box;
        }
    }
}


程序運行結果:

推理結果

檢測結果

性能測試結果:

Batch Size 1 2 4 6 8 10 12 14 16 18 20 22 24
前處理 (ms) 9 13 27 38 56 59 63 83 96 105 118 130 144
模型推理 (ms) 7 15 24 36 48 60 96 84 93 153 120 133 203
後處理 (ms) 25 26 26 26 28 27 27 28 28 28 27 31 29

🔗程序路徑鏈接:完整程序已經上傳到GitHub,請自行下載,鏈接為:

https://github.com/guojin-yan/TensorRT-CSharp-API/tree/TensorRtSharp3.0/samples/YoloDetInfer

同時也提供了YoloOBB模型的推理程序,請自行下載,鏈接為:

https://github.com/guojin-yan/TensorRT-CSharp-API/tree/TensorRtSharp3.0/samples/YoloObbInfer

示例 4:動態形狀推理

對於輸入尺寸可變的模型,需要根據輸入的數據配置動態形狀。

核心代碼:

using JYPPX.TensorRtSharp.Cuda;
using JYPPX.TensorRtSharp.Nvinfer;
using OpenCvSharp;
using OpenCvSharp.Dnn;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace YoloObbBatchInfer
{
    internal class Program
    {
        // ================= 配置參數 =================
        // 模型輸入尺寸 (寬=高)
        private const int InputSize = 1024;
        // 建議根據實際模型動態獲取或使用 Netron 查看
        private const int OutputSize = 21504;
        // 模型類別數 (根據您的具體數據集修改,此處假設為15類)
        private const int CategoryNum = 15;
        // 置信度閾值
        private const float ConfThreshold = 0.25f;

        // NMS IOU 閾值
        private const float NmsThreshold = 0.3f;
        private const int MaxBatchSize = 24;
        static void Main(string[] args)
        {
            //  ============= 配置 TensorRT 日誌回調 =============
            // 定義一個委託,用於處理 TensorRT 內部產生的日誌消息。
            // 這允許我們將 C++ 層面的日誌輸出到 C# 的控制枱。
            LogCallbackFunction _callbackDelegate = (message) =>
            {
                Console.WriteLine(message);
            };
            // 將自定義的回調函數註冊給 TensorRT 的全局 Logger 實例。
            Logger.Instance.SetCallback(_callbackDelegate);

            // 設置日誌的嚴重性級別閾值。
            // LoggerSeverity.kINFO: 打印信息、警告和錯誤。
            // 開發調試階段通常設為 kINFO 或 kVERBOSE;生產環境可設為 kWARNING 或 kERROR 以減少輸出。
            Logger.Instance.SetThreshold(LoggerSeverity.kINFO);

            string enginePath = "yolov8s-obb_b.engine";
            string[] imagePaths = { 
                "P0006.png" , "P0016.png", "P0456.png", "P0813.png"};
            // ================= 1. 加載 TensorRT Engine =================
            // 使用 using 語句確保文件流正確關閉
            byte[] engineData;
            using (FileStream fs = new FileStream(enginePath, FileMode.Open, FileAccess.Read))
            using (BinaryReader br = new BinaryReader(fs))
            {
                engineData = br.ReadBytes((int)fs.Length);
            }

            // 反序列化 Engine
            // Runtime 必須在 Engine 生命週期內保持存活,通常建議設為全局或靜態,或者確保它最後釋放
            Runtime runtime = new Runtime();
            runtime.setMaxThreads(10);
            // 創建 CudaEngine (此處使用 using 確保推理完成後引擎被銷燬)
            using (CudaEngine cudaEngine = runtime.deserializeCudaEngineByBlob(engineData, (ulong)engineData.Length))
            {
                // ================= 2. 初始化推理上下文與顯存 =================
                // 創建執行上下文
                using (JYPPX.TensorRtSharp.Nvinfer.ExecutionContext executionContext = cudaEngine.createExecutionContext(TrtExecutionContextAllocationStrategy.kSTATIC))
                using (CudaStream cudaStream = new CudaStream()) // 創建 CUDA 流用於異步執行
                {
                    // 獲取輸入維度信息 (用於校驗)
                    Dims inputDims = executionContext.getTensorShape("images");
                    Logger.Instance.INFO($"Input Shape: {inputDims.d[0]}x{inputDims.d[1]}x{inputDims.d[2]}x{inputDims.d[3]}");

                    // 計算所需顯存大小
                    // 輸入: Batch=1, Channel=3, Height=1024, Width=1024
                    ulong inputSizeInBytes = MaxBatchSize * 3 * InputSize * InputSize;
                    // 輸出: Batch=1, Channels=CategoryNum+4(box)+1(angle), Num=8400
                    int outputChannels = CategoryNum + 5; // 4座標 + 1角度 + N類別
                    ulong outputSizeInBytes = (ulong)(MaxBatchSize * outputChannels * OutputSize);

                    Stopwatch sw = new Stopwatch();
                    // 分配 GPU 顯存
                    using (Cuda1DMemory<float> inputGpuMemory = new Cuda1DMemory<float>(inputSizeInBytes))
                    using (Cuda1DMemory<float> outputGpuMemory = new Cuda1DMemory<float>(outputSizeInBytes))
                    {
                        // 綁定顯存地址到 TensorRT 上下文
                        executionContext.setInputTensorAddress("images", inputGpuMemory.get());
                        executionContext.setOutputTensorAddress("output0", outputGpuMemory.get());

                        // 關鍵一步,修改本次推理的形狀
                        executionContext.setinputShape("images", new Dims(imagePaths.Count(), 3, 1024, 1024));
                        // 預熱推理 (可選,但推薦,尤其是首次推理時)
                        executionContext.executeV3(cudaStream);
                        cudaStream.Synchronize();

                        // ================= 3. 圖像預處理 =================
                        List<Mat> images = new List<Mat>();
                        foreach (var path in imagePaths) 
                        {
                            Mat img = Cv2.ImRead(path);
                            if (img.Empty())
                            {
                                Logger.Instance.INFO("Image not found!");
                                return;
                            }
                            images.Add(img);
                        }

                        (float[] inputData1, float[] scales1, int[] xOffsets1, int[] yOffsets1) = PreProcessBatch(images);
                        sw.Start();
                        (float[] inputData, float[] scales, int[] xOffsets, int[] yOffsets) = PreProcessBatch(images);
                        sw.Stop();
                        Logger.Instance.INFO($"Pre-processing time: {sw.ElapsedMilliseconds} ms");
                        // ================= 4. 推理 =================

                        // 準備主機內存接收結果
                        float[] outputData1 = new float[imagePaths.Count() * outputChannels * OutputSize];
                        // 將數據從主機 拷貝到設備
                        inputGpuMemory.copyFromHostAsync(inputData, cudaStream);

                        // 執行推理 (enqueueV3 是異步的)
                        executionContext.executeV3(cudaStream);
                        // 等待推理完成
                        cudaStream.Synchronize();
                        // 將結果從設備 拷貝回主機
                        // 這裏的拷貝是同步的,會等待 GPU 計算完成
                        outputGpuMemory.copyToHostAsync(outputData1, cudaStream);

                        sw.Restart();
                        // 準備主機內存接收結果
                        float[] outputData = new float[imagePaths.Count() * outputChannels * OutputSize];
                        // 將數據從主機 拷貝到設備
                        inputGpuMemory.copyFromHostAsync(inputData, cudaStream);

                        // 執行推理 (enqueueV3 是異步的)
                        executionContext.executeV3(cudaStream);
                        // 等待推理完成
                        cudaStream.Synchronize();
                        // 將結果從設備 拷貝回主機
                        // 這裏的拷貝是同步的,會等待 GPU 計算完成
                        outputGpuMemory.copyToHostAsync(outputData, cudaStream);

                        sw.Stop();
                        Logger.Instance.INFO($"Inference time: {sw.ElapsedMilliseconds} ms");
                        // ================= 5. 後處理 =================
                        List<List<ObbData>> results1 = PostProcessBatch(outputData, scales, xOffsets, yOffsets);
                        sw.Restart();
                        List<List<ObbData>> results = PostProcessBatch(outputData, scales, xOffsets, yOffsets);
                        sw.Stop();
                        Logger.Instance.INFO($"Post-processing time: {sw.ElapsedMilliseconds} ms");

                        // ================= 6. 結果可視化 =================
                        List<Mat> resultMats = new List<Mat>();
                        for(int i = 0; i < results.Count; ++i)
                        {
                            resultMats.Add(DrawObbResult(results[i], images[i]));
                        }
                        Mat putResultImgs = StitchHorizontalWithPadding(resultMats);
                        Cv2.ImWrite("YOLO11-OBB Result.png", putResultImgs);
                        Cv2.ImShow("YOLO11-OBB Result", putResultImgs);
                        Cv2.WaitKey(0);
                    }
                }
            }
        }

        /// <summary>
        /// 圖像預處理:Letterbox 縮放、歸一化、HWC 轉 CHW
        /// </summary>
        private static (float[], float[] ,  int[] , int[] ) PreProcessBatch(List<Mat> imgs)
        {
            int dataLen = 3 * InputSize * InputSize;
            float[] chwData = new float[imgs.Count * dataLen];
            float[] scales = new float[imgs.Count];
            int[] xOffsets = new int[imgs.Count];
            int[]  yOffsets = new int[imgs.Count];
            Parallel.For(0, imgs.Count, i =>
            {
                Mat img = imgs[i];
                // 轉換顏色空間 BGR -> RGB
                Mat rgbImg = new Mat();
                Cv2.CvtColor(img, rgbImg, ColorConversionCodes.BGR2RGB);

                // 計算 Letterbox 縮放比例
                int maxDim = Math.Max(rgbImg.Width, rgbImg.Height);
                scales[i] = (float)maxDim / InputSize;

                // 計算縮放後的尺寸
                int newWidth = (int)(rgbImg.Width / scales[i]);
                int newHeight = (int)(rgbImg.Height / scales[i]);

                // Resize 圖像
                Mat resizedImg = new Mat();
                Cv2.Resize(rgbImg, resizedImg, new Size(newWidth, newHeight));

                // 創建黑色背景 Canvas (InputSize x InputSize)
                Mat paddedImg = Mat.Zeros(InputSize, InputSize, MatType.CV_8UC3);

                // 計算粘貼位置 (居中)
                xOffsets[i] = (InputSize - newWidth) / 2;
                yOffsets[i] = (InputSize - newHeight) / 2;

                // 將圖像拷貝到 Canvas 中央
                Rect roi = new Rect(xOffsets[i], yOffsets[i], newWidth, newHeight);
                resizedImg.CopyTo(new Mat(paddedImg, roi));

                // 歸一化 (0-255 -> 0-1) 並轉為 float 類型
                Mat floatImg = new Mat();
                paddedImg.ConvertTo(floatImg, MatType.CV_32FC3, 1.0 / 255.0);

                // HWC 轉 CHW 並展平為一維數組
                Mat[] channels = Cv2.Split(floatImg);


                // 拷貝數據:R通道 -> C通道 -> B通道 (OpenCV Split 出來順序是 B, G, R,對應索引 0, 1, 2)
                int channelSize = InputSize * InputSize;
                // 將 R, G, B 依次拷入數組
                Marshal.Copy(channels[0].Data, chwData, dataLen * i, channelSize); // R
                Marshal.Copy(channels[1].Data, chwData, dataLen * i + channelSize, channelSize); // G
                Marshal.Copy(channels[2].Data, chwData, dataLen * i + channelSize * 2, channelSize); // B

                // 釋放臨時 Mat
                rgbImg.Dispose();
                resizedImg.Dispose();
                paddedImg.Dispose();
                floatImg.Dispose();
                foreach (var c in channels) c.Dispose();
            });



            return (chwData, scales, xOffsets, yOffsets);
        }

        /// <summary>
        /// 後處理:解析 TensorRT 輸出、NMS 過濾
        /// </summary>
        private static List<List<ObbData>> PostProcessBatch(float[] result, float[] scales, int[] xOffsets, int[] yOffsets)
        {
            List<ObbData>[] obbDatas = new List<ObbData>[scales.Length];

            Parallel.For(0, scales.Length, b =>
            {
                List<RotatedRect> boxes = new List<RotatedRect>();
                List<float> confidences = new List<float>();
                List<int> classIds = new List<int>();

                // 遍歷所有預測框 (OutputSize)
                // 數據佈局: [4(box) + 15(classes) + 1(angle)] * OutputSize
                // 展平數組中,同一屬性的數據是連續存儲的,例如所有 cx 在一起,所有 cy 在在一起...
                int stride = OutputSize; // 步長,不同屬性在數組中的偏移量

                int resultDataOffset = OutputSize * (CategoryNum + 5) * b;

                for (int i = 0; i < OutputSize; i++)
                {
                    // 查找最大類別概率及其索引
                    float maxConf = 0;
                    int maxClassId = -1;

                    // 遍歷類別 
                    for (int c = 0; c < CategoryNum; c++)
                    {
                        // 數組索引:(座標/角度偏移量 + 類別偏移) * 框索引
                        // 注意:原始代碼中 result[outputSize * j + i] 這種訪問方式基於 Transposed 數據佈局
                        float conf = result[(4 + c) * stride + i + resultDataOffset];
                        if (conf > maxConf)
                        {
                            maxConf = conf;
                            maxClassId = c;
                        }
                    }

                    // 置信度過濾
                    if (maxConf > ConfThreshold)
                    {
                        // 提取座標 (cx, cy, w, h)
                        float cx = result[0 * stride + i + resultDataOffset];
                        float cy = result[1 * stride + i + resultDataOffset];
                        float w = result[2 * stride + i + resultDataOffset];
                        float h = result[3 * stride + i + resultDataOffset];

                        // 提取角度 (通常在第 5 個位置,即類別之前)
                        float angleRad = result[(CategoryNum + 4) * stride + i + resultDataOffset];

                        // 還原座標到原圖尺寸
                        float rx = (cx - xOffsets[b]) * scales[b];
                        float ry = (cy - yOffsets[b]) * scales[b];
                        float rw = w * scales[b];
                        float rh = h * scales[b];

                        // 將弧度轉換為角度
                        // Normalize angle to [-π/2, π/2] range
                        // 將角度歸一化到[-π/2, π/2]範圍
                        if (angleRad >= Math.PI && angleRad <= 0.75 * Math.PI)
                        {
                            angleRad -= (float)Math.PI;
                        }
                        float angleDeg = angleRad * (float)(180f / Math.PI);  // Convert to degrees/轉換為角度制

                        boxes.Add(new RotatedRect(new Point2f(rx, ry), new Size2f(rw, rh), angleDeg));
                        confidences.Add(maxConf);
                        classIds.Add(maxClassId);
                    }
                }

                // 執行 NMS (旋轉框 NMS)
                // OpenCV 的 NMSBoxes 支持 RotatedRect
                int[] indices;
                CvDnn.NMSBoxes(boxes, confidences, ConfThreshold, NmsThreshold, out indices);

                List<ObbData> finalResults = new List<ObbData>();
                foreach (int idx in indices)
                {
                    finalResults.Add(new ObbData
                    {
                        index = classIds[idx],
                        score = confidences[idx],
                        box = boxes[idx]
                    });
                }
                obbDatas[b] = finalResults;
            });

           

            return obbDatas.Select(x => x?.ToList() ?? new List<ObbData>()).ToList();
        }

        /// <summary>
        /// 繪製旋轉檢測結果
        /// </summary>
        public static Mat DrawObbResult(List<ObbData> results, Mat image)
        {
            // 克隆圖像以免修改原圖
            Mat mat = image.Clone();

            foreach (var item in results)
            {
                // 獲取旋轉矩形的四個頂點
                Point2f[] points = item.box.Points();

                // 繪製多邊形框
                for (int j = 0; j < 4; j++)
                {
                    Cv2.Line(mat, (Point)points[j], (Point)points[(j + 1) % 4],
                            new Scalar(0, 255, 0), 2);
                }

                // 繪製標籤 (類別 - 置信度)
                string label = $"{item.index} - {item.score:F2}";
                Point2f textPos = points[0]; // 左上角

                Cv2.PutText(mat, label, (Point)textPos, HersheyFonts.HersheySimplex, 0.8,
                            new Scalar(255, 0, 0), 2);
            }

            return mat;
        }

        public class ObbData
        {
            public int index;
            public float score;
            public RotatedRect box;
        }


        /// <summary>
        /// 智能水平拼接:自動處理高度不一致的圖片
        /// </summary>
        /// <param name="images">圖片列表</param>
        /// <param name="backgroundColor">填充背景顏色,默認為黑色</param>
        /// <returns>拼接後的 Mat</returns>
        public static Mat StitchHorizontalWithPadding(List<Mat> images, Scalar? backgroundColor = null)
        {
            if (images == null || images.Count == 0)
                return new Mat();
            // 1. 找到所有圖片中的最大高度
            int maxHeight = images.Max(img => img.Rows);
            // 計算總寬度
            int totalWidth = images.Sum(img => img.Cols);
            // 2. 準備結果畫布
            Mat result = new Mat(maxHeight, totalWidth, images[0].Type(), backgroundColor ?? Scalar.Black);
            // 3. 將每一張圖片複製到畫布的對應位置
            int currentX = 0; // 當前 X 軸偏移量
            foreach (var img in images)
            {
                if (img.Empty()) continue;
                // 計算當前圖片需要垂直偏移多少(底部對齊邏輯)
                // 如果想頂部對齊,yOffset = 0
                // 如果想居中,yOffset = (maxHeight - img.Rows) / 2
                int yOffset = maxHeight - img.Rows;
                // 定義 ROI (感興趣區域)
                Rect roi = new Rect(currentX, yOffset, img.Cols, img.Rows);

                // 將原圖片拷貝到結果圖的 ROI 區域
                img.CopyTo(new Mat(result, roi));
                // 移動 X 軸指針
                currentX += img.Cols;
            }
            return result;
        }
    }
}


下圖為上述程序運行後的輸出,模型輸入形狀為 -1x3x1024x1024,其中Batch Size為動態輸入;項目示例使用了四張圖片進行同時推理,開啓並行處理後,四張圖像預處理時間僅用21ms,推理時間為25ms,後處理時間為26ms,累計時間為72ms.

image-20260109225451392

下圖為推理結果展示:

YOLO11-OBB Result

性能測試(不同 Batch Size):

為了探究不同Batch Size推理時間差異,此處對不同Batch Size進行了測試,測試結果如下:

Batch Size 1 2 4 6 8 10 12 14 16 18 20 22 24
前處理 (ms ) 9 13 27 38 56 59 63 83 96 105 118 130 144
模型推理 (ms) 7 15 24 36 48 60 96 84 93 153 120 133 203
後處理 (ms) 25 26 26 26 28 27 27 28 28 28 27 31 29

🔗程序路徑鏈接:完整程序已經上傳到GitHub,請自行下載,鏈接為:

https://github.com/guojin-yan/TensorRT-CSharp-API/tree/TensorRtSharp3.0/samples/YoloObbBatchInfer

示例 5:並行推理

使用一個 Runtime 創建多執行上下文,實現多並行推理。

核心代碼:

using JYPPX.TensorRtSharp.Cuda;
using JYPPX.TensorRtSharp.Nvinfer;
using OpenCvSharp;
using OpenCvSharp.Dnn;
using System.Diagnostics;
using System.Runtime.InteropServices;
using static OpenCvSharp.FileStorage;

namespace YoloDetParallelInfer
{
    internal class Program
    {
        // ================= 配置參數 =================
        // 模型輸入尺寸 (寬=高)
        private const int InputSize = 640;


        // 建議根據實際模型動態獲取或使用 Netron 查看
        private const int OutputSize = 8400;

        // 模型類別數 (根據您的具體數據集修改,此處假設為15類)
        private const int CategoryNum = 80;

        // 置信度閾值
        private const float ConfThreshold = 0.25f;

        // NMS IOU 閾值
        private const float NmsThreshold = 0.3f;

        static void Main(string[] args)
        {
            //  ============= 配置 TensorRT 日誌回調 =============
            // 定義一個委託,用於處理 TensorRT 內部產生的日誌消息。
            // 這允許我們將 C++ 層面的日誌輸出到 C# 的控制枱。
            LogCallbackFunction _callbackDelegate = (message) =>
            {
                Console.WriteLine(message);
            };

            // 將自定義的回調函數註冊給 TensorRT 的全局 Logger 實例。
            Logger.Instance.SetCallback(_callbackDelegate);

            // 設置日誌的嚴重性級別閾值。
            // LoggerSeverity.kINFO: 打印信息、警告和錯誤。
            // 開發調試階段通常設為 kINFO 或 kVERBOSE;生產環境可設為 kWARNING 或 kERROR 以減少輸出。
            Logger.Instance.SetThreshold(LoggerSeverity.kINFO);

            string enginePath = "yolov8s.engine";
            string imagePath = "bus.jpg";

                           Mat img = Cv2.ImRead(imagePath);
                            if (img.Empty())
                            {
                                Logger.Instance.INFO("Image not found!");
                                return;
                            }
            // ================= 1. 加載 TensorRT Engine =================
            // 使用 using 語句確保文件流正確關閉
            byte[] engineData;
            using (FileStream fs = new FileStream(enginePath, FileMode.Open, FileAccess.Read))
            using (BinaryReader br = new BinaryReader(fs))
            {
                engineData = br.ReadBytes((int)fs.Length);
            }

            // 反序列化 Engine
            // Runtime 必須在 Engine 生命週期內保持存活,通常建議設為全局或靜態,或者確保它最後釋放
            Runtime runtime = new Runtime();
            runtime.setMaxThreads(6);
            // 創建 CudaEngine (此處使用 using 確保推理完成後引擎被銷燬)
            using (CudaEngine cudaEngine = runtime.deserializeCudaEngineByBlob(engineData, (ulong)engineData.Length))
            {
                // ================= 2. 初始化推理上下文與顯存 =================
                Stopwatch totalSw = new Stopwatch();
                totalSw.Start();
                Parallel.For(0, 24, b =>
                {           
                    
                    // 創建執行上下文
                    using (JYPPX.TensorRtSharp.Nvinfer.ExecutionContext executionContext = cudaEngine.createExecutionContext(TrtExecutionContextAllocationStrategy.kSTATIC))
                    using (CudaStream cudaStream = new CudaStream()) // 創建 CUDA 流用於異步執行
                    {
                        // 獲取輸入維度信息 (用於校驗)
                        Dims inputDims = executionContext.getTensorShape("images");
                        Logger.Instance.INFO($"Input Shape: {inputDims.d[0]}x{inputDims.d[1]}x{inputDims.d[2]}x{inputDims.d[3]}");

                        // 計算所需顯存大小
                        // 輸入: Batch=1, Channel=3, Height=640, Width=640
                        ulong inputSizeInBytes = 1 * 3 * InputSize * InputSize;
                        // 輸出: Batch=1, Channels=CategoryNum+4(box)+1(angle), Num=8400
                        int outputChannels = CategoryNum + 4; // 4座標 + N類別
                        ulong outputSizeInBytes = (ulong)(1 * outputChannels * OutputSize);

                        Stopwatch sw = new Stopwatch();
                        // 分配 GPU 顯存
                        using (Cuda1DMemory<float> inputGpuMemory = new Cuda1DMemory<float>(inputSizeInBytes))
                        using (Cuda1DMemory<float> outputGpuMemory = new Cuda1DMemory<float>(outputSizeInBytes))
                        {
                            // 綁定顯存地址到 TensorRT 上下文
                            executionContext.setInputTensorAddress("images", inputGpuMemory.get());
                            executionContext.setOutputTensorAddress("output0", outputGpuMemory.get());
                            // 預熱推理 (可選,但推薦,尤其是首次推理時)
                            executionContext.executeV3(cudaStream);
                            cudaStream.Synchronize();
                            // ================= 3. 圖像預處理 =================
             

                            sw.Start();
                            float[] inputData = PreProcess(img, out float scale, out int xOffset, out int yOffset);
                            sw.Stop();
                            Logger.Instance.INFO($"Channel {b}: Pre-processing time: {sw.ElapsedMilliseconds} ms");
                            // ================= 4. 推理 =================
                            // 準備主機內存接收結果
                            float[] outputData = new float[outputChannels * OutputSize];


                            sw.Restart();
                            // 將數據從主機 拷貝到設備
                            inputGpuMemory.copyFromHostAsync(inputData, cudaStream);

                            // 執行推理 (enqueueV3 是異步的)
                            executionContext.executeV3(cudaStream);
                            // 等待推理完成
                            cudaStream.Synchronize();



                            // 將結果從設備 拷貝回主機
                            // 這裏的拷貝是同步的,會等待 GPU 計算完成
                            outputGpuMemory.copyToHostAsync(outputData, cudaStream);
                            sw.Stop();
                            Logger.Instance.INFO($"Channel {b}: Inference time: {sw.ElapsedMilliseconds} ms");
                            // ================= 5. 後處理 =================

                            sw.Restart();
                            List<DetData> results = PostProcess(outputData, scale, xOffset, yOffset);
                            sw.Stop();
                            Logger.Instance.INFO($"Channel {b}: Post-processing time: {sw.ElapsedMilliseconds} ms");

                            // ================= 6. 結果可視化 =================
                            //Mat resultImg = DrawDetResult(results, img);
                            //Cv2.ImShow("YOLO11-DET Result", resultImg);
                            //Cv2.WaitKey(0);
                        }
                    }
                });

                totalSw.Stop();
                Logger.Instance.INFO($"Total time for 8 inferences: {totalSw.ElapsedMilliseconds} ms");


            }
        }

        /// <summary>
        /// 圖像預處理:Letterbox 縮放、歸一化、HWC 轉 CHW
        /// </summary>
        private static float[] PreProcess(Mat img, out float scale, out int xOffset, out int yOffset)
        {
            // 轉換顏色空間 BGR -> RGB
            Mat rgbImg = new Mat();
            Cv2.CvtColor(img, rgbImg, ColorConversionCodes.BGR2RGB);

            // 計算 Letterbox 縮放比例
            int maxDim = Math.Max(rgbImg.Width, rgbImg.Height);
            scale = (float)maxDim / InputSize;

            // 計算縮放後的尺寸
            int newWidth = (int)(rgbImg.Width / scale);
            int newHeight = (int)(rgbImg.Height / scale);

            // Resize 圖像
            Mat resizedImg = new Mat();
            Cv2.Resize(rgbImg, resizedImg, new Size(newWidth, newHeight));

            // 創建黑色背景 Canvas (InputSize x InputSize)
            Mat paddedImg = Mat.Zeros(InputSize, InputSize, MatType.CV_8UC3);

            // 計算粘貼位置 (居中)
            xOffset = (InputSize - newWidth) / 2;
            yOffset = (InputSize - newHeight) / 2;

            // 將圖像拷貝到 Canvas 中央
            Rect roi = new Rect(xOffset, yOffset, newWidth, newHeight);
            resizedImg.CopyTo(new Mat(paddedImg, roi));

            // 歸一化 (0-255 -> 0-1) 並轉為 float 類型
            Mat floatImg = new Mat();
            paddedImg.ConvertTo(floatImg, MatType.CV_32FC3, 1.0 / 255.0);

            // HWC 轉 CHW 並展平為一維數組
            Mat[] channels = Cv2.Split(floatImg);
            float[] chwData = new float[3 * InputSize * InputSize];

            // 拷貝數據:R通道 -> C通道 -> B通道 (OpenCV Split 出來順序是 B, G, R,對應索引 0, 1, 2)
            int channelSize = InputSize * InputSize;
            // 將 R, G, B 依次拷入數組
            Marshal.Copy(channels[0].Data, chwData, 0, channelSize); // R
            Marshal.Copy(channels[1].Data, chwData, channelSize, channelSize); // G
            Marshal.Copy(channels[2].Data, chwData, channelSize * 2, channelSize); // B

            // 釋放臨時 Mat
            rgbImg.Dispose();
            resizedImg.Dispose();
            paddedImg.Dispose();
            floatImg.Dispose();
            foreach (var c in channels) c.Dispose();

            return chwData;
        }

        /// <summary>
        /// 後處理:解析 TensorRT 輸出、NMS 過濾
        /// </summary>
        private static List<DetData> PostProcess(float[] result, float scale, int xOffset, int yOffset)
        {
            List<Rect> boxes = new List<Rect>();
            List<float> confidences = new List<float>();
            List<int> classIds = new List<int>();

            // 遍歷所有預測框 (OutputSize)
            // 數據佈局: [4(box) + 80(classes)] * OutputSize
            // 展平數組中,同一屬性的數據是連續存儲的,例如所有 cx 在一起,所有 cy 在在一起...
            int stride = OutputSize; // 步長,不同屬性在數組中的偏移量

            for (int i = 0; i < OutputSize; i++)
            {
                // 查找最大類別概率及其索引
                float maxConf = 0;
                int maxClassId = -1;

                // 遍歷類別
                for (int c = 0; c < CategoryNum; c++)
                {
                    // 數組索引:(座標/角度偏移量 + 類別偏移) * 框索引
                    // 注意:原始代碼中 result[outputSize * j + i] 這種訪問方式基於 Transposed 數據佈局
                    float conf = result[(4 + c) * stride + i];
                    if (conf > maxConf)
                    {
                        maxConf = conf;
                        maxClassId = c;
                    }
                }

                // 置信度過濾
                if (maxConf > ConfThreshold)
                {
                    // 提取座標 (cx, cy, w, h)
                    float cx = result[0 * stride + i];
                    float cy = result[1 * stride + i];
                    float w = result[2 * stride + i];
                    float h = result[3 * stride + i];
                    // 還原座標到原圖尺寸
                    int rx = (int)((cx - xOffset - 0.5 * w) * scale);
                    int ry = (int)((cy - yOffset - 0.5 * h) * scale);
                    int rw = (int)(w * scale);
                    int rh = (int)(h * scale);

                    boxes.Add(new Rect(rx, ry, rw, rh));
                    confidences.Add(maxConf);
                    classIds.Add(maxClassId);
                }
            }

            // 執行 NMS (旋轉框 NMS)
            // OpenCV 的 NMSBoxes 支持 RotatedRect
            int[] indices;
            CvDnn.NMSBoxes(boxes, confidences, ConfThreshold, NmsThreshold, out indices);

            List<DetData> finalResults = new List<DetData>();
            foreach (int idx in indices)
            {
                finalResults.Add(new DetData
                {
                    index = classIds[idx],
                    score = confidences[idx],
                    box = boxes[idx]
                });
            }

            return finalResults;
        }

        /// <summary>
        /// 繪製檢測結果(水平矩形框)
        /// </summary>
        /// <param name="results">檢測結果列表</param>
        /// <param name="image">原始圖像</param>
        /// <returns>繪製後的圖像</returns>
        public static Mat DrawDetResult(List<DetData> results, Mat image)
        {
            // 克隆圖像以免修改原圖
            Mat mat = image.Clone();

            foreach (var item in results)
            {
                // 1. 繪製矩形框
                // Rect 結構包含 X, Y, Width, Height
                Cv2.Rectangle(mat, item.box, new Scalar(0, 255, 0), thickness: 2);
                // 2. 準備標籤文本 (類別ID - 置信度)
                string label = $"{item.index} - {item.score:F2}";
                // 3. 計算文本的尺寸,用於繪製背景
                int baseLine = 1;
                Size textSize = Cv2.GetTextSize(label, HersheyFonts.HersheySimplex, 0.6, 1, out baseLine);
                // 4. 繪製標籤背景(半透明黑色矩形),防止文字與背景混淆
                // 位置:矩形左上角略微上移,或者直接貼着左上角
                Point labelPosition = new Point(item.box.X, item.box.Y - (int)textSize.Height - 5);

                // 確保標籤不畫出圖像邊界
                if (labelPosition.Y < 0) labelPosition.Y = item.box.Y + (int)textSize.Height + 5;
                Rect labelBgRect = new Rect(labelPosition.X,
                                            labelPosition.Y - (int)textSize.Height, // OpenCV GetTextSize 返回的高度是基線到底部的距離,需調整
                                            (int)textSize.Width,
                                            (int)textSize.Height + (int)baseLine);
                // 如果背景框也在圖像範圍內,則繪製
                // 注意:這裏簡化處理,直接畫在框上方
                Cv2.Rectangle(mat,
                               new Point(item.box.X, item.box.Y - textSize.Height - 5),
                               new Point(item.box.X + textSize.Width, item.box.Y),
                               new Scalar(0, 255, 0),
                               thickness: -1); // -1 表示填充
                // 5. 繪製文本(白色文字)
                Cv2.PutText(mat,
                            label,
                            new Point(item.box.X, item.box.Y - 5),
                            HersheyFonts.HersheySimplex,
                            0.6,
                            new Scalar(0, 0, 0),
                            1);
            }
            return mat;
        }

        public class DetData
        {
            public int index;
            public float score;
            public Rect box;
        }
    }
}


為了方便編寫代碼,上述並行處理即使時間包括了推理上下文的創建、推理預熱等步驟,所以實際時間會偏長,上述程序運行後輸出如下所示:

image-20260109230808256

並行測試結果:

同時為了比較不同並行數,測試了從1到24不同並行數的情況,推理總時間如下:

並行數 1 2 4 6 8 10 12 14 16 18 20 22 24
推理總時間 (ms) 80 85 95 115 130 155 180 210 230 255 270 285 310

🔗程序路徑鏈接:完整程序已經上傳到GitHub,請自行下載,鏈接為:

https://github.com/guojin-yan/TensorRT-CSharp-API/tree/TensorRtSharp3.0/samples/YoloObbBatchInfer

七、異常處理

TensorRtSharp 提供了完善的異常處理機制。

try
{
    Runtime runtime = new Runtime();
    byte[] data = File.ReadAllBytes("model.engine");
    using CudaEngine engine = runtime.deserializeCudaEngineByBlob(data, (ulong)data.Length);
}
catch (TrtException ex)
{
    // TensorRT 特定錯誤
    Console.WriteLine($"TensorRT Error: {ex.ErrMsg}");
    Console.WriteLine($"Status: {ex.Status}");
}
catch (CudaException ex)
{
    // CUDA 運行時錯誤
    Console.WriteLine($"CUDA Error: {ex.Message}");
    Console.WriteLine($"Status: {ex.Status}");
}
catch (InitException ex)
{
    // 初始化錯誤
    Console.WriteLine($"Initialization Failed: {ex.Message}");
    Console.WriteLine($"Status: {ex.Status}");
}

異常類型説明:

異常類型 説明
TrtException TensorRT API 錯誤(20+ 錯誤碼)
CudaException CUDA 運行時錯誤(40+ 錯誤碼)
InitException 庫初始化錯誤

八、日誌系統

TensorRtSharp 提供了單例日誌系統。

// 獲取日誌實例
Logger logger = Logger.Instance;

// 設置日誌級別
logger.SetThreshold(LoggerSeverity.kINFO);  // INFO、WARNING、ERROR

// 設置自定義回調
logger.SetCallback((message) =>
{
    Console.WriteLine($"[TensorRT] {message}");
});

// 記錄日誌
logger.INFO("Engine building started...");
logger.WARNING("FP16 not supported, falling back to FP32");
logger.ERROR("Failed to parse ONNX model");

// 靜默模式
logger.SetThreshold(LoggerSeverity.kINTERNAL_ERROR);  // 僅嚴重錯誤

九、與其他庫的對比

特性 TensorRtSharp ML.NET ONNX Runtime
編程語言 C# C# C++/Python
API 類型 託管封裝 託管庫 原生綁定
性能 原生速度 中等 原生速度
易用性 中等
TensorRT 支持 完整 有限
自定義算子 支持 困難 支持
動態形狀 支持 有限 支持
多 GPU 支持 有限 支持

十、常見問題

問題一:找不到 DLL 模塊

錯誤信息:

Unable to load DLL 'TensorRT-C-API' or one of its dependencies: 找不到指定的模塊。

解決方案:

  1. 檢查是否安裝了對應版本的 Runtime NuGet 包
  2. 確認系統 PATH 環境變量中包含 TensorRT 的 lib 目錄和 CUDA 的 bin 目錄
  3. 確認 TensorRT 版本為 10.x 系列

錯誤截圖:

錯誤1
錯誤2


問題二:SEHException 異常

錯誤信息:

System.Runtime.InteropServices.SEHException: "External component has thrown an exception."

可能原因:

  • TensorRT 版本不匹配(必須使用 10.x)
  • CUDA 版本不兼容
  • 模型文件損壞

解決方案:

  1. 確認 TensorRT 版本為 10.x
  2. 檢查 CUDA 版本是否匹配
  3. 重新生成 Engine 文件

錯誤截圖:

SEHException

問題三:System.ExecutionEngineException 異常

錯誤信息:

System.ExecutionEngineException 

可能原因:

  • 模型文件與設備不匹配

解決方案:

  1. 在當前設備上重新生成模型文件

錯誤截圖:

image-20260110002045090

image-20260110002045090


十一、總結

TensorRtSharp 是一個功能完整、設計精良的 TensorRT C# 封裝庫,它填補了 .NET 生態在高性能深度學習推理方面的空白。通過提供類型安全的 API、自動資源管理和完善的異常處理,TensorRtSharp 讓 C# 開發者能夠充分發揮 GPU 的計算能力,而無需面對複雜的原生代碼。

核心優勢

完整的 API 覆蓋:支持 TensorRT 核心功能
類型安全:強類型系統,編譯時錯誤檢查
自動資源管理:RAII + Dispose 模式
高性能:異步執行、多流並行
易用性:直觀的 API、詳細註釋
跨平台:支持 Windows/Linux
開箱即用:NuGet 包含所有依賴

適用場景

無論您是構建以下類型的應用,TensorRtSharp 都是您的理想選擇:

  • 🎯 實時視覺應用:目標檢測、圖像分割、姿態估計
  • 🎤 語音處理:語音識別、語音合成
  • 🚀 邊緣計算:嵌入式設備推理

立即開始

安裝命令:

dotnet add package JYPPX.TensorRT.CSharp.API
dotnet add package JYPPX.TensorRT.CSharp.API.runtime.win-x64.cuda12

GitHub 倉庫:

https://github.com/guojin-yan/TensorRT-CSharp-API

立即安裝並體驗 C# 世界中的 GPU 推理極致性能吧!


技術支持

如有問題或建議,歡迎通過以下方式交流:

  • 📧 GitHub Issues:在項目倉庫提 Issue 或 Pull Request
  • 💬 QQ 交流羣:加入 945057948,回覆更方便更快哦

QQ羣二維碼


作者:Guojin Yan
版本:0.0.5
最後更新:2026年1月


【文章聲明】

本文主要內容基於作者的研究與實踐,部分表述藉助AI工具進行了輔助優化。由於技術侷限性,文中可能存在錯誤或疏漏之處,懇請各位讀者批評指正。如果內容無意中侵犯了您的權益,請及時通過公眾號後台與我們聯繫,我們將第一時間核實並妥善處理。感謝您的理解與支持!

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.