Associação no DirectML
No DirectML, a associação refere-se à anexação de recursos ao pipeline para a GPU usar durante a inicialização e a execução de seus operadores de aprendizado de máquina. Esses recursos podem ser tensores de entrada e saída, por exemplo, bem como quaisquer recursos temporários ou persistentes que o operador precise.
Este tópico aborda os detalhes conceituais e processuais da associação. Recomendamos que você também leia a documentação completa das APIs que você chama, incluindo parâmetros e Observações.
Ideias importantes sobre a associação
A lista de etapas abaixo contém uma descrição de alto nível das tarefas relacionadas à associação. Você precisa seguir essas etapas sempre que executar um dispatchable. Um check for updates é um inicializador de operador ou um operador compilado. Essas etapas apresentam as ideias, estruturas e métodos importantes envolvidos na associação do DirectML.
As seções subsequentes deste tópico se aprofundam e explicam essas tarefas de associação com mais detalhes, com trechos de código ilustrativos retirados do exemplo de código do aplicativo mínimo do DirectML.
- Chame IDMLDispatchable::GetBindingProperties no dispatchable para determinar quantos descritores ele precisa e também suas necessidades temporárias/persistentes de recursos.
- Crie um heap de descritores do Direct3D 12 com tamanho suficiente para os descritores e associe-o ao pipeline.
- Chame IDMLDevice::CreateBindingTable para criar uma tabela de associação do DirectML que representará os recursos associados ao pipeline. Use a estrutura DML_BINDING_TABLE_DESC para descrever sua tabela de associação, incluindo o subconjunto dos descritores para os quais ela aponta no heap de descritores.
- Crie recursos temporários/persistentes como recursos de buffer do Direct3D 12, descreva-os com as estruturas DML_BUFFER_BINDING e DML_BINDING_DESC e adicione-os à tabela de associação.
- Se o dispatchable for um operador compilado, crie um buffer de elementos tensores como um recurso de buffer do Direct3D 12. Preencha/carregue, descreva-o com as estruturas DML_BUFFER_BINDING e DML_BINDING_DESC e adicione-o à tabela de associação.
- Passe sua tabela de associação como um parâmetro quando chamar IDMLCommandRecorder::RecordDispatch.
Recuperar as propriedades de associação de um dispatchable
A estrutura DML_BINDING_PROPERTIES descreve as necessidades de associação de um dispatchable (inicializador do operador ou operador compilado). Essas propriedades relacionadas à associação incluem o número de descritores que você deve associar ao dispatchable, bem como o tamanho em bytes de qualquer recurso temporário e/ou persistente necessário.
Observação
Mesmo para vários operadores do mesmo tipo, não presuma que eles terão os mesmos requisitos de associação. Consulte as propriedades da associação para cada inicializador e operador criado.
Chame IDMLDispatchable::GetBindingProperties para recuperar um 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 = ...
O valor descriptorCount
recuperado aqui determina o tamanho (mínimo) do heap de descritores e da tabela de associação que você criar nas próximas duas etapas.
DML_BINDING_PROPERTIES também contém um membro TemporaryResourceSize
, que é o tamanho mínimo em bytes do recurso temporário que deve seguir a tabela de associação para esse objeto dispatchable. Um valor zero significa que um recurso temporário não é necessário.
E um membro PersistentResourceSize
, que é o tamanho mínimo em bytes do recurso persistente que deve seguir a tabela de associação para esse objeto dispatchable. Um valor zero significa que um recurso persistente não é necessário. Um recurso persistente, se necessário, deve ser fornecido durante a inicialização de um operador compilado (onde ele é associado como uma saída do inicializador do operador), bem como durante a execução. Falaremos mais sobre isso mais adiante neste tópico. Somente operadores compilados têm recursos persistentes. Os inicializadores de operador sempre retornam um valor 0 para esse membro.
Se você chamar IDMLDispatchable::GetBindingProperties em um inicializador do operador antes e depois de uma chamada para IDMLOperatorInitializer::Reset, os dois conjuntos de propriedades da associação recuperados não serão garantidamente idênticos.
Descrever, criar e associar um heap de descritores
Em termos de descritores, sua responsabilidade começa e termina com o próprio descritor heap. O próprio DirectML se encarrega de criar e gerenciar os descritores dentro do heap que você fornecer.
Portanto, use uma estrutura D3D12_DESCRIPTOR_HEAP_DESC para descrever um heap com tamanho suficiente para o número de descritores que o dispatchable precisa. Em seguida, crie-o com ID3D12Device::CreateDescriptorHeap. E, por último, chame ID3D12GraphicsCommandList::SetDescriptorHeaps para associar seu heap de descritores ao pipeline.
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()
);
Descrever e criar uma tabela de associação
Uma tabela de associação DirectML representa os recursos que você associa ao pipeline para um dispatchable usar. Esses recursos podem ser tensores de entrada e saída (ou outros parâmetros) para um operador, ou podem ser vários recursos persistentes e temporários com os quais um dispatchable trabalha.
Use a estrutura DML_BINDING_TABLE_DESC para descrever sua tabela de associação, incluindo o dispatchable para o qual a tabela de associação representará as associações e o intervalo de descritores (do heap de descritores que você acabou de criar) para o qual você deseja que a tabela de associação se refira (e no qual o DirectML pode gravar descritores). O valor descriptorCount
(uma das propriedades de associação que recuperamos na primeira etapa) nos informa o tamanho mínimo, em descritores, da tabela de associação necessária para o objeto dispatchable. Aqui, usamos esse valor para indicar o número máximo de descritores que o DirectML tem permissão para gravar em nosso heap, desde o início dos identificadores de descritores de CPU e GPU fornecidos.
Em seguida, chame IDMLDevice::CreateBindingTable para criar a tabela de associação do DirectML. Em etapas posteriores, após criarmos mais recursos para o dispatchable, adicionaremos esses recursos à tabela de associação.
Em vez de passar um DML_BINDING_TABLE_DESC para essa chamada, você pode passar o nullptr
, indicando uma tabela de associação vazia.
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()
)
);
A ordem na qual o DirectML grava os descritores no heap não é especificada, portanto, seu aplicativo deverá tomar cuidado para não substituir os descritores encapsulados pela tabela de associação. Os identificadores do descritor da CPU e GPU fornecidos podem vir de heaps diferentes, porém, é responsabilidade do aplicativo garantir que todo o intervalo de descritores referido pelo identificador do descritor da CPU seja copiado no intervalo referido pelo identificador do descritor da GPU, antes da execução, usando essa tabela de associação. O heap do descritor do qual os identificadores são fornecidos deve ter o tipo D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV. Além disso, o heap referido pelo GPUDescriptorHandle
deve ser um heap de descritor visível no sombreador.
Você pode redefinir uma tabela de associação para remover quaisquer recursos adicionados a ela e, ao mesmo tempo, alterar qualquer propriedade definida no DML_BINDING_TABLE_DESC inicial (para encapsular um novo intervalo de descritores ou reutilizá-lo para um dispatchable diferente). Basta fazer as alterações na estrutura de descrição e chamar IDMLBindingTable::Reset.
dmlBindingTableDesc.Dispatchable = pIDMLCompiledOperator.get();
winrt::check_hresult(
pIDMLBindingTable->Reset(
&dmlBindingTableDesc
)
);
Descrever e associar quaisquer recursos temporários/persistentes
A estrutura DML_BINDING_PROPERTIES que preenchemos quando recuperamos as propriedades de associação do dispatchable contém o tamanho em bytes de qualquer recurso temporário e/ou persistente do qual o dispatchable precisa. Se um desses tamanhos for diferente de zero, crie um recurso de buffer do Direct3D 12 e adicione-o à tabela de associação.
No exemplo de código abaixo, criamos um recurso temporário (temporaryResourceSize
bytes de tamanho) para o dispatchable. Descrevemos como queremos associar o recurso e, em seguida, adicionamos essa associação à tabela de associações.
Como estamos associando um único recurso de buffer, descrevemos nossa associação com uma estrutura DML_BUFFER_BINDING. Nessa estrutura, especificamos o recurso de buffer do Direct3D 12 (o recurso deve ter dimensão D3D12_RESOURCE_DIMENSION_BUFFER), bem como um deslocamento e tamanho no buffer. Também é possível descrever uma associação para uma matriz de buffers (em vez de para um único buffer), e a estrutura DML_BUFFER_ARRAY_BINDING existe para essa finalidade.
Para abstrair a distinção entre uma associação de buffer e uma associação de matriz de buffer, usamos a estrutura DML_BINDING_DESC. Você pode definir o membro Type
do DML_BINDING_DESC como DML_BINDING_TYPE_BUFFER ou DML_BINDING_TYPE_BUFFER_ARRAY. E você pode definir o membro Desc
para apontar para um DML_BUFFER_BINDING ou para um DML_BUFFER_ARRAY_BINDING, dependendo do Type
.
Estamos lidando com o recurso temporário neste exemplo, então o adicionamos à tabela de associação com uma chamada para 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);
Um recurso temporário (se for necessário) é a memória de rascunho usada internamente durante a execução do operador, para que você não precise se preocupar com o conteúdo. Também não será necessário mantê-lo por perto depois que a chamada para IDMLCommandRecorder::RecordDispatch for concluída na GPU. Isso significa que seu aplicativo pode liberar ou substituir o recurso temporário entre as expedições do operador compilado. O intervalo de buffer fornecido a ser associado como o recurso temporário deve ter seu deslocamento inicial alinhado a DML_TEMPORARY_BUFFER_ALIGNMENT. O tipo de heap subjacente ao buffer deve ser D3D12_HEAP_TYPE_DEFAULT.
No entanto, se o dispatchable relatar um tamanho diferente de zero para seu recurso persistente de vida mais longa, o procedimento será um pouco diferente. Você deve criar um buffer e descrever uma associação seguindo o mesmo padrão mostrado acima. Porém, adicione-o à tabela de associação do inicializador do operador com uma chamada para IDMLBindingTable::BindOutputs, porque é responsabilidade do inicializador do operador inicializar o recurso persistente. Em seguida, adicione-o à tabela de associação do operador compilado com uma chamada para IDMLBindingTable::BindPersistentResource. Confira o exemplo de código do aplicativo DirectML mínimo para ver esse fluxo de trabalho em ação. O conteúdo e a vida útil do recurso persistente devem persistir enquanto o operador compilado o fizer. Ou seja, se um operador exige um recurso persistente, o aplicativo deverá fornecê-lo durante a inicialização e, subsequentemente, também fornecê-lo a todas as execuções futuras do operador sem modificar seu conteúdo. O recurso persistente é normalmente usado pelo DirectML para armazenar tabelas de pesquisa ou outros dados de longa duração que são computados durante a inicialização de um operador e reutilizados em execuções futuras desse operador. O intervalo de buffer fornecido a ser associado como o recurso temporário deve ter seu deslocamento inicial alinhado a DML_PERSISTENT_BUFFER_ALIGNMENT. O tipo de heap subjacente ao buffer deve ser D3D12_HEAP_TYPE_DEFAULT.
Descrever e associar todos os tensores
Se você estiver lidando com um operador compilado (em vez de com um inicializador de operador), precisará associar os recursos de entrada e saída (para tensores e outros parâmetros) à tabela de associação do operador. O número de associações deve corresponder exatamente ao número de entradas do operador, incluindo tensores opcionais. Os tensores de entrada e saída específicos e outros parâmetros que um operador usa estão documentados no tópico para esse operador (por exemplo, DML_ELEMENT_WISE_IDENTITY_OPERATOR_DESC).
Um recurso tensor é um buffer que contém os valores do elemento individual do tensor. Você carrega e retorna esse buffer de/para a GPU usando as técnicas regulares do Direct3D 12 (Carregar recursos e Ler dados de retorno por meio de um buffer). Confira o exemplo de código do aplicativo DirectML mínimo para ver essas técnicas em ação.
Por fim, descreva as associações de recursos de entrada e saída com as estruturas DML_BUFFER_BINDING e DML_BINDING_DESC e, em seguida, adicione-as à tabela de associação do operador compilado com chamadas para IDMLBindingTable::BindInputs e IDMLBindingTable::BindOutputs. Quando você chama um método IDMLBindingTable::Bind*, o DirectML grava um ou mais descritores no intervalo de descritores da 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);
Uma das etapas da criação de um operador DirectML (confira IDMLDevice::CreateOperator) é declarar uma ou mais estruturas DML_BUFFER_TENSOR_DESC para descrever os buffers de dados do tensor que o operador recebe e retorna. Além do tipo e tamanho do buffer de tensores, você pode opcionalmente especificar o sinalizador DML_TENSOR_FLAG_OWNED_BY_DML.
DML_TENSOR_FLAG_OWNED_BY_DML indica que os dados do tensor devem ser de propriedade e gerenciados pelo DirectML. O DirectML faz uma cópia dos dados do tensor durante a inicialização do operador e os armazena no recurso persistente. Isso permite que o DirectML execute a reformatação dos dados do tensor em outros formulários mais eficientes. A configuração desse sinalizador pode aumentar o desempenho, mas normalmente é útil apenas para tensores cujos dados não são alterados durante a vida útil do operador (por exemplo, tensores de peso). E o sinalizador só pode ser usado em tensores de entrada. Quando o sinalizador for definido em uma descrição de tensor específica, o tensor correspondente deverá ser associado à tabela de associação durante a inicialização do operador e não durante a execução (o que resultará em um erro). Isso é o oposto do comportamento padrão (o comportamento sem o sinalizador DML_TENSOR_FLAG_OWNED_BY_DML), no qual o esperado é que o tensor seja associado durante a execução e não durante a inicialização. Todos os recursos associados ao DirectML devem ser recursos de heap PADRÃO ou PERSONALIZADOS.
Para obter mais informações, consulte IDMLBindingTable::BindInputs e IDMLBindingTable::BindOutputs.
Execute o dispatchable
Passe sua tabela de associação como um parâmetro quando chamar IDMLCommandRecorder::RecordDispatch.
Quando você usa a tabela de associação durante uma chamada para IDMLCommandRecorder::RecordDispatch, o DirectML associa os descritores de GPU correspondentes ao pipeline. Os identificadores do descritor da CPU e GPU não precisam vir das mesmas entradas em um heap de descritores, porém, é responsabilidade do aplicativo garantir que todo o intervalo de descritores referido pelo identificador do descritor da CPU seja copiado no intervalo referido pelo identificador do descritor da GPU, antes da execução, usando essa tabela de associação.
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 fim, feche sua lista de comandos do Direct3D 12 e envie-a para execução como faria com qualquer outra lista de comandos.
Antes da execução do RecordDispatch na GPU, você deve fazer a transição de todos os recursos associados para o estado D3D12_RESOURCE_STATE_UNORDERED_ACCESS ou para um estado implicitamente promocional para D3D12_RESOURCE_STATE_UNORDERED_ACCESS, como D3D12_RESOURCE_STATE_COMMON. Após a conclusão dessa chamada, os recursos permanecem no estado D3D12_RESOURCE_STATE_UNORDERED_ACCESS. A única exceção a isso é para heaps de upload associados ao executar um inicializador de operador e enquanto um ou mais tensores tiverem o sinalizador DML_TENSOR_FLAG_OWNED_BY_DML definido. Nesse caso, todos os heaps de upload associados à entrada devem estar no estado D3D12_RESOURCE_STATE_GENERIC_READ e permanecerão nesse estado, conforme exigido por todos os heaps de upload. Se DML_EXECUTION_FLAG_DESCRIPTORS_VOLATILE não foi definido ao compilar o operador, todas as associações devem ser definidas na tabela de associação antes que RecordDispatch seja chamado, caso contrário, o comportamento será indefinido. Caso contrário, se um operador for compatível com uma associação tardia, a associação de recursos poderá ser adiada até que a lista de comandos do Direct3D 12 seja enviada à fila de comandos para execução.
RecordDispatch age logicamente como uma chamada para ID3D12GraphicsCommandList::Dispatch. Dessa forma, as barreiras de exibição de acesso não ordenado (UAV) são necessárias para garantir a ordenação correta se houver dependências de dados entre as expedições. Esse método não insere barreiras de UAV em recursos de entrada ou saída. Seu aplicativo deve garantir que as barreiras de UAV corretas sejam executadas em todas as entradas, se o conteúdo dele depender de uma expedição upstream, e em todas as saídas, se houver expedições downstream que dependam dessas saídas.
Vida útil e sincronização dos descritores e da tabela de associação
Um bom modelo mental de associação no DirectML é que, nos bastidores, a própria tabela de associação do DirectML está criando e gerenciando descritores de exibição de acesso não ordenado (UAV) dentro do heap de descritores fornecido. Portanto, todas as regras normais do Direct3D 12 se aplicam à sincronização do acesso a esse heap e a seus descritores. É responsabilidade do seu aplicativo executar a sincronização correta entre o trabalho da CPU e da GPU que usa uma tabela de associação.
Uma tabela de associação não pode substituir um descritor enquanto ele estiver em uso (por um quadro anterior, por exemplo). Portanto, se você quiser reutilizar um heap de descritores já associado (por exemplo, chamando Bind* novamente em uma tabela de associação que aponte para ele, ou substituindo o heap de descritores manualmente), espere até que o dispatchable que estiver usando o heap de descritores termine de executar na GPU. Uma tabela de associação não mantém uma referência forte no heap de descritores no qual ela grava, portanto, não libere o heap de descritores de apoio, visível no sombreador, até que todas as tarefas que estiverem usando essa tabela de associação tenha concluído a execução na GPU.
Por outro lado, embora uma tabela de associação especifique e gerencie um heap de descritores, a tabela em si não contém nenhuma dessa memória. Assim, você pode liberar ou redefinir uma tabela de associação a qualquer momento depois que chamar o IDMLCommandRecorder::RecordDispatch com ela (não é necessário esperar que a chamada seja concluída na GPU, desde que os descritores subjacentes permaneçam válidos).
A tabela de associação não mantém referências fortes em nenhum recurso associado ao seu uso. Seu aplicativo deve garantir que os recursos não sejam excluídos enquanto ainda estiverem em uso pela GPU. Além disso, uma tabela de associação não é segura para threads. Seu aplicativo não deve chamar métodos simultaneamente em uma tabela de associação de threads diferentes sem sincronização.
E considere que, de qualquer forma, a reassociação será necessária somente quando você alterar quais recursos estão associados. Se você não precisar alterar os recursos associados, poderá associar uma vez na inicialização e passar a mesma tabela de associação sempre que chamar RecordDispatch.
Para intercalar as cargas de trabalho de aprendizado de máquina e de renderização, verifique apenas se as tabelas de associação de cada quadro estão apontando para os intervalos do heap de descritores que ainda não estão em uso na GPU.
Opcionalmente, especifique as associações do operador que foram associadas tardiamente
Se você estiver lidando com um operador compilado (em vez de com um inicializador de operador), terá a opção de especificar a associação tardia para o operador. Sem associação tardia, você deve definir todas as associações na tabela de associação antes de gravar um operador em uma lista de comandos. Com a associação tardia, você pode definir (ou alterar) associações em operadores que você já registrou em uma lista de comandos, antes de ser enviada para a fila de comandos.
Para especificar a associação tardia, chame IDMLDevice::CompileOperator com um argumento flags
de DML_EXECUTION_FLAG_DESCRIPTORS_VOLATILE.