diff --git a/docs/OperatorKernels.md b/docs/OperatorKernels.md index f0b79eb9e429f..a2bb39da76235 100644 --- a/docs/OperatorKernels.md +++ b/docs/OperatorKernels.md @@ -385,6 +385,7 @@ Do not modify directly.* |||[1, 10]|**T** = tensor(bfloat16), tensor(bool), tensor(double), tensor(float), tensor(float16), tensor(int16), tensor(int32), tensor(int64), tensor(int8), tensor(string), tensor(uint16), tensor(uint32), tensor(uint64), tensor(uint8)| |StringConcat|*in* X:**T**
*in* Y:**T**
*out* Z:**T**|20+|**T** = tensor(string)| |StringNormalizer|*in* X:**tensor(string)**
*out* Y:**tensor(string)**|10+|**X** = tensor(string)| +|StringSplit|*in* X:**T1**
*out* Y:**T2**
*out* Z:**T3**|20+|**T1** = tensor(string)
**T2** = tensor(string)
**T3** = tensor(int64)| |Sub|*in* A:**T**
*in* B:**T**
*out* C:**T**|14+|**T** = tensor(double), tensor(float), tensor(int32), tensor(int64)| |||13|**T** = tensor(double), tensor(float), tensor(int32), tensor(int64)| |||[7, 12]|**T** = tensor(double), tensor(float), tensor(int32), tensor(int64)| diff --git a/onnxruntime/contrib_ops/cpu/quantization/qlinear_concat.cc b/onnxruntime/contrib_ops/cpu/quantization/qlinear_concat.cc index ee9ae7167945c..af163b6be702b 100644 --- a/onnxruntime/contrib_ops/cpu/quantization/qlinear_concat.cc +++ b/onnxruntime/contrib_ops/cpu/quantization/qlinear_concat.cc @@ -1,4 +1,4 @@ -// Copyright (c Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. #include "qlinear_util.h" diff --git a/onnxruntime/core/providers/cpu/cpu_execution_provider.cc b/onnxruntime/core/providers/cpu/cpu_execution_provider.cc index 9cd0b3d0620af..6aef03a32db09 100644 --- a/onnxruntime/core/providers/cpu/cpu_execution_provider.cc +++ b/onnxruntime/core/providers/cpu/cpu_execution_provider.cc @@ -991,6 +991,7 @@ class ONNX_OPERATOR_TYPED_KERNEL_CLASS_NAME(kCpuExecutionProvider, kOnnxDomain, class ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kOnnxDomain, 20, IsInf); class ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kOnnxDomain, 20, StringConcat); class ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kOnnxDomain, 20, RegexFullMatch); +class ONNX_OPERATOR_KERNEL_CLASS_NAME(kCpuExecutionProvider, kOnnxDomain, 20, StringSplit); // !!PLEASE READ BELOW!! Following that, add new entries above this comment @@ -2451,6 +2452,7 @@ Status RegisterOnnxOperatorKernels(KernelRegistry& kernel_registry) { BuildKernelCreateInfo, BuildKernelCreateInfo, BuildKernelCreateInfo, + BuildKernelCreateInfo, }; for (auto& function_table_entry : function_table) { diff --git a/onnxruntime/core/providers/cpu/text/string_split.cc b/onnxruntime/core/providers/cpu/text/string_split.cc new file mode 100644 index 0000000000000..2b82309838464 --- /dev/null +++ b/onnxruntime/core/providers/cpu/text/string_split.cc @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "string_split.h" +#include +#include +#include +#include "core/common/common.h" +namespace onnxruntime { + +ONNX_CPU_OPERATOR_KERNEL(StringSplit, 20, + KernelDefBuilder() + .TypeConstraint("T1", DataTypeImpl::GetTensorType()) + .TypeConstraint("T2", DataTypeImpl::GetTensorType()) + .TypeConstraint("T3", DataTypeImpl::GetTensorType()), + StringSplit); + +/// Calculate substrings in ``str`` delimited by ``delimiter``. A maximum of ``max_splits`` splits are permitted. +/// Returns a vector of string slices into ``str`` representing the substrings as string views. The user must ensure +/// the returned views' lifetime does not exceed ``str``'s. +void ComputeSubstrings(std::string_view str, std::string_view delimiter, int64_t max_splits, InlinedVector& out) { + if (str.empty()) { + return; + } + if (delimiter.empty()) { + // Count consecutive whitespace as one delimiter. Preceding and trailing whitespace is meant to be ignored. + size_t pos = str.find_first_not_of(" "); + int64_t token_count = 0; + while (pos != std::string::npos) { + if (token_count++ == max_splits) { + // Trim down last substring as required in specification + size_t next_pos = str.length() - 1; + while (str[next_pos] == ' ') { + next_pos--; + } + out.push_back(str.substr(pos, next_pos - pos + 1)); + break; + } else { + auto next_pos = str.find_first_of(" ", pos); + out.push_back(str.substr(pos, next_pos - pos)); + pos = str.find_first_not_of(" ", next_pos); + } + } + } else { + size_t pos = 0; + int64_t token_count = 0; + while (pos != std::string::npos) { + auto next_pos = str.find(delimiter, pos); + if (token_count++ == max_splits || next_pos == std::string::npos) { + out.push_back(str.substr(pos)); + break; + } + out.push_back(str.substr(pos, next_pos - pos)); + pos = next_pos + delimiter.size(); + } + } +} + +StringSplit::StringSplit(const OpKernelInfo& info) : OpKernel(info) { + info.GetAttrOrDefault("maxsplit", &maxsplit_, std::numeric_limits::max() - 1); + info.GetAttrOrDefault("delimiter", &delimiter_, std::string()); +} + +Status StringSplit::Compute(OpKernelContext* context) const { + const Tensor* input = context->Input(0); + auto input_data = input->template DataAsSpan(); + + // Set up number of tokens output + auto num_tokens_data = context->Output(1, input->Shape())->template MutableDataAsSpan(); + auto num_tokens_iter = num_tokens_data.begin(); + + InlinedVector> input_slices; + input_slices.reserve(input_data.size()); + size_t last_dim = 0; + + for (const auto& s : input_data) { + auto& substrs = input_slices.emplace_back(); + ComputeSubstrings(s, delimiter_, maxsplit_, substrs); + auto substr_count = substrs.size(); + last_dim = std::max(last_dim, substr_count); + *num_tokens_iter = static_cast(substr_count); + ++num_tokens_iter; + } + + // Set up splits output + auto splits_shape = input->Shape().AsShapeVector(); + splits_shape.push_back(last_dim); + + auto splits_data = context->Output(0, splits_shape)->template MutableDataAsSpan(); + auto slices_iter = input_slices.begin(); + for (auto output_splits_iter = splits_data.begin(); output_splits_iter != splits_data.end(); output_splits_iter += last_dim, ++slices_iter) { + std::copy(slices_iter->begin(), slices_iter->end(), output_splits_iter); + } + + return Status::OK(); +} + +} // namespace onnxruntime diff --git a/onnxruntime/core/providers/cpu/text/string_split.h b/onnxruntime/core/providers/cpu/text/string_split.h new file mode 100644 index 0000000000000..6be249261d4e3 --- /dev/null +++ b/onnxruntime/core/providers/cpu/text/string_split.h @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "core/framework/op_kernel.h" + +namespace onnxruntime { + +class StringSplit final : public OpKernel { + public: + explicit StringSplit(const OpKernelInfo& info); + Status Compute(OpKernelContext* context) const override; + + private: + std::string delimiter_; + int64_t maxsplit_; +}; + +} // namespace onnxruntime diff --git a/onnxruntime/test/providers/cpu/text/string_split_test.cc b/onnxruntime/test/providers/cpu/text/string_split_test.cc new file mode 100644 index 0000000000000..d5e1c296d0b25 --- /dev/null +++ b/onnxruntime/test/providers/cpu/text/string_split_test.cc @@ -0,0 +1,126 @@ +#include "gtest/gtest.h" +#include "test/providers/provider_test_utils.h" + +namespace onnxruntime { +namespace test { + +TEST(StringSplit, BasicSplitTest) { + OpTester test("StringSplit", 20); + test.AddInput("X", {3}, {"hello world", "hello", "world"}); + test.AddAttribute("delimiter", " "); + test.AddOutput("Y", {3, 2}, {"hello", "world", "hello", "", "world", ""}); + test.AddOutput("Z", {3}, {2, 1, 1}); + test.Run(); +} + +TEST(StringSplit, MaxSplitTest) { + OpTester test("StringSplit", 20); + test.AddInput("X", {2, 2}, {"eggs;milk;chesse", "pepper;salt", "chicken;fish;pork", "spinach"}); + test.AddAttribute("delimiter", ";"); + test.AddAttribute("maxsplit", 1); + test.AddOutput("Y", {2, 2, 2}, + {"eggs", "milk;chesse", "pepper", "salt", "chicken", "fish;pork", "spinach", ""}); + test.AddOutput("Z", {2, 2}, {2, 2, 2, 1}); + test.Run(); +} + +TEST(StringSplit, EmptyStringDelimiterTest) { + OpTester test("StringSplit", 20); + test.AddInput("X", {1, 4}, {"hello world", "hello world", " hello world", "hello world "}); + test.AddAttribute("delimiter", ""); + test.AddOutput("Y", {1, 4, 2}, {"hello", "world", "hello", "world", "hello", "world", "hello", "world"}); + test.AddOutput("Z", {1, 4}, {2, 2, 2, 2}); + test.Run(); +} + +TEST(StringSplit, SubsequentWhitespaceDefaultTest) { + OpTester test("StringSplit", 20); + test.AddInput("X", {1, 4}, {"hello world", "hello world", " hello world", "hello world "}); + test.AddOutput("Y", {1, 4, 2}, {"hello", "world", "hello", "world", "hello", "world", "hello", "world"}); + test.AddOutput("Z", {1, 4}, {2, 2, 2, 2}); + test.Run(); +} + +TEST(StringSplit, SubsequentWhitespaceWithLimitTest) { + OpTester test("StringSplit", 20); + test.AddInput("X", {1, 4}, + {"lorem ipsum doler", " Open Neural Network Exchange (ONNX)", "onnx", "ONNX runtime "}); + test.AddAttribute("maxsplit", 1); + test.AddOutput( + "Y", {1, 4, 2}, + {"lorem", "ipsum doler", "Open", "Neural Network Exchange (ONNX)", "onnx", "", "ONNX", "runtime"}); + test.AddOutput("Z", {1, 4}, {2, 2, 1, 2}); + test.Run(); +} + +TEST(StringSplit, SingleTokenTest) { + OpTester test("StringSplit", 20); + test.AddAttribute("delimiter", "*"); + test.AddInput("X", {1, 1, 1}, {"lorem"}); + test.AddOutput("Y", {1, 1, 1, 1}, {"lorem"}); + test.AddOutput("Z", {1, 1, 1}, {1}); + test.Run(); +} + +TEST(StringSplit, SingleTokenWhitespaceTest) { + OpTester test("StringSplit", 20); + test.AddInput("X", {1, 1, 1}, {"lorem"}); + test.AddOutput("Y", {1, 1, 1, 1}, {"lorem"}); + test.AddOutput("Z", {1, 1, 1}, {1}); + test.Run(); +} + +TEST(StringSplit, EdgeWhitespaceTest) { + OpTester test("StringSplit", 20); + test.AddInput("X", {1, 1, 1}, {" lorem "}); + test.AddOutput("Y", {1, 1, 1, 1}, {"lorem"}); + test.AddOutput("Z", {1, 1, 1}, {1}); + test.Run(); +} + +TEST(StringSplit, EmptyInputTest) { + OpTester test("StringSplit", 20); + test.AddInput("X", {1, 3, 1}, {"", "+", "*"}); + test.AddAttribute("delimiter", "*"); + test.AddOutput("Y", {1, 3, 1, 2}, {"", "", "+", "", "", ""}); + test.AddOutput("Z", {1, 3, 1}, {0, 1, 2}); + test.Run(); +} + +TEST(StringSplit, OnlyEmptyInputTest) { + OpTester test("StringSplit", 20); + test.AddAttribute("delimiter", "*"); + test.AddInput("X", {1, 2, 1}, {"", ""}); + test.AddOutput("Y", {1, 2, 1, 0}, {}); + test.AddOutput("Z", {1, 2, 1}, {0, 0}); + test.Run(); +} + +TEST(StringSplit, OnlyEmptyNoDelimiterInputTest) { + OpTester test("StringSplit", 20); + test.AddInput("X", {1, 2, 1}, {"", ""}); + test.AddOutput("Y", {1, 2, 1, 0}, {}); + test.AddOutput("Z", {1, 2, 1}, {0, 0}); + test.Run(); +} + +TEST(StringSplit, NoInputTest) { + OpTester test("StringSplit", 20); + test.AddInput("X", { + 0, + }, + {}); + test.AddOutput("Y", { + 0, + 0, + }, + {}); + test.AddOutput("Z", { + 0, + }, + {}); + test.Run(); +} + +} // namespace test +} // namespace onnxruntime diff --git a/onnxruntime/test/testdata/onnx_backend_test_series_filters.jsonc b/onnxruntime/test/testdata/onnx_backend_test_series_filters.jsonc index c2ca5f860a107..ed263515d6dd6 100644 --- a/onnxruntime/test/testdata/onnx_backend_test_series_filters.jsonc +++ b/onnxruntime/test/testdata/onnx_backend_test_series_filters.jsonc @@ -248,12 +248,6 @@ "^test_image_decoder_decode_pnm_rgb", "^test_image_decoder_decode_tiff_rgb", "^test_image_decoder_decode_webp_rgb", - "^test_string_split_basic", - "^test_string_split_consecutive_delimiters", - "^test_string_split_empty_string_delimiter", - "^test_string_split_empty_tensor", - "^test_string_split_maxsplit", - "^test_string_split_no_delimiter", "^test_reduce_l1_empty_set_cuda", "^test_reduce_l1_empty_set_expanded_cuda", "^test_reduce_l2_empty_set_cuda",