diff --git a/onnxruntime/core/platform/posix/env.cc b/onnxruntime/core/platform/posix/env.cc index aeddef0c5188f..4edfb4296ccb8 100644 --- a/onnxruntime/core/platform/posix/env.cc +++ b/onnxruntime/core/platform/posix/env.cc @@ -38,12 +38,11 @@ limitations under the License. #include // for std::forward #include -// We can not use CPUINFO if it is not supported and we do not want to used +// We can not use CPUINFO if it is not supported and we do not want to use // it on certain platforms because of the binary size increase. // We could use it to find out the number of physical cores for certain supported platforms -#if defined(CPUINFO_SUPPORTED) && !defined(__APPLE__) && !defined(__ANDROID__) && !defined(__wasm__) && !defined(_AIX) +#if defined(CPUINFO_SUPPORTED) && defined(__linux__) #include -#define ORT_USE_CPUINFO #endif #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) @@ -266,17 +265,17 @@ class PosixEnv : public Env { // Return the number of physical cores int GetNumPhysicalCpuCores() const override { -#ifdef ORT_USE_CPUINFO +#if defined(CPUINFO_SUPPORTED) && defined(__linux__) if (cpuinfo_available_) { return narrow(cpuinfo_get_cores_count()); } -#endif // ORT_USE_CPUINFO +#endif // defined(CPUINFO_SUPPORTED) && defined(__linux__) return DefaultNumCores(); } std::vector GetDefaultThreadAffinities() const override { std::vector ret; -#ifdef ORT_USE_CPUINFO +#if defined(CPUINFO_SUPPORTED) && defined(__linux__) if (cpuinfo_available_) { auto num_phys_cores = cpuinfo_get_cores_count(); ret.reserve(num_phys_cores); @@ -292,7 +291,7 @@ class PosixEnv : public Env { ret.push_back(std::move(th_aff)); } } -#endif +#endif // defined(CPUINFO_SUPPORTED) && defined(__linux__) // Just the size of the thread-pool if (ret.empty()) { ret.resize(GetNumPhysicalCpuCores()); @@ -614,7 +613,7 @@ class PosixEnv : public Env { private: Telemetry telemetry_provider_; -#ifdef ORT_USE_CPUINFO +#if defined(CPUINFO_SUPPORTED) && defined(__linux__) PosixEnv() { cpuinfo_available_ = cpuinfo_initialize(); if (!cpuinfo_available_) { @@ -622,7 +621,7 @@ class PosixEnv : public Env { } } bool cpuinfo_available_{false}; -#endif // ORT_USE_CPUINFO +#endif // defined(CPUINFO_SUPPORTED) && defined(__linux__) }; } // namespace diff --git a/onnxruntime/core/providers/cpu/cpu_execution_provider.cc b/onnxruntime/core/providers/cpu/cpu_execution_provider.cc index fb854b19accfa..f8b49a98eadfd 100644 --- a/onnxruntime/core/providers/cpu/cpu_execution_provider.cc +++ b/onnxruntime/core/providers/cpu/cpu_execution_provider.cc @@ -1072,6 +1072,7 @@ class ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kOnnxDomain, 17, ST class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kOnnxDomain, 17, float, LayerNormalization); class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kOnnxDomain, 17, double, LayerNormalization); class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kOnnxDomain, 17, MLFloat16, LayerNormalization); +class ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kOnnxDomain, 17, SequenceMap); // Opset 18 class ONNX_OPERATOR_VERSIONED_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kOnnxDomain, 18, 18, float, Resize); class ONNX_OPERATOR_VERSIONED_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kOnnxDomain, 18, 18, int32_t, Resize); @@ -3068,6 +3069,7 @@ Status RegisterOnnxOperatorKernels(KernelRegistry& kernel_registry) { LayerNormalization)>, BuildKernelCreateInfo, + BuildKernelCreateInfo, // Opset 18 BuildKernelCreateInfo feed_names; + feed_names.reserve(subgraph_inputs.size() + implicit_defs.size()); + for (const auto* input : subgraph_inputs) { + feed_names.push_back(input->Name()); + } + for (const auto* implicit_def : implicit_defs) { + feed_names.push_back(implicit_def->Name()); + } + + std::vector fetch_names; + fetch_names.reserve(subgraph_outputs.size()); + for (const auto* output : subgraph_outputs) { + fetch_names.push_back(output->Name()); + } + + const auto& subgraph_map = subgraph_session_state.GetOrtValueNameIdxMap(); + std::unique_ptr ffm; + ORT_RETURN_IF_ERROR(FeedsFetchesManager::Create(feed_names, fetch_names, subgraph_map, ffm)); + ORT_RETURN_IF_ERROR(utils::InitializeFeedFetchCopyInfo(subgraph_session_state, *ffm)); + + // Feeds come from the outer node's explicit inputs and implicit inputs (outer-scope captures). + const auto& node_inputs = node.InputDefs(); + std::vector outer_feed_names; + outer_feed_names.reserve(node_inputs.size() + implicit_defs.size()); + for (const auto* input_def : node_inputs) { + outer_feed_names.push_back(input_def->Name()); + } + for (const auto* implicit_def : implicit_defs) { + outer_feed_names.push_back(implicit_def->Name()); + } + + std::vector feed_locations; + ORT_RETURN_IF_ERROR( + controlflow::detail::FindDevicesForValues(session_state, outer_feed_names, feed_locations)); + + // Outputs go to the SequenceMap node's output sequence slots. + std::vector fetch_locations; + const auto& node_outputs = node.OutputDefs(); + fetch_locations.reserve(node_outputs.size()); + for (const auto* output_def : node_outputs) { + fetch_locations.push_back(&utils::FindDeviceForValue(session_state, output_def->Name())); + } + + utils::FinalizeFeedFetchCopyInfo(*ffm, feed_locations, fetch_locations); + feeds_fetches_manager_ = std::move(ffm); + + return Status::OK(); +} + +Status SequenceMap::Compute(OpKernelContext* ctx) const { + ORT_ENFORCE(feeds_fetches_manager_, + "SetupSubgraphExecutionInfo must be called prior to SequenceMap Compute."); + + auto* ctx_internal = static_cast(ctx); + const auto* session_state = ctx_internal->SubgraphSessionState("body"); + ORT_ENFORCE(session_state, "Subgraph SessionState was not found for 'body' attribute."); + + const auto* input_seq = ctx->Input(0); + ORT_ENFORCE(input_seq != nullptr, "SequenceMap: first input (input_sequence) must be a sequence."); + const auto seq_len = input_seq->Size(); + + const int num_outer_inputs = ctx->InputCount(); + const int num_outputs = ctx->OutputCount(); + + // Initialise each output TensorSeq with its element type before the iteration loop so that + // an empty input sequence still produces correctly-typed outputs (change D). + std::vector output_seqs(num_outputs, nullptr); + for (int j = 0; j < num_outputs; ++j) { + output_seqs[j] = ctx->Output(j); + ORT_ENFORCE(output_seqs[j] != nullptr, "SequenceMap: failed to get output TensorSeq slot ", j); + const auto* out_type = ctx->OutputType(j); + ORT_ENFORCE(out_type != nullptr, "SequenceMap: could not determine type for output ", j); + output_seqs[j]->SetType(out_type->AsSequenceTensorType()->GetElementType()); + output_seqs[j]->Reserve(seq_len); + } + + // Validate that all additional sequence inputs have the same length as input_sequence. + for (int k = 1; k < num_outer_inputs; ++k) { + const auto* seq = ctx->Input(k); + if (seq != nullptr && seq->Size() != seq_len) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "SequenceMap: additional sequence input ", k, + " has length ", seq->Size(), + " but input_sequence has length ", seq_len); + } + } + + // Hoist feeds/fetches outside the iteration loop to avoid repeated allocation (change C). + const auto& implicit_inputs = ctx_internal->GetImplicitInputs(); + const auto num_implicit = implicit_inputs.size(); + + std::vector feeds; + std::vector fetches; + feeds.reserve(static_cast(num_outer_inputs) + num_implicit); + + for (size_t i = 0; i < seq_len; ++i) { + feeds.clear(); + fetches.clear(); + + // Build feeds: sequence inputs -> element i; tensor inputs -> pass-through OrtValue. + for (int k = 0; k < num_outer_inputs; ++k) { + const auto* seq_k = (k == 0) ? input_seq : ctx->Input(k); + if (seq_k != nullptr) { + feeds.push_back(seq_k->GetAt(i)); + } else { + // Tensor input: shallow-copy the OrtValue (shared_ptr, safe) from the kernel context. + const auto* input_val = ctx_internal->GetInputMLValue(k); + ORT_ENFORCE(input_val != nullptr, "SequenceMap: input ", k, " is neither a sequence nor a tensor."); + feeds.push_back(*input_val); + } + } + + // Append implicit inputs (outer-scope captures) in ImplicitInputDefs() order (change B). + for (size_t m = 0; m < num_implicit; ++m) { + feeds.push_back(*implicit_inputs[m]); + } + + ORT_RETURN_IF_ERROR(utils::ExecuteSubgraph(*session_state, *feeds_fetches_manager_, + feeds, fetches, {}, + ExecutionMode::ORT_SEQUENTIAL, + ctx->GetTerminateFlag(), + ctx->Logger(), + ctx->GetComputeStream(), + /*sync_subgraph_fetches=*/false, + ctx_internal->GetRunProfiler())); + + ORT_ENFORCE(static_cast(fetches.size()) == num_outputs, + "SequenceMap: body returned ", fetches.size(), " outputs but ", num_outputs, " were expected."); + + for (int j = 0; j < num_outputs; ++j) { + ORT_ENFORCE(fetches[j].IsTensor(), + "SequenceMap: body output ", j, " must be a tensor."); + // SetType was called before the loop (change D); per-iteration call removed (change E). + output_seqs[j]->Add(std::move(fetches[j])); + } + } + + return Status::OK(); +} + } // namespace onnxruntime diff --git a/onnxruntime/core/providers/cpu/sequence/sequence_ops.h b/onnxruntime/core/providers/cpu/sequence/sequence_ops.h index ccca226fb07ee..0942adbe34333 100644 --- a/onnxruntime/core/providers/cpu/sequence/sequence_ops.h +++ b/onnxruntime/core/providers/cpu/sequence/sequence_ops.h @@ -4,7 +4,9 @@ #pragma once #include "core/common/common.h" +#include "core/framework/feeds_fetches_manager.h" #include "core/framework/op_kernel.h" +#include "core/providers/cpu/controlflow/utils.h" namespace onnxruntime { @@ -70,4 +72,18 @@ class SplitToSequence final : public OpKernel { int64_t keepdims_{1}; const int64_t DEFAULT_LENGTH_EACH_OUTPUT_ = 1; }; + +class SequenceMap final : public controlflow::IControlFlowKernel { + public: + SequenceMap(const OpKernelInfo& info) : IControlFlowKernel(info) {} + + Status Compute(OpKernelContext* ctx) const override; + + Status SetupSubgraphExecutionInfo(const SessionState& session_state, + const std::string& attribute_name, + const SessionState& subgraph_session_state) override; + + private: + std::unique_ptr feeds_fetches_manager_; +}; } // namespace onnxruntime diff --git a/onnxruntime/test/providers/cpu/sequence/sequence_ops_test.cc b/onnxruntime/test/providers/cpu/sequence/sequence_ops_test.cc index d819b0973adc2..7bb27f6ede75c 100644 --- a/onnxruntime/test/providers/cpu/sequence/sequence_ops_test.cc +++ b/onnxruntime/test/providers/cpu/sequence/sequence_ops_test.cc @@ -3,6 +3,9 @@ #include "gtest/gtest.h" #include "test/providers/provider_test_utils.h" +#include "test/util/include/default_providers.h" +#include "core/common/logging/logging.h" +#include "core/session/inference_session.h" #include namespace onnxruntime { @@ -550,5 +553,161 @@ TEST(SequenceOpsTest, SplitToSequence_BoolSplit) { test.AddSeqOutput("S2", output); test.Run(); } + +// SequenceMap (opset 17) tests +// Each test builds a body GraphProto via a minimal Model, then uses a +// SequenceMapOpTester (OpTester subclass) that attaches it via AddNodes. + +namespace { + +using namespace ONNX_NAMESPACE; + +// Build a body graph with a single Identity node: input -> output. +// Used for the identity pass-through test. +static GraphProto BuildIdentityBody() { + Model model("SequenceMap_identity_body", false, DefaultLoggingManager().DefaultLogger()); + auto& graph = model.MainGraph(); + + TypeProto float_tensor; + float_tensor.mutable_tensor_type()->set_elem_type(TensorProto_DataType_FLOAT); + float_tensor.mutable_tensor_type()->mutable_shape()->add_dim(); + + auto& x_arg = graph.GetOrCreateNodeArg("x", &float_tensor); + auto& y_arg = graph.GetOrCreateNodeArg("y", &float_tensor); + + graph.AddNode("identity", "Identity", "Identity", {&x_arg}, {&y_arg}); + + auto status = graph.Resolve(); + EXPECT_EQ(status, Status::OK()); + return graph.ToGraphProto(); +} + +// Build a body graph with Add: two inputs x + scalar_in -> out. +static GraphProto BuildAddBody() { + Model model("SequenceMap_add_body", false, DefaultLoggingManager().DefaultLogger()); + auto& graph = model.MainGraph(); + + TypeProto float_tensor; + float_tensor.mutable_tensor_type()->set_elem_type(TensorProto_DataType_FLOAT); + float_tensor.mutable_tensor_type()->mutable_shape()->add_dim(); + + // scalar_in is rank-0 (no dims) so the body Add performs scalar broadcasting. + TypeProto float_scalar; + float_scalar.mutable_tensor_type()->set_elem_type(TensorProto_DataType_FLOAT); + float_scalar.mutable_tensor_type()->mutable_shape(); // empty shape = rank-0 + + auto& x_arg = graph.GetOrCreateNodeArg("x", &float_tensor); + auto& scalar_arg = graph.GetOrCreateNodeArg("scalar_in", &float_scalar); + auto& out_arg = graph.GetOrCreateNodeArg("add_out", &float_tensor); + + graph.AddNode("add", "Add", "Add x and scalar", {&x_arg, &scalar_arg}, {&out_arg}); + + auto status = graph.Resolve(); + EXPECT_EQ(status, Status::OK()); + return graph.ToGraphProto(); +} + +// Build a body graph with two outputs: x (Identity) and x*x (Mul). +static GraphProto BuildSquareBody() { + Model model("SequenceMap_square_body", false, DefaultLoggingManager().DefaultLogger()); + auto& graph = model.MainGraph(); + + TypeProto float_tensor; + float_tensor.mutable_tensor_type()->set_elem_type(TensorProto_DataType_FLOAT); + float_tensor.mutable_tensor_type()->mutable_shape()->add_dim(); + + auto& x_arg = graph.GetOrCreateNodeArg("x", &float_tensor); + auto& id_out = graph.GetOrCreateNodeArg("id_out", &float_tensor); + auto& sq_out = graph.GetOrCreateNodeArg("sq_out", &float_tensor); + + graph.AddNode("identity", "Identity", "Pass through", {&x_arg}, {&id_out}); + graph.AddNode("mul", "Mul", "Square", {&x_arg, &x_arg}, {&sq_out}); + + auto status = graph.Resolve(); + EXPECT_EQ(status, Status::OK()); + return graph.ToGraphProto(); +} + +// OpTester subclass for SequenceMap: attaches the body GraphProto via AddNodes. +class SequenceMapOpTester : public OpTester { + public: + explicit SequenceMapOpTester(GraphProto body) + : OpTester("SequenceMap", 17), body_(std::move(body)) {} + + protected: + void AddNodes(onnxruntime::Graph& graph, + std::vector& graph_input_defs, + std::vector& graph_output_defs, + std::vector>& /*add_attribute_funcs*/) override { + auto& seq_map_node = graph.AddNode("sequence_map", "SequenceMap", "SequenceMap node", + graph_input_defs, graph_output_defs); + seq_map_node.AddAttribute("body", body_); + } + + private: + GraphProto body_; +}; + +} // namespace + +// Test 1: identity body — output sequence equals input sequence. +TEST(SequenceOpsTest, SequenceMap_Identity) { + SequenceMapOpTester test{BuildIdentityBody()}; + + SeqTensors input; + input.AddTensor({3}, {1.0f, 2.0f, 3.0f}); + input.AddTensor({2}, {4.0f, 5.0f}); + test.AddSeqInput("input_sequence", input); + + SeqTensors expected; + expected.AddTensor({3}, {1.0f, 2.0f, 3.0f}); + expected.AddTensor({2}, {4.0f, 5.0f}); + test.AddSeqOutput("out_sequence", expected); + + test.Run(); +} + +// Test 2: Add body with a rank-0 scalar extra input broadcast to each sequence element. +TEST(SequenceOpsTest, SequenceMap_AddScalar) { + SequenceMapOpTester test{BuildAddBody()}; + + SeqTensors input; + input.AddTensor({3}, {1.0f, 2.0f, 3.0f}); + input.AddTensor({3}, {10.0f, 20.0f, 30.0f}); + test.AddSeqInput("input_sequence", input); + + // additional_inputs is a rank-0 scalar passed through to every iteration. + test.AddInput("additional_inputs", {}, {10.0f}); + + SeqTensors expected; + expected.AddTensor({3}, {11.0f, 12.0f, 13.0f}); + expected.AddTensor({3}, {20.0f, 30.0f, 40.0f}); + test.AddSeqOutput("out_sequence", expected); + + test.Run(); +} + +// Test 3: Two-output body (identity + square) — verify both output sequences. +TEST(SequenceOpsTest, SequenceMap_TwoOutputs) { + SequenceMapOpTester test{BuildSquareBody()}; + + SeqTensors input; + input.AddTensor({2}, {2.0f, 3.0f}); + input.AddTensor({2}, {4.0f, 5.0f}); + test.AddSeqInput("input_sequence", input); + + SeqTensors expected_id; + expected_id.AddTensor({2}, {2.0f, 3.0f}); + expected_id.AddTensor({2}, {4.0f, 5.0f}); + test.AddSeqOutput("out_sequence", expected_id); + + SeqTensors expected_sq; + expected_sq.AddTensor({2}, {4.0f, 9.0f}); + expected_sq.AddTensor({2}, {16.0f, 25.0f}); + test.AddSeqOutput("out_sequence_1", expected_sq); + + test.Run(); +} + } // namespace test } // namespace onnxruntime