這篇文章是關于優化端到端人工智能.
雖然 NVIDIA 硬件可以以難以置信的速度處理構成神經網絡的單個操作,但確保您正確使用這些工具是很重要的。在 ONNX 中使用 ONNX Runtime 或 TensorRT 等開箱即用的工具通常會給您帶來良好的性能,但既然您可以擁有出色的性能,為什么還要滿足于良好的性能呢?
在這篇文章中,我討論了一個常見的場景,即帶有 DirectML 后端的 ONNX Runtime 。這是構建 WinML 的兩個主要組件。當在 WinML 之外使用時,它們可以在支持運算符集以及支持 DML 以外的后端(如 TensorRT )方面提供極大的靈活性。
為了獲得 ONNX Runtime 和 DML 的出色性能,通常值得超越基本實現。從使用 ONNX Runtime 時的常見場景開始。

- 一些圖像數據是從磁盤加載的。
- int8 圖像數據以某種方式進行預處理,例如縮放并轉換為 float16 。
- 圖像數據被加載到 GPU 上。
- 對圖像數據進行推理。
- 將結果加載回 CPU 。
- 結果經過后處理,或者可能發送到另一個模型。
這里有幾個問題。如果從頭開始使用 ONNX Runtime ,則提供指向系統( CPU )內存中數據的指針。當您通過調用Ort::Session::Run(...)
以及當推理完成時將數據傳輸回系統( CPU )存儲器。
雖然從實現的角度來看,這聽起來很方便,但您可能在推理之前有一個預處理階段,在推理之后有一個后處理階段。使用當前的工作流程,您必須對 CPU 上的數據進行預處理和后處理,或者在 ONNX Runtime 第二次將所有數據傳輸回 GPU 進行推理之前,往返于 GPU ‘。
一個更好的方法是將原始數據加載到 GPU ,無論是完整加載還是分塊加載,先對 GPU ‘執行預處理步驟(稍后將對此進行詳細介紹),然后將其保留在 GPU 上,以便進行推理。通過這種方式,您已經利用 GPU 的大規模并行能力來執行預處理步驟,并減少了初始傳輸的大小,因為您現在正在傳輸 int8 數據,而不是 float16 數據。
理論上這一切都很好,但如何使用 ONNX Runtime 和 DirectML 將其付諸實踐?為此,您必須深入研究 DirectX 12 , DirectML 就是基于它構建的。
有關我在本文中討論的實現的更多信息,請參閱code example以及評論。
DirectX12
與 OpenGL 和 CUDA 相比, DirectX 12 可能有些冗長。對于渲染圖形,可能有很多管道狀態需要管理。您只需要使用計算管道,這要簡單得多。無論如何, DirectX 12 與其他任何 API 或 SDK 一樣,都有學習曲線,但既不陡峭也不冗長。
DirectX12 通過公開較低級別的構造來實現對 GPU 的快速且高度可配置的訪問,您可以使用這些構造來控制在 GPU ‘上安排工作的時間和方式。帶 DML 的 ONNX Runtime 已經使用了它,但您希望訪問 ONNX 和 DML 正在使用的相同資源,以便使用它們執行預處理和傳輸。
DirectX 12 公開了名為命令隊列。您可以將命令記錄到 CPU 上的這些隊列中,并將它們發送到要調度的 GPU 。這些命令可以多次運行,而無需重新記錄。通過創建多個隊列,您可以在 GPU 上并行執行多個作業。通常情況下,單個推理可能不會使 GPU 上的處理器飽和,并且您可能能夠同時做多件事。稍后將對此進行詳細介紹。
以下是 DirectX 12 工作流的高級視圖:
- 獲取圖形卡(適配器)的參考。
- 創建對圖形設備的邏輯引用。您可以使用它來分配內存和發出命令。
- 從設備中獲取對命令隊列的引用。
- 編寫用于預處理的計算著色器,這比您想象的要簡單。
- 創建計算管道狀態對象。
- 在設備上分配一些內存用于輸入和輸出。您可以隨時在該內存之間進行傳輸。
- 將命令添加到命令隊列中。
- 執行隊列。
您創建的隊列與 ONNX Runtime 提供給 DirectML 的隊列相同。您可以構建新的高性能功能,作為 ONNX Runtime 已經提供的功能的擴展。
您將要學習的內容適用于 ONNX 和 DirectML ,以及許多其他計算任務。
獲取圖形卡的參考
根據您的系統類型,您可能有一個圖形卡、多個圖形卡,或者根本沒有圖形卡。
要做的第一件事是查詢系統,以發現您可以玩什么。這使您能夠通過 Direct X Graphics interface ( DXGI )獲取與實際物理設備的接口。通過此物理設備接口,您可以創建對邏輯設備使您可以訪問 DirectX 運行所需的設備內存和命令隊列。
有幾種類型的命令隊列可用于不同的任務,例如渲染、復制和計算工作。可以并行執行一些任務,例如復制和計算工作。有關詳細信息,請參閱代碼示例.
設置 ONNX 運行時

要在此項目中將 ONNX Runtime 與 DirectML 一起使用,請首先設置對邏輯設備的引用。然后,使用邏輯 DirectX12 設備創建對 DML 設備的引用。您還可以創建一個隊列供 DML 使用。然后,當您為 DML 執行提供程序創建會話選項結構時,您可以使用擴展表單,使用先前創建的 DirectX12 構造來創建 ORT 會話。
Ort::SessionOptions opts; OrtSessionOptionsAppendExecutionProviderEx_DML( opts, m_dml_device.Get(), m_copy_queue->GetD3D12CmdQueue().Get()) |
現在,您可以使用SessionOptions
對象
創建會話后,您現在可以開始初始化資源,將輸入數據傳遞給模型,并從模型接收輸出數據。要做到這一點,您可以在模型中查詢它所期望的張量形狀和格式。
內存和內存傳輸
如果從基本實現中使用 ONNX Runtime ,則輸入和輸出數據將在 CPU 內存中啟動, ONNX Run 將管理與 GPU 之間的傳輸。在簡單的情況下,例如當對圖像的整體進行推理時,這可能是可以的。
然而,在實踐中,大多數大圖像都被分解成瓦片,可能有一些重疊并按順序處理。在這種情況下,通過自己管理轉移可以獲得相當大的性能提升。

- 您可以控制何時進行轉賬。
- 您可以與其他計算工作并行執行傳輸。
DirectX 12 的存儲器接口是靈活的,可以以各種方式使用以執行傳輸。在執行數據傳輸方面,為您提供最大粒度的方法是自己暫存內存。

- 為暫存內存創建專用隊列:
- 類型: D3D12 _ COMMAND _ LIST _ type _ COPY
- 創建兩個 ID3D12Resource 對象:
- D3D12 _ HEAP _ TYPE _ UPLOAD :從主機可見。
- D3D12 _ HEAP _ TYPE _ DEFAULT : GPU 的本地。
- 使用已提交或已放置的資源:
- 提交的資源: DX12 為您創建和管理堆。
- 放置的資源:您提供堆。用于子分配。
- 創建一個命令列表并發送一個復制命令。這將執行從主機到設備的復制。
在 GPU 上獲得數據后,創建一個引用它的視圖對象和一個綁定到此內存的 Ort-Value 對象。然而,您并沒有將原始傳輸的數據按原樣輸入到模型中,因為還有一個更重要的步驟需要執行。
更快的預處理
現在,您可以控制數據何時以及如何傳輸到 GPU 。現在,您還可以了解如何將預處理和后處理移動到 GPU 。
在大多數計算機視覺應用程序中,以整數格式(如 RGB8 )提供一些輸入,并將其轉換為縮放和偏置的浮點表示是很常見的。
如果您使用開箱即用的 ONNX Runtime 和 DML ,則很難在 GPU 上執行此操作,因為數據在 CPU 上開始和結束其行程。現在,您可以自己執行這些傳輸,從而可以控制內存的生命周期。您還可以將此預處理和后處理轉移到自定義計算過程中,并將其作為端到端推理管道的一部分運行。
您必須做的是在轉移到 GPU 之后但在運行推理之前插入一個計算步驟。本例中的計算步驟獲取傳輸到 GPU 的 RGB8 整數數據,并將其傳遞給執行縮放和偏移的計算內核(著色器)。在這樣做的同時,它還將數據轉換為模型所需精度的浮點值。為了獲得最佳性能這里應該使用 FP16.
必須對數據執行的所有操作都是就地操作,因為輸入中的每個像素都對其執行了相同的操作,并且不依賴于其任何鄰居。這種類型的工作很容易并行執行,因此它是利用 GPU 的力量的絕佳候選者。
要使用 DirectX 12 運行計算著色器,請創建所謂的管道狀態對象。對于圖形渲染來說,這可能是一個相當復雜的過程,但對于計算處理來說,它要簡單得多。
管道狀態對象本質上預編譯在 GPU 上執行某些工作所需的所有狀態,包括運行的著色器字節碼和要使用的資源的綁定。

首先創建一個名為根簽名,這與函數簽名類似,因為它描述了管道的屬性和輸出。然后,您可以使用這個根簽名來創建管道狀態對象本身,為輸入和輸出提供實際的緩沖區綁定。
創建管道后,創建一個命令緩沖區并記錄運行計算著色器所需的命令。有關詳細信息,請參閱代碼示例.
同步和利用更多并行性

NVIDIA 硬件可以并行執行一些不同的任務,在執行任何計算工作的同時,顯著地執行與 GPU 的并行傳輸。當 DML 模型在 GPU 上執行時,它是計算工作。
我建議您設置端到端管道,以便一批推理工作(例如瓦片)可以執行推理,而下一批推理任務則轉移到 GPU ,以便它可以下一步運行。事實上,如果 GPU 上有足夠的可用資源,甚至可以并行運行多個瓦片的實際計算或推理部分。
為了在處理中發生這些重疊,計算或傳輸工作必須在它們自己的隊列中執行,其中一些隊列可以相互并行運行。這就提出了同步。如果在某些數據的一個隊列中運行傳輸,而在另一個隊列上運行推理,則必須確保在必須運行任何計算或推理步驟時數據已完成傳輸。
同步可以通過多種方式從 CPU 側和 GPU 側執行,但您希望 CPU ‘盡可能少地進行交互。使用資源壁壘這導致隊列等待,直到滿足由屏障設置的條件為止。您使用兩個障礙:
資源轉換障礙
請記住,您正在將數據從主機傳輸到設備。傳輸數據時,目標緩沖區處于可以從 CPU 向其傳輸數據的狀態。當綁定到管道時,這可能不是它所處的最佳狀態,因此必須提供轉換。
這一要求取決于硬件平臺,但需要轉換才能使 DirectX12 的使用有效。
UAV 屏障
這種類型的屏障只是阻塞隊列,直到所有數據都完成傳輸。通過以這種方式使用屏障,您可以讓 GPU 等待,而 CPU 根本不會參與并提高性能。
CD3DX12_RESOURCE_BARRIER barrier2 = CD3DX12_RESOURCE_BARRIER::UAV( m_ort_input_buffer->GetD3DResource().Get() ); |
創建兩個屏障后,一步將它們添加到命令列表中。
CD3DX12_RESOURCE_BARRIER barriers[2] = {barrier1, barrier2}; m_cmd_list_stage_input->ResourceBarrier(2, barriers); |
你現在可以把所有的部分放在一起了。您已經看到,您不僅可以創建和管理可用于調度傳輸和計算工作的資源,還可以創建并管理這些資源的調度時間。
現在您只需要兩個隊列:
- 傳輸隊列:用于調度傳輸命令。
- 計算隊列:用于調度預處理和后處理命令以及實際 ONNX 運行時會話本身。
您還需要為每個記錄命令的命令列表。
傳輸和計算之間必須有一些同步,以確保在傳輸數據的工作開始之前傳輸已經完成。這里有一個優化的機會。
NVIDIA 硬件完全是并行的,它可以同時執行傳輸和計算等操作。當您處理單個作業時,幾乎沒有機會將轉移與計算重疊,因為您必須等待轉移完成后才能開始計算。
通常,在圖像處理作業的情況下,您會將作業拆分為瓦片。對于大型圖像,很可能沒有足夠的設備內存來在一次運行中執行工作。通過將每個瓦片視為要執行的一系列任務來使用這種并行性。然后,您可以在任何時候“飛行”幾個瓦片,每個關鍵階段之間都有一個同步點:
- 第一個磁貼:將數據復制回 CPU 內存。
- 第二個磁貼:運行推理和計算工作。
- 第三個瓦片:正在將數據復制到 GPU 內存。
這三項任務都可以并行進行。甚至可能存在這樣的情況,即如果沒有使 GPU 飽和,則可以在一定程度的重疊的情況下進行一個以上的計算工作。
結論
我在這篇文章中涵蓋了很多內容。要想實際理解這些方法的機制,唯一的方法就是動手。我鼓勵你們花時間試驗example code,使用從導出的 ONNX NVIDIA DL Designer.
有關執行過程中發生的事情的更多信息,請參閱代碼注釋。
?