Home | History | Annotate | Download | only in fuzzing
      1 /*
      2  * Copyright (C) 2019 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 #include "fuzzing/RandomGraphGenerator.h"
     18 
     19 #include <gtest/gtest.h>
     20 
     21 #include <cmath>
     22 #include <string>
     23 #include <unordered_map>
     24 #include <vector>
     25 
     26 #include "TestNeuralNetworksWrapper.h"
     27 #include "fuzzing/OperationManager.h"
     28 #include "fuzzing/RandomGraphGeneratorUtils.h"
     29 #include "fuzzing/RandomVariable.h"
     30 
     31 namespace android {
     32 namespace nn {
     33 namespace fuzzing_test {
     34 
     35 using test_wrapper::Result;
     36 using test_wrapper::Type;
     37 
     38 // Construct a RandomOperand from OperandSignature.
     39 RandomOperand::RandomOperand(const OperandSignature& operand, Type dataType, uint32_t rank)
     40     : type(operand.type), finalizer(operand.finalizer) {
     41     NN_FUZZER_LOG << "Operand: " << toString(type);
     42     if (operand.constructor) operand.constructor(dataType, rank, this);
     43 }
     44 
     45 std::vector<uint32_t> RandomOperand::getDimensions() const {
     46     std::vector<uint32_t> result(dimensions.size(), 0);
     47     for (uint32_t i = 0; i < dimensions.size(); i++) result[i] = dimensions[i].getValue();
     48     return result;
     49 }
     50 
     51 // Check if an edge between [this, other] is valid. If yes, add the edge.
     52 bool RandomOperand::createEdgeIfValid(const RandomOperand& other) const {
     53     if (other.type != RandomOperandType::INPUT) return false;
     54     if (dataType != other.dataType || dimensions.size() != other.dimensions.size() ||
     55         scale != other.scale || zeroPoint != other.zeroPoint || doNotConnect || other.doNotConnect)
     56         return false;
     57     return RandomVariableNetwork::get()->setEqualIfCompatible(dimensions, other.dimensions);
     58 }
     59 
     60 uint32_t RandomOperand::getNumberOfElements() const {
     61     uint32_t num = 1;
     62     for (const auto& d : dimensions) num *= d.getValue();
     63     return num;
     64 }
     65 
     66 size_t RandomOperand::getBufferSize() const {
     67     return kSizeOfDataType[static_cast<int32_t>(dataType)] * getNumberOfElements();
     68 }
     69 
     70 // Construct a RandomOperation from OperationSignature.
     71 RandomOperation::RandomOperation(const OperationSignature& operation)
     72     : opType(operation.opType), finalizer(operation.finalizer) {
     73     NN_FUZZER_LOG << "Operation: " << kOperationNames[static_cast<int32_t>(opType)];
     74 
     75     // Determine the data type and rank of the operation and invoke the constructor.
     76     Type dataType = getRandomChoice(operation.supportedDataTypes);
     77     uint32_t rank = getRandomChoice(operation.supportedRanks);
     78 
     79     // Initialize operands and operation.
     80     for (const auto& op : operation.inputs) {
     81         inputs.emplace_back(new RandomOperand(op, dataType, rank));
     82     }
     83     for (const auto& op : operation.outputs) {
     84         outputs.emplace_back(new RandomOperand(op, dataType, rank));
     85     }
     86     if (operation.constructor) operation.constructor(dataType, rank, this);
     87 
     88     // Add constraints on the number of elements.
     89     if (RandomVariable::defaultValue > 10) {
     90         for (auto in : inputs) RandomVariableNetwork::get()->addDimensionProd(in->dimensions);
     91         for (auto out : outputs) RandomVariableNetwork::get()->addDimensionProd(out->dimensions);
     92     }
     93     // The output operands should have dimensions larger than 0.
     94     for (auto out : outputs) {
     95         for (auto& dim : out->dimensions) dim.setRange(1, kInvalidValue);
     96     }
     97 }
     98 
     99 bool RandomGraph::generate(uint32_t seed, uint32_t numOperations, uint32_t dimensionRange) {
    100     RandomNumberGenerator::generator.seed(seed);
    101     // The generator may (with low probability) end up with an invalid graph.
    102     // If so, regenerate the graph until a valid one is produced.
    103     while (true) {
    104         RandomVariableNetwork::get()->initialize(dimensionRange);
    105         mOperations.clear();
    106         mOperands.clear();
    107         if (generateGraph(numOperations) && generateValue()) break;
    108         std::cout << "[ Retry    ]   The RandomGraphGenerator produces an invalid graph.\n";
    109     }
    110     return true;
    111 }
    112 
    113 bool RandomGraph::generateGraph(uint32_t numOperations) {
    114     NN_FUZZER_LOG << "Generate Graph";
    115     // Randomly generate a vector of operations, this is a valid topological sort.
    116     for (uint32_t i = 0; i < numOperations; i++) {
    117         mOperations.emplace_back(OperationManager::get()->getRandomOperation());
    118     }
    119 
    120     // Randomly add edges from the output of one operation to the input of another operation
    121     // with larger positional index.
    122     for (uint32_t i = 0; i < numOperations; i++) {
    123         for (auto& output : mOperations[i].outputs) {
    124             for (uint32_t j = i + 1; j < numOperations; j++) {
    125                 for (auto& input : mOperations[j].inputs) {
    126                     // For each [output, input] pair, add an edge with probability prob.
    127                     // TODO: Find a better formula by first defining what "better" is.
    128                     float prob = 0.1f;
    129                     if (getBernoulli(prob)) {
    130                         if (output->createEdgeIfValid(*input)) {
    131                             NN_FUZZER_LOG << "Add edge: operation " << i << " -> " << j;
    132                             input = output;
    133                             output->type = RandomOperandType::INTERNAL;
    134                         }
    135                     }
    136                 }
    137             }
    138         }
    139     }
    140     return true;
    141 }
    142 
    143 static bool asConstant(const std::shared_ptr<RandomOperand>& operand, float prob = 0.5f) {
    144     if (operand->type == RandomOperandType::CONST) return true;
    145     if (operand->type != RandomOperandType::INPUT) return false;
    146     // Force all scalars to be CONST.
    147     if (kScalarDataType[static_cast<int32_t>(operand->dataType)]) return true;
    148     if (getBernoulli(prob)) return true;
    149     return false;
    150 }
    151 
    152 // Freeze the dimensions to a random but valid combination.
    153 // Generate random buffer values for model inputs and constants.
    154 bool RandomGraph::generateValue() {
    155     NN_FUZZER_LOG << "Generate Value";
    156     if (!RandomVariableNetwork::get()->freeze()) return false;
    157 
    158     // Fill all unique operands into mOperands.
    159     std::set<std::shared_ptr<RandomOperand>> seen;
    160     auto fillOperands = [&seen, this](const std::vector<std::shared_ptr<RandomOperand>>& ops) {
    161         for (const auto& op : ops) {
    162             if (seen.find(op) == seen.end()) {
    163                 seen.insert(op);
    164                 mOperands.push_back(op);
    165             }
    166         }
    167     };
    168     for (const auto& operation : mOperations) {
    169         fillOperands(operation.inputs);
    170         fillOperands(operation.outputs);
    171     }
    172 
    173     // Count the number of INPUTs.
    174     uint32_t numInputs = 0;
    175     for (auto& operand : mOperands) {
    176         if (operand->type == RandomOperandType::INPUT) numInputs++;
    177     }
    178 
    179     for (auto& operand : mOperands) {
    180         // Turn INPUT into CONST with probability prob. Need to keep at least one INPUT.
    181         float prob = 0.5f;
    182         if (asConstant(operand, prob) && numInputs > 1) {
    183             if (operand->type == RandomOperandType::INPUT) numInputs--;
    184             operand->type = RandomOperandType::CONST;
    185         }
    186         if (operand->type != RandomOperandType::INTERNAL) {
    187             if (operand->buffer.empty()) operand->resizeBuffer<uint8_t>(operand->getBufferSize());
    188             // If operand is set by randomBuffer, copy the frozen values into buffer.
    189             if (!operand->randomBuffer.empty()) {
    190                 int32_t* data = reinterpret_cast<int32_t*>(operand->buffer.data());
    191                 for (uint32_t i = 0; i < operand->randomBuffer.size(); i++) {
    192                     data[i] = operand->randomBuffer[i].getValue();
    193                 }
    194             }
    195             if (operand->finalizer) operand->finalizer(operand.get());
    196         }
    197     }
    198 
    199     for (auto& operation : mOperations) {
    200         if (operation.finalizer) operation.finalizer(&operation);
    201     }
    202     return true;
    203 }
    204 
    205 void RandomGraph::createModel(test_wrapper::Model* model) {
    206     NN_FUZZER_LOG << "Create Model";
    207 
    208     // Set model operands.
    209     std::vector<uint32_t> modelInputs;
    210     std::vector<uint32_t> modelOutputs;
    211     for (auto& operand : mOperands) {
    212         // TODO: Model operands are always fully-specified at model construction time.
    213         test_wrapper::OperandType type(operand->dataType, operand->getDimensions(), operand->scale,
    214                                        operand->zeroPoint);
    215         operand->opIndex = model->addOperand(&type);
    216 
    217         // For INPUT/OUTPUT, prepare vectors for identifyInputsAndOutputs(...).
    218         // For CONST, set operand buffer.
    219         if (operand->type == RandomOperandType::INPUT) {
    220             operand->ioIndex = modelInputs.size();
    221             modelInputs.push_back(operand->opIndex);
    222         } else if (operand->type == RandomOperandType::OUTPUT) {
    223             operand->ioIndex = modelOutputs.size();
    224             modelOutputs.push_back(operand->opIndex);
    225         } else if (operand->type == RandomOperandType::CONST) {
    226             model->setOperandValue(operand->opIndex, operand->buffer.data(),
    227                                    operand->getBufferSize());
    228         }
    229     }
    230 
    231     // Set model operations.
    232     for (auto& operation : mOperations) {
    233         NN_FUZZER_LOG << "Operation: " << kOperationNames[static_cast<int32_t>(operation.opType)];
    234         std::vector<uint32_t> inputIndices, outputIndices;
    235         for (auto& op : operation.inputs) {
    236             NN_FUZZER_LOG << toString(*op);
    237             inputIndices.push_back(op->opIndex);
    238         }
    239         for (auto& op : operation.outputs) {
    240             NN_FUZZER_LOG << toString(*op);
    241             outputIndices.push_back(op->opIndex);
    242         }
    243         model->addOperation(operation.opType, inputIndices, outputIndices);
    244     }
    245 
    246     // Set model inputs and outputs.
    247     model->identifyInputsAndOutputs(modelInputs, modelOutputs);
    248 }
    249 
    250 void RandomGraph::createRequest(test_wrapper::Execution* execution,
    251                                 std::vector<OperandBuffer>* buffers) {
    252     NN_FUZZER_LOG << "Create Request";
    253     if (buffers != nullptr) buffers->clear();
    254     for (const auto& operand : mOperands) {
    255         if (operand->type == RandomOperandType::INPUT) {
    256             EXPECT_EQ(execution->setInput(operand->ioIndex, operand->buffer.data(),
    257                                           operand->getBufferSize(), nullptr),
    258                       Result::NO_ERROR);
    259         } else if (operand->type == RandomOperandType::OUTPUT) {
    260             if (buffers == nullptr) {
    261                 EXPECT_EQ(execution->setOutput(operand->ioIndex, operand->buffer.data(),
    262                                                operand->getBufferSize(), nullptr),
    263                           Result::NO_ERROR);
    264             } else {
    265                 // The order of the output buffers corresponds to the order in mOperands.
    266                 buffers->emplace_back(operand->buffer.size());
    267                 EXPECT_EQ(execution->setOutput(operand->ioIndex, buffers->back().data(),
    268                                                operand->getBufferSize(), nullptr),
    269                           Result::NO_ERROR);
    270             }
    271         }
    272     }
    273 }
    274 
    275 // Check if the actual results meet the accuracy criterion.
    276 constexpr uint32_t kMaxNumberOfPrintedErrors = 5;
    277 template <typename T>
    278 void expectNear(const RandomOperand& op, const OperandBuffer& test,
    279                 const AccuracyCriterion& criterion) {
    280     constexpr uint32_t kMinNumberOfElementsToTestBiasMSE = 10;
    281     const T* actualBuffer = reinterpret_cast<const T*>(test.data());
    282     const T* expectedBuffer = reinterpret_cast<const T*>(op.buffer.data());
    283     uint32_t len = op.getNumberOfElements();
    284     uint32_t numSkip = 0, numErrors = 0;
    285     double bias = 0.0f, mse = 0.0f;
    286     for (uint32_t i = 0; i < len; i++) {
    287         SCOPED_TRACE(testing::Message() << "When comparing element " << i);
    288 
    289         // Compare all data types in double for precision and signed arithmetic.
    290         double actual = static_cast<double>(actualBuffer[i]);
    291         double expected = static_cast<double>(expectedBuffer[i]);
    292         double tolerableRange = criterion.atol + criterion.rtol * std::fabs(expected);
    293 
    294         // Skip invalid floating point values.
    295         if (std::isnan(expected) || std::isinf(expected) || std::isnan(actual) ||
    296             std::isinf(actual) || std::fabs(expected) > 1e3) {
    297             numSkip++;
    298             continue;
    299         }
    300 
    301         // Accumulate bias and MSE. Use relative bias and MSE for floating point values.
    302         double diff = actual - expected;
    303         if constexpr (NN_IS_FLOAT(T)) {
    304             diff /= std::max(1.0, std::abs(expected));
    305         }
    306         bias += diff;
    307         mse += diff * diff;
    308 
    309         // Print at most kMaxNumberOfPrintedErrors errors by EXPECT_NEAR.
    310         if (numErrors < kMaxNumberOfPrintedErrors) EXPECT_NEAR(expected, actual, tolerableRange);
    311         if (!(std::fabs(diff) <= tolerableRange)) numErrors++;
    312     }
    313     EXPECT_EQ(numErrors, 0u);
    314 
    315     // Test bias and MSE.
    316     if (len < numSkip + kMinNumberOfElementsToTestBiasMSE) return;
    317     bias /= static_cast<double>(len - numSkip);
    318     mse /= static_cast<double>(len - numSkip);
    319     EXPECT_LE(std::fabs(bias), criterion.bias);
    320     EXPECT_LE(mse, criterion.mse);
    321 }
    322 
    323 // For boolean values, we expect the number of mismatches does not exceed a certain ratio.
    324 void expectBooleanNearlyEqual(const RandomOperand& op, const OperandBuffer& test,
    325                               float allowedErrorRatio) {
    326     const bool8* actual = reinterpret_cast<const bool8*>(test.data());
    327     const bool8* expected = reinterpret_cast<const bool8*>(op.buffer.data());
    328     uint32_t len = op.getNumberOfElements();
    329     uint32_t numErrors = 0;
    330     std::stringstream errorMsg;
    331     for (uint32_t i = 0; i < len; i++) {
    332         if (expected[i] != actual[i]) {
    333             if (numErrors < kMaxNumberOfPrintedErrors)
    334                 errorMsg << "    Expected: " << expected[i] << ", actual: " << actual[i]
    335                          << ", when comparing element " << i << "\n";
    336             numErrors++;
    337         }
    338     }
    339     // When |len| is small, the allowedErrorCount will intentionally ceil at 1, which allows for
    340     // greater tolerance.
    341     uint32_t allowedErrorCount = static_cast<uint32_t>(std::ceil(allowedErrorRatio * len));
    342     EXPECT_LE(numErrors, allowedErrorCount) << errorMsg.str();
    343 }
    344 
    345 void RandomGraph::checkResults(const std::vector<OperandBuffer>& buffers,
    346                                const AccuracyCriteria& criteria) const {
    347     NN_FUZZER_LOG << "Check Results";
    348     // Make sure to keep the same order as the buffers are created.
    349     int i = 0;
    350     for (const auto& op : mOperands) {
    351         if (op->type == RandomOperandType::OUTPUT) {
    352             SCOPED_TRACE(testing::Message()
    353                          << "When comparing output " << op->ioIndex << " (op" << op->opIndex << ")"
    354                          << " of type " << toString(op->dataType));
    355             if (!op->doNotCheckAccuracy) {
    356                 switch (op->dataType) {
    357                     case Type::TENSOR_FLOAT32:
    358                         expectNear<float>(*op, buffers[i], criteria.float32);
    359                         break;
    360                     case Type::TENSOR_FLOAT16:
    361                         expectNear<_Float16>(*op, buffers[i], criteria.float16);
    362                         break;
    363                     case Type::TENSOR_INT32:
    364                         expectNear<int32_t>(*op, buffers[i], criteria.int32);
    365                         break;
    366                     case Type::TENSOR_QUANT8_ASYMM:
    367                         expectNear<uint8_t>(*op, buffers[i], criteria.quant8Asymm);
    368                         break;
    369                     case Type::TENSOR_QUANT8_SYMM:
    370                         expectNear<int8_t>(*op, buffers[i], criteria.quant8Symm);
    371                         break;
    372                     case Type::TENSOR_QUANT16_ASYMM:
    373                         expectNear<uint16_t>(*op, buffers[i], criteria.quant16Asymm);
    374                         break;
    375                     case Type::TENSOR_QUANT16_SYMM:
    376                         expectNear<int16_t>(*op, buffers[i], criteria.quant16Symm);
    377                         break;
    378                     case Type::TENSOR_BOOL8:
    379                         expectBooleanNearlyEqual(*op, buffers[i], /*allowedErrorRatio=*/0.01);
    380                         break;
    381                     default:
    382                         NN_FUZZER_CHECK(false) << "Data type not supported.";
    383                 }
    384             }
    385             i++;
    386         }
    387     }
    388 }
    389 
    390 void RandomGraph::dumpSpecFile(std::string filename, std::string testname = "") {
    391     NN_FUZZER_LOG << "Dump Spec File to " << filename;
    392     SpecWriter writer(filename, testname);
    393     ASSERT_TRUE(writer.isOpen());
    394     writer.dump(mOperations, mOperands);
    395 }
    396 
    397 }  // namespace fuzzing_test
    398 }  // namespace nn
    399 }  // namespace android
    400