[DML EP] Add MatMulNBits (#20308)

This commit is contained in:
Patrice Vignola 2024-04-19 15:05:37 -07:00 committed by GitHub
parent 55e0aaeeef
commit 8fbb8a149f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 288 additions and 6 deletions

View file

@ -1296,6 +1296,7 @@ Do not modify directly.*
|GroupNorm|*in* X:**T**<br> *in* gamma:**M**<br> *in* beta:**M**<br> *out* Y:**T**|1+|**M** = tensor(float), tensor(float16)<br/> **T** = tensor(float), tensor(float16)|
|GroupQueryAttention|*in* query:**T**<br> *in* key:**T**<br> *in* value:**T**<br> *in* past_key:**T**<br> *in* past_value:**T**<br> *in* seqlens_k:**M**<br> *in* total_sequence_length:**M**<br> *in* cos_cache:**T**<br> *in* sin_cache:**T**<br> *out* output:**T**<br> *out* present_key:**T**<br> *out* present_value:**T**|1+|**M** = tensor(int32)<br/> **T** = tensor(float), tensor(float16)|
|MatMulIntegerToFloat|*in* A:**T1**<br> *in* B:**T2**<br> *in* a_scale:**T3**<br> *in* b_scale:**T3**<br> *in* a_zero_point:**T1**<br> *in* b_zero_point:**T2**<br> *in* bias:**T3**<br> *out* Y:**T3**|1+|**T1** = tensor(int8), tensor(uint8)<br/> **T2** = tensor(int8), tensor(uint8)<br/> **T3** = tensor(float), tensor(float16)|
|MatMulNBits|*in* A:**T1**<br> *in* B:**T2**<br> *in* scales:**T1**<br> *in* zero_points:**T3**<br> *in* g_idx:**T4**<br> *out* Y:**T1**|1+|**T1** = tensor(float), tensor(float16)<br/> **T2** = tensor(uint8)|
|MultiHeadAttention|*in* query:**T**<br> *in* key:**T**<br> *in* value:**T**<br> *in* bias:**T**<br> *in* key_padding_mask:**M**<br> *in* relative_position_bias:**T**<br> *in* past_key:**T**<br> *in* past_value:**T**<br> *out* output:**T**<br> *out* present_key:**T**<br> *out* present_value:**T**|1+|**M** = tensor(int32)<br/> **T** = tensor(float), tensor(float16)|
|NhwcConv|*in* X:**T**<br> *in* W:**T**<br> *in* B:**T**<br> *out* Y:**T**|1+|**T** = tensor(float), tensor(float16)|
|QAttention|*in* input:**T1**<br> *in* weight:**T2**<br> *in* bias:**T3**<br> *in* input_scale:**T3**<br> *in* weight_scale:**T3**<br> *in* mask_index:**T4**<br> *in* input_zero_point:**T1**<br> *in* weight_zero_point:**T2**<br> *in* past:**T3**<br> *out* output:**T3**<br> *out* present:**T3**|1+|**T1** = tensor(int8), tensor(uint8)<br/> **T2** = tensor(int8), tensor(uint8)<br/> **T3** = tensor(float), tensor(float16)<br/> **T4** = tensor(int32)|

View file

@ -0,0 +1,200 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
#include "precomp.h"
namespace Dml
{
class DmlOperatorMatMulNBits : public DmlOperator
{
public:
DmlOperatorMatMulNBits(const MLOperatorKernelCreationContext& kernelInfo)
: DmlOperator(kernelInfo)
{
ML_CHECK_VALID_ARGUMENT(kernelInfo.GetInputCount() >= 3 && kernelInfo.GetInputCount() <= 4);
ML_CHECK_VALID_ARGUMENT(kernelInfo.GetOutputCount() == 1);
DmlOperator::Initialize(kernelInfo);
const bool hasZeroPoint = kernelInfo.IsInputValid(3);
const uint32_t bRowCount = gsl::narrow_cast<uint32_t>(kernelInfo.GetAttribute<int64_t>(AttrName::UppercaseN));
const uint32_t bColCount = gsl::narrow_cast<uint32_t>(kernelInfo.GetAttribute<int64_t>(AttrName::UppercaseK));
const auto bitCount = kernelInfo.GetAttribute<int64_t>(AttrName::Bits);
MLOperatorTensorDataType mlDataType = kernelInfo.GetInputEdgeDescription(0).tensorDataType;
ML_CHECK_VALID_ARGUMENT(bitCount == 4 || bitCount == 8);
const DML_TENSOR_DATA_TYPE quantizedDataType = bitCount == 4 ? DML_TENSOR_DATA_TYPE_UINT4 : DML_TENSOR_DATA_TYPE_UINT8;
std::vector<DimensionType> aShape = kernelInfo.GetTensorShapeDescription().GetInputTensorShape(0);
ML_CHECK_VALID_ARGUMENT(aShape.size() >= 2);
// The quantized input to MatMulNBits always comes as uint8, but the real shape is provided through the N and K attributes
std::vector<DimensionType> bBroadcastedShape = aShape;
bBroadcastedShape[bBroadcastedShape.size() - 2] = bRowCount;
bBroadcastedShape[bBroadcastedShape.size() - 1] = bColCount;
// The B tensor always has a batch size and channel of 1 since it's a weight, but DML requires it to have the same channels
// and channel count as the A tensor so we need to broadcast it
std::vector<DimensionType> bShape(aShape.size(), 1);
bShape[bShape.size() - 2] = bRowCount;
bShape[bShape.size() - 1] = bColCount;
std::vector<DimensionType> outputShape = kernelInfo.GetTensorShapeDescription().GetOutputTensorShape(0);
std::vector<DimensionType> scaleShape = bShape;
uint32_t scaleElementCount = ComputeElementCountFromDimensions(m_inputTensorDescs[2].GetSizes());
scaleShape[scaleShape.size() - 1] = scaleElementCount / bRowCount;
std::vector<DimensionType> scaleBroadcastedShape = bBroadcastedShape;
scaleBroadcastedShape.back() = scaleShape.back();
// The quantized input and zero point to MatMulNBits always comes as uint8, but DML will expect the real data type (int4 or int8)
m_inputTensorDescs[0] = TensorDesc::ConstructDefaultTensorDesc(mlDataType, aShape);
m_inputTensorDescs[1] = TensorDesc::ConstructBroadcastedTensorDesc(GetMlDataTypeFromDmlDataType(quantizedDataType), bBroadcastedShape, bShape);
m_inputTensorDescs[2] = TensorDesc::ConstructBroadcastedTensorDesc(mlDataType, scaleBroadcastedShape, scaleShape);
if (hasZeroPoint)
{
m_inputTensorDescs[3] = TensorDesc::ConstructBroadcastedTensorDesc(GetMlDataTypeFromDmlDataType(quantizedDataType), scaleBroadcastedShape, scaleShape);
// Zero points are stored in uint8 data in ORT, which means that it will not be tightly packed if the last dimension is an odd number. To account for that,
// we round the strides of the next multiple of 2.
auto lastDimension = m_inputTensorDescs[3].GetSizes().back();
if (lastDimension % 2 != 0)
{
std::vector<uint32_t> strides(m_inputTensorDescs[3].GetDimensionCount());
strides[strides.size() - 1] = 1;
strides[strides.size() - 2] = lastDimension + 1;
m_inputTensorDescs[3].SetStrides(strides);
}
}
auto dequantizedInputTensorDesc = TensorDesc::ConstructDefaultTensorDesc(mlDataType, bBroadcastedShape);
auto dequantizedInputDmlTensorDesc = dequantizedInputTensorDesc.GetDmlDesc();
// Initialize the output description while overriding the shape
m_outputTensorDescs[0] = TensorDesc::ConstructDefaultTensorDesc(mlDataType, outputShape);
std::vector<DML_TENSOR_DESC> inputDescs = GetDmlInputDescs();
std::vector<DML_TENSOR_DESC> outputDescs = GetDmlOutputDescs();
std::vector<DML_TENSOR_DESC> quantizationTensors;
quantizationTensors.push_back(inputDescs[2]);
if (hasZeroPoint)
{
quantizationTensors.push_back(inputDescs[3]);
}
DML_DEQUANTIZE_OPERATOR_DESC dequantizeDesc = {};
dequantizeDesc.InputTensor = &inputDescs[1];
dequantizeDesc.QuantizationType = hasZeroPoint ? DML_QUANTIZATION_TYPE_SCALE_ZERO_POINT : DML_QUANTIZATION_TYPE_SCALE;
dequantizeDesc.QuantizationTensorCount = gsl::narrow_cast<uint32_t>(quantizationTensors.size());
dequantizeDesc.QuantizationTensors = quantizationTensors.data();
dequantizeDesc.OutputTensor = &dequantizedInputDmlTensorDesc;
DML_OPERATOR_DESC dequantizeOpDesc = { DML_OPERATOR_DEQUANTIZE, &dequantizeDesc };
DML_GEMM_OPERATOR_DESC gemmDesc = {};
gemmDesc.ATensor = &inputDescs[0];
gemmDesc.BTensor = &dequantizedInputDmlTensorDesc;
gemmDesc.CTensor = nullptr;
gemmDesc.OutputTensor = &outputDescs[0];
gemmDesc.TransA = DML_MATRIX_TRANSFORM_NONE;
gemmDesc.TransB = DML_MATRIX_TRANSFORM_TRANSPOSE;
gemmDesc.Alpha = 1.0f;
gemmDesc.Beta = 0.0f;
DML_OPERATOR_DESC gemmOpDesc = { DML_OPERATOR_GEMM, &gemmDesc };
// Construct the graph
std::vector<DML_INPUT_GRAPH_EDGE_DESC> inputEdges;
std::vector<DML_INTERMEDIATE_GRAPH_EDGE_DESC> intermediateEdges;
std::vector<DML_OUTPUT_GRAPH_EDGE_DESC> outputEdges;
std::vector<const DML_OPERATOR_DESC*> opDescs = {
&dequantizeOpDesc,
&gemmOpDesc,
};
DML_INPUT_GRAPH_EDGE_DESC secondInputToDequantizeEdge = {};
secondInputToDequantizeEdge.GraphInputIndex = 1;
secondInputToDequantizeEdge.ToNodeIndex = 0;
secondInputToDequantizeEdge.ToNodeInputIndex = 0;
inputEdges.push_back(secondInputToDequantizeEdge);
DML_INPUT_GRAPH_EDGE_DESC scaleToDequantizeEdge = {};
scaleToDequantizeEdge.GraphInputIndex = 2;
scaleToDequantizeEdge.ToNodeIndex = 0;
scaleToDequantizeEdge.ToNodeInputIndex = 1;
inputEdges.push_back(scaleToDequantizeEdge);
if (hasZeroPoint)
{
DML_INPUT_GRAPH_EDGE_DESC zeroPointToDequantizeEdge = {};
zeroPointToDequantizeEdge.GraphInputIndex = 3;
zeroPointToDequantizeEdge.ToNodeIndex = 0;
zeroPointToDequantizeEdge.ToNodeInputIndex = 2;
inputEdges.push_back(zeroPointToDequantizeEdge);
}
DML_INPUT_GRAPH_EDGE_DESC firstInputToGemmEdge = {};
firstInputToGemmEdge.GraphInputIndex = 0;
firstInputToGemmEdge.ToNodeIndex = 1;
firstInputToGemmEdge.ToNodeInputIndex = 0;
inputEdges.push_back(firstInputToGemmEdge);
DML_INTERMEDIATE_GRAPH_EDGE_DESC dequantizeToGemmEdge = {};
dequantizeToGemmEdge.FromNodeIndex = 0;
dequantizeToGemmEdge.FromNodeOutputIndex = 0;
dequantizeToGemmEdge.ToNodeIndex = 1;
dequantizeToGemmEdge.ToNodeInputIndex = 1;
intermediateEdges.push_back(dequantizeToGemmEdge);
DML_OUTPUT_GRAPH_EDGE_DESC gemmToOutputEdge = {};
gemmToOutputEdge.FromNodeIndex = 1;
gemmToOutputEdge.FromNodeOutputIndex = 0;
gemmToOutputEdge.GraphOutputIndex = 0;
outputEdges.push_back(gemmToOutputEdge);
MLOperatorGraphDesc operatorGraphDesc = {};
operatorGraphDesc.inputEdgeCount = gsl::narrow_cast<uint32_t>(inputEdges.size());
operatorGraphDesc.inputEdges = inputEdges.data();
operatorGraphDesc.intermediateEdgeCount = gsl::narrow_cast<uint32_t>(intermediateEdges.size());
operatorGraphDesc.intermediateEdges = intermediateEdges.data();
operatorGraphDesc.outputEdgeCount = gsl::narrow_cast<uint32_t>(outputEdges.size());
operatorGraphDesc.outputEdges = outputEdges.data();
operatorGraphDesc.nodeCount = gsl::narrow_cast<uint32_t>(opDescs.size());
operatorGraphDesc.nodes = opDescs.data();
SetDmlOperatorGraphDesc(std::move(operatorGraphDesc), kernelInfo);
}
};
void CALLBACK QueryMatMulNBits(IMLOperatorSupportQueryContextPrivate* context, /*out*/ bool* isSupported)
{
*isSupported = false;
MLOperatorAttributes attributes(context);
const auto bitCount = attributes.GetAttribute<int64_t>(AttrName::Bits);
if (bitCount != 4 && bitCount != 8)
{
return;
}
// DML only supports perfect blocks at the moment, which is what most quantization tools produce anyways since they pad
// as part of the quantization process
const uint32_t bColCount = gsl::narrow_cast<uint32_t>(attributes.GetAttribute<int64_t>(AttrName::UppercaseK));
const auto blockSize = attributes.GetAttribute<int64_t>(AttrName::MatMulNBitsBlockSize);
ML_CHECK_VALID_ARGUMENT(blockSize > 0);
if (bColCount % blockSize != 0)
{
return;
}
*isSupported = true;
}
DML_OP_DEFINE_CREATION_FUNCTION(MatMulNBits, DmlOperatorMatMulNBits);
} // namespace Dml

View file

@ -530,6 +530,7 @@ DML_OP_EXTERN_CREATION_FUNCTION(BitwiseOr);
DML_OP_EXTERN_CREATION_FUNCTION(BitwiseXor);
DML_OP_EXTERN_CREATION_FUNCTION(BitwiseNot);
DML_OP_EXTERN_CREATION_FUNCTION(RotaryEmbedding);
DML_OP_EXTERN_CREATION_FUNCTION(MatMulNBits);
DML_OP_EXTERN_QUERY_FUNCTION(MaxPool);
DML_OP_EXTERN_QUERY_FUNCTION(Slice);
@ -544,6 +545,7 @@ DML_OP_EXTERN_QUERY_FUNCTION(SkipLayerNormalization);
DML_OP_EXTERN_QUERY_FUNCTION(QLinearSigmoid);
DML_OP_EXTERN_QUERY_FUNCTION(QAttention);
DML_OP_EXTERN_QUERY_FUNCTION(Attention);
DML_OP_EXTERN_QUERY_FUNCTION(MatMulNBits);
constexpr static std::array<const char*, 1> typeNameListDefault = {"T"};
constexpr static std::array<const char*, 1> typeNameListDefaultV = {"V"};
@ -632,6 +634,7 @@ constexpr static std::array<SupportedTensorDataTypes, 2> supportedTypeListAttent
constexpr static std::array<SupportedTensorDataTypes, 2> supportedTypeListRotaryEmbedding = {SupportedTensorDataTypes::Float16to32, SupportedTensorDataTypes::Int64};
constexpr static std::array<SupportedTensorDataTypes, 2> supportedTypeListGroupNorm = {SupportedTensorDataTypes::Float16to32, SupportedTensorDataTypes::Float16to32};
constexpr static std::array<SupportedTensorDataTypes, 1> supportedTypeListNonZero = {SupportedTensorDataTypes::Float16to32 | SupportedTensorDataTypes::Ints8Bit | SupportedTensorDataTypes::Ints16Bit | SupportedTensorDataTypes::Ints32Bit | SupportedTensorDataTypes::Bool};
constexpr static std::array<SupportedTensorDataTypes, 2> supportedTypeListMatMulNBits = {SupportedTensorDataTypes::Float16to32, SupportedTensorDataTypes::UInt8};
constexpr static std::array<SupportedTensorDataTypes, 3> supportedTypeListQLinearMatMul = {
SupportedTensorDataTypes::Ints8Bit,
@ -1140,6 +1143,7 @@ constexpr static OperatorRegistrationInformation operatorRegistrationInformation
{REG_INFO_MS( 1, BiasAdd, typeNameListDefault, supportedTypeListFloat16to32, DmlGraphSupport::Supported)},
{REG_INFO_MS( 1, QuickGelu, typeNameListDefault, supportedTypeListFloat16to32, DmlGraphSupport::Supported)},
{REG_INFO_MS( 1, GroupNorm, typeNameListGroupNorm, supportedTypeListGroupNorm, DmlGraphSupport::Supported)},
{REG_INFO_MS( 1, MatMulNBits, typeNameListTwo, supportedTypeListMatMulNBits, DmlGraphSupport::Supported, requiredConstantCpuInputs(), std::nullopt, QueryMatMulNBits)},
// Operators that need to alias an input with an output
{REG_INFO_MS_ALIAS(1, GroupQueryAttention, Aliases(std::make_pair(3, 1), std::make_pair(4, 2)), typeNameListAttention, supportedTypeListAttention, DmlGraphSupport::Supported, requiredConstantCpuInputs(6))},

View file

@ -126,6 +126,10 @@ namespace AttrName
static constexpr const char* GraphFusedActivation = "activation";
static constexpr const char* GraphFusedAxis = "activation_axis";
static constexpr const char* Interleaved = "interleaved";
static constexpr const char* Bits = "bits";
static constexpr const char* UppercaseN = "N";
static constexpr const char* UppercaseK = "K";
static constexpr const char* MatMulNBitsBlockSize = "block_size";
} // namespace AttrName

View file

@ -2919,4 +2919,29 @@ namespace OperatorHelper
return { EdgeShapes(std::move(outputShape)) };
}
std::vector<EdgeShapes> MatMulNBitsHelper::GetOutputShapes(const MLShapeInferenceContext& shapeInfo) const
{
auto inputShape = shapeInfo.GetInputTensorShape(0);
onnxruntime::TensorShape aShape(std::vector<int64_t>(inputShape.begin(), inputShape.end()));
onnxruntime::TensorShape bShape({m_bRowCount, m_bColCount});
onnxruntime::MatMulComputeHelper helper;
// The B tensor is always transposed
ML_CHECK_VALID_ARGUMENT(helper.Compute(aShape, bShape, false, true).IsOK());
const auto outputShape = helper.OutputShape().GetDims();
std::vector<uint32_t> uint32OutputShape;
uint32OutputShape.reserve(outputShape.size());
std::transform(outputShape.begin(), outputShape.end(), std::back_inserter(uint32OutputShape), [](int64_t dimSize){ return static_cast<uint32_t>(dimSize); });
return { EdgeShapes(uint32OutputShape) };
}
void MatMulNBitsHelper::Initialize(const IKernelInformationAdapter& kernelInformation)
{
m_bRowCount = kernelInformation.GetAttributes().GetAttribute<int64_t>(AttrName::UppercaseN);
m_bColCount = kernelInformation.GetAttributes().GetAttribute<int64_t>(AttrName::UppercaseK);
}
} // namespace OperatorHelper

View file

@ -1606,6 +1606,24 @@ public:
std::vector<EdgeShapes> GetOutputShapes(const MLShapeInferenceContext& shapeInfo) const;
};
class MatMulNBitsHelper
{
public:
template <typename Info_t, typename Shape_t>
MatMulNBitsHelper(const Info_t& info, const Shape_t& shapeInfo)
{
Initialize(KernelInformationAdapter(info));
}
std::vector<EdgeShapes> GetOutputShapes(const MLShapeInferenceContext& shapeInfo) const;
private:
void Initialize(const IKernelInformationAdapter& kernelInformation);
int64_t m_bRowCount;
int64_t m_bColCount;
};
using ShapeInferenceHelper_Conv = ConvHelper;
using ShapeInferenceHelper_NhwcConv = NhwcConvHelper;
using ShapeInferenceHelper_ConvTranspose = ConvTransposeHelper;
@ -1859,5 +1877,6 @@ using ShapeInferenceHelper_DmlFusedSum = GetBroadcastedOutputShapeHelper;
using ShapeInferenceHelper_Shape = ShapeHelper;
using ShapeInferenceHelper_Size = SizeHelper;
using ShapeInferenceHelper_BiasSplitGelu = BiasSplitGeluHelper;
using ShapeInferenceHelper_MatMulNBits = MatMulNBitsHelper;
} // namespace OperatorHelper

View file

@ -467,6 +467,7 @@ namespace OperatorHelper
static const int sc_sinceVer_RotaryEmbedding = 1;
static const int sc_sinceVer_QLinearAveragePool = 1;
static const int sc_sinceVer_QLinearGlobalAveragePool = 1;
static const int sc_sinceVer_MatMulNBits = 1;
static const int sc_sinceVer_DynamicQuantizeMatMul = 1;
} // namespace MsftOperatorSet1

View file

@ -156,7 +156,15 @@ void RunTest(int64_t M, int64_t N, int64_t K, int64_t block_size, int64_t accura
test.SetOutputAbsErr("Y", fp16_abs_error);
std::vector<std::unique_ptr<IExecutionProvider>> execution_providers;
#ifdef USE_CUDA
execution_providers.push_back(DefaultCudaExecutionProvider());
#endif
#ifdef USE_DML
execution_providers.push_back(DefaultDmlExecutionProvider());
#endif
test.Run(OpTester::ExpectResult::kExpectSuccess, "", {}, nullptr, &execution_providers);
} else {
test.AddInput<float>("A", {M, K}, input0_vals, false);
@ -205,7 +213,7 @@ TEST(MatMulNBits, Float32) {
for (auto N : {1, 2, 32, 288}) {
for (auto K : {16, 32, 64, 128, 256, 1024, 93, 1234}) {
for (auto block_size : {16, 32, 64, 128}) {
#ifdef ORT_NEURAL_SPEED
#if defined(ORT_NEURAL_SPEED) || defined(USE_DML)
for (auto accuracy_level : {0, 1, 4}) {
RunTest(M, N, K, block_size, accuracy_level, false, false);
RunTest(M, N, K, block_size, accuracy_level, true, false);
@ -224,15 +232,25 @@ TEST(MatMulNBits, Float32) {
}
}
#if defined(USE_CUDA)
#if defined(USE_CUDA) || defined(USE_DML)
TEST(MatMulNBits, Float16) {
#ifdef USE_CUDA
auto has_gidx_options = {true, false};
#else
auto has_gidx_options = {false};
#endif
for (auto M : {1, 2, 100}) {
for (auto N : {1, 2, 32, 288}) {
for (auto K : {16, 32, 64, 128, 256, 1024, 93, 1234}) {
for (auto block_size : {16, 32, 64, 128}) {
for (auto has_gidx : {true, false}) {
for (auto has_gidx : has_gidx_options) {
#ifdef USE_DML
RunTest(M, N, K, block_size, 0, false, true, has_gidx, true, 0.04f);
#else
RunTest(M, N, K, block_size, 0, false, true, has_gidx);
RunTest(M, N, K, block_size, 0, true, true, has_gidx, false);
#endif
}
}
}
@ -241,11 +259,21 @@ TEST(MatMulNBits, Float16) {
}
TEST(MatMulNBits, Float16Large) {
#ifdef USE_DML
// For some reason, the A10 machine that runs these tests during CI has a much bigger error than all retail
// machines we tested on. All consumer-grade machines from Nvidia/AMD/Intel seem to pass these tests with an
// absolute error of 0.08, but the A10 has errors going as high as 0.22. Ultimately, given the large number
// of elements in this test, ULPs should probably be used instead of absolute/relative tolerances.
float abs_error = 0.3f;
#else
float abs_error = 0.05f;
#endif
for (auto block_size : {16, 32, 64, 128}) {
for (auto symmetric : {false, true}) {
RunTest(1, 4096, 4096, block_size, 0, symmetric, true, false, true, 0.05f);
RunTest(1, 4096, 11008, block_size, 0, symmetric, true, false, true, 0.05f);
RunTest(1, 11008, 4096, block_size, 0, symmetric, true, false, true, 0.05f);
RunTest(1, 4096, 4096, block_size, 0, symmetric, true, false, true, abs_error);
RunTest(1, 4096, 11008, block_size, 0, symmetric, true, false, true, abs_error);
RunTest(1, 11008, 4096, block_size, 0, symmetric, true, false, true, abs_error);
}
}
}