Release 18.08
[platform/upstream/armnn.git] / tests / ExecuteNetwork / ExecuteNetwork.cpp
1 //
2 // Copyright © 2017 Arm Ltd. All rights reserved.
3 // See LICENSE file in the project root for full license information.
4 //
5 #include "armnn/ArmNN.hpp"
6
7 #include <armnn/TypesUtils.hpp>
8
9 #if defined(ARMNN_CAFFE_PARSER)
10 #include "armnnCaffeParser/ICaffeParser.hpp"
11 #endif
12 #if defined(ARMNN_TF_PARSER)
13 #include "armnnTfParser/ITfParser.hpp"
14 #endif
15 #if defined(ARMNN_TF_LITE_PARSER)
16 #include "armnnTfLiteParser/ITfLiteParser.hpp"
17 #endif
18 #if defined(ARMNN_ONNX_PARSER)
19 #include "armnnOnnxParser/IOnnxParser.hpp"
20 #endif
21 #include "CsvReader.hpp"
22 #include "../InferenceTest.hpp"
23
24 #include <Logging.hpp>
25 #include <Profiling.hpp>
26
27 #include <boost/algorithm/string/trim.hpp>
28 #include <boost/algorithm/string/split.hpp>
29 #include <boost/algorithm/string/classification.hpp>
30 #include <boost/program_options.hpp>
31
32 #include <iostream>
33 #include <fstream>
34 #include <functional>
35 #include <future>
36 #include <algorithm>
37 #include <iterator>
38
39 namespace
40 {
41
42 // Configure boost::program_options for command-line parsing and validation.
43 namespace po = boost::program_options;
44
45 template<typename T, typename TParseElementFunc>
46 std::vector<T> ParseArrayImpl(std::istream& stream, TParseElementFunc parseElementFunc)
47 {
48     std::vector<T> result;
49     // Processes line-by-line.
50     std::string line;
51     while (std::getline(stream, line))
52     {
53         std::vector<std::string> tokens;
54         try
55         {
56             // Coverity fix: boost::split() may throw an exception of type boost::bad_function_call.
57             boost::split(tokens, line, boost::algorithm::is_any_of("\t ,;:"), boost::token_compress_on);
58         }
59         catch (const std::exception& e)
60         {
61             BOOST_LOG_TRIVIAL(error) << "An error occurred when splitting tokens: " << e.what();
62             continue;
63         }
64         for (const std::string& token : tokens)
65         {
66             if (!token.empty()) // See https://stackoverflow.com/questions/10437406/
67             {
68                 try
69                 {
70                     result.push_back(parseElementFunc(token));
71                 }
72                 catch (const std::exception&)
73                 {
74                     BOOST_LOG_TRIVIAL(error) << "'" << token << "' is not a valid number. It has been ignored.";
75                 }
76             }
77         }
78     }
79
80     return result;
81 }
82
83 bool CheckOption(const po::variables_map& vm,
84                  const char* option)
85 {
86     // Check that the given option is valid.
87     if (option == nullptr)
88     {
89         return false;
90     }
91
92     // Check whether 'option' is provided.
93     return vm.find(option) != vm.end();
94 }
95
96 void CheckOptionDependency(const po::variables_map& vm,
97                            const char* option,
98                            const char* required)
99 {
100     // Check that the given options are valid.
101     if (option == nullptr || required == nullptr)
102     {
103         throw po::error("Invalid option to check dependency for");
104     }
105
106     // Check that if 'option' is provided, 'required' is also provided.
107     if (CheckOption(vm, option) && !vm[option].defaulted())
108     {
109         if (CheckOption(vm, required) == 0 || vm[required].defaulted())
110         {
111             throw po::error(std::string("Option '") + option + "' requires option '" + required + "'.");
112         }
113     }
114 }
115
116 void CheckOptionDependencies(const po::variables_map& vm)
117 {
118     CheckOptionDependency(vm, "model-path", "model-format");
119     CheckOptionDependency(vm, "model-path", "input-name");
120     CheckOptionDependency(vm, "model-path", "input-tensor-data");
121     CheckOptionDependency(vm, "model-path", "output-name");
122     CheckOptionDependency(vm, "input-tensor-shape", "model-path");
123 }
124
125 template<typename T>
126 std::vector<T> ParseArray(std::istream& stream);
127
128 template<>
129 std::vector<float> ParseArray(std::istream& stream)
130 {
131     return ParseArrayImpl<float>(stream, [](const std::string& s) { return std::stof(s); });
132 }
133
134 template<>
135 std::vector<unsigned int> ParseArray(std::istream& stream)
136 {
137     return ParseArrayImpl<unsigned int>(stream,
138         [](const std::string& s) { return boost::numeric_cast<unsigned int>(std::stoi(s)); });
139 }
140
141 void PrintArray(const std::vector<float>& v)
142 {
143     for (size_t i = 0; i < v.size(); i++)
144     {
145         printf("%f ", v[i]);
146     }
147     printf("\n");
148 }
149
150 void RemoveDuplicateDevices(std::vector<armnn::Compute>& computeDevices)
151 {
152     // Mark the duplicate devices as 'Undefined'.
153     for (auto i = computeDevices.begin(); i != computeDevices.end(); ++i)
154     {
155         for (auto j = std::next(i); j != computeDevices.end(); ++j)
156         {
157             if (*j == *i)
158             {
159                 *j = armnn::Compute::Undefined;
160             }
161         }
162     }
163
164     // Remove 'Undefined' devices.
165     computeDevices.erase(std::remove(computeDevices.begin(), computeDevices.end(), armnn::Compute::Undefined),
166                          computeDevices.end());
167 }
168
169 bool CheckDevicesAreValid(const std::vector<armnn::Compute>& computeDevices)
170 {
171     return (!computeDevices.empty()
172             && std::none_of(computeDevices.begin(), computeDevices.end(),
173                             [](armnn::Compute c){ return c == armnn::Compute::Undefined; }));
174 }
175
176 } // namespace
177
178 template<typename TParser, typename TDataType>
179 int MainImpl(const char* modelPath,
180              bool isModelBinary,
181              const std::vector<armnn::Compute>& computeDevice,
182              const char* inputName,
183              const armnn::TensorShape* inputTensorShape,
184              const char* inputTensorDataFilePath,
185              const char* outputName,
186              bool enableProfiling,
187              const size_t subgraphId,
188              const std::shared_ptr<armnn::IRuntime>& runtime = nullptr)
189 {
190     // Loads input tensor.
191     std::vector<TDataType> input;
192     {
193         std::ifstream inputTensorFile(inputTensorDataFilePath);
194         if (!inputTensorFile.good())
195         {
196             BOOST_LOG_TRIVIAL(fatal) << "Failed to load input tensor data file from " << inputTensorDataFilePath;
197             return EXIT_FAILURE;
198         }
199         input = ParseArray<TDataType>(inputTensorFile);
200     }
201
202     try
203     {
204         // Creates an InferenceModel, which will parse the model and load it into an IRuntime.
205         typename InferenceModel<TParser, TDataType>::Params params;
206         params.m_ModelPath = modelPath;
207         params.m_IsModelBinary = isModelBinary;
208         params.m_ComputeDevice = computeDevice;
209         params.m_InputBinding = inputName;
210         params.m_InputTensorShape = inputTensorShape;
211         params.m_OutputBinding = outputName;
212         params.m_EnableProfiling = enableProfiling;
213         params.m_SubgraphId = subgraphId;
214         InferenceModel<TParser, TDataType> model(params, runtime);
215
216         // Executes the model.
217         std::vector<TDataType> output(model.GetOutputSize());
218         model.Run(input, output);
219
220         // Prints the output tensor.
221         PrintArray(output);
222     }
223     catch (armnn::Exception const& e)
224     {
225         BOOST_LOG_TRIVIAL(fatal) << "Armnn Error: " << e.what();
226         return EXIT_FAILURE;
227     }
228
229     return EXIT_SUCCESS;
230 }
231
232 // This will run a test
233 int RunTest(const std::string& modelFormat,
234             const std::string& inputTensorShapeStr,
235             const vector<armnn::Compute>& computeDevice,
236             const std::string& modelPath,
237             const std::string& inputName,
238             const std::string& inputTensorDataFilePath,
239             const std::string& outputName,
240             bool enableProfiling,
241             const size_t subgraphId,
242             const std::shared_ptr<armnn::IRuntime>& runtime = nullptr)
243 {
244     // Parse model binary flag from the model-format string we got from the command-line
245     bool isModelBinary;
246     if (modelFormat.find("bin") != std::string::npos)
247     {
248         isModelBinary = true;
249     }
250     else if (modelFormat.find("txt") != std::string::npos || modelFormat.find("text") != std::string::npos)
251     {
252         isModelBinary = false;
253     }
254     else
255     {
256         BOOST_LOG_TRIVIAL(fatal) << "Unknown model format: '" << modelFormat << "'. Please include 'binary' or 'text'";
257         return EXIT_FAILURE;
258     }
259
260     // Parse input tensor shape from the string we got from the command-line.
261     std::unique_ptr<armnn::TensorShape> inputTensorShape;
262     if (!inputTensorShapeStr.empty())
263     {
264         std::stringstream ss(inputTensorShapeStr);
265         std::vector<unsigned int> dims = ParseArray<unsigned int>(ss);
266
267         try
268         {
269             // Coverity fix: An exception of type armnn::InvalidArgumentException is thrown and never caught.
270             inputTensorShape = std::make_unique<armnn::TensorShape>(dims.size(), dims.data());
271         }
272         catch (const armnn::InvalidArgumentException& e)
273         {
274             BOOST_LOG_TRIVIAL(fatal) << "Cannot create tensor shape: " << e.what();
275             return EXIT_FAILURE;
276         }
277     }
278
279     // Forward to implementation based on the parser type
280     if (modelFormat.find("caffe") != std::string::npos)
281     {
282 #if defined(ARMNN_CAFFE_PARSER)
283         return MainImpl<armnnCaffeParser::ICaffeParser, float>(modelPath.c_str(), isModelBinary, computeDevice,
284                                                                inputName.c_str(), inputTensorShape.get(),
285                                                                inputTensorDataFilePath.c_str(), outputName.c_str(),
286                                                                enableProfiling, subgraphId, runtime);
287 #else
288         BOOST_LOG_TRIVIAL(fatal) << "Not built with Caffe parser support.";
289         return EXIT_FAILURE;
290 #endif
291     }
292     else if (modelFormat.find("onnx") != std::string::npos)
293 {
294 #if defined(ARMNN_ONNX_PARSER)
295     return MainImpl<armnnOnnxParser::IOnnxParser, float>(modelPath.c_str(), isModelBinary, computeDevice,
296                                                          inputName.c_str(), inputTensorShape.get(),
297                                                          inputTensorDataFilePath.c_str(), outputName.c_str(),
298                                                          enableProfiling, subgraphId, runtime);
299 #else
300     BOOST_LOG_TRIVIAL(fatal) << "Not built with Onnx parser support.";
301     return EXIT_FAILURE;
302 #endif
303     }
304     else if (modelFormat.find("tensorflow") != std::string::npos)
305     {
306 #if defined(ARMNN_TF_PARSER)
307         return MainImpl<armnnTfParser::ITfParser, float>(modelPath.c_str(), isModelBinary, computeDevice,
308                                                          inputName.c_str(), inputTensorShape.get(),
309                                                          inputTensorDataFilePath.c_str(), outputName.c_str(),
310                                                          enableProfiling, subgraphId, runtime);
311 #else
312         BOOST_LOG_TRIVIAL(fatal) << "Not built with Tensorflow parser support.";
313         return EXIT_FAILURE;
314 #endif
315     }
316     else if(modelFormat.find("tflite") != std::string::npos)
317     {
318 #if defined(ARMNN_TF_LITE_PARSER)
319         if (! isModelBinary)
320         {
321             BOOST_LOG_TRIVIAL(fatal) << "Unknown model format: '" << modelFormat << "'. Only 'binary' format supported \
322               for tflite files";
323             return EXIT_FAILURE;
324         }
325         return MainImpl<armnnTfLiteParser::ITfLiteParser, float>(modelPath.c_str(), isModelBinary, computeDevice,
326                                                                  inputName.c_str(), inputTensorShape.get(),
327                                                                  inputTensorDataFilePath.c_str(), outputName.c_str(),
328                                                                  enableProfiling, subgraphId, runtime);
329 #else
330         BOOST_LOG_TRIVIAL(fatal) << "Unknown model format: '" << modelFormat <<
331             "'. Please include 'caffe', 'tensorflow', 'tflite' or 'onnx'";
332         return EXIT_FAILURE;
333 #endif
334     }
335     else
336     {
337         BOOST_LOG_TRIVIAL(fatal) << "Unknown model format: '" << modelFormat <<
338                                  "'. Please include 'caffe', 'tensorflow', 'tflite' or 'onnx'";
339         return EXIT_FAILURE;
340     }
341 }
342
343 int RunCsvTest(const armnnUtils::CsvRow &csvRow,
344                const std::shared_ptr<armnn::IRuntime>& runtime)
345 {
346     std::string modelFormat;
347     std::string modelPath;
348     std::string inputName;
349     std::string inputTensorShapeStr;
350     std::string inputTensorDataFilePath;
351     std::string outputName;
352
353     size_t subgraphId = 0;
354
355     po::options_description desc("Options");
356     try
357     {
358         desc.add_options()
359         ("model-format,f", po::value(&modelFormat),
360          "caffe-binary, caffe-text, tflite-binary, onnx-binary, onnx-text, tensorflow-binary or tensorflow-text.")
361         ("model-path,m", po::value(&modelPath), "Path to model file, e.g. .caffemodel, .prototxt, .tflite,"
362          " .onnx")
363         ("compute,c", po::value<std::vector<armnn::Compute>>()->multitoken(),
364          "The preferred order of devices to run layers on by default. Possible choices: CpuAcc, CpuRef, GpuAcc")
365         ("input-name,i", po::value(&inputName), "Identifier of the input tensor in the network.")
366         ("subgraph-number,n", po::value<size_t>(&subgraphId)->default_value(0), "Id of the subgraph to be "
367          "executed. Defaults to 0")
368         ("input-tensor-shape,s", po::value(&inputTensorShapeStr),
369          "The shape of the input tensor in the network as a flat array of integers separated by whitespace. "
370          "This parameter is optional, depending on the network.")
371         ("input-tensor-data,d", po::value(&inputTensorDataFilePath),
372          "Path to a file containing the input data as a flat array separated by whitespace.")
373         ("output-name,o", po::value(&outputName), "Identifier of the output tensor in the network.")
374         ("event-based-profiling,e", po::bool_switch()->default_value(false),
375          "Enables built in profiler. If unset, defaults to off.");
376     }
377     catch (const std::exception& e)
378     {
379         // Coverity points out that default_value(...) can throw a bad_lexical_cast,
380         // and that desc.add_options() can throw boost::io::too_few_args.
381         // They really won't in any of these cases.
382         BOOST_ASSERT_MSG(false, "Caught unexpected exception");
383         BOOST_LOG_TRIVIAL(fatal) << "Fatal internal error: " << e.what();
384         return EXIT_FAILURE;
385     }
386
387     std::vector<const char*> clOptions;
388     clOptions.reserve(csvRow.values.size());
389     for (const std::string& value : csvRow.values)
390     {
391         clOptions.push_back(value.c_str());
392     }
393
394     po::variables_map vm;
395     try
396     {
397         po::store(po::parse_command_line(static_cast<int>(clOptions.size()), clOptions.data(), desc), vm);
398
399         po::notify(vm);
400
401         CheckOptionDependencies(vm);
402     }
403     catch (const po::error& e)
404     {
405         std::cerr << e.what() << std::endl << std::endl;
406         std::cerr << desc << std::endl;
407         return EXIT_FAILURE;
408     }
409
410     // Remove leading and trailing whitespaces from the parsed arguments.
411     boost::trim(modelFormat);
412     boost::trim(modelPath);
413     boost::trim(inputName);
414     boost::trim(inputTensorShapeStr);
415     boost::trim(inputTensorDataFilePath);
416     boost::trim(outputName);
417
418     // Get the value of the switch arguments.
419     bool enableProfiling = vm["event-based-profiling"].as<bool>();
420
421     // Get the preferred order of compute devices.
422     std::vector<armnn::Compute> computeDevices = vm["compute"].as<std::vector<armnn::Compute>>();
423
424     // Remove duplicates from the list of compute devices.
425     RemoveDuplicateDevices(computeDevices);
426
427     // Check that the specified compute devices are valid.
428     if (!CheckDevicesAreValid(computeDevices))
429     {
430         BOOST_LOG_TRIVIAL(fatal) << "The list of preferred devices contains an invalid compute";
431         return EXIT_FAILURE;
432     }
433
434     return RunTest(modelFormat, inputTensorShapeStr, computeDevices,
435                    modelPath, inputName, inputTensorDataFilePath, outputName, enableProfiling, subgraphId, runtime);
436 }
437
438 int main(int argc, const char* argv[])
439 {
440     // Configures logging for both the ARMNN library and this test program.
441 #ifdef NDEBUG
442     armnn::LogSeverity level = armnn::LogSeverity::Info;
443 #else
444     armnn::LogSeverity level = armnn::LogSeverity::Debug;
445 #endif
446     armnn::ConfigureLogging(true, true, level);
447     armnnUtils::ConfigureLogging(boost::log::core::get().get(), true, true, level);
448
449     std::string testCasesFile;
450
451     std::string modelFormat;
452     std::string modelPath;
453     std::string inputName;
454     std::string inputTensorShapeStr;
455     std::string inputTensorDataFilePath;
456     std::string outputName;
457
458     size_t subgraphId = 0;
459
460     po::options_description desc("Options");
461     try
462     {
463         desc.add_options()
464             ("help", "Display usage information")
465             ("test-cases,t", po::value(&testCasesFile), "Path to a CSV file containing test cases to run. "
466              "If set, further parameters -- with the exception of compute device and concurrency -- will be ignored, "
467              "as they are expected to be defined in the file for each test in particular.")
468             ("concurrent,n", po::bool_switch()->default_value(false),
469              "Whether or not the test cases should be executed in parallel")
470             ("model-format,f", po::value(&modelFormat),
471              "caffe-binary, caffe-text, onnx-binary, onnx-text, tflite-binary, tensorflow-binary or tensorflow-text.")
472             ("model-path,m", po::value(&modelPath), "Path to model file, e.g. .caffemodel, .prototxt,"
473              " .tflite, .onnx")
474             ("compute,c", po::value<std::vector<armnn::Compute>>()->multitoken(),
475              "The preferred order of devices to run layers on by default. Possible choices: CpuAcc, CpuRef, GpuAcc")
476             ("input-name,i", po::value(&inputName), "Identifier of the input tensor in the network.")
477             ("subgraph-number,x", po::value<size_t>(&subgraphId)->default_value(0), "Id of the subgraph to be executed."
478               "Defaults to 0")
479             ("input-tensor-shape,s", po::value(&inputTensorShapeStr),
480              "The shape of the input tensor in the network as a flat array of integers separated by whitespace. "
481              "This parameter is optional, depending on the network.")
482             ("input-tensor-data,d", po::value(&inputTensorDataFilePath),
483              "Path to a file containing the input data as a flat array separated by whitespace.")
484             ("output-name,o", po::value(&outputName), "Identifier of the output tensor in the network.")
485             ("event-based-profiling,e", po::bool_switch()->default_value(false),
486              "Enables built in profiler. If unset, defaults to off.");
487     }
488     catch (const std::exception& e)
489     {
490         // Coverity points out that default_value(...) can throw a bad_lexical_cast,
491         // and that desc.add_options() can throw boost::io::too_few_args.
492         // They really won't in any of these cases.
493         BOOST_ASSERT_MSG(false, "Caught unexpected exception");
494         BOOST_LOG_TRIVIAL(fatal) << "Fatal internal error: " << e.what();
495         return EXIT_FAILURE;
496     }
497
498     // Parses the command-line.
499     po::variables_map vm;
500     try
501     {
502         po::store(po::parse_command_line(argc, argv, desc), vm);
503
504         if (CheckOption(vm, "help") || argc <= 1)
505         {
506             std::cout << "Executes a neural network model using the provided input tensor. " << std::endl;
507             std::cout << "Prints the resulting output tensor." << std::endl;
508             std::cout << std::endl;
509             std::cout << desc << std::endl;
510             return EXIT_SUCCESS;
511         }
512
513         po::notify(vm);
514     }
515     catch (const po::error& e)
516     {
517         std::cerr << e.what() << std::endl << std::endl;
518         std::cerr << desc << std::endl;
519         return EXIT_FAILURE;
520     }
521
522     // Get the value of the switch arguments.
523     bool concurrent = vm["concurrent"].as<bool>();
524     bool enableProfiling = vm["event-based-profiling"].as<bool>();
525
526     // Check whether we have to load test cases from a file.
527     if (CheckOption(vm, "test-cases"))
528     {
529         // Check that the file exists.
530         if (!boost::filesystem::exists(testCasesFile))
531         {
532             BOOST_LOG_TRIVIAL(fatal) << "Given file \"" << testCasesFile << "\" does not exist";
533             return EXIT_FAILURE;
534         }
535
536         // Parse CSV file and extract test cases
537         armnnUtils::CsvReader reader;
538         std::vector<armnnUtils::CsvRow> testCases = reader.ParseFile(testCasesFile);
539
540         // Check that there is at least one test case to run
541         if (testCases.empty())
542         {
543             BOOST_LOG_TRIVIAL(fatal) << "Given file \"" << testCasesFile << "\" has no test cases";
544             return EXIT_FAILURE;
545         }
546
547         // Create runtime
548         armnn::IRuntime::CreationOptions options;
549         std::shared_ptr<armnn::IRuntime> runtime(armnn::IRuntime::Create(options));
550
551         const std::string executableName("ExecuteNetwork");
552
553         // Check whether we need to run the test cases concurrently
554         if (concurrent)
555         {
556             std::vector<std::future<int>> results;
557             results.reserve(testCases.size());
558
559             // Run each test case in its own thread
560             for (auto&  testCase : testCases)
561             {
562                 testCase.values.insert(testCase.values.begin(), executableName);
563                 results.push_back(std::async(std::launch::async, RunCsvTest, std::cref(testCase), std::cref(runtime)));
564             }
565
566             // Check results
567             for (auto& result : results)
568             {
569                 if (result.get() != EXIT_SUCCESS)
570                 {
571                     return EXIT_FAILURE;
572                 }
573             }
574         }
575         else
576         {
577             // Run tests sequentially
578             for (auto&  testCase : testCases)
579             {
580                 testCase.values.insert(testCase.values.begin(), executableName);
581                 if (RunCsvTest(testCase, runtime) != EXIT_SUCCESS)
582                 {
583                     return EXIT_FAILURE;
584                 }
585             }
586         }
587
588         return EXIT_SUCCESS;
589     }
590     else // Run single test
591     {
592         // Get the preferred order of compute devices.
593         std::vector<armnn::Compute> computeDevices = vm["compute"].as<std::vector<armnn::Compute>>();
594
595         // Remove duplicates from the list of compute devices.
596         RemoveDuplicateDevices(computeDevices);
597
598         // Check that the specified compute devices are valid.
599         if (!CheckDevicesAreValid(computeDevices))
600         {
601             BOOST_LOG_TRIVIAL(fatal) << "The list of preferred devices contains an invalid compute";
602             return EXIT_FAILURE;
603         }
604
605         try
606         {
607             CheckOptionDependencies(vm);
608         }
609         catch (const po::error& e)
610         {
611             std::cerr << e.what() << std::endl << std::endl;
612             std::cerr << desc << std::endl;
613             return EXIT_FAILURE;
614         }
615
616         return RunTest(modelFormat, inputTensorShapeStr, computeDevices,
617                        modelPath, inputName, inputTensorDataFilePath, outputName, enableProfiling, subgraphId);
618     }
619 }