From 122959c28fe07781330b38a3bbc5441a35a46d86 Mon Sep 17 00:00:00 2001 From: Rishi Dave Date: Wed, 6 May 2026 11:07:11 +0000 Subject: [PATCH 1/3] fix: narrow ORT_USE_CPUINFO guard to __linux__ point-of-use Replace the broad ORT_USE_CPUINFO macro (with negated platform exclusions) with inline defined(CPUINFO_SUPPORTED) && defined(__linux__) guards at each point of use. Since __APPLE__ and __linux__ are mutually exclusive, the previous negation-based condition collapses to simply defined(__linux__). Drop the intermediate ORT_USE_CPUINFO macro in favour of direct guards. --- onnxruntime/core/platform/posix/env.cc | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) 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 From 58312a759ed7c547d0ed1dbe378c87959646bd1d Mon Sep 17 00:00:00 2001 From: Rishi Dave Date: Fri, 5 Jun 2026 11:33:56 +0000 Subject: [PATCH 2/3] feat: add native CPU kernel for SequenceMap (opset 17) Without a dedicated kernel, SequenceMap falls back to the ONNX context-dependent function body, which expands the op into a Loop over SequenceInsert calls. Each SequenceInsert copies the accumulator sequence, producing O(n^2) memory traffic for an n-element input. This adds a native CPU kernel that: - Derives from IControlFlowKernel and sets up the body subgraph via the standard FeedsFetchesManager flow used by Loop, If, and Scan. - Iterates the input sequence sequentially in O(n), forwarding the i-th element of each sequence-typed input and passing tensor-typed additional_inputs through unchanged. - Validates that sequence-typed additional_inputs share the input sequence length. - Assembles one TensorSeq per body output and appends fetched tensors per iteration without intermediate copies. Adds unit tests for the identity body, an add-scalar body with a tensor additional input, and a body that emits two outputs to cover the multi-output path. Fixes #23024 --- .../providers/cpu/cpu_execution_provider.cc | 2 + .../providers/cpu/sequence/sequence_ops.cc | 151 +++++++++++++++++ .../providers/cpu/sequence/sequence_ops.h | 16 ++ .../cpu/sequence/sequence_ops_test.cc | 154 ++++++++++++++++++ 4 files changed, 323 insertions(+) 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()); + for (const auto* input : subgraph_inputs) { + feed_names.push_back(input->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. + const auto& node_inputs = node.InputDefs(); + std::vector outer_feed_names; + outer_feed_names.reserve(node_inputs.size()); + for (const auto* input_def : node_inputs) { + outer_feed_names.push_back(input_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(); + + // Each output sequence collects one tensor per iteration. + // We defer SetType until after the first iteration populates the fetches. + 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); + 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); + } + } + + for (size_t i = 0; i < seq_len; ++i) { + std::vector feeds; + feeds.reserve(static_cast(num_outer_inputs)); + + // 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); + } + } + + std::vector fetches; + 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."); + if (i == 0) { + output_seqs[j]->SetType(fetches[j].Get().DataType()); + } + 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..e5a849fa142a8 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,156 @@ 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(); + + auto& x_arg = graph.GetOrCreateNodeArg("x", &float_tensor); + auto& scalar_arg = graph.GetOrCreateNodeArg("scalar_in", &float_tensor); + 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 an additional scalar tensor input broadcast to each 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 tensor (passed through to every iteration) + test.AddInput("additional_inputs", {3}, {100.0f, 100.0f, 100.0f}); + + SeqTensors expected; + expected.AddTensor({3}, {101.0f, 102.0f, 103.0f}); + expected.AddTensor({3}, {110.0f, 120.0f, 130.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 From 62a1bea9530c322a67f69eb774ae651282345440 Mon Sep 17 00:00:00 2001 From: Rishi Dave Date: Sat, 6 Jun 2026 11:18:59 +0000 Subject: [PATCH 3/3] fix(sequence_map): address review feedback - Include implicit inputs in subgraph feed setup and Compute - Initialize output TensorSeq element type before iteration loop so empty input sequences produce correctly-typed outputs - Remove redundant per-iteration SetType - Hoist feeds/fetches allocations outside the iteration loop - Fix scalar broadcasting test: use rank-0 TypeProto and scalar value --- .../providers/cpu/sequence/sequence_ops.cc | 43 ++++++++++++++----- .../cpu/sequence/sequence_ops_test.cc | 17 +++++--- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/onnxruntime/core/providers/cpu/sequence/sequence_ops.cc b/onnxruntime/core/providers/cpu/sequence/sequence_ops.cc index 3ae5d050c9852..772fadb0bf3fa 100644 --- a/onnxruntime/core/providers/cpu/sequence/sequence_ops.cc +++ b/onnxruntime/core/providers/cpu/sequence/sequence_ops.cc @@ -609,11 +609,16 @@ common::Status SequenceMap::SetupSubgraphExecutionInfo(const SessionState& sessi const auto& subgraph_inputs = subgraph.GetInputs(); const auto& subgraph_outputs = subgraph.GetOutputs(); + const auto& implicit_defs = node.ImplicitInputDefs(); + std::vector feed_names; - feed_names.reserve(subgraph_inputs.size()); + 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()); @@ -626,13 +631,16 @@ common::Status SequenceMap::SetupSubgraphExecutionInfo(const SessionState& sessi 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. + // 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()); + 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( @@ -667,12 +675,15 @@ Status SequenceMap::Compute(OpKernelContext* ctx) const { const int num_outer_inputs = ctx->InputCount(); const int num_outputs = ctx->OutputCount(); - // Each output sequence collects one tensor per iteration. - // We defer SetType until after the first iteration populates the fetches. + // 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); } @@ -687,9 +698,17 @@ Status SequenceMap::Compute(OpKernelContext* ctx) const { } } + // 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) { - std::vector feeds; - feeds.reserve(static_cast(num_outer_inputs)); + feeds.clear(); + fetches.clear(); // Build feeds: sequence inputs -> element i; tensor inputs -> pass-through OrtValue. for (int k = 0; k < num_outer_inputs; ++k) { @@ -704,7 +723,11 @@ Status SequenceMap::Compute(OpKernelContext* ctx) const { } } - std::vector fetches; + // 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, @@ -720,9 +743,7 @@ Status SequenceMap::Compute(OpKernelContext* ctx) const { for (int j = 0; j < num_outputs; ++j) { ORT_ENFORCE(fetches[j].IsTensor(), "SequenceMap: body output ", j, " must be a tensor."); - if (i == 0) { - output_seqs[j]->SetType(fetches[j].Get().DataType()); - } + // SetType was called before the loop (change D); per-iteration call removed (change E). output_seqs[j]->Add(std::move(fetches[j])); } } diff --git a/onnxruntime/test/providers/cpu/sequence/sequence_ops_test.cc b/onnxruntime/test/providers/cpu/sequence/sequence_ops_test.cc index e5a849fa142a8..7bb27f6ede75c 100644 --- a/onnxruntime/test/providers/cpu/sequence/sequence_ops_test.cc +++ b/onnxruntime/test/providers/cpu/sequence/sequence_ops_test.cc @@ -591,8 +591,13 @@ static GraphProto BuildAddBody() { 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_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}); @@ -662,7 +667,7 @@ TEST(SequenceOpsTest, SequenceMap_Identity) { test.Run(); } -// Test 2: Add body with an additional scalar tensor input broadcast to each element. +// Test 2: Add body with a rank-0 scalar extra input broadcast to each sequence element. TEST(SequenceOpsTest, SequenceMap_AddScalar) { SequenceMapOpTester test{BuildAddBody()}; @@ -671,12 +676,12 @@ TEST(SequenceOpsTest, SequenceMap_AddScalar) { input.AddTensor({3}, {10.0f, 20.0f, 30.0f}); test.AddSeqInput("input_sequence", input); - // additional_inputs is a tensor (passed through to every iteration) - test.AddInput("additional_inputs", {3}, {100.0f, 100.0f, 100.0f}); + // additional_inputs is a rank-0 scalar passed through to every iteration. + test.AddInput("additional_inputs", {}, {10.0f}); SeqTensors expected; - expected.AddTensor({3}, {101.0f, 102.0f, 103.0f}); - expected.AddTensor({3}, {110.0f, 120.0f, 130.0f}); + 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();