Enlaces en DirectML

En DirectML, el enlace hace referencia a los datos adjuntos de los recursos a la canalización para que la GPU los use durante la inicialización y ejecución de los operadores de aprendizaje automático. Estos recursos pueden ser tensores de entrada y salida, por ejemplo, así como cualquier recurso temporal o persistente que el operador necesite.

En este tema se tratan los detalles conceptuales y de procedimientos del enlace. Se recomienda leer completamente la documentación de las API a las que llama, incluidos los parámetros y los comentarios.

Ideas importantes sobre el enlace

La lista de pasos siguiente contiene una descripción general de las tareas relacionadas con el enlace. Debe seguir estos pasos cada vez que ejecute un distribuidor. Un distribuidor es un inicializador de operadores o un operador compilado. Estos pasos presentan las ideas, estructuras y métodos importantes implicados en el enlace de DirectML.

Las secciones posteriores de este tema profundizan más y explican estas tareas de enlace con más detalle, con fragmentos de código ilustrativos tomados del ejemplo de código de la aplicación DirectML mínima.

  • Llame a IDMLDispatchable::GetBindingProperties en el distribuidor para determinar cuántos descriptores necesita y también sus necesidades de recursos temporales o persistentes.
  • Cree un montón descriptor de Direct3D 12 lo suficientemente grande para los descriptores y enlácelo a la canalización.
  • Llame a IDMLDevice::CreateBindingTable para crear una tabla de enlace de DirectML que represente los recursos enlazados a la canalización. Use la estructura DML_BINDING_TABLE_DESC para describir la tabla de enlace, incluido el subconjunto de los descriptores a los que apunta en el montón descriptor.
  • Cree recursos temporales o persistentes como recursos de búfer de Direct3D 12, descríbalos con DML_BUFFER_BINDING y estructuras de DML_BINDING_DESC, y agréguelos a la tabla de enlace.
  • Si el distribuidor es un operador compilado, cree un búfer de elementos de tensor como un recurso de búfer de Direct3D 12. Complételo o cárguelo, descríbalo con DML_BUFFER_BINDING y estructuras de DML_BINDING_DESC y agréguelo a la tabla de enlace.
  • Pase la tabla de enlace como parámetro al llamar a IDMLCommandRecorder::RecordDispatch.

Recuperación de propiedades de enlace de un distribuidor

La estructura DML_BINDING_PROPERTIES describe las necesidades de enlace de un distribuidor (inicializador de operadores o operador compilado). Estas propiedades relacionadas con el enlace incluyen el número de descriptores que debe enlazar al distribuidor, así como el tamaño en bytes de cualquier recurso temporal o persistente que necesite.

Nota:

Incluso para varios operadores del mismo tipo, no suponga que tendrán los mismos requisitos de enlace. Consulte las propiedades de enlace de cada inicializador y operador que cree.

Llame a IDMLDispatchable::GetBindingProperties para recuperar 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 = ...

El valor descriptorCount que recupera aquí determina el tamaño (mínimo) del montón descriptor y de la tabla de enlace que se crea en los dos pasos siguientes.

DML_BINDING_PROPERTIES también contiene un miembro TemporaryResourceSize, que es el tamaño mínimo en bytes del recurso temporal que se debe enlazar a la tabla de enlace para este objeto de distribuidor. Un valor de cero significa que no se requiere un recurso temporal.

Y un miembro PersistentResourceSize, que es el tamaño mínimo en bytes del recurso persistente que se debe enlazar a la tabla de enlace para este objeto de distribuidor. Un valor de cero significa que no se requiere un recurso persistente. Un recurso persistente, si es necesario, debe proporcionarse durante la inicialización de un operador compilado (donde se enlaza como salida del inicializador de operadores) y durante la ejecución. Hay más información sobre esto más adelante en este tema. Solo los operadores compilados tienen recursos persistentes. Los inicializadores de operadores siempre devuelven un valor de 0 para este miembro.

Si llama a IDMLDispatchable::GetBindingProperties en un inicializador de operadores antes y después de una llamada a IDMLOperatorInitializer::Reset, no se garantiza que los dos conjuntos de propiedades de enlace recuperados sean idénticos.

Descripción, creación y enlace de un montón descriptor

En cuestiones de descriptores, su responsabilidad comienza y termina con el propio montón descriptor. DirectML se encarga de crear y administrar los descriptores dentro del montón que proporcione.

Por lo tanto, use una estructura D3D12_DESCRIPTOR_HEAP_DESC para describir un montón lo suficientemente grande para el número de descriptores que necesita el distribuidor. Después, créelo con ID3D12Device::CreateDescriptorHeap. Por último, llame a ID3D12GraphicsCommandList::SetDescriptorHeaps para enlazar el montón descriptor a la canalización.

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()
);

Descripción y creación de una tabla de enlace

Una tabla de enlace de DirectML representa los recursos que se enlazan a la canalización para que un distribuidor los utilice. Esos recursos pueden ser tensores de entrada y salida (u otros parámetros) para un operador, o pueden ser varios recursos persistentes y temporales con los que funciona un distribuidor.

Use la estructura DML_BINDING_TABLE_DESC para describir la tabla de enlace, incluida la distribución para la que la tabla de enlace representará los enlaces y el intervalo de descriptores (desde el montón descriptor que acaba de crear) a los que desea que haga referencia la tabla de enlace (y en la que DirectML puede escribir descriptores). El valor descriptorCount (una de las propiedades de enlace que recuperamos en el primer paso) nos indica cuál es el tamaño mínimo, en los descriptores, de la tabla de enlace necesaria para el objeto distribuidor. Aquí, usamos ese valor para indicar el número máximo de descriptores que DirectML puede escribir en nuestro montón, desde el inicio de los identificadores de descriptores de CPU y GPU proporcionados.

A continuación, llame a IDMLDevice::CreateBindingTable para crear la tabla de enlace de DirectML. En pasos posteriores, después de crear más recursos para el distribuidor, agregaremos esos recursos a la tabla de enlace.

En lugar de pasar un valor de DML_BINDING_TABLE_DESC a esta llamada, puede pasar nullptr, lo que indica una tabla de enlace vacía.

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()
    )
);

El orden en el que DirectML escribe descriptores en el montón no se especifica, por lo que la aplicación debe tener cuidado de no sobrescribir los descriptores encapsulados por la tabla de enlace. Los identificadores de descriptores de CPU y GPU proporcionados pueden provenir de distintos montones; sin embargo, es responsabilidad de la aplicación asegurarse de que todo el rango de descriptores al que hace referencia el identificador del descriptor de la CPU se copie en el rango al que hace referencia el identificador del descriptor de la GPU antes de la ejecución mediante esta tabla de enlace. El montón descriptor desde el que se proporcionan los identificadores debe tener el tipo D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV. Además, el montón al que GPUDescriptorHandle hace referencia debe ser un montón descriptor visible para sombreador.

Puede restablecer una tabla de enlace para quitar los recursos que haya agregado a ella, mientras que al mismo tiempo cambia cualquier propiedad que haya establecido en su DML_BINDING_TABLE_DESC inicial (para ajustar un nuevo rango de descriptores o para volver a usarlo para otro distribuidor). Solo tiene que realizar los cambios en la estructura de descripción y llamar a IDMLBindingTable::Reset.

dmlBindingTableDesc.Dispatchable = pIDMLCompiledOperator.get();

winrt::check_hresult(
    pIDMLBindingTable->Reset(
        &dmlBindingTableDesc
    )
);

Describir y enlazar los recursos temporales o persistentes

La estructura DML_BINDING_PROPERTIES que rellenamos cuando recuperamos las propiedades de enlace de nuestro distribuidor contiene el tamaño en bytes de cualquier recurso temporal o persistente que necesite el distribuidor. Si cualquiera de estos tamaños es distinto de cero, cree un recurso de búfer de Direct3D 12 y agréguelo a la tabla de enlace.

En el ejemplo de código siguiente, creamos un recurso temporal (temporaryResourceSize bytes de tamaño) para el distribuidor. Se describe cómo deseamos enlazar el recurso y, a continuación, se agrega ese enlace a la tabla de enlace.

Dado que estamos enlazando un único recurso de búfer, se describe el enlace con una estructura DML_BUFFER_BINDING. En esa estructura, especificamos el recurso de búfer de Direct3D 12 (el recurso debe tener la dimensión D3D12_RESOURCE_DIMENSION_BUFFER), así como un desplazamiento y un tamaño en el búfer. También es posible describir un enlace para una matriz de búferes (en lugar de para un único búfer) y la estructura DML_BUFFER_ARRAY_BINDING existe para ese propósito.

Para abstraer la distinción entre un enlace de búfer y un enlace de matriz de búfer, usamos la estructura DML_BINDING_DESC. Puede establecer el miembro Type de DML_BINDING_DESC en DML_BINDING_TYPE_BUFFER o DML_BINDING_TYPE_BUFFER_ARRAY. A continuación, puede establecer el miembro Desc para que apunte a un DML_BUFFER_BINDING o a un DML_BUFFER_ARRAY_BINDING, en función de Type.

En este ejemplo estamos tratando con el recurso temporal, por lo que lo agregamos a la tabla de enlace con una llamada a 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);

Un recurso temporal (si es necesario) es la memoria temporal que se usa internamente durante la ejecución del operador, por lo que no es necesario preocuparse por su contenido. Tampoco es necesario mantenerlo después de que la llamada a IDMLCommandRecorder::RecordDispatch se haya completado en la GPU. Esto significa que la aplicación puede liberar o sobrescribir el recurso temporal entre los distribuidores del operador compilado. El intervalo de búfer proporcionado que se va a enlazar como recurso temporal debe tener su desplazamiento inicial alineado con DML_TEMPORARY_BUFFER_ALIGNMENT. El tipo del montón subyacente al búfer debe ser D3D12_HEAP_TYPE_DEFAULT.

Sin embargo, si el distribuidor notifica un tamaño distinto de cero para su recurso persistente de más larga duración, el procedimiento es un poco diferente. Debe crear un búfer y describir un enlace siguiendo el mismo patrón que se muestra anteriormente. Sin embargo, debe agregarlo a la tabla de enlace del inicializador del operador con una llamada a IDMLBindingTable::BindOutputs, ya que es trabajo del inicializador del operador inicializar el recurso persistente. A continuación, agréguelo a la tabla de enlace del operador compilado con una llamada a IDMLBindingTable::BindPersistentResource. Consulte el ejemplo de código mínimo de la aplicación DirectML para ver este flujo de trabajo en acción. El contenido y la duración del recurso persistente deben conservarse siempre que el operador compilado lo haga. Es decir, si un operador requiere un recurso persistente, la aplicación debe proporcionarla durante la inicialización y, posteriormente, proporcionarla a todas las ejecuciones futuras del operador sin modificar su contenido. DirectML suele usar el recurso persistente para almacenar tablas de búsqueda u otros datos de larga duración que se calculan durante la inicialización de un operador y se reutilizan en futuras ejecuciones de dicho operador. El intervalo de búfer proporcionado que se va a enlazar en forma de búfer persistente debe tener su desplazamiento inicial alineado con DML_PERSISTENT_BUFFER_ALIGNMENT. El tipo del montón subyacente al búfer debe ser D3D12_HEAP_TYPE_DEFAULT.

Describir y enlazar los tensores

Si trabaja con un operador compilado (en lugar de con un inicializador de operador), debe enlazar los recursos de entrada y salida (para tensores y otros parámetros) a la tabla de enlace del operador. El número de enlaces debe coincidir exactamente con el número de entradas del operador, incluidos los tensores opcionales. Los tensores de entrada y salida concretos y el resto de parámetros que utiliza un operador se documentan en el tema de ese operador (por ejemplo, DML_ELEMENT_WISE_IDENTITY_OPERATOR_DESC).

Un recurso tensor es un búfer que contiene los valores de elementos individuales del tensor. Puede cargar y leer de nuevo este búfer hacia o desde la GPU mediante las técnicas normales de Direct3D 12 (Cargar recursos y Leer datos a través de un búfer). Consulte el ejemplo de código mínimo de la aplicación DirectML para ver estas técnicas en acción.

Por último, describa los enlaces de recursos de entrada y salida con las estructuras DML_BUFFER_BINDING y DML_BINDING_DESC y, a continuación, agréguelos a la tabla de enlace del operador compilado con llamadas a IDMLBindingTable::BindInputs y IDMLBindingTable::BindOutputs. Cuando se llama a un método IDMLBindingTable::Bind*, DirectML escribe uno o varios descriptores en el intervalo de descriptores de la 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);

Uno de los pasos para crear un operador DirectML (vea IDMLDevice::CreateOperator) consiste en declarar una o varias estructuras DML_BUFFER_TENSOR_DESC para describir los búferes de datos del tensor que toma y devuelve el operador. Además del tipo y el tamaño del búfer del tensor, puede especificar opcionalmente la marca DML_TENSOR_FLAG_OWNED_BY_DML.

DML_TENSOR_FLAG_OWNED_BY_DML indica que los datos de tensor deben ser propiedad de DirectML y administrarse allí. DirectML realiza una copia de los datos de tensor durante la inicialización del operador y los almacena en el recurso persistente. Esto permite que DirectML vuelva a aplicar formato a los datos de tensor en otros formularios más eficaces. Establecer esta marca puede aumentar el rendimiento, pero normalmente solo es útil para tensores cuyos datos no cambian durante la vigencia del operador (por ejemplo, los tensores de peso). Además, la marca solo se puede usar en tensores de entrada. Cuando la marca se establece en una descripción determinada del tensor, el tensor correspondiente debe enlazarse a la tabla de enlace durante la inicialización del operador y no durante la ejecución (lo que provocará un error). Es lo contrario al comportamiento predeterminado (el comportamiento sin la marca DML_TENSOR_FLAG_OWNED_BY_DML), donde se espera que el tensor se enlace durante la ejecución y no durante la inicialización. Todos los recursos enlazados a DirectML deben ser recursos PREDETERMINADOS o PERSONALIZADOS del montón.

Para obtener más información, consulte IDMLBindingTable::BindInputs y IDMLBindingTable::BindOutputs.

Ejecutar el distribuidor

Pase la tabla de enlace como parámetro al llamar a IDMLCommandRecorder::RecordDispatch.

Cuando se usa la tabla de enlace durante una llamada a IDMLCommandRecorder::RecordDispatch, DirectML enlaza los descriptores de GPU correspondientes a la canalización. No es necesario que los identificadores de descriptores de CPU y GPU apunten a las mismas entradas de un montón descriptor; sin embargo, es responsabilidad de la aplicación asegurarse de que todo el rango de descriptores al que hace referencia el identificador del descriptor de CPU se copie en el rango al que hace referencia el identificador del descriptor de GPU antes de la ejecución mediante esta tabla de enlace.

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()
);

Por último, cierre la lista de comandos de Direct3D 12 y envíela para su ejecución como haría con cualquier otra lista de comandos.

Antes de la ejecución de RecordDispatch en la GPU, debe realizar la transición de todos los recursos enlazados al estado D3D12_RESOURCE_STATE_UNORDERED_ACCESS o a un estado que se pueda promover implícitamente a D3D12_RESOURCE_STATE_UNORDERED_ACCESS, como D3D12_RESOURCE_STATE_COMMON. Una vez completada esta llamada, los recursos permanecen en el estado D3D12_RESOURCE_STATE_UNORDERED_ACCESS. La única excepción a esto es para los montones de carga enlazados al ejecutar un inicializador de operador mientras que uno o varios tensores tienen establecida la marca DML_TENSOR_FLAG_OWNED_BY_DML. En ese caso, los montones de carga enlazados a la entrada deben estar en el estado D3D12_RESOURCE_STATE_GENERIC_READ, y permanecerán en ese estado según sea necesario para todos los montones de carga. Si no se estableció DML_EXECUTION_FLAG_DESCRIPTORS_VOLATILE al compilar el operador, todos los enlaces deben establecerse en la tabla de enlace antes de llamar a RecordDispatch; de lo contrario, el comportamiento no estará bien definido. De lo contrario, si un operador admite el enlace en tiempo de ejecución, el enlace de recursos se puede aplazar hasta que se envíe la lista de comandos de Direct3D 12 a la cola de comandos para su ejecución.

RecordDispatch actúa de manera lógica como una llamada a ID3D12GraphicsCommandList::Dispatch. Por lo tanto, las barreras de la vista de acceso desordenado (UAV) son necesarias para garantizar la ordenación correcta si hay dependencias de datos entre distribuidores. Este método no inserta barreras UAV en los recursos de entrada ni salida. La aplicación debe asegurarse de que se ejecutan las barreras de UAV correctas en cualquier entrada si su contenido depende de una distribución ascendente y en cualquier salida si hay distribuciones descendentes que dependen de esas salidas.

Duración y sincronización de los descriptores y la tabla de enlace

Un buen modelo de enlace en DirectML es que, en segundo plano, la propia tabla de enlace de DirectML esté creando y administrando descriptores de vista de acceso desordenado (UAV) dentro del montón descriptor que proporcione. Por lo tanto, todas las reglas habituales de Direct3D 12 se aplican en torno a la sincronización del acceso a ese montón y a sus descriptores. Es responsabilidad de la aplicación realizar la sincronización correcta entre el trabajo de la CPU y la GPU que usa una tabla de enlace.

Una tabla de enlace no puede sobrescribir un descriptor mientras el descriptor está en uso (por ejemplo, en un marco anterior). Por lo tanto, si desea reutilizar un montón descriptor ya enlazado (por ejemplo, llamando a Bind* de nuevo en una tabla de enlace que apunte a él o sobrescribiendo el montón descriptor manualmente), debe esperar a que el distribuidor que esté usando el montón descriptor actualmente termine de ejecutarse en la GPU. Una tabla de enlace no mantiene una referencia segura en el montón descriptor en el que escribe, por lo que no debe liberar el montón descriptor visible para el sombreador de respaldo hasta que todo el trabajo con esa tabla de enlace haya completado su ejecución en la GPU.

Por otro lado, mientras que una tabla de enlace especifica y administra un montón descriptor, la tabla no contiene ninguna de esas memorias. Es decir, puede liberar o restablecer una tabla de enlace en cualquier momento después de llamar a IDMLCommandRecorder::RecordDispatch con ella (no es necesario esperar a que se complete esa llamada en la GPU, siempre y cuando los descriptores subyacentes sigan siendo válidos).

La tabla de enlace no mantiene referencias seguras en ningún recurso enlazado que la utilice: la aplicación debe asegurarse de que los recursos no se eliminan mientras la GPU los siga usando. Además, una tabla de enlace no es segura para subprocesos: la aplicación no debe llamar a métodos en una tabla de enlace simultáneamente desde diferentes subprocesos sin sincronización.

Y tenga en cuenta que, en cualquier caso, volver a enlazar solo es necesario cuando se cambian los recursos que están enlazados. Si no necesita cambiar los recursos enlazados, puede enlazar una vez al inicio y pasar la misma tabla de enlace cada vez que llame a RecordDispatch.

Para intercalar cargas de trabajo de procesamiento y Machine Learning, asegúrese de que las tablas de enlace de cada marco apunte a rangos del montón descriptor que ya no estén en uso en la GPU.

Opcionalmente, especifique enlaces de operador enlazados en tiempo de ejecución

Si está gestionando un operador compilado (en lugar de un inicializador de operador), tiene la opción de especificar el enlace en tiempo de ejecución para el operador. Sin enlace en tiempo de ejecución, debe establecer todos los enlaces de la tabla de enlace antes de registrar un operador en una lista de comandos. Con el enlace en tiempo de ejecución, puede establecer (o cambiar) enlaces en operadores que ya ha registrado en una lista de comandos, antes de que esta se haya enviado a la cola de comandos.

Para especificar el enlace en tiempo de ejecución, llame a IDMLDevice::CompileOperator con un argumento flags deDML_EXECUTION_FLAG_DESCRIPTORS_VOLATILE.

Consulte también