Partilhar via


Vinculaçã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 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 vinculação. Recomendamos que você também leia na íntegra a documentação das APIs que você chama, incluindo parâmetros e observações.

Ideias importantes na vinculação

A lista de etapas abaixo contém uma descrição de alto nível de tarefas relacionadas à vinculação. Você precisa seguir estas etapas sempre que executar um despachável — um despachável é um inicializador de operador ou um operador compilado. Estas etapas apresentam as ideias, estruturas e métodos importantes envolvidos na vinculação do DirectML.

As seções subsequentes neste tópico aprofundam e explicam essas tarefas de vinculação com mais detalhes, com trechos de código ilustrativos retirados do exemplo de código mínimo do aplicativo DirectML .

  • Chame IDMLDispatchable::GetBindingProperties no dispatchable para determinar quantos descritores ele precisa e também suas necessidades de recursos temporários/persistentes.
  • Crie uma pilha de descritores Direct3D 12 grande o suficiente para os descritores e associe-a ao pipeline.
  • Chame IDMLDevice::CreateBindingTable para criar uma tabela de vinculação DirectML para representar os recursos vinculados ao pipeline. Use a estrutura DML_BINDING_TABLE_DESC para descrever sua tabela de vinculação, incluindo o subconjunto dos descritores para os quais ela aponta no heap do descritor.
  • Crie recursos temporários/persistentes como recursos de buffer do Direct3D 12, descreva-os com estruturas DML_BUFFER_BINDING e DML_BINDING_DESC e adicione-os à tabela de vinculação.
  • Se o operador a ser despachado for um operador compilado, crie um buffer de elementos de tensor como um recurso de buffer do Direct3D 12. Preencha-o/carregue-o, descreva-o com DML_BUFFER_BINDING e DML_BINDING_DESC estruturas e adicione-o à tabela de vinculação.
  • Passe sua tabela de vinculação como um parâmetro quando você chamar IDMLCommandRecorder::RecordDispatch.

Recuperar as propriedades de vinculação de um objeto despachável

A estrutura DML_BINDING_PROPERTIES descreve as necessidades de ligação de um elemento despachável (inicializador de operador ou operador compilado). Essas propriedades relacionadas à associação incluem o número de descritores que você deve vincular ao despachável, bem como o tamanho em bytes de qualquer recurso temporário e/ou persistente de que ele precise.

Observação

Mesmo para vários operadores do mesmo tipo, não faça suposições sobre eles terem os mesmos requisitos vinculativos. Consulte as propriedades de associação para cada inicializador e operador que você criar.

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 que se recupera aqui determina o tamanho (mínimo) do heap do descritor e da tabela de vinculação que se cria nas duas etapas seguintes.

DML_BINDING_PROPERTIES também contém um TemporaryResourceSize membro, que é o tamanho mínimo em bytes do recurso temporário que deve ser vinculado à tabela de vinculação para esse objeto despachável. Um valor zero significa que um recurso temporário não é necessário.

E um PersistentResourceSize membro, que é o tamanho mínimo em bytes do recurso persistente que deve ser associado à tabela de associações para este objeto que pode ser despachado. 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 é vinculado como uma saída do inicializador do operador), bem como durante a execução. Há mais sobre isso mais adiante neste tópico. Somente os operadores compilados têm recursos persistentes — os inicializadores do operador sempre retornam um valor de 0 para esse membro.

Se você chamar IDMLDispatchable::GetBindingProperties em um inicializador de operador antes e depois de uma chamada para IDMLOperatorInitializer::Reset, não é garantido que os dois conjuntos de propriedades de vinculação recuperados sejam idênticos.

Descrever, criar e vincular uma pilha de descritor

Em termos de descritores, a sua responsabilidade começa e termina com o próprio descritor heap. O próprio DirectML se encarrega de criar e gerir os descritores dentro da pilha que o utilizador fornece.

Portanto, use uma estrutura D3D12_DESCRIPTOR_HEAP_DESC para descrever uma pilha grande o suficiente para o número de descritores que o despachável precisa. Em seguida, crie-o com ID3D12Device::CreateDescriptorHeap. E, por último, chame ID3D12GraphicsCommandList::SetDescriptorHeaps para vincular o heap do descritor 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 vinculação

Uma tabela de vinculação DirectML representa os recursos que você vincula ao pipeline para um despachável 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 despachável trabalha.

Use a estrutura DML_BINDING_TABLE_DESC para descrever sua tabela de vinculação, incluindo o despachável para o qual a tabela de vinculação representará as associações e o intervalo de descritores (do heap do descritor que você acabou de criar) ao qual você deseja que a tabela de vinculação se refira (e no qual o DirectML pode escrever descritores). O descriptorCount valor (uma das propriedades de vinculação que recuperamos na primeira etapa) nos diz qual é o tamanho mínimo, em descritores, da tabela de vinculação necessária para o objeto despachável. Aqui, usamos esse valor para indicar o número máximo de descritores que o DirectML pode escrever no nosso heap, começando a partir dos identificadores de descritor de CPU e GPU fornecidos.

Em seguida, chame IDMLDevice::CreateBindingTable para criar a tabela de vinculação DirectML. Em etapas posteriores, depois de criarmos mais recursos para o despachável, adicionaremos esses recursos à tabela de associação.

Em vez de passar um DML_BINDING_TABLE_DESC para esta chamada, você pode passar nullptr, indicando uma tabela de vinculaçã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 descritores no heap não é especificada, portanto, seu aplicativo deve tomar cuidado para não substituir os descritores encapsulados pela tabela de vinculação. Os manipuladores de descritores de CPU e GPU fornecidos podem vir de heaps diferentes; todavia, é responsabilidade do seu aplicativo assegurar que toda a gama de descritores mencionada pelo manipulador de descritores da CPU seja transferida para a gama referida pelo manipulador de descritores da GPU antes da execução usando esta tabela de vinculação. A pilha do descritor a partir da qual as alças são fornecidas deve ter o tipo D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV. Além disso, a pilha referida pelo GPUDescriptorHandle deve ser uma pilha de descritor visível pelo sombreador.

Você pode redefinir uma tabela de vinculação para remover quaisquer recursos que tenha adicionado a ela e, ao mesmo tempo, alterar qualquer propriedade definida em sua DML_BINDING_TABLE_DESC inicial (para encapsular um novo intervalo de descritores ou para reutilizá-la para um despachável 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 vincular quaisquer recursos temporários/persistentes

A estrutura de DML_BINDING_PROPERTIES que preenchemos quando recuperamos as propriedades de vinculação de nosso despachável contém o tamanho em bytes de qualquer recurso temporário e/ou persistente de que o despachável precisa. Se qualquer um desses tamanhos for diferente de zero, crie um recurso de buffer do Direct3D 12 e adicione-o à tabela de vinculação.

No exemplo de código abaixo, criamos um recurso temporário (temporaryResourceSize bytes de tamanho) para o despachável. Descrevemos como desejamos vincular o recurso e, em seguida, adicionamos essa vinculação à tabela de vinculação.

Como estamos vinculando 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 dentro do buffer. Também é possível descrever uma ligaçã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 ligação de buffer e uma ligação de matriz de buffer, usamos a estrutura DML_BINDING_DESC . Você pode definir o Type membro do DML_BINDING_DESC como DML_BINDING_TYPE_BUFFER ou DML_BINDING_TYPE_BUFFER_ARRAY. E você pode definir o Desc membro 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 vinculaçã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 necessário) é a memória de rascunho que é usada internamente durante a execução do operador, então você não precisa se preocupar com seu conteúdo. Também não é necessário mantê-lo por perto a partir do momento em que a sua chamada para IDMLCommandRecorder::RecordDispatch for concluída na GPU. Isso significa que a sua aplicação pode liberar ou sobrescrever o recurso temporário durante as execuções do operador compilado. O intervalo de buffer fornecido para ser vinculado como o recurso temporário deve ter o seu offset inicial alinhado a DML_TEMPORARY_BUFFER_ALIGNMENT. O tipo de heap que sustenta o buffer deve ser D3D12_HEAP_TYPE_DEFAULT.

No entanto, se o despachável 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. Mas adicione-o à tabela de vinculação do inicializador do operador com uma chamada para IDMLBindingTable::BindOutputs, porque é tarefa do inicializador do operador inicializar o recurso persistente. Em seguida, adicione-o à tabela de vinculação do operador compilado com uma chamada para IDMLBindingTable::BindPersistentResource. Consulte o exemplo de código de aplicativo DirectML mínimo para ver esse fluxo de trabalho em ação. O conteúdo e o tempo de vida do recurso persistente devem persistir enquanto o operador compilado o fizer. Ou seja, se um operador requer um recurso persistente, seu aplicativo deve fornecê-lo durante a inicialização e, posteriormente, 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 calculados durante a inicialização de um operador e reutilizados em execuções futuras desse operador. O intervalo do buffer fornecido a ser vinculado como buffer persistente deve ter o seu deslocamento inicial alinhado com DML_PERSISTENT_BUFFER_ALIGNMENT. O tipo de heap que sustenta o buffer deve ser D3D12_HEAP_TYPE_DEFAULT.

Descrever e associar quaisquer tensores

Se você estiver lidando com um operador compilado (em vez de com um inicializador de operador), precisará vincular recursos de entrada e saída (para tensores e outros parâmetros) à tabela de vinculação do operador. O número de ligaçõ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 de elementos individuais do tensor. Você carrega e lê de volta esse buffer de/para a GPU usando as técnicas regulares do Direct3D 12 (Carregar recursos e Ler dados de volta através de um buffer). Consulte o exemplo de código de aplicativo DirectML mínimo para ver essas técnicas em ação.

Por fim, descreva suas ligações de recursos de entrada e saída com estruturas DML_BUFFER_BINDING e DML_BINDING_DESC e, em seguida, adicione-as à tabela de vinculação do operador compilado com chamadas para IDMLBindingTable::BindInputs e IDMLBindingTable::BindOutputs. Quando você chama um IDMLBindingTable::Bind* método, DirectML grava um ou mais descritores no intervalo de descritores de 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 na criação de um operador DirectML (consulte IDMLDevice::CreateOperator) é declarar uma ou mais estruturas DML_BUFFER_TENSOR_DESC para descrever os buffers de dados tensores que o operador usa e retorna. Além do tipo e tamanho do buffer tensor, pode-se, opcionalmente, especificar o indicador DML_TENSOR_FLAG_OWNED_BY_DML.

DML_TENSOR_FLAG_OWNED_BY_DML indica que os dados tensores devem ser de propriedade e gerenciados pelo DirectML. O DirectML faz uma cópia dos dados tensores durante a inicialização do operador e os armazena no recurso persistente. Isso permite que o DirectML execute a reformatação dos dados tensores em outros formulários mais eficientes. Definir esse sinalizador pode aumentar o desempenho, mas normalmente só é útil para tensores cujos dados não mudam durante o tempo de vida do operador (por exemplo, tensores de peso). E a bandeira só pode ser usada em tensores de entrada. Quando o sinalizador é definido em uma descrição de tensor específica, o tensor correspondente deve ser vinculado à tabela de vinculaçã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), onde se espera que o tensor seja vinculado durante a execução, e não durante a inicialização. Todos os recursos vinculados ao DirectML devem ser recursos heap do tipo DEFAULT ou CUSTOM.

Para obter mais informações, consulte IDMLBindingTable::BindInputs e IDMLBindingTable::BindOutputs.

Execute o despachável

Passe sua tabela de vinculação como um parâmetro quando você chamar IDMLCommandRecorder::RecordDispatch.

Quando você usa a tabela de vinculação durante uma chamada para IDMLCommandRecorder::RecordDispatch, o DirectML vincula os descritores de GPU correspondentes ao pipeline. Os identificadores do descritor de CPU e GPU não são obrigados a apontar para as mesmas entradas em um heap de descritor, no entanto, é responsabilidade do seu aplicativo garantir que todo o intervalo de descritores referido pelo identificador do descritor da CPU seja copiado para o intervalo referido pelo identificador do descritor da GPU antes da execução usando esta tabela de vinculaçã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()
);

Finalmente, feche a 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 vinculados 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 vinculados quando se executa um inicializador de operador e enquanto um ou mais tensores têm a flag DML_TENSOR_FLAG_OWNED_BY_DML definida. Nesse caso, qualquer pilha de upload destinada à entrada deve estar no estado D3D12_RESOURCE_STATE_GENERIC_READ e permanecerá nesse estado, conforme exigido por todas as pilhas de upload. Se DML_EXECUTION_FLAG_DESCRIPTORS_VOLATILE não foi definido ao compilar o operador, todas as ligações devem ser definidas na tabela de vinculação antes de RecordDispatch ser chamado, caso contrário, o comportamento é indefinido. Caso contrário, se um operador oferecer suporte à vinculação tardia, a vinculaçã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. Como tal, as barreiras de visualização de acesso não ordenado (UAV) são necessárias para garantir a ordenação correta se houver dependências de dados entre despachos. Este método não insere barreiras UAV em recursos de entrada ou saída. A sua aplicação deve assegurar que as barreiras de UAV corretas sejam executadas em todas as entradas, caso o seu conteúdo dependa de um despacho a montante, e em todas as saídas, se houver despachos a jusante que dependam dessas saídas.

Tempo de vida e sincronização de descritores e tabela de vinculação

Um bom modelo mental de vinculação no DirectML é que, nos bastidores, a própria tabela de vinculação do DirectML está criando e gerindo descritores de acesso não ordenado (UAV) dentro da pilha de descritores que você fornece. Portanto, todas as regras usuais do Direct3D 12 se aplicam à sincronização do acesso a esse heap e aos seus descritores. É responsabilidade do seu aplicativo executar a sincronização correta entre o trabalho da CPU e da GPU que usa uma tabela de vinculação.

Uma tabela de vinculação não pode substituir um descritor enquanto o descritor estiver em uso (por um quadro anterior, por exemplo). Portanto, se tu quiseres reutilizar um heap de descritor já vinculado (por exemplo, chamando Bind* novamente numa tabela de vinculação que aponte para ele, ou ao substituir o heap de descritor manualmente), então deves esperar que o despachável que está atualmente a usar o heap de descritor termine de executar na GPU. Uma tabela de vinculação não mantém uma referência forte no heap de descritores no qual escreve, portanto, não deve libertar o heap de descritores visível pelo sombreador de suporte até que todo o trabalho que utilize essa tabela de vinculação tenha concluído a sua execução na GPU.

Por outro lado, embora uma tabela de vinculação especifique e gerencie um heap de descritor, a tabela em si não contém nenhuma dessa memória. Assim, você pode liberar ou redefinir uma tabela de vinculação a qualquer momento depois de ter chamado IDMLCommandRecorder::RecordDispatch com ela (você não precisa esperar que essa chamada seja concluída na GPU, desde que os descritores subjacentes permaneçam válidos).

A tabela de vinculação não mantém referências fortes em nenhum recurso vinculado a ela — 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 vinculação não é segura para execução simultânea — a sua aplicação não deve chamar métodos numa tabela de vinculação a partir de diferentes threads sem sincronização.

E considere que, em qualquer caso, a revinculação é necessária apenas quando você altera quais recursos estão vinculados. Se você não precisar alterar os recursos vinculados, poderá vincular uma vez na inicialização e passar a mesma tabela de vinculação sempre que chamar RecordDispatch.

Para intercalar tarefas de machine learning e renderização, garanta que as tabelas de vinculação de cada quadro apontem para intervalos do heap do descritor que ainda não estão em uso na GPU.

Opcionalmente, especifique as ligações de operador vinculadas tardiamente

Se você estiver lidando com um operador compilado (em vez de com um inicializador de operador), então você tem a opção de especificar a vinculação tardia para o operador. Sem vinculação tardia, você deve definir todas as associações na tabela de vinculação antes de gravar um operador em uma lista de comandos. Com a vinculação tardia, você pode definir (ou alterar) associações em operadores que você já gravou em uma lista de comandos, antes que ela tenha sido enviada para a fila de comandos.

Para especificar a vinculação tardia, chame IDMLDevice::CompileOperator com um flags argumento de DML_EXECUTION_FLAG_DESCRIPTORS_VOLATILE.

Ver também

  • de IA do Windows