在 DirectML 中,綁定 是指將資源綁定到資料管線,以便 GPU 在初始化和執行機器學習操作期間使用。 例如,這些資源可以是輸入和輸出張量,以及作員需要的任何暫存或持續性資源。
本主題說明系結的概念細節和程序細節。 建議您也完整閱讀您所呼叫 API 的檔,包括參數和備註。
結合中的重要想法
下列步驟清單包含系結相關工作的高階描述。 每次執行可調度項目時,您需要遵循這些步驟 — 可調度項目是運算符初始化器或編譯運算符。 這些步驟介紹 DirectML 系結中涉及的重要概念、結構和方法。
本主題的後續各節會更深入探討並更詳細地說明這些系結工作,其中說明代碼段取自 最少的 DirectML 應用程式 程式代碼範例。
- 在分派器上呼叫 IDMLDispatchable::GetBindingProperties ,以判斷其需要多少描述元,以及其暫存/持續性資源需求。
- 建立 Direct3D 12 描述元堆積,足以供描述項使用,並將它系結至管線。
- 呼叫 IDMLDevice::CreateBindingTable 來建立 DirectML 系結數據表,以代表系結至管線的資源。 使用 DML_BINDING_TABLE_DESC 結構來描述系結數據表,包括它指向描述項堆積中描述項的子集。
- 建立暫存/持續性資源作為 Direct3D 12 緩衝區資源、使用 DML_BUFFER_BINDING 和 DML_BINDING_DESC 結構加以描述,並將其新增至系結數據表。
- 如果可指派的元件是已編譯的運算符,則建立 Tensor 元素的緩衝區作為 Direct3D 12 的緩衝資源。 填入/上傳、使用 DML_BUFFER_BINDING 和 DML_BINDING_DESC 結構加以描述,並將它新增至系結數據表。
- 當您呼叫 IDMLCommandRecorder::RecordDispatch 時,請將系結數據表當做參數傳遞。
擷取可分派的系結屬性
DML_BINDING_PROPERTIES 結構描述可分派的繫結需求(運算元初始化器或編譯運算元)。 這些系結相關屬性包括您應該系結至可分派的描述項數目,以及其需要之任何暫存和/或持續性資源的大小。
備註
即使是相同類型的多個運算符,也不要假設它們具有相同的系結需求。 查詢您所建立之每個初始化表達式和運算子的系結屬性。
呼叫 IDMLDispatchable::GetBindingProperties 以擷取 DML_BINDING_PROPERTIES。
winrt::com_ptr<::IDMLCompiledOperator> dmlCompiledOperator;
// Code to create and compile a DirectML operator goes here.
DML_BINDING_PROPERTIES executeDmlBindingProperties{
dmlCompiledOperator->GetBindingProperties()
};
winrt::com_ptr<::IDMLOperatorInitializer> dmlOperatorInitializer;
// Code to create a DirectML operator initializer goes here.
DML_BINDING_PROPERTIES initializeDmlBindingProperties{
dmlOperatorInitializer->GetBindingProperties()
};
UINT descriptorCount = ...
descriptorCount您在這裡擷取的值會決定描述元堆積和您在後續兩個步驟中建立之系結數據表的大小下限。
DML_BINDING_PROPERTIES 也包含 TemporaryResourceSize 成員,這是暫存資源的位元組大小下限,必須系結至這個可分派對象的系結數據表。 值為零表示不需要暫存資源。
PersistentResourceSize成員,它是必須綁定在此可調度對象的綁定表中的持續性資源的最小位元組大小。 值為零表示不需要持續性資源。 如果需要持續性資源,則必須在編譯運算子的初始化期間提供(其中系結為運算元初始化表達式的輸出),以及在執行期間提供。 本主題稍後會有更多相關信息。 只有已編譯的運算符具有永續性資源—運算元初始化表達式一律會傳回這個成員的0值。
如果您在呼叫 IDMLOperatorInitializer::Reset 之前和之後,在運算符初始化表達式上呼叫 IDMLDispatchable::GetBindingProperties,則擷取的兩組系結屬性不保證完全相同。
描述、建立和系結描述項堆積
就描述元而言,您的責任會以描述元堆積本身開始和結束。 DirectML 本身會負責建立和管理您提供的堆積內的描述項。
因此,請使用 D3D12_DESCRIPTOR_HEAP_DESC 結構來描述足以容納可分派之描述項數目的堆積。 然後使用 ID3D12Device::CreateDescriptorHeap加以建立。 最後,呼叫 ID3D12GraphicsCommandList::SetDescriptorHeaps 將您的描述元堆積系結至管線。
winrt::com_ptr<::ID3D12DescriptorHeap> d3D12DescriptorHeap;
D3D12_DESCRIPTOR_HEAP_DESC descriptorHeapDescription{};
descriptorHeapDescription.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
descriptorHeapDescription.NumDescriptors = descriptorCount;
descriptorHeapDescription.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
winrt::check_hresult(
d3D12Device->CreateDescriptorHeap(
&descriptorHeapDescription,
_uuidof(d3D12DescriptorHeap),
d3D12DescriptorHeap.put_void()
)
);
std::array<ID3D12DescriptorHeap*, 1> d3D12DescriptorHeaps{ d3D12DescriptorHeap.get() };
d3D12GraphicsCommandList->SetDescriptorHeaps(
static_cast<UINT>(d3D12DescriptorHeaps.size()),
d3D12DescriptorHeaps.data()
);
描述及建立系結數據表
DirectML 系結數據表代表您系結至管線的資源,以供分派使用。 這些資源可以是運算子的輸入和輸出張量(或其他參數),或者可能是可分派使用的各種持續性和暫存資源。
使用 DML_BINDING_TABLE_DESC 結構來描述系結數據表,包括系結數據表將代表系結的可分派專案,以及您希望系結數據表參考的描述元範圍(以及 DirectML 可寫入描述元的範圍)。
descriptorCount值(我們在第一個步驟中擷取的其中一個系結屬性)告訴我們分派物件所需的系結數據表大小下限。 在這裡,我們會使用該值來指出允許 DirectML 寫入堆積的最大描述元數目,從提供的 CPU 和 GPU 描述元句柄開始。
然後呼叫 IDMLDevice::CreateBindingTable 來建立 DirectML 系結數據表。 在後續步驟中,為可分派資源創建進一步資源之後,我們會將這些資源新增至綁定表。
與其將 DML_BINDING_TABLE_DESC 傳遞至此呼叫,您可以傳遞 nullptr,指出空的系結數據表。
DML_BINDING_TABLE_DESC dmlBindingTableDesc{};
dmlBindingTableDesc.Dispatchable = dmlOperatorInitializer.get();
dmlBindingTableDesc.CPUDescriptorHandle = d3D12DescriptorHeap->GetCPUDescriptorHandleForHeapStart();
dmlBindingTableDesc.GPUDescriptorHandle = d3D12DescriptorHeap->GetGPUDescriptorHandleForHeapStart();
dmlBindingTableDesc.SizeInDescriptors = descriptorCount;
winrt::com_ptr<::IDMLBindingTable> dmlBindingTable;
winrt::check_hresult(
dmlDevice->CreateBindingTable(
&dmlBindingTableDesc,
__uuidof(dmlBindingTable),
dmlBindingTable.put_void()
)
);
DirectML 將描述項寫入堆積的順序未指定,因此您的應用程式必須小心不要覆寫系結數據表所包裝的描述元。 提供的 CPU 和 GPU 描述元句柄可能來自不同的堆積,不過,應用程式必須負責確保 CPU 描述元句柄所參照的整個描述元範圍會複製到 GPU 描述元句柄在執行之前使用此系結數據表所參考的範圍。 提供句柄的描述元堆積必須具有類型 D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV。 此外,GPUDescriptorHandle 所參考的堆必須是著色器可見的描述符堆。
您可以重設系結數據表來移除已新增至它的任何資源,同時變更您在初始 DML_BINDING_TABLE_DESC 上設定的任何屬性(包裝新範圍的描述元,或將它重新用於不同的可分派專案)。 只要變更描述結構,並呼叫 IDMLBindingTable::Reset。
dmlBindingTableDesc.Dispatchable = pIDMLCompiledOperator.get();
winrt::check_hresult(
pIDMLBindingTable->Reset(
&dmlBindingTableDesc
)
);
描述並系結任何暫存/持續性資源
當我們擷取可分派之屬性時所填入的DML_BINDING_PROPERTIES結構,包含可分派所需要的任何暫存和/或持續資源的大小(以位元組為單位)。 如果其中一個大小不是零,請建立 Direct3D 12 緩衝區資源,並將它新增至系結數據表。
在下列程式代碼範例中,我們會建立可分派的暫存資源(temporaryResourceSize 大小為位元組)。 我們會描述我們想要如何系結資源,然後將該系結新增至系結數據表。
由於我們正在系結單一緩衝區資源,因此我們會使用 DML_BUFFER_BINDING 結構來描述我們的系結。 在該結構中,我們會指定 Direct3D 12 緩衝區資源(資源必須有維度 D3D12_RESOURCE_DIMENSION_BUFFER),以及緩衝區中的位移和大小。 您也可以描述緩衝區陣列的綁定(而不是單一緩衝區的綁定),並且 DML_BUFFER_ARRAY_BINDING 結構即為此用途而存在。
為了將緩衝區系結與緩衝區數位系結之間的差異抽象化,我們使用 DML_BINDING_DESC 結構。 您可以將Type的成員設定為 DML_BINDING_TYPE_BUFFER 或 DML_BINDING_TYPE_BUFFER_ARRAY。 然後,您可以根據Desc,將成員設置為指向DML_BUFFER_BINDING或Type。
我們在此範例中處理暫存資源,因此我們會使用 IDMLBindingTable::BindTemporaryResource的呼叫將它新增至系結數據表。
D3D12_HEAP_PROPERTIES defaultHeapProperties{ CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT) };
winrt::com_ptr<::ID3D12Resource> temporaryBuffer;
D3D12_RESOURCE_DESC temporaryBufferDesc{ CD3DX12_RESOURCE_DESC::Buffer(temporaryResourceSize) };
winrt::check_hresult(
d3D12Device->CreateCommittedResource(
&defaultHeapProperties,
D3D12_HEAP_FLAG_NONE,
&temporaryBufferDesc,
D3D12_RESOURCE_STATE_COMMON,
nullptr,
__uuidof(temporaryBuffer),
temporaryBuffer.put_void()
)
);
DML_BUFFER_BINDING bufferBinding{ temporaryBuffer.get(), 0, temporaryResourceSize };
DML_BINDING_DESC bindingDesc{ DML_BINDING_TYPE_BUFFER, &bufferBinding };
dmlBindingTable->BindTemporaryResource(&bindingDesc);
暫存資源(如果需要的話)是在運算符執行期間於內部使用的臨時記憶體,因此您不需要擔心其內容。 在 GPU 上呼叫 IDMLCommandRecorder::RecordDispatch 之後,您也不需要加以保留。 這表示您的應用程式可能會在已編譯運算子的分派之間釋放或覆寫暫存資源。 提供作為暫存資源綁定的緩衝區範圍,其開始偏移量必須與 DML_TEMPORARY_BUFFER_ALIGNMENT 對齊。 緩衝區底層的堆類型必須為 D3D12_HEAP_TYPE_DEFAULT。
不過,如果可調度的資源報告其較長期永久資源的大小不是零,則程序會略有不同。 您應該建立緩衝區,並描述遵循與上述相同模式的系結。 但是,將它新增至運算元初始化器的系結表,並呼叫 IDMLBindingTable::BindOutputs,因為初始化永久性資源是運算元初始化器的工作。 然後將它新增至已編譯運算符的系結數據表,並呼叫 IDMLBindingTable::BindPersistentResource。 請參閱 最少的 DirectML 應用程式 程式代碼範例,以查看此工作流程的運作情形。 只要編譯運算符仍然存在,持續性資源的內容和存續時間也必須得以保留。 也就是說,如果作員需要永續性資源,則您的應用程式必須在初始化期間提供它,然後也會提供它給運算符的所有未來執行,而不需修改其內容。 DirectML 通常會使用永續性資源來儲存查閱數據表或其他在運算元初始化期間計算的長時間存留數據,並在該運算子的未來執行時重複使用。 提供的緩衝區範圍要綁定為持續性緩衝區,其開始偏移必須對齊 DML_PERSISTENT_BUFFER_ALIGNMENT。 緩衝區底層的堆類型必須為 D3D12_HEAP_TYPE_DEFAULT。
描述及系結任何張量
如果您要處理已編譯的運算元(而不是運算元初始化器),則必須將輸入和輸出資源(適用於張量和其他參數)繫結至運算子的繫結表。 係結數目必須完全符合運算子的輸入數目,包括選擇性的張量。 運算子採用的特定輸入和輸出張量和其他參數記載於該運算符的主題中(例如 ,DML_ELEMENT_WISE_IDENTITY_OPERATOR_DESC)。
張量資源是包含張量各個元素值的緩衝區。 您可以使用一般 Direct3D 12 技術將這類緩衝區上傳到 GPU(上傳資源),並透過緩衝區從 GPU 回讀數據(讀取數據)。 請參閱 最少的 DirectML 應用程式 程式代碼範例,以查看這些技術的運作情形。
最後,使用 DML_BUFFER_BINDING 和 DML_BINDING_DESC 結構來描述輸入和輸出資源系結,然後將它們新增至編譯運算符的系結數據表,並呼叫 IDMLBindingTable::BindInputs 和 IDMLBindingTable::BindOutputs。 當您呼叫 IDMLBindingTable::Bind* 方法時,DirectML 會將一或多個描述元寫入 CPU 描述元的範圍。
DML_BUFFER_BINDING inputBufferBinding{ inputBuffer.get(), 0, tensorBufferSize };
DML_BINDING_DESC inputBindingDesc{ DML_BINDING_TYPE_BUFFER, &inputBufferBinding };
dmlBindingTable->BindInputs(1, &inputBindingDesc);
DML_BUFFER_BINDING outputBufferBinding{ outputBuffer.get(), 0, tensorBufferSize };
DML_BINDING_DESC outputBindingDesc{ DML_BINDING_TYPE_BUFFER, &outputBufferBinding };
dmlBindingTable->BindOutputs(1, &outputBindingDesc);
建立 DirectML 運算符的其中一個步驟(請參閱 IDMLDevice::CreateOperator)是宣告一或多個 DML_BUFFER_TENSOR_DESC 結構,以描述運算符所接受和傳回的張量數據緩衝區。 除了張量緩衝區的類型和大小之外,您還可以選擇性地指定DML_TENSOR_FLAG_OWNED_BY_DML 旗標。
DML_TENSOR_FLAG_OWNED_BY_DML 表示應該由 DirectML 擁有和管理張量數據。 DirectML 會在運算元初始化期間製作張量數據的複本,並將其儲存在永續性資源中。 這可讓 DirectML 將張量數據重新格式化成其他更有效率的形式。 設定此旗標可能會提高效能,但通常只適用於在運算符整個存留期間內數據不會變更的張量(例如,表示權重的張量)。 旗標只能用於輸入張量。 在特定張量描述上設定旗標時,對應的張量必須在運算元初始化期間系結至系結數據表,而不是在執行期間(這會導致錯誤)。 這與預設的行為相反,即在沒有使用DML_TENSOR_FLAG_OWNED_BY_DML標誌的情況下,張量預期在執行期間會被系結,而不是在初始化期間被系結。 系結至 DirectML 的所有資源都必須是 DEFAULT 或 CUSTOM 堆積資源。
如需詳細資訊,請參閱 IDMLBindingTable::BindInputs 和 IDMLBindingTable::BindOutputs。
執行可調度的任务
當您呼叫 IDMLCommandRecorder::RecordDispatch 時,請將系結數據表當做參數傳遞。
當您在呼叫 IDMLCommandRecorder::RecordDispatch 期間使用系結數據表時,DirectML 會將對應的 GPU 描述元系結至管線。 CPU 和 GPU 描述元句柄不需要指向描述元堆積中的相同條目,但應用程式必須負責確保在執行前,將 CPU 描述元句柄所參照的整個描述元範圍複製到 GPU 描述元句柄所參考的範圍,以便使用該綁定表。
winrt::com_ptr<::ID3D12GraphicsCommandList> d3D12GraphicsCommandList;
// Code to create a Direct3D 12 command list goes here.
winrt::com_ptr<::IDMLCommandRecorder> dmlCommandRecorder;
// Code to create a DirectML command recorder goes here.
dmlCommandRecorder->RecordDispatch(
d3D12GraphicsCommandList.get(),
dmlOperatorInitializer.get(),
dmlBindingTable.get()
);
最後,關閉您的 Direct3D 12 命令清單,並提交它以供執行,就像任何其他命令清單一樣。
在 GPU 上執行 RecordDispatch 之前,您必須將所有系結的資源轉換為 D3D12_RESOURCE_STATE_UNORDERED_ACCESS 狀態,或轉換為可隱式提升至 D3D12_RESOURCE_STATE_UNORDERED_ACCESS 的狀態,例如 D3D12_RESOURCE_STATE_COMMON。 此呼叫完成之後,資源會 維持D3D12_RESOURCE_STATE_UNORDERED_ACCESS狀態 。 唯一的例外是在執行運算元初始化時綁定上傳堆積,且一個或多個張量已設定為擁有DML_TENSOR_FLAG_OWNED_BY_DML標誌。 在此情況下,系結給輸入的任何上傳堆積都必須處於 D3D12_RESOURCE_STATE_GENERIC_READ 狀態,而且會保持該狀態,因為所有上傳堆積都需要。 如果在編譯運算符時未設定 DML_EXECUTION_FLAG_DESCRIPTORS_VOLATILE ,則在呼叫 RecordDispatch 之前,必須先在系結數據表上設定所有系結,否則行為未定義。 否則,如果運算符支援 晚期系結,則資源系結可能會延後,直到將 Direct3D 12 命令清單提交至命令佇列以執行為止。
RecordDispatch 在邏輯上就像呼叫 ID3D12GraphicsCommandList::Dispatch。 因此,若工作分派之間存在數據相依性,就必須設置無序存取視圖(UAV)障礙,以確保排序正確。 此方法不會在輸入和輸出資源上插入UAV屏障。 如果您的應用程式必須在輸入的內容相依於上游分派時,在任何輸入上執行正確的 UAV 屏障;同樣地,如果有下游分派依賴此輸出內容,則也必須在輸出上執行適當的屏障。
描述項和系結數據表的存留期和同步處理
DirectML 中系結的良好心理模型是,DirectML 系結數據表本身會在幕後建立和管理您提供的描述元堆積內的未排序存取檢視 (UAV) 描述元。 因此,所有一般 Direct3D 12 規則都適用於同步處理該堆積及其描述項的存取權。 應用程式負責確保在使用綁定表的 CPU 和 GPU 工作之間進行正確的同步。
當描述項正在使用中時(例如,被先前的幀所使用),绑定表無法覆寫描述項。 因此,如果您想要重複使用已經系結的描述元堆積(例如,在指向它的系結數據表上再次呼叫 Bind*,或手動覆寫描述項堆積),則您應該等候目前使用描述元堆積的可分派作業完成在 GPU 上執行。 系結表不會在其寫入的描述符堆上維護強引用,因此在使用該系結表的所有工作在 GPU 上完成執行之前,您不應釋放支持著色器可見的描述符堆。
另一方面,雖然系結數據表確實指定和管理描述元堆積,但 數據表本身不包含 任何記憶體。 因此,在呼叫 IDMLCommandRecorder::RecordDispatch 之後,您可以隨時釋放或重設系結數據表(您不需要等待該呼叫在 GPU 上完成,只要基礎描述項維持有效)。
綁定表不會保留對任何使用它綁定的資源的強參考,您的應用程式必須確保資源在仍由 GPU 使用時不被刪除。 此外,系結數據表不是安全線程,您的應用程式不得從不同線程同時呼叫系結數據表上的方法,而不需要同步處理。
而且,只有在您變更系結的資源時,才需要重新系結。 如果您不需要變更系結資源,則可以在啟動時系結一次,並在每次呼叫 RecordDispatch 時傳遞相同的系結數據表。
若要交互執行機器學習和繪圖工作負載,請確保每幀的綁定表指向 GPU 上尚未使用的描述符堆範圍。
選擇性地指定晚期綁定運算子系結
如果您要處理已編譯的運算元(而不是運算子初始化表達式),則可以選擇指定運算子的晚期系結。 如果沒有延遲綁定,您必須先在綁定表上設定所有綁定,才能將操作記錄到指令列表中。 使用晚期系結,您可以在已記錄到命令清單的運算符上設定 (或變更) 系結,然後才將其提交至命令佇列。
若要指定晚期繫結,請使用 DML_EXECUTION_FLAG_DESCRIPTORS_VOLATILE 的參數呼叫 flags。