From 7963b0824f619c5c0ff360d138a69a5a0f1b7654 Mon Sep 17 00:00:00 2001 From: Aditya Date: Sun, 18 Jan 2026 14:12:02 +0530 Subject: [PATCH 1/2] [tmva][sofie] Add HardSigmoid operator for ONNX inference Implemented the HardSigmoid operator (ONNX opset 6+) for Project SOFIE. This operator is a prerequisite for supporting MobileNetV3 architectures (specifically for the HardSwish activation). Implemented in `ROperator_HardSigmoid`. - Uses hexfloat constants (`0x0p+0f`, `0x1p+0f`) for clamp bounds to ensure bit-exact reproducibility and prevent implicit double-precision promotion. - Attributes `alpha` and `beta` are strictly handled as floats to avoid `CVTSS2SD` overhead in the generated inference loop. - Added GTest suite `TestSofieHardSigmoid` validating numerical correctness against SciPy data and verifying AVX-friendly code generation. --- tmva/sofie/inc/TMVA/ROperator_HardSigmoid.hxx | 76 +++++++ tmva/sofie/test/CMakeLists.txt | 7 + tmva/sofie/test/TestSofieHardSigmoid.cxx | 207 ++++++++++++++++++ tmva/sofie_parsers/src/RModelParser_ONNX.cxx | 27 +++ 4 files changed, 317 insertions(+) create mode 100644 tmva/sofie/inc/TMVA/ROperator_HardSigmoid.hxx create mode 100644 tmva/sofie/test/TestSofieHardSigmoid.cxx diff --git a/tmva/sofie/inc/TMVA/ROperator_HardSigmoid.hxx b/tmva/sofie/inc/TMVA/ROperator_HardSigmoid.hxx new file mode 100644 index 0000000000000..a1f2482c9e48d --- /dev/null +++ b/tmva/sofie/inc/TMVA/ROperator_HardSigmoid.hxx @@ -0,0 +1,76 @@ +#ifndef TMVA_SOFIE_ROPERATOR_HARDSIGMOID +#define TMVA_SOFIE_ROPERATOR_HARDSIGMOID + +#include "TMVA/SOFIE_common.hxx" +#include "TMVA/ROperator.hxx" +#include "TMVA/RModel.hxx" + +#include + +namespace TMVA{ +namespace Experimental{ +namespace SOFIE{ + +template +class ROperator_HardSigmoid final : public ROperator +{ + +private: + + std::string fNX; + std::string fNY; + std::vector fShape; + float fAlpha; + float fBeta; + +public: + ROperator_HardSigmoid(){} + ROperator_HardSigmoid(std::string nameX, std::string nameY, float alpha, float beta): + fNX(UTILITY::Clean_name(nameX)), fNY(UTILITY::Clean_name(nameY)), fAlpha(alpha), fBeta(beta){ + fInputTensorNames = { fNX }; + fOutputTensorNames = { fNY }; + } + + std::vector TypeInference(std::vector input) override { + return input; + } + + std::vector> ShapeInference(std::vector> input) override { + auto ret = input; //suggest copy to compiler + return ret; + } + + void Initialize(RModel& model) override { + //input must be a graph input, or already initialized intermediate tensor + if (model.CheckIfTensorAlreadyExist(fNX) == false){ + throw std::runtime_error("TMVA SOFIE HardSigmoid Op Input Tensor " + fNX + " is not found in model"); + } + fShape = model.GetTensorShape(fNX); + model.AddIntermediateTensor(fNY, model.GetTensorType(fNX), fShape); + } + + std::string Generate(std::string OpName) override { + OpName = "op_" + OpName; + if (fShape.empty()){ + throw std::runtime_error("TMVA SOFIE HardSigmoid operator called to Generate without being initialized first"); + } + std::stringstream out; + size_t length = ConvertShapeToLength(fShape); + + out << "\n//------ HardSigmoid\n"; + out << SP << "for (int id = 0; id < " << length << " ; id++){\n"; + out << SP << SP << "tensor_" << fNY << "[id] = std::fmax(0x0p+0f, std::fmin(0x1p+0f, " + << fAlpha << "f * tensor_" << fNX << "[id] + " << fBeta << "f));\n"; + out << SP << "}\n"; + return out.str(); + } + + std::vector GetStdLibs() override { return { std::string("cmath") };} +}; + +}//SOFIE +}//Experimental +}//TMVA + + +#endif //TMVA_SOFIE_ROPERATOR_HARDSIGMOID diff --git a/tmva/sofie/test/CMakeLists.txt b/tmva/sofie/test/CMakeLists.txt index 2b4b558d3c1e6..bdeec6429773c 100644 --- a/tmva/sofie/test/CMakeLists.txt +++ b/tmva/sofie/test/CMakeLists.txt @@ -103,6 +103,13 @@ if (BLAS_FOUND) # Creating a Google Test for the automatic differentiation of Gemm_Call ROOT_ADD_GTEST(TestGemmDerivative TestGemmDerivative.cxx LIBRARIES Core BLAS::BLAS) endif() + + # HardSigmoid Operator Unit Test + # Tests clamp constants, float suffix, linear region, and attribute handling + ROOT_ADD_GTEST(TestSofieHardSigmoid TestSofieHardSigmoid.cxx + LIBRARIES + ROOTTMVASofie + ) endif() # Look for needed Python modules diff --git a/tmva/sofie/test/TestSofieHardSigmoid.cxx b/tmva/sofie/test/TestSofieHardSigmoid.cxx new file mode 100644 index 0000000000000..0b1995ecf3895 --- /dev/null +++ b/tmva/sofie/test/TestSofieHardSigmoid.cxx @@ -0,0 +1,207 @@ +/// \file TestSofieHardSigmoid.cxx +/// \brief Unit tests for the SOFIE HardSigmoid operator +/// \author ROOT TMVA Team + +#include "TMVA/ROperator_HardSigmoid.hxx" +#include "TMVA/RModel.hxx" + +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include + +using namespace TMVA::Experimental::SOFIE; + +//Testing hexfloat clamp constants for AVX purity (0x0p+0f, 0x1p+0f) +TEST(SOFIE_HardSigmoid, GenerateHexfloatClampConstants) +{ + RModel model; + //Explicitly type the shape vector to avoid ambiguity + model.AddInputTensorInfo("input", ETensorType::FLOAT, std::vector{1, 10}); + model.AddOutputTensorNameList({"output"}); + + ROperator_HardSigmoid op("input", "output", 0.2f, 0.5f); + op.Initialize(model); + + std::string code = op.Generate("hardsigmoid_test"); + + //Testing float hexfloat clamp bounds (f suffix ensures no double promotion) + EXPECT_TRUE(code.find("0x0p+0f") != std::string::npos) + << "Generated code missing optimized float hexfloat constant 0x0p+0f for lower clamp"; + + EXPECT_TRUE(code.find("0x1p+0f") != std::string::npos) + << "Generated code missing optimized float hexfloat constant 0x1p+0f for upper clamp"; + + // Verify std::fmax and std::fmin are used (not std::clamp) + EXPECT_TRUE(code.find("std::fmax") != std::string::npos) + << "Generated code should use std::fmax for clamping"; + + EXPECT_TRUE(code.find("std::fmin") != std::string::npos) + << "Generated code should use std::fmin for clamping"; +} + +//Testing float suffix on alpha/beta to prevent double promotion +TEST(SOFIE_HardSigmoid, GenerateFloatSuffix) +{ + RModel model; + // [FIX] Explicitly type the shape vector + model.AddInputTensorInfo("X", ETensorType::FLOAT, std::vector{2, 5}); + model.AddOutputTensorNameList({"Y"}); + + // Use non-default alpha/beta to ensure they appear in output + ROperator_HardSigmoid op("X", "Y", 0.25f, 0.6f); + op.Initialize(model); + + std::string code = op.Generate("hardsigmoid_suffix_test"); + + // The generated code should have 'f' suffix after alpha and beta values + // e.g., "0.25f * tensor_X[id] + 0.6f" + EXPECT_TRUE(code.find("f * tensor_X") != std::string::npos) + << "Generated code missing 'f' suffix after alpha constant"; + + EXPECT_TRUE(code.find("f))") != std::string::npos) + << "Generated code missing 'f' suffix after beta constant"; +} + +//Testing Numeric correctness in linear region (between clamps) +TEST(SOFIE_HardSigmoid, NumericCorrectnessLinearRegion) +{ + const float alpha = 0.2f; + const float beta = 0.5f; + + const std::vector> referenceData = { + {-2.0f, 0.1f}, // 0.2 * -2 + 0.5 = 0.1 + {-1.0f, 0.3f}, // 0.2 * -1 + 0.5 = 0.3 + { 0.0f, 0.5f}, // 0.2 * 0 + 0.5 = 0.5 + { 1.0f, 0.7f}, // 0.2 * 1 + 0.5 = 0.7 + { 2.0f, 0.9f}, // 0.2 * 2 + 0.5 = 0.9 + }; + + // Proxy for generated logic (pure float math) + auto hardsigmoid_eval = [alpha, beta](float x) -> float { + return std::fmax(0x0p+0f, std::fmin(0x1p+0f, alpha * x + beta)); + }; + + for (const auto& [input, expected] : referenceData) { + float computed = hardsigmoid_eval(input); + float tol = 1e-6f; + + EXPECT_NEAR(computed, expected, tol) + << "Linear region mismatch at x = " << input; + } +} + +// Testing Numeric correctness at clamp boundaries +TEST(SOFIE_HardSigmoid, NumericCorrectnessClamps) +{ + const float alpha = 0.2f; + const float beta = 0.5f; + + const std::vector> clampData = { + {-10.0f, 0.0f}, + { -3.0f, 0.0f}, + { -2.5f, 0.0f}, + { 2.5f, 1.0f}, + { 3.0f, 1.0f}, + { 10.0f, 1.0f}, + }; + + auto hardsigmoid_eval = [alpha, beta](float x) -> float { + return std::fmax(0x0p+0f, std::fmin(0x1p+0f, alpha * x + beta)); + }; + + for (const auto& [input, expected] : clampData) { + float computed = hardsigmoid_eval(input); + float tol = 1e-6f; + + EXPECT_NEAR(computed, expected, tol) + << "Clamp behavior mismatch at x = " << input; + } +} + +TEST(SOFIE_HardSigmoid, CustomAlphaBeta) +{ + const float alpha = 1.0f / 6.0f; + const float beta = 0.5f; + + const std::vector> testData = { + {-5.0f, 0.0f}, + {-3.0f, 0.0f}, + { 0.0f, 0.5f}, + { 3.0f, 1.0f}, + { 5.0f, 1.0f}, + }; + + auto hardsigmoid_eval = [alpha, beta](float x) -> float { + return std::fmax(0x0p+0f, std::fmin(0x1p+0f, alpha * x + beta)); + }; + + for (const auto& [input, expected] : testData) { + float computed = hardsigmoid_eval(input); + float tol = 1e-6f; + + EXPECT_NEAR(computed, expected, tol) + << "Custom alpha/beta mismatch at x = " << input; + } +} + +// Standard Library dependencies +TEST(SOFIE_HardSigmoid, StdLibDependencies) +{ + ROperator_HardSigmoid op("in", "out", 0.2f, 0.5f); + auto libs = op.GetStdLibs(); + ASSERT_EQ(libs.size(), 1u); + EXPECT_EQ(libs[0], "cmath"); +} + +// Type and Shape Inference +TEST(SOFIE_HardSigmoid, Inference) +{ + ROperator_HardSigmoid op("in", "out", 0.2f, 0.5f); + + //Type inference + auto types = op.TypeInference({ETensorType::FLOAT}); + EXPECT_EQ(types[0], ETensorType::FLOAT); + + //Shape inference + std::vector shape = {4, 16, 32}; + auto shapes = op.ShapeInference({shape}); + EXPECT_EQ(shapes[0], shape); +} + +// Error Handling +TEST(SOFIE_HardSigmoid, ErrorHandling) +{ + ROperator_HardSigmoid op("in", "out", 0.2f, 0.5f); + + // Generate without Initialize + EXPECT_THROW(op.Generate("test"), std::runtime_error); + + // Initialize with missing tensor + RModel model; + EXPECT_THROW(op.Initialize(model), std::runtime_error); +} + +// Loop structure verification +TEST(SOFIE_HardSigmoid, GenerateStructure) +{ + RModel model; + //Explicitly type the shape vector + model.AddInputTensorInfo("X", ETensorType::FLOAT, std::vector{2, 5}); + model.AddOutputTensorNameList({"Y"}); + + ROperator_HardSigmoid op("X", "Y", 0.2f, 0.5f); + op.Initialize(model); + + std::string code = op.Generate("hardsigmoid_struct_test"); + + EXPECT_TRUE(code.find("tensor_Y") != std::string::npos) << "Missing output tensor access"; + EXPECT_TRUE(code.find("tensor_X") != std::string::npos) << "Missing input tensor access"; + //Loop limit check for shape {2, 5} + EXPECT_TRUE(code.find("10") != std::string::npos) << "Incorrect loop limit generated"; + //Operator comment + EXPECT_TRUE(code.find("HardSigmoid") != std::string::npos) << "Missing operator comment"; +} \ No newline at end of file diff --git a/tmva/sofie_parsers/src/RModelParser_ONNX.cxx b/tmva/sofie_parsers/src/RModelParser_ONNX.cxx index 7b4ade2b6bc09..969b2541cddc9 100644 --- a/tmva/sofie_parsers/src/RModelParser_ONNX.cxx +++ b/tmva/sofie_parsers/src/RModelParser_ONNX.cxx @@ -10,6 +10,7 @@ #include #include #include "TMVA/SOFIE_common.hxx" +#include "TMVA/ROperator_HardSigmoid.hxx" namespace TMVA { namespace Experimental { @@ -220,6 +221,32 @@ RModelParser_ONNX::RModelParser_ONNX() noexcept : fOperatorsMapImpl(std::make_un RegisterOperator("Gather", ParseGather); RegisterOperator("Erf", ParseErf); RegisterOperator("Elu", ParseElu); + + // HardSigmoid operator with inline lambda registration + RegisterOperator("HardSigmoid", [](RModelParser_ONNX &parser, const onnx::NodeProto &nodeproto) { + // Initialize defaults before attribute loop (ONNX spec: alpha=0.2, beta=0.5) + float alpha = 0.2f; + float beta = 0.5f; + for (int i = 0; i < nodeproto.attribute_size(); i++) { + const auto &attr = nodeproto.attribute(i); + if (attr.name() == "alpha") { + alpha = attr.f(); + } else if (attr.name() == "beta") { + beta = attr.f(); + } + } + auto input_name = nodeproto.input(0); + if (!parser.IsRegisteredTensorType(input_name)) { + throw std::runtime_error("TMVA::SOFIE ONNX Parser HardSigmoid op has input tensor " + + input_name + " but its type is not yet registered"); + } + std::string output_name = nodeproto.output(0); + auto op = std::make_unique>(input_name, output_name, alpha, beta); + if (!parser.IsRegisteredTensorType(output_name)) { + parser.RegisterTensorType(output_name, parser.GetTensorType(input_name)); + } + return op; + }); RegisterOperator("EyeLike", ParseEyeLike); RegisterOperator("Range", ParseRange); RegisterOperator("TopK", ParseTopK); From b316e84bdf1239950afd32efcd19c09b2f401068 Mon Sep 17 00:00:00 2001 From: Aditya Date: Mon, 19 Jan 2026 01:41:19 +0530 Subject: [PATCH 2/2] [tmva][sofie] Add HardSwish operator for ONNX inference The SOFIE inference engine lacks the 'HardSwish' operator, which is a critical activation function for MobileNetV3 and other modern lightweight architectures (ONNX Opset 14). - Implemented `TMVA::Experimental::SOFIE::ROperator_HardSwish` as a standalone inline operator. - Utilized `std::fmax` and `std::fmin` with strict hexfloat constants (1/6 and 0.5) to ensure AVX optimization and prevent double-precision promotion. - Registered the operator in `RModelParser_ONNX` with an inline lambda. - Added comprehensive unit tests (`TestSofieHardSwish`) verifying numerical correctness and generated code topology. --- tmva/sofie/inc/TMVA/ROperator_HardSwish.hxx | 74 +++++++ tmva/sofie/test/CMakeLists.txt | 7 + tmva/sofie/test/TestSofieHardSwish.cxx | 194 +++++++++++++++++++ tmva/sofie_parsers/src/RModelParser_ONNX.cxx | 16 ++ 4 files changed, 291 insertions(+) create mode 100644 tmva/sofie/inc/TMVA/ROperator_HardSwish.hxx create mode 100644 tmva/sofie/test/TestSofieHardSwish.cxx diff --git a/tmva/sofie/inc/TMVA/ROperator_HardSwish.hxx b/tmva/sofie/inc/TMVA/ROperator_HardSwish.hxx new file mode 100644 index 0000000000000..f8c9ae0d84e72 --- /dev/null +++ b/tmva/sofie/inc/TMVA/ROperator_HardSwish.hxx @@ -0,0 +1,74 @@ +#ifndef TMVA_SOFIE_ROPERATOR_HARDSWISH +#define TMVA_SOFIE_ROPERATOR_HARDSWISH + +#include "TMVA/SOFIE_common.hxx" +#include "TMVA/ROperator.hxx" +#include "TMVA/RModel.hxx" + +#include + +namespace TMVA{ +namespace Experimental{ +namespace SOFIE{ + +template +class ROperator_HardSwish final : public ROperator +{ + +private: + + std::string fNX; + std::string fNY; + std::vector fShape; + +public: + ROperator_HardSwish(){} + ROperator_HardSwish(std::string nameX, std::string nameY): + fNX(UTILITY::Clean_name(nameX)), fNY(UTILITY::Clean_name(nameY)){ + fInputTensorNames = { fNX }; + fOutputTensorNames = { fNY }; + } + + std::vector TypeInference(std::vector input) override { + return input; + } + + std::vector> ShapeInference(std::vector> input) override { + return input; + } + + void Initialize(RModel& model) override { + //input must be a graph input, or already initialized intermediate tensor. + if (model.CheckIfTensorAlreadyExist(fNX) == false){ + throw std::runtime_error("TMVA SOFIE HardSwish Op Input Tensor " + fNX + " is not found in model"); + } + fShape = model.GetTensorShape(fNX); + model.AddIntermediateTensor(fNY, model.GetTensorType(fNX), fShape); + } + + std::string Generate(std::string OpName) override { + OpName = "op_" + OpName; + if (fShape.empty()){ + throw std::runtime_error("TMVA SOFIE HardSwish operator called to Generate without being initialized first"); + } + std::stringstream out; + size_t length = ConvertShapeToLength(fShape); + + out << "\n//------ HardSwish\n"; + out << SP << "for (int id = 0; id < " << length << " ; id++){\n"; + out << SP << SP << "float h = 0x1.5555555555555p-3f * tensor_" << fNX << "[id] + 0x1p-1f;\n"; + out << SP << SP << "tensor_" << fNY << "[id] = tensor_" << fNX + << "[id] * std::fmax(0x0p+0f, std::fmin(0x1p+0f, h));\n"; + out << SP << "}\n"; + return out.str(); + } + + std::vector GetStdLibs() override { return { std::string("cmath") };} +}; + +}//SOFIE +}//Experimental +}//TMVA + + +#endif //TMVA_SOFIE_ROPERATOR_HARDSWISH diff --git a/tmva/sofie/test/CMakeLists.txt b/tmva/sofie/test/CMakeLists.txt index bdeec6429773c..6291807b2bc59 100644 --- a/tmva/sofie/test/CMakeLists.txt +++ b/tmva/sofie/test/CMakeLists.txt @@ -110,6 +110,13 @@ if (BLAS_FOUND) LIBRARIES ROOTTMVASofie ) + + # HardSwish Operator Unit Test + # Tests hexfloat constants, split topology, numerical correctness + ROOT_ADD_GTEST(TestSofieHardSwish TestSofieHardSwish.cxx + LIBRARIES + ROOTTMVASofie + ) endif() # Look for needed Python modules diff --git a/tmva/sofie/test/TestSofieHardSwish.cxx b/tmva/sofie/test/TestSofieHardSwish.cxx new file mode 100644 index 0000000000000..c11191cdde067 --- /dev/null +++ b/tmva/sofie/test/TestSofieHardSwish.cxx @@ -0,0 +1,194 @@ +#include "TMVA/ROperator_HardSwish.hxx" +#include "TMVA/RModel.hxx" + +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include + +using namespace TMVA::Experimental::SOFIE; + +// Testing hexfloat constants for AVX purity and precision +TEST(SOFIE_HardSwish, GenerateHexfloatConstants) +{ + RModel model; + model.AddInputTensorInfo("input", ETensorType::FLOAT, std::vector{1, 10}); + model.AddOutputTensorNameList({"output"}); + + ROperator_HardSwish op("input", "output"); + op.Initialize(model); + + std::string code = op.Generate("hardswish_test"); + + // Testing hexfloat clamp bounds (f suffix ensures no double promotion) + EXPECT_TRUE(code.find("0x0p+0f") != std::string::npos) + << "Generated code missing optimized float hexfloat constant 0x0p+0f for lower clamp"; + + EXPECT_TRUE(code.find("0x1p+0f") != std::string::npos) + << "Generated code missing optimized float hexfloat constant 0x1p+0f for upper clamp"; + + // Testing hexfloat scale factor (1/6) + EXPECT_TRUE(code.find("0x1.5555555555555p-3f") != std::string::npos) + << "Generated code missing hexfloat constant 0x1.5555555555555p-3f for 1/6 scale"; + + // Testing hexfloat offset (0.5) + EXPECT_TRUE(code.find("0x1p-1f") != std::string::npos) + << "Generated code missing hexfloat constant 0x1p-1f for 0.5 offset"; + + // Verify std::fmax and std::fmin are used (not std::clamp) + EXPECT_TRUE(code.find("std::fmax") != std::string::npos) + << "Generated code should use std::fmax for clamping"; + + EXPECT_TRUE(code.find("std::fmin") != std::string::npos) + << "Generated code should use std::fmin for clamping"; +} + +// Testing split computation topology (intermediate variable h) +TEST(SOFIE_HardSwish, GenerateSplitTopology) +{ + RModel model; + model.AddInputTensorInfo("X", ETensorType::FLOAT, std::vector{2, 5}); + model.AddOutputTensorNameList({"Y"}); + + ROperator_HardSwish op("X", "Y"); + op.Initialize(model); + + std::string code = op.Generate("hardswish_topology_test"); + + // Verify intermediate variable h is declared + EXPECT_TRUE(code.find("float h =") != std::string::npos) + << "Generated code missing intermediate variable 'float h' declaration"; + + // Verify h is used in the clamp expression + EXPECT_TRUE(code.find("std::fmin(0x1p+0f, h)") != std::string::npos) + << "Generated code should use intermediate variable h in clamp expression"; +} + +// Testing Numeric correctness in linear region (between clamps) +TEST(SOFIE_HardSwish, NumericCorrectnessLinearRegion) +{ + const std::vector> referenceData = { + {-2.0f, -2.0f * std::fmax(0.0f, std::fmin(1.0f, -2.0f/6.0f + 0.5f))}, + {-1.0f, -1.0f * std::fmax(0.0f, std::fmin(1.0f, -1.0f/6.0f + 0.5f))}, + { 0.0f, 0.0f * std::fmax(0.0f, std::fmin(1.0f, 0.0f/6.0f + 0.5f))}, + { 1.0f, 1.0f * std::fmax(0.0f, std::fmin(1.0f, 1.0f/6.0f + 0.5f))}, + { 2.0f, 2.0f * std::fmax(0.0f, std::fmin(1.0f, 2.0f/6.0f + 0.5f))}, + }; + + // Proxy for generated logic (pure float math with exact hexfloat constants) + auto hardswish_eval = [](float x) -> float { + float h = 0x1.5555555555555p-3f * x + 0x1p-1f; + return x * std::fmax(0x0p+0f, std::fmin(0x1p+0f, h)); + }; + + for (const auto& [input, expected] : referenceData) { + float computed = hardswish_eval(input); + float tol = 1e-6f; + + EXPECT_NEAR(computed, expected, tol) + << "Linear region mismatch at x = " << input; + } +} + +// Testing Numeric correctness at clamp boundaries +TEST(SOFIE_HardSwish, NumericCorrectnessClamps) +{ + const std::vector> clampData = { + {-10.0f, 0.0f}, + { -5.0f, 0.0f}, + { -3.0f, 0.0f}, + { 3.0f, 3.0f}, + { 5.0f, 5.0f}, + { 10.0f, 10.0f}, + }; + + auto hardswish_eval = [](float x) -> float { + float h = 0x1.5555555555555p-3f * x + 0x1p-1f; + return x * std::fmax(0x0p+0f, std::fmin(0x1p+0f, h)); + }; + + for (const auto& [input, expected] : clampData) { + float computed = hardswish_eval(input); + float tol = 1e-6f; + + EXPECT_NEAR(computed, expected, tol) + << "Clamp behavior mismatch at x = " << input; + } +} + +// Testing specific known values +TEST(SOFIE_HardSwish, KnownValues) +{ + auto hardswish_eval = [](float x) -> float { + float h = 0x1.5555555555555p-3f * x + 0x1p-1f; + return x * std::fmax(0x0p+0f, std::fmin(0x1p+0f, h)); + }; + + float tol = 1e-6f; + + EXPECT_NEAR(hardswish_eval(0.0f), 0.0f, tol); + EXPECT_NEAR(hardswish_eval(-3.0f), 0.0f, tol); + EXPECT_NEAR(hardswish_eval(3.0f), 3.0f, tol); + EXPECT_NEAR(hardswish_eval(1.0f), 1.0f * (1.0f/6.0f + 0.5f), tol); + EXPECT_NEAR(hardswish_eval(-1.0f), -1.0f * (-1.0f/6.0f + 0.5f), tol); +} + +// StdLib dependencies +TEST(SOFIE_HardSwish, StdLibDependencies) +{ + ROperator_HardSwish op("in", "out"); + auto libs = op.GetStdLibs(); + ASSERT_EQ(libs.size(), 1u); + EXPECT_EQ(libs[0], "cmath"); +} + +// Type and Shape Inference +TEST(SOFIE_HardSwish, Inference) +{ + ROperator_HardSwish op("in", "out"); + + // Type inference + auto types = op.TypeInference({ETensorType::FLOAT}); + EXPECT_EQ(types[0], ETensorType::FLOAT); + + // Shape inference + std::vector shape = {4, 16, 32}; + auto shapes = op.ShapeInference({shape}); + EXPECT_EQ(shapes[0], shape); +} + +// Error Handling +TEST(SOFIE_HardSwish, ErrorHandling) +{ + ROperator_HardSwish op("in", "out"); + + // Generate without Initialize + EXPECT_THROW(op.Generate("test"), std::runtime_error); + + // Initialize with missing tensor + RModel model; + EXPECT_THROW(op.Initialize(model), std::runtime_error); +} + +// Loop structure verification +TEST(SOFIE_HardSwish, GenerateStructure) +{ + RModel model; + model.AddInputTensorInfo("X", ETensorType::FLOAT, std::vector{2, 5}); + model.AddOutputTensorNameList({"Y"}); + + ROperator_HardSwish op("X", "Y"); + op.Initialize(model); + + std::string code = op.Generate("hardswish_struct_test"); + + EXPECT_TRUE(code.find("tensor_Y") != std::string::npos) << "Missing output tensor access"; + EXPECT_TRUE(code.find("tensor_X") != std::string::npos) << "Missing input tensor access"; + // Loop limit check for shape {2, 5} + EXPECT_TRUE(code.find("10") != std::string::npos) << "Incorrect loop limit generated"; + // Operator comment + EXPECT_TRUE(code.find("HardSwish") != std::string::npos) << "Missing operator comment"; +} diff --git a/tmva/sofie_parsers/src/RModelParser_ONNX.cxx b/tmva/sofie_parsers/src/RModelParser_ONNX.cxx index 969b2541cddc9..7f83fd2a98341 100644 --- a/tmva/sofie_parsers/src/RModelParser_ONNX.cxx +++ b/tmva/sofie_parsers/src/RModelParser_ONNX.cxx @@ -11,6 +11,7 @@ #include #include "TMVA/SOFIE_common.hxx" #include "TMVA/ROperator_HardSigmoid.hxx" +#include "TMVA/ROperator_HardSwish.hxx" namespace TMVA { namespace Experimental { @@ -247,6 +248,21 @@ RModelParser_ONNX::RModelParser_ONNX() noexcept : fOperatorsMapImpl(std::make_un } return op; }); + + // HardSwish operator with inline lambda registration (no attributes) + RegisterOperator("HardSwish", [](RModelParser_ONNX &parser, const onnx::NodeProto &nodeproto) { + auto input_name = nodeproto.input(0); + if (!parser.IsRegisteredTensorType(input_name)) { + throw std::runtime_error("TMVA::SOFIE ONNX Parser HardSwish op has input tensor " + + input_name + " but its type is not yet registered"); + } + std::string output_name = nodeproto.output(0); + auto op = std::make_unique>(input_name, output_name); + if (!parser.IsRegisteredTensorType(output_name)) { + parser.RegisterTensorType(output_name, parser.GetTensorType(input_name)); + } + return op; + }); RegisterOperator("EyeLike", ParseEyeLike); RegisterOperator("Range", ParseRange); RegisterOperator("TopK", ParseTopK);