aboutsummaryrefslogtreecommitdiff
path: root/src/backends/tosaCommon/operatorMappings/ResizeOperator.cpp
blob: bb1eabd27bb84bb1a3c7a13fb573eff8c6f788f6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
//
// Copyright © 2023-2024 Arm Ltd and Contributors. All rights reserved.
// SPDX-License-Identifier: MIT
//
// Copyright © 2020, 2023 The TensorFlow Authors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
//

#include <numeric>
#include "ResizeOperator.hpp"

// This function is paraphrased from:
// tensorflow/compiler/mlir/tosa/transforms/legalize_common.cc from function convertResizeOp
// tensorflow/lite/kernels/internal/reference/resize_utils.h
TosaSerializationBasicBlock* ConvertResizeToTosaOperator(const Layer* layer,
                                                         const std::vector<const TensorInfo*>& inputs,
                                                         const std::vector<const TensorInfo*>& outputs,
                                                         const ResizeDescriptor* resizeDescriptor)
{
    ARMNN_THROW_INVALIDARG_MSG_IF_FALSE( inputs.size() == 1,
                                         "ConvertResizeToTosaOperator: Resize must have only one input." );
    ARMNN_THROW_INVALIDARG_MSG_IF_FALSE( resizeDescriptor->m_DataLayout == DataLayout::NHWC,
                                         "ConvertResizeToTosaOperator: NCHW not supported.");

    ResizeMode mode;
    if (resizeDescriptor->m_Method == ResizeMethod::NearestNeighbor)
    {
        mode = tosa::ResizeMode_NEAREST;
    }
    else if (resizeDescriptor->m_Method == ResizeMethod::Bilinear)
    {
        mode = tosa::ResizeMode_BILINEAR;
        throw armnn::InvalidArgumentException("ConvertResizeToTosaOperator: Unimplemented Resize method.");
    }
    else
    {
        throw armnn::InvalidArgumentException("ConvertResizeToTosaOperator: Unsupported Resize method.");
    }

    std::string inputName = std::string("input_");
    std::string outputName = std::string("output0_");
    std::string blockName  = std::string("Op_RESIZE_block_") + GetUniqueTosaMappingID();

    // If a layer is present then the block will be used for execution, so input and output names need to be determined
    // using the previous and following layers so the graph is connected correctly. For validation this doesn't matter.
    if(layer != nullptr)
    {
        inputName  = GenerateUniqueInputName(layer->GetInputSlot(0));
        outputName = GenerateUniqueOutputName(*layer);
    }

    int32_t inputHeight = static_cast<int32_t>(inputs[0]->GetShape()[1]);
    int32_t inputWidth = static_cast<int32_t>(inputs[0]->GetShape()[2]);

    int32_t outputHeight = static_cast<int32_t>(resizeDescriptor->m_TargetHeight);
    int32_t outputWidth = static_cast<int32_t>(resizeDescriptor->m_TargetWidth);
    bool alignCorners = resizeDescriptor->m_AlignCorners;
    bool halfPixel = resizeDescriptor->m_HalfPixelCenters;

    // Go from ArmNN parameters (outputShape, halfPixel and alignedCorners)
    // to TOSA parameters (scale, offset and border)
    // Align corners sets the scaling ratio to (O - 1)/(I - 1) rather than O / I.
    auto preprocessResizeParameters = [&](int inputSize, int outputSize, int& scale_n, int& scale_d, int& offset)
    {
        // Dimension is length 1, we are just sampling from one value.
        if (inputSize == 1)
        {
            scale_n = outputSize;
            scale_d = 1;
            offset = 0;
            return;
        }

        // Apply if aligned and capable to be aligned.
        // Align corners sets the scaling ratio to (OH - 1)/(IH - 1) rather than OH / IH. Same for width.
        bool applyAligned = alignCorners && (outputSize > 1);
        scale_n = applyAligned ? (outputSize - 1) : outputSize;
        scale_d = applyAligned ? (inputSize - 1) : inputSize;

        // Simplify the scales, make sure they are even values.
        int gcd = std::gcd(scale_n, scale_d);
        scale_n = 2 * scale_n / gcd;
        scale_d = 2 * scale_d / gcd;

        // If half pixel centers then input and output sampling positions are offset by 1/2 pixel.
        offset = halfPixel ? (scale_d / 2 - scale_n / 2) : 0;

        // Reduce the scaling ratio if possible, we know scale_n and scale_d are even
        if ((offset & 1) == 0)
        {
            scale_n /= 2;
            scale_d /= 2;
            offset /= 2;
        }
    };

    int scale_y_n, scale_y_d, offset_y;
    int scale_x_n, scale_x_d, offset_x;
    preprocessResizeParameters(inputHeight, outputHeight, scale_y_n, scale_y_d, offset_y);
    preprocessResizeParameters(inputWidth, outputWidth, scale_x_n, scale_x_d, offset_x);

    int border_y = scale_y_d * (outputHeight - 1) - scale_y_n * (inputHeight - 1) + offset_y;
    int border_x = scale_x_d * (outputWidth - 1) - scale_x_n * (inputWidth - 1) + offset_x;

    // [scale_y_n, scale_y_d, scale_x_n, scale_x_d]
    std::vector<int16_t> scale = { static_cast<int16_t>(scale_y_n),
                                   static_cast<int16_t>(scale_y_d),
                                   static_cast<int16_t>(scale_x_n),
                                   static_cast<int16_t>(scale_x_d) };

    // [offset_y, offset_x]
    std::vector<int16_t> offset = { static_cast<int16_t>(offset_y),
                                    static_cast<int16_t>(offset_x) };
    // [border_y, border_x]
    std::vector<int16_t> border = { static_cast<int16_t>(border_y),
                                    static_cast<int16_t>(border_x) };

    auto isInt16Range = [](int x)
    {
        return (x <= std::numeric_limits<int16_t>::max()) && (x >= std::numeric_limits<int16_t>::min());
    };

    if (inputs[0]->IsQuantized())
    {
        // It isn't commonly seen these numbers aren't fit within 16 bits, and won't match TFLite reference.
        if (!isInt16Range(scale_y_n) || !isInt16Range(scale_y_d) ||
            !isInt16Range(scale_x_n) || !isInt16Range(scale_x_d) ||
            !isInt16Range(offset_y) || !isInt16Range(offset_x) ||
            !isInt16Range(border_y) || !isInt16Range(border_x))
        {
            throw armnn::Exception("ConvertResizeToTosaOperator: stride or offset out of 16 bit range");
        }
    }

    TosaResizeAttribute resizeAttribute(scale, offset, border, mode);

    auto* op = new TosaSerializationOperator(Op_RESIZE,
                                             Attribute_ResizeAttribute,
                                             &resizeAttribute,
                                             {inputName},
                                             {outputName});

    std::vector<TosaSerializationTensor*> tensors;

    // Only add input tensors if connected layer is an input layer.
    // As intermediate or constant tensors will be created separately.
    // There also can't be duplicate tensor.
    if(inputName.find("input_") != std::string::npos)
    {
        std::vector<int32_t> inputShape = GetTosaTensorShape(inputs[0]->GetShape());
        DType inputDType = ArmNNToDType(inputs[0]->GetDataType());

        tensors.push_back(new TosaSerializationTensor(inputName, inputShape, inputDType, {}));
    }

    std::vector<int32_t> outputShape = GetTosaTensorShape(outputs[0]->GetShape());
    DType outputDType = ArmNNToDType(outputs[0]->GetDataType());

    tensors.push_back(new TosaSerializationTensor(outputName, outputShape, outputDType, {}));

    // operatorInputNames/operatorOutputNames ends up being the same as
    // blockInputNames/blockOutputNames for one-to-one ArmNN to TOSA mappings
    return new TosaSerializationBasicBlock(blockName, // name
                                           mainName, // region name
                                           {op}, // operators
                                           tensors, // tensors
                                           {inputName}, // inputs
                                           {outputName}); // outputs
}