diff --git a/onnxruntime/core/framework/execution_frame.cc b/onnxruntime/core/framework/execution_frame.cc index 3256d207ad..ebd4d5d124 100644 --- a/onnxruntime/core/framework/execution_frame.cc +++ b/onnxruntime/core/framework/execution_frame.cc @@ -20,19 +20,14 @@ using namespace onnxruntime::common; namespace onnxruntime { -IExecutionFrame::IExecutionFrame(const std::vector& feed_mlvalue_idxs, const std::vector& feeds, - const std::unordered_map& initializers, - const std::vector& fetch_mlvalue_idxs, const std::vector& fetches, - const OrtValueNameIdxMap& ort_value_idx_map, const NodeIndexInfo& node_index_info) +IExecutionFrame::IExecutionFrame(const OrtValueNameIdxMap& ort_value_idx_map, + const NodeIndexInfo& node_index_info, + const std::vector& fetch_mlvalue_idxs) : node_index_info_(node_index_info), all_values_size_(static_cast(ort_value_idx_map.MaxIdx()) + 1), fetch_mlvalue_idxs_(fetch_mlvalue_idxs) { - ORT_ENFORCE(feeds.size() == feed_mlvalue_idxs.size()); - ORT_ENFORCE(fetches.empty() || fetches.size() == fetch_mlvalue_idxs_.size()); ORT_ENFORCE(node_index_info_.GetMaxMLValueIdx() == ort_value_idx_map.MaxIdx(), "node_index_info and ort_value_idx_map are out of sync and cannot be used"); - - Init(feed_mlvalue_idxs, feeds, initializers, fetches); } IExecutionFrame::~IExecutionFrame() = default; @@ -109,6 +104,9 @@ int IExecutionFrame::GetNodeIdxToMLValueIdx(int index) const { void IExecutionFrame::Init(const std::vector& feed_mlvalue_idxs, const std::vector& feeds, const std::unordered_map& initializers, const std::vector& fetches) { + ORT_ENFORCE(feeds.size() == feed_mlvalue_idxs.size()); + ORT_ENFORCE(fetches.empty() || fetches.size() == fetch_mlvalue_idxs_.size()); + // 1. resize the all_value_ vector all_values_.resize(all_values_size_); @@ -123,15 +121,44 @@ void IExecutionFrame::Init(const std::vector& feed_mlvalue_idxs, const std: } // 3. handle the weights. - // We do this after the fetches to handle an edge case (possibly dubious) where a Constant is an output. - // The Constant gets lifted to an initializer so there's no Node producing the value as an output during Graph - // execution (i.e. Graph execution won't write the value to all_values_). + // We do this after the fetches to handle an edge case where an initializer is an output. + // e.g. A Constant node gets lifted to an initializer so there's no Node producing the value as an output during + // Graph execution (i.e. Graph execution won't write the value to all_values_). // A non-empty fetches vector will overwrite the actual weight in all_values_[ort_value_idx] if we did this earlier. // This makes the ONNX Constant test (onnx\backend\test\data\node\test_constant) happy as that // involves a graph with a single Constant node. for (const auto& entry : initializers) { int ort_value_index = entry.first; - all_values_[ort_value_index] = entry.second; + + // if the initializer is an output we need to allocate or use a provided fetch buffer and copy the data + // so it can be returned to the caller. + // + // The alternative to handling this as a special case would be to disallow an initializer providing a graph output. + // There's nothing in the ONNX spec that says a graph output must come from a node output though. + // If we took that approach we'd need to: + // - reject a model with an initializer or Constant node (as we convert those to initializers in Graph::Graph) + // that produces a graph output even though it conforms to the ONNX spec + // - update optimizers to not convert something to an initializer that is a graph output + // (e.g. constant folding) + if (IsOutput(ort_value_index)) { + const Tensor& src = entry.second.Get(); // all initializers in ONNX are tensors + OrtValue& dest = all_values_[ort_value_index]; + + if (!dest.IsAllocated()) { + // NOTE: This doesn't need to support ExecutionFrame custom allocators as they only come into play + // for a subgraph with an output of unknown shape that needs to be accumulated by the control flow node. + // If the initializer is providing the output, the shape is known. + AllocatorPtr allocator = GetAllocator(src.Location()); + + auto p_tensor = onnxruntime::make_unique(src.DataType(), src.Shape(), allocator); + auto ml_tensor = DataTypeImpl::GetType(); + dest.Init(p_tensor.release(), ml_tensor, ml_tensor->GetDeleteFunc()); + } + + ORT_THROW_IF_ERROR(CopyTensor(src, *dest.GetMutable())); + } else { + all_values_[ort_value_index] = entry.second; + } } // 4. handle feed in values. these can override initializer values so must be last @@ -171,11 +198,12 @@ ExecutionFrame::ExecutionFrame(const std::vector& feed_mlvalue_idxs, const const std::vector& fetch_mlvalue_idxs, const std::vector& fetches, const std::unordered_map& fetch_allocators, const SessionState& session_state) - : IExecutionFrame(feed_mlvalue_idxs, feeds, session_state.GetInitializedTensors(), fetch_mlvalue_idxs, fetches, - session_state.GetOrtValueNameIdxMap(), session_state.GetNodeIndexInfo()), + : IExecutionFrame(session_state.GetOrtValueNameIdxMap(), session_state.GetNodeIndexInfo(), fetch_mlvalue_idxs), session_state_(session_state), mem_patterns_(nullptr), planner_(nullptr) { + Init(feed_mlvalue_idxs, feeds, session_state.GetInitializedTensors(), fetches); + // map the custom allocators to ort_value_idx entries if (!fetch_allocators.empty()) { for (size_t idx = 0, end = fetch_mlvalue_idxs.size(); idx < end; ++idx) { @@ -232,6 +260,10 @@ ExecutionFrame::ExecutionFrame(const std::vector& feed_mlvalue_idxs, const ExecutionFrame::~ExecutionFrame() = default; +Status ExecutionFrame::CopyTensor(const Tensor& src, Tensor& dest) const { + return session_state_.GetDataTransferMgr().CopyTensor(src, dest); +} + Status ExecutionFrame::AllocateMLValueTensorSelfOwnBuffer(OrtValue& ort_value, int ort_value_index, MLDataType element_type, const OrtMemoryInfo& location, const TensorShape& shape, bool create_fence) { @@ -343,7 +375,7 @@ Status ExecutionFrame::AllocateMLValueTensorPreAllocateBuffer(OrtValue& ort_valu // be generous and use the buffer if it's large enough. log a warning though as it indicates a bad model if (buffer_num_elements >= required_num_elements) { - // View Operator is reusing the buffer bigger than the required size. + // View Operator is reusing the buffer bigger than the required size. // Disabling warning message for now. The op is in the process of being deprecated. #ifndef ENABLE_TRAINING LOGS(session_state_.Logger(), WARNING) << message; @@ -455,13 +487,13 @@ Status ExecutionFrame::AllocateAsPerAllocationPlan(OrtValue& ort_value, int ort_ } case AllocKind::kReuse: { int reuse_mlvalue_index = per_alloc_plan.reused_buffer; - + // In case OrtRunOptions.only_execute_path_to_fetches == true, it is possible that 'reuse_value' // is not allocated (its upstream op is not executed due to the option). - // In this case we need to allocate 'reuse_value' and then let 'ort_value' to reuse it. + // In this case we need to allocate 'reuse_value' and then let 'ort_value' to reuse it. OrtValue& reuse_value = GetMutableMLValue(reuse_mlvalue_index); if (!reuse_value.IsAllocated()) { - ORT_RETURN_IF_ERROR(AllocateAsPerAllocationPlan(reuse_value, reuse_mlvalue_index, shape, nnz)); + ORT_RETURN_IF_ERROR(AllocateAsPerAllocationPlan(reuse_value, reuse_mlvalue_index, shape, nnz)); } ORT_RETURN_IF_ERROR(AllocateMLValueTensorPreAllocateBuffer( ort_value, reuse_mlvalue_index, ml_data_type, alloc_info, *shape, per_alloc_plan.create_fence_if_async)); @@ -497,7 +529,8 @@ AllocatorPtr ExecutionFrame::GetAllocatorImpl(const OrtMemoryInfo& info) const { // This method is not thread safe! // Return S_OK and nullptr if index map to an value that is an unused optional input/output -Status ExecutionFrame::CreateNodeOutputMLValueImpl(OrtValue& ort_value, int ort_value_idx, const TensorShape* shape, size_t nnz) { +Status ExecutionFrame::CreateNodeOutputMLValueImpl(OrtValue& ort_value, int ort_value_idx, + const TensorShape* shape, size_t nnz) { return AllocateAsPerAllocationPlan(ort_value, ort_value_idx, shape, nnz); } diff --git a/onnxruntime/core/framework/execution_frame.h b/onnxruntime/core/framework/execution_frame.h index 08ff2d2026..f0b211994f 100644 --- a/onnxruntime/core/framework/execution_frame.h +++ b/onnxruntime/core/framework/execution_frame.h @@ -25,10 +25,15 @@ class NodeIndexInfo; class IExecutionFrame { protected: - IExecutionFrame(const std::vector& feed_mlvalue_idxs, const std::vector& feeds, - const std::unordered_map& initializers, const std::vector& fetch_mlvalue_idxs, - const std::vector& fetches, const OrtValueNameIdxMap& ort_value_idx_map, - const NodeIndexInfo& node_index_info); + // Derived class must call Init in its ctor. We need to use some of the virtual methods in Init and those aren't + // initialized until the derived class is constructed. + IExecutionFrame(const OrtValueNameIdxMap& ort_value_idx_map, + const NodeIndexInfo& node_index_info, + const std::vector& fetch_mlvalue_idxs); + + void Init(const std::vector& feed_mlvalue_idxs, const std::vector& feeds, + const std::unordered_map& initializers, + const std::vector& fetches); public: virtual ~IExecutionFrame(); @@ -72,10 +77,6 @@ class IExecutionFrame { private: ORT_DISALLOW_COPY_ASSIGNMENT_AND_MOVE(IExecutionFrame); - void Init(const std::vector& feed_mlvalue_idxs, const std::vector& feeds, - const std::unordered_map& initializers, - const std::vector& fetches); - const OrtValue& GetMLValue(int ort_value_index) const { ORT_ENFORCE(ort_value_index >= 0 && static_cast(ort_value_index) < all_values_size_); return all_values_[ort_value_index]; @@ -83,7 +84,10 @@ class IExecutionFrame { virtual AllocatorPtr GetAllocatorImpl(const OrtMemoryInfo& info) const = 0; - virtual Status CreateNodeOutputMLValueImpl(OrtValue& ort_value, int ort_value_idx, const TensorShape* shape, size_t nnz) = 0; + virtual Status CreateNodeOutputMLValueImpl(OrtValue& ort_value, int ort_value_idx, const TensorShape* shape, + size_t nnz) = 0; + + virtual Status CopyTensor(const Tensor& src, Tensor& dest) const = 0; const NodeIndexInfo& node_index_info_; @@ -131,6 +135,7 @@ class ExecutionFrame final : public IExecutionFrame { AllocatorPtr GetAllocatorImpl(const OrtMemoryInfo& info) const override; Status ReleaseMLValueImpl(int ort_value_idx) override; Status CreateNodeOutputMLValueImpl(OrtValue& ort_value, int ort_value_idx, const TensorShape* shape, size_t nnz) override; + Status CopyTensor(const Tensor& src, Tensor& dest) const override; common::Status AllocateAsPerAllocationPlan(OrtValue& ort_value, int ort_value_index, const TensorShape* shape, size_t nnz); diff --git a/onnxruntime/core/optimizer/optimizer_execution_frame.cc b/onnxruntime/core/optimizer/optimizer_execution_frame.cc index e946a88c70..8ff9d325ac 100644 --- a/onnxruntime/core/optimizer/optimizer_execution_frame.cc +++ b/onnxruntime/core/optimizer/optimizer_execution_frame.cc @@ -40,12 +40,12 @@ OptimizerExecutionFrame::Info::Info(const std::vector& nodes, size_t cpu_tensor_length; ORT_RETURN_IF_ERROR(utils::GetSizeInBytesFromTensorProto<0>(tensor_proto, &cpu_tensor_length)); OrtValue ort_value; - const OrtMemoryInfo& info = cpu_execution_provider_->GetAllocator(0, OrtMemTypeDefault)->Info(); std::unique_ptr data(new char[cpu_tensor_length]); std::unique_ptr p_tensor; OrtCallback d; ORT_RETURN_IF_ERROR(utils::TensorProtoToMLValue(Env::Default(), nullptr, tensor_proto, - MemBuffer(data.get(), cpu_tensor_length, info), ort_value, d)); + MemBuffer(data.get(), cpu_tensor_length, allocator_ptr_->Info()), + ort_value, d)); initializers_[idx] = ort_value; buffer_for_initialized_tensors_[idx] = std::move(data); @@ -83,14 +83,19 @@ std::unique_ptr OptimizerExecutionFrame::Info::CreateKernel(cons // For optimizer, probably no need to pass feed_mlvalue_idxs, feeds to initialize IExecutionFrame. // If needed, the parameters of OptimizerExecutionFrame ctor can be changed later. OptimizerExecutionFrame::OptimizerExecutionFrame(const Info& info, const std::vector& fetch_mlvalue_idxs) - : IExecutionFrame(std::vector(), std::vector(), info.GetInitializers(), fetch_mlvalue_idxs, - std::vector(), info.GetMLValueNameIdxMap(), info.GetNodeIndexInfo()), - info_(info) {} + : IExecutionFrame(info.GetMLValueNameIdxMap(), info.GetNodeIndexInfo(), fetch_mlvalue_idxs), + info_(info) { + Init(std::vector(), std::vector(), info.GetInitializers(), std::vector()); +} AllocatorPtr OptimizerExecutionFrame::GetAllocatorImpl(const OrtMemoryInfo& info) const { return info_.GetAllocator(info); } +Status OptimizerExecutionFrame::CopyTensor(const Tensor& src, Tensor& dest) const { + return info_.GetDataTransferManager().CopyTensor(src, dest); +} + // This method is not thread safe! // Return S_OK and nullptr if index map to an value that is an unused optional input/output Status OptimizerExecutionFrame::CreateNodeOutputMLValueImpl(OrtValue& ort_value, int ort_value_idx, diff --git a/onnxruntime/core/optimizer/optimizer_execution_frame.h b/onnxruntime/core/optimizer/optimizer_execution_frame.h index 4fc0c5a0de..944cf4fbe4 100644 --- a/onnxruntime/core/optimizer/optimizer_execution_frame.h +++ b/onnxruntime/core/optimizer/optimizer_execution_frame.h @@ -51,6 +51,8 @@ class OptimizerExecutionFrame final : public IExecutionFrame { std::unique_ptr CreateKernel(const Node* node) const; + const DataTransferManager& GetDataTransferManager() const { return data_transfer_mgr_; } + private: // The optimizer is running on CPU execution provider by default. std::unique_ptr cpu_execution_provider_; @@ -83,6 +85,8 @@ class OptimizerExecutionFrame final : public IExecutionFrame { Status CreateNodeOutputMLValueImpl(OrtValue& ort_value, int ort_value_idx, const TensorShape* shape, size_t nnz) override; + Status CopyTensor(const Tensor& src, Tensor& dest) const override; + const Info& info_; }; diff --git a/onnxruntime/test/framework/execution_frame_test.cc b/onnxruntime/test/framework/execution_frame_test.cc index 41cf044cff..3b61c40542 100644 --- a/onnxruntime/test/framework/execution_frame_test.cc +++ b/onnxruntime/test/framework/execution_frame_test.cc @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#include "core/common/make_unique.h" #include "core/framework/execution_frame.h" #include "core/framework/op_kernel.h" #include "core/framework/session_state.h" @@ -9,6 +10,7 @@ #include "core/session/inference_session.h" #include "test_utils.h" #include "test/test_environment.h" +#include "test/framework/TestAllocatorManager.h" #include "asserts.h" #include "gtest/gtest.h" #include "gmock/gmock.h" @@ -20,17 +22,6 @@ namespace onnxruntime { namespace test { typedef std::vector ArgMap; -std::shared_ptr DummyGraphWithClip() { - auto model = std::make_shared("test", false, DefaultLoggingManager().DefaultLogger()); - onnxruntime::Graph& graph = model->MainGraph(); - TypeProto tensor_float; - tensor_float.mutable_tensor_type()->set_elem_type(TensorProto_DataType_FLOAT); - onnxruntime::NodeArg input_def("X", &tensor_float), output_def("Y", &tensor_float); - - graph.AddNode("node1", "Clip", "clip operator", ArgMap{&input_def}, ArgMap{&output_def}); - return model; -} - std::unique_ptr CreateCPUExecutionProvider() { CPUExecutionProviderInfo info; return onnxruntime::make_unique(info); @@ -69,8 +60,9 @@ TEST_F(ExecutionFrameTest, TensorAllocationTest) { std::unique_ptr p_seq_exec_plan; // TODO below line is for testing only. In production use SequentialPlanner::CreatePlan() SequentialPlannerContext context(ExecutionMode::ORT_SEQUENTIAL); - ASSERT_STATUS_OK(SequentialPlanner::CreatePlan(nullptr, GraphViewer(graph), {}, execution_providers, kernel_registry_manager, - state.GetOrtValueNameIdxMap(), context, p_seq_exec_plan)); + ASSERT_STATUS_OK(SequentialPlanner::CreatePlan(nullptr, GraphViewer(graph), {}, execution_providers, + kernel_registry_manager, state.GetOrtValueNameIdxMap(), context, + p_seq_exec_plan)); state.SetExecutionPlan(std::move(p_seq_exec_plan)); vector outputs; @@ -81,8 +73,9 @@ TEST_F(ExecutionFrameTest, TensorAllocationTest) { TensorShape shape(std::vector{2, 3}); OrtValue& mlvalue0 = *frame.GetMutableNodeInputOrOutputMLValue(start_index); + const auto& memory_info = execution_providers.Get(xp_typ)->GetAllocator(0, OrtMemTypeDefault)->Info(); ASSERT_STATUS_OK(frame.AllocateMLValueTensorSelfOwnBuffer(mlvalue0, start_index, DataTypeImpl::GetType(), - execution_providers.Get(xp_typ)->GetAllocator(0, OrtMemTypeDefault)->Info(), shape)); + memory_info, shape)); OrtValue* p_ml_value = frame.GetMutableNodeInputOrOutputMLValue(0); ASSERT_TRUE(p_ml_value != nullptr); @@ -97,10 +90,10 @@ TEST_F(ExecutionFrameTest, TensorAllocationTest) { TensorShape shape2(std::vector{3, 2}); OrtValue& mlvalue1 = *frame.GetMutableNodeInputOrOutputMLValue(start_index + 1); ASSERT_STATUS_OK(frame.AllocateMLValueTensorPreAllocateBuffer(mlvalue1, - start_index, - DataTypeImpl::GetType(), - p_tensor->Location(), - shape2)); + start_index, + DataTypeImpl::GetType(), + p_tensor->Location(), + shape2)); const OrtValue* p_ml_value_const = frame.GetNodeInputOrOutputMLValue(1); auto tensor2 = p_ml_value_const ? &(p_ml_value_const->Get()) : nullptr; @@ -224,8 +217,9 @@ TEST_F(ExecutionFrameTest, MemPatternTest) { std::unique_ptr p_seq_exec_plan = onnxruntime::make_unique(); SequentialPlannerContext context(ExecutionMode::ORT_SEQUENTIAL); - ASSERT_STATUS_OK(SequentialPlanner::CreatePlan(nullptr, GraphViewer(graph), {}, execution_providers, kernel_registry_manager, - mlvalue_name_idx_map, context, p_seq_exec_plan)); + ASSERT_STATUS_OK(SequentialPlanner::CreatePlan(nullptr, GraphViewer(graph), {}, execution_providers, + kernel_registry_manager, mlvalue_name_idx_map, context, + p_seq_exec_plan)); state.SetExecutionPlan(std::move(p_seq_exec_plan)); @@ -237,19 +231,19 @@ TEST_F(ExecutionFrameTest, MemPatternTest) { OrtValue& mlvalue5 = *frame.GetMutableNodeInputOrOutputMLValue(5); ASSERT_STATUS_OK(frame.AllocateMLValueTensorSelfOwnBuffer(mlvalue3, 3, - DataTypeImpl::GetType(), - cpu_allocator->Info(), - TensorShape(std::vector{2, 2}))); + DataTypeImpl::GetType(), + cpu_allocator->Info(), + TensorShape(std::vector{2, 2}))); ASSERT_STATUS_OK(frame.AllocateMLValueTensorSelfOwnBuffer(mlvalue4, 4, - DataTypeImpl::GetType(), - cpu_allocator->Info(), - TensorShape(std::vector{2, 3}))); + DataTypeImpl::GetType(), + cpu_allocator->Info(), + TensorShape(std::vector{2, 3}))); ASSERT_STATUS_OK(frame.AllocateMLValueTensorSelfOwnBuffer(mlvalue5, 5, - DataTypeImpl::GetType(), - cpu_allocator->Info(), - TensorShape(std::vector{2, 3}))); + DataTypeImpl::GetType(), + cpu_allocator->Info(), + TensorShape(std::vector{2, 3}))); MemoryPatternGroup pattern; ASSERT_STATUS_OK(frame.GeneratePatterns(&pattern)); @@ -297,5 +291,64 @@ TEST(ExecutionFrameTestWithoutSessionState, BadModelInvalidDimParamUsage) { EXPECT_THAT(st.ErrorMessage(), testing::HasSubstr("Shape mismatch attempting to re-use buffer.")); } +// Test that when an initializer is a graph output it is handled correctly +TEST(ExecutionFrameTestInit, InitializerAsOutput) { + const std::vector expected{ + 1.764052391052246f, 0.40015721321105957f, 0.978738009929657f, 2.2408931255340576f, 1.8675580024719238f, + -0.9772778749465942f, 0.9500884413719177f, -0.15135720372200012f, -0.10321885347366333f, 0.4105985164642334f, + 0.14404356479644775f, 1.4542734622955322f, 0.7610377073287964f, 0.12167501449584961f, 0.44386324286460876f, + 0.3336743414402008f, 1.4940791130065918f, -0.2051582634449005f, 0.3130677044391632f, -0.8540957570075989f, + -2.5529897212982178f, 0.653618574142456f, 0.8644362092018127f, -0.7421650290489197f, 2.269754648208618f}; + + SessionOptions so; + + // test if pre-allocated fetch is provided the initializer values are copied into that buffer + { + InferenceSession session(so, GetEnvironment()); + ASSERT_STATUS_OK(session.Load(ORT_TSTR("testdata/initializer_as_output.onnx"))); + ASSERT_STATUS_OK(session.Initialize()); + + auto allocator = test::AllocatorManager::Instance().GetAllocator(CPU); + auto p_tensor = onnxruntime::make_unique(DataTypeImpl::GetType(), TensorShape({5, 5}), allocator); + const void* orig_buffer = p_tensor->DataRaw(); + + std::vector results; + results.resize(1); + results[0].Init(p_tensor.release(), DataTypeImpl::GetType(), + DataTypeImpl::GetType()->GetDeleteFunc()); + RunOptions ro; + ASSERT_STATUS_OK(session.Run(ro, {}, {}, {"values"}, &results)); + + EXPECT_EQ(results[0].Get().DataRaw(), orig_buffer); + EXPECT_THAT(results[0].Get().DataAsSpan(), ::testing::ContainerEq(gsl::make_span(expected))); + } + + // test that if no pre-allocated fetch is provided a new OrtValue is allocated for the results + { + class TestInferenceSesssion : public InferenceSession { + public: + TestInferenceSesssion(const SessionOptions& session_options, + const Environment& session_env) + : InferenceSession(session_options, session_env) { + } + + const SessionState& GetSessionState() const { return *session_state_; } + }; + + TestInferenceSesssion session(so, GetEnvironment()); + ASSERT_STATUS_OK(session.Load(ORT_TSTR("testdata/initializer_as_output.onnx"))); + ASSERT_STATUS_OK(session.Initialize()); + + std::vector results; + RunOptions ro; + ASSERT_STATUS_OK(session.Run(ro, {}, {}, {"values"}, &results)); + + // output buffer should not be the same as the initializer in SessionState + const auto& initializers = session.GetSessionState().GetInitializedTensors(); + EXPECT_NE(results[0].Get().DataRaw(), initializers.at(0).Get().DataRaw()); + EXPECT_THAT(results[0].Get().DataAsSpan(), ::testing::ContainerEq(gsl::make_span(expected))); + } +} + } // namespace test } // namespace onnxruntime diff --git a/onnxruntime/test/testdata/initializer_as_output.onnx b/onnxruntime/test/testdata/initializer_as_output.onnx new file mode 100644 index 0000000000..2a3d11bb08 --- /dev/null +++ b/onnxruntime/test/testdata/initializer_as_output.onnx @@ -0,0 +1,7 @@ + backend-test:Ç +›values"Constant*† +value*z"dxÌá?háÌ>“Žz?Ëj@$ ï?â.z¿ÿ8s?bý¾hdÓ½ø9Ò>(€>¢%º?^ÓB?À0ù= Bã>]ת>ü=¿?R¾iJ >¦Z¿/d#ÀŒS'?±K]?‡þ=¿©C@B const_tensor  test_constantb +values +  + +B \ No newline at end of file