• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
RandomOperand(const OperandSignature & operand,Type dataType,uint32_t rank)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 
getDimensions() const45 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.
createEdgeIfValid(const RandomOperand & other) const52 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 
getNumberOfElements() const60 uint32_t RandomOperand::getNumberOfElements() const {
61     uint32_t num = 1;
62     for (const auto& d : dimensions) num *= d.getValue();
63     return num;
64 }
65 
getBufferSize() const66 size_t RandomOperand::getBufferSize() const {
67     return kSizeOfDataType[static_cast<int32_t>(dataType)] * getNumberOfElements();
68 }
69 
70 // Construct a RandomOperation from OperationSignature.
RandomOperation(const OperationSignature & operation)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 
generate(uint32_t seed,uint32_t numOperations,uint32_t dimensionRange)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 
generateGraph(uint32_t numOperations)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 
asConstant(const std::shared_ptr<RandomOperand> & operand,float prob=0.5f)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.
generateValue()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 
createModel(test_wrapper::Model * model)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 
createRequest(test_wrapper::Execution * execution,std::vector<OperandBuffer> * buffers)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>
expectNear(const RandomOperand & op,const OperandBuffer & test,const AccuracyCriterion & criterion)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.
expectBooleanNearlyEqual(const RandomOperand & op,const OperandBuffer & test,float allowedErrorRatio)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 
checkResults(const std::vector<OperandBuffer> & buffers,const AccuracyCriteria & criteria) const345 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 
dumpSpecFile(std::string filename,std::string testname="")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