blob: 5f421076eb2d556ad5c0510e8ddcf0a4490b1e1b [file] [log] [blame]
arovir01b0717b52018-09-05 17:03:25 +01001//
2// Copyright © 2017 Arm Ltd. All rights reserved.
3// SPDX-License-Identifier: MIT
4//
5
6#include "HalPolicy.hpp"
7
Matthew Benthamf61c2702019-04-23 16:43:27 +01008#include <armnn/Optional.hpp>
9
10#include "FullyConnected.hpp"
arovir015602b192018-10-04 16:15:02 +010011
arovir01b0717b52018-09-05 17:03:25 +010012namespace armnn_driver
13{
14namespace hal_1_0
15{
16
17bool HalPolicy::ConvertOperation(const Operation& operation, const Model& model, ConversionData& data)
18{
19 switch (operation.type)
20 {
21 case V1_0::OperationType::ADD:
22 return ConvertAdd(operation, model, data);
23 case V1_0::OperationType::AVERAGE_POOL_2D:
24 return ConvertAveragePool2d(operation, model, data);
25 case V1_0::OperationType::CONCATENATION:
26 return ConvertConcatenation(operation, model, data);
27 case V1_0::OperationType::CONV_2D:
28 return ConvertConv2d(operation, model, data);
29 case V1_0::OperationType::DEPTHWISE_CONV_2D:
30 return ConvertDepthwiseConv2d(operation, model, data);
David Monahanacf479a2019-05-29 14:27:04 +010031 case V1_0::OperationType::DEQUANTIZE:
32 return ConvertDequantize(operation, model, data);
arovir01b0717b52018-09-05 17:03:25 +010033 case V1_0::OperationType::FLOOR:
34 return ConvertFloor(operation, model, data);
35 case V1_0::OperationType::FULLY_CONNECTED:
36 return ConvertFullyConnected(operation, model, data);
37 case V1_0::OperationType::LOCAL_RESPONSE_NORMALIZATION:
38 return ConvertLocalResponseNormalization(operation, model, data);
39 case V1_0::OperationType::LOGISTIC:
40 return ConvertLogistic(operation, model, data);
41 case V1_0::OperationType::LSTM:
42 return ConvertLstm(operation, model, data);
43 case V1_0::OperationType::L2_NORMALIZATION:
44 return ConvertL2Normalization(operation, model, data);
45 case V1_0::OperationType::L2_POOL_2D:
46 return ConvertL2Pool2d(operation, model, data);
47 case V1_0::OperationType::MAX_POOL_2D:
48 return ConvertMaxPool2d(operation, model, data);
49 case V1_0::OperationType::MUL:
50 return ConvertMul(operation, model, data);
51 case V1_0::OperationType::RELU:
52 return ConvertReLu(operation, model, data);
53 case V1_0::OperationType::RELU1:
54 return ConvertReLu1(operation, model, data);
55 case V1_0::OperationType::RELU6:
56 return ConvertReLu6(operation, model, data);
57 case V1_0::OperationType::SOFTMAX:
58 return ConvertSoftmax(operation, model, data);
59 case V1_0::OperationType::TANH:
60 return ConvertTanH(operation, model, data);
61 case V1_0::OperationType::RESHAPE:
62 return ConvertReshape(operation, model, data);
63 case V1_0::OperationType::RESIZE_BILINEAR:
64 return ConvertResizeBilinear(operation, model, data);
65 default:
66 return Fail("%s: Operation type %s not supported in ArmnnDriver",
67 __func__, toString(operation.type).c_str());
68 }
69}
70
71bool HalPolicy::ConvertAdd(const Operation& operation, const Model& model, ConversionData& data)
72{
73 LayerInputHandle input0 = ConvertToLayerInputHandle(operation, 0, model, data);
74 LayerInputHandle input1 = ConvertToLayerInputHandle(operation, 1, model, data);
75
76 if (!input0.IsValid() || !input1.IsValid())
77 {
78 return Fail("%s: Operation has invalid inputs", __func__);
79 }
80
81 // The FuseActivation parameter is always the input index 2
82 // and it should be optional
83 ActivationFn activationFunction;
84 if (!GetOptionalInputActivation(operation, 2, activationFunction, model, data))
85 {
86 return Fail("%s: Operation has invalid inputs", __func__);
87 }
88
89 const Operand* outputOperand = GetOutputOperand(operation, 0, model);
90 if (!outputOperand)
91 {
92 return false;
93 }
94
95 const armnn::TensorInfo outInfo = GetTensorInfoForOperand(*outputOperand);
96
Nattapat Chaimanowongd5fd9762019-04-04 13:33:10 +010097 if (!IsLayerSupportedForAnyBackend(__func__,
98 armnn::IsAdditionSupported,
99 data.m_Backends,
100 input0.GetTensorInfo(),
101 input1.GetTensorInfo(),
102 outInfo))
arovir01b0717b52018-09-05 17:03:25 +0100103 {
104 return false;
105 }
106
107 armnn::IConnectableLayer* const startLayer = data.m_Network->AddAdditionLayer();
108 armnn::IConnectableLayer* const endLayer = ProcessActivation(outInfo, activationFunction, startLayer, data);
109
110 const armnn::TensorInfo& inputTensorInfo0 = input0.GetTensorInfo();
111 const armnn::TensorInfo& inputTensorInfo1 = input1.GetTensorInfo();
112
113 if (endLayer != nullptr)
114 {
115 BroadcastTensor(input0, input1, startLayer, *data.m_Network);
116 return SetupAndTrackLayerOutputSlot(operation, 0, *endLayer, model, data);
117 }
118 else
119 {
120 return Fail("%s: ProcessActivation failed", __func__);
121 }
122}
123
124bool HalPolicy::ConvertAveragePool2d(const Operation& operation, const Model& model, ConversionData& data)
125{
126 return ConvertPooling2d(operation, __func__, armnn::PoolingAlgorithm::Average, model, data);
127}
128
129bool HalPolicy::ConvertConcatenation(const Operation& operation, const Model& model, ConversionData& data)
130{
131 // The first N (0..N-1) inputs are tensors. The Nth input is the concatenation axis.
132 if (operation.inputs.size() <= 1)
133 {
134 return Fail("%s: Operation has insufficient arguments", __func__);
135 }
136
137 // Get inputs and outputs
138 const std::size_t numInputTensors = operation.inputs.size() - 1;
139
140 int32_t concatDim;
141 if (!GetInputScalar(operation, numInputTensors, OperandType::INT32, concatDim, model, data))
142 {
143 return Fail("%s: Operation has invalid inputs", __func__);
144 }
145
146 const Operand* const outputOperand = GetOutputOperand(operation, 0, model);
147 if (!outputOperand)
148 {
149 return Fail("%s: Operation has no outputs", __func__);
150 }
151
152
153 armnn::TensorInfo outputInfo = GetTensorInfoForOperand(*outputOperand);
154 armnn::TensorShape outputShape = outputInfo.GetShape();
155
156 //
157 // handle negative concat dims along the lines of tensorflow as described here:
158 // https://www.tensorflow.org/api_docs/python/tf/concat
159 // "negative axis refers to axis + rank(values)-th dimension"
160 //
161 if (concatDim < 0)
162 {
163 concatDim += outputShape.GetNumDimensions();
164 }
165
166 if (concatDim >= static_cast<int32_t>(outputShape.GetNumDimensions()) || concatDim < 0)
167 {
168 return Fail("%s: Operation has invalid concat axis: %d", __func__, concatDim);
169 }
170
171 std::vector<LayerInputHandle> inputHandles;
172 std::vector<armnn::TensorShape> inputShapes;
173
174 inputHandles.reserve(numInputTensors);
175 inputShapes.reserve(numInputTensors);
176
177 bool inputsHaveBeenReshaped = false;
178 unsigned int tensorDimensionsAdded = 0;
179
180 for (uint32_t i = 0; i < numInputTensors; ++i)
181 {
182 const Operand* const operand = GetInputOperand(operation, i, model);
183 if (!operand)
184 {
185 return Fail("%s: Operation has invalid inputs", __func__);
186 }
187
188 armnn::TensorShape operandShape = GetTensorShapeForOperand(*operand);
189 LayerInputHandle operandInputHandle = ConvertToLayerInputHandle(operation, i, model, data);
190
191 if (operandShape.GetNumDimensions() == 0)
192 {
193 return Fail("%s: Operands with rank 0 are not supported", __func__);
194 }
195
196 if (RequiresReshape(operandShape))
197 {
198 inputsHaveBeenReshaped = true;
199
200 armnn::TensorInfo reshapeInfo = operandInputHandle.GetTensorInfo();
201
202 // Expand the tensor to three dimensions
203 if (operandShape.GetNumDimensions() == 2)
204 {
205 reshapeInfo.SetShape(armnn::TensorShape({1, operandShape[0], operandShape[1]}));
206 tensorDimensionsAdded = 1;
207 }
208 else
209 {
210 reshapeInfo.SetShape(armnn::TensorShape({1, 1, operandShape[0]}));
211 tensorDimensionsAdded = 2;
212 }
213
214 armnn::IConnectableLayer& newReshape = AddReshapeLayer(
215 *data.m_Network,
216 operandInputHandle,
217 reshapeInfo
218 );
219
220 // Point to the reshape operation rather then the input operation
221 operandShape = reshapeInfo.GetShape();
222 operandInputHandle = LayerInputHandle(true, &newReshape.GetOutputSlot(0), reshapeInfo);
223 }
224
225 inputShapes.emplace_back(operandShape);
226 inputHandles.emplace_back(operandInputHandle);
227
228 if (!inputHandles.back().IsValid())
229 {
230 return Fail("%s: Operation has invalid inputs", __func__);
231 }
232 }
233
234 BOOST_ASSERT(inputShapes.size() == inputHandles.size());
235
236 if (inputsHaveBeenReshaped)
237 {
238 // Adjust the concatenation dimension by the amount of dimensions added (if any)
239 concatDim += tensorDimensionsAdded;
240
241 // Add extra dimensions to the output shape to reflect the addition of the reshape layers
242 if (tensorDimensionsAdded == 1)
243 {
244 outputShape = armnn::TensorShape({1, outputShape[0], outputShape[1]});
245 }
246 else if (tensorDimensionsAdded == 2)
247 {
narpra01f176d5a2018-11-18 20:17:48 +0000248 outputShape = armnn::TensorShape({1, 1, outputShape[0]});
arovir01b0717b52018-09-05 17:03:25 +0100249 }
250 }
251
narpra01f176d5a2018-11-18 20:17:48 +0000252 // Check if permutations is required and get the pair of permutations required for the concatenation.
253 // Permutation is required when the concat dimension is 2 for a 4D tensor or 1 for a 3D tensor.
arovir01b0717b52018-09-05 17:03:25 +0100254 std::pair<armnn::PermutationVector, armnn::PermutationVector> permutationPair =
255 std::make_pair(IdentityPermutation4D, IdentityPermutation4D);
256
narpra01f176d5a2018-11-18 20:17:48 +0000257 bool needPermute = CreateConcatPermutationParameters(inputShapes[0].GetNumDimensions(), concatDim, permutationPair);
arovir01b0717b52018-09-05 17:03:25 +0100258
narpra01f176d5a2018-11-18 20:17:48 +0000259 if (needPermute)
260 {
261 outputShape = armnnUtils::Permuted(outputShape, permutationPair.first);
262 }
263
arovir01b0717b52018-09-05 17:03:25 +0100264 outputInfo.SetShape(outputShape);
265
266 // this is no-op for identity swizzles, otherwise it replaces both
267 // the handles and shapes with the swizzled layer output handles and shapes
268 SwizzleInputs(*data.m_Network, inputHandles, inputShapes, permutationPair.first);
269
Jim Flynn7b1e41f2019-05-22 18:00:04 +0100270 // Create an armnn concat layer descriptor - this will also perform validation on the input shapes
271 armnn::OriginsDescriptor concatDescriptor;
narpra01f176d5a2018-11-18 20:17:48 +0000272
arovir01b0717b52018-09-05 17:03:25 +0100273 try
274 {
Jim Flynn7b1e41f2019-05-22 18:00:04 +0100275 // The concat descriptor is always created across the only supported concat dimension
narpra01f176d5a2018-11-18 20:17:48 +0000276 // which is 0, 1 or 3 for a 4-D tensor, or 0 or 2 for a 3-D tensor.
Jim Flynn7b1e41f2019-05-22 18:00:04 +0100277 concatDescriptor =
Jim Flynn52aa9352019-05-20 12:52:30 +0100278 armnn::CreateDescriptorForConcatenation(inputShapes.begin(), inputShapes.end(), concatDim);
arovir01b0717b52018-09-05 17:03:25 +0100279 }
280 catch (const armnn::Exception& error)
281 {
Jim Flynn7b1e41f2019-05-22 18:00:04 +0100282 return Fail("%s: Error preparing concat descriptor. %s", __func__, error.what());
arovir01b0717b52018-09-05 17:03:25 +0100283 }
284
285 // Validate the output shape is correct given the input shapes based on the
narpra01f176d5a2018-11-18 20:17:48 +0000286 // only valid concat dimension which is 0, 1 or 3 for a 4-D tensor, or 0 or 2 for a 3-D tensor.
arovir01b0717b52018-09-05 17:03:25 +0100287 if (!ValidateConcatOutputShape(inputShapes, outputShape, concatDim))
288 {
289 return Fail("%s: Error validating the output shape for concat", __func__);
290 }
291
292 std::vector<const armnn::TensorInfo*> inputTensorInfos;
293 std::transform(inputHandles.begin(), inputHandles.end(), std::back_inserter(inputTensorInfos),
294 [](const LayerInputHandle& h) -> const armnn::TensorInfo*{ return &h.GetTensorInfo(); });
Nattapat Chaimanowongd5fd9762019-04-04 13:33:10 +0100295 if (!IsLayerSupportedForAnyBackend(__func__,
Jim Flynn073d7a32019-05-13 13:52:56 +0100296 armnn::IsConcatSupported,
Nattapat Chaimanowongd5fd9762019-04-04 13:33:10 +0100297 data.m_Backends,
298 inputTensorInfos,
299 outputInfo,
Jim Flynn7b1e41f2019-05-22 18:00:04 +0100300 concatDescriptor))
arovir01b0717b52018-09-05 17:03:25 +0100301 {
302 return false;
303 }
304
Jim Flynn7b1e41f2019-05-22 18:00:04 +0100305 armnn::IConnectableLayer* layer = data.m_Network->AddConcatLayer(concatDescriptor);
arovir01b0717b52018-09-05 17:03:25 +0100306 assert(layer != nullptr);
307 layer->GetOutputSlot(0).SetTensorInfo(outputInfo);
308
309 // Connect inputs to the layer
310 const int numInputSlots = layer->GetNumInputSlots();
311 assert(static_cast<std::size_t>(numInputSlots) == inputHandles.size());
312 for (int i = 0; i < numInputSlots; ++i)
313 {
314 // connect the input directly to the merge (concat) layer
315 inputHandles[static_cast<unsigned int>(i)].Connect(layer->GetInputSlot(i));
316 }
317
narpra01f176d5a2018-11-18 20:17:48 +0000318 if (needPermute)
319 {
320 // Add permutation layer and connect the output to it, the permutation becomes the output layer
321 armnn::IConnectableLayer& deswizzleLayer = AddPermuteLayer(*data.m_Network,
322 layer->GetOutputSlot(0),
323 permutationPair.second);
324 layer = &deswizzleLayer;
325 }
arovir01b0717b52018-09-05 17:03:25 +0100326
327 if (inputsHaveBeenReshaped)
328 {
329 armnn::TensorInfo afterConcatInfo = layer->GetOutputSlot(0).GetTensorInfo();
330
331 // Undo the reshape knowing the amount of dimensions added
332 if (tensorDimensionsAdded == 1)
333 {
334 afterConcatInfo.SetShape(armnn::TensorShape({ afterConcatInfo.GetShape()[1],
335 afterConcatInfo.GetShape()[2] }));
336 }
337 else if (tensorDimensionsAdded == 2)
338 {
narpra01f176d5a2018-11-18 20:17:48 +0000339 afterConcatInfo.SetShape(armnn::TensorShape({ afterConcatInfo.GetShape()[2] }));
arovir01b0717b52018-09-05 17:03:25 +0100340 }
341
342 layer = &AddReshapeLayer(
343 *data.m_Network,
344 layer->GetOutputSlot(0),
345 afterConcatInfo
346 );
347 }
348
349 return SetupAndTrackLayerOutputSlot(operation, 0, *layer, model, data);
350}
351
352bool HalPolicy::ConvertConv2d(const Operation& operation, const Model& model, ConversionData& data)
353{
354 LayerInputHandle input = ConvertToLayerInputHandle(operation, 0, model, data);
355 if (!input.IsValid())
356 {
357 return Fail("%s: Operation has invalid inputs", __func__);
358 }
359
360 const Operand* output = GetOutputOperand(operation, 0, model);
361 if (!output)
362 {
363 return Fail("%s: Could not read output 0", __func__);
364 }
365
366 const armnn::TensorInfo& inputInfo = input.GetTensorInfo();
367 const armnn::TensorInfo& outputInfo = GetTensorInfoForOperand(*output);
368
arovir01b0717b52018-09-05 17:03:25 +0100369 // ArmNN does not currently support non-fixed weights or bias
narpra01fb60a562018-10-30 15:46:01 +0000370 const ConstTensorPin weightsPin = ConvertOperationInputToConstTensorPin(operation, 1, model, data);
arovir01b0717b52018-09-05 17:03:25 +0100371 const ConstTensorPin biasPin = ConvertOperationInputToConstTensorPin(operation, 2, model, data);
372
373 if (!weightsPin.IsValid() || !biasPin.IsValid())
374 {
375 return Fail("%s: Operation has invalid inputs", __func__);
376 }
377
378 armnn::ConstTensor weights = weightsPin.GetConstTensor();
379 armnn::ConstTensor bias = biasPin.GetConstTensor();
narpra01fb60a562018-10-30 15:46:01 +0000380 SanitizeBiasQuantizationScale(bias.GetInfo(), weights.GetInfo(), inputInfo);
arovir01b0717b52018-09-05 17:03:25 +0100381
382 armnn::Convolution2dDescriptor desc;
narpra01fb60a562018-10-30 15:46:01 +0000383 desc.m_DataLayout = armnn::DataLayout::NHWC;
arovir01b0717b52018-09-05 17:03:25 +0100384 ActivationFn activation;
385
386 if (operation.inputs.size() == 10)
387 {
388 if (!GetInputScalar(operation, 3, OperandType::INT32, desc.m_PadLeft, model, data) ||
389 !GetInputScalar(operation, 4, OperandType::INT32, desc.m_PadRight, model, data) ||
390 !GetInputScalar(operation, 5, OperandType::INT32, desc.m_PadTop, model, data) ||
391 !GetInputScalar(operation, 6, OperandType::INT32, desc.m_PadBottom, model, data) ||
392 !GetInputScalar(operation, 7, OperandType::INT32, desc.m_StrideX, model, data) ||
393 !GetInputScalar(operation, 8, OperandType::INT32, desc.m_StrideY, model, data) ||
394 !GetInputActivationFunction(operation, 9, activation, model, data))
395 {
396 return Fail("%s: Operation has invalid inputs", __func__);
397 }
398 }
399 else if (operation.inputs.size() == 7)
400 {
401 android::nn::PaddingScheme paddingScheme;
402 if (!GetInputPaddingScheme(operation, 3, paddingScheme, model, data) ||
403 !GetInputScalar(operation, 4, OperandType::INT32, desc.m_StrideX, model, data) ||
404 !GetInputScalar(operation, 5, OperandType::INT32, desc.m_StrideY, model, data) ||
405 !GetInputActivationFunction(operation, 6, activation, model, data))
406 {
407 return Fail("%s: Operation has invalid inputs", __func__);
408 }
409
narpra01fb60a562018-10-30 15:46:01 +0000410 const uint32_t kernelX = weights.GetShape()[2];
411 const uint32_t kernelY = weights.GetShape()[1];
412 const uint32_t inputX = inputInfo.GetShape()[2];
413 const uint32_t inputY = inputInfo.GetShape()[1];
arovir01b0717b52018-09-05 17:03:25 +0100414
415 CalcPadding(inputX, kernelX, desc.m_StrideX, desc.m_PadLeft, desc.m_PadRight, paddingScheme);
416 CalcPadding(inputY, kernelY, desc.m_StrideY, desc.m_PadTop, desc.m_PadBottom, paddingScheme);
417 }
418 else
419 {
420 return Fail("%s: Unsupported number of operation inputs", __func__);
421 }
422
423 desc.m_BiasEnabled = true;
arovir015602b192018-10-04 16:15:02 +0100424 armnn::Optional<armnn::TensorInfo> biases(bias.GetInfo());
arovir01b0717b52018-09-05 17:03:25 +0100425
Nattapat Chaimanowongd5fd9762019-04-04 13:33:10 +0100426 if (!IsLayerSupportedForAnyBackend(__func__,
427 armnn::IsConvolution2dSupported,
428 data.m_Backends,
429 inputInfo,
430 outputInfo,
431 desc,
432 weights.GetInfo(),
433 biases))
arovir01b0717b52018-09-05 17:03:25 +0100434 {
435 return false;
436 }
437
Matteo Martincighba01f372019-05-14 13:28:21 +0100438 armnn::IConnectableLayer* startLayer =
439 data.m_Network->AddConvolution2dLayer(desc, weights, armnn::Optional<armnn::ConstTensor>(bias));
arovir01b0717b52018-09-05 17:03:25 +0100440
narpra01fb60a562018-10-30 15:46:01 +0000441 if (!startLayer)
arovir01b0717b52018-09-05 17:03:25 +0100442 {
narpra01fb60a562018-10-30 15:46:01 +0000443 return Fail("%s: AddConvolution2dLayer failed", __func__);
arovir01b0717b52018-09-05 17:03:25 +0100444 }
narpra01fb60a562018-10-30 15:46:01 +0000445
446 armnn::IConnectableLayer* endLayer = ProcessActivation(outputInfo, activation, startLayer, data);
447
448 if (!endLayer)
arovir01b0717b52018-09-05 17:03:25 +0100449 {
450 return Fail("%s: ProcessActivation failed", __func__);
451 }
narpra01fb60a562018-10-30 15:46:01 +0000452
453 input.Connect(startLayer->GetInputSlot(0));
454
455 return SetupAndTrackLayerOutputSlot(operation, 0, *endLayer, model, data);
arovir01b0717b52018-09-05 17:03:25 +0100456}
457
458bool HalPolicy::ConvertDepthwiseConv2d(const Operation& operation, const Model& model, ConversionData& data)
459{
460 LayerInputHandle input = ConvertToLayerInputHandle(operation, 0, model, data);
461 if (!input.IsValid())
462 {
463 return Fail("%s: Operation has invalid inputs", __func__);
464 }
465
466 const Operand* output = GetOutputOperand(operation, 0, model);
467 if (!output)
468 {
469 return Fail("%s: Could not read output 0", __func__);
470 }
471
472 const armnn::TensorInfo& inputInfo = input.GetTensorInfo();
473 const armnn::TensorInfo& outputInfo = GetTensorInfoForOperand(*output);
474
arovir01b0717b52018-09-05 17:03:25 +0100475 // ArmNN does not currently support non-fixed weights or bias
476
477 // Find the shape of the weights tensor. In AndroidNN this will be [ 1, H, W, I * M ]
arovir01b0717b52018-09-05 17:03:25 +0100478 const Operand* weightsOperand = GetInputOperand(operation, 1, model);
479
480 if (weightsOperand == nullptr)
481 {
482 return Fail("%s: Operand is invalid", __func__);
483 }
484
485 // Reinterpret weight data as [ H, W, I, M ]
Matteo Martincigh361ccc82018-12-18 09:32:02 +0000486 armnn::TensorShape weightsShape({ weightsOperand->dimensions[1],
487 weightsOperand->dimensions[2],
arovir01b0717b52018-09-05 17:03:25 +0100488 inputInfo.GetShape()[3],
489 weightsOperand->dimensions[3] / inputInfo.GetShape()[3] });
490
Matteo Martincigh361ccc82018-12-18 09:32:02 +0000491 // Swizzle weight data [ H, W, I, M ] -> [ M, I, H, W ]
492 const armnn::PermutationVector HWIMToMIHW = { 2U, 3U, 1U, 0U };
James Conroy6bf1cf02018-10-12 14:13:18 +0100493
Matteo Martincigh361ccc82018-12-18 09:32:02 +0000494 const ConstTensorPin weightsPin = ConvertOperationInputToConstTensorPin(operation, 1, model, data,
495 HWIMToMIHW, &weightsShape);
arovir01b0717b52018-09-05 17:03:25 +0100496
497 // Bias is a 1D tensor
Matteo Martincigh361ccc82018-12-18 09:32:02 +0000498 const ConstTensorPin biasPin = ConvertOperationInputToConstTensorPin(operation, 2, model, data);
arovir01b0717b52018-09-05 17:03:25 +0100499
500 if (!weightsPin.IsValid() || !biasPin.IsValid())
501 {
502 return Fail("%s: Operation has invalid inputs", __func__);
503 }
504
505 armnn::ConstTensor weights = weightsPin.GetConstTensor();
506 armnn::ConstTensor bias = biasPin.GetConstTensor();
James Conroy6bf1cf02018-10-12 14:13:18 +0100507 SanitizeBiasQuantizationScale(bias.GetInfo(), weights.GetInfo(), inputInfo);
arovir01b0717b52018-09-05 17:03:25 +0100508
509 armnn::DepthwiseConvolution2dDescriptor desc;
James Conroy6bf1cf02018-10-12 14:13:18 +0100510 desc.m_DataLayout = armnn::DataLayout::NHWC;
arovir01b0717b52018-09-05 17:03:25 +0100511 ActivationFn activation;
512
513 if (operation.inputs.size() == 11)
514 {
James Conroy6bf1cf02018-10-12 14:13:18 +0100515 if (!GetInputScalar(operation, 3, OperandType::INT32, desc.m_PadLeft, model, data) ||
516 !GetInputScalar(operation, 4, OperandType::INT32, desc.m_PadRight, model, data) ||
517 !GetInputScalar(operation, 5, OperandType::INT32, desc.m_PadTop, model, data) ||
518 !GetInputScalar(operation, 6, OperandType::INT32, desc.m_PadBottom, model, data) ||
519 !GetInputScalar(operation, 7, OperandType::INT32, desc.m_StrideX, model, data) ||
520 !GetInputScalar(operation, 8, OperandType::INT32, desc.m_StrideY, model, data) ||
arovir01b0717b52018-09-05 17:03:25 +0100521 !GetInputActivationFunction(operation, 10, activation, model, data))
522 {
523 return Fail("%s: Operation has invalid inputs", __func__);
524 }
525 }
526 else if (operation.inputs.size() == 8)
527 {
528 android::nn::PaddingScheme paddingScheme;
James Conroy6bf1cf02018-10-12 14:13:18 +0100529 if (!GetInputPaddingScheme(operation, 3, paddingScheme, model, data) ||
530 !GetInputScalar(operation, 4, OperandType::INT32, desc.m_StrideX, model, data) ||
531 !GetInputScalar(operation, 5, OperandType::INT32, desc.m_StrideY, model, data) ||
arovir01b0717b52018-09-05 17:03:25 +0100532 !GetInputActivationFunction(operation, 7, activation, model, data))
533 {
534 return Fail("%s: Operation has invalid inputs", __func__);
535 }
536
Matteo Martincigh361ccc82018-12-18 09:32:02 +0000537 const uint32_t kernelX = weights.GetShape()[3];
538 const uint32_t kernelY = weights.GetShape()[2];
James Conroy6bf1cf02018-10-12 14:13:18 +0100539 const uint32_t inputX = inputInfo.GetShape()[2];
540 const uint32_t inputY = inputInfo.GetShape()[1];
arovir01b0717b52018-09-05 17:03:25 +0100541
542 CalcPadding(inputX, kernelX, desc.m_StrideX, desc.m_PadLeft, desc.m_PadRight, paddingScheme);
543 CalcPadding(inputY, kernelY, desc.m_StrideY, desc.m_PadTop, desc.m_PadBottom, paddingScheme);
544 }
545 else
546 {
547 return Fail("%s: Unsupported number of operation inputs", __func__);
548 }
549
550 desc.m_BiasEnabled = true;
arovir015602b192018-10-04 16:15:02 +0100551 armnn::Optional<armnn::TensorInfo> biases(bias.GetInfo());
arovir01b0717b52018-09-05 17:03:25 +0100552
Nattapat Chaimanowongd5fd9762019-04-04 13:33:10 +0100553 if (!IsLayerSupportedForAnyBackend(__func__,
554 armnn::IsDepthwiseConvolutionSupported,
555 data.m_Backends,
556 inputInfo,
557 outputInfo,
558 desc,
559 weights.GetInfo(),
560 biases))
arovir01b0717b52018-09-05 17:03:25 +0100561 {
562 return false;
563 }
564
Matteo Martincighba01f372019-05-14 13:28:21 +0100565 armnn::IConnectableLayer* startLayer =
566 data.m_Network->AddDepthwiseConvolution2dLayer(desc, weights, armnn::Optional<armnn::ConstTensor>(bias));
James Conroy6bf1cf02018-10-12 14:13:18 +0100567 if (!startLayer)
arovir01b0717b52018-09-05 17:03:25 +0100568 {
James Conroy6bf1cf02018-10-12 14:13:18 +0100569 return Fail("%s: AddDepthwiseConvolution2dLayer failed", __func__);
arovir01b0717b52018-09-05 17:03:25 +0100570 }
James Conroy6bf1cf02018-10-12 14:13:18 +0100571
572 armnn::IConnectableLayer* endLayer = ProcessActivation(outputInfo, activation, startLayer, data);
573 if (!endLayer)
arovir01b0717b52018-09-05 17:03:25 +0100574 {
575 return Fail("%s: ProcessActivation failed", __func__);
576 }
James Conroy6bf1cf02018-10-12 14:13:18 +0100577
578 input.Connect(startLayer->GetInputSlot(0));
579
580 return SetupAndTrackLayerOutputSlot(operation, 0, *endLayer, model, data);
arovir01b0717b52018-09-05 17:03:25 +0100581}
582
David Monahanacf479a2019-05-29 14:27:04 +0100583bool HalPolicy::ConvertDequantize(const Operation& operation, const Model& model, ConversionData& data)
584{
585 LayerInputHandle input = ConvertToLayerInputHandle(operation, 0, model, data);
586
587 if (!input.IsValid())
588 {
589 return Fail("%s: Operation has invalid input", __func__);
590 }
591
592 const Operand* const outputOperand = GetOutputOperand(operation, 0, model);
593 if (!outputOperand)
594 {
595 return Fail("%s: Operation has invalid outputs", __func__);
596 }
597
598 if (!IsLayerSupportedForAnyBackend(__func__,
599 armnn::IsDequantizeSupported,
600 data.m_Backends,
601 input.GetTensorInfo(),
602 GetTensorInfoForOperand(*outputOperand)))
603 {
604 return false;
605 }
606
607 armnn::IConnectableLayer* const layer = data.m_Network->AddDequantizeLayer();
608 assert(layer != nullptr);
609 input.Connect(layer->GetInputSlot(0));
610
611 return SetupAndTrackLayerOutputSlot(operation, 0, *layer, model, data);
612}
613
arovir01b0717b52018-09-05 17:03:25 +0100614bool HalPolicy::ConvertFloor(const Operation& operation, const Model& model, ConversionData& data)
615{
616 LayerInputHandle input = ConvertToLayerInputHandle(operation, 0, model, data);
617 if (!input.IsValid())
618 {
619 return Fail("%s: Operation has invalid inputs", __func__);
620 }
621
622 const Operand* const outputOperand = GetOutputOperand(operation, 0, model);
623 if (!outputOperand)
624 {
625 return Fail("%s: Operation has invalid outputs", __func__);
626 }
627
Nattapat Chaimanowongd5fd9762019-04-04 13:33:10 +0100628 if (!IsLayerSupportedForAnyBackend(__func__,
629 armnn::IsFloorSupported,
630 data.m_Backends,
631 input.GetTensorInfo(),
632 GetTensorInfoForOperand(*outputOperand)))
arovir01b0717b52018-09-05 17:03:25 +0100633 {
634 return false;
635 }
636
637 armnn::IConnectableLayer* layer = data.m_Network->AddFloorLayer();
638 assert(layer != nullptr);
639 input.Connect(layer->GetInputSlot(0));
640
641 return SetupAndTrackLayerOutputSlot(operation, 0, *layer, model, data);
642}
643
644bool HalPolicy::ConvertFullyConnected(const Operation& operation, const Model& model, ConversionData& data)
645{
646 LayerInputHandle input = ConvertToLayerInputHandle(operation, 0, model, data);
647 if (!input.IsValid())
648 {
649 return Fail("%s: Operation has invalid inputs", __func__);
650 }
651
652 const Operand* output = GetOutputOperand(operation, 0, model);
653 if (!output)
654 {
655 return Fail("%s: Could not read output 0", __func__);
656 }
657
658 const armnn::TensorInfo& inputInfo = input.GetTensorInfo();
659 const armnn::TensorInfo& outputInfo = GetTensorInfoForOperand(*output);
660
661 // ArmNN does not currently support non-fixed weights or bias
662 ConstTensorPin weightsPin = ConvertOperationInputToConstTensorPin(operation, 1, model, data); // 2D
663 ConstTensorPin biasPin = ConvertOperationInputToConstTensorPin(operation, 2, model, data); // 1D
664
665 if (!weightsPin.IsValid() || !biasPin.IsValid())
666 {
667 return Fail("%s: Operation has invalid inputs", __func__);
668 }
669
670 armnn::ConstTensor weights = weightsPin.GetConstTensor();
671 armnn::ConstTensor bias = biasPin.GetConstTensor();
arovir01b0717b52018-09-05 17:03:25 +0100672 armnn::TensorInfo reshapedInfo = inputInfo;
Matthew Benthamf61c2702019-04-23 16:43:27 +0100673
674 try
arovir01b0717b52018-09-05 17:03:25 +0100675 {
Matthew Benthamf61c2702019-04-23 16:43:27 +0100676 reshapedInfo.SetShape(FlattenFullyConnectedInput(inputInfo.GetShape(), weights.GetInfo().GetShape()));
677 } catch (const std::exception &e) {
678 return Fail("%s: %s", __func__, e.what());
arovir01b0717b52018-09-05 17:03:25 +0100679 }
680
681 // ensuring that the bias value is within 1% of the weights input (small float differences can exist)
682 SanitizeBiasQuantizationScale(bias.GetInfo(), weights.GetInfo(), reshapedInfo);
683
684 ActivationFn activationFunction;
685 if (!GetInputActivationFunction(operation, 3, activationFunction, model, data))
686 {
687 return Fail("%s: Operation has invalid inputs", __func__);
688 }
689
690 armnn::FullyConnectedDescriptor desc;
691 desc.m_TransposeWeightMatrix = true;
692 desc.m_BiasEnabled = true;
693
Nattapat Chaimanowongd5fd9762019-04-04 13:33:10 +0100694 if (!IsLayerSupportedForAnyBackend(__func__,
695 armnn::IsFullyConnectedSupported,
696 data.m_Backends,
697 reshapedInfo,
698 outputInfo,
699 weights.GetInfo(),
700 bias.GetInfo(),
701 desc))
arovir01b0717b52018-09-05 17:03:25 +0100702 {
703 return false;
704 }
705
Matteo Martincighba01f372019-05-14 13:28:21 +0100706 armnn::IConnectableLayer* startLayer =
707 data.m_Network->AddFullyConnectedLayer(desc, weights, armnn::Optional<armnn::ConstTensor>(bias));
arovir01b0717b52018-09-05 17:03:25 +0100708 armnn::IConnectableLayer* endLayer = ProcessActivation(outputInfo, activationFunction, startLayer, data);
709
710 if (endLayer != nullptr)
711 {
712 if (inputInfo.GetNumDimensions() > 2U)
713 {
714 armnn::ReshapeDescriptor reshapeDescriptor;
715 reshapeDescriptor.m_TargetShape = reshapedInfo.GetShape();
716
717 armnn::IConnectableLayer* reshapeLayer = data.m_Network->AddReshapeLayer(reshapeDescriptor);
718 assert(reshapeLayer != nullptr);
719 input.Connect(reshapeLayer->GetInputSlot(0));
720 reshapeLayer->GetOutputSlot(0).SetTensorInfo(reshapedInfo);
721 reshapeLayer->GetOutputSlot(0).Connect(startLayer->GetInputSlot(0));
722 }
723 else
724 {
725 input.Connect(startLayer->GetInputSlot(0));
726 }
727
728 return SetupAndTrackLayerOutputSlot(operation, 0, *endLayer, model, data);
729 }
730 else
731 {
732 return Fail("%s: ProcessActivation failed", __func__);
733 }
734}
735
736bool HalPolicy::ConvertLocalResponseNormalization(const Operation& operation,
737 const Model& model,
738 ConversionData& data)
739{
740 LayerInputHandle input = ConvertToLayerInputHandle(operation, 0, model, data);
741 if (!input.IsValid())
742 {
743 return Fail("%s: Operation has invalid inputs", __func__);
744 }
745
746 const Operand* output = GetOutputOperand(operation, 0, model);
747 if (!output)
748 {
749 return Fail("%s: Could not read output 0", __func__);
750 }
751
narpra012fb804a2018-10-22 14:52:32 +0100752 const armnn::TensorInfo& inputInfo = input.GetTensorInfo();
arovir01b0717b52018-09-05 17:03:25 +0100753 const armnn::TensorInfo& outputInfo = GetTensorInfoForOperand(*output);
754
arovir01b0717b52018-09-05 17:03:25 +0100755 armnn::NormalizationDescriptor descriptor;
756
narpra012fb804a2018-10-22 14:52:32 +0100757 descriptor.m_DataLayout = armnn::DataLayout::NHWC;
arovir01b0717b52018-09-05 17:03:25 +0100758 descriptor.m_NormChannelType = armnn::NormalizationAlgorithmChannel::Across;
narpra012fb804a2018-10-22 14:52:32 +0100759 descriptor.m_NormMethodType = armnn::NormalizationAlgorithmMethod::LocalBrightness;
arovir01b0717b52018-09-05 17:03:25 +0100760
761 if (!input.IsValid() ||
762 !GetInputScalar(operation, 1, OperandType::INT32, descriptor.m_NormSize, model, data) ||
763 !GetInputFloat32(operation, 2, descriptor.m_K, model, data) ||
764 !GetInputFloat32(operation, 3, descriptor.m_Alpha, model, data) ||
765 !GetInputFloat32(operation, 4, descriptor.m_Beta, model, data))
766 {
767 return Fail("%s: Operation has invalid inputs", __func__);
768 }
769
770 // ArmNN expects normSize to be the full size of the normalization
771 // window rather than the radius as in AndroidNN.
772 descriptor.m_NormSize = 1 + (2 * descriptor.m_NormSize);
773
Nattapat Chaimanowongd5fd9762019-04-04 13:33:10 +0100774 if (!IsLayerSupportedForAnyBackend(__func__,
775 armnn::IsNormalizationSupported,
776 data.m_Backends,
777 inputInfo,
778 outputInfo,
779 descriptor))
arovir01b0717b52018-09-05 17:03:25 +0100780 {
781 return false;
782 }
783
784
785 armnn::IConnectableLayer* layer = data.m_Network->AddNormalizationLayer(descriptor);
786 assert(layer != nullptr);
narpra012fb804a2018-10-22 14:52:32 +0100787 input.Connect(layer->GetInputSlot(0));
arovir01b0717b52018-09-05 17:03:25 +0100788
narpra012fb804a2018-10-22 14:52:32 +0100789 return SetupAndTrackLayerOutputSlot(operation, 0, *layer, model, data);
arovir01b0717b52018-09-05 17:03:25 +0100790}
791
792bool HalPolicy::ConvertLogistic(const Operation& operation, const Model& model, ConversionData& data)
793{
794 armnn::ActivationDescriptor desc;
795 desc.m_Function = armnn::ActivationFunction::Sigmoid;
796
797 return ConvertToActivation(operation, __func__, desc, model, data);
798}
799
800bool HalPolicy::ConvertLstm(const Operation& operation, const Model& model, ConversionData& data)
801{
802 // Inputs:
803 // 00: The input: A 2-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape [batch_size, input_size], where
804 // “batch_size” corresponds to the batching dimension, and “input_size” is the size of the input.
805 LayerInputHandle input = ConvertToLayerInputHandle(operation, 0, model, data);
806 if (!input.IsValid())
807 {
808 return Fail("%s: Could not read input 0: input", __func__);
809 }
810 // 18: The output state: A 2-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape [batch_size, output_size].
811 LayerInputHandle outputStateIn = ConvertToLayerInputHandle(operation, 18, model, data);
812 if (!outputStateIn.IsValid())
813 {
814 return Fail("%s: Could not read input 18: outputStateIn", __func__);
815 }
816 // 19: The cell state: A 2-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape [batch_size, num_units].
817 LayerInputHandle cellStateIn = ConvertToLayerInputHandle(operation, 19, model, data);
818 if (!cellStateIn.IsValid())
819 {
820 return Fail("%s: Could not read input 19: cellStateIn", __func__);
821 }
822
823 // Get the mandatory input tensors:
824 // 02: The input-to-forget weights: A 2-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape
825 // [num_units, input_size].
826 const ConstTensorPin inputToForgetWeightsPin = ConvertOperationInputToConstTensorPin(operation, 2, model, data);
827 // 03: The input-to-cell weights: A 2-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape [num_units, input_size].
828 const ConstTensorPin inputToCellWeightsPin = ConvertOperationInputToConstTensorPin(operation, 3, model, data);
829 // 04: The input-to-output weights: A 2-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape
830 // [num_units, input_size].
831 const ConstTensorPin inputToOutputWeightsPin = ConvertOperationInputToConstTensorPin(operation, 4, model, data);
832 // 06: The recurrent-to-forget weights: A 2-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape
833 // [num_units, output_size].
834 const ConstTensorPin recurrentToForgetWeightsPin =
835 ConvertOperationInputToConstTensorPin(operation, 6, model, data);
836 // 07: The recurrent-to-cell weights: A 2-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape
837 // [num_units, output_size].
838 const ConstTensorPin recurrentToCellWeightsPin = ConvertOperationInputToConstTensorPin(operation, 7, model, data);
839 // 08: The recurrent-to-output weights: A 2-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape
840 // [num_units, output_size].
841 const ConstTensorPin recurrentToOutputWeightsPin =
842 ConvertOperationInputToConstTensorPin(operation, 8, model, data);
843 // 13: The forget gate bias: A 1-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape [num_units].
844 const ConstTensorPin forgetGateBiasPin = ConvertOperationInputToConstTensorPin(operation, 13, model, data);
845 // 14: The cell bias: A 1-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape [num_units].
846 const ConstTensorPin cellBiasPin = ConvertOperationInputToConstTensorPin(operation, 14, model, data);
847 // 15: The output gate bias: A 1-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape [num_units].
848 const ConstTensorPin outputGateBiasPin = ConvertOperationInputToConstTensorPin(operation, 15, model, data);
849
850 if (!inputToForgetWeightsPin.IsValid() ||
851 !inputToCellWeightsPin.IsValid() ||
852 !inputToOutputWeightsPin.IsValid() ||
853 !recurrentToForgetWeightsPin.IsValid() ||
854 !recurrentToCellWeightsPin.IsValid() ||
855 !recurrentToOutputWeightsPin.IsValid() ||
856 !forgetGateBiasPin.IsValid() ||
857 !cellBiasPin.IsValid() ||
858 !outputGateBiasPin.IsValid())
859 {
860 return Fail("%s: Operation has invalid tensor inputs", __func__);
861 }
862
863 // Get the optional input tensors:
864 // 01: The input-to-input weights: Optional. A 2-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape
865 // [num_units, input_size], where “num_units” corresponds to the number of cell units.
David Monahanecd7ca62019-02-22 14:29:51 +0000866 const ConstTensorPin inputToInputWeightsPin = ConvertOperationInputToConstTensorPin(operation, 1, model, data,
867 g_DontPermute, nullptr, true);
arovir01b0717b52018-09-05 17:03:25 +0100868 // 05: The recurrent-to-input weights: Optional. A 2-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape
869 // [num_units, output_size], where “output_size” corresponds to either the number of cell units (i.e.,
870 // “num_units”), or the second dimension of the “projection_weights”, if defined.
David Monahanecd7ca62019-02-22 14:29:51 +0000871 const ConstTensorPin recurrentToInputWeightsPin = ConvertOperationInputToConstTensorPin(operation, 5, model, data,
872 g_DontPermute, nullptr, true);
arovir01b0717b52018-09-05 17:03:25 +0100873 // 09: The cell-to-input weights: Optional. A 1-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape [num_units].
David Monahanecd7ca62019-02-22 14:29:51 +0000874 const ConstTensorPin cellToInputWeightsPin = ConvertOperationInputToConstTensorPin(operation, 9, model, data,
875 g_DontPermute, nullptr, true);
arovir01b0717b52018-09-05 17:03:25 +0100876 // 10: The cell-to-forget weights: Optional. A 1-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape [num_units].
David Monahanecd7ca62019-02-22 14:29:51 +0000877 const ConstTensorPin cellToForgetWeightsPin = ConvertOperationInputToConstTensorPin(operation, 10, model, data,
878 g_DontPermute, nullptr, true);
arovir01b0717b52018-09-05 17:03:25 +0100879 // 11: The cell-to-output weights: Optional. A 1-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape [num_units].
David Monahanecd7ca62019-02-22 14:29:51 +0000880 const ConstTensorPin cellToOutputWeightsPin = ConvertOperationInputToConstTensorPin(operation, 11, model, data,
881 g_DontPermute, nullptr, true);
arovir01b0717b52018-09-05 17:03:25 +0100882 // 12: The input gate bias: Optional. A 1-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape [num_units].
David Monahanecd7ca62019-02-22 14:29:51 +0000883 const ConstTensorPin inputGateBiasPin = ConvertOperationInputToConstTensorPin(operation, 12, model, data,
884 g_DontPermute, nullptr, true);
arovir01b0717b52018-09-05 17:03:25 +0100885 // 16: The projection weights: Optional. A 2-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape
886 // [output_size, num_units].
David Monahanecd7ca62019-02-22 14:29:51 +0000887 const ConstTensorPin projectionWeightsPin = ConvertOperationInputToConstTensorPin(operation, 16, model, data,
888 g_DontPermute, nullptr, true);
arovir01b0717b52018-09-05 17:03:25 +0100889 // 17: The projection bias: Optional. A 1-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape [output_size].
David Monahanecd7ca62019-02-22 14:29:51 +0000890 const ConstTensorPin projectionBiasPin = ConvertOperationInputToConstTensorPin(operation, 17, model, data,
891 g_DontPermute, nullptr, true);
arovir01b0717b52018-09-05 17:03:25 +0100892
893 if ((!inputToInputWeightsPin.IsValid() && !inputToInputWeightsPin.IsOptional()) ||
894 (!recurrentToInputWeightsPin.IsValid() && !recurrentToInputWeightsPin.IsOptional()) ||
895 (!cellToInputWeightsPin.IsValid() && !cellToInputWeightsPin.IsOptional()) ||
896 (!cellToForgetWeightsPin.IsValid() && !cellToForgetWeightsPin.IsOptional()) ||
897 (!cellToOutputWeightsPin.IsValid() && !cellToOutputWeightsPin.IsOptional()) ||
898 (!inputGateBiasPin.IsValid() && !inputGateBiasPin.IsOptional()) ||
899 (!projectionWeightsPin.IsValid() && !projectionWeightsPin.IsOptional()) ||
900 (!projectionBiasPin.IsValid() && !projectionBiasPin.IsOptional()))
901 {
902 return Fail("%s: Operation has invalid tensor inputs", __func__);
903 }
904
905 // Get the mandatory input scalars (actually 1-D tensors of size 1):
906 // 20: The activation function: A value indicating the activation function:
907 // 0: None; 1: Relu; 3: Relu6; 4: Tanh; 6: Sigmoid.
908 // 21: The clipping threshold: for the cell state, such that values are bound within [-cell_clip, cell_clip].
909 // If set to 0.0 then clipping is disabled.
910 // 22: The clipping threshold: for the output from the projection layer, such that values are bound within
911 // [-proj_clip, proj_clip]. If set to 0.0 then clipping is disabled.
912 ActivationFn activation;
913 float cellClip;
914 float projClip;
915 if (!GetInputActivationFunctionFromTensor(operation, 20, activation, model, data) ||
916 !GetInputScalar(operation, 21, OperandType::FLOAT32, cellClip, model, data) ||
917 !GetInputScalar(operation, 22, OperandType::FLOAT32, projClip, model, data))
918 {
919 return Fail("%s: Operation has invalid scalar inputs", __func__);
920 }
921
922 // Outputs:
923 // 00: The scratch buffer: A 2-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape [batch_size, num_units * 4] with
924 // CIFG, or [batch_size, num_units * 3] without CIFG.
925 const Operand* scratchBuffer = GetOutputOperand(operation, 0, model);
926 if (!scratchBuffer)
927 {
928 return Fail("%s: Could not read output 0: scratchBuffer", __func__);
929 }
930 // 01: The output state (out): A 2-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape [batch_size, output_size].
931 const Operand* outputStateOut = GetOutputOperand(operation, 1, model);
932 if (!outputStateOut)
933 {
934 return Fail("%s: Could not read output 1: outputStateOut", __func__);
935 }
936 // 02: The cell state (out): A 2-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape [batch_size, num_units].
937 const Operand* cellStateOut = GetOutputOperand(operation, 2, model);
938 if (!cellStateOut)
939 {
940 return Fail("%s: Could not read output 2: cellStateOut", __func__);
941 }
942 // 03: The output: A 2-D tensor of ANEURALNETWORKS_TENSOR_FLOAT32, of shape [batch_size, output_size]. This is
943 // effectively the same as the current “output state (out)” value.
944 const Operand* output = GetOutputOperand(operation, 3, model);
945 if (!output)
946 {
947 return Fail("%s: Could not read output 3: output", __func__);
948 }
949
950 // set the params structure for the AddLstmLayer call
951 armnn::LstmInputParams params;
952 params.m_InputToInputWeights = inputToInputWeightsPin.GetConstTensorPtr();
953 params.m_InputToForgetWeights = inputToForgetWeightsPin.GetConstTensorPtr();
954 params.m_InputToCellWeights = inputToCellWeightsPin.GetConstTensorPtr();
955 params.m_InputToOutputWeights = inputToOutputWeightsPin.GetConstTensorPtr();
956 params.m_RecurrentToInputWeights = recurrentToInputWeightsPin.GetConstTensorPtr();
957 params.m_RecurrentToForgetWeights = recurrentToForgetWeightsPin.GetConstTensorPtr();
958 params.m_RecurrentToCellWeights = recurrentToCellWeightsPin.GetConstTensorPtr();
959 params.m_RecurrentToOutputWeights = recurrentToOutputWeightsPin.GetConstTensorPtr();
960 params.m_CellToInputWeights = cellToInputWeightsPin.GetConstTensorPtr();
961 params.m_CellToForgetWeights = cellToForgetWeightsPin.GetConstTensorPtr();
962 params.m_CellToOutputWeights = cellToOutputWeightsPin.GetConstTensorPtr();
963 params.m_InputGateBias = inputGateBiasPin.GetConstTensorPtr();
964 params.m_ForgetGateBias = forgetGateBiasPin.GetConstTensorPtr();
965 params.m_CellBias = cellBiasPin.GetConstTensorPtr();
966 params.m_OutputGateBias = outputGateBiasPin.GetConstTensorPtr();
967 params.m_ProjectionWeights = projectionWeightsPin.GetConstTensorPtr();
968 params.m_ProjectionBias = projectionBiasPin.GetConstTensorPtr();
969
970 // set the layer descriptor
971 armnn::LstmDescriptor desc;
972 desc.m_ActivationFunc = activation;
973 desc.m_ClippingThresCell = cellClip;
974 desc.m_ClippingThresProj = projClip;
975 desc.m_CifgEnabled = (params.m_InputToInputWeights == nullptr ||
976 params.m_RecurrentToInputWeights == nullptr ||
977 params.m_InputGateBias == nullptr);
978 desc.m_PeepholeEnabled = (params.m_CellToForgetWeights != nullptr ||
979 params.m_CellToOutputWeights != nullptr);
980 desc.m_ProjectionEnabled = (params.m_ProjectionWeights != nullptr);
981
982 // validate the optional input groups
983 if (desc.m_CifgEnabled &&
984 (params.m_InputToInputWeights != nullptr ||
985 params.m_RecurrentToInputWeights != nullptr ||
986 params.m_InputGateBias != nullptr))
987 {
988 return Fail("%s: All, or none, of input-to-input weights, recurrent-to-input weights,"
989 " and input gate bias must be provided", __func__);
990 }
991
992 if (!desc.m_ProjectionEnabled && params.m_ProjectionBias != nullptr)
993 {
994 return Fail("%s: projection bias should not be provided without projection weights", __func__);
995 }
996
997 if (desc.m_PeepholeEnabled &&
998 (params.m_CellToForgetWeights == nullptr ||
999 params.m_CellToOutputWeights == nullptr ||
1000 (!desc.m_CifgEnabled && params.m_CellToInputWeights == nullptr)))
1001 {
1002 return Fail("%s: All, or none, of cell-to-forget weights and cell-to-output weights must be provided"
1003 " and, if CIFG is not enabled, cell-to-input weights must also be provided", __func__);
1004 }
1005
1006 // Check if the layer is supported
1007 // Inputs
1008 const armnn::TensorInfo& inputInfo = input.GetTensorInfo();
1009 const armnn::TensorInfo& outputStateInInfo = outputStateIn.GetTensorInfo();
1010 const armnn::TensorInfo& cellStateInInfo = cellStateIn.GetTensorInfo();
1011
1012 // Outputs
1013 const armnn::TensorInfo& scratchBufferInfo = GetTensorInfoForOperand(*scratchBuffer);
1014 const armnn::TensorInfo& outputStateOutInfo = GetTensorInfoForOperand(*outputStateOut);
1015 const armnn::TensorInfo& cellStateOutInfo = GetTensorInfoForOperand(*cellStateOut);
1016 const armnn::TensorInfo& outputInfo = GetTensorInfoForOperand(*output);
1017
1018 // Basic parameters
1019 const armnn::TensorInfo& inputToForgetWeights = params.m_InputToForgetWeights->GetInfo();
1020 const armnn::TensorInfo& inputToCellWeights = params.m_InputToCellWeights->GetInfo();
1021 const armnn::TensorInfo& inputToOutputWeights = params.m_InputToOutputWeights->GetInfo();
1022 const armnn::TensorInfo& recurrentToForgetWeights = params.m_RecurrentToForgetWeights->GetInfo();
1023 const armnn::TensorInfo& recurrentToCellWeights = params.m_RecurrentToCellWeights->GetInfo();
1024 const armnn::TensorInfo& recurrentToOutputWeights = params.m_RecurrentToOutputWeights->GetInfo();
1025 const armnn::TensorInfo& forgetGateBias = params.m_ForgetGateBias->GetInfo();
1026 const armnn::TensorInfo& cellBias = params.m_CellBias->GetInfo();
1027 const armnn::TensorInfo& outputGateBias = params.m_OutputGateBias->GetInfo();
1028
1029 //Optional parameters
1030 const armnn::TensorInfo* inputToInputWeights = nullptr;
1031 const armnn::TensorInfo* recurrentToInputWeights = nullptr;
1032 const armnn::TensorInfo* cellToInputWeights = nullptr;
1033 const armnn::TensorInfo* inputGateBias = nullptr;
1034 const armnn::TensorInfo* projectionWeights = nullptr;
1035 const armnn::TensorInfo* projectionBias = nullptr;
1036 const armnn::TensorInfo* cellToForgetWeights = nullptr;
1037 const armnn::TensorInfo* cellToOutputWeights = nullptr;
1038
1039 if(!desc.m_CifgEnabled)
1040 {
1041 inputToInputWeights = &(params.m_InputToInputWeights->GetInfo());
1042 recurrentToInputWeights = &(params.m_RecurrentToInputWeights->GetInfo());
1043 if (params.m_CellToInputWeights != nullptr)
1044 {
1045 cellToInputWeights = &(params.m_CellToInputWeights->GetInfo());
1046 }
1047 inputGateBias = &(params.m_InputGateBias->GetInfo());
1048 }
1049
1050 if(desc.m_ProjectionEnabled)
1051 {
1052 projectionWeights = &(params.m_ProjectionWeights->GetInfo());
1053 if (params.m_ProjectionBias != nullptr)
1054 {
1055 projectionBias = &(params.m_ProjectionBias->GetInfo());
1056 }
1057 }
1058
1059 if(desc.m_PeepholeEnabled)
1060 {
1061 cellToForgetWeights = &(params.m_CellToForgetWeights->GetInfo());
1062 cellToOutputWeights = &(params.m_CellToOutputWeights->GetInfo());
1063 }
1064
Nattapat Chaimanowongd5fd9762019-04-04 13:33:10 +01001065 if (!IsLayerSupportedForAnyBackend(__func__,
1066 armnn::IsLstmSupported,
1067 data.m_Backends,
1068 inputInfo,
1069 outputStateInInfo,
1070 cellStateInInfo,
1071 scratchBufferInfo,
1072 outputStateOutInfo,
1073 cellStateOutInfo,
1074 outputInfo,
1075 desc,
1076 inputToForgetWeights,
1077 inputToCellWeights,
1078 inputToOutputWeights,
1079 recurrentToForgetWeights,
1080 recurrentToCellWeights,
1081 recurrentToOutputWeights,
1082 forgetGateBias,
1083 cellBias,
1084 outputGateBias,
1085 inputToInputWeights,
1086 recurrentToInputWeights,
1087 cellToInputWeights,
1088 inputGateBias,
1089 projectionWeights,
1090 projectionBias,
1091 cellToForgetWeights,
1092 cellToOutputWeights))
arovir01b0717b52018-09-05 17:03:25 +01001093 {
1094 return false;
1095 }
1096
1097 // Add the layer
1098 armnn::IConnectableLayer* layer = data.m_Network->AddLstmLayer(desc, params, "Lstm");
1099
1100 input.Connect(layer->GetInputSlot(0));
1101 outputStateIn.Connect(layer->GetInputSlot(1));
1102 cellStateIn.Connect(layer->GetInputSlot(2));
1103
1104 return (SetupAndTrackLayerOutputSlot(operation, 0, *layer, 0, model, data) &&
1105 SetupAndTrackLayerOutputSlot(operation, 1, *layer, 1, model, data) &&
1106 SetupAndTrackLayerOutputSlot(operation, 2, *layer, 2, model, data) &&
1107 SetupAndTrackLayerOutputSlot(operation, 3, *layer, 3, model, data));
1108}
1109
1110bool HalPolicy::ConvertL2Normalization(const Operation& operation, const Model& model, ConversionData& data)
1111{
1112 LayerInputHandle input = ConvertToLayerInputHandle(operation, 0, model, data);
1113 if (!input.IsValid())
1114 {
1115 return Fail("%s: Operation has invalid inputs", __func__);
1116 }
1117
1118 const Operand* output = GetOutputOperand(operation, 0, model);
1119 if (!output)
1120 {
1121 return Fail("%s: Could not read output 0", __func__);
1122 }
1123
1124 const armnn::TensorInfo& inputInfo = input.GetTensorInfo();
1125 const armnn::TensorInfo& outputInfo = GetTensorInfoForOperand(*output);
1126
Matteo Martincigh58f71092018-09-25 15:58:52 +01001127 armnn::L2NormalizationDescriptor desc;
Matteo Martincigh5e0ed9f2018-10-01 09:26:32 +01001128 desc.m_DataLayout = armnn::DataLayout::NHWC;
Matteo Martincigh58f71092018-09-25 15:58:52 +01001129
Nattapat Chaimanowongd5fd9762019-04-04 13:33:10 +01001130 if (!IsLayerSupportedForAnyBackend(__func__,
1131 armnn::IsL2NormalizationSupported,
1132 data.m_Backends,
1133 inputInfo,
1134 outputInfo,
1135 desc))
arovir01b0717b52018-09-05 17:03:25 +01001136 {
1137 return false;
1138 }
1139
Matteo Martincigh58f71092018-09-25 15:58:52 +01001140 armnn::IConnectableLayer* layer = data.m_Network->AddL2NormalizationLayer(desc);
arovir01b0717b52018-09-05 17:03:25 +01001141 assert(layer != nullptr);
Matteo Martincigh5e0ed9f2018-10-01 09:26:32 +01001142 input.Connect(layer->GetInputSlot(0));
arovir01b0717b52018-09-05 17:03:25 +01001143
Matteo Martincigh5e0ed9f2018-10-01 09:26:32 +01001144 return SetupAndTrackLayerOutputSlot(operation, 0, *layer, model, data);
arovir01b0717b52018-09-05 17:03:25 +01001145}
1146
1147bool HalPolicy::ConvertL2Pool2d(const Operation& operation, const Model& model, ConversionData& data)
1148{
1149 return ConvertPooling2d(operation, __func__, armnn::PoolingAlgorithm::L2, model, data);
1150}
1151
1152bool HalPolicy::ConvertMaxPool2d(const Operation& operation, const Model& model, ConversionData& data)
1153{
1154 return ConvertPooling2d(operation, __func__, armnn::PoolingAlgorithm::Max, model, data);
1155}
1156
1157bool HalPolicy::ConvertMul(const Operation& operation, const Model& model, ConversionData& data)
1158{
1159 LayerInputHandle input0 = ConvertToLayerInputHandle(operation, 0, model, data);
1160 LayerInputHandle input1 = ConvertToLayerInputHandle(operation, 1, model, data);
1161
1162 if (!input0.IsValid() || !input1.IsValid())
1163 {
1164 return Fail("%s: Operation has invalid inputs", __func__);
1165 }
1166
1167 // The FuseActivation parameter is always the input index 2
1168 // and it should be optional
1169 ActivationFn activationFunction;
1170 if (!GetOptionalInputActivation(operation, 2, activationFunction, model, data))
1171 {
1172 return Fail("%s: Operation has invalid inputs", __func__);
1173 }
1174
1175 const Operand* outputOperand = GetOutputOperand(operation, 0, model);
1176
1177 if (outputOperand == nullptr)
1178 {
1179 return false;
1180 }
1181
1182 const armnn::TensorInfo& outInfo = GetTensorInfoForOperand(*outputOperand);
1183
Nattapat Chaimanowongd5fd9762019-04-04 13:33:10 +01001184 if (!IsLayerSupportedForAnyBackend(__func__,
1185 armnn::IsMultiplicationSupported,
1186 data.m_Backends,
1187 input0.GetTensorInfo(),
1188 input1.GetTensorInfo(),
1189 outInfo))
arovir01b0717b52018-09-05 17:03:25 +01001190 {
1191 return false;
1192 }
1193
1194 armnn::IConnectableLayer* const startLayer = data.m_Network->AddMultiplicationLayer();
1195 armnn::IConnectableLayer* const endLayer = ProcessActivation(outInfo, activationFunction, startLayer, data);
1196
1197 const armnn::TensorInfo& inputTensorInfo0 = input0.GetTensorInfo();
1198 const armnn::TensorInfo& inputTensorInfo1 = input1.GetTensorInfo();
1199
1200 if (endLayer != nullptr)
1201 {
1202 BroadcastTensor(input0, input1, startLayer, *data.m_Network);
1203 return SetupAndTrackLayerOutputSlot(operation, 0, *endLayer, model, data);
1204 }
1205 else
1206 {
1207 return Fail("%s: ProcessActivation failed", __func__);
1208 }
1209}
1210
1211bool HalPolicy::ConvertReLu(const Operation& operation, const Model& model, ConversionData& data)
1212{
1213 armnn::ActivationDescriptor desc;
1214 desc.m_Function = armnn::ActivationFunction::ReLu;
1215
1216 return ConvertToActivation(operation, __func__, desc, model, data);
1217}
1218
1219bool HalPolicy::ConvertReLu1(const Operation& operation, const Model& model, ConversionData& data)
1220{
1221 armnn::ActivationDescriptor desc;
1222 desc.m_Function = armnn::ActivationFunction::BoundedReLu;
1223 desc.m_A = 1.0f;
1224 desc.m_B = -1.0f;
1225
1226 return ConvertToActivation(operation, __func__, desc, model, data);
1227}
1228
1229bool HalPolicy::ConvertReLu6(const Operation& operation, const Model& model, ConversionData& data)
1230{
1231 armnn::ActivationDescriptor desc;
1232 desc.m_Function = armnn::ActivationFunction::BoundedReLu;
1233 desc.m_A = 6.0f;
1234
1235 return ConvertToActivation(operation, __func__, desc, model, data);
1236}
1237
1238bool HalPolicy::ConvertSoftmax(const Operation& operation, const Model& model, ConversionData& data)
1239{
1240 LayerInputHandle input = ConvertToLayerInputHandle(operation, 0, model, data);
1241 if (!input.IsValid())
1242 {
1243 return Fail("%s: Operation has invalid inputs", __func__);
1244 }
1245
1246 const Operand* outputOperand = GetOutputOperand(operation, 0, model);
1247 if (!outputOperand)
1248 {
1249 return Fail("%s: Operation has no outputs", __func__);
1250 }
1251
1252 const armnn::TensorInfo outInfo = GetTensorInfoForOperand(*outputOperand);
1253
1254 armnn::SoftmaxDescriptor desc;
1255 if (!GetInputFloat32(operation, 1, desc.m_Beta, model, data))
1256 {
1257 return Fail("%s: Operation has invalid inputs", __func__);
1258 }
1259
Nattapat Chaimanowongd5fd9762019-04-04 13:33:10 +01001260 if (!IsLayerSupportedForAnyBackend(__func__,
1261 armnn::IsSoftmaxSupported,
1262 data.m_Backends,
1263 input.GetTensorInfo(),
1264 outInfo,
1265 desc))
arovir01b0717b52018-09-05 17:03:25 +01001266 {
1267 return false;
1268 }
1269
1270 armnn::IConnectableLayer* layer = data.m_Network->AddSoftmaxLayer(desc);
1271 assert(layer != nullptr);
1272 input.Connect(layer->GetInputSlot(0));
1273
1274 return SetupAndTrackLayerOutputSlot(operation, 0, *layer, model, data);
1275}
1276
1277bool HalPolicy::ConvertTanH(const Operation& operation, const Model& model, ConversionData& data)
1278{
1279 armnn::ActivationDescriptor desc;
1280 desc.m_Function = armnn::ActivationFunction::TanH;
1281 desc.m_A = 1.0f; // android nn does not support tanH parameters
1282 desc.m_B = 1.0f; // set to 1.0f for unity scaling
1283
1284 return ConvertToActivation(operation, __func__, desc, model, data);
1285}
1286
1287bool HalPolicy::ConvertReshape(const Operation& operation, const Model& model, ConversionData& data)
1288{
1289 const Operand* inputOperand = GetInputOperand(operation, 0, model);
1290 const Operand* requestedShapeOperand = GetInputOperand(operation, 1, model);
1291 const Operand* outputOperand = GetOutputOperand(operation, 0, model);
1292
1293 if (inputOperand == nullptr
1294 || requestedShapeOperand == nullptr
1295 || outputOperand == nullptr)
1296 {
1297 return Fail("%s: Operation has invalid inputs", __func__);
1298 }
1299
1300
1301 if (requestedShapeOperand->dimensions.size() != 1)
1302 {
1303 return Fail("%s: Input 1 expected to be one-dimensional (found %i dimensions)",
1304 __func__, requestedShapeOperand->dimensions.size());
1305 }
1306
1307 std::vector<int32_t> targetDimensions;
1308 if (!GetTensorInt32Values(*requestedShapeOperand, targetDimensions, model, data))
1309 {
1310 return Fail("%s: Could not read values of input 1", __func__);
1311 }
1312
1313 const Shape inputOperandShape = GetOperandShape(*inputOperand);
1314
1315 Shape requestedShape;
1316 // targetDimensions may contain special values (e.g. -1). reshapePrepare() is an AndroidNN provided utility
1317 // function that resolves these values into a fully specified tensor shape.
1318 if (!reshapePrepare(inputOperandShape, targetDimensions.data(), targetDimensions.size(), &requestedShape))
1319 {
1320 return Fail("%s: Failed to resolve the requested shape", __func__);
1321 }
1322
1323 const Shape outputOperandShape = GetOperandShape(*outputOperand);
1324 if (!SameShape(requestedShape, outputOperandShape))
1325 {
1326 return Fail("%s: Shape of output operand does not match resolved requested shape", __func__);
1327 }
1328
1329 LayerInputHandle input = ConvertToLayerInputHandle(operation, 0, model, data);
1330 if (!input.IsValid())
1331 {
1332 return Fail("%s: Could not read input 0", __func__);
1333 }
1334
arovir01b0717b52018-09-05 17:03:25 +01001335 armnn::ReshapeDescriptor reshapeDescriptor;
1336 reshapeDescriptor.m_TargetShape = armnn::TensorShape(requestedShape.dimensions.size(),
1337 requestedShape.dimensions.data());
1338
Nattapat Chaimanowongd5fd9762019-04-04 13:33:10 +01001339 if (!IsLayerSupportedForAnyBackend(__func__,
1340 armnn::IsReshapeSupported,
1341 data.m_Backends,
1342 input.GetTensorInfo(),
1343 reshapeDescriptor))
Matteo Martincigh265d1ad2019-01-08 18:14:53 +00001344 {
1345 return false;
1346 }
1347
arovir01b0717b52018-09-05 17:03:25 +01001348 armnn::IConnectableLayer* layer = data.m_Network->AddReshapeLayer(reshapeDescriptor);
1349 assert(layer != nullptr);
1350 input.Connect(layer->GetInputSlot(0));
1351
1352 return SetupAndTrackLayerOutputSlot(operation, 0, *layer, model, data);
1353}
1354
1355bool HalPolicy::ConvertResizeBilinear(const Operation& operation, const Model& model, ConversionData& data)
1356{
1357 LayerInputHandle input = ConvertToLayerInputHandle(operation, 0, model, data);
1358 if (!input.IsValid())
1359 {
1360 return Fail("%s: Could not read input 0", __func__);
1361 }
1362
1363 const Operand* output = GetOutputOperand(operation, 0, model);
1364 if (!output)
1365 {
1366 return Fail("%s: Could not read output 0", __func__);
1367 }
1368
1369 const armnn::TensorInfo& inputInfo = input.GetTensorInfo();
1370 const armnn::TensorInfo& outputInfo = GetTensorInfoForOperand(*output);
1371
Mohamed Nour Abouelseoud81afa302018-10-29 14:32:55 +00001372 armnn::ResizeBilinearDescriptor desc;
1373 desc.m_DataLayout = armnn::DataLayout::NHWC;
arovir01b0717b52018-09-05 17:03:25 +01001374
Nattapat Chaimanowongd5fd9762019-04-04 13:33:10 +01001375 if (!IsLayerSupportedForAnyBackend(__func__,
1376 armnn::IsResizeBilinearSupported,
1377 data.m_Backends,
1378 inputInfo,
1379 outputInfo))
arovir01b0717b52018-09-05 17:03:25 +01001380 {
1381 return false;
1382 }
1383
arovir01b0717b52018-09-05 17:03:25 +01001384
1385 if ( !GetInputScalar(operation, 1, OperandType::INT32, desc.m_TargetHeight, model, data)
1386 || !GetInputScalar(operation, 2, OperandType::INT32, desc.m_TargetWidth, model, data))
1387 {
1388 return Fail("%s: Operation has invalid inputs", __func__);
1389 }
1390
1391 armnn::IConnectableLayer* layer = data.m_Network->AddResizeBilinearLayer(desc);
Mohamed Nour Abouelseoud81afa302018-10-29 14:32:55 +00001392
arovir01b0717b52018-09-05 17:03:25 +01001393 assert(layer != nullptr);
arovir01b0717b52018-09-05 17:03:25 +01001394
Mohamed Nour Abouelseoud81afa302018-10-29 14:32:55 +00001395 layer->GetOutputSlot(0).SetTensorInfo(outputInfo);
1396 input.Connect(layer->GetInputSlot(0));
arovir01b0717b52018-09-05 17:03:25 +01001397
Mohamed Nour Abouelseoud81afa302018-10-29 14:32:55 +00001398 return SetupAndTrackLayerOutputSlot(operation, 0, *layer, model, data);
arovir01b0717b52018-09-05 17:03:25 +01001399
1400}
1401
1402} // namespace hal_1_0
Matteo Martincigh58f71092018-09-25 15:58:52 +01001403} // namespace armnn_driver