diff --git a/include/onnxruntime/core/framework/data_types.h b/include/onnxruntime/core/framework/data_types.h index a496787b51..49ffe68742 100644 --- a/include/onnxruntime/core/framework/data_types.h +++ b/include/onnxruntime/core/framework/data_types.h @@ -3,14 +3,16 @@ #pragma once +#include +#include #include -#include #include #include #include #include "core/common/common.h" #include "core/common/exceptions.h" +#include "core/framework/endian.h" struct OrtValue; @@ -60,43 +62,29 @@ inline bool operator<(const MLFloat16& left, const MLFloat16& right) { return left.val < right.val; } -struct ort_endian { - union q { - uint16_t v_; - uint8_t b_[2]; - constexpr explicit q(uint16_t v) noexcept : v_(v) {} - }; - static constexpr bool is_little() { - return q(0x200).b_[0] == 0x0; - } - static constexpr bool is_big() { - return q(0x200).b_[0] == 0x2; - } -}; - //BFloat16 struct BFloat16 { uint16_t val{0}; explicit BFloat16() = default; explicit BFloat16(uint16_t v) : val(v) {} explicit BFloat16(float v) { - uint16_t* dst = reinterpret_cast(&v); - if (ort_endian::is_little()) { - val = dst[1]; + if (endian::native == endian::little) { + std::memcpy(&val, reinterpret_cast(&v) + sizeof(uint16_t), sizeof(uint16_t)); } else { - val = dst[0]; + std::memcpy(&val, &v, sizeof(uint16_t)); } } float ToFloat() const { float result; - uint16_t* dst = reinterpret_cast(&result); - if (ort_endian::is_little()) { - dst[1] = val; - dst[0] = 0; + char* const first = reinterpret_cast(&result); + char* const second = first + sizeof(uint16_t); + if (endian::native == endian::little) { + std::memset(first, 0, sizeof(uint16_t)); + std::memcpy(second, &val, sizeof(uint16_t)); } else { - dst[0] = val; - dst[1] = 0; + std::memcpy(first, &val, sizeof(uint16_t)); + std::memset(second, 0, sizeof(uint16_t)); } return result; } diff --git a/include/onnxruntime/core/framework/endian.h b/include/onnxruntime/core/framework/endian.h new file mode 100644 index 0000000000..629fb78f0f --- /dev/null +++ b/include/onnxruntime/core/framework/endian.h @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +namespace onnxruntime { + +// the semantics of this enum should match std::endian from C++20 +enum class endian { +#if defined(_WIN32) + little = 0, + big = 1, + native = little, +#elif defined(__GNUC__) || defined(__clang__) + little = __ORDER_LITTLE_ENDIAN__, + big = __ORDER_BIG_ENDIAN__, + native = __BYTE_ORDER__, +#else +#error onnxruntime::endian is not implemented in this environment. +#endif +}; + +static_assert( + endian::native == endian::little || endian::native == endian::big, + "Only little-endian or big-endian native byte orders are supported."); + +} // namespace onnxruntime diff --git a/onnxruntime/core/framework/endian_utils.cc b/onnxruntime/core/framework/endian_utils.cc new file mode 100644 index 0000000000..9fee46b22c --- /dev/null +++ b/onnxruntime/core/framework/endian_utils.cc @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "core/framework/endian_utils.h" + +#include +#include + +#include "core/framework/endian.h" + +namespace onnxruntime { +namespace utils { + +namespace { + +// analogous to std::reverse_copy +template +OutputIt ReverseCopy(BidirIt first, BidirIt last, OutputIt d_first) { + while (last != first) { + --last; + *d_first = *last; + ++d_first; + } + return d_first; +} + +} // namespace + +void SwapByteOrderCopy( + size_t element_size_in_bytes, + gsl::span source_bytes, gsl::span destination_bytes) { + assert(element_size_in_bytes > 0); + assert(source_bytes.size_bytes() % element_size_in_bytes == 0); + assert(source_bytes.size_bytes() == destination_bytes.size_bytes()); + + for (size_t element_offset = 0, element_offset_end = source_bytes.size_bytes(); + element_offset < element_offset_end; + element_offset += element_size_in_bytes) { + const auto source_element_bytes = + source_bytes.subspan(element_offset, element_size_in_bytes); + const auto dest_element_bytes = + destination_bytes.subspan(element_offset, element_size_in_bytes); + ReverseCopy( + source_element_bytes.data(), + source_element_bytes.data() + source_element_bytes.size_bytes(), + dest_element_bytes.data()); + } +} + +namespace detail { + +void CopyLittleEndian(size_t element_size_in_bytes, gsl::span source_bytes, gsl::span destination_bytes) { + if (endian::native == endian::little) { + std::memcpy(destination_bytes.data(), source_bytes.data(), source_bytes.size_bytes()); + } else { + SwapByteOrderCopy(element_size_in_bytes, source_bytes, destination_bytes); + } +} + +} // namespace detail + +} // namespace utils +} // namespace onnxruntime diff --git a/onnxruntime/core/framework/endian_utils.h b/onnxruntime/core/framework/endian_utils.h new file mode 100644 index 0000000000..25f136705a --- /dev/null +++ b/onnxruntime/core/framework/endian_utils.h @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include + +#include "gsl/gsl" + +#include "core/common/status.h" +#include "core/common/common.h" + +namespace onnxruntime { +namespace utils { + +/** + * Swaps the byte order of elements in a buffer. + * This is a low-level funtion - please be sure to pass in valid arguments. + * In particular, source_bytes and destination_bytes should have the same size, + * which should be a multiple of element_size_in_bytes. element_size_in_bytes + * should also be greater than zero. + * + * @param element_size_in_bytes The size of an individual element, in bytes. + * @param source_bytes The source byte span. + * @param destination_bytes The destination byte span. + */ +void SwapByteOrderCopy( + size_t element_size_in_bytes, gsl::span source_bytes, gsl::span destination_bytes); + +namespace detail { + +/** + * Copies between two buffers where one is little-endian and the other has + * native endian-ness. + */ +void CopyLittleEndian( + size_t element_size_in_bytes, gsl::span source_bytes, gsl::span destination_bytes); + +} // namespace detail + +/** + * Reads from a little-endian source. + */ +template +common::Status ReadLittleEndian(gsl::span source_bytes, gsl::span destination) { +// std::is_trivially_copyable is not implemented in older versions of GCC +#if !defined(__GNUC__) || __GNUC__ >= 5 + static_assert(std::is_trivially_copyable::value, "T must be trivially copyable"); +#endif + ORT_RETURN_IF_NOT(source_bytes.size_bytes() == destination.size_bytes(), + "source and destination buffer size mismatch"); + const auto destination_bytes = gsl::make_span( + reinterpret_cast(destination.data()), destination.size_bytes()); + detail::CopyLittleEndian(sizeof(T), source_bytes, destination_bytes); + return common::Status::OK(); +} + +/** + * Writes to a little-endian destination. + */ +template +common::Status WriteLittleEndian(gsl::span source, gsl::span destination_bytes) { +// std::is_trivially_copyable is not implemented in older versions of GCC +#if !defined(__GNUC__) || __GNUC__ >= 5 + static_assert(std::is_trivially_copyable::value, "T must be trivially copyable"); +#endif + ORT_RETURN_IF_NOT(source.size_bytes() == destination_bytes.size_bytes(), + "source and destination buffer size mismatch"); + const auto source_bytes = gsl::make_span( + reinterpret_cast(source.data()), source.size_bytes()); + detail::CopyLittleEndian(sizeof(T), source_bytes, destination_bytes); + return common::Status::OK(); +} + +} // namespace utils +} // namespace onnxruntime diff --git a/onnxruntime/core/framework/tensorprotoutils.cc b/onnxruntime/core/framework/tensorprotoutils.cc index 47aec7d19a..c920ef6030 100644 --- a/onnxruntime/core/framework/tensorprotoutils.cc +++ b/onnxruntime/core/framework/tensorprotoutils.cc @@ -10,6 +10,7 @@ #include "core/common/logging/logging.h" #include "core/graph/onnx_protobuf.h" +#include "core/framework/endian_utils.h" #include "core/framework/op_kernel.h" #include "core/framework/tensor.h" #include "core/framework/ort_value_pattern_planner.h" @@ -24,17 +25,6 @@ using namespace ::onnxruntime::common; namespace { -#ifdef __GNUC__ -constexpr inline bool IsLittleEndianOrder() noexcept { return __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__; } -#else -// On Windows and Mac, this function should always return true -GSL_SUPPRESS(type .1) // allow use of reinterpret_cast for this special case -inline bool IsLittleEndianOrder() noexcept { - static int n = 1; - return (*reinterpret_cast(&n) == 1); -} -#endif - std::vector GetTensorShapeFromTensorProto(const ONNX_NAMESPACE::TensorProto& tensor_proto) { const auto& dims = tensor_proto.dims(); std::vector tensor_shape_vec(static_cast(dims.size())); @@ -49,35 +39,19 @@ std::vector GetTensorShapeFromTensorProto(const ONNX_NAMESPACE::TensorP template static Status UnpackTensorWithRawData(const void* raw_data, size_t raw_data_length, size_t expected_size, /*out*/ T* p_data) { - // allow this low level routine to be somewhat unsafe. assuming it's thoroughly tested and valid - GSL_SUPPRESS(type) // type.1 reinterpret-cast; type.4 C-style casts; type.5 'T result;' is uninitialized; - GSL_SUPPRESS(bounds .1) // pointer arithmetic - GSL_SUPPRESS(f .23) // buff and temp_bytes never tested for nullness and could be gsl::not_null - { - size_t expected_size_in_bytes; - if (!onnxruntime::IAllocator::CalcMemSizeForArray(expected_size, sizeof(T), &expected_size_in_bytes)) { - return Status(onnxruntime::common::ONNXRUNTIME, onnxruntime::common::INVALID_ARGUMENT, "size overflow"); - } - if (raw_data_length != expected_size_in_bytes) - return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, - "UnpackTensor: the pre-allocated size does not match the raw data size, expected ", - expected_size_in_bytes, ", got ", raw_data_length); - if (IsLittleEndianOrder()) { - memcpy(p_data, raw_data, raw_data_length); - } else { - const size_t type_size = sizeof(T); - const char* buff = reinterpret_cast(raw_data); - for (size_t i = 0; i < raw_data_length; i += type_size, buff += type_size) { - T result; - const char* temp_bytes = reinterpret_cast(&result); - for (size_t j = 0; j < type_size; ++j) { - memcpy((void*)&temp_bytes[j], (void*)&buff[type_size - 1 - i], 1); - } - p_data[i] = result; - } - } - return Status::OK(); + size_t expected_size_in_bytes; + if (!onnxruntime::IAllocator::CalcMemSizeForArray(expected_size, sizeof(T), &expected_size_in_bytes)) { + return Status(onnxruntime::common::ONNXRUNTIME, onnxruntime::common::INVALID_ARGUMENT, "size overflow"); } + if (raw_data_length != expected_size_in_bytes) + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "UnpackTensor: the pre-allocated size does not match the raw data size, expected ", + expected_size_in_bytes, ", got ", raw_data_length); + + const char* const raw_data_bytes = reinterpret_cast(raw_data); + ORT_RETURN_IF_ERROR(onnxruntime::utils::ReadLittleEndian( + gsl::make_span(raw_data_bytes, raw_data_length), gsl::make_span(p_data, expected_size))); + return Status::OK(); } } // namespace @@ -280,7 +254,7 @@ TensorShape GetTensorShapeFromTensorShapeProto(const ONNX_NAMESPACE::TensorShape std::vector tensor_shape_vec(static_cast(dims.size())); for (int i = 0; i < dims.size(); ++i) { tensor_shape_vec[i] = HasDimValue(dims[i]) ? dims[i].dim_value() - : -1; /* symbolic dimensions are represented as -1 in onnxruntime*/ + : -1; /* symbolic dimensions are represented as -1 in onnxruntime*/ } return TensorShape(std::move(tensor_shape_vec)); } @@ -401,7 +375,7 @@ Status TensorProtoToMLValue(const Env& env, const ORTCHAR_T* tensor_proto_path, raw_data = tensor_proto.raw_data().data(); raw_data_len = tensor_proto.raw_data().size(); } - if (IsLittleEndianOrder() && raw_data != nullptr && deleter_for_file_data.d.f != nullptr) { + if (endian::native == endian::little && raw_data != nullptr && deleter_for_file_data.d.f != nullptr) { tensor_data = const_cast(raw_data); MoveOrtCallback(deleter_for_file_data.d, deleter); } else { @@ -541,7 +515,7 @@ TensorProto::DataType GetTensorProtoType(const Tensor& tensor) { ONNX_NAMESPACE::TensorProto TensorToTensorProto(const Tensor& tensor, const std::string& tensor_proto_name, const ONNX_NAMESPACE::TypeProto& tensor_proto_type) { // Given we are using the raw_data field in the protobuf, this will work only for little-endian format. - ORT_ENFORCE(IsLittleEndianOrder()); + ORT_ENFORCE(endian::native == endian::little); // Set name, dimensions, type, and data of the TensorProto. ONNX_NAMESPACE::TensorProto tensor_proto; diff --git a/onnxruntime/test/framework/endian_test.cc b/onnxruntime/test/framework/endian_test.cc new file mode 100644 index 0000000000..fce9741925 --- /dev/null +++ b/onnxruntime/test/framework/endian_test.cc @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "core/framework/endian.h" +#include "core/framework/endian_utils.h" + +#include + +#include "gtest/gtest.h" + +namespace onnxruntime { +namespace utils { +namespace test { + +TEST(EndianTest, EndiannessDetection) { + const uint16_t test_value = 0x1234; + const char* test_value_first_byte = reinterpret_cast(&test_value); + if (endian::native == endian::little) { + EXPECT_EQ(*test_value_first_byte, 0x34); + } else if (endian::native == endian::big) { + EXPECT_EQ(*test_value_first_byte, 0x12); + } +} + +TEST(EndianTest, SwapByteOrderCopy) { + const auto src = std::vector{ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l'}; + + auto result = std::vector(src.size()); + { + SwapByteOrderCopy(3, gsl::make_span(src), gsl::make_span(result)); + const auto expected = std::vector{ + 'c', 'b', 'a', + 'f', 'e', 'd', + 'i', 'h', 'g', + 'l', 'k', 'j'}; + EXPECT_EQ(result, expected); + } + + { + SwapByteOrderCopy(4, gsl::make_span(src), gsl::make_span(result)); + const auto expected = std::vector{ + 'd', 'c', 'b', 'a', + 'h', 'g', 'f', 'e', + 'l', 'k', 'j', 'i'}; + EXPECT_EQ(result, expected); + } +} + +} // namespace test +} // namespace utils +} // namespace onnxruntime diff --git a/onnxruntime/test/onnx/tensorprotoutils.cc b/onnxruntime/test/onnx/tensorprotoutils.cc index 0ef8527424..23e998cab2 100644 --- a/onnxruntime/test/onnx/tensorprotoutils.cc +++ b/onnxruntime/test/onnx/tensorprotoutils.cc @@ -8,6 +8,7 @@ #include #include #include "core/framework/data_types.h" +#include "core/framework/endian.h" #include "core/framework/allocator.h" #include "core/session/onnxruntime_cxx_api.h" #include "core/graph/onnx_protobuf.h" @@ -22,16 +23,6 @@ struct OrtStatus { namespace onnxruntime { namespace test { -#ifdef __GNUC__ -constexpr inline bool IsLittleEndianOrder() noexcept { return __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__; } -#else -// On Windows and Mac, this function should always return true -GSL_SUPPRESS(type .1) // allow use of reinterpret_cast for this special case -inline bool IsLittleEndianOrder() noexcept { - static int n = 1; - return (*reinterpret_cast(&n) == 1); -} -#endif //From core common inline void MakeStringInternal(std::ostringstream& /*ss*/) noexcept { @@ -78,34 +69,19 @@ std::vector GetTensorShapeFromTensorProto(const onnx::TensorProto& tens template static void UnpackTensorWithRawData(const void* raw_data, size_t raw_data_length, size_t expected_size, /*out*/ T* p_data) { - // allow this low level routine to be somewhat unsafe. assuming it's thoroughly tested and valid - GSL_SUPPRESS(type) // type.1 reinterpret-cast; type.4 C-style casts; type.5 'T result;' is uninitialized; - GSL_SUPPRESS(bounds .1) // pointer arithmetic - GSL_SUPPRESS(f .23) // buff and temp_bytes never tested for nullness and could be gsl::not_null - { - size_t expected_size_in_bytes; - if (!onnxruntime::IAllocator::CalcMemSizeForArray(expected_size, sizeof(T), &expected_size_in_bytes)) { - throw Ort::Exception("size overflow", OrtErrorCode::ORT_FAIL); - } - if (raw_data_length != expected_size_in_bytes) - throw Ort::Exception(MakeString("UnpackTensor: the pre-allocated size does not match the raw data size, expected ", - expected_size_in_bytes, ", got ", raw_data_length), - OrtErrorCode::ORT_FAIL); - if (IsLittleEndianOrder()) { - memcpy(p_data, raw_data, raw_data_length); - } else { - const size_t type_size = sizeof(T); - const char* buff = reinterpret_cast(raw_data); - for (size_t i = 0; i < raw_data_length; i += type_size, buff += type_size) { - T result; - const char* temp_bytes = reinterpret_cast(&result); - for (size_t j = 0; j < type_size; ++j) { - memcpy((void*)&temp_bytes[j], (void*)&buff[type_size - 1 - i], 1); - } - p_data[i] = result; - } - } + size_t expected_size_in_bytes; + if (!onnxruntime::IAllocator::CalcMemSizeForArray(expected_size, sizeof(T), &expected_size_in_bytes)) { + throw Ort::Exception("size overflow", OrtErrorCode::ORT_FAIL); } + if (raw_data_length != expected_size_in_bytes) + throw Ort::Exception(MakeString("UnpackTensor: the pre-allocated size does not match the raw data size, expected ", + expected_size_in_bytes, ", got ", raw_data_length), + OrtErrorCode::ORT_FAIL); + if (endian::native != endian::little) { + throw Ort::Exception("UnpackTensorWithRawData only handles little-endian native byte order for now.", + OrtErrorCode::ORT_NOT_IMPLEMENTED); + } + memcpy(p_data, raw_data, raw_data_length); } // This macro doesn't work for Float16/bool/string tensors