DirectML 中的绑定

在 DirectML 中,绑定 是指将资源附加到管道,使 GPU 可以在初始化和执行机器学习算子时使用这些资源。 例如,这些资源可以是输入和输出张量,也可以是运算符需要的任何临时性或永久性资源。

本主题介绍绑定的概念和过程详细信息。 我们还建议你充分阅读调用的 API 的文档,包括参数和备注。

绑定中的重要概念

以下步骤列表包含绑定相关的任务的概要说明。 每次执行 可调度对象时,都需要执行这些步骤 — 可调度对象是运算符初始值设定项或已编译的运算符。 这些步骤介绍了 DirectML 绑定中涉及的重要想法、结构和方法。

本主题的后续部分更深入地探讨并更详细地解释这些绑定任务,并演示了从 最少的 DirectML 应用程序 代码示例中获取的代码片段。

  • 针对可调度对象调用 IDMLDispatchable::GetBindingProperties,以确定它需要多少个描述符,及其临时性/永久性资源需求。
  • 为描述符创建足够大的 Direct3D 12 描述符堆,并将其绑定到管道。
  • 调用 IDMLDevice::CreateBindingTable 以创建一个 DirectML 绑定表来表示绑定到管道的资源。 使用 DML_BINDING_TABLE_DESC 结构来描述您的绑定表,包括它在描述符堆中指向的描述符的子集。
  • 创建临时/永久性资源作为 Direct3D 12 缓冲区资源,使用 DML_BUFFER_BINDINGDML_BINDING_DESC 结构描述它们,并将其添加到绑定表中。
  • 如果可调度对象是编译的运算符,则创建张量元素的缓冲区作为 Direct3D 12 缓冲区资源。 填充/上传,使用 DML_BUFFER_BINDINGDML_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_BUFFERDML_BINDING_TYPE_BUFFER_ARRAY。 然后,可以根据 Desc,将 成员设置为指向 DML_BUFFER_BINDING 或 DML_BUFFER_ARRAY_BINDINGType

我们在此示例中处理临时资源,因此,我们将它添加到绑定表中,并调用 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 中读取此类缓冲区。 请参阅 简单的 DirectML 应用程序 代码示例,以了解这些技术的实际应用。

最后,使用DML_BUFFER_BINDINGDML_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::BindInputsIDMLBindingTable::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_VOLATILEflags 参数调用 IDMLDevice::CompileOperator

另请参阅