Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions include/onnxruntime/core/session/onnxruntime_c_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -5767,9 +5767,13 @@ struct OrtApi {

/** \brief Compute total size in bytes of the tensor data contained in an OrtValue.
*
* Returns the total number of bytes used to store the tensor data. For numeric tensors,
* this is sizeof(element_type) * total_element_count. OrtValues that are not tensors or
* that are tensors that contain strings will cause an error to be returned.
* Returns the total number of bytes used to store the tensor data. For numeric tensors of a
* type that occupies at least one byte per element, this is sizeof(element_type) *
* total_element_count. For packed sub-byte types (e.g. int4/uint4, which store multiple
* elements per byte) it is the actual packed storage size, which is smaller than
* sizeof(element_type) * total_element_count. Use this value (not the element count) when
* copying or bounds-checking the raw tensor buffer. OrtValues that are not tensors or that are
* tensors that contain strings will cause an error to be returned.
*
* \param[in] ort_value OrtValue instance containing a tensor
* \param[out] size The total size of the tensor data in bytes
Expand Down
13 changes: 11 additions & 2 deletions include/onnxruntime/core/session/onnxruntime_cxx_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -2064,7 +2064,12 @@ struct TensorTypeAndShapeInfoImpl : Base<T> {
using B::B;

ONNXTensorElementDataType GetElementType() const; ///< Wraps OrtApi::GetTensorElementType
size_t GetElementCount() const; ///< Wraps OrtApi::GetTensorShapeElementCount

/// Wraps OrtApi::GetTensorShapeElementCount.
/// Returns the number of logical elements in the tensor (the product of its shape dimensions).
/// Use Ort::Value::GetTensorSizeInBytes() when sizing or bounds-checking the raw buffer returned
/// by GetTensorRawData()/GetTensorData<T>().
size_t GetElementCount() const;

size_t GetDimensionsCount() const; ///< Wraps OrtApi::GetDimensionsCount

Expand Down Expand Up @@ -2345,7 +2350,11 @@ struct ConstValueImpl : Base<T> {
/// <summary>
/// Returns the total size of the tensor data in bytes. Throws an exception if the OrtValue
/// does not contain a tensor or if it contains a tensor that contains strings.
/// For numeric tensors, this is sizeof(element_type) * total_element_count.
/// For numeric tensors of a type that occupies at least one byte per element, this is
/// sizeof(element_type) * total_element_count. For packed sub-byte types (e.g. int4/uint4)
/// it is the actual packed storage size, which is smaller than the element count returned by
/// GetTensorTypeAndShapeInfo().GetElementCount(). Use this value (not the element count) when
/// copying or bounds-checking the raw buffer from GetTensorRawData()/GetTensorData<T>().
/// </summary>
/// <returns>The total size of the tensor data in bytes</returns>
size_t GetTensorSizeInBytes() const; ///< Wraps OrtApi::GetTensorSizeInBytes
Expand Down
60 changes: 50 additions & 10 deletions onnxruntime/core/optimizer/constant_folding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "core/graph/graph_utils.h"
#include "core/optimizer/optimizer_execution_frame.h"
#include "core/framework/op_kernel.h"
#include "core/framework/tensor.h"
#include "core/framework/tensorprotoutils.h"
#include "core/session/onnxruntime_session_options_config_keys.h"
#include "core/common/safeint.h"
Expand Down Expand Up @@ -148,6 +149,15 @@ static Status ConstantFoldIfNode(Graph& graph, Node& if_node, const logging::Log
static constexpr int64_t kDefaultConstantFoldingMaxOutputSizeInBytes = 1024 * 1024 * 1024;

static size_t GetElementSizeForConstantFolding(ONNX_NAMESPACE::TensorProto_DataType elem_type) {
// Complex types are excluded: ORT's tensor type system (DataTypeImpl::TensorTypeFromONNXEnum)
// does not support them, so we cannot size them via Tensor::CalculateTensorStorageSize().
// Returning 0 makes callers treat the size as unknown instead of attempting an unsupported
// conversion.
if (elem_type == ONNX_NAMESPACE::TensorProto_DataType_COMPLEX64 ||
elem_type == ONNX_NAMESPACE::TensorProto_DataType_COMPLEX128) {
return 0;
}

const size_t element_size = utils::GetElementSizeOfTensor(elem_type);
if (element_size != 0) {
return element_size;
Expand All @@ -157,6 +167,28 @@ static size_t GetElementSizeForConstantFolding(ONNX_NAMESPACE::TensorProto_DataT
return elem_type == ONNX_NAMESPACE::TensorProto_DataType_STRING ? sizeof(std::string) : 0;
}

// Computes the packed storage size in bytes for `num_elements` elements of `elem_type`.
// Delegates to Tensor::CalculateTensorStorageSize() so the sub-byte packing math (e.g. int4/uint4
// pack 2 elements per byte, int2/uint2 pack 4) lives in a single place and does not have to be kept
// in sync here. Returns -1 if the element type is not a recognized tensor element type or the
// computed size cannot be represented.
static int64_t ComputeTensorSizeInBytesForConstantFolding(ONNX_NAMESPACE::TensorProto_DataType elem_type,
int64_t num_elements) {
if (GetElementSizeForConstantFolding(elem_type) == 0) {
return -1; // Unknown element type.
}

const MLDataType elt_type = DataTypeImpl::TensorTypeFromONNXEnum(elem_type)->GetElementType();
size_t storage_size = 0;
const Status status =
Tensor::CalculateTensorStorageSize(elt_type, TensorShape({num_elements}), /*alignment*/ 0, storage_size);
if (!status.IsOK() || storage_size > static_cast<size_t>(std::numeric_limits<int64_t>::max())) {
return -1;
}

return static_cast<int64_t>(storage_size);
}

static int64_t EstimateTensorElementCount(const ONNX_NAMESPACE::TensorShapeProto& shape) {
SafeInt<int64_t> num_elements = 1;
for (int i = 0; i < shape.dim_size(); ++i) {
Expand Down Expand Up @@ -197,7 +229,7 @@ static int64_t EstimateTensorSizeInBytes(const NodeArg& node_arg) {
return -1;
}

return SafeInt<int64_t>(num_elements) * static_cast<int64_t>(element_size);
return ComputeTensorSizeInBytesForConstantFolding(elem_type, num_elements);
}

static int64_t EstimateUniqueOutputSizeInBytes(const Node& node) {
Expand Down Expand Up @@ -233,8 +265,17 @@ static int64_t EstimateUniqueOutputSizeInBytes(const Node& node) {
continue;
}

const size_t element_size = output_idx == 0 ? input_element_size : sizeof(int64_t);
total_size += SafeInt<int64_t>(input_num_elements) * static_cast<int64_t>(element_size);
if (output_idx == 0) {
// Output 0 has the same (possibly packed sub-byte) element type as the input.
const int64_t output0_size = ComputeTensorSizeInBytesForConstantFolding(input_elem_type, input_num_elements);
if (output0_size < 0) {
return -1; // Output 0 size is unknown; treat the whole node's output size as unknown.
}
total_size += output0_size;
} else {
Comment thread
neilmsft marked this conversation as resolved.
Resolved
// The remaining Unique outputs are int64 index/count tensors.
total_size += SafeInt<int64_t>(input_num_elements) * sizeof(int64_t);
}
}

return total_size;
Expand Down Expand Up @@ -280,21 +321,20 @@ static int64_t EstimateConstantOfShapeOutputSizeInBytes(const Node& node, const
num_elements *= dim;
}

// Determine the element size of the output. The ONNX spec for ConstantOfShape defaults the
// Determine the element type of the output. The ONNX spec for ConstantOfShape defaults the
// element type to float when the optional 'value' attribute is absent.
size_t element_size = sizeof(float);
auto elem_type = ONNX_NAMESPACE::TensorProto_DataType_FLOAT;
const auto& attrs = node.GetAttributes();
auto it = attrs.find("value");
if (it != attrs.end() && it->second.type() == ONNX_NAMESPACE::AttributeProto::TENSOR) {
const auto elem_type = static_cast<ONNX_NAMESPACE::TensorProto_DataType>(
const auto value_elem_type = static_cast<ONNX_NAMESPACE::TensorProto_DataType>(
it->second.t().data_type());
const size_t es = GetElementSizeForConstantFolding(elem_type);
if (es != 0) {
element_size = es;
if (GetElementSizeForConstantFolding(value_elem_type) != 0) {
elem_type = value_elem_type;
}
}

return num_elements * static_cast<int64_t>(element_size);
return ComputeTensorSizeInBytesForConstantFolding(elem_type, num_elements);
}

// Estimate the total output size in bytes for a node using shape inference results.
Expand Down
36 changes: 19 additions & 17 deletions onnxruntime/core/session/onnxruntime_c_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2018,28 +2018,34 @@ static ORT_STATUS_PTR OrtGetValueImplSeqOfMap(const OrtValue* p_ml_value, int in
}
#endif

ORT_STATUS_PTR PopulateTensorWithData(Tensor& tensor, bool is_string, _In_ const void* data_elem, size_t num_elems,
size_t elem_size) {
auto len = narrow<size_t>(tensor.Shape().Size());
if (num_elems < len) {
return OrtApis::CreateStatus(ORT_INVALID_ARGUMENT, "input array is too short");
}
if (!is_string) {
memcpy(tensor.MutableDataRaw(), data_elem, elem_size * num_elems);
// Copies the contents of `data_elem` into `tensor`.
//
// Both the element type and the number of elements to copy are derived from `tensor`:
// - For numeric tensors, tensor.SizeInBytes() bytes are copied. This is packing-aware,
// so sub-byte types (e.g. int4/uint4) copy only their packed storage size rather than
// one byte per logical element.
// - For string tensors, exactly tensor.Shape().Size() std::string elements are copied.
//
// Assumption: `data_elem` points to a buffer that holds at least as many elements as
// `tensor`'s shape, with a matching element type. All current callers satisfy this because
// they size the tensor from the same source data.
ORT_STATUS_PTR PopulateTensorWithData(Tensor& tensor, _In_ const void* data_elem) {
if (!tensor.IsDataTypeString()) {
memcpy(tensor.MutableDataRaw(), data_elem, tensor.SizeInBytes());
} else {
const auto len = narrow<size_t>(tensor.Shape().Size());
const std::string* strings = reinterpret_cast<const std::string*>(data_elem);
auto str_span = gsl::make_span(strings, num_elems);
auto str_span = gsl::make_span(strings, len);
auto* dst = tensor.MutableData<std::string>();
std::copy(str_span.begin(), str_span.end(), dst);
}
return nullptr;
}

ORT_STATUS_PTR CreateTensorAndPopulate(MLDataType element_type, const int64_t* shape, size_t shape_len,
const void* data, size_t num_elements, _Inout_ OrtAllocator* allocator, OrtValue& result) {
const void* data, _Inout_ OrtAllocator* allocator, OrtValue& result) {
ORT_API_RETURN_IF_ERROR(CreateTensorImpl(element_type, shape, shape_len, allocator, result));
ORT_API_RETURN_IF_ERROR(PopulateTensorWithData(*result.GetMutable<Tensor>(), utils::IsDataTypeString(element_type),
data, num_elements, element_type->Size()));
ORT_API_RETURN_IF_ERROR(PopulateTensorWithData(*result.GetMutable<Tensor>(), data));
return nullptr;
}

Expand All @@ -2057,7 +2063,6 @@ static ORT_STATUS_PTR OrtGetValueImplSeqOfTensors(_In_ const OrtValue* p_ml_valu
auto result = std::make_unique<OrtValue>();
ORT_API_RETURN_IF_ERROR(c_api_internal::CreateTensorAndPopulate(one_tensor.DataType(), tensor_shape.GetDims().data(),
tensor_shape.NumDimensions(), one_tensor.DataRaw(),
narrow<size_t>(one_tensor.Shape().Size()),
allocator, *result));
*out = result.release();
return nullptr;
Expand Down Expand Up @@ -2105,28 +2110,25 @@ static ORT_STATUS_PTR OrtGetValueImplMapHelper(_In_ const OrtValue* p_ml_value,
std::vector<TKey> vec_keys;
std::vector<TVal> vec_vals;
const void* data_ptr;
size_t data_size;
MLDataType element_type;
switch (index) {
case 0: { // user is requesting keys
element_type = DataTypeImpl::TensorTypeFromONNXEnum(GetONNXTensorElementDataType<TKey>())->GetElementType();
vec_keys.reserve(static_cast<size_t>(num_kv_pairs));
std::transform(data.cbegin(), data.cend(), std::back_inserter(vec_keys), [](const auto& k) { return k.first; });
data_ptr = vec_keys.data();
data_size = vec_keys.size();
} break;
case 1: { // user is requesting values
element_type = DataTypeImpl::TensorTypeFromONNXEnum(GetONNXTensorElementDataType<TVal>())->GetElementType();
vec_vals.reserve(static_cast<size_t>(num_kv_pairs));
std::transform(data.cbegin(), data.cend(), std::back_inserter(vec_vals), [](const auto& k) { return k.second; });
data_ptr = vec_vals.data();
data_size = vec_vals.size();
} break;
default:
return OrtApis::CreateStatus(ORT_FAIL, "Invalid index requested for map type.");
}
ORT_API_RETURN_IF_ERROR(c_api_internal::CreateTensorAndPopulate(element_type, dims.data(), dims.size(), data_ptr,
data_size, allocator, *result));
allocator, *result));
*out = result.release();
return nullptr;
}
Expand Down
71 changes: 71 additions & 0 deletions onnxruntime/test/optimizer/graph_transform_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#include "core/common/span_utils.h"
#include "core/framework/data_types.h"
#include "core/framework/int4.h"
#include "core/framework/ort_value.h"
#include "core/graph/graph_utils.h"
#include "core/graph/graph_viewer.h"
Expand Down Expand Up @@ -1752,6 +1753,76 @@ TEST_F(GraphTransformationTests, ConstantFoldingConstantOfShapeBlockedWhenOutput
pre_graph_checker, post_graph_checker));
}

// The constant-folding output-size estimate must be packing-aware for sub-byte types.
// A QuantizeLinear node that produces a packed int4 output stores 2 elements per byte, so its
// real storage size is ceil(num_elements / 2) bytes. The pre-execution size estimate previously
// used num_elements * 1 byte, over-counting by ~2x and needlessly blocking folding of sub-byte
// outputs that actually fit within the configured limit.
TEST_F(GraphTransformationTests, ConstantFoldingSubByteOutputSizeIsPacked) {
// 20000 int4 elements -> 10000 packed bytes of real storage (the old estimate was 20000 bytes).
constexpr int64_t kNumElements = 20000;

auto build_model = [&](ModelTestBuilder& builder) {
auto* input_data = builder.MakeInitializer<float>({kNumElements}, -1.0f, 1.0f);
auto* output_arg = builder.MakeOutput();
builder.AddQuantizeLinearNode<Int4x2>(input_data, 1.0f, Int4x2(0, 0), output_arg);
};

// Case 1: limit (15000 bytes) sits between the packed size (10000) and the old over-estimate
// (20000). With the packing-aware estimate the node SHOULD be folded. Under the old estimate
// it would have been (incorrectly) skipped.
{
auto pre_graph_checker = [](Graph& graph) -> Status {
auto op_to_count = CountOpsInGraph(graph);
TEST_RETURN_IF_NOT(op_to_count["QuantizeLinear"] == 1);
return Status::OK();
};

auto post_graph_checker = [](Graph& graph) -> Status {
auto op_to_count = CountOpsInGraph(graph);
// Real packed size (10000 bytes) is within the 15000 byte limit, so folding proceeds.
TEST_RETURN_IF_NOT(op_to_count["QuantizeLinear"] == 0);
return Status::OK();
};

std::unique_ptr<CPUExecutionProvider> e = std::make_unique<CPUExecutionProvider>(CPUExecutionProviderInfo());
ConfigOptions config_options;
ASSERT_STATUS_OK(config_options.AddConfigEntry(
kOrtSessionOptionsConstantFoldingMaxOutputSizeInBytes, "15000"));

ASSERT_STATUS_OK(TestGraphTransformer(build_model, 21, *logger_,
std::make_unique<ConstantFolding>(*e.get(), false, config_options),
TransformerLevel::Level1, 1,
pre_graph_checker, post_graph_checker));
}

// Case 2: limit (5000 bytes) is below even the packed size (10000), so folding is blocked.
{
auto pre_graph_checker = [](Graph& graph) -> Status {
auto op_to_count = CountOpsInGraph(graph);
TEST_RETURN_IF_NOT(op_to_count["QuantizeLinear"] == 1);
return Status::OK();
};

auto post_graph_checker = [](Graph& graph) -> Status {
auto op_to_count = CountOpsInGraph(graph);
// Real packed size (10000 bytes) exceeds the 5000 byte limit, so the node is not folded.
TEST_RETURN_IF_NOT(op_to_count["QuantizeLinear"] == 1);
return Status::OK();
};

std::unique_ptr<CPUExecutionProvider> e = std::make_unique<CPUExecutionProvider>(CPUExecutionProviderInfo());
ConfigOptions config_options;
ASSERT_STATUS_OK(config_options.AddConfigEntry(
kOrtSessionOptionsConstantFoldingMaxOutputSizeInBytes, "5000"));

ASSERT_STATUS_OK(TestGraphTransformer(build_model, 21, *logger_,
std::make_unique<ConstantFolding>(*e.get(), false, config_options),
TransformerLevel::Level1, 1,
pre_graph_checker, post_graph_checker));
}
}

// Test that small constant folding still works with the size limit.
TEST_F(GraphTransformationTests, ConstantFoldingSmallOutputAllowed) {
// Build a model with a small Expand: scalar -> [4, 4] = 16 * 4 = 64 bytes.
Expand Down
47 changes: 47 additions & 0 deletions onnxruntime/test/shared_lib/test_nontensor_types.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <functional>
#include <iostream>
#include <set>
#include <array>

#include "core/common/common.h"
#include "core/session/onnxruntime_cxx_api.h"
Expand Down Expand Up @@ -277,6 +278,52 @@ TEST(CApiTest, CreateGetSeqStringTensors) {
ASSERT_EQ(string_set, std::set<std::string>(std::begin(string_input_data), std::end(string_input_data)));
}

// Test - GetValue() on a sequence of packed sub-byte tensors
// (int4/uint4) must copy only the packed storage bytes.
TEST(CApiTest, CreateGetSeqSubByteTensors) {
auto default_allocator = std::make_unique<MockedOrtAllocator>();
Ort::MemoryInfo info("Cpu", OrtDeviceAllocator, 0, OrtMemTypeDefault);

auto run_for_type = [&](ONNXTensorElementDataType elem_type, std::array<uint8_t, 4> packed) {
const std::vector<int64_t> dims{7}; // 7 4-bit elements -> 4 packed bytes
constexpr int N = 2;

std::vector<Ort::Value> in;
for (int i = 0; i < N; ++i) {
Ort::Value tensor = Ort::Value::CreateTensor(info, packed.data(), packed.size(),
dims.data(), dims.size(), elem_type);
in.push_back(std::move(tensor));
}

Ort::Value seq_ort = Ort::Value::CreateSequence(in);

for (int idx = 0; idx < N; ++idx) {
Ort::Value out = seq_ort.GetValue(idx, default_allocator.get());

auto type_info = out.GetTypeInfo();
auto tensor_info = type_info.GetTensorTypeAndShapeInfo();
ASSERT_EQ(tensor_info.GetElementType(), elem_type);
ASSERT_EQ(tensor_info.GetShape(), dims);

// `out` is a fresh tensor that GetValue() allocated and copied into, so its data is a
// distinct buffer from the input `packed` bytes (the input tensors alias `packed`).
// Comparing them therefore validates the copy rather than reading the same memory twice.
const size_t out_bytes = out.GetTensorSizeInBytes();
ASSERT_EQ(out_bytes, packed.size());
const auto* ret = static_cast<const uint8_t*>(out.GetTensorRawData());
ASSERT_NE(static_cast<const void*>(ret), static_cast<const void*>(packed.data()));
for (size_t i = 0; i < out_bytes; ++i) {
ASSERT_EQ(ret[i], packed[i]);
Comment thread
edgchen1 marked this conversation as resolved.
Resolved
}
}
};

// {0, 1, 2, 3, -8, 7, 6, pad_0}
run_for_type(ONNXTensorElementDataType::ONNX_TENSOR_ELEMENT_DATA_TYPE_INT4, {0x10, 0x32, 0x78, 0x06});
// {0, 1, 2, 3, 4, 5, 15, pad_0}
run_for_type(ONNXTensorElementDataType::ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT4, {0x10, 0x32, 0x54, 0x0F});
}

TEST(CApiTest, TypeInfoSequence) {
// Creation
auto default_allocator = std::make_unique<MockedOrtAllocator>();
Expand Down
Loading