Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
11 changes: 10 additions & 1 deletion onnxruntime/core/framework/tensorprotoutils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,17 @@ Status TensorProtoWithExternalDataToTensorProto(
} else {
// Load the external data into memory
std::vector<uint8_t> unpacked_data;
ORT_RETURN_IF_ERROR(ReadExternalDataForTensor(ten_proto, model_path, unpacked_data));
// ReadExternalDataForTensor expects a directory. Preserve existing behavior for callers that
// already pass a directory, and only use parent_path() when model_path is a confirmed file.
std::filesystem::path external_data_path = model_path;
std::error_code ec;
if (std::filesystem::is_regular_file(model_path, ec)) {
external_data_path = model_path.parent_path();
} else if (ec) {
ec.clear();
}

ORT_RETURN_IF_ERROR(ReadExternalDataForTensor(ten_proto, external_data_path, unpacked_data));
// Set the raw data in the new tensor
onnxruntime::utils::SetRawDataInTensorProto(result, unpacked_data.data(), unpacked_data.size());
}
Expand Down
134 changes: 134 additions & 0 deletions onnxruntime/test/providers/coreml/coreml_basic_test.cc
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#include <cstdint>
#include <filesystem>
#include <fstream>
#include <memory>
#include <vector>

#include "core/common/logging/logging.h"
#include "core/graph/constants.h"
Expand All @@ -17,6 +21,7 @@
#include "test/util/include/current_test_name.h"
#include "test/util/include/default_providers.h"
#include "test/util/include/inference_session_wrapper.h"
#include "test/util/include/temp_dir.h"
#include "test/util/include/test_environment.h"
#include "test/util/include/test_utils.h"
#include "core/graph/onnx_protobuf.h"
Expand Down Expand Up @@ -430,5 +435,134 @@ TEST(CoreMLExecutionProviderTest, TestModelCache) {
TestModelLoad(model_data, MakeCoreMLExecutionProvider(), ExpectedEPNodeAssignment::All);
#endif
}

// Test that CoreML EP can load a model with initializers stored in an external data file.
// Regression test for https://github.com/microsoft/onnxruntime/issues/28005
// The bug was that TensorProtoWithExternalDataToTensorProto passed a model file path
// (e.g. "/path/to/model.onnx") to ReadExternalDataForTensor which expects a directory,
// causing it to construct an invalid path like "/path/to/model.onnx/model.onnx_data".
#if !defined(ORT_MINIMAL_BUILD)
TEST(CoreMLExecutionProviderTest, ExternalDataInitializer) {
// Create a temp directory for the model and external data file
TemporaryDirectory tmp_dir(ORT_TSTR("coreml_external_data_test"));
const auto model_path = std::filesystem::path(tmp_dir.Path()) / ORT_TSTR("model.onnx");
const auto external_data_path = std::filesystem::path(tmp_dir.Path()) / ORT_TSTR("model.onnx_data");

// Write external data file: 6 floats for a {1,1,3,2} initializer
const std::vector<float> initializer_data = {0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f};
{
std::ofstream ofs(external_data_path, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ofs.write(reinterpret_cast<const char*>(initializer_data.data()),
initializer_data.size() * sizeof(float));
ofs.close();
}

// Build a simple model: output = X + initializer (Add op)
{
ONNX_NAMESPACE::ModelProto model_proto;
model_proto.set_ir_version(ONNX_NAMESPACE::IR_VERSION);
auto* opset = model_proto.add_opset_import();
opset->set_domain("");
opset->set_version(13);

auto* graph_proto = model_proto.mutable_graph();
graph_proto->set_name("test_external_data");

// Input X: {1,1,3,2} float tensor
auto* input = graph_proto->add_input();
input->set_name("X");
auto* input_type = input->mutable_type()->mutable_tensor_type();
input_type->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT);
auto* input_shape = input_type->mutable_shape();
input_shape->add_dim()->set_dim_value(1);
input_shape->add_dim()->set_dim_value(1);
input_shape->add_dim()->set_dim_value(3);
input_shape->add_dim()->set_dim_value(2);

// Output Y: {1,1,3,2} float tensor
auto* output = graph_proto->add_output();
output->set_name("Y");
auto* output_type = output->mutable_type()->mutable_tensor_type();
output_type->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT);
auto* output_shape = output_type->mutable_shape();
output_shape->add_dim()->set_dim_value(1);
output_shape->add_dim()->set_dim_value(1);
output_shape->add_dim()->set_dim_value(3);
output_shape->add_dim()->set_dim_value(2);

// Initializer W with external data
auto* initializer = graph_proto->add_initializer();
initializer->set_name("W");
initializer->set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT);
initializer->add_dims(1);
initializer->add_dims(1);
initializer->add_dims(3);
initializer->add_dims(2);
initializer->set_data_location(ONNX_NAMESPACE::TensorProto_DataLocation_EXTERNAL);

auto* ext_location = initializer->add_external_data();
ext_location->set_key("location");
ext_location->set_value("model.onnx_data");
auto* ext_offset = initializer->add_external_data();
ext_offset->set_key("offset");
ext_offset->set_value("0");
auto* ext_length = initializer->add_external_data();
ext_length->set_key("length");
ext_length->set_value(std::to_string(initializer_data.size() * sizeof(float)));

// Add node: Y = X + W
auto* node = graph_proto->add_node();
node->set_op_type("Add");
node->add_input("X");
node->add_input("W");
node->add_output("Y");

// Save model
std::ofstream ofs(model_path, std::ios::binary);
ASSERT_TRUE(ofs.is_open());
ASSERT_TRUE(model_proto.SerializeToOstream(&ofs));
ofs.close();
}

// Input data
std::vector<int64_t> dims = {1, 1, 3, 2};
std::vector<float> input_data = {1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f};
OrtValue ml_value_x;
AllocatorPtr allocator = CPUAllocator::DefaultInstance();
CreateMLValue<float>(allocator, dims, input_data, &ml_value_x);

NameMLValMap feeds;
feeds.insert(std::make_pair("X", ml_value_x));

RunOptions run_options;
run_options.run_tag = "ExternalDataInitializer";
std::vector<std::string> output_names = {"Y"};

// Load the model from a file path (not from memory) with the CoreML EP.
// This is the scenario that triggers the bug: CoreML EP must resolve external data
// relative to the model file's directory, not treat the model path as a directory.
SessionOptions so;
so.session_logid = "ExternalDataInitializer";
InferenceSessionWrapper session{so, GetEnvironment()};
ASSERT_STATUS_OK(session.RegisterExecutionProvider(MakeCoreMLExecutionProvider()));
ASSERT_STATUS_OK(session.Load(model_path.native()));
Comment thread
skottmckay marked this conversation as resolved.
ASSERT_STATUS_OK(session.Initialize());

std::vector<OrtValue> fetches;
ASSERT_STATUS_OK(session.Run(run_options, feeds, output_names, &fetches));
Comment thread
tianleiwu marked this conversation as resolved.

// Verify the output: Y = X + W = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6}
ASSERT_EQ(fetches.size(), 1u);
const auto& output_tensor = fetches[0].Get<Tensor>();
auto output_data = output_tensor.DataAsSpan<float>();
ASSERT_EQ(static_cast<size_t>(output_data.size()), input_data.size());
for (size_t i = 0; i < input_data.size(); ++i) {
EXPECT_NEAR(output_data[i], input_data[i] + initializer_data[i], 1e-5f)
<< "Mismatch at index " << i;
}
}
#endif // !(ORT_MINIMAL_BUILD)

} // namespace test
} // namespace onnxruntime
Loading