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/ROperator_Pool.hxx b/tmva/sofie/inc/TMVA/ROperator_Pool.hxx index 6f142808d8e3d..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; @@ -331,6 +340,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 +367,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 +384,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 +408,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 +429,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 +454,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 +480,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/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/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__': diff --git a/tmva/sofie/test/TestCustomModelsFromONNX.cxx b/tmva/sofie/test/TestCustomModelsFromONNX.cxx index 02cc1762ad877..4aa43e8f20d4f 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); } @@ -296,14 +296,10 @@ 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) -TEST(DISABLED_ONNX, ConvWithAsymmetricPadding) +TEST(ONNX, ConvWithAsymmetricPadding) { SofieReference ref = readReference("ConvWithAsymmetricPadding"); @@ -312,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"); @@ -348,6 +402,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"); @@ -357,6 +429,69 @@ 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, 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"); + + ASSERT_INCLUDE_AND_RUN(std::vector, "AveragePool2d_CeilMode", ref.f32("input0")); + + 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"); + + 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"); @@ -390,9 +525,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) @@ -401,9 +534,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) @@ -424,10 +555,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) @@ -440,9 +568,6 @@ TEST(ONNX, ReduceMean_kFirst) } TEST(ONNX, ReduceSum){ - constexpr float TOLERANCE = DEFAULT_TOLERANCE; - - // Preparing the standard input std::vector input({ 5, 2, 3, @@ -454,21 +579,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); - - float correct[] = {24}; + std::vector 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, @@ -480,15 +596,9 @@ TEST(ONNX, ReduceSumSquare){ ASSERT_INCLUDE_AND_RUN(std::vector, "ReduceSumSquare", input); - // Checking output size - EXPECT_EQ(output.size(), 2); - - float correct[] = {38, 66}; + std::vector 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) @@ -912,22 +1022,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); - // 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, DEFAULT_TOLERANCE); } TEST(ONNX, LayerNormalization2d) @@ -1145,13 +1245,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]] @@ -1162,19 +1256,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 @@ -1182,19 +1268,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 @@ -1202,53 +1284,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); } } } @@ -1261,12 +1335,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) @@ -1277,12 +1346,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) @@ -1292,14 +1356,9 @@ 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); - // 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) @@ -1312,12 +1371,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) { @@ -1326,12 +1380,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) @@ -1341,12 +1390,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) @@ -1357,14 +1401,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) @@ -1375,14 +1412,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) @@ -1393,14 +1423,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) @@ -1413,12 +1436,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) @@ -1432,12 +1450,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) @@ -1451,12 +1464,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) @@ -1470,12 +1478,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) @@ -1489,12 +1492,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) @@ -1509,12 +1507,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) @@ -1526,12 +1519,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) @@ -1542,12 +1530,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) { @@ -1558,12 +1541,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) @@ -1574,12 +1552,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) @@ -1592,12 +1565,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) @@ -1610,12 +1578,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) @@ -1628,12 +1591,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) @@ -1645,18 +1603,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) @@ -1708,13 +1657,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/generate_input_models.py b/tmva/sofie/test/generate_input_models.py index cc4f82527ac33..ef7aa27ac9d96 100644 --- a/tmva/sofie/test/generate_input_models.py +++ b/tmva/sofie/test/generate_input_models.py @@ -244,6 +244,184 @@ 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 = [ + 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 = [ @@ -591,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]), @@ -711,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 = [ @@ -3273,6 +3556,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 = [ @@ -4685,6 +5019,13 @@ def make_Where(): 'AddBroadcast5': make_AddBroadcast5, '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, 'AvgPool': make_AvgPool, 'Cast': make_Cast, 'Clip': make_Clip, @@ -4699,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, @@ -4765,7 +5112,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, @@ -4885,6 +5234,13 @@ 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))], + '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))], '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))], @@ -4896,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))], @@ -4930,9 +5292,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,)), @@ -4951,9 +5315,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))], @@ -4974,6 +5340,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': [ @@ -5029,9 +5397,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)), 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;