[DML EP] Re-architect | Partitioning as Transformer (#13131)

### Description
Re-architect DML EP to allow ORT L2/L3 transformers. This change
includes:
- During ORT graph partitioning, DML EP will only set the
dmlExecutionProvider to all eligible nodes.
- Moved DML specific operator transformer as L2 transformer
- Introduced a new DMLGraphFusionTransformer, applicable only for DML
EP, which is responsible to
    - partition the graph
    - fuse each partition into a IDMLCompiledOperator
    - register the kernel for each partition


### Motivation and Context
- Why is this change required? What problem does it solve? 
It enables ORT L2/L3 transformers for DML EP, which will increase the
perf of Transformer-based models.
- If it fixes an open issue, please link to the issue here. N/A

Co-authored-by: Sumit Agarwal <sumitagarwal@microsoft.com>
This commit is contained in:
sumitsays 2022-10-07 22:35:47 -07:00 committed by GitHub
parent 38906625a3
commit e01a8519e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1422 additions and 1141 deletions

View file

@ -0,0 +1,499 @@
#pragma once
#include "DmlGraphFusionHelper.h"
namespace Dml
{
namespace DmlGraphFusionHelper
{
Microsoft::WRL::ComPtr<ID3D12Resource>
CreateResource(
const ExecutionProviderImpl* provider,
const std::byte* tensorPtr,
size_t tensorByteSize)
{
Microsoft::WRL::ComPtr<ID3D12Resource> buffer;
D3D12_HEAP_PROPERTIES heapProperties = {
D3D12_HEAP_TYPE_DEFAULT, D3D12_CPU_PAGE_PROPERTY_UNKNOWN, D3D12_MEMORY_POOL_UNKNOWN, 0, 0};
D3D12_RESOURCE_DESC resourceDesc = {D3D12_RESOURCE_DIMENSION_BUFFER,
0,
static_cast<uint64_t>((tensorByteSize + 3) & ~3),
1,
1,
1,
DXGI_FORMAT_UNKNOWN,
{1, 0},
D3D12_TEXTURE_LAYOUT_ROW_MAJOR,
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS};
Microsoft::WRL::ComPtr<ID3D12Device> d3dDevice;
ORT_THROW_IF_FAILED(provider->GetD3DDevice(d3dDevice.GetAddressOf()));
ORT_THROW_IF_FAILED(d3dDevice->CreateCommittedResource(
&heapProperties,
D3D12_HEAP_FLAG_NONE,
&resourceDesc,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
nullptr,
IID_GRAPHICS_PPV_ARGS(buffer.GetAddressOf())));
ORT_THROW_IF_FAILED(provider->UploadToResource(buffer.Get(), tensorPtr, tensorByteSize));
return buffer;
}
Microsoft::WRL::ComPtr<ID3D12Resource>
CreateCpuResource(
const ExecutionProviderImpl* provider,
const std::byte* tensorPtr,
size_t tensorByteSize)
{
Microsoft::WRL::ComPtr<ID3D12Resource> buffer;
D3D12_HEAP_PROPERTIES heapProperties = {
D3D12_HEAP_TYPE_CUSTOM, D3D12_CPU_PAGE_PROPERTY_WRITE_COMBINE, D3D12_MEMORY_POOL_L0, 0, 0};
D3D12_RESOURCE_DESC resourceDesc = {D3D12_RESOURCE_DIMENSION_BUFFER,
0,
static_cast<uint64_t>((tensorByteSize + 3) & ~3),
1,
1,
1,
DXGI_FORMAT_UNKNOWN,
{1, 0},
D3D12_TEXTURE_LAYOUT_ROW_MAJOR,
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS};
Microsoft::WRL::ComPtr<ID3D12Device> d3dDevice;
ORT_THROW_IF_FAILED(provider->GetD3DDevice(d3dDevice.GetAddressOf()));
ORT_THROW_IF_FAILED(d3dDevice->CreateCommittedResource(
&heapProperties,
D3D12_HEAP_FLAG_NONE,
&resourceDesc,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
nullptr,
IID_GRAPHICS_PPV_ARGS(buffer.GetAddressOf())));
// Map the buffer and copy the data
void* bufferData = nullptr;
D3D12_RANGE range = {0, tensorByteSize};
ORT_THROW_IF_FAILED(buffer->Map(0, &range, &bufferData));
memcpy(bufferData, tensorPtr, tensorByteSize);
buffer->Unmap(0, &range);
return buffer;
}
void UnwrapTensor(
Windows::AI::MachineLearning::Adapter::IWinmlExecutionProvider* winmlProvider,
const onnxruntime::Tensor* tensor,
ID3D12Resource** resource,
uint64_t* allocId)
{
IUnknown* allocationUnk = static_cast<IUnknown*>(const_cast<void*>(tensor->DataRaw()));
Microsoft::WRL::ComPtr<IUnknown> resourceUnk;
winmlProvider->GetABIDataInterface(false, allocationUnk, &resourceUnk);
*allocId = winmlProvider->TryGetPooledAllocationId(allocationUnk, 0);
ORT_THROW_IF_FAILED(resourceUnk->QueryInterface(resource));
}
void ProcessInputData(
const ExecutionProviderImpl* providerImpl,
const std::vector<uint8_t>& isInputsUploadedByDmlEP,
std::vector<DML_INPUT_GRAPH_EDGE_DESC>& inputEdges,
const gsl::span<const std::string> subGraphInputArgNames,
const std::unordered_map<std::string, std::pair<const ONNX_NAMESPACE::TensorProto*, bool>>& initializerNameToInitializerMap,
onnxruntime::Graph& graph,
_Out_ std::vector<bool>& inputsUsed,
_Inout_ std::vector<DML_BUFFER_BINDING>& initInputBindings,
_Inout_ std::vector<ComPtr<ID3D12Resource>>& initInputResources,
_Inout_ std::vector<ComPtr<ID3D12Resource>>& nonOwnedGraphInputsFromInitializers,
_Inout_ std::vector<ComPtr<ID3D12Resource>>& initializeResourceRefs,
_Inout_opt_ std::vector<std::vector<std::byte>>* inputRawData)
{
const uint32_t fusedNodeInputCount = gsl::narrow_cast<uint32_t>(subGraphInputArgNames.size());
// Determine the last input which uses an initializer, so initializers can be freed incrementally
// while processing each input in order.
std::map<const onnx::TensorProto*, uint32_t> initializerToLastInputIndexMap;
for (uint32_t i = 0; i < fusedNodeInputCount; i++)
{
auto iter = initializerNameToInitializerMap.find(subGraphInputArgNames[i]);
if (iter != initializerNameToInitializerMap.end()) {
initializerToLastInputIndexMap[iter->second.first] = i;
}
}
// Walk through each graph edge and mark used inputs
inputsUsed.assign(fusedNodeInputCount, false);
for (const DML_INPUT_GRAPH_EDGE_DESC& edge : inputEdges) {
inputsUsed[edge.GraphInputIndex] = true;
}
for (uint32_t i = 0; i < initInputBindings.size(); i++)
{
bool isInitializerAlreadyRemoved = false;
// If the input isn't actually used by the graph, nothing ever needs to be bound (either for
// initialization or execution). So just throw away the transferred initializer and skip this input.
if (!inputsUsed[i])
{
auto iter = initializerNameToInitializerMap.find(subGraphInputArgNames[i]);
if(iter != initializerNameToInitializerMap.end() && iter->second.second)
{
graph.RemoveInitializedTensor(subGraphInputArgNames[i]);
}
if (inputRawData)
{
inputRawData->push_back(std::vector<std::byte>());
}
continue;
}
// Look for the initializer among those transferred from the graph during partitioning
auto iter = initializerNameToInitializerMap.find(subGraphInputArgNames[i]);
if (iter != initializerNameToInitializerMap.end())
{
std::byte* tensorPtr = nullptr;
size_t tensorByteSize = 0;
std::unique_ptr<std::byte[]> unpackedTensor;
//auto& initializer = iter->second;
auto* initializer = iter->second.first;
// The tensor may be stored as raw data or in typed fields.
if (initializer->has_raw_data())
{
tensorPtr = (std::byte*)(initializer->raw_data().c_str());
tensorByteSize = initializer->raw_data().size();
}
else
{
std::tie(unpackedTensor, tensorByteSize) = Windows::AI::MachineLearning::Adapter::UnpackTensor(*initializer);
tensorPtr = unpackedTensor.get();
// Free the initializer if this is the last usage of it.
if (initializerToLastInputIndexMap[initializer] == i)
{
if (iter->second.second)
{
graph.RemoveInitializedTensor(subGraphInputArgNames[i]);
isInitializerAlreadyRemoved = true;
}
}
}
// Tensor sizes in DML must be a multiple of 4 bytes large.
tensorByteSize = AlignToPow2<size_t>(tensorByteSize, 4);
if (inputRawData)
{
inputRawData->push_back(std::vector<std::byte>(tensorPtr, tensorPtr + tensorByteSize));
}
if (!isInputsUploadedByDmlEP[i])
{
// Store the resource to use during execution
ComPtr<ID3D12Resource> defaultBuffer = CreateResource(providerImpl, tensorPtr, tensorByteSize);
nonOwnedGraphInputsFromInitializers[i] = defaultBuffer;
initializeResourceRefs.push_back(std::move(defaultBuffer));
}
else
{
ComPtr<ID3D12Resource> initializeInputBuffer;
// D3D_FEATURE_LEVEL_1_0_CORE doesn't support Custom heaps
if (providerImpl->IsMcdmDevice())
{
initializeInputBuffer = CreateResource(providerImpl, tensorPtr, tensorByteSize);
}
else
{
initializeInputBuffer = CreateCpuResource(providerImpl, tensorPtr, tensorByteSize);
}
// Set the binding for operator initialization to the buffer
initInputBindings[i].Buffer = initializeInputBuffer.Get();
initInputBindings[i].SizeInBytes = tensorByteSize;
initializeResourceRefs.push_back(std::move(initializeInputBuffer));
}
// Free the initializer if this is the last usage of it.
if (!isInitializerAlreadyRemoved && initializerToLastInputIndexMap[initializer] == i)
{
if (iter->second.second)
{
graph.RemoveInitializedTensor(subGraphInputArgNames[i]);
}
}
}
else if (inputRawData)
{
inputRawData->push_back(std::vector<std::byte>());
}
}
}
std::unordered_map<const onnx::TensorProto*, std::vector<uint32_t>>
GetInitializerToPartitionMap(
const onnxruntime::GraphViewer& graph,
gsl::span<std::unique_ptr<GraphPartition>> partitions
)
{
std::unordered_map<const onnx::TensorProto*, std::vector<uint32_t>> initializerPartitionMap;
for (uint32_t partitionIndex = 0; partitionIndex < gsl::narrow_cast<uint32_t>(partitions.size()); ++partitionIndex)
{
auto& partition = partitions[partitionIndex];
// Skip partitions which have been merged into other partitions
if (partition->GetRootMergedPartition() != partition.get())
{
continue;
}
for (const std::string& input : partition->GetInputs())
{
const onnx::TensorProto* tensor = nullptr;
if (graph.GetInitializedTensor(input, tensor))
{
initializerPartitionMap[tensor].push_back(partitionIndex);
}
}
}
return initializerPartitionMap;
}
void ConvertGraphDesc(
const Dml::GraphDescBuilder::GraphDesc& graphDesc,
_Out_ DML_GRAPH_DESC& dmlGraphDesc,
const uint32_t inputCount,
const uint32_t outputCount,
_Inout_ std::vector<DML_OPERATOR_GRAPH_NODE_DESC>& dmlOperatorGraphNodes,
_Inout_ std::vector<DML_GRAPH_NODE_DESC>& dmlGraphNodes,
_Inout_ std::vector<DML_GRAPH_EDGE_DESC>& dmlInputEdges,
_Inout_ std::vector<DML_GRAPH_EDGE_DESC>& dmlOutputEdges,
_Inout_ std::vector<DML_GRAPH_EDGE_DESC>& dmlIntermediateEdges)
{
for (size_t i = 0; i < graphDesc.nodes.size(); ++i)
{
dmlOperatorGraphNodes[i] = DML_OPERATOR_GRAPH_NODE_DESC{graphDesc.nodes[i].op.Get()};
dmlGraphNodes[i] = DML_GRAPH_NODE_DESC{DML_GRAPH_NODE_TYPE_OPERATOR, &dmlOperatorGraphNodes[i]};
}
for (size_t i = 0; i < graphDesc.inputEdges.size(); ++i)
{
dmlInputEdges[i] = DML_GRAPH_EDGE_DESC{DML_GRAPH_EDGE_TYPE_INPUT, &graphDesc.inputEdges[i]};
}
for (size_t i = 0; i < graphDesc.outputEdges.size(); ++i)
{
dmlOutputEdges[i] = DML_GRAPH_EDGE_DESC{DML_GRAPH_EDGE_TYPE_OUTPUT, &graphDesc.outputEdges[i]};
}
for (size_t i = 0; i < graphDesc.intermediateEdges.size(); ++i)
{
dmlIntermediateEdges[i] =
DML_GRAPH_EDGE_DESC{DML_GRAPH_EDGE_TYPE_INTERMEDIATE, &graphDesc.intermediateEdges[i]};
}
dmlGraphDesc.InputCount = inputCount;
dmlGraphDesc.OutputCount = outputCount;
dmlGraphDesc.NodeCount = gsl::narrow_cast<uint32_t>(dmlGraphNodes.size());
dmlGraphDesc.Nodes = dmlGraphNodes.data();
dmlGraphDesc.InputEdgeCount = gsl::narrow_cast<uint32_t>(dmlInputEdges.size());
dmlGraphDesc.InputEdges = dmlInputEdges.data();
dmlGraphDesc.OutputEdgeCount = gsl::narrow_cast<uint32_t>(dmlOutputEdges.size());
dmlGraphDesc.OutputEdges = dmlOutputEdges.data();
dmlGraphDesc.IntermediateEdgeCount = gsl::narrow_cast<uint32_t>(dmlIntermediateEdges.size());
dmlGraphDesc.IntermediateEdges = dmlIntermediateEdges.data();
}
void CreateIDmlCompiledOperatorAndRegisterKernel(
onnxruntime::Graph& graph,
const onnxruntime::IndexedSubGraph& indexedSubGraph,
const onnxruntime::Node& fusedNode,
const std::unordered_map<std::string, GraphNodeProperties>& partitionNodePropsMap,
const std::unordered_map<std::string, std::pair<const ONNX_NAMESPACE::TensorProto*, bool>>& initializerNameToInitializerMap,
const ExecutionProviderImpl* providerImpl,
onnxruntime::KernelRegistry* registryForPartitionKernels)
{
// convert partitionONNXGraph into DML EP GraphDesc
const uint32_t fusedNodeInputCount = gsl::narrow_cast<uint32_t>(indexedSubGraph.GetMetaDef()->inputs.size());
const uint32_t fusedNodeOutputCount = gsl::narrow_cast<uint32_t>(indexedSubGraph.GetMetaDef()->outputs.size());
std::vector<uint8_t> isInputsUploadedByDmlEP(fusedNodeInputCount);
for (uint32_t index = 0; index < fusedNodeInputCount; ++index)
{
auto iter = initializerNameToInitializerMap.find(indexedSubGraph.GetMetaDef()->inputs[index]);
isInputsUploadedByDmlEP[index] = iter != initializerNameToInitializerMap.end() ? true : false;
}
ComPtr<IDMLDevice> device;
ORT_THROW_IF_FAILED(providerImpl->GetDmlDevice(device.GetAddressOf()));
GraphDescBuilder::GraphDesc graphDesc = GraphDescBuilder::BuildGraphDesc(
isInputsUploadedByDmlEP.data(),
isInputsUploadedByDmlEP.size(),
initializerNameToInitializerMap,
graph,
indexedSubGraph,
partitionNodePropsMap,
device.Get(),
providerImpl);
// convert DML EP GraphDesc into DML_GRAPH_DESC and create IDMLCompiledOperator
DML_GRAPH_DESC dmlGraphDesc = {};
std::vector<DML_OPERATOR_GRAPH_NODE_DESC> dmlOperatorGraphNodes(graphDesc.nodes.size());
std::vector<DML_GRAPH_NODE_DESC> dmlGraphNodes(graphDesc.nodes.size());
std::vector<DML_GRAPH_EDGE_DESC> dmlInputEdges(graphDesc.inputEdges.size());
std::vector<DML_GRAPH_EDGE_DESC> dmlOutputEdges(graphDesc.outputEdges.size());
std::vector<DML_GRAPH_EDGE_DESC> dmlIntermediateEdges(graphDesc.intermediateEdges.size());
ConvertGraphDesc(
graphDesc,
dmlGraphDesc,
fusedNodeInputCount,
fusedNodeOutputCount,
dmlOperatorGraphNodes,
dmlGraphNodes,
dmlInputEdges,
dmlOutputEdges,
dmlIntermediateEdges);
DML_EXECUTION_FLAGS executionFlags = DML_EXECUTION_FLAG_NONE;
if (graphDesc.reuseCommandList)
{
executionFlags |= DML_EXECUTION_FLAG_DESCRIPTORS_VOLATILE;
}
// Query DML execution provider to see if metacommands is enabled
if (!providerImpl->MetacommandsEnabled())
{
executionFlags |= DML_EXECUTION_FLAG_DISABLE_META_COMMANDS;
}
ComPtr<IDMLDevice1> device1;
ORT_THROW_IF_FAILED(device.As(&device1));
ComPtr<IDMLCompiledOperator> compiledExecutionPlanOperator;
ORT_THROW_IF_FAILED(device1->CompileGraph(
&dmlGraphDesc,
executionFlags,
IID_PPV_ARGS(&compiledExecutionPlanOperator)));
// Populate input bindings for operator initialization
std::vector<Microsoft::WRL::ComPtr<ID3D12Resource>> initializeResourceRefs; // For lifetime control
std::vector<DML_BUFFER_BINDING> initInputBindings(fusedNodeInputCount);
std::vector<Microsoft::WRL::ComPtr<ID3D12Resource>> initInputResources; // For lifetime control
std::vector<ComPtr<ID3D12Resource>> nonOwnedGraphInputsFromInitializers(fusedNodeInputCount);
std::vector<bool> inputsUsed;
ProcessInputData(
providerImpl,
isInputsUploadedByDmlEP,
graphDesc.inputEdges,
indexedSubGraph.GetMetaDef()->inputs,
initializerNameToInitializerMap,
graph,
inputsUsed,
initInputBindings,
initInputResources,
nonOwnedGraphInputsFromInitializers,
initializeResourceRefs,
nullptr);
// lamda captures for the kernel registration
Windows::AI::MachineLearning::Adapter::EdgeShapes outputShapes;
ORT_THROW_HR_IF(E_UNEXPECTED, !TryGetStaticOutputShapes(fusedNode, outputShapes));
bool resuableCommandList = graphDesc.reuseCommandList;
auto fused_kernel_func = [compiledExecutionPlanOperator,
outputShapes,
resuableCommandList,
nonOwnedGraphInputsFromInitializers,
initializeResourceRefs,
initInputBindings,
isInputsUploadedByDmlEP,
inputsUsed]
(onnxruntime::FuncManager& func_mgr, const onnxruntime::OpKernelInfo& info, std::unique_ptr<onnxruntime::OpKernel>& out) mutable ->onnxruntime::Status
{
out.reset(CreateFusedGraphKernel(info,
compiledExecutionPlanOperator,
outputShapes,
resuableCommandList,
nonOwnedGraphInputsFromInitializers,
initializeResourceRefs,
initInputBindings,
isInputsUploadedByDmlEP,
inputsUsed));
return Status::OK();
};
// build the kernel definition on the fly, and register it to the fused_kernel_regisitry.
onnxruntime::KernelDefBuilder builder;
builder.SetName(indexedSubGraph.GetMetaDef()->name)
.SetDomain(indexedSubGraph.GetMetaDef()->domain)
.SinceVersion(indexedSubGraph.GetMetaDef()->since_version)
.Provider(onnxruntime::kDmlExecutionProvider);
ORT_THROW_IF_ERROR(registryForPartitionKernels->Register(builder, fused_kernel_func));
}
void FusePartitionAndRegisterKernel(
GraphPartition* partition,
uint32_t partitionIndex,
onnxruntime::Graph& graph,
std::unordered_map<const onnxruntime::Node*, GraphNodeProperties>& graphNodePropertyMap,
onnxruntime::KernelRegistry* registryForPartitionKernels,
const std::string& partitionKernelPrefix,
const std::unordered_map<std::string, std::pair<const ONNX_NAMESPACE::TensorProto*, bool>>& initializerNameToInitializerMap,
const ExecutionProviderImpl* providerImpl)
{
assert(partition->IsDmlGraphPartition());
onnxruntime::IndexedSubGraph indexedSubGraph;
// Create a definition for the node. The name must be unique.
auto def = std::make_unique<onnxruntime::IndexedSubGraph::MetaDef>();
def->name = DmlGraphFusionTransformer::DML_GRAPH_FUSION_NODE_NAME_PREFIX + partitionKernelPrefix + std::to_string(partitionIndex);
def->domain = DmlGraphFusionTransformer::DML_GRAPH_FUSION_NODE_DOMAIN;
def->since_version = 1;
def->inputs.insert(def->inputs.begin(), partition->GetInputs().begin(), partition->GetInputs().end());
def->outputs.insert(def->outputs.begin(), partition->GetOutputs().begin(), partition->GetOutputs().end());
indexedSubGraph.SetMetaDef(std::move(def));
indexedSubGraph.nodes = std::move(partition->GetNodeIndices());
auto& fusedNode = graph.BeginFuseSubGraph(indexedSubGraph, indexedSubGraph.GetMetaDef()->name);
fusedNode.SetExecutionProviderType(onnxruntime::kDmlExecutionProvider);
// Populate properties which will be passed to OpKernel for this graph via the function below
std::unordered_map<std::string, GraphNodeProperties> partitionNodePropsMap;
for (auto nodeIndex : indexedSubGraph.nodes)
{
const onnxruntime::Node* node = graph.GetNode(nodeIndex);
#ifdef PRINT_PARTITON_INFO
printf("Partition %u\t%s\n", partitionIndex, GraphDescBuilder::GetUniqueNodeName(*node).c_str());
#endif
partitionNodePropsMap.insert(std::make_pair(
GraphDescBuilder::GetUniqueNodeName(*node), std::move(graphNodePropertyMap[node])));
}
#ifdef PRINT_PARTITON_INFO
printf("\n");
#endif
CreateIDmlCompiledOperatorAndRegisterKernel(
graph,
indexedSubGraph,
fusedNode,
partitionNodePropsMap,
initializerNameToInitializerMap,
providerImpl,
registryForPartitionKernels);
graph.FinalizeFuseSubGraph(indexedSubGraph, fusedNode);
}
}
}

View file

@ -0,0 +1,92 @@
#pragma once
#include "precomp.h"
#include "GraphDescBuilder.h"
#include "ExecutionProvider.h"
#include "GraphPartitioner.h"
#include "FusedGraphKernel.h"
#include "MLOperatorAuthorImpl.h"
namespace Dml
{
namespace DmlGraphFusionHelper
{
template <typename T>
static T AlignToPow2(T offset, T alignment)
{
static_assert(std::is_unsigned_v<T>);
assert(alignment != 0);
assert((alignment & (alignment - 1)) == 0);
return (offset + alignment - 1) & ~(alignment - 1);
}
Microsoft::WRL::ComPtr<ID3D12Resource>
CreateResource(
const ExecutionProviderImpl* provider,
const std::byte* tensorPtr,
size_t tensorByteSize);
Microsoft::WRL::ComPtr<ID3D12Resource>
CreateCpuResource(
const ExecutionProviderImpl* provider,
const std::byte* tensorPtr,
size_t tensorByteSize);
void UnwrapTensor(
Windows::AI::MachineLearning::Adapter::IWinmlExecutionProvider* winmlProvider,
const onnxruntime::Tensor* tensor,
ID3D12Resource** resource,
uint64_t* allocId);
void ProcessInputData(
const ExecutionProviderImpl* providerImpl,
const std::vector<uint8_t>& isInputsUploadedByDmlEP,
std::vector<DML_INPUT_GRAPH_EDGE_DESC>& inputEdges,
const gsl::span<const std::string> subGraphInputArgNames,
const std::unordered_map<std::string, std::pair<const ONNX_NAMESPACE::TensorProto*, bool>>& isInitializerTransferable,
onnxruntime::Graph& graph,
_Out_ std::vector<bool>& inputsUsed,
_Inout_ std::vector<DML_BUFFER_BINDING>& initInputBindings,
_Inout_ std::vector<ComPtr<ID3D12Resource>>& initInputResources,
_Inout_ std::vector<ComPtr<ID3D12Resource>>& nonOwnedGraphInputsFromInitializers,
_Inout_ std::vector<ComPtr<ID3D12Resource>>& initializeResourceRefs,
_Inout_opt_ std::vector<std::vector<std::byte>>* inputRawData);
std::unordered_map<const onnx::TensorProto*, std::vector<uint32_t>>
GetInitializerToPartitionMap(
const onnxruntime::GraphViewer& graph,
gsl::span<std::unique_ptr<GraphPartition>> partitions
);
void ConvertGraphDesc(
const Dml::GraphDescBuilder::GraphDesc& graphDesc,
_Out_ DML_GRAPH_DESC& dmlGraphDesc,
const uint32_t inputCount,
const uint32_t outputCount,
_Inout_ std::vector<DML_OPERATOR_GRAPH_NODE_DESC>& dmlOperatorGraphNodes,
_Inout_ std::vector<DML_GRAPH_NODE_DESC>& dmlGraphNodes,
_Inout_ std::vector<DML_GRAPH_EDGE_DESC>& dmlInputEdges,
_Inout_ std::vector<DML_GRAPH_EDGE_DESC>& dmlOutputEdges,
_Inout_ std::vector<DML_GRAPH_EDGE_DESC>& dmlIntermediateEdges);
void CreateIDmlCompiledOperatorAndRegisterKernel(
onnxruntime::Graph& graph,
const onnxruntime::IndexedSubGraph& indexedSubGraph,
const onnxruntime::Node& fusedNode,
const std::unordered_map<std::string, GraphNodeProperties>& partitionNodePropsMap,
const std::unordered_map<std::string, std::pair<const ONNX_NAMESPACE::TensorProto*, bool>>& isInitializerTransferable,
const ExecutionProviderImpl* providerImpl,
onnxruntime::KernelRegistry* registryForPartitionKernels);
void FusePartitionAndRegisterKernel(
GraphPartition* partition,
uint32_t partitionIndex,
onnxruntime::Graph& graph,
std::unordered_map<const onnxruntime::Node*, GraphNodeProperties>& graphNodePropertyMap,
onnxruntime::KernelRegistry* registryForPartitionKernels,
const std::string& partitionKernelPrefix,
const std::unordered_map<std::string, std::pair<const ONNX_NAMESPACE::TensorProto*, bool>>& isInitializerTransferable,
const ExecutionProviderImpl* providerImpl);
}
}

View file

@ -0,0 +1,122 @@
#pragma once
#include "precomp.h"
#include "GraphDescBuilder.h"
#include "ExecutionProvider.h"
#include "DmlGraphFusionTransformer.h"
#include "GraphPartitioner.h"
#include "core/framework/kernel_type_str_resolver.h"
#include "core/framework/kernel_lookup.h"
#include "FusedGraphKernel.h"
#include "MLOperatorAuthorImpl.h"
#include "DmlGraphFusionHelper.h"
namespace Dml
{
DmlGraphFusionTransformer::DmlGraphFusionTransformer(
const std::string& name,
const onnxruntime::IExecutionProvider* provider
)
:onnxruntime::GraphTransformer(name),
m_providerImpl(static_cast<const ExecutionProvider*>(provider)->GetImpl())
{
}
onnxruntime::common::Status DmlGraphFusionTransformer::ApplyImpl(
onnxruntime::Graph& graph,
bool& modified,
int graph_level,
const onnxruntime::logging::Logger& logger) const
{
onnxruntime::ProviderType provider_type = onnxruntime::kDmlExecutionProvider;
const gsl::not_null<const onnxruntime::KernelRegistry*> registry = m_providerImpl->GetKernelRegistry().get();
const auto kernel_type_str_resolver = onnxruntime::OpSchemaKernelTypeStrResolver{};
const auto kernel_lookup = onnxruntime::KernelLookup{provider_type,
gsl::make_span(&registry, 1),
kernel_type_str_resolver};
// Initializers needed by any graph partition
std::unordered_set<std::string> requiredInitializerMap;
std::unordered_map<const onnxruntime::Node*, GraphNodeProperties> graphNodePropertyMap;
onnxruntime::GraphViewer graphViewer(graph);
std::vector<std::unique_ptr<GraphPartition>> partitions = BuildPartitions(
graphViewer,
*m_providerImpl->GetInternalRegistrationInfoMap(),
kernel_lookup,
m_providerImpl->GetSupportedDeviceDataTypeMask(),
graphNodePropertyMap,
requiredInitializerMap);
// Create a map between each initialized tensor and the partition(s) it is part of.
auto initializerPartitionMap = DmlGraphFusionHelper::GetInitializerToPartitionMap(graphViewer, partitions);
for (uint32_t partitionIndex = 0; partitionIndex < partitions.size(); ++partitionIndex)
{
auto& partition = partitions[partitionIndex];
if (partition->GetRootMergedPartition() != partition.get() ||
!partition->IsDmlPartition())
{
continue;
}
// This map will tell which initializer can be removed from onnxruntime::Graph (and from it's field
// onnx::GraphProto) while we upload the initializer to GPU.
// Why we want to remove the initializer from ORT?
// 1. To keep the peak memory usage as low as possible. That's why we are doing incremental upload to GPU.
// What is initializer?
// An initializer is a input tensor to an operator or the graph itself, which is contant and will never change.
// Why are we uploading the initialzer now?
// This prevents OnnxRuntime from allocating GPU resources and uploading those initializers,
// so the partiton's kernel can do so. In the process, it will pre-process weights while consuming a CPU
// backed resource, avoiding an extra set of GPU resources in memory.
std::unordered_map<std::string, std::pair<const ONNX_NAMESPACE::TensorProto*, bool>> isInitializerTransferable;
if (partition->IsDmlGraphPartition())
{
// populate transferredInitializerMap
for (const auto& input : partition->GetInputs())
{
const onnx::TensorProto* tensor = nullptr;
if (graph.GetInitializedTensor(input, tensor))
{
// It's only safe to transfer tensors which are used by this partition alone.
auto iter = initializerPartitionMap.find(tensor);
assert(iter != initializerPartitionMap.end());
if (iter->second.size() > 1)
{
if (requiredInitializerMap.find(input) != requiredInitializerMap.end())
{
// The kernel relies on this input to be initialized, and it should be small enough to copy
// cheaply. FusedGraphKernel only handles constant CPU inputs through transferred initializers,
// rather than ORT, to avoid mismatches in policy or implementation causing failures.
isInitializerTransferable[input] = {tensor, false};
}
continue;
}
isInitializerTransferable[input] = {tensor, true};
}
}
std::string partitionKernelPrefix = std::to_string(m_providerImpl->GetPartitionKernelPrefixVal()) + "_";
m_providerImpl->IncreasePartitionKernelPrefixVal();
DmlGraphFusionHelper::FusePartitionAndRegisterKernel(
partition.get(),
partitionIndex,
graph,
graphNodePropertyMap,
m_providerImpl->GetKernelRegistry().get(),
partitionKernelPrefix,
isInitializerTransferable,
m_providerImpl
);
}
}
return onnxruntime::common::Status::OK();
}
}

View file

@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once
#include "core/optimizer/graph_transformer.h"
#include "core/framework/execution_providers.h"
namespace Dml
{
class ExecutionProviderImpl;
class DmlGraphFusionTransformer : public onnxruntime::GraphTransformer
{
public:
DmlGraphFusionTransformer(
const std::string& name,
const onnxruntime::IExecutionProvider* provider
);
public:
inline const static char* const DML_GRAPH_FUSION_NODE_NAME_PREFIX = "DmlFusedNode_";
inline const static char* const DML_GRAPH_FUSION_NODE_DOMAIN = "DmlFusedNodeDomain";
private:
onnxruntime::common::Status ApplyImpl(onnxruntime::Graph& graph,
bool& modified,
int graph_level,
const onnxruntime::logging::Logger& logger) const final;
private:
const ExecutionProviderImpl* m_providerImpl = nullptr;
};
}

View file

@ -518,22 +518,165 @@ namespace Dml
return Dml::GetSupportedDeviceDataTypeMask(m_dmlDevice.Get());
}
bool TryGetTensorDataType(
const onnxruntime::NodeArg& nodeArg,
_Out_ MLOperatorTensorDataType* onnxElementType
)
{
*onnxElementType = MLOperatorTensorDataType::Undefined;
const ::onnx::TypeProto* typeProto = nodeArg.TypeAsProto();
if (typeProto != nullptr && typeProto->has_tensor_type())
{
const ::onnx::TypeProto_Tensor& tensorTypeProto = typeProto->tensor_type();
if (tensorTypeProto.has_elem_type())
{
*onnxElementType = static_cast<MLOperatorTensorDataType>(tensorTypeProto.elem_type());
return true;
}
}
return false;
}
bool DoesNodeContainSupportedDataTypes(
const onnxruntime::Node& node,
_In_opt_ const InternalRegistrationInfo* regInfo,
uint32_t supportedDeviceDataTypeMask // Each bit corresponds to each DML_TENSOR_DATA_TYPE.
)
{
std::vector<onnxruntime::NodeArg const*> constantCpuInputs;
if (regInfo != nullptr)
{
// Collect the list of CPU-bound input tensors, needed when checking 64-bit fallback
// or for other data types like int-8 which may be supported for CPU inputs but not
// GPU inputs.
auto inputDefinitions = node.InputDefs();
for (uint32_t i : regInfo->requiredConstantCpuInputs)
{
if (i < inputDefinitions.size())
{
constantCpuInputs.push_back(inputDefinitions[i]);
}
}
}
// Assume data types are supported until proven otherwise.
bool nodeContainsSupportedDataTypes = true;
// Callback to check each node's data type against registered operator support.
std::function<void(const onnxruntime::NodeArg& nodeArg, bool isInput)> nodeCallback = [&](const onnxruntime::NodeArg& nodeArg, bool isInput) -> void
{
// Get the tensor element data type for this node, comparing against what the device actually supports.
// Use the enumeration from the proto instead of nodeArg.Type() which returns a string.
// Reject node if undefined data type or non-tensor, as DML cannot handle it.
MLOperatorTensorDataType onnxElementType;
if (!TryGetTensorDataType(nodeArg, &onnxElementType))
{
// We shouldn't have arrived here because (1) no DML operators should have been
// registered which use non-tensor types (2) ONNX validation should have already
// been done, checking for the right kind of inputs and attributes. In theory,
// this branch could be reached with a bad custom operator or malformed file. If
// a legitimate case reaches here and DML needs to support a new input/output type
// besides tensors, then remove the assert.
assert(false);
nodeContainsSupportedDataTypes = false;
return;
}
// Reject node for unknown DML data types.
DML_TENSOR_DATA_TYPE dmlElementType = GetDmlDataTypeFromMlDataTypeNoThrow(onnxElementType);
if (dmlElementType == DML_TENSOR_DATA_TYPE_UNKNOWN)
{
nodeContainsSupportedDataTypes = false;
return;
}
// Succeed if the tensor is CPU-bound, as the CPU-side reading code is generic enough
// to handle multiple types regardless of GPU capability (typically these are just
// scalars or simple 1D arrays).
bool isConstantCpuInput = isInput && std::find(constantCpuInputs.begin(), constantCpuInputs.end(), &nodeArg) != constantCpuInputs.end();
if (isConstantCpuInput)
{
// Leave nodeContainsSupportedDataTypes alone.
return;
}
bool isDataTypeSupported = (1 << dmlElementType) & supportedDeviceDataTypeMask;
// Reject node if the data type is unsupported by the device.
if (!isDataTypeSupported)
{
nodeContainsSupportedDataTypes = false;
return;
}
// Otherwise the node supports the tensor data type.
};
// Check whether the node uses any data types which are unsupported by the device.
node.ForEachDef(nodeCallback);
return nodeContainsSupportedDataTypes;
}
bool ExecutionProviderImpl::IsNodeSupportedByDml(
const onnxruntime::Node& node,
const onnxruntime::IExecutionProvider::IKernelLookup& kernel_lookup,
uint32_t supportedDeviceDataTypeMask // Each bit corresponds to each DML_TENSOR_DATA_TYPE.
) const
{
const onnxruntime::KernelCreateInfo* createInfo = kernel_lookup.LookUpKernel(node);
if (!createInfo)
{
return false;
}
auto regInfoIter = m_internalRegInfoMap->find(createInfo->kernel_def.get());
std::shared_ptr<InternalRegistrationInfo> internalRegInfo;
if (regInfoIter != m_internalRegInfoMap->end())
{
internalRegInfo = regInfoIter->second;
if (internalRegInfo->supportQuery && !internalRegInfo->supportQuery(node))
{
return false;
}
}
// Check whether the node uses any data types which are unsupported by the device.
if (!DoesNodeContainSupportedDataTypes(node, internalRegInfo.get(), supportedDeviceDataTypeMask))
{
return false;
}
return true;
}
std::vector<std::unique_ptr<onnxruntime::ComputeCapability>>
ExecutionProviderImpl::GetCapability(
const onnxruntime::GraphViewer& graph,
const onnxruntime::IExecutionProvider::IKernelLookup& kernel_lookup) const
{
std::string partitionKernelPrefix = std::to_string(m_partitionKernelPrefixVal++) + "_";
uint32_t deviceDataTypeMask = GetSupportedDeviceDataTypeMask();
uint32_t deviceDataTypeMask = GetSupportedDeviceDataTypeMask(); // Each bit corresponds to each DML_TENSOR_DATA_TYPE.
return PartitionGraph(
graph,
*m_internalRegInfoMap,
kernel_lookup,
deviceDataTypeMask,
m_kernelRegistry.get(),
partitionKernelPrefix
);
std::vector<std::unique_ptr<onnxruntime::ComputeCapability>> result;
// Get the list of node indices in toplogical order, so nodes are visited before
// downstream nodes consuming them.
const std::vector<onnxruntime::NodeIndex>& toplogicalOrder = graph.GetNodesInTopologicalOrder();
for (size_t nodeIndex : toplogicalOrder)
{
const onnxruntime::Node& node = *graph.GetNode(nodeIndex);
if (IsNodeSupportedByDml(node, kernel_lookup, deviceDataTypeMask))
{
std::unique_ptr<onnxruntime::IndexedSubGraph> subGraph = std::make_unique<onnxruntime::IndexedSubGraph>();
subGraph->nodes = {nodeIndex};
result.push_back(std::make_unique<onnxruntime::ComputeCapability>(std::move(subGraph)));
}
}
return result;
}
bool IsGpuTensor(const onnxruntime::Tensor& tensor)

View file

@ -158,11 +158,27 @@ namespace Dml
std::shared_ptr<const Windows::AI::MachineLearning::Adapter::InternalRegistrationInfoMap>
GetInternalRegistrationInfoMap() const;
void IncreasePartitionKernelPrefixVal() const
{
m_partitionKernelPrefixVal++;
}
uint64_t GetPartitionKernelPrefixVal() const
{
return m_partitionKernelPrefixVal;
}
onnxruntime::common::Status OnSessionInitializationEnd();
private:
void Initialize(ID3D12CommandQueue* queue, ExecutionProvider& executionProvider);
bool IsNodeSupportedByDml(
const onnxruntime::Node& node,
const onnxruntime::IExecutionProvider::IKernelLookup& kernel_lookup,
uint32_t supportedDeviceDataTypeMask // Each bit corresponds to each DML_TENSOR_DATA_TYPE.
) const;
ComPtr<ID3D12Device> m_d3d12Device;
ComPtr<IDMLDevice> m_dmlDevice;
bool m_isMcdmDevice = false;
@ -176,7 +192,6 @@ namespace Dml
std::shared_ptr<onnxruntime::KernelRegistry> m_kernelRegistry;
std::shared_ptr<const Windows::AI::MachineLearning::Adapter::InternalRegistrationInfoMap> m_internalRegInfoMap;
mutable uint64_t m_partitionKernelPrefixVal = 0;
bool m_closed = false;
};

View file

@ -5,7 +5,7 @@
#include "MLOperatorAuthorImpl.h"
#include "FusedGraphKernel.h"
#include "GraphKernelHelper.h"
#include "DmlGraphFusionHelper.h"
using namespace Windows::AI::MachineLearning::Adapter;
@ -18,23 +18,21 @@ namespace Dml
FusedGraphKernel(
const onnxruntime::OpKernelInfo& kernelInfo,
const std::unordered_map<std::string, GraphNodeProperties> &graphNodePropertyMap,
std::unordered_map<std::string, onnx::TensorProto>& transferredInitializerMap,
const gsl::span<const std::string> fusedNodeInputArgOriginalNames,
const gsl::span<const std::string> fusedNodeOutputArgOriginalNames) : OpKernel(kernelInfo)
{
// Get the graph for the function which was created according to the computational
// capacity returned by the execution provider's graph partitioner
auto& node = kernelInfo.node();
ORT_THROW_HR_IF(E_UNEXPECTED, node.NodeType() != onnxruntime::Node::Type::Fused);
auto func = node.GetFunctionBody();
const onnxruntime::Graph& graph = func->Body();
// Get the shapes for outputs of the overall graph. These should be static, because
// the partitioner checked that each node has static shapes before fusing into a
// graph partition.
ORT_THROW_HR_IF(E_UNEXPECTED, !TryGetStaticOutputShapes(node, m_outputShapes));
ComPtr<IDMLCompiledOperator> compiledExecutionPlanOperator,
Windows::AI::MachineLearning::Adapter::EdgeShapes& outputShapes,
bool reuseCommandList,
std::vector<ComPtr<ID3D12Resource>>& nonOwnedGraphInputsFromInitializers,
std::vector<Microsoft::WRL::ComPtr<ID3D12Resource>>& initializeResourceRefs,
std::vector<DML_BUFFER_BINDING> initInputBindings,
std::vector<uint8_t>& isInputsUploadedByDmlEP,
std::vector<bool>& inputsUsed) :
OpKernel(kernelInfo),
m_compiledExecutionPlanOperator(compiledExecutionPlanOperator),
m_inputsUsed(inputsUsed),
m_outputShapes(outputShapes),
m_isInputsUploadedByDmlEP(isInputsUploadedByDmlEP),
m_nonOwnedGraphInputsFromInitializers(nonOwnedGraphInputsFromInitializers)
{
// Get the execution provider interfaces
m_executionHandle = kernelInfo.GetExecutionProvider()->GetExecutionHandle();
if (m_executionHandle)
@ -48,105 +46,19 @@ namespace Dml
}
TranslateAndCompileGraph(
kernelInfo,
graph,
fusedNodeInputArgOriginalNames,
fusedNodeOutputArgOriginalNames,
graphNodePropertyMap,
transferredInitializerMap);
kernelInfo,
initializeResourceRefs,
initInputBindings,
reuseCommandList);
}
void TranslateAndCompileGraph(
const onnxruntime::OpKernelInfo& kernelInfo,
const onnxruntime::Graph& graph,
const gsl::span<const std::string> fusedNodeInputArgOriginalNames,
const gsl::span<const std::string> fusedNodeOutputArgOriginalNames,
const std::unordered_map<std::string, GraphNodeProperties>& graphNodePropertyMap,
std::unordered_map<std::string, onnx::TensorProto>& transferredInitializerMap
std::vector<Microsoft::WRL::ComPtr<ID3D12Resource>>& initializeResourceRefs,
std::vector<DML_BUFFER_BINDING> initInputBindings,
bool reuseCommandList
)
{
ComPtr<IDMLDevice> device;
ORT_THROW_IF_FAILED(m_provider->GetDmlDevice(device.GetAddressOf()));
ComPtr<IDMLDevice1> device1;
ORT_THROW_IF_FAILED(device.As(&device1));
const uint32_t graphInputCount = kernelInfo.GetInputCount();
m_inputsConstant.resize(graphInputCount);
for (uint32_t i = 0; i < graphInputCount; ++i)
{
m_inputsConstant[i] = GraphKernelHelper::GetGraphInputConstness(i, kernelInfo, fusedNodeInputArgOriginalNames, transferredInitializerMap);
}
GraphDescBuilder::GraphDesc graphDesc = GraphDescBuilder::BuildGraphDesc(
kernelInfo,
m_inputsConstant.data(),
m_inputsConstant.size(),
transferredInitializerMap,
graph,
fusedNodeInputArgOriginalNames,
fusedNodeOutputArgOriginalNames,
graphNodePropertyMap,
device.Get(),
m_executionHandle);
// Populate input bindings for operator initialization
std::vector<Microsoft::WRL::ComPtr<ID3D12Resource>> initInputResources; // For lifetime control
std::vector<DML_BUFFER_BINDING> initInputBindings(graphInputCount);
m_nonOwnedGraphInputsFromInitializers.resize(graphInputCount);
std::vector<Microsoft::WRL::ComPtr<ID3D12Resource>> initializeResourceRefs;
GraphKernelHelper::ProcessInputData(
m_provider.Get(),
m_winmlProvider.Get(),
m_inputsConstant,
kernelInfo,
graphDesc,
fusedNodeInputArgOriginalNames,
m_inputsUsed,
initInputBindings,
initInputResources,
m_nonOwnedGraphInputsFromInitializers,
initializeResourceRefs,
nullptr,
transferredInitializerMap);
DML_GRAPH_DESC dmlGraphDesc = {};
std::vector<DML_OPERATOR_GRAPH_NODE_DESC> dmlOperatorGraphNodes(graphDesc.nodes.size());
std::vector<DML_GRAPH_NODE_DESC> dmlGraphNodes(graphDesc.nodes.size());
std::vector<DML_GRAPH_EDGE_DESC> dmlInputEdges(graphDesc.inputEdges.size());
std::vector<DML_GRAPH_EDGE_DESC> dmlOutputEdges(graphDesc.outputEdges.size());
std::vector<DML_GRAPH_EDGE_DESC> dmlIntermediateEdges(graphDesc.intermediateEdges.size());
GraphKernelHelper::ConvertGraphDesc(
graphDesc,
dmlGraphDesc,
kernelInfo,
dmlOperatorGraphNodes,
dmlGraphNodes,
dmlInputEdges,
dmlOutputEdges,
dmlIntermediateEdges);
DML_EXECUTION_FLAGS executionFlags = DML_EXECUTION_FLAG_NONE;
if (graphDesc.reuseCommandList)
{
executionFlags |= DML_EXECUTION_FLAG_DESCRIPTORS_VOLATILE;
}
// Query DML execution provider to see if metacommands is enabled
if (!m_provider->MetacommandsEnabled())
{
executionFlags |= DML_EXECUTION_FLAG_DISABLE_META_COMMANDS;
}
ORT_THROW_IF_FAILED(device1->CompileGraph(
&dmlGraphDesc,
executionFlags,
IID_PPV_ARGS(&m_compiledExecutionPlanOperator)));
// Allocate a persistent resource and initialize the operator
UINT64 persistentResourceSize = m_compiledExecutionPlanOperator->GetBindingProperties().PersistentResourceSize;
if (persistentResourceSize > 0)
@ -175,7 +87,7 @@ namespace Dml
[&](ComPtr<ID3D12Resource>& resource){ m_winmlProvider->QueueReference(WRAP_GRAPHICS_UNKNOWN(resource).Get()); }
);
if (graphDesc.reuseCommandList)
if (reuseCommandList)
{
BuildReusableCommandList();
}
@ -213,7 +125,7 @@ namespace Dml
{
inputPtrs[i] = m_nonOwnedGraphInputsFromInitializers[i].Get();
}
else if (!m_inputsConstant[i])
else if (!m_isInputsUploadedByDmlEP[i])
{
ORT_THROW_IF_FAILED(contextWrapper.GetInputTensor(i, inputTensors[i].GetAddressOf()));
inputPtrs[i] = m_provider->DecodeResource(MLOperatorTensor(inputTensors[i].Get()).GetDataInterface().Get());
@ -380,7 +292,7 @@ namespace Dml
for (uint32_t i = 0; i < inputBindings.size(); ++i)
{
if (!m_inputsConstant[i] && m_inputsUsed[i])
if (!m_isInputsUploadedByDmlEP[i] && m_inputsUsed[i])
{
if (m_nonOwnedGraphInputsFromInitializers[i])
{
@ -393,10 +305,10 @@ namespace Dml
const onnxruntime::Tensor* tensor = kernelContext->Input<onnxruntime::Tensor>(i);
uint64_t allocId;
GraphKernelHelper::UnwrapTensor(m_winmlProvider.Get(), tensor, &inputBindings[i].Buffer, &allocId);
DmlGraphFusionHelper::UnwrapTensor(m_winmlProvider.Get(), tensor, &inputBindings[i].Buffer, &allocId);
inputBindingsChanged = inputBindingsChanged || (!allocId || m_inputBindingAllocIds[i] != allocId);
inputBindings[i].Buffer->Release(); // Avoid holding an additional reference
inputBindings[i].SizeInBytes = GraphKernelHelper::AlignToPow2<size_t>(tensor->SizeInBytes(), 4);
inputBindings[i].SizeInBytes = DmlGraphFusionHelper::AlignToPow2<size_t>(tensor->SizeInBytes(), 4);
inputBindingDescs[i] = {DML_BINDING_TYPE_BUFFER, &inputBindings[i]};
m_inputBindingAllocIds[i] = allocId;
}
@ -430,10 +342,10 @@ namespace Dml
);
uint64_t allocId;
GraphKernelHelper::UnwrapTensor(m_winmlProvider.Get(), tensor, &outputBindings[i].Buffer, &allocId);
DmlGraphFusionHelper::UnwrapTensor(m_winmlProvider.Get(), tensor, &outputBindings[i].Buffer, &allocId);
outputBindingsChanged = outputBindingsChanged || (!allocId || m_outputBindingAllocIds[i] != allocId);
outputBindings[i].Buffer->Release(); // Avoid holding an additional reference
outputBindings[i].SizeInBytes = GraphKernelHelper::AlignToPow2<size_t>(tensor->SizeInBytes(), 4);
outputBindings[i].SizeInBytes = DmlGraphFusionHelper::AlignToPow2<size_t>(tensor->SizeInBytes(), 4);
outputBindingDescs[i] = {DML_BINDING_TYPE_BUFFER, &outputBindings[i]};
m_outputBindingAllocIds[i] = allocId;
}
@ -488,7 +400,7 @@ namespace Dml
const void* m_executionHandle = nullptr;
ComPtr<IWinmlExecutionProvider> m_winmlProvider;
ComPtr<Dml::IExecutionProvider> m_provider;
Windows::AI::MachineLearning::Adapter::EdgeShapes m_outputShapes;
Windows::AI::MachineLearning::Adapter::EdgeShapes& m_outputShapes;
// Re-usable command list, supporting descriptor heap, and DML binding table to update that heap.
ComPtr<ID3D12GraphicsCommandList> m_graphicsCommandList;
@ -509,23 +421,32 @@ namespace Dml
mutable ComPtr<ID3D12Fence> m_fence;
mutable uint64_t m_completionValue = 0;
std::vector<uint8_t> m_inputsConstant;
std::vector<uint8_t> m_isInputsUploadedByDmlEP;
std::vector<ComPtr<ID3D12Resource>> m_nonOwnedGraphInputsFromInitializers;
};
onnxruntime::OpKernel* CreateFusedGraphKernel(
const onnxruntime::OpKernelInfo& info,
const std::unordered_map<std::string, GraphNodeProperties> &graphNodePropertyMap,
std::unordered_map<std::string, onnx::TensorProto>& transferredInitializerMap,
const gsl::span<const std::string> fusedNodeInputArgOriginalNames,
const gsl::span<const std::string> fusedNodeOutputArgOriginalNames
const onnxruntime::OpKernelInfo& info,
ComPtr<IDMLCompiledOperator> compiledExecutionPlanOperator,
Windows::AI::MachineLearning::Adapter::EdgeShapes& outputShapes,
bool reuseCommandList,
std::vector<ComPtr<ID3D12Resource>>& nonOwnedGraphInputsFromInitializers,
std::vector<Microsoft::WRL::ComPtr<ID3D12Resource>>& initializeResourceRefs,
std::vector<DML_BUFFER_BINDING> initInputBindings,
std::vector<uint8_t>& isInputsUploadedByDmlEP,
std::vector<bool>& inputsUsed
)
{
return new FusedGraphKernel(
info,
graphNodePropertyMap,
transferredInitializerMap,
fusedNodeInputArgOriginalNames,
fusedNodeOutputArgOriginalNames);
compiledExecutionPlanOperator,
outputShapes,
reuseCommandList,
nonOwnedGraphInputsFromInitializers,
initializeResourceRefs,
initInputBindings,
isInputsUploadedByDmlEP,
inputsUsed
);
}
} // namespace Dml

View file

@ -3,14 +3,19 @@
#include "core/framework/op_kernel.h"
#include "GraphDescBuilder.h"
#include "DmlGraphFusionTransformer.h"
namespace Dml
{
onnxruntime::OpKernel* CreateFusedGraphKernel(
const onnxruntime::OpKernelInfo& info,
const std::unordered_map<std::string, GraphNodeProperties>& graphNodePropertyMap,
std::unordered_map<std::string, onnx::TensorProto>& transferredInitializerMap,
const gsl::span<const std::string> fusedNodeInputArgOriginalNames,
const gsl::span<const std::string> fusedNodeOutputArgOriginalNames
const onnxruntime::OpKernelInfo& info,
ComPtr<IDMLCompiledOperator> compiledExecutionPlanOperator,
Windows::AI::MachineLearning::Adapter::EdgeShapes& outputShapes,
bool reuseCommandList,
std::vector<ComPtr<ID3D12Resource>>& nonOwnedGraphInputsFromInitializers,
std::vector<Microsoft::WRL::ComPtr<ID3D12Resource>>& initializeResourceRefs,
std::vector<DML_BUFFER_BINDING> initInputBindings,
std::vector<uint8_t>& isInputsUploadedByDmlEP,
std::vector<bool>& inputsUsed
);
} // namespace Dml

View file

@ -3,7 +3,6 @@
#include "precomp.h"
#include "GraphDescBuilder.h"
#include "GraphKernelHelper.h"
using namespace Windows::AI::MachineLearning::Adapter;
@ -33,17 +32,18 @@ namespace Dml::GraphDescBuilder
#pragma warning(pop)
GraphDesc BuildGraphDesc(
const onnxruntime::OpKernelInfo& kernelInfo,
const uint8_t* isConstGpuGraphInput,
const size_t isConstGpuGraphInputCount,
std::unordered_map<std::string, onnx::TensorProto>& transferredInitializerMap,
const std::unordered_map<std::string, std::pair<const ONNX_NAMESPACE::TensorProto*, bool>>& isInitializerTransferable,
const onnxruntime::Graph& graph,
const gsl::span<const std::string> fusedNodeInputArgOriginalNames,
const gsl::span<const std::string> fusedNodeOutputArgOriginalNames,
const onnxruntime::IndexedSubGraph& indexedSubGraph,
const std::unordered_map<std::string, GraphNodeProperties>& graphNodePropertyMap,
IDMLDevice* device,
const void* executionHandle)
{
const gsl::span<const std::string> subGraphInputArgNames = indexedSubGraph.GetMetaDef()->inputs;
const gsl::span<const std::string> subGraphOutputArgNames = indexedSubGraph.GetMetaDef()->outputs;
struct NodeAndIndex
{
uint32_t nodeIndex; // The index of the node itself
@ -56,9 +56,9 @@ namespace Dml::GraphDescBuilder
// Map from Lotus node argument names to input indices of the fused kernel node.
std::unordered_map<std::string, uint32_t> nameToDmlFusedNodeInputIndex;
for (size_t inputIndex = 0; inputIndex < fusedNodeInputArgOriginalNames.size(); ++inputIndex)
for (size_t inputIndex = 0; inputIndex < subGraphInputArgNames.size(); ++inputIndex)
{
const onnxruntime::NodeArg* graphInput = graph.GetNodeArg(fusedNodeInputArgOriginalNames[inputIndex]);
const onnxruntime::NodeArg* graphInput = graph.GetNodeArg(subGraphInputArgNames[inputIndex]);
if (!graphInput)
{
@ -79,37 +79,34 @@ namespace Dml::GraphDescBuilder
std::vector<DML_INTERMEDIATE_GRAPH_EDGE_DESC> graphIntermediateEdges;
std::vector<DML_OUTPUT_GRAPH_EDGE_DESC> graphOutputEdges;
// Get the topological sorting of Lotus nodes
// paulm: breaking change from LOTUS that removed GetNodesInTopologicalOrder from Graph
onnxruntime::GraphViewer viewer(graph);
const std::vector<onnxruntime::NodeIndex>& orderedNodeIndices = viewer.GetNodesInTopologicalOrder();
// Avoid using separate command lists for small graphs. This value can be reduced by tuning the
// flushing behavior of DmlCommandRecorder. Its current behavior is to assume that graphs contain
// enough GPU work to be worth flushing immediately.
const uint32_t minNodeCountToReuseCommandList = 5;
bool reuseCommandList = false;
if (orderedNodeIndices.size() >= minNodeCountToReuseCommandList)
if (indexedSubGraph.nodes.size() >= minNodeCountToReuseCommandList)
{
reuseCommandList = true;
}
auto constantCpuGraphInputGetter = [&transferredInitializerMap](const std::string& argName)
auto constantCpuGraphInputGetter = [&isInitializerTransferable](const std::string& argName)
{
ComPtr<OnnxTensorWrapper> tensorWrapper;
auto iter = transferredInitializerMap.find(argName);
if (iter != transferredInitializerMap.end())
auto iter = isInitializerTransferable.find(argName);
if (iter != isInitializerTransferable.end())
{
tensorWrapper = wil::MakeOrThrow<OnnxTensorWrapper>(&iter->second);
// Using const_cast here is simpler than making surrounding code const correct.
tensorWrapper = wil::MakeOrThrow<OnnxTensorWrapper>(const_cast<ONNX_NAMESPACE::TensorProto*>(iter->second.first));
}
return tensorWrapper;
};
// Iterate through each node and create a corresponding node in the new graph
for (size_t sortedNodeIndex : orderedNodeIndices)
// We can iterate the nodes in any order because the edge connectivity will take care of the topological order
for (size_t sortedNodeIndex : indexedSubGraph.nodes)
{
const onnxruntime::Node& node = *graph.GetNode(sortedNodeIndex);
@ -257,9 +254,9 @@ namespace Dml::GraphDescBuilder
}
// Add graph output nodes, which might be in a different order from the encapsulating node
for (size_t outputIndex = 0; outputIndex < fusedNodeOutputArgOriginalNames.size(); ++outputIndex)
for (size_t outputIndex = 0; outputIndex < subGraphOutputArgNames.size(); ++outputIndex)
{
const onnxruntime::NodeArg* graphOutput = graph.GetNodeArg(fusedNodeOutputArgOriginalNames[outputIndex]);
const onnxruntime::NodeArg* graphOutput = graph.GetNodeArg(subGraphOutputArgNames[outputIndex]);
ORT_THROW_HR_IF_NULL_MSG(E_POINTER, graphOutput, "FusedNode's nodeArgList does not contain one of the nodeArg");
const auto& outputNodeAndIndex = nameToNodeAndIndexMap.at(graphOutput->Name());

View file

@ -40,13 +40,11 @@ namespace Dml
};
GraphDesc BuildGraphDesc(
const onnxruntime::OpKernelInfo& kernelInfo,
const uint8_t* isConstGpuGraphInput,
const size_t isConstGpuGraphInputCount,
std::unordered_map<std::string, onnx::TensorProto>& transferredInitializerMap,
const std::unordered_map<std::string, std::pair<const ONNX_NAMESPACE::TensorProto*, bool>>& isInitializerTransferable,
const onnxruntime::Graph& graph,
const gsl::span<const std::string> fusedNodeInputArgOriginalNames,
const gsl::span<const std::string> fusedNodeOutputArgOriginalNames,
const onnxruntime::IndexedSubGraph& indexedSubGraph,
const std::unordered_map<std::string, GraphNodeProperties>& graphNodePropertyMap,
IDMLDevice* device,
const void* executionHandle);

View file

@ -1,324 +0,0 @@
#include "precomp.h"
#include "GraphKernelHelper.h"
namespace Dml
{
namespace GraphKernelHelper
{
Microsoft::WRL::ComPtr<ID3D12Resource>
CreateResource(
Dml::IExecutionProvider* provider,
const std::byte* tensorPtr,
size_t tensorByteSize)
{
Microsoft::WRL::ComPtr<ID3D12Resource> buffer;
D3D12_HEAP_PROPERTIES heapProperties = {
D3D12_HEAP_TYPE_DEFAULT, D3D12_CPU_PAGE_PROPERTY_UNKNOWN, D3D12_MEMORY_POOL_UNKNOWN, 0, 0};
D3D12_RESOURCE_DESC resourceDesc = {D3D12_RESOURCE_DIMENSION_BUFFER,
0,
static_cast<uint64_t>((tensorByteSize + 3) & ~3),
1,
1,
1,
DXGI_FORMAT_UNKNOWN,
{1, 0},
D3D12_TEXTURE_LAYOUT_ROW_MAJOR,
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS};
Microsoft::WRL::ComPtr<ID3D12Device> d3dDevice;
ORT_THROW_IF_FAILED(provider->GetD3DDevice(d3dDevice.GetAddressOf()));
ORT_THROW_IF_FAILED(d3dDevice->CreateCommittedResource(
&heapProperties,
D3D12_HEAP_FLAG_NONE,
&resourceDesc,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
nullptr,
IID_GRAPHICS_PPV_ARGS(buffer.GetAddressOf())));
ORT_THROW_IF_FAILED(provider->UploadToResource(buffer.Get(), tensorPtr, tensorByteSize));
return buffer;
}
Microsoft::WRL::ComPtr<ID3D12Resource>
CreateCpuResource(
Dml::IExecutionProvider* provider,
const std::byte* tensorPtr,
size_t tensorByteSize)
{
Microsoft::WRL::ComPtr<ID3D12Resource> buffer;
D3D12_HEAP_PROPERTIES heapProperties = {
D3D12_HEAP_TYPE_CUSTOM, D3D12_CPU_PAGE_PROPERTY_WRITE_COMBINE, D3D12_MEMORY_POOL_L0, 0, 0};
D3D12_RESOURCE_DESC resourceDesc = {D3D12_RESOURCE_DIMENSION_BUFFER,
0,
static_cast<uint64_t>((tensorByteSize + 3) & ~3),
1,
1,
1,
DXGI_FORMAT_UNKNOWN,
{1, 0},
D3D12_TEXTURE_LAYOUT_ROW_MAJOR,
D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCESS};
Microsoft::WRL::ComPtr<ID3D12Device> d3dDevice;
ORT_THROW_IF_FAILED(provider->GetD3DDevice(d3dDevice.GetAddressOf()));
ORT_THROW_IF_FAILED(d3dDevice->CreateCommittedResource(
&heapProperties,
D3D12_HEAP_FLAG_NONE,
&resourceDesc,
D3D12_RESOURCE_STATE_UNORDERED_ACCESS,
nullptr,
IID_GRAPHICS_PPV_ARGS(buffer.GetAddressOf())));
// Map the buffer and copy the data
void* bufferData = nullptr;
D3D12_RANGE range = {0, tensorByteSize};
ORT_THROW_IF_FAILED(buffer->Map(0, &range, &bufferData));
memcpy(bufferData, tensorPtr, tensorByteSize);
buffer->Unmap(0, &range);
return buffer;
}
void UnwrapTensor(
IWinmlExecutionProvider* winmlProvider,
const onnxruntime::Tensor* tensor,
ID3D12Resource** resource,
uint64_t* allocId)
{
IUnknown* allocationUnk = static_cast<IUnknown*>(const_cast<void*>(tensor->DataRaw()));
Microsoft::WRL::ComPtr<IUnknown> resourceUnk;
winmlProvider->GetABIDataInterface(false, allocationUnk, &resourceUnk);
*allocId = winmlProvider->TryGetPooledAllocationId(allocationUnk, 0);
ORT_THROW_IF_FAILED(resourceUnk->QueryInterface(resource));
}
bool GetGraphInputConstness(
uint32_t index,
const onnxruntime::OpKernelInfo& kernelInfo,
const gsl::span<const std::string> fusedNodeInputArgOriginalNames,
const std::unordered_map<std::string, onnx::TensorProto>& transferredInitializerMap)
{
// Transferred initializers are uploaded to GPU memory
auto iter = transferredInitializerMap.find(fusedNodeInputArgOriginalNames[index]);
if (iter != transferredInitializerMap.end())
{
return true;
}
// If an initializer wasn't transferred, the constant input may be available from ORT
const onnxruntime::Tensor* inputTensor = nullptr;
if (!kernelInfo.TryGetConstantInput(index, &inputTensor) || inputTensor == nullptr)
{
return false;
}
// Check that the constant ORT input is in GPU memory
if (!strcmp(inputTensor->Location().name, onnxruntime::CPU) ||
inputTensor->Location().mem_type == ::OrtMemType::OrtMemTypeCPUOutput ||
inputTensor->Location().mem_type == ::OrtMemType::OrtMemTypeCPUInput)
{
return false;
}
return true;
};
void ProcessInputData(
Dml::IExecutionProvider* provider,
IWinmlExecutionProvider* winmlProvider,
const std::vector<uint8_t>& inputsConstant,
const onnxruntime::OpKernelInfo& kernelInfo,
const Dml::GraphDescBuilder::GraphDesc& graphDesc,
const gsl::span<const std::string> fusedNodeInputArgOriginalNames,
_Out_ std::vector<bool>& inputsUsed,
_Inout_ std::vector<DML_BUFFER_BINDING>& initInputBindings,
_Inout_ std::vector<ComPtr<ID3D12Resource>>& initInputResources,
_Inout_ std::vector<ComPtr<ID3D12Resource>>& nonOwnedGraphInputsFromInitializers,
_Inout_ std::vector<ComPtr<ID3D12Resource>>& initializeResourceRefs,
_Inout_opt_ std::vector<std::vector<std::byte>>* inputRawData,
_Inout_ std::unordered_map<std::string, onnx::TensorProto>& transferredInitializerMap)
{
const uint32_t graphInputCount = kernelInfo.GetInputCount();
// Determine the last input which uses an initializer, so initializers can be freed incrementally
// while processing each input in order.
std::map<const onnx::TensorProto*, uint32_t> initializerToLastInputIndexMap;
for (uint32_t i = 0; i < graphInputCount; i++)
{
auto iter = transferredInitializerMap.find(fusedNodeInputArgOriginalNames[i]);
if (iter != transferredInitializerMap.end()) {
initializerToLastInputIndexMap[&iter->second] = i;
}
}
// Walk through each graph edge and mark used inputs
inputsUsed.assign(graphInputCount, false);
for (const DML_INPUT_GRAPH_EDGE_DESC& edge : graphDesc.inputEdges) {
inputsUsed[edge.GraphInputIndex] = true;
}
for (uint32_t i = 0; i < initInputBindings.size(); i++)
{
// If the input isn't actually used by the graph, nothing ever needs to be bound (either for
// initialization or execution). So just throw away the transferred initializer and skip this input.
if (!inputsUsed[i])
{
transferredInitializerMap.erase(fusedNodeInputArgOriginalNames[i]);
if (inputRawData)
{
inputRawData->push_back(std::vector<std::byte>());
}
continue;
}
// Look for the initializer among those transferred from the graph during partitioning
auto iter = transferredInitializerMap.find(fusedNodeInputArgOriginalNames[i]);
if (iter != transferredInitializerMap.end())
{
std::byte* tensorPtr = nullptr;
size_t tensorByteSize = 0;
std::unique_ptr<std::byte[]> unpackedTensor;
auto& initializer = iter->second;
// The tensor may be stored as raw data or in typed fields.
if (initializer.has_raw_data())
{
tensorPtr = (std::byte*)(initializer.raw_data().c_str());
tensorByteSize = initializer.raw_data().size();
}
else
{
std::tie(unpackedTensor, tensorByteSize) = UnpackTensor(initializer);
tensorPtr = unpackedTensor.get();
}
// Tensor sizes in DML must be a multiple of 4 bytes large.
tensorByteSize = AlignToPow2<size_t>(tensorByteSize, 4);
if (inputRawData)
{
inputRawData->push_back(std::vector<std::byte>(tensorPtr, tensorPtr + tensorByteSize));
}
if (!inputsConstant[i])
{
// Store the resource to use during execution
ComPtr<ID3D12Resource> defaultBuffer = CreateResource(provider, tensorPtr, tensorByteSize);
nonOwnedGraphInputsFromInitializers[i] = defaultBuffer;
initializeResourceRefs.push_back(std::move(defaultBuffer));
}
else
{
ComPtr<ID3D12Resource> initializeInputBuffer;
// D3D_FEATURE_LEVEL_1_0_CORE doesn't support Custom heaps
if (provider->IsMcdmDevice())
{
initializeInputBuffer = CreateResource(provider, tensorPtr, tensorByteSize);
}
else
{
initializeInputBuffer = CreateCpuResource(provider, tensorPtr, tensorByteSize);
}
// Set the binding for operator initialization to the buffer
initInputBindings[i].Buffer = initializeInputBuffer.Get();
initInputBindings[i].SizeInBytes = tensorByteSize;
initializeResourceRefs.push_back(std::move(initializeInputBuffer));
}
// Free the initializer if this is the last usage of it.
if (initializerToLastInputIndexMap[&initializer] == i)
{
transferredInitializerMap.erase(iter);
}
}
else if (inputsConstant[i])
{
const onnxruntime::Tensor* inputTensor = nullptr;
ORT_THROW_HR_IF(E_UNEXPECTED, !kernelInfo.TryGetConstantInput(i, &inputTensor));
const std::byte* tensorData = reinterpret_cast<const std::byte*>(inputTensor->DataRaw());
if (inputRawData)
{
inputRawData->push_back(
std::vector<std::byte>(tensorData, tensorData + inputTensor->SizeInBytes()));
}
uint64_t allocId;
UnwrapTensor(winmlProvider, inputTensor, &initInputBindings[i].Buffer, &allocId);
initInputBindings[i].SizeInBytes = initInputBindings[i].Buffer->GetDesc().Width;
initInputBindings[i].Buffer->Release(); // Avoid holding an additional reference
initInputResources.push_back(initInputBindings[i].Buffer);
}
else if (inputRawData)
{
inputRawData->push_back(std::vector<std::byte>());
}
}
// All initializers should have been consumed and freed above
assert(transferredInitializerMap.empty());
}
void ConvertGraphDesc(
const Dml::GraphDescBuilder::GraphDesc& graphDesc,
_Out_ DML_GRAPH_DESC& dmlGraphDesc,
const onnxruntime::OpKernelInfo& kernelInfo,
_Inout_ std::vector<DML_OPERATOR_GRAPH_NODE_DESC>& dmlOperatorGraphNodes,
_Inout_ std::vector<DML_GRAPH_NODE_DESC>& dmlGraphNodes,
_Inout_ std::vector<DML_GRAPH_EDGE_DESC>& dmlInputEdges,
_Inout_ std::vector<DML_GRAPH_EDGE_DESC>& dmlOutputEdges,
_Inout_ std::vector<DML_GRAPH_EDGE_DESC>& dmlIntermediateEdges)
{
const uint32_t graphInputCount = kernelInfo.GetInputCount();
for (size_t i = 0; i < graphDesc.nodes.size(); ++i)
{
dmlOperatorGraphNodes[i] = DML_OPERATOR_GRAPH_NODE_DESC{graphDesc.nodes[i].op.Get()};
dmlGraphNodes[i] = DML_GRAPH_NODE_DESC{DML_GRAPH_NODE_TYPE_OPERATOR, &dmlOperatorGraphNodes[i]};
}
for (size_t i = 0; i < graphDesc.inputEdges.size(); ++i)
{
dmlInputEdges[i] = DML_GRAPH_EDGE_DESC{DML_GRAPH_EDGE_TYPE_INPUT, &graphDesc.inputEdges[i]};
}
for (size_t i = 0; i < graphDesc.outputEdges.size(); ++i)
{
dmlOutputEdges[i] = DML_GRAPH_EDGE_DESC{DML_GRAPH_EDGE_TYPE_OUTPUT, &graphDesc.outputEdges[i]};
}
for (size_t i = 0; i < graphDesc.intermediateEdges.size(); ++i)
{
dmlIntermediateEdges[i] =
DML_GRAPH_EDGE_DESC{DML_GRAPH_EDGE_TYPE_INTERMEDIATE, &graphDesc.intermediateEdges[i]};
}
dmlGraphDesc.InputCount = graphInputCount;
dmlGraphDesc.OutputCount = kernelInfo.GetOutputCount();
dmlGraphDesc.NodeCount = gsl::narrow_cast<uint32_t>(dmlGraphNodes.size());
dmlGraphDesc.Nodes = dmlGraphNodes.data();
dmlGraphDesc.InputEdgeCount = gsl::narrow_cast<uint32_t>(dmlInputEdges.size());
dmlGraphDesc.InputEdges = dmlInputEdges.data();
dmlGraphDesc.OutputEdgeCount = gsl::narrow_cast<uint32_t>(dmlOutputEdges.size());
dmlGraphDesc.OutputEdges = dmlOutputEdges.data();
dmlGraphDesc.IntermediateEdgeCount = gsl::narrow_cast<uint32_t>(dmlIntermediateEdges.size());
dmlGraphDesc.IntermediateEdges = dmlIntermediateEdges.data();
}
} // namespace GraphKernelHelper
} // namespace Dml

View file

@ -1,73 +0,0 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#pragma once
#include "GraphDescBuilder.h"
namespace Dml
{
namespace GraphKernelHelper
{
using namespace Windows::AI::MachineLearning::Adapter;
template <typename T>
static T AlignToPow2(T offset, T alignment)
{
static_assert(std::is_unsigned_v<T>);
assert(alignment != 0);
assert((alignment & (alignment - 1)) == 0);
return (offset + alignment - 1) & ~(alignment - 1);
}
Microsoft::WRL::ComPtr<ID3D12Resource>
CreateResource(
Dml::IExecutionProvider* provider,
const std::byte* tensorPtr,
size_t tensorByteSize);
Microsoft::WRL::ComPtr<ID3D12Resource>
CreateCpuResource(
Dml::IExecutionProvider* provider,
const std::byte* tensorPtr,
size_t tensorByteSize);
void UnwrapTensor(
IWinmlExecutionProvider* winmlProvider,
const onnxruntime::Tensor* tensor,
ID3D12Resource** resource,
uint64_t* allocId);
bool GetGraphInputConstness(
uint32_t index,
const onnxruntime::OpKernelInfo& kernelInfo,
const gsl::span<const std::string> fusedNodeInputArgOriginalNames,
const std::unordered_map<std::string, onnx::TensorProto>& transferredInitializerMap);
void ProcessInputData(
Dml::IExecutionProvider* provider,
IWinmlExecutionProvider* winmlProvider,
const std::vector<uint8_t>& inputsConstant,
const onnxruntime::OpKernelInfo& kernelInfo,
const Dml::GraphDescBuilder::GraphDesc& graphDesc,
const gsl::span<const std::string> fusedNodeInputArgOriginalNames,
_Out_ std::vector<bool>& inputsUsed,
_Inout_ std::vector<DML_BUFFER_BINDING>& initInputBindings,
_Inout_ std::vector<ComPtr<ID3D12Resource>>& initInputResources,
_Inout_ std::vector<ComPtr<ID3D12Resource>>& nonOwnedGraphInputsFromInitializers,
_Inout_ std::vector<ComPtr<ID3D12Resource>>& initializeResourceRefs,
_Inout_opt_ std::vector<std::vector<std::byte>>* inputRawData,
_Inout_ std::unordered_map<std::string, onnx::TensorProto>& transferredInitializerMap);
void ConvertGraphDesc(
const Dml::GraphDescBuilder::GraphDesc& graphDesc,
_Out_ DML_GRAPH_DESC& dmlGraphDesc,
const onnxruntime::OpKernelInfo& kernelInfo,
_Inout_ std::vector<DML_OPERATOR_GRAPH_NODE_DESC>& dmlOperatorGraphNodes,
_Inout_ std::vector<DML_GRAPH_NODE_DESC>& dmlGraphNodes,
_Inout_ std::vector<DML_GRAPH_EDGE_DESC>& dmlInputEdges,
_Inout_ std::vector<DML_GRAPH_EDGE_DESC>& dmlOutputEdges,
_Inout_ std::vector<DML_GRAPH_EDGE_DESC>& dmlIntermediateEdges);
} // namespace GraphKernelHelper
} // namespace Dml

View file

@ -141,144 +141,6 @@ namespace Dml
}
};
bool TryGetTensorDataType(
const onnxruntime::NodeArg& nodeArg,
_Out_ MLOperatorTensorDataType* onnxElementType
)
{
*onnxElementType = MLOperatorTensorDataType::Undefined;
const ::onnx::TypeProto* typeProto = nodeArg.TypeAsProto();
if (typeProto != nullptr && typeProto->has_tensor_type())
{
const ::onnx::TypeProto_Tensor& tensorTypeProto = typeProto->tensor_type();
if (tensorTypeProto.has_elem_type())
{
*onnxElementType = static_cast<MLOperatorTensorDataType>(tensorTypeProto.elem_type());
return true;
}
}
return false;
}
bool DoesNodeContainSupportedDataTypes(
const onnxruntime::Node& node,
_In_opt_ const InternalRegistrationInfo* regInfo,
uint32_t supportedDeviceDataTypeMask // Each bit corresponds to each DML_TENSOR_DATA_TYPE.
)
{
std::vector<onnxruntime::NodeArg const*> constantCpuInputs;
if (regInfo != nullptr)
{
// Collect the list of CPU-bound input tensors, needed when checking 64-bit fallback
// or for other data types like int-8 which may be supported for CPU inputs but not
// GPU inputs.
auto inputDefinitions = node.InputDefs();
for (uint32_t i : regInfo->requiredConstantCpuInputs)
{
if (i < inputDefinitions.size())
{
constantCpuInputs.push_back(inputDefinitions[i]);
}
}
}
// Assume data types are supported until proven otherwise.
bool nodeContainsSupportedDataTypes = true;
// Callback to check each node's data type against registered operator support.
std::function<void(const onnxruntime::NodeArg& nodeArg, bool isInput)> nodeCallback = [&](const onnxruntime::NodeArg& nodeArg, bool isInput) -> void
{
// Get the tensor element data type for this node, comparing against what the device actually supports.
// Use the enumeration from the proto instead of nodeArg.Type() which returns a string.
// Reject node if undefined data type or non-tensor, as DML cannot handle it.
MLOperatorTensorDataType onnxElementType;
if (!TryGetTensorDataType(nodeArg, &onnxElementType))
{
// We shouldn't have arrived here because (1) no DML operators should have been
// registered which use non-tensor types (2) ONNX validation should have already
// been done, checking for the right kind of inputs and attributes. In theory,
// this branch could be reached with a bad custom operator or malformed file. If
// a legitimate case reaches here and DML needs to support a new input/output type
// besides tensors, then remove the assert.
assert(false);
nodeContainsSupportedDataTypes = false;
return;
}
// Reject node for unknown DML data types.
DML_TENSOR_DATA_TYPE dmlElementType = GetDmlDataTypeFromMlDataTypeNoThrow(onnxElementType);
if (dmlElementType == DML_TENSOR_DATA_TYPE_UNKNOWN)
{
nodeContainsSupportedDataTypes = false;
return;
}
// Succeed if the tensor is CPU-bound, as the CPU-side reading code is generic enough
// to handle multiple types regardless of GPU capability (typically these are just
// scalars or simple 1D arrays).
bool isConstantCpuInput = isInput && std::find(constantCpuInputs.begin(), constantCpuInputs.end(), &nodeArg) != constantCpuInputs.end();
if (isConstantCpuInput)
{
// Leave nodeContainsSupportedDataTypes alone.
return;
}
bool isDataTypeSupported = (1 << dmlElementType) & supportedDeviceDataTypeMask;
// Reject node if the data type is unsupported by the device.
if (!isDataTypeSupported)
{
nodeContainsSupportedDataTypes = false;
return;
}
// Otherwise the node supports the tensor data type.
};
// Check whether the node uses any data types which are unsupported by the device.
node.ForEachDef(nodeCallback);
return nodeContainsSupportedDataTypes;
}
bool IsNodeSupportedByDml(
const onnxruntime::Node& node,
const onnxruntime::IExecutionProvider::IKernelLookup& kernel_lookup,
uint32_t supportedDeviceDataTypeMask, // Each bit corresponds to each DML_TENSOR_DATA_TYPE.
const InternalRegistrationInfoMap& internalRegInfoMap
)
{
const onnxruntime::KernelCreateInfo* createInfo = kernel_lookup.LookUpKernel(node);
if (!createInfo)
{
return false;
}
auto regInfoIter = internalRegInfoMap.find(createInfo->kernel_def.get());
std::shared_ptr<InternalRegistrationInfo> internalRegInfo;
if (regInfoIter != internalRegInfoMap.end())
{
internalRegInfo = regInfoIter->second;
if (internalRegInfo->supportQuery && !internalRegInfo->supportQuery(node))
{
return false;
}
}
// Check whether the node uses any data types which are unsupported by the device.
if (!DoesNodeContainSupportedDataTypes(node, internalRegInfo.get(), supportedDeviceDataTypeMask))
{
return false;
}
return true;
}
// Gets properties of the registration for a node
void GetRegistrationProperties(
const onnxruntime::GraphViewer& graph,
@ -289,68 +151,57 @@ namespace Dml
_In_opt_ const std::unordered_map<std::string, GraphPartition*>* nodeNameToPartitionMap,
_Inout_ std::unordered_map<const onnxruntime::Node*, GraphNodeProperties>& dmlNodePropertyMap,
_Inout_ std::unordered_set<std::string>& requiredInitializerMap,
_Out_ bool* isDmlNode,
_Out_ bool* isDmlGraphNode
)
{
*isDmlNode = false;
*isDmlGraphNode = false;
// Find the highest priority DML registry supporting this node, and get its highest-priority
// registration. Determine if that registration supports usage as a graph node.
// Get the kernel creation info for the registration, and check if it carries the property
// set during registration of kernels that support DML graph node usage.
auto graphNodeProperty = dmlNodePropertyMap.insert(std::make_pair(&node, GraphNodeProperties()));
if (IsNodeSupportedByDml(node, kernel_lookup, supportedDeviceDataTypeMask,
internalRegInfoMap))
// Ensure that shape information is known statically for the inputs and outputs of the node,
// which is required for MLGraph compilation.
const onnxruntime::KernelCreateInfo* createInfo = kernel_lookup.LookUpKernel(node);
assert(createInfo != nullptr); // since GetRegistrationProperties is called only when node is a DML node
auto regInfoIter = internalRegInfoMap.find(createInfo->kernel_def.get());
if (regInfoIter != internalRegInfoMap.end())
{
*isDmlNode = true;
auto internalRegInfo = regInfoIter->second;
// Get the kernel creation info for the registration, and check if it carries the property
// set during registration of kernels that support DML graph node usage.
auto graphNodeProperty = dmlNodePropertyMap.insert(std::make_pair(&node, GraphNodeProperties()));
// Ensure that shape information is known statically for the inputs and outputs of the node,
// which is required for MLGraph compilation.
const onnxruntime::KernelCreateInfo* createInfo = kernel_lookup.LookUpKernel(node);
assert(createInfo != nullptr); // since IsNodeSupportedByDml() returned true
auto regInfoIter = internalRegInfoMap.find(createInfo->kernel_def.get());
if (regInfoIter != internalRegInfoMap.end())
if (internalRegInfo && internalRegInfo->graphNodeFactoryRegistration)
{
auto internalRegInfo = regInfoIter->second;
if (internalRegInfo && internalRegInfo->graphNodeFactoryRegistration)
bool requiredCpuInputsConstant = true;
for (uint32_t inputIndex : internalRegInfo->requiredConstantCpuInputs)
{
bool requiredCpuInputsConstant = true;
for (uint32_t inputIndex : internalRegInfo->requiredConstantCpuInputs)
if (inputIndex >= node.InputDefs().size() || !node.InputDefs()[inputIndex]->Exists())
{
if (inputIndex >= node.InputDefs().size() || !node.InputDefs()[inputIndex]->Exists())
{
continue;
}
const onnx::TensorProto* tensor = nullptr;
const std::string& inputName = node.InputDefs()[inputIndex]->Name();
if (!graph.GetInitializedTensor(inputName, tensor))
{
requiredCpuInputsConstant = false;
break;
}
requiredInitializerMap.insert(inputName);
continue;
}
std::optional<uint32_t> requiredInputCount = internalRegInfo->graphNodeFactoryRegistration->requiredInputCount;
if (requiredCpuInputsConstant &&
TryGetStaticInputShapes( node, graphNodeProperty.first->second.inputShapes) &&
!ContainsEmptyDimensions(graphNodeProperty.first->second.inputShapes, internalRegInfo->requiredConstantCpuInputs) &&
TryGetStaticOutputShapes(node, graphNodeProperty.first->second.outputShapes) &&
!ContainsEmptyDimensions(graphNodeProperty.first->second.outputShapes, internalRegInfo->requiredConstantCpuInputs) &&
(requiredInputCount == std::nullopt || *requiredInputCount == node.InputDefs().size()))
const onnx::TensorProto* tensor = nullptr;
const std::string& inputName = node.InputDefs()[inputIndex]->Name();
if (!graph.GetInitializedTensor(inputName, tensor))
{
*isDmlGraphNode = true;
graphNodeProperty.first->second.internalRegInfo = internalRegInfo;
requiredCpuInputsConstant = false;
break;
}
requiredInitializerMap.insert(inputName);
}
std::optional<uint32_t> requiredInputCount = internalRegInfo->graphNodeFactoryRegistration->requiredInputCount;
if (requiredCpuInputsConstant &&
TryGetStaticInputShapes( node, graphNodeProperty.first->second.inputShapes) &&
!ContainsEmptyDimensions(graphNodeProperty.first->second.inputShapes, internalRegInfo->requiredConstantCpuInputs) &&
TryGetStaticOutputShapes(node, graphNodeProperty.first->second.outputShapes) &&
!ContainsEmptyDimensions(graphNodeProperty.first->second.outputShapes, internalRegInfo->requiredConstantCpuInputs) &&
(requiredInputCount == std::nullopt || *requiredInputCount == node.InputDefs().size()))
{
*isDmlGraphNode = true;
graphNodeProperty.first->second.internalRegInfo = internalRegInfo;
}
}
}
@ -490,78 +341,6 @@ namespace Dml
return partition;
}
std::unique_ptr<onnxruntime::ComputeCapability> ComputationCapacityFromPartition(
GraphPartition* partition,
uint32_t partitionIndex,
const onnxruntime::GraphViewer& graph,
std::unordered_map<const onnxruntime::Node*, GraphNodeProperties>&& graphNodePropertyMap,
onnxruntime::KernelRegistry* registryForPartitionKernels,
const std::string& partitionKernelPrefix,
std::shared_ptr<std::unordered_map<std::string, onnx::TensorProto>> transferredInitializerMap)
{
std::unique_ptr<onnxruntime::IndexedSubGraph> subGraph = std::make_unique<onnxruntime::IndexedSubGraph>();
if (partition->IsDmlGraphPartition())
{
assert(partition->IsDmlGraphPartition());
// Create a definition for the node. The name must be unique.
auto def = std::make_unique<onnxruntime::IndexedSubGraph::MetaDef>();
def->name = std::string("DmlFusedNode_") + partitionKernelPrefix + std::to_string(partitionIndex);
def->domain = "DmlFusedNodeDomain";
def->since_version = 1;
def->inputs.insert(def->inputs.begin(), partition->GetInputs().begin(), partition->GetInputs().end());
def->outputs.insert(def->outputs.begin(), partition->GetOutputs().begin(), partition->GetOutputs().end());
// Populate properties which will be passed to OpKernel for this graph via the function below
std::unordered_map<std::string, GraphNodeProperties> partitionNodePropsMap;
for (auto nodeIndex : partition->GetNodeIndices())
{
const onnxruntime::Node* node = graph.GetNode(nodeIndex);
#ifdef PRINT_PARTITON_INFO
printf("Partition %u\t%s\n", partitionIndex, GraphDescBuilder::GetUniqueNodeName(*node).c_str());
#endif
partitionNodePropsMap.insert(std::make_pair(
GraphDescBuilder::GetUniqueNodeName(*node), std::move(graphNodePropertyMap[node])));
}
#ifdef PRINT_PARTITON_INFO
printf("\n");
#endif
// These nodeArgNames will be used while creating DML Graph inside FusedGraphKernel.cpp
// Ordering of input/output nodeArgs in below vector will be same as Node::Definitions::input_defs because
// ORT is populating these args as it is while creating the FusedNode at Graph::CreateFusedSubGraphNode()
// Why we need these names?
// After Partitioning and before reaching to FusedGraphKernel, ORT may modify the input/output nodeArg names
// present in FusedNode (Node::Definitions::input_defs) as part of some transformers like memcopy, or L1/L2/L3 transformers.
std::vector<std::string> fusedNodeInputArgOriginalNames = def->inputs;
std::vector<std::string> fusedNodeOutputArgOriginalNames = def->outputs;
auto fused_kernel_func = [partitionNodePropsMap, transferredInitializerMap, fusedNodeInputArgOriginalNames, fusedNodeOutputArgOriginalNames](onnxruntime::FuncManager& func_mgr, const onnxruntime::OpKernelInfo& info, std::unique_ptr<onnxruntime::OpKernel>& out) mutable ->onnxruntime::Status
{
out.reset(CreateFusedGraphKernel(info, partitionNodePropsMap, *transferredInitializerMap, fusedNodeInputArgOriginalNames, fusedNodeOutputArgOriginalNames));
return Status::OK();
};
// build the kernel definition on the fly, and register it to the fused_kernel_regisitry.
onnxruntime::KernelDefBuilder builder;
builder.SetName(def->name)
.SetDomain(def->domain)
.SinceVersion(def->since_version)
.Provider(onnxruntime::kDmlExecutionProvider);
ORT_THROW_IF_ERROR(registryForPartitionKernels->Register(builder, fused_kernel_func));
subGraph->SetMetaDef(std::move(def));
}
subGraph->nodes = std::move(partition->GetNodeIndices());
return std::make_unique<onnxruntime::ComputeCapability>(std::move(subGraph));
}
// Whether any operator in the model contains a subgraph. This is true
// if the graph being partitioned is itself within a subgraph, or contains
// an operator with a subgraph.
@ -647,25 +426,27 @@ namespace Dml
const onnxruntime::Node& node = *graph.GetNode(nodeIndex);
// Whether the node is implemented through DML.
bool isDmlNode = false;
bool isDmlNode = node.GetExecutionProviderType() == onnxruntime::kDmlExecutionProvider;
// Whether the node is implemented through DML and as a graph node, meaning it
// can generate DML operations through a private interface for use as an MLGraph node.
bool isDmlGraphNode = false;
// Get the registration properties above and populate nodeNameToPartitionMap.
GetRegistrationProperties(
graph,
node,
kernel_lookup,
supportedDeviceDataTypeMask,
internalRegInfoMap,
&nodeNameToPartitionMap,
graphNodePropertyMap,
requiredInitializerMap,
/*out*/ &isDmlNode,
/*out*/ &isDmlGraphNode
);
if (isDmlNode)
{
GetRegistrationProperties(
graph,
node,
kernel_lookup,
supportedDeviceDataTypeMask,
internalRegInfoMap,
&nodeNameToPartitionMap,
graphNodePropertyMap,
requiredInitializerMap,
/*out*/ &isDmlGraphNode
);
}
// Add a unique partition if graph node usage is not supported.
//
@ -739,131 +520,4 @@ namespace Dml
return partitions;
}
std::unordered_map<const onnx::TensorProto*, std::vector<uint32_t>>
GetInitializerToPartitionMap(
const onnxruntime::GraphViewer& graph,
gsl::span<std::unique_ptr<GraphPartition>> partitions
)
{
std::unordered_map<const onnx::TensorProto*, std::vector<uint32_t>> initializerPartitionMap;
for (uint32_t partitionIndex = 0; partitionIndex < gsl::narrow_cast<uint32_t>(partitions.size()); ++partitionIndex)
{
auto& partition = partitions[partitionIndex];
// Skip partitions which have been merged into other partitions
if (partition->GetRootMergedPartition() != partition.get())
{
continue;
}
std::unordered_map<std::string, onnx::TensorProto> transferredInitializerMap;
for (const std::string& input : partition->GetInputs())
{
const onnx::TensorProto* tensor = nullptr;
if (graph.GetInitializedTensor(input, tensor))
{
initializerPartitionMap[tensor].push_back(partitionIndex);
}
}
}
return initializerPartitionMap;
}
std::vector<std::unique_ptr<onnxruntime::ComputeCapability>>
PartitionGraph(
const onnxruntime::GraphViewer& graph,
const InternalRegistrationInfoMap& internalRegInfoMap,
const onnxruntime::IExecutionProvider::IKernelLookup& kernel_lookup,
uint32_t supportedDeviceDataTypeMask, // Each bit corresponds to each DML_TENSOR_DATA_TYPE.
onnxruntime::KernelRegistry* registryForPartitionKernels,
const std::string& partitionKernelPrefix
)
{
std::vector<std::unique_ptr<onnxruntime::ComputeCapability>> result;
// Initializers needed by any graph partition
std::unordered_set<std::string> requiredInitializerMap;
std::unordered_map<const onnxruntime::Node*, GraphNodeProperties> graphNodePropertyMap;
std::vector<std::unique_ptr<GraphPartition>> partitions = BuildPartitions(
graph,
internalRegInfoMap,
kernel_lookup,
supportedDeviceDataTypeMask,
graphNodePropertyMap,
requiredInitializerMap);
// Create a map between each initialized tensor and the partition(s) it is part of.
auto initializerPartitionMap = GetInitializerToPartitionMap(graph, partitions);
for (uint32_t partitionIndex = 0; partitionIndex < partitions.size(); ++partitionIndex)
{
auto& partition = partitions[partitionIndex];
if (partition->GetRootMergedPartition() != partition.get() ||
!partition->IsDmlPartition())
{
continue;
}
// Create a map which will store by name each initializer which should be transferred to the
// partition. This prevents OnnxRuntime from allocating GPU resources and uploading those initializers,
// so the partiton's kernel can do so. In the process, it will pre-process weights while consuming a CPU
// backed resource, avoiding an extra set of GPU resources in memory.
// A shared pointer is used so the functor and contained initializer captures can be cheaply copied within ORT.
auto transferredInitializerMap = std::make_shared<std::unordered_map<std::string, onnx::TensorProto>>();
for (const auto& input : partition->GetInputs())
{
if (partition->IsDmlGraphPartition())
{
const onnx::TensorProto* tensor = nullptr;
if (graph.GetInitializedTensor(input, tensor))
{
// It's only safe to transfer tensors which are used by this partition alone.
auto iter = initializerPartitionMap.find(tensor);
assert(iter != initializerPartitionMap.end());
if (iter->second.size() > 1)
{
if (requiredInitializerMap.find(input) != requiredInitializerMap.end())
{
// The kernel relies on this input to be initialized, and it should be small enough to copy
// cheaply. FusedGraphKernel only handles constant CPU inputs through transferred initializers,
// rather than ORT, to avoid mismatches in policy or implementation causing failures.
(*transferredInitializerMap)[input] = const_cast<onnx::TensorProto&>(*tensor);
}
continue;
}
// Transfer the initializer
auto& graphTensor = const_cast<onnx::TensorProto&>(*tensor);
onnx::TensorProto partitionTensor;
graphTensor.Swap(&partitionTensor);
(*transferredInitializerMap)[input] = std::move(partitionTensor);
const_cast<onnxruntime::InitializedTensorSet&>(graph.GetAllInitializedTensors()).erase(graph.GetAllInitializedTensors().find(input));
}
}
}
result.push_back(ComputationCapacityFromPartition(
partition.get(),
partitionIndex,
graph,
std::move(graphNodePropertyMap),
registryForPartitionKernels,
partitionKernelPrefix,
transferredInitializerMap
));
}
return result;
}
} // namespace Dml

View file

@ -49,22 +49,4 @@ namespace Dml
std::unordered_map<const onnxruntime::Node*, GraphNodeProperties>& graphNodePropertyMap,
std::unordered_set<std::string>& requiredInitializerMap,
std::function<void(const onnxruntime::Node&)> onNodeUnsupportedInGraph = nullptr);
std::vector<std::unique_ptr<onnxruntime::ComputeCapability>>
PartitionGraph(
const onnxruntime::GraphViewer& graph,
const Windows::AI::MachineLearning::Adapter::InternalRegistrationInfoMap& internalRegInfoMap,
const onnxruntime::IExecutionProvider::IKernelLookup& kernel_lookup,
uint32_t supportedDeviceDataTypeMask, // Each bit corresponds to each DML_TENSOR_DATA_TYPE.
onnxruntime::KernelRegistry* registryForPartitionKernels,
const std::string& partitionKernelPrefix
);
bool IsNodeSupportedByDml(
const onnxruntime::Node& node,
const onnxruntime::IExecutionProvider::IKernelLookup& kernel_lookup,
uint32_t supportedDeviceDataTypeMask, // Each bit corresponds to each DML_TENSOR_DATA_TYPE.
const Windows::AI::MachineLearning::Adapter::InternalRegistrationInfoMap& internalRegInfoMap
);
} // namespace Dml

View file

@ -17,15 +17,6 @@
namespace Dml
{
GraphTransformer::GraphTransformer(
const std::string& name,
const onnxruntime::IExecutionProvider* provider
)
: onnxruntime::GraphTransformer(name),
m_providerImpl(static_cast<const ExecutionProvider* >(provider)->GetImpl())
{
}
onnxruntime::common::Status GraphTransformer::ApplyImpl(
onnxruntime::Graph& graph,
bool& modified,
@ -39,10 +30,6 @@ namespace Dml
PerformOperatorFusion(&graph, &transformModifiedGraph);
modified |= transformModifiedGraph;
transformModifiedGraph = false;
PerformQuantizedOperatorDecomposition(&graph, &transformModifiedGraph);
modified |= transformModifiedGraph;
if (modified)
{
ORT_RETURN_IF_ERROR(graph.Resolve());
@ -84,24 +71,10 @@ namespace Dml
// graph while iterating over it
std::vector<NodeToAdd> nodesToAdd;
onnxruntime::ProviderType provider_type = onnxruntime::kDmlExecutionProvider;
const gsl::not_null<const onnxruntime::KernelRegistry*> registry = m_providerImpl->GetKernelRegistry().get();
const auto kernel_type_str_resolver = onnxruntime::OpSchemaKernelTypeStrResolver{};
const auto kernel_lookup = onnxruntime::KernelLookup{provider_type,
gsl::make_span(&registry, 1),
kernel_type_str_resolver};
for (auto& node : graph->Nodes())
{
// We need to predict whether the nodes will be assigned to the DML transformer by Lotus,
// which occurs in IExecutionProvider::GetCapability.
if (!IsNodeSupportedByDml(
node,
kernel_lookup,
m_providerImpl->GetSupportedDeviceDataTypeMask(),
*m_providerImpl->GetInternalRegistrationInfoMap().get()
))
// Ignore the nodes which were not assigned to the DML by ORT during IExecutionProvider::GetCapability()
if (!onnxruntime::graph_utils::IsSupportedProvider(node, {onnxruntime::kDmlExecutionProvider}))
{
// Can't fuse nodes that don't belong to this execution provider
continue;
@ -119,9 +92,8 @@ namespace Dml
const auto& outputNode = *node.OutputNodesBegin();
// We need to predict whether the nodes will be assigned to the DML transformer by Lotus,
// which occurs in IExecutionProvider::GetCapability.
if (!kernel_lookup.LookUpKernel(outputNode))
// Can't fuse if outputNode was not assigned to the DML by ORT during IExecutionProvider::GetCapability()
if (!onnxruntime::graph_utils::IsSupportedProvider(outputNode, GetCompatibleExecutionProviders()))
{
// Can't fuse nodes that don't belong to this execution provider
continue;
@ -207,7 +179,8 @@ namespace Dml
nodeToAdd.outputs,
&nodeToAdd.attributes,
nodeToAdd.domain);
node.SetExecutionProviderType(onnxruntime::kDmlExecutionProvider);
// Add a dynamic attribute to the fuseable operator to specify activation
node.AddAttribute(AttrName::FusedActivation, nodeToAdd.activationOpType);
node.AddAttribute(AttrName::FusedActivationDomain, nodeToAdd.activationOpDomain);
@ -224,103 +197,4 @@ namespace Dml
}
}
// Converts certain QLinear operations unsupported by the DML API into a sequence of DeQuantizeLinear, 32-bit operator, QuantizeLinear
void GraphTransformer::PerformQuantizedOperatorDecomposition(onnxruntime::Graph* graph, bool* modified) const
{
struct NodeToAdd
{
std::string name;
std::string description;
std::string opType;
std::string domain;
onnxruntime::NodeAttributes attributes;
std::vector<onnxruntime::NodeArg*> inputs;
std::vector<onnxruntime::NodeArg*> outputs;
};
// Defer adding and removing nodes in the graph until after we're done iterating over it, because we can't mutate the
// graph while iterating over it
std::vector<NodeToAdd> nodesToAdd;
std::vector<onnxruntime::NodeIndex> nodesToRemove;
for (auto& node : graph->Nodes())
{
// For now, only QLinearSigmoid is handled
if (node.Domain() == onnxruntime::kMSDomain &&
node.OpType() == "QLinearSigmoid")
{
// Intermediate node arg type proto with floating point format
onnx::TypeProto floatTensorProto;
floatTensorProto.mutable_tensor_type()->set_elem_type(onnx::TensorProto_DataType_FLOAT);
// Add intermediate graph edges for the input and output of the FP32 sigmoid operator
auto* sigmoidInputArg = &graph->GetOrCreateNodeArg("decomposed_QLinearSigmoid_input_" + GetUniqueNodeName(&node), &floatTensorProto);
auto* sigmoidOutputArg = &graph->GetOrCreateNodeArg("decomposed_QLinearSigmoid_output_" + GetUniqueNodeName(&node), &floatTensorProto);
{
NodeToAdd dequantizeNode;
dequantizeNode.name = "decomposed_QLinearSigmoid_DequantizeLinear_" + GetUniqueNodeName(&node);
dequantizeNode.description = "";
dequantizeNode.opType = "DequantizeLinear";
dequantizeNode.domain = "";
dequantizeNode.inputs.push_back(graph->GetNodeArg(node.InputDefs()[0]->Name()));
dequantizeNode.inputs.push_back(graph->GetNodeArg(node.InputDefs()[1]->Name()));
dequantizeNode.inputs.push_back(graph->GetNodeArg(node.InputDefs()[2]->Name()));
dequantizeNode.outputs.push_back(sigmoidInputArg);
nodesToAdd.push_back(std::move(dequantizeNode));
}
{
NodeToAdd sigmoidNode;
sigmoidNode.name = "decomposed_QLinearSigmoid_Sigmoid_" + GetUniqueNodeName(&node);
sigmoidNode.description = "";
sigmoidNode.opType = "Sigmoid";
sigmoidNode.domain = "";
sigmoidNode.inputs.push_back(sigmoidInputArg);
sigmoidNode.outputs.push_back(sigmoidOutputArg);
nodesToAdd.push_back(std::move(sigmoidNode));
}
{
NodeToAdd quantizeNode;
quantizeNode.name = "decomposed_QLinearSigmoid_QuantizeLinear_" + GetUniqueNodeName(&node);
quantizeNode.description = "";
quantizeNode.opType = "QuantizeLinear";
quantizeNode.domain = "";
quantizeNode.inputs.push_back(sigmoidOutputArg);
quantizeNode.inputs.push_back(graph->GetNodeArg(node.InputDefs()[3]->Name()));
quantizeNode.inputs.push_back(graph->GetNodeArg(node.InputDefs()[4]->Name()));
quantizeNode.outputs.push_back(graph->GetNodeArg(node.OutputDefs()[0]->Name()));
nodesToAdd.push_back(std::move(quantizeNode));
}
nodesToRemove.push_back(node.Index());
*modified = true;
}
}
for (auto& nodeToAdd : nodesToAdd)
{
graph->AddNode(
nodeToAdd.name,
nodeToAdd.opType,
nodeToAdd.description,
nodeToAdd.inputs,
nodeToAdd.outputs,
&nodeToAdd.attributes,
nodeToAdd.domain);
}
for (const auto& nodeIndex : nodesToRemove)
{
onnxruntime::Node* node = graph->GetNode(nodeIndex);
onnxruntime::graph_utils::RemoveNodeOutputEdges(*graph, *node);
graph->RemoveNode(node->Index());
}
}
} // namespace Dml

View file

@ -11,7 +11,6 @@
namespace Dml
{
class ExecutionProviderImpl;
// Applies transforms to a Lotus graph. The graph transformer is responsible for setting the execution provider
// on the graph nodes which DML supports.
@ -19,19 +18,16 @@ namespace Dml
{
public:
GraphTransformer(
const std::string& name,
const onnxruntime::IExecutionProvider* provider
);
const std::string& name
) : onnxruntime::GraphTransformer(name)
{
}
private:
onnxruntime::common::Status ApplyImpl(onnxruntime::Graph& graph, bool& modified, int graph_level, const onnxruntime::logging::Logger& logger) const final;
private:
void PerformOperatorFusion(onnxruntime::Graph* graph, bool* modified) const;
void PerformQuantizedOperatorDecomposition(onnxruntime::Graph* graph, bool* modified) const;
std::shared_ptr<onnxruntime::KernelRegistry> m_registry;
const ExecutionProviderImpl* m_providerImpl = nullptr;
};
} // namespace Dml

View file

@ -55,7 +55,7 @@ namespace Dml
std::vector<DML_INPUT_GRAPH_EDGE_DESC> inputEdges;
for (uint32_t inputIndex = 0; inputIndex < m_kernelInputIndices.size(); inputIndex++)
{
if (m_kernelInputIndices[inputIndex].has_value())
if (m_kernelInputIndices[inputIndex].has_value())
{
DML_INPUT_GRAPH_EDGE_DESC inputEdge = {};
inputEdge.GraphInputIndex = *m_kernelInputIndices[inputIndex];
@ -67,11 +67,11 @@ namespace Dml
operatorGraphDesc.inputEdgeCount = gsl::narrow_cast<uint32_t>(inputEdges.size());
operatorGraphDesc.inputEdges = inputEdges.data();
std::vector<DML_OUTPUT_GRAPH_EDGE_DESC> outputEdges;
for (uint32_t outputIndex = 0; outputIndex < m_kernelOutputIndices.size(); outputIndex++)
{
if (m_kernelOutputIndices[outputIndex].has_value())
if (m_kernelOutputIndices[outputIndex].has_value())
{
DML_OUTPUT_GRAPH_EDGE_DESC outputEdge = {};
outputEdge.FromNodeIndex = 0;
@ -111,6 +111,105 @@ namespace Dml
}
}
void DmlOperator::SetDmlOperatorGraphDesc(
const MLOperatorGraphDesc&& operatorGraphDesc,
const MLOperatorKernelCreationContext& kernelInfo
)
{
// Initialize should only be called once.
assert(m_compiledOperator == nullptr);
// DML doesn't support empty tensors. If an operator is still executable with empty tensors, the empty tensors
// should be removed or massaged depending on the definition.
for (const TensorDesc& desc : m_inputTensorDescs)
{
if (OperatorHelper::ContainsEmptyDimensions(desc.GetSizes()))
{
return;
}
}
for (const TensorDesc& desc : m_outputTensorDescs)
{
if (OperatorHelper::ContainsEmptyDimensions(desc.GetSizes()))
{
return;
}
}
// m_kernelInputIndices should be identity
for (uint32_t idx = 0; idx < m_kernelInputIndices.size(); idx++)
{
if (m_kernelInputIndices[idx] == std::nullopt || !kernelInfo.IsInputValid(*m_kernelInputIndices[idx]))
{
continue;
}
assert(m_kernelInputIndices[idx] == idx);
}
// m_kernelOutputIndices should be identity
for (uint32_t idx = 0; idx < m_kernelOutputIndices.size(); idx++)
{
if (m_kernelOutputIndices[idx] == std::nullopt || !kernelInfo.IsOutputValid(*m_kernelOutputIndices[idx]))
{
continue;
}
assert(m_kernelOutputIndices[idx] == idx);
}
ComPtr<IMLOperatorKernelCreationContextPrivate> contextPrivate;
ORT_THROW_IF_FAILED(kernelInfo.GetInterface()->QueryInterface(contextPrivate.GetAddressOf()));
if (contextPrivate->IsDmlGraphNode())
{
ORT_THROW_IF_FAILED(contextPrivate->SetDmlOperator(&operatorGraphDesc));
}
else
{
DML_GRAPH_DESC graphDesc = {};
std::vector<DML_GRAPH_NODE_DESC> dmlGraphNodes(operatorGraphDesc.nodeCount);
std::vector<ComPtr<IDMLOperator>> dmlOperators(operatorGraphDesc.nodeCount);
std::vector<DML_OPERATOR_GRAPH_NODE_DESC> dmlOperatorGraphNodes(operatorGraphDesc.nodeCount);
std::vector<DML_GRAPH_EDGE_DESC> dmlInputEdges(operatorGraphDesc.inputEdgeCount);
std::vector<DML_GRAPH_EDGE_DESC> dmlOutputEdges(operatorGraphDesc.outputEdgeCount);
std::vector<DML_GRAPH_EDGE_DESC> dmlIntermediateEdges(operatorGraphDesc.intermediateEdgeCount);
// DML Graph validator will check the validity of the graph. No need to check here.
ConvertToDmlGraphDesc(operatorGraphDesc,
graphDesc,
dmlOperators,
dmlOperatorGraphNodes,
dmlGraphNodes,
dmlInputEdges,
dmlOutputEdges,
dmlIntermediateEdges);
// compile the graph and create IDMLCompiledOperator
Microsoft::WRL::ComPtr<IDMLDevice1> dmlDevice1;
DMLX_THROW_IF_FAILED(m_dmlDevice->QueryInterface(IID_PPV_ARGS(&dmlDevice1)));
DML_EXECUTION_FLAGS executionFlags = GetExecutionFlags();
ORT_THROW_IF_FAILED(dmlDevice1->CompileGraph(&graphDesc, executionFlags, IID_PPV_ARGS(&m_compiledOperator)));
UINT64 persistentResourceSize = m_compiledOperator->GetBindingProperties().PersistentResourceSize;
if (persistentResourceSize > 0)
{
ORT_THROW_IF_FAILED(m_executionProvider->AllocatePooledResource(
static_cast<size_t>(persistentResourceSize),
AllocatorRoundingMode::Enabled,
m_persistentResource.GetAddressOf(),
m_persistentResourcePoolingUnk.GetAddressOf()));
m_persistentResourceBinding = DML_BUFFER_BINDING{ m_persistentResource.Get(), 0, persistentResourceSize };
}
std::vector<DML_BUFFER_BINDING> initializationInputBindings(m_kernelInputIndices.size());
ORT_THROW_IF_FAILED(m_executionProvider->InitializeOperator(
m_compiledOperator.Get(),
m_persistentResourceBinding ? &*m_persistentResourceBinding : nullptr,
gsl::make_span(initializationInputBindings)));
}
}
void DmlOperator::SetDmlOperatorDesc(
const DML_OPERATOR_DESC& operatorDesc,
const MLOperatorKernelContext& kernelInfo
@ -596,4 +695,52 @@ namespace Dml
);
}
void DmlOperator::ConvertToDmlGraphDesc(const MLOperatorGraphDesc& operatorGraphDesc,
_Out_ DML_GRAPH_DESC& graphDesc,
_Inout_ std::vector<ComPtr<IDMLOperator>>& dmlOperators,
_Inout_ std::vector<DML_OPERATOR_GRAPH_NODE_DESC>& dmlOperatorGraphNodes,
_Inout_ std::vector<DML_GRAPH_NODE_DESC>& dmlGraphNodes,
_Inout_ std::vector<DML_GRAPH_EDGE_DESC>& dmlInputEdges,
_Inout_ std::vector<DML_GRAPH_EDGE_DESC>& dmlOutputEdges,
_Inout_ std::vector<DML_GRAPH_EDGE_DESC>& dmlIntermediateEdges)
{
graphDesc.InputCount = gsl::narrow_cast<uint32_t>(m_kernelInputIndices.size());
graphDesc.OutputCount = gsl::narrow_cast<uint32_t>(m_kernelOutputIndices.size());
// set the graph nodes
graphDesc.NodeCount = operatorGraphDesc.nodeCount;
for (size_t i = 0; i < graphDesc.NodeCount; ++i)
{
// Create the operator.
ORT_THROW_IF_FAILED(m_dmlDevice->CreateOperator(operatorGraphDesc.nodesAsOpDesc[i], IID_PPV_ARGS(&dmlOperators[i])));
dmlOperatorGraphNodes[i] = DML_OPERATOR_GRAPH_NODE_DESC{dmlOperators[i].Get()};
dmlGraphNodes[i] = DML_GRAPH_NODE_DESC{DML_GRAPH_NODE_TYPE_OPERATOR, &dmlOperatorGraphNodes[i]};
}
graphDesc.Nodes = dmlGraphNodes.data();
// set the input edges
graphDesc.InputEdgeCount = operatorGraphDesc.inputEdgeCount;
for (size_t i = 0; i < operatorGraphDesc.inputEdgeCount; ++i)
{
dmlInputEdges[i] = DML_GRAPH_EDGE_DESC{DML_GRAPH_EDGE_TYPE_INPUT, &operatorGraphDesc.inputEdges[i]};
}
graphDesc.InputEdges = dmlInputEdges.data();
// set the output edges
graphDesc.OutputEdgeCount = operatorGraphDesc.outputEdgeCount;
for (size_t i = 0; i < operatorGraphDesc.outputEdgeCount; ++i)
{
dmlOutputEdges[i] = DML_GRAPH_EDGE_DESC{DML_GRAPH_EDGE_TYPE_OUTPUT, &operatorGraphDesc.outputEdges[i]};
}
graphDesc.OutputEdges = dmlOutputEdges.data();
// set the intermediate edges
graphDesc.IntermediateEdgeCount = operatorGraphDesc.intermediateEdgeCount;
for (size_t i = 0; i < operatorGraphDesc.intermediateEdgeCount; ++i)
{
dmlIntermediateEdges[i] = DML_GRAPH_EDGE_DESC{DML_GRAPH_EDGE_TYPE_INTERMEDIATE, &operatorGraphDesc.intermediateEdges[i]};
}
graphDesc.IntermediateEdges = dmlIntermediateEdges.data();
}
} // namespace Dml

View file

@ -63,6 +63,15 @@ namespace Dml
const MLOperatorKernelCreationContext& kernelInfo
);
// This method only works with DML_GRAPH.
// To make it work without DML_GRAPH, we need to add new functionality
// in DMLX i.e. DMLX should also give access to DML_OPERATOR_DESC
// rather than IDMLOperator.
void SetDmlOperatorGraphDesc(
const MLOperatorGraphDesc&& operatorGraphDesc,
const MLOperatorKernelCreationContext& kernelInfo
);
void SetDmlOperatorDesc(
const DML_OPERATOR_DESC& operatorDesc,
const MLOperatorKernelContext& kernelInfo
@ -122,6 +131,16 @@ namespace Dml
// kernel. Entries for unused DML inputs are nullopt.
std::vector<std::optional<uint32_t>> m_kernelInputIndices;
std::vector<std::optional<uint32_t>> m_kernelOutputIndices;
void ConvertToDmlGraphDesc(const MLOperatorGraphDesc& operatorGraphDesc,
_Out_ DML_GRAPH_DESC& graphDesc,
_Inout_ std::vector<ComPtr<IDMLOperator>>& dmlOperators,
_Inout_ std::vector<DML_OPERATOR_GRAPH_NODE_DESC>& dmlOperatorGraphNodes,
_Inout_ std::vector<DML_GRAPH_NODE_DESC>& dmlGraphNodes,
_Inout_ std::vector<DML_GRAPH_EDGE_DESC>& dmlInputEdges,
_Inout_ std::vector<DML_GRAPH_EDGE_DESC>& dmlOutputEdges,
_Inout_ std::vector<DML_GRAPH_EDGE_DESC>& dmlIntermediateEdges);
};
} // namespace Dml

View file

@ -0,0 +1,173 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#include "precomp.h"
namespace Dml
{
// QLinearSigmoid = Dequantize + Sigmoid + Quantize
// This kernel is the first usage of graph based implementation
class DmlOperatorQLinearSigmoid : public DmlOperator
{
// This order matches the ONNX schema.
enum OnnxInputIndex
{
X, // Input
X_scale,
X_zero_point,
Y_scale,
Y_zero_point,
Count,
};
public:
DmlOperatorQLinearSigmoid(const MLOperatorKernelCreationContext& kernelCreationContext)
: DmlOperator(kernelCreationContext)
{
DmlOperator::Initialize(kernelCreationContext);
std::vector<uint32_t> outputShape = kernelCreationContext.GetTensorShapeDescription().GetOutputTensorShape(0);
const uint32_t outputShapeDimCount = gsl::narrow_cast<uint32_t>(outputShape.size());
uint32_t axis = 0;
// Explicitly reshape each of the inputs after the first input (scale and zero point tensors).
for (uint32_t index = OnnxInputIndex::X_scale, inputCount = gsl::narrow_cast<uint32_t>(OnnxInputIndex::Count); index < inputCount; ++index)
{
auto edgeDesc = kernelCreationContext.GetInputEdgeDescription(index);
assert(edgeDesc.edgeType == MLOperatorEdgeType::Tensor);
// Fix up the the tensor shape by filling with trailing ones. So input[2,3] with axis=0 and scale[2]
// becomes scale[2,1], so that broadcasting works correctly.
std::vector<uint32_t> inputTensorShape = kernelCreationContext.GetTensorShapeDescription().GetInputTensorShape(index);
// If the input tensor is a 1D vector, then extra massaging is needed to project their
// 1D vectors back to the full shape for broadcasting along the given axis.
// The 1D vector should have a length equal to the output tensor's dimension on that axis.
if (inputTensorShape.size() == 1 && inputTensorShape != outputShape)
{
ML_CHECK_VALID_ARGUMENT(axis < outputShapeDimCount);
uint32_t broadcastAxisLength = outputShape[axis];
ML_CHECK_VALID_ARGUMENT(inputTensorShape[0] == broadcastAxisLength);
inputTensorShape.insert(inputTensorShape.begin(), axis, 1);
inputTensorShape.insert(inputTensorShape.end(), outputShapeDimCount - 1 - axis, 1);
}
// For any other shape (scalar/ND), leave it alone, and the TensorDesc constructor
// will apply broadcasting with standard elementwise alignment.
m_inputTensorDescs[index] = TensorDesc(
edgeDesc.tensorDataType,
gsl::make_span(outputShape),
gsl::make_span(inputTensorShape),
TensorAxis::DoNotCoerce,
TensorAxis::W,
TensorAxis::RightAligned,
NchwDimensionCount, // minDimensionCount
0 // guaranteedBaseOffsetAlignment
);
}
// 1. output edge between Dequantize and Sigmoid node
// 2. input edge between Sigmoid and Quantize node
TensorDesc intermediateOutputTensorDesc = TensorDesc(
MLOperatorTensorDataType::Float,
gsl::make_span(outputShape),
gsl::make_span(outputShape),
TensorAxis::DoNotCoerce,
TensorAxis::W,
TensorAxis::RightAligned,
NchwDimensionCount, // minDimensionCount
0 // guaranteedBaseOffsetAlignment
);
std::vector<DML_TENSOR_DESC> inputDescs = GetDmlInputDescs();
std::vector<DML_TENSOR_DESC> outputDescs = GetDmlOutputDescs();
DML_ELEMENT_WISE_DEQUANTIZE_LINEAR_OPERATOR_DESC dequantizeOperatorDesc = {};
dequantizeOperatorDesc.InputTensor = &inputDescs[OnnxInputIndex::X];
dequantizeOperatorDesc.ScaleTensor = &inputDescs[OnnxInputIndex::X_scale];
dequantizeOperatorDesc.ZeroPointTensor = &inputDescs[OnnxInputIndex::X_zero_point];
dequantizeOperatorDesc.OutputTensor = &intermediateOutputTensorDesc.GetDmlDesc();
const DML_OPERATOR_DESC opDesc1{DML_OPERATOR_ELEMENT_WISE_DEQUANTIZE_LINEAR, &dequantizeOperatorDesc};
DML_ACTIVATION_SIGMOID_OPERATOR_DESC sigmoidOperatorDesc = {};
sigmoidOperatorDesc.InputTensor = dequantizeOperatorDesc.OutputTensor;
sigmoidOperatorDesc.OutputTensor = dequantizeOperatorDesc.OutputTensor;
const DML_OPERATOR_DESC opDesc2{DML_OPERATOR_ACTIVATION_SIGMOID, &sigmoidOperatorDesc};
DML_ELEMENT_WISE_QUANTIZE_LINEAR_OPERATOR_DESC quantizeOperatorDesc = {};
quantizeOperatorDesc.InputTensor = sigmoidOperatorDesc.OutputTensor;
quantizeOperatorDesc.ScaleTensor = &inputDescs[OnnxInputIndex::Y_scale];
quantizeOperatorDesc.ZeroPointTensor = &inputDescs[OnnxInputIndex::Y_zero_point];
quantizeOperatorDesc.OutputTensor = &outputDescs[0];
const DML_OPERATOR_DESC opDesc3{DML_OPERATOR_ELEMENT_WISE_QUANTIZE_LINEAR, &quantizeOperatorDesc};
MLOperatorGraphDesc operatorGraphDesc = {};
operatorGraphDesc.nodeCount = 3;
std::vector<const DML_OPERATOR_DESC*> opDescs{&opDesc1, &opDesc2, &opDesc3};
operatorGraphDesc.nodesAsOpDesc = opDescs.data();
// set input edges
std::pair<uint32_t, uint32_t> nodeToNodeInputIndex[5] {{0, 0}, {0, 1}, {0, 2}, {2, 1}, {2, 2}};
std::vector<DML_INPUT_GRAPH_EDGE_DESC> inputEdges(OnnxInputIndex::Count);
for (uint32_t inputIndex = 0; inputIndex < OnnxInputIndex::Count; inputIndex++)
{
DML_INPUT_GRAPH_EDGE_DESC inputEdge = {};
inputEdge.GraphInputIndex = inputIndex; // OnnxInputIndex and DmlInputIndex are identity for QLinearSigmoid
inputEdge.ToNodeIndex = nodeToNodeInputIndex[inputIndex].first;
inputEdge.ToNodeInputIndex = nodeToNodeInputIndex[inputIndex].second;
inputEdges[inputIndex] = inputEdge;
}
operatorGraphDesc.inputEdgeCount = gsl::narrow_cast<uint32_t>(inputEdges.size());
operatorGraphDesc.inputEdges = inputEdges.data();
// set intermediate edges
std::vector<DML_INTERMEDIATE_GRAPH_EDGE_DESC> intermediateEdges;
DML_INTERMEDIATE_GRAPH_EDGE_DESC dequantizeToSigmoidEdge = {};
dequantizeToSigmoidEdge.FromNodeIndex = 0;
dequantizeToSigmoidEdge.FromNodeOutputIndex = 0;
dequantizeToSigmoidEdge.ToNodeIndex = 1;
dequantizeToSigmoidEdge.ToNodeInputIndex = 0;
intermediateEdges.push_back(dequantizeToSigmoidEdge);
DML_INTERMEDIATE_GRAPH_EDGE_DESC sigmoidToQuantizeEdge = {};
sigmoidToQuantizeEdge.FromNodeIndex = 1;
sigmoidToQuantizeEdge.FromNodeOutputIndex = 0;
sigmoidToQuantizeEdge.ToNodeIndex = 2;
sigmoidToQuantizeEdge.ToNodeInputIndex = 0;
intermediateEdges.push_back(sigmoidToQuantizeEdge);
operatorGraphDesc.intermediateEdgeCount = gsl::narrow_cast<uint32_t>(intermediateEdges.size());
operatorGraphDesc.intermediateEdges = intermediateEdges.data();
// set the output edges
std::vector<DML_OUTPUT_GRAPH_EDGE_DESC> outputEdges;
DML_OUTPUT_GRAPH_EDGE_DESC outputEdge = {};
outputEdge.FromNodeIndex = 2;
outputEdge.FromNodeOutputIndex = 0;
outputEdge.GraphOutputIndex = 0;
outputEdges.push_back(outputEdge);
operatorGraphDesc.outputEdgeCount = gsl::narrow_cast<uint32_t>(outputEdges.size());
operatorGraphDesc.outputEdges = outputEdges.data();
SetDmlOperatorGraphDesc(std::move(operatorGraphDesc), kernelCreationContext);
}
};
void CALLBACK QueryQLinearSigmoid(IMLOperatorSupportQueryContextPrivate* context, /*out*/ bool* isSupported)
{
*isSupported = false;
// Right now the contract is if optional input tensors (like x_zero_point, y_zero_point) are
// not present, then fallback to CPU because DML Quantize_Linear and Dequantize_Linear does not support
// optionality of the zero_point tensor. BUG: https://microsoft.visualstudio.com/OS/_queries/edit/41599005
if (context->GetInputCount() < 5)
{
return;
}
*isSupported = true;
}
DML_OP_DEFINE_CREATION_FUNCTION(QLinearSigmoid, DmlOperatorQLinearSigmoid);
} // namespace Dml

View file

@ -220,6 +220,7 @@ DML_OP_EXTERN_CREATION_FUNCTION(DmlFusedAdd);
DML_OP_EXTERN_CREATION_FUNCTION(DmlFusedSum);
DML_OP_EXTERN_CREATION_FUNCTION(QuantizeLinear);
DML_OP_EXTERN_CREATION_FUNCTION(DequantizeLinear);
DML_OP_EXTERN_CREATION_FUNCTION(QLinearSigmoid);
DML_OP_EXTERN_CREATION_FUNCTION(Sign);
DML_OP_EXTERN_CREATION_FUNCTION(IsNaN);
DML_OP_EXTERN_CREATION_FUNCTION(Sinh);
@ -270,6 +271,7 @@ DML_OP_EXTERN_QUERY_FUNCTION(RecurrentNeuralNetwork);
DML_OP_EXTERN_QUERY_FUNCTION(BatchNormalization);
DML_OP_EXTERN_QUERY_FUNCTION(Pad);
DML_OP_EXTERN_QUERY_FUNCTION(LayerNormalization);
DML_OP_EXTERN_QUERY_FUNCTION(QLinearSigmoid);
constexpr static std::array<const char*, 1> typeNameListDefault = {"T"};
constexpr static std::array<const char*, 2> typeNameListTwo = { "T1", "T2" };
@ -330,6 +332,7 @@ constexpr static std::array<SupportedTensorDataTypes, 1> supportedTypeListIntege
constexpr static std::array<SupportedTensorDataTypes, 2> supportedTypeListRoiAlign = {SupportedTensorDataTypes::Float16to32, SupportedTensorDataTypes::Int32|SupportedTensorDataTypes::Int64 };
constexpr static std::array<SupportedTensorDataTypes, 1> supportedTypeListArgMinMax = {SupportedTensorDataTypes::Float16to32|SupportedTensorDataTypes::Ints8to64};
constexpr static std::array<SupportedTensorDataTypes, 2> supportedTypeListLayerNormalization = {SupportedTensorDataTypes::Float16to32, SupportedTensorDataTypes::Float32};
constexpr static std::array<SupportedTensorDataTypes, 1> supportedTypeListQLinearSigmoid = {SupportedTensorDataTypes::UInt8 | SupportedTensorDataTypes::Int8};
constexpr static std::array<SupportedTensorDataTypes, 3> supportedTypeListQLinearMatMul = {
SupportedTensorDataTypes::Int8|SupportedTensorDataTypes::UInt8,
@ -699,6 +702,7 @@ constexpr static OperatorRegistrationInformation operatorRegistrationInformation
// Contrib operators
{REG_INFO_MS( 1, Gelu, typeNameListDefault, supportedTypeListFloat16to32, DmlGraphSupport::Supported)},
{REG_INFO_MS( 1, FusedMatMul, typeNameListDefault, supportedTypeListFloat16to32, DmlGraphSupport::Supported)},
{REG_INFO_MS( 1, QLinearSigmoid, typeNameListDefault, supportedTypeListQLinearSigmoid, DmlGraphSupport::Supported, requiredConstantCpuInputs(), std::nullopt, QueryQLinearSigmoid)},
{REG_INFO( 10, IsInf, typeNameListTwo, supportedTypeListIsInf, DmlGraphSupport::Supported)},
{REG_INFO( 10, Mod, typeNameListDefault, supportedTypeListNumericDefault, DmlGraphSupport::Supported)},

View file

@ -1461,6 +1461,7 @@ using ShapeInferenceHelper_Atan = GetOutputShapeAsInputShapeHelper;
using ShapeInferenceHelper_Affine = GetOutputShapeAsInputShapeHelper;
using ShapeInferenceHelper_QuantizeLinear = GetOutputShapeAsInputShapeHelper;
using ShapeInferenceHelper_DequantizeLinear = GetOutputShapeAsInputShapeHelper;
using ShapeInferenceHelper_QLinearSigmoid = GetOutputShapeAsInputShapeHelper;
using ShapeInferenceHelper_Sign = GetBroadcastedOutputShapeHelper;
using ShapeInferenceHelper_IsNaN = GetBroadcastedOutputShapeHelper;
using ShapeInferenceHelper_Erf = GetBroadcastedOutputShapeHelper;

View file

@ -388,6 +388,7 @@ namespace OperatorHelper
static const int sc_sinceVer_QLinearAdd = 1;
static const int sc_sinceVer_Gelu = 1;
static const int sc_sinceVer_FusedMatMul = 1;
static const int sc_sinceVer_QLinearSigmoid = 1;
} // namespace MsftOperatorSet1
} // namespace OperatorHelper

View file

@ -49,6 +49,7 @@
#include "core/providers/cpu/controlflow/utils.h"
#include "core/providers/cpu/cpu_execution_provider.h"
#ifdef USE_DML // TODO: This is necessary for the workaround in TransformGraph
#include "core/providers/dml/DmlExecutionProvider/src/DmlGraphFusionTransformer.h"
#include "core/providers/dml/DmlExecutionProvider/src/GraphTransformer.h"
#endif
#include "core/session/environment.h"
@ -899,23 +900,6 @@ common::Status InferenceSession::TransformGraph(onnxruntime::Graph& graph,
ORT_RETURN_IF_ERROR_SESSIONID_(
graph_transformer_mgr.ApplyTransformers(graph, TransformerLevel::Level1, *session_logger_));
#ifdef USE_DML
// TODO: this is a temporary workaround to apply the DML EP's custom graph transformer prior to partitioning. This
// transformer applies DML-specific fusions that go beyond what ORT offers by default. Ideally the DML EP should
// apply these transforms during partitioning, but the full mutable Graph object isn't exposed to
// IExecutionProvider::GetCapability, which is necessary for the DML EP's transforms.
//
// To prevent this from interfering with other EPs, we only apply this transform if the DML EP is the only one that's
// registered (aside from the CPU EP, which is always registered by default.)
if (execution_providers_.Get(kDmlExecutionProvider) && execution_providers_.NumProviders() <= 2) {
Dml::GraphTransformer dml_transformer(onnxruntime::kDmlExecutionProvider,
execution_providers_.Get(kDmlExecutionProvider));
bool modified = false;
ORT_RETURN_IF_ERROR_SESSIONID_(dml_transformer.Apply(graph, modified, *session_logger_));
}
#endif
// if saving model to ORT format we only assign nodes a custom EP can handle and don't compile them.
// we do this to preserve the original nodes in the model but prevent optimizers from changing them.
// at runtime, the ORT format model will re-do the partitioning/compilation of these nodes, which may change
@ -1376,6 +1360,24 @@ common::Status InferenceSession::Initialize() {
minimal_build_optimization_handling,
record_runtime_optimization_produced_op_schema));
#ifdef USE_DML
if (execution_providers_.Get(kDmlExecutionProvider)) {
std::unique_ptr<onnxruntime::GraphTransformer> dmlGraphFusionTransformer = std::make_unique<Dml::DmlGraphFusionTransformer>("DmlGraphFusionTransformer",
execution_providers_.Get(kDmlExecutionProvider));
if (dmlGraphFusionTransformer == nullptr) {
return Status(common::ONNXRUNTIME, common::FAIL, "DmlGraphFusionTransformer is nullptr");
}
ORT_RETURN_IF_ERROR_SESSIONID_(graph_transformation_mgr_.Register(std::move(dmlGraphFusionTransformer), onnxruntime::TransformerLevel::Level3));
// This transformer applies DML-specific fusions that go beyond what ORT offers by default
std::unique_ptr<onnxruntime::GraphTransformer> dmlOperatorFusionTransformer = std::make_unique<Dml::GraphTransformer>("DmlOperatorFusionTransformer");
if (dmlOperatorFusionTransformer == nullptr) {
return Status(common::ONNXRUNTIME, common::FAIL, "DmlOperatorFusionTransformer is nullptr");
}
ORT_RETURN_IF_ERROR_SESSIONID_(graph_transformation_mgr_.Register(std::move(dmlOperatorFusionTransformer), onnxruntime::TransformerLevel::Level2));
}
#endif
// apply any transformations to the main graph and any subgraphs
ORT_RETURN_IF_ERROR_SESSIONID_(TransformGraph(graph, graph_transformation_mgr_,
execution_providers_, kernel_registry_manager_,