platform_hardware_interfaces/neuralnetworks/1.2/vts/functional/ValidateBurst.cpp
Michael Butler 68a6de7abe NNAPI VTS: Add validation for Priority
This CL also cleans up the validation framework code.

Bug: 67828197
Test: mma
Test: VtsHalNeuralnetworksV1_*TargetTest
Change-Id: I84661fb2b8204148788d10425ca0ac986158b15f
Merged-In: I84661fb2b8204148788d10425ca0ac986158b15f
(cherry picked from commit da1a692880)
2020-03-16 11:05:59 +00:00

406 lines
18 KiB
C++

/*
* Copyright (C) 2019 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_hidl_hal_test"
#include "VtsHalNeuralnetworks.h"
#include "1.2/Callbacks.h"
#include "ExecutionBurstController.h"
#include "ExecutionBurstServer.h"
#include "GeneratedTestHarness.h"
#include "TestHarness.h"
#include <android-base/logging.h>
#include <chrono>
#include <cstring>
namespace android::hardware::neuralnetworks::V1_2::vts::functional {
using nn::ExecutionBurstController;
using nn::RequestChannelSender;
using nn::ResultChannelReceiver;
using V1_0::ErrorStatus;
using V1_0::Request;
using ExecutionBurstCallback = ExecutionBurstController::ExecutionBurstCallback;
using BurstExecutionMutation = std::function<void(std::vector<FmqRequestDatum>*)>;
// This constant value represents the length of an FMQ that is large enough to
// return a result from a burst execution for all of the generated test cases.
constexpr size_t kExecutionBurstChannelLength = 1024;
// This constant value represents a length of an FMQ that is not large enough
// to return a result from a burst execution for some of the generated test
// cases.
constexpr size_t kExecutionBurstChannelSmallLength = 8;
///////////////////////// UTILITY FUNCTIONS /////////////////////////
static bool badTiming(Timing timing) {
return timing.timeOnDevice == UINT64_MAX && timing.timeInDriver == UINT64_MAX;
}
static void createBurst(const sp<IPreparedModel>& preparedModel, const sp<IBurstCallback>& callback,
std::unique_ptr<RequestChannelSender>* sender,
std::unique_ptr<ResultChannelReceiver>* receiver,
sp<IBurstContext>* context,
size_t resultChannelLength = kExecutionBurstChannelLength) {
ASSERT_NE(nullptr, preparedModel.get());
ASSERT_NE(nullptr, sender);
ASSERT_NE(nullptr, receiver);
ASSERT_NE(nullptr, context);
// create FMQ objects
auto [fmqRequestChannel, fmqRequestDescriptor] =
RequestChannelSender::create(kExecutionBurstChannelLength);
auto [fmqResultChannel, fmqResultDescriptor] =
ResultChannelReceiver::create(resultChannelLength, std::chrono::microseconds{0});
ASSERT_NE(nullptr, fmqRequestChannel.get());
ASSERT_NE(nullptr, fmqResultChannel.get());
ASSERT_NE(nullptr, fmqRequestDescriptor);
ASSERT_NE(nullptr, fmqResultDescriptor);
// configure burst
ErrorStatus errorStatus;
sp<IBurstContext> burstContext;
const Return<void> ret = preparedModel->configureExecutionBurst(
callback, *fmqRequestDescriptor, *fmqResultDescriptor,
[&errorStatus, &burstContext](ErrorStatus status, const sp<IBurstContext>& context) {
errorStatus = status;
burstContext = context;
});
ASSERT_TRUE(ret.isOk());
ASSERT_EQ(ErrorStatus::NONE, errorStatus);
ASSERT_NE(nullptr, burstContext.get());
// return values
*sender = std::move(fmqRequestChannel);
*receiver = std::move(fmqResultChannel);
*context = burstContext;
}
static void createBurstWithResultChannelLength(
const sp<IPreparedModel>& preparedModel, size_t resultChannelLength,
std::shared_ptr<ExecutionBurstController>* controller) {
ASSERT_NE(nullptr, preparedModel.get());
ASSERT_NE(nullptr, controller);
// create FMQ objects
std::unique_ptr<RequestChannelSender> sender;
std::unique_ptr<ResultChannelReceiver> receiver;
sp<ExecutionBurstCallback> callback = new ExecutionBurstCallback();
sp<IBurstContext> context;
ASSERT_NO_FATAL_FAILURE(createBurst(preparedModel, callback, &sender, &receiver, &context,
resultChannelLength));
ASSERT_NE(nullptr, sender.get());
ASSERT_NE(nullptr, receiver.get());
ASSERT_NE(nullptr, context.get());
// return values
*controller = std::make_shared<ExecutionBurstController>(std::move(sender), std::move(receiver),
context, callback);
}
// Primary validation function. This function will take a valid serialized
// request, apply a mutation to it to invalidate the serialized request, then
// pass it to interface calls that use the serialized request.
static void validate(RequestChannelSender* sender, ResultChannelReceiver* receiver,
const std::string& message,
const std::vector<FmqRequestDatum>& originalSerialized,
const BurstExecutionMutation& mutate) {
std::vector<FmqRequestDatum> serialized = originalSerialized;
mutate(&serialized);
// skip if packet is too large to send
if (serialized.size() > kExecutionBurstChannelLength) {
return;
}
SCOPED_TRACE(message);
// send invalid packet
ASSERT_TRUE(sender->sendPacket(serialized));
// receive error
auto results = receiver->getBlocking();
ASSERT_TRUE(results.has_value());
const auto [status, outputShapes, timing] = std::move(*results);
EXPECT_NE(ErrorStatus::NONE, status);
EXPECT_EQ(0u, outputShapes.size());
EXPECT_TRUE(badTiming(timing));
}
// For validation, valid packet entries are mutated to invalid packet entries,
// or invalid packet entries are inserted into valid packets. This function
// creates pre-set invalid packet entries for convenience.
static std::vector<FmqRequestDatum> createBadRequestPacketEntries() {
const FmqRequestDatum::PacketInformation packetInformation = {
/*.packetSize=*/10, /*.numberOfInputOperands=*/10, /*.numberOfOutputOperands=*/10,
/*.numberOfPools=*/10};
const FmqRequestDatum::OperandInformation operandInformation = {
/*.hasNoValue=*/false, /*.location=*/{}, /*.numberOfDimensions=*/10};
const int32_t invalidPoolIdentifier = std::numeric_limits<int32_t>::max();
std::vector<FmqRequestDatum> bad(7);
bad[0].packetInformation(packetInformation);
bad[1].inputOperandInformation(operandInformation);
bad[2].inputOperandDimensionValue(0);
bad[3].outputOperandInformation(operandInformation);
bad[4].outputOperandDimensionValue(0);
bad[5].poolIdentifier(invalidPoolIdentifier);
bad[6].measureTiming(MeasureTiming::YES);
return bad;
}
// For validation, valid packet entries are mutated to invalid packet entries,
// or invalid packet entries are inserted into valid packets. This function
// retrieves pre-set invalid packet entries for convenience. This function
// caches these data so they can be reused on subsequent validation checks.
static const std::vector<FmqRequestDatum>& getBadRequestPacketEntries() {
static const std::vector<FmqRequestDatum> bad = createBadRequestPacketEntries();
return bad;
}
///////////////////////// REMOVE DATUM ////////////////////////////////////
static void removeDatumTest(RequestChannelSender* sender, ResultChannelReceiver* receiver,
const std::vector<FmqRequestDatum>& serialized) {
for (size_t index = 0; index < serialized.size(); ++index) {
const std::string message = "removeDatum: removed datum at index " + std::to_string(index);
validate(sender, receiver, message, serialized,
[index](std::vector<FmqRequestDatum>* serialized) {
serialized->erase(serialized->begin() + index);
});
}
}
///////////////////////// ADD DATUM ////////////////////////////////////
static void addDatumTest(RequestChannelSender* sender, ResultChannelReceiver* receiver,
const std::vector<FmqRequestDatum>& serialized) {
const std::vector<FmqRequestDatum>& extra = getBadRequestPacketEntries();
for (size_t index = 0; index <= serialized.size(); ++index) {
for (size_t type = 0; type < extra.size(); ++type) {
const std::string message = "addDatum: added datum type " + std::to_string(type) +
" at index " + std::to_string(index);
validate(sender, receiver, message, serialized,
[index, type, &extra](std::vector<FmqRequestDatum>* serialized) {
serialized->insert(serialized->begin() + index, extra[type]);
});
}
}
}
///////////////////////// MUTATE DATUM ////////////////////////////////////
static bool interestingCase(const FmqRequestDatum& lhs, const FmqRequestDatum& rhs) {
using Discriminator = FmqRequestDatum::hidl_discriminator;
const bool differentValues = (lhs != rhs);
const bool sameDiscriminator = (lhs.getDiscriminator() == rhs.getDiscriminator());
const auto discriminator = rhs.getDiscriminator();
const bool isDimensionValue = (discriminator == Discriminator::inputOperandDimensionValue ||
discriminator == Discriminator::outputOperandDimensionValue);
return differentValues && !(sameDiscriminator && isDimensionValue);
}
static void mutateDatumTest(RequestChannelSender* sender, ResultChannelReceiver* receiver,
const std::vector<FmqRequestDatum>& serialized) {
const std::vector<FmqRequestDatum>& change = getBadRequestPacketEntries();
for (size_t index = 0; index < serialized.size(); ++index) {
for (size_t type = 0; type < change.size(); ++type) {
if (interestingCase(serialized[index], change[type])) {
const std::string message = "mutateDatum: changed datum at index " +
std::to_string(index) + " to datum type " +
std::to_string(type);
validate(sender, receiver, message, serialized,
[index, type, &change](std::vector<FmqRequestDatum>* serialized) {
(*serialized)[index] = change[type];
});
}
}
}
}
///////////////////////// BURST VALIATION TESTS ////////////////////////////////////
static void validateBurstSerialization(const sp<IPreparedModel>& preparedModel,
const Request& request) {
// create burst
std::unique_ptr<RequestChannelSender> sender;
std::unique_ptr<ResultChannelReceiver> receiver;
sp<ExecutionBurstCallback> callback = new ExecutionBurstCallback();
sp<IBurstContext> context;
ASSERT_NO_FATAL_FAILURE(createBurst(preparedModel, callback, &sender, &receiver, &context));
ASSERT_NE(nullptr, sender.get());
ASSERT_NE(nullptr, receiver.get());
ASSERT_NE(nullptr, context.get());
// load memory into callback slots
std::vector<intptr_t> keys;
keys.reserve(request.pools.size());
std::transform(request.pools.begin(), request.pools.end(), std::back_inserter(keys),
[](const auto& pool) { return reinterpret_cast<intptr_t>(&pool); });
const std::vector<int32_t> slots = callback->getSlots(request.pools, keys);
// ensure slot std::numeric_limits<int32_t>::max() doesn't exist (for
// subsequent slot validation testing)
ASSERT_TRUE(std::all_of(slots.begin(), slots.end(), [](int32_t slot) {
return slot != std::numeric_limits<int32_t>::max();
}));
// serialize the request
const auto serialized = android::nn::serialize(request, MeasureTiming::YES, slots);
// validations
removeDatumTest(sender.get(), receiver.get(), serialized);
addDatumTest(sender.get(), receiver.get(), serialized);
mutateDatumTest(sender.get(), receiver.get(), serialized);
}
// This test validates that when the Result message size exceeds length of the
// result FMQ, the service instance gracefully fails and returns an error.
static void validateBurstFmqLength(const sp<IPreparedModel>& preparedModel,
const Request& request) {
// create regular burst
std::shared_ptr<ExecutionBurstController> controllerRegular;
ASSERT_NO_FATAL_FAILURE(createBurstWithResultChannelLength(
preparedModel, kExecutionBurstChannelLength, &controllerRegular));
ASSERT_NE(nullptr, controllerRegular.get());
// create burst with small output channel
std::shared_ptr<ExecutionBurstController> controllerSmall;
ASSERT_NO_FATAL_FAILURE(createBurstWithResultChannelLength(
preparedModel, kExecutionBurstChannelSmallLength, &controllerSmall));
ASSERT_NE(nullptr, controllerSmall.get());
// load memory into callback slots
std::vector<intptr_t> keys(request.pools.size());
for (size_t i = 0; i < keys.size(); ++i) {
keys[i] = reinterpret_cast<intptr_t>(&request.pools[i]);
}
// collect serialized result by running regular burst
const auto [nRegular, outputShapesRegular, timingRegular, fallbackRegular] =
controllerRegular->compute(request, MeasureTiming::NO, keys);
const ErrorStatus statusRegular = nn::legacyConvertResultCodeToErrorStatus(nRegular);
EXPECT_FALSE(fallbackRegular);
// skip test if regular burst output isn't useful for testing a failure
// caused by having too small of a length for the result FMQ
const std::vector<FmqResultDatum> serialized =
android::nn::serialize(statusRegular, outputShapesRegular, timingRegular);
if (statusRegular != ErrorStatus::NONE ||
serialized.size() <= kExecutionBurstChannelSmallLength) {
return;
}
// by this point, execution should fail because the result channel isn't
// large enough to return the serialized result
const auto [nSmall, outputShapesSmall, timingSmall, fallbackSmall] =
controllerSmall->compute(request, MeasureTiming::NO, keys);
const ErrorStatus statusSmall = nn::legacyConvertResultCodeToErrorStatus(nSmall);
EXPECT_NE(ErrorStatus::NONE, statusSmall);
EXPECT_EQ(0u, outputShapesSmall.size());
EXPECT_TRUE(badTiming(timingSmall));
EXPECT_FALSE(fallbackSmall);
}
static bool isSanitized(const FmqResultDatum& datum) {
using Discriminator = FmqResultDatum::hidl_discriminator;
// check to ensure the padding values in the returned
// FmqResultDatum::OperandInformation are initialized to 0
if (datum.getDiscriminator() == Discriminator::operandInformation) {
static_assert(
offsetof(FmqResultDatum::OperandInformation, isSufficient) == 0,
"unexpected value for offset of FmqResultDatum::OperandInformation::isSufficient");
static_assert(
sizeof(FmqResultDatum::OperandInformation::isSufficient) == 1,
"unexpected value for size of FmqResultDatum::OperandInformation::isSufficient");
static_assert(offsetof(FmqResultDatum::OperandInformation, numberOfDimensions) == 4,
"unexpected value for offset of "
"FmqResultDatum::OperandInformation::numberOfDimensions");
static_assert(sizeof(FmqResultDatum::OperandInformation::numberOfDimensions) == 4,
"unexpected value for size of "
"FmqResultDatum::OperandInformation::numberOfDimensions");
static_assert(sizeof(FmqResultDatum::OperandInformation) == 8,
"unexpected value for size of "
"FmqResultDatum::OperandInformation");
constexpr size_t paddingOffset =
offsetof(FmqResultDatum::OperandInformation, isSufficient) +
sizeof(FmqResultDatum::OperandInformation::isSufficient);
constexpr size_t paddingSize =
offsetof(FmqResultDatum::OperandInformation, numberOfDimensions) - paddingOffset;
FmqResultDatum::OperandInformation initialized{};
std::memset(&initialized, 0, sizeof(initialized));
const char* initializedPaddingStart =
reinterpret_cast<const char*>(&initialized) + paddingOffset;
const char* datumPaddingStart =
reinterpret_cast<const char*>(&datum.operandInformation()) + paddingOffset;
return std::memcmp(datumPaddingStart, initializedPaddingStart, paddingSize) == 0;
}
// there are no other padding initialization checks required, so return true
// for any sum-type that isn't FmqResultDatum::OperandInformation
return true;
}
static void validateBurstSanitized(const sp<IPreparedModel>& preparedModel,
const Request& request) {
// create burst
std::unique_ptr<RequestChannelSender> sender;
std::unique_ptr<ResultChannelReceiver> receiver;
sp<ExecutionBurstCallback> callback = new ExecutionBurstCallback();
sp<IBurstContext> context;
ASSERT_NO_FATAL_FAILURE(createBurst(preparedModel, callback, &sender, &receiver, &context));
ASSERT_NE(nullptr, sender.get());
ASSERT_NE(nullptr, receiver.get());
ASSERT_NE(nullptr, context.get());
// load memory into callback slots
std::vector<intptr_t> keys;
keys.reserve(request.pools.size());
std::transform(request.pools.begin(), request.pools.end(), std::back_inserter(keys),
[](const auto& pool) { return reinterpret_cast<intptr_t>(&pool); });
const std::vector<int32_t> slots = callback->getSlots(request.pools, keys);
// send valid request
ASSERT_TRUE(sender->send(request, MeasureTiming::YES, slots));
// receive valid result
auto serialized = receiver->getPacketBlocking();
ASSERT_TRUE(serialized.has_value());
// sanitize result
ASSERT_TRUE(std::all_of(serialized->begin(), serialized->end(), isSanitized))
<< "The result serialized data is not properly sanitized";
}
///////////////////////////// ENTRY POINT //////////////////////////////////
void validateBurst(const sp<IPreparedModel>& preparedModel, const Request& request) {
ASSERT_NO_FATAL_FAILURE(validateBurstSerialization(preparedModel, request));
ASSERT_NO_FATAL_FAILURE(validateBurstFmqLength(preparedModel, request));
ASSERT_NO_FATAL_FAILURE(validateBurstSanitized(preparedModel, request));
}
} // namespace android::hardware::neuralnetworks::V1_2::vts::functional