/* * 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 #include #include #include #include #include #include #include #include #include #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; ///////////////////////// UTILITY FUNCTIONS ///////////////////////// static void validateGetSupportedOperations(const std::shared_ptr& device, const std::string& message, const Model& model) { SCOPED_TRACE(message + " [getSupportedOperations]"); std::vector supported; const auto retStatus = device->getSupportedOperations(model, &supported); ASSERT_FALSE(retStatus.isOk()); ASSERT_EQ(retStatus.getExceptionCode(), EX_SERVICE_SPECIFIC); ASSERT_EQ(static_cast(retStatus.getServiceSpecificError()), ErrorStatus::INVALID_ARGUMENT); } static void validatePrepareModel(const std::shared_ptr& device, const std::string& message, const Model& model, ExecutionPreference preference, Priority priority) { SCOPED_TRACE(message + " [prepareModel]"); std::shared_ptr preparedModelCallback = ndk::SharedRefBase::make(); 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(prepareLaunchStatus.getServiceSpecificError()), ErrorStatus::INVALID_ARGUMENT); preparedModelCallback->wait(); ErrorStatus prepareReturnStatus = preparedModelCallback->getStatus(); ASSERT_EQ(ErrorStatus::INVALID_ARGUMENT, prepareReturnStatus); std::shared_ptr 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& 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 static size_t sizeForBinder(const Type& val) { static_assert(std::is_trivially_copyable_v>, "expected a trivially copyable type"); return sizeof(val); } template static size_t sizeForBinder(const std::vector& 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& 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()); case Tag::extension: return sizeForBinder(extraParams.get()); } LOG(FATAL) << "Unrecognized extraParams tag: " << static_cast(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 = sizeof(Memory); // Only hardwareBuffer type memory has dynamic memory that needs to be accounted for (in the // form of a NativeHandle type). The other other types of memory (MappableFile, Ashmem) use a // single file descriptor (with metadata) instead. if (memory.getTag() == Memory::Tag::hardwareBuffer) { const NativeHandle& handle = memory.get().handle; size += sizeof(decltype(handle.fds)::value_type) * handle.fds.size(); size += sizeof(decltype(handle.ints)::value_type) * handle.ints.size(); } 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& device, const Model& model, const std::vector& 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(*(ndk::enum_range().end() - 1)) + 1, }; static void mutateOperandTypeTest(const std::shared_ptr& 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(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& 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(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& 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 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& device, const Model& model) { for (size_t operand = 0; operand < model.main.operands.size(); ++operand) { const std::vector 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 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 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& device, const Model& model) { const size_t modelSize = sizeForBinder(model); for (size_t operand = 0; operand < model.main.operands.size(); ++operand) { const std::vector 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 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& device, const Model& model) { const size_t modelSize = sizeForBinder(model); for (size_t operand = 0; operand < model.main.operands.size(); ++operand) { const std::optional 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& 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({1}); newOperand.scale = 0.0f; newOperand.zeroPoint = 0; break; case OperandType::TENSOR_INT32: newOperand.dimensions = operand->dimensions.size() > 0 ? operand->dimensions : std::vector({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({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({1}); newOperand.scale = 0.0f; newOperand.zeroPoint = 0; SymmPerChannelQuantParams channelQuant; channelQuant.channelDim = 0; channelQuant.scales = std::vector( operand->dimensions.size() > 0 ? static_cast(operand->dimensions[0]) : 0); for (size_t i = 0; i < channelQuant.scales.size(); ++i) { channelQuant.scales[i] = 1.0f; } newOperand.extraParams->set( 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& device, const Model& model) { for (size_t operand = 0; operand < model.main.operands.size(); ++operand) { for (OperandType invalidOperandType : ndk::enum_range()) { 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(*(ndk::enum_range().end() - 1)) + 1, }; static void mutateOperationTypeTest(const std::shared_ptr& 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(invalidOperationType); }); } } } ///////////////////////// VALIDATE MODEL OPERATION INPUT OPERAND INDEX ///////////////////////// static void mutateOperationInputOperandIndexTest(const std::shared_ptr& 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& 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& device, const Model& model, const std::vector& 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* 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& 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& device, const Model& model, const std::vector& 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& 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& 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& 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& 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& 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(ExecutionPreference::LOW_POWER) - 1, // lower bound static_cast(ExecutionPreference::SUSTAINED_SPEED) + 1, // upper bound }; static void mutateExecutionPreferenceTest(const std::shared_ptr& 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(invalidPreference); }); } } ///////////////////////// VALIDATE PRIORITY ///////////////////////// static const int32_t invalidPriorities[] = { static_cast(Priority::LOW) - 1, // lower bound static_cast(Priority::HIGH) + 1, // upper bound }; static void mutateExecutionPriorityTest(const std::shared_ptr& 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(invalidPriority); }); } } ////////////////////////// ENTRY POINT ////////////////////////////// void validateModel(const std::shared_ptr& 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