From 390dae469783f462b36da52260ec2580d30f4860 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Mon, 20 Jul 2026 20:58:05 +0000 Subject: [PATCH 1/6] [tmva][sofie] Fix AveragePool divisor for partial ceil_mode windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The divisor of an AveragePool window was computed at run time only when count_include_pad == 0 && doPadding, and was the compile-time kernel area otherwise. A window can also be partial because of ceil_mode, which lets the last window overhang the input, so that case was averaged over the full kernel area instead of over the cells the window actually covers. Compute the divisor as the kernel extent clipped to the input ([0, size) for count_include_pad = 0, [-padBegin, size + padEnd) otherwise) whenever there is padding or ceil_mode is set, and keep the constexpr kernel area when every window is full, so ordinary pooling generates the same code as before. This replaces the counter that was incremented in the innermost loop. MaxPool is unaffected. Cells that overhang past the padded input are never counted, matching the ONNX reference implementation and PyTorch. Note that ONNX Runtime differs here for count_include_pad = 1, where it divides by the full kernel area. For the 1D example of the issue (input [1..6], kernel 3, stride 2, ceil_mode 1) the generated code now returns [2, 4, 5.5] instead of [2, 4, 3.6667]. Adds AveragePool tests for ceil_mode in 1D/2D/3D, for ceil_mode combined with count_include_pad, and for pads combined with count_include_pad. The four ceil_mode ones fail without this fix. Closes #22523 🤖 Done with the help of AI. --- tmva/sofie/inc/TMVA/ROperator_Pool.hxx | 48 ++++--- tmva/sofie/test/TestCustomModelsFromONNX.cxx | 45 ++++++ tmva/sofie/test/generate_input_models.py | 137 +++++++++++++++++++ 3 files changed, 212 insertions(+), 18 deletions(-) diff --git a/tmva/sofie/inc/TMVA/ROperator_Pool.hxx b/tmva/sofie/inc/TMVA/ROperator_Pool.hxx index 6f142808d8e3d..3f8d6dbc90773 100644 --- a/tmva/sofie/inc/TMVA/ROperator_Pool.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_Pool.hxx @@ -331,6 +331,21 @@ public: for ( auto & e : fAttrPads) doPadding |= (e > 0); + // An AveragePool window can cover fewer cells than the kernel area either because it overlaps the + // padding region or because ceil_mode lets the last window overhang the input. In both cases the + // divisor has to be computed per window instead of being the constant kernel area. + bool dynamicDivisor = doPadding || fAttrCeilMode; + // Number of cells the window starting at "var" covers along one dimension: the kernel extent + // clipped to [0, size) when count_include_pad = 0, and to the padded input [-padBegin, size + + // padEnd) otherwise. The lower bound needs no clipping in the latter case, since "var" starts at + // -padBegin. Note that the cells the window overhangs past the padded input are never counted. + auto windowExtent = [this](const std::string &var, const std::string &kern, size_t size, size_t padEnd) { + std::string hi = std::to_string(fAttrCountIncludePad ? size + padEnd : size); + std::string lo = fAttrCountIncludePad ? var : "(" + var + " > 0 ? " + var + " : 0)"; + return "((" + var + " + " + kern + " < " + hi + " ? " + var + " + " + kern + " : " + hi + ")" + " - " + lo + ")"; + }; + if(fDim==1){ // loop on batches and channels @@ -343,9 +358,10 @@ public: out << SP << SP << SP << SP << "float value = -INFINITY;\n"; else if (fPoolMode == AveragePool) { out << SP << SP << SP << SP << "float value = 0;\n"; - if (fAttrCountIncludePad == 0 && doPadding) - out << SP << SP << SP << SP << "int nsum = 0;\n"; - else // in case we count the pad values in average + if (dynamicDivisor) + out << SP << SP << SP << SP << "const int nsum = " + << windowExtent("i", "kh", fShapeX[2], fAttrPads[fDim]) << ";\n"; + else // every window is full, so the divisor is the kernel area out << SP << SP << SP << SP << "constexpr int nsum = kh;\n"; } // loop on rows of filtered region @@ -359,9 +375,6 @@ public: else if (fPoolMode == AveragePool) { // compute sum of values out << SP << SP << SP << SP << SP << SP << "value += tensor_" << fNX << "[index];\n"; - if (fAttrCountIncludePad == 0 && doPadding) - // compute number of elements used for the average - out << SP << SP << SP << SP << SP << SP << "nsum++;\n"; } out << SP << SP << SP << SP << SP << "}\n"; // end loop on region elements if (fPoolMode == AveragePool) { @@ -386,9 +399,11 @@ public: out << SP << SP << SP << SP << "float value = -INFINITY;\n"; else if (fPoolMode == AveragePool) { out << SP << SP << SP << SP << "float value = 0;\n"; - if (fAttrCountIncludePad == 0 && doPadding) - out << SP << SP << SP << SP << "int nsum = 0;\n"; - else // in case we count the pad values in average + if (dynamicDivisor) + out << SP << SP << SP << SP << "const int nsum = " + << windowExtent("i", "kh", fShapeX[2], fAttrPads[fDim]) << " * " + << windowExtent("j", "kw", fShapeX[3], fAttrPads[fDim + 1]) << ";\n"; + else // every window is full, so the divisor is the kernel area out << SP << SP << SP << SP << "constexpr int nsum = kw*kh;\n"; } // loop on rows of filtered region @@ -405,9 +420,6 @@ public: else if (fPoolMode == AveragePool) { // compute sum of values out << SP << SP << SP << SP << SP << SP << SP << "value += tensor_" << fNX << "[index];\n"; - if (fAttrCountIncludePad == 0 && doPadding) - // compute number of elements used for the average - out << SP << SP << SP << SP << SP << SP << SP << "nsum++;\n"; } out << SP << SP << SP << SP << SP << SP << "}\n"; out << SP << SP << SP << SP << SP << "}\n"; // end loop on region elements @@ -433,9 +445,12 @@ public: out << SP << SP << SP << SP << "float value = -INFINITY;\n"; else if (fPoolMode == AveragePool) { out << SP << SP << SP << SP << "float value = 0;\n"; - if (fAttrCountIncludePad == 0 && doPadding) - out << SP << SP << SP << SP << "int nsum = 0;\n"; - else // in case we count the pad values in average + if (dynamicDivisor) + out << SP << SP << SP << SP << "const int nsum = " + << windowExtent("i", "kh", fShapeX[2], fAttrPads[fDim]) << " * " + << windowExtent("j", "kw", fShapeX[3], fAttrPads[fDim + 1]) << " * " + << windowExtent("k", "kd", fShapeX[4], fAttrPads[fDim + 2]) << ";\n"; + else // every window is full, so the divisor is the kernel area out << SP << SP << SP << SP << "constexpr int nsum = kw*kh*kd;\n"; } // loop on rows of filtered region @@ -456,9 +471,6 @@ public: else if (fPoolMode == AveragePool) { // compute sum of values out << SP << SP << SP << SP << SP << SP << SP << SP << "value += tensor_" << fNX << "[index];\n"; - if (fAttrCountIncludePad == 0 && doPadding) - // compute number of elements used for the average - out << SP << SP << SP << SP << SP << SP << SP << SP << "nsum++;\n"; } out << SP << SP << SP << SP << SP << SP << "}\n"; out << SP << SP << SP << SP << SP << "}\n"; diff --git a/tmva/sofie/test/TestCustomModelsFromONNX.cxx b/tmva/sofie/test/TestCustomModelsFromONNX.cxx index 02cc1762ad877..bfabd265def10 100644 --- a/tmva/sofie/test/TestCustomModelsFromONNX.cxx +++ b/tmva/sofie/test/TestCustomModelsFromONNX.cxx @@ -357,6 +357,51 @@ TEST(ONNX, MaxPool3d) expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); } +TEST(ONNX, AveragePool1d_CeilMode) +{ + SofieReference ref = readReference("AveragePool1d_CeilMode"); + + ASSERT_INCLUDE_AND_RUN(std::vector, "AveragePool1d_CeilMode", ref.f32("input0")); + + expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); +} + +TEST(ONNX, AveragePool2d_CeilMode) +{ + SofieReference ref = readReference("AveragePool2d_CeilMode"); + + ASSERT_INCLUDE_AND_RUN(std::vector, "AveragePool2d_CeilMode", ref.f32("input0")); + + expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); +} + +TEST(ONNX, AveragePool2d_CeilMode_CountIncludePad) +{ + SofieReference ref = readReference("AveragePool2d_CeilMode_CountIncludePad"); + + ASSERT_INCLUDE_AND_RUN(std::vector, "AveragePool2d_CeilMode_CountIncludePad", ref.f32("input0")); + + expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); +} + +TEST(ONNX, AveragePool2d_Pads_CountIncludePad) +{ + SofieReference ref = readReference("AveragePool2d_Pads_CountIncludePad"); + + ASSERT_INCLUDE_AND_RUN(std::vector, "AveragePool2d_Pads_CountIncludePad", ref.f32("input0")); + + expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); +} + +TEST(ONNX, AveragePool3d_CeilMode) +{ + SofieReference ref = readReference("AveragePool3d_CeilMode"); + + ASSERT_INCLUDE_AND_RUN(std::vector, "AveragePool3d_CeilMode", ref.f32("input0")); + + expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); +} + TEST(ONNX, AvgPool) { SofieReference ref = readReference("AvgPool"); diff --git a/tmva/sofie/test/generate_input_models.py b/tmva/sofie/test/generate_input_models.py index cc4f82527ac33..68ddece4e4539 100644 --- a/tmva/sofie/test/generate_input_models.py +++ b/tmva/sofie/test/generate_input_models.py @@ -244,6 +244,133 @@ def make_AddBroadcast7(): return _model(graph, opset=17, ir_version=8) +def make_AveragePool1d_CeilMode(): + """Ops: AveragePool""" + nodes = [ + helper.make_node( + 'AveragePool', + ['X'], + ['Y'], + ceil_mode=1, + kernel_shape=[3], + strides=[2], + ), + ] + graph = helper.make_graph( + nodes, + 'averagepool1d_ceil', + inputs=[ + _vi('X', FLOAT, [1, 1, 6]), + ], + outputs=[ + _vi('Y', FLOAT, [1, 1, 3]), + ], + ) + return _model(graph, opset=11, ir_version=6) + + +def make_AveragePool2d_CeilMode(): + """Ops: AveragePool""" + nodes = [ + helper.make_node( + 'AveragePool', + ['X'], + ['Y'], + ceil_mode=1, + kernel_shape=[2, 2], + strides=[2, 2], + ), + ] + graph = helper.make_graph( + nodes, + 'averagepool2d_ceil', + inputs=[ + _vi('X', FLOAT, [1, 1, 5, 5]), + ], + outputs=[ + _vi('Y', FLOAT, [1, 1, 3, 3]), + ], + ) + return _model(graph, opset=11, ir_version=6) + + +def make_AveragePool2d_CeilMode_CountIncludePad(): + """Ops: AveragePool""" + nodes = [ + helper.make_node( + 'AveragePool', + ['X'], + ['Y'], + ceil_mode=1, + count_include_pad=1, + kernel_shape=[2, 2], + strides=[2, 2], + ), + ] + graph = helper.make_graph( + nodes, + 'averagepool2d_ceil_cip', + inputs=[ + _vi('X', FLOAT, [1, 1, 5, 5]), + ], + outputs=[ + _vi('Y', FLOAT, [1, 1, 3, 3]), + ], + ) + return _model(graph, opset=11, ir_version=6) + + +def make_AveragePool2d_Pads_CountIncludePad(): + """Ops: AveragePool""" + nodes = [ + helper.make_node( + 'AveragePool', + ['X'], + ['Y'], + count_include_pad=1, + kernel_shape=[3, 3], + pads=[1, 1, 1, 1], + strides=[2, 2], + ), + ] + graph = helper.make_graph( + nodes, + 'averagepool2d_pads_cip', + inputs=[ + _vi('X', FLOAT, [1, 1, 5, 5]), + ], + outputs=[ + _vi('Y', FLOAT, [1, 1, 3, 3]), + ], + ) + return _model(graph, opset=11, ir_version=6) + + +def make_AveragePool3d_CeilMode(): + """Ops: AveragePool""" + nodes = [ + helper.make_node( + 'AveragePool', + ['X'], + ['Y'], + ceil_mode=1, + kernel_shape=[2, 2, 2], + strides=[2, 2, 2], + ), + ] + graph = helper.make_graph( + nodes, + 'averagepool3d_ceil', + inputs=[ + _vi('X', FLOAT, [1, 1, 5, 4, 4]), + ], + outputs=[ + _vi('Y', FLOAT, [1, 1, 3, 2, 2]), + ], + ) + return _model(graph, opset=11, ir_version=6) + + def make_AvgPool(): """Ops: AveragePool""" nodes = [ @@ -4685,6 +4812,11 @@ def make_Where(): 'AddBroadcast5': make_AddBroadcast5, 'AddBroadcast6': make_AddBroadcast6, 'AddBroadcast7': make_AddBroadcast7, + 'AveragePool1d_CeilMode': make_AveragePool1d_CeilMode, + 'AveragePool2d_CeilMode': make_AveragePool2d_CeilMode, + 'AveragePool2d_CeilMode_CountIncludePad': make_AveragePool2d_CeilMode_CountIncludePad, + 'AveragePool2d_Pads_CountIncludePad': make_AveragePool2d_Pads_CountIncludePad, + 'AveragePool3d_CeilMode': make_AveragePool3d_CeilMode, 'AvgPool': make_AvgPool, 'Cast': make_Cast, 'Clip': make_Clip, @@ -4885,6 +5017,11 @@ def rand_f32(seed, shape): f32([-0.4216483533382416, -0.6176707744598389, -0.6877889633178711, -1.1417591571807861, 0.6320437788963318, -0.6063031554222107], (2, 1, 3, 1)), f32([1.4051986932754517, -0.2876608669757843, 0.0749375969171524, 1.2207484245300293, -0.48621267080307007, -0.688210129737854, -0.6774346828460693, 0.3670888841152191, 0.0008057440281845629, -0.2080310881137848, 0.9697791337966919, 0.7583738565444946], (1, 1, 3, 4)), ], + 'AveragePool1d_CeilMode': [f32(np.arange(1, 7), (1, 1, 6))], + 'AveragePool2d_CeilMode': [f32(np.arange(1, 26), (1, 1, 5, 5))], + 'AveragePool2d_CeilMode_CountIncludePad': [f32(np.arange(1, 26), (1, 1, 5, 5))], + 'AveragePool2d_Pads_CountIncludePad': [f32(np.arange(1, 26), (1, 1, 5, 5))], + 'AveragePool3d_CeilMode': [f32(np.arange(1, 81), (1, 1, 5, 4, 4))], 'AvgPool': [f32([0.4763999879360199, -0.19760000705718994, 1.6505999565124512, -0.24210000038146973, 0.6412000060081482, 1.9984999895095825, 0.3937999904155731, 0.1347000002861023, 0.22040000557899475, -0.7502999901771545, 0.21389999985694885, 0.7285000085830688, -0.020999999716877937, -0.4584999978542328, -1.5333000421524048, -0.4772000014781952, 0.5559999942779541, 0.6323000192642212, -2.5371999740600586, 1.4905999898910522, -1.1061999797821045, -0.970300018787384, 0.23659999668598175, -0.91839998960495, 0.30140000581741333, 0.7985000014305115, -0.6840999722480774, -2.285399913787842, -2.7727999687194824, -1.2805999517440796, -1.0946999788284302, -0.5989999771118164, -0.30329999327659607, -1.9041999578475952, -0.5403000116348267, 0.23319999873638153, 0.921500027179718, -0.15489999949932098, 0.05570000037550926, -0.5566999912261963, -1.4970999956130981, 0.5386000275611877, -0.2921999990940094, 0.4860000014305115, -0.39730000495910645, -0.46239998936653137, 0.4514000117778778, 0.23849999904632568, 0.3783000111579895, -1.0499999523162842], (1, 1, 5, 10))], 'Cast': [i64([1, 2, 3, 4, 5, 6], (2, 3))], 'ComplexTopK': [f32([9.0, 8.0, 4.5, 1.7000000476837158, 2.9000000953674316, 3.200000047683716, 4.0, 2.5999999046325684, 7.400000095367432, 3.5, 5.599999904632568, 7.099999904632568, 9.800000190734863, 1.100000023841858, 3.299999952316284, 6.199999809265137, 8.399999618530273, 0.699999988079071, 2.200000047683716, 3.299999952316284, 4.400000095367432, 5.5, 6.599999904632568, 7.699999809265137, 8.800000190734863, 9.899999618530273, 1.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 5.0, 4.0, 3.0, 2.0, 1.0, 6.0, 7.0, 8.0, 9.0], (2, 3, 9))], From ab2b6167a65fba07eee5797618e4430071c110a7 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Mon, 20 Jul 2026 21:10:49 +0000 Subject: [PATCH 2/6] [tmva][sofie] Ignore ceil_mode pooling windows that start past the input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With ceil_mode = 1 the rounding up of the output dimension can add a sliding window that starts past the end of the input, i.e. entirely inside the right padding or in the overhang region. ONNX ignores such a window, but SOFIE emitted it, so both the inferred output shape and the generated loop produced one pooling window too many along the affected dimension. For example an input of 6 with kernel 2, stride 3 and ceil_mode = 1 gave 3 outputs instead of 2, and a 5x5 input with kernel 2, stride 2, pads 1,1,1,1 and ceil_mode = 1 gave 4x4 instead of 3x3. Clip the output dimension to the number of valid window starts in ShapeInference and clip the loop upper bounds in Generate() accordingly. Both are done only when ceil_mode is set; in floor mode the clipping is a no-op for any pad smaller than the kernel, so ordinary pooling is unaffected. This concerns MaxPool as well as AveragePool. Verified against onnxruntime over a sweep of 1760 1D pooling configurations (sizes, kernels, strides, begin/end pads, ceil_mode, both pool modes) with no remaining shape mismatch. Adds AveragePool and MaxPool tests for a ceil_mode window overhanging the input and for ceil_mode combined with pads. All four fail without this fix. 🤖 Done with the help of AI. --- tmva/sofie/inc/TMVA/ROperator_Pool.hxx | 35 +++--- tmva/sofie/test/TestCustomModelsFromONNX.cxx | 36 ++++++ tmva/sofie/test/generate_input_models.py | 110 +++++++++++++++++++ 3 files changed, 168 insertions(+), 13 deletions(-) diff --git a/tmva/sofie/inc/TMVA/ROperator_Pool.hxx b/tmva/sofie/inc/TMVA/ROperator_Pool.hxx index 3f8d6dbc90773..2ac24fccd4c91 100644 --- a/tmva/sofie/inc/TMVA/ROperator_Pool.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_Pool.hxx @@ -171,14 +171,17 @@ public: size_t input2 = (fDim > 1) ? input[0][3] : 1; size_t input3 = (fDim > 2) ? input[0][4] : 1; - // use ceiling division when ceil_mode=1, floor otherwise - auto poolOutDim = [this](size_t in, size_t pad, size_t kern, size_t stride) -> size_t { - size_t n = in + pad - kern; - return (fAttrCeilMode ? (n + stride - 1) / stride : n / stride) + 1; + // use ceiling division when ceil_mode=1, floor otherwise. With ceil_mode the rounding up can add + // a window that starts past the end of the input (i.e. entirely in the right padding or in the + // overhang region); ONNX ignores such a window, so clip to the number of valid window starts. + auto poolOutDim = [this](size_t in, size_t padBegin, size_t padEnd, size_t kern, size_t stride) -> size_t { + size_t n = in + padBegin + padEnd - kern; + if (!fAttrCeilMode) + return n / stride + 1; + return std::min((n + stride - 1) / stride, (in - 1 + padBegin) / stride) + 1; }; - size_t pad1 = fAttrPads[0] + fAttrPads[i1]; - size_t output1 = poolOutDim(input1, pad1, fAttrKernelShape[0], fAttrStrides[0]); + size_t output1 = poolOutDim(input1, fAttrPads[0], fAttrPads[i1], fAttrKernelShape[0], fAttrStrides[0]); size_t batch_size = input[0][0]; // first element in input tensor size_t output_channels = input[0][1]; // first element in output tensor @@ -188,15 +191,13 @@ public: if (fDim == 1) return ret; - size_t pad2 = fAttrPads[1] + fAttrPads[i2]; - size_t output2 = poolOutDim(input2, pad2, fAttrKernelShape[1], fAttrStrides[1]); + size_t output2 = poolOutDim(input2, fAttrPads[1], fAttrPads[i2], fAttrKernelShape[1], fAttrStrides[1]); // output is N x C x OH x OW ret[0].push_back(output2); if (fDim == 2) return ret; - size_t pad3 = fAttrPads[2] + fAttrPads[i3]; - size_t output3 = poolOutDim(input3, pad3, fAttrKernelShape[2], fAttrStrides[2]); + size_t output3 = poolOutDim(input3, fAttrPads[2], fAttrPads[i3], fAttrKernelShape[2], fAttrStrides[2]); // output is N x C x OH x OW x OD ret[0].push_back(output3); @@ -284,15 +285,22 @@ public: assert(fShapeX[1] == fShapeY[1]); assert(fAttrPads.size() == 6); assert(fAttrKernelShape.size() == 3); + // A window must start inside the input: with ceil_mode the loop bound below can otherwise run one + // window too far, which ONNX ignores. This mirrors the clipping done in ShapeInference. + auto clipToInput = [this](int upper, size_t size) { + return (fAttrCeilMode && upper > (int)size) ? (int)size : upper; + }; // find lower bounds of filtered area int hmin = - fAttrPads[0]; // minimum lower bound value of filter area // use stride instead of 1 when ceil_mode=1, so the loop covers the extra partial window - int hmax = fShapeX[2] + fAttrPads[fDim] - fAttrKernelShape[0] + (fAttrCeilMode ? (int)fAttrStrides[0] : 1); + int hmax = clipToInput(fShapeX[2] + fAttrPads[fDim] - fAttrKernelShape[0] + (fAttrCeilMode ? (int)fAttrStrides[0] : 1), + fShapeX[2]); int wmin,wmax,dmin,dmax; if(fDim >= 2){ wmin = -fAttrPads[1]; // minimum lower bound value of filter area - wmax = fShapeX[3] + fAttrPads[fDim + 1] - fAttrKernelShape[1] + (fAttrCeilMode ? (int)fAttrStrides[1] : 1); + wmax = clipToInput(fShapeX[3] + fAttrPads[fDim + 1] - fAttrKernelShape[1] + (fAttrCeilMode ? (int)fAttrStrides[1] : 1), + fShapeX[3]); } else{ wmin=1; @@ -300,7 +308,8 @@ public: } if(fDim == 3){ dmin = -fAttrPads[2]; // minimum lower bound value of filter area - dmax = fShapeX[4] + fAttrPads[fDim + 2] - fAttrKernelShape[2] + (fAttrCeilMode ? (int)fAttrStrides[2] : 1); + dmax = clipToInput(fShapeX[4] + fAttrPads[fDim + 2] - fAttrKernelShape[2] + (fAttrCeilMode ? (int)fAttrStrides[2] : 1), + fShapeX[4]); } else{ dmin=1; diff --git a/tmva/sofie/test/TestCustomModelsFromONNX.cxx b/tmva/sofie/test/TestCustomModelsFromONNX.cxx index bfabd265def10..678ec9a9aa67c 100644 --- a/tmva/sofie/test/TestCustomModelsFromONNX.cxx +++ b/tmva/sofie/test/TestCustomModelsFromONNX.cxx @@ -348,6 +348,24 @@ TEST(ONNX, MaxPool2d_CeilMode) expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); } +TEST(ONNX, MaxPool1d_CeilMode_Overhang) +{ + SofieReference ref = readReference("MaxPool1d_CeilMode_Overhang"); + + ASSERT_INCLUDE_AND_RUN(std::vector, "MaxPool1d_CeilMode_Overhang", ref.f32("input0")); + + expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); +} + +TEST(ONNX, MaxPool2d_CeilMode_Pads) +{ + SofieReference ref = readReference("MaxPool2d_CeilMode_Pads"); + + ASSERT_INCLUDE_AND_RUN(std::vector, "MaxPool2d_CeilMode_Pads", ref.f32("input0")); + + expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); +} + TEST(ONNX, MaxPool3d) { SofieReference ref = readReference("MaxPool3d"); @@ -366,6 +384,15 @@ TEST(ONNX, AveragePool1d_CeilMode) expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); } +TEST(ONNX, AveragePool1d_CeilMode_Overhang) +{ + SofieReference ref = readReference("AveragePool1d_CeilMode_Overhang"); + + ASSERT_INCLUDE_AND_RUN(std::vector, "AveragePool1d_CeilMode_Overhang", ref.f32("input0")); + + expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); +} + TEST(ONNX, AveragePool2d_CeilMode) { SofieReference ref = readReference("AveragePool2d_CeilMode"); @@ -375,6 +402,15 @@ TEST(ONNX, AveragePool2d_CeilMode) expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); } +TEST(ONNX, AveragePool2d_CeilMode_Pads) +{ + SofieReference ref = readReference("AveragePool2d_CeilMode_Pads"); + + ASSERT_INCLUDE_AND_RUN(std::vector, "AveragePool2d_CeilMode_Pads", ref.f32("input0")); + + expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); +} + TEST(ONNX, AveragePool2d_CeilMode_CountIncludePad) { SofieReference ref = readReference("AveragePool2d_CeilMode_CountIncludePad"); diff --git a/tmva/sofie/test/generate_input_models.py b/tmva/sofie/test/generate_input_models.py index 68ddece4e4539..3f72b9938005a 100644 --- a/tmva/sofie/test/generate_input_models.py +++ b/tmva/sofie/test/generate_input_models.py @@ -244,6 +244,57 @@ def make_AddBroadcast7(): return _model(graph, opset=17, ir_version=8) +def make_AveragePool1d_CeilMode_Overhang(): + """Ops: AveragePool""" + nodes = [ + helper.make_node( + 'AveragePool', + ['X'], + ['Y'], + ceil_mode=1, + kernel_shape=[2], + strides=[3], + ), + ] + graph = helper.make_graph( + nodes, + 'averagepool1d_ceil_overhang', + inputs=[ + _vi('X', FLOAT, [1, 1, 6]), + ], + outputs=[ + _vi('Y', FLOAT, [1, 1, 2]), + ], + ) + return _model(graph, opset=11, ir_version=6) + + +def make_AveragePool2d_CeilMode_Pads(): + """Ops: AveragePool""" + nodes = [ + helper.make_node( + 'AveragePool', + ['X'], + ['Y'], + ceil_mode=1, + kernel_shape=[2, 2], + pads=[1, 1, 1, 1], + strides=[2, 2], + ), + ] + graph = helper.make_graph( + nodes, + 'averagepool2d_ceil_pads', + inputs=[ + _vi('X', FLOAT, [1, 1, 5, 5]), + ], + outputs=[ + _vi('Y', FLOAT, [1, 1, 3, 3]), + ], + ) + return _model(graph, opset=11, ir_version=6) + + def make_AveragePool1d_CeilMode(): """Ops: AveragePool""" nodes = [ @@ -3400,6 +3451,57 @@ def make_MaxPool2d_CeilMode(): return _model(graph, opset=11, ir_version=6) +def make_MaxPool1d_CeilMode_Overhang(): + """Ops: MaxPool""" + nodes = [ + helper.make_node( + 'MaxPool', + ['X'], + ['Y'], + ceil_mode=1, + kernel_shape=[2], + strides=[3], + ), + ] + graph = helper.make_graph( + nodes, + 'maxpool1d_ceil_overhang', + inputs=[ + _vi('X', FLOAT, [1, 1, 6]), + ], + outputs=[ + _vi('Y', FLOAT, [1, 1, 2]), + ], + ) + return _model(graph, opset=11, ir_version=6) + + +def make_MaxPool2d_CeilMode_Pads(): + """Ops: MaxPool""" + nodes = [ + helper.make_node( + 'MaxPool', + ['X'], + ['Y'], + ceil_mode=1, + kernel_shape=[2, 2], + pads=[1, 1, 1, 1], + strides=[2, 2], + ), + ] + graph = helper.make_graph( + nodes, + 'maxpool2d_ceil_pads', + inputs=[ + _vi('X', FLOAT, [1, 1, 5, 5]), + ], + outputs=[ + _vi('Y', FLOAT, [1, 1, 3, 3]), + ], + ) + return _model(graph, opset=11, ir_version=6) + + def make_MaxPool3d(): """Ops: MaxPool""" nodes = [ @@ -4813,7 +4915,9 @@ def make_Where(): 'AddBroadcast6': make_AddBroadcast6, 'AddBroadcast7': make_AddBroadcast7, 'AveragePool1d_CeilMode': make_AveragePool1d_CeilMode, + 'AveragePool1d_CeilMode_Overhang': make_AveragePool1d_CeilMode_Overhang, 'AveragePool2d_CeilMode': make_AveragePool2d_CeilMode, + 'AveragePool2d_CeilMode_Pads': make_AveragePool2d_CeilMode_Pads, 'AveragePool2d_CeilMode_CountIncludePad': make_AveragePool2d_CeilMode_CountIncludePad, 'AveragePool2d_Pads_CountIncludePad': make_AveragePool2d_Pads_CountIncludePad, 'AveragePool3d_CeilMode': make_AveragePool3d_CeilMode, @@ -4897,7 +5001,9 @@ def make_Where(): 'MaxPool1d': make_MaxPool1d, 'MaxPool2d': make_MaxPool2d, 'MaxPool2d_AsymPad': make_MaxPool2d_AsymPad, + 'MaxPool1d_CeilMode_Overhang': make_MaxPool1d_CeilMode_Overhang, 'MaxPool2d_CeilMode': make_MaxPool2d_CeilMode, + 'MaxPool2d_CeilMode_Pads': make_MaxPool2d_CeilMode_Pads, 'MaxPool3d': make_MaxPool3d, 'MeanMultidirectionalBroadcast': make_MeanMultidirectionalBroadcast, 'MinMultidirectionalBroadcast': make_MinMultidirectionalBroadcast, @@ -5018,7 +5124,9 @@ def rand_f32(seed, shape): f32([1.4051986932754517, -0.2876608669757843, 0.0749375969171524, 1.2207484245300293, -0.48621267080307007, -0.688210129737854, -0.6774346828460693, 0.3670888841152191, 0.0008057440281845629, -0.2080310881137848, 0.9697791337966919, 0.7583738565444946], (1, 1, 3, 4)), ], 'AveragePool1d_CeilMode': [f32(np.arange(1, 7), (1, 1, 6))], + 'AveragePool1d_CeilMode_Overhang': [f32(np.arange(1, 7), (1, 1, 6))], 'AveragePool2d_CeilMode': [f32(np.arange(1, 26), (1, 1, 5, 5))], + 'AveragePool2d_CeilMode_Pads': [f32(np.arange(1, 26), (1, 1, 5, 5))], 'AveragePool2d_CeilMode_CountIncludePad': [f32(np.arange(1, 26), (1, 1, 5, 5))], 'AveragePool2d_Pads_CountIncludePad': [f32(np.arange(1, 26), (1, 1, 5, 5))], 'AveragePool3d_CeilMode': [f32(np.arange(1, 81), (1, 1, 5, 4, 4))], @@ -5111,6 +5219,8 @@ def rand_f32(seed, shape): 'MaxPool1d': [f32([0.09070000052452087, 0.10289999842643738, 0.814300000667572, 1.4496999979019165, -0.7785000205039978, 0.3824999928474426, -0.3763999938964844, 1.5785000324249268, -0.08349999785423279, 0.16220000386238098, 1.5866999626159668, 0.9822999835014343, -0.882099986076355, 0.4438999891281128, -0.13779999315738678, -0.2273000031709671, -0.01979999989271164, -2.0230000019073486, 0.09049999713897705, 0.6674000024795532, -1.4290000200271606, -1.309999942779541, -0.9438999891281128, -0.08330000191926956, -0.19189999997615814, 0.6886000037193298, 0.9388999938964844, -1.2913999557495117, -1.3583999872207642, -2.03410005569458, -0.32690000534057617, 0.1703999936580658, 1.1776000261306763, 1.3971999883651733, -1.8874000310897827, -1.533400058746338, 1.154099941253662, 0.3010999858379364, 0.6568999886512756, -2.350399971008301, 0.4032999873161316, 0.11420000344514847, 2.284600019454956, -1.3947999477386475, -0.8572999835014343, 0.5756000280380249, -1.086400032043457, 0.22830000519752502, 0.8946999907493591, 1.7626999616622925, -0.1657000035047531, 0.0649000033736229, -1.606600046157837, 0.41620001196861267, -1.152500033378601, -0.8184000253677368, 1.1324000358581543, -1.1086000204086304, 0.10610000044107437, 1.007099986076355], (1, 6, 10))], 'MaxPool2d': [f32([0.6266000270843506, 0.1656000018119812, 0.275299996137619, -0.45579999685287476, -1.4592000246047974, 0.9284999966621399, -1.340999960899353, 1.3222999572753906, -0.5935999751091003, -1.364799976348877, -0.2989000082015991, 0.5900999903678894, -0.8845000267028809, -0.043299999088048935, 0.8313999772071838, -1.71589994430542, -0.5764999985694885, 0.8677999973297119, 1.0256999731063843, 0.7846999764442444, -0.34209999442100525, -1.2364000082015991, -0.5805000066757202, 0.44209998846054077, 1.218400001525879, 0.5042999982833862, 1.6822999715805054, -1.04830002784729, -2.2797999382019043, -1.892699956893921, 0.7716000080108643, 0.04050000011920929, 0.31209999322891235, -0.3010999858379364, -0.32659998536109924, -1.965999960899353, 1.0836999416351318, 0.23170000314712524, 0.9083999991416931, -0.32850000262260437, -0.9398000240325928, -0.20649999380111694, -0.9498999714851379, -0.9739000201225281, -0.12880000472068787, -0.13750000298023224, -1.261199951171875, 0.8809999823570251, 0.850600004196167, 0.445499986410141], (1, 1, 5, 10))], 'MaxPool2d_AsymPad': [f32([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0], (1, 1, 4, 4))], + 'MaxPool1d_CeilMode_Overhang': [f32(np.arange(1, 7), (1, 1, 6))], + 'MaxPool2d_CeilMode_Pads': [f32(np.arange(1, 26), (1, 1, 5, 5))], 'MaxPool2d_CeilMode': [f32(np.arange(25), (1, 1, 5, 5))], 'MaxPool3d': [f32([-2.649600028991699, 1.0476000308990479, -0.5152999758720398, 0.37709999084472656, 0.41290000081062317, -0.3077000081539154, -0.8716999888420105, -0.8040000200271606, -0.35249999165534973, -0.17649999260902405, -0.33640000224113464, 0.8737000226974487, -0.23810000717639923, -0.8296999931335449, 0.4666000008583069, 0.6984000205993652, -0.6759999990463257, 0.629800021648407, 1.3832999467849731, 0.11010000109672546, 0.20389999449253082, -0.5476999878883362, 0.23409999907016754, 0.9180999994277954, 0.38420000672340393, 0.24279999732971191, 1.7924000024795532], (1, 1, 3, 3, 3))], 'MeanMultidirectionalBroadcast': [ From 1462b0b88f0612f1c1c61689a6365e8a40c624a6 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Mon, 20 Jul 2026 21:31:57 +0000 Subject: [PATCH 3/6] [tmva][sofie] Fix tests with non-discriminating reference values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several SOFIE ONNX tests had reference results that a broken implementation would still pass: - LinearWithLeakyRelu compared against a tolerance of 1, while the outputs span [-0.23, 1.33] with a mean magnitude of ~0.3. Returning all zeros passed 23 of the 24 elements. The test passes with the DEFAULT_TOLERANCE of 1e-3 that every other test uses. - Concat0D checked "expected_output.size() == expected_output.size()" and never looked at the output size, so an empty output passed silently and an oversized one read past the reference array. Use the expectNear() helper, which asserts on the size first. - Greater and LessOrEqual shared an input pair that happens to be non-discriminating for exactly those two operators: the references were all-false and all-true respectively, so a constant implementation passed. Adjust the second operand to mix both results. - The Sub reference was {1.0, 1.0}, which an implementation swapping the operands or returning a constant would also produce. Use asymmetric operands giving distinct results. Also stop passing Einsum_dotprod's weight file to the Einsum_3 session. It is harmless today because Einsum_3 declares no initializers, so the file is never read, but it would silently load the wrong weights as soon as that changes. 🤖 Done with the help of AI. --- tmva/sofie/test/TestCustomModelsFromONNX.cxx | 14 +++----------- tmva/sofie/test/generate_input_models.py | 14 ++++++++++---- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/tmva/sofie/test/TestCustomModelsFromONNX.cxx b/tmva/sofie/test/TestCustomModelsFromONNX.cxx index 678ec9a9aa67c..dbe088b96a86b 100644 --- a/tmva/sofie/test/TestCustomModelsFromONNX.cxx +++ b/tmva/sofie/test/TestCustomModelsFromONNX.cxx @@ -190,7 +190,7 @@ TEST(ONNX, LinearWithLeakyRelu) ASSERT_INCLUDE_AND_RUN(std::vector, "LinearWithLeakyRelu", ref.f32("input0")); - expectNear(output, ref.f32("output0"), 1); + expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); } @@ -1000,15 +1000,7 @@ TEST(ONNX, Concat0D) { std::vector expected_output({1.40519865e+00, -2.87660856e-01, 1.40519865e+00, -2.87660856e-01}); ASSERT_INCLUDE_AND_RUN(std::vector, "Concat_0D", input); - // Checking the output size - EXPECT_EQ(expected_output.size(), expected_output.size()); - - float* correct = expected_output.data(); - - // Checking every output value, one by one - for (size_t i = 0; i < output.size(); i++) { - EXPECT_LE(std::abs(output[i] - correct[i]), TOLERANCE); - } + expectNear(output, expected_output, TOLERANCE); } TEST(ONNX, LayerNormalization2d) @@ -1373,7 +1365,7 @@ TEST(ONNX, Einsum_3) std::vector input2 {1.,2.,3,4,5,6,7,8,9,10,11,12}; std::vector correct_output {66. , 87. , 108., 498., 555., 612. }; - ASSERT_INCLUDE_AND_RUN_SESSION_ARGS(std::vector, "Einsum_3", "\"Einsum_dotprod_FromONNX.dat\"", input1, input2); + ASSERT_INCLUDE_AND_RUN(std::vector, "Einsum_3", input1, input2); // Checking output size EXPECT_EQ(output.size(), 6); diff --git a/tmva/sofie/test/generate_input_models.py b/tmva/sofie/test/generate_input_models.py index 3f72b9938005a..9e668f6569217 100644 --- a/tmva/sofie/test/generate_input_models.py +++ b/tmva/sofie/test/generate_input_models.py @@ -5175,9 +5175,11 @@ def rand_f32(seed, shape): 'GatherAxis3': [f32(np.arange(0.0, 120.0), (5, 4, 3, 2))], 'GatherNegativeIndices': [f32(np.arange(0.0, 10.0), (10,))], 'Gelu': [f32([1.0, -2.0, 3.0, 0.5, -1.0, 2.0], (6,))], + # Note: the second operand must produce a mix of true and false results, + # otherwise a constant implementation would pass the test. 'Greater': [ f32([1.0, 2.0, 3.0], (3,)), - f32([4.0, 2.0, 6.0], (3,)), + f32([4.0, 2.0, 1.0], (3,)), ], 'GreaterOrEqual': [ f32([1.0, 2.0, 3.0], (3,)), @@ -5196,9 +5198,11 @@ def rand_f32(seed, shape): f32([1.0, 2.0, 3.0], (3,)), f32([4.0, 2.0, 6.0], (3,)), ], + # Note: the second operand must produce a mix of true and false results, + # otherwise a constant implementation would pass the test. 'LessOrEqual': [ f32([1.0, 2.0, 3.0], (3,)), - f32([4.0, 2.0, 6.0], (3,)), + f32([4.0, 2.0, 1.0], (3,)), ], 'LinearWithLeakyRelu': [f32([0.43689998984336853, -0.6881999969482422, 1.030900001525879, -1.0262999534606934, -0.15189999341964722, 1.2237000465393066, -0.7053999900817871, -0.1762000024318695, -0.6811000108718872, -2.259700059890747, 1.0388000011444092, -0.7993000149726868, 0.1467999964952469, 1.325700044631958, -0.4713999927043915, -0.0957999974489212, 0.7056999802589417, -0.3749000132083893, -0.3310000002384186, 0.09860000014305115, -0.13699999451637268, 0.08320000022649765, -1.6464999914169312, -0.2793000042438507], (24,))], 'LinearWithSelu': [f32(np.full(48, 1.0), (2, 24))], @@ -5276,9 +5280,11 @@ def rand_f32(seed, shape): 'Softmax3d': [f32([-0.8938999772071838, -0.36739999055862427, 0.17630000412464142, 1.580399990081787, -0.46869999170303345, 1.2252999544143677, -1.3487999439239502, -0.10000000149011612, -0.12620000541210175, 0.49619999527931213, 1.0870000123977661, 0.690500020980835, -0.3450999855995178, -1.698099970817566, -0.46880000829696655, 0.44679999351501465, -0.5479000210762024, 0.06499999761581421, 1.044600009918213, -1.624899983406067, -0.718999981880188, -1.7519999742507935, 3.7753000259399414, -1.493899941444397], (2, 3, 4))], 'Softmax4d': [f32([-0.586899995803833, -1.4271999597549438, -0.15459999442100525, 0.009600000455975533, 0.17059999704360962, 0.03880000114440918, -0.3483999967575073, -0.7828999757766724, 1.113800048828125, -0.5644000172615051, -0.6263999938964844, -1.1890000104904175, 1.6741000413894653, -0.7129999995231628, 0.9592000246047974, 1.7476999759674072, -0.47749999165534973, 1.3407000303268433, -0.3882000148296356, -0.4560000002384186, 1.0384999513626099, -0.16689999401569366, 0.5540000200271606, -1.0789999961853027, -0.6152999997138977, -0.6273999810218811, -1.2303999662399292, -0.6757000088691711, 1.017799973487854, -0.2379000037908554, -0.7911999821662903, -0.016499999910593033, -0.5422999858856201, 0.14589999616146088, 1.3585000038146973, -0.5005000233650208, -0.21870000660419464, -1.8180999755859375, -0.6642000079154968, 0.028699999675154686, -1.9103000164031982, 0.7983999848365784, -0.7860000133514404, 1.5133999586105347, 1.3873000144958496, -0.6462000012397766, -0.6353999972343445, -0.13349999487400055], (2, 3, 4, 2))], 'Sqrt': [f32([0.8343999981880188, 0.4715999960899353, 0.6226000189781189, 0.8447999954223633, 0.2483000010251999, 0.9466999769210815], (2, 3))], + # Note: the operands are asymmetric and give distinct results, so that an + # implementation swapping them or returning a constant would be caught. 'Sub': [ - f32([1.0, 2.0], (2,)), - f32([0.0, 1.0], (2,)), + f32([1.5, 2.0], (2,)), + f32([0.25, -1.0], (2,)), ], 'SumMultidirectionalBroadcast': [ f32([0.35974153876304626, -2.2087337970733643, 0.957462728023529], (3, 1)), From 806af4844950a142c4be8f60b129bb22860a81a4 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Mon, 20 Jul 2026 21:36:54 +0000 Subject: [PATCH 4/6] [tmva][sofie] Use the comparison helpers instead of hand-rolled loops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit About 30 tests re-implemented what expectNear() and expectEqual() already do, and did it less safely: they checked the output size with EXPECT_EQ rather than ASSERT_EQ, and then bounded the comparison loop by output.size(). A model returning more values than the reference array holds therefore read past the end of that array instead of failing on the size mismatch. The helpers assert on the size first and report the offending index in the failure message. Add expectNear() and expectEqual() overloads for std::vector> so the multi-output models (Split, Clip) can use them too, and drop the seven local "constexpr float TOLERANCE = DEFAULT_TOLERANCE" aliases. Along the way: - ComparisonBroadcast indexed output[2] without ever checking that the model returned three tensors. Assert the count first. - Softplus keeps its bespoke branch logic, but its size check becomes an ASSERT_EQ, since the loop indexes the input tensor as well. - The Einsum tests compared floats with EXPECT_EQ against a literal size. They now use expectNear() with the DEFAULT_TOLERANCE the rest of the file uses; the expected values are small exact integers, so nothing meaningful is lost. No change in coverage: all 153 tests still pass, and perturbing a reference value still makes the corresponding test fail. 🤖 Done with the help of AI. --- tmva/sofie/test/TestCustomModelsFromONNX.cxx | 290 ++++--------------- tmva/sofie/test/test_helpers.h | 22 ++ 2 files changed, 75 insertions(+), 237 deletions(-) diff --git a/tmva/sofie/test/TestCustomModelsFromONNX.cxx b/tmva/sofie/test/TestCustomModelsFromONNX.cxx index dbe088b96a86b..f469967f96e78 100644 --- a/tmva/sofie/test/TestCustomModelsFromONNX.cxx +++ b/tmva/sofie/test/TestCustomModelsFromONNX.cxx @@ -296,10 +296,7 @@ TEST(ONNX, ConvWithDynShapeStride) ASSERT_INCLUDE_AND_RUN_SESSION_ARGS(std::vector, "ConvWithDynShapeStride", "\"ConvWithDynShapeStride_FromONNX.dat\", 7", 7, input); - EXPECT_EQ(output.size(), correct_output.size()); - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_LE(std::abs(output[i] - correct_output[i]), DEFAULT_TOLERANCE); - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } // Disables test (asymmetric padding not supported) @@ -471,9 +468,7 @@ TEST(ONNX, FMod_ConstantFolding) // fmod([10, 7, 5], [3, 3, 3]) = [1, 1, 2] std::vector correct_output = {1, 1, 2}; ASSERT_INCLUDE_AND_RUN_0(std::vector, "FMod_ConstantFolding"); - EXPECT_EQ(output.size(), correct_output.size()); - for (size_t i = 0; i < output.size(); ++i) - EXPECT_LE(std::abs(output[i] - correct_output[i]), DEFAULT_TOLERANCE); + expectNear(output, correct_output, DEFAULT_TOLERANCE); } TEST(ONNX, Mod_ConstantFolding) @@ -482,9 +477,7 @@ TEST(ONNX, Mod_ConstantFolding) // [10, 7, 5] % [3, 3, 3] = [1, 1, 2] std::vector correct_output = {1, 1, 2}; ASSERT_INCLUDE_AND_RUN_0(std::vector, "Mod_ConstantFolding"); - EXPECT_EQ(output.size(), correct_output.size()); - for (size_t i = 0; i < output.size(); ++i) - EXPECT_EQ(output[i], correct_output[i]); + expectEqual(output, correct_output); } TEST(ONNX, ReduceMean) @@ -505,10 +498,7 @@ TEST(ONNX, ReduceMean_kFirst) ASSERT_INCLUDE_AND_RUN(std::vector, "ReduceMean_kFirst", input); - EXPECT_EQ(output.size(), correct_output.size()); - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_LE(std::abs(output[i] - correct_output[i]), DEFAULT_TOLERANCE); - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } TEST(ONNX, ReduceProd) @@ -521,9 +511,6 @@ TEST(ONNX, ReduceMean_kFirst) } TEST(ONNX, ReduceSum){ - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - - // Preparing the standard input std::vector input({ 5, 2, 3, @@ -535,21 +522,12 @@ TEST(ONNX, ReduceSum){ // output tensod is shape [1,1,1] and value = 24 (sum of all elements) ASSERT_INCLUDE_AND_RUN(std::vector, "ReduceSum", input); - // Checking output size - EXPECT_EQ(output.size(), 1); + std::vector correct{24}; - float correct[] = {24}; - - // Checking every output value, one by one - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_LE(std::abs(output[i] - correct[i]), TOLERANCE); - } + expectNear(output, correct, DEFAULT_TOLERANCE); } TEST(ONNX, ReduceSumSquare){ - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - - // Preparing the standard input std::vector input({ 5, 2, 3, @@ -561,15 +539,9 @@ TEST(ONNX, ReduceSumSquare){ ASSERT_INCLUDE_AND_RUN(std::vector, "ReduceSumSquare", input); - // Checking output size - EXPECT_EQ(output.size(), 2); + std::vector correct{38, 66}; - float correct[] = {38, 66}; - - // Checking every output value, one by one - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_LE(std::abs(output[i] - correct[i]), TOLERANCE); - } + expectNear(output, correct, DEFAULT_TOLERANCE); } TEST(ONNX, Max) @@ -993,14 +965,12 @@ TEST(ONNX, AddBroadcast7) } TEST(ONNX, Concat0D) { - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - // input std::vector input({1.40519865e+00, -2.87660856e-01}); std::vector expected_output({1.40519865e+00, -2.87660856e-01, 1.40519865e+00, -2.87660856e-01}); ASSERT_INCLUDE_AND_RUN(std::vector, "Concat_0D", input); - expectNear(output, expected_output, TOLERANCE); + expectNear(output, expected_output, DEFAULT_TOLERANCE); } TEST(ONNX, LayerNormalization2d) @@ -1218,13 +1188,7 @@ TEST(ONNX, Pad) { 4, 0, 0, 0, 0, 0, 0, 0}; ASSERT_INCLUDE_AND_RUN(std::vector, "Pad", input); - // Checking the output size - EXPECT_EQ(output.size(), correct.size()); - - // Checking every output value, one by one - for (size_t i = 0; i < output.size(); i++) { - EXPECT_EQ(output[i], correct[i]); - } + expectEqual(output, correct); } TEST(ONNX, Where) { // test of Where using [[1,2]] and [[3,4],[5,6],[7,8]] with condition [[true],[false],[true]] -> [[1,2],[5,6],[1,2]] @@ -1235,19 +1199,11 @@ TEST(ONNX, Where) { std::vector correct = {1,2,5,6,1,2}; ASSERT_INCLUDE_AND_RUN(std::vector, "Where", input1, input2, cond); - // Checking the output size - EXPECT_EQ(output.size(), correct.size()); - - // Checking every output value, one by one - for (size_t i = 0; i < output.size(); i++) { - EXPECT_EQ(output[i], correct[i]); - } + expectEqual(output, correct); } TEST(ONNX, Sin) { - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - // Preparing some random input std::vector input({ -0.786738,-0.197796,-0.187787,0.142758,0.876096,-0.653239,0.145444,-1.107658,2.259171,-0.947054,-0.506689,1.801250 @@ -1255,19 +1211,15 @@ TEST(ONNX, Sin) ASSERT_INCLUDE_AND_RUN(std::vector, "Sin", input); - // Checking output size - EXPECT_EQ(output.size(), input.size()); + std::vector correct_output; + for (float x : input) + correct_output.push_back(std::sin(x)); - // Checking every output value, one by one - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_LE(std::abs(output[i] - std::sin(input[i])), TOLERANCE); - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } TEST(ONNX, Cos) { - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - // Preparing the random input std::vector input({ 1.152504,-1.459324,0.691594,0.347690,-1.307323,1.832516,-1.261772,0.014224,1.311477,1.147405,-0.567206,-0.530606 @@ -1275,53 +1227,45 @@ TEST(ONNX, Cos) ASSERT_INCLUDE_AND_RUN(std::vector, "Cos", input); - // Checking output size - EXPECT_EQ(output.size(), input.size()); + std::vector correct_output; + for (float x : input) + correct_output.push_back(std::cos(x)); - // Checking every output value, one by one - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_LE(std::abs(output[i] - std::cos(input[i])), TOLERANCE); - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } TEST(ONNX, Abs) { - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - // Preparing the random input std::vector input({1.,-2.,-3,4,-5.,6}); ASSERT_INCLUDE_AND_RUN(std::vector, "Abs", input); - // Checking output size - EXPECT_EQ(output.size(), input.size()); + std::vector correct_output; + for (float x : input) + correct_output.push_back(std::abs(x)); - // Checking every output value, one by one - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_LE(std::abs(output[i] - std::abs(input[i])), TOLERANCE); - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } TEST(ONNX, Softplus) { - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - // Inputs spanning stable region, threshold boundary, and overflow-prone range std::vector input({0.1f, -0.2f, 100.0f, 89.0f, 0.0f, 50.0f}); ASSERT_INCLUDE_AND_RUN(std::vector, "Softplus", input); - EXPECT_EQ(output.size(), input.size()); + ASSERT_EQ(output.size(), input.size()); for (size_t i = 0; i < output.size(); ++i) { EXPECT_FALSE(std::isinf(output[i])) << "Inf at input=" << input[i]; EXPECT_FALSE(std::isnan(output[i])) << "NaN at input=" << input[i]; // For large positive x (>= 20.0), softplus(x) ≈ x if (input[i] >= 20.0f) { - EXPECT_NEAR(output[i], input[i], TOLERANCE); + EXPECT_NEAR(output[i], input[i], DEFAULT_TOLERANCE); } else { float exp_value = std::log1p(std::exp(input[i])); - EXPECT_LE(std::abs(output[i] - exp_value), TOLERANCE); + EXPECT_LE(std::abs(output[i] - exp_value), DEFAULT_TOLERANCE); } } } @@ -1334,12 +1278,7 @@ TEST(ONNX, Einsum_matmul) ASSERT_INCLUDE_AND_RUN(std::vector, "Einsum_matmul", input1, input2); - // Checking output size - EXPECT_EQ(output.size(), 4); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_EQ(output[i], correct_output[i]); - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } // test dot prod using Einsum TEST(ONNX, Einsum_dotprod) @@ -1350,12 +1289,7 @@ TEST(ONNX, Einsum_dotprod) ASSERT_INCLUDE_AND_RUN(std::vector, "Einsum_dotprod", input1, input2); - // Checking output size - EXPECT_EQ(output.size(), 1); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_EQ(output[i], correct_output[i]); - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } // test tensor contraction of rank 3 tensors TEST(ONNX, Einsum_3) @@ -1367,12 +1301,7 @@ TEST(ONNX, Einsum_3) ASSERT_INCLUDE_AND_RUN(std::vector, "Einsum_3", input1, input2); - // Checking output size - EXPECT_EQ(output.size(), 6); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_EQ(output[i], correct_output[i]); - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } // test tensor contraction of rank 4 tensors TEST(ONNX, Einsum_4) @@ -1385,12 +1314,7 @@ TEST(ONNX, Einsum_4) ASSERT_INCLUDE_AND_RUN(std::vector, "Einsum_4", input1, input2); - // Checking output size - EXPECT_EQ(output.size(), 12); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_EQ(output[i], correct_output[i]); - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } TEST(ONNX, RandomUniform) { @@ -1399,12 +1323,7 @@ TEST(ONNX, RandomUniform) ASSERT_INCLUDE_AND_RUN_0(std::vector, "RandomUniform"); - // Checking output size - EXPECT_EQ(output.size(), correct_output.size()); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_LE(std::abs(output[i] - correct_output[i]), DEFAULT_TOLERANCE); - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } TEST(ONNX, RandomNormal) @@ -1414,12 +1333,7 @@ TEST(ONNX, RandomNormal) ASSERT_INCLUDE_AND_RUN_0(std::vector, "RandomNormal"); - // Checking output size - EXPECT_EQ(output.size(), correct_output.size()); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_LE(std::abs(output[i] - correct_output[i]), DEFAULT_TOLERANCE); - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } TEST(ONNX, Split_0) @@ -1430,14 +1344,7 @@ TEST(ONNX, Split_0) ASSERT_INCLUDE_AND_RUN(std::vector>, "Split_0", input); - // Checking output size - EXPECT_EQ(output.size(), correct_output.size()); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - for (size_t j = 0; j < output[i].size(); ++j) { - EXPECT_LE(std::abs(output[i][j] - correct_output[i][j]), DEFAULT_TOLERANCE); - } - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } TEST(ONNX, Split_1) @@ -1448,14 +1355,7 @@ TEST(ONNX, Split_1) ASSERT_INCLUDE_AND_RUN(std::vector>, "Split_1", input); - // Checking output size - EXPECT_EQ(output.size(), correct_output.size()); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - for (size_t j = 0; j < output[i].size(); ++j) { - EXPECT_LE(std::abs(output[i][j] - correct_output[i][j]), DEFAULT_TOLERANCE); - } - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } TEST(ONNX, Split_2) @@ -1466,14 +1366,7 @@ TEST(ONNX, Split_2) ASSERT_INCLUDE_AND_RUN(std::vector>, "Split_2", input); - // Checking output size - EXPECT_EQ(output.size(), correct_output.size()); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - for (size_t j = 0; j < output[i].size(); ++j) { - EXPECT_LE(std::abs(output[i][j] - correct_output[i][j]), DEFAULT_TOLERANCE); - } - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } TEST(ONNX, ScatterElements) @@ -1486,12 +1379,7 @@ TEST(ONNX, ScatterElements) ASSERT_INCLUDE_AND_RUN(std::vector, "ScatterElements", input, indices, updates); - // Checking output size - EXPECT_EQ(output.size(), correct_output.size()); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_LE(std::abs(output[i] - correct_output[i]), DEFAULT_TOLERANCE); - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } TEST(ONNX, MatMul_Stacked) @@ -1505,12 +1393,7 @@ TEST(ONNX, MatMul_Stacked) // model is dynamic , use N = 2 ASSERT_INCLUDE_AND_RUN_SESSION_ARGS(std::vector, "MatMul_Stacked", "\"MatMul_Stacked_FromONNX.dat\", 2", 2, input1, input2); - // Checking output size - EXPECT_EQ(output.size(), correct_output.size()); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_LE(std::abs(output[i] - correct_output[i]), DEFAULT_TOLERANCE); - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } TEST(ONNX, MatMul_Stacked2) @@ -1524,12 +1407,7 @@ TEST(ONNX, MatMul_Stacked2) // model is dynamic , use N = 2 ASSERT_INCLUDE_AND_RUN_SESSION_ARGS(std::vector, "MatMul_Stacked2", "\"MatMul_Stacked2_FromONNX.dat\", 2", 2, input1, input2); - // Checking output size - EXPECT_EQ(output.size(), correct_output.size()); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_LE(std::abs(output[i] - correct_output[i]), DEFAULT_TOLERANCE); - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } TEST(ONNX, GatherND_1) @@ -1543,12 +1421,7 @@ TEST(ONNX, GatherND_1) ASSERT_INCLUDE_AND_RUN(std::vector, "GatherND_1", input, indices); - // Checking output size - EXPECT_EQ(output.size(), correct_output.size()); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_EQ(output[i] , correct_output[i]); - } + expectEqual(output, correct_output); } TEST(ONNX, GatherND_2) @@ -1562,12 +1435,7 @@ TEST(ONNX, GatherND_2) ASSERT_INCLUDE_AND_RUN(std::vector, "GatherND_2", input, indices); - // Checking output size - EXPECT_EQ(output.size(), correct_output.size()); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_EQ(output[i] , correct_output[i]); - } + expectEqual(output, correct_output); } TEST(ONNX, GatherND_3) @@ -1582,12 +1450,7 @@ TEST(ONNX, GatherND_3) ASSERT_INCLUDE_AND_RUN(std::vector, "GatherND_3", input, indices); - // Checking output size - EXPECT_EQ(output.size(), correct_output.size()); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_EQ(output[i] , correct_output[i]); - } + expectEqual(output, correct_output); } TEST(ONNX, NonZero) @@ -1599,12 +1462,7 @@ TEST(ONNX, NonZero) ASSERT_INCLUDE_AND_RUN(std::vector, "NonZero", input); - // Checking output size - EXPECT_EQ(output.size(), correct_output.size()); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_EQ(output[i] , correct_output[i]); - } + expectEqual(output, correct_output); } TEST(ONNX, NonZero_Constant) @@ -1615,12 +1473,7 @@ TEST(ONNX, NonZero_Constant) ASSERT_INCLUDE_AND_RUN_0(std::vector, "NonZero_Constant"); - // Checking output size - EXPECT_EQ(output.size(), correct_output.size()); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_EQ(output[i] , correct_output[i]); - } + expectEqual(output, correct_output); } TEST(ONNX, IsInf) { @@ -1631,12 +1484,7 @@ TEST(ONNX, IsInf) // not cannot use input.size() in string because input symbol will not be visible when running inference ASSERT_INCLUDE_AND_RUN_SESSION_ARGS(std::vector, "IsInf",std::string("\"\", ") + std::to_string(input.size()), input.size(),input); - // Checking output size - EXPECT_EQ(output.size(), correct_output.size()); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_EQ(output[i] , correct_output[i]); - } + expectEqual(output, correct_output); } TEST(ONNX, NotIsNaN) @@ -1647,12 +1495,7 @@ TEST(ONNX, NotIsNaN) ASSERT_INCLUDE_AND_RUN_SESSION_ARGS(std::vector, "NotIsNaN",std::string("\"\", ") + std::to_string(input.size()), input.size(),input); - // Checking output size - EXPECT_EQ(output.size(), correct_output.size()); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_EQ(output[i] , correct_output[i]); - } + expectEqual(output, correct_output); } TEST(ONNX, ScatterND_1) @@ -1665,12 +1508,7 @@ TEST(ONNX, ScatterND_1) ASSERT_INCLUDE_AND_RUN(std::vector, "ScatterND_1", input, indices, updates); - // Checking output size - EXPECT_EQ(output.size(), correct_output.size()); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_LE(std::abs(output[i] - correct_output[i]), DEFAULT_TOLERANCE); - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } TEST(ONNX, ScatterND_2) @@ -1683,12 +1521,7 @@ TEST(ONNX, ScatterND_2) ASSERT_INCLUDE_AND_RUN(std::vector, "ScatterND_2", input, indices, updates); - // Checking output size - EXPECT_EQ(output.size(), correct_output.size()); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_LE(std::abs(output[i] - correct_output[i]), DEFAULT_TOLERANCE); - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } TEST(ONNX, ScatterND_3) @@ -1701,12 +1534,7 @@ TEST(ONNX, ScatterND_3) ASSERT_INCLUDE_AND_RUN(std::vector, "ScatterND_3", input, indices, updates); - // Checking output size - EXPECT_EQ(output.size(), correct_output.size()); - // Checking output - for (size_t i = 0; i < output.size(); ++i) { - EXPECT_LE(std::abs(output[i] - correct_output[i]), DEFAULT_TOLERANCE); - } + expectNear(output, correct_output, DEFAULT_TOLERANCE); } TEST(ONNX, Clip) @@ -1718,18 +1546,9 @@ TEST(ONNX, Clip) ASSERT_INCLUDE_AND_RUN_SESSION_ARGS(std::vector>, "Clip", "\"Clip_FromONNX.dat\", 2", 2, input); - // Checking output size - - EXPECT_EQ(output.size(), 2); - EXPECT_EQ(output[0].size(), correct_output1.size()); - EXPECT_EQ(output[1].size(), correct_output2.size()); - // Checking output - for (size_t i = 0; i < output[0].size(); ++i) { - EXPECT_LE(std::abs(output[0][i] - correct_output1[i]), DEFAULT_TOLERANCE); - } - for (size_t i = 0; i < output[1].size(); ++i) { - EXPECT_LE(std::abs(output[1][i] - correct_output2[i]), DEFAULT_TOLERANCE); - } + ASSERT_EQ(output.size(), 2u); + expectNear(output[0], correct_output1, DEFAULT_TOLERANCE); + expectNear(output[1], correct_output2, DEFAULT_TOLERANCE); } TEST(ONNX, Gelu) @@ -1781,13 +1600,10 @@ TEST(ONNX, ComparisonBroadcast) ASSERT_INCLUDE_AND_RUN(std::vector>, "Comparison_broadcast", input_A, input_B); + ASSERT_EQ(output.size(), 3u); const std::vector &output_less = output[2]; - EXPECT_EQ(output_less.size(), expected_output_less.size()); - - for (size_t i = 0; i < output_less.size(); ++i) { - EXPECT_EQ(output_less[i], expected_output_less[i]); - } + expectEqual(output_less, expected_output_less); } TEST(ONNX, ComparisonBroadcast3d) diff --git a/tmva/sofie/test/test_helpers.h b/tmva/sofie/test/test_helpers.h index 277366db32bce..94debe6062dea 100644 --- a/tmva/sofie/test/test_helpers.h +++ b/tmva/sofie/test/test_helpers.h @@ -96,6 +96,17 @@ void expectNear(std::vector const &output, std::vector const &expected, fl << "at output index " << i; } +/// Element-wise |output - expected| <= tolerance, for models with several outputs +template +void expectNear(std::vector> const &output, std::vector> const &expected, float tolerance) +{ + ASSERT_EQ(output.size(), expected.size()); + for (std::size_t i = 0; i < output.size(); ++i) { + SCOPED_TRACE("output tensor " + std::to_string(i)); + expectNear(output[i], expected[i], tolerance); + } +} + /// Element-wise exact equality template void expectEqual(std::vector const &output, std::vector const &expected) @@ -105,6 +116,17 @@ void expectEqual(std::vector const &output, std::vector const &expected) EXPECT_EQ(static_cast(output[i]), static_cast(expected[i])) << "at output index " << i; } +/// Element-wise exact equality, for models with several outputs +template +void expectEqual(std::vector> const &output, std::vector> const &expected) +{ + ASSERT_EQ(output.size(), expected.size()); + for (std::size_t i = 0; i < output.size(); ++i) { + SCOPED_TRACE("output tensor " + std::to_string(i)); + expectEqual(output[i], expected[i]); + } +} + bool includeModel(std::string const &modelName) { const std::string header = modelName + modelHeaderSuffix; From d74835045f1507f65cd9ad30f4b61598bb7430be Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Mon, 20 Jul 2026 21:55:32 +0000 Subject: [PATCH 5/6] [tmva][sofie] Share the common code of the PyTorch model generators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The six generators used by TestSofieModels were copy-pasted from one another: Conv1d and Conv2d differed in about 20 of their 186 lines. The command-line parsing, the ONNX export and the writing of the expected output were identical in all of them, so move those to a new ModelGeneratorUtils.py and keep only the network and the input tensor in each generator. The duplication had already caused the copy to drift from the original, and the --bn / --maxpool / --avgpool paths were broken in three of the four convolution generators: - Conv1d used BatchNorm2d, MaxPool2d and AvgPool2d. --bn failed with "expected 4D input (got 3D input)"; --maxpool did not fail but pooled over the channel dimension, since MaxPool2d accepts a 3D tensor as an unbatched input. - Conv3d likewise used the 2d layers, and failed the same way. - ConvTrans2d had the layers commented out in __init__ while forward() still referenced self.bn1, so --bn raised an AttributeError. Use the layer matching each generator's dimensionality. This is not covered today, since only Conv2d and Linear are exercised with those flags (Conv2d_BNORM_B5, Conv2d_MAXPOOL_B2, Conv2d_AVGPOOL_B2 and Linear_BNORM_B8), but the flags are advertised by every generator and are useful when debugging a model by hand. The "new ONNX exporter does not work for batchnorm" guard existed only in Conv2d and Linear, and was missing from the generators it was added after. It now lives in the shared export_onnx(). Also fix RecurrentModelGenerator, where --v assigned to a local instead of the module-level "verbose" flag it was meant to set. All 14 TestSofieModels tests still pass, and every --bn / --maxpool / --avgpool combination now runs for all the generators. 🤖 Done with the help of AI. --- tmva/sofie/test/CMakeLists.txt | 12 +-- tmva/sofie/test/Conv1dModelGenerator.py | 98 ++--------------- tmva/sofie/test/Conv2dModelGenerator.py | 98 ++--------------- tmva/sofie/test/Conv3dModelGenerator.py | 101 +++--------------- tmva/sofie/test/ConvTrans2dModelGenerator.py | 104 +++---------------- tmva/sofie/test/LinearModelGenerator.py | 96 +++-------------- tmva/sofie/test/ModelGeneratorUtils.py | 92 ++++++++++++++++ tmva/sofie/test/RecurrentModelGenerator.py | 97 +++-------------- 8 files changed, 175 insertions(+), 523 deletions(-) create mode 100644 tmva/sofie/test/ModelGeneratorUtils.py diff --git a/tmva/sofie/test/CMakeLists.txt b/tmva/sofie/test/CMakeLists.txt index 02242e5037536..3c3224cb4325b 100644 --- a/tmva/sofie/test/CMakeLists.txt +++ b/tmva/sofie/test/CMakeLists.txt @@ -122,12 +122,12 @@ if (BLAS_FOUND) endif() if (ROOT_TORCH_FOUND AND ROOT_ONNX_FOUND AND NOT broken_onnx) - configure_file(Conv1dModelGenerator.py Conv1dModelGenerator.py COPYONLY) - configure_file(Conv2dModelGenerator.py Conv2dModelGenerator.py COPYONLY) - configure_file(Conv3dModelGenerator.py Conv3dModelGenerator.py COPYONLY) - configure_file(ConvTrans2dModelGenerator.py ConvTrans2dModelGenerator.py COPYONLY) - configure_file(LinearModelGenerator.py LinearModelGenerator.py COPYONLY) - configure_file(RecurrentModelGenerator.py RecurrentModelGenerator.py COPYONLY) + # ModelGeneratorUtils.py holds the code shared by the generators below, so it + # has to be copied next to them. + configure_file(ModelGeneratorUtils.py ModelGeneratorUtils.py COPYONLY) + foreach(generator Conv1d Conv2d Conv3d ConvTrans2d Linear Recurrent) + configure_file(${generator}ModelGenerator.py ${generator}ModelGenerator.py COPYONLY) + endforeach() if (BLAS_FOUND) ROOT_ADD_GTEST(TestSofieModels TestSofieModels.cxx diff --git a/tmva/sofie/test/Conv1dModelGenerator.py b/tmva/sofie/test/Conv1dModelGenerator.py index b404955d3f148..b5c5783ef5587 100644 --- a/tmva/sofie/test/Conv1dModelGenerator.py +++ b/tmva/sofie/test/Conv1dModelGenerator.py @@ -1,15 +1,12 @@ #!/usr/bin/python3 -### generate COnv2d model using Pytorch - -import argparse -import sys +### generate Conv1d model using Pytorch import torch import torch.nn as nn import torch.nn.functional as F -result = [] +from ModelGeneratorUtils import make_parser, model_name, export_onnx, write_reference_output class Net(nn.Module): @@ -25,9 +22,9 @@ def __init__(self, nc = 1, ng = 1, nl = 4, use_bn = False, use_maxpool = False, self.use_avgpool = use_avgpool self.conv0 = nn.Conv1d(in_channels=self.nc, out_channels=4, kernel_size=2, groups=1, stride=1, padding=1) - if (self.use_bn): self.bn1 = nn.BatchNorm2d(4) - if (self.use_maxpool): self.pool1 = nn.MaxPool2d(2) - if (self.use_avgpool): self.pool1 = nn.AvgPool2d(2) + if (self.use_bn): self.bn1 = nn.BatchNorm1d(4) + if (self.use_maxpool): self.pool1 = nn.MaxPool1d(2) + if (self.use_avgpool): self.pool1 = nn.AvgPool1d(2) if (self.nl > 1): # output is 4x4 with optionally using group convolution self.conv1 = nn.Conv1d(in_channels=4, out_channels=8, groups = self.ng, kernel_size=3, stride=1, padding=1) @@ -51,29 +48,11 @@ def forward(self, x): def main(): - #print(arguments) - parser = argparse.ArgumentParser(description='PyTorch model generator') - parser.add_argument('params', type=int, nargs='+', - help='parameters for the Conv network : batchSize , inputChannels, inputImageSize, nGroups, nLayers ') - - parser.add_argument('--bn', action='store_true', default=False, - help='For using batch norm layer') - parser.add_argument('--maxpool', action='store_true', default=False, - help='For using max pool layer') - parser.add_argument('--avgpool', action='store_true', default=False, - help='For using average pool layer') - parser.add_argument('--v', action='store_true', default=False, - help='For verbose mode') - -# parser.add_argument('--oneD',action='store_true', default=False, help='For 1D convolution') - - + parser = make_parser('parameters for the Conv network : batchSize , inputChannels, inputImageSize, nGroups, nLayers ', + pooling=True) args = parser.parse_args() - #args.params = (4,2,4,1,4) - - np = len(args.params) - if (np < 5) : exit() + if (len(args.params) < 5) : exit() bsize = args.params[0] nc = args.params[1] d = args.params[2] @@ -82,14 +61,11 @@ def main(): use_bn = args.bn use_maxpool = args.maxpool use_avgpool = args.avgpool -# use_1d = args.oneD print ("using batch-size =",bsize,"nchannels =",nc,"dim =",d,"ngroups =",ngroups,"nlayers =",nlayers) if (use_bn): print("using batch normalization layer") if (use_maxpool): print("using maxpool layer") - #sample = torch.zeros([2,1,5,5]) - input = torch.zeros([]) for ib in range(0,bsize): xa = torch.ones([1, 1, d]) * (ib+1) if (nc > 1) : @@ -110,68 +86,16 @@ def main(): print("input data",xinput.shape) print(xinput) - name = "Conv1dModel" - if (use_bn): name += "_BN" - if (use_maxpool): name += "_MAXP" - if (use_avgpool): name += "_AVGP" - name += "_B" + str(bsize) - - saveOnnx=True - loadModel=False - savePtModel = False - + name = model_name("Conv1dModel", bsize, use_bn, use_maxpool, use_avgpool) model = Net(nc,ngroups,nlayers, use_bn, use_maxpool, use_avgpool) print(model) model(xinput) - model.forward(xinput) - - if savePtModel : - torch.save({'model_state_dict':model.state_dict()}, name + ".pt") - - if saveOnnx: - # dynamo export doesn't work on Python 3.14 - dynamo_export = (sys.version_info.major, sys.version_info.minor) != (3, 14) - - #check torch version - v = torch.__version__ - from packaging.version import Version - if (Version(v) >= Version("2.5.0")) : - torch.onnx.export( - model, - xinput, - name + ".onnx", - export_params=True, - dynamo=dynamo_export, - external_data=False - ) - else : - torch.onnx.export(model, xinput,name + ".onnx", export_params=True) - - if loadModel : - print('Loading model from file....') - checkpoint = torch.load(name + ".pt") - model.load_state_dict(checkpoint['model_state_dict']) - - # evaluate model in test mode - model.eval() - y = model.forward(xinput) - - print("output data : shape, ",y.shape) - print(y) - - outSize = y.nelement() - yvec = y.reshape([outSize]) - # for i in range(0,outSize): - # print(float(yvec[i])) - - f = open(name + ".out", "w") - for i in range(0,outSize): - f.write(str(float(yvec[i].detach()))+" ") - + export_onnx(model, xinput, name, use_bn) + write_reference_output(model, xinput, name) if __name__ == '__main__': diff --git a/tmva/sofie/test/Conv2dModelGenerator.py b/tmva/sofie/test/Conv2dModelGenerator.py index 37ba1308f269a..67ff6ee74bf85 100644 --- a/tmva/sofie/test/Conv2dModelGenerator.py +++ b/tmva/sofie/test/Conv2dModelGenerator.py @@ -1,15 +1,13 @@ #!/usr/bin/python3 -### generate COnv2d model using Pytorch - -import argparse -import sys +### generate Conv2d model using Pytorch import torch import torch.nn as nn import torch.nn.functional as F -result = [] +from ModelGeneratorUtils import make_parser, model_name, export_onnx, write_reference_output + class Net(nn.Module): @@ -47,36 +45,20 @@ def forward(self, x): if (self.nl == 1) : return x x = self.conv1(x) x = F.relu(x) - #print(x) x = self.conv2(x) x = F.relu(x) x = self.conv3(x) return x -def main(): - - #print(arguments) - parser = argparse.ArgumentParser(description='PyTorch model generator') - parser.add_argument('params', type=int, nargs='+', - help='parameters for the Conv network : batchSize , inputChannels, inputImageSize, nGroups, nLayers ') - - parser.add_argument('--bn', action='store_true', default=False, - help='For using batch norm layer') - parser.add_argument('--maxpool', action='store_true', default=False, - help='For using max pool layer') - parser.add_argument('--avgpool', action='store_true', default=False, - help='For using average pool layer') - parser.add_argument('--v', action='store_true', default=False, - help='For verbose mode') +def main(): + parser = make_parser('parameters for the Conv network : batchSize , inputChannels, inputImageSize, nGroups, nLayers ', + pooling=True) args = parser.parse_args() - #args.params = (4,2,4,1,4) - - np = len(args.params) - if (np < 5) : exit() + if (len(args.params) < 5) : exit() bsize = args.params[0] nc = args.params[1] d = args.params[2] @@ -90,8 +72,6 @@ def main(): if (use_bn): print("using batch normalization layer") if (use_maxpool): print("using maxpool layer") - #sample = torch.zeros([2,1,5,5]) - input = torch.zeros([]) for ib in range(0,bsize): xa = torch.ones([1, 1, d, d]) * (ib+1) if (nc > 1) : @@ -112,74 +92,16 @@ def main(): print("input data",xinput.shape) print(xinput) - name = "Conv2dModel" - if (use_bn): name += "_BN" - if (use_maxpool): name += "_MAXP" - if (use_avgpool): name += "_AVGP" - name += "_B" + str(bsize) - - saveOnnx=True - loadModel=False - savePtModel = False - + name = model_name("Conv2dModel", bsize, use_bn, use_maxpool, use_avgpool) model = Net(nc,ngroups,nlayers, use_bn, use_maxpool, use_avgpool) print(model) model(xinput) - model.forward(xinput) - - if savePtModel : - torch.save({'model_state_dict':model.state_dict()}, name + ".pt") - - - if saveOnnx: - # dynamo export doesn't work on Python 3.14 - dynamo_export = (sys.version_info.major, sys.version_info.minor) != (3, 14) - - #new ONNX exporter does not work for batchmorm - if (use_bn): dynamo_export=False - - #check torch version - v = torch.__version__ - from packaging.version import Version - if (Version(v) >= Version("2.5.0")) : - torch.onnx.export( - model, - xinput, - name + ".onnx", - export_params=True, - dynamo=dynamo_export, - external_data=False - ) - else : - torch.onnx.export(model, xinput,name + ".onnx", export_params=True) - - - - if loadModel : - print('Loading model from file....') - checkpoint = torch.load(name + ".pt") - model.load_state_dict(checkpoint['model_state_dict']) - - # evaluate model in test mode - model.eval() - y = model.forward(xinput) - - print("output data : shape, ",y.shape) - print(y) - - outSize = y.nelement() - yvec = y.reshape([outSize]) - # for i in range(0,outSize): - # print(float(yvec[i])) - - f = open(name + ".out", "w") - for i in range(0,outSize): - f.write(str(float(yvec[i].detach()))+" ") - + export_onnx(model, xinput, name, use_bn) + write_reference_output(model, xinput, name) if __name__ == '__main__': diff --git a/tmva/sofie/test/Conv3dModelGenerator.py b/tmva/sofie/test/Conv3dModelGenerator.py index 5dd60431dd48e..1611c00bcdf2e 100644 --- a/tmva/sofie/test/Conv3dModelGenerator.py +++ b/tmva/sofie/test/Conv3dModelGenerator.py @@ -1,15 +1,13 @@ #!/usr/bin/python3 -### generate COnv2d model using Pytorch - -import argparse -import sys +### generate Conv3d model using Pytorch import torch import torch.nn as nn import torch.nn.functional as F -result = [] +from ModelGeneratorUtils import make_parser, model_name, export_onnx, write_reference_output + class Net(nn.Module): @@ -24,9 +22,9 @@ def __init__(self, nc = 1, ng = 1, nl = 4, use_bn = False, use_maxpool = False, self.use_avgpool = use_avgpool self.conv0 = nn.Conv3d(in_channels=self.nc, out_channels=4, kernel_size=(2,3,3), groups=1, stride=1, padding=(0,1,1)) - if (self.use_bn): self.bn1 = nn.BatchNorm2d(4) - if (self.use_maxpool): self.pool1 = nn.MaxPool2d(2) - if (self.use_avgpool): self.pool1 = nn.AvgPool2d(2) + if (self.use_bn): self.bn1 = nn.BatchNorm3d(4) + if (self.use_maxpool): self.pool1 = nn.MaxPool3d(2) + if (self.use_avgpool): self.pool1 = nn.AvgPool3d(2) if (self.nl > 1): # output is 4x4 with optionally using group convolution self.conv1 = nn.Conv3d(in_channels=4, out_channels=8, groups = self.ng, kernel_size=3, stride=1, padding=1) @@ -47,42 +45,25 @@ def forward(self, x): if (self.nl == 1) : return x x = self.conv1(x) x = F.relu(x) - #print(x) x = self.conv2(x) x = F.relu(x) x = self.conv3(x) return x -def main(): - - #print(arguments) - parser = argparse.ArgumentParser(description='PyTorch model generator') - parser.add_argument('params', type=int, nargs='+', - help='parameters for the Conv network : batchSize , inputChannels, inputImageSize, nGroups, nLayers ') - - parser.add_argument('--bn', action='store_true', default=False, - help='For using batch norm layer') - parser.add_argument('--maxpool', action='store_true', default=False, - help='For using max pool layer') - parser.add_argument('--avgpool', action='store_true', default=False, - help='For using average pool layer') - parser.add_argument('--v', action='store_true', default=False, - help='For verbose mode') +def main(): + parser = make_parser('parameters for the Conv network : batchSize , inputChannels, inputImageSize, depth, nLayers ', + pooling=True) args = parser.parse_args() - #args.params = (4,2,4,1,4) - - np = len(args.params) - if (np < 5) : exit() + if (len(args.params) < 5) : exit() bsize = args.params[0] nc = args.params[1] d = args.params[2] depth = args.params[3] -# ngroups = args.params[3] - ngroups =1 + ngroups = 1 nlayers = args.params[4] use_bn = args.bn use_maxpool = args.maxpool @@ -92,8 +73,6 @@ def main(): if (use_bn): print("using batch normalization layer") if (use_maxpool): print("using maxpool layer") - #sample = torch.zeros([2,1,5,5]) - input = torch.zeros([]) xa = torch.zeros([]) for ib in range(0,bsize): for id in range(0,depth): @@ -121,68 +100,16 @@ def main(): print("input data",xinput.shape) print(xinput) - name = "Conv3dModel" - if (use_bn): name += "_BN" - if (use_maxpool): name += "_MAXP" - if (use_avgpool): name += "_AVGP" - name += "_B" + str(bsize) - - saveOnnx=True - loadModel=False - savePtModel = False - + name = model_name("Conv3dModel", bsize, use_bn, use_maxpool, use_avgpool) model = Net(nc,ngroups,nlayers, use_bn, use_maxpool, use_avgpool) print(model) model(xinput) - model.forward(xinput) - - if savePtModel : - torch.save({'model_state_dict':model.state_dict()}, name + ".pt") - - if saveOnnx: - # dynamo export doesn't work on Python 3.14 - dynamo_export = (sys.version_info.major, sys.version_info.minor) != (3, 14) - - #check torch version - v = torch.__version__ - from packaging.version import Version - if (Version(v) >= Version("2.5.0")) : - torch.onnx.export( - model, - xinput, - name + ".onnx", - export_params=True, - dynamo=dynamo_export, - external_data=False - ) - else : - torch.onnx.export(model, xinput,name + ".onnx", export_params=True) - - if loadModel : - print('Loading model from file....') - checkpoint = torch.load(name + ".pt") - model.load_state_dict(checkpoint['model_state_dict']) - - # evaluate model in test mode - model.eval() - y = model.forward(xinput) - - print("output data : shape, ",y.shape) - print(y) - - outSize = y.nelement() - yvec = y.reshape([outSize]) - # for i in range(0,outSize): - # print(float(yvec[i])) - - f = open(name + ".out", "w") - for i in range(0,outSize): - f.write(str(float(yvec[i].detach()))+" ") - + export_onnx(model, xinput, name, use_bn) + write_reference_output(model, xinput, name) if __name__ == '__main__': diff --git a/tmva/sofie/test/ConvTrans2dModelGenerator.py b/tmva/sofie/test/ConvTrans2dModelGenerator.py index ae4068dd7e34e..9d33aee9da5a6 100644 --- a/tmva/sofie/test/ConvTrans2dModelGenerator.py +++ b/tmva/sofie/test/ConvTrans2dModelGenerator.py @@ -1,15 +1,13 @@ #!/usr/bin/python3 -### generate COnv2d model using Pytorch - -import argparse -import sys +### generate ConvTranspose2d model using Pytorch import torch import torch.nn as nn import torch.nn.functional as F -result = [] +from ModelGeneratorUtils import make_parser, model_name, export_onnx, write_reference_output + class Net(nn.Module): @@ -24,9 +22,9 @@ def __init__(self, nc = 1, ng = 1, nl = 4, use_bn = False, use_maxpool = False, self.use_avgpool = use_avgpool self.conv0 = nn.ConvTranspose2d(in_channels=self.nc, out_channels=3, kernel_size=2, groups=1, stride=1, padding=1) - #if (self.use_bn): self.bn1 = nn.BatchNorm2d(4) - #if (self.use_maxpool): self.pool1 = nn.MaxPool2d(2) - #if (self.use_avgpool): self.pool1 = nn.AvgPool2d(2) + if (self.use_bn): self.bn1 = nn.BatchNorm2d(3) + if (self.use_maxpool): self.pool1 = nn.MaxPool2d(2) + if (self.use_avgpool): self.pool1 = nn.AvgPool2d(2) if (self.nl > 1): # output is 4x4 with optionally using group convolution self.conv1 = nn.ConvTranspose2d(in_channels=3, out_channels=8, groups = self.ng, kernel_size=3, stride=1, padding=1) @@ -42,41 +40,25 @@ def forward(self, x): if (self.use_bn): x = self.bn1(x) -# if (self.use_maxpool or self.use_avgpool): -# x = self.pool1(x) + if (self.use_maxpool or self.use_avgpool): + x = self.pool1(x) if (self.nl == 1) : return x x = self.conv1(x) x = F.relu(x) - #print(x) x = self.conv2(x) x = F.relu(x) x = self.conv3(x) return x -def main(): - - #print(arguments) - parser = argparse.ArgumentParser(description='PyTorch model generator') - parser.add_argument('params', type=int, nargs='+', - help='parameters for the Conv network : batchSize , inputChannels, inputImageSize, nGroups, nLayers ') - - parser.add_argument('--bn', action='store_true', default=False, - help='For using batch norm layer') - parser.add_argument('--maxpool', action='store_true', default=False, - help='For using max pool layer') - parser.add_argument('--avgpool', action='store_true', default=False, - help='For using average pool layer') - parser.add_argument('--v', action='store_true', default=False, - help='For verbose mode') +def main(): + parser = make_parser('parameters for the Conv network : batchSize , inputChannels, inputImageSize, nGroups, nLayers ', + pooling=True) args = parser.parse_args() - #args.params = (4,2,4,1,4) - - np = len(args.params) - if (np < 5) : exit() + if (len(args.params) < 5) : exit() bsize = args.params[0] nc = args.params[1] d = args.params[2] @@ -90,13 +72,9 @@ def main(): if (use_bn): print("using batch normalization layer") if (use_maxpool): print("using maxpool layer") - #sample = torch.zeros([2,1,5,5]) - input = torch.zeros([]) for ib in range(0,bsize): #fill input tensor with an increasing inputs xa = torch.arange(d*d).reshape([1,1,d,d])*float(ib+1) - #fill with all ones - #xa = torch.ones([1, 1, d, d]) * (ib+1) if (nc > 1) : xb = xa.neg() xc = torch.cat((xa,xb),1) # concatenate tensors @@ -115,68 +93,16 @@ def main(): print("input data",xinput.shape) print(xinput) - name = "ConvTrans2dModel" - if (use_bn): name += "_BN" - if (use_maxpool): name += "_MAXP" - if (use_avgpool): name += "_AVGP" - name += "_B" + str(bsize) - - saveOnnx=True - loadModel=False - savePtModel = False - + name = model_name("ConvTrans2dModel", bsize, use_bn, use_maxpool, use_avgpool) model = Net(nc,ngroups,nlayers, use_bn, use_maxpool, use_avgpool) print(model) model(xinput) - model.forward(xinput) - - if savePtModel : - torch.save({'model_state_dict':model.state_dict()}, name + ".pt") - - if saveOnnx: - # dynamo export doesn't work on Python 3.14 - dynamo_export = (sys.version_info.major, sys.version_info.minor) != (3, 14) - - #check torch version - v = torch.__version__ - from packaging.version import Version - if (Version(v) >= Version("2.5.0")) : - torch.onnx.export( - model, - xinput, - name + ".onnx", - export_params=True, - dynamo=dynamo_export, - external_data=False - ) - else : - torch.onnx.export(model, xinput,name + ".onnx", export_params=True) - - if loadModel : - print('Loading model from file....') - checkpoint = torch.load(name + ".pt") - model.load_state_dict(checkpoint['model_state_dict']) - - # evaluate model in test mode - model.eval() - y = model.forward(xinput) - - print("output data : shape, ",y.shape) - print(y) - - outSize = y.nelement() - yvec = y.reshape([outSize]) - # for i in range(0,outSize): - # print(float(yvec[i])) - - f = open(name + ".out", "w") - for i in range(0,outSize): - f.write(str(float(yvec[i].detach()))+" ") - + export_onnx(model, xinput, name, use_bn) + write_reference_output(model, xinput, name) if __name__ == '__main__': diff --git a/tmva/sofie/test/LinearModelGenerator.py b/tmva/sofie/test/LinearModelGenerator.py index 797e14e23ee48..e81ff59f8ba9e 100644 --- a/tmva/sofie/test/LinearModelGenerator.py +++ b/tmva/sofie/test/LinearModelGenerator.py @@ -1,15 +1,13 @@ #!/usr/bin/python3 -### generate COnv2d model using Pytorch - -import argparse -import sys +### generate Linear model using Pytorch import torch import torch.nn as nn import torch.nn.functional as F -result = [] +from ModelGeneratorUtils import make_parser, model_name, export_onnx, write_reference_output + class Net(nn.Module): @@ -20,8 +18,6 @@ def __init__(self, nd = 1, nc = 1, nl = 4, use_bn = False): self.nl = nl self.use_bn = use_bn - nout = 50 - if (nl == 1) : nout = nc self.out0 = nn.Linear(in_features=nd, out_features=50) if (self.use_bn): self.bn1 = nn.BatchNorm1d(50) self.out1 = nn.Linear(in_features=50, out_features=100) @@ -44,40 +40,25 @@ def forward(self, x): return x -def main(): - - parser = argparse.ArgumentParser(description='PyTorch model generator') - parser.add_argument('params', type=int, nargs='+', - help='parameters for the Dense network : batchSize , inputChannels, nlayers ') - parser.add_argument('--bn', action='store_true', default=False, - help='For using batch norm layer') - parser.add_argument('--v', action='store_true', default=False, - help='For verbose mode') +def main(): + parser = make_parser('parameters for the Dense network : batchSize , inputChannels, nlayers ') args = parser.parse_args() - - bsize = 1 - d = 10 nlayers = 4 noutput = 4 - np = len(args.params) - if (np < 2) : exit() + if (len(args.params) < 2) : exit() bsize = args.params[0] d = args.params[1] - if (np > 2) : nlayers = args.params[2] - + if (len(args.params) > 2) : nlayers = args.params[2] print ("using batch-size =",bsize,"input dim =",d,"nlayers =",nlayers) use_bn = args.bn if (use_bn) : print("using batch normalization layer") - verbose = args.v - - xinput = torch.zeros([]) for ib in range(0,bsize): xa = torch.ones([1,d]) * (ib+1) #concatenate tensors @@ -97,73 +78,20 @@ def main(): else : xinput = torch.cat((xinput,xa),1) - #if (verbose): print("input data",xinput.shape) print(xinput) - name = "LinearModel" - if (use_bn): name += "_BN" - name += "_B" + str(bsize) - - saveOnnx=True - loadModel=False - savePtModel = False - + name = model_name("LinearModel", bsize, use_bn) model = Net(d,noutput,nlayers,use_bn) model(xinput) - model.forward(xinput) - - - if savePtModel : - torch.save({'model_state_dict':model.state_dict()}, name + ".pt") - - - if saveOnnx: - # dynamo export doesn't work on Python 3.14 - dynamo_export = (sys.version_info.major, sys.version_info.minor) != (3, 14) - - #new ONNX exporter does not work for batchmorm - if (use_bn): dynamo_export=False - - #check torch version - v = torch.__version__ - print("using torch version: ",v) - from packaging.version import Version - if (Version(v) >= Version("2.5.0")) : - torch.onnx.export( - model, - xinput, - name + ".onnx", - export_params=True, - dynamo=dynamo_export, - external_data=False - ) - else : - torch.onnx.export(model, xinput,name + ".onnx", export_params=True) - - - if loadModel : - print('Loading model from file....') - checkpoint = torch.load(name + ".pt") - model.load_state_dict(checkpoint['model_state_dict']) - - #set model in evaluation format - model.eval() - y = model.forward(xinput_test) - - print("output data : shape, ",y.shape) - print(y) - - outSize = y.nelement() - yvec = y.reshape([outSize]) - - f = open(name + ".out", "w") - for i in range(0,outSize): - f.write(str(float(yvec[i].detach()))+" ") + export_onnx(model, xinput, name, use_bn) + # the expected output is evaluated on the test input, which differs from the + # training input when batch normalization is used + write_reference_output(model, xinput_test, name) if __name__ == '__main__': diff --git a/tmva/sofie/test/ModelGeneratorUtils.py b/tmva/sofie/test/ModelGeneratorUtils.py new file mode 100644 index 0000000000000..1dcd1c0e354cb --- /dev/null +++ b/tmva/sofie/test/ModelGeneratorUtils.py @@ -0,0 +1,92 @@ +#!/usr/bin/python3 + +### Helpers shared by the PyTorch model generators used in TestSofieModels. +### +### The generators (Conv1d/Conv2d/Conv3d/ConvTrans2d/Linear/Recurrent) only +### differ in the network they build and in the shape of the input tensor; +### the command line, the ONNX export and the writing of the expected output +### are identical, so they live here. Keeping a single copy avoids the drift +### that repeated copy-pasting had already introduced between them. + +import argparse +import sys + +import torch + + +def make_parser(params_help, pooling=False, recurrent=False): + """Command line shared by all the generators. + + The positional "params" are generator specific and described by params_help. + """ + parser = argparse.ArgumentParser(description='PyTorch model generator') + parser.add_argument('params', type=int, nargs='+', help=params_help) + if recurrent: + parser.add_argument('--lstm', action='store_true', default=False, + help='For using LSTM layer') + parser.add_argument('--gru', action='store_true', default=False, + help='For using GRU layer') + else: + parser.add_argument('--bn', action='store_true', default=False, + help='For using batch norm layer') + if pooling: + parser.add_argument('--maxpool', action='store_true', default=False, + help='For using max pool layer') + parser.add_argument('--avgpool', action='store_true', default=False, + help='For using average pool layer') + parser.add_argument('--v', action='store_true', default=False, + help='For verbose mode') + return parser + + +def model_name(base, bsize, use_bn=False, use_maxpool=False, use_avgpool=False): + """The naming convention TestSofieModels expects, e.g. Conv2dModel_BN_B5.""" + name = base + if use_bn: + name += "_BN" + if use_maxpool: + name += "_MAXP" + if use_avgpool: + name += "_AVGP" + return name + "_B" + str(bsize) + + +def export_onnx(model, xinput, name, use_bn=False, dynamo=None): + """Export model to .onnx. + + dynamo=None selects the exporter automatically: the dynamo based one, + except where it is known not to work. Pass an explicit value to override. + """ + if dynamo is None: + # dynamo export doesn't work on Python 3.14 + dynamo = (sys.version_info.major, sys.version_info.minor) != (3, 14) + # the new ONNX exporter does not work for batch normalization + if use_bn: + dynamo = False + + from packaging.version import Version + if Version(torch.__version__) >= Version("2.5.0"): + torch.onnx.export( + model, + xinput, + name + ".onnx", + export_params=True, + dynamo=dynamo, + external_data=False, + ) + else: + torch.onnx.export(model, xinput, name + ".onnx", export_params=True) + + +def write_reference_output(model, xinput, name): + """Evaluate the model and write its flattened output to .out.""" + model.eval() + y = model.forward(xinput) + + print("output data : shape, ", y.shape) + print(y) + + yvec = y.reshape([y.nelement()]) + with open(name + ".out", "w") as f: + for i in range(0, y.nelement()): + f.write(str(float(yvec[i].detach())) + " ") diff --git a/tmva/sofie/test/RecurrentModelGenerator.py b/tmva/sofie/test/RecurrentModelGenerator.py index 9050e5db97868..d8b6a47c5f70a 100644 --- a/tmva/sofie/test/RecurrentModelGenerator.py +++ b/tmva/sofie/test/RecurrentModelGenerator.py @@ -1,21 +1,20 @@ #!/usr/bin/python3 -### generate COnv2d model using Pytorch - -import argparse +### generate recurrent (RNN/LSTM/GRU) model using Pytorch import torch import torch.nn as nn -result = [] -verbose=False +from ModelGeneratorUtils import make_parser, model_name, export_onnx, write_reference_output + +verbose = False + class Net(nn.Module): def __init__(self, type, input_size, hidden_size, num_layers=1, output_size=2): super(Net, self).__init__() - if (type == "LSTM") : self.rc = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True) if (type == "GRU"): @@ -30,17 +29,9 @@ def __init__(self, type, input_size, hidden_size, num_layers=1, output_size=2): def forward(self, x): - batch_size = x.size(0) - - #Initializing hidden state for first input using method defined below - #hidden = self.init_hidden(batch_size) - # Passing in the input and hidden state into the model and obtaining outputs rc_out,self.hidden_cell = self.rc(x) - # Reshaping the outputs such that it can be fit into the fully connected layer - #out = lstm_out.view(self.hidden_dim,-1) - if (verbose): print("recurrent output", rc_out) print("hidden out ",self.hidden_cell) @@ -55,45 +46,30 @@ def init_hidden(self, batch_size): hidden = (torch.zeros(self.n_layers, batch_size, self.hidden_dim), torch.zeros(1, 1, self.hidden_dim)) return hidden -def main(): - - #print(arguments) - parser = argparse.ArgumentParser(description='PyTorch model generator') - parser.add_argument('params', type=int, nargs='+', - help='parameters for the Recurrent network : batchSize , inputSize, seqSize, hiddenSize, nLayers ') - parser.add_argument('--lstm', action='store_true', default=False, - help='For using LSTM layer') - parser.add_argument('--gru', action='store_true', default=False, - help='For using GRU layer') - # parser.add_argument('--avgpool', action='store_true', default=False, - # help='For using average pool layer') - parser.add_argument('--v', action='store_true', default=False, - help='For verbose mode') +def main(): + global verbose + parser = make_parser('parameters for the Recurrent network : batchSize , inputSize, seqSize, hiddenSize, nLayers ', + recurrent=True) args = parser.parse_args() - ##args.params = (1,3,10,4,1) - - np = len(args.params) - if (np < 5) : exit() + if (len(args.params) < 5) : exit() bsize = args.params[0] nd = args.params[1] nt = args.params[2] nh = args.params[3] nl = args.params[4] - type = "RNN" if (args.lstm): type = "LSTM" if (args.gru): type = "GRU" - if (args.v) : verbose=True + verbose = args.v print ("using batch-size =",bsize,"inputSize=",nd,"sequence length",nt,"hidden Size",nh,"nlayers",nl) - xinput = torch.zeros([]) xb = torch.zeros([]) for ib in range(0, bsize): for it in range(0,nt) : @@ -111,60 +87,17 @@ def main(): print("input data",xinput.shape) print(xinput) - name = type + "Model" - name += "_B" + str(bsize) - - saveOnnx=True - loadModel=False - savePtModel = False - + name = model_name(type + "Model", bsize) model = Net(type, nd, nh, nl) print(model) model(xinput) - model.forward(xinput) - - if savePtModel : - torch.save({'model_state_dict':model.state_dict()}, name + ".pt") - - if saveOnnx: - #check torch version - v = torch.__version__ - from packaging.version import Version - if (Version(v) >= Version("2.5.0")) : - torch.onnx.export( - model, - xinput, - name + ".onnx", - export_params=True, - dynamo=False, #for recurrent model new export does not work - external_data=False - ) - else : - torch.onnx.export(model, xinput,name + ".onnx", export_params=True) - - if loadModel : - print('Loading model from file....') - checkpoint = torch.load(name + ".pt") - model.load_state_dict(checkpoint['model_state_dict']) - - # evaluate model in test mode - model.eval() - y = model.forward(xinput) - - print("output data : shape, ",y.shape) - print(y) - - outSize = y.nelement() - yvec = y.reshape([outSize]) - - f = open(name + ".out", "w") - for i in range(0,outSize): - f.write(str(float(yvec[i].detach()))+" ") - + # the new exporter does not work for recurrent models + export_onnx(model, xinput, name, dynamo=False) + write_reference_output(model, xinput, name) if __name__ == '__main__': From 99a71137d8f85d2c9b4e016d9928b79973492a6d Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Mon, 20 Jul 2026 22:20:37 +0000 Subject: [PATCH 6/6] [tmva][sofie] Support asymmetric padding in Im2col MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UTILITY::Im2col and Im2col_3d took a single pad per axis and used it both as the offset of the first window and, doubled, in the output size: output_h = (height + 2 * pad_h - (dilation_h * (kernel_h - 1) + 1)) / stride_h + 1 ONNX allows the padding at the beginning and at the end of an axis to differ, through the "pads" attribute and through SAME_UPPER/SAME_LOWER whenever the total padding is odd, which happens for an even kernel size. ROperator_Conv's shape inference already handles that correctly with fAttrPads[d] + fAttrPads[d + fDim], and so does ROperator_Pool, but the Im2col call could not express it: GenerateInitCode replaced the two pads by their average and printed TMVA SOFIE Operator Conv: asymmetric padding not supported. Assume an average padding The result was worse than an approximation. The _xcol buffer is sized from the (correct) inferred output shape, while Im2col filled it using the averaged padding, so it wrote a different number of columns than were allocated and the convolution read uninitialized memory. For a 1x1x4x4 input with a 2x2 kernel and SAME_UPPER, SOFIE returned 21 25 20 24 11 14 ... where ONNX gives 10 14 18 10 26 30 ... Take the begin and end pads separately in Im2col and Im2col_3d, use their sum in the output size and the begin pad as the window offset, and pass fAttrPads[d] and fAttrPads[d + fDim] from ROperator_Conv. The unreachable Im2col_3d calls emitted by ROperator_ConvTranspose (they sit behind a "3D Conv Transpose not yet supported" throw) are updated too, so they do not become a latent compile error. Add six models covering the 1d, 2d, 3d and grouped code paths of ROperator_Conv, with SAME_UPPER, SAME_LOWER and explicit "pads". All six fail before this change and pass after it. ConvWithAsymmetricPadding, disabled since it was added, is enabled. It turned out not to be blocked by this bug at all: its pads [1, 0, 1, 0] are symmetric on each axis, so the averaging never triggered. What made it fail is that the model declared an output shape of [1, 1, 5, 5] while the attributes give [1, 1, 4, 2]. Correct the declared shape. Closes #21873 🤖 Done with the help of AI. --- tmva/sofie/inc/TMVA/ROperator_Conv.hxx | 66 ++++------ .../inc/TMVA/ROperator_ConvTranspose.hxx | 18 +-- tmva/sofie/inc/TMVA/SOFIE_common.hxx | 29 +++-- tmva/sofie/test/TestCustomModelsFromONNX.cxx | 61 ++++++++- tmva/sofie/test/generate_input_models.py | 119 +++++++++++++++++- 5 files changed, 231 insertions(+), 62 deletions(-) diff --git a/tmva/sofie/inc/TMVA/ROperator_Conv.hxx b/tmva/sofie/inc/TMVA/ROperator_Conv.hxx index 4e2e90cbc130c..a306121fb0714 100644 --- a/tmva/sofie/inc/TMVA/ROperator_Conv.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_Conv.hxx @@ -458,30 +458,14 @@ public: // trick for speed is using caffe im2col and output a matrix which contains filtered values as rows. // By doing this one has consecutive memory reads and writes // Resulting matrix op_xcol is (input channels * filter_h * filter_w , output_h * output_w) - if (fDim ==1) { - if (fAttrPads[0] != fAttrPads[1] ) { - std::cout << "TMVA SOFIE Operator Conv: asymmetric padding not supported. Assume an average padding " - << std::endl; - fAttrPads[0] = (fAttrPads[0] + fAttrPads[1]) / 2; - } - fAttrPads[1] = 0; + // fAttrPads holds the begin pads in [0, fDim) and the end pads in [fDim, 2 * fDim), + // which is the layout Im2col expects. They may differ: ONNX allows it through the + // "pads" attribute, and SAME_UPPER / SAME_LOWER produce it whenever the total + // padding along an axis is odd (an even kernel size). + if (fDim == 1) { + // the 1d case is emitted as a 2d one of height 1, for which stride_h is 1 fAttrStrides[1] = 1; } - if (fDim == 2) { - if (fAttrPads[0] != fAttrPads[2] || fAttrPads[1] != fAttrPads[3]) { - std::cout << "TMVA SOFIE Operator Conv: asymmetric padding not supported. Assume an average padding " << std::endl; - fAttrPads[0] = (fAttrPads[0] + fAttrPads[2]) / 2; - fAttrPads[1] = (fAttrPads[1] + fAttrPads[3]) / 2; - } - } - if (fDim == 3) { - if (fAttrPads[0] != fAttrPads[3] || fAttrPads[1] != fAttrPads[4] || fAttrPads[2] != fAttrPads[5]) { - std::cout << "TMVA SOFIE Operator Conv: asymmetric padding not supported. Assume an average padding " << std::endl; - fAttrPads[0] = (fAttrPads[0] + fAttrPads[3]) / 2; - fAttrPads[1] = (fAttrPads[1] + fAttrPads[4]) / 2; - fAttrPads[2] = (fAttrPads[2] + fAttrPads[5]) / 2; - } - } out << SP << SP << "size_t out_offset = n * " << outputBatchStride << ";\n"; if (fAttrGroup == 1) { @@ -491,15 +475,16 @@ public: if (fDim < 3) { out << SP << SP << "TMVA::Experimental::SOFIE::UTILITY::Im2col(tensor_" << fNX << " + x_offset," - // channels, height, width, kernel_h, kernel_w, pad_h, pad_w, stride_h, stride_w, dilation_h, - // dilation_w, + // channels, height, width, kernel_h, kernel_w, pad_h_begin, pad_h_end, pad_w_begin, + // pad_w_end, stride_h, stride_w, dilation_h, dilation_w, // << fShapeW[1] << "," << iHeight << "," << iWidth << ","; if (fDim == 1) - out << "1, " << fAttrKernelShape[0] << ",0," << fAttrPads[0] << ",1," << fAttrStrides[0] << ",1," - << fAttrDilations[0]; + out << "1, " << fAttrKernelShape[0] << ",0,0," << fAttrPads[0] << "," << fAttrPads[1] << ",1," + << fAttrStrides[0] << ",1," << fAttrDilations[0]; else // dim ==2 - out << fAttrKernelShape[0] << "," << fAttrKernelShape[1] << "," << fAttrPads[0] << "," << fAttrPads[1] + out << fAttrKernelShape[0] << "," << fAttrKernelShape[1] << "," << fAttrPads[0] << "," + << fAttrPads[2] << "," << fAttrPads[1] << "," << fAttrPads[3] << "," << fAttrStrides[0] << "," << fAttrStrides[1] << "," << fAttrDilations[0] << "," << fAttrDilations[1]; out << "," << "tensor_" <(tensor_" << fNX << " + x_offset," - // channels, d, h, w, k_d, k_h, k_w, pad_d, pad_h, pad_w, stride_d, stride_h, stride_w, - // dilation_d, dilation_h, dilation_w, + // channels, d, h, w, k_d, k_h, k_w, pad_d_begin, pad_d_end, pad_h_begin, pad_h_end, + // pad_w_begin, pad_w_end, stride_d, stride_h, stride_w, dilation_d, dilation_h, dilation_w, // << fShapeW[1] << "," << iDepth << "," << iHeight << "," << iWidth << "," << fAttrKernelShape[0] << "," << fAttrKernelShape[1] << "," << fAttrKernelShape[2] << "," - << fAttrPads[0] << "," << fAttrPads[1] << "," << fAttrPads[2] << "," + << fAttrPads[0] << "," << fAttrPads[3] << "," << fAttrPads[1] << "," << fAttrPads[4] << "," + << fAttrPads[2] << "," << fAttrPads[5] << "," << fAttrStrides[0] << "," << fAttrStrides[1] << "," << fAttrStrides[2] << "," << fAttrDilations[0] << "," << fAttrDilations[1] << "," << fAttrDilations[2] << "," << "tensor_" << fNX << "_xcol);\n\n "; @@ -549,15 +535,16 @@ public: if (fDim < 3) { out << SP << SP << "TMVA::Experimental::SOFIE::UTILITY::Im2col(tensor_" << fNX << " + x_offset," - // channels, height, width, kernel_h, kernel_w, pad_h, pad_w, stride_h, stride_w, dilation_h, - // dilation_w, + // channels, height, width, kernel_h, kernel_w, pad_h_begin, pad_h_end, pad_w_begin, + // pad_w_end, stride_h, stride_w, dilation_h, dilation_w, // << fShapeW[1] << "," << iHeight << "," << iWidth << ","; if (fDim == 1) - out << "1, " << fAttrKernelShape[0] << ",0," << fAttrPads[0] << ",1," << fAttrStrides[0] << ",1," - << fAttrDilations[0]; + out << "1, " << fAttrKernelShape[0] << ",0,0," << fAttrPads[0] << "," << fAttrPads[1] << ",1," + << fAttrStrides[0] << ",1," << fAttrDilations[0]; else // dim ==2 - out << fAttrKernelShape[0] << "," << fAttrKernelShape[1] << "," << fAttrPads[0] << "," << fAttrPads[1] + out << fAttrKernelShape[0] << "," << fAttrKernelShape[1] << "," << fAttrPads[0] << "," + << fAttrPads[2] << "," << fAttrPads[1] << "," << fAttrPads[3] << "," << fAttrStrides[0] << "," << fAttrStrides[1] << "," << fAttrDilations[0] << "," << fAttrDilations[1]; out << ", tensor_" << fNX << "_xcol);\n\n "; @@ -565,12 +552,13 @@ public: // 3d im2col out << SP << SP << "TMVA::Experimental::SOFIE::UTILITY::Im2col_3d(tensor_" << fNX << " + x_offset," - // channels, d, h, w, k_d, k_h, k_w, pad_d, pad_h, pad_w, stride_d, stride_h, stride_w, - // dilation_d, dilation_h, dilation_w, + // channels, d, h, w, k_d, k_h, k_w, pad_d_begin, pad_d_end, pad_h_begin, pad_h_end, + // pad_w_begin, pad_w_end, stride_d, stride_h, stride_w, dilation_d, dilation_h, dilation_w, // << fShapeW[1] << "," << iDepth << "," << iHeight << "," << iWidth << "," << fAttrKernelShape[0] << "," - << fAttrKernelShape[1] << "," << fAttrKernelShape[2] << "," << fAttrPads[0] << "," << fAttrPads[1] - << "," << fAttrPads[2] << "," << fAttrStrides[0] << "," << fAttrStrides[1] << "," << fAttrStrides[2] + << fAttrKernelShape[1] << "," << fAttrKernelShape[2] << "," << fAttrPads[0] << "," << fAttrPads[3] + << "," << fAttrPads[1] << "," << fAttrPads[4] << "," << fAttrPads[2] << "," << fAttrPads[5] + << "," << fAttrStrides[0] << "," << fAttrStrides[1] << "," << fAttrStrides[2] << "," << fAttrDilations[0] << "," << fAttrDilations[1] << "," << fAttrDilations[2] << ",tensor_" << fNX << "_xcol);\n\n "; } diff --git a/tmva/sofie/inc/TMVA/ROperator_ConvTranspose.hxx b/tmva/sofie/inc/TMVA/ROperator_ConvTranspose.hxx index ed7c2a23bc849..fd307ec350bf2 100644 --- a/tmva/sofie/inc/TMVA/ROperator_ConvTranspose.hxx +++ b/tmva/sofie/inc/TMVA/ROperator_ConvTranspose.hxx @@ -502,12 +502,13 @@ std::string ROperator_ConvTranspose::Generate(std::string OpName) throw std::runtime_error("TMVA SOFIE 3D Conv Transpose not yet supported"); out << SP << SP << "TMVA::Experimental::SOFIE::UTILITY::Im2col_3d(tensor_" << fNX << " + x_offset," - // channels, d, h, w, k_d, k_h, k_w, pad_d, pad_h, pad_w, stride_d, stride_h, stride_w, - // dilation_d, dilation_h, dilation_w, + // channels, d, h, w, k_d, k_h, k_w, pad_d_begin, pad_d_end, pad_h_begin, pad_h_end, + // pad_w_begin, pad_w_end, stride_d, stride_h, stride_w, dilation_d, dilation_h, dilation_w, // << fShapeX[1] << "," << oDepth << "," << oHeight << "," << oWidth << "," << fAttrKernelShape[0] << "," - << fAttrKernelShape[1] << "," << fAttrKernelShape[2] << "," << fAttrPads[0] << "," << fAttrPads[1] << "," - << fAttrPads[2] << "," << fAttrStrides[0] << "," << fAttrStrides[1] << "," << fAttrStrides[2] << "," + << fAttrKernelShape[1] << "," << fAttrKernelShape[2] << "," << fAttrPads[0] << "," << fAttrPads[3] << "," + << fAttrPads[1] << "," << fAttrPads[4] << "," << fAttrPads[2] << "," << fAttrPads[5] << "," + << fAttrStrides[0] << "," << fAttrStrides[1] << "," << fAttrStrides[2] << "," << fAttrDilations[0] << "," << fAttrDilations[1] << "," << fAttrDilations[2] << ",tensor_" << fNX << "_xcol);\n\n "; } @@ -555,12 +556,13 @@ std::string ROperator_ConvTranspose::Generate(std::string OpName) out << SP << SP << "TMVA::Experimental::SOFIE::UTILITY::Im2col_3d(tensor_" << fNX << " + x_offset," - // channels, d, h, w, k_d, k_h, k_w, pad_d, pad_h, pad_w, stride_d, stride_h, stride_w, - // dilation_d, dilation_h, dilation_w, + // channels, d, h, w, k_d, k_h, k_w, pad_d_begin, pad_d_end, pad_h_begin, pad_h_end, + // pad_w_begin, pad_w_end, stride_d, stride_h, stride_w, dilation_d, dilation_h, dilation_w, // << fShapeX[1] << "," << oDepth << "," << oHeight << "," << oWidth << "," << fAttrKernelShape[0] << "," - << fAttrKernelShape[1] << "," << fAttrKernelShape[2] << "," << fAttrPads[0] << "," << fAttrPads[1] << "," - << fAttrPads[2] << "," << fAttrStrides[0] << "," << fAttrStrides[1] << "," << fAttrStrides[2] << "," + << fAttrKernelShape[1] << "," << fAttrKernelShape[2] << "," << fAttrPads[0] << "," << fAttrPads[3] << "," + << fAttrPads[1] << "," << fAttrPads[4] << "," << fAttrPads[2] << "," << fAttrPads[5] << "," + << fAttrStrides[0] << "," << fAttrStrides[1] << "," << fAttrStrides[2] << "," << fAttrDilations[0] << "," << fAttrDilations[1] << "," << fAttrDilations[2] << "," << "tensor_" << fNX << "_xcol);\n\n "; } diff --git a/tmva/sofie/inc/TMVA/SOFIE_common.hxx b/tmva/sofie/inc/TMVA/SOFIE_common.hxx index 09f247eed24d2..445885fd04086 100644 --- a/tmva/sofie/inc/TMVA/SOFIE_common.hxx +++ b/tmva/sofie/inc/TMVA/SOFIE_common.hxx @@ -537,25 +537,29 @@ inline bool is_a_ge_zero_and_a_lt_b(int a, int b) { /// ( a1 a2 a3 0 b1 b2 b3 0 c1 c2 c3 0 0 0 0 0 ) for k4 /// +/// The padding at the beginning and at the end of an axis can differ, as ONNX +/// allows for the "pads" attribute and as the SAME_UPPER / SAME_LOWER autopad +/// modes produce whenever the total padding is odd. template void Im2col(const T *data_im, const int channels, const int height, const int width, const int kernel_h, - const int kernel_w, const int pad_h, const int pad_w, const int stride_h, const int stride_w, + const int kernel_w, const int pad_h_begin, const int pad_h_end, const int pad_w_begin, + const int pad_w_end, const int stride_h, const int stride_w, const int dilation_h, const int dilation_w, T *data_col) { - const int output_h = (height + 2 * pad_h - (dilation_h * (kernel_h - 1) + 1)) / stride_h + 1; - const int output_w = (width + 2 * pad_w - (dilation_w * (kernel_w - 1) + 1)) / stride_w + 1; + const int output_h = (height + pad_h_begin + pad_h_end - (dilation_h * (kernel_h - 1) + 1)) / stride_h + 1; + const int output_w = (width + pad_w_begin + pad_w_end - (dilation_w * (kernel_w - 1) + 1)) / stride_w + 1; const int channel_size = height * width; for (int channel = channels; channel--; data_im += channel_size) { for (int kernel_row = 0; kernel_row < kernel_h; kernel_row++) { for (int kernel_col = 0; kernel_col < kernel_w; kernel_col++) { - int input_row = -pad_h + kernel_row * dilation_h; + int input_row = -pad_h_begin + kernel_row * dilation_h; for (int output_rows = output_h; output_rows; output_rows--) { if (!is_a_ge_zero_and_a_lt_b(input_row, height)) { for (int output_cols = output_w; output_cols; output_cols--) { *(data_col++) = 0; } } else { - int input_col = -pad_w + kernel_col * dilation_w; + int input_col = -pad_w_begin + kernel_col * dilation_w; for (int output_col = output_w; output_col; output_col--) { if (is_a_ge_zero_and_a_lt_b(input_col, width)) { *(data_col++) = data_im[input_row * width + input_col]; @@ -577,20 +581,21 @@ template void Im2col_3d(const T *data_im, const int channels, const int depth, const int height, const int width, const int kernel_d, const int kernel_h, const int kernel_w, - const int pad_d, const int pad_h, const int pad_w, + const int pad_d_begin, const int pad_d_end, const int pad_h_begin, const int pad_h_end, + const int pad_w_begin, const int pad_w_end, const int stride_d, const int stride_h, const int stride_w, const int dilation_d, const int dilation_h, const int dilation_w, T *data_col) { - const int output_h = (height + 2 * pad_h - (dilation_h * (kernel_h - 1) + 1)) / stride_h + 1; - const int output_w = (width + 2 * pad_w - (dilation_w * (kernel_w - 1) + 1)) / stride_w + 1; - const int output_d = (depth + 2 * pad_d - (dilation_d * (kernel_d - 1) + 1)) / stride_d + 1; + const int output_h = (height + pad_h_begin + pad_h_end - (dilation_h * (kernel_h - 1) + 1)) / stride_h + 1; + const int output_w = (width + pad_w_begin + pad_w_end - (dilation_w * (kernel_w - 1) + 1)) / stride_w + 1; + const int output_d = (depth + pad_d_begin + pad_d_end - (dilation_d * (kernel_d - 1) + 1)) / stride_d + 1; const int channel_size = height * width * depth; // assume data are c x d x h x w for (int channel = channels; channel--; data_im += channel_size) { for (int kernel_depth = 0; kernel_depth < kernel_d; kernel_depth++) { for (int kernel_row = 0; kernel_row < kernel_h; kernel_row++) { for (int kernel_col = 0; kernel_col < kernel_w; kernel_col++) { - int input_dep = -pad_d + kernel_depth * dilation_d; + int input_dep = -pad_d_begin + kernel_depth * dilation_d; for (int output_dep = output_d; output_dep; output_dep--) { if (!is_a_ge_zero_and_a_lt_b(input_dep, depth)) { for (int output_rows = output_h; output_rows; output_rows--) { @@ -599,14 +604,14 @@ void Im2col_3d(const T *data_im, const int channels, } } } else { - int input_row = -pad_h + kernel_row * dilation_h; + int input_row = -pad_h_begin + kernel_row * dilation_h; for (int output_rows = output_h; output_rows; output_rows--) { if (!is_a_ge_zero_and_a_lt_b(input_row, height)) { for (int output_cols = output_w; output_cols; output_cols--) { *(data_col++) = 0; } } else { - int input_col = -pad_w + kernel_col * dilation_w; + int input_col = -pad_w_begin + kernel_col * dilation_w; for (int output_col = output_w; output_col; output_col--) { if (is_a_ge_zero_and_a_lt_b(input_col, width)) { *(data_col++) = data_im[input_dep * width * height + input_row * width + input_col]; diff --git a/tmva/sofie/test/TestCustomModelsFromONNX.cxx b/tmva/sofie/test/TestCustomModelsFromONNX.cxx index f469967f96e78..4aa43e8f20d4f 100644 --- a/tmva/sofie/test/TestCustomModelsFromONNX.cxx +++ b/tmva/sofie/test/TestCustomModelsFromONNX.cxx @@ -299,8 +299,7 @@ TEST(ONNX, ConvWithDynShapeStride) expectNear(output, correct_output, DEFAULT_TOLERANCE); } -// Disables test (asymmetric padding not supported) -TEST(DISABLED_ONNX, ConvWithAsymmetricPadding) +TEST(ONNX, ConvWithAsymmetricPadding) { SofieReference ref = readReference("ConvWithAsymmetricPadding"); @@ -309,6 +308,64 @@ TEST(DISABLED_ONNX, ConvWithAsymmetricPadding) expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); } +// The models below pad differently at the beginning and at the end of the same +// axis, which is what SAME_UPPER / SAME_LOWER produce for an even kernel size. +// They cover the 1d, 2d, 3d and grouped code paths of ROperator_Conv. + +TEST(ONNX, ConvSameUpperEvenKernel) +{ + SofieReference ref = readReference("ConvSameUpperEvenKernel"); + + ASSERT_INCLUDE_AND_RUN(std::vector, "ConvSameUpperEvenKernel", ref.f32("input0")); + + expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); +} + +TEST(ONNX, ConvSameLowerEvenKernel) +{ + SofieReference ref = readReference("ConvSameLowerEvenKernel"); + + ASSERT_INCLUDE_AND_RUN(std::vector, "ConvSameLowerEvenKernel", ref.f32("input0")); + + expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); +} + +TEST(ONNX, ConvAsymmetricPads1d) +{ + SofieReference ref = readReference("ConvAsymmetricPads1d"); + + ASSERT_INCLUDE_AND_RUN(std::vector, "ConvAsymmetricPads1d", ref.f32("input0")); + + expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); +} + +TEST(ONNX, ConvAsymmetricPads2d) +{ + SofieReference ref = readReference("ConvAsymmetricPads2d"); + + ASSERT_INCLUDE_AND_RUN(std::vector, "ConvAsymmetricPads2d", ref.f32("input0")); + + expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); +} + +TEST(ONNX, ConvAsymmetricPads3d) +{ + SofieReference ref = readReference("ConvAsymmetricPads3d"); + + ASSERT_INCLUDE_AND_RUN(std::vector, "ConvAsymmetricPads3d", ref.f32("input0")); + + expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); +} + +TEST(ONNX, ConvAsymmetricPadsGrouped) +{ + SofieReference ref = readReference("ConvAsymmetricPadsGrouped"); + + ASSERT_INCLUDE_AND_RUN(std::vector, "ConvAsymmetricPadsGrouped", ref.f32("input0")); + + expectNear(output, ref.f32("output0"), DEFAULT_TOLERANCE); +} + TEST(ONNX, MaxPool1d) { SofieReference ref = readReference("MaxPool1d"); diff --git a/tmva/sofie/test/generate_input_models.py b/tmva/sofie/test/generate_input_models.py index 9e668f6569217..ef7aa27ac9d96 100644 --- a/tmva/sofie/test/generate_input_models.py +++ b/tmva/sofie/test/generate_input_models.py @@ -769,7 +769,8 @@ def make_ConvWithAsymmetricPadding(): _vi('W', FLOAT, [1, 1, 3, 3]), ], outputs=[ - _vi('y', FLOAT, [1, 1, 5, 5]), + # h: (7 + 1 + 1 - 3) / 2 + 1 = 4, w: (5 + 0 + 0 - 3) / 2 + 1 = 2 + _vi('y', FLOAT, [1, 1, 4, 2]), ], initializer=[ _tensor('W', FLOAT, [1, 1, 3, 3], [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]), @@ -889,6 +890,110 @@ def make_ConvWithDynShapeStride(): return _model(graph, opset=13, ir_version=13) +# The models below have a different padding at the beginning and at the end of +# the same axis. This is what ONNX produces for the SAME_UPPER / SAME_LOWER +# autopad modes whenever the total padding is odd, i.e. for an even kernel size, +# and what the "pads" attribute can express directly. They cover the three code +# paths of ROperator_Conv (1d, 2d and 3d) plus the grouped one. + +def make_ConvSameUpperEvenKernel(): + """Ops: Conv""" + # 4 - 1 + 2 - 4 = 1 total pad -> begin 0, end 1 + nodes = [ + helper.make_node('Conv', ['x', 'W'], ['y'], kernel_shape=[2, 2], auto_pad='SAME_UPPER', strides=[1, 1]), + ] + graph = helper.make_graph( + nodes, + 'ConvSameUpperEvenKernel', + inputs=[_vi('x', FLOAT, [1, 1, 4, 4]), _vi('W', FLOAT, [1, 1, 2, 2])], + outputs=[_vi('y', FLOAT, [1, 1, 4, 4])], + initializer=[_tensor('W', FLOAT, [1, 1, 2, 2], [1.0, 2.0, 3.0, 4.0])], + ) + return _model(graph, opset=14, ir_version=7, producer_name='python_script') + + +def make_ConvSameLowerEvenKernel(): + """Ops: Conv""" + # same as above but the extra pad goes at the beginning -> begin 1, end 0 + nodes = [ + helper.make_node('Conv', ['x', 'W'], ['y'], kernel_shape=[2, 2], auto_pad='SAME_LOWER', strides=[1, 1]), + ] + graph = helper.make_graph( + nodes, + 'ConvSameLowerEvenKernel', + inputs=[_vi('x', FLOAT, [1, 1, 4, 4]), _vi('W', FLOAT, [1, 1, 2, 2])], + outputs=[_vi('y', FLOAT, [1, 1, 4, 4])], + initializer=[_tensor('W', FLOAT, [1, 1, 2, 2], [1.0, 2.0, 3.0, 4.0])], + ) + return _model(graph, opset=14, ir_version=7, producer_name='python_script') + + +def make_ConvAsymmetricPads1d(): + """Ops: Conv""" + # 1d path: pads = [begin, end] = [0, 1] + nodes = [ + helper.make_node('Conv', ['x', 'W'], ['y'], kernel_shape=[2], pads=[0, 1], strides=[1]), + ] + graph = helper.make_graph( + nodes, + 'ConvAsymmetricPads1d', + inputs=[_vi('x', FLOAT, [1, 1, 5]), _vi('W', FLOAT, [1, 1, 2])], + outputs=[_vi('y', FLOAT, [1, 1, 5])], + initializer=[_tensor('W', FLOAT, [1, 1, 2], [1.0, 2.0])], + ) + return _model(graph, opset=14, ir_version=7, producer_name='python_script') + + +def make_ConvAsymmetricPads2d(): + """Ops: Conv""" + # pads = [begin_h, begin_w, end_h, end_w] = [1, 0, 0, 1] + nodes = [ + helper.make_node('Conv', ['x', 'W'], ['y'], kernel_shape=[3, 3], pads=[1, 0, 0, 1], strides=[1, 1]), + ] + graph = helper.make_graph( + nodes, + 'ConvAsymmetricPads2d', + inputs=[_vi('x', FLOAT, [1, 1, 5, 5]), _vi('W', FLOAT, [1, 1, 3, 3])], + outputs=[_vi('y', FLOAT, [1, 1, 4, 4])], + initializer=[_tensor('W', FLOAT, [1, 1, 3, 3], [1.0] * 9)], + ) + return _model(graph, opset=14, ir_version=7, producer_name='python_script') + + +def make_ConvAsymmetricPads3d(): + """Ops: Conv""" + # pads = [begin_d, begin_h, begin_w, end_d, end_h, end_w] = [0, 0, 0, 1, 1, 1] + nodes = [ + helper.make_node('Conv', ['x', 'W'], ['y'], kernel_shape=[2, 2, 2], pads=[0, 0, 0, 1, 1, 1], + strides=[1, 1, 1]), + ] + graph = helper.make_graph( + nodes, + 'ConvAsymmetricPads3d', + inputs=[_vi('x', FLOAT, [1, 1, 3, 3, 3]), _vi('W', FLOAT, [1, 1, 2, 2, 2])], + outputs=[_vi('y', FLOAT, [1, 1, 3, 3, 3])], + initializer=[_tensor('W', FLOAT, [1, 1, 2, 2, 2], [1.0] * 8)], + ) + return _model(graph, opset=14, ir_version=7, producer_name='python_script') + + +def make_ConvAsymmetricPadsGrouped(): + """Ops: Conv""" + # same as ConvAsymmetricPads2d, but through the group != 1 code path + nodes = [ + helper.make_node('Conv', ['x', 'W'], ['y'], kernel_shape=[3, 3], pads=[1, 0, 0, 1], strides=[1, 1], + group=2), + ] + graph = helper.make_graph( + nodes, + 'ConvAsymmetricPadsGrouped', + inputs=[_vi('x', FLOAT, [1, 2, 5, 5]), _vi('W', FLOAT, [2, 1, 3, 3])], + outputs=[_vi('y', FLOAT, [1, 2, 4, 4])], + initializer=[_tensor('W', FLOAT, [2, 1, 3, 3], [1.0] * 9 + [2.0] * 9)], + ) + return _model(graph, opset=14, ir_version=7, producer_name='python_script') + + def make_ConvWithPadding(): """Ops: Conv""" nodes = [ @@ -4935,6 +5040,12 @@ def make_Where(): 'ConvTransposeBias2d': make_ConvTransposeBias2d, 'ConvTransposeBias2dBatched': make_ConvTransposeBias2dBatched, 'ConvWithAsymmetricPadding': make_ConvWithAsymmetricPadding, + 'ConvSameUpperEvenKernel': make_ConvSameUpperEvenKernel, + 'ConvSameLowerEvenKernel': make_ConvSameLowerEvenKernel, + 'ConvAsymmetricPads1d': make_ConvAsymmetricPads1d, + 'ConvAsymmetricPads2d': make_ConvAsymmetricPads2d, + 'ConvAsymmetricPads3d': make_ConvAsymmetricPads3d, + 'ConvAsymmetricPadsGrouped': make_ConvAsymmetricPadsGrouped, 'ConvWithAutopadSameLower': make_ConvWithAutopadSameLower, 'ConvWithAutopadSameUpper': make_ConvWithAutopadSameUpper, 'ConvWithDilation': make_ConvWithDilation, @@ -5141,6 +5252,12 @@ def rand_f32(seed, shape): 'ConvTransposeBias2d': [f32(np.arange(0.0, 9.0), (1, 1, 3, 3))], 'ConvTransposeBias2dBatched': [f32(np.arange(0.0, 18.0), (2, 1, 3, 3))], 'ConvWithAsymmetricPadding': [f32(np.arange(0.0, 35.0), (1, 1, 7, 5))], + 'ConvSameUpperEvenKernel': [f32(np.arange(0.0, 16.0), (1, 1, 4, 4))], + 'ConvSameLowerEvenKernel': [f32(np.arange(0.0, 16.0), (1, 1, 4, 4))], + 'ConvAsymmetricPads1d': [f32(np.arange(0.0, 5.0), (1, 1, 5))], + 'ConvAsymmetricPads2d': [f32(np.arange(0.0, 25.0), (1, 1, 5, 5))], + 'ConvAsymmetricPads3d': [f32(np.arange(0.0, 27.0), (1, 1, 3, 3, 3))], + 'ConvAsymmetricPadsGrouped': [f32(np.arange(0.0, 50.0), (1, 2, 5, 5))], 'ConvWithAutopadSameLower': [f32(np.arange(0.0, 25.0), (1, 1, 5, 5))], 'ConvWithAutopadSameUpper': [f32(np.arange(0.0, 25.0), (1, 1, 5, 5))], 'ConvWithDilation': [f32(np.arange(0.0, 49.0), (1, 1, 7, 7))],