diff --git a/modules/gapi/CMakeLists.txt b/modules/gapi/CMakeLists.txt index 3ca898d4a1..98ce66ded9 100644 --- a/modules/gapi/CMakeLists.txt +++ b/modules/gapi/CMakeLists.txt @@ -346,7 +346,7 @@ ocv_add_samples() # Required for sample with inference on host -if (TARGET example_gapi_onevpl_infer_single_roi) +if(TARGET example_gapi_onevpl_infer_single_roi) if(OPENCV_GAPI_INF_ENGINE) ocv_target_link_libraries(example_gapi_onevpl_infer_single_roi PRIVATE ${INF_ENGINE_TARGET}) ocv_target_compile_definitions(example_gapi_onevpl_infer_single_roi PRIVATE -DHAVE_INF_ENGINE) @@ -356,6 +356,12 @@ if (TARGET example_gapi_onevpl_infer_single_roi) endif() endif() +if(TARGET example_gapi_pipeline_modeling_tool) + if(WIN32) + ocv_target_link_libraries(example_gapi_pipeline_modeling_tool winmm.lib) + endif() +endif() + # perf test dependencies postprocessing if(HAVE_GAPI_ONEVPL) # NB: TARGET opencv_perf_gapi doesn't exist before `ocv_add_perf_tests` diff --git a/modules/gapi/samples/data/config_template.yml b/modules/gapi/samples/data/config_template.yml new file mode 100644 index 0000000000..75012d959d --- /dev/null +++ b/modules/gapi/samples/data/config_template.yml @@ -0,0 +1,192 @@ +%YAML:1.0 + +# Application running time in milliseconds: integer. +work_time: 2000 + +Pipelines: + PL1: + source: + name: 'Src' + latency: 33.0 + output: + dims: [1, 3, 1280, 720] + precision: 'U8' + + nodes: + - name: 'PP' + type: 'Dummy' + time: 0.2 + output: + dims: [1, 3, 300, 300] + precision: 'U8' + + - name: 'Infer' + type: 'Infer' + xml: 'face-detection-retail-0004.xml' + bin: 'face-detection-retail-0004.bin' + device: 'CPU' + input_layers: + - 'data' + output_layers: + - 'detection_out' + + edges: + - from: 'Src' + to: 'PP' + - from: 'PP' + to: 'Infer' + + # Path to the dump file (*.dot)' + dump: 'pl1.dot' + + PL2: + source: + name: 'Src' + latency: 50.0 + output: + dims: [1, 3, 1280, 720] + precision: 'U8' + + nodes: + - name: 'M1_PP' + type: 'Dummy' + time: 0.2 + output: + dims: [1, 3, 300, 300] + precision: 'U8' + + - name: 'M1_Infer' + type: 'Infer' + xml: 'face-detection-retail-0004.xml' + bin: 'face-detection-retail-0004.bin' + device: 'CPU' + input_layers: + - 'data' + output_layers: + - 'detection_out' + + - name: 'M2_PP' + type: 'Dummy' + time: 0.2 + output: + dims: [1, 3, 300, 300] + precision: 'U8' + + - name: 'M2_Infer' + type: 'Infer' + xml: 'face-detection-retail-0004.xml' + bin: 'face-detection-retail-0004.bin' + device: 'CPU' + input_layers: + - 'data' + output_layers: + - 'detection_out' + + - name: 'M3_PP' + type: 'Dummy' + time: 0.2 + output: + dims: [1, 3, 300, 300] + precision: 'U8' + + - name: 'M3_Infer' + type: 'Infer' + xml: 'face-detection-retail-0004.xml' + bin: 'face-detection-retail-0004.bin' + device: 'CPU' + input_layers: + - 'data' + output_layers: + - 'detection_out' + + - name: 'M4_PP' + type: 'Dummy' + time: 0.2 + output: + dims: [1, 3, 300, 300] + precision: 'U8' + + - name: 'M4_Infer' + type: 'Infer' + xml: 'face-detection-retail-0004.xml' + bin: 'face-detection-retail-0004.bin' + device: 'CPU' + input_layers: + - 'data' + output_layers: + - 'detection_out' + + - name: 'M5_PP' + type: 'Dummy' + time: 0.2 + output: + dims: [1, 3, 300, 300] + precision: 'U8' + + - name: 'M5_Infer' + type: 'Infer' + xml: 'face-detection-retail-0004.xml' + bin: 'face-detection-retail-0004.bin' + device: 'CPU' + input_layers: + - 'data' + output_layers: + - 'detection_out' + + edges: + - from: 'Src' + to: 'M1_PP' + - from: 'M1_PP' + to: 'M1_Infer' + - from: 'M1_Infer' + to: 'M2_PP' + - from: 'M2_PP' + to: 'M2_Infer' + - from: 'M2_Infer' + to: 'M3_PP' + - from: 'M3_PP' + to: 'M3_Infer' + - from: 'M3_Infer' + to: 'M4_PP' + - from: 'M4_PP' + to: 'M4_Infer' + - from: 'M4_Infer' + to: 'M5_PP' + - from: 'M5_PP' + to: 'M5_Infer' + + dump: 'pl2.dot' + + PL3: + source: + name: 'Src' + latency: 33.0 + output: + dims: [1, 3, 1280, 720] + precision: 'U8' + + nodes: + - name: 'PP' + type: 'Dummy' + time: 0.2 + output: + dims: [1, 3, 300, 300] + precision: 'U8' + + - name: 'Infer' + type: 'Infer' + xml: 'face-detection-retail-0004.xml' + bin: 'face-detection-retail-0004.bin' + device: 'CPU' + input_layers: + - 'data' + output_layers: + - 'detection_out' + + edges: + - from: 'Src' + to: 'PP' + - from: 'PP' + to: 'Infer' + + dump: 'pl3.dot' diff --git a/modules/gapi/samples/pipeline_modeling_tool.cpp b/modules/gapi/samples/pipeline_modeling_tool.cpp new file mode 100644 index 0000000000..07934756cc --- /dev/null +++ b/modules/gapi/samples/pipeline_modeling_tool.cpp @@ -0,0 +1,406 @@ +#include +#include +#include +#include +#include +#include + +#include +#include // cv::CommandLineParser +#include + +#if defined(_WIN32) +#include +#endif + +#include "pipeline_modeling_tool/dummy_source.hpp" +#include "pipeline_modeling_tool/utils.hpp" +#include "pipeline_modeling_tool/pipeline_builder.hpp" + +enum class AppMode { + REALTIME, + BENCHMARK +}; + +static AppMode strToAppMode(const std::string& mode_str) { + if (mode_str == "realtime") { + return AppMode::REALTIME; + } else if (mode_str == "benchmark") { + return AppMode::BENCHMARK; + } else { + throw std::logic_error("Unsupported AppMode: " + mode_str + + "\nPlease chose between: realtime and benchmark"); + } +} + +template +T read(const cv::FileNode& node) { + return static_cast(node); +} + +static cv::FileNode check_and_get_fn(const cv::FileNode& fn, + const std::string& field, + const std::string& uplvl) { + const bool is_map = fn.isMap(); + if (!is_map || fn[field].empty()) { + throw std::logic_error(uplvl + " must contain field: " + field); + } + return fn[field]; +} + +static cv::FileNode check_and_get_fn(const cv::FileStorage& fs, + const std::string& field, + const std::string& uplvl) { + auto fn = fs[field]; + if (fn.empty()) { + throw std::logic_error(uplvl + " must contain field: " + field); + } + return fn; +} + +template +T check_and_read(const FileT& f, + const std::string& field, + const std::string& uplvl) { + auto fn = check_and_get_fn(f, field, uplvl); + return read(fn); +} + +template +cv::optional readOpt(const cv::FileNode& fn) { + return fn.empty() ? cv::optional() : cv::optional(read(fn)); +} + +template +std::vector readList(const cv::FileNode& fn, + const std::string& field, + const std::string& uplvl) { + auto fn_field = check_and_get_fn(fn, field, uplvl); + if (!fn_field.isSeq()) { + throw std::logic_error(field + " in " + uplvl + " must be a sequence"); + } + + std::vector vec; + for (auto iter : fn_field) { + vec.push_back(read(iter)); + } + return vec; +} + +template +std::vector readVec(const cv::FileNode& fn, + const std::string& field, + const std::string& uplvl) { + auto fn_field = check_and_get_fn(fn, field, uplvl); + + std::vector vec; + fn_field >> vec; + return vec; +} + +static int strToPrecision(const std::string& precision) { + static std::unordered_map str_to_precision = { + {"U8", CV_8U}, {"FP32", CV_32F}, {"FP16", CV_16F} + }; + auto it = str_to_precision.find(precision); + if (it == str_to_precision.end()) { + throw std::logic_error("Unsupported precision: " + precision); + } + return it->second; +} + +template <> +OutputDescr read(const cv::FileNode& fn) { + auto dims = readVec(fn, "dims", "output"); + auto str_prec = check_and_read(fn, "precision", "output"); + return OutputDescr{dims, strToPrecision(str_prec)}; +} + +template <> +Edge read(const cv::FileNode& fn) { + auto from = check_and_read(fn, "from", "edge"); + auto to = check_and_read(fn, "to", "edge"); + + auto splitNameAndPort = [](const std::string& str) { + auto pos = str.find(':'); + auto name = + pos == std::string::npos ? str : std::string(str.c_str(), pos); + size_t port = + pos == std::string::npos ? 0 : std::atoi(str.c_str() + pos + 1); + return std::make_pair(name, port); + }; + + auto p1 = splitNameAndPort(from); + auto p2 = splitNameAndPort(to); + return Edge{Edge::P{p1.first, p1.second}, Edge::P{p2.first, p2.second}}; +} + +static std::string getModelsPath() { + static char* models_path_c = std::getenv("PIPELINE_MODELS_PATH"); + static std::string models_path = models_path_c ? models_path_c : "."; + return models_path; +} + +template <> +ModelPath read(const cv::FileNode& fn) { + using cv::utils::fs::join; + if (!fn["xml"].empty() && !fn["bin"].empty()) { + return ModelPath{LoadPath{join(getModelsPath(), fn["xml"].string()), + join(getModelsPath(), fn["bin"].string())}}; + } else if (!fn["blob"].empty()){ + return ModelPath{ImportPath{join(getModelsPath(), fn["blob"].string())}}; + } else { + const std::string emsg = R""""( + Path to OpenVINO model must be specified in either of two formats: +1. + xml: path to *.xml + bin: path to *.bin +2. + blob: path to *.blob + )""""; + throw std::logic_error(emsg); + } +} + +static PLMode strToPLMode(const std::string& mode_str) { + if (mode_str == "streaming") { + return PLMode::STREAMING; + } else if (mode_str == "regular") { + return PLMode::REGULAR; + } else { + throw std::logic_error("Unsupported PLMode: " + mode_str + + "\nPlease chose between: streaming and regular"); + } +} + +static std::vector parseExecList(const std::string& exec_list) { + std::vector pl_types; + std::stringstream ss(exec_list); + std::string pl_type; + while (getline(ss, pl_type, ',')) { + pl_types.push_back(pl_type); + } + return pl_types; +} + +static void loadConfig(const std::string& filename, + std::map& config) { + cv::FileStorage fs(filename, cv::FileStorage::READ); + if (!fs.isOpened()) { + throw std::runtime_error("Failed to load config: " + filename); + } + + cv::FileNode root = fs.root(); + for (auto it = root.begin(); it != root.end(); ++it) { + auto device = *it; + if (!device.isMap()) { + throw std::runtime_error("Failed to parse config: " + filename); + } + for (auto item : device) { + config.emplace(item.name(), item.string()); + } + } +} + +int main(int argc, char* argv[]) { +#if defined(_WIN32) + timeBeginPeriod(1); +#endif + try { + const std::string keys = + "{ h help | | Print this help message. }" + "{ cfg | | Path to the config which is either" + " YAML file or string. }" + "{ load_config | | Optional. Path to XML/YAML/JSON file" + " to load custom IE parameters. }" + "{ cache_dir | | Optional. Enables caching of loaded models" + " to specified directory. }" + "{ log_file | | Optional. If file is specified, app will" + " dump expanded execution information. }" + "{ pl_mode | streaming | Optional. Pipeline mode: streaming/regular" + " if it's specified will be applied for" + " every pipeline. }" + "{ qc | 1 | Optional. Calculated automatically by G-API" + " if set to 0. If it's specified will be" + " applied for every pipeline. }" + "{ app_mode | realtime | Application mode (realtime/benchmark). }" + "{ exec_list | | A comma-separated list of pipelines that" + " will be executed. Spaces around commas" + " are prohibited. }"; + + cv::CommandLineParser cmd(argc, argv, keys); + if (cmd.has("help")) { + cmd.printMessage(); + return 0; + } + + const auto cfg = cmd.get("cfg"); + const auto load_config = cmd.get("load_config"); + const auto cached_dir = cmd.get("cache_dir"); + const auto log_file = cmd.get("log_file"); + const auto pl_mode = strToPLMode(cmd.get("pl_mode")); + const auto qc = cmd.get("qc"); + const auto app_mode = strToAppMode(cmd.get("app_mode")); + const auto exec_str = cmd.get("exec_list"); + + cv::FileStorage fs; + if (cfg.empty()) { + throw std::logic_error("Config must be specified via --cfg option"); + } + // NB: *.yml + if (cfg.size() < 5) { + throw std::logic_error("--cfg string must contain at least 5 symbols" + " to determine if it's a file (*.yml) a or string"); + } + if (cfg.substr(cfg.size() - 4, cfg.size()) == ".yml") { + if (!fs.open(cfg, cv::FileStorage::READ)) { + throw std::logic_error("Failed to open config file: " + cfg); + } + } else { + fs = cv::FileStorage(cfg, cv::FileStorage::FORMAT_YAML | + cv::FileStorage::MEMORY); + } + + std::map config; + if (!load_config.empty()) { + loadConfig(load_config, config); + } + // NB: Takes priority over config from file + if (!cached_dir.empty()) { + config = + std::map{{"CACHE_DIR", cached_dir}}; + } + + const double work_time_ms = + check_and_read(fs, "work_time", "Config"); + if (work_time_ms < 0) { + throw std::logic_error("work_time must be positive"); + } + + auto pipelines_fn = check_and_get_fn(fs, "Pipelines", "Config"); + if (!pipelines_fn.isMap()) { + throw std::logic_error("Pipelines field must be a map"); + } + + auto exec_list = !exec_str.empty() ? parseExecList(exec_str) + : pipelines_fn.keys(); + + + std::vector pipelines; + pipelines.reserve(exec_list.size()); + // NB: Build pipelines based on config information + PipelineBuilder builder; + for (const auto& name : exec_list) { + const auto& pl_fn = check_and_get_fn(pipelines_fn, name, "Pipelines"); + builder.setName(name); + // NB: Set source + { + const auto& src_fn = check_and_get_fn(pl_fn, "source", name); + auto src_name = + check_and_read(src_fn, "name", "source"); + auto latency = + check_and_read(src_fn, "latency", "source"); + auto output = + check_and_read(src_fn, "output", "source"); + // NB: In case BENCHMARK mode sources work with zero latency. + if (app_mode == AppMode::BENCHMARK) { + latency = 0.0; + } + builder.setSource(src_name, latency, output); + } + + const auto& nodes_fn = check_and_get_fn(pl_fn, "nodes", name); + if (!nodes_fn.isSeq()) { + throw std::logic_error("nodes in " + name + " must be a sequence"); + } + for (auto node_fn : nodes_fn) { + auto node_name = + check_and_read(node_fn, "name", "node"); + auto node_type = + check_and_read(node_fn, "type", "node"); + if (node_type == "Dummy") { + auto time = + check_and_read(node_fn, "time", node_name); + if (time < 0) { + throw std::logic_error(node_name + " time must be positive"); + } + auto output = + check_and_read(node_fn, "output", node_name); + builder.addDummy(node_name, time, output); + } else if (node_type == "Infer") { + InferParams params; + params.path = read(node_fn); + params.device = + check_and_read(node_fn, "device", node_name); + params.input_layers = + readList(node_fn, "input_layers", node_name); + params.output_layers = + readList(node_fn, "output_layers", node_name); + params.config = config; + builder.addInfer(node_name, params); + } else { + throw std::logic_error("Unsupported node type: " + node_type); + } + } + + const auto edges_fn = check_and_get_fn(pl_fn, "edges", name); + if (!edges_fn.isSeq()) { + throw std::logic_error("edges in " + name + " must be a sequence"); + } + for (auto edge_fn : edges_fn) { + auto edge = read(edge_fn); + builder.addEdge(edge); + } + + // NB: Pipeline mode from config takes priority over cmd. + auto mode = readOpt(pl_fn["mode"]); + builder.setMode(mode.has_value() ? strToPLMode(mode.value()) : pl_mode); + + // NB: Queue capacity from config takes priority over cmd. + auto config_qc = readOpt(pl_fn["queue_capacity"]); + auto queue_capacity = config_qc.has_value() ? config_qc.value() : qc; + // NB: 0 is special constant that means + // queue capacity should be calculated automatically. + if (queue_capacity != 0) { + builder.setQueueCapacity(queue_capacity); + } + + auto dump = readOpt(pl_fn["dump"]); + if (dump) { + builder.setDumpFilePath(dump.value()); + } + + pipelines.emplace_back(builder.build()); + } + + // NB: Compille pipelines + for (size_t i = 0; i < pipelines.size(); ++i) { + pipelines[i]->compile(); + } + + // NB: Execute pipelines + std::vector threads(pipelines.size()); + for (size_t i = 0; i < pipelines.size(); ++i) { + threads[i] = std::thread([&, i]() { + pipelines[i]->run(work_time_ms); + }); + } + + std::ofstream file; + if (!log_file.empty()) { + file.open(log_file); + } + + for (size_t i = 0; i < threads.size(); ++i) { + threads[i].join(); + if (file.is_open()) { + file << pipelines[i]->report().toStr(true) << std::endl; + } + std::cout << pipelines[i]->report().toStr() << std::endl; + } + } catch (std::exception& e) { + std::cout << e.what() << std::endl; + throw; + } + return 0; +} diff --git a/modules/gapi/samples/pipeline_modeling_tool/dummy_source.hpp b/modules/gapi/samples/pipeline_modeling_tool/dummy_source.hpp new file mode 100644 index 0000000000..e807aa206f --- /dev/null +++ b/modules/gapi/samples/pipeline_modeling_tool/dummy_source.hpp @@ -0,0 +1,66 @@ +#ifndef OPENCV_GAPI_PIPELINE_MODELING_TOOL_DUMMY_SOURCE_HPP +#define OPENCV_GAPI_PIPELINE_MODELING_TOOL_DUMMY_SOURCE_HPP + +#include +#include +#include + +#include +#include // cv::gapi::wip::IStreamSource + +#include "utils.hpp" + +class DummySource final: public cv::gapi::wip::IStreamSource { +public: + using Ptr = std::shared_ptr; + DummySource(const double latency, + const OutputDescr& output); + bool pull(cv::gapi::wip::Data& data) override; + cv::GMetaArg descr_of() const override; + +private: + double m_latency; + cv::Mat m_mat; + using TimePoint = + std::chrono::time_point; + cv::optional m_prev_pull_tp; +}; + +DummySource::DummySource(const double latency, + const OutputDescr& output) + : m_latency(latency), m_mat(output.dims, output.precision) { + if (output.dims.size() == 1) { + //FIXME: Well-known 1D mat WA + m_mat.dims = 1; + } + utils::generateRandom(m_mat); +} + +bool DummySource::pull(cv::gapi::wip::Data& data) { + using namespace std::chrono; + using namespace cv::gapi::streaming; + // NB: In case it's the first pull. + if (!m_prev_pull_tp) { + m_prev_pull_tp = cv::util::make_optional(high_resolution_clock::now()); + } + // NB: Just increase reference counter not to release mat memory + // after assigning it to the data. + cv::Mat mat = m_mat; + auto end = high_resolution_clock::now(); + auto elapsed = + duration_cast>(end - *m_prev_pull_tp).count(); + auto delta = m_latency - elapsed; + if (delta > 0) { + utils::sleep(delta); + } + data.meta[meta_tag::timestamp] = int64_t{utils::timestamp()}; + data = mat; + m_prev_pull_tp = cv::util::make_optional(high_resolution_clock::now()); + return true; +} + +cv::GMetaArg DummySource::descr_of() const { + return cv::GMetaArg{cv::descr_of(m_mat)}; +} + +#endif // OPENCV_GAPI_PIPELINE_MODELING_TOOL_DUMMY_SOURCE_HPP diff --git a/modules/gapi/samples/pipeline_modeling_tool/pipeline.hpp b/modules/gapi/samples/pipeline_modeling_tool/pipeline.hpp new file mode 100644 index 0000000000..c0f2c02948 --- /dev/null +++ b/modules/gapi/samples/pipeline_modeling_tool/pipeline.hpp @@ -0,0 +1,204 @@ +#ifndef OPENCV_GAPI_PIPELINE_MODELING_TOOL_PIPELINE_HPP +#define OPENCV_GAPI_PIPELINE_MODELING_TOOL_PIPELINE_HPP + +struct PerfReport { + std::string name; + double avg_latency = 0.0; + double throughput = 0.0; + int64_t first_run_latency = 0; + int64_t elapsed = 0; + int64_t compilation_time = 0; + std::vector latencies; + + std::string toStr(bool expanded = false) const; +}; + +std::string PerfReport::toStr(bool expand) const { + std::stringstream ss; + ss << name << ": Compilation time: " << compilation_time << " ms; " + << "Average latency: " << avg_latency << " ms; Throughput: " + << throughput << " FPS; First latency: " + << first_run_latency << " ms"; + + if (expand) { + ss << "\nTotal processed frames: " << latencies.size() + << "\nTotal elapsed time: " << elapsed << " ms" << std::endl; + for (size_t i = 0; i < latencies.size(); ++i) { + ss << std::endl; + ss << "Frame:" << i << "\nLatency: " + << latencies[i] << " ms"; + } + } + + return ss.str(); +} + +class Pipeline { +public: + using Ptr = std::shared_ptr; + + Pipeline(std::string&& name, + cv::GComputation&& comp, + cv::gapi::wip::IStreamSource::Ptr&& src, + cv::GCompileArgs&& args, + const size_t num_outputs); + + void compile(); + void run(double work_time_ms); + const PerfReport& report() const; + + virtual ~Pipeline() = default; + +protected: + struct RunPerf { + int64_t elapsed = 0; + std::vector latencies; + }; + + virtual void _compile() = 0; + virtual RunPerf _run(double work_time_ms) = 0; + + std::string m_name; + cv::GComputation m_comp; + cv::gapi::wip::IStreamSource::Ptr m_src; + cv::GCompileArgs m_args; + size_t m_num_outputs; + PerfReport m_perf; +}; + +Pipeline::Pipeline(std::string&& name, + cv::GComputation&& comp, + cv::gapi::wip::IStreamSource::Ptr&& src, + cv::GCompileArgs&& args, + const size_t num_outputs) + : m_name(std::move(name)), + m_comp(std::move(comp)), + m_src(std::move(src)), + m_args(std::move(args)), + m_num_outputs(num_outputs) { + m_perf.name = m_name; +} + +void Pipeline::compile() { + m_perf.compilation_time = + utils::measure([this]() { + _compile(); + }); +} + +void Pipeline::run(double work_time_ms) { + auto run_perf = _run(work_time_ms); + + m_perf.elapsed = run_perf.elapsed; + m_perf.latencies = std::move(run_perf.latencies); + + m_perf.avg_latency = + std::accumulate(m_perf.latencies.begin(), + m_perf.latencies.end(), + 0.0) / static_cast(m_perf.latencies.size()); + m_perf.throughput = + (m_perf.latencies.size() / static_cast(m_perf.elapsed)) * 1000; + + m_perf.first_run_latency = m_perf.latencies[0]; +} + +const PerfReport& Pipeline::report() const { + return m_perf; +} + +class StreamingPipeline : public Pipeline { +public: + using Pipeline::Pipeline; + +private: + void _compile() override { + m_compiled = + m_comp.compileStreaming({m_src->descr_of()}, + cv::GCompileArgs(m_args)); + } + + Pipeline::RunPerf _run(double work_time_ms) override { + // NB: Setup. + using namespace std::chrono; + // NB: N-1 buffers + timestamp. + std::vector out_mats(m_num_outputs - 1); + int64_t start_ts = -1; + cv::GRunArgsP pipeline_outputs; + for (auto& m : out_mats) { + pipeline_outputs += cv::gout(m); + } + pipeline_outputs += cv::gout(start_ts); + m_compiled.setSource(m_src); + + // NB: Start execution & measure performance statistics. + Pipeline::RunPerf perf; + auto start = high_resolution_clock::now(); + m_compiled.start(); + while (m_compiled.pull(cv::GRunArgsP{pipeline_outputs})) { + int64_t latency = utils::timestamp() - start_ts; + + perf.latencies.push_back(latency); + perf.elapsed = duration_cast( + high_resolution_clock::now() - start).count(); + + if (perf.elapsed >= work_time_ms) { + m_compiled.stop(); + break; + } + }; + return perf; + } + + cv::GStreamingCompiled m_compiled; +}; + +class RegularPipeline : public Pipeline { +public: + using Pipeline::Pipeline; + +private: + void _compile() override { + m_compiled = + m_comp.compile({m_src->descr_of()}, + cv::GCompileArgs(m_args)); + } + + Pipeline::RunPerf _run(double work_time_ms) override { + // NB: Setup + using namespace std::chrono; + cv::gapi::wip::Data d; + std::vector out_mats(m_num_outputs); + cv::GRunArgsP pipeline_outputs; + for (auto& m : out_mats) { + pipeline_outputs += cv::gout(m); + } + + // NB: Start execution & measure performance statistics. + Pipeline::RunPerf perf; + auto start = high_resolution_clock::now(); + while (m_src->pull(d)) { + auto in_mat = cv::util::get(d); + int64_t latency = utils::measure([&]{ + m_compiled(cv::gin(in_mat), cv::GRunArgsP{pipeline_outputs}); + }); + + perf.latencies.push_back(latency); + perf.elapsed = duration_cast( + high_resolution_clock::now() - start).count(); + + if (perf.elapsed >= work_time_ms) { + break; + } + }; + return perf; + } + + cv::GCompiled m_compiled; +}; + +enum class PLMode { + REGULAR, + STREAMING +}; + +#endif // OPENCV_GAPI_PIPELINE_MODELING_TOOL_PIPELINE_HPP diff --git a/modules/gapi/samples/pipeline_modeling_tool/pipeline_builder.hpp b/modules/gapi/samples/pipeline_modeling_tool/pipeline_builder.hpp new file mode 100644 index 0000000000..c62322e244 --- /dev/null +++ b/modules/gapi/samples/pipeline_modeling_tool/pipeline_builder.hpp @@ -0,0 +1,502 @@ +#ifndef OPENCV_GAPI_PIPELINE_MODELING_TOOL_PIPELINE_BUILDER_HPP +#define OPENCV_GAPI_PIPELINE_MODELING_TOOL_PIPELINE_BUILDER_HPP + +#include + +#include // cv::gapi::GNetPackage +#include // cv::gapi::wip::IStreamSource +#include // cv::gapi::ie::Params +#include // cv::gapi::GCompileArgs +#include // GAPI_OCV_KERNEL +#include // G_API_OP + +#include "pipeline.hpp" +#include "utils.hpp" + +struct Edge { + struct P { + std::string name; + size_t port; + }; + + P src; + P dst; +}; + +struct CallNode { + using F = std::function; + + std::string name; + F run; +}; + +struct DataNode { + cv::optional arg; +}; + +struct Node { + using Ptr = std::shared_ptr; + using WPtr = std::weak_ptr; + using Kind = cv::util::variant; + + std::vector in_nodes; + std::vector out_nodes; + Kind kind; +}; + +struct DummyCall { + G_API_OP(GDummy, + , + "custom.dummy") { + static cv::GMatDesc outMeta(const cv::GMatDesc& /* in */, + double /* time */, + const OutputDescr& output) { + if (output.dims.size() == 2) { + return cv::GMatDesc(output.precision, + 1, + cv::Size(output.dims[0], output.dims[1])); + } + return cv::GMatDesc(output.precision, output.dims); + } + }; + + struct DummyState { + cv::Mat mat; + }; + + // NB: Generate random mat once and then + // copy to dst buffer on every iteration. + GAPI_OCV_KERNEL_ST(GCPUDummy, GDummy, DummyState) { + static void setup(const cv::GMatDesc& /*in*/, + double /*time*/, + const OutputDescr& output, + std::shared_ptr& state, + const cv::GCompileArgs& /*args*/) { + state.reset(new DummyState{}); + state->mat.create(output.dims, output.precision); + utils::generateRandom(state->mat); + } + + static void run(const cv::Mat& /*in_mat*/, + double time, + const OutputDescr& /*output*/, + cv::Mat& out_mat, + DummyState& state) { + using namespace std::chrono; + double total = 0; + auto start = high_resolution_clock::now(); + state.mat.copyTo(out_mat); + while (total < time) { + total = duration_cast>( + high_resolution_clock::now() - start).count(); + } + } + }; + + void operator()(const cv::GProtoArgs& inputs, cv::GProtoArgs& outputs); + + size_t numInputs() const { return 1; } + size_t numOutputs() const { return 1; } + + double time; + OutputDescr output; +}; + +void DummyCall::operator()(const cv::GProtoArgs& inputs, + cv::GProtoArgs& outputs) { + GAPI_Assert(inputs.size() == 1u); + GAPI_Assert(cv::util::holds_alternative(inputs[0])); + GAPI_Assert(outputs.empty()); + auto in = cv::util::get(inputs[0]); + outputs.emplace_back(GDummy::on(in, time, output)); +} + +struct InferCall { + void operator()(const cv::GProtoArgs& inputs, cv::GProtoArgs& outputs); + size_t numInputs() const { return input_layers.size(); } + size_t numOutputs() const { return output_layers.size(); } + + std::string tag; + std::vector input_layers; + std::vector output_layers; +}; + +void InferCall::operator()(const cv::GProtoArgs& inputs, + cv::GProtoArgs& outputs) { + GAPI_Assert(inputs.size() == input_layers.size()); + GAPI_Assert(outputs.empty()); + + cv::GInferInputs g_inputs; + // TODO: Add an opportunity not specify input/output layers in case + // there is only single layer. + for (size_t i = 0; i < inputs.size(); ++i) { + // TODO: Support GFrame as well. + GAPI_Assert(cv::util::holds_alternative(inputs[i])); + auto in = cv::util::get(inputs[i]); + g_inputs[input_layers[i]] = in; + } + auto g_outputs = cv::gapi::infer(tag, g_inputs); + for (size_t i = 0; i < output_layers.size(); ++i) { + outputs.emplace_back(g_outputs.at(output_layers[i])); + } +} + +struct SourceCall { + void operator()(const cv::GProtoArgs& inputs, cv::GProtoArgs& outputs); + size_t numInputs() const { return 0; } + size_t numOutputs() const { return 1; } +}; + +void SourceCall::operator()(const cv::GProtoArgs& inputs, + cv::GProtoArgs& outputs) { + GAPI_Assert(inputs.empty()); + GAPI_Assert(outputs.empty()); + // NB: Since NV12 isn't exposed source always produce GMat. + outputs.emplace_back(cv::GMat()); +} + +struct LoadPath { + std::string xml; + std::string bin; +}; + +struct ImportPath { + std::string blob; +}; + +using ModelPath = cv::util::variant; + +struct InferParams { + std::string name; + ModelPath path; + std::string device; + std::vector input_layers; + std::vector output_layers; + std::map config; +}; + +class PipelineBuilder { +public: + PipelineBuilder(); + void addDummy(const std::string& name, + const double time, + const OutputDescr& output); + + void addInfer(const std::string& name, const InferParams& params); + + void setSource(const std::string& name, + double latency, + const OutputDescr& output); + + void addEdge(const Edge& edge); + void setMode(PLMode mode); + void setDumpFilePath(const std::string& dump); + void setQueueCapacity(const size_t qc); + void setName(const std::string& name); + + Pipeline::Ptr build(); + +private: + template + void addCall(const std::string& name, + CallT&& call); + + Pipeline::Ptr construct(); + + template + using M = std::unordered_map; + struct State { + struct NodeEdges { + std::vector input_edges; + std::vector output_edges; + }; + + M calls_map; + std::vector all_calls; + + cv::gapi::GNetPackage networks; + cv::gapi::GKernelPackage kernels; + cv::GCompileArgs compile_args; + cv::gapi::wip::IStreamSource::Ptr src; + PLMode mode = PLMode::STREAMING; + std::string name; + }; + + std::unique_ptr m_state; +}; + +PipelineBuilder::PipelineBuilder() : m_state(new State{}) { }; + +void PipelineBuilder::addDummy(const std::string& name, + const double time, + const OutputDescr& output) { + m_state->kernels.include(); + addCall(name, DummyCall{time, output}); +} + +template +void PipelineBuilder::addCall(const std::string& name, + CallT&& call) { + + size_t num_inputs = call.numInputs(); + size_t num_outputs = call.numOutputs(); + Node::Ptr call_node(new Node{{},{},Node::Kind{CallNode{name, std::move(call)}}}); + // NB: Create placeholders for inputs. + call_node->in_nodes.resize(num_inputs); + // NB: Create outputs with empty data. + for (size_t i = 0; i < num_outputs; ++i) { + call_node->out_nodes.emplace_back(new Node{{call_node}, + {}, + Node::Kind{DataNode{}}}); + } + + auto it = m_state->calls_map.find(name); + if (it != m_state->calls_map.end()) { + throw std::logic_error("Node: " + name + " already exists!"); + } + m_state->calls_map.emplace(name, call_node); + m_state->all_calls.emplace_back(call_node); +} + +void PipelineBuilder::addInfer(const std::string& name, + const InferParams& params) { + // NB: No default ctor for Params. + std::unique_ptr> pp; + if (cv::util::holds_alternative(params.path)) { + auto load_path = cv::util::get(params.path); + pp.reset(new cv::gapi::ie::Params(name, + load_path.xml, + load_path.bin, + params.device)); + } else { + GAPI_Assert(cv::util::holds_alternative(params.path)); + auto import_path = cv::util::get(params.path); + pp.reset(new cv::gapi::ie::Params(name, + import_path.blob, + params.device)); + } + + pp->pluginConfig(params.config); + m_state->networks += cv::gapi::networks(*pp); + + addCall(name, InferCall{name, params.input_layers, params.output_layers}); +} + +void PipelineBuilder::addEdge(const Edge& edge) { + const auto& src_it = m_state->calls_map.find(edge.src.name); + if (src_it == m_state->calls_map.end()) { + throw std::logic_error("Failed to find node: " + edge.src.name); + } + auto src_node = src_it->second; + if (src_node->out_nodes.size() <= edge.src.port) { + throw std::logic_error("Failed to access node: " + edge.src.name + + " by out port: " + std::to_string(edge.src.port)); + } + + auto dst_it = m_state->calls_map.find(edge.dst.name); + if (dst_it == m_state->calls_map.end()) { + throw std::logic_error("Failed to find node: " + edge.dst.name); + } + auto dst_node = dst_it->second; + if (dst_node->in_nodes.size() <= edge.dst.port) { + throw std::logic_error("Failed to access node: " + edge.dst.name + + " by in port: " + std::to_string(edge.dst.port)); + } + + auto out_data = src_node->out_nodes[edge.src.port]; + auto& in_data = dst_node->in_nodes[edge.dst.port]; + // NB: in_data != nullptr. + if (!in_data.expired()) { + throw std::logic_error("Node: " + edge.dst.name + + " already connected by in port: " + + std::to_string(edge.dst.port)); + } + dst_node->in_nodes[edge.dst.port] = out_data; + out_data->out_nodes.push_back(dst_node); +} + +void PipelineBuilder::setSource(const std::string& name, + double latency, + const OutputDescr& output) { + GAPI_Assert(!m_state->src); + m_state->src = std::make_shared(latency, output); + addCall(name, SourceCall{}); +} + +void PipelineBuilder::setMode(PLMode mode) { + m_state->mode = mode; +} + +void PipelineBuilder::setDumpFilePath(const std::string& dump) { + m_state->compile_args.emplace_back(cv::graph_dump_path{dump}); +} + +void PipelineBuilder::setQueueCapacity(const size_t qc) { + m_state->compile_args.emplace_back(cv::gapi::streaming::queue_capacity{qc}); +} + +void PipelineBuilder::setName(const std::string& name) { + m_state->name = name; +} + +static bool visit(Node::Ptr node, + std::vector& sorted, + std::unordered_map& visited) { + if (!node) { + throw std::logic_error("Found null node"); + } + + visited[node] = 1; + for (auto in : node->in_nodes) { + auto in_node = in.lock(); + if (visited[in_node] == 0) { + if (visit(in_node, sorted, visited)) { + return true; + } + } else if (visited[in_node] == 1) { + return true; + } + } + visited[node] = 2; + sorted.push_back(node); + return false; +} + +static cv::optional> +toposort(const std::vector nodes) { + std::vector sorted; + std::unordered_map visited; + for (auto n : nodes) { + if (visit(n, sorted, visited)) { + return cv::optional>{}; + } + } + return cv::util::make_optional(sorted); +} + +Pipeline::Ptr PipelineBuilder::construct() { + // NB: Unlike G-API, pipeline_builder_tool graph always starts with CALL node + // (not data) that produce datas, so the call node which doesn't have + // inputs is considered as "producer" node. + // + // Graph always starts with CALL node and ends with DATA node. + // Graph example: [source] -> (source:0) -> [PP] -> (PP:0) + // + // The algorithm is quite simple: + // 0. Verify that every call input node exists (connected). + // 1. Sort all nodes by visiting only call nodes, + // since there is no data nodes that's not connected with any call node, + // it's guarantee that every node will be visited. + // 2. Fillter call nodes. + // 3. Go through every call node. + // FIXME: Add toposort in case user passed nodes + // in arbitrary order which is unlikely happened. + // 4. Extract proto input from every input node + // 5. Run call and get outputs + // 6. If call node doesn't have inputs it means that it's "producer" node, + // so collect all outputs to graph_inputs vector. + // 7. Assign proto outputs to output data nodes, + // so the next calls can use them as inputs. + cv::GProtoArgs graph_inputs; + cv::GProtoArgs graph_outputs; + // 0. Verify that every call input node exists (connected). + for (auto call_node : m_state->all_calls) { + for (size_t i = 0; i < call_node->in_nodes.size(); ++i) { + const auto& in_data_node = call_node->in_nodes[i]; + // NB: in_data_node == nullptr. + if (in_data_node.expired()) { + const auto& call = cv::util::get(call_node->kind); + throw std::logic_error( + "Node: " + call.name + " in Pipeline: " + m_state->name + + " has dangling input by in port: " + std::to_string(i)); + } + } + } + // (0) Sort all nodes; + auto has_sorted = toposort(m_state->all_calls); + if (!has_sorted) { + throw std::logic_error( + "Pipeline: " + m_state->name + " has cyclic dependencies") ; + } + auto& sorted = has_sorted.value(); + // (1). Fillter call nodes. + std::vector sorted_calls; + for (auto n : sorted) { + if (cv::util::holds_alternative(n->kind)) { + sorted_calls.push_back(n); + } + } + // (2). Go through every call node. + for (auto call_node : sorted_calls) { + cv::GProtoArgs outputs; + cv::GProtoArgs inputs; + for (size_t i = 0; i < call_node->in_nodes.size(); ++i) { + auto in_node = call_node->in_nodes.at(i); + auto in_data = cv::util::get(in_node.lock()->kind); + if (!in_data.arg.has_value()) { + throw std::logic_error("data hasn't been provided"); + } + // (3). Extract proto input from every input node. + inputs.push_back(in_data.arg.value()); + } + // (4). Run call and get outputs. + auto call = cv::util::get(call_node->kind); + call.run(inputs, outputs); + // (5) If call node doesn't have inputs + // it means that it's input producer node (Source). + if (call_node->in_nodes.empty()) { + for (auto out : outputs) { + graph_inputs.push_back(out); + } + } + // (6). Assign proto outputs to output data nodes, + // so the next calls can use them as inputs. + GAPI_Assert(outputs.size() == call_node->out_nodes.size()); + for (size_t i = 0; i < outputs.size(); ++i) { + auto out_node = call_node->out_nodes[i]; + auto& out_data = cv::util::get(out_node->kind); + out_data.arg = cv::util::make_optional(outputs[i]); + if (out_node->out_nodes.empty()) { + graph_outputs.push_back(out_data.arg.value()); + } + } + } + + m_state->compile_args.emplace_back(m_state->networks); + m_state->compile_args.emplace_back(m_state->kernels); + + if (m_state->mode == PLMode::STREAMING) { + GAPI_Assert(graph_inputs.size() == 1); + GAPI_Assert(cv::util::holds_alternative(graph_inputs[0])); + // FIXME: Handle GFrame when NV12 comes. + const auto& graph_input = cv::util::get(graph_inputs[0]); + // NB: In case streaming mode need to expose timestamp in order to + // calculate performance metrics. + graph_outputs.emplace_back( + cv::gapi::streaming::timestamp(graph_input).strip()); + + return std::make_shared(std::move(m_state->name), + cv::GComputation( + cv::GProtoInputArgs{graph_inputs}, + cv::GProtoOutputArgs{graph_outputs}), + std::move(m_state->src), + std::move(m_state->compile_args), + graph_outputs.size()); + } + GAPI_Assert(m_state->mode == PLMode::REGULAR); + return std::make_shared(std::move(m_state->name), + cv::GComputation( + cv::GProtoInputArgs{graph_inputs}, + cv::GProtoOutputArgs{graph_outputs}), + std::move(m_state->src), + std::move(m_state->compile_args), + graph_outputs.size()); +} + +Pipeline::Ptr PipelineBuilder::build() { + auto pipeline = construct(); + m_state.reset(new State{}); + return pipeline; +} + +#endif // OPENCV_GAPI_PIPELINE_MODELING_TOOL_PIPELINE_BUILDER_HPP diff --git a/modules/gapi/samples/pipeline_modeling_tool/test_pipeline_modeling_tool.py b/modules/gapi/samples/pipeline_modeling_tool/test_pipeline_modeling_tool.py new file mode 100644 index 0000000000..ef4bce6476 --- /dev/null +++ b/modules/gapi/samples/pipeline_modeling_tool/test_pipeline_modeling_tool.py @@ -0,0 +1,931 @@ +import os +import subprocess + +pipeline_modeling_tool = os.getenv('PIPELINE_MODELING_TOOL') + +def get_output(exec_str): + try: + out = subprocess.check_output(exec_str, + stderr=subprocess.STDOUT, + shell=True).strip().decode() + except subprocess.CalledProcessError as exc: + out = exc.output.strip().decode() + return out + + +def test_error_no_config_specified(): + out = get_output(pipeline_modeling_tool) + assert out.startswith('Config must be specified via --cfg option') + + +def test_error_no_config_exists(): + cfg_file = 'not_existing_cfg.yml' + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert 'Failed to open config file: not_existing_cfg.yml' in out + + +def test_error_no_work_time(): + cfg_file = """\"%YAML:1.0\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('Config must contain field: work_time') + + +def test_error_work_time_not_positive(): + cfg_file = """\"%YAML:1.0 +work_time: -1\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('work_time must be positive') + + +def test_error_no_pipelines(): + cfg_file = """\"%YAML:1.0 +work_time: 1000\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('Config must contain field: Pipelines') + + +def test_error_pipelines_node_not_map(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines:\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('Pipelines field must be a map') + + +def test_error_config_not_contain_pl(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1:\" """ + + exec_str = '{} --cfg={} --exec_list=PL2'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('Pipelines must contain field: PL2') + + +def test_error_no_source(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1:\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('PL1 must contain field: source') + + +def test_error_source_no_name(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source:\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('source must contain field: name') + + +def test_error_source_no_latency(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('source must contain field: latency') + + +def test_error_source_no_output(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('source must contain field: output') + + +def test_error_source_output_no_dims(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output:\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('output must contain field: dims') + + +def test_error_source_output_no_precision(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4]\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('output must contain field: precision') + + +def test_error_no_nodes(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('PL1 must contain field: nodes') + + +def test_error_nodes_not_sequence(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes:\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('nodes in PL1 must be a sequence') + + +def test_error_node_no_name(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + -\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('node must contain field: name') + + +def test_error_node_no_type(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('node must contain field: type') + + +def test_error_node_unknown_type(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Unknown'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('Unsupported node type: Unknown') + + +def test_error_node_dummy_no_time(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Dummy'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('Node0 must contain field: time') + + +def test_error_node_dummy_not_positive_time(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Dummy' + time: -0.2\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('Node0 time must be positive') + + +def test_error_node_dummy_no_output(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Dummy' + time: 0.2\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('Node0 must contain field: output') + + +def test_error_node_infer_no_model_path(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Infer'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + + error_msg = """Path to OpenVINO model must be specified in either of two formats: +1. + xml: path to *.xml + bin: path to *.bin +2. + blob: path to *.blob""" + assert out.startswith(error_msg) + + +def test_error_node_infer_no_input_layers(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Infer' + blob: model.blob + device: 'CPU'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + + assert out.startswith('Node0 must contain field: input_layers') + + +def test_error_node_infer_input_layers_are_empty(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Infer' + blob: model.blob + device: 'CPU' + input_layers: + \" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + + assert out.startswith('input_layers in Node0 must be a sequence') + + +def test_error_node_infer_no_output_layers(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Infer' + blob: model.blob + device: 'CPU' + input_layers: + - 'layer_name'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + + assert out.startswith('Node0 must contain field: output_layers') + + +def test_error_node_infer_output_layers_are_empty(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Infer' + blob: model.blob + device: 'CPU' + input_layers: + - 'layer_name' + output_layers:\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + + assert out.startswith('output_layers in Node0 must be a sequence') + + +def test_error_no_edges(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Dummy' + time: 0.2 + output: + dims: [1,2,3,4] + precision: 'U8'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + + assert out.startswith('PL1 must contain field: edges') + + +def test_error_edges_not_sequence(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Dummy' + time: 0.2 + output: + dims: [1,2,3,4] + precision: 'U8' + edges:\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + + assert out.startswith('edges in PL1 must be a sequence') + + +def test_error_edges_no_from(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Dummy' + time: 0.2 + output: + dims: [1,2,3,4] + precision: 'U8' + edges: + -\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + + assert out.startswith('edge must contain field: from') + + +def test_error_edges_no_to(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Dummy' + time: 0.2 + output: + dims: [1,2,3,4] + precision: 'U8' + edges: + - from: 'Node0'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + + assert out.startswith('edge must contain field: to') + + +def test_error_edges_from_not_exists(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Dummy' + time: 0.2 + output: + dims: [1,2,3,4] + precision: 'U8' + edges: + - from: 'Node1' + to: 'Node2'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + + assert out.startswith('Failed to find node: Node1') + + +def test_error_edges_from_port_not_exists(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Dummy' + time: 0.2 + output: + dims: [1,2,3,4] + precision: 'U8' + edges: + - from: 'Node0:10' + to: 'Node2'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + + assert out.startswith('Failed to access node: Node0 by out port: 10') + + +def test_error_edges_to_not_exists(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Dummy' + time: 0.2 + output: + dims: [1,2,3,4] + precision: 'U8' + edges: + - from: 'Src' + to: 'Node2'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + + assert out.startswith('Failed to find node: Node2') + + +def test_error_edges_to_port_not_exists(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Dummy' + time: 0.2 + output: + dims: [1,2,3,4] + precision: 'U8' + edges: + - from: 'Src' + to: 'Node0:3'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + + assert out.startswith('Failed to access node: Node0 by in port: 3') + + +def test_error_connect_to_source(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Dummy' + time: 0.2 + output: + dims: [1,2,3,4] + precision: 'U8' + edges: + - from: 'Node0' + to: 'Src'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + + assert out.startswith('Failed to access node: Src by in port: 0') + + +def test_error_double_edge(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Dummy' + time: 0.2 + output: + dims: [1,2,3,4] + precision: 'U8' + edges: + - from: 'Src' + to: 'Node0' + - from: 'Src' + to: 'Node0'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + + assert out.startswith('Node: Node0 already connected by in port: 0') + + +def test_error_double_edge(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Dummy' + time: 0.2 + output: + dims: [1,2,3,4] + precision: 'U8' + edges: + - from: 'Src' + to: 'Node0' + - from: 'Src' + to: 'Node0'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + + assert out.startswith('Node: Node0 already connected by in port: 0') + + +def test_node_has_dangling_input(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Dummy' + time: 0.2 + output: + dims: [1,2,3,4] + precision: 'U8' + + - name: 'Node1' + type: 'Dummy' + time: 0.2 + output: + dims: [1,2,3,4] + precision: 'U8' + edges: + - from: 'Node0' + to: 'Node1'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + + assert out.startswith('Node: Node0 in Pipeline: PL1 has dangling input by in port: 0') + + +def test_error_has_cycle_0(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node' + type: 'Infer' + blob: 'model.blob' + device: 'CPU' + input_layers: + - 'in_layer_name_0' + - 'in_layer_name_1' + output_layers: + - 'out_layer_name' + edges: + - from: 'Src' + to: 'Node:0' + - from: 'Node:0' + to: 'Node:1'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('Pipeline: PL1 has cyclic dependencies') + + +def test_error_has_cycle_0(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Infer' + blob: 'model.blob' + device: 'CPU' + input_layers: + - 'in_layer_name_0' + - 'in_layer_name_1' + output_layers: + - 'out_layer_name' + + - name: 'Node1' + type: 'Dummy' + time: 0.2 + output: + dims: [1,2,3,4] + precision: 'U8' + edges: + - from: 'Src' + to: 'Node0:0' + - from: 'Node0:0' + to: 'Node1:0' + - from: 'Node1' + to: 'Node0:1'\" """ + + exec_str = '{} --cfg={}'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('Pipeline: PL1 has cyclic dependencies') + + +def test_error_no_load_config_exists(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Dummy' + time: 0.2 + output: + dims: [1,2,3,4] + precision: 'U8' + edges: + - from: 'Src' + to: 'Node0'\" """ + + exec_str = '{} --cfg={} --load_config=not_existing.yml'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert 'Failed to load config: not_existing.yml' in out + + +def test_error_invalid_app_mode(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: + PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Dummy' + time: 0.2 + output: + dims: [1,2,3,4] + precision: 'U8' + edges: + - from: 'Src' + to: 'Node0'\" """ + + exec_str = '{} --cfg={} --pl_mode=unknown'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('Unsupported PLMode: unknown\n' + 'Please chose between: streaming and regular') + + +def test_error_invalid_pl_mode(): + cfg_file = """\"%YAML:1.0 +work_time: 1000 +Pipelines: +PL1: + source: + name: 'Src' + latency: 20 + output: + dims: [1,2,3,4] + precision: 'U8' + nodes: + - name: 'Node0' + type: 'Dummy' + time: 0.2 + output: + dims: [1,2,3,4] + precision: 'U8' + edges: + - from: 'Src' + to: 'Node0'\" """ + + exec_str = '{} --cfg={} --app_mode=unknown'.format(pipeline_modeling_tool, cfg_file) + out = get_output(exec_str) + assert out.startswith('Unsupported AppMode: unknown\n' + 'Please chose between: realtime and benchmark') diff --git a/modules/gapi/samples/pipeline_modeling_tool/utils.hpp b/modules/gapi/samples/pipeline_modeling_tool/utils.hpp new file mode 100644 index 0000000000..df5061b2fa --- /dev/null +++ b/modules/gapi/samples/pipeline_modeling_tool/utils.hpp @@ -0,0 +1,81 @@ +#ifndef OPENCV_GAPI_PIPELINE_MODELING_TOOL_UTILS_HPP +#define OPENCV_GAPI_PIPELINE_MODELING_TOOL_UTILS_HPP + +#include + +#if defined(_WIN32) +#include +#endif + +// FIXME: It's better to place it somewhere in common.hpp +struct OutputDescr { + std::vector dims; + int precision; +}; + +namespace utils { +inline void generateRandom(cv::Mat& out) { + switch (out.depth()) { + case CV_8U: + cv::randu(out, 0, 255); + break; + case CV_32F: + cv::randu(out, 0.f, 1.f); + break; + case CV_16F: { + cv::Mat fp32_mat(out.size(), CV_MAKETYPE(CV_32F, out.channels())); + cv::randu(fp32_mat, 0.f, 1.f); + fp32_mat.convertTo(out, out.type()); + break; + } + default: + throw std::logic_error("Unsupported preprocessing depth"); + } +} + +inline void sleep(double ms) { +#if defined(_WIN32) + // NB: It takes portions of 100 nanoseconds. + int64_t ns_units = static_cast(ms * 1e4); + // FIXME: Wrap it to RAII and instance only once. + HANDLE timer = CreateWaitableTimer(NULL, true, NULL); + if (!timer) { + throw std::logic_error("Failed to create timer"); + } + + LARGE_INTEGER li; + li.QuadPart = -ns_units; + if(!SetWaitableTimer(timer, &li, 0, NULL, NULL, false)){ + CloseHandle(timer); + throw std::logic_error("Failed to set timer"); + } + if (WaitForSingleObject(timer, INFINITE) != WAIT_OBJECT_0) { + CloseHandle(timer); + throw std::logic_error("Failed to wait timer"); + } + CloseHandle(timer); +#else + using namespace std::chrono; + std::this_thread::sleep_for(duration(ms)); +#endif +} + +template +typename duration_t::rep measure(std::function f) { + using namespace std::chrono; + auto start = high_resolution_clock::now(); + f(); + return duration_cast( + high_resolution_clock::now() - start).count(); +} + +template +typename duration_t::rep timestamp() { + using namespace std::chrono; + auto now = high_resolution_clock::now(); + return duration_cast(now.time_since_epoch()).count(); +} + +} // namespace utils + +#endif // OPENCV_GAPI_PIPELINE_MODELING_TOOL_UTILS_HPP