diff --git a/onnxruntime/core/framework/tensorprotoutils.cc b/onnxruntime/core/framework/tensorprotoutils.cc index d981f610a4097..e0b31c29a054b 100644 --- a/onnxruntime/core/framework/tensorprotoutils.cc +++ b/onnxruntime/core/framework/tensorprotoutils.cc @@ -1741,117 +1741,140 @@ void MakeCpuTensorCopy(const Tensor& src_tensor, Tensor& dst_tensor) { } #if !defined(DISABLE_SPARSE_TENSORS) -static Status CopySparseData(size_t n_sparse_elements, +static Status CopySparseData(const std::string& name, + int64_t nnz_elements, const ONNX_NAMESPACE::TensorProto& indices, const std::filesystem::path& model_path, - gsl::span - dims, - std::function - copier) { + gsl::span dense_dims, + int64_t dense_elements, + std::function copier) { Status status = Status::OK(); TensorShape indices_shape(indices.dims().data(), indices.dims().size()); - const auto elements = narrow(indices_shape.Size()); + const int64_t indices_elements = indices_shape.Size(); - std::vector indices_values; // used for conversion of smaller size indices + InlinedVector indices_values; // used for conversion of smaller size indices std::vector unpack_buffer; gsl::span indices_data; - const bool has_raw_data = indices.has_raw_data(); + const bool needs_unpack = utils::HasRawData(indices) || utils::HasExternalData(indices); switch (indices.data_type()) { case ONNX_NAMESPACE::TensorProto_DataType_INT64: - if (has_raw_data) { - ORT_RETURN_IF_NOT(indices.raw_data().size() == (elements * sizeof(int64_t)), - "Sparse Indices raw data size does not match expected."); + if (needs_unpack) { + ORT_RETURN_IF_NOT(indices.raw_data().size() == (narrow(indices_elements) * sizeof(int64_t)), + "Sparse tensor: ", name, " indices raw data size does not match expected: ", + indices_elements * sizeof(int64_t)); ORT_RETURN_IF_ERROR(UnpackInitializerData(indices, model_path, unpack_buffer)); indices_data = ReinterpretAsSpan(gsl::make_span(unpack_buffer)); } else { - ORT_RETURN_IF_NOT(indices.int64_data_size() == static_cast(elements), - "Sparse indices int64 data size does not match expected"); - indices_data = gsl::make_span(indices.int64_data().data(), elements); + ORT_RETURN_IF_NOT(indices.int64_data_size() == indices_elements, + "Sparse tensor: ", name, " indices int64 data size does not match expected: ", + indices_elements); + indices_data = gsl::make_span(indices.int64_data().data(), narrow(indices_elements)); } break; case ONNX_NAMESPACE::TensorProto_DataType_INT32: { - if (has_raw_data) { - ORT_RETURN_IF_NOT(indices.raw_data().size() == (elements * sizeof(int32_t)), - "Sparse Indices raw data size does not match expected."); + if (needs_unpack) { + ORT_RETURN_IF_NOT(indices.raw_data().size() == (narrow(indices_elements) * sizeof(int32_t)), + "Sparse tensor: ", name, " indices raw data size does not match expected: ", + indices_elements * sizeof(int32_t)); ORT_RETURN_IF_ERROR(UnpackInitializerData(indices, model_path, unpack_buffer)); auto int32_span = ReinterpretAsSpan(gsl::make_span(unpack_buffer)); indices_values.insert(indices_values.cend(), int32_span.begin(), int32_span.end()); unpack_buffer.clear(); unpack_buffer.shrink_to_fit(); } else { - ORT_RETURN_IF_NOT(indices.int32_data_size() == static_cast(elements), - "Sparse indices int32 data size does not match expected"); + ORT_RETURN_IF_NOT(indices.int32_data_size() == indices_elements, + "Sparse tensor: ", name, " indices int32 data size does not match expected: ", + indices_elements); indices_values.insert(indices_values.cend(), indices.int32_data().cbegin(), indices.int32_data().cend()); } indices_data = gsl::make_span(indices_values); break; } case ONNX_NAMESPACE::TensorProto_DataType_INT16: { - if (has_raw_data) { - ORT_RETURN_IF_NOT(indices.raw_data().size() == (elements * sizeof(int16_t)), - "Sparse Indices raw data size does not match expected."); + if (needs_unpack) { + ORT_RETURN_IF_NOT(indices.raw_data().size() == (narrow(indices_elements) * sizeof(int16_t)), + "Sparse tensor: ", name, " indices raw data size does not match expected: ", + indices_elements * sizeof(int16_t)); ORT_RETURN_IF_ERROR(UnpackInitializerData(indices, model_path, unpack_buffer)); auto int16_span = ReinterpretAsSpan(gsl::make_span(unpack_buffer)); indices_values.insert(indices_values.cend(), int16_span.begin(), int16_span.end()); - indices_data = gsl::make_span(indices_values); unpack_buffer.clear(); unpack_buffer.shrink_to_fit(); } else { - return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_GRAPH, - "Invalid SparseTensor indices. INT16 indices must be in the raw data of indices tensor"); + ORT_RETURN_IF_NOT(indices.int32_data_size() == indices_elements, + "Sparse tensor: ", name, " indices int16 data size does not match expected: ", + indices_elements); + indices_values.insert(indices_values.cend(), indices.int32_data().cbegin(), indices.int32_data().cend()); } + indices_data = gsl::make_span(indices_values); break; } case ONNX_NAMESPACE::TensorProto_DataType_INT8: { - if (has_raw_data) { - ORT_RETURN_IF_NOT(indices.raw_data().size() == elements, - "Sparse Indices raw data size does not match expected."); + if (needs_unpack) { + ORT_RETURN_IF_NOT(indices.raw_data().size() == narrow(indices_elements), + "Sparse tensor: ", name, " indices raw data size does not match expected: ", + indices_elements * sizeof(int8_t)); ORT_RETURN_IF_ERROR(UnpackInitializerData(indices, model_path, unpack_buffer)); auto int8_span = ReinterpretAsSpan(gsl::make_span(unpack_buffer)); indices_values.insert(indices_values.cend(), int8_span.begin(), int8_span.end()); - indices_data = gsl::make_span(indices_values); unpack_buffer.clear(); unpack_buffer.shrink_to_fit(); } else { - return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_GRAPH, - "Invalid SparseTensor indices. INT8 indices must be in the raw data of indices tensor"); + ORT_RETURN_IF_NOT(indices.int32_data_size() == indices_elements, + "Sparse tensor: ", name, " indices int8 data size does not match expected: ", + indices_elements); + indices_values.insert(indices_values.cend(), indices.int32_data().cbegin(), indices.int32_data().cend()); } + indices_data = gsl::make_span(indices_values); break; } default: return ORT_MAKE_STATUS( ONNXRUNTIME, INVALID_GRAPH, - "Invalid SparseTensor indices. Should one of the following types: int8, int16, int32 or int64"); + "Sparse tensor: ", name, " indices. Should be one of the following types: int8, int16, int32 or int64"); } - if (indices_shape.NumDimensions() == 1) { + const auto indices_rank = indices_shape.NumDimensions(); + if (indices_rank == 1) { // flattened indexes - for (size_t i = 0; i < n_sparse_elements; ++i) { - copier(i, narrow(indices_data[i])); + for (size_t i = 0, lim = narrow(nnz_elements); i < lim; ++i) { + const auto idx = indices_data[i]; + ORT_RETURN_IF_NOT(idx >= 0 && idx < dense_elements, + "Sparse tensor: ", name, " index is out of bounds. Got:", idx, + " expected to be in [0, ", dense_elements, ")"); + + copier(i, narrow(idx)); } - } else if (indices_shape.NumDimensions() == 2) { + } else if (indices_rank == 2) { // entries in format {NNZ, rank} - ORT_ENFORCE(indices_shape[1] > 0 && static_cast(indices_shape[1]) == dims.size()); - auto rank = static_cast(indices_shape[1]); + ORT_ENFORCE(indices_shape[1] > 0 && static_cast(indices_shape[1]) == dense_dims.size()); + const auto rank = static_cast(indices_shape[1]); auto cur_index = indices_data.begin(); - std::vector multipliers; + InlinedVector multipliers; multipliers.resize(rank); // calculate sum of inner dimension elements for each dimension. // e.g. if shape {2,3,4}, the result should be {3*4, 4, 1} multipliers[rank - 1] = 1; for (auto r = rank - 1; r > 0; --r) { - multipliers[r - 1] = SafeInt(dims[r]) * multipliers[r]; + multipliers[r - 1] = SafeInt(dense_dims[r]) * multipliers[r]; } // calculate the offset for the entry // e.g. if shape was {2,3,4} and entry was (1, 0, 2) the offset is 14 // as there are 2 rows, each with 12 entries per row - for (size_t i = 0; i < n_sparse_elements; ++i) { + for (size_t i = 0, lim = narrow(nnz_elements); i < lim; ++i) { SafeInt idx = 0; for (size_t j = 0; j < rank; ++j) { - idx += SafeInt(cur_index[j]) * multipliers[j]; + const auto dim_index = cur_index[j]; + ORT_RETURN_IF_NOT(dim_index >= 0 && dim_index < dense_dims[j], + "Sparse tensor: ", name, " index is out of bounds. Got:", dim_index, + " expected to be in [0, ", dense_dims[j], ")"); + idx += SafeInt(dim_index) * multipliers[j]; } + ORT_RETURN_IF_NOT(idx >= 0 && idx < dense_elements, + "Sparse tensor: ", name, " index is out of bounds. Got:", static_cast(idx), + " expected to be in [0, ", dense_elements, ")"); copier(i, static_cast(idx)); cur_index += rank; @@ -1860,7 +1883,7 @@ static Status CopySparseData(size_t n_sparse_elements, ORT_ENFORCE(cur_index == indices_data.end()); } else { status = ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_GRAPH, - "Invalid SparseTensor indices. Should be rank 0 or 1. Got:", indices_shape); + "Sparse tensor: ", name, " indices shape. Expected to be rank 1 or 2. Got:", indices_shape); } return status; @@ -1869,53 +1892,110 @@ static Status CopySparseData(size_t n_sparse_elements, common::Status SparseTensorProtoToDenseTensorProto(const ONNX_NAMESPACE::SparseTensorProto& sparse, const std::filesystem::path& model_path, ONNX_NAMESPACE::TensorProto& dense) { - Status status = Status::OK(); + Status status; const auto& sparse_values = sparse.values(); - auto type = sparse_values.data_type(); - dense.set_data_type(type); - *dense.mutable_name() = sparse_values.name(); + const auto& name = sparse_values.name(); - SafeInt n_sparse_elements = 1; - for (auto dim : sparse_values.dims()) { - n_sparse_elements *= dim; + const auto values_rank = sparse_values.dims_size(); + if (values_rank != 1) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_GRAPH, + "Sparse tensor: ", name, " values should be rank 1 for COO format. Got:", values_rank); } - SafeInt n_dense_elements = 1; + auto type = sparse_values.data_type(); + dense.set_data_type(type); + *dense.mutable_name() = name; + SafeInt dense_elements = 1; + for (auto dim : sparse.dims()) { - n_dense_elements *= dim; + if (dim < 0) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_GRAPH, + "Sparse tensor: ", name, " dense dims expected to be non-negative. Got:", dim); + } + dense_elements *= dim; dense.add_dims(dim); } + const auto dense_dims = gsl::make_span(dense.dims().data(), dense.dims().size()); + + SafeInt nnz_elements = 1; + for (auto dim : sparse_values.dims()) { + if (dim < 0) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_GRAPH, + "Sparse tensor: ", name, " tensor dims expected to be non-negative. Got:", dim); + } + nnz_elements *= dim; + } + const auto& indices = sparse.indices(); - auto dims = gsl::make_span(dense.dims().data(), dense.dims().size()); + const auto indices_rank = indices.dims_size(); + if (indices_rank != 1 && indices_rank != 2) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_GRAPH, + "Sparse tensor: ", name, " indices should be rank 1 or 2 for supported COO format. Got:", indices_rank); + } - if (type != TensorProto_DataType_STRING) { - auto ml_data = DataTypeImpl::TensorTypeFromONNXEnum(type)->GetElementType(); - size_t element_size = ml_data->Size(); + const auto indices_dims = gsl::make_span(indices.dims().data(), indices.dims().size()); + + if (indices_dims[0] != nnz_elements) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_GRAPH, + "Sparse tensor: ", name, + " indices outer dimension should match the number of non-zero values. Got:", + indices_dims[0], " expected: ", static_cast(nnz_elements)); + } - // need to read in sparse data first as it could be in a type specific field, in raw data, or in external data - std::vector sparse_data_storage; - ORT_RETURN_IF_ERROR(UnpackInitializerData(sparse_values, model_path, sparse_data_storage)); - void* sparse_data = sparse_data_storage.data(); + if (indices_rank == 2 && dense_dims.size() != narrow(indices_dims[1])) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_GRAPH, + "Sparse tensor: ", name, + " indices is rank 2, its inner dimension should match the rank of the dense tensor. Got:", + indices_dims[1], " expected: ", dense_dims.size()); + } + + if (indices_rank == 2) { + const auto num_indices = TensorShape(indices_dims).Size(); + const int64_t expected_indices_entries = SafeInt(nnz_elements) * indices_dims[1]; + if (num_indices != expected_indices_entries) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_GRAPH, + "Sparse tensor: ", name, + " indices is rank 2, it should have NNZ values * indices_dims[1] entries. Got:", + num_indices, " expected: ", expected_indices_entries); + } + } + + if (dense_elements == 0) { + // if there are no elements in the dense tensor, we can return early with an empty tensor proto + return status; + } + + if (type != ONNX_NAMESPACE::TensorProto_DataType_STRING) { + auto ml_data = DataTypeImpl::TensorTypeFromONNXEnum(type)->GetElementType(); + const size_t element_size = ml_data->Size(); // by putting the data into a std::string we can avoid a copy as set_raw_data can do a std::move // into the TensorProto. - std::string dense_data_storage(n_dense_elements * element_size, 0); - if (n_sparse_elements > 0) { + std::string dense_data_storage(narrow(dense_elements) * element_size, 0); + if (nnz_elements > 0) { + // need to read in sparse data first as it could be in a type specific field, in raw data, or in external data + std::vector values_data; + ORT_RETURN_IF_ERROR(UnpackInitializerData(sparse_values, model_path, values_data)); + ORT_RETURN_IF_NOT(values_data.size() == static_cast(nnz_elements) * element_size, + "Sparse tensor: ", name, " values data size does not match expected: ", + static_cast(nnz_elements) * element_size); + void* sparse_data = values_data.data(); void* dense_data = dense_data_storage.data(); switch (element_size) { case 1: { status = CopySparseData( - n_sparse_elements, indices, model_path, dims, [sparse_data, dense_data](size_t from_idx, size_t to_idx) { + name, nnz_elements, indices, model_path, dense_dims, dense_elements, + [sparse_data, dense_data](size_t from_idx, size_t to_idx) { static_cast(dense_data)[to_idx] = static_cast(sparse_data)[from_idx]; }); break; } case 2: { - status = CopySparseData(n_sparse_elements, indices, model_path, dims, + status = CopySparseData(name, nnz_elements, indices, model_path, dense_dims, dense_elements, [sparse_data, dense_data](size_t from_idx, size_t to_idx) { const auto* src = static_cast(sparse_data) + from_idx; auto* dst = static_cast(dense_data) + to_idx; @@ -1925,7 +2005,7 @@ common::Status SparseTensorProtoToDenseTensorProto(const ONNX_NAMESPACE::SparseT break; } case 4: { - status = CopySparseData(n_sparse_elements, indices, model_path, dims, + status = CopySparseData(name, nnz_elements, indices, model_path, dense_dims, dense_elements, [sparse_data, dense_data](size_t from_idx, size_t to_idx) { const auto* src = static_cast(sparse_data) + from_idx; auto* dst = static_cast(dense_data) + to_idx; @@ -1935,7 +2015,7 @@ common::Status SparseTensorProtoToDenseTensorProto(const ONNX_NAMESPACE::SparseT break; } case 8: { - status = CopySparseData(n_sparse_elements, indices, model_path, dims, + status = CopySparseData(name, nnz_elements, indices, model_path, dense_dims, dense_elements, [sparse_data, dense_data](size_t from_idx, size_t to_idx) { const auto* src = static_cast(sparse_data) + from_idx; auto* dst = static_cast(dense_data) + to_idx; diff --git a/onnxruntime/core/framework/tensorprotoutils.h b/onnxruntime/core/framework/tensorprotoutils.h index 8c9f64e9fbb9f..685fa65a73720 100644 --- a/onnxruntime/core/framework/tensorprotoutils.h +++ b/onnxruntime/core/framework/tensorprotoutils.h @@ -249,10 +249,14 @@ common::Status ConstantNodeProtoToTensorProto(const ONNX_NAMESPACE::NodeProto& n void MakeCpuTensorCopy(const Tensor& src_tensor, Tensor& dst_tensor); #if !defined(DISABLE_SPARSE_TENSORS) -// Convert a SparseTensorProto to a dense TensorProto -// If the SparseTensorProto contains external data then it loads the data and converts to dense tensor proto -// The resulting TensorProto will contain the data as raw data. -// model_path is used for constructing full path for external_data +/// +// The function supports only COO format with 1D or 2D indices. Values shape is expected to be 1D. +// The function does not support sparse tensors of other formats like CSR/CSC. +/// +/// +/// model path is only used if there are references to external data. +/// The resulting dense tensor proto. +/// Status common::Status SparseTensorProtoToDenseTensorProto(const ONNX_NAMESPACE::SparseTensorProto& sparse, const std::filesystem::path& model_path, ONNX_NAMESPACE::TensorProto& dense); diff --git a/onnxruntime/test/framework/sparse_kernels_test.cc b/onnxruntime/test/framework/sparse_kernels_test.cc index 89e928af10b8b..d2842d203dccd 100644 --- a/onnxruntime/test/framework/sparse_kernels_test.cc +++ b/onnxruntime/test/framework/sparse_kernels_test.cc @@ -756,8 +756,7 @@ static NodeProto CreateConstantNodeAllZeros(bool indices_1D, std::vector& exp constant_node.set_op_type("Constant"); constant_node.add_output("dense_tensor_output"); - std::vector indices; - std::vector shape{2, 3, 2}; + const std::array shape{2, 3, 2}; AttributeProto& attrib = *constant_node.mutable_attribute()->Add(); attrib.set_name("sparse_value_all_zeros"); @@ -772,7 +771,7 @@ static NodeProto CreateConstantNodeAllZeros(bool indices_1D, std::vector& exp } else { // indices are shape {NNZ, rank} so convert flattened values of 2, 5, 6 and 10 to rank 3 values indices_tp.add_dims(0); - indices_tp.add_dims(0); + indices_tp.add_dims(3); } indices_tp.set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT64); @@ -1870,7 +1869,387 @@ TEST(SparseTensorConversionTests, BlockSparse) { indices_span.begin(), indices_span.end())); } } -#endif // !defined(DISABLE_SPARSE_TENSORS) +template +void TestSparseToDenseConversion(gsl::span dense_shape, + const std::vector& values, + gsl::span indices, + gsl::span indices_shape, + bool raw_data_indices, + const std::vector& expected_dense_data) { + ONNX_NAMESPACE::SparseTensorProto sparse_proto; + for (auto dim : dense_shape) { + sparse_proto.add_dims(dim); + } + + // Create values tensor + auto* values_tensor = sparse_proto.mutable_values(); + values_tensor->set_name("values"); + // Simplification: assuming float/int32 for now based on T + if constexpr (std::is_same_v) { + values_tensor->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + for (float v : values) values_tensor->add_float_data(v); + } else if constexpr (std::is_same_v) { + values_tensor->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT32); + for (int32_t v : values) values_tensor->add_int32_data(v); + } + // Set values shape [NNZ] + values_tensor->add_dims(values.size()); + + // Create indices tensor + auto* indices_tensor = sparse_proto.mutable_indices(); + indices_tensor->set_name("indices"); + indices_tensor->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT64); + if constexpr (std::is_same_v) { + indices_tensor->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT8); + if (raw_data_indices) { + indices_tensor->set_raw_data(indices.data(), indices.size() * sizeof(I)); + } else { + for (auto idx : indices) { + indices_tensor->add_int32_data(static_cast(idx)); // indices are stored in int32_data for types < int32 + } + } + } else if constexpr (std::is_same_v) { + indices_tensor->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT16); + if (raw_data_indices) { + indices_tensor->set_raw_data(indices.data(), indices.size() * sizeof(I)); + } else { + for (auto idx : indices) { + indices_tensor->add_int32_data(static_cast(idx)); + } + } + } else if constexpr (std::is_same_v) { + indices_tensor->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT32); + if (raw_data_indices) { + indices_tensor->set_raw_data(indices.data(), indices.size() * sizeof(I)); + } else { + for (auto idx : indices) { + indices_tensor->add_int32_data(idx); + } + } + } else if constexpr (std::is_same_v) { + indices_tensor->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT64); + if (raw_data_indices) { + indices_tensor->set_raw_data(indices.data(), indices.size() * sizeof(I)); + } else { + for (auto idx : indices) { + indices_tensor->add_int64_data(idx); + } + } + } + for (auto dim : indices_shape) { + indices_tensor->add_dims(dim); + } + + ONNX_NAMESPACE::TensorProto dense_proto; + std::filesystem::path model_path; // empty path + ASSERT_STATUS_OK(utils::SparseTensorProtoToDenseTensorProto(sparse_proto, model_path, dense_proto)); + + // Verify dense proto + ASSERT_EQ(dense_proto.dims_size(), dense_shape.size()); + for (size_t i = 0; i < (size_t)dense_shape.size(); ++i) { + ASSERT_EQ(dense_proto.dims(static_cast(i)), dense_shape[i]); + } + + std::vector unpacked_data(expected_dense_data.size()); + ASSERT_STATUS_OK(utils::UnpackTensor(dense_proto, model_path, unpacked_data.data(), unpacked_data.size())); + + EXPECT_EQ(unpacked_data, expected_dense_data); +} + +TEST(SparseTensorConversionTests, SparseTensorProtoToDense_Rank1Indices64) { + // Dense Shape: [2, 2] -> 4 elements + // Indices: [0, 3] (linear) + // Values: [1.0, 2.0] + // Expected: [1.0, 0.0, 0.0, 2.0] + std::vector dense_shape = {2, 2}; + std::vector values = {1.0f, 2.0f}; + std::vector indices = {0, 3}; + std::vector indices_shape = {2}; // [NNZ] + std::vector expected = {1.0f, 0.0f, 0.0f, 2.0f}; + + TestSparseToDenseConversion(dense_shape, values, indices, indices_shape, false, expected); + TestSparseToDenseConversion(dense_shape, values, indices, indices_shape, true, expected); +} + +TEST(SparseTensorConversionTests, SparseTensorProtoToDense_Rank1Indices32) { + // Dense Shape: [2, 2] -> 4 elements + // Indices: [0, 3] (linear) + // Values: [1.0, 2.0] + // Expected: [1.0, 0.0, 0.0, 2.0] + std::vector dense_shape = {2, 2}; + std::vector values = {1.0f, 2.0f}; + std::vector indices = {0, 3}; + std::vector indices_shape = {2}; // [NNZ] + std::vector expected = {1.0f, 0.0f, 0.0f, 2.0f}; + + TestSparseToDenseConversion(dense_shape, values, indices, indices_shape, false, expected); + TestSparseToDenseConversion(dense_shape, values, indices, indices_shape, true, expected); +} + +TEST(SparseTensorConversionTests, SparseTensorProtoToDense_Rank1Indices16) { + // Dense Shape: [2, 2] -> 4 elements + // Indices: [0, 3] (linear) + // Values: [1.0, 2.0] + // Expected: [1.0, 0.0, 0.0, 2.0] + std::vector dense_shape = {2, 2}; + std::vector values = {1.0f, 2.0f}; + std::vector indices = {0, 3}; + std::vector indices_shape = {2}; // [NNZ] + std::vector expected = {1.0f, 0.0f, 0.0f, 2.0f}; + + TestSparseToDenseConversion(dense_shape, values, indices, indices_shape, false, expected); + TestSparseToDenseConversion(dense_shape, values, indices, indices_shape, true, expected); +} + +TEST(SparseTensorConversionTests, SparseTensorProtoToDense_Rank1Indices8) { + // Dense Shape: [2, 2] -> 4 elements + // Indices: [0, 3] (linear) + // Values: [1.0, 2.0] + // Expected: [1.0, 0.0, 0.0, 2.0] + std::vector dense_shape = {2, 2}; + std::vector values = {1.0f, 2.0f}; + std::vector indices = {0, 3}; + std::vector indices_shape = {2}; // [NNZ] + std::vector expected = {1.0f, 0.0f, 0.0f, 2.0f}; + + TestSparseToDenseConversion(dense_shape, values, indices, indices_shape, false, expected); + TestSparseToDenseConversion(dense_shape, values, indices, indices_shape, true, expected); +} + +TEST(SparseTensorConversionTests, SparseTensorProtoToDense_Rank2Indices_COO) { + // Dense Shape: [3, 3] -> 9 elements + // Indices: [[0, 0], [1, 1], [2, 2]] -> flattened: 0,0, 1,1, 2,2 + // Shape: [3, 2] (NNZ=3, Rank=2) + // Values: [10, 20, 30] + // Expected: [10, 0, 0, 0, 20, 0, 0, 0, 30] + std::vector dense_shape = {3, 3}; + std::vector values = {10, 20, 30}; + std::vector indices = {0, 0, 1, 1, 2, 2}; + std::vector indices_shape = {3, 2}; + std::vector expected = { + 10, 0, 0, + 0, 20, 0, + 0, 0, 30}; + + TestSparseToDenseConversion(dense_shape, values, indices, indices_shape, false, expected); + TestSparseToDenseConversion(dense_shape, values, indices, indices_shape, true, expected); +} + +TEST(SparseTensorConversionTests, SparseTensorProtoToDense_OutOfBounds_Rank1) { + // Dense size 4 + // Index 5 -> Out of bounds + ONNX_NAMESPACE::SparseTensorProto sparse; + sparse.mutable_values()->set_name("test_tensor"); + sparse.add_dims(4); + + auto* val = sparse.mutable_values(); + val->add_dims(1); + val->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + val->add_float_data(1.0f); + + auto* ind = sparse.mutable_indices(); + ind->add_dims(1); + ind->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT64); + ind->add_int64_data(5); // Out of bounds + + ONNX_NAMESPACE::TensorProto dense; + auto status = utils::SparseTensorProtoToDenseTensorProto(sparse, {}, dense); + EXPECT_FALSE(status.IsOK()); + EXPECT_THAT(status.ErrorMessage(), testing::HasSubstr("Sparse tensor: test_tensor index is out of bounds")); +} + +TEST(SparseTensorConversionTests, SparseTensorProtoToDense_OutOfBounds_Rank2) { + // Dense Shape [2, 2] -> linear 0..3 + // Index [2, 0] -> linear 4 -> Out of bounds + ONNX_NAMESPACE::SparseTensorProto sparse; + sparse.mutable_values()->set_name("test_tensor"); + sparse.add_dims(2); + sparse.add_dims(2); + + auto* val = sparse.mutable_values(); + val->add_dims(1); + val->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + val->add_float_data(1.0f); + + auto* ind = sparse.mutable_indices(); + ind->add_dims(1); // NNZ=1 + ind->add_dims(2); // Rank=2 + ind->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT64); + ind->add_int64_data(2); + ind->add_int64_data(0); + + ONNX_NAMESPACE::TensorProto dense; + auto status = utils::SparseTensorProtoToDenseTensorProto(sparse, {}, dense); + EXPECT_FALSE(status.IsOK()); + EXPECT_THAT(status.ErrorMessage(), testing::HasSubstr("Sparse tensor: test_tensor index is out of bounds")); +} + +TEST(SparseTensorConversionTests, SparseTensorProtoToDense_OutOfBounds_Rank2_Dim1) { + // Dense Shape [2, 2] + // Index [0, 2] -> 2 is out of bounds for the 2nd dimension (size 2) + ONNX_NAMESPACE::SparseTensorProto sparse; + sparse.mutable_values()->set_name("test_tensor_dim1_oob"); + sparse.add_dims(2); + sparse.add_dims(2); + + auto* val = sparse.mutable_values(); + val->add_dims(1); + val->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + val->add_float_data(1.0f); + + auto* ind = sparse.mutable_indices(); + ind->add_dims(1); // NNZ=1 + ind->add_dims(2); // Rank=2 + ind->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT64); + ind->add_int64_data(0); + ind->add_int64_data(2); // Out of bounds for dim 1 + + ONNX_NAMESPACE::TensorProto dense; + auto status = utils::SparseTensorProtoToDenseTensorProto(sparse, {}, dense); + EXPECT_FALSE(status.IsOK()); + EXPECT_THAT(status.ErrorMessage(), testing::HasSubstr("Sparse tensor: test_tensor_dim1_oob index is out of bounds")); +} + +TEST(SparseTensorConversionTests, SparseTensorProtoToDense_InvalidValuesRank) { + ONNX_NAMESPACE::SparseTensorProto sparse; + sparse.mutable_values()->set_name("test_tensor"); + sparse.add_dims(10); + + auto* val = sparse.mutable_values(); + // Set values rank to 2 (invalid) + val->add_dims(1); + val->add_dims(1); + val->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + val->add_float_data(1.0f); + + auto* ind = sparse.mutable_indices(); + ind->add_dims(1); + ind->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT64); + ind->add_int64_data(0); + + ONNX_NAMESPACE::TensorProto dense; + auto status = utils::SparseTensorProtoToDenseTensorProto(sparse, {}, dense); + EXPECT_FALSE(status.IsOK()); + EXPECT_THAT(status.ErrorMessage(), testing::HasSubstr("Sparse tensor: test_tensor values should be rank 1")); +} + +TEST(SparseTensorConversionTests, SparseTensorProtoToDense_NegativeValuesShape) { + ONNX_NAMESPACE::SparseTensorProto sparse; + sparse.mutable_values()->set_name("test_tensor"); + sparse.add_dims(10); // Dense shape + + auto* val = sparse.mutable_values(); + val->add_dims(-5); // Negative dimension in values + val->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + val->add_float_data(1.0f); + + auto* ind = sparse.mutable_indices(); + ind->add_dims(1); + ind->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT64); + ind->add_int64_data(0); + + ONNX_NAMESPACE::TensorProto dense; + auto status = utils::SparseTensorProtoToDenseTensorProto(sparse, {}, dense); + EXPECT_FALSE(status.IsOK()); + EXPECT_THAT(status.ErrorMessage(), testing::HasSubstr("Sparse tensor: test_tensor tensor dims expected to be non-negative")); +} + +TEST(SparseTensorConversionTests, SparseTensorProtoToDense_NegativeDenseShape) { + ONNX_NAMESPACE::SparseTensorProto sparse; + sparse.mutable_values()->set_name("test_tensor"); + sparse.add_dims(10); + sparse.add_dims(-2); // Negative dimension in dense shape + + auto* val = sparse.mutable_values(); + val->add_dims(1); + val->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + val->add_float_data(1.0f); + + auto* ind = sparse.mutable_indices(); + ind->add_dims(1); + ind->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT64); + ind->add_int64_data(0); + + ONNX_NAMESPACE::TensorProto dense; + auto status = utils::SparseTensorProtoToDenseTensorProto(sparse, {}, dense); + EXPECT_FALSE(status.IsOK()); + EXPECT_THAT(status.ErrorMessage(), testing::HasSubstr("Sparse tensor: test_tensor dense dims expected to be non-negative")); +} + +TEST(SparseTensorConversionTests, SparseTensorProtoToDense_InvalidValuesRank_Zero) { + ONNX_NAMESPACE::SparseTensorProto sparse; + sparse.mutable_values()->set_name("test_tensor_val_rank_0"); + sparse.add_dims(10); + + auto* val = sparse.mutable_values(); + // No dims added -> Rank 0 (Scalar) + val->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + val->add_float_data(1.0f); + + auto* ind = sparse.mutable_indices(); + ind->add_dims(1); + ind->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT64); + ind->add_int64_data(0); + + ONNX_NAMESPACE::TensorProto dense; + auto status = utils::SparseTensorProtoToDenseTensorProto(sparse, {}, dense); + EXPECT_FALSE(status.IsOK()); + EXPECT_THAT(status.ErrorMessage(), testing::HasSubstr("Sparse tensor: test_tensor_val_rank_0 values should be rank 1")); +} + +TEST(SparseTensorConversionTests, SparseTensorProtoToDense_ValuesSizeMismatch) { + // Case where the actual data in 'values' doesn't match the dimension specified in 'values' + ONNX_NAMESPACE::SparseTensorProto sparse; + sparse.mutable_values()->set_name("test_tensor_val_size_mismatch"); + sparse.add_dims(10); + + auto* val = sparse.mutable_values(); + val->add_dims(2); // Claiming 2 elements + val->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + val->add_float_data(1.0f); + // Only added 1 element, this should fail during UnpackInitializerData or subsequent checks depending on where it's caught + // Note: UnpackTensor checks if size matches. + + auto* ind = sparse.mutable_indices(); + ind->add_dims(2); + ind->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT64); + ind->add_int64_data(0); + ind->add_int64_data(1); + + ONNX_NAMESPACE::TensorProto dense; + auto status = utils::SparseTensorProtoToDenseTensorProto(sparse, {}, dense); + EXPECT_FALSE(status.IsOK()); + // The error comes from UnpackTensor usually + EXPECT_THAT(status.ErrorMessage(), testing::HasSubstr("data size")); +} + +TEST(SparseTensorConversionTests, SparseTensorProtoToDense_ValuesSizeMismatch_RawData) { + // Case where raw data size doesn't match the shape size * element size + ONNX_NAMESPACE::SparseTensorProto sparse; + sparse.mutable_values()->set_name("test_tensor_val_size_mismatch_raw"); + sparse.add_dims(10); + + auto* val = sparse.mutable_values(); + val->add_dims(2); // Claiming 2 elements + val->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + + // 1 float is 4 bytes. We provide 4 bytes, but claim 2 elements (8 bytes needed). + float raw_val = 1.0f; + val->set_raw_data(&raw_val, sizeof(float)); + + auto* ind = sparse.mutable_indices(); + ind->add_dims(2); + ind->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_INT64); + ind->add_int64_data(0); + ind->add_int64_data(1); + + ONNX_NAMESPACE::TensorProto dense; + auto status = utils::SparseTensorProtoToDenseTensorProto(sparse, {}, dense); + EXPECT_FALSE(status.IsOK()); + EXPECT_THAT(status.ErrorMessage(), testing::HasSubstr("values data size does not match expected")); +} + +#endif // !defined(DISABLE_SPARSE_TENSORS) } // namespace test } // namespace onnxruntime diff --git a/tools/ci_build/github/azure-pipelines/templates/publish-symbolrequestprod-api.yml b/tools/ci_build/github/azure-pipelines/templates/publish-symbolrequestprod-api.yml index 9f0230c4b1141..e7a4975122784 100644 --- a/tools/ci_build/github/azure-pipelines/templates/publish-symbolrequestprod-api.yml +++ b/tools/ci_build/github/azure-pipelines/templates/publish-symbolrequestprod-api.yml @@ -52,7 +52,6 @@ steps: inputs: azureSubscription: ${{ parameters.subscription }} azurePowerShellVersion: LatestVersion - pwsh: true ScriptType: InlineScript Inline: | # Part 1: Generate an Azure Token