diff --git a/onnxruntime/core/providers/cpu/ml/linearclassifier.cc b/onnxruntime/core/providers/cpu/ml/linearclassifier.cc index 1a35c24c69676..db8f7dd5d214e 100644 --- a/onnxruntime/core/providers/cpu/ml/linearclassifier.cc +++ b/onnxruntime/core/providers/cpu/ml/linearclassifier.cc @@ -41,6 +41,52 @@ LinearClassifier::LinearClassifier(const OpKernelInfo& info) ORT_ENFORCE(class_count_ > 0, "LinearClassifier: intercepts must not be empty."); ORT_ENFORCE(!coefficients_.empty(), "LinearClassifier: coefficients must not be empty."); + // The ONNX spec requires exactly one of classlabels_strings or classlabels_ints to be provided. + // However, existing models in the wild omit both and rely on default labels (0/1) for binary + // classification. Rejecting both-empty would break those models, so we only enforce that both + // are not simultaneously set. For multi-class (class_count > 1) the compute path has no default + // label fallback and would index into an empty vector, so class labels are required. + ORT_ENFORCE(classlabels_strings_.empty() || classlabels_ints_.empty(), + "LinearClassifier: only one of classlabels_strings or classlabels_ints may be specified."); + + if (!using_strings_ && classlabels_ints_.empty() && class_count_ > 1) { + ORT_ENFORCE(false, "LinearClassifier: classlabels_ints or classlabels_strings must be provided ", + "when the number of classes (", class_count_, ") is greater than 1."); + } + + // Validate that the class labels array is consistent with the number of classes. + // For binary classification (class_count == 1), the compute path uses labels only when + // the array has exactly 2 elements (negative/positive). Any other non-empty size would be + // silently ignored, so reject it to catch misconfigured models. + // For multi-class, classlabels must have at least class_count elements. + if (using_strings_) { + if (class_count_ == 1) { + ORT_ENFORCE(classlabels_strings_.size() == 2, + "LinearClassifier: classlabels_strings must have exactly 2 elements for binary ", + "classification, got ", classlabels_strings_.size(), "."); + } else { + ORT_ENFORCE(classlabels_strings_.size() >= static_cast(class_count_), + "LinearClassifier: classlabels_strings has ", classlabels_strings_.size(), + " elements but intercepts defines ", class_count_, " classes."); + } + } else if (!classlabels_ints_.empty()) { + if (class_count_ == 1) { + ORT_ENFORCE(classlabels_ints_.size() == 2, + "LinearClassifier: classlabels_ints must have exactly 2 elements for binary ", + "classification, got ", classlabels_ints_.size(), "."); + } else { + ORT_ENFORCE(classlabels_ints_.size() >= static_cast(class_count_), + "LinearClassifier: classlabels_ints has ", classlabels_ints_.size(), + " elements but intercepts defines ", class_count_, " classes."); + } + } + + // Validate that coefficients size is consistent with the number of classes. + // coefficients must be divisible by class_count to form a valid [class_count, num_features] matrix. + ORT_ENFORCE(coefficients_.size() % static_cast(class_count_) == 0, + "LinearClassifier: coefficients size (", coefficients_.size(), + ") must be a multiple of the number of classes (", class_count_, ")."); + SetupMlasBackendKernelSelectorFromConfigOptions(mlas_backend_kernel_selector_config_, info.GetConfigOptions()); } @@ -75,7 +121,8 @@ void LinearClassifier::ComputeImpl(const gsl::span input, threadpool, &mlas_backend_kernel_selector_config_); float* score = scores_output_data.data(); - float* end_scores = score + (num_batches * num_targets); // we haven't added extra targets yet so iterate the original scores + // Use SafeInt to guard against overflow in pointer arithmetic. + float* end_scores = score + static_cast(SafeInt(num_batches) * num_targets); if (num_targets == 1) { if (using_strings_) { @@ -141,10 +188,8 @@ static void CastInputToFloat(const Tensor& in, gsl::span& out) { Status LinearClassifier::Compute(OpKernelContext* ctx) const { const auto& X = *ctx->Input(0); const TensorShape& input_shape = X.Shape(); - if (input_shape.NumDimensions() == 0) { - return Status(common::ONNXRUNTIME, common::INVALID_ARGUMENT, - "Input shape needs to be at least a single dimension."); - } + ORT_RETURN_IF_NOT(input_shape.NumDimensions() == 1 || input_shape.NumDimensions() == 2, + "LinearClassifier: input must be 1-D or 2-D, got ", input_shape.NumDimensions(), "-D."); ptrdiff_t num_batches = input_shape.NumDimensions() == 1 ? 1 : narrow(input_shape[0]); ptrdiff_t num_features = input_shape.NumDimensions() == 1 ? narrow( diff --git a/onnxruntime/test/providers/cpu/ml/linearclassifer_test.cc b/onnxruntime/test/providers/cpu/ml/linearclassifer_test.cc index 8083874213b1f..713109eb17746 100644 --- a/onnxruntime/test/providers/cpu/ml/linearclassifer_test.cc +++ b/onnxruntime/test/providers/cpu/ml/linearclassifer_test.cc @@ -126,6 +126,8 @@ TEST(MLOpTest, LinearClassifierBinaryWithLabels) { test.Run(); } +#if !defined(ORT_NO_EXCEPTIONS) +// coefficients size (3) is not a multiple of class_count (2) - caught at construction time. TEST(MLOpTest, LinearClassifierInvalidCoefficientsSize) { OpTester test("LinearClassifier", 1, onnxruntime::kMLDomain); @@ -137,8 +139,10 @@ TEST(MLOpTest, LinearClassifierInvalidCoefficientsSize) { test.AddOutput("Y", {1}, {0}); test.AddOutput("Z", {1, 2}, {0.f, 0.f}); - test.Run(OpTester::ExpectResult::kExpectFailure, "coefficients size"); + test.Run(OpTester::ExpectResult::kExpectFailure, + "coefficients size (3) must be a multiple of the number of classes (2)"); } +#endif // !defined(ORT_NO_EXCEPTIONS) template void LinearClassifierMulticlass() { @@ -222,7 +226,8 @@ TEST(MLOpTest, LinearClassifierExtraCoefficientsAreIgnored) { test.Run(); } -// Regression test: coefficients not divisible by class_count. +#if !defined(ORT_NO_EXCEPTIONS) +// Regression test: coefficients not divisible by class_count - caught at construction time. TEST(MLOpTest, LinearClassifierCoefficientsSizeNotDivisibleByClassCountFails) { OpTester test("LinearClassifier", 1, onnxruntime::kMLDomain); @@ -240,8 +245,9 @@ TEST(MLOpTest, LinearClassifierCoefficientsSizeNotDivisibleByClassCountFails) { test.AddOutput("Z", {1, 3}, {0.f, 0.f, 0.f}); test.Run(OpTester::ExpectResult::kExpectFailure, - "coefficients size (5) is less than class_count (3) * num_features (2)"); + "coefficients size (5) must be a multiple of the number of classes (3)"); } +#endif // !defined(ORT_NO_EXCEPTIONS) TEST(MLOpTest, LinearClassifierInputFeatureCountMismatchFails) { OpTester test("LinearClassifier", 1, onnxruntime::kMLDomain); @@ -262,5 +268,168 @@ TEST(MLOpTest, LinearClassifierInputFeatureCountMismatchFails) { test.Run(OpTester::ExpectResult::kExpectFailure, "coefficients size (6) is less than class_count (3) * num_features (3)"); } + +#if !defined(ORT_NO_EXCEPTIONS) +// Regression test: classlabels_ints has fewer elements than classes defined by intercepts. +TEST(MLOpTest, LinearClassifierClassLabelsIntsTooFewFails) { + OpTester test("LinearClassifier", 1, onnxruntime::kMLDomain); + + // 2 intercepts => class_count = 2, but only 1 class label provided. + std::vector coefficients = {1.f, 2.f, 3.f, 4.f}; + std::vector classes = {42}; + std::vector intercepts = {0.f, 100.f}; + + test.AddAttribute("coefficients", coefficients); + test.AddAttribute("intercepts", intercepts); + test.AddAttribute("classlabels_ints", classes); + + test.AddInput("X", {1, 2}, {1.f, 2.f}); + test.AddOutput("Y", {1}, {0LL}); + test.AddOutput("Z", {1, 2}, {0.f, 0.f}); + + test.Run(OpTester::ExpectResult::kExpectFailure, + "classlabels_ints has 1 elements but intercepts defines 2 classes"); +} + +// Regression test: classlabels_strings has fewer elements than classes defined by intercepts. +TEST(MLOpTest, LinearClassifierClassLabelsStringsTooFewFails) { + OpTester test("LinearClassifier", 1, onnxruntime::kMLDomain); + + // 3 intercepts => class_count = 3, but only 2 class labels provided. + std::vector coefficients = {1.f, 2.f, 3.f, 4.f, 5.f, 6.f}; + std::vector labels = {"cat", "dog"}; + std::vector intercepts = {0.f, 0.f, 100.f}; + + test.AddAttribute("coefficients", coefficients); + test.AddAttribute("intercepts", intercepts); + test.AddAttribute("classlabels_strings", labels); + + test.AddInput("X", {1, 2}, {1.f, 2.f}); + test.AddOutput("Y", {1}, {std::string("cat")}); + test.AddOutput("Z", {1, 3}, {0.f, 0.f, 0.f}); + + test.Run(OpTester::ExpectResult::kExpectFailure, + "classlabels_strings has 2 elements but intercepts defines 3 classes"); +} + +// Regression test: both classlabels_ints and classlabels_strings specified. +TEST(MLOpTest, LinearClassifierBothClassLabelsFails) { + OpTester test("LinearClassifier", 1, onnxruntime::kMLDomain); + + std::vector coefficients = {1.f, 2.f, 3.f, 4.f}; + std::vector intercepts = {0.f, 0.f}; + + test.AddAttribute("coefficients", coefficients); + test.AddAttribute("intercepts", intercepts); + test.AddAttribute("classlabels_ints", std::vector{0, 1}); + test.AddAttribute("classlabels_strings", std::vector{"a", "b"}); + + test.AddInput("X", {1, 2}, {1.f, 2.f}); + test.AddOutput("Y", {1}, {std::string("a")}); + test.AddOutput("Z", {1, 2}, {0.f, 0.f}); + + test.Run(OpTester::ExpectResult::kExpectFailure, + "only one of classlabels_strings or classlabels_ints may be specified"); +} + +// Regression test: multi-class with no classlabels at all would index an empty vector. +TEST(MLOpTest, LinearClassifierMulticlassNoClassLabelsFails) { + OpTester test("LinearClassifier", 1, onnxruntime::kMLDomain); + + // 3 intercepts => class_count = 3, but no classlabels provided. + std::vector coefficients = {1.f, 2.f, 3.f, 4.f, 5.f, 6.f}; + std::vector intercepts = {0.f, 0.f, 0.f}; + + test.AddAttribute("coefficients", coefficients); + test.AddAttribute("intercepts", intercepts); + + test.AddInput("X", {1, 2}, {1.f, 2.f}); + test.AddOutput("Y", {1}, {0LL}); + test.AddOutput("Z", {1, 3}, {0.f, 0.f, 0.f}); + + test.Run(OpTester::ExpectResult::kExpectFailure, + "classlabels_ints or classlabels_strings must be provided"); +} + +// Regression test: binary classification with 1 label (not 2) should be rejected. +TEST(MLOpTest, LinearClassifierBinaryWrongLabelCountFails) { + OpTester test("LinearClassifier", 1, onnxruntime::kMLDomain); + + // 1 intercept => binary, but only 1 label instead of required 2. + std::vector coefficients = {1.f, 2.f}; + std::vector intercepts = {0.f}; + + test.AddAttribute("coefficients", coefficients); + test.AddAttribute("intercepts", intercepts); + test.AddAttribute("classlabels_ints", std::vector{42}); + + test.AddInput("X", {1, 2}, {1.f, 2.f}); + test.AddOutput("Y", {1}, {0LL}); + test.AddOutput("Z", {1, 1}, {0.f}); + + test.Run(OpTester::ExpectResult::kExpectFailure, + "classlabels_ints must have exactly 2 elements for binary classification"); +} + +// Regression test: binary classification with 3 string labels should be rejected. +TEST(MLOpTest, LinearClassifierBinaryTooManyStringLabelsFails) { + OpTester test("LinearClassifier", 1, onnxruntime::kMLDomain); + + std::vector coefficients = {1.f, 2.f}; + std::vector intercepts = {0.f}; + + test.AddAttribute("coefficients", coefficients); + test.AddAttribute("intercepts", intercepts); + test.AddAttribute("classlabels_strings", std::vector{"a", "b", "c"}); + + test.AddInput("X", {1, 2}, {1.f, 2.f}); + test.AddOutput("Y", {1}, {std::string("a")}); + test.AddOutput("Z", {1, 1}, {0.f}); + + test.Run(OpTester::ExpectResult::kExpectFailure, + "classlabels_strings must have exactly 2 elements for binary classification"); +} +#endif // !defined(ORT_NO_EXCEPTIONS) + +// Input must be 1-D or 2-D. 3-D input should fail at runtime. +TEST(MLOpTest, LinearClassifierInput3DFails) { + OpTester test("LinearClassifier", 1, onnxruntime::kMLDomain); + + std::vector coefficients = {1.f, 2.f}; + std::vector intercepts = {0.f}; + std::vector classes = {0, 1}; + + test.AddAttribute("coefficients", coefficients); + test.AddAttribute("intercepts", intercepts); + test.AddAttribute("classlabels_ints", classes); + + // Bypass ONNX shape inference so we exercise the kernel's own runtime rank validation. + test.AddShapeToTensorData(false); + test.AddInput("X", {1, 1, 2}, {1.f, 2.f}); + test.AddOutput("Y", {1}, {0LL}); + test.AddOutput("Z", {1, 2}, {0.f, 0.f}); + + test.Run(OpTester::ExpectResult::kExpectFailure, "input must be 1-D or 2-D"); +} + +// 1-D input should be treated as [1, C]. +TEST(MLOpTest, LinearClassifier1DInput) { + OpTester test("LinearClassifier", 1, onnxruntime::kMLDomain); + + // 1 intercept => binary, 2 features + std::vector coefficients = {1.f, -1.f}; + std::vector intercepts = {0.f}; + + test.AddAttribute("coefficients", coefficients); + test.AddAttribute("intercepts", intercepts); + test.AddAttribute("classlabels_ints", std::vector{10, 20}); + + // Input [2] treated as [1,2]. score = 1*1 + (-1)*2 + 0 = -1 < 0 => class 0 (label 10) + test.AddInput("X", {2}, {1.f, 2.f}); + test.AddOutput("Y", {1}, {10LL}); + test.AddOutput("Z", {1, 2}, {2.f, -1.f}); + test.Run(); +} + } // namespace test } // namespace onnxruntime