DirectML でのバインド

DirectML では、"バインド" は、お使いの機械学習演算子の初期化や実行中に GPU によって使用されるパイプラインへのリソースの添付ファイルを差します。 これらのリソースには、たとえば、入力および出力テンソルのほか、演算子が必要とする任意の一時的または永続的なリソースを使用できます。

このトピックではバインドの概念と手順の詳細を説明します。 呼び出す API のドキュメントを、パラメーターや備考なども含めてすべて読むことをお勧めします。

バインドにおける重要な考え方

次の手順の一覧には、バインドに関連したタスクの概要が含まれています。 ディスパッチ可能要素を実行するたびに、これらの手順に従う必要があります。ディスパッチ可能要素は、演算子初期化子またはコンパイル済み演算子のいずれかです。 これらの手順では、DirectML バインドに関連する重要なアイデア、構造体、およびメソッドを紹介します。

このトピック内の後続のセクションでは、最小の DirectML アプリケーションのコード例から抜粋した説明用のコード スニペットを使用して、これらのバインド タスクを深く掘り下げて説明します。

  • ディスパッチ可能要素で IDMLDispatchable::GetBindingProperties を呼び出して、必要とする記述子の数のほか、一時的/永続的なリソースのニーズも判断します。
  • 記述子に対して十分な大きさの Direct3D 12 記述子ヒープを作成し、それをパイプラインにバインドします。
  • IDMLDevice::CreateBindingTable を呼び出して、パイプラインにバインドされるリソースを表す DirectML バインド テーブルを作成します。 DML_BINDING_TABLE_DESC 構造体を使用して、記述子ヒープ内で指す記述子のサブセットを含むバインド テーブルを記述します。
  • 一時的または永続的なリソースを Direct3D 12 バッファー リソースとして作成し、DML_BUFFER_BINDING および DML_BINDING_DESC 構造体を使用してそれらを記述し、バインド テーブルに追加します。
  • ディスパッチ可能要素がコンパイル済み演算子の場合は、テンソル要素のバッファーを Direct3D 12 バッファー リソースとして作成します。 これを設定またはアップロードし、DML_BUFFER_BINDING および DML_BINDING_DESC 構造体を使用して記述し、バインド テーブルに追加します。
  • IDMLCommandRecorder::RecordDispatch を呼び出すときにバインド テーブルをパラメーターとして渡します。

ディスパッチ可能要素のバインド プロパティを取得する

DML_BINDING_PROPERTIES 構造体は、ディスパッチ可能要素 (演算子初期化子またはコンパイル済み演算子) のバインドのニーズを記述します。 これらのバインド関連のプロパティには、ディスパッチ可能要素にバインドする必要がある記述子の数と、必要となる一時的または永続的なリソースのサイズ (バイト単位) が含まれます。

Note

同じ型の複数の演算子であっても、それらのバインド要件が同じであると想定しないでください。 作成した初期化子と演算子ごとにバインド プロパティを照会します。

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 値によって、次の 2 つの手順で作成する記述子ヒープとバインド テーブルの (最小) サイズが決まります。

DML_BINDING_PROPERTIES には TemporaryResourceSize メンバーも含まれています。これは、このディスパッチ可能オブジェクトのバインド テーブルにバインドする必要がある一時的なリソースの最小サイズ (バイト単位) です。 値がゼロの場合は、一時的なリソースが必要でないことを意味します。

また、PersistentResourceSize メンバーは、このディスパッチ可能オブジェクトのバインド テーブルにバインドする必要がある永続的なリソースの最小サイズ (バイト単位) です。 値がゼロの場合は、永続的なリソースが必要でないことを意味します。 永続的なリソースが必要な場合は、実行時だけでなく、コンパイル済み演算子の初期化中 (演算子初期化子の出力としてバインドされている場合) にもこれを指定する必要があります。 これについてはこのトピックの後半で詳しく説明します。 永続的なリソースを持つのはコンパイル済み演算子のみです。演算子初期化子は、このメンバーに対して常に値 0 を返します。

IDMLOperatorInitializer::Reset の呼び出し前と呼び出し後の両方で演算子初期化子に対して IDMLDispatchable::GetBindingProperties を呼び出した場合、取得された 2 つのセットのバインド プロパティは同一であることが保証されません。

記述子ヒープを記述、作成、バインドする

記述子に関しては、記述子ヒープそのものが開発者の責任範囲の始まりと終わりになります。 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 値 (最初の手順で取得したバインド プロパティの 1 つ) は、ディスパッチ可能オブジェクトに必要なバインド テーブルの最小サイズを記述子単位で表したものです。 ここでは、この値を使用して、指定された CPU および GPU 記述子ハンドルの両方の先頭から DirectML がヒープに書き込むことができる記述子の最大数を示します。

次に、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 バイト) を作成します。 リソースをバインドする方法を記述してから、そのバインドをバインド テーブルに追加します。

バインドしているバッファー リソースは 1 つなので、DML_BUFFER_BINDING 構造体を使用してバインドを記述します。 その構造体内で、Direct3D 12 バッファー リソース (このリソースにはディメンション D3D12_RESOURCE_DIMENSION_BUFFER が必要) に加え、バッファーへのオフセットとサイズを指定します。 (単一のバッファーではなく) バッファーの配列に対してバインドを記述することもでき、DML_BUFFER_ARRAY_BINDING 構造体はそのためにあります。

バッファーのバインドとバッファー配列のバインドの違いを抽象化するために、DML_BINDING_DESC 構造体を使用します。 DML_BINDING_DESCType メンバーは DML_BINDING_TYPE_BUFFER または DML_BINDING_TYPE_BUFFER_ARRAY に設定できます。 さらに、Desc メンバーを、Type に応じて DML_BUFFER_BINDING または DML_BUFFER_ARRAY_BINDING のいずれかを指すように設定できます。

この例では一時的なリソースを処理しているため、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_BINDING および DML_BINDING_DESC 構造体を使用して入力および出力リソース バインドを記述してから、IDMLBindingTable::BindInputs および IDMLBindingTable::BindOutputs への呼び出しを使用してそれらをコンパイル済み演算子のバインド テーブルに追加します。 IDMLBindingTable::Bind* メソッドを呼び出すと、DirectML によって 1 つ以上の記述子が 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 演算子の作成手順の 1 つ (IDMLDevice::CreateOperator に関するページを参照) として、1 つ以上の 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 の呼び出し中にバインド テーブルを使用すると、対応する GPU 記述子が DirectML によってパイプラインにバインドされます。 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 状態のままです。 これに対する唯一の例外は、演算子初期化子を実行するとき、および 1 つ以上のテンソルに 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 で使用されている間は削除されないようにする必要があります。 また、バインド テーブルはスレッド セーフではありません。アプリケーションは、同期せずに異なるスレッドから同時にバインド テーブルでメソッドを呼び出してはなりません。

いずれにしても、再バインドが必要なのは、バインドするリソースを変更した場合のみであることを考慮してください。 バインドされたリソースを変更する必要がない場合は、起動時に 1 回バインドし、RecordDispatch を呼び出すたびに同じバインド テーブルを渡すことができます。

機械学習とレンダリングのワークロードをインターリーブする場合は、各フレームのバインド テーブルが GPU でまだ使用されていない記述子ヒープの範囲を指すようにするだけです。

遅延バインディング演算子のバインドを必要に応じて指定する

(演算子初期化子ではなく) コンパイル済み演算子を扱う場合は、演算子に遅延バインディングを指定することもできます。 遅延バインディングを使用しない場合は、演算子をコマンド リストに記録する前に、バインド テーブルにすべてのバインドを設定する必要があります。 遅延バインディングを使用すると、既にコマンド リストに記録済みの演算子に対するバインドを設定 (または変更) した後に、それがコマンド キューに送信されます。

遅延バインディングを指定するには、flags 引数に DML_EXECUTION_FLAG_DESCRIPTORS_VOLATILE を指定して IDMLDevice::CompileOperator を呼び出します。

関連項目