1 /*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #define LOG_TAG "OperationsUtils"
18
19 #include "OperationsUtils.h"
20 #include "Operations.h"
21 #include "Utils.h"
22
23 #include <cmath>
24
25 namespace android {
26 namespace nn {
27
SameShape(const Shape & in1,const Shape & in2)28 bool SameShape(const Shape& in1, const Shape& in2) {
29 if (in1.type != in2.type || in1.dimensions.size() != in2.dimensions.size()) {
30 return false;
31 }
32 for (size_t i = 0; i < in1.dimensions.size(); i++) {
33 if (in1.dimensions[i] != in2.dimensions[i]) {
34 return false;
35 }
36 }
37 return true;
38 }
39
SetShape(const Shape & in,Shape * out)40 bool SetShape(const Shape& in, Shape* out) {
41 if (in.type != out->type || in.dimensions.size() != out->dimensions.size()) {
42 return false;
43 }
44 out->dimensions = in.dimensions;
45 return true;
46 }
47
getNumberOfElements(const Shape & shape)48 uint32_t getNumberOfElements(const Shape& shape) {
49 uint32_t count = 1;
50 for (size_t i = 0; i < shape.dimensions.size(); i++) {
51 count *= shape.dimensions[i];
52 }
53 return count;
54 }
55
getNumberOfDimensions(const Shape & shape)56 uint32_t getNumberOfDimensions(const Shape& shape) {
57 return shape.dimensions.size();
58 }
59
getSizeOfDimension(const Shape & shape,uint32_t dimensionIdx)60 uint32_t getSizeOfDimension(const Shape& shape, uint32_t dimensionIdx) {
61 if (dimensionIdx >= shape.dimensions.size()) {
62 // TODO, log the error
63 return 0;
64 }
65 return shape.dimensions[dimensionIdx];
66 }
67
QuantizeMultiplierSmallerThanOne(double double_multiplier,int32_t * quantized_multiplier,int32_t * right_shift)68 bool QuantizeMultiplierSmallerThanOne(double double_multiplier,
69 int32_t* quantized_multiplier,
70 int32_t* right_shift) {
71 NN_OPS_CHECK(double_multiplier >= 0.);
72 NN_OPS_CHECK(double_multiplier < 1.);
73 if (double_multiplier == 0.) {
74 *quantized_multiplier = 0;
75 *right_shift = 0;
76 return true;
77 }
78 NN_OPS_CHECK(double_multiplier > 0.);
79 const double q = std::frexp(double_multiplier, right_shift);
80 *right_shift *= -1;
81 int64_t q_fixed = static_cast<int64_t>(std::round(q * (1ll << 31)));
82 NN_OPS_CHECK(q_fixed <= (1ll << 31));
83 if (q_fixed == (1ll << 31)) {
84 q_fixed /= 2;
85 --*right_shift;
86 }
87 NN_OPS_CHECK(*right_shift >= 0);
88 NN_OPS_CHECK(q_fixed <= std::numeric_limits<int32_t>::max());
89 *quantized_multiplier = static_cast<int32_t>(q_fixed);
90 return true;
91 }
92
QuantizeMultiplierGreaterThanOne(double double_multiplier,int32_t * quantized_multiplier,int * left_shift)93 bool QuantizeMultiplierGreaterThanOne(double double_multiplier,
94 int32_t* quantized_multiplier,
95 int* left_shift) {
96 NN_OPS_CHECK(double_multiplier > 1.);
97 const double q = std::frexp(double_multiplier, left_shift);
98 int64_t q_fixed = static_cast<int64_t>(std::round(q * (1ll << 31)));
99 NN_OPS_CHECK(q_fixed <= (1ll << 31));
100 if (q_fixed == (1ll << 31)) {
101 q_fixed /= 2;
102 ++*left_shift;
103 }
104 NN_OPS_CHECK(*left_shift >= 0);
105 NN_OPS_CHECK(q_fixed <= std::numeric_limits<int32_t>::max());
106 *quantized_multiplier = static_cast<int32_t>(q_fixed);
107 return true;
108 }
109
GetQuantizedConvolutionMultipler(const Shape & inputShape,const Shape & filterShape,const Shape & biasShape,const Shape & outputShape,float * multiplier)110 bool GetQuantizedConvolutionMultipler(const Shape& inputShape,
111 const Shape& filterShape,
112 const Shape& biasShape,
113 const Shape& outputShape,
114 float* multiplier) {
115 const float input_product_scale = inputShape.scale * filterShape.scale;
116 const float bias_scale = biasShape.scale;
117 const float output_scale = outputShape.scale;
118
119 // The following conditions must be guaranteed by the training pipeline.
120 NN_OPS_CHECK(std::abs(input_product_scale - bias_scale) <=
121 1e-6 * std::min(input_product_scale, bias_scale));
122 NN_OPS_CHECK(input_product_scale >= 0);
123 NN_OPS_CHECK(input_product_scale < output_scale);
124 *multiplier = input_product_scale / output_scale;
125 return true;
126 }
127
CalculateActivationRangeUint8(int32_t activation,const Shape & outputShape,int32_t * act_min,int32_t * act_max)128 void CalculateActivationRangeUint8(int32_t activation,
129 const Shape& outputShape,
130 int32_t* act_min,
131 int32_t* act_max) {
132 const int32_t qmin = std::numeric_limits<uint8_t>::min();
133 const int32_t qmax = std::numeric_limits<uint8_t>::max();
134
135 const auto scale = outputShape.scale;
136 const auto zero_point = outputShape.offset;
137
138 auto quantize = [scale, zero_point](float f) {
139 return zero_point + static_cast<int32_t>(std::round(f / scale));
140 };
141
142 if (activation == kActivationRelu) {
143 *act_min = std::max(qmin, quantize(0.0));
144 *act_max = qmax;
145 } else if (activation == kActivationRelu6) {
146 *act_min = std::max(qmin, quantize(0.0));
147 *act_max = std::min(qmax, quantize(6.0));
148 } else if (activation == kActivationRelu1) {
149 *act_min = std::max(qmin, quantize(-1.0));
150 *act_max = std::min(qmax, quantize(1.0));
151 } else {
152 *act_min = qmin;
153 *act_max = qmax;
154 }
155 }
156
CalculateInputRadius(int input_integer_bits,int input_left_shift)157 int32_t CalculateInputRadius(int input_integer_bits, int input_left_shift) {
158 const double max_input_rescaled = 1.0 * ((1 << input_integer_bits) - 1) *
159 (1ll << (31 - input_integer_bits)) /
160 (1ll << input_left_shift);
161 // Tighten bound using floor. Suppose that we could use the exact value.
162 // After scaling the difference, the result would be at the maximum. Thus we
163 // must ensure that our value has lower magnitude.
164 return static_cast<int32_t>(std::floor(max_input_rescaled));
165 }
166
addMulPrepare(const Shape & in1,const Shape & in2,Shape * out)167 bool addMulPrepare(const Shape& in1, const Shape& in2, Shape* out) {
168 NN_OPS_CHECK(getNumberOfDimensions(in1) <= 4 && getNumberOfDimensions(in2) <= 4);
169 NN_OPS_CHECK(in1.type == in2.type);
170 if (SameShape(in1, in2)) {
171 return SetShape(in1, out);
172 } else {
173 // BroadcastAdd needed
174 uint32_t numberOfDims1 = getNumberOfDimensions(in1);
175 uint32_t numberOfDims2 = getNumberOfDimensions(in2);
176 uint32_t maxDims = std::max(numberOfDims1, numberOfDims2);
177 out->dimensions = std::vector<uint32_t>(maxDims);
178 for (uint32_t i = 1; i <= maxDims; i++) {
179 uint32_t dim1 = 1;
180 if (i <= numberOfDims1) {
181 dim1 = getSizeOfDimension(in1, numberOfDims1 - i);
182 }
183 uint32_t dim2 = 1;
184 if (i <= numberOfDims2) {
185 dim2 = getSizeOfDimension(in2, numberOfDims2 - i);
186 }
187 if (dim1 != dim2 && dim1 != 1 && dim2 != 1) {
188 LOG(ERROR) << "Dimensions mismatch for BroadcastAdd";
189 return false;
190 }
191 out->dimensions[maxDims - i] = std::max(dim1, dim2);
192 }
193 }
194 return true;
195 }
196
floorPrepare(const Shape & input,Shape * output)197 bool floorPrepare(const Shape& input, Shape* output) {
198 return SetShape(input, output);
199 }
200
dequantizePrepare(const Shape & input,Shape * output)201 bool dequantizePrepare(const Shape& input, Shape* output) {
202 if (input.type != OperandType::TENSOR_QUANT8_ASYMM ||
203 output->type != OperandType::TENSOR_FLOAT32) {
204 LOG(ERROR) << "bad input / output operand type.";
205 return false;
206 }
207 if (input.dimensions.size() != output->dimensions.size()) {
208 LOG(ERROR) << "input and output tensors don't have the same rank.";
209 return false;
210 }
211 output->dimensions = input.dimensions;
212 return true;
213 }
214
convPrepare(const Shape & input,const Shape & filter,const Shape & bias,int32_t padding_left,int32_t padding_right,int32_t padding_top,int32_t padding_bottom,int32_t stride_width,int32_t stride_height,Shape * output)215 bool convPrepare(const Shape& input,
216 const Shape& filter,
217 const Shape& bias,
218 int32_t padding_left, int32_t padding_right,
219 int32_t padding_top, int32_t padding_bottom,
220 int32_t stride_width, int32_t stride_height,
221 Shape* output) {
222 NN_OPS_CHECK(input.type == filter.type);
223 if (input.type == OperandType::TENSOR_QUANT8_ASYMM) {
224 NN_OPS_CHECK(bias.type == OperandType::TENSOR_INT32);
225 } else {
226 NN_OPS_CHECK(input.type == bias.type);
227 }
228 NN_OPS_CHECK(getNumberOfDimensions(input) == 4);
229 NN_OPS_CHECK(getNumberOfDimensions(filter) == 4);
230 NN_OPS_CHECK(getNumberOfDimensions(bias) == 1);
231
232 NN_OPS_CHECK(getSizeOfDimension(filter, 0) == getSizeOfDimension(bias, 0));
233 NN_OPS_CHECK(getSizeOfDimension(filter, 3) == getSizeOfDimension(input, 3));
234
235 uint32_t channels_out = getSizeOfDimension(filter, 0);
236 uint32_t width = getSizeOfDimension(input, 2);
237 uint32_t height = getSizeOfDimension(input, 1);
238 uint32_t filterWidth = getSizeOfDimension(filter, 2);
239 uint32_t filterHeight = getSizeOfDimension(filter, 1);
240 uint32_t batches = getSizeOfDimension(input, 0);
241
242 uint32_t outWidth = computeOutSize(width, filterWidth, stride_width,
243 padding_left, padding_right);
244 uint32_t outHeight = computeOutSize(height, filterHeight, stride_height,
245 padding_top, padding_bottom);
246
247 output->type = input.type;
248 output->dimensions = {batches, outHeight, outWidth, channels_out};
249 return true;
250 }
251
depthwiseConvPrepare(const Shape & input,const Shape & filter,const Shape & bias,int32_t padding_left,int32_t padding_right,int32_t padding_top,int32_t padding_bottom,int32_t stride_width,int32_t stride_height,Shape * output)252 bool depthwiseConvPrepare(const Shape& input,
253 const Shape& filter,
254 const Shape& bias,
255 int32_t padding_left, int32_t padding_right,
256 int32_t padding_top, int32_t padding_bottom,
257 int32_t stride_width, int32_t stride_height,
258 Shape* output) {
259 NN_OPS_CHECK(input.type == filter.type);
260 if (input.type == OperandType::TENSOR_QUANT8_ASYMM) {
261 NN_OPS_CHECK(bias.type == OperandType::TENSOR_INT32);
262 } else {
263 NN_OPS_CHECK(input.type == bias.type);
264 }
265 NN_OPS_CHECK(getNumberOfDimensions(input) == 4);
266 NN_OPS_CHECK(getNumberOfDimensions(filter) == 4);
267 NN_OPS_CHECK(getNumberOfDimensions(bias) == 1);
268
269 NN_OPS_CHECK(getSizeOfDimension(filter, 3) == getSizeOfDimension(bias, 0));
270
271 uint32_t channels_out = getSizeOfDimension(filter, 3);
272 uint32_t width = getSizeOfDimension(input, 2);
273 uint32_t height = getSizeOfDimension(input, 1);
274 uint32_t filterWidth = getSizeOfDimension(filter, 2);
275 uint32_t filterHeight = getSizeOfDimension(filter, 1);
276 uint32_t batches = getSizeOfDimension(input, 0);
277
278 uint32_t outWidth = computeOutSize(width, filterWidth, stride_width,
279 padding_left, padding_right);
280 uint32_t outHeight = computeOutSize(height, filterHeight, stride_height,
281 padding_top, padding_bottom);
282
283 output->type = input.type;
284 output->dimensions = {batches, outHeight, outWidth, channels_out};
285 return true;
286 }
287
288
genericPoolingPrepare(const Shape & input,int32_t padding_left,int32_t padding_right,int32_t padding_top,int32_t padding_bottom,int32_t stride_width,int32_t stride_height,int32_t filter_width,int32_t filter_height,Shape * output)289 bool genericPoolingPrepare(const Shape& input,
290 int32_t padding_left, int32_t padding_right,
291 int32_t padding_top, int32_t padding_bottom,
292 int32_t stride_width, int32_t stride_height,
293 int32_t filter_width, int32_t filter_height,
294 Shape* output) {
295 NN_OPS_CHECK(getNumberOfDimensions(input) == 4);
296
297 uint32_t batches = getSizeOfDimension(input, 0);
298 uint32_t width = getSizeOfDimension(input, 2);
299 uint32_t height = getSizeOfDimension(input, 1);
300 uint32_t channels_out = getSizeOfDimension(input, 3);
301
302 uint32_t outWidth = computeOutSize(width, filter_width, stride_width,
303 padding_left, padding_right);
304 uint32_t outHeight = computeOutSize(height, filter_height, stride_height,
305 padding_top, padding_bottom);
306
307 output->type = input.type;
308 output->dimensions = {batches, outHeight, outWidth, channels_out};
309 return true;
310 }
311
312
genericActivationPrepare(const Shape & input,Shape * output)313 bool genericActivationPrepare(const Shape& input,
314 Shape* output) {
315 NN_OPS_CHECK(getNumberOfDimensions(input) <= 4);
316 return SetShape(input, output);
317 }
318
fullyConnectedPrepare(const Shape & input,const Shape & weights,const Shape & bias,Shape * output)319 bool fullyConnectedPrepare(const Shape& input,
320 const Shape& weights,
321 const Shape& bias,
322 Shape* output) {
323 // Check all the parameters of tensor match within themselves and match the
324 // input configuration.
325 NN_OPS_CHECK(input.type == weights.type);
326 if (input.type == OperandType::TENSOR_QUANT8_ASYMM) {
327 NN_OPS_CHECK(bias.type == OperandType::TENSOR_INT32);
328 } else {
329 NN_OPS_CHECK(input.type == bias.type);
330 }
331 NN_OPS_CHECK(getNumberOfDimensions(input) >= 2);
332 uint32_t input_size = getNumberOfElements(input);
333 uint32_t num_units = getSizeOfDimension(weights, 0);
334 uint32_t batch_size = input_size / getSizeOfDimension(weights, 1);
335
336 NN_OPS_CHECK(getSizeOfDimension(bias, 0) == num_units);
337 NN_OPS_CHECK(getSizeOfDimension(weights, 1) * batch_size == input_size);
338 NN_OPS_CHECK(getNumberOfDimensions(weights) == 2);
339
340 output->type = input.type;
341 output->dimensions = {batch_size, num_units};
342
343 return true;
344 }
345
concatenationPrepare(const std::vector<Shape> & inputShapes,int32_t axis,Shape * output)346 bool concatenationPrepare(const std::vector<Shape>& inputShapes,
347 int32_t axis,
348 Shape* output) {
349
350 int num_inputs = inputShapes.size();
351 OperandType input_type = inputShapes[0].type;
352 uint32_t num_dimensions = getNumberOfDimensions(inputShapes[0]);
353
354 NN_OPS_CHECK(axis >= 0);
355 NN_OPS_CHECK(axis < (int32_t)num_dimensions);
356
357 int sum_axis = getSizeOfDimension(inputShapes[0], axis);
358 for (int i = 1; i < num_inputs; ++i) {
359 NN_OPS_CHECK(getNumberOfDimensions(inputShapes[i]) == num_dimensions);
360 NN_OPS_CHECK(inputShapes[i].type == inputShapes[0].type);
361 if (input_type == OperandType::TENSOR_QUANT8_ASYMM) {
362 NN_OPS_CHECK(inputShapes[0].offset == inputShapes[i].offset);
363 NN_OPS_CHECK(inputShapes[0].scale == inputShapes[i].scale);
364 }
365 for (int d = 0; d < (int32_t)num_dimensions; ++d) {
366 if (d == axis) {
367 sum_axis += getSizeOfDimension(inputShapes[i], axis);
368 } else {
369 NN_OPS_CHECK(getSizeOfDimension(inputShapes[0], d) ==
370 getSizeOfDimension(inputShapes[i], d));
371 }
372 }
373 }
374
375 output->type = input_type;
376 output->dimensions = inputShapes[0].dimensions;
377 output->dimensions[axis] = sum_axis;
378
379 if (input_type == OperandType::TENSOR_QUANT8_ASYMM) {
380 NN_OPS_CHECK(inputShapes[0].offset == output->offset);
381 NN_OPS_CHECK(inputShapes[0].scale == output->scale);
382 }
383
384 return true;
385 }
386
387
genericNormalizationPrepare(const Shape & input,Shape * output)388 bool genericNormalizationPrepare(const Shape& input, Shape* output) {
389 NN_OPS_CHECK(getNumberOfDimensions(input) == 4);
390 return SetShape(input, output);
391 }
392
reshapePrepare(const Shape & input,const int32_t * targetDims,const int32_t targetDimsSize,Shape * output)393 bool reshapePrepare(const Shape& input,
394 const int32_t* targetDims,
395 const int32_t targetDimsSize,
396 Shape* output) {
397 // Reshape allows one of the targetDims components to have the
398 // special -1 value, meaning it will be calculated automatically based on the
399 // input. Here we calculate what that dimension should be so that the number
400 // of output elements in the same as the number of input elements.
401 int32_t numInputElements = (int32_t) getNumberOfElements(input);
402
403 std::vector<uint32_t> outDims(targetDimsSize);
404 int32_t numOutputElements = 1;
405 int32_t strechDim = -1;
406 for (int32_t i = 0; i < targetDimsSize; ++i) {
407 int32_t value = targetDims[i];
408 if (value == -1) {
409 NN_OPS_CHECK(strechDim == -1);
410 strechDim = i;
411 } else {
412 numOutputElements *= value;
413 outDims[i] = (uint32_t)value;
414 }
415 }
416 if (strechDim != -1) {
417 int32_t strechValue = numInputElements / numOutputElements;
418 outDims[strechDim] = (uint32_t) strechValue;
419 numOutputElements *= strechValue;
420 }
421
422 NN_OPS_CHECK(numInputElements == numOutputElements);
423
424 output->type = input.type;
425 output->dimensions = outDims;
426 output->offset = input.offset;
427 output->scale = input.scale;
428
429 return true;
430 }
431
resizeBilinearPrepare(const Shape & input,int32_t width,int32_t height,Shape * output)432 bool resizeBilinearPrepare(const Shape& input,
433 int32_t width,
434 int32_t height,
435 Shape* output) {
436 NN_OPS_CHECK(getNumberOfDimensions(input) == 4);
437 uint32_t batches = getSizeOfDimension(input, 0);
438 uint32_t channels = getSizeOfDimension(input, 3);
439
440 output->type = input.type;
441 output->dimensions = {batches, (uint32_t)height, (uint32_t)width, channels};
442
443 return true;
444 }
445
depthToSpacePrepare(const Shape & input,int32_t blockSize,Shape * output)446 bool depthToSpacePrepare(const Shape& input,
447 int32_t blockSize,
448 Shape* output) {
449 NN_OPS_CHECK(getNumberOfDimensions(input) == 4);
450 NN_OPS_CHECK(blockSize > 0);
451
452 uint32_t batches = getSizeOfDimension(input, 0);
453 uint32_t height = getSizeOfDimension(input, 1);
454 uint32_t width = getSizeOfDimension(input, 2);
455 uint32_t channels = getSizeOfDimension(input, 3);
456
457 NN_OPS_CHECK(channels % (blockSize * blockSize) == 0);
458 output->type = input.type;
459 output->dimensions = {batches,
460 height * blockSize,
461 width * blockSize,
462 channels / (blockSize * blockSize)};
463 output->offset = input.offset;
464 output->scale = input.scale;
465
466 return true;
467 }
468
spaceToDepthPrepare(const Shape & input,int32_t blockSize,Shape * output)469 bool spaceToDepthPrepare(const Shape& input,
470 int32_t blockSize,
471 Shape* output) {
472 NN_OPS_CHECK(getNumberOfDimensions(input) == 4);
473 NN_OPS_CHECK(blockSize > 0);
474
475 uint32_t batches = getSizeOfDimension(input, 0);
476 uint32_t height = getSizeOfDimension(input, 1);
477 uint32_t width = getSizeOfDimension(input, 2);
478 uint32_t channels = getSizeOfDimension(input, 3);
479
480 NN_OPS_CHECK(height % blockSize == 0);
481 NN_OPS_CHECK(width % blockSize == 0);
482
483 output->type = input.type;
484 output->dimensions = {batches,
485 height / blockSize,
486 width / blockSize,
487 channels * (blockSize * blockSize)};
488 output->offset = input.offset;
489 output->scale = input.scale;
490
491 return true;
492 }
493
embeddingLookupPrepare(const Shape & valueShape,const Shape & lookupShape,Shape * outputShape)494 bool embeddingLookupPrepare(const Shape &valueShape,
495 const Shape &lookupShape,
496 Shape *outputShape) {
497 NN_OPS_CHECK(getNumberOfDimensions(valueShape) >= 2);
498 NN_OPS_CHECK(getNumberOfDimensions(lookupShape) == 1);
499
500 const uint32_t rows = getSizeOfDimension(valueShape, 0);
501 const uint32_t columns = getSizeOfDimension(valueShape, 1);
502
503 const uint32_t lookups = getSizeOfDimension(lookupShape, 0);
504
505 outputShape->type = valueShape.type;
506 outputShape->dimensions = { lookups, columns };
507 for (uint32_t i = 2; i < getNumberOfDimensions(valueShape); i++) {
508 outputShape->dimensions.push_back(getSizeOfDimension(valueShape, i));
509 }
510 outputShape->offset = valueShape.offset;
511 outputShape->scale = valueShape.scale;
512
513 return true;
514 }
515
hashtableLookupPrepare(const Shape & lookupShape,const Shape & keyShape,const Shape & valueShape,Shape * outputShape,Shape * hitShape)516 bool hashtableLookupPrepare(const Shape &lookupShape,
517 const Shape &keyShape,
518 const Shape &valueShape,
519 Shape *outputShape,
520 Shape *hitShape) {
521 NN_OPS_CHECK(getNumberOfDimensions(lookupShape) == 1);
522 NN_OPS_CHECK(getNumberOfDimensions(keyShape) == 1);
523 NN_OPS_CHECK(getNumberOfDimensions(valueShape) >= 1);
524
525 const uint32_t lookups = getSizeOfDimension(lookupShape, 0);
526 const uint32_t keys = getSizeOfDimension(keyShape, 0);
527 const uint32_t rows = getSizeOfDimension(valueShape, 0);
528 outputShape->type = valueShape.type;
529 outputShape->dimensions = { lookups };
530 for (uint32_t i = 1; i < getNumberOfDimensions(valueShape); i++) {
531 outputShape->dimensions.push_back(getSizeOfDimension(valueShape, i));
532 }
533 outputShape->offset = valueShape.offset;
534 outputShape->scale = valueShape.scale;
535
536 hitShape->type = OperandType::TENSOR_QUANT8_ASYMM;
537 hitShape->dimensions = { lookups };
538 hitShape->offset = 0;
539 hitShape->scale = 1.f;
540
541 return true;
542 }
543
544 } // namespace nn
545 } // namespace android
546