blob: 94d3daf6bbba5414bff86790749f760672f39de5 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define LOG_TAG "neuralnetworks_aidl_hal_test"
#include <aidl/android/hardware/common/NativeHandle.h>
#include <android/binder_auto_utils.h>
#include <android/binder_enums.h>
#include <android/binder_interface_utils.h>
#include <nnapi/TypeUtils.h>
#include <nnapi/hal/aidl/Conversions.h>
#include <nnapi/hal/aidl/Utils.h>
#include <optional>
#include <type_traits>
#include <utility>
#include "Callbacks.h"
#include "GeneratedTestHarness.h"
#include "Utils.h"
#include "VtsHalNeuralnetworks.h"
namespace aidl::android::hardware::neuralnetworks::vts::functional {
using common::NativeHandle;
using implementation::PreparedModelCallback;
using PrepareModelMutation = std::function<void(Model*, ExecutionPreference*, Priority*)>;
///////////////////////// UTILITY FUNCTIONS /////////////////////////
static void validateGetSupportedOperations(const std::shared_ptr<IDevice>& device,
const std::string& message, const Model& model) {
SCOPED_TRACE(message + " [getSupportedOperations]");
std::vector<bool> supported;
const auto retStatus = device->getSupportedOperations(model, &supported);
ASSERT_FALSE(retStatus.isOk());
ASSERT_EQ(retStatus.getExceptionCode(), EX_SERVICE_SPECIFIC);
ASSERT_EQ(static_cast<ErrorStatus>(retStatus.getServiceSpecificError()),
ErrorStatus::INVALID_ARGUMENT);
}
static void validatePrepareModel(const std::shared_ptr<IDevice>& device, const std::string& message,
const Model& model, ExecutionPreference preference,
Priority priority) {
SCOPED_TRACE(message + " [prepareModel]");
std::shared_ptr<PreparedModelCallback> preparedModelCallback =
ndk::SharedRefBase::make<PreparedModelCallback>();
const auto prepareLaunchStatus =
device->prepareModel(model, preference, priority, kNoDeadline, {}, {}, kEmptyCacheToken,
preparedModelCallback);
ASSERT_FALSE(prepareLaunchStatus.isOk());
ASSERT_EQ(prepareLaunchStatus.getExceptionCode(), EX_SERVICE_SPECIFIC);
ASSERT_EQ(static_cast<ErrorStatus>(prepareLaunchStatus.getServiceSpecificError()),
ErrorStatus::INVALID_ARGUMENT);
preparedModelCallback->wait();
ErrorStatus prepareReturnStatus = preparedModelCallback->getStatus();
ASSERT_EQ(ErrorStatus::INVALID_ARGUMENT, prepareReturnStatus);
std::shared_ptr<IPreparedModel> preparedModel = preparedModelCallback->getPreparedModel();
ASSERT_EQ(nullptr, preparedModel.get());
}
static bool validExecutionPreference(ExecutionPreference preference) {
return preference == ExecutionPreference::LOW_POWER ||
preference == ExecutionPreference::FAST_SINGLE_ANSWER ||
preference == ExecutionPreference::SUSTAINED_SPEED;
}
static bool validExecutionPriority(Priority priority) {
return priority == Priority::LOW || priority == Priority::MEDIUM || priority == Priority::HIGH;
}
// Primary validation function. This function will take a valid model, apply a
// mutation to invalidate the model, the execution preference, or the priority,
// then pass these to supportedOperations and/or prepareModel if that method is
// called with an invalid argument.
static void validate(const std::shared_ptr<IDevice>& device, const std::string& message,
const Model& originalModel, const PrepareModelMutation& mutate) {
Model model = utils::clone(originalModel).value();
ExecutionPreference preference = ExecutionPreference::FAST_SINGLE_ANSWER;
Priority priority = kDefaultPriority;
mutate(&model, &preference, &priority);
if (validExecutionPreference(preference) && validExecutionPriority(priority)) {
validateGetSupportedOperations(device, message, model);
}
validatePrepareModel(device, message, model, preference, priority);
}
static uint32_t addOperand(Model* model) {
model->main.operands.push_back({
.type = OperandType::INT32,
.dimensions = {},
.scale = 0.0f,
.zeroPoint = 0,
.lifetime = OperandLifeTime::SUBGRAPH_INPUT,
.location = {.poolIndex = 0, .offset = 0, .length = 0},
});
return model->main.operands.size() - 1;
}
static uint32_t addOperand(Model* model, OperandLifeTime lifetime) {
uint32_t index = addOperand(model);
model->main.operands[index].lifetime = lifetime;
return index;
}
// If we introduce a CONSTANT_COPY for an operand of size operandSize,
// how much will this increase the size of the model? This assumes
// that we can (re)use all of model.operandValues for the operand
// value.
static size_t constantCopyExtraSize(const Model& model, size_t operandSize) {
const size_t operandValuesSize = model.operandValues.size();
return (operandValuesSize < operandSize) ? (operandSize - operandValuesSize) : 0;
}
// Highly specialized utility routine for converting an operand to
// CONSTANT_COPY lifetime.
//
// Expects that:
// - operand has a known size
// - operand->lifetime has already been set to CONSTANT_COPY
// - operand->location has been zeroed out
//
// Does the following:
// - initializes operand->location to point to the beginning of model->operandValues
// - resizes model->operandValues (if necessary) to be large enough for the operand
// value, padding it with zeroes on the end
//
// Potential problem:
// By changing the operand to CONSTANT_COPY lifetime, this function is effectively initializing the
// operand with unspecified (but deterministic) data. This means that the model may be invalidated
// in two ways: not only is the lifetime of CONSTANT_COPY invalid, but the operand's value in the
// graph may also be invalid (e.g., if the operand is used as an activation code and has an invalid
// value). For now, this should be fine because it just means we're not testing what we think we're
// testing in certain cases; but we can handwave this and assume we're probabilistically likely to
// exercise the validation code over the span of the entire test set and operand space.
//
// Aborts if the specified operand type is an extension type or OEM type.
static void becomeConstantCopy(Model* model, Operand* operand) {
// sizeOfData will abort if the specified type is an extension type or OEM type.
const size_t sizeOfOperand = sizeOfData(*operand);
EXPECT_NE(sizeOfOperand, size_t(0));
operand->location.poolIndex = 0;
operand->location.offset = 0;
operand->location.length = sizeOfOperand;
if (model->operandValues.size() < sizeOfOperand) {
model->operandValues.resize(sizeOfOperand);
}
}
// The sizeForBinder() functions estimate the size of the
// representation of a value when sent to binder. It's probably a bit
// of an under-estimate, because we don't know the size of the
// metadata in the binder format (e.g., representation of the size of
// a vector); but at least it adds up "big" things like vector
// contents. However, it doesn't treat inter-field or end-of-struct
// padding in a methodical way -- there's no attempt to be consistent
// in whether or not padding in the native (C++) representation
// contributes to the estimated size for the binder representation;
// and there's no attempt to understand what padding (if any) is
// needed in the binder representation.
//
// This assumes that non-metadata uses a fixed length encoding (e.g.,
// a uint32_t is always encoded in sizeof(uint32_t) bytes, rather than
// using an encoding whose length is related to the magnitude of the
// encoded value).
template <typename Type>
static size_t sizeForBinder(const Type& val) {
static_assert(std::is_trivially_copyable_v<std::remove_reference_t<Type>>,
"expected a trivially copyable type");
return sizeof(val);
}
template <typename Type>
static size_t sizeForBinder(const std::vector<Type>& vec) {
return std::accumulate(vec.begin(), vec.end(), 0,
[](size_t acc, const Type& x) { return acc + sizeForBinder(x); });
}
template <>
size_t sizeForBinder(const SymmPerChannelQuantParams& symmPerChannelQuantParams) {
size_t size = 0;
size += sizeForBinder(symmPerChannelQuantParams.scales);
size += sizeForBinder(symmPerChannelQuantParams.channelDim);
return size;
}
template <>
size_t sizeForBinder(const std::optional<OperandExtraParams>& optionalExtraParams) {
if (!optionalExtraParams.has_value()) {
return 0;
}
const auto& extraParams = optionalExtraParams.value();
using Tag = OperandExtraParams::Tag;
switch (extraParams.getTag()) {
case Tag::channelQuant:
return sizeForBinder(extraParams.get<Tag::channelQuant>());
case Tag::extension:
return sizeForBinder(extraParams.get<Tag::extension>());
}
LOG(FATAL) << "Unrecognized extraParams tag: " << static_cast<int>(extraParams.getTag());
return 0;
}
template <>
size_t sizeForBinder(const Operand& operand) {
size_t size = 0;
size += sizeForBinder(operand.type);
size += sizeForBinder(operand.dimensions);
size += sizeForBinder(operand.scale);
size += sizeForBinder(operand.zeroPoint);
size += sizeForBinder(operand.lifetime);
size += sizeForBinder(operand.location);
size += sizeForBinder(operand.extraParams);
return size;
}
template <>
size_t sizeForBinder(const Operation& operation) {
size_t size = 0;
size += sizeForBinder(operation.type);
size += sizeForBinder(operation.inputs);
size += sizeForBinder(operation.outputs);
return size;
}
template <>
size_t sizeForBinder(const std::string& name) {
return name.size();
}
template <>
size_t sizeForBinder(const Memory& memory) {
// This is just a guess.
size_t size = 0;
const NativeHandle& handle = memory.handle;
size += sizeof(decltype(handle.fds)::value_type) * handle.fds.size();
size += sizeof(decltype(handle.ints)::value_type) * handle.ints.size();
size += sizeForBinder(memory.name);
size += sizeof(memory);
return size;
}
template <>
size_t sizeForBinder(const Subgraph& subgraph) {
size_t size = 0;
size += sizeForBinder(subgraph.operands);
size += sizeForBinder(subgraph.operations);
size += sizeForBinder(subgraph.inputIndexes);
size += sizeForBinder(subgraph.outputIndexes);
return size;
}
template <>
size_t sizeForBinder(const ExtensionNameAndPrefix& extensionNameToPrefix) {
size_t size = 0;
size += sizeForBinder(extensionNameToPrefix.name);
size += sizeForBinder(extensionNameToPrefix.prefix);
return size;
}
template <>
size_t sizeForBinder(const Model& model) {
size_t size = 0;
size += sizeForBinder(model.main);
size += sizeForBinder(model.referenced);
size += sizeForBinder(model.operandValues);
size += sizeForBinder(model.pools);
size += sizeForBinder(model.relaxComputationFloat32toFloat16);
size += sizeForBinder(model.extensionNameToPrefix);
return size;
}
// https://developer.android.com/reference/android/os/TransactionTooLargeException.html
//
// "The Binder transaction buffer has a limited fixed size,
// currently 1Mb, which is shared by all transactions in progress
// for the process."
//
// Will our representation fit under this limit? There are two complications:
// - Our representation size is just approximate (see sizeForBinder()).
// - This object may not be the only occupant of the Binder transaction buffer.
// So we'll be very conservative: We want the representation size to be no
// larger than half the transaction buffer size.
//
// If our representation grows large enough that it still fits within
// the transaction buffer but combined with other transactions may
// exceed the buffer size, then we may see intermittent HAL transport
// errors.
static bool exceedsBinderSizeLimit(size_t representationSize) {
// Instead of using this fixed buffer size, we might instead be able to use
// ProcessState::self()->getMmapSize(). However, this has a potential
// problem: The binder/mmap size of the current process does not necessarily
// indicate the binder/mmap size of the service (i.e., the other process).
// The only way it would be a good indication is if both the current process
// and the service use the default size.
static const size_t kHalfBufferSize = 1024 * 1024 / 2;
return representationSize > kHalfBufferSize;
}
///////////////////////// VALIDATE EXECUTION ORDER ////////////////////////////
static void mutateExecutionOrderTest(const std::shared_ptr<IDevice>& device, const Model& model,
const std::vector<uint32_t>& numberOfConsumers) {
for (size_t operation = 0; operation < model.main.operations.size(); ++operation) {
const Operation& operationObj = model.main.operations[operation];
for (uint32_t input : operationObj.inputs) {
if (model.main.operands[input].lifetime == OperandLifeTime::TEMPORARY_VARIABLE ||
model.main.operands[input].lifetime == OperandLifeTime::SUBGRAPH_OUTPUT) {
// This operation reads an operand written by some
// other operation. Move this operation to the
// beginning of the sequence, ensuring that it reads
// the operand before that operand is written, thereby
// violating execution order rules.
const std::string message = "mutateExecutionOrderTest: operation " +
std::to_string(operation) + " is a reader";
validate(device, message, model,
[operation](Model* model, ExecutionPreference*, Priority*) {
auto& operations = model->main.operations;
std::rotate(operations.begin(), operations.begin() + operation,
operations.begin() + operation + 1);
});
break; // only need to do this once per operation
}
}
for (uint32_t output : operationObj.outputs) {
if (numberOfConsumers[output] > 0) {
// This operation writes an operand read by some other
// operation. Move this operation to the end of the
// sequence, ensuring that it writes the operand after
// that operand is read, thereby violating execution
// order rules.
const std::string message = "mutateExecutionOrderTest: operation " +
std::to_string(operation) + " is a writer";
validate(device, message, model,
[operation](Model* model, ExecutionPreference*, Priority*) {
auto& operations = model->main.operations;
std::rotate(operations.begin() + operation,
operations.begin() + operation + 1, operations.end());
});
break; // only need to do this once per operation
}
}
}
}
///////////////////////// VALIDATE MODEL OPERAND TYPE /////////////////////////
static const int32_t invalidOperandTypes[] = {
-1,
static_cast<int32_t>(*(ndk::enum_range<OperandType>().end() - 1)) + 1,
};
static void mutateOperandTypeTest(const std::shared_ptr<IDevice>& device, const Model& model) {
for (size_t operand = 0; operand < model.main.operands.size(); ++operand) {
for (int32_t invalidOperandType : invalidOperandTypes) {
const std::string message = "mutateOperandTypeTest: operand " +
std::to_string(operand) + " set to value " +
std::to_string(invalidOperandType);
validate(device, message, model,
[operand, invalidOperandType](Model* model, ExecutionPreference*, Priority*) {
model->main.operands[operand].type =
static_cast<OperandType>(invalidOperandType);
});
}
}
}
///////////////////////// VALIDATE OPERAND RANK /////////////////////////
static uint32_t getInvalidRank(OperandType type) {
switch (type) {
case OperandType::FLOAT16:
case OperandType::FLOAT32:
case OperandType::INT32:
case OperandType::UINT32:
case OperandType::BOOL:
return 1;
case OperandType::TENSOR_BOOL8:
case OperandType::TENSOR_FLOAT16:
case OperandType::TENSOR_FLOAT32:
case OperandType::TENSOR_INT32:
case OperandType::TENSOR_QUANT8_ASYMM:
case OperandType::TENSOR_QUANT8_SYMM:
case OperandType::TENSOR_QUANT16_ASYMM:
case OperandType::TENSOR_QUANT16_SYMM:
case OperandType::TENSOR_QUANT8_SYMM_PER_CHANNEL:
return 0;
default:
return 0;
}
}
static void mutateOperandRankTest(const std::shared_ptr<IDevice>& device, const Model& model) {
for (size_t operand = 0; operand < model.main.operands.size(); ++operand) {
const uint32_t invalidRank = getInvalidRank(model.main.operands[operand].type);
if (invalidRank == 0) {
continue;
}
const std::string message = "mutateOperandRankTest: operand " + std::to_string(operand) +
" has rank of " + std::to_string(invalidRank);
validate(device, message, model,
[operand, invalidRank](Model* model, ExecutionPreference*, Priority*) {
model->main.operands[operand].dimensions =
std::vector<int32_t>(invalidRank, 0);
});
}
}
///////////////////////// VALIDATE OPERAND SCALE /////////////////////////
static float getInvalidScale(OperandType type) {
switch (type) {
case OperandType::FLOAT16:
case OperandType::FLOAT32:
case OperandType::INT32:
case OperandType::UINT32:
case OperandType::BOOL:
case OperandType::TENSOR_BOOL8:
case OperandType::TENSOR_FLOAT16:
case OperandType::TENSOR_FLOAT32:
case OperandType::TENSOR_QUANT8_SYMM_PER_CHANNEL:
case OperandType::SUBGRAPH:
return 1.0f;
case OperandType::TENSOR_INT32:
return -1.0f;
case OperandType::TENSOR_QUANT8_SYMM:
case OperandType::TENSOR_QUANT8_ASYMM:
case OperandType::TENSOR_QUANT16_ASYMM:
case OperandType::TENSOR_QUANT16_SYMM:
return 0.0f;
default:
return 0.0f;
}
}
static void mutateOperandScaleTest(const std::shared_ptr<IDevice>& device, const Model& model) {
for (size_t operand = 0; operand < model.main.operands.size(); ++operand) {
const float invalidScale = getInvalidScale(model.main.operands[operand].type);
const std::string message = "mutateOperandScaleTest: operand " + std::to_string(operand) +
" has scale of " + std::to_string(invalidScale);
validate(device, message, model,
[operand, invalidScale](Model* model, ExecutionPreference*, Priority*) {
model->main.operands[operand].scale = invalidScale;
});
}
}
///////////////////////// VALIDATE OPERAND ZERO POINT /////////////////////////
static std::vector<int32_t> getInvalidZeroPoints(OperandType type) {
switch (type) {
case OperandType::FLOAT16:
case OperandType::FLOAT32:
case OperandType::INT32:
case OperandType::UINT32:
case OperandType::BOOL:
case OperandType::TENSOR_BOOL8:
case OperandType::TENSOR_FLOAT16:
case OperandType::TENSOR_FLOAT32:
case OperandType::TENSOR_INT32:
case OperandType::TENSOR_QUANT8_SYMM_PER_CHANNEL:
case OperandType::SUBGRAPH:
return {1};
case OperandType::TENSOR_QUANT8_ASYMM:
return {-1, 256};
case OperandType::TENSOR_QUANT8_SYMM:
return {-129, -1, 1, 128};
case OperandType::TENSOR_QUANT16_ASYMM:
return {-1, 65536};
case OperandType::TENSOR_QUANT16_SYMM:
return {-32769, -1, 1, 32768};
default:
return {};
}
}
static void mutateOperandZeroPointTest(const std::shared_ptr<IDevice>& device, const Model& model) {
for (size_t operand = 0; operand < model.main.operands.size(); ++operand) {
const std::vector<int32_t> invalidZeroPoints =
getInvalidZeroPoints(model.main.operands[operand].type);
for (int32_t invalidZeroPoint : invalidZeroPoints) {
const std::string message = "mutateOperandZeroPointTest: operand " +
std::to_string(operand) + " has zero point of " +
std::to_string(invalidZeroPoint);
validate(device, message, model,
[operand, invalidZeroPoint](Model* model, ExecutionPreference*, Priority*) {
model->main.operands[operand].zeroPoint = invalidZeroPoint;
});
}
}
}
///////////////////////// VALIDATE OPERAND LIFETIME /////////////////////////////////////////////
static std::vector<OperandLifeTime> getInvalidLifeTimes(const Model& model, size_t modelSize,
const Operand& operand) {
// TODO: Support OperandLifeTime::CONSTANT_REFERENCE as an invalid lifetime
// TODO: Support OperandLifeTime::NO_VALUE as an invalid lifetime
// Ways to get an invalid lifetime:
// - change whether a lifetime means an operand should have a writer
std::vector<OperandLifeTime> ret;
switch (operand.lifetime) {
case OperandLifeTime::SUBGRAPH_OUTPUT:
case OperandLifeTime::TEMPORARY_VARIABLE:
ret = {
OperandLifeTime::SUBGRAPH_INPUT,
OperandLifeTime::CONSTANT_COPY,
};
break;
case OperandLifeTime::CONSTANT_COPY:
case OperandLifeTime::CONSTANT_POOL:
case OperandLifeTime::SUBGRAPH_INPUT:
ret = {
OperandLifeTime::TEMPORARY_VARIABLE,
OperandLifeTime::SUBGRAPH_OUTPUT,
};
break;
case OperandLifeTime::NO_VALUE:
// Not enough information to know whether
// TEMPORARY_VARIABLE or CONSTANT_COPY would be invalid --
// is this operand written (then CONSTANT_COPY would be
// invalid) or not (then TEMPORARY_VARIABLE would be
// invalid)?
break;
case OperandLifeTime::SUBGRAPH:
break;
default:
ADD_FAILURE();
break;
}
const size_t operandSize = sizeOfData(operand); // will be zero if shape is unknown
if (!operandSize ||
exceedsBinderSizeLimit(modelSize + constantCopyExtraSize(model, operandSize))) {
// Unknown size or too-large size
ret.erase(std::remove(ret.begin(), ret.end(), OperandLifeTime::CONSTANT_COPY), ret.end());
}
return ret;
}
static void mutateOperandLifeTimeTest(const std::shared_ptr<IDevice>& device, const Model& model) {
const size_t modelSize = sizeForBinder(model);
for (size_t operand = 0; operand < model.main.operands.size(); ++operand) {
const std::vector<OperandLifeTime> invalidLifeTimes =
getInvalidLifeTimes(model, modelSize, model.main.operands[operand]);
for (OperandLifeTime invalidLifeTime : invalidLifeTimes) {
const std::string message = "mutateOperandLifetimeTest: operand " +
std::to_string(operand) + " has lifetime " +
toString(invalidLifeTime) + " instead of lifetime " +
toString(model.main.operands[operand].lifetime);
validate(device, message, model,
[operand, invalidLifeTime](Model* model, ExecutionPreference*, Priority*) {
static const DataLocation kZeroDataLocation = {};
Operand& operandObj = model->main.operands[operand];
switch (operandObj.lifetime) {
case OperandLifeTime::SUBGRAPH_INPUT: {
auto& inputs = model->main.inputIndexes;
inputs.erase(std::remove(inputs.begin(), inputs.end(), operand),
inputs.end());
break;
}
case OperandLifeTime::SUBGRAPH_OUTPUT: {
auto& outputs = model->main.outputIndexes;
outputs.erase(std::remove(outputs.begin(), outputs.end(), operand),
outputs.end());
break;
}
default:
break;
}
operandObj.lifetime = invalidLifeTime;
operandObj.location = kZeroDataLocation;
switch (invalidLifeTime) {
case OperandLifeTime::CONSTANT_COPY: {
becomeConstantCopy(model, &operandObj);
break;
}
case OperandLifeTime::SUBGRAPH_INPUT:
model->main.inputIndexes.push_back(operand);
break;
case OperandLifeTime::SUBGRAPH_OUTPUT:
model->main.outputIndexes.push_back(operand);
break;
default:
break;
}
});
}
}
}
///////////////////////// VALIDATE OPERAND INPUT-or-OUTPUT //////////////////////////////////////
static std::optional<OperandLifeTime> getInputOutputLifeTime(const Model& model, size_t modelSize,
const Operand& operand) {
// Ways to get an invalid lifetime (with respect to model inputIndexes and outputIndexes):
// - change whether a lifetime means an operand is a model input, a model output, or neither
// - preserve whether or not a lifetime means an operand should have a writer
switch (operand.lifetime) {
case OperandLifeTime::CONSTANT_COPY:
case OperandLifeTime::CONSTANT_POOL:
return OperandLifeTime::SUBGRAPH_INPUT;
case OperandLifeTime::SUBGRAPH_INPUT: {
const size_t operandSize = sizeOfData(operand); // will be zero if shape is unknown
if (!operandSize ||
exceedsBinderSizeLimit(modelSize + constantCopyExtraSize(model, operandSize))) {
// Unknown size or too-large size
break;
}
return OperandLifeTime::CONSTANT_COPY;
}
case OperandLifeTime::SUBGRAPH_OUTPUT:
return OperandLifeTime::TEMPORARY_VARIABLE;
case OperandLifeTime::TEMPORARY_VARIABLE:
return OperandLifeTime::SUBGRAPH_OUTPUT;
case OperandLifeTime::NO_VALUE:
// Not enough information to know whether
// TEMPORARY_VARIABLE or CONSTANT_COPY would be an
// appropriate choice -- is this operand written (then
// TEMPORARY_VARIABLE would be appropriate) or not (then
// CONSTANT_COPY would be appropriate)?
break;
case OperandLifeTime::SUBGRAPH:
break;
default:
ADD_FAILURE();
break;
}
return std::nullopt;
}
static void mutateOperandInputOutputTest(const std::shared_ptr<IDevice>& device,
const Model& model) {
const size_t modelSize = sizeForBinder(model);
for (size_t operand = 0; operand < model.main.operands.size(); ++operand) {
const std::optional<OperandLifeTime> changedLifeTime =
getInputOutputLifeTime(model, modelSize, model.main.operands[operand]);
if (changedLifeTime) {
const std::string message = "mutateOperandInputOutputTest: operand " +
std::to_string(operand) + " has lifetime " +
toString(*changedLifeTime) + " instead of lifetime " +
toString(model.main.operands[operand].lifetime);
validate(device, message, model,
[operand, changedLifeTime](Model* model, ExecutionPreference*, Priority*) {
static const DataLocation kZeroDataLocation = {};
Operand& operandObj = model->main.operands[operand];
operandObj.lifetime = *changedLifeTime;
operandObj.location = kZeroDataLocation;
if (*changedLifeTime == OperandLifeTime::CONSTANT_COPY) {
becomeConstantCopy(model, &operandObj);
}
});
}
}
}
///////////////////////// VALIDATE OPERAND NUMBER OF WRITERS ////////////////////////////////////
static void mutateOperandAddWriterTest(const std::shared_ptr<IDevice>& device, const Model& model) {
for (size_t operation = 0; operation < model.main.operations.size(); ++operation) {
for (size_t badOutputNum = 0;
badOutputNum < model.main.operations[operation].outputs.size(); ++badOutputNum) {
const uint32_t outputOperandIndex =
model.main.operations[operation].outputs[badOutputNum];
const std::string message = "mutateOperandAddWriterTest: operation " +
std::to_string(operation) + " writes to " +
std::to_string(outputOperandIndex);
// We'll insert a copy of the operation, all of whose
// OTHER output operands are newly-created -- i.e.,
// there'll only be a duplicate write of ONE of that
// operation's output operands.
validate(device, message, model,
[operation, badOutputNum](Model* model, ExecutionPreference*, Priority*) {
Operation newOperation = model->main.operations[operation];
for (size_t outputNum = 0; outputNum < newOperation.outputs.size();
++outputNum) {
if (outputNum == badOutputNum) continue;
Operand operandValue =
model->main.operands[newOperation.outputs[outputNum]];
if (operandValue.lifetime == OperandLifeTime::SUBGRAPH_OUTPUT) {
operandValue.lifetime = OperandLifeTime::TEMPORARY_VARIABLE;
} else {
ASSERT_EQ(operandValue.lifetime,
OperandLifeTime::TEMPORARY_VARIABLE);
}
newOperation.outputs[outputNum] = model->main.operands.size();
model->main.operands.push_back(operandValue);
}
// Where do we insert the extra writer (a new
// operation)? It has to be later than all the
// writers of its inputs. The easiest thing to do
// is to insert it at the end of the operation
// sequence.
model->main.operations.push_back(newOperation);
});
}
}
}
///////////////////////// VALIDATE EXTRA ??? /////////////////////////
// TODO: Operand::location
///////////////////////// VALIDATE OPERATION OPERAND TYPE /////////////////////////
static void mutateOperand(Operand* operand, OperandType type) {
Operand newOperand = *operand;
newOperand.type = type;
switch (type) {
case OperandType::FLOAT16:
case OperandType::FLOAT32:
case OperandType::INT32:
case OperandType::UINT32:
case OperandType::BOOL:
newOperand.dimensions = {};
newOperand.scale = 0.0f;
newOperand.zeroPoint = 0;
break;
case OperandType::TENSOR_BOOL8:
case OperandType::TENSOR_FLOAT16:
case OperandType::TENSOR_FLOAT32:
newOperand.dimensions = operand->dimensions.size() > 0 ? operand->dimensions
: std::vector<int32_t>({1});
newOperand.scale = 0.0f;
newOperand.zeroPoint = 0;
break;
case OperandType::TENSOR_INT32:
newOperand.dimensions = operand->dimensions.size() > 0 ? operand->dimensions
: std::vector<int32_t>({1});
newOperand.zeroPoint = 0;
break;
case OperandType::TENSOR_QUANT8_ASYMM:
case OperandType::TENSOR_QUANT8_SYMM:
case OperandType::TENSOR_QUANT16_ASYMM:
case OperandType::TENSOR_QUANT16_SYMM:
newOperand.dimensions = operand->dimensions.size() > 0 ? operand->dimensions
: std::vector<int32_t>({1});
newOperand.scale = operand->scale != 0.0f ? operand->scale : 1.0f;
break;
case OperandType::TENSOR_QUANT8_SYMM_PER_CHANNEL: {
newOperand.dimensions = operand->dimensions.size() > 0 ? operand->dimensions
: std::vector<int32_t>({1});
newOperand.scale = 0.0f;
newOperand.zeroPoint = 0;
SymmPerChannelQuantParams channelQuant;
channelQuant.channelDim = 0;
channelQuant.scales = std::vector<float>(
operand->dimensions.size() > 0 ? static_cast<size_t>(operand->dimensions[0])
: 0);
for (size_t i = 0; i < channelQuant.scales.size(); ++i) {
channelQuant.scales[i] = 1.0f;
}
newOperand.extraParams->set<OperandExtraParams::Tag::channelQuant>(
std::move(channelQuant));
} break;
default:
break;
}
*operand = newOperand;
}
static bool mutateOperationOperandTypeSkip(size_t operand, OperandType type, const Model& model) {
if (type == model.main.operands[operand].type) {
return true;
}
for (const Operation& operation : model.main.operations) {
// Skip mutateOperationOperandTypeTest for the following operations.
// - LSH_PROJECTION's second argument is allowed to have any type.
// - ARGMIN and ARGMAX's first argument can be any of
// TENSOR_(FLOAT16|FLOAT32|INT32|QUANT8_ASYMM).
// - CAST's argument can be any of TENSOR_(FLOAT16|FLOAT32|INT32|QUANT8_ASYMM).
// - RANDOM_MULTINOMIAL's argument can be either TENSOR_FLOAT16 or TENSOR_FLOAT32.
// - DEQUANTIZE input can be any of
// TENSOR_(QUANT8_ASYMM|QUANT8_ASYMM_SIGNED|QUANT8_SYMM|QUANT8_SYMM_PER_CHANNEL),
// output can be of either TENSOR_FLOAT16 or TENSOR_FLOAT32.
// - QUANTIZE input can be either TENSOR_FLOAT16 or TENSOR_FLOAT32
// - CONV_2D filter type (arg 1) can be QUANT8_ASYMM or QUANT8_SYMM_PER_CHANNEL
// - DEPTHWISE_CONV_2D filter type (arg 1) can be QUANT8_ASYMM or QUANT8_SYMM_PER_CHANNEL
// - GROUPED_CONV_2D filter type (arg 1) can be QUANT8_ASYMM or QUANT8_SYMM_PER_CHANNEL
// - TRANSPOSE_CONV_2D filter type (arg 1) can be QUANT8_ASYMM or QUANT8_SYMM_PER_CHANNEL
// - AXIS_ALIGNED_BBOX_TRANSFORM bounding boxes (arg 1) can be of
// TENSOR_QUANT8_ASYMM or TENSOR_QUANT8_ASYMM_SIGNED.
// - RANK's input can have any TENSOR_* type.
switch (operation.type) {
case OperationType::LSH_PROJECTION: {
if (operand == operation.inputs[1]) {
return true;
}
} break;
case OperationType::CAST:
case OperationType::ARGMAX:
case OperationType::ARGMIN: {
if (type == OperandType::TENSOR_FLOAT16 || type == OperandType::TENSOR_FLOAT32 ||
type == OperandType::TENSOR_INT32 || type == OperandType::TENSOR_QUANT8_ASYMM ||
type == OperandType::TENSOR_QUANT8_ASYMM_SIGNED) {
return true;
}
} break;
case OperationType::QUANTIZE: {
if (operand == operation.inputs[0] &&
(type == OperandType::TENSOR_FLOAT16 || type == OperandType::TENSOR_FLOAT32)) {
return true;
}
if (operand == operation.outputs[0] &&
(type == OperandType::TENSOR_QUANT8_ASYMM ||
type == OperandType::TENSOR_QUANT8_ASYMM_SIGNED)) {
return true;
}
} break;
case OperationType::RANDOM_MULTINOMIAL: {
if (operand == operation.inputs[0] &&
(type == OperandType::TENSOR_FLOAT16 || type == OperandType::TENSOR_FLOAT32)) {
return true;
}
} break;
case OperationType::DEQUANTIZE: {
if (operand == operation.inputs[0] &&
(type == OperandType::TENSOR_QUANT8_ASYMM ||
type == OperandType::TENSOR_QUANT8_ASYMM_SIGNED ||
type == OperandType::TENSOR_QUANT8_SYMM ||
type == OperandType::TENSOR_QUANT8_SYMM_PER_CHANNEL)) {
return true;
}
if (operand == operation.outputs[0] &&
(type == OperandType::TENSOR_FLOAT16 || type == OperandType::TENSOR_FLOAT32)) {
return true;
}
} break;
case OperationType::TRANSPOSE_CONV_2D:
case OperationType::GROUPED_CONV_2D:
case OperationType::DEPTHWISE_CONV_2D:
case OperationType::CONV_2D: {
if (operand == operation.inputs[1] &&
(type == OperandType::TENSOR_QUANT8_ASYMM ||
type == OperandType::TENSOR_QUANT8_SYMM_PER_CHANNEL)) {
return true;
}
} break;
case OperationType::AXIS_ALIGNED_BBOX_TRANSFORM: {
if (operand == operation.inputs[1] &&
(type == OperandType::TENSOR_QUANT8_ASYMM ||
type == OperandType::TENSOR_QUANT8_ASYMM_SIGNED)) {
return true;
}
} break;
case OperationType::RANK: {
if (operand == operation.inputs[0] &&
(type == OperandType::TENSOR_FLOAT16 || type == OperandType::TENSOR_FLOAT32 ||
type == OperandType::TENSOR_INT32 ||
type == OperandType::TENSOR_QUANT8_ASYMM ||
type == OperandType::TENSOR_QUANT16_SYMM ||
type == OperandType::TENSOR_BOOL8 ||
type == OperandType::TENSOR_QUANT8_SYMM_PER_CHANNEL ||
type == OperandType::TENSOR_QUANT16_ASYMM ||
type == OperandType::TENSOR_QUANT8_SYMM ||
type == OperandType::TENSOR_QUANT8_ASYMM_SIGNED)) {
return true;
}
} break;
default:
break;
}
}
return false;
}
static void mutateOperationOperandTypeTest(const std::shared_ptr<IDevice>& device,
const Model& model) {
for (size_t operand = 0; operand < model.main.operands.size(); ++operand) {
for (OperandType invalidOperandType : ndk::enum_range<OperandType>()) {
if (mutateOperationOperandTypeSkip(operand, invalidOperandType, model)) {
continue;
}
const std::string message = "mutateOperationOperandTypeTest: operand " +
std::to_string(operand) + " set to type " +
toString(invalidOperandType);
validate(device, message, model,
[operand, invalidOperandType](Model* model, ExecutionPreference*, Priority*) {
mutateOperand(&model->main.operands[operand], invalidOperandType);
});
}
}
}
///////////////////////// VALIDATE MODEL OPERATION TYPE /////////////////////////
static const int32_t invalidOperationTypes[] = {
-1,
static_cast<int32_t>(*(ndk::enum_range<OperationType>().end() - 1)) + 1,
};
static void mutateOperationTypeTest(const std::shared_ptr<IDevice>& device, const Model& model) {
for (size_t operation = 0; operation < model.main.operations.size(); ++operation) {
for (int32_t invalidOperationType : invalidOperationTypes) {
const std::string message = "mutateOperationTypeTest: operation " +
std::to_string(operation) + " set to value " +
std::to_string(invalidOperationType);
validate(device, message, model,
[operation, invalidOperationType](Model* model, ExecutionPreference*,
Priority*) {
model->main.operations[operation].type =
static_cast<OperationType>(invalidOperationType);
});
}
}
}
///////////////////////// VALIDATE MODEL OPERATION INPUT OPERAND INDEX /////////////////////////
static void mutateOperationInputOperandIndexTest(const std::shared_ptr<IDevice>& device,
const Model& model) {
for (size_t operation = 0; operation < model.main.operations.size(); ++operation) {
const uint32_t invalidOperand = model.main.operands.size();
for (size_t input = 0; input < model.main.operations[operation].inputs.size(); ++input) {
const std::string message = "mutateOperationInputOperandIndexTest: operation " +
std::to_string(operation) + " input " +
std::to_string(input);
validate(device, message, model,
[operation, input, invalidOperand](Model* model, ExecutionPreference*,
Priority*) {
model->main.operations[operation].inputs[input] = invalidOperand;
});
}
}
}
///////////////////////// VALIDATE MODEL OPERATION OUTPUT OPERAND INDEX /////////////////////////
static void mutateOperationOutputOperandIndexTest(const std::shared_ptr<IDevice>& device,
const Model& model) {
for (size_t operation = 0; operation < model.main.operations.size(); ++operation) {
const uint32_t invalidOperand = model.main.operands.size();
for (size_t output = 0; output < model.main.operations[operation].outputs.size();
++output) {
const std::string message = "mutateOperationOutputOperandIndexTest: operation " +
std::to_string(operation) + " output " +
std::to_string(output);
validate(device, message, model,
[operation, output, invalidOperand](Model* model, ExecutionPreference*,
Priority*) {
model->main.operations[operation].outputs[output] = invalidOperand;
});
}
}
}
///////////////////////// VALIDATE MODEL OPERANDS WRITTEN ///////////////////////////////////////
static void mutateOperationRemoveWriteTest(const std::shared_ptr<IDevice>& device,
const Model& model,
const std::vector<uint32_t>& numberOfConsumers) {
for (size_t operation = 0; operation < model.main.operations.size(); ++operation) {
for (size_t outputNum = 0; outputNum < model.main.operations[operation].outputs.size();
++outputNum) {
const uint32_t outputOperandIndex = model.main.operations[operation].outputs[outputNum];
if (numberOfConsumers[outputOperandIndex] > 0) {
const std::string message = "mutateOperationRemoveWriteTest: operation " +
std::to_string(operation) + " writes to " +
std::to_string(outputOperandIndex);
validate(device, message, model,
[operation, outputNum](Model* model, ExecutionPreference*, Priority*) {
int32_t& outputOperandIndex =
model->main.operations[operation].outputs[outputNum];
Operand operandValue = model->main.operands[outputOperandIndex];
if (operandValue.lifetime == OperandLifeTime::SUBGRAPH_OUTPUT) {
operandValue.lifetime = OperandLifeTime::TEMPORARY_VARIABLE;
} else {
ASSERT_EQ(operandValue.lifetime,
OperandLifeTime::TEMPORARY_VARIABLE);
}
outputOperandIndex = model->main.operands.size();
model->main.operands.push_back(operandValue);
});
}
}
}
}
///////////////////////// REMOVE OPERAND FROM EVERYTHING /////////////////////////
static void removeValueAndDecrementGreaterValues(std::vector<int32_t>* vec, uint32_t value) {
if (vec) {
// remove elements matching "value"
vec->erase(std::remove(vec->begin(), vec->end(), value), vec->end());
// decrement elements exceeding "value"
std::transform(vec->begin(), vec->end(), vec->begin(),
[value](uint32_t v) { return v > value ? v-- : v; });
}
}
static void removeOperand(Model* model, uint32_t index) {
model->main.operands.erase(model->main.operands.begin() + index);
for (Operation& operation : model->main.operations) {
removeValueAndDecrementGreaterValues(&operation.inputs, index);
removeValueAndDecrementGreaterValues(&operation.outputs, index);
}
removeValueAndDecrementGreaterValues(&model->main.inputIndexes, index);
removeValueAndDecrementGreaterValues(&model->main.outputIndexes, index);
}
static bool removeOperandSkip(size_t operandIndex, const Model& model,
const std::vector<uint32_t>& numberOfConsumers) {
if (numberOfConsumers[operandIndex] == 0) {
// Removing an unused operand has no effect.
return true;
}
for (const Operation& operation : model.main.operations) {
// Skip removeOperandTest for the following operations.
// - SPLIT's outputs are not checked during prepareModel.
if (operation.type == OperationType::SPLIT) {
for (const size_t index : operation.outputs) {
if (index == operandIndex) {
return true;
}
}
}
// BIDIRECTIONAL_SEQUENCE_LSTM and BIDIRECTIONAL_SEQUENCE_RNN can have
// either one, two, three or four outputs depending on their
// mergeOutputs parameter and if state outputs are provided.
// UNIDIRECTIONAL_SEQUENCE_LSTM and UNIDIRECTIONAL_SEQUENCE_RNN can have
// either one or three outputs depending on whether state outputs are
// provided.
if (operation.type == OperationType::UNIDIRECTIONAL_SEQUENCE_LSTM ||
operation.type == OperationType::UNIDIRECTIONAL_SEQUENCE_RNN ||
operation.type == OperationType::BIDIRECTIONAL_SEQUENCE_LSTM ||
operation.type == OperationType::BIDIRECTIONAL_SEQUENCE_RNN) {
for (const size_t index : operation.outputs) {
if (index == operandIndex) {
return true;
}
}
}
}
return false;
}
static void removeOperandTest(const std::shared_ptr<IDevice>& device, const Model& model,
const std::vector<uint32_t>& numberOfConsumers) {
for (size_t operand = 0; operand < model.main.operands.size(); ++operand) {
if (removeOperandSkip(operand, model, numberOfConsumers)) {
continue;
}
const std::string message = "removeOperandTest: operand " + std::to_string(operand);
validate(device, message, model, [operand](Model* model, ExecutionPreference*, Priority*) {
removeOperand(model, operand);
});
}
}
///////////////////////// REMOVE OPERATION /////////////////////////
static void removeOperation(Model* model, uint32_t index) {
auto& operations = model->main.operations;
operations.erase(operations.begin() + index);
}
static void removeOperationTest(const std::shared_ptr<IDevice>& device, const Model& model) {
for (size_t operation = 0; operation < model.main.operations.size(); ++operation) {
const std::string message = "removeOperationTest: operation " + std::to_string(operation);
validate(device, message, model,
[operation](Model* model, ExecutionPreference*, Priority*) {
removeOperation(model, operation);
});
}
}
///////////////////////// REMOVE OPERATION INPUT /////////////////////////
static bool removeOperationInputSkip(const Operation& op, size_t input) {
// Skip removeOperationInputTest for the following operations.
// - CONCATENATION has at least 2 inputs, with the last element being INT32.
// - CONV_2D, DEPTHWISE_CONV_2D, MAX_POOL_2D, AVERAGE_POOL_2D, L2_POOL_2D, RESIZE_BILINEAR,
// SPACE_TO_DEPTH, SPACE_TO_DEPTH, SPACE_TO_BATCH_ND, BATCH_TO_SPACE_ND can have an optional
// layout parameter.
// RESIZE_BILINEAR and RESIZE_NEAREST_NEIGHBOR can have optional
// align_corners and half_pixel_centers parameters.
// - L2_NORMALIZATION, LOCAL_RESPONSE_NORMALIZATION, SOFTMAX can have an optional axis
// parameter.
switch (op.type) {
case OperationType::CONCATENATION: {
if (op.inputs.size() > 2 && input != op.inputs.size() - 1) {
return true;
}
} break;
case OperationType::DEPTHWISE_CONV_2D: {
if ((op.inputs.size() == 12 && input == 11) || (op.inputs.size() == 9 && input == 8)) {
return true;
}
} break;
case OperationType::CONV_2D:
case OperationType::AVERAGE_POOL_2D:
case OperationType::MAX_POOL_2D:
case OperationType::L2_POOL_2D: {
if ((op.inputs.size() == 11 && input == 10) || (op.inputs.size() == 8 && input == 7)) {
return true;
}
} break;
case OperationType::RESIZE_BILINEAR: {
if (op.inputs.size() >= 4 && input >= 3) {
return true;
}
} break;
case OperationType::RESIZE_NEAREST_NEIGHBOR: {
if (op.inputs.size() >= 5 && input >= 3) {
return true;
}
} break;
case OperationType::SPACE_TO_DEPTH:
case OperationType::DEPTH_TO_SPACE:
case OperationType::BATCH_TO_SPACE_ND: {
if (op.inputs.size() == 3 && input == 2) {
return true;
}
} break;
case OperationType::SPACE_TO_BATCH_ND: {
if (op.inputs.size() == 4 && input == 3) {
return true;
}
} break;
case OperationType::L2_NORMALIZATION: {
if (op.inputs.size() == 2 && input == 1) {
return true;
}
} break;
case OperationType::LOCAL_RESPONSE_NORMALIZATION: {
if (op.inputs.size() == 6 && input == 5) {
return true;
}
} break;
case OperationType::SOFTMAX: {
if (op.inputs.size() == 3 && input == 2) {
return true;
}
} break;
default:
break;
}
return false;
}
static void removeOperationInputTest(const std::shared_ptr<IDevice>& device, const Model& model) {
for (size_t operation = 0; operation < model.main.operations.size(); ++operation) {
for (size_t input = 0; input < model.main.operations[operation].inputs.size(); ++input) {
const Operation& op = model.main.operations[operation];
if (removeOperationInputSkip(op, input)) {
continue;
}
const std::string message = "removeOperationInputTest: operation " +
std::to_string(operation) + ", input " +
std::to_string(input);
validate(device, message, model,
[operation, input](Model* model, ExecutionPreference*, Priority*) {
auto& inputs = model->main.operations[operation].inputs;
inputs.erase(inputs.begin() + input);
});
}
}
}
///////////////////////// REMOVE OPERATION OUTPUT /////////////////////////
static void removeOperationOutputTest(const std::shared_ptr<IDevice>& device, const Model& model) {
for (size_t operation = 0; operation < model.main.operations.size(); ++operation) {
for (size_t output = 0; output < model.main.operations[operation].outputs.size();
++output) {
const std::string message = "removeOperationOutputTest: operation " +
std::to_string(operation) + ", output " +
std::to_string(output);
validate(device, message, model,
[operation, output](Model* model, ExecutionPreference*, Priority*) {
auto& outputs = model->main.operations[operation].outputs;
outputs.erase(outputs.begin() + output);
});
}
}
}
///////////////////////// MODEL VALIDATION /////////////////////////
// TODO: remove model input
// TODO: remove model output
// TODO: add unused operation
///////////////////////// ADD OPERATION INPUT /////////////////////////
static bool addOperationInputSkip(const Operation& op) {
// Skip addOperationInputTest for the following operations.
// - L2_NORMALIZATION, LOCAL_RESPONSE_NORMALIZATION, SOFTMAX can have an optional INT32 axis
// parameter.
if ((op.type == OperationType::L2_NORMALIZATION && op.inputs.size() == 1) ||
(op.type == OperationType::LOCAL_RESPONSE_NORMALIZATION && op.inputs.size() == 5) ||
(op.type == OperationType::SOFTMAX && op.inputs.size() == 2) ||
(op.type == OperationType::RESIZE_BILINEAR && op.inputs.size() < 6) ||
(op.type == OperationType::RESIZE_NEAREST_NEIGHBOR && op.inputs.size() < 6)) {
return true;
}
return false;
}
static void addOperationInputTest(const std::shared_ptr<IDevice>& device, const Model& model) {
for (size_t operation = 0; operation < model.main.operations.size(); ++operation) {
if (addOperationInputSkip(model.main.operations[operation])) {
continue;
}
const std::string message = "addOperationInputTest: operation " + std::to_string(operation);
validate(device, message, model,
[operation](Model* model, ExecutionPreference*, Priority*) {
uint32_t index = addOperand(model, OperandLifeTime::SUBGRAPH_INPUT);
model->main.operations[operation].inputs.push_back(index);
model->main.inputIndexes.push_back(index);
});
}
}
///////////////////////// ADD OPERATION OUTPUT /////////////////////////
static void addOperationOutputTest(const std::shared_ptr<IDevice>& device, const Model& model) {
for (size_t operation = 0; operation < model.main.operations.size(); ++operation) {
const std::string message =
"addOperationOutputTest: operation " + std::to_string(operation);
validate(device, message, model,
[operation](Model* model, ExecutionPreference*, Priority*) {
uint32_t index = addOperand(model, OperandLifeTime::SUBGRAPH_OUTPUT);
model->main.operations[operation].outputs.push_back(index);
model->main.outputIndexes.push_back(index);
});
}
}
///////////////////////// VALIDATE EXECUTION PREFERENCE /////////////////////////
static const int32_t invalidExecutionPreferences[] = {
static_cast<int32_t>(ExecutionPreference::LOW_POWER) - 1, // lower bound
static_cast<int32_t>(ExecutionPreference::SUSTAINED_SPEED) + 1, // upper bound
};
static void mutateExecutionPreferenceTest(const std::shared_ptr<IDevice>& device,
const Model& model) {
for (int32_t invalidPreference : invalidExecutionPreferences) {
const std::string message =
"mutateExecutionPreferenceTest: preference " + std::to_string(invalidPreference);
validate(device, message, model,
[invalidPreference](Model*, ExecutionPreference* preference, Priority*) {
*preference = static_cast<ExecutionPreference>(invalidPreference);
});
}
}
///////////////////////// VALIDATE PRIORITY /////////////////////////
static const int32_t invalidPriorities[] = {
static_cast<int32_t>(Priority::LOW) - 1, // lower bound
static_cast<int32_t>(Priority::HIGH) + 1, // upper bound
};
static void mutateExecutionPriorityTest(const std::shared_ptr<IDevice>& device,
const Model& model) {
for (int32_t invalidPriority : invalidPriorities) {
const std::string message =
"mutatePriorityTest: priority " + std::to_string(invalidPriority);
validate(device, message, model,
[invalidPriority](Model*, ExecutionPreference*, Priority* priority) {
*priority = static_cast<Priority>(invalidPriority);
});
}
}
////////////////////////// ENTRY POINT //////////////////////////////
void validateModel(const std::shared_ptr<IDevice>& device, const Model& model) {
const auto numberOfConsumers =
nn::countNumberOfConsumers(model.main.operands.size(),
nn::unvalidatedConvert(model.main.operations).value())
.value();
mutateExecutionOrderTest(device, model, numberOfConsumers);
mutateOperandTypeTest(device, model);
mutateOperandRankTest(device, model);
mutateOperandScaleTest(device, model);
mutateOperandZeroPointTest(device, model);
mutateOperandLifeTimeTest(device, model);
mutateOperandInputOutputTest(device, model);
mutateOperandAddWriterTest(device, model);
mutateOperationOperandTypeTest(device, model);
mutateOperationTypeTest(device, model);
mutateOperationInputOperandIndexTest(device, model);
mutateOperationOutputOperandIndexTest(device, model);
mutateOperationRemoveWriteTest(device, model, numberOfConsumers);
removeOperandTest(device, model, numberOfConsumers);
removeOperationTest(device, model);
removeOperationInputTest(device, model);
removeOperationOutputTest(device, model);
addOperationInputTest(device, model);
addOperationOutputTest(device, model);
mutateExecutionPreferenceTest(device, model);
mutateExecutionPriorityTest(device, model);
}
} // namespace aidl::android::hardware::neuralnetworks::vts::functional