Skip to content

Commit 70dcc0e

Browse files
authored
Improve error reporting for pre-allocated outputs with a wrong shape (microsoft#28481)
This pull request improves the handling of pre-allocated output buffers in ONNX Runtime, especially for models with dynamic output shapes. The changes ensure that when a user provides an output buffer whose shape does not match the computed output shape, the library returns a clear error message. Additionally, the error handling and testing around this scenario are strengthened. The most important changes are: **Pre-allocated Output Buffer Shape Validation:** * Enhanced the logic in `IExecutionFrame::GetOrCreateNodeOutputMLValue` to check if the shape of a pre-allocated output OrtValue matches the computed output shape. If there is a mismatch (typically due to dynamic shapes), the code now returns an explicit `INVALID_ARGUMENT` error with a detailed message, guiding the user to fix their usage. **API and Error Handling Improvements:** * Updated `OpKernelContext::OutputMLValue` to throw an exception with the detailed error message if output OrtValue allocation fails, ensuring that shape mismatches are surfaced clearly to the caller. * Added a catch block for `OnnxRuntimeException` in `sequential_executor.cc` to convert exceptions into proper `Status` objects, improving robustness and error propagation. **Testing and Regression Coverage:** * Added a comprehensive regression test (`ExecutionFrameTestInit.FetchWithMismatchedDynamicShapes`) to verify correct handling of pre-allocated outputs with mismatched shapes, including both error and success cases. This test covers scenarios where output buffers are reused across runs with different dynamic shapes, ensuring the new logic works as intended.
1 parent 04f7440 commit 70dcc0e

4 files changed

Lines changed: 188 additions & 9 deletions

File tree

onnxruntime/core/framework/execution_frame.cc

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,19 +154,58 @@ Status IExecutionFrame::GetOrCreateNodeOutputMLValue(const int output_index, int
154154
p_ort_value = &all_values_[ort_value_idx];
155155

156156
if (p_ort_value->IsAllocated()) {
157-
// already allocated. verify shape matches if tensor.
157+
// The OrtValue at this index is already allocated. This happens when the caller provides
158+
// a pre-allocated OrtValue as an output for Run(). IExecutionFrame::Init() populates
159+
// all_values_ with caller-provided outputs (fetches) before any kernel executes.
160+
// Each NodeArg in an ONNX graph has exactly one producer, so at this point no kernel
161+
// in the current run could have written here — the only source is a value placed
162+
// during Init().
163+
//
164+
// When the shapes match, we reuse the caller's buffer (zero-cost for repeated runs
165+
// with the same shapes).
166+
//
167+
// When they differ, it means the caller supplied an output OrtValue whose shape does
168+
// not match what the kernel computed for this run. This typically happens when the
169+
// caller reuses the output OrtValue array across Run() calls with different input
170+
// shapes on a model with dynamic dimensions. The caller should either supply
171+
// unallocated output OrtValues or ensure the pre-allocated shape matches.
172+
//
173+
// Pre-run validation (ValidateInputsOutputs in inference_session.cc) catches
174+
// structural mismatches (element type, rank, fixed dimensions) before execution
175+
// begins. Only dynamic dimension differences reach this point, since the actual
176+
// shape is only known once the kernel computes it.
177+
bool shape_matched = true;
178+
158179
if (p_ort_value->IsTensor()) {
180+
ORT_RETURN_IF_NOT(shape != nullptr, "shape must not be null for tensor output that is already allocated");
159181
const Tensor& tensor = p_ort_value->Get<Tensor>();
160-
ORT_ENFORCE(shape && tensor.Shape() == *shape,
161-
"OrtValue shape verification failed. Current shape:", tensor.Shape(),
162-
" Requested shape:", shape ? shape->ToString() : "null");
182+
shape_matched = (tensor.Shape() == *shape);
163183
} else if (p_ort_value->IsSparseTensor()) {
164184
#if !defined(DISABLE_SPARSE_TENSORS)
185+
ORT_RETURN_IF_NOT(shape != nullptr, "shape must not be null for sparse tensor output that is already allocated");
165186
const SparseTensor& sp_tensor = p_ort_value->Get<SparseTensor>();
166-
ORT_ENFORCE(shape && sp_tensor.DenseShape() == *shape,
167-
"OrtValue shape verification failed. Current shape:", sp_tensor.DenseShape(),
168-
" Requested shape:", shape ? shape->ToString() : "null");
187+
shape_matched = (sp_tensor.DenseShape() == *shape);
188+
#endif
189+
}
190+
191+
if (!shape_matched) {
192+
const TensorShape& existing_shape = p_ort_value->IsTensor()
193+
? p_ort_value->Get<Tensor>().Shape()
194+
#if !defined(DISABLE_SPARSE_TENSORS)
195+
: p_ort_value->Get<SparseTensor>().DenseShape();
196+
#else
197+
: *shape; // unreachable, but satisfies compiler
169198
#endif
199+
return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT,
200+
"The output OrtValue provided for output '",
201+
node.OutputDefs()[output_index]->Name(),
202+
"' of node '", node.Name(),
203+
"' (", node.OpType(), ") has shape ", existing_shape,
204+
" but the computed output shape for this run is ", *shape,
205+
". When calling Run() with pre-allocated output OrtValues on a model "
206+
"with dynamic output shapes, either supply unallocated output OrtValues "
207+
"or ensure the pre-allocated shapes match the expected output shapes "
208+
"for each run.");
170209
}
171210
} else {
172211
// shape is nullptr for traditional ML output values

onnxruntime/core/framework/op_kernel.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ OrtValue* OpKernelContext::OutputMLValue(int index, const TensorShape& shape) {
8080

8181
OrtValue* p_ml_value = nullptr;
8282
Status status = execution_frame_->GetOrCreateNodeOutputMLValue(index, GetOutputArgIndex(index), &shape, p_ml_value, kernel_->Node());
83-
ORT_ENFORCE(status.IsOK(), status.ErrorMessage());
83+
ORT_THROW_IF_ERROR(status);
8484
return p_ml_value;
8585
}
8686

@@ -126,7 +126,7 @@ OrtValue* OpKernelContext::GetOrCreateOutputMLValue(int index) {
126126
auto output_arg_index = GetOutputArgIndex(index);
127127
OrtValue* value = nullptr;
128128
auto status = execution_frame_->GetOrCreateNodeOutputMLValue(index, output_arg_index, nullptr, value, kernel_->Node());
129-
ORT_ENFORCE(status.IsOK(), status.ErrorMessage());
129+
ORT_THROW_IF_ERROR(status);
130130
return value;
131131
}
132132

onnxruntime/core/framework/sequential_executor.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,11 @@ onnxruntime::Status ExecuteKernel(StreamExecutionContext& ctx,
594594
#endif
595595
#endif
596596
}
597+
ORT_CATCH(const OnnxRuntimeException& ort_ex) {
598+
ORT_HANDLE_EXCEPTION([&]() {
599+
status = Status(ort_ex.Category(), ort_ex.Code(), ort_ex.what());
600+
});
601+
}
597602
ORT_CATCH(const std::exception& ex) {
598603
ORT_HANDLE_EXCEPTION([&]() {
599604
status = ORT_MAKE_STATUS(ONNXRUNTIME, RUNTIME_EXCEPTION, ex.what());

onnxruntime/test/framework/execution_frame_test.cc

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,141 @@ TEST(ExecutionFrameTestInit, InitializerAsOutput) {
559559
}
560560
}
561561

562+
// Test that when a caller provides pre-allocated output OrtValues whose shapes don't match
563+
// the computed output shapes, ORT returns a clear INVALID_ARGUMENT error with an actionable
564+
// message. This is the scenario described in GitHub issue #28359.
565+
//
566+
// The caller's copy of the old output remains valid (OrtValue uses shared_ptr internally).
567+
// Pre-run validation (ValidateInputsOutputs) catches structural mismatches (wrong type, rank,
568+
// fixed dims). What remains at kernel execution time is purely dynamic dimension differences.
569+
TEST(ExecutionFrameTestInit, FetchWithMismatchedDynamicShapes) {
570+
// Regression test for https://github.com/microsoft/onnxruntime/issues/28359
571+
// Verifies that Run() returns a clear INVALID_ARGUMENT error when the caller provides
572+
// a pre-allocated output OrtValue whose shape doesn't match the kernel's computed shape,
573+
// and that pre-allocated outputs with matching shapes are used correctly.
574+
SessionOptions so;
575+
so.enable_mem_pattern = true;
576+
577+
InferenceSession session(so, GetEnvironment());
578+
579+
// Use Relu which preserves shape: output shape == input shape
580+
onnxruntime::Model model("dynamic_shape_test", false, ModelMetaData(), PathString(),
581+
IOnnxRuntimeOpSchemaRegistryList(),
582+
{{kOnnxDomain, 12}}, {}, DefaultLoggingManager().DefaultLogger());
583+
auto& graph = model.MainGraph();
584+
585+
TypeProto float_tensor;
586+
float_tensor.mutable_tensor_type()->set_elem_type(TensorProto_DataType_FLOAT);
587+
// dynamic shape: {N, M}
588+
auto* input_shape = float_tensor.mutable_tensor_type()->mutable_shape();
589+
input_shape->add_dim()->set_dim_param("N");
590+
input_shape->add_dim()->set_dim_param("M");
591+
592+
auto& input_arg = graph.GetOrCreateNodeArg("X", &float_tensor);
593+
auto& output_arg = graph.GetOrCreateNodeArg("Y", &float_tensor);
594+
graph.AddNode("relu", "Relu", "relu", {&input_arg}, {&output_arg});
595+
graph.SetInputs({&input_arg});
596+
graph.SetOutputs({&output_arg});
597+
ASSERT_STATUS_OK(graph.Resolve());
598+
599+
std::string serialized;
600+
ASSERT_TRUE(model.ToProto().SerializeToString(&serialized));
601+
std::istringstream model_stream(serialized);
602+
ASSERT_STATUS_OK(session.Load(model_stream));
603+
ASSERT_STATUS_OK(session.Initialize());
604+
605+
RunOptions ro;
606+
auto allocator = test::AllocatorManager::Instance().GetAllocator(CPU);
607+
608+
// Run 1: pre-allocate the output buffer with the correct shape {2, 3}.
609+
// The kernel should write into this buffer directly.
610+
std::vector<float> input_data_1(6, 1.0f);
611+
OrtValue input_1;
612+
Tensor::InitOrtValue(DataTypeImpl::GetType<float>(), TensorShape({2, 3}), input_data_1.data(),
613+
allocator->Info(), input_1);
614+
615+
OrtValue preallocated_output;
616+
Tensor::InitOrtValue(DataTypeImpl::GetType<float>(), TensorShape({2, 3}), allocator, preallocated_output);
617+
const void* preallocated_buffer = preallocated_output.Get<Tensor>().DataRaw();
618+
619+
std::vector<OrtValue> results = {preallocated_output};
620+
ASSERT_STATUS_OK(session.Run(ro,
621+
AsSpan({std::string("X")}), AsSpan({input_1}),
622+
AsSpan({std::string("Y")}), &results, nullptr));
623+
624+
ASSERT_EQ(results.size(), 1u);
625+
ASSERT_TRUE(results[0].IsTensor());
626+
EXPECT_EQ(results[0].Get<Tensor>().Shape(), TensorShape({2, 3}));
627+
628+
// The output should be in the pre-allocated buffer since shapes matched
629+
EXPECT_EQ(results[0].Get<Tensor>().DataRaw(), preallocated_buffer);
630+
631+
// Verify Run 1 output correctness (Relu of all 1.0f = all 1.0f)
632+
auto run1_data = results[0].Get<Tensor>().DataAsSpan<float>();
633+
for (float v : run1_data) {
634+
EXPECT_EQ(v, 1.0f);
635+
}
636+
637+
// Run 2: different input shape {4, 5}, but results still contains the {2,3} output
638+
// from Run 1. Run() should fail with INVALID_ARGUMENT because the pre-allocated
639+
// output shape doesn't match the computed output shape.
640+
std::vector<float> input_data_2(20, 2.0f);
641+
OrtValue input_2;
642+
Tensor::InitOrtValue(DataTypeImpl::GetType<float>(), TensorShape({4, 5}), input_data_2.data(),
643+
allocator->Info(), input_2);
644+
645+
auto status = session.Run(ro,
646+
AsSpan({std::string("X")}), AsSpan({input_2}),
647+
AsSpan({std::string("Y")}), &results, nullptr);
648+
649+
ASSERT_FALSE(status.IsOK());
650+
ASSERT_EQ(status.Code(), common::StatusCode::INVALID_ARGUMENT);
651+
EXPECT_THAT(status.ErrorMessage(), ::testing::HasSubstr("pre-allocated"));
652+
EXPECT_THAT(status.ErrorMessage(), ::testing::HasSubstr("{2,3}"));
653+
EXPECT_THAT(status.ErrorMessage(), ::testing::HasSubstr("{4,5}"));
654+
655+
// Run 3: clear the output and retry — should succeed
656+
results = {};
657+
ASSERT_STATUS_OK(session.Run(ro,
658+
AsSpan({std::string("X")}), AsSpan({input_2}),
659+
AsSpan({std::string("Y")}), &results, nullptr));
660+
661+
ASSERT_EQ(results.size(), 1u);
662+
ASSERT_TRUE(results[0].IsTensor());
663+
EXPECT_EQ(results[0].Get<Tensor>().Shape(), TensorShape({4, 5}));
664+
665+
// Verify Run 3 output correctness (Relu of all 2.0f = all 2.0f)
666+
auto run3_data = results[0].Get<Tensor>().DataAsSpan<float>();
667+
for (float v : run3_data) {
668+
EXPECT_EQ(v, 2.0f);
669+
}
670+
671+
// Run 4: same shape {4, 5} with results carrying over — shapes match, should reuse
672+
const void* run3_buffer = results[0].Get<Tensor>().DataRaw();
673+
674+
std::vector<float> input_data_4(20, 3.0f);
675+
OrtValue input_4;
676+
Tensor::InitOrtValue(DataTypeImpl::GetType<float>(), TensorShape({4, 5}), input_data_4.data(),
677+
allocator->Info(), input_4);
678+
679+
ASSERT_STATUS_OK(session.Run(ro,
680+
AsSpan({std::string("X")}), AsSpan({input_4}),
681+
AsSpan({std::string("Y")}), &results, nullptr));
682+
683+
ASSERT_EQ(results.size(), 1u);
684+
ASSERT_TRUE(results[0].IsTensor());
685+
EXPECT_EQ(results[0].Get<Tensor>().Shape(), TensorShape({4, 5}));
686+
687+
// Same shape — buffer should be reused
688+
EXPECT_EQ(results[0].Get<Tensor>().DataRaw(), run3_buffer);
689+
690+
// Verify Run 4 output correctness (Relu of all 3.0f = all 3.0f)
691+
auto run4_data = results[0].Get<Tensor>().DataAsSpan<float>();
692+
for (float v : run4_data) {
693+
EXPECT_EQ(v, 3.0f);
694+
}
695+
}
696+
562697
#if !defined(DISABLE_SPARSE_TENSORS)
563698
TEST(ExecutionFrameTestInit, SparseInitializerAsOutput) {
564699
constexpr std::array<int64_t, 2> dense_shape{3, 3};

0 commit comments

Comments
 (0)