/* * 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 "ShimConverter" #include "ShimConverter.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace ::android::nn::sl_wrapper; namespace aidl::android::hardware::neuralnetworks { namespace { // Assumes that isValid(model) holds ANeuralNetworksModel* convertSubgraphFromHAL( const NnApiSupportLibrary* nnapi, const std::vector>& memoryPools, const neuralnetworks::Model& model, std::vector>* allModels, size_t subgraphIndex, const std::vector& copiedOperandValues, ErrorStatus* errorStatus) { *errorStatus = ErrorStatus::NONE; if (allModels == nullptr || subgraphIndex >= (*allModels).size()) { *errorStatus = ErrorStatus::INVALID_ARGUMENT; return nullptr; } if ((*allModels)[subgraphIndex].has_value()) { return (*allModels)[subgraphIndex]->getHandle(); } const auto& subgraph = subgraphIndex == 0 ? model.main : model.referenced[subgraphIndex - 1]; ::android::nn::sl_wrapper::Model resultModel(nnapi); resultModel.relaxComputationFloat32toFloat16(model.relaxComputationFloat32toFloat16); auto getExtensionName = [&](uint16_t prefix) -> const std::string* { for (const auto& nameToPrefix : model.extensionNameToPrefix) { if (prefix == nameToPrefix.prefix) { return &nameToPrefix.name; } } return nullptr; }; for (int i = 0; i < subgraph.operands.size(); ++i) { const auto& operand = subgraph.operands[i]; const std::vector dimensions = ::android::nn::toUnsigned(operand.dimensions).value(); ::android::nn::wrapper::OperandType operandType( static_cast<::android::nn::wrapper::Type>(operand.type), dimensions, operand.scale, operand.zeroPoint); if (operand.type == OperandType::TENSOR_QUANT8_SYMM_PER_CHANNEL) { const auto& params = operand.extraParams->get(); operandType.channelQuant = ::android::nn::wrapper::SymmPerChannelQuantParams( params.scales, static_cast(params.channelDim)); } if (::android::nn::isExtension(static_cast<::android::nn::OperandType>(operand.type))) { uint16_t extensionPrefix = ::android::nn::getExtensionPrefix(static_cast(operand.type)); uint16_t typeWithinExtension = ::android::nn::getTypeWithinExtension(static_cast(operand.type)); auto* extensionName = getExtensionName(extensionPrefix); if (extensionName == nullptr) { LOG(ERROR) << "Unknown extension prefix " << extensionPrefix; *errorStatus = ErrorStatus::INVALID_ARGUMENT; return nullptr; } resultModel.getExtensionOperandType(*extensionName, typeWithinExtension, &operandType.operandType.type); if (!resultModel.isValid()) { LOG(ERROR) << "Failed to get extension operand with index " << i; *errorStatus = ErrorStatus::INVALID_ARGUMENT; return nullptr; } } uint32_t operandIndex = resultModel.addOperand(&operandType); if (!resultModel.isValid()) { LOG(ERROR) << "Failed to add operand with index " << i; *errorStatus = ErrorStatus::INVALID_ARGUMENT; return nullptr; } if (operand.extraParams && operand.extraParams->getTag() == OperandExtraParams::Tag::extension) { const auto& extensionData = operand.extraParams->get(); resultModel.setOperandExtensionData(operandIndex, extensionData.data(), extensionData.size()); if (!resultModel.isValid()) { LOG(ERROR) << "Failed to add extension data for operand with index " << i; *errorStatus = ErrorStatus::INVALID_ARGUMENT; return nullptr; } } switch (operand.lifetime) { case OperandLifeTime::CONSTANT_COPY: { if (operand.location.length + operand.location.offset > model.operandValues.size()) { *errorStatus = ErrorStatus::INVALID_ARGUMENT; return nullptr; } if (operand.location.length <= ANEURALNETWORKS_MAX_SIZE_OF_IMMEDIATELY_COPIED_VALUES) { resultModel.setOperandValue( i, model.operandValues.data() + operand.location.offset, operand.location.length); } else { // If length is larger than 128 bytes, we are responsible for making sure // that value outlives the model. If this case exists, then we created // an internal copy, that is used here: resultModel.setOperandValue( i, copiedOperandValues.data() + operand.location.offset, operand.location.length); } break; } case OperandLifeTime::CONSTANT_POOL: { if (operand.location.poolIndex >= memoryPools.size()) { *errorStatus = ErrorStatus::INVALID_ARGUMENT; return nullptr; } resultModel.setOperandValueFromMemory( i, memoryPools[operand.location.poolIndex].get(), operand.location.offset, operand.location.length); break; } case OperandLifeTime::SUBGRAPH: { ErrorStatus otherErrorStatus = ErrorStatus::NONE; auto subgraph = convertSubgraphFromHAL(nnapi, memoryPools, model, allModels, operand.location.offset + 1, copiedOperandValues, &otherErrorStatus); if (subgraph) { resultModel.setOperandValueFromModel(i, subgraph); } else { LOG(ERROR) << "Failed to set subgraph operand value"; *errorStatus = otherErrorStatus; return nullptr; } break; } case OperandLifeTime::NO_VALUE: { resultModel.setOperandValue(i, nullptr, 0); break; } case OperandLifeTime::TEMPORARY_VARIABLE: case OperandLifeTime::SUBGRAPH_OUTPUT: case OperandLifeTime::SUBGRAPH_INPUT: { break; } default: LOG(ERROR) << "Invalid operand type: " << static_cast(operand.lifetime); *errorStatus = ErrorStatus::INVALID_ARGUMENT; return nullptr; } if (!resultModel.isValid()) { LOG(ERROR) << "Failed to add operand with index " << i; *errorStatus = ErrorStatus::INVALID_ARGUMENT; return nullptr; } } for (int i = 0; i < subgraph.operations.size(); ++i) { const auto& operation = subgraph.operations[i]; std::vector inputs(operation.inputs.begin(), operation.inputs.end()); std::vector outputs(operation.outputs.begin(), operation.outputs.end()); int operationType = static_cast(operation.type); if (::android::nn::isExtension(static_cast<::android::nn::OperationType>(operationType))) { uint16_t extensionPrefix = ::android::nn::getExtensionPrefix(static_cast(operationType)); uint16_t typeWithinExtension = ::android::nn::getTypeWithinExtension(static_cast(operationType)); auto* extensionName = getExtensionName(extensionPrefix); if (extensionName == nullptr) { LOG(ERROR) << "Unknown extension prefix " << extensionPrefix; *errorStatus = ErrorStatus::INVALID_ARGUMENT; return nullptr; } resultModel.getExtensionOperationType(*extensionName, typeWithinExtension, &operationType); if (!resultModel.isValid()) { LOG(ERROR) << "Failed to get extension operation with index " << i; *errorStatus = ErrorStatus::INVALID_ARGUMENT; return nullptr; } } resultModel.addOperation(operationType, inputs, outputs); if (!resultModel.isValid()) { LOG(ERROR) << "Failed to add operation with index " << i; *errorStatus = ErrorStatus::INVALID_ARGUMENT; return nullptr; } } std::vector inputIndexes(subgraph.inputIndexes.begin(), subgraph.inputIndexes.end()); std::vector outputIndexes(subgraph.outputIndexes.begin(), subgraph.outputIndexes.end()); resultModel.identifyInputsAndOutputs(inputIndexes, outputIndexes); if (!resultModel.isValid()) { LOG(ERROR) << "Model identifyInputsAndOutputs failed"; *errorStatus = ErrorStatus::INVALID_ARGUMENT; return nullptr; } if (resultModel.finish() != Result::NO_ERROR) { LOG(ERROR) << "Model finish failed"; *errorStatus = ErrorStatus::INVALID_ARGUMENT; return nullptr; } if (!resultModel.isValid()) { LOG(ERROR) << "Invalid model"; *errorStatus = ErrorStatus::INVALID_ARGUMENT; return nullptr; } (*allModels)[subgraphIndex] = std::move(resultModel); return (*allModels)[subgraphIndex]->getHandle(); } // This is needed for CONSTANT_COPY operands > 128 bytes, we have to // store them in intenal buffer bool needsCopiedOperandValues(const neuralnetworks::Model& model) { for (int sindex = 0; sindex < model.referenced.size() + 1; ++sindex) { const auto& subgraph = sindex == 0 ? model.main : model.referenced[sindex - 1]; for (int i = 0; i < subgraph.operands.size(); ++i) { const auto& operand = subgraph.operands[i]; if (operand.lifetime == OperandLifeTime::CONSTANT_COPY) { if (operand.location.length > ANEURALNETWORKS_MAX_SIZE_OF_IMMEDIATELY_COPIED_VALUES) { return true; } } } } return false; } bool isValid(const Subgraph& subgraph) { // Either the operand has a known value before model execution begins, or we've seen a writer // for this operand while walking operands in execution order. Initialize to known operands. std::vector operandValueKnown; operandValueKnown.reserve(subgraph.operands.size()); std::transform(subgraph.operands.begin(), subgraph.operands.end(), std::back_inserter(operandValueKnown), [](const Operand& operand) { return operand.lifetime != OperandLifeTime::TEMPORARY_VARIABLE && operand.lifetime != OperandLifeTime::SUBGRAPH_OUTPUT; }); // Validate that operations are sorted into execution order. // // If there is a cycle in the graph, the operations will not // appear to be sorted into execution order: Some operation will // have an input for which operandValueKnown[] is false. for (size_t i = 0; i < subgraph.operations.size(); ++i) { const auto& operation = subgraph.operations[i]; for (size_t j = 0; j < operation.inputs.size(); ++j) { const uint32_t k = operation.inputs[j]; if (!operandValueKnown[k]) { LOG(ERROR) << "Operation " << i << " input " << j << " (operand " << k << ") is read before it is written"; return false; } } for (size_t j = 0; j < operation.outputs.size(); ++j) { const uint32_t k = operation.outputs[j]; // Assuming validateOperations() has not returned an error, we know that this output is // TEMPORARY_VARIABLE or MODEL_OUTPUT, and so the only way operandValueKnown[k] can be // true is if we've already seen a writer for this operand. if (operandValueKnown[k]) { LOG(ERROR) << "Operation " << i << " output " << j << " (operand " << k << ") has already been written"; return false; } operandValueKnown[k] = true; } } // Verify all operands are written. for (size_t i = 0; i < subgraph.operands.size(); ++i) { if (!operandValueKnown[i]) { LOG(ERROR) << "Operand " << i << " is never written"; return false; } const auto& operand = subgraph.operands[i]; if (operand.lifetime == OperandLifeTime::SUBGRAPH_OUTPUT) { if (std::find(subgraph.outputIndexes.begin(), subgraph.outputIndexes.end(), i) == subgraph.outputIndexes.end()) { LOG(ERROR) << "Op with output liftime, but not on output list: " << i; return false; } } } // Validate input and output lifetime for (auto index : subgraph.inputIndexes) { if (subgraph.operands[index].lifetime != OperandLifeTime::SUBGRAPH_INPUT) { LOG(ERROR) << "Input with index" << index << " has invalid lifetime"; return false; } } for (auto index : subgraph.outputIndexes) { if (subgraph.operands[index].lifetime != OperandLifeTime::SUBGRAPH_OUTPUT) { LOG(ERROR) << "Output with index" << index << " has invalid lifetime"; return false; } } // TODO(b/77871786): verify that every operation has at least one output operand that is read? return true; } } // namespace bool isValid(const neuralnetworks::Model& model) { return (isValid(model.main) && std::all_of(model.referenced.begin(), model.referenced.end(), [](const Subgraph& subgraph) { return isValid(subgraph); })); } std::optional convertFromHAL(const NnApiSupportLibrary* nnapi, const neuralnetworks::Model& model, std::vector* copiedOperandValues, ErrorStatus* errorStatus) { CHECK(copiedOperandValues != nullptr); *errorStatus = ErrorStatus::NONE; // Using this pulls in OperationResolver and huge chunk of dependencies. // TODO(172925288): Replace as followup work // if (!::aidl::android::hardware::neuralnetworks::utils::valid(model)) { if (!isValid(model)) { LOG(ERROR) << "Invalid HAL model, failed to convert into SL model"; *errorStatus = ErrorStatus::INVALID_ARGUMENT; return std::nullopt; } std::vector> memoryPools; memoryPools.reserve(model.pools.size()); for (const auto& pool : model.pools) { std::unique_ptr<::android::nn::sl_wrapper::Memory> memory = convertFromHAL(nnapi, pool); if (!memory) { LOG(ERROR) << "Failed to convert HAL memory into SL memory"; *errorStatus = ErrorStatus::INVALID_ARGUMENT; return std::nullopt; } memoryPools.push_back(std::move(memory)); } std::vector> allModels(model.referenced.size() + 1); if (needsCopiedOperandValues(model)) { *copiedOperandValues = model.operandValues; } for (size_t i = 0; i < allModels.size(); ++i) { if (convertSubgraphFromHAL(nnapi, memoryPools, model, &allModels, i, *copiedOperandValues, errorStatus) == nullptr) { LOG(ERROR) << "Failed to convert HAL subgraphs into SL subgraphs, index: " << i; // Error status already set by convertSubgraphFromHAL return std::nullopt; } } std::vector<::android::nn::sl_wrapper::Model> result; result.reserve(allModels.size()); for (size_t i = 0; i < allModels.size(); ++i) { if (!allModels[i].has_value()) { LOG(ERROR) << "Missing SL subgraph"; *errorStatus = ErrorStatus::INVALID_ARGUMENT; return std::nullopt; } result.push_back(std::move(*allModels[i])); } return ShimConvertedModel{.memory = std::move(memoryPools), .models = std::move(result)}; } std::unique_ptr<::android::nn::sl_wrapper::Memory> convertFromHAL( const NnApiSupportLibrary* nnapi, const neuralnetworks::Memory& pool) { using Tag = neuralnetworks::Memory::Tag; switch (pool.getTag()) { case Tag::ashmem: { const auto& ashmem = pool.get(); size_t size = ashmem.size; int fd = ashmem.fd.get(); auto memory = std::make_unique<::android::nn::sl_wrapper::Memory>( nnapi, size, PROT_READ | PROT_WRITE, fd, 0, /*ownsFd=*/false); if (!memory->isValid()) { return nullptr; } return memory; } case Tag::mappableFile: { const auto& mappableFile = pool.get(); size_t size = mappableFile.length; int fd = mappableFile.fd.get(); int prot = mappableFile.prot & (PROT_READ | PROT_WRITE); size_t offset = mappableFile.offset; auto memory = std::make_unique<::android::nn::sl_wrapper::Memory>( nnapi, size, prot, fd, offset, /*ownsFd=*/false); if (!memory->isValid()) { return nullptr; } return memory; } case Tag::hardwareBuffer: { const auto& hardwareBuffer = pool.get(); native_handle_t* handle = ::android::dupFromAidl(hardwareBuffer.handle); if (handle == nullptr) { LOG(ERROR) << "Dup of the hardware_buffer_blob memory pool failed"; return nullptr; } const auto handleGuard = ::android::base::make_scope_guard([handle] { native_handle_close(handle); native_handle_delete(handle); }); for (size_t i = 0; i < handle->numFds; ++i) { if (handle->data[i] == -1) { LOG(ERROR) << "Dup of the hardware_buffer_blob memory pool failed"; return nullptr; } } const AHardwareBuffer_Desc desc{ .width = static_cast(hardwareBuffer.description.width), .height = static_cast(hardwareBuffer.description.height), .layers = static_cast(hardwareBuffer.description.layers), .format = static_cast(hardwareBuffer.description.format), .usage = static_cast(hardwareBuffer.description.usage), .stride = static_cast(hardwareBuffer.description.stride), }; AHardwareBuffer* ahwb = nullptr; const ::android::status_t status = AHardwareBuffer_createFromHandle( &desc, handle, AHARDWAREBUFFER_CREATE_FROM_HANDLE_METHOD_CLONE, &ahwb); if (status != ::android::NO_ERROR) { LOG(ERROR) << "createFromHandle failed"; return nullptr; } const bool isBlob = desc.format == AHARDWAREBUFFER_FORMAT_BLOB; const size_t size = isBlob ? desc.width : 0; // Takes ownership of hardwareBuffer, handle gets closed auto memory = std::make_unique<::android::nn::sl_wrapper::Memory>(nnapi, ahwb, /*ownAHB=*/true, size); if (!memory->isValid()) { return nullptr; } return memory; } } LOG(ERROR) << "Can't convert to SL Memory, unknown pool tag: " << pool.getTag(); return nullptr; } } // namespace aidl::android::hardware::neuralnetworks