From 9bce73eb42bad4da7876f6bafa2521f3c411937e Mon Sep 17 00:00:00 2001 From: HailoRT-Automation <98901220+HailoRT-Automation@users.noreply.github.com> Date: Thu, 29 Jun 2023 15:02:42 +0300 Subject: [PATCH] v4.14.0 (#9) --- .hailort.jpg | Bin 0 -> 269975 bytes .hailort.png | Bin 1454787 -> 0 bytes CMakeLists.txt | 12 +- README.md | 8 +- common/include/context_switch_defs.h | 30 + common/include/control_protocol.h | 5 + common/include/d2h_events.h | 9 + common/include/firmware_status.h | 10 + common/include/utils.h | 18 + hailort/CMakeLists.txt | 31 +- hailort/common/CMakeLists.txt | 1 + hailort/common/barrier.hpp | 3 + hailort/common/device_measurements.cpp | 35 +- hailort/common/device_measurements.hpp | 7 + hailort/common/ethernet_utils.hpp | 7 +- hailort/common/event_internal.cpp | 73 ++ .../src/utils => common}/event_internal.hpp | 70 +- hailort/common/filesystem.hpp | 1 + hailort/common/latency_meter.hpp | 2 +- hailort/common/os/posix/ethernet_utils.cpp | 122 +- hailort/common/os/posix/filesystem.cpp | 7 + hailort/common/os/posix/os_utils.cpp | 49 +- hailort/common/os/posix/traffic_control.cpp | 13 +- hailort/common/os/posix/traffic_control.hpp | 1 - hailort/common/os/windows/ethernet_utils.cpp | 30 +- hailort/common/os/windows/filesystem.cpp | 7 + hailort/common/os/windows/os_utils.cpp | 49 + hailort/common/os_utils.hpp | 20 +- hailort/common/socket.hpp | 2 + hailort/common/utils.hpp | 23 +- hailort/drivers/common/hailo_ioctl_common.h | 21 +- hailort/hailort_service/CMakeLists.txt | 1 + .../hailort_service/hailort_rpc_service.cpp | 347 +++++- .../hailort_service/hailort_rpc_service.hpp | 30 + hailort/hailort_service/hailort_service | 1 - .../service_resource_manager.hpp | 86 +- .../windows/hailort_service_env_vars.bat | 3 +- hailort/hailortcli/CMakeLists.txt | 10 +- .../download_action_list_command.cpp | 17 + .../download_action_list_command.hpp | 8 +- hailort/hailortcli/hailortcli.cpp | 2 + hailort/hailortcli/hailortcli.hpp | 49 +- .../measure_nnc_performance_command.cpp | 118 ++ .../measure_nnc_performance_command.hpp | 33 + hailort/hailortcli/mon_command.hpp | 2 +- hailort/hailortcli/parse_hef_command.cpp | 2 +- hailort/hailortcli/run2/io_wrappers.cpp | 26 + hailort/hailortcli/run2/io_wrappers.hpp | 261 +++++ hailort/hailortcli/run2/live_printer.cpp | 86 -- hailort/hailortcli/run2/live_stats.cpp | 149 +++ .../run2/{live_printer.hpp => live_stats.hpp} | 29 +- .../run2/measurement_live_track.cpp | 48 +- .../run2/measurement_live_track.hpp | 12 +- .../hailortcli/run2/network_live_track.cpp | 92 +- .../hailortcli/run2/network_live_track.hpp | 22 +- hailort/hailortcli/run2/network_runner.cpp | 587 +++++++--- hailort/hailortcli/run2/network_runner.hpp | 323 +++++- hailort/hailortcli/run2/run2_command.cpp | 305 +++-- hailort/hailortcli/run2/run2_command.hpp | 73 +- hailort/hailortcli/run2/timer_live_track.cpp | 18 +- hailort/hailortcli/run2/timer_live_track.hpp | 9 +- hailort/hailortcli/run_command.hpp | 2 +- hailort/libhailort/CMakeLists.txt | 2 +- .../bindings/gstreamer/CMakeLists.txt | 8 +- .../bindings/gstreamer/gst-hailo/common.hpp | 2 +- .../gstreamer/gst-hailo/gsthailonet.cpp | 27 +- .../gstreamer/gst-hailo/gsthailonet.hpp | 3 +- .../gstreamer/gst-hailo/gsthailorecv.cpp | 6 +- .../gstreamer/gst-hailo/gsthailosend.cpp | 26 +- .../gst-hailo/network_group_handle.cpp | 7 +- .../gst-hailo/network_group_handle.hpp | 2 +- .../libhailort/bindings/python/CMakeLists.txt | 3 + .../bindings/python/externals/pybind11.cmake | 35 + .../platform/hailo_platform/__init__.py | 4 +- .../pyhailort/control_object.py | 8 +- .../hailo_platform/pyhailort/hw_object.py | 143 +-- .../hailo_platform/pyhailort/pyhailort.py | 176 +-- .../hailo_platform/tools/udp_rate_limiter.py | 35 +- .../notebooks/HRT_0_Inference_Tutorial.ipynb | 2 +- .../HRT_1_Power_Measurement_Tutorial.ipynb | 8 +- ...rence_Tutorial_Multi_Process_Service.ipynb | 130 +++ .../bindings/python/platform/requirements.txt | 6 +- .../bindings/python/platform/setup.py | 2 +- .../bindings/python/src/CMakeLists.txt | 43 +- .../bindings/python/src/bindings_common.hpp | 2 - .../bindings/python/src/device_api.cpp | 34 +- .../bindings/python/src/device_api.hpp | 3 +- .../bindings/python/src/hef_api.cpp | 129 ++- .../bindings/python/src/hef_api.hpp | 1 - .../src/internal/pyhailort_internal.cpp | 18 +- .../bindings/python/src/net_flow_api.hpp | 132 --- .../bindings/python/src/pyhailort.cpp | 115 +- .../bindings/python/src/quantization_api.cpp | 35 +- .../libhailort/bindings/python/src/utils.hpp | 8 +- .../bindings/python/src/vdevice_api.hpp | 16 +- .../bindings/python/src/vstream_api.cpp | 20 +- .../bindings/python/src/vstream_api.hpp | 2 - .../cmake/toolchains/toolchains.yaml | 6 +- hailort/libhailort/examples/CMakeLists.txt | 26 +- hailort/libhailort/examples/README.md | 22 +- hailort/libhailort/examples/c/CMakeLists.txt | 15 +- hailort/libhailort/examples/c/common/common.h | 9 +- .../examples/c/common/hailo_thread.h | 60 +- .../data_quantization_example/CMakeLists.txt | 2 +- .../data_quantization_example.c | 4 +- .../c/infer_pipeline_example/CMakeLists.txt | 2 +- .../infer_pipeline_example.c | 4 +- .../c/multi_device_example/CMakeLists.txt | 2 +- .../multi_device_example.c | 13 +- .../CMakeLists.txt | 2 +- .../multi_network_vstream_example.c | 4 +- .../CMakeLists.txt | 2 +- .../notification_callback_example.c | 2 +- .../power_measurement_example/CMakeLists.txt | 2 +- .../power_measurement_example.c | 2 +- .../CMakeLists.txt | 20 + .../raw_async_streams_single_thread_example.c | 253 +++++ .../c/raw_streams_example/CMakeLists.txt | 2 +- .../raw_streams_example/raw_streams_example.c | 4 +- .../CMakeLists.txt | 2 +- .../switch_network_groups_example.c | 26 +- .../CMakeLists.txt | 2 +- .../switch_network_groups_manually_example.c | 20 +- .../c/vstreams_example/CMakeLists.txt | 2 +- .../c/vstreams_example/vstreams_example.c | 23 +- .../libhailort/examples/cpp/CMakeLists.txt | 21 +- .../cpp/infer_pipeline_example/CMakeLists.txt | 2 +- .../cpp/multi_device_example/CMakeLists.txt | 2 +- .../multi_device_example.cpp | 32 +- .../CMakeLists.txt | 2 +- .../cpp/multi_process_example/CMakeLists.txt | 2 +- .../multi_process_example.cpp | 2 +- .../CMakeLists.txt | 2 +- .../notification_callback_example.cpp | 11 +- .../power_measurement_example/CMakeLists.txt | 2 +- .../raw_async_streams_example/CMakeLists.txt | 19 - .../raw_async_streams_example/buffer_pool.cpp | 76 -- .../raw_async_streams_example/buffer_pool.hpp | 50 - .../raw_async_streams_example.cpp | 146 --- .../CMakeLists.txt | 19 + ...raw_async_streams_multi_thread_example.cpp | 164 +++ .../CMakeLists.txt | 19 + ...aw_async_streams_single_thread_example.cpp | 193 ++++ .../cpp/raw_streams_example/CMakeLists.txt | 2 +- .../raw_streams_example.cpp | 11 +- .../CMakeLists.txt | 2 +- .../switch_network_groups_example.cpp | 36 +- .../CMakeLists.txt | 2 +- .../cpp/vstreams_example/CMakeLists.txt | 2 +- .../cpp/vstreams_example/vstreams_example.cpp | 44 +- hailort/libhailort/hef.proto | 111 +- hailort/libhailort/include/hailo/buffer.hpp | 42 +- .../include/hailo/buffer_storage.hpp | 240 ++++ hailort/libhailort/include/hailo/device.hpp | 47 +- .../include/hailo/dma_mapped_buffer.hpp | 76 -- hailort/libhailort/include/hailo/event.hpp | 34 +- hailort/libhailort/include/hailo/expected.hpp | 1 + hailort/libhailort/include/hailo/hailort.h | 368 +++++- hailort/libhailort/include/hailo/hailort.hpp | 1 - .../include/hailo/hailort_common.hpp | 26 +- .../include/hailo/hailort_defaults.hpp | 2 +- hailort/libhailort/include/hailo/hef.hpp | 4 +- .../include/hailo/inference_pipeline.hpp | 1 + .../include/hailo/network_group.hpp | 21 +- .../include/hailo/network_rate_calculator.hpp | 6 + .../libhailort/include/hailo/quantization.hpp | 74 +- .../include/hailo/runtime_statistics.hpp | 2 +- hailort/libhailort/include/hailo/stream.hpp | 296 ++++- .../libhailort/include/hailo/transform.hpp | 3 +- hailort/libhailort/include/hailo/vdevice.hpp | 1 + hailort/libhailort/include/hailo/vstream.hpp | 11 + hailort/libhailort/src/CMakeLists.txt | 9 +- hailort/libhailort/src/core_op/CMakeLists.txt | 7 +- hailort/libhailort/src/core_op/core_op.cpp | 171 --- hailort/libhailort/src/core_op/core_op.hpp | 29 +- .../resource_manager/config_buffer.cpp | 68 +- .../resource_manager/config_buffer.hpp | 3 +- .../context_switch_buffer_builder.cpp | 5 + .../context_switch_buffer_builder.hpp | 1 + .../resource_manager/ddr_channels_pair.cpp | 140 --- .../resource_manager/ddr_channels_pair.hpp | 68 -- .../resource_manager/inter_context_buffer.cpp | 166 --- .../resource_manager/inter_context_buffer.hpp | 54 - .../resource_manager/intermediate_buffer.cpp | 196 ++++ .../resource_manager/intermediate_buffer.hpp | 65 ++ .../resource_manager/resource_manager.cpp | 296 +++-- .../resource_manager/resource_manager.hpp | 79 +- .../resource_manager_builder.cpp | 448 +++++--- .../libhailort/src/device_common/control.cpp | 538 ++------- .../libhailort/src/device_common/control.hpp | 19 +- .../src/device_common/control_protocol.cpp | 24 +- .../src/device_common/control_protocol.hpp | 4 +- .../src/device_common/d2h_events_parser.cpp | 23 +- .../libhailort/src/device_common/device.cpp | 51 +- .../src/device_common/device_internal.cpp | 5 +- .../src/device_common/device_internal.hpp | 8 + hailort/libhailort/src/eth/eth_device.cpp | 27 +- hailort/libhailort/src/eth/eth_device.hpp | 1 - hailort/libhailort/src/eth/eth_stream.cpp | 35 +- hailort/libhailort/src/eth/eth_stream.hpp | 18 +- .../libhailort/src/eth/hcp_config_core_op.cpp | 6 + .../libhailort/src/eth/hcp_config_core_op.hpp | 1 + .../src/eth/network_rate_calculator.cpp | 39 + hailort/libhailort/src/hailort.cpp | 318 +++++- hailort/libhailort/src/hailort_defaults.cpp | 8 + .../src/hef/context_switch_actions.cpp | 157 ++- .../src/hef/context_switch_actions.hpp | 53 +- .../libhailort/src/hef/core_op_metadata.cpp | 255 +++-- .../libhailort/src/hef/core_op_metadata.hpp | 148 ++- hailort/libhailort/src/hef/hef.cpp | 814 +++++++++---- hailort/libhailort/src/hef/hef_internal.hpp | 87 +- hailort/libhailort/src/hef/layer_info.hpp | 114 +- hailort/libhailort/src/hw_consts.hpp | 3 + hailort/libhailort/src/mipi/mipi_stream.cpp | 10 +- hailort/libhailort/src/mipi/mipi_stream.hpp | 4 +- .../libhailort/src/net_flow/CMakeLists.txt | 10 +- .../src/net_flow/ops/argmax_post_process.cpp | 219 ++++ .../src/net_flow/ops/argmax_post_process.hpp | 151 +++ .../src/net_flow/ops/nms_post_process.cpp | 45 +- .../src/net_flow/ops/nms_post_process.hpp | 6 +- hailort/libhailort/src/net_flow/ops/op.hpp | 12 + .../src/net_flow/ops/softmax_post_process.cpp | 192 ++++ .../src/net_flow/ops/softmax_post_process.hpp | 139 +++ .../src/net_flow/ops/ssd_post_process.cpp | 30 +- .../src/net_flow/ops/ssd_post_process.hpp | 3 +- .../src/net_flow/ops/yolo_post_process.cpp | 48 +- .../src/net_flow/ops/yolo_post_process.hpp | 30 +- .../src/net_flow/ops/yolox_post_process.cpp | 132 +++ .../src/net_flow/ops/yolox_post_process.hpp | 175 +++ .../src/net_flow/pipeline/pipeline.cpp | 42 +- .../src/net_flow/pipeline/pipeline.hpp | 18 +- .../src/net_flow/pipeline/vstream.cpp | 748 ++++++++++-- .../net_flow/pipeline/vstream_internal.hpp | 96 +- .../src/network_group/network_group.cpp | 263 +++-- .../network_group/network_group_internal.hpp | 33 +- hailort/libhailort/src/os/CMakeLists.txt | 19 +- hailort/libhailort/src/os/hailort_driver.hpp | 77 +- hailort/libhailort/src/os/mmap_buffer.hpp | 17 + .../libhailort/src/os/posix/CMakeLists.txt | 16 + .../src/os/posix/hailort_driver.cpp | 191 +++- .../src/os/posix/linux/CMakeLists.txt | 8 + .../os/posix/{unix => linux}/driver_scan.cpp | 0 .../src/os/posix/{unix => linux}/event.cpp | 94 +- .../libhailort/src/os/posix/mmap_buffer.cpp | 28 +- .../src/os/posix/qnx/CMakeLists.txt | 8 + hailort/libhailort/src/os/posix/qnx/event.cpp | 100 +- .../libhailort/src/os/windows/CMakeLists.txt | 13 + .../libhailort/src/os/windows/driver_scan.cpp | 2 +- hailort/libhailort/src/os/windows/event.cpp | 77 +- .../src/os/windows/hailort_driver.cpp | 102 +- .../libhailort/src/os/windows/mmap_buffer.cpp | 19 +- .../src/os/windows/virtual_alloc_guard.cpp | 48 + .../src/os/windows/virtual_alloc_guard.hpp | 45 + .../src/service/hailort_rpc_client.cpp | 265 ++++- .../src/service/hailort_rpc_client.hpp | 31 +- .../src/service/network_group_client.cpp | 45 +- .../src/service/rpc_client_utils.hpp | 86 +- .../src/stream_common/CMakeLists.txt | 1 + .../src/stream_common/async_common.hpp | 31 + .../src/stream_common/nms_stream_reader.cpp | 300 +++++ .../src/stream_common/nms_stream_reader.hpp | 47 + .../libhailort/src/stream_common/stream.cpp | 84 +- .../src/stream_common/stream_internal.cpp | 72 ++ .../src/stream_common/stream_internal.hpp | 62 +- .../libhailort/src/transform/transform.cpp | 89 +- .../src/transform/transform_internal.hpp | 26 + hailort/libhailort/src/utils/CMakeLists.txt | 1 + hailort/libhailort/src/utils/buffer.cpp | 90 +- .../libhailort/src/utils/buffer_storage.cpp | 345 ++++++ .../src/utils/exported_resource_manager.hpp | 94 ++ .../libhailort/src/utils/hailort_common.cpp | 2 - .../libhailort/src/utils/hailort_logger.cpp | 46 +- .../libhailort/src/utils/hailort_logger.hpp | 22 +- .../src/utils/profiler/CMakeLists.txt | 2 + .../libhailort/src/utils/profiler/handler.hpp | 199 ++++ .../src/utils/profiler/monitor_handler.cpp | 341 ++++++ .../src/utils/profiler/monitor_handler.hpp | 135 +++ .../profiler/scheduler_profiler_handler.cpp | 219 ++++ .../profiler/scheduler_profiler_handler.hpp | 49 + .../libhailort/src/utils/profiler/tracer.cpp | 214 +--- .../libhailort/src/utils/profiler/tracer.hpp | 191 +--- .../src/utils/shared_resource_manager.hpp | 1 + .../src/utils/thread_safe_queue.hpp | 3 +- hailort/libhailort/src/vdevice/CMakeLists.txt | 5 +- .../src/vdevice/callback_reorder_queue.cpp | 74 ++ .../src/vdevice/callback_reorder_queue.hpp | 91 ++ .../src/vdevice/pipeline_multiplexer.cpp | 10 +- .../src/vdevice/pipeline_multiplexer.hpp | 6 +- .../multi_device_scheduled_stream.cpp | 65 +- .../multi_device_scheduled_stream.hpp | 20 +- .../scheduler/network_group_scheduler.cpp | 1006 ----------------- .../scheduler/network_group_scheduler.hpp | 144 --- .../scheduler/scheduled_core_op_cv.hpp | 2 - .../scheduler/scheduled_core_op_state.cpp | 184 +-- .../scheduler/scheduled_core_op_state.hpp | 74 +- .../vdevice/scheduler/scheduled_stream.cpp | 361 ++++++ .../vdevice/scheduler/scheduled_stream.hpp | 225 +++- .../src/vdevice/scheduler/scheduler.cpp | 778 +++++++++++++ .../src/vdevice/scheduler/scheduler.hpp | 121 ++ .../src/vdevice/scheduler/scheduler_base.hpp | 47 +- .../src/vdevice/scheduler/scheduler_mon.hpp | 61 - .../vdevice/scheduler/scheduler_oracle.cpp | 105 +- .../vdevice/scheduler/scheduler_oracle.hpp | 12 +- hailort/libhailort/src/vdevice/vdevice.cpp | 163 ++- .../src/vdevice/vdevice_core_op.cpp | 144 ++- .../src/vdevice/vdevice_core_op.hpp | 29 +- .../src/vdevice/vdevice_internal.hpp | 23 +- .../src/vdevice/vdevice_native_stream.cpp | 259 +++++ .../src/vdevice/vdevice_native_stream.hpp | 104 +- .../libhailort/src/vdevice/vdevice_stream.cpp | 512 ++------- .../libhailort/src/vdevice/vdevice_stream.hpp | 114 +- .../vdevice_stream_multiplexer_wrapper.cpp | 160 +-- .../vdevice_stream_multiplexer_wrapper.hpp | 35 +- hailort/libhailort/src/vdma/CMakeLists.txt | 6 +- .../src/vdma/channel/async_channel.cpp | 138 ++- .../src/vdma/channel/async_channel.hpp | 14 +- .../src/vdma/channel/boundary_channel.cpp | 187 +-- .../src/vdma/channel/boundary_channel.hpp | 46 +- .../src/vdma/channel/buffered_channel.cpp | 86 +- .../src/vdma/channel/buffered_channel.hpp | 17 +- .../src/vdma/channel/channel_base.cpp | 19 +- .../src/vdma/channel/channel_base.hpp | 1 + .../src/vdma/channel/channel_id.hpp | 6 + .../src/vdma/channel/channel_state.cpp | 8 +- .../src/vdma/channel/channel_state.hpp | 33 +- .../vdma/channel/interrupts_dispatcher.cpp | 98 +- .../vdma/channel/interrupts_dispatcher.hpp | 33 +- .../src/vdma/integrated/integrated_device.cpp | 14 +- .../src/vdma/integrated/integrated_device.hpp | 7 +- .../src/vdma/memory/buffer_requirements.cpp | 146 +++ .../src/vdma/memory/buffer_requirements.hpp | 59 + .../src/vdma/memory/continuous_buffer.cpp | 40 +- .../src/vdma/memory/continuous_buffer.hpp | 8 +- .../src/vdma/memory/descriptor_list.cpp | 213 +--- .../src/vdma/memory/descriptor_list.hpp | 94 +- .../src/vdma/memory/dma_able_buffer.cpp | 267 +++++ .../src/vdma/memory/dma_able_buffer.hpp | 54 + .../src/vdma/memory/dma_mapped_buffer.cpp | 96 -- .../src/vdma/memory/mapped_buffer.cpp | 125 ++ .../src/vdma/memory/mapped_buffer.hpp | 81 ++ .../src/vdma/memory/mapped_buffer_factory.cpp | 30 - .../src/vdma/memory/mapped_buffer_factory.hpp | 33 - .../src/vdma/memory/mapped_buffer_impl.cpp | 279 ----- .../src/vdma/memory/mapped_buffer_impl.hpp | 98 -- .../libhailort/src/vdma/memory/sg_buffer.cpp | 49 +- .../libhailort/src/vdma/memory/sg_buffer.hpp | 22 +- .../src/vdma/memory/vdma_buffer.hpp | 4 +- .../libhailort/src/vdma/pcie/pcie_device.cpp | 37 +- .../libhailort/src/vdma/pcie/pcie_device.hpp | 13 +- .../libhailort/src/vdma/vdma_async_stream.cpp | 272 ++++- .../libhailort/src/vdma/vdma_async_stream.hpp | 69 +- .../src/vdma/vdma_config_core_op.cpp | 7 +- .../src/vdma/vdma_config_core_op.hpp | 1 + .../src/vdma/vdma_config_manager.cpp | 11 +- .../src/vdma/vdma_config_manager.hpp | 4 +- hailort/libhailort/src/vdma/vdma_device.cpp | 56 +- hailort/libhailort/src/vdma/vdma_device.hpp | 16 +- hailort/libhailort/src/vdma/vdma_stream.cpp | 76 +- hailort/libhailort/src/vdma/vdma_stream.hpp | 23 +- .../libhailort/src/vdma/vdma_stream_base.cpp | 100 +- .../libhailort/src/vdma/vdma_stream_base.hpp | 20 +- hailort/pre_build/external/CMakeLists.txt | 2 +- hailort/rpc/hailort_rpc.proto | 77 ++ hailort/scripts/download_firmware_eth.cmd | 2 +- hailort/scripts/download_firmware_eth.sh | 2 +- hailort/scripts/download_hefs.cmd | 6 +- hailort/scripts/download_hefs.sh | 5 +- .../tools/hailo15-scripts/hailo15_env_vars.sh | 8 + hailort/tools/hailo15-scripts/load_driver.sh | 13 + .../tools/hailo15-scripts/load_firmware.sh | 11 + hailort/tools/hailo15-scripts/load_hrt.sh | 14 + hailort/tools/hailo15-scripts/load_pcr.sh | 12 + hailort/tools/hailo15-scripts/read_log.sh | 15 + hailort/tools/hailo15-scripts/sanity_infer.sh | 8 + .../hailo15-scripts/update_hrt_and_infer.sh | 23 + hailort/tools/hw_debug/CMakeLists.txt | 7 +- hailort/tools/hw_debug/main.cpp | 8 +- 377 files changed, 17923 insertions(+), 8637 deletions(-) create mode 100644 .hailort.jpg delete mode 100644 .hailort.png create mode 100644 hailort/common/event_internal.cpp rename hailort/{libhailort/src/utils => common}/event_internal.hpp (52%) create mode 100644 hailort/hailortcli/measure_nnc_performance_command.cpp create mode 100644 hailort/hailortcli/measure_nnc_performance_command.hpp create mode 100644 hailort/hailortcli/run2/io_wrappers.cpp create mode 100644 hailort/hailortcli/run2/io_wrappers.hpp delete mode 100644 hailort/hailortcli/run2/live_printer.cpp create mode 100644 hailort/hailortcli/run2/live_stats.cpp rename hailort/hailortcli/run2/{live_printer.hpp => live_stats.hpp} (55%) create mode 100644 hailort/libhailort/bindings/python/externals/pybind11.cmake create mode 100644 hailort/libhailort/bindings/python/platform/hailo_tutorials/notebooks/HRT_2_Inference_Tutorial_Multi_Process_Service.ipynb delete mode 100644 hailort/libhailort/bindings/python/src/net_flow_api.hpp create mode 100644 hailort/libhailort/examples/c/raw_async_streams_single_thread_example/CMakeLists.txt create mode 100644 hailort/libhailort/examples/c/raw_async_streams_single_thread_example/raw_async_streams_single_thread_example.c delete mode 100644 hailort/libhailort/examples/cpp/raw_async_streams_example/CMakeLists.txt delete mode 100644 hailort/libhailort/examples/cpp/raw_async_streams_example/buffer_pool.cpp delete mode 100644 hailort/libhailort/examples/cpp/raw_async_streams_example/buffer_pool.hpp delete mode 100644 hailort/libhailort/examples/cpp/raw_async_streams_example/raw_async_streams_example.cpp create mode 100644 hailort/libhailort/examples/cpp/raw_async_streams_multi_thread_example/CMakeLists.txt create mode 100644 hailort/libhailort/examples/cpp/raw_async_streams_multi_thread_example/raw_async_streams_multi_thread_example.cpp create mode 100644 hailort/libhailort/examples/cpp/raw_async_streams_single_thread_example/CMakeLists.txt create mode 100644 hailort/libhailort/examples/cpp/raw_async_streams_single_thread_example/raw_async_streams_single_thread_example.cpp create mode 100644 hailort/libhailort/include/hailo/buffer_storage.hpp delete mode 100644 hailort/libhailort/include/hailo/dma_mapped_buffer.hpp delete mode 100644 hailort/libhailort/src/core_op/resource_manager/ddr_channels_pair.cpp delete mode 100644 hailort/libhailort/src/core_op/resource_manager/ddr_channels_pair.hpp delete mode 100644 hailort/libhailort/src/core_op/resource_manager/inter_context_buffer.cpp delete mode 100644 hailort/libhailort/src/core_op/resource_manager/inter_context_buffer.hpp create mode 100644 hailort/libhailort/src/core_op/resource_manager/intermediate_buffer.cpp create mode 100644 hailort/libhailort/src/core_op/resource_manager/intermediate_buffer.hpp create mode 100644 hailort/libhailort/src/net_flow/ops/argmax_post_process.cpp create mode 100644 hailort/libhailort/src/net_flow/ops/argmax_post_process.hpp create mode 100644 hailort/libhailort/src/net_flow/ops/softmax_post_process.cpp create mode 100644 hailort/libhailort/src/net_flow/ops/softmax_post_process.hpp create mode 100644 hailort/libhailort/src/net_flow/ops/yolox_post_process.cpp create mode 100644 hailort/libhailort/src/net_flow/ops/yolox_post_process.hpp create mode 100644 hailort/libhailort/src/os/posix/CMakeLists.txt create mode 100644 hailort/libhailort/src/os/posix/linux/CMakeLists.txt rename hailort/libhailort/src/os/posix/{unix => linux}/driver_scan.cpp (100%) rename hailort/libhailort/src/os/posix/{unix => linux}/event.cpp (65%) create mode 100644 hailort/libhailort/src/os/posix/qnx/CMakeLists.txt create mode 100644 hailort/libhailort/src/os/windows/CMakeLists.txt create mode 100644 hailort/libhailort/src/os/windows/virtual_alloc_guard.cpp create mode 100644 hailort/libhailort/src/os/windows/virtual_alloc_guard.hpp create mode 100644 hailort/libhailort/src/stream_common/async_common.hpp create mode 100644 hailort/libhailort/src/stream_common/nms_stream_reader.cpp create mode 100644 hailort/libhailort/src/stream_common/nms_stream_reader.hpp create mode 100644 hailort/libhailort/src/utils/buffer_storage.cpp create mode 100644 hailort/libhailort/src/utils/exported_resource_manager.hpp create mode 100644 hailort/libhailort/src/utils/profiler/handler.hpp create mode 100644 hailort/libhailort/src/utils/profiler/monitor_handler.cpp create mode 100644 hailort/libhailort/src/utils/profiler/monitor_handler.hpp create mode 100644 hailort/libhailort/src/utils/profiler/scheduler_profiler_handler.cpp create mode 100644 hailort/libhailort/src/utils/profiler/scheduler_profiler_handler.hpp create mode 100644 hailort/libhailort/src/vdevice/callback_reorder_queue.cpp create mode 100644 hailort/libhailort/src/vdevice/callback_reorder_queue.hpp delete mode 100644 hailort/libhailort/src/vdevice/scheduler/network_group_scheduler.cpp delete mode 100644 hailort/libhailort/src/vdevice/scheduler/network_group_scheduler.hpp create mode 100644 hailort/libhailort/src/vdevice/scheduler/scheduled_stream.cpp create mode 100644 hailort/libhailort/src/vdevice/scheduler/scheduler.cpp create mode 100644 hailort/libhailort/src/vdevice/scheduler/scheduler.hpp delete mode 100644 hailort/libhailort/src/vdevice/scheduler/scheduler_mon.hpp create mode 100644 hailort/libhailort/src/vdevice/vdevice_native_stream.cpp create mode 100644 hailort/libhailort/src/vdma/memory/buffer_requirements.cpp create mode 100644 hailort/libhailort/src/vdma/memory/buffer_requirements.hpp create mode 100644 hailort/libhailort/src/vdma/memory/dma_able_buffer.cpp create mode 100644 hailort/libhailort/src/vdma/memory/dma_able_buffer.hpp delete mode 100644 hailort/libhailort/src/vdma/memory/dma_mapped_buffer.cpp create mode 100644 hailort/libhailort/src/vdma/memory/mapped_buffer.cpp create mode 100644 hailort/libhailort/src/vdma/memory/mapped_buffer.hpp delete mode 100644 hailort/libhailort/src/vdma/memory/mapped_buffer_factory.cpp delete mode 100644 hailort/libhailort/src/vdma/memory/mapped_buffer_factory.hpp delete mode 100644 hailort/libhailort/src/vdma/memory/mapped_buffer_impl.cpp delete mode 100644 hailort/libhailort/src/vdma/memory/mapped_buffer_impl.hpp create mode 100644 hailort/tools/hailo15-scripts/hailo15_env_vars.sh create mode 100755 hailort/tools/hailo15-scripts/load_driver.sh create mode 100755 hailort/tools/hailo15-scripts/load_firmware.sh create mode 100755 hailort/tools/hailo15-scripts/load_hrt.sh create mode 100755 hailort/tools/hailo15-scripts/load_pcr.sh create mode 100755 hailort/tools/hailo15-scripts/read_log.sh create mode 100755 hailort/tools/hailo15-scripts/sanity_infer.sh create mode 100755 hailort/tools/hailo15-scripts/update_hrt_and_infer.sh diff --git a/.hailort.jpg b/.hailort.jpg new file mode 100644 index 0000000000000000000000000000000000000000..84d29889cdfb658fd5188efd085f448f85ea38b2 GIT binary patch literal 269975 zcmeFa2Ut|e5-@s(A&4Xu1qBHbP2>y`6af(tBnOGZ00YAeFhdqFCsft|Cg_r34g$~o2D)zwwi)zy8v`ZP&|!2Lb6cJ4R?39nne?{&c2qj~k6@kBhq8Sl<3DJ?3&yIt1~g~7ooJ0MAM zNhw8f8AVA6UI_rmDoRNLI>S>zJ9>ZDeL< z-UwfqH!c=g|91#1Jb~7)feu1FWa2th?n zLrb@Yek}tqfFCmv5EXJca~*`BMj)xEsc7hE*U%str9kF-B=xop5;S{{*s=0?oRFm5 zc=hh1ef$EO44&FcNqe5GQKu8UW;ohCh9bzY-OD^BwEw!1gT}KsqAYu@@dxzRtkciy zygrU=3g3u7lO6Wr{ilihIWOybCQTf@&xR-DzH0b9C8uTThtXb9GIP`IhQYZhS9wo8T|b?cI+E6o--B+9aL(hDHsU$^(eBl)Wx3N0)41 zVnE34%?*LE6Rm9v(C7EeZo}=5&lP;GJ=*L3Bl@zul}ciywrAC6%uDRB*{>cl@r0*+-lpBTmsoYTt`a4C zN-vcmnV3`Qi)kW<8w~97Nmzhf$@4Ew)OxI{`$H?^*f*nUE?hp{%Bdm$;Wc+kvE}XQ z9;@qww%$B4--A}}c8(9tpBV*2bF@cM{4WIgU+f??2FLq!7)+)wKnF%1-@6>ZlD7cu zwfk9SeA>U0$;W^fK%nApa>Vo%yO!1}jqKQ2Z8x<5ec3JGdph3tjH<=R#Fg1{>zvL$ zuIU?L87p}=$Q1OQ0@t*Kv_IXrp0e<6{DqHQTVvBROBwyPh=F;gr=eKPTyNj4K{dsb z*}MDG14I&RzNl`xA(^#qx2ub5>U`ptK?|zFoXoAq%Z^j29$bKmS;$NeB+@$?%<2x0 z>R7oRedm3d6kre}GW00yY!nX~TacE@&Xs|G(9WUi)0ZaJy7{vEL`qIhh!x6mDEyqU zqiSN&`+(G@0Zx+X0wjM@@vKDrd)<1@?e|i4r^*qmLgzy#E_E7LE zerhh;cdiAU@$Ke26md4wtW2`}ChlRKuD5(nb$~kh(v(7J_m+u{s6nw=xt3xVjbobQ zMaS?H-h=Z#E`^G%fx(7#?&w=4orJ*q9nop&`N^G$*6QBM^Kr^z{?+W_wt+X0`8NIM zFr420N65rb7G1+FPQvfyWv;}^MySXyK&QU!11ikjcuia2K2YWkZGmeC(dMpL*Jvg6 z6A~53^k(hK7NTTbir)DJ$R_??qP|!F2Fh>gYLK}Tfmf8Oa}BHtD7jKytxZ=%J%sDc zDSfOqTOk^CKhT&eimUPA&`ZCxDSOhbrf-J>BI)j*A3T(+x-BMaIw+GIJLW2!G!b*Z z)@s1PuKj{>C_i_jyv38ck))j78dA>m-~zO!GFE*izD8VE;FN{J8$mw5dVwC*g_`k~ zyUhDeJn;T}PVJN4=f?pzj0SCS@5<;RAnz;cq2J^*KL^(?K$j-!r&#mr>)((Hyi66c zdkno~h9~ht9F-Xpf%B&_qeyzaJ0?12Z897DjXO=_Pnhm6tTyo6j!BzklrMP1*-GeW zn7@2wI_SdCtmz!pE)p#b4nfvDd337L^5*8L2z8C2srFJgZ&Fz1$mimn10~fujr*MT zyQXJ`NRFj=2_@tVtCaaaQHbL8zuYpNRo|KQjx3yE^G*8Qv0$3FRyFt(j<%~;L(xWq z?T?1rblUr#@5(If9GbT-@0_Qfzb9*2Kj56!kS7$;P&pftC#xgu>bje)5gon&-RtZ( zp68t4NmMWU^4`*ltz_s@&haSktU5L`Z{y3B(J1SImZ6H$hFK-mZe_8$Z9%Wo8c4=A zx<2v+sb!DZ+O{?tmG>3oWR_dH&FA))>SRsGl47oze*Cucnj>-dW6ch1?z!8JoNlgL zKd1}lJR&u4l5_?$cxvYZ>a!-oNH+KN-YfEtrW8&Am2RwRyjd}^i=mM2<&4Uw}!Ow%WCR0hq6%b)o+zZUe+6d$^#UFG*$`aIl1 zUE@NmE)2B~bq-GE;^tky_$MR#r}kFFZMHdr+v|ZoMY!@x;Szx?Br}{h(>cnWhMo9o z?p86&t(UN~P*%Ak#WF%W=SoIxEeB~Zt+BA#r}#?Z1U5IV^d4EMbFR9CKkogA)hV&f z9N106*E7r3RhGo&1yvhVmOA@<$ql-?OL+mJ5mGh%8fjU?&*>V;m2bg|ckR5gwU@S$ zWOJ~(wqbThMs40iY$fT!0%V$HZa5@c>n+>J(KGb$VsJEhQ;tM^(Ri*^Ur=aW)|@hT zd@89}FSVg2_F{jUA6hi4(yeVa|LZH(VaNHzOtNW>Kn&$L%!ZV8EQvXb7>h&rD#*V6C| z;+5FzxQaRKAft}+!$F**hH)uDjV#=p8HL)N(mB-eLL0K!IYDq0dGs{Qw7=;CM}v5T zLX}6$E16W%;eZHZD_=5m)Ijop#9`%AJV}##JV^Bgy!gC(L>;VZ^2ZN(B=6vFrt?=9 zpgXgoBITE=;(#L**E{m&S-&bOMOZQPQDAh1+s$t?Tck<3wuV}z4X=``55Ej+ELGP# z@YK%dzSiM;-#p&6-YQZO8W~V}pB&qdYKRkfeWEUq9bt0BVG?Wlu|4EM?ZdI;!o4{| zL7Mus%AIa+9uDQr5K%P?5L?up&yF8@GcWCl95cT$a=5`k`$evLWI5qWD_SpMs3SB9 z9eig}*~TQc*9g;R`%!9VD@**~u}cwbOdLDNOwv(SBC{F=diV1TvnHh5^+*oPcTc-# z#$N2jpUr8xe1+;&h_X_0g#VUJ!_9$8rJ57->-QrHpN0z=G2a7Yeasd`WlPBTK;=u< zq-TYVAJKMJkBe9(W4-dl@^f07vq?>I_(^3RlaNyFQ2RH&&>E&w?eVdL(Ve#oad(4T z3J)7k%LO*K^CKTvp-&f-3A67e;*TnK#$LTR8>lxCliX8#Lcq0d|IOoA=LKk?7Pvu+ zSBOWgJ$Leh`pA{fTNa?f2T4u4IKAa>L8aO)!SXC|{lRH2vn={w%xlHX(P2doYG3En zH+-UJpb&;oRZEY+Vf;ew2NC;ICCOjG4#Y-a7>B($oa#p(I<<$| zdJQVW1BR13zwsOmpP0YGgK>0EPJWvUeYPIppzR-YN}R$UuE?k;BLo{SKsn*{{T~y# zgzvB0su)M3S*K5EYPgAUwI;`8#g3qgUAX3|6UjpT*0+L$2ajGN-e!t08;(hNq;1hm z#=-u~1*oQT3iyYieI9j%JJ?uS_E~V4J7ps|mGL#hoqZ#d%3`yCF@(#RXw`V~{hY@7 z%{u~^Vp47%&~OipHx3W2$YS!^qa+WQA2Xu z*}>NtGkUX1%6fhH*JN|%s_W-lY*30ARPA0!-V-B|@4D~an zni5jbho~A0AIeF#wDsyY23?;BSb(AyAbJaa|6mQb?98frb%wW4nWDDc9Staw%%i;K z;ehF;x3>mJn=DC+%I9q^%xcsgIFJzbGd1Oiye){xdnNPUW(t)m(3LY0S%c*ZMbP!A_^L`H3zgBZ!Lj z1UpB$>+AH2izPMcTbXX)Wk&Gc{uOPc()9NNU3%^++wZoN4FvMDCUy1N6Q5tORz5d$ zc~fh$UGemUMj* zDJlGeGeOZ2f#IcD{-xc4DnS~yZ21bk`uk^tt9Yt{--{fMXt314rYJQ868CI9lilswMsH_@ggOmf({cy6a+6c=g@YAE(Q{SGs)yh5ko9^^$TM8R68EOt zYcgYLrmSvVh&tovp%gn%S5s<`GaEZG5O{G*pi_K(=ogv6s`%q$b>hz6EQcqdDOHsL z@5#d{ead0xeU^O-Q1^{XJr?_0j&n5*m}fWET5p%q(|1&1KjivkQq^%{ykI87wuuz% zGAWzePd*s@6fJ6JUj3$26!hLtW+Isld4x!$MSGm>-LOZgXx+O}X63GA&RxdW?{cp{ zP*lHAH~!+%s|x}dj}-H~L@#-AyYwB}$iC+xJMH7_aMEri&GN|dNK&u{311tS*f49; zw=vjx;&M8s&rR^g;FQh6r*F!!0DgIQxgPd$` zaiMyJyzh{U)8JJ2`K!ZRC$3MZUc2=;wtoRi|MX^8isDm&?EUf^_VO|z+(|akv&n+- z^xGsop3VrQXDl=?3~{XazG@;RDh|o-)7GVi*IFP*a4W(BLUIafZ3SyG;Qv-C8fd)} zhUn~NZ{vu;*C_INy&f}xEgqR}`nH#h@^;k!E46OOY`${J#PVT^qk zB3uT}UZjckJZ$HNHqtON0!fRNEf&!C#N!#cL4wHh`y02h^7=_F}ED^WF5u?2q21~@?C?r8XJq24`*IZ{&8%i($=>h46`vJgN>d(V}KffGS@C8F+$Q~w^p5x++c*<%SSIftEy-YXFe zH|+kE2+-qq()&=ZPD_ed)DmQ%wND*z125P?1`fQu4tOtnHGCHwr35Wc+#k~RVgHD? z&(p!k7)R9PH^9PeyAt+c9e971Za}~iDe3yYST%zc^tIk-6cO*a-;QVpOF&^#-$|da zY!xsJBZE84HgpK>xJc%A)f$2Iz@JrXh{7(W>wBWq%wYkH!L3Kv{Zy~IR|E8RXWphx&g+?d36TEat7dS1t0D!$e~3ZmR1L; zmhY5)Bt*$$UaAz_RF6Z8_Qpg>gL|z71#`DexL90qA2`-CTfuj(-vP`Q609MEe!T{gn47`s$WCOer z2Esvh5Ej4$0Q&>aBmzkR%%zSBpzmQ-8cc0vy?O;>6~ao|l1OkI=Y@s6C#)D+d%PFU zf$%%8fFeqOJZkU zNZtVc=qM5G{j2JtbMnM{x&M$vi}%DhVOD&yh9OK{kCFyxFxhz#@mgpc+S87Rb^r;G zk)QkWkg|3W8BT$7wB4L|!R`OF{75fP><Hd#+OM`bGNtv{o`e5$G@;2kV-ih<69WJ^{VjAFjm$$N7g;Mth2* z{X;qfXkzEzqf>hKcDN!it^;#c9i_mF{SV$lS~TIsehp(l>u!Jx+&S)mzk>kmf}o2} zK)Nrq8SJ?H+L8ISBlBxV=GTtQuN|3RJ2Jm^WPa_){MwQEwIlOuN9Na#%pUpq3t zc4U6-$owDLk)f<(Yy&G25M&Dez}h6(@PPJ04q%(Y9xMd%f=vr&029F4PubsP?gs8X} zv`5v~-45l7Ci2>&!K#}w-$+p@A1}s1na@;4S6tU!9qo)c;O~hx^gnEb@^?ikIPj^e z@b2+d^mTK0Llfy zL5Yit^U5iRI@n3u$)RP?j=OomQC>+g2?;StNfB_AS4Lb>T#k1s@PP_F9UK)6_G>Ox z2V%;6OLF=6_=x&QiQ+w-#3U3H6vV_OVHyBKgy4rG+WCs$2>i0U!%IY zt&k-U4bb0Z`zc`pyeKDTfF|I*JW*(X&c9q0fvDjP#QCcR|FN)!H;`+kI0lRLA`rpC zD&F(wL{=eCtY^7iU<*e2PCqZRY-vO>cc)cS?#H==Zl!)jiRG z;!jbf6eZ+WQ>CDS6<@F^x7=s7zZ(Rvq^zjS>a+J0R&;e0wQ&TZ9S()o-mlCDBoW16 z92D)Pq#VR$rQ}599mP>1(strfBKFea@&IyBke3&ik(HN|TrR&KkMe?h7hHbPg*f0* zfX0gQ6dfGVGAOhoN<;=FE-fMyATB2bv?JpnFKH)f?@{$cW3U%P$i!K;s-R@HVbG){a0>=A&!@ zD}sgxPVu`o6b*@Xo?y!v>=ZjCwrys9`tf;sgKwCCoMarVSF5UsiBDFeQTU~ucNCMbO=GE3vmz7YE zl@*Z`m4MwUFlr)(h(#~?=)G9t5`@LTo>CEOhXbC$S7d1`f=}fSM-BFf!LAsK0p%#r zA_=iogn?ZFOWTk3#CW3}G(GWdyc9R7i1~@_$=f?h*`p=oMeLG46E&U3l5h{@QIU z7DPD%yFi0!h8Q0#>Y@cK@&303vXhmOmXb%yi^$2#ONvN4NQsNsOQ2*#q$K1dq#WgB zaabnwucqkXaCg9 z92Wh`GB2y!WTj5tKeWzIngVXEzf<9V?5KZl{9Nov|5qD^;-42?kl63N6NtlunXa8L zJPa9vGi0DUgZZ1H4;YbT#l=OzoF4>O&2Xx-GM~JR%|h2a^gArYJ;0z(_vL5HKf;BiV^LP_6{7u+Zy zuzJ{j;0#eYaV0l9S2UjJC<=yPNl_V58F?v5inoPHf=4oUJOKmG{g%f4#j0SzG{FSp zC&GyWT0i#vWr-G%b^Sasb`A&7_MT`T5d*Z77uL>mSpo{#C50^F@h&3%hyW|*Cnf)} zTBOBcwSZ%Y;^IoHl_IO~d!^FQhaHYcxKjy+(`D3YnrW83*NuUdgtt^78)gmwdd$q_)%gM(Ei1XA2ujCY z>^X49g*&PyEFb7eOHq9+cu;^%XStuPo@uD4``O`E&C`}9Y`UPMgGcN?W|)J6EO?(k zrtiaowT4y6zw;+BTR-rz#G9dy9TDZM1s@gqDR+zWht&`FprDo_(@Pw99lrkN*`|fmBD+7LD(^4c*cv_@x>I}N>5;-@@92!0}x|Beq zcoBUAJ#`I3!=*G7P^U8Acb7<^2tN`6ZT=zMkD|;!B-_KfT$Q?NeES1~6utky#Gu4? z*Z)^6sw<20>M8?dUULBHC@((iYAOCqec=2i7$#N`UTl&-sQ$0j{D<=Yf}+(a66h)` zNC7dVWPu#gi;tH7h9G}nc*SBAD9#lOJWFl7EQu6&%!9*n`jX8^%ZY-OG7we|Ab&6# zb!B-exJeX%C=xRNYLm!IE;6vJn-xJM!0I3+{7w*wrJ2I-1X<}pe{ESZ@<0yQu;l+$ zDP-hD#ihVA)8bl&mg0&OD{L*=<#KDti2^sSpa7n`DZ2hA9avpiP8!G{D-DE@l>$=8 zFKgm|?;mo~qLLEwax%+3Zbb}PX;EnzDG5n1^iUXHmII6{GOHVDNf3E)QLv&S1xBjB zZVkAP1GAEcMfq3TI) z#y=ZT7e|BTu>dse66rs<_@9~oU%UN3>72hyUNVJMCIuVyf3L{jm4s(Hznj(kSv!DT z54h%)9T%15ZINI1UmEzOfnOT_@#mWA8FwGnGLXgq|E07PGl^6U^D|;bv9s` zK~wvX2IYVWd%*-u#>8ij>Vf5gQHSqeE9g5(tYp4moQVW57 zJzjW&4~CZlIHWSwu=sXboyY-wUf$)5zj0H62vf?o$i z9s=-LZw%T8!0!Ppfc0|202qE(aibgB4!oj94ZkNOKt!XQ0W1z+Mo%LH@UAKK9`JEH zMyFM<{VJFU-pc~GkUHKStgM1Jm3X(KcJP7|C-PwJ)CY?u5=HdEf{dM~132d5=5B}c zgP=v3;Zo3sKWv^-k{2(3nYjw@uRyqM7xTxDP}&T^R`EUUd&%!<_B?>EaCHzN(70f>eR@Em8*SE6q z%)7jv2U|4?yr|~o$qP2cQD|O~-y89NU2qj!tN7Rr&fB2j&381f39vG-0|1(PX%m+h zgZp87d=-WNve+sLFt}*f0AP)uhc?=XLhGtGLdc;Ch?T$r4?+;J3n7V+N2nk)5Qh*32y=uj z!U=&xcp;7DdG*H2GNY@M)V=RB1lw7Dn_b}R9sX-RAN-} zRC}p(sEnwrsGO)gsQjr;Q(d9DNp+7Zl`5C&Db-u5da5p}0jf#x0Z0ZU8*&>`1SyYH zN9rLhkd8=CWDxQqG8}minT{+(mLcnqUC2S?3^gq^D>W~*2(=RR0cvAv6txF+5cMVM zNa_dFxzsPH>!`b_N2%v&m}s`r?4nVm(WWt@aiZ~|IZG2xlR%S0^OB~XriW&VmX?-{ zR+v_f_5iIptqW}c?Iqf1+6>w!v^BIJX(#FE=(f=9qEn{RqqCzU(w(KdMVCrfN>@wQ zL-%bB!y2A7l4~^ASggUVIk_f$P4b$OHMMJc*UZwdqZg!Cq}QW&p!cH>rH`jCq_3v$ zp`Ts5ey#9Y<+Vm@UDlphdvk62+LvqF){ZkUFz_)bFdSjPFdS#N$?%ZjHA6SU4C4mI zos8;?M;W~suP`PuK4ENT9A{!;5@J$gvS1=IU13UMdd}3r^o^O7S(I6u*?~EjIf^-# zxt4jDg`P!_WiN|0i$6;^OBPEt%iuctbwcaZ*V(NLS{JphU|r+7$@Q%3CD!Y$cUymH zee(J@>%VNE+aR<-V}rwn(;MP8Jm2t<70Jras?LgHJ;fT&`jWM0Bke}vjoKSsHim3W z+gP=6bkl}S(wj^+`EH8ZRJ^H+4ap|Nrp<YhWX>bFuGbN3&mGPiL=XpWe*5 zS#2|V^To{>o9j1|w(xGz*y6h7>Xy7M?Htq`J2{SU_;AEG-naSC* zm1^tGtp-~Iw#IF(*gDC@&85ZV&UK6HIoBXJ8@C#_3-=A~67D`8Rvr}|49^XoQl5U^ zO}u+~vAj2VU+|7@+q&(*Hm_}Ww^ePM)h}7uN=qmGHhjJX_eiFKKF)pJdBUBH@RGu+m=*}4_r zHsD-wFYw%WUwn=GF88zUT^@=aVIHHNI-Usx1i^}sPuvJ5Qx#s@z0P=bd#iXyd4KaU z@yYUC?~C)T@Z0Hk!LQd})Bk<|bpR^h*|BZMP8{nF+#7f|2oYo-^emV^_;hg3ajoMI zPOLrQdZO~A_{nQ0Cr+83Dm=|~`uOROXSB|woMk@idA9MK(z)34)aNnht1d`gxOHLv zqQk}V5b==kkhx2!OXZg(F5kSoaK-UTRj6!e%x|>6;eKnls(SUowe{EhuYJ6(dp-9C z?~RK$CcTErDCV-I|MZj;xQ`7nN~)>+K7-r=uOBYh%=6 z(qp+|Lt;sHFn5~nYTeDfCv-17jyBFa?sL3JeA#{Z`$-8L2_XsOL|kIm1A_-IlH`(- zlD8&@rckALrSzp9O?{WPKP^99B>ip%TSmx3=%Lrc{ztZt8Zvb>9dD-5w($~tbAHPw2Q&28np7&PvZEl53MNXwm zWloiBRc^Ihb^bercZD^3YD#KVYoFHbuY2`g`~BPc!}T={#tlu4M;p7E9Gbo~W1GiX zyjx~lkGIjZU2I?19^SFF<6h^E&WtYEuHx?f-4!1UKeT>CeH{2i_%zpZ`ZLq#@LrzY zq%V?R9`|YXz3aE^?-_6(m>oPjw0`LJ@b=;C5w($uQM1vWG0(Auub0NRj3-V=PduB{ zpX``&o0^@z_-)I#2Q%_BWh7Hl@2t-p-P}#`4szkV&V1W~8~A$1Ftm+weh_j{19JiJ zx#V?7D&*oZb9iz90_8KtiysO8`ITxhnd*mt{4p$*1gKyPr=t2X^Y6qKo`KI8Q+=XU zLn6R;e-}Sv`~qPKW;`%C#P^90{aU&;vNAB z7Ce|R(jci}BnbYBF!=Z}`10|(wc?xDH}k5k-@v+2T80grGjo@alHa0l%dt&TK#@;~ zfBSJk;XCWWrzc=x*3i&1Y+_ggKC8U=ab)nZT?%e<$@{rU>z$z{HR5M^2K?SWxE`Ir#W}Te} zpv=d5_i2r!)OG0_w7lZ`4D7+Tk27oe30ZX;Wz^h9yYFq%w|#bs=%sG>Tp*i`ZVVN7 zI{tl*?8gsbJ;(1D-LH38{J1d!eA5{S3MH~<$iUBYzrrsK{GZT3o^@u_!~1-CsmOO> z>l8ZmR9bnYkCMLnoN?UX_NeQC?90))+ipD72PEGuK#zPC>D&>*0XpMv?FJ(yS%`Xr z)V+qE0_d{CB9a*DHkNE_eSvVexHYXyX49!_US!CAYYdlrYeyQFVg8-}g?WTeyM}Gn zaB)OG%>B~a0Xu8$hFt|($?p)X%4}Mf@u4~gzINiOs&=AV!-#YpF~8z zoC(a9Embs)sIn;t-*KvR;(Do`mm z$CxV=Udj0H1AE|m6NVHQzi&4)yAcz<3HB+m^+R7=E*~4NIqBe|*Q3Sv=G2*XLDCxs zJ)QYA`&FXJduw-!W$b@EEcSNjZq7{Y;}TDD#Uoud!>p|8+>UEvmioS`J`f!WcQJd= zn+|U#e0L`=^MII8r!!q)ikEitx+f5{h4lQsBr)OfKWCfnZU43R{=tchC22anHl1vZ zta3R%o19B0-rv(qe|JdPy5sDF#;Tz$v8jp%&w?(_DWr`=1_wMkZSuUMX!`Q~Qv5v1 zWV({4xRWQrz}knsXX;(cT%Wa#LeOWkgpeK0&AqL7ZT7SWdw7bDBOm2ySa>TT^HN!X z$q32hZC=5VQJqx=AIJ+CLTpF>`FF`^9W6F*rCrl$R0U zW0F6j!fX0rh|||i-S%cnu9sFWc01K9$-b;H@UiPAho*;J|Cskx1rVrmfz%3PsRyP7 zDaSoK(yc-YMQaXOr6J?w$Wl9$EXTvSh-LkuY%d<2$ZQB!eSP@V=qGs$zTT>%*pES~ zvfnrPeP_JqON+_^&6H?pt<{h(H>$q8Ewak>H@HI{P2cF#nnQN{t#4JG|DOZt`ld6+ zXPW*!z6nSBBi266J^rsz

oppU-KKTYy5V@BC|21n#)IF~jUTF*)wO9xKHJ|HQK1ci&SvdxFQ#F+b12 zOiI1KgS{NJw|C2sYFxIS?bsaVAq1h5E~86}7+#P#)uy~TPPfdy3>cg8j;%Xlo&gg%X>s(JF4@~+x*l!QKdtW-(a_;bj!0f!42!V2aHOO6lHYV0k z8GL-~KZHH8vetFIojPZ&3mSZ$tRQpV_x|b_QCp1uIR0fN5n@pHPn>CN>>nP{O}PG5FO z{M6aU|MesAM+mswl%1SdPX@!&++)L!=fQCHM$%U1RZfZP(_Wr-`p=!z}ZO_Zge7VdUPPN`yMne+QI}X*7Oj92R1umxnE}Zd3 z3y>m-Y}GzV8ZR4q%}QSoEFzHSG#_koy~5@l_m1<;ybwep*W4=f(YG^G&Kpw{>}3rI zQW0iRzKOIsI2rMS2NhxJkL|UwNhvC!a6|>`IzAlydPH@9tzQTU`BH*Y$Z)Exm$|ZK z&vbSC4jb1J;jOPaK13MIU+*6>tnvH&G(n)zc|JfZJbzj?!#Hr0d{pb$bO63XCOP(c zaD3?7P}|?EE_`4bF8x*)?PQpLMKDTd`+Uyj%jl8nx4jOOs+v!?m4@fQs;x?TUo0dT zftz-DZTf$Dsd7twpylKf-15k1M7y_^OB5M5o%euKi-ioy`e=BvD^B+v})US z(Y7sP1h(b-S+?^;j`<{%YUEopJy^~F&(wE|4d({t<{uB~KJd3?I`imDTa8@Ofc9sx zFDx5hU~eh5*u?k=9X>TG@wvFe_u7ukJ;Ki)4jHG6^Nb94ADzoloV zuM!?j&wW?O!-%+GE1ANqQUzpH*;ZZ5Z zs?;~0k@G9PcnkBvkcnA$>T+Ccx!cQv8S7;3L-W`2*7o)!jZCn7c(EOPXTNa`xiP5i zw(iik+2Xtrj(XKM!L>GHQyzJxA3G*HkKxU#MHZlabF?Z0lL2vV^!=f|PL{hkNu?^j zrdG+m)Up6x+|Dbyn?r`TyAC#V$5oAN!*Ek}E53<|ec=`crA2lzV!R1$a(U zo8P_fY$(}1wB2Xu&5MrC(4y9$u{6WS5o93-h{QQ2?3`xpmE3!M0qi&Je%`kL^>&7$ zs!SFjFx|gqnY93^IEVLH7GN)oavm zeVxr7{6@g|(3rnq3jT9&r#Q{kVu^2DlL07KxUc@LhfTzd`#DuX5Bqeb;z>p0GZoP8 zGwNY&57I}W=eLgT%z4zk0LeX(dOUyYjRBEqZ=!$bk5k0|Fvu-HqRQd6Fq316(xpZ( zOOnZ(TN!*J9cD4}K3^HnA58QUBOPkn6&s#d`c5@u-CgiYA4mD8t!6#ld?G%X%wxqi zs>j?UAF1QeOZ92B_02Vx9r=9Bsz0Mfm*?Ir(lxEbCBp6H%n45a>Q3>V`wNiKComcX zRS5OWX=9FX_78EaP6)OlhXU3)$=v* zjr9LKH;7{TIK#us$@1cqK{9t_!_K_B_w}$rcw;5tIUN7!WupQ-zfKH|RKJSNYTv|a z0eXmxOz0QtjuE}r%9VvP>Qg=SAIFE2jmUzuE&NkFC$-w*f1)W~C6*TiyT`%&-9W40;3EwQeBmQG=`jZ&vWy06ojQV0$*gr$Eo~lI!z2!b74{Q$9 z*M?UQv z-`yX(>3QbZCCgR*`A-=6p3HPV$L}rj$#9V1Iq*U4tgGbqY8Auc$$ccvD8}%|5S?7r z1IHa{rm6M8dqJiDbKul*84ssB(Ke^UTtQbfKKxbv6W&Ngb4J}^O2D~Qc}p(YpF2%n z@rX#x9Vy>7pIB~uw0?!5MTU9u{Hg!BazH>fyuNog3$kE*jw$mkaf>URfxu_ar081` z0d#c3AD=1Ze=SKjXNilL=;ms(SFAik|{w~7RMaP7!V)yYWx z<|>w6Jnj5iHec%{&2^T&LXRHNnwo9liHn(f?r<=gAc^ajWZM$=X6S{}vF)wyJA!lW zxEA|en(O@E8KJFzHQgC^$ZT*r1Kk~2e?m7piN-3R!fEs12SA*zbbN5kPyi>9;Uw?LxBoXzXs$S4D-Pk1tj^fdZUliPDUwwj zgpBa=>})z3{qXYe>{gX0J>^QhU%!EGd7rUVeWp7dISHFFdmohyx9&vxLJ84SKs$(+A*wF6P z3Etmsna0*laf-YX^eRp&>o6t?8KMs>e6g| zR3&&DK|ZFUa{L&}7qPvGsis=*r?|6t)QkpR`iAfrhPEd4`wf)z@1piUj56TFnGOUT z3;DL^+i^;rI+ZA;?9!^-Zicf^Dzn-K z)jO&T+zt>_;GGiv_GWP`OKka>!7qmxqTmB}vSM_3SG~U0?+^_+Hb7r~3i}QA@igr@ zTqhCfk4gl{wed-;dAqtmBKQT^!YTBFoQjvSQ}JwZ+7l4O7Sm|B$*$?)hq{)2`&1>i ztq8mFJ@K|??n6@P2|E_cErWK=x#dc(8E96Brp{i7Oka z>KQ#OvQIrbwyJThx7&6_R-8){B@@1IZgYHh%{c2ZB0)OV%=tb;vubs$8uDS;0dvxL zULXq3uQbe>lE_#7@|;=Vnf3hc=H?7>G4szp4V}h&`#c*=plvu=Ry;@ZL5Atvv2B^# zwF(BV7wBHSqCIHdka(b%@$l=&%x)bnS6QT}$HN@#liZT*$fir&E!o%ufuiAM>=U&O z2eYwemjXJUb+r@{4hoJfK=@e9o9wRCxV>R*+e=6V*aIQ6&Re?Q zo1K)@OGvZaz^&)n9CJ?WYMknYrp9||@`hsRF`SyrwWNO3W{1(l^mnH+i)l>(ejh%k zOUv%cZ&{PQIMS0riCVUx5|LK(f}rH+3bW#{HO-^f0<7O&Bw!|Jfe&saXoxM@w{k}M@E;XU2nPJ+4+7WXs ze{^#zr`@HP4c><`tj)Fkp^$;?|&)83Rv;(@tX)bn(rGp`(gIubVSy4oFmRU zs6J9JPUY~P-zYSoN>=|!%yjUlY`U50sn{-5<#9UHtMl3QQV#0n_X$#K(39oHKB6ZA z67uA8=(pBisPb^xu(7h`p}`iquq+AkW5c{&#PG+bMfsmo4|}~U*66I$G3V{?x zc`*2R)mlW)PLV9UnqeZ3$v&n9(nYY&6 z@|sS(>9-LT*$0hM5?ZKdIpO)Y?sX7oWK z{ez_5Nk`JoSjn)XGaF95Y4xp|bLxCn$ev$x!KL~2`0ZEcQZyL}RUbO44>vtP7N(e2 zvt$lMUVFNasoHW7Un3hSHN19mK0EE&{aI0t*1{^6=vGY@WxFZ-3sGGQ+1<)@GJB?W zzvY3@K&ec3ga5+j);d$5F*++OYG4C+Tbu_PIq-Xo& zUH?Xt9V%DmkC76JkB1%fRzvb53#O<-!@B%_U`ncc1eU%`$;j=`%002aj>q4LmK(3sBd4U%(*5Bh3o9MRV>6c$kI92nA66-nTnlx zl#^P?mdE73pZBYGtyGCYN&%6fU8cy;_10~vutxq-bN#n2Pag%c9fobQf>hwm={&o~ ztZB6GP4=BL$ubocN18^{Hf0F*)n(uLL@K_OGRv zCMG<}nzl``%7rTZYL>^5-<~u_8^0@l*`{Msr9l(HVlhs-*Z=aI$B?%utHyt8`()l*D8r7r>3|?P%1R%YH=ff{q<<9Qhzes2Jl)`|LAKmRqWz{lw>=CCz%4*3i;I?08yixeKP)-~hN;c*2Tfx} z*dO^SXPP1+I1QU}QyW=1Y(#1{$?g&Pc>eWq%XnO!mX8||Pc&{CEfXH-!Ergs*vzKl zz?9wjQ;F)@;g89ZR#N=N)u&R95y=Lh?y&W7+PUn%9ov|-YyCre>9aOl_3A}+v;;u| z`)px2Ro>xz?21(L*OVd-?ts2);ta&gRlb4YVU2{%A;PCQkMhUcGwej*u9j3+7oX-h z_*R4Ix%HER;(f-uZbilk)?Eqs*3xNNF_7+KY~#Ik|8$*zEn6Q4&Dk9r!g`BdhAZprX0=>kJ+zMvD-ESCgTiDG7KQMg{T=!J`^2k*@{@KN>dK-!?D~Ilk8&_vA_?ox` zXWFSPA<|bRzMhJisqJn!Q=07csN^N{3pXyO*kq%K!;KHXWRx}b354W3SQAuz%!L1R zb0-+4zgO`i#J;IMt58hhvemBlWU9k|Vlg`;Qe@9QRR)t6PngUHkYu{(i zJhRd}$;OS3Umk~$QLl}H60ta$i%mRFR#p0UK}74*&q~>mjbEQY`foiOcYx7bqt#3B zWSf7vYr^dN3B3Bq8;3N=Z*IxwN@7@czKZc?udFMC#Dx9ITcz5TJ4_* znp?s8=YS-gp2~o#P^W)`PrE^`X;VgDTUbv>{Q+4A5rL`pV=zIwh|U;QmE!=~J(6*M zmTN*I@jm#?-#t3l8+X^Ic7UHVm9|nm7pS0p=`AfUz=8wfwWsa3Dd{ds1VlwPr8Y%m zc7qKg8_rmf{+pn9~?7UnfX6^&SZPEw`2&`eF+55XAJB$=xxvHPNjTR<^{fq z{Z#9i-?697(YAmVQ^)z&(<*^A=9g?CB=%##T#3w1y*r4jiVAOr0z`S;HmfXExN@Vi z+@(;Qqk<_Ue(}E+8Yi{6wUXs}n@vwqnPGK&-@|Uo+Vs{1XeY~Y`zm^>>@O)c!y^?O z+x!xz%A!9#b7wSv2iVVE-`-voV^lne_TPDzntgkRZsXe`{x?3u`OzaLsVYy;FV`p3 z>o>3N5~x)>HknQ?o|zkjASDsvru3934({SS-?uMaIGK%J^_D5g_ubCe?^69?NZQ8> zElaHiKbER+4#`6hSf)-D1~!t%jCOyQl-UUfhY} z7bn(avs_GdWsf@LrTrI@(uE@XYRN zaJAh(J{;rIUB{+U!qjR`rJvG#O_(N3ev*FehGCb_$|GHpVp>X@j-YA0@J9l;PPBSP zn<{Cy=OD)Yj%8~;jtJ3V69PkqD4dPM9Cs74fXjzu!i;$6v0BT_}{( zzqG0HUfQO8&ElO-=i0L_4_D_~-B4)@>!{68Z0!%rw43j>`5_svWZ6~N&68=ymCXz$ zNZMEMJTA&+EKl8y$*-VECQ3@!Rd_9*n^m@_80byZ zk`gY8JWc&PB9S=AB|e{e51{CT4U|vaX;-~9%95)5#@fnsWUp6-`n+a8UXZC9$q%+gRz>*C;sLtTAEmwqIV zRGVv4JyO0Fcd@I~I^q#IoyUK68tqbcL;ENqs(y{hm$Cncy|;|2YiSlm7aE*E0wfTE z1osf!LvVLpXprD8!QI_8xGmf*xVyW%6Ck+dPImU*XP@`Zch0!q{qx=!x5pYx>6+Cw ztGlbqX7fa7(a|HdgD&R*QJJnY$hJ5R*&Ji`swD&wydfC8Taf_hyHUGxsjhbw)j$$b z3211)ya`k8Nwb|R+_^J-2HtSMrEf0hgeP9Y?YAfmx1V-qcKHP|^EgN`-w?c-CfDv% zw^1FkyE^Wl0<4m_iaTiXKbsRa3Qsl1s%Y*K$$l$IV!sIAuww^w1#1^B3mIb=neJU@SZ8y8}~jN5II=RRo*2omJ_Tk6vmN$NXm$gT4E zsQ%(@@o^LNb($qqQ%u9`n{Z@2s?}L_Iek<^u&MS_mi3*Jg|?9 zmeF@^vhASNv2&(G{WZp9;bJcivNMmTDL!NV6NkF``#dn(Q0qc|;D(ZqlJ-|7W7L>Z z17aBU$^8tNx}1{uORAEd&*7Gs@x4!Pv%?ljFF||iG-L~(0Z$_@&~ZM1EGm5>fYv4~ zN$s0^APV+(#I4-kQF`0yLoLCW-$cE6qA^Bu5$m|MiBdn{fCc-qW0OCp5(a7O1Q^7Y zfFUCfoc?9c=-$%gVhyYTht#Lxw#$+09RBm>vd%SEbIPm5Jih!1`>{8~unr&s@a)a& zAE{7Z?m6Q>K|dy`^$8xeX&%Cj*Qfw_kVf};0t1RR=egK!}s-6Oy{rMkP@6`ck(L6>PCoimb)DAAg8j+hEpiwIdi=oZ4n9|N zDM8D-_`y5>g@Pyzkl`WSGU`&#_}x$ME31)Njk;&7`G{Cv{eAdvPO+6Wm%Faou^PAx z2kGYCdi96Q-AmBNPlI4+Kif-RP>EGt?S2Ea+5OYIkP0?L_re`jOn+)wja05pM^p^XB{2heex99CN6l{lSE$Z$hv84}LdQNPKSdyIRPt{RoirTA#z062p%t z_vk``$3}ir^%^HDE$3=mXm`iM#;Jl{m{%RVikQ0(8L2khgoSI`JS3EDPo5~X_mA$a zWbmpyju?<0aFbsb=9O{kW~+U;_z-g5>j8X>dH>bxhjOYvhR9OA*5kv?LesW*jtPBa zjLpInfc0OO#r&c$@tG?DGAjVfzb;VBvFvEmX{?J*TCVB>(}RzRZiPGc12-i*6B+wr zJRyU?=hPr)mxdgvgY7SsIk##@Jc0MN{z2$P+Vm=S0b;&0gQtZu=e)1?A_2fH4VT6; zu0+=Z-{mQ%p2JsRiit2wyqg*sA48WU^N(BqH68zB&FdBP+6MK0oZ(`0fr(YWE&O4Q zuU86RIc{deo2V`Tm+KP-)b6y&`8kZTIs>k{r)!A^dEV-Up&7c=AF^ZTbcT3tI*oV! zT=h=-_UFaKZw{~1pP>HDa5pMC^05;0sVE=n3FqPgT({pkq2B2qwLQ5^0g=k&=II9!b zqT_kJc4W7jakP>p2F|FquF1I*ljmj@BYS`2-@1%i>unMH_5_GVR5G z)6m3?D*G1{tiP!{ituvdxh+(f$*| z9|x#ftJK>-^3i|ZgA1joXxu@L06~AB3Ftjd>B8Jn9X`tVDLImFGf|Z=lGeG?#B~?O z1-!|2pI@f=dWpwhkE?%B8?r~)qo$YY$=|*8rW`9{(`XuSQ);r?KE54NwC7^@3zC)P zXbbgvmWMjb)#L}NT!E7W=-J=i=txJJ60D_ef%J-6w|DRGu1*VcJN?rj;27woclvVm ziT;dr7L#-mALkkL>FN^^Y5-?IPR~)^#bkGf_T^-jetVw;ywZN>ra6k1tGGRwo_OtY z=62rj^`_O<1Fz9iRr)WG@^X7Bk-@>((nVOSE2Tc5IYR`FdXbNelJOa8s?cVHu1tl{ z%}0bC42q=s6A$Y&GQy6lN*WTb+}=sKW4@(jV@aNo#~9!Y21MnpFXhCKQ<0dr3Zx64 zcgQl{wQ<%Bxr*~N;RN)~ee2v=;yN-c-ez;Kc=fKX=O<5T`50#~uR=XVVfot7gU}~8 zB5M;1m!S67sW$Gi5i0@Uq=gmYtZ5tEQQS+d&t;s(u@ywwaWK?2#1-t}$_v&elnv0w zkVWCSGQPjK`R5U)CU1yUB(hS+ZruW@wJxkAr5QfC`~vOPIy&*K z|D)*1tYs+CLe@Mm3L-ofJ4&Fy95u2CuUc-1f6_MQv%rI0_~A%j3o00?;bQr@N5Jig zuI}Tig{qCwSSv&fiD0cXeBy-N@dDGYt@$G6w#u#ymsKyPCMz$eKQyE+h@ogvOU~{~ zR;-!Mm2TDW!qfoERq<%hkFc+Us^AiXetjD{z*9y51XECjJJoLpJEqO zhW9r#*Eh*tP&4b=l=$w`Jt+=~uA%6r#b(T(PzS(=B;zc`mOubQ-^2*=%3N(nhb*%4 zqg^|vDhWsC8Q>FrP-V}pNX4xrolwKOSG+lI3IkNun!mXD*AcpZ^0j&I1F8{_z;yob zPAK>N%`-HON*?3-^}}!YO3NX6E@j+eOYe$<7bk6QeIGbQ0k6T=aGc6Ihk^K@ zn8RK*s$<;UztF;FnQ0A&6w?;3SIR#|eEu?U4MOX{Co()3Ub^u7e<)CY8d`%2h?>o* zb81g^K`H7wovE=k=9ucx!O>E6ifUwsjcx^<9mG(FW5JUw_WnC+;*d3W54DX`doKNA77^wr@Q-4Y1DDzw;S$EA8HXbk@sSH(wA}JU9yDBU%Suwk;e(~6sbukkf>8IN{W3Tz>C#GjB~y8!n4X74gWSBdo;#i|=gnN- zl|FpYsy9hi#am**Vn-bg{E-d6RJth@X2Ys5Ts>WzSP0+kzmU*;JA8=ntxb|tKaXYH zyMh@_DGbl8X*M*=3=4Yh#9jypjC5GcG-6blWs=j>$vk{%1H7N=vwI8JffApU`LA%Y zDy|W=+XG#de{9!wWgy zxPGxdc$V<(9-h|+ryTq0Epg*El`Fv_CJS44V3x{K9(yd^ZV}S7pd< zfT0`0eN@-_-~>UeBBB$A?6{>7G`LcZ;l#b+3^3js;)~h!Xv{Vz?pw1$)nL!ZOnr`H z8ybKogw@V$yRF#0BQF_`OgeR6mr$I>i zt4%txoYG&LmfgjT|Jz0PUpDtY+%I|je=6gJnR(bf)U9vN!GY;hQ_{$^G zxE{uMa`zm%JqhofIaaTl)S9opw56Wh zIm>?EvD{g5b(MM>S)jw!T=qZE@ZVOmzXfC8n5Mnnr^T6gEMdy4o{Ank{cuO?vQ=5z zjz?bTJ%)vFklOXy5U=9Q$*av`NQX7w+LihJoJ@6LOP6rbG6e{#-bxjlH=|#jS8Yht z6kY>t|Arg!irGea(@AKu$HCtJ>-tLBwM)Z>tI=k_hw$_NmY)Bq@=Y)!^4A*Wa3%+} zJD8BQS;JXP4|7z|@aY)yw;xPA1lWSgagwNX~@miV$OFmF8d z)j^9t%idNEM$?(T*1&RrO9+1AjB2Ro3#o^!XSmVV9uoTMoD~}%)!&!8qm_&Z%GINd zoN+mixtjh5Wk6!-tqY?E2*lXT%A-Ctp!u@_xOghiYSgObpLIJH1DYT3<9i59{Fi!z ze~isNxkI7)oNh$Uu@y8`EL)$li=dObfy_m~{v5geYVyrNW;4~L=H0VeMUn`3uJfn*>kybkmp+*3qm-hV7r7H-imP(zU5}JE{&bAmk z5}ko5}>UdS~j*l4n|D(y6C6)KyWP=KHCF5 zmurJ1xLC}gV7x_{KVad9gs&vvv{=5^2J}S~8g_SHlJ?r^9pT$wI2xo#4Mgns&2t>> zp8&9158&*67<$&V%P%S2ETMy+pQ8Cs;azPIY{pC_ZGF*p)}slY-{q-Ca<0E|Y9txqGvDjy03E10`{dQ}C-~lOWVrKEd_CcCEii3T0!&;e2v683-XXJC5 z)X5<|(QZw{<@3K=ftlM0C&GKtu|R4tDzgM+$0{~kD0sl6+5T)nODMlMF`w_n9-6LH zA3={Sd&ONPyQsK4xWcZN^X|y#WTc4N!M6#+J?AfbIyal3iM0C0KZdeSM!6 zKK$JwV3>C#?CO)=d@^{@LV!o`87sH-^{shV?pYJw4s!I&RwK32`Sb^u+QP$@sh|1k z^(K*qpF41nrY0ig^*R8Z(v^Z`@NXNY8*q0xo3ZN#HIm^+=F6p8bIzA)BPql-Rr{m@M=7{bT*9{-$yM1%w{_25}7e0J`Yv0AEF{K7m_DpQf=@+P0(fBjf z+`siT}{PcF!;+~v|GdHE4pWUCqToPwQ$M5o|N&6c?3UDE)QYVDmA zo?eeC?KaeIj#h;N-lmR2AlP}ss>WFGg;Tp-S`fTRp1!$@=*d&MEY0 zJ*qd^-u(JXpR@&3S$wIV5GiqGimjq77m~{4I6bmz7%w;t8^57tdk^&S9hm1b^k#iU znKlr^Xvl_s>gTR;ydJiHRkx%!rkqK=t>MDOjajk59#;*w@9vr7%sW5j?j63S3H84^ z`6=SqLl;i}-6h4n#SO^1+qnvlU*@zb# zR%=gdX$wqk=ya@pA1(vUs!N5(qZY0@ijMIXfMMP`{zVW7@%wUGgjHG*??>k0>SjRw z{D*TGf9Fhyu%b@b;Wbm~*4R(;nG#jV&KE%JIXoq&!5t?p{h3cv;tw&*yzTI>vvR!v z&yHT6On8CdHp$SnChs zF{dVyMCrqU+>ez&snDEf;dj@r=Df~&Tr)NY1m{>{Gfwk{8a54(jdEhTPle<<%~PJt z)s#ryhu7kn`+@UMq&%g_JXu*Zc^~8M-R^;ZdFiO*%K5$P@S?Os?F%3|^@`KVUT7w>E>+s>3lL!?1m^ukb246c z5>5$s9FQ?^eipYsY6*DB`V|2etZ>SNnHC|FM$xM{Wd(5;8T3txeFvTY`eC?P1|79hBi_(zza9 z9sAWc%SFFEuoWe&Jmd>!H%`V~!n93V=j`u!q_ybxw}3ow9&+9Uf)9mkYnQ;M3hcZD z-_QmR1`@uX2@7*;MeN-_R+5o(0b0v9hC4R`-h^=SB2=HYqnYo=MOon^Yc@CCoIC~+ zQjVM-O7%DHs(W|~Io2+<|8Cc>d)59R&Rt2j>u^ouy8|$ZfpBjSH6205)%K}?o&k+i zZ)3TRCy<7}NZuHZakvrL!2U$_Z#T2AgVh4X@cqZwna)NxEAwGaRPTjS)g`S~Qz>){ z&rm-5lHyR(!@1X<=rAnt7xuop%`g~G(X0Q1 z0&`$@sTIYPrFy#vD0c&8F1|T2yvi=iMUTRNc19g14ZnoWVc3C}MdxaEI`;Vq!Dw9WQW^2lIfbRiZ(H%$~%ISsX*60=pGAh}A zaw`n?*Db|N&qpZ9W{7Pbx;f%~fZ!LivCw4pThkF3|0r9h%_}CWT2{_1y+b12!_9OC zg3nZy z6B=gwt3YmodG}ddhRH9G^!;PXD$g;PWyiFIe6ObF!Vteb^$87-a<6+7dN+i-CAq@I z&olZD)b=m;kXfJB+(E!3a9TSvh|5cx>1j0K_TT_~7Y_}E>WX+v#!FTos%4j!+!r8K zobXJsNgobhrKFV})AX*9l(9Q1)0Y(9;l5o{Yp_Auy6D;pevx|aTIAOjcnAfor#YsM zM&5-xit(R6Emum-mX&B}h}D4UN_x*d{z1pTJbjk7%qi;hGmhSSwk{J!ALv`lOWruU z4_Z!!JLlUTJkttN%ZJi*`#j&zuUb2Sm>clAS7&QBg5h!pUBXWPz>A*+=!G-KM|k8@ zhYh?(^A9~2dRtF1y%~;WL&L1bns(NPLQdSbL${CW!a1jpKw4<|DQhS*GuJ&v@{FeQ zcl`j0GiOBa1$3p5*HFHFvl4G-wW?QDwgV|KrcG&&{Cf+-Is$-1O*1QcE2YWlXmtP< zDqdukwodpTWc=$>uIBqW>TjN_k;+_>P?Z22%Yx2Lv=W78_&J9S2YSe7~R3LZtr~Ww#v1f{*MO#<>|AlA44A1MsI0-^*T>DX#hAg<{!fxL!##d zIa9#GWNl9$`}I^M4jXvkG=Vr%v-*Uy96pK|aon27?x?Aotx2C89``_5?tbx>vYuH-e9 zYEF#Rzd)5-f98+=d#yhs?)Tn_QW*U7tg;oraOG@8=I-pK+GCJ;HC)Mev$g5gf#=wNdYqtzE~TEi$dSplBWP{zKM-!JGH9-FnLmAw_wSZl`~)p$ zVw`}rxz;BX?r!(J;|kK@6O^3{#ed_3IlSt;12?d3GwuF;?|=6M;MQMTQ`poaS-`i} zUEkmSg#<9~T-|wGVSxSBfQ{QgP;j7Mpa)X0o#4wp))bRmWwu0!ZTxjk4~n;6cx_$T zf({ibOQVyuRc3%sVfS(lS$>>dI&q9e$L3xI0>R6=Rm9QZ-N6$^@E+ydSKUK3R0y>REfBuc1VSh$DWl$ z)h(dweZ8;5!P!cnKYWB6Nx8j<5XtoRUL%`yA(;>NNtQi{Hb#?|gg^4xndJ4URes0` z36bdOq07JCnj(19keOGR*`T|8U*5hOWUr(b&6fg^eth&)vmw@>=&8 z9;^J{s{em+13zeu=2@VI)@5c4!flJeQ19VfJ$^DPp|kZe3PNoQ%_kkq>M2=4I=;;D z^Z_cFHt_xwb7zFo1WNp?jDVWAy+3aZ=;#L+wC3)S(|+82{mJX$ykO>E*YI2lk6v1h zvgiN){=dh8E-^9YiFIY=hvM*rx}-}Lpqs@sRSXF)a2313=Q<}bJCs$^n{|;^GLGth z%NnHZEI%A$gt2)qCj0bI1js5K`4SRU)Pj!oOH%Vm`FGVXdyfPeIb|ihp+!Dp_Xid5 z-IF@AD7=vqsy#5;Ulh*Ng_%qC!Yv?^#YpasMx;`-mdO#_@Xk+JpR|;0&NNtM36017 zD57MN!TlY^^E?a^j@Icd9`KM@_pXu^*4|(>Y;Y0dPYkl)Bd=lpc6GQu#krt+d)*fN zgwh@Z>JzmYDg|OSBRwyIN0+bU>~8P;VE5^-!$i=ziQUuI87=AzZyw)~-TD1@7hzw? zujRhUWpxL#h{8vd2p0}tuL_dO(zp9`e9N?C4dj5biY%JEx6FAt)WSQ`<5`RB?N!vH zBrLt|GbL;3JC}L*hKA^!D>N>{0R%su|CVqU>G`7GJzv_Kts*^3iEr~V)7sl$ZF4JX z4F!7*toi+|cX3;`O=Mz;WK1NCu}T4##!dK1rNRbYJjYWd~$Ye}ZMRO@h1 zDw%=hjs=dUbV{DYECJ26v_ay@@Y;@p?fhIwet}<=jw;=otv#Aml)ct32c)lnpSiM10Clw@Ke;MJk19p0 zzf`dB3g-$AYZ6~TGG3j+B_Lb))LM|5 zL%UIea5P3#iEkvF{0=(P$j+KOg>|FS@7~^W#YX^|0*W6{GTU_P)$(3m?9U+d(xU%Oj`VihBfIO8{+a<FQ)*W(sjjP{7SafIa+&v(UbI`{)YDwq! z@FN2UkL!=?RA1!0f?hj75qTCtamnJp`IJdR?E{j>Y-mX*zk@m0(q!qGC30V>nozW3 zNz#)=zLp?J%-p)Wr0p5FSE-0eY9F>n4tlx6B+Qk=a{lz%>cy_i?i+autOPrSoH(x$ z2y<3hes>4%*ENx_bQ!qJ*fAM9$0!aqe-kAY1W+FP6z?wLKgPy^b&!Boa73gP}-_r`~# z553j&zPaU?{#YRA{fizYDMUWp91g?PBJj(YtkeQ>VPny;`6yfa>{R>$@{iPUVcMN} zlmkH=4E`p{DkuO!!|tQ<|Cnc!M7uQA=t6tGXSR&si|I%+r}f!!iY}1VRe5&%3sl!qpG@Y1jW$7r z>L|9n?iWDAKJn9CWb|e zqCg}A3||+aF}nN#D(LxSyF*QbYvsvO@e%n$`9~dEDqp;Bd=y;2K-O=($-`0$ec=WX z@ipD3sq}aV6W&zoeLI$82()X5?aNCx8~l8-D13$|RDBmwRl?wfz^oUo@UoD|Olh!< zQ!fMs`wL4@D~Tx12^^Km*Vo3Zs2>}Ry|@T!#9H~is2~b!MEl>P5lT1pRbsZ--Ck2s zlV`#>KC@Wt1yaGm6vf{^L1%eU#b}~> zwL=RFnz5q!S2khpm6ea7N_ij)n+EZBvz4~GVd&#|B+J8L}07CIN58|=SL(H^b^ z>UeIvppO#Cze}CzNnx|lZyez{jb$dB5hE6iOK02S!FSy$^rl|%EX#w)iWlzHCJ17V zi)lhlOfe=3%a+3qO_H#fA{R3kB&Rkuc_d9PU#)9|s zu?}5#y9*XW0%=0mD_R;W9!c)GxH76Lzuk;VY+g1cM2UYEf(}Pp0m7u4GRKZE+Jp}J zgccS#K1|X4dR!aZt34x2c z#ADZbQC0SR&#T`2Vqd%c$fI>e7x-7&W9xJVZNeDxo04S7h8&%IOs`^)g*~?b1aDP) z;;jBUaU(3SAbI~;P$CMi!?G#Bs0vB&wIYF_YpXQDTZ@6;gO+SE4vL zB7JhQEPD=lkb}ykiEEAc`y%3Ru<-Ww#FvA+@}jFaOFgv zd2V#c2Q|DE#wnx&+k9rF9fjKxeECc`U_z03z`MQ#*(aej2(0e1mq6?||E^pEO@A8q z2Ln!m7-c#8S1wZNg3+=lMLpkQ7LpM5r67vJvTX&1kbp@QriO?gc4-!=oR$({dQIJO zWP0ThXgI4kidK^^jLhZ*X-RDSqLb0OuySV9+OkOk7oE0+91vUi^pQP66gU^+94j&;H3}~k zF^Tpe8!Skip8Kjaw_^gf^Cd`UzIr8Wt*=3r5?)m&EK%FoYiVpIzTG!z-v0aCcKtyp zVENph)K;s!=_rWgm*AmJy9Py?QU7Q}SJ^j~;?VXRA@7sSk#1d$kYSbjPONzliGs;H zngh8OeHQhBm8AQM*pFsenuF4;v6n4-*A7yPUyn1qpxDzbc|;*d2KKy`;EjE|W$<-dAW&hwB-{D`;j;B4E04Q>@L(_y zvmi$jUPW%bX+Asm(u!4CUKK`(Q}tNMP*YDNR+8Eplgx}ZyL3jxybacxqaA^PKRE3e zK72sxCZO&ol}tV6ah2`>!e!^RcW%>P_#D9yE`i`3(Kpd7eYG(&ta!C-|N}h2#TlDjV1(Gu*rVd7roqLV18F_H8wz=e>+k@ z^kGV-?=7%10kSy48an~v3#E^>kO%ERt4V>kRX(dx@+!Qni^fzVY$kH zplMJL2(|rw*dnd6g@jpIo)N!(iK$7J{!LaUE4Id{!S4Haqh=S+^?pHOfa4eD7nsRL zmIi0_%&k7KIJ}vGT=!Ae^E_-Zl0UX$>3Io#2oTqOwy8g^H0h zvQUo6s-%olg5y`t&Wo_k`HMao(j=9Oow4$Y*e@c3D?kiY+uptfY?3jhpN!3!2PK z0QUM5pCs4h;tOwMWPTW-OYAAF_)V3~&C_;+H131h3f1Kr-m_Rc72Y6ifWHz0~Jb z?LtZ~sg+<0g+EF0)3-sTO&T9FX|_mw6x$bw#rCD5!=g4w_@dWn93!D-l$)e5_n^D( zy@^B_1DTh5-jtjt$_+pGki63U8})vz2y_^2K#B%;>m?Lh!G@K-7>;a4nU%dNg~Tu3wN0$??q9qEh0$m^i40&vI&G7Y8wv=sn(II!gdma zv)nWC+KT>)f_-aB_~AAbtj*7W>02SrKx|hbK|`T4iiKR%X*Wh$ts_=N*01j7yX_=) z9bK(+vx{;bgLMD(x|~_Cn~`3<_{5HG+#`zAE?A@4tk+W-=PN6vrsR`NPKQ`Xc2bYf zoS*JjpvIW5D?{P-8V)IV@RY+OW^41Yi>cqr%c^1k_2tMV`;=bi-o4ync#wMJskk){ zrMYkyZSQqXJqQgdqK-{rGc1&SxS(v@sr;GunnekAVm`N1DW#a)X>foy$p!pbU4U_0 zAYVpDER{G#Wy}_*OTjb(H`J$Um=F2|5Uu!ClfvmoqLX5a;@cMF%&g4}3?5_1qQ zj%v*>y)u15Q5*yzrKIpxF@$zrq>ajn=bGpWePnEA4dIXyThhTn%7g@)(i4VtIo*&) z9P^&`quFbXMgrv8&ujLFE_6#Ce6VHUxBgA8eWnObif?h)#V073R?op}8QomBSmYz!fj)N{KsX0U5|M7fT^t_gcM<~J* zy9|!R*gJb|ReC1ba*Fk!cm;<^VvLAOgMB_AKWi<4Ln1r4AU8SfS^IwC2ahos9c2xu z3DRt%nJ4so_nM4BDbXKWCpfUM7)40~2q<#pv*k2}g_AyezSb>8RRu>;GR0a`>)peU z1xLfOW@*djuuEx8yd`s0tVBIq&h+a_9AznDC(k_ONc5LVtle>fLcE@D~|ug zA4dssfnJ@0`MbJgM#2$?<;Kq~ag(zjUJ1j3qH-tSq+bZ4lB?U08*!WT-?GS+s0uZ)diUv_m6jMnH=PBEs-snqh18!6-|GRaCJ+hU_?(zekv6sG*h zCxRi|rg#bcitf@sd8{ZtQqWnAHzlpD7rn4_zT74Kx*cy%kqTDIyje8i+0>vTT~}C+ zKrP}6syKcdL+J468CgHQcbrCZ>E!`wGhGf(!N*$B=4`Kr=Yp&sbz7t9Tl+Gz+ntI?a7`=XFp+A+ zaj7h+;v@14O49inXDcdX+Z2XJlhVvY5eO{+MWuA0+_n?iK#QUs#bZV}YMl5i2ceHv zDP?->nHfBC@%L^-D53THs%4sOD{}nW0YvSx#cu2YPH;@Ed~bD1Ntsl2N->|B6bD%J zhkSJ&SBD|{n~&cpHgsM~a$zPS*i?C22TYj3Z!)ZWq?;gBOO8FuP^^@Jj_b%!iK+&= z1Ka;YOt8*#1=!iT7Fvc!1*gkXs67vLdPRY zYQ{BRX%Be+(Z0r0`#SN=3U!N+eX+9|1A*+I{tb3D`IxP-TkX#iFmDrHowxSn!gDjc z-}4NYKylL6jf8=?n1{LvWQ%bVk07U$M-mb8N)(P(&YvljBpGGKJGTt2&ywyVD8KoU zpoA+m9}NM9e9rb{I+7Dkx?A9w3G5l}>UGvwgPEDzr)9d7xbz|pc~oW^(n2ppC+d=N zlM{2yu4i%Li+*!5Z_Nb^u~4zX8Uz~3Bqy(7(+gR#)Z(03zUclQAf;8<9xBPz`?2C? zareKaGd?E zdlDhJf~|Lgo@}a7+6p)?u4f^T>>y~qj1|axD>pUmXM(6IwP!!^;|YL`cTt%pB$?$U z6&Awp-_S+uVy_Bz=gOPCCugr_Q(LicRv@J{G2vsX&D<1=m@%gxP`amrF|qvJ`?mgk zwa|3TjLAlU>_hpFH_c>JooPIcHW}JEZD6u#3R^FjmsGHhon0!?J2U3CG%xF@edfE+ zBKUNl30fdDF#c4~oI(^=n;%7`Jw~-kQJ}g#8usn1J@6nY8-z_91OMFlRGN|5&{2?W zJMQi5JAO&F_`L)TN6P(VA?mD-1AA4+$Lm%>R{C8VP0C1qzg*0>9al4V-xhf}Tmu*s z-3R1bO<~27v|pgA)nA|+$c*z$*AVCELN?Y+%yidD++>Hacx`?ah2rEM0~Ib?+lRrR zMOzIO;VbE9JNG>f)qS0vZ)$y=>9ND*uYwG3GP(vZkd9Ah|#RO z?Q6%{n4p5F2H4z}EBi=!TU%#<73&}TRRoeqoi3&$`IruI)6{zRZ2P)FbJ;SVJSdi- z(&8n21Em~XLiVO&v?q~bNsZ=ijsm95oUENmmV$_sD*fDsE+?0%uwiJx;b}SkH(ov|u^Epd0Rf{- z!RSCdoeUjxPVOrPGcM2T%pjO7Pp_>ZSd|R9k0D?fCZC`%I2ZBUyqB-~rzeADW+A^=PBi`4Iu*hL}7m6t@~DRxFo$MVikIbOiZ^;&+y26L zBIl5qP00mrr62c+=Vw$vdj3g9gPASWKrEIG!+NXV7j8m62o+u=ij%k#Au0wbDdl|t zqe4Mza6Fv?9h|&;z!cUPUkrRm*z*Xz0(nmyI&eN1C6>yPcxxaQL1A(&~5o(s?C8GINtFV#Oo?RwHe8NGsI(G$Dx( z{wSzjrQ)V2={Uoa7GkwM= zs5Ou7^;kCJll8VH9-XndX3p=X0RPjj5d=b&ao_k+7Pd$hF9Q}6tA}$7$J1eb%{mta z<%_wG&KD4dZ@yJ=xds2S*W2J3EY#cE)CyX{WOwg(mcAco(^Yub{n22Vqoo zj>`95@$bC=A5aqFGHLHVP|uzRu&gzR7mluV+DtxQOHk9tBk)0s;?`WP&TEWONnStsC4+Rmb(%BB~K_obeS`ZPJLp4ey= zL>U<=4u`bIRjg6h`K_3tjMVkc|Sx#QC#*6=_kA5iw=d`)VA*GNyGrECi|%D zlMLq8EE~B2q-d-0m}j2LZ+*wm9G-RPAwtkxF=|esT>5@?MT@b#=?14zmqhH!)*He3 z(Aowm5xvV%K;{6qpyJ5Z8IizH+KM7jei9tP=5oo@`K&CGpsREvrY>CF{3EbmKHAA0 zwK=0p&^)iWii61JMM^=mn!IHb)+9#(bJ^;3gm>)1$!WDoNW?cr*I`Kp3cFESq(a2q z9Nz}tP2!Cv+SaLBEm|t-=fX6)d$GAyhPRCQ!}@FLopO`Vic(6lh@{L#2Z%!wWDU1t zuCB{5D-dNqc1fsC!A{uMj8J^2;B}@T{;k}E*I(5OFx7)K+!7JP(aNo;^X=rm;2<&@ zF*bPI3J7DWU5oAGD+v-1{#XTuVwBl z!%=d5C@n|It6Jn|4PgN@*YQ0j9gFPi513Z&L*CHwqYPqG>FF9gSK`x&r;6H$^^p$2@dao7(5{{mSuxv4p)qe-;IGE;jwdSdqFu)(SpEhPJX%&kZ?K?zh< z&pEynw+)2#=O-eyEd-l>MC9&ftlAY9krt-AHpAqKHE~^O-;8>m6b_Exr#2S(1Ox24 zbs*i78Lz}@)^Aae!P%@<^L7$iljC1< zsVd|Pn`t9C6K&;9Ch?KvFUeD=OmNYP+sH8Y8t4*%MNVdC?m$N&YMh^i5V)z)m08 zAdsW*GzQOsh(i7=F{_7yimWO@J1}9|0kyj0Mt0x=^Y;S#hIl;MvH+5_SKprlfyz3Y z*y?0f98AjjE=yrre>U1+sc>#DZ^qt@nZ=#{E!8-4&$+4T7!F;#TITpXz=A(FIF;^F zg)YG#=!dr5L$6%VL)dnle*Bg$mvTNuz)Jm8LB0%bYdoJSK+Kj&0NO@)fMwZ4Q6h z`Ch-G=%TkAQX^{D6cX2)FM^ARd58&wMj5|h02en-;}_tz5P8_yK7oq^Z~gA_1Oo!JoOw$DLIH>arr1QeWl>BL%3^8+DPWi>Y|Fu=YQ_l5icqQB0ae^A^J62q!+@lH zq%tO)siEa$%{&QS#jEEbuuh98h)HI*x4uRQo3e=C7xKV@a{U2JqFE z?33P~JZWTnV1@9K#Imos)O1R5UJ`*YlCBdeY=V3>dSlaxNY^4!4)+iB0VIsty8A$oQ0U*i&6NZ&}Zg6CW=jmYr?8kwf&QKeG<|U zyf~VSedFZ%%ZZrYLhQXETpE#Q;^%ghvV!~yL!WznuBoYC3KG{;SbY@?n@t*9890eZ zd_RK9qUMvME_3vp+OZS(mrc3}W@tebRtRTQW2EEYH)eWQV>M1)=_N@u_NSl5Zx z`yK`x_<}4ZyWtl|R6MHxHfeXSz&|#mdQNZDXf-`umLV&)+o;p7ZkcVJi!V@bJk3Wh z5Y5q@DOe`pVE;2Bvp!RL6XjRw7pz)6Jzv!o zi0}7S=PaH;BA*dsrt!I25iyE6um#8|+ZGoKGEHfSV4n)|zrc(hM?O(hA`X%5-R4+l z3J9=sJT;iN(W$={5()I)5ED@IK}&se{RowrGaPUKK_Ua;Bj37eZpUoYW*vkmhsLki zRN~pQu_?@buVArF63tciR*z$+rT6neX;F>o)ogFq>ixXUu{y@RjCNvR#cG0tbnq)e z{ORb(b!{A(eRJSCL})jTVMIiHWP(a%H-%5zV4maNzIY}Y^S{`8>%SG+6r8FdmFNPB&E88j$(yA;?&%!So&6*yR_ep#tPXdQO_ z22kA^crL?4e@>p+GpVURw>Y?9Vs-Q`Ig6w4tqOmwGs4|4aGi7|tzt|Yje!54LicKZ zxNfea$j3%XgU$NNw$~I)2?1>Fh)#5#z3&dFHH*b8x`DPSjX!3lF&}pMY8dSDmQ)Gz z*1aQPp06-eI5$BE1w8y4qi^L>pae!@YVDny0olmq3EkS_x7q3&)0KEC^Ld*YQqv>b|mv3Wc+j5Gk%SpwH{N8rl#{ za|;HGJVpgGMDA@J76wObt;Q_M*Q{AZO;qK}AYcnkX4L6zbAzBSazrqt6p= zN-Le(S#A~xpF zumaSVL5gsYkQ3DsC-EHYjsPkfxsnXkel2xU!v2YQsub$|Vyo&^v@(#|Ac93DKadgT z|J3V~nuaQ$W4M-nvp?g6`0Y_RPGDZa?>;K@P2}=VQiAfG;Qxz0BCRQbq_P-?KOure z;~G3lXDta^d)|JqsS#Z-lP|37SOAk#%{cFU_rZt!j&^vyJVaWDo-Nq?H{fn4;b#1Q zRwdy*w3sjS0WKTZ!-s>nm^K0_19+{Ox*?FdSg7^%cmqeAx)YAu(6&w8Soh<;*0yl{ z%gI^&Po#V?Ko&DhiAb)WN{(nieh4OmFpHYv0xnXt&J1}~32rJ>wp69F@NdDvlMAZX z)d;33KvAFV!GmWJAY+Cw#vCd7#%~d$Ore+7S5XEMarzsdg5CtgjQ<3(2M6jn+1#zl zh(hIki@B)W|0NAE_TAb|)e;Qh6PQTm4tG0o*yP4La$JWRM`H`}X88jSqgDfSh<^iS z6_Y50iP5h_<4SeDD(ZDcR>3GqvuE`?Bp<3tYxnw1b5$yqy-8ZflRi<_i4sc`Uj2?C zuC-(OZ$Q%n1KM_F-r{5E;O5 zn}T$<9yGY%UaCB+x!vflQL&q^$5+(aU4(6RqOko#o>AT1D(4rTNXxpQa7KUfRD*^8 zF_YJ3xz%ov_0q&D}TT6+FFEJ2FJCvjsa0? z=4~~Lg#N5~ZmfKLg* z;8{LGPt9Q5mraMRbIZ8vif#H-gl3lF3QfWB9BD&s^)i~)!F9p>*=Iu#-!p#WglS3^ z056fRVvSi%S?-3T-Fa%Xk`MKSSK3DMC6CPf5~lBQ@*CIOU9AfEBti;js)HA@ zG*`GsCNU3MK-Ci1L&!2(A$?RemDnu*bWVJ3GJ)-P&RCa4%04W|p$)AnaIc~ktmLSr zQBUBWIYu)JejyOc-!%yjpvS48q2FtN=r}4Me=i&)6g#EAvck4+s$2Vv<*M zVj{4zvU#)zB{d`oP5hXIj!^^bpS&rw-+zY%%Pj^c}S9*w5p%>~g23qO0F5YW^WWtLaK zhb^pBVlprkuq<;PCnKDuil~@boeeYmyI$kel31Qum>k#9DS_-yfdHq<%Mt;BC@a5o z5r}-W6GT`Qo9|s-X!`5t-{ik(FNcWShprcP3su*oCERsS#fpLq`er!*ewUz zoHAeoskVT=2`ctcH1aP3OPG(@@p(N-AY^JF^NAbNTK~;b)K0U)s9MOKrFQsS(HQJb z1*M!dt$ix5;>RYz*Hn;_YQR!xKJ#ZtdPSWMA}urAN7XRVJ8NJ{Lx(X#4C(g{1#R&F zAU*_$F_;l%?O)h>utr2+vvlwh=e246Pn9xq72!T@_kYTRXVJPiELpYMcSEi{h()LZLeG7ofdPD%3>f`1BXPS!tP zGOX*W1-<0|gS~IPW~h*#CwSTIP{Aa7z(|Cq*UFr{$Zmv8Q>8|tVAyN-sz4(qUIvHj zb)bqHYGZ#FbqTB>%~vHYqRtrICwRk9rwuE&T{HH)e=hNv%!!CUESA0NynM-7u4EE1 z_{22slk-&4w70!Fu;t*^b|rxza&8d8XIkG%GYQKR^>*b*2;5t!dyFyal*wY-&7&yI zQNanHi50T?`KnS}7jBNn-mt2u_i-5Px#Vpsn7R)_zd&`3wwtnVRq8RHYt)qb{>|qg z`VPz%gl&>>7MgTX>DM}E>TTIAW@K&3IhQ2D8yg{q%FM~3?nJ)(138%Hta$(Hq`1eG8;8)w;WpgN9!#Fc7mk20Y=k=M#*2JwdFw4m;Ue`4y+vmzp!~&_c z)eE-*i41=OrVpaC1K}>74kSF~Wcg-L4NEIVYsi&tSQph53A|=_S{9wi6f;XQG=zd@ zqeJpVPh?DYoXckhb#WOOip=_Q`zgTJD52^#HnULEHYH=fd=ZZbDXm-VGfCNW&7suQ zYekM|J0r*#5)0e;o5lD)=!`K5q@tZ(6 z#k7K99cj>{r3+$)kHb2@Q)9A#u=x-DRxCH;J&{y0*-l0<#9&6hADfwKD#A@TZX{%#!kmxfiN@OHe|1{nxmdRQ6mjr@v|uBSfSPPuRmd##5Tu-Q<4v z;P&Iakh2f8{(1L8XHQ`5h4jx=P-Nw{prqJo2Fr#sEK+h_M4;YS*SZj6Ml*&r|MB7dynS=Y zEBzkkFA`$N`;1=qXk}$4Fa0{39`YH_sKVzmIpZf^sOC#M?H&tUc$yP*l|+mmnXVE$ zsE%{4YD$s!T5|gLuHv|gU#!0T!TBJxJ7xTX@5c+;o2m}|u6v6Aed>R&+91LVir+}! z3w3>x8XxvoOqdrQ?^Z4H7mc~c%2IFnN%tbbJ#ZVOGP0?7ZE+*$vN8F;Y)=J}u?9P$ zYnqo*E=^`IgF@*K&!*&JCqv8Gm6P0dq|32=7|3_GJB3yE53;4Uv^HEUUX=J&g?iZp zAg<+=3_e}>8urHPfR*dv4qM5bzH^J>npaNMal?d9{iQ-qrkE-H4T$vie#q=zJ@4YD zzyRI|3@C56EPY;EpdO=Z15Rm!kSD>N2L+ll>kV97CEh@|9w3zWDgr<1)%j5B`PN8D z{st5?GGjb0bye!r#1NP7>5B}wpZno{1FXX6i)D*Fo5EfmvYnjdr2YsMP;0IVyi#U6usiVSbNe+Jw?x(XR7mtHYl=h*B?AGC@_x-;-zE7sz=vkH< zc2AWi`^dZHtNKH4jCP6hZvY?S=;PDI^x%BoCo=CFT%RAY01~z%w(e_x^Zp1fzeDV8 zs{8{?U4BLtc|id|dwH%miD~jF(k*VM`^&T}E>zzSyQ#!=xwrQ4`^)sNV`IAiWbf>? zwd4vdUp22~**tYZR9Gbz!46auHqXEQDcbt?P`=O=#XaIp)|aZf=hPb%>f(j=7LKN_ zPRc6HYO3WEd*tgBhW77r27cV}uLRZUYOwmI{=_+sy#<1$yGDKkot2koa&P;Sv=G6d;o{vVs{4nVxud5s7xAkvt`$0t zT=69K=9?2?&XFU#KSEt)JTAG5@Be%`?zz5y;?4RZ7`*tl$MH8{5&Z`3dGo@SqVG=Y zTKk^jSZW>o+SWlk8-CV>+h*k6aijPV;(V;h^X>Zt;6mZ1wv8f1M;@61@lmuR%l_Bc zE;3O0WD;r5bq7KS)jBiRUGkqqjp1Ap-@L^a;soRu3;xnO3_Qu$f_UGVlFD@`+=T9F z{VC{=v*TA@Hdp78sE1o`O~00%Xn$z0-yJGvcHO$P6W4rfy^FnLHx635{-~%hTmHC( z@?oNOaP0caYmA0sOP3`AIuj z)kkgPl!$G3!ILGxDzX3k-iNWT;Hh3`+BIMd`^YDqZ{~Mel1th5pK}L4{xadFSezoUWl7I9 z(a3+Z)LF}#%Ic!Inx9bZ31f(iqVXX<_Zy*t<08BZuD{CooY{Xf%r){r z^6OlU>`Bx`LeCcyKjErp)-P!*digOumxxX46(bF^p^sQ`Q8GiU-2}JYjTM$}0x2CT zzKOEOk2}LbEblG(_uo*3GP_(6B~hNM5;|c`irqZglDbHnU)1-_F@kHMe`lxchfYT* zJjt(LOy=_C?TLmjk~yq$ux0_W`lE#NcB-Ez_>n97U#r1#$f7~&e*WGX-)uReiCeJE z&pp{Z_!e|>^Z`W<&q6LFru z%SiRVmhtm(l-L|z8gW|ov0A;bvHz&o+bozR{~<^Alf{Pae;e+AU+K|%=3S+{X)mjb z7ZYy=ULSQy*|$5z+Po_8@{kGXnq&Go(vlwAZhw!WE3xl?jFP}+1} zCf6rhDs9%hZ2IN27OSC^OJEfXwcBn@dV|4ZP$^QTkV!YXLp5yubCq{28e2>r;1;u< zo!3L6B~aDUTkr1oNG|Gfg+E^XGhUoi#9p%-yv*7zk{iPGH1VRSd=Af zOK)#7c{*gkdiSu&EG_4bhw1a~ICtRKhPzvfFO$eP zLKd0@{;^M|uJ@32`=0g&-L;mS_y6gF1-}8j^n7mjWE!ziZK7# zqIHp6bTl7rcd~5qshz+$3;KT4zx?h$N7-Qh7|H?$2<$crnC6|%yJML?GLsRKSDVnr zA24gUe0KEg)@8%#{dAR8*T+qizWsTkk+VdCnY5x5yz5lJ<2d^PzOGT zxe2MpIQI|rI?AfcZ@XIX=G2)_>e2KvsG)q26z>2!V@56Ut_Nm@cAg0&6iO;TuLXa+ zPRBlow-*{f}{6Z=_aV-Q|3+*PUfeT`f*|0SO*aF$@LM8`6yc zDb(Q3OQh~{So{7MBYAUWy~29ItPo#>{(^g9HRRLi$*KtEY$bd_t+9|I&;ig7^ z)&;+luCt-rUP3ZsleqiP&ZA4TF`?Db#Zt!CS4!z=YqPo+>|z8k&h? zA_)0>NpMk&q_qCwnffu{X(O3sl)qXxq^$Ym{D_EuMwEfxhkf2-t7#D1>3yVzJIS< z6!$&2lJSXc9`5XsWA({F7TV{2MVUF-g>i{pZX+%y`18WbS0SY;jkayt%tRnZO@d!k z0k%;M&{GL@hJ*vsYV46f@uzsGieGD9*DNF6f13guk8r@S&T#Xow~_oI9dV6<`~56= z>%E@C_m%f_-RB)|8o!}m2>g&TUEf3QFVVDn|IIVk3O#>flfpB5K;HxkwFjU^066T*T7$#02 zRv?_W{Tij}oh|Tqd~tk+GM_Y%IU>rGPawb;SrtmMRfauQ*DV9xrZe4&Ow|I1@Rp%W zCFx$}H}PA2f6^1Z13MTJW@4>2uhiD-q|_^k2Ncoq&e@xG(7j0W`ADySj8ThZ10Sl9 zbcJ_N+n4apjhE9QnAp@I#AfFsb{FFVJI{@ z8ndDg#yI#Ps_cHPRn$+fComNo=IG98U6v96kXm#HZ!~)&q%$i!aDq#O8Coww5O>oT zR!az8L&0GL$uTJGK@^KwHwaTU(<~ccv}kYwaC>u3v|c&1_Li; z#tS_Vb-s#iWEJB8iKkSCXc=a*lL51ZRX!C8H%2O3gqNr41yBcs3t`eE)yug|Bn@Us zNh<_1-LMMN}QB#431aZiI`1j#|Twq#N6* zwa`!TB9<&eA6@~yEpLjJH#K!$8eyp9<=t9J-LnQgdWJX}i}s1C#_J~w04OX_Qx2ST zA%b)(k5URe>PynMs}d@*nRTcz7RDsgr1BaXU>J;(;<&jMC6+j`NsRcS4R%<0#@Jw(t6I|px2=aSfRZT9N z(3wkK?IStDdjH04t0&TqC;60*&;wa8L7JrBg~{?pXP42Fg^K_x?e*)Z&?;OLm5-LOU+u3%hS;ZX%w1@22C7S z7ME4>?$WD9K7TWL)olK0TI^WAEYV^%P1e$%$^of2n0+p|tX=D|RTMA}b2ubyV9`|$ znTDDOn{Q>tFgMibNN@G4*v%%>WhJEHakD7?M^oHI=!{?JC zpzBWNoh*a=Pwca~?6bMk=#gcSuE~0Bz6UxsRS4X*ljIAFXRBHubkgI|yVPVt4rZlm za!&!1+oTrU_Hh(_Vyrx}Q|zXIfzm+zEph8!t!e6z*Oh+1qK6darUi&m_AYi0%i@fP zXcc2gNqi0F5cvhlRo2@DZ578LiY{RRO06y?-gQPgzKHGl>}W}fc)6$*O6}X$#8o&^ zQZ(Ae)UX;gJun5A6()w#XKEBM_H=>ONCf?&BA$w}y@&z5&{`_+pNp{8r*xNxjiWsM z^UijW&*Z|^b#4SV7~c$T&`}M}ylG#hBy<@VEP}IKpEl|O&l({Psb1!d$3c5*x;DkI ztCkfD&J{u0VpzG*W>r#ZeLZZa86dtHY3VzRvDS)oJa#H+n|oJG`+^k^?XL%d z4B=Qs@0X6JINu&|h(A$2B|Nv5v?IR&lWK&h&(8Q_|BNNyaO1=U*h;1JK}i~dM;lbX z%DMaX6MGwA=5ZICVm2JI+QgYBKw%9tC&|K_b2p^0oixN_L$al;?bM`KLt7<59(WA) z4g>0`ylI1xXvn1B-6wD-&GUg{^u`Qf$G!T4O3hIL(rm-rs>+4-SQ=`NcVt<&-CoP8 zV5Lj1oPAKjlxk+NW$AOe4Ef{30Bi_@4_z5hFsB-Q+wVrDoN8?fD6UkbQy3p1DAi0Y z2^79EL;}x!qv+J`(fC+7D-TAv7hb;jb4^b{DxRo=iAT%iC_INSGGK*vr$<_3y!>Mj4`+qZgXg@^RrvM5;8~_yO8SEjp7T{QlC6`N z%)n^bu^%SfHRlqi(I^zdhhsgL(%mZmA|$lTzP9R(dD`UU=m=gwdV1;hgng!S&S}g&h6{Ut|W0MPExhxW%3h>pVV*@Uu-$)g$Qb~i0CIg_#lI4 zspCtr*Us}fCRhqLmAZn%EagFWpV-eQWplg45ojDaRObS>7WVF_dph(_8V0MAo?6Ff_ZPl7|k zM!DVJvplkTbCKd*qf}5Q;t7<}0Uiv%+4wP)S43Ei2<9u+tePBl56ChZJK-~P;Tox& zkO?@|ADp378J17F^2)p9QgBRd-c&}rLYc=xA`gmb0XX8AWrZrU)#p)du7 zsle;*&AFV)Y9J@-M)69-pDK0cgFTlPAC-H0%WgI)8+ezRTXm<;{ibp}2MYT^^w)xS zqgEfKMe7MPqomoHa%8FOnV?3=ID@L`k-sq1-j7Y$Loi{4j}kv2g$9Zmn9wM z5hM6IsBqyN>YNC?2Z|wUFhhbexXu)UvB1Z3i9AIGH|zetevI>nJ2VNjA*KAY%2vMN zU+S3l31v>qyXH?v-PF<%4PUvgZg8xuo#JSI`(g;{%l+HUigPkzp_@XFnrM@tZ^b|}(P(XiBy^cFbsJ4LfwzgV4OdWJt- za;Fboq5;XK)DQ6c4mUl)2|WrKZvn%vn}$9&;&!FL7}x*ZzO(LQuBCUb^wa`_Bd?Py zcdn#sMUb^^2)P$uZck@z7Xvp)#QB(Hg=x5vGS-t(KsJW#sDLTAt&Vi5GW!X)lCy4h zjyYtpFYn4v0jFMqH%OUo~6#=Wm()1|SG5fh~mp zQBU<+OzMfKj(?>v{iYHZ>c=mt;);Ce-Qc@{hKmBh9?;Z%{H(ES>)%b7Ubw9v9XzIs zzWrA0^WjGB&U>u{(jI*%-!c%qP>IY42(CPTKFP1{%K(cHQj1Z*DGe$UzS}e4L8R=k zEdK5(cST{xv0fS~pe*~EGL)lCivn6XF@`8ijb+t2<@<#jcV3D+kGq{&+TL$e+;;k+ z@9rC$TRal%?4CQjwXnk{uMVy%mU9hb9lb<1jjCz_Ahunv&p8?1#`quKvU!{I%KK;g z^~z`VR?c53UNA~2zs`@5w2%fD$Qr@q5Iz_%>yH#l+b=Kovjis;!DVdUbErwBPN5fX z|5$`~OawCdtu6jY2i6b|O3u%LqS;WZYhgXc(cd$Lr7I3nby3g0SLG1-O<0E{Qrm|l zC3iS=(OTV3;AeRI*7cKOd9q#veqP`2pjeqkBtFfa_wRY_viaxjIA;`+QZCgrpg;|0 zxR(qv_)JHr)OTMy{g0#SOKR#PhnA*dRSP{kNMPlE2Fii%k6F9>Lf_*5{tNs0ScHS@ z1*sOx*iKqFh)_z(zRp@AQ_}Bk6}-S0yN6vo+vGOt<~F3+ot4yKN?Hu|>Y4!V>sG8s zSmzE<&Q^z%w8#14Uf}BwyZ#Mxn@)9O{dbB|UftyKexSU#XJoOrjz;1i({26+jCk83 z+W$^?*dpH2qO3?63`BQ9UMDws9y3Z5IJ*pI;=Rh;iJ^GW1nso+vOSw<4pih{51*@; zH;he)zO&&1S+_@C%$%ooOz1Z^E5d{IM zStf@H^OwFzLW9jM0?l|MO~N4?hQLBtS*F~rkyHXHM7Gu;I-6;Z3r)Z*w4JDhq}9se zm@uO45LOR2%s+2g;_apq&Du2FaPCF?du*9N6-5Wm389kY<+T5~A1>87-4)SKOL$i1 z-z9@9E)TWsGWc3_QyP*iye z3wmlSTzpD3dO)@U)makHSX=J0aZ|CD-M zZpa#SROZ1GO`?6=%K}`7Nm;@r=lRBUZA8MiF$jJDDn1?j}FgdUOj=Q<%0*@XRFGeH??=RCT%#a zAl@3Pq1j3}?R_<>un;*Y8v_QNI#dU*wTcWhjfmB z%HK&jPh{YM3D4|k=hAKK5mOuWIlxRFsoV1$TbUH|DJnE-Qa`=QK8OSw%Zza^i%GxY zOJ#lYAP+q0#m;ebZ zWT}iuO~+Wo%q%>z)`2#oq?mva<%w2_WxF}TG{`OF(}i}MBh&1yqBWUO@mvmObQ&}^ z6pqFS1I~t+@wuh@zdWoN3*)Qm$e~-8OFu8Qrj|o|Q^68505`9lFC zz?J;^Ea)abZZ+#x!+r@1xHo{$T!~Fzic}CYEbT^Z zAd{`8U_S&^xgJiRM5Pz+IL!y7V;{&kS?JW`w2FddJMoAJWO0e`2NEz|6X{(cGlLMd zRsr_!V)YoUl8+vk~Y8*0lxpR|F^m zR3x}?p!QRQB+Lu_RS2y; z91x{HT9q(`P{f@NxsrlA8Tnbbe3TGS7dd8rr3gc_ya0Hd^_4e8m!8@atM^ux3@F5% zUmgfImAsXXFKJU@Ep-=GJlmX#kWxfaJX?17x_XDJK1l;9omB=cr8GhKLpgX`N?n`1 zqP%QM8mAYTF(H!{-wDfOen)WNuGO+xa*RtGF{NZ%BGpFJwU~WU(F}?e@U7Te-vRo6 z<0P~frK+FMV-(a#n}8YHca6No`}j=~GDv-!hoqe)oenUaT9gay8!jasGP7MWD5yGz zP`>U&pjAqQ<{qy_sYLgprfq~1XDouMaMLov+X}!4B@QwI(sk(Es8Y`su_aDHsERbY zKx$sV6u>~|amL}({-@0ha-F@yFGI5HPM$k092Zku^Yi&%zB&+-T0vuE)+C@FE&9mA zQrSgP57^32Zh`KEZ-cwiI}>Jrk>TAET|B!SKVG~?KVZC!&AdkdSB)esHgZ+(M7n*6 z-XyFdUoe)3`0I6t$OWo*-_ZKF917LqA)=i!Zjv=eu)^}^W`dg;|lXDlU z0S~=s1TWcrM;L{58S3ziaOPVwfMGjewOBbbpp~rQ8~~)=3!X=-z>0U?4jgx%?_ByEt9a)t@u|2eJhUgES~BVjMK%iMg84>hAYcVpk@wWO%( z^m|bgc>(QNSV+e$%7WSwLTHsHNcFjU5Z)>*$Dd$?6Fxp=zSS7*_sm6rMvaL2x+c}C z!Z64FXfu2Z1j#0;wh+>_5K@=r%aIERq0X0hOc&ot3}%x0lMxO;1R9}|eKp3jd`SUL zaWfEUHDWVsG_i}(5Ck7E3&RqVBl3L+0F6Lefe*jqHR~8|Or?>BZln_F2nJ=`A}k(l zo(18gSSnN%t#ejR?9edeqs2!n1wpU#vYN`VHB6gC8pr>e8R7(2`lEQojhu!c-@)us zs1KE5y9oY{T_&`a2v+ug4kp1p#3O`^B+Xn@SqYZ?#76s2Iz>&rHJ|x z(1qg|gyA*PA%IzW9K~DmtY~=^A`;#e1`&0sacmABpLVT-D#fC);M|m3WSiuhBAIFF z%!Fz0`~Yogvr0Ps#`r?}h;y0==NCFra&SbX54Ae5wrrG1TOMg6kR@?fvkWZ?&M#30 zVDi`jTFh7ptp+#3f+mMKi72bDp?L>@REAGRJ+oxHBR<#lCKH#r=0S4TvXPev>uJuJ z(kHK`zOMTbgqe#aDLaiGT$4Kr7nxm5M?-7nGPhMoVo$UVWR?&&%Yb1E_}u$VqZqWV zgDV)Sh(#?VfYfv{TyE^VozudPfzD(%WY#00Y~>DM8oi0Cs3v0LH@U0PsSF6Qi3!$7 zZF_3Yb`3i{3+SGg;;6CM+KOjOX2EtSm=H}k!p|VzFRWtCmsi>zt_^%AQ@Adb zWlTy`PbFIF*}ei>tLF055w3@sB0jon-^(5&MJbD;(P)!oqPgo@lk1|{GF7^$C^0^5 zI**Zy+xd9hXGu=W9F3JXEn_Z4?_Pff`UL{vGNVKzLJha@NCDKP)?0AAay}fB0Kn!0 z=-#6r#@pfFO#XqS+)g*Pjl}6oN8|K_9n3%fVq-Xxb6$xN^dNFGxoutIpsq2?&grk6 zDFjlNwJNh z*=nQ_*Y(O`X~>y&SrUp}U!u+=;!-$7QD^sFM|+9fQn(*;+!qs_&1=Tc4*C}kFydv+ zLcOqNo7{mmB{A89q+E8zgwjOwqCwo*Fc%P7YY-nT#6)i8=EtT`33RTa&w%;(G;5PX zU2Zjmn04bloIBI>t2d~5;Q7j<`S}eAk4hwGmD%h>Y)By~@P?!1Q2d^7dd0{tTEf;L zUf)bJ%F@?9>&o}WQFJ(tq%oX3d_j#nYqIpp{(MikI3JTFHUl_8Z=$5^%@F^3?w#U$ zk=Qd(V(M=|q`HRATcVfSCk1pQ{^+?EZqkCWob3zxPj6eqZ;EE4#_~H&%#XjDi&J-U zmemQwNRb)RYKj0vPIUvY&JX}kiq8>FQ#kZ z@y&zNTj{s!)7@@Z5CCtS&=b*$8bsJAnNOYhYm z)1T164GPl}+$BOZrxv;*1T6A9UWdg+QnAg2k$957^EIhp#i3nA{^WA|q zFn9%qP<}(=%z_RP)Fa1r&>PwtvPG9(QNFB%W<E8AJ1{QH5LRMD zlteUm&ZBx&r)JPPP9qEYR@Bsi@GuxdX~zzdgb!0XyH2Tg`Xvva5KeN3tiU6(vPR*$ z^VIN2GG$X9`p1!M5kx9gX<(=NRqdIx8QrFNt>`097!vOHIhR|Plkf~8x(h@k;OMdf@t$QK z+ZhS9l`x5`LtKk{)@i_(hhk6K3m5q&#*g$khSLz2t$!K?*qK(su1hW&Zhq z;H)R?dvGsg+wfIm()p^;iZV!*dvpCoa=7Y=q`QHfX{d@QyE4`@B`m@-EqW=p4S0pj z%H>9CN^hbq12Q_t{MjBbjKxyNY8Ro&QuX}mQA4c+&$H`T$6ZElgfa27@MO%J976)rJ$A^Y2|8N<`{I6X!{ap$&DLDls{cT~{5p6=#M zpBC<6_ABLkAK(jC-e?;6+T>NWwgz?7R{4#7Y|EPsKwf4*2xz9@dr7i_-7*JU5iRE{ z#Rro|-vF_crW+|K^sWrSFNB*Aridj3W}dW=0{@`3)>OM#{ZU@Ejl02v^)H_-8fzf^ zAD`VYlAAWspr2M7Y9=m&`5zsWbSw|y@%4Xu-Uz9gNiYR35|6LJ=C+A~;bjCg!Pf|F zs@%`1AL8(q6E0QSi$+^-6I_16E(r0WWj#rGS%e_qeueAOWMEq`wUjR*&h8RI@t^0k z)!+#%4BoE9E*>BY8t>F8c~BOXfqMVzFUjHV0ZU`=iuVsSW7-SWjp*LwbSrVtI`<>&3?E0J z`Rf&iKG#{h-a;1P-ba7G%-AMO5hkfyoDxpDGAbsGWoDFM)sG2X33wQuD!S?Q8Bu7uMNSLK;P^zIMaY-9<+o)fGnW&kq6|E)j!nv=ZaK#VDi<7RB0qfkWbcq!8WQBlkBml zdz|KQWT7A_QLl^j^`F2;$*NU~hOduyM`!POHqQ7n1_%dTA@O=?Pn9bIJrE~tJ4K>t zcZOkdEJX;ZH_*rH#n**`_=k$o$9fG$6f>wQPon7jp4FK^Q}s1bjZAJ=##GZ;ZSr)D z6BeXp5%LDb7b{Hlg~w;)NddHo$#BJYM;Tjk#7RVzcr_#xK&p9l^dxBioqdJ}{gz~% ziaGTDiy$h!3KqSQf|Fu94IAEfnpM6)x~&$g1hPB*!)8pBW&HEXiCIK4nVK`szPI}+ zT{X$b2nL1Ogd%C2{iI=t6>w;k*8e@3K^TsA{|E+m84-^NTH%Tuh;|+MjeU&75#8Ki zfw*X`!leR>%2@afBiDN+jjeK}shb>9L=3oJ3$OMaO!Xq;6O=mT8Vsnh_OfK}UqkT) zkkGfu&^&voAq7~#jaJp$eMNgJpe}WxGRe*I*zwo3yabY5k?Nw_~ywn=-l z%&kONb_%z(+UAl;#F^@;KA!k!8o2Ms2qw<__eP=<~F{nVC}^s9bdY6#r?jy z?poBwmVEfrQ(498(L>74G2AZf@w59$aCr%2g^TwJVr{{Dw4?8$4y}dd7MfZ#f zJuLLrKfIW86;yEgbC(3Y3~u}{;@p=;b6;KV>x^Z{D^D5!;%s)}>CUNjI`x}P3Zu1Z zNUSRvh1pcl;Fb_MuU;A)htm|6SF>;)5aV*G7NKfY9BQE*?G0n~(mw9?`V*8z4?ot$SwgrF{J_ zdEp7`@vV&~7w>fBKSr5%ZQ&s|K8!vZ^v}Le+Mh)Cw&sC}CF6~t#LSo6ocB;*ak`bQ)1-nPQT^oX*Yf0(!`C55kp#>A(@@vwu(*vN z-9HJ}F-B{nkhQO(z(Zp8i%%_7Z+}Qe6P5#)bESWAv-$J!R6F{ z=CjMKIvi(l_{&N*^Ke$`ghp+H}{`z+PWg&55KfL{AE)xo_e%5_c$!~ z?#q?k?@`M;$S`S{zP(K{9YRplLp}Lf-4$gId8ZPS@0U7vOV4|8&#%NTbzWuUI%xc= zUXgD<2TSgR3fvhl{r}AdZRT5VUy8cl=<}EObio|WRnwM)q$amZ5pKRGGgf?Va!`a> z$+Q`EKhpUqk~#d~K(casC}6yA!8vi(;g51QRdBcKM7+}GmPlJpa$H}(<1RswC=|vq z-^W$=a%A3I-MJ35{#V#<0PFW7yGIhQ9KJTBoVtAaM1I;I(RECGtiZyvcm`-RvHXT# z<8CA`3}ZdGXDQ-Vy8Qg+VmNU^(~?_FZ&ml5%3wCTex~oKtC+L?!`{cMrz`J#FMaJ4 ze*|PV`uB%FI{U4{1bR=; zS10Zbz@y$zHY{s0ob|PgwG*;a}!jjgPssa{07*S&gG8NKYjoDRKngbYH4)M$L5X;n`kt#-&OT3 zF6{av&9HvA`=|YxcT+8fzu0%)yuQHk>!xN{=g;z9c=eAZjd(y#%aV{a_-2uJmW4s6 z%>Vc6SsU_%yd;F)=yxpMH%jL;=$AWt=O>#yy({-<9W zCUbI(Y8>k2?sac*UOPWL>rL%2PdvH(Ch~MClDa{c%f0j=MZ;^)|A(*l4r^-Z`an^R zm8PN~T}42p2}p@F6~RKWQbP|tgp$y!ih@!_N`O#Q4xNM^2%(oyLMT!~2vtbvp@bGG z-gv(EeD`_p^F8;UJlV<2-ZOjF%&fJ3zqLlPtK<@O@qCkD{svMuQ&c$=cA%KZGL(n2 z;6FY2XamQzynJUDe0PtD=5qIxXFN7eE&OFySk7}`fqv)QSvpvHZ@@vMc(x~kEAIBu zBTVw@@#S*OIJQ$F9UsEtW?P!uQ_9#87gBj=m!mz@|(gjK*WIRjFa|P3e3rEVh$_Br6nJCgw`@Kfe6KKiDhLg&Ntc@aC zjWuaBdnKls4+}U@Ncv%LEo(#fi8EJS+(c1nN_eVS+6O4}STB+Pmdp$7(LxHbmkYZq zIGBmK{tAV9mW!a|O1QoTd4xpXz==Iw;!uwHj4E?Q>kiyYKCYNUZ?ji=WfkZdyic1u z0FC&o$EwZRBx}nlWw8nzy`75tHZ|`0s1PCMxi_p;r~m4G&pCykMqdVm{e^0GA&MNg z?@RJGF0lXi6pRyNQ4h|x^}RaM@A8M8!N30NRO^4WMeMaAyXS>SZ4Un{_+Y5m&0*Dm zNCik(^3u2L*~CACNthZxcnV8gslZnX=*LTi47g^!jrq-TZ>^($jizhiIdoal*I-iI z{4blg=V}@_IiXz+z4j)J>nq*Ncu~%t+_?Y#)^L#HKMY>|I?Zn4ru3y>C_s?edUXz! ze_Ai~->tXf>8_R)bTCjanR0lmC({LbB&}1^(}_3DC)<5nZx$I(ca2z%24_6PAVhez zER8wg7LyfRf!FGh_|LD)^1Gw2!|En4{nU8$Tmvv2)nCz|r0m(9QM3-!b~#vY_3KNmV2LhZ#)Y^d5izcyFUSPBipdz`^^V{buE}l^PYV zI96dL3~u6_6|xy@B_f?O5i;!U*2t#i6jRr0fk5BC`0<^QbZCD{s3>)9H7vIB=uyaW?$p`R(PK+>_ccS}!W z;PKD$>H0%;9Z-{lv8r8ZHOxC|Q{?j3yRyPIW^B)ODCUmpME^g68$EH@w@!S8FOdDq z^=;g+FKg_7WEWFb*7-F@tFuPie3FMdgf5!$YK?DNf%}ai%u2bb`284@arK)e_v`rb zml2tIsEla7@P{))3t;w;RSU!C@6X)_Pe(3$RZB=x0v5kHuh5~d4p*b5!pa@ zRKb=kzrN$+2lEMy${_+jcYB_dr0wO2=NAiC{lbh6Q@$nOt!g4-E?0EZ%lG|5XCdZT zzWo%&<_0BkT)-?UDR1?gnVFE(E|||!y06z?mF_#!ARE;7i&4Vdpkx)K-FX-f%Jmn7 z)u6pDU)o+cdyX}k+@Y~mE0GnKi^Mn3k@0TZxlm&!Yc^uF9KHL3_lkdDmGWnsDVgU9 z$ef9UWzoAE_U`LbhGvM~Xo68|h{?*Cj%}V+!_v-sjSNuqXyH_sj4Lw8tadF+S;Md4 zVJ*3xEH;aze}-?)6Wif_v{m#4qMM@&PXP8RA+?+>KwO3HJrj1I=g=1<-vd=-rmS|L zzENyM9Gl;jHu9h81Ra;NX=E?sI#cd`mlj)zu`RQ@bo|!qf(w|921u~=?bJL1M`?7z z`MDkcabpRc#Nzz-IpKnx0=L@AZp)^MSYeWHh#t2Vie0EU?IUBX=H-b{VW-d*J+rYn zxL6vTU$xfUiFx_mWYKe8)j2|^M|`R$m~$%r;KmS_tp4Vxm`PV;%3%agp2p6Hh_Ok# zmC%${rHVw;URc(fp6WGnEYkhXR3-kK_`I~k9Pfh9W-nwWcgX08;XT9k?cus&3emLQ z)h&yeV)DxzL|OQ;fH!3xv-%}c8M=_2??d(gp~+!F=<&-*w3-VUk=-wkOUwyWMFk<3 ze%yL@<~K|8%ZD8KvH4>_?Yvy(F?QUK)zH&MELRfrC8~QJc?@HO7c6uW0!7k2CCwQn z>5(-#U0cg9@`SdI6-q&mWp>yDhYUF3uPkCG#t6%mB;GQ&wX?bKF0b)Qcb||I75Tyok7L&JiOv(f%j2)}PU%pV{W#;KTMPo$h&Z z2N{~B^IK?jb8F+W&cgI=>Pn2it2S(O?zV@t1dKDv;OL`jjN@;_Q{v99ul! zWpo{OL~VX3J*E5DkdI?02w@d$k&2z;xFNSvlxs%UBji(5g#4!YN zZz?8jP}0$9Ik0|akVlKK0O&SAjJ{O38dRr5A; zz4DeT8g+*};D`{-o)w4cUH)-{W=g-nW;Pd&$)6)Z;8?|*AC-t|I~pdmhwh`+|#MCm`bO6;TksFo=hO^_a1(Dhe(gQ%?EZ z3O?T2U|y7Lv}_g_=O*IwmhmP{x&lyCpyVitQZ~UWhFu21V7p>(D5zm0<*-nUaq$Oy zIm&2^4(Ry*l(hAV`^1RI%^wnPkR&;{LaO;atvKlpfKX-^jh@gm%zFDd#_;1VO#+1U zR9mfNO`yt;$xdNIBaY@O_^~(JRnMaT8i1q{ES^*VEIey*rYOOpP^Vb__TMapg%<$T zXD3Q1j-^VLlf0CaVIzHIm@#`v_+NJ7@2enj`%mlqqnrdxcHzqOqfDQ}H`}!((IFIL zo7>2{aZ({wn-*BasC0b5Uj2ch7QXSbzUdbC1PhqY6Z6P7RRpO7YqJptblV^VPS^ATv;Vaoav$&vMPNRb6nn z3Mgp?uR2hm{@7F>Xc|4y8{|Wrh#S+Hf`XikdD}30t|#7ONlB#~{h>|M3~Gl_z=saHLVl?MIGq`TQfoK>dq$FcxgHVR<4 zfO4tAWsFVRq$opUdiMr$;T5`B&>-EXGW(LNznu zyU*|5m}s1cc=T1td+M7&mp>}`f%$}jW6W@~>pXP>my4{%b6Q5#yb{@x`;6i8m(QiG zQ{*+0;YD23-Q)&M#H88Y>i(ro@Ph&HHl$`XLBd8Uyc+nCVb%Mx`~H>vx#hz?*Jav^ zP|d~pZlM@`V0^LwqE8o@;ZJaM7aQnUPO7^ipn9%9+V(J)^Kh>5&WJ^==ga1fTgfns z=*;#~q_puqNR`kU^TCeuZfLaSbT-R63?LkkPoegnD=h)l_3i0OfkriNp`@#jgQ3dM zcw!m`)RVOXpvYE83Ao4lXQfmZ-QDE61l_$>nz;lP`ZzN>LdJxZy1NFR3cW;08n@v# z1w`{11~B{>`tI%Dt#Cmxl%TPgBB)hQNO!ua;-K>R2a24hgtT<&iwxJQbRHgCVUm*Y zz(}spxa1pp?OKhCqIpT4Mw~ZG|A_v_KEdU(yBi%D?_FiDptVB|p7B9lwq_2OhHxXU zJ3m}y`V}i2ecWSGg<^LdFUq>|-Ui%fgrei5BW*3)Z`T)P?4cIAt05WC3_>U{1p`}j z;g(d}E4fj($18St{d2j=1>HMy%$W3t4f;P~lWr9hM%F#RTi4e+f18xra#46(CyeU& zHT40B#ds7;NGda~(P9;mh{xn##7u)($Vi(iz1v~m&cgf#K3c4cUA)e`Bn{JfZ?ojz zsStBLc+|iCh{AsOex6VVHcww6EO+B6M61UTH((l-*f6#)Zv{~q%kxh->QF2G&EkWy zv#J+X3i*j4)R<`g^50}ob(PtafIa7}clPZz&qSr~`_ytCenRg`#^_rLcKv1nWg3NI zjpb!0OzZ+gays5LzfOr9|zhH1GB6XR}8jfsYrg)S8pBSKwea$7Ke!;Gy> zDxlujU|%i-0M-i;%m~G-W(0T3&L65VUMWb!<4g zt&f5=K2)vgG||Uie!Z)FSbi1FhNuIWZ;P`*PVQP3ImY_H!B{{oS+mRPQcF6%-j6r_ zvP9ZQsI|#pk4?cf1|g0PPY6dTbdgc6Jq~I>UXCn6M9F8Q;re?0l`lE(kAxajDJdrF zhOZ1SdF^T(m}9FS-FNgQkd(fIxaGgB%!J=}V|Cq006$g9O5WW)BWu_=FH-j@#xd)T z0cgA~XUrjl>665O^VFFyftzRPc7oV`x_#`FlJh~aUEWEpVIpMGr~>h0R|2>SUs2qy zP>Un!Z2o5X+#@)g$*jt63NDu-9NnkukkwSxYp{s%sm;aau)4;#TlC8uJ%~&dy>5Sc z!uTt~`0{A@YggNa_5Zv6qf6DX$f$s(b{WV)WhHxh9kru}JV$u`T%$PjLN$EC-S|Ze zM$CWX8a;>Z#M~{f(#`B|j;E4%9GDqQ7}=kIcv^K)zWn1E0JK*X#Ur@{zA1lxIf=pB z`aJN?(ap_QN}E;<8YZKGzghe@!WT?AJ;ozlsue`jI+V-LfBpJ3*yKpeUAA@-RkKtb zG=+1!=7E!@`+5$)5Y6K@0J$q>K9)lo>A)V&ac_iS>IqDDb$p&hc__EBri8nu-%4Vr zyXD8|pLuG_-EBCi6qLDF)?y?N_W}M{e zqO0&_JYf)yZZAm;8c&6Jrx`pF{V+E}H2S5GWxm=j1FimuoQPU&mGg1KA_?;j$&~_j z7`*w+H`nX{zU=ZXE@4V-^PBa3i}5znj`%t7Zg`saP*Hi%h`Vxs+GD|9RZE30#wG1r zs#RhZZWL4Qw!|*jP4IPgNj#@uK50vA6{r5`C!4QoX;%-~(gCBU1RnKzI?3^X`mQi_ zP;*6Bz2{hANBw7V_@Q9xHaN6wh3As}$ZPH+L7yy<*4V+>JL{dRCLhP&`C;D4C>960MnuT+G-x$PpPJ(7$U)VK2zo}x%nG7_ z*G0rV;f^U9RA#@r+C=AdR1 zLD8VguxO1f-KJg6c6j&nNgYN=UWAD8NPmL{yIj?<~CrajG2rWpCX}#>s1f; z>#c@KcfBEH5@FSo$V_K7F;;;Ja`;>Mesk>bZx%(g%qf5Gss8Hm2YE)*NHwj{x5*+3 zAv}?^7cjq)byh`C%6!nK0j+->ivakaV@*Y4hulf94zK7;mfwyrNL zx*-aHa^wegBw`PHd}G$_r9w$2t2*Cv6;>XB)&bAaPecx+W|{!aH3a!6 zTc+361OD^M*rt%<1090knW!pap^T{|u%up?1c(TY8vpw$VcXvAYI@PI5_}%*BEt#p>28%C29TuH|KnASJoZ5?hH8Y*;J9X%7RlL+&PBOhfa? z4vGcMIlZ9Z8dOba?&i1h8{Vt;cNTBq>~7r@GJP3q4D=9~hs?(&>5KW&F2sYO#+ zEk64hw^GuyRV5QrocV6@t5zLaw)91PBk^2QYJJ4l_Cp{1w<6jYsrhPQU4EV`z#}64 z<#;$e!Cdj-K5A}RGd&}b&$uzFo9Us>beoU_o`USCQ)izvCEU66o8^?QiM)8ID{or0 z%7Z%pPhb9Onyi*JhuYy_M>{r&_wB>FHUEA0)5(I?Jh@d*Um<+RNW?hWczIuHjf<0+ zkFcyyQgfmR)K~vx$x*ayKNWuhc4E8dZ2{1+gj~Us^FCl{Lf9EhvShjyVpGec)CNdg zcW@>T;~CWU##P*MR`T-NBSQS{lURM~YV%EJe{cG5 zVK>QO#v{@EuTjn#k3(P8idRr_5u4Ti>1u?XXZ==V1aG` zgaNEk)(x1?ijuC|`#xUZ3X{}x?Qr+qBwhvB_>T&8xcI{Q)JK zl|aDwWcZUOnB2z?7}gKwf0WJQ1K?0*Ub%o9;)L@`-Yw9dmi0vKW>bEZ=JP%I2G`hC zykF!iu0EMz2$L`?|1g#Im84U;(>Xh;bds8N2e>O=W~Fo2qeI}E^+fte%CIf*!m4;7 zCc*VmSGBuk_#6!QqPsD-_24Vc-t~NtPVR~IIfm=wNjEAUg%;=Yy4y#b<`c|+9Dlt9C`qy^ z*k}S;F{gn$=YluvxMF=dw~^j}mi@D&F&S(Ezg?{m>)j$hCTlbOLxreMScL4gy;uts zSj^RakcgV)6p)uPXjZi+>jwF5INO)_{(^)-+Dp~x&8F${Ni}N=JVk%3eX6FGXZWPg z(m*RV`cmex710E7*~~PCzm;+o{GDL2iAh=A1pWMIg73eQH-g)|eQjp?o6vQp*5>ds zndr3|y8U!(<{xv-alZ;Cjp2bS-xTZ#`&gaxxD0;QD0YVqGi_JJeya1gm2S1V0bIR69ZK z`diTA7kBin^KXb4X=Hv4W`;#6s&fNJI1%YldXuL46pI&Xk6g_=K0HkrXJTBQapV%& za?lH)$H6G-)L3j^cGAVAuoClwe~c@KN0Ivo&pLqV(W-f8V}QCcfG4%C;zf7ucfZ(C zOx)Qd3Ct0YtE`9D*}7oTB6cnPyXQM1JXz9GdDfFyBvP(muZH8gwA1@>8moK4U~|Pc zh{7GsQ&(4Jw$UgMTW@k0kGM$%K&7uP6av&AoqxuI2~OaTBu7x&Jv4@Vf?GYLgn-31 zjlR7vREXhLP|`9V2~Lx=Tf-YfC5i}0u@+$ak=L?Se)u|C7mvuT{AP)ekiOG*#iv-> zdRa0;;=IRF1NoVPsfELq%H8+T6Rn3BtK}(2X$dw~h)$hyCCf1>&QVl^6C=J=4^G>| zv;|zv29PGWq@TP!IrvD$#$%~9Q?PQFXq1L&S5kVR>Q^#BIW8xVK!!{*Nlxi6ZCy1aXC|-HO(KC9qPkbSF<%?(8J=o#W>? zVHektj%~7PYpUi7th;a5g{r+j@Q>LQ9-}lE4G{fAs^|tMj~p(?1{DP=)w->&gA=SS zpRlCpMHH)Fz1yvPmp03a$c!o*fFTT(tfo8S0^r3Q- zHJ~NoK(`A!l%12!PlyFa#!i&l{Q$~w zq0+HB(qXAQqBben=z3c+jf*#BJd=^;y<3q^YK>7_uKsuNocXu!OU6g2vVSi&M}uQQ z53#E4z{Zd8{RH}zcYg7d44k=2hv;iwb|TX{rQXr4s58XweG0{B%Ac0_By2iv-x>{7 zyzfDPNgT$460&xdK_vpq!tR%TR_$FPoQihUcox*DvOi^TYYEQDxvZ+wm3cnBHM>=tz^J9;7D9k=!2?&QM zm!`3h2)Oh)FQGg1$l~KC<<=>&LtL<(pW^2mz({(|GK^Xkr@yS zagg^Flf&^E8%?kUNmun@jGPyJ+$|`LZplJD7#^ezB@00G{H!uwT-pun#Z|$>S4TS_ zU6|cloAuWy_TeuleUt@5$st`E{wG(~BwVYJl4de>-j7C49_{cnjfVSpl={E=;#WGY z+#^;~kQ2?Wdmwo-25hd}m@??NN6%QxM-w$Drg=G}df(*3T0dL2SUOp$OzUB7 zbm;PZkS*!sbe~>Ncy|40t5q$)DzUnmf5cLt8YEOLK0jX6y%m*|%%|%=I^eu1du8Qn zRCPRtEi}4SS4uwVE%^I?m7MY0Y`qB;yDLDGVAZ$p$Rzb|xM~;IDZAlR-Sqn~#z~7U z|5e_Z*}+n2FEf0r&k6`VH@=_bZ<$P#l27iDs{yk2^{8Ht@U7JX<*hf}RRunj z_8hd9Kd$Cqnb2*+@=7v`(#yX0GH)|m?xj3#a54=ar(W^x=je1#pWtjY2Qg}5(UbSU zF)@BtE6~B1kZRFU4ojq{P$Ywo6G`D0|iAxPuH93>qZZC z0UZ(?N@z*Q(3a7~N~SUy%ctde&EOPAV)@8#7LnE7pm{v-LRz0PB6p@XK1eT$?O0lG7`7>%Tw#?rdcmi0}nqv}w zzNSBkoJ)YTC_x;xOI6P#vrf%rV$s##aRATW@u|4#a6H36XR5EAs8QziE@~Q?8gH0EDHz8`#vTu)QkZ@Q#&~ z8?I%WbzMG>brsNiXxVM@3n-+5QSXf;Dv95X0i&gVnDp#)3;H;l3xw~~_Hn}D^FlhE zt0>$+$+W+E>E3WmlE788g1~meH7kqqkaN!Xt4>|3cJxoPUz6Z7cwpi^JD*}kft(U{ z4YejP_nW0{;Jh?#sQe?lGO>g6rK{J4Imdc3MtWJ%1tl;OFwcxiIZ?Tv<9fWhM)|;Y z*-|sw0^G3?h4ed;^DYs=$yEKUw*i~g`e$sX#_XxXRG=Z0OPp)~?7?g9+lmRTNv!fW z$}tsQ(yclhK=Vc%93^BoA6?Gp7WjFqY246#MqGp*?4W<_+)Ot)@|}sgD_wr>@G`x~ z{c>%}xn;fkY7due`Z#+R9IG-Ot`BS)G>*V3{LZAX#Ah@}zw;r%{b7Nrnk*ZVEG3m7 zjy#vC^iShH=@w=QK04{!#}~;Ktt6eZcemT&@o(+|GHtCbtIya5#CpOwt}Zp7-Ssom z#Vt$2fp@2p#X&olTs40QKc3bB*&dhJQV*oxvfnvtl11n5WE#10#c$*O`&*XE$bEk$ z%?ha)66NPiws|ubo06&F9iqY1`j9L4=tDDX3cY++nXNo2F0H1DkX>>$xnIpO>6zuV zDZ!+gXVedcHuE^My9x=i*%!-ZW|I9yT_J-8ePlOR*Luzk>Ta~DAgNX z?Bs%JK%JE8Z>W=9Z9K!ixY&{+h{qQX5JC*2dvHa;>_YrO zo_Q%l!b~kaDLS!X?}J-eg*DbTJti%N&zHlVVsxHdWD~$OK?Dis9Q*R3~o%2{Pq|LQQ7h~NX4nV9H zM9#=%Gkt5Tbkopwji)ZYPer;p5?Vrx1jo8^#q~BnQ6OcGDu|-hQztJ^_v+14NDiA2 zb2=f|cVX>zRUF2^ISy-dWybQ!yafpzgdlpFo>B)mRW!1v@_#0BYdM*oPf6)nYjyqB zQf%T&?KO1M)Z;vSDyd`Q9BJZ>6moIeRZwzCbWE5T0th864PM)ZdHftP!+#1yiG8D7 z=*$sM*4a#^#GpXnP`3F}y`eFOW1&pHB_BB3U^$?+MbBw1E?HS1SwO0p0IN&utK}!3 z9L}@?>r>vR3`~hgxhYF2nTDm!`zvO7DWW-w)EY(*UFvGT$`0;Q40_q$J6dr*6O3?C zTBB)LMw{0%eQ8shc-Y3XWC;a%Z@dk_Q-C)@yjs5~ncBaK8dnUM8s6?aLi}w$!=<)8 zLx_0$b5hM=raQizMH(`SO)iKCS`F#)csc{lmOilJmm8a>WbCy_X`}a~qsB+}ubdnk zlM&1w6}&|rl#o9bA6c^-D6`!Ao5hlg53RH_H!dMogPe6j2G)>RX^p3tpKGXkE22nRj+1hu3s=vnBh4x$nCvzQTdj1Pd!K>|+X!}4((~o-yyihEO z?=#U+(%p`kdSz$>s~R~r1F@YoSXzH14Zy9cTAbX3dLPYW-l0?D0Ro7$ccVhTTJ*Z- zxym8QsC!U`JjAkbivdqX^bs6U>8Cut9Rx=4=?N&Uq#2eF%_8 zyrDgFl;Kv%8|g(s4fq$xS5&JG_@?Ked9TPh&RxtxR%fiDC=5Y7y*_iNPHH370sQWPT z>-9z7(7Lh9O|e-4{x8cW&;&qywI0XLf`CcN7nlkRn>H)`)WUVdo?O%jA4d~)EwLiF zOHottzGN}u*(Co77k=z(>ah`VQi&y)6Aory zenQ6ac1B|rgSlYP3>wFGZ;Ov_I&3&VBfms8PyVrEwrD2x?@oOpVLVc#*$P`ncP`Xz zZ=^gQ^u}C{%}{mCDX36;Fv(dkpahHNL>DDR&Tp7G7uI4tw~jQiBrydlMxz!qiP&^_J*!+N5y7YJQ- zOH(@|&@dw|HT6lK&2sP!e?oRb$*y`u2}b81lq>u3OYn;7z3O*;=F6;5|KL)yMSEPE zf4e4Z{0?tY;!mT2`@`R!tXa96nocQ$1oQlDFQxa6pKLPnDOlrJ#mW?I`)&OhjNSc2 zDC}qg^hCr**gX`ZWjPgB-D_&h=G`=4K{09u=RoOJOjK^D+m53q#I!@frnB1<+B72BWXhM{{ zOYLx2A^hmnMbup--(BmEldz%%I$A+L&Y!sA3FiLIlEyYAV$?1ULdtb zjw{-fkrRs3^UIWR#5v?w>h*zql1}Uu<{laeopWj%%z4r_bKj|Yw*);LoN8xokd`B3 zm|+F|D-T^rW=T=B;KdX;$Hl1)YHUdiDfNDJ*6b>4|c8is#KTt zEmneTX$3T=k1BD}vU$p~_2qKem;NEfR8h98`qaZ*vfTm43K7vuRP8~?#D!5hC`lb| zyno$eU(zQPLzq{fe;>5vF`hUO>$DhK$ph|dJ_rx1>TdG614z1!zgdj0NKrVk@u{qb zwYmp9*5;(^tn!~QT33Q{R=j2*`((I0>JBxNLkA0$PWZ^c)+1v{99j0g@0DLf znN?KDp~j=^ermX_fk9|3M-r@fwb%|@ae(u)gn9GJiYLTD^R7Xu~H2& zA~p!s%~QbSXSyMtTQ^ZV`j5z|W5xBrI?@l}0q97JrElh4O9Bpd)kJWyHf7qFswf z`yYCU?@ji4M|F0kc088?nX^Esg@)U3nYAYz*;-sJeXUfFT}c?bzsdxpec7-A?L{$g zWyUA3@aE@ZJ8R|7oKL01<$Wj(0L?3EU1B)L(Uq28@gYdn>8a;Sb!Waw|2=zzbQiXr z*co|X-(%w_Uyl&`M0Oy#HIy$ALJbCey3+3wdUT?y%8y*=SC{U%$~Tzbc~a}hiAIG1 zeh$^>>#uLpSwCwP=G?k^b1(UmzxNQ0ui)`_e*>Gt#S;mIFSIqh1Kft5?zC^-JaOWD zaYWB*<$8JeekjakY?~|v9epO(3jy)?Q+GLlmRS2(;2A1_P;A?$HhENqKT*R1JXJCg zS%pz_^eJG9NU>nz%-=$^2JhbKs%nGrNpf(F&@mH2bV9f5lTKG@tdM$Eg);Dpn@l>x z6+*pn*~H|r`}MWJ6uS|f6RoF8ySE)tk7i=8nH39lplpzJNlm(Eue>`eF(Y76jG`Hx zIyYtegOgVi_dfkv?fc%v1e+4})Cv;)?TDnp!8yZq;(HKz(5JQt7DD{b5I<_Yp0Rkfc|3UB_3O_wl}Z<;Vzk3G`aWg zyS{xCeg~wzfX5FM#2A_h0Y`Wq%3lT^RlUDd~*F_PRsyMKhtdGh&ZCQ0&Q~*GX;niV+k#tR)JkHa8Wb05ueHF zzuH3OP*J+_ibd}kD^c+AukL>hY#P4?a<@g!%SuGV-d*pNs^LA)cnH0@q;!69)@qOS z!T0aB{BB*0ZyTX}t#~y;m+MTe+U;4ka$YrAhXQX&8*hk{0$IIS7+WkeTEWlE&!PDq zlH5{(d|ut+w?5!qJDjo9a3UKEmuWT6kX{QPTJgkusp$a8|LR{Q!l$lO%>~8^xoXL+ z-z0oUpFm@}c@!PRuTc5*ELT`tq~iJKpxrKfN22gV0V|26l7vQXan(R(6#Vh0Y;FvT zZQ+hWJeb!qWnGbZw5T#!LXLbH2*@DhMe>wK&ykb;T(3_FnJ!x0^zmA1j%LMW_?aTV ztk4Cj5*+9i)&P#O^svrvb^Btok!QNki2o7**_0%$$id1)C0$cxFSR3?7@Az!+sX+O z-x|6y5R+XJE1kR4THldcJ3$Z%@^VFkGk?~>rk~css*wis zGc+LSVlRg{xc;E-XEXbZ<9DL={o#S1oEPS3{^Lax_A+h&i?}8Il==_V-F)_g(k|nF&yoL+U@@#2(DT?;SF8UcEw_zdbRx z7cg$jC7|s=ASdu<3oKlex)O8zJ4nK-0xb=S0+_V~2hI9XtyZJAUJuNg-58k(hR(=ti4Q`a^yi<3q=&d-g(6YgR-9S8 zfS#v*hDFjl6rbwoX&=J+E=Vf+BTg_Fuj-p6n7w5GzVqNwSGtzp78=?<85ABcZFSRo ztLFPpo>M=4&d8OEh)W5JlPwBh4<-)6{%Q_8#eV;pOQWK{>uxOfT~C#Xgi*GE9M18e zj-WCvGhHDL$EGSNQKHcTUDvXEL&3`2OF9Q}K4fLNIt_7p<5M#jJordntQODiShEL< zPlZ^oXD%!^R}*02M)huVXI)cZ(BlC5UrYODy~{7N3u*R2ZuLAFQ#Pobw^A1jUOTDp zV<(K3I0j<<=ksaS8#}2b@Z`$PeIRB(d*w`vgRgQ@xL+at!qVEsFP_-@fkx@8e>g{;6}y+vc!iQPITxr?9Vfr7RFB~5c0nIf=FWY!3k6g0df0Ep%X4mFcWJG#wthzg_qqW)4+e^NMd9OfT76>Tf#?q5Wn#SI{z% z46U0vC|3pn&-L)rVO|H+0sL>4TB4D61FR@bXWE8pIqn;5e0GK+9o4emjQs1raV0q{g^hZu!ATDDmsgNWE7?q2b(^BKwkFcg{xFtFB9L2$L0;)2CdtH_ z=)Km)U!U4D-Nt#n$L4tqUBY65zKKA?jkL`&(k$uH3z{! z&3@qO_9_0)SI|;g&Oeq6eG0ZN{$|18Twb;D9r^tFsfkp;Tc(qg%_cuY@)17)8U||6 z-QyNJnWoV4<>aF?U(EBc7pksVsU0JXJ8`#`rvxWqu7CvnO?UkL+uDU+>G4!PFjWgG z^Be1{I`rL5VK&?S zQki3r5@$J2Dv83x%se#4NXXvj`bh1j<`B!}2h(lmJqHu5zhc!McU*Tf$*?`_1~AQv zfdZ>vwX=>i$@C2S=@WIa`n#-al|?_&#~za#0hP}6Y=Sv>-wAr6f!v#UNzQ9lleX*? zZN)>qdz51Qk{rw-&}#&UJms6-18}ms6ieqT2lCCJ)NW_+$5HD4SitulV&3?aA;1{QYRA$A zc6C5s;QJsf-Wh>c6f7!~__!LCvR?2qFpJuvQA{LyL4&30Sb8nUhod%?;}(83M8&WW zh)xpIy(nsbKGLkEL8MJ+7%i(S3G>=@gK$hzYW|4|AXxi+t(=WRSda*WF@q zr379D&fUQpH7?zWOa(WGJu*^bS@RD^K&a(nAnubj^WD`I;9T024pwod#A8dYq8NxBjSB^6 z%Ij6o&ukOkkJqjQF*7+wpn@oW@Ghz%Ge+3hgmQX6@y|5Ihr;-hjUV&LcCUve8gRgz z+;A@Hp$w9j{qC$r%JLN~4RCpAgvuG(F9k z%cw;%8h^pZrb@%~AQ!K;*emtK-@4)T|6)+B@QUYu(`}dB5dO*vx8#pj${}0|sB(ad zf_KInQ%rTHSzQIx4y^!fhj4D!Gh#O*gv$gh`A?8o11(IeF(@nW{uc%-D$}HR0s|jR zgWLEJC1kgt?GE)xZF-BO4yh40OY_zr=nPVAX;KK}j7aRPl~FbaZ?0S_qSyOYLzHfux>@!R!)Z-er8AqX{wd zq8k95(O)OgDD|nOE|{sm@xP9-hh)6Y@CJKpcT{Q`N#YNWed9g7;+nN`+d`y_;KCak zxe+g%M$Gq1wI>PWf zoh}J!SR3#w*SxaH2mIPWj?_*rCt>x7XEn4Xqz63?+?W};P!S?KQ{_RH_pz#7Wa;In zf5?;}(kNFdBvmU=bIMmbK*q?#UV0v+^&fPp-wHex=VpvuT!jh!BT2}=yVyULicWxc zgJo+IKRVu$_oJjmJxRdav~~(I=iTnL`X77=>hdL0K8Dhz7TMYQ zICV9HBe$@&rQ)r=;uCE?9r_&YshuyIeiEO$HGU%KO{4PkE>AL$0zOI9&TddHYy!~4 zx@xNZ=hFWE9-ZyK=_@9$8V0&kap_C%U9X?<`2S#Ij`FQHJxQwXG;AsOfT8yE&geHC z_K(PiyIvWi%jk^HwoxIA?l3prPge~MUwA&1i?H{f4Y!lUE%~H=iRo3ME5Vcol=|gW zubRPi{s#w(kt*#rI)REZ^>dqu_8CWHK}#h2uWGZrG~LZ8Ab=f1+>??6>ZhK8Q&ytX z;igt`HT+adNVfcWif-oJ9K-Xx`z-24F&Z zv0YPxVhkjaIr{!bA;Oo_;sXO3HE)fa;C!qa@%hFrC%9$~GbLI)@U_A10e|$Gq}w!k zIQ&-0wt#M>-cp$#_Ha#>;L>hmowH(EAwv{z7^>ov7QdUX|w z|ALbipfmBOkI9rjKqh%%_0YbC3o#<@oNo+lM zUJ1y+GZ4VO5lKycuM@dO)J{vdamt{X*_IFJ$YGI~qBq&=ah{GW#ts=|d9*ayxMV3& z<$)g{sTy0xr3c4<=;RME2NAyJTiZ1R>l0EkEzH=gfq|InB)5`&qA{gfW%7pn&LE6;AAWXdt%)mP8bKc;V>HuG*TfXiEP@=KP{(n);W^d%nlokv_s^JU_Q$=y|+ zW@t30Bqs@)4t&&i8{Kz%|3*o`&oF^%Jh|z8`ZC!)y5k*^0+;pptA=RJ1*94 z|GHqCwsF9UQJHtagu3V5piDt)OMzyW{qeo6qu~yBEB0@%F{0SLTd*w?H?;~?K@Mu*T(wSw=Krr@VUJw&9c zK^4h46XfQo6IG!yaT_m$HwZGjhz5rz_3NaUq zX$mT*UL$}N(*t>q=DtV1FC>?q$>zZ*3(l6#YgzwA=kLy3Uw=N*_w~kDuB5>>x*Q~g z=t(q4YSg?oypHnbn%(^qgocpbM7Kzo>lVOhn_D-SKzfnsBu|!9`S;fI7-HJGw+Kp>gQj9KxO->24dq?!Lt7VNB^iMLL!>oDp0~+_heBK?m0q3ThXvJ{@z< z=2?&r$v!zUxH5Z+!Z24>mvyXPH*5@UCaL>yfIy`ufEqIv0!g*(!*=-p~IyN|EpQzaCCR@azZA ze`LSI*ZaCxrEv6PG00#~4Gfmah>XpNYqDj1$>ORzZ60LwRVb9S)I`H^4POZoTvm2p z71=!H#-nyH@3s^6d*b2(uPJaS>Ez^Y_3Zh@`T4cR<*ea{-5+m}ZysgU%S`@@%MBYF zUjgtJaFf>ba`H%y^@WEZk72e6|0!%T8j>Nfci1HdCoPS`rRE|3RZ%hFpxnqBE!$Q< zDxxKtTd@SD_cBYIPE>3UUX-6-C1igkA74$vHr8gZp%*H`oTzo`(+iBph>F54D)XE? zw&^R^X*yE$F>ZIbRYs|#P||RG@5C8LT&Dw1Zl}Zt+ zKAQ@3f0}F~y@%N??DO(Z?3ayVyDD!H-n19@WgxKFed}ghf{k@&=%CR17l2@TRHvIE zr>Yz9dDJ`mUFG@AiEOl~_F)4~u$jyRkIrd)AefEW$M^^GR%D&WEx@iDu;L-aWo;LPzc}g1geXNq3#PxSN$i(%Yr&--8ml@Z;_-~i?*vsPg ze@ow$TJIQ6Iy%Q^Omu=jzywD@fo$@+>+I^f>Ayx%3p;mbJ3iM6Ax0|HSRb(}@FDSo zf9`r`vJZ1O?tb0!!IsS$7R#RSH;g4Hq0qZjyN+81*QkGcrs_94M1X>5zbWASt2YOq z%rbAF1Msg!xhEbBTw7tPi8gLs_(spUN(F}H6j(3(NBCgF^aJ}(_!%Nd`?o~26m$j# zbmXd&ue?{~#tC4dk`J2-G2BsgCOz2!?AHMrY~Q|lu9u6dd9Kw@T8-XJgxqG_9sZy& zRR1mgqSUnXJ2)i$&^7qiwVJ?-;1_w1%-xn}RUM4^kp1oemgFCVr_S39-RxwtcE9ZV z%M-1S1H{e}zNFyiiv%}rEQ~zzlVuJV{x%w^%h>bre~SK(1pdDyfskQy7y8#(a^ezv zn1zbanAAVFcNvH#qpuOQs>Uc33IY*%@ibZ9?WxKAl?B7n)%M=tqM@-%NizEV{jD{B z@XZ^+Z#?kXD(lQAmH2T-_H(SF?`^9VGo|6ijs{gpH~}OCv_`9RH6uY>s}+qVu}^EJ z4nF^RYB`bY5dOARF7C?eU|m247d~dx(D1sw_1RjxP>gw+*x+t6$*Y638uAFzY`sXt(h;s6I&>SuX_7IH@m*ch=QMN(LJX9I8gN|*Wh;(RnUCD*`{e0S*DUg0^m<;X0Om z2!}`I6iK1qPcvU3GjFaR99J^8n0KMM(@gJz-U<0XIO6~D4a`i}1tdvV{B^b=;LSUm zf@Oh{4+$Xt9QR{WBH7H0Y7)R~xSvKgl*GLCN*h0n)vi=IaENEF?$we}RF>fZk#m#m z>w2)i{dm=Rx!=EiWN&DY5!nR|8_Q30qxvJzUkNU?vzYKz4}+#m$Jzc6L7{7lGbdEp zvnD~@{BUCkl1FiBj21h@eo;=mCnJ1>o1Hpl^ksL{%5_ueQf@8?HE*X{g7YzI#$9h( zCtCv?hTQJSU2$XkMTl*{LuBq7_g=UFvpIqovn%t5ATLcDx8F;KHIM-)^gkHkRV*^P zfY)nHu@ztE-6oI;Ut11?r!r})Nr{{|)$e#Y=0b!EG&OU%qvFg~goywXr$p^Alkzj zdaSe~IpaKtW2#5J@Sh(sSj}&zJiMoW`U^Pkx3X?Eh&Gy&ZeYyH8=4r$S;=_8<4%B0GczDG z`*P!+Q`3I(-RaBUjqhh72cb_-NEs%@r8k_6!JiYc{{m=Pd!3ncY7hG&$Aa;NMGkI+gU#&pF{72g^f#SCg5LQ4?_cO7l98pH8N0$YyR|k&iVN81VhIY> zZz88Hv}@W`LU|v}mo1-$VhnGvW0uOtsb2p%I;p%#{IL9J{PWD+3(po$GhohvY$yB& z*@U7~LHb#yuE=Kv%X@sgBn3q+vbQ$Wc(WKiWuVSb4L9Pnv$IK6=_iPgtxHML&13Yw zOe_2)SUaV&%|j_rz>{-;{~L5`+>ykv2COEc3NiS#ByV;Z*o%9s@}~}_z9G6?ZAz5r z>@LyqC;R`VJ(eS8(~=fX_<_mTQjY~w1VTr>SwwUED)sA;qP&TR9WE>}M{jn@Ha9P8 zJ}lKwe4c*k`%~-}4hM>X%9iq?Inlb#Dq>5r2_+6Xkf4$SVk43MqCQ#u%Z(36NQ69(G^2G!Yxq! zXCcAFw^!fSbq-MShUe2BydYyi&b9L^VTyRyM@FiTwAIguLI?|_h;i+8y_Ibi6 znCek~xnnmqQ)tbRx>q0x-SHEogxnKTlAyNKz~D4cjekiwd^*)ADz+gqKstHP_3*>4 z$3LB94(|N$UYlbCPxD98)XGy20?rCK)FI@h!5fg8|DnK(oR_9g3Q<&7vLB)t7{#i{ zDdrg->HHL4Bw>8SxR9@v*N9oUuDtXWo+U!W*Am54BiBy)5CZw(RsCW#FBMlm`MIt1f{+BgYnVkyl9 zu4QOAB=)wg>D0I5fD0>@B?Tt51x_0pP!(amI9#2&Zit~*_R%GEBe;Y2h)uc6yq$0V zF{Sn#MKdSimD*jMkV@c08V=W6C00(^l*G7+9aJJ^2HQMvG*ja{C-N>$9A!SnLAG*x)N~IGcVcJmR6UzL zk0>i)W3h*aW@akP;wrIsY|njhD{FT~jk3K0fJ=As|5yad>(56UC22$RJcb;q@kF8^ z5&}Ef>c0Tu_AjHEFE@>EJI?EWgV+7ZOpuF5bfoA_UleEPxJhGY-p$Rr)2EL8UDwCU zExGpzm3EANYwPB)t3DlCylz}{bnfAyV}~BX$6kr10~}ztB3zy7Ym3)pomz(sC*W@n z>RzVhq<Ey{A1AgwT z4n4yvz&Z5(cf}kwS5r~9(3zM3E>wWj_j?c){0W*OSER4POf?cT|93N)85@Y7yHHju_q^3mvP@u^lx(D4+<0nP+ zI)%hw0BxlNwqjalUUZ-84@lOQ``e~X!H?kZ64YEM3vuJZ+=A3i;+w^L!=85&zm8mA zRx-vk-(Pz0ZvtAB)Ty~H9(Qu$7a=QD8;D?2>_)y~T)x=MEd<@&d)@%R8a=0Ccit|@`hT!I0g(uGF5j85C ztn+FHYT%?S?))j0*GT-nXnZU2rzx84;H~-()}%IA?0_eGWfNbD)d5Dt3=h}3nP>hX zgc^T*=GjiGJY+O&@oJJ?%WzvTP)Iki@t>~-=B5lPRZSO6kLmoD^f~?{=bo%N0<}W;WTrU0J5^{8 zYA0}GzVm8Iq&v&Byg1QK{glyL7&>3K3$?qfS=fdHf1G>BX=m*(qSFfFjLk5c5|oM^ zOrJ^!E!#o>Gab=1BK)Y(&SaEbWHxP~XtxuwGc1~Rj|rWiCMc@c+_I|KrTqMJY18|Q!+UZHlq3IX z>)OSCS|Zom`o{9EYYB<>6^QRoOB52=_Z&gEnXH??YUiJBaxFUi+junldb*1Sr0g*X z7}avYwHBBAl}lYbjwkkOHljgsoA}ypv5>ZdIL347Kj8A|i0>IS02m&X~ zZT6&5MbdL2NgE$iMc^lgK(?23hI<3M2fHpjT&~g(a1sKIg{+^!34$!Y3Yw=7@ItMX!LbFNdAv{KKcwvaN?v&&Yv*y^MbCjas=@|%xkfn}8# z;Nh+)C~xZDb~P>zA6Kj~S+^WR3Nh@vWCcYNWANJ@$$UB+d5VhhJ7@@sX}zNMr~92_ zNU2U5bep`|2KVOe>=OK%;5q+25xLW!QvRNyfgMOOVP=WS?1dj`9kN!W(nUI1QN?x| z30`8(FBU#dKQe7J!)_=Lw>#j9g+i^Zks((~h`J>3} z^{nB#jHHyp2mg)u{Hy<3SJ@j~s z?lBUTUl>_q) zBz^?H%>jd-2(Zp z;mTDMz^JWDB@<3xZ>14ToNg<$m(RV1$~p^IhrA>)-GBa`_j6C=Hl8%@mi;C2xrK5; zSr^{%s6jnAIQ1eELtLR=eL_sYUZJJ)rHbH|dV$iA_bYsJP5vsqcf{($YSI!1@zRJ>B( zWi~OTq?zI0gZ>^Yvi8%y=Xf-DCHca;uQrc{SH*YHjrpCCQ2O{po2yAf`S-E6x8WX5 zZLqx`mt^YF+I=qi-bovPA_~pbA#0kFTa?6FhLBs|Z|!HtxjyjQs1|Y>iFv~A zCHuO=NDE2={*;m#gXYzSm#BEbsjQ81!?rNR@(+-i@g`wbq_I~HKAJp=Ql+0hf^3Kl zc++2LBwaOn#|a?4d*69=k{KSwP}hTCS}{VJ1PI@w8XI!rUQHqLDi@WBo6>4ZES0E% zNp=hz2Y424B<35wzF}71?@?SN<~TxTkxrajTmy`!(Xt#Vgf@oLvY24u^=R8zr%0Uw z)PqVl+My|(Cb~EEQ-f~PQMa?Ee#V%uFB1xjkn^_BKY20xBvnJddcG*7J^Y^1+2oo% zQBba}5>b)L`sAm4G5!0xmf6pW@Hbpgu}dH(Hmt; z`_hRK>4_<;2-zE@!y}-NItdfs{rq(&9#?cRTlA2!wN?`*cUY|R znQll{x%W#_#TksxLw-)CG+iaWpd)teip(O_p)Ugf|8BAl@?+04e3tZyA^$h0= z$@e9bj&)G_%1kI3A;>w$$nFc7339IfQdY*SIwdGE7B#)gDkmM`GN^Gv^$h>Lo5T&u z7_%8}-i-S~WHUxEz;=?I(R(zSweeps1XcHufo~Qat;~n1B6+;Rt9Z@gE*XZ;LZ1HX z@lAeUB?YmR%W}CuVA8HY@JZWA&*0k*9sri+0Z^RY>kIjmpuL5bANId%cJ43>-K^Am zWkdtQ0PuFwy(m{6sqbiyG}2R4>pfvySHs17d@bk7}AH8R5 zqk&3XumYpmmpyRy)d`#Lz;~Nj1bX4nv{(8z0004ip5lE&OIjC2ab@-AwielVrh!(;nKlL5f&qH}VhNM#tmwArNFkqyxQpH5@#m0l^HtjH`CQ9(bVzb0!EtJcM=Ev3tNiYq$-QPe z@B+L_xx1N+V0wS7a{apk!$jUswas z2thK_bIIyDf2hb#@K@{;V%o_>q6Cj=7@RW7)v}f-z@T+GUF(e|flf{DyhHCkv_V4Yi zaH52yY(+sh(P{%eQ|6q}Ce8|GX7sRy{4~Y%f2H@wsh+FgPMXyle5WXgPp8Jop?0&u+O#oNHn|1&7%$wN{wP~>%~CS^-gB6xSldx2TfcB0#E~70Hy@{hRTF%?fz^) z25}7^=40jGq`qtfA9FlF^o3T?E^0;=Le2pS+=u$(j0_2TM_o}B3!pqo?I!ukv$hKw zPAy*<+WqJ!y!`?9YrQ8pQ8`Pc_Ba8+^90gx!JxGfMpH*Rwk%{0W2v;y; z#a@(s3UPj|QZ^2!9v-9y^_f1Ypm)V*5!zqVZ+`p&W8(ZhN-5qv4w$EmNA@o5a3mMJ zKll<)*|A?_u!u%H%jV(`Frk+E(p9O2g{-@?C-+(pBC1=~_aoK8c40K)L)x$I51()z zHJ&T?P)Ix~E%q-$|71+ch9RN8pOaKl1@m~uK6knwP zvDzP944gd~1c|n2*5U(eYL4JFJS{`vW=tLT2tX6%lk4m>;#N`MH}EtM>LAaLi!J^1BQgc`pHzV#g3&h{PhVknU~$JNoc zM0*o#xPyGyyat)`Jr%{!qdPM`PWTG%aoX(5bgoy|{#@K_i`F;Fh1BFyH`%+#PXvoR zaYjbeLeYDals(MJ5sF!oe0gOnBNif-n^5;GB+o|GM*0x$X620(N*whfovTx3*Ljls zR8S{xpUG3BDLU~xxUSTHS`xz*US7dyIbs!A9A@;YAhL}AxSTdy(2K#3EY7HVTX@6t z;eP((LqB$1HsL-yn`4cz`8bk!+F7SLz#&fgXVeXx8uw^jukWxLgo&T2HbxZAhk0pUBu&(PW!O#1~+K! zI#UdGzrV`asA0o-z(F()4S}7?cW~MuV!^)V@YOvPJ3ncw8lxpXS@^LUsM9*Z$8r;= zXb3tzItwJzOKn-I6Z9R6Pz0oNK8z23;#|eG1gcQ%v+2|vnnF5gpAV?!BxS4y>T3yR z&YI|PRodaHTBl1Hih(}j?uu|Y=uIB~j#w&+#}+AtZ!zzI0RdsJR$25mK;*G(oIAS2 zm2LiOZu35aFS%B(mUJ>J;b)ftiZaWSX1(|(^Q?shZaI85CLs%fEgg(Gg7^#~O8+TO zC(i%J0=4M$tStZ8>hJv*V3ql`9B{ISw5{=G=?6yAy6$8$E_#h941&ge-4jkRyx^bW@!IrGu=l{AUFCWfIiJ;_r-$=<{ceaf1LZ7wzR>f zje85>gIS4CT>C;716eC1PzPfsceLRMA&c_>ewUPuQHINgFqx$Nc-X+7{382jq5!@ zPaWtvRB87ol_7rvX=>tPe;a9IzzRmDl;T;#et#$*Zj0$L8!f^Xc%Cyu#nE94HeB%9 zc(EjD&x|T8jL9evp|3Q;KuOJ2{4fNO>TV7Ko>ynojMi<}?=9chdpae;dNPu9gzs}D;y4g@hFKTS;)&QgTWkIV-&+*V2FBlN>PQ__3wvGll&3J5 zf{-ABd}3y2HA#xFCb)1zU}4*};wq@?_)++*em;r3g&gE>lDoN$^ z<$e?OP7C$|oZ=9W$|tEyqkdzT1Omn&sfVM z0&p1asYs=1`NC!BQ^*JUNU!=H6>M85q^iN}w32{jJn&(Th&~>vKA9 z@dJ}bn(=AXum~ko#v#r_O3wW0iG`Y0upP=*v4_r4l(U#6V`9*)Sn}?}C&Q?liQHSv zzm|(XO>bl$&JmMY4vKx!FtQ6*D-`Dq$?>3`Lb!%0sc{8nP}fNI(;QpcwS{&?{}zS; zS8=4A#ab(#2&v4R5~8{kNYWYg7PF{qNQ|>Mxhv=!oKiES=O1hPn;SC63Jxa0E7y1c zbvsBG*rKiW_}fiAgco(!0MfSNLxmM1rBcZbr6;a!JVpq;+tit8izIARs%XYvsUU5Z zoI3h4b7$q*R!(sr7P!rS_x%$*J`jlb;L9&(I>YqS77{2%q>UTK`qfM_&7DWUu%fu8 ztmb0XxSB6F{5`XQ`?gG9;)rq~y5P_x08ZwJ&&?Z!E>AP$GS%0-{Snhnls*oGI)MOR zrN`&BOV%lLAJfhQx5xw^`^@-hMdRU3VbhNrs(}HEMqZO)Xru8?w_!;f5U8hy`?7`+{_lU7sFg1{-}It1uxtot@KuR_xIJz2Lq~ml;@4+2wenY* z9ovug_bl(&xE8V^)W9(&ssNlXFy~jr8n$pOMP>jsPOOLiJyi0IqcP;AvDB2_N4NQM z*n$qJjZDM(fZ`T5jX;Ky&TAq(Y(;;~x9QFjId)A(`pbsKLGA)Z89IiG^XVj|?i;Eh zubzlxBLv>IiZFTE8OtL1cQVG7X$?YuZ=MV|oNG|OCIU#!H2I`EOMcs3B8@8uV^goK z=KH|0)mFfG{`&CbsmVq=KkI5 z!54_94FQFzWe%dG!|luYG!IE(<*}-?CB-!rRrlx$QKE^UEN^{33=Hj7=Z**U0KwxT zZ-+M$lv*t4eloWlEQ-^_03Ig#pGG_$`&^&f4jJAnn;7}^x=y{WHX zNI?GMZ^RtYa1)B)+Sh+`ei#^DAmw{`AefQ&d?$e;MQ7 zKoyGtc+wxGkgf^+ant*hpCUpZT_K#w7=B=?*tj@eT88{VC*3IBB99W!*08&|!jH~gCaMRoXB$~X6*wS5~ zI%Cvh`eU_YH}O22kG0uZuXnta7hFZY>-#i&k{dYSmEr61w5@#${_5`7@(}w(^p5P@ zY;jb3xY)Re{QlgmZfM}Cy63!@9U{&K6H6Ac4bLf@f5M?JCjAp_U9PkSVh%jcu`Abu zv3vPax^XE{e?}!YKjhA?m(Yg3NPDK^`b~pA)h=tU+$9p6aFY6KFa~;Cz#%Y)HP?sW z+#*EF+R0Mq^JVM3jgZAYCp;w?Nmn7` zapfWn^GJg)C6fFqv200SB#cj5#L#<#oz>F2X`AHrINX?&M`eV2nbDfCV>ZfEg)Jeo zq7}S^7>$acf=9O`h|pszU&AnFV)yI9S;kGp%Wi#8V0 ztFE+6ILBDoaNj9HiAYYtd9o-ck-VF(b$6YAqOwK|Z`a!@>HgII(M{)y%Czx;EYl>3xG%KqaiMGmXgx+U3x*@9OZ8ZjfczXfXPhBmu16Si+Uo9thcnfB+o>Zx{I3SwQnCBx zM7enH6A0uZRx}O|in=N=piDrR8_rXJNXDOECsk^?N04_;k`II|?V#~S(G3GC&mx&l zFnJio8hEgyqB&}fj&?H1(FZBVHDh7W4XKn?IfHvf&9i(ob^cQPheLj*#^F!RbM)km zrs^{;T3%ymZ#>PpI6zi6!snqqho0DQwMd*!nMFTq^r;3az7y(;gLJ7>RYaOl0-LpZ z26*{bVPUvBMxsm6G_P&oET?=Nx=ey2*RMh^ZwxfCS%@08?E2Ek=9~PMruMu$eIAJkyyToL+=Z+x9lqf0Q+xdC!<^6le3OuMb~V6`psHN2MiT zTmYNqCY%_;{-_cuczX~>BaoDqQTCkaCLBORA`+JzRvpNjYOMi4PX%e3ImBBgnI6LM z2y|$9f)od3et~z{`;C|=aQF>pnUa#)DByK{c9KFW=$3sGg5IuS32ay>M-^XrDCZz) z>x}pDfWbH`+TYf9M_<3m`tlq0~ZuzJ0Z4yGi~xK!SXp+q_6a(v^r zS9p@GWNZB+)INYY^hFA!hgi*pJerG4e7!HiQGdYa-bY;5{>d=0_se7o-g`T_#2jjx z9>_otX*3$z!lr6Q>hqPyc)_-%u8aoPbBetA@eglB+GW+5CVD$X5~Kz*FNCvdM~8a~ z^gdNx6SizKmi&4m#O|4)Ly%H-QR|c3N9M0-WRfJ1)^WHn*u=abP`A#l%#{18tI8m& zb2CH!DN=v(^@=yMD-_+rv{lycK7f$0NG$PqwQ{T6WEfPXVFJN(6wusXMFMzMn5^)t z5tZY$!>|liQDp)bzFi4ALt|7$>^$(o+R83X&?dq$ApAHh^j#E-f}(#3!n}QMVVQ`H z=Zf?Fi#E34cvr#d59y1`g8SwD`0G9v!9K^N=|0A4jm+9xZvZ{*J7ScEyqN}1Bfdt4ySd}MXSHn@S@ilUP@*rViwd{;qp#Y3Le8CK$qaUKC#y8iwg?*ox zHDgEp;U&$_xqM;yo4IO|T_lugmMp#F8+;5oG+&T)CC%spT;(jK$vN8Z=^&G3wD^Rx zEWS0s?G}#CtWBFOlXmwgBJ9jaP-4DPLW5M!S4LIxreRW9HA<<^Hn-we%yqPbCS&fI zSpaBWZ0rIU5sRa9o3Y}`7f%ZwvnyOXDrh(Qo)-#xB43Nk^9nGIEQnW;9B+=6)?Zk~DvXAWbH2ki2)I$a0dwlHKGj3<3IWSAx+ykO29GZWWz6DoM3_x~008u3M0u;9OwQOmE$cdh z-aRp$KfRZpn#2~F=Mwj0k6#?+Tgi34VK}=7BdIpm764tfOjet zZ!eZzfQXiAo*4zb-X6+l_F?8+?bg{}L3 z0U%0nB;*&9^tCvi^pq5;{jTQ?!KC;48*+W{eGDc>H`wQ9E*lK>u0GFCsEwDAd0@6! z$7(PrcMY4&7(|G+3`5-mWVbcA?ql2v`rWde4{!=I;8~{}spnHiJoLT3pCFT*SXc(Z zhdjDicH)s(a19aw0~W2{U%-~IueN4aZ{735k7AAX)WiJ#Wfp&mvgYqYUzdu?*z0Le zyT!gHA!H3Qk{0q$6f_iQB5~$J>#K8birVZ)xD&{(-10_<8_>efEkuQ{m0>&h+=kzR zH2OU4Aae-RRbDAN-?rs9R4RREOm7rTVBWa3GEr#71frQn@*!qSL+mN%qk(sa_0I_cfZNF`{W23(8T zJXVzSvC2pUr>|&@psHlieV--C>I~mkv4DFWzL={SH=rwnF z19bFQ9+$dcap;rglBwB5vkWmgWoSPyd04DLGr5fQTjVMpuvE+Mo5HE`ofv@SShxIE z$JoQbqrIXMYEMu4-0)aqsGUmK5z%Hrl3Mb`7K0459U6s|A%NHk+L7LEOTX1EJ_yvY z*ONAOy*;yco+PcK$vy63e&tJ^cz@)I5T~?R95W7idl$!MmrRuD?A5p#ZlYk;O#Kt+ z>k{B#7Q11)8V*e*t^xpv0C-`5huGwH6=KyaUqB=-Lxdb1ZtmAApQNFOv|+M6whsmg zPNCXr)Gmq09hU|G{wcPfj)RT@6?#~wr8J3b!;%htgq`C_C{4&gs>x@>! zsFvS85QY<=2OyE-St#;`+{x`Ph?ZvMi7%S6W41k#uk?v8RrYsx^)IO@lmb@g#i|;< zgv_HpC@7C#m7Io za`m`2=D8|vq2BRGU5*}Mb%C`qu~D`va&HmwCKH z=T*~!f9}_xQR<*~j!C@WGrXdkcz`c&0` zjc5whbPe?Z7lxZT6rE&MAO%eplZV_|5FLKYR!;?1pN=PFc%ClWTcfc4`cd^rp{8tL1-Q$Wrg81Rp(SUlRGC-+jrJ z^V{w0GKv3I^Md=Emu=Rs>gnw_`f1{XSUCt2K>R8> z6^z<}hjRYn8msuN`b{0Bf0gh#g|qmKp%XjWivG3GL;)WwK!fb_WTbdmQ0rQsaK7;i=R1r65Y_+-8X`8MXq(m!i;BP1T42hEs@`AtQ8i>k?1sJ zwA``(<4qpeiwncD5lu8cYEz<|U6=q%rqjKwXwhD~gVE0HJ9hcDD(U&U=2C|u5H%tg zc8DZI%Zy$s8A9$(#F4VZM6&Ofs+gM7bcsZTMR6^xi{DZ1Q>5v#e{+KPT=2_(M}uQJ?tkJq2SNkUvX^N0xp&>Zpt zwy==c?NGLSX>1B3VHw9($!FQ8t-p9_EQ`F4EA~)*I zb_h`%aP3%JHg45bn4VR^?$HL`c*YrvMgx{Dq+7j~b?<#>9U7*rFGM$R_Gc!SHDai zEI+JAi5oR;?}cL(wNH(Y#P}yr zh*6s)zBLe|^3kv$5Yb6!6$!?*D>DB`@UO_ycmk~Fh=qWu3mIdaS`i?+SfbIdypEnudliHRIn zfj^1oq_SXt$bi_+YjQ&IpA!BwwFRVb%?*x^x>4aZ z{U0qn2kU?}D(KAbsNp;y7I4f`3DdH7Pbqin;uYF?oxa3Yo?%Q!dK$3+AGYlS5u!on zu<+E&mt#M1*+vMgDM2q^Ht&jAx#L#J85*z7F<`wwE-!~F_d@o+yQ zo$g0QwGx-!440Mi;h6=Mm`+Uz5BXr(D}GR;Sb`Q6v7nwN252fni8)T#F8MJqFCYu5 zYz*U6p>jI*SXv2$b{x$OCwX;b;S+x0EQF!MZAIV0EV{KoLdEG>LQmBf0#7?>XK9iD z643L%fcQg)V|374e}JQWqwkl8k4MvIFD`U)c+Z5?BIMjdNbX8$eXphR$Z%UN57%NFp*6RAj=t` z0o>HL(NpZO1U(gRuJm=+SI#kw!;ul8;m7EzMlO@9%M)ov=Lpgr|y)lYL4J7!xDt`gy7ay(~{{rl=YfFY@o4#lpsbh!9 zShsYmJ5@4y1qnQ>Q2#0Z2H`btW2lE64g@rrmsSUf@~3CR!4gbXF9rjc!h1T=vkl)r z0yp$UCrrK7kR1@a4g!)3w~gHU31~rY7edqCNQm*m6UJwiw9QOg%Y6UI!l5>Mu}sX> zhq13$0eTiGn1}Ydh(W~b3*xBS0O9uaE#4}Urei5&NczT zPRhJ0>G*GLs=^d)_0C{j8kiGxWPU-dPDMk9zNzL9oGQ2M zRdvp!RSo^R))Hvk;5mt{m{s3VT`{*eH?8OgOpR=(w1W;g#aUCVT*k!FKC^wHang|< z4hrw?Q_?a$tt>rf@M6szXqqu!M$`EoP{`fs=lp}}>c9EAr}0wV&&V*lR1>3#TXSl9 zJY=rYPh2$UaMI%2;ulHWbxI|75TDK}ajFWHaTRuL{C! zl|MDM^jkt#P`DXejR(|7K%qI^b39Oyal+~TW+iaYxQJ8*u=sgmEsp$Sa)CyEf}1!b z@JD}~5(>XnScxG*h4V&XG*^#& z=SfFZ^k+o!6ouixj%*c(M>cHf+9ItJ=9oW!EZ)8!H1yQok7WHHZrE=dS@|~6V<=e< zko!+zITpG&gRveYm3*dn=+7Wtfem-usn;a}52?e0Peg|T3;*#Y@=S~^-w&ivtZ0M@ zji~6@X6xAS+7ur(&pgk{U3RXx3vfu0Ona(!KlLQ`5I1hWZP1YS&Ex9#sNs|MABlcJ z-FP*)U{k~`eLAy#P?=ML1St% zhR~>8v&ndH*UvDw*XU6?9hReQ#iXmKladHx9QkHsF3Z=RAl}3cRFh z{BXt;TV?~d7PSBF**_zJQ0TdJNyQx6h#Hu~usTo>-X56^N55}A^Ws9ZV~?U(6#1B_ zZ3#CA5Dcv`F@85afbI(0zj5Ed^{1FDpNXS~x zv({X5e!rhN1+Qg=C%BL$Iu=Aao9}qs!t?TpXkhwSd6!E6Uh)P-YG(^DC)icB(#a5> zup&b?l?c|E)pPf-`e<>)?1lx!8OmMD@^};m+l4(?=6If)1IBf)Y=G&zldD#y1y^XP z!O2G!XNo)5d}?Bf+^0#}f8uoe4@*qLWa_h(>obJdeW3H+LS&DG-n%2k6?U}}ff{qz zVoyf?q`SeR5tq&U0&qFdUgd;PX$cFf*m@8@b4qS})@D6)_tQIQVBkcjNq?baNT{L_ zr`{rKyjAdwQJX5=&f#OdcNs25rw<1b6!X{=Av3ve_)|=u;Tdysdy7;IErT`4g3^KK zRgv56|CoGw-;iJ=eT9a_Yc-C(vHG)WiI=p@Vk}%?ESJvNwp#BRlFb79q#I#fD9r?X zJmK5d7!SOU$9-mab=2OYI?eZ~3V6J_8nSCPySOVVCYhVswLxbeVb9yRQl}U{Ww`xLm_N#f*oR z{W3mkPeE1h?Y~5-yySBj?|diiB5}Y%pK>`}{`IjQKB1AQ2fmfck)?%p!8n7jNl zya%dYC_EbTTMmbZ%*ud#?bAg%0$$D+Vz@ESt<)-pceC5fi~six-BzmZs&y5#@n6{3F(g zzM8?LuZ7H3A-jZ4?99Z@DwE14mNJ==#xkgrT=&GvHkVn$Q#2LFDV=MoObVOIso)gD z8adoW$aK&q{?HHc_okMo*kdi?anK|rfl-oVOWv*iC*Zzi-m05@Vu1TB!qwK-(|9tx zUhQde#j{V6EpGlcwKag_3JZrgs2)JJM@@USyXjDl&|~)tuHI&{${BEl?xIyCGrzWa zQy*|bJb5a``k=ICcU8rET%9+z4!Uash!O@RL_JhH9CISW=aN>5a1?Hho}#}_)n7Q~ z+s*;w``2>hC37mXN)J<%7f8xZpuWULurjBXDU*CC*Exp(A;UOAbD+F}Q-GhciJvKL z<|tYO?6j;F=6~RT$lJ8BfcU5w*e&uW}E7wTGMN zH3NgotOA(c@J6gQt_TRH0~F9sU{<77SO=6)Sb#rPo9F z{q(me0}vqSlht&;qf{Yd71Z{l@()kVIH33V~CEbtb|a)Tk{sj2#D zt*4`_`&&0t3I3(NHz3huB?nsGSEd0S&S{+G-)h2szaWxFRz*{})EQ!xo%l<#B#c&2 z#6^gzw+WhwT6B(SjiOdV{=PKpv2gNyXCAr@<;B)!niv0R9ka0Fzx3MKxFOO;!Hy|r z1Z?p5^|9wHo4WcZ!`qwRlp#ENgrIqo3Sy33l6rp{;ZD@#-gV-315}j5u0pJx1(qKC z&{J;78rW_JNia+Kv{W{eo-x{$RoyOavUJ#lmH|6Rxjc>5CAA=InHOsHt{V2+y zygN1-9v&!WYCo&hmD!L6)K?G|)26Lz&f3$BzmR>xoVztFYyP|FNjX=IX$3`@3Uo;d zNT~=6HgDpu(?a=(!gm06q8d`uA2eiV8uqWlqy!Wf^D1qh5gN)4Bmk|Y@@7TM??JQe zoIzvJ^-PnFEiIBvRGK|eExZoB3iw6f4Y@A87k>e`)U?8n4@28FChlP~=v-ACJ;9=jP8wvz1)SX1NM^yp6)?gI{H1RE2!Ayw2BTqy4^wb# zRhqxAOdC#1J^c1owk&sY-u&g`zUiUjbPtdX7XmKNyz!}ya2pi8d{sZb#21#S1`pr3 zm-DWxm^2)ok@xG(>6=56%oU9`*H~Iux~dCFu3d=kIe2J!*r9%Afs68rAikUIP3Tl= zW|*}{x#4C*FsCI8olg_(0@z>8!P7&6-Y2szMHzQ!xB7y^s4xO@J!th@=mFg;Ddr@E z3!pEW-I;pIq{O90o?Xq-Jl1s;+kw{F-+kZ0w$!`|sev-ZW?_)>l2~kdD%xi$;1#`qIF{4u>!;Tvq5S+5e>kiIrL0wC%%8+ZIMknDaB4~n zSa(Fou&$piE@6`c|9xE5!Tj@=koVt(`w4%0(k!XZZM&HC#5Qwb?4M%lZ~H|lvJ zEx+(cRj>Jm9FF-RzDprR-XQ^L>;ZPe#v3!kE6;8XaapU-oqOh0e#uW%FS=_>HnvGk z)rOu;qE2rmS!I6hje~l_G4PvIZn0=+Ra>G3T>-A5mwt39?v+Mi6Bk|C4W!$F6J89g6!`lWR~Be=U)_^XVA^@#CDI+u z%&*J~iWm+VR`BTS$Z3^`Z_QNxuT@d{>3_*sfKTct?n?e$+4as|tX9B#U#G3X)}Lxg zQE@NC1sF(?mznilYfU<&(m0Z&vuFz$xRU+q7OXfIXTMJiS+dVDnms9hI7H@fmgnT$ z>;`r7g?6;A*(LvbMJ64p4dA~`pRNV>XK6FBjhnms3_$xKcrJ)qA;oah*sNkgIA@fj z1;%h9SU$^%DbhXcyWlBfcrf5A;a5k)tVwZce&LA=Ce~iT3 z-y?BxmbEGPc(|mn*QO4Z9ofh~vqk725>4+{V6L>kK3({c3|f$DEh^DE4nYEe4>YAX za?18v03>8d?@jTR#adai#>^DB?r+-EK2ArmKG$YHQ5Ec+n0PRVcNv?~{CFHi&KGyy zo+G8ZChv8=riQ96!Pt-%xelm!t#P7#wqK1%KYK)bGxBvbDx7I!`CRW4%R4rb-wuHe z{ZEoTPl9;9rEH|({}FrF^v_H2@&)HUxIRiFkB_a?RH}^t;40wQ0>~mZCVFdQd~3d{ zX-+3zzL*X4eN$UbX?!=E>F50hTYuRVX%HTsl44GHT>lE*vGZfnJudt0CyE?QwZ9Js z#n6k7SSzU|s|R1N`HX67&2_{3>h-}Fwu)1t%~7=H1!ZysUF^pTP5KOv0MKg=v}0A6 zL0%dejo#xgM(X7{h?IT%6lF_RyyP)lJKyJ$14$FQISv&?JnF4tr8BGCl*M2ozWq}4}Dwm-}K_E$`(;l+u% zQAR~pu3@$L`Na2LkE?*E;Lf91cQR4%+(VV+2>gASf^xCE)ta~-M_jY)RQh&5!vS}U zlluwnx=iStkd_BuT{QxNjc!bT;gUdGH?WR#Lm8II^-wJ!hgvZre6!zga80w zO`cF>=l^~th) zp8ppg^Y$rsZk0+~Oc_6o!LV2m0&M(xNkQy{vI3%jOn^NJ4Oa%I z9oN^ZyzVyshK9p(Hbe4&=N4`{0sOJ|BXIstV;5LIwO?cG;ibcLk?v53z2n?!V7z+( ziV>{d@bVDm5;c}suLw#v13<^{F{=u4k>9(50}_|X%DV9MhsZl9WFsI2{j@|eOvg0{ z-nv`6zi%1&&Gufyr1tiTn2YQ}Qhz#*q^JnMF2jqgY@){cm+^IPWT>^?w+48s5e4wy zy9`MGMj!BngYP3-e|8tKn3&I1`hf-rgJnipDSl9)c-2Y_jF1(=@}9&~ICVa0{4eVl z{RJ$Q8@-2zHfj#pHVfQE`^2_w&IIS&+ZCa&fG;ATp~AN}c%DLhlhnAuXv9OSVjb_Y z3uGngkVkWu0l-;P&V%VZcpb~O($Pk?d*@4>gbY$m#j&DAk$lgFsr;k>2@?pGf}u`ihhh812gXR7nr)$&l@rM#|4d z;xaiE$RB-xt)Y7jL`sT;wn^BSpE8Q`{7ao<*4u$9M)IndqTi4ua!HGd7BMd5bMTz* zPq8d5u-oE)BIU`GEBOOSChtqryskhs%aT13A_RBmULTEQhG*XY`UU4d*4UQ_1_AO; zls~@sx!kk^cBJqNbA7$8h3i+dj&=j`a)eT&X6{`sy25V0T6WNj^YqqPSDqPD-P;xV zRq_V;^1d~G1=wlH^#lItw99;L{YN|P89OgQShTcm3tLM{!~|RnhW?Zz&b%68K5!v+Ai|Y zm{osk)b0Z95!WHL5PtMB!HNNJ+Da>M=zl{kSkSPvd&HPwWkP zx6p`dEfy-~w1Ew}Pk7YZc0l(mbp3Wmw+%9!m^0?<{5apCBR@%+ZxQEjRgPkl+%SV| zA~Gr>_yFQ?xw=#xH-WP6PW$8J{&5c}%sRuQfbNXM(5wbOQ{@!GNPKZJnp*R{e6od2 zwrV;}7!!2#O7_t^u}`(32mi095`S8kjFjsS7*Fk6(kJ_;n+$k}x1X-}E+`^YCDvkf zxr=||rxGOhJcFotHuM}{ItE6vcXk*#&(nssSe~&dTu9M&gYCp6unIjwqv{&eNql)L zZ(titqKzdl$QVs|bH`V=^w0pu5i3iFecUK?ZbWM_AOQk5z??OTD zL|WIJd|t5hG4-L?N$lfmzC}BU$!Pua)WmPQwn1r!0!7OwZhZP9c{yTY;xWriUS_L= zoP^jT2zHO90Ud!>@73Q^e?pPyr6W7tY(s5a6^-+bt}=1OM%wIzYNVY#2~e<|QHh{#dW?r09M(XgJ3<<%>ts83q3U8&Tkk|h|7*O zC_!7HePhCyLb&Uw3CCxOIRB9XD7w_W$~P!gSY5XMtl-ecQX15pjJZLkW#_l^VBY53T&sbIk#6Obb= zkG$9FN+N}*|B)anW64OIamVfKcyHI4Otk5c_lJl}i-JoEu7Tu3KY$lEVu#_S!FiS& zA%#o80?{uO#(U4&vv=CU7EZ3*u2#aEE8jj#GhQ0c_TC-b0_p}?d?l`IlkKf*7@i$Y zG_ywbW=;n=d1vjvr@Lm=0@Q6685t4lf6VNpNEDK1A12IEN^;~rNuXOehl*48!j$&E zXRQfNfk@ahofKX87!C!5;GXx?-rtjH7BOE6akcvrO_(s*+kSUCG!r2IOg5_ceh|0h zv%>R^wya9@ExXifi+D+Xuw;de(c_P7EYs0(cc#P86jvT0Yhk73CL9Ni9Zg8VlBs3M z1Uls5vdcWyNk{=N0L(#Q^^hVR{1#wM5^acnkHxx>L*jU(`$^PSg@3Ihzo6F*f_~N+*75`sf0o56u&@qGy1FaYaEoC?8h!q3m>}k6khFOjpe+?t%Qm z)kA*fAfHc#+01?ey{m>fNvDaxr-N4;-i5y~Ou-7)kyWq1QRGf=I-0w4Yru+2M&ReM zoCczNUk;?K9>7`Z4F+dTa&CyCvCy3{ge@@$gH@xlf|8P72sp#~I&eb77RoL{@HBC` zmANqat0hJv!PGCm`HXm`kj&K+h%a`d)va|A8ch-NK8zD417Qf=Aeo+0eU5`Uvm<>W zo+zx?Ut0j_WbL8PzDN4|9I_aVsFjSq--xV!IDZy3-3Nx2`W-62G3+;U=#R4<`;jIl zCd82On)j5ML)%zc%P*rVCs->lx}Uy1#ZF99GiyJ7y-p615Ko(t^;OI)z*>1uV0JXs zbsnildWcWWDInD0+deXh_@WypXxy|i%kFN)Y&Pb93TVG11xC4=aD-WCq)RYxMU)(L zA7(Wq8$*Qo3JU5OzS-j4lpe#Q-IJ)ER)g0or?W zIehD$Iln2&7XyqOtsXW7qK%5_-(!`ReZL=5*3`Z+U(yY;q%XdYYgYOAX7YuA$9+~O z!_LMT@*h>4!pe9qPWtoDo*7p(q!bXd@})L~{O(vGkkm-rb9H+wqnY07NYMzPZJz_^xqsWRT zdbq(?K8BWi^vdJngZ{!^-1;!|M`{eV79Y(a5nuVV`uNw9fM5_eyUYxkUK8B{r*Zwk zzqc9-namtdsqXKTjI5dYapqTvrtTc22e$4I6xvDdE6rBoV$J@vL%n`8xrw3ptDx^E zsuqioFCvSL<_yC!2{YZAz7GzLy8LM>zSLLMk?HoIK|dxwfSW*|!!{N=hw zrQ1z78_Rer1fiXtoA853+=)8H;KYjeAU)4ODUzYCJ zZnvOq#u$^)oE0t=xLM_O-ngcneERPX;R~c$tquuRf|5i{?qeZp&KTMAQPBW1p z(eOo8y8D9OsX)E?qgg8jOA;_?f|TUQZ!oJcE)!vvgZ-DaJNLvTn%YPldrvjFl^Ts&RSM?<$y z5KqOp)KXmW6&{_tc7heIDM{~bUpI}!h!^i@xlT>vrZCfkbWVY*lneXR7nPEs!vU05 z5n6Q?wJm3AKw1IK-fkXNMOl4gQ{=4{*#GbXBsbD=z; z!}ru(Br-^a&s{SG_d>EbEMoRU$=j7s3Iu3tu6!+BbNVb_@`W#bw_Svhe}52VWCEl#*@JZGkVV#1Ddx6A3b{b_K{e@+Rp;( zkHzUbLGEN;AjY@@!ke# zpvdd}6KGVvg0!InFs>^}HdJwqKxCiTn6EXFqp>~jtfM9ciSEZ6-M)kaRM7{>3ct1v zAS+8dS5tO8PuBLdJD=1GT3t;C0Gh?(M>>LrECm$XcmA=J|Fl-)h$VzKuD706tG{(5 za1wEoD?waH2h@TbJ-{#X%;;807kz#g%9vmSElzA?bn zETu;RY2+Z!S)$t_jutp^y_P2&!s!+OirxqdZ8=Z;dXn8<`HWnldo2oe1J&Fyplgn; zQWi}_rKWF_6yX}@CIsk;1AmTZXz2X#O!U5t=Lup8tAzP~*Rhk{){K1bW-qs^Ch$27 zq_psB)J}2;;Dx^&mTsIqpH+{apl+%xu2$kEnqyvcLczHlSCv`OnvH5AZig=lfNt$`_N0ZKPOPBSfSlMIGy zljxh6TB($!dV#z@)PTvvK`}cuP-GBe!nBd)*#GZ0NPN!BLoy{s?!T=-MhStj9;S$C zOPb`jJJ!B*;z0P2#F+nRrYURy!!xr2|g5ap>$J-77dr6L3n@ys)_CaP>d3 z@kLhkYoYVNT`zjmHKS93QYatpMs@ zPcBZ7dO|9zs*Cr{<*zJrts9&&h*@W;yu>qn6wW+hC+)+KWjVxR3A_B0TSn#Z~$w@btxuz+T51h?!YnbmQI_}upB(^h^)S%Qgo6J`C={>Y<` z$IVzAq;|l6DQPnP*Ls4gd}&^Dfh1#bWnb&^*(Av$!-xY#d?>XuXkBrR++g`~SyJK7 zA<~nIK3j3Yk{Z&hKx;=kerNVYR35lO8+=vKCc24d4fX11UT6>D~5>zyVsB?Ej#Sp2v4aJBt%;V5XnF4A(87pXU@HZ^Luhq<-q^k>fheZ#S5n zu*}p#rF`Eg{g5o>mw*#LVt~|nh}NX-qItS`4jRF%lkw`$r@L0fi>6*EJbZBWRub!r zF~L?7N5XH@u4k1!nx_l$y#^!j1K5shSNxO zPB~d{ZlPA^?ViNQKOEbS=rS*jfVzR4VhAc?C$iCL{y6-+n`iYIKH|ci>&EBF`xi*O zK3z5(l2?jSlajsP_8NYhQJxpN_aGH^tLX&J7KIiCf9!SEfB3CbY?zXuX-tG0K_uby zxc+;5!yooP(^-he4ZS9x|9eGMg-&~Gr8qeXeHDQ2vP^v53RZ|T%pk7^6N~>HvJyu( z)5SGd)d*A?m_6pKbuXcKRz&s6+^G10WC2aNIkx#;;ym}u9zU-hkaMI7lF)<}ABYu) zQwJp$JfV{O4&b$BcO(;I#*d?}FW})MUQ|hW=GwezTU(U^W5XjUU}XkQ>ej#>+Pt_f zp$x19rZjKx%Xoe_E+P#rk<_K8yRKxPpJ=Oi8aYOk(U?&SADLV+H_Jg!vWf61ns~LAyHRSaE7c)lfh7X z^{!cI@d~htuoQ%=;1j2|4xvQ*f~pyD$#*M5A+A|2#vG;^l_oTsfOF+N9NY#*ds9u5 z42pi4=vt+~_|Q#{0V73--l>Z;KnUFLZah6<9cXj0n9`)(0{q7z?rt$8@a`(%30x}{ z!LrQ@a-m(M70X@8yH}yYk!R=3)p{h#xm|Y~4 z#WMZ^+*3dK=qIUJ?^$n-9?954{Yk^BDF$a1FXp zk`y;;F+}>;DmJTqEai3xmQrXmEJdXBA;b5t@r?CY0=?aQ?c65r#prXD@l9%N_ME@t zNw4F7410Qq%KTG{`uc?Uja2w=9j~{W!Z3G^ua{S@Y7EKsOGON?NX6!pL@2fPf(_Y(jqDoPa&%em` zDNjl3vQ{pEhHib}8JQDl_Bd9WbPmIX>z=EkvbOlJ_nPIC!M*EvE$}(NiQcxs%bSw< zNfV2%(EuSsG`D{HE-AndqEWyC?MW$_Us+Lxey*7x044?4^|YQdK)T5}90%FyrHYpV z8uESN6WRoXOD@ed!Xw`W2n(RFF(>@lQRX6YNh*)~aRVx_iO;ojb=fZ2IIo!78%Y6p zls|pUJn+{Zx=Jd*y^SOPvv-LfOb6CboR~F_>GcSfHbs`xoMYB-t17xaSsTvkm?!3B zBe?|Qy7?dy@t8j{hwrTycaA23w#Ph=GZDCiV!773!(@u@L^@SZ<`Y~AjIwBPf~-uJ_ZB@H;Sl zn^&?NFq8Y}^W;f46PjNU3 z()8;1o0Jy89SjD^3F6afI%pcY-&LAHnXHDjvI_XyeC!H7Nen{y2t+Xngokb!a@oGd zA*FYMJno!3XX4PlUVSh5C3zmV!W$^Td zc)Q}*>J}GgKz-kwo#mY1t+A^3z#GF-x&B&hihS=P!+CnCGG=8#Mg=B$_O%er(IHhmSP(yx7!;-RLw(2XlxjgZ zqs~00XNM`Mim;Zg{T&H&3=Vq*sDoJ6gX<8|jCGolrxM#@adC>#`IjM5&a}Qwwpeh_mMPRSl%2xX{cnLcwZ7Y*w1+sq^U5Hd%_eLD2;_ zK2t+QtcOel-sGF^6rEBH=FOn-6OUcl9D|Rjso9 zrE357b_SurO4nmBqq~RduV)X`UfwZ19!v97OS6}sg{%)lqw^ZXG+dYVTx}*btY!Y@ z2-|C7QKzk$0jNJ71?DCg%6REYxlxv7y)LreC-pxIiw%4hs$+Ojv{EiyI3w7l@{~W! zTCIMXklQms%NAHJ+M|Ok%Juy0`&@O@u#BqA7P?ZAP6VVj5@^LXQ(WjZNs>WC;g8HTm4;IWx6Dlp+ z(O0snxlx2W(|Z+pZTHFa@7b>wtaea0t6B=>D`iuSXC+XDyR{`@i``u{OB1)|m&h=Z z*{>8Y6%92xrPHhV*%yOkv1Fg1?=K=&3fB_-KR{c6IJZ}SM*()tETDWCMe+}M#p)1a z*>}4l>D;y)xEN>zcIWM*;2Jo!ZY){q;VV#;hpukvcX^$?+ZdhsLZ0o(Y~IR$%+m0(To-#!L}u3?q{GrMoKFij6WDQVWzvJyRGUx7rdfr z`shK5#OqPC7wNHCDPZO5n}GA>rv_LNwoBvm28l%GW3<}c*0jqC#_q8@xXi}TNI#V; z!sa_I0*(MK;BA6$A7KXunu#u_YAp?^$RR;0$O4Xq3#Y-py@*H3#9N&XTqU@#TU?PC zNM?LVv$cq8=1Qa0xrS?m6O0l4L`b3Ans88PY20PM8ux&&kQE^t5ogkhonMOWRnCS= z>4^n-!nvx|$2T85?L58%oFF>g6Coe1p)x7h=f44!d06knx7fFqDB~DdKZ#1S2o$2* z_}Y#J99{;c;A^B1vWPbp)NB7j%Zr_vEe{5bcP#gpqm)wH>1`E>LRnPoSFZ9HwF>ml zYfzs6tZD@EO$s&FmksxwM;sCgq5$Kc4N$i<-W|g(V!LdO;jDRD@DQ55j$vAdArN|^ zY_VYNiQ+?rwlti60>A8dbogwQ>chUaUTOy%DjJ;#GEKP;T`6^s+CAvU|xMZPS(axbmW_ z;-1b>p3Z;NN<`9GMbJ{|&4n+%z1I;RS(1~{U6`c&07i{D39KahkZ_PJdrB+SZFQ#X ziv?}fI!l0k>#x)|&a)PoU9SVS?KmFta8@u$K9jW5578=O*rt@_N#-H9Tcu;m`N>M# zH{cqSu%4qQZir&WYk!a=^9)2--v+q8{8*$^j0xjrrs%sL-uh^EZ=vhyy9eq@@jH~^ z?VP0sd?BlDcisAGZj#24v&peHb=)VO0p`pi#yt~AVH!ca&@qLYHdpeux5mi#4kg{< zp2S4l8UgvP(wZ~CGp7n(gPtQ&n7pTLY6`d18KcyW4U2AsN5<5bO{eARCVf|f^3YY? zOo!?T5{@5N#0Mp($Wqm+AZYCPUap4VXz#P7QIr)y6y!_Li+qIW?)=A5OEIngzJR}Hn?;;fNoHU~UgE2XqpKd1`__!+_Qh;>CVm5+g_WLZ zmabQ+bPDDP9{_oel=$^mz78KIq)))I3!5nH;S^ZzPVmbyxSK;Alx8w=Nzsh2EenrB zp|R*#-igh%){uE4D%DWM_HQ#7*8HsYD57ZIum#`Xxjo>%R4NYPxd%}q8EleYY+l#@mW=z6ZNH4*{QPEs z>F54iw}k!US)cbQdgFF0sVNdKNR#er)RAdG?JuKRnIaUbr?j*UCW(}c4&xD&7`&pX z7B}L^5W%N=eRj$XcIXp1eU*wzyq&xNN_MAenZga#DjhFJ5ao1{()ThJzD67a zpZbCMGrEnl{0%MgWac)r7O_08OifvAYaFutDcw}lHSLPqG0L=O?hDMbWXQ33Z10>t zKjd?4?;O;9u^hQAbR|))O*~;teI~=iZ>`5P$L-8LI5&y7>sqo}@~f*VJ&mY2L|}b5 z=W@d--Yy=8|mDhkwuAwtW)wc7Y+%hzvK# z0QQDF4c}zKV>}<0N+;~Nmvr9ITqTh3ksE@4?}MMrTga$Zwz~PM7(W9ov&xWZLn~UhOlUZh znQBmDFGcjVJp0C-9tnU1QH+>85>!>e7(L~pF(tcLOr_d8s|B^E@_89G#&{AjF{hoL z|8l3U61$ERm~;4bPoQwhKh#n=d0Gc@ULK$M>8pnXUB@+rH`R~~3 z8W6Okd-V3gv^ZMG*RwKa9IwY$$*m6}#-_IHP$nitRU`r&jvg7E%BQ4D8MCU_)1ItR zdR>H+i>r$h`gvsVF9O37C}RU3j~C0+hUKXGHw}%VuX;$UZaQ~!rbU(@(46);=n3Jr zAi$OR?tdy^1iCu;5o%)7f&d=&UQcGslgbh;<56v+A4d}YFu+?ukV~SR(#sHI#hh|K z5z$4uoFJ>;EYz^DN()tp<`FWwFSh>|;O4;}XE|j@$JI(%dw04<5QUIVk^t{~ zXl!$Op|}Z-+bjr%`|0sj#0O@zDSTF%#3#G7CrACuD>=WcI`j2bj26!+iZAP5%ORz3 z$S;$z#Ui5x_2%$6j~}IKX$O*}Do`X-@+;9#nH0gF;lC!jnU61ludWgWy7D`?Zdid$}s-%u`iB;Vh z%NOI$j4yutTe)c6tp z{n^E6wB2iT{pfa7hyiN#XK%qA6FPj!SZHo1-Nk!Gz1ljqNeJ`kAHU#c**$mFiVRy0 zle|dO8t_C?r;m*fb%;t_`WryjVxZvo7br&i2-#1R&lH}YTl4eTfiL&q>fYE->rj3 z3L>}oj5Z$<+NN@-vD1C`C!edW6Tt(hv$hX7%_=hs{KsEf;=$TcC=sihSxT@+QCfLiyF<3IQSe~ zm=37HuLdP48XwBxz2IQj<@RU(2|afmn3doYKO0u#d`S+w40IQBf|_Yt?4*xpq@2RX zz;KO(3_jDf2CLFi#Z+=o^kSX-cbcM)=Jak0uU!J%85EUu?fuSTCb!OQuR%Ybgy{Cgd=&l$Yj(wQVH5hgD2-rFn~RaDT{vg zjIIk0&4HdZB13s)V35$+{&RxY&$Dh1fF?B0D!%@?TgbfEL-RBm_!r>olQm_J*w?Pi zS7QS(fLLN$C}uy-RF$G+1+8qXY)4GqQ5CP3NfYxWQfbC2rV5xSN2e!-lH}uSwq=gQ zUSL!-bM6M>WmvG!U1?&^bzSGH_rmv>%&ZgI`I)nrVq0?nl1YYnQn5w5hxRn&g6k|! zPPpO{_+yqECMyXc5%;oK1&tzP+MZnRsXshU7{O5b|)Olh&KKRc$M&^Fo!-%M@) z>0S)^P#iEqleSgxghC(zHH&t5y=MLs>j~AdupG)&;O-F>W>9_f)d=FM@0g7w)wyvs z;z~^?RH1Q)fFkLh#fq^TD{C9fJxm_O;u=xp3pq%DCl@r~O!o%3@vcZBMOzg@wnO-V zlu$)47_3&{$dO~8@}fC9k4%>^x6z-cOrtYeMvn{%G@O}ja;>(NNZ6#j5hsdzvj1M4 z1fM2(0*)E%#p#Kv?2HFtSl~jy#GgM+H`Uz2eg=3x675a%Xg@ZvcAcx%bcr=JCzQ&V zYn7~3$d19bxEz()sBCN=%6+Z;ka34lqxpxP^N)gW*v23W>5f%%(sz7^_FJSExm^G? zhuQTl)epJ){#4!zHk_Oyp@EG5mXUlbp_sRACPH$J5k7i}KKVYIq^|c!Jkx9?4E|+1 zOL5(ksxZN3Y$_v$$ekq$O-3n%rFVD-b{!O0DjGQz4#qPZ zu29p8RlHoJE-w9{k|5Gg!}8fl?~o_G+v)O>zlIYQmI)wxZ)Bt!D?Rm{RSHYWP!>4kNx39XDim1O*>9Zbpf zAlWzB@6AmSUBjX8Tu>w3b#wcQc<0aa14usHx4&r_dN^9ICudV6^f`bio>qJa^xCS| zu4iD}sdpxB>YSO>{Sw+-B!7F+IQW}(EA_pcacytAeNfC-mI16Mph&9r%W^f>wS=2A zg{Z*k=uz&S1(nsAxJBJ?QVbK&J42ucI@#(K8q-GD`jX-Ry+MzB`JhqRKjFL3Ruxy(_J3QnMw)b&eE2a@~Kw z#3?djDCd*E()L!w7#0yQCnPoq=80gnDeId0F=^OKa;)F~oMyIP9%pr8|blHgMBn+8bp90&PQShUx>PR{ge;)LTm;6B0XvgU#H8QZ+|9X=1 zTPJ{9Q=F6PEW$X0Jq*d9PyBbYG#<5$U*X}zYdT7fWX8wz2+)T12FGAJY9{<8wjCev zP5o13*FV)s;x|`b7f>gwA3H8MZ%9Ovni)bZ^JL?!9eMD~&G{ZFOh$!#)2#h5%3pDQ zr4)vm`3vClXO(Ejz^1m1tCDXs(=U+lBUHA~s%3|`X#ZLV0b@W#lDkdVI8Om)UI1@b&+|9Ak9Gb2tq#L3badw3T+UGZUY?7tqz@35t;s ztKu=Rs^l2RTvg(67nvGADrw|^GCb=NlXY(=huh`)8vav`pDJ>2$ViF6rp6H3qtu6D7GUPcRt zzR!=jpY+6DzH|`4x`PEGDmsEcl}*hCbYp->IM_9ED4PyaWEV38!@!aC^=T?)W9?wN znlU?bvEmY^2iboC4qF~PChWsB?@zX-Kq#<{VD%C`5dw5baRHL77YrFAUwvTDwQ03X zC`bt+^vt_a|00eN7r;`9IyH4Oafn?nTE#1V%AKNp*#!+Y@<=WVV?CJGiu(gZYCUGS_lU&_DWDndb+6p!^x~nTxXmdCLW+!#XdUsnoL>^j zKXT~;%xgV#;^GXe(jx-7voVWk!r1CQUp(H-&*N-|2JZwbt{o~a*Wys8)Qko5#RN*r)$qHp-_%HQ8en=ynT=aH1|w!Fh1#4>0rWx!(WmlXy7$HsP*ykzjO z&g%xHlAbIE{k1Wi`W)$V{Pj%|K^s<~I128-`AV<=TGth21b7hV=*5VxInWWC4U#=${W?9( zHcW)bjZMy-rcW8pe|*uBQD=j|DQ=hxhwx2qkYMYe5(~dtSf%cRP?2pE^4OvA7Anox zlI1rGw&y2iFqQzF{)vCg{Tp8SFBI2i_4to9i^?75Y(iv*Z9DW~S1J73hOkAR5*ue{ zdz%(^HC*{#OZrKFGV|fp<|=m;;%*QS(nC1v)|ARFm<5-wIiOYG8*SYhA=hEowF_nUNOOwDPXM z)b>X-z=-#sehmF>?qFbd24C^u+HW4^*)07Ee zv)~t0D&pWpg*!3nu_0vH?H*gXZz8xKO#fs&>+EpxYda*!GT73o43aZ_TP|&|%Rp>r z~$1WL@}s=^(0LM)AZXjWDJ&d{YnDIrYr z7Bjj^#=ee^iD*@ z1!igMhX<;#ty6cO@zEm8sUVF^vUC&|p1fxRm7ojG#JP_%TY3WDZ%<(LYMN0t;n|Zx3RLaF4J>yM!&g* z!&+~KL~3-k!!NQ8uvuD%anMr%GwR?o2C`{=iTLzph!NIdz5oEvJNHw3f5risnq(9u zP!yDb@$8+p5wTa3YnoY8VGujjGk3e(7X&>Yk-VbUfBTSPlrG>xVpGMq0XW>eA3zEl z6hXnPvq7hrx(JDP=6=?&f<`1qVQZ$_+BP2-omL8kY9dB5|5T4r@c0FW=HAL=wSUR& zRKnCQL#jwt)6|`&!3K#pwLD!R2(ubd1w(S$Zo;VHVyKoCBf1-z=H8jcCNNciMir|m z7V@$LxVc{rMY`(OfV=MB#P0vX*#>-(cA7aRzuuh6!pHYOgOi#L%8!m1Mn$jBscC2R z{%l{|&r@lMEL&($;q>)LpbAW6rw&fXLLqz}qy#_>>;LnQarzcp1tP4}a9hb@_G7*0 zbD@q}>k8qC!M)0O{3tpK_9hmQG1$q*cGBs#;;z{w8x$>4S58HutZVE(B7hOX)?F#T z_^gGSuE=apaMD9yKQ$KQ-Dv%2`tHbgr4a~&1E-n`PF3~b z%EuT~bB=%U6tKKWA-#q|GJXL{UZz#tmz>?x^1@=6u-40waxf^!=r^0xse=~oUx`MB zN6*XqlCM+)nR^L8j|K|TVS3BMOo}MYIT$D-PvVa6>!x^>B`MHpq6kqtjPxXrvbArx~ts4Ox&Bn_!csc|z1V;?slr4eT! zvm?lb(4Cf8ugrIMG(OIFVWaG1NSpH$%}6XviWYN&s)wW4>Q%OQxaETVzEj|q$-@0ox3UuYg*|`Lv?1^ z{8tNib z`%btbPl5*7H*Hd-nVB$zl!>&!|NB2Xur%aVEBd37N0z(8pbL%;pTTD&btE$w%3krk07wcB zBLYG#9Cp>>1iTrm@u!8#DFF}!ii7AE+wY5~5lg=$6+4u6+hiNa4W=C7(Ip-13?~%R z!0#zKYx~}9HDVo=iLZccvKYEaVz4u8PIX>dB7t@HZgfko1kJoTP&`7*l1&bPI?3`q z-O7>M{;1DB4SxbQWU7y-v$QtgM!CqyQdvT2#1sB+ zSa~d>W$PqO0(yYk|eUF^2lJ_m_YEL^}hC8}KB{ z%S%ok(>3aI%M%2_)rRAH%D5lOC^*yBaIfa?FAH9@njK^0QOBT}I4G$QvNn`w<6-Y9 z1>M#IC$nXdic@RK%fM6+ZMC>2BAcYbl zbUew3S&VxO@q$UXOINNob&zJbe0Dm!)smIN&3<6LN+%Y2weg{h2qYT@CC3C!n>J1h zW_?|L@hx*RQR%DxCfdFdwy7fTCkSzNss~rVO2ji%ht^lD?@fuBu(T?UNhqC}{PEZn zINS?6^YhE=6~Wm)2(Z!&SK&8Aao{2qRbe9{tWM;Q@g_$3B-A`CADM*neV#kFZjweq ztfeY(xElLVr}mJVa358LTr$#F;B;BJ)cWMpx#Z!+Sr5UebMy z+d{DCm1p4NllckRe45}6M$@ueBam-Ha%wD`e-M56BGdn;JO64odP5?0qpq5m$Hb{x z!p6{5Kk~KIYzw&vcIKeVwHnFFN5_!&M#c#ue{&A@F#;McPW*s>-jBi-J7^ zFD!*1X#>eyDL=kR?zDAB$L9s?RSAF3O6uhvwldgk$8*W#B!{novvg-r!wPy9JcV-G z#@xo-7N-{~<%V0i*qtD9`b21_;{lfC_3K;mkqL6ot7m?Hv-QNjfGW1ve5nQ%aW&YN zXw(D1yvx1eh-uMf59gdESI%U7SZmXYeS2;j1GMD`dGR)}DD*t0SdQgItoz?;za$Ku`xecO~;u2O#PA# zwfcoU|27gk)Rlw;&W2{_AzCpbfr;UrCA||!7iPSzg2v!ES26yum0K~Rjs(*qxwaPj zVJSmC?)NN^DvgHa1>d02?Apw&Rn%4>>$vbV<9lAIclxPZoX92HMk{V|n>?w;n>@=z zl#V~rM z-M1BJ>ql+fiYCM(%p8!dEm`b*HqxVvenA@pslo7m=EhR7a&u!NYD_5C)bIwO6JqaP z9-XdX(4#wnI)*Nl+_rM*2+v6iH?h>v)9nwJ8`Q8;^L8xG=sPm{ZQ}UiMn#sN4zn%) z5{I94Nw~zOQav#$xKF~yr7^*5(D+#bGw;L049u-Fh667ro#wN2D~;~r-|4&`P%2jA zGkW1s-el5EcP_)uI-sUC3IbKexBuQRXB3M8rl*vw%af5>mqdn zGeL@|u?L&UIEuWU{HuK{3`#}`Y*8kCuqCV(>sVA9OZB*I(tgPfw~)h$8czxXst8Ox|oxE>F^v0MSi5 zc8(Y6Zg%gL;~W4)B%N;>(7Gr#sRk>;1?1RXCH6t8xpPo>9)8#h-3oBoRb#&*Z63i2 z6aCVZSEW#&117*xSe-)bEC0=Wq;}FoTA0x?Ifp)b70IvN$H~WNuZ9Eg4G|W)F^am1 z+YBm9$cC>Ljfn!9@+)HJjE;$E0_5fogbscv60}6@)nS6W3MptcwqFw4;-pG3$dK{p*ViR$n}X|Sw?+}6 zmDz#(|J}#QH+mpIiS*M6Plz7`C2;bkE9wcs{{f-Wzu={m&1SJ3YRl5$W%f`U)W)_Q zZhPO^w3V~OT9|TEp10WV_gsdNgl{)YWqEaq+M>33vm-?zjy!9|zDXTZ2yWYmcCH?M zE>CTRuqvJOnIzPSE!h*Es8E6Vb+i(S;06_TO4xT@>|LxJexe z;x8SAu8s@;5&9lqW-g^4D`QIlPy*^}Gs|Y{mK0ur=FlXqxDvvMP{QzLBoK5gc=&(O zu@q@WOR8=&HBO@VJe)^M_CNd;gC?-&Q~`fS{l(D|+P6?;xKq|Sg)V4!d38kZ6+gg& zH=8BO4m_vfRJhvGqAG) zH07{%7)1Zu@}428=8d?NO@f@@s-94#NYE#%;0(Os*G5zOJRK8NDzW_=_m^I=RYMO* ze~vh&g@s;z3Elt|CG(I~GxzNbT%#%&m4n}~3pv!%YH$+-eS3p~c-Z7(?8P$+3yP|t z`!csaT@+fcsTnrq2ruCE)S5Ae2fabBKmlFf8ZoPpN#+?glVuGFm%SU{RHLX1?!kse zf>P0WF`}0l-np7gVe!nut;1>;A^Fl10c^o~Pe&xp5(vCV+hxY?rE8OOYF6|4%pzZ&(8dr@yyys|X-`5K|IZCgEK*}%qYxr|D`vcQIf{$@as z780yvtFC%v1!Z|tdG>%hhtSSh=DntW+5})Cp&+Bn0-gZ(={0^eR7UnMp2JTx5)@ytXOMk9G<{Fs z-YO+3Oc^qwZ$fGU^*6~J&PKjOV(=OtS`QBOJn(rXJ!Vn{qiX+H-9_m#gTg7qAQT}! zxQ(TaP9#Qg;W~+m=eUwn`>7$xfrT^qO(5b79o{SEp+*+OFOQ?L@-m)9Pu%Cn2z*?6 z(1YB{ZpIf~{S1oUrA``@==Avnbw3!&n$UGg@@E%kWhsn-nM6C6ES(~yCLX|~4E%h0 zKphMTVk5Cp}4>r6QX?lAEBiL`= zk0}@%9;aZv)6RgGT^M3#viF8fHS_E(>-uw~Vvsq)#R_1b_r!aGg6;!9>-czR=zU}d zr_IK1tjxtxNl~9sn@aS75l+&%EV%dn0@y+9#cD-+%SN}SO1)USMWHgsaX_vby6|Jn zfuN`)Zwjw=3SL&VSh=3g^B_`In^jP})H+@KSzcF1$FP3ISPH0LuWx#-W6chwy2f2t zKj3*y(vHd#V8o$+Ei*{`dUaq|m^w1-oR0X|bf?HZa#-R4bb;?=86+`ZuX7c;EIsQY zk#|Kd5K&|S({>gGWe)L#g}q#Q$^8{+na(`-r{ohrOu9#S-|x}wmA~W6$)EMh)A`&F zcg^26iiI10+D^B=aJ1ZipW_fRH!zDJRkEY` z-BKVzuM{2TXySxJnu-QR)(A5RRjj=TZGWc2uMJGGo&5b}{qoK2jdXE8;JLfxz?&~# z`0g_wDmbS6qeFj%BndMo2)>6=Zrm(-2eUF;%y!U2$WE z8M3%HWLr2*|G;{2ZP26>Ut#xJXk*#FWL=mfJjXKo74}MUT-K2uo3%^5c#eutPpB<~ z^fGr!%_K|tUG<9glwTyXlUOyBfk5S;Jao6!(iJ2_eSf@jk()F$81M}1n)my>(9kZ; zBP{{OhD_O za0|k&ZIryf32RV{;~QIDY&NL*=`2~04!iKKHi~FQ;9PF^BxRVmxIgKy1Kf4*rJF-K z=5hIJRA|Pnw3J0;6a=6=*U?E}k32tsy%hq@{};B)8r4WXac+9D)I7JS$dPFijodaq z@u9<_;%Fj$99ns{6I=TPQ0=%jZQd7iwjI7JfIsq#2|hA8_E8rlJpp27U)OV7d7jjU z4m*frAZ@b~ZH;UvKLpE6eE-n;_ZJbf2&IOd7)!rR+6fW;iFE!?i0gtojSf=Rr|;kN zL2=+n*rFTPo-HyMjPeZkS|v(r-$LOU7m=q?%lo3u4K?<3FFh=Y0Zb z)_l9Xn=QQ(b#>rcso?wurR%8`IWn--Et|@!Db@KHi4KV}8xOXJ2@MMKhD?(hU&ZQW zYL8;2c4a$ECVXK&EE?8q{H>@Ee~dQaDP$aRu`#T-tG-Sb1QU# zMIbMktkekAHdP^VyRnNas?=f`ffANLF}}qOU*T6=)g~Eq5m$ZD6&`^}#HL z2qpU+H&)44tr~dt6*E`a4TNjh9#e@cBQwlGwWMiWxT9`}z3Z~v#4OYaoqElZSx7xN z8-6$)raQfcss`TIpVLJbQ?1PRk!W?X7b4}7Q7*L?|M+ITB{+W{v>kb% z`hPntmJ<&!ZV9;R_dYH?0q*WFJ!R|SjfLDj*HpBaU;I{jw)SHFp)Hn%zV3DLZ)XBo zqu;VOOAK$CZ$i&15d06PPk=38`2G8hCqPFU%GEZ@)_rw-JJ%DSTvrv>{$@)6Ip0?$ zPLspkie;uOM7DBiL%4kaB6?$zE2=EC8^fF9rVq`s;$e`#BKO2E7Y0t)w7e9&chHP) z9#&qE)tYhZ9iQ7g)8jROv2iQ1>9>n=%*s!#*N5_llOQ{LZ+Z(Pni#Xp3w3v z5OZ~@p}_iPk#}tzj17m&U5pDioJZU{n189RAy`mPO08yD0_01pQ2v>5)@>GwCW76 z5ZWmr!{sn8edpAAgLVZkHa4hos-r){B&4El3BiW9f+$D?k!&B}LB8~mA%EHB`of}} zDv@TXUPEWl?c0Zi+UemgOisXanB6waqSZUp6h8)H&rvSpnGWqxJ#r%?%F zc*I%kos}=AbIFp;0M)$%zosu!x;PV%=y;X#Sc+(6OGLLFZG98h&DXXGI!J2~x!d*L z@=p%169$sks<~jLH zw(AcaD_&`o8j8wuN{Lw{n;#KE(3CUuXLX&!l=dT3_LQ5yQd`bm7XQAz#B1EY(|Y*x z3jM>+kgCT==}6bReneG;(#f~~+2MQw45Tn@K6++80d~gK7YZXYH}!wM`l7Apa^*b< zr1GsXaC6J;PG)Oc$71%4MOUy3HE2k=%U?uvq`8G7U$-%CU)4+6&Z}3Ov_yE(Fo)u0 zAv0Pyebt%ekKD%2z^mXVfb0`s-0`()`4iyt$?N`uvf{5>!jIT*><ZiJiU~$1X_r z4$mA@w!XznjKqGD{`MN}NoM^5mJH^%{YQJuD7kEU6_&F1!k7iUi_QbH3OZ%|%} z6^*aI`}4`9?gCPtF$1Z%H7d7;XKt9{aC4>!S;@B%62C}x*LnhY&OWU8^>{k$w>bNA z-36^gU2B$cY?tPf7IwVwC}Rud7t*OFj{2a~WmtkQV$C#2rL^mVW0HWOij)-&{fmvc zDS{URm{k5q&x)`Ft<`&}?>|E?C>xLW$=WEM0EIHd=!HS5)HO$0l@pY1i&*&;EXQsv zN#B<)|J{06Tki?xRhP(|!4>6LQ?Aw+9iqqzM^{4E3u?l}WS}Vi(~4-EvELK0rU#lS z-ATJcWTGSz1WU7Bx&j>8Z4`k-a$tobs3-rMTJl-^{^tp?+QeSC?bdHi1`|g!HbOVh zv&OBoir`_0qnJK>rozEz$k^f3Zmog3;5(~IokV(LIDWEP(MsGr)+qN%iXny~y;#_8X0~ zpWuOy?<+RUV4^Tk(c&x@69)uBYJ>*SMh@cx0GJm;I_$|{lKGYnwdH`Rsf{Vdtjrmq zCKkj1Oi9gr>6Vgb|9CFTHgQ*JdA{q@BjsSvVW75Kt;MfQ>wx*(SX%(fLP;&p^yR@^ zqz0=Ni@^G|_!XESyh?}6J3Akh&n6$q7=Yg`zwCHq%!j}7Z26|~cILxYZ;;Frp!f;! zMYz8lVa6AJ{X0I3T9@v*PXIJUAD4ULW-(1=*_;n9>V|?J)*RK4vjkW{*f2!SqZsm= zsb{+#$(Ahm@esH?75`LOEWXW4|60&P&}iz?uvAvL zX*@+z!|k=A41>}@V%{l(OJKKn%~{&3m(k5JYSAZ`-&y(936WO9u5u$fSnc3H3a>sa z9_w>Ew5$ppRzpovfbs21`8FmHr+17-Ss8JZU_!e$rp>)OQV201%-MULBW2un+%en- zvzi777fe_~=+0y?&7|G`eSA-ZV)QTx8Y+=Z&Yg$hnU7T5cU7@7Pt`u8>4wdD9ag$` zArf33JdDO{cj*;s-L0C%?RBFBlr6t?v*!_L)NkHx$jKNDStp@;7vsW5VjdwPgI|xo ztw%dA_LBIdSXi*eHQe1OWiUL;_+KxATSjA#D4fu43{a;5Er~oUKW)a&j-v z21Bo&N)FvYx&HCGrtwaErA=$&MbkAMqDSQJKdzQ9YW*xkG#s_!{qTm_RE#PS!{Al< z;%8G<9fTF^x_2ncl&5*Ap%5KmXdRLI7diG-vLh6h^e8xU)q@UU?JXS`$hJB;N52jK zIJlmD5-Q3MlH?TgVO_hnY00U&T5-`10o6raakW(pYcM+zy})s}tG9fZ3FOV+Zx8(? z__@O~Zxz9qJg(<_Z}!fwI=ynG`jIYQ&x~h%j$xGxJhzA0s`6hBFJ^!7YHRV&jUsQx zJnp*@i%KiVGj)DYLIKI;Dv{Fi=rdy>H7McZ-4VKGac`+b)s)DWqX%x~7YB~QJ_m+* zlpXvPE5f95&kg!PmF8HB;^1++WI_!XeB5)9U(M;7ay#QQycHLB(9YM|mQ~5#*PUx{ zf|}VTNIl0pL0wdh8@Rp^G{fmYawuH!opS~qdkIRNuXoa7`NOwUEaz^{_7TVNT=xmU zJN3}u_)_L@f4dx^qF3YWcym_`?9P>uh6xPBaw*rWsxOz}X9mH5_NayJFN9# zYMgMw!(X>kPXH@%F}mTa1kbMs7JB-`6OOeRJy2YVlfy9^s}1I8aszu%lQ_aXpGk?& z9fcga&6*FWiVRZfP1S=(-rdx$mda^HoF~*r$yW5K+qd{~Hdt~h$MR)Da+~!>L{D6# zTKg%Z(VSzv!jrWY3!{N+vj;UWDxgSfE;!$-EUvY=nLwBl0CxMX+@j*!;cpUS42H$( zRJQ$~y%~x9o^dRL7*~a7A8J+0iVviOAI08Y$6ZRS;3s1;d>@+o+N{5km1pOqt>wab z#uFK12w3-cpz9c#=rk^Dv?`S7hs8}5dYB3s&_a<5X*LXU8&wf~B`6F0S ze9ce4drH6APrs=3>O@8o0g)H?$cUR#aFdgLKn%K;t+KzGp8y{Qu73+Z0fOoO$GM!1 z$HK3nD5fGwy6M*&LF;LzSi;k*HMMLlVEHA~P8pIX0NY0Kg}ykO)whn$3R7WjZAy~* z;;CgTKhmHY`Ms3K56uG)^Q^yqJOMUu#Ge4~zV<)L=HGJ{O}k^0dP`Vcx3pIqR=U(e z6e?kCe&g_J0Zj&h8IU$DZY)DG;k))1J&1vnT;0bxv|wNMbH|%+{FmE}oBILC2{C)8 zEg$g;)25ejN<{Va1ZIZ~YEXJ>Yklm3*fZr4(xw(G2eE8w!>Hz7uYzF5f=e@+`>xh! zCN>SG5+E2A+{49ByS$MqgK{>`Z1&Rcc)_;5THnR7rMa19sjA?Qib=`MaDzny(wMQc zIN3kl&NX+-Yi7je=;6Ht#N=H(q=uQSV4MXxOIl2$cC{?~*N;D2w%MDepcddktu;Fe z0Q4=*A=A05E!CntlC5T%K}HoOyZ>sS-RVm2&w)ee_`+jz|J)`ftr_9a5dCy3tk}We z6|y@vR%<%3OorFto>j`CDPyX&p^Uz@k|~gF$TxKEMK1C+&7P7wahs8qpYICj1mg-F z;Z-Q}eg~we4*Yxq{Ba0a36g!p@7y{M;(XL-mHiA?+;8RMT&}1)pRuc{!9p;K1Bf)k zDGQ5Reu{cqq%8Y`C|%rE?yt}lI$Uv+RI7OqgSTdCdQCQ-&V#|q{b|l0bKY6lqYv~6 z5V7$)b70zgz%}=pIz)6zh`}qAZ;H;_CPHRb2kA2p;{8aLzHR;n;v*Fx=9^})C%}yN zNNg8IljeUp?)6XE2wkcH@;2engcFxXei?OgNYTuqW;D(0UuG#{5syfSb5r};rqK5py zeg9yIT#(C<>$h;REQ+;y)*FBU@kq4cN|4^8WBzZ06lK{tf5$)W&rVo1iVNh7BBNnW zlDNBlWLJsJlPk`8{sqw;Ce}U@{fzIGiKrWekwXf;t*hIPu*)+!8H<70Mq|4fv7 zj{aY!?_c%7ZeZ`o!=3+U!TUdC^ZyLx=Hc@W1wcok4at!ZUbN}?>~I=wF4kjt8m9Fr z_qx(To{v@~hN) z2S&@xs}77C{ziIt`0FwB36L>&4jkXUf*7Jn7kRfVk-tU}td^U8ldvrPvgtL8|ByWRT#Xu{gF{$2#5M0+5mJ#9-`_L-%Lh*)MH1P3!b)Lr>o-G0gFwhq zH5O!qt|oG74v3hY3d=^25!@eq^b4kc579DoPXGkK=tu63&LN^%BzHXD9ai0m%^(c9 zlrHTwBt54dy76wTvTSKp_L+8B(v1~)ispV$4h%6jo)&ADRMV{&9ba)-a1^N_p=GOz zsfT!lb&b~5EHONCKU#qz^ssZ?P9#0px#Tc{};RmP4j;euj z%G@!xF|p3yLBWgpS9*_GE7b+F2i5K5b9tUBN!Wf!CNZfs)y5~JtlFQI6qMyo^*NIe zGdt~{9HDrv@Pc+Srtz75ft+FAJbe@lD#B3H_p1{krA9i}hxFa^+VKII+rc zCJ7NlllPE?My1axp7z{mKVr>ZgJy?Q0Su`U-wAigzu=z&^>wwnX+O^J9b+y4F^Z&_ z{#bvE*e}aVQAwAp2{U}2My--&9j*yYB?HKo z{5w+z(W8_H*~$K38wc~6J}PXB4rOSzuSh1DTN6vWWb8BjLhMonUQo0VFkuu;N5~D} z9B`>NIhRn3K%sxQV)?^BOP}5~ZKD z)*25Iv%$DPHOOViq@;4$|ErB<`n*VqrgpU_4{5R?F|5+&H_hg=BZZd9u-QhNdo@Gg zbM0cX6_h6LcvgwoVa8O2%2LOJl(gu3yr@a@YGUp#nnI@=k}JhHe)iv>RmO_bSkBny z1diNbhX=~5n6MeU*0x#6wk=S8EYqqVgOFtE#Bp7P06Mqh1KIS-Rz8#6a<%-+eP`BP zA4*?}NuAdwTvrnm`8DhSw|;x7LM0)4D+q!pdm^$|<+BnRG#9{&z=2Vz#Y28dNaoYq zr{nDZ^xe$mtXL>ncllt$9u?N+XbUl#x~D%J6V~F+GTl^xnIyS`R*jJ2VJhv3VPsrr z%8>sN6#mmaT)z6aT?`}1_7hWi`I6pMQKV1(a*fjZH{NXhAtazsBRALNYUl}|m#>Qr zUulQ0*uqzddxKIyp))^2T^11zWe(v`l1d*Or|wnLX)PSg%Y?FODO>e)wa9C= zLL`21c_mvP`xtM(%UD)nj?pn)rU#lf!rSplUFFht!eUv_218Y8<}0M0IUXG6?+9j0 zulZcm=sBDYHXfdjP1w8BZMfNw6ESWX3v*xzl$fSg%_1Vj4(<41Io9A z`MzxEP1tK+)-zcBC9M^Qz(7!iksHE`Rh*t7HauXnhwYv-8RuhnOdmv``APZcmjg9^T{b_&K17;QfRY7-mR8lNNJ zkNo2B)Z`>FX%(qSue6WR{=)!<9r%TZj7y-MedSizDjA0~s{cfCme?Nget}w4@_xYa z7Ie(A&Lebn-0;M(00HdF$zxSk_woKnTsIUDUDYPBx5^VR?*vq;hFbv8&nobre}Bu7 zfGHi^;)S_Goct5(mzY1_sfwec+i^S>M4@&KjFkqp34>gayYNKeU_m9-cyMlc1g zs;=-0zwau|n9?`FPNwKVtke1fu-NsZf2EOllTtT|Wb!ew;6~p%1hb5j5+SogA}*j= z0%n)^vwgAmqL9;~0~=m=VBLPE?|KoD7W^>%Ij*mZIAyW)mr9hEz#E=ZrJ8Dsi8H#< z*TgXgUkAMHy*k=kn_nIzsx6zJ+`jh?tp$@P&0BdvpIKJ!3_oxCaj6sqEYN_PPM0WE zU!h6=tx6J}Sto{UX``Q^&sV7_ zml3Vw3jNwe&B@Q_mzt8@3W_%aW}4NVa^%HZW~7=C$K*xCzn?)^Q7@D;w-VKhbUvvk z2H27j1Y#Bd??*HD%vH{LE!)#rg*;t6_dYz!oYOODt3hr^F3rmPL}^4Izl517X3Lre zL0^j4{q9)4ed$EVoMCh+n!=1Y#J$0@frYAG-ojT|HiG6jRsbWYKXLhJ4+Qy7~bhj+FOwnLQxy*?5P2>gDXn}8XvE1L@FkD=EyC*De$y^h$^_W7;Lh>{1@)Bm5NmU6m!EJY8yW zk{pw=drB0Z^CLjQPadHe@{`uY80?Se+a41pVC>u;nfZV&Szk1H1`*7AMUPVFhwYa! zIg1Vd>==l3s#35?%#SVV3obojbU|1a+1bOF&4_wyx=J^sz2qxz%-FM@`zp8B(eO5gdeQ{paTY=0AOgJsw?xpJN)F49n5K@!L{gh=oc4h z%R7_Are_OK>a+GC&CT{HEJ0#YN$YEnmid_`6p;8~FGxbz@2ZBc^O`&-1_OV-py&O-0_EJ~85XU@p} zIJ^T_gur!vX_TAHH#AG5%KLIRY6ZJ_Vd+kEO{*?ss6;$W1_0m4M!#0DDFLo0QH%GY z4Ap}=&0sGUrKZW9TTBoWNF(xQ%{{q|X)k??@x`t4z?#cHasmZ#d(ONDU8NtJICU(t z(p!xDw$>kbPR|OL>TZn7WT9vHS&kiB_9qYS@(UJ|t3!<$KGhXLD7Ps-hJ5vln$TRq zq;81l@#piQ03j5$NG)!wn@uhBhBuhGZN&@qA2Upn0?(2j-vAM-zz7mN{pj+3s_}ka zscrho7oM*6p=fOrhX*Y~y3InZ4aQb8yo1*@W*oy=oO3VrmO>RJMCMJTNKwb)QoN4c7!ROmZH*Z$;IQ)R>~v%Eo$jB5=(u1yIZ zY9p*#r4RaZ*2PuY{qUoPs3A9P>!Hi~R@!)7z%W7{LNYcwi& zRip)#n=?MrJ2o_~sBk8?{y73`c*TUVK@m}4B(%(wf-Ei$Y{6Gpw&;=8#Yv6!HOaC))meun zy5D<1W6*rQi%Ta&(_m4+UV_+)xPQX2R=<|mX|%(iVX4&PG0s&$mefG$d-Oqp?Uepl zue~QlYcm=JB21KZ0Ui_4W`wex{Qh!j=+}3qxeatqeAQhV$b!Ln zF8@c=IIX{amP4c_LDkNcU|K(oQHpg7oi1;?PHs;qd~kqAPMMf(m)60fl6IH-`D|gV zY&4>>M-VQryG*r7rMYKKVleNj2>;Z|FOTv@!<5qPg@t%lz3AIY zxl3(~!rfp_?Aoh{xulRSnMYk0CQ9bcB*GS6dLhiGCml~5;aO?@{0BR~Pg`8klXw5_DMxEwhBT ze|~{#O+8X9N%S-hw3n=OD_KwxJOgdN-fOSEuL^$at5?Ye2AYij+@xM`fe6uN^fJS2 z5^_}OS_dZYsf9Q7jX&<4y*JeMqGgUiU{O@%sm8zWx)dR(YRYTFeqM|w+Op|yF80;E z^ie8m!55_EnH9Wv>n~kXJWjiQSrba3nz?*C%rk>H(2E)hK(&d26qRVQSPL1}UuwP4 ziw{<1Td!ULM93duQ?t85=Ltb!wjlG{D6=7VcB%43ZUdKZ8Z{OJ5nH8FxL`?8Lp|!G zA<7q#K-oQuQ;eAjjt;$CnQGS#vMgbNjNBcWiQ3^E77x8!OO~O}gnCgviV&aITO)GO z*lw3$keB_9B*kFoWY5 z-(n2>bu#m$+h5nhoaf2nEM*SIbsy}sS{zYVh!l9+sC|{?L?fq={^h<%`G(*wRbT(o zR5N!PjCZ~-UVeA&;-jf*=HOY&@c0ruGS{pZLoI?<>6kt!VYLEx(1hw%4$Cnc$;=Y8 ztG1$tFqxMz*3jF=UYerA6omrJirwCT-iJM+p8w-8rdraaU%SB|LW?{*c&kR#l))`t z>G3(bhD?_k*rSPnL${@lq?@n5L7FFh{o4mhgL645s&@*=!76s5uo)i&qG;2$R-!Bm zF@yP>5|mPSqrA^~W!>by2}Moo9!BtHms-(KC#lU`fK&tL<~UUi&x*_{wcCUu)GHVG zy1Z0_Bvk#Wx$~)~oy0(MioUq!C%#&=?{k`;pP;Ssep&wbF-KU8g z5qw6HF2k?T>Rd^lJ!Hpr^m;|-Q#=L9DHRn0$n#1E$4Z6Qa4aQXz#8iOdGb|=^~V_S z!chsEPjRr@9nah8IWYLwSEahonKTT~*Pb~|2|#pv-hX%kU}O=)7jL&ek3s4?RS#)x zlQXskMesKs7F?*$QH&oxK>5b)HUp61xq@Qtn?Gv<->emJh`PMQV>u>V)0ak(qc1}& zPsJuGjFTr>c1w0N9ZIJ77)63g(x_W}mQC>=%RlbK=VHIbvR+A{4qxgKO3K6SPCw(|lLxnHb!Bm-W|Cc3gj z4Zd&Al{|A6Ecl|aPLYXp;&zsf_}1_pUUr?`Wv{V}%G|{H5KMQN35F7KFsjs}?Yx4j zo_e9W=YmWspLvzx$WrA-heY~ZRqWdf3d=x=P2_qNfD>+TbNI0^N=Y+w5ogB0h91R4 zQ#c<-8d1C%_azt~h4jUnjW~`Roi`uwqc;Fz0AQ(x}Av$=^MXo^4!7EA?nkK(}T(RQ|I-I|V_!JiA#N zo=>=%`ylVG_)pC0!9Sl$AAa0%cYnI6^5CpjPr$nT_#QhmPM_(AeRowaD|VdDY}sqJ zRO*{C{^$qjzjie$4Xc+Maa@Gsbe##V;jC6oIATF3rMx(qtS+d>;lrymv_|>zh+-hYMu>FyDJ}%Unaa_6moKRz2%p#YL=P$t36&o9>icKAe_qNH(K`435-bweB9Qyh&m-Mxb^UuMc(_4#aqf0^=$waEQ%+$&s zjYR@)#hk=`&QVo5d$q;UFn#Hh-5fdDYvte(nZ5#0_nB%ry(}92g{ilwj^Xy>r+UB- zKjo^v^Ir@rJ`B8qiR)#%@Bq!?#b^_U#SN-N?6#{LeE5lTa^8(f!vWi24PRH1t(LFn zpuI5qB8pJic+kJ;mWr8mNC0uG#;m`%_}#m>rh?Fqr<_ej|bp>p;4SFS9w)VYo> z8{I9{xp4CYq3bC8hJyLXinfIbGTo2NC-jj+QkVtT=^ojSeM1I?Uv4tdk1v{*C1zZ@ z$-yx}S^EkRdlSD}_$0%bdk^E410$1x1E24t)J%Z^-GG?i7n^FlTdc%&~ zC6JLA1>J(H47lq@+)5=ACWv#4@BL)Ybo1B<@)*O9%g2QnOReIhdozF>%3`(87 zSU5=ZOT5NIiM%N~{@)Z?+EVH+e59#cbk*iHPE|u^;>i5onl^SsO+icJAbZX!H)iT{ z5yP<0&9I@mD*oqc@I-8ui+sv6r^tDvsYIL?u|kD;;G}JW)o*V}>I%*QmJV>Y2CO|? zc3C%8-!`xFK9ddxmSjfH^PQNQ56#7AL)|XBV}hKTCh{wOiUyg@lvB5)oVkRRtPm91 zFMNv;ZG&=w^Ze&=(0?uEKBhd9WKy`G5#S=(si}v$Poeo{Gl;b;XwOE#P+qS|3onWe zIHZgw*cU30!@{nJPkB^uz`I*Rd9gRj8q%XeRR@dB;MEgO8M0bHJ%G<0`6C`&K^okX z9_VIOLoT$NZ$s#vIcT1yudaCPt&-^x|N2<%zgx3P@dJn?fyDXII%0EXPk&};!Q)|D z$DRB2uk*8U^z??(tq-$ZBlrxP?A(6aR;$*$082)MvT9F)B03EV>%!wpH zt{VC`U$*{Ht)6p^Q2bFcTOnKIs~nxhq1rqim}mdP$F6%t20rBPTQLGG@y9Y-dz%Ko zzkj5l0(HOjv=l$u&F>G^LJyzxq#@}0xi00H(V}vw_$k3O^4CWs>;wk|NZ$#3I9I6&oU!(jPOFHIa*pMmmBk%EM?ZAp}IVk zJ^f_Id*!9U_h7L3gR=gNP7xs^1Kzff^pBM#GfWZJtZkIw`!q?QiAhwT*w+;F?X=HZ z>C)8Rs*xVR3-?J1GXj-JhhyCZjb7yC^*Br}K6MQIqS*q|lFpEuqJ=%$bgQiIc|%*$ zW|t4#aGxdh&JVxLC-Hzg<$;F}?eYdqNn^ETUj{wTe|V6S%k%~X_r7Vpt?L>wcSm(j z`4+7Eo($Q>_L^F&rdu&bi{{|?Xu3bG7%ly5+V3aEr(~baxAkYr1>*QK;BzkO=&G;) z@LRgwoSJgy?`ArJlaYoRU7WsLmU&NJ6r1e?Q2TLXE znof%2pPluQ*VJ5^20@8|D&JBtx0`pVv!$86-$!{ET_jCY6d__J`0cA2^>=Fh+PAdJ+TKV^!2I z7l&&WZ(79m+;sNbr*FzT#k_q3OR~B5;svn663Wy*cD(47$+W?CT8+eH(wJ0PAn7rF z3SL>zgYDr;CxDGzhcZx}=6Aa=WtynPDk5NH!9pox;f=d?=MM@_V?N<=iK|JT?CHdH z#^O2seBIEHn!87H83}@C;EcumkfH0j;xK))JLML)rciR!>m8S3bX<*9wBiR*O8R3_ z!7898iE1M<(`$cJ(elv7M9>=dA3Y$qS}V_M`MRo|iSH#%@@gYVfN- zH{0jL{+b_FVOb}W%LY_48f?*kIlnguK2tKUlnH@EY}sl_?zriBnhmgjO09-G&G z5KG75bzAirP$oZs%3?~j+0!97JbG^(_y1z=EZpMQnteUEL-0XEa1TykAh>352`&Q+ z8r&f`1RdNpxCRdv2<{Nv-Q9vD5XhbEy}u*(p7R&n?_n68r+cKksp?g$)?2^gTSjQF z3myj<7eXIJuLar_TQHN2Dxl6TWp64hExU4txvXi&=Sq{A%i{Gu?y~CRJr$P2TiZ;j zr6E6|*4wi8e%8#^dZSK|{s{vM%P9B{*OwGjSqLVkH3{NC){!ZgBQ9UC!5_A#l$;(w zNp1I$gL}glpJYG0+mB^88r|~ejn936E_{V}NbVunbNcN(XOTr)=crO-s-@Ckq0c9pH@%fZ9Ce%jNnPDn}DPr7;a9HTM94a$83Ot%aZvK5d~ z?c7wyu|2KuQq!tMr*}qr1b7rigK#pAF<_dVK;q~xr=p%X3Ubs}jmYkNU&>U=xdQGH zno`)kDew5;O{J;1OpIJUOJFfnfy?EBOje%Bf&q%yR9|eJB|B;(wS+Q^B=Yn{q(k1Z zgNHu5#6+cE$C7Rvvq;ar6fsXCut{vHwCFZ+-)40G_J{jWs2ov)5B6xED_0GoiDjiB_gjw^dm>aWAdmeIc`J#(y@I z?$vAN>e!^by4-3aVc9~Ru#2P)xc4_Mv4F37e>`vCH0ByuG+@zUDuM|0g=Wsh-;|1m z%gv$hh<$a>^od^}hxo=Al$cn~E;$>iP!tKhW?Xo{<7eb1wKK@yAoy#U%!CbPNbR7Bv!unqX@P_y0Xjpi<=L#HDRm$x9V{(&6#3!n7k7q8+8_ zrg~FUt|+Z4v<8n}cLB!q7F4IBPGzPNiILOw|USSJB zY6jF!5qn;^XQJWU7gT9=(<3)CHUFJ}kSLr(Wv442p6=riRDnt70L?8Dan(9-bC(7W zF*4qbaGvlx*LBM$-2i+XXOy_OF_isVaUJ;Au)>1bhSt`m%up~5smWBrk&xjzD!ipd zBf;}$YLzsDmofsQWVC<*zahV+jokx{ZtSWW12=S`FY}(9t7^fYl7+e(0}G!0+{@U+|Q6Q4SOWx$M~Qk&RBHO2wYFSbZtW08AA@A6{_%G_U@COXxdSHiFt-g-ASz? zJSU2(w!5gLq)}BfGIIy7+nl1J4Ita3z~;VwPHv|txl)%i9|$Xz;>v*Avij|SGgTvt zXz;P|RcihT?)>O|!J{H&S`>1ioYC@=g0Z_YrU zo@gCOo$LKx>Z1iXM>+4j;gcCx$-;`YI$e=jb5E$gi`^-oon}JI$n@;&Za^O9xlplj zXv8#y@zihyR4+!Pk;k6Huyr`Zp?p%@que2TxFh|ZOrCL0BC;JN&R{EWoii{Uaof$* z^l&hxKvz+ipt2I(!o*wxbwUCz3&t(=l;v%yIrB*tJH;Y;sTiL-wNmN)#Wg=pHD^`v zk2l_9ykF#A(6W5a_u)CSpB52g?&Iz7`YI2yofiH0hE8_n#psb1?M;D#M449m7(WE1 zSQ09>bAC@3SCvc_WL;#c@@a{9|EV|u4ctuYLRVFa2G%T;0C*ez1{i#IW7Vo$H8W4B zEz>KYP#A|^a;M)aNTf%1{gRB+W-)iRVwud3QX(=ajU?7oUPJ-lY1+Oiyi-waOIq$? z<{S-4~`$OK#!8W}`S4=@o2b)C-oM7rM)Zf2+G6=n-> zmzS{^n>=33F6~eyK9u0;>(&R$L0EphOfaK{7v;y*6GU-LNN*Er=iKt2Vhn87(TSNVkLG%HhsAQ9p2geQ3hVDRT{{M10b)B;uEAkKVWt903If^aK~+4WTu) zfHU{L()Yw(DOC9=9COrmjYQR|M;Egn~X1h{2wbw*FgjtnD>F&m-Lh0=>85U2^^7nMg!HOEQ z7M%z1==#H6myH&l;?J<3HG8?#3soW~InjaH;%n(5JvoHzAMoJ`GI_<-@1jfpnSrB7 zf;(3BdKa`A`HW8ck{nc{Y!GHB+p}_VDL18nhAtZT+6gmB&%ufG@|^0RB$>4Fmv+t& zrG8pW`5gZ8R&4Js5WHS`huiiFq9cdh+p@n9=DR!;(`$fxajWszc!oKMzy`YOqH5;P z0_eSU0D$z%QcM5}^2;FPCVqaEQ~!H8&fS0Fl5eHH$!{c(VMwM_9lRLvC8dO&oVx;- z_5#Yzrrg6&JeIK1UGsc*<+aX9q<}t^;49hPKTBwsGw0-w+j#?qNluKP^@>T3_T?5fj|-J|m;Rk=do_wI+`}$DejX z@;1MccNy5Y+Ox9*O@GP$UjDZq#Z3Mc_R_LCiV1A|oq8np)ZdBei61ktXFQ~$=%pw) zFu?!H_2&%qL8fiD(-KG0<}uP99k3x*5IO*GT_ncO0Tm1UNqq$qB9 zo{Eszt8W@7c)+=!|NVfvr$^G8I~HN0Pq^}AB14H-sJ}kZ`MR4Otje#ai3WIW=Cmv4 zM7SV$ZB8Di9W=y>p+^nS{BMs42vWENl$XhEpyjkr;!Hp7ta**{Qq?DA?{(wbr(!!+ z*-t9`A1)*Fp`j*-n7w${TH*#uCpM$C$uf}?mS%75KHIW)9nMHGYEuQFr&-Wcu&{QP z(L5oEec%@gE>URdXIWY{2HwBHjF)p2DfJMi;(`7+0(Jpd8jHX{x>G zj*+2gt$Kcf1RMvoAm(idvV{O6STRL8h85$Sg*N||$e!wB1A0|fCHnBLMB<#&Nyb%@ zYMhNG?Fg|Hh&5v^{$5UJZd6+YLkam}s<8eAOCFCF#Yta!I?Q|=VKO9<{$8J@99W3>A#$~ha|-(_X*}Ga<=o`8)MtDD{qvkO7?f+weeh$3Ag+nLs2Oc2!U#N{A2{I+ zApgt77$uUkKoh6ORa(?=SNkHr7tun0o__Yvb^D(;4yn8A9?`qlgUNv1v%nagSPu`( z856;@J2OJE?B9T?@VSyKpv4^eiG6&P$}6t7IuTWDLiEGKP|P@a zc)<`4w+6giR+^ro#Pl&hf;K_lhTNjUxAogh44INXY zBusGE$H?@(vHURp`c9oe{t`+Z``DKJy$eUVII{!a$3%TW_GePa+v~(B^8Qbd+8Q5B zD~vA1jFpB7GG@}H(Ko!@6mnK!BPRxJijAgb+NRbY#WS(+*V`);lv7!Y!}=FyNM9z8 zlq$MTC}_zB7Q*|C@^ z?s_yh8xmCR#Jt6oc=4#F^b@A);{(e`gGsf3k9l?pR!TF{u8cs`4L|LMITYQ9D%jwa zb=}~~Vv{mtc)e9H9hxo6*i@b|*)W3z(aRh_fBL6}Z;ixG2$0qY8FrXr##Dg}Nj9Gv zgS0rYmUs#s4h&va*BtaF>Ozf#%ru zjSj=Xxe|KE>Ai^)Kbk$8FESbi)fCozC81y5nhr0X`(r?UB|wc z`YERA5Lb%CUR+d9i&f3$qjW&UYz-a_Vi+Azm546@5!SOn<~qE~vyG`CLBj}DV6;e& zU>}7SSF-mes8rKTv~#Mga}1PKraNMxnK>~xr_qBQuvE;P8@1ZQ(KZ6wt7kD^r^{bN zfmD;j?sJ@$S5+O9BTn5Js7%_if5d!f@cat)?V6sX+;1Ghdn-@kn2BcTIx9$Xk||Vz zvb2kQCE?=5yZiX2qBRT(NRU1!taI8(+=A_R(*G!Ep~iU4d?pv95($d`&y85>adFm* zU2H8LMu8L1S0^H?X>O!}+}#|H%f%5b1Ll5*w=@L?(y^5MB<Jm4dc92QmSA8$3-7(Wg5R?-I93`Rr@>Ig^8Jq(}D?!CU>X z(=*Wi(NwY;KW|!tLE%BLa$H>8oY(#%I*t~bsQ*MzJVHRLN6f*6S<*l-Ww8iwmwRJ? zB^Ff(?8!ZBMx{iG;JB(SNP&lNMn=_N%=yKUqJ?t>H{}`h)kj;7QIN14eqe@#Tg;N^ zCzUCfX=sE;$74?;%2FrtHzI!Mln*%?LI^-N3UAV zZT(n`T`Q7geWs-`J7yU>RIi9m@gqcc>`eiVCjTv)XhBKZqT8N>Hqe+w_u3P)Qcl5! zCcda1M`c&vbO%?3>-ajRfuYmuRKaXucXmRC6PW&55d1J?e%q`*WgK)c1`%vK}1gH zCM;PT_aYZ+mcdf*np$LaSNv!&;fd_#8uCpL+w}gsN4rGm?{%(8+p$1hP3MI11${x$lH)0XlQ zg(vkhVTbpt7gpms(IviaeB(3#hVVGT?2LC&tPu}uD-UX{`8`ggKt0|RI+k@RCEjoV z2814YADRgn&iA`(dOf39TC*b4PftfI`HY(P&-rKl$JLf%WPc@nmTM=Gj+qyS@nvw> z#1AgXJd>&|o>iTe`#Dto2Uq&)>xEF&AA3x$ZI<^!26x>(-Jtvhve0&R5wvEn@x$Tv zF{{nlfp0yinGr$sCR6rwl~!V(bEi;PAr>F4ihAl?b6$~YwpWReH~(z*(C6tsM#F_? z)3|2!b_s*k&dqoS?zIf|A0@|j56q7$Sc-HoKX5Tj6{mag&eKyTuyVy`DnLyv{13zm z(ksj7m~ah&GyM8Q_d9tX!<5r<6}GI z_e#yp;P}xc^1fU_m#0DRGbip^PdFmNWi!Lei#^yrAHk(M1uu~-blI4)G;w64t+u*r1YhU#1?$-t8VkrODw=Pmr+?R*T%C3orGu$ zy1dMy0gO(sJ7f0S;Wawb$ENLzRwvVw^&vZrVTMkXgs$bV{CDOxPI~Qws`f_lTaQ<; zN`oK+==2h$no1by8m@a}7F`Ag{bqKsbX)7DH8|OpVN@FILxcC~RL-EFu#=kC;~74z zfWTw>Vivm#RbxWtOG0obT{Y6*yGXA3g)4Q=J4a^dD-1=^n%%@pQ?SVO`@4lUdD&bd zoT6If_+Gy|Q{!CRp}d%7n34oHi#;->5TpdX+aSq-%Un9+4n0nVf~+p zCZxl#j^BSqUO_^mxT>EG8l-VtT*@L3AT=cTtUnd3Y!?uLZE!SM(?N z`LZ95b`zjJWOdXuUID#W9EWGhZ5(L|BG^EUkCCND%P{EbM7<&g*bmE(mSjl^O{D#K z**mJSDY8EKsNqpE;Z08r13WZ#FAfuUZ{{?aA6L z?H4<*MI~^8W5A?BPhMS6=Zem6qo?nv{$^pAB+{VZX?g&Owe_9-#Oak^vhif>y;9Ok zqs&IeZ&3BlBgNbvirC6G=u1e%=px>S-DmB$rrY z&9QC}bfucp3|z%ZUST9_BUP5I&sWOTRp}PrVDE`DA%a7+^hN)=lmBDQnj3!a{$7xw z#Uc8FO6fxH5*}X@Zr4RX&@Fh1e5J_hII!(P!n(xNUJ_unz7bX^%w7z6AKYnAcPCMz zDj#)6khvnC#ev<7$sjZmB5!pfIsGy$QKE~h%JcIiKQ`ItTWuNCd9BWrh+V6}@+;eQ z98?i~-lry61MMkv-#D3VVtfd^Ai3Btzfhk3S7J!xJPCp0X0o4iGlDA~|3@&S@N`Z- zsRf%F0WtgaPjP~ELrSw8%$oRTY;W>YzWIxvcJ-Wy8!03%a#^phTABAB5WAYCSYHIypKo_S7 zH{3n#31rw!JzAXyb)!DUX9I0q0x!79a$39t$e-(T`7k*hYt>|0^qFij<&ekUn!E{7 zX#jJXnh{9MiMhi&Dn%sbld>umOC?Nsnf^c%(YiYw_h6}MeP4a>GaTDzx3<~s^3JSM zVp@4{3S@S778zv)2VmODQ52yjtIxypgdvxe9$xU00R(T=_Q0NZK^mV=X{59j0;W1102KEh zuPP}BQArfFBXD?6>@gb=Xr*=Ef1&@+7vv>3DsA63_X{UMS1^4T*Lsy^s<^NN;&~^9 zMsDX(8x=Mfoe8oUjX6i*w#XVR5tsAPQ~;gWdJP1cV5O~_Azh%C!`_Dmb99cFl7wSq zmxRaA%E4sSx47Ap*^v)vlnrx+LXHA~mO>-oM#6Hk@*pUKd7HV^jDC6qCDajkK-29e z9?hGzTdWr`OJzZ4AD?#)MB`O7cVKfFCdo16qM`Lr)@T5*KX06<*;Cvozq4 zX_ps?vzo-lpmj-Jkv>e>C`cu2rqh zjsu3xWhQ6$4!70`n!*~@e0D>!2azMo+4z_V zHqixP_x}*~*w7f6?nKBsp$c4`e58K9kO_3~fD7Aj*~41X4g^i^aA8U-#2m%*=MJzH z_-2#&5!{scYF~%%7wdHQ!0L1oD?^`sA0HKz3+HP*X(P9DN0$qGLSpN{YgZNd^8+Ko z1xmFSCip+RE%Q^)o6g$8{O4TDR0oml(DI!Cc?L4|!kzQ2$*g36cLCSRG(=e8pDlIB ze*=o)D!`f#(@SU0Ht$I^Q-s^Ev&*A?DO9(N~x(xKN@rn{+iWpc{x9Oujr>rOcGZMHD#&yB#O5iCYs zSc;hs)py4k0Ad%F7Voru=lsg!dbwVvij5{b(RC_#At9p}(}55l!6BR##!V~sB5Y6< zl3s2`f?8jx=EGUnF=)&gSLe)E?nFg%3_s2dVe~mcZ-KIQpNwd;LQmQFo(DxPZC#i> zQa;x1y=}!J?)mF$Q&U<|SAL_Q^EFR2k9wdrE@9zC?t+}I=AhraZ8XNDlAa2l^Xa5w zTsn6M97V6bldRIFvQXI2U7GqbqMDZ848|HvuTUM$on=reYA8pRMqYAA2}F$CE~;bU zZwfWRgF2~j{J^wVMc~Ox{7OX``r<-b!C7;l>sk7j7?dJ4?Aavw;4jjqwyaqF?UxU_ zREBDuuEFgT8#7&gifQE0&0wpq^5ajx2X)3W0dr8f1F8_Xo7(+RSGk%Vbte> zrP(BOJ`!62Euryq9F(cwdk?wnksKHva^(XqF|O80SSXQi_o+xshu|7~ZTPm_J7+R^ zO&FqO(udP(U)j7IR^W4~nC}F6SlQ67RnhQ!?RwQ3XY1|U(zJu(6wxZEaH~|vp$IqZ zW5Wtx?rLS9VzNpt4>=$ud>N}O_OK#*aW;`#W|aNwQ8M~Y?MIk(i;iVyK5$1wkN*agc7fp{7>LF8 z)2DKFw;$L|)o5s@OQzoIq2ak?;3yWG_rN>dP*^>;hYzz#W2afAPIWH28Oe;?PB_DWZD&lS z6n!p`<8{I=&tEG%xjK4EZuw-6FF<6-qD7n91;* z+;0Y=d(>=aMw&hQH!4^*u`N3LMMy9G@6F1p!7PHxhQX$&0DY}br!Zy*Pp>&Kg5dnS zbHVfdoCBP7{;DNPgYQqlWex7Zp_=gf2CT?QZ81r2d8gYkQxKY`rDi>)Tz|}8b}oxF zr6ebZFgZq8_-=|1_HqGk0zikSIa&4!U&1BPHJu(U#q6ia=1cEeavPm_lx_F0>y7c71-3ui{OgtxUfR` z3O2;bLLIoGr?1ws*!5KTiQRW4nC_*3Y=Mm`x8`ty|7>JZKtdG}N#9E;u_Kt`Cx1&U zmg&*gOhicm#&p@hxqN22gRB{8f?^a@M%{PSsn`wQHOuByr+P`s(Nw44>yrUI^vYZB zL8^n4m8ejT1vu&hZPXJk zzSX|?h9s0qfU1!^C5T^6%1YvyrY~xaRfHMn1R?A&EIkf~1JE4@aO!;mPVtj+XEa)m zOX>1V79W3_2a=@9g}5jhrU^yoCE_3sWlP(xhyHD>@OWoV173kBDg}1)f`>lEhqsKL zU~>C3R;yND*`W)39GP?DC10P|WYMeD@%GGDw6enUH8Wz(xVk-g%}cNFoX?p1?Im7| zQmrxfb7BKF(3<=y)qfZ-2BxyHv-#f0+pLG>vjm3>yF5t@kGeR>gM>Dzprf|AKz zqEiKc99@K*7)g#=FJ!;27I<9Dw)M~FQQnx0x;ThbkM{M{0@Bvucfm^BEg@vu-;0^ zEE%9x!RFx^`oO%7rgCb|&)5zwxJ(~^hSOM6X$_57vNOd)zYwM~@<-UUg-iEP}q2;#N-xvwC+q<`~4T7w+w#o%D` zm-jBh8y=>jxqHj&KYj!9a>a5S%L6i9hlNA?-as{M zEx3S481=XBxFYkwhfMx+;I{>!fEDbJAii*nbYNJmKd||hj+g{3H`*9zfXGh)aozji zJvrzjEqxfb9d~PYGBEXmQrmg;__=IoGeX3dIEPFxd?UtkKq2As>KT>8{CWw6JVgN_Y?vhF8_4ZTC{&zAxx5EBx*?|Vu zs!epSV#Tyv8L~Hd8PVu;ST9q|u|-pG@bASoCpsMtE^34&d`LxchyYO}8FPm)?i}MX;21Up&ur{0D8Z z$v%0|wtqGGp`M#_MRRinlY>zVOFW=gK2cQkrgPl`*<)zf&yKR#JlGQ%yqJH%f!2sZ z-Hjbfgj{M~$Tv0xge97L`fn@mP);FUk=_kNYC8?+%psVNe{lcf6d%e{EBtz3gn}uA zjN%uBC2N^?%`cq!R1`u>d=$1t!^^sbzAE(gOM)(kX=U@T!atY8f9ssXFAgt5JY9cu zbBr?eKU)StW_9x#1G6+1Kk^k?Z*xnSB-!nlT9J?^xUv-h&}JaqCAT8S|a zcrRUni3A_~&eWEZAjMKRCf{6&BR$2Vtgi@!qIJ->V=YD=@+VX%JrXyW+NdyXUWM%} z49dL^KhpORbcbjXG0(k&N4bAijei5QW=}4!Zvp{Fb-cPqH;gt-Z~UOlr>y>mM1(!} zP&1p;lQBIXMi`_{V{9)FT*YdumWum^y%|NjyN)rToR_`gEsh*iVu@=SVR%u!NI_*z3?*ZOusKG**XpcG$eGNI0?(zs@3{g!CA_ftXD{od z_2+nhvKf!Ooq9nE8f_7 z-g|+5>}=1~SSN^!$TVi|$Hcc~>;CcUJ9v9xZrHWXD=}XVVK5SV8Xj_cn%mx&Iud;q-f(n-?l%4`PNC3ji{{xcQXkE@;gjzYtnq4t zdDep^mG4=Y2?fqHML<-q#(3jVNue9zy@?)VxE8c_j98v>Wz7V0~Mr??W|u)2k| zQoEl<5%(klb+4#r$Q#lWs^Faku=z%Kc8^Zk5uWYnhUsN_pkg6p0XpLM4lMOr5daRr0D|6wP!%VRfilKH}NJc+-BEEXh zQJu;~W#F2zG2+-23&{pyBN>>gzH1=xs66eSEQS)efc9IOWu$1jHP*BE_2{`EO2yL3 z3pj>>rJVt&6+&IrRm9DwIEX&Q-faW&9meat&z`6i;%XMMq)tTYkCa$7TM@>G&+eyv=$9Sz>Wl zYIku)NcN0VM9O}e4X>x(4Wx*%1ZFRi1#7>*FvH3k4`2;s6LsP7Xg8v#r`l$Y^ST(k zbvWtQXVKtHmT|{g=ZP1K20d%kO9uuBJagE3#%5VtUYi`K^c5(%$GpYb4e45qLUP7E zakWnD7C#%1OrxaF+0j|>1U&B76PEThE4;2tkICWIEO-s8MYGzynkD8|ijEFz9^S3H zknvQv6|r%VV*8zyCL z>K&#(MLBf?R9nCik6(t>aN~S}`Zo3d1zdcO0kpU*{2klZ4y6*k~IoHkv3N45%|$L z9Gvv{?~G7O^shnvVg%X?bD0rvyIy!Y~&Ca)-|vPha-%T5hsotw+@&YOgoFLSB!5 zV>Wk`X{D=1sIV(PcT%yjYYm+2E07nCt?p0}_&C{xOa+gz(nYB8B~ z)7P&Q;Y~^Zyaq32M588%$z*%Zb7Q7r?WOtA#ytFeS)FwdMWF~0?pZA`r~Q2IYQbW# zLe&&*?epkyLzV#hlpxszSM(lb_+$dN6DjxJFzs-AMY`q4pOzB<}{#@T#P}Q z*(Q)kr3}d-5u|$Drjb|+Ou6kf74G7B-a$-~BPa|m=?2HrL<>bq8pzd%??>~YJ{zNX z1rC8L#@oB5tAwOeO8{MnLYiYu0>7bM9MkkG3bmMsI>n4%pRS>9@c#xZ0$J4$E`k^0 zsM3Bxm5&EW@r$J-$fY$lWBc}G>1c?I-9HX^wZgSEo8QH4)BaE28D=AenP{^1RNtsq zte9_}HVVD6Bu8M0YPU3d{xG|bFSiEQ(t4lUVEsW3SwCNRrGvGNvqJABCPv5-g=i03 zfH|M{gN2JC6{UO-)AMhisTqfPT_=>qsSXUzG?z_GOMe5zmEM=`^fE21lzcFT$T~8d zO8-=`QR6-p-!;MACifUB8TQc+SN|nxPzydxbk^lu(a=1%T&i~)&(gk-f73*b--0(5 zaI95~-4|1^(I?yj&1ApEXOkce{EFZrg@sG;3VpH%dDmw2B5W@nKeW<<2IeS@GKS6m z%5H;B4x2E!g14I@Lt0X;%9ANz!Cd;kLCPANA%+N+!zzyAZePe?%vc#f9mmB^Ss0Ls zP1`a=kS{z~v1q%#L#(pDARWOg@Sr5Ww_d3GtQDnyL9daN+NS8!-cPvWqCMSs^{U@h zjQyo&bg&U6z59x|6jD}i!=FDzMBU5iL%SJ_kxl_G5lzU7wM-ogr%dIW?ud9hIQ za;`u|T9XBp-{)EOpmC>}=o#0LD{#$b46T0Q6W=gS5yUn6>N!v7C@itY9mT$lF=1&5 z%-X&bDk0Qw#MM`rH75^lC&9&Ae;z!V&Q;?uyd~IQdLSJCmCK9XYfFpVO=;?D;u1XR zw1{Hk5#x6`;Zr+rL8(xc0;6RbV(v?KBr@haL{yvA@C(@uKX~3|l-8G8iZ1++@3IqU zR3AXH%(O_4fsUYqLSUfxl2`P9+)G?aeI-t1PT=DlFt}jzl6j3ze#YMumRU+M&6FUO zBZijBYi7EZy>p)Z&-!qv+$#xz#5`H{c5#XMRl;0)%$qF0=f&4UH1e{n?x6q`!@c+E z{Xyb$69I z84Ff@qD;@GL3$oM&c)Agycl?f$7jj#R0Fp}xP}*mL21zwgK{!rh{^YxOF8F)8+at) z6psJ>p$?S`NI-kO56#*I2kpg35ywQo`JOtgaCP!|wNcWA}agkw(`p-mN_=)K+8XE&De|@TgYS z_89Ld!u^vP*(6FG@`xWZ)cM*N^UGp54FH=#;0pjm-BLfBg-B(W^CQyvwjgtMmU;)? z_n_T#_pn;msFVgrlGX0l)#hx(sQ|8E~fJ zA=)glZ8W}wBRqp@)V*cq$D}~)3BY$XB$b(TH6#`%e*t(@(xTq!iFJMNIaS%5bin1F z($9plD+e9K+{aP#6f4%Em>jJjx$XuPkt5i^fff3D|QdWzG-TW1jhdCP@t)6 zpF4F#i;?g`JujZ{QAwZb`25h4s&@`2^d%?*w zw)2s1S@$q>@W!1)a!$!o@1BHEDtIe1|0SYms^{)QN09g9pi?`0Jj~s&snzf*zKOET zqIB7I;fzGsxlN2v+wn}mh~b_dDZdpnsYFQ(%*ekLR{W_N8)v=|P~c&g zqeJo8b=Gxk0m7UfEk)#OLQCm45mWorIPsjRU#BmPjZ(8#Rw(@A<|6vAA?=F>DzB); zO3z_gt!b;h0A!XGPM7ks0FeUp@E=e4W-r!X=#Qw|ZZwF1H4{X&dFPmz0WE6gDhv7S zs;_|JQWCn-y~r|jA5h^zDl`Lh z49ibj>eqeg_PV6ZWeFXr_viM3}00&(LOe!Hvy#+W5=;xbF?C$+Pv6T#Wpa z>QF-Kriz1nEX?l9vJA|K5{;zEn1hRyFF&d}#w*rbgcun0)_{t~hNhMD`H(~Br28h2 zw06%p*V{B#19w?lJ`5}`iTuZZXW$fl`T4OS!jI zhPx|HofM2>^DWLMKLY3#p1Jqw85N+M*4azMyuF_K{b_3f`5M7tRMnLg&64_1%qJX_ z6RZeVy`%__y*KzXZ*K6F_>!F`wbYgPV3MTY8SbL|1K)ujIuy43;euy8eoR95(QmI~ z<$hV(Zj6b^0oksMpKpGeJPvm&I&qe-v1eonB`L!#{}Sg)KO;dhm@G5Lg_GYtShb0x!P;)} znz$OBH0D`?j%EO$3UdVRk5E;=Tj%RHrCF~WZ^?3vF6G$DhS%k@#?3RX1E#gM4wp=w z6D-sa18mp|mX zP~7-xMAhqG^~-On`Q`ivr{~B1_%87v-VjSR9NJ+$?nURbjaia9{Bgz0%m4)+>p@f6 z5SA8YHRi*-fDe|ze2*TQ1BR;Nr8vldA+m~Q(1!3>ymTv=v?<86Kd-Ia&age1umERLf zWi||E^Tw_^+C~$;j(Pdw(~%5felDA56c?VJJ)5K#J#D`I?LvFtup1$bbN*|}vZuJW zVrW9!tp0u6V$qdffDZ=_0#B@H!EgzU5`p!ev)3hperF=dKau;hEc}bWR6m~9j~tCB z_PBp3nDwJR_GvndL)o&jwKK%IQ-EMsQ>G}5M(T)F7vw!SB1zZ{5X7fqx-~0c4eg179|Mrjn?fyVW zLMG2lUqF*z$Y`#q_>HJ8qBhFPMqD=v?j&{R1Kt|P6H3GR*-xgJ>HS~M zIxg#6x4474nECoxC>AthVmj3vOymTUgRFsbQswF15y=pYU{xfv*w}N9+NTB$H~>oe zd`;i%(W>M(V2)Gl%jbdnNtNG#N&~fHXYlP5{v6hO8(eYyE$f{u4;Je}pzr~!Ek;1e zVX3_ahMdHN>vL4=`&V7Y3Gf6GvZ%>nrQ(Wbiv8BjYlw#@s!eL;EPwEsw!0IZrZhi6 zM4u9JS>}hjSlAp%y=I8k)K-RtLZO`q=(T+YdNDbIh5{F>bsWeQF^PZTRo%LUcLZ|% zB)tYwZdNUq((6lnppraP^VrCk?aAc)nf3F3G)k9e@hpj>^TEyBV+i>l> zgW=L5tpFF`Ah2gRm4uwWM2XnA>0C8|l`zZngngJ*dWckfZU(Ph5@M7>Wu zbr)vmAHOXBD5b{!@RYUekJo&kwffL}^>&Al+$s4fnagAQ!N07_Un*_jk|7pIaYwB7 zmD3CBSdlNVe!-NSwKl7Kj@CwzzZHfpS!gjMlTpZ=1rI#IFFu6==EZSE{o8!BKv%!K z;i|5;r=+Px;g8oPCMC%Fz#IYA00abt zqg&rT|Dyj3%X)qFN`>O#akwS%Qs~GtBC=sTfdKpSuR3t;Qdm~8+gzL$tIpUUME4Xqkye3~N48mtI& z&hNriWhu!<@x~-?mf#m#xzUeWG${lPaUaFfAM)jL!it5?r5O=vy=wgE_7KVsoI!|) z02&(MfJ;Kon=Ycy^Y?DYk$M5FUb0pnB#jemyq?nyZmTjA5fN)}wsUXhIDg_j5p)Uu zN+0A$=fy!|r+9e#D-0`5?l+*`imK!}s^)KiJX>yX)@8ETR$6Yqwh{LGYr#x^Vx%ss zT-)aM;`}-1K0&TUQi7iWl=N%L3U5n`o0&AYy@oXcD3ZF#kMqLaFCWYWVt)hJp(;0W zvCA(S{C}G9`2QcmzB;O{|7kP06as7!mHCumYFu{$8&Rvbp2Nqtu%bFxj zUTA;w@=Kp%Ve~x4$v8_CyNVXrxzC6W3z2X@#gpzf>s6bWCTDWxJ00QpNZm&&}A$fY=U&ig53fpXKeu{Dm>;Pq}n3&F!w;>?nbAp za#8qzWlF4YE@F-~OWn@7b9L!b`AjpR!Xu$!nsnCOCk+YCwrq-}01r)*c(Jtrzv<8E z@Jni828W2+XZb>ZZbA<$943FgP9C!X@p40PkrdB%E0nWjagNADISL|=+^+B-r{3P> z8m6ulX>@_rP<1JNj)`uEVdbkTD+7JourM?(luz_JDV^<+JmVR!c3uP3neoa;gq~TE z_3QZx0F%1UO;#^HcUTx1g+^cf^*oG4mR~CudeD;oM2tpQ#Q3ktcVn?dk>HYQ!s0Rf zd15>K$iYRK?{sbAYF|c?65xbN*CEkfnAWNR`lZxA04Ii_`)A&!e|`=BmHH-mY?08j z^ht4L=%VNGn(@QOi-#%6j!*_39OxgFk{Cipq>@N1O+5rlR#>j;D6%`u~x@E%tES=tU%_Z z!hAt3&50j;#sI*Nm9}pkBP&XaLYuUY9@?ayJ~Uu}v3>r+y_;9hz0JkLsATP*<*m>^ zpLgyvJWa0~`~Lx4$9}%rB5m+cy>`d1iL#R`suvx+&XlK!V|B{7cImeJ6tAYjNZd=} zF&iuRp)XTXI&TB!`)V}fK+dP12`|sIR!9@d2%d&Y#u!KQyuSF?&iEg4`Xk8^5pY>hF)p+VTun}_8**YJZJs^OiB_wphNR- zOILa@YF2FR-|up~!h9n$>~73#fPGmon}nH)O?}04J z{P??&7!io|G!n+brT15SZHe_D?)T3VAi>X2k+?(OoLq@x@-c$HQy$+y{sIy!(6KU| zW+d>54_cG{mi_C_7AjQ-yF1M5b)Mv9RJ4#Yy7vh#t{6EJF2N8sv+!RL|GrG_8p9yy zT#L9w0mC1=P|1_uqXBKfmiaDbHwM}x9~K&F(+MUM4k`GJwn+4(z`gj`XqBhr7_~2( zc9Qw~_Cz3bvhcc&@m=yn$)|u^PyCIpk_Vn9)ji`eM@5;v=`b5o1fX=Q09}bP+_5m- zWf=OvnWyZMcT~UE{sMKHNW*+y!AjGB*yldp#sYqmcO6u`-0Qg;9Ly``u0`(Fv=Qa4 zC7giGP52Zd(+vbx>$1J_H0jv6)1yV1AhcRV=yE2wLL&(Y75l!zC;S{hgb3KnOPe@Kq&Wmu9l#u>Ospr($4_q7GG=wt5R1&yH?# zV7y5~b#!FlletN?U_b(T2&r7R?w9gbIt({2JV_txTp;jvO3?0xbAt0IFu6HbZ#{yRv$7yPT)u{kS#xw7gM)@83z&%QBnm zByjzp=lyGKK3(p`&x@`3d$O*4E|$~oe*k;!m|@?)K@Pcj9fnHNDOJ49tf{@&C5Deu zFM|Tj*Zog14Rsx02~!dpfw}&G;;Ms1Zw`yV4{*~6_U*aB+KxvE{=yKK_^^~x&fNOJ zZMpR_sglid5ZmW5w$BD=F&F|44<-@HcFE(D3ZgL}C0F(_@ve(EP7ZL?N~mf~_CbSn zlqd*!A#6JU<|%o*!Hvt)(!25Dy1?!StCrB*!0S|+(DAm^D|fNExHPG z)op#x)Z_3aROjjOFh-m`_wV0gFaVOTCjzv?6*|!~2G*Uo|MIh*>m2@DRA{yhapxIy zojp<##kQtb=fY}83otf%kFMvbz}X>Z1G?jEVqw}*KV}0O=8~A>W(@L#Avy6EBpS#! znb~%wNsI2Van64;zhX7(!5{ujjhUrdeII!L3>sRJ&JsP`yA2Kas}-H?GW$b6V-YR2 zg1_!ZyazlV-QLwJI4nq@L>E`1@PU0;JMb~H14QWGwmQtbGVhtN*2VAC07_!vt8^dN z%P?P}F1Ku{!%B~?Kn}V`&{34Ea9`RjG5JY#6&e?Eb0rb(X3##MVHWrhr!{2naSrvwH7Wf1#gK@a2!``_M@e(isM+@!G=%#)u~59OBwWv!PplIcZ1; z036RH_Wn&=hJ%nbxM(MLJ+>1h(`;y8zlsk3--(dTe>Ov-1=fnD8!%E~_qySJhq5STlKB*GRR>-6##7o2IGVg0zbUC^1jJ?h6$DI@0^M) zjh{CY$vV77CWf&Jj^3S=4kbpoVwd6NcNh&w@+24%2m>Pk3c?SP>NTn`Fsj#wwuF@v zrlvyyM@zewE)ZMm z=+p{92JLZG8r1u?)?iEo?PIQr&#a_ee@@+Vl%k|WonMM*M>kc;?TYk~Nnmg)@(UZv zKE*Jeng?a;gRaz8V0D&wo=IY}M?2N&wU751+JrC=VROO7qAFwPQU~auv2N3uw5kAM zH?i-`EgmeZLeU-3RYn~A6#7j@=&z9D6{caiV6`t?_9Y7%oVKf?C<@$_GEVL?=sqPO z8V-N-`AgX&n$6{Z0N5LUjTPUsJYrB#&w3pB^hKVB`Dc#ldVw#iRRCMwE});LTqXCB zl4T9wJp~>X7I@^Sl-$EE4i1jB)@#ib^JKDE&IR}-H5N^2#DgA&26^4{wJq42L zH#2d3Uu7Q|94lcwBBW71unP$V+HvT^)RCmq7P@Rk*Ov%_p9&*R6w|oIepQs1Z1%HN zIrToK{-UihxQ$*gyrN)ZY$?|3eOOS2bhKz3p>HPIPjcnwm!AgRzuB0f2h@=xTzHE6 z-|hk{dBj4PpXGRpeJ$FuU*HM3TM?G1PAQ+4#q}rqF%fMJt1o9q6ypyI9a7#bqX2bM}o{Ee{pfglrWWsR(HV@>u%a1XWS?~SXyBs+Qd^eOaS|BFl-bYo4#u+FE;Q+F>ar!}`m1%EUyWC8jO;`!nP2E?kAcQin{dNz_ z7Q}H0(r{7Lkz>5dPx(@e&c{n7_L->Uufa=uOARkOZ6>VzmYQqt-cZ)It7?{dS&4(4 z2xJ6OqS}!#W52EP$7uDYC|06DPW6lkJxcOeY_!$@^OJB#)pr7CSy62q;aFnwoJx&_ z!Vn^~GdA$ucu71+PhE?9oB2IsJK))@6>F@_lhiQm@^N=C_Cbbwbd{1Kfr734e zBTvAcrRkA3%AMZA{28n=#2DH{5`HqDGq44%+Ho<9n)1muT@+vL6LnK!!gF|eS#H$J z4CCct{0HD%+E1nMC#miva?gqXa&k_ZK_1GoqmO zSg{RMCC~o7WRM}K!&Yt4iqf4^FJYUii7at}GF4)@lt*>|Z!81w4n!ont#I-M1^K_5O;b(cixie){Qe>>&jJzKKnu$_Gx!U8@VpXIe@ zds$(9a)Kr4gzvl>0D}q`0?%2yEQcuYORNAP8)Q#E`{YLY{PN4@#!tA+*YP6%8~R^% zBC~UC_iplW%-Iv4j57lY`L9zVgoC2@YT$%3vZ*b<%)hIubNT31Fs{mdW$3S*w4O=1 z(3T$$mzOlRm8mxv#PRJ3h6*Ki!Nr+U_at2;HXxCX>cfmIXuVJ{)rzCb_ub=yKbh^h zBA9i?{ytLVp3h!)=+_$nfb?I%yB37nCXo1yfo4}Dppu(0#g50s^mI!9g0@!N%_olR zE7s0^vV-U1*~SCc@L&2dObe2pJKeCTj-~I%A_9UEIO2(o10(%C{4b*{q_zE4G%7L* z*=zixDzPu)uAF7U&i$qUwu}{QV+!oxzAdBl(?#%iACx@&93hQRVvwgE`?OXkGVl#f9)HK)x^k6tKlM_4%lk|j; zCM590_8No*LimNxbfz8yHL_Zsl?}j8H`F@$+G=9*UX706RDRyx2EOVHvY1jCLa|x} zb?Rr@t5wrHja4p?4eW`}#Oa74)WTsW^VBdqT--c_-(9$tMuhGK{--E}l$+PIi5HG~ zkZbr`s=`N+jXvvQn%IWKf&*u!-{;|klQl;HuZ9!DqYJd@TlFLR^m*OPI(Tg_ItR)h z2hF7zTM4Ee;o+obH{=A zu;w+bHyzBdxh>2jaQM^xaO#R5RkxDGKKVwI8XQ046+WQ?-ei@JtoYbtTh7jrn+F#Y z=HDCZ8&Rpy#W;K7bj&+ zcl11NIub(Dg0dq77cs+_A-j9qcHf&UUXOI48$Kyj0d!x}Mk4|6Bilb%Q#h-vz6d;gctIa`gvabr)K5f*Nd+oRB1M1J_y=Zca!) zVTFBf|9j~KCANC~>eq;7=onf5#|jLtHHf zgGG*PC}_5pq91puAJoSW#G>{n&?PuUM>se1K0Xd-UL+Hbq^vD6-Td^_>U# z0;($4*Ja`Gk`TQ;VXzivWQ$rPcX8BrsrV#2!niohDu5^s&|?(7)g5^%o$Q0dgI-BU zDKxa)TQP#5FE=E+iEHQO6ztJ+T;8~R$b7blBha?MViK|$Ev+_$ZHr$dAHl4MuUNCV zx6$9$rO+%@;7faGqO;yyUosFk#5C^B(}$;Xr&A4pla1&}l+WW1>kUayLF^d!DQSn8 zXJAPlGb~?N{%Z-4{&%+Zp`>d%28W)w?^%M8tXwp8uG})%57Qy}yy8KZ0JT*OW{dN> za5c=y>U)3SEsFiKf_+58oHo{8*aw|r_k<~L4;F%jlLlAm#p5PQvcaGV5D3Hs0N^CZ z`}ZMISY58Gj7Jni1ZapOsWfHoOQCE-CVs`YSTFv-kStMS+T#*2sPI3K4uXUKr4oYw z<6-IA1_M|j*f>>$0fdBsQ3OdZy+uAciTaym?;0)To5!4nXUznCVm5W*w+fYQPf9k{ zX6{}x9`=Fpg!@bHTPK;1WM#6@5+}B6vyuNMVOzy!yO#RDEtXH&W&)G?^@PzkNf3xF z6EyvKO#Y4Fqcj)RyjXp51ErG3zFC{}Z%2T@gfa^5+^9;PNF^M=0V}JbvHlvz;1!ko zLw5SM`3h`8LRp~Z@r7gcCGe>&ZhfhEDGLP;)P2aiswn=7xDKv87@W~F<87=zZ-d23A zvoYIUHraO1!Fd$RD!^!Gs#%oA2Oq$kC@Gll@3*V1!potKaY+;2tw|aqXq5b>GDi5* z2gUWxIRPXXD9s5&0a)3J?v*&o!X`E!(^7eLMep%Ov0l*@LGR8;ktNv@ry#E9eiuxD z;k`=~z}CwFEL`v%1WU5{*V{xO)P6_*e`6m*0tF$(`Zf%q)g8f|1H=eA+*Qti&Zw7S z5L>&{gFwDo!v#l)F^gL+*u{@Es{A15!z_@Fc7o3QCFRq?X1g*6M0F}pf*p;X@N>m{ zNr5DSUH#A9q}tRjVZa!}55F#WdgVf2?t^{<0^NDgr=;CAj*yBP!+CQgV;CVVD5IfU zTOs$Qw2^iVJ_zfQk=&1&*2L9of|a$@;7}wv1p!hLz9>{lb;;7(+2d`Bflb*^G7bsH zrA8EE=m%eWs0)7c_8^5rldnDzE#gYQobW*8;>|9N^XhD|7f70^{GlY1hKH;WoCZQ! zb~H2x3QdrID;w)LpLWLQ#3N_>JMCU`7nKmFGkfGcqq2sL(dUYgrO0##Da&(RrV~wy z%2{ltxdu1|HZvOv;z1L@2Mp-E06>=i>Gnm!(+*xO^+JY1fA#`mAfqHa{WS;WJ`0ad zJRPqAO^bS71#>^JKy$Z19rT51*_dXLH*z=*SsxS$#+oSO`yQb>M4-%MN+e|r<1Mkv z`i>_}NxeTPx?RMOiB7S}Qh7>Uv?r;Ij{SS~N>MS+;@bxZT(49}HuP1J~} z9wSUgTYaZyfjU}`*k5B&dLBDLSs5#8YtW9i#Y;cCWt__<8yp5t~})f z=Lm6AP#7scA;H-^aqeP;Fy$TpJE-)fN}Ty2oT@$N6A=MX&bp7V#yZ&)`gvvwdLRGI zN)is}jYCT(FF)Nq=M;8kl_4bS#mKqVtiY)C9VnBBb}UnEq@z@D*ENJMr+$^!c=4Pf z^0D&4X@Wu8F;h=q>Ptp{bwch84=e&sW88lLo;p*Zr_#}iUxJana8FYg!_4Lk{^Bhg zn_o<^_InkaWoy}4ZkCqMcrxp@#-mdZX-W{r_a!n8=_VaAx)hKpLar%b0TR0D{DR4QXY%x<@M5<8@U;#KlB= zgn-n(O8n9pX?hWO#QON_KVaP=AGGxN&gK}mFXR7|d*Qg05(xX9%Ks0-oU6<Z_o$!!#vft0RqYy0t4DTu_3r{Y*VDZ$Z>A*(1chf5}glDjA@SA|wh z@7zdnJOBCi+*>@;g9@^XPtSYr%h9IvOrsL+T-i$EjC{oR>0kCocfUfh9SG69H9CeU z>51IiF#-_;YSM|%5Y#;VBaT=sGd?hKUn@&RA6sy3NJ#iuaRPfartXZOxi6tyFJNL; zqrGKtM3gTIdFhTgc{9WxP$UmWp#;N#oOAlq1W(xBwZ`MwelL2;$wq%IfNNkb4t=pp+}Q3v=%r$ZwHAJ&NiF9h}=$}Cg9N$7v88^&n0%3iH0RL5V+_|Qqm+oYwCQR zV}?tCAW_wyNC)O~ckGft!PD?O@-PULd9GkcZwS`$e+NgWj7&_^Tff!pl56Gbv`o5t z%7~r(4th)v581dBKQ|PboZ0hy_WnKb-L6oE$U+XlhrjU?4)?+;Pa44xVagk}e~;Hl z?;U;l@~xyhVW0OAfbr3Ll7SQH0xbVw2|=>g30Lm&{r zwiw8^-BM@I@X@~*M_D|gb!woEo8VBX8*dK!wy+Z4)9KSXzx(8}J=Z(Yj00MQ>2lVt z=zjpd$7;UjO}eI*JeB%^Jn@9lspG%oj)tppV|63-H}$hdJMlFr@nbaZr-m-r0WlHS z`LR%}2U7xL!$M<>oju~VWBmRvm+iYm2}F@FHA?lP!`pBWvkk3zE^Q+vSrWpm>a8b} z$UaG#K|l~znn0M&RPUwIw*n%5lViDCTz?8}-LWySnw3ArCu8`7{9e!Ycb2u-_@49Y ze*o|Qd@Ox9oFKY2jz9ObyXPHJOiO5=@5N*>+3u&M93HS!uixelwBK3o{7t)%d|>Xu z+@rTB$QHSx!8MlM&3&+6`ziRs_r71?u>=pscAkTyLCg$N!xn67ySvY(o*c?tky#%5 z=dgy7mpZq!I51kDVu1*(!dTgh$O!AdyTQa>Kf6rus9$+c)F1^8c6=-dd3Ck>^S5TH zLH;S=BD|?}R?R)l_{0-^5u?=5q+rxAGRaJxA7E#j?aBC!{=*)gWiI{^6*k={q1^$# zUYK0p(DpB_7|xqN-&q`j65I+Mq2uXpX_C00_d8OWzVqd!sZWjb)I_WHCepZAi;*z4 zs6pX);+piAuPQbS5hQl>g$3t-oTMME+Hri_f{|Ab>rAt$Ls`hdYVa-pdJFJcdR{9I z-Q1rLN%W|9awgY$-L@_RwRtBcS+oi--rWPu`*qhv-r%dJKhRnzj=~VD9_QGN9fu60 zZ9rIT7W^Fk|9G@_B-G1ozcNMsRLihSUKw}b>6Q0O8|onHImvvJ&Q9AOr8WfH_GadQgKemf1)uZ@PPMp|?UdVE?<`#$bc^q00H;2?vYH6-o4wxX z$mbKx3s-z2l+ldov=~tPjyJ=M_s=M8$R1at2X6T^?miRBcEEa#CGPyhMjsdNwa~CQ zF@rxPDaN3iIWAQ}$>)a*^@VTHh?l`!I+JNt*9(;UM_jUQ;c?Q^FSVyM2#Dfb&X4=z zUiEEZ7nE?qv|n>Oz)VV@RQJ;-Y!!pDkuFfe%w#}@zqR^QyoDfs+- z=4F=Be-1u0Z~F%z3X7*o5y^{B87l@9E#mr5tGvgq!pD@q;KDULR;iB-Ub_u01v;fl zf-KI2qH&Ywv4Mue&V^LQ!a;a!HF1<2gr)%8PF7 z$w2B&@41C{WAj#`>f!UQ)7q3aM33uKhI`>E!dibRxvQMZX5H2)j^wIKay3~1UevFK zor2Ayx2cp&Nz_;^i>*2KvVHJoiypM~$eGAf-I^kc4wR+?)8czuJ`8pd4XwkS z_-KgVa1N6J-<0nfjHVG%Rv#()*cuyQLb3Q4fR9d*4g1)==i~8%r7P6bsP-URoGK_@ zL4iLLNky1Us=Q|loS;FRyHmV2IJbsW5~}d-!ahj?j08_E6BbFmDxm8v2lj%b(7}$d z+piyL zGoktMe^xJonpqkzsGP{&4sw#oDIf1qNh}OQ=k!1z044Ow?Q6!MwEIl{qlH>wYR8lJ zGn2$mynV$}xWdc+V!OQ0x)EnRVL4D!ffj4}O&OB!O2Y@FNf;}55_cy3E8yvKSlG4RJ`Zyllrm~z2dv83J;6tB zN=D{1F#SG#v;h_uR$yt-urlw@R;-RMlL+t3LW+&?O38X%Dw&*77!wfkmhcrdIcnIO zsP`icjM>9zD|o`-gXHrB4WXhE9hq)sO)uHa)w+e`IhLbSD`z2$ygVbl03;zt{ zJlB&3Vp1{c#E%^LEm$Wr#XznzrACGx3$-SP5dvQ&JmRnja|r2vu=}^sb%!HaC?G}>k+_yevd%lt-_C8)@w@Z2K-0?i$0yAwxFbM z#0RW%^gM^uX3v@Hi;VUzPcCtdy9Q5SjHY(B#zJvAUQ6a$ua(-(e+1kPV;{LL_`J<- zUg2D4I4c;_(XQ#PBqr@sy&DtvDrTWUDv8Xo!&#b}Rp(%bj{b>aNfv>R^Y1D#+ zZ?}p=it1TrbQcoUNFdeFnxtqP)rZHX@wOGXn^+(68ySic9m*MS9zp7 zYMvE!*_-3dNE;(Di4Z1n1HA~0=dF*$6Xr*L3WU0w@TLn_Y%!NL)m=UCCZ#4fXlM`- zlvHTe#cC={#i1$jj9w6p_>B-rrn_nT`_Qa@WjKTS-928oO2$oP(RUbY%7Zz{0wSPDaHgO@Jr%~=sBZ8~nx8bizU2)8K14wOI_VGj=CCtGi* z8mzTT_-n|}x`i3;O^(jC#?mPp)B|G`eyc;=dK7t+tl?kd;&7lp2B@jeJ1pFnhdL7* z@3^r`8SvM>rx#>_H6!y$n|ja36kcaqma+0bDD{uimh(vWiIA5IdRuLX8z2*(}kt|__c0|J^}`QxT4Q#f`hn{=xdO&hDP<5PI;8O z7{9VokvrC-E8yJN^3)O95Pyq^ZTub#NFOL+#kLcVYnHRj3uY}rN;+8wwxU~N@Tyi~ z=rWofnH-tNGNK-DNP6UEd#MMehos4xm2t<@7gfJD@`Di76oupK5C@1TQ+rS=ouCHf znk7NAjUtUxs3~O;P5CYL8X&n@tcJZYn~qABrZo;Ac9E@CoT2dwh+|*FBiBpkaF?{8 zpXJQ*W7KYR23Gd$5f)MfVz!5uYP4<&JBtE*5RDo(@!gTTNeU`lmbgg&)?oKsx2t#| zJyb0Xr8zs!n8O?U16KOYKpji1F4s4!RLHm;{`oNUk=~i{pVpX*_*WhLb9X~tb1lUkyfqr(u#s;qhmCovHDZmBb%)O zD!jjHl-%H2>0OGrDxR1UIQH+~Ph^ldMO!%N7@jfCr7XXB@w>r<^$cPuvz=n~Ttq+n zAFMuIPUgL8vR_bRQ82+R`}NNmq-af3B}9WGounk8s<@Ld&mLNWaSt#dO?_rwhK^s! zb225>W-Bn&`8sz5C9mDs8Sm1Y9Y*{L%AiLQRaNu=pW8J8V>`_ZLXMTFm-qunus%fF zyvcEa72?8?3nbCbr z$}uEhG02Z}LjL|qV9a-HXLFcHHJkmIbikF%s5HeEYgu{Tcb~jhiq_~CUfAHXU3;K! zJS8g?2L*S83)G+@S3Q?l@4W1@n?>LV9fa@GBd(>AVJ^!d;qkOTf>~2qErnV1#1)Lg z)Fr3Af5JKqz_)#c1QHj6rQt*on>l?iJteaQ%b~c}#dnXgk z`?Cv)@|I}lv*y0ksTl*8182y0o(I+9NBYC8ehH&SjXCLBMI~PQt%!M&2|RaQS5AlP z1pRd~L{O<4qkv&!X*EO%EN1ad3;X4@0a8TNKIbNcbTnyKUQIK{*>gndxn@@t-A;tO z22}|MRfcK3_!#3#kTCS^)!uBv_yD(~mp{{rm@Ti?U|_z8OCg(`I^=>>T3y1C=y`;* z;fSR7)V7k8g_TYK^B2=5WMlq&v*TH}iXtZ)`>#~HZXd)xc+Y$6HAm?n^{l8!RML8F zF&vG+w^PeRtuH}8V3Cwv2G$h(RxGkq`LD<$M&rd;BZF*sG&3LZy5#Bsjaafj`xFH{ zCs9HlivIq=Iqhf(|60B1sA?xf#<2$2G$yJ_H!?vDSk;yR{TLyBOG-$PUaVwn140pw zP|WS8@U90MWNj@WP!OcLd<+gaR5n#X8C$>|HM8#(F0JX1gm;Q1ba1{cqlA%?t{Aoi zpLn%~|BwBa}B${(>HqdsXk_#bl8X{{Ep+fdbynMaLZc5$f&dI+o#QTa*3UMpwR9b^ImU+Lyg$z58o49@tFb2Br^a!2 zKU6e@5gFUA^r=m%l$7XYU_WADy1aLYs+g@I|8XLbZ~R)LOpY`JD9~A`t~su$00SxE z8giqLxUB&|0S5b(^$1_4Q%m3(#a5uB;O39+a5rp$^^%j%bG{&BOL1OrrIEc$l}Qc` zZ9}!vEke?E+uU8S#V_qc^j;gxp>0lrp*uS9YFy@#?*^ic2y@lsa7}fpqI?s zN_6qpaV!D$R&Wtmr1Di30#Tp8jV>TIn zj^;Ei(C^**SAP@j$#B=VK4RbeplFoV{pCU}3w}+7O|H^l2(i`&PKupue(yM=k@~7r z6`v)a&%fc6RGj=vRl1ym3IzI%2t1rJt8`Q+IKtv|*yT_scBwSXUX2C1%Zgvz`v}o) z%ok3`O6O`<(+*@IYCg?0}|jJ4(n)ITKL8i1!i@c4-Cvu#yI%1U?lmgNj8clcJ8NvE^^ zYmILewos`(gL5%h8!<2{<+{ZV-OF4Y<6a;4r|??;4g5^8fJx)jRi860MDb? z)NaXZmEraLTf2X4f-zTL%l+?7sJ$ZQ>W#`j03*5obKl^8`|pPu;#r-MsmWH9w!Jf? z;F$_QDq-qoxzc7&%1GLzy49?UL&w~PCrj={+^Pt(a#R1D^L!PLta#g!ZOAu4;*VG) z(_hLt%k7zn3^l+8Y$?99!AdJt2aM`AmRxMIsw?iiZmQJesC}rfv^G*^p+afI9iZt0 zgGAvK6}={9h~=w9(v&ERo+Fm{yeVEkBKeuf9Vo@E{>!IXROC`eb4rF6Di98~`keEK zF&+}Z0mB4KHT8wVye+>dT;X!CK^D(4sgkSu#;W?g|N1(2`#1Ynu))p4fls*?en*cT zNGFgnx9TM}(%;AoiLFj46dRRh#MK$PG^8(#&EwkAC%Baqmt<;m;m)IMa7XCTh|kqg z2vhgMCLdoq4HLj0N?bd?RSTc_PHMHfQKtrbE9&V@uKF*F#*h1Pjvc=E|U_jroc>&+AZTJ{b;=CnuliB0Bd&rAG+!kzoQ5gK>vsTf0go4}%z5v=QeMg=XFJzQ zY8?L}#&!+!g?e3P>gA{nZ|6G1oxd~^<+dT-%YE+d-Bu8r?6ogb^3APruKRK4 zvJ?9W_38&-(;wu4_qC=|vp$=9=KVbUMRbS}>#*^c8`wq@S;^*t7EU!oWzS&~Fh zbxE%0=1liW_+PAKwVUxT)AvNF6Qz~ZqI?n@MP$5f#x2+@n(DSAr(OM5{*w5My9JN) z?3%UD3OY3zfPcBKbT|XeJCajC0h0ue64`v39Q5s;eJI#@`PBr_wO{iIveA<5B=i;Y zBbyxPPm*QHM>OW`S(|VNd}b$U5`2`Ul8w8Y=$fK=NT^~l2?Ag*LcTabM7r{z+4iXvyXW#1fb3X)sqoCwo zFFg-nA16DSC9}me=$o^*oZ+Xu6gg-$s~vr=MpTh8giYlueGDS`>akKUWe7ekF__Qw z)|PhQ!n0?~MYP$mO9ZshcruFe*F|{j&ZHcL)0K77SL8V$h*?Yvp3CMp0k>+F)|~Q0 z9HqnK1_LJCiS(Wmp*}BIMp7SHMR%1;U8`xz7X)NRma=9zBI2^*LCS?lIXcg8@|d>X zF-em@`GvaclF5wgEMU$2M*E&vYG!W?^G21%=HHAL)@CBwh(tK0@)EGJ4tUoPCAw8p z0RlIsw2+Up*~W6z$aML`k?C7i;*esj@n6c31X`x~!xI9W7vbaH7qhsXV zv5A;Bmi*;tTcBr@uVCkVU&aZYm7cr)(W4#4TA}U9XkKi!=5yTOQ}uD*xG5ICZJ%PB zx+r=3m#ytOviVDKPou>Q*_&W(Ol z-|*8&BEZAV`Z1|KzgVlpGk2avPIK8VW$i%_aI_x5K={ z**vOZf_cRwEga@L>(@zY26cw*sn`Ye1%(Z9iH`iVZS#m1+IgbRD?+sQqm&pdeU)Gk z+lw{OHghxMY|d1&V?m(0Pjnx$s>NB%lzD8(1 z68%A~DQaF;t#tz~*k9bEeQ?7F}= zwU znzHU2FH=Q@1qd4_S-U9Gi=t5H>8Dm0PV4S9z;ajfh2=35^}Elr2Qhn>u|0@(Ql_-< zBI1)UlnhZy1d=om6Z7Q3)Zl=7F(oksN<3k~$}EJhjJtpF|9JZwzRQ9yyGG5}GO65M zyIj1`xrkQK=eA5oYTuGN7O$296%t``|&?)(aG~8v45}_S87j^mTr9#h1 z*aZa91sS4L=Bu|q{12AkxD-`U&pHmpEslzjq6!wu1ymqi->;u0Bv1YUSd5yPvN`4z zJ|?4I^`4ify9;LeFf=I3-ELD^)Fek(GvvIC{9oPPye;gS#B>r4|V=h9XI8JJ7VM4gz^ z&mdxlJ1&=3EqKNk9ZxIM+Xf2n68L(m5lYQqLZ2{Jg9e^3g@fZDwM*TZhVehyleo=!*l78zdC0(kJk*X3+^8ItS_kPoVdm9vfCA=XM{%G-R zf7~oLP+43G${CfLDziLJV z{f$dzvx`z9y9)hT@}1`E?pIZ)k+i89N^S?8pX#r~*$9~m98aFL6~uh!w|0HCzN;!Q z^2JU3*rNNDzQid1l+9p5)s%wt%9LF}g?=fAMwQD5HqOxU^H;I&R~|+)noX4Z7i3Y) znk_rk38I82sS$3S3f``F?oVn-?m1^eNfxMMe4(A1Yh&rkyv1?Vd@r3RU-KuMmZ0p` zV%>ZN5;>2BU(c$~m#9BTgIT`Xa?wQ)NJr7|plSPAe0<0}86q3^tVVjihV(c3SIBTN zJn`{sWqvAbAE;M1)K$=^Z3b4xZpEY3Fx91PNe`GXHX9*QRxIO@d*YxS`*#%TL-mb9 zHg}?5n!)ogrjn9hnMl20!h`V{;hB8Xu@Bdj({Uj=vxgc!fUT*GalA6T*X%Q)Il6aU zbfT0_R>1b4AQHD?`F-T}1ZcR?-)>21wWh-=K>$m+?83t?Mw^JVmcVM#DQ1lyhj@{koNJtC+5^ox61YtUN(6BIMUO zjcrAaEk*jC3x-`UNH}yBy3XhXBU|(3kyJ*gQQo}CbQdPZnekhKGd=skL=Gyyi{oL< z2-3%5u3@xCyu}j+&|G3DED8WX1U~D~4Ql)TtXs59G0BGeF^|0~C?M~%hJcYPog$k( z4IvyZ5AG%1Z^4ZEknT+}cH82#pmu%V!1BT_F~H2a?*Y@s?{710K%}hSOIsJPCdbks~yl+9RwP+(ymR^lW zzw_Z%Y#Y!K#Rd8j3K|N-{s(Vo6%<$4b!ps#ySuwsjF+4-cWI81_4cw1|J>K{x0gbowx( zloFiPDgJPRx^k!5R#4`IlXR(b)|=J&)WuOgr=tSVNoezuBh{DapecTcDJd;ut1 zDIJD`UO4K0Hy#DHA&kwa=a@IQ)#HG(s)|*n+>W`CN(fY#dL*}*K>g2TDl+0)>00Ai zP9LOX*_+*|Wfcwsj&pzI8W41m3%j6gahmfg!386_*}d3p`Sw)Rp66QD*K&BgJB_TK z*Np+~G5;*%&&>8k}qnU?L$i{jj%An1VR=^c)GzMxw)PPL{n4jbu-Ky5IS*QeH z#qf8HJ|+e(o8iQmFI7Tml0cR*E9_zJBaUNPm22A1J~*8Bh(Y+46AY;oZKD|*-cn@W zO9mywa%wwLCBJxe;Q3kVlVzs)hsZ?6ZjE!YjM=!nuB*daB7z!muG>WOzWn^dNlLp(#QXq_iRuwi6|KZamqMc?eM-x^EY zYx7~jtxM6ioF{PsfB_@7)au&i)b>843|=H#sADy4WJp*|uxniR0ModIuc%hb=@mJ$ zSNK=31^pT}sVpSa5B)1a#40SzinJf!7G1egMQ#WMas*8ip4&Ic4C;@jSC~iDV80rq zg)v_5OmxIWJ2tmER>)DnMW{(J(KKW~gp1WTI3F z=~R)vd9BMO4=R=?8x$|IJ>3NUu^#5ljQQ*MQ~eh^Nu`g_d)`pfw7#tZhow`THyZ0Q zmujoEZ@eS^`KECfPHYpjl+o-cwX-r9Yw8o|AL?ZPgQ34R+g5WQCOJPyqG$BT5V2^l z>snr4;!tS?BDy@jnYE$hQ#o82{9+ULJJ zX&1PinLw6bRZVBR9$rqQL_W=0tgqqxWXh+GJc zOj(O3N(3J7L)bCPa_Oy?r0S&^kM*^ALu23}^eIyxFFzbhUlWSJ^_jwD`WKA0+IM9e zwXqHDv#lSHF#p78l96Quq5xrUA0)@oX}wzR zM9cv#R;8StG7y2HC3Jc+0>q6%`Tus}wEMfvw$uus?hf%^*DBBKZxem6TkVm-=CvMJ zl?+yU>kT{{B=ESg9YWUDTLwzw<(jtR9qMibTU{pnKqlUpDjEC{6xH|}zUOeQZ^;ac z&NW%AYLS!&6J^r<^s9dT;*&W`giV-eXz6@01P_afKZzm2G!r!DcHbKOy|(V|Vdiai zL|oHbk2=mTElaf0v-D})7DX>c6!Yb9X;#GgvEhT$B5>ex4q7kYQ5HBabg%Dl?!H|o z(&%+Am19iGqgTC8^o4hu()n#{5_j<|`c^Aq zUaogUsjU1{24?^d-Nb1HKMkxCne|l~Ap!XcFUnsWTnM{JrLMI2EAXH>kh*%@VDsB& zo1{7`)m#?U&@UGL`0J@4GuGu4Wie1QKQ3>HCu0l%o5sBNtIzr$e#dT|#(Lpe)h({o zE(DNnp>_lG9^yD?k?SW9RqJaPu0(5CJ<`ObDEUzE5oCmTKhLz~BDeWqCYv9FvsJQ- zbZO6|uBr=i7$@^QKGn#QDweT~HDT-%I437-j*I(cg~WR<;on4;ypFCCLBC z&8|;=Rl&zo_jaaC_g^xyfrg)F|EM?|%i67??=6>F@T2k&K)6`3HbmI64W!x0Kl4W_ z+51_0aLpgRpB8+Sav~=~98;E>agKi~1`a(GmHQ=cH-3&@GZ~>cP+5DXJE~RY%E@zU^^dyJ9 z9xUs+6LAo89ZXXXO$sZ1^$JPy*^R^_`uIh_MshUs58o!HLWhD&q2omDmEvWXb+nf0 zNz%c-YBeK$<0EijY9oUK5eL}0*p#RV*1kvADfBXis-&!aJ^j80t5voVn?iWsMf80a zHutmSMs!GrgiP#g@G6ka<3sdFlTDOK$;t+<_&6wXe0Hq!?mxH+f-Li8DH$#L)mvPA z8FchO^Lu6@N_^Rn=x(p+Kp-u+^JkIqy^AcHU5@R8JN%oP3u*0_L6S`D0`);dQ!8Yn z@)|5iJ!ALO<4<+EavX*b0Ua@9J89b^AT|A!?l|c&>99d|YQyZ11U2Iwt0T$TfsK_( z1)MmfsMC?Y$Z5?o$~~-g6u^_zXzTnHSre5T?5m_ly{d1z;c~sfbVSk3ugq0$oK~P& zwz&(1H3<*Bec8a2nt6ezn^>T-X8+;b}4YR!!7xr{ZL zi6*RoWW@-PDHw`xM6UjY-J)dp-PtVt7ic!kgnNbnqdUfIP-OIk$d*tsr&mHTqTIlT z!Y7#|oJ&NVZ`(a)dzcWQ37?E-KEosh=^`|nY1eZacNyr?p>F5xZYlXpXtH9= z_y`dyMdZ`Akz*{FUl(=xJyARmh(vG2c|75 zbrUB33uJ?-C;9Tp9XH&`zS*{?$tL{R4wGUnX65!>SG4!95{#QR>U?6Bu@q)DqDOR$ zH_drRaT%zFa%-Bpf@>Oopj#{5N1b2UZr-Wvq1>H_dszTiti8>m3V%!fyhHY8P)uX^ zX20<-@95ZY_oPV0-0)HwA`fag0lT=k4xIpwp{dQSQ3o*31B|VaOn|w!Xz4+G?H;lJ zejR8#74KhCMyEa9WjUgxSBpYbaJt)s4s<}JnQdA_Qfmi zAj*~_{ihAMgAb3iWoyB4V9#8wRcws-?6ORJ;8YNNExmx=VxFUBD56PTRmB+cx2MPr z7Z(myKxK07XQVK)t8KS`r{s-l%2I)bog|lSPoATs>4=!+@OXaBTUu66+XlXUY5SCa zLUEj)p4jF%3Imr?Ebs^%y`<6I1#IReC-5j}Uvwol>`iwfv+SG{UG@(4nz>z7RNYtq zbe-xS3vBgx)}q5I z24~XJze2)2`Kc{qfXehca^i=TzQ2HONJy2E-G`TvoOwF&3(xI1Tq1qpKL88C%>LYShkr^%&RxF;Y%Ws=FVn$5@Z?BC)jObCBwv^Ij0;q0S&6&reWwNhl&RjD3_Ix7zsY z8HM?44L_^cjE=0GLzV@o=ix;Q_Eu2&o2)LabikLTYkLS0(ie$B5(*{T+GvlJil7~D zp1^Z?>Zr72wu&njzXa)#w0rD&DO1jzN%Ol)N!1-K9l@bmLwOJG3mjyUiGk}r8d*Kk zOsd<>UP(nqrLB`8QBX}`gf=>_L>vDjp#`L8buuPfoNNny?EBvItsEU(|Pp?xOW zj1O0@yKBlcm8sJcqQ8!L?rfC?K^Qq-x$Q;Myl%fl=r{;GCK9LoNWB1LRZ|$nbE9~A zm(2jrnN6hRkcL1mVxKJ6w5xzty9NviO1_A?U5D>Z+FKkDn&>G_Z^=bP#K z>L|!Hl=bup_kty|n3KD0^6IZV;yT0Hl3kf&$rI8?5>N^a!zLZ;p10cZA?h+$?WGf)7Xl;bcst`_dU8z zApv98R}@;dDxT#CL;_V9@ggmIYANo0iecynWsa6bq$13CY}vl+>l@$bPkk9fx@h|T z+BarHgfKup4qq_4{7t6nhbjW?NLLae@0TE!E8WqATfO3fq(LjEUgQ4~0H}u!>)Y3IN)8RmFHj8KQCAa$Ahv_=T5P=fyS)&v%h1y*( z=D2k5w`R*D0QPsys|=s-Z^MEHv{`7)2F%N=U=Nlm?ERdfBM|+JF?t)ht9fR%;Fe1z zJsF*vw3sIa9Y;bVzB9A(oNnN<40&An_Il}3V6yC^42SBvtgm;bk~M{ZiS0}-dv3&x zQI(e-xRVJH#{@C;f29TqPp*$hPj}Ib?zyL#?ugUTNYXfO)E{JwCTFxY!FVv&hQ}(x zvaQCDPjGB!ioRgs@*v78&6 zi?X5kjdM+w;N`9-$>Hy>Hrs`~1B0ONE4~CVdzBaW$za%}fLhg3k`(sPGiJ^I;Ey6t z-DB>zFHav|dw;cdK4VWE`nq|zX`|4v`8RV97?898^1s-SV0;OA2p7N25fBXynU|)< zKCSvl5%MnJV)UD4`*-OVwBx?vq2JqfH@<(-bWC|iK6_K0*se+cy>7aXs#B-*lqFr3 zKW+)Ejv~7mwq|*uYjCx5C33#XuU~lHO#hPU&L$+px(Ct*=RmTiigwqD2Xcu=SfT_I z6P*#|IvH4%@XNV~MMptRGnKJ-y7CnVsyk9La;JKn_(pmApiB?fqEsBYp)_i762-6K z!PP`6C1?ah=xJ~En>Ic)Ejly%JayZR{sxt*A?8iH>nOIhmt4uPc<9B^jUx&aKp2)} z+Yni8?0pdp@jkl~Bw=L@pBMK7Oc2&ij+JBkW**-WKR4j{sspIirX)90qo^n-`k*h*%ks)jmxCQ8q==RSV3Ad4_HicS@D7{e(Y621 z(_J7eUd?51WXy@Nj|V}(Mjz|}$-&}lfq|l-fl@3TEkxOrX3kH{s~L94S^iu(O})a9 zZ6<4aOioJioUpbTggdaRm1n)bd^GupP}~T7h|Ar}b}BM(Ll-!r?dE&BHq|EG+M;`f zc803zZ=u{_lH|5)D#ZQKQh`B5rs&oq&kHx&w;9pMK9~9VY17HuEykfDTSFq{n~NEp zVScnW3@l%?l3r}y0BP1J9+av<`~T*daDJBPRwZba*eWB@ttfeS{VggfZ1sdmU!2Rd zje$~xHy4p=UiH3F>9U^-Lp&gLz`vg*{FSy5wEo*xl^#**jGIw#OoJ$38;yS;MGoKm zFf}EFnkY-EHil6oj+U;I>QjX3S8LV$9Gb$9!|de?stIL|%P)9yZeKyXDClV^jhiOz zfxpN7Sx4cSET!q(Bne}eCrq{?cDx(aHe9stkZ5*h4A?!Cr_@z?M9sBgW=z;26R65C zjNGTNyVE=Eo(zGex0Pg9ul|L9C|Bc$-i8j?R4Y8daxqgc(ga(?p!;>V%cTmcA)RqL z!Mpi*`GEzwfuAr{T77Qy6f4nw!58e>bf4tb4tyjVzW?xhsKqkAsPjYCX~F^qhf?xa#6OsJ%7MO>ng8jh zP=4l<%~wgQW7vZAmA5Qio}lRwkzq>Pw9t`^s#vid8F%f`Uv+N>4|?89;KNa)!m26) zS>obhZEpTS|6Jzf~cMx;7XGP#iG1!~h9!b%vQe z@1mU1>4faF*`FX82Q1oHA^iP%-2A9H&Q~tQ%P_~dMf;Ob)dL`fsEq=H8ejh_Fr>OL z4fpm2{eAOu)1RK%Tf_&p*4e#M(1ANyYU7AdTIzR>h-&rM>+N*&4kMG{yUHUvnax4Zj zrv@UC@-&&E5Q1+I+bbg+(FF5BqFh3eaUBDndr+*4shr=s;&T+)B;W)fRnzL0mEASs zly)j2bxz&sNLxJS()D@h@Itk?2`m~jNFr_XGfV59#uIp znA!3hxW1fAv*G~!o5My|ntd_vmXQ(Ub6ysDTAAE1|8Nfpz3@j6k=~6~528nwrkoKR zy#BKhmf4;Yo$QD^3qD=g{uq2_G3BqJcX77duE656CO2ct1@mD;u0G0q0~p>&NM*&dIVFW7?dlK7q5~ z{OA+#?E{v3JS??3m2{FojZGiD(cFz0n|A4GqPRq2tb)@J1hBCW5H1_G8WW2zay?=i z8t}5LXq!NEq`aTCs~Qu7hxOxgjo^rHhKOFfX~ zGV-hdSK6iyLt^o&xk2MGB06Pp38s6r(T&5^yS3CQ42g|Ql)0G@;-amv$Wc64gk3Ja zFNO@Z`jdXYW_fGmqLK$hDvTrC;^eI(ovXvNS@#C!SA^iK5_cG;wC0J5cD#fr@6rlP z`;_(REVpXeYK$sdSZJ6*xHVI5xBwY}#CYnEa0G1F-+pUUi@M8wp%gdq7p||%L_3VT zjOE<-B-X(R^u^5W6JIH^J?Ky}%I7v0Vw48*fr8z~meGm~^Xg8=`+VsI>flju=BF}z zv^#ZEDXUIDG12rtUsSCdSY7DofZX)iIEtZ7`3fqJ6r7!vF4<4hskV&~Qm!~$2(@wU zBxA!WfT>1cYSaaVrHv3yQ~V;&*j1N5VQg7$S(2NUH&AtWgTmKC*WhxNCavkNt-AqQ z-?!EdUB3xel>LKY)mXjc#XSMa<%lOV6mU-m|3Gv$2M051szUvzPb)0$Gp#}L5IJ=< zIj+pcBbrvJ$!lv9)?-~CIFmO$b<#l|B#x2=pR9_UW1mjBWtl9pk5*x4(5B8Uw!)S4 znhKfK^c(*= z5t!;S%;Tl43|#bJ5L*>cNV=vclH=hn!b;UwM&OO)`0DHpEV&N+mKU3eQ3vHC(k)-C zp!%>?uh7T!D6n)#`Q;AY2SS|ZeY+u^UaQ8zXxSJF?VASK+|`td)ednL*81oIwmm}h zshY96%Oat`@Q5EJ*xRu7FFAKDEu*f7{|Sqk{O-8- zvCJJgcEvZDc-=UY9jRnDN99NWJrI;KdZ&7X7wqd%=AUao8Rqi#Cf(6~y#Iz0At=H=)o7@r8V5oestwAL)T&r8-h-^jS>j zg+MK6ovnxj8fzdPmr{0vmB9tFfJ>)OdBaR^Cx`X=B0uh>g!xRwxCHCNtE$H(uiIf2 zqj$r#zf;5ren~lkLVNm@-DoCM>YQs?{0Cyfy=*-P8H2W|bnJ{P7+kFCv)wPm>nE}W zbmIM}8C8dYr6CtZ7UCr30dvTOkEHpKRA>F#cmyjR1bH9^TuLNaZHa}J3Q~iUf zhsHN#2~+(gh{c+draE!2EV&IifTB4VenAgd1m@-{F&wI}>O_ov88RGf7?8{EL)o9h zWol3(qHQD`+<2b$6=z{-mD}-!-l5y{eE4}qLd2YSzyRW|vN;sMMQs^nPfc?Gi}lUG zntextN?c2!HCJ4?ix^6(0s+}d*o5d$fuL1tRKTvg+<)-=?56VqOs;c{4G|JJ=Gzms z?)p0Wc&ei2`CwGqGERRne|*jAA~+ZZ+Cxh6wQJ8iU6D$wl7_N8602lx?yUHqWu%Ou z^h^HOfUl|{Yg)lyjC~x2RVW76KXSxDtobl&6zi>MxYz#H~Z!7za9OL^um5B(xtR8l@$0 zGrJX(nn2xQ$mHdCMNO8LNq>eopIdEs*w^z8^3u6JC!24X#)k~k3GLf&g6KFX<%^`5@{ zsR1hZp4F?XGa(%u1~fkF;#2wGG#|JS{0=VF6zcDK6=1B! zpFykBsJu4fTX9^jQhbo^Lh(cU!@lr=aRdS0oD~;^#~+~BPAlx z1-qC(keZq+t2W0`#1@38Gw-&porbc|0j_+{P!c;LD;F`=Sq)*J__u?t zzPDdNr)~XW4{RNhd!>#CL>qTH6QEkV%o-nwKGWLzt&$1e_EBnaD+Ys{JhwyT6!%ko z%i_3qZyg%4;w2A@c}i`AHemDBxNC-|qX?rYrlDnesAsDOy$`~7`2iX%@v#R|K;OCY zLv&7fm%rdN+C0H#{W>_3%~RMZv9bQP=fEHC_v6|(2bFR#l_z1Wc4oO=kx$g>o-L7& z4VoKnR#8R~G)Kl^n2q4=hiAd{5zlpCh0JF5BCq_xU4_=j;$O#18L#hbMyMi@a}f3Z zkFJz2n_4{}16B@lqzL>3$`AiXnS@hc_uOV&V@i)=*0M&k1<0Qt?@J4))W~L&BZS8$ zXUXh{TLo5Lte!) zDm?cIoggb~Qw@D}PI7=jnM$MgZL<*ns&r+)c~vm7>{Y3PsXPI zR790kpU@0Kk!_)Lx&MOpHWEaeVv5n=(j^#?J|;Zq%16kqK*pYf&PB$=;~8E1v);{t zoiuEO=Q=`|fU^W(txF+*&Ev}v!{AHSe$}DH^B34WWYI~;-fiU8BGxUuU@&Ox?En4ZftNNOC*f>70f~MYYh<8XzSEH%%! z8Mu9U_G26JOT-b5Ca0#=P>$Rpm!hMo-&%Rd@t7r#v=1EcXjhFNp+v6dbdv*}ApK3@eF@60PlO?hXvfbfMAP(9VyeFi|tQ8PLJbd&FSkf^aS3#+^o5 zb21j)365*hz#Y7c7&a=8Ew2Oe6MS< zncwW8mdd%1(_3}(>@Vxl0*dj#qe6eZU9mPYsEVAIoWK*`=bqI>xK6^)@{%(!Jzd`C zRXqvUi9cOI0f*z11`vtb9frqO(A?M-*(i`5e+$|WXvSxK-}(oGQ-7CmKAlOneniJ$ zKRWjPj*fbC8e46jQXs+ly+a2X!tS+`}d8%Xwj04#Fc} z6#1&(%DE#fWe^qSMv97XLe32zmSz)^ah2A@f(}BSw*c?NEDAvPp2ZjuP|-lX#XhFo zK&Gmw-AIde85t8KM<6(RT~%tJo!7Nd28V;~JcT`352mWI=Mq%MX3B1_I`Y4Vd2~Z@?|}1W8fi`D zXzybf3jPD9xBIoS&)u_P3e}3!&6S`<>8%<%i@2mPk*}#?(SQl&{ZGxbQ%Aa}@V=?& z(FiDVrqr>Xf7XW&YBnJQ4QrZ!39qBLvZ0pP2mc?_6IWewscIAGX?C6_ zU-;N0BAkhKOLx9LCwf~?+Fu;6C8M ztfrH$`on8kNd^T~6$pcyg{(BkHLdnd4H`=WV_I+9D5cT%qMIBFgO|14#6 z#U2_7Dwz(i6)3fdOoRrDJ436s<4!;3^T=t>mVw9{fZV3d)#F+sH0@KWk6y-|SBv{! zs~T?F3YvsJv{?XgT9oJ|5JZDuuFT%*!$NTq&((+-2JR;{RqX;28Ih!7(=)pOhET7k zQR3|EZ`riS(64)>yqp)&(va#E$hmVn&O8S`E5}LkOy~(?BgjewHj<6eaoC%VSaHLs z#f{VCqfatP(F8|Vn-nAOu@o8gp3W_P#lD4#X|qcvuK-kWPp@8cQfw;Qs)eS6}-XcM{-EKUZ`c;XM-`Z)am>9fDIN(A1r(5CKD5H z6VS>$9yC^rE}X1>TWy@I17Fc(;k4Ph&TVT1m3~T80(TUrbViaR$j3H@t%#`BjlMQDNXGuo zqZ!F6M}wL3Lz}3~k{FN2Gs59HZm-H%DZS)ZJ!t$pt?~*zWJdlaF5xzSBDn@M36rA? zJDk0*MyHsm1G{Jk$>y|QhohG?<@12JDGpYpVTcB?#VcGqM6soD<#T`hpjK}?edHBf zL%{a|mOh)+?{+O$hH8NX9Hz%HG%RwEnhca=p@b=iOA2R(;&kxCX=I^xp-U)OXJ{l@ z+<~IwYTlt%@2i&MK@EoaO5X|jxMO0u6}y$u z7l(EO>o{;M@rWE;GT|5_TxldYgIA5XS*pTdGF=NCjR^^-p0rWXN)eb+&@xo-3BKh` z2)<l+B`Y zY;nTWj4V1{P!onBJEl3ar0vouWfplUsBgh`&A=fu=8&Pc+%i_LfkeWvNuGfRk&m*^ zP82hzHf5YleIW}uhVifaCbeKW7*Y548$V90xmupQL){=+dJS2yBAU#H6x5P%Vf>zY z%RJ8NuX-U3%N`*!h;PP&l@!CjW24$gaR4*oKcpz9rI14!S4DK~J3c|m3gbd0D(+-M z{sVo&frY)q#(skhW3ztkwTO|f@$2li6GMCB<&(mev#^v!ss9G!v`wBb2M>hHDxWKU zj1Ka)yVSY=2b1q^>p4>R@gK}bQ^(VQheNUtH?wAfl#w0B_pXIq4L%Q1VxzJ(%d$0# z8gWh?2Fnp&&HbOwyg? zb-PQy8RGZO<;r!rgaAe*hE;NMy?~i)%eoJYJyT@?p`*Se)O!2Y%FQk6F~bIYLiHzC zZDy2n9vuEeJfmJIAIdChy~pt?{A1!fVPDjL%5$73(HKAuDM5Hj1B^-wD4a>W6Gr4C zZ7M|ya37O_d=j~2y3?$v#3jEUNlwvK3W5vM2G*LXq1JD7q$11RsbQ}o&;j&v5`J|9 zX8L)|{appPZoSUn*kDeZF)kuwk$2EGn&@%g*8)gZYGjS3b+_b9Ra*ibXi&&p_J-9)Q3sHk+o~g`n^%F-5?v7?NfRm%#>B(_3#S>c_IC&}_+v*XcLhphA zGYoYMG)F;Z_MR-Fn|mf`MR1vXzm9P=hLN!0A-n>gsy|sxyeEGXxve@9K3cD^!Ekr} zfQMVAea%OG!0sAri(M(Bxh7-Px=>>0Ch)9YATQRYu2Nsp)MQQMNVC()G1!&di3li@ z>Ncmy`a67+LzT5r&(~GpywD=)SLfj)3;F7N`>l#N~wEVVtlK^|MjD*koNG51{F!J?~Wiepdm=C zP$=k6_ru`gjlw(ZFt+~S%8tOmbMN!9>HA+*h4Pit_xs9a38a58$2VfHSl*CL@x|>w zJ0}|QcA7c&zCmYPa~BVyCg7O7yv@zX#WbIte=rX_Z&Z?2C$q@^U`DX=gH(FG-wFT0 zTpNDh6pJi1dz&@u{s(jVN%G6yKbTJ6CoccR-i0PL$&>q&<_@o3w`M@S*?r=)T?w0~ zucXLJSLqqC_opv`UN5io`+ZThr%OT-k&r8c;T3bpju(&SsQePDbToQe7lxsE+`&jw(B>HD|*s42^nBgXtch5niOd7dapQJf~ zb5f%Dp|l$;VDRs}*I)753iGFrE0l>=ah}-k{l?v~y^d>(PCe2!u?sCgVmpdGokFt4zV^aNrma-bH9%9g9ASB+wsnE z;)&<+%|DpTf~-$pPXECa374P~f7&>1H+|mzGr7|@9`(?CeI_pEv*!aw$ zjz{&LFBVqb=3;r(!a4Z3CmUy1NCA(Dvg$&I3OFK5F5UWi?%vxyP zq^gM7$a6i03sk&0`07CA^wrRJX zo1T}s#?Oll1u?n@4*y`p|G`KU#JvhYi;a?}I`lBCmhe*C zPfHwhKLor+emXk4Z(Ge1cIF}#wZ|f@c+BdM)`8s+P_?AL`UZhrQXHWPh7f5%vzxHY zZ#&e6uSM0A|E@V@=Ki%9b}3uHXM88ESXfYDR_$k__eueU*QO%q4>YUEJkloxFksrr z@n#o#kbJOWahd8s8}(0iSp^OH*)Gh+*&9xI#G>SO41Hs>AEDi)zd_2WPx5esDs$_U(h6xpQ zlU6b>_tR5R=T{isBU|0mm{2SF6qaZ$_P-y0`2CGjx7jbp$Yt%kzI8kBQ@i40_{Aecl*YaV9c-4J2`wP*- z$Im^!X57y+_b2$B`GJW_A2UH3NJ`H(fx{ZAy>5-RNA8uguCtk?+xG8Cm$vKv7snfO z&szsmfBlZHw}Ui#9=y(5&mHAccxE=gC7B-~*P8K~=r)juJ63TnZzw3Ds^?Vam5VxJ zL)ls|Qc|)}wQ^ngt(I3O19EbLVTR+9HtvaF-L)0=ek5gUf*slQeFjtfBbt>@*i{s6 zu)MpzNMd0u(XExO-N2^2&+LC<46za!Wd;MkU^FLdSrT?Y8#Q1il*Ql$$e=ui#PagwEXA7|Y!NgIC{>==yc;)y9v-#br-4{!~@4284TD0dL0^hPc z6sCw~Z)S!uNt2IV^EGn%xcy58@U}hg6H7DHAD0%(=TGS-pG{=H%#wZG6e-=~!iE`PKb>Aa$H z=l9AAC0Ws1FRSCRvESP6C$uwSs-qWFrCH3zxEY5tNBXbKOVfC`G8Kh360ZxsoY7^9jg;G*5 zvUvb0m4Y^$jyS`FlcT$LtwhyIpgFI@FYw2K^jGJhvWaA`C@O`zFMxt-dWtp->nmaQ$u=xpcxj&;9Un4w7rlxh z7}M1H)q4Q$R#Yic4+;$2%hDi2wCFSPt>$NG;i@5vG>X1Ju8q*m>gSftbowzZ!OZ7C zT{9zCxU_pUOfxUY$Jn1pxa@&O_ z(Rk$uL+!Q`TJ6^N0bXl!b{p;v^CMf0ob(jSKDFYhZ}3_b{#WB0=Od$T+p#Omzn_V4 zS9<0PBq~=)VjzIlCiW4&p8UhoXcjwkLXmcFldRxE7BspM*!9Fq)0g6x_|E+<=$&@f zuJUhM!1_MPCg}J^_8lku+T?lm%RiX969o#&MA>B1N(0ej=~2NhMACcBbSDwLHjqt0 z|C%|&dSQM`xK4}vq5(8XCXxq}G&ov869+b?_F4h%Ky6wy8Z2Mx8`UJU-a#<)X9qBY@Y&akG6fD|hCKooYogA7>&Sx)-Ryc4YQpvgPXsFaATfovYVsVth5iE6UQdvqlO{hvvqZg#Gtlm>ey{HalhX(9<6 z7|jugA_>*{p%WCc6fFu}cTFQ}%*16mDVt?wbzb%16>znmx(vmH7*#PCa8wz70HE)v z04%#`_TOc6)C9-G<<#;$RTuKRuDFYom^MQBe1p_yE;D7N7)DX^zo7fD5)dJslyl4k zy_FS)c|cH#n^E>jh7fTgqaJ26@r%C|ww;aR)6a=1hIJ7yoyB9eNc9~2VfZi}?mUgH z!=CLq&daSWnU}4M6KlHNuDOeOqxt1ibz^9T+f-q`fY6WPKm_*+Sx>EDVw#p*mN1Jc zodsZ;T#6QtR!4Ml-M)}Ruz_?;K~Vtw686N2>ob1Oxj4 zFRYf3?No5JW)_kg1$e#>OmF$hxPRacrhgEQtvQGiPF=Z zBWbP_CPKvr1Y~L0zdUX;Y}+TD3C*@&LhZc+f)otXWe3+QdIv~zi8f0}rBZnt@YsmQ zttzrQ%vbfjbJ z*iuHzD%}ZAw5+fx1l*_FHyI(J*ENj*bxC{qV`!g*dJ{c*g|fMGVIgrV{9a6!`|ixt z%S6>e=NVKN{yelhN)ZnBlKP5=f=9T9&m97d4oQlRj+3;9K~UJ;ccnZM73d?|aZ(RS zk4X=xSJkO=Q=dxKqx1jt=Z;wn?1??BSq`E-*T(O!1|S5GxotT7Y$W(riI zJJhIXWfS&A{tl1EO-3&*6};W>dHu5Fu=F5OrnbXFr3P-?@01OG;41t>RE7gBosygp zY^d0n?zT4o;q1Jq^42x6QHRc9C#qrd?G>9b$UKOCb6U0CdlFgK?jh z)M)^^@{^h|qxD}aKeS`Q(w1=lwepj8C<1`4{6MWv2*~Q}|Bbn`>WZsf*K}dQ-5m-q z+?^nWyAXY)wQQij@k6{7LT--iS@fHCk;A82X+-;(ffs85m+q%)Ss2l0|MSwMvgIMr_jZGEp3`U zVz^V-44t^-XqlGMY0}QRZrYK~lHri54AC4=34g!g&jJ9c82}KO7-`VJ`be}5n{PG; zD!0BGRu3Ab@de>rEmr|E$_2#?6lA_3LCUwsv%oUI*X*b+LFBpz`g8xz#U~*4``C}` z2^^7lJp|pHGj#c|Ittiy%SA*sORbQFhOG*{S&!avp5|&pvo=Za%9V3Tzwc^zu%xJ= zlRakaE*#QRTc7?HE=|Q0MApbf`-|*xh8;f)bShR1R@uWq(NYGEad-kJp$7OQ@C!mu zwjc0J8c^}$`tPV9Ghe`r(CFT-Hw8Q~LrvjASh6S8dV6<5<3}Y~&y7TP43Q}3=#2zg z-Nlefj*KHE+OP11G8GLEF|GNlIb~7Nr*hR(rj^c4@g=NPkcv1XE9cEv9wYFEx9zU; z)GTT;9`jt5;M$iU4b%*TW+OrO;eQyzQ_PI~^|E(R7Y5jnD*NXZv*4r-DDY;iwxv zBHg}(KSG}GOy7+KzJ3XTy#{f;J(NJh`TJ;45)pBp=PpmPH#bZ?SNcppvL`)78se|` zY10g8n|SHLCb!eL+8ogi+-P@2IL&oEIPifD!w$mpUIhgk0FV&Zi}oI=zsl37y7VOJ z2o7T^mYivt_h{fae@n*t!OKvA8Oi^3UvMD=$%E1??%hJhT#t?cy|I>znVx=XjCF>3 zzspbQ&@8iAjd5|X8jVi+*fA~LU#f5Yn8;d8Z;QEW4ihNXStDSk4)H zMjq}J!3Z=qBa94>^kWBYrW|qOwk!|t3b2%5s=nscnS5SjbqL&4K)N@^Elv2J%4|kX5{vG0k^To>^**Xo-eX7Xowk)XI$kfpNPcD3NUw8 zN1BLR*Vt8LGX??&AHKWXwS>gf!!qc&hCUf68zxmP%&8!HcQQHsOe&j4=datvAkN#O!x@L+KHohf$=}_LGeU`TU1KJssiRx`t&aolR`M~ zz^xtK;9$O_<2T-y%&Jl>x!3_Z`lmD-HBE~Zht_cEs>2CsM2s@NM-xit9rt+B_OIdI znv4?RT%ex`%d*Xlx^>OjhHn^;kL=LRlT?6grJ~YE;01ZUHxUS%2M4;8Lc#1{w{fgP zHFs>Cx8mR{p&Zch`FQ2B)@2d=cutJNb?~Q028_@msOZEeSs`XsA`p@di6N*U9OC~* zN^Wr5?#0il;u{v&=(A=^BLwCl>Pv70QGe52KrT55yUI*P)y8?p3iiX_@}Da`n!yHI za}*?f8Les()1aLZv!FUy&7nt?WnCrdY&jMxWAv3nWp!kJzQwVos?aT{@?w3jIU!MX z#PbU$YC|l{-vVV)88f)^f>%DJK9QIh*_bokD+A*w!8dY@=PrJdWNTs+5sn}XL}9&0 zBU-z9=Vq;}(s0u9R-;XUty)9ji+zGViCqF*5(mxZYV*PoAAvL7TvJv$gE4n-JTa+S zJzIkGiOL6OKLh`3KHPH>acu}MwzP^R9$Xpv;X1chzUV~gI_1_p^P~}8porho%R9#5 z@pZ%(Co^EF{;Lm}bcGnQ$SlfB5Qk* zJW+&%JS%FF)C5|A9#t0B`+~A-<|TDKcL%JU%%Z-rSRQYVg!fzWI=pcn5sFuF*t_;S z4i>SaI(Bt5Q)|FOQbF%A+ddKbWW!B@{v7}XQniK&AHH+n$EfTKnR$8L_u`ubM6?bg%|a2^}8(6n5>gOv&@*`W|s0pP)Z_CmTk?6m?-| z7Fr4AC^_h3Aj9ZRibK;&QanQEMjMD(C?ZAnsGGCyjJ5Y;$M4IU8Sa^$u2CS~| zB{nC;v)l-+N5P%p$go!_2;zF1*_y7G#rlgZmK80O{-Ll%vU`g#^h4JbW}?KgW#fco z^XQ1`k?VV)qcKrvR6U?3!IEfi#)XTS~HKrv$(6{X<8tpb670wm!#sm>j=@VAKt5XW*wjk0eH%M7_b|Vk+2~?>Y zyzsC#aASA+@eVml&nuWMj#_WE?usN3W+Jh8Zl|4m zfb^HP{Bvfa*Bbm~=^Ha9vL2(X9VJBixuO^vEEj?Gj zIQQ~$qQ?AA7C^1P|3^;^3tCu-LUnZr&>c7EXs|FFuh;n{6cWS<=d>A~ zQ2StT>KHBS@1s?~U>v=0%)^`~4x`X(kJvEA@p(TMq?2uiaoFj36;>+J7L%@@KQnE~ zZC2JuA>k~kmjMw>Gh))AU*-+3`jqIo_qOxsD&E;2tM2C(nCL@HxVoEYwixeih1Rh= zfpc9f27(TJF%8iGPuU>$U^v(YA&iERyvfimvfqurS?B8j0(&BOKXDDxTN6i=ysaH# zg{)5=o#5dR%lKx=`|63oi9A^OwV=G{1V)$x+|^Qs_=Yv6L^A4+`X-8<{rOA(8XzLd;tYsRhITn#S{y7vxiYzbX3pE{Zoo>QTm@ykiyUCUB0#mQal-L;(H% z?$H$2%mF3UF4S^@`Oksx;qovKXHAY!E!rOpjrJU0%ilOHQJ2nNw+_`JPU7AZL0syB z-Qt}y*eZY!J^hISD|?+RY3U($M_SyN9!&38h2T`BXy}A~O+fquL#1{MAMZkXD|*3% zHVWe&AK&#HA|xGNd|4((sod`nUOy;SA@;$0ROo`&dKa|h#LuU0S3-1z*z7q0Aa?Dg ziiBnf-1*@Ut24>?#=)Z1E!pBQ@v88EC33%FdHl~LONaZ~NdzVc^~L}_#U^D=ouU-O zz)pNr<^ZYLmMv!VGl>H~xm=lb&4%mLx5R_nf+KnJ#(+r&nq-RnsdS>1QP^SYpgbr_ zq!!bsUfP6y1s@3`-Ji9hieZi!XK_Ubr{=*a}-b#?Jdjzp4Fzc<_MzkT=_st1_@JC$$D{OZN zgPM53jK89lozV&y56Xym!vRER<0zXrK89Wut z|BW;;TkU^1Ni8riTmKYb{>A^hs&7w^7|i=VQ6lUMZNRch2f<94R;z#f@M3_v{s-sQ zvJB1o=FC+n8=h?h)f6ajO_PeHCV_KB7*Fc9DPW=tyKh6UX2Lc!%rxK2tz2f!TN-Gr zU^Tit2l1@6wCMAlEU#krZgpaPg4Oq`i^bEoz5R;+hp5R^HvG6y_4lUZbB$!_$tBlY zpOh^&BmGnRh82quTeZ$$pA|cos!3gUM$=Y{l7}{jakPmwNe6Uh*sCrP>c#LL^>;L6 z^6Q64pByT)#QLSt+Ldd;V6SZSKYh6;24bixt&t5oQE#Yy(XJZiZnt8R`IIGnq9wON z;2>C;sU`W3~oLz1k zBSu&M7voVcb@q);P#0rfq@c;HKs;k1_Oarzk?`%}KJ@1jB#307EoQFJtMfIt1LT?4 z=U+S2JaI+}W;VvIU9kUiR-UF;x^_ctm@j+4xZWqq!WF5-BeNeI9k~hu3wY<6&SxeK zShSD-zLVTir|NdzIRNiWD|JJd@vgw%R*YnfH1r#aKw0+Nom~+Od~;`e>qcVjii&DO z!e201YAWiKuSXnMI6g|bw?_cpTA5kV*bYQuFi!Cn#7xz26L0G;Ybt$5;W5Zeok2J5 ztD!V$LPqUV??E6c>V6cBM3ZzxWEA{v9Okd zyso1DWhqY|9GRbqix{yL5S8MhS3;Onzb3q;_%U`d-R-5{)MniR_OW_?`Mi3rWm#?g z|NXrEVNLzjK9v8=eYb7CgjSFo_mq&xFbwpxSSmjG)8a1yOyhH*&DWBRUikVQT|xNl z&eznpkSv~ZFo*vovKq7S@pEBYugdr$y088mwrWy~bNUT2SM0r=OxbvA88-5-ACwNqt!c7hAR_YTWHtBiHHXFjI{6I!m9p6pczeg8JU za(qshsmowKFLga$UE0`!$*{v8rpYa;-aE>NOQcZhU9rB|=&3Zuig(BMegIr0D(y!} z^6PcguXm4cw)u9AoFwZ488euJdg;_Sg>mKve{v%^F4~v4$eL9;B`rxEdCh1!fSu!6 zata5|B&aB3FDqR#B380pRo0tf)MnBZ0c4Hh;qih65w2)!vL4fCMvj;Qc$O(nT==*b zK8bm#pd0HSI(S86#MGwa$j}2fgi+Af7A_)c-ug2A_#erQb^pW&f6xIc;e?8E`v}|K zrU9M(5#8m_e1jrRokiXp{l?9Y)`P%i@er=t-n#pmV^2K)7E`9w=9BX_zC4A>WOu&x z8Wd8Po{RU~>Cr*|)Mf}0ERh*q>ORDdrkbYg45OC#Fe6VcZ8H09on3cW&*b{nj;cv^ z<-F|n0clb7&UvPXv5NAl8-CIbzqdb3+;Vh-ynO8v-~OC6e}v%Iq&tfjALLP3Snzj# zw1A*2(+IUxv1XL#@s*i*6UoE$I$MYO^mT+ja`&cNL8rqC3>^M#4f8%m zFK%HOBlJKjuN9>5EO6#2^B2bF$wf6M-9M<{r2h?YV8XK~A5}{;sLv_|Y%)pV&I)*3PQwBfk<=G@W zZ|hY1N}VY!y41KKZCqABU=Z-tcdNy6b$C4mLZesE;hfg^%@1{-4NaRTbqj|tMjuL; zZ^C?}+9{jGS9PMbB&^C_z_6$_!~pm3F|YXDzB+RY*-vz7|2CIgG+n!NSE*C%QNEN9 zzxoH@?YqA$3a)F^MiS$}V2#5g(rWm&so3Ixx0s>b0NiSq>Gv@_!h18p<336d2?obU zFoP4cScw276#ia?HoR%PoWv=r9i2@~L~Kc_{*x+HjTCYTZ$oYzn^u*p)xbprnQ7o& z+$g>i;pCsj^Y*bNvK*yB8VbZpaHF3(>yBc(u4jLN4%u!Ai(83*){Xq$iT{eGaiCoq$2iw87oKo6~?SY#&#_X2B+$2)}@dR;1r#&Q6i^wss+Pi3- zpWTZ05#OHnsN+lw3{CsII$YT?fk$Jf;(5-=RHEOwaixQRL_?pk;)sfQw0D?VQ1#t#(7VvH(ek@T;wA?X?(tsny+_v6} zQ|x?JLY9o)FDtCX2GodwYTrwv6)T|@e7I>&zw5XJDZEkLT!@vBxJM_boYCi(=ao;Y zMFW^y0jJ_sOVN6PKq~dYJWpRoqafCFZ}gus1LKn7yru+%l)1&31-Ve)b#=w@2SW>d zb-8>4!WBk2?o*9AJ5z-MlEcTEx9JgCM8lIRVbAFgd+wG5kAFRB=>p5HDQVSRWVf); zA#E84JLGr4SOipJnX*QCGazoxM+z6vNP z;|EpV%@IW3YdAFleDL;3dY3+?x2LLIps$48hBsXNUdja5o&swotuds8BRhzdYED%Z zMO>J8t-RBV%4g2vsC1Ntrcs2RZie73bVEm@ZpKbfI{V#`5}&Y#9|l#9wctLzeGpm~ zt01;!Nuxv=%cz0d`|C1FkJbHgkn-`mIva;q;ipW-4W_x)AADKPnd`(u(fqcAZ;Q$n zQE0gy#?9;|qP)1HEZ?b{=>J6v_{{oMwx{XVONvkEx{L6`bRYKJO{bT{d*$(^h3siy zyBH)K`{P?129m7}Ot3z1d=^%II^5yIN%zC7Vj;$U*LM^0>#(0XW%wY}Dk$4SHlo?M zy5{MTc$i?IS9>NgJmO>hKu{T{9Rg8dx8vrW+fQ}*h=z%>i9^%F&NCm#ucvOoYHKQ+ zr9SVc6rL$ayH46WatrT2r>4#KSo)|*6s%=cu6+TMSx2s%_^L^6i3clueDLp>MyVnP zxc5Z)VQ6Hle^IuyHfRJU!unn`%7UbHY{OsX!-O$}fluX`-Sg2W#T848!lPzaKn!)`^2Dpjs-*9qH~>B`Bnd0IrG? zA?X<-KCjtaI@`<8*ONn$t5d#KAxw^pj8;{=YC_*76e0q;c*ShSf!Z{{2UgVg7;gwi zdnkF$clkmXx3Ux2NL-`byr{{boD6gy+$&bMhf8)MQO$J9nAF78=&0QI8INBJZt93_ zdQa!SR0v;Xb`|z0R1Q2Hx;~1K6-yes-eMbeeAg@zvfXa%2~MofU_8;5B?@rJ@Om@* z2+8)^O+8^)y!<7!J!jg#blN}P8ElKymFFfH7prC#U@&Xi>a$?FE4TnFRYb~dAf+lY9YERI!a@VzRsQEzPdtop72n%JU+Z$3K0ciA#kA2qrq>ICGW0C zEIOd!1CI^`jk_JAt3N-+sHlv{#LWX;%^g(`zEHHtnf4ZHUl%DT$y{oCj?XUbN)WnE zb$i=Npk12EOC5Fq5Mw@|_(bt!tHQM2H;uP-=J}rsW%TQ`NbYUx;{I28m^vGQ_2kTl zsQp^!dQecU$+3N(ZCJ*8IBUne$4~_#Gnrt8&kaUK$`ag*nIXzlg9`hTd+;5(;!@pu zizlZ}KqwgtBem6&?6MVe2G!MNdkil**Ytss$Rk6|(9LC(F@;*%8X{z2c0L?4n*Cat^bNuIBqwWcUr0L^zFF)6E>xuha z88LZK+(@Udeuo#|F+1^fp&&HJegLB1Ga)3bU+D2T z@`E-+ntK>bcnAPtM{LS71Hr#sETjJb0$<{X-nJHmhE!`5+f*Dn1;S>VS5(YJ#?#5X zvVN-gkIWMrB>9{O`sHcY^%yi^oxiaYS7=0Nuy7Y2+_;ir86(Zp|H+Dp0t-$p_A=$= zz^tU2=sH8cm_f}4u4&;bPsbz|Gk*oI7-H-saHI&%iMz)NT6h#h8T-?ZNwRlwLl3bk zP2|~7Z?hk{=lrH|c}6V*2g?#b_S0RVc^V2si$s+m5~Dnr|JN2GcJ>&z9_g~;kJ%oq zS68sWzm3;RszS2*Znma{i9Ko~G6jlt?};IdY5H<5Ce)jDnw+-%`p3b)4o_lGAVdr zD#Kja8t*r9lqfia*j+WpIJZIFjmUZhG@<$oD!AVn90(5Q=gw|EE+K z6STI0gPlCnv>@T`IINdljmM3#6`bzExGL~rF^&EAF}_)E!bwrbR|D5ys~_%&6XF>_ zqx@w{h9p2;k$y^b9!rRnxzqPgN5wbvtJDEjrdKEDcrVu`lA?> z1w&VI2wb(@cS(44r1zjyEgwoj|1cF5d{#2# z6Qw6q4jbDKJ|K|qNDMvv9(*CiC3IqF_DNto2$4E6&^%M6cBj~U$L#d`I%6vGZqMG> zSRN^cxM^}6l;~KP2QA*0+Og+d;LnS{r(N$toqerz(_n*NuXG>nTO7HqKMV=7$FYzj zn8#>as_$0bIZ5!j09aer{MR^44nr?*zJ|xV(ca=D<(J5mpVpuRS2X#_FC4JTYv8b+ zsQ1jdUm49Jb?_BR@9c1XH-6OZjES%2Yp6|c`u&A=yf1m&ztDT~BALrSA&lOZ(6~w1 zP^*qj99eI>jX^&pqQSV&l_^N!nRxbHV*G7xS8eZ(@4C{{yht zxPhm%)kiO;n-w2&cB>|3(?J1fun>?6U5A@E_+c>mJ63UP`FW(w zwDG4}V$>bcra$B3xHQD=tJq>7rsryE;on6BcD>Ao9pO?aTH@W=rVRtDU@9R2d!)!Maqu z3I5Qiyfajm82D18BQrr9m1onYq5Zx`wOotecX!XrTrMiW=(A0TTTHS~(_0pX2@^?u zBSYnhafAax;4v}!3`N9w4O&FRZR{d5!+C%I?i?RxHk>rT3I?CuwnCyD} zyiGJs&7>lMv}JM%f*)|7vPB7d4=W>2*!qQcvu}qx_4FzW70+O;x%-f0C|{gvC(qC% zkIF*9xeJ7o>1bQvS3#)FM;4nL?6B*oa5>V?0y9+@VTpbG_mr?{Hm9X7ziB{{NEIX-S|iAwZrR4g>? zV#f+lflUb}%jh38`I9!97NXVo8CO|q(@!q{W+54NqYUrf!kRCv7y{wE@hg9nlwD`M zvC@|B51Xr{JHi;8Y1yo19oVT{tRe4p|1Q!*KiF6QmG@KUutLvzac24v=f^sZdd4H0 zh+Fh@?}X?RY-!&WWcYn*i^lltpQR-K*>+c_#J1hf=mvZo-x`+Ge{2W4g<}jj(%)x} zUG!wJQ4T9ah-3uYCrz;cV_o+*#@+s2y;J=4w^#zdP=4F1kv=oPP=?ofM?Ky-J4k6X zIQa#o>=zz3vWgvQdti6Spb&e?{_V$owue5mj=ngteolgE%hp4GX6R=ts%=O^;q6=% zsoh_-Y*4Ho+-T_gm7T0MQ@=R*EG^ZwE99ZD zo`f+}@aiBj_c2wY=_VxLajaT_SQMk4`tRsle;1O-U#zAKI^B_8X~buCB*?p47R;H$ z%*}|2)DCXeqm9CI;Wvs`&3rdYsos5R*-F#wZ84`LOunJ=wzaYfv{kF};8|q>Ish;o z%TlP{%Ju$cZ(4w)vv3}p+V0EsGFfY;PM!r!Lk_Db{C85YBXI|pmKw4Hle)kCdxDLsPIU))KIi@uZ^v7W&C z8O?@VylGKQy=@up%OhFh`;lf_5E#P__aI!6pU zi(t|gzj=_{+IWp2*xiFKvsQ-*4ED=l7@LjwUN&6tiZ(H9UjdbQy~qk zn*~-;rO}H?(<=N2fXXwP<-6XcId>S6la*x6ZoI{Obh;#}D2J~UDkun)BML`AP(zjw zFX+Lrl(O7vT`kd*7F2WVaqnT7Li+ogUh&D){oqIbm}{l5tI^GxB8o6u!Nsuv*8al zqnAD)STf%t&#b>P_S1=h}=&6gz)B+qctfqI- z^K0Euy66jnuA>(BsZ+HBC1-2ApbiV4Z8u--bQ2qgK#s=>%iF;j-VfAL)NjSFBBd8D zb%aQt(@e=>mI}O+tP+)8Y|P#1eYP*Ca*c49baQWk^uCA5`&RIDJ}*!*fFxj;P$BLZ z4-Z|LR5oEg>CG{Kt(T1q49~40AQ0;K-=d9{GqWbX`E}~0%$HypX*D6c3ObM_&H7X% zXwIzjCX#w%fG(VR;xjys^u_nvwRCe z#mwQ5iTsWW+K!nzL(P>HF)!mFi~CkfVa|6SO#^sm1SFy56q#7N*HXB@A*~C))rI&w zw|&QSgQzfFOKLLM4>EK?s~!UI#Thu1Sh@cN-nRRWbi%4cKBn~> zcAi;%wfd*lFyLgZ6*<0Vna=1Doi)~dF~g7%MN7LJF&imOPb?^wbU>byI_~?Na^8=i zXJq+w-rd)&n|V`9bN~@S(=-zcARP=oNEFC^`%7zmPv&L*%eiXOsNZDxgNNLm&X<^q8yud-d?G!^s3flrs6th?8VtxnLg2c~SOP`sRbebe;|a0cW-=a4?X1@S zXk*o^Vr<+T*J*eD{3MStNZce})9VHKJxJJNJ8*+jm)(m)Rkd-QAzi@#dP_T~@-V`3 zV>I$5IVS}aeq9_wjd0ojPPJk1&=B(_Vdiz6cGK(FX@N~nWnn9e!6pLQ#GJ{j#%NW* z`Pg|bii%A_A90kt8Qbr+EA%3HWZ-oWlu5Bm#R^L}gYt~#5obXLzj8a_Xl3E!So@%% z#Y#$kdY-hH+IYvpDe#rz1XSqrTfdbL4=B&*B23Z#euoW{NB^SiTBtJky7vq^w0Usq zewF-(66x7_tLul{r`vzur+W>13@#;X)T6dv-8*+>5ExC_8~Pv(m(3p4aWU!H;b-r@ za(KKM=EyiO!bF<++L^sG)2l9M3(;|LS`WB8;2IGfplZxeY1G0)f<_Yp;xVhq0@(AI zNV#~Y0$=FKku%jH`WR*%m1#QaOkfNf%05RFo(QyGXy;?|k0xNm%`?NC@9wdA1@WSV zZ610%YFLC9qs=EQnLGLcCe|~J@K~|RLkrRi_ln2qUQ^DY7-R|p@=sOu-Jjj;eg_#% zlgs3bQi?@Yw@Y%0r*9k;Aj-hwuS#Il;i^vis5rfzTb0pkC%)SSG_inQ)Npg;DxZ>w zcG3CG_{339XJb)~<1ddfjGYR_64EVVx_E5rc0Fu%>m{cwRt1f#Os}03)}+NxZ3d8_ zPNds7O3dm5un7?;vBlO2ejzUF7UtA%T2`c!-0`&i;9g10i%)ZRU*g&vQUTVY-l%Eg zMh#PU@8B$D-P6nKyJjmmeJy3=5WE}P%Fiw=i%m`r9+cx;=87jXIOZw?YEuhr&#m37 znIpO--NnwWSJdgW?8acsXv#Lb6#(UeS|LC0S2EUQ>1#M=HvUXvMB`}lQ1!)Z?U5u7 zXn1|(eyaTGx-ofWQIj_Nr`znWSqn_YaOw70j#0`TH~u7MZZ~dT zHA@CPG9}lw6|%Q6BP_8W;rOBHA`tDQoa|Chj2}xB2r_89-ztK7QpPY!-iy`-@@FNNz{{)8p`$;7L2Lol5;S_1`JC#&2^S%*@8x zJJ_j9AT_LlZw!cLV3g>F?DEGHHltsr_*v)V$+x|s zMfN8~i0MObc!>3)&kH7bVkwOun=HGB`rf5pdg=-bC1s(`m-FgtcqR!h^>7^XWoc^BCTsc6iCD)Q5Bk4(q-+$e};JM3eH z;_338a%Izy;=h)f!Rwf?DGef%f3m}+EmFcm-xlwtwZJhr#Orz2tv3=;soTKBzs_Xc z@~b93HUsE&0WL2Y?qSCmF^#m6pyqvXWADlH&3B%FRje&Hem@>Di}Q&PHDk;+Eull@ zMPo)FAXUFNX=#-YD$JbC!%DB$0ypk)3C?~RP(G;W%NszD*0L1>%+3k28tDydzDTk{>nVQ3)(A zVR-g6P!_~|+NZ%7wol1%-?Krs4?G8F3bWDjst6YLx|yP$$Ke=vnIb(iPQ68?oRcrG zzt#~?LOeSck{2N4uiiGPkP1B-Q39aI=V;4ki^NomCi$R2hZy!ZbC>nOe-u2%CXILI z;(-Kkda01T>3|M)BPoVam7^_dy!vsz7urD;g+EkUkb4(dfVjxv7-rzVnIz?w$6<#~ zeZm$rIi9&xZ*q^0Rd&ym=Gqs*O7*%akOb5tTuwgI@h*=AS?UKxPLpNaDQYdYT)A#| zy>DG{5nR1MbYvc}+2eRmctpQbEI+k)E{Qk`TfTN~0|* zM|@4HRV9mUp+CSMCYgW1u>ao|G{T7<4PT78Mj>NTKg8jVSTBINm@_W^=d6VAh}82% zGsYF}(}+NJ?0hk!zMv%hyu(TYwVjI4QPwr@^A1niDmqc;T~FMzOx8bC9-<^}1szH^ zJecJbn?wt0bH#4%X;1f=_;dhb5+ivMy#tEE6W$qkup$Mn80UBC{=qx@h+VZ75SGnH z!nj5CdAqaX5wv})E3TK-y?ug?EA@*20pFFbWG$+FFPc~T*4+2j zD%p_&x!i64m5;|J;UEX+j9kjlLHg6ZA(cwCCd0ib!JCUa(c>fnzo|STLxA7g>(gD~ zR`K`$0BZLr-Dy3o<^~OuABVMEeXuL(v`3MFIB%3J>Ncvk12JzHHB^V*xhyblk|8-Pe7-=JB=?# z>M~T62UW@`$rHhXeOi)Mo z!r98gBEQEgD1HF|fgIf$&Ri<%v21AySs!{N`M&)5lxehOYp16Cyp`=gpJ2DwmFo&=}IF+Q(pJ!(}I_L+9gob9F?dCqiivb!nmn7udX)3_R7B zuJK|<5ntc_CmAV{2{2y00~gk*uYC{soDTrtI+ z6RLHD5OVoJSJISPfYb!~=>7riW8zi9ddn-Zm<6dO~8#nE)xfW>>o!S)3 zMad}otMmS%pk11yfv_T6g4PuHYa$RiQ9hD5)qd7BAAN#{K8udr(*%Nu)GFhZJ!YJ7f zx)eZ=eiKnUBXB&iRa2^Zhu?nVSJ*BIqjRgb8=8YZ`gQs?Tiy$iRPgz(bGC2$=wbc1 zsdHeqdUk~>c$kD9Us4&^WeI~^!MiCt+qc>Jk>oJ3H!;1CB!GhkK zcs{MFbhd~{8$2vJIjRylvfd}4H2?|#OIW_4mfE;Y6n02)AvP(RCJOdC3Eo{{8f#-Z zUlW0jwbJ;Yv0zh&9oJ9Y;Gc`Id{rtI<`qp4)$|XdXn|Cjw!%B%0vnbhP1$)rM;^w7 zy3n2s6nS2+f6X7J`{rI3_4@O|`z|x9bbp&`ps{c75van0paPSUvHvf(GZI6{deb_IDW=-&AWn`S@v85z#5Y<_t$m zJB2Nt7>$n3|NDrBw%X~6$iL@C`nTS5On+S4!xSz)E$5w9OKP_3HU`YR!vucMV!Hhd zwlo&jq!76yX&&re9rQgsB~vObohAciUMp46v~<^nr2DWi7~vususvxhJzF7@WnDv!JDS_Ly_Q)?N6N-s-<=w%)A%Cj6BeMWJnng@gQ! zRCB9`y~+efT62TVgi=|5V6NFv3W*uxX4T4=DKXQ3nI6XO--dkop;Pu-2=(V`hsSe$=}J{$$F{jm({fjD&K zl{vRwS_9~#%WHxrx3>>g64lx(I%y?F?hoJQ7LPu?CGY&m%Rs|XHWe*X24Rqwo-8?P zm?^Mt5pR9WcYljMq5Z=*QFS|*TGm)Y9FL6~`5(Zi!P#S~qD-TKnhF<4+^&xt6G1vP z_e92A$_7j*NT+Il{f_I`U{{XLt*=pR@~#3U)1GUL^twd)No+juTc1>ceR|rNI4caT zq^<>7{%O1NKh4sE{~k&HtM2%dR9~-Rmq>4ZD??!U-SN~T0870QkaBcw= z)`4k-R}k!{F~U%lC?_F=Mf`xQAu5~gHNI(WGToq!$W|*awC2vDo87%4 zrhGls4ktFi=sgwVQGC1eXlB5_xrvlBuQ&ey22~Qj{g07q_9y#y@;?d6f&cdP3Fp1+ z{sV~2CQbeuCg@)Mn|@J(q}Ug?U&sCnwrtC|AuKTbTX?n`wnocxV}?SG6mMpSBmn`9 zJF}S`qw|smDH~ja?(@U*p3I~A64L&jIZ*j%tyN*@n&)seb_t+|V=U(X8n68|2}M zSz=w>-m2+&9cSk0Qk5lUZ8nxy#djMfo5CUlA;aD^Ofr5Z2rK8%ICNrAV+@X99!knep#Kk+!vv)O{I;;^FCx;NQrWHL2kY#(W zO1r08iOh(0?S1%kkH~f! z9VS^7@n#k0egBiv=Bn_7iRv2N+3NsDscfe*`Y(YtR2E}3s~pG2#VfBCnS8ah725ky!Vx7J)na)Q67mn%1VmpwBXs%-0G~-h|1kgAcaRIw0bMhBz=0-&Yf_Z2z|G%G;tc z4Uy)~|Ha!`Mzz(pUp}}Lhu|(D!4q8C;t+~Ma3{DFcPQ@e5Zs*tElyk9p?LA)PK&m* zrO)uZ^B-CBan_poo|O;htaaVz-q+sy2Rebp6*m}L;&=G5t2I?x%WQbwF1)CA^+w@0 z&RBz2<#icDUaqxsJzCei-2LW_frmnn4#Ma3B?LDdcgR_V>!go!u!3tzJOeO&X7}C$+LDXaLL@F zx3g2dQjzZ#r`m36dUDkDHP86EN1-#Gc!{j6Y&)BaxCLl%-Ve(h6U75ovPhq)WVb)N ziDA-jl#+hSmS{=FspVdVC0IpwCsb3bxQKKE9GbT=`-+JZ27B=zk^#FK=nGY38aprn zL)^q1F`vJg^N^T?ovWC^k6`S|BVWNbwOPeB{;ajTEee!Ev1Oprw=%QYnBlvcr26fr zFn#xu1~YZv-d%h;<>^8Wz6!)p!COftEm=#U8#gwzZXvanLZLf>&D}>506jt&LP!s+CvKhpDURWT9XMm;>l# z;quC5Q4u2s6NF%uMOLHO6s}esm`Y~J-vIxg6jdH~u&C8c#QNU8>I}1_zH!85S1jKaIh$_x}I{)kOjU)}j2IMSK}})$6rs5u*d+o-waa)L3(g?FRRV zWBS`7voz9tFK!A3`afFYob^= zqE7}1<$?+p6-Jz&b_}iw?z;3Z!E64RMUYC{sT{qLd7cY+A)TDktpP-cby-C@zDDV! zW>MuUk%cZ1HiL6<%aL2xC`N$I;hR@~ZDG?tr~(kjKLT!WDvBb;p#QcDKEDh;D=y!n zI=Q~nJf>sKrj%_Arsszeiy zRI?hPLI0M_gDp>&?-AovRHI4*(4@p9B-GfFTrHHKgogq9Ddbp@6l zdN^IMb33_dIvuXJRqH-ORfR)X)pZwGe7S-_NcZfC*8p<=Scq`pSaixKQ+9=VaJZ^7rq;Y#x&8%w6*c}+; zLXd}g8(P1=yg)!L%CB0wxhARxMT5$R>l6_DrM&7pZ(W0FwI&v~kGjo@tZuS9;MJG9cJXT{V% z&sMp=PsnfanlWOJXJecOi%K$rx;ssYs({VW_x1Wc&&VB*UEEPlWD8>cZK{s;rgt0V zduPWHnYsj3xOmFax_qVw*i;_7Hl)_Uri0Ey^*fPbkF zI7fy>JXmal5jk#Hv@F|za%f0%FK8D}a_!8ho1jiV<3fS<7F5yNWI?HDDgbHp=wd`{ zsbEr{1QAunIK@bp?V;bK&|JOW#y#e9kt`QdHOG6c$;iFHv38jZ>l$x=d~|72@h4$Q z#MA3`)Rmf?X-fHFJPCG`yt6#!Gg>Q4i&toLLr3|0W9hOpzm(Pp8Bl7dk0#lFYr@d$L#V0cJ(J*OTqOr~he)eiGGRuaw|`S= zT<3;wSDpy33B7Lxp*(xrn%cl$8gGA4?zP6aGtPa_2X7h*WogZ;KV7xsarXFrj|J~L z=(&-J_0Sr!ff4ms16=}_C>*}U=x*e?^{<4{1`tcN zP%7?Cv~6TSp)Ia(Lc4O042;>GoK>3y5V59-w)5;S`Gg@I(bigPAnn>&0^hxJu*+)( zP%axAV^n&@N%;Wt*HBDT)&?w1Ndk0wHDtlZ)NkVYtOkEJPY8druiJeSg?|$qD#ai_ zs#hy~8pl2Ll#O{JXHBm8xD|2YI;v4b+&>2eF7;m?1Ucyh0y*?X^SGh=#!IyC*|3QE z0*GWkYQIzNwwU&DW{|L&)0-Yc8aLEI;&e0*7^!?C1ly4Vmzz9xLaO=qVDz8mvB%Zxq-~Tn(^jb1|qHM#6wcPuXzlb`T zRTe^_!ptl~7GrVpwXlEjDPZ$U5BLt&GP2r&^$NOgw8ht=WMW7Q+hmeV+1b~M^NFmt zF6>QLr=@Swfo2#GX_0(qc{AYuip_F;T~EW>*wo!-YdWQ|aj#+l6KVaqRku*cPoACx zkWhgLt;4Ylm5Xc0$hl@X;@W@3gz97Yh(tL*9-cfu^vh(n3c7P>>-2h9@wQrO18sUw zFUU}&adJ7NmVS+j+4y(hdc`*Od2*O0O!=p(%Mc?9XK4zT3Ltmg zN$)I*Q}BLHrw5AIKrk2_M8X>Tts>N#5) zn>t&+*^VwRU(Gv&!=T58T=%~W10tT3kv5}=UQq$k|oMZk4{5A>D%7NKRPxzSSR-RBQb{SmW^sy}H_I|VVI2q-)@PDo*iE-ige?1vM zrT33rCQmBGE>DUv+@S3Hq4ikjh(v%wKKfEcL327rqac^*PEpE3!>yXb$5&T zVXD&3nLP9Zgz?3YMEcO|eB9{5^1*d8>5esikvulfu~k>W>t%-@hiK$zMN`vUJyA+p zq>HD-i6Pso`oDJ1&~!e!2y*nd%MX@tbCRZ>QZA}%{!~?*Vsv;I5xo|qlu~0heRaq) zCN#jzGWCpcPTLn-H}WH~Kyy_AF0MVx7v|^T=uCj0eBQqJ0CEl&ebDl1g|)*h_cOnG zGcwK6@41bvThYb#BTNyA8Oa5BDqD{-o%1=+zh6n;trYZgasIhPjGEE6`ot$~wwc;C zq3X1_YQ$ViD!RESf)pX4DlGSdXFQdxTRlnXNj{r@f+BF92?h1vD1;3vC2n?*)uh*g zwF`xDEug9ZW@eTlTdHX?7dB55Cuv#qV0VRb`l{hFiMTPeiF_Rgkd|QfFn~Kfv?&x2 zUdt(|)GMR3TIXYbS9ZjFKzb$^H0^?^Vu6N1vM&IX*L&j7a^NG#q3YVXutUDkkcI*V z);mcYf{TGj#Fya?A5M$S>)KF?OH$<_h5pb=G|gxdi1%kcap*!w#-I9Nw+xO4p>M+R zZ0=Dh0`!4O*>L~xV}`Km^%d1zy-Ug7CIxLaYWattK8trO4EZJY(n;hAsNMz+t#9*!J(ePozCB3_Z60y0k z1c<#HYSoVqNYJQ3Q@kzTTL&tW?Tm{qrgC+bk#~$Bt%EG zl0`-H*UVtuLXcC`~?w?O?2w)8F~}v&?d)}24iEp1n#u%?qxr)(9J}ykX`k!L4MA$c zAvI|cZgfHwr*+bQNbv>`KLje_%hg{f&`E+e<8jY~bgn&yC72$Q?t7`&! zp1B*GX-XVIMnRtql`@rzQWdX7mZviLCo^;mRy9|)XjZYJ7@P5a9M@ua`)-)_2_<|| zzL<=|uJW7&p?sMaDXay=lvmXI_nv9w>H)tU8hVnF4sJ6mGBTu6L#_tGK~GjD@kPx; z2KfP2DVJqfYd4WqN}R*|h}*^x%xTiV3U1NDUHW2wB-QI!m-#_Kq*62!V+1%bWtzF= zkX$fM398T}{#9mb(S%UYrn)4`cd1m8^NG^~-vdHZXk${MGvzWE*NR6JImxMl-+h{T z(aF3@f!2m7u9;WB)-Yj}=4XrHnX=9m-}n)M7iq`0;;>4`+ce4iu3~?`Zg=PGM}10W zS@bzXMLSFf&OV_HlFbsD(59#)Zdh=?uzB0tYIfnh<`P^gJz%Gk?eAsDY7C&#B9%C+ zG6e`;Sg8lCGW01y`l2 z*j$u!$bu%#fUi5h5I{>|3N7Me9p;nunk&H22~EfYXs*>BB=cmIGPF<=(qRddA9?D_ zd}>8pNK{KekqV)#o$cgHI@B1=;_Y=#r=7-0`OIcCxh>F7R;#JW(l^I}!Z@#;=_REJ zYu6?=Uh5DSR$1XB}>03nR8XVCtPiTo(k)Y6@ed5ScTj~<65dehA{O*J&3;*l5P z2zbkaL;x;*6mz$882#&#d$Crn(dt+a>+;{~SAxT$}g)&0Wt0ie#} zwV+M1+ySBB6TjnUnDo4Pna`@@^L)8KcyN_AdxRG`D;%U?R!z7m>Ckk6eyC6YNp&OiT*p6)A=#bdVT|~xL>o(i#R{ssN zKH?Cyx3?GTyh0C+rI1P`1)zO*9wf`E>9e-jV3f*gc@M#oG0IcU=eWK$(|qC;e3?s$ z$(5Gwc4R3bj_`wBZ2Q)z5I!c;M!PCt9t+SvH?U4VSDX8rZzTcu#`r1`v(P5}XUxYv z3lV&T*Sa&tc|*p zZ#G?96qRVX=^zWt#t>kiH`X0Ya>Q!~k0Kuk;KpxQw)q0>^WR=ve$KB8Z8GQ{sQm&R z7-zOT;7ZJ#@TrVG8e(WChT9VCxuz(S()YCGivOlWwXK@O#0GQDlgq_L zm4BL&5F?6|6_=wHBC>kS=?XqGu$s!~S)4N4Zb2>0QDo6h>y&zqrx+IO)HRyJ`7swM z_@c1NsW>yOV#&Q}ZqpfET9#W(Hs3JuDaeSTWwa7=;Lz7?gi0M<-jJy=H0b^*QQ*+1 zRgr-P(FnR}6JYrAyNwYOCsJ(T%4cJ2oO%%%AQ)TraaI~)6{l`VB87^m2n<$-zleROTp9ZW7J)x~q z!16&IL=>X-XX_6M7u)Oy3_X^cBL^F2vF?A7JzIimlpR@yBIY0j%B1`Zh|68>+tPAw zwf-EQMrpqo-wm^MYmFw)e6r>3oV3jT27T4I!D0Ft;OzN#yp#M-|L9*v6#YMdR@bM! zqrg*5Z%ozb%X3uN%PIOd|LgbI%)k45qyQdq^6+MNQa^MvmuRET($3Aw#x@UH8^3isx2Q^Y%~HCBQTD~V|eE!1jrJ(S&gDTnV)&xk`V{4 z+XB1U&Cc)3NY04Wm{X&Sf=eH~V3#Vx{NPV{eq+?xkJRuw5sgYC=;H{B446`-#Vyk8 zqu6BbCkt@R>Od6a9itWr$pw+1>naS5NW?@uB(FAKdU|S!n}F64<20))9p^fJ&6dMQ zyHR?xj|y`U;7#0GCw84ibUEz#!SJiYdurTk^pt9zWq;jLW-FSf%3XxViC3B`X2@~> z2w?5PYnncTZ11=xkf5Go7_`j2pO#`VmK~`hh87POU-2N$V7Y_h)+)0~^}1|Z0sRGl zMSN9z9(z6$$H=vIoOcDU@Q;sE91xnzG&sCZP+67dAzDj5$w z)QU$Qe^b)3z1+J9^+g1?I8A%>#S%n-Ay68FLzfLHlQrt;lm7ublvO2E5*MN{>pIq? z03d;)AQ)u#yq=Hast?SJ3z<0Q(Ny%xS585mozbw@j;|BL^=y^grjqq>bFk1^9|TAR zvUtf&FfyaK4jKGBnb@Zn1MgpfSNEuZELB<#S^(U5p_j4v!tV8Vdn z^ZFZ{{1#i!@i$N==m+n!Ka)s0Lh`AP;(v2R4sLRC16TZSy#ha)SI(PPF6#6HX164+ z3uf_xw$~P`qhEzo=y7}JqALnZv<2mWZBXxJ!E8yeS`F3`L8?ecrI&#MSC%%HduAmc z$T+a8ocvoFN~U(p4z7Y(DbuZKp76!3X)j*mdeU`r*@k{Mjj+V*l72C=`hj^|;6vJ8 zZ+&-T@1E0L;0)~Z-;l~Jk9`HJg=a_hz_tI#*QP|*;}hJ~(=+k4HQEIto2qN-oDWrv z+btaVvo1-u-#E7ZT!=HdQ&+w2Iee9u=N7}}c5;j2%D4WZ$c*afj}pjQXA7Fs^EiA} zV;x73?>No}*4FUMQ~AjB;#X%yri@9u!a}6Na;vh}9C`heR6*5%){G$M>=K)ZzRPQ? zX7&-g5k`mi+7&1ki`uS8yx%ms`wnc>S2(hFQ2n|(i%<-A%|EtAlO`XW$>9UH6MxPR zrRnXPw77fJKDb@oskmDV_;ly9UX(*}%FdAlR=xtNbOk_I?7nHe5wFb{c+n8SS=U9w z^>xwMoGbfu&%a%;@l5dz$W}J}R3gbOteAJA2jQ;^6<3wAS7mNKZQ0>WkEXlVL}{H6W+8z0wP-N`O9D~yPXFkUa5^B6cM zquu&WmzEYucY2Z;hRNH?TBJ=}hoKdllndYfw`59-fi8P$MKXw zsrrAl-!{Z`PXM0UZ^LE%Vzp!JuE^daHLWNQ7V#GVfMtD+X-MUAqY}C-zQHJ3(oVf$yL-iA)ocoJLvP7ete{lZsmnO73b&?w zPNf>d=R0S{_>LP2BYcxHq^CFQ%KiCj@n;NT$|(s#$Rtpq z$y_YKJq{aT1MMtS^X~)IfXIDQK5u=P14t9zO1NJBb$wYOKNGjtHx;zl?{8?Me>tU7 zeqfUpp<<+E$$-Dih-@Km!jZYA!38+7^v8Pqi!rEzgg%iB`b*=jQY=DaEJCk3O{hnB z<~kppv{#vkuGx{R+;OeYS!69VMLbj@GWN6?&>9Lr-w9vC4rL?=&beCbeoM7s?$xk; z(gr@|Ols4uA}x&aVMinKNE^V5TOj>>uvm3Yx`%fe$XFLEZPP99{JZmA&NnW^g|erD zX>AJPv#>j%z~;Ht$bwuRskk>wyyfVJs{$ul-`6iwui^iD1; zqy`r)J>Z_#YkDdzlrK+`SzWMU&iz2I->1ui$k;RrPp-7@u2wq@zIuMx1=u1`3+efGxR`18#g0g0$2~KXAnTIYtc1Uw;^B~ zAzybPh`B9>?Hg?}ln<2HL1YCogUny9Zk4IeIgvB!Yr76f%1}@|X&Z0iYP2e%%lBvz z=1dE_${b`P%aG5p`X#wH6!_>6Vb?UwHS99lKT?|aJtmpoyr0)-CY;dXa!+ln`tmW) zFIcG#q{aa+-IVo7z`62cO}=sVI-a4AsS{+uI!Ib;H#KyHX$j3Xtt4fDY&vDoCvQLn z7(j}{fe-+rkz>9;Btq(`OZ-ZBuHq44z>`3PS6u`2Qk%)l(EY%GHNcMcxf(9B$S?`4 z*{H>hn=v*aw~;sKKsy(XX2(2jB_ZzC&y$ywme?4aU_!V)PD&pAB@xFN#Crd+usF~B zxpvbEv3eio!|3H!jAFLj^j6}-Ax?4cjfZ#dSF_@j)W3X!BA-J%UiiMS|NB%v^$>IX zrsPFxKyW;B>zg_=ul&oxg;~?lKUE$2<;%afbt+T~zv0y#t?ISMLt6v6wl5l|DJYOUs7Zu}!E%1C46oNiuVW-pus+*Eip+9y4G3i*d_vdx&_*`0p4u?pUeK zo9Yvf|6ttQ&jQ4rFm7^b>gp{&doGTS+79VHZ!p+Q{muEa{^$5%a_;QHHsyFbSLz>t z>G543>o?Pz55*Zhx4*DY@5p-3M|qD%zjvD;1i^%hT6ny;{r7+&NU?c6PuD`bzz@| z3~H$Rn)U^<@2|Gh-j{3+jOktT(G(9}rDVdP5^9>Q{RT~}2p|bM1;R=e5Q~?reahn1 zpSPAInB1C#u6re7G&JvUm#ULW<-o#t*OKXam+(#%mCe7#RqhL>Phc)x5LXqCq5Jdq zPk+$D=bfg%{txvJi~r)8&si`IXGH^)j4%712lTmiZ)|%m@Nc@OZoM*bq<)371QWhY zfsa4%5t&Mcm_${r7cA47*Z$0=u~~Xe#fs9Aa&9OX^x%@KvW=H=_2{C(3nK01amwZ8 zDeqYiNcu)Oy^}uKt4!p*TkcF?=VA}k$*kbEVI;*&F748j^q{H}4dTu_$q@Suw$2*` zu16{QqnERgv&Z3gD`HewSM>Pef-|Z#=xV&^I}n_Ni&{@%mAapTuMO@!LC70_y79tZ4S=w)P<>6{*7z zwEOi-F~587MAJ~$Y68Tmttfu8m`Y^bdszzryA%gwJjvx?(0AsOx-${4Rpf?<$EI9a zkN2DyIR2O?H!RkJ8ZZo%$HW9kd+(8bBZG7c(!3(m0sjuqHB2Ly=(isIbVPCKA(X&Y zQ#IZBaF385!Y~(Q=0wjet)_guGvU5Cfa&=bIpna`Yhz35XVh#sJGZ!!)|YWW0~(_R zV&41HQj-MitA=O;a%}!y1zG4Go>Af|{!AXWwfJ0D&8_l59+D#vG|{Aez2FRRBK2Rw z0CDt@bX+1r-M|=`x@1|B;c3Z)Vtb!(3yPvOO6Ptnv>DFV>>h=34~inVYySYM zK8n74o-_7KshTl4!2EbN#I^3P=dInx;U8OO*@aAEEKS{a7*FKwt&qSMA;GXG0QKz2 zn!H27xkKU-9Jr3sd+=>U*C;4~X5sG7f%(w|YEPNMVS&O1k<~kVp2=$vhm3yIT z!Uln+u;nDV&@3wJNT^oWrUhsTWDV+#fYGXWCEYe&NN&8#6!etBr(c5JlXhu{!dr9= z-y_Xc4OoG`i$`4c&$I32QcEn1Yb=!F)(Yz=^*Ze9r^Z*DvsRah%BsF{?wA#cSW#N> zUMaWPshO~s5z#fs77F8A(U(||?C^-TOZi-Wvymy~Xh^>_DJN7b*Pr@IM?2{K?!{aW z<;LDvn!IXHL}G%O}~=(=eR+Nr|qIY8veBP@1R2md@v zERB6Dum2aC|1aP`aYDS?D8WSz?ZiHv$5_Nk3{nD`W#r6EtC1ai4Bc1)tXmD^;2@cQ=oU^$+M!9SkD|MzTkBH_V zK_+Skb#fC`r%;4VMxLbHAN2wMY^Ay(*pgD)g5Q9aFr{HU#J=@R?q#Y3UY1QkRfL!7 z+tvF6;V$AYM~>D>2OfmHzrIxRRIFVG+}11l(9RR8*)=k>y`z=9i8rZsUzEX-WiD2J zIm?C25?P;F@6Z7@P~o zrwHQIf`9T2ld(He$fzw&*GajeqHp4GmjQ^PS#!H0EE;F%@Klju#tUH~5hr(-#NHA@zE*qc-+%BYIB ze-2hJ+cni?I5iYRaNfoi#=&wd@V1fRL%zqNQr19)8YLWzt)Nulru8ZYnzij04a*%T zJpkG`B)df>P(sIu3V{VvEggL!(xMVZcPzMQGqiX;;+pkiWr;cm!$?yi*Q&;(dVzV3I z23EZ|A;?Q+7H}-*@>z^d^}M#P(;LZ`&&_NqwOttt1MqI#w}PQbLNTa5nwzN~oIVHp zXa1iyHJDhW%m`?{fu25#3WIL7+Yj6xavw5o?dF<`f4y=_L49x!=1cunl`)nOM{fv^yfxN@nsv683|DR5I86zMn@3N| z_62-t(Ksx{Jg@_v4YcaMIF#?eO%-G@_E$_;}={y1LrB{mW3h!{o6ACXnGZzU7QtENhKdE_1>t)9b~*uaH&rKXIvH|O znd|r8+@o!aZTppw^++Cv=aH0|UU88l32`||d7UD_OjQ7hwWuDe7~k%X zH*$DYqGX?L8qqNIyC?O@N-&|vyUXe`ySJGtF;!vEP;Upp1&7y>NN?EEzrOi>gLR`J z7v1x4bl7~PGyB)~Z6q;DYV(BQb@%Pv>0S4kXYj1*NAD5UG+)1J%OE=Vg#ROdOwj?IURBe+e8|6mDzL}B?e%p}!t(0U5m0-5 z9VDO|xLwFp&=4Y;bwIna55u$=&K8qy#A59YUOvQ2U}{^H-XR&PIA;OhrGBh4@-ZDt$^o2jb~p<-GB3h zL^=4yeG~N0(o1x{_vn;}B5Pix!i|%b?i0kxdYb#IFk6$>JY%NO9DLiYCmI{ZzOzq% z54izSm93`!IYkBXS2O@UJVjUaE_QpOf5xfcVqhl5r{9ZNA*1va$ar>LsfsLgFsICb z6+UaNa-A(#BqW0UqUoBLy{xn_DuHsdK=^X{S_E9^R(rC1|5X~Z-x`#kSTUz8AFC|t zvr#3$jwAZz=SSCESBiyIIv%53#8*0MAU7q4dEV6fXqN#F@zmr5Br1-EQz~N4IlRtZ zs+)*??JuuJz1fKO(CWBG!xAXn*Ell1DznQBq@0RUg0*sWdO>b-Ye2|#&~pkMu-@aA z*nr!!z-cwBZ+RzEo{dyZR${egH@a0~TAeqb-BIvDpO~sTHaE7Zunvq&p7gs-v!B{2 zML+o~7q&#)M4Ob;M+=VCP zP>We*8rFjAuEFF~{Fd2C7=52rsYx}#qTR+){u~8N`mXEAzwbqR zDSwlFQdzZKF)fTafbvRyTd_*Qs5=iK=XbHEIXC#w~}&d#k<8|oF@Fp z2uIG_Vv#Jy*R){D3-MKb789Hvv7y}`m{OMG6*(b;stb2kaPa8VR-@+~=!xW$%GfI; zyS)0bLn2D)dnGHU(D!y)G6`m%*KKQY2uI|_yUb|E;X`t}NmS4^&q)Y{e%1S48g9S* z9ya!Z_Wy`V?lIfFVzVh)aqG+QHna*xd1C=L2li?Joht1znykImTG);hRVT$VB#b(K z(bJi}q*;x6!<-zOeUasuQ|Z*ZVMM-m?o^1f1vD+>a0V+ix$3a8zQHFx2A-YydcMjw zq(2k$x(5++m(?W09{tdIHS%0(q(g-+JDL7{ZD*Fms11;Vx@XRpF>NALT%miMB%a|@ zV;{YpdUA4XBWHT)w>ftxULNjPtF6=AAulghY zrN&SDwO*Q@A4JIJ$t|J1;Ex)D|2)|duU=j{Y`d2?QMYmF9YTHwWOw#}g}r7>@_Rq= zv+I+LMzphPy+X^V2|wKaw(dc?7eA#wfW(B-az{z}3Xoxi!71f`4Ae zvBox)CFEGF;-StLynew&?Xeuw`&KEok!t;R&tBC>tBo(?h&*HO{xjY#KMEF2j*7AZ zL5&ngg;{y54cVWIT%^Ov{Mm`o)a9sSp30lVF;%Rn%WGA2aO`o3?Qz{HmXN-I-HuUK zWT3i8O0mlADyWH-_sgueiqX?ksU}Lo{27RXH zbVukzdQm0%{X~S%$o!4kZd2mL_i*i21D#}v`nG@_HU?B=nn!Nf96ue~wEHiDW zm=8V3ng;_p_F1n8?mT>2>xbcwnJ4*QA6awzr!diTv>?h@Tg0D*1+T-+1V5~w(THrr z*KIfX?+Ju~iFFyRZY0Z-*)Ibct(iBz)sEz$<#;s=@UIJ~ZA>pl}<@=~Pa&+hTw3M0`dA&!-73SF(RPBE&+b5T%f)7}jxho*%_3gSY zxg^B15}3;}zPY zqNWb >PwyFewJOa-Mv!2?;mPY^U15%|~$|@L^TZ=kgU277W=A)t<=9cb8GNR4w z0J*>MJf=x3&0{M`$->w!H)PVb4(@9ATt1rqnLsqA$~*C1JILNx?V!A*|_5 zCb^%l@8!ez*Mf82;PuH0h2WnQIyplRBX~wHR2n;}X@*MOU`$xYq^B2M zR-lbmZ?^{h_E)sjt2%k;S+ES#U%<6=2% z<1x5wXFj1(cPD4C6Gm_#4-nUM+!`v2gH&4@)nNXPaL>EXVMU%B7P|_sD?+O}-0-=1 zD(=D94-u~TLfkG01+29QiFC&bE%LIZIRXWMy4=M~8#ir1Asgwdt_?*QI1=IZh1s1f zc1j=B=PJFg)w9m^7TQE^@F_3D!$N=_r=mQF1&?FvWLkku4rVa6=H~jwmNr#WZ)*pxL=#f3VF`)I; zuawJOKYRJ3S&xu=HNZr_>4&_Pjov0<81J^~OUHEegG$CS?f@7$GapV8q6&u`E@#Pwx|_w%tl6G205J{*|ZOk!np z8v_HS$wN4_%DKpiIxiLeN-(c>!xYMT&`D97H93fgzZ%b21ZwH6*CwVJ43`)Ft9$vl>Q+!E~_RBGh;GKf&lO&$n0_%-W>p37h&^if4wvPBuiWbzS zRx&lSzR%v2x|W&bSkbM!WzG+aUlVS%lAqYj^Z<*>N3xJAN3y68Jm2^#F4{wikBW za2C6p*=#M^UdMMtt+Pv;E?geukZlgvzR6Q+b=n26g-Zh%*t%$G?1%%*h zBuYk%H{=_FP!&rhz|Kf7bi^S%Q<;{co1RSso?q7iyAHD z5ii9G`29xoOO>_!T1rNBM+b34rqySo$--jI2!&&yfr#R+R zH&>9QwNf2*Yaut>tBlgeXV59sDU{0{Yz~KfP%(p4Jocd5s3cSQa(R(rw)U)2I|#@= z9}EMNF4QeZva#c1Ow}}iQweOO%QJ2=870PhO4k=B=1zBfavRT3b1jXC#Ak~2^AV(@ zbq-=b;Nm#=4L#Juq(R;U`&1G~hV0EhwWbr?e|t0^1SCFkvs%59(4X=p^r-t{^h5mT zLq0H1WQ==R#*^Kb;WH(KQe34}rRCH5e>GSPA06xChwwqZewqE;+=IG?(2WdzD6)8o z)4c~gZ*%p9oLjn2-r5=J@a!8kq<>z%&z)YpIdw5B)yua99K;lwk4$p2-<6GZV&=1N z3QMS)P#7N=QEH4)Oy$o#aFC245~9M%j^PxWRezeXr|EY@qjeh6RpJugo?1~TX#w`6 zER)8Co$NM1Vv2*w=Ej-{DYEJsoJ*&*rUGVm$rk+rYHg!jbtoF4wKNj1a({|3d1EQe zK`V0&0j7ve_xO9m4#~`WfzE?pf(^CV9=v*Rd#6BE+f)BLk!H<$H9>&K;7s$LO;&oq z9MS|`jm$N_kdqIe_qy>J=RUrzn*E>jSR~i*KtG-rd%WvHh<=1^vKwj;vJB^tA2INT z;bsbz0+;VhH9Z(368p=!u4*0dMjljA+`TNYxbVI~C)=g7!&TCwdwf^7&R}_zUz_e=E3-Q9+rlcgBB3r- z+OcZIRRZ;aI?sC9DhF;#0Bo|RH}hS@Dx69YviUO2t7oN2RTf z0>FtC`yZ8Qe>5Ibd{8XCmBfM!g-s=!c1L{NF5P8C}oz~L|%ct5nq7r6ij1jYS zj(3c`Np))Z=5$78tawpJ6b8w@x7?0)s%UIn_0Q>)Ng0$AZW-D!aLZocZ*HMPISe}% zs>^tRPV|D6)RUhKO4!M>qVf^~V#DU$mL}j$@lgya9-%Sz?)np)zLcY4pBGZfT|t>+ zI<9_F;Y&GN=T}#y2b{##3WD(A+!EvhsoM~RmMwQI&rH!gm!OxJ1H@Pz`<)bwAWo?D z82E~k^DUga6wdHMo{Z}mmq%9lZGIOt4B(Z&%!Qc2*K2mInNeA81lBI|v4Ki>NM5JV zK?!;3k1G*4yak{#ZuZirEUtVRfaM;lEc;*ZX=tJ~QK)vPPi*YZh$vs3PW*mD*K(;9 z+H}T`d<*INxVtoH>hLUt7bTI;LYplsakYvIg>xS5Te7k`4VCs+8_Ons8nAh*#3a|& zj0%)0lKVE_PO&)U?30^7-eN`xdZD!Hf%8UvuK`+*jjBm$XT!LN!+BCRuf~N4j>2hH z2D5qAJ46Vudqk|1fpT2rYOb+sh}1D09-6{eKEk(t35pdul%LEIb3~Y?-r+BCrq4(x zbJ*xBS3l`LuFqbidU24ItOE63rpqu2Y<+1-X9lV&sal%y#G$H|HuHmZiqtnfG89Y3Ci%x7!K^M4R-m^kw8ux-#og|X$yNWlr!am`kK ztD2An>=y1EZT|IpRhJ&2sVwRrgs5@Rs+BD?U9GxWjPL*%OV0E(`SOFZNv+S`IP4^3 z--F&naDUgMQU0z)#(^m-Trt#xemk?Ph^WqC-_r!wZq8V0dY?+Le3A=8efYAa%9WRS zNdS;}C#0f6>`1DmY#+~30-*k?rTGg`1H^~Xh+yY+Cz?z!;3O@%bEG9@9JKW*ws&ca5o&-kH*{q2t4JVv zTm!CJpIcMOuRH`i-+J&{9+ukDh$m*pW+!^sYC}EJW>pkWovtlJ>Bn5M1u;+F)UZ&w z#D@>Y2v^(piB(YT^*19h9fFO~jTua-c1KlNNBh9(_Sf%gQ?0jY@-X&t*E8f z6uzlgwmI zCI?Uw_HXU_>`w@*@^+d4-CIq*%k^kq6dSVE?>^c$IAxD*<^{yWX3Hpk|6y-?Xx!CM zXN6GpW}~8yh!EsJJaThd{T7N+-!n=>r_m!zJ0hfXJRpUs8;L8A_gLI@%|uj9RP10; zC}`7fxV%K$R|373ned+p)9WS`wM}tn)Atn1zj>=#T*Uz+LK=h0u_sCVW9x>X(jx%0 z^^w_jHj~kxIE+&Ls+SFy=g=fuZ`>j6c9r?^maUiJt)iZ0wu#!LaUVhutR`HR0iUJE zG79J|f~a&h7L)Fk*bZoIg70iaUmKga+Ie@7*U&B>+$Kn*JPeVTCfUOIiU;zQ9zde@!;f?b z^^|?>MOb`NB1DwyaHB1ga`4#)R+%5ll?u`XHsY9@2UfYNv{WcB|Gvnaf|D5!wo^tS zQ{jNblqC0f`BI$3oZ!KsMM8k!t`*#Aai_(Lwm^YW+CC@m z8E5aa|Af6pJ|y2Vvc||iRQ!a zyjTB=B{JA5R#QkZc-b?g+7rzD&++;h<1vjhK^@3#fH$pmn^_d8ZO8E=C}WY<8#kR? zotia;ZZxiCUWN;fE_d|gGi-9Aw5SlX>{2&c5ng^^2)Mc3B!#ZotSPvWkhsqWT~TZ4 z54#iAn)9`h@R?_Q<2D7xxrMm3IBm~s#?UjPXcc`9kxGOsO1ZTAVe4$n^3)??d@=A1P zV9|@wrUg+qJJ*%Js-m|st6t9XY8w;dsun)f*u|29bgsBQQ(rr?WL(W!Y%vFCkS^%( zIRe#En$|J#`^kn>q~L@Tr8ppo@i^Ga(rsTHf92rN&r<0uU({xBO_INA4(ciYyBAm^ z$fn9Y?qR5p0TWWXZ5ixWx~D64AJIY3bOh=)?h)&YR8T1sI?;JW~20DVnp%8VcExqR(m=blP$J^IAqF_8rfE! zpl;TIz*SCIbZF4}n9BAt-rCTJL0)}wXB$c^BT{}eHT`WYipapqszo-akm5AY43?6N zO1X~y(w$r6yp%Yy>EU>$Hz>K_ffRMvb!w8^(wc#DcPrj*H{xEtCCs@X=BoOdluwY1xEyPP z_Y6<7h#fQ{@}$IwCseSmK{CHe?%r0{i6f<|E zT|r$YJwrC_Z;=w1r5Br;7v11H>4)h9E-~j#B%f?J8Cg1c>syYVr6dNsdj!YW>7kzAjUB#*}QyuduK2Zb#*qX zmV{|%kn7BB(iBY}&l%}l*k$XI(}HN{f_BewJ!y}~UazkIB|c5|(p+CFAN${uo}-?oC-)fa2o&gRZO|E)LdR*YXV~AF5|)JK z!ZZtq&P!7`;tYiQrl@$pdWm2N1aq9Tpf3r)Jl;}bBXGd%ANPZmI=8Cadlv4qxEmu+ zpO(10d8~h9>_OW??3s-7;}8_@K%b^dYSVOaIa8^HH+w<6)Yc?DFS^#N?X)H??N}F& zJ<88%INkl^We#Ri>Org&2y%lQv2M8{xkMP~tiQZhvguuFmIt%McXq6aO-q}3QO(tazeh>Y*sHWKD z*oHgiNA%=9qa|ol_9V4U!|8`*dzrRe7Z7)ls`luQTkiYU_6g~4l({}_%HbIi@rtnq z5d6p_tI;1+G$8B@IN=Ta7dCX)#b6K3t%o<<3Xw*}6CS3uPz%qVLH}O0uXtx$`mc+c zHLOn=l(!P1-3373lRTEGn|n@2Oyy@8hp@_V8qO>dT_^HTU8Aj`w%hdEl1R5vG39zm zI-c9->wRLh+OWBtI@U>aPJ#q2NP`7&Qmg5~;e_GqQZ5mk+ddnRia=mwrp#3=I;(+X zdHJau2Mh8_$+O)81+LdFn~a3-oQp?~B3h{R4u!Oe(oUuwP;QdF)(s>2*20!V-LqAk z<>eJcZ***3Z1s1O)52!6K-vX1=7@MsBPbz&@Q1Z<#BUur&z`3`j!($c(N@AHk$D!| zQ{j>}-1&#{3b{Zhgkd-?PFp56rMdoe!L~6#3m5QQv&QLP61C&i%(Mgjy?qT8r}?J6 zQN5`U`c5pMIlL2g&GA(sC(s4*beF!CHi>Ygz#0Ad1f$jE0<3oylBG% z6AtE=fS&&RrfxUMa3g6b1Bc;XM@?rL^BR#lO)F(gOFj+yaQIl-I21BjWG!>$-9Z`@u-FGZ#(A^-8kt!*{+Z{IipBBaQASE_0hV ztB2P7>V;>7jc1%f=xW?hB1XJ9>g@%%%Go7D!mvdgVx9nBLS@T)u@a$P=6S88@m_M& z5kI|_rQ1bKcGpODe9(k=*YsdJEn!e{6I4|&_USZIhAe%)qGZhLs&@1UEq^*p*YL)#X& z6-nFVfs@Ldcgn4PG4Lrhja)=(4u_EwNSqo$prNsTjiT+{OV6ASOz&zm8DbRa)VRs2 z5&h0KE>&ymcb%VfE5Cqo-1uDaWmBQ_@!g)O6X9et5W0%YyVd&q8}^7alzuvq56lzo z*LfO(sKc;_HISt2Hnzj94!bsLofFfC@XPnE{NPfl|k4N!x#}?7g0-rzgw=XsR zST>0N+hts9)`DD2c~)UKGv3*tCaaEDBrUW{w@pL~p&iT(lvUWurzn+HIc7IP-Sjqv zj*+iF3;z9PT)(s3FNmbD*Rrz8$}<=%6(0|*=#9)lj@#r?IM0qdUmQSew5%=`R#M#r zL}eyuN2zcIn%6}j)6gO-(eeVChdBGsJYcsYa66LPX!fyyE_z$i^RM5oL92$HSC~L` zLI)Brzn2FwLc8Q0djT4@(nI#~Q}imR#gGFTn|Ib*Xm19#^*?YnTVHo)_>;ko@WmUp zx4eHoK2H6Eh&3!fB70;KogkR!6BqeR((3rzs@2CCtdFz%qJqK>!9>&I15+WDH(lF4-!K80){2WxqLJ+h(`#*rE0&dv_Xvq=7K6ycM|!_D z1JrKYC~$T~U*l9}wg2C}yXuE$<=iZtYB6427rp351}&Pjil#}og3$X0=Gssz8`-a^ zac@mW=;+v8O;Q7PCc35^P`()i{LqKOznOP13iLBlgi2g_zAWv^P%44;~2 zT8wPnzQe>vf4<#rbJI_ufmUN=fetUFtn0LzHv9rs6R?O~@v%tYc z^TkUxKjXZx>6}mjLr|vBa%-L+9!d^gW-#diW&d3e@HQkohyb=CKf#u5rlh?cENqTN zSi|DIh0;sw1kDE=8Kw2E?|G8Cz#20v*JW(zOkY8jf6Ej%;aZ3Z+N-J+d)E!`j@#T| zEw?r5yajKj>r(3L9R5;-L z0{ySbk00aTTTVa>T|>uM>!J0y!SdgL8!eaf#kLn-&A{R#h?=CUI$bL7l2K4*?$=Gn zLrE{Umw<9Z$k^Jm0V!z*P}xu=^W#(ZTo$x#Fin^2R(b z>E9V!Uh;7Ua$Gam~ku=$eekYo&8c9RG-Cp>sk^{EY$0n5D zdUikGfq_mq?uFgcqW;8L8c@INgBXu$VjeDrj)E+HMT@!hl36mNmUlKr9fFxF!=TJSXz;2{f|-c`KR zsh{VQxKys|N|_3zD>U@v@dEQZbG4lV&2Hd>IHey27@KFDDf&1G+EYmG2oT8y=*@w( zRe?Rj9CF9`ARd^_@+H?#I6`}oi9R8qXMD2FI+Z6l1;U*;25D;)*<87|eb)n9Dw0;W zY8LNW?UuA4)frKDYZNVVo%PZzFQlx)EMcb_2wdc60ISo>@R%>fzFd_JE-4`4Spo3*W_3?|rLziWu?FGh`;Nb#MTE*!e2WV=V zXx|iJ;ju!z5oUP7T)K`@q$t;>gZg}unV*ak(`3=~;!TDdMF+$_9|tPeS+qB+cNqoZ zKugX&K^I4V?{pee{MF;x} z+L{nP>Kgii;9OZ9Q*%{GK^OQzhi@gCG%y+5mU3|WOIVP);fK6?Zz28V=+xojO8b2Li$aUEd%vkM;Oju2i$e!wDaLxMGW*+2Au1#!C zrNuq6fgoGsV75GG0s>iB_8Nk0w65G3(5f%^$!yJh%!nc%X>azpkTcUc9I3>rQkExt zIZh>RjUUfaw~XA*U#7LbhmiQ{{bYI0w%flVrBmB1yc4)@3tAC28j#A{>hN-1ma_5y zT1-KTYkBl0^N;&8L+$zW3w)qy=4+%=ZOaXh#43`7%c)YDPi)fsqK`GJL&+W0hCbVpE_a!lFze-EF?=PHdxTc~4~CL}y$q z0{q4^i*`xo4ijI26r^xSkVPC`s4M^85X!ueq54d9g>^8;m2qr4N zm7$#`@ttaxY*=<6PV{=IK`0qrw z%d?G2dX@0qSQ!|x;ZM?qa$6q5Z0ux7r-*7uZJrjv-N~6|xYnWQa<0i7C!*qy{^8;M z!W}GmsR~*S@-H*Q+1I_N^=d1KbMA0kg7OK>8gp>7tS6Xs#;nbOYG`JLIp?2PIKXN4vD85Z;*G`0cVL~nX*Gj-MwTa^=Pl~tlFBFX-eBx!*}S0aN9@bAlDD}8j+|TYRrF54T!Y&d;8R!aZbnlCUL(# z=2_j?`lh(6Yi1*CphPe@4(jYqdw6E?mS_1?zhA$7bOBf%gla-eSwLS-Ng* zTKnlC4wWZ*-Pr;8kA2YLErzluykx$FtJfY5$^*eH1ira2;wnFv-)|oc{AN+N_~xp|D2gVgHAEJ zG`PxSVaTRmY0s2wqZ*t-48~8Rco4_JrEs1t?_ZIH;orFq_jql3GILXNGXvZ*_mS30 zf^_O3besz|bb97l7~u&(P4iqa=K96gkc54SK8~U3#IAJ@x3KFXA`{*%$iCRHq5xY5 zVI+xoY=EhyUL@qX)_!cQJ^&zx38}sS;Gov4bkcLL2)44N7dcTePNXHoK{4q;cv^gMgxT@r^==YXP)*-+7quqT*Qk(lD|~3s`-k6 z5RuV%c+9#)(7YV!cbMUTG!Y6fKxoLNmjByzArJpt_JWPYoVm1%uCdz3Ry$OOh)X98 z8Q)5JvR?g-iT?yaxHS-Ua>L5Chtk-`4)<@8sLjLr(Fp;rH zKlV@I9g)R@&Nn98kS zWoQ1#8!u@l%h#qyKYRy$D>eWSxLtH8&{c3mbbMKyhoC}F}0?Io^5fXrxqby8d|ZTXf{zb?%VLM{3Q zX98+RK27#0oft82Jbe6S0kcxf6jI(}R#Lngl^C6gB03NzEB0z=iWui#Ww-RryD#TG zYY*auv>%bD+>bRE$#aQ8cPcTT5jhUrbXrqR`6C5fAKF~b)DL{DxkX5x!u_-=vp-YT zDJgOgRw&Ue={KAQ5E2`tdZX~&PAaEn8+k@o>cZEAd*O!c)#`>%=6o?&$~=YHCfQsimgR6SRG1M8MunE_;@Pfb@d z>rhdsvYgHW!Z~jpZ;{057R=6NhJ*VZkFT=1M!yBj%;%^Ci)7YzBdkTtG)H}|)+P~> zHom3y&=r{A$|0HTv+ZVb4e~^Fl$1XX^>|(|+TcQTc+xEbC~9HCRmn?DI;-W^o*!i)=wK-$84E zGlMRD?E9-usJNd@b`fZ?)mDY*+#~@uIdxd|fYSihn1p)y;%30rGhAATin13b<+6p5 zfBqmtSZR5p*CXrdl4h&8T)3+2m=5Agk-3Xh^cSDCk|3^m3VIr}Mo=s@UCa()mzeuY zy4X4Q&_9D^E{I?jvEoI}gn_4nxldYz9fP4-<)u!w)Q-gTTR&iolQs?C z_3olxY#3vAW}XCI(y*}I?LAfw(;d(-rRyR^Ix+X7H__KS(U%d9EZ`EX0c;tc)EITW8wR|<+kLZ z?Nh%33SO9EqW(0LkveScw9^_-M%NG@_Y z32+cLb9B}oK;ZYa8slb&MWR%!czrJ?g>%odyQih=0_xO9jBpnpSJv zXt9VJ8kD0{Ig*@Ka6H!b)Hh>`Og}}&ykFKORxY!PN|L}OTg&IKCL@ipAP5hfkKX*s zciI&}Fg-el;YOL1A6LLO-l9$wZP`K6rLqoE1i<8mB4hA5CD!jfGG>ndnZxw^-DVdQ z=UC;fzZMH{q&o2_%sFEtsNz&E;zsWr};TS_+)G*5=A3w4`Rkp(;&X9QkB^=u>x!s zRgB0D6`Yie*IMx46qFQZo`;dkRE`FhFWc}M?FG$C%uCMJOZRPxE3JF-enio(w`ps> z0-V-OqZV)~ zF}t+(HD9ZpNU;pE-yQUCrPyd|ONxO?qoseEnT5u{><>9Hfk;|FCA+9{Nl0zX z>pa!BVY1q$?rfpy-V^gi)o|0GU^b7@B?;xdkEEehtO7(p ze!nVRQQm-<7tNvSoT2=gL9~jXoOdq$QR#hT7qkyDseTS2oKGaIF#bSIWG`8z8|b-v z2`8Bds5IbVOkm~*X;}h+pqh*+uKj4QRY~uZEaxJTo-QyvF-gJ*yB%Vu82GjM@DEvz zNMQ{xWc5tbu5O`zbdslN1{}=(1G&rqFU3-q^I_+!cPBb6J`pHP(dkclu}>)y=&&^6 zO}`qrl-zCHnx6g%8sY#`w8L(=9@xb=@!sm>Ekz;}mK3RcQ_9u9v^GvPW_hf*Y2`H8 zdzxQarjDH9;Tq^>)3l(QmWNz8IOlRuWNMmYID`A$a<9+~o=Zjd0`N@sxIFuMN23Jx zbG_$j_EMf|>f9dX;sJdPb57*-t|WCjW|OV&#Kp5T$Xw58AMFjBVCo0*6L7T8(o<5!sjkRT2{p+Z!|9UJJ2& z4aKmWV(%G^LKNV0G_DdZ!RbcKxI^QSQ5pTuLwyz>SCv1;Dj>~_cd)uC$ibh}fCit6Yoe_RP~ zO-va~Df>TRVPT)83b^|D`q+(2uQJC~VhN)8%D|LLJQjbz1S(S0mAD<_<)gZ*icq1r)67yz5FaIL6=*_bqx7Z>6hrgM?w*CH#sEcAD!meSeLbH)7h?6_` z{n{%Tnv68>Io8uc6vP!beFM3>P+MM6zcjMBb5p&bQt(MMG&dd8aoD=SMXu3p>k};C zfj-k7KxNmV`7B0-lDtHFXeVcnjV_*QnF%z~s8#4H_@2Bm?<>PHQ>-w}v<_96QDoa6 zE-0TRcPA$`qXm3c7#C6kG32_N3PBhT3uJ;4tQq)hWt*Sr`x2m%Ss+`53(Y8EQ^;Ee)KMXi`}~jK zGE}Aq(mD(?b3(uBBYy-wZ$`(qLeY_p4c=hzZ8sg$`gq>1IcKT96?sJ|hpFO)_Oynu z+lmiE4?L0}r?s<=k|1l5h_R`P2W)iG(rSrfAz~Xdkfte}<*{xkJ|Qe5jxlFNwH&c3 zmi~LOL1g9dy@nJzb^0G5xlkZb5&V!yEGRA(s#UcpQk2T;QjWk|08gP)i6cy)8<&Ay z(6Qz>T-U>Tk`vZ})ty?gG*s>Tn^>%)iWX*ATFud|4wae|;Q=WF<%xCvAMmH4Hp6uE z%~JTJ-1WX>L;K}|+;Zt|`Aj_>!~m|Ba;k-f!pNmFn})Tn%DsK`hjne1N;30&JtGwq z?HdO0XWb5*Bx5ddhfR8=Fh0c$iT7@kM0~|HSG6mm=l^_D6`vC!m+F!}8k1Jrsa*c< z3C?)O#A3{D+lxWV>n4U{n1_@Ai*Z=ty+PVE}2W=`Yov znm_)&A9}OcQ?@mS0%Ls}nseS*Hz|KHl^AZCBPWoVi7-$M1H|6_Si2*AmUlhBS(rpM zQPlM9a}&AoX9j_NIw8+O+-R+GHGyC8xJvSFj}+&t{xTTFd)d@ylyuX&sBcshQ51u> zi&%Bq27tS2XC6Scq(%^zl^{r{@jp3l+G;^f4Ma8hM*!ya)sH13bwd6Z@p>u{?R+kX8!cZ@@3YCYaWCPoRo~_Lva6aiP5jJ?nTYE7 z@<1hyYd+8&i;jg^QaqJGUH`jZ`Nf;B!CFNUQ_br8q`aACX6Z@$c)U>;#Eq6R=7PYg z;8VBn!sK82Ou*Qpc;~+pgJmX;8LNB)&B_-pdhB3^xq}p;b~!pYELs9EOr22xD>cLp zsFMz=i3!^p*On>VE$dz|k8^4ci45sCOp6pw%Jl(d(Slsxuj}RLgnY=a3HQmS_V`>Q zX|!3Ml_LL)`zWPy*3rG>sV2>?9>InFiz%AMWV{n`byL;A6rW%|$=nQcTIMXZYT)IP z&wP0ArvQ46fL2R0$=7O0;3-;r8&jqC{a%f-g28XI$`X{u(J7?^7K(G2M`*RegW_Ms z)$4yU+buH@F4rI4V$gfLu(*>> zhbD>fu*b7_2O!4e4NQQPw9{#Z5>2U-=@#}8cm(X3Ncm<46lN&Yn7q@2or~oN7?p%3 zL#V%#H-Z~!_$7?Y843q$2?-;H&%N<40{o*oWqYFL%yLGTTZuZ>m&3^r<*@ z??jc!L@HC#DW0jyuZhrrA!`Evek|9-6qY;2$2jrajaT(?AK+LsPD^S^5#u&AK}%CriaPpv z#7nL@nL(L7y6W3xv>m&Gvd}Ir9e<80ow-#tFmN_sDD<}|Eo}zYZn>XspuF7C4T&og zx-l#*_+e&v<9b3sw8@q%sPaP)l@1bQ*_taGowwqE)5A5$^DUG zR6ZuBKNOc6%J9+~?vYI#3_!#|V;MEomYxfvmkb(n&fIe(QrPV4)F;=Dk)#?or45ys zy``Z_tPPH0XnEL=ok`#IrZeF3YI?>k@^6|yQy4YfqV%Y^QrC4`zzIi4+Oi>pxE!G{ zxp|0>;+%3}O{8W`!V*k&$-po-Rmw56e4N|K8TC$5*PHrmA%v%Ju9V}?Pfx)8Wuojc zlC9h_$3D3tp&ZN^5ueOrfmlc>7M=)v{;&TnC6W5IJm3{fB6c;7vdG?p<+ zZ5sa+YUN*5dW|?_uh;cLDc-S$JlQLz3IxsdO|w>02~Y?UbC(wmf2{p#8tosGDPh2h zQsk%pqxh?Xoz2id9t>fy7#B2hrp~HVM3g@%yA4?J8d&NKntXHWmCZ@gkDS3S&(KCMgw5!MITHoxDp|4fO6ZG?{Gr1BmeS&&KFH=Hvv< zf~Xx=6u-{8qoz!!Xa+yn5kbP*MD0YL$Pme}7hh~~RCzMidDPK=tTg08yM$Qz_3`7- zj?z#b$X3+prm=ghm7sUFr8Eb_MZ}VK%IlR$US`RsPCaCIxOfSfK5t1r*!a^KQjo9F`Z=?$1(5#6CG{OvQSCZok;i)S&67#)U)|p2d`s_)ohT_t zn0kn81QZI#al=`}C?A%5N~1fHEJ|BrCPpB)Zaf`3_uBVx`3+ zZixn6S_)tn{mp9%e&%wu&qz>w8%Coo;9&~D_iaD=^y5zVLcA6B&-XGDv^Pl(v7H=gmCbgbG>rfoR zZ?MxNlXf+%$K# z&nR)tZMN;07C9{0lyhF+uY{WzDE_*If5sVYPG*<+7%fl%F*Y@Y^IiWlYx$De@=b-G z{PgF<>8hde986yv)fWb2zEg!;1p#+cH~+)G z4wdt#Xza!R;_+_dkYXvf-M)c(C7Lv~IQiy%afwa15P2mxIu)a&s3f6xHVO}zilm;} z?;kR+{UJ%uv|5P`=znv}s+kI!$2wIA>E;sP!>*j7l1z!~FIG7WA{D+7Nb|H#Z&)}= z7Olw!5%o?k}+vI#onQw#< zKh^sgYdt(V7W|gm1QCQe7&16*IRJCGHJlW%k;U{q{7^kiQ{U~+TrK0P)07nI?CG08 z1KObrPD<0fd_%g^*eB?`41Fz2v-vVPimYQvN5d{rf~?YF>jd@JxEWCK=2aJ4L^?N= z8EZx-a0aa3$4hm z=gs3xdZW^FmnPr%)z_TbR$>I5=Gf=P~ib6FBLnt*s8Psv$W&-W1x zD2zIMeD!g?etT~~Q}|sG2jt!1?-ij`Psh<#NH5O}6?(@Lsn7G(AE*kP{E(4}JC4e! zpXkCXl?~ZHXG89?oSLju9;wWJwYLh<5j?4KrK>v-uA>9DR`~Z0PD7iJMj(ljc6F~ZJ(LND@j<;t;`0KFVI%i#cSz~V2$j| zwTu=#sY-X?_cADewpl!zIxx~R?08*NA`#edrcdAS`8&mqa*TRz#*6uYw$$zu#_d9i zlY06X<(B>J505`ye(N5N{a4)TzCTT_g1NUJ2>zwTbn8w2B&w0=-;wWLY3`_};V^Pk zv-7;sv-S=KVYw8RSvIp&_B}tOJQDE2W-Em%`}Pz>tN^p&uKI4S&qi+r72B1x*X2XY z%?9iSzVq|BB56EM_6W0!N~HCg7*1q3YL#IF+`T%ot3PeK)XSw&_A zU>EeY0J5d5N2)XiUl#!|=8=7`^SV#uy-WT=qr?i|lw=-rTI8gRo9kjSY_=~2H@v@VT<(hTY3pNCvb%N@c8Sb?EIPM)jHKX}oCr+m|;dpkB~VCcL`ImSN7qJkCIKB8P>XSbyTHMr+z0>l6*=D^}dvI zi!^Y^0l>h>iK*25j3-c=j;?@ed!`GmhEw`@b>yGukW54n|74~TXjbK>&(V(N`x~;Q zucxFc0dIeuh5CldHoP6!eoT_II{eNY`2I@`rn)+K@m#(hd*AJ8;5*l|3->20loIq( zFy#3{-55V7M+h;LZ@$7pswVCFTMkieMTa16M0s?s!#huoBX`)HaTG9}sk}3ZQo@GR z*|*~zcy|jsSjtT7fH#|i3d&^hMDYmVU)LV9_Wi)xr!eTRgoV^?35i;Z8e%l3X0y0U zyijk$i*1<5t(=Q{J^&Q`oN+klee&<<8@4pLz?{D%7{F%b4kAjhH~MEy(F^WNGnMIq zppeH;0fCdva(cf#$N#bX2e`+yy#CpZJYDOKZ0pMhPOYo_QZq$fv-)a8DMoz~hf-BX zRlPm+(6Dx{dD7&ppB2J{Q=Or6qM($Qt8VZU!sX(%-YvYzB;__N%L1UX`KA4pooiz| z{rs9NN1MUKMSGKOAtFbMfQ(H}B9$aBfb@U=fNURx%^ih&^m_&0${ZfGrfW3mGKvp5eF&+sQ}V?#J7O>gq z$`)r-1FbXY66Fa~QE4{3`w;P3j&%YOrge?Qu0N$yI@9cf1l_`#o|nt#7<*r(8Ecn8 z5W3ctg-#P&J1-rw6qR&M9Ttoj75Zb|OR8@$Lo2rzicP;uT{7yjQ^u*rduA?>>c96a zD~{JdLt={D9E*~xdMTp#Lv;UjpP7hm)35%RIY}*Fjsg#~#83OKbq;kWDoXu&B^<3t zV%F*DTIbc*pYO%uCD~cpi0e_KLI|AvfSVB?Kgjm72-)w}V6s$|vQv@$_|S7a8LZzo ztif@uu8*QSFs;gs=1gF_Y;S;?1@7F#rbh^b{=%FJ%VzTxmmq_=Ko9~~gZ}0N9KMRP zI0s{EPP|I{WtZ=i>!-+g?k>th5IR96PDses{DzkDh;za31u6S0-*8e$U=@R&tnsjm z&$hnz#jM+49ox28F219s-p4@26rSDB<*|jMJ~@Kl)bgh{)2BK~bu*nmj@U-6^H`2$ zMK4HBYFfFl#AQ~)=mbdxbalEMruryA+#br-Ki&*}ZgXN)Ja5|LyRmq+m^l}I#J7z1 zj-H-h!Hen)P-sx^ipVp4Cv{(VMOU19x!u%WW1J7c1ik1KFIna7_t4W#ZI%CXz(B)@ z5ffsr)sJ8P1Nar(?vCQhtm@RglsaGezQ5h;^iVt!Fn=bq>~&EepFWp^dFcY z3*;Wb^M7Q`+I2{J4z%Seb6gpBd0QMl`*pUN!Z-xio)&;_OEtd$nNcqJ)lJO7FG(!M z{_TQZklaZi}@R>Y3W?2P;A=3WDuBiCJYofvJW9t(C{h~v2=n}mHYa-**$a_IOEqiNYP#U>0N}B zu<@6?xR3Cm#vH!=8Ut-R8Uj~^^JcVZVEIn2+-}uQV*VT=vO?<77XHU|P#)2bqyoTp z3GL9J27Ph38_iZHS(TZQ!EY6FXi2Nl$etmY!FSsaR@s^hsnZ2y%j*05)tuxZb0sKW zIwb<#DS1NpW?FM(eQr)s``pgGlCWqHtneGVmN7w95Uz>9P0YbidHXkE zR5ou6=~V(v-@7K)Ymv+NyL5~)&dW+ziBAl!H&_;n;^ z{WdSl!tc5Hy_5R~t-UFWsG}s!_)L{oya9SjQv{$hE?QjQ_ly%8g}RSQCGCr z`s-fivG2e;LPu?KWKO=>4cKI517~coAm$wnBX$i_*wE543hrcz19(mtzX;uDj$Q~8 z!Lq8|pNYRqe4b`KZd5W1B@xjS8Y9h6ouOl1w8+um4J{Rz#dOn>3V)i-h(oo4V-~rP}j1;HxW%KB574 zwiKkBCt@S6ldxX1B$&t0)Fq5a)!1QE;3m1r98komIZ4Sz%Arzn^qcM;{!g2y=i^I| zb^Fy}5~F|g(`)SRRu_komZ@a(tB`(;S*su&wOMj+b`N-XnC}D%q6HJ0AmB1ij}ByeLA=^?8-H7D!T+%BD&vd@;MR#UzW9$mm~ zRa{d(fqDo+J0v(v3`5Q0%!TlpziZ`60TzA4MG3^dO69DjaJstu@)xs>Z8!QKAaJuH z>W;$Qn%p8gw_(0O#;M5mdP+n|c@%Wp$xCe}O3f@`+1y}4X~TeB(lycLK&3cmr{_!& zHa7GvA0Di0eQR+YAJXuYwtkaEmzD5p+fS7G1FwYe^3(yaI=?6^?$QYCphZhZT)X)r zOm{Pwtkz#qv|Uiuq&QXvcE8x}_sZyWP-}{k>I$#3g1+vRKJYeCM0J4il7+im^svc( zliu2|NHn5&X@DjLf1)YgH-~OIpf-wGz5z*3+U)q>!_oF~?+VJRfOBrzeEvLM zExAn+U2JvjoUst_2y$pKhb)yDJ6TEH(UcQUealTM32r9!#KA{QvF_XXNFlO z@T4h{>8P*py}jvOG63=HaBW>m-&E)^XLK!%I)$7MX5^pd4?m+alZXWhRnkzey&*i>X!JSG2o>)ayN1IBNOZ9p6e%nM4LP`08ujd;f>et>_eSiAsnsEN_JEPzz z^d9rH7ZUOh%+* zcSS1jvf0dE(o%Iot|)3V9Hv|fp=Y7jGnW2-X?Cpt|8VwJQEfn7v~F;xcyTKZ#VuH| z;80wO6n70SrMNr6p}1Rdr$CY5t_e;k-a?_20`19v&i&6F$OYnS%@&31M@n~8Il-??$4%!K}v?@QiP9ro`w z`6@Hi$n7s64ed!MX%SRHbMW@(&lojsAiMLpaVR}VKC@zqjgaYX2|Ae<2J9(%{fewS zjX%zDn@}6&cwNwgA~X5jkj>L+wuk>Q)km?7N!dEhQoB|@mPv_1uLW-rChEoB+IIJ2 zUMfRXV#1635}#hVSN6@Iy`v{IIv!~mb|?R!BZT~}QC2XPUA;+JvD}6wTX_z}60s2fjqU$}J`n30YE*L~{ZLrL zvFhJZ<8rdM9`k?{_cjw6vx?)RD)|Bi+$6tlT{t*7{*a3yKtMLHdL-8SjXs%nJY9KO zi7Uf)>*QZx1m^+u225??*I-hczHCV#_6gU3#oXP9mOhHMVU|u`rSDm{N^n&H2U@t&q1a8_k)OjOz3GTL;DBHZBkztxnJ|Ux?bCP z8te7}o`xnW=<>>~Jv7z-bpi%P_@(ZQVQ;Dm%fXuc6SA*=+X!W5HV+tdTUr(3wx~Z& z{Iz$zo#Bj~{8o$s#m*cOGO}>+yIL`&uLr{IYp5yDRs#8^steH*xyS*} z-Jo?GaQ^xuRcH6xu^ItyU9UWAL&86kg-ZQ%-hagx2WxW?N zfP}4L?w3qWwslqe=LUOCusip3)~}OB{^9nFNfJ;)rvB1PCE4af2*~e@JKt~pgLxaw z@FmocizTU^Y%URJg_%Cy+BF@D^uLMTXv}Os`mBfn^^7ZJVo(T)?i(IgY#5LCcUBL< z(P(tG;{JO2YV3gl zY46E21u{>`9C-JaeV7Tc2tg}$2iVN|i~7mQ($!)a?0k%r4g=pTzW+%GuzrwEc-IgqjR%^5qUKF`aK3k)9)J81{GQP1 zKEck=_0{#S9Y(wBEB@loX~|Z3N5J0Ak%WNW-)EmX?H8)d$xz>WSkiNkPt z1s-Wscj%gG6D3hw`{WWNS?Hc3E9Db64cWPf?N#9v@%{N~m$+?R1jjo1-*y#Z@=J z@q2EIRWSK`kX4PM1Ng|5D?G*G*I&Hcz3`#PDO8`)Jp*t5E85BTIsHx(1K#lYUV|-e z5zT)1e7tQAsHKwST5lqmwTz8XPg@Ol*5;f!saS-Rvdpipg;ULEOH$mfr~|RFE4u5& z+Hq&EKM5Hcw~Hf7JzvZ9W@w*bJ8cNBwE^06r!+W{eRrNsJ~@ z`PIPUHK)H{o@^EQg)WPyBkc-$-MDbT*OK`5J+p$6 ztF6-)jQFg;bnUUh)Paiet zP|viz!Tw1gkurd%o_eE~#Yl{rKT*YLM|-(w8pDPb1(#jIBZoge(QE6MPr03;V+RO6 zimxAE-fUpqz^I$Cn|eI8<;?KKFSlY?kQ^Fi zGLorje?+sA`)c5W_JX5`iD<=VIiB;0=jCOkIeo%(mH2`7+LL2BB1&~3R>)qIP-MuJ;+?4#vXsKL2=B4cuF-ct_bn|RS~6a)YyWWBF`26t6HD+ zoV>knA8H*Moal!pxPGZ)3bzMQh8F))5bXc#-Y%JuvoGX70k|NhU-&aZ@Pi)qGt`nh zS}pC(MECfIg+hDmP-C+YSN7Y zYuUi2_IEei;6Fgg8G#)GKffcRH<1rY@+bJ9>|}6#Ydcw%7ph%qte4tU4hZnVw7Snr zs+sBQ+rf7ztauycAJElDDE|Qf2R#0C`2rCrXX_k^` zd?P|BnArJl78KIwfZAc3=Y!_+2sz^i)XBpa*{CVw)Xf%gkt}H+~s*&p0 ziQ|{s()W5jE96=>i5#=9!EN@8+-Ze;*b4(uCHi>|x|ec;bEnBZB4WuVF&VgP9!2oOt0$g5AMKAO~bNcP>1K@yn~MO2%1RV*%)gNY;qbKuM{ zV-OBqf6$_vI#vZuCxJi7K$YZJmJJLda@?G3$ud=SP; z6s&!)(qsDrEe9Uhsu(F6`kxk8VEoZNdC}TN8+SO;mObDq`*-?|#>XXw7O2!>IZVTA zmuWFdC!ZMbXmjFJ7`MG#5u~KaPH~4rf6Y1Ge08l{L`U%djuH5SVBoEfo_IYla9`HRbQ>R^wae1=#BqKl_hDli`unY zjJ-0-6g9()8&eWWZGSq)Ic$oCJvk|;+|cl%xc$#vG%AW>EB|HZv`Z)Bq`AVCIupg% zkP~bsErj~Fn1g*xp5K%Qz4hAw;UaVSsS&+}Sx2IgTOmAqek-J2J{ z=`~JI?KB9=@Pk$@V!bG9tCDu1J(Uk(2Z^P`$^Ayrdma?qDX=AW&ue=w8qSmxWKrO~ z?DN;k5zqsm{Krks;X##T#gDX*U-u@m0Zkr5qTg3~41(hhILN3JdA-EPV1IfSOVYx7 z5h3|V#YZ-9NSH2!M!9Gl?~OW>0Q?iltyFr9-oQYSclu#oO9Fa^fW&0KV9}!eu^97Y zjZX@yLN1nND*>5?dy^9^*1=K*udJe8prE?(t@iN|ftrImJD_w$G(sz$;1ZTfXAk)q;`+AU=%jT^17ow#EEXY~ugRvZJfcz4`?YjBU7k zz0urDQw3CVLX5YW8gXoJ|5|r7U1zRh@FoDYdztw=uB(x^%yGk^WOD%hkO`UhC~ zj+9r#y+iwQQYXc;M=2zxT33?Cq+`K#4HWSi3Hi43B%%g>Z%ul@vj^zr0y^YcT9#;D zCUXVSP`k;Mc(T@yK!$;vUUY)Kc zFOE;#E|2~wDUD8xB^#$}UkD{bF8NYswuuF>r!akKP^YtmkhuWigsdtN7<_&A+xH!t zm7Fu3@l?q__b~-_CAUU@iMXnT!ty1q;>+SBlblC%O~AD1`Y$!sPb+9+jBG4^6~3j; z%d|6ZHUjHvfI#y&+S8LPbpm?aMZCKp-o~Q!sG(Z*9b3o(IU3o~tm$V@)^IreeZt9a z#6}p2S8oqfZ}&P*wzM2Gh$t?CXZE>NUm|>}cf0x+9S!AK#Cm=roA!UB2@Ae>9;-bK z9BPTdZhyY{E!`Yxr{nc@&3+t_*;!=TDyAWe!->1@Mo}MxPwR5XWNV!(@!}quGu%G~ zPs@4^5&9taQAkQtOkhISpY9$fU8ih_MI0zsZkvsqP{uS$n5tLYGTwgZfRcaLAspV> zVQs=8zf#Q@)8NJx8T<7p8Q30md7%`et0S6f%E4PGpFl{5&D;02wm32{8IsaBsve~! z6qh1-$Kln@)YpYIwx0wE!N6dq6%kCyF^l3{%`o_r5~o}RmOp|w7P;uG!>u~h2Gl;# z28)m3P>e_ ziK4Sx44WmO{-^mut95Hm@-v&%64=JAGVXQSm-}hXdo?VeY_OGSUn!|&heLI6Y&nT$ zTcVQS%);G63G+LDqErEHc=vPD+;$?|ibtIMA!VF=e6M}<;@(kFTk`v48 zoLdq&>zAKG+E$Y`e>$%55qsf|6DLCQ(e%;!S?UsUD@tZ8y6_-B4FQ2p@pjoQewIBZ zU|X96#i)3aR#m;CE%MMm)COhjIzL&+c)(IVxWX0TrGFW+{>sV*ye1*a^ zsY#~#HGM{Q7cbnj1>u%fgbvN9CAzWWFDzlvRKN`HUtm&`C3QPGK$@L!AX}A`-}u8q z*pRj8iCN;h-}HmqpG7~eCV~6#O-2@29P0J5c0lrNh2AatN3DcYw$hJMXv4;?i%YYx z8Ox-3I*tr`#H(XYrM4W+`mkpKlxf5Bu#bv)%6o@_&^FiYI%Tggb2>I?RfN6Iql-`M zd|_@p08rC9#nD<#wDL_L1u1zc`nhS0r_fbKzCU)p^&ZQ`SxgPx>5APgCRdWtp--bD zr;18!FQDU9;MhpGft-+PiShlfYu`=!b~cY?$}2^B1dnfZV-e!xNw00zP~pl5M;9T< zbCXhNdJO%9G%aCu{E*dmzIOk{vzAv=hw!72mT7y4-<+5nR|K^x%3L}i`DHBAKXQj? z1@GN2=9UvU9;#p|2d59FthQ8rcJKN){yhC?qlwFN<5Z3P6xtRKuKPtOEs~#GJHe$m zW*zC1@aD45rnt#4_#dEN>-FCY>HDo~*0F;u*to>RdOt|arCV8Ex2RWZH-Wu4|L%P= zk7`iE!;HP=vU98SY9gJHKvJF45v^Q$>00w{%ZIsXxgPoEnopwBdPsV|Rjn2y?Ge{W zaaE}T1o5BHUX(*h>x<`*hvtzSxJh3^r!Mpp#E?@addcpdFVyLc&28^VWNz-&S$I-& zNVQ`0n&tLPnZ|;-D;Y|nu{G5XC#sxl06e_m{I=ael zCx{d6JGqihwTXa-Rj*dODW9T<_)B^VW0)Sa%u64qq<8Ob-KeyVvR=``>p7_sUf6PU z!rqo$5r6|0V)#{T>Y3;AG#fC0y2(P{*v0%()Xjvh!nSUg!3eW2scS|OX0Hgh+rQ6V z^d4kubj&eZ*y;JF*@fS_$kS>r#JS(qDn`Fj!2B7S-?Dokl-ep=!IAs*2f$_GzW2)t z%>(>n@ayM>e}K8ema@Y?I0eX0v>yX{{;s%7QcCi@e?{j*z3 zH?ZtkBn3kit;+h>hc;(g>2~?g8n$XVd;$rWWUJzV64=j;A!dB9*bpFy{xK0}609N~l46F4p7f*7jbVK7J_`K&L5k`AiX?Lu4Bh#f5+4X40ArFUZl+8HLkEW)YO+ z4f&nZk*2$g(baDkMR0Hv=hxSBYa2Y=o@I-%>c*&vh>oqdtZXQ`l$_-Si^w0wwz|tq zt+GoM?aU`%8NkMn!C3uA=*KR+=3>M|mjDcrA3(1>(b6ud!so>#-^!-nHuT2T$gX$m zWNuQq=9(2t=kdS=vRgb-KY20UjD0C5kZ_QiF2sr>Oz5?u0dZ3roy`vaL3Ej<{Tpw1 zoHQC-@aH0)5~$H{S-&d3LaM@j3ch zv+~5GFw8IJy8QU%=ea${wb`^;OQNY5k#r$SJ~7q?sNBkfw2NC-#E+J5=}49?_!3-@9lyR=={+Z6M4p)_qlZQitp+$vs~=nj4c0CWlHU z&1{ki!jjjyozyS$)jZP);I!n1GJTYL&r1T;{D7=NFm}oNr78x4wM@~ZS2OvsY{i@S zSSx68Q$N=@%M|m%sp_-|ZBuj|^+ISh*3tLMT#IPqivO^5tVC9ye8{6BFpE$2_5O%O zaee9BZd3Jit(%XOO^jVE(ixF4B zn>i4K9@Lyg00@Lrs$i~6vSs&vu#{fjxqgu7F{Sw2#52~ZQTV&%*0nido3hsM+7*#y zN5WzdGbk;(x*pLNTJK|=UNa9-_OXRx&VGNp~qt8S{u{)esy zkL6YmhtkoJVrx{Et?w*?F4FUovJ*H(LvtCFy7Le4J7U5Ssy|a?>p`vHSeDgW#gd-w zemB`Cp)!zd{V6|woiU6I_F*E}Vvgh&-ROjo>AFx8$46$<%u|ZXcXfAC^KRLW&GOA$ z&x_&B<+56!0y>PXF6RPYgJ7q7`NxrO=bzi_Y4FH}2sb@4nb7pRbiSvoWdB`SgEpqt zui2A0m+9duhvtB}o=R#&6pAZ~+Ja1X;J)^!pAg)FSwUqj>D$BoiY{$cf-7)h#h{kT z3!w~~1=ZW$+A@9WR23b|j}OMXBjVM!X}RiZDW6!s%m=I?T7({X=|S-5*M%X=rIO82!Vn zJvO-lQJHT7RF8uaL7*r?M7Sf`)jN=MJR>JF04@U3&(>uk|+E^%b!WW)z532(9OU-r!D}H%T-JcaqVElQdO4o_gSh4`k zkFN+I7)X7Qno7ONN@noYN6zBJH+9gX^nJkmk*<0Y*6nKI8+v0e<$DkJX{N3*suwcj z8b_CHI~MqQtmR-s(U2&dXVT09AoXirPE9m#xrQ=jh~@qP0fcJ|gY=3xxNxg)#$z`q zH0z7e>+$WUZA56@!=Q(m-`D2*H?_}fh)aeqewbbO#BpF!o`C_^!oavoK&%J`#!R= zGNqYFhCH8Fj!yiCOt7M(5SRNu%~t=l{3fM;-ab)PRCY_1RGpk$br&OB2vn?i=cXOq zV17nLEEWYMY1}kngV*)y2iJ??)Htx9)AxY4cU10!ulz|QN5y>ctn2i=6&~MsHI1s# z(lM@eWL;0=Q8~N{7mCm2ZGWd0AuG?-m+1|Zjuj1F7V}T6HxA8t!FFa%O0rSOEn-b) zALGIB+IA*T1j3b^;ogz}R_=&AlQ62vGlQV(8ZoX5m~Zc>kAF6zAp^Z&`qBifk?LD1 zM;N%EIe8fd)lDns{j{z+>}@D1k_pud*Fe=iAXzla@W-=ioF1Q-ol&l{|9)U^wL>D$ ztD1qhWQOwQk!$R4VRF2n!MX2)EyY^}j7s`XX2eYqQD;z>TMZcHul*DxnKK|)E;k7z zx3QlICexthEujswBQ^4(`IIf0<^pLI?)#I;UL@;9kYXj9Rp^A;Lnd%*0KwxpKEFB9 z<5DQZ)fVIM(A%afsG?1eG(JrPk?U(@{3J5|2jFZ_kjk$Y@?%q+TDfX0Feb&&J5B5n zmXif)SjVh=*cLC*j+>yd9QP8FWVPlWay+TWh#=<5=BWo_kV!rsUJL4w z*1ak-{{I-bh>|A+r%6ci`c7vB%YjEuIbI(4 zaqQ(S`kEtWmdbAmtqdR@ZFwY@?9YMszxyPtj;N`?@U! zjnYu{F0@lVN$!^V6dv2(l+m6W;0*Iz``41p1*ERYOWUQxS>r`Z7d&FtN z=|g-o6D4`&bA(>vO(G3jB~>+uOUK+EO!ZI9SK8x{Oy+hiurD3m29$Ftu(KIqc(z8#iD4JLqKI|a`GCsp0sBz1#mr-*Qb16Zp^Zx-}k{$*Y5A6Qt)CowsH~wzDm!;>azZGo}Zv25=h#b!mioK|ZXkqUc@G(<1ZHUjEeyf5$`3 z9vJ&}h9hH&g)}|^7eyA|<^nS|*VumcyFC{q+hJZ5`&eN1h`Fx&q|tichV{T$+{gJ= z7f&-a_h47T<>?47IWuG)U* z?X%SCtTa$XSVWQIEftu9GhFz&yP)bHpq(BGIhVYSeEsm+{FbAsq`!fX@YVLg1!sN4fdBg?s5P#q+D>ZWTm#2*pqv7X2PnZxd?*T zdz2}rV>TB?NJVpAkl&^mhoO^l9EkimE7WRs>$~UqUi@43hygvdPspd4x$@Utap4Te zYU~Q9lHIi7h^9-DhLG)(4YL7FKI$ovyvt5fZ?bG0t;0fjrM}j2s#-Zh$C4uw+wlmdEW8qCL(Hh7X#kvY`+ou_ z8EmscW%Y7y%TQoLp{0t!0GT847r&)n96{a`4SgJLL?TTI-%M(L$&mf)&u|n|{tvJk z`?DLF2-?o@x8cj&;d_BL;uoEH%3YVgV(HyKf946qeiLj$_Eq-99AJ-|JWQ~L$5Jwf z9fiP$P8NW5ZuN(0)a2t`)!s+N(At4E3R{{%kq(jN-pv|{o-?=bF3vE-)lZRig2GG% z05-UWZlGV-hM|uJQ!UbEa-N{T#elY7BW8TnF+Xv5t8 zK`?Zuqzo==XU~BPF6qCl_>vV=*B>r|<3iW)UEYZqbD?zF*u|InI3_;Cb_U;UyvNab(>4R!+2uC2zVwiK^wUtAdxY z0vY79D;1YzRUq(7(&Z!!?efyd_iB-m4}BEhnLo%LqYtMDjV2R|$HL=Rat68t)ZVy~ z68{4Pk9yG*p z{inuMd={$Bar>M--oS`Br+;<)4)B|MBenKpRB{1m82^=Ws^~os*G(38Y2Lv@NMbj~ z;wux6!iN@mRBx$ov|V|XaV9*shhE)0m~49N&16aF)G zS&_Qr`WqL}SprZ9dy@E(iNQwYKf?M4GGL7|_dzOc*@DgX$dop@s2jM|ToO;FY>q3n z!`FuIor?tA=Z^EoC)rT?l2R(SD=Lq_d1s|RQZPP~)eDh-4U=DSs4_kZf{=>FXv}WN z8r--z#o*KARB2yb(p2S!<(A2D&S8GU3)uN(;zq@>sq_|RgXB>N^S&U7B{5TIC-`)sqniw@59UX1=q@|~)f!ii-UhMEV@X-$h9F6N3WNDA_s8C4B zWTy2}qZX!ya834vf;HzpqZkUlXq>0#k`pHbjObzZVh-}5QuMp|cOAmiZ4KPFrj=V~ z^}oSNK0~G%qh@d~;Y6_{5DncLfCOgg1}%MCewS&F)E85|%-Z);z-10wY=zr7F7|nH zSBPKOO|IhDs?6{ua>(rxD@d8=?fk8D`7RT^XZisnMA)#wRk2IJ)y%RXZUohFnys)* z5mSPAQ|+h(kBpF3{4M&`OYwdE?QR2DnYBjShY57>sVz#~oEb$C;|yWx54YSaE8UggHxKIsRY%r=uaG zQjr^Z0XdDfWr$biyyHrupag`^xV-dFJz{+$kS@7mmd21ZC18w*2;~6d*WS-#=ec?3 z#@&tvDdhI_&&+Kj>ZzKHrQ{Mb^Ws5CVR#Wb{$BEESV@(fICfeu21U;R-NukEZdk`_ zQBtqK2(CH;4f!hjVGA4n0;C1)i z9t{0~oD8+3t92COqkX3kE8{4{E56sM1PGxN zx9OgI&C(RV4B&efcdj>{yKR5iuqt!oV zthG-{Hyc|P@JABXmE@#3mJ+lz}e^Lo21XFHHk5m}0Ld?sVmj?%Q)d6PEO(15Dm zX5dI&|8ff<(+~m{z$Y(gH!(u;`O_S?F>CUCXIMbO%e=^#W8$AgH!rO14lFt{5+TVx zeN>d26x>X-a#7S!MnQi-A^gh}mB zq0|6UtAOe#%bv1ufHU8+pv8g(dEsBuLJ|JW#nR?RZ|VDeYMM@AUFz{EWp%XU5(%^; z{}Z>ZobZ=_Mlx&u;XU=r&X}q}%dy%_AjO?hqsp*CvqfZD^GYKLZ?2Ge0hU&7yFnY6 zisGCr!CHR>s2pGO;fuS`o4xMDS8Gd#9R0xD8(x8wPwDny9ZpQbt?3A75aBxw~>%l&lvb@i-i6`8i3>U z9%KR1{aQ!(1-_j}E{M_%6%+UMK9XHKkH6a8FY$CbI)^IIZ(oW}9;nW#V0dz8L{2Ri< zfv2(&=|}hD6M=p$nXi0Wy9y|As%DY|=qC}D{NXeCLpGRURzv{6MKeZ{&0L_|0CTo~L$(T?a(S_MeKT7=nV+*|=yYWoL|K!>A@tGV4Zp$Nr7K z$iK#Bx|KxR*2Ico6_-Q`zbP6+aZmo_Z)vuF5o8-#>-ZQ1FcjsBV-D>P&0lSAk86p3 z^-9AC+O)l6o-Ohv)h@FHYG8^Ek}P}4AS-4P7rJXEvw^^I@Kk%TfJt{*Xp^xdWyz8(eozv zeC&#Npq@rF4xCOA-t@83%2a`y)~$d@zl|Q$cyLbUJClq7n&15Whfu`NR%s6{Q>Ku>B>{u})!rvGHeXM?#ZdNl6-yRdrc8N^Tf{-3v0t zE%Bp77tfyreq`DKHz~F}^exf+Qth1IanIawh?mD?pEoR!jD1_=Z}{3&&~4ppTbNjt z1YDJei^CgH$K1<>bYrU=Yw9DfYM)hAd2AdF9RNu#mNxyu-b%pp+0|+^$K_b<=|vY^ zZvJZ0$`(!m^giOJpTPl{a&SFJp3MOA;T zGS3Z64vnUnFMTphrkr_M4dltbp;4AFNq?ApNn;AM5R2qUZJt&hwwAjt1TXLd1MHj%i-6?PE=81T$C!@}Yfxm&?b;ww&DtcKZ6&60xM7kyZ5#o6B~Y zo}1IYgbA1HihE_;C9T$&Oi<*dh%AakC@AYOBDD9tn&b9=D$IKIuCtN`3R>;enw8%& zF4|*jwnX<#4!9<^GVc4RAcVtZH9^>z2w>QIvmf-fQ{6KA?3l+btgfR}gxkqHt>o3s zFtz?>nW8Ew+fzfXUfbEJMlyX}!5Ze$IChl?lk+Q;?yXjWv!!QMQ|YDeR@oge9&iE# zy8&{S66o-PUYwt-#IBIXI}QLyb%=3h@PK8_|K??e-nC~X@DbWeB_`QaSD2K%ey5@- zPyuaGT{>l9&UbFoqpU=3Lj!#Nvc;an5=Z|7tTVht{CHFL#&`;}t`(qc_0+B?4q@ng zSiiUN63%Q<&KngxEdxajn#@oG$OwL1+hbiAIw}kDQn<4-C4fjJA{ec)WAZ~|*E>*% zh_12yus^a#nG1&?Shay#xmkK)0EqAM=YU(aH4auw%`d72V?A;pGEQOkY>y~KnO)rW zkZr9Cn}R+d>REp6Ig&U<_lfTy>)h^l8!+f~XJHmzI&zo5?WaR*)zjUJ>{9ZkBZ|=6 zjq~s9u)yK`saGpbTfDC*AI)`bym7>p=!-<08WTQWK!% z$;T0BGcxn3%*vgORLm2l&bdmul1S_%)Q$-`hw>kK(ZXLhVrg^Q%JXOTTQ>|L4qJ4*kXyn7dSPUH<@z^*xwUE(6bM#8! zQ$@TWKmzd^#l7F-z^m7}RTX6&E8le}atZpBOfxJ!IgbzZ)SB%JazIm+XAdJo_TSB3 z?B={J+)BQReHE1$*5#(%-2cfOCLT24FJfri6NLY6$JuCWOSPkWgb|^1`o)SL9)#Om zmIO<<{KTCjI+jgSrT&SngAe^cu9dr##x@$f4RmfCp-c%GMDS7WS7d5^ZXT)-b ziTXyjJ$H<8W3#4Q|5cl6(>&T5I#(Ipq*{|NNb$D~0<%*rPI*1QypgvQvM+0`%}pRn zc>a{uvSrbW0RJiaMcO-R__e5GI0%*SaXs|<5vj=z!4n4KmehU=TJs9&mip;q1S zoO@YL(tdXiseq~W@K55jn{*v5xV+T#1NsTO#94|uHKU$jd`gNSuPdP`Pr+yIPRC9O z`PLLumAz*FlyR;enHG7^MqR_l)T26@4--F|4vi#8Tvi({xH~z6nWe3tZ|olR!*9yU zA=y6hnLhpFa50>YxC^S@L>B#-M$lu$IXYDLyemoQA&&>t?F0z5oE;Q(kCrO*n zo2giz$ce54R=R)gFts&olM?_Xe<9!18ofqVAK$d8{0=LMDnk8j<}zLzRuUDBe2*;& zW@R1bs?r7IYRhU`WL9NgeTs6aH9Tf``6dFom)mb>#iVccw!L910H+%IBW+E2Fj3TN7HS ze|2rVxf1>Wc$Hg37uH0T6xZ>M+Xuj?I^i_iZ_uGkYe?72Tq~AYB&0mS-I1;%I!o1O znhr{c;F`Mh-kxmb`3R}@^6oO|3)O6%SJJJ2eW|H09P;F7hvk)xCo+8QZa^gyidZE6 z_PE6hXUWZp`js_cUTEm){^3g(I7XE4wCwQitDFp7OCWXk!Knib{hO|-|57<&shu)dK=SLv_5xb14kp= zGi^9@(^b`!=DgbJ$;Kq3;mI}>mRt%RQkA55F9W(-C+?_fG4*(hcxG6OD=X^f(z21cblp<=5EPnUR{Z=IM7~ENrLA3C4=&RGA zV9~x-K{rMb2Fii9mc-Y2GSw7WdGL70 zqi?f~YgaPo5ob?%)(P`?D5DRtJ2fMUgfOtpb zT`e!+Hf)07So}Bah3;|wd7oe7r^uOn^xF|w$gp5XfaHb@qzstes;03bfQZ&Vtgr*A z%2SPwZbjLU8LCIJ@u9*@K^I4#^f-+t{h531}4z6BNko;vRojoKWCP zv3-b8Y*`@k8d(Vgg^?;i$jlpsLS-(gA)nqrj$+f>l)jWZj2>DQJ|9-y)7d7#jSO0; z_MR#65Q8LGj&qwAhknS} znKY|hAIOZZO@G-vLpA63u_jBL9T_k|7gAai7k5HesMH!{5gAESSj+JF0FzGE2U!I} zTY_G6Q64_#{&;-i{CE|4hpjpW)WKK)zCFs7MdGmHHBcs0p^S4ovt1dU2&O^~c_-M9 zl=6}5Ruwu{VqgL$GGTZRt%MTm{8ZAhWzmE*92teGCJ}c2!&3x?T~Rota?=T}=Aiw? z=|ey$J$9s;T0HP#X%RL-cYj;hO>adp;oDlE#;c$;ZJMh z^~B0$3?iL46)6x}HPpM48D1%YzC7IbP@0_;-8l&NEfa-tn`rt|)Hs_d5jIKIPRolp zt}a#iB<8f832?cs3uLoJD;^#d8LaPGoMoHa5!Ov}>%*xN>gr-hYwGG4ULfD2iIK0r zzQmXGoO$#GUy|1pVAs!0eSUS*U3KnciDR2wZP;_e&=Q}|4;oldTWB$wZ2tUzPO#aWjjGVQB$9i2^bbV4a41IX}@>L$)x=Q4|2MF;8P zcK@mV&erf{$fUKHvKgFN=n`NXtv1YJiwdRuB79g;gmOTS=VKs5BWZsc7aj=XYQ^cM zdU)^m576H+@M&bMJRa4Ewefe3C$U%J1M`yL$U1Fus5@0}EPZi6(Du}1+L2z_V3>hw zDwqVHn`+)eY)T7My~zmY<}=KLi6_G1z^D?$Z|ktPu8G@&YiHY!DNQfDvfH@i%X`m@u(mef<| zW)LX_2f74HM<*(ipf{ zSYu*BLYp?9zlk4MhtD9b7G_S0zWG`_0roZrnMMH~SJL75DoSoz(dztD6rAax(vYFO zVCY+%U26`r=YQz5ZGo`(=T?4Wr+=%~;xWS_m^hOy*Lnpg@xHfR;*ZyUt0h*dI+a3O z8*Xxp$78KNNvEJR(b8r$jybKj8qkXVHZ2zH+oRz--!wY-$?^?W@1eW+ z^Yk*OVZ$W}$sqx)#7#$W|FLP*>#P&PUg!M*KbFW6tY1Vc%57?d2IX&l8mv=oJNIA~ zb3$<+I+rzy39 ze`-mnF3>aWb%FE*+B)5H<6aiE)-x7_7iZmjJWiW#UhupVPh$nY+l}`}c}e*tY|NdKTcN^=>bW*8X2O~|p z`i77d4MkZ8&1tCQu&<+|Ljfv&Lk?Qu40-o^!s2CWntPeD$}y+n7(dDsS_wAxbEtr# zmhJ3L27wwgTD6#?pBJXLU#>A58$*<`?DanYk?5lBc3<3hpJ(3Cs@ePOoI=ZEB|t|5 z3^Hi1W{Ue7FvU*mpf@|ocvAYOsJmv*2R%y8G-krzJd7`ZdCNnE=GBR(K4)WdX<+U$V{G5iKV-e zM5>>w-@2wwJFK#eQCviEkdH+shnWmIto*5UdWmls^_JO~NU7A&04#p{hJMW^y-~+S zNkW3f4mRE)JD;Ue$u&>7Kb~JFCHDdMvhBCyY6umBb?{_SOeD$L+bTb~wv{9A)q)R4P ze;+Z$r=8kh+wk$F1J9Eq*{HC0HGV<;f`CfhoK|kH*{2_Tf%L~-ww3k7!oEzPm%~T<_XalS1|YeQNCor>*%6#fnm|C*ssF56#^+(P1Esu z=^m+jySzVs1!g;it0YDJyeF(QW0v~nn%}sQXE$7GApE{JC-go>zaL@HIIMl5m(jM^ z0mGC_F0IN4Xl6WkWVvCp5B3oPvy9!Ujr4wBl5@aRop$ih1%2gl>ZNNn@oN4k@Xo}8 zbqbOQ1tq4VHXNj*pjcY+;-+#&3vLAB|BnEi3uE+sOU_d0ZY3)D_c7SAU%Vp5ql*?R zn4?djT(wTxUb97uYT8(-bz~a0$E*yHuO%-okiAt-w{v=fhb`laX{ctmQYr0+aLB+! zU*#Dfyo#98V>E+8^^V?Zo}P;xr{RtM9mK0 z-IJkDI&(FJ!dh{`w9>fOrcoC!`-8zEKwvLO!V4zvZTLfUb@S}%XFI}F znQl(#p&%<1{Cg`Vik6ptk-rt2$RP%q-A6E=SP^F=bP{+XgGKtqE!$$d#>Lm1b!ITi z)uS_d64A*uf_CGbkhIE}nWcfG**XFuM@c|4Da4>0x)@Gef4qIp?xzM+sdyaL8%vnX zH)Op!C6$|s!>DHw*ISlBhpM!Rh5&SB#QSEm z?p9&*c#1QqNK3rEpCDxh(~in?6>8VduH$0gxdJqD4BM(Ik&=aT!EW0X-Zn102rAxr(u-aV4 zVY?;i(JZXoRvkk)j=J2k3_Vq(OfUnZFD52I;a5Q+ZXl0XTp>x_X3i|p(hatm2a4%5 z44)E^DFcx0xVG;vkRLKR^EN%JPaWcrPkOy|e9tYwy>FsAdd1!ThF z5n*jRbH=V_2#cE~2O&~dRz2M1NyGB%6Juk_M3oaPfrUdqj#aB1mAU<~R`HL0<#}hL z4pqBe{M=VWC@9v8KVPrM_0ES-+=sDpkl<4DoT4Wzzq*rQ_Y^Ie;?-H2pxx#orz7X5 zarAxQ6>XO?HdV62rQJ(!nMbc4${h~wvjR9t$rEVRizesG=uS zg}ZE5c-Xr0j;zKRTC`?wLRvW{uuj}_vKEgI< z0?Ef(F}5bfRSXjwkRiw(Xvp$)OBCI%df5=@1bfpmyq#Jto6rCwlo_V7B6JRthyFPV zLKIA62o$9N)I>+H0021jzSsmsvvPG?H(x*#m^`BP8^^(#HaVNbwlI+d6CP3tOXPs) zU#(pOUmSYj+B$gC9v**$E&b4bsl0@G$3 z=JyGO;uY)lVzD-8$r$E2PV~4sb<|@|l4g+g6)99xQu#&zFzO+%2DN33k9^7&bEYXC z$B`};8?5otGJ>g-G{u#}mjY2RiG&4BH4z^e_!Ha2rsZ?|mH@#*6y5+;QN8c3jn_Q?Y-RGg+o$OTk|FnA9cgLf~u3^5v~Tb@4?=3I@An4i2X zE*Ix%P?Bbt^QdDS`j@oACLKg|;4Jx@E99)XWCJ-kSaNmh(S4i)JULW$V3xHycH>n{ zLK#8oc|uSVY9JmHQ>ok0-IY$FcIve~TC=#!!2?`7RnZpfwlo}E!c9dprXoX-QqW7} zh-ulw`xUBoX3b!gSR6`D*d*S}%*ifP%86HhDI#?jSl~jGpk)ptpa5&22Z6+<4$ZlD z49UAO4(>-3WSf>deCX-v(=&&42dvaMmz25*j5=rW=pq{EBgdgO?da~x(@p85m!eiZ#%2l1$4LatiYfVqPGMHOq#=w# zm!+TphzDSgf(HAI?l!D_daFN9RA*FcoI&5a0qJ z1KYzJo_x%iS2$cFAlp_WZJB=|+p=AjKD5dEnO1e?Gq*R7T_nXPPhL4mqH2OgRdtjp z0P37g$1V`u4sOF`71eYl#aUYSj{sdRKuLExRA00E;mQD0pT-~n0y=Pm4C}c(`u00yEWKL7^-XtVOS zXxzPYQd>ah68S~)NBP_ti!%xlEyPnvA_$bGDSH5P53Sq7`X&np{{TREcm5Es_2czz z;N?zqiPkAlRG=aQ%xyVGh}a0R6D07UvxNs=C^YgzPp?Oh{`8mi+(Q2Vfe=0)wfsOs zV6(Z)zilnFEo!=kC;79(&AB1!7e=`by3f=k`U`T<=0Jq0N~csOAL!x^!v)~j6eYrki5cR{O%NsWz5<&p^Sl2Cy)o}k1uEk z=zVh38vWBHzqyYNzxL{XUG;1S?Kw<;`dmZy>&n!j`W{Eum=FH)m-XC3`2PUB$^5|l z$d-n9x=q|Js2MXTVwb2I{m!Grt3@BSFuRXY1e+C7nCCLY&;$w`Ia~ukS#?wZ(@w*j zP4&w$l5Dz!7HauLOu}F6W8x4F9TdPNFfO172nYxdH;6!ft(gA+>(l;k5q-WU<^Hrj zypxv-i+I**l**_bhA@G?Y>KMVUpKI%l5L7^^s=8!2kBACmXgoD!gBEL* z(?W{oF$iNUDi_M_)3=9SWkjend;;K7AZS7;an!{wpH|d<(!?M4c#qI^(GU;*h<$lj ziA9Le7f=qG>KF=MU>^{O@cqA^S*WYVhHg}oSf&vLOQyO2{{Z=aBZb@=UqM+@s3E9+ zoZQ@hmHzHh#Xgh&4Xc2D`ATq(*T1}K4mL&gFFsBnu0sshD8yr~&M5fYcg06Z!)$0{=; zsHV1|+F|hk2=H_kA&i!yDXIbFn7;>a)fD9&3{e3A54Ic-Tqr7X28Gw)53d1}F^FLC z7@{B{{E*-QR|_Zs;sdB-0Dmla`=ftUJblr>svbV*-_;KvbZ_d1kGePYL&x14`k~>N ziH+EB9~ki9e>lI){KlFO+El;S2MCY;p+D<>yj74;$*55Q0~B-~!QdjNNohp?0GGFv4cn;c z_FH8XFcgJ9X@{@_`8+J5XDP}$c9eAw7u)>CgHIm4pQFc@uDW(>_P)RkHP>FvJbL^Z qaKAq((A#s(_MRX@oV-_a_g_br;m>-U4K97KmXY)BUH)& literal 0 HcmV?d00001 diff --git a/.hailort.png b/.hailort.png deleted file mode 100644 index c9adb26f806db2cbc4bed441e8260ddca53eb6c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1454787 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 lA^=heEk$BzjSiB{J$JZ9sC}f9b)cO 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%0D+g8mW8 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

P= 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�!_H(wYPMU)~6(uq=~iFBzE=}meUklu-O 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

Fa8|LgopX5~)_E;+MYuAam^1xU#6x#7{`u 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 AiwvbwZuYE6wFsoj#ahxcvh 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

s|dl)^}Da)z6?$ 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�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

*j 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

UQQ#WN7osUocw8+!+8$7;8qEH2s$6 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 z ^shOyF}=srW+a?smM?wqHV8MOQ2C!aqqQa+j+)X(_Pt2uL7EAX?58?Pao*L%x; zzZW%T;VKpscl#zqdY&ve7}akH2KSImzR^N+x1alyH*8*Ur*8Y;UKEWN?oD}<)ASo9 zdNCtNCYNiy%<@*$i9Zg8=2V2|CrhD7!lY1p0SR&85%mi(buyUw+^@1=6xsyio;W~9 zYA=-iimp|7U!Dv1cd&E?u&5C2SgG7>9Z3k5OzP53561mgvi{Ll_@P+9auTa%Z?kPKtCw-;{iNu{c z(PzGi?cB)6-I1^3m}7b6UaiS(9~2jbf+-wSwwa88JlK?OEO(>0zH0`uMXbp)w zYd0MEn_FDHoY5EZ3+7j-b%%iFATxK1JPFUdON{K8$z@Arr|Qslyx4e6NUsa={f9D# z4H|2h8vahFwPcZSE2ZY)6{?Z1z#z*28OY>TTVq_O(Kt;n;CL->uoE*L2|0T7M;tG2 z)vja^`Qv==iHQY|=S5WyOJ)9;?=LGi_nY`t{L`x-?*7Z5fagqqI=CNVWR(%6AYi}R z&%El|E5_QbR0NNM$M9EOAw%vR{KdG#xa$B%p*7|vk zpIciSK`?duc0E(LQ_16#+i=MtrMR?%wUc zxf!}@cP%B1jTHI|=f2^H)5YD@=ATl8+$PysvYq26hv}Y^Sl1*#ppcGP9PGJG$=htM3#!pFm($81AU+A&4F>J?m`u71E@Wp zQGFQ=BXe4x)AEj7>os(mnS6L0hh(`&XNnno6eByw&o{~eGln#NALaj1T{sTDyxWI^Tz^3KhQl>OP)U3~>q~kvqq%c+%aw zIH7K?Jxk9pt3tt#<)@7~_>Yd(0OH5JUebK>*2G8sEZu9^7L;^8`^fur(OpP&#*wV| zTV6V$u)j0ZDnmjFUwiF8~rz;geJw=)=Q9M@!7!^0DZ>mzT>pes0kho7nzGc@M}* zDAg5a;|w_%n8{I(meH9q?OrgLKX%Qkwr&p+AQs4F-M?;T{HU!Lhai|=ez(Hy<>wD) zw8m1O_N9fR+23~l7T#q>Md`%#0GuL_RFj;9RtIz6fp+vm-GLUdSFcR_DFD9_zfL7T z7J$OSgUrf;cUE>h#p0b|#esJc)&jD+MQ!{T412z3tS#XJ>+An(we+=#&KCl! z0*%|_x+ja&+BJ!HT&p_UHvDBnz%g z;?7al-n-Z2t)F-|Psdh>*L&T@c281^YMVmcr4LQ494>B5vFB|k3+UqSNJDG-sW53wsySn+g-DhY+-ZX z{qrR#zf*k)y8_JXefw*Wgf2WE2@;PhDi^dPd=TFfNK=YG*lMwi12(7BjGM6~l4UKa zWo$zjgEu-u%bh$|@ojFr&b%QxKkG`qi@n68qMMk?49&5Jd|O+5G_s{!du;eNS1rd7 zNba{6VsdHqHCxYH#_+p3?9b;PsMaFFH%8EDsmp#q?hDfvy3eTf4>YsZ*Et*Q2LSd3 z0OJoBT3!{Yha>Md$U9(SU92V};ThiRo&ViU2!!_D*}bg&Bcsxl>lu9R<|E>w7LWpL zWk(zd+}20`Ui}9ej<0ESdu?VL6# zYS4#KCn$on0s5Gv1D%Wh54o}E37|S=G0a2nPXm#RXX{6aozNi{Krqf1Q@oVJ<*ggFgU@JEjo0?bXu%vK8IJlIYR zWk{K5>g70|IbiT#wA6Q6d zSwuWbRDNa(p3&*0_^zrU<4|%|_&EtstzUm+f{;V^2I1>SZy>Nlc*g*eo&VJc9hw3C zUKL7!rQ)mce;}t}_J>J;EBFr-kKmu9)LZ%oBD@lEt*P>6r;R`RX7Q0$c=gBh^UXK7 zrX@phRZ$U1HXH_;7J5L3YoaGwnmfMfGH=RRq30}%nfXiQ16JaViK*u7eq7h+%zJO| z>dTOeA+2SJ;PM}%Bk8`a?LS{E-Ng3g3c%{TXV2E4 zuHB0NO+@;?Rj|@Kj;=k99;rxvGl%ePf5fCm4M_*raB$ySO%O`6l3)mlM_{Sh#Qa8P zUTd=x%}(*eV?TdHfo$ya9s6w^vSsx3q@JuzA4Qk-%v8(zRGRK_qzG7Hy(Ns_IV?6Y z!2yYw+Qb|Rg)g^m@eZAQkBMjP6z1>=S>!9qu?~0gZZ$uZw-xA1nmggP;34g%?<;uM zy|uS09^vugQA&}!EIdgPY#oPG{9Yb@)wywNWp47@HF=~n!XPR=Gl%g@&#_|f(i^YB zB)Pk7T^~P_Tnm^2gKTiC_5!&#yW!b1CE56Y$0__@+eiNQuP)&c9$9IRY3B(w{5GPW z+P>MzD+#f%6=Y#06S7cZ)WXBVU=l2gqgB^Xj0r5-#0zUBdxf6%NhEv7Wux7 zE1K;WySvSv=o`Fae>>v|Y1Owg3{y6{ymlo$Tzqf+0y^}vbZnpa$r6|3l zT_BG-hD5hpB`s+Soh(5AKmvAoN8kwyMV&#LPW($*Etvs<44P>N9I#)00#@uFCChVg zq?6-%O;s)r(ruhwHeV%jZ})(*m>yirI_#c)A1QqYlYaWNo9tkGJGXOGQG~=nY<0uS zd4y|uc`sIb!k5*v%oP#_H;c?J<}JR!>{-nOjCFJbVNBvD>+~HEd-e##?t&1?0>4mo z%9?>atnkksryh@W$q%y{^@*t+!YKs& zY2?AwK5i@gVjtC`3>&0;2F4K+mSfPp=z=NGs7|WMFtb>m{+5=ocuSOV6?pIH6LE*4 zHXjZo5-Z;dKGs??mGG=>9<2mR_>#>`Aet-sX32(dZRNS^inVaaKhIp2B^MT@^uLcP zD$r^2^t!G%{e4_qk-8yTNw7HU1>o`Pf446_-v64ssmmfCH`}Zg;*ZbGUtQL!cXUZ< z3sl)bo;WspQH#b8PtKT@n&lKp`~$t0Ez)JyP@N7p=8Vmvb!F` zAM^JPm2wJ^VLo0_!l{*%!U;%UjZ@zHIv?jD=ttJyPbF42My`be2F+pevu>B^lmX-% zTk#eu_OaDwT24HUzev;WHmR$zg^vZj+NiBb1HHX}8R)+LUo$qXW;S-|cerUQz%M_H z9y%^g`l2{-jQdKBvs=yK|)!6zp3S64L|n5Tv9|p2ydEIz7q*;#QPpW*e5~~^_NN@qxDk7 z1`H2Z9QyeFFV*noZ+)L5Nh12D+U=wmU`c$82b)oHgIrg#g8*AGJIRe&9aRQxF)DD= ze#E+rZ-r}}$WLy@7~Aui6JB_W;g4eD@4>QLKVGqjbd>6UhuMsyt0E^w)JX@txSUc4*5fAJfji*zfo)~J7Sfl7rllz zYaXHW8lmGRHQMs+IAKSQIv(@|#lfi>-NWcZus+!j#&1O5242&yey@}#)WXcu`@TX- zIUSZee)40D)^Wp0CDn1U4NsmsArZS`vQ|`0@luSbG>?&~_n9ie+eh2mj{51&|Fs$P zKe@X0>Z%b~8TOYq!|=Y>&9_pTcGeZ+eLFQN&jq?$S@kG5ws!L(vNb6x2(Twx>#a$9 zZQj2X&J6;M{P~bpS0Uu3=V@}IjxSnnC429i?FHBB9R4p$18KcLwXy=CYA|v=USdlf z-ulp*%l5b5xO!@w&6@1~K=-?YcRXMj*_U#FUSQ#0Y&KDWL-KZQ?clX$@c%d6kei*B zR>-f;cAfZS-oTIOWQva@S;v1y%ePds`~&T%#L&drnmjq$=Lp|>F9w#oOusV!W~+SQ zY|A4300r6t5%%W*#p~Uzbxf2sGW{s=@tzQaN(^@{u&~`jftG}o< zt7*E&FT9u+AHIH)Zo9hgd^}q44jS!$oUh@xd;Tb(jN$c3@2h3+?2B;Jq}{0B;F);) zt|w@lci-5zhQ{e7##1bn+3twUpz`!uMm>kqdT#@>Fuo_RK&V_y;p2wy1-||G^d)9p zq`M}`;gaJ-VB!J?UTfS7FR`v{-U)&gJ)rRMB-H^#WdInezPV_o)ap5Su2falFh4%m z-qdr z1asfF=`lrIAD%;{AYk$bPA=2LE62?Ur3*}MezoJkdJkawJ}{^<=<4nU-f&WU?h~Wo zQ9WhvbFDzBNFbl^UTXc%wHg#q<~BOIzRfz-cJ$VU8d=bKm|q=KK5Iv%-MV>K{Uhc_ zc*@IH7X%j&RXpJE2xyab?4+;tUpV>BoT1;tH$g{SP*Tf-9WhGgKlg1`HhK?)eA`Ig z6G-#N>pOW{2?P0n4{B{7F=kaeR~h`ZwPII#Uo7Q9N`CZX#iOvk|V zhiv36Q^eQz*jg5-;xuSyvh>cx-(=>OlBy8nqb7ipwL~eba2@}=42q_E(HD7*jo}YI zBKWGdm2^FBaXuP3)X%=Wy^W*raXo#-s`CL*3vIlSBdh4?pbM4QgW4Yx2WAI>QR#?h@$Ksn`KDYKf z-nQ$LKbu5UN7k`a&2p#LO)Z}4a=evIL21x^eqqppP{%J6I{b4)FSsYoY0ehwPF`1r^|fPT<^6u-vyVklesQnGq0B%%x_YB*eXNOS9<@CnTUV zTFx@KrGKX=-o59aJ(Tl;BstiLZ|erMjtH&B9~_ydsLf}1XJg{X{^|!n=L8GWj?e%4 zRMAa;78wJq)%2(J&3)>7j0+<>w|YuVBdjblJk98kaXGNu$@(5j| z(aj8GUI7*iJu68D^3b`hb*1rID`Jj=U@l6Obm8iQ4-U`BT;_)zAi{||05(i}+gtfd z+xav^py=pJ?aY27cEQFV^MLGqauP%(7gBf){40hvPNC%C7l#Nua&LhW(=TtQMC!)@ zVyB)_CIo2X@E9^W=&$1xo$a}1R<@&u=%6`3RKUj=Cnk`tD2rsaO{I#0JnA4 zs9o~Xa9prl@}z9xuT`+wcHI!)oX)H&9`Ysqhp!!pK2wBQc<;qow>1F-B$obZguUx* zz0dNIx0)y0b)+F0(Ev8ZIwBYtB(GbTcVDJqm9>v9B$X0}jaa)!>EoQe`=jC9A6{7aY8}47!FQ z;M{7N&E7ogFL|wpBgbuJgSB^ae`-zjDe+L7riwgSIS@PrA1l<^tv8>&!B;h(D-3m- zptwI&t?S;tNdQM-J1Nh za`LcE=Mj7UeN;{1Cw_2pU(Bw{u57i7Q4Y^d@sM$e4Rh7iAHf$rQDxk)BnCYLTrbTP z?L6)?coQIf@rR;#T+i?KmnKmM^SIMZ!g3H(Hl-`~6`s<9ua^d02em4Pm{BA6_}csv zZ^a`{&Fz`0SwrSRx9yNYU@A%Q`#cX_tEtaHl)S3AvcN>6`v(#o4kGNeHi?a;D_~)_ zlU;~}HUgax_6Yl9g0ExMJywoZMF+4T0x1mV`{=Bn>RP*#&1c|<)}c<;+4e%Iu;?#5 zJlMEDW6`=5h|frnKc!gbff9t$0M!~HbB6diAhz6Dl5BU?gt>N}CNK1*b0(*H8Z-c0~GRrVLBJG;@+D(hT>J}fu zAxAf%@S`-^0cA?Iyn+RS+xX;-bBoR>4&%x>DiqlJI5; zRfs|i=8@?cl`ZeWPKS({1_z$ZXA}&5<@ih5;puF@GsdRR;Q@Hod^29Y8MKWZ0*$7s{0ENU6HKuQj6!>zdWhD6Ys2`u&lR5m8MX zC17OV#6834lbW_(<9g-MhJZ{HEdKQp-*y+WFtSrV^EjaU6mKb*(tZ+@``Ijw7|SLv zL|A9#t?Z!$O2&1BhWW=K-&AmPMsU*VCvW`aNxF zIGg@UoPeHCBRee9@UD;!qJZ-`{~)~lytm3B-(y0$e&CVNhwrnFJbEn+uu+eE#IuwF zDFQ#s`zM3|JLPRipuOglJquT?hOO;^b&G6?BmwH2+?yhvIJ*$0{YpV`4%}`V9T!yI zy>k`w>^Tw%bsP_~rwcgv*f-YM{yl)|Kdb8j|ti;hPsM!1zvmQjUn*Z~3N8G?Pi z`dEIK2ZwKpvN&rXh03`IkuL^&DGEn4Za}8ijrwa=o1F@2byOPm2Ruxm&(!?IVAyqZ zzSv8f-yfodGdKBIqhc@2igHdUu$cVV2Y&di{qmNs0*^!l?hDl1eXc2MoGlllg?`@w zB+1Rmz2L#m`o8`#XBbd+)@!OZ#aU#@>O;)Y2D4Oy&1kX*cA%YtZ(^tE+ ziFn&&yBc#eO=O`vs2-hnNW4m8-*D+E&EEi@WU0SSCh=_7VP`y3 zq)Rtddgo9JLXXPkWHv<&rM# z?+{1+AI!Z~R9jK|?hAzirBDjRy+w)_r%)hJibI2YfkJUfaS0Hd;;t=F+})uNq&O4{ zZpGapLDRFoan8km@3C#{t8MSqP<@O^Pu0aZk?jAnm9L%(pP+(}6-;5M7~i+ao=_X~Qs$K^!GuCiUJAQ8 zSAyKWw2p=}&fmp_#4@zf$#Hn!CeqU71(Ed?nKN;l*qG#p13C*&zMvQqlA1v<775AU zuNbCJumwgtW*t`66jB?{GvQw~2-`0uGV;PeWkui2qxD7K?zN$fn^xzPqW^-?Vf+X} zwzmxe3AkU!D!h|Ff3@d%3@ibRftDHw#47ABfP@3#>NHcL9Zg~Vza}*{MLOQ6go!lP z^2e^HDOhVL9@yCY{Sn?Vb*!IOFh^cPZLT$ibGLj=rPvSRsX-Ts)-je*?&cr&hPz?S0)+Z)o76j2(pOi&JQMr+|{=?8_CPsHg zLOCU-5t$Z?Q0t!g2bTs9*GN@udm%P}c(#wKR0!Ad%?8E^S*v}<~vsPVZ zE(TJwSchRQ)^03*S(aLk{Fw?*Sys7w!I`jZY_#PJ7M6KUniQuhBmD^9bCT4FK5&@1^7niv)dk80 z6-p1=vWxEbco=OQYC4x$Tl6F+uD8J^V0KNgts~iznF;2`#X@Wx!Npj&tQ{Rwp4zIu zN{%)7jgwXk(^14TkAv2d`2vvm3p*eA8^v~K-i>OyCkyoYeZ~7O&81PQEbua&Escn1 z&?dY5HI)9?@AMqRvsj<{ePvmVKkg4tL4|k%LZu(++fFC~au#01SC~~J!^y0ZJ3^n) zh-cS$quRo*+wMdCq}*NKj=-Ie!%+T*@3?9Wod2x4wHgs{HDT=@X`K?N`{*yj7J6g2#B)1r@A~xgLvPurbqCH^ zAov_Xfw@^s8v~_I`$C@_Sg=`h8w>IEDCEZI-tIa&xM1vX zw2I7qzwk^}7`2qZqbYVc(I8j{Bg=TZcgD%z#uaD29zx5fp|G<;R!BqJEenMo__wAd^#9)(GWD4p#Jg#gsQ!C(x;l6)}ip=>fLO%w0#i=lRWx*nCgD9C|4uPY@o!L zTlU+vQIWZhlas_VBRfuhn>lZhD#~42heO;=1t&u*nuLvsZ7ZL-7-NuMwLB1{$_GLO zKhs=a)o_Kod4xf(Zu%$Pw^2F1s9#NeI}zlaSMIB2`3iOEdOy$7@`tUAQw$WpSf=>H(4%|EkbH0u!X;h8n*ARS%6dtqV)LXpQOB8TEIy{szQ8(O|qmG|jxkVoE&C4FG1nxlU8yo$rKsf<$NriI0CL-TCK{9>B} z$KrF)L7tPPNAfAU85ioB3W&eG>Y8!#q%L4lUzVVS@|w0Kyxyzdm>&qa1obK$_5v&% zq)JJ`1tS<*nEa!Up*0E%%ldkQMf=QBf&%ivp<-VHIWRDe+%aoQ=;$7nP&y7DN^|-w z_Sa3;uCIxm;z5&&T6zkQD+Pz%6OWxc#x$a&8wb?8u=X?tLfXCJ-w8S$KH8oc=Gx@V zZu`93bW>d>m_#l<@)O|H1y~m{CNj(i`I|-~S=gPv@GeqFb#p)w^Gba}0`Wb_t+*8M zf*bnj<(MyD!f^h^)11>hyJWxiC}d&BPI+$iB})9g<~>c-p21Ho4cvef*FlI_gkp`W z9i;&E+w&`4Sdzy3Dv={Y==;Z`Pn_2|)$nwnee6GDNxpTz+C6;od0pa{Gn4GQ=VP3D zzg*k3W?z3}tc^NNIrVp=mq^*~)_sfli%&f(QfIPqfGyT#+{|piBGivA7Lle>Avh+V zwA{cu%%P>l^%1~Ja2D#n4#y2B#aC-b=oTj1ITez7?5Y-gU#RyYJoVV<=V@35 zQokTYk(o%tKcW7nf!$4{)N1f$rBQor34OPU1-N@%(AEHKk(Ck%NG zjq&}%P<%c?39=*ct|yA{yp>3t6oCAyjIdnM)?Pd>&2_}=%QlE_PgrHKfo9CMDW;w)32&|u0fEP=Syup> z@nscIX=JPDyl-t@-g-Z6!s>1mN$Et-?j~~CY%<7@1wfAspMkt*JQK_S+h8x<<8C$? zN4=jr@z?aXSSY?MO^3J9{tu6tZ*c)7Db=% z?4OdW`31u|Hs+$gosFBFCdKijtwb|`LAumq_dk3){VrU%E?j6Mjy`GfJM})_!zw_= zVG%P85S_YxUL6|S*d8A95np*%*+%y9>TBn@y!gP=XkN}!#b%1v52wi6=MQ~ldXxk3 zJEG639ptetG4Bc4Ks~b!?r2~9%(j+OFK~$Aj)S_k)O6bZb|EpRQDH&^rb$2Og!YaZ zy2HS1xSmU*I4vq!H3+|B*gL6RS#eYbz3F8v5a&Zb3rMY*G?VnTdA)FL^Yh4dK^LK{ z!&h^k0Gu1chp?v4(In>i+8Ro#8+L$d9O(fv0GtNVCX}TKn}C z!}V%C>v{s+xRV!$4R)jq8$UyTenbE4#W8s6oyX=_aecaLdeR_V7KP$2sNUy!#i&kcOqyMdet|DokR@ac#9I$L=mN$L1@q! z__O})6}w5mGMDR zt?kaPWaLQ;m0loR_4UE!_Uh5YGHHy27SXv|gq((4c0otj*~#mRLtmjK~H~ya*STh<&WX7fo)X7d{)8i=`zvIgMT21K${Yh8k*<2wn6nul&1| zUM{z^I7eV1XZNnTiOh(U^4^z>3oN1S;O3weH#wm`*+ru$ejz4Hpfss46rsS?9$Ozr z1<(EV=ja?iAU*9A|7RU7hT`5Pj6f?6V_oqlhIeA8_hsX}9(UI-58>7MJ58d5CBq_D zOB?s?`OeSlUI(0I2SrM@Vq!|3NrOVNsvk;dor<~sbZL=Dp)N4dma9T_CX$k$kq{_Y z!ujy6Exh^D>yWB%T|6L5Gg}ThYSEHWS^c7lf%=Or&h;0=)L-ll$Z?+I#q&odc-cF5 ztfXvWa{85T(n*c1xnWp0;E|bY=4-2w2)#gv0+4}gvki=>J1FG&3$RfJ;(qX8U-`bp zkJhvU|6bwXS!j-dLG+?>EKO761tN;sDC=mj_vyB+hB~2WMnZC4OkjA1P~x>O9b>9i z&hj^k8X$z2-sJz3{+SCxEGpmKEzo(?M(jtro)}r;=cCC1G}kP1Y<7y4Ys8babM8fd zt+DwM+~o)*V(cW%74*QL_Z-BJvHST5^#*3>anxpa-rC+6(4ojYQ0dp1pLk9OU0E0^ zjVLW8(|W9}Zi?|219zOrXN|Sp_i1SeDJ2#L5#ZVa_dD=~{#S}>c4y?xO%@CfcE&gS z52NMn8y85!>bd+XeCHc%x~#IsWfgv-?ZHSl zHdQ>mRr?j}?x&ykDGw69(wtWC;TyC{k}m|kDicfB67(8^(x3bSi1Jo)jotPm(k$|1 zbAO{&g5>U*B1J|ZMBGaJqOWHFxhMSK0)dcL<$@U64E^=?q{{mh)}?Y0S`iU<^8g}j zY4R6zf7}XuOX-LzbnP8IO#Nnd@&rAsiCz$kg#QgduA*av87@NpVI0T<4zx%MG}b?i zKL_RTOUhEFt5UZ2XCMQk80u z0j*%#{APUK*9jSINwH+?+WrzU?0^DzvVOygjqk`o5z3!zJQL)C|*R1I5Vxo_qkFBM-Z%Owk9@8RTQNqM8{AjiD> zmj8`M?IF(1yBYn;joqxCgNhb)uxi%WMoHoCjKT@p#BtrpFL^~C&BPITifc2z?3;*{ zINvB=_-f8p=K2Ei?+aS{wOx+)gEYORS>A5@KL-m15QZnZufWG9oX0dQ^?~e_X5c3a zttxS&blpIvd#&JmVbf>HW(x)x;P%;m9oH%!cOn}fpNbU;&L%NU$9va6eEY_&9MUty z4t-RoSrndyN7)MrwmNnoEpU3s$};_xKp5YJ$GYrO0 zK7^DZvQHPldV_=LhU#fT6`za_D2IfdmJY_z@-{O?P9{Ox5s#i6?>9!HZ7;XAu3 zQPT(+bb7oZLp-zF#l$hu8<iN9>h;)wR&l+mC|_A=Gm&7-HOAX3-Yu)vBwm` zj7cul4@zaus^GcQY$U^n^JUNRg0iKv`^T=8$;S@mVp~8NuZN6JZbZ&;T_;)G_j`|i z9DVRy5ZyLmPI7w}`G#EH$tnX#sbW{BSzlxMMp~YynR=88SaA`d&7f8SI5J<4>B|mP zY!yQDFk6tlEL=SPnYMMNo4>X5c*RAM1LKMmsgo=1$8TfD3N@jb9~D5aK%*a{LBL7q z)`ll-pDtd#tte}F*Hm(T{{BF@1!;L+RZ$%C9ydB8+gUaFM@Oi-^6%t(}{j*qw36eFrRc>;j55WDsnGDIC3t zp93d?h8ez^bJn$m@hv6`M2t45s+5pPz9tIKXslxiU4jTs__l6rr(zmDn%0``QPv`% zET&ob6Yjkr@L%um|9d;`|NB?3oSffX3!L@CVJWCTK=WGW_xkhsn3>(twqNITPT%u} zzgYjxPbIAGa}OxUEhvsiDVI%OG6Q;cOE_ku83AhvSAaU&+UpY z&(CNL9{onoB6u!m4q1NBdWP!%(ZQ9Z+W_y3i(pTJO9w0T=d8Ef)d52 zrJL6p|1Z#q#TToA3|(?4)Tn?3MntO7{2 zWaT%~nF`o-@?7v1EFlFt88|f7t&43|N*ngfY_?r(XmfdTsO2IR*7eZDlaY}vJSFSY zQ(>6sXAy6Dz+0=Q2RAS?k8Or^U6qa2!t6R1-fh&ViWk=(+SAhbCZnpL^Y$@zc z{u)<#^%^|((6S1YSkRRE@P|VYhSnkEKMdIiBiMhX6c?<}q`*LMd|t+rJY6~3GB&38lMlKTWn@CM za`GzQQyu<_q%1Ab$hfVuiQvpG>nXK2weX7S&Da|39VY54#ulcZ9q8y%^Q{-JbTM%B9 zK9TSItu@$&nwu%y{90a#hi9|3=$qX~THI?fHaH$B zl?e3cK6GoI^g$+ld19j`9e>F_ur;QRm{2_1MOyGhvflpYV<8*6Kxsy(@muUY8_o9Y0K^}T&6l}zmg;8K2s5Ad__H#XXi-O5?q|jnJz##( zK&BRyZ*l!aPy&J}w_egxQ44{n`9}cG5o#mew{4_0ib*cE~xFmpxrUY;lG` zIAh;u6umN?^izb&nBWN+w-~1)lR(Skv=rt$cYzXVNmE!*ekyQn5MK#8ef{I7V|}QP z2sZ#i9bb|jC2yuXwFSj$>fm-mSwqSTSi}Y6wYCNBl!;RY5%S6(|{%)~$J-@`+6*u!Q{DbA>|FUBFKE{y?QbO~B|ry?h0^c^%t?uBETf2e2{} zRje)W7YG4br#ep>ANEI&VA3m1hW`~w|6i{6msZa;uGwc-`O#7*guTn82l0D;{)_eB zd))uBCUvPM4d?2Hz*-{&3Tdy44VP3ky~R0zLHd?^E{a!_k5+!7>0nv3RxF`GcdRXO`FY2m=)CB?)gb6vY3v-i`p%F&?z%)B zh|#)>q@WtQI^JHqOBKrgt+}}t9nCl}=}7&n@KXuF?We5Bm&)2NFqxfeOG0IdE()&A z#}}C_FJJ$GN2Dn^lua?P+LYj!*sOmx@Nr<3XAc)$lC>aYw#eyg1k;hJmE=Z!GtG*R zQYci!xT?=ji2m*7Ff;L-Ya)X4#!D+Ac5!b~7ySZ$-`(-i9b|f+jn)SmK0W9xkvC6I zw=0|fdi-(D*+0LU`@!?o;l~xu$5so)^MlagF*)mV{?Ou*!bwW|5+wR2XU|aHo#x>W_vx7!SB^0@Ojjt~P`q5!D?bJqyEYViU$B!pmFC_70>cHq@ zvR8cCw&5Gk1~Ss@HjN`9k~7g;JTr)aKp0CJ%(=9I`C9Ranlz zOVv$GN3N@+`t5Cm!bYCwz8%Mk7rD}Jj%c?OL6eu`f%>`gecw85*i%4Tz^&;YM&y+F zPd~2v!+#hd+{aNszJ*Cz=4^Y15RB)Rds#PhuQK#NQLa2!O$%>RMMX$0%0VlMBu8m< zG&#RA#VAl!QCfyhQ1zK)1F*AOlLDZTqBpEcbPuzXw<75UpWur1XWR-A5bNok81M5) zzo%Mtr#$`N+b?Y{9@-h-A+9Rq=cT9Nyg*4T9o%8sy-Jv-YslqB?{~% zC0Et@Uox87;gioQ7XR<($p5l7T$uu$7R3UZnja%@LUVqBP$G~!Y4c9B$=0sy2(9KJ z`^*z%&v;}O;LVV*M)+>n-oJS z6u+Vl%r_Vt$3!;+G4Z~43+^eu-nt!UogwTGpZB$@|XPGWwRUK<_H=rIxl$~ht^(AQYU{XG&+0mXE z5-Fvv<-2Kx0ClyXJ1zwK?hW^j%#9e{EKYsK*b`N3`pbc-;cEm4LZr07^$agn3N`B< z5=EvMuHmOLWBg1sVHBADtu0stPFnn+ira4yr|g3xeCJM31EDj-_rB9~2u#+cWL&3>J-O(pd$RPXlEi#Ry^z?@SOrT&Ctm zu}Of|$D~fo996b=dSUo)>!f8g-l$i~viQI1iM7)%0mYh2dxK?hh$mc1=1Bv-4}Q%3 zy@cuXAK&xp`@8-3$@u2+GC1Tl$mwy_E&>hzp|8?(4wz90rvpjqm?Ar!-lQHdH<=1i zF;MY@clqB29kwIEVENS9bU^G#*kWRpUcU@u%I+UOJCGe^;-!g-2>UwgticUK2)ZbR-;rs;0? zDQ!&wTY?AeJ8vH#Eq?J+YDjVKb_vs6Xuw9^03*FZI-;WMHhyeselBM~W7AA2LSI8~ zQ>rru_u$xg&MOcw9w8ye9=FvD)!fDt#Vy|7pFaASLOQ0ExU|gD+G;_c_)?YcCl$MK z7&jiP=<_|MY4bsbpd*BQ2`=DLMO0P88(K=F(4aPztE%5(ko|Sjt>$ZkfnE)6T-n%U zXVAmjre9pyv$vgt`(!Ov*_R`wKg44rNrfjoMpD=Fw~@p)xE>K zxnEp)LJ_;?1lX5n9di%?Aiuyw#GHO%nXC;hxh4?%2h305$WcygOXgFe>Vl==$I7O< zdCX*VG~8|S>Yr2}{Vs^)251>y^xf^9LvgykLi@?#%U>tN;wnEcYbn18?HVjzQ%Bfr zSZbPe0^tW6=%&{8oA19UPI4COHE@zi+dn2=T0U^VW=v3Uv(+z1joafT)0!z0Der_E z?^S&bPmb90tT46!y>W(OHKJ;85g!(zELfL+p|tR8Z|qEOIB+uNR6 z|IUI1CRyd>aD^Ou>Qn&Yi=T2dr%=u4(^)S?2$EG(8sE=7Xop?rVf5U~x5?%Xy!0R5 zr030*0y=M)ji4llKNjF0Ew~=BO&eq2>q^Mz3I|JOScOQs?H51~){mZ@EH&TiIgJTx zR0_ybKLQ_9rO|s)%Oh5IB`y?A2U_woSc?t5u> zg|S`_*+LUfL_OJX_xn@sQPwyre2_nIm136pbZVpbBkzzX@c=7_<^kIBD-p%tJFlW7 zLEDAOiUA~BO&f_Ew&{f7^_f7DMn{^7*15$QI`9HTXEm4Z5)RVN+p-) z^yvyTMSuR$;NL=~SbR*)8<5^FO4obze*dz)qquM7Ctivdg_qSMVa$SWWcb~=dd2}8 zqLg1T27j8COHaMeTDd=LdgP=@<37+)3L7=@8BuzHwz7#-2x6u& zdq|3H$zrGGTXIQZ*Q>r_P@zOE)p81l7P%l9Ip6fZztocMQ{~|ljUT6Xl!}?&0~XoB)FS66}8Gqgwru2^BWAot<6zfg4wOWYGu5Lo2iG8t55w zVb9^hbg?3_1CE-|N=j*3f{f-!?k1M!Cdqqbf-oj=v#=|J8>S_MCGO?;huRI^A;TApUG0g7IJ`;;%k=BnH)X|BRRtv<#1|A;aG39ZVWKa+UhZ+UG4Vv6>pH{_< zQL@<#EM#O3_59Y#drL!8h6Q~tO~8Z47=M`SF4>tkcO7B$?4Wo|8F9T7vDvy)cN)uN z)rxwh=ZU4oQ?k>2P@9)2O)~zmJWJ%ovt_}?bMlC&k&gw8Hfc4}pBRguef$#b8cuWd zm+uc1rLuXm->EtC=MP)1!%txru1Pr?*Cm~2w1m1Z6YzsFlS{38NvUp3?;LuYIAwQT z1T%b`>3Ar)hKXO&j|^}>mP){)31;0qaEM=gtHhIaBP;Qk$iZy4G)nlBfZfWI8^h7+T~GC ze_3y6zF%Ti33Q!6@P`vWDbgU~agpdihE#kP#9W6&OOHPGJQbx6irB1?&FFofiCr};_sunYU{6H_BIU#h{oHy)Q$Fr?YGetmrkl!6kZNHYI_9~N^H|R z**!9dAhJV`;k6O6@q{CqhzPb^e_xj^nel=nn`pGl@@j1zg0gadG0ky(F{woq2-c$) zjB@{?sm`8P;FZQXrSmp%dhi<`OO>-5nInVKa^m+_(9EoT=+o3_;*B2-y)9mBBbeqD z7;2gb#S891rsT+v?@bH7Uj=x?-ZCq z-d7fx9nYveUY@G~B`At#yGu8K0LZTkJwNx>o*wGj(YeG9j_ezYIgE1oUE?X<;08N> zFPM^Km)^;&pu4v(LeU$&dJH+neSJ*wpO$HRgCaNRlOS6ng_)WZ-A>BV*~23B%9A5p zUi(Sm?=UK8@u^rx`S|k&0uJ0qm>cRrD)8)6AhlQExawPbe4XD8#r%V2YFU|=E#-O> zi=c&g0w0z_*#`vfMgoWt8Z3%R5rtbUjZmbd&J2}4O0S^OstxGPAV+`E9WGwie70eU^(aSFI$Ikbm%(>@%`5y7%a|;5aydVp%@K zJ!~3ej(v#W@tQ3xAgs8VK77IQ4@2OX<0bD~&cI>FCzCzvD$Vmr@7D=lTUaU@BtE}@ zl7fg4;a9%Ci7p)j%>wP5B)tW6S?M0by1hhv%-)UuX;4X1vZh5_YDt`VDOEvk{}&n& z9l;@a(xg#cfrOtA&m$iQ!&-{seShqrooXMg+AkTZ{qzXiGZ!)z+a$)kiL==n@3-A_ zKzXY<93Cq*Vv+Lose-~U0gTVOJ)}3fg}%%26z%sQU&ott4T){HYK5|Pyj9Wi8l4M+ zm`=$MGRx%Dl9W1G=xO{l*x0w=kwTxK%~p0rBb~w^As>qZr#oub!o^Chbr`3qEZrsj z+6ix9$2VRucHeI6ie6c58*xMtW2QSG(`z@XGVhr{))EW6*A|bv7kXvBS8ISC z^(-^^4BfPxYi=FSRRE+8U8Lr-DPmPgP=)XZD+>SUb#-mbtjCrmgG04)9jzs_wwMV* zrukN>HRyr!vAXZU&JOMY$1;r2gIXS*p*m7u>;PI0nHLK2o*6YI`!<`7@_>2HU+y1_ z1Fj`m=9RX5piPjd5eK`$}tX!tr8=CyfTc6%)!xG^WBmDsmp|yO1yO^$q|boU*%#>4ca zWT-5x_3+skZJD3j8L$bNad3>bhzJu|e=c*mi+o8gLKYjaZ)^L7pkKx5S5@xY>HK7X z9v(d5BBN6u9`_nRS@&XzA#Y`*`y5{l5Zfde8_=fL%*5x;>by0}VapbF>k!OM) z|6OSG{tu1MFKt%D21$H-wJH&mtt&n*?l&SpP{#LbI_(tsMd4!Ogpo-y_XI_yGQ7>x zJfe5OGo;){9gllc|6gpin+w z0vviNVdK0lk}7`T6AYX_+lcIxn)lfx(}LMIOmkBStnIxxwu%~vDmTCQWv_X7kS{9X zfzX=wkra5H(7nxj_biB4q;GoOaHPKF*Rk=>@{@Lt%S0^hk51cePPn9Ly}+GPnJCK} z8Ty=JEtmE(`YU$@#c4PE=>W$Up-H8LlO9JTD<@UUnO^CBgxJHdo{6=iPX|3maScMd z`ESuR4@qcV8c;-89n_K%u=$qA&i7ENTzWfG^?LDY3(zpSdC9nWlv-Vivo$H#2;?hm zI*RrR-1urHDY0tjVTL(@qaoNy{ClKNsV6(kk_(eJ5&_~o*J1nKp>gZFrkhCf07(Rz z-?_u>Sa*+LSI`uK5u>*TI*Cns-1q1lpd92fUHl?y9Ijd~$0#b32*5@dm!s!BYZQ2s~;uEo)rpRib+BGj& zGo={3pv8fN9Typ?70@vAPz>rmV}ETmsOd<#N$yD7cqT{i+0_Q>MrHecTP{wkW}1nL zQbC5FEkT=@i)5ZZN~B`Y{qBpW_Bwy(=j!lIqQ?Quiq42Np$**QN3ANsq9#O@cQ3Xi zajO~&aXro0YiR_y7if5jtbKImC$+Vwwvn+~5hwJ9H7>*>W+jpC$YZ6XVHur?ehs#t z26N6rWLo-f_raN3qz&~K8ifi~Mv`}N!2G>tjW15sjGv$qhQap?Q2+f^(s;EY@{V1ktde zawH-DBaBI{xY0btCs57|s&#dp&a+&hCu~?D)*t*eRs1&;HFS&&u?nsKVbnQW^Skn{ zC*SCl`pjgRj&Dt^yQ2?PvzL5bxIc;LLc~Jv#Hl&KJj)}U1nH_x>WpVO1{y2&&}MLR z&KK(*y!3e?aJ}x`AnUqk+_cv3`n}hPat_6*vD-p``2L9^9zNPSTrAEkSrhCt%`HrO zs(B&UU*P9UHT;XM=~RA=6vVG6!N65DyprAR#^W4FH>E5oZ?mS0m7qvPoKme>;#p4B z5~)TwM(A`!zwIb|C$^wCz%rdesk>JyZ~6^>s27U_BpC7SNX>?0ovC;oY%aTd4bjQ6 zJ5}%i!^eiYD#Y)Dt@pogIU*A8a8(&dMI0IGTREMFrsE44m^AfnlxkJ;azCkRpg>YD zv9Ye1p?=}6yUL*)jph%k<={Jps$V5d#f{NVUcY~gi`Y!?n_<4x+W7=Nym6~~c2jYN zj5Tu%8T2^qcwV`C>MH74 z3+3#9_rgzVyI7r){c%_t%H?y1n2+2tKz8Q+#y`~h{6@;O2a@HBHNb%Wtaqab4C!$B~3}IiANA`N_b=ID_8vS)Vy&k z0(E|zE-boV-`LP~`o? zlMm}Q0e51HJv2+-?fiUxEFvgXg0;bl(ceBYLg#F7wpR~e9rWYA0V=lDi+r5+T#@aF+fp_x`e?GxL|qdnhW61~jH5@s~zOR*KM8W{I> z6_wPt>ACX8d8^f^P@+*$ISIZIaASoELNdE;#gg>08&M6@9VMI&huv1DSG<44u}iAucax z`0N4Xx~fK3^v`IyN%z`xIb6fO=pJyZ{4YdO>?jl;~kB5V8 zmN`_hS_4j;&0Yv#m-1$jiHNvL)?=e%D%<&L#UO`$B*fA>k{-xR0ZJEp{nhVPWWcTb zt}h{l<~^`RKwIzs6%*wB+(FIAI~OqZ4ObD%=bTW=c9hV${bN>7)2pF7eZ2|Pa<1s7 zynfi%Yv(=^3xp@gc4K=374)L#*eFymkwA$;R~26m`?$ zR8Zv;t{g1h^;OMFK=)+u`Q1G;DSt;V(P+w4<(8y{gB5hyjLy2QZ>q-Gh?W7=@EVkB zkn}*-tDob4Hi`KR zJoPUMXAG+$M{h8)E8tA4a{n+siUOYinEu{5@y&drlTRzT6&eL_s{r)X6^cX~J?%iFyB39{}eDeG)7@u9oL4>83q7l3kEM8H|zwWyOXQfE99_Y zR{M-a>C^qIr!GL>5Y|e4_GvyS^GZT=@}1RMw(+)IO=j3R6V=1PVQVW+Sy4)?spgE9 z2Foi1ms;P3`?j@Jew>W@>l+*acEC-GaiH^$01{>T>CaJRKK{hwa||&(nglWgu?}Nj zdF>Z=#F-dho*Mh8VSLj!!z10TCg@E%0564f|78fb8Bb# zuN>R|zEoWoa>_|aZ?|Ql%RE$pBdk(rDdpg+HRpZvlGDvpq4+HEQW!fBkA4H>$rD}0 zvmD@rMyVtvhz?b(nHmf_E4k$XILrd`ZBw=o^h%QI&Zl0KE#~hZ&)N@9jw|A8QPX6S@1RG7Rll4rlR;2r z>DHRdbUd>(ð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

  • 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

    @Z7dei7^R z=Ua7HZT!Ai*uVP^CtRC5PM2ON9Ji%~;0n?Ksd70%w>^=rtHnl1U4uG1kiB}TMtF&& zC3=Lt-!Ppd0&j_5sTGU9cjj33WmgGl{>9u;5f66=$k_NUKyYP?)W%6_ z7wdRs(EWH-V9VNOg)U8-=+Rq}o851<=#R&)3pf1*14GGW+Se2%=**!jgD&Q6EGgvR z1{<+P6SV2KtJ%NkI`gokZ@R0MDa7GG4f+WcsB;TIqRP{;1|Vuq6rGp+g7KHIo!(8v z6CLR*A`hFVWLIcD(3w0!ycE%@;O;v-+Nrs9U*ZLLIRUN&J^Bc2ZirA{f=YKB{PsRY ztxDy&sIB*e%tCJoMlYKX9)zHHpjtVc1De9X!-Nu%2clVx-zwvB_|ggm1TH7(_yhl*TM`Rko*mQ%HHwFmBOW`__yNMv~ zh#juoo3xVrn+PS-c7tACX>M-!54UpRSN+I*R-?Sb3Q%t>hlu0fC~O(BcvNGrd*JGt zYhd#@;6tm2CcRXR*v)6e0nP)vDY0?Mk*LbPc5^bdO-A_3NmD2#+bCoJI)r>>Rcf~& z5|iZ>tjLS{`h`^J@^f(T&yC70EXd2P^2C~u0OzIrlB$FO%3o9=ZY`|k(#Ac#BSPh* z18JF6?Bg^z#e(OFpCEh7jLyVzi|2U;^ptq$c590#JpU?}us}qIP@h{tB_cs?C%1B| z8zD9ewSsH~gI@ZAldXahCIphF`^Tf%m|?A*x>u6L#ZP3&a17RG`g@PYYprPX7`P?! zlQEEPo&j()LVX@u-69-Nn+NEraD6No39Ocw=l=S~;Enz64SsGbcl@(hPb%yWOmK4x zI%k}5_MMsAHH~t`+H@mO2LO*;m#b^qfCy{b_AIJ%X&jUSlULnTCDjQ7u{by-E`p}n z$xd`GT8#6>-i+Q`7F#^D4iTUTz#9W=1(Gi~pNz3`ee$lP3uH$g{ zA%*pez0qiiMlG?(1Hd40vM%`Lm#cxa9%rBSGlD1^7sl$`=PjJBL7RK6W`ljBcmqwu ztOXjSv>`?I^4sQVj8LJ~CDVqE(L;T=13Zq}dD1=5xZO9v<3FWEa&KqjMv>#p&_=YM_pGX^5dK?lV2G_aIw`({=)HWT#j3OBL1>*sd3?{^m3dD zkVj9sJxcNZmges*W-O=Q`P$xlnL^EiB!@@`pVLO+HGfinCD9IXLEE^nCQJ#@7Eiu%oo8BfbtqKy(FC$RHWw`sB)Rl z>3e|Q-#|Q0b{nki9B(c0LEc;A$$OO-|6L_Rf#trV$59+!p~o<-bOH7DK)zyKu%nw8 z#Bpv!E5lO7tOhF+{Jdl@^9w!{tq*mZt{U6NX!*T<<7US9#*JesIFtu1sfQ z>Bnp^mxYcRID%YZdX2%g31?OJAMbb2y>+D?+{daSjs4?T?{2*v{^2l#)8nRZro&1q zR~P<_S|bXpgh4SJHoDSAiY6^Mbnd_rw&t{}3BKTaYOky{J+|jeR2q`(N4NArLA!Ia z*WN#ukU3=1Un41_)IBKhkm$At;~v|JTAu!MT!3De!TG5r%=hrm)AaD zVR!OVE4Q;B3&sNCh^r~f#RIU34CC8)pe#An^g*Qa3oPEqpC>#;De?9)MRlaW8wYH5 z8Z&e2#R0sX#zh^~x)-M!T?}53*O{KUad&_e=%Xl@}5pl5>CM z@^p3v4_sjJ7NAbMbkPmOk(yduGP`Q9r5u(BS@T8-rOBq`{F?1ZC&Z>7vbmBf2pe^H_mb?tI-(Yw2o69?IdBg}do z-#GA=nVFob@Ycz#DslmrLeHD$mp;B}cBt1W&dz(>cqPwt_eJ6dCMaRuQ?uQu+NxC4 zfEbj(XjSk|d~iunPUoD_{5*)rivcR7%(G^{Q<4yoG(l;dCmCp7%# z@11+@s&DB5>u?M&5|yc_!e2&WL`;v#A~~tr`Yu>aXtz7jC>x(Ys5M}$s7XFERos}M z&M5teiuvJsC>E6y#PwJf$_B{nYuMzRR~fDAW~2S1Idvnl=6?0asVL%M81J|?(C^S>>gTY(rd4Pku4l9~u7PvY8%$7hBn-^c>dJG{7osz?zN}_a z3PTzrT{ZDvkj=Evy_J7BgTb@erpqq*@l%>l5;Z*T>aYc&YA=-SD?b)`8G3wr;Tc#i zg(UV;_$e0{4KRo?@P*9bxGZKMsRM7q1?GEV&qsY_68wsMdn!^^l_3O&lMLUV@_x`G=iF;e>cnkuxK5>Fq?}x5IBg%JIe<8(K`>*NMUHh@UCA zUQC%iBE3Vr*Mx$%&z{-sMn5tb+Q?TJ;FmFAWygrI31Ld|m|gYBLIhLszH~A`tinMf z8#p%xB)wc0SsI)x^XvJv)n%j)lB zgOlL<$*8hKA=syso=6%UCr{ImW-8X@=<}1YMACaCxI5m@RLDnIlr|})bDca5LZpfm z1uhQL&bE{u5tb-DrI3y~c+fzOx2INzhLsj2H6kx$8Z0NKSxl_(F%VVcJ;B|EL#*+X7vdU=?O9JcUWvN=>Se^5z!KJ56KeoFY&@|AoiQ%e zt5A`ofe-n-i6uAAi6f3~?gGdVI%aGa9}0dW7IdJ7NyU37vCvEP8&T4La9^f<>`ayl zf?$!xw}(RqPxkdrJWZr1{C*I4nSx}zv(}h^LcjgujiP$gvOt0RY@-;3U{d=S-ro;0SzaJi+U!HtV7qpcFq8Br09k~Oqo{oss_Q&@eD>89B&cCHv z%`E2J7cDQUH@2cm%BWLvnOvKF;!+rCoim_zZsE{x$y zG`ME4#_uSaGC}6L+*yEmUDN&0{2!_tg;lndur$ zI-ns?E^Z6gF}zQdfLXZNM~rBR_klzH3f(RTsBeLluH}ROVBe!}mW^SV?$?huR^l)9 zyG&Fvr{8Hx{TS6BClSjEZDm@C#{I2c8)ZLs5j)_dNfX` zOllBCHI2*6hV(q>3*UKuSgjQ6Q%)*tiRWbMHm;xLou{-aXNoK+=EmF@#YNg2iX{AGic6w>QN(Ys?Ek8z1-cp?2$W89uIR&U%>rBwvgrOtvEm)E;d z>l|Uroj!^&M)dmq!O5kt!sYPnhr3@0d4^gjbEwi*$-{AJGYIWpr#n2i&9CmaVV3=# zA7}f=7x|6touuEZMbj|M6pPx6Ur`h9jTAO4UJ|p~W>_b@uTdtV#R;T8uo=;3HqBy9 z-ObU-Y`m*Tll@WV(;U)JYg{V|jb<@1Av{#HWUW?*(6!T*BH4XT#MX^Y0_RXGek4Z< zou3gtJH+Gqt>s!x9+9KgLRiqLqeIimQ;i|+@}wah>t+%;QCJC`*+h!ogK`q7Z@(;R z7GgVN1}xmEM^(iikdj1%b2^HOidZNdl$AMb=!6scKG#*)j@703PkQV%sp)BKDVm@-w4UG5@$%MX7#;eyfEkg&$c4l@$<_FTQ_|d=Xj3nvpb7-7I9$ z^+#BOi#K(6@1jL@Hop6mXW&Pw<7lc@JI$Nzy~;tonK2$&UF-0~--sa=f9VL$+<@=T znT#*6jDx2(?Bf{S!*5&@>z@ls3?&8nRh7I2D#&s3f33<-^{kt_aAZeibXaY9Ug(s? zDH3qe;TX!J=@-s#&{%>R<%>c+pPqi9S=Nwp;mz0(R^kt}gCz;ftpm!-4h~!A>6-dX zel;5q-oKYGPodEUaZ`(^03SCMZQ1CB#qPFRgCbS_ow7ABiHa`IiybKaNWNwMxX4$c z3TK+F;i$TXk|WqowyWC^cq=HvmFFw}_k(QKY{5#^Ab*Wm-6>JQbQoNM8A^o{_mcAa zhj2o7GBRm=o!BS%?`1at(_#(s$zHp38mQHBd6H{f|4juExjwh)*q4_eEk(Qq#zQQC zPd9wU97M(=$@rc(N1D#1?7l_Ls9sAhC`aZom9548%MXSQRt_Og2hXlZx&ik>MAQB5 zpTf0#jnnT>;zx1>y%nd6$nlCF-m>OH?3ZWOH{Gl!JL=x^CF76VXduDm#McXHF0;u*Aj= z%rXlA?mO%l3e>$Xx(vY|A|w?8AHTg$`cYD^+^}3YO-8$4X_|st#vc7dRDD-{8CyMG z>C-OnIne22W=S7N@z(KhWw|yyii4Bhgr^5a;3+ADHpW;SbDjjII_AfzMl_V?fZBv7 zl1S7OOIiZGAAH4frCuJe+yv1_{8|bthrJ~0d=o$%5Zl(*?5sc&X!wf2G2`{$0@196 zRB`|#&u>bd&-R}**ELjZ_Pn9(2Kk&&v^{ zGARZhH92x=^o&7qpSQw0Dtl{iKQ&z*cyWY`1*W{DwA2yBU+`v=kkTA6>{_V}Be*+&Booe0` z@`DLoTd2)GaQ9(&rvMmf^gf?TfwkQLz07<8jCiUcEU~7Qk$EFKHclx>V&V32eN|`` z?rxgsfE7R!6msoNiKD|3z*E5fcp(4##adH3vT2e`8>q<7mcAvu3ntOOytg*GwQ{mQ z2uB-|S7Q`f@YX0t)Q@nhG4!OELN69`pk)={3?!9PeNCG6*e95+p$Ln(%DW#OM*;_( zbcUGXR^MbF%m;~D;h0}ko($Y|p@u=7`+7^rzalr48GgQiuE+lV-3ur7T`XhNJYpyn zmeVZU&?80cVRcon%oc)^o|$mvcGX#lvU8uYU|V)lmtU0zl^0!fH!#Quyc&%g^GS)1J# zJN{YU%oMG8f-OMM>-FgczDW|SR7(i_tq!mYjyFDs_`SKVHN$k|yj(|TQ5-MoqvUT$ z-&<{vmy9eBJrVYA>9%|Y)aT_3n0%iu|%l4k$?(t_drPtMdb*)ly_t2>B`TEfT6`>348O}(Qf z!H174xc*A+eu0A%E*WP&b{=*AuGC`+QVDLzv%qT4od6tgj|=T7`KsFFMlk(Wyp|t3 zmOunS0a}d4eCq`EwIS_9IG&AfXLKGVFNU(iKR@$=ahosE>rnf^rbh)U7u$)iNyd_)x$s)-*eH?E zOxpX6&l%`8S?>FaCArn^F2bRKx)xK+G^Q;~ae~GNSU5U*SfG;XOmNh1)<56gp}LJDQWGB*+qkouN^bz0 z87yQA2+-EH>3p#g6A~(;&WrHglg=+0FI_Zk*NrO(FPfZDW-Mx9Mw?%_Lg)i*344Bp z;~}~j#ebxMSZ=Q*{Z(_5zD%}7-!sNE-e9wDZ*mUjb4{)@B&aY`ko+|2r zfCW0eREX=&F&{1(b=+5_B{g(!YXxF{Fb??t7>KY6pfg~xwG=|t8f6fN0<{et_qs?A zeOuUpN5ZqSXOb(6VJF>SsOuwfc{7`81KO0^sbO#tFonKdPew-U>0hxQLgueetd0-c zIF9Dk)tN6K>{o8%cYm-576)AQ!e|pg;FpcjK}}sh{R{*KsQZ*o?K@n=<1SQlk>AwaPAZ5_e4d`6mlOhyv&?+DVyhYHkE5O)n z12qARA_O6R$nwRXmIXwfEwxG%I?50Yy`f25zO9Kvq&sY>z z{?tD)f3!PdYyIqJ_CzNsu3F`Eog@952LzujfDytrcd9@(<<>{TYDDtTop%9mH6PfC zE6TQDHEo!ROB$#bW!d!ITtppP&6Ls|olYKWzx)uM6$bh{tZ?M$fS~cG-r;q7>uWxO z>d_$f;4^r6vdVubZ;4M{;U`OMeC7T0+u!~jnla<5&zGg1_SRjSU#RBA)QL58zQ`M= zLV6tFscNm@&AxyVw|*<=nb28Z1)EgiV^$10_@l3~8if@n`AAIPEu&QF4lLt4t(cAB zbo9#n1Zm}{IBLD^+U$SV-Eho~ZbqqzUrY?umvd^Voax&Xb`3oOw|LB|T=^cP2i?0j zJ^cb+uR6}QN7RYfy=XWC>EtJ@=Ej3~DPG2b6I*ZLrV*%=kNbbBwCJ2;h*2h$xS(&# zN-8zAx3&rq!`yc{0X63C1BRjhLw?M{^R;zQ8)`>lF>jd;Db~TaoC?v*@a^BHj=`!`ITXR)v~7k@G3nTZ+6sDGG!**#&s@na$#- z<@Uj5*AKk-4HrxSH-xEC?C#V=f~-&T8+ zo@t_+aWY!iY z+h`j=dTR`|PjXeZi$n5pMYph}pta_HS4hR!Pee)?9SX3ljpd+)e3T*M17~h%v?b)O z_mm$fwpQ1%n#Yhm@^$3o%-BC1+_*l#{T*xxbmyTUD2t=DVdwz#oAI+I6v57?>6L)x zhW*-ADWKEhq*3VE3wipBsiIG2>53-P+niRZ5Qh8K4T^-0ue=OO4MAC*zklwwGt?bE zbTO=U>2P?TFsN(X`*}aZMho8tOlBqdroTqMeys~oOItsZWS#DHGP}I1m=o%!TbKo6 zwX*P(tefNCOg-27Oj(GG!9BBW$I`tkg70l#H)DhOz!-~bdPw!@**~0@Nbp6J8_NC= zSOy^jz@->Xa;OARM0K z3C~`LXr>Dr)s`oRb*IW2oNT_Bh$=4TtdyHB>3f2 zDX&qx_fDBJ{pI$3KPm==$=Mwn&09S#k~bcAj;a-s%yvlOw78RwAZ^m=1{wCj!K?S6 zu^)lc6%we!i@&>O+V8BL@1qLKaX{aUf9V;f2gb5|Ll;Q|1*~=cypXRF`(HO{tt~=^ z!CNJLjLA1)A-B92K9z0tKC|eW(+(;_uUU3Cmt%=fw4#o%;TS!J18qK4O-jMK+2Xdk zD#(&v|CH8mWmv8yD#Fg?r5e5}skCa{l1Tnj72LV5SGlBv9#=ys(Mjm-33~0e8^N_# zyno||ZV7<2At?yDfp*%oRtc$juUCiTE*VjG!<>XBYr&$6uPMBeY^m_$Olc==t&I!VL?y>;=k<(}HTd{HnGVz9_hSa_&+uM^Wb#aOAonhES4NJ47^z_;;% z9|59#9Wu@0_{ZKg{AO-^UF1-#iL?HsiE+Q87*!DzC`g_Ha?@8QKfh&hqn{jLUYs=G z^Hj=Xp$GSfzFwM86b;9TJ0{con`2hi4A4@S7}BP}!@RrIHeNs}?N(KKY}$n@fH?Gob3%D_HHr}?;3)| ztl`Py13X8@bB2y4I;i|;w_~H1e4`nMJN+q1?H1-`olP4O?Yy5$M z@9obWB8VIaeK{TWWEpj_jE(y@bm(e$rulETNCqB6I~n@or?L0kyOzJY-`h;n5>|n) z>=4b}I_d)Yl-f`Dtls!bA%hRYxaoN!jT3=@z{dmVitFbldZ$>8Rn@QEzYjSPbi!11 zsh#BUah3whK94xKp0&TE@c450<X?LCm4u3|dhb(_dkyFPG5z6MwG9!<(S6M8f&XxL z)<45XoUc|iPyx2cQT({|vqOD&~ zbIz)qv-XzID1-KnHm#s4uR<=$-ZnBynY>m+KTcs)sjh~&;vo~+2}BCPF}LfB4trqq zC4wues$ggf;pJfs;L`)k1jS{=2J2+Rp2>~T*No6Tkk`^p4%d<*SX%%M32&+ofK!3! z#)6$!&zk(Js}ZOH51*=?1p|qd^6ss}X1NEHOLRhx9PYOnQMQ9x#NOUIDVowr+&nD9 z420^hJyB&w!sP5B18L7Dpbiaje;yFyE0Y-!#?5Z{PxfNTtcm(JpUEka$;A<7CJ!-mn2f{ciW!h%?L>%c}-ff7cYYNT4xm#3KBWf=};QOUl&&J z2e+g&C-47lY)^J&x@x)ZgC}5I5NF$a+JOm&Dj7RSDp%(_m)URJZi1{;#JUT^H8_)| zQPok7qJ@pAzO45)?n(W_`E*bfG5+7Ytf#7IC_Lp_w`4#GnqbcE?^BI>=Y9?Ajz0=2 zUuxmD^-O!#crcP7mQ4O#$+6U;a^-1j>C>;X$VYNk-p@uWjJ@B^%Cc(5iu;qF(D~o^ z?`SBy-^Y`>1%$mXS%E0{rULvo(2~4c#QtkN9Kahc1qn6=3ka(5Hv2RyoSO76SN=W| z&7+;);&iHMO#V0yU8?DCD1Leei5!YI)EpChSCuI-+D7DkN%p^6cmIDVJa_jgZpxDt@k zFs)-eEX7;45K|M@9J^Mm(X59t8L?vvkZOoEx_e~uj8d!TUKv>}Nugy_@c0ev60Jo& zz8pH0WAC~&*4fJUo%u=-?`)IgNlG`kr36|z4lJ%5%Z9fioYqQSiZXBO>Tz$d9ZBOI z)<1VzotYez%cfw!FN>yyKtA|ahG`>QC z$z1_Hjo>-PJr1PQT2zYi=bt%wk}d|4IWz|Hd;o)iU)k}Vuz^XR3*DF@S7G?JMhQlD zzv+%m(1!Z3?x*B2$#nGKsgDVF{B-{pr|B=`UyeRH;SK;AjKwid$h|$??)!fs)o9ty zUGeUzFWFw0EHW?U+$*J}u&8GTZdK(AK<69iTw<4m^{)=J3m{=2W3L_yR_EM`{uMYh zN)fK51WSU~g3kSPc`=ZZ_5s|a>Styxiv0cJ?^itBEUmc-BB4T(D}cc_a?j`x#1GDq zb97R#&uhz?5g>6oh!Ia6KrxxEtGS)UP4zcfdG9!Izc-T-)FxVB0*rL7E*6mShM$`k z#DOI>1E2Od!>mBRW&nec-R|rO%ef89NF+)$@ZcmN@i==i$zhu*G2b~``o(vv`-_GE zj_oF!pq_3YA+P>)KZ^R7M?MCJ^4|Ip?peyawIg0WSK?7+N?X?RGS;X5lyX05^(z(Ls}5{q{`4;3Ez@P5@w=ZK>Vur(Ty!(k50gK4 z<6-2d)I)$;svq$lX51C24FSA%a_w~U`=JqbStne1Tvd!Jc#rVNdlh^UuMUK7m=wzTC zlqLCbDa*05tC8vd5C_oS+y-tIP+FF@_nLkrDx3Zc#T3C8g2cnF2c2A)5c&Ub1|rmZ z78$rdT`8Dk(OC9{f^k0H(P04BUQddhIlXis&lfehJx8@zBq8cw+E`3ZuxmMc-`YTGd-xUF?+G8AQK8>rqkV9P;0n z_{cvr=4Ti#>Z&HCG0#!41ETU>0O`v^xja{$&jUBhSoW3bAP3#*?2m}*TTzY&S@)<9 zyXbM$5^*N{{>%YIGU+rLA~CRHqcHU=Gf8d^Sa|ju>zK1OK3Alju1GNK7NU&N+22$J z+ue!^Q=5eRecNJ%793>y2lP6UU~@JgdB}q0*jD0O&R&jyF{7oNCDbr73 zhzCF-myufa6?{!R0`+yKJEd%{qD-JzK~aoi!^HULG-$^@uVrb2P)5K5Cc#gQr{Q$# z6=f-7_dDy^tJ%^)DYy?kLh^FvR%}c*2!|DCQY=kRqk|04YCTxv53ZClyh$VtfT4jv z>r3;DHdQ~WfqVSR*vAViWmSs6o;HsvWkPU46}W)p>UN_34%ArMe#pq@hJ4)Ie1n2I zTg`erDS8(h2*>lXV4hUm*BIne(UZ-vdWrHTasm~WU5Z_tI(r$M^u>mUa;Z{Ss}eSS z4{=)fOAV^50FGFDejcH0Y(~{t6hg>2T&HyAe&|#ZCTSbd&%~uHnhn{>$HY72TtUi_ zSJpZAe?ojn`-MveTslh5gF4M4pGXk#2gW}qUQ!AdqiDWQS~7p`TGTydbzqbcP9DYN z3=6>_*a#RP8AxzJiZBvcPnRYt4uKusiqrJ9ijRcHC;BhsY!8e%HwVsd`LG>0ssv0= zeZ%&oz+G&FxHm|LKx${wgmhL1#R_I2CZsw#VRT@wgEtsSM=gwY(>49O94M`Ub1a2KH>XKr=xiboc)flByvmSg09!qZ4k~Qug)?u zC~^0SiOMkw98^?){_J77&R)A91%o0PB^BhY)VB&0vmmf z)42Cl+><(SV#%F+zdcgFT z?I*Lq#c1FnNo6@Z)1^ari~G6vUwFA1>qJTDU>f&lHm=_%w;$QQ{%++(WW$=$oa%O+i2-*RFI4V?7y7!=WQ%{o z795-r`}HQ%8n7nw=m?Sy=flr~%)+0U%glBJn%{D0&aXx^78%{L0MMsAC!UU9Ab`7B z&64vMN$h4)#cuJ!)=f~%>&A+XFo%62^a^k7rR%b>;i%Y?1#QX>nv-t#ec z;Cbk7A)uPlm$S6dVd#`StWF0CrX~*`Y~3Y8^9b%hA3MY5Bc7rE;Vc|68o_4JP@@>+ z+;n;0S<1kimb$XB3|~oUNvRf;zVZj1F)i2rphII04y>z(D3KgzP zJ&+pqHG4ZgMF!TBaa6wcrA-SQ1+slT94~4-h!2? z>%r8BI^yy};t|E+9uo9X298`6+Ul~EILs)1VHc|o6K<|#oZA5fg2S|R`BnK>3&0J& z!07aMPDv;?t}X!V-8@)^ZKv^3HjZxK%_YO9vvIMr0r)1$je%)IJu3wgHCgXpcCZkF zU}r546Ce-c;$_;7cE*SOBUSi?KsDvFzt`KlIxpaQ7I!*C1F3Ltt89)d= zD9wsMO0vz}nPc3}k(8e(ly^OS8H!X{9#oVoMfFn8=qhs3_9kq_C_ev(gCo96TAS(b z*+1X%k*6n0mOPpwz}ueo8^u4I;I|3`(kuUPh6V5og&RXymyb@G%Gnb1&lpA?T8Fw?P1|oIbA}m? zgzS=Dg?qx)0#$GBt^Gz~%@hBOTP@g)wr9;`uI)G~zRo?@5xGpTZxDIP{{1jffnqn^ zUG}f`v}QLZ;?|pp=cfmC=OQAsUFc#6vvT+tUUCDUoZ^Hrl@GCO25&v@%i^2RpQSW| z`0_DtZIXM2v^kTpNC9ggf)NpH5KX(22Y_h@tHtPJ>Z1+9lnKW7+EOrg@GB z`bd3Ot*U)9mDZ(xVnMV_vLwgb*0EUt`}vc;1mVYkXI8{Mp!1)4+|u`c;oLi9$2g(B z$=IJ>-p6(W;PMU!V)7*jnhsPzR7xEC!d^~Yjsmw`I4k365XG%k0aa?{S4m# zkFoY}|A!-*nrC-eWzpZBlx_5|8Yt*0Q%JV)Z9uyy;jK1jedAzhS@?R8cojKgC;PoD z+!QYgu)`}@hql-YMk6K1Z-0^>e8{ML+)!6fqVE*Fwzl-<_!;gGH&_*I+1k4CLsqu= zEhLg>hk7&Q51;f>(3({uUI-}j?(C+Egv$_ffAU#mX@;JhDI zQW$B-6172*m{$t>aeE`7=`J9rZ0TB)4n?j)ZyVW;@ZC}|9o8Ho+4oVBF1n?8yt`R9 zp1-%7NeIdxfqH({PmJy~6@G4>B-_~EWCG9?@vYB*?NmH{u z6uuw8&J~{odTd1agHZnD5Fb+)rmOhVg+M&gN_cIuiR~HSB!3^PQ6oTJFF+4)kQN$F(iS--pV~W$+s8 zU|q_8v*ZX{5^J`0`}ku>RaI}+OhWJ@BK0)lp(gTip2|mu7kt-0p~j6=U<}+`jHlv?W?O0SZS`px2>o={Y~aV z!fbYN{7mV~_}s&=a2)04>s;|s3l z-9A^IYdFGtadg&6Q-8K6T0d=AM|X$w=@TAW#W&nmQ=fHkPy8Pe3qs5w3m|voE{(X( zj+c9zXKP{U&{WZfTp(p!S6$iEJ)gr!=v>6gi9ho-+9bf{7p2j|jpxyt$~?H8)%Lc& z@#g_}Ps{M`$WHkeD_QTnqrTqou+m(H%g3q@m%=13Sqj8^!nJ^x2c`Phqye^{HK?H6 z{@COX9qKA=W#WYpJKaLdioBhLgTruJ2hGYWiK=328^T2HMfDEijk{e#+Md0YGK%kB z7~TW0F#@vialG-FGh&oOO+*Zk`?u~a=+q5svOYKOa~72A^N%S5sq#wqd1$G0Iq?p) zbnbzfvAxu_jSh*gfflmO91Ye55}^6KX4B;g3*UyglT2&1wkBcbL43GFmjgdUq91DR^xT8*w9UxUEZn1Jf* zNN=M%`GZ$aQuOZi!SS-y=NJ787bd4a>0}l#V;zSeTgwVk%=k~UE$8233v{!7 z%}JRQ25-4K9rgSSV&-dYWj_pDQy8aDl!>ge_I>4I6aV=W z(I+H}ji=VMq*ct*K>Uir&$l%#dkJSn!K_5Wm#WJ z>6Q6%K@C?vL3A|^u)m!y(4}|E#O+Yet=E^AUJEcsUt3sZc~LP@RS=6>E1gbaJAN$Q z$a5XJ4+i{iO$Sn!W{vBKl!dtJQ3otb7z^ky>3WyTvOv#I7{7X)0tcf>qfz|BY$Fw` zwAb1z<>_p6U+t$Id@@AxW2t&UTjx<<^&%S`l6~6Cf?iAo3A_@GcRAVmqUqe<)b4H9 z=3)9~O^`}q=B52iBzaMj!cloQ-eMnCc-mGolebmk4|samLvE=b4y1wdA!-k9H;UPIG8i2%S!W& zd@pMXi(;AQq-mlAehUun6tq3n31r)x8l4+1E^snBCxS%M2Mp(Q1`_ikFC@Xz=HP?s z9g$1znkD&JVGcX2z`yl*@;Iqd! zEy!U2&^z1RA!Mh&rhDElrt z0(QzBa_zTxghh%DIQx5VisM6hn1VJ7CglC};U88(_61V|?n4&71jc07kUuM-<09AR zYe&+%&UkvFDdGRHu>aFTzLvkyc)WN*fdbA^)KXIIvBgUP20=G$Sul&e5+d?KZuleA zsmOgYq#uQ(VFvU>zRWd${$zAsnE-zCQ*v$wm>@G`Zv*EDirCwa0Pbo|81)1^ZxxvA zOkw|*!%A~?C83S=86f5diPK7DzUWF75Bds+TwONNA~;X&iW;#Xk1!)%nzca$m@E{U z1hkE^MOI$R_Z{R@qo2*Z*; zh8bsZnsvaKAkElXXqct7+51Un{tVAEka_XrK*Q^E-GxQMJHcEg0N_`)e%|Q;TCE`4 zv4(wfLw(&h+jDpZ8~%B*1N*|0^_oKX@O=?^!w@@Jd)1TQpoD9(>#I6pk^bKOtq!)* z&fCL#fR(q%+j2tu!y1-45(JvY7v4XVG0=f16k}2f(~37e1>Qe(Ci$BIG;HCcmB2~< z)U0Q$;B0EnHL@g`&GjHgzd*(p3%(GVmZx^_4dW|4VCXsE;y!+( zfly*Z7X%XG?B^u)|M)aAZw#^i2spHFezVhQ13=1?rTZHLRa*deqOiIC;N|@nYfK{w z`4N7nk2ushBwxnJk#^3lLqqu48y3ew&_HsMKR8E+t6mPLgSL{(DW|aK5INxUfri|t z%$LqvWP!IYBpaj~*>p*XMpaF$`SFQS0F7Q3T(zyw_g#o(QQ`+16C@#1zdI9SR zj`86qf|9z%jyGiFEx7r>n^n$&+7Vy z?B0ACkIk&&`+<$507=cp`aDMd?#2u>vb%e-fH7Tvbzxe}75LG^XYQ~x7&@8ln^kIU zQsuHacXj18bJQ^X!3W@s39v9eR1BG?bSiPI{q`}?u-qsvi!7%Wqwi1|Ikvw^Yp&29dpyXN^MD$@HEfNFT>n8s8}ZiP~zTsD5b;%f=^; zJJo2KY9gA=b>mHvoHA97qzDm)_&oVDg^U&L-kF)qhI)Q(v$rzZG>@x0M|pi*+lM=D z^tRWj#>G-rkUjpc&sXPLZ&)1lbLw6v56lc2nwsiMBwy&v&Z;7Y8HvVDM%2cCB{wyx zD9*9Xa4?i{#6Ho*!jgXb96t}B^wro3GbJl+C=E&lON;a`E-!PsxEyEIyEm^kH2~4G zDbB}nZC|3;_xAW_NB}aTrj`ta!QZ07q9}%F;Rl+eI#^iO(CZtW{fqx6h^fV%TdCF3 zDfD(kI=AjMQZ6X(Xmnz#hESG+Sx(;MPh$8nN4L|*d-koI_3{BQzrXfa7lXQZ?z9-9_s{ztY`^EQQq8z$eKk%Q!Dxup1-9=ZOnqxHVhx)${I<)+IS zv5e8-pQX}Ur!zlkI-jan#U_r|=qCBK>*?VhAIUjhSM?*_hG2Fu4Zlb-k0e9G{Wx`a zxh|fU_o+{0ivuKs<#0Iz4NF}%72wZGd8Yqq09`-Hl=FOmsW_{DrzNa2UanvP_(Dtl z_9615U6%AERPnB2Cwf;l^uFLjcYe=6$=|u)Q~to7_Qi1j~CSvjIOU|ZQjSG za&zLn6_(eDK7VWl3)H{D`FIKM&1qerqea@)xocdLEq$ptdE+j0M|q(9%F~~6rNDh= zR(tZ7hmeC&Qe!!vNb2nC517Fg=(iiN$7z*lW#i&#eG%yCz+^w->1I5Gu3^_tHUsyP z-{n@t!JWG~hR9YIDhSI*>{CZkDsL-+KAMpvMl+LP&!4NWZN5Hu^YcfqYJe@r*G*U; zP?U#MOGTCU`PeN zHF!Dndm;>92Inz#_?%uJhk6W?^D6^vJ+0@pNg6T~^s!-l3T8K^AhOP*YH2$_TXGWf z56hENY7Dap8G_XUV=jKcqmy1?e*F=nj{$@>naix`JX7Ga0G&KOWEa0_-Mwa3wwUb4`k~B^uhQnz ztB)szZitvPbIX+&IT)ftwMO2)qPIRjTtiq2z6tJ%FB<;i*}SsGQlm0ueNGIdWwAXj zU*bLGtHT><|V3ERJJZYQBlzrV9v~?Ev#=K zUp;fVgn2MEIa>OnZYe}Zva&umQOg0?+l*pMKM4K2I5yJp=XWz}j=L!YmU-5wws6}) zT@Ciw+?TQq5oxac;VUMV*f;$(^O0uPV5z}utH}TMkNGcuqb+y?2Ts$6nBGd)^1@yb z)lNuQ%bVB=59$V5)NoQe`=EhMX^${@wb7$5Kl z3VSrcxLX<${HL>5`sz-pO`cT8)Uf72K*H{cDQlc3GOVDIY^^7ZhN%%Os0n`jMdFwH zuN16zcB}OeX;@MggnKpH;%vS|^^IBKt#3WM2*Q%D>JUs862S4!$eMmX@q_z>=2Y*z z-IqM&+hJ@TsT%w5Tz)yRO&MJtL?||P;IL5*8lEMGbX#;Vom7y+rFSqN+7S&@k?qnU zr%T84EP?KD;#swZ{aVr~VUtf)C0QR&D9i2_r0h+YU`w1P!1 z+@7gtNTY76;_3r?jwflW@-m+k=B*^FGSGh(prq+}>7t@0uc?Ri0zv1}cTYr$kOa-5 zS~Yz@e#Trq8>*srknw9TVC0pqrfZIH#{{g?cc!y!mZcD!Lw07OI;BN}SCSQElnk|I z7xD5*#lF9^CVY`Llbs@`XU)#`%vN#3JfJbqe8RVK2EELR08|i71t{}>Sc8C0Jb3N+ zI_D56cnA6HWm6And4^fWh_7lwQS73amC6ec5MDozLB~ZMzea6Aj<*3ZBIN9}4{|E} z`KHb769llgoOGVZq4QCX3Qtk2yO@>JGsr)z7hLO@YpR4*n?Oh&rV6w=38BxvYxOC< zU_9im3D2o<4~Eu>>P@eGo2a|ZiB52{7JVW(@`*%RvWW6Kqi}uQf@noKa~r7~qWFEA zVCs(tJ5NEioquVZfCc?L>X0V21N05#>Bc>%@)ns=6o_N(mh-Ek!~k`^B|ue9+EC6| zoD@AE+3eC~V68h_YV~5+`b9*_U8PG9?Ks8~%3Zze?VwG?>ontv9sl(K<*Y5cgXi6o z2Zq0GU{&AWJT~kJ(7uf19@PlW=5JwBS|KpPtXz6RlCdvW+WrAKmq8>eR5gb-BQldS zZMw_@X%1#F4~&h&`dVz?*(&r~ISdiuO_Ty7{>rMGJFr{u=s za%~yrY`d4GTHi23MQ)`tH5Q4m$`lrnuC9cCx}y55nOlQE7Ryf(LL;czC*h4e_|X~#Uk?xkGNAK!eJfSF3ww z0r?0!6!8d@ zTy;>@w<2s>ewH7wE5$z>T~bt096`H5q3!v%lv<6F?gdpixuL?-STfVWPD#PGnsms}v1a}##Bmd27&Lbm-XOkKmdTr7 zPIJvNb8wQ||933b8@#N)%P_RFpR4jVX?|RdnxvnPv}VTZ{ZJpgc0ccIhPZbo=kv9% z<&Hk?S&2qRjyLE4QEWs3Jobd`<)A-fAimYwBVf@i`n(1}_p!>sNt>865 zm+{Zbsi^{HMM^bZv!1hhToq$2&R^tJ1j^_jpmAID!`AzB%w>?`nNpFb&jKWSa6su`O@dfzQ56A8ir+xw=!2(mxn4ml|atXQy*&kF{hD$fAqHNbp=9arAp&Q^z^5Eus3=(`7RA#xKf5Q>ONs1riA*Mrj(Yy zlzEmhD(_q)uSGq3b%SD^-bi)M_Fke&Xzs8+MG5}!IbB&6DCQ+&pxkO5tJnJD*=9$@ z^mKpUfU`DK*gl|?#$3KZjYXauZ4T98w{cloDv-9;OGSO98@c4xxGqLM)myz7Wd1v3e{~ga?ed%OD`z`xAJy4ek3s6Uii127g4}w_wP`|^4SIs?hSy*F zjlSA(5Z2o!)5Yop#8)-yHFYP}=v@NuF7F+IO#d0i0XqKiPW|KszSCW9knaM7Ra#Jg z%Yn~7=i-1e$mT`$Fh5^G;&vm7#4q6apxMzk!&nwceetgjj_l#D$jziNd+Ou2gUiM} zg%aUzDvdXDLj%D%vS~ZY0bfnueQUTBrqp$;xtcku(;mT^rNUE=ry;d`qV@~a~z9WD8T<2gO ztt=cSzL_1zZJ>{oCPS?yF;4%5^t>?Biu#j2Nhv0na?zl2w5>6-ql~@u(mY%8em?xJ z03tkgb1R41lcx2`k?XX-@A-2QxLGCI)FJC)w1U4>VN~5xH5mC?Bg1Hf+idGgUS5Km zhRhT0C(deIlL|CK@AH24<_!n&VSpLyBM1Iv(Oo;A6L6wAgVh(x?BhAKzKD&-_l)oF zrzS|2`_sx&a|K=+Gkz;7D~;tk*O+!ZkG`!{IeSi^jTu)*GgqduSdQ*Iu?GZI;Oz;D z&JkV=2`*}kvvxHURo5ZrLKh9#8*NKU3J!BtPlkd!7GsaaGtyq3G}QV361h4SN3p6L-9J5W!jcRBv9qJuGsqRWybs64(geVJ;Lb$b1kNU=pw)mqy} zS6ABBeM#HRJG{=?&ijCWMweSdNsf3b@&loGXwelRZsUW^Kb1V)Qz( zq;Zb$D%(MstOGClwd;LD*ge1JgdIfLQewkaKT~^=zWVV!Vy1VIfQOg5Hp;?~)q9`IUw9wsm=)CPGmogbA#{&&*6USwuP(V z{8B0*ZNQZuOmcz8FL|+57dTF(%;-(mrma9D;KVKXwafnIe)tDn8ffA0?ZW8tdvG@R zPQ`0CEdfK?ciu^7ym%M4d7i!_&4}Y%q*8L`*IwXQRP-r5Q{2&6di8Ln-ARQiOHRq0 z+d7A@{=B||NQ)2!2c<$Sf{25g76N^}r*0Jb$GVc40E_bHsF!lWI^4P{a;eeJ${ePY z%X9^%bh|jG=HFGE-~JGKenkGz#9ca@p>sKNEEc~|k_za69gORRq6$FSZh-VqkmANy z=cGN!_l@M-WQ`)^yF5HisDctl7|Sl7kHq#RF5l8~31sh022nRLOYX9Ot) zI+vj&Em+n+9?bS#6MS&aDOp%2={+SsQh$gNdH!Qg zM|SXU(5DpgxOmI6J6mb?4D$c^W=Q=nZ$`fNKddqMT|V#>%x2fJolt2Qt`9X*V$~Pi zQR6|-lRfq<0}%s`tlM@qmeuZ8lc@8JSi_26^&8>|FCLAnS1y!x{(WP*Xs%GhkJN9M z7Pb~>%FR2H9Fy~mw%^V0>b)Y~63l|rDc7_ntd3+DIYOPQI<-+-+Ktx#@{(EyF!*g=G( zQWhvo)(v;=1-FXjXSwFJuM1y1-pj-wfqt9J7(bjjvI zZJrd6$))H;dHZL+Zu6!nise-NbKhwf`S zQ6TM)FUIcxNVopj`ZDOOQX0`a?E|^jzcoBp7YB$L@l%%}f8drZ<^0MOlNw4Gw0c)s zYFMhYc|QC^M`!s820mWB=Tvf9L0cnGBJ3g~*((gW}t?+45;8q`ukL z-%Qh$6=fF&0o20k8x=zu+8@}?6Ee$2RXCm->}`G>yHb;eQ4|}RzX7`pF6T(p$YNvd&7w39knhZy421yA`; zSw+%%UdrbQ!RPwA-z&^Zuv>nH`?vw^Wuo!~Ok;6-Wc&Vi|8NYf+FRpVz)A%+4%&4) z^J19yE=F-Wl`&*_X`>=;zY;t&q2n= zzTCnlO!FK0%!xzf#}TX_PU6z>&q{`L)_Wqo>~Wr zJ6{z?YC3W2;{U40oLa21pmE$_^GU^8UCO z#O58;mt|++)^(zPdOKjVKm@x4k*YcOn`6(a!rsBDR@XaaQd~y`tU|-(cXl3p<(R_6 z(PJXm_#KYVn*X$MFE>`jy;nah9t7N_t;Wj&?p~n|LO!DM?jjZ;k0B|n@YSqEs_;U@ zm4W)o-RmVIo9;<907q%aZcmigMD;Z{w`ib`nM>Hac3$P1{SJmI-iA;eWqEXZtL_^7 zWeF1B$>sAlA{MldZFE%8ZR%U!@$_cyLnz)YUC+xb-24lO!*~CLI<#D+UmABDk2KJ0 z$xXNw-v+5cx*Hm6v;Wc!^M(f}I+8l#=yt_*zx}}fN%di#S^e))VX_)letL3o>x8hU z>laUMug*Xv9_@x5(!@czBR8%D^=iT1uDvTS%g4yczCWN z9Qo8Pf!lfym0ZkANI1zFG-f)G`A`lwOlF7Ilv8>&Bq;`lUP zBBgMiJPv;CI#-%Qt#?pWi412lD8nfqo3y4cE)2CT8MH!NXg;%1BqFi3i6BRHbf!nT zL-@?bDwZV3?0vrcUUI;*v$<|G%TbP|en*`4QoHA&BoqW`MEAE}X*DN;js5Kz&ZpnBVUiY5 zl%X**tD`eWy-6CDE6~qzxb&)Cs7C%VDe*uOmFWtN$3LuRC%5UTDV0c0!4(M4%?V!C ztG0_k^-iuClB>zIi?3xq*D8*dhFWhm6u|n4{J082;~WEP(_b=GqXArVQ$C9dr|aCz z?%3yZRCL1Gl|q|-6X>rlRNURe=LURyngE=M1K^$u*MgY&gzqBZNk!SPpvK zf;J?R2pLs$oSY4aYQRFa68dc4-CH1eFN-IiwVfjX9l;Wc!S7!AQ^+C|z~A z?hc^FO1W)|CH;1#+le}#hjnNMSr2aQ^Z8fZ?OXRx%xR2r<^2Gyf3V2wnVeC5=2t4P zpz5eUVilI$@%>KraCr?)zPL^$VeEU^>F&~U-H2W^B&Zp-+G0>FRFF^0tc~|Zx0X>Z z`6`v}fK`)PbMR)k6!1C3nKH>jqxpW)xEP+NZ#Ag}b?oCsAgFS0ym_t;@%#E9L|xk! z+9YR#RQ06a@9*uLe5P#Q=_(!!ABd#vO%49rVCOI2u=WQQXrKQ zetVI&{^l3&x#}EiUuw}xmU`H8SWB0+N2*(13ad6<+_pV9Mf<%eqqXIM9ObMB6hWJQ zOg8BKTA!3sY2&>kP7-aqHqbo7-8lt}E1`5at*~ZkmTTX8wj%t!m1U?0-_wV=-&Z!6?DD3?#!I|V0+4WOlOF}RTE&);FgMO z@j&uc^S_~qi&)MobFL{XSrzu4^}5m}9{n+3Z5x*TH^44ghC{12hAHe#_u%m*SF{SC zVem3{Pa0=4cDJ`@bL#ID2xvb2f{wf3-8k*4T01e{eYR)};#>$e%wAbdX>RfvEbfZt zFin+<`&pJ(P*}X|_e)+%&EzB8keXkJz*sVYuLR{FgWJl8@fnvNIPN#Ui>fMNrJ{B`SJ{E-f(0FyfRbvM-UG&e)tTx?))oGv#w3M8fk~hz0KE7^N>?%K{ObO zm$2J76?hJuW*$*6D0n{e|H)T;@U4%+lUpeT5XzJv(P&V;s!;&vK*m;|wKyxFI8`Eq%wO2@m)rHVg`y~HE4(hEw$OYPQ{S|~Pyz9T zLf}-2k2sVLJNe#rQ+Y->*&$KmkXs!6s;IOuz9>M^k)6YlU4ZkQ4WY1!G~0HfVmZ<0 zHnymkO2uM%Uri<+T`CRQVDn-G`V<0aHkhTk0J)(%D)07Ov~O*7>nRVFd+My&duv4#{8gM}_$6RWDjnlu6fJy`cH||2J7`|7Xc-|Np1|yR<9J zG%~De)`@a4v`9_i-m@1Fvm>_@I2Q>Xx0Ik2auH0S+C6C9!`HScDj}vpMWShnFTQDF zQBzXHfVOa39?k=Lfj+4^xQ-6wM?-TRq!&_i9n75w>4A@U>$(T1^_X!%+<9_@I`7vL z{6|efpIxq_^^qr1MP#_;<3|Tu6Z)mx1{)$GW;ic!rqPko+nXv%+K+2JLpJCO60U17 zbmsQH-h{Gy%}0qp{$aiQhm}=XqzK~25KgBN<`~_-vTk8%#cCpJI^o?slSfCH>BZ~X z556~EjGRvxZ`O3t^ZOdWC)7nTe&6h6YZOUCA*jzSY|X1W{vzqX#&6(-5XS9_6$L5o z@RMA%@o{5T&={C_uk$|t{DT5=v_17BjCpbK=o-;qAVmSqRun^W&N6&!<3mtOn{wTk z*Ebm+0i11Xg|Fj8Kbg)*NXbwL9JESl5U$hKm z2k;bL;vDT?jnmcLd0E|Dk(%A~y2{5tS3210U!CecQF#>dpv`;{v=XS#9aj!35vqC} zug&=RA66VLp=ieDk-e6(n!7bfkpd39lW|_DvaUI zOUZ#Q;rym=t$WTm94&Ue!Q!OdgE(AuPgCQ-dgi_k9!DMqlH>V~?(kvcccMW?DPBHiJ4fijgm%0~3f!+U zoA+)CNasn0Hf zmB7l%26PptXXex|h8eT6YFc&=FK>8W;Uz9DaXW>$U;xZqQQAumDM)I-Y(P3#@MT;9 zZ~T(WT@42M7v3PkpfWct{Iz|sX>($&n`$e-WsCM33K^*%`9j>tyjes*kv{AuBfp0}UR3VzbH10ed4 zAzdy%(tP5x_ZO|=pAzdNT(*cd{KI0xg<7aP?#9q+XZNB^*FJgVQitfm!F>->6&9xs zt-Tw!*S$y}-BxZiq||0g+`nxt_B*#iKVp`bh9y#i%O8_?ivEzUp(}`qp#?J;q+sCC z!=nd7S?_|bkG;w5^BhmGGk(LrQwQ@;ex9dO#u9_!N2nb_OAp!pbj>a0$xbtU=pRyi z?#({~QF1H3s=3xd=%ji#qtHa)qmJyOVUE_!2FGaDI zT@O#|Z+RNN_?-AHxwdBaJoNH~-=p>1utF0vAjfAk0r06c6}5bY^$K3VbPw^QxQzw~ zzQ`0GI4j&^;3gkcsKHedboFk509sD_0Bh6v;7ubX$Y^Ut{b;$H;JsJ%mt zgR{VlOo1VpQxA%2Rm`Df@u1!Zn2Td@U9Ef3pJ4@UdWDjRckj-gY5gKNr$@$1&2KNB z`WWjqlAdibmw1wVAqCMZfQ|X=Y_9&AgRb;{#^j2o5#uhl{n(BEHeiWa`zC;`mrBck z^*BM3nqd5XZKY{z4)co3-db~;XxJ4?T8cxhN(^TKAW*gZF>c;*-Iydbn#sf|1-eRZ zFLOz0JGb`r5x$_?1&C42P7^@MlkPZn{{($qn zqqaiw*+31O-%1ug7C|8#w0Q-B`gvwJatU*#3~98+z=@IuF~umUra{`p(O)KIpLCs4 zz}s_M!3>p{!F^4H;7*mXRA^34QN3~y^E-`}hu6}1c(=5}*&zp|gIk`(pXVtblHD}s zae>yPcIT!CUmi844ROe(6*_=F(_IBzD$irTcZ2Hu%s8Ts-xza!W!NpH>TxjXxxP!c z7xL>IBR!8)hdNAXHFz6M!qd-!w=VQP0%>;d`0zaXB0jZ2!0x-v^VIOxmBLDBtjmk? zmxMl*Kt9GZZ6iXX6q(7wGI@&9Lch%OB5{7SsM55b>U(qHR&6S>PP;&fRV!2Xy`t(%z|k6g}R@Xk*t&?!O!~;scmz5M0?t#wRFzY z4aZc=T%Ab`>YWxk=sVw!^>NAI;&*oSqE!&#=0Xg6#=w??s){z4T z7!R#BAs<3KfI9Uur$ZTJ+psU+7LteV+>&}mMN|RAT2;3?%-Tg8 z-L(hz4=gS_fWfH$4S}3x?Xf7(5=GZfwNsF@=NxBsabZ{@ZWIic6Zpfk18b$zm;ZemWzC&z`zf7qunwdQD{!}| zMY4^RYn^+8`yKOa4d%p zgu?fwO4&ybCM(v7?d>B1jg+=1tT6%)H=>Oc8_Q7yzi(E zmZ%T;ocqaXjoI(R(ejJ_m|W=L$*U;5EJxiZ8{!>W`5s8Q1QQ&(*Av|s2Y*2Pm#iYs ze{wkm-aY$ImfghQtgg%IAUHA2khyoq)PeI6g5d*xhP$VctJw`XfJ+$w*c*^a&8{DJ-^sj#bFYIT>3(sRU{{(6T~cV47Q=J}JqJyb>U zO;P3R-d98<0T$)jd3yKHNDUUmu0NDJSd{4cA8D(74^ac^v&+>zGRp_wNqH2vKsDXT$&>EN-RZ{1$)@7N)!_ekDZ2=r7HxKHopXaOEb45#k zf<)H(pE9*40>(*VtpI-c330JNkfo-#>qAKvj!cRI#jqJgg_36M8z;d164H4p{de|l z3J@#MB}sQp9jFF*zS2)x2i;Xqm0^DmlUs*Z-ZhJ)-3)J+lzA~xd>@sUwCviv2$P{s zehQjLH#G*7bn$DCEYX!c6hNpZ5YJ;rY%NIe)`q{}3isXR~RDQM?b zt{mck7C&4!pT6u$DlPb#pVQbXgU2o4&-{ITiuY<`Yb8-CN@UFtnpMSE*P!M(V4okE zo`HCnEifJ9ZWxEPIcdEBWjhGrov`(lR{mMMOb8FC(5dwH5q+-laX{prV7%kaQ3Sqv zJmpHzzNQMtnrOoQ<(BZ>3lKfh^cSLUp526ss(Hkdh0Txe3TK28=$-NE#@qaG($)yN z$2$mw(IB1|vo4X`*_KB#i>9(`HVBKO$;h+^I3asdBYtx_S0=bi08~0S(#^SV-pQkg zpjf^9+nYVt{rIENOEmc0SsZL0o7|$!8c`Z3$FbsQBNxoWdrNmJy1ME6?7~6hy(?j7+j~vBn3|(5dxA6{=*xts5V7>7%}wLO%~#rv)|cykavht`brz z6m#c1)E_?5Mr5>bw33BG)sj?Ie=ah&UdewU>|4$|ChkT0MGedi5UP4OKZ7x>G0loJ!*Atdy(f8rD3v#oejO{ncc!Q$WC9h$T^cEay zLX8<;cqYXYNG55^S|=MtX+1r`;W{F5=e?8O+IJ1fFftS>%d2YyKY0SQv}H+t5J-ls zc$v@_AY1nL98@o6^>h>zo_r#AFx7N^z;feujTw6)L^wI6t<^W;l~^dG4|q8!pU|B? zTIUuGyk0%oX@x{PSf_R@(qy69@seb*@&~l9y{oUsdb@zw&4uOq%kyttmhAi<84pDG zrr%d7Hb1UgK}(%i*&;c@2ulC*CtR&K0QaIr?A;iL`g69SPN!>vomIBRkus^L02=2` zTo6jhrY($M%==T$PKt+rRjIE91ssJpe2U z$F7MnZ`t4C9Kr9z?Heo3VRr#f4h8(Vms>pz2N}yM2Tq)i&0G~f=%p!Sb~rd@lVBM} z`Hw$Jxw;wGLc0a#NZn_CeY89Gi8Vo+_)%8h=11Y#dTu!|SP8s^Y_U*!DP_V|`N!^J zk4H9{=yxI>>^R12B=I=Ai;)k@f&(a@W;g%@6=DT*=BC~J-Z=XR!?Fxrqz4UUv;Lwh zxWWJfs|S1SK~aO#@JT5c<7_V+nW__&51pH2L$U4Z->(gE+Zu(k?;w2;!r&?^gdl>dqMbO%(J>*SMHpZ--T;09?0w1*OZ_#^FKTpMZ-W} z7dv&kP@;5y??i9#m4>?I2S$VH;725TyL&)2+?}lIy6!M1O?J?Y6VLQDl-_$PoBEjQ zknMN)6`9Dr#|bwuwo;c7LocRP8}AACmnhIBPm+l?%`fP!r;^R>Y)Fxm%k#lp-qq|0 zQ9D$Tm0k($?;po7*-a`3odE&7e?m;@xCd8f7pu~5=YK&xqf66WPRw#g8t5G`V`|0C zl%2#XobvXw%Wkf%%wI%o7%06wN0`WZ(pN|*%DmHRgL(NtF6y~L$*GiI-!=aUF<8{w z)F!av(Z60h&0^tOQdlYsGW@Q^CPVKlEC5b^-lj1@Xhc5Du!J(N_7I{=f7=KsDQZL6! z$=GRCXghT({v(mldtzm-W={>3WM#G>&tKN)k(h($zlmNNZE9eqgUK%u^V5M~H?w)y z;Qn(jU8l#+Sgar_J?JrU)NS08NB$>rA`!rJe0}#HmhQuV)46xGBR6FTX`8sJ)&vr8m)9 zE96gq#9cZoTNm}<#ooxno!qdPx8Sc#Hul?18a=_8IE#ObbDy;|SLit?*OvQ8&RmUs zJycFsdwS?f7bW4?X5|es@L|y8KjIl*`i`!dygi6EpeZe}&V%=)tOk^!Ic$~Y(kYeg zy)wm2#uEmWFA?__ULBWfmsyzFR~8ab!s|8(8r}WmaJO6QlA>SM2{d}v-wzLdZ?T@; z8f7%a5ue^@E(kcE z@iCAIm^453YqEYRuQb%#9uvA+S(;NSxO+NOfepVStOf+(^YS}?s+NO&7aLZS5OO|K zI{n%nJ}k17kwG3M0)k2ji(W+V_XA^vh}Uyf1i$*9Z4o17^WI$3?)y_jylK)3`BkG! z9pY*X#J9p1s6Szj=Rpt z79whZ{>1M{;2IhDP5O`wL(aPrrt}YsGUW&}2v+ouE*A)fZts!b!S$?<)ry9`rY*Z? z#e-4LoQN^)?U_#D4deH9p}_6%4AMv)*!^6AT;RnASJvHx5)|s5_+=39b?3#Yo_@e$ zYg9Yf?F6gyb;vgcAVT6C7;cy^`rO{wj+|I_mtJN!%plH z%{T%1P*e(<-JN`O`!aEKfofCduc}||oMvE>=EqCW-{a}dVA4Q6fTy~P0c#&YPkuDi zt~mV17^`>h8{}N9?3UyCpymGNfRLT*=hu6GhzPKM9{GJNcj=^tJhTM18479WVPKz% zJ8?z?7*#L%4{N7WffXqZFw;@lbR2X30k67yx!QxCa)2suyBNAxAl26dURF={L@Ck~ zCxY(>?dUxULtPA^4 zaJYzGYuXpqYy--4-S+P$pF$cz(RB$SlwyVF4&I1MR(G`BtI+vAb(ty=3R&U4zPz-9#j5>lkY z^!fmA(y~gO$oh>|eAK=JnK`3QS{o6T`8Z&PgxUK^XtO5t_C&&4nI&J|zTAIOzYybi z9}|wbIMHssHg;Y0#a=l{(W&T)s^Y-szrKv}6_uZ;&xOA=B_CLw|Gbpwe8l`w`|th+ zUxhN>fJI@%;Km_*H^IKNi94|V%c~a;^?6xilgj>K&8-709gFQ4zwStlrMD03g^CLU z6m6}{p)yR!Wp>UHi!@~m&DZ|1sxp@AT62;q5EpKJP43}=c}c>fB?-li?$BwQsQsVi zWsa^kse|`~kS$1{7 z6C?Cyee(pW)gF*-jWqP9R6B(LU4|s&g?2LvK;HvlKz~nVw`iLRu67E)J=pK4;s#!a zVE8Wryodo{>8XPNgR7ex*QYD9d?j`6V3E!VAeSV;PdP`4>D&qFE-ZedvGpkiT7t=6 zv5nD6`A`u26etm}yX#WcpNFe5emrlm2+<(vGXYXm7%#BWs;>)~2a;(DmHk9yDkYD61Sq25=W*V_A} zw`<1LWxe&|dpWSYyy_PvYyKYUSz#htA~yPXqv$AL{1R=r6)&Rl_g62IvgtyEj*+7e z%O|{dSW42?4|)>?r?r%;*?omHIFzb@MXjHR6eqcdlm-&CBPFaaPjy^^;t@x#eJgw} z*~N`DjR+mx8BxmY#n;6}p^sEFH946`CYb8RncVwNI)&VJ6fTB?{$cSYB)5e(H@*01 zA;LnG-Ds|Sq!9l!p4@qQW?YKP+xAE2s;vMqnA!Gyk&>O2CVydZD*wzkl9@%kp&!jZ zT3r63v~usGkhX_tBb>J#+~GSbZ#ij4lc z2is*imN-XhljaKR|4+=V|Miy~fzLR%oP)>b_+4oF%xbTBO|;}<2BRZ?&EC#F6sJyX z##VTq_dLbNjf1Q9rK3h*x0|0B(Ad0d33YJsG@J`>6!ZH9d3~D_>A-v##1~Vvy&_Uze(WUwKR;Xs;k(>55_Xz<#P-HvrD# zR>_m`C_4I-`nE=?pR-8gnrfQ)7IUenb??*n9A{CQ-^7$&7bcgrX4g!=hBRf5sMo!X z*|6}DNkbK%NDuiO5_|YE*QfDwxnU6$SytcDSL?XFv@-hS4sX`MZz((hudZkHUvyN> zHy$Ii87LdRmm__@L|ITP$OA{K@8c(0d0-meZ~dRJlhzcHeH@sQ-*a1I@uZZ2 zb~`S;&KA8HbsUv3ntThSM`9JKDU}p707DqPkeT+YUVY#Bd5M_r8-3A> zXN8brqNm6(cA;~#!Wd!=&bK&b*Y;?*sj5`qs?!|G*+-lwU(lM_$|!wnvzVy7&WY^dW9a?P9wXE=B`J!19;v2)tAxO7UUPpoA3yt_!0 zk+#L8nw!b)y$ zrYafjIg9H*{Z1-DE?VBn%+hf>BpBOaWOrFltD3r96|w}Q4u*8{)8fg5c5-K;C7JQV z^&bkk&;0VNP^ASi;fZH35xDtBrrU_}IVYi?j3mY3`SkV1KH8t6+)&G|A%;>TH2YYWF6=nFh`+|Tdij;ulkRmNe$Bc-yh;)Z^mvoJSbV-+V zmo!78ARR;3(A^y~FpTGU&pKw<@TqJA^UB>PAw zi7-2ijNgB>+1PUnp}eeAu3t{rvVCQa0IcuB*WR+8y$ENH4l}oNBPGTSd1-=Aans6w zgMF2j`M*$_O9%M+MMW01bU9?h%9Fv}hY_MRI@7_O9zib#O@H-gJTUXndaX`VPGwja zmGEI>vGeeTh2DA*Z+Hm9Z990R8qWH#Bpa{-62SsRxrcZ9I(g3`48x8ZUDcfzDlj_vccOLey4}ji&4EXi%Z{3AEwFGe+zszbM!F)`JNc;izU!nt7urxYn&h(= zvJmih8oehvS!?aDI`>iOM2MN6pwk?qzf0K=uqT=wqiD$f^H+?-^=036=YZlN=i(<; z89^BtYR%x7cOZwu-CEIkrX-d?}1Ab{mh-#8%mSo`^iq>fRBDN2g?}|F!y7F zcGb2dO%y>I$NoO;KdiE3Z{65i%*?kB&%z9czt;@BEIyKSSeb2oVsmBGH7{vaeR|pI zYwo^0*=O&c{Vk6oB9bbiAS5I(nGcgkphJR>eCB=QW8BZX8G>W$+4~O*`E3DWldum- z(U&c@Ykcy$`>g%@-C>fn_LIS1$ozQN zRkH7xL3Ncj(6vu1B%ZQJQUOm8az@qu$bwrFD$pUY)4}BT^Jp}1lprm4UOY?XxI-_O3KF(@KyLTC;lQ_C zP*%*VEte`;?3;jCr0nua1Q5lt+jzXV(BJPD12!8fhhdCBuRZ$UppYf9^H2wBF`t;; z`5L;tz=Ak)3|X@^Rn1)}#c$nd8`r7HuqR~#^_6(CUlVxig3!@`C+Hr85|Ho#jroda zt3S;BJ`LU3C8qg5bhpG#NHwSXX8mm_M!o9(j)rXh8^#ubi?Q&if&=j{hCwqcRKD51 zhtWeVeA7CF(~Z@d(;9+tJzxK-_g`L-ydNJdmEidvXg}D!FFCQSseu(Yd6)FF_TzBL zU(#1^1y;p4K?A&~G$`+{I6yK)d|LDJ>4^59n0(~7<=0ahA~B-;b2cRmc&V>uHH~aa z^Yp8my`N)yu)Jasvy$2tdk^G0p9&uqBYo=I@S7t zPYV?YDmLY0qR8D7_z3K4{tk~=e9}#%xkpl-mfJ$vM&dfb?tT|bt;S9sVn@`Og)>Wr z-z$o%%8PvR==0*^by&PyTVuHxW3mMDZP-gnsf%nQLLwB5l2~eK5^_bnvtkY_AI?3U zBqn@?4?@-k7@z3@73~}NHd_ZIyV&e1<$N5Y(@N&!*pcsmlIv*oZGq8)%c-Qa>4>fO z><)5EBgB5vPy9&_w=IlsH?8QOEo7DCW*T`;GP4g~UTSZ;`-jzhC)X-$EAwpqi2Evj z8O0&yVHJ^7ORlCuNBi_QUvXae%%fkt8_~S*hTRq{`b>Pw#NjPCslt7`BkU|A*CV9| z9&5;b1~(lLl%3i$s_(w)W$GxU5Ivxzlp*J8CP;kuNpsUmt~6U=ayzMiW4RwPWQAE| zM*|bVAJ zs`$|1%FeO-gG}V!Vsvc3pDW7leY->3S?l-GKGm)LjpR@oU5z@>1G95J&7r7%+p!Ri zFpifAeKG~D?({V!E>wH;lWvSqLJZ}fDgvOgHvuf4rc?i6bwxr_X7*6RXZa3(^~l?1 z1MMNFz15{AZ9f!)pDv=8s6UT^d1`Nh;tXiQA@Z1+>(?x4ca zLU2C@luE~~skN)7FHcc{V@Do*CZA%!q3wITAU40wx-!NSQ!5%o}1Q8S4v^;d{k>?RmZ6=(tl}umQuB5yfdDky_9?l9cS6t$G@Wm zPx$1}7Y9OjrqO#}=nmopsZO@W(KY9LA^ToWM0*@ZmIzI!)V1GNQbm-}F6+}ai^cU; z@CXwrs8gr+P8I*d`rZwg?%$mQCybCV8#rdILCaN5L~wMbehRX4u5)XT<{#D*zJFM0 z#;2SB=CpJVI6xj%o@hDQ-9Ec&S@}-{axoT|H}+RXY1E56cI_>~6=K<( zsX2@+p^tfurQ(MAXp_03jmZgA8#DL<^L)jA@aPlRCC>IIpl_cqkAALu+ZnD3UaW|{Bis%T287xV>O&Ai{}mfv#Jm@2y?4a*vominL)wS zv|H_CCTF>)-=q1N4Oybog|%cY#7tg%x^nwM^|a_abTI(P!ey{Djf=Q)mL}Zwl_F3eX$<% z0OiHhLls7{I1U6@D)=)yotqp*6~BkFIb|3QeG7lNNN2M^bLVx`zx6);O?=)#dxj8X zIObjbg9cOn7^uBqQgq_XmV1Bx6wzr`)HO9?IA_ z-bq*VFJA=o_;)xSL%6Ze^GV{4V*Uhiuy00DDi-zCcRk>eDCFtqIVd@H5=>c4``NL+ zNX4-@;|obB8L_;e5~a3HZy78#@<~q*|Cy5Py&ab&XR4bPNvd#diYN9oUT5k|(&-CK z=osJH;gDi-Vb0HE89`%As08!a#vvk5pZGMU`ZjUO&8=FNLWI4vbX|Tu`mKhH<|izi z<*v@tjL{@S>%o?%)%7tH^h~y@z-Iu`0(LG?v+$(*kO=Zi5o;S?FSXm7`l`XL6fce2 z!a0-oZ6Eq)e7T3%`w6Iv53o$)oGAq^z+dn<@w!SdksDmv!LLyEW#}f!wL?M&!MY{Z4HX)DLS24^Gx%5(*~B-ubgEH}+BqRp;5!!f0nq?oTsW_}pMJ?%_8Q(zY{ z82!#+@uJn@`R3!(CAbXYQsmo4(3_WY1FL*AF=b`1;<87YghGxZ=`{L_``MG-Ra*Fm z7=dZ$_od5@l@$=DuN>@6J>TrKTec3bbK}M>EHF8M)?=tM_k%>t{HYR!nF1MKzoj12vPPLH)E!|WZ4zMj|PI4 zWHzcVdZO?~ zG!_&VBO6H)StMu4r~b)o=2eoDt7d`ggm@iCG_DEuxaRQo)=>7FJNzF9pP|5RgshRW z3lu@eewJV5BH*>?sehO*M%Qilu_oNLc`r}lV!mvES%YlGfTL^t6GO5U7!Mx1$r-{8R@fu01xlp7TbUOj?us5Z6pGqQeqU9IV5=)NnfLW-ke(G>#%^+iGcq5}&Ju9xEM9Q1auZo@$3V5>adGUF< zwa)Id_115xxeVh?$!7?eXeOVCKd~cQ?7%6ga2HK2nSmxC547> ze%YDngbY@p!Yn)2z-a+{NJ8`gSopfPugh_Hv+VGw7=d7>PdZikggpF6P+OLAT+r9o z1IM$ z7^ptzJ65r)lj`g<>ExT_)V(73%c=j}3AnM8U(QPye8OlzR;%02q#`Jz@Yk&9pNz{q$*A)Y7KR>>abLPUCpm z3*wZcm!?eaQKpj-RF){CXW!ci&SK9^dciNor=|9Y1>_rg)A#8xb|>LD%J-IbRJyGu`23d+g`5z3?%R2ApPly!u>ICwqw)99_5vV!`PHqY?PVZ4&w z>6jdAzgw~q;$7J#-C9ESQ+@ZQudeKIluczy=B9i2vay&48)t&0K-SDcljW;s<)Co((QcM< znHR8bf_%nDKWw*$10wGn_!ikxt?Kw^%;&@V(6myGH>SQBbQudzK92r*Fp@aBQF6)> z=-A7FBB3}Oeoo5i`^CWlUR#$&SkuBk~yryQjCw6>N*-jJhG2ZGpek#CpOO-;tZ;i zlUpKm_iUM`9!=9!18MTU3;JG;PecEJkScweQ&$>w4*ozZz2P!8;W)Ar2^L#Nw9x)C zEi+GN1GCfEtTO%0UzBi;KiDX(E5nk|1u}pJ#)1*Mhao>HMIn1-(BJGcNi_JtGcqk3 z%>iv?7jGf(-V{aw@D6yAr3rP)lT1^x`kJN>pi3HoP?Z zDTm!TJNg}W+E*CNGM#SwHPA?6d^+t;9OANy7F*4n&1gPMH6+GYR1d=NbRO!9iI~Sd zVTtG;F8+`kOr$Nx_hZmnszj2ZRi*rz$o;DKW*crmg!YZ%*TClqk8F#mm41(4+h z=ad8=YK})pPQ15>XMZq6RuEPpq>1rI(seF&(gxu8ZRK^Ve}+Nq4rv3}T+&rPbJMRP zE;H)LK6J_1xNTS@Fw`WJ?3H|GUNUQ;`8Z<%s3Bg8ftTMVi)>#oXkSo=qI@PVFl5-x zH!6KA$$;(N6O`SiC!Xc%d?wuj)rai~@{yA69}9@0Rx>L*;vVWh<$Sn0jj4p3WpR2e zb&7A9fdz3n5Z=6Z1Vw8so&dIAWJ@Oj*w+A#cRAX9Ujrm$LN|uSV}Ksv8x&E03;Bmd znc)CE!I{rGa6)C=WF4SzE{Zo}&X&lhHHjTC@FDl2(VP-+IxPjFs`T#$yJ^@PIc%@e z==b~I%QfMu>|7IX-kkD$$JsJQOwe_i2@I>mG5CL-EceDJcT!(Nda}0)&&W*d+uX@n zKkn`Qq;x((XEL;I!ho#!@trS%2_O37rylK;KTm`+JWCX)uf}^HO!5hY+lp{RTcZ5O z=yqE7I5<60jIZ$|1NIc<+b;T|%OdRk-IXOI05$BM?Xp)sE@iynr$KpU*P;dnx22z5 z%bH())879Sdk#%Y>Ghw#pj=fMREhQ_=t!aK0N!8QnhXv-RL4S=K|Adf>VpHFrW{jN zJaFR0Y_=GaMT$HvOb9YVJ5zVgE0?o}*&6t>MZv_L_!>7tb4Lcx82RsvK6Jeu&<_7Q zwr=*K?6l6T-ObHeP?OF+QSf=g%TH?y@8a@9R^JJTFlEvkJM><)S?j!JFzpBgdUDH5 z+wlMLEvW(?&m39tU4T+Ec3W!9AF!qMzD(YMD|}myGhr!Jc=aW1G1)_F)O2WaD<&$~ z|DEdWM(%*xtZLX(OMx&2L1K@c^FOG&iRaTv_utN}>dku0HZEo@8~p`n(nB6q{2h_B zTqqe*|3eylB@pwC2WO89Tca-DnK{qf1C@*Xfzkbkh37M0l?VN~a^8eu)Rv0cho|3j z^Vo3;FKh^1V=7AL=Q+~CKQR>LEI)7JXi1x^eiSEr#G2asf|FP}O_wTeI%zthwz>w_ zSqlG$)b)*gOP#rUoaT7a_>`C!u@tTWu^cYRNmanXlIwrxnD`$ih5x%#+y7>m`~ScE z-_5o_V$UJNjRcnIiUVwNlS$0NyrJW%3N&37;L%ciD8`CK)2# z0a497!c-yI7QGInI`S%boqnTzwO_7$#EM<;anM&$9@oBi9NgIZ@)-%B+nvp(HTF_G z__Uy_`+Ol>=iCj}>^{IiW)aI`1r?W#zCc~V$Z$_o0&Ty0=Ggdc zBR>CBq%Xy05BXD2(gj1GBqDa8W!C9zOCHCkg_*5%z({<mXYY**Ppcip2>DRsN z>tzb1&d=9j5?O76=3j&I=M&d}~ZSO@5EUz{&&%(_Uvu-tw#LqQA~#wh8JsH;1)oOv?$jira< z=7(V4DJwleA9Kw&KX`43A3R1Q?qgxYh0IobNdrm(yl+^8D;xuITrS(&gRc$8McRROD>xJ?U#91$ltZ~hk8IttQooQvS!AIz%_ zI{X~vq1t~LhWy^6Q}_qx77+*p$N>R4ZCmrr9o@haou*ZusqGoQRGNbkpBmq^>oHwq zbG5*Y<~w`^#EJ6!R|R~+o0yWc8=#1xvB zO-Ma_SNR9vc!&;AEc$yV`5^aSO0?}nGyUu=XFpQ=qsRfWf%ZOeSFlaF3bvy@lMfk& zZ<2fZTzgpeX|o{h!Df9A9=6bd-!@WC0%}PXU}%bM@=bmsr)1hy4ssH3@xZ%H;wQ$R z_)7coa)13d`PI1`B=&4$@hWOx;}eexmcL|5T1b3v=hnfJXukRp6DT5D7-R|*mVv$vMYbS*5quX<+ zsxRD5YSHKA6s`M(w2?=4fhL7m3KhJx!cj0Ki59NB_WA z{zT8Kw!!UG8kBIvY!R<=1_YBXtenwrRoUz$q;RDb`GeW+6OmrLbnp0GBJCrnBrEK5t#Um1w)@$Y8qRc_=4@lhhM*>3=EV&>>Sn0*jd zeU@+?7rPS^@6QJ|#OkitXTwfTn0R~7knsu*mxsz{}r`SZ8Eu~tqoo%#WZlTXF*2(RUz{eRkx}BbvgkMx!dLSZn)y1L~K|y z5KxK99}k^KcR80NgzlbOL^w_D^Nqd*tr7m-xV@JwE`8lVVz%M6;Q?{JF+Q!mqdcY^ z!M(AMTJ?BocOm&wheqY8#^v|FM_j?+hmY0LEsfg~6QA_21q7i1EIis*g^TxLfGDd1 za|8!+^GC)h?41(^R^yk#`K7*ARx>RpPCNJs@nx~Xl+6$Aub(HLapV5O#E3q;vVzQ3{v50TX)b`?u3^`5NKdz~8KK zM$oPo7_9T@ba&ZAm(bQ_`uh#jq2g%G5_@=FQ3li8WqFQQzNurv`<@@?M8ML^zpwV5 zB|?(<$`+*36Uz+5L%v!2-l#$Vg404r#yKIHRoYtl*E6#-*pry2JR*SIf8Tv<1J?zK zlBT1eEVcx+OpWLcYJ#xyfrAEsf|JU`=aWteVCLszJf%$%Sq0m_^W_6LpiE^6sFVw) zZ&AmVD3H@YFR^nL^Aw!DapcgGb7nvdnwsoBgq#^_>gAvBHO9C1u(IQQ#ehmmA*U|= z>p&pIH4UpqMJZW(M64P#cIN_fcfaA+UDx$H1t+}Quce`_!1+LE3O<`JTP^W57`M%M z10~#v*6>|xv8+hXP*-d#%2R+$W&r8Lova81V&rWx{tNncqjgzkNnhy97Dq{>g4les zT@CIT5_Sh%qg6+2)0av-$GtXmD*{$z%%r zIRxUDld2be-F=fqjfD@TjTsGYXA9uHQI<@_4P`sa>|`d^)hGG_@vx&5+yc)g#&F)t z>y6IBo}u@;{;ifP{-He|H^?aUU&t2(d@9|LRfnIJF4B?T5Y;g z?u=|_@@BY5wKyh0&l!G4_bTA6N^Rk>o}NP-lOQ3_B&ik`qHl8}fB3@-z3yhHkUesm7lZ}B))ojkRI8JM> zcd-p(q3Dnm;7#fws=s67Ek}$#gML!KUkiX=#5B0DPl4g*SIn-*y0R+I`UjKy=#=1~ zwjDGSaZgm^Y2~PyMiEolHa6S=F|6!IW4N8X#*hDDHI_oj+N^O1;@SIRk1m-DwJvOn zT)||!KBABpn77CD%}c(Xf)T@-V>284#M)-%46c<=$HiJ+|Cj3eA6A$UcnEH?Ue*Hf zkG={IN0>X>eAh)X;mZh&c1_M1;F)@3zTbP(qq2u-J9)ZY9e!hYv7D#!3Ulvd1m(Kx z)%z19T(8$aBDyrX@PoeJSua!Z(V^E@m)JV4 zhv@~MoL0+l)Z+TMvSVGvrSaqQ46J;w;=?bHA{2g0c#M~uH9HD%k7(ZnQ>tHeA`uIc zwZ;XU9N_Pkgk_a>{v6?78VjD!}CFA68WM>7{@y@+bT( zCT~UKwYkfDltH~)^(>+4^dx_Ea_Y+cJhe|g!-{-Mrov&LJsm3Ka49={2@&AJ&Fbfw z^%K)%pK>5ECpISc3r57zSLw^D&=I-YB5lUlj9N(`b`B*IkX+dyIxJ|D_!S% z^Jn2gzoKvRj>q;SpuNin!-o@T=SVk6mRGx7?sIN6`K41G8!`K&G9_df83Svuy0 zuLez|@RbnrJX)pX)2*8kQOet66rw&FPkpkyE|^vEfmAgM39=Jtts{K*jj%dt?|dxF zsu0tYvGSuO-(}JkslSrSgi7zHMwi-iItd0a~kU3(n~I5#*AkuX9nWmf6 zY!8JB#+GDp_~hcF2jqVEF-c;N(MIOpm5(Zkq}q|-xcpbo{N)hm2l}9GJJ^iGPpZ+M z1K`75f|hRry0-jAiUhE&5?2Dz@HNNh)wclwSzU&?t#;QV(y5!IWBq^1UTuTpMVgIw zH%_5*{vtQ=m^EL23%E0Gnhk#-bD9j@M;rC+j+~8)-H5LHicx-vKl?DB*v1=Ptu2=F z9xdD6kNeZevJf!4dbF1Q9E6qxFdEd4wfusg)w{DhQ$?kgAmgh<*V}}+SE|}T6YZRx zV2i@zc~Shke|$+mC(Wm-{O%Fqn{v`H^w^pY3nIbT!Xk zMB)gr=)A6Jp8<$gOed_hl!93ss9RJ_O1ZyJYZtP27dKhu7Grm~B0!9>@FT_j!vHmc zXs8Prxz>Yg?U-v52eV7?stPXOe!7C%EA2Er^+JsR1yg(cTTDN+oOJmEbE9@#S_z4i zB|0!rWq#tDdyXd{f~-}JM0Yh-J{u~{uEPx3Tf9aVXMVB&R%&bhq!5P;`lK!nsA+tz z6Auj*K41T>{=Hvk5tdhMAz{s|@#~Sq0+<&w4fQ5GBx226@gr3Xl?j^}>(AX z%luu&pq?$K>jI+o0Y?66AUOSHX}^6-%im#EE$s%tuuwxyzM|8nq%Dd7wnq}I@_AZT z5J6sZZOVzdp^);Vu;Z#aq*rlwiN*+rGou{PX038K;C$>q^0-O7A91(XydlRlE5BVGniocpT9*rS-9 zd%Zbs?gHE)mH>&(m<_>!Gkxt5X%4bJSb_fc4uN4gEVtMi7YzKZRX^PhrOAyDYVk!c z@#?E{GlwHR50^Wyp8|HVx$kkJ!36%Mi=><9PRGW^@0um&qhsXoYz%AdH3B}X4P34g zTF3z@6<$^PpSOmZHB<|v9Gi=;`=n4)1X({9HqMGw_m=L&63!+Gm0HD9EH?JdiR5JB zF{j!#l=m%++vwBh)vEWR|FA%ose}0k0p4KLAeZhfwRM$t5CuRRKaCs0ZQ$M(#2Vs> zEn!w5pa!bT15EW!n4tTs0a}Z%?eB!|a6dEt!*V1;c0CVW>riv z=L|G~@@U8HQMDt7orJPp^WBm%=SzW8bI#oON853=^&d&uS#{7cs#NXNF-fLgl-EIr zT=_hMi&1oDgb{IqSSt&)h*+dx=iJ4y;dM}QGT0JPLKW78tMyfbIa|y}J5>J0D_LBZ z5K4r4yIYP9TP7PB_s_DZj35zOv{p1+`biz}*@Q$#$7r$oT2WbT5HhN`@S76r)vf-y z4#3G1`7_>HhMhk6j*52<&;Y?pR*C1}8g(~g8n286l~5Y2T?KaiFqSl+5oiP*O13*L zl%{utRNkv-H&joXcQT~(ss=)Hw^XW&VKef{3BO-6FA0~NMOZI6$^XN8v}br=>+U2J zL3U>*H8ye%VOJHpUYOUOWNB83&D2h0QY|6lH8^_jQL~yU|J&<I@2~cc_tH;D|Mf z3T9GnQ9JPTy+77%68NnAgtd;vce?%-d)B&Ki@JPma1*VG;TvmBniFh|m4l4P70-`% zAZ6O>n9E=u6xqDG(s=Zj@uhpa#=nV{#^2?uerJ9Sar82Md;9hAHGlsM&&_3Cik0c} zS2!8*Zy$7RX5jl>bHKHt8 zfrPFTqLwTP+tBH)O?*wcABTPzsPpEVyA08|#kAod%F3b5pY^qdk{sBbF(Q9z->0i$ zVFQt+HWAB70vejm>L21N?Fuw`L|sb?<`wLCD$V(7SX^8V@j89o?b;0gNdC20K^nas>`s+pcGERjxG?Uv3IIXxL6^>6Xf>iMMqP8T?)pN|E1 zJ}O2?7J2d&Rgz{pEKZkG@4Kp8&~XRu)_qgoWJme6klxy*bJ50+97eFQV+#zhpg@iN`6Sw4$P|>mVN13k^1u(C$-Xo7&5o5z z#}5!g-?I4Gz~#Y@Kl7_gXjDDxPm7#+{kF53sC>Ws;GXx$j$2uhUtDzid27k-J*00E zdJR4wve?`^8RqA}qE6)Ug>DE6?6Sc1rmo6j9sKgEY_ob0Eey>rp+ za(aw;BAZJcu7o)Nn?2>s~q98yWpQh{tipmg|*jk8FuBe zv23or@0dToi5RDZaX=NtSuoJrfmD?2(XYj9A*&(n8BOgO^H$F{y6U>doGKEv%Bni& zBNDZ56u2hEQ{v@JM>p$8|Hi-Z*u(jCqW}Y0wAJIwjW|(N64I69_aQE zKsRST68@mCfJs5;=SQ9|l*QfRR6_^jwlI{6fiI?;IMqq#5=^i#c|cwLIkZnsHFR;H zSk>S=n<`}HzshaU8F;6&J@M&HtM5b3TXpBlsjQB6O4oszSe13qsw6q+TZ1ZLL+82| zZd7xGA=f6xG$Dhie`kSt^D$)kkONR8@&w;Pqkw&xKTxN-pJ8@+V$p2_fG%*hz+k|b z4(Kf}N&FVA0A9#fm0aN;R=}Y!f#`N&=iIq}`Ve;G!`f26M^UXASRb%-Fi@eJAVmxt zpO&Ci(Yk(v!tW+gl+Wx?uBbpHq#MZoT*~KjXS!c9rjwTu-0PT*_7KFd8m z<+Z`BA++VHHLz8HwJ`7(BcV9CkSPn7y$D35xUKE3M|TLWrR9-fBj>d_vkJo-FW%Yq za-*B?o20br@iN;Pe~1j#R+_#`@H!^kqI0KvCL3OA9mRKkvc>ybh(Z72sITseIMi)s zeM@jinSy__e6BA3_bE1GK(?{P@}O(hPQO+PaeV;u;%nOyM)k%twpgu2iZ1~Y{ftT8t!d;FN^7vvu{zPO#lKuK}x(Q>jvoCZ>qZi*C z!<0?kl%V`EP%xg=kiyZapLWidplI1$v5K6On??CHnFi>*tNJ_Xx96JqDHwNN%0ACE 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 z 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 - +

    # HailoRT # HailoRT is a lightweight, production-grade runtime library that runs on the host processor and provides a robust -user-space runtime library (the HailoRT Library) with intuitive APIs in C/C++ for optimized performance +user-space library (the HailoRT Library) with intuitive APIs in C/C++ for optimized performance HailoRT consists of the following main components: - HailoRT Library. @@ -42,8 +42,8 @@ Contact information and support is available at [**hailo.ai**](https://hailo.ai/ ## About Hailo-8™ -Hailo-8 is a deep learning processor for edge devices. The Hailo-8 provides groundbraking efficiency for neural network deployment. -The Hailo-8 edge AI processor, featuring up to 26 tera-operations per second (TOPS), significantly outperforms all other edge processors. +Hailo-8 is a deep learning processor for edge devices. The Hailo-8 provides groundbreaking efficiency for neural network deployment. +The Hailo-8 edge AI processor, featuring up to 26 Tera-Operations-Per-Second (TOPS), significantly outperforms all other edge processors. Hailo-8 is available in various form-factors, including the Hailo-8 M.2 Module. The Hailo-8 AI processor is designed to fit into a multitude of smart machines and devices, for a wide variety of sectors including Automotive, Smart Cities, Industry 4.0, diff --git a/common/include/context_switch_defs.h b/common/include/context_switch_defs.h index b7a0fe8..cf4858f 100644 --- a/common/include/context_switch_defs.h +++ b/common/include/context_switch_defs.h @@ -54,6 +54,9 @@ extern "C" { (vdma_channel_index) = ((src) & CONTEXT_SWITCH_DEFS__PACKED_VDMA_CHANNEL_ID__VDMA_CHANNEL_INDEX_MASK); \ } while (0) +#define CONTEXT_SWITCH_DEFS__WRITE_ACTION_BY_TYPE_MAX_SIZE (4) + + #pragma pack(push, 1) typedef struct { uint16_t core_bytes_per_buffer; @@ -104,6 +107,8 @@ typedef enum __attribute__((packed)) { CONTEXT_SWITCH_DEFS__ACTION_TYPE_OPEN_BOUNDARY_INPUT_CHANNEL, CONTEXT_SWITCH_DEFS__ACTION_TYPE_OPEN_BOUNDARY_OUTPUT_CHANNEL, CONTEXT_SWITCH_DEFS__ACTION_TYPE_ENABLE_NMS, + CONTEXT_SWITCH_DEFS__ACTION_TYPE_WRITE_DATA_BY_TYPE, + CONTEXT_SWITCH_DEFS__ACTION_TYPE_SWITCH_LCU_BATCH, /* Must be last */ CONTEXT_SWITCH_DEFS__ACTION_TYPE_COUNT @@ -358,8 +363,33 @@ typedef struct { typedef struct { uint8_t nms_unit_index; uint8_t network_index; + uint16_t number_of_classes; + uint16_t burst_size; } CONTEXT_SWITCH_DEFS__enable_nms_action_t; +typedef enum { + WRITE_ACTION_TYPE_GENERAL = 0, + WRITE_ACTION_TYPE_WRITE_BATCH = 1, + + /* Must be last */ + WRITE_ACTION_BY_TYPE_COUNT +} CONTEXT_SWITCH_DEFS__WRITE_ACTION_TYPE_t; + +typedef struct { + uint32_t address; + uint8_t data_type; //CONTEXT_SWITCH_DEFS__WRITE_ACTION_TYPE_t + uint32_t data; + uint8_t shift; + uint32_t mask; + uint8_t network_index; +} CONTEXT_SWITCH_DEFS__write_data_by_type_action_t; + +typedef struct { + uint8_t packed_lcu_id; + uint8_t network_index; + uint32_t kernel_done_count; +} CONTEXT_SWITCH_DEFS__switch_lcu_batch_action_data_t; + #pragma pack(pop) #ifdef __cplusplus diff --git a/common/include/control_protocol.h b/common/include/control_protocol.h index c5889ec..73d31d5 100644 --- a/common/include/control_protocol.h +++ b/common/include/control_protocol.h @@ -1017,6 +1017,7 @@ typedef enum { CONTROL_PROTOCOL__CONTEXT_SWITCH_STATUS_COUNT, } CONTROL_PROTOCOL__CONTEXT_SWITCH_STATUS_t; +#define CONTROL_PROTOCOL__INIFINITE_BATCH_COUNT (0) typedef struct { uint32_t state_machine_status_length; uint8_t state_machine_status; @@ -1024,6 +1025,8 @@ typedef struct { uint8_t application_index; uint32_t dynamic_batch_size_length; uint16_t dynamic_batch_size; + uint32_t batch_count_length; + uint16_t batch_count; uint32_t keep_nn_config_during_reset_length; uint8_t keep_nn_config_during_reset; } CONTROL_PROTOCOL__change_context_switch_status_request_t; @@ -1315,6 +1318,8 @@ typedef struct { uint8_t application_index; uint32_t dynamic_batch_size_length; uint16_t dynamic_batch_size; + uint32_t batch_count_length; + uint16_t batch_count; uint32_t channels_info_length; CONTROL_PROTOCOL__hw_infer_channels_info_t channels_info; } CONTROL_PROTOCOL__change_hw_infer_status_request_t; diff --git a/common/include/d2h_events.h b/common/include/d2h_events.h index 6eff396..b9009ef 100644 --- a/common/include/d2h_events.h +++ b/common/include/d2h_events.h @@ -57,6 +57,8 @@ typedef enum { HEALTH_MONITOR_CPU_ECC_FATAL_EVENT_ID, CONTEXT_SWITCH_BREAKPOINT_REACHED, HEALTH_MONITOR_CLOCK_CHANGED_EVENT_ID, + HW_INFER_MANAGER_INFER_DONE, + D2H_EVENT_ID_COUNT /* Must be last*/ } D2H_EVENT_ID_t; @@ -138,6 +140,12 @@ typedef struct { #define D2H_EVENT_HEALTH_MONITOR_CLOCK_CHANGED_EVENT_PARAMETER_COUNT (2) +typedef struct { + uint32_t infer_cycles; +} D2H_EVENT_hw_infer_mamager_infer_done_message_t; + +#define D2H_EVENT_HW_INFER_MANAGER_INFER_DONE_PARAMETER_COUNT (1) + /* D2H_EVENT__message_parameters_t should be in the same order as hailo_notification_message_parameters_t */ typedef union { D2H_EVENT_rx_error_event_message_t rx_error_event; @@ -149,6 +157,7 @@ typedef union { D2H_EVENT_health_monitor_cpu_ecc_event_message_t health_monitor_cpu_ecc_event; D2H_EVENT_context_switch_breakpoint_reached_event_massage_t context_switch_breakpoint_reached_event; D2H_EVENT_health_monitor_clock_changed_event_message_t health_monitor_clock_changed_event; + D2H_EVENT_hw_infer_mamager_infer_done_message_t hw_infer_manager_infer_done_event; } D2H_EVENT__message_parameters_t; typedef struct { diff --git a/common/include/firmware_status.h b/common/include/firmware_status.h index 193bfef..f45d9c1 100644 --- a/common/include/firmware_status.h +++ b/common/include/firmware_status.h @@ -411,6 +411,7 @@ Updating rules: FIRMWARE_STATUS__X(CONTROL_PROTOCOL_STATUS_INVALID_SLEEP_STATE)\ FIRMWARE_STATUS__X(CONTROL_PROTOCOL_STATUS_INVALID_HW_INFER_STATE_LENGTH)\ FIRMWARE_STATUS__X(CONTROL_PROTOCOL_STATUS_INVALID_CHANNELS_INFO_LENGTH)\ + FIRMWARE_STATUS__X(CONTROL_PROTOCOL_STATUS_INVALID_BATCH_COUNT_LENGTH)\ \ FIRMWARE_MODULE__X(FIRMWARE_MODULE__POWER_MEASUREMENT)\ FIRMWARE_STATUS__X(HAILO_POWER_MEASUREMENT_STATUS_POWER_INIT_ERROR)\ @@ -554,6 +555,7 @@ Updating rules: FIRMWARE_STATUS__X(PCIE_SERVICE_STATUS_INVALID_H2D_CHANNEL_INDEX)\ FIRMWARE_STATUS__X(PCIE_SERVICE_STATUS_INVALID_D2H_CHANNEL_INDEX)\ FIRMWARE_STATUS__X(PCIE_SERVICE_INVALID_INITIAL_CREDIT_SIZE)\ + FIRMWARE_STATUS__X(PCIE_SERVICE_ERROR_ADDING_CREDITS_TO_PCIE_CHANNEL)\ \ FIRMWARE_MODULE__X(FIRMWARE_MODULE__FIRMWARE_UPDATE)\ FIRMWARE_STATUS__X(FIRMWARE_UPDATE_STATUS_INVALID_PARAMETERS)\ @@ -753,6 +755,9 @@ Updating rules: FIRMWARE_STATUS__X(CONTEXT_SWITCH_STATUS_INVALID_DYNAMIC_CONTEXT_COUNT)\ FIRMWARE_STATUS__X(CONTEXT_SWITCH_STATUS_CONTEXT_INDEX_OUT_OF_RANGE)\ FIRMWARE_STATUS__X(CONTEXT_SWITCH_STATUS_TOTAL_PROVIDED_EDGE_LAYERS_LARGER_THEN_EXPECTED)\ + FIRMWARE_STATUS__X(CONTEXT_SWITCH_STATUS_REACHED_TIMEOUT_WHILE_WAITING_FOR_NETWORK_IDLE)\ + FIRMWARE_STATUS__X(CONTEXT_SWITCH_STATUS_WRITE_DATA_BY_TYPE_ACTION_INVALID_TYPE)\ + FIRMWARE_STATUS__X(CONTEXT_SWITCH_STATUS_WRITE_DATA_BY_TYPE_ACTION_INVALID_MEMORY_SPACE)\ \ FIRMWARE_MODULE__X(FIRMWARE_MODULE__D2H_EVENT_MANAGER)\ FIRMWARE_STATUS__X(HAILO_D2H_EVENT_MANAGER_STATUS_MESSAGE_HIGH_PRIORITY_QUEUE_CREATE_FAILED)\ @@ -1010,6 +1015,7 @@ Updating rules: FIRMWARE_STATUS__X(VDMA_SERVICE_STATUS_INVALID_CONSTANTS)\ FIRMWARE_STATUS__X(VDMA_SERVICE_STATUS_INVALID_CHANNEL_INDEX)\ FIRMWARE_STATUS__X(VDMA_SERVICE_STATUS_INVALID_EDGE_LAYER_DIRECTION)\ + FIRMWARE_STATUS__X(VDMA_SERVICE_INSUFFICIENT_DESCRIPTORS_COUNT)\ \ FIRMWARE_MODULE__X(FIRMWARE_MODULE__MEMORY_LOGGER)\ FIRMWARE_STATUS__X(MEMORY_LOGGER_STATUS_DEBUG_INSUFFICIENT_MEMORY)\ @@ -1079,6 +1085,9 @@ Updating rules: FIRMWARE_STATUS__X(NMS_MANAGER_STATUS_INVALID_NETWORK_INDEX)\ FIRMWARE_STATUS__X(NMS_MANAGER_STATUS_INVALID_NMS_UNIT_INDEX)\ FIRMWARE_STATUS__X(NMS_MANAGER_STATUS_INVALID_BATCH_SIZE)\ + FIRMWARE_STATUS__X(NMS_MANAGER_STATUS_INVALID_NUM_CLASSES_SIZE)\ + FIRMWARE_STATUS__X(NMS_MANAGER_STATUS_INVALID_BURST_SIZE)\ + FIRMWARE_STATUS__X(NMS_MANAGER_STATUS_INVALID_LAST_FRAME_IN_BATCH_SIZE)\ \ FIRMWARE_MODULE__X(FIRMWARE_MODULE__CLUSTER_MANAGER)\ FIRMWARE_STATUS__X(CLUSTER_MANAGER_STATUS_INVALID_CLUSTER_INDEX)\ @@ -1087,6 +1096,7 @@ Updating rules: FIRMWARE_STATUS__X(CLUSTER_MANAGER_STATUS_INVALID_LCU_INDEX)\ FIRMWARE_STATUS__X(CLUSTER_MANAGER_STATUS_INVALID_KERNEL_DONE_ADDRESS)\ FIRMWARE_STATUS__X(CLUSTER_MANAGER_STATUS_RECEIVED_UNEXPECTED_INTERRUPT)\ + FIRMWARE_STATUS__X(CLUSTER_MANAGER_STATUS_INVALID_NETWORK_INDEX)\ \ FIRMWARE_MODULE__X(FIRMWARE_MODULE__HW_INFER_MANAGER)\ FIRMWARE_STATUS__X(HW_INFER_MANAGER_STATUS_NETWORK_GROUP_NOT_CONFIGURED_BEFORE_INFER_START)\ diff --git a/common/include/utils.h b/common/include/utils.h index 7e48489..860d1fa 100644 --- a/common/include/utils.h +++ b/common/include/utils.h @@ -10,6 +10,8 @@ #ifndef __UTILS_H__ #define __UTILS_H__ +#include + /** A compile time assertion check. * * Validate at compile time that the predicate is true without @@ -125,4 +127,20 @@ _PP_ISEMPTY( \ #define MICROSECONDS_IN_MILLISECOND (1000) +static inline uint8_t ceil_log2(uint32_t n) +{ + uint8_t result = 0; + + if (n <= 1) { + return 0; + } + + while (n > 1) { + result++; + n = (n + 1) >> 1; + } + + return result; +} + #endif /* __UTILS_H__ */ diff --git a/hailort/CMakeLists.txt b/hailort/CMakeLists.txt index 7d90f9d..5f790bf 100644 --- a/hailort/CMakeLists.txt +++ b/hailort/CMakeLists.txt @@ -9,6 +9,18 @@ option(HAILO_BUILD_EXAMPLES "Build examples" OFF) option(HAILO_OFFLINE_COMPILATION "Don't download external dependencies" OFF) option(HAILO_BUILD_SERVICE "Build hailort service" OFF) option(HAILO_BUILD_PROFILER "Build hailort profiler" ON) +option(HAILO_COMPILE_WARNING_AS_ERROR "Add compilation flag for treating compilation warnings as errors" OFF) +option(HAILO_SUPPORT_PACKAGING "Create HailoRT package (internal)" OFF) + +if (HAILO_COMPILE_WARNING_AS_ERROR) + if(WIN32) + set(HAILORT_COMPILE_OPTIONS ${HAILORT_COMPILE_OPTIONS} /WX) + elseif(UNIX) + set(HAILORT_COMPILE_OPTIONS ${HAILORT_COMPILE_OPTIONS} -Werror) + else() + message(FATAL_ERROR "Unexpeced host, stopping build") + endif() +endif() # Flag for emulator (FPGA/Veloce) if(HAILO_BUILD_EMULATOR) @@ -18,7 +30,7 @@ endif() # Set firmware version add_definitions( -DFIRMWARE_VERSION_MAJOR=4 ) -add_definitions( -DFIRMWARE_VERSION_MINOR=13 ) +add_definitions( -DFIRMWARE_VERSION_MINOR=14 ) add_definitions( -DFIRMWARE_VERSION_REVISION=0 ) if(HAILO_BUILD_SERVICE) add_definitions( -DHAILO_SUPPORT_MULTI_PROCESS ) @@ -78,20 +90,6 @@ set(COMMON_INC_DIR ${PROJECT_SOURCE_DIR}/common/include) set(DRIVER_INC_DIR ${PROJECT_SOURCE_DIR}/hailort/drivers/common) set(RPC_DIR ${PROJECT_SOURCE_DIR}/hailort/rpc) -if(HAILO_BUILD_PYBIND) - if(NOT PYTHON_EXECUTABLE AND PYBIND11_PYTHON_VERSION) - # PYBIND11_PYTHON_VERSION is prioritized (not virtual environment) if PYTHON_EXECUTABLE is not set. - # See https://pybind11.readthedocs.io/en/stable/changelog.html#v2-6-0-oct-21-2020 - if((${CMAKE_VERSION} VERSION_LESS "3.22.0") AND (NOT WIN32)) - find_package(PythonInterp ${PYBIND11_PYTHON_VERSION} REQUIRED) - set(PYTHON_EXECUTABLE ${Python_EXECUTABLE}) - else() - find_package(Python3 ${PYBIND11_PYTHON_VERSION} REQUIRED EXACT COMPONENTS Interpreter Development) - set(PYTHON_EXECUTABLE ${Python3_EXECUTABLE}) - endif() - endif() - add_subdirectory(external/pybind11 EXCLUDE_FROM_ALL) -endif() add_subdirectory(external/Catch2 EXCLUDE_FROM_ALL) add_subdirectory(external/CLI11 EXCLUDE_FROM_ALL) add_subdirectory(external/json EXCLUDE_FROM_ALL) @@ -128,6 +126,9 @@ endif() if(HAILO_WIN_DRIVER) add_subdirectory(drivers/win) +endif() + +if(HAILO_SUPPORT_PACKAGING) add_subdirectory(packaging) endif() diff --git a/hailort/common/CMakeLists.txt b/hailort/common/CMakeLists.txt index 1056647..b3bed6b 100644 --- a/hailort/common/CMakeLists.txt +++ b/hailort/common/CMakeLists.txt @@ -19,6 +19,7 @@ set(SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/barrier.cpp ${CMAKE_CURRENT_SOURCE_DIR}/file_utils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/string_utils.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/event_internal.cpp ${CMAKE_CURRENT_SOURCE_DIR}/device_measurements.cpp ) diff --git a/hailort/common/barrier.hpp b/hailort/common/barrier.hpp index 3062754..1fae129 100644 --- a/hailort/common/barrier.hpp +++ b/hailort/common/barrier.hpp @@ -16,6 +16,9 @@ namespace hailort { +class Barrier; +using BarrierPtr = std::shared_ptr; + /** * A barrier is a synchronization object that allows an expected number of threads to block until all of them * arrive at the barrier. diff --git a/hailort/common/device_measurements.cpp b/hailort/common/device_measurements.cpp index 89fd0d3..b498def 100644 --- a/hailort/common/device_measurements.cpp +++ b/hailort/common/device_measurements.cpp @@ -56,16 +56,22 @@ Expected> TemperatureMeasurement::create return ptr; } - TemperatureMeasurement::TemperatureMeasurement(Device &device, hailo_status &status) : BaseMeasurement(device, status) -{} +{ + /* Executing the check only if BaseMeasurement constructor has succeeded */ + if (HAILO_SUCCESS == status) { + status = sanity_check(); + } +} -hailo_status TemperatureMeasurement::start_measurement() +hailo_status TemperatureMeasurement::sanity_check() { - // Checking sensor before starting thread - auto temp_info = m_device.get_chip_temperature(); - CHECK_EXPECTED_AS_STATUS(temp_info); + auto temp_measurement = m_device.get_chip_temperature(); + return temp_measurement.status(); +} +hailo_status TemperatureMeasurement::start_measurement() +{ m_is_thread_running = true; m_thread = std::thread([this] () { while (m_is_thread_running.load()) { @@ -102,14 +108,21 @@ Expected> PowerMeasurement::create_shared(Devi PowerMeasurement::PowerMeasurement(Device &device, hailo_power_measurement_types_t measurement_type, hailo_status &status) : BaseMeasurement(device, status), m_measurement_type(measurement_type) -{} +{ + /* Executing the check only if BaseMeasurement constructor has succeeded */ + if (HAILO_SUCCESS == status) { + status = sanity_check(); + } +} -hailo_status PowerMeasurement::start_measurement() +hailo_status PowerMeasurement::sanity_check() { - // Checking sensor before starting thread - auto power_info = m_device.power_measurement(HAILO_DVM_OPTIONS_AUTO, m_measurement_type); - CHECK_EXPECTED_AS_STATUS(power_info); + auto power_measurement = m_device.power_measurement(HAILO_DVM_OPTIONS_AUTO, m_measurement_type); + return power_measurement.status(); +} +hailo_status PowerMeasurement::start_measurement() +{ m_is_thread_running = true; m_thread = std::thread([this] () { while (m_is_thread_running.load()) { diff --git a/hailort/common/device_measurements.hpp b/hailort/common/device_measurements.hpp index 6089be8..a3c266c 100644 --- a/hailort/common/device_measurements.hpp +++ b/hailort/common/device_measurements.hpp @@ -38,6 +38,9 @@ protected: std::atomic_bool m_is_thread_running; std::mutex m_mutex; hailort::AccumulatorPtr m_acc; + +private: + virtual hailo_status sanity_check() = 0; }; @@ -56,6 +59,9 @@ public: } TemperatureMeasurement(hailort::Device &device, hailo_status &status); + +private: + virtual hailo_status sanity_check() override; }; @@ -89,6 +95,7 @@ public: private: hailo_power_measurement_types_t m_measurement_type; + virtual hailo_status sanity_check() override; }; #endif /* _HAILO_DEVICE_MEASUREMENTS_HPP_ */ diff --git a/hailort/common/ethernet_utils.hpp b/hailort/common/ethernet_utils.hpp index 108b8a7..eadfaed 100644 --- a/hailort/common/ethernet_utils.hpp +++ b/hailort/common/ethernet_utils.hpp @@ -86,13 +86,12 @@ public: static const uint32_t MAX_INTERFACE_SIZE = IFNAMSIZ; #endif - static hailo_status get_interface_from_board_ip(const char *board_ip, char *interface_name, size_t interface_name_length); - static hailo_status get_ip_from_interface(const char *interface_name, char *ip, size_t ip_length); + static Expected get_interface_from_board_ip(const std::string &board_ip); + static Expected get_ip_from_interface(const std::string &interface_name); private: #if defined(__GNUG__) - static hailo_status get_interface_from_arp_entry(char *arp_entry, char *interface_name, - size_t max_interface_name_length); + static Expected get_interface_from_arp_entry(char *arp_entry); #endif }; diff --git a/hailort/common/event_internal.cpp b/hailort/common/event_internal.cpp new file mode 100644 index 0000000..e699379 --- /dev/null +++ b/hailort/common/event_internal.cpp @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file event_internal.cpp + * @brief Internal implementation for events, shared between all os. + **/ + +#include "common/event_internal.hpp" +#include "common/logger_macros.hpp" +#include "common/utils.hpp" + +namespace hailort +{ + +Waitable::Waitable(underlying_waitable_handle_t handle) : + m_handle(handle) +{} + +hailo_status Waitable::wait(std::chrono::milliseconds timeout) +{ + auto status = wait_for_single_object(m_handle, timeout); + if (HAILO_TIMEOUT == status) { + LOGGER__TRACE("wait_for_single_object failed with timeout (timeout={}ms)", timeout.count()); + return status; + } + CHECK_SUCCESS(status); + + status = post_wait(); + CHECK_SUCCESS(status); + + return HAILO_SUCCESS; +} + +underlying_waitable_handle_t Waitable::get_underlying_handle() +{ + return m_handle; +} + +WaitOrShutdown::WaitOrShutdown(WaitablePtr waitable, EventPtr shutdown_event) : + m_waitable(waitable), + m_shutdown_event(shutdown_event), + m_waitable_group(create_waitable_group(m_waitable, m_shutdown_event)) +{} + +hailo_status WaitOrShutdown::wait(std::chrono::milliseconds timeout) +{ + auto index = m_waitable_group.wait_any(timeout); + if (index.status() == HAILO_TIMEOUT) { + return index.status(); + } + CHECK_EXPECTED_AS_STATUS(index); + + assert(index.value() <= WAITABLE_INDEX); + return (index.value() == SHUTDOWN_INDEX) ? HAILO_SHUTDOWN_EVENT_SIGNALED : HAILO_SUCCESS; +} + +hailo_status WaitOrShutdown::signal() +{ + return m_waitable->signal(); +} + +WaitableGroup WaitOrShutdown::create_waitable_group(WaitablePtr waitable, EventPtr shutdown_event) +{ + // Note the order - consistent with SHUTDOWN_INDEX, WAITABLE_INDEX. + std::vector> waitables; + waitables.emplace_back(std::ref(*shutdown_event)); + waitables.emplace_back(std::ref(*waitable)); + return waitables; +} + +} /* namespace hailort */ diff --git a/hailort/libhailort/src/utils/event_internal.hpp b/hailort/common/event_internal.hpp similarity index 52% rename from hailort/libhailort/src/utils/event_internal.hpp rename to hailort/common/event_internal.hpp index 04559e8..295d4a8 100644 --- a/hailort/libhailort/src/utils/event_internal.hpp +++ b/hailort/common/event_internal.hpp @@ -10,8 +10,7 @@ #ifndef _EVENT_INTERNAL_HPP_ #define _EVENT_INTERNAL_HPP_ -#include "hailo/hailort.h" -#include "hailo/expected.hpp" +#include "hailo/event.hpp" #include #include @@ -24,9 +23,50 @@ namespace hailort { -// TODO: Replace with a static wait_multiple func belonging to Waitable (SDK-16567). -// Will get a vector of pointers as an argument. Can also use variadic -// template args for cases with fixed number Waitables +// Group of Waitable objects that can be waited for together +class WaitableGroup final +{ +public: + WaitableGroup(std::vector> &&waitables) : + m_waitables(std::move(waitables)), + m_waitable_handles(create_waitable_handle_vector(m_waitables)) + {} + + /** + * Waits until any of the given waitables are signaled. Returns the index in the waitables vector + * of the signaled waitable with the smallest index value. + */ + Expected wait_any(std::chrono::milliseconds timeout); + +private: + +#if defined(__linux__) + using WaitableHandle = pollfd; +#else + using WaitableHandle = underlying_waitable_handle_t; +#endif + + static std::vector create_waitable_handle_vector( + const std::vector> &waitables) + { + std::vector waitable_handles; + waitable_handles.reserve(waitables.size()); + for (auto &waitable : waitables) { +#if defined(__linux__) + waitable_handles.emplace_back(pollfd{waitable.get().get_underlying_handle(), POLLIN, 0}); +#else + waitable_handles.emplace_back(waitable.get().get_underlying_handle()); +#endif + } + return waitable_handles; + } + + // Initialization dependency + std::vector> m_waitables; + // Store this vector here to avoid runtime allocations. + std::vector m_waitable_handles; +}; + class WaitOrShutdown final { public: @@ -55,29 +95,19 @@ public: hailo_status signal(); private: + static WaitableGroup create_waitable_group(WaitablePtr waitable, EventPtr shutdown_event); + // Note: We want to guarantee that if the shutdown event is signaled, HAILO_SHUTDOWN_EVENT_SIGNALED will be // returned. - // * In Unix, using poll this isn't a problem since we'll get all the readable fds in a single call. - // * In Windows, using WaitForMultipleObjects, this works differently (from msdn): - // If bWaitAll is FALSE, the return value minus WAIT_OBJECT_0 indicates the lpHandles array index - // of the object that satisfied the wait. If more than one object became signaled during the call, - // this is the array index of the signaled object with the smallest index value of all the signaled - // objects. - // (https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitformultipleobjects) - // * Hence, SHUTDOWN_INDEX must come before WAITABLE_INDEX! + // Waitable::wait_any returns the smallest index value of all the signaled objects. + // Hence, SHUTDOWN_INDEX must come before WAITABLE_INDEX! static const size_t SHUTDOWN_INDEX = 0; static const size_t WAITABLE_INDEX = 1; - #if defined(_MSC_VER) || defined(__QNX__) - using WaitHandleArray = std::array; - #else - using WaitHandleArray = std::array; - #endif const WaitablePtr m_waitable; const EventPtr m_shutdown_event; - WaitHandleArray m_wait_handle_array; - static WaitHandleArray create_wait_handle_array(WaitablePtr waitable, EventPtr shutdown_event); + WaitableGroup m_waitable_group; }; } /* namespace hailort */ diff --git a/hailort/common/filesystem.hpp b/hailort/common/filesystem.hpp index 74d6c77..b650b46 100644 --- a/hailort/common/filesystem.hpp +++ b/hailort/common/filesystem.hpp @@ -35,6 +35,7 @@ public: static Expected get_file_modified_time(const std::string &file_path); static Expected is_directory(const std::string &path); static hailo_status create_directory(const std::string &dir_path); + static hailo_status remove_directory(const std::string &dir_path); static Expected get_current_dir(); static std::string get_home_directory(); static bool is_path_accesible(const std::string &path); diff --git a/hailort/common/latency_meter.hpp b/hailort/common/latency_meter.hpp index c4a141f..5178e1f 100644 --- a/hailort/common/latency_meter.hpp +++ b/hailort/common/latency_meter.hpp @@ -29,7 +29,7 @@ public: using duration = std::chrono::nanoseconds; using TimestampsArray = CircularArray; - explicit LatencyMeter(const std::set &output_names, size_t timestamps_list_length) : + LatencyMeter(const std::set &output_names, size_t timestamps_list_length) : m_start_timestamps(timestamps_list_length), m_latency_count(0), m_latency_sum(0) diff --git a/hailort/common/os/posix/ethernet_utils.cpp b/hailort/common/os/posix/ethernet_utils.cpp index 0908c4d..f675ec5 100644 --- a/hailort/common/os/posix/ethernet_utils.cpp +++ b/hailort/common/os/posix/ethernet_utils.cpp @@ -10,6 +10,9 @@ #include "common/utils.hpp" #include "common/logger_macros.hpp" #include "common/ethernet_utils.hpp" +#include "common/socket.hpp" + +#include namespace hailort { @@ -20,8 +23,7 @@ namespace hailort #define ETHERNET_UTILS__ARP_DEVICE_NAME_INDEX (4) -hailo_status EthernetUtils::get_interface_from_arp_entry(char *arp_entry, char *interface_name, - size_t max_interface_name_length) +Expected EthernetUtils::get_interface_from_arp_entry(char *arp_entry) { /* This function parses the interface name out from the arp entry * Each entry is built as follows: @@ -30,132 +32,62 @@ hailo_status EthernetUtils::get_interface_from_arp_entry(char *arp_entry, char * * For example: * 10.0.0.163 0x1 0x2 80:00:de:ad:be:3f * enp1s0 * */ - hailo_status status = HAILO_UNINITIALIZED; size_t token_counter = 0; char* token = NULL; /* Start splitting the arp entry into tokens according to the delimiter */ token = strtok(arp_entry, ETHERNET_UTILS__ARP_ENTRY_DELIMIETERS); - if (NULL == token) { - LOGGER__ERROR("Invalid arp entry, could not split it to tokens"); - status = HAILO_ETH_FAILURE; - goto l_exit; - } + CHECK_AS_EXPECTED(nullptr != token, HAILO_ETH_FAILURE, "Invalid arp entry, could not split it to tokens"); /* Iterate over the tokens until the device name is found */ while (NULL != token) { token = strtok(NULL, ETHERNET_UTILS__ARP_ENTRY_DELIMIETERS); if (ETHERNET_UTILS__ARP_DEVICE_NAME_INDEX == token_counter) { LOGGER__DEBUG("Interface name: {}", token); - strncpy(interface_name, token, max_interface_name_length); - break; + return std::string(token); } token_counter++; } - status = HAILO_SUCCESS; -l_exit: - return status; + return make_unexpected(HAILO_ETH_FAILURE); } -hailo_status EthernetUtils::get_interface_from_board_ip(const char *board_ip, char *interface_name, size_t interface_name_length) +Expected EthernetUtils::get_interface_from_board_ip(const std::string &board_ip) { - hailo_status status = HAILO_UNINITIALIZED; - FILE* arp_file = NULL; - int fclose_rc = -1; - char buffer[ETHERNET_UTILS__ARP_MAX_ENTRY_LENGTH] = {}; - - CHECK_ARG_NOT_NULL(interface_name); - CHECK_ARG_NOT_NULL(board_ip); + std::ifstream arp_file(ETHERNET_UTILS__ARP_FILE, std::ios::in); + CHECK_AS_EXPECTED(arp_file, HAILO_OPEN_FILE_FAILURE, "Cannot open file {}. errno: {:#x}", ETHERNET_UTILS__ARP_FILE, errno); - /* Open arp file */ - arp_file = fopen(ETHERNET_UTILS__ARP_FILE, "r"); - if (NULL == arp_file) { - LOGGER__ERROR("Cannot open file {}. Errno: {:#x}", ETHERNET_UTILS__ARP_FILE, errno); - status = HAILO_OPEN_FILE_FAILURE; - goto l_exit; - } + char buffer[ETHERNET_UTILS__ARP_MAX_ENTRY_LENGTH] = {}; /* Go over all of the lines at the file */ - while(fgets(buffer, ARRAY_LENGTH(buffer), arp_file)) { - /* Check if the arp line contains the board_ip */ - if (strstr(buffer, board_ip)) { - status = get_interface_from_arp_entry(buffer, interface_name, interface_name_length); - if (HAILO_SUCCESS != status) { - goto l_exit; - } - break; + while (arp_file.getline(buffer, sizeof(buffer))) { + if (strstr(buffer, board_ip.c_str())) { + return get_interface_from_arp_entry(buffer); } } - status = HAILO_SUCCESS; -l_exit: - if (NULL != arp_file) { - fclose_rc = fclose(arp_file); - if (0 != fclose_rc) { - LOGGER__ERROR("Cannot close arp file {} ", ETHERNET_UTILS__ARP_FILE); - if (HAILO_SUCCESS == status) { - status = HAILO_CLOSE_FAILURE; - } else { - LOGGER__ERROR("Did not override status. Left status value at: {} (not assigned {}", - status, - HAILO_CLOSE_FAILURE); - } - } - } - - return status; + LOGGER__ERROR("Failed to find interface name for ip {}", board_ip); + return make_unexpected(HAILO_ETH_FAILURE); } -hailo_status EthernetUtils::get_ip_from_interface(const char *interface_name, char *ip, size_t ip_length) +Expected EthernetUtils::get_ip_from_interface(const std::string &interface_name) { - hailo_status status = HAILO_UNINITIALIZED; struct ifreq ifr = {}; - int fd = 0; - int posix_rc = 0; - - CHECK_ARG_NOT_NULL(interface_name); - CHECK_ARG_NOT_NULL(ip); /* Create socket */ - fd = socket(AF_INET, SOCK_DGRAM, 0); - if (fd < 0) { - LOGGER__ERROR("Failed to create socket. Errno: {:#x}", errno); - status = HAILO_ETH_FAILURE; - goto l_exit; - } + auto socket = Socket::create(AF_INET, SOCK_DGRAM, 0); + CHECK_EXPECTED(socket); /* Convert interface name to ip address */ ifr.ifr_addr.sa_family = AF_INET; - (void)strncpy(ifr.ifr_name, interface_name, IFNAMSIZ-1); - posix_rc = ioctl(fd, SIOCGIFADDR, &ifr); - if (0 > posix_rc) { - LOGGER__ERROR("Interface was not found. ioctl with SIOCGIFADDR has failed. Errno: {:#x}", errno); - status = HAILO_ETH_INTERFACE_NOT_FOUND; - goto l_exit; - } - (void)strncpy(ip, inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr), ip_length); - LOGGER__DEBUG("Interface {} | IP: {}", interface_name, ip); - - status = HAILO_SUCCESS; -l_exit: - /* Close the socket if it was created */ - if (0 < fd) { - posix_rc = close(fd); - if (0 != posix_rc) { - LOGGER__ERROR("Failed closing socket. Errno: {:#x}", errno); - /* Update status if only in case there was not previous error */ - if (HAILO_SUCCESS == status) { - status = HAILO_CLOSE_FAILURE; - } else { - LOGGER__ERROR("Did not override status. Left status value at: {} (not assigned {}", - status, - HAILO_CLOSE_FAILURE); - } - } - } - - return status; + (void)strncpy(ifr.ifr_name, interface_name.c_str(), IFNAMSIZ-1); + auto posix_rc = ioctl(socket->get_fd(), SIOCGIFADDR, &ifr); + CHECK_AS_EXPECTED(posix_rc >= 0, HAILO_ETH_INTERFACE_NOT_FOUND, + "Interface was not found. ioctl with SIOCGIFADDR has failed. errno: {:#x}", errno); + + std::string res = inet_ntoa(((struct sockaddr_in *)&ifr.ifr_addr)->sin_addr); + LOGGER__DEBUG("Interface {} | IP: {}", interface_name, res); + return res; } } /* namespace hailort */ diff --git a/hailort/common/os/posix/filesystem.cpp b/hailort/common/os/posix/filesystem.cpp index 1a41347..57a12ac 100644 --- a/hailort/common/os/posix/filesystem.cpp +++ b/hailort/common/os/posix/filesystem.cpp @@ -165,6 +165,13 @@ hailo_status Filesystem::create_directory(const std::string &dir_path) return HAILO_SUCCESS; } +hailo_status Filesystem::remove_directory(const std::string &dir_path) +{ + auto ret_val = rmdir(dir_path.c_str()); + CHECK(0 == ret_val, HAILO_FILE_OPERATION_FAILURE, "Failed to remove directory {}", dir_path); + return HAILO_SUCCESS; +} + Expected Filesystem::get_current_dir() { char cwd[PATH_MAX]; diff --git a/hailort/common/os/posix/os_utils.cpp b/hailort/common/os/posix/os_utils.cpp index 36f1819..c9b0e98 100644 --- a/hailort/common/os/posix/os_utils.cpp +++ b/hailort/common/os/posix/os_utils.cpp @@ -8,15 +8,20 @@ **/ #include "hailo/hailort.h" - #include "common/os_utils.hpp" - +#include "common/utils.hpp" #include "spdlog/sinks/syslog_sink.h" +#include +#include +#include + namespace hailort { +#define EXISTENCE_CHECK_SIGNAL (0) + HailoRTOSLogger::HailoRTOSLogger() { m_hailort_os_logger = spdlog::syslog_logger_mt("syslog", "hailort_service", LOG_PID); @@ -29,6 +34,46 @@ uint32_t OsUtils::get_curr_pid() return getpid(); } +bool OsUtils::is_pid_alive(uint32_t pid) +{ + return (0 == kill(pid, EXISTENCE_CHECK_SIGNAL)); +} + +void OsUtils::set_current_thread_name(const std::string &name) +{ + (void)name; +#ifndef NDEBUG + // pthread_setname_np name size is limited to 16 chars (including null terminator) + assert(name.size() < 16); + pthread_setname_np(pthread_self(), name.c_str()); +#endif /* NDEBUG */ +} + +hailo_status OsUtils::set_current_thread_affinity(uint8_t cpu_index) +{ +#if defined(__linux__) + cpu_set_t cpuset; + CPU_ZERO(&cpuset); + CPU_SET(cpu_index, &cpuset); + + static const pid_t CURRENT_THREAD = 0; + int rc = sched_setaffinity(CURRENT_THREAD, sizeof(cpu_set_t), &cpuset); + CHECK(rc == 0, HAILO_INTERNAL_FAILURE, "sched_setaffinity failed with status {}", rc); + + return HAILO_SUCCESS; +#elif defined(__QNX__) + (void)cpu_index; + // TODO: impl on qnx (HRT-10889) + return HAILO_NOT_IMPLEMENTED; +#endif +} + +size_t OsUtils::get_page_size() +{ + static const auto page_size = sysconf(_SC_PAGESIZE); + return page_size; +} + CursorAdjustment::CursorAdjustment(){} CursorAdjustment::~CursorAdjustment(){} diff --git a/hailort/common/os/posix/traffic_control.cpp b/hailort/common/os/posix/traffic_control.cpp index fccbbc9..71aa3f4 100644 --- a/hailort/common/os/posix/traffic_control.cpp +++ b/hailort/common/os/posix/traffic_control.cpp @@ -22,7 +22,7 @@ namespace hailort Expected TrafficControlUtil::create(const std::string &ip, uint16_t port, uint32_t rate_bytes_per_sec) { - auto interface_name = get_interface_name(ip); + auto interface_name = EthernetUtils::get_interface_from_board_ip(ip); CHECK_EXPECTED(interface_name, "get_interface_name failed with status {}", interface_name.status()); auto board_id = ip_to_board_id(ip); @@ -158,17 +158,6 @@ hailo_status TrafficControlUtil::tc_class_del_dev_for_board(const std::string &i return run_command(cmd.str(), m_is_sudo_needed, {}, true); } -Expected TrafficControlUtil::get_interface_name(const std::string &ip) -{ - auto interface_name = Buffer::create(EthernetUtils::MAX_INTERFACE_SIZE, 0); - CHECK_EXPECTED(interface_name); - - CHECK_SUCCESS_AS_EXPECTED(EthernetUtils::get_interface_from_board_ip(ip.c_str(), - interface_name->as_pointer(), interface_name->size())); - - return interface_name->to_string(); -} - Expected TrafficControlUtil::ip_to_board_id(const std::string &ip) { // Takes last digit from 3 octet + the whole 4th octet diff --git a/hailort/common/os/posix/traffic_control.hpp b/hailort/common/os/posix/traffic_control.hpp index cbca2e8..82cbfff 100644 --- a/hailort/common/os/posix/traffic_control.hpp +++ b/hailort/common/os/posix/traffic_control.hpp @@ -23,7 +23,6 @@ class TrafficControlUtil final { public: static Expected create(const std::string &ip, uint16_t port, uint32_t rate_bytes_per_sec); - static Expected get_interface_name(const std::string &ip); ~TrafficControlUtil() = default; TrafficControlUtil(TrafficControlUtil&) = delete; TrafficControlUtil &operator=(const TrafficControlUtil &) = delete; diff --git a/hailort/common/os/windows/ethernet_utils.cpp b/hailort/common/os/windows/ethernet_utils.cpp index 8fec8d4..4dcfd3f 100644 --- a/hailort/common/os/windows/ethernet_utils.cpp +++ b/hailort/common/os/windows/ethernet_utils.cpp @@ -160,48 +160,40 @@ Expected ArpTable::create(uint32_t interface_index) return result; } -hailo_status EthernetUtils::get_interface_from_board_ip(const char *board_ip, char *interface_name, size_t interface_name_length) +Expected EthernetUtils::get_interface_from_board_ip(const std::string &board_ip) { - CHECK_ARG_NOT_NULL(interface_name); - CHECK_ARG_NOT_NULL(board_ip); - auto network_interfaces = NetworkInterface::get_all_interfaces(); - CHECK_EXPECTED_AS_STATUS(network_interfaces); + CHECK_EXPECTED(network_interfaces); struct in_addr board_ip_struct{}; - auto status = Socket::pton(AF_INET, board_ip, &board_ip_struct); - CHECK_SUCCESS(status, "Invalid board ip address {}", board_ip); + auto status = Socket::pton(AF_INET, board_ip.c_str(), &board_ip_struct); + CHECK_SUCCESS_AS_EXPECTED(status, "Invalid board ip address {}", board_ip); for (const auto& network_interface : network_interfaces.value()) { auto arp_table = ArpTable::create(network_interface.index()); - CHECK_EXPECTED_AS_STATUS(arp_table); + CHECK_EXPECTED(arp_table); const auto mac_address = arp_table->get_mac_address(static_cast(board_ip_struct.S_un.S_addr)); if (mac_address) { - (void)strncpy(interface_name, network_interface.friendly_name().c_str(), interface_name_length); - return HAILO_SUCCESS; + return network_interface.friendly_name(); } } - return HAILO_ETH_INTERFACE_NOT_FOUND; + return make_unexpected(HAILO_ETH_INTERFACE_NOT_FOUND); } -hailo_status EthernetUtils::get_ip_from_interface(const char *interface_name, char *ip, size_t ip_length) +Expected EthernetUtils::get_ip_from_interface(const std::string &interface_name) { - CHECK_ARG_NOT_NULL(interface_name); - CHECK_ARG_NOT_NULL(ip); - auto network_interfaces = NetworkInterface::get_all_interfaces(); - CHECK_EXPECTED_AS_STATUS(network_interfaces); + CHECK_EXPECTED(network_interfaces); for (const auto& network_interface : network_interfaces.value()) { if (network_interface.friendly_name() == interface_name) { - (void)strncpy(ip, network_interface.ip().c_str(), ip_length); - return HAILO_SUCCESS; + return network_interface.ip(); } } - return HAILO_ETH_INTERFACE_NOT_FOUND; + return make_unexpected(HAILO_ETH_INTERFACE_NOT_FOUND); } } /* namespace hailort */ diff --git a/hailort/common/os/windows/filesystem.cpp b/hailort/common/os/windows/filesystem.cpp index d9adeae..6dbcc25 100644 --- a/hailort/common/os/windows/filesystem.cpp +++ b/hailort/common/os/windows/filesystem.cpp @@ -164,6 +164,13 @@ hailo_status Filesystem::create_directory(const std::string &dir_path) return HAILO_SUCCESS; } +hailo_status Filesystem::remove_directory(const std::string &dir_path) +{ + bool was_removed = RemoveDirectoryA(dir_path.c_str()); + CHECK(was_removed, HAILO_FILE_OPERATION_FAILURE, "Failed to remove directory {}", dir_path); + return HAILO_SUCCESS; +} + bool Filesystem::is_path_accesible(const std::string &path) { // The code is based on examples from: https://cpp.hotexamples.com/examples/-/-/AccessCheck/cpp-accesscheck-function-examples.html diff --git a/hailort/common/os/windows/os_utils.cpp b/hailort/common/os/windows/os_utils.cpp index 3146d31..3d4022f 100644 --- a/hailort/common/os/windows/os_utils.cpp +++ b/hailort/common/os/windows/os_utils.cpp @@ -8,6 +8,7 @@ **/ #include "common/os_utils.hpp" +#include "common/utils.hpp" #include "hailo/hailort.h" #include @@ -29,6 +30,54 @@ uint32_t OsUtils::get_curr_pid() return static_cast(GetCurrentProcessId()); } +bool OsUtils::is_pid_alive(uint32_t pid) +{ + HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid); + if (hProcess == NULL) { + // Process is not running + return false; + } + + DWORD exitCode; + BOOL result = GetExitCodeProcess(hProcess, &exitCode); + + CloseHandle(hProcess); + + if (result && exitCode == STILL_ACTIVE) { + return true; + } + else { + return false; + } +} + +void OsUtils::set_current_thread_name(const std::string &name) +{ + (void)name; +} + +hailo_status OsUtils::set_current_thread_affinity(uint8_t cpu_index) +{ + const DWORD_PTR affinity_mask = static_cast(1ULL << cpu_index); + CHECK(0 != SetThreadAffinityMask(GetCurrentThread(), affinity_mask), HAILO_INTERNAL_FAILURE, + "SetThreadAffinityMask failed. LE={}", GetLastError()); + + return HAILO_SUCCESS; +} + +static size_t get_page_size_impl() +{ + SYSTEM_INFO system_info{}; + GetSystemInfo(&system_info); + return system_info.dwPageSize; +} + +size_t OsUtils::get_page_size() +{ + static const auto page_size = get_page_size_impl(); + return page_size; +} + CursorAdjustment::CursorAdjustment() { // Enables Vitual Terminal Processing - enables ANSI Escape Sequences on Windows diff --git a/hailort/common/os_utils.hpp b/hailort/common/os_utils.hpp index 30d0c2e..025ef1d 100644 --- a/hailort/common/os_utils.hpp +++ b/hailort/common/os_utils.hpp @@ -57,22 +57,12 @@ class OsUtils final { public: OsUtils() = delete; - static uint32_t get_curr_pid(); - - static void set_current_thread_name(const std::string &name) - { - (void)name; -#ifndef NDEBUG -#ifndef _WIN32 - // pthread_setname_np name size is limited to 16 chars (including null terminator) - assert(name.size() < 16); - pthread_setname_np(pthread_self(), name.c_str()); -#else -// TODO: implement for windows -#endif /* _WIN32 */ -#endif /* NDEBUG */ - } + static uint32_t get_curr_pid(); + static bool is_pid_alive(uint32_t pid); + static void set_current_thread_name(const std::string &name); + static hailo_status set_current_thread_affinity(uint8_t cpu_index); + static size_t get_page_size(); }; } /* namespace hailort */ diff --git a/hailort/common/socket.hpp b/hailort/common/socket.hpp index 8df9daf..afe0afd 100644 --- a/hailort/common/socket.hpp +++ b/hailort/common/socket.hpp @@ -42,6 +42,8 @@ public: m_module_wrapper(std::move(other.m_module_wrapper)), m_socket_fd(std::exchange(other.m_socket_fd, INVALID_SOCKET)) {}; + socket_t get_fd() { return m_socket_fd; } + static hailo_status ntop(int af, const void *src, char *dst, socklen_t size); static hailo_status pton(int af, const char *src, void *dst); diff --git a/hailort/common/utils.hpp b/hailort/common/utils.hpp index 3dd3bc6..a285651 100644 --- a/hailort/common/utils.hpp +++ b/hailort/common/utils.hpp @@ -18,6 +18,7 @@ #include #include #include +#include namespace hailort @@ -25,7 +26,7 @@ namespace hailort #define IS_FIT_IN_UINT8(number) ((std::numeric_limits::max() >= ((int32_t)(number))) && (std::numeric_limits::min() <= ((int32_t)(number)))) #define IS_FIT_IN_UINT16(number) ((std::numeric_limits::max() >= ((int32_t)(number))) && (std::numeric_limits::min() <= ((int32_t)(number)))) - +#define IS_FIT_IN_UINT32(number) ((std::numeric_limits::max() >= ((int64_t)(number))) && (std::numeric_limits::min() <= ((int64_t)(number)))) template static inline bool contains(const std::vector &container, const T &value) @@ -51,6 +52,12 @@ static inline bool contains(const std::set &container, T value) return (container.find(value) != container.end()); } +template +static inline bool contains(const std::unordered_set &container, T value) +{ + return (container.find(value) != container.end()); +} + // From https://stackoverflow.com/questions/57092289/do-stdmake-shared-and-stdmake-unique-have-a-nothrow-version template static inline std::unique_ptr make_unique_nothrow(Args&&... args) @@ -202,6 +209,14 @@ _ISEMPTY( \ } while(0) #define CHECK_SUCCESS_AS_EXPECTED(status, ...) _CHECK_SUCCESS_AS_EXPECTED(status, ISEMPTY(__VA_ARGS__), "" __VA_ARGS__) +// Define macro CHECK_IN_DEBUG - that checks cond in debug with CHECK macro but in release does nothing and will get optimized out +#ifdef NDEBUG +// In release have this macro do nothing - empty macro +#define CHECK_IN_DEBUG(cond, ret_val, ...) +#else // NDEBUG +#define CHECK_IN_DEBUG(cond, ret_val, ...) CHECK(cond, ret_val, __VA_ARGS__) +#endif // NDEBUG + #ifdef HAILO_SUPPORT_MULTI_PROCESS #define _CHECK_SUCCESS_AS_RPC_STATUS(status, reply, is_default, fmt, ...) \ do { \ @@ -314,6 +329,12 @@ static uint32_t get_min_value_of_unordered_map(const std::unordered_map &m return min_count; } +static inline bool is_env_variable_on(const char* env_var_name) +{ + auto env_var = std::getenv(env_var_name); + return ((nullptr != env_var) && (strnlen(env_var, 2) == 1) && (strncmp(env_var, "1", 1) == 0)); +} + } /* namespace hailort */ #endif /* HAILO_UTILS_H_ */ \ No newline at end of file diff --git a/hailort/drivers/common/hailo_ioctl_common.h b/hailort/drivers/common/hailo_ioctl_common.h index f11eae7..a70d4e0 100644 --- a/hailort/drivers/common/hailo_ioctl_common.h +++ b/hailort/drivers/common/hailo_ioctl_common.h @@ -27,6 +27,19 @@ #define INVALID_VDMA_CHANNEL (0xff) +#if !defined(__cplusplus) && defined(NTDDI_VERSION) +#include +typedef ULONG uint32_t; +typedef UCHAR uint8_t; +typedef USHORT uint16_t; +typedef ULONGLONG uint64_t; +typedef uint64_t u64; +typedef uint32_t u32; +typedef uint16_t u16; +typedef uint8_t u8; +#endif /* !defined(__cplusplus) && defined(NTDDI_VERSION) */ + + #ifdef _MSC_VER #if !defined(bool) && !defined(__cplusplus) typedef uint8_t bool; @@ -64,6 +77,8 @@ typedef uint8_t bool; #include #include #include +#include + // defines for devctl #define _IOW_ __DIOF #define _IOR_ __DIOT @@ -132,9 +147,9 @@ struct hailo_vdma_buffer_unmap_params { /* structure used in ioctl HAILO_DESC_LIST_CREATE */ struct hailo_desc_list_create_params { size_t desc_count; // in + bool is_circular; // in uintptr_t desc_handle; // out - // Note: The dma address is required for CONTEXT_SWITCH firmware controls - uint64_t dma_address; // out + uint64_t dma_address; // out }; /* structure used in ioctl HAILO_NON_LINUX_DESC_LIST_MMAP */ @@ -277,7 +292,7 @@ struct hailo_vdma_channel_write_register_params { /* structure used in ioctl HAILO_VDMA_BUFFER_SYNC */ enum hailo_vdma_buffer_sync_type { - HAILO_SYNC_FOR_HOST, + HAILO_SYNC_FOR_CPU, HAILO_SYNC_FOR_DEVICE, /** Max enum value to maintain ABI Integrity */ diff --git a/hailort/hailort_service/CMakeLists.txt b/hailort/hailort_service/CMakeLists.txt index 2b9e3a5..3755c4e 100644 --- a/hailort/hailort_service/CMakeLists.txt +++ b/hailort/hailort_service/CMakeLists.txt @@ -13,6 +13,7 @@ add_executable(hailort_service service_resource_manager.hpp ${HAILORT_SERVICE_OS_DIR}/hailort_service.cpp ${HAILORT_COMMON_CPP_SOURCES} + ${HAILO_FULL_OS_DIR}/event.cpp # TODO HRT-10681: move event.cpp to common ) target_compile_options(hailort_service PRIVATE ${HAILORT_COMPILE_OPTIONS}) set_property(TARGET hailort_service PROPERTY CXX_STANDARD 14) diff --git a/hailort/hailort_service/hailort_rpc_service.cpp b/hailort/hailort_service/hailort_rpc_service.cpp index 09a0472..d76ed61 100644 --- a/hailort/hailort_service/hailort_rpc_service.cpp +++ b/hailort/hailort_service/hailort_rpc_service.cpp @@ -32,32 +32,146 @@ HailoRtRpcService::HailoRtRpcService() }); } -void HailoRtRpcService::keep_alive() +hailo_status HailoRtRpcService::abort_input_vstream(uint32_t handle) { - while (true) { - std::this_thread::sleep_for(hailort::HAILO_KEEPALIVE_INTERVAL / 2); - auto now = std::chrono::high_resolution_clock::now(); + if (is_input_vstream_aborted(handle)) { + return HAILO_SUCCESS; + } + + auto lambda = [](std::shared_ptr input_vstream) { + return input_vstream->abort(); + }; + auto &manager = ServiceResourceManager::get_instance(); + auto status = manager.execute(handle, lambda); + if (HAILO_SUCCESS != status) { + LOGGER__ERROR("Failed to abort input vstream with status {}", status); + } + return status; +} + +hailo_status HailoRtRpcService::abort_output_vstream(uint32_t handle) +{ + if (is_output_vstream_aborted(handle)) { + return HAILO_SUCCESS; + } + + auto lambda = [](std::shared_ptr output_vstream) { + return output_vstream->abort(); + }; + auto &manager = ServiceResourceManager::get_instance(); + auto status = manager.execute(handle, lambda); + if (HAILO_SUCCESS != status) { + LOGGER__ERROR("Failed to abort output vstream with status {}", status); + } + return status; +} + +bool HailoRtRpcService::is_input_vstream_aborted(uint32_t handle) +{ + auto lambda = [](std::shared_ptr input_vstream) { + return input_vstream->is_aborted(); + }; + auto &manager = ServiceResourceManager::get_instance(); + return manager.execute(handle, lambda); +} + +bool HailoRtRpcService::is_output_vstream_aborted(uint32_t handle) +{ + auto lambda = [](std::shared_ptr output_vstream) { + return output_vstream->is_aborted(); + }; + auto &manager = ServiceResourceManager::get_instance(); + return manager.execute(handle, lambda); +} + +hailo_status HailoRtRpcService::resume_input_vstream(uint32_t handle) +{ + if (!is_input_vstream_aborted(handle)) { + return HAILO_SUCCESS; + } + + auto lambda = [](std::shared_ptr input_vstream) { + return input_vstream->resume(); + }; + auto &manager = ServiceResourceManager::get_instance(); + auto status = manager.execute(handle, lambda); + if (HAILO_SUCCESS != status) { + LOGGER__ERROR("Failed to resume input vstream with status {}", status); + } + return status; +} + +hailo_status HailoRtRpcService::resume_output_vstream(uint32_t handle) +{ + if (!is_output_vstream_aborted(handle)) { + return HAILO_SUCCESS; + } + + auto lambda = [](std::shared_ptr output_vstream) { + return output_vstream->resume(); + }; + auto &manager = ServiceResourceManager::get_instance(); + auto status = manager.execute(handle, lambda); + if (HAILO_SUCCESS != status) { + LOGGER__ERROR("Failed to resume output vstream with status {}", status); + } + return status; +} + +// TODO: Add a named templated release functions for InputVStream and OutputVStream to call abort before release. +void HailoRtRpcService::abort_vstreams_by_pids(std::set &pids) +{ + auto inputs_handles = ServiceResourceManager::get_instance().resources_handles_by_pids(pids); + auto outputs_handles = ServiceResourceManager::get_instance().resources_handles_by_pids(pids); + for (auto &input_handle : inputs_handles) { + abort_input_vstream(input_handle); + } + for (auto &output_handle : outputs_handles) { + abort_output_vstream(output_handle); + } +} + + +void HailoRtRpcService::remove_disconnected_clients() +{ + std::this_thread::sleep_for(hailort::HAILO_KEEPALIVE_INTERVAL / 2); + auto now = std::chrono::high_resolution_clock::now(); + std::set pids_to_remove; + { std::unique_lock lock(m_mutex); - std::set pids_to_remove; for (auto pid_to_last_alive : m_clients_pids) { auto duration = std::chrono::duration_cast(now - pid_to_last_alive.second); if (duration > hailort::HAILO_KEEPALIVE_INTERVAL) { - auto client_id = pid_to_last_alive.first; - pids_to_remove.insert(client_id); - LOGGER__INFO("Client disconnected, pid: {}", client_id); - HAILORT_OS_LOG_INFO("Client disconnected, pid: {}", client_id); - ServiceResourceManager::get_instance().release_by_pid(client_id); - ServiceResourceManager::get_instance().release_by_pid(client_id); - ServiceResourceManager::get_instance().release_by_pid(client_id); - ServiceResourceManager::get_instance().release_by_pid(client_id); + auto client_pid = pid_to_last_alive.first; + pids_to_remove.insert(client_pid); } } - for (auto &pid : pids_to_remove) { - m_clients_pids.erase(pid); + + // We abort vstreams before releasing them to avoid cases where the vstream is stuck in execute of a + // blocking operation (which will be finished with timeout). + // To release the vstream the ServiceResourceManager is waiting for the resource_mutex which is also locked in execute. + abort_vstreams_by_pids(pids_to_remove); + for (auto &client_pid : pids_to_remove) { + ServiceResourceManager::get_instance().release_by_pid(client_pid); + ServiceResourceManager::get_instance().release_by_pid(client_pid); + ServiceResourceManager::get_instance().release_by_pid(client_pid); + ServiceResourceManager::get_instance().release_by_pid(client_pid); + + LOGGER__INFO("Client disconnected, pid: {}", client_pid); + HAILORT_OS_LOG_INFO("Client disconnected, pid: {}", client_pid); + m_clients_pids.erase(client_pid); } } } + +void HailoRtRpcService::keep_alive() +{ + while (true) { + remove_disconnected_clients(); + } +} + grpc::Status HailoRtRpcService::client_keep_alive(grpc::ServerContext*, const keepalive_Request *request, empty*) { @@ -93,6 +207,8 @@ grpc::Status HailoRtRpcService::VDevice_dup_handle(grpc::ServerContext*, const d grpc::Status HailoRtRpcService::VDevice_create(grpc::ServerContext *, const VDevice_create_Request *request, VDevice_create_Reply *reply) { + remove_disconnected_clients(); + // Deserialization const auto params_proto = request->hailo_vdevice_params(); std::vector device_ids; @@ -125,8 +241,8 @@ grpc::Status HailoRtRpcService::VDevice_release(grpc::ServerContext*, const Rele Release_Reply *reply) { auto &manager = ServiceResourceManager::get_instance(); - auto status = manager.release_resource(request->handle()); - reply->set_status(static_cast(status)); + manager.release_resource(request->handle(), request->pid()); + reply->set_status(static_cast(HAILO_SUCCESS)); return grpc::Status::OK; } @@ -236,8 +352,8 @@ grpc::Status HailoRtRpcService::ConfiguredNetworkGroup_release(grpc::ServerConte Release_Reply *reply) { auto &manager = ServiceResourceManager::get_instance(); - auto status = manager.release_resource(request->handle()); - reply->set_status(static_cast(status)); + manager.release_resource(request->handle(), request->pid()); + reply->set_status(static_cast(HAILO_SUCCESS)); return grpc::Status::OK; } @@ -468,11 +584,12 @@ grpc::Status HailoRtRpcService::ConfiguredNetworkGroup_set_scheduler_timeout(grp const ConfiguredNetworkGroup_set_scheduler_timeout_Request *request, ConfiguredNetworkGroup_set_scheduler_timeout_Reply *reply) { - auto lambda = [](std::shared_ptr cng, std::chrono::milliseconds timeout_ms) { - return cng->set_scheduler_timeout(timeout_ms); + auto lambda = [](std::shared_ptr cng, std::chrono::milliseconds timeout_ms, std::string network_name) { + return cng->set_scheduler_timeout(timeout_ms, network_name); }; auto &net_group_manager = ServiceResourceManager::get_instance(); - auto status = net_group_manager.execute(request->handle(), lambda, static_cast(request->timeout_ms())); + auto status = net_group_manager.execute(request->handle(), lambda, static_cast(request->timeout_ms()), + request->network_name()); reply->set_status(status); return grpc::Status::OK; } @@ -561,21 +678,24 @@ grpc::Status HailoRtRpcService::InputVStreams_create(grpc::ServerContext *, cons }; inputs_params.emplace(param_proto.name(), std::move(params)); } + auto network_group_handle = request->net_group(); + auto client_pid = request->pid(); auto lambda = [](std::shared_ptr cng, const std::map &inputs_params) { return cng->create_input_vstreams(inputs_params); }; auto &net_group_manager = ServiceResourceManager::get_instance(); - auto vstreams_expected = net_group_manager.execute>>(request->net_group(), lambda, inputs_params); + auto vstreams_expected = net_group_manager.execute>>(network_group_handle, lambda, inputs_params); CHECK_EXPECTED_AS_RPC_STATUS(vstreams_expected, reply); auto vstreams = vstreams_expected.release(); - auto &manager = ServiceResourceManager::get_instance(); - auto client_pid = request->pid(); + auto &manager = ServiceResourceManager::get_instance(); for (size_t i = 0; i < vstreams.size(); i++) { auto handle = manager.register_resource(client_pid, make_shared_nothrow(std::move(vstreams[i]))); reply->add_handles(handle); } + net_group_manager.dup_handle(client_pid, network_group_handle); + reply->set_status(static_cast(HAILO_SUCCESS)); return grpc::Status::OK; } @@ -584,8 +704,8 @@ grpc::Status HailoRtRpcService::InputVStream_release(grpc::ServerContext *, cons Release_Reply *reply) { auto &manager = ServiceResourceManager::get_instance(); - auto status = manager.release_resource(request->handle()); - reply->set_status(static_cast(status)); + manager.release_resource(request->handle(), request->pid()); + reply->set_status(static_cast(HAILO_SUCCESS)); return grpc::Status::OK; } @@ -610,20 +730,24 @@ grpc::Status HailoRtRpcService::OutputVStreams_create(grpc::ServerContext *, con output_params.emplace(param_proto.name(), std::move(params)); } + auto network_group_handle = request->net_group(); + auto client_pid = request->pid(); + auto lambda = [](std::shared_ptr cng, const std::map &output_params) { return cng->create_output_vstreams(output_params); }; auto &net_group_manager = ServiceResourceManager::get_instance(); - auto vstreams_expected = net_group_manager.execute>>(request->net_group(), lambda, output_params); + auto vstreams_expected = net_group_manager.execute>>(network_group_handle, lambda, output_params); CHECK_EXPECTED_AS_RPC_STATUS(vstreams_expected, reply); auto vstreams = vstreams_expected.release(); - auto &manager = ServiceResourceManager::get_instance(); - auto client_pid = request->pid(); + auto &manager = ServiceResourceManager::get_instance(); for (size_t i = 0; i < vstreams.size(); i++) { auto handle = manager.register_resource(client_pid, make_shared_nothrow(std::move(vstreams[i]))); reply->add_handles(handle); } + net_group_manager.dup_handle(client_pid, network_group_handle); + reply->set_status(static_cast(HAILO_SUCCESS)); return grpc::Status::OK; } @@ -631,8 +755,17 @@ grpc::Status HailoRtRpcService::OutputVStreams_create(grpc::ServerContext *, con grpc::Status HailoRtRpcService::OutputVStream_release(grpc::ServerContext *, const Release_Request *request, Release_Reply *reply) { + auto was_aborted = is_output_vstream_aborted(request->handle()); + abort_output_vstream(request->handle()); auto &manager = ServiceResourceManager::get_instance(); - auto status = manager.release_resource(request->handle()); + auto resource = manager.release_resource(request->handle(), request->pid()); + auto status = HAILO_SUCCESS; + if (resource && (!was_aborted)) { + status = resource->resume(); + if (HAILO_SUCCESS != status) { + LOGGER__INFO("Failed to resume output vstream {} after destruction", resource->name()); + } + } reply->set_status(static_cast(status)); return grpc::Status::OK; } @@ -752,6 +885,8 @@ grpc::Status HailoRtRpcService::ConfiguredNetworkGroup_get_all_stream_infos(grpc auto proto_nms_info_defuse_info = proto_nms_info->mutable_defuse_info(); proto_nms_info_defuse_info->set_class_group_index(stream_info.nms_info.defuse_info.class_group_index); proto_nms_info_defuse_info->set_original_name(std::string(stream_info.nms_info.defuse_info.original_name)); + proto_nms_info->set_burst_size(stream_info.nms_info.burst_size); + proto_nms_info->set_burst_type(static_cast(proto_stream_info.nms_info().burst_type())); } else { auto proto_stream_shape = proto_stream_info.mutable_stream_shape(); auto proto_stream_shape_shape = proto_stream_shape->mutable_shape(); @@ -793,9 +928,13 @@ grpc::Status HailoRtRpcService::ConfiguredNetworkGroup_get_latency_measurement(g }; auto &manager = ServiceResourceManager::get_instance(); auto expected_latency_result = manager.execute>(request->handle(), lambda, request->network_name()); - CHECK_EXPECTED_AS_RPC_STATUS(expected_latency_result, reply); - reply->set_avg_hw_latency(static_cast(expected_latency_result.value().avg_hw_latency.count())); - reply->set_status(static_cast(HAILO_SUCCESS)); + if (HAILO_NOT_AVAILABLE == expected_latency_result.status()) { + reply->set_status(static_cast(HAILO_NOT_AVAILABLE)); + } else { + CHECK_EXPECTED_AS_RPC_STATUS(expected_latency_result, reply); + reply->set_avg_hw_latency(static_cast(expected_latency_result.value().avg_hw_latency.count())); + reply->set_status(static_cast(HAILO_SUCCESS)); + } return grpc::Status::OK; } @@ -813,6 +952,60 @@ grpc::Status HailoRtRpcService::ConfiguredNetworkGroup_is_multi_context(grpc::Se return grpc::Status::OK; } +grpc::Status HailoRtRpcService::ConfiguredNetworkGroup_get_sorted_output_names(grpc::ServerContext*, + const ConfiguredNetworkGroup_get_sorted_output_names_Request *request, + ConfiguredNetworkGroup_get_sorted_output_names_Reply *reply) +{ + auto lambda = [](std::shared_ptr cng) { + return cng->get_sorted_output_names(); + }; + auto &manager = ServiceResourceManager::get_instance(); + auto sorted_output_names_expected = manager.execute>>(request->handle(), lambda); + CHECK_EXPECTED_AS_RPC_STATUS(sorted_output_names_expected, reply); + auto sorted_output_names_proto = reply->mutable_sorted_output_names(); + for (auto &name : sorted_output_names_expected.value()) { + sorted_output_names_proto->Add(std::move(name)); + } + reply->set_status(static_cast(HAILO_SUCCESS)); + return grpc::Status::OK; +} + +grpc::Status HailoRtRpcService::ConfiguredNetworkGroup_get_stream_names_from_vstream_name(grpc::ServerContext*, + const ConfiguredNetworkGroup_get_stream_names_from_vstream_name_Request *request, + ConfiguredNetworkGroup_get_stream_names_from_vstream_name_Reply *reply) +{ + auto lambda = [](std::shared_ptr cng, const std::string &vstream_name) { + return cng->get_stream_names_from_vstream_name(vstream_name); + }; + auto &manager = ServiceResourceManager::get_instance(); + auto streams_names_expected = manager.execute>>(request->handle(), lambda, request->vstream_name()); + CHECK_EXPECTED_AS_RPC_STATUS(streams_names_expected, reply); + auto streams_names_proto = reply->mutable_streams_names(); + for (auto &name : streams_names_expected.value()) { + streams_names_proto->Add(std::move(name)); + } + reply->set_status(static_cast(HAILO_SUCCESS)); + return grpc::Status::OK; +} + +grpc::Status HailoRtRpcService::ConfiguredNetworkGroup_get_vstream_names_from_stream_name(grpc::ServerContext*, + const ConfiguredNetworkGroup_get_vstream_names_from_stream_name_Request *request, + ConfiguredNetworkGroup_get_vstream_names_from_stream_name_Reply *reply) +{ + auto lambda = [](std::shared_ptr cng, const std::string &stream_name) { + return cng->get_vstream_names_from_stream_name(stream_name); + }; + auto &manager = ServiceResourceManager::get_instance(); + auto vstreams_names_expected = manager.execute>>(request->handle(), lambda, request->stream_name()); + CHECK_EXPECTED_AS_RPC_STATUS(vstreams_names_expected, reply); + auto vstreams_names_proto = reply->mutable_vstreams_names(); + for (auto &name : vstreams_names_expected.value()) { + vstreams_names_proto->Add(std::move(name)); + } + reply->set_status(static_cast(HAILO_SUCCESS)); + return grpc::Status::OK; +} + grpc::Status HailoRtRpcService::InputVStream_get_frame_size(grpc::ServerContext*, const VStream_get_frame_size_Request *request, VStream_get_frame_size_Reply *reply) { @@ -906,11 +1099,7 @@ grpc::Status HailoRtRpcService::OutputVStream_network_name(grpc::ServerContext*, grpc::Status HailoRtRpcService::InputVStream_abort(grpc::ServerContext*, const VStream_abort_Request *request, VStream_abort_Reply *reply) { - auto lambda = [](std::shared_ptr input_vstream) { - return input_vstream->abort(); - }; - auto &manager = ServiceResourceManager::get_instance(); - auto status = manager.execute(request->handle(), lambda); + auto status = abort_input_vstream(request->handle()); reply->set_status(status); return grpc::Status::OK; } @@ -918,11 +1107,7 @@ grpc::Status HailoRtRpcService::InputVStream_abort(grpc::ServerContext*, const V grpc::Status HailoRtRpcService::OutputVStream_abort(grpc::ServerContext*, const VStream_abort_Request *request, VStream_abort_Reply *reply) { - auto lambda = [](std::shared_ptr output_vstream) { - return output_vstream->abort(); - }; - auto &manager = ServiceResourceManager::get_instance(); - auto status = manager.execute(request->handle(), lambda); + auto status = abort_output_vstream(request->handle()); reply->set_status(status); return grpc::Status::OK; } @@ -951,6 +1136,54 @@ grpc::Status HailoRtRpcService::OutputVStream_resume(grpc::ServerContext*, const return grpc::Status::OK; } +grpc::Status HailoRtRpcService::InputVStream_stop_and_clear(grpc::ServerContext*, const VStream_stop_and_clear_Request *request, + VStream_stop_and_clear_Reply *reply) +{ + auto lambda = [](std::shared_ptr input_vstream) { + return input_vstream->stop_and_clear(); + }; + auto &manager = ServiceResourceManager::get_instance(); + auto status = manager.execute(request->handle(), lambda); + reply->set_status(status); + return grpc::Status::OK; +} + +grpc::Status HailoRtRpcService::OutputVStream_stop_and_clear(grpc::ServerContext*, const VStream_stop_and_clear_Request *request, + VStream_stop_and_clear_Reply *reply) +{ + auto lambda = [](std::shared_ptr output_vstream) { + return output_vstream->stop_and_clear(); + }; + auto &manager = ServiceResourceManager::get_instance(); + auto status = manager.execute(request->handle(), lambda); + reply->set_status(status); + return grpc::Status::OK; +} + +grpc::Status HailoRtRpcService::InputVStream_start_vstream(grpc::ServerContext*, const VStream_start_vstream_Request *request, + VStream_start_vstream_Reply *reply) +{ + auto lambda = [](std::shared_ptr input_vstream) { + return input_vstream->start_vstream(); + }; + auto &manager = ServiceResourceManager::get_instance(); + auto status = manager.execute(request->handle(), lambda); + reply->set_status(status); + return grpc::Status::OK; +} + +grpc::Status HailoRtRpcService::OutputVStream_start_vstream(grpc::ServerContext*, const VStream_start_vstream_Request *request, + VStream_start_vstream_Reply *reply) +{ + auto lambda = [](std::shared_ptr output_vstream) { + return output_vstream->start_vstream(); + }; + auto &manager = ServiceResourceManager::get_instance(); + auto status = manager.execute(request->handle(), lambda); + reply->set_status(status); + return grpc::Status::OK; +} + grpc::Status HailoRtRpcService::InputVStream_get_user_buffer_format(grpc::ServerContext*, const VStream_get_user_buffer_format_Request *request, VStream_get_user_buffer_format_Reply *reply) { @@ -1015,5 +1248,31 @@ grpc::Status HailoRtRpcService::OutputVStream_get_info(grpc::ServerContext*, con return grpc::Status::OK; } +grpc::Status HailoRtRpcService::InputVStream_is_aborted(grpc::ServerContext*, const VStream_is_aborted_Request *request, + VStream_is_aborted_Reply *reply) +{ + auto lambda = [](std::shared_ptr input_vstream) { + return input_vstream->is_aborted(); + }; + auto &manager = ServiceResourceManager::get_instance(); + auto is_aborted = manager.execute(request->handle(), lambda); + reply->set_is_aborted(is_aborted); + reply->set_status(static_cast(HAILO_SUCCESS)); + return grpc::Status::OK; +} + +grpc::Status HailoRtRpcService::OutputVStream_is_aborted(grpc::ServerContext*, const VStream_is_aborted_Request *request, + VStream_is_aborted_Reply *reply) +{ + auto lambda = [](std::shared_ptr input_vstream) { + return input_vstream->is_aborted(); + }; + auto &manager = ServiceResourceManager::get_instance(); + auto is_aborted = manager.execute(request->handle(), lambda); + reply->set_is_aborted(is_aborted); + reply->set_status(static_cast(HAILO_SUCCESS)); + return grpc::Status::OK; +} + } diff --git a/hailort/hailort_service/hailort_rpc_service.hpp b/hailort/hailort_service/hailort_rpc_service.hpp index a77d701..fe1e9c5 100644 --- a/hailort/hailort_service/hailort_rpc_service.hpp +++ b/hailort/hailort_service/hailort_rpc_service.hpp @@ -26,6 +26,7 @@ #endif #include +#include "hailo/hailort.h" namespace hailort { @@ -98,6 +99,18 @@ public: dup_handle_Reply*) override; virtual grpc::Status OutputVStream_dup_handle(grpc::ServerContext *ctx, const dup_handle_Request *request, dup_handle_Reply*) override; + virtual grpc::Status InputVStream_stop_and_clear(grpc::ServerContext *ctx, const VStream_stop_and_clear_Request *request, + VStream_stop_and_clear_Reply*) override; + virtual grpc::Status OutputVStream_stop_and_clear(grpc::ServerContext *ctx, const VStream_stop_and_clear_Request *request, + VStream_stop_and_clear_Reply*) override; + virtual grpc::Status InputVStream_start_vstream(grpc::ServerContext *ctx, const VStream_start_vstream_Request *request, + VStream_start_vstream_Reply*) override; + virtual grpc::Status OutputVStream_start_vstream(grpc::ServerContext *ctx, const VStream_start_vstream_Request *request, + VStream_start_vstream_Reply*) override; + virtual grpc::Status InputVStream_is_aborted(grpc::ServerContext *ctx, const VStream_is_aborted_Request *request, + VStream_is_aborted_Reply*) override; + virtual grpc::Status OutputVStream_is_aborted(grpc::ServerContext *ctx, const VStream_is_aborted_Request *request, + VStream_is_aborted_Reply*) override; virtual grpc::Status ConfiguredNetworkGroup_dup_handle(grpc::ServerContext *ctx, const dup_handle_Request *request, dup_handle_Reply*) override; @@ -157,9 +170,26 @@ public: virtual grpc::Status ConfiguredNetworkGroup_get_config_params(grpc::ServerContext*, const ConfiguredNetworkGroup_get_config_params_Request *request, ConfiguredNetworkGroup_get_config_params_Reply *reply) override; + virtual grpc::Status ConfiguredNetworkGroup_get_sorted_output_names(grpc::ServerContext*, + const ConfiguredNetworkGroup_get_sorted_output_names_Request *request, + ConfiguredNetworkGroup_get_sorted_output_names_Reply *reply) override; + virtual grpc::Status ConfiguredNetworkGroup_get_stream_names_from_vstream_name(grpc::ServerContext*, + const ConfiguredNetworkGroup_get_stream_names_from_vstream_name_Request *request, + ConfiguredNetworkGroup_get_stream_names_from_vstream_name_Reply *reply) override; + virtual grpc::Status ConfiguredNetworkGroup_get_vstream_names_from_stream_name(grpc::ServerContext*, + const ConfiguredNetworkGroup_get_vstream_names_from_stream_name_Request *request, + ConfiguredNetworkGroup_get_vstream_names_from_stream_name_Reply *reply) override; private: void keep_alive(); + hailo_status abort_input_vstream(uint32_t handle); + hailo_status abort_output_vstream(uint32_t handle); + hailo_status resume_input_vstream(uint32_t handle); + hailo_status resume_output_vstream(uint32_t handle); + bool is_input_vstream_aborted(uint32_t handle); + bool is_output_vstream_aborted(uint32_t handle); + void abort_vstreams_by_pids(std::set &pids); + void remove_disconnected_clients(); std::mutex m_mutex; std::map> m_clients_pids; diff --git a/hailort/hailort_service/hailort_service b/hailort/hailort_service/hailort_service index 0259a28..67ee22b 100644 --- a/hailort/hailort_service/hailort_service +++ b/hailort/hailort_service/hailort_service @@ -7,5 +7,4 @@ [Service] HAILORT_LOGGER_PATH="/var/log/hailo" -HAILO_DISABLE_MULTIPLEXER=0 HAILO_MONITOR=0 diff --git a/hailort/hailort_service/service_resource_manager.hpp b/hailort/hailort_service/service_resource_manager.hpp index 488999c..5b5930b 100644 --- a/hailort/hailort_service/service_resource_manager.hpp +++ b/hailort/hailort_service/service_resource_manager.hpp @@ -13,9 +13,11 @@ #include "hailo/expected.hpp" #include "common/utils.hpp" +#include "common/os_utils.hpp" #include #include +#include namespace hailort { @@ -23,11 +25,13 @@ namespace hailort template struct Resource { Resource(uint32_t pid, std::shared_ptr resource) - : pid(pid), resource(std::move(resource)) - {} + : resource(std::move(resource)) + { + pids.insert(pid); + } - uint32_t pid; std::shared_ptr resource; + std::unordered_set pids; }; template @@ -69,42 +73,88 @@ public: uint32_t dup_handle(uint32_t pid, uint32_t handle) { - // Keeping this function for future possible usage - (void)pid; + std::unique_lock lock(m_mutex); + auto resource_expected = resource_lookup(handle); + assert(resource_expected); + auto resource = resource_expected.release(); + + assert(contains(m_resources_mutexes, handle)); + std::unique_lock resource_lock(m_resources_mutexes[handle]); + resource->pids.insert(pid); + return handle; } - hailo_status release_resource(uint32_t handle) + std::shared_ptr release_resource(uint32_t handle, uint32_t pid) { + std::shared_ptr res = nullptr; std::unique_lock lock(m_mutex); auto found = m_resources.find(handle); - CHECK(found != m_resources.end(), HAILO_NOT_FOUND, "Failed to release resource with handle {}, resource does not exist", handle); + if (found == m_resources.end()) { + LOGGER__INFO("Failed to release resource with handle {} and PID {}. The resource no longer exists or may have already been released", + handle, pid); + return res; + } + assert(contains(m_resources_mutexes, handle)); auto resource = m_resources[handle]; + bool release_resource = false; { std::unique_lock resource_lock(m_resources_mutexes[handle]); - m_resources.erase(handle); + resource->pids.erase(pid); + if (all_pids_dead(resource)) { + release_resource = true; + res = resource->resource; + m_resources.erase(handle); + } + } + if (release_resource) { + m_resources_mutexes.erase(handle); } - m_resources_mutexes.erase(handle); - return HAILO_SUCCESS; + return res; } - void release_by_pid(uint32_t pid) + std::vector> release_by_pid(uint32_t pid) { + std::vector> res; std::unique_lock lock(m_mutex); for (auto iter = m_resources.begin(); iter != m_resources.end(); ) { auto handle = iter->first; - if (iter->second->pid == pid) { + bool release_resource = false; + if (contains(iter->second->pids, pid)) { assert(contains(m_resources_mutexes, handle)); { std::unique_lock resource_lock(m_resources_mutexes[handle]); - iter = m_resources.erase(iter); + iter->second->pids.erase(pid); + if (iter->second->pids.empty()) { + release_resource = true; + res.push_back(iter->second->resource); + iter = m_resources.erase(iter); + } } + } + if (release_resource) { m_resources_mutexes.erase(handle); } else { ++iter; } } + + return res; + } + + std::vector resources_handles_by_pids(std::set &pids) + { + std::unique_lock lock(m_mutex); + std::vector resources_handles; + for (auto &handle_resource_pair : m_resources) { + for (auto &pid : pids) { + if (contains(handle_resource_pair.second->pids, pid)) { + resources_handles.emplace_back(handle_resource_pair.first); + } + } + } + return resources_handles; } private: @@ -120,6 +170,16 @@ private: return resource; } + bool all_pids_dead(std::shared_ptr> resource) + { + for (auto &pid : resource->pids) { + if (OsUtils::is_pid_alive(pid)) { + return false; + } + } + return true; + } + std::mutex m_mutex; std::atomic m_current_handle_index; std::unordered_map>> m_resources; diff --git a/hailort/hailort_service/windows/hailort_service_env_vars.bat b/hailort/hailort_service/windows/hailort_service_env_vars.bat index a615ab4..2b4e82a 100644 --- a/hailort/hailort_service/windows/hailort_service_env_vars.bat +++ b/hailort/hailort_service/windows/hailort_service_env_vars.bat @@ -7,5 +7,4 @@ @REM Running this script requires Administrator permissions. reg ADD HKLM\SYSTEM\CurrentControlSet\Services\hailort_service /f /v Environment /t REG_MULTI_SZ /d ^ -HAILORT_LOGGER_PATH="%PROGRAMDATA%\HailoRT_Service\logs"\0^ -HAILO_DISABLE_MULTIPLEXER=0\0 \ No newline at end of file +HAILORT_LOGGER_PATH="%PROGRAMDATA%\HailoRT_Service\logs"\0^ \ No newline at end of file diff --git a/hailort/hailortcli/CMakeLists.txt b/hailort/hailortcli/CMakeLists.txt index cd7ff1d..b5c190b 100644 --- a/hailort/hailortcli/CMakeLists.txt +++ b/hailort/hailortcli/CMakeLists.txt @@ -26,19 +26,21 @@ set(HAILORTCLI_CPP_FILES run2/run2_command.cpp run2/network_runner.cpp - run2/live_printer.cpp + run2/live_stats.cpp run2/timer_live_track.cpp run2/network_live_track.cpp run2/measurement_live_track.cpp + run2/io_wrappers.cpp ) - + if(UNIX) # Unix only modules set(HAILORTCLI_CPP_FILES ${HAILORTCLI_CPP_FILES} udp_rate_limiter_command.cpp # TODO: We dont compile download_action_list_command on windows, as it uses packed enums (HRT-5919) download_action_list_command.cpp - ) + measure_nnc_performance_command.cpp + ) endif() # 'config_definitions_json_file' is used in generate_definitions_json_str.in for configure_file() @@ -70,7 +72,7 @@ target_link_libraries(hailortcli scheduler_mon_proto) if(WIN32) - target_link_libraries(hailortcli Ws2_32 Iphlpapi Shlwapi) + target_link_libraries(hailortcli Ws2_32 Iphlpapi Shlwapi winmm.lib) elseif(CMAKE_SYSTEM_NAME STREQUAL QNX) target_link_libraries(hailortcli pevents) endif() diff --git a/hailort/hailortcli/download_action_list_command.cpp b/hailort/hailortcli/download_action_list_command.cpp index be53034..d4a4cc9 100644 --- a/hailort/hailortcli/download_action_list_command.cpp +++ b/hailort/hailortcli/download_action_list_command.cpp @@ -308,6 +308,14 @@ Expected DownloadActionListCommand::parse_action_data(uint32_t bas data_json = *reinterpret_cast(action); action_length_local = sizeof(CONTEXT_SWITCH_DEFS__enable_nms_action_t); break; + case CONTEXT_SWITCH_DEFS__ACTION_TYPE_WRITE_DATA_BY_TYPE: + data_json = *reinterpret_cast(action); + action_length_local = sizeof(CONTEXT_SWITCH_DEFS__write_data_by_type_action_t); + break; + case CONTEXT_SWITCH_DEFS__ACTION_TYPE_SWITCH_LCU_BATCH: + data_json = *reinterpret_cast(action); + action_length_local = sizeof(CONTEXT_SWITCH_DEFS__switch_lcu_batch_action_data_t); + break; case CONTEXT_SWITCH_DEFS__ACTION_TYPE_COUNT: // Fallthrough // Handling CONTEXT_SWITCH_DEFS__ACTION_TYPE_COUNT is needed because we compile this file with -Wswitch-enum @@ -622,3 +630,12 @@ void to_json(json &j, const CONTEXT_SWITCH_DEFS__open_boundary_output_channel_da { j = unpack_vdma_channel_id(data); } + +void to_json(json& j, const CONTEXT_SWITCH_DEFS__switch_lcu_batch_action_data_t& data) { + const auto cluster_index = CONTEXT_SWITCH_DEFS__PACKED_LCU_ID_CLUSTER_INDEX_READ(data.packed_lcu_id); + const auto lcu_index = CONTEXT_SWITCH_DEFS__PACKED_LCU_ID_LCU_INDEX_READ(data.packed_lcu_id); + const auto network_index = data.network_index; + const auto kernel_done_count = data.kernel_done_count; + j = json{{"cluster_index", cluster_index}, {"lcu_index", lcu_index}, {"network_index", network_index}, + {"kernel_done_count", kernel_done_count}}; +} diff --git a/hailort/hailortcli/download_action_list_command.hpp b/hailort/hailortcli/download_action_list_command.hpp index 1aa271e..4a4bb6f 100644 --- a/hailort/hailortcli/download_action_list_command.hpp +++ b/hailort/hailortcli/download_action_list_command.hpp @@ -100,6 +100,8 @@ static std::pair mapping[] = { {CONTEXT_SWITCH_DEFS__ACTION_TYPE_OPEN_BOUNDARY_INPUT_CHANNEL, "open_boundary_input_channel"}, {CONTEXT_SWITCH_DEFS__ACTION_TYPE_OPEN_BOUNDARY_OUTPUT_CHANNEL, "open_boundary_output_channel"}, {CONTEXT_SWITCH_DEFS__ACTION_TYPE_ENABLE_NMS, "enable_nms"}, + {CONTEXT_SWITCH_DEFS__ACTION_TYPE_WRITE_DATA_BY_TYPE, "write_data_by_type"}, + {CONTEXT_SWITCH_DEFS__ACTION_TYPE_SWITCH_LCU_BATCH, "switch_lcu_batch"}, }; static_assert(ARRAY_ENTRIES(mapping) == CONTEXT_SWITCH_DEFS__ACTION_TYPE_COUNT, "Missing a mapping from a CONTEXT_SWITCH_DEFS__ACTION_TYPE_t to it's string value"); @@ -112,8 +114,9 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(CONTEXT_SWITCH_DEFS__trigger_sequencer_action NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(CONTEXT_SWITCH_DEFS__sequencer_interrupt_data_t, sequencer_index); NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(CONTEXT_SWITCH_DEFS__wait_nms_data_t, aggregator_index); NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(CONTEXT_SWITCH_DEFS__module_config_done_interrupt_data_t, module_index); -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(CONTEXT_SWITCH_DEFS__fetch_ccw_bursts_action_data_t, config_stream_index); -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(CONTEXT_SWITCH_DEFS__enable_nms_action_t, nms_unit_index, network_index); +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(CONTEXT_SWITCH_DEFS__fetch_ccw_bursts_action_data_t, config_stream_index, ccw_bursts); +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(CONTEXT_SWITCH_DEFS__enable_nms_action_t, nms_unit_index, network_index, number_of_classes, burst_size); +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(CONTEXT_SWITCH_DEFS__write_data_by_type_action_t, address, data_type, data, shift, mask, network_index); // Non-default implementations void to_json(json &j, const CONTEXT_SWITCH_DEFS__deactivate_vdma_channel_action_data_t &data); @@ -138,5 +141,6 @@ void to_json(json &j, const CONTEXT_SWITCH_DEFS__deactivate_cfg_channel_t &data) void to_json(json &j, const CONTEXT_SWITCH_DEFS__add_ddr_pair_info_action_data_t &data); void to_json(json &j, const CONTEXT_SWITCH_DEFS__open_boundary_input_channel_data_t &data); void to_json(json &j, const CONTEXT_SWITCH_DEFS__open_boundary_output_channel_data_t &data); +void to_json(json &j, const CONTEXT_SWITCH_DEFS__switch_lcu_batch_action_data_t &data); #endif /* _HAILO_DOWNLOAD_ACTION_LIST_COMMAND_HPP_ */ diff --git a/hailort/hailortcli/hailortcli.cpp b/hailort/hailortcli/hailortcli.cpp index 474a747..ed246d7 100644 --- a/hailort/hailortcli/hailortcli.cpp +++ b/hailort/hailortcli/hailortcli.cpp @@ -26,6 +26,7 @@ #endif #include "parse_hef_command.hpp" #include "fw_control_command.hpp" +#include "measure_nnc_performance_command.hpp" #include "firmware_header_utils.h" #include "hailo/hailort.h" @@ -200,6 +201,7 @@ public: add_subcommand(); #if defined(__GNUC__) add_subcommand(); + add_subcommand(); #endif add_subcommand(); add_subcommand(); diff --git a/hailort/hailortcli/hailortcli.hpp b/hailort/hailortcli/hailortcli.hpp index 4ed2ab6..8e0ef96 100644 --- a/hailort/hailortcli/hailortcli.hpp +++ b/hailort/hailortcli/hailortcli.hpp @@ -46,6 +46,12 @@ void add_device_options(CLI::App *app, hailo_device_params &device_params, bool Expected>> create_devices(const hailo_device_params &device_params); Expected> get_device_ids(const hailo_device_params &device_params); + +enum class OptionVisibility { + VISIBLE, + HIDDEN +}; + /** * CLI11 transformer object, converting enum argument from string. * Use this object instead of CLI::CheckedTransformer in order @@ -55,13 +61,48 @@ template class HailoCheckedTransformer : public CLI::CheckedTransformer { public: - HailoCheckedTransformer(std::vector> values) : - CLI::CheckedTransformer(values) + + struct Enum { - desc_function_ = [values]() { - return CLI::detail::generate_map(CLI::detail::smart_deref(values), true); + std::string name; + EnumType value; + OptionVisibility visibility = OptionVisibility::VISIBLE; + + std::pair to_pair() const { return std::make_pair(name, value); } + }; + + HailoCheckedTransformer(std::vector values) : + CLI::CheckedTransformer(to_values_vector(values, true)) // Getting hidden value for the enum transformer. + { + // Hide hidden values for help and autocomplete. + const auto non_hidden_values = to_values_vector(values, false); + + desc_function_ = [non_hidden_values]() { + return CLI::detail::generate_map(CLI::detail::smart_deref(non_hidden_values), true); + }; + + autocomplete_func_ = [non_hidden_values](const std::string &) { + std::vector completions; + for (const auto &completion : non_hidden_values) { + completions.emplace_back(completion.first); + } + return completions; }; } + +private: + static std::vector> to_values_vector(const std::vector &values, + bool get_hidden) + { + std::vector> values_vector; + for (const auto &value : values) { + if (get_hidden || (value.visibility == OptionVisibility::VISIBLE)) { + values_vector.emplace_back(value.to_pair()); + } + } + return values_vector; + + } }; class DeprecationAction diff --git a/hailort/hailortcli/measure_nnc_performance_command.cpp b/hailort/hailortcli/measure_nnc_performance_command.cpp new file mode 100644 index 0000000..eefed1a --- /dev/null +++ b/hailort/hailortcli/measure_nnc_performance_command.cpp @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file measure_nnc_performance_command.cpp +* @brief measure nerual network performance for given network using only the HW components without host SW + **/ + +#include "measure_nnc_performance_command.hpp" +#include "hailortcli.hpp" + +#include "hailo/hailort.h" +#include "hailo/network_group.hpp" +#include "hailo/hef.hpp" +#include "hailo/vstream.hpp" +#include "hailo/vdevice.hpp" + +#include + +#define BYTES_TO_KILOBYTES (1024) + +HwInferEstimatorCommand::HwInferEstimatorCommand(CLI::App &parent_app) : + Command(parent_app.add_subcommand("measure-nnc-performance", + "measure nerual network performance for given network using only the HW components without host SW")), + m_params({}) +{ + // This will make the command to be hidden in the --help print in the command line. + m_app->group(""); + + add_vdevice_options(m_app, m_params.vdevice_params); + m_app->add_option("hef", m_params.hef_path, "Path of the HEF to load") + ->check(CLI::ExistingFile) + ->required(); + m_app->add_option("--batch-size", m_params.batch_size, + "Inference batch.\n" + "This batch applies to the whole network_group.") + ->check(CLI::NonNegativeNumber) + ->default_val(HAILO_DEFAULT_BATCH_SIZE); +} + +Expected> get_configure_params(const hw_infer_runner_params ¶ms, + hailort::Hef &hef, hailo_stream_interface_t interface) +{ + std::map configure_params{}; + + hailo_configure_params_t config_params{}; + hailo_status status = hailo_init_configure_params(reinterpret_cast(&hef), interface, &config_params); + CHECK_SUCCESS_AS_EXPECTED(status); + + /* For default case overwrite batch to 1 */ + uint16_t batch_size = (HAILO_DEFAULT_BATCH_SIZE == params.batch_size ? 1 : params.batch_size); + + /* Fill all network and network group structs with batch size value */ + for (size_t network_group_idx = 0; network_group_idx < config_params.network_group_params_count; network_group_idx++) { + config_params.network_group_params[network_group_idx].batch_size = batch_size; + } + + for (size_t network_group_idx = 0; network_group_idx < config_params.network_group_params_count; network_group_idx++) { + config_params.network_group_params[network_group_idx].power_mode = params.power_mode; + configure_params.emplace(std::string(config_params.network_group_params[network_group_idx].name), + ConfigureNetworkParams(config_params.network_group_params[network_group_idx])); + } + + return configure_params; +} + +hailo_status HwInferEstimatorCommand::execute() +{ + auto devices = create_devices(m_params.vdevice_params.device_params); + CHECK_EXPECTED_AS_STATUS(devices, "Failed creating device"); + /* This function supports controls for multiple devices. + We validate there is only 1 device generated as we are on a single device flow */ + CHECK(1 == devices->size(), HAILO_INTERNAL_FAILURE, "Hw infer command support only one physical device"); + auto &device = devices.value()[0]; + + auto hef = Hef::create(m_params.hef_path.c_str()); + CHECK_EXPECTED_AS_STATUS(hef, "Failed reading hef file {}", m_params.hef_path); + + auto interface = device->get_default_streams_interface(); + CHECK_EXPECTED_AS_STATUS(interface, "Failed to get default streams interface"); + + auto configure_params = get_configure_params(m_params, hef.value(), interface.value()); + CHECK_EXPECTED_AS_STATUS(configure_params); + + /* Use Env var to configure all desc list with max depth */ + setenv("HAILO_CONFIGURE_FOR_HW_INFER","Y",1); + auto network_group_list = device->configure(hef.value(), configure_params.value()); + CHECK_EXPECTED_AS_STATUS(network_group_list, "Failed configure device from hef"); + unsetenv("HAILO_CONFIGURE_FOR_HW_INFER"); + + CHECK(1 == network_group_list->size(), HAILO_INVALID_OPERATION, + "HW Inference is not supported on HEFs with multiple network groups"); + + auto network_group_ptr = network_group_list.value()[0]; + + std::cout << "Starting HW infer Estimator..." << std::endl; + + auto results = network_group_ptr->run_hw_infer_estimator(); + CHECK_EXPECTED_AS_STATUS(results); + + std::cout << std::endl; + std::cout << "======================" << std::endl; + std::cout << " Summary" << std::endl; + std::cout << "======================" << std::endl; + + std::cout << "Batch count: " << results->batch_count << std::endl; + std::cout << "Total transfer size [KB]: " << (results->total_transfer_size / BYTES_TO_KILOBYTES) << std::endl; + std::cout << "Total frames passed: " << results->total_frames_passed << std::endl; + std::cout << "Total time [s]: " << results->time_sec << std::endl; + std::cout << "Total FPS [1/s]: " << results->fps << std::endl; + std::cout << "BW [Gbps]: " << results->BW_Gbps << std::endl; + + std::cout << "======================" << std::endl; + std::cout << " End of report" << std::endl; + std::cout << "======================" << std::endl; + return HAILO_SUCCESS; +} diff --git a/hailort/hailortcli/measure_nnc_performance_command.hpp b/hailort/hailortcli/measure_nnc_performance_command.hpp new file mode 100644 index 0000000..8bcbadb --- /dev/null +++ b/hailort/hailortcli/measure_nnc_performance_command.hpp @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file measure_nnc_performance_command.hpp + * @brief measure nerual network performance for given network using only the HW components without host SW + **/ + +#ifndef _HAILO_HW_INFER_ESTIMATOR_COMMAND_HPP_ +#define _HAILO_HW_INFER_ESTIMATOR_COMMAND_HPP_ + +#include "hailortcli.hpp" +#include "command.hpp" +#include "CLI/CLI.hpp" + +struct hw_infer_runner_params { + hailo_vdevice_params vdevice_params; + std::string hef_path; + uint16_t batch_size; + hailo_power_mode_t power_mode; +}; + +class HwInferEstimatorCommand : public Command { +public: + explicit HwInferEstimatorCommand(CLI::App &parent_app); + hailo_status execute() override; + +private: + hw_infer_runner_params m_params; +}; + +#endif /*_HAILO_HW_INFER_ESTIMATOR_COMMAND_HPP_*/ \ No newline at end of file diff --git a/hailort/hailortcli/mon_command.hpp b/hailort/hailortcli/mon_command.hpp index 6fe1774..653076a 100644 --- a/hailort/hailortcli/mon_command.hpp +++ b/hailort/hailortcli/mon_command.hpp @@ -14,7 +14,7 @@ #include "hailortcli.hpp" #include "command.hpp" -#include "vdevice/scheduler/scheduler_mon.hpp" +#include "utils/profiler/monitor_handler.hpp" #include "CLI/CLI.hpp" diff --git a/hailort/hailortcli/parse_hef_command.cpp b/hailort/hailortcli/parse_hef_command.cpp index 200e7c0..51f830e 100644 --- a/hailort/hailortcli/parse_hef_command.cpp +++ b/hailort/hailortcli/parse_hef_command.cpp @@ -40,7 +40,7 @@ hailo_status ParseHefCommand::parse_hefs_info(const std::string &hef_path, bool CHECK_EXPECTED_AS_STATUS(hef_exp, "Failed to parse HEF"); auto hef = hef_exp.release(); - auto hef_info = hef.get_hef_description(stream_infos, vstream_infos); + auto hef_info = hef.get_description(stream_infos, vstream_infos); CHECK_EXPECTED_AS_STATUS(hef_info, "Failed to parse HEF"); std::cout << hef_info.release(); return HAILO_SUCCESS; diff --git a/hailort/hailortcli/run2/io_wrappers.cpp b/hailort/hailortcli/run2/io_wrappers.cpp new file mode 100644 index 0000000..3af75f0 --- /dev/null +++ b/hailort/hailortcli/run2/io_wrappers.cpp @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file io_wrappers.cpp + **/ + +#include "io_wrappers.hpp" + +FramerateThrottle::FramerateThrottle(uint32_t framerate) : + m_framerate(framerate), + m_framerate_interval(std::chrono::duration(1) / framerate), + m_last_write_time(std::chrono::steady_clock::now()) +{} + +void FramerateThrottle::throttle() +{ + if (m_framerate == UNLIMITED_FRAMERATE) { + return; + } + + const auto elapsed_time = std::chrono::steady_clock::now() - m_last_write_time; + std::this_thread::sleep_for(m_framerate_interval - elapsed_time); + m_last_write_time = std::chrono::steady_clock::now(); +} diff --git a/hailort/hailortcli/run2/io_wrappers.hpp b/hailort/hailortcli/run2/io_wrappers.hpp new file mode 100644 index 0000000..d437675 --- /dev/null +++ b/hailort/hailortcli/run2/io_wrappers.hpp @@ -0,0 +1,261 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file io_wrappers.hpp + * @brief Wrappers for Input/Output Stream/VStream. Manages buffer allocation, framerate throttle, latency meter and + * more. + **/ + +#ifndef _HAILO_IO_WRAPPERS_HPP_ +#define _HAILO_IO_WRAPPERS_HPP_ + +#include "network_live_track.hpp" + +#include "common/file_utils.hpp" +#include "common/latency_meter.hpp" + +#include +#include + +using namespace hailort; + +constexpr uint32_t UNLIMITED_FRAMERATE = 0; + +#ifndef HAILO_EMULATOR +constexpr std::chrono::milliseconds HAILORTCLI_DEFAULT_TIMEOUT(HAILO_DEFAULT_VSTREAM_TIMEOUT_MS); +#else /* ifndef HAILO_EMULATOR */ +constexpr std::chrono::milliseconds HAILORTCLI_DEFAULT_TIMEOUT(HAILO_DEFAULT_VSTREAM_TIMEOUT_MS * 100); +#endif /* ifndef HAILO_EMULATOR */ + + +class FramerateThrottle final +{ +public: + FramerateThrottle(uint32_t framerate); + ~FramerateThrottle() = default; + void throttle(); + +private: + const uint32_t m_framerate; + const std::chrono::duration m_framerate_interval; + decltype(std::chrono::steady_clock::now()) m_last_write_time; +}; + +// Wrapper for InputStream or InputVStream objects. +template +class WriterWrapper final +{ +public: + template + static Expected> create(Writer &writer, const WriterParams ¶ms, + const LatencyMeterPtr &overall_latency_meter, uint32_t framerate) + { + auto dataset = create_dataset(writer, params); + CHECK_EXPECTED(dataset); + + std::shared_ptr wrapper( + new (std::nothrow) WriterWrapper(writer, dataset.release(), overall_latency_meter, framerate)); + CHECK_NOT_NULL_AS_EXPECTED(wrapper, HAILO_OUT_OF_HOST_MEMORY); + + return wrapper; + } + + Writer &get() { return m_writer.get(); } + Writer &get() const { return m_writer.get(); } + + hailo_status write() + { + before_write_start(); + auto status = get().write(MemoryView(*next_buffer())); + if (HAILO_SUCCESS != status) { + return status; + } + + m_framerate_throttle.throttle(); + return HAILO_SUCCESS; + } + + hailo_status wait_for_async_ready() + { + return get().wait_for_async_ready(m_dataset[0]->size(), HAILORTCLI_DEFAULT_TIMEOUT); + } + + hailo_status write_async(typename Writer::TransferDoneCallback callback) + { + before_write_start(); + // We can use the same buffer for multiple writes simultaneously. That is OK since we don't modify the buffers. + auto status = get().write_async(MemoryView(*next_buffer()), callback); + if (HAILO_SUCCESS != status) { + return status; + } + + m_framerate_throttle.throttle(); + return HAILO_SUCCESS; + } + +private: + WriterWrapper(Writer &writer, std::vector &&dataset, const LatencyMeterPtr &overall_latency_meter, + uint32_t framerate) : + m_writer(std::ref(writer)), + m_dataset(std::move(dataset)), + m_overall_latency_meter(overall_latency_meter), + m_framerate_throttle(framerate) + {} + + void before_write_start() + { + if (m_overall_latency_meter) { + m_overall_latency_meter->add_start_sample(std::chrono::steady_clock::now().time_since_epoch()); + } + } + + size_t next_buffer_index() + { + const auto index = m_current_buffer_index; + m_current_buffer_index = (m_current_buffer_index + 1) % m_dataset.size(); + return index; + } + + BufferPtr next_buffer() + { + return m_dataset[next_buffer_index()]; + } + + template + static Expected> create_dataset(Writer &writer, const WriterParams ¶ms) + { + if (params.input_file_path.empty()) { + return create_constant_dataset(writer.get_frame_size()); + } else { + return create_dataset_from_input_file(params.input_file_path, writer.get_frame_size()); + } + } + + static Expected> create_constant_dataset(size_t frame_size) + { + const uint8_t const_byte = 0xAB; + auto constant_buffer = Buffer::create_shared(frame_size, const_byte, BufferStorageParams::create_dma()); + CHECK_EXPECTED(constant_buffer); + + return std::vector{constant_buffer.release()}; + } + + static Expected> create_dataset_from_input_file(const std::string &file_path, size_t frame_size) + { + auto buffer = read_binary_file(file_path); + CHECK_EXPECTED(buffer); + CHECK_AS_EXPECTED(0 == (buffer->size() % frame_size), HAILO_INVALID_ARGUMENT, + "Input file ({}) size {} must be a multiple of the frame size {}", + file_path, buffer->size(), frame_size); + + auto buffer_ptr = make_shared_nothrow(buffer.release()); + CHECK_NOT_NULL_AS_EXPECTED(buffer_ptr, HAILO_OUT_OF_HOST_MEMORY); + + std::vector dataset; + const size_t frames_count = buffer->size() / frame_size; + dataset.reserve(frames_count); + for (size_t i = 0; i < frames_count; i++) { + const auto offset = frame_size * i; + auto frame_buffer = Buffer::create_shared(buffer->data() + offset, frame_size, BufferStorageParams::create_dma()); + CHECK_EXPECTED(frame_buffer); + dataset.emplace_back(frame_buffer.release()); + } + + return dataset; + } + + std::reference_wrapper m_writer; + + std::vector m_dataset; + size_t m_current_buffer_index = 0; + + LatencyMeterPtr m_overall_latency_meter; + FramerateThrottle m_framerate_throttle; +}; + +template +using WriterWrapperPtr = std::shared_ptr>; + +// Wrapper for OutputStream or OutputVStream objects. +// We use std::enable_from_this because on async api the callback is using `this`. We want to increase the reference +// count until the callback is over. +template +class ReaderWrapper final : public std::enable_shared_from_this> +{ +public: + static Expected> create(Reader &reader, const LatencyMeterPtr &overall_latency_meter, + std::shared_ptr net_live_track) + { + auto buffer = Buffer::create_shared(reader.get_frame_size(), BufferStorageParams::create_dma()); + CHECK_EXPECTED(buffer); + + std::shared_ptr wrapper( + new (std::nothrow) ReaderWrapper(reader, buffer.release(), overall_latency_meter, net_live_track)); + CHECK_NOT_NULL_AS_EXPECTED(wrapper, HAILO_OUT_OF_HOST_MEMORY); + + return wrapper; + } + + Reader &get() { return m_reader.get(); } + Reader &get() const { return m_reader.get(); } + + hailo_status read() + { + auto status = get().read(MemoryView(*m_buffer)); + if (HAILO_SUCCESS != status) { + return status; + } + + on_read_done(); + return HAILO_SUCCESS; + } + + hailo_status wait_for_async_ready() + { + return get().wait_for_async_ready(m_buffer->size(), HAILORTCLI_DEFAULT_TIMEOUT); + } + + hailo_status read_async(typename Reader::TransferDoneCallback callback) + { + auto self = std::enable_shared_from_this>::shared_from_this(); + return get().read_async(MemoryView(*m_buffer), + [self, original=callback](const typename Reader::CompletionInfo &completion_info) { + original(completion_info); + if (completion_info.status == HAILO_SUCCESS) { + self->on_read_done(); + } + }); + } + +private: + ReaderWrapper(Reader &reader, BufferPtr &&buffer, const LatencyMeterPtr &overall_latency_meter, + std::shared_ptr net_live_track) : + m_reader(std::ref(reader)), + m_buffer(std::move(buffer)), + m_overall_latency_meter(overall_latency_meter), + m_net_live_track(net_live_track) + {} + + void on_read_done() + { + if (m_overall_latency_meter) { + m_overall_latency_meter->add_end_sample(get().name(), std::chrono::steady_clock::now().time_since_epoch()); + } + + if (m_net_live_track) { + m_net_live_track->progress(); + } + } + + std::reference_wrapper m_reader; + BufferPtr m_buffer; + LatencyMeterPtr m_overall_latency_meter; + std::shared_ptr m_net_live_track; +}; + +template +using ReaderWrapperPtr = std::shared_ptr>; + +#endif /* _HAILO_IO_WRAPPERS_HPP_ */ diff --git a/hailort/hailortcli/run2/live_printer.cpp b/hailort/hailortcli/run2/live_printer.cpp deleted file mode 100644 index 5e4e866..0000000 --- a/hailort/hailortcli/run2/live_printer.cpp +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. - * Distributed under the MIT license (https://opensource.org/licenses/MIT) - **/ -/** - * @file live_printer.cpp - * @brief Live printer - **/ - -#include "live_printer.hpp" -#include "../common.hpp" -#include "common/os_utils.hpp" -#include "common/utils.hpp" -#include -#include - -using namespace hailort; - -LivePrinter::LivePrinter(std::chrono::milliseconds interval) : - m_interval(interval), - m_stop_event(Event::create_shared(Event::State::not_signalled)), - m_tracks(), - m_mutex(), - m_prev_count(0), - m_enable_ansi_escape_sequences(CursorAdjustment()) -{ -} - -LivePrinter::~LivePrinter() -{ - (void)m_stop_event->signal(); - if (m_thread.joinable()) { - m_thread.join(); - } - print(); -} - -void LivePrinter::add(std::shared_ptr track, uint8_t level) -{ - std::unique_lock lock(m_mutex); - if (!contains(m_tracks, level)) { - m_tracks[level] = {}; - } - m_tracks[level].emplace_back(track); -} - -void LivePrinter::print() -{ - std::stringstream ss; - uint32_t count = 0; - - { - std::unique_lock lock(m_mutex); - for (auto &level_pair : m_tracks) { - for (auto &track : level_pair.second) { - count += track->get_text(ss); - } - } - } - CliCommon::reset_cursor(m_prev_count); - // On the first print m_prev_count = 0, so no lines will be deleted - std::cout << ss.str() << std::flush; - m_prev_count = count; -} - -hailo_status LivePrinter::start() -{ - for (auto &level_pair : m_tracks) { - for (auto &track : level_pair.second) { - CHECK_SUCCESS(track->start()); - } - } - - m_thread = std::thread([this] () { - OsUtils::set_current_thread_name("LIVE_PRINTER"); - while (true) { - print(); - auto status = m_stop_event->wait(m_interval); - if (HAILO_TIMEOUT != status) { - break; - } - } - }); - - return HAILO_SUCCESS; -} diff --git a/hailort/hailortcli/run2/live_stats.cpp b/hailort/hailortcli/run2/live_stats.cpp new file mode 100644 index 0000000..fe2f98c --- /dev/null +++ b/hailort/hailortcli/run2/live_stats.cpp @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file live_stats.cpp + * @brief Live stats + **/ + +#include "live_stats.hpp" +#include "../common.hpp" +#include "common/os_utils.hpp" +#include "common/utils.hpp" +#include +#include +#include + +using namespace hailort; + +hailo_status LiveStats::Track::start() +{ + CHECK_SUCCESS(start_impl()); + m_started = true; + return HAILO_SUCCESS; +} + +uint32_t LiveStats::Track::push_text(std::stringstream &ss) +{ + if (!m_started) { + return 0; + } + return push_text_impl(ss); +} + +void LiveStats::Track::push_json(nlohmann::ordered_json &json) +{ + if (!m_started) { + return; + } + push_json_impl(json); +} + +LiveStats::LiveStats(std::chrono::milliseconds interval) : + m_running(false), + m_interval(interval), + m_stop_event(Event::create_shared(Event::State::not_signalled)), + m_tracks(), + m_mutex(), + m_prev_count(0), + m_enable_ansi_escape_sequences(CursorAdjustment()) +{ +} + +LiveStats::~LiveStats() +{ + stop(); + print(); +} + +void LiveStats::add(std::shared_ptr track, uint8_t level) +{ + std::unique_lock lock(m_mutex); + m_tracks[level].emplace_back(track); +} + +void LiveStats::print() +{ + std::stringstream ss; + uint32_t count = 0; + + { + std::unique_lock lock(m_mutex); + for (auto &level_pair : m_tracks) { + for (auto &track : level_pair.second) { + count += track->push_text(ss); + } + } + } + CliCommon::reset_cursor(m_prev_count); + // On the first print m_prev_count = 0, so no lines will be deleted + std::cout << ss.str() << std::flush; + m_prev_count = count; +} + +hailo_status LiveStats::dump_stats(const std::string &json_path, const std::string &inference_mode) +{ + stop(); // stop measuring before creating json because we want the json to hold the last measurements + nlohmann::ordered_json json; + + auto time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + auto str_time = std::string(std::ctime(&time)); + if (str_time.length()){ + str_time.pop_back(); + } + + json["time"] = str_time; + json["inference_mode"] = inference_mode; + json["network_groups"] = nlohmann::ordered_json::array(); + + std::unique_lock lock(m_mutex); + for (auto &level_pair : m_tracks) { + for (auto &track : level_pair.second) { + track->push_json(json); + } + } + + std::ofstream output_json(json_path); + CHECK(output_json, HAILO_FILE_OPERATION_FAILURE, "Failed opening file '{}'", json_path); + + output_json << std::setw(4) << json << std::endl; // 4: amount of spaces to indent (for pretty printing) + CHECK(!output_json.bad() && !output_json.fail(), HAILO_FILE_OPERATION_FAILURE, + "Failed writing to file '{}'", json_path); + + return HAILO_SUCCESS; +} + +hailo_status LiveStats::start() +{ + // In order to re-start LiveStats, we should add m_stop_event->reset() here + m_running = true; + for (auto &level_pair : m_tracks) { + for (auto &track : level_pair.second) { + CHECK_SUCCESS(track->start()); + } + } + + m_thread = std::thread([this] () { + OsUtils::set_current_thread_name("LIVE_PRINTER"); + while (true) { + print(); + auto status = m_stop_event->wait(m_interval); + if (HAILO_TIMEOUT != status) { + break; + } + } + }); + return HAILO_SUCCESS; +} + +void LiveStats::stop() +{ + if (m_running){ + (void)m_stop_event->signal(); + if (m_thread.joinable()) { + m_thread.join(); + } + m_running = false; + } +} diff --git a/hailort/hailortcli/run2/live_printer.hpp b/hailort/hailortcli/run2/live_stats.hpp similarity index 55% rename from hailort/hailortcli/run2/live_printer.hpp rename to hailort/hailortcli/run2/live_stats.hpp index 26ea8ea..6c1b4ea 100644 --- a/hailort/hailortcli/run2/live_printer.hpp +++ b/hailort/hailortcli/run2/live_stats.hpp @@ -3,15 +3,16 @@ * Distributed under the MIT license (https://opensource.org/licenses/MIT) **/ /** - * @file live_printer.hpp - * @brief Live printer + * @file live_stats.hpp + * @brief Live stats **/ -#ifndef _HAILO_HAILORTCLI_RUN2_LIVE_PRINTER_HPP_ -#define _HAILO_HAILORTCLI_RUN2_LIVE_PRINTER_HPP_ +#ifndef _HAILO_HAILORTCLI_RUN2_LIVE_STATS_HPP_ +#define _HAILO_HAILORTCLI_RUN2_LIVE_STATS_HPP_ #include "common/os_utils.hpp" #include "hailo/event.hpp" +#include #include #include #include @@ -19,7 +20,7 @@ #include #include -class LivePrinter final +class LiveStats final { public: class Track @@ -28,20 +29,28 @@ public: Track() : m_started(false) {} - virtual hailo_status start() = 0; - virtual uint32_t get_text(std::stringstream &ss) = 0; + hailo_status start(); + uint32_t push_text(std::stringstream &ss); + void push_json(nlohmann::ordered_json &json); protected: + virtual hailo_status start_impl() = 0; + virtual uint32_t push_text_impl(std::stringstream &ss) = 0; + virtual void push_json_impl(nlohmann::ordered_json &json) = 0; + bool m_started; }; - LivePrinter(std::chrono::milliseconds interval); - ~LivePrinter(); + LiveStats(std::chrono::milliseconds interval); + ~LiveStats(); void add(std::shared_ptr track, uint8_t level); // prints tracks in consecutive order from low-to-high levels void print(); + hailo_status dump_stats(const std::string &json_path, const std::string &inference_mode); hailo_status start(); + void stop(); private: + bool m_running; std::chrono::milliseconds m_interval; hailort::EventPtr m_stop_event; std::map>> m_tracks; @@ -51,4 +60,4 @@ private: hailort::CursorAdjustment m_enable_ansi_escape_sequences; }; -#endif /* _HAILO_HAILORTCLI_RUN2_LIVE_PRINTER_HPP_ */ \ No newline at end of file +#endif /* _HAILO_HAILORTCLI_RUN2_LIVE_STATS_HPP_ */ \ No newline at end of file diff --git a/hailort/hailortcli/run2/measurement_live_track.cpp b/hailort/hailortcli/run2/measurement_live_track.cpp index cf001bf..f098cfe 100644 --- a/hailort/hailortcli/run2/measurement_live_track.cpp +++ b/hailort/hailortcli/run2/measurement_live_track.cpp @@ -17,7 +17,6 @@ #include #include - using namespace hailort; Expected> MeasurementLiveTrack::create_shared(Device &device, bool measure_power, bool measure_current, @@ -53,35 +52,27 @@ Expected> MeasurementLiveTrack::create_sha MeasurementLiveTrack::MeasurementLiveTrack(std::shared_ptr power_measurement, std::shared_ptr current_measurement, std::shared_ptr temp_measurement, const std::string &device_id) : - LivePrinter::Track(), m_power_measurement(std::move(power_measurement)), m_current_measurement(std::move(current_measurement)), + LiveStats::Track(), m_power_measurement(std::move(power_measurement)), m_current_measurement(std::move(current_measurement)), m_temp_measurement(std::move(temp_measurement)), m_device_id(device_id) {} -hailo_status MeasurementLiveTrack::start() +hailo_status MeasurementLiveTrack::start_impl() { if (m_power_measurement) { CHECK_SUCCESS(m_power_measurement->start_measurement()); } - if (m_current_measurement) { CHECK_SUCCESS(m_current_measurement->start_measurement()); } - if (m_temp_measurement) { CHECK_SUCCESS(m_temp_measurement->start_measurement()); } - m_started = true; - return HAILO_SUCCESS; } -uint32_t MeasurementLiveTrack::get_text(std::stringstream &ss) +uint32_t MeasurementLiveTrack::push_text_impl(std::stringstream &ss) { - if (!m_started) { - return 0; - } - auto rows_count = 0; if (m_power_measurement || m_current_measurement || m_temp_measurement) { @@ -138,4 +129,37 @@ uint32_t MeasurementLiveTrack::get_text(std::stringstream &ss) } return rows_count; +} + +void MeasurementLiveTrack::push_json_measurment_val(nlohmann::ordered_json &device_json, std::shared_ptr measurment, const std::string &measurment_name) +{ + auto measurment_info = measurment->get_data(); + auto measurement_unit = measurment->measurement_unit(); + auto min = measurment_info.min(); + auto max = measurment_info.max(); + auto mean = measurment_info.mean(); + if (min && max && mean){ + device_json[measurment_name] = { + {"min", std::to_string(min.value()) + " " + measurement_unit}, + {"max", std::to_string(max.value()) + " " + measurement_unit}, + {"average", std::to_string(mean.value()) + " " + measurement_unit} + }; + } +} + +void MeasurementLiveTrack::push_json_impl(nlohmann::ordered_json &json) +{ + nlohmann::ordered_json device_json; + device_json["device_id"] = m_device_id; + + if (m_power_measurement){ + push_json_measurment_val(device_json, m_power_measurement, "power"); + } + if (m_current_measurement){ + push_json_measurment_val(device_json, m_current_measurement, "current"); + } + if (m_temp_measurement){ + push_json_measurment_val(device_json, m_temp_measurement, "temperature"); + } + json["devices"].emplace_back(device_json); } \ No newline at end of file diff --git a/hailort/hailortcli/run2/measurement_live_track.hpp b/hailort/hailortcli/run2/measurement_live_track.hpp index 17288d9..782681e 100644 --- a/hailort/hailortcli/run2/measurement_live_track.hpp +++ b/hailort/hailortcli/run2/measurement_live_track.hpp @@ -13,24 +13,26 @@ #include "hailo/hailort.h" #include "common/device_measurements.hpp" +#include "live_stats.hpp" -#include "live_printer.hpp" +#include - -class MeasurementLiveTrack : public LivePrinter::Track +class MeasurementLiveTrack : public LiveStats::Track { public: static hailort::Expected> create_shared(hailort::Device &vdevice, bool measure_power, bool measure_current, bool measure_temp); virtual ~MeasurementLiveTrack() = default; - virtual hailo_status start() override; - virtual uint32_t get_text(std::stringstream &ss) override; + virtual hailo_status start_impl() override; + virtual uint32_t push_text_impl(std::stringstream &ss) override; + virtual void push_json_impl(nlohmann::ordered_json &json) override; MeasurementLiveTrack(std::shared_ptr power_measurement, std::shared_ptr current_measurement, std::shared_ptr temp_measurement, const std::string &device_id); private: + void push_json_measurment_val(nlohmann::ordered_json &device_json, std::shared_ptr measurment, const std::string &measurment_name); std::shared_ptr m_power_measurement; std::shared_ptr m_current_measurement; std::shared_ptr m_temp_measurement; diff --git a/hailort/hailortcli/run2/network_live_track.cpp b/hailort/hailortcli/run2/network_live_track.cpp index b4e85c2..ae59018 100644 --- a/hailort/hailortcli/run2/network_live_track.cpp +++ b/hailort/hailortcli/run2/network_live_track.cpp @@ -13,52 +13,110 @@ #include #include -NetworkLiveTrack::NetworkLiveTrack(const std::string &name, std::shared_ptr cng, LatencyMeterPtr overall_latency_meter) : - m_name(name), m_count(0), m_last_get_time(), m_cng(cng), m_overall_latency_meter(overall_latency_meter) +size_t NetworkLiveTrack::max_ng_name = 0; +std::mutex NetworkLiveTrack::mutex; + +NetworkLiveTrack::NetworkLiveTrack(const std::string &name, std::shared_ptr cng, + LatencyMeterPtr overall_latency_meter, bool measure_fps, const std::string &hef_path) : + m_name(name), + m_count(0), + m_last_get_time(), + m_cng(cng), + m_overall_latency_meter(overall_latency_meter), + m_measure_fps(measure_fps), + m_hef_path(hef_path) { + std::lock_guard lock(mutex); + max_ng_name = std::max(m_name.size(), max_ng_name); } -hailo_status NetworkLiveTrack::start() +hailo_status NetworkLiveTrack::start_impl() { m_last_get_time = std::chrono::steady_clock::now(); m_count = 0; - m_started = true; return HAILO_SUCCESS; } -uint32_t NetworkLiveTrack::get_text(std::stringstream &ss) +double NetworkLiveTrack::get_fps() { - if (!m_started) { - return 0; - } - auto elapsed_time = std::chrono::steady_clock::now() - m_last_get_time; auto count = m_count.load(); - auto fps = count / std::chrono::duration(elapsed_time).count(); - ss << fmt::format("{}:\n\t| fps: {:.2f}", m_name, fps); + return fps; +} + +uint32_t NetworkLiveTrack::push_text_impl(std::stringstream &ss) +{ + ss << fmt::format("{}:", m_name); + ss << std::string(max_ng_name - m_name.size(), ' '); + + bool first = true; + auto get_separator = [&first] () { + auto res = first ? " " : " | "; + first = false; + return res; + }; + + if (m_measure_fps) { + auto fps = get_fps(); + ss << fmt::format("{}fps: {:.2f}", get_separator(), fps); + } auto hw_latency_measurement = m_cng->get_latency_measurement(); if (hw_latency_measurement) { - ss << fmt::format(" | hw latency: {:.2f} ms", InferResultsFormatUtils::latency_result_to_ms(hw_latency_measurement->avg_hw_latency)); + ss << fmt::format("{}hw latency: {:.2f} ms", get_separator(), InferResultsFormatUtils::latency_result_to_ms(hw_latency_measurement->avg_hw_latency)); } else if (HAILO_NOT_AVAILABLE != hw_latency_measurement.status()) { // HAILO_NOT_AVAILABLE is a valid error, we ignore it - ss << fmt::format(" | hw latency: failed with status={}", hw_latency_measurement.status()); + ss << fmt::format("{}hw latency: NaN (err)", get_separator()); } if (m_overall_latency_meter) { - auto overall_latency_measurement = m_overall_latency_meter->get_latency(true); + auto overall_latency_measurement = m_overall_latency_meter->get_latency(false); if (overall_latency_measurement) { - ss << fmt::format(" | overall latency: {:.2f} ms", InferResultsFormatUtils::latency_result_to_ms(*overall_latency_measurement)); + ss << fmt::format("{}overall latency: {:.2f} ms", get_separator(), InferResultsFormatUtils::latency_result_to_ms(*overall_latency_measurement)); } else if (HAILO_NOT_AVAILABLE != overall_latency_measurement.status()) { // HAILO_NOT_AVAILABLE is a valid error, we ignore it - ss << fmt::format(" | overall latency: failed with status={}", overall_latency_measurement.status()); + ss << fmt::format("{}overall latency: NaN (err)", get_separator()); } } ss << "\n"; - return 2; + return 1; +} + +void NetworkLiveTrack::push_json_impl(nlohmann::ordered_json &json) +{ + nlohmann::ordered_json network_group_json; + network_group_json["name"] = m_name; + network_group_json["full_hef_path"] = m_hef_path; + + // TODO: HRT-8695 Support stats display per network + // auto networks_info = m_cng->get_network_infos(); + // if (networks_info){ + // network_group_json["networks"] = nlohmann::ordered_json::array(); + // for (const auto &network_info : networks_info.value()){ + // network_group_json["networks"].emplace_back(nlohmann::json::object({ {"name", network_info.name} })); + // } + // } + + if (m_measure_fps) { + auto fps = get_fps(); + network_group_json["FPS"] = std::to_string(fps); + } + + auto hw_latency_measurement = m_cng->get_latency_measurement(); + if (hw_latency_measurement){ + network_group_json["hw_latency"] = InferResultsFormatUtils::latency_result_to_ms(hw_latency_measurement->avg_hw_latency); + } + + if (m_overall_latency_meter){ + auto overall_latency_measurement = m_overall_latency_meter->get_latency(false); + if (overall_latency_measurement){ + network_group_json["overall_latency"] = InferResultsFormatUtils::latency_result_to_ms(*overall_latency_measurement); + } + } + json["network_groups"].emplace_back(network_group_json); } void NetworkLiveTrack::progress() diff --git a/hailort/hailortcli/run2/network_live_track.hpp b/hailort/hailortcli/run2/network_live_track.hpp index dec00fd..ba3138c 100644 --- a/hailort/hailortcli/run2/network_live_track.hpp +++ b/hailort/hailortcli/run2/network_live_track.hpp @@ -15,24 +15,36 @@ #include "common/latency_meter.hpp" -#include "live_printer.hpp" +#include "live_stats.hpp" +#include -class NetworkLiveTrack : public LivePrinter::Track + +class NetworkLiveTrack : public LiveStats::Track { public: - NetworkLiveTrack(const std::string &name, std::shared_ptr cng, hailort::LatencyMeterPtr overall_latency_meter); + NetworkLiveTrack(const std::string &name, std::shared_ptr cng, + hailort::LatencyMeterPtr overall_latency_meter, bool measure_fps, const std::string &hef_path); virtual ~NetworkLiveTrack() = default; - virtual hailo_status start() override; - virtual uint32_t get_text(std::stringstream &ss) override; + virtual hailo_status start_impl() override; + virtual uint32_t push_text_impl(std::stringstream &ss) override; + virtual void push_json_impl(nlohmann::ordered_json &json) override; + void progress(); private: + double get_fps(); + + static size_t max_ng_name; + static std::mutex mutex; + std::string m_name; std::atomic m_count; std::chrono::time_point m_last_get_time; std::shared_ptr m_cng; hailort::LatencyMeterPtr m_overall_latency_meter; + const bool m_measure_fps; + const std::string &m_hef_path; }; #endif /* _HAILO_HAILORTCLI_RUN2_NETWORK_LIVE_TRACK_HPP_ */ \ No newline at end of file diff --git a/hailort/hailortcli/run2/network_runner.cpp b/hailort/hailortcli/run2/network_runner.cpp index f2901a8..f095b1d 100644 --- a/hailort/hailortcli/run2/network_runner.cpp +++ b/hailort/hailortcli/run2/network_runner.cpp @@ -11,38 +11,62 @@ #include "hailo/hailort_common.hpp" #include "hailo/hailort_defaults.hpp" -#include "common/async_thread.hpp" #include "common/file_utils.hpp" #include "common/latency_meter.hpp" #include "network_runner.hpp" +#if defined(_MSC_VER) +#include +#endif using namespace hailort; +SignalEventScopeGuard::SignalEventScopeGuard(Event &event) : + m_event(event) +{} -class SignalEventScopeGuard final +SignalEventScopeGuard::~SignalEventScopeGuard() { -public: - SignalEventScopeGuard(Event &event) : m_event(event) - {} + m_event.signal(); +} - ~SignalEventScopeGuard() - { - m_event.signal(); +BarrierTerminateScopeGuard::BarrierTerminateScopeGuard(BarrierPtr barrier) : + m_barrier(barrier) +{} + +BarrierTerminateScopeGuard::~BarrierTerminateScopeGuard() +{ + if (m_barrier) { + m_barrier->terminate(); } +} - Event &m_event; +#if defined(_MSC_VER) +class TimeBeginScopeGuard final +{ +public: + TimeBeginScopeGuard() { + // default interval between timer interrupts on Windows is 15.625 ms. + // This will change it to be 1 ms, enabling us to sleep in granularity of 1 milliseconds. + // As from Windows 10 2004, in general processes are no longer affected by other processes calling timeBeginPeriod. + // https://randomascii.wordpress.com/2020/10/04/windows-timer-resolution-the-great-rule-change/ + timeBeginPeriod(1); + } + ~TimeBeginScopeGuard() { + timeEndPeriod(1); + } }; +#endif //TODO: duplicated -static hailo_status wait_for_threads(std::vector> &threads) +hailo_status NetworkRunner::wait_for_threads(std::vector> &threads) { auto last_error_status = HAILO_SUCCESS; for (auto &thread : threads) { auto thread_status = thread->get(); - if ((HAILO_SUCCESS != thread_status) && (HAILO_STREAM_ABORTED_BY_USER != thread_status)) { + if (!inference_succeeded(thread_status)) { last_error_status = thread_status; LOGGER__ERROR("Thread failed with with status {}", thread_status); } @@ -50,218 +74,192 @@ static hailo_status wait_for_threads(std::vector> & return last_error_status; } -VStreamParams::VStreamParams() : name(), params(HailoRTDefaults::get_vstreams_params()) +IoParams::IoParams() : name(), input_file_path() { } -NetworkParams::NetworkParams() : hef_path(), net_group_name(), vstream_params(), scheduling_algorithm(HAILO_SCHEDULING_ALGORITHM_ROUND_ROBIN), - batch_size(HAILO_DEFAULT_BATCH_SIZE), scheduler_threshold(0), scheduler_timeout_ms(0), framerate(UNLIMITED_FRAMERATE), measure_hw_latency(false), +VStreamParams::VStreamParams() : IoParams(), params(HailoRTDefaults::get_vstreams_params()) +{ +} + +StreamParams::StreamParams() : IoParams(), flags(HAILO_STREAM_FLAGS_NONE) +{ +} + +NetworkParams::NetworkParams() : hef_path(), net_group_name(), vstream_params(), stream_params(), + scheduling_algorithm(HAILO_SCHEDULING_ALGORITHM_ROUND_ROBIN), batch_size(HAILO_DEFAULT_BATCH_SIZE), + scheduler_threshold(0), scheduler_timeout_ms(0), framerate(UNLIMITED_FRAMERATE), measure_hw_latency(false), measure_overall_latency(false) { } NetworkRunner::NetworkRunner(const NetworkParams ¶ms, const std::string &name, - std::vector &&input_vstreams, std::vector &&output_vstreams, - std::shared_ptr cng, LatencyMeterPtr overall_latency_meter) - : m_params(params), m_name(name), m_input_vstreams(std::move(input_vstreams)), - m_output_vstreams(std::move(output_vstreams)), m_cng(cng), m_overall_latency_meter(overall_latency_meter) + VDevice &vdevice, std::shared_ptr cng) : + m_vdevice(vdevice), + m_params(params), + m_name(name), + m_cng(cng), + m_overall_latency_meter(nullptr), + m_latency_barrier(nullptr) { } Expected> NetworkRunner::create_shared(VDevice &vdevice, const NetworkParams ¶ms) { - auto hef = Hef::create(params.hef_path); + // The network params passed to the NetworkRunner may be changed by this function, hence we copy them. + auto final_net_params = params; + + auto hef = Hef::create(final_net_params.hef_path); CHECK_EXPECTED(hef); // Get NG's name if single - auto net_group_name = params.net_group_name; + auto net_group_name = final_net_params.net_group_name; if (net_group_name.empty()) { auto net_groups_names = hef->get_network_groups_names(); - CHECK_AS_EXPECTED(net_groups_names.size() == 1, HAILO_INVALID_ARGUMENT, "HEF {} doesn't contain a single NetworkGroup. Pass --name", params.hef_path); + CHECK_AS_EXPECTED(net_groups_names.size() == 1, HAILO_INVALID_ARGUMENT, "HEF {} doesn't contain a single NetworkGroup. Pass --name", final_net_params.hef_path); net_group_name = net_groups_names[0]; } auto cfg_params = vdevice.create_configure_params(hef.value(), net_group_name); CHECK_EXPECTED(cfg_params); - cfg_params->batch_size = params.batch_size; - if (params.measure_hw_latency) { + cfg_params->batch_size = final_net_params.batch_size; + if (final_net_params.batch_size == HAILO_DEFAULT_BATCH_SIZE) { + // Changing batch_size to 1. If HAILO_DEFAULT_BATCH_SIZE is configured, the sched will send one frame per batch + final_net_params.batch_size = 1; + } + if (final_net_params.measure_hw_latency) { cfg_params->latency |= HAILO_LATENCY_MEASURE; } + if (final_net_params.is_async()) { + for (auto &stream_name_params_pair : cfg_params->stream_params_by_name) { + stream_name_params_pair.second.flags = HAILO_STREAM_FLAGS_ASYNC; + } + } auto cfgr_net_groups = vdevice.configure(hef.value(), {{net_group_name, cfg_params.value()}}); CHECK_EXPECTED(cfgr_net_groups); assert(1 == cfgr_net_groups->size()); auto cfgr_net_group = cfgr_net_groups.value()[0]; - if (HAILO_SCHEDULING_ALGORITHM_NONE!= params.scheduling_algorithm) { - CHECK_SUCCESS_AS_EXPECTED(cfgr_net_group->set_scheduler_threshold(params.scheduler_threshold)); - CHECK_SUCCESS_AS_EXPECTED(cfgr_net_group->set_scheduler_timeout(std::chrono::milliseconds(params.scheduler_timeout_ms))); - CHECK_SUCCESS_AS_EXPECTED(cfgr_net_group->set_scheduler_priority(params.scheduler_priority)); + if (HAILO_SCHEDULING_ALGORITHM_NONE!= final_net_params.scheduling_algorithm) { + CHECK_SUCCESS_AS_EXPECTED(cfgr_net_group->set_scheduler_threshold(final_net_params.scheduler_threshold)); + CHECK_SUCCESS_AS_EXPECTED(cfgr_net_group->set_scheduler_timeout(std::chrono::milliseconds(final_net_params.scheduler_timeout_ms))); + CHECK_SUCCESS_AS_EXPECTED(cfgr_net_group->set_scheduler_priority(final_net_params.scheduler_priority)); } - std::map vstreams_params; - for (auto &vstream_params : params.vstream_params) { - vstreams_params.emplace(vstream_params.name, vstream_params.params); - } - auto vstreams = create_vstreams(*cfgr_net_group, vstreams_params); - CHECK_EXPECTED(vstreams); - - LatencyMeterPtr overall_latency_meter = nullptr; - if (params.measure_overall_latency) { - CHECK_AS_EXPECTED((1 == vstreams->first.size()), HAILO_INVALID_OPERATION, - "Overall latency measurement over multiple inputs network is not supported"); - - std::set output_names; - for (auto &output_vstream : vstreams->second) { - output_names.insert(output_vstream.name()); + std::shared_ptr net_runner_ptr = nullptr; + switch (final_net_params.mode) + { + case InferenceMode::FULL: + { + std::map vstreams_params; + for (auto &vstream_params : final_net_params.vstream_params) { + vstreams_params.emplace(vstream_params.name, vstream_params.params); } - - overall_latency_meter = make_shared_nothrow(output_names, OVERALL_LATENCY_TIMESTAMPS_LIST_LENGTH); - CHECK_NOT_NULL_AS_EXPECTED(overall_latency_meter, HAILO_OUT_OF_HOST_MEMORY); + auto vstreams = create_vstreams(*cfgr_net_group, vstreams_params); + CHECK_EXPECTED(vstreams); + + auto net_runner = make_shared_nothrow(final_net_params, net_group_name, vdevice, + std::move(vstreams->first), std::move(vstreams->second), cfgr_net_group); + CHECK_NOT_NULL_AS_EXPECTED(net_runner, HAILO_OUT_OF_HOST_MEMORY); + net_runner_ptr = std::static_pointer_cast(net_runner); + break; } - auto net_runner = make_shared_nothrow(params, net_group_name, std::move(vstreams->first), - std::move(vstreams->second), cfgr_net_group, overall_latency_meter); - CHECK_NOT_NULL_AS_EXPECTED(net_runner, HAILO_OUT_OF_HOST_MEMORY); - return net_runner; -} -Expected NetworkRunner::create_dataset_from_input_file(const std::string &file_path, - const InputVStream &input_vstream) -{ - auto buffer = read_binary_file(file_path); - CHECK_EXPECTED(buffer); - CHECK_AS_EXPECTED(0 == (buffer->size() % input_vstream.get_frame_size()), HAILO_INVALID_ARGUMENT, - "Input file ({}) size {} must be a multiple of the frame size {} ({})", - file_path, buffer->size(), input_vstream.get_frame_size(), input_vstream.name()); + case InferenceMode::RAW: // Fallthrough + case InferenceMode::RAW_ASYNC: // Fallthrough + case InferenceMode::RAW_ASYNC_SINGLE_THREAD: + { + auto input_streams = cfgr_net_group->get_input_streams(); + CHECK_AS_EXPECTED(input_streams.size() > 0, HAILO_INTERNAL_FAILURE); - auto buffer_ptr = make_shared_nothrow(buffer.release()); - CHECK_NOT_NULL_AS_EXPECTED(buffer_ptr, HAILO_OUT_OF_HOST_MEMORY); + auto output_streams = cfgr_net_group->get_output_streams(); + CHECK_AS_EXPECTED(output_streams.size() > 0, HAILO_INTERNAL_FAILURE); - return buffer_ptr; -} + auto net_runner = make_shared_nothrow(final_net_params, net_group_name, vdevice, + std::move(input_streams), std::move(output_streams), cfgr_net_group); + CHECK_NOT_NULL_AS_EXPECTED(net_runner, HAILO_OUT_OF_HOST_MEMORY); + net_runner_ptr = std::static_pointer_cast(net_runner); + break; + } + default: + // Shouldn't get here + return make_unexpected(HAILO_INTERNAL_FAILURE); + } -Expected NetworkRunner::create_constant_dataset(const InputVStream &input_vstream) -{ - const uint8_t const_byte = 0xAB; - auto constant_buffer = Buffer::create_shared(input_vstream.get_frame_size(), const_byte); - CHECK_EXPECTED(constant_buffer); + if (final_net_params.measure_overall_latency || final_net_params.measure_hw_latency) { + auto input_names = net_runner_ptr->get_input_names(); + auto output_names = net_runner_ptr->get_output_names(); - return constant_buffer.release(); -} + CHECK_AS_EXPECTED((1 == input_names.size()), HAILO_INVALID_OPERATION, + "Latency measurement over multiple inputs network is not supported"); -hailo_status NetworkRunner::run_input_vstream(InputVStream &vstream, Event &shutdown_event, BufferPtr dataset, - LatencyMeterPtr overall_latency_meter) -{ - auto signal_event_scope_guard = SignalEventScopeGuard(shutdown_event); - - auto last_write_time = std::chrono::steady_clock::now(); - auto framerate_interval = std::chrono::duration(1) / m_params.framerate; - size_t buffer_offset = 0; - while(true) { - if (overall_latency_meter) { - overall_latency_meter->add_start_sample(std::chrono::steady_clock::now().time_since_epoch()); - } - auto status = vstream.write(MemoryView((dataset->data() + buffer_offset), vstream.get_frame_size())); - if (status == HAILO_STREAM_ABORTED_BY_USER) { - return status; - } - CHECK_SUCCESS(status); - buffer_offset += vstream.get_frame_size(); - buffer_offset %= dataset->size(); - - if (m_params.framerate != UNLIMITED_FRAMERATE) { - auto elapsed_time = std::chrono::steady_clock::now() - last_write_time; - std::this_thread::sleep_for(framerate_interval - elapsed_time); - last_write_time = std::chrono::steady_clock::now(); + if (final_net_params.measure_overall_latency) { + auto overall_latency_meter = make_shared_nothrow(output_names, OVERALL_LATENCY_TIMESTAMPS_LIST_LENGTH); + CHECK_NOT_NULL_AS_EXPECTED(overall_latency_meter, HAILO_OUT_OF_HOST_MEMORY); + net_runner_ptr->set_overall_latency_meter(overall_latency_meter); } + + // We use a barrier for both hw and overall latency + auto latency_barrier = make_shared_nothrow(input_names.size() + output_names.size()); + CHECK_NOT_NULL_AS_EXPECTED(latency_barrier, HAILO_OUT_OF_HOST_MEMORY); + net_runner_ptr->set_latency_barrier(latency_barrier); } - return HAILO_SUCCESS; + + return net_runner_ptr; } -hailo_status NetworkRunner::run_output_vstream(OutputVStream &vstream, bool first, std::shared_ptr net_live_track, - Event &shutdown_event, LatencyMeterPtr overall_latency_meter) +bool NetworkRunner::inference_succeeded(hailo_status status) { - auto signal_event_scope_guard = SignalEventScopeGuard(shutdown_event); - - auto result = Buffer::create(vstream.get_frame_size()); - CHECK_EXPECTED_AS_STATUS(result); - while(true) { - auto status = vstream.read(MemoryView(result.value())); - if (status == HAILO_STREAM_ABORTED_BY_USER) { - return status; - } - CHECK_SUCCESS(status); - if (overall_latency_meter) { - overall_latency_meter->add_end_sample(vstream.name(), std::chrono::steady_clock::now().time_since_epoch()); - } - if (first) { - net_live_track->progress(); - } - } - return HAILO_SUCCESS; + const auto status_find_result = std::find(NetworkRunner::ALLOWED_INFERENCE_RETURN_VALUES.cbegin(), + NetworkRunner::ALLOWED_INFERENCE_RETURN_VALUES.cend(), status); + // If the status is in the allowed list, the inference has succeeded + return status_find_result != NetworkRunner::ALLOWED_INFERENCE_RETURN_VALUES.cend(); } -hailo_status NetworkRunner::run(Event &shutdown_event, LivePrinter &live_printer, Barrier &barrier) +hailo_status NetworkRunner::run(EventPtr shutdown_event, LiveStats &live_stats, Barrier &activation_barrier) { auto ang = std::unique_ptr(nullptr); if (HAILO_SCHEDULING_ALGORITHM_NONE == m_params.scheduling_algorithm) { auto ang_exp = m_cng->activate(); if (!ang_exp) { - barrier.terminate(); + activation_barrier.terminate(); } CHECK_EXPECTED_AS_STATUS(ang_exp); ang = ang_exp.release(); } - auto net_live_track = std::make_shared(m_name, m_cng, m_overall_latency_meter); - live_printer.add(net_live_track, 1); //support progress over multiple outputs - barrier.arrive_and_wait(); + // If we measure latency (hw or overall) we send frames one at a time. Hence we don't measure fps. + const auto measure_fps = !m_params.measure_hw_latency && !m_params.measure_overall_latency; + auto net_live_track = std::make_shared(m_name, m_cng, m_overall_latency_meter, measure_fps, m_params.hef_path); + live_stats.add(net_live_track, 1); //support progress over multiple outputs - std::vector> threads; - for (auto &input_vstream : m_input_vstreams) { - BufferPtr dataset = nullptr; - for (auto ¶ms : m_params.vstream_params) { - if ((input_vstream.name() == params.name) && (!params.input_file_path.empty())) { - auto dataset_exp = create_dataset_from_input_file(params.input_file_path, input_vstream); - CHECK_EXPECTED_AS_STATUS(dataset_exp); - dataset = dataset_exp.release(); - } - } - if (nullptr == dataset) { - auto dataset_exp = create_constant_dataset(input_vstream); - CHECK_EXPECTED_AS_STATUS(dataset_exp); - dataset = dataset_exp.release(); - } +#if defined(_MSC_VER) + TimeBeginScopeGuard time_begin_scope_guard; +#endif - threads.emplace_back(std::make_unique>("SEND", [this, &input_vstream, &shutdown_event, - dataset](){ - return run_input_vstream(input_vstream, shutdown_event, dataset, m_overall_latency_meter); - })); - } + activation_barrier.arrive_and_wait(); - bool first = true; //TODO: check with multiple outputs - for (auto &output_vstream : m_output_vstreams) { - threads.emplace_back(std::make_unique>("RECV", [this, &output_vstream, first, net_live_track, - &shutdown_event](){ - return run_output_vstream(output_vstream, first, net_live_track, shutdown_event, m_overall_latency_meter); - })); - first = false; + if (m_params.mode == InferenceMode::RAW_ASYNC_SINGLE_THREAD) { + return run_single_thread_async_infer(shutdown_event, net_live_track); + } else { + auto threads = start_inference_threads(shutdown_event, net_live_track); + CHECK_EXPECTED_AS_STATUS(threads); + + CHECK_SUCCESS(shutdown_event->wait(HAILO_INFINITE_TIMEOUT)); + stop(); + return wait_for_threads(threads.value()); } +} - //TODO: return threads and move stop outside? - CHECK_SUCCESS(shutdown_event.wait(HAILO_INFINITE_TIMEOUT)); - stop(); - return wait_for_threads(threads); +void NetworkRunner::set_overall_latency_meter(LatencyMeterPtr latency_meter) +{ + m_overall_latency_meter = latency_meter; } -void NetworkRunner::stop() +void NetworkRunner::set_latency_barrier(BarrierPtr latency_barrier) { - for (auto &input_vstream : m_input_vstreams) { - (void) input_vstream.abort(); - } - for (auto &output_vstream : m_output_vstreams) { - (void) output_vstream.abort(); - } + m_latency_barrier = latency_barrier; } Expected, std::vector>> NetworkRunner::create_vstreams( @@ -277,8 +275,7 @@ Expected, std::vector>> Netwo if (elem_it != params.end()) { input_vstreams_params.emplace(input_vstream_info.name, elem_it->second); match_count++; - } - else { + } else { input_vstreams_params.emplace(input_vstream_info.name, HailoRTDefaults::get_vstreams_params()); } } @@ -306,4 +303,276 @@ Expected, std::vector>> Netwo CHECK_EXPECTED(output_vstreams); return {{input_vstreams.release(), output_vstreams.release()}};//TODO: move? copy elision? +} + +const std::vector NetworkRunner::ALLOWED_INFERENCE_RETURN_VALUES{ + {HAILO_SUCCESS, HAILO_STREAM_ABORTED_BY_USER, HAILO_SHUTDOWN_EVENT_SIGNALED} +}; + +FullNetworkRunner::FullNetworkRunner(const NetworkParams ¶ms, const std::string &name, VDevice &vdevice, + std::vector &&input_vstreams, std::vector &&output_vstreams, + std::shared_ptr cng) : + NetworkRunner(params, name, vdevice, cng), + m_input_vstreams(std::move(input_vstreams)), + m_output_vstreams(std::move(output_vstreams)) +{ +} + +Expected>> FullNetworkRunner::start_inference_threads(EventPtr shutdown_event, + std::shared_ptr net_live_track) +{ + std::vector> threads; + for (auto &input_vstream : m_input_vstreams) { + const auto vstream_params = get_params(input_vstream.name()); + auto writer = WriterWrapper::create(input_vstream, vstream_params, m_overall_latency_meter, + m_params.framerate); + CHECK_EXPECTED(writer); + + threads.emplace_back(std::make_unique>("WRITE", + [this, writer = writer.release(), shutdown_event]() mutable { + return run_write(writer, shutdown_event, m_latency_barrier); + })); + } + + bool first = true; //TODO: check with multiple outputs + for (auto &output_vstream : m_output_vstreams) { + auto reader = ReaderWrapper::create(output_vstream, m_overall_latency_meter, + first ? net_live_track : nullptr); + CHECK_EXPECTED(reader); + + threads.emplace_back(std::make_unique>("READ", + [this, reader=reader.release(), shutdown_event]() mutable { + return run_read(reader, shutdown_event, m_latency_barrier); + })); + first = false; + } + + return threads; +} + +void FullNetworkRunner::stop() +{ + for (auto &input_vstream : m_input_vstreams) { + (void) input_vstream.abort(); + } + for (auto &output_vstream : m_output_vstreams) { + (void) output_vstream.abort(); + } +} + +std::set FullNetworkRunner::get_input_names() +{ + std::set result; + + for (const auto &vstream : m_input_vstreams) { + result.insert(vstream.name()); + } + + return result; +} + +std::set FullNetworkRunner::get_output_names() +{ + std::set result; + + for (const auto &vstream : m_output_vstreams) { + result.insert(vstream.name()); + } + + return result; +} + +VStreamParams FullNetworkRunner::get_params(const std::string &name) +{ + for (const auto ¶ms : m_params.vstream_params) { + if (name == params.name) { + return params; + } + } + return VStreamParams(); +} + +RawNetworkRunner::RawNetworkRunner(const NetworkParams ¶ms, const std::string &name, VDevice &vdevice, + InputStreamRefVector &&input_streams, OutputStreamRefVector &&output_streams, + std::shared_ptr cng) : + NetworkRunner(params, name, vdevice, cng), + m_input_streams(std::move(input_streams)), + m_output_streams(std::move(output_streams)) +{ +} + +Expected>> RawNetworkRunner::start_inference_threads(EventPtr shutdown_event, + std::shared_ptr net_live_track) +{ + const bool async_streams = (m_params.is_async()); + std::vector> threads; + for (auto &input_stream : m_input_streams) { + const auto stream_params = get_params(input_stream.get().name()); + auto writer = WriterWrapper::create(input_stream.get(), stream_params, m_overall_latency_meter, + m_params.framerate); + CHECK_EXPECTED(writer); + + if (async_streams) { + threads.emplace_back(std::make_unique>("WRITE_ASYNC", + [this, writer = writer.release(), shutdown_event]() mutable { + return run_write_async(writer, shutdown_event, m_latency_barrier); + })); + } else { + threads.emplace_back(std::make_unique>("WRITE", + [this, writer = writer.release(), shutdown_event]() mutable { + return run_write(writer, shutdown_event, m_latency_barrier); + })); + } + } + + bool first = true; //TODO: check with multiple outputs + for (auto &output_stream : m_output_streams) { + auto reader = ReaderWrapper::create(output_stream.get(), m_overall_latency_meter, + first ? net_live_track : nullptr); + CHECK_EXPECTED(reader); + + if (async_streams) { + threads.emplace_back(std::make_unique>("READ_ASYNC", + [this, reader=reader.release(), shutdown_event]() mutable { + return run_read_async(reader, shutdown_event, m_latency_barrier); + })); + } else { + threads.emplace_back(std::make_unique>("READ", + [this, reader=reader.release(), shutdown_event]() mutable { + return run_read(reader, shutdown_event, m_latency_barrier); + })); + } + first = false; + } + + return threads; +} + +hailo_status RawNetworkRunner::run_single_thread_async_infer(EventPtr shutdown_event, + std::shared_ptr net_live_track) +{ + // Build output wrappers + std::vector> reader_wrappers; + std::vector output_semaphores; + bool is_first_output = true; + for (auto &output_stream : m_output_streams) { + auto reader_wrapper = ReaderWrapper::create(output_stream.get(), m_overall_latency_meter, + is_first_output ? net_live_track : nullptr); + CHECK_EXPECTED_AS_STATUS(reader_wrapper); + is_first_output = false; + + auto max_queue_size = reader_wrapper.value()->get().get_async_max_queue_size(); + CHECK_EXPECTED_AS_STATUS(max_queue_size); + + auto semaphore = Semaphore::create_shared(static_cast(*max_queue_size)); + CHECK_NOT_NULL(semaphore, HAILO_OUT_OF_HOST_MEMORY); + + output_semaphores.emplace_back(semaphore); + reader_wrappers.emplace_back(reader_wrapper.release()); + } + + // Build input wrappers + std::vector> writer_wrappers; + std::vector input_semaphores; + for (auto &input_stream : m_input_streams) { + auto writer_wrapper = WriterWrapper::create(input_stream.get(), + get_params(input_stream.get().name()), m_overall_latency_meter, m_params.framerate); + CHECK_EXPECTED_AS_STATUS(writer_wrapper); + + auto max_queue_size = writer_wrapper.value()->get().get_async_max_queue_size(); + CHECK_EXPECTED_AS_STATUS(max_queue_size); + + auto semaphore = Semaphore::create_shared(static_cast(*max_queue_size)); + CHECK_NOT_NULL(semaphore, HAILO_OUT_OF_HOST_MEMORY); + + input_semaphores.emplace_back(semaphore); + writer_wrappers.emplace_back(writer_wrapper.release()); + } + + // Build waitables list with reference to previous input/output semaphores. + // We put output semaphores before inputs because we want to always have place to write + // the data into. It also makes sure that the framerate throttle will work properly. + const size_t shutdown_index = 0; + const size_t output_index_start = shutdown_index + 1; + const size_t input_index_start = output_index_start + output_semaphores.size(); + + std::vector> waitables; + waitables.emplace_back(std::ref(*shutdown_event)); + auto add_to_waitables = [&waitables](const SemaphorePtr &sem) { waitables.emplace_back(std::ref(*sem)); }; + std::for_each(output_semaphores.begin(), output_semaphores.end(), add_to_waitables); + std::for_each(input_semaphores.begin(), input_semaphores.end(), add_to_waitables); + WaitableGroup wait_group(std::move(waitables)); + + // Inference + while (true) { + auto wait_index = wait_group.wait_any(HAILORTCLI_DEFAULT_TIMEOUT); + CHECK_EXPECTED_AS_STATUS(wait_index); + + if (*wait_index == shutdown_index) { + // Stopping the network so we won't get timeout on the flush. The async operations may still be active + // (until network deactivation). + stop(); + break; + } else if ((*wait_index >= output_index_start) && (*wait_index < input_index_start)) { + // output is ready + const size_t output_index = *wait_index - output_index_start; + auto status = reader_wrappers[output_index]->read_async( + [semaphore=output_semaphores[output_index]](const OutputStream::CompletionInfo &) { + (void)semaphore->signal(); + } + ); + CHECK_SUCCESS(status); + } else { + // input is ready + const size_t input_index = *wait_index - input_index_start; + auto status = writer_wrappers[input_index]->write_async( + [semaphore=input_semaphores[input_index]](const InputStream::CompletionInfo &) { + (void)semaphore->signal(); + } + ); + CHECK_SUCCESS(status); + } + } + + return HAILO_SUCCESS; +} + +void RawNetworkRunner::stop() +{ + for (auto &input_stream : m_input_streams) { + (void) input_stream.get().abort(); + } + for (auto &output_stream : m_output_streams) { + (void) output_stream.get().abort(); + } +} + +std::set RawNetworkRunner::get_input_names() +{ + std::set result; + for (const auto &stream : m_input_streams) { + result.insert(stream.get().name()); + } + + return result; +} + +std::set RawNetworkRunner::get_output_names() +{ + std::set result; + for (const auto &stream : m_output_streams) { + result.insert(stream.get().name()); + } + + return result; +} + +StreamParams RawNetworkRunner::get_params(const std::string &name) +{ + for (const auto ¶ms : m_params.stream_params) { + if (name == params.name) { + return params; + } + } + return StreamParams(); } \ No newline at end of file diff --git a/hailort/hailortcli/run2/network_runner.hpp b/hailort/hailortcli/run2/network_runner.hpp index dda0651..5eafec0 100644 --- a/hailort/hailortcli/run2/network_runner.hpp +++ b/hailort/hailortcli/run2/network_runner.hpp @@ -10,7 +10,15 @@ #ifndef _HAILO_HAILORTCLI_RUN2_NETWORK_RUNNER_HPP_ #define _HAILO_HAILORTCLI_RUN2_NETWORK_RUNNER_HPP_ +#include "io_wrappers.hpp" +#include "live_stats.hpp" +#include "network_live_track.hpp" + +#include "../hailortcli.hpp" + #include "common/barrier.hpp" +#include "common/async_thread.hpp" +#include "common/event_internal.hpp" #include "hailo/vdevice.hpp" #include "hailo/vstream.hpp" @@ -19,25 +27,44 @@ #include "hailo/expected.hpp" #include "hailo/buffer.hpp" -#include "../hailortcli.hpp" - -#include "live_printer.hpp" -#include "network_live_track.hpp" - #include #include -constexpr uint32_t UNLIMITED_FRAMERATE = 0; +using namespace hailort; + +constexpr std::chrono::milliseconds SYNC_EVENT_TIMEOUT(1000); + -struct VStreamParams +enum class InferenceMode { + FULL, + + RAW, + RAW_ASYNC, + RAW_ASYNC_SINGLE_THREAD, +}; + +struct IoParams { - VStreamParams(); + IoParams(); std::string name; - hailo_vstream_params_t params; std::string input_file_path; }; +struct VStreamParams : public IoParams +{ + VStreamParams(); + + hailo_vstream_params_t params; +}; + +struct StreamParams : public IoParams +{ + StreamParams(); + + hailo_stream_flags_t flags; +}; + struct NetworkParams { NetworkParams(); @@ -45,6 +72,7 @@ struct NetworkParams std::string hef_path; std::string net_group_name; std::vector vstream_params; + std::vector stream_params; hailo_scheduling_algorithm_t scheduling_algorithm; // Network parameters @@ -58,35 +86,274 @@ struct NetworkParams bool measure_hw_latency; bool measure_overall_latency; + InferenceMode mode; + + bool is_async() const + { + return (mode == InferenceMode::RAW_ASYNC) || (mode == InferenceMode::RAW_ASYNC_SINGLE_THREAD); + } +}; + +class SignalEventScopeGuard final +{ +public: + SignalEventScopeGuard(Event &event); + ~SignalEventScopeGuard(); + +private: + Event &m_event; +}; + +class BarrierTerminateScopeGuard final +{ +public: + BarrierTerminateScopeGuard(BarrierPtr barrier); + ~BarrierTerminateScopeGuard(); + +private: + BarrierPtr m_barrier; }; class NetworkRunner { public: + static Expected> create_shared(VDevice &vdevice, const NetworkParams ¶ms); + NetworkRunner(const NetworkParams ¶ms, const std::string &name, - std::vector &&input_vstreams, std::vector &&output_vstreams, - std::shared_ptr cng, hailort::LatencyMeterPtr overall_latency_meter); - static hailort::Expected> create_shared(hailort::VDevice &vdevice, const NetworkParams ¶ms); - hailo_status run(hailort::Event &shutdown_event, LivePrinter &live_printer, hailort::Barrier &barrier); - void stop(); + VDevice &vdevice, std::shared_ptr cng); + virtual ~NetworkRunner() = default; -private: - static hailort::Expected, std::vector>> create_vstreams( - hailort::ConfiguredNetworkGroup &net_group, const std::map ¶ms); - hailo_status run_input_vstream(hailort::InputVStream &vstream, hailort::Event &shutdown_event, hailort::BufferPtr dataset, - hailort::LatencyMeterPtr overall_latency_meter); - static hailo_status run_output_vstream(hailort::OutputVStream &vstream, bool first, std::shared_ptr net_live_track, - hailort::Event &shutdown_event, hailort::LatencyMeterPtr overall_latency_meter); + hailo_status run(EventPtr shutdown_event, LiveStats &live_stats, Barrier &activation_barrier); + virtual void stop() = 0; + // Must be called prior to run + void set_overall_latency_meter(LatencyMeterPtr latency_meter); + void set_latency_barrier(BarrierPtr latency_barrier); -static hailort::Expected create_constant_dataset(const hailort::InputVStream &input_vstream); -static hailort::Expected create_dataset_from_input_file(const std::string &file_path, const hailort::InputVStream &input_vstream); +protected: + static bool inference_succeeded(hailo_status status); + // Use 'inference_succeeded(async_thread->get())' to check for a thread's success + virtual Expected>> start_inference_threads(EventPtr shutdown_event, + std::shared_ptr net_live_track) = 0; + virtual hailo_status run_single_thread_async_infer(EventPtr shutdown_event, + std::shared_ptr net_live_track) = 0; - const NetworkParams &m_params;//TODO: copy instead of ref? + virtual std::set get_input_names() = 0; + virtual std::set get_output_names() = 0; + + static Expected, std::vector>> create_vstreams( + ConfiguredNetworkGroup &net_group, const std::map ¶ms); + + template + hailo_status run_write(WriterWrapperPtr writer, EventPtr shutdown_event, + std::shared_ptr latency_barrier) + { + auto latency_barrier_scope_guard = BarrierTerminateScopeGuard(latency_barrier); + auto signal_event_scope_guard = SignalEventScopeGuard(*shutdown_event); + + while (true) { + if (latency_barrier) { + latency_barrier->arrive_and_wait(); + } + + for (auto i = 0; i < m_params.batch_size; i++) { + auto status = writer->write(); + if (status == HAILO_STREAM_ABORTED_BY_USER) { + return status; + } + CHECK_SUCCESS(status); + } + } + return HAILO_SUCCESS; + } + + template + hailo_status run_write_async(WriterWrapperPtr writer, EventPtr shutdown_event, + std::shared_ptr latency_barrier) + { + auto latency_barrier_scope_guard = BarrierTerminateScopeGuard(latency_barrier); + auto signal_event_scope_guard = SignalEventScopeGuard(*shutdown_event); + + // When measuring latency we want to send one frame at a time (to avoid back-pressure) + // sync_event will be used to send one frame at a time + EventPtr sync_event = nullptr; + if (m_params.measure_hw_latency || m_params.measure_overall_latency) { + sync_event = Event::create_shared(Event::State::not_signalled); + CHECK_NOT_NULL(sync_event, HAILO_OUT_OF_HOST_MEMORY); + } + + while (true) { + if (latency_barrier) { + latency_barrier->arrive_and_wait(); + } + + for (auto i = 0; i < m_params.batch_size; i++) { + auto status = writer->wait_for_async_ready(); + if (status == HAILO_STREAM_ABORTED_BY_USER) { + return status; + } + CHECK_SUCCESS(status); + + status = writer->write_async( + [sync_event](const typename Writer::CompletionInfo &) { + if (sync_event) { + (void)sync_event->signal(); + } + }); + if (status == HAILO_STREAM_ABORTED_BY_USER) { + return status; + } + CHECK_SUCCESS(status); + + if (m_params.measure_hw_latency || m_params.measure_overall_latency) { + status = WaitOrShutdown(sync_event, shutdown_event).wait(SYNC_EVENT_TIMEOUT); + if (HAILO_SHUTDOWN_EVENT_SIGNALED == status) { + // Don't print an error for this + return status; + } + CHECK_SUCCESS(status); + status = sync_event->reset(); + CHECK_SUCCESS(status); + } + } + } + return HAILO_SUCCESS; + } + + template + hailo_status run_read(ReaderWrapperPtr reader, EventPtr shutdown_event, + std::shared_ptr latency_barrier) + { + auto latency_barrier_scope_guard = BarrierTerminateScopeGuard(latency_barrier); + auto signal_event_scope_guard = SignalEventScopeGuard(*shutdown_event); + + while (true) { + if (latency_barrier) { + latency_barrier->arrive_and_wait(); + } + + for (auto i = 0; i < m_params.batch_size; i++) { + auto status = reader->read(); + if (status == HAILO_STREAM_ABORTED_BY_USER) { + return status; + } + CHECK_SUCCESS(status); + } + } + return HAILO_SUCCESS; + } + + template + hailo_status run_read_async(ReaderWrapperPtr reader, EventPtr shutdown_event, + std::shared_ptr latency_barrier) + { + auto latency_barrier_scope_guard = BarrierTerminateScopeGuard(latency_barrier); + auto signal_event_scope_guard = SignalEventScopeGuard(*shutdown_event); + + // When measuring latency we want to send one frame at a time (to avoid back-pressure) + // sync_event will be used to send one frame at a time + EventPtr sync_event = nullptr; + if (m_params.measure_hw_latency || m_params.measure_overall_latency) { + sync_event = Event::create_shared(Event::State::not_signalled); + CHECK_NOT_NULL(sync_event, HAILO_OUT_OF_HOST_MEMORY); + } + + while (true) { + if (latency_barrier) { + latency_barrier->arrive_and_wait(); + } + + for (auto i = 0; i < m_params.batch_size; i++) { + auto status = reader->wait_for_async_ready(); + if (status == HAILO_STREAM_ABORTED_BY_USER) { + return status; + } + CHECK_SUCCESS(status); + + status = reader->read_async( + [sync_event](const typename Reader::CompletionInfo &) { + if (sync_event) { + (void)sync_event->signal(); + } + }); + if (status == HAILO_STREAM_ABORTED_BY_USER) { + return status; + } + CHECK_SUCCESS(status); + + if (m_params.measure_hw_latency || m_params.measure_overall_latency) { + status = WaitOrShutdown(sync_event, shutdown_event).wait(SYNC_EVENT_TIMEOUT); + if (HAILO_SHUTDOWN_EVENT_SIGNALED == status) { + // Don't print an error for this + return status; + } + CHECK_SUCCESS(status); + status = sync_event->reset(); + CHECK_SUCCESS(status); + } + } + } + return HAILO_SUCCESS; + } + + VDevice &m_vdevice; + const NetworkParams m_params; std::string m_name; - std::vector m_input_vstreams; - std::vector m_output_vstreams; - std::shared_ptr m_cng; - hailort::LatencyMeterPtr m_overall_latency_meter; + std::shared_ptr m_cng; + LatencyMeterPtr m_overall_latency_meter; + BarrierPtr m_latency_barrier; + +private: + static const std::vector ALLOWED_INFERENCE_RETURN_VALUES; + static hailo_status wait_for_threads(std::vector> &threads); + static Expected create_constant_dataset(size_t size); + static Expected create_dataset_from_input_file(const std::string &file_path, size_t size); +}; + +class FullNetworkRunner : public NetworkRunner +{ +public: + FullNetworkRunner(const NetworkParams ¶ms, const std::string &name, VDevice &vdevice, + std::vector &&input_vstreams, std::vector &&output_vstreams, + std::shared_ptr cng); + + virtual Expected>> start_inference_threads(EventPtr shutdown_event, + std::shared_ptr net_live_track) override; + virtual hailo_status run_single_thread_async_infer(EventPtr, std::shared_ptr) override + { + return HAILO_NOT_IMPLEMENTED; + }; + + virtual void stop() override; + virtual std::set get_input_names() override; + virtual std::set get_output_names() override; + VStreamParams get_params(const std::string &name); + +private: + std::vector m_input_vstreams; + std::vector m_output_vstreams; +}; + +class RawNetworkRunner : public NetworkRunner +{ +public: + RawNetworkRunner(const NetworkParams ¶ms, const std::string &name, VDevice &vdevice, + InputStreamRefVector &&input_streams, OutputStreamRefVector &&output_streams, + std::shared_ptr cng); + + virtual Expected>> start_inference_threads(EventPtr shutdown_event, + std::shared_ptr net_live_track) override; + + virtual hailo_status run_single_thread_async_infer(EventPtr shutdown_event, + std::shared_ptr net_live_track) override; + + virtual void stop() override; + virtual std::set get_input_names() override; + virtual std::set get_output_names() override; + StreamParams get_params(const std::string &name); + +private: + InputStreamRefVector m_input_streams; + OutputStreamRefVector m_output_streams; }; #endif /* _HAILO_HAILORTCLI_RUN2_NETWORK_RUNNER_HPP_ */ \ No newline at end of file diff --git a/hailort/hailortcli/run2/run2_command.cpp b/hailort/hailortcli/run2/run2_command.cpp index e4c1070..3d8cf98 100644 --- a/hailort/hailortcli/run2/run2_command.cpp +++ b/hailort/hailortcli/run2/run2_command.cpp @@ -8,13 +8,14 @@ **/ #include "run2_command.hpp" -#include "live_printer.hpp" +#include "live_stats.hpp" #include "timer_live_track.hpp" #include "measurement_live_track.hpp" #include "network_runner.hpp" #include "common/barrier.hpp" #include "common/async_thread.hpp" +#include "../common.hpp" #include "hailo/vdevice.hpp" #include "hailo/hef.hpp" @@ -73,32 +74,102 @@ std::vector VStreamNameValidator::get_values(const std::string &hef return names; } +class StreamNameValidator : public CLI::Validator { + public: + StreamNameValidator(const CLI::Option *hef_path_option, const CLI::Option *net_group_name_option); +private: + static std::vector get_values(const std::string &hef_path, const std::string &net_group_name); +}; + +StreamNameValidator::StreamNameValidator(const CLI::Option *hef_path_option, const CLI::Option *net_group_name_option) : Validator("STREAM") { + func_ = [](std::string&) { + //TODO: support? + return std::string(); + }; + autocomplete_func_ = [hef_path_option, net_group_name_option](const std::string&) { + // TODO: remove existing names from prev user input + return get_values(hef_path_option->as(), net_group_name_option->as()); + }; +} + +std::vector StreamNameValidator::get_values(const std::string &hef_path, const std::string &net_group_name) +{ + auto hef = Hef::create(hef_path); + if (!hef.has_value()) { + return {}; + } + + // TODO: duplicate + auto actual_net_group_name = net_group_name; + if (actual_net_group_name.empty()) { + auto net_groups_names = hef->get_network_groups_names(); + if (net_groups_names.size() != 1) { + return {}; + } + actual_net_group_name = net_groups_names[0]; + } + + auto streams_info = hef->get_all_stream_infos(actual_net_group_name); + if (!streams_info.has_value()) { + return {}; + } + + std::vector names; + for (auto &stream_info : streams_info.value()) { + names.emplace_back(stream_info.name); + } + return names; +} + +IoApp::IoApp(const std::string &description, const std::string &name, Type type) : + CLI::App(description, name), + m_type(type), + m_vstream_params(), + m_stream_params() +{ +} + +IoApp::Type IoApp::get_type() const +{ + return m_type; +} + +const VStreamParams &IoApp::get_vstream_params() const +{ + // TODO: instead of copy do a move + call reset()? change func name to move_params? same for NetworkParams/NetworkApp class + return m_vstream_params; +} + +const StreamParams &IoApp::get_stream_params() const +{ + // TODO: instead of copy do a move + call reset()? change func name to move_params? same for NetworkParams/NetworkApp class + return m_stream_params; +} + /** VStreamApp */ -class VStreamApp : public CLI::App +class VStreamApp : public IoApp { public: VStreamApp(const std::string &description, const std::string &name, CLI::Option *hef_path_option, CLI::Option *net_group_name_option); - const VStreamParams& get_params(); private: CLI::Option* add_flag_callback(CLI::App *app, const std::string &name, const std::string &description, std::function function); - - VStreamParams m_params; }; VStreamApp::VStreamApp(const std::string &description, const std::string &name, CLI::Option *hef_path_option, - CLI::Option *net_group_name_option) : CLI::App(description, name), m_params() + CLI::Option *net_group_name_option) : + IoApp(description, name, IoApp::Type::VSTREAM) { - add_option("name", m_params.name, "vStream name") + add_option("name", m_vstream_params.name, "vStream name") ->check(VStreamNameValidator(hef_path_option, net_group_name_option)); - add_option("--input-file", m_params.input_file_path, + add_option("--input-file", m_vstream_params.input_file_path, "Input file path. If not given, random data will be used. File format should be raw binary data with size that is a factor of the input shape size") ->default_val(""); auto format_opt_group = add_option_group("Format"); - format_opt_group->add_option("--type", m_params.params.user_buffer_format.type, "Format type") + format_opt_group->add_option("--type", m_vstream_params.params.user_buffer_format.type, "Format type") ->transform(HailoCheckedTransformer({ { "auto", HAILO_FORMAT_TYPE_AUTO }, { "uint8", HAILO_FORMAT_TYPE_UINT8 }, @@ -107,7 +178,7 @@ VStreamApp::VStreamApp(const std::string &description, const std::string &name, })) ->default_val("auto"); - format_opt_group->add_option("--order", m_params.params.user_buffer_format.order, "Format order") + format_opt_group->add_option("--order", m_vstream_params.params.user_buffer_format.order, "Format order") ->transform(HailoCheckedTransformer({ { "auto", HAILO_FORMAT_ORDER_AUTO }, { "nhwc", HAILO_FORMAT_ORDER_NHWC }, @@ -130,27 +201,50 @@ VStreamApp::VStreamApp(const std::string &description, const std::string &name, add_flag_callback(format_opt_group, "-q,--quantized,!--no-quantized", "Whether or not data is quantized", [this](bool result){ - m_params.params.user_buffer_format.flags = result ? - static_cast(m_params.params.user_buffer_format.flags | HAILO_FORMAT_FLAGS_QUANTIZED) : - static_cast(m_params.params.user_buffer_format.flags & (~HAILO_FORMAT_FLAGS_QUANTIZED));}) + m_vstream_params.params.user_buffer_format.flags = result ? + static_cast(m_vstream_params.params.user_buffer_format.flags | HAILO_FORMAT_FLAGS_QUANTIZED) : + static_cast(m_vstream_params.params.user_buffer_format.flags & (~HAILO_FORMAT_FLAGS_QUANTIZED));}) ->run_callback_for_default() ->default_val(true); // default_val() must be after run_callback_for_default() } -const VStreamParams& VStreamApp::get_params() +CLI::Option* VStreamApp::add_flag_callback(CLI::App *app, const std::string &name, const std::string &description, + std::function function) { - //TODO: instead of copy do a move + call reset()? change func name to move_params? same for NetworkParams/NetworkApp class - return m_params; + // get_option doesn't support multiple names so taking the first one + auto first_name = name.substr(0, name.find(',')); + auto wrap_function = [app, function, first_name](std::int64_t){function(app->get_option(first_name)->as());}; + return app->add_flag_function(name, wrap_function, description); } -CLI::Option* VStreamApp::add_flag_callback(CLI::App *app, const std::string &name, const std::string &description, - std::function function) - { - // get_option doesn't support multiple names so taking the first one - auto first_name = name.substr(0, name.find(',')); - auto wrap_function = [app, function, first_name](std::int64_t){function(app->get_option(first_name)->as());}; - return app->add_flag_function(name, wrap_function, description); - } +/** StreamApp */ +class StreamApp : public IoApp +{ +public: + StreamApp(const std::string &description, const std::string &name, CLI::Option *hef_path_option, CLI::Option *net_group_name_option); +}; + +StreamApp::StreamApp(const std::string &description, const std::string &name, CLI::Option *hef_path_option, + CLI::Option *net_group_name_option) : + IoApp(description, name, IoApp::Type::STREAM) +{ + add_option("name", m_stream_params.name, "Stream name") + ->check(StreamNameValidator(hef_path_option, net_group_name_option)); + + add_option("--input-file", m_stream_params.input_file_path, + "Input file path. If not given, random data will be used. File format should be raw binary data with size that is a factor of the input shape size") + ->default_val(""); + + // TODO: async option (HRT-9580) + // TODO: flag callback? + // add_flag_callback(format_opt_group, "-q,--quantized,!--no-quantized", "Whether or not data is quantized", + // [this](bool result){ + // m_params.params.user_buffer_format.flags = result ? + // static_cast(m_params.params.user_buffer_format.flags | HAILO_FORMAT_FLAGS_QUANTIZED) : + // static_cast(m_params.params.user_buffer_format.flags & (~HAILO_FORMAT_FLAGS_QUANTIZED));}) + // ->run_callback_for_default() + // ->default_val(true); // default_val() must be after run_callback_for_default() +} /** NetworkGroupNameValidator */ class NetworkGroupNameValidator : public CLI::Validator { @@ -173,18 +267,9 @@ NetworkGroupNameValidator::NetworkGroupNameValidator(const CLI::Option *hef_path } /** NetworkApp */ -class NetworkApp : public CLI::App -{ -public: - NetworkApp(const std::string &description, const std::string &name); - const NetworkParams& get_params(); - -private: - void add_vstream_app_subcom(CLI::Option *hef_path_option, CLI::Option *net_group_name_option); - NetworkParams m_params; -}; - -NetworkApp::NetworkApp(const std::string &description, const std::string &name) : CLI::App(description, name), m_params() +NetworkApp::NetworkApp(const std::string &description, const std::string &name) : + CLI::App(description, name), + m_params() { auto hef_path_option = add_option("hef", m_params.hef_path, "HEF file path")->check(CLI::ExistingFile); auto net_group_name_option = add_option("--name", m_params.net_group_name, "Network group name") @@ -204,34 +289,11 @@ NetworkApp::NetworkApp(const std::string &description, const std::string &name) // TODO: support multiple scheduling algorithms m_params.scheduling_algorithm = HAILO_SCHEDULING_ALGORITHM_ROUND_ROBIN; - add_vstream_app_subcom(hef_path_option, net_group_name_option); -} - -void NetworkApp::add_vstream_app_subcom(CLI::Option *hef_path_option, CLI::Option *net_group_name_option) -{ - auto vstream_app = std::make_shared("Set vStream", "set-vstream", hef_path_option, net_group_name_option); - vstream_app->immediate_callback(); - vstream_app->callback([this, vstream_app, hef_path_option, net_group_name_option]() { - m_params.vstream_params.push_back(vstream_app->get_params()); - - // Throw an error if anything is left over and should not be. - _process_extras(); - - // NOTE: calling "net_app->clear(); m_params = NetworkParams();" is not sufficient because default values - // need to be re-set. we can override clear and reset them but there might be other issues as well - // and this one feels less hacky ATM - remove_subcommand(vstream_app.get()); - // Remove from parsed_subcommands_ as well (probably a bug in CLI11) - parsed_subcommands_.erase(std::remove_if( - parsed_subcommands_.begin(), parsed_subcommands_.end(), - [vstream_app](auto x){return x == vstream_app.get();}), - parsed_subcommands_.end()); - add_vstream_app_subcom(hef_path_option, net_group_name_option); - }); - - // Must set fallthrough to support nested repeated subcommands. - vstream_app->fallthrough(); - add_subcommand(vstream_app); + auto vstream_subcommand = add_io_app_subcom("Set vStream", "set-vstream", hef_path_option, net_group_name_option); + auto stream_subcommand = add_io_app_subcom("Set Stream", "set-stream", hef_path_option, net_group_name_option); + // TODO: doesn't seam to be working (HRT-9886) + vstream_subcommand->excludes(stream_subcommand); + stream_subcommand->excludes(vstream_subcommand); } const NetworkParams& NetworkApp::get_params() @@ -252,16 +314,23 @@ public: bool get_measure_power(); bool get_measure_current(); bool get_measure_temp(); + bool get_measure_hw_latency(); + bool get_measure_overall_latency(); bool get_multi_process_service(); const std::string &get_group_id(); + InferenceMode get_mode() const; + const std::string &get_output_json_path(); void set_scheduling_algorithm(hailo_scheduling_algorithm_t scheduling_algorithm); + void set_inference_mode(); void set_measure_latency(); private: void add_net_app_subcom(); std::vector m_network_params; uint32_t m_time_to_run; + InferenceMode m_mode; + std::string m_stats_json_path; std::vector m_device_id; uint32_t m_device_count; bool m_multi_process_service; @@ -282,6 +351,17 @@ Run2::Run2() : CLI::App("Run networks (preview)", "run2") add_option("-t,--time-to-run", m_time_to_run, "Time to run (seconds)") ->default_val(DEFAULT_TIME_TO_RUN_SECONDS) ->check(CLI::PositiveNumber); + add_option("-m,--mode", m_mode, "Inference mode") + ->transform(HailoCheckedTransformer({ + { "full", InferenceMode::FULL }, + { "raw", InferenceMode::RAW }, + { "raw_async", InferenceMode::RAW_ASYNC }, + { "raw_async_single_thread", InferenceMode::RAW_ASYNC_SINGLE_THREAD, OptionVisibility::HIDDEN } + }))->default_val("full"); + static const char *JSON_SUFFIX = ".json"; + add_option("-j,--json", m_stats_json_path, "If set save statistics as json to the specified path") + ->default_val("") + ->check(FileSuffixValidator(JSON_SUFFIX)); auto vdevice_options_group = add_option_group("VDevice Options"); @@ -303,13 +383,13 @@ Run2::Run2() : CLI::App("Run networks (preview)", "run2") auto measure_power_opt = measurement_options_group->add_flag("--measure-power", m_measure_power, "Measure power consumption") ->default_val(false); - + measurement_options_group->add_flag("--measure-current", m_measure_current, "Measure current")->excludes(measure_power_opt) ->default_val(false); - measurement_options_group->add_flag("--measure-latency", m_measure_hw_latency, "Measure network latency") + measurement_options_group->add_flag("--measure-latency", m_measure_hw_latency, "Measure network latency on the NN core") ->default_val(false); - + measurement_options_group->add_flag("--measure-overall-latency", m_measure_overall_latency, "Measure overall latency measurement") ->default_val(false); @@ -341,6 +421,7 @@ void Run2::add_net_app_subcom() // NOTE: fallthrough() is not a must here but it is also not working (causing only a single vstream param // instead of >1). Debug - App.hpp::void _parse(std::vector &args) add_subcommand(net_app); + // TODO: set _autocomplete based on m_mode (HRT-9886) } const std::vector& Run2::get_network_params() @@ -368,6 +449,16 @@ bool Run2::get_measure_temp() return m_measure_temp; } +bool Run2::get_measure_hw_latency() +{ + return m_measure_hw_latency; +} + +bool Run2::get_measure_overall_latency() +{ + return m_measure_overall_latency; +} + std::vector Run2::get_dev_ids() { std::vector res; @@ -386,6 +477,13 @@ uint32_t Run2::get_device_count() return m_device_count; } +void Run2::set_inference_mode() +{ + for (auto ¶ms : m_network_params) { + params.mode = m_mode; + } +} + void Run2::set_scheduling_algorithm(hailo_scheduling_algorithm_t scheduling_algorithm) { for (auto ¶ms: m_network_params) { @@ -395,7 +493,7 @@ void Run2::set_scheduling_algorithm(hailo_scheduling_algorithm_t scheduling_algo void Run2::set_measure_latency() { - for (auto ¶ms: m_network_params) { + for (auto ¶ms : m_network_params) { params.measure_hw_latency = m_measure_hw_latency; params.measure_overall_latency = m_measure_overall_latency; } @@ -411,6 +509,15 @@ const std::string &Run2::get_group_id() return m_group_id; } +InferenceMode Run2::get_mode() const +{ + return m_mode; +} + +const std::string &Run2::get_output_json_path() +{ + return m_stats_json_path; +} /** Run2Command */ Run2Command::Run2Command(CLI::App &parent_app) : Command(parent_app.add_subcommand(std::make_shared())) @@ -437,10 +544,27 @@ bool is_valid_ip(const std::string &ip) IS_FIT_IN_UINT8(a) && IS_FIT_IN_UINT8(b) && IS_FIT_IN_UINT8(c) && IS_FIT_IN_UINT8(d); } +std::string get_str_infer_mode(const InferenceMode& infer_mode) +{ + switch(infer_mode){ + case InferenceMode::FULL: + return "full"; + case InferenceMode::RAW: + return "raw"; + case InferenceMode::RAW_ASYNC: + return "raw_async"; + case InferenceMode::RAW_ASYNC_SINGLE_THREAD: + return "raw_async_single_thread"; + } + + return ""; +} + hailo_status Run2Command::execute() { Run2 *app = reinterpret_cast(m_app); + app->set_inference_mode(); app->set_measure_latency(); if (0 == app->get_network_params().size()) { @@ -450,8 +574,12 @@ hailo_status Run2Command::execute() if (1 == app->get_network_params().size()) { LOGGER__WARN("\"hailortcli run2\" is in preview. It is recommended to use \"hailortcli run\" command for a single network group"); } + if (app->get_measure_hw_latency() || app->get_measure_overall_latency()) { + CHECK(1 == app->get_network_params().size(), HAILO_INVALID_OPERATION, "When latency measurement is enabled, only one model is allowed"); + LOGGER__WARN("Measuring latency; frames are sent one at a time and FPS will not be measured"); + } - hailo_vdevice_params_t vdevice_params = {}; + hailo_vdevice_params_t vdevice_params{}; CHECK_SUCCESS(hailo_init_vdevice_params(&vdevice_params)); auto dev_ids = app->get_dev_ids(); if (!dev_ids.empty()) { @@ -467,6 +595,12 @@ hailo_status Run2Command::execute() } else { vdevice_params.device_count = app->get_device_count(); } + // TODO: Async stream support for scheduler (HRT-9878) + if ((app->get_mode() == InferenceMode::RAW_ASYNC) || (app->get_mode() == InferenceMode::RAW_ASYNC_SINGLE_THREAD)) { + vdevice_params.scheduling_algorithm = HAILO_SCHEDULING_ALGORITHM_NONE; + CHECK(1 == app->get_network_params().size(), HAILO_INVALID_OPERATION, "Only one model is allowed with aw async inference mode"); + app->set_scheduling_algorithm(HAILO_SCHEDULING_ALGORITHM_NONE); + } vdevice_params.group_id = app->get_group_id().c_str(); vdevice_params.multi_process_service = app->get_multi_process_service(); @@ -482,40 +616,51 @@ hailo_status Run2Command::execute() net_runners.emplace_back(net_runner.release()); } - auto live_printer = std::make_unique(std::chrono::seconds(1)); - live_printer->add(std::make_shared(app->get_time_to_run()), 0); + auto live_stats = std::make_unique(std::chrono::seconds(1)); + + live_stats->add(std::make_shared(app->get_time_to_run()), 0); + + auto shutdown_event = Event::create_shared(Event::State::not_signalled); + CHECK_NOT_NULL(shutdown_event, HAILO_OUT_OF_HOST_MEMORY); - auto shutdown_event = Event::create(Event::State::not_signalled); - CHECK_EXPECTED_AS_STATUS(shutdown_event); std::vector> threads; - Barrier barrier(net_runners.size() + 1); // We wait for all nets to finish activation + this thread to start sampling + Barrier activation_barrier(net_runners.size() + 1); // We wait for all nets to finish activation + this thread to start sampling for (auto &net_runner : net_runners) { threads.emplace_back(std::make_unique>("NG_INFER", [&net_runner, &shutdown_event, - &live_printer, &barrier](){ - return net_runner->run(shutdown_event.value(), *live_printer, barrier); + &live_stats, &activation_barrier](){ + return net_runner->run(shutdown_event, *live_stats, activation_barrier); })); } + auto signal_event_scope_guard = SignalEventScopeGuard(*shutdown_event); + auto physical_devices = vdevice.value()->get_physical_devices(); CHECK_EXPECTED_AS_STATUS(physical_devices); for (auto &device : physical_devices.value()) { auto measurement_live_track = MeasurementLiveTrack::create_shared(device.get(), app->get_measure_power(), app->get_measure_current(), app->get_measure_temp()); + if (HAILO_SUCCESS != measurement_live_track.status()) { + activation_barrier.terminate(); + } CHECK_EXPECTED_AS_STATUS(measurement_live_track); - live_printer->add(measurement_live_track.release(), 2); + + live_stats->add(measurement_live_track.release(), 2); } // TODO: wait for all nets before starting timer. start() should update TimerLiveTrack to start. or maybe append here but first in vector... - barrier.arrive_and_wait(); - CHECK_SUCCESS(live_printer->start()); + activation_barrier.arrive_and_wait(); + CHECK_SUCCESS(live_stats->start()); auto status = shutdown_event->wait(app->get_time_to_run()); if (HAILO_TIMEOUT != status) { // if shutdown_event is signaled its because one of the send/recv threads failed LOGGER__ERROR("Encountered error during inference. See log for more information."); } - live_printer.reset(); // Ensures that the final print will include real values and not with values of when streams are already aborted. + if (!app->get_output_json_path().empty()){ + live_stats->dump_stats(app->get_output_json_path(), get_str_infer_mode(app->get_mode())); + } + live_stats.reset(); // Ensures that the final print will include real values and not with values of when streams are already aborted. shutdown_event->signal(); return wait_for_threads(threads); } \ No newline at end of file diff --git a/hailort/hailortcli/run2/run2_command.hpp b/hailort/hailortcli/run2/run2_command.hpp index e569deb..015fe8c 100644 --- a/hailort/hailortcli/run2/run2_command.hpp +++ b/hailort/hailortcli/run2/run2_command.hpp @@ -11,6 +11,10 @@ #define _HAILO_HAILORTCLI_RUN2_RUN2_COMMAND_HPP_ #include "../command.hpp" +#include "network_runner.hpp" + +#include + class Run2Command : public Command { public: @@ -20,4 +24,71 @@ public: private: }; -#endif /* _HAILO_HAILORTCLI_RUN2_RUN2_COMMAND_HPP_ */ \ No newline at end of file +class IoApp : public CLI::App +{ +public: + enum class Type { + STREAM, + VSTREAM + }; + + IoApp(const std::string &description, const std::string &name, Type type); + Type get_type() const; + const VStreamParams& get_vstream_params() const; + const StreamParams& get_stream_params() const; + +protected: + Type m_type; + VStreamParams m_vstream_params; + StreamParams m_stream_params; +}; + +class NetworkApp : public CLI::App +{ +public: + NetworkApp(const std::string &description, const std::string &name); + const NetworkParams& get_params(); + +private: + template + CLI::App *add_io_app_subcom(const std::string &description, const std::string &name, + CLI::Option *hef_path_option, CLI::Option *net_group_name_option) + { + static_assert(std::is_base_of::value, "T is not a subclass of IoApp"); + + auto io_app = std::make_shared(description, name, hef_path_option, net_group_name_option); + io_app->immediate_callback(); + io_app->callback([this, description, name, io_app, hef_path_option, net_group_name_option]() { + if (io_app->get_type() == IoApp::Type::VSTREAM) { + auto vstream_params = io_app->get_vstream_params(); + m_params.vstream_params.push_back(vstream_params); + } else { + auto stream_params = io_app->get_stream_params(); + m_params.stream_params.push_back(stream_params); + } + + // Throw an error if anything is left over and should not be. + _process_extras(); + + // NOTE: calling "net_app->clear(); m_params = NetworkParams();" is not sufficient because default values + // need to be re-set. we can override clear and reset them but there might be other issues as well + // and this one feels less hacky ATM + remove_subcommand(io_app.get()); + // Remove from parsed_subcommands_ as well (probably a bug in CLI11) + parsed_subcommands_.erase(std::remove_if( + parsed_subcommands_.begin(), parsed_subcommands_.end(), + [io_app](auto x){return x == io_app.get();}), + parsed_subcommands_.end()); + add_io_app_subcom(description, name, hef_path_option, net_group_name_option); + }); + + // Must set fallthrough to support nested repeated subcommands. + io_app->fallthrough(); + return add_subcommand(io_app); + } + + NetworkParams m_params; +}; + + +#endif /* _HAILO_HAILORTCLI_RUN2_RUN2_COMMAND_HPP_ */ diff --git a/hailort/hailortcli/run2/timer_live_track.cpp b/hailort/hailortcli/run2/timer_live_track.cpp index 05fd73d..3367082 100644 --- a/hailort/hailortcli/run2/timer_live_track.cpp +++ b/hailort/hailortcli/run2/timer_live_track.cpp @@ -13,23 +13,18 @@ #include TimerLiveTrack::TimerLiveTrack(std::chrono::milliseconds duration) : - LivePrinter::Track(), m_duration(duration), m_start_time() + LiveStats::Track(), m_duration(duration), m_start_time() { } -hailo_status TimerLiveTrack::start() +hailo_status TimerLiveTrack::start_impl() { m_start_time = std::chrono::steady_clock::now(); - m_started = true; - return HAILO_SUCCESS; } -uint32_t TimerLiveTrack::get_text(std::stringstream &ss) +uint32_t TimerLiveTrack::push_text_impl(std::stringstream &ss) { - if (!m_started) { - return 0; - } static const uint32_t MAX_PROGRESS_BAR_WIDTH = 20; auto elapsed_time = std::chrono::steady_clock::now() - m_start_time; auto eta = std::chrono::seconds(std::max(0, static_cast(std::round(std::chrono::duration(m_duration - elapsed_time).count())))); // std::chrono::round is from C++17 @@ -39,4 +34,11 @@ uint32_t TimerLiveTrack::get_text(std::stringstream &ss) ss << fmt::format("[{:=>{}}{:{}}] {:>3}% {}\n", '>', progress_bar_width, "", MAX_PROGRESS_BAR_WIDTH - progress_bar_width, elapsed_percentage, CliCommon::duration_to_string(eta)); return 1; +} + +void TimerLiveTrack::push_json_impl(nlohmann::ordered_json &json) +{ + std::stringstream time_to_run; + time_to_run << std::fixed << std::setprecision(2) << std::round(std::chrono::duration(m_duration).count()) << " seconds"; + json["time_to_run"] = time_to_run.str(); } \ No newline at end of file diff --git a/hailort/hailortcli/run2/timer_live_track.hpp b/hailort/hailortcli/run2/timer_live_track.hpp index af6e7e7..836b692 100644 --- a/hailort/hailortcli/run2/timer_live_track.hpp +++ b/hailort/hailortcli/run2/timer_live_track.hpp @@ -7,18 +7,19 @@ * @brief Timer live track **/ -#include "live_printer.hpp" +#include "live_stats.hpp" #ifndef _HAILO_HAILORTCLI_RUN2_TIMER_LIVE_TRACK_HPP_ #define _HAILO_HAILORTCLI_RUN2_TIMER_LIVE_TRACK_HPP_ -class TimerLiveTrack : public LivePrinter::Track +class TimerLiveTrack : public LiveStats::Track { public: TimerLiveTrack(std::chrono::milliseconds duration); virtual ~TimerLiveTrack() = default; - virtual hailo_status start() override; - virtual uint32_t get_text(std::stringstream &ss) override; + virtual hailo_status start_impl() override; + virtual uint32_t push_text_impl(std::stringstream &ss) override; + virtual void push_json_impl(nlohmann::ordered_json &json) override; private: std::chrono::milliseconds m_duration; diff --git a/hailort/hailortcli/run_command.hpp b/hailort/hailortcli/run_command.hpp index e00199e..502911d 100644 --- a/hailort/hailortcli/run_command.hpp +++ b/hailort/hailortcli/run_command.hpp @@ -133,7 +133,7 @@ public: }; desc_function_ = []() { - return "\t\tInput file path/paths. On single input network, give the full path of the data file.\n\ + return "\t\tInput file (.bin) path/paths. On single input network, give the full path of the data file.\n\ \t\tOn multiple inputs network, the format is input_name1=path1 input_name2=path2, where\n\ \t\tinput_name1 is the name of the input stream. If not given, random data will be used"; }; diff --git a/hailort/libhailort/CMakeLists.txt b/hailort/libhailort/CMakeLists.txt index 3993b61..504348b 100644 --- a/hailort/libhailort/CMakeLists.txt +++ b/hailort/libhailort/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.0.0) # set(CMAKE_C_CLANG_TIDY "clang-tidy;-checks=*") set(HAILORT_MAJOR_VERSION 4) -set(HAILORT_MINOR_VERSION 13) +set(HAILORT_MINOR_VERSION 14) set(HAILORT_REVISION_VERSION 0) # Add the cmake folder so the modules there are found diff --git a/hailort/libhailort/bindings/gstreamer/CMakeLists.txt b/hailort/libhailort/bindings/gstreamer/CMakeLists.txt index e0c06c9..dde4203 100644 --- a/hailort/libhailort/bindings/gstreamer/CMakeLists.txt +++ b/hailort/libhailort/bindings/gstreamer/CMakeLists.txt @@ -8,7 +8,7 @@ if(NOT CMAKE_HOST_UNIX) message(FATAL_ERROR "Only unix hosts are supported, stopping build") endif() -find_package(HailoRT 4.13.0 EXACT REQUIRED) +find_package(HailoRT 4.14.0 EXACT REQUIRED) # GST_PLUGIN_DEFINE needs PACKAGE to be defined set(GST_HAILO_PACKAGE_NAME "hailo") @@ -36,6 +36,12 @@ set_property(TARGET gsthailo PROPERTY CXX_STANDARD 14) set_target_properties(gsthailo PROPERTIES PUBLIC_HEADER "gst-hailo/metadata/tensor_meta.hpp" + CXX_STANDARD 14 + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO + C_VISIBILITY_PRESET hidden + CXX_VISIBILITY_PRESET hidden + # VISIBILITY_INLINES_HIDDEN YES ) target_compile_options(gsthailo PRIVATE diff --git a/hailort/libhailort/bindings/gstreamer/gst-hailo/common.hpp b/hailort/libhailort/bindings/gstreamer/gst-hailo/common.hpp index 73d126e..ee8d5a4 100644 --- a/hailort/libhailort/bindings/gstreamer/gst-hailo/common.hpp +++ b/hailort/libhailort/bindings/gstreamer/gst-hailo/common.hpp @@ -48,7 +48,7 @@ using namespace hailort; #define DEFAULT_VDEVICE_KEY (0) #define MIN_VALID_VDEVICE_KEY (1) -#define HAILO_SUPPORTED_FORMATS "{ RGB, RGBA, YUY2, NV12, NV21, I420 }" +#define HAILO_SUPPORTED_FORMATS "{ RGB, RGBA, YUY2, NV12, NV21, I420, GRAY8 }" #define HAILO_VIDEO_CAPS GST_VIDEO_CAPS_MAKE(HAILO_SUPPORTED_FORMATS) #define HAILO_DEFAULT_SCHEDULER_TIMEOUT_MS (0) diff --git a/hailort/libhailort/bindings/gstreamer/gst-hailo/gsthailonet.cpp b/hailort/libhailort/bindings/gstreamer/gst-hailo/gsthailonet.cpp index 7b4f755..77ce6bd 100644 --- a/hailort/libhailort/bindings/gstreamer/gst-hailo/gsthailonet.cpp +++ b/hailort/libhailort/bindings/gstreamer/gst-hailo/gsthailonet.cpp @@ -102,6 +102,7 @@ enum PROP_SCHEDULING_ALGORITHM, PROP_SCHEDULER_TIMEOUT_MS, PROP_SCHEDULER_THRESHOLD, + PROP_SCHEDULER_PRIORITY, PROP_MULTI_PROCESS_SERVICE, PROP_INPUT_QUANTIZED, PROP_OUTPUT_QUANTIZED, @@ -187,6 +188,10 @@ static void gst_hailonet_class_init(GstHailoNetClass *klass) g_object_class_install_property(gobject_class, PROP_SCHEDULER_THRESHOLD, g_param_spec_uint("scheduler-threshold", "Frames threshold for scheduler", "The minimum number of send requests required before the hailonet is considered ready to get run time from the scheduler.", HAILO_DEFAULT_SCHEDULER_THRESHOLD, std::numeric_limits::max(), HAILO_DEFAULT_SCHEDULER_THRESHOLD, (GParamFlags)(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); + g_object_class_install_property(gobject_class, PROP_SCHEDULER_PRIORITY, + g_param_spec_uint("scheduler-priority", "Priority index for scheduler", "When the scheduler will choose the next hailonet to run, higher priority will be prioritized in the selection. " + "Bigger number represent higher priority", + HAILO_SCHEDULER_PRIORITY_MIN, HAILO_SCHEDULER_PRIORITY_MAX, HAILO_SCHEDULER_PRIORITY_NORMAL, (GParamFlags)(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); g_object_class_install_property(gobject_class, PROP_MULTI_PROCESS_SERVICE, g_param_spec_boolean("multi-process-service", "Should run over HailoRT service", "Controls wether to run HailoRT over its service. " "To use this property, the service should be active and scheduling-algorithm should be set. Defaults to false.", @@ -474,7 +479,7 @@ void HailoNetImpl::set_property(GObject *object, guint property_id, const GValue break; case PROP_SCHEDULER_TIMEOUT_MS: if (m_was_configured) { - g_warning("The network was already configured so changing the scheduling algorithm will not take place!"); + g_warning("The network was already configured so changing the scheduling timeout will not take place!"); break; } if (m_props.m_is_active.was_changed()) { @@ -485,7 +490,7 @@ void HailoNetImpl::set_property(GObject *object, guint property_id, const GValue break; case PROP_SCHEDULER_THRESHOLD: if (m_was_configured) { - g_warning("The network was already configured so changing the scheduling algorithm will not take place!"); + g_warning("The network was already configured so changing the scheduling threshold will not take place!"); break; } if (m_props.m_is_active.was_changed()) { @@ -494,6 +499,17 @@ void HailoNetImpl::set_property(GObject *object, guint property_id, const GValue } m_props.m_scheduler_threshold = g_value_get_uint(value); break; + case PROP_SCHEDULER_PRIORITY: + if (m_was_configured) { + g_warning("The network was already configured so changing the scheduling priority will not take place!"); + break; + } + if (m_props.m_is_active.was_changed()) { + g_error("scheduler usage (scheduler-priority) in combination with 'is-active' is not supported."); + break; + } + m_props.m_scheduler_priority = static_cast(g_value_get_uint(value)); + break; case PROP_MULTI_PROCESS_SERVICE: if (m_was_configured) { g_warning("The network was already configured so changing the multi-process-service property will not take place!"); @@ -596,6 +612,9 @@ void HailoNetImpl::get_property(GObject *object, guint property_id, GValue *valu case PROP_SCHEDULER_THRESHOLD: g_value_set_uint(value, m_props.m_scheduler_threshold.get()); break; + case PROP_SCHEDULER_PRIORITY: + g_value_set_uint(value, m_props.m_scheduler_priority.get()); + break; case PROP_MULTI_PROCESS_SERVICE: g_value_set_boolean(value, m_props.m_multi_process_service.get()); break; @@ -696,6 +715,10 @@ hailo_status HailoNetImpl::configure_network_group() status = m_net_group_handle->set_scheduler_threshold(m_props.m_network_name.get(), m_props.m_scheduler_threshold.get()); GST_CHECK_SUCCESS(status, m_element, RESOURCE, "Setting scheduler threshold failed, status = %d", status); } + if (m_props.m_scheduler_priority.was_changed()) { + status = m_net_group_handle->set_scheduler_priority(m_props.m_network_name.get(), m_props.m_scheduler_priority.get()); + GST_CHECK_SUCCESS(status, m_element, RESOURCE, "Setting scheduler priority failed, status = %d", status); + } auto vstreams = m_net_group_handle->create_vstreams(m_props.m_network_name.get(), m_props.m_scheduling_algorithm.get(), m_output_formats, static_cast(m_props.m_input_quantized.get()), static_cast(m_props.m_output_quantized.get()), m_props.m_input_format_type.get(), m_props.m_output_format_type.get()); diff --git a/hailort/libhailort/bindings/gstreamer/gst-hailo/gsthailonet.hpp b/hailort/libhailort/bindings/gstreamer/gst-hailo/gsthailonet.hpp index 0c3e6f8..2840eb8 100644 --- a/hailort/libhailort/bindings/gstreamer/gst-hailo/gsthailonet.hpp +++ b/hailort/libhailort/bindings/gstreamer/gst-hailo/gsthailonet.hpp @@ -53,7 +53,7 @@ struct HailoNetProperties final public: HailoNetProperties() : m_device_id(nullptr), m_hef_path(nullptr), m_network_name(nullptr), m_batch_size(HAILO_DEFAULT_BATCH_SIZE), m_is_active(false), m_device_count(0), m_vdevice_key(DEFAULT_VDEVICE_KEY), m_scheduling_algorithm(HAILO_SCHEDULING_ALGORITHM_ROUND_ROBIN), - m_scheduler_timeout_ms(HAILO_DEFAULT_SCHEDULER_TIMEOUT_MS), m_scheduler_threshold(HAILO_DEFAULT_SCHEDULER_THRESHOLD), + m_scheduler_timeout_ms(HAILO_DEFAULT_SCHEDULER_TIMEOUT_MS), m_scheduler_threshold(HAILO_DEFAULT_SCHEDULER_THRESHOLD), m_scheduler_priority(HAILO_SCHEDULER_PRIORITY_NORMAL), m_multi_process_service(HAILO_DEFAULT_MULTI_PROCESS_SERVICE), m_input_quantized(true), m_output_quantized(true), m_input_format_type(HAILO_FORMAT_TYPE_AUTO), m_output_format_type(HAILO_FORMAT_TYPE_AUTO) @@ -69,6 +69,7 @@ public: HailoElemProperty m_scheduling_algorithm; HailoElemProperty m_scheduler_timeout_ms; HailoElemProperty m_scheduler_threshold; + HailoElemProperty m_scheduler_priority; HailoElemProperty m_multi_process_service; HailoElemProperty m_input_quantized; HailoElemProperty m_output_quantized; diff --git a/hailort/libhailort/bindings/gstreamer/gst-hailo/gsthailorecv.cpp b/hailort/libhailort/bindings/gstreamer/gst-hailo/gsthailorecv.cpp index e7955da..322545a 100644 --- a/hailort/libhailort/bindings/gstreamer/gst-hailo/gsthailorecv.cpp +++ b/hailort/libhailort/bindings/gstreamer/gst-hailo/gsthailorecv.cpp @@ -234,9 +234,11 @@ hailo_status HailoRecvImpl::read_from_vstreams(bool should_print_latency) std::chrono::duration latency = std::chrono::system_clock::now() - start_time; GST_DEBUG("%s latency: %f milliseconds", output_info.vstream().name().c_str(), latency.count()); } - GST_CHECK_SUCCESS(status, m_element, STREAM, "Reading from vstream failed, status = %d", status); - gst_buffer_unmap(*buffer, &buffer_info); + if (HAILO_STREAM_ABORTED_BY_USER == status) { + return status; + } + GST_CHECK_SUCCESS(status, m_element, STREAM, "Reading from vstream failed, status = %d", status); } if (should_print_latency) { diff --git a/hailort/libhailort/bindings/gstreamer/gst-hailo/gsthailosend.cpp b/hailort/libhailort/bindings/gstreamer/gst-hailo/gsthailosend.cpp index 184886c..c04927b 100644 --- a/hailort/libhailort/bindings/gstreamer/gst-hailo/gsthailosend.cpp +++ b/hailort/libhailort/bindings/gstreamer/gst-hailo/gsthailosend.cpp @@ -30,6 +30,7 @@ GST_DEBUG_CATEGORY_STATIC(gst_hailosend_debug_category); #define GST_CAT_DEFAULT gst_hailosend_debug_category #define RGB_FEATURES_SIZE (3) #define RGBA_FEATURES_SIZE (4) +#define GRAY8_FEATURES_SIZE (1) #define YUY2_FEATURES_SIZE (2) #define NV12_FEATURES_SIZE (3) #define NV21_FEATURES_SIZE (3) @@ -65,7 +66,7 @@ static void gst_hailosend_class_init(GstHailoSendClass *klass) gst_pad_template_new("sink", GST_PAD_SINK, GST_PAD_ALWAYS, gst_caps_from_string(HAILO_VIDEO_CAPS))); gst_element_class_set_static_metadata(GST_ELEMENT_CLASS(klass), - "hailosend element", "Hailo/Filter/Video", "Send RGB/RGBA/YUY2/NV12/NV21/I420 video to HailoRT", PLUGIN_AUTHOR); + "hailosend element", "Hailo/Filter/Video", "Send RGB/RGBA/GRAY8/YUY2/NV12/NV21/I420 video to HailoRT", PLUGIN_AUTHOR); element_class->change_state = GST_DEBUG_FUNCPTR(gst_hailosend_change_state); @@ -212,15 +213,28 @@ GstCaps *HailoSendImpl::get_caps(GstBaseTransform */*trans*/, GstPadDirection /* format = "RGBA"; break; } + else if (m_input_vstream_infos[0].shape.features == GRAY8_FEATURES_SIZE) + { + format = "GRAY8"; + break; + } /* Fallthrough */ case HAILO_FORMAT_ORDER_NHCW: case HAILO_FORMAT_ORDER_FCR: case HAILO_FORMAT_ORDER_F8CR: - format = "RGB"; - GST_CHECK(RGB_FEATURES_SIZE == m_input_vstream_infos[0].shape.features, NULL, m_element, STREAM, - "Features of input vstream %s is not %d for RGB format! (features=%d)", m_input_vstream_infos[0].name, RGB_FEATURES_SIZE, - m_input_vstream_infos[0].shape.features); - break; + if (m_input_vstream_infos[0].shape.features == GRAY8_FEATURES_SIZE) + { + format = "GRAY8"; + break; + } + else + { + format = "RGB"; + GST_CHECK(RGB_FEATURES_SIZE == m_input_vstream_infos[0].shape.features, NULL, m_element, STREAM, + "Features of input vstream %s is not %d for RGB format! (features=%d)", m_input_vstream_infos[0].name, RGB_FEATURES_SIZE, + m_input_vstream_infos[0].shape.features); + break; + } case HAILO_FORMAT_ORDER_YUY2: format = "YUY2"; GST_CHECK(YUY2_FEATURES_SIZE == m_input_vstream_infos[0].shape.features, NULL, m_element, STREAM, diff --git a/hailort/libhailort/bindings/gstreamer/gst-hailo/network_group_handle.cpp b/hailort/libhailort/bindings/gstreamer/gst-hailo/network_group_handle.cpp index fb0aecd..83f075a 100644 --- a/hailort/libhailort/bindings/gstreamer/gst-hailo/network_group_handle.cpp +++ b/hailort/libhailort/bindings/gstreamer/gst-hailo/network_group_handle.cpp @@ -180,6 +180,11 @@ hailo_status NetworkGroupHandle::set_scheduler_threshold(const char *network_nam return m_cng->set_scheduler_threshold(threshold, network_name); } +hailo_status NetworkGroupHandle::set_scheduler_priority(const char *network_name, uint8_t priority) +{ + return m_cng->set_scheduler_priority(priority, network_name); +} + Expected, std::vector>> NetworkGroupHandle::create_vstreams(const char *network_name, hailo_scheduling_algorithm_t scheduling_algorithm, const std::vector &output_formats, bool input_quantized, bool output_quantized, hailo_format_type_t input_format_type, hailo_format_type_t output_format_type) @@ -294,10 +299,10 @@ Expected> NetworkGroupConfigManager::con std::shared_ptr found_cng = get_configured_network_group(device_id, hef->hash(), network_group_name, batch_size); if (nullptr != found_cng) { - // If cng was already configured auto infos = found_cng->get_network_infos(); GST_CHECK_EXPECTED(infos, element, RESOURCE, "Failed getting network infos"); if ((infos.release().size() > 1) || (scheduling_algorithm == HAILO_SCHEDULING_ALGORITHM_NONE)) { + // If cng was already configured // But hailonet is not running all networks in the cng (or if not using scheduler) - // Do not use multiplexer! return found_cng; diff --git a/hailort/libhailort/bindings/gstreamer/gst-hailo/network_group_handle.hpp b/hailort/libhailort/bindings/gstreamer/gst-hailo/network_group_handle.hpp index 665d0ac..0a4fabf 100644 --- a/hailort/libhailort/bindings/gstreamer/gst-hailo/network_group_handle.hpp +++ b/hailort/libhailort/bindings/gstreamer/gst-hailo/network_group_handle.hpp @@ -90,7 +90,7 @@ public: hailo_status set_scheduler_timeout(const char *network_name, uint32_t timeout_ms); hailo_status set_scheduler_threshold(const char *network_name, uint32_t threshold); - + hailo_status set_scheduler_priority(const char *network_name, uint8_t priority); std::shared_ptr hef() { diff --git a/hailort/libhailort/bindings/python/CMakeLists.txt b/hailort/libhailort/bindings/python/CMakeLists.txt index febd4f0..ffdcfc9 100644 --- a/hailort/libhailort/bindings/python/CMakeLists.txt +++ b/hailort/libhailort/bindings/python/CMakeLists.txt @@ -1 +1,4 @@ +cmake_minimum_required(VERSION 3.11.0) + +include(externals/pybind11.cmake) add_subdirectory(src) diff --git a/hailort/libhailort/bindings/python/externals/pybind11.cmake b/hailort/libhailort/bindings/python/externals/pybind11.cmake new file mode 100644 index 0000000..db0b705 --- /dev/null +++ b/hailort/libhailort/bindings/python/externals/pybind11.cmake @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.11.0) + +include(FetchContent) + +if(NOT PYTHON_EXECUTABLE AND PYBIND11_PYTHON_VERSION) + # venv version is prioritized (instead of PYBIND11_PYTHON_VERSION) if PYTHON_EXECUTABLE is not set. + # See https://pybind11.readthedocs.io/en/stable/changelog.html#v2-6-0-oct-21-2020 + if((${CMAKE_VERSION} VERSION_LESS "3.22.0") AND (NOT WIN32)) + find_package(PythonInterp ${PYBIND11_PYTHON_VERSION} REQUIRED) + set(PYTHON_EXECUTABLE ${Python_EXECUTABLE}) + else() + find_package(Python3 ${PYBIND11_PYTHON_VERSION} REQUIRED EXACT COMPONENTS Interpreter Development) + set(PYTHON_EXECUTABLE ${Python3_EXECUTABLE}) + endif() +endif() + +FetchContent_Declare( + pybind11 + GIT_REPOSITORY https://github.com/pybind/pybind11.git + GIT_TAG 80dc998efced8ceb2be59756668a7e90e8bef917 # Version 2.10.1 + #GIT_SHALLOW TRUE + SOURCE_DIR "${CMAKE_CURRENT_LIST_DIR}/pybind11" + BINARY_DIR "${CMAKE_CURRENT_LIST_DIR}/pybind11" +) + +if(NOT HAILO_OFFLINE_COMPILATION) + # https://stackoverflow.com/questions/65527126/disable-install-for-fetchcontent + FetchContent_GetProperties(pybind11) + if(NOT pybind11_POPULATED) + FetchContent_Populate(pybind11) + add_subdirectory(${pybind11_SOURCE_DIR} ${pybind11_BINARY_DIR} EXCLUDE_FROM_ALL) + endif() +else() + add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/pybind11 EXCLUDE_FROM_ALL) +endif() \ No newline at end of file diff --git a/hailort/libhailort/bindings/python/platform/hailo_platform/__init__.py b/hailort/libhailort/bindings/python/platform/hailo_platform/__init__.py index 18d66a1..ed8ac13 100644 --- a/hailort/libhailort/bindings/python/platform/hailo_platform/__init__.py +++ b/hailort/libhailort/bindings/python/platform/hailo_platform/__init__.py @@ -26,7 +26,7 @@ from hailo_platform.pyhailort.pyhailort import (HEF, ConfigureParams, InputVStreams, OutputVStreams, InferVStreams, HailoStreamDirection, HailoFormatFlags, HailoCpuId, Device, VDevice, DvmTypes, PowerMeasurementTypes, SamplingPeriod, AveragingFactor, MeasurementBufferIndex, - HailoRTException, YOLOv5PostProcessOp, HailoSchedulingAlgorithm) + HailoRTException, HailoSchedulingAlgorithm, HailoRTStreamAbortedByUser) def _verify_pyhailort_lib_exists(): python_version = "".join(str(i) for i in sys.version_info[:2]) @@ -62,4 +62,4 @@ __all__ = ['EthernetDevice', 'DvmTypes', 'PowerMeasurementTypes', 'MipiIspImageInOrder', 'MipiIspImageOutDataType', 'join_drivers_path', 'IspLightFrequency', 'HailoPowerMode', 'Endianness', 'HailoStreamInterface', 'InputVStreamParams', 'OutputVStreamParams', 'InputVStreams', 'OutputVStreams', 'InferVStreams', 'HailoStreamDirection', 'HailoFormatFlags', 'HailoCpuId', - 'Device', 'VDevice', 'HailoRTException', 'YOLOv5PostProcessOp', 'HailoSchedulingAlgorithm'] + 'Device', 'VDevice', 'HailoRTException', 'HailoSchedulingAlgorithm', 'HailoRTStreamAbortedByUser'] diff --git a/hailort/libhailort/bindings/python/platform/hailo_platform/pyhailort/control_object.py b/hailort/libhailort/bindings/python/platform/hailo_platform/pyhailort/control_object.py index a1f5095..4b472bb 100644 --- a/hailort/libhailort/bindings/python/platform/hailo_platform/pyhailort/control_object.py +++ b/hailort/libhailort/bindings/python/platform/hailo_platform/pyhailort/control_object.py @@ -2,6 +2,7 @@ """Control operations for the Hailo hardware device.""" +from hailo_platform.common.logger.logger import default_logger from hailo_platform.pyhailort.pyhailort import (Control, InternalPcieDevice, ExceptionWrapper, BoardInformation, # noqa F401 CoreInformation, DeviceArchitectureTypes, ExtendedDeviceInformation, # noqa F401 HealthInformation, SamplingPeriod, AveragingFactor, DvmTypes, # noqa F401 @@ -38,8 +39,7 @@ class UdpHcpControl(HcpControl): """ # In the C API we define the total amount of attempts, instead of the amount of retries. - # TODO: HRT-9987 - Add this deprecation warning - # default_logger().warning("UdpHcpControl is deprecated! Please Use Control object") + default_logger().warning("UdpHcpControl is deprecated! Please Use Control object") max_number_of_attempts = retries + 1 response_timeout_milliseconds = int(response_timeout_seconds * 1000) if device is None: @@ -57,8 +57,8 @@ class PcieHcpControl(HcpControl): def __init__(self, device=None, device_info=None): """Initializes a new HailoPcieController object.""" - # TODO: HRT-9987 - Add this deprecation warning - # default_logger().warning("PcieHcpControl is deprecated! Please Use Control object") + + default_logger().warning("PcieHcpControl is deprecated! Please Use Control object") if device_info is None: device_info = InternalPcieDevice.scan_devices()[0] diff --git a/hailort/libhailort/bindings/python/platform/hailo_platform/pyhailort/hw_object.py b/hailort/libhailort/bindings/python/platform/hailo_platform/pyhailort/hw_object.py index 32483c5..bf078b7 100644 --- a/hailort/libhailort/bindings/python/platform/hailo_platform/pyhailort/hw_object.py +++ b/hailort/libhailort/bindings/python/platform/hailo_platform/pyhailort/hw_object.py @@ -27,8 +27,7 @@ class HailoHWObjectException(Exception): class HailoHWObject(object): - # TODO: HRT-9987 - Add (deprecated) to this doc - """Abstract Hailo hardware device representation""" + """Abstract Hailo hardware device representation (deprecated)""" NAME = InferenceTargets.UNINITIALIZED IS_HARDWARE = True @@ -44,8 +43,7 @@ class HailoHWObject(object): self._is_device_used = False self._hef_loaded = False - # TODO: HRT-9987 - Add this deprecation warning - # self._logger.warning("HailoHWObject is deprecated! Please use VDevice/Device object.") + self._logger.warning("HailoHWObject is deprecated! Please use VDevice/Device object.") # TODO: HRT-6310 Remove this. def __eq__(self, other): @@ -53,17 +51,15 @@ class HailoHWObject(object): @property def name(self): - """str: The name of this target. Valid values are defined by :class:`~hailo_platform.pyhailort.hw_object.InferenceTargets`""" - # TODO: HRT-9987 - Add this deprecation warning - # self._logger.warning("HailoHWObject name property is deprecated! Please use VDevice/Device object with device_id.") + """str: The name of this target. Valid values are defined by :class:`~hailo_platform.pyhailort.hw_object.InferenceTargets` (deprecated)""" + self._logger.warning("HailoHWObject name property is deprecated! Please use VDevice/Device object with device_id.") return type(self).NAME @property def is_hardware(self): - """bool: Indicates this target runs on a physical hardware device.""" + """bool: Indicates this target runs on a physical hardware device. (deprecated)""" # TODO: SDK should implement in Target - # TODO: HRT-9987 - Add this deprecation warning - # self._logger.warning("HailoHWObject is_hardware property is deprecated! Please use VDevice/Device object, or derive from it.") + self._logger.warning("HailoHWObject is_hardware property is deprecated! Please use VDevice/Device object, or derive from it.") return type(self).IS_HARDWARE @property @@ -76,46 +72,42 @@ class HailoHWObject(object): @property def sorted_output_layer_names(self): - """Getter for the property sorted_output_names. + """Getter for the property sorted_output_names (deprecated). Returns: list of str: Sorted list of the output layer names. """ - # TODO: HRT-9987 - Add this deprecation warning - # self._logger.warning("HailoHWObject sorted_output_layer_names property is deprecated! Please use ConfiguredNetwork get_sorted_output_names.") + self._logger.warning("HailoHWObject sorted_output_layer_names property is deprecated! Please use ConfiguredNetwork get_sorted_output_names.") if len(self._loaded_network_groups) != 1: raise HailoHWObjectException("Access to sorted_output_layer_names is only allowed when there is a single loaded network group") return self._loaded_network_groups[0].get_sorted_output_names() @contextmanager def use_device(self, *args, **kwargs): - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoHWObject use_device context manager is deprecated! Please use VDevice/Device object.") - """A context manager that wraps the usage of the device.""" + """A context manager that wraps the usage of the device. (deprecated)""" + self._logger.warning("HailoHWObject use_device context manager is deprecated! Please use VDevice/Device object.") self._is_device_used = True yield self._is_device_used = False def get_output_device_layer_to_original_layer_map(self): - """Get a mapping between the device outputs to the layers' names they represent. + """Get a mapping between the device outputs to the layers' names they represent (deprecated). Returns: dict: Keys are device output names and values are lists of layers' names. """ - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoHWObject get_output_device_layer_to_original_layer_map function is deprecated!") + self._logger.warning("HailoHWObject get_output_device_layer_to_original_layer_map function is deprecated!") if len(self._loaded_network_groups) != 1: raise HailoHWObjectException("Access to layer names is only allowed when there is a single loaded network group") return {stream_info.name : self._loaded_network_groups[0].get_vstream_names_from_stream_name(stream_info.name) for stream_info in self.get_output_stream_infos()} def get_original_layer_to_device_layer_map(self): - """Get a mapping between the layer names and the device outputs that contain them. + """Get a mapping between the layer names and the device outputs that contain them (deprecated). Returns: dict: Keys are the names of the layers and values are device outputs names. """ - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoHWObject get_original_layer_to_device_layer_map function is deprecated!") + self._logger.warning("HailoHWObject get_original_layer_to_device_layer_map function is deprecated!") if len(self._loaded_network_groups) != 1: raise HailoHWObjectException("Access to layer names is only allowed when there is a single loaded network group") return {vstream_info.name : self._loaded_network_groups[0].get_stream_names_from_vstream_name(vstream_info.name) @@ -123,69 +115,61 @@ class HailoHWObject(object): @property def device_input_layers(self): - """Get a list of the names of the device's inputs.""" - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoHWObject device_input_layers function is deprecated! Please use ConfiguredNetwork object.") + """Get a list of the names of the device's inputs. (deprecated)""" + self._logger.warning("HailoHWObject device_input_layers function is deprecated! Please use ConfiguredNetwork object.") return [layer.name for layer in self.get_input_stream_infos()] @property def device_output_layers(self): - """Get a list of the names of the device's outputs.""" - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoHWObject device_output_layers function is deprecated! Please use ConfiguredNetwork object.") + """Get a list of the names of the device's outputs. (deprecated)""" + self._logger.warning("HailoHWObject device_output_layers function is deprecated! Please use ConfiguredNetwork object.") return [layer.name for layer in self.get_output_stream_infos()] def hef_loaded(self): - """Return True if this object has loaded the model HEF to the hardware device.""" + """Return True if this object has loaded the model HEF to the hardware device. (deprecated)""" # TODO: SDK should implement in Target - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoHWObject hef_loaded function is deprecated! Please use VDevice/Device object, or derive from it.") + self._logger.warning("HailoHWObject hef_loaded function is deprecated! Please use VDevice/Device object, or derive from it.") return self._hef_loaded def outputs_count(self): """Return the amount of output tensors that are returned from the hardware device for every - input image. + input image (deprecated). """ - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoHWObject outputs_count function is deprecated! Please use ConfiguredNetwork object.") + self._logger.warning("HailoHWObject outputs_count function is deprecated! Please use ConfiguredNetwork object.") return len(self.get_output_vstream_infos()) def _clear_shapes(self): # TODO: SDK should implement in Target - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoHWObject _clear_shapes function is deprecated! Please use ConfiguredNetwork object.") + self._logger.warning("HailoHWObject _clear_shapes function is deprecated! Please use ConfiguredNetwork object.") self._hw_consts = None @property def model_name(self): - """Get the name of the current model. + """Get the name of the current model (deprecated). Returns: str: Model name. """ - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoHWObject model_name property is deprecated! Please use ConfiguredNetwork object.") + self._logger.warning("HailoHWObject model_name property is deprecated! Please use ConfiguredNetwork object.") if len(self._loaded_network_groups) == 1: return self._loaded_network_groups[0].name raise HailoHWObjectException( "This function is only supported when there is exactly 1 loaded network group. one should use HEF.get_network_group_names() / ConfiguredNetwork.name / ActivatedNetwork.name") def get_output_shapes(self): - """Get the model output shapes, as returned to the user (without any hardware padding). + """Get the model output shapes, as returned to the user (without any hardware padding) (deprecated). Returns: Tuple of output shapes, sorted by the output names. """ - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoHWObject get_output_shapes function is deprecated! Please use ConfiguredNetwork object.") + self._logger.warning("HailoHWObject get_output_shapes function is deprecated! Please use ConfiguredNetwork object.") if len(self._loaded_network_groups) != 1: raise HailoHWObjectException("Calling get_output_shapes is only allowed when there is a single loaded network group") return self._loaded_network_groups[0].get_output_shapes() class HailoChipObject(HailoHWObject): - # TODO: HRT-9987 - Add (deprecated) to this docs - """Hailo hardware device representation""" + """Hailo hardware device representation (deprecated)""" def __init__(self): """Create the Hailo Chip hardware object.""" @@ -208,17 +192,16 @@ class HailoChipObject(HailoHWObject): return self._control_object def get_all_input_layers_dtype(self): - """Get the model inputs dtype. + """Get the model inputs dtype (deprecated). Returns: dict of :obj:'numpy.dtype': where the key is model input_layer name, and the value is dtype as the device expect to get for this input. """ - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoChipObject get_all_input_layers_dtype function is deprecated! Please use ConfiguredNetwork object.") + self._logger.warning("HailoChipObject get_all_input_layers_dtype function is deprecated! Please use ConfiguredNetwork object.") return {stream.name: HailoRTTransformUtils.get_dtype(stream.data_bytes) for stream in self.get_input_stream_infos()} def get_input_vstream_infos(self, network_name=None): - """Get input vstreams information of a specific network group. + """Get input vstreams information of a specific network group (deprecated). Args: network_name (str, optional): The name of the network to access. In case not given, all the networks in the network group will be addressed. @@ -227,14 +210,13 @@ class HailoChipObject(HailoHWObject): If there is exactly one configured network group, returns a list of :obj:`hailo_platform.pyhailort._pyhailort.VStreamInfo`: with all the information objects of all input vstreams """ - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoChipObject get_input_vstream_infos function is deprecated! Please use ConfiguredNetwork object.") + self._logger.warning("HailoChipObject get_input_vstream_infos function is deprecated! Please use ConfiguredNetwork object.") if len(self._loaded_network_groups) != 1: raise HailoHWObjectException("Access to network vstream info is only allowed when there is a single loaded network group") return self._loaded_network_groups[0].get_input_vstream_infos(network_name=network_name) def get_output_vstream_infos(self, network_name=None): - """Get output vstreams information of a specific network group. + """Get output vstreams information of a specific network group (deprecated). Args: network_name (str, optional): The name of the network to access. In case not given, all the networks in the network group will be addressed. @@ -243,14 +225,13 @@ class HailoChipObject(HailoHWObject): If there is exactly one configured network group, returns a list of :obj:`hailo_platform.pyhailort._pyhailort.VStreamInfo`: with all the information objects of all output vstreams """ - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoChipObject get_output_vstream_infos function is deprecated! Please use ConfiguredNetwork object.") + self._logger.warning("HailoChipObject get_output_vstream_infos function is deprecated! Please use ConfiguredNetwork object.") if len(self._loaded_network_groups) != 1: raise HailoHWObjectException("Access to network vstream info is only allowed when there is a single loaded network group") return self._loaded_network_groups[0].get_output_vstream_infos(network_name=network_name) def get_all_vstream_infos(self, network_name=None): - """Get input and output vstreams information. + """Get input and output vstreams information (deprecated). Args: network_name (str, optional): The name of the network to access. In case not given, all the networks in the network group will be addressed. @@ -259,14 +240,13 @@ class HailoChipObject(HailoHWObject): If there is exactly one configured network group, returns a list of :obj:`hailo_platform.pyhailort._pyhailort.VStreamInfo`: with all the information objects of all input and output vstreams """ - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoChipObject get_all_vstream_infos function is deprecated! Please use ConfiguredNetwork object.") + self._logger.warning("HailoChipObject get_all_vstream_infos function is deprecated! Please use ConfiguredNetwork object.") if len(self._loaded_network_groups) != 1: raise HailoHWObjectException("Access to network vstream info is only allowed when there is a single loaded network group") return self._loaded_network_groups[0].get_all_vstream_infos(network_name=network_name) def get_input_stream_infos(self, network_name=None): - """Get the input low-level streams information of a specific network group. + """Get the input low-level streams information of a specific network group (deprecated). Args: network_name (str, optional): The name of the network to access. In case not given, all the networks in the network group will be addressed. @@ -276,14 +256,13 @@ class HailoChipObject(HailoHWObject): :obj:`hailo_platform.pyhailort._pyhailort.VStreamInfo`: with information objects of all input low-level streams. """ - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoChipObject get_input_stream_infos function is deprecated! Please use ConfiguredNetwork object.") + self._logger.warning("HailoChipObject get_input_stream_infos function is deprecated! Please use ConfiguredNetwork object.") if len(self._loaded_network_groups) != 1: raise HailoHWObjectException("Access to network stream info is only allowed when there is a single loaded network group") return self._loaded_network_groups[0].get_input_stream_infos(network_name=network_name) def get_output_stream_infos(self, network_name=None): - """Get the output low-level streams information of a specific network group. + """Get the output low-level streams information of a specific network group (deprecated). Args: network_name (str, optional): The name of the network to access. In case not given, all the networks in the network group will be addressed. @@ -293,14 +272,13 @@ class HailoChipObject(HailoHWObject): :obj:`hailo_platform.pyhailort._pyhailort.VStreamInfo`: with information objects of all output low-level streams. """ - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoChipObject get_output_stream_infos function is deprecated! Please use ConfiguredNetwork object.") + self._logger.warning("HailoChipObject get_output_stream_infos function is deprecated! Please use ConfiguredNetwork object.") if len(self._loaded_network_groups) != 1: raise HailoHWObjectException("Access to network stream info is only allowed when there is a single loaded network group") return self._loaded_network_groups[0].get_output_stream_infos(network_name=network_name) def get_all_stream_infos(self, network_name=None): - """Get input and output streams information of a specific network group. + """Get input and output streams information of a specific network group (deprecated). Args: network_name (str, optional): The name of the network to access. In case not given, all the networks in the network group will be addressed. @@ -309,8 +287,7 @@ class HailoChipObject(HailoHWObject): If there is exactly one configured network group, returns a list of :obj:`hailo_platform.pyhailort._pyhailort.StreamInfo`: with all the information objects of all input and output streams """ - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoChipObject get_all_stream_infos function is deprecated! Please use ConfiguredNetwork object.") + self._logger.warning("HailoChipObject get_all_stream_infos function is deprecated! Please use ConfiguredNetwork object.") if len(self._loaded_network_groups) != 1: raise HailoHWObjectException("Access to network stream info is only allowed when there is a single loaded network group") return self._loaded_network_groups[0].get_all_stream_infos(network_name=network_name) @@ -339,12 +316,12 @@ class HailoChipObject(HailoHWObject): raise HailoRTException("Device can only be configured from the process it was created in.") configured_apps = self.control.configure(hef, configure_params_by_name) self._hef_loaded = True - configured_networks = [ConfiguredNetwork(configured_app, self, hef) for configured_app in configured_apps] + configured_networks = [ConfiguredNetwork(configured_app) for configured_app in configured_apps] self._loaded_network_groups.extend(configured_networks) return configured_networks def get_input_shape(self, name=None): - """Get the input shape (not padded) of a network. + """Get the input shape (not padded) of a network (deprecated). Args: name (str, optional): The name of the desired input. If a name is not provided, return @@ -353,8 +330,7 @@ class HailoChipObject(HailoHWObject): Returns: Tuple of integers representing the input_shape. """ - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoChipObject get_input_shape function is deprecated! Please use ConfiguredNetwork object.") + self._logger.warning("HailoChipObject get_input_shape function is deprecated! Please use ConfiguredNetwork object.") if name is None: name = self.get_input_vstream_infos()[0].name @@ -366,7 +342,7 @@ class HailoChipObject(HailoHWObject): [input_vstream.name for input_vstream in self.get_input_vstream_infos()])) def get_index_from_name(self, name): - """Get the index in the output list from the name. + """Get the index in the output list from the name (deprecated). Args: name (str): The name of the output. @@ -374,8 +350,7 @@ class HailoChipObject(HailoHWObject): Returns: int: The index of the layer name in the output list. """ - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("HailoChipObject get_index_from_name function is deprecated! Please use ConfiguredNetwork object.") + self._logger.warning("HailoChipObject get_index_from_name function is deprecated! Please use ConfiguredNetwork object.") try: return self.sorted_output_layer_names.index(name) except ValueError: @@ -398,8 +373,7 @@ class HailoChipObject(HailoHWObject): class EthernetDevice(HailoChipObject): - # TODO: HRT-9987 - Add (deprecated) to this docs - """Represents any Hailo hardware device that supports UDP control and dataflow""" + """Represents any Hailo hardware device that supports UDP control and dataflow (deprecated)""" NAME = InferenceTargets.UDP_CONTROLLER @@ -417,6 +391,8 @@ class EthernetDevice(HailoChipObject): super(EthernetDevice, self).__init__() + self._logger.warning("EthernetDevice is deprecated! Please use VDevice/Device object.") + gc.collect() self._remote_ip = remote_ip @@ -442,8 +418,7 @@ class EthernetDevice(HailoChipObject): Returns: list of str: IPs of scanned devices. """ - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # default_logger().warning("EthernetDevice scan_devices method is deprecated! Please use scan() of Device object.") + default_logger().warning("EthernetDevice scan_devices method is deprecated! Please use scan() of Device object.") udp_scanner = HailoUdpScan() return udp_scanner.scan_devices(interface_name, timeout_seconds=timeout_seconds) @@ -463,15 +438,13 @@ class EthernetDevice(HailoChipObject): @property def remote_ip(self): - """Return the IP of the remote device.""" - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # self._logger.warning("EthernetDevice remote_ip method is deprecated! Please use VDevice/Device object.") + """Return the IP of the remote device (deprecated).""" + self._logger.warning("EthernetDevice remote_ip method is deprecated! Please use VDevice/Device object.") return self._remote_ip class PcieDevice(HailoChipObject): - # TODO: HRT-9987 - Add (deprecated) to this docs - """Hailo PCIe production device representation""" + """Hailo PCIe production device representation (deprecated)""" NAME = InferenceTargets.PCIE_CONTROLLER @@ -486,8 +459,7 @@ class PcieDevice(HailoChipObject): :func:`PcieDevice.scan_devices` to get list of all available devices. """ super(PcieDevice, self).__init__() - # TODO: HRT-9987 - Add this deprecation warning - # self._logger.warning("PcieDevice is deprecated! Please use VDevice/Device object.") + self._logger.warning("PcieDevice is deprecated! Please use VDevice/Device object.") gc.collect() # PcieDevice __del__ function tries to release self._device. @@ -506,13 +478,12 @@ class PcieDevice(HailoChipObject): @staticmethod def scan_devices(): - """Scans for all pcie devices on the system. + """Scans for all pcie devices on the system (deprecated). Returns: list of :obj:`hailo_platform.pyhailort.pyhailort.PcieDeviceInfo` """ - # TODO: HRT-9987 - Add this deprecation warning and (deprecated) to this docs - # default_logger().warning("PcieDevice scan_devices method is deprecated! Please use Device object.") + default_logger().warning("PcieDevice scan_devices method is deprecated! Please use Device object.") return InternalPcieDevice.scan_devices() def _open_device(self, device_info): diff --git a/hailort/libhailort/bindings/python/platform/hailo_platform/pyhailort/pyhailort.py b/hailort/libhailort/bindings/python/platform/hailo_platform/pyhailort/pyhailort.py index 6b85acf..6f73059 100644 --- a/hailort/libhailort/bindings/python/platform/hailo_platform/pyhailort/pyhailort.py +++ b/hailort/libhailort/bindings/python/platform/hailo_platform/pyhailort/pyhailort.py @@ -29,8 +29,7 @@ from hailo_platform.pyhailort._pyhailort import (TemperatureInfo, # noqa F401 MipiClockSelection, MipiIspImageInOrder, MipiIspImageOutDataType, IspLightFrequency, BootSource, HailoSocketDefs, Endianness, - MipiInputStreamParams, SensorConfigTypes, - SensorConfigOpCode) + MipiInputStreamParams, SensorConfigTypes) BBOX_PARAMS = _pyhailort.HailoRTDefaults.BBOX_PARAMS() HAILO_DEFAULT_ETH_CONTROL_PORT = _pyhailort.HailoRTDefaults.HAILO_DEFAULT_ETH_CONTROL_PORT() @@ -75,6 +74,9 @@ class HailoRTTimeout(HailoRTException): class HailoRTStreamAborted(HailoRTException): pass +class HailoRTStreamAbortedByUser(HailoRTException): + pass + class HailoRTInvalidOperationException(HailoRTException): pass @@ -127,6 +129,8 @@ class ExceptionWrapper(object): raise HailoRTTimeout("Received a timeout - hailort has failed because a timeout had occurred") from libhailort_exception if string_error_code == "HAILO_STREAM_ABORTED_BY_HW": raise HailoRTStreamAborted("Stream aborted due to an external event") from libhailort_exception + if string_error_code == "HAILO_STREAM_ABORTED_BY_USER": + raise HailoRTStreamAbortedByUser("Stream was aborted by user") from libhailort_exception if string_error_code == "HAILO_INVALID_OPERATION": raise HailoRTInvalidOperationException("Invalid operation. See hailort.log for more information") from libhailort_exception @@ -170,23 +174,26 @@ class HailoUdpScan(object): return device_ip_addresses -class TrafficControl(object): +class NetworkRateLimiter(object): def __init__(self, ip, port, rate_bytes_per_sec): if sys.platform != 'linux': - raise HailoRTInvalidOperationException('TrafficControl is supported only on UNIX os') - with ExceptionWrapper(): - self._tc_util = _pyhailort.TrafficControlUtil(ip, port, int(rate_bytes_per_sec)) - + raise HailoRTInvalidOperationException('NetworkRateLimiter is supported only on UNIX os') + self._ip = ip + self._port = port + self._rate_bytes_per_sec = rate_bytes_per_sec + def set_rate_limit(self): - self._tc_util.set_rate_limit() + with ExceptionWrapper(): + return _pyhailort.NetworkRateLimiter.set_rate_limit(self._ip, self._port, self._rate_bytes_per_sec) def reset_rate_limit(self): - self._tc_util.reset_rate_limit() + with ExceptionWrapper(): + return _pyhailort.NetworkRateLimiter.reset_rate_limit(self._ip, self._port) def get_interface_name(ip): "get the interface corresponding to the given ip" with ExceptionWrapper(): - return _pyhailort.TrafficControlUtil.get_interface_name(ip) + return _pyhailort.NetworkRateLimiter.get_interface_name(ip) class ConfigureParams(object): @@ -524,15 +531,13 @@ class HEF(object): class ConfiguredNetwork(object): """Represents a network group loaded to the device.""" - def __init__(self, configured_network, target, hef): + def __init__(self, configured_network): self._configured_network = configured_network self._input_vstreams_holders = [] self._output_vstreams_holders = [] - self._target = target - self._hef = hef def get_networks_names(self): - return self._hef.get_networks_names(self.name) + return self._configured_network.get_networks_names() def activate(self, network_group_params=None): """Activate this network group in order to infer data through it. @@ -544,14 +549,18 @@ class ConfiguredNetwork(object): Returns: :class:`ActivatedNetworkContextManager`: Context manager that returns the activated network group. + + Note: + Usage of `activate` when scheduler enabled is deprecated. On this case, this function will return None and print deprecation warning. """ - # TODO: HRT-9988 - Add deprecation warning when changing to service by default - network_group_params = network_group_params or self.create_params() + if self._configured_network.is_scheduled(): + default_logger().warning("Calls to `activate()` when working with scheduler are deprecated! On future versions this call will raise an error.") + return EmptyContextManager() + network_group_params = network_group_params or self.create_params() with ExceptionWrapper(): return ActivatedNetworkContextManager(self, - self._configured_network.activate(network_group_params), - self._target, self._hef) + self._configured_network.activate(network_group_params)) def wait_for_activation(self, timeout_ms=None): """Block until activated, or until ``timeout_ms`` is passed. @@ -590,7 +599,7 @@ class ConfiguredNetwork(object): return tuple(results) def get_sorted_output_names(self): - return self._hef.get_sorted_output_names(self.name) + return self._configured_network.get_sorted_output_names() def get_input_vstream_infos(self, network_name=None): """Get input vstreams information. @@ -602,8 +611,8 @@ class ConfiguredNetwork(object): list of :obj:`hailo_platform.pyhailort._pyhailort.VStreamInfo`: with all the information objects of all input vstreams """ - name = network_name if network_name is not None else self.name - return self._hef.get_input_vstream_infos(name) + name = network_name if network_name is not None else "" + return self._configured_network.get_input_vstream_infos(name) def get_output_vstream_infos(self, network_name=None): """Get output vstreams information. @@ -615,8 +624,8 @@ class ConfiguredNetwork(object): list of :obj:`hailo_platform.pyhailort._pyhailort.VStreamInfo`: with all the information objects of all output vstreams """ - name = network_name if network_name is not None else self.name - return self._hef.get_output_vstream_infos(name) + name = network_name if network_name is not None else "" + return self._configured_network.get_output_vstream_infos(name) def get_all_vstream_infos(self, network_name=None): """Get input and output vstreams information. @@ -628,8 +637,8 @@ class ConfiguredNetwork(object): list of :obj:`hailo_platform.pyhailort._pyhailort.VStreamInfo`: with all the information objects of all input and output vstreams """ - name = network_name if network_name is not None else self.name - return self._hef.get_all_vstream_infos(name) + name = network_name if network_name is not None else "" + return self._configured_network.get_all_vstream_infos(name) def get_input_stream_infos(self, network_name=None): """Get the input low-level streams information of a specific network group. @@ -642,8 +651,8 @@ class ConfiguredNetwork(object): of all input low-level streams. """ - name = network_name if network_name is not None else self.name - return self._hef.get_input_stream_infos(name) + name = network_name if network_name is not None else "" + return self._configured_network.get_input_stream_infos(name) def get_output_stream_infos(self, network_name=None): """Get the output low-level streams information of a specific network group. @@ -656,8 +665,8 @@ class ConfiguredNetwork(object): of all output low-level streams. """ - name = network_name if network_name is not None else self.name - return self._hef.get_output_stream_infos(name) + name = network_name if network_name is not None else "" + return self._configured_network.get_output_stream_infos(name) def get_all_stream_infos(self, network_name=None): """Get input and output streams information of a specific network group. @@ -669,8 +678,8 @@ class ConfiguredNetwork(object): list of :obj:`hailo_platform.pyhailort._pyhailort.StreamInfo`: with all the information objects of all input and output streams """ - name = network_name if network_name is not None else self.name - return self._hef.get_all_stream_infos(name) + name = network_name if network_name is not None else "" + return self._configured_network.get_all_stream_infos(name) def get_udp_rates_dict(self, fps, max_supported_rate_bytes): with ExceptionWrapper(): @@ -720,7 +729,7 @@ class ConfiguredNetwork(object): list of str: All the underlying streams names for the provided vstream name. """ with ExceptionWrapper(): - return self._hef.get_stream_names_from_vstream_name(vstream_name, self.name) + return self._configured_network.get_stream_names_from_vstream_name(vstream_name) def get_vstream_names_from_stream_name(self, stream_name): """Get vstream names list from their underlying stream name for a specific network group. @@ -732,7 +741,7 @@ class ConfiguredNetwork(object): list of str: All the matching vstream names for the provided stream name. """ with ExceptionWrapper(): - return self._hef.get_vstream_names_from_stream_name(stream_name, self.name) + return self._configured_network.get_vstream_names_from_stream_name(stream_name) def set_scheduler_timeout(self, timeout_ms, network_name=None): """Sets the maximum time period that may pass before getting run time from the scheduler, @@ -767,19 +776,29 @@ class ConfiguredNetwork(object): return self._configured_network.set_scheduler_priority(priority) +class EmptyContextManager(object): + """An empty context manager that returns instead of activated network group when scheduler is enabled`.""" + + def __init__(self): + pass + + def __enter__(self): + pass + + def __exit__(self, *args): + pass + + class ActivatedNetworkContextManager(object): """A context manager that returns the activated network group upon enter.""" - def __init__(self, configured_network, activated_network, target, hef): + def __init__(self, configured_network, activated_network): self._configured_network = configured_network self._activated_network = activated_network - self._target = target - self._hef = hef def __enter__(self): with ExceptionWrapper(): - activated_network_group = ActivatedNetwork(self._configured_network, self._activated_network.__enter__(), self._target, - self._hef) + activated_network_group = ActivatedNetwork(self._configured_network, self._activated_network.__enter__()) return activated_network_group def __exit__(self, *args): @@ -789,16 +808,10 @@ class ActivatedNetworkContextManager(object): class ActivatedNetwork(object): """The network group that is currently activated for inference.""" - def __init__(self, configured_network, activated_network, target, hef): + def __init__(self, configured_network, activated_network): self._configured_network = configured_network self._activated_network = activated_network - self._target = target - self._hef = hef self._last_number_of_invalid_frames_read = 0 - - @property - def target(self): - return self._target @property def name(self): @@ -826,7 +839,7 @@ class ActivatedNetwork(object): raise HailoRTException("There are {} invalid frames.".format(number_of_invalid_frames)) def get_sorted_output_names(self): - return self._hef.get_sorted_output_names(self.name) + return self._configured_network.get_sorted_output_names() def _get_intermediate_buffer(self, src_context_index, src_stream_index): with ExceptionWrapper(): @@ -859,7 +872,6 @@ class InferVStreams(object): ``[class_count, BBOX_PARAMS, detections_count]`` padded with empty bboxes. """ - self._logger = default_logger() self._configured_net_group = configured_net_group self._net_group_name = configured_net_group.name self._input_vstreams_params = input_vstreams_params @@ -895,8 +907,9 @@ class InferVStreams(object): network_name = self._input_name_to_network_name[input_name] if (network_name not in already_seen_networks) : already_seen_networks.add(network_name) + output_vstream_infos = self._configured_net_group.get_output_vstream_infos() for output_name in self._network_name_to_outputs[network_name]: - output_buffers_info[output_name] = OutputLayerUtils(self._configured_net_group._hef, output_name, self._infer_pipeline, + output_buffers_info[output_name] = OutputLayerUtils(output_vstream_infos, output_name, self._infer_pipeline, self._net_group_name) output_tensor_info = output_buffers_info[output_name].output_tensor_info shape, dtype = output_tensor_info @@ -920,7 +933,7 @@ class InferVStreams(object): are output data tensors as :obj:`numpy.ndarray` (or list of :obj:`numpy.ndarray` in case of nms output and tf_nms_format=False). """ - time_before_infer_calcs = time.time() + time_before_infer_calcs = time.perf_counter() if not isinstance(input_data, dict): input_stream_infos = self._configured_net_group.get_input_stream_infos() if len(input_stream_infos) != 1: @@ -938,9 +951,9 @@ class InferVStreams(object): self._make_c_contiguous_if_needed(input_layer_name, input_data) with ExceptionWrapper(): - time_before_infer = time.time() + time_before_infer = time.perf_counter() self._infer_pipeline.infer(input_data, output_buffers, batch_size) - self._hw_time = time.time() - time_before_infer + self._hw_time = time.perf_counter() - time_before_infer for name, result_array in output_buffers.items(): is_nms = output_buffers_info[name].is_nms @@ -957,7 +970,7 @@ class InferVStreams(object): else: output_buffers[name] = HailoRTTransformUtils.output_raw_buffer_to_nms_format(result_array, nms_shape.number_of_classes) - self._total_time = time.time() - time_before_infer_calcs + self._total_time = time.perf_counter() - time_before_infer_calcs return output_buffers def get_hw_time(self): @@ -982,7 +995,7 @@ class InferVStreams(object): input_expected_dtype = self._infer_pipeline.get_host_dtype(input_layer_name) if input_dtype != input_expected_dtype: - self._logger.warning("Given input data dtype ({}) is different than inferred dtype ({}). " + default_logger().warning("Given input data dtype ({}) is different than inferred dtype ({}). " "conversion for every frame will reduce performance".format(input_dtype, input_expected_dtype)) input_data[input_layer_name] = input_data[input_layer_name].astype(input_expected_dtype) @@ -1015,7 +1028,7 @@ class InferVStreams(object): def _make_c_contiguous_if_needed(self, input_layer_name, input_data): if not input_data[input_layer_name].flags.c_contiguous: - self._logger.warning("Converting {} numpy array to be C_CONTIGUOUS".format( + default_logger().warning("Converting {} numpy array to be C_CONTIGUOUS".format( input_layer_name)) input_data[input_layer_name] = numpy.asarray(input_data[input_layer_name], order='C') @@ -1139,10 +1152,9 @@ class HailoRTTransformUtils(object): return FormatType.FLOAT32 raise HailoRTException("unsupported data type {}".format(dtype)) +# TODO: HRT-10427 - Remove class InternalEthernetDevice(object): def __init__(self, address, port, response_timeout_seconds=10, max_number_of_attempts=3): - # TODO: HRT-9987 - Add this deprecation warning - # default_logger().warning("InternalEthernetDevice is deprecated! Please use VDevice object.") self.device = None self._address = address self._port = port @@ -1204,7 +1216,7 @@ class PcieDeviceInfo(_pyhailort.PcieDeviceInfo): except HailoRTException: raise ArgumentTypeError('Invalid device info string, format is []::.') - +# TODO: HRT-10427 - Remove class InternalPcieDevice(object): def __init__(self, device_info=None): self.device = None @@ -1224,6 +1236,7 @@ class InternalPcieDevice(object): self.device.release() self.device = None + # TODO: HRT-10427 - Move to a static method in pyhailort_internal when InternalPcieDevice removed @staticmethod def scan_devices(): with ExceptionWrapper(): @@ -1242,7 +1255,7 @@ class InternalPcieDevice(object): with ExceptionWrapper(): return self.device.direct_read_memory(address, size) - +# TODO: HRT-10427 - Remove when removing InternalPcieDevice class PcieDebugLog(object): def __init__(self, pci_device): self._pcie_device = pci_device @@ -1300,7 +1313,7 @@ class HailoFormatFlags(_pyhailort.FormatFlags): SUPPORTED_PROTOCOL_VERSION = 2 SUPPORTED_FW_MAJOR = 4 -SUPPORTED_FW_MINOR = 13 +SUPPORTED_FW_MINOR = 14 SUPPORTED_FW_REVISION = 0 MEGA_MULTIPLIER = 1000.0 * 1000.0 @@ -1622,7 +1635,6 @@ class Control: def __init__(self, device: '_pyhailort.Device'): self.__device = device - self._logger = default_logger() # TODO: should remove? if sys.platform != "win32": @@ -2269,7 +2281,6 @@ class Device: """ gc.collect() - self._logger = default_logger() # Device __del__ function tries to release self._device. # to avoid AttributeError if the __init__ func fails, we set it to None first. # https://stackoverflow.com/questions/6409644/is-del-called-on-an-object-that-doesnt-complete-init @@ -2323,12 +2334,16 @@ class Device: Args: hef (:class:`~hailo_platform.pyhailort.pyhailort.HEF`): HEF to configure the vdevice from configure_params_by_name (dict, optional): Maps between each net_group_name to configure_params. If not provided, default params will be applied + + Note: + This function is deprecated. Support will be removed in future versions. """ + default_logger().warning("Usage of Device.configure is deprecated! One should use VDevice for inference") if self._creation_pid != os.getpid(): raise HailoRTException("Device can only be configured from the process it was created in.") with ExceptionWrapper(): - configured_apps = self._device.configure(hef._hef, configure_params_by_name) - configured_networks = [ConfiguredNetwork(configured_app, self, hef) for configured_app in configured_apps] + configured_ngs_handles = self._device.configure(hef._hef, configure_params_by_name) + configured_networks = [ConfiguredNetwork(configured_ng_handle) for configured_ng_handle in configured_ngs_handles] self._loaded_network_groups.extend(configured_networks) return configured_networks @@ -2385,7 +2400,6 @@ class VDevice(object): list of all available devices. Excludes 'params'. Cannot be used together with device_id. """ gc.collect() - self._logger = default_logger() # VDevice __del__ function tries to release self._vdevice. # to avoid AttributeError if the __init__ func fails, we set it to None first. @@ -2461,8 +2475,8 @@ class VDevice(object): if self._creation_pid != os.getpid(): raise HailoRTException("VDevice can only be configured from the process it was created in.") with ExceptionWrapper(): - configured_apps = self._vdevice.configure(hef._hef, configure_params_by_name) - configured_networks = [ConfiguredNetwork(configured_app, self, hef) for configured_app in configured_apps] + configured_ngs_handles = self._vdevice.configure(hef._hef, configure_params_by_name) + configured_networks = [ConfiguredNetwork(configured_ng_handle) for configured_ng_handle in configured_ngs_handles] self._loaded_network_groups.extend(configured_networks) return configured_networks @@ -2539,9 +2553,9 @@ class InputVStreamParams(object): timeout_ms = DEFAULT_VSTREAM_TIMEOUT_MS if queue_size is None: queue_size = DEFAULT_VSTREAM_QUEUE_SIZE - name = network_name if network_name is not None else configured_network.name + name = network_name if network_name is not None else "" with ExceptionWrapper(): - return configured_network._hef._hef.get_input_vstreams_params(name, quantized, + return configured_network._configured_network.make_input_vstream_params(name, quantized, format_type, timeout_ms, queue_size) @staticmethod @@ -2613,9 +2627,9 @@ class OutputVStreamParams(object): timeout_ms = DEFAULT_VSTREAM_TIMEOUT_MS if queue_size is None: queue_size = DEFAULT_VSTREAM_QUEUE_SIZE - name = network_name if network_name is not None else configured_network.name + name = network_name if network_name is not None else "" with ExceptionWrapper(): - return configured_network._hef._hef.get_output_vstreams_params(name, quantized, + return configured_network._configured_network.make_output_vstream_params(name, quantized, format_type, timeout_ms, queue_size) @staticmethod @@ -2820,8 +2834,8 @@ class InputVStreams(object): class OutputLayerUtils(object): - def __init__(self, hef, vstream_name, pipeline, net_group_name=""): - self._hef = hef + def __init__(self, output_vstream_infos, vstream_name, pipeline, net_group_name=""): + self._output_vstream_infos = output_vstream_infos self._vstream_info = self._get_vstream_info(net_group_name, vstream_name) if isinstance(pipeline, (_pyhailort.InferVStreams)): @@ -2866,8 +2880,7 @@ class OutputLayerUtils(object): return self._quantized_empty_bbox def _get_vstream_info(self, net_group_name, vstream_name): - output_vstream_infos = self._hef.get_output_vstream_infos(net_group_name) - for info in output_vstream_infos: + for info in self._output_vstream_infos: if info.name == vstream_name: return info raise HailoRTException("No vstream matches the given name {}".format(vstream_name)) @@ -2885,7 +2898,8 @@ class OutputVStream(object): def __init__(self, configured_network, recv_object, name, tf_nms_format=False, net_group_name=""): self._recv_object = recv_object - self._output_layer_utils = OutputLayerUtils(configured_network._hef, name, self._recv_object, net_group_name) + output_vstream_infos = configured_network.get_output_vstream_infos() + self._output_layer_utils = OutputLayerUtils(output_vstream_infos, name, self._recv_object, net_group_name) self._output_dtype = self._output_layer_utils.output_dtype self._vstream_info = self._output_layer_utils._vstream_info self._output_tensor_info = self._output_layer_utils.output_tensor_info @@ -3030,15 +3044,3 @@ class OutputVStreams(object): def _after_fork_in_child(self): for vstream in self._vstreams.values(): vstream._after_fork_in_child() - - -class YOLOv5PostProcessOp(object): - - def __init__(self, anchors, shapes, formats, quant_infos, image_height, image_width, confidence_threshold, iou_threshold, num_of_classes, - max_boxes, cross_classes=True): - - self._op = _pyhailort.YOLOv5PostProcessOp.create(anchors, shapes, formats, quant_infos, image_height, image_width, confidence_threshold, - iou_threshold, num_of_classes, max_boxes, cross_classes) - - def execute(self, net_flow_tensors): - return self._op.execute(net_flow_tensors) \ No newline at end of file diff --git a/hailort/libhailort/bindings/python/platform/hailo_platform/tools/udp_rate_limiter.py b/hailort/libhailort/bindings/python/platform/hailo_platform/tools/udp_rate_limiter.py index b597e32..c9d4035 100644 --- a/hailort/libhailort/bindings/python/platform/hailo_platform/tools/udp_rate_limiter.py +++ b/hailort/libhailort/bindings/python/platform/hailo_platform/tools/udp_rate_limiter.py @@ -6,10 +6,9 @@ from __future__ import division from builtins import object -from hailo_platform.pyhailort.pyhailort import ConfiguredNetwork, HEF, TrafficControl, INPUT_DATAFLOW_BASE_PORT +from hailo_platform.pyhailort.pyhailort import HEF, NetworkRateLimiter, INPUT_DATAFLOW_BASE_PORT DEFAULT_MAX_KBPS = 850e3 -DEFAULT_MAX_KBPS_PAPRIKA_B0 = 160e3 BYTES_IN_Kbits = 125.0 @@ -28,15 +27,9 @@ class BadTCCallError(Exception): pass -def get_max_supported_kbps(hw_arch="hailo8"): - # TODO: What should be here? - if hw_arch == "paprika_b0": - return DEFAULT_MAX_KBPS_PAPRIKA_B0 - return DEFAULT_MAX_KBPS - class RateLimiterWrapper(object): """UDPRateLimiter wrapper enabling ``with`` statements.""" - def __init__(self, network_group, fps=1, fps_factor=1.0, remote_ip=None, hw_arch=None): + def __init__(self, configured_network_group, fps=1, fps_factor=1.0, remote_ip=None): """RateLimiterWrapper constructor. Args: @@ -44,32 +37,24 @@ class RateLimiterWrapper(object): target network_group. fps (int): Frame rate. fps_factor (float): Safety factor by which to multiply the calculated UDP rate. + remote_ip (str): Device IP address. """ - if not isinstance(network_group, ConfiguredNetwork): - return RateLimiterException("The API was changed. RateLimiterWrapper accept ConfiguredNetwork instead of ActivatedNetwork") - self._network_group = network_group - if remote_ip is not None: - self._remote_ip = remote_ip - else: - # this line should be removed. this parameter will be removed from the object - self._remote_ip = network_group._target.device_id + self._network_group = configured_network_group + if remote_ip is None: + raise RateLimiterException("In order to use RateLimiterWrapper, one should pass 'remote_ip'") + self._remote_ip = remote_ip self._fps = fps self._fps_factor = fps_factor - if hw_arch is not None: - self._hw_arch = hw_arch - else: - # this line should be removed. this parameter will be removed from the object - self._hw_arch = network_group._target._hw_arch if hasattr(network_group._target, '_hw_arch') else None self._rates_dict = {} self._tc_dict = {} def __enter__(self): - max_supported_kbps_rate = get_max_supported_kbps(self._hw_arch) + max_supported_kbps_rate = DEFAULT_MAX_KBPS self._rates_dict = self._network_group.get_udp_rates_dict((self._fps * self._fps_factor), (max_supported_kbps_rate * BYTES_IN_Kbits)) for port, rate in self._rates_dict.items(): - self._tc_dict[port] = TrafficControl(self._remote_ip, port, rate) + self._tc_dict[port] = NetworkRateLimiter(self._remote_ip, port, rate) self._tc_dict[port].reset_rate_limit() self._tc_dict[port].set_rate_limit() @@ -82,7 +67,7 @@ class RateLimiterWrapper(object): class UDPRateLimiter(object): """Enables limiting or removing limits on UDP communication rate to a board.""" def __init__(self, remote_ip, port, rate_kbits_per_sec = 0): - self._tc = TrafficControl(remote_ip, port, rate_kbits_per_sec * BYTES_IN_Kbits) + self._tc = NetworkRateLimiter(remote_ip, port, rate_kbits_per_sec * BYTES_IN_Kbits) def set_rate_limit(self): return self._tc.set_rate_limit() diff --git a/hailort/libhailort/bindings/python/platform/hailo_tutorials/notebooks/HRT_0_Inference_Tutorial.ipynb b/hailort/libhailort/bindings/python/platform/hailo_tutorials/notebooks/HRT_0_Inference_Tutorial.ipynb index 8a7785c..d376d08 100644 --- a/hailort/libhailort/bindings/python/platform/hailo_tutorials/notebooks/HRT_0_Inference_Tutorial.ipynb +++ b/hailort/libhailort/bindings/python/platform/hailo_tutorials/notebooks/HRT_0_Inference_Tutorial.ipynb @@ -126,8 +126,8 @@ "outputs": [], "source": [ "def send(configured_network, num_frames):\n", - " vstreams_params = InputVStreamParams.make(configured_network)\n", " configured_network.wait_for_activation(1000)\n", + " vstreams_params = InputVStreamParams.make(configured_network)\n", " with InputVStreams(configured_network, vstreams_params) as vstreams:\n", " vstream_to_buffer = {vstream: np.ndarray([1] + list(vstream.shape), dtype=vstream.dtype) for vstream in vstreams}\n", " for _ in range(num_frames):\n", diff --git a/hailort/libhailort/bindings/python/platform/hailo_tutorials/notebooks/HRT_1_Power_Measurement_Tutorial.ipynb b/hailort/libhailort/bindings/python/platform/hailo_tutorials/notebooks/HRT_1_Power_Measurement_Tutorial.ipynb index b361e0d..d4edead 100644 --- a/hailort/libhailort/bindings/python/platform/hailo_tutorials/notebooks/HRT_1_Power_Measurement_Tutorial.ipynb +++ b/hailort/libhailort/bindings/python/platform/hailo_tutorials/notebooks/HRT_1_Power_Measurement_Tutorial.ipynb @@ -25,8 +25,8 @@ "source": [ "## Single power measurement" ], - "cell_type": "markdown", - "metadata": {} + "cell_type": "markdown", + "metadata": {} }, { "cell_type": "code", @@ -37,7 +37,7 @@ "%matplotlib inline\n", "import time\n", "\n", - "from hailo_platform import PcieDevice, DvmTypes, PowerMeasurementTypes, SamplingPeriod, AveragingFactor, MeasurementBufferIndex # noqa F401\n" + "from hailo_platform import Device, DvmTypes, PowerMeasurementTypes, SamplingPeriod, AveragingFactor, MeasurementBufferIndex # noqa F401\n" ] }, { @@ -53,7 +53,7 @@ "metadata": {}, "outputs": [], "source": [ - "target = PcieDevice()" + "target = Device()" ] }, { diff --git a/hailort/libhailort/bindings/python/platform/hailo_tutorials/notebooks/HRT_2_Inference_Tutorial_Multi_Process_Service.ipynb b/hailort/libhailort/bindings/python/platform/hailo_tutorials/notebooks/HRT_2_Inference_Tutorial_Multi_Process_Service.ipynb new file mode 100644 index 0000000..05fc1bc --- /dev/null +++ b/hailort/libhailort/bindings/python/platform/hailo_tutorials/notebooks/HRT_2_Inference_Tutorial_Multi_Process_Service.ipynb @@ -0,0 +1,130 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "# Python inference tutorial - Multi Process Service and Model Scheduler\n", + "\n", + "This tutorial will walk you through the inference process using The Model Scheduler.\n", + "\n", + "**Requirements:**\n", + "\n", + "* Run HailoRT Multi-Process Service before running inference. See installation steps in [Multi-Process Service](../../inference/inference.rst)\n", + "* Run the notebook inside the Python virtual environment: ```source hailo_virtualenv/bin/activate```\n", + "\n", + "It is recommended to use the command ``hailo tutorial`` (when inside the virtualenv) to open a Jupyter server that contains the tutorials." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Running Inference using HailoRT\n", + "\n", + "In this example we will use the Model Scheduler to run inference on multiple models.\n", + "Each model is represented by an HEF which is built using the Hailo Dataflow Compiler.\n", + "An HEF is Hailo's binary format for neural networks. The HEF files contain:\n", + "\n", + "* Target HW configuration\n", + "* Weights\n", + "* Metadata for HailoRT (e.g. input/output scaling)\n", + "\n", + "The Model Scheduler is an HailoRT component that comes to enhance and simplify the usage\n", + "of the same Hailo device by multiple networks. The responsibility for activating/deactivating the network\n", + "groups is now under HailoRT, and done **automatically** without user application intervention.\n", + "In order to use the Model Scheduler, create the VDevice with scheduler enabled, configure all models to the device, and start inference on all models:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from multiprocessing import Process\n", + "from hailo_platform import (HEF, VDevice, HailoStreamInterface, InferVStreams, ConfigureParams,\n", + " InputVStreamParams, OutputVStreamParams, InputVStreams, OutputVStreams, FormatType, HailoSchedulingAlgorithm)\n", + "\n", + "\n", + "# Define the function to run inference on the model\n", + "def infer(network_group, input_vstreams_params, output_vstreams_params, input_data):\n", + " rep_count = 100\n", + " with InferVStreams(network_group, input_vstreams_params, output_vstreams_params) as infer_pipeline:\n", + " for i in range(rep_count):\n", + " infer_results = infer_pipeline.infer(input_data)\n", + "\n", + "\n", + "# Loading compiled HEFs:\n", + "first_hef_path = '../hefs/resnet_v1_18.hef'\n", + "second_hef_path = '../hefs/shortcut_net.hef'\n", + "first_hef = HEF(first_hef_path)\n", + "second_hef = HEF(second_hef_path)\n", + "hefs = [first_hef, second_hef]\n", + "\n", + "# Creating the VDevice target with scheduler enabled\n", + "params = VDevice.create_params()\n", + "params.scheduling_algorithm = HailoSchedulingAlgorithm.ROUND_ROBIN\n", + "with VDevice(params) as target:\n", + " infer_processes = []\n", + "\n", + " # Configure network groups\n", + " for hef in hefs:\n", + " configure_params = ConfigureParams.create_from_hef(hef=hef, interface=HailoStreamInterface.PCIe)\n", + " network_groups = target.configure(hef, configure_params)\n", + " network_group = network_groups[0]\n", + "\n", + " # Create input and output virtual streams params\n", + " # Quantized argument signifies whether or not the incoming data is already quantized.\n", + " # Data is quantized by HailoRT if and only if quantized == False.\n", + " input_vstreams_params = InputVStreamParams.make(network_group, quantized=False, format_type=FormatType.FLOAT32)\n", + " output_vstreams_params = OutputVStreamParams.make(network_group, quantized=True, format_type=FormatType.UINT8)\n", + "\n", + " # Define dataset params\n", + " input_vstream_info = hef.get_input_vstream_infos()[0]\n", + " image_height, image_width, channels = input_vstream_info.shape\n", + " num_of_frames = 10\n", + " low, high = 2, 20\n", + "\n", + " # Generate random dataset\n", + " dataset = np.random.randint(low, high, (num_of_frames, image_height, image_width, channels)).astype(np.float32)\n", + " input_data = {input_vstream_info.name: dataset}\n", + "\n", + " # Create infer process\n", + " infer_process = Process(target=infer, args=(network_group, input_vstreams_params, output_vstreams_params, input_data))\n", + " infer_processes.append(infer_process)\n", + "\n", + " print(f'Starting streaming on multiple models using scheduler')\n", + " for infer_process in infer_processes:\n", + " infer_process.start()\n", + " for infer_process in infer_processes:\n", + " infer_process.join()\n", + "\n", + " print('Done inference')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/hailort/libhailort/bindings/python/platform/requirements.txt b/hailort/libhailort/bindings/python/platform/requirements.txt index c11aa35..bce1a18 100644 --- a/hailort/libhailort/bindings/python/platform/requirements.txt +++ b/hailort/libhailort/bindings/python/platform/requirements.txt @@ -1,8 +1,8 @@ appdirs==1.4.4 argcomplete==2.0.0 contextlib2==0.6.0.post1 -distlib==0.3.4 -filelock==3.4.1 +distlib==0.3.6 +filelock==3.8.0 future==0.18.2 importlib-metadata==5.1.0 importlib-resources==5.1.2 @@ -11,4 +11,4 @@ netifaces==0.10.9 numpy==1.23.3 typing_extensions==4.1.1 verboselogs==1.7 -virtualenv==20.4.3 +virtualenv==20.17.0 diff --git a/hailort/libhailort/bindings/python/platform/setup.py b/hailort/libhailort/bindings/python/platform/setup.py index 445e867..425291b 100644 --- a/hailort/libhailort/bindings/python/platform/setup.py +++ b/hailort/libhailort/bindings/python/platform/setup.py @@ -69,6 +69,6 @@ if __name__ == "__main__": "linux_aarch64", ], url="https://hailo.ai/", - version="4.13.0", + version="4.14.0", zip_safe=False, ) diff --git a/hailort/libhailort/bindings/python/src/CMakeLists.txt b/hailort/libhailort/bindings/python/src/CMakeLists.txt index bd032c4..0f170a8 100644 --- a/hailort/libhailort/bindings/python/src/CMakeLists.txt +++ b/hailort/libhailort/bindings/python/src/CMakeLists.txt @@ -1,12 +1,23 @@ cmake_minimum_required(VERSION 3.0.0) -option(HAILO_BUILD_PYHAILORT_INTERNAL OFF) +include(ExternalProject) + +FUNCTION(exclude_archive_libs_symbols target) # should be same as in common_compiler_options.cmake + if(WIN32) + # TODO: check if there are required actions for Windows + elseif(UNIX) + get_property(TEMP_LINK_FLAGS TARGET ${target} PROPERTY LINK_FLAGS) + set(TEMP_LINK_FLAGS "${TEMP_LINK_FLAGS} -Wl,--exclude-libs=ALL") + set_property(TARGET ${target} PROPERTY LINK_FLAGS ${TEMP_LINK_FLAGS}) + else() + message(FATAL_ERROR "Unexpeced host, stopping build") + endif() +ENDFUNCTION() if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") if(NOT DEFINED PYBIND11_PYTHON_VERSION) message(FATAL_ERROR "PYBIND11_PYTHON_VERSION is not defined. To build _pyhailort, pass python version") endif() - string(REPLACE "." "" dpython ${PYBIND11_PYTHON_VERSION}) # E.g "3.5" -> "35" if(${dpython} LESS "38") set(m_flag "m") @@ -16,6 +27,8 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") set(PYTHON_MODULE_EXTENSION ".cpython-${dpython}${m_flag}-${CMAKE_SYSTEM_PROCESSOR}-linux-gnu.so") endif() +option(HAILO_BUILD_PYHAILORT_INTERNAL OFF) + set(PYHAILORT_DIR ${CMAKE_CURRENT_LIST_DIR}) pybind11_add_module(_pyhailort @@ -24,29 +37,27 @@ pybind11_add_module(_pyhailort hef_api.cpp vstream_api.cpp quantization_api.cpp - ${HAILORT_OPS_CPP_SOURCES} - ${HAILORT_COMMON_CPP_SOURCES} ) set_target_properties(_pyhailort PROPERTIES CXX_STANDARD 14 CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO + C_VISIBILITY_PRESET hidden + CXX_VISIBILITY_PRESET hidden + # VISIBILITY_INLINES_HIDDEN YES ) -target_include_directories(_pyhailort - PRIVATE - $ - $ - $ - $ -) +find_package(HailoRT 4.14.0 EXACT REQUIRED) -target_link_libraries(_pyhailort PRIVATE libhailort spdlog::spdlog) +target_link_libraries(_pyhailort PRIVATE HailoRT::libhailort) if(WIN32) - target_link_libraries(_pyhailort PRIVATE Ws2_32 Iphlpapi Shlwapi) -endif() -if(HAILO_BUILD_SERVICE) - target_link_libraries(_pyhailort PRIVATE grpc++_unsecure hailort_rpc_grpc_proto hef_proto) + target_link_libraries(_pyhailort PRIVATE Ws2_32) + target_compile_options(_pyhailort PRIVATE + /DWIN32_LEAN_AND_MEAN + /DNOMINMAX # NOMINMAX is required in order to play nice with std::min/std::max (otherwise Windows.h defines it's own) + /wd4201 /wd4251 + ) endif() target_compile_options(_pyhailort PRIVATE ${HAILORT_COMPILE_OPTIONS}) diff --git a/hailort/libhailort/bindings/python/src/bindings_common.hpp b/hailort/libhailort/bindings/python/src/bindings_common.hpp index 513175b..6d87df9 100644 --- a/hailort/libhailort/bindings/python/src/bindings_common.hpp +++ b/hailort/libhailort/bindings/python/src/bindings_common.hpp @@ -14,8 +14,6 @@ #include "hailo/hailort_common.hpp" #include "hailo/network_group.hpp" -#include "common/logger_macros.hpp" - #include "utils.hpp" #include diff --git a/hailort/libhailort/bindings/python/src/device_api.cpp b/hailort/libhailort/bindings/python/src/device_api.cpp index c6f4a5b..6315daf 100644 --- a/hailort/libhailort/bindings/python/src/device_api.cpp +++ b/hailort/libhailort/bindings/python/src/device_api.cpp @@ -9,6 +9,7 @@ **/ #include "device_api.hpp" +#include namespace hailort @@ -39,28 +40,7 @@ DeviceWrapper DeviceWrapper::create_pcie(hailo_pcie_device_info_t &device_info) DeviceWrapper DeviceWrapper::create_eth(const std::string &device_address, uint16_t port, uint32_t timeout_milliseconds, uint8_t max_number_of_attempts) { - hailo_eth_device_info_t device_info = {}; - - /* Validate address length */ - if (INET_ADDRSTRLEN < device_address.size()) { - EXIT_WITH_ERROR("device_address is too long") - } - - device_info.host_address.sin_family = AF_INET; - device_info.host_address.sin_port = HAILO_ETH_PORT_ANY; - auto status = Socket::pton(AF_INET, HAILO_ETH_ADDRESS_ANY, &(device_info.host_address.sin_addr)); - VALIDATE_STATUS(status); - - device_info.device_address.sin_family = AF_INET; - device_info.device_address.sin_port = port; - status = Socket::pton(AF_INET, device_address.c_str(), &(device_info.device_address.sin_addr)); - VALIDATE_STATUS(status); - - device_info.timeout_millis = timeout_milliseconds; - device_info.max_number_of_attempts = max_number_of_attempts; - device_info.max_payload_size = HAILO_DEFAULT_ETH_MAX_PAYLOAD_SIZE; - - auto device = Device::create_eth(device_info); + auto device = Device::create_eth(device_address, port, timeout_milliseconds, max_number_of_attempts); VALIDATE_EXPECTED(device); return DeviceWrapper(device.release()); @@ -125,7 +105,7 @@ bool DeviceWrapper::get_overcurrent_state() py::bytes DeviceWrapper::read_memory(uint32_t address, uint32_t length) { - std::unique_ptr response = make_unique_nothrow(length, '\x00'); + std::unique_ptr response = std::make_unique(length, '\x00'); VALIDATE_NOT_NULL(response, HAILO_OUT_OF_HOST_MEMORY); MemoryView data_view(const_cast(reinterpret_cast(response->data())), length); @@ -162,7 +142,7 @@ py::bytes DeviceWrapper::i2c_read(hailo_i2c_slave_config_t *slave_config, uint32 { VALIDATE_NOT_NULL(slave_config, HAILO_INVALID_ARGUMENT); - std::unique_ptr response = make_unique_nothrow(length, '\x00'); + std::unique_ptr response = std::make_unique(length, '\x00'); VALIDATE_NOT_NULL(response, HAILO_OUT_OF_HOST_MEMORY); MemoryView data_view(const_cast(reinterpret_cast(response->data())), length); @@ -229,7 +209,7 @@ py::bytes DeviceWrapper::read_user_config() auto config_buffer = device().read_user_config(); VALIDATE_EXPECTED(config_buffer); - std::unique_ptr response = make_unique_nothrow( + std::unique_ptr response = std::make_unique( const_cast(reinterpret_cast(config_buffer->data())), config_buffer->size()); VALIDATE_NOT_NULL(response, HAILO_OUT_OF_HOST_MEMORY); @@ -255,7 +235,7 @@ py::bytes DeviceWrapper::read_board_config() auto config_buffer = device().read_board_config(); VALIDATE_EXPECTED(config_buffer); - std::unique_ptr response = make_unique_nothrow( + std::unique_ptr response = std::make_unique( const_cast(reinterpret_cast(config_buffer->data())), config_buffer->size()); VALIDATE_NOT_NULL(response, HAILO_OUT_OF_HOST_MEMORY); @@ -307,7 +287,7 @@ py::bytes DeviceWrapper::sensor_get_sections_info() auto buffer = device().sensor_get_sections_info(); VALIDATE_EXPECTED(buffer); - std::unique_ptr response = make_unique_nothrow( + std::unique_ptr response = std::make_unique( const_cast(reinterpret_cast(buffer->data())), buffer->size()); VALIDATE_NOT_NULL(response, HAILO_OUT_OF_HOST_MEMORY); diff --git a/hailort/libhailort/bindings/python/src/device_api.hpp b/hailort/libhailort/bindings/python/src/device_api.hpp index b216cce..d357316 100644 --- a/hailort/libhailort/bindings/python/src/device_api.hpp +++ b/hailort/libhailort/bindings/python/src/device_api.hpp @@ -12,10 +12,9 @@ #define _DEVICE_API_HPP_ #include "hailo/hailort.h" +#include #include "hailo/device.hpp" -#include "common/socket.hpp" - #include "utils.hpp" #include "hef_api.hpp" diff --git a/hailort/libhailort/bindings/python/src/hef_api.cpp b/hailort/libhailort/bindings/python/src/hef_api.cpp index f255768..5644f70 100644 --- a/hailort/libhailort/bindings/python/src/hef_api.cpp +++ b/hailort/libhailort/bindings/python/src/hef_api.cpp @@ -10,6 +10,7 @@ **/ #include "hef_api.hpp" +#include namespace hailort @@ -20,7 +21,7 @@ HefWrapper::HefWrapper(const std::string &hef_path) auto hef_expected = Hef::create(hef_path); VALIDATE_EXPECTED(hef_expected); - hef = make_unique_nothrow(hef_expected.release()); + hef = std::make_unique(hef_expected.release()); if (nullptr == hef) { THROW_STATUS_ERROR(HAILO_OUT_OF_HOST_MEMORY); } @@ -31,7 +32,7 @@ HefWrapper::HefWrapper(const MemoryView &hef_buffer) auto hef_expected = Hef::create(hef_buffer); VALIDATE_EXPECTED(hef_expected); - hef = make_unique_nothrow(hef_expected.release()); + hef = std::make_unique(hef_expected.release()); if (nullptr == hef) { THROW_STATUS_ERROR(HAILO_OUT_OF_HOST_MEMORY); } @@ -255,7 +256,11 @@ void HefWrapper::initialize_python_module(py::module &m) .def("get_networks_names", &HefWrapper::get_networks_names) ; - py::class_(m, "ConfiguredNetworkGroup") + py::class_>(m, "ConfiguredNetworkGroup") + .def("is_scheduled", [](ConfiguredNetworkGroup& self) + { + return self.is_scheduled(); + }) .def("get_name", [](ConfiguredNetworkGroup& self) { return self.name(); @@ -300,30 +305,18 @@ void HefWrapper::initialize_python_module(py::module &m) }) .def("before_fork", [](ConfiguredNetworkGroup& self) { -#ifdef HAILO_SUPPORT_MULTI_PROCESS auto status = self.before_fork(); VALIDATE_STATUS(status); -#else - (void)self; -#endif // HAILO_SUPPORT_MULTI_PROCESS }) .def("after_fork_in_parent", [](ConfiguredNetworkGroup& self) { -#ifdef HAILO_SUPPORT_MULTI_PROCESS auto status = self.after_fork_in_parent(); VALIDATE_STATUS(status); -#else - (void)self; -#endif // HAILO_SUPPORT_MULTI_PROCESS }) .def("after_fork_in_child", [](ConfiguredNetworkGroup& self) { -#ifdef HAILO_SUPPORT_MULTI_PROCESS auto status = self.after_fork_in_child(); VALIDATE_STATUS(status); -#else - (void)self; -#endif // HAILO_SUPPORT_MULTI_PROCESS }) .def("set_scheduler_timeout", [](ConfiguredNetworkGroup& self, int timeout, const std::string &network_name="") { @@ -341,6 +334,112 @@ void HefWrapper::initialize_python_module(py::module &m) auto status = self.set_scheduler_priority(priority); VALIDATE_STATUS(status); }) + .def("get_networks_names", [](ConfiguredNetworkGroup& self) + { + auto network_infos = self.get_network_infos(); + VALIDATE_EXPECTED(network_infos); + std::vector result; + result.reserve(network_infos->size()); + for (const auto &info : network_infos.value()) { + result.push_back(info.name); + } + return py::cast(result); + }) + .def("get_sorted_output_names", [](ConfiguredNetworkGroup& self) + { + auto names_list = self.get_sorted_output_names(); + VALIDATE_EXPECTED(names_list); + return py::cast(names_list.release()); + }) + .def("get_input_vstream_infos", [](ConfiguredNetworkGroup& self, const std::string &name) + { + auto result = self.get_input_vstream_infos(name); + VALIDATE_EXPECTED(result); + return py::cast(result.value()); + }) + .def("get_output_vstream_infos", [](ConfiguredNetworkGroup& self, const std::string &name) + { + auto result = self.get_output_vstream_infos(name); + VALIDATE_EXPECTED(result); + return py::cast(result.value()); + }) + .def("get_all_vstream_infos", [](ConfiguredNetworkGroup& self, const std::string &name) + { + auto result = self.get_all_vstream_infos(name); + VALIDATE_EXPECTED(result); + return py::cast(result.value()); + }) + .def("get_all_stream_infos", [](ConfiguredNetworkGroup& self, const std::string &name) + { + auto result = self.get_all_stream_infos(name); + VALIDATE_EXPECTED(result); + return py::cast(result.value()); + }) + .def("get_input_stream_infos", [](ConfiguredNetworkGroup& self, const std::string &name) + { + std::vector input_streams_infos; + auto all_streams = self.get_all_stream_infos(name); + VALIDATE_EXPECTED(all_streams); + for (auto &info : all_streams.value()) { + if (HAILO_H2D_STREAM == info.direction) { + input_streams_infos.push_back(std::move(info)); + } + } + return py::cast(input_streams_infos); + }) + .def("get_output_stream_infos", [](ConfiguredNetworkGroup& self, const std::string &name) + { + std::vector output_streams_infos; + auto all_streams = self.get_all_stream_infos(name); + VALIDATE_EXPECTED(all_streams); + for (auto &info : all_streams.value()) { + if (HAILO_D2H_STREAM == info.direction) { + output_streams_infos.push_back(std::move(info)); + } + } + return py::cast(output_streams_infos); + }) + .def("get_vstream_names_from_stream_name", [](ConfiguredNetworkGroup& self, const std::string &stream_name) + { + auto result = self.get_vstream_names_from_stream_name(stream_name); + VALIDATE_EXPECTED(result); + return py::cast(result.release()); + }) + .def("get_stream_names_from_vstream_name", [](ConfiguredNetworkGroup& self, const std::string &vstream_name) + { + auto result = self.get_stream_names_from_vstream_name(vstream_name); + VALIDATE_EXPECTED(result); + return py::cast(result.release()); + }) + .def("make_input_vstream_params", [](ConfiguredNetworkGroup& self, const std::string &name, bool quantized, hailo_format_type_t format_type, + uint32_t timeout_ms, uint32_t queue_size) + { + auto result = self.make_input_vstream_params(quantized, format_type, timeout_ms, queue_size, name); + VALIDATE_EXPECTED(result); + return py::cast(result.release()); + }) + .def("make_output_vstream_params", [](ConfiguredNetworkGroup& self, const std::string &name, bool quantized, hailo_format_type_t format_type, + uint32_t timeout_ms, uint32_t queue_size) + { + auto result = self.make_output_vstream_params(quantized, format_type, timeout_ms, queue_size, name); + VALIDATE_EXPECTED(result); + return py::cast(result.release()); + }) + .def(py::pickle( + [](const ConfiguredNetworkGroup &cng) { // __getstate__ + auto handle = cng.get_client_handle(); + VALIDATE_EXPECTED(handle); + return py::make_tuple(handle.value(), cng.name()); + }, + [](py::tuple t) { // __setstate__ + auto handle = t[0].cast(); + auto net_group_name = t[1].cast(); + auto net_group = ConfiguredNetworkGroup::duplicate_network_group_client(handle, net_group_name); + VALIDATE_EXPECTED(net_group); + + return net_group.value(); + } + )) ; ActivatedAppContextManagerWrapper::add_to_python_module(m); diff --git a/hailort/libhailort/bindings/python/src/hef_api.hpp b/hailort/libhailort/bindings/python/src/hef_api.hpp index 4de0a3c..87b4069 100644 --- a/hailort/libhailort/bindings/python/src/hef_api.hpp +++ b/hailort/libhailort/bindings/python/src/hef_api.hpp @@ -18,7 +18,6 @@ #include "vstream_api.hpp" #include "utils.hpp" -#include "common/logger_macros.hpp" #include #include diff --git a/hailort/libhailort/bindings/python/src/internal/pyhailort_internal.cpp b/hailort/libhailort/bindings/python/src/internal/pyhailort_internal.cpp index b81794f..fe7515d 100644 --- a/hailort/libhailort/bindings/python/src/internal/pyhailort_internal.cpp +++ b/hailort/libhailort/bindings/python/src/internal/pyhailort_internal.cpp @@ -173,12 +173,15 @@ py::array PyhailortInternal::get_yolov5_post_process_expected_buffer() auto buffer = get_expected_buffer_float32(); VALIDATE_EXPECTED(buffer); + auto type = py::dtype(HailoRTBindingsCommon::convert_format_type_to_string(HAILO_FORMAT_TYPE_FLOAT32)); + auto shape = *py::array::ShapeContainer({buffer->size()}); + // Note: The ownership of the buffer is transferred to Python wrapped as a py::array. // When the py::array isn't referenced anymore in Python and is destructed, the py::capsule's dtor // is called too (and it deletes the raw buffer) - auto type = py::dtype(HailoRTBindingsCommon::convert_format_type_to_string(HAILO_FORMAT_TYPE_FLOAT32)); - auto shape = *py::array::ShapeContainer({buffer->size()}); - const auto unmanaged_addr = buffer.release().release(); + auto unmanaged_addr_exp = buffer->storage().release(); + VALIDATE_EXPECTED(unmanaged_addr_exp); + const auto unmanaged_addr = unmanaged_addr_exp.release(); return py::array(type, shape, unmanaged_addr, py::capsule(unmanaged_addr, [](void *p) { delete reinterpret_cast(p); })); } @@ -277,7 +280,7 @@ py::list PyhailortInternal::get_all_layers_info(const HefWrapper &hef, const std auto core_op_metadata = hef.hef_ptr()->pimpl->get_core_op_metadata(net_group_name); VALIDATE_EXPECTED(core_op_metadata); - return py::cast(core_op_metadata->get_all_layer_infos()); + return py::cast(core_op_metadata.value()->get_all_layer_infos()); } PYBIND11_MODULE(_pyhailort_internal, m) { @@ -296,6 +299,13 @@ PYBIND11_MODULE(_pyhailort_internal, m) { .def_readonly("cluster_index", &BufferIndices::cluster_index) ; + py::enum_(m, "SensorConfigOpCode") + .value("SENSOR_CONFIG_OPCODES_WR", SENSOR_CONFIG_OPCODES_WR) + .value("SENSOR_CONFIG_OPCODES_RD", SENSOR_CONFIG_OPCODES_RD) + .value("SENSOR_CONFIG_OPCODES_RMW", SENSOR_CONFIG_OPCODES_RMW) + .value("SENSOR_CONFIG_OPCODES_DELAY", SENSOR_CONFIG_OPCODES_DELAY) + ; + py::class_(m, "HailoLayerInfo", py::module_local()) .def_readonly("is_mux", &LayerInfo::is_mux) .def_readonly("mux_predecessors", &LayerInfo::predecessor) diff --git a/hailort/libhailort/bindings/python/src/net_flow_api.hpp b/hailort/libhailort/bindings/python/src/net_flow_api.hpp deleted file mode 100644 index df29b36..0000000 --- a/hailort/libhailort/bindings/python/src/net_flow_api.hpp +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Copyright (c) 2022 Hailo Technologies Ltd. All rights reserved. - * Distributed under the MIT license (https://opensource.org/licenses/MIT) -**/ -/** - * @file net_flow_api.hpp - * @brief Defines binding to a HailoRT++ ops usage over Python. - **/ - -#ifndef _HAILO_NET_FLOW_API_HPP_ -#define _HAILO_NET_FLOW_API_HPP_ - -#include "hailo/hailort.h" - -#include "net_flow/ops/yolo_post_process.hpp" - -#include "utils.hpp" -#include "bindings_common.hpp" - - -namespace hailort -{ -namespace net_flow -{ - -class YOLOv5PostProcessOpWrapper -{ -public: - static YOLOv5PostProcessOpWrapper create(const std::vector> &anchors, - const std::vector &shapes, const std::vector &formats, - const std::vector &quant_infos, float32_t image_height, float32_t image_width, float32_t confidence_threshold, - float32_t iou_threshold, uint32_t num_of_classes, uint32_t max_boxes, - bool cross_classes=true) - { - std::map inputs_metadata; - std::map outputs_metadata; - - net_flow::NmsPostProcessConfig nms_post_process_config{}; - nms_post_process_config.nms_score_th = confidence_threshold; - nms_post_process_config.nms_iou_th = iou_threshold; - nms_post_process_config.max_proposals_per_class = max_boxes; - nms_post_process_config.classes = num_of_classes; - nms_post_process_config.background_removal = false; - nms_post_process_config.background_removal_index = 0; - nms_post_process_config.cross_classes = cross_classes; - net_flow::YoloPostProcessConfig yolo_post_process_config{}; - yolo_post_process_config.image_height = image_height; - yolo_post_process_config.image_width = image_width; - // Each layer anchors vector is structured as {w,h} pairs. - for (size_t i = 0; i < anchors.size(); ++i) { - auto name = std::to_string(i); - yolo_post_process_config.anchors.insert({name, anchors[i]}); - BufferMetaData input_metadata = { - shapes[i], - shapes[i], - formats[i], - quant_infos[i] - }; - inputs_metadata.insert({name, input_metadata}); - } - auto op = YOLOv5PostProcessOp::create(inputs_metadata, outputs_metadata, nms_post_process_config, yolo_post_process_config); - VALIDATE_EXPECTED(op); - - return YOLOv5PostProcessOpWrapper(op.release(), num_of_classes, max_boxes); - } - - static void add_to_python_module(py::module &m) - { - py::class_(m, "YOLOv5PostProcessOp") - .def("create", &YOLOv5PostProcessOpWrapper::create) - .def("execute",[](YOLOv5PostProcessOpWrapper &self, const std::vector &tensors) - { - std::map data_views; - for (size_t i = 0; i < tensors.size(); ++i) { - data_views.insert({std::to_string(i), - MemoryView(const_cast(reinterpret_cast(tensors[i].data())), tensors[i].nbytes())}); - } - - hailo_nms_info_t nms_info = { - self.m_num_of_classes, - self.m_max_boxes, - sizeof(hailo_bbox_float32_t), - 1, - false, - hailo_nms_defuse_info_t() - }; - hailo_format_t output_format = { - HAILO_FORMAT_TYPE_FLOAT32, - HAILO_FORMAT_ORDER_HAILO_NMS, - HAILO_FORMAT_FLAGS_QUANTIZED, - }; - - auto buffer = Buffer::create(HailoRTCommon::get_nms_host_frame_size(nms_info, output_format), 0); - VALIDATE_STATUS(buffer.status()); - std::map outputs; - outputs.insert({"", MemoryView(buffer.value().data(), buffer.value().size())}); - auto status = self.m_post_processing_op->execute(data_views, outputs); - VALIDATE_STATUS(status); - - // Note: The ownership of the buffer is transferred to Python wrapped as a py::array. - // When the py::array isn't referenced anymore in Python and is destructed, the py::capsule's dtor - // is called too (and it deletes the raw buffer) - auto type = py::dtype(HailoRTBindingsCommon::convert_format_type_to_string(HAILO_FORMAT_TYPE_FLOAT32)); - auto shape = *py::array::ShapeContainer({buffer.value().size()}); - const auto unmanaged_addr = buffer.release().release(); - return py::array(type, shape, unmanaged_addr, - py::capsule(unmanaged_addr, [](void *p) { delete reinterpret_cast(p); })); - }) - ; - } - -private: - YOLOv5PostProcessOpWrapper(std::shared_ptr post_processing_op, uint32_t num_of_classes, uint32_t max_bboxes) - : m_post_processing_op(post_processing_op), - m_num_of_classes(num_of_classes), - m_max_boxes(max_bboxes) {} - - std::shared_ptr m_post_processing_op; - uint32_t m_num_of_classes = 0; - uint32_t m_max_boxes = 0; -}; - -void NetFlow_api_initialize_python_module(py::module &m) -{ - YOLOv5PostProcessOpWrapper::add_to_python_module(m); -} - - -} /* namespace net_flow */ -} /* namespace hailort */ - -#endif /* _HAILO_NET_FLOW_API_HPP_ */ diff --git a/hailort/libhailort/bindings/python/src/pyhailort.cpp b/hailort/libhailort/bindings/python/src/pyhailort.cpp index 2890c91..4cca840 100644 --- a/hailort/libhailort/bindings/python/src/pyhailort.cpp +++ b/hailort/libhailort/bindings/python/src/pyhailort.cpp @@ -10,23 +10,24 @@ using namespace std; #include "hailo/hailort.h" #include "hailo/hailort_defaults.hpp" +#include "hailo/network_rate_calculator.hpp" #include "hef_api.hpp" #include "vstream_api.hpp" #include "vdevice_api.hpp" #include "device_api.hpp" #include "quantization_api.hpp" -#include "net_flow_api.hpp" #include "utils.hpp" -#include "utils.h" #include "bindings_common.hpp" -#include "sensor_config_exports.h" -#if defined(__GNUC__) -#include "common/os/posix/traffic_control.hpp" -#endif +// should be same as socket.hpp +#define PADDING_BYTES_SIZE (6) +#define PADDING_ALIGN_BYTES (8 - PADDING_BYTES_SIZE) +#define MIN_UDP_PAYLOAD_SIZE (24) +#define MAX_UDP_PAYLOAD_SIZE (1456) +#define MAX_UDP_PADDED_PAYLOAD_SIZE (MAX_UDP_PAYLOAD_SIZE - PADDING_BYTES_SIZE - PADDING_ALIGN_BYTES) namespace hailort { @@ -102,36 +103,22 @@ std::string get_status_message(uint32_t status_in) } } -#if defined(__GNUC__) - -class TrafficControlUtilWrapper final +class NetworkRateLimiter final { public: - static TrafficControlUtilWrapper create(const std::string &ip, uint16_t port, uint32_t rate_bytes_per_sec) + static void set_rate_limit(const std::string &ip, uint16_t port, uint32_t rate_bytes_per_sec) { - auto tc_expected = TrafficControlUtil::create(ip, port, rate_bytes_per_sec); - VALIDATE_STATUS(tc_expected.status()); - - auto tc_ptr = make_unique_nothrow(tc_expected.release()); - if (nullptr == tc_ptr) { - VALIDATE_STATUS(HAILO_OUT_OF_HOST_MEMORY); - } - return TrafficControlUtilWrapper(std::move(tc_ptr)); + VALIDATE_STATUS(NetworkUdpRateCalculator::set_rate_limit(ip, port, rate_bytes_per_sec)); } - void set_rate_limit() + static void reset_rate_limit(const std::string &ip, uint16_t port) { - VALIDATE_STATUS(m_tc->set_rate_limit()); - } - - void reset_rate_limit() - { - VALIDATE_STATUS(m_tc->reset_rate_limit()); + VALIDATE_STATUS(NetworkUdpRateCalculator::reset_rate_limit(ip, port)); } static std::string get_interface_name(const std::string &ip) { - auto name = TrafficControlUtil::get_interface_name(ip); + auto name = NetworkUdpRateCalculator::get_interface_name(ip); VALIDATE_STATUS(name.status()); return name.value(); @@ -139,26 +126,16 @@ public: static void add_to_python_module(py::module &m) { - py::class_(m, "TrafficControlUtil") - .def(py::init(&TrafficControlUtilWrapper::create)) - .def("set_rate_limit", &TrafficControlUtilWrapper::set_rate_limit) - .def("reset_rate_limit", &TrafficControlUtilWrapper::reset_rate_limit) + py::class_(m, "NetworkRateLimiter") + .def("set_rate_limit", &NetworkRateLimiter::set_rate_limit) + .def("reset_rate_limit", &NetworkRateLimiter::reset_rate_limit) .def_static("get_interface_name", [](const std::string &ip) { - return TrafficControlUtilWrapper::get_interface_name(ip); + return NetworkRateLimiter::get_interface_name(ip); }) ; } - -private: - TrafficControlUtilWrapper(std::unique_ptr tc) : - m_tc(std::move(tc)) - {} - - std::unique_ptr m_tc; }; -#endif - static void validate_versions_match() { hailo_version_t libhailort_version = {}; @@ -437,13 +414,6 @@ PYBIND11_MODULE(_pyhailort, m) { .value("HAILO8_ISP", HAILO_SENSOR_TYPES_HAILO8_ISP) ; - py::enum_(m, "SensorConfigOpCode") - .value("SENSOR_CONFIG_OPCODES_WR", SENSOR_CONFIG_OPCODES_WR) - .value("SENSOR_CONFIG_OPCODES_RD", SENSOR_CONFIG_OPCODES_RD) - .value("SENSOR_CONFIG_OPCODES_RMW", SENSOR_CONFIG_OPCODES_RMW) - .value("SENSOR_CONFIG_OPCODES_DELAY", SENSOR_CONFIG_OPCODES_DELAY) - ; - py::class_(m, "I2CSlaveConfig") .def(py::init<>()) .def_readwrite("endianness", &hailo_i2c_slave_config_t::endianness) @@ -755,11 +725,45 @@ PYBIND11_MODULE(_pyhailort, m) { .value("MIPI", HAILO_STREAM_INTERFACE_MIPI) ; + py::enum_(m, "VStreamStatsFlags") + .value("NONE", hailo_vstream_stats_flags_t::HAILO_VSTREAM_STATS_NONE) + .value("MEASURE_FPS", hailo_vstream_stats_flags_t::HAILO_VSTREAM_STATS_MEASURE_FPS) + .value("MEASURE_LATENCY", hailo_vstream_stats_flags_t::HAILO_VSTREAM_STATS_MEASURE_LATENCY) + ; + + py::enum_(m, "PipelineElemStatsFlags") + .value("NONE", hailo_pipeline_elem_stats_flags_t::HAILO_PIPELINE_ELEM_STATS_NONE) + .value("MEASURE_FPS", hailo_pipeline_elem_stats_flags_t::HAILO_PIPELINE_ELEM_STATS_MEASURE_FPS) + .value("MEASURE_LATENCY", hailo_pipeline_elem_stats_flags_t::HAILO_PIPELINE_ELEM_STATS_MEASURE_LATENCY) + .value("MEASURE_QUEUE_SIZE", hailo_pipeline_elem_stats_flags_t::HAILO_PIPELINE_ELEM_STATS_MEASURE_QUEUE_SIZE) + ; + py::class_(m, "VStreamParams") .def(py::init<>()) .def_readwrite("user_buffer_format", &hailo_vstream_params_t::user_buffer_format) .def_readwrite("timeout_ms", &hailo_vstream_params_t::timeout_ms) .def_readwrite("queue_size", &hailo_vstream_params_t::queue_size) + .def_readonly("vstream_stats_flags", &hailo_vstream_params_t::vstream_stats_flags) + .def_readonly("pipeline_elements_stats_flags", &hailo_vstream_params_t::pipeline_elements_stats_flags) + .def(py::pickle( + [](const hailo_vstream_params_t &vstream_params) { // __getstate__ + return py::make_tuple( + vstream_params.user_buffer_format, + vstream_params.timeout_ms, + vstream_params.queue_size, + vstream_params.vstream_stats_flags, + vstream_params.pipeline_elements_stats_flags); + }, + [](py::tuple t) { // __setstate__ + hailo_vstream_params_t vstream_params; + vstream_params.user_buffer_format = t[0].cast(); + vstream_params.timeout_ms = t[1].cast(); + vstream_params.queue_size = t[2].cast(); + vstream_params.vstream_stats_flags = t[3].cast(); + vstream_params.pipeline_elements_stats_flags = t[4].cast(); + return vstream_params; + } + )) ; py::enum_(m, "LatencyMeasurementFlags") @@ -794,7 +798,7 @@ PYBIND11_MODULE(_pyhailort, m) { }, [](VDeviceParamsWrapper& params, const uint32_t& device_count) { params.orig_params.device_count = device_count; - } + } ) .def_property("scheduling_algorithm", [](const VDeviceParamsWrapper& params) -> uint32_t { @@ -802,7 +806,8 @@ PYBIND11_MODULE(_pyhailort, m) { }, [](VDeviceParamsWrapper& params, hailo_scheduling_algorithm_t scheduling_algorithm) { params.orig_params.scheduling_algorithm = scheduling_algorithm; - } + params.orig_params.multi_process_service = (HAILO_SCHEDULING_ALGORITHM_NONE != scheduling_algorithm); + } ) .def_property("group_id", [](const VDeviceParamsWrapper& params) -> py::str { @@ -813,12 +818,9 @@ PYBIND11_MODULE(_pyhailort, m) { params.orig_params.group_id = params.group_id_str.c_str(); } ) - .def_property("multi_process_service", - [](const VDeviceParamsWrapper& params) -> uint32_t { + .def_property_readonly("multi_process_service", + [](const VDeviceParamsWrapper& params) -> bool { return params.orig_params.multi_process_service; - }, - [](VDeviceParamsWrapper& params, bool multi_process_service) { - params.orig_params.multi_process_service = multi_process_service; } ) .def_static("default", []() { @@ -1103,11 +1105,8 @@ PYBIND11_MODULE(_pyhailort, m) { VStream_api_initialize_python_module(m); VDevice_api_initialize_python_module(m); DeviceWrapper::add_to_python_module(m); - hailort::net_flow::NetFlow_api_initialize_python_module(m); - #if defined(__GNUC__) - TrafficControlUtilWrapper::add_to_python_module(m); - #endif + NetworkRateLimiter::add_to_python_module(m); std::stringstream version; version << HAILORT_MAJOR_VERSION << "." << HAILORT_MINOR_VERSION << "." << HAILORT_REVISION_VERSION; diff --git a/hailort/libhailort/bindings/python/src/quantization_api.cpp b/hailort/libhailort/bindings/python/src/quantization_api.cpp index 9d6c8cd..893afb8 100644 --- a/hailort/libhailort/bindings/python/src/quantization_api.cpp +++ b/hailort/libhailort/bindings/python/src/quantization_api.cpp @@ -12,6 +12,8 @@ #include "quantization_api.hpp" #include "bindings_common.hpp" +#include + namespace hailort { @@ -32,8 +34,7 @@ void QuantizationBindings::dequantize_output_buffer_from_uint8(py::array src_buf static_cast(dst_buffer.mutable_data()), shape_size, quant_info); break; default: - LOGGER__ERROR("Output quantization isn't supported from src format type uint8 to dst format type = {}", - HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype)); + std::cerr << "Output quantization isn't supported from src format type uint8 to dst format type = " << HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype); THROW_STATUS_ERROR(HAILO_INVALID_ARGUMENT); break; } @@ -52,8 +53,7 @@ void QuantizationBindings::dequantize_output_buffer_from_uint16(py::array src_bu static_cast(dst_buffer.mutable_data()), shape_size, quant_info); break; default: - LOGGER__ERROR("Output quantization isn't supported from src dormat type uint16 to dst format type = {}", - HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype)); + std::cerr << "Output quantization isn't supported from src dormat type uint16 to dst format type = " << HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype); THROW_STATUS_ERROR(HAILO_INVALID_ARGUMENT); break; } @@ -68,8 +68,7 @@ void QuantizationBindings::dequantize_output_buffer_from_float32(py::array src_b static_cast(dst_buffer.mutable_data()), shape_size, quant_info); break; default: - LOGGER__ERROR("Output quantization isn't supported from src format type float32 to dst format type = {}", - HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype)); + std::cerr << "Output quantization isn't supported from src format type float32 to dst format type = " << HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype); THROW_STATUS_ERROR(HAILO_INVALID_ARGUMENT); break; } @@ -92,8 +91,7 @@ void QuantizationBindings::dequantize_output_buffer_from_uint8_in_place(py::arra static_cast(dst_buffer.mutable_data()), shape_size, quant_info); break; default: - LOGGER__ERROR("Output quantization isn't supported from src format type uint8 to dst format type = {}", - HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype)); + std::cerr << "Output quantization isn't supported from src format type uint8 to dst format type = " << HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype); THROW_STATUS_ERROR(HAILO_INVALID_ARGUMENT); break; } @@ -112,8 +110,7 @@ void QuantizationBindings::dequantize_output_buffer_from_uint16_in_place(py::arr static_cast(dst_buffer.mutable_data()), shape_size, quant_info); break; default: - LOGGER__ERROR("Output quantization isn't supported from src dormat type uint16 to dst format type = {}", - HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype)); + std::cerr << "Output quantization isn't supported from src dormat type uint16 to dst format type = " << HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype); THROW_STATUS_ERROR(HAILO_INVALID_ARGUMENT); break; } @@ -128,8 +125,7 @@ void QuantizationBindings::dequantize_output_buffer_from_float32_in_place(py::ar static_cast(dst_buffer.mutable_data()), shape_size, quant_info); break; default: - LOGGER__ERROR("Output quantization isn't supported from src format type float32 to dst format type = {}", - HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype)); + std::cerr << "Output quantization isn't supported from src format type float32 to dst format type = " << HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype); THROW_STATUS_ERROR(HAILO_INVALID_ARGUMENT); break; } @@ -149,7 +145,7 @@ void QuantizationBindings::dequantize_output_buffer_in_place(py::array dst_buffe QuantizationBindings::dequantize_output_buffer_from_float32_in_place(dst_buffer, dst_dtype, shape_size, quant_info); break; default: - LOGGER__ERROR("Unsupported src format type = {}", HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype)); + std::cerr << "Unsupported src format type = " << HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype); THROW_STATUS_ERROR(HAILO_INVALID_ARGUMENT); break; } @@ -169,7 +165,7 @@ void QuantizationBindings::dequantize_output_buffer(py::array src_buffer, py::ar QuantizationBindings::dequantize_output_buffer_from_float32(src_buffer, dst_buffer, dst_dtype, shape_size, quant_info); break; default: - LOGGER__ERROR("Unsupported src format type = {}", HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype)); + std::cerr << "Unsupported src format type = " << HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype); THROW_STATUS_ERROR(HAILO_INVALID_ARGUMENT); break; } @@ -184,7 +180,7 @@ void QuantizationBindings::quantize_input_buffer_from_uint8(py::array src_buffer static_cast(dst_buffer.mutable_data()), shape_size, quant_info); break; default: - LOGGER__ERROR("Input quantization isn't supported from src format type uint8 to dst format type = {}", HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype)); + std::cerr << "Input quantization isn't supported from src format type uint8 to dst format type = " << HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype); THROW_STATUS_ERROR(HAILO_INVALID_ARGUMENT); break; } @@ -203,8 +199,7 @@ void QuantizationBindings::quantize_input_buffer_from_uint16(py::array src_buffe static_cast(dst_buffer.mutable_data()), shape_size, quant_info); break; default: - LOGGER__ERROR("Input quantization isn't supported from src format type uint16 to dst format type = {}", - HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype)); + std::cerr << "Input quantization isn't supported from src format type uint16 to dst format type = " << HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype); THROW_STATUS_ERROR(HAILO_INVALID_ARGUMENT); break; } @@ -223,8 +218,8 @@ void QuantizationBindings::quantize_input_buffer_from_float32(py::array src_buff static_cast(dst_buffer.mutable_data()), shape_size, quant_info); break; default: - LOGGER__ERROR("Input quantization isn't supported from src format type float32 to dst format type = {}", - HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype)); + std::cerr << "Input quantization isn't supported from src format type float32 to dst format type = " << + HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype); THROW_STATUS_ERROR(HAILO_INVALID_ARGUMENT); break; } @@ -244,7 +239,7 @@ void QuantizationBindings::quantize_input_buffer(py::array src_buffer, py::array QuantizationBindings::quantize_input_buffer_from_float32(src_buffer, dst_buffer, dst_dtype, shape_size, quant_info); break; default: - LOGGER__ERROR("Input quantization isn't supported for src format type = {}", HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype)); + std::cerr << "Input quantization isn't supported for src format type = " << HailoRTBindingsCommon::convert_format_type_to_string(dst_dtype); THROW_STATUS_ERROR(HAILO_INVALID_ARGUMENT); break; } diff --git a/hailort/libhailort/bindings/python/src/utils.hpp b/hailort/libhailort/bindings/python/src/utils.hpp index e5356e1..da26368 100644 --- a/hailort/libhailort/bindings/python/src/utils.hpp +++ b/hailort/libhailort/bindings/python/src/utils.hpp @@ -68,11 +68,11 @@ class HailoRTStatusException : public HailoRTException { [](hailo_stream_parameters_t& self) -> const __property_type& \ { \ if (__interface_value != self.stream_interface) { \ - LOGGER__ERROR("Stream params interface is not {}.", #__interface_value); \ + std::cerr << "Stream params interface is not " << __interface_value << "."; \ THROW_STATUS_ERROR(HAILO_INVALID_OPERATION); \ } \ if (__direction_value != self.direction) { \ - LOGGER__ERROR("Stream params direction is not {}.", #__direction_value); \ + std::cerr << "Stream params direction is not " << __direction_value << "."; \ THROW_STATUS_ERROR(HAILO_INVALID_OPERATION); \ } \ return self.__property_name; \ @@ -80,11 +80,11 @@ class HailoRTStatusException : public HailoRTException { [](hailo_stream_parameters_t& self, const __property_type& value) \ { \ if (__interface_value != self.stream_interface) { \ - LOGGER__ERROR("Stream params interface is not {}.", #__interface_value); \ + std::cerr << "Stream params interface is not " << __interface_value << "."; \ THROW_STATUS_ERROR(HAILO_INVALID_OPERATION); \ } \ if (__direction_value != self.direction) { \ - LOGGER__ERROR("Stream params direction is not {}.", #__direction_value); \ + std::cerr << "Stream params direction is not " << __direction_value << "."; \ THROW_STATUS_ERROR(HAILO_INVALID_OPERATION); \ } \ self.__property_name = value; \ diff --git a/hailort/libhailort/bindings/python/src/vdevice_api.hpp b/hailort/libhailort/bindings/python/src/vdevice_api.hpp index 02a7ee6..eae48d7 100644 --- a/hailort/libhailort/bindings/python/src/vdevice_api.hpp +++ b/hailort/libhailort/bindings/python/src/vdevice_api.hpp @@ -16,14 +16,8 @@ #include "hailo/vdevice.hpp" #include "hailo/hailort_common.hpp" -#include "common/logger_macros.hpp" - -#ifdef HAILO_SUPPORT_MULTI_PROCESS -#include "service/rpc_client_utils.hpp" -#endif // HAILO_SUPPORT_MULTI_PROCESS - #include "utils.hpp" - +#include #include #include #include @@ -57,7 +51,7 @@ public: static VDeviceWrapper create(const VDeviceParamsWrapper ¶ms, const std::vector &device_ids) { if (params.orig_params.device_ids != nullptr && (!device_ids.empty())) { - LOGGER__ERROR("VDevice device_ids can be set in params or device_ids argument. Both parameters were passed to the c'tor"); + std::cerr << "VDevice device_ids can be set in params or device_ids argument. Both parameters were passed to the c'tor"; throw HailoRTStatusException(std::to_string(HAILO_INVALID_OPERATION)); } auto modified_params = params; @@ -124,32 +118,26 @@ public: void before_fork() { -#ifdef HAILO_SUPPORT_MULTI_PROCESS if (m_vdevice != nullptr) { auto status = m_vdevice->before_fork(); VALIDATE_STATUS(status); } -#endif // HAILO_SUPPORT_MULTI_PROCESS } void after_fork_in_parent() { -#ifdef HAILO_SUPPORT_MULTI_PROCESS if (m_vdevice != nullptr) { auto status = m_vdevice->after_fork_in_parent(); VALIDATE_STATUS(status); } -#endif // HAILO_SUPPORT_MULTI_PROCESS } void after_fork_in_child() { -#ifdef HAILO_SUPPORT_MULTI_PROCESS if (m_vdevice != nullptr) { auto status = m_vdevice->after_fork_in_child(); VALIDATE_STATUS(status); } -#endif // HAILO_SUPPORT_MULTI_PROCESS } private: diff --git a/hailort/libhailort/bindings/python/src/vstream_api.cpp b/hailort/libhailort/bindings/python/src/vstream_api.cpp index a1b651b..a17b6fd 100644 --- a/hailort/libhailort/bindings/python/src/vstream_api.cpp +++ b/hailort/libhailort/bindings/python/src/vstream_api.cpp @@ -7,12 +7,10 @@ * @brief Implementation of binding to virtual stream usage over Python. **/ -#include "common/logger_macros.hpp" -#include "common/utils.hpp" - #include "vstream_api.hpp" #include "bindings_common.hpp" #include "utils.hpp" +#include namespace hailort @@ -87,7 +85,7 @@ InputVStreamsWrapper InputVStreamsWrapper::create(ConfiguredNetworkGroup &net_gr std::unordered_map> input_vstreams; for (auto &input : input_vstreams_expected.value()) { auto input_name = input.name(); - input_vstreams.emplace(input_name, make_shared_nothrow(std::move(input))); + input_vstreams.emplace(input_name, std::make_unique(std::move(input))); } return InputVStreamsWrapper(input_vstreams); } @@ -106,7 +104,7 @@ std::shared_ptr InputVStreamsWrapper::get_input_by_name(const std: { auto input = m_input_vstreams.find(name); if (m_input_vstreams.end() == input) { - LOGGER__ERROR("Input virtual stream for name={} not found", name); + std::cerr << "Input virtual stream for name=" << name << " not found"; THROW_STATUS_ERROR(HAILO_NOT_FOUND); } @@ -210,7 +208,9 @@ void OutputVStreamWrapper::add_to_python_module(py::module &m) // Note: The ownership of the buffer is transferred to Python wrapped as a py::array. // When the py::array isn't referenced anymore in Python and is destructed, the py::capsule's dtor // is called too (and it deletes the raw buffer) - const auto unmanaged_addr = buffer.release().release(); + auto unmanaged_addr_exp = buffer->storage().release(); + VALIDATE_EXPECTED(unmanaged_addr_exp); + const auto unmanaged_addr = unmanaged_addr_exp.release(); return py::array(get_dtype(self), get_shape(self), unmanaged_addr, py::capsule(unmanaged_addr, [](void *p) { delete reinterpret_cast(p); })); }) @@ -263,7 +263,7 @@ OutputVStreamsWrapper OutputVStreamsWrapper::create(ConfiguredNetworkGroup &net_ std::unordered_map> output_vstreams; for (auto &output : output_vstreams_expected.value()) { auto output_name = output.name(); - output_vstreams.emplace(output_name, make_shared_nothrow(std::move(output))); + output_vstreams.emplace(output_name, std::make_unique(std::move(output))); } return OutputVStreamsWrapper(output_vstreams); } @@ -272,7 +272,7 @@ std::shared_ptr OutputVStreamsWrapper::get_output_by_name(const s { auto output = m_output_vstreams.find(name); if (m_output_vstreams.end() == output) { - LOGGER__ERROR("Output virtual stream for name={} not found", name); + std::cerr << "Output virtual stream for name=" << name << " not found"; THROW_STATUS_ERROR(HAILO_NOT_FOUND); } @@ -361,7 +361,7 @@ InferVStreamsWrapper InferVStreamsWrapper::create(ConfiguredNetworkGroup &networ { auto infer_pipeline = InferVStreams::create(network_group, input_vstreams_params, output_vstreams_params); VALIDATE_EXPECTED(infer_pipeline); - auto infer_vstream_ptr = make_shared_nothrow(std::move(infer_pipeline.value())); + auto infer_vstream_ptr = std::make_shared(std::move(infer_pipeline.value())); return InferVStreamsWrapper(infer_vstream_ptr); } @@ -426,7 +426,7 @@ std::vector InferVStreamsWrapper::get_shape(const std::string &stream_na return HailoRTBindingsCommon::get_pybind_shape(output->get().get_info(), output->get().get_user_buffer_format()); } - LOGGER__ERROR("Stream {} not found", stream_name); + std::cerr << "Stream " << stream_name << " not found"; THROW_STATUS_ERROR(HAILO_NOT_FOUND); } diff --git a/hailort/libhailort/bindings/python/src/vstream_api.hpp b/hailort/libhailort/bindings/python/src/vstream_api.hpp index 83f3bc3..d39c11e 100644 --- a/hailort/libhailort/bindings/python/src/vstream_api.hpp +++ b/hailort/libhailort/bindings/python/src/vstream_api.hpp @@ -10,8 +10,6 @@ #ifndef _VSTREAM_API_HPP_ #define _VSTREAM_API_HPP_ -#include "common/logger_macros.hpp" -#include "common/utils.hpp" #include "hailo/vstream.hpp" #include "hailo/inference_pipeline.hpp" #include "utils.hpp" diff --git a/hailort/libhailort/cmake/toolchains/toolchains.yaml b/hailort/libhailort/cmake/toolchains/toolchains.yaml index 6218084..1f142d3 100644 --- a/hailort/libhailort/cmake/toolchains/toolchains.yaml +++ b/hailort/libhailort/cmake/toolchains/toolchains.yaml @@ -19,15 +19,15 @@ python_versions: - version: '3.8' installation: manual - package_name: https://launchpad.net/ubuntu/+source/python3.8/3.8.2-1ubuntu1/+build/18834117/+files/libpython3.8-dev_3.8.2-1ubuntu1_arm64.deb + package_name: https://launchpad.net/ubuntu/+source/python3.8/3.8.2-1ubuntu1/+build/18834117/+files/libpython3.8-dev_3.8.2-1ubuntu1_arm64.deb package_dest: /usr/include/aarch64-linux-gnu - version: '3.9' installation: manual - package_name: https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa/+build/24906233/+files/libpython3.9-dev_3.9.16-1+bionic1_arm64.deb + package_name: https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa/+build/26280901/+files/libpython3.9-dev_3.9.17-1+focal1_arm64.deb package_dest: /usr/include/aarch64-linux-gnu - version: '3.10' installation: manual - package_name: https://launchpadlibrarian.net/569418529/libpython3.10-dev_3.10.0-5_arm64.deb + package_name: https://launchpadlibrarian.net/569418529/libpython3.10-dev_3.10.0-5_arm64.deb package_dest: /usr/include/aarch64-linux-gnu - name: linux.armv7l required_packages: diff --git a/hailort/libhailort/examples/CMakeLists.txt b/hailort/libhailort/examples/CMakeLists.txt index b0cfdda..ae2aacd 100644 --- a/hailort/libhailort/examples/CMakeLists.txt +++ b/hailort/libhailort/examples/CMakeLists.txt @@ -2,9 +2,33 @@ cmake_minimum_required(VERSION 3.0.0) project(hailort-examples) +if(WIN32) + add_compile_options(/W4) +elseif(UNIX) + if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "QCC") + add_compile_options(-Wall -Wextra -Wconversion) + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + add_compile_options(-Wall -Wextra -Wconversion -Wno-missing-braces) + endif() +else() + message(FATAL_ERROR "Unexpeced host, stopping build") +endif() + +if (HAILO_COMPILE_WARNING_AS_ERROR) + # Treat warnings as errors for all examples + if(WIN32) + add_compile_options(/WX) + elseif(UNIX) + add_compile_options(-Werror) + else() + message(FATAL_ERROR "Unexpeced host, stopping build") + endif() +endif() + add_subdirectory(cpp) add_subdirectory(c) # We add a costum target in order to compile all of the hailort examples add_custom_target(hailort_examples) -add_dependencies(hailort_examples c_hailort_examples cpp_hailort_examples) \ No newline at end of file + +add_dependencies(hailort_examples c_hailort_examples cpp_hailort_examples) diff --git a/hailort/libhailort/examples/README.md b/hailort/libhailort/examples/README.md index 04dd107..dd15bce 100644 --- a/hailort/libhailort/examples/README.md +++ b/hailort/libhailort/examples/README.md @@ -26,6 +26,11 @@ The following examples are provided, demonstrating the HailoRT API: - this example uses udp device. - `raw_streams_example` - Basic inference of a shortcut network using raw stream api. - The data is transformed before sent and after received in the same thread sending/receiving using the transformation api. + - `raw_async_streams_single_thread_example` - Basic inference of a shortcut network using raw stream async api with + a single thread. + - Each async read operation will re-launch some new async read operation. + - Each async write operation will re-launch some new async write operation. + - The main thread will stop the async operations by deactivating the network group. - `notification_callback_example` - Demonstrates how to work with notification callbacks. - C++ examples: @@ -38,9 +43,19 @@ The following examples are provided, demonstrating the HailoRT API: - `infer_pipeline_example` - Basic inference of a shortcut network using inference pipeline (blocking) api. - same as `infer_pipeline_example` C example, uses HailoRT C++ api. - `raw_streams_example` - Basic inference of a shortcut network, same as `raw_streams_example` C example, uses HailoRT C++ api. - - `multi_process_example` - Demonstrates how to work with HailoRT as a service and using the HailoRT Model Scheduler for network groups switching. + - `raw_async_streams_single_thread_example` - Basic inference of a shortcut network using raw stream async api with + a single thread. + - Each async read operation will re-launch some new async read operation. + - Each async write operation will re-launch some new async write operation. + - The main thread will stop the async operations by deactivating the network group. + - `raw_async_streams_multi_thread_example` - Basic inference of a shortcut network using raw stream async api with + a thread for each stream. + - The threads will continuously initiate an async read or write operations. + - The main thread will stop the async operations and the threads by deactivating the network group. + - `multi_process_example` - Demonstrates how to work with HailoRT multi-process service and using the HailoRT Model Scheduler for network groups switching. Using the script `multi_process_example.sh` one can specify the number of processes to run each hef, see `multi_process_example.sh -h` for more information. - `notification_callback_example` - Demonstrates how to work with notification callbacks, same as `notification_callback_example` C example. +You can find more details about each example in the HailoRT user guide. ## Compiling with CMake Examples are configured and compiled using the following commands: ```sh @@ -58,9 +73,10 @@ cmake --build build --config release --target cpp_vstreams_example ## Running the examples -Before running an example, download the HEFs using the [download script](../../scripts/download_hefs.sh): +Before running an example, download the HEFs using the [download script](../../scripts/download_hefs.sh) from the scripts directory: ```sh - ../../scripts/download_hefs.sh + cd ../../scripts + ./download_hefs.sh ``` To run an example, use (from this examples directory): diff --git a/hailort/libhailort/examples/c/CMakeLists.txt b/hailort/libhailort/examples/c/CMakeLists.txt index 46e0ce7..f006e35 100644 --- a/hailort/libhailort/examples/c/CMakeLists.txt +++ b/hailort/libhailort/examples/c/CMakeLists.txt @@ -11,8 +11,7 @@ add_subdirectory(multi_device_example) add_subdirectory(power_measurement_example) add_subdirectory(notification_callback_example) -add_custom_target(c_hailort_examples) -add_dependencies(c_hailort_examples +set(C_EXAMPLE_TARGETS c_data_quantization_example c_raw_streams_example c_vstreams_example @@ -22,4 +21,14 @@ add_dependencies(c_hailort_examples c_switch_network_groups_manually_example c_multi_device_example c_power_measurement_example - c_notification_callback_example) \ No newline at end of file + c_notification_callback_example +) + +if(NOT CMAKE_SYSTEM_NAME STREQUAL QNX) + # TODO: HRT-10956 support QNX async examples + add_subdirectory(raw_async_streams_single_thread_example) + set(C_EXAMPLE_TARGETS ${C_EXAMPLE_TARGETS} c_raw_async_streams_single_thread_example) +endif() + +add_custom_target(c_hailort_examples) +add_dependencies(c_hailort_examples ${C_EXAMPLE_TARGETS}) \ No newline at end of file diff --git a/hailort/libhailort/examples/c/common/common.h b/hailort/libhailort/examples/c/common/common.h index aafb298..76f973c 100644 --- a/hailort/libhailort/examples/c/common/common.h +++ b/hailort/libhailort/examples/c/common/common.h @@ -36,7 +36,14 @@ #define ARRAY_LENGTH(__array) (sizeof((__array)) / sizeof((__array)[0])) -#define NSEC_IN_SEC (1e+9) + +#if defined(__unix__) +#define hailo_sleep(seconds) sleep((seconds)) +#elif defined(_MSC_VER) +#define hailo_sleep(seconds) Sleep((seconds) * 1000) +#else /* defined(_MSC_VER) */ +#pragma error("sleep not supported") +#endif #endif /* _EXAMPLE_COMMON_H_ */ diff --git a/hailort/libhailort/examples/c/common/hailo_thread.h b/hailort/libhailort/examples/c/common/hailo_thread.h index 2c0d3be..9d2121a 100644 --- a/hailort/libhailort/examples/c/common/hailo_thread.h +++ b/hailort/libhailort/examples/c/common/hailo_thread.h @@ -12,11 +12,17 @@ #include "hailo/hailort.h" -#if defined(__unix__) || defined(__QNX__) +#if defined(__unix__) || defined(__QNX__) #include +#include +#include + typedef pthread_t hailo_thread; typedef void* thread_return_type; +typedef atomic_int hailo_atomic_int; + +#define MICROSECONDS_PER_MILLISECOND (1000) hailo_status hailo_create_thread(thread_return_type(*func_ptr)(void*), void* args, hailo_thread *thread_out) { @@ -34,11 +40,37 @@ hailo_status hailo_join_thread(hailo_thread *thread) return (hailo_status)results; } +void hailo_atomic_init(hailo_atomic_int *atomic, int value) +{ + atomic_init(atomic, value); +} + +int hailo_atomic_load(hailo_atomic_int *atomic) +{ + return atomic_load(atomic); +} + +int hailo_atomic_fetch_add(hailo_atomic_int *atomic, int value) +{ + return atomic_fetch_add(atomic, value); +} + +void hailo_atomic_increment(hailo_atomic_int *atomic) +{ + atomic_fetch_add(atomic, 1); +} + +void hailo_atomic_store(hailo_atomic_int *atomic, int value) +{ + atomic_store(atomic, value); +} + #elif defined _MSC_VER // __unix__ || __QNX__ #include typedef HANDLE hailo_thread; typedef DWORD thread_return_type; +typedef LONG hailo_atomic_int; hailo_status hailo_create_thread(thread_return_type(func_ptr)(void*), void* args, hailo_thread *thread_out) { @@ -61,6 +93,32 @@ hailo_status hailo_join_thread(hailo_thread *thread) return (hailo_status)result; } +void hailo_atomic_init(hailo_atomic_int *atomic, int value) +{ + InterlockedExchange(atomic, (LONG)value); +} + +int hailo_atomic_load(hailo_atomic_int *atomic) +{ + return InterlockedExchangeAdd(atomic, (LONG)0); +} + +int hailo_atomic_fetch_add(hailo_atomic_int *atomic, int value) +{ + return InterlockedExchangeAdd(atomic, (LONG)value); +} + +void hailo_atomic_increment(hailo_atomic_int *atomic) +{ + InterlockedIncrement(atomic); +} + +void hailo_atomic_store(hailo_atomic_int *atomic, int value) +{ + InterlockedExchange(atomic, value); +} + + #endif #endif /* _HAILO_THREAD_H_ */ diff --git a/hailort/libhailort/examples/c/data_quantization_example/CMakeLists.txt b/hailort/libhailort/examples/c/data_quantization_example/CMakeLists.txt index 4933fbd..0264495 100644 --- a/hailort/libhailort/examples/c/data_quantization_example/CMakeLists.txt +++ b/hailort/libhailort/examples/c/data_quantization_example/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.0.0) find_package(Threads REQUIRED) set(THREADS_PREFER_PTHREAD_FLAG ON) -find_package(HailoRT 4.13.0 EXACT REQUIRED) +find_package(HailoRT 4.14.0 EXACT REQUIRED) SET_SOURCE_FILES_PROPERTIES(data_quantization_example.c PROPERTIES LANGUAGE C) diff --git a/hailort/libhailort/examples/c/data_quantization_example/data_quantization_example.c b/hailort/libhailort/examples/c/data_quantization_example/data_quantization_example.c index 7688d22..bbdf614 100644 --- a/hailort/libhailort/examples/c/data_quantization_example/data_quantization_example.c +++ b/hailort/libhailort/examples/c/data_quantization_example/data_quantization_example.c @@ -269,7 +269,7 @@ int main(int argc, char **argv) status = hailo_create_hef_file(&hef, HEF_FILE); REQUIRE_SUCCESS(status, l_release_vdevice, "Failed reading hef file"); - status = hailo_init_configure_params(hef, HAILO_STREAM_INTERFACE_PCIE, &config_params); + status = hailo_init_configure_params_by_vdevice(hef, vdevice, &config_params); REQUIRE_SUCCESS(status, l_release_hef, "Failed initializing configure parameters"); status = hailo_configure_vdevice(vdevice, hef, &config_params, &network_group, &network_group_size); @@ -295,5 +295,5 @@ l_release_hef: l_release_vdevice: (void) hailo_release_vdevice(vdevice); l_exit: - return status; + return (int)status; } diff --git a/hailort/libhailort/examples/c/infer_pipeline_example/CMakeLists.txt b/hailort/libhailort/examples/c/infer_pipeline_example/CMakeLists.txt index e7523d7..0a807a5 100644 --- a/hailort/libhailort/examples/c/infer_pipeline_example/CMakeLists.txt +++ b/hailort/libhailort/examples/c/infer_pipeline_example/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.0.0) find_package(Threads REQUIRED) set(THREADS_PREFER_PTHREAD_FLAG ON) -find_package(HailoRT 4.13.0 EXACT REQUIRED) +find_package(HailoRT 4.14.0 EXACT REQUIRED) SET_SOURCE_FILES_PROPERTIES(infer_pipeline_example.c PROPERTIES LANGUAGE C) diff --git a/hailort/libhailort/examples/c/infer_pipeline_example/infer_pipeline_example.c b/hailort/libhailort/examples/c/infer_pipeline_example/infer_pipeline_example.c index 51478c8..0a682d2 100644 --- a/hailort/libhailort/examples/c/infer_pipeline_example/infer_pipeline_example.c +++ b/hailort/libhailort/examples/c/infer_pipeline_example/infer_pipeline_example.c @@ -115,7 +115,7 @@ int main(int argc, char **argv) status = hailo_create_hef_file(&hef, HEF_FILE); REQUIRE_SUCCESS(status, l_release_device, "Failed reading hef file"); - status = hailo_init_configure_params(hef, HAILO_STREAM_INTERFACE_ETH, &config_params); + status = hailo_init_configure_params_by_device(hef, device, &config_params); REQUIRE_SUCCESS(status, l_release_hef, "Failed initializing configure parameters"); status = hailo_configure_device(device, hef, &config_params, &network_group, &network_group_size); @@ -156,5 +156,5 @@ l_release_hef: l_release_device: (void) hailo_release_device(device); l_exit: - return status; + return (int)status; } diff --git a/hailort/libhailort/examples/c/multi_device_example/CMakeLists.txt b/hailort/libhailort/examples/c/multi_device_example/CMakeLists.txt index 9729466..0b501c9 100644 --- a/hailort/libhailort/examples/c/multi_device_example/CMakeLists.txt +++ b/hailort/libhailort/examples/c/multi_device_example/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.0.0) find_package(Threads REQUIRED) set(THREADS_PREFER_PTHREAD_FLAG ON) -find_package(HailoRT 4.13.0 EXACT REQUIRED) +find_package(HailoRT 4.14.0 EXACT REQUIRED) SET_SOURCE_FILES_PROPERTIES(multi_device_example.c PROPERTIES LANGUAGE C) diff --git a/hailort/libhailort/examples/c/multi_device_example/multi_device_example.c b/hailort/libhailort/examples/c/multi_device_example/multi_device_example.c index 78cc1fc..a676779 100644 --- a/hailort/libhailort/examples/c/multi_device_example/multi_device_example.c +++ b/hailort/libhailort/examples/c/multi_device_example/multi_device_example.c @@ -16,6 +16,7 @@ #define INFER_FRAME_COUNT (100) #define MAX_EDGE_LAYERS (16) #define MAX_DEVICES (16) +#define BATCH_SIZE (1) #define HEF_FILE ("hefs/shortcut_net.hef") @@ -133,6 +134,7 @@ int main() hailo_vdevice_params_t params = {0}; hailo_hef hef = NULL; hailo_configure_params_t config_params = {0}; + uint16_t batch_size = BATCH_SIZE; hailo_configured_network_group network_group = NULL; size_t network_group_size = 1; hailo_input_vstream_params_by_name_t input_vstream_params[MAX_EDGE_LAYERS] = {0}; @@ -144,6 +146,7 @@ int main() status = hailo_scan_devices(NULL, device_ids, &actual_count); REQUIRE_SUCCESS(status, l_exit, "Failed to scan devices"); + printf("Found %zu devices\n", actual_count); status = hailo_init_vdevice_params(¶ms); REQUIRE_SUCCESS(status, l_exit, "Failed init vdevice_params"); @@ -155,9 +158,15 @@ int main() status = hailo_create_hef_file(&hef, HEF_FILE); REQUIRE_SUCCESS(status, l_release_vdevice, "Failed reading hef file"); - status = hailo_init_configure_params(hef, HAILO_STREAM_INTERFACE_PCIE, &config_params); + status = hailo_init_configure_params_by_vdevice(hef, vdevice, &config_params); REQUIRE_SUCCESS(status, l_release_hef, "Failed initializing configure parameters"); + // Modify batch_size and power_mode for each network group + for (size_t i = 0; i < config_params.network_group_params_count; i++) { + config_params.network_group_params[i].batch_size = batch_size; + config_params.network_group_params[i].power_mode = HAILO_POWER_MODE_ULTRA_PERFORMANCE; + } + status = hailo_configure_vdevice(vdevice, hef, &config_params, &network_group, &network_group_size); REQUIRE_SUCCESS(status, l_release_hef, "Failed configure vdevcie from hef"); REQUIRE_ACTION(network_group_size == 1, status = HAILO_INVALID_ARGUMENT, l_release_hef, @@ -196,5 +205,5 @@ l_release_hef: l_release_vdevice: (void) hailo_release_vdevice(vdevice); l_exit: - return status; + return (int)status; } diff --git a/hailort/libhailort/examples/c/multi_network_vstream_example/CMakeLists.txt b/hailort/libhailort/examples/c/multi_network_vstream_example/CMakeLists.txt index fd39710..0ca96b4 100644 --- a/hailort/libhailort/examples/c/multi_network_vstream_example/CMakeLists.txt +++ b/hailort/libhailort/examples/c/multi_network_vstream_example/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.0.0) find_package(Threads REQUIRED) set(THREADS_PREFER_PTHREAD_FLAG ON) -find_package(HailoRT 4.13.0 EXACT REQUIRED) +find_package(HailoRT 4.14.0 EXACT REQUIRED) SET_SOURCE_FILES_PROPERTIES(multi_network_vstream_example.c PROPERTIES LANGUAGE C) diff --git a/hailort/libhailort/examples/c/multi_network_vstream_example/multi_network_vstream_example.c b/hailort/libhailort/examples/c/multi_network_vstream_example/multi_network_vstream_example.c index 552d07c..e9cdbe2 100644 --- a/hailort/libhailort/examples/c/multi_network_vstream_example/multi_network_vstream_example.c +++ b/hailort/libhailort/examples/c/multi_network_vstream_example/multi_network_vstream_example.c @@ -180,7 +180,7 @@ int main() status = hailo_create_hef_file(&hef, HEF_FILE); REQUIRE_SUCCESS(status, l_release_vdevice, "Failed reading hef file"); - status = hailo_init_configure_params(hef, HAILO_STREAM_INTERFACE_PCIE, &config_params); + status = hailo_init_configure_params_by_vdevice(hef, vdevice, &config_params); REQUIRE_SUCCESS(status, l_release_hef, "Failed initializing configure parameters"); // Modify batch_size for each network @@ -254,5 +254,5 @@ l_release_hef: l_release_vdevice: (void) hailo_release_vdevice(vdevice); l_exit: - return status; + return (int)status; } diff --git a/hailort/libhailort/examples/c/notification_callback_example/CMakeLists.txt b/hailort/libhailort/examples/c/notification_callback_example/CMakeLists.txt index 0081620..6345906 100644 --- a/hailort/libhailort/examples/c/notification_callback_example/CMakeLists.txt +++ b/hailort/libhailort/examples/c/notification_callback_example/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.0.0) find_package(Threads REQUIRED) set(THREADS_PREFER_PTHREAD_FLAG ON) -find_package(HailoRT 4.13.0 EXACT REQUIRED) +find_package(HailoRT 4.14.0 EXACT REQUIRED) SET_SOURCE_FILES_PROPERTIES(notification_callback_example.c PROPERTIES LANGUAGE C) diff --git a/hailort/libhailort/examples/c/notification_callback_example/notification_callback_example.c b/hailort/libhailort/examples/c/notification_callback_example/notification_callback_example.c index 9c7d15c..b95f55a 100644 --- a/hailort/libhailort/examples/c/notification_callback_example/notification_callback_example.c +++ b/hailort/libhailort/examples/c/notification_callback_example/notification_callback_example.c @@ -73,5 +73,5 @@ int main() l_release_device: (void) hailo_release_device(device); l_exit: - return status; + return (int)status; } diff --git a/hailort/libhailort/examples/c/power_measurement_example/CMakeLists.txt b/hailort/libhailort/examples/c/power_measurement_example/CMakeLists.txt index 6545a54..d3921a3 100644 --- a/hailort/libhailort/examples/c/power_measurement_example/CMakeLists.txt +++ b/hailort/libhailort/examples/c/power_measurement_example/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.0.0) -find_package(HailoRT 4.13.0 EXACT REQUIRED) +find_package(HailoRT 4.14.0 EXACT REQUIRED) SET_SOURCE_FILES_PROPERTIES(power_measurement_example.c PROPERTIES LANGUAGE C) diff --git a/hailort/libhailort/examples/c/power_measurement_example/power_measurement_example.c b/hailort/libhailort/examples/c/power_measurement_example/power_measurement_example.c index b68687f..750cc73 100644 --- a/hailort/libhailort/examples/c/power_measurement_example/power_measurement_example.c +++ b/hailort/libhailort/examples/c/power_measurement_example/power_measurement_example.c @@ -134,5 +134,5 @@ int main(int argc, char **argv) l_release_vdevice: (void) hailo_release_vdevice(vdevice); l_exit: - return status; + return (int)status; } \ No newline at end of file diff --git a/hailort/libhailort/examples/c/raw_async_streams_single_thread_example/CMakeLists.txt b/hailort/libhailort/examples/c/raw_async_streams_single_thread_example/CMakeLists.txt new file mode 100644 index 0000000..2fe6c27 --- /dev/null +++ b/hailort/libhailort/examples/c/raw_async_streams_single_thread_example/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.0.0) + +find_package(Threads REQUIRED) +set(THREADS_PREFER_PTHREAD_FLAG ON) + +find_package(HailoRT 4.14.0 EXACT REQUIRED) + +SET_SOURCE_FILES_PROPERTIES(raw_async_streams_single_thread_example.c PROPERTIES LANGUAGE C) + +add_executable(c_raw_async_streams_single_thread_example raw_async_streams_single_thread_example.c) +target_link_libraries(c_raw_async_streams_single_thread_example PRIVATE HailoRT::libhailort Threads::Threads) +target_include_directories(c_raw_async_streams_single_thread_example PRIVATE "${CMAKE_CURRENT_LIST_DIR}/../common") + +if(WIN32) + target_compile_options(c_raw_async_streams_single_thread_example PRIVATE + /DWIN32_LEAN_AND_MEAN + /DNOMINMAX # NOMINMAX is required in order to play nice with std::min/std::max (otherwise Windows.h defines it's own) + /wd4201 /wd4251 + ) +endif() \ No newline at end of file diff --git a/hailort/libhailort/examples/c/raw_async_streams_single_thread_example/raw_async_streams_single_thread_example.c b/hailort/libhailort/examples/c/raw_async_streams_single_thread_example/raw_async_streams_single_thread_example.c new file mode 100644 index 0000000..b5ce769 --- /dev/null +++ b/hailort/libhailort/examples/c/raw_async_streams_single_thread_example/raw_async_streams_single_thread_example.c @@ -0,0 +1,253 @@ +/** + * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file raw_async_streams_single_thread_example.c + * This example demonstrates basic usage of HailoRT async streaming api with a single thread. + **/ + +#include "common.h" +#include "hailo/hailort.h" + +#include + +#if defined(__unix__) +#include +#endif + + +#define HEF_FILE ("hefs/shortcut_net.hef") +#define MAX_EDGE_LAYERS_PER_DIR (16) +#define MAX_EDGE_LAYERS (MAX_EDGE_LAYERS_PER_DIR * 2) +#define MAX_ONGOING_TRANSFERS (16) +#define INFER_TIME_SECONDS (5) + +#if defined(__unix__) +#define INVALID_ADDR (MAP_FAILED) +#define page_aligned_alloc(size) mmap(NULL, (size), PROT_WRITE | PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0) +#define page_aligned_free(addr, size) munmap((addr), (size)) +#elif defined(_MSC_VER) +#define INVALID_ADDR (NULL) +#define page_aligned_alloc(size) VirtualAlloc(NULL, (size), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE) +#define page_aligned_free(addr, size) VirtualFree((addr), 0, MEM_RELEASE) +#else /* defined(_MSC_VER) */ +#pragma error("Aligned alloc not supported") +#endif + +#ifndef MIN +#define MIN(a, b) (((a) < (b)) ? (a) : (b)) +#endif + + +static void output_done_callback(const hailo_stream_read_async_completion_info_t *completion_info) +{ + hailo_output_stream stream = (hailo_output_stream)completion_info->opaque; + hailo_status status = HAILO_UNINITIALIZED; + + switch (completion_info->status) { + case HAILO_SUCCESS: + // Real applications can forward the buffer to post-process/display. Here we just re-launch new async reads. + status = hailo_stream_read_raw_buffer_async(stream, completion_info->buffer_addr, completion_info->buffer_size, + output_done_callback, stream); + if ((HAILO_SUCCESS != status) && (HAILO_STREAM_NOT_ACTIVATED != status)) { + fprintf(stderr, "Failed read async with status=%d\n", status); + } + break; + case HAILO_STREAM_ABORTED_BY_USER: + // Transfer was canceled, finish gracefully. + break; + default: + fprintf(stderr, "Got an unexpected status on callback. status=%d\n", completion_info->status); + } +} + +static void input_done_callback(const hailo_stream_write_async_completion_info_t *completion_info) +{ + hailo_input_stream stream = (hailo_input_stream)completion_info->opaque; + hailo_status status = HAILO_UNINITIALIZED; + + switch (completion_info->status) { + case HAILO_SUCCESS: + // Real applications may free the buffer and replace it with new buffer ready to be sent. Here we just re-launch + // new async writes. + status = hailo_stream_write_raw_buffer_async(stream, completion_info->buffer_addr, completion_info->buffer_size, + input_done_callback, stream); + if ((HAILO_SUCCESS != status) && (HAILO_STREAM_NOT_ACTIVATED != status)) { + fprintf(stderr, "Failed write async with status=%d\n", status); + } + break; + case HAILO_STREAM_ABORTED_BY_USER: + // Transfer was canceled, finish gracefully. + break; + default: + fprintf(stderr, "Got an unexpected status on callback. status=%d\n", completion_info->status); + } +} + +static hailo_status infer(hailo_configured_network_group network_group, size_t number_input_streams, + hailo_input_stream *input_streams, size_t number_output_streams, hailo_output_stream *output_streams, + size_t ongoing_transfers) +{ + hailo_status status = HAILO_UNINITIALIZED; + hailo_activated_network_group activated_network_group = NULL; + size_t i = 0; + size_t frame_index = 0; + size_t frame_size = 0; + size_t stream_index = 0; + void *current_buffer = NULL; + void *buffers[MAX_EDGE_LAYERS * MAX_ONGOING_TRANSFERS] = {0}; + size_t allocated_buffers = 0; + + status = hailo_activate_network_group(network_group, NULL, &activated_network_group); + REQUIRE_SUCCESS(status, l_exit, "Failed activate network group status=%d", status); + + // We launch "ongoing_transfers" async operations for both input and output streams. On each async callback, we launch + // some new operation with the same buffer. + for (stream_index = 0; stream_index < number_output_streams; stream_index++) { + frame_size = hailo_get_output_stream_frame_size(output_streams[stream_index]); + + // ongoing_transfers is less than or equal to the stream's max async queue size, so we can start parallel reads. + for (frame_index = 0; frame_index < ongoing_transfers; frame_index++) { + // Buffers read from async operation must be page aligned. + current_buffer = page_aligned_alloc(frame_size); + REQUIRE_ACTION(INVALID_ADDR != current_buffer, status=HAILO_OUT_OF_HOST_MEMORY, l_deactivate, "allocation failed"); + buffers[allocated_buffers++] = current_buffer; + + status = hailo_stream_read_raw_buffer_async(output_streams[stream_index], current_buffer, frame_size, + output_done_callback, output_streams[stream_index]); + REQUIRE_SUCCESS(status, l_deactivate, "Failed read async with status=%d", status); + } + } + + for (stream_index = 0; stream_index < number_input_streams; stream_index++) { + frame_size = hailo_get_input_stream_frame_size(input_streams[stream_index]); + + // ongoing_transfers is less than or equal to the stream's max async queue size, so we can start parallel writes. + for (frame_index = 0; frame_index < ongoing_transfers; frame_index++) { + // Buffers written to async operation must be page aligned. + current_buffer = page_aligned_alloc(frame_size); + REQUIRE_ACTION(INVALID_ADDR != current_buffer, status=HAILO_OUT_OF_HOST_MEMORY, l_deactivate, "allocation failed"); + buffers[allocated_buffers++] = current_buffer; + + status = hailo_stream_write_raw_buffer_async(input_streams[stream_index], current_buffer, frame_size, + input_done_callback, input_streams[stream_index]); + REQUIRE_SUCCESS(status, l_deactivate, "Failed write async with status=%d", status); + } + } + + // After all async operations are launched, the inference will continue until we deactivate the network. + hailo_sleep(INFER_TIME_SECONDS); + + status = HAILO_SUCCESS; +l_deactivate: + // Calling hailo_deactivate_network_group will make sure that all async operations are done. All pending async I/O + // operations will be canceled and their callbacks called with status=HAILO_STREAM_ABORTED_BY_USER. + (void) hailo_deactivate_network_group(activated_network_group); + + // There are no async I/O operations ongoing so it is safe to free the buffers now. + for (i = 0; i < allocated_buffers; i++) page_aligned_free(buffers[i], frame_size); + +l_exit: + return status; +} + +static hailo_status configure_device(hailo_device device, const char *hef_file, + hailo_configured_network_group *network_group) +{ + hailo_status status = HAILO_UNINITIALIZED; + hailo_hef hef = NULL; + hailo_configure_params_t configure_params = {0}; + size_t i = 0; + size_t network_group_size = 1; + + // Load HEF file. + status = hailo_create_hef_file(&hef, hef_file); + REQUIRE_SUCCESS(status, l_exit, "Failed reading hef file %s", hef_file); + + // Create configure params + status = hailo_init_configure_params_by_device(hef, device, &configure_params); + REQUIRE_SUCCESS(status, l_exit, "Failed init configure params"); + REQUIRE_ACTION(configure_params.network_group_params_count == 1, status=HAILO_INVALID_ARGUMENT, l_exit, + "Unexpected network group size"); + + // Set HAILO_STREAM_FLAGS_ASYNC for all streams in order to use async api. + for (i = 0; i < configure_params.network_group_params[0].stream_params_by_name_count; i++) { + configure_params.network_group_params[0].stream_params_by_name[i].stream_params.flags = HAILO_STREAM_FLAGS_ASYNC; + } + + status = hailo_configure_device(device, hef, &configure_params, network_group, &network_group_size); + REQUIRE_SUCCESS(status, l_release_hef, "Failed configuring device"); + + status = HAILO_SUCCESS; +l_release_hef: + (void) hailo_release_hef(hef); +l_exit: + return status; +} + +int main() +{ + hailo_status status = HAILO_UNINITIALIZED; + hailo_device device = NULL; + hailo_configured_network_group network_group = NULL; + hailo_stream_info_t input_streams_info[MAX_EDGE_LAYERS_PER_DIR] = {0}; + hailo_stream_info_t output_streams_info[MAX_EDGE_LAYERS_PER_DIR] = {0}; + hailo_input_stream input_streams[MAX_EDGE_LAYERS_PER_DIR] = {NULL}; + hailo_output_stream output_streams[MAX_EDGE_LAYERS_PER_DIR] = {NULL}; + size_t number_input_streams = 0; + size_t number_output_streams = 0; + size_t index = 0; + size_t queue_size = 0; + size_t ongoing_transfers = MAX_ONGOING_TRANSFERS; + + // Create device object. + status = hailo_create_device_by_id(NULL, &device); + REQUIRE_SUCCESS(status, l_exit, "Failed to create device"); + + // Configure device with HEF. + status = configure_device(device, HEF_FILE, &network_group); + REQUIRE_SUCCESS(status, l_release_device, "Failed configure_device"); + + // Get input/output stream objects. + status = hailo_network_group_get_input_stream_infos(network_group, input_streams_info, MAX_EDGE_LAYERS_PER_DIR, + &number_input_streams); + REQUIRE_SUCCESS(status, l_release_device, "Failed getting input streams infos"); + + status = hailo_network_group_get_output_stream_infos(network_group, output_streams_info, MAX_EDGE_LAYERS_PER_DIR, + &number_output_streams); + REQUIRE_SUCCESS(status, l_release_device, "Failed getting output streams infos"); + + for (index = 0; index < number_input_streams; index++) { + status = hailo_get_input_stream(network_group, input_streams_info[index].name, &input_streams[index]); + REQUIRE_SUCCESS(status, l_release_device, "Failed getting input stream %s", input_streams_info[index].name); + + status = hailo_input_stream_get_async_max_queue_size(input_streams[index], &queue_size); + REQUIRE_SUCCESS(status, l_release_device, "Failed getting queue size"); + + ongoing_transfers = MIN(queue_size, ongoing_transfers); + } + + for (index = 0; index < number_output_streams; index++) { + status = hailo_get_output_stream(network_group, output_streams_info[index].name, &output_streams[index]); + REQUIRE_SUCCESS(status, l_release_device, "Failed getting output stream %s", output_streams_info[index].name); + + status = hailo_output_stream_get_async_max_queue_size(output_streams[index], &queue_size); + REQUIRE_SUCCESS(status, l_release_device, "Failed getting queue size"); + + ongoing_transfers = MIN(queue_size, ongoing_transfers); + } + + // Run infer. + status = infer(network_group, number_input_streams, input_streams, number_output_streams, output_streams, + ongoing_transfers); + REQUIRE_SUCCESS(status, l_release_device, "Failed performing inference"); + + status = HAILO_SUCCESS; + printf("Inference ran successfully\n"); + +l_release_device: + (void) hailo_release_device(device); +l_exit: + return (int)status; +} diff --git a/hailort/libhailort/examples/c/raw_streams_example/CMakeLists.txt b/hailort/libhailort/examples/c/raw_streams_example/CMakeLists.txt index 8ed9ef7..b92b40c 100644 --- a/hailort/libhailort/examples/c/raw_streams_example/CMakeLists.txt +++ b/hailort/libhailort/examples/c/raw_streams_example/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.0.0) find_package(Threads REQUIRED) set(THREADS_PREFER_PTHREAD_FLAG ON) -find_package(HailoRT 4.13.0 EXACT REQUIRED) +find_package(HailoRT 4.14.0 EXACT REQUIRED) SET_SOURCE_FILES_PROPERTIES(raw_streams_example.c PROPERTIES LANGUAGE C) diff --git a/hailort/libhailort/examples/c/raw_streams_example/raw_streams_example.c b/hailort/libhailort/examples/c/raw_streams_example/raw_streams_example.c index 04093fd..dfad815 100644 --- a/hailort/libhailort/examples/c/raw_streams_example/raw_streams_example.c +++ b/hailort/libhailort/examples/c/raw_streams_example/raw_streams_example.c @@ -198,7 +198,7 @@ int main() status = hailo_create_hef_file(&hef, HEF_FILE); REQUIRE_SUCCESS(status, l_release_device, "Failed creating hef file %s", HEF_FILE); - status = hailo_init_configure_params(hef, HAILO_STREAM_INTERFACE_PCIE, &configure_params); + status = hailo_init_configure_params_by_device(hef, device, &configure_params); REQUIRE_SUCCESS(status, l_release_hef, "Failed init configure params"); status = hailo_configure_device(device, hef, &configure_params, &network_group, &network_group_size); @@ -239,5 +239,5 @@ l_release_hef: l_release_device: (void) hailo_release_device(device); l_exit: - return status; + return (int)status; } diff --git a/hailort/libhailort/examples/c/switch_network_groups_example/CMakeLists.txt b/hailort/libhailort/examples/c/switch_network_groups_example/CMakeLists.txt index 7aee572..05ee65e 100644 --- a/hailort/libhailort/examples/c/switch_network_groups_example/CMakeLists.txt +++ b/hailort/libhailort/examples/c/switch_network_groups_example/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.0.0) find_package(Threads REQUIRED) set(THREADS_PREFER_PTHREAD_FLAG ON) -find_package(HailoRT 4.13.0 EXACT REQUIRED) +find_package(HailoRT 4.14.0 EXACT REQUIRED) SET_SOURCE_FILES_PROPERTIES(switch_network_groups_example.c PROPERTIES LANGUAGE C) diff --git a/hailort/libhailort/examples/c/switch_network_groups_example/switch_network_groups_example.c b/hailort/libhailort/examples/c/switch_network_groups_example/switch_network_groups_example.c index df4e39b..2efcf9a 100644 --- a/hailort/libhailort/examples/c/switch_network_groups_example/switch_network_groups_example.c +++ b/hailort/libhailort/examples/c/switch_network_groups_example/switch_network_groups_example.c @@ -20,6 +20,8 @@ #define INFER_FRAME_COUNT (100) #define HEF_COUNT (2) #define DEVICE_COUNT (1) +#define BATCH_SIZE_1 (1) +#define BATCH_SIZE_2 (2) #define SCHEDULER_TIMEOUT_MS (100) #define SCHEDULER_THRESHOLD (3) @@ -136,7 +138,7 @@ hailo_status build_vstreams(hailo_configured_network_group network_group, for (size_t frame_index = 0; frame_index < input_frame_sizes[i]; frame_index++) { src_data[i][frame_index] = (uint8_t)(rand() % 256); } - } + } for (size_t i = 0; i < output_vstream_size; i++) { status = hailo_get_output_vstream_frame_size(output_vstreams[i], &output_frame_sizes[i]); @@ -190,6 +192,7 @@ int main() read_thread_args_t read_args[HEF_COUNT][MAX_EDGE_LAYERS]; char HEF_FILES[HEF_COUNT][MAX_HEF_PATH_LEN] = {"hefs/multi_network_shortcut_net.hef", "hefs/shortcut_net.hef"}; + uint16_t batch_sizes[HEF_COUNT] = {BATCH_SIZE_1, BATCH_SIZE_2}; status = hailo_init_vdevice_params(¶ms); REQUIRE_SUCCESS(status, l_exit, "Failed init vdevice_params"); @@ -203,21 +206,34 @@ int main() status = hailo_create_hef_file(&hef[hef_index], HEF_FILES[hef_index]); REQUIRE_SUCCESS(status, l_release_hef, "Failed creating hef file %s", HEF_FILES[hef_index]); - status = hailo_init_configure_params(hef[hef_index], HAILO_STREAM_INTERFACE_PCIE, &configure_params); + status = hailo_init_configure_params_by_vdevice(hef[hef_index], vdevice, &configure_params); REQUIRE_SUCCESS(status, l_release_hef, "Failed init configure params"); + // Modify batch_size for each network group + for (size_t i = 0; i < configure_params.network_group_params_count; i++) { + configure_params.network_group_params[i].batch_size = batch_sizes[hef_index]; + configure_params.network_group_params[i].power_mode = HAILO_POWER_MODE_ULTRA_PERFORMANCE; + } + status = hailo_configure_vdevice(vdevice, hef[hef_index], &configure_params, &network_groups[hef_index], &network_groups_size); REQUIRE_SUCCESS(status, l_release_hef, "Failed configuring vdevcie"); REQUIRE_ACTION(network_groups_size == 1, status = HAILO_INVALID_ARGUMENT, l_release_hef, "Unexpected network group size"); - // Set scheduler's timeout and threshold for the first network group, in order to give priority to the second network group if (0 == hef_index) { + // Set scheduler's timeout and threshold for the first network group, it will give priority to the second network group status = hailo_set_scheduler_timeout(network_groups[hef_index], SCHEDULER_TIMEOUT_MS, NULL); REQUIRE_SUCCESS(status, l_release_hef, "Failed setting scheduler timeout"); status = hailo_set_scheduler_threshold(network_groups[hef_index], SCHEDULER_THRESHOLD, NULL); REQUIRE_SUCCESS(status, l_release_hef, "Failed setting scheduler threshold"); + + // Setting higher priority to the first network-group directly. + // The practical meaning is that the first network will be ready to run only if ``SCHEDULER_THRESHOLD`` send requests have been accumulated, + // or more than ``SCHEDULER_TIMEOUT_MS`` time has passed and at least one send request has been accumulated. + // However when both the first and the second networks are ready to run, the first network will be preferred over the second network. + status = hailo_set_scheduler_priority(network_groups[hef_index], HAILO_SCHEDULER_PRIORITY_NORMAL+1, NULL); + REQUIRE_SUCCESS(status, l_release_hef, "Failed setting scheduler priority"); } status = build_vstreams(network_groups[hef_index], @@ -282,10 +298,10 @@ l_release_vstreams: l_release_hef: for (hef_index = 0; hef_index < HEF_COUNT; hef_index++) { if (NULL != hef[hef_index]) { - (void)hailo_release_hef(hef[hef_index]); + (void)hailo_release_hef(hef[hef_index]); } } (void)hailo_release_vdevice(vdevice); l_exit: - return status; + return (int)status; } \ No newline at end of file diff --git a/hailort/libhailort/examples/c/switch_network_groups_manually_example/CMakeLists.txt b/hailort/libhailort/examples/c/switch_network_groups_manually_example/CMakeLists.txt index e0768bc..7687b0a 100644 --- a/hailort/libhailort/examples/c/switch_network_groups_manually_example/CMakeLists.txt +++ b/hailort/libhailort/examples/c/switch_network_groups_manually_example/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.0.0) find_package(Threads REQUIRED) set(THREADS_PREFER_PTHREAD_FLAG ON) -find_package(HailoRT 4.13.0 EXACT REQUIRED) +find_package(HailoRT 4.14.0 EXACT REQUIRED) SET_SOURCE_FILES_PROPERTIES(switch_network_groups_manually_example.c PROPERTIES LANGUAGE C) diff --git a/hailort/libhailort/examples/c/switch_network_groups_manually_example/switch_network_groups_manually_example.c b/hailort/libhailort/examples/c/switch_network_groups_manually_example/switch_network_groups_manually_example.c index f7872e0..07bc2c7 100644 --- a/hailort/libhailort/examples/c/switch_network_groups_manually_example/switch_network_groups_manually_example.c +++ b/hailort/libhailort/examples/c/switch_network_groups_manually_example/switch_network_groups_manually_example.c @@ -5,7 +5,7 @@ /** * @file switch_network_groups_manually_example.c * This example demonstrates basic usage of HailoRT streaming api over multiple network groups, using vstreams. - * It loads several HEF networks with a single input and a single output into a Hailo VDevice and performs a inference on each one. + * It loads several HEF networks with a single input and a single output into a Hailo VDevice and performs a inference on each one. * After inference is finished, the example switches to the next HEF and start inference again. **/ @@ -116,14 +116,14 @@ thread_return_type output_vstream_thread_func(void *args) status = hailo_vstream_read_raw_buffer(output_vstreams[hef_index], dst_data[hef_index], output_frame_size[hef_index]); REQUIRE_SUCCESS(status, l_deactivate_network_group, "Failed reading output frame from device"); - + // Process data here } - + // Deavticate network after finishing inference status = hailo_deactivate_network_group(*(output_vstream_args->activated_network_group)); REQUIRE_SUCCESS(status, l_deactivate_network_group, "Failed Deactivating network"); - + // Dont activate on last iteration if (hef_index < HEF_COUNT - 1) { // Activate next network so input thread can start sending again @@ -192,25 +192,25 @@ int main() status = hailo_create_hef_file(&hef[hef_index], HEF_FILES[hef_index]); REQUIRE_SUCCESS(status, l_release_hef, "Failed creating hef file %s", HEF_FILES[hef_index]); - status = hailo_init_configure_params(hef[hef_index], HAILO_STREAM_INTERFACE_PCIE, &configure_params); + status = hailo_init_configure_params_by_vdevice(hef[hef_index], vdevice, &configure_params); REQUIRE_SUCCESS(status, l_release_hef, "Failed init configure params"); status = hailo_configure_vdevice(vdevice, hef[hef_index], &configure_params, &network_groups[hef_index], &network_groups_size); REQUIRE_SUCCESS(status, l_release_hef, "Failed configuring vdevcie"); - REQUIRE_ACTION(network_groups_size == 1, status = HAILO_INVALID_ARGUMENT, l_release_hef, + REQUIRE_ACTION(network_groups_size == 1, status = HAILO_INVALID_ARGUMENT, l_release_hef, "Unexpected network group size"); // Mae sure each hef is single input single output status = hailo_make_input_vstream_params(network_groups[hef_index], true, HAILO_FORMAT_TYPE_AUTO, &input_vstream_params[hef_index], &input_vstream_size); REQUIRE_SUCCESS(status, l_release_hef, "Failed making input virtual stream params"); - REQUIRE_ACTION(input_vstream_size == 1, status = HAILO_INVALID_ARGUMENT, l_release_hef, + REQUIRE_ACTION(input_vstream_size == 1, status = HAILO_INVALID_ARGUMENT, l_release_hef, "INVALID HEF - Only hefs with single input vstream are allowed"); status = hailo_make_output_vstream_params(network_groups[hef_index], true, HAILO_FORMAT_TYPE_AUTO, &output_vstream_params[hef_index], &output_vstream_size); REQUIRE_SUCCESS(status, l_release_hef, "Failed making output virtual stream params"); - REQUIRE_ACTION(output_vstream_size == 1, status = HAILO_INVALID_ARGUMENT, l_release_hef, + REQUIRE_ACTION(output_vstream_size == 1, status = HAILO_INVALID_ARGUMENT, l_release_hef, "INVALID HEF - Only hefs with single output vstream are allowed"); } @@ -251,10 +251,10 @@ l_join_input_thread: l_release_hef: for (hef_index = 0; hef_index < HEF_COUNT; hef_index++) { if (NULL != hef[hef_index]) { - (void)hailo_release_hef(hef[hef_index]); + (void)hailo_release_hef(hef[hef_index]); } } (void)hailo_release_vdevice(vdevice); l_exit: - return status; + return (int)status; } diff --git a/hailort/libhailort/examples/c/vstreams_example/CMakeLists.txt b/hailort/libhailort/examples/c/vstreams_example/CMakeLists.txt index 1c32dfc..fb4af1b 100644 --- a/hailort/libhailort/examples/c/vstreams_example/CMakeLists.txt +++ b/hailort/libhailort/examples/c/vstreams_example/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.0.0) find_package(Threads REQUIRED) set(THREADS_PREFER_PTHREAD_FLAG ON) -find_package(HailoRT 4.13.0 EXACT REQUIRED) +find_package(HailoRT 4.14.0 EXACT REQUIRED) SET_SOURCE_FILES_PROPERTIES(vstreams_example.c PROPERTIES LANGUAGE C) diff --git a/hailort/libhailort/examples/c/vstreams_example/vstreams_example.c b/hailort/libhailort/examples/c/vstreams_example/vstreams_example.c index 04c5076..6338492 100644 --- a/hailort/libhailort/examples/c/vstreams_example/vstreams_example.c +++ b/hailort/libhailort/examples/c/vstreams_example/vstreams_example.c @@ -137,6 +137,7 @@ int main() size_t output_vstreams_size = MAX_EDGE_LAYERS; hailo_input_vstream input_vstreams[MAX_EDGE_LAYERS] = {NULL}; hailo_output_vstream output_vstreams[MAX_EDGE_LAYERS] = {NULL}; + bool quantized = true; status = hailo_create_vdevice(NULL, &vdevice); REQUIRE_SUCCESS(status, l_exit, "Failed to create vdevice"); @@ -144,19 +145,31 @@ int main() status = hailo_create_hef_file(&hef, HEF_FILE); REQUIRE_SUCCESS(status, l_release_vdevice, "Failed reading hef file"); - status = hailo_init_configure_params(hef, HAILO_STREAM_INTERFACE_PCIE, &config_params); + status = hailo_init_configure_params_by_vdevice(hef, vdevice, &config_params); REQUIRE_SUCCESS(status, l_release_hef, "Failed initializing configure parameters"); status = hailo_configure_vdevice(vdevice, hef, &config_params, &network_group, &network_group_size); - REQUIRE_SUCCESS(status, l_release_hef, "Failed configure vdevcie from hef"); + REQUIRE_SUCCESS(status, l_release_hef, "Failed configure vdevice from hef"); REQUIRE_ACTION(network_group_size == 1, status = HAILO_INVALID_ARGUMENT, l_release_hef, "Invalid network group size"); - status = hailo_make_input_vstream_params(network_group, true, HAILO_FORMAT_TYPE_AUTO, + + // Set input format type to auto, and mark the data as quantized - libhailort will not scale the data before writing to the HW + quantized = true; + status = hailo_make_input_vstream_params(network_group, quantized, HAILO_FORMAT_TYPE_AUTO, input_vstream_params, &input_vstreams_size); REQUIRE_SUCCESS(status, l_release_hef, "Failed making input virtual stream params"); - status = hailo_make_output_vstream_params(network_group, true, HAILO_FORMAT_TYPE_AUTO, + /* The input format order in the example HEF is NHWC in the user-side (may be seen using 'hailortcli parse-hef ). + Here we override the user-side format order to be NCHW */ + for (size_t i = 0 ; i < input_vstreams_size; i++) { + input_vstream_params[i].params.user_buffer_format.order = HAILO_FORMAT_ORDER_NCHW; + } + + // Set output format type to float32, and mark the data as not quantized - libhailort will de-quantize the data after reading from the HW + // Note: this process might affect the overall performance + quantized = false; + status = hailo_make_output_vstream_params(network_group, quantized, HAILO_FORMAT_TYPE_FLOAT32, output_vstream_params, &output_vstreams_size); REQUIRE_SUCCESS(status, l_release_hef, "Failed making output virtual stream params"); @@ -186,5 +199,5 @@ l_release_hef: l_release_vdevice: (void) hailo_release_vdevice(vdevice); l_exit: - return status; + return (int)status; } diff --git a/hailort/libhailort/examples/cpp/CMakeLists.txt b/hailort/libhailort/examples/cpp/CMakeLists.txt index 10eea7a..ba966bf 100644 --- a/hailort/libhailort/examples/cpp/CMakeLists.txt +++ b/hailort/libhailort/examples/cpp/CMakeLists.txt @@ -3,7 +3,6 @@ cmake_minimum_required(VERSION 3.0.0) add_subdirectory(vstreams_example) add_subdirectory(infer_pipeline_example) add_subdirectory(raw_streams_example) -add_subdirectory(raw_async_streams_example) add_subdirectory(multi_network_vstream_example) add_subdirectory(switch_network_groups_example) add_subdirectory(switch_network_groups_manually_example) @@ -12,16 +11,28 @@ add_subdirectory(power_measurement_example) add_subdirectory(multi_process_example) add_subdirectory(notification_callback_example) -add_custom_target(cpp_hailort_examples) -add_dependencies(cpp_hailort_examples + +set(CPP_EXAMPLE_TARGETS cpp_vstreams_example cpp_infer_pipeline_example cpp_raw_streams_example - cpp_raw_async_streams_example cpp_multi_network_vstream_example cpp_switch_network_groups_example cpp_switch_network_groups_manually_example cpp_multi_device_example cpp_power_measurement_example cpp_multi_process_example - cpp_notification_callback_example) \ No newline at end of file + cpp_notification_callback_example +) + +if(NOT CMAKE_SYSTEM_NAME STREQUAL QNX) + # TODO: HRT-10956 support QNX async examples + add_subdirectory(raw_async_streams_multi_thread_example) + add_subdirectory(raw_async_streams_single_thread_example) + set(CPP_EXAMPLE_TARGETS ${C_EXAMPLE_TARGETS} + cpp_raw_async_streams_multi_thread_example + cpp_raw_async_streams_single_thread_example) +endif() + +add_custom_target(cpp_hailort_examples) +add_dependencies(cpp_hailort_examples ${CPP_EXAMPLE_TARGETS}) \ No newline at end of file diff --git a/hailort/libhailort/examples/cpp/infer_pipeline_example/CMakeLists.txt b/hailort/libhailort/examples/cpp/infer_pipeline_example/CMakeLists.txt index 9f13d63..8967d42 100644 --- a/hailort/libhailort/examples/cpp/infer_pipeline_example/CMakeLists.txt +++ b/hailort/libhailort/examples/cpp/infer_pipeline_example/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.0.0) -find_package(HailoRT 4.13.0 EXACT REQUIRED) +find_package(HailoRT 4.14.0 EXACT REQUIRED) add_executable(cpp_infer_pipeline_example infer_pipeline_example.cpp) target_link_libraries(cpp_infer_pipeline_example PRIVATE HailoRT::libhailort) diff --git a/hailort/libhailort/examples/cpp/multi_device_example/CMakeLists.txt b/hailort/libhailort/examples/cpp/multi_device_example/CMakeLists.txt index 99fd22e..d766e49 100644 --- a/hailort/libhailort/examples/cpp/multi_device_example/CMakeLists.txt +++ b/hailort/libhailort/examples/cpp/multi_device_example/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.0.0) find_package(Threads REQUIRED) set(THREADS_PREFER_PTHREAD_FLAG ON) -find_package(HailoRT 4.13.0 EXACT REQUIRED) +find_package(HailoRT 4.14.0 EXACT REQUIRED) add_executable(cpp_multi_device_example multi_device_example.cpp) target_link_libraries(cpp_multi_device_example PRIVATE HailoRT::libhailort Threads::Threads) diff --git a/hailort/libhailort/examples/cpp/multi_device_example/multi_device_example.cpp b/hailort/libhailort/examples/cpp/multi_device_example/multi_device_example.cpp index 2d8b9c6..5bb6b47 100644 --- a/hailort/libhailort/examples/cpp/multi_device_example/multi_device_example.cpp +++ b/hailort/libhailort/examples/cpp/multi_device_example/multi_device_example.cpp @@ -14,6 +14,7 @@ #define HEF_FILE ("hefs/shortcut_net.hef") +constexpr size_t BATCH_SIZE = 1; constexpr size_t FRAMES_COUNT = 100; constexpr bool QUANTIZED = true; constexpr hailo_format_type_t FORMAT_TYPE = HAILO_FORMAT_TYPE_AUTO; @@ -21,20 +22,23 @@ constexpr size_t MAX_LAYER_EDGES = 16; using namespace hailort; -Expected> configure_network_group(VDevice &vdevice) +Expected> configure_network_group(VDevice &vdevice, Hef &hef, uint16_t batch_size) { - auto hef = Hef::create(HEF_FILE); - if (!hef) { - return make_unexpected(hef.status()); - } - - auto configure_params = vdevice.create_configure_params(hef.value()); + auto configure_params = vdevice.create_configure_params(hef); if (!configure_params) { + std::cerr << "Failed to create configure params" << std::endl; return make_unexpected(configure_params.status()); } - auto network_groups = vdevice.configure(hef.value(), configure_params.value()); + // Modify batch_size and power_mode for each network group + for (auto& network_group_params : configure_params.value()) { + network_group_params.second.batch_size = batch_size; + network_group_params.second.power_mode = HAILO_POWER_MODE_ULTRA_PERFORMANCE; + } + + auto network_groups = vdevice.configure(hef, configure_params.value()); if (!network_groups) { + std::cerr << "Failed to configure vdevice" << std::endl; return make_unexpected(network_groups.status()); } @@ -82,7 +86,6 @@ void read_all(OutputVStream &output, hailo_status &status) hailo_status infer(std::vector &input_streams, std::vector &output_streams) { - hailo_status status = HAILO_SUCCESS; // Success oriented hailo_status input_status[MAX_LAYER_EDGES] = {HAILO_UNINITIALIZED}; hailo_status output_status[MAX_LAYER_EDGES] = {HAILO_UNINITIALIZED}; @@ -128,11 +131,14 @@ hailo_status infer(std::vector &input_streams, std::vectorat(0)); + auto device = Device::create(); if (!device) { std::cerr << "Failed to create device " << device.status() << std::endl; return device.status(); diff --git a/hailort/libhailort/examples/cpp/power_measurement_example/CMakeLists.txt b/hailort/libhailort/examples/cpp/power_measurement_example/CMakeLists.txt index 0f412ee..17522af 100644 --- a/hailort/libhailort/examples/cpp/power_measurement_example/CMakeLists.txt +++ b/hailort/libhailort/examples/cpp/power_measurement_example/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.0.0) -find_package(HailoRT 4.13.0 EXACT REQUIRED) +find_package(HailoRT 4.14.0 EXACT REQUIRED) add_executable(cpp_power_measurement_example power_measurement_example.cpp) target_link_libraries(cpp_power_measurement_example PRIVATE HailoRT::libhailort) diff --git a/hailort/libhailort/examples/cpp/raw_async_streams_example/CMakeLists.txt b/hailort/libhailort/examples/cpp/raw_async_streams_example/CMakeLists.txt deleted file mode 100644 index b453e64..0000000 --- a/hailort/libhailort/examples/cpp/raw_async_streams_example/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -cmake_minimum_required(VERSION 3.0.0) - -find_package(Threads REQUIRED) -set(THREADS_PREFER_PTHREAD_FLAG ON) - -find_package(HailoRT 4.13.0 EXACT REQUIRED) - -add_executable(cpp_raw_async_streams_example buffer_pool.cpp raw_async_streams_example.cpp) -target_link_libraries(cpp_raw_async_streams_example PRIVATE HailoRT::libhailort Threads::Threads) - -if(WIN32) - target_compile_options(cpp_raw_async_streams_example PRIVATE - /DWIN32_LEAN_AND_MEAN - /DNOMINMAX # NOMINMAX is required in order to play nice with std::min/std::max (otherwise Windows.h defines it's own) - /wd4201 /wd4251 - ) -endif() - -set_target_properties(cpp_raw_async_streams_example PROPERTIES CXX_STANDARD 14) \ No newline at end of file diff --git a/hailort/libhailort/examples/cpp/raw_async_streams_example/buffer_pool.cpp b/hailort/libhailort/examples/cpp/raw_async_streams_example/buffer_pool.cpp deleted file mode 100644 index 706fbe8..0000000 --- a/hailort/libhailort/examples/cpp/raw_async_streams_example/buffer_pool.cpp +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. - * Distributed under the MIT license (https://opensource.org/licenses/MIT) -**/ -/** - * @file buffer_pool.cpp - * @brief Implementation of vdma buffer pool - **/ - -#include "buffer_pool.hpp" -#include "hailo/hailort.h" -#include "hailo/expected.hpp" - -Expected BufferPool::create(size_t num_buffers, size_t buffer_size, - hailo_vdma_buffer_direction_flags_t data_direction_flags, Device &device) -{ - std::queue> queue; - for (auto i = 0; i < num_buffers; i++) { - auto mapped_buffer = DmaMappedBuffer::create(buffer_size, data_direction_flags, device); - if (!mapped_buffer) { - return make_unexpected(mapped_buffer.status()); - } - - auto mapped_buffer_ptr = std::make_shared(mapped_buffer.release()); - if (nullptr == mapped_buffer_ptr) { - return make_unexpected(HAILO_OUT_OF_HOST_MEMORY); - } - - queue.push(mapped_buffer_ptr); - } - - auto result = std::make_shared(num_buffers, std::move(queue)); - if (nullptr == result) { - return make_unexpected(HAILO_OUT_OF_HOST_MEMORY); - } - - return result; -} - -BufferPool::BufferPool(size_t max_size, std::queue> &&queue) : - m_max_size(max_size), - m_mutex(), - m_cv(), - m_queue(queue) -{} - -BufferPool::~BufferPool() -{ - m_cv.notify_all(); -} - -std::shared_ptr BufferPool::dequeue() -{ - std::unique_lock lock(m_mutex); - m_cv.wait(lock, [this] { return m_queue.size() > 0; }); - auto buffer = m_queue.front(); - m_queue.pop(); - - return buffer; -} -void BufferPool::enqueue(std::shared_ptr buffer) -{ - { - std::unique_lock lock(m_mutex); - m_cv.wait(lock, [this] { return m_max_size > m_queue.size(); }); - m_queue.push(buffer); - } - - m_cv.notify_one(); -} - -void BufferPool::wait_for_pending_buffers() -{ - std::unique_lock lock(m_mutex); - m_cv.wait(lock, [this] { return m_max_size == m_queue.size(); }); -} diff --git a/hailort/libhailort/examples/cpp/raw_async_streams_example/buffer_pool.hpp b/hailort/libhailort/examples/cpp/raw_async_streams_example/buffer_pool.hpp deleted file mode 100644 index 4ff5f63..0000000 --- a/hailort/libhailort/examples/cpp/raw_async_streams_example/buffer_pool.hpp +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. - * Distributed under the MIT license (https://opensource.org/licenses/MIT) -**/ -/** - * @file buffer_pool.hpp - * @brief Pool of vdma mapped buffers, allowing FIFO queue access to buffers - **/ - -#ifndef _HAILO_BUFFER_POOL_HPP_ -#define _HAILO_BUFFER_POOL_HPP_ - -#include "hailo/hailort.hpp" -#include "hailo/expected.hpp" - -#include -#include -#include -#include - - -using namespace hailort; - -class BufferPool; -using BufferPoolPtr = std::shared_ptr; - -class BufferPool final -{ -public: - static Expected create(size_t num_buffers, size_t buffer_size, - hailo_vdma_buffer_direction_flags_t data_direction_flags, Device &device); - BufferPool(size_t max_size, std::queue> &&queue); - BufferPool(BufferPool &&) = delete; - BufferPool(const BufferPool &) = delete; - BufferPool &operator=(BufferPool &&) = delete; - BufferPool &operator=(const BufferPool &) = delete; - ~BufferPool(); - - std::shared_ptr dequeue(); - void enqueue(std::shared_ptr buffer); - void wait_for_pending_buffers(); - -private: - const size_t m_max_size; - std::mutex m_mutex; - std::condition_variable m_cv; - std::queue> m_queue; -}; - -#endif /* _HAILO_BUFFER_POOL_HPP_ */ diff --git a/hailort/libhailort/examples/cpp/raw_async_streams_example/raw_async_streams_example.cpp b/hailort/libhailort/examples/cpp/raw_async_streams_example/raw_async_streams_example.cpp deleted file mode 100644 index fa0f1b4..0000000 --- a/hailort/libhailort/examples/cpp/raw_async_streams_example/raw_async_streams_example.cpp +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. - * Distributed under the MIT license (https://opensource.org/licenses/MIT) - **/ -/** - * @file raw_async_streams_example - * This example demonstrates using low level async streams over c++ - **/ - -#include "hailo/hailort.hpp" -#include "buffer_pool.hpp" - -#include -#include - - -constexpr size_t FRAMES_COUNT = 10000; -constexpr size_t BUFFER_POOL_SIZE = 10; -constexpr auto TIMEOUT = std::chrono::milliseconds(1000); - -using namespace hailort; - -Expected> configure_network_group(Device &device, const std::string &hef_path) -{ - auto hef = Hef::create(hef_path); - if (!hef) { - return make_unexpected(hef.status()); - } - - auto configure_params = device.create_configure_params(hef.value()); - if (!configure_params) { - return make_unexpected(configure_params.status()); - } - - // change stream_params here - for (auto &ng_name_params_pair : *configure_params) { - for (auto &stream_params_name_pair : ng_name_params_pair.second.stream_params_by_name) { - stream_params_name_pair.second.flags = HAILO_STREAM_FLAGS_ASYNC; - } - } - - auto network_groups = device.configure(hef.value(), configure_params.value()); - if (!network_groups) { - return make_unexpected(network_groups.status()); - } - - if (1 != network_groups->size()) { - std::cerr << "Invalid amount of network groups" << std::endl; - return make_unexpected(HAILO_INTERNAL_FAILURE); - } - - return std::move(network_groups->at(0)); -} - -void read_all(OutputStream &output, BufferPoolPtr buffer_pool, size_t frames_to_read, hailo_status &status) -{ - for (size_t i = 0; i < frames_to_read; i++) { - status = output.wait_for_ready(output.get_frame_size(), TIMEOUT); - if (HAILO_SUCCESS != status) { - return; - } - status = output.read_async(buffer_pool->dequeue(), - [buffer_pool](std::shared_ptr buffer, const hailo_async_transfer_completion_info_t &, void *) { - buffer_pool->enqueue(buffer); - }); - if (HAILO_SUCCESS != status) { - return; - } - } -} - -void write_all(InputStream &input, BufferPoolPtr buffer_pool, size_t frames_to_write, hailo_status &status) -{ - for (size_t i = 0; i < frames_to_write; i++) { - status = input.wait_for_ready(input.get_frame_size(), TIMEOUT); - if (HAILO_SUCCESS != status) { - return; - } - status = input.write_async(buffer_pool->dequeue(), - [buffer_pool](std::shared_ptr buffer, const hailo_async_transfer_completion_info_t &, void *) { - buffer_pool->enqueue(buffer); - }); - if (HAILO_SUCCESS != status) { - return; - } - } -} - -int main() -{ - auto device = Device::create(); - if (!device) { - std::cerr << "Failed create device " << device.status() << std::endl; - return device.status(); - } - - static const auto HEF_FILE = "hefs/shortcut_net.hef"; - auto network_group = configure_network_group(*device.value(), HEF_FILE); - if (!network_group) { - std::cerr << "Failed to configure network group" << HEF_FILE << std::endl; - return network_group.status(); - } - - auto activated_network_group = network_group.value()->activate(); - if (!activated_network_group) { - std::cerr << "Failed to activate network group " << activated_network_group.status() << std::endl; - return activated_network_group.status(); - } - - // Assume one input and output - auto output = network_group->get()->get_output_streams()[0]; - auto input = network_group->get()->get_input_streams()[0]; - - auto output_buffer_pool = BufferPool::create(BUFFER_POOL_SIZE, output.get().get_frame_size(), HAILO_VDMA_BUFFER_DIRECTION_FLAGS_D2H, *device.value()); - if (!output_buffer_pool) { - std::cerr << "Failed to create output buffer pool" << std::endl; - return output_buffer_pool.status(); - } - hailo_status output_status = HAILO_UNINITIALIZED; - auto output_thread = std::make_unique(read_all, output, output_buffer_pool.value(), FRAMES_COUNT, std::ref(output_status)); - - auto input_buffer_pool = BufferPool::create(BUFFER_POOL_SIZE, input.get().get_frame_size(), HAILO_VDMA_BUFFER_DIRECTION_FLAGS_H2D, *device.value()); - if (!input_buffer_pool) { - std::cerr << "Failed to create input buffer pool" << std::endl; - return input_buffer_pool.status(); - } - hailo_status input_status = HAILO_UNINITIALIZED; - auto input_thread = std::make_unique(write_all, input, input_buffer_pool.value(), FRAMES_COUNT, std::ref(input_status)); - - // Join threads - input_thread->join(); - output_thread->join(); - if (HAILO_SUCCESS != input_status) { - return input_status; - } - if (HAILO_SUCCESS != output_status) { - return output_status; - } - - // The read/write threads have completed but the transfers issued by them haven't necessarily completed. - // We'll wait for the output buffer queue to fill back up, since the callback we registered enqueues buffers - // back to the pool + we issued the same number of reads as writes - output_buffer_pool.value()->wait_for_pending_buffers(); - - return HAILO_SUCCESS; -} diff --git a/hailort/libhailort/examples/cpp/raw_async_streams_multi_thread_example/CMakeLists.txt b/hailort/libhailort/examples/cpp/raw_async_streams_multi_thread_example/CMakeLists.txt new file mode 100644 index 0000000..d89940f --- /dev/null +++ b/hailort/libhailort/examples/cpp/raw_async_streams_multi_thread_example/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.0.0) + +find_package(Threads REQUIRED) +set(THREADS_PREFER_PTHREAD_FLAG ON) + +find_package(HailoRT 4.14.0 EXACT REQUIRED) + +add_executable(cpp_raw_async_streams_multi_thread_example raw_async_streams_multi_thread_example.cpp) +target_link_libraries(cpp_raw_async_streams_multi_thread_example PRIVATE HailoRT::libhailort Threads::Threads) + +if(WIN32) + target_compile_options(cpp_raw_async_streams_multi_thread_example PRIVATE + /DWIN32_LEAN_AND_MEAN + /DNOMINMAX # NOMINMAX is required in order to play nice with std::min/std::max (otherwise Windows.h defines it's own) + /wd4201 /wd4251 + ) +endif() + +set_target_properties(cpp_raw_async_streams_multi_thread_example PROPERTIES CXX_STANDARD 14) \ No newline at end of file diff --git a/hailort/libhailort/examples/cpp/raw_async_streams_multi_thread_example/raw_async_streams_multi_thread_example.cpp b/hailort/libhailort/examples/cpp/raw_async_streams_multi_thread_example/raw_async_streams_multi_thread_example.cpp new file mode 100644 index 0000000..c423a3a --- /dev/null +++ b/hailort/libhailort/examples/cpp/raw_async_streams_multi_thread_example/raw_async_streams_multi_thread_example.cpp @@ -0,0 +1,164 @@ +/** + * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file raw_async_streams_multi_thread_example + * This example demonstrates using low level async streams over c++ + **/ + +#include "hailo/hailort.hpp" + +#include +#include + +#if defined(__unix__) +#include +#endif + +constexpr auto TIMEOUT = std::chrono::milliseconds(1000); + +using namespace hailort; + +using AlignedBuffer = std::shared_ptr; +static AlignedBuffer page_aligned_alloc(size_t size) +{ +#if defined(__unix__) + auto addr = mmap(NULL, size, PROT_WRITE | PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); + if (MAP_FAILED == addr) throw std::bad_alloc(); + return AlignedBuffer(reinterpret_cast(addr), [size](void *addr) { munmap(addr, size); }); +#elif defined(_MSC_VER) + auto addr = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + if (!addr) throw std::bad_alloc(); + return AlignedBuffer(reinterpret_cast(addr), [](void *addr){ VirtualFree(addr, 0, MEM_RELEASE); }); +#else +#pragma error("Aligned alloc not supported") +#endif +} + +Expected> configure_network_group(Device &device, const std::string &hef_path) +{ + auto hef = Hef::create(hef_path); + if (!hef) { + return make_unexpected(hef.status()); + } + + auto configure_params = device.create_configure_params(hef.value()); + if (!configure_params) { + return make_unexpected(configure_params.status()); + } + + // change stream_params here + for (auto &ng_name_params_pair : *configure_params) { + for (auto &stream_params_name_pair : ng_name_params_pair.second.stream_params_by_name) { + stream_params_name_pair.second.flags = HAILO_STREAM_FLAGS_ASYNC; + } + } + + auto network_groups = device.configure(hef.value(), configure_params.value()); + if (!network_groups) { + return make_unexpected(network_groups.status()); + } + + if (1 != network_groups->size()) { + std::cerr << "Invalid amount of network groups" << std::endl; + return make_unexpected(HAILO_INTERNAL_FAILURE); + } + + return std::move(network_groups->at(0)); +} + +static void output_async_callback(const OutputStream::CompletionInfo &completion_info) +{ + // Real applications can free the buffer or forward it to post-process/display. + if ((HAILO_SUCCESS != completion_info.status) && (HAILO_STREAM_ABORTED_BY_USER != completion_info.status)) { + // We will get HAILO_STREAM_ABORTED_BY_USER when activated_network_group is destructed. + std::cerr << "Got an unexpected status on callback. status=" << completion_info.status << std::endl; + } +} + +static void input_async_callback(const InputStream::CompletionInfo &completion_info) +{ + // Real applications can free the buffer or reuse it for next transfer. + if ((HAILO_SUCCESS != completion_info.status) && (HAILO_STREAM_ABORTED_BY_USER != completion_info.status)) { + // We will get HAILO_STREAM_ABORTED_BY_USER when activated_network_group is destructed. + std::cerr << "Got an unexpected status on callback. status=" << completion_info.status << std::endl; + } +} + +int main() +{ + auto device = Device::create(); + if (!device) { + std::cerr << "Failed create device " << device.status() << std::endl; + return EXIT_FAILURE; + } + + static const auto HEF_FILE = "hefs/shortcut_net.hef"; + auto network_group = configure_network_group(*device.value(), HEF_FILE); + if (!network_group) { + std::cerr << "Failed to configure network group " << HEF_FILE << std::endl; + return EXIT_FAILURE; + } + + // Assume one input and output + auto &output = network_group->get()->get_output_streams()[0].get(); + auto &input = network_group->get()->get_input_streams()[0].get(); + + // Allocate buffers. The buffers sent to the async API must be page aligned. + // For simplicity, in this example, we pass one buffer for each stream (It may be problematic in output since the + // buffer will be overridden on each read). + // Note - the buffers are allocated before we activate the network group. This will ensure that they won't be freed + // until the network group will become inactive. + auto output_buffer = page_aligned_alloc(output.get_frame_size()); + auto input_buffer = page_aligned_alloc(input.get_frame_size()); + + // The destructor of activated_network_group will make sure that all async operations are done. All pending + // operations will be canceled and their callbacks will be called with status=HAILO_STREAM_ABORTED_BY_USER. + // Be sure to capture variables in the callbacks that will be destructed after the activated_network_group. + // Otherwise, the lambda would have access an uninitialized data. + auto activated_network_group = network_group.value()->activate(); + if (!activated_network_group) { + std::cerr << "Failed to activate network group " << activated_network_group.status() << std::endl; + return EXIT_FAILURE; + } + + std::atomic output_status(HAILO_UNINITIALIZED); + std::thread output_thread([&]() { + while (true) { + output_status = output.wait_for_async_ready(output.get_frame_size(), TIMEOUT); + if (HAILO_SUCCESS != output_status) { return; } + + output_status = output.read_async(output_buffer.get(), output.get_frame_size(), output_async_callback); + if (HAILO_SUCCESS != output_status) { return; } + } + }); + + std::atomic input_status(HAILO_UNINITIALIZED); + std::thread input_thread([&]() { + while (true) { + input_status = input.wait_for_async_ready(input.get_frame_size(), TIMEOUT); + if (HAILO_SUCCESS != input_status) { return; } + + input_status = input.write_async(input_buffer.get(), input.get_frame_size(), input_async_callback); + if (HAILO_SUCCESS != input_status) { return; } + } + }); + + // After all async operations are launched, the inference is running. + std::this_thread::sleep_for(std::chrono::seconds(5)); + + // Make it stop. We explicitly destruct activated_network_group to stop all async I/O. + activated_network_group->reset(); + + // Thread should be stopped with HAILO_STREAM_NOT_ACTIVATED status. + output_thread.join(); + input_thread.join(); + if ((HAILO_STREAM_NOT_ACTIVATED != output_status) || (HAILO_STREAM_NOT_ACTIVATED != input_status)) { + std::cerr << "Got unexpected statues from thread: " << output_status << ", " << input_status << std::endl; + return EXIT_FAILURE; + } + + std::cout << "Inference finished successfully" << std::endl; + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/hailort/libhailort/examples/cpp/raw_async_streams_single_thread_example/CMakeLists.txt b/hailort/libhailort/examples/cpp/raw_async_streams_single_thread_example/CMakeLists.txt new file mode 100644 index 0000000..0c24087 --- /dev/null +++ b/hailort/libhailort/examples/cpp/raw_async_streams_single_thread_example/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.0.0) + +find_package(Threads REQUIRED) +set(THREADS_PREFER_PTHREAD_FLAG ON) + +find_package(HailoRT 4.14.0 EXACT REQUIRED) + +add_executable(cpp_raw_async_streams_single_thread_example raw_async_streams_single_thread_example.cpp) +target_link_libraries(cpp_raw_async_streams_single_thread_example PRIVATE HailoRT::libhailort Threads::Threads) + +if(WIN32) + target_compile_options(cpp_raw_async_streams_single_thread_example PRIVATE + /DWIN32_LEAN_AND_MEAN + /DNOMINMAX # NOMINMAX is required in order to play nice with std::min/std::max (otherwise Windows.h defines it's own) + /wd4201 /wd4251 + ) +endif() + +set_target_properties(cpp_raw_async_streams_single_thread_example PROPERTIES CXX_STANDARD 14) \ No newline at end of file diff --git a/hailort/libhailort/examples/cpp/raw_async_streams_single_thread_example/raw_async_streams_single_thread_example.cpp b/hailort/libhailort/examples/cpp/raw_async_streams_single_thread_example/raw_async_streams_single_thread_example.cpp new file mode 100644 index 0000000..219b2da --- /dev/null +++ b/hailort/libhailort/examples/cpp/raw_async_streams_single_thread_example/raw_async_streams_single_thread_example.cpp @@ -0,0 +1,193 @@ +/** + * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file raw_async_streams_single_thread_example + * This example demonstrates using low level async streams using single thread over c++. + **/ + +#include "hailo/hailort.hpp" + +#include +#include +#include +#include + +#if defined(__unix__) +#include +#endif + +using namespace hailort; + +using AlignedBuffer = std::shared_ptr; +static AlignedBuffer page_aligned_alloc(size_t size) +{ +#if defined(__unix__) + auto addr = mmap(NULL, size, PROT_WRITE | PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); + if (MAP_FAILED == addr) throw std::bad_alloc(); + return AlignedBuffer(reinterpret_cast(addr), [size](void *addr) { munmap(addr, size); }); +#elif defined(_MSC_VER) + auto addr = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + if (!addr) throw std::bad_alloc(); + return AlignedBuffer(reinterpret_cast(addr), [](void *addr){ VirtualFree(addr, 0, MEM_RELEASE); }); +#else +#pragma error("Aligned alloc not supported") +#endif +} + +static hailo_status infer(ConfiguredNetworkGroup &network_group, InputStream &input, OutputStream &output) +{ + auto input_queue_size = input.get_async_max_queue_size(); + auto output_queue_size = output.get_async_max_queue_size(); + if (!input_queue_size || !output_queue_size) { + std::cerr << "Failed getting async queue size" << std::endl; + return HAILO_INTERNAL_FAILURE; + } + + // We store buffers vector here as a guard for the memory. The buffer will be freed only after + // activated_network_group will be released. + std::vector buffer_guards; + + OutputStream::TransferDoneCallback read_done = [&output, &read_done](const OutputStream::CompletionInfo &completion_info) { + hailo_status status = HAILO_UNINITIALIZED; + switch (completion_info.status) { + case HAILO_SUCCESS: + // Real applications can forward the buffer to post-process/display. Here we just re-launch new async read. + status = output.read_async(completion_info.buffer_addr, completion_info.buffer_size, read_done); + if ((HAILO_SUCCESS != status) && (HAILO_STREAM_NOT_ACTIVATED != status)) { + std::cerr << "Failed read async with status=" << status << std::endl; + } + break; + case HAILO_STREAM_ABORTED_BY_USER: + // Transfer was canceled, finish gracefully. + break; + default: + std::cerr << "Got an unexpected status on callback. status=" << completion_info.status << std::endl; + } + }; + + InputStream::TransferDoneCallback write_done = [&input, &write_done](const InputStream::CompletionInfo &completion_info) { + hailo_status status = HAILO_UNINITIALIZED; + switch (completion_info.status) { + case HAILO_SUCCESS: + // Real applications may free the buffer and replace it with new buffer ready to be sent. Here we just + // re-launch new async write. + status = input.write_async(completion_info.buffer_addr, completion_info.buffer_size, write_done); + if ((HAILO_SUCCESS != status) && (HAILO_STREAM_NOT_ACTIVATED != status)) { + std::cerr << "Failed read async with status=" << status << std::endl; + } + break; + case HAILO_STREAM_ABORTED_BY_USER: + // Transfer was canceled, finish gracefully. + break; + default: + std::cerr << "Got an unexpected status on callback. status=" << completion_info.status << std::endl; + } + }; + + // The destructor of activated_network_group will make sure that all async operations are done. All pending + // operations will be canceled and their callbacks will be called with status=HAILO_STREAM_ABORTED_BY_USER. + // Be sure to capture variables in the callbacks that will be destructed after the activated_network_group. + // Otherwise, the lambda would have access an uninitialized data. + auto activated_network_group = network_group.activate(); + if (!activated_network_group) { + std::cerr << "Failed to activate network group " << activated_network_group.status() << std::endl; + return activated_network_group.status(); + } + + // We launch "*output_queue_size" async read operation. On each async callback, we launch a new async read operation. + for (size_t i = 0; i < *output_queue_size; i++) { + // Buffers read from async operation must be page aligned. + auto buffer = page_aligned_alloc(output.get_frame_size()); + auto status = output.read_async(buffer.get(), output.get_frame_size(), read_done); + if (HAILO_SUCCESS != status) { + std::cerr << "read_async failed with status=" << status << std::endl; + return status; + } + + buffer_guards.emplace_back(buffer); + } + + // We launch "*input_queue_size" async write operation. On each async callback, we launch a new async write operation. + for (size_t i = 0; i < *input_queue_size; i++) { + // Buffers written to async operation must be page aligned. + auto buffer = page_aligned_alloc(input.get_frame_size()); + auto status = input.write_async(buffer.get(), input.get_frame_size(), write_done); + if (HAILO_SUCCESS != status) { + std::cerr << "write_async failed with status=" << status << std::endl; + return status; + } + + buffer_guards.emplace_back(buffer); + } + + // After all async operations are launched, the inference will continue until the activated_network_group + // destructor is called. + std::this_thread::sleep_for(std::chrono::seconds(5)); + + return HAILO_SUCCESS; +} + + +static Expected> configure_network_group(Device &device, const std::string &hef_path) +{ + auto hef = Hef::create(hef_path); + if (!hef) { + return make_unexpected(hef.status()); + } + + auto configure_params = device.create_configure_params(hef.value()); + if (!configure_params) { + return make_unexpected(configure_params.status()); + } + + // change stream_params to operate in async mode + for (auto &ng_name_params_pair : *configure_params) { + for (auto &stream_params_name_pair : ng_name_params_pair.second.stream_params_by_name) { + stream_params_name_pair.second.flags = HAILO_STREAM_FLAGS_ASYNC; + } + } + + auto network_groups = device.configure(hef.value(), configure_params.value()); + if (!network_groups) { + return make_unexpected(network_groups.status()); + } + + if (1 != network_groups->size()) { + std::cerr << "Invalid amount of network groups" << std::endl; + return make_unexpected(HAILO_INTERNAL_FAILURE); + } + + return std::move(network_groups->at(0)); +} + +int main() +{ + auto device = Device::create(); + if (!device) { + std::cerr << "Failed to create device " << device.status() << std::endl; + return device.status(); + } + + static const auto HEF_FILE = "hefs/shortcut_net.hef"; + auto network_group = configure_network_group(*device.value(), HEF_FILE); + if (!network_group) { + std::cerr << "Failed to configure network group" << HEF_FILE << std::endl; + return network_group.status(); + } + + // Assume one input and output + auto output = network_group->get()->get_output_streams()[0]; + auto input = network_group->get()->get_input_streams()[0]; + + // Now start the inference + auto status = infer(*network_group.value(), input.get(), output.get()); + if (HAILO_SUCCESS != status) { + std::cerr << "Inference failed with " << status << std::endl; + return status; + } + + std::cout << "Inference finished successfully" << std::endl; + return HAILO_SUCCESS; +} diff --git a/hailort/libhailort/examples/cpp/raw_streams_example/CMakeLists.txt b/hailort/libhailort/examples/cpp/raw_streams_example/CMakeLists.txt index 4184783..d30f854 100644 --- a/hailort/libhailort/examples/cpp/raw_streams_example/CMakeLists.txt +++ b/hailort/libhailort/examples/cpp/raw_streams_example/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.0.0) find_package(Threads REQUIRED) set(THREADS_PREFER_PTHREAD_FLAG ON) -find_package(HailoRT 4.13.0 EXACT REQUIRED) +find_package(HailoRT 4.14.0 EXACT REQUIRED) add_executable(cpp_raw_streams_example raw_streams_example.cpp) target_link_libraries(cpp_raw_streams_example PRIVATE HailoRT::libhailort Threads::Threads) diff --git a/hailort/libhailort/examples/cpp/raw_streams_example/raw_streams_example.cpp b/hailort/libhailort/examples/cpp/raw_streams_example/raw_streams_example.cpp index a73fd7c..c780cad 100644 --- a/hailort/libhailort/examples/cpp/raw_streams_example/raw_streams_example.cpp +++ b/hailort/libhailort/examples/cpp/raw_streams_example/raw_streams_example.cpp @@ -145,16 +145,7 @@ hailo_status infer(InputStreamRefVector &input_streams, OutputStreamRefVector &o int main() { - auto device_ids = Device::scan(); - if (!device_ids) { - std::cerr << "Failed to scan, status = " << device_ids.status() << std::endl; - return device_ids.status(); - } - if (device_ids->size() < 1){ - std::cerr << "Failed to find a connected hailo device." << std::endl; - return HAILO_INVALID_OPERATION; - } - auto device = Device::create(device_ids->at(0)); + auto device = Device::create(); if (!device) { std::cerr << "Failed to create device " << device.status() << std::endl; return device.status(); diff --git a/hailort/libhailort/examples/cpp/switch_network_groups_example/CMakeLists.txt b/hailort/libhailort/examples/cpp/switch_network_groups_example/CMakeLists.txt index a0e9c7e..3338ff1 100644 --- a/hailort/libhailort/examples/cpp/switch_network_groups_example/CMakeLists.txt +++ b/hailort/libhailort/examples/cpp/switch_network_groups_example/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.0.0) find_package(Threads REQUIRED) set(THREADS_PREFER_PTHREAD_FLAG ON) -find_package(HailoRT 4.13.0 EXACT REQUIRED) +find_package(HailoRT 4.14.0 EXACT REQUIRED) add_executable(cpp_switch_network_groups_example switch_network_groups_example.cpp) target_link_libraries(cpp_switch_network_groups_example PRIVATE HailoRT::libhailort Threads::Threads) diff --git a/hailort/libhailort/examples/cpp/switch_network_groups_example/switch_network_groups_example.cpp b/hailort/libhailort/examples/cpp/switch_network_groups_example/switch_network_groups_example.cpp index e7c4d9f..156d523 100644 --- a/hailort/libhailort/examples/cpp/switch_network_groups_example/switch_network_groups_example.cpp +++ b/hailort/libhailort/examples/cpp/switch_network_groups_example/switch_network_groups_example.cpp @@ -20,6 +20,8 @@ constexpr bool QUANTIZED = true; constexpr hailo_format_type_t FORMAT_TYPE = HAILO_FORMAT_TYPE_AUTO; constexpr size_t INFER_FRAME_COUNT = 100; constexpr uint32_t DEVICE_COUNT = 1; +constexpr size_t BATCH_SIZE_1 = 1; +constexpr size_t BATCH_SIZE_2 = 2; constexpr std::chrono::milliseconds SCHEDULER_TIMEOUT_MS(100); constexpr uint32_t SCHEDULER_THRESHOLD = 3; @@ -102,10 +104,13 @@ Expected> create_vdevice() return VDevice::create(params); } -Expected>> configure_hefs(VDevice &vdevice, std::vector &hef_paths) +Expected>> configure_hefs(VDevice &vdevice, std::vector &hef_paths, + const std::vector &batch_sizes) { std::vector> results; + assert(hef_paths.size() == batch_sizes.size()); + size_t i = 0; for (const auto &path : hef_paths) { auto hef_exp = Hef::create(path); if (!hef_exp) { @@ -113,6 +118,19 @@ Expected>> configure_hefs(VD } auto hef = hef_exp.release(); + auto configure_params = vdevice.create_configure_params(hef); + if (!configure_params) { + std::cerr << "Failed to create configure params" << std::endl; + return make_unexpected(configure_params.status()); + } + + // Modify batch_size for each network group + for (auto& network_group_params : configure_params.value()) { + network_group_params.second.batch_size = batch_sizes[i]; + network_group_params.second.power_mode = HAILO_POWER_MODE_ULTRA_PERFORMANCE; + } + i++; + auto added_network_groups = vdevice.configure(hef); if (!added_network_groups) { return make_unexpected(added_network_groups.status()); @@ -132,15 +150,17 @@ int main() } auto vdevice = vdevice_exp.release(); + std::vector batch_sizes { BATCH_SIZE_1, BATCH_SIZE_2 }; std::vector hef_paths = {"hefs/multi_network_shortcut_net.hef", "hefs/shortcut_net.hef"}; - auto configured_network_groups_exp = configure_hefs(*vdevice, hef_paths); + + auto configured_network_groups_exp = configure_hefs(*vdevice, hef_paths, batch_sizes); if (!configured_network_groups_exp) { std::cerr << "Failed to configure HEFs, status = " << configured_network_groups_exp.status() << std::endl; return configured_network_groups_exp.status(); } auto configured_network_groups = configured_network_groups_exp.release(); - // Set scheduler's timeout and threshold for the first network group, in order to give priority to the second network group + // Set scheduler's timeout and threshold for the first network group, it will give priority to the second network group auto status = configured_network_groups[0]->set_scheduler_timeout(SCHEDULER_TIMEOUT_MS); if (HAILO_SUCCESS != status) { std::cerr << "Failed to set scheduler timeout, status = " << status << std::endl; @@ -153,6 +173,16 @@ int main() return status; } + // Setting higher priority to the first network-group directly. + // The practical meaning is that the first network will be ready to run only if ``SCHEDULER_THRESHOLD`` send requests have been accumulated, + // or more than ``SCHEDULER_TIMEOUT_MS`` time has passed and at least one send request has been accumulated. + // However when both the first and the second networks are ready to run, the first network will be preferred over the second network. + status = configured_network_groups[0]->set_scheduler_priority(HAILO_SCHEDULER_PRIORITY_NORMAL+1); + if (HAILO_SUCCESS != status) { + std::cerr << "Failed to set scheduler priority, status = " << status << std::endl; + return status; + } + auto vstreams_per_network_group_exp = build_vstreams(configured_network_groups); if (!vstreams_per_network_group_exp) { std::cerr << "Failed to create vstreams, status = " << vstreams_per_network_group_exp.status() << std::endl; diff --git a/hailort/libhailort/examples/cpp/switch_network_groups_manually_example/CMakeLists.txt b/hailort/libhailort/examples/cpp/switch_network_groups_manually_example/CMakeLists.txt index 9c6114e..8ef520b 100644 --- a/hailort/libhailort/examples/cpp/switch_network_groups_manually_example/CMakeLists.txt +++ b/hailort/libhailort/examples/cpp/switch_network_groups_manually_example/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.0.0) find_package(Threads REQUIRED) set(THREADS_PREFER_PTHREAD_FLAG ON) -find_package(HailoRT 4.13.0 EXACT REQUIRED) +find_package(HailoRT 4.14.0 EXACT REQUIRED) add_executable(cpp_switch_network_groups_manually_example switch_network_groups_manually_example.cpp) target_link_libraries(cpp_switch_network_groups_manually_example PRIVATE HailoRT::libhailort Threads::Threads) diff --git a/hailort/libhailort/examples/cpp/vstreams_example/CMakeLists.txt b/hailort/libhailort/examples/cpp/vstreams_example/CMakeLists.txt index 522ea6a..7715164 100644 --- a/hailort/libhailort/examples/cpp/vstreams_example/CMakeLists.txt +++ b/hailort/libhailort/examples/cpp/vstreams_example/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.0.0) find_package(Threads REQUIRED) set(THREADS_PREFER_PTHREAD_FLAG ON) -find_package(HailoRT 4.13.0 EXACT REQUIRED) +find_package(HailoRT 4.14.0 EXACT REQUIRED) add_executable(cpp_vstreams_example vstreams_example.cpp) target_link_libraries(cpp_vstreams_example PRIVATE HailoRT::libhailort Threads::Threads) diff --git a/hailort/libhailort/examples/cpp/vstreams_example/vstreams_example.cpp b/hailort/libhailort/examples/cpp/vstreams_example/vstreams_example.cpp index 097d8a1..5baae18 100644 --- a/hailort/libhailort/examples/cpp/vstreams_example/vstreams_example.cpp +++ b/hailort/libhailort/examples/cpp/vstreams_example/vstreams_example.cpp @@ -15,8 +15,6 @@ #define HEF_FILE ("hefs/shortcut_net.hef") constexpr size_t FRAMES_COUNT = 100; -constexpr bool QUANTIZED = true; -constexpr hailo_format_type_t FORMAT_TYPE = HAILO_FORMAT_TYPE_AUTO; constexpr size_t MAX_LAYER_EDGES = 16; using namespace hailort; @@ -140,19 +138,49 @@ int main() return network_group.status(); } - auto vstreams = VStreamsBuilder::create_vstreams(*network_group.value(), QUANTIZED, FORMAT_TYPE); - if (!vstreams) { - std::cerr << "Failed creating vstreams " << vstreams.status() << std::endl; - return vstreams.status(); + // Set input format type to auto, and mark the data as quantized - libhailort will not scale the data before writing to the HW + bool quantized = true; + auto input_vstream_params = network_group.value()->make_input_vstream_params(quantized, HAILO_FORMAT_TYPE_AUTO, HAILO_DEFAULT_VSTREAM_TIMEOUT_MS, + HAILO_DEFAULT_VSTREAM_QUEUE_SIZE); + if (!input_vstream_params) { + std::cerr << "Failed creating input vstreams params " << input_vstream_params.status() << std::endl; + return input_vstream_params.status(); } - if (vstreams->first.size() > MAX_LAYER_EDGES || vstreams->second.size() > MAX_LAYER_EDGES) { + /* The input format order in the example HEF is NHWC in the user-side (may be seen using 'hailortcli parse-hef ). + Here we override the user-side format order to be NCHW */ + for (auto ¶ms_pair : *input_vstream_params) { + params_pair.second.user_buffer_format.order = HAILO_FORMAT_ORDER_NCHW; + } + + auto input_vstreams = VStreamsBuilder::create_input_vstreams(*network_group.value(), *input_vstream_params); + if (!input_vstreams) { + std::cerr << "Failed creating input vstreams " << input_vstreams.status() << std::endl; + return input_vstreams.status(); + } + + // Set output format type to float32, and mark the data as not quantized - libhailort will de-quantize the data after reading from the HW + // Note: this process might affect the overall performance + quantized = false; + auto output_vstream_params = network_group.value()->make_output_vstream_params(quantized, HAILO_FORMAT_TYPE_FLOAT32, HAILO_DEFAULT_VSTREAM_TIMEOUT_MS, + HAILO_DEFAULT_VSTREAM_QUEUE_SIZE); + if (!output_vstream_params) { + std::cerr << "Failed creating output vstreams params " << output_vstream_params.status() << std::endl; + return output_vstream_params.status(); + } + auto output_vstreams = VStreamsBuilder::create_output_vstreams(*network_group.value(), *output_vstream_params); + if (!output_vstreams) { + std::cerr << "Failed creating output vstreams " << output_vstreams.status() << std::endl; + return output_vstreams.status(); + } + + if (input_vstreams->size() > MAX_LAYER_EDGES || output_vstreams->size() > MAX_LAYER_EDGES) { std::cerr << "Trying to infer network with too many input/output virtual streams, Maximum amount is " << MAX_LAYER_EDGES << " (either change HEF or change the definition of MAX_LAYER_EDGES)"<< std::endl; return HAILO_INVALID_OPERATION; } - auto status = infer(vstreams->first, vstreams->second); + auto status = infer(*input_vstreams, *output_vstreams); if (HAILO_SUCCESS != status) { std::cerr << "Inference failed " << status << std::endl; return status; diff --git a/hailort/libhailort/hef.proto b/hailort/libhailort/hef.proto index 3407238..3c5e909 100644 --- a/hailort/libhailort/hef.proto +++ b/hailort/libhailort/hef.proto @@ -41,6 +41,14 @@ enum ProtoHEFExtensionType { HAILO_NET_FLOW_YOLOX_NMS = 15; HAILO_NET_FLOW_SSD_NMS = 16; HAILO_NET_FLOW_IOU_NMS = 17; + WRITE_DATA_BY_TYPE = 18; + NMS_OUTPUT_BURST = 19; + DUAL_DIRECTION_STREAM_INDEX = 20; + HAILO_NET_FLOW_ARGMAX = 21; + HAILO_NET_FLOW_SOFTMAX = 22; + ALIGNED_FORMAT_TYPE = 23; + OUTPUT_SCALE_PER_FEATURE = 25; + PERIPH_CALCULATION_IN_HAILORT = 26; UNUSED = 0XFFFF; } @@ -163,6 +171,29 @@ message ProtoHEFSSDBboxDecoder { uint32 cls_pad_index = 4; }; +message ProtoHEFYoloxBboxDecoder { + // Pixels stride for given bbox + uint32 stride = 1; + + // Index of the pad connected to the encoded layer in the decoder (reg layer) + uint32 reg_pad_index = 2; + + // Index of the pad connected to the classes scores layer in the decoder (cls layer) + uint32 cls_pad_index = 3; + + // Index of the pad connected to the objectness scores layer in the decoder (objectness layer) + uint32 obj_pad_index = 4; +}; + +message ProtoHEFYoloxNmsOp { + // Input image dimensions + double image_height = 1; + double image_width = 2; + + // List of bbox decoders (anchors) for the NMS layer. Each model has its own number of boxes per anchor + repeated ProtoHEFYoloxBboxDecoder bbox_decoders = 3; +}; + message ProtoHEFSSDNmsOp { // Input image dimensions double image_height = 1; @@ -208,12 +239,22 @@ message ProtoHEFNmsOp { // Additional information needed for specific NMS types oneof nms_op { ProtoHEFYoloNmsOp yolo_nms_op = 7; // YOLOv5 post process - ProtoHEFYoloNmsOp yolox_nms_op = 8; // YOLO-X post process (ignores bbox decoder coordinations) + ProtoHEFYoloxNmsOp yolox_nms_op = 8; // YOLO-X post process ProtoHEFSSDNmsOp ssd_nms_op = 9; // SSD post process ProtoHEFIOUNmsOp iou_op = 10; // IoU only } }; +enum ProtoHEFLogitsType { + PROTO_HEF_ARGMAX_TYPE = 0; + PROTO_HEF_SOFTMAX_TYPE = 1; +} + +message ProtoHEFLogitsOp { + // Logits type (softmax/argmax) + ProtoHEFLogitsType logits_type = 1; +}; + enum ProtoHEFFormatOrder { PROTO__FORMAT__ORDER__AUTO = 0; PROTO__FORMAT__ORDER__NHWC = 1; @@ -240,6 +281,13 @@ enum ProtoHEFDataType { PROTO__UINT16 = 1; }; +enum ProtoHEFFormatType { + PROTO__FORMAT__TYPE__AUTO = 0; + PROTO__FORMAT__TYPE__UINT8 = 1; + PROTO__FORMAT__TYPE__UINT16 = 2; + PROTO__FORMAT__TYPE__MAX_ENUM = 0XFFFF; +}; + message ProtoHEFTensorShape { uint32 height = 1; uint32 padded_height = 2; @@ -268,8 +316,9 @@ message ProtoHEFPad { string name = 2; // Additional information describing the data going through this pad's interface - ProtoHEFFormatOrder format = 3; - ProtoHEFDataType data_bytes = 4; + ProtoHEFFormatOrder format_order = 3; + ProtoHEFDataType data_bytes = 4; // Unused (kept for compatibility). Should use format_type field + ProtoHEFFormatType format_type = 8; ProtoHEFEdgeLayerNumericInfo numeric_info = 5; oneof shape_info { ProtoHEFTensorShape tensor_shape = 6; @@ -291,6 +340,9 @@ message ProtoHEFOp { // Op type for NMS post-process ProtoHEFNmsOp nms_op = 5; + + // Op type for Logits post-processing + ProtoHEFLogitsOp logits_op = 6; } }; @@ -464,6 +516,7 @@ message ProtoHEFAction { ProtoHEFActionWaitForModuleConfigDone wait_for_module_config_done = 11; ProtoHEFActionDebugSleep debug_sleep = 12; ProtoHEFActionEnableNMS enable_nms = 13; + ProtoHEFActionWriteDataByType write_data_by_type = 14; } } @@ -494,6 +547,31 @@ message ProtoHEFActionDebugSleep { uint64 duration_in_usec = 1; } +enum ProtoHEFWriteDataType { + DATA_FROM_ACTION = 0; + BATCH_SIZE = 1; +}; + +message ProtoHEFActionWriteDataByType { + // The address to write the data + uint64 address = 1; + + // Data type - the data to write + ProtoHEFWriteDataType data_type = 2; + + // The data that would be written if data_type=DATA_FROM_ACTION + bytes data = 3; + + // The mask to use - ignore if data_type=DATA_FROM_ACTION and data size > 4 + uint32 mask = 4; + + // Network index + uint32 network_index = 5; + + // data shift + uint32 shift = 6; +} + message InitialL3 { // L3 cut index sequencer should start from uint32 initial_l3_index = 1; @@ -572,6 +650,12 @@ message ProtoHEFActionEnableNMS { // Index of the network uint32 network_index = 2; + + // Number of classes + uint32 number_of_classes = 3; + + // Burst-size + uint32 burst_size = 4; } // None action - Do not do anything @@ -740,6 +824,17 @@ message ProtoHEFAdditionalInfo { ProtoHEFNmsInfo nms_info = 1; } +enum ProtoHEFNmsBurstType { + // No burst + PROTO__NMS_BURST_TYPE__NO_BURST = 0; + // No image delimiter, burst per class + PROTO__NMS_BURST_TYPE__H8_PER_CLASS = 1; + // Image delimiter and burst per class + PROTO__NMS_BURST_TYPE__H15_PER_CLASS = 2; + // Image delimiter and burst per image + PROTO__NMS_BURST_TYPE__H15_PER_FRAME = 3; +} + // NMS specific parameters message ProtoHEFNmsInfo { uint32 type_index = 1; @@ -749,6 +844,8 @@ message ProtoHEFNmsInfo { bool is_defused = 5; ProtoHEFNmsDefuseInfo defuse_info = 6; uint64 input_division_factor = 7; + uint32 burst_size = 8; + ProtoHEFNmsBurstType burst_type = 9; } message ProtoHEFNmsDefuseInfo { @@ -757,10 +854,12 @@ message ProtoHEFNmsDefuseInfo { } message ProtoHEFEdgeLayerNumericInfo { - float qp_zp = 1; - float qp_scale = 2; + float qp_zp = 1; // TODO: Remove, use vector + float qp_scale = 2; // TODO: Remove, use vector float limvals_min = 3; float limvals_max = 4; + repeated double qp_zps = 5; // zp per feature + repeated double qp_scales = 6; // scale per feature } // An object that can be repeated in order to provide the order of the triggers. @@ -781,4 +880,4 @@ message ProtoHEFHwPackageInfo { uint32 dense_alignment_size = 1; uint32 axi_width = 2; uint32 memory_width = 3; -} \ No newline at end of file +} diff --git a/hailort/libhailort/include/hailo/buffer.hpp b/hailort/libhailort/include/hailo/buffer.hpp index 311185e..e0693ba 100644 --- a/hailort/libhailort/include/hailo/buffer.hpp +++ b/hailort/libhailort/include/hailo/buffer.hpp @@ -11,6 +11,7 @@ #define _HAILO_BUFFER_HPP_ #include "hailo/expected.hpp" +#include "hailo/buffer_storage.hpp" #include #include @@ -19,6 +20,7 @@ #include +/** hailort namespace */ namespace hailort { @@ -48,6 +50,8 @@ public: // Empty buffer (points to null, size is zero) Buffer(); + // Buffer backed by the storage param + Buffer(BufferStoragePtr storage); ~Buffer() = default; Buffer(const Buffer& other) = delete; @@ -60,18 +64,20 @@ public: * Create functions, may fail be due to out of memory */ // Creates a buffer size bytes long, without setting the memory - static Expected create(size_t size); + static Expected create(size_t size, const BufferStorageParams ¶ms = {}); // Creates a buffer size bytes long, setting the memory to default_value - static Expected create(size_t size, uint8_t default_value); + static Expected create(size_t size, uint8_t default_value, const BufferStorageParams ¶ms = {}); // Creates a copy of the data pointed to by src, size bytes long - static Expected create(const uint8_t *src, size_t size); + static Expected create(const uint8_t *src, size_t size, const BufferStorageParams ¶ms = {}); // Creates a new buffer with the contents of the initializer_list - static Expected create(std::initializer_list init); - + static Expected create(std::initializer_list init, const BufferStorageParams ¶ms = {}); + // Creates a buffer size bytes long, without setting the memory - static Expected create_shared(size_t size); + static Expected create_shared(size_t size, const BufferStorageParams ¶ms = {}); // Creates a buffer size bytes long, setting the memory to default_value - static Expected create_shared(size_t size, uint8_t default_value); + static Expected create_shared(size_t size, uint8_t default_value, const BufferStorageParams ¶ms = {}); + // Creates a copy of the data pointed to by src, size bytes long + static Expected create_shared(const uint8_t *src, size_t size, const BufferStorageParams ¶ms = {}); // Moves the data pointed to by other into the lvalue: // * other is invalidated. @@ -92,16 +98,14 @@ public: iterator begin(); iterator end(); + BufferStorage &storage(); + // Returns a pointer to the start of the buffer uint8_t* data() noexcept; const uint8_t* data() const noexcept; // Returns the size of the buffer size_t size() const noexcept; - - // Returns a pointer to the start of the buffer and releases the ownership - // Free the returned pointer with `delete` - uint8_t* release() noexcept; // Casts the buffer to a string of length size(). // If there's a null char in the buffer, the string will terminate at the null char @@ -117,7 +121,7 @@ public: T* as_pointer() const { assert(m_size >= sizeof(T)); - return reinterpret_cast(m_data.get()); + return reinterpret_cast(m_data); } // Returns a copy of the data at the start of the buffer, cast to T @@ -126,11 +130,11 @@ public: T as_type() const { assert(m_size >= sizeof(T)); - return *(reinterpret_cast(m_data.get())); + return *(reinterpret_cast(m_data)); } // The following functions return a copy of the data at the start of the buffer, cast to uint16/32/64_t - // Note: If this->size() is less than the size of the ineger type, then the copy will hold data + // Note: If this->size() is less than the size of the integer type, then the copy will hold data // that isn't from the buffer! uint16_t as_uint16() const; uint32_t as_uint32() const; @@ -146,16 +150,16 @@ public: } // The following functions return references of the data at the start of the buffer, cast to uint16/32/64_t - // Note: If this->size() is less than the size of the ineger type, then the copy will hold data + // Note: If this->size() is less than the size of the integer type, then the copy will hold data // that isn't from the buffer! uint16_t& as_uint16(); uint32_t& as_uint32(); uint64_t& as_uint64(); private: - Buffer(std::unique_ptr data, size_t size); - - std::unique_ptr m_data; + // Initialization dependency + BufferStoragePtr m_storage; + uint8_t *m_data; size_t m_size; }; @@ -170,7 +174,7 @@ public: explicit MemoryView(Buffer &buffer); MemoryView(void *data, size_t size); ~MemoryView() = default; - + MemoryView& operator=(MemoryView&& other) = default; MemoryView(const MemoryView &) = default; MemoryView& operator=(MemoryView &) = default; diff --git a/hailort/libhailort/include/hailo/buffer_storage.hpp b/hailort/libhailort/include/hailo/buffer_storage.hpp new file mode 100644 index 0000000..6a9bd45 --- /dev/null +++ b/hailort/libhailort/include/hailo/buffer_storage.hpp @@ -0,0 +1,240 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file buffer_storage.hpp + * @brief TODO: fill me (HRT-10026) + **/ + +#ifndef _HAILO_BUFFER_STORAGE_HPP_ +#define _HAILO_BUFFER_STORAGE_HPP_ + +#include "hailo/hailort.h" +#include "hailo/expected.hpp" + +#include +#include +#include +#include +#include +#include + + +/** hailort namespace */ +namespace hailort +{ + +// Forward declarations +class Device; +class VDevice; +class BufferStorage; +class HeapStorage; +class DmaStorage; +class HailoRTDriver; + +namespace vdma { + class DmaAbleBuffer; + using DmaAbleBufferPtr = std::shared_ptr; + + class MappedBuffer; + using MappedBufferPtr = std::shared_ptr; +} + + +/*! Buffer storage parameters. Analogical to hailo_buffer_parameters_t */ +struct HAILORTAPI BufferStorageParams +{ +public: + struct HeapParams + { + public: + HeapParams(); + }; + + struct DmaMappingParams + { + public: + static Expected create(const hailo_buffer_dma_mapping_params_t ¶ms); + // DmaMappingParams for a buffer to be mapped to device + DmaMappingParams(Device &device, hailo_dma_buffer_direction_t data_direction); + // DmaMappingParams for a buffer to be mapped to all the underlying devices held by vdevice + DmaMappingParams(VDevice &vdevice, hailo_dma_buffer_direction_t data_direction); + // DmaMappingParams for a buffer to be lazily mapped upon it's first async transfer to a given device + DmaMappingParams(); + + // Note: We hold a pointer to a Device/VDevice/neither, since DmaMappingParams support mapping to + // a device, vdevice or lazy mapping + Device *device; + VDevice *vdevice; + hailo_dma_buffer_direction_t data_direction; + + private: + DmaMappingParams(const hailo_buffer_dma_mapping_params_t ¶ms); + }; + + static Expected create(const hailo_buffer_parameters_t ¶ms); + // Dma buffer params for lazy mapping + static BufferStorageParams create_dma(); + // Dma buffer params for mapping to device in data_direction + static BufferStorageParams create_dma(Device &device, hailo_dma_buffer_direction_t data_direction); + // Dma buffer params for mapping to vdevice in data_direction + static BufferStorageParams create_dma(VDevice &vdevice, hailo_dma_buffer_direction_t data_direction); + + // Defaults to heap params + BufferStorageParams(); + + hailo_buffer_flags_t flags; + union { + HeapParams heap_params; + DmaMappingParams dma_mapping_params; + }; +}; + +using BufferStoragePtr = std::shared_ptr; + +class HAILORTAPI BufferStorage +{ +public: + enum class Type { + HEAP, + DMA + }; + + static Expected create(size_t size, const BufferStorageParams ¶ms); + + BufferStorage(BufferStorage&& other) noexcept = default; + BufferStorage(const BufferStorage &) = delete; + BufferStorage &operator=(BufferStorage &&) = delete; + BufferStorage &operator=(const BufferStorage &) = delete; + virtual ~BufferStorage() = default; + + Type type() const; + virtual size_t size() const = 0; + virtual void *user_address() = 0; + // Returns the pointer managed by this object and releases ownership + // TODO: Add a free function pointer? (HRT-10024) + // // Free the returned pointer with `delete` + // TODO: after release the containing buffer will hold pointers to values that were released. + // Document that this can happen? Disable this behavior somehow? (HRT-10024) + virtual Expected release() noexcept = 0; + // Maps the storage to device in data_direction. + // - If the mapping is new - true is returned. + // - If the mapping already exists - false is returned. + // - Otherwise - Unexpected with a failure status is returned. + virtual Expected dma_map(Device &device, hailo_dma_buffer_direction_t data_direction) = 0; + // Maps the backing buffer to a device via driver in data_direction, returning a pointer to it. + // - If the mapping is new - true is returned. + // - If the mapping already exists - false is returned. + // - Otherwise - Unexpected with a failure status is returned. + virtual Expected dma_map(HailoRTDriver &driver, hailo_dma_buffer_direction_t data_direction) = 0; + + // Internal functions + virtual Expected get_dma_mapped_buffer(const std::string &device_id) = 0; + +protected: + explicit BufferStorage(Type type); + + const Type m_type; +}; + +using HeapStoragePtr = std::shared_ptr; + +class HAILORTAPI HeapStorage : public BufferStorage +{ +public: + static Expected create(size_t size); + HeapStorage(std::unique_ptr data, size_t size); + HeapStorage(HeapStorage&& other) noexcept; + HeapStorage(const HeapStorage &) = delete; + HeapStorage &operator=(HeapStorage &&) = delete; + HeapStorage &operator=(const HeapStorage &) = delete; + virtual ~HeapStorage() = default; + + virtual size_t size() const override; + virtual void *user_address() override; + virtual Expected release() noexcept override; + virtual Expected dma_map(Device &device, hailo_dma_buffer_direction_t data_direction) override; + virtual Expected dma_map(HailoRTDriver &driver, hailo_dma_buffer_direction_t data_direction) override; + + // Internal functions + virtual Expected get_dma_mapped_buffer(const std::string &device_id) override; + +private: + std::unique_ptr m_data; + size_t m_size; +}; + +// ************************************* NOTE - START ************************************* // +// DmaStorage isn't currently supported and is for internal use only // +// **************************************************************************************** // +using DmaStoragePtr = std::shared_ptr; + +// TODO: HRT-10026 doc this +class HAILORTAPI DmaStorage : public BufferStorage +{ +public: + // Creates a DmaStorage instance holding a dma-able buffer size bytes large. + // The buffer isn't mapped to dma until dma_map is called. + static Expected create(size_t size); + // Creates a DmaStorage instance holding a dma-able buffer size bytes large. + // The buffer is mapped to device in data_direction. + static Expected create(size_t size, + hailo_dma_buffer_direction_t data_direction, Device &device); + // Creates a DmaStorage instance holding a dma-able buffer size bytes large. + // The buffer is mapped to vdevice.get_physical_devices() in data_direction. + static Expected create(size_t size, + hailo_dma_buffer_direction_t data_direction, VDevice &vdevice); + + // TODO: doc that the addr needs to be on a new page and aligned to 64B (HRT-9559) + // probably best just to call mmap + // Creates a DmaStorage instance backed by the size bytes large buffer pointed to by user_address. + // The buffer isn't mapped to dma until dma_map is called. + static Expected create_from_user_address(void *user_address, size_t size); + // Creates a DmaStorage instance backed by the size bytes large buffer pointed to by user_address. + // The buffer is mapped to device in data_direction. + static Expected create_from_user_address(void *user_address, size_t size, + hailo_dma_buffer_direction_t data_direction, Device &device); + // Creates a DmaStorage instance backed by the size bytes large buffer pointed to by user_address. + // The buffer is mapped to vdevice.get_physical_devices() in data_direction. + static Expected create_from_user_address(void *user_address, size_t size, + hailo_dma_buffer_direction_t data_direction, VDevice &device); + + DmaStorage(const DmaStorage &other) = delete; + DmaStorage &operator=(const DmaStorage &other) = delete; + DmaStorage(DmaStorage &&other) noexcept = default; + DmaStorage &operator=(DmaStorage &&other) = delete; + virtual ~DmaStorage() = default; + + virtual size_t size() const override; + virtual void *user_address() override; + virtual Expected release() noexcept override; + // TODO: thread safety (HRT-10669) + virtual Expected dma_map(Device &device, hailo_dma_buffer_direction_t data_direction) override; + virtual Expected dma_map(HailoRTDriver &driver, hailo_dma_buffer_direction_t data_direction) override; + + // Internal functions + DmaStorage(vdma::DmaAbleBufferPtr &&dma_able_buffer); + virtual Expected get_dma_mapped_buffer(const std::string &device_id) override; + +private: + // Creates a backing dma-able buffer (either user or hailort allocated). + // Maps said buffer to physical_devices in data_direction. + // By default (if physical_devices is empty), no mapping will occur + static Expected create(void *user_address, size_t size, + hailo_dma_buffer_direction_t data_direction = HAILO_DMA_BUFFER_DIRECTION_MAX_ENUM, + std::vector> &&physical_devices = {}); + + vdma::DmaAbleBufferPtr m_dma_able_buffer; + + // For each device (key is device_id), we store some vdma mapping. + // TODO: use (device_id, direction) as key - HRT-10656 + std::unordered_map m_mappings; +}; +// ************************************** NOTE - END ************************************** // +// DmaStorage isn't currently supported and is for internal use only // +// **************************************************************************************** // + +} /* namespace hailort */ + +#endif /* _HAILO_BUFFER_STORAGE_HPP_ */ diff --git a/hailort/libhailort/include/hailo/device.hpp b/hailort/libhailort/include/hailo/device.hpp index 904b37e..bb12702 100644 --- a/hailort/libhailort/include/hailo/device.hpp +++ b/hailort/libhailort/include/hailo/device.hpp @@ -22,6 +22,7 @@ #include +/** hailort namespace */ namespace hailort { @@ -89,8 +90,8 @@ public: std::chrono::milliseconds timeout); /** - * Creates a device if there is only one system device detected in the system. - * + * Creates a device. If there are more than one device detected in the system, an arbitrary device is returned. + * * @return Upon success, returns Expected of a unique_ptr to Device object. * Otherwise, returns Unexpected of ::hailo_status error. */ @@ -98,19 +99,20 @@ public: /** * Creates a device by the given device id. - * + * * @param[in] device_id Device id string, can represent several device types: * [-] for pcie devices - pcie bdf (XXXX:XX:XX.X) * [-] for ethernet devices - ip address (xxx.xxx.xxx.xxx) - * + * * @return Upon success, returns Expected of a unique_ptr to Device object. * Otherwise, returns Unexpected of ::hailo_status error. */ static Expected> create(const std::string &device_id); /** - * Creates pcie device if there is only one pcie device connected - * + * Creates pcie device. If there are more than one device detected in the system, an arbitrary pcie device is + * returned. + * * @return Upon success, returns Expected of a unique_ptr to Device object. * Otherwise, returns Unexpected of ::hailo_status error. */ @@ -118,7 +120,7 @@ public: /** * Creates a PCIe device by the given info. - * + * * @param[in] device_info Information about the device to open. * @return Upon success, returns Expected of a unique_ptr to Device object. * Otherwise, returns Unexpected of ::hailo_status error. @@ -127,7 +129,7 @@ public: /** * Creates an ethernet device by the given info. - * + * * @param[in] device_info Information about the device to open. * @return Upon success, returns Expected of a unique_ptr to Device object. * Otherwise, returns Unexpected of ::hailo_status error. @@ -136,16 +138,28 @@ public: /** * Creates an ethernet device by IP address. - * + * * @param[in] ip_addr The device IP address. * @return Upon success, returns Expected of a unique_ptr to Device object. * Otherwise, returns Unexpected of ::hailo_status error. */ static Expected> create_eth(const std::string &ip_addr); + /** + * Creates an ethernet device by IP address, port number, timeout duration and max number of attempts + * + * @param[in] device_address The device IP address. + * @param[in] port The port number that the device will use for the Ethernet communication. + * @param[in] timeout_milliseconds The time in milliseconds to scan devices. + * @param[in] max_number_of_attempts The number of attempts to find a device. + * @return Upon success, returns Expected of a unique_ptr to Device object. + * Otherwise, returns Unexpected of ::hailo_status error. + */ + static Expected> create_eth(const std::string &device_address, uint16_t port, uint32_t timeout_milliseconds, uint8_t max_number_of_attempts); + /** * Parse PCIe device BDF string into hailo device info structure. - * + * * @param[in] device_info_str BDF device info, format [\].\.\.\, same format as in lspci. * @return Upon success, returns Expected of ::hailo_pcie_device_info_t containing the information. * Otherwise, returns Unexpected of ::hailo_status error. @@ -154,7 +168,7 @@ public: /** * Returns a string of pcie device info. - * + * * @param[in] device_info A ::hailo_pcie_device_info_t containing the pcie device information. * @return Upon success, returns Expected of a string containing the information. * Otherwise, returns Unexpected of ::hailo_status error. @@ -163,13 +177,22 @@ public: /** * Returns the device type of the given device id string. - * + * * @param[in] device_id A std::string device id to check. * @return Upon success, returns Expected of the device type. * Otherwise, returns Unexpected of ::hailo_status error. */ static Expected get_device_type(const std::string &device_id); + /** + * Checks if 2 device ids represents the same device. + * + * @param[in] first A std::string first device id to check. + * @param[in] second A std::string second device id to check. + * @return true if the device ids represents the same device. + */ + static bool device_ids_equal(const std::string &first, const std::string &second); + /** * Create the default configure params from an hef. * diff --git a/hailort/libhailort/include/hailo/dma_mapped_buffer.hpp b/hailort/libhailort/include/hailo/dma_mapped_buffer.hpp deleted file mode 100644 index f25ac37..0000000 --- a/hailort/libhailort/include/hailo/dma_mapped_buffer.hpp +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. - * Distributed under the MIT license (https://opensource.org/licenses/MIT) - **/ -/** - * @file dma_mapped_buffer.hpp - * @brief The mapped buffer that is continuous in virtual memory, but not on physical memory. - * We map the buffer to the IOMMU. - * - * The buffer can be used only with the help of a descriptors list that contains pointers to a physical - * continuous "dma pages". - * - * There are 2 options to allocated the buffer: - * 1. User mode allocation - the user mode calls `malloc` or `mmap` to allocate the buffer, then - * using HailoRTDriver we map the driver to the IOMMU (and pin the pages to avoid pagigs). - * This is the default option - * 2. Kernel mode allocation - on some systems, the user mode doesn't allocate the memory in a "dma-able" address, - * so we need to allocate the pages in driver. - **/ - -#ifndef _HAILO_DMA_MAPPED_BUFFER_HPP_ -#define _HAILO_DMA_MAPPED_BUFFER_HPP_ - -#include "hailo/expected.hpp" -#include "hailo/device.hpp" - - -namespace hailort { - -// Forward deceleration across namespaces -namespace vdma { - class DescriptorList; - class MappedBufferFactory; - class BufferedChannel; -} - -// ******************************************** NOTE ******************************************** // -// Async Stream API and DmaMappedBuffer are currently not supported and are for internal use only // -// ********************************************************************************************** // -class HAILORTAPI DmaMappedBuffer final -{ -public: - static Expected create(size_t size, - hailo_vdma_buffer_direction_flags_t data_direction_flags, Device &device); - // TODO: doc that the addr needs to be on a new page and aligned to 64B (HRT-9559) - // probably best just to call mmap - static Expected create_from_user_address(void *user_address, size_t size, - hailo_vdma_buffer_direction_flags_t data_direction_flags, Device &device); - - DmaMappedBuffer(const DmaMappedBuffer &other) = delete; - DmaMappedBuffer &operator=(const DmaMappedBuffer &other) = delete; - DmaMappedBuffer(DmaMappedBuffer &&other) noexcept; - DmaMappedBuffer &operator=(DmaMappedBuffer &&other) = delete; - ~DmaMappedBuffer(); - - void *user_address(); - size_t size() const; - hailo_status synchronize(); - -private: - static Expected create(void *user_address, size_t size, - hailo_vdma_buffer_direction_flags_t data_direction_flags, Device &device); - - // Need access to pimpl - friend class vdma::DescriptorList; - friend class vdma::MappedBufferFactory; - friend class vdma::BufferedChannel; - - class Impl; - explicit DmaMappedBuffer(std::unique_ptr pimpl); - std::unique_ptr pimpl; -}; - -} /* namespace hailort */ - -#endif /* _HAILO_DMA_MAPPED_BUFFER_HPP_ */ \ No newline at end of file diff --git a/hailort/libhailort/include/hailo/event.hpp b/hailort/libhailort/include/hailo/event.hpp index e46517b..28ebf61 100644 --- a/hailort/libhailort/include/hailo/event.hpp +++ b/hailort/libhailort/include/hailo/event.hpp @@ -31,6 +31,7 @@ namespace neosmart { } #endif // defined (__QNX__) +/** hailort namespace */ namespace hailort { @@ -50,7 +51,7 @@ using WaitablePtrList = std::vector; class HAILORTAPI Waitable { -public: +public: explicit Waitable(underlying_waitable_handle_t handle); virtual ~Waitable(); Waitable(Waitable&& other); @@ -60,30 +61,31 @@ public: Waitable& operator=(Waitable&&) = delete; // Blocks the current thread until the waitable is signaled - // * If this->is_auto_reset(), then the Waitable is reset after wait returns with HAILO_SUCCESS + // * If this->is_auto_reset(), then the Waitable is reset after wait returns with HAILO_SUCCESS // * Otherwise, the Waitable is not reset - virtual hailo_status wait(std::chrono::milliseconds timeout) = 0; + virtual hailo_status wait(std::chrono::milliseconds timeout); virtual hailo_status signal() = 0; virtual bool is_auto_reset() = 0; underlying_waitable_handle_t get_underlying_handle(); -#if defined(__QNX__) - virtual void post_wait() = 0; -#endif // defined (__QNX__) static constexpr auto INIFINITE_TIMEOUT() { return std::chrono::milliseconds(HAILO_INFINITE); } protected: - #if defined(_MSC_VER) || defined(__QNX__) + virtual hailo_status post_wait() = 0; + static hailo_status wait_for_single_object(underlying_waitable_handle_t handle, std::chrono::milliseconds timeout); - #else + +#if defined(__linux__) // Waits on the fd until the waitable is signaled static hailo_status eventfd_poll(underlying_waitable_handle_t fd, std::chrono::milliseconds timeout); // Expected to be called after eventfd_poll returns HAILO_SUCCESS static hailo_status eventfd_read(underlying_waitable_handle_t fd); static hailo_status eventfd_write(underlying_waitable_handle_t fd); - #endif +#endif underlying_waitable_handle_t m_handle; + + friend class WaitableGroup; }; class Event; @@ -105,15 +107,15 @@ public: static Expected create(const State& initial_state); static EventPtr create_shared(const State& initial_state); - virtual hailo_status wait(std::chrono::milliseconds timeout) override; virtual hailo_status signal() override; virtual bool is_auto_reset() override; hailo_status reset(); -#if defined(__QNX__) - virtual void post_wait() override; -#endif // defined (__QNX__) + +protected: + virtual hailo_status post_wait() override { return HAILO_SUCCESS; } private: + static underlying_waitable_handle_t open_event_handle(const State& initial_state); }; @@ -129,16 +131,18 @@ public: static Expected create(uint32_t initial_count); static SemaphorePtr create_shared(uint32_t initial_count); - virtual hailo_status wait(std::chrono::milliseconds timeout) override; virtual hailo_status signal() override; virtual bool is_auto_reset() override; + #if defined(__QNX__) Semaphore(underlying_waitable_handle_t handle, uint32_t initial_count); Semaphore(Semaphore&& other); - virtual void post_wait() override; #endif // defined (__QNX__) +protected: + virtual hailo_status post_wait() override; + private: static underlying_waitable_handle_t open_semaphore_handle(uint32_t initial_count); #if defined (__QNX__) diff --git a/hailort/libhailort/include/hailo/expected.hpp b/hailort/libhailort/include/hailo/expected.hpp index 6c766f7..d911539 100644 --- a/hailort/libhailort/include/hailo/expected.hpp +++ b/hailort/libhailort/include/hailo/expected.hpp @@ -168,6 +168,7 @@ #include +/** hailort namespace */ namespace hailort { diff --git a/hailort/libhailort/include/hailo/hailort.h b/hailort/libhailort/include/hailo/hailort.h index 2ac0702..67e57b9 100644 --- a/hailort/libhailort/include/hailo/hailort.h +++ b/hailort/libhailort/include/hailo/hailort.h @@ -48,7 +48,7 @@ extern "C" { #define HAILO_DEFAULT_INIT_AVERAGING_FACTOR (HAILO_AVERAGE_FACTOR_256) #define HAILO_DEFAULT_BUFFERS_THRESHOLD (0) #define HAILO_DEFAULT_MAX_ETHERNET_BANDWIDTH_BYTES_PER_SEC (106300000) -#define HAILO_MAX_STREAMS_COUNT (32) +#define HAILO_MAX_STREAMS_COUNT (40) #define HAILO_DEFAULT_BATCH_SIZE (0) #define HAILO_MAX_NETWORK_GROUPS (8) #define HAILO_MAX_NETWORK_GROUP_NAME_SIZE (HAILO_MAX_NAME_SIZE) @@ -159,6 +159,10 @@ typedef uint16_t nms_bbox_counter_t; HAILO_STATUS__X(77, HAILO_RPC_FAILED /*!< RPC failed */)\ HAILO_STATUS__X(78, HAILO_INVALID_SERVICE_VERSION /*!< Invalid service version */)\ HAILO_STATUS__X(79, HAILO_NOT_SUPPORTED /*!< Not supported operation */)\ + HAILO_STATUS__X(80, HAILO_NMS_BURST_INVALID_DATA /*!< Invalid data in NMS burst */)\ + HAILO_STATUS__X(81, HAILO_OUT_OF_HOST_CMA_MEMORY /*!< Cannot allocate more CMA memory at host */)\ + HAILO_STATUS__X(82, HAILO_QUEUE_IS_FULL /*!< Cannot push more items into the queue */)\ + HAILO_STATUS__X(83, HAILO_DMA_MAPPING_ALREADY_EXISTS /*!< DMA mapping already exists */)\ typedef enum { #define HAILO_STATUS__X(value, name) name = value, @@ -167,7 +171,7 @@ typedef enum { /** Must be last! */ HAILO_STATUS_COUNT, - + /** Max enum value to maintain ABI Integrity */ HAILO_STATUS_MAX_ENUM = HAILO_MAX_ENUM } hailo_status; @@ -771,9 +775,6 @@ typedef enum { HAILO_STREAM_DIRECTION_MAX_ENUM = HAILO_MAX_ENUM } hailo_stream_direction_t; -// ******************************************** NOTE ******************************************** // -// Async Stream API and DmaMappedBuffer are currently not supported and are for internal use only // -// ********************************************************************************************** // /** Stream flags */ typedef enum { HAILO_STREAM_FLAGS_NONE = 0, /*!< No flags */ @@ -783,15 +784,57 @@ typedef enum { HAILO_STREAM_FLAGS_MAX_ENUM = HAILO_MAX_ENUM } hailo_stream_flags_t; -/** Hailo vdma buffer direction */ +// ************************************* NOTE - START ************************************* // +// Dma buffer allocation isn't currently supported and is for internal use only // +// **************************************************************************************** // +/** Hailo dma buffer direction */ +typedef enum { + HAILO_DMA_BUFFER_DIRECTION_H2D = 0, + HAILO_DMA_BUFFER_DIRECTION_D2H = 1, + HAILO_DMA_BUFFER_DIRECTION_BOTH = 2, + + /** Max enum value to maintain ABI Integrity */ + HAILO_DMA_BUFFER_DIRECTION_MAX_ENUM = HAILO_MAX_ENUM +} hailo_dma_buffer_direction_t; + +/** Hailo buffer flags */ typedef enum { - HAILO_VDMA_BUFFER_DIRECTION_FLAGS_NONE = 0, - HAILO_VDMA_BUFFER_DIRECTION_FLAGS_H2D = 1 << 0, - HAILO_VDMA_BUFFER_DIRECTION_FLAGS_D2H = 1 << 1, + HAILO_BUFFER_FLAGS_NONE = 0, /*!< No flags - heap allocated buffer */ + HAILO_BUFFER_FLAGS_DMA = 1 << 0, /*!< Buffer is mapped to DMA (will be page aligned implicitly) */ /** Max enum value to maintain ABI Integrity */ - HAILO_VDMA_BUFFER_DIRECTION_FLAGS_MAX_ENUM = HAILO_MAX_ENUM -} hailo_vdma_buffer_direction_flags_t; + HAILO_BUFFER_FLAGS_MAX_ENUM = HAILO_MAX_ENUM +} hailo_buffer_flags_t; + +/** Hailo buffer heap parameters */ +typedef struct { + EMPTY_STRUCT_PLACEHOLDER +} hailo_buffer_heap_params_t; + +// Hailo buffer dma mapping parameters. +// - If device is not NULL, the resulting buffer created by hailo_allocate_buffer will be mapped to the device. +// - If vdevice is not NULL, the resulting buffer created by hailo_allocate_buffer will be mapped to all the +// underlying devices held be vdevice. +// - If both device and vdevice are null, the resulting buffer created by hailo_allocate_buffer will be lazily +// mapped upon the first async transfer (i.e. when the buffer is passed to hailo_stream_read_raw_buffer_async +// or hailo_stream_write_raw_buffer_async). +typedef struct { + hailo_device device; + hailo_vdevice vdevice; + hailo_dma_buffer_direction_t direction; +} hailo_buffer_dma_mapping_params_t; + +/** Hailo buffer parameters */ +typedef struct { + hailo_buffer_flags_t flags; + union { + hailo_buffer_heap_params_t heap_params; + hailo_buffer_dma_mapping_params_t dma_mapping_params; + }; +} hailo_buffer_parameters_t; +// ************************************** NOTE - END ************************************** // +// Dma buffer allocation isn't currently supported and is for internal use only // +// **************************************************************************************** // /** Input or output data transform parameters */ typedef struct { @@ -1159,6 +1202,13 @@ typedef struct { char original_name[HAILO_MAX_STREAM_NAME_SIZE]; } hailo_nms_defuse_info_t; +typedef enum { + HAILO_BURST_TYPE_NO_BURST = 0, + HAILO_BURST_TYPE_H8_PER_CLASS = 1, + HAILO_BURST_TYPE_H15_PER_CLASS = 2, + HAILO_BURST_TYPE_H15_PER_FRAME = 3 +} hailo_nms_burst_type_t; + /** NMS Internal HW Info */ typedef struct { /** Amount of NMS classes */ @@ -1171,6 +1221,10 @@ typedef struct { uint32_t chunks_per_frame; bool is_defused; hailo_nms_defuse_info_t defuse_info; + /** Size of NMS burst in bytes */ + uint32_t burst_size; + /** NMS burst type */ + hailo_nms_burst_type_t burst_type; } hailo_nms_info_t; /** NMS Fuse Input */ @@ -1206,13 +1260,61 @@ typedef struct { } hailo_bbox_float32_t; #pragma pack(pop) +/** + * Completion info struct passed to the ::hailo_stream_write_async_callback_t after the async operation is + * done or has failed. + */ +typedef struct { + /** + * Status of the async transfer: + * - ::HAILO_SUCCESS - The transfer is complete. + * - ::HAILO_STREAM_ABORTED_BY_USER - The transfer was canceled (can happen after network deactivation). + * - Any other ::hailo_status on unexpected errors. + */ + hailo_status status; + + /** Address of the buffer passed to the async operation */ + const void *buffer_addr; + + /** Size of the buffer passed to the async operation. */ + size_t buffer_size; + + /** User specific data. Can be used as a context for the callback. */ + void *opaque; +} hailo_stream_write_async_completion_info_t; + +/** + * Async stream write complete callback prototype. + */ +typedef void (*hailo_stream_write_async_callback_t)(const hailo_stream_write_async_completion_info_t *info); + +/** + * Completion info struct passed to the ::hailo_stream_read_async_callback_t after the async operation is + * done or has failed. + */ typedef struct { /** - * - HAILO_SUCCESS when transfer is complete - * - HAILO_STREAM_NOT_ACTIVATED due to stream deactivation + * Status of the async transfer: + * - ::HAILO_SUCCESS - The transfer is complete. + * - ::HAILO_STREAM_ABORTED_BY_USER - The transfer was canceled (can happen after network deactivation). + * - Any other ::hailo_status on unexpected errors. */ hailo_status status; -} hailo_async_transfer_completion_info_t; + + /** Address of the buffer passed to the async operation */ + void *buffer_addr; + + /** Size of the buffer passed to the async operation. */ + size_t buffer_size; + + /** User specific data. Can be used as a context for the callback. */ + void *opaque; +} hailo_stream_read_async_completion_info_t; + +/** + * Async stream read complete callback prototype. + */ +typedef void (*hailo_stream_read_async_callback_t)(const hailo_stream_read_async_completion_info_t *info); /** * Input or output stream information. In case of multiple inputs or outputs, each one has @@ -1358,6 +1460,8 @@ typedef enum { HAILO_NOTIFICATION_ID_CONTEXT_SWITCH_BREAKPOINT_REACHED, /** Matches hailo_notification_message_parameters_t::health_monitor_clock_changed_notification */ HAILO_NOTIFICATION_ID_HEALTH_MONITOR_CLOCK_CHANGED_EVENT, + /** Matches hailo_notification_message_parameters_t::hailo_hw_infer_manager_infer_done_notification */ + HAILO_NOTIFICATION_ID_HW_INFER_MANAGER_INFER_DONE, /** Must be last! */ HAILO_NOTIFICATION_ID_COUNT, @@ -1443,6 +1547,10 @@ typedef struct { uint32_t current_clock; } hailo_health_monitor_clock_changed_notification_message_t; +typedef struct { + uint32_t infer_cycles; +} hailo_hw_infer_manager_infer_done_notification_message_t; + /** Union of all notification messages parameters. See ::hailo_notification_t */ typedef union { /** Ethernet rx error */ @@ -1463,6 +1571,8 @@ typedef union { hailo_context_switch_breakpoint_reached_message_t context_switch_breakpoint_reached_notification; /** Neural network core clock changed due to health monitor event */ hailo_health_monitor_clock_changed_notification_message_t health_monitor_clock_changed_notification; + /* HW infer manager finished infer notification */ + hailo_hw_infer_manager_infer_done_notification_message_t hw_infer_manager_infer_done_notification; } hailo_notification_message_parameters_t; /** Notification data that will be passed to the callback passed in ::hailo_notification_callback */ @@ -1689,7 +1799,7 @@ HAILORTAPI const char* hailo_get_status_message(hailo_status status); * device scanned. * @note ethernet devices are not considered "devices in the system", so they are not scanned in this function. * use :hailo_scan_ethernet_devices for ethernet devices. - * + * * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns a ::hailo_status error. */ HAILORTAPI hailo_status hailo_scan_devices(hailo_scan_devices_params_t *params, hailo_device_id_t *device_ids, @@ -1697,15 +1807,15 @@ HAILORTAPI hailo_status hailo_scan_devices(hailo_scan_devices_params_t *params, /** * Creates a device by the given device id. - * + * * @param[in] device_id Device id, can represent several device types: * [-] for pcie devices - pcie bdf (XXXX:XX:XX.X or XX:XX.X) * [-] for ethernet devices - ip address (xxx.xxx.xxx.xxx) - * If NULL is given and there is only one available system device, use this device. + * If NULL is given, uses an arbitrary device found on the system. * @param[out] device A pointer to a ::hailo_device that receives the allocated PCIe device. * @return Upon success, returns Expected of a unique_ptr to Device object. * Otherwise, returns Unexpected of ::hailo_status error. - * + * * @note To release a device, call the ::hailo_release_device function with the returned ::hailo_device. */ HAILORTAPI hailo_status hailo_create_device_by_id(const hailo_device_id_t *device_id, hailo_device *device); @@ -1727,7 +1837,7 @@ HAILORTAPI hailo_status hailo_scan_pcie_devices( /** * Parse PCIe device BDF string into hailo device info structure. - * + * * @param[in] device_info_str BDF device info, format [\].\.\.\, same format as in lspci. * @param[out] device_info A pointer to a ::hailo_pcie_device_info_t that receives the parsed device info. * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns an ::hailo_status error. @@ -1738,9 +1848,9 @@ HAILORTAPI hailo_status hailo_parse_pcie_device_info(const char *device_info_str /** * Creates a PCIe device. - * - * @param[in] device_info Information about the device to open. If NULL is given and there is only - * one available PCIe device, use this device. + * + * @param[in] device_info Information about the device to open. If NULL is given, uses an arbitrary device found on + * the system. * @param[out] device A pointer to a ::hailo_device that receives the allocated PCIe device. * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns an ::hailo_status error. * @note To release a device, call the ::hailo_release_device function with the returned ::hailo_device. @@ -1749,7 +1859,7 @@ HAILORTAPI hailo_status hailo_create_pcie_device(hailo_pcie_device_info_t *devic /** * Returns information on all available ethernet devices in the system. - * + * * @param[in] interface_name The name of the network interface to scan. * @param[out] eth_device_infos A pointer to a buffer of ::hailo_eth_device_info_t that receives the * information. @@ -1766,7 +1876,7 @@ HAILORTAPI hailo_status hailo_scan_ethernet_devices(const char *interface_name, /** * Creates an ethernet device. - * + * * @param[in] device_info Information about the device to open. * @param[out] device A pointer to a ::hailo_device that receives the allocated ethernet device corresponding to * the given information. @@ -1777,7 +1887,7 @@ HAILORTAPI hailo_status hailo_create_ethernet_device(hailo_eth_device_info_t *de /** * Release an open device. - * + * * @param[in] device A ::hailo_device object to be released. * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns a ::hailo_status error. */ @@ -1785,7 +1895,7 @@ HAILORTAPI hailo_status hailo_release_device(hailo_device device); /** * Returns the device type of the given device id string. - * + * * @param[in] device_id A :hailo_device_id_t device id to check. * @param[out] device_type A :hailo_device_type_t returned device type. * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns a ::hailo_status error. @@ -2451,6 +2561,28 @@ HAILORTAPI hailo_status hailo_calculate_eth_input_rate_limits(hailo_hef hef, con HAILORTAPI hailo_status hailo_init_configure_params(hailo_hef hef, hailo_stream_interface_t stream_interface, hailo_configure_params_t *params); +/** + * Init configure params with default values for a given hef by virtual device. + * + * @param[in] hef A ::hailo_hef object to configure the @a device by. + * @param[in] vdevice A @a hailo_vdevice for which we init the params for. + * @param[out] params A @a hailo_configure_params_t to be filled. + * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns a ::hailo_status error. + */ +HAILORTAPI hailo_status hailo_init_configure_params_by_vdevice(hailo_hef hef, hailo_vdevice vdevice, + hailo_configure_params_t *params); + +/** + * Init configure params with default values for a given hef by device. + * + * @param[in] hef A ::hailo_hef object to configure the @a device by. + * @param[in] device A @a hailo_device for which we init the params for. + * @param[out] params A @a hailo_configure_params_t to be filled. + * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns a ::hailo_status error. + */ +HAILORTAPI hailo_status hailo_init_configure_params_by_device(hailo_hef hef, hailo_device device, + hailo_configure_params_t *params); + /** * Init configure params with default values for a given hef, where all input_streams_params are init to be MIPI type. * @@ -2690,6 +2822,24 @@ HAILORTAPI hailo_status hailo_set_scheduler_priority(hailo_configured_network_gr /** @} */ // end of group_network_group_functions +/** @defgroup group_buffer_functions Buffer functions + * @{ + */ +// ************************************* NOTE - START ************************************* // +// Dma buffer allocation isn't currently supported and is for internal use only // +// **************************************************************************************** // +// Free returned buffer via hailo_free_buffer +HAILORTAPI hailo_status hailo_allocate_buffer(size_t size, const hailo_buffer_parameters_t *allocation_params, void **buffer_out); +HAILORTAPI hailo_status hailo_free_buffer(void *buffer); +// Maps buffer to dma. Free mapping by calling hailo_dma_unmap_buffer_from_device and then free buffer as needed +// If buffer has already been mapped to device, then HAILO_DMA_MAPPING_ALREADY_EXISTS shall be returned +HAILORTAPI hailo_status hailo_dma_map_buffer_to_device(void *buffer, size_t size, hailo_device device, hailo_dma_buffer_direction_t direction); +HAILORTAPI hailo_status hailo_dma_unmap_buffer_from_device(void *buffer, hailo_device device, hailo_dma_buffer_direction_t direction); +// ************************************** NOTE - END ************************************** // +// Dma buffer allocation isn't currently supported and is for internal use only // +// **************************************************************************************** // +/** @} */ // end of group_buffer_functions + /** @defgroup group_stream_functions Stream functions * @{ */ @@ -2748,36 +2898,166 @@ HAILORTAPI hailo_status hailo_get_output_stream_info(hailo_output_stream stream, /** * Synchronously reads data from a stream. - * + * * @param[in] stream A ::hailo_output_stream object. * @param[in] buffer A pointer to a buffer that receives the data read from @a stream. * @param[in] size The amount of bytes to read, should be the frame size. - * + * * @note The output buffer format comes from the \e format field inside ::hailo_stream_info_t and the shape comes from * the \e hw_shape field inside ::hailo_stream_info_t. + * @note @a size is expected to be stream_info.hw_frame_size. * - * @note @a size is expected to be a product of stream_info.hw_frame_size (i.e. more than one frame may be read) - * * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns a ::hailo_status error. */ HAILORTAPI hailo_status hailo_stream_read_raw_buffer(hailo_output_stream stream, void *buffer, size_t size); /** * Synchronously writes all data to a stream. - * + * * @param[in] stream A ::hailo_input_stream object. * @param[in] buffer A pointer to a buffer that contains the data to be written to @a stream. * @param[in] size The amount of bytes to write. - * + * * @note The input buffer format comes from the \e format field inside ::hailo_stream_info_t and the shape comes from * the \e hw_shape field inside ::hailo_stream_info_t. + * @note @a size is expected to be stream_info.hw_frame_size. * - * @note @a size is expected to be a product of stream_info.hw_frame_size (i.e. more than one frame may be read) - * * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns a ::hailo_status error. */ HAILORTAPI hailo_status hailo_stream_write_raw_buffer(hailo_input_stream stream, const void *buffer, size_t size); +/** + * Waits until the stream is ready to launch a new ::hailo_stream_read_raw_buffer_async operation. Each stream has a + * limited-size queue for ongoing transfers. You can retrieve the queue size for the given stream by calling + * ::hailo_output_stream_get_async_max_queue_size. + * + * @param[in] stream A ::hailo_output_stream object. + * @param[in] transfer_size Must be the result of ::hailo_get_output_stream_frame_size for the given stream. + * @param[in] timeout_ms Amount of time to wait until the stream is ready in milliseconds. + * + * @return Upon success, returns ::HAILO_SUCCESS. Otherwise: + * - If @a timeout_ms has passed and the stream is not ready, returns ::HAILO_TIMEOUT. + * - In any other error case, returns ::hailo_status error. + * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns a ::hailo_status error. + */ +HAILORTAPI hailo_status hailo_stream_wait_for_async_output_ready(hailo_output_stream stream, size_t transfer_size, + uint32_t timeout_ms); + +/** + * Waits until the stream is ready to launch a new ::hailo_stream_write_raw_buffer_async operation. Each stream has a + * limited-size queue for ongoing transfers. You can retrieve the queue size for the given stream by calling + * ::hailo_input_stream_get_async_max_queue_size. + * + * @param[in] stream A ::hailo_input_stream object. + * @param[in] transfer_size Must be the result of ::hailo_get_input_stream_frame_size for the given stream. + * @param[in] timeout_ms Amount of time to wait until the stream is ready in milliseconds. + * + * @return Upon success, returns ::HAILO_SUCCESS. Otherwise: + * - If @a timeout_ms has passed and the stream is not ready, returns ::HAILO_TIMEOUT. + * - In any other error case, returns ::hailo_status error. + */ +HAILORTAPI hailo_status hailo_stream_wait_for_async_input_ready(hailo_input_stream stream, size_t transfer_size, + uint32_t timeout_ms); + +/** + * Returns the maximum amount of frames that can be simultaneously read from the stream (by + * ::hailo_stream_read_raw_buffer_async calls) before any one of the read operations is complete, as signified by + * @a user_callback being called. + * + * @param[in] stream A ::hailo_output_stream object. + * @param[out] queue_size Returns value of the queue + * + * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns a ::hailo_status error. + */ +HAILORTAPI hailo_status hailo_output_stream_get_async_max_queue_size(hailo_output_stream stream, size_t *queue_size); + +/** + * Returns the maximum amount of frames that can be simultaneously written to the stream (by + * ::hailo_stream_write_raw_buffer_async calls) before any one of the write operations is complete, as signified by + * @a user_callback being called. + * + * @param[in] stream A ::hailo_input_stream object. + * @param[out] queue_size Returns value of the queue + * + * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns a ::hailo_status error. + */ +HAILORTAPI hailo_status hailo_input_stream_get_async_max_queue_size(hailo_input_stream stream, size_t *queue_size); + +/** + * Reads into @a buffer from the stream asynchronously, initiating a deferred operation that will be completed + * later. + * - If the function call succeeds (i.e., ::hailo_stream_read_raw_buffer_async returns ::HAILO_SUCCESS), the deferred + * operation has been initiated. Until @a user_callback is called, the user cannot change or delete @a buffer. + * - If the function call fails (i.e., ::hailo_stream_read_raw_buffer_async returns a status other than + * ::HAILO_SUCCESS), the deferred operation will not be initiated and @a user_callback will not be invoked. The user + * is free to change or delete @a buffer. + * - @a user_callback is triggered upon successful completion or failure of the deferred operation. + * The callback receives a ::hailo_stream_read_async_completion_info_t object containing a pointer to the transferred + * buffer (@a buffer_addr) and the transfer status (@a status). If the operation has completed successfully, the + * contents of @a buffer will have been updated by the read operation. + * + * @param[in] stream A ::hailo_output_stream object. + * @param[in] buffer The buffer to be read into. + * The buffer must be aligned to the system page size. + * @param[in] size The size of the given buffer, expected to be the result of + * ::hailo_get_output_stream_frame_size. + * @param[in] user_callback The callback that will be called when the transfer is complete or has failed. + * @param[in] opaque Optional pointer to user-defined context (may be NULL if not desired). + * + * @return Upon success, returns ::HAILO_SUCCESS. Otherwise: + * - If the stream queue is full, returns ::HAILO_QUEUE_IS_FULL. + * In this case, please wait until @a user_callback is called on previous + * reads, or call ::hailo_stream_wait_for_async_output_ready. The size of the queue can be + * determined by calling ::hailo_output_stream_get_async_max_queue_size. + * - In any other error case, returns a ::hailo_status error. + * + * @note @a user_callback should execute as quickly as possible. + * @note The output buffer format comes from the \e format field inside ::hailo_stream_info_t and the shape comes from + * the \e hw_shape field inside ::hailo_stream_info_t. + * @note The address provided must be aligned to the system's page size, and the rest of the page should not be in + * use by any other part of the program to ensure proper functioning of the DMA operation. Memory for the + * provided address can be allocated using `mmap` on Unix-like systems or `VirtualAlloc` on Windows. + */ +HAILORTAPI hailo_status hailo_stream_read_raw_buffer_async(hailo_output_stream stream, void *buffer, size_t size, + hailo_stream_read_async_callback_t user_callback, void *opaque); + +/** + * Writes the contents of @a buffer to the stream asynchronously, initiating a deferred operation that will be + * completed later. + * - If the function call succeeds (i.e., ::hailo_stream_write_raw_buffer_async returns ::HAILO_SUCCESS), the deferred + * operation has been initiated. Until @a user_callback is called, the user cannot change or delete @a buffer. + * - If the function call fails (i.e., ::hailo_stream_write_raw_buffer_async returns a status other than + * ::HAILO_SUCCESS), the deferred operation will not be initiated and @a user_callback will not be invoked. The user + * is free to change or delete @a buffer. + * - @a user_callback is triggered upon successful completion or failure of the deferred operation. The callback + * receives a ::hailo_stream_write_async_completion_info_t object containing a pointer to the transferred buffer + * (@a buffer_addr) and the transfer status (@a status). + * + * @param[in] stream A ::hailo_input_stream object. + * @param[in] buffer The buffer to be written. + * The buffer must be aligned to the system page size. + * @param[in] size The size of the given buffer, expected to be the result of + * ::hailo_get_input_stream_frame_size. + * @param[in] user_callback The callback that will be called when the transfer is complete + * or has failed. + * @param[in] opaque Optional pointer to user-defined context (may be NULL if not desired). + * + * @return Upon success, returns ::HAILO_SUCCESS. Otherwise: + * - If the stream queue is full, returns ::HAILO_QUEUE_IS_FULL. In this case please wait + * until @a user_callback is called on previous writes, or call ::hailo_stream_wait_for_async_input_ready. + * The size of the queue can be determined by calling ::hailo_input_stream_get_async_max_queue_size. + * - In any other error case, returns a ::hailo_status error. + * + * @note @a user_callback should run as quickly as possible. + * @note The input buffer format comes from the \e format field inside ::hailo_stream_info_t and the shape comes from + * the \e hw_shape field inside ::hailo_stream_info_t. + * @note The address provided must be aligned to the system's page size, and the rest of the page should not be in + * use by any other part of the program to ensure proper functioning of the DMA operation. Memory for the + * provided address can be allocated using `mmap` on Unix-like systems or `VirtualAlloc` on Windows. + */ +HAILORTAPI hailo_status hailo_stream_write_raw_buffer_async(hailo_input_stream stream, const void *buffer, size_t size, + hailo_stream_write_async_callback_t user_callback, void *opaque); + /** * Gets the size of a stream's frame on the host side in bytes * (the size could be affected by the format type - for example using UINT16, or by the data not being quantized yet) @@ -2937,7 +3217,7 @@ HAILORTAPI hailo_status hailo_create_demuxer_by_stream(hailo_output_stream strea HAILORTAPI hailo_status hailo_release_output_demuxer(hailo_output_demuxer demuxer); /** - * Demultiplexing an output frame pointed to by @a src directly to the buffer pointed to by @a dst. + * Demultiplexing an output frame pointed to by @a src directly to the buffers pointed to by @a raw_buffers. * * @param[in] demuxer A ::hailo_output_demuxer object used for the demuxing. * @param[in] src A pointer to a buffer to be demultiplexed. @@ -2947,11 +3227,29 @@ HAILORTAPI hailo_status hailo_release_output_demuxer(hailo_output_demuxer demuxe * demultiplexed data read from the @a stream. * @param[in] raw_buffers_count The number of ::hailo_stream_raw_buffer_t elements in the array pointed to by * @a raw_buffers. + * @note The order of @a raw_buffers should be the same as returned from the function 'hailo_get_mux_infos_by_output_demuxer()'. * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns a ::hailo_status error. */ HAILORTAPI hailo_status hailo_demux_raw_frame_by_output_demuxer(hailo_output_demuxer demuxer, const void *src, size_t src_size, hailo_stream_raw_buffer_t *raw_buffers, size_t raw_buffers_count); +/** + * Demultiplexing an output frame pointed to by @a src directly to the buffers pointed to by @a raw_buffers_by_name. + * + * @param[in] demuxer A ::hailo_output_demuxer object used for the demuxing. + * @param[in] src A pointer to a buffer to be demultiplexed. + * @param[in] src_size The number of bytes to demultiplexed. This number must be equal to the + * hw_frame_size, and less than or equal to the size of @a src buffer. + * @param[in,out] raw_buffers_by_name A pointer to an array of ::hailo_stream_raw_buffer_by_name_t that receives the + * demultiplexed data read from the @a stream. hailo_stream_raw_buffer_by_name_t::name should + * be filled with the demuxes names. + * @param[in] raw_buffers_count The number of ::hailo_stream_raw_buffer_by_name_t elements in the array pointed to by + * @a raw_buffers_by_name. + * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns a ::hailo_status error. + */ +HAILORTAPI hailo_status hailo_demux_by_name_raw_frame_by_output_demuxer(hailo_output_demuxer demuxer, const void *src, + size_t src_size, hailo_stream_raw_buffer_by_name_t *raw_buffers_by_name, size_t raw_buffers_count); + /** * Gets all multiplexed stream infos. * diff --git a/hailort/libhailort/include/hailo/hailort.hpp b/hailort/libhailort/include/hailo/hailort.hpp index d429a7d..7c503a1 100644 --- a/hailort/libhailort/include/hailo/hailort.hpp +++ b/hailort/libhailort/include/hailo/hailort.hpp @@ -28,7 +28,6 @@ #include "hailo/runtime_statistics.hpp" #include "hailo/network_rate_calculator.hpp" #include "hailo/quantization.hpp" -#include "hailo/dma_mapped_buffer.hpp" #include "hailo/hailort_defaults.hpp" #endif /* _HAILORT_HPP_ */ diff --git a/hailort/libhailort/include/hailo/hailort_common.hpp b/hailort/libhailort/include/hailo/hailort_common.hpp index d9ab7cf..996c6ab 100644 --- a/hailort/libhailort/include/hailo/hailort_common.hpp +++ b/hailort/libhailort/include/hailo/hailort_common.hpp @@ -19,6 +19,7 @@ #include +/** hailort namespace */ namespace hailort { @@ -35,8 +36,6 @@ public: static const uint32_t BBOX_PARAMS = sizeof(hailo_bbox_t) / sizeof(uint16_t); static const uint32_t MAX_DEFUSED_LAYER_COUNT = 9; static const size_t HW_DATA_ALIGNMENT = 8; - static const uint64_t NMS_DELIMITER = 0xFFFFFFFFFFFFFFFF; - static const uint64_t NMS_DUMMY_DELIMITER = 0xFFFFFFFFFFFFFFFE; static const uint32_t MUX_INFO_COUNT = 32; static const uint32_t MAX_MUX_PREDECESSORS = 4; static const uint16_t ETH_INPUT_BASE_PORT = 32401; @@ -279,10 +278,11 @@ public: static constexpr uint32_t get_nms_hw_frame_size(const hailo_nms_info_t &nms_info) { const uint32_t size_per_class = static_cast(sizeof(nms_bbox_counter_t)) + - nms_info.bbox_size * nms_info.max_bboxes_per_class; + nms_info.bbox_size * std::max(nms_info.burst_size, nms_info.max_bboxes_per_class); const uint32_t size_per_chunk = nms_info.number_of_classes * size_per_class; - // 1 delimiter for an entire frame (since we are reading delimiters directly into the buffer and replacing them) - return nms_info.bbox_size + (nms_info.chunks_per_frame * size_per_chunk); + // Extra Burst size for frame (since may be reading bursts directly into the buffer and replacing them) + const uint32_t size_for_extra_burst = nms_info.bbox_size * nms_info.burst_size; + return (nms_info.chunks_per_frame * size_per_chunk) + size_for_extra_burst; } /** @@ -386,6 +386,22 @@ inline constexpr hailo_format_flags_t& operator|=(hailo_format_flags_t &a, hailo return a; } +inline constexpr hailo_format_flags_t operator&(hailo_format_flags_t a, hailo_format_flags_t b) +{ + return static_cast(static_cast(a) & static_cast(b)); +} + +inline constexpr hailo_format_flags_t& operator&=(hailo_format_flags_t &a, hailo_format_flags_t b) +{ + a = a & b; + return a; +} + +inline constexpr hailo_format_flags_t operator~(hailo_format_flags_t a) +{ + return static_cast(~(static_cast(a))); +} + inline constexpr hailo_vstream_stats_flags_t operator|(hailo_vstream_stats_flags_t a, hailo_vstream_stats_flags_t b) { return static_cast(static_cast(a) | static_cast(b)); diff --git a/hailort/libhailort/include/hailo/hailort_defaults.hpp b/hailort/libhailort/include/hailo/hailort_defaults.hpp index 9577eec..c0edbac 100644 --- a/hailort/libhailort/include/hailo/hailort_defaults.hpp +++ b/hailort/libhailort/include/hailo/hailort_defaults.hpp @@ -14,7 +14,7 @@ #include "hailo/expected.hpp" #include "hailo/network_group.hpp" - +/** hailort namespace */ namespace hailort { diff --git a/hailort/libhailort/include/hailo/hef.hpp b/hailort/libhailort/include/hailo/hef.hpp index ad16c39..3a06ada 100644 --- a/hailort/libhailort/include/hailo/hef.hpp +++ b/hailort/libhailort/include/hailo/hef.hpp @@ -18,7 +18,7 @@ #include #include - +/** hailort namespace */ namespace hailort { @@ -452,7 +452,7 @@ public: */ std::string hash() const; - Expected get_hef_description(bool stream_infos, bool vstream_infos); + Expected get_description(bool stream_infos, bool vstream_infos); ~Hef(); Hef(Hef &&); diff --git a/hailort/libhailort/include/hailo/inference_pipeline.hpp b/hailort/libhailort/include/hailo/inference_pipeline.hpp index c4a3254..201d644 100644 --- a/hailort/libhailort/include/hailo/inference_pipeline.hpp +++ b/hailort/libhailort/include/hailo/inference_pipeline.hpp @@ -13,6 +13,7 @@ #include "hailo/vstream.hpp" +/** hailort namespace */ namespace hailort { diff --git a/hailort/libhailort/include/hailo/network_group.hpp b/hailort/libhailort/include/hailo/network_group.hpp index ae9ae8e..6b8d029 100644 --- a/hailort/libhailort/include/hailo/network_group.hpp +++ b/hailort/libhailort/include/hailo/network_group.hpp @@ -18,7 +18,7 @@ #include #include - +/** hailort namespace */ namespace hailort { @@ -44,6 +44,15 @@ using OutputStreamWithParamsVector = std::vector get_intermediate_buffer(const IntermediateBufferKey &key) = 0; - + // TODO HRT-10799: remove when enable batch switch flow for hailo15 virtual hailo_status set_keep_nn_config_during_reset(const bool keep_nn_config_during_reset) = 0; /** @@ -385,10 +394,18 @@ public: virtual Expected> create_input_vstreams(const std::map &inputs_params) = 0; virtual Expected> create_output_vstreams(const std::map &outputs_params) = 0; + virtual Expected run_hw_infer_estimator() = 0; + virtual hailo_status before_fork() { return HAILO_SUCCESS; } virtual hailo_status after_fork_in_parent() { return HAILO_SUCCESS; } virtual hailo_status after_fork_in_child() { return HAILO_SUCCESS; } + virtual Expected> get_sorted_output_names() = 0; + virtual Expected> get_stream_names_from_vstream_name(const std::string &vstream_name) = 0; + virtual Expected> get_vstream_names_from_stream_name(const std::string &stream_name) = 0; + + static Expected> duplicate_network_group_client(uint32_t handle, const std::string &network_group_name); + virtual Expected get_client_handle() const; protected: ConfiguredNetworkGroup() = default; diff --git a/hailort/libhailort/include/hailo/network_rate_calculator.hpp b/hailort/libhailort/include/hailo/network_rate_calculator.hpp index 047cb1c..6c964b2 100644 --- a/hailort/libhailort/include/hailo/network_rate_calculator.hpp +++ b/hailort/libhailort/include/hailo/network_rate_calculator.hpp @@ -21,6 +21,7 @@ #include +/** hailort namespace */ namespace hailort { @@ -77,6 +78,11 @@ public: Expected> get_udp_ports_rates_dict( std::vector> &udp_input_streams, uint32_t fps, uint32_t max_supported_bandwidth = HAILO_DEFAULT_MAX_ETHERNET_BANDWIDTH_BYTES_PER_SEC); + + // Undocumented, exported here for pyhailort usage + static hailo_status set_rate_limit(const std::string &ip, uint16_t port, uint32_t rate_bytes_per_sec); + static hailo_status reset_rate_limit(const std::string &ip, uint16_t port); + static Expected get_interface_name(const std::string &ip); }; } /* namespace hailort */ diff --git a/hailort/libhailort/include/hailo/quantization.hpp b/hailort/libhailort/include/hailo/quantization.hpp index d48b008..8d80c1c 100644 --- a/hailort/libhailort/include/hailo/quantization.hpp +++ b/hailort/libhailort/include/hailo/quantization.hpp @@ -16,10 +16,26 @@ #include #include +#ifdef _MSC_VER +#include +#endif +/** hailort namespace */ namespace hailort { +inline float bankers_round(float x) +{ +#ifdef _MSC_VER + // These instructions are intrinsics that the Microsoft C/C++ compiler supports when x86 is targeted + __m128 xmm = _mm_set_ss(x); + xmm = _mm_round_ss(xmm, xmm, _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC); + return _mm_cvtss_f32(xmm); +#else + return rintf(x); +#endif +} + class RoundingToNearestGuard final { public: @@ -70,7 +86,6 @@ public: dst_ptr[i] = (T)(src_ptr[i]); } } else { - auto rounding_tonearest_guard = RoundingToNearestGuard(); for (uint32_t i = 0; i < buffer_elements_count; i++) { dst_ptr[i] = dequantize_output(src_ptr[i], quant_info); } @@ -79,7 +94,7 @@ public: /** * De-quantize in place the output buffer pointed by @a dst_ptr from data type @a Q to data type @a T. - * + * * @param[inout] dst_ptr A pointer to the buffer to be de-quantized. * @param[in] buffer_elements_count The number of elements in @a dst_ptr array. * @param[in] quant_info Quantization info. @@ -87,14 +102,28 @@ public: template static void dequantize_output_buffer_in_place(T *dst_ptr, uint32_t buffer_elements_count, hailo_quant_info_t quant_info) { - if (is_identity_qp(quant_info)) { + dequantize_output_buffer_in_place(dst_ptr, 0, buffer_elements_count, quant_info.qp_zp, quant_info.qp_scale); + } + + /** + * De-quantize in place the output buffer pointed by @a dst_ptr starting from @a offset from data type @a Q to data type @a T. + * + * @param[inout] dst_ptr A pointer to the buffer to be de-quantized. + * @param[in] offset The offset in @a dst_ptr array to start from. + * @param[in] buffer_elements_count The number of elements in @a dst_ptr array. + * @param[in] qp_zp Quantization zero point. + * @param[in] qp_scale Quantization scale. + */ + template + static void dequantize_output_buffer_in_place(T *dst_ptr, uint32_t offset, uint32_t buffer_elements_count, float32_t qp_zp, float32_t qp_scale) + { + if (is_identity_qp(qp_zp, qp_scale)) { for (int32_t i = (int32_t)buffer_elements_count - 1; i >= 0; i--) { - dst_ptr[i] = (T)(*((Q*)dst_ptr + i)); + dst_ptr[offset + i] = (T)(*((Q*)dst_ptr + offset + i)); } } else { - auto rounding_tonearest_guard = RoundingToNearestGuard(); for (int32_t i = (int32_t)buffer_elements_count - 1; i >= 0; i--) { - dst_ptr[i] = dequantize_output(*((Q*)dst_ptr + i), quant_info); + dst_ptr[offset + i] = dequantize_output(*((Q*)dst_ptr + offset + i), qp_zp, qp_scale); } } } @@ -113,7 +142,7 @@ public: auto rounding_tonearest_guard = RoundingToNearestGuard(); if (is_identity_qp(quant_info)) { for (uint32_t i = 0; i < buffer_elements_count; i++) { - dst_ptr[i] = (Q)rintf(src_ptr[i]); + dst_ptr[i] = (Q)bankers_round(src_ptr[i]); } } else { for (uint32_t i = 0; i < buffer_elements_count; i++) { @@ -155,7 +184,16 @@ public: */ static inline bool is_identity_qp(const hailo_quant_info_t &quant_info) { - return ((1 == quant_info.qp_scale) && (0 == quant_info.qp_zp)); + return is_identity_qp(quant_info.qp_zp, quant_info.qp_scale); + } + + /** + * Indicates whether the @a qp_zp and @a qp_scale is the identity scale. + * If true there is no need to fix the data's scale. + */ + static inline bool is_identity_qp(float32_t qp_zp, float32_t qp_scale) + { + return ((1 == qp_scale) && (0 == qp_zp)); } /** @@ -170,7 +208,23 @@ public: template static inline T dequantize_output(Q number, hailo_quant_info_t quant_info) { - return (T)((number - quant_info.qp_zp) * quant_info.qp_scale); + return dequantize_output(number, quant_info.qp_zp, quant_info.qp_scale); + } + + /** + * De-quantize @a number from data type @a Q to data type @a T and fix it's scale according to @a qp_zp and @a qp_scale. + * + * @param[in] number The value to be de-quantized. + * @param[in] qp_zp Quantization zero point. + * @param[in] qp_scale Quantization scale. + * + * @return Returns the dequantized value of @a number. + * + */ + template + static inline T dequantize_output(Q number, float32_t qp_zp, float32_t qp_scale) + { + return (T)((number - qp_zp) * qp_scale); } static inline float32_t clip(float32_t n, float32_t limval_min, float32_t limval_max) @@ -191,7 +245,7 @@ private: static inline Q quantize_input(T number, hailo_quant_info_t quant_info) { float32_t clipped_number = clip((float32_t)number, quant_info.limvals_min, quant_info.limvals_max); - return (Q)rintf((clipped_number / quant_info.qp_scale) + quant_info.qp_zp); + return (Q)bankers_round((clipped_number / quant_info.qp_scale) + quant_info.qp_zp); } }; diff --git a/hailort/libhailort/include/hailo/runtime_statistics.hpp b/hailort/libhailort/include/hailo/runtime_statistics.hpp index 60a5b29..3de7422 100644 --- a/hailort/libhailort/include/hailo/runtime_statistics.hpp +++ b/hailort/libhailort/include/hailo/runtime_statistics.hpp @@ -16,7 +16,7 @@ #include #include - +/** hailort namespace */ namespace hailort { diff --git a/hailort/libhailort/include/hailo/stream.hpp b/hailort/libhailort/include/hailo/stream.hpp index 2c5ee19..d3252a0 100644 --- a/hailort/libhailort/include/hailo/stream.hpp +++ b/hailort/libhailort/include/hailo/stream.hpp @@ -20,16 +20,12 @@ #include +/** hailort namespace */ namespace hailort { // Forward declaration struct LayerInfo; -class DmaMappedBuffer; - -using TransferDoneCallback = std::function buffer, - const hailo_async_transfer_completion_info_t &status, - void *opaque)>; /*! Input (host to device) stream representation */ @@ -41,6 +37,24 @@ public: InputStream(const InputStream&) = delete; InputStream& operator=(const InputStream&) = delete; + /** Context passed to the \ref TransferDoneCallback after the async operation is done or has failed. */ + struct CompletionInfo + { + /** + * Status of the async transfer. + * - ::HAILO_SUCCESS - When transfer is complete successfully. + * - ::HAILO_STREAM_ABORTED_BY_USER - The transfer was canceled (can happen after network deactivation). + * - Any other ::hailo_status on unexpected errors. + */ + hailo_status status; + + const void *buffer_addr; /* Points to the transferred buffer. */ + size_t buffer_size; /* Size of the transferred buffer. */ + }; + + /** Async transfer complete callback prototype. */ + using TransferDoneCallback = std::function; + /** * Set new timeout value to the input stream * @@ -61,21 +75,21 @@ public: /** * Aborting the stream. - * + * * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns a ::hailo_status error. */ virtual hailo_status abort() = 0; /** * Clearing the aborted state of the stream. - * + * * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns a ::hailo_status error. */ virtual hailo_status clear_abort() = 0; /** * Writes all pending data to the underlying stream. - * + * * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns a ::hailo_status error. */ virtual hailo_status flush(); @@ -92,21 +106,117 @@ public: virtual bool is_scheduled() = 0; /** - * Writes the entire buffer to the stream without transformations + * Writes the entire buffer to the stream without transformations. * * @param[in] buffer The buffer to be written. * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns an ::hailo_status error. + * * @note @a buffer is expected to be in the format dictated by this.stream_info.format - * @note @a size is expected to be a product of this.stream_info.hw_frame_size (i.e. more than one frame may be written) + * @note @a buffer.size() is expected to be get_frame_size(). */ virtual hailo_status write(const MemoryView &buffer); - // ******************************************** NOTE ******************************************** // - // Async Stream API and DmaMappedBuffer are currently not supported and are for internal use only // - // ********************************************************************************************** // - virtual hailo_status wait_for_ready(size_t transfer_size, std::chrono::milliseconds timeout); // Internal use only - virtual hailo_status write_async(std::shared_ptr buffer, const TransferDoneCallback &user_callback, - void *opaque = nullptr); // Internal use only + /** + * Writes the entire buffer to the stream without transformations. + * + * @param[in] buffer The buffer to be written. + * @param[in] size The size of the buffer given. + * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns an ::hailo_status error. + * + * @note @a buffer is expected to be in the format dictated by this.stream_info.format + * @note @a size is expected to be get_frame_size(). + */ + virtual hailo_status write(const void *buffer, size_t size); + + /** + * Waits until the stream is ready to launch a new write_async() operation. Each stream contains some limited sized + * queue for ongoing transfers. Calling get_async_max_queue_size() will return the queue size for current stream. + * + * @param[in] transfer_size Must be get_frame_size(). + * @param[in] timeout Amount of time to wait until the stream is ready in milliseconds. + * + * @return Upon success, returns ::HAILO_SUCCESS. Otherwise: + * - If @a timeout has passed and the stream is not ready, returns ::HAILO_TIMEOUT. + * - In any other error case, returns ::hailo_status error. + */ + virtual hailo_status wait_for_async_ready(size_t transfer_size, std::chrono::milliseconds timeout); + + /** + * Returns the maximum amount of frames that can be simultaneously written to the stream (by write_async() calls) + * before any one of the write operations is complete, as signified by @a user_callback being called. + * + * @return Upon success, returns Expected of a the queue size. + * Otherwise, returns Unexpected of ::hailo_status error. + */ + virtual Expected get_async_max_queue_size() const; + + /** + * Writes the contents of @a buffer to the stream asynchronously, initiating a deferred operation that will be + * completed later. + * - If the function call succeeds (i.e., write_async() returns ::HAILO_SUCCESS), the deferred operation has been + * initiated. Until @a user_callback is called, the user cannot change or delete @a buffer. + * - If the function call fails (i.e., write_async() returns a status other than ::HAILO_SUCCESS), the deferred + * operation will not be initiated and @a user_callback will not be invoked. The user is free to change or delete + * @a buffer. + * - @a user_callback is triggered upon successful completion or failure of the deferred operation. The callback + * receives a \ref CompletionInfo object containing a pointer to the transferred buffer (@a buffer_addr) and the + * transfer status (@a status). + * + * @param[in] buffer The buffer to be written. + * The buffer must be aligned to the system page size. + * @param[in] user_callback The callback that will be called when the transfer is complete + * or has failed. + * + * @return Upon success, returns ::HAILO_SUCCESS. Otherwise: + * - If the stream queue is full, returns ::HAILO_QUEUE_IS_FULL. In this case please wait + * until @a user_callback is called on previous writes, or call wait_for_async_ready(). + * The size of the queue can be determined by calling get_async_max_queue_size(). + * - In any other error case, returns a ::hailo_status error. + * + * @note @a user_callback should run as quickly as possible. + * @note The buffer's format comes from the @a format field inside get_info() and the shape comes from + * the @a hw_shape field inside get_info(). + * @note The address provided must be aligned to the system's page size, and the rest of the page should not be in + * use by any other part of the program to ensure proper functioning of the DMA operation. Memory for the + * provided address can be allocated using `mmap` on Unix-like systems or `VirtualAlloc` on Windows. + */ + virtual hailo_status write_async(const MemoryView &buffer, const TransferDoneCallback &user_callback) = 0; + + /** + * Writes the contents of @a buffer to the stream asynchronously, initiating a deferred operation that will be + * completed later. + * - If the function call succeeds (i.e., write_async() returns ::HAILO_SUCCESS), the deferred operation has been + * initiated. Until @a user_callback is called, the user cannot change or delete @a buffer. + * - If the function call fails (i.e., write_async() returns a status other than ::HAILO_SUCCESS), the deferred + * operation will not be initiated and @a user_callback will not be invoked. The user is free to change or delete + * @a buffer. + * - @a user_callback is triggered upon successful completion or failure of the deferred operation. The callback + * receives a \ref CompletionInfo object containing a pointer to the transferred buffer (@a buffer_addr) and the + * transfer status (@a status). + * + * @param[in] buffer The buffer to be written. + * The buffer must be aligned to the system page size. + * @param[in] size The size of the given buffer, expected to be get_frame_size(). + * @param[in] user_callback The callback that will be called when the transfer is complete + * or has failed. + * + * @return Upon success, returns ::HAILO_SUCCESS. Otherwise: + * - If the stream queue is full, returns ::HAILO_QUEUE_IS_FULL. In this case please wait + * until @a user_callback is called on previous writes, or call wait_for_async_ready(). + * The size of the queue can be determined by calling get_async_max_queue_size(). + * - In any other error case, returns a ::hailo_status error. + * + * @note @a user_callback should run as quickly as possible. + * @note The buffer's format comes from the @a format field inside get_info() and the shape comes from + * the @a hw_shape field inside get_info(). + * @note The address provided must be aligned to the system's page size, and the rest of the page should not be in + * use by any other part of the program to ensure proper functioning of the DMA operation. Memory for the + * provided address can be allocated using `mmap` on Unix-like systems or `VirtualAlloc` on Windows. + */ + virtual hailo_status write_async(const void *buffer, size_t size, const TransferDoneCallback &user_callback) = 0; + + // The usage of BufferPtr for async API isn't currently supported and is for internal use only. + virtual hailo_status write_async(BufferPtr buffer, const TransferDoneCallback &user_callback) = 0; /** * @returns A ::hailo_stream_info_t object containing the stream's info. @@ -139,18 +249,17 @@ public: // get_network_group_activated_event is same as this function virtual EventPtr &get_core_op_activated_event() = 0; + protected: InputStream() = default; InputStream(InputStream &&) = delete; - // Note: Implement sync_write_all_raw_buffer_no_transform_impl for the actual stream interaction in sub classes - virtual hailo_status sync_write_all_raw_buffer_no_transform_impl(void *buffer, size_t offset, size_t size) = 0; + // Note: Implement write_impl for the actual stream interaction in sub classes + virtual hailo_status write_impl(const MemoryView &buffer) = 0; virtual hailo_status activate_stream(uint16_t dynamic_batch_size, bool resume_pending_stream_transfers) = 0; virtual hailo_status deactivate_stream() = 0; - virtual Expected sync_write_raw_buffer(const MemoryView &buffer) = 0; - hailo_stream_info_t m_stream_info; uint8_t m_dataflow_manager_id; @@ -169,6 +278,24 @@ public: OutputStream(const OutputStream&) = delete; OutputStream& operator=(const OutputStream&) = delete; + /** Context passed to the \ref TransferDoneCallback after the async operation is done or has failed. */ + struct CompletionInfo + { + /** + * Status of the async transfer. + * - ::HAILO_SUCCESS - When transfer is complete successfully. + * - ::HAILO_STREAM_ABORTED_BY_USER - The transfer was canceled (can happen after network deactivation). + * - Any other ::hailo_status on unexpected errors. + */ + hailo_status status; + + void *buffer_addr; /* Points to the transferred buffer. */ + size_t buffer_size; /* Size of the transferred buffer. */ + }; + + /** Async transfer complete callback prototype. */ + using TransferDoneCallback = std::function; + /** * Set new timeout value to the output stream * @@ -181,7 +308,7 @@ public: * @return returns the output stream's timeout in milliseconds. */ virtual std::chrono::milliseconds get_timeout() const = 0; - + /** * @return returns the output stream's interface. */ @@ -189,14 +316,14 @@ public: /** * Aborting the stream. - * + * * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns a ::hailo_status error. */ virtual hailo_status abort() = 0; /** * Clearing the abort flag of the stream. - * + * * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns a ::hailo_status error. */ virtual hailo_status clear_abort() = 0; @@ -249,19 +376,115 @@ public: /** * Reads the entire buffer from the stream without transformations * - * @param[out] buffer A pointer to a buffer that receives the data read from the stream. + * @param[in] buffer A buffer that receives the data read from the stream. * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns an ::hailo_status error. - * @note Upon return, @a buffer is expected to be in the format dictated by this.stream_info.format - * @note @a size is expected to be a product of this.stream_info.hw_frame_size (i.e. more than one frame may be read) + * @note Upon return, @a buffer is expected to be in the format dictated by this.get_info().format + * @note @a size is expected to be get_frame_size(). */ virtual hailo_status read(MemoryView buffer); - // ******************************************** NOTE ******************************************** // - // Async Stream API and DmaMappedBuffer are currently not supported and are for internal use only // - // ********************************************************************************************** // - virtual hailo_status wait_for_ready(size_t transfer_size, std::chrono::milliseconds timeout); // Internal use only - virtual hailo_status read_async(std::shared_ptr buffer, const TransferDoneCallback &user_callback, - void *opaque = nullptr); // Internal use only + /** + * Reads the entire buffer from the stream without transformations + * + * @param[in] buffer A pointer to a buffer that receives the data read from the stream. + * @param[in] size The size of the given buffer. + * + * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns an ::hailo_status error. + * + * @note Upon return, @a buffer is expected to be in the format dictated by this.get_info().format + * @note @a size is expected to be get_frame_size(). + */ + virtual hailo_status read(void *buffer, size_t size); + + /** + * Waits until the stream is ready to launch a new read_async() operation. Each stream contains some limited sized + * queue for ongoing transfers. Calling get_async_max_queue_size() will return the queue size for current stream. + * + * @param[in] transfer_size Must be get_frame_size(). + * @param[in] timeout Amount of time to wait until the stream is ready in milliseconds. + * + * @return Upon success, returns ::HAILO_SUCCESS. Otherwise: + * - If @a timeout has passed and the stream is not ready, returns ::HAILO_TIMEOUT. + * - In any other error case, returns ::hailo_status error. + */ + virtual hailo_status wait_for_async_ready(size_t transfer_size, std::chrono::milliseconds timeout); + + /** + * Returns the maximum amount of frames that can be simultaneously read from the stream (by read_async() calls) + * before any one of the read operations is complete, as signified by @a user_callback being called. + * + * @return Upon success, returns Expected of a the queue size. + * Otherwise, returns Unexpected of ::hailo_status error. + */ + virtual Expected get_async_max_queue_size() const; + + /** + * Reads into @a buffer from the stream asynchronously, initiating a deferred operation that will be completed + * later. + * - If the function call succeeds (i.e., read_async() returns ::HAILO_SUCCESS), the deferred operation has been + * initiated. Until @a user_callback is called, the user cannot change or delete @a buffer. + * - If the function call fails (i.e., read_async() returns a status other than ::HAILO_SUCCESS), the deferred + * operation will not be initiated and @a user_callback will not be invoked. The user is free to change or + * delete @a buffer. + * - @a user_callback is triggered upon successful completion or failure of the deferred operation. + * The callback receives a \ref CompletionInfo object containing a pointer to the transferred buffer + * (@a buffer_addr) and the transfer status (@a status). If the operation has completed successfully, the contents + * of @a buffer will have been updated by the read operation. + * + * @param[in] buffer The buffer to be read into. + * The buffer must be aligned to the system page size. + * @param[in] user_callback The callback that will be called when the transfer is complete or has failed. + * + * @return Upon success, returns ::HAILO_SUCCESS. Otherwise: + * - If the stream queue is full, returns ::HAILO_QUEUE_IS_FULL. + * In this case, please wait until @a user_callback is called on previous + * reads, or call wait_for_async_ready(). The size of the queue can be + * determined by calling get_async_max_queue_size(). + * - In any other error case, returns a ::hailo_status error. + * @note @a user_callback should execute as quickly as possible. + * @note The buffer's format is determined by the @a format field inside get_info(), + * and the shape is determined by the @a hw_shape field inside get_info(). + * @note The address provided must be aligned to the system's page size, and the rest of the page should not be in + * use by any other part of the program to ensure proper functioning of the DMA operation. Memory for the + * provided address can be allocated using `mmap` on Unix-like systems or `VirtualAlloc` on Windows. + */ + virtual hailo_status read_async(MemoryView buffer, const TransferDoneCallback &user_callback) = 0; + + /** + * Reads into @a buffer from the stream asynchronously, initiating a deferred operation that will be completed + * later. + * - If the function call succeeds (i.e., read_async() returns ::HAILO_SUCCESS), the deferred operation has been + * initiated. Until @a user_callback is called, the user cannot change or delete @a buffer. + * - If the function call fails (i.e., read_async() returns a status other than ::HAILO_SUCCESS), the deferred + * operation will not be initiated and @a user_callback will not be invoked. The user is free to change or + * delete @a buffer. + * - @a user_callback is triggered upon successful completion or failure of the deferred operation. + * The callback receives a \ref CompletionInfo object containing a pointer to the transferred buffer + * (@a buffer_addr) and the transfer status (@a status). If the operation has completed successfully, the contents + * of @a buffer will have been updated by the read operation. + * + * @param[in] buffer The buffer to be read into. + * The buffer must be aligned to the system page size. + * @param[in] size The size of the given buffer, expected to be get_frame_size(). + * @param[in] user_callback The callback that will be called when the transfer is complete or has failed. + * + * @return Upon success, returns ::HAILO_SUCCESS. Otherwise: + * - If the stream queue is full, returns ::HAILO_QUEUE_IS_FULL. + * In this case, please wait until @a user_callback is called on previous + * reads, or call wait_for_async_ready(). The size of the queue can be + * determined by calling get_async_max_queue_size(). + * - In any other error case, returns a ::hailo_status error. + * @note @a user_callback should execute as quickly as possible. + * @note The buffer's format is determined by the @a format field inside get_info(), + * and the shape is determined by the @a hw_shape field inside get_info() + * @note The address provided must be aligned to the system's page size, and the rest of the page should not be in + * use by any other part of the program to ensure proper functioning of the DMA operation. Memory for the + * provided address can be allocated using `mmap` on Unix-like systems or `VirtualAlloc` on Windows. + */ + virtual hailo_status read_async(void *buffer, size_t size, const TransferDoneCallback &user_callback) = 0; + + // The usage of BufferPtr for async API isn't currently supported and is for internal use only. + virtual hailo_status read_async(BufferPtr buffer, const TransferDoneCallback &user_callback) = 0; // get_network_group_activated_event is same as this function virtual EventPtr &get_core_op_activated_event() = 0; @@ -271,17 +494,17 @@ protected: virtual hailo_status activate_stream(uint16_t dynamic_batch_size, bool resume_pending_stream_transfers) = 0; virtual hailo_status deactivate_stream() = 0; - virtual hailo_status read_all(MemoryView &buffer) = 0; - - virtual Expected sync_read_raw_buffer(MemoryView &buffer) = 0; + virtual hailo_status read_impl(MemoryView &buffer) = 0; hailo_stream_info_t m_stream_info; uint8_t m_dataflow_manager_id; std::atomic m_invalid_frames_count; +protected: + hailo_status read_nms(void *buffer, size_t offset, size_t size); + private: virtual const LayerInfo& get_layer_info() = 0; - hailo_status read_nms(void *buffer, size_t offset, size_t size); void increase_invalid_frames_count(uint32_t value); friend class HefConfigurator; @@ -289,6 +512,7 @@ private: friend class HwReadElement; friend class OutputDemuxer; friend class CoreOp; + friend class NMSStreamReader; }; } /* namespace hailort */ diff --git a/hailort/libhailort/include/hailo/transform.hpp b/hailort/libhailort/include/hailo/transform.hpp index 883dbca..db1d7cf 100644 --- a/hailort/libhailort/include/hailo/transform.hpp +++ b/hailort/libhailort/include/hailo/transform.hpp @@ -20,7 +20,7 @@ #include #include - +/** hailort namespace */ namespace hailort { @@ -284,6 +284,7 @@ public: * @param[in] src A buffer to be demultiplexed. * @param[out] raw_buffers A vector of buffers that receives the demultiplexed data read from the stream. * The order of @a raw_buffers vector will remain as is. + * @note The order of @a raw_buffers should be the same as returned from the function 'get_edges_stream_info()'. * @return Upon success, returns ::HAILO_SUCCESS. Otherwise, returns a ::hailo_status error. */ virtual hailo_status transform_demux(const MemoryView src, std::vector &raw_buffers) = 0; diff --git a/hailort/libhailort/include/hailo/vdevice.hpp b/hailort/libhailort/include/hailo/vdevice.hpp index 9dd592a..0aa7bd3 100644 --- a/hailort/libhailort/include/hailo/vdevice.hpp +++ b/hailort/libhailort/include/hailo/vdevice.hpp @@ -17,6 +17,7 @@ #include "hailo/device.hpp" +/** hailort namespace */ namespace hailort { diff --git a/hailort/libhailort/include/hailo/vstream.hpp b/hailort/libhailort/include/hailo/vstream.hpp index faf2bf2..0cfe563 100644 --- a/hailort/libhailort/include/hailo/vstream.hpp +++ b/hailort/libhailort/include/hailo/vstream.hpp @@ -13,6 +13,7 @@ #include "hailo/network_group.hpp" #include "hailo/runtime_statistics.hpp" +/** hailort namespace */ namespace hailort { @@ -159,6 +160,10 @@ public: hailo_status before_fork(); hailo_status after_fork_in_parent(); hailo_status after_fork_in_child(); + bool is_aborted(); + + // Added to match the same API as InputStream. Will be filled when async API will be implemented for vstreams. + using TransferDoneCallback = void(*); protected: explicit InputVStream(std::shared_ptr vstream); @@ -171,6 +176,7 @@ protected: std::shared_ptr m_vstream; friend class VStreamsBuilderUtils; + friend class HailoRtRpcService; }; class HAILORTAPI OutputVStream @@ -304,6 +310,10 @@ public: hailo_status before_fork(); hailo_status after_fork_in_parent(); hailo_status after_fork_in_child(); + bool is_aborted(); + + // Added to match the same API as InputStream. Will be filled when async API will be implemented for vstreams. + using TransferDoneCallback = void(*); protected: explicit OutputVStream(std::shared_ptr vstream); @@ -317,6 +327,7 @@ protected: friend class VStreamsBuilderUtils; friend class VDeviceCoreOp; + friend class HailoRtRpcService; }; /*! Contains the virtual streams creation functions */ diff --git a/hailort/libhailort/src/CMakeLists.txt b/hailort/libhailort/src/CMakeLists.txt index 6dc846a..1c44fd0 100644 --- a/hailort/libhailort/src/CMakeLists.txt +++ b/hailort/libhailort/src/CMakeLists.txt @@ -34,9 +34,6 @@ add_subdirectory(network_group) add_subdirectory(core_op) add_subdirectory(net_flow) - -set(HAILORT_CPP_SOURCES "${HAILORT_CPP_SOURCES}" "${HAILORT_OPS_CPP_SOURCES}") - if(HAILO_BUILD_SERVICE) add_subdirectory(service) endif() @@ -57,10 +54,8 @@ relative_to_absolute_paths(HAILO_FULL_OS_DIR ${HAILO_FULL_OS_DIR}) set(HAILO_OS_DIR ${HAILO_OS_DIR} CACHE INTERNAL "Absolute path of os-dir") set(HAILO_FULL_OS_DIR ${HAILO_FULL_OS_DIR} CACHE INTERNAL "Absolute Full path of os-dir") set(HAILORT_CPP_SOURCES ${HAILORT_CPP_SOURCES} CACHE INTERNAL "Absolute paths of hailort's cpp source files") -set(HAILORT_CPP_OS_SOURCES ${HAILORT_CPP_OS_SOURCES} CACHE INTERNAL "Absolute paths of os-related source files") set(COMMON_C_SOURCES ${COMMON_C_SOURCES} CACHE INTERNAL "Absolute paths of common source files") -set(HAILORT_SRCS_ABS ${HAILORT_CPP_SOURCES} ${HAILORT_CPP_OS_SOURCES} ${HAILORT_COMMON_CPP_SOURCES} ${COMMON_C_SOURCES} CACHE INTERNAL "All absolute paths of hailort's source files") -set(HAILORT_OPS_CPP_SOURCES ${HAILORT_OPS_CPP_SOURCES} PARENT_SCOPE) +set(HAILORT_SRCS_ABS ${HAILORT_CPP_SOURCES} ${HAILORT_COMMON_CPP_SOURCES} ${COMMON_C_SOURCES} CACHE INTERNAL "All absolute paths of hailort's source files") SET_SOURCE_FILES_PROPERTIES(${C_SOURCES} PROPERTIES LANGUAGE CXX) add_library(libhailort SHARED ${HAILORT_SRCS_ABS}) @@ -102,6 +97,7 @@ set(HAILORT_PUBLIC_HEADERS ${HAILORT_INC_DIR}/hailo/platform.h ${HAILORT_INC_DIR}/hailo/hailort.hpp + ${HAILORT_INC_DIR}/hailo/buffer_storage.hpp ${HAILORT_INC_DIR}/hailo/buffer.hpp ${HAILORT_INC_DIR}/hailo/device.hpp ${HAILORT_INC_DIR}/hailo/event.hpp @@ -117,7 +113,6 @@ set(HAILORT_PUBLIC_HEADERS ${HAILORT_INC_DIR}/hailo/network_rate_calculator.hpp ${HAILORT_INC_DIR}/hailo/vdevice.hpp ${HAILORT_INC_DIR}/hailo/quantization.hpp - ${HAILORT_INC_DIR}/hailo/dma_mapped_buffer.hpp ${HAILORT_INC_DIR}/hailo/hailort_defaults.hpp ) diff --git a/hailort/libhailort/src/core_op/CMakeLists.txt b/hailort/libhailort/src/core_op/CMakeLists.txt index d70f0ee..a8d7a54 100644 --- a/hailort/libhailort/src/core_op/CMakeLists.txt +++ b/hailort/libhailort/src/core_op/CMakeLists.txt @@ -2,14 +2,13 @@ cmake_minimum_required(VERSION 3.0.0) set(SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/core_op.cpp - + ${CMAKE_CURRENT_SOURCE_DIR}/resource_manager/resource_manager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/resource_manager/resource_manager_builder.cpp ${CMAKE_CURRENT_SOURCE_DIR}/resource_manager/config_buffer.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/resource_manager/inter_context_buffer.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/resource_manager/ddr_channels_pair.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/resource_manager/intermediate_buffer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/resource_manager/channel_allocator.cpp ${CMAKE_CURRENT_SOURCE_DIR}/resource_manager/context_switch_buffer_builder.cpp ) -set(HAILORT_CPP_SOURCES ${HAILORT_CPP_SOURCES} ${SRC_FILES} ${HAILORT_OPS_CPP_SOURCES} PARENT_SCOPE) +set(HAILORT_CPP_SOURCES ${HAILORT_CPP_SOURCES} ${SRC_FILES} PARENT_SCOPE) diff --git a/hailort/libhailort/src/core_op/core_op.cpp b/hailort/libhailort/src/core_op/core_op.cpp index 0fb260e..0967e78 100644 --- a/hailort/libhailort/src/core_op/core_op.cpp +++ b/hailort/libhailort/src/core_op/core_op.cpp @@ -150,85 +150,6 @@ Expected CoreOp::get_latency_measurement(const std::st return result; } -Expected CoreOp::get_output_streams_from_vstream_names( - const std::map &outputs_params) -{ - OutputStreamWithParamsVector results; - std::unordered_map outputs_edges_params; - for (auto &name_params_pair : outputs_params) { - auto stream_names = m_metadata->get_stream_names_from_vstream_name(name_params_pair.first); - CHECK_EXPECTED(stream_names); - - for (auto &stream_name : stream_names.value()) { - CHECK_AS_EXPECTED(contains(m_output_streams, stream_name), HAILO_NOT_FOUND); - auto output_stream = m_output_streams.at(stream_name); - if (output_stream->get_info().is_mux) { - outputs_edges_params.emplace(name_params_pair); - } - else { - NameToVStreamParamsMap name_to_params = {name_params_pair}; - results.emplace_back(output_stream, name_to_params); - } - } - } - // Add non mux streams to result - hailo_status status = add_mux_streams_by_edges_names(results, outputs_edges_params); - CHECK_SUCCESS_AS_EXPECTED(status); - - return results; -} - -// This function adds to results the OutputStreams that correspond to the edges in outputs_edges_params. -// If an edge name appears in outputs_edges_params then all of its predecessors must appear in outputs_edges_params as well, Otherwise, an error is returned. -// We use the set seen_edges in order to mark the edges already evaluated by one of its' predecessor. -hailo_status CoreOp::add_mux_streams_by_edges_names(OutputStreamWithParamsVector &results, - const std::unordered_map &outputs_edges_params) -{ - std::unordered_set seen_edges; - for (auto &name_params_pair : outputs_edges_params) { - if (seen_edges.end() != seen_edges.find(name_params_pair.first)) { - // Edge has already been seen by one of its predecessors - continue; - } - auto output_streams = get_output_streams_by_vstream_name(name_params_pair.first); - CHECK_EXPECTED_AS_STATUS(output_streams); - CHECK(output_streams->size() == 1, HAILO_INVALID_ARGUMENT, - "mux streams cannot be separated into multiple streams"); - auto output_stream = output_streams.release()[0]; - - // TODO: Find a better way to get the mux edges without creating OutputDemuxer - auto expected_demuxer = OutputDemuxer::create(*output_stream); - CHECK_EXPECTED_AS_STATUS(expected_demuxer); - - NameToVStreamParamsMap name_to_params; - for (auto &edge : expected_demuxer.value()->get_edges_stream_info()) { - auto edge_name_params_pair = outputs_edges_params.find(edge.name); - CHECK(edge_name_params_pair != outputs_edges_params.end(), HAILO_INVALID_ARGUMENT, - "All edges of stream {} must be in output vstream params. edge {} is missing.", - name_params_pair.first, edge.name); - seen_edges.insert(edge.name); - name_to_params.insert(*edge_name_params_pair); - } - results.emplace_back(output_stream, name_to_params); - } - return HAILO_SUCCESS; -} - -Expected CoreOp::get_output_streams_by_vstream_name(const std::string &name) -{ - auto stream_names = m_metadata->get_stream_names_from_vstream_name(name); - CHECK_EXPECTED(stream_names); - - OutputStreamPtrVector output_streams; - output_streams.reserve(stream_names->size()); - for (const auto &stream_name : stream_names.value()) { - CHECK_AS_EXPECTED(contains(m_output_streams, stream_name), HAILO_NOT_FOUND); - output_streams.emplace_back(m_output_streams.at(stream_name)); - } - - return output_streams; -} - Expected CoreOp::get_layer_info(const std::string &stream_name) { for (auto layer_info : m_metadata->get_all_layer_infos()) { @@ -311,11 +232,6 @@ hailo_status CoreOp::deactivate_low_level_streams() return status; } -Expected> CoreOp::get_vstream_names_from_stream_name(const std::string &stream_name) -{ - return m_metadata->get_vstream_names_from_stream_name(stream_name); -} - const SupportedFeatures &CoreOp::get_supported_features() { return m_metadata->supported_features(); @@ -587,99 +503,12 @@ hailo_status CoreOp::wait_for_activation(const std::chrono::milliseconds &timeou return m_core_op_activated_event->wait(timeout); } -Expected>> CoreOp::get_output_vstream_groups() -{ - std::vector> results; - - for (auto output_stream : get_output_streams()) { - auto vstreams_group = get_vstream_names_from_stream_name(output_stream.get().name()); - CHECK_EXPECTED(vstreams_group); - results.push_back(vstreams_group.release()); - } - - return results; -} - -Expected>> CoreOp::make_output_vstream_params_groups( - bool quantized, hailo_format_type_t format_type, uint32_t timeout_ms, uint32_t queue_size) -{ - auto params = make_output_vstream_params(quantized, format_type, timeout_ms, queue_size); - CHECK_EXPECTED(params); - - auto groups = get_output_vstream_groups(); - CHECK_EXPECTED(groups); - - std::vector> results(groups->size(), std::map()); - - size_t pipeline_group_index = 0; - for (const auto &group : groups.release()) { - for (const auto &name_pair : params.value()) { - if (contains(group, name_pair.first)) { - results[pipeline_group_index].insert(name_pair); - } - } - pipeline_group_index++; - } - - return results; -} - -Expected> CoreOp::make_input_vstream_params( - bool quantized, hailo_format_type_t format_type, uint32_t timeout_ms, uint32_t queue_size, - const std::string &network_name) -{ - auto input_vstream_infos = m_metadata->get_input_vstream_infos(network_name); - CHECK_EXPECTED(input_vstream_infos); - - std::map res; - auto status = Hef::Impl::fill_missing_vstream_params_with_default(res, input_vstream_infos.value(), quantized, - format_type, timeout_ms, queue_size); - CHECK_SUCCESS_AS_EXPECTED(status); - return res; -} - -Expected> CoreOp::make_output_vstream_params( - bool quantized, hailo_format_type_t format_type, uint32_t timeout_ms, uint32_t queue_size, - const std::string &network_name) -{ - auto output_vstream_infos = m_metadata->get_output_vstream_infos(network_name); - CHECK_EXPECTED(output_vstream_infos); - std::map res; - auto status = Hef::Impl::fill_missing_vstream_params_with_default(res, output_vstream_infos.value(), quantized, - format_type, timeout_ms, queue_size); - CHECK_SUCCESS_AS_EXPECTED(status); - return res; -} - -Expected> CoreOp::get_network_infos() const -{ - return m_metadata->get_network_infos(); -} - Expected> CoreOp::get_all_stream_infos( const std::string &network_name) const { return m_metadata->get_all_stream_infos(network_name); } -Expected> CoreOp::get_input_vstream_infos( - const std::string &network_name) const -{ - return m_metadata->get_input_vstream_infos(network_name); -} - -Expected> CoreOp::get_output_vstream_infos( - const std::string &network_name) const -{ - return m_metadata->get_output_vstream_infos(network_name); -} - -Expected> CoreOp::get_all_vstream_infos( - const std::string &network_name) const -{ - return m_metadata->get_all_vstream_infos(network_name); -} - AccumulatorPtr CoreOp::get_activation_time_accumulator() const { return m_activation_time_accumulator; diff --git a/hailort/libhailort/src/core_op/core_op.hpp b/hailort/libhailort/src/core_op/core_op.hpp index f5ba474..3c00c19 100644 --- a/hailort/libhailort/src/core_op/core_op.hpp +++ b/hailort/libhailort/src/core_op/core_op.hpp @@ -112,43 +112,21 @@ public: virtual std::vector> get_output_streams_by_interface(hailo_stream_interface_t stream_interface); virtual ExpectedRef get_input_stream_by_name(const std::string& name); virtual ExpectedRef get_output_stream_by_name(const std::string& name); - virtual Expected get_output_streams_from_vstream_names( - const std::map &outputs_params); virtual Expected get_latency_measurement(const std::string &network_name=""); - // TODO: HRT-9546 - Remove func, should be only in CNG - virtual Expected> make_input_vstream_params( - bool quantized, hailo_format_type_t format_type, uint32_t timeout_ms, uint32_t queue_size, - const std::string &network_name=""); - // TODO: HRT-9546 - Remove func, should be only in CNG - virtual Expected> make_output_vstream_params( - bool quantized, hailo_format_type_t format_type, uint32_t timeout_ms, uint32_t queue_size, - const std::string &network_name=""); - // TODO: HRT-9546 - Remove func, should be only in CNG - virtual Expected>> make_output_vstream_params_groups( - bool quantized, hailo_format_type_t format_type, uint32_t timeout_ms, uint32_t queue_size); - - // TODO: HRT-9546 - Remove func, should be only in CNG - virtual Expected>> get_output_vstream_groups(); - - // TODO: HRT-9546 - Remove func, should be only in CNG - Expected> get_vstream_names_from_stream_name(const std::string &stream_name); virtual hailo_status activate_impl(uint16_t dynamic_batch_size, bool resume_pending_stream_transfers = false) = 0; virtual hailo_status deactivate_impl(bool keep_nn_config_during_reset = false) = 0; - virtual Expected> get_network_infos() const; virtual Expected> get_all_stream_infos(const std::string &network_name="") const; - virtual Expected> get_input_vstream_infos(const std::string &network_name="") const; - virtual Expected> get_output_vstream_infos(const std::string &network_name="") const; - virtual Expected> get_all_vstream_infos(const std::string &network_name="") const; + virtual AccumulatorPtr get_activation_time_accumulator() const; virtual AccumulatorPtr get_deactivation_time_accumulator() const; hailo_status create_streams_from_config_params(Device &device); virtual bool is_multi_context() const; virtual const ConfigureNetworkParams get_config_params() const; - + virtual Expected run_hw_infer_estimator() = 0; const SupportedFeatures &get_supported_features(); Expected get_stream_batch_size(const std::string &stream_name); @@ -173,9 +151,6 @@ protected: const hailo_stream_parameters_t &stream_params, const std::string &stream_name); hailo_status create_input_stream_from_config_params(Device &device, const hailo_stream_parameters_t &stream_params, const std::string &stream_name); - hailo_status add_mux_streams_by_edges_names(OutputStreamWithParamsVector &result, - const std::unordered_map &outputs_edges_params); - Expected get_output_streams_by_vstream_name(const std::string &name); hailo_status activate_low_level_streams(uint16_t dynamic_batch_size, bool resume_pending_stream_transfers); hailo_status deactivate_low_level_streams(); diff --git a/hailort/libhailort/src/core_op/resource_manager/config_buffer.cpp b/hailort/libhailort/src/core_op/resource_manager/config_buffer.cpp index d15f487..6256911 100644 --- a/hailort/libhailort/src/core_op/resource_manager/config_buffer.cpp +++ b/hailort/libhailort/src/core_op/resource_manager/config_buffer.cpp @@ -11,20 +11,33 @@ #include "core_op/resource_manager/config_buffer.hpp" #include "vdma/memory/sg_buffer.hpp" #include "vdma/memory/continuous_buffer.hpp" +#include "vdma/memory/buffer_requirements.hpp" #include namespace hailort { -Expected ConfigBuffer::create(HailoRTDriver &driver, vdma::ChannelId channel_id, - const std::vector &cfg_sizes) +Expected> ConfigBuffer::create_buffer(HailoRTDriver &driver, vdma::ChannelId channel_id, + const std::vector &cfg_sizes, const uint32_t buffer_size) { - const auto buffer_size = std::accumulate(cfg_sizes.begin(), cfg_sizes.end(), 0); - auto buffer_ptr = should_use_ccb(driver) ? create_ccb_buffer(driver, buffer_size) : create_sg_buffer(driver, channel_id, cfg_sizes); + if (should_use_ccb(driver) && (HAILO_OUT_OF_HOST_CMA_MEMORY == buffer_ptr.status())) { + /* Try to use sg buffer instead */ + return create_sg_buffer(driver, channel_id, cfg_sizes); + } else { + return buffer_ptr; + } +} + +Expected ConfigBuffer::create(HailoRTDriver &driver, vdma::ChannelId channel_id, + const std::vector &cfg_sizes) +{ + const auto buffer_size = std::accumulate(cfg_sizes.begin(), cfg_sizes.end(), 0); + CHECK_AS_EXPECTED(IS_FIT_IN_UINT32(buffer_size), HAILO_INTERNAL_FAILURE, "config buffer size exceeded UINT32 range limit"); + auto buffer_ptr = create_buffer(driver, channel_id, cfg_sizes, static_cast(buffer_size)); CHECK_EXPECTED(buffer_ptr); return ConfigBuffer(buffer_ptr.release(), channel_id, buffer_size); @@ -42,7 +55,7 @@ Expected ConfigBuffer::program_descriptors() { // TODO HRT-9657: remove DEVICE interrupts auto descriptors_count = - m_buffer->program_descriptors(m_acc_buffer_offset, vdma::InterruptsDomain::DEVICE, m_acc_desc_count, false); + m_buffer->program_descriptors(m_acc_buffer_offset, vdma::InterruptsDomain::DEVICE, m_acc_desc_count); CHECK_EXPECTED(descriptors_count); m_acc_desc_count += descriptors_count.value(); @@ -125,19 +138,19 @@ hailo_status ConfigBuffer::write_inner(const MemoryView &data) Expected> ConfigBuffer::create_sg_buffer(HailoRTDriver &driver, vdma::ChannelId channel_id, const std::vector &cfg_sizes) { - auto desc_sizes_pair = vdma::DescriptorList::get_desc_buffer_sizes_for_multiple_transfers(driver, 1, cfg_sizes); - CHECK_EXPECTED(desc_sizes_pair); - const auto page_size = desc_sizes_pair->first; - const auto descs_count = desc_sizes_pair->second; - - size_t buffer_size = 0; - for (const auto cfg_size : cfg_sizes) { - const auto descs_count_for_cfg = DIV_ROUND_UP(cfg_size, page_size); - buffer_size += descs_count_for_cfg * page_size; - } - - auto buffer = vdma::SgBuffer::create(driver, buffer_size, descs_count, page_size, HailoRTDriver::DmaDirection::H2D, - channel_id); + static const bool NOT_CIRCULAR = false; + // For config channels (In Hailo15), the page size must be a multiplication of host default page size. + // Therefore we use the flag force_default_page_size for those types of buffers. + auto const FORCE_DEFAULT_PAGE_SIZE = true; + auto buffer_size_requirements = vdma::BufferSizesRequirements::get_sg_buffer_requirements_multiple_transfers( + driver.desc_max_page_size(), 1, cfg_sizes, NOT_CIRCULAR, FORCE_DEFAULT_PAGE_SIZE); + CHECK_EXPECTED(buffer_size_requirements); + const auto page_size = buffer_size_requirements->desc_page_size(); + const auto descs_count = buffer_size_requirements->descs_count(); + const auto buffer_size = buffer_size_requirements->buffer_size(); + + auto buffer = vdma::SgBuffer::create(driver, buffer_size, descs_count, page_size, NOT_CIRCULAR, + HailoRTDriver::DmaDirection::H2D, channel_id); CHECK_EXPECTED(buffer); auto buffer_ptr = make_unique_nothrow(buffer.release()); @@ -149,9 +162,20 @@ Expected> ConfigBuffer::create_sg_buffer(Hailo Expected> ConfigBuffer::create_ccb_buffer(HailoRTDriver &driver, uint32_t buffer_size) { - buffer_size = vdma::ContinuousBuffer::get_buffer_size(buffer_size); - auto buffer = vdma::ContinuousBuffer::create(buffer_size, driver); - CHECK_EXPECTED(buffer); + static const bool NOT_CIRCULAR = false; + static const uint16_t SINGLE_TRANSFER = 1; + auto buffer_size_requirements = vdma::BufferSizesRequirements::get_ccb_buffer_requirements_single_transfer( + SINGLE_TRANSFER, buffer_size, NOT_CIRCULAR); + CHECK_EXPECTED(buffer_size_requirements); + + auto buffer = vdma::ContinuousBuffer::create(buffer_size_requirements->buffer_size(), driver); + /* Don't print error here since this might be expected error that the libhailoRT can recover from + (out of host memory). If it's not the case, there is a print in hailort_driver.cpp file */ + if (HAILO_OUT_OF_HOST_CMA_MEMORY == buffer.status()) { + return make_unexpected(buffer.status()); + } else { + CHECK_EXPECTED(buffer); + } auto buffer_ptr = make_unique_nothrow(buffer.release()); CHECK_NOT_NULL_AS_EXPECTED(buffer_ptr, HAILO_OUT_OF_HOST_MEMORY); @@ -165,7 +189,7 @@ bool ConfigBuffer::should_use_ccb(HailoRTDriver &driver) case HailoRTDriver::DmaType::PCIE: return false; case HailoRTDriver::DmaType::DRAM: - if (std::getenv("HAILO_FORCE_CONF_CHANNEL_OVER_DESC") != nullptr) { + if (nullptr != std::getenv("HAILO_FORCE_CONF_CHANNEL_OVER_DESC")) { LOGGER__WARNING("Using desc instead of CCB for config channel is not optimal for performance.\n"); return false; } diff --git a/hailort/libhailort/src/core_op/resource_manager/config_buffer.hpp b/hailort/libhailort/src/core_op/resource_manager/config_buffer.hpp index b27611e..534bab5 100644 --- a/hailort/libhailort/src/core_op/resource_manager/config_buffer.hpp +++ b/hailort/libhailort/src/core_op/resource_manager/config_buffer.hpp @@ -22,7 +22,6 @@ namespace hailort { #define CCW_DATA_OFFSET (CCW_BYTES_IN_WORD * 2) #define CCW_HEADER_SIZE (CCW_DATA_OFFSET) - class ConfigBuffer final { public: @@ -57,6 +56,8 @@ private: vdma::ChannelId channel_id, const std::vector &cfg_sizes); static Expected> create_ccb_buffer(HailoRTDriver &driver, uint32_t buffer_size); + static Expected> create_buffer(HailoRTDriver &driver, vdma::ChannelId channel_id, + const std::vector &cfg_sizes, const uint32_t buffer_size); static bool should_use_ccb(HailoRTDriver &driver); diff --git a/hailort/libhailort/src/core_op/resource_manager/context_switch_buffer_builder.cpp b/hailort/libhailort/src/core_op/resource_manager/context_switch_buffer_builder.cpp index e7c60b3..5684abc 100644 --- a/hailort/libhailort/src/core_op/resource_manager/context_switch_buffer_builder.cpp +++ b/hailort/libhailort/src/core_op/resource_manager/context_switch_buffer_builder.cpp @@ -39,6 +39,11 @@ const std::vector &get_controls() const; + const CONTROL_PROTOCOL__context_switch_context_type_t &get_context_type() const; private: CONTROL_PROTOCOL__context_switch_context_info_single_control_t ¤t_control(); diff --git a/hailort/libhailort/src/core_op/resource_manager/ddr_channels_pair.cpp b/hailort/libhailort/src/core_op/resource_manager/ddr_channels_pair.cpp deleted file mode 100644 index b170300..0000000 --- a/hailort/libhailort/src/core_op/resource_manager/ddr_channels_pair.cpp +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Copyright (c) 2022 Hailo Technologies Ltd. All rights reserved. - * Distributed under the MIT license (https://opensource.org/licenses/MIT) -**/ -/** - * @file ddr_channels_pair.cpp - **/ - -#include "common/utils.hpp" - -#include "core_op/resource_manager/ddr_channels_pair.hpp" -#include "vdma/memory/continuous_buffer.hpp" -#include "vdma/memory/sg_buffer.hpp" - - -namespace hailort -{ - - -Expected DdrChannelsPair::create(HailoRTDriver &driver, const DdrChannelsInfo &ddr_channels_info) -{ - auto buffer_exp = should_use_ccb(driver) ? - create_ccb_buffer(driver, ddr_channels_info.row_size, ddr_channels_info.min_buffered_rows) : - create_sg_buffer(driver, ddr_channels_info.row_size, ddr_channels_info.min_buffered_rows, ddr_channels_info.d2h_channel_id); - CHECK_EXPECTED(buffer_exp); - auto buffer_ptr = buffer_exp.release(); - - CHECK_AS_EXPECTED(0 == (ddr_channels_info.row_size % buffer_ptr->desc_page_size()), HAILO_INTERNAL_FAILURE, - "DDR channel buffer row size must be a multiple of descriptor page size"); - - const auto interrupts_domain = vdma::InterruptsDomain::NONE; - const auto total_size = buffer_ptr->descs_count() * buffer_ptr->desc_page_size(); - auto desc_count_local = buffer_ptr->program_descriptors(total_size, interrupts_domain, 0, true); - CHECK_EXPECTED(desc_count_local); - - return DdrChannelsPair(std::move(buffer_ptr), ddr_channels_info); -} - -uint16_t DdrChannelsPair::descs_count() const -{ - assert(IS_FIT_IN_UINT16(m_buffer->descs_count())); - return static_cast(m_buffer->descs_count()); -} - -uint32_t DdrChannelsPair::descriptors_per_frame() const -{ - return (m_info.row_size / m_buffer->desc_page_size()) * m_info.total_buffers_per_frame; -} - -Expected DdrChannelsPair::read() const -{ - const auto size = m_buffer->size(); - auto res = Buffer::create(size); - CHECK_EXPECTED(res); - - auto status = m_buffer->read(res->data(), size, 0); - CHECK_SUCCESS_AS_EXPECTED(status); - - return res.release(); -} - -const DdrChannelsInfo& DdrChannelsPair::info() const -{ - return m_info; -} - - -bool DdrChannelsPair::need_manual_credit_management() const -{ - // On scatter gather manual credit management is needed - return m_buffer->type() == vdma::VdmaBuffer::Type::SCATTER_GATHER; -} - -CONTROL_PROTOCOL__host_buffer_info_t DdrChannelsPair::get_host_buffer_info() const -{ - return m_buffer->get_host_buffer_info(m_info.row_size); -} - -Expected> DdrChannelsPair::create_sg_buffer(HailoRTDriver &driver, - uint32_t row_size, uint16_t buffered_rows, vdma::ChannelId d2h_channel_id) -{ - auto desc_sizes_pair = vdma::DescriptorList::get_desc_buffer_sizes_for_single_transfer(driver, - buffered_rows, buffered_rows, row_size); - CHECK_EXPECTED(desc_sizes_pair); - const auto desc_page_size = desc_sizes_pair->first; - const auto descs_count = desc_sizes_pair->second; - // DdrChannels are circular so we need to allocate the full descriptors list. - const auto buffer_size = desc_page_size * descs_count; - - auto buffer = vdma::SgBuffer::create(driver, buffer_size, descs_count, desc_page_size, - HailoRTDriver::DmaDirection::BOTH, d2h_channel_id); - CHECK_EXPECTED(buffer); - - auto buffer_ptr = make_unique_nothrow(buffer.release()); - CHECK_NOT_NULL_AS_EXPECTED(buffer_ptr, HAILO_OUT_OF_HOST_MEMORY); - - return std::unique_ptr(std::move(buffer_ptr)); -} - -DdrChannelsPair::DdrChannelsPair(std::unique_ptr &&buffer, const DdrChannelsInfo &ddr_channels_info) : - m_buffer(std::move(buffer)), - m_info(ddr_channels_info) -{} - -Expected> DdrChannelsPair::create_ccb_buffer(HailoRTDriver &driver, - uint32_t row_size, uint16_t buffered_rows) -{ - // The first 12 channels in D2H CCB ("regular channels") requires that the amount of descriptors will be a power - // of 2. Altough the 4 last channels ("enhanced channels") don't have this requirements, we keep the code the same. - auto buffer_size = vdma::ContinuousBuffer::get_buffer_size_desc_power2(row_size * buffered_rows); - auto buffer = vdma::ContinuousBuffer::create(buffer_size, driver); - CHECK_EXPECTED(buffer); - - auto buffer_ptr = make_unique_nothrow(buffer.release()); - CHECK_NOT_NULL_AS_EXPECTED(buffer_ptr, HAILO_OUT_OF_HOST_MEMORY); - - return std::unique_ptr(std::move(buffer_ptr)); -} - -bool DdrChannelsPair::should_use_ccb(HailoRTDriver &driver) -{ - switch (driver.dma_type()) { - case HailoRTDriver::DmaType::PCIE: - return false; - case HailoRTDriver::DmaType::DRAM: - if (std::getenv("HAILO_FORCE_DDR_CHANNEL_OVER_DESC") != nullptr) { - LOGGER__WARNING("Using desc instead of CCB for ddr channel is not optimal for performance.\n"); - return false; - } - else { - return true; - } - } - - // Shouldn't reach here - assert(false); - return false; -} - -} /* namespace hailort */ diff --git a/hailort/libhailort/src/core_op/resource_manager/ddr_channels_pair.hpp b/hailort/libhailort/src/core_op/resource_manager/ddr_channels_pair.hpp deleted file mode 100644 index 4caadc3..0000000 --- a/hailort/libhailort/src/core_op/resource_manager/ddr_channels_pair.hpp +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright (c) 2022 Hailo Technologies Ltd. All rights reserved. - * Distributed under the MIT license (https://opensource.org/licenses/MIT) -**/ -/** - * @file ddr_channels_pair.hpp - * @brief DDR channel pairs are pair of vdma channels used in the same context for skip-connection. - **/ - -#ifndef _HAILO_DDR_CHANNELS_PAIR_HPP_ -#define _HAILO_DDR_CHANNELS_PAIR_HPP_ - -#include "hailo/hailort.h" -#include "hailo/buffer.hpp" - -#include "vdma/memory/vdma_buffer.hpp" - - -namespace hailort -{ - -struct DdrChannelsInfo -{ - vdma::ChannelId d2h_channel_id; - uint8_t d2h_stream_index; - vdma::ChannelId h2d_channel_id; - uint8_t h2d_stream_index; - uint8_t network_index; - uint16_t row_size; - uint16_t min_buffered_rows; - // total_buffers_per_frame not same as core_buffer_per frame. - //(In DDR core buffer per frame is 1). Used to calc total host descriptors_per_frame. - uint16_t total_buffers_per_frame; -}; - -class DdrChannelsPair final -{ -public: - static Expected create(HailoRTDriver &driver, const DdrChannelsInfo &ddr_channels_info); - - uint16_t descs_count() const; - uint32_t descriptors_per_frame() const; - Expected read() const; - const DdrChannelsInfo & info() const; - - // Checks if the credits are automaticaly going from d2h channel to its h2d channel, or it needs to be done manually - // (Using a fw task). - bool need_manual_credit_management() const; - - CONTROL_PROTOCOL__host_buffer_info_t get_host_buffer_info() const; - -private: - DdrChannelsPair(std::unique_ptr &&buffer, const DdrChannelsInfo &ddr_channels_info); - - static Expected> create_sg_buffer(HailoRTDriver &driver, - uint32_t row_size, uint16_t buffered_rows, vdma::ChannelId d2h_channel_id); - static Expected> create_ccb_buffer(HailoRTDriver &driver, - uint32_t row_size, uint16_t buffered_rows); - - static bool should_use_ccb(HailoRTDriver &driver); - - std::unique_ptr m_buffer; - DdrChannelsInfo m_info; -}; - -} /* namespace hailort */ - -#endif /* _HAILO_DDR_CHANNELS_PAIR_HPP_ */ diff --git a/hailort/libhailort/src/core_op/resource_manager/inter_context_buffer.cpp b/hailort/libhailort/src/core_op/resource_manager/inter_context_buffer.cpp deleted file mode 100644 index 5bac263..0000000 --- a/hailort/libhailort/src/core_op/resource_manager/inter_context_buffer.cpp +++ /dev/null @@ -1,166 +0,0 @@ -/** - * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. - * Distributed under the MIT license (https://opensource.org/licenses/MIT) - **/ -/** - * @file inter_context_buffer.cpp - * @brief Manages inter-context buffer. - */ - -#include "core_op/resource_manager/resource_manager.hpp" -#include "core_op/resource_manager/inter_context_buffer.hpp" -#include "vdma/memory/sg_buffer.hpp" -#include "vdma/memory/continuous_buffer.hpp" - - -namespace hailort -{ - -Expected InterContextBuffer::create(HailoRTDriver &driver, uint32_t transfer_size, - uint16_t max_batch_size, vdma::ChannelId d2h_channel_id) -{ - auto buffer_exp = should_use_ccb(driver) ? - create_ccb_buffer(driver, transfer_size, max_batch_size) : - create_sg_buffer(driver, transfer_size, max_batch_size, d2h_channel_id); - CHECK_EXPECTED(buffer_exp); - auto buffer_ptr = buffer_exp.release(); - - size_t acc_offset = 0; - for (uint16_t i = 0; i < max_batch_size; i++) { - const auto last_desc_interrupts_domain = ((max_batch_size - 1) == i) ? - vdma::InterruptsDomain::DEVICE : vdma::InterruptsDomain::NONE; - static const auto BUFFER_NOT_CIRCULAR = false; - auto desc_count_local = buffer_ptr->program_descriptors(transfer_size, last_desc_interrupts_domain, acc_offset, - BUFFER_NOT_CIRCULAR); - CHECK_EXPECTED(desc_count_local, "Failed to program descs for inter context channels. Given max_batch_size is too big."); - acc_offset += desc_count_local.value(); - } - - return InterContextBuffer(std::move(buffer_ptr), transfer_size, max_batch_size); -} - -hailo_status InterContextBuffer::reprogram(uint16_t batch_size) -{ - const auto prev_batch_size = m_dynamic_batch_size; - auto status = set_dynamic_batch_size(batch_size); - CHECK_SUCCESS(status); - - if (prev_batch_size == m_dynamic_batch_size) { - LOGGER__TRACE("Batch size hasn't changed ({}); nothing to be done.", batch_size); - return HAILO_SUCCESS; - } - - status = m_buffer->reprogram_device_interrupts_for_end_of_batch(m_transfer_size, prev_batch_size, - vdma::InterruptsDomain::NONE); - CHECK_SUCCESS(status, "Failed reprogramming device interrupts for the end of the previous batch (size {})", - prev_batch_size); - status = m_buffer->reprogram_device_interrupts_for_end_of_batch(m_transfer_size, m_dynamic_batch_size, - vdma::InterruptsDomain::DEVICE); - CHECK_SUCCESS(status, "Failed reprogramming device interrupts for the end of the current batch (size {})", - m_dynamic_batch_size); - - return HAILO_SUCCESS; -} - -Expected InterContextBuffer::read() -{ - const auto size = m_transfer_size * m_dynamic_batch_size; - assert(size <= m_buffer->size()); - - auto res = Buffer::create(size); - CHECK_EXPECTED(res); - - auto status = m_buffer->read(res->data(), size, 0); - CHECK_SUCCESS_AS_EXPECTED(status); - - return res.release(); -} - -CONTROL_PROTOCOL__host_buffer_info_t InterContextBuffer::get_host_buffer_info() const -{ - return m_buffer->get_host_buffer_info(m_transfer_size); -} - -InterContextBuffer::InterContextBuffer(std::unique_ptr &&buffer, uint32_t transfer_size, - uint16_t batch_size) : - m_buffer(std::move(buffer)), - m_transfer_size(transfer_size), - m_max_batch_size(batch_size), - m_dynamic_batch_size(batch_size) -{} - -hailo_status InterContextBuffer::set_dynamic_batch_size(uint16_t batch_size) -{ - if (CONTROL_PROTOCOL__IGNORE_DYNAMIC_BATCH_SIZE == batch_size) { - LOGGER__TRACE("Received CONTROL_PROTOCOL__IGNORE_DYNAMIC_BATCH_SIZE == batch_size; " - "Leaving previously set value of {}", m_dynamic_batch_size); - } else { - CHECK(batch_size <= m_max_batch_size, HAILO_INVALID_ARGUMENT, - "batch_size ({}) must be <= than m_max_batch_size ({})", - batch_size, m_max_batch_size); - - LOGGER__TRACE("Setting intermediate buffer's batch_size to {}", batch_size); - m_dynamic_batch_size = batch_size; - } - - return HAILO_SUCCESS; -} - -Expected> InterContextBuffer::create_sg_buffer(HailoRTDriver &driver, - uint32_t transfer_size, uint16_t batch_size, vdma::ChannelId d2h_channel_id) -{ - auto desc_sizes_pair = vdma::DescriptorList::get_desc_buffer_sizes_for_single_transfer(driver, - batch_size, batch_size, transfer_size); - CHECK_EXPECTED(desc_sizes_pair); - const auto desc_page_size = desc_sizes_pair->first; - const auto descs_count = desc_sizes_pair->second; - - // TODO: HRT-9914 - Instead of using aligned descriptor for each transfer, we should do it for the all frame. - const size_t desc_per_transfer = DIV_ROUND_UP(transfer_size, desc_page_size); - const size_t buffer_size = desc_per_transfer * desc_page_size * batch_size; - auto buffer = vdma::SgBuffer::create(driver, buffer_size, descs_count, desc_page_size, - HailoRTDriver::DmaDirection::BOTH, d2h_channel_id); - CHECK_EXPECTED(buffer); - - auto buffer_ptr = make_unique_nothrow(buffer.release()); - CHECK_NOT_NULL_AS_EXPECTED(buffer_ptr, HAILO_OUT_OF_HOST_MEMORY); - - return std::unique_ptr(std::move(buffer_ptr)); -} - -Expected> InterContextBuffer::create_ccb_buffer(HailoRTDriver &driver, - uint32_t transfer_size, uint16_t batch_size) -{ - // The first 12 channels in D2H CCB ("regular channels") requires that the amount of descriptors will be a power - // of 2. Altough the 4 last channels ("enhanced channels") don't have this requirements, we keep the code the same. - auto buffer_size = vdma::ContinuousBuffer::get_buffer_size_desc_power2(transfer_size * batch_size); - auto buffer = vdma::ContinuousBuffer::create(buffer_size, driver); - CHECK_EXPECTED(buffer); - - auto buffer_ptr = make_unique_nothrow(buffer.release()); - CHECK_NOT_NULL_AS_EXPECTED(buffer_ptr, HAILO_OUT_OF_HOST_MEMORY); - - return std::unique_ptr(std::move(buffer_ptr)); -} - -bool InterContextBuffer::should_use_ccb(HailoRTDriver &driver) -{ - switch (driver.dma_type()) { - case HailoRTDriver::DmaType::PCIE: - return false; - case HailoRTDriver::DmaType::DRAM: - if (nullptr == std::getenv("HAILO_FORCE_INFER_CONTEXT_CHANNEL_OVER_DESC")) { - return false; - } - else { - LOGGER__INFO("Using (non default mode) CCB for inter context channels.\n"); - return true; - } - } - - // Shouldn't reach here - assert(false); - return false; -} - -} /* namespace hailort */ diff --git a/hailort/libhailort/src/core_op/resource_manager/inter_context_buffer.hpp b/hailort/libhailort/src/core_op/resource_manager/inter_context_buffer.hpp deleted file mode 100644 index 912501e..0000000 --- a/hailort/libhailort/src/core_op/resource_manager/inter_context_buffer.hpp +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. - * Distributed under the MIT license (https://opensource.org/licenses/MIT) - **/ -/** - * @file inter_context_buffer.hpp - * @brief Manages inter-context buffer. - */ - -#ifndef _HAILO_INTER_CONTEXT_BUFFER_HPP_ -#define _HAILO_INTER_CONTEXT_BUFFER_HPP_ - -#include "hailo/expected.hpp" -#include "hailo/buffer.hpp" - -#include "os/hailort_driver.hpp" -#include "vdma/memory/vdma_buffer.hpp" - -#include "control_protocol.h" - - -namespace hailort -{ - -class InterContextBuffer final { -public: - static Expected create(HailoRTDriver &driver, uint32_t transfer_size, - uint16_t max_batch_size, vdma::ChannelId d2h_channel_id); - - hailo_status reprogram(uint16_t batch_size); - Expected read(); - - CONTROL_PROTOCOL__host_buffer_info_t get_host_buffer_info() const; - -private: - InterContextBuffer(std::unique_ptr &&buffer, uint32_t transfer_size, uint16_t batch_size); - hailo_status set_dynamic_batch_size(uint16_t batch_size); - - static Expected> create_sg_buffer(HailoRTDriver &driver, - uint32_t transfer_size, uint16_t batch_size, vdma::ChannelId d2h_channel_id); - static Expected> create_ccb_buffer(HailoRTDriver &driver, - uint32_t transfer_size, uint16_t batch_size); - - static bool should_use_ccb(HailoRTDriver &driver); - - std::unique_ptr m_buffer; - const uint32_t m_transfer_size; - const uint16_t m_max_batch_size; - uint16_t m_dynamic_batch_size; -}; - -} /* namespace hailort */ - -#endif /* _HAILO_INTER_CONTEXT_BUFFER_HPP_ */ \ No newline at end of file diff --git a/hailort/libhailort/src/core_op/resource_manager/intermediate_buffer.cpp b/hailort/libhailort/src/core_op/resource_manager/intermediate_buffer.cpp new file mode 100644 index 0000000..2023de1 --- /dev/null +++ b/hailort/libhailort/src/core_op/resource_manager/intermediate_buffer.cpp @@ -0,0 +1,196 @@ +/** + * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file intermediate_buffer.cpp + * @brief Manages intermediate buffers, including inter-context and ddr buffers. + */ + +#include "intermediate_buffer.hpp" + +#include "core_op/resource_manager/resource_manager.hpp" +#include "vdma/memory/sg_buffer.hpp" +#include "vdma/memory/continuous_buffer.hpp" +#include "vdma/memory/buffer_requirements.hpp" + + +namespace hailort +{ +Expected> IntermediateBuffer::create_buffer(HailoRTDriver &driver, uint32_t transfer_size, + uint16_t max_batch_size, vdma::ChannelId d2h_channel_id, StreamingType streaming_type) +{ + const bool is_circular = (streaming_type == StreamingType::CIRCULAR_CONTINUOS); + auto buffer_exp = should_use_ccb(driver, streaming_type) ? + create_ccb_buffer(driver, transfer_size, max_batch_size, is_circular) : + create_sg_buffer(driver, transfer_size, max_batch_size, d2h_channel_id, is_circular); + + if (should_use_ccb(driver, streaming_type) && (HAILO_OUT_OF_HOST_CMA_MEMORY == buffer_exp.status())) { + /* Try to use sg buffer instead */ + return create_sg_buffer(driver, transfer_size, max_batch_size, d2h_channel_id, is_circular); + } else { + return buffer_exp; + } +} + +Expected IntermediateBuffer::create(HailoRTDriver &driver, uint32_t transfer_size, + uint16_t max_batch_size, vdma::ChannelId d2h_channel_id, StreamingType streaming_type) +{ + auto buffer_exp = create_buffer(driver, transfer_size, max_batch_size, d2h_channel_id, streaming_type); + CHECK_EXPECTED(buffer_exp); + auto buffer_ptr = buffer_exp.release(); + + if (streaming_type == StreamingType::BURST) { + // We have max_batch_size transfers, so we program them one by one. The last transfer should report interrupt + // to the device. + size_t acc_offset = 0; + for (uint16_t i = 0; i < max_batch_size; i++) { + const auto last_desc_interrupts_domain = ((max_batch_size - 1) == i) ? + vdma::InterruptsDomain::DEVICE : vdma::InterruptsDomain::NONE; + auto desc_count_local = buffer_ptr->program_descriptors(transfer_size, last_desc_interrupts_domain, acc_offset); + CHECK_EXPECTED(desc_count_local, "Failed to program descs for inter context channels. Given max_batch_size is too big."); + acc_offset += desc_count_local.value(); + } + } else { + // Program all descriptors, no need for interrupt. + const auto interrupts_domain = vdma::InterruptsDomain::NONE; + const auto total_size = buffer_ptr->descs_count() * buffer_ptr->desc_page_size(); + auto desc_count_local = buffer_ptr->program_descriptors(total_size, interrupts_domain, 0); + CHECK_EXPECTED(desc_count_local); + } + + return IntermediateBuffer(std::move(buffer_ptr), transfer_size, max_batch_size, streaming_type); +} + +hailo_status IntermediateBuffer::set_dynamic_batch_size(uint16_t batch_size) +{ + if (m_streaming_type == StreamingType::CIRCULAR_CONTINUOS) { + // The buffer pattern does not depend on the batch for circular continuous buffers. + return HAILO_SUCCESS; + } + + CHECK(batch_size <= m_max_batch_size, HAILO_INVALID_ARGUMENT, + "batch_size ({}) must be <= than m_max_batch_size ({})", + batch_size, m_max_batch_size); + + LOGGER__TRACE("Setting intermediate buffer's batch_size to {}", batch_size); + const auto prev_batch_size = m_dynamic_batch_size; + m_dynamic_batch_size = batch_size; + + auto status = m_buffer->reprogram_device_interrupts_for_end_of_batch(m_transfer_size, prev_batch_size, + vdma::InterruptsDomain::NONE); + CHECK_SUCCESS(status, "Failed reprogramming device interrupts for the end of the previous batch (size {})", + prev_batch_size); + status = m_buffer->reprogram_device_interrupts_for_end_of_batch(m_transfer_size, m_dynamic_batch_size, + vdma::InterruptsDomain::DEVICE); + CHECK_SUCCESS(status, "Failed reprogramming device interrupts for the end of the current batch (size {})", + m_dynamic_batch_size); + + return HAILO_SUCCESS; +} + +Expected IntermediateBuffer::read() +{ + const auto size = m_transfer_size * m_dynamic_batch_size; + assert(size <= m_buffer->size()); + + auto res = Buffer::create(size); + CHECK_EXPECTED(res); + + auto status = m_buffer->read(res->data(), size, 0); + CHECK_SUCCESS_AS_EXPECTED(status); + + return res.release(); +} + +CONTROL_PROTOCOL__host_buffer_info_t IntermediateBuffer::get_host_buffer_info() const +{ + return m_buffer->get_host_buffer_info(m_transfer_size); +} + +IntermediateBuffer::IntermediateBuffer(std::unique_ptr &&buffer, uint32_t transfer_size, + uint16_t batch_size, StreamingType streaming_type) : + m_buffer(std::move(buffer)), + m_transfer_size(transfer_size), + m_max_batch_size(batch_size), + m_streaming_type(streaming_type), + m_dynamic_batch_size(batch_size) +{} + +Expected> IntermediateBuffer::create_sg_buffer(HailoRTDriver &driver, + uint32_t transfer_size, uint16_t batch_size, vdma::ChannelId d2h_channel_id, bool is_circular) +{ + auto const DONT_FORCE_DEFAULT_PAGE_SIZE = false; + auto buffer_requirements = vdma::BufferSizesRequirements::get_sg_buffer_requirements_single_transfer( + driver.desc_max_page_size(), batch_size, batch_size, transfer_size, is_circular, DONT_FORCE_DEFAULT_PAGE_SIZE); + CHECK_EXPECTED(buffer_requirements); + const auto desc_page_size = buffer_requirements->desc_page_size(); + const auto descs_count = buffer_requirements->descs_count(); + const auto buffer_size = buffer_requirements->buffer_size(); + + auto buffer = vdma::SgBuffer::create(driver, buffer_size, descs_count, desc_page_size, is_circular, + HailoRTDriver::DmaDirection::BOTH, d2h_channel_id); + CHECK_EXPECTED(buffer); + + auto buffer_ptr = make_unique_nothrow(buffer.release()); + CHECK_NOT_NULL_AS_EXPECTED(buffer_ptr, HAILO_OUT_OF_HOST_MEMORY); + + return std::unique_ptr(std::move(buffer_ptr)); +} + +Expected> IntermediateBuffer::create_ccb_buffer(HailoRTDriver &driver, + uint32_t transfer_size, uint16_t batch_size, bool is_circular) +{ + auto buffer_size_requirements = vdma::BufferSizesRequirements::get_ccb_buffer_requirements_single_transfer( + batch_size, transfer_size, is_circular); + CHECK_EXPECTED(buffer_size_requirements); + + auto buffer = vdma::ContinuousBuffer::create(buffer_size_requirements->buffer_size(), driver); + /* Don't print error here since this might be expected error that the libhailoRT can recover from + (out of host memory). If it's not the case, there is a print in hailort_driver.cpp file */ + if (HAILO_OUT_OF_HOST_CMA_MEMORY == buffer.status()) { + return make_unexpected(buffer.status()); + } else { + CHECK_EXPECTED(buffer); + } + + auto buffer_ptr = make_unique_nothrow(buffer.release()); + CHECK_NOT_NULL_AS_EXPECTED(buffer_ptr, HAILO_OUT_OF_HOST_MEMORY); + + return std::unique_ptr(std::move(buffer_ptr)); +} + +bool IntermediateBuffer::should_use_ccb(HailoRTDriver &driver, StreamingType streaming_type) +{ + if (driver.dma_type() == HailoRTDriver::DmaType::PCIE) { + // CCB not supported on PCIe + return false; + } + + switch (streaming_type) { + case StreamingType::BURST: + // On burst (aka inter-context), because the buffers are big (And depends on the max_batch_size), we currently + // don't want to use CCB by default. + if (nullptr != std::getenv("HAILO_FORCE_INFER_CONTEXT_CHANNEL_OVER_DESC")) { + LOGGER__WARNING("Using desc instead of CCB for inter context channels is not optimal for performance.\n"); + return false; + } else { + return true; + } + case StreamingType::CIRCULAR_CONTINUOS: + // On circular_continuous (aka ddr), the buffers are relatively small and we want to verify the C2C mechanism, + // therefore the CCB is the default behaviour. + if (nullptr != std::getenv("HAILO_FORCE_DDR_CHANNEL_OVER_DESC")) { + LOGGER__WARNING("Using desc instead of CCB for ddr channel is not optimal for performance.\n"); + return false; + } else { + return true; + } + } + + // Shouldn't reach here + assert(false); + return false; +} + +} /* namespace hailort */ diff --git a/hailort/libhailort/src/core_op/resource_manager/intermediate_buffer.hpp b/hailort/libhailort/src/core_op/resource_manager/intermediate_buffer.hpp new file mode 100644 index 0000000..0d4deca --- /dev/null +++ b/hailort/libhailort/src/core_op/resource_manager/intermediate_buffer.hpp @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file intermediate_buffer.hpp + * @brief Manages intermediate buffers, including inter-context and ddr buffers. + */ + +#ifndef _HAILO_INTERMEDIATE_BUFFER_HPP_ +#define _HAILO_INTERMEDIATE_BUFFER_HPP_ + +#include "hailo/expected.hpp" +#include "hailo/buffer.hpp" + +#include "os/hailort_driver.hpp" +#include "vdma/memory/vdma_buffer.hpp" + +#include "control_protocol.h" + + +namespace hailort +{ + +class IntermediateBuffer final { +public: + + enum class StreamingType { + // Used for inter-context buffer. The buffer is not circular and the data is fetched in bursts. + BURST, + + // Used for ddr-channel buffers. The buffer is circular and fetched continuously. + CIRCULAR_CONTINUOS, + }; + + static Expected create(HailoRTDriver &driver, uint32_t transfer_size, + uint16_t max_batch_size, vdma::ChannelId d2h_channel_id, StreamingType streaming_type); + + hailo_status set_dynamic_batch_size(uint16_t batch_size); + Expected read(); + CONTROL_PROTOCOL__host_buffer_info_t get_host_buffer_info() const; + +private: + IntermediateBuffer(std::unique_ptr &&buffer, uint32_t transfer_size, uint16_t batch_size, + StreamingType streaming_type); + + static Expected> create_sg_buffer(HailoRTDriver &driver, + uint32_t transfer_size, uint16_t batch_size, vdma::ChannelId d2h_channel_id, bool is_circular); + static Expected> create_ccb_buffer(HailoRTDriver &driver, + uint32_t transfer_size, uint16_t batch_size, bool is_circular); + static Expected> create_buffer(HailoRTDriver &driver, uint32_t transfer_size, + uint16_t max_batch_size, vdma::ChannelId d2h_channel_id, StreamingType streaming_type); + + static bool should_use_ccb(HailoRTDriver &driver, StreamingType streaming_type); + + std::unique_ptr m_buffer; + const uint32_t m_transfer_size; + const uint16_t m_max_batch_size; + const StreamingType m_streaming_type; + uint16_t m_dynamic_batch_size; +}; + +} /* namespace hailort */ + +#endif /* _HAILO_INTERMEDIATE_BUFFER_HPP_ */ \ No newline at end of file diff --git a/hailort/libhailort/src/core_op/resource_manager/resource_manager.cpp b/hailort/libhailort/src/core_op/resource_manager/resource_manager.cpp index 64b97e9..be3ac4c 100644 --- a/hailort/libhailort/src/core_op/resource_manager/resource_manager.cpp +++ b/hailort/libhailort/src/core_op/resource_manager/resource_manager.cpp @@ -2,6 +2,7 @@ #include "core_op/resource_manager/resource_manager.hpp" #include "vdma/channel/boundary_channel.hpp" +#include "vdma/memory/buffer_requirements.hpp" #include "device_common/control.hpp" #include @@ -42,14 +43,24 @@ ContextSwitchBufferBuilder &ContextResources::builder() return m_builder; } -void ContextResources::add_edge_layer(const LayerInfo &layer_info, vdma::ChannelId channel_id, - const CONTROL_PROTOCOL__host_buffer_info_t &buffer_info) +hailo_status ContextResources::add_edge_layer(const LayerInfo &layer_info, vdma::ChannelId channel_id, + const CONTROL_PROTOCOL__host_buffer_info_t &buffer_info, const SupportedFeatures &supported_features) { + auto status = validate_edge_layer(layer_info, channel_id, supported_features); + CHECK_SUCCESS(status); + m_edge_layers.emplace_back(EdgeLayer{ layer_info, channel_id, buffer_info }); + + return HAILO_SUCCESS; +} + +void ContextResources::add_ddr_channels_info(const DdrChannelsInfo &ddr_info) +{ + m_ddr_channels_infos.emplace_back(ddr_info); } std::vector ContextResources::get_edge_layers() const @@ -80,10 +91,11 @@ std::vector ContextResources::get_edge_layers(LayerType layer_type, h return edge_layers; } -Expected ContextResources::get_edge_layer_by_stream_index(uint8_t stream_index) const +Expected ContextResources::get_edge_layer_by_stream_index(const uint8_t stream_index, + const hailo_stream_direction_t direction) const { for (const auto &edge_layer : m_edge_layers) { - if (edge_layer.layer_info.stream_index == stream_index) { + if ((stream_index == edge_layer.layer_info.stream_index) && (direction == edge_layer.layer_info.direction)) { return EdgeLayer(edge_layer); } } @@ -92,21 +104,11 @@ Expected ContextResources::get_edge_layer_by_stream_index(uint8_t str return make_unexpected(HAILO_INTERNAL_FAILURE); } - -ExpectedRef ContextResources::create_ddr_channels_pair(const DdrChannelsInfo &ddr_info) -{ - auto buffer = DdrChannelsPair::create(m_driver, ddr_info); - CHECK_EXPECTED(buffer); - - m_ddr_channels_pairs.emplace_back(buffer.release()); - return std::ref(m_ddr_channels_pairs.back()); -} - -ExpectedRef ContextResources::get_ddr_channels_pair(uint8_t d2h_stream_index) const +Expected ContextResources::get_ddr_channels_info(uint8_t d2h_stream_index) const { - for (auto &ddr_channels_pair : m_ddr_channels_pairs) { - if (ddr_channels_pair.info().d2h_stream_index == d2h_stream_index) { - return std::ref(ddr_channels_pair); + for (const auto &ddr_channels_info : m_ddr_channels_infos) { + if (ddr_channels_info.d2h_stream_index == d2h_stream_index) { + return DdrChannelsInfo{ddr_channels_info}; } } @@ -114,18 +116,39 @@ ExpectedRef ContextResources::get_ddr_channels_pair(uint8 return make_unexpected(HAILO_INTERNAL_FAILURE); } -const std::vector &ContextResources::get_ddr_channels_pairs() const +const std::vector &ContextResources::get_ddr_channels_infos() const { - return m_ddr_channels_pairs; + return m_ddr_channels_infos; } -hailo_status ContextResources::validate_edge_layers() +hailo_status ContextResources::validate_edge_layer(const LayerInfo &layer_info, vdma::ChannelId channel_id, + const SupportedFeatures &supported_features) { - std::set used_channel_ids; + bool stream_index_already_used = false; + for (const auto &edge_layer : m_edge_layers) { - CHECK(used_channel_ids.find(edge_layer.channel_id) == used_channel_ids.end(), HAILO_INTERNAL_FAILURE, - "Same stream use the same channel id {}", edge_layer.channel_id); - used_channel_ids.insert(edge_layer.channel_id); + CHECK(!(edge_layer.channel_id == channel_id), HAILO_INTERNAL_FAILURE, + "Same stream use the same channel id {}", channel_id); + + // In Activation Context it is ok to have multiple edge layers with same stream index seeing as they could be for + // Different contexts etc... + if (CONTROL_PROTOCOL__CONTEXT_SWITCH_CONTEXT_TYPE_ACTIVATION != m_builder.get_context_type()) { + if (edge_layer.layer_info.stream_index == layer_info.stream_index) { + // Validate that the amount of edge layers with the same stream index per context is 2 (And with opposite directions) + // In the case of dual direction supported feature - otherwise 1 + if (supported_features.dual_direction_stream_index) { + CHECK(!stream_index_already_used, HAILO_INTERNAL_FAILURE, + "Stream Index {} used for too many edge layers in one context", edge_layer.layer_info.stream_index); + CHECK(layer_info.direction != edge_layer.layer_info.direction, HAILO_INTERNAL_FAILURE, + "Stream Index {} used for other edge layer in same direction", edge_layer.layer_info.stream_index); + stream_index_already_used = true; + } else { + LOGGER__ERROR("Stream Index {} used for too many edge layers in one context", + edge_layer.layer_info.stream_index); + return HAILO_INTERNAL_FAILURE; + } + } + } } return HAILO_SUCCESS; @@ -169,7 +192,7 @@ static Expected create_latency_meters_from_config_params( LatencyMetersMap latency_meters_map; if ((config_params.latency & HAILO_LATENCY_MEASURE) == HAILO_LATENCY_MEASURE) { - // Best affort for starting latency meter. + // Best effort for starting latency meter. auto networks_names = core_op_metadata->get_network_names(); for (auto &network_name : networks_names) { auto layer_infos = core_op_metadata->get_all_layer_infos(network_name); @@ -196,7 +219,7 @@ Expected ResourcesManager::create(VdmaDevice &vdma_device, Hai const auto &config_channels_info = core_op_metadata->config_channels_info(); config_channels_ids.reserve(config_channels_info.size()); for (uint8_t cfg_index = 0; cfg_index < config_channels_info.size(); cfg_index++) { - const auto layer_identifier = std::make_tuple(LayerType::CFG, "", cfg_index); + const auto layer_identifier = std::make_tuple(LayerType::CFG, HAILO_H2D_STREAM, "", cfg_index); const auto engine_index = config_channels_info[cfg_index].engine_index; auto channel_id = allocator.get_available_channel_id(layer_identifier, HailoRTDriver::DmaDirection::H2D, engine_index); CHECK_EXPECTED(channel_id); @@ -225,7 +248,7 @@ ResourcesManager::ResourcesManager(VdmaDevice &vdma_device, HailoRTDriver &drive m_vdma_device(vdma_device), m_driver(driver), m_config_params(config_params), - m_inter_context_buffers(), + m_intermediate_buffers(), m_core_op_metadata(std::move(core_op_metadata)), m_core_op_index(core_op_index), m_dynamic_context_count(0), @@ -244,7 +267,7 @@ ResourcesManager::ResourcesManager(ResourcesManager &&other) noexcept : m_vdma_device(other.m_vdma_device), m_driver(other.m_driver), m_config_params(other.m_config_params), - m_inter_context_buffers(std::move(other.m_inter_context_buffers)), + m_intermediate_buffers(std::move(other.m_intermediate_buffers)), m_core_op_metadata(std::move(other.m_core_op_metadata)), m_core_op_index(other.m_core_op_index), m_dynamic_context_count(std::exchange(other.m_dynamic_context_count, static_cast(0))), @@ -340,6 +363,24 @@ void ResourcesManager::process_interrupts(IrqData &&irq_data) } } +// TODO: after adding NMS single int, we can create an async channel for async nms output stream (HRT-10553) +Expected ResourcesManager::get_boundary_vdma_channel_type(const LayerInfo &layer_info) +{ + CHECK_AS_EXPECTED(contains(m_config_params.stream_params_by_name, layer_info.name), HAILO_INVALID_ARGUMENT, + "Can't find stream params for layer {}", layer_info.name); + const auto async_stream = (0 != (m_config_params.stream_params_by_name.at(layer_info.name).flags & HAILO_STREAM_FLAGS_ASYNC)); + if (async_stream) { + // NMS async streams use buffered channels + if (layer_info.format.order == HAILO_FORMAT_ORDER_HAILO_NMS) { + return vdma::BoundaryChannel::Type::BUFFERED; + } + // Non-nms async streams use async channels + return vdma::BoundaryChannel::Type::ASYNC; + } + // Buffered streams => buffered channels + return vdma::BoundaryChannel::Type::BUFFERED; +} + hailo_status ResourcesManager::create_boundary_vdma_channel(const LayerInfo &layer_info) { // TODO: put in layer info @@ -349,34 +390,46 @@ hailo_status ResourcesManager::create_boundary_vdma_channel(const LayerInfo &lay channel_direction, layer_info.dma_engine_index); CHECK_EXPECTED_AS_STATUS(channel_id); - auto network_batch_size = get_network_batch_size(layer_info.network_name); + const auto network_batch_size = get_network_batch_size(layer_info.network_name); CHECK_EXPECTED_AS_STATUS(network_batch_size); - uint32_t min_active_trans = MIN_ACTIVE_TRANSFERS_SCALE * network_batch_size.value(); - uint32_t max_active_trans = MAX_ACTIVE_TRANSFERS_SCALE * network_batch_size.value(); + const auto nms_max_detections_per_frame = + layer_info.nms_info.number_of_classes * layer_info.nms_info.max_bboxes_per_class * layer_info.nms_info.chunks_per_frame; + + const auto max_active_transfers_scale = (layer_info.format.order == HAILO_FORMAT_ORDER_HAILO_NMS) ? + (nms_max_detections_per_frame * MAX_ACTIVE_TRANSFERS_SCALE) : MAX_ACTIVE_TRANSFERS_SCALE; + + const auto min_active_trans = MIN_ACTIVE_TRANSFERS_SCALE * network_batch_size.value(); + const auto max_active_trans = (layer_info.format.order == HAILO_FORMAT_ORDER_HAILO_NMS) ? + /* NMS Case - Value be be higher than UINT16_MAX. in this case we only limit to UART16_MAX with no error */ + std::min(static_cast(UINT16_MAX), max_active_transfers_scale * network_batch_size.value()) : + max_active_transfers_scale * network_batch_size.value(); - CHECK(IS_FIT_IN_UINT16(min_active_trans), HAILO_INVALID_ARGUMENT, + CHECK(IS_FIT_IN_UINT16(min_active_trans), HAILO_INVALID_ARGUMENT, "calculated min_active_trans for vdma descriptor list is out of UINT16 range"); - CHECK(IS_FIT_IN_UINT16(max_active_trans), HAILO_INVALID_ARGUMENT, + CHECK(IS_FIT_IN_UINT16(max_active_trans), HAILO_INVALID_ARGUMENT, "calculated min_active_trans for vdma descriptor list is out of UINT16 range"); auto latency_meter = (contains(m_latency_meters, layer_info.network_name)) ? m_latency_meters.at(layer_info.network_name) : nullptr; /* TODO - HRT-6829- page_size should be calculated inside the vDMA channel class create function */ - const auto transfer_size = (layer_info.nn_stream_config.periph_bytes_per_buffer * - layer_info.nn_stream_config.core_buffers_per_frame); - auto desc_sizes_pair = vdma::DescriptorList::get_desc_buffer_sizes_for_single_transfer(m_driver, - static_cast(min_active_trans), static_cast(max_active_trans), transfer_size); - CHECK_EXPECTED_AS_STATUS(desc_sizes_pair); - - const auto page_size = desc_sizes_pair->first; + static const bool IS_CIRCULAR = true; + const auto transfer_size = LayerInfoUtils::get_layer_transfer_size(layer_info); + + auto const DONT_FORCE_DEFAULT_PAGE_SIZE = false; + auto buffer_sizes_requirements = vdma::BufferSizesRequirements::get_sg_buffer_requirements_single_transfer( + m_driver.desc_max_page_size(), static_cast(min_active_trans), static_cast(max_active_trans), + transfer_size, IS_CIRCULAR, DONT_FORCE_DEFAULT_PAGE_SIZE); + CHECK_EXPECTED_AS_STATUS(buffer_sizes_requirements); + + const auto page_size = buffer_sizes_requirements->desc_page_size(); const auto descs_count = (nullptr != std::getenv("HAILO_CONFIGURE_FOR_HW_INFER")) ? - MAX_DESCS_COUNT : desc_sizes_pair->second; + MAX_DESCS_COUNT : buffer_sizes_requirements->descs_count(); - const auto channel_type = (0 == (m_config_params.stream_params_by_name.at(layer_info.name).flags & HAILO_STREAM_FLAGS_ASYNC)) ? - vdma::BoundaryChannel::Type::BUFFERED : vdma::BoundaryChannel::Type::ASYNC; + auto channel_type = get_boundary_vdma_channel_type(layer_info); + CHECK_EXPECTED_AS_STATUS(channel_type); auto channel = vdma::BoundaryChannel::create(channel_id.value(), channel_direction, m_driver, descs_count, page_size, - layer_info.name, latency_meter, network_batch_size.value(), channel_type); + layer_info.name, latency_meter, network_batch_size.value(), channel_type.release()); CHECK_EXPECTED_AS_STATUS(channel); m_boundary_channels.emplace(channel_id.value(), channel.release()); @@ -410,25 +463,23 @@ hailo_power_mode_t ResourcesManager::get_power_mode() const return m_config_params.power_mode; } -ExpectedRef ResourcesManager::create_inter_context_buffer(uint32_t transfer_size, - uint8_t src_stream_index, uint8_t src_context_index, const std::string &network_name, vdma::ChannelId d2h_channel_id) +ExpectedRef ResourcesManager::create_intermediate_buffer(uint32_t transfer_size, + uint16_t batch_size, uint8_t src_stream_index, uint8_t src_context_index, + vdma::ChannelId d2h_channel_id, IntermediateBuffer::StreamingType streaming_type) { - auto network_batch_size_exp = get_network_batch_size(network_name); - CHECK_EXPECTED(network_batch_size_exp); - auto network_batch_size = network_batch_size_exp.value(); - - auto buffer = InterContextBuffer::create(m_driver, transfer_size, network_batch_size, d2h_channel_id); + auto buffer = IntermediateBuffer::create(m_driver, transfer_size, batch_size, d2h_channel_id, + streaming_type); CHECK_EXPECTED(buffer); const auto key = std::make_pair(src_context_index, src_stream_index); - auto emplace_res = m_inter_context_buffers.emplace(key, buffer.release()); + auto emplace_res = m_intermediate_buffers.emplace(key, buffer.release()); return std::ref(emplace_res.first->second); } -ExpectedRef ResourcesManager::get_inter_context_buffer(const IntermediateBufferKey &key) +ExpectedRef ResourcesManager::get_intermediate_buffer(const IntermediateBufferKey &key) { - auto buffer_it = m_inter_context_buffers.find(key); - if (std::end(m_inter_context_buffers) == buffer_it) { + auto buffer_it = m_intermediate_buffers.find(key); + if (std::end(m_intermediate_buffers) == buffer_it) { return make_unexpected(HAILO_NOT_FOUND); } @@ -490,10 +541,15 @@ Expected ResourcesManager::get_default_streams_interfa return m_vdma_device.get_default_streams_interface(); } -hailo_status ResourcesManager::set_inter_context_channels_dynamic_batch_size(uint16_t dynamic_batch_size) +hailo_status ResourcesManager::set_dynamic_batch_size(uint16_t dynamic_batch_size) { - for (auto &key_buff_pair : m_inter_context_buffers) { - const auto status = key_buff_pair.second.reprogram(dynamic_batch_size); + if (CONTROL_PROTOCOL__IGNORE_DYNAMIC_BATCH_SIZE == dynamic_batch_size) { + LOGGER__TRACE("Received CONTROL_PROTOCOL__IGNORE_DYNAMIC_BATCH_SIZE == batch_size"); + return HAILO_SUCCESS; + } + + for (auto &key_buff_pair : m_intermediate_buffers) { + const auto status = key_buff_pair.second.set_dynamic_batch_size(dynamic_batch_size); CHECK_SUCCESS(status); } @@ -520,24 +576,11 @@ Expected ResourcesManager::get_network_batch_size(const std::string &n Expected ResourcesManager::read_intermediate_buffer(const IntermediateBufferKey &key) { - auto inter_context_buffer_it = m_inter_context_buffers.find(key); - if (std::end(m_inter_context_buffers) != inter_context_buffer_it) { - return inter_context_buffer_it->second.read(); - } - - const auto dynamic_context_index = key.first; - const size_t context_index = dynamic_context_index + CONTROL_PROTOCOL__CONTEXT_SWITCH_NUMBER_OF_NON_DYNAMIC_CONTEXTS; - CHECK_AS_EXPECTED(context_index < m_contexts_resources.size(), HAILO_NOT_FOUND, "Context index {} out of range", - dynamic_context_index); - const auto d2h_stream_index = key.second; - if (auto ddr_channels_pair = m_contexts_resources[context_index].get_ddr_channels_pair(d2h_stream_index)) { - return ddr_channels_pair->get().read(); - } - - LOGGER__ERROR("Failed to find intermediate buffer for src_context {}, src_stream_index {}", key.first, + auto intermediate_buffer_it = m_intermediate_buffers.find(key); + CHECK_AS_EXPECTED(std::end(m_intermediate_buffers) != intermediate_buffer_it, + HAILO_NOT_FOUND, "Failed to find intermediate buffer for src_context {}, src_stream_index {}", key.first, key.second); - return make_unexpected(HAILO_NOT_FOUND); - + return intermediate_buffer_it->second.read(); } hailo_status ResourcesManager::configure() @@ -559,9 +602,9 @@ hailo_status ResourcesManager::configure() return HAILO_SUCCESS; } -hailo_status ResourcesManager::enable_state_machine(uint16_t dynamic_batch_size) +hailo_status ResourcesManager::enable_state_machine(uint16_t dynamic_batch_size, uint16_t batch_count) { - return Control::enable_core_op(m_vdma_device, m_core_op_index, dynamic_batch_size); + return Control::enable_core_op(m_vdma_device, m_core_op_index, dynamic_batch_size, batch_count); } hailo_status ResourcesManager::reset_state_machine(bool keep_nn_config_during_reset) @@ -627,9 +670,8 @@ Expected ResourcesManager::program_desc_for_hw_only_flow(std::shared_p for (uint16_t transfer_index = 0; transfer_index < dynamic_batch_size; transfer_index++) { const auto last_desc_interrupts_domain = ((dynamic_batch_size - 1) == transfer_index) ? vdma::InterruptsDomain::DEVICE : vdma::InterruptsDomain::NONE; - static const auto BUFFER_NOT_CIRCULAR = false; auto desc_count_local = desc_list->program_last_descriptor(single_transfer_size, - last_desc_interrupts_domain, acc_desc_offset, BUFFER_NOT_CIRCULAR); + last_desc_interrupts_domain, acc_desc_offset); CHECK_EXPECTED(desc_count_local, "Failed to program descs for inter context channels. Given max_batch_size is too big."); acc_desc_offset += desc_count_local.value(); } @@ -640,7 +682,7 @@ Expected ResourcesManager::program_desc_for_hw_only_flow(std::shared_p } Expected> ResourcesManager::create_mapped_buffer_for_hw_only_infer( - vdma::BoundaryChannelPtr boundary_channel_ptr, const hailo_vdma_buffer_direction_flags_t direction, + vdma::BoundaryChannelPtr boundary_channel_ptr, const HailoRTDriver::DmaDirection direction, const uint32_t single_transfer_size, const uint16_t dynamic_batch_size, const uint16_t batch_count) { auto total_frames_per_run = dynamic_batch_size * batch_count; @@ -652,15 +694,12 @@ Expected> ResourcesManager::create_mapped_b CHECK_AS_EXPECTED(IS_FIT_IN_UINT16(total_desc_count), HAILO_INVALID_ARGUMENT, "calculated total_desc_count for vdma descriptor list is out of UINT16 range"); - auto mapped_buffer_exp = DmaMappedBuffer::create(total_desc_count * desc_list->desc_page_size(), direction, m_vdma_device); - CHECK_EXPECTED(mapped_buffer_exp); - - auto mapped_buffer = make_shared_nothrow(mapped_buffer_exp.release()); - CHECK_NOT_NULL_AS_EXPECTED(mapped_buffer, HAILO_OUT_OF_HOST_MEMORY); - m_hw_only_boundary_buffers.push_back(mapped_buffer); + auto mapped_buffer = vdma::MappedBuffer::create_shared(m_driver, direction, total_desc_count * desc_list->desc_page_size()); + CHECK_EXPECTED(mapped_buffer); + m_hw_only_boundary_buffers.emplace_back(mapped_buffer.release()); uint32_t STARTING_DESC = 0; - auto status = desc_list->configure_to_use_buffer(*mapped_buffer, boundary_channel_ptr->get_channel_id(), STARTING_DESC); + auto status = desc_list->configure_to_use_buffer(*m_hw_only_boundary_buffers.back(), boundary_channel_ptr->get_channel_id(), STARTING_DESC); CHECK_SUCCESS_AS_EXPECTED(status); auto desc_programed = program_desc_for_hw_only_flow(desc_list, single_transfer_size, dynamic_batch_size, batch_count); @@ -684,13 +723,32 @@ void ResourcesManager::add_channel_to_hw_infer_channel_info(std::pair(opaque)->notify_one(); + return; + }; + + auto status = get_device().set_notification_callback(callback, + HAILO_NOTIFICATION_ID_HW_INFER_MANAGER_INFER_DONE, static_cast(&infer_done_cond)); + CHECK_SUCCESS(status); + + return HAILO_SUCCESS; +} + Expected ResourcesManager::calc_hw_infer_batch_count(uint16_t dynamic_batch_size) { uint16_t batch_count = UINT16_MAX; for (const auto &layer_info : m_core_op_metadata->get_all_layer_infos()) { const auto stream_info = LayerInfoUtils::get_stream_info_from_layer_info(layer_info); - const auto single_transfer_size = (HAILO_FORMAT_ORDER_HAILO_NMS == stream_info.format.order) ? - stream_info.nms_info.bbox_size : stream_info.hw_frame_size; + uint32_t single_transfer_size = LayerInfoUtils::get_stream_transfer_size(stream_info, layer_info); auto boundary_channel_ptr_exp = get_boundary_vdma_channel_by_stream_name(layer_info.name); CHECK_EXPECTED(boundary_channel_ptr_exp); auto boundary_channel_ptr = boundary_channel_ptr_exp.release(); @@ -701,33 +759,40 @@ Expected ResourcesManager::calc_hw_infer_batch_count(uint16_t dynamic_ return batch_count; } -void ResourcesManager::hw_infer_calc_stats(uint16_t batch_count, uint16_t dynamic_batch_size, +HwInferResults ResourcesManager::hw_infer_calc_stats(uint16_t batch_count, uint16_t dynamic_batch_size, size_t single_frame_transfer_size, uint32_t infer_cycles) { - const auto total_transfer_size = single_frame_transfer_size * dynamic_batch_size * batch_count; - const auto total_frames = dynamic_batch_size * batch_count; + HwInferResults hw_infer_results{}; + const size_t total_transfer_size = single_frame_transfer_size * dynamic_batch_size * batch_count; + const size_t total_frames_passed = dynamic_batch_size * batch_count; // TODO - get clock rate from Chip (still not supported in VPU mode) const float32_t CPU_CLOCK_RATE = static_cast(5.0 / (1000 * 1000 * 1000)); const float32_t time_sec = static_cast(infer_cycles) * CPU_CLOCK_RATE; - const float32_t fps = static_cast(total_frames) / time_sec; + const float32_t fps = static_cast(total_frames_passed) / time_sec; const float32_t BYTE_TO_BIT = 8.0; const float32_t BITS_TO_GBIT = static_cast(1.0 * 1000 * 1000 * 1000); const float32_t BW_Gbps = static_cast(total_transfer_size) * BYTE_TO_BIT / time_sec / BITS_TO_GBIT; - LOGGER__ERROR("\nBatch count - {}\nTotal transfer size: {}\ntotal_frames - {}\ntime_sec - {}\nfps - {}\nBW_Gbps - {}", - batch_count, total_transfer_size, total_frames, time_sec, fps, BW_Gbps); + + /* Prepare results */ + hw_infer_results.batch_count = batch_count; + hw_infer_results.total_transfer_size = total_transfer_size; + hw_infer_results.total_frames_passed = total_frames_passed; + hw_infer_results.time_sec = time_sec; + hw_infer_results.fps = fps; + hw_infer_results.BW_Gbps = BW_Gbps; + + return hw_infer_results; } -Expected ResourcesManager::run_hw_only_infer(uint16_t dynamic_batch_size) +Expected ResourcesManager::run_hw_only_infer() { - CONTROL_PROTOCOL__hw_only_infer_results_t infer_results = {}; - CONTROL_PROTOCOL__hw_infer_channels_info_t channels_info = {}; + CONTROL_PROTOCOL__hw_only_infer_results_t fw_infer_results{}; + CONTROL_PROTOCOL__hw_infer_channels_info_t channels_info{}; channels_info.channel_count = 0; + static constexpr auto INFER_TIMEOUT = std::chrono::milliseconds(120000); - CHECK_AS_EXPECTED(dynamic_batch_size <= m_config_params.batch_size, HAILO_INVALID_ARGUMENT, - "Dynamic batch size must be up to configured batch size"); - - auto batch_count = calc_hw_infer_batch_count(dynamic_batch_size); + auto batch_count = calc_hw_infer_batch_count(m_config_params.batch_size); CHECK_EXPECTED(batch_count); for (const auto &layer_info : m_core_op_metadata->get_all_layer_infos()) { @@ -737,31 +802,36 @@ Expected ResourcesManager::run_hw_onl auto single_transfer_size = (HAILO_FORMAT_ORDER_HAILO_NMS == stream_info.format.order) ? stream_info.nms_info.bbox_size : stream_info.hw_frame_size; const auto direction = (layer_info.direction == HAILO_H2D_STREAM) ? - HAILO_VDMA_BUFFER_DIRECTION_FLAGS_H2D : HAILO_VDMA_BUFFER_DIRECTION_FLAGS_D2H; + HailoRTDriver::DmaDirection::H2D : HailoRTDriver::DmaDirection::D2H; auto channel_info_pair = create_mapped_buffer_for_hw_only_infer(boundary_channel_ptr.release(), direction, - single_transfer_size, dynamic_batch_size, batch_count.value()); + single_transfer_size, m_config_params.batch_size, batch_count.value()); CHECK_EXPECTED(channel_info_pair); add_channel_to_hw_infer_channel_info(channel_info_pair.release(), channels_info); } - auto status = Control::start_hw_only_infer(m_vdma_device, m_core_op_index, dynamic_batch_size, &channels_info); + std::condition_variable infer_done_cond; + auto status = set_hw_infer_done_notification(infer_done_cond); + CHECK_SUCCESS_AS_EXPECTED(status); + + std::mutex mutex; + std::unique_lock lock(mutex); + + status = Control::start_hw_only_infer(m_vdma_device, m_core_op_index, m_config_params.batch_size, + batch_count.value(), &channels_info); CHECK_SUCCESS_AS_EXPECTED(status); - // Delay until infer ends - // TODO HRT-9829 - chagne to notification from FW - std::this_thread::sleep_for(std::chrono::milliseconds(20000)); + infer_done_cond.wait_for(lock, INFER_TIMEOUT); - status = Control::stop_hw_only_infer(m_vdma_device, &infer_results); + status = Control::stop_hw_only_infer(m_vdma_device, &fw_infer_results); CHECK_SUCCESS_AS_EXPECTED(status); auto single_frame_transfer_size = m_core_op_metadata->get_total_transfer_size(); CHECK_EXPECTED(single_frame_transfer_size); - hw_infer_calc_stats(batch_count.value(), dynamic_batch_size, single_frame_transfer_size.release(), infer_results.infer_cycles); - - return infer_results; + return hw_infer_calc_stats(batch_count.value(), m_config_params.batch_size, single_frame_transfer_size.release(), + fw_infer_results.infer_cycles); } } /* namespace hailort */ diff --git a/hailort/libhailort/src/core_op/resource_manager/resource_manager.hpp b/hailort/libhailort/src/core_op/resource_manager/resource_manager.hpp index 9417167..3a6b4db 100644 --- a/hailort/libhailort/src/core_op/resource_manager/resource_manager.hpp +++ b/hailort/libhailort/src/core_op/resource_manager/resource_manager.hpp @@ -28,8 +28,7 @@ #include "hailo/hailort.h" -#include "core_op/resource_manager/inter_context_buffer.hpp" -#include "core_op/resource_manager/ddr_channels_pair.hpp" +#include "core_op/resource_manager/intermediate_buffer.hpp" #include "core_op/resource_manager/config_buffer.hpp" #include "core_op/resource_manager/channel_allocator.hpp" #include "core_op/resource_manager/context_switch_buffer_builder.hpp" @@ -42,6 +41,7 @@ namespace hailort { #define DEFAULT_ACTUAL_BATCH_SIZE (1) +#define MAX_NUMBER_DATA_STREAM_INDEX (20) struct EdgeLayer { @@ -50,6 +50,39 @@ struct EdgeLayer { CONTROL_PROTOCOL__host_buffer_info_t buffer_info; }; +struct DdrChannelsInfo +{ + vdma::ChannelId d2h_channel_id; + uint8_t d2h_stream_index; + vdma::ChannelId h2d_channel_id; + uint8_t h2d_stream_index; + CONTROL_PROTOCOL__host_buffer_info_t host_buffer_info; + uint8_t network_index; + uint16_t row_size; + uint16_t min_buffered_rows; + // total_buffers_per_frame not same as core_buffer_per frame. + //(In DDR core buffer per frame is 1). Used to calc total host descriptors_per_frame. + uint16_t total_buffers_per_frame; + + // Checks if the credits are automaticaly going from d2h channel to its h2d channel, or it needs to be done manually + // (Using a fw task). + bool need_manual_credit_management() const + { + return host_buffer_info.buffer_type == CONTROL_PROTOCOL__HOST_BUFFER_TYPE_EXTERNAL_DESC; + } + + uint16_t descs_count() const + { + assert(IS_FIT_IN_UINT16(host_buffer_info.total_desc_count)); + return static_cast(host_buffer_info.total_desc_count); + } + + uint32_t descriptors_per_frame() const + { + return (row_size / host_buffer_info.desc_page_size) * total_buffers_per_frame; + } +}; + class ContextResources final { public: static Expected create(HailoRTDriver &driver, CONTROL_PROTOCOL__context_switch_context_type_t context_type, @@ -58,21 +91,23 @@ public: const std::vector &get_controls() const; ContextSwitchBufferBuilder &builder(); - void add_edge_layer(const LayerInfo &layer_info, vdma::ChannelId channel_id, - const CONTROL_PROTOCOL__host_buffer_info_t &buffer_info); + hailo_status add_edge_layer(const LayerInfo &layer_info, vdma::ChannelId channel_id, + const CONTROL_PROTOCOL__host_buffer_info_t &buffer_info, const SupportedFeatures &supported_features); + void add_ddr_channels_info(const DdrChannelsInfo &ddr_info); std::vector get_edge_layers() const; std::vector get_edge_layers(LayerType layer_type) const; std::vector get_edge_layers(hailo_stream_direction_t direction) const; std::vector get_edge_layers(LayerType layer_type, hailo_stream_direction_t direction) const; - Expected get_edge_layer_by_stream_index(uint8_t stream_index) const; + Expected get_edge_layer_by_stream_index(const uint8_t stream_index, + const hailo_stream_direction_t direction) const; - ExpectedRef create_ddr_channels_pair(const DdrChannelsInfo &ddr_info); - ExpectedRef get_ddr_channels_pair(uint8_t d2h_stream_index) const; - const std::vector &get_ddr_channels_pairs() const; + Expected get_ddr_channels_info(uint8_t d2h_stream_index) const; + const std::vector &get_ddr_channels_infos() const; - hailo_status validate_edge_layers(); + hailo_status validate_edge_layer(const LayerInfo &layer_info, vdma::ChannelId channel_id, + const SupportedFeatures &supported_features); std::vector &get_config_buffers(); @@ -87,9 +122,9 @@ private: std::reference_wrapper m_driver; ContextSwitchBufferBuilder m_builder; std::vector m_config_buffers; - std::vector m_ddr_channels_pairs; std::vector m_edge_layers; + std::vector m_ddr_channels_infos; }; class ResourcesManager final @@ -106,9 +141,11 @@ public: ResourcesManager &operator=(ResourcesManager &&other) = delete; ResourcesManager(ResourcesManager &&other) noexcept; - ExpectedRef create_inter_context_buffer(uint32_t transfer_size, uint8_t src_stream_index, - uint8_t src_context_index, const std::string &network_name, vdma::ChannelId d2h_channel_id); - ExpectedRef get_inter_context_buffer(const IntermediateBufferKey &key); + ExpectedRef create_intermediate_buffer(uint32_t transfer_size, uint16_t batch_size, + uint8_t src_stream_index, uint8_t src_context_index, vdma::ChannelId d2h_channel_id, + IntermediateBuffer::StreamingType streaming_type); + ExpectedRef get_intermediate_buffer(const IntermediateBufferKey &key); + Expected get_boundary_vdma_channel_type(const LayerInfo &layer_info); hailo_status create_boundary_vdma_channel(const LayerInfo &layer_info); Expected get_control_core_op_header(); @@ -149,9 +186,10 @@ public: Expected read_intermediate_buffer(const IntermediateBufferKey &key); - hailo_status set_inter_context_channels_dynamic_batch_size(uint16_t dynamic_batch_size); + hailo_status set_dynamic_batch_size(uint16_t dynamic_batch_size); hailo_status configure(); - hailo_status enable_state_machine(uint16_t dynamic_batch_size); + hailo_status enable_state_machine(uint16_t dynamic_batch_size, + uint16_t batch_count = CONTROL_PROTOCOL__INIFINITE_BATCH_COUNT); hailo_status reset_state_machine(bool keep_nn_config_during_reset = false); hailo_status cancel_pending_async_transfers(); hailo_status start_vdma_interrupts_dispatcher(); @@ -163,14 +201,15 @@ public: Expected program_desc_for_hw_only_flow(std::shared_ptr desc_list, const uint32_t single_transfer_size, const uint16_t dynamic_batch_size, const uint16_t batch_count); Expected> create_mapped_buffer_for_hw_only_infer( - vdma::BoundaryChannelPtr boundary_channel_ptr, const hailo_vdma_buffer_direction_flags_t direction, + vdma::BoundaryChannelPtr boundary_channel_ptr, const HailoRTDriver::DmaDirection direction, const uint32_t single_transfer_size, const uint16_t dynamic_batch_size, const uint16_t batch_count); void add_channel_to_hw_infer_channel_info(std::pair channel_info, CONTROL_PROTOCOL__hw_infer_channels_info_t &channels_info); Expected calc_hw_infer_batch_count(uint16_t dynamic_batch_size); - void hw_infer_calc_stats(uint16_t batch_count, uint16_t dynamic_batch_size, + HwInferResults hw_infer_calc_stats(uint16_t batch_count, uint16_t dynamic_batch_size, size_t single_frame_transfer_size, uint32_t infer_cycles); - Expected run_hw_only_infer(uint16_t dynamic_batch_size); + hailo_status set_hw_infer_done_notification(std::condition_variable &infer_done_cond); + Expected run_hw_only_infer(); private: hailo_status fill_infer_features(CONTROL_PROTOCOL__application_header_t &app_header); @@ -184,7 +223,7 @@ private: VdmaDevice &m_vdma_device; HailoRTDriver &m_driver; const ConfigureNetworkParams m_config_params; - std::map m_inter_context_buffers; + std::map m_intermediate_buffers; std::shared_ptr m_core_op_metadata; uint8_t m_core_op_index; uint8_t m_dynamic_context_count; @@ -198,7 +237,7 @@ private: // config_stream_index. std::vector m_config_channels_ids; // Mapped buffers would be used only in hw only flow - std::vector> m_hw_only_boundary_buffers; + std::vector> m_hw_only_boundary_buffers; ResourcesManager(VdmaDevice &vdma_device, HailoRTDriver &driver, ChannelAllocator &&channel_allocator, const ConfigureNetworkParams config_params, diff --git a/hailort/libhailort/src/core_op/resource_manager/resource_manager_builder.cpp b/hailort/libhailort/src/core_op/resource_manager/resource_manager_builder.cpp index 0218b10..b05b833 100644 --- a/hailort/libhailort/src/core_op/resource_manager/resource_manager_builder.cpp +++ b/hailort/libhailort/src/core_op/resource_manager/resource_manager_builder.cpp @@ -15,7 +15,7 @@ namespace hailort { -static uint16_t calculate_periph_buffers_per_frame(const CONTROL_PROTOCOL__hw_consts_t &hw_consts, +static uint16_t calculate_power_optimized_periph_buffers_per_frame(const CONTROL_PROTOCOL__hw_consts_t &hw_consts, uint16_t min_periph_buffers_per_frame, uint32_t frame_size, uint16_t periph_buffers_per_frame) { const auto max_periph_buffers_per_frame = MIN(frame_size, static_cast(hw_consts.max_periph_buffers_per_frame)); @@ -37,78 +37,165 @@ static uint16_t calculate_periph_buffers_per_frame(const CONTROL_PROTOCOL__hw_co } } -static hailo_status calculate_credit_params(const CONTROL_PROTOCOL__hw_consts_t &hw_consts, uint16_t desc_page_size, - hailo_stream_direction_t direction, bool should_optimize_credits, uint16_t *periph_bytes_per_buffer, - uint16_t *periph_buffers_per_frame) +static Expected calculate_credit_params(const CONTROL_PROTOCOL__hw_consts_t &hw_consts, uint16_t desc_page_size, + bool should_optimize_credits, const LayerInfo &layer_info) { // Next parameters differ between RX and TX - auto local_periph_bytes_per_buffer = (*periph_bytes_per_buffer); - auto local_periph_buffers_per_frame = (*periph_buffers_per_frame); - uint32_t periph_frame_size = (*periph_bytes_per_buffer) * (*periph_buffers_per_frame); - const auto max_bytes_per_buffer = MAX(hw_consts.max_acceptable_bytes_per_buffer, (*periph_bytes_per_buffer)); + auto local_periph_bytes_per_buffer = layer_info.nn_stream_config.periph_bytes_per_buffer; + auto local_periph_buffers_per_frame = layer_info.nn_stream_config.periph_buffers_per_frame; + uint32_t periph_frame_size = local_periph_bytes_per_buffer * local_periph_buffers_per_frame; + const auto max_bytes_per_buffer = MAX(hw_consts.max_acceptable_bytes_per_buffer, local_periph_bytes_per_buffer); - if (0 != (local_periph_bytes_per_buffer % hw_consts.fifo_word_granularity_bytes)) { - return HAILO_INTERNAL_FAILURE; - } + CHECK_AS_EXPECTED(0 == (local_periph_bytes_per_buffer % hw_consts.fifo_word_granularity_bytes), HAILO_INTERNAL_FAILURE, + "Error, Invalid periph bytes ber puffer value {} must divide by {} with no remainder", + local_periph_bytes_per_buffer, hw_consts.fifo_word_granularity_bytes); if (should_optimize_credits) { // If credits optimizations flag is on, assuming periph_buffers_per_frame * periph_bytes_per_buffer == periph_frame_size // Find the lowest periph_buffers_per_frame that divides periph_frame_size and is bigger than periph_frame_size / max_bytes_per_buffer // Also, periph_bytes_per_buffer must be a multiple of 8 const auto min_periph_buffers_per_frame = DIV_ROUND_UP(periph_frame_size, max_bytes_per_buffer); - local_periph_buffers_per_frame = calculate_periph_buffers_per_frame(hw_consts, static_cast(min_periph_buffers_per_frame), - periph_frame_size, local_periph_buffers_per_frame); + local_periph_buffers_per_frame = calculate_power_optimized_periph_buffers_per_frame(hw_consts, + static_cast(min_periph_buffers_per_frame), periph_frame_size, local_periph_buffers_per_frame); assert(IS_FIT_IN_UINT16(periph_frame_size / local_periph_buffers_per_frame)); local_periph_bytes_per_buffer = static_cast(periph_frame_size / local_periph_buffers_per_frame); // Must be integer according to last function } // Periph credits size must be lower than the following value to make sure that the credit size allows // for at least desc_page_size bytes left in the FIFO for the last descriptor in the pattern - if ((direction == HAILO_D2H_STREAM) && - (static_cast(local_periph_bytes_per_buffer) > (hw_consts.outbound_data_stream_size - 8 - desc_page_size))) { - LOGGER__ERROR("Current periph_bytes_per_buffer is {} which is too high. Exiting.", local_periph_bytes_per_buffer); - return HAILO_INTERNAL_FAILURE; + const bool space_left_in_fifo = ((layer_info.direction != HAILO_D2H_STREAM) || + (static_cast(local_periph_bytes_per_buffer) <= (hw_consts.outbound_data_stream_size - 8 - desc_page_size))); + CHECK_AS_EXPECTED(space_left_in_fifo, HAILO_INTERNAL_FAILURE, + "Current periph_bytes_per_buffer is {} which is too high. Exiting.", local_periph_bytes_per_buffer); + + auto updated_layer_info = layer_info; + updated_layer_info.nn_stream_config.periph_bytes_per_buffer = local_periph_bytes_per_buffer; + updated_layer_info.nn_stream_config.periph_buffers_per_frame = local_periph_buffers_per_frame; + + return updated_layer_info; +} + +// NOTE: in case of ddr where periph is aligned to PERIPH_BYTES_PER_BUFFER_DDR_ALIGNMENT_SIZE we cant force that +// periph_bytes_per_buffer * periph_buffers_per_frame will equal exactly hw_frame_size. +static bool is_logical_periph_bytes_per_buffer(const uint32_t periph_bytes_per_buffer, const size_t hw_frame_size, const bool is_ddr, + const uint32_t max_shmifo_size, const uint32_t desc_page_size, const uint32_t max_periph_bytes_value, + const uint16_t core_bytes_per_buffer) +{ + if (is_ddr) { + // In DDR there is no residue of descriptor - but has to divide with no remainder by core_bytes_per_buffer + // Calculated by DFC + return (periph_bytes_per_buffer < max_shmifo_size) && (periph_bytes_per_buffer <= max_periph_bytes_value) && + (0 == (core_bytes_per_buffer % periph_bytes_per_buffer)); } + return ((periph_bytes_per_buffer < (max_shmifo_size - desc_page_size)) && + (0 == (hw_frame_size % periph_bytes_per_buffer)) && (periph_bytes_per_buffer <= max_periph_bytes_value)); +} - *periph_bytes_per_buffer = local_periph_bytes_per_buffer; - *periph_buffers_per_frame = local_periph_buffers_per_frame; - return HAILO_SUCCESS; +static Expected> calculate_periph_requirements(const LayerInfo &layer_info, const uint32_t desc_page_size, + const bool is_periph_calculated_in_hailort, const uint32_t max_periph_bytes_value) +{ + // If extension for calculating periph values in hailort is false - copy values from core registers , otherwise + // If extesnion is true - calculate them according to shape and other layer information + if (!is_periph_calculated_in_hailort) { + return std::make_tuple(static_cast(layer_info.nn_stream_config.core_bytes_per_buffer), + static_cast(layer_info.nn_stream_config.core_buffers_per_frame)); + } + + if (HAILO_FORMAT_ORDER_HAILO_NMS == layer_info.format.order) { + CHECK_AS_EXPECTED(IS_FIT_IN_UINT16(layer_info.nms_info.bbox_size * layer_info.nms_info.burst_size), + HAILO_INVALID_HEF, "Invalid burst size"); + return std::make_tuple(static_cast(layer_info.nms_info.bbox_size * layer_info.nms_info.burst_size), + static_cast(1)); + } + + CHECK_AS_EXPECTED(IS_FIT_IN_UINT32(layer_info.hw_shape.width * layer_info.hw_shape.features * + layer_info.hw_shape.height * layer_info.hw_data_bytes), HAILO_INVALID_HEF, "Invalid core frame size"); + + const auto is_ddr = (LayerType::DDR == layer_info.type); + const uint32_t alignment = is_ddr ? PERIPH_BYTES_PER_BUFFER_DDR_ALIGNMENT_SIZE : PERIPH_BYTES_PER_BUFFER_ALIGNMENT_SIZE; + const auto row_size = static_cast(layer_info.hw_shape.width * layer_info.hw_shape.features * + layer_info.hw_data_bytes); + const auto core_frame_size = layer_info.hw_shape.height * row_size; + + // Currently takes the largest periph_bytes_per_buffer that is possible with shmifo size and desc page size + // TODO HRT-10961 : calculate optimal periph size + auto periph_bytes_per_buffer = HailoRTCommon::align_to(row_size, alignment); + while (!is_logical_periph_bytes_per_buffer(periph_bytes_per_buffer, core_frame_size, is_ddr, layer_info.max_shmifo_size, + desc_page_size, max_periph_bytes_value, layer_info.nn_stream_config.core_bytes_per_buffer) && (0 < periph_bytes_per_buffer)) { + periph_bytes_per_buffer -= alignment; + } + + CHECK_AS_EXPECTED(0 != periph_bytes_per_buffer, HAILO_INVALID_ARGUMENT, "Error, Could not find logical periph bytes per buffer value"); + + uint32_t periph_buffers_per_frame = (core_frame_size / periph_bytes_per_buffer); + // In ddr if we get a periph bytes per buffer os small that the periph buffers per frame cant fit in uint16 + // put uint16_t max - seeing as this value doesnt really affect anything and we should not fail in that case. + if (is_ddr && !IS_FIT_IN_UINT16(periph_buffers_per_frame)) { + LOGGER__DEBUG("periph buffers per frame in ddr too large for 16 bit register - putting uint16_t max"); + periph_buffers_per_frame = UINT16_MAX; + } + CHECK_AS_EXPECTED(IS_FIT_IN_UINT16(periph_buffers_per_frame), HAILO_INVALID_ARGUMENT); + + return std::make_tuple(static_cast(periph_bytes_per_buffer), static_cast(periph_buffers_per_frame)); } static Expected update_layer_info(const LayerInfo &original_layer_info, const CONTROL_PROTOCOL__host_buffer_info_t &buffer_info, - const CONTROL_PROTOCOL__hw_consts_t &hw_consts, bool should_optimize_credits) + const CONTROL_PROTOCOL__hw_consts_t &hw_consts, const ProtoHEFHwArch &hw_arch, const bool should_optimize_credits, + const bool is_periph_calculated_in_hailort) { LayerInfo local_layer_info = original_layer_info; - auto status = calculate_credit_params(hw_consts, buffer_info.desc_page_size, local_layer_info.direction, - should_optimize_credits, &local_layer_info.nn_stream_config.periph_bytes_per_buffer, - &local_layer_info.nn_stream_config.periph_buffers_per_frame); - CHECK_SUCCESS_AS_EXPECTED(status); - if (local_layer_info.max_shmifo_size == 0) { local_layer_info.max_shmifo_size = hw_consts.default_initial_credit_size; } - return local_layer_info; + // If Hw padding supported dont update periph registers because they were updated in get_hw_padding + // TODO HRT-11006 : currently check is_hw_padding_supported and the feature_padding_payload because in MIPI Input stream + // Even if is_hw_padding_supported is true we will not use hw padding. + auto max_periph_bytes_from_hef = HefConfigurator::max_periph_bytes_value(DeviceBase::hef_arch_to_device_arch(hw_arch)); + CHECK_EXPECTED(max_periph_bytes_from_hef); + const auto max_periph_bytes = MIN(max_periph_bytes_from_hef.value(), local_layer_info.max_shmifo_size); + + const bool hw_padding_supported = HefConfigurator::is_hw_padding_supported(local_layer_info, + max_periph_bytes) && (0 != original_layer_info.nn_stream_config.feature_padding_payload); + if (!hw_padding_supported) { + // Update periph values + const auto periph_requirements = calculate_periph_requirements(local_layer_info, buffer_info.desc_page_size, + is_periph_calculated_in_hailort, max_periph_bytes); + CHECK_EXPECTED(periph_requirements); + + // Calculate and update value of periph bytes per buffer and periph buffers per frame + local_layer_info.nn_stream_config.periph_bytes_per_buffer = std::get<0>(periph_requirements.value()); + local_layer_info.nn_stream_config.periph_buffers_per_frame = std::get<1>(periph_requirements.value()); + } + + auto updated_local_layer_info = calculate_credit_params(hw_consts, buffer_info.desc_page_size, should_optimize_credits, + local_layer_info); + CHECK_EXPECTED(updated_local_layer_info); + + return updated_local_layer_info; } static hailo_status fill_boundary_input_layer(ContextResources &context_resources, ResourcesManager &resources_manager, const LayerInfo layer_info, const CONTROL_PROTOCOL__hw_consts_t &hw_consts, - bool should_optimize_credits) + const ProtoHEFHwArch &hw_arch, bool should_optimize_credits) { - const auto transfer_size = (layer_info.nn_stream_config.periph_bytes_per_buffer * - layer_info.nn_stream_config.core_buffers_per_frame); + const auto transfer_size = LayerInfoUtils::get_layer_transfer_size(layer_info); auto vdma_channel = resources_manager.get_boundary_vdma_channel_by_stream_name(layer_info.name); CHECK_EXPECTED_AS_STATUS(vdma_channel); const auto buffer_info = vdma_channel.value()->get_boundary_buffer_info(transfer_size); - auto local_layer_info = update_layer_info(layer_info, buffer_info, hw_consts, should_optimize_credits); + const bool is_periph_calculated_in_hailort = resources_manager.get_supported_features().periph_calculation_in_hailort; + auto local_layer_info = update_layer_info(layer_info, buffer_info, hw_consts, hw_arch, should_optimize_credits, + is_periph_calculated_in_hailort); CHECK_EXPECTED_AS_STATUS(local_layer_info); const auto channel_id = vdma_channel.value()->get_channel_id(); - context_resources.add_edge_layer(local_layer_info.value(), channel_id, buffer_info); + auto status = context_resources.add_edge_layer(local_layer_info.value(), channel_id, buffer_info, + resources_manager.get_supported_features()); + CHECK_SUCCESS(status); LOGGER__DEBUG("Boundary input stream: {} h2d_channel: {}.", layer_info.stream_index, channel_id); return HAILO_SUCCESS; @@ -116,7 +203,7 @@ static hailo_status fill_boundary_input_layer(ContextResources &context_resource static hailo_status fill_inter_context_input_layer(ContextResources &context_resources, ResourcesManager &resources_manager, const LayerInfo &layer_info, const CONTROL_PROTOCOL__hw_consts_t &hw_consts, - bool should_optimize_credits) + const ProtoHEFHwArch &hw_arch, bool should_optimize_credits) { const auto channel_id = resources_manager.get_available_channel_id(to_layer_identifier(layer_info), HailoRTDriver::DmaDirection::H2D, layer_info.dma_engine_index); @@ -125,17 +212,19 @@ static hailo_status fill_inter_context_input_layer(ContextResources &context_res /* Get inter context buffer previously created */ const auto &connected_context = layer_info.connected_context_info; auto intermediate_buffer_key = std::make_pair(connected_context.context_index, connected_context.stream_index); - auto inter_context_buffer_exp = resources_manager.get_inter_context_buffer(intermediate_buffer_key); + auto inter_context_buffer_exp = resources_manager.get_intermediate_buffer(intermediate_buffer_key); CHECK_EXPECTED_AS_STATUS(inter_context_buffer_exp, "Failed to find inter context buffer for src context {}, src_stream_index {}", connected_context.context_index, connected_context.stream_index); auto &inter_context_buffer = inter_context_buffer_exp->get(); + const bool is_periph_calculated_in_hailort = resources_manager.get_supported_features().periph_calculation_in_hailort; auto local_layer_info = update_layer_info(layer_info, inter_context_buffer.get_host_buffer_info(), hw_consts, - should_optimize_credits); + hw_arch, should_optimize_credits, is_periph_calculated_in_hailort); CHECK_EXPECTED_AS_STATUS(local_layer_info); - context_resources.add_edge_layer(local_layer_info.value(), channel_id.value(), - inter_context_buffer.get_host_buffer_info()); + auto status = context_resources.add_edge_layer(local_layer_info.value(), channel_id.value(), + inter_context_buffer.get_host_buffer_info(), resources_manager.get_supported_features()); + CHECK_SUCCESS(status); LOGGER__DEBUG("Intermediate input stream {}, src_context:{}, dst_context: {}, h2d_channel {}.", layer_info.stream_index, layer_info.context_index, layer_info.connected_context_info.context_index, @@ -146,20 +235,23 @@ static hailo_status fill_inter_context_input_layer(ContextResources &context_res static hailo_status fill_boundary_output_layer(ContextResources &context_resources, ResourcesManager &resources_manager, const LayerInfo &layer_info, const CONTROL_PROTOCOL__hw_consts_t &hw_consts, - bool should_optimize_credits) + const ProtoHEFHwArch &hw_arch, bool should_optimize_credits) { - const auto transfer_size = (layer_info.nn_stream_config.periph_bytes_per_buffer * - layer_info.nn_stream_config.core_buffers_per_frame); + const auto transfer_size = LayerInfoUtils::get_layer_transfer_size(layer_info); auto vdma_channel = resources_manager.get_boundary_vdma_channel_by_stream_name(layer_info.name); CHECK_EXPECTED_AS_STATUS(vdma_channel); const auto buffer_info = vdma_channel.value()->get_boundary_buffer_info(transfer_size); - auto local_layer_info = update_layer_info(layer_info, buffer_info, hw_consts, should_optimize_credits); + const bool is_periph_calculated_in_hailort = resources_manager.get_supported_features().periph_calculation_in_hailort; + auto local_layer_info = update_layer_info(layer_info, buffer_info, hw_consts, hw_arch, should_optimize_credits, + is_periph_calculated_in_hailort); CHECK_EXPECTED_AS_STATUS(local_layer_info); const auto channel_id = vdma_channel.value()->get_channel_id(); - context_resources.add_edge_layer(local_layer_info.value(), channel_id, buffer_info); + auto status = context_resources.add_edge_layer(local_layer_info.value(), channel_id, buffer_info, + resources_manager.get_supported_features()); + CHECK_SUCCESS(status); LOGGER__DEBUG("Boundary output stream: {} d2h_channel: {}.", layer_info.stream_index, channel_id); return HAILO_SUCCESS; @@ -167,26 +259,31 @@ static hailo_status fill_boundary_output_layer(ContextResources &context_resourc static hailo_status fill_inter_context_output_layer(ContextResources &context_resources, ResourcesManager &resources_manager, const LayerInfo &layer_info, - const CONTROL_PROTOCOL__hw_consts_t &hw_consts, bool should_optimize_credits) + const CONTROL_PROTOCOL__hw_consts_t &hw_consts, const ProtoHEFHwArch &hw_arch, bool should_optimize_credits) { const auto channel_id = resources_manager.get_available_channel_id(to_layer_identifier(layer_info), HailoRTDriver::DmaDirection::D2H, layer_info.dma_engine_index); CHECK_EXPECTED_AS_STATUS(channel_id); - const auto frame_credits_in_bytes = (layer_info.nn_stream_config.periph_bytes_per_buffer * - layer_info.nn_stream_config.core_buffers_per_frame); + const auto frame_credits_in_bytes = LayerInfoUtils::get_layer_transfer_size(layer_info); + + auto network_batch_size = resources_manager.get_network_batch_size(layer_info.network_name); + CHECK_EXPECTED_AS_STATUS(network_batch_size); - auto inter_context_buffer_exp = resources_manager.create_inter_context_buffer(frame_credits_in_bytes, - layer_info.stream_index, layer_info.context_index, layer_info.network_name, channel_id.value()); + auto inter_context_buffer_exp = resources_manager.create_intermediate_buffer(frame_credits_in_bytes, + network_batch_size.value(), layer_info.stream_index, layer_info.context_index, channel_id.value(), + IntermediateBuffer::StreamingType::BURST); CHECK_EXPECTED_AS_STATUS(inter_context_buffer_exp); auto &inter_context_buffer = inter_context_buffer_exp->get(); + const bool is_periph_calculated_in_hailort = resources_manager.get_supported_features().periph_calculation_in_hailort; auto local_layer_info = update_layer_info(layer_info, inter_context_buffer.get_host_buffer_info(), hw_consts, - should_optimize_credits); + hw_arch, should_optimize_credits, is_periph_calculated_in_hailort); CHECK_EXPECTED_AS_STATUS(local_layer_info); - context_resources.add_edge_layer(local_layer_info.value(), channel_id.value(), - inter_context_buffer.get_host_buffer_info()); + auto status = context_resources.add_edge_layer(local_layer_info.value(), channel_id.value(), + inter_context_buffer.get_host_buffer_info(), resources_manager.get_supported_features()); + CHECK_SUCCESS(status); LOGGER__DEBUG("Inter-context output stream {}, src_context:{}, d2h_channel {}.", layer_info.stream_index, layer_info.context_index, channel_id.value()); @@ -195,78 +292,103 @@ static hailo_status fill_inter_context_output_layer(ContextResources &context_re static hailo_status fill_ddr_output_layer(ContextResources &context_resources, ResourcesManager &resources_manager, const LayerInfo &layer_info, - const CONTROL_PROTOCOL__hw_consts_t &hw_consts) + const CONTROL_PROTOCOL__hw_consts_t &hw_consts, const ProtoHEFHwArch &hw_arch) { CHECK(resources_manager.get_supported_features().padded_ddr_buffers, HAILO_INVALID_HEF, "Failed opening non-compatible HEF that uses the following deprecated features: host-managed DDR buffers." "Please re-compile the HEF using a newer Dataflow Compiler version (v3.11.0 or newer)"); - // Allocate resources and prepare ddr_info - - DdrChannelsInfo ddr_pair_info = {}; - ddr_pair_info.h2d_stream_index = layer_info.connected_context_info.stream_index; - ddr_pair_info.d2h_stream_index = layer_info.stream_index; - ddr_pair_info.network_index = layer_info.network_index; - // It is assumed that output channels are parsed before input channels. + // It is assumed that output channels are parsed before input channels. // Allocate vdma channel index for both edges - const auto h2d_layer_identifier = std::make_tuple(LayerType::DDR, layer_info.name, ddr_pair_info.h2d_stream_index); + const auto h2d_stream_index = layer_info.connected_context_info.stream_index; + const auto h2d_layer_identifier = std::make_tuple(LayerType::DDR, HAILO_H2D_STREAM, + layer_info.name, h2d_stream_index); const auto h2d_channel_id = resources_manager.get_available_channel_id(h2d_layer_identifier, HailoRTDriver::DmaDirection::H2D, layer_info.connected_context_info.dma_engine_index); CHECK_EXPECTED_AS_STATUS(h2d_channel_id); - ddr_pair_info.h2d_channel_id = h2d_channel_id.value(); - const auto d2h_layer_identifier = std::make_tuple(LayerType::DDR, layer_info.name, ddr_pair_info.d2h_stream_index); + const auto d2h_stream_index = layer_info.stream_index; + const auto d2h_layer_identifier = std::make_tuple(LayerType::DDR, HAILO_D2H_STREAM, + layer_info.name, d2h_stream_index); const auto d2h_channel_id = resources_manager.get_available_channel_id(d2h_layer_identifier, HailoRTDriver::DmaDirection::D2H, layer_info.dma_engine_index); CHECK_EXPECTED_AS_STATUS(d2h_channel_id); - ddr_pair_info.d2h_channel_id = d2h_channel_id.value(); - ddr_pair_info.row_size = layer_info.nn_stream_config.core_bytes_per_buffer; - ddr_pair_info.min_buffered_rows = layer_info.ddr_info.min_buffered_rows; - ddr_pair_info.total_buffers_per_frame = layer_info.ddr_info.total_buffers_per_frame; + // In DDR layer there is no residue - so can ignore descriptor size + const auto IGNORE_DESCRIPTOR_SIZE = 0; + // Send layer info with updated shmifo size + auto layer_info_updated_shmifo = layer_info; + if (layer_info_updated_shmifo.max_shmifo_size == 0) { + layer_info_updated_shmifo.max_shmifo_size = hw_consts.default_initial_credit_size; + } + + auto max_periph_bytes = HefConfigurator::max_periph_bytes_value(DeviceBase::hef_arch_to_device_arch(hw_arch)); + CHECK_EXPECTED_AS_STATUS(max_periph_bytes, "Error calculating max periph bytes per buffer"); + const auto periph_values = calculate_periph_requirements(layer_info_updated_shmifo, IGNORE_DESCRIPTOR_SIZE, + resources_manager.get_supported_features().periph_calculation_in_hailort, max_periph_bytes.value()); + CHECK_EXPECTED_AS_STATUS(periph_values); + + const auto row_size = std::get<0>(periph_values.value()); + const auto min_buffered_rows = layer_info.ddr_info.min_buffered_rows; - // Create the ddr buffer - auto ddr_channels_pair = context_resources.create_ddr_channels_pair(ddr_pair_info); - CHECK_EXPECTED_AS_STATUS(ddr_channels_pair); + // Allocate the ddr buffer + auto ddr_buffer = resources_manager.create_intermediate_buffer(row_size, min_buffered_rows, + d2h_stream_index, layer_info.context_index, d2h_channel_id.value(), + IntermediateBuffer::StreamingType::CIRCULAR_CONTINUOS); + CHECK_EXPECTED_AS_STATUS(ddr_buffer); + + DdrChannelsInfo ddr_pair_info{}; + ddr_pair_info.h2d_stream_index = h2d_stream_index; + ddr_pair_info.d2h_stream_index = d2h_stream_index; + ddr_pair_info.network_index = layer_info.network_index; + ddr_pair_info.h2d_channel_id = h2d_channel_id.value(); + ddr_pair_info.d2h_channel_id = d2h_channel_id.value(); + ddr_pair_info.row_size = row_size; + ddr_pair_info.min_buffered_rows = min_buffered_rows; + ddr_pair_info.total_buffers_per_frame = layer_info.ddr_info.total_buffers_per_frame; + ddr_pair_info.host_buffer_info = ddr_buffer->get().get_host_buffer_info(); + context_resources.add_ddr_channels_info(ddr_pair_info); // On ddr layers, we assume the periph credit size is aligned to the size of descriptor, so we don't want to // optimize the credits. const bool should_optimize_credits = false; - auto local_layer_info = update_layer_info(layer_info, ddr_channels_pair->get().get_host_buffer_info(), hw_consts, - should_optimize_credits); + const bool is_periph_calculated_in_hailort = resources_manager.get_supported_features().periph_calculation_in_hailort; + auto local_layer_info = update_layer_info(layer_info, ddr_buffer->get().get_host_buffer_info(), hw_consts, + hw_arch, should_optimize_credits, is_periph_calculated_in_hailort); CHECK_EXPECTED_AS_STATUS(local_layer_info); - context_resources.add_edge_layer(local_layer_info.value(), ddr_pair_info.d2h_channel_id, - ddr_channels_pair->get().get_host_buffer_info()); + auto status = context_resources.add_edge_layer(local_layer_info.value(), ddr_pair_info.d2h_channel_id, + ddr_buffer->get().get_host_buffer_info(), resources_manager.get_supported_features()); + CHECK_SUCCESS(status); return HAILO_SUCCESS; } -static hailo_status fill_ddr_input_layer(ContextResources &context_resources, - const LayerInfo &layer_info, const CONTROL_PROTOCOL__hw_consts_t &hw_consts) +static hailo_status fill_ddr_input_layer(ContextResources &context_resources, ResourcesManager &resources_manager, + const LayerInfo &layer_info, const CONTROL_PROTOCOL__hw_consts_t &hw_consts, const ProtoHEFHwArch &hw_arch) { auto connected_stream_index = layer_info.connected_context_info.stream_index; - auto ddr_channels_pair = context_resources.get_ddr_channels_pair(connected_stream_index); - CHECK(ddr_channels_pair, HAILO_INVALID_HEF, "Matching DDR layer as not found for context {} src stream {}", + auto ddr_info = context_resources.get_ddr_channels_info(connected_stream_index); + CHECK_EXPECTED_AS_STATUS(ddr_info, "Matching DDR layer as not found for context {} src stream {}", layer_info.context_index, connected_stream_index); - - const auto ddr_info = ddr_channels_pair->get().info(); LOGGER__DEBUG("DDR layer: input stream_index: {}, output stream_index: {}, h2d_channel {}, d2h_channel: {}.", - ddr_info.h2d_stream_index, ddr_info.d2h_stream_index, ddr_info.h2d_channel_id, ddr_info.d2h_channel_id); + ddr_info->h2d_stream_index, ddr_info->d2h_stream_index, ddr_info->h2d_channel_id, ddr_info->d2h_channel_id); - CHECK(layer_info.stream_index == ddr_info.h2d_stream_index, HAILO_INVALID_HEF, "DDR channel pair mismatch in h2d channel"); - CHECK(layer_info.connected_context_info.stream_index == ddr_info.d2h_stream_index, HAILO_INVALID_HEF, "DDR channel pair mismatch in d2h channel"); - CHECK(layer_info.network_index == ddr_info.network_index, HAILO_INVALID_HEF, "DDR channel pair mismatch network_index"); + CHECK(layer_info.stream_index == ddr_info->h2d_stream_index, HAILO_INVALID_HEF, "DDR channel pair mismatch in h2d channel"); + CHECK(layer_info.connected_context_info.stream_index == ddr_info->d2h_stream_index, HAILO_INVALID_HEF, "DDR channel pair mismatch in d2h channel"); + CHECK(layer_info.network_index == ddr_info->network_index, HAILO_INVALID_HEF, "DDR channel pair mismatch network_index"); // On ddr layers, we assume the periph credit size is aligned to the size of descriptor, so we don't want to // optimize the credits. const bool should_optimize_credits = false; - auto local_layer_info = update_layer_info(layer_info, ddr_channels_pair->get().get_host_buffer_info(), hw_consts, - should_optimize_credits); + const bool is_periph_calculated_in_hailort = resources_manager.get_supported_features().periph_calculation_in_hailort; + auto local_layer_info = update_layer_info(layer_info, ddr_info->host_buffer_info, hw_consts, + hw_arch, should_optimize_credits, is_periph_calculated_in_hailort); CHECK_EXPECTED_AS_STATUS(local_layer_info); - context_resources.add_edge_layer(local_layer_info.value(), ddr_channels_pair->get().info().h2d_channel_id, - ddr_channels_pair->get().get_host_buffer_info()); + auto status = context_resources.add_edge_layer(local_layer_info.value(), ddr_info->h2d_channel_id, + ddr_info->host_buffer_info, resources_manager.get_supported_features()); + CHECK_SUCCESS(status); return HAILO_SUCCESS; } @@ -275,11 +397,10 @@ static hailo_status add_ddr_buffers_info(std::vector find_dummy_stream(const LayerInfo &layer_info, const ContextResources &context_resources) +// TODO HRT-10073: change to supported features list +static bool is_hailo15_device_type(const hailo_device_architecture_t dev_arch) { - const auto other_direction = (HAILO_H2D_STREAM == layer_info.direction) ? HAILO_D2H_STREAM : HAILO_H2D_STREAM; - const auto other_direction_edge_layers = context_resources.get_edge_layers(other_direction); - CHECK_AS_EXPECTED(!other_direction_edge_layers.empty(), HAILO_INTERNAL_FAILURE, "Couldn't find dummy stream"); - return Expected(other_direction_edge_layers.front().layer_info.stream_index); + // Compare with HAILO15 device arch + return (HAILO_ARCH_HAILO15 == dev_arch); } -static hailo_status add_change_vdma_to_stream_mapping( +static Expected find_dummy_stream(const LayerInfo &layer_info, const ContextResources &context_resources, + const bool is_null_shmifo_supported) +{ + if (is_null_shmifo_supported) { + static const uint8_t DUMMY_STREAM_INDEX = 31; + return Expected(DUMMY_STREAM_INDEX); + } else { + const auto other_direction = (HAILO_H2D_STREAM == layer_info.direction) ? HAILO_D2H_STREAM : HAILO_H2D_STREAM; + const auto other_direction_edge_layers = context_resources.get_edge_layers(other_direction); + CHECK_AS_EXPECTED(!other_direction_edge_layers.empty(), HAILO_INTERNAL_FAILURE, "Couldn't find dummy stream"); + return Expected(other_direction_edge_layers.front().layer_info.stream_index); + } +} + +static hailo_status add_change_vdma_to_stream_mapping(const ProtoHEFHwArch &hw_arch, const CoreOpMetadata &core_op_metadata, const ResourcesManager &resources_manager, ContextResources &context_resources, uint8_t context_index, std::vector &processed_configuration_actions) @@ -557,7 +688,8 @@ static hailo_status add_change_vdma_to_stream_mapping( const bool is_dummy_stream = layer_info.context_index != context_index; uint8_t stream_index = layer_info.stream_index; if (is_dummy_stream) { - auto dummy_stream_index = find_dummy_stream(layer_info, context_resources); + auto dummy_stream_index = find_dummy_stream(layer_info, context_resources, + is_hailo15_device_type(DeviceBase::hef_arch_to_device_arch(hw_arch))); CHECK_EXPECTED_AS_STATUS(dummy_stream_index); stream_index = *dummy_stream_index; } @@ -603,9 +735,9 @@ static hailo_status push_edge_layer_activation_actions( for (const auto &edge_layer : context_resources.get_edge_layers(LayerType::DDR, HAILO_H2D_STREAM)) { const auto d2h_stream_index = edge_layer.layer_info.connected_context_info.stream_index; - auto pair = context_resources.get_ddr_channels_pair(d2h_stream_index); - CHECK_EXPECTED_AS_STATUS(pair); - const auto d2h_channel_id = pair->get().info().d2h_channel_id; + auto ddr_channels_info = context_resources.get_ddr_channels_info(d2h_stream_index); + CHECK_EXPECTED_AS_STATUS(ddr_channels_info); + const auto d2h_channel_id = ddr_channels_info->d2h_channel_id; auto activate_action = ActivateDdrInputChannelAction::create(edge_layer.channel_id, edge_layer.layer_info.stream_index, edge_layer.layer_info.nn_stream_config, edge_layer.buffer_info, @@ -633,7 +765,8 @@ static hailo_status push_edge_layer_activation_actions( return HAILO_SUCCESS; } -static hailo_status proccess_trigger_new_data_input_action(const ContextSwitchConfigActionPtr &configuration_action, +static hailo_status proccess_trigger_new_data_input_action(const ProtoHEFHwArch &hw_arch, + const ContextSwitchConfigActionPtr &configuration_action, uint32_t trigger_new_data_from_input_group_start, uint32_t trigger_new_data_from_input_group_end, const uint32_t &action_index, @@ -648,7 +781,7 @@ static hailo_status proccess_trigger_new_data_input_action(const ContextSwitchCo CHECK_SUCCESS(status); if (!is_single_context) { - status = add_change_vdma_to_stream_mapping(core_op_metadata, resources_manager, + status = add_change_vdma_to_stream_mapping(hw_arch, core_op_metadata, resources_manager, context_resources, context_index, processed_configuration_actions); CHECK_SUCCESS(status); } @@ -734,8 +867,8 @@ static hailo_status add_config_channel_activation_actions(std::vector &configuration_actions, - const CoreOpMetadata &core_op_metadata, +static hailo_status handle_edge_layer_activation_actions(const ProtoHEFHwArch &hw_arch, + std::vector &configuration_actions, const CoreOpMetadata &core_op_metadata, const ResourcesManager &resources_manager, ContextResources &context_resources, uint8_t context_index, bool is_single_context) { @@ -751,7 +884,7 @@ static hailo_status handle_edge_layer_activation_actions(std::vectorget_type()) { - auto status = proccess_trigger_new_data_input_action(configuration_action, + auto status = proccess_trigger_new_data_input_action(hw_arch, configuration_action, trigger_new_data_from_input_group_start, trigger_new_data_from_input_group_end, action_index, core_op_metadata, resources_manager, context_resources, context_index, processed_configuration_actions, is_single_context); CHECK_SUCCESS(status); @@ -809,13 +942,6 @@ static hailo_status handle_repeated_actions(std::vector &actions) { @@ -854,17 +980,17 @@ static hailo_status fill_context_recipes_for_multi_context(const ProtoHEFHwArch hailo_status status = HAILO_UNINITIALIZED; // Add edge layers mapping - status = parse_and_fill_edge_layers_mapping(context_resources, context_metadata, resources_manager); + status = parse_and_fill_edge_layers_mapping(context_resources, context_metadata, resources_manager, hw_arch); CHECK_SUCCESS(status); // Parse context std::vector actions = context_metadata.get_actions(); - const auto support_pre_fetch = is_hailo15_device_type(hw_arch); + const auto support_pre_fetch = is_hailo15_device_type(DeviceBase::hef_arch_to_device_arch(hw_arch)); status = add_fetch_config_actions(actions, context_resources.get_config_buffers(), support_pre_fetch); CHECK_SUCCESS(status); - status = handle_edge_layer_activation_actions(actions, core_op_metadata, resources_manager, + status = handle_edge_layer_activation_actions(hw_arch, actions, core_op_metadata, resources_manager, context_resources, context_index, is_single_context); CHECK_SUCCESS(status); @@ -899,7 +1025,7 @@ static hailo_status create_boundary_channels(ResourcesManager &resources_manager static hailo_status fill_activation_config_recepies_for_multi_context( ContextResources &context_resources, ResourcesManager &resources_manager, - std::shared_ptr core_op_metadata) + std::shared_ptr core_op_metadata, const ProtoHEFHwArch &hw_arch) { auto hw_consts = Control::get_hw_consts(resources_manager.get_device()); CHECK_EXPECTED_AS_STATUS(hw_consts); @@ -908,19 +1034,16 @@ static hailo_status fill_activation_config_recepies_for_multi_context( for (const auto &layer_info : core_op_metadata->get_output_layer_infos()){ auto status = fill_boundary_output_layer(context_resources, resources_manager, layer_info, *hw_consts, - should_optimize_credits); + hw_arch, should_optimize_credits); CHECK_SUCCESS(status); } for (const auto &layer_info : core_op_metadata->get_input_layer_infos()) { auto status = fill_boundary_input_layer(context_resources, resources_manager, layer_info, *hw_consts, - should_optimize_credits); + hw_arch, should_optimize_credits); CHECK_SUCCESS(status); } - auto status = context_resources.validate_edge_layers(); - CHECK_SUCCESS(status); - std::vector actions; for (const auto &edge_layer : context_resources.get_edge_layers(LayerType::BOUNDARY)) { auto action = edge_layer.layer_info.direction == HAILO_H2D_STREAM ? @@ -933,6 +1056,38 @@ static hailo_status fill_activation_config_recepies_for_multi_context( return write_action_list(context_resources, context_resources.builder(), actions); } +static Expected create_switch_lcu_batch_action(const ContextSwitchConfigActionPtr action, + ContextResources &context_resources) +{ + uint8_t cluster_index = 0; + uint8_t lcu_index = 0; + uint8_t network_index = 0; + uint32_t kernel_done_count = 0; + + CHECK_AS_EXPECTED((ContextSwitchConfigAction::Type::EnableLcuDefault == action->get_type()) || + (ContextSwitchConfigAction::Type::EnableLcuNonDefault == action->get_type()), HAILO_INVALID_ARGUMENT, + "Invalid action type - must be enable lcu (default or non default), Received type {}", action->get_type()); + + const auto params_buffer = action->serialize_params(context_resources); + CHECK_EXPECTED(params_buffer); + + if (ContextSwitchConfigAction::Type::EnableLcuDefault == action->get_type()) { + const auto params = reinterpret_cast(params_buffer.value().data()); + cluster_index = CONTEXT_SWITCH_DEFS__PACKED_LCU_ID_CLUSTER_INDEX_READ(params->packed_lcu_id); + lcu_index = CONTEXT_SWITCH_DEFS__PACKED_LCU_ID_LCU_INDEX_READ(params->packed_lcu_id); + network_index = params->network_index; + kernel_done_count = CONTEXT_SWITCH_DEFS__ENABLE_LCU_DEFAULT_KERNEL_COUNT; + } else { + const auto params = reinterpret_cast(params_buffer.value().data()); + cluster_index = CONTEXT_SWITCH_DEFS__PACKED_LCU_ID_CLUSTER_INDEX_READ(params->packed_lcu_id); + lcu_index = CONTEXT_SWITCH_DEFS__PACKED_LCU_ID_LCU_INDEX_READ(params->packed_lcu_id); + network_index = params->network_index; + kernel_done_count = params->kernel_done_count; + } + + return SwitchLcuBatchAction::create(cluster_index, lcu_index, network_index, kernel_done_count); +} + static hailo_status fill_batch_switching_context_config_recepies_for_multi_context( ContextResources &context_resources, const CoreOpMetadata &core_op_metadata) { @@ -943,14 +1098,19 @@ static hailo_status fill_batch_switching_context_config_recepies_for_multi_conte CHECK_EXPECTED_AS_STATUS(reset_ddr_action); actions.emplace_back(reset_ddr_action.release()); - // We need to re-enable all the lcus of the first context since some of their config regs are batch dependent. - // => We'll filter out all of the "enable lcu" actions from the preliminary context - static const std::set BATCH_SWITCHING_ACTIONS = { + // Find all the enabled lcus from the preliminary context in order to create coresponding switch lcu batch actions to run + // In the batch switch context + static const std::set ENABLE_LCU_ACTIONS = { ContextSwitchConfigAction::Type::EnableLcuDefault, ContextSwitchConfigAction::Type::EnableLcuNonDefault }; - const auto batch_switch_actions = core_op_metadata.preliminary_context().get_actions_of_type(BATCH_SWITCHING_ACTIONS); - actions.insert(actions.end(), batch_switch_actions.begin(), batch_switch_actions.end()); + + const auto batch_switch_actions = core_op_metadata.preliminary_context().get_actions_of_type(ENABLE_LCU_ACTIONS); + for (const auto &action : batch_switch_actions) { + auto switch_lcu_batch_action = create_switch_lcu_batch_action(action, context_resources); + CHECK_EXPECTED_AS_STATUS(switch_lcu_batch_action); + actions.insert(actions.end(), switch_lcu_batch_action.release()); + } auto status = handle_repeated_actions(actions); CHECK_SUCCESS(status); @@ -969,19 +1129,19 @@ static hailo_status fill_preliminary_config_recepies_for_multi_context(const Pro // Add edge layers mapping (only preliminary_run_asap networks have edge layers in the preliminary context) assert(PRELIMINARY_CONTEXT_INDEX < core_op_metadata->dynamic_contexts().size()); auto status = parse_and_fill_edge_layers_mapping(context_resources, - core_op_metadata->dynamic_contexts()[PRELIMINARY_CONTEXT_INDEX], resources_manager); + core_op_metadata->dynamic_contexts()[PRELIMINARY_CONTEXT_INDEX], resources_manager, hw_arch); CHECK_SUCCESS(status); } // Parse preliminary config std::vector actions = preliminary_context.get_actions(); - const auto support_pre_fetch = is_hailo15_device_type(hw_arch); + const auto support_pre_fetch = is_hailo15_device_type(DeviceBase::hef_arch_to_device_arch(hw_arch)); auto status = add_fetch_config_actions(actions, context_resources.get_config_buffers(), support_pre_fetch); CHECK_SUCCESS(status); if (resources_manager.get_supported_features().preliminary_run_asap) { - status = handle_edge_layer_activation_actions(actions, *core_op_metadata, resources_manager, + status = handle_edge_layer_activation_actions(hw_arch, actions, *core_op_metadata, resources_manager, context_resources, PRELIMINARY_CONTEXT_INDEX, is_single_context); CHECK_SUCCESS(status); } @@ -1026,7 +1186,7 @@ Expected> ResourcesManagerBuilder::build(uint8 auto activation_context = resources_manager->add_new_context(CONTROL_PROTOCOL__CONTEXT_SWITCH_CONTEXT_TYPE_ACTIVATION); CHECK_EXPECTED(activation_context); status = fill_activation_config_recepies_for_multi_context(activation_context.value().get(), - resources_manager.value(), core_op_metadata); + resources_manager.value(), core_op_metadata, hw_arch); CHECK_SUCCESS_AS_EXPECTED(status); auto batch_switching_context = resources_manager->add_new_context(CONTROL_PROTOCOL__CONTEXT_SWITCH_CONTEXT_TYPE_BATCH_SWITCHING); diff --git a/hailort/libhailort/src/device_common/control.cpp b/hailort/libhailort/src/device_common/control.cpp index 19946b9..9c72ebb 100644 --- a/hailort/libhailort/src/device_common/control.cpp +++ b/hailort/libhailort/src/device_common/control.cpp @@ -197,22 +197,9 @@ hailo_status control__parse_core_identify_results(CONTROL_PROTOCOL__core_identif return HAILO_SUCCESS; } -hailo_status Control::validate_arch_supported(Device &device, const std::vector &supported_archs) -{ - auto dev_arch = device.get_architecture(); - CHECK_EXPECTED_AS_STATUS(dev_arch); - for (const auto &arch : supported_archs) { - if (*dev_arch == arch) { - return HAILO_SUCCESS; - } - } - LOGGER__ERROR("Control is not supported for this device architecture - {}", HailoRTCommon::get_device_arch_str(*dev_arch)); - return HAILO_NOT_SUPPORTED; -} - hailo_status Control::parse_and_validate_response(uint8_t *message, uint32_t message_size, CONTROL_PROTOCOL__response_header_t **header, CONTROL_PROTOCOL__payload_t **payload, - CONTROL_PROTOCOL__request_t *request) + CONTROL_PROTOCOL__request_t *request, Device &device) { hailo_status status = HAILO_UNINITIALIZED; HAILO_COMMON_STATUS_t common_status = HAILO_COMMON_STATUS__UNINITIALIZED; @@ -251,12 +238,29 @@ hailo_status Control::parse_and_validate_response(uint8_t *message, uint32_t mes (FIRMWARE_STATUS_t)fw_status.minor_status, common_status); } + if ((CONTROL_PROTOCOL_STATUS_CONTROL_UNSUPPORTED == fw_status.minor_status) || + (CONTROL_PROTOCOL_STATUS_CONTROL_UNSUPPORTED == fw_status.major_status)) { + auto device_arch = device.get_architecture(); + auto dev_arch_str = (device_arch) ? HailoRTCommon::get_device_arch_str(*device_arch) : "Unable to parse arch"; + LOGGER__ERROR("Opcode {} is not supported on the device." \ + " This error usually occurs when the control is not supported for the device arch - ({}), or not compiled to the FW", + CONTROL_PROTOCOL__get_textual_opcode((CONTROL_PROTOCOL__OPCODE_t)BYTE_ORDER__ntohl(request->header.common_header.opcode)), + dev_arch_str); + } + + if ((CONTROL_PROTOCOL_STATUS_UNSUPPORTED_DEVICE == fw_status.minor_status) || + (CONTROL_PROTOCOL_STATUS_UNSUPPORTED_DEVICE == fw_status.major_status)) { + LOGGER__ERROR("Opcode {} is not supported on the current board.", + CONTROL_PROTOCOL__get_textual_opcode((CONTROL_PROTOCOL__OPCODE_t)BYTE_ORDER__ntohl(request->header.common_header.opcode))); + } + if ((HAILO_CONTROL_STATUS_UNSUPPORTED_OPCODE == fw_status.minor_status) || (HAILO_CONTROL_STATUS_UNSUPPORTED_OPCODE == fw_status.major_status)) { status = HAILO_UNSUPPORTED_OPCODE; LOGGER__ERROR("Opcode {} is not supported", CONTROL_PROTOCOL__get_textual_opcode((CONTROL_PROTOCOL__OPCODE_t)BYTE_ORDER__ntohl(request->header.common_header.opcode))); } + goto exit; } @@ -301,7 +305,7 @@ Expected Control::identify(Device &device) /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); CHECK_SUCCESS_AS_EXPECTED(status); identify_response = (CONTROL_PROTOCOL_identify_response_t *)(payload->parameters); @@ -336,7 +340,7 @@ hailo_status Control::core_identify(Device &device, hailo_core_information_t *co /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -359,14 +363,10 @@ hailo_status Control::set_fw_logger(Device &device, hailo_fw_logger_level_t leve CONTROL_PROTOCOL__request_t request = {}; size_t request_size = 0; - /* Validate arch */ - auto status = Control::validate_arch_supported(device); - CHECK_SUCCESS(status); - auto common_status = CONTROL_PROTOCOL__pack_set_fw_logger_request(&request, &request_size, device.get_control_sequence(), level, static_cast(interface_mask)); - status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; + auto status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; CHECK_SUCCESS(status); uint8_t response_buffer[RESPONSE_MAX_BUFFER_SIZE] = {}; @@ -378,7 +378,7 @@ hailo_status Control::set_fw_logger(Device &device, hailo_fw_logger_level_t leve CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); CHECK_SUCCESS(status); return HAILO_SUCCESS; @@ -389,13 +389,9 @@ hailo_status Control::set_clock_freq(Device &device, uint32_t clock_freq) CONTROL_PROTOCOL__request_t request = {}; size_t request_size = 0; - /* Validate arch */ - auto status = Control::validate_arch_supported(device); - CHECK_SUCCESS(status); - auto common_status = CONTROL_PROTOCOL__pack_set_clock_freq_request(&request, &request_size, device.get_control_sequence(), clock_freq); - status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; + auto status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; CHECK_SUCCESS(status); uint8_t response_buffer[RESPONSE_MAX_BUFFER_SIZE] = {}; @@ -407,7 +403,7 @@ hailo_status Control::set_clock_freq(Device &device, uint32_t clock_freq) CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); CHECK_SUCCESS(status); return HAILO_SUCCESS; @@ -418,13 +414,9 @@ hailo_status Control::set_throttling_state(Device &device, bool should_activate) CONTROL_PROTOCOL__request_t request = {}; size_t request_size = 0; - /* Validate arch */ - auto status = Control::validate_arch_supported(device); - CHECK_SUCCESS(status); - auto common_status = CONTROL_PROTOCOL__pack_set_throttling_state_request(&request, &request_size, device.get_control_sequence(), should_activate); - status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; + auto status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; CHECK_SUCCESS(status); uint8_t response_buffer[RESPONSE_MAX_BUFFER_SIZE] = {}; @@ -436,7 +428,7 @@ hailo_status Control::set_throttling_state(Device &device, bool should_activate) CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); CHECK_SUCCESS(status); return HAILO_SUCCESS; @@ -454,10 +446,6 @@ Expected Control::get_throttling_state(Device &device) CONTROL_PROTOCOL__payload_t *payload = NULL; CONTROL_PROTOCOL__get_throttling_state_response_t *get_throttling_state_response = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - CHECK_SUCCESS_AS_EXPECTED(status); - common_status = CONTROL_PROTOCOL__pack_get_throttling_state_request(&request, &request_size, device.get_control_sequence()); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; @@ -467,7 +455,7 @@ Expected Control::get_throttling_state(Device &device) CHECK_SUCCESS_AS_EXPECTED(status); /* Parse response */ - status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, &request); + status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, &request, device); CHECK_SUCCESS_AS_EXPECTED(status); get_throttling_state_response = (CONTROL_PROTOCOL__get_throttling_state_response_t *)(payload->parameters); @@ -479,13 +467,9 @@ hailo_status Control::set_overcurrent_state(Device &device, bool should_activate CONTROL_PROTOCOL__request_t request = {}; size_t request_size = 0; - /* Validate arch */ - auto status = Control::validate_arch_supported(device); - CHECK_SUCCESS(status); - auto common_status = CONTROL_PROTOCOL__pack_set_overcurrent_state_request(&request, &request_size, device.get_control_sequence(), should_activate); - status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; + auto status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; CHECK_SUCCESS(status); uint8_t response_buffer[RESPONSE_MAX_BUFFER_SIZE] = {}; @@ -496,7 +480,7 @@ hailo_status Control::set_overcurrent_state(Device &device, bool should_activate /* Parse response */ CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, &request); + status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, &request, device); CHECK_SUCCESS(status); return HAILO_SUCCESS; @@ -514,10 +498,6 @@ Expected Control::get_overcurrent_state(Device &device) CONTROL_PROTOCOL__payload_t *payload = NULL; CONTROL_PROTOCOL__get_overcurrent_state_response_t *get_overcurrent_state_response = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - CHECK_SUCCESS_AS_EXPECTED(status); - common_status = CONTROL_PROTOCOL__pack_get_overcurrent_state_request(&request, &request_size, device.get_control_sequence()); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; @@ -527,7 +507,7 @@ Expected Control::get_overcurrent_state(Device &device) CHECK_SUCCESS_AS_EXPECTED(status); /* Parse response */ - status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, &request); + status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, &request, device); CHECK_SUCCESS_AS_EXPECTED(status); get_overcurrent_state_response = (CONTROL_PROTOCOL__get_overcurrent_state_response_t *)(payload->parameters); @@ -538,6 +518,7 @@ Expected Control::get_hw_consts(Device &device) { size_t request_size = 0; CONTROL_PROTOCOL__request_t request = {}; + auto common_status = CONTROL_PROTOCOL__pack_get_hw_consts_request(&request, &request_size, device.get_control_sequence()); auto status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; CHECK_SUCCESS_AS_EXPECTED(status); @@ -549,7 +530,8 @@ Expected Control::get_hw_consts(Device &device) CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, &request); + status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, &request, + device); CHECK_SUCCESS_AS_EXPECTED(status); const auto &response = *reinterpret_cast(payload->parameters); @@ -587,7 +569,7 @@ hailo_status Control::write_memory_chunk(Device &device, uint32_t address, const /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -663,7 +645,7 @@ hailo_status Control::read_memory_chunk(Device &device, uint32_t address, uint8_ /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -726,12 +708,6 @@ hailo_status Control::open_stream(Device &device, uint8_t dataflow_manager_id, b CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_open_stream_request(&request, &request_size, device.get_control_sequence(), dataflow_manager_id, is_input); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; @@ -746,7 +722,7 @@ hailo_status Control::open_stream(Device &device, uint8_t dataflow_manager_id, b /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -767,12 +743,6 @@ hailo_status Control::close_stream(Device &device, uint8_t dataflow_manager_id, CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_close_stream_request(&request, &request_size, device.get_control_sequence(), dataflow_manager_id, is_input); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; @@ -787,7 +757,7 @@ hailo_status Control::close_stream(Device &device, uint8_t dataflow_manager_id, /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -834,12 +804,6 @@ hailo_status Control::config_stream_udp_input(Device &device, CONTROL_PROTOCOL__ /* Validate arguments */ CHECK_ARG_NOT_NULL(params); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_config_stream_udp_input_request(&request, &request_size, device.get_control_sequence(), params); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; @@ -854,7 +818,7 @@ hailo_status Control::config_stream_udp_input(Device &device, CONTROL_PROTOCOL__ /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -891,12 +855,6 @@ hailo_status Control::config_stream_udp_output(Device &device, CONTROL_PROTOCOL_ /* Validate arguments */ CHECK_ARG_NOT_NULL(params); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_config_stream_udp_output_request(&request, &request_size, device.get_control_sequence(), params); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; @@ -911,7 +869,7 @@ hailo_status Control::config_stream_udp_output(Device &device, CONTROL_PROTOCOL_ /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -948,12 +906,6 @@ hailo_status Control::config_stream_mipi_input(Device &device, CONTROL_PROTOCOL_ /* Validate arguments */ CHECK_ARG_NOT_NULL(params); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_config_stream_mipi_input_request(&request, &request_size, device.get_control_sequence(), params); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; @@ -968,7 +920,7 @@ hailo_status Control::config_stream_mipi_input(Device &device, CONTROL_PROTOCOL_ /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -1005,12 +957,6 @@ hailo_status Control::config_stream_mipi_output(Device &device, CONTROL_PROTOCOL /* Validate arguments */ CHECK_ARG_NOT_NULL(params); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_config_stream_mipi_output_request(&request, &request_size, device.get_control_sequence(), params); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; @@ -1025,7 +971,7 @@ hailo_status Control::config_stream_mipi_output(Device &device, CONTROL_PROTOCOL /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -1062,12 +1008,6 @@ hailo_status Control::config_stream_pcie_input(Device &device, CONTROL_PROTOCOL_ /* Validate arguments */ CHECK_ARG_NOT_NULL(params); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_config_stream_pcie_input_request(&request, &request_size, device.get_control_sequence(), params); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; @@ -1082,7 +1022,7 @@ hailo_status Control::config_stream_pcie_input(Device &device, CONTROL_PROTOCOL_ /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -1119,12 +1059,6 @@ hailo_status Control::config_stream_pcie_output(Device &device, CONTROL_PROTOCOL /* Validate arguments */ CHECK_ARG_NOT_NULL(params); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_config_stream_pcie_output_request(&request, &request_size, device.get_control_sequence(), params); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; @@ -1139,7 +1073,7 @@ hailo_status Control::config_stream_pcie_output(Device &device, CONTROL_PROTOCOL /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -1175,12 +1109,6 @@ hailo_status Control::power_measurement(Device &device, CONTROL_PROTOCOL__dvm_op CONTROL_PROTOCOL__payload_t *payload = NULL; CONTROL_PROTOCOL__power_measurement_response_t *response = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - /* Validate arguments */ CHECK_ARG_NOT_NULL(measurement); @@ -1198,7 +1126,7 @@ hailo_status Control::power_measurement(Device &device, CONTROL_PROTOCOL__dvm_op /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -1230,12 +1158,6 @@ hailo_status Control::set_power_measurement(Device &device, hailo_measurement_bu CONTROL_PROTOCOL__payload_t *payload = NULL; CONTROL_PROTOCOL__set_power_measurement_response_t *response = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - CHECK(CONTROL_PROTOCOL__MAX_NUMBER_OF_POWER_MEASUREMETS > buffer_index, HAILO_INVALID_ARGUMENT, "Invalid power measurement index {}", buffer_index); @@ -1253,7 +1175,7 @@ hailo_status Control::set_power_measurement(Device &device, hailo_measurement_bu /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -1283,12 +1205,6 @@ hailo_status Control::get_power_measurement(Device &device, hailo_measurement_bu CONTROL_PROTOCOL__payload_t *payload = NULL; CONTROL_PROTOCOL__get_power_measurement_response_t *get_power_response = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - /* Validate arguments */ CHECK(CONTROL_PROTOCOL__MAX_NUMBER_OF_POWER_MEASUREMETS > buffer_index, HAILO_INVALID_ARGUMENT, "Invalid power measurement index {}", buffer_index); @@ -1305,7 +1221,7 @@ hailo_status Control::get_power_measurement(Device &device, hailo_measurement_bu } /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -1339,12 +1255,6 @@ hailo_status Control::start_power_measurement(Device &device, CONTROL_PROTOCOL__payload_t *payload = NULL; uint32_t delay_milliseconds = 0; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - delay_milliseconds = POWER_MEASUREMENT_DELAY_MS(sampling_period, averaging_factor); // There is no logical way that measurement delay can be 0 - because sampling_period and averaging_factor cant be 0 // Hence if it is 0 - it means it was 0.xx and we want to round up to 1 in that case @@ -1366,7 +1276,7 @@ hailo_status Control::start_power_measurement(Device &device, /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -1387,12 +1297,6 @@ hailo_status Control::stop_power_measurement(Device &device) CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_stop_power_measurement_request(&request, &request_size, device.get_control_sequence()); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -1406,7 +1310,7 @@ hailo_status Control::stop_power_measurement(Device &device) /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -1432,12 +1336,6 @@ hailo_status Control::i2c_write(Device &device, const hailo_i2c_slave_config_t * CHECK_ARG_NOT_NULL(slave_config); CHECK_ARG_NOT_NULL(data); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - /* Pack request */ common_status = CONTROL_PROTOCOL__pack_i2c_write_request(&request, &request_size, device.get_control_sequence(), register_address, static_cast(slave_config->endianness), @@ -1455,7 +1353,7 @@ hailo_status Control::i2c_write(Device &device, const hailo_i2c_slave_config_t * /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -1483,12 +1381,6 @@ hailo_status Control::i2c_read(Device &device, const hailo_i2c_slave_config_t *s CHECK_ARG_NOT_NULL(slave_config); CHECK_ARG_NOT_NULL(data); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - /* Pack request */ common_status = CONTROL_PROTOCOL__pack_i2c_read_request(&request, &request_size, device.get_control_sequence(), register_address, static_cast(slave_config->endianness), @@ -1507,7 +1399,7 @@ hailo_status Control::i2c_read(Device &device, const hailo_i2c_slave_config_t *s /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -1546,12 +1438,6 @@ hailo_status Control::config_core_top(Device &device, CONTROL_PROTOCOL__config_c /* Validate arguments */ CHECK_ARG_NOT_NULL(params); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_config_core_top_request(&request, &request_size, device.get_control_sequence(), config_type, params); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -1565,7 +1451,7 @@ hailo_status Control::config_core_top(Device &device, CONTROL_PROTOCOL__config_c /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -1586,12 +1472,6 @@ hailo_status Control::phy_operation(Device &device, CONTROL_PROTOCOL__phy_operat CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_phy_operation_request(&request, &request_size, device.get_control_sequence(), operation_type); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -1605,7 +1485,7 @@ hailo_status Control::phy_operation(Device &device, CONTROL_PROTOCOL__phy_operat /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -1630,12 +1510,6 @@ hailo_status Control::examine_user_config(Device &device, hailo_fw_user_config_i /* Validate arguments */ CHECK_ARG_NOT_NULL(info); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_examine_user_config(&request, &request_size, device.get_control_sequence()); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -1649,7 +1523,7 @@ hailo_status Control::examine_user_config(Device &device, hailo_fw_user_config_i /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -1678,10 +1552,6 @@ hailo_status Control::read_user_config_chunk(Device &device, uint32_t read_offse CONTROL_PROTOCOL__payload_t *payload = NULL; CONTROL_PROTOCOL__read_user_config_response_t *response = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - CHECK_SUCCESS(status); - common_status = CONTROL_PROTOCOL__pack_read_user_config(&request, &request_size, device.get_control_sequence(), read_offset, read_length); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; @@ -1693,7 +1563,7 @@ hailo_status Control::read_user_config_chunk(Device &device, uint32_t read_offse /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); CHECK_SUCCESS(status); response = (CONTROL_PROTOCOL__read_user_config_response_t *)(payload->parameters); @@ -1713,10 +1583,6 @@ hailo_status Control::read_user_config(Device &device, uint8_t *buffer, uint32_t /* Validate arguments */ CHECK_ARG_NOT_NULL(buffer); - /* Validate arch */ - status = Control::validate_arch_supported(device); - CHECK_SUCCESS(status); - status = examine_user_config(device, &user_config_info); CHECK_SUCCESS(status); @@ -1747,10 +1613,6 @@ hailo_status Control::write_user_config_chunk(Device &device, uint32_t offset, c CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - CHECK_SUCCESS(status); - common_status = CONTROL_PROTOCOL__pack_write_user_config_request(&request, &request_size, device.get_control_sequence(), offset, data + offset, chunk_size); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; @@ -1762,7 +1624,7 @@ hailo_status Control::write_user_config_chunk(Device &device, uint32_t offset, c /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); CHECK_SUCCESS(status); return HAILO_SUCCESS; @@ -1777,10 +1639,6 @@ hailo_status Control::write_user_config(Device &device, const uint8_t *data, uin /* Validate arguments */ CHECK_ARG_NOT_NULL(data); - /* Validate arch */ - status = Control::validate_arch_supported(device); - CHECK_SUCCESS(status); - while (offset < data_length) { chunk_size = MIN(WRITE_CHUNK_SIZE, (data_length - offset)); status = write_user_config_chunk(device, offset, data, chunk_size); @@ -1802,12 +1660,6 @@ hailo_status Control::erase_user_config(Device &device) CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_erase_user_config_request(&request, &request_size, device.get_control_sequence()); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -1821,7 +1673,7 @@ hailo_status Control::erase_user_config(Device &device) /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -1849,10 +1701,6 @@ hailo_status Control::read_board_config(Device &device, uint8_t *buffer, uint32_ /* Validate arguments */ CHECK_ARG_NOT_NULL(buffer); - /* Validate arch */ - status = Control::validate_arch_supported(device); - CHECK_SUCCESS(status); - CHECK(buffer_length >= BOARD_CONFIG_SIZE, HAILO_INSUFFICIENT_BUFFER, "read buffer is too small. provided buffer size: {} bytes, board config size: {} bytes", buffer_length, BOARD_CONFIG_SIZE); @@ -1870,7 +1718,7 @@ hailo_status Control::read_board_config(Device &device, uint8_t *buffer, uint32_ /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); CHECK_SUCCESS(status); response = (CONTROL_PROTOCOL__read_board_config_response_t *)(payload->parameters); actual_read_data_length = BYTE_ORDER__ntohl(response->data_length); @@ -1896,10 +1744,6 @@ hailo_status Control::write_board_config(Device &device, const uint8_t *data, ui /* Validate arguments */ CHECK_ARG_NOT_NULL(data); - /* Validate arch */ - status = Control::validate_arch_supported(device); - CHECK_SUCCESS(status); - CHECK(BOARD_CONFIG_SIZE >= data_length, HAILO_INVALID_OPERATION, "Invalid size of board config. data_length={}, max_size={}" , data_length, BOARD_CONFIG_SIZE); @@ -1914,7 +1758,7 @@ hailo_status Control::write_board_config(Device &device, const uint8_t *data, ui /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); CHECK_SUCCESS(status); return HAILO_SUCCESS; @@ -1934,12 +1778,6 @@ hailo_status Control::write_second_stage_to_internal_memory(Device &device, uint /* Validate arguments */ CHECK_ARG_NOT_NULL(data); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__write_second_stage_to_internal_memory_request(&request, &request_size, device.get_control_sequence(), offset, data, data_length); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; @@ -1954,7 +1792,7 @@ hailo_status Control::write_second_stage_to_internal_memory(Device &device, uint /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -1979,12 +1817,6 @@ hailo_status Control::copy_second_stage_to_flash(Device &device, MD5_SUM_t *expe /* Validate arguments */ CHECK_ARG_NOT_NULL(expected_md5); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__copy_second_stage_to_flash_request(&request, &request_size, device.get_control_sequence(), expected_md5, second_stage_size); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -1998,7 +1830,7 @@ hailo_status Control::copy_second_stage_to_flash(Device &device, MD5_SUM_t *expe /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -2019,12 +1851,6 @@ hailo_status Control::start_firmware_update(Device &device) CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_start_firmware_update_request(&request, &request_size, device.get_control_sequence()); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -2038,7 +1864,7 @@ hailo_status Control::start_firmware_update(Device &device) /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -2059,12 +1885,6 @@ hailo_status Control::finish_firmware_update(Device &device) CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_finish_firmware_update_request(&request, &request_size, device.get_control_sequence()); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -2078,7 +1898,7 @@ hailo_status Control::finish_firmware_update(Device &device) /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -2102,12 +1922,6 @@ hailo_status Control::write_firmware_update(Device &device, uint32_t offset, con /* Validate arguments */ CHECK_ARG_NOT_NULL(data); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__write_firmware_update_request(&request, &request_size, device.get_control_sequence(), offset, data, data_length); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; @@ -2122,7 +1936,7 @@ hailo_status Control::write_firmware_update(Device &device, uint32_t offset, con /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -2146,12 +1960,6 @@ hailo_status Control::validate_firmware_update(Device &device, MD5_SUM_t *expect /* Validate arguments */ CHECK_ARG_NOT_NULL(expected_md5); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_validate_firmware_update_request(&request, &request_size, device.get_control_sequence(), expected_md5, firmware_size); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; @@ -2166,7 +1974,7 @@ hailo_status Control::validate_firmware_update(Device &device, MD5_SUM_t *expect /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -2191,12 +1999,6 @@ hailo_status Control::latency_measurement_read(Device &device, uint32_t *inbound /* Validate arguments */ CHECK_ARG_NOT_NULL(inbound_to_outbound_latency_nsec); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_latency_measurement_read_request(&request, &request_size, device.get_control_sequence()); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -2210,7 +2012,7 @@ hailo_status Control::latency_measurement_read(Device &device, uint32_t *inbound /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -2236,12 +2038,6 @@ hailo_status Control::latency_measurement_config(Device &device, uint8_t latency CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_latency_measurement_config_request(&request, &request_size, device.get_control_sequence(), latency_measurement_en, inbound_start_buffer_number, outbound_stop_buffer_number, inbound_stream_index, outbound_stream_index); @@ -2257,7 +2053,7 @@ hailo_status Control::latency_measurement_config(Device &device, uint8_t latency /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -2286,12 +2082,6 @@ hailo_status Control::sensor_store_config(Device &device, uint32_t is_first, uin CHECK_ARG_NOT_NULL(data); CHECK_ARG_NOT_NULL(config_name); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_sensor_store_config_request(&request, &request_size, device.get_control_sequence(), is_first, section_index, start_offset, reset_data_size, sensor_type, total_data_size, data, data_length, config_height, config_width, config_fps, config_name_length, config_name); @@ -2308,7 +2098,7 @@ hailo_status Control::sensor_store_config(Device &device, uint32_t is_first, uin /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -2328,10 +2118,6 @@ hailo_status Control::sensor_set_i2c_bus_index(Device &device, uint32_t sensor_t CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - CHECK_SUCCESS(status); - status = CONTROL_PROTOCOL__pack_sensor_set_i2c_bus_index_request(&request, &request_size, device.get_control_sequence(), sensor_type, bus_index); CHECK_SUCCESS(status); @@ -2339,7 +2125,7 @@ hailo_status Control::sensor_set_i2c_bus_index(Device &device, uint32_t sensor_t CHECK_SUCCESS(status); /* Parse response */ - status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, &request); + status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, &request, device); CHECK_SUCCESS(status); return HAILO_SUCCESS; @@ -2356,12 +2142,6 @@ hailo_status Control::sensor_load_and_start_config(Device &device, uint32_t sect CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_sensor_load_and_start_config_request(&request, &request_size, device.get_control_sequence(), section_index); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -2375,7 +2155,7 @@ hailo_status Control::sensor_load_and_start_config(Device &device, uint32_t sect /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -2396,12 +2176,6 @@ hailo_status Control::sensor_reset(Device &device, uint32_t section_index) CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_sensor_reset_request(&request, &request_size, device.get_control_sequence(), section_index); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -2415,7 +2189,7 @@ hailo_status Control::sensor_reset(Device &device, uint32_t section_index) /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -2437,12 +2211,6 @@ hailo_status Control::sensor_set_generic_i2c_slave(Device &device, uint16_t slav CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_sensor_set_generic_i2c_slave_request(&request, &request_size, device.get_control_sequence(), slave_address, register_address_size, bus_index, should_hold_bus, endianness); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -2456,7 +2224,7 @@ hailo_status Control::sensor_set_generic_i2c_slave(Device &device, uint16_t slav /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -2484,12 +2252,6 @@ hailo_status Control::sensor_get_config(Device &device, uint32_t section_index, /* Validate arguments */ CHECK_ARG_NOT_NULL(data); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_sensor_get_config_request(&request, &request_size, device.get_control_sequence(), section_index, offset, data_length); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; @@ -2504,7 +2266,7 @@ hailo_status Control::sensor_get_config(Device &device, uint32_t section_index, /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -2539,12 +2301,6 @@ hailo_status Control::sensor_get_sections_info(Device &device, uint8_t *data) /* Validate arguments */ CHECK_ARG_NOT_NULL(data); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_sensor_get_sections_info_request(&request, &request_size, device.get_control_sequence()); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; @@ -2559,7 +2315,7 @@ hailo_status Control::sensor_get_sections_info(Device &device, uint8_t *data) /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -2604,7 +2360,7 @@ hailo_status Control::context_switch_set_network_group_header(Device &device, /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -2640,7 +2396,7 @@ hailo_status Control::context_switch_set_context_info_chunk(Device &device, /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { /* In case of max memory error, add LOGGER ERROR, and set indicative error to the user */ CHECK((CONTEXT_SWITCH_TASK_STATUS_ADD_TRIGGER_FUNCTION_REACHED_FORBIDDEN_MEMORY_SPACE != header->status.major_status), @@ -2679,12 +2435,6 @@ hailo_status Control::idle_time_get_measurement(Device &device, uint64_t *measur /* Validate arguments */ CHECK_ARG_NOT_NULL(measurement); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_idle_time_get_measuremment_request(&request, &request_size, device.get_control_sequence()); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -2700,7 +2450,7 @@ hailo_status Control::idle_time_get_measurement(Device &device, uint64_t *measur /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { LOGGER__ERROR("failed validating idle_time_get_measurement control response with status {}", status); goto exit; @@ -2732,12 +2482,6 @@ hailo_status Control::idle_time_set_measurement(Device &device, uint8_t measurem CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_idle_time_set_measuremment_request(&request, &request_size, device.get_control_sequence(), measurement_enable); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -2752,7 +2496,7 @@ hailo_status Control::idle_time_set_measurement(Device &device, uint8_t measurem /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { LOGGER__ERROR("failed idle_time_set_measurement control with status {}", status); goto exit; @@ -2767,13 +2511,9 @@ hailo_status Control::set_pause_frames(Device &device, uint8_t rx_pause_frames_e CONTROL_PROTOCOL__request_t request = {}; size_t request_size = 0; - /* Validate arch */ - auto status = Control::validate_arch_supported(device); - CHECK_SUCCESS(status); - HAILO_COMMON_STATUS_t common_status = CONTROL_PROTOCOL__pack_set_pause_frames_request(&request, &request_size, device.get_control_sequence(), rx_pause_frames_enable); - status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; + auto status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; CHECK_SUCCESS(status); uint8_t response_buffer[RESPONSE_MAX_BUFFER_SIZE] = {}; @@ -2785,7 +2525,7 @@ hailo_status Control::set_pause_frames(Device &device, uint8_t rx_pause_frames_e CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); CHECK_SUCCESS(status); return HAILO_SUCCESS; @@ -2826,7 +2566,7 @@ hailo_status Control::download_context_action_list_chunk(Device &device, uint32_ /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -2905,7 +2645,8 @@ hailo_status Control::download_context_action_list(Device &device, uint32_t netw hailo_status Control::change_context_switch_status(Device &device, CONTROL_PROTOCOL__CONTEXT_SWITCH_STATUS_t state_machine_status, - uint8_t network_group_index, uint16_t dynamic_batch_size, bool keep_nn_config_during_reset) + uint8_t network_group_index, uint16_t dynamic_batch_size, uint16_t batch_count, + bool keep_nn_config_during_reset) { hailo_status status = HAILO_UNINITIALIZED; HAILO_COMMON_STATUS_t common_status = HAILO_COMMON_STATUS__UNINITIALIZED; @@ -2918,7 +2659,7 @@ hailo_status Control::change_context_switch_status(Device &device, common_status = CONTROL_PROTOCOL__pack_change_context_switch_status_request(&request, &request_size, device.get_control_sequence(), state_machine_status, network_group_index, dynamic_batch_size, - keep_nn_config_during_reset); + batch_count, keep_nn_config_during_reset); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { goto exit; @@ -2931,7 +2672,7 @@ hailo_status Control::change_context_switch_status(Device &device, /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -2941,19 +2682,20 @@ exit: return status; } -hailo_status Control::enable_core_op(Device &device, uint8_t network_group_index, uint16_t dynamic_batch_size) +hailo_status Control::enable_core_op(Device &device, uint8_t network_group_index, uint16_t dynamic_batch_size, + uint16_t batch_count) { - static const auto REMOVE_NN_CONFIG_DURING_RESET = false; return Control::change_context_switch_status(device, CONTROL_PROTOCOL__CONTEXT_SWITCH_STATUS_ENABLED, - network_group_index, dynamic_batch_size, REMOVE_NN_CONFIG_DURING_RESET); + network_group_index, dynamic_batch_size, batch_count); } hailo_status Control::reset_context_switch_state_machine(Device &device, bool keep_nn_config_during_reset) { static const auto IGNORE_NETWORK_GROUP_INDEX = 0; static const auto IGNORE_DYNAMIC_BATCH_SIZE = 0; + static const auto DEFAULT_BATCH_COUNT = 0; return Control::change_context_switch_status(device, CONTROL_PROTOCOL__CONTEXT_SWITCH_STATUS_RESET, - IGNORE_NETWORK_GROUP_INDEX, IGNORE_DYNAMIC_BATCH_SIZE, keep_nn_config_during_reset); + IGNORE_NETWORK_GROUP_INDEX, IGNORE_DYNAMIC_BATCH_SIZE, DEFAULT_BATCH_COUNT, keep_nn_config_during_reset); } hailo_status Control::wd_enable(Device &device, uint8_t cpu_id, bool should_enable) @@ -2967,12 +2709,6 @@ hailo_status Control::wd_enable(Device &device, uint8_t cpu_id, bool should_enab CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_wd_enable(&request, &request_size, device.get_control_sequence(), cpu_id, should_enable); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -2987,7 +2723,7 @@ hailo_status Control::wd_enable(Device &device, uint8_t cpu_id, bool should_enab /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { LOGGER__ERROR("failed wd_enable control with status {}", status); goto exit; @@ -3008,12 +2744,6 @@ hailo_status Control::wd_config(Device &device, uint8_t cpu_id, uint32_t wd_cycl CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_wd_config(&request, &request_size, device.get_control_sequence(), cpu_id, wd_cycles, wd_mode); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -3028,7 +2758,7 @@ hailo_status Control::wd_config(Device &device, uint8_t cpu_id, uint32_t wd_cycl /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { LOGGER__ERROR("failed wd_config control with status {}", status); goto exit; @@ -3053,12 +2783,6 @@ hailo_status Control::previous_system_state(Device &device, uint8_t cpu_id, CONT CHECK_ARG_NOT_NULL(system); - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_previous_system_state(&request, &request_size, device.get_control_sequence(), cpu_id); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -3073,7 +2797,7 @@ hailo_status Control::previous_system_state(Device &device, uint8_t cpu_id, CONT /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { LOGGER__ERROR("failed previous_system_state control with status {}", status); goto exit; @@ -3115,7 +2839,7 @@ hailo_status Control::set_dataflow_interrupt(Device &device, uint8_t interrupt_t /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -3152,7 +2876,7 @@ hailo_status Control::d2h_notification_manager_set_host_info(Device &device, uin /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -3186,7 +2910,7 @@ hailo_status Control::d2h_notification_manager_send_host_info_notification(Devic /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -3223,7 +2947,7 @@ hailo_status Control::clear_configured_apps(Device &device) } /* Parse response */ - status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, &request); + status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, &request, device); if (HAILO_SUCCESS != status) { LOGGER__ERROR("failed clear_configured_apps control with status {}", status); goto exit; @@ -3246,12 +2970,6 @@ hailo_status Control::get_chip_temperature(Device &device, hailo_chip_temperatur CONTROL_PROTOCOL__payload_t *payload = NULL; CONTROL_PROTOCOL__get_chip_temperature_response_t* temps = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_get_chip_temperature_request(&request, &request_size, device.get_control_sequence()); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -3265,7 +2983,7 @@ hailo_status Control::get_chip_temperature(Device &device, hailo_chip_temperatur /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -3291,12 +3009,6 @@ hailo_status Control::enable_debugging(Device &device, bool is_rma) CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - status = Control::validate_arch_supported(device); - if (HAILO_SUCCESS != status) { - goto exit; - } - common_status = CONTROL_PROTOCOL__pack_enable_debugging_request(&request, &request_size, device.get_control_sequence(), is_rma); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; if (HAILO_SUCCESS != status) { @@ -3310,7 +3022,7 @@ hailo_status Control::enable_debugging(Device &device, bool is_rma) /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -3331,12 +3043,6 @@ Expected Control:: CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arguments */ - - /* Validate arch */ - status = Control::validate_arch_supported(device); - CHECK_SUCCESS_AS_EXPECTED(status); - common_status = CONTROL_PROTOCOL__pack_get_extended_device_information_request(&request, &request_size, device.get_control_sequence()); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; CHECK_SUCCESS_AS_EXPECTED(status); @@ -3345,7 +3051,7 @@ Expected Control:: CHECK_SUCCESS_AS_EXPECTED(status); /* Parse response */ - status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, &request); + status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, &request, device); CHECK_SUCCESS_AS_EXPECTED(status); return std::move(*(CONTROL_PROTOCOL__get_extended_device_information_response_t *)(payload->parameters)); @@ -3383,12 +3089,6 @@ Expected Control::get_health_information(Device &device) CONTROL_PROTOCOL__payload_t *payload = NULL; CONTROL_PROTOCOL__get_health_information_response_t *get_health_information_response = NULL; - /* Validate arguments */ - - /* Validate arch */ - status = Control::validate_arch_supported(device); - CHECK_SUCCESS_AS_EXPECTED(status); - common_status = CONTROL_PROTOCOL__pack_get_health_information_request(&request, &request_size, device.get_control_sequence()); status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; CHECK_SUCCESS_AS_EXPECTED(status); @@ -3397,7 +3097,8 @@ Expected Control::get_health_information(Device &device) CHECK_SUCCESS_AS_EXPECTED(status); /* Parse response */ - status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, &request); + status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, &request, + device); CHECK_SUCCESS_AS_EXPECTED(status); get_health_information_response = (CONTROL_PROTOCOL__get_health_information_response_t *)(payload->parameters); @@ -3428,7 +3129,7 @@ hailo_status Control::config_context_switch_breakpoint(Device &device, uint8_t b /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -3463,7 +3164,7 @@ hailo_status Control::get_context_switch_breakpoint_status(Device &device, uint8 /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -3503,7 +3204,7 @@ hailo_status Control::get_context_switch_main_header(Device &device, CONTROL_PRO /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); if (HAILO_SUCCESS != status) { goto exit; } @@ -3539,7 +3240,7 @@ hailo_status Control::config_context_switch_timestamp(Device &device, uint16_t b /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); CHECK_SUCCESS(status); return HAILO_SUCCESS; @@ -3598,14 +3299,10 @@ hailo_status Control::run_bist_test(Device &device, bool is_top_test, uint32_t t CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - auto status = Control::validate_arch_supported(device); - CHECK_SUCCESS(status); - auto common_status = CONTROL_PROTOCOL__pack_run_bist_test_request( &request, &request_size, device.get_control_sequence(), is_top_test, top_bypass_bitmap, cluster_index, cluster_bypass_bitmap_0, cluster_bypass_bitmap_1); - status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; + auto status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; CHECK_SUCCESS(status); status = device.fw_interact((uint8_t*)(&request), request_size, (uint8_t*)&response_buffer, &response_size); @@ -3613,7 +3310,7 @@ hailo_status Control::run_bist_test(Device &device, bool is_top_test, uint32_t t /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); CHECK_SUCCESS(status); return HAILO_SUCCESS; @@ -3628,13 +3325,9 @@ hailo_status Control::set_sleep_state(Device &device, hailo_sleep_state_t sleep_ CONTROL_PROTOCOL__response_header_t *header = NULL; CONTROL_PROTOCOL__payload_t *payload = NULL; - /* Validate arch */ - auto status = Control::validate_arch_supported(device); - CHECK_SUCCESS(status); - auto common_status = CONTROL_PROTOCOL__pack_set_sleep_state_request( &request, &request_size, device.get_control_sequence(), static_cast(sleep_state)); - status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; + auto status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; CHECK_SUCCESS(status); status = device.fw_interact((uint8_t*)(&request), request_size, (uint8_t*)&response_buffer, &response_size); @@ -3642,14 +3335,14 @@ hailo_status Control::set_sleep_state(Device &device, hailo_sleep_state_t sleep_ /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); CHECK_SUCCESS(status); return HAILO_SUCCESS; } hailo_status Control::change_hw_infer_status(Device &device, CONTROL_PROTOCOL__hw_infer_state_t state, - uint8_t network_group_index, uint16_t dynamic_batch_size, + uint8_t network_group_index, uint16_t dynamic_batch_size, uint16_t batch_count, CONTROL_PROTOCOL__hw_infer_channels_info_t *channels_info, CONTROL_PROTOCOL__hw_only_infer_results_t *results) { CONTROL_PROTOCOL__request_t request = {}; @@ -3664,7 +3357,7 @@ hailo_status Control::change_hw_infer_status(Device &device, CONTROL_PROTOCOL__h auto common_status = CONTROL_PROTOCOL__pack_change_hw_infer_status_request( &request, &request_size, device.get_control_sequence(), static_cast(state), - network_group_index, dynamic_batch_size, channels_info); + network_group_index, dynamic_batch_size, batch_count, channels_info); auto status = (HAILO_COMMON_STATUS__SUCCESS == common_status) ? HAILO_SUCCESS : HAILO_INTERNAL_FAILURE; CHECK_SUCCESS(status); @@ -3673,7 +3366,7 @@ hailo_status Control::change_hw_infer_status(Device &device, CONTROL_PROTOCOL__h /* Parse response */ status = parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, - &request); + &request, device); CHECK_SUCCESS(status); change_hw_infer_status_response = (CONTROL_PROTOCOL__change_hw_infer_status_response_t *)(payload->parameters); @@ -3684,20 +3377,21 @@ hailo_status Control::change_hw_infer_status(Device &device, CONTROL_PROTOCOL__h } hailo_status Control::start_hw_only_infer(Device &device, uint8_t network_group_index, uint16_t dynamic_batch_size, - CONTROL_PROTOCOL__hw_infer_channels_info_t *channels_info) + uint16_t batch_count, CONTROL_PROTOCOL__hw_infer_channels_info_t *channels_info) { CONTROL_PROTOCOL__hw_only_infer_results_t results = {}; return Control::change_hw_infer_status(device, CONTROL_PROTOCOL__HW_INFER_STATE_START, - network_group_index, dynamic_batch_size, channels_info ,&results); + network_group_index, dynamic_batch_size, batch_count, channels_info ,&results); } hailo_status Control::stop_hw_only_infer(Device &device, CONTROL_PROTOCOL__hw_only_infer_results_t *results) { const uint8_t DEFAULT_NETWORK_GROUP = 0; const uint16_t DEFAULT_DYNAMIC_BATCH_SIZE = 1; + const uint16_t DEFAULT_BATCH_COUNT = 1; CONTROL_PROTOCOL__hw_infer_channels_info_t channels_info_default = {}; return Control::change_hw_infer_status(device, CONTROL_PROTOCOL__HW_INFER_STATE_STOP, - DEFAULT_NETWORK_GROUP, DEFAULT_DYNAMIC_BATCH_SIZE, &channels_info_default, results); + DEFAULT_NETWORK_GROUP, DEFAULT_DYNAMIC_BATCH_SIZE, DEFAULT_BATCH_COUNT, &channels_info_default, results); } } /* namespace hailort */ diff --git a/hailort/libhailort/src/device_common/control.hpp b/hailort/libhailort/src/device_common/control.hpp index d79ad85..aa65dd5 100644 --- a/hailort/libhailort/src/device_common/control.hpp +++ b/hailort/libhailort/src/device_common/control.hpp @@ -42,7 +42,7 @@ public: static hailo_status parse_and_validate_response(uint8_t *message, uint32_t message_size, CONTROL_PROTOCOL__response_header_t **header, CONTROL_PROTOCOL__payload_t **payload, - CONTROL_PROTOCOL__request_t *request); + CONTROL_PROTOCOL__request_t *request, Device &device); /** * Receive information about the device. @@ -288,11 +288,14 @@ public: * Enable core-op * * @param[in] device - The Hailo device. - * @param[in] core_op_index - core_op index + * @param[in] core_op_index - core_op index + * @param[in] dynamic_batch_size - actual batch size + * @param[in] batch_count - number of batches user wish to run on hailo chip * * @return Upon success, returns @a HAILO_SUCCESS. Otherwise, returns an @a static hailo_status error. */ - static hailo_status enable_core_op(Device &device, uint8_t core_op_index, uint16_t dynamic_batch_size); + static hailo_status enable_core_op(Device &device, uint8_t core_op_index, uint16_t dynamic_batch_size, + uint16_t batch_count); /** * reset context switch state machine * @@ -373,10 +376,10 @@ public: static Expected get_hw_consts(Device &device); static hailo_status set_sleep_state(Device &device, hailo_sleep_state_t sleep_state); static hailo_status change_hw_infer_status(Device &device, CONTROL_PROTOCOL__hw_infer_state_t state, - uint8_t network_group_index, uint16_t dynamic_batch_size, + uint8_t network_group_index, uint16_t dynamic_batch_size, uint16_t batch_count, CONTROL_PROTOCOL__hw_infer_channels_info_t *channels_info, CONTROL_PROTOCOL__hw_only_infer_results_t *results); static hailo_status start_hw_only_infer(Device &device, uint8_t network_group_index, uint16_t dynamic_batch_size, - CONTROL_PROTOCOL__hw_infer_channels_info_t *channels_info); + uint16_t batch_count, CONTROL_PROTOCOL__hw_infer_channels_info_t *channels_info); static hailo_status stop_hw_only_infer(Device &device, CONTROL_PROTOCOL__hw_only_infer_results_t *results); // TODO: needed? static hailo_status power_measurement(Device &device, CONTROL_PROTOCOL__dvm_options_t dvm, @@ -403,11 +406,11 @@ private: bool *is_action_list_end, uint32_t *batch_counter); static hailo_status context_switch_set_context_info_chunk(Device &device, const CONTROL_PROTOCOL__context_switch_context_info_single_control_t &context_info); - static hailo_status change_context_switch_status(Device &device, + static hailo_status change_context_switch_status(Device &device, CONTROL_PROTOCOL__CONTEXT_SWITCH_STATUS_t state_machine_status, - uint8_t network_group_index, uint16_t dynamic_batch_size, bool keep_nn_config_during_reset); + uint8_t network_group_index, uint16_t dynamic_batch_size, uint16_t batch_count, + bool keep_nn_config_during_reset = false); static Expected get_extended_device_info_response(Device &device); - static hailo_status validate_arch_supported(Device &device, const std::vector &supported_archs = { HAILO_ARCH_HAILO8, HAILO_ARCH_HAILO8L }); }; } /* namespace hailort */ diff --git a/hailort/libhailort/src/device_common/control_protocol.cpp b/hailort/libhailort/src/device_common/control_protocol.cpp index 92412bb..1c5b38c 100644 --- a/hailort/libhailort/src/device_common/control_protocol.cpp +++ b/hailort/libhailort/src/device_common/control_protocol.cpp @@ -57,7 +57,7 @@ const char *CONTROL_PROTOCOL__get_textual_opcode(CONTROL_PROTOCOL__OPCODE_t opco return CONTROL_PROTOCOL__textual_format[opcode]; } -#define CHANGE_HW_INFER_REQUEST_PARAMETER_COUNT (4) +#define CHANGE_HW_INFER_REQUEST_PARAMETER_COUNT (5) /* Functions declarations */ HAILO_COMMON_STATUS_t control_protocol__parse_message(uint8_t *message, @@ -1810,10 +1810,11 @@ exit: return status; } +#define CONTEXT_SWITCH_SWITCH_STATUS_REQUEST_PARAMS (5) HAILO_COMMON_STATUS_t CONTROL_PROTOCOL__pack_change_context_switch_status_request( CONTROL_PROTOCOL__request_t *request, size_t *request_size, uint32_t sequence, CONTROL_PROTOCOL__CONTEXT_SWITCH_STATUS_t state_machine_status, uint8_t application_index, - uint16_t dynamic_batch_size, bool keep_nn_config_during_reset) + uint16_t dynamic_batch_size, uint16_t batch_count, bool keep_nn_config_during_reset) { HAILO_COMMON_STATUS_t status = HAILO_COMMON_STATUS__UNINITIALIZED; size_t local_request_size = 0; @@ -1826,7 +1827,8 @@ HAILO_COMMON_STATUS_t CONTROL_PROTOCOL__pack_change_context_switch_status_reques /* Header */ local_request_size = CONTROL_PROTOCOL__REQUEST_BASE_SIZE + sizeof(CONTROL_PROTOCOL__change_context_switch_status_request_t); - control_protocol__pack_request_header(request, sequence, HAILO_CONTROL_OPCODE_CHANGE_CONTEXT_SWITCH_STATUS, 4); + control_protocol__pack_request_header(request, sequence, + HAILO_CONTROL_OPCODE_CHANGE_CONTEXT_SWITCH_STATUS, CONTEXT_SWITCH_SWITCH_STATUS_REQUEST_PARAMS); /* state_machine_status */ request->parameters.change_context_switch_status_request.state_machine_status_length = @@ -1844,8 +1846,13 @@ HAILO_COMMON_STATUS_t CONTROL_PROTOCOL__pack_change_context_switch_status_reques request->parameters.change_context_switch_status_request.dynamic_batch_size_length = BYTE_ORDER__htonl(sizeof(request->parameters.change_context_switch_status_request.dynamic_batch_size)); request->parameters.change_context_switch_status_request.dynamic_batch_size = dynamic_batch_size; - - /* dynamic_batch_size */ + + /* batch_count */ + request->parameters.change_context_switch_status_request.batch_count_length = + BYTE_ORDER__htonl(sizeof(request->parameters.change_context_switch_status_request.batch_count)); + request->parameters.change_context_switch_status_request.batch_count = batch_count; + + /* keep_nn_config_during_reset */ request->parameters.change_context_switch_status_request.keep_nn_config_during_reset_length = BYTE_ORDER__htonl(sizeof(request->parameters.change_context_switch_status_request.keep_nn_config_during_reset)); request->parameters.change_context_switch_status_request.keep_nn_config_during_reset = keep_nn_config_during_reset; @@ -2392,7 +2399,7 @@ exit: HAILO_COMMON_STATUS_t CONTROL_PROTOCOL__pack_change_hw_infer_status_request( CONTROL_PROTOCOL__request_t *request, size_t *request_size, uint32_t sequence, uint8_t hw_infer_state, uint8_t network_group_index, uint16_t dynamic_batch_size, - CONTROL_PROTOCOL__hw_infer_channels_info_t *channels_info) + uint16_t batch_count, CONTROL_PROTOCOL__hw_infer_channels_info_t *channels_info) { HAILO_COMMON_STATUS_t status = HAILO_COMMON_STATUS__UNINITIALIZED; size_t local_request_size = 0; @@ -2423,6 +2430,11 @@ HAILO_COMMON_STATUS_t CONTROL_PROTOCOL__pack_change_hw_infer_status_request( BYTE_ORDER__htonl(sizeof(request->parameters.change_hw_infer_status_request.dynamic_batch_size)); request->parameters.change_hw_infer_status_request.dynamic_batch_size = dynamic_batch_size; + /* batch_count */ + request->parameters.change_hw_infer_status_request.batch_count_length = + BYTE_ORDER__htonl(sizeof(request->parameters.change_hw_infer_status_request.batch_count)); + request->parameters.change_hw_infer_status_request.batch_count = batch_count; + /* channels_info */ request->parameters.change_hw_infer_status_request.channels_info_length = BYTE_ORDER__htonl(sizeof(request->parameters.change_hw_infer_status_request.channels_info)); diff --git a/hailort/libhailort/src/device_common/control_protocol.hpp b/hailort/libhailort/src/device_common/control_protocol.hpp index 544f4e2..ade0260 100644 --- a/hailort/libhailort/src/device_common/control_protocol.hpp +++ b/hailort/libhailort/src/device_common/control_protocol.hpp @@ -106,7 +106,7 @@ HAILO_COMMON_STATUS_t CONTROL_PROTOCOL__pack_download_context_action_list_reques HAILO_COMMON_STATUS_t CONTROL_PROTOCOL__pack_change_context_switch_status_request( CONTROL_PROTOCOL__request_t *request, size_t *request_size, uint32_t sequence, CONTROL_PROTOCOL__CONTEXT_SWITCH_STATUS_t state_machine_status, uint8_t application_index, - uint16_t dynamic_batch_size, bool keep_nn_config_during_reset); + uint16_t dynamic_batch_size, uint16_t batch_count, bool keep_nn_config_during_reset); HAILO_COMMON_STATUS_t CONTROL_PROTOCOL__pack_wd_enable( CONTROL_PROTOCOL__request_t *request, size_t *request_size, @@ -172,6 +172,6 @@ HAILO_COMMON_STATUS_t CONTROL_PROTOCOL__pack_get_hw_consts_request(CONTROL_PROTO HAILO_COMMON_STATUS_t CONTROL_PROTOCOL__pack_set_sleep_state_request(CONTROL_PROTOCOL__request_t *request, size_t *request_size, uint32_t sequence, uint8_t sleep_state); HAILO_COMMON_STATUS_t CONTROL_PROTOCOL__pack_change_hw_infer_status_request(CONTROL_PROTOCOL__request_t *request, size_t *request_size, uint32_t sequence, uint8_t hw_infer_state, uint8_t network_group_index, - uint16_t dynamic_batch_size, CONTROL_PROTOCOL__hw_infer_channels_info_t *channels_info); + uint16_t dynamic_batch_size, uint16_t batch_count, CONTROL_PROTOCOL__hw_infer_channels_info_t *channels_info); #endif /* _CONTROL_PROTOCOL_HPP_ */ \ No newline at end of file diff --git a/hailort/libhailort/src/device_common/d2h_events_parser.cpp b/hailort/libhailort/src/device_common/d2h_events_parser.cpp index 412e928..53384e2 100644 --- a/hailort/libhailort/src/device_common/d2h_events_parser.cpp +++ b/hailort/libhailort/src/device_common/d2h_events_parser.cpp @@ -43,6 +43,7 @@ static HAILO_COMMON_STATUS_t D2H_EVENTS__parse_health_monitor_cpu_ecc_error_noti static HAILO_COMMON_STATUS_t D2H_EVENTS__parse_health_monitor_cpu_ecc_fatal_notification(D2H_EVENT_MESSAGE_t *d2h_notification_message); static HAILO_COMMON_STATUS_t D2H_EVENTS__parse_context_switch_breakpoint_reached(D2H_EVENT_MESSAGE_t *d2h_notification_message); static HAILO_COMMON_STATUS_t D2H_EVENTS__parse_health_monitor_clock_changed_event_notification(D2H_EVENT_MESSAGE_t *d2h_notification_message); +static HAILO_COMMON_STATUS_t D2H_EVENTS__parse_hw_infer_manager_infer_done_notification(D2H_EVENT_MESSAGE_t *d2h_notification_message); /********************************************************************** * Globals @@ -58,7 +59,8 @@ firmware_notifications_parser_t g_firmware_notifications_parser[D2H_EVENT_ID_COU D2H_EVENTS__parse_health_monitor_cpu_ecc_error_notification, D2H_EVENTS__parse_health_monitor_cpu_ecc_fatal_notification, D2H_EVENTS__parse_context_switch_breakpoint_reached, - D2H_EVENTS__parse_health_monitor_clock_changed_event_notification + D2H_EVENTS__parse_health_monitor_clock_changed_event_notification, + D2H_EVENTS__parse_hw_infer_manager_infer_done_notification }; /********************************************************************** * Internal Functions @@ -176,6 +178,25 @@ l_exit: return status; } +static HAILO_COMMON_STATUS_t D2H_EVENTS__parse_hw_infer_manager_infer_done_notification(D2H_EVENT_MESSAGE_t *d2h_notification_message) +{ + HAILO_COMMON_STATUS_t status = HAILO_COMMON_STATUS__UNINITIALIZED; + + if (D2H_EVENT_HW_INFER_MANAGER_INFER_DONE_PARAMETER_COUNT != d2h_notification_message->header.parameter_count) { + LOGGER__ERROR("d2h notification invalid parameter count: {}", d2h_notification_message->header.parameter_count); + status = HAILO_STATUS__D2H_EVENTS__INCORRECT_PARAMETER_COUNT; + goto l_exit; + } + + LOGGER__INFO("Got hw infer done notification - Infer took {} cycles", + d2h_notification_message->message_parameters.hw_infer_manager_infer_done_event.infer_cycles); + + status = HAILO_COMMON_STATUS__SUCCESS; + +l_exit: + return status; +} + static HAILO_COMMON_STATUS_t D2H_EVENTS__parse_health_monitor_closed_streams_notification(D2H_EVENT_MESSAGE_t *d2h_notification_message) { HAILO_COMMON_STATUS_t status = HAILO_COMMON_STATUS__UNINITIALIZED; diff --git a/hailort/libhailort/src/device_common/device.cpp b/hailort/libhailort/src/device_common/device.cpp index 2043e3e..22bb85a 100644 --- a/hailort/libhailort/src/device_common/device.cpp +++ b/hailort/libhailort/src/device_common/device.cpp @@ -95,9 +95,9 @@ Expected> Device::create() { auto device_ids = scan(); CHECK_EXPECTED(device_ids, "Failed scan devices"); - CHECK_AS_EXPECTED(device_ids->size() == 1, HAILO_INVALID_OPERATION, - "Expected only 1 device on the system (found {}). Pass device_id to create a specific device", device_ids->size()); + CHECK_AS_EXPECTED(device_ids->size() >= 1, HAILO_INVALID_OPERATION, "There is no hailo device on the system"); + // Choose the first device. return Device::create(device_ids->at(0)); } @@ -155,6 +155,31 @@ Expected> Device::create_eth(const std::string &ip_addr) return device; } +Expected> Device::create_eth(const std::string &device_address, uint16_t port, + uint32_t timeout_milliseconds, uint8_t max_number_of_attempts) +{ + /* Validate address length */ + CHECK_AS_EXPECTED(INET_ADDRSTRLEN >= device_address.size(), + HAILO_INVALID_ARGUMENT, "device_address is too long"); + + hailo_eth_device_info_t device_info = {}; + device_info.host_address.sin_family = AF_INET; + device_info.host_address.sin_port = HAILO_ETH_PORT_ANY; + auto status = Socket::pton(AF_INET, HAILO_ETH_ADDRESS_ANY, &(device_info.host_address.sin_addr)); + CHECK_SUCCESS_AS_EXPECTED(status); + + device_info.device_address.sin_family = AF_INET; + device_info.device_address.sin_port = port; + status = Socket::pton(AF_INET, device_address.c_str(), &(device_info.device_address.sin_addr)); + CHECK_SUCCESS_AS_EXPECTED(status); + + device_info.timeout_millis = timeout_milliseconds; + device_info.max_number_of_attempts = max_number_of_attempts; + device_info.max_payload_size = HAILO_DEFAULT_ETH_MAX_PAYLOAD_SIZE; + + return create_eth(device_info); +} + Expected Device::parse_pcie_device_info(const std::string &device_info_str) { const bool LOG_ON_FAILURE = true; @@ -184,6 +209,28 @@ Expected Device::get_device_type(const std::string &device_id) } } +bool Device::device_ids_equal(const std::string &first, const std::string &second) +{ + const bool DONT_LOG_ON_FAILURE = false; + if (IntegratedDevice::DEVICE_ID == first) { + // On integrated devices device all ids should be the same + return first == second; + } else if (auto first_pcie_info = PcieDevice::parse_pcie_device_info(first, DONT_LOG_ON_FAILURE)) { + auto second_pcie_info = PcieDevice::parse_pcie_device_info(second, DONT_LOG_ON_FAILURE); + if (!second_pcie_info) { + // second is not pcie + return false; + } + return PcieDevice::pcie_device_infos_equal(*first_pcie_info, *second_pcie_info); + } else if (auto eth_info = EthernetDevice::parse_eth_device_info(first, DONT_LOG_ON_FAILURE)) { + // On ethernet devices, device ids should e equal + return first == second; + } else { + // first device does not match. + return false; + } +} + uint32_t Device::get_control_sequence() { return m_control_sequence; diff --git a/hailort/libhailort/src/device_common/device_internal.cpp b/hailort/libhailort/src/device_common/device_internal.cpp index 5fd1ea8..3045ded 100644 --- a/hailort/libhailort/src/device_common/device_internal.cpp +++ b/hailort/libhailort/src/device_common/device_internal.cpp @@ -570,8 +570,6 @@ void DeviceBase::d2h_notification_thread_main(const std::string &device_id) continue; } - LOGGER__INFO("[{}] Got notification from fw with id: {}", device_id, hailo_notification_id); - std::shared_ptr callback_func = nullptr; void *callback_opaque = nullptr; { @@ -665,6 +663,9 @@ hailo_status DeviceBase::fw_notification_id_to_hailo(D2H_EVENT_ID_t fw_notificat case HEALTH_MONITOR_CLOCK_CHANGED_EVENT_ID: *hailo_notification_id = HAILO_NOTIFICATION_ID_HEALTH_MONITOR_CLOCK_CHANGED_EVENT; break; + case HW_INFER_MANAGER_INFER_DONE: + *hailo_notification_id = HAILO_NOTIFICATION_ID_HW_INFER_MANAGER_INFER_DONE; + break; default: status = HAILO_INVALID_ARGUMENT; goto l_exit; diff --git a/hailort/libhailort/src/device_common/device_internal.hpp b/hailort/libhailort/src/device_common/device_internal.hpp index 58a51ad..8ffe767 100644 --- a/hailort/libhailort/src/device_common/device_internal.hpp +++ b/hailort/libhailort/src/device_common/device_internal.hpp @@ -83,6 +83,14 @@ public: virtual hailo_status erase_user_config() override; static hailo_device_architecture_t hef_arch_to_device_arch(ProtoHEFHwArch hef_arch); + virtual Expected get_architecture() const override + { + // FW is always up if we got here (device implementations's ctor would fail otherwise) + // Hence, just return it + return Expected(m_device_architecture); + } + + protected: struct NotificationThreadSharedParams { NotificationThreadSharedParams() : is_running(false) {} diff --git a/hailort/libhailort/src/eth/eth_device.cpp b/hailort/libhailort/src/eth/eth_device.cpp index 32f955d..9b4eeca 100644 --- a/hailort/libhailort/src/eth/eth_device.cpp +++ b/hailort/libhailort/src/eth/eth_device.cpp @@ -79,7 +79,8 @@ hailo_status EthernetDevice::wait_for_wakeup() CHECK_SUCCESS(status); /* Parse and validate the response */ - return Control::parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, &request); + return Control::parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, &payload, &request, + *this); } Expected> EthernetDevice::create(const hailo_eth_device_info_t &device_info) @@ -213,11 +214,10 @@ Expected> EthernetDevice::scan(const std::s std::chrono::milliseconds timeout) { // Convert interface name to IP address - std::array interface_ip_address{}; - auto status = EthernetUtils::get_ip_from_interface(interface_name.c_str(), interface_ip_address.data(), interface_ip_address.size()); - CHECK_SUCCESS_AS_EXPECTED(status); + auto interface_ip_address = EthernetUtils::get_ip_from_interface(interface_name); + CHECK_EXPECTED(interface_ip_address); - return scan_by_host_address(interface_ip_address.data(), timeout); + return scan_by_host_address(*interface_ip_address, timeout); } hailo_status get_udp_broadcast_params(const char *host_address, struct in_addr &interface_ip_address, @@ -348,7 +348,7 @@ hailo_status EthernetDevice::reset_impl(CONTROL_PROTOCOL__reset_type_t reset_typ // TODO: fix logic with respect to is_expecting_response if (0 != response_size) { status = Control::parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, - &payload, &request); + &payload, &request, *this); CHECK_SUCCESS(status); CHECK(is_expecting_response, HAILO_INTERNAL_FAILURE, "Recived valid response from FW for control who is not expecting one."); @@ -361,13 +361,6 @@ hailo_status EthernetDevice::reset_impl(CONTROL_PROTOCOL__reset_type_t reset_typ return HAILO_SUCCESS; } -Expected EthernetDevice::get_architecture() const -{ - // FW is always up if we got here (EthernetDevice's ctor would fail otherwise) - // Hence, just return it - return Expected(m_device_architecture); -} - hailo_eth_device_info_t EthernetDevice::get_device_info() const { return m_device_info; @@ -438,11 +431,9 @@ Expected EthernetDevice::create_networks_group_vec auto core_op_metadata = hef.pimpl->get_core_op_metadata(network_group_name); CHECK_EXPECTED(core_op_metadata); + auto core_op_metadata_ptr = core_op_metadata.release(); - auto core_op_metadata_ptr = make_shared_nothrow(core_op_metadata.release()); - CHECK_AS_EXPECTED(nullptr != core_op_metadata_ptr, HAILO_OUT_OF_HOST_MEMORY); - - auto net_flow_ops = hef.pimpl->post_process_ops(core_op_metadata_ptr->core_op_name()); + auto metadata = hef.pimpl->network_group_metadata(core_op_metadata_ptr->core_op_name()); auto status = HAILO_UNINITIALIZED; auto single_context_app = HcpConfigCoreOp(*this, m_active_core_op_holder, net_group_config.release(), @@ -462,7 +453,7 @@ Expected EthernetDevice::create_networks_group_vec m_core_ops.push_back(core_op_ptr); core_ops_ptrs.push_back(core_op_ptr); - auto net_group_expected = ConfiguredNetworkGroupBase::create(config_params, std::move(core_ops_ptrs), std::move(net_flow_ops)); + auto net_group_expected = ConfiguredNetworkGroupBase::create(config_params, std::move(core_ops_ptrs), std::move(metadata)); CHECK_EXPECTED(net_group_expected); auto net_group_ptr = net_group_expected.release(); diff --git a/hailort/libhailort/src/eth/eth_device.hpp b/hailort/libhailort/src/eth/eth_device.hpp index e79f93b..fca41f1 100644 --- a/hailort/libhailort/src/eth/eth_device.hpp +++ b/hailort/libhailort/src/eth/eth_device.hpp @@ -56,7 +56,6 @@ public: static Expected> create(const hailo_eth_device_info_t &device_info); static Expected> create(const std::string &ip_addr); - virtual Expected get_architecture() const override; hailo_eth_device_info_t get_device_info() const; virtual const char* get_dev_id() const override; diff --git a/hailort/libhailort/src/eth/eth_stream.cpp b/hailort/libhailort/src/eth/eth_stream.cpp index 5f6919d..8b6cada 100644 --- a/hailort/libhailort/src/eth/eth_stream.cpp +++ b/hailort/libhailort/src/eth/eth_stream.cpp @@ -138,20 +138,19 @@ Expected EthernetInputStream::sync_write_raw_buffer(const MemoryView &bu return size; } -hailo_status EthernetInputStream::sync_write_all_raw_buffer_no_transform_impl(void *buffer, size_t offset, size_t size) +hailo_status EthernetInputStream::write_impl(const MemoryView &buffer) { hailo_status status = HAILO_UNINITIALIZED; - ASSERT(NULL != buffer); - - CHECK(size >= MIN_UDP_PAYLOAD_SIZE, HAILO_INVALID_ARGUMENT, "Input must be larger than {}", MIN_UDP_PAYLOAD_SIZE); - CHECK(((size % HailoRTCommon::HW_DATA_ALIGNMENT) == 0), HAILO_INVALID_ARGUMENT, - "Input must be aligned to {} (got {})", HailoRTCommon::HW_DATA_ALIGNMENT, size); + CHECK(buffer.size() >= MIN_UDP_PAYLOAD_SIZE, HAILO_INVALID_ARGUMENT, "Input must be larger than {}", MIN_UDP_PAYLOAD_SIZE); + CHECK(((buffer.size() % HailoRTCommon::HW_DATA_ALIGNMENT) == 0), HAILO_INVALID_ARGUMENT, + "Input must be aligned to {} (got {})", HailoRTCommon::HW_DATA_ALIGNMENT, buffer.size()); + const size_t offset = 0; if (this->configuration.is_sync_enabled) { - status = eth_stream__write_all_with_sync(buffer, offset, size); + status = eth_stream__write_all_with_sync(buffer.data(), offset, buffer.size()); } else { - status = eth_stream__write_all_no_sync(buffer, offset, size); + status = eth_stream__write_all_no_sync(buffer.data(), offset, buffer.size()); } if (HAILO_STREAM_ABORTED_BY_USER == status) { LOGGER__INFO("eth_stream__write_all was aborted!"); @@ -163,7 +162,7 @@ hailo_status EthernetInputStream::sync_write_all_raw_buffer_no_transform_impl(vo return HAILO_SUCCESS; } -hailo_status EthernetInputStream::eth_stream__write_all_no_sync(void *buffer, size_t offset, size_t size) { +hailo_status EthernetInputStream::eth_stream__write_all_no_sync(const void *buffer, size_t offset, size_t size) { size_t remainder_size = 0; size_t packet_size = this->configuration.max_payload_size; @@ -180,13 +179,13 @@ hailo_status EthernetInputStream::eth_stream__write_all_no_sync(void *buffer, si return eth_stream__write_with_remainder(buffer, offset, size, remainder_size); } -hailo_status EthernetInputStream::eth_stream__write_with_remainder(void *buffer, size_t offset, size_t size, size_t remainder_size) { +hailo_status EthernetInputStream::eth_stream__write_with_remainder(const void *buffer, size_t offset, size_t size, size_t remainder_size) { size_t transfer_size = 0; size_t offset_end_without_remainder = offset + size - remainder_size; while (offset < offset_end_without_remainder) { transfer_size = offset_end_without_remainder - offset; - auto expected_bytes_written = sync_write_raw_buffer(MemoryView(static_cast(buffer) + offset, transfer_size)); + auto expected_bytes_written = sync_write_raw_buffer(MemoryView::create_const(static_cast(buffer) + offset, transfer_size)); if (HAILO_STREAM_ABORTED_BY_USER == expected_bytes_written.status()) { LOGGER__INFO("sync_write_raw_buffer was aborted!"); return expected_bytes_written.status(); @@ -195,7 +194,7 @@ hailo_status EthernetInputStream::eth_stream__write_with_remainder(void *buffer, offset += expected_bytes_written.release(); } if (0 < remainder_size) { - auto expected_bytes_written = sync_write_raw_buffer(MemoryView(static_cast(buffer) + offset, remainder_size)); + auto expected_bytes_written = sync_write_raw_buffer(MemoryView::create_const(static_cast(buffer) + offset, remainder_size)); if (HAILO_STREAM_ABORTED_BY_USER == expected_bytes_written.status()) { LOGGER__INFO("sync_write_raw_buffer was aborted!"); return expected_bytes_written.status(); @@ -220,7 +219,7 @@ TokenBucketEthernetInputStream::TokenBucketEthernetInputStream(Device &device, U token_bucket() {} -hailo_status TokenBucketEthernetInputStream::eth_stream__write_with_remainder(void *buffer, size_t offset, size_t size, size_t remainder_size) { +hailo_status TokenBucketEthernetInputStream::eth_stream__write_with_remainder(const void *buffer, size_t offset, size_t size, size_t remainder_size) { size_t transfer_size = 0; size_t offset_end_without_remainder = offset + size - remainder_size; @@ -231,7 +230,7 @@ hailo_status TokenBucketEthernetInputStream::eth_stream__write_with_remainder(vo (void)token_bucket.consumeWithBorrowAndWait(MAX_CONSUME_SIZE, rate_bytes_per_sec, BURST_SIZE); transfer_size = offset_end_without_remainder - offset; - auto expected_bytes_written = sync_write_raw_buffer(MemoryView(static_cast(buffer) + offset, transfer_size)); + auto expected_bytes_written = sync_write_raw_buffer(MemoryView::create_const(static_cast(buffer) + offset, transfer_size)); if (HAILO_STREAM_ABORTED_BY_USER == expected_bytes_written.status()) { LOGGER__INFO("sync_write_raw_buffer was aborted!"); return expected_bytes_written.status(); @@ -244,7 +243,7 @@ hailo_status TokenBucketEthernetInputStream::eth_stream__write_with_remainder(vo // However, since remainder_size is modulo MAX_UDP_PAYLOAD_SIZE and BURST_SIZE == MAX_UDP_PAYLOAD_SIZE, it should be smaller. (void)token_bucket.consumeWithBorrowAndWait(static_cast(remainder_size), rate_bytes_per_sec, BURST_SIZE); - auto expected_bytes_written = sync_write_raw_buffer(MemoryView(static_cast(buffer) + offset, remainder_size)); + auto expected_bytes_written = sync_write_raw_buffer(MemoryView::create_const(static_cast(buffer) + offset, remainder_size)); if (HAILO_STREAM_ABORTED_BY_USER == expected_bytes_written.status()) { LOGGER__INFO("sync_write_raw_buffer was aborted!"); return expected_bytes_written.status(); @@ -296,7 +295,7 @@ TrafficControlEthernetInputStream::TrafficControlEthernetInputStream(Device &dev {} #endif -hailo_status EthernetInputStream::eth_stream__write_all_with_sync(void *buffer, size_t offset, size_t size) { +hailo_status EthernetInputStream::eth_stream__write_all_with_sync(const void *buffer, size_t offset, size_t size) { hailo_status status = HAILO_UNINITIALIZED; size_t number_of_frames = 0; size_t frame_size = m_stream_info.hw_frame_size; @@ -635,7 +634,7 @@ bool EthernetOutputStream::is_sync_packet(const void* buffer, size_t offset, siz ((hailo_output_sync_packet_t*)((uint8_t*)buffer + offset))->barker == BYTE_ORDER__ntohl(SYNC_PACKET_BARKER)); } -hailo_status EthernetOutputStream::read_all(MemoryView &buffer) +hailo_status EthernetOutputStream::read_impl(MemoryView &buffer) { if ((buffer.size() % HailoRTCommon::HW_DATA_ALIGNMENT) != 0) { LOGGER__ERROR("Size must be aligned to {} (got {})", HailoRTCommon::HW_DATA_ALIGNMENT, buffer.size()); @@ -649,7 +648,7 @@ hailo_status EthernetOutputStream::read_all(MemoryView &buffer) status = this->read_all_no_sync(buffer.data(), 0, buffer.size()); } if (HAILO_STREAM_ABORTED_BY_USER == status) { - LOGGER__INFO("read_all was aborted!"); + LOGGER__INFO("read was aborted!"); return status; } CHECK_SUCCESS(status); diff --git a/hailort/libhailort/src/eth/eth_stream.hpp b/hailort/libhailort/src/eth/eth_stream.hpp index 0b6c0a9..7702f83 100644 --- a/hailort/libhailort/src/eth/eth_stream.hpp +++ b/hailort/libhailort/src/eth/eth_stream.hpp @@ -54,15 +54,15 @@ private: Device &m_device; hailo_status eth_stream__config_input_sync_params(uint32_t frames_per_sync); - hailo_status eth_stream__write_all_no_sync(void *buffer, size_t offset, size_t size); - hailo_status eth_stream__write_all_with_sync(void *buffer, size_t offset, size_t size); + hailo_status eth_stream__write_all_no_sync(const void *buffer, size_t offset, size_t size); + hailo_status eth_stream__write_all_with_sync(const void *buffer, size_t offset, size_t size); hailo_status set_timeout(std::chrono::milliseconds timeout); void set_max_payload_size(uint16_t size); protected: - virtual hailo_status eth_stream__write_with_remainder(void *buffer, size_t offset, size_t size, size_t remainder_size); - virtual Expected sync_write_raw_buffer(const MemoryView &buffer) override; - virtual hailo_status sync_write_all_raw_buffer_no_transform_impl(void *buffer, size_t offset, size_t size) override; + virtual hailo_status eth_stream__write_with_remainder(const void *buffer, size_t offset, size_t size, size_t remainder_size); + Expected sync_write_raw_buffer(const MemoryView &buffer); + virtual hailo_status write_impl(const MemoryView &buffer) override; public: EthernetInputStream(Device &device, Udp &&udp, EventPtr &&core_op_activated_event, const LayerInfo &layer_info, hailo_status &status) : @@ -103,7 +103,7 @@ private: static const uint32_t MAX_CONSUME_SIZE = MAX_UDP_PAYLOAD_SIZE; protected: - virtual hailo_status eth_stream__write_with_remainder(void *buffer, size_t offset, size_t size, size_t remainder_size); + virtual hailo_status eth_stream__write_with_remainder(const void *buffer, size_t offset, size_t size, size_t remainder_size) override; public: TokenBucketEthernetInputStream(Device &device, Udp &&udp, EventPtr &&core_op_activated_event, @@ -140,7 +140,7 @@ private: Device &m_device; EthernetOutputStream(Device &device, const LayerInfo &edge_layer, Udp &&udp, EventPtr &&core_op_activated_event, hailo_status &status) : - OutputStreamBase(edge_layer, std::move(core_op_activated_event), status), + OutputStreamBase(edge_layer, HAILO_STREAM_INTERFACE_ETH, std::move(core_op_activated_event), status), leftover_buffer(), leftover_size(0), // Firmware starts sending sync sequence from 0, so treating the first previous as max value (that will be overflowed to 0) @@ -151,7 +151,7 @@ private: m_device(device) {} - hailo_status read_all(MemoryView &buffer) override; + hailo_status read_impl(MemoryView &buffer) override; hailo_status read_all_with_sync(void *buffer, size_t offset, size_t size); hailo_status read_all_no_sync(void *buffer, size_t offset, size_t size); @@ -166,7 +166,7 @@ private: public: virtual ~EthernetOutputStream(); - virtual Expected sync_read_raw_buffer(MemoryView &buffer); + Expected sync_read_raw_buffer(MemoryView &buffer); static Expected> create(Device &device, const LayerInfo &edge_layer, const hailo_eth_output_stream_params_t ¶ms, EventPtr core_op_activated_event); diff --git a/hailort/libhailort/src/eth/hcp_config_core_op.cpp b/hailort/libhailort/src/eth/hcp_config_core_op.cpp index eb6d8bc..39ad039 100644 --- a/hailort/libhailort/src/eth/hcp_config_core_op.cpp +++ b/hailort/libhailort/src/eth/hcp_config_core_op.cpp @@ -80,6 +80,12 @@ Expected HcpConfigCoreOp::get_boundary_vdma_channel_by return make_unexpected(HAILO_INVALID_OPERATION); } +Expected HcpConfigCoreOp::run_hw_infer_estimator() +{ + LOGGER__ERROR("run_hw_infer_estimator function is not supported on ETH core-ops"); + return make_unexpected(HAILO_INVALID_OPERATION); +} + hailo_status HcpConfigCoreOp::activate_impl(uint16_t dynamic_batch_size, bool resume_pending_stream_transfers) { m_active_core_op_holder.set(*this); diff --git a/hailort/libhailort/src/eth/hcp_config_core_op.hpp b/hailort/libhailort/src/eth/hcp_config_core_op.hpp index 710d98c..9ef18bd 100644 --- a/hailort/libhailort/src/eth/hcp_config_core_op.hpp +++ b/hailort/libhailort/src/eth/hcp_config_core_op.hpp @@ -50,6 +50,7 @@ public: virtual hailo_status activate_impl(uint16_t dynamic_batch_size, bool resume_pending_stream_transfers) override; virtual hailo_status deactivate_impl(bool keep_nn_config_during_reset) override; + virtual Expected run_hw_infer_estimator() override; virtual ~HcpConfigCoreOp() = default; HcpConfigCoreOp(const HcpConfigCoreOp &other) = delete; diff --git a/hailort/libhailort/src/eth/network_rate_calculator.cpp b/hailort/libhailort/src/eth/network_rate_calculator.cpp index 0578d67..5a2c450 100644 --- a/hailort/libhailort/src/eth/network_rate_calculator.cpp +++ b/hailort/libhailort/src/eth/network_rate_calculator.cpp @@ -12,6 +12,7 @@ #include "hailo/network_rate_calculator.hpp" #include "common/utils.hpp" +#include "common/ethernet_utils.hpp" #include "eth/eth_stream.hpp" @@ -148,4 +149,42 @@ Expected> NetworkUdpRateCalculator::get_udp_ports_r return results; } +hailo_status NetworkUdpRateCalculator::set_rate_limit(const std::string &ip, uint16_t port, uint32_t rate_bytes_per_sec) +{ +#if defined(__GNUC__) + auto tc = TrafficControlUtil::create(ip, port, rate_bytes_per_sec); + CHECK_EXPECTED_AS_STATUS(tc); + CHECK_SUCCESS(tc->set_rate_limit()); + + return HAILO_SUCCESS; +#else + (void)ip; + (void)port; + (void)rate_bytes_per_sec; + LOGGER__ERROR("set_rate_limit is only supported on Unix platforms"); + return HAILO_NOT_IMPLEMENTED; +#endif +} + +hailo_status NetworkUdpRateCalculator::reset_rate_limit(const std::string &ip, uint16_t port) +{ +#if defined(__GNUC__) + auto tc = TrafficControlUtil::create(ip, port, 0); + CHECK_EXPECTED_AS_STATUS(tc); + CHECK_SUCCESS(tc->reset_rate_limit()); + + return HAILO_SUCCESS; +#else + (void)ip; + (void)port; + LOGGER__ERROR("reset_rate_limit is only supported on Unix platforms"); + return HAILO_NOT_IMPLEMENTED; +#endif +} + +Expected NetworkUdpRateCalculator::get_interface_name(const std::string &ip) +{ + return EthernetUtils::get_interface_from_board_ip(ip); +} + } /* namespace hailort */ diff --git a/hailort/libhailort/src/hailort.cpp b/hailort/libhailort/src/hailort.cpp index e065116..8704bed 100644 --- a/hailort/libhailort/src/hailort.cpp +++ b/hailort/libhailort/src/hailort.cpp @@ -34,12 +34,36 @@ #include "utils/shared_resource_manager.hpp" #include "vdevice/vdevice_internal.hpp" #include "utils/profiler/tracer_macros.hpp" +#include "utils/exported_resource_manager.hpp" #include +#include using namespace hailort; +// Note: Async stream API uses BufferPtr as a param. When exporting BufferPtrs to the user via c-api, they must be +// stored in some container, otherwise their ref count may reach zero and they will be freed, despite the +// c-api user still using them. (shared_ptr doesn't have a release method like unique_ptr) +// Singleton holding a mapping between the address of a buffer allocated/mapped via hailo_allocate_buffer/hailo_dma_map_buffer_to_device +// to the underlying BufferPtr. When a buffer is freed via hailo_free_buffer, the BufferPtr object will be removed from +// the storage. +using ExportedBufferManager = ExportedResourceManager; + +struct ThreeTupleHash { + template + std::size_t operator()(const T& tuple) const { + auto hash = std::hash::type>()(std::get<0>(tuple)); + hash ^= std::hash::type>()(std::get<1>(tuple)); + hash ^= std::hash::type>()(std::get<2>(tuple)); + return hash; + } +}; + +// (buffer_addr, device_id, mapping_direction) +using DmaMappingKey = std::tuple; +using DmaMappingManager = ExportedResourceManager; + COMPAT__INITIALIZER(hailort__initialize_logger) { // Init logger singleton if compiling only HailoRT @@ -203,7 +227,7 @@ hailo_status hailo_create_pcie_device(hailo_pcie_device_info_t *device_info, hai { CHECK_ARG_NOT_NULL(device_out); - auto device = (device_info == nullptr) ? PcieDevice::create() : PcieDevice::create(*device_info); + auto device = (device_info == nullptr) ? Device::create_pcie() : Device::create_pcie(*device_info); CHECK_EXPECTED_AS_STATUS(device, "Failed creating pcie device"); *device_out = reinterpret_cast(device.release().release()); @@ -574,6 +598,71 @@ hailo_status hailo_init_configure_params(hailo_hef hef, hailo_stream_interface_t return HAILO_SUCCESS; } +void fill_cfg_params_struct_by_class(const std::string &network_group_name, const ConfigureNetworkParams &class_in, hailo_configure_network_group_params_t *struct_out) +{ + strncpy(struct_out->name, network_group_name.c_str(), network_group_name.size() + 1); + struct_out->batch_size = class_in.batch_size; + struct_out->power_mode = class_in.power_mode; + struct_out->latency = class_in.latency; + + int i = 0; + for (auto & pair: class_in.network_params_by_name) { + strncpy(struct_out->network_params_by_name[i].name, pair.first.c_str(), pair.first.length() + 1); + struct_out->network_params_by_name[i].network_params = pair.second; + i++; + } + struct_out->network_params_by_name_count = class_in.network_params_by_name.size(); + + i = 0; + for (auto & pair: class_in.stream_params_by_name) { + strncpy(struct_out->stream_params_by_name[i].name, pair.first.c_str(), pair.first.length() + 1); + struct_out->stream_params_by_name[i].stream_params = pair.second; + i++; + } + struct_out->stream_params_by_name_count = class_in.stream_params_by_name.size(); +} + +hailo_status hailo_init_configure_params_by_vdevice(hailo_hef hef, hailo_vdevice vdevice, + hailo_configure_params_t *params) +{ + CHECK_ARG_NOT_NULL(hef); + CHECK_ARG_NOT_NULL(vdevice); + CHECK_ARG_NOT_NULL(params); + + auto configure_params = (reinterpret_cast(vdevice))->create_configure_params(*reinterpret_cast(hef)); + CHECK_EXPECTED_AS_STATUS(configure_params); + + params->network_group_params_count = configure_params->size(); + uint8_t net_group = 0; + for (auto &cfg_params : configure_params.value()) { + fill_cfg_params_struct_by_class(cfg_params.first, cfg_params.second, &(params->network_group_params[net_group])); + net_group++; + } + + return HAILO_SUCCESS; +} + +hailo_status hailo_init_configure_params_by_device(hailo_hef hef, hailo_device device, + hailo_configure_params_t *params) +{ + CHECK_ARG_NOT_NULL(hef); + CHECK_ARG_NOT_NULL(device); + CHECK_ARG_NOT_NULL(params); + + auto configure_params = (reinterpret_cast(device))->create_configure_params(*reinterpret_cast(hef)); + CHECK_EXPECTED_AS_STATUS(configure_params); + + params->network_group_params_count = configure_params->size(); + uint8_t net_group = 0; + for (auto &cfg_params : configure_params.value()) { + fill_cfg_params_struct_by_class(cfg_params.first, cfg_params.second, &(params->network_group_params[net_group])); + net_group++; + } + + return HAILO_SUCCESS; +} + + hailo_status hailo_init_configure_params_mipi_input(hailo_hef hef, hailo_stream_interface_t output_interface, hailo_mipi_input_stream_params_t *mipi_params, hailo_configure_params_t *params) { @@ -1008,6 +1097,106 @@ hailo_status hailo_set_scheduler_priority(hailo_configured_network_group configu return (reinterpret_cast(configured_network_group))->set_scheduler_priority(priority, network_name_str); } +hailo_status hailo_allocate_buffer(size_t size, const hailo_buffer_parameters_t *allocation_params, void **buffer_out) +{ + CHECK_ARG_NOT_NULL(allocation_params); + CHECK_ARG_NOT_NULL(buffer_out); + CHECK(0 != size, HAILO_INVALID_ARGUMENT, "Buffer size must be greater than zero"); + + auto buffer_storage_params = BufferStorageParams::create(*allocation_params); + CHECK_EXPECTED_AS_STATUS(buffer_storage_params); + + // Create buffer + auto buffer = Buffer::create_shared(size, *buffer_storage_params); + CHECK_EXPECTED_AS_STATUS(buffer); + + // Store the buffer in manager (otherwise it'll be freed at the end of this func) + const auto status = ExportedBufferManager::register_resource(*buffer, buffer->get()->data()); + CHECK_SUCCESS(status); + + *buffer_out = buffer->get()->data(); + + return HAILO_SUCCESS; +} + +hailo_status hailo_free_buffer(void *buffer) +{ + CHECK_ARG_NOT_NULL(buffer); + return ExportedBufferManager::unregister_resource(buffer); +} + +static Expected get_mapping_key(void *buffer, hailo_device device, hailo_dma_buffer_direction_t direction) +{ + hailo_device_id_t device_id{}; + auto status = hailo_get_device_id(device, &device_id); + CHECK_SUCCESS_AS_EXPECTED(status); + + return std::make_tuple(buffer, std::string(device_id.id), direction); +} + +// TODO: hailo_dma_map_buffer_to_device/hailo_dma_unmap_buffer_from_device aren't thread safe when crossed with +// hailo_allocate_buffer/hailo_free_buffer (HRT-10669) +hailo_status hailo_dma_map_buffer_to_device(void *buffer, size_t size, hailo_device device, hailo_dma_buffer_direction_t direction) +{ + CHECK_ARG_NOT_NULL(buffer); + CHECK_ARG_NOT_NULL(device); + + auto hailort_allocated_buffer = ExportedBufferManager::get_resource(buffer); + if (hailort_allocated_buffer) { + // TODO: this will change here HRT-10983 + // The buffer has been allocated by hailort + // The mapping is held by the Buffer object + auto mapping_result = hailort_allocated_buffer->get()->storage().dma_map(*reinterpret_cast(device), direction); + CHECK_EXPECTED_AS_STATUS(mapping_result); + const auto new_mapping = mapping_result.value(); + + if (!new_mapping) { + return HAILO_DMA_MAPPING_ALREADY_EXISTS; + } + } else { + // The buffer has been allocated by the user + // Create dma storage + auto dma_mapped_buffer = DmaStorage::create_from_user_address(buffer, size, direction, *reinterpret_cast(device)); + CHECK_EXPECTED_AS_STATUS(dma_mapped_buffer); + assert(buffer == dma_mapped_buffer.value()->user_address()); + auto dma_mapped_buffer_ptr = dma_mapped_buffer.release(); + + // Store the mapping in manager (otherwise it'll be freed at the end of this func) + auto key = get_mapping_key(dma_mapped_buffer_ptr->user_address(), device, direction); + CHECK_EXPECTED_AS_STATUS(key); + const auto status = DmaMappingManager::register_resource(dma_mapped_buffer_ptr, key.release()); + if (HAILO_INVALID_ARGUMENT == status) { + // TODO: This will change once we allow mapping the same buffer in different directions (HRT-10656). + // Checking that the mapping exists will need to be at DmaStorage's level + return HAILO_DMA_MAPPING_ALREADY_EXISTS; + } + CHECK_SUCCESS(status); + } + + return HAILO_SUCCESS; +} + +hailo_status hailo_dma_unmap_buffer_from_device(void *buffer, hailo_device device, hailo_dma_buffer_direction_t direction) +{ + // TODO: support mapping the same buffer in different directions (HRT-10656) + (void)direction; + + CHECK_ARG_NOT_NULL(buffer); + CHECK_ARG_NOT_NULL(device); + + auto hailort_allocated_buffer = ExportedBufferManager::get_resource(buffer); + if (hailort_allocated_buffer) { + // TODO: mappings get dtor'd when the Buffer object is dtor'd. + // We want all the mapping to be held in one place for hailort::Buffers and for user alloacted buffers + // so this will change (HRT-10983) + return HAILO_SUCCESS; + } + + auto key = get_mapping_key(buffer, device, direction); + CHECK_EXPECTED_AS_STATUS(key); + return DmaMappingManager::unregister_resource(key.release()); +} + hailo_status hailo_calculate_eth_input_rate_limits(hailo_hef hef, const char *network_group_name, uint32_t fps, hailo_rate_limit_t *rates, size_t *rates_length) { @@ -1106,6 +1295,114 @@ hailo_status hailo_stream_write_raw_buffer(hailo_input_stream stream, const void return HAILO_SUCCESS; } +hailo_status hailo_stream_wait_for_async_output_ready(hailo_output_stream stream, size_t transfer_size, uint32_t timeout_ms) +{ + CHECK_ARG_NOT_NULL(stream); + return (reinterpret_cast(stream))->wait_for_async_ready(transfer_size, std::chrono::milliseconds(timeout_ms)); +} + +hailo_status hailo_stream_wait_for_async_input_ready(hailo_input_stream stream, size_t transfer_size, uint32_t timeout_ms) +{ + CHECK_ARG_NOT_NULL(stream); + return (reinterpret_cast(stream))->wait_for_async_ready(transfer_size, std::chrono::milliseconds(timeout_ms)); +} + +hailo_status hailo_output_stream_get_async_max_queue_size(hailo_output_stream stream, size_t *queue_size) +{ + CHECK_ARG_NOT_NULL(stream); + CHECK_ARG_NOT_NULL(queue_size); + + auto local_queue_size = reinterpret_cast(stream)->get_async_max_queue_size(); + CHECK_EXPECTED_AS_STATUS(local_queue_size); + *queue_size = local_queue_size.release(); + + return HAILO_SUCCESS; +} + +hailo_status hailo_input_stream_get_async_max_queue_size(hailo_input_stream stream, size_t *queue_size) +{ + CHECK_ARG_NOT_NULL(stream); + CHECK_ARG_NOT_NULL(queue_size); + + auto local_queue_size = reinterpret_cast(stream)->get_async_max_queue_size(); + CHECK_EXPECTED_AS_STATUS(local_queue_size); + *queue_size = local_queue_size.release(); + + return HAILO_SUCCESS; +} + +static InputStream::TransferDoneCallback wrap_c_user_callback(hailo_stream_write_async_callback_t callback, void *opaque) +{ + return [callback, opaque](const InputStream::CompletionInfo &completion_info) { + hailo_stream_write_async_completion_info_t c_completion_info{}; + c_completion_info.status = completion_info.status; + c_completion_info.buffer_addr = completion_info.buffer_addr; + c_completion_info.buffer_size = completion_info.buffer_size; + c_completion_info.opaque = opaque; + callback(&c_completion_info); + }; +} + +static OutputStream::TransferDoneCallback wrap_c_user_callback(hailo_stream_read_async_callback_t callback, void *opaque) +{ + return [callback, opaque](const OutputStream::CompletionInfo &completion_info) { + hailo_stream_read_async_completion_info_t c_completion_info{}; + c_completion_info.status = completion_info.status; + c_completion_info.buffer_addr = completion_info.buffer_addr; + c_completion_info.buffer_size = completion_info.buffer_size; + c_completion_info.opaque = opaque; + callback(&c_completion_info); + }; +} + +hailo_status hailo_stream_read_raw_buffer_async(hailo_output_stream stream, void *buffer, size_t size, + hailo_stream_read_async_callback_t callback, void *opaque) +{ + CHECK_ARG_NOT_NULL(stream); + CHECK_ARG_NOT_NULL(buffer); + CHECK_ARG_NOT_NULL(callback); + + auto buffer_ref = ExportedBufferManager::get_resource(buffer); + if (HAILO_NOT_FOUND == buffer_ref.status()) { + // User addr (buffer hasn't been allocated by hailo_allocate_buffer) + return (reinterpret_cast(stream))->read_async(buffer, size, + wrap_c_user_callback(callback, opaque)); + } + + // buffer has been allocated by hailo_allocate_buffer + CHECK_EXPECTED_AS_STATUS(buffer_ref); + auto buffer_ptr = buffer_ref->get(); + assert(buffer_ptr != nullptr); + CHECK(size == buffer_ptr->size(), HAILO_INVALID_ARGUMENT); + + return (reinterpret_cast(stream))->read_async(buffer_ptr, + wrap_c_user_callback(callback, opaque)); +} + +hailo_status hailo_stream_write_raw_buffer_async(hailo_input_stream stream, const void *buffer, size_t size, + hailo_stream_write_async_callback_t callback, void *opaque) +{ + CHECK_ARG_NOT_NULL(stream); + CHECK_ARG_NOT_NULL(buffer); + CHECK_ARG_NOT_NULL(callback); + + auto buffer_ref = ExportedBufferManager::get_resource(const_cast(buffer)); + if (HAILO_NOT_FOUND == buffer_ref.status()) { + // User addr (buffer hasn't been allocated by hailo_allocate_buffer) + return (reinterpret_cast(stream))->write_async(buffer, size, + wrap_c_user_callback(callback, opaque)); + } + + // buffer has been allocated by hailo_allocate_buffer + CHECK_EXPECTED_AS_STATUS(buffer_ref); + auto buffer_ptr = buffer_ref->get(); + assert(buffer_ptr != nullptr); + CHECK(size == buffer_ptr->size(), HAILO_INVALID_ARGUMENT); + + return (reinterpret_cast(stream))->write_async(buffer_ptr, + wrap_c_user_callback(callback, opaque)); +} + hailo_status hailo_fuse_nms_frames(const hailo_nms_fuse_input_t *nms_fuse_inputs, uint32_t inputs_count, uint8_t *fused_buffer, size_t fused_buffer_size) { @@ -1328,6 +1625,25 @@ hailo_status hailo_demux_raw_frame_by_output_demuxer(hailo_output_demuxer demuxe return HAILO_SUCCESS; } +hailo_status hailo_demux_by_name_raw_frame_by_output_demuxer(hailo_output_demuxer demuxer, const void *src, + size_t src_size, hailo_stream_raw_buffer_by_name_t *raw_buffers_by_name, size_t raw_buffers_count) +{ + CHECK_ARG_NOT_NULL(src); + CHECK_ARG_NOT_NULL(raw_buffers_by_name); + CHECK_ARG_NOT_NULL(demuxer); + + std::map raw_buffers_map; + for (size_t i = 0; i < raw_buffers_count; i++) { + raw_buffers_map.emplace(std::string(raw_buffers_by_name[i].name), + MemoryView(raw_buffers_by_name[i].raw_buffer.buffer, raw_buffers_by_name[i].raw_buffer.size)); + } + auto src_memview = MemoryView::create_const(src, src_size); + auto status = reinterpret_cast(demuxer)->transform_demux(src_memview, raw_buffers_map); + CHECK_SUCCESS(status); + + return HAILO_SUCCESS; +} + hailo_status hailo_get_mux_infos_by_output_demuxer(hailo_output_demuxer demuxer, hailo_stream_info_t *stream_infos, size_t *number_of_streams) { diff --git a/hailort/libhailort/src/hailort_defaults.cpp b/hailort/libhailort/src/hailort_defaults.cpp index c845d3e..527b95e 100644 --- a/hailort/libhailort/src/hailort_defaults.cpp +++ b/hailort/libhailort/src/hailort_defaults.cpp @@ -361,6 +361,14 @@ std::string HailoRTDefaults::get_network_name(const std::string &net_group_name) hailo_format_t HailoRTDefaults::expand_auto_format(const hailo_format_t &host_format, const hailo_format_t &hw_format) { + if (HAILO_FORMAT_ORDER_HAILO_NMS == hw_format.order) { + assert(HAILO_FORMAT_TYPE_UINT16 == hw_format.type); + // TODO (HRT-11082): On NMS, change meaning of auto to float + if (HAILO_FORMAT_TYPE_AUTO == host_format.type) { + LOGGER__WARNING("Received 'HAILO_FORMAT_TYPE_AUTO' for NMS output, which is currently translated as HAILO_FORMAT_TYPE_UINT16. "\ + "Starting HailoRT version 4.15, this will change to HAILO_FORMAT_TYPE_FLOAT32"); + } + } auto host_format_copy = host_format; if (HAILO_FORMAT_TYPE_AUTO == host_format_copy.type) { host_format_copy.type = hw_format.type; diff --git a/hailort/libhailort/src/hef/context_switch_actions.cpp b/hailort/libhailort/src/hef/context_switch_actions.cpp index eda6184..0279ef4 100644 --- a/hailort/libhailort/src/hef/context_switch_actions.cpp +++ b/hailort/libhailort/src/hef/context_switch_actions.cpp @@ -16,7 +16,6 @@ namespace hailort { - static uint8_t pack_vdma_channel_id(const vdma::ChannelId &channel_id) { return static_cast(channel_id.channel_index | @@ -83,7 +82,7 @@ Expected ContextSwitchConfigAction::serialize_header() const Expected NoneAction::create() { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) NoneAction()); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -113,7 +112,7 @@ Expected ActivateConfigChannelAction::create(uint8 { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) ActivateConfigChannelAction(config_stream_index, channel_id, host_buffer_info)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -146,7 +145,7 @@ Expected DeactivateConfigChannelAction::create(uin { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) DeactivateConfigChannelAction(config_stream_index, channel_id)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -179,7 +178,7 @@ Expected WriteDataCcwAction::create( "Too many ccw burst {} (must fit in uint16)", total_ccw_burst); auto result = ContextSwitchConfigActionPtr(new (std::nothrow) WriteDataCcwAction( std::move(data), config_stream_index, static_cast(total_ccw_burst))); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -211,7 +210,7 @@ Expected WriteDataCcwAction::serialize_params(const ContextResources &) Expected AddCcwBurstAction::create(uint8_t config_stream_index, uint16_t ccw_bursts) { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) AddCcwBurstAction(config_stream_index, ccw_bursts)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -241,7 +240,7 @@ Expected FetchCfgChannelDescriptorsAction::create( "On cfg with continuous mode, max descriptors size must fit in uint16_t"); auto result = ContextSwitchConfigActionPtr(new (std::nothrow) FetchCfgChannelDescriptorsAction(channel_id, static_cast(desc_count))); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -267,7 +266,7 @@ Expected FetchCfgChannelDescriptorsAction::serialize_params(const Contex Expected StartBurstCreditsTaskAction::create() { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) StartBurstCreditsTaskAction()); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -290,7 +289,7 @@ Expected StartBurstCreditsTaskAction::serialize_params(const ContextReso Expected WaitForNetworkGroupChangeAction::create() { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) WaitForNetworkGroupChangeAction()); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -323,7 +322,7 @@ Expected RepeatedAction::create( "Invalid repeated sub-action type (can't have sub-action with type CONTEXT_SWITCH_DEFS__ACTION_TYPE_COUNT)"); auto result = ContextSwitchConfigActionPtr(new (std::nothrow) RepeatedAction(std::move(actions))); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -371,7 +370,7 @@ Expected> RepeatedAction::serialize(const ContextResources & Expected DisableLcuAction::create(uint8_t cluster_index, uint8_t lcu_index) { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) DisableLcuAction(cluster_index, lcu_index)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -396,7 +395,7 @@ Expected DisableLcuAction::serialize_params(const ContextResources &) co Expected WaitForLcuAction::create(uint8_t cluster_index, uint8_t lcu_index) { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) WaitForLcuAction(cluster_index, lcu_index)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -426,7 +425,7 @@ Expected EnableLcuAction::create(uint8_t cluster_i (CONTEXT_SWITCH_DEFS__ENABLE_LCU_DEFAULT_KERNEL_COUNT == kernel_done_count); auto result = ContextSwitchConfigActionPtr(new (std::nothrow) EnableLcuAction(cluster_index, lcu_index, network_index, kernel_done_address, kernel_done_count, is_default)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -481,7 +480,7 @@ Expected EnableSequencerAction::create(uint8_t clu { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) EnableSequencerAction(cluster_index, initial_l3_cut, initial_l3_offset, active_apu, active_ia, active_sc, active_l2, l2_offset_0, l2_offset_1)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -523,7 +522,7 @@ Expected EnableSequencerAction::serialize_params(const ContextResources Expected WaitForSequencerAction::create(uint8_t cluster_index) { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) WaitForSequencerAction(cluster_index)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -548,7 +547,7 @@ Expected WaitForSequencerAction::serialize_params(const ContextResources Expected AllowInputDataflowAction::create(uint8_t stream_index) { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) AllowInputDataflowAction(stream_index)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -571,7 +570,8 @@ bool AllowInputDataflowAction::supports_repeated_block() const Expected AllowInputDataflowAction::serialize_params(const ContextResources &context_resources) const { - const auto edge_layer = context_resources.get_edge_layer_by_stream_index(m_stream_index); + // H2D direction because it is Input actions + const auto edge_layer = context_resources.get_edge_layer_by_stream_index(m_stream_index, HAILO_H2D_STREAM); CHECK_EXPECTED(edge_layer); CONTEXT_SWITCH_DEFS__fetch_data_action_data_t params{}; @@ -602,7 +602,7 @@ Expected AllowInputDataflowAction::serialize_params(const ContextResourc Expected WaitForModuleConfigDoneAction::create(uint8_t module_index) { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) WaitForModuleConfigDoneAction(module_index)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -629,7 +629,7 @@ Expected DdrPairInfoAction::create(const vdma::Cha { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) DdrPairInfoAction( h2d_channel_id, d2h_channel_id, network_index, descriptors_per_frame, descs_count)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -662,7 +662,7 @@ Expected DdrPairInfoAction::serialize_params(const ContextResources &) c Expected StartDdrBufferingTaskAction::create() { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) StartDdrBufferingTaskAction()); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -685,7 +685,7 @@ Expected StartDdrBufferingTaskAction::serialize_params(const ContextReso Expected ResetDdrBufferingTaskAction::create() { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) ResetDdrBufferingTaskAction()); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -710,7 +710,7 @@ Expected ChangeVdmaToStreamMapping::create(const v { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) ChangeVdmaToStreamMapping(channel_id, stream_index, is_dummy_stream)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -740,7 +740,7 @@ Expected ChangeVdmaToStreamMapping::serialize_params(const ContextResour Expected WaitOutputTransferDoneAction::create(uint8_t stream_index) { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) WaitOutputTransferDoneAction(stream_index)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -757,7 +757,8 @@ bool WaitOutputTransferDoneAction::supports_repeated_block() const Expected WaitOutputTransferDoneAction::serialize_params(const ContextResources &context_resources) const { - const auto edge_layer = context_resources.get_edge_layer_by_stream_index(m_stream_index); + // D2H direction because it is output action + const auto edge_layer = context_resources.get_edge_layer_by_stream_index(m_stream_index, HAILO_D2H_STREAM); CHECK_EXPECTED(edge_layer); CONTEXT_SWITCH_DEFS__vdma_dataflow_interrupt_data_t params{}; @@ -770,7 +771,7 @@ Expected OpenBoundaryInputChannelAction::create(co { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) OpenBoundaryInputChannelAction(channel_id, host_buffer_info)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -801,7 +802,7 @@ Expected OpenBoundaryOutputChannelAction::create(c { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) OpenBoundaryOutputChannelAction(channel_id, host_buffer_info)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -847,7 +848,7 @@ Expected ActivateBoundaryInputChannelAction::creat { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) ActivateBoundaryInputChannelAction(channel_id, stream_index, nn_stream_config, host_buffer_info, initial_credit_size)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -886,7 +887,7 @@ Expected ActivateBoundaryOutputChannelAction::crea { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) ActivateBoundaryOutputChannelAction(channel_id, stream_index, nn_stream_config, host_buffer_info)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -923,7 +924,7 @@ Expected ActivateInterContextInputChannelAction::c { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) ActivateInterContextInputChannelAction(channel_id, stream_index, nn_stream_config, host_buffer_info, initial_credit_size)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -962,7 +963,7 @@ Expected ActivateInterContextOutputChannelAction:: { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) ActivateInterContextOutputChannelAction(channel_id, stream_index, network_index, nn_stream_config, host_buffer_info)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -1002,7 +1003,7 @@ Expected ActivateDdrInputChannelAction::create(con { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) ActivateDdrInputChannelAction(channel_id, stream_index, nn_stream_config, host_buffer_info, initial_credit_size, connected_d2h_channel_id)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -1044,7 +1045,7 @@ Expected ActivateDdrOutputChannelAction::create(co { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) ActivateDdrOutputChannelAction(channel_id, stream_index, nn_stream_config, host_buffer_info, buffered_rows_count)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -1084,7 +1085,7 @@ Expected ValidateChannelAction::create(const EdgeL edge_layer.layer_info.direction, is_inter_context, static_cast(edge_layer.buffer_info.buffer_type), edge_layer.layer_info.max_shmifo_size)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -1126,7 +1127,7 @@ Expected DeactivateChannelAction::create(const Edg edge_layer.layer_info.direction, is_inter_context, static_cast(edge_layer.buffer_info.buffer_type), edge_layer.layer_info.max_shmifo_size)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -1164,7 +1165,7 @@ Expected DeactivateChannelAction::serialize_params(const ContextResource Expected WaitDmaIdleAction::create(uint8_t stream_index) { auto result = ContextSwitchConfigActionPtr(new (std::nothrow) WaitDmaIdleAction(stream_index)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -1181,7 +1182,8 @@ bool WaitDmaIdleAction::supports_repeated_block() const Expected WaitDmaIdleAction::serialize_params(const ContextResources &context_resources) const { - const auto edge_layer = context_resources.get_edge_layer_by_stream_index(m_stream_index); + // D2H direction because it is output action + const auto edge_layer = context_resources.get_edge_layer_by_stream_index(m_stream_index, HAILO_D2H_STREAM); CHECK_EXPECTED(edge_layer); CONTEXT_SWITCH_DEFS__wait_dma_idle_data_t params{}; @@ -1198,7 +1200,7 @@ Expected WaitNmsIdleAction::create(uint8_t aggrega auto result = ContextSwitchConfigActionPtr(new (std::nothrow) WaitNmsIdleAction(aggregator_index, pred_cluster_ob_index, pred_cluster_ob_cluster_index, pred_cluster_ob_interface, succ_prepost_ob_index, succ_prepost_ob_interface)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } @@ -1231,17 +1233,20 @@ Expected WaitNmsIdleAction::serialize_params(const ContextResources &) c return Buffer::create(reinterpret_cast(¶ms), sizeof(params)); } -Expected EnableNmsAction::create(uint8_t nms_unit_index, uint8_t network_index) +Expected EnableNmsAction::create(uint8_t nms_unit_index, uint8_t network_index, uint16_t number_of_classes, + uint16_t burst_size) { - auto result = ContextSwitchConfigActionPtr(new (std::nothrow) EnableNmsAction(nms_unit_index, network_index)); - CHECK_AS_EXPECTED((nullptr != result), HAILO_OUT_OF_HOST_MEMORY); + auto result = ContextSwitchConfigActionPtr(new (std::nothrow) EnableNmsAction(nms_unit_index, network_index, number_of_classes, burst_size)); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); return result; } -EnableNmsAction::EnableNmsAction(uint8_t nms_unit_index, uint8_t network_index) : +EnableNmsAction::EnableNmsAction(uint8_t nms_unit_index, uint8_t network_index, uint16_t number_of_classes, uint16_t burst_size) : ContextSwitchConfigAction(ContextSwitchConfigAction::Type::EnableNms, CONTEXT_SWITCH_DEFS__ACTION_TYPE_ENABLE_NMS), m_nms_unit_index(nms_unit_index), - m_network_index(network_index) + m_network_index(network_index), + m_number_of_classes(number_of_classes), + m_burst_size(burst_size) {} Expected EnableNmsAction::serialize_params(const ContextResources &) const @@ -1249,6 +1254,8 @@ Expected EnableNmsAction::serialize_params(const ContextResources &) con CONTEXT_SWITCH_DEFS__enable_nms_action_t params{}; params.nms_unit_index = m_nms_unit_index; params.network_index = m_network_index; + params.number_of_classes = m_number_of_classes; + params.burst_size = m_burst_size; return Buffer::create(reinterpret_cast(¶ms), sizeof(params)); } @@ -1257,4 +1264,70 @@ bool EnableNmsAction::supports_repeated_block() const return true; } +Expected WriteDataByTypeAction::create(uint32_t address, uint8_t data_type, uint32_t data, + uint8_t shift, uint32_t mask, uint8_t network_index) +{ + auto result = ContextSwitchConfigActionPtr(new (std::nothrow) WriteDataByTypeAction(address, data_type, data, shift, mask, network_index)); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); + return result; +} + +WriteDataByTypeAction::WriteDataByTypeAction(uint32_t address, uint8_t data_type, uint32_t data, uint8_t shift, uint32_t mask, uint8_t network_index) : + ContextSwitchConfigAction(ContextSwitchConfigAction::Type::WriteDataByType, CONTEXT_SWITCH_DEFS__ACTION_TYPE_WRITE_DATA_BY_TYPE), + m_address(address), + m_data_type(data_type), + m_data(data), + m_shift(shift), + m_mask(mask), + m_network_index(network_index) +{} + +Expected WriteDataByTypeAction::serialize_params(const ContextResources &) const +{ + CONTEXT_SWITCH_DEFS__write_data_by_type_action_t params{}; + params.address = m_address; + params.data_type = m_data_type; + params.data = m_data; + params.shift = m_shift; + params.mask = m_mask; + params.network_index = m_network_index; + + return Buffer::create(reinterpret_cast(¶ms), sizeof(params)); +} + +bool WriteDataByTypeAction::supports_repeated_block() const +{ + return false; +} + +Expected SwitchLcuBatchAction::create(uint8_t cluster_index, uint8_t lcu_index, uint8_t network_index, + uint32_t kernel_done_count) +{ + auto result = ContextSwitchConfigActionPtr(new (std::nothrow) SwitchLcuBatchAction(cluster_index, lcu_index, network_index, kernel_done_count)); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); + return result; +} + +SwitchLcuBatchAction::SwitchLcuBatchAction(uint8_t cluster_index, uint8_t lcu_index, uint8_t network_index, uint32_t kernel_done_count) : + ContextSwitchConfigAction(Type::SwitchLcuBatch, CONTEXT_SWITCH_DEFS__ACTION_TYPE_SWITCH_LCU_BATCH), + m_cluster_index(cluster_index), + m_lcu_index(lcu_index), + m_network_index(network_index), + m_kernel_done_count(kernel_done_count) +{} + +bool SwitchLcuBatchAction::supports_repeated_block() const +{ + return true; +} + +Expected SwitchLcuBatchAction::serialize_params(const ContextResources &) const +{ + CONTEXT_SWITCH_DEFS__switch_lcu_batch_action_data_t params{}; + params.packed_lcu_id = pack_lcu_id(m_cluster_index, m_lcu_index); + params.network_index = m_network_index; + params.kernel_done_count = m_kernel_done_count; + return Buffer::create(reinterpret_cast(¶ms), sizeof(params)); +} + } /* namespace hailort */ diff --git a/hailort/libhailort/src/hef/context_switch_actions.hpp b/hailort/libhailort/src/hef/context_switch_actions.hpp index 155958f..defe31a 100644 --- a/hailort/libhailort/src/hef/context_switch_actions.hpp +++ b/hailort/libhailort/src/hef/context_switch_actions.hpp @@ -71,6 +71,8 @@ public: WaitDmaIdle, WaitNmsIdle, EnableNms, + WriteDataByType, + SwitchLcuBatch, }; ContextSwitchConfigAction(ContextSwitchConfigAction &&) = default; @@ -744,7 +746,8 @@ private: class EnableNmsAction : public ContextSwitchConfigAction { public: - static Expected create(uint8_t nms_unit_index, uint8_t network_index); + static Expected create(uint8_t nms_unit_index, uint8_t network_index, uint16_t number_of_classes, + uint16_t burst_size); EnableNmsAction(EnableNmsAction &&) = default; EnableNmsAction(const EnableNmsAction &) = delete; EnableNmsAction &operator=(EnableNmsAction &&) = delete; @@ -754,12 +757,58 @@ public: virtual Expected serialize_params(const ContextResources &context_resources) const override; private: - EnableNmsAction(uint8_t nms_unit_index, uint8_t network_index); + EnableNmsAction(uint8_t nms_unit_index, uint8_t network_index, uint16_t number_of_classes, uint16_t burst_size); const uint8_t m_nms_unit_index; const uint8_t m_network_index; + const uint16_t m_number_of_classes; + const uint16_t m_burst_size; }; +class WriteDataByTypeAction : public ContextSwitchConfigAction +{ +public: + static Expected create(uint32_t address, uint8_t data_type, uint32_t data, + uint8_t shift, uint32_t mask, uint8_t network_index); + + virtual bool supports_repeated_block() const override; + virtual Expected serialize_params(const ContextResources &context_resources) const override; + +private: + WriteDataByTypeAction(uint32_t address, uint8_t data_type, uint32_t data, uint8_t shift, uint32_t mask, uint8_t network_index); + + const uint32_t m_address; + const uint8_t m_data_type; + const uint32_t m_data; + const uint8_t m_shift; + const uint32_t m_mask; + const uint8_t m_network_index; + +}; + +class SwitchLcuBatchAction : public ContextSwitchConfigAction +{ +public: + static Expected create(uint8_t cluster_index, uint8_t lcu_index, uint8_t network_index, + uint32_t kernel_done_count); + SwitchLcuBatchAction(SwitchLcuBatchAction &&) = default; + SwitchLcuBatchAction(const SwitchLcuBatchAction &) = delete; + SwitchLcuBatchAction &operator=(SwitchLcuBatchAction &&) = delete; + SwitchLcuBatchAction &operator=(const SwitchLcuBatchAction &) = delete; + virtual ~SwitchLcuBatchAction() = default; + virtual bool supports_repeated_block() const override; + virtual Expected serialize_params(const ContextResources &context_resources) const override; + +private: + SwitchLcuBatchAction(uint8_t cluster_index, uint8_t lcu_index, uint8_t network_index, uint32_t kernel_done_count); + + const uint8_t m_cluster_index; + const uint8_t m_lcu_index; + const uint8_t m_network_index; + const uint32_t m_kernel_done_count; +}; + + } /* namespace hailort */ #endif /* _HAILO_CONTEXT_SWITCH_ACTIONS_HPP_ */ diff --git a/hailort/libhailort/src/hef/core_op_metadata.cpp b/hailort/libhailort/src/hef/core_op_metadata.cpp index 8d148d9..e10f415 100644 --- a/hailort/libhailort/src/hef/core_op_metadata.cpp +++ b/hailort/libhailort/src/hef/core_op_metadata.cpp @@ -8,6 +8,7 @@ **/ #include "core_op_metadata.hpp" +#include "hef_internal.hpp" #include namespace hailort @@ -181,25 +182,13 @@ CoreOpMetadata::CoreOpMetadata(const std::string &core_op_name, ContextMetadata &&preliminary_context, std::vector &&dynamic_contexts, std::vector &&config_channels_info, - std::vector &&sorted_output_names, SupportedFeatures &supported_features, - const std::vector &sorted_network_names) + std::vector sorted_network_names) : m_preliminary_context(std::move(preliminary_context)), m_dynamic_contexts(std::move(dynamic_contexts)), m_config_channels_info(std::move(config_channels_info)), - m_core_op_name(core_op_name), m_sorted_output_names(std::move(sorted_output_names)), - m_supported_features(supported_features), m_sorted_network_names(sorted_network_names) {} - -Expected CoreOpMetadata::get_layer_info_by_stream_name(const std::string &stream_name) const -{ - for (auto layer_info : get_all_layer_infos()) { - if (layer_info.name == stream_name) { - return layer_info; - } - } - LOGGER__ERROR("Failed to find layer with name {}", stream_name); - return make_unexpected(HAILO_NOT_FOUND); -} + m_core_op_name(core_op_name), m_supported_features(supported_features), + m_sorted_network_names(sorted_network_names) {} std::vector CoreOpMetadata::get_input_layer_infos() const { @@ -301,18 +290,24 @@ Expected> CoreOpMetadata::get_all_layer_infos(const std:: Expected> CoreOpMetadata::get_input_stream_infos(const std::string &network_name) const { - auto input_layer_infos = get_input_layer_infos(network_name); - CHECK_EXPECTED(input_layer_infos); - - return convert_layer_infos_to_stream_infos(input_layer_infos.value()); + std::vector res; + auto input_layers = get_input_layer_infos(network_name); + CHECK_EXPECTED(input_layers); + for (auto &layer_info : input_layers.value()) { + res.push_back(LayerInfoUtils::get_stream_info_from_layer_info(layer_info)); + } + return res; } Expected> CoreOpMetadata::get_output_stream_infos(const std::string &network_name) const { - auto output_layer_infos = get_output_layer_infos(network_name); - CHECK_EXPECTED(output_layer_infos); - - return convert_layer_infos_to_stream_infos(output_layer_infos.value()); + std::vector res; + auto output_layers = get_output_layer_infos(network_name); + CHECK_EXPECTED(output_layers); + for (auto &layer_info : output_layers.value()) { + res.push_back(LayerInfoUtils::get_stream_info_from_layer_info(layer_info)); + } + return res; } Expected> CoreOpMetadata::get_all_stream_infos(const std::string &network_name) const @@ -331,42 +326,92 @@ Expected> CoreOpMetadata::get_all_stream_infos( return res; } -Expected> CoreOpMetadata::get_input_vstream_infos(const std::string &network_name) const + +size_t CoreOpMetadata::get_contexts_count() { - auto input_layer_infos = get_input_layer_infos(network_name); - CHECK_EXPECTED(input_layer_infos); + return (m_dynamic_contexts.size() + CONTROL_PROTOCOL__CONTEXT_SWITCH_NUMBER_OF_NON_DYNAMIC_CONTEXTS); +} - return convert_layer_infos_to_vstream_infos(input_layer_infos.value()); +Expected CoreOpMetadata::get_total_transfer_size() +{ + size_t total_transfer_size = 0; + for (const auto &dynamic_context : m_dynamic_contexts) { + auto context_size = dynamic_context.get_context_transfer_size(); + CHECK_EXPECTED(context_size); + total_transfer_size += context_size.release(); + } + return total_transfer_size; } -Expected> CoreOpMetadata::get_output_vstream_infos(const std::string &network_name) const +Expected CoreOpMetadataPerArch::get_metadata(uint32_t partial_clusters_layout_bitmap) const { - std::vector res; - if (m_supported_features.hailo_net_flow) { - res = m_output_vstreams_infos; - return res; + if (PARTIAL_CLUSTERS_LAYOUT_IGNORE == partial_clusters_layout_bitmap) { + // Passing PARTIAL_CLUSTERS_LAYOUT_IGNORE is magic for getting one of the metadata + assert(0 != m_metadata_per_arch.size()); + auto result = m_metadata_per_arch.begin()->second; + return result; } - auto expected_output_layer_infos = get_output_layer_infos(network_name); - CHECK_EXPECTED(expected_output_layer_infos); - auto output_layer_infos = expected_output_layer_infos.release(); + if (contains(m_metadata_per_arch, partial_clusters_layout_bitmap)) { + auto result = m_metadata_per_arch.at(partial_clusters_layout_bitmap); + return result; + } + LOGGER__ERROR("CoreOpPerArch does not contain metadata for partial_clusters_layout_bitmap {}", partial_clusters_layout_bitmap); + return make_unexpected(HAILO_INTERNAL_FAILURE); +} - res = convert_layer_infos_to_vstream_infos(output_layer_infos); +void CoreOpMetadataPerArch::add_metadata(const CoreOpMetadataPtr &metadata, uint32_t partial_clusters_layout_bitmap) +{ + m_metadata_per_arch[partial_clusters_layout_bitmap] = metadata; +} +Expected NetworkGroupMetadata::create(const std::string &network_group_name, + std::map &&core_ops_metadata_per_arch, std::vector &sorted_output_names, + SupportedFeatures &supported_features, const std::vector &sorted_network_names, + std::vector> &net_flow_ops) +{ + auto all_layers_infos = get_all_layer_infos(core_ops_metadata_per_arch); + CHECK_EXPECTED(all_layers_infos); + + std::vector input_vstream_infos; + std::vector output_vstream_infos; + for (auto &layer_info : all_layers_infos.value()) { + if (std::any_of(net_flow_ops.begin(), net_flow_ops.end(), + [&layer_info](auto &op) { return contains(op->input_streams, layer_info.name); })) { + continue; // all output_vstream_infos that relates to the op are coming from the op itself instead of layer_infos + } + auto vstreams_info = LayerInfoUtils::get_vstream_infos_from_layer_info(layer_info); + if (HAILO_D2H_STREAM == layer_info.direction) { + // In case of fused nms layers, several LayerInfos will contain data about the same fused layer + for (auto &vstream_info : vstreams_info) { + if (!LayerInfoUtils::vstream_info_already_in_vector(output_vstream_infos, vstream_info.name)) { + output_vstream_infos.push_back(vstream_info); + } + } + } else { + input_vstream_infos.insert(input_vstream_infos.end(), + std::make_move_iterator(vstreams_info.begin()), std::make_move_iterator(vstreams_info.end())); + } + } + for (auto &op : net_flow_ops) { + output_vstream_infos.push_back(op->output_vstream_info); + } + + // Sort vstream infos by sorted_output_names hailo_status status = HAILO_SUCCESS; - std::sort(res.begin(), res.end(), - [this, &status](const auto &info1, const auto &info2) + std::sort(output_vstream_infos.begin(), output_vstream_infos.end(), + [&sorted_output_names, &status](const auto &info1, const auto &info2) { - const auto index1 = std::find(m_sorted_output_names.begin(), m_sorted_output_names.end(), std::string(info1.name)); - const auto index2 = std::find(m_sorted_output_names.begin(), m_sorted_output_names.end(), std::string(info2.name)); + const auto index1 = std::find(sorted_output_names.begin(), sorted_output_names.end(), std::string(info1.name)); + const auto index2 = std::find(sorted_output_names.begin(), sorted_output_names.end(), std::string(info2.name)); - if (m_sorted_output_names.end() == index1) { - LOGGER__ERROR("Stream {} not found in sorted output names", info1.name); + if (sorted_output_names.end() == index1) { + LOGGER__ERROR("VStream {} not found in sorted output names", info1.name); status = HAILO_INTERNAL_FAILURE; return false; } - if (m_sorted_output_names.end() == index2) { - LOGGER__ERROR("Stream {} not found in sorted output names", info2.name); + if (sorted_output_names.end() == index2) { + LOGGER__ERROR("VStream {} not found in sorted output names", info2.name); status = HAILO_INTERNAL_FAILURE; return false; } @@ -375,10 +420,37 @@ Expected> CoreOpMetadata::get_output_vstream_i }); CHECK_SUCCESS_AS_EXPECTED(status); + return NetworkGroupMetadata(network_group_name, std::move(core_ops_metadata_per_arch), sorted_output_names, supported_features, sorted_network_names, + input_vstream_infos, output_vstream_infos, net_flow_ops); +} + +Expected> NetworkGroupMetadata::get_input_vstream_infos(const std::string &network_name) const +{ + std::vector res; + for (auto &vstream_info : m_input_vstreams_infos) { + if ((network_name == std::string(vstream_info.network_name)) || (network_name.empty()) || (network_name == default_network_name())) { + res.push_back(vstream_info); + } + } + CHECK_AS_EXPECTED(0 != res.size(), HAILO_NOT_FOUND, "No VStreams where found for network {}", network_name); + return res; } -Expected> CoreOpMetadata::get_all_vstream_infos(const std::string &network_name) const +Expected> NetworkGroupMetadata::get_output_vstream_infos(const std::string &network_name) const +{ + std::vector res; + for (auto &vstream_info : m_output_vstreams_infos) { + if ((network_name == std::string(vstream_info.network_name)) || (network_name.empty()) || (network_name == default_network_name())) { + res.push_back(vstream_info); + } + } + CHECK_AS_EXPECTED(0 != res.size(), HAILO_NOT_FOUND, "No VStreams where found for network {}", network_name); + + return res; +} + +Expected> NetworkGroupMetadata::get_all_vstream_infos(const std::string &network_name) const { auto input_vstream_infos = get_input_vstream_infos(network_name); CHECK_EXPECTED(input_vstream_infos); @@ -394,10 +466,21 @@ Expected> CoreOpMetadata::get_all_vstream_info return res; } -Expected> CoreOpMetadata::get_vstream_names_from_stream_name(const std::string &stream_name) const +Expected> NetworkGroupMetadata::get_vstream_names_from_stream_name(const std::string &stream_name) { std::vector results; - for (auto &layer_info : get_all_layer_infos()) { + for (auto &pp : m_net_flow_ops) { + if (contains(pp->input_streams, stream_name)) { + for (auto &output_metadata : pp->op->outputs_metadata()) { + results.push_back(output_metadata.first); + } + return results; + } + } + + auto all_layers_infos = get_all_layer_infos(m_core_ops_metadata_per_arch); + CHECK_EXPECTED(all_layers_infos); + for (auto &layer_info : all_layers_infos.release()) { if (stream_name == layer_info.name) { if (layer_info.is_defused_nms) { return std::vector (1, layer_info.fused_nms_layer[0].name); @@ -411,10 +494,21 @@ Expected> CoreOpMetadata::get_vstream_names_from_stream return make_unexpected(HAILO_NOT_FOUND); } -Expected> CoreOpMetadata::get_stream_names_from_vstream_name(const std::string &vstream_name) const +Expected> NetworkGroupMetadata::get_stream_names_from_vstream_name(const std::string &vstream_name) { std::vector results; - for (auto &layer_info : get_all_layer_infos()) { + for (auto &pp : m_net_flow_ops) { + if (contains(pp->op->outputs_metadata(), vstream_name)) { + for (auto &input_name : pp->input_streams) { + results.push_back(input_name); + } + return results; + } + } + + auto all_layers_infos = get_all_layer_infos(m_core_ops_metadata_per_arch); + CHECK_EXPECTED(all_layers_infos); + for (auto &layer_info : all_layers_infos.release()) { if (layer_info.is_mux) { if (is_edge_under_mux(layer_info, vstream_name)) { // vstream_name is a demux of the layer info @@ -436,31 +530,7 @@ Expected> CoreOpMetadata::get_stream_names_from_vstream return results; } -std::vector CoreOpMetadata::convert_layer_infos_to_stream_infos(const std::vector &layer_infos) const -{ - std::vector res; - for (auto &layer_info : layer_infos) { - res.push_back(LayerInfoUtils::get_stream_info_from_layer_info(layer_info)); - } - return res; -} - -std::vector CoreOpMetadata::convert_layer_infos_to_vstream_infos(const std::vector &layer_infos) const -{ - std::vector res; - for (auto &layer_info : layer_infos) { - auto vstream_infos = LayerInfoUtils::get_vstream_infos_from_layer_info(layer_info); - for (const auto &vstream_info : vstream_infos) { - // In case of fused nms layers, several LayerInfos will contain data about the same fused layer - if (!LayerInfoUtils::vstream_info_already_in_vector(res, vstream_info.name)) { - res.push_back(vstream_info); - } - } - } - return res; -} - -Expected> CoreOpMetadata::get_network_infos() const +Expected> NetworkGroupMetadata::get_network_infos() const { std::vector network_infos; network_infos.reserve(m_sorted_network_names.size()); @@ -476,41 +546,4 @@ Expected> CoreOpMetadata::get_network_infos() return network_infos; } -size_t CoreOpMetadata::get_contexts_count() -{ - return (m_dynamic_contexts.size() + CONTROL_PROTOCOL__CONTEXT_SWITCH_NUMBER_OF_NON_DYNAMIC_CONTEXTS); -} - -Expected CoreOpMetadata::get_total_transfer_size() -{ - size_t total_transfer_size = 0; - for (const auto &dynamic_context : m_dynamic_contexts) { - auto context_size = dynamic_context.get_context_transfer_size(); - CHECK_EXPECTED(context_size); - total_transfer_size += context_size.release(); - } - return total_transfer_size; -} - -Expected CoreOpMetadataPerArch::get_metadata(uint32_t partial_clusters_layout_bitmap) -{ - if (PARTIAL_CLUSTERS_LAYOUT_IGNORE == partial_clusters_layout_bitmap) { - // Passing PARTIAL_CLUSTERS_LAYOUT_IGNORE is magic for getting one of the metadata - assert(0 != m_metadata_per_arch.size()); - auto result = m_metadata_per_arch.begin()->second; - return result; - } - if (contains(m_metadata_per_arch, partial_clusters_layout_bitmap)) { - auto result = m_metadata_per_arch[partial_clusters_layout_bitmap]; - return result; - } - LOGGER__ERROR("CoreOpPerArch does not contain metadata for partial_clusters_layout_bitmap {}", partial_clusters_layout_bitmap); - return make_unexpected(HAILO_INTERNAL_FAILURE); -} - -void CoreOpMetadataPerArch::add_metadata(const CoreOpMetadata &metadata, uint32_t partial_clusters_layout_bitmap) -{ - m_metadata_per_arch[partial_clusters_layout_bitmap] = metadata; -} - } /* namespace hailort */ diff --git a/hailort/libhailort/src/hef/core_op_metadata.hpp b/hailort/libhailort/src/hef/core_op_metadata.hpp index d725524..b449679 100644 --- a/hailort/libhailort/src/hef/core_op_metadata.hpp +++ b/hailort/libhailort/src/hef/core_op_metadata.hpp @@ -25,6 +25,10 @@ struct SupportedFeatures { bool multi_context = false; bool preliminary_run_asap = false; bool hailo_net_flow = false; + bool dual_direction_stream_index = false; + bool nms_burst_mode = false; + bool output_scale_by_feature = false; + bool periph_calculation_in_hailort = false; }; // For each config_stream_index we store vector of all ccw write length. The vector is used to build the config buffer.g @@ -33,7 +37,6 @@ using ConfigBufferInfoMap = std::unordered_map>; class ContextMetadata final { public: - ContextMetadata() = default; // TODO HRT-8478: remove ContextMetadata(std::vector &&actions, ConfigBufferInfoMap&& config_buffers_info); @@ -74,14 +77,12 @@ struct ConfigChannelInfo { class CoreOpMetadata final { public: - CoreOpMetadata() = default; // TODO HRT-8478: remove CoreOpMetadata(const std::string &core_op_name, ContextMetadata &&preliminary_context, std::vector &&dynamic_contexts, std::vector &&config_channels_info, - std::vector &&sorted_output_names, SupportedFeatures &supported_features, - const std::vector &sorted_network_names); + std::vector sorted_network_names); std::vector get_input_layer_infos() const; std::vector get_output_layer_infos() const; @@ -90,38 +91,112 @@ public: Expected> get_input_layer_infos(const std::string &network_name) const; Expected> get_output_layer_infos(const std::string &network_name) const; Expected> get_all_layer_infos(const std::string &network_name) const; - Expected get_layer_info_by_stream_name(const std::string &stream_name) const; const ContextMetadata &preliminary_context() const; const std::vector &dynamic_contexts() const; const std::vector &config_channels_info() const; + // TODO: Move stream infos into NetworkGroupMetadata Expected> get_input_stream_infos(const std::string &network_name = "") const; Expected> get_output_stream_infos(const std::string &network_name = "") const; Expected> get_all_stream_infos(const std::string &network_name = "") const; - // TODO: HRT-9546 - Remove, should only be in CNG + size_t get_contexts_count(); + + const std::string &core_op_name() const + { + return m_core_op_name; + } + + const SupportedFeatures &supported_features() const + { + return m_supported_features; + } + + Expected get_total_transfer_size(); + + // TODO: Remove + const std::vector &get_network_names() const + { + return m_sorted_network_names; + } + +private: + // TODO: Remove + const std::string default_network_name() const + { + return HailoRTDefaults::get_network_name(m_core_op_name); + } + + ContextMetadata m_preliminary_context; + std::vector m_dynamic_contexts; + std::vector m_config_channels_info; + std::string m_core_op_name; + SupportedFeatures m_supported_features; + std::vector m_sorted_network_names; +}; + +using CoreOpMetadataPtr = std::shared_ptr; + +class CoreOpMetadataPerArch final +{ +public: + CoreOpMetadataPerArch() = default; + + Expected get_metadata(uint32_t partial_clusters_layout_bitmap) const; + void add_metadata(const CoreOpMetadataPtr &metadata, uint32_t partial_clusters_layout_bitmap); + +private: + std::map m_metadata_per_arch; +}; + +struct NetFlowElement; + +class NetworkGroupMetadata final { +public: + static Expected create(const std::string &network_group_name, + std::map &&core_ops_metadata_per_arch, + std::vector &sorted_output_names, + SupportedFeatures &supported_features, + const std::vector &sorted_network_names, + std::vector> &net_flow_ops); + + NetworkGroupMetadata(const std::string &network_group_name, + std::map &&core_ops_metadata_per_arch, + std::vector &sorted_output_names, + SupportedFeatures &supported_features, + const std::vector &sorted_network_names, + std::vector &input_vstreams_infos, + std::vector &output_vstreams_infos, + std::vector> &net_flow_ops) : + m_network_group_name(network_group_name), + m_sorted_output_names(sorted_output_names), + m_supported_features(supported_features), + m_sorted_network_names(sorted_network_names), + m_input_vstreams_infos(input_vstreams_infos), + m_output_vstreams_infos(output_vstreams_infos), + m_core_ops_metadata_per_arch(std::move(core_ops_metadata_per_arch)), + m_net_flow_ops(net_flow_ops) + {}; + Expected> get_input_vstream_infos(const std::string &network_name = "") const; Expected> get_output_vstream_infos(const std::string &network_name = "") const; Expected> get_all_vstream_infos(const std::string &network_name = "") const; - // TODO: HRT-9546 - Remove, should only be in CNG - need to decide if relevant only for one CoreOp case. - Expected> get_vstream_names_from_stream_name(const std::string &stream_name) const; - Expected> get_stream_names_from_vstream_name(const std::string &vstream_name) const; + Expected> get_vstream_names_from_stream_name(const std::string &stream_name); + Expected> get_stream_names_from_vstream_name(const std::string &vstream_name); Expected> get_network_infos() const; - size_t get_contexts_count(); - - const std::string &core_op_name() const + const std::string &name() const { - return m_core_op_name; + return m_network_group_name; } const std::string default_network_name() const { - return HailoRTDefaults::get_network_name(m_core_op_name); + return HailoRTDefaults::get_network_name(m_network_group_name); } const std::vector get_sorted_output_names() const @@ -129,7 +204,6 @@ public: return m_sorted_output_names; } - // duplicated for each CoreOp const SupportedFeatures &supported_features() const { return m_supported_features; @@ -140,41 +214,31 @@ public: return m_sorted_network_names; } - // TODO: HRT-9546 - Move to CNG - void add_output_vstream_info(const hailo_vstream_info_t &output_vstream_info) { - m_output_vstreams_infos.push_back(output_vstream_info); - } - - Expected get_total_transfer_size(); - private: - std::vector convert_layer_infos_to_stream_infos(const std::vector &layer_infos) const; - std::vector convert_layer_infos_to_vstream_infos(const std::vector &layer_infos) const; + static Expected> get_all_layer_infos(std::map &core_ops_metadata_per_arch) + /* This function is used for names getters (such as get_vstream_names_from_stream_name), + so should be same across all clusters layouts */ + { + CHECK_AS_EXPECTED(1 == core_ops_metadata_per_arch.size(), HAILO_INTERNAL_FAILURE); + auto core_op_metadata = core_ops_metadata_per_arch.begin()->second.get_metadata(PARTIAL_CLUSTERS_LAYOUT_IGNORE); + CHECK_EXPECTED(core_op_metadata); - ContextMetadata m_preliminary_context; - std::vector m_dynamic_contexts; - std::vector m_config_channels_info; - std::string m_core_op_name; + return core_op_metadata.value()->get_all_layer_infos(); + } + + std::string m_network_group_name; std::vector m_sorted_output_names; SupportedFeatures m_supported_features; std::vector m_sorted_network_names; - // TODO: remove this from here! NetworkGroupMetadata should be CoreOpMetadata and contain no net_flow information! (HRT-9546) - // To add insult to injury, this is being constructed lazyly by add_output_layer_info - std::vector m_output_vstreams_infos; // Valid only in case of post process -}; + std::vector m_input_vstreams_infos; + std::vector m_output_vstreams_infos; + std::map m_core_ops_metadata_per_arch; // Key is core_op_name + std::vector> m_net_flow_ops; -class CoreOpMetadataPerArch final -{ -public: - CoreOpMetadataPerArch() = default; - - Expected get_metadata(uint32_t partial_clusters_layout_bitmap); - void add_metadata(const CoreOpMetadata &metadata, uint32_t partial_clusters_layout_bitmap); - -private: - std::map m_metadata_per_arch; + friend class Hef; + friend class ConfiguredNetworkGroupBase; }; } /* namespace hailort */ diff --git a/hailort/libhailort/src/hef/hef.cpp b/hailort/libhailort/src/hef/hef.cpp index cd543a0..f719c72 100644 --- a/hailort/libhailort/src/hef/hef.cpp +++ b/hailort/libhailort/src/hef/hef.cpp @@ -23,13 +23,17 @@ #include "net_flow/ops/nms_post_process.hpp" #include "net_flow/ops/yolo_post_process.hpp" +#include "net_flow/ops/yolox_post_process.hpp" #include "net_flow/ops/ssd_post_process.hpp" +#include "net_flow/ops/argmax_post_process.hpp" +#include "net_flow/ops/softmax_post_process.hpp" #include "hef/hef_internal.hpp" #include "vdma/pcie/pcie_device.hpp" #include "vdma/vdma_config_manager.hpp" #include "eth/hcp_config_core_op.hpp" #include "hef/layer_info.hpp" #include "device_common/control.hpp" +#include "stream_common/nms_stream_reader.hpp" #include "byte_order.h" #include "context_switch_defs.h" @@ -51,6 +55,8 @@ namespace hailort #define HEF__MD5_BUFFER_SIZE (1024) #define DEFAULT_BATCH_SIZE (1) #define SKIP_SPACE_COMMA_CHARACTERS (2) +#define ALIGNED_TO_4_BYTES (4) +#define DEFAULT_NMS_NO_BURST_SIZE (1) static const uint8_t ENABLE_LCU_CONTROL_WORD[4] = {1, 0, 0, 0}; @@ -487,26 +493,50 @@ hailo_status Hef::Impl::fill_networks_metadata() { fill_extensions_bitset(); - CoreOpMetadataPerArch metadata; + CoreOpMetadataPerArch core_op_metadata; uint32_t partial_clusters_layout_bitmap = 0; for (auto &network_group : m_groups) { + // Prepare core_op_metadata auto network_group_name = HefUtils::get_network_group_name(*network_group, m_supported_features); // TODO: keep metadata per core_op (HRT-9551) const auto &core_ops = m_core_ops_per_group[network_group_name]; assert(core_ops.size() == 1); const auto &core_op = core_ops[0]; + + // TODO: Clean this code after hef.proto refactor + std::vector sorted_network_names; + if (m_supported_features.multi_network_support) { + if (0 != network_group->networks_names_size()) { + sorted_network_names.reserve(core_op.networks_names.size()); + for (auto &partial_network_name : core_op.networks_names) { + auto network_name = HefUtils::get_network_name(network_group_name, partial_network_name); + sorted_network_names.push_back(network_name); + } + } else if (0 != network_group->partial_network_groups_size()) { + sorted_network_names.reserve(network_group->partial_network_groups().begin()->network_group().networks_names_size()); + for (auto &partial_network_name : network_group->partial_network_groups().begin()->network_group().networks_names()) { + auto network_name = HefUtils::get_network_name(network_group_name, partial_network_name); + sorted_network_names.push_back(network_name); + } + } + } + if (sorted_network_names.empty()) { + sorted_network_names.push_back(HailoRTDefaults::get_network_name(network_group_name)); + } + if (ProtoHEFHwArch::PROTO__HW_ARCH__HAILO8L == get_device_arch()) { if (m_supported_features.hailo_net_flow) { for (auto &partial_core_op : core_op.partial_core_ops) { partial_clusters_layout_bitmap = partial_core_op->layout.partial_clusters_layout_bitmap(); - auto metadata_per_arch = create_metadata_per_arch(*(partial_core_op->core_op)); - CHECK_EXPECTED_AS_STATUS(metadata_per_arch); - auto &&arch_metadata = metadata_per_arch.release(); - auto expected_net_flow_ops = create_net_flow_ops(*network_group, arch_metadata); + auto metadata_per_arch_exp = create_metadata_per_arch(*(partial_core_op->core_op), sorted_network_names); + CHECK_EXPECTED_AS_STATUS(metadata_per_arch_exp); + auto metadata_per_arch = metadata_per_arch_exp.release(); + + auto expected_net_flow_ops = create_net_flow_ops(*network_group, *metadata_per_arch, get_device_arch()); CHECK_EXPECTED_AS_STATUS(expected_net_flow_ops); - m_post_process_ops_per_group.insert({arch_metadata.core_op_name(), expected_net_flow_ops.value()}); - metadata.add_metadata(arch_metadata, partial_clusters_layout_bitmap); + m_post_process_ops_per_group.insert({metadata_per_arch->core_op_name(), expected_net_flow_ops.value()}); + core_op_metadata.add_metadata(metadata_per_arch, partial_clusters_layout_bitmap); } } else { for (auto &partial_network_group : network_group->partial_network_groups()) { @@ -520,27 +550,72 @@ hailo_status Hef::Impl::fill_networks_metadata() partial_network_group.network_group().networks_names(), {} }; - auto metadata_per_arch = create_metadata_per_arch(partial_core_op); - CHECK_EXPECTED_AS_STATUS(metadata_per_arch); - auto &&arch_metadata = metadata_per_arch.release(); + + auto metadata_per_arch_exp = create_metadata_per_arch(partial_core_op, sorted_network_names); + CHECK_EXPECTED_AS_STATUS(metadata_per_arch_exp); + auto metadata_per_arch = metadata_per_arch_exp.release(); + std::vector> empty_ops; - m_post_process_ops_per_group.insert({arch_metadata.core_op_name(), empty_ops}); - metadata.add_metadata(arch_metadata, partial_clusters_layout_bitmap); + m_post_process_ops_per_group.insert({metadata_per_arch->core_op_name(), empty_ops}); + core_op_metadata.add_metadata(metadata_per_arch, partial_clusters_layout_bitmap); } } } else { partial_clusters_layout_bitmap = PARTIAL_CLUSTERS_LAYOUT_IGNORE; - auto metadata_per_arch = create_metadata_per_arch(core_op); - CHECK_EXPECTED_AS_STATUS(metadata_per_arch); - auto &&arch_metadata = metadata_per_arch.release(); - auto expected_net_flow_ops = create_net_flow_ops(*network_group, arch_metadata); + auto metadata_per_arch_exp = create_metadata_per_arch(core_op, sorted_network_names); + CHECK_EXPECTED_AS_STATUS(metadata_per_arch_exp); + auto metadata_per_arch = metadata_per_arch_exp.release(); + + auto expected_net_flow_ops = create_net_flow_ops(*network_group, *metadata_per_arch, get_device_arch()); CHECK_EXPECTED_AS_STATUS(expected_net_flow_ops); - m_post_process_ops_per_group.insert({arch_metadata.core_op_name(), expected_net_flow_ops.value()}); - metadata.add_metadata(arch_metadata, partial_clusters_layout_bitmap); + m_post_process_ops_per_group.insert({metadata_per_arch->core_op_name(), expected_net_flow_ops.value()}); + core_op_metadata.add_metadata(metadata_per_arch, partial_clusters_layout_bitmap); } - CHECK(!contains(m_core_op_per_arch, network_group_name), + + // Taking the full-layout's name (name is same across all layouts) + auto metadata_exp = core_op_metadata.get_metadata(PARTIAL_CLUSTERS_LAYOUT_IGNORE); + CHECK_EXPECTED_AS_STATUS(metadata_exp); + auto core_op_name = metadata_exp.value()->core_op_name(); + std::map core_op_metadata_map; + core_op_metadata_map[core_op_name] = core_op_metadata; + // Prepare network_group_metadata + CHECK(!contains(m_network_group_metadata, network_group_name), HAILO_INVALID_OPERATION, "Network group with the name {} is already configured on the device", network_group_name); - m_core_op_per_arch.emplace(network_group_name, metadata); + + // TODO: Clean this code after hef.proto refactor + std::vector sorted_output_names; + if (core_op.fused_layers_metadata.network_has_fused_layers()) { + // If the model has fused layers, updated sorted_output_names is under the fused layer metadata + for (auto &name : core_op.fused_layers_metadata.updated_sorted_output_names()) { + sorted_output_names.push_back(name); + } + } else if(!m_supported_features.hailo_net_flow && (0 != network_group->partial_network_groups_size()) && + (network_group->partial_network_groups().begin()->network_group().sorted_outputs_order_size())) { + // If the model doesnt support net_flow, its possible that sorted output names will be under the partial_network_groups metadata + for (auto &name : network_group->partial_network_groups().begin()->network_group().sorted_outputs_order()) { + sorted_output_names.push_back(name); + } + } else if (0 != network_group->sorted_outputs_order_size()) { + // Most cases should fall here - either net_flow is supported, or network_group->sorted_outputs_order() has values + for (auto &name : network_group->sorted_outputs_order()) { + sorted_output_names.push_back(name); + } + } else { + // For very old HEFs, sorted_output_names might be in the last context's metadata + uint32_t number_of_contexts = core_op.contexts.size(); + const auto& context_metadata = core_op.contexts[number_of_contexts - 1].metadata(); + CHECK(0 < context_metadata.sorted_outputs_order_size(), HAILO_INVALID_HEF, + "Sorted output names is not set up in the HEF."); + for (auto &name : context_metadata.sorted_outputs_order()) { + sorted_output_names.push_back(name); + } + } + + auto network_group_metadata = NetworkGroupMetadata::create(network_group_name, std::move(core_op_metadata_map), + sorted_output_names, m_supported_features, sorted_network_names, m_post_process_ops_per_group.at(network_group_name)); + + CHECK_EXPECTED_AS_STATUS(network_group_metadata); + m_network_group_metadata.emplace(network_group_name, network_group_metadata.release()); } return HAILO_SUCCESS; } @@ -578,36 +653,22 @@ static Expected> parse_config_channels_info(const return config_channels_info; } -Expected Hef::Impl::create_metadata_per_arch(const ProtoHEFCoreOpMock &core_op) +Expected Hef::Impl::create_metadata_per_arch(const ProtoHEFCoreOpMock &core_op, const std::vector &sorted_network_names) { auto preliminary_context = HefUtils::parse_preliminary_context(core_op.preliminary_config, m_supported_features); CHECK_EXPECTED(preliminary_context); - auto dynamic_contexts = HefUtils::parse_dynamic_contexts(core_op, m_supported_features); + auto dynamic_contexts = HefUtils::parse_dynamic_contexts(core_op, m_supported_features, get_device_arch()); CHECK_EXPECTED(dynamic_contexts); auto config_channels_info = parse_config_channels_info(core_op); CHECK_EXPECTED(config_channels_info); - auto sorted_output_names = HefUtils::get_sorted_output_names(core_op); - CHECK_EXPECTED(sorted_output_names); - - std::vector sorted_network_names; - if (m_supported_features.multi_network_support) { - sorted_network_names.reserve(core_op.networks_names.size()); - for (auto &partial_network_name : core_op.networks_names) { - auto network_name = HefUtils::get_network_name(core_op, partial_network_name); - sorted_network_names.push_back(network_name); - } - } else { - sorted_network_names.push_back(HailoRTDefaults::get_network_name(core_op.network_group_metadata.network_group_name())); - } - // Currently, CoreOp name is the same as network_group_name, thats why we init it with it. // TODO: HRT-9551 - Change it when supporting multi core ops. - CoreOpMetadata metadata_per_arch(core_op.network_group_metadata.network_group_name(), - preliminary_context.release(), dynamic_contexts.release(), config_channels_info.release(), - sorted_output_names.release(), m_supported_features, sorted_network_names); + auto metadata_per_arch = make_shared_nothrow(core_op.network_group_metadata.network_group_name(), + preliminary_context.release(), dynamic_contexts.release(), config_channels_info.release(), m_supported_features, sorted_network_names); + CHECK_NOT_NULL_AS_EXPECTED(metadata_per_arch, HAILO_OUT_OF_HOST_MEMORY); return metadata_per_arch; } @@ -772,6 +833,14 @@ SupportedFeatures Hef::Impl::get_supported_features(const ProtoHEFHeader &header header, hef_extensions, included_features); supported_features.hailo_net_flow = check_hef_extension(ProtoHEFExtensionType::HAILO_NET_FLOW, header, hef_extensions, included_features); + supported_features.dual_direction_stream_index = check_hef_extension(ProtoHEFExtensionType::DUAL_DIRECTION_STREAM_INDEX, + header, hef_extensions, included_features); + supported_features.nms_burst_mode = check_hef_extension(ProtoHEFExtensionType::NMS_OUTPUT_BURST, + header, hef_extensions, included_features); + supported_features.output_scale_by_feature = check_hef_extension(ProtoHEFExtensionType::OUTPUT_SCALE_PER_FEATURE, + header, hef_extensions, included_features); + supported_features.periph_calculation_in_hailort = check_hef_extension(ProtoHEFExtensionType::PERIPH_CALCULATION_IN_HAILORT, + header, hef_extensions, included_features); return supported_features; } @@ -782,7 +851,7 @@ net_flow::NmsPostProcessConfig create_nms_config(const ProtoHEFOp &op_proto) nms_config.nms_score_th = (float32_t)op_proto.nms_op().nms_score_th(); nms_config.nms_iou_th = (float32_t)op_proto.nms_op().nms_iou_th(); nms_config.max_proposals_per_class = op_proto.nms_op().max_proposals_per_class(); - nms_config.classes = op_proto.nms_op().classes(); + nms_config.number_of_classes = op_proto.nms_op().classes(); nms_config.background_removal = op_proto.nms_op().background_removal(); nms_config.background_removal_index = op_proto.nms_op().background_removal_index(); @@ -836,15 +905,25 @@ Expected> create_yolox_op(const ProtoHEFOp &op_pro const std::map &pad_index_to_streams_info, const std::map &input_to_output_pads) { auto nms_config = create_nms_config(op_proto); - net_flow::YoloPostProcessConfig yolo_config{}; - yolo_config.image_height = (float32_t)op_proto.nms_op().yolo_nms_op().image_height(); - yolo_config.image_width = (float32_t)op_proto.nms_op().yolo_nms_op().image_width(); + net_flow::YoloxPostProcessConfig yolox_config{}; + yolox_config.image_height = (float32_t)op_proto.nms_op().yolox_nms_op().image_height(); + yolox_config.image_width = (float32_t)op_proto.nms_op().yolox_nms_op().image_width(); std::map inputs_metadata; std::map outputs_metadata; net_flow::BufferMetaData output_metadata{}; output_metadata.format = output_format; outputs_metadata.insert({op_proto.output_pads()[0].name(), output_metadata}); + + for (auto &bbox_proto : op_proto.nms_op().yolox_nms_op().bbox_decoders()) { + assert(contains(pad_index_to_streams_info, static_cast(bbox_proto.reg_pad_index()))); + auto reg_name = pad_index_to_streams_info.at(bbox_proto.reg_pad_index()).name; + assert(contains(pad_index_to_streams_info, static_cast(bbox_proto.cls_pad_index()))); + auto cls_name = pad_index_to_streams_info.at(bbox_proto.cls_pad_index()).name; + assert(contains(pad_index_to_streams_info, static_cast(bbox_proto.obj_pad_index()))); + auto obj_name = pad_index_to_streams_info.at(bbox_proto.obj_pad_index()).name; + yolox_config.input_names.emplace_back(net_flow::MatchingLayersNames{reg_name, obj_name, cls_name}); + } for (auto &input_pad : op_proto.input_pads()) { CHECK_AS_EXPECTED(contains(input_to_output_pads, static_cast(input_pad.index())), HAILO_INVALID_HEF, @@ -861,7 +940,7 @@ Expected> create_yolox_op(const ProtoHEFOp &op_pro input_metadata.padded_shape = op_input_stream.hw_shape; inputs_metadata.insert({op_input_stream.name, input_metadata}); } - return net_flow::YOLOXPostProcessOp::create(inputs_metadata, outputs_metadata, nms_config, yolo_config); + return net_flow::YOLOXPostProcessOp::create(inputs_metadata, outputs_metadata, nms_config, yolox_config); } Expected> create_ssd_op(const ProtoHEFOp &op_proto, hailo_format_t output_format, @@ -925,8 +1004,124 @@ Expected> create_ssd_op(const ProtoHEFOp &op_proto return net_flow::SSDPostProcessOp::create(inputs_metadata, outputs_metadata, nms_config, ssd_config); } +Expected> create_argmax_op(const ProtoHEFPad &input_pad, const ProtoHEFPad &output_pad, + const std::string &input_name, const std::string &output_name, const bool &is_hw_padding_supported) +{ + // create input meta + std::map inputs_metadata; + hailort::net_flow::BufferMetaData input_metadata{}; + input_metadata.shape = {input_pad.tensor_shape().height(), input_pad.tensor_shape().width(), input_pad.tensor_shape().features()}; + // If padding is done in HW, the padded shape is as the shape (TODO: Remove once HRT support hw_padding from DFC) + if (is_hw_padding_supported) { + input_metadata.padded_shape = input_metadata.shape; + } else { + input_metadata.padded_shape = {input_pad.tensor_shape().padded_height(), input_pad.tensor_shape().padded_width(), + input_pad.tensor_shape().padded_features()}; + } + + input_metadata.format.type = static_cast(input_pad.format_type()); + input_metadata.format.order = static_cast(input_pad.format_order()); + input_metadata.format.flags = HAILO_FORMAT_FLAGS_NONE; + input_metadata.quant_info.qp_zp = input_pad.numeric_info().qp_zp(); + input_metadata.quant_info.qp_scale = input_pad.numeric_info().qp_scale(); + input_metadata.quant_info.limvals_min = input_pad.numeric_info().limvals_min(); + input_metadata.quant_info.limvals_max = input_pad.numeric_info().limvals_max(); + inputs_metadata.insert({input_name, input_metadata}); + + // create output meta + std::map outputs_metadata; + hailort::net_flow::BufferMetaData output_metadata{}; + output_metadata.shape = {input_pad.tensor_shape().height(), input_pad.tensor_shape().width(), hailort::net_flow::ARGMAX_OUTPUT_FEATURES_SIZE}; + output_metadata.padded_shape = output_metadata.shape; // padded_shape is the same as the output_shape in argmax op + output_metadata.format.order = static_cast(output_pad.format_order()); + output_metadata.format.type = static_cast(output_pad.format_type()); + output_metadata.quant_info.qp_zp = output_pad.numeric_info().qp_zp(); + output_metadata.quant_info.qp_scale = output_pad.numeric_info().qp_scale(); + output_metadata.quant_info.limvals_min = output_pad.numeric_info().limvals_min(); + output_metadata.quant_info.limvals_max = output_pad.numeric_info().limvals_max(); + output_metadata.format.flags = HAILO_FORMAT_FLAGS_NONE; + outputs_metadata.insert({output_name, output_metadata}); + return net_flow::ArgmaxPostProcessOp::create(inputs_metadata, outputs_metadata); +} + +Expected> create_softmax_op(const ProtoHEFPad &input_pad, const ProtoHEFPad &output_pad, + const std::string &input_name, const std::string &output_name) +{ + // create input meta + std::map inputs_metadata; + hailort::net_flow::BufferMetaData input_metadata{}; + input_metadata.shape = {input_pad.tensor_shape().height(), input_pad.tensor_shape().width(), input_pad.tensor_shape().features()}; + input_metadata.padded_shape = input_metadata.shape; // since softmax is connected to transform context, shape and padded shape are the same + + input_metadata.format.type = static_cast(input_pad.format_type()); + input_metadata.format.order = static_cast(input_pad.format_order()); + input_metadata.format.flags = HAILO_FORMAT_FLAGS_NONE; + input_metadata.quant_info.qp_zp = input_pad.numeric_info().qp_zp(); + input_metadata.quant_info.qp_scale = input_pad.numeric_info().qp_scale(); + input_metadata.quant_info.limvals_min = input_pad.numeric_info().limvals_min(); + input_metadata.quant_info.limvals_max = input_pad.numeric_info().limvals_max(); + inputs_metadata.insert({input_name, input_metadata}); + + // create output meta + std::map outputs_metadata; + hailort::net_flow::BufferMetaData output_metadata{}; + output_metadata.shape = {input_pad.tensor_shape().height(), input_pad.tensor_shape().width(), input_pad.tensor_shape().features()}; + output_metadata.padded_shape = output_metadata.shape; // padded_shape is the same as the output_shape in softmax op + output_metadata.format.order = static_cast(output_pad.format_order()); + output_metadata.format.type = static_cast(output_pad.format_type()); + output_metadata.quant_info.qp_zp = output_pad.numeric_info().qp_zp(); + output_metadata.quant_info.qp_scale = output_pad.numeric_info().qp_scale(); + output_metadata.quant_info.limvals_min = output_pad.numeric_info().limvals_min(); + output_metadata.quant_info.limvals_max = output_pad.numeric_info().limvals_max(); + output_metadata.format.flags = HAILO_FORMAT_FLAGS_NONE; + outputs_metadata.insert({output_name, output_metadata}); + return net_flow::SoftmaxPostProcessOp::create(inputs_metadata, outputs_metadata); +} + +Expected> create_logits_op(const ProtoHEFOp &op_proto, const std::map &input_to_output_pads, + const std::map &pad_index_to_pad_data, NetFlowElement &net_flow_element, + const std::map &pad_index_to_streams_info, const ProtoHEFHwArch &hef_arch) +{ + // connect input_streams to net_flow element + CHECK_AS_EXPECTED(op_proto.input_pads().size() == 1, HAILO_INVALID_HEF, "Logits op must have 1 input only"); + CHECK_AS_EXPECTED(op_proto.output_pads().size() == 1, HAILO_INVALID_HEF, "Logits op must have 1 output only"); + auto input_pad = op_proto.input_pads()[0]; + auto output_pad = op_proto.output_pads()[0]; + CHECK_AS_EXPECTED(contains(input_to_output_pads, static_cast(input_pad.index())), HAILO_INVALID_HEF, + "Logits op is not connected to core-op"); + auto output_pad_index = input_to_output_pads.at(input_pad.index()); + CHECK_AS_EXPECTED(contains(pad_index_to_streams_info, output_pad_index), HAILO_INVALID_HEF, + "Pad {} of post-process {} is not connected to any core output stream", input_pad.index(), op_proto.name()); + + // Data of the input_pad is taken from the output_pad of the core op + const auto &connected_output_pad = pad_index_to_pad_data.at(output_pad_index); + net_flow_element.input_streams.insert(connected_output_pad.name()); + // TODO: HRT-10603 + const auto &op_input_stream = pad_index_to_streams_info.at(output_pad_index); + auto max_periph_bytes_from_hef = HefConfigurator::max_periph_bytes_value(DeviceBase::hef_arch_to_device_arch(hef_arch)); + CHECK_EXPECTED(max_periph_bytes_from_hef); + const auto max_periph_bytes = (0 == op_input_stream.max_shmifo_size) ? max_periph_bytes_from_hef.value(): + MIN(max_periph_bytes_from_hef.value(), op_input_stream.max_shmifo_size); + const auto is_hw_padding_supported = HefConfigurator::is_hw_padding_supported(op_input_stream, max_periph_bytes); + net_flow_element.name = op_proto.name(); + + switch (op_proto.logits_op().logits_type()) { + case ProtoHEFLogitsType::PROTO_HEF_ARGMAX_TYPE: { + net_flow_element.op_type = HAILO_NET_FLOW_OP_TYPE_ARGMAX; + return create_argmax_op(connected_output_pad, output_pad, input_pad.name(), output_pad.name(), is_hw_padding_supported); + } + case ProtoHEFLogitsType::PROTO_HEF_SOFTMAX_TYPE: { + net_flow_element.op_type = HAILO_NET_FLOW_OP_TYPE_SOFTMAX; + return create_softmax_op(connected_output_pad, output_pad, input_pad.name(), output_pad.name()); + } + default: { + LOGGER__ERROR("Invalid Net-Flow Logits-Op {}", ProtoHEFLogitsType_Name(op_proto.logits_op().logits_type())); + return make_unexpected(HAILO_INTERNAL_FAILURE); + } + } +} Expected>> Hef::Impl::create_net_flow_ops(const ProtoHEFNetworkGroup &network_group_proto, - CoreOpMetadata &core_op_metadata) const + CoreOpMetadata &core_op_metadata, const ProtoHEFHwArch &hef_arch) const { std::vector> result; if (!m_supported_features.hailo_net_flow) { @@ -943,6 +1138,16 @@ Expected>> Hef::Impl::create_net_flo for (auto &pad_edge : network_group_proto.pad_edges()) { input_to_output_pads.insert({pad_edge.dst(), pad_edge.src()}); } + std::map pad_index_to_pad_data; + for (auto &op_proto : network_group_proto.ops()) { + for (auto &output_pad : op_proto.output_pads()) { + pad_index_to_pad_data.insert({output_pad.index(), output_pad}); + } + for (auto &input_pad : op_proto.input_pads()) { + pad_index_to_pad_data.insert({input_pad.index(), input_pad}); + } + } + for (auto &op_proto : network_group_proto.ops()) { switch (op_proto.op_case()) { case ProtoHEFOp::kCoreOp: { @@ -950,10 +1155,10 @@ Expected>> Hef::Impl::create_net_flo } case ProtoHEFOp::kNmsOp: { hailo_format_t output_format{}; - output_format.type = HAILO_FORMAT_TYPE_FLOAT32; - output_format.order = HAILO_FORMAT_ORDER_HAILO_NMS; - output_format.flags = HAILO_FORMAT_FLAGS_QUANTIZED; + output_format.order = HAILO_FORMAT_ORDER_HAILO_NMS; // TODO Remove- HRT-9737 + NetFlowElement net_flow_element{}; + net_flow_element.op_type = HAILO_NET_FLOW_OP_TYPE_NMS; // TODO: HRT-9902 - Move nms_info to be an op member instead of NetFlowElement net_flow_element.nms_info = { @@ -962,7 +1167,9 @@ Expected>> Hef::Impl::create_net_flo sizeof(hailo_bbox_float32_t), 1, // input_division_factor false, - hailo_nms_defuse_info_t() + hailo_nms_defuse_info_t(), + DEFAULT_NMS_NO_BURST_SIZE, + HAILO_BURST_TYPE_NO_BURST }; for (auto &input_pad : op_proto.input_pads()) { CHECK_AS_EXPECTED(contains(input_to_output_pads, static_cast(input_pad.index())), HAILO_INVALID_HEF, @@ -1007,7 +1214,6 @@ Expected>> Hef::Impl::create_net_flo } } net_flow_element.op = post_process_op; - // Fill meta-data output vstream info auto net_group_name = HefUtils::get_network_group_name(network_group_proto, m_supported_features); auto network_name = HailoRTDefaults::get_network_name(net_group_name); @@ -1024,11 +1230,34 @@ Expected>> Hef::Impl::create_net_flo net_flow_output_vstream_info.nms_shape.number_of_classes--; net_flow_element.nms_info.number_of_classes--; } + net_flow_element.output_vstream_info = net_flow_output_vstream_info; - result.push_back(std::make_shared(net_flow_element)); + auto net_flow_element_ptr = make_shared_nothrow(net_flow_element); + CHECK_NOT_NULL_AS_EXPECTED(net_flow_element_ptr, HAILO_OUT_OF_HOST_MEMORY); + result.push_back(net_flow_element_ptr); + break; + } + case ProtoHEFOp::kLogitsOp: { + NetFlowElement net_flow_element{}; + auto expected_logits_op = create_logits_op(op_proto, input_to_output_pads, pad_index_to_pad_data, net_flow_element, + pad_index_to_streams_info, hef_arch); + CHECK_EXPECTED(expected_logits_op); + net_flow_element.op = expected_logits_op.release(); + + hailo_vstream_info_t net_flow_output_vstream_info{}; + auto proto_output_pad = op_proto.output_pads()[0]; + auto net_group_name = HefUtils::get_network_group_name(network_group_proto, m_supported_features); + auto network_name = HailoRTDefaults::get_network_name(net_group_name); + strncpy(net_flow_output_vstream_info.name, proto_output_pad.name().c_str(), proto_output_pad.name().length() + 1); + strncpy(net_flow_output_vstream_info.network_name, network_name.c_str(), network_name.length() + 1); + net_flow_output_vstream_info.direction = HAILO_D2H_STREAM; + net_flow_output_vstream_info.format = net_flow_element.op.get()->outputs_metadata().begin()->second.format; + net_flow_output_vstream_info.shape = net_flow_element.op.get()->outputs_metadata().begin()->second.shape; + net_flow_element.output_vstream_info = net_flow_output_vstream_info; - // TODO: HRT-9546 - Move vstreams out of core op - core_op_metadata.add_output_vstream_info(net_flow_output_vstream_info); + auto net_flow_element_ptr = make_shared_nothrow(net_flow_element); + CHECK_NOT_NULL_AS_EXPECTED(net_flow_element_ptr, HAILO_OUT_OF_HOST_MEMORY); + result.push_back(net_flow_element_ptr); break; } default: { @@ -1040,11 +1269,14 @@ Expected>> Hef::Impl::create_net_flo return result; } -Expected Hef::Impl::get_core_op_metadata(const std::string &network_group_name, uint32_t partial_clusters_layout_bitmap) +Expected Hef::Impl::get_core_op_metadata(const std::string &network_group_name, uint32_t partial_clusters_layout_bitmap) { - CHECK_AS_EXPECTED(contains(m_core_op_per_arch, network_group_name), HAILO_NOT_FOUND, + CHECK_AS_EXPECTED(contains(m_network_group_metadata, network_group_name), HAILO_NOT_FOUND, "Network group with name {} wasn't found", network_group_name); - auto metadata_per_arch = m_core_op_per_arch.at(network_group_name); + auto &ng_metadata = m_network_group_metadata.at(network_group_name); + CHECK_AS_EXPECTED(contains(ng_metadata.m_core_ops_metadata_per_arch, network_group_name), HAILO_NOT_FOUND, + "Core-op with name {} wasn't found", network_group_name); + auto metadata_per_arch = ng_metadata.m_core_ops_metadata_per_arch.at(network_group_name); auto metadata = metadata_per_arch.get_metadata(partial_clusters_layout_bitmap); return metadata; } @@ -1107,29 +1339,29 @@ hailo_status get_hw_padding_params(hailo_format_order_t format_order, uint32_t w } Expected HefConfigurator::parse_nn_stream_config(hailo_format_order_t format_order, uint32_t width, uint32_t features, - uint32_t hw_data_bytes, uint16_t core_buffers_per_frame, uint16_t core_bytes_per_buffer, bool hw_padding_supported, bool is_ddr) + uint32_t hw_data_bytes, uint16_t core_buffers_per_frame, uint16_t core_bytes_per_buffer, bool hw_padding_supported, bool is_ddr, + uint16_t periph_buffers_per_frame, uint16_t periph_bytes_per_buffer) { CONTROL_PROTOCOL__nn_stream_config_t stream_config = {}; stream_config.core_buffers_per_frame = core_buffers_per_frame; stream_config.core_bytes_per_buffer = core_bytes_per_buffer; - stream_config.periph_buffers_per_frame = core_buffers_per_frame; // periph buffers per frame is the same (even if - // for hw padding each buffer is smaller). + stream_config.periph_buffers_per_frame = periph_buffers_per_frame; + stream_config.periph_bytes_per_buffer = periph_bytes_per_buffer; /* For DDR buffering - core buffers is depended on the amount of buffers per PCIe interrupt. No HW padding required */ if (is_ddr) { stream_config.core_buffers_per_frame = 1; stream_config.feature_padding_payload = 0; - stream_config.periph_bytes_per_buffer = stream_config.core_bytes_per_buffer; } else { if (hw_padding_supported) { auto status = get_hw_padding_params(format_order, width, features, hw_data_bytes, stream_config.feature_padding_payload, stream_config.periph_bytes_per_buffer); CHECK_SUCCESS_AS_EXPECTED(status); + stream_config.periph_buffers_per_frame = core_buffers_per_frame; } else { stream_config.feature_padding_payload = 0; - stream_config.periph_bytes_per_buffer = stream_config.core_bytes_per_buffer; } /* For now, no support for buffer padding */ stream_config.buffer_padding_payload = 0; @@ -1151,24 +1383,72 @@ Expected HefConfigurator::parse_nn_stream_ auto format_order = format_order_exp.release(); auto is_ddr = ProtoHEFEdgeConnectionType::PROTO__EDGE_CONNECTION_TYPE__DDR == edge_connection_type; + CHECK_AS_EXPECTED(IS_FIT_IN_UINT32(edge_layer.padded_width() * edge_layer.padded_features() * + edge_layer.padded_height() * edge_layer.data_bytes()), HAILO_INVALID_HEF, "padded shape too big"); + + // TODO HRT-10993: Remove these parameters for the parse_nn_stream_config function call + // These values will get overrided in update_layer_info in resource_manager_builder - except in case of + // MIPI stream with hw padding supported (HRT-11030) + // TODO HRT-11030 - in MIPI with hw padding supported - in this case because the layer thinks hw padding is + // supported it wont recalculate periph values , but when creating the InputStreamBase - it will not use hw padding + // and then will take the initial values. Should fix this behavior + const uint16_t INITIAL_PERIPH_BYTES_PER_BUFFER = static_cast(edge_layer.core_bytes_per_buffer()); + const uint16_t INITIAL_PERIPH_BUFFERS_PER_FRAME = static_cast(edge_layer.core_buffers_per_frame()); + // Width and features only used in case hw_padding is supported. In that case, they represent the HW shape (without padding) return parse_nn_stream_config(format_order, edge_layer.width(), edge_layer.features(), edge_layer.data_bytes(), static_cast(edge_layer.core_buffers_per_frame()), - static_cast(edge_layer.core_bytes_per_buffer()), hw_padding_supported, is_ddr); + static_cast(edge_layer.core_bytes_per_buffer()), hw_padding_supported, is_ddr, + INITIAL_PERIPH_BUFFERS_PER_FRAME, INITIAL_PERIPH_BYTES_PER_BUFFER); } Expected HefConfigurator::parse_nn_stream_config(const LayerInfo &edge_layer, bool hw_padding_supported) { // TODO HRT-7177 - pass interface to layer info instead of re-calculated Layer info from stream_internal.hpp // After passing stream interface, there is no need for this function. Just use CONTROL_PROTOCOL__nn_stream_config_t from layer info. - auto is_ddr = false; // This function is called only on boundary layers, so no DDR + assert(LayerType::BOUNDARY == edge_layer.type); + const auto is_ddr = false; // This function is called only on boundary layers, so no DDR + return parse_nn_stream_config(edge_layer.format.order, edge_layer.hw_shape.width, edge_layer.hw_shape.features, edge_layer.hw_data_bytes, edge_layer.nn_stream_config.core_buffers_per_frame, - edge_layer.nn_stream_config.core_bytes_per_buffer, hw_padding_supported, is_ddr); + edge_layer.nn_stream_config.core_bytes_per_buffer, hw_padding_supported, is_ddr, edge_layer.nn_stream_config.periph_buffers_per_frame, + edge_layer.nn_stream_config.periph_bytes_per_buffer); +} + +Expected HefConfigurator::max_periph_bytes_value(const hailo_device_architecture_t hw_arch) +{ + switch (hw_arch) { + case HAILO_ARCH_HAILO8_A0: + case HAILO_ARCH_HAILO8: + case HAILO_ARCH_HAILO8L: + return HAILO8_INBOUND_DATA_STREAM_SIZE; + case HAILO_ARCH_HAILO15: + return HAILO15_PERIPH_BYTES_PER_BUFFER_MAX_SIZE; + default: + LOGGER__ERROR("Unknown device architecture!"); + return make_unexpected(HAILO_INVALID_ARGUMENT); + } +} + +// TODO HRT-11006: remove this function when hw padding is removed from InputStreamBase / OutputStreamBase constructor +Expected HefConfigurator::max_periph_bytes_value(const hailo_stream_interface_t interface) +{ + switch (interface) { + case HAILO_STREAM_INTERFACE_ETH: + case HAILO_STREAM_INTERFACE_MIPI: + case HAILO_STREAM_INTERFACE_PCIE: + return HAILO8_INBOUND_DATA_STREAM_SIZE; + case HAILO_STREAM_INTERFACE_INTEGRATED: + return HAILO15_PERIPH_BYTES_PER_BUFFER_MAX_SIZE; + default: + LOGGER__ERROR("Unknown stream interface!"); + return make_unexpected(HAILO_INVALID_ARGUMENT); + } } bool HefConfigurator::is_hw_padding_supported(bool is_boundary, bool is_mux, hailo_format_order_t format_order, - uint16_t core_buffers_per_frame, uint32_t height, uint32_t width, uint32_t features, uint32_t hw_data_bytes) + uint16_t core_buffers_per_frame, uint32_t height, uint32_t width, uint32_t features, uint32_t hw_data_bytes, + const uint32_t max_periph_bytes_value) { if (!is_boundary || is_mux) { return false; @@ -1196,16 +1476,15 @@ bool HefConfigurator::is_hw_padding_supported(bool is_boundary, bool is_mux, hai return false; } - if ((width * features * hw_data_bytes) > - (HAILO8_INBOUND_DATA_STREAM_SIZE - 1)) { + if ((width * features * hw_data_bytes) > (max_periph_bytes_value - 1)) { // TODO: HRT-4177 - LOGGER__DEBUG("HW padding is supported only on layers with features * width * data size > stream size"); + LOGGER__DEBUG("HW padding is supported only on layers with shape size < stream size"); return false; } return true; } -bool HefConfigurator::is_hw_padding_supported(const LayerInfo &layer_info) +bool HefConfigurator::is_hw_padding_supported(const LayerInfo &layer_info, const uint32_t max_periph_bytes_value) { /* If the network is transposed, the width and height are swapped in LayerInfo c'tor, so need to swap it again for calculations */ auto height = layer_info.shape.height; @@ -1214,13 +1493,13 @@ bool HefConfigurator::is_hw_padding_supported(const LayerInfo &layer_info) std::swap(height, width); } - auto is_boundary = true; // This function is called only on boundary layers + auto is_boundary = (LayerType::BOUNDARY == layer_info.type); return is_hw_padding_supported(is_boundary, layer_info.is_mux, layer_info.format.order, layer_info.nn_stream_config.core_buffers_per_frame, height, width, - layer_info.shape.features, layer_info.hw_data_bytes); + layer_info.shape.features, layer_info.hw_data_bytes, max_periph_bytes_value); } -bool HefConfigurator::is_hw_padding_supported(const ProtoHEFEdgeLayer &edge_layer) +bool HefConfigurator::is_hw_padding_supported(const ProtoHEFEdgeLayer &edge_layer, const uint32_t max_periph_bytes_value) { auto is_boundary = (ProtoHEFEdgeConnectionType::PROTO__EDGE_CONNECTION_TYPE__BOUNDARY == edge_layer.context_switch_info().edge_connection_type()); auto is_mux = (ProtoHEFEdgeLayerType::PROTO__EDGE_LAYER_TYPE__MUX == edge_layer.edge_layer_type()); @@ -1238,48 +1517,51 @@ bool HefConfigurator::is_hw_padding_supported(const ProtoHEFEdgeLayer &edge_laye auto format_order = format_order_exp.release(); return is_hw_padding_supported(is_boundary, is_mux, format_order, static_cast(edge_layer_base.core_buffers_per_frame()), - edge_layer_base.height(), edge_layer_base.width(), edge_layer_base.features(), edge_layer_base.data_bytes()); + edge_layer_base.height(), edge_layer_base.width(), edge_layer_base.features(), edge_layer_base.data_bytes(), + max_periph_bytes_value); } Expected> Hef::Impl::get_input_stream_infos(const std::string &net_group_name, const std::string &network_name) { - auto network_group_metadata = get_core_op_metadata(net_group_name); - CHECK_EXPECTED(network_group_metadata); - return network_group_metadata->get_input_stream_infos(network_name); + auto core_op_metadata = get_core_op_metadata(net_group_name); + CHECK_EXPECTED(core_op_metadata); + + return core_op_metadata.value()->get_input_stream_infos(network_name); } Expected> Hef::Impl::get_output_stream_infos(const std::string &net_group_name, const std::string &network_name) { - auto network_group_metadata = get_core_op_metadata(net_group_name); - CHECK_EXPECTED(network_group_metadata); - return network_group_metadata->get_output_stream_infos(network_name); + auto core_op_metadata = get_core_op_metadata(net_group_name); + CHECK_EXPECTED(core_op_metadata); + + return core_op_metadata.value()->get_output_stream_infos(network_name); } Expected> Hef::Impl::get_all_stream_infos(const std::string &net_group_name, const std::string &network_name) { - auto network_group_metadata = get_core_op_metadata(net_group_name); - CHECK_EXPECTED(network_group_metadata); - return network_group_metadata->get_all_stream_infos(network_name); + auto core_op_metadata = get_core_op_metadata(net_group_name); + CHECK_EXPECTED(core_op_metadata); + + return core_op_metadata.value()->get_all_stream_infos(network_name); } Expected> Hef::Impl::get_network_infos(const std::string &net_group_name) { - auto network_group_metadata = get_core_op_metadata(net_group_name); - CHECK_EXPECTED(network_group_metadata); - return network_group_metadata->get_network_infos(); + CHECK_AS_EXPECTED(contains(m_network_group_metadata, net_group_name), HAILO_NOT_FOUND); + return m_network_group_metadata.at(net_group_name).get_network_infos(); } Expected Hef::Impl::get_stream_info_by_name(const std::string &stream_name, hailo_stream_direction_t stream_direction, const std::string &net_group_name) { - auto network_group_metadata = get_core_op_metadata(net_group_name); - CHECK_EXPECTED(network_group_metadata); + auto core_op_metadata = get_core_op_metadata(net_group_name); + CHECK_EXPECTED(core_op_metadata); if (HAILO_H2D_STREAM == stream_direction) { - auto stream_infos = network_group_metadata->get_input_stream_infos(); + auto stream_infos = core_op_metadata.value()->get_input_stream_infos(); CHECK_EXPECTED(stream_infos); for (auto &stream_info : stream_infos.value()) { if (stream_name == stream_info.name) { @@ -1287,7 +1569,7 @@ Expected Hef::Impl::get_stream_info_by_name(const std::stri } } } else { - auto stream_infos = network_group_metadata->get_output_stream_infos(); + auto stream_infos = core_op_metadata.value()->get_output_stream_infos(); CHECK_EXPECTED(stream_infos); for (auto &stream_info : stream_infos.value()) { if (stream_name == stream_info.name) { @@ -1302,25 +1584,22 @@ Expected Hef::Impl::get_stream_info_by_name(const std::stri Expected> Hef::Impl::get_input_vstream_infos(const std::string &net_group_name, const std::string &network_name) { - auto network_group_metadata = get_core_op_metadata(net_group_name); - CHECK_EXPECTED(network_group_metadata); - return network_group_metadata->get_input_vstream_infos(network_name); + CHECK_AS_EXPECTED(contains(m_network_group_metadata, net_group_name), HAILO_NOT_FOUND); + return m_network_group_metadata.at(net_group_name).get_input_vstream_infos(network_name); } Expected> Hef::Impl::get_output_vstream_infos(const std::string &net_group_name, const std::string &network_name) { - auto network_group_metadata = get_core_op_metadata(net_group_name); - CHECK_EXPECTED(network_group_metadata); - return network_group_metadata->get_output_vstream_infos(network_name); + CHECK_AS_EXPECTED(contains(m_network_group_metadata, net_group_name), HAILO_NOT_FOUND); + return m_network_group_metadata.at(net_group_name).get_output_vstream_infos(network_name); } Expected> Hef::Impl::get_all_vstream_infos(const std::string &net_group_name, const std::string &network_name) { - auto network_group_metadata = get_core_op_metadata(net_group_name); - CHECK_EXPECTED(network_group_metadata); - return network_group_metadata->get_all_vstream_infos(network_name); + CHECK_AS_EXPECTED(contains(m_network_group_metadata, net_group_name), HAILO_NOT_FOUND); + return m_network_group_metadata.at(net_group_name).get_all_vstream_infos(network_name); } const std::vector& Hef::Impl::network_groups() const @@ -1334,10 +1613,11 @@ const std::vector& Hef::Impl::core_ops(const std::string &ne return m_core_ops_per_group.at(net_group_name); }; -const std::vector> Hef::Impl::post_process_ops(const std::string &net_group_name) const +const NetworkGroupMetadata Hef::Impl::network_group_metadata(const std::string &net_group_name) const { - assert(contains(m_post_process_ops_per_group, net_group_name)); - return m_post_process_ops_per_group.at(net_group_name); + assert(contains(m_network_group_metadata, net_group_name)); + auto metadata = m_network_group_metadata.at(net_group_name); + return metadata; } bool Hef::Impl::check_hef_extension(const ProtoHEFExtensionType &extension, const ProtoHEFHeader &header, @@ -1456,8 +1736,9 @@ Expected Hef::Impl::get_number_of_input_streams(const std::string &net_g auto core_op_metadata = get_core_op_metadata(net_group_name); CHECK_EXPECTED(core_op_metadata); - auto input_layer_infos = core_op_metadata->get_input_layer_infos(); - return input_layer_infos.size(); + auto input_stream_infos = core_op_metadata.value()->get_input_stream_infos(); + CHECK_EXPECTED(input_stream_infos); + return input_stream_infos->size(); } Expected Hef::Impl::get_number_of_output_streams(const std::string &net_group_name) @@ -1465,8 +1746,9 @@ Expected Hef::Impl::get_number_of_output_streams(const std::string &net_ auto core_op_metadata = get_core_op_metadata(net_group_name); CHECK_EXPECTED(core_op_metadata); - auto output_layer_infos = core_op_metadata->get_output_layer_infos(); - return output_layer_infos.size(); + auto output_stream_infos = core_op_metadata.value()->get_output_stream_infos(); + CHECK_EXPECTED(output_stream_infos); + return output_stream_infos->size(); } static Expected get_layer_type(const ProtoHEFEdgeConnectionType &edge_connection_type) @@ -1484,20 +1766,7 @@ static Expected get_layer_type(const ProtoHEFEdgeConnectionType &edge } } -hailo_status HefUtils::fill_layer_info_with_base_info(const ProtoHEFEdgeLayerBase &base_info, - const ProtoHEFEdgeConnectionType &edge_connection_type, const ProtoHEFNetworkGroupMetadata &network_group_proto, - bool hw_padding_supported, bool transposed, const uint8_t context_index, const uint8_t network_index, - LayerInfo &layer_info) -{ - auto format_order_exp = HailoRTDefaults::get_device_format_order(base_info.format()); - CHECK_EXPECTED_AS_STATUS(format_order_exp); - - auto format_oder = format_order_exp.release(); - - auto layer_type = get_layer_type(edge_connection_type); - CHECK_EXPECTED_AS_STATUS(layer_type); - layer_info.type = layer_type.value(); - +static void parse_layer_shape(LayerInfo &layer_info, const ProtoHEFEdgeLayerBase &base_info, const bool hw_padding_supported) { if (HEF__FORMAT__NMS != base_info.format()) { layer_info.shape.height = base_info.height(); layer_info.shape.width = base_info.width(); @@ -1519,6 +1788,23 @@ hailo_status HefUtils::fill_layer_info_with_base_info(const ProtoHEFEdgeLayerBas layer_info.hw_shape.features = base_info.padded_features(); } layer_info.hw_data_bytes = base_info.data_bytes(); +} + +hailo_status HefUtils::fill_layer_info_with_base_info(const ProtoHEFEdgeLayerBase &base_info, + const ProtoHEFEdgeConnectionType &edge_connection_type, const ProtoHEFNetworkGroupMetadata &network_group_proto, + bool hw_padding_supported, bool transposed, const uint8_t context_index, const uint8_t network_index, + LayerInfo &layer_info, const SupportedFeatures &supported_features, const ProtoHEFHwArch &hef_arch) +{ + auto format_order_exp = HailoRTDefaults::get_device_format_order(base_info.format()); + CHECK_EXPECTED_AS_STATUS(format_order_exp); + + auto format_oder = format_order_exp.release(); + + auto layer_type = get_layer_type(edge_connection_type); + CHECK_EXPECTED_AS_STATUS(layer_type); + layer_info.type = layer_type.value(); + + parse_layer_shape(layer_info, base_info, hw_padding_supported); // TODO: remove duplications with stream info parse layer_info.format.order = format_oder; @@ -1539,7 +1825,7 @@ hailo_status HefUtils::fill_layer_info_with_base_info(const ProtoHEFEdgeLayerBas CHECK_EXPECTED_AS_STATUS(type); layer_info.format.type = type.value(); - auto nn_stream_config = HefConfigurator::parse_nn_stream_config(base_info, hw_padding_supported, + auto nn_stream_config = HefConfigurator::parse_nn_stream_config(base_info, hw_padding_supported, edge_connection_type); CHECK_EXPECTED_AS_STATUS(nn_stream_config, "Failed parse nn stream config"); layer_info.nn_stream_config = nn_stream_config.release(); @@ -1554,7 +1840,8 @@ hailo_status HefUtils::fill_layer_info_with_base_info(const ProtoHEFEdgeLayerBas layer_info.dma_engine_index = static_cast(base_info.engine_id()); if (HAILO_FORMAT_ORDER_HAILO_NMS == layer_info.format.order) { - auto expected_nms_info = parse_proto_nms_info(base_info.additional_info().nms_info()); + auto expected_nms_info = parse_proto_nms_info(base_info.additional_info().nms_info(), supported_features.nms_burst_mode, + hef_arch); CHECK_EXPECTED_AS_STATUS(expected_nms_info); layer_info.nms_info = expected_nms_info.release(); } @@ -1568,10 +1855,10 @@ hailo_status HefUtils::fill_layer_info(const ProtoHEFEdgeLayerInfo &info, const ProtoHEFEdgeConnectionType &edge_connection_type, const ProtoHEFCoreOpMock &core_op, hailo_stream_direction_t direction, bool hw_padding_supported, const uint8_t context_index, const std::string &partial_network_name, - uint8_t network_index, LayerInfo &layer_info) + uint8_t network_index, LayerInfo &layer_info, const SupportedFeatures &supported_features, const ProtoHEFHwArch &hef_arch) { auto status = fill_layer_info_with_base_info(info.edge_layer_base(), edge_connection_type, core_op.network_group_metadata, - hw_padding_supported, info.transposed(), context_index, network_index, layer_info); + hw_padding_supported, info.transposed(), context_index, network_index, layer_info, supported_features, hef_arch); CHECK_SUCCESS(status); if (HAILO_MAX_STREAM_NAME_SIZE < (info.name().length() + 1)) { @@ -1591,6 +1878,21 @@ hailo_status HefUtils::fill_layer_info(const ProtoHEFEdgeLayerInfo &info, layer_info.quant_info.limvals_min = info.numeric_info().limvals_min(); layer_info.quant_info.qp_scale = info.numeric_info().qp_scale(); layer_info.quant_info.qp_zp = info.numeric_info().qp_zp(); + + for (uint32_t i = 0; i < layer_info.shape.features; i++) { + hailo_quant_info_t quant_info = {}; + if (supported_features.output_scale_by_feature) { + quant_info.qp_zp = static_cast(info.numeric_info().qp_zps()[i]); + quant_info.qp_scale = static_cast(info.numeric_info().qp_scales()[i]); + } else { + quant_info.qp_zp = info.numeric_info().qp_zp(); + quant_info.qp_scale = info.numeric_info().qp_scale(); + } + quant_info.limvals_min = info.numeric_info().limvals_min(); + quant_info.limvals_max = info.numeric_info().limvals_max(); + layer_info.quant_infos.push_back(std::move(quant_info)); + } + // Simulation info assert (1 == info.edge_layer_base().buffer_indices_size()); layer_info.buffer_indices.cluster_index = info.edge_layer_base().buffer_indices(0).cluster_index(); @@ -1605,7 +1907,8 @@ hailo_status HefUtils::fill_layer_info(const ProtoHEFEdgeLayerInfo &info, // This creates a new LayerInfo for the fused layer *for each defused layer*, even though they all share the same fused layer. // TODO Make it so all defused layer reference the same LayerInfo of the fused layer. LayerInfo fused_layer_info = {}; - status = fill_fused_nms_info(fused_layer, fused_layer_info, layer_info.quant_info, layer_info.network_name); + status = fill_fused_nms_info(fused_layer, fused_layer_info, layer_info.quant_info, layer_info.network_name, + supported_features.nms_burst_mode, hef_arch); CHECK_SUCCESS(status); layer_info.fused_nms_layer.push_back(fused_layer_info); break; @@ -1618,7 +1921,8 @@ hailo_status HefUtils::fill_layer_info(const ProtoHEFEdgeLayerInfo &info, } hailo_status HefUtils::fill_fused_nms_info(const ProtoHEFEdgeLayerFused &info, LayerInfo &layer_info, - hailo_quant_info_t &defuse_quant_info, const std::string &network_name) + hailo_quant_info_t &defuse_quant_info, const std::string &network_name, const bool burst_mode_enabled, + const ProtoHEFHwArch &hef_arch) { auto base_info = info.layer_info().edge_layer_base(); auto format_order_exp = HailoRTDefaults::get_device_format_order(base_info.format()); @@ -1637,7 +1941,7 @@ hailo_status HefUtils::fill_fused_nms_info(const ProtoHEFEdgeLayerFused &info, L CHECK_EXPECTED_AS_STATUS(type); layer_info.format.type = type.value(); - auto expected_nms_info = parse_proto_nms_info(info.nms_info()); + auto expected_nms_info = parse_proto_nms_info(info.nms_info(), burst_mode_enabled, hef_arch); CHECK_EXPECTED_AS_STATUS(expected_nms_info); layer_info.nms_info = expected_nms_info.release(); @@ -1664,11 +1968,11 @@ hailo_status HefUtils::fill_mux_info(const ProtoHEFEdgeLayerMux &info, const ProtoHEFEdgeConnectionType &edge_connection_type, const ProtoHEFCoreOpMock &core_op, hailo_stream_direction_t direction, bool hw_padding_supported, const uint8_t context_index, const std::string &partial_network_name, - uint8_t network_index, LayerInfo &layer_info) + uint8_t network_index, LayerInfo &layer_info, const SupportedFeatures &supported_features, const ProtoHEFHwArch &hef_arch) { const bool transposed = false; auto status = fill_layer_info_with_base_info(info.edge_layer_base(), edge_connection_type, core_op.network_group_metadata, - hw_padding_supported, transposed, context_index, network_index, layer_info); + hw_padding_supported, transposed, context_index, network_index, layer_info, supported_features, hef_arch); CHECK_SUCCESS(status); if (HAILO_MAX_STREAM_NAME_SIZE < (info.name().length() + 1)) { @@ -1699,7 +2003,8 @@ hailo_status HefUtils::fill_mux_info(const ProtoHEFEdgeLayerMux &info, switch (info.predecessors(i).edge_case()) { case ProtoHefEdge::kLayerInfo: status = fill_layer_info(info.predecessors(i).layer_info(), edge_connection_type, core_op, - direction, hw_padding_supported, context_index, partial_network_name, network_index, temp_layer); + direction, hw_padding_supported, context_index, partial_network_name, network_index, temp_layer, + supported_features, hef_arch); if (HAILO_SUCCESS != status) { return status; } @@ -1707,7 +2012,8 @@ hailo_status HefUtils::fill_mux_info(const ProtoHEFEdgeLayerMux &info, break; case ProtoHefEdge::kLayerMux: status = fill_mux_info(info.predecessors(i).layer_mux(), edge_connection_type, core_op, - direction, hw_padding_supported, context_index, partial_network_name, network_index, temp_layer); + direction, hw_padding_supported, context_index, partial_network_name, network_index, temp_layer, + supported_features, hef_arch); if (HAILO_SUCCESS != status) { return status; } @@ -1728,9 +2034,10 @@ hailo_status HefUtils::fill_boundary_layers_info( const uint8_t context_index, const ProtoHEFEdgeLayer &layer, const SupportedFeatures &supported_features, - ContextMetadata &context_metadata) + ContextMetadata &context_metadata, + const ProtoHEFHwArch &hef_arch) { - auto layer_info = get_boundary_layer_info(core_op, context_index, layer, supported_features); + auto layer_info = get_boundary_layer_info(core_op, context_index, layer, supported_features, hef_arch); CHECK_EXPECTED_AS_STATUS(layer_info); context_metadata.add_boundary_layer(layer_info.release()); @@ -1743,9 +2050,9 @@ hailo_status HefUtils::fill_inter_context_layers_info( const uint8_t context_index, const ProtoHEFEdgeLayer &layer, const SupportedFeatures &supported_features, - ContextMetadata &context_metadata) + ContextMetadata &context_metadata, const ProtoHEFHwArch &hef_arch) { - auto layer_info = get_inter_context_layer_info(core_op, context_index, layer, supported_features); + auto layer_info = get_inter_context_layer_info(core_op, context_index, layer, supported_features, hef_arch); CHECK_EXPECTED_AS_STATUS(layer_info); context_metadata.add_inter_context_layer(layer_info.release()); @@ -1757,9 +2064,9 @@ hailo_status HefUtils::fill_ddr_layers_info( const uint8_t context_index, const ProtoHEFEdgeLayer &layer, const SupportedFeatures &supported_features, - ContextMetadata &context_metadata) + ContextMetadata &context_metadata, const ProtoHEFHwArch &hef_arch) { - auto layer_info = get_ddr_layer_info(core_op, context_index, layer, supported_features); + auto layer_info = get_ddr_layer_info(core_op, context_index, layer, supported_features, hef_arch); CHECK_EXPECTED_AS_STATUS(layer_info); context_metadata.add_ddr_layer(layer_info.release()); @@ -1987,14 +2294,57 @@ static Expected parse_action(const ProtoHEFAction CHECK_AS_EXPECTED(IS_FIT_IN_UINT8(proto_action.enable_nms().network_index()), HAILO_INVALID_HEF, "Failed to parse HEF. Invalid network_index: {}.", proto_action.enable_nms().network_index()); + uint16_t number_of_classes = 0; + uint16_t burst_size = 0; + // TODO: HRT-10750 - change to error and failure in case of old enable nms action + if (0 == proto_action.enable_nms().number_of_classes() || 0 == proto_action.enable_nms().burst_size()) { + LOGGER__WARNING("Enable NMS Action must have number of classes and burst size, Please update Hef to SDK version newer than 3.24"); + number_of_classes = 1; + burst_size = 1; + } else { + number_of_classes = static_cast(proto_action.enable_nms().number_of_classes()); + burst_size = static_cast(proto_action.enable_nms().burst_size()); + } + auto support_multi_networks = supported_features.multi_network_support; auto network_index = static_cast((support_multi_networks) ? proto_action.enable_nms().network_index() : 0); const auto nms_unit_index = static_cast(proto_action.enable_nms().nms_unit_index()); - return EnableNmsAction::create(nms_unit_index, network_index); + return EnableNmsAction::create(nms_unit_index, network_index, number_of_classes, burst_size); } + case ProtoHEFAction::kWriteDataByType: + { + CHECK_AS_EXPECTED(IS_FIT_IN_UINT32(proto_action.write_data_by_type().address()), HAILO_INVALID_HEF, + "Failed to parse HEF. Invalid write_data_by_type address: {} (should fit uint32_t).", + proto_action.write_data_by_type().address()); + CHECK_AS_EXPECTED((0 == (proto_action.write_data_by_type().address() % ALIGNED_TO_4_BYTES)), HAILO_INVALID_HEF, + "Failed to parse HEF. Invalid write_data_by_type address. Address should be aligned to 4 bytes: {}.", + proto_action.write_data_by_type().address()); + CHECK_AS_EXPECTED(proto_action.write_data_by_type().data_type() == ProtoHEFWriteDataType::DATA_FROM_ACTION || + proto_action.write_data_by_type().data_type() == ProtoHEFWriteDataType::BATCH_SIZE, HAILO_INVALID_HEF, + "Failed to parse HEF. Invalid write_data_by_type data_type: {} ", proto_action.write_data_by_type().data_type()); + CHECK_AS_EXPECTED(proto_action.write_data_by_type().data().length() <= CONTEXT_SWITCH_DEFS__WRITE_ACTION_BY_TYPE_MAX_SIZE, HAILO_INVALID_HEF, + "Failed to parse HEF. Invalid write_data_by_type data size: {} ", proto_action.write_data_by_type().data().length()); + CHECK_AS_EXPECTED(IS_FIT_IN_UINT8(proto_action.write_data_by_type().shift()), HAILO_INVALID_HEF, + "Failed to parse HEF. Invalid write_data_by_type shift: {} (should fit uint8_t).", + proto_action.write_data_by_type().shift()); + + uint32_t data = 0x0; + memcpy(&data, proto_action.write_data_by_type().data().data(), + /* Limit the data to one register */ + MIN(CONTEXT_SWITCH_DEFS__WRITE_ACTION_BY_TYPE_MAX_SIZE, proto_action.write_data_by_type().data().length())); + + const auto address = static_cast(proto_action.write_data_by_type().address()); + const auto data_type = static_cast(proto_action.write_data_by_type().data_type()); + const auto mask = proto_action.write_data_by_type().mask(); + auto support_multi_networks = supported_features.multi_network_support; + const auto network_index = static_cast((support_multi_networks) ? proto_action.write_data_by_type().network_index() : 0); + const auto shift = static_cast(proto_action.write_data_by_type().shift()); + + return WriteDataByTypeAction::create(address, data_type, data, shift, mask, network_index); + } default: LOGGER__ERROR("Action {} not implemented", proto_action.action_case()); break; @@ -2119,7 +2469,8 @@ Expected HefUtils::parse_preliminary_context(const ProtoHEFPrel } Expected HefUtils::parse_single_dynamic_context(const ProtoHEFCoreOpMock &core_op, - const ProtoHEFContext &context_proto, uint8_t context_index, const SupportedFeatures &supported_features) + const ProtoHEFContext &context_proto, uint8_t context_index, const SupportedFeatures &supported_features, + const ProtoHEFHwArch &hef_arch) { auto context_metadata_exp = parse_operations(context_proto.operations(), supported_features); CHECK_EXPECTED(context_metadata_exp); @@ -2129,17 +2480,17 @@ Expected HefUtils::parse_single_dynamic_context(const ProtoHEFC if (ProtoHEFEdgeConnectionType::PROTO__EDGE_CONNECTION_TYPE__BOUNDARY == edge_layer.context_switch_info().edge_connection_type()) { auto status = fill_boundary_layers_info(core_op, context_index, edge_layer, - supported_features, context_metadata); + supported_features, context_metadata, hef_arch); CHECK_SUCCESS_AS_EXPECTED(status); } else if (ProtoHEFEdgeConnectionType::PROTO__EDGE_CONNECTION_TYPE__INTERMEDIATE == edge_layer.context_switch_info().edge_connection_type()) { auto status = fill_inter_context_layers_info(core_op, context_index, edge_layer, - supported_features, context_metadata); + supported_features, context_metadata, hef_arch); CHECK_SUCCESS_AS_EXPECTED(status); } else if (ProtoHEFEdgeConnectionType::PROTO__EDGE_CONNECTION_TYPE__DDR == edge_layer.context_switch_info().edge_connection_type()) { auto status = fill_ddr_layers_info(core_op, context_index, edge_layer, - supported_features, context_metadata); + supported_features, context_metadata, hef_arch); CHECK_SUCCESS_AS_EXPECTED(status); } } @@ -2170,12 +2521,13 @@ static hailo_status validate_unique_boundary_names(const std::vector> HefUtils::parse_dynamic_contexts(const ProtoHEFCoreOpMock &core_op, const SupportedFeatures &supported_features) +Expected> HefUtils::parse_dynamic_contexts(const ProtoHEFCoreOpMock &core_op, const SupportedFeatures &supported_features, + const ProtoHEFHwArch &hef_arch) { std::vector contexts_metadata; for (uint8_t context_index = 0; context_index < core_op.contexts.size(); context_index++) { auto &context_proto = core_op.contexts[context_index]; - auto context_metadata = parse_single_dynamic_context(core_op, context_proto, context_index, supported_features); + auto context_metadata = parse_single_dynamic_context(core_op, context_proto, context_index, supported_features, hef_arch); CHECK_EXPECTED(context_metadata); contexts_metadata.emplace_back(context_metadata.release()); } @@ -2186,13 +2538,39 @@ Expected> HefUtils::parse_dynamic_contexts(const Pr return contexts_metadata; } -Expected HefUtils::parse_proto_nms_info(const ProtoHEFNmsInfo &proto_nms_info) +Expected HefUtils::parse_proto_nms_info(const ProtoHEFNmsInfo &proto_nms_info, const bool burst_mode_enabled, + const ProtoHEFHwArch &hef_arch) { hailo_nms_info_t nms_info = {}; nms_info.number_of_classes = static_cast(proto_nms_info.number_of_classes()); nms_info.bbox_size = static_cast(proto_nms_info.bbox_size()); nms_info.max_bboxes_per_class = static_cast(proto_nms_info.max_output_size()); nms_info.chunks_per_frame = static_cast(proto_nms_info.input_division_factor()); + + if (burst_mode_enabled) { + nms_info.burst_size = static_cast(proto_nms_info.burst_size()); + nms_info.burst_type = static_cast(proto_nms_info.burst_type()); + + CHECK_AS_EXPECTED(nms_info.burst_type != HAILO_BURST_TYPE_NO_BURST, HAILO_INVALID_HEF, + "Invalid HEF, nms burst type is no burst but burst extension is enabled"); + + CHECK_AS_EXPECTED((nms_info.burst_size * nms_info.bbox_size) <= MAX_NMS_BURST_SIZE, + HAILO_INVALID_HEF, "Invalid HEF, nms burst size {} larger than maximum burst size {}", + (nms_info.burst_size * nms_info.bbox_size), MAX_NMS_BURST_SIZE); + + // Validate that burst type matches architecture + const auto dev_arch = DeviceBase::hef_arch_to_device_arch(hef_arch); + CHECK_AS_EXPECTED(LayerInfoUtils::validate_nms_burst_type(nms_info.burst_type, dev_arch), HAILO_INVALID_HEF, + "Invalid HEF, nms burst type {} on device architecture {}", nms_info.burst_type, dev_arch); + } else { + CHECK_AS_EXPECTED(HAILO_BURST_TYPE_NO_BURST == static_cast(proto_nms_info.burst_type()), + HAILO_INVALID_HEF, "Invalid HEF, nms burst extension is disabled yet burst type is {}", nms_info.burst_type); + + // In case of HAILO_BURST_TYPE_NO_BURST make burst size DEFAULT_NMS_NO_BURST_SIZE + nms_info.burst_size = DEFAULT_NMS_NO_BURST_SIZE; + nms_info.burst_type = static_cast(proto_nms_info.burst_type()); + } + if (nms_info.chunks_per_frame == 0) { // Old hef, use default value 1 nms_info.chunks_per_frame = 1; @@ -2213,7 +2591,8 @@ Expected HefUtils::parse_proto_nms_info(const ProtoHEFNmsInfo } Expected HefUtils::get_boundary_layer_info(const ProtoHEFCoreOpMock &core_op, - const uint8_t context_index, const ProtoHEFEdgeLayer &layer, const SupportedFeatures &supported_features) + const uint8_t context_index, const ProtoHEFEdgeLayer &layer, const SupportedFeatures &supported_features, + const ProtoHEFHwArch &hef_arch) { // We parse only boundary layers for user usage CHECK_AS_EXPECTED( @@ -2228,16 +2607,22 @@ Expected HefUtils::get_boundary_layer_info(const ProtoHEFCoreOpMock & auto network_index = static_cast((support_multi_networks) ? layer.network_index() : 0); auto partial_network_name = HefUtils::get_partial_network_name_by_index(core_op, network_index, supported_features); CHECK_EXPECTED(partial_network_name); - const bool hw_padding_supported = HefConfigurator::is_hw_padding_supported(layer); + auto max_periph_bytes_from_hef = HefConfigurator::max_periph_bytes_value(DeviceBase::hef_arch_to_device_arch(hef_arch)); + CHECK_EXPECTED(max_periph_bytes_from_hef); + const auto max_periph_bytes = (0 == layer.layer_info().edge_layer_base().max_shmifo_size()) ? max_periph_bytes_from_hef.value(): + MIN(max_periph_bytes_from_hef.value(), layer.layer_info().edge_layer_base().max_shmifo_size()); + const bool hw_padding_supported = HefConfigurator::is_hw_padding_supported(layer, max_periph_bytes); if (ProtoHEFEdgeLayerType::PROTO__EDGE_LAYER_TYPE__INFO == layer.edge_layer_type()) { // TODO: return LayerInfo auto status = fill_layer_info(layer.layer_info(), layer.context_switch_info().edge_connection_type(), - core_op, direction, hw_padding_supported, context_index, partial_network_name.value(), network_index, result); + core_op, direction, hw_padding_supported, context_index, partial_network_name.value(), network_index, result, + supported_features, hef_arch); CHECK_SUCCESS_AS_EXPECTED(status); } else if (ProtoHEFEdgeLayerType::PROTO__EDGE_LAYER_TYPE__MUX == layer.edge_layer_type()) { // TODO: return LayerInfo auto status = fill_mux_info(layer.layer_mux(), layer.context_switch_info().edge_connection_type(), - core_op, direction, hw_padding_supported, context_index, partial_network_name.value(), network_index, result); + core_op, direction, hw_padding_supported, context_index, partial_network_name.value(), network_index, result, + supported_features, hef_arch); CHECK_SUCCESS_AS_EXPECTED(status); } else { LOGGER__ERROR("Invalid layer type"); @@ -2272,7 +2657,8 @@ static Expected parse_connected_context_info( } Expected HefUtils::get_inter_context_layer_info(const ProtoHEFCoreOpMock &core_op, - const uint8_t context_index, const ProtoHEFEdgeLayer &layer, const SupportedFeatures &supported_features) + const uint8_t context_index, const ProtoHEFEdgeLayer &layer, const SupportedFeatures &supported_features, + const ProtoHEFHwArch &hef_arch) { LayerInfo result = {}; CHECK_AS_EXPECTED(PROTO__EDGE_LAYER_TYPE__INFO == layer.edge_layer_type(), HAILO_INVALID_HEF, "Inter-context layer can't be mux."); @@ -2284,9 +2670,14 @@ Expected HefUtils::get_inter_context_layer_info(const ProtoHEFCoreOpM CHECK_EXPECTED(partial_network_name); result.network_name = HefUtils::get_network_name(core_op, partial_network_name.release()); result.context_index = context_index; - const bool hw_padding_supported = HefConfigurator::is_hw_padding_supported(layer); + auto max_periph_bytes_from_hef = HefConfigurator::max_periph_bytes_value(DeviceBase::hef_arch_to_device_arch(hef_arch)); + CHECK_EXPECTED(max_periph_bytes_from_hef); + const auto max_periph_bytes = (0 == layer.layer_info().edge_layer_base().max_shmifo_size()) ? max_periph_bytes_from_hef.value(): + MIN(max_periph_bytes_from_hef.value(), layer.layer_info().edge_layer_base().max_shmifo_size()); + const bool hw_padding_supported = HefConfigurator::is_hw_padding_supported(layer, max_periph_bytes); result.name = layer.layer_info().name(); - auto nn_stream_config_exp = HefConfigurator::parse_nn_stream_config(layer.layer_info().edge_layer_base(), + + auto nn_stream_config_exp = HefConfigurator::parse_nn_stream_config(layer.layer_info().edge_layer_base(), hw_padding_supported, layer.context_switch_info().edge_connection_type()); CHECK_EXPECTED(nn_stream_config_exp); result.nn_stream_config = nn_stream_config_exp.release(); @@ -2299,6 +2690,8 @@ Expected HefUtils::get_inter_context_layer_info(const ProtoHEFCoreOpM result.max_shmifo_size = layer.layer_info().edge_layer_base().max_shmifo_size(); + parse_layer_shape(result, layer.layer_info().edge_layer_base(), hw_padding_supported); + result.direction = (ProtoHEFEdgeLayerDirection::PROTO__EDGE_LAYER_DIRECTION__DEVICE_TO_HOST == layer.direction()) ? HAILO_D2H_STREAM : HAILO_H2D_STREAM; @@ -2313,7 +2706,8 @@ Expected HefUtils::get_inter_context_layer_info(const ProtoHEFCoreOpM } Expected HefUtils::get_ddr_layer_info(const ProtoHEFCoreOpMock &core_op, - const uint8_t context_index, const ProtoHEFEdgeLayer &layer, const SupportedFeatures &supported_features) + const uint8_t context_index, const ProtoHEFEdgeLayer &layer, const SupportedFeatures &supported_features, + const ProtoHEFHwArch &hef_arch) { LayerInfo result = {}; CHECK_AS_EXPECTED(PROTO__EDGE_LAYER_TYPE__INFO == layer.edge_layer_type(), HAILO_INVALID_HEF, "DDR layer can't be mux."); @@ -2326,9 +2720,13 @@ Expected HefUtils::get_ddr_layer_info(const ProtoHEFCoreOpMock &core_ CHECK_EXPECTED(partial_network_name); result.network_name = HefUtils::get_network_name(core_op, partial_network_name.release()); result.context_index = context_index; - const bool hw_padding_supported = HefConfigurator::is_hw_padding_supported(layer); + auto max_periph_bytes_from_hef = HefConfigurator::max_periph_bytes_value(DeviceBase::hef_arch_to_device_arch(hef_arch)); + CHECK_EXPECTED(max_periph_bytes_from_hef); + const auto max_periph_bytes = (0 == layer.layer_info().edge_layer_base().max_shmifo_size()) ? max_periph_bytes_from_hef.value(): + MIN(max_periph_bytes_from_hef.value(), layer.layer_info().edge_layer_base().max_shmifo_size()); + const bool hw_padding_supported = HefConfigurator::is_hw_padding_supported(layer, max_periph_bytes); result.name = layer.layer_info().name(); - auto nn_stream_config_exp = HefConfigurator::parse_nn_stream_config(layer.layer_info().edge_layer_base(), + auto nn_stream_config_exp = HefConfigurator::parse_nn_stream_config(layer.layer_info().edge_layer_base(), hw_padding_supported, layer.context_switch_info().edge_connection_type()); CHECK_EXPECTED(nn_stream_config_exp); result.nn_stream_config = nn_stream_config_exp.release(); @@ -2351,6 +2749,8 @@ Expected HefUtils::get_ddr_layer_info(const ProtoHEFCoreOpMock &core_ result.direction = (ProtoHEFEdgeLayerDirection::PROTO__EDGE_LAYER_DIRECTION__DEVICE_TO_HOST == layer.direction()) ? HAILO_D2H_STREAM : HAILO_H2D_STREAM; + parse_layer_shape(result, layer.layer_info().edge_layer_base(), hw_padding_supported); + CHECK_AS_EXPECTED(IS_FIT_IN_UINT16(layer.layer_info().edge_layer_base().core_buffers_per_frame()), HAILO_INVALID_HEF, "Failed to parse HEF. Invalid core_buffers_per_frame: {}.", layer.layer_info().edge_layer_base().core_buffers_per_frame()); result.ddr_info.total_buffers_per_frame = static_cast(layer.layer_info().edge_layer_base().core_buffers_per_frame()); @@ -2362,28 +2762,6 @@ Expected HefUtils::get_ddr_layer_info(const ProtoHEFCoreOpMock &core_ return result; } -Expected> HefUtils::get_sorted_output_names(const ProtoHEFCoreOpMock &core_op) -{ - if (core_op.fused_layers_metadata.network_has_fused_layers()) { - return std::vector(std::begin(core_op.fused_layers_metadata.updated_sorted_output_names()), - std::end(core_op.fused_layers_metadata.updated_sorted_output_names())); - } else if (0 != core_op.sorted_outputs_order.size()) { - // For backwards compatibility before we've added updated_sorted_output_names - return std::vector(std::begin(core_op.sorted_outputs_order), - std::end(core_op.sorted_outputs_order)); - } else { - // For backwards compatibility before we've added this field - uint32_t number_of_contexts = core_op.contexts.size(); - const auto& context_metadata = core_op.contexts[number_of_contexts - 1].metadata(); - - CHECK_AS_EXPECTED(0 < context_metadata.sorted_outputs_order_size(), HAILO_INVALID_HEF, - "Sorted output names is not set up in the HEF."); - - return std::vector(std::begin(context_metadata.sorted_outputs_order()), - std::end(context_metadata.sorted_outputs_order())); - } -} - Expected HefUtils::get_partial_network_name_by_index(const ProtoHEFCoreOpMock &core_op, uint8_t network_index, const SupportedFeatures &supported_features) { @@ -2436,25 +2814,8 @@ Expected> Hef::Impl::get_core_op_per_arch(co Expected> Hef::Impl::get_sorted_output_names(const std::string &net_group_name) { - if (m_supported_features.hailo_net_flow) { - std::vector res; - for (const auto &net_group : m_groups) { - auto curr_name = HefUtils::get_network_group_name(*net_group, m_supported_features); - if (curr_name == net_group_name) { - res.reserve(net_group->sorted_outputs_order().size()); - for (auto &name : net_group->sorted_outputs_order()) { - res.push_back(name); - } - return res; - } - } - LOGGER__ERROR("Did not find network group of name {}", net_group_name); - return make_unexpected(HAILO_INVALID_HEF); - } - auto core_op_metadata = get_core_op_metadata(net_group_name); - CHECK_EXPECTED(core_op_metadata); - - auto res = core_op_metadata->get_sorted_output_names(); + CHECK_AS_EXPECTED(contains(m_network_group_metadata, net_group_name), HAILO_NOT_FOUND); + auto res = m_network_group_metadata.at(net_group_name).get_sorted_output_names(); return res; } @@ -2587,19 +2948,15 @@ bool Hef::Impl::contains_ddr_layers(const ProtoHEFCoreOpMock& core_op) Expected> Hef::Impl::get_stream_names_from_vstream_name(const std::string &vstream_name, const std::string &net_group_name) { - auto core_op_metadata = get_core_op_metadata(net_group_name); - CHECK_EXPECTED(core_op_metadata); - - return core_op_metadata->get_stream_names_from_vstream_name(vstream_name); + CHECK_AS_EXPECTED(contains(m_network_group_metadata, net_group_name), HAILO_NOT_FOUND); + return m_network_group_metadata.at(net_group_name).get_stream_names_from_vstream_name(vstream_name); } Expected> Hef::Impl::get_vstream_names_from_stream_name(const std::string &stream_name, const std::string &net_group_name) { - auto core_op_metadata = get_core_op_metadata(net_group_name); - CHECK_EXPECTED(core_op_metadata); - - return core_op_metadata->get_vstream_names_from_stream_name(stream_name); + CHECK_AS_EXPECTED(contains(m_network_group_metadata, net_group_name), HAILO_NOT_FOUND); + return m_network_group_metadata.at(net_group_name).get_vstream_names_from_stream_name(stream_name); } Expected Hef::Impl::get_vstream_name_from_original_name_mux(const std::string &original_name, const ProtoHefEdge &layer) @@ -2864,11 +3221,16 @@ Expected> Hef::Impl::get_post_processes_infos_descripti std::vector infos_strings; std::string infos_string; - auto post_process = post_process_ops(network_group_name); + CHECK_AS_EXPECTED(contains(m_network_group_metadata, network_group_name), HAILO_INTERNAL_FAILURE); + + auto post_process = m_network_group_metadata.at(network_group_name).m_net_flow_ops; for (const auto &post_process_info : post_process) { infos_string = post_process_info->op->get_op_description(); - infos_string += ", Bbox size: " + std::to_string(post_process_info->nms_info.bbox_size) + - ", Max bboxes per class: " + std::to_string(post_process_info->nms_info.max_bboxes_per_class); + if (HAILO_NET_FLOW_OP_TYPE_NMS == post_process_info->op_type) { + + infos_string += ", Bbox size: " + std::to_string(post_process_info->nms_info.bbox_size) + + ", Max bboxes per class: " + std::to_string(post_process_info->nms_info.max_bboxes_per_class); + } } /* If the string is empty there is no need to continue. */ if (infos_string.empty()) { @@ -2890,14 +3252,14 @@ Expected> Hef::Impl::get_post_processes_infos_descripti return infos_strings; } -Expected Hef::get_hef_description(bool stream_infos, bool vstream_infos) +Expected Hef::get_description(bool stream_infos, bool vstream_infos) { auto arch = get_hef_device_arch(); CHECK_EXPECTED(arch); - return pimpl->get_hef_description(stream_infos, vstream_infos, arch.value()); + return pimpl->get_description(stream_infos, vstream_infos, arch.value()); } -Expected Hef::Impl::get_hef_description(bool stream_infos, bool vstream_infos, hailo_device_architecture_t device_arch) +Expected Hef::Impl::get_description(bool stream_infos, bool vstream_infos, hailo_device_architecture_t device_arch) { std::string hef_infos; auto hef_arch_str = HailoRTCommon::get_device_arch_str(device_arch); @@ -2906,9 +3268,9 @@ Expected Hef::Impl::get_hef_description(bool stream_infos, bool vst auto network_group_infos = get_network_groups_infos(); CHECK_EXPECTED(network_group_infos); for (const auto &network_group_info : network_group_infos.release()) { - auto core_op_meta_data = get_core_op_metadata(network_group_info.name); - CHECK_EXPECTED(core_op_meta_data); - auto number_of_contexts = core_op_meta_data->get_contexts_count(); + auto core_op_metadata = get_core_op_metadata(network_group_info.name); + CHECK_EXPECTED(core_op_metadata); + auto number_of_contexts = core_op_metadata.value()->get_contexts_count(); auto contexts_str = (network_group_info.is_multi_context ? "Multi Context - Number of contexts: " + std::to_string(number_of_contexts) : "Single Context"); hef_infos += "Network group name: " + std::string(network_group_info.name) + ", " + contexts_str + "\n"; @@ -3020,9 +3382,8 @@ hailo_status Hef::Impl::fill_missing_input_vstream_params_with_default(const std const std::string &network_name, std::map &input_vstreams_params, bool quantized, hailo_format_type_t format_type, uint32_t timeout_ms, uint32_t queue_size) { - auto core_op_metadata = get_core_op_metadata(net_group_name); - CHECK_EXPECTED_AS_STATUS(core_op_metadata); - auto input_vstream_infos = core_op_metadata->get_input_vstream_infos(network_name); + CHECK(contains(m_network_group_metadata, net_group_name), HAILO_NOT_FOUND); + auto input_vstream_infos = m_network_group_metadata.at(net_group_name).get_input_vstream_infos(network_name); CHECK_EXPECTED_AS_STATUS(input_vstream_infos); return fill_missing_vstream_params_with_default(input_vstreams_params, input_vstream_infos.value(), @@ -3033,9 +3394,8 @@ hailo_status Hef::Impl::fill_missing_output_vstream_params_with_default(const st const std::string &network_name, std::map &output_vstream_params, bool quantized, hailo_format_type_t format_type, uint32_t timeout_ms, uint32_t queue_size) { - auto core_op_metadata = get_core_op_metadata(net_group_name); - CHECK_EXPECTED_AS_STATUS(core_op_metadata); - auto output_vstream_infos = core_op_metadata->get_output_vstream_infos(network_name); + CHECK(contains(m_network_group_metadata, net_group_name), HAILO_NOT_FOUND); + auto output_vstream_infos = m_network_group_metadata.at(net_group_name).get_output_vstream_infos(network_name); CHECK_EXPECTED_AS_STATUS(output_vstream_infos); return fill_missing_vstream_params_with_default(output_vstream_params, output_vstream_infos.value(), @@ -3110,12 +3470,16 @@ Expected> Hef::Impl::create_str CHECK_EXPECTED(core_op_metadata); std::map results; - for (auto &input_layer : core_op_metadata->get_input_layer_infos()) { + auto input_stream_infos = core_op_metadata.value()->get_input_stream_infos(); + CHECK_EXPECTED(input_stream_infos); + for (auto &input_layer : input_stream_infos.value()) { auto params = HailoRTDefaults::get_stream_parameters(stream_interface, HAILO_H2D_STREAM); CHECK_EXPECTED(params); results.emplace(std::make_pair(input_layer.name, params.release())); } - for (auto &output_layer : core_op_metadata->get_output_layer_infos()) { + auto output_stream_infos = core_op_metadata.value()->get_output_stream_infos(); + CHECK_EXPECTED(output_stream_infos); + for (auto &output_layer : output_stream_infos.value()) { auto params = HailoRTDefaults::get_stream_parameters(stream_interface, HAILO_D2H_STREAM); CHECK_EXPECTED(params); results.emplace(std::make_pair(output_layer.name, params.release())); @@ -3141,7 +3505,7 @@ Expected> Hef::Impl::create_ne std::map results; - if (core_op_metadata->supported_features().multi_network_support) { + if (core_op_metadata.value()->supported_features().multi_network_support) { CHECK_AS_EXPECTED((core_op.value()->networks_names.size() != 0), HAILO_INTERNAL_FAILURE, "Hef support multiple networks, but no networks found in the proto"); for (const auto &partial_network_name : core_op.value()->networks_names) { @@ -3178,14 +3542,18 @@ Expected> Hef::Impl::create_str CHECK_EXPECTED(core_op_metadata); std::map results; - for (auto &input_layer : core_op_metadata->get_input_layer_infos()) { + auto input_stream_infos = core_op_metadata.value()->get_input_stream_infos(); + CHECK_EXPECTED(input_stream_infos); + for (auto &input_layer : input_stream_infos.value()) { hailo_stream_parameters_t params = {}; params.direction = HAILO_H2D_STREAM; params.stream_interface = HAILO_STREAM_INTERFACE_MIPI; params.mipi_input_params = mipi_params; results.emplace(std::make_pair(input_layer.name, params)); } - for (auto &output_layer : core_op_metadata->get_output_layer_infos()) { + auto output_stream_infos = core_op_metadata.value()->get_output_stream_infos(); + CHECK_EXPECTED(output_stream_infos); + for (auto &output_layer : output_stream_infos.value()) { auto params = HailoRTDefaults::get_stream_parameters(output_interface, HAILO_D2H_STREAM); CHECK_EXPECTED(params); results.emplace(std::make_pair(output_layer.name, params.release())); diff --git a/hailort/libhailort/src/hef/hef_internal.hpp b/hailort/libhailort/src/hef/hef_internal.hpp index f2a3b53..bdee745 100644 --- a/hailort/libhailort/src/hef/hef_internal.hpp +++ b/hailort/libhailort/src/hef/hef_internal.hpp @@ -130,12 +130,23 @@ typedef enum { HEF__FORMAT__F8CR, } HEF__net_io_formatter_type_t; +typedef enum { + HAILO_NET_FLOW_OP_TYPE_NMS = 0, + HAILO_NET_FLOW_OP_TYPE_ARGMAX = 1, + HAILO_NET_FLOW_OP_TYPE_SOFTMAX = 2, + + /** Max enum value to maintain ABI Integrity */ + HAILO_NET_FLOW_OP_TYPE_MAX_ENUM = HAILO_MAX_ENUM +} hailo_net_flow_op_type_t; + struct NetFlowElement { std::string name; std::shared_ptr op; std::set input_streams; hailo_nms_info_t nms_info; + hailo_net_flow_op_type_t op_type; + hailo_vstream_info_t output_vstream_info; // Should be vector? }; const static uint32_t SUPPORTED_EXTENSIONS_BITSET_SIZE = 1000; @@ -151,7 +162,17 @@ static const std::vector SUPPORTED_EXTENSIONS = { OFFLOAD_ARGMAX, KO_RUN_ASAP, HAILO_NET_FLOW, - HAILO_NET_FLOW_YOLO_NMS // Extention added in platform 4.12 release + HAILO_NET_FLOW_YOLO_NMS, // Extention added in platform 4.12 release + HAILO_NET_FLOW_SSD_NMS, // Extention added in platform 4.14 release + WRITE_DATA_BY_TYPE, // Extention added in platform 4.14 release + NMS_OUTPUT_BURST, // Extention added in platform 4.14 release + DUAL_DIRECTION_STREAM_INDEX, // Extention added in platform 4.14 release + HAILO_NET_FLOW_ARGMAX, // Extention added in platform 4.14 release + HAILO_NET_FLOW_SOFTMAX, // Extention added in platform 4.14 release + ALIGNED_FORMAT_TYPE, // Extention added in platform 4.14 release + HAILO_NET_FLOW_YOLOX_NMS, // Extention added in platform 4.14 release + OUTPUT_SCALE_PER_FEATURE, // Extension added in platform 4.14 release + PERIPH_CALCULATION_IN_HAILORT, // Extension added in platform 4.14 release }; static inline bool is_h2d_boundary_info_layer(const ProtoHEFEdgeLayer& layer) @@ -209,7 +230,7 @@ public: const std::vector& network_groups() const; const std::vector& core_ops(const std::string &net_group_name) const; - const std::vector> post_process_ops(const std::string &net_group_name) const; + const NetworkGroupMetadata network_group_metadata(const std::string &net_group_name) const; Expected> get_network_group_and_network_name(const std::string &name); @@ -292,12 +313,12 @@ public: // Also adds information to CoreOpMetadata // TODO: When supporting multiple core ops in same netflow - Change metadata param to a map of core_ops_metadata. Expected>> create_net_flow_ops(const ProtoHEFNetworkGroup &network_group_proto, - CoreOpMetadata &core_op_metadata) const; + CoreOpMetadata &core_op_metadata, const ProtoHEFHwArch &hef_arch) const; // TODO: Should return map of NG's core_ops metadata? - Expected get_core_op_metadata(const std::string &network_group_name, uint32_t partial_clusters_layout_bitmap = PARTIAL_CLUSTERS_LAYOUT_IGNORE); + Expected get_core_op_metadata(const std::string &network_group_name, uint32_t partial_clusters_layout_bitmap = PARTIAL_CLUSTERS_LAYOUT_IGNORE); - Expected get_hef_description(bool stream_infos, bool vstream_infos, hailo_device_architecture_t device_arch); + Expected get_description(bool stream_infos, bool vstream_infos, hailo_device_architecture_t device_arch); const MD5_SUM_t &md5() const { @@ -371,7 +392,7 @@ private: static Expected get_vstream_name_from_original_name_mux(const std::string &original_name, const ProtoHefEdge &layer); static Expected> get_original_names_from_vstream_name_mux(const std::string &vstream_name, const ProtoHefEdge &layer); - Expected create_metadata_per_arch(const ProtoHEFCoreOpMock &core_op); + Expected create_metadata_per_arch(const ProtoHEFCoreOpMock &core_op, const std::vector &sorted_network_names); // TODO: Remove sorted_network_names Expected> get_stream_infos_description(const std::string &network_group_name, const std::string &network_name); Expected> get_vstream_infos_description(const std::string &network_group_name, const std::string &network_name); Expected> get_post_processes_infos_description(const std::string &network_group_name); @@ -392,8 +413,7 @@ private: Buffer m_hef_buffer; #endif // HAILO_SUPPORT_MULTI_PROCESS - // CoreOps information - TODO: Should be a map of map, mapping network_groups to it's core ops (second map is mapping core op name to its metadata). - std::map m_core_op_per_arch; + std::map m_network_group_metadata; // Key is NG name }; // TODO: Make this part of a namespace? (HRT-2881) @@ -409,15 +429,20 @@ public: static Expected parse_nn_stream_config(const LayerInfo &edge_layer, bool hw_padding_supported); - static bool is_hw_padding_supported(const ProtoHEFEdgeLayer &edge_layer); - static bool is_hw_padding_supported(const LayerInfo &layer_info); + static Expected max_periph_bytes_value(const hailo_device_architecture_t hw_arch); + static Expected max_periph_bytes_value(const hailo_stream_interface_t interface); + + static bool is_hw_padding_supported(const ProtoHEFEdgeLayer &edge_layer, const uint32_t max_periph_bytes_value); + static bool is_hw_padding_supported(const LayerInfo &layer_info, const uint32_t max_periph_bytes_value); private: static Expected parse_nn_stream_config(hailo_format_order_t format_order, uint32_t width, uint32_t features, uint32_t hw_data_bytes, uint16_t core_buffers_per_frame, - uint16_t core_bytes_per_buffer, bool hw_padding_supported, bool is_ddr); + uint16_t core_bytes_per_buffer, bool hw_padding_supported, bool is_ddr, uint16_t periph_buffers_per_frame, + uint16_t periph_bytes_per_buffer); static bool is_hw_padding_supported(bool is_boundary, bool is_mux, hailo_format_order_t format_order, - uint16_t core_buffers_per_frame, uint32_t height, uint32_t width, uint32_t features, uint32_t hw_data_bytes); + uint16_t core_buffers_per_frame, uint32_t height, uint32_t width, uint32_t features, uint32_t hw_data_bytes, + const uint32_t max_periph_bytes_value); }; class HefUtils final @@ -430,25 +455,26 @@ public: const uint8_t context_index, const ProtoHEFEdgeLayer &layer, const SupportedFeatures &supported_features, - ContextMetadata &context_metadata); + ContextMetadata &context_metadata, + const ProtoHEFHwArch &hef_arch); static Expected get_inter_context_layer_info( const ProtoHEFCoreOpMock &core_op, const uint8_t context_index, - const ProtoHEFEdgeLayer &layer, const SupportedFeatures &supported_features); + const ProtoHEFEdgeLayer &layer, const SupportedFeatures &supported_features, const ProtoHEFHwArch &hef_arch); static hailo_status fill_inter_context_layers_info( const ProtoHEFCoreOpMock &core_op, const uint8_t context_index, const ProtoHEFEdgeLayer &layer, const SupportedFeatures &supported_features, - ContextMetadata &context_metadata); + ContextMetadata &context_metadata, const ProtoHEFHwArch &hef_arch); static Expected get_ddr_layer_info( const ProtoHEFCoreOpMock &core_op, const uint8_t context_index, - const ProtoHEFEdgeLayer &layer, const SupportedFeatures &supported_features); + const ProtoHEFEdgeLayer &layer, const SupportedFeatures &supported_features, const ProtoHEFHwArch &hef_arch); static hailo_status fill_ddr_layers_info( const ProtoHEFCoreOpMock &core_op, const uint8_t context_index, const ProtoHEFEdgeLayer &layer, const SupportedFeatures &supported_features, - ContextMetadata &context_metadata); + ContextMetadata &context_metadata, const ProtoHEFHwArch &hef_arch); static hailo_status check_ddr_pairs_match( const std::vector &context_ddr_input_layers, const std::vector &context_ddr_output_layers, @@ -456,19 +482,18 @@ public: static Expected parse_preliminary_context(const ProtoHEFPreliminaryConfig &preliminary_proto, const SupportedFeatures &supported_features); static Expected parse_single_dynamic_context(const ProtoHEFCoreOpMock &core_op, - const ProtoHEFContext &context_proto, uint8_t context_index, const SupportedFeatures &supported_features); + const ProtoHEFContext &context_proto, uint8_t context_index, const SupportedFeatures &supported_features, + const ProtoHEFHwArch &hef_arch); static Expected> parse_dynamic_contexts(const ProtoHEFCoreOpMock &core_op, - const SupportedFeatures &supported_features); - static Expected parse_proto_nms_info(const ProtoHEFNmsInfo &proto_nms_info); + const SupportedFeatures &supported_features, const ProtoHEFHwArch &hef_arch); + static Expected parse_proto_nms_info(const ProtoHEFNmsInfo &proto_nms_info, + const bool burst_mode_enabled, const ProtoHEFHwArch &hef_arch); static Expected get_boundary_layer_info(const ProtoHEFCoreOpMock &core_op, - const uint8_t context_index, const ProtoHEFEdgeLayer &layer, const SupportedFeatures &supported_features); - static Expected> get_sorted_output_names(const ProtoHEFCoreOpMock &core_op); + const uint8_t context_index, const ProtoHEFEdgeLayer &layer, const SupportedFeatures &supported_features, + const ProtoHEFHwArch &hef_arch); static Expected get_partial_network_name_by_index(const ProtoHEFCoreOpMock &core_op, uint8_t network_index, const SupportedFeatures &supported_features); - static Expected> get_network_infos(const ProtoHEFNetworkGroup &net_group, - const std::string &net_group_name, const SupportedFeatures &supported_features); - static std::string get_network_group_name(const ProtoHEFNetworkGroup &net_group, const SupportedFeatures &supported_features); static std::string get_network_name(const ProtoHEFCoreOpMock &core_op, const std::string &partial_network_name); static std::string get_network_name(const std::string &net_group_name, const std::string &partial_network_name); @@ -477,19 +502,23 @@ private: static hailo_status fill_layer_info_with_base_info(const ProtoHEFEdgeLayerBase &base_info, const ProtoHEFEdgeConnectionType &edge_connection_type, const ProtoHEFNetworkGroupMetadata &network_group_proto, bool hw_padding_supported, bool transposed, - const uint8_t context_index, const uint8_t network_index, LayerInfo &layer_info); + const uint8_t context_index, const uint8_t network_index, LayerInfo &layer_info, + const SupportedFeatures &supported_features, const ProtoHEFHwArch &hef_arch); static hailo_status fill_layer_info(const ProtoHEFEdgeLayerInfo &info, const ProtoHEFEdgeConnectionType &edge_connection_type, const ProtoHEFCoreOpMock &core_op, hailo_stream_direction_t direction, bool hw_padding_supported, const uint8_t context_index, const std::string &partial_network_name, - uint8_t network_index, LayerInfo &layer_info); + uint8_t network_index, LayerInfo &layer_info, const SupportedFeatures &supported_features, + const ProtoHEFHwArch &hef_arch); static hailo_status fill_fused_nms_info(const ProtoHEFEdgeLayerFused &info, - LayerInfo &layer_info, hailo_quant_info_t &defuse_quant_info, const std::string &network_name); + LayerInfo &layer_info, hailo_quant_info_t &defuse_quant_info, const std::string &network_name, + const bool burst_mode_enabled, const ProtoHEFHwArch &hef_arch); static hailo_status fill_mux_info(const ProtoHEFEdgeLayerMux &info, const ProtoHEFEdgeConnectionType &edge_connection_type, const ProtoHEFCoreOpMock &core_op, hailo_stream_direction_t direction, bool hw_padding_supported, const uint8_t context_index, const std::string &partial_network_name, - uint8_t network_index, LayerInfo &layer_info); + uint8_t network_index, LayerInfo &layer_info, const SupportedFeatures &supported_features, + const ProtoHEFHwArch &hef_arch); }; } /* namespace hailort */ diff --git a/hailort/libhailort/src/hef/layer_info.hpp b/hailort/libhailort/src/hef/layer_info.hpp index af1ed07..2d0769f 100644 --- a/hailort/libhailort/src/hef/layer_info.hpp +++ b/hailort/libhailort/src/hef/layer_info.hpp @@ -26,6 +26,8 @@ namespace hailort { #define INVALID_PAD_INDEX (UINT32_MAX) +#define PERIPH_BYTES_PER_BUFFER_ALIGNMENT_SIZE (8) +#define PERIPH_BYTES_PER_BUFFER_DDR_ALIGNMENT_SIZE (512) enum class LayerType { @@ -54,7 +56,6 @@ struct DdrInfo { uint16_t min_buffered_rows; }; - struct LayerInfo { LayerType type = LayerType::NOT_SET; hailo_stream_direction_t direction; @@ -73,7 +74,8 @@ struct LayerInfo { hailo_3d_image_shape_t hw_shape; uint32_t hw_data_bytes; hailo_format_t format; - hailo_quant_info_t quant_info; + hailo_quant_info_t quant_info; // TODO: Remove, use vector + std::vector quant_infos; hailo_nms_info_t nms_info; // Mux info @@ -95,12 +97,12 @@ struct LayerInfo { DdrInfo ddr_info; }; -// LayerIdentifier = -using LayerIdentifier = std::tuple; +// LayerIdentifier = +using LayerIdentifier = std::tuple; inline LayerIdentifier to_layer_identifier(const LayerInfo &info) { - return std::make_tuple(info.type, info.name, info.stream_index); + return std::make_tuple(info.type, info.direction, info.name, info.stream_index); } class LayerInfoUtils { @@ -171,6 +173,10 @@ public: static Expected get_transfer_size(const LayerInfo &layer_info) { switch (layer_info.type) { case LayerType::BOUNDARY: + if (is_nms_burst_layer(layer_info)) { + return get_nms_layer_transfer_size(layer_info); + } + return layer_info.nn_stream_config.periph_bytes_per_buffer * layer_info.nn_stream_config.periph_buffers_per_frame; case LayerType::INTER_CONTEXT: return layer_info.nn_stream_config.periph_bytes_per_buffer * layer_info.nn_stream_config.periph_buffers_per_frame; case LayerType::DDR: @@ -180,6 +186,104 @@ public: } } + /** + * Validate nms burst type vs device architecture + * + * @param[in] burst_type A hailo_nms_burst_type_t burst_type. + * @param[in] arch A ::hailo_device_architecture_t architecture. + * @return true if the burst type matches the device architecture, otherwise false. + */ + static bool validate_nms_burst_type(const hailo_nms_burst_type_t burst_type, const hailo_device_architecture_t arch) + { + switch (arch) + { + case HAILO_ARCH_HAILO8_A0: + case HAILO_ARCH_HAILO8: + case HAILO_ARCH_HAILO8L: + return (HAILO_BURST_TYPE_H8_PER_CLASS == burst_type); + case HAILO_ARCH_HAILO15: + return ((HAILO_BURST_TYPE_H15_PER_CLASS == burst_type) || (HAILO_BURST_TYPE_H15_PER_FRAME == burst_type)); + default: + return false; + } + } + + /** + * Gets stream's transfer size in bytes by stream info and layer info params. + * + * @param[in] stream_info A ::hailo_stream_info_t object. + * @param[in] layer_info A ::LayerInfo object. + * @return The streams's transfer size in bytes. + */ + static constexpr uint32_t get_stream_transfer_size(const hailo_stream_info_t &stream_info, const LayerInfo &layer_info) + { + if (HAILO_FORMAT_ORDER_HAILO_NMS == layer_info.format.order) { + return get_nms_layer_transfer_size(layer_info); + } + return stream_info.hw_frame_size; + } + + /** + * Get NMS layers's transfer size in bytes by NMS. + * + * @param[in] layer_info A ::LayerInfo object. + * @return The layer's transfer size in bytes. + */ + static constexpr uint32_t get_nms_layer_transfer_size(const LayerInfo &layer_info) + { + switch (layer_info.nms_info.burst_type) { + // If No Burst mode - size of transfer is size of bbox + case HAILO_BURST_TYPE_NO_BURST: + return layer_info.nms_info.bbox_size; + // In hailo8 per class and hailo15 per class mode - check if can support interrupt per frame and if not do interrupt per burst + case HAILO_BURST_TYPE_H8_PER_CLASS: + case HAILO_BURST_TYPE_H15_PER_CLASS: + { + // In case of hailo8 - nn-core adds one delimeter per burst - in case of hailo15 nn-core adds delimeter and image delimeter per class + const size_t bboxes_needed_for_delimeter = (HAILO_BURST_TYPE_H8_PER_CLASS == layer_info.nms_info.burst_type) ? + 1 : 2; + // If burst size is bigger than max bboxes per class + bboxes_needed_for_delimeter - we can enable 1 interrupt per frame + // Becasue we know output size will be burst size * num classes + if (layer_info.nms_info.burst_size >= (layer_info.nms_info.max_bboxes_per_class + bboxes_needed_for_delimeter)) { + return layer_info.nms_info.burst_size * layer_info.nms_info.bbox_size * layer_info.nms_info.number_of_classes; + } else { + // support regular interrupt per burst + return layer_info.nms_info.burst_size * layer_info.nms_info.bbox_size; + } + } + // Currently HAILO_BURST_TYPE_H15_PER_FRAME mode isnt supported - Shouldn't reach here + case HAILO_BURST_TYPE_H15_PER_FRAME: + default: + assert(false); + return 0; + } + } + + /** + * Return if layer is NMS Burst layers. + * + * @param[in] layer_info A ::LayerInfo object. + * @return True if the layer is NMS layer with burst mode - false otherwise. + */ + static constexpr uint32_t is_nms_burst_layer(const LayerInfo &layer_info) + { + return (1 < layer_info.nms_info.burst_size); + } + + /** + * Get layers's transfer size. + * + * @param[in] layer_info A ::LayerInfo object. + * @return The layer's transfer size in bytes. + */ + static constexpr uint32_t get_layer_transfer_size(const LayerInfo &layer_info) + { + if (HAILO_FORMAT_ORDER_HAILO_NMS == layer_info.format.order) { + return get_nms_layer_transfer_size(layer_info); + } + return (layer_info.hw_shape.width * layer_info.hw_shape.features * layer_info.hw_shape.height * layer_info.hw_data_bytes); + } + private: static hailo_vstream_info_t get_vstream_info_from_layer_info_impl(const LayerInfo &layer_info) { diff --git a/hailort/libhailort/src/hw_consts.hpp b/hailort/libhailort/src/hw_consts.hpp index 4faec77..3acd38f 100644 --- a/hailort/libhailort/src/hw_consts.hpp +++ b/hailort/libhailort/src/hw_consts.hpp @@ -14,6 +14,9 @@ /** Package constants *********************************************************/ #define HAILO8_INBOUND_DATA_STREAM_SIZE (0x00010000L) +// Max periph bytes per buffer for hailo15 because (we use its value shifted right by 3 - according to the spec) to +// configure shmifo credit size - which in hailo15 only has a width of 10 bits +#define HAILO15_PERIPH_BYTES_PER_BUFFER_MAX_SIZE (0x00002000L) /** PCIe constants and macors ************************************************/ #define PCIE_CONFIG_BASE_ADDRESS (0x00200000L) // ::HW_BASE_ADDRESSES__PCIE_CONFIG(0, 0, 0) diff --git a/hailort/libhailort/src/mipi/mipi_stream.cpp b/hailort/libhailort/src/mipi/mipi_stream.cpp index e7c92fe..36007ec 100644 --- a/hailort/libhailort/src/mipi/mipi_stream.cpp +++ b/hailort/libhailort/src/mipi/mipi_stream.cpp @@ -128,17 +128,9 @@ hailo_status MipiInputStream::activate_stream(uint16_t /* dynamic_batch_size */, return HAILO_SUCCESS; } -Expected MipiInputStream::sync_write_raw_buffer(const MemoryView &buffer) +hailo_status MipiInputStream::write_impl(const MemoryView &buffer) { (void)buffer; - return make_unexpected(HAILO_INVALID_OPERATION); -} - -hailo_status MipiInputStream::sync_write_all_raw_buffer_no_transform_impl(void *buffer, size_t offset, size_t size) -{ - (void)buffer; - (void)offset; - (void)size; return HAILO_INVALID_OPERATION; } diff --git a/hailort/libhailort/src/mipi/mipi_stream.hpp b/hailort/libhailort/src/mipi/mipi_stream.hpp index b52597f..73178e1 100644 --- a/hailort/libhailort/src/mipi/mipi_stream.hpp +++ b/hailort/libhailort/src/mipi/mipi_stream.hpp @@ -35,8 +35,7 @@ private: CONTROL_PROTOCOL__mipi_input_config_params_t m_mipi_input_params; protected: - virtual Expected sync_write_raw_buffer(const MemoryView &buffer) override; - virtual hailo_status sync_write_all_raw_buffer_no_transform_impl(void *buffer, size_t offset, size_t size) override; + virtual hailo_status write_impl(const MemoryView &buffer) override; virtual hailo_status set_timeout(std::chrono::milliseconds timeout) { (void)timeout; return HAILO_INVALID_OPERATION; }; public: @@ -51,7 +50,6 @@ public: virtual std::chrono::milliseconds get_timeout() const override; virtual hailo_status abort() override; virtual hailo_status clear_abort() override; - }; } /* namespace hailort */ diff --git a/hailort/libhailort/src/net_flow/CMakeLists.txt b/hailort/libhailort/src/net_flow/CMakeLists.txt index ece9aa3..dd4a2c5 100644 --- a/hailort/libhailort/src/net_flow/CMakeLists.txt +++ b/hailort/libhailort/src/net_flow/CMakeLists.txt @@ -1,16 +1,16 @@ cmake_minimum_required(VERSION 3.0.0) -set(HAILORT_OPS_CPP_SOURCES +set(SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/ops/nms_post_process.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ops/yolo_post_process.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ops/yolox_post_process.cpp ${CMAKE_CURRENT_SOURCE_DIR}/ops/ssd_post_process.cpp -) + ${CMAKE_CURRENT_SOURCE_DIR}/ops/argmax_post_process.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/ops/softmax_post_process.cpp -set(SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/pipeline/pipeline.cpp ${CMAKE_CURRENT_SOURCE_DIR}/pipeline/inference_pipeline.cpp ${CMAKE_CURRENT_SOURCE_DIR}/pipeline/vstream.cpp ) -set(HAILORT_CPP_SOURCES ${HAILORT_CPP_SOURCES} ${SRC_FILES} ${HAILORT_OPS_CPP_SOURCES} PARENT_SCOPE) -set(HAILORT_OPS_CPP_SOURCES ${HAILORT_OPS_CPP_SOURCES} PARENT_SCOPE) +set(HAILORT_CPP_SOURCES ${HAILORT_CPP_SOURCES} ${SRC_FILES} PARENT_SCOPE) diff --git a/hailort/libhailort/src/net_flow/ops/argmax_post_process.cpp b/hailort/libhailort/src/net_flow/ops/argmax_post_process.cpp new file mode 100644 index 0000000..b7e2df9 --- /dev/null +++ b/hailort/libhailort/src/net_flow/ops/argmax_post_process.cpp @@ -0,0 +1,219 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file argmax_post_process.cpp + * @brief: Argsmax op + **/ + +#include "argmax_post_process.hpp" +#include "hailo/hailort.h" +#include "hailo/hailort_common.hpp" +#include "common/utils.hpp" + +#include + + +namespace hailort +{ +namespace net_flow +{ + +// Source https://stackoverflow.com/questions/3793838/which-is-the-first-integer-that-an-ieee-754-float-is-incapable-of-representing-e +#define FLOAT_LAST_CONSECUTIVE_REPRESENTABLE_INT (1 << std::numeric_limits::digits) + +hailo_status ArgmaxPostProcessOp::execute_not_supported(const BufferMetaData &input_metadata, const BufferMetaData &output_metadata, + const std::map &inputs, std::map &outputs) + { + (void)inputs; + (void)outputs; + LOGGER__ERROR("Argmax post-process not supported with params: input_order {}, input_type {}, output_type {}", + HailoRTCommon::get_format_order_str(input_metadata.format.order), + HailoRTCommon::get_format_type_str(input_metadata.format.type), + HailoRTCommon::get_format_type_str(output_metadata.format.type)); + return HAILO_INVALID_ARGUMENT; + } + +ArgmaxFunction ArgmaxPostProcessOp::m_argmax_function_array[ARGMAX_NUM_OF_POSSIBLE_FORMAT_ORDERS][ARGMAX_NUM_OF_POSSIBLE_FORMAT_TYPES][ARGMAX_NUM_OF_POSSIBLE_FORMAT_TYPES] +{ + { + { + // NHCW x AUTO + // We don't support input_format_type to be auto + ArgmaxPostProcessOp::execute_not_supported, + ArgmaxPostProcessOp::execute_not_supported, + ArgmaxPostProcessOp::execute_not_supported, + ArgmaxPostProcessOp::execute_not_supported + }, + { + // NHCW x UINT8 + ArgmaxPostProcessOp::execute_not_supported, // We don't support output_format_type to be auto + ArgmaxPostProcessOp::NHCW_to_NHW_feature_axis, + ArgmaxPostProcessOp::NHCW_to_NHW_feature_axis, + ArgmaxPostProcessOp::NHCW_to_NHW_feature_axis + }, + { + // NHCW x UINT16 + ArgmaxPostProcessOp::execute_not_supported, // We don't support output_format_type to be auto + ArgmaxPostProcessOp::NHCW_to_NHW_feature_axis, + ArgmaxPostProcessOp::NHCW_to_NHW_feature_axis, + ArgmaxPostProcessOp::NHCW_to_NHW_feature_axis + }, + { + // NHCW x FLOAT32 + // We don't support input_format_type to be float32 + ArgmaxPostProcessOp::execute_not_supported, + ArgmaxPostProcessOp::execute_not_supported, + ArgmaxPostProcessOp::execute_not_supported, + ArgmaxPostProcessOp::execute_not_supported + } + }, + { + { + // NHWC x AUTO + // We don't support input_format_type to be auto + ArgmaxPostProcessOp::execute_not_supported, + ArgmaxPostProcessOp::execute_not_supported, + ArgmaxPostProcessOp::execute_not_supported, + ArgmaxPostProcessOp::execute_not_supported + }, + { + // NHWC x UINT8 + ArgmaxPostProcessOp::execute_not_supported, // We don't support output_format_type to be auto + ArgmaxPostProcessOp::NHWC_to_NHW_feature_axis, + ArgmaxPostProcessOp::NHWC_to_NHW_feature_axis, + ArgmaxPostProcessOp::NHWC_to_NHW_feature_axis + }, + { + // NHWC x UINT16 + ArgmaxPostProcessOp::execute_not_supported, // We don't support output_format_type to be auto + ArgmaxPostProcessOp::NHWC_to_NHW_feature_axis, + ArgmaxPostProcessOp::NHWC_to_NHW_feature_axis, + ArgmaxPostProcessOp::NHWC_to_NHW_feature_axis, + }, + { + // NHWC x FLOAT32 + ArgmaxPostProcessOp::execute_not_supported, + ArgmaxPostProcessOp::execute_not_supported, + ArgmaxPostProcessOp::execute_not_supported, + ArgmaxPostProcessOp::execute_not_supported + } + }, + { + { + // NC x AUTO + // We don't support input_format_type to be auto + ArgmaxPostProcessOp::execute_not_supported, + ArgmaxPostProcessOp::execute_not_supported, + ArgmaxPostProcessOp::execute_not_supported, + ArgmaxPostProcessOp::execute_not_supported + }, + { + // NC x UINT8 + ArgmaxPostProcessOp::execute_not_supported, // We don't support output_format_type to be auto + ArgmaxPostProcessOp::NC_to_N, + ArgmaxPostProcessOp::NC_to_N, + ArgmaxPostProcessOp::NC_to_N, + }, + { + // NC x UINT16 + ArgmaxPostProcessOp::execute_not_supported, // We don't support output_format_type to be auto + ArgmaxPostProcessOp::NC_to_N, + ArgmaxPostProcessOp::NC_to_N, + ArgmaxPostProcessOp::NC_to_N, + }, + { + // NC x FLOAT32 + // We don't support input_format_type to be float32 + ArgmaxPostProcessOp::execute_not_supported, + ArgmaxPostProcessOp::execute_not_supported, + ArgmaxPostProcessOp::execute_not_supported, + ArgmaxPostProcessOp::execute_not_supported + } + } +}; + +hailo_status ArgmaxPostProcessOp::execute(const std::map &inputs, + std::map &outputs) +{ + auto &input_name = inputs.begin()->first; + auto &output_name = outputs.begin()->first; + auto &input_metadata = m_inputs_metadata[input_name]; + auto &output_metadata = m_outputs_metadata[output_name]; + + uint8_t format_index = UINT8_MAX; + switch (input_metadata.format.order) { + case HAILO_FORMAT_ORDER_NHCW: + format_index = 0; + break; + case HAILO_FORMAT_ORDER_NHWC: + format_index = 1; + break; + case HAILO_FORMAT_ORDER_NC: + format_index = 2; + break; + default: + LOGGER__ERROR("Argmax post-process received invalid input order {}", + HailoRTCommon::get_format_order_str(input_metadata.format.order)); + return HAILO_INVALID_ARGUMENT; + } + return ArgmaxPostProcessOp::m_argmax_function_array[format_index][input_metadata.format.type][output_metadata.format.type](input_metadata, output_metadata, inputs, outputs); +} + +std::string ArgmaxPostProcessOp::get_op_description() +{ + auto config_info = fmt::format("ArgmaxPostProcess Op, Name: {}", m_name); + return config_info; +} + +hailo_status ArgmaxPostProcessOp::validate_metadata() +{ + assert(m_inputs_metadata.size() == hailort::net_flow::ARGMAX_NUMBER_OF_SRCS); + assert(m_outputs_metadata.size() == hailort::net_flow::ARGMAX_NUMBER_OF_DSTS); + + auto &input_metadata = m_inputs_metadata.begin()->second; + auto &output_metadata = m_outputs_metadata.begin()->second; + + CHECK(( + ((output_metadata.format.type == HAILO_FORMAT_TYPE_UINT8) && (input_metadata.shape.features <= std::numeric_limits::max())) || + ((output_metadata.format.type == HAILO_FORMAT_TYPE_UINT16) && (input_metadata.shape.features <= std::numeric_limits::max())) || + ((output_metadata.format.type == HAILO_FORMAT_TYPE_FLOAT32) && (input_metadata.shape.features <= FLOAT_LAST_CONSECUTIVE_REPRESENTABLE_INT))), + HAILO_INVALID_OPERATION, "Dst format type {} can't represent possible range {} for Argmax op", + HailoRTCommon::get_format_type_str(output_metadata.format.type), input_metadata.shape.features); + CHECK( + ((input_metadata.format.order == HAILO_FORMAT_ORDER_NHCW) && (output_metadata.format.order == HAILO_FORMAT_ORDER_NHW)) || + ((input_metadata.format.order == HAILO_FORMAT_ORDER_NHWC) && (output_metadata.format.order == HAILO_FORMAT_ORDER_NHW)) || + ((input_metadata.format.order == HAILO_FORMAT_ORDER_NC) && (output_metadata.format.order == HAILO_FORMAT_ORDER_NC)), + HAILO_INVALID_OPERATION, "Argmax op is not supported for src format order ({}) and dst format order ({})", + HailoRTCommon::get_format_order_str(input_metadata.format.order), + HailoRTCommon::get_format_order_str(output_metadata.format.order)); + + CHECK(output_metadata.shape.features == hailort::net_flow::ARGMAX_OUTPUT_FEATURES_SIZE, HAILO_INVALID_OPERATION, + "Dst features ({}) must be 1 on Argmax op", output_metadata.shape.features); + CHECK(input_metadata.shape.height == output_metadata.shape.height, HAILO_INVALID_OPERATION, + "Argmax op is supported only when src height ({}) is equal to dst height ({})", + input_metadata.shape.height, output_metadata.shape.height); + CHECK(input_metadata.shape.width == output_metadata.shape.width, HAILO_INVALID_OPERATION, + "Argmax op is supported only when src width ({}) is equal to dst width ({})", + input_metadata.shape.width, output_metadata.shape.width); + CHECK(( + (input_metadata.format.type == HAILO_FORMAT_TYPE_UINT8) || (input_metadata.format.type == HAILO_FORMAT_TYPE_UINT16)), + HAILO_INVALID_OPERATION, "Src format type {} is not valid. Must be either {} or {}", + HailoRTCommon::get_format_type_str(input_metadata.format.type), HailoRTCommon::get_format_type_str(HAILO_FORMAT_TYPE_UINT8), + HailoRTCommon::get_format_type_str(HAILO_FORMAT_TYPE_UINT16)); + + return HAILO_SUCCESS; +} + +Expected> ArgmaxPostProcessOp::create(const std::map &inputs_metadata, + std::map &outputs_metadata) +{ + auto op = std::shared_ptr(new (std::nothrow) ArgmaxPostProcessOp(inputs_metadata, outputs_metadata)); + CHECK_AS_EXPECTED(op != nullptr, HAILO_OUT_OF_HOST_MEMORY); + + return std::shared_ptr(std::move(op)); +} + +} /* namespace net_flow */ +} /* namespace hailort */ diff --git a/hailort/libhailort/src/net_flow/ops/argmax_post_process.hpp b/hailort/libhailort/src/net_flow/ops/argmax_post_process.hpp new file mode 100644 index 0000000..23dd6b4 --- /dev/null +++ b/hailort/libhailort/src/net_flow/ops/argmax_post_process.hpp @@ -0,0 +1,151 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file argmax_post_process.hpp + * @brief: Argmax op perform argmax op as described: https://www.tensorflow.org/api_docs/python/tf/math/argmax + * A few notes: + * - Support only on features axis + * - Support only on NHWC, NHCW and NC input data order + * - In case of 2 maximal values - the lower index one will be given. + **/ + +#ifndef _HAILO_ARGMAX_POST_PROCESS_HPP_ +#define _HAILO_ARGMAX_POST_PROCESS_HPP_ + + +#include "hailo/hailort.h" +#include "net_flow/ops/op.hpp" +#include "common/utils.hpp" + +#include + +namespace hailort +{ +namespace net_flow +{ + +#define ARGMAX_NUM_OF_POSSIBLE_FORMAT_ORDERS (3) +#define ARGMAX_NUM_OF_POSSIBLE_FORMAT_TYPES (4) + +constexpr std::size_t ARGMAX_OUTPUT_FEATURES_SIZE {1}; +constexpr std::size_t ARGMAX_NUMBER_OF_SRCS {1}; +constexpr std::size_t ARGMAX_NUMBER_OF_DSTS {1}; + +typedef hailo_status (*ArgmaxFunction)(const BufferMetaData &input_metadata, const BufferMetaData &output_metadata, + const std::map &inputs, std::map &outputs); + +class ArgmaxPostProcessOp : public Op +{ + +private: + ArgmaxPostProcessOp(const std::map &inputs_metadata, + const std::map &outputs_metadata) + : Op(inputs_metadata, outputs_metadata, "Argmax-Post-Process") + {} + + template + static hailo_status NHCW_to_NHW_feature_axis(const BufferMetaData &input_metadata, const BufferMetaData &output_metadata, + const std::map &inputs, std::map &outputs) + { + auto src_ptr = (DeviceType*)inputs.begin()->second.data(); + auto dst_ptr = (HostType*)outputs.begin()->second.data(); + const auto src_row_size = input_metadata.padded_shape.width * input_metadata.padded_shape.features; + const auto dst_row_size = output_metadata.shape.width; + + for (uint32_t r = 0; r < input_metadata.shape.height; r++) { + const DeviceType *src_row = src_ptr + (r * src_row_size); + HostType *dst_row = dst_ptr + (r * dst_row_size); + for (uint32_t w = 0; w < input_metadata.shape.width; w++) { + const DeviceType *offset_in_row = src_row + w; + HostType max_index = 0; + auto max_value = *offset_in_row; + for (uint32_t c = 1; c < input_metadata.shape.features; c++) { + offset_in_row += input_metadata.padded_shape.width; + const auto ¤t_value = *offset_in_row; + if (current_value > max_value) { + max_index = static_cast(c); + max_value = current_value; + } + } + dst_row[w] = max_index; + } + } + return HAILO_SUCCESS; + } + + template + static hailo_status NHWC_to_NHW_feature_axis(const BufferMetaData &input_metadata, const BufferMetaData &output_metadata, + const std::map &inputs, std::map &outputs) + { + auto src_ptr = (DeviceType*)inputs.begin()->second.data(); + auto dst_ptr = (HostType*)outputs.begin()->second.data(); + const auto src_row_size = input_metadata.padded_shape.width * input_metadata.padded_shape.features; + const auto dst_row_size = output_metadata.shape.width; + + for (uint32_t r = 0; r < input_metadata.shape.height; r++) { + const DeviceType *src_row = src_ptr + (r * src_row_size); + HostType *dst_row = dst_ptr + (r * dst_row_size); + for (uint32_t w = 0; w < input_metadata.shape.width; w++) { + const DeviceType *offset_in_row = src_row + (w * input_metadata.padded_shape.features); + HostType max_index = 0; + auto max_value = *offset_in_row; + for (uint32_t c = 1; c < input_metadata.shape.features; c++) { + const auto ¤t_value = *(offset_in_row + c); + if (current_value > max_value) { + max_index = static_cast(c); + max_value = current_value; + } + } + dst_row[w] = max_index; + } + } + return HAILO_SUCCESS; + } + + template + static hailo_status NC_to_N(const BufferMetaData &input_metadata, const BufferMetaData &output_metadata, + const std::map &inputs, std::map &outputs) + { + (void) output_metadata; // only reason to have output_metadata is so that the function array will work + auto src_ptr = (DeviceType*)inputs.begin()->second.data(); + auto dst_ptr = (HostType*)outputs.begin()->second.data(); + HostType max_index = 0; + DeviceType max_value = 0; + + for (uint32_t c = 0; c < input_metadata.shape.features; c++) { + const auto ¤t_value = *(src_ptr + c); + if (current_value > max_value) { + max_index = static_cast(c); + max_value = current_value; + } + } + *dst_ptr = max_index; + return HAILO_SUCCESS; + } + + static hailo_status execute_not_supported(const BufferMetaData &input_metadata, const BufferMetaData &output_metadata, + const std::map &inputs, std::map &outputs); + +public: + static Expected> create(const std::map &inputs_metadata, + std::map &outputs_metadata); + virtual hailo_status execute(const std::map &inputs, + std::map &outputs) override; + virtual std::string get_op_description() override; + hailo_status validate_metadata() override; + + // A 3D array of argmax functions to call: + // 1st dim represent the data format order + // 2nd dim represent the input data type (only uint8 or uint16 are valid) + // 3rd dim represent the output data type + // Note: Assumption here the ordering of the enum hailo_format_type_t doesn't change + static ArgmaxFunction m_argmax_function_array[ARGMAX_NUM_OF_POSSIBLE_FORMAT_ORDERS][ARGMAX_NUM_OF_POSSIBLE_FORMAT_TYPES][ARGMAX_NUM_OF_POSSIBLE_FORMAT_TYPES]; + +}; + +} /* namespace net_flow */ +} /* namespace hailort */ + +#endif /* _HAILO_ARGMAX_POST_PROCESS_HPP_ */ diff --git a/hailort/libhailort/src/net_flow/ops/nms_post_process.cpp b/hailort/libhailort/src/net_flow/ops/nms_post_process.cpp index 1bf1859..8fced3b 100644 --- a/hailort/libhailort/src/net_flow/ops/nms_post_process.cpp +++ b/hailort/libhailort/src/net_flow/ops/nms_post_process.cpp @@ -15,6 +15,44 @@ namespace hailort { namespace net_flow { + + hailo_status NmsPostProcessOp::validate_metadata() + { + for (const auto& output_metadata : m_outputs_metadata) { + CHECK(HAILO_FORMAT_ORDER_HAILO_NMS == output_metadata.second.format.order, HAILO_INVALID_ARGUMENT, "The given output format order {} is not supported, " + "should be HAILO_FORMAT_ORDER_HAILO_NMS", HailoRTCommon::get_format_order_str(output_metadata.second.format.order)); + + CHECK(HAILO_FORMAT_TYPE_FLOAT32 == output_metadata.second.format.type, HAILO_INVALID_ARGUMENT, "The given output format type {} is not supported, " + "should be HAILO_FORMAT_TYPE_FLOAT32", HailoRTCommon::get_format_type_str(output_metadata.second.format.type)); + + CHECK(!(HAILO_FORMAT_FLAGS_TRANSPOSED & output_metadata.second.format.flags), HAILO_INVALID_ARGUMENT, "Output {} is marked as transposed, which is not supported for this model.", + output_metadata.first); + CHECK(!(HAILO_FORMAT_FLAGS_HOST_ARGMAX & output_metadata.second.format.flags), HAILO_INVALID_ARGUMENT, "Output {} is marked as argmax, which is not supported for this model.", + output_metadata.first); + CHECK(!(HAILO_FORMAT_FLAGS_QUANTIZED & output_metadata.second.format.flags), HAILO_INVALID_ARGUMENT, "Output {} is marked as quantized, which is not supported for this model.", + output_metadata.first); + } + + assert(1 <= m_inputs_metadata.size()); + const hailo_format_type_t& first_input_type = m_inputs_metadata.begin()->second.format.type; + for (const auto& input_metadata : m_inputs_metadata) { + CHECK(HAILO_FORMAT_ORDER_NHCW == input_metadata.second.format.order, HAILO_INVALID_ARGUMENT, "The given input format order {} is not supported, " + "should be HAILO_FORMAT_ORDER_NHCW", HailoRTCommon::get_format_order_str(input_metadata.second.format.order)); + + CHECK((HAILO_FORMAT_TYPE_UINT8 == input_metadata.second.format.type) || + (HAILO_FORMAT_TYPE_UINT16 == input_metadata.second.format.type), + HAILO_INVALID_ARGUMENT, "The given input format type {} is not supported, should be HAILO_FORMAT_TYPE_UINT8 or HAILO_FORMAT_TYPE_UINT16", + HailoRTCommon::get_format_type_str(input_metadata.second.format.type)); + + CHECK(input_metadata.second.format.type == first_input_type, HAILO_INVALID_ARGUMENT,"All inputs format type should be the same"); + + CHECK(HAILO_FORMAT_FLAGS_QUANTIZED == input_metadata.second.format.flags, HAILO_INVALID_ARGUMENT, "The given input format flag is not supported," + "should be HAILO_FORMAT_FLAGS_QUANTIZED"); + } + + return HAILO_SUCCESS; + } + float NmsPostProcessOp::compute_iou(const hailo_bbox_float32_t &box_1, const hailo_bbox_float32_t &box_2) { const float overlap_area_width = std::min(box_1.x_max, box_2.x_max) - std::max(box_1.x_min, box_2.x_min); @@ -64,10 +102,9 @@ namespace net_flow std::vector &classes_detections_count) { // Calculate the number of detections before each class, to help us later calculate the buffer_offset for it's detections. - std::vector num_of_detections_before; - num_of_detections_before.reserve(m_nms_config.classes); + std::vector num_of_detections_before(m_nms_config.number_of_classes, 0); uint32_t ignored_detections_count = 0; - for (size_t class_idx = 0; class_idx < m_nms_config.classes; class_idx++) { + for (size_t class_idx = 0; class_idx < m_nms_config.number_of_classes; class_idx++) { if (classes_detections_count[class_idx] > m_nms_config.max_proposals_per_class) { ignored_detections_count += (classes_detections_count[class_idx] - m_nms_config.max_proposals_per_class); classes_detections_count[class_idx] = m_nms_config.max_proposals_per_class; @@ -123,7 +160,7 @@ namespace net_flow std::string NmsPostProcessOp::get_nms_config_description() { auto config_info = fmt::format("Score threshold: {:.3f}, Iou threshold: {:.2f}, Classes: {}, Cross classes: {}", - m_nms_config.nms_score_th, m_nms_config.nms_iou_th, m_nms_config.classes, m_nms_config.cross_classes); + m_nms_config.nms_score_th, m_nms_config.nms_iou_th, m_nms_config.number_of_classes, m_nms_config.cross_classes); if (m_nms_config.background_removal) { config_info += fmt::format(", Background removal index: {}", m_nms_config.background_removal_index); } diff --git a/hailort/libhailort/src/net_flow/ops/nms_post_process.hpp b/hailort/libhailort/src/net_flow/ops/nms_post_process.hpp index 8b95a84..e7c9d59 100644 --- a/hailort/libhailort/src/net_flow/ops/nms_post_process.hpp +++ b/hailort/libhailort/src/net_flow/ops/nms_post_process.hpp @@ -66,7 +66,7 @@ struct NmsPostProcessConfig uint32_t max_proposals_per_class = 0; // The model's number of classes. (This depends on the dataset that the model trained on). - uint32_t classes = 0; + uint32_t number_of_classes = 0; // Toggle background class removal from results bool background_removal = false; @@ -107,7 +107,7 @@ protected: float32_t objectness, hailo_quant_info_t quant_info, uint32_t width) { std::pair max_id_score_pair; - for (uint32_t class_index = 0; class_index < m_nms_config.classes; class_index++) { + for (uint32_t class_index = 0; class_index < m_nms_config.number_of_classes; class_index++) { auto class_id = class_index; if (m_nms_config.background_removal) { if (m_nms_config.background_removal_index == class_index) { @@ -158,6 +158,8 @@ protected: std::string get_nms_config_description(); + hailo_status validate_metadata() override; + }; } diff --git a/hailort/libhailort/src/net_flow/ops/op.hpp b/hailort/libhailort/src/net_flow/ops/op.hpp index d6a02b3..cd8b3ae 100644 --- a/hailort/libhailort/src/net_flow/ops/op.hpp +++ b/hailort/libhailort/src/net_flow/ops/op.hpp @@ -50,6 +50,8 @@ public: */ virtual hailo_status execute(const std::map &inputs, std::map &outputs) = 0; + virtual hailo_status validate_metadata() = 0; + const std::map &inputs_metadata() const { return m_inputs_metadata; @@ -60,6 +62,16 @@ public: return m_outputs_metadata; } + void set_outputs_metadata(std::map &outputs_metadata) + { + m_outputs_metadata = outputs_metadata; + } + + void set_inputs_metadata(std::map &inputs_metadata) + { + m_inputs_metadata = inputs_metadata; + } + std::string get_name() { return m_name; } diff --git a/hailort/libhailort/src/net_flow/ops/softmax_post_process.cpp b/hailort/libhailort/src/net_flow/ops/softmax_post_process.cpp new file mode 100644 index 0000000..97fb1e3 --- /dev/null +++ b/hailort/libhailort/src/net_flow/ops/softmax_post_process.cpp @@ -0,0 +1,192 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file softmax_post_process.cpp + * @brief: Softmax op + **/ + +#include "softmax_post_process.hpp" +#include "hailo/hailort.h" +#include "hailo/hailort_common.hpp" +#include "common/utils.hpp" + +#include + +namespace hailort +{ +namespace net_flow +{ + +// This function is for when trying to perform softmax op for unsupported formats +hailo_status SoftmaxPostProcessOp::execute_not_supported(const BufferMetaData &input_metadata, const BufferMetaData &output_metadata, + const std::map &inputs, std::map &outputs) + { + (void)inputs; + (void)outputs; + LOGGER__ERROR("Softmax post-process not supported with params: input_order {}, input_type {}, output_type {}", + HailoRTCommon::get_format_order_str(input_metadata.format.order), + HailoRTCommon::get_format_type_str(input_metadata.format.type), + HailoRTCommon::get_format_type_str(output_metadata.format.type)); + return HAILO_INVALID_ARGUMENT; + } + +SoftmaxFunction SoftmaxPostProcessOp::m_softmax_function_array[SOFTMAX_NUM_OF_POSSIBLE_FORMAT_ORDERS][SOFTMAX_NUM_OF_POSSIBLE_FORMAT_TYPES][SOFTMAX_NUM_OF_POSSIBLE_FORMAT_TYPES] +{ + // Currently supported on: + // NC, float_32 to NC, float_32 + // NHWC, float_32 to NHWC, float_32 + { + { + // NHWC x AUTO + // We don't support input_format_type to be auto + SoftmaxPostProcessOp::execute_not_supported, + SoftmaxPostProcessOp::execute_not_supported, + SoftmaxPostProcessOp::execute_not_supported, + SoftmaxPostProcessOp::execute_not_supported + }, + { + // NHWC x UINT8 + // We don't support input_format_type to be UINT8 + SoftmaxPostProcessOp::execute_not_supported, + SoftmaxPostProcessOp::execute_not_supported, + SoftmaxPostProcessOp::execute_not_supported, + SoftmaxPostProcessOp::execute_not_supported + }, + { + // NHWC x UINT16 + // We don't support input_format_type to be UINT16 + SoftmaxPostProcessOp::execute_not_supported, + SoftmaxPostProcessOp::execute_not_supported, + SoftmaxPostProcessOp::execute_not_supported, + SoftmaxPostProcessOp::execute_not_supported + }, + { + // NHWC x FLOAT32 + SoftmaxPostProcessOp::execute_not_supported, // We don't support output_format_type format of AUTO + SoftmaxPostProcessOp::execute_not_supported, // We don't support output_format_type format of UINT8 + SoftmaxPostProcessOp::execute_not_supported, // We don't support output_format_type format of UINT16 + SoftmaxPostProcessOp::NHWC_to_NHWC_feature_axis + } + }, + { + { + // NC x AUTO + // We don't support input_format_type to be auto + SoftmaxPostProcessOp::execute_not_supported, + SoftmaxPostProcessOp::execute_not_supported, + SoftmaxPostProcessOp::execute_not_supported, + SoftmaxPostProcessOp::execute_not_supported + }, + { + // NC x UINT8 + // We don't support input_format_type to be UINT8 + SoftmaxPostProcessOp::execute_not_supported, + SoftmaxPostProcessOp::execute_not_supported, + SoftmaxPostProcessOp::execute_not_supported, + SoftmaxPostProcessOp::execute_not_supported, + }, + { + // NC x UINT16 + // We don't support input_format_type to be UINT16 + SoftmaxPostProcessOp::execute_not_supported, + SoftmaxPostProcessOp::execute_not_supported, + SoftmaxPostProcessOp::execute_not_supported, + SoftmaxPostProcessOp::execute_not_supported, + }, + { + // NC x FLOAT32 + SoftmaxPostProcessOp::execute_not_supported, // We don't support output_format_type format of AUTO + SoftmaxPostProcessOp::execute_not_supported, // We don't support output_format_type format of UINT8 + SoftmaxPostProcessOp::execute_not_supported, // We don't support output_format_type format of UINT16 + SoftmaxPostProcessOp::NC_to_NC, + } + } +}; + +hailo_status SoftmaxPostProcessOp::execute(const std::map &inputs, + std::map &outputs) +{ + auto &input_name = inputs.begin()->first; + auto &output_name = outputs.begin()->first; + auto &input_metadata = m_inputs_metadata[input_name]; + auto &output_metadata = m_outputs_metadata[output_name]; + + uint8_t format_index = UINT8_MAX; + switch (input_metadata.format.order) { + case HAILO_FORMAT_ORDER_NHWC: + format_index = 0; + break; + case HAILO_FORMAT_ORDER_NC: + format_index = 1; + break; + default: + LOGGER__ERROR("Softmax post-process received invalid input order {}", + HailoRTCommon::get_format_order_str(input_metadata.format.order)); + return HAILO_INVALID_ARGUMENT; + } + return SoftmaxPostProcessOp::m_softmax_function_array[format_index][input_metadata.format.type][output_metadata.format.type](input_metadata, output_metadata, inputs, outputs); +} + +std::string SoftmaxPostProcessOp::get_op_description() +{ + auto config_info = fmt::format("SoftmaxPostProcess Op, Name: {}", m_name); + return config_info; +} + +hailo_status SoftmaxPostProcessOp::validate_metadata() +{ + assert(m_inputs_metadata.size() == hailort::net_flow::SOFTMAX_NUMBER_OF_SRCS); + assert(m_outputs_metadata.size() == hailort::net_flow::SOFTMAX_NUMBER_OF_DSTS); + + auto &input_metadata = m_inputs_metadata.begin()->second; + auto &output_metadata = m_outputs_metadata.begin()->second; + + CHECK( + ((input_metadata.format.flags & HAILO_FORMAT_FLAGS_QUANTIZED) == 0) && ((output_metadata.format.flags & HAILO_FORMAT_FLAGS_QUANTIZED) == 0), + HAILO_INVALID_OPERATION, "Softmax op is supported only on dequantized data"); + + CHECK( + ((input_metadata.format.order == HAILO_FORMAT_ORDER_NHWC) && (output_metadata.format.order == HAILO_FORMAT_ORDER_NHWC)) || + ((input_metadata.format.order == HAILO_FORMAT_ORDER_NC) && (output_metadata.format.order == HAILO_FORMAT_ORDER_NC)), + HAILO_INVALID_OPERATION, "Softmax op is not supported for src format order ({}) and dst format order ({})", + HailoRTCommon::get_format_order_str(input_metadata.format.order), + HailoRTCommon::get_format_order_str(output_metadata.format.order)); + + CHECK(input_metadata.shape.features == output_metadata.shape.features, HAILO_INVALID_OPERATION, + "Softmax op is supported only when src num of features ({}) is equal to dst num of features ({})", + input_metadata.shape.features, output_metadata.shape.features); + CHECK(input_metadata.shape.height == output_metadata.shape.height, HAILO_INVALID_OPERATION, + "Softmax op is supported only when src height ({}) is equal to dst height ({})", + input_metadata.shape.height, output_metadata.shape.height); + CHECK(input_metadata.shape.width == output_metadata.shape.width, HAILO_INVALID_OPERATION, + "Softmax op is supported only when src width ({}) is equal to dst width ({})", + input_metadata.shape.width, output_metadata.shape.width); + CHECK(input_metadata.format.type == HAILO_FORMAT_TYPE_FLOAT32, + HAILO_INVALID_OPERATION, "Src format type {} is not valid. Must be {}", + HailoRTCommon::get_format_type_str(input_metadata.format.type), + HailoRTCommon::get_format_type_str(HAILO_FORMAT_TYPE_FLOAT32)); + CHECK(output_metadata.format.type == HAILO_FORMAT_TYPE_FLOAT32, + HAILO_INVALID_OPERATION, "Dst format type {} is not valid. Must be {}", + HailoRTCommon::get_format_type_str(output_metadata.format.type), + HailoRTCommon::get_format_type_str(HAILO_FORMAT_TYPE_FLOAT32)); + CHECK(!(HAILO_FORMAT_FLAGS_HOST_ARGMAX & output_metadata.format.flags), HAILO_INVALID_ARGUMENT, "Output {} is marked as argmax, which is not supported for this model.", + m_outputs_metadata.begin()->first); + CHECK(!(HAILO_FORMAT_FLAGS_QUANTIZED & output_metadata.format.flags), HAILO_INVALID_ARGUMENT, "Output {} is marked as quantized, which is not supported for this model.", + m_outputs_metadata.begin()->first); + + return HAILO_SUCCESS; +} + +Expected> SoftmaxPostProcessOp::create(const std::map &inputs_metadata, + std::map &outputs_metadata) +{ + auto op = std::shared_ptr(new (std::nothrow) SoftmaxPostProcessOp(inputs_metadata, outputs_metadata)); + CHECK_AS_EXPECTED(op != nullptr, HAILO_OUT_OF_HOST_MEMORY); + + return std::shared_ptr(std::move(op)); +} + +} /* namespace net_flow */ +} /* namespace hailort */ \ No newline at end of file diff --git a/hailort/libhailort/src/net_flow/ops/softmax_post_process.hpp b/hailort/libhailort/src/net_flow/ops/softmax_post_process.hpp new file mode 100644 index 0000000..9ebdb8a --- /dev/null +++ b/hailort/libhailort/src/net_flow/ops/softmax_post_process.hpp @@ -0,0 +1,139 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file softmax_post_process.hpp + * @brief: Softmax op perform softmax op as described: https://www.tensorflow.org/api_docs/python/tf/nn/softmax + * A few notes: + * - Support only on features axis + * - Support only on NHWC and NC input data order + **/ + +#ifndef _HAILO_SOFTMAX_POST_PROCESS_HPP_ +#define _HAILO_SOFTMAX_POST_PROCESS_HPP_ + +#include "hailo/hailort.h" +#include "net_flow/ops/op.hpp" +#include "common/utils.hpp" +#include "hailo/quantization.hpp" + +#include + +namespace hailort +{ +namespace net_flow +{ + +#define SOFTMAX_NUM_OF_POSSIBLE_FORMAT_ORDERS (2) // NHWC, NC +#define SOFTMAX_NUM_OF_POSSIBLE_FORMAT_TYPES (4) // Auto, UINT8, UINT16, FLOAT32 + +constexpr std::size_t SOFTMAX_NUMBER_OF_SRCS {1}; +constexpr std::size_t SOFTMAX_NUMBER_OF_DSTS {1}; + +typedef hailo_status (*SoftmaxFunction)(const BufferMetaData &input_metadata, const BufferMetaData &output_metadata, + const std::map &inputs, std::map &outputs); + +class SoftmaxPostProcessOp : public Op +{ + +private: + SoftmaxPostProcessOp(const std::map &inputs_metadata, + const std::map &outputs_metadata) + : Op(inputs_metadata, outputs_metadata, "Softmax-Post-Process") + {} + + template + static hailo_status NHWC_to_NHWC_feature_axis(const BufferMetaData &input_metadata, const BufferMetaData &output_metadata, + const std::map &inputs, std::map &outputs) + { + auto src_ptr = (dst_type*)inputs.begin()->second.data(); + auto dst_ptr = (src_type*)outputs.begin()->second.data(); + const auto src_row_size = input_metadata.shape.width * input_metadata.shape.features; + const auto dst_row_size = output_metadata.shape.width * output_metadata.shape.features; + const auto src_width_size = input_metadata.shape.features; + const auto dst_width_size = output_metadata.shape.features; + + for (uint32_t r = 0; r < input_metadata.shape.height; r++) { // H axis - rows + dst_type *src_row = src_ptr + (r * src_row_size); + src_type *dst_row = dst_ptr + (r * dst_row_size); + for (uint32_t w = 0; w < input_metadata.shape.width; w++) { // W axis - coloums + dst_type *src_col = src_row + (w * src_width_size); + src_type *dst_col = dst_row + (w * dst_width_size); + // In order to avoid overflows, we will perform the following: + // For each HW, we will find the maximal c value and then we will substract this value from + // all of the values in this HW. This will preserve the original softmax values + prevent overflows + src_type max_val = std::numeric_limits::min(); + for (uint32_t c = 0; c < input_metadata.shape.features; c++) { + auto ¤t_value = *(src_col + c); + if (current_value > max_val) + max_val = current_value; + } + dst_type sum_exp = 0; // denominator + for (uint32_t c = 0; c < input_metadata.shape.features; c++) { // C axis - features + auto ¤t_value = *(src_col + c); + current_value -= max_val; // This step preserves the original softmax values + prevent overflows + current_value = std::exp(static_cast(current_value)); // Set src_ptr[c] to e^(src_ptr[c]) so that we only calculate it once + sum_exp += current_value; + } + for (uint32_t c = 0; c < input_metadata.shape.features; c++) { + const auto ¤t_value = *(src_col + c); + dst_col[c] = static_cast(current_value / sum_exp); + } + } + } + return HAILO_SUCCESS; + } + + template + static hailo_status NC_to_NC(const BufferMetaData &input_metadata, const BufferMetaData &output_metadata, + const std::map &inputs, std::map &outputs) + { + (void) output_metadata; + auto src_ptr = (src_type*)inputs.begin()->second.data(); + auto dst_ptr = (dst_type*)outputs.begin()->second.data(); + // In order to avoid overflows, we will perform the following: + // For each HW, we will find the maximal c value and then we will substract this value from + // all of the values in this HW. This will preserve the original softmax values + prevent overflows + src_type max_val = std::numeric_limits::min(); + for (uint32_t c = 0; c < input_metadata.shape.features; c++) { + auto ¤t_value = *(src_ptr + c); + if (current_value > max_val) + max_val = current_value; + } + dst_type sum_exp = 0; + for (uint32_t c = 0; c < input_metadata.shape.features; c++) { + auto ¤t_value = *(src_ptr + c); + current_value -= max_val; // This step preserves the original softmax values + prevent overflows + current_value = std::exp(static_cast(current_value)); // Set src_ptr[c] to e^(src_ptr[c]) + sum_exp += current_value; + } + for (uint32_t c = 0; c < input_metadata.shape.features; c++) { + dst_ptr[c] = static_cast(src_ptr[c] / sum_exp); + } + return HAILO_SUCCESS; + } + + static hailo_status execute_not_supported(const BufferMetaData &input_metadata, const BufferMetaData &output_metadata, + const std::map &inputs, std::map &outputs); + + public: + static Expected> create(const std::map &inputs_metadata, + std::map &outputs_metadata); + virtual hailo_status execute(const std::map &inputs, + std::map &outputs) override; + virtual std::string get_op_description() override; + hailo_status validate_metadata() override; + + // A 3D array of softmax functions to call: + // 1st dim represent the data format order (NHWC and NC are supported) + // 2nd dim represent the input data type (only float_32 is supported) + // 3rd dim represent the output data type (only float_32 is supported) + static SoftmaxFunction m_softmax_function_array[SOFTMAX_NUM_OF_POSSIBLE_FORMAT_ORDERS][SOFTMAX_NUM_OF_POSSIBLE_FORMAT_TYPES][SOFTMAX_NUM_OF_POSSIBLE_FORMAT_TYPES]; + +}; + +} /* namespace net_flow */ +} /* namespace hailort */ + +#endif /* _HAILO_SOFTMAX_POST_PROCESS_HPP_ */ \ No newline at end of file diff --git a/hailort/libhailort/src/net_flow/ops/ssd_post_process.cpp b/hailort/libhailort/src/net_flow/ops/ssd_post_process.cpp index 12816c1..4504630 100644 --- a/hailort/libhailort/src/net_flow/ops/ssd_post_process.cpp +++ b/hailort/libhailort/src/net_flow/ops/ssd_post_process.cpp @@ -16,16 +16,21 @@ namespace hailort namespace net_flow { +hailo_status SSDPostProcessOp::validate_metadata() +{ + auto status = NmsPostProcessOp::validate_metadata(); + if (HAILO_SUCCESS != status) { + return status; + } + + return HAILO_SUCCESS; +} + Expected> SSDPostProcessOp::create(const std::map &inputs_metadata, const std::map &outputs_metadata, const NmsPostProcessConfig &nms_post_process_config, const SSDPostProcessConfig &ssd_post_process_config) { - for (auto &name_to_inputs_metadata : inputs_metadata) { - CHECK_AS_EXPECTED(name_to_inputs_metadata.second.format.order == HAILO_FORMAT_ORDER_NHCW, HAILO_INVALID_ARGUMENT, - "SSDPostProcessOp: Unexpected input format {}", name_to_inputs_metadata.second.format.order); - } - // Validate each anchor is mapped by reg and cls inputs for (const auto ®_to_cls_name : ssd_post_process_config.reg_to_cls_inputs) { CHECK_AS_EXPECTED(ssd_post_process_config.anchors.count(reg_to_cls_name.first), HAILO_INVALID_ARGUMENT, @@ -63,6 +68,7 @@ Expected> SSDPostProcessOp::create(const std::map(new (std::nothrow) SSDPostProcessOp(inputs_metadata, outputs_metadata, nms_post_process_config, ssd_post_process_config)); CHECK_AS_EXPECTED(op != nullptr, HAILO_OUT_OF_HOST_MEMORY); + return std::shared_ptr(std::move(op)); } @@ -73,8 +79,8 @@ hailo_status SSDPostProcessOp::execute(const std::map & m_ssd_config.anchors.size(), inputs.size()); std::vector detections; - std::vector classes_detections_count(m_nms_config.classes, 0); - detections.reserve(m_nms_config.max_proposals_per_class * m_nms_config.classes); + std::vector classes_detections_count(m_nms_config.number_of_classes, 0); + detections.reserve(m_nms_config.max_proposals_per_class * m_nms_config.number_of_classes); for (const auto ®_to_cls : m_ssd_config.reg_to_cls_inputs) { assert(inputs.count(reg_to_cls.first)); assert(inputs.count(reg_to_cls.second)); @@ -113,18 +119,20 @@ hailo_status SSDPostProcessOp::extract_detections(const std::string ®_input_n const auto &layer_anchors = m_ssd_config.anchors[reg_input_name]; assert(layer_anchors.size() % 2 == 0); const size_t num_of_anchors = (layer_anchors.size() / 2); + // TODO: HRT-11044 support mixed data types + auto data_size_in_bytes = HailoRTCommon::get_data_bytes(m_inputs_metadata.begin()->second.format.type); // Validate reg buffer size static const uint32_t reg_entry_size = 4; auto number_of_entries = reg_padded_shape.height * reg_padded_shape.width * num_of_anchors; - auto buffer_size = number_of_entries * reg_entry_size; + auto buffer_size = number_of_entries * reg_entry_size * data_size_in_bytes; CHECK(buffer_size == reg_buffer.size(), HAILO_INVALID_ARGUMENT, "Failed to extract_detections, reg {} buffer_size should be {}, but is {}", reg_input_name, buffer_size, reg_buffer.size()); // Validate cls buffer size - const uint32_t cls_entry_size = m_nms_config.classes; + const uint32_t cls_entry_size = m_nms_config.number_of_classes; number_of_entries = cls_padded_shape.height * cls_padded_shape.width * num_of_anchors; - buffer_size = number_of_entries * cls_entry_size; + buffer_size = number_of_entries * cls_entry_size * data_size_in_bytes; CHECK(buffer_size == cls_buffer.size(), HAILO_INVALID_ARGUMENT, "Failed to extract_detections, cls {} buffer_size should be {}, but is {}", cls_input_name, buffer_size, cls_buffer.size()); @@ -167,7 +175,7 @@ hailo_status SSDPostProcessOp::extract_detections(const std::string ®_input_n detections, classes_detections_count); CHECK_SUCCESS(status); } else if (m_inputs_metadata[reg_input_name].format.type == HAILO_FORMAT_TYPE_FLOAT32) { - // For testing - TODO: Remove after generator tests are in, and return error. + // For testing - TODO: HRT-9341 - Remove after generator tests are in, and return error. auto status = extract_bbox_detections( reg_input_name, cls_input_name, reg_buffer, cls_buffer, diff --git a/hailort/libhailort/src/net_flow/ops/ssd_post_process.hpp b/hailort/libhailort/src/net_flow/ops/ssd_post_process.hpp index 5bc9b10..bdce014 100644 --- a/hailort/libhailort/src/net_flow/ops/ssd_post_process.hpp +++ b/hailort/libhailort/src/net_flow/ops/ssd_post_process.hpp @@ -60,6 +60,7 @@ public: hailo_status execute(const std::map &inputs, std::map &outputs) override; std::string get_op_description() override; + hailo_status validate_metadata() override; // TODO: HRT-10676 static const uint32_t DEFAULT_Y_OFFSET_IDX = 0; static const uint32_t DEFAULT_X_OFFSET_IDX = 1; @@ -92,7 +93,7 @@ private: classes_detections_count[max_id_score_pair.first]++; } } else { - for (uint32_t class_index = 0; class_index < m_nms_config.classes; class_index++) { + for (uint32_t class_index = 0; class_index < m_nms_config.number_of_classes; class_index++) { auto class_id = class_index; if (m_nms_config.background_removal) { if (m_nms_config.background_removal_index == class_index) { diff --git a/hailort/libhailort/src/net_flow/ops/yolo_post_process.cpp b/hailort/libhailort/src/net_flow/ops/yolo_post_process.cpp index 2f6a118..2c4b90a 100644 --- a/hailort/libhailort/src/net_flow/ops/yolo_post_process.cpp +++ b/hailort/libhailort/src/net_flow/ops/yolo_post_process.cpp @@ -17,6 +17,17 @@ namespace hailort namespace net_flow { +hailo_status YOLOv5PostProcessOp::validate_metadata() +{ + auto status = NmsPostProcessOp::validate_metadata(); + if (HAILO_SUCCESS != status) { + return status; + } + + return HAILO_SUCCESS; +} + +//TODO- move to a dedicated module and maybe convert all yolo function to yolov5, HRT-10858 Expected> YOLOv5PostProcessOp::create(const std::map &inputs_metadata, const std::map &outputs_metadata, const NmsPostProcessConfig &nms_post_process_config, @@ -28,26 +39,7 @@ Expected> YOLOv5PostProcessOp::create(const std::map(new (std::nothrow) YOLOv5PostProcessOp(inputs_metadata, outputs_metadata, nms_post_process_config, yolo_post_process_config)); CHECK_AS_EXPECTED(op != nullptr, HAILO_OUT_OF_HOST_MEMORY); - return std::shared_ptr(std::move(op)); -} -Expected> YOLOXPostProcessOp::create(const std::map &inputs_metadata, - const std::map &outputs_metadata, - const NmsPostProcessConfig &nms_post_process_config, - const YoloPostProcessConfig &yolo_post_process_config) -{ - for (auto &name_to_inputs_metadata : inputs_metadata) { - CHECK_AS_EXPECTED(name_to_inputs_metadata.second.format.order == HAILO_FORMAT_ORDER_NHCW, HAILO_INVALID_ARGUMENT, - "YOLOv5PostProcessOp: Unexpected input format {}", name_to_inputs_metadata.second.format.order); - } - auto modified_yolo_post_process_config = yolo_post_process_config; - for (auto &name_to_meta : inputs_metadata) { - std::vector anchors = {1, 1}; - modified_yolo_post_process_config.anchors.insert({name_to_meta.first, anchors}); - } - auto op = std::shared_ptr(new (std::nothrow) YOLOXPostProcessOp(inputs_metadata, outputs_metadata, nms_post_process_config, - modified_yolo_post_process_config)); - CHECK_AS_EXPECTED(op != nullptr, HAILO_OUT_OF_HOST_MEMORY); return std::shared_ptr(std::move(op)); } @@ -58,8 +50,8 @@ hailo_status YOLOPostProcessOp::execute(const std::map m_yolo_config.anchors.size(), inputs.size()); std::vector detections; - std::vector classes_detections_count(m_nms_config.classes, 0); - detections.reserve(m_nms_config.max_proposals_per_class * m_nms_config.classes); + std::vector classes_detections_count(m_nms_config.number_of_classes, 0); + detections.reserve(m_nms_config.max_proposals_per_class * m_nms_config.number_of_classes); for (const auto &name_to_input : inputs) { hailo_status status; auto &name = name_to_input.first; @@ -71,7 +63,7 @@ hailo_status YOLOPostProcessOp::execute(const std::map status = extract_detections(name_to_input.second, input_metadata.quant_info, input_metadata.shape, input_metadata.padded_shape, m_yolo_config.anchors[name], detections, classes_detections_count); } else { - CHECK_SUCCESS(HAILO_INVALID_ARGUMENT, "YOLOv5 post-process received invalid input type"); + CHECK_SUCCESS(HAILO_INVALID_ARGUMENT, "YOLO post-process received invalid input type {}", input_metadata.format.type); } CHECK_SUCCESS(status); } @@ -100,18 +92,6 @@ hailo_bbox_float32_t YOLOv5PostProcessOp::decode(float32_t tx, float32_t ty, flo return hailo_bbox_float32_t{y_min, x_min, (y_min+h), (x_min+w), 0}; } -hailo_bbox_float32_t YOLOXPostProcessOp::decode(float32_t tx, float32_t ty, float32_t tw, float32_t th, - int wa, int ha, uint32_t col, uint32_t row, uint32_t w_stride, uint32_t h_stride) const -{ - auto w = exp(tw) * static_cast(wa) / m_yolo_config.image_width; - auto h = exp(th) * static_cast(ha) / m_yolo_config.image_height; - auto x_center = (tx + static_cast(col)) / static_cast(w_stride); - auto y_center = (ty + static_cast(row)) / static_cast(h_stride); - auto x_min = (x_center - (w / 2.0f)); - auto y_min = (y_center - (h / 2.0f)); - return hailo_bbox_float32_t{y_min, x_min, (y_min+h), (x_min+w), 0}; -} - } // namespace net_flow } // namespace hailort diff --git a/hailort/libhailort/src/net_flow/ops/yolo_post_process.hpp b/hailort/libhailort/src/net_flow/ops/yolo_post_process.hpp index 0c61ca1..049d587 100644 --- a/hailort/libhailort/src/net_flow/ops/yolo_post_process.hpp +++ b/hailort/libhailort/src/net_flow/ops/yolo_post_process.hpp @@ -39,6 +39,7 @@ class YOLOPostProcessOp : public NmsPostProcessOp public: hailo_status execute(const std::map &inputs, std::map &outputs) override; std::string get_op_description() override; + virtual hailo_status validate_metadata() = 0; // TODO: HRT-10676 protected: virtual hailo_bbox_float32_t decode(float32_t tx, float32_t ty, float32_t tw, float32_t th, @@ -92,10 +93,10 @@ private: assert(layer_anchors.size() % 2 == 0); const size_t num_of_anchors = (layer_anchors.size() / 2); - uint32_t entry_size = (uint32_t)((CLASSES_START_INDEX + m_nms_config.classes) * sizeof(DeviceType)); + uint32_t entry_size = (uint32_t)(CLASSES_START_INDEX + m_nms_config.number_of_classes); auto number_of_entries = padded_shape.height * padded_shape.width * num_of_anchors; // TODO: this can also be part of the Op configuration - auto buffer_size = number_of_entries * entry_size; + auto buffer_size = number_of_entries * entry_size * sizeof(DeviceType); CHECK(buffer_size == buffer.size(), HAILO_INVALID_ARGUMENT, "Failed to extract_detections, buffer_size should be {}, but is {}", buffer_size, buffer.size()); @@ -105,7 +106,6 @@ private: for (uint32_t col = 0; col < shape.width; col++) { for (uint32_t anchor = 0; anchor < num_of_anchors; anchor++) { auto entry_idx = (row_size * row) + col + ((anchor * entry_size) * padded_shape.width); - auto objectness = Quantization::dequantize_output(data[entry_idx + OBJECTNESS_OFFSET], quant_info); if (objectness < m_nms_config.nms_score_th) { continue; @@ -130,7 +130,7 @@ private: } } else { - for (uint32_t class_index = 0; class_index < m_nms_config.classes; class_index++) { + for (uint32_t class_index = 0; class_index < m_nms_config.number_of_classes; class_index++) { auto class_entry_idx = entry_idx + ((CLASSES_START_INDEX + class_index) * padded_shape.width); auto class_confidence = Quantization::dequantize_output( data[class_entry_idx], quant_info); @@ -157,6 +157,7 @@ public: const std::map &outputs_metadata, const NmsPostProcessConfig &nms_post_process_config, const YoloPostProcessConfig &yolo_post_process_config); + hailo_status validate_metadata() override; // TODO: HRT-10676 protected: virtual hailo_bbox_float32_t decode(float32_t tx, float32_t ty, float32_t tw, float32_t th, @@ -171,27 +172,6 @@ private: {} }; -class YOLOXPostProcessOp : public YOLOPostProcessOp -{ -public: - static Expected> create(const std::map &inputs_metadata, - const std::map &outputs_metadata, - const NmsPostProcessConfig &nms_post_process_config, - const YoloPostProcessConfig &yolo_post_process_config); - -protected: - virtual hailo_bbox_float32_t decode(float32_t tx, float32_t ty, float32_t tw, float32_t th, - int wa, int ha, uint32_t col, uint32_t row, uint32_t w_stride, uint32_t h_stride) const override; - -private: - YOLOXPostProcessOp(const std::map &inputs_metadata, - const std::map &outputs_metadata, - const NmsPostProcessConfig &nms_post_process_config, - const YoloPostProcessConfig &yolo_post_process_config) - : YOLOPostProcessOp(inputs_metadata, outputs_metadata, nms_post_process_config, yolo_post_process_config, "YOLOX-Post-Process") - {} -}; - } // namespace net_flow } // namespace hailort diff --git a/hailort/libhailort/src/net_flow/ops/yolox_post_process.cpp b/hailort/libhailort/src/net_flow/ops/yolox_post_process.cpp new file mode 100644 index 0000000..23229e8 --- /dev/null +++ b/hailort/libhailort/src/net_flow/ops/yolox_post_process.cpp @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file yolox_post_process.cpp + * @brief YOLOX post process + * + **/ + +#include "net_flow/ops/yolox_post_process.hpp" + +namespace hailort +{ +namespace net_flow +{ + +Expected> YOLOXPostProcessOp::create(const std::map &inputs_metadata, + const std::map &outputs_metadata, + const NmsPostProcessConfig &nms_post_process_config, + const YoloxPostProcessConfig &yolox_post_process_config) +{ + auto op = std::shared_ptr(new (std::nothrow) YOLOXPostProcessOp(inputs_metadata, outputs_metadata, nms_post_process_config, + yolox_post_process_config)); + CHECK_AS_EXPECTED(op != nullptr, HAILO_OUT_OF_HOST_MEMORY); + + return std::shared_ptr(std::move(op)); +} + +hailo_status YOLOXPostProcessOp::validate_metadata() +{ + auto status = NmsPostProcessOp::validate_metadata(); + if (HAILO_SUCCESS != status) { + return status; + } + + // Validate regs, clss and objs matching layers have same shape + for (const auto &layer_names : m_yolox_config.input_names) { + CHECK(contains(m_inputs_metadata, layer_names.reg), HAILO_INVALID_ARGUMENT, + "YOLOXPostProcessOp: inputs_metadata does not contain reg layer {}", layer_names.reg); + CHECK(contains(m_inputs_metadata, layer_names.cls), HAILO_INVALID_ARGUMENT, + "YOLOXPostProcessOp: inputs_metadata does not contain cls layer {}", layer_names.cls); + CHECK(contains(m_inputs_metadata, layer_names.obj), HAILO_INVALID_ARGUMENT, + "YOLOXPostProcessOp: inputs_metadata does not contain obj layer {}", layer_names.obj); + + const auto ®_input_metadata = m_inputs_metadata.at(layer_names.reg); + const auto &cls_input_metadata = m_inputs_metadata.at(layer_names.cls); + const auto &obj_input_metadata = m_inputs_metadata.at(layer_names.obj); + + // NOTE: padded shape might be different because features might be different, + // and padding is added when width*features % 8 != 0 + CHECK((reg_input_metadata.shape.height == cls_input_metadata.shape.height) + && (reg_input_metadata.shape.width == cls_input_metadata.shape.width), + HAILO_INVALID_ARGUMENT, "YOLOXPostProcess: reg input {} has different shape than cls input {}", + layer_names.reg, layer_names.cls); + CHECK((obj_input_metadata.shape.height == reg_input_metadata.shape.height) + && (obj_input_metadata.shape.width == reg_input_metadata.shape.width), + HAILO_INVALID_ARGUMENT, "YOLOXPostProcess: reg input {} has different shape than obj input {}", + layer_names.reg, layer_names.obj); + + CHECK((cls_input_metadata.format.type == reg_input_metadata.format.type) + && (cls_input_metadata.format.flags == reg_input_metadata.format.flags) + && (cls_input_metadata.format.order == reg_input_metadata.format.order), + HAILO_INVALID_ARGUMENT, "YOLOXPostProcess: reg input {} has different format than cls input {}", + layer_names.reg, layer_names.cls); + CHECK((obj_input_metadata.format.type == reg_input_metadata.format.type) + && (obj_input_metadata.format.flags == reg_input_metadata.format.flags) + && (obj_input_metadata.format.order == reg_input_metadata.format.order), + HAILO_INVALID_ARGUMENT, "YOLOXPostProcess: reg input {} has different format than obj input {}", + layer_names.reg, layer_names.obj); + + } + + return HAILO_SUCCESS; +} + +hailo_status YOLOXPostProcessOp::execute(const std::map &inputs, std::map &outputs) +{ + std::vector detections; + std::vector classes_detections_count(m_nms_config.number_of_classes, 0); + detections.reserve(m_nms_config.max_proposals_per_class * m_nms_config.number_of_classes); + for (const auto &layers_names_triplet : m_yolox_config.input_names) { + hailo_status status; + assert(contains(inputs, layers_names_triplet.cls)); + assert(contains(inputs, layers_names_triplet.obj)); + assert(contains(inputs, layers_names_triplet.reg)); + + auto &input_metadata = m_inputs_metadata[layers_names_triplet.reg]; + if (input_metadata.format.type == HAILO_FORMAT_TYPE_UINT8) { + status = extract_detections(layers_names_triplet, inputs.at(layers_names_triplet.reg), inputs.at(layers_names_triplet.cls), + inputs.at(layers_names_triplet.obj), detections, classes_detections_count); + } else if (input_metadata.format.type == HAILO_FORMAT_TYPE_UINT16) { + status = extract_detections(layers_names_triplet, inputs.at(layers_names_triplet.reg), inputs.at(layers_names_triplet.cls), + inputs.at(layers_names_triplet.obj), detections, classes_detections_count); + } else { + CHECK_SUCCESS(HAILO_INVALID_ARGUMENT, "YOLO post-process received invalid input type {}", input_metadata.format.type); + } + + CHECK_SUCCESS(status); + } + + return hailo_nms_format(std::move(detections), outputs.begin()->second, classes_detections_count); +} + +hailo_bbox_float32_t YOLOXPostProcessOp::decode(float32_t tx, float32_t ty, float32_t tw, float32_t th, + uint32_t col, uint32_t row, float32_t reg_shape_width, float32_t reg_shape_height) const +{ + /** + * Note that the calculations are bit different from the source (In order to save some run time) + * Each "/ reg_shape_width" is equivalent to "* w_stride / m_yolox_config.image_width". + * Each "/ reg_shape_height" is equivalent to "* h_stride / m_yolox_config.image_height". + **/ + auto w = exp(tw) / reg_shape_width; + auto h = exp(th) / reg_shape_height; + auto x_center = (tx + static_cast(col)) / reg_shape_width; + auto y_center = (ty + static_cast(row)) / reg_shape_height; + auto x_min = (x_center - (w / 2.0f)); + auto y_min = (y_center - (h / 2.0f)); + + return hailo_bbox_float32_t{y_min, x_min, (y_min+h), (x_min+w), 0}; +} + +std::string YOLOXPostProcessOp::get_op_description() +{ + auto nms_config_info = get_nms_config_description(); + auto config_info = fmt::format("Name: {}, {}, Image height: {:.2f}, Image width: {:.2f}", + m_name, nms_config_info, m_yolox_config.image_height, m_yolox_config.image_width); + return config_info; +} + +} +} diff --git a/hailort/libhailort/src/net_flow/ops/yolox_post_process.hpp b/hailort/libhailort/src/net_flow/ops/yolox_post_process.hpp new file mode 100644 index 0000000..de5d268 --- /dev/null +++ b/hailort/libhailort/src/net_flow/ops/yolox_post_process.hpp @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file yolox_post_process.hpp + * @brief YOLOX post process + * + **/ + +#ifndef _HAILO_YOLOX_POST_PROCESS_HPP_ +#define _HAILO_YOLOX_POST_PROCESS_HPP_ + +#include "net_flow/ops/nms_post_process.hpp" + +namespace hailort +{ +namespace net_flow +{ + +struct MatchingLayersNames +{ + // Regression layer + std::string reg; + + // Objectness layer + std::string obj; + + // Classifications layer + std::string cls; +}; + +struct YoloxPostProcessConfig +{ + // The image height. + float32_t image_height = 0; + + // The image width. + float32_t image_width = 0; + + // A vector off three strings that represents the relations between the outputs names. + std::vector input_names; +}; + +class YOLOXPostProcessOp : public NmsPostProcessOp +{ +public: + static Expected> create(const std::map &inputs_metadata, + const std::map &outputs_metadata, + const NmsPostProcessConfig &nms_post_process_config, + const YoloxPostProcessConfig &yolo_post_process_config); + + hailo_status execute(const std::map &inputs, std::map &outputs) override; + std::string get_op_description() override; + hailo_status validate_metadata() override;// TODO: HRT-10676 + +private: + template + hailo_status extract_detections(const MatchingLayersNames &layers_names, const MemoryView ®_buffer, const MemoryView &cls_buffer, + const MemoryView &obj_buffer, std::vector &detections, std::vector &classes_detections_count) + { + const auto ®_shape = m_inputs_metadata[layers_names.reg].shape; + const auto ®_padded_shape = m_inputs_metadata[layers_names.reg].padded_shape; + const auto &cls_padded_shape = m_inputs_metadata[layers_names.cls].padded_shape; + const auto &obj_padded_shape = m_inputs_metadata[layers_names.obj].padded_shape; + const auto ®_quant_info = m_inputs_metadata[layers_names.reg].quant_info; + const auto &cls_quant_info = m_inputs_metadata[layers_names.cls].quant_info; + const auto &obj_quant_info = m_inputs_metadata[layers_names.obj].quant_info; + + static const uint32_t X_INDEX = 0; + static const uint32_t Y_INDEX = 1; + static const uint32_t W_INDEX = 2; + static const uint32_t H_INDEX = 3; + + const uint32_t X_OFFSET = X_INDEX * reg_padded_shape.width; + const uint32_t Y_OFFSET = Y_INDEX * reg_padded_shape.width; + const uint32_t W_OFFSET = W_INDEX * reg_padded_shape.width; + const uint32_t H_OFFSET = H_INDEX * reg_padded_shape.width; + + static const uint32_t CLASSES_START_INDEX = 0; + + // Validate regression buffer size + static const uint32_t reg_entry_size = 4; + auto number_of_entries = reg_padded_shape.height * reg_padded_shape.width; + auto buffer_size = number_of_entries * reg_entry_size * sizeof(DeviceType); + CHECK(buffer_size == reg_buffer.size(), HAILO_INVALID_ARGUMENT, + "Failed to extract_detections, reg {} buffer_size should be {}, but is {}", layers_names.reg, buffer_size, reg_buffer.size()); + + // Validate classes buffer size + const uint32_t cls_entry_size = m_nms_config.number_of_classes; + number_of_entries = cls_padded_shape.height * cls_padded_shape.width; + buffer_size = number_of_entries * cls_entry_size * sizeof(DeviceType); + CHECK(buffer_size == cls_buffer.size(), HAILO_INVALID_ARGUMENT, + "Failed to extract_detections, cls {} buffer_size should be {}, but is {}", layers_names.cls, buffer_size, cls_buffer.size()); + + // Validate objectness buffer size + static const uint32_t obj_entry_size = 1; + number_of_entries = obj_padded_shape.height * obj_padded_shape.width; + buffer_size = number_of_entries * obj_entry_size * sizeof(DeviceType); + CHECK(buffer_size == obj_buffer.size(), HAILO_INVALID_ARGUMENT, + "Failed to extract_detections, obj {} buffer_size should be {}, but is {}", layers_names.obj, buffer_size, obj_buffer.size()); + + auto reg_row_size = reg_padded_shape.width * reg_padded_shape.features; + auto cls_row_size = cls_padded_shape.width * cls_padded_shape.features; + auto obj_row_size = obj_padded_shape.width * obj_padded_shape.features; + + DeviceType *reg_data = (DeviceType*)reg_buffer.data(); + DeviceType *obj_data = (DeviceType*)obj_buffer.data(); + DeviceType *cls_data = (DeviceType*)cls_buffer.data(); + + for (uint32_t row = 0; row < reg_shape.height; row++) { + for (uint32_t col = 0; col < reg_shape.width; col++) { + auto obj_idx = (obj_row_size * row) + col; + auto objectness = Quantization::dequantize_output(obj_data[obj_idx], obj_quant_info); + + if (objectness < m_nms_config.nms_score_th) { + continue; + } + + auto reg_idx = (reg_row_size * row) + col; + auto cls_idx = (cls_row_size * row) + col; + + auto tx = Quantization::dequantize_output(reg_data[reg_idx + X_OFFSET], reg_quant_info); + auto ty = Quantization::dequantize_output(reg_data[reg_idx + Y_OFFSET], reg_quant_info); + auto tw = Quantization::dequantize_output(reg_data[reg_idx + W_OFFSET], reg_quant_info); + auto th = Quantization::dequantize_output(reg_data[reg_idx + H_OFFSET], reg_quant_info); + auto bbox = decode(tx, ty, tw, th, col, row, static_cast(reg_shape.width), static_cast(reg_shape.height)); + + if (m_nms_config.cross_classes) { + // Pre-NMS optimization. If NMS checks IOU over different classes, only the maximum class is relevant + auto max_id_score_pair = get_max_class(cls_data, cls_idx, CLASSES_START_INDEX, objectness, cls_quant_info, cls_padded_shape.width); + bbox.score = max_id_score_pair.second; + if (max_id_score_pair.second >= m_nms_config.nms_score_th) { + detections.emplace_back(DetectionBbox(bbox, max_id_score_pair.first)); + classes_detections_count[max_id_score_pair.first]++; + } + } + else { + for (uint32_t curr_class_idx = 0; curr_class_idx < m_nms_config.number_of_classes; curr_class_idx++) { + auto class_entry_idx = cls_idx + (curr_class_idx * cls_padded_shape.width); + auto class_confidence = Quantization::dequantize_output( + cls_data[class_entry_idx], cls_quant_info); + auto class_score = class_confidence * objectness; + if (class_score >= m_nms_config.nms_score_th) { + bbox.score = class_score; + detections.emplace_back(DetectionBbox(bbox, curr_class_idx)); + classes_detections_count[curr_class_idx]++; + } + } + } + } + } + + return HAILO_SUCCESS; + } + + virtual hailo_bbox_float32_t decode(float32_t tx, float32_t ty, float32_t tw, float32_t th, + uint32_t col, uint32_t row, float32_t w_stride, float32_t h_stride) const; + + YoloxPostProcessConfig m_yolox_config; + + YOLOXPostProcessOp(const std::map &inputs_metadata, + const std::map &outputs_metadata, + const NmsPostProcessConfig &nms_post_process_config, + const YoloxPostProcessConfig &yolo_post_process_config) + : NmsPostProcessOp(inputs_metadata, outputs_metadata, nms_post_process_config, "YOLOX-Post-Process") + , m_yolox_config(yolo_post_process_config) + {} + +}; + +} // namespace net_flow +} // namespace hailort + +#endif // _HAILO_YOLOX_POST_PROCESS_HPP_ diff --git a/hailort/libhailort/src/net_flow/pipeline/pipeline.cpp b/hailort/libhailort/src/net_flow/pipeline/pipeline.cpp index 03abf9b..521cc86 100644 --- a/hailort/libhailort/src/net_flow/pipeline/pipeline.cpp +++ b/hailort/libhailort/src/net_flow/pipeline/pipeline.cpp @@ -381,9 +381,9 @@ hailo_status PipelinePad::deactivate() return m_element.deactivate(); } -hailo_status PipelinePad::post_deactivate() +hailo_status PipelinePad::post_deactivate(bool should_clear_abort) { - return m_element.post_deactivate(); + return m_element.post_deactivate(should_clear_abort); } hailo_status PipelinePad::clear() @@ -406,9 +406,9 @@ hailo_status PipelinePad::wait_for_finish() return m_element.wait_for_finish(); } -hailo_status PipelinePad::resume() +hailo_status PipelinePad::clear_abort() { - return m_element.resume(); + return m_element.clear_abort(); } hailo_status PipelinePad::run_push(PipelineBuffer &&buffer) @@ -582,9 +582,9 @@ hailo_status PipelineElement::deactivate() return execute_deactivate(); } -hailo_status PipelineElement::post_deactivate() +hailo_status PipelineElement::post_deactivate(bool should_clear_abort) { - return execute_post_deactivate(); + return execute_post_deactivate(should_clear_abort); } hailo_status PipelineElement::clear() @@ -602,9 +602,9 @@ hailo_status PipelineElement::abort() return execute_abort(); } -hailo_status PipelineElement::resume() +hailo_status PipelineElement::clear_abort() { - return execute_resume(); + return execute_clear_abort(); } hailo_status PipelineElement::wait_for_finish() @@ -622,9 +622,9 @@ hailo_status PipelineElement::execute_deactivate() return execute([&](auto *pad){ return pad->deactivate(); }); } -hailo_status PipelineElement::execute_post_deactivate() +hailo_status PipelineElement::execute_post_deactivate(bool should_clear_abort) { - return execute([&](auto *pad){ return pad->post_deactivate(); }); + return execute([&](auto *pad){ return pad->post_deactivate(should_clear_abort); }); } hailo_status PipelineElement::execute_clear() @@ -642,9 +642,9 @@ hailo_status PipelineElement::execute_abort() return execute([&](auto *pad){ return pad->abort(); }); } -hailo_status PipelineElement::execute_resume() +hailo_status PipelineElement::execute_clear_abort() { - return execute([&](auto *pad){ return pad->resume(); }); + return execute([&](auto *pad){ return pad->clear_abort(); }); } hailo_status PipelineElement::execute_wait_for_finish() @@ -830,7 +830,7 @@ hailo_status BaseQueueElement::execute_activate() return HAILO_SUCCESS; } -hailo_status BaseQueueElement::execute_post_deactivate() +hailo_status BaseQueueElement::execute_post_deactivate(bool should_clear_abort) { hailo_status status = m_deactivation_event.wait(INIFINITE_TIMEOUT()); if (HAILO_SUCCESS != status) { @@ -842,7 +842,7 @@ hailo_status BaseQueueElement::execute_post_deactivate() LOGGER__ERROR("Failed to reset of deactivation event in {} with status {}", name(), status); } - return PipelineElement::execute_post_deactivate(); + return PipelineElement::execute_post_deactivate(should_clear_abort); } hailo_status BaseQueueElement::execute_clear() @@ -877,14 +877,12 @@ hailo_status PushQueueElement::execute_abort() return m_activation_event.signal(); } -hailo_status BaseQueueElement::execute_resume() +hailo_status BaseQueueElement::execute_clear_abort() { auto status = m_shutdown_event->reset(); CHECK_SUCCESS(status); m_pipeline_status->store(HAILO_SUCCESS); - status = PipelineElement::execute_resume(); - CHECK_SUCCESS(status); - return m_activation_event.signal(); + return PipelineElement::execute_clear_abort(); } hailo_status BaseQueueElement::set_timeout(std::chrono::milliseconds timeout) @@ -1258,7 +1256,8 @@ Expected UserBufferQueueElement::run_pull(PipelineBuffer &&optio LOGGER__INFO("Shutdown event was signaled in dequeue of queue element {}!", name()); return make_unexpected(HAILO_SHUTDOWN_EVENT_SIGNALED); } - CHECK_AS_EXPECTED(HAILO_TIMEOUT != output.status(), HAILO_TIMEOUT, "{} (D2H) failed with status={} (timeout={}ms)", name(), HAILO_TIMEOUT, m_timeout.count()); + CHECK_AS_EXPECTED(HAILO_TIMEOUT != output.status(), HAILO_TIMEOUT, "{} (D2H) failed with status={} (timeout={}ms)", + name(), HAILO_TIMEOUT, m_timeout.count()); CHECK_EXPECTED(output); CHECK_AS_EXPECTED(output->data() == optional.data(), HAILO_INTERNAL_FAILURE, "The buffer received in {} was not the same as the user buffer!", name()); @@ -1461,6 +1460,7 @@ hailo_status BaseDemuxElement::execute_activate() } m_is_activated = true;// TODO Should this always be true, no matter the status of source().activate()? m_was_stream_aborted = false; + return PipelineElement::execute_activate(); } @@ -1488,12 +1488,12 @@ hailo_status BaseDemuxElement::execute_deactivate() return HAILO_SUCCESS; } -hailo_status BaseDemuxElement::execute_post_deactivate() +hailo_status BaseDemuxElement::execute_post_deactivate(bool should_clear_abort) { for (uint32_t i = 0; i < m_was_source_called.size(); i++) { m_was_source_called[i] = false; } - return PipelineElement::execute_post_deactivate(); + return PipelineElement::execute_post_deactivate(should_clear_abort); } hailo_status BaseDemuxElement::execute_abort() diff --git a/hailort/libhailort/src/net_flow/pipeline/pipeline.hpp b/hailort/libhailort/src/net_flow/pipeline/pipeline.hpp index 702f8a7..77d56dc 100644 --- a/hailort/libhailort/src/net_flow/pipeline/pipeline.hpp +++ b/hailort/libhailort/src/net_flow/pipeline/pipeline.hpp @@ -204,12 +204,12 @@ public: hailo_status activate(); hailo_status deactivate(); - hailo_status post_deactivate(); + hailo_status post_deactivate(bool should_clear_abort); hailo_status clear(); hailo_status flush(); hailo_status abort(); hailo_status wait_for_finish(); - hailo_status resume(); + hailo_status clear_abort(); virtual hailo_status run_push(PipelineBuffer &&buffer); virtual Expected run_pull(PipelineBuffer &&optional = PipelineBuffer()); void set_push_complete_callback(PushCompleteCallback push_complete_callback); @@ -252,11 +252,11 @@ public: hailo_status activate(); hailo_status deactivate(); - hailo_status post_deactivate(); + hailo_status post_deactivate(bool should_clear_abort); hailo_status clear(); hailo_status flush(); hailo_status abort(); - hailo_status resume(); + hailo_status clear_abort(); hailo_status wait_for_finish(); virtual hailo_status run_push(PipelineBuffer &&buffer) = 0; virtual Expected run_pull(PipelineBuffer &&optional, const PipelinePad &source) = 0; @@ -291,11 +291,11 @@ protected: virtual std::vector execution_pads() = 0; virtual hailo_status execute_activate(); virtual hailo_status execute_deactivate(); - virtual hailo_status execute_post_deactivate(); + virtual hailo_status execute_post_deactivate(bool should_clear_abort); virtual hailo_status execute_clear(); virtual hailo_status execute_flush(); virtual hailo_status execute_abort(); - virtual hailo_status execute_resume(); + virtual hailo_status execute_clear_abort(); virtual hailo_status execute_wait_for_finish(); virtual hailo_status execute(std::function); @@ -372,9 +372,9 @@ protected: hailo_status pipeline_status(); virtual hailo_status execute_activate() override; - virtual hailo_status execute_post_deactivate() override; + virtual hailo_status execute_post_deactivate(bool should_clear_abort) override; virtual hailo_status execute_clear() override; - virtual hailo_status execute_resume() override; + virtual hailo_status execute_clear_abort() override; virtual hailo_status execute_wait_for_finish() override; /// Starts/stops the queue thread. This functions needs to be called on subclasses ctor and dtor @@ -527,7 +527,7 @@ public: protected: virtual hailo_status execute_activate() override; virtual hailo_status execute_deactivate() override; - virtual hailo_status execute_post_deactivate() override; + virtual hailo_status execute_post_deactivate(bool should_clear_abort) override; virtual hailo_status execute_abort() override; virtual Expected> action(PipelineBuffer &&input) = 0; virtual std::vector execution_pads() override; diff --git a/hailort/libhailort/src/net_flow/pipeline/vstream.cpp b/hailort/libhailort/src/net_flow/pipeline/vstream.cpp index adfc6fa..426bd4b 100644 --- a/hailort/libhailort/src/net_flow/pipeline/vstream.cpp +++ b/hailort/libhailort/src/net_flow/pipeline/vstream.cpp @@ -7,8 +7,10 @@ * @brief Implementation of the virtual stream **/ +#include "common/utils.hpp" #include "hailo/vstream.hpp" #include "hailo/hailort_defaults.hpp" +#include "hailo/hailort_common.hpp" #include "common/runtime_statistics_internal.hpp" @@ -130,8 +132,14 @@ Expected PreInferElement::action(PipelineBuffer &&input, Pipelin Expected> PostInferElement::create(const hailo_3d_image_shape_t &src_image_shape, const hailo_format_t &src_format, const hailo_3d_image_shape_t &dst_image_shape, const hailo_format_t &dst_format, const hailo_quant_info_t &dst_quant_info, const hailo_nms_info_t &nms_info, const std::string &name, - hailo_pipeline_elem_stats_flags_t elem_flags, std::shared_ptr> pipeline_status) + hailo_pipeline_elem_stats_flags_t elem_flags, std::shared_ptr> pipeline_status, + std::chrono::milliseconds timeout, hailo_vstream_stats_flags_t vstream_flags, EventPtr shutdown_event, + size_t buffer_pool_size) { + auto frame_size = (dst_format.order == HAILO_FORMAT_ORDER_HAILO_NMS) ? HailoRTCommon::get_nms_host_frame_size(nms_info, dst_format) : HailoRTCommon::get_frame_size(dst_image_shape, dst_format); + auto buffer_pool_expected = BufferPool::create(frame_size, buffer_pool_size, shutdown_event, elem_flags, vstream_flags); + CHECK_EXPECTED(buffer_pool_expected, "Failed creating BufferPool for {}", name); + auto transform_context = OutputTransformContext::create(src_image_shape, src_format, dst_image_shape, dst_format, dst_quant_info, nms_info); CHECK_EXPECTED(transform_context, "Failed Creating OutputTransformContext"); @@ -140,7 +148,7 @@ Expected> PostInferElement::create(const hailo CHECK_EXPECTED(duration_collector); auto post_infer_elem_ptr = make_shared_nothrow(transform_context.release(), - name, duration_collector.release(), std::move(pipeline_status)); + name, duration_collector.release(), std::move(pipeline_status), buffer_pool_expected.release(), timeout); CHECK_AS_EXPECTED(nullptr != post_infer_elem_ptr, HAILO_OUT_OF_HOST_MEMORY); LOGGER__INFO("Created {}", post_infer_elem_ptr->name()); @@ -150,17 +158,22 @@ Expected> PostInferElement::create(const hailo Expected> PostInferElement::create(const hailo_3d_image_shape_t &src_image_shape, const hailo_format_t &src_format, const hailo_3d_image_shape_t &dst_image_shape, const hailo_format_t &dst_format, const hailo_quant_info_t &dst_quant_info, const hailo_nms_info_t &nms_info, - const std::string &name, const hailo_vstream_params_t &vstream_params, std::shared_ptr> pipeline_status) + const std::string &name, const hailo_vstream_params_t &vstream_params, std::shared_ptr> pipeline_status, + EventPtr shutdown_event) { return PostInferElement::create(src_image_shape, src_format, dst_image_shape, dst_format, dst_quant_info, nms_info, - name, vstream_params.pipeline_elements_stats_flags, pipeline_status); + name, vstream_params.pipeline_elements_stats_flags, pipeline_status, std::chrono::milliseconds(vstream_params.timeout_ms), + vstream_params.vstream_stats_flags, shutdown_event, vstream_params.queue_size); } PostInferElement::PostInferElement(std::unique_ptr &&transform_context, const std::string &name, DurationCollector &&duration_collector, - std::shared_ptr> &&pipeline_status) : + std::shared_ptr> &&pipeline_status, + BufferPoolPtr buffer_pool, std::chrono::milliseconds timeout) : FilterElement(name, std::move(duration_collector), std::move(pipeline_status)), - m_transform_context(std::move(transform_context)) + m_transform_context(std::move(transform_context)), + m_pool(buffer_pool), + m_timeout(timeout) {} hailo_status PostInferElement::run_push(PipelineBuffer &&/*buffer*/) @@ -184,18 +197,30 @@ std::string PostInferElement::description() const Expected PostInferElement::action(PipelineBuffer &&input, PipelineBuffer &&optional) { - CHECK_AS_EXPECTED(optional, HAILO_INVALID_ARGUMENT, "Optional buffer must be valid in {}!", name()); + auto buffer = m_pool->get_available_buffer(std::move(optional), m_timeout); + if (HAILO_SHUTDOWN_EVENT_SIGNALED == buffer.status()) { + return make_unexpected(buffer.status()); + } + CHECK_EXPECTED(buffer, "{} (D2H) failed with status={}", name(), buffer.status()); // Note: The latency to be measured starts as the buffer is read from the HW (it's 'input' in this case) - optional.set_metadata(input.get_metadata()); + buffer->set_metadata(input.get_metadata()); - auto dst = optional.as_view(); + auto dst = buffer->as_view(); m_duration_collector.start_measurement(); const auto status = m_transform_context->transform(input.as_view(), dst); m_duration_collector.complete_measurement(); CHECK_SUCCESS_AS_EXPECTED(status); - return std::move(optional); + return buffer.release(); +} + +std::vector PostInferElement::get_queue_size_accumulators() +{ + if (nullptr == m_pool->get_queue_size_accumulator()) { + return std::vector(); + } + return {m_pool->get_queue_size_accumulator()}; } static hailo_nms_info_t fuse_nms_info(const std::vector &nms_infos) @@ -426,6 +451,116 @@ Expected> TransformDemuxElement::action(PipelineBuff return outputs; } +Expected> ArgmaxPostProcessElement::create(std::shared_ptr argmax_op, + const std::string &name, hailo_pipeline_elem_stats_flags_t elem_flags, + std::shared_ptr> pipeline_status) +{ + auto duration_collector = DurationCollector::create(elem_flags); + CHECK_EXPECTED(duration_collector); + auto argmax_elem_ptr = make_shared_nothrow(argmax_op, + name, duration_collector.release(), std::move(pipeline_status)); + CHECK_AS_EXPECTED(nullptr != argmax_elem_ptr, HAILO_OUT_OF_HOST_MEMORY); + LOGGER__INFO("Created {}", argmax_elem_ptr->name()); + return argmax_elem_ptr; +} + +ArgmaxPostProcessElement::ArgmaxPostProcessElement(std::shared_ptr argmax_op, const std::string &name, + DurationCollector &&duration_collector, + std::shared_ptr> &&pipeline_status) : + FilterElement(name, std::move(duration_collector), std::move(pipeline_status)), + m_argmax_op(argmax_op) +{} + +hailo_status ArgmaxPostProcessElement::run_push(PipelineBuffer &&/*buffer*/) +{ + LOGGER__ERROR("ArgmaxPostProcessElement does not support run_push operation"); + return HAILO_INVALID_OPERATION; +} + +PipelinePad &ArgmaxPostProcessElement::next_pad() +{ + // Note: The next elem to be run is upstream from this elem (i.e. buffers are pulled) + return *m_sinks[0].prev(); +} + +std::string ArgmaxPostProcessElement::description() const +{ + std::stringstream element_description; + element_description << "(" << this->name() << " | " << m_argmax_op->get_op_description() << ")"; + return element_description.str(); +} + +Expected ArgmaxPostProcessElement::action(PipelineBuffer &&input, PipelineBuffer &&optional) +{ + std::map inputs; + std::map outputs; + auto &input_name = m_argmax_op->inputs_metadata().begin()->first; + auto &output_name = m_argmax_op->outputs_metadata().begin()->first; + inputs.insert({input_name, input.as_view()}); + outputs.insert({output_name, optional.as_view()}); + m_duration_collector.start_measurement(); + auto post_process_result = m_argmax_op->execute(inputs, outputs); + CHECK_SUCCESS_AS_EXPECTED(post_process_result); + m_duration_collector.complete_measurement(); + + return std::move(optional); +} + +Expected> SoftmaxPostProcessElement::create(std::shared_ptr softmax_op, + const std::string &name, hailo_pipeline_elem_stats_flags_t elem_flags, + std::shared_ptr> pipeline_status) +{ + auto duration_collector = DurationCollector::create(elem_flags); + CHECK_EXPECTED(duration_collector); + auto softmax_elem_ptr = make_shared_nothrow(softmax_op, + name, duration_collector.release(), std::move(pipeline_status)); + CHECK_AS_EXPECTED(nullptr != softmax_elem_ptr, HAILO_OUT_OF_HOST_MEMORY); + LOGGER__INFO("Created {}", softmax_elem_ptr->name()); + return softmax_elem_ptr; +} + +SoftmaxPostProcessElement::SoftmaxPostProcessElement(std::shared_ptr softmax_op, const std::string &name, + DurationCollector &&duration_collector, + std::shared_ptr> &&pipeline_status) : + FilterElement(name, std::move(duration_collector), std::move(pipeline_status)), + m_softmax_op(softmax_op) +{} + +hailo_status SoftmaxPostProcessElement::run_push(PipelineBuffer &&/*buffer*/) +{ + LOGGER__ERROR("SoftmaxPostProcessElement does not support run_push operation"); + return HAILO_INVALID_OPERATION; +} + +PipelinePad &SoftmaxPostProcessElement::next_pad() +{ + // Note: The next elem to be run is upstream from this elem (i.e. buffers are pulled) + return *m_sinks[0].prev(); +} + +std::string SoftmaxPostProcessElement::description() const +{ + std::stringstream element_description; + element_description << "(" << this->name() << " | " << m_softmax_op->get_op_description() << ")"; + return element_description.str(); +} + +Expected SoftmaxPostProcessElement::action(PipelineBuffer &&input, PipelineBuffer &&optional) +{ + std::map inputs; + std::map outputs; + auto &input_name = m_softmax_op->inputs_metadata().begin()->first; + auto &output_name = m_softmax_op->outputs_metadata().begin()->first; + inputs.insert({input_name, input.as_view()}); + outputs.insert({output_name, optional.as_view()}); + m_duration_collector.start_measurement(); + auto post_process_result = m_softmax_op->execute(inputs, outputs); + CHECK_SUCCESS_AS_EXPECTED(post_process_result); + m_duration_collector.complete_measurement(); + + return std::move(optional); +} + BaseVStream::BaseVStream(const hailo_vstream_info_t &vstream_info, const hailo_vstream_params_t &vstream_params, std::shared_ptr pipeline_entry, std::vector> &&pipeline, std::shared_ptr> &&pipeline_status, @@ -494,28 +629,38 @@ hailo_status BaseVStream::start_vstream() auto status = m_shutdown_event->reset(); CHECK_SUCCESS(status); - LOGGER__DEBUG("Activating {}...", name()); - status = m_entry_element->activate(); - CHECK_SUCCESS(status); - status = resume(); CHECK(((status == HAILO_SUCCESS) || (status == HAILO_STREAM_NOT_ACTIVATED)), status, "Failed to resume stream in {}", name()); + LOGGER__DEBUG("Activating {}...", name()); + status = m_entry_element->activate(); + CHECK_SUCCESS(status); + m_is_activated = true; return HAILO_SUCCESS; } hailo_status BaseVStream::abort() { + auto status = m_entry_element->abort(); + CHECK_SUCCESS(status); m_is_aborted = true; - return m_entry_element->abort(); + + return HAILO_SUCCESS; } hailo_status BaseVStream::resume() { + auto status = m_entry_element->clear_abort(); + CHECK_SUCCESS(status); m_is_aborted = false; - return m_entry_element->resume(); + + if (m_is_activated) { + status = m_entry_element->activate(); + CHECK_SUCCESS(status); + } + return HAILO_SUCCESS; } hailo_status BaseVStream::stop_vstream() @@ -528,7 +673,10 @@ hailo_status BaseVStream::stop_vstream() LOGGER__WARNING("Failed deactivate of vstream {} status {}", name(), status); } - status = m_entry_element->post_deactivate(); + // If VStream was aborted, do not clear low-level stream abortion, + // otherwise flush would be called on low-level stream d-tor when there is no receiver. + auto should_clear_abort = (!m_is_aborted); + status = m_entry_element->post_deactivate(should_clear_abort); if (HAILO_SUCCESS != status) { LOGGER__WARNING("Failed post deactivate of vstream {} status {}", name(), status); } @@ -538,9 +686,12 @@ hailo_status BaseVStream::stop_vstream() hailo_status BaseVStream::stop_and_clear() { - auto status = m_core_op_activated_event->wait(std::chrono::milliseconds(0)); - CHECK(HAILO_TIMEOUT == status, HAILO_INVALID_OPERATION, - "Trying to clear {} vstream before its network group is deactivated", name()); + auto status = HAILO_SUCCESS; + if (nullptr != m_core_op_activated_event) { + status = m_core_op_activated_event->wait(std::chrono::milliseconds(0)); + CHECK(HAILO_TIMEOUT == status, HAILO_INVALID_OPERATION, + "Trying to clear {} vstream before its network group is deactivated", name()); + } status = stop_vstream(); CHECK_SUCCESS(status); @@ -553,8 +704,8 @@ hailo_status BaseVStream::stop_and_clear() LOGGER__TRACE("Overwritting current pipeline status {}", curr_pipeline_status); m_pipeline_status->store(HAILO_SUCCESS); } - - return HAILO_SUCCESS; + + return status; } size_t BaseVStream::get_frame_size() const @@ -758,6 +909,11 @@ hailo_status InputVStream::after_fork_in_child() return m_vstream->after_fork_in_child(); } +bool InputVStream::is_aborted() +{ + return m_vstream->is_aborted(); +} + InputVStream::InputVStream(std::shared_ptr vstream) : m_vstream(std::move(vstream)) {} Expected OutputVStream::create( @@ -902,6 +1058,11 @@ hailo_status OutputVStream::after_fork_in_child() return m_vstream->after_fork_in_child(); } +bool OutputVStream::is_aborted() +{ + return m_vstream->is_aborted(); +} + OutputVStream::OutputVStream(std::shared_ptr vstream) : m_vstream(std::move(vstream)) {} std::map get_pipeline_accumulators_by_type( @@ -1008,11 +1169,6 @@ InputVStreamImpl::InputVStreamImpl(const hailo_vstream_info_t &vstream_info, con InputVStreamImpl::~InputVStreamImpl() { (void)stop_vstream(); - if (m_is_aborted) { - // If VStream was aborted, do not clear low-level stream abortion, - // otherwise flush would be called on low-level stream d-tor when there is no receiver. - (void)abort(); - } } hailo_status InputVStreamImpl::write(const MemoryView &buffer) @@ -1074,7 +1230,7 @@ InputVStreamClient::InputVStreamClient(std::unique_ptr client, InputVStreamClient::~InputVStreamClient() { - auto reply = m_client->InputVStream_release(m_handle); + auto reply = m_client->InputVStream_release(m_handle, OsUtils::get_curr_pid()); if (reply != HAILO_SUCCESS) { LOGGER__CRITICAL("InputVStream_release failed!"); } @@ -1103,6 +1259,24 @@ hailo_status InputVStreamClient::resume() return m_client->InputVStream_resume(m_handle); } +hailo_status InputVStreamClient::stop_and_clear() +{ + auto expected_client = HailoRtRpcClientUtils::create_client(); + CHECK_EXPECTED_AS_STATUS(expected_client); + auto stop_and_clear_client = expected_client.release(); + + return stop_and_clear_client->InputVStream_stop_and_clear(m_handle); +} + +hailo_status InputVStreamClient::start_vstream() +{ + auto expected_client = HailoRtRpcClientUtils::create_client(); + CHECK_EXPECTED_AS_STATUS(expected_client); + auto start_vstream_client = expected_client.release(); + + return start_vstream_client->InputVStream_start_vstream(m_handle); +} + size_t InputVStreamClient::get_frame_size() const { auto frame_size = m_client->InputVStream_get_frame_size(m_handle); @@ -1198,6 +1372,17 @@ hailo_status InputVStreamClient::after_fork_in_child() m_handle = expected_dup_handle.value(); return HAILO_SUCCESS; } + +bool InputVStreamClient::is_aborted() +{ + auto is_aborted_exp = m_client->InputVStream_is_aborted(m_handle); + if (!is_aborted_exp) { + LOGGER__CRITICAL("InputVStream_is_aborted failed with status={}", is_aborted_exp.status()); + return true; + } + return is_aborted_exp.release(); +} + #endif // HAILO_SUPPORT_MULTI_PROCESS std::string InputVStreamInternal::get_pipeline_description() const @@ -1300,11 +1485,6 @@ OutputVStreamImpl::OutputVStreamImpl(const hailo_vstream_info_t &vstream_info, c OutputVStreamImpl::~OutputVStreamImpl() { (void)stop_vstream(); - if (m_is_aborted) { - // If VStream was aborted, do not clear low-level stream abortion, - // otherwise flush would be called on low-level stream d-tor when there is no receiver. - (void)abort(); - } } hailo_status OutputVStreamImpl::read(MemoryView buffer) @@ -1361,7 +1541,7 @@ OutputVStreamClient::OutputVStreamClient(std::unique_ptr clien OutputVStreamClient::~OutputVStreamClient() { - auto reply = m_client->OutputVStream_release(m_handle); + auto reply = m_client->OutputVStream_release(m_handle, OsUtils::get_curr_pid()); if (reply != HAILO_SUCCESS) { LOGGER__CRITICAL("OutputVStream_release failed!"); } @@ -1385,6 +1565,24 @@ hailo_status OutputVStreamClient::resume() return m_client->OutputVStream_resume(m_handle); } +hailo_status OutputVStreamClient::stop_and_clear() +{ + auto expected_client = HailoRtRpcClientUtils::create_client(); + CHECK_EXPECTED_AS_STATUS(expected_client); + auto stop_and_clear_client = expected_client.release(); + + return stop_and_clear_client->OutputVStream_stop_and_clear(m_handle); +} + +hailo_status OutputVStreamClient::start_vstream() +{ + auto expected_client = HailoRtRpcClientUtils::create_client(); + CHECK_EXPECTED_AS_STATUS(expected_client); + auto start_vstream_client = expected_client.release(); + + return start_vstream_client->OutputVStream_start_vstream(m_handle); +} + size_t OutputVStreamClient::get_frame_size() const { auto frame_size = m_client->OutputVStream_get_frame_size(m_handle); @@ -1409,7 +1607,7 @@ std::string OutputVStreamClient::name() const { auto expected_name = m_client->OutputVStream_name(m_handle); if (!expected_name) { - LOGGER__CRITICAL("InputVStream_name failed with status={}", expected_name.status()); + LOGGER__CRITICAL("OutputVStream_name failed with status={}", expected_name.status()); return ""; } return expected_name.release(); @@ -1419,7 +1617,7 @@ std::string OutputVStreamClient::network_name() const { auto expected_name = m_client->OutputVStream_network_name(m_handle); if (!expected_name) { - LOGGER__CRITICAL("InputVStream_name failed with status={}", expected_name.status()); + LOGGER__CRITICAL("OutputVStream_name failed with status={}", expected_name.status()); return ""; } return expected_name.release(); @@ -1480,6 +1678,16 @@ hailo_status OutputVStreamClient::after_fork_in_child() m_handle = expected_dup_handle.value(); return HAILO_SUCCESS; } + +bool OutputVStreamClient::is_aborted() +{ + auto is_aborted_exp = m_client->OutputVStream_is_aborted(m_handle); + if (!is_aborted_exp) { + LOGGER__CRITICAL("OutputVStream_is_aborted failed with status={}", is_aborted_exp.status()); + return true; + } + return is_aborted_exp.release(); +} #endif // HAILO_SUPPORT_MULTI_PROCESS Expected> HwReadElement::create(std::shared_ptr stream, const std::string &name, std::chrono::milliseconds timeout, @@ -1535,11 +1743,13 @@ std::string HwReadElement::description() const return element_description.str(); } -hailo_status HwReadElement::execute_post_deactivate() +hailo_status HwReadElement::execute_post_deactivate(bool should_clear_abort) { - auto status = m_stream->clear_abort(); - CHECK(((HAILO_SUCCESS == status) || (HAILO_STREAM_NOT_ACTIVATED == status)), status, - "Failed to clear abort stream in {}", name()); + if (should_clear_abort) { + auto status = m_stream->clear_abort(); + CHECK(((HAILO_SUCCESS == status) || (HAILO_STREAM_NOT_ACTIVATED == status)), status, + "Failed to clear abort stream in {}", name()); + } return HAILO_SUCCESS; } @@ -1561,11 +1771,11 @@ hailo_status HwReadElement::execute_abort() return HAILO_SUCCESS; } -hailo_status HwReadElement::execute_resume() +hailo_status HwReadElement::execute_clear_abort() { auto status = m_stream->clear_abort(); CHECK(((status == HAILO_SUCCESS) || (status == HAILO_STREAM_NOT_ACTIVATED)), status, - "Failed to execute resume stream in {}", name()); + "Failed to execute clear_abort stream in {}", name()); return HAILO_SUCCESS; } @@ -1734,7 +1944,9 @@ hailo_status HwWriteElement::execute_deactivate() hailo_status flush_status = m_stream->flush(); if (HAILO_STREAM_ABORTED_BY_USER == flush_status) { LOGGER__INFO("Failed flushing input stream {} because stream was aborted", m_stream->to_string()); - // TODO: HRT-3621 + return HAILO_SUCCESS; + } else if (HAILO_STREAM_NOT_ACTIVATED == flush_status) { + LOGGER__INFO("Failed flushing input stream {} because stream is not activated", m_stream->to_string()); return HAILO_SUCCESS; } else if (HAILO_SUCCESS != flush_status) { LOGGER__ERROR("flush has failed in {} with status {}", name(), flush_status); @@ -1746,11 +1958,13 @@ hailo_status HwWriteElement::execute_deactivate() return HAILO_SUCCESS; } -hailo_status HwWriteElement::execute_post_deactivate() +hailo_status HwWriteElement::execute_post_deactivate(bool should_clear_abort) { - auto status = m_stream->clear_abort(); - CHECK(((status == HAILO_SUCCESS) || (status == HAILO_STREAM_NOT_ACTIVATED)), status, - "Failed to clear abort stream in {}", name()); + if (should_clear_abort) { + auto status = m_stream->clear_abort(); + CHECK(((status == HAILO_SUCCESS) || (status == HAILO_STREAM_NOT_ACTIVATED)), status, + "Failed to clear abort stream in {}", name()); + } return HAILO_SUCCESS; } @@ -1778,11 +1992,11 @@ hailo_status HwWriteElement::execute_abort() return HAILO_SUCCESS; } -hailo_status HwWriteElement::execute_resume() +hailo_status HwWriteElement::execute_clear_abort() { auto status = m_stream->clear_abort(); CHECK(((status == HAILO_SUCCESS) || (status == HAILO_STREAM_NOT_ACTIVATED)), status, - "Failed to execute resume stream in {}", name()); + "Failed to execute clear_abort stream in {}", name()); return HAILO_SUCCESS; } @@ -1876,12 +2090,51 @@ Expected, std::vector>> VStre static hailo_vstream_params_t expand_vstream_params_autos(const hailo_stream_info_t &stream_info, const hailo_vstream_params_t &vstream_params) { + if (HAILO_FORMAT_ORDER_HAILO_NMS == stream_info.format.order) { + // TODO (HRT-11082): On NMS, return error if UINT16 + if (HAILO_FORMAT_TYPE_UINT16 == vstream_params.user_buffer_format.type) { + LOGGER__WARNING("Passing 'HAILO_FORMAT_TYPE_UINT16' for NMS output is deprecated and will soon be unsupported. "\ + "One should use HAILO_FORMAT_TYPE_FLOAT32"); + } + } auto local_vstream_params = vstream_params; local_vstream_params.user_buffer_format = HailoRTDefaults::expand_auto_format(vstream_params.user_buffer_format, stream_info.format); return local_vstream_params; } +static hailo_vstream_params_t expand_vstream_params_autos_argmax(const hailo_vstream_params_t &vstream_params, + hailo_format_t &op_input_format) +{ + auto local_vstream_params = vstream_params; + if (local_vstream_params.user_buffer_format.type == HAILO_FORMAT_TYPE_AUTO) { + local_vstream_params.user_buffer_format.type = op_input_format.type; + } + if (local_vstream_params.user_buffer_format.order == HAILO_FORMAT_ORDER_AUTO) { + if (op_input_format.order == HAILO_FORMAT_ORDER_NHCW || op_input_format.order == HAILO_FORMAT_ORDER_NHWC) { + local_vstream_params.user_buffer_format.order = HAILO_FORMAT_ORDER_NHW; + } + if (op_input_format.order == HAILO_FORMAT_ORDER_NC) { + local_vstream_params.user_buffer_format.order = HAILO_FORMAT_ORDER_NC; + } + } + return local_vstream_params; +} + +static hailo_vstream_params_t expand_vstream_params_autos_softmax(const hailo_vstream_params_t &vstream_params, + hailo_format_t &op_input_format) +{ + auto local_vstream_params = vstream_params; + // Type should be float32, after de-quantization, and order NHWC or NC in softmax + if (local_vstream_params.user_buffer_format.type == HAILO_FORMAT_TYPE_AUTO) { + local_vstream_params.user_buffer_format.type = HAILO_FORMAT_TYPE_FLOAT32; + } + if (local_vstream_params.user_buffer_format.order == HAILO_FORMAT_ORDER_AUTO) { + local_vstream_params.user_buffer_format.order = op_input_format.order; + } + return local_vstream_params; +} + Expected> VStreamsBuilder::create_input_vstreams(ConfiguredNetworkGroup &net_group, const std::map &inputs_params) { @@ -2001,21 +2254,18 @@ Expected> VStreamsBuilderUtils::create_outputs(std::s CHECK_AS_EXPECTED(!(hw_read_stream_stats_flags & HAILO_VSTREAM_STATS_MEASURE_FPS), HAILO_NOT_IMPLEMENTED, "Pipeline FPS statistics measurement is not implemented"); - auto hw_read_elem = HwReadElement::create(output_stream, - PipelineObject::create_element_name("HwReadElement", output_stream->name(), output_stream->get_info().index), - HAILO_INFINITE_TIMEOUT, buffer_pool_size, hw_read_element_stats_flags, hw_read_stream_stats_flags, shutdown_event, pipeline_status); - CHECK_EXPECTED(hw_read_elem); - elements.push_back(hw_read_elem.value()); + auto hw_read_element = add_hw_read_element(output_stream, pipeline_status, elements, "HwReadElement", shutdown_event, + buffer_pool_size, hw_read_element_stats_flags, hw_read_stream_stats_flags); + CHECK_EXPECTED(hw_read_element); if (output_stream->get_info().is_mux) { - hailo_status status = add_demux(output_stream, vstreams_params_map, std::move(elements), vstreams, hw_read_elem.value(), + hailo_status status = add_demux(output_stream, vstreams_params_map, std::move(elements), vstreams, hw_read_element.value(), shutdown_event, pipeline_status, output_vstream_infos); CHECK_SUCCESS_AS_EXPECTED(status); } else { auto vstream_info = output_vstream_infos.find(output_stream->name()); CHECK_AS_EXPECTED(vstream_info != output_vstream_infos.end(), HAILO_NOT_FOUND, "Failed to find vstream info of {}", output_stream->name()); - assert(1 == vstreams_params_map.size()); auto vstream_params = expand_vstream_params_autos(output_stream->get_info(), vstreams_params_map.begin()->second); @@ -2027,37 +2277,27 @@ Expected> VStreamsBuilderUtils::create_outputs(std::s vstream_params.user_buffer_format, output_stream->get_info().quant_info); if (should_transform) { - auto hw_read_queue_elem = PullQueueElement::create( - PipelineObject::create_element_name("PullQueueElement_hw_read", output_stream->name(), output_stream->get_info().index), - vstream_params, shutdown_event, pipeline_status); - CHECK_EXPECTED(hw_read_queue_elem); - elements.push_back(hw_read_queue_elem.value()); - CHECK_SUCCESS_AS_EXPECTED(PipelinePad::link_pads(hw_read_elem.value(), hw_read_queue_elem.value())); - - auto post_infer_elem = PostInferElement::create(output_stream->get_info().hw_shape, output_stream->get_info().format, - output_stream->get_info().shape, vstream_params.user_buffer_format, output_stream->get_info().quant_info, output_stream->get_info().nms_info, - PipelineObject::create_element_name("PostInferElement", output_stream->name(), output_stream->get_info().index), - vstream_params, pipeline_status); - CHECK_EXPECTED(post_infer_elem); - elements.push_back(post_infer_elem.value()); - CHECK_SUCCESS_AS_EXPECTED(PipelinePad::link_pads(hw_read_queue_elem.value(), post_infer_elem.value())); - - auto post_infer_queue_elem = UserBufferQueueElement::create( - PipelineObject::create_element_name("UserBufferQueueElement_post_infer", output_stream->name(), output_stream->get_info().index), - vstream_params, shutdown_event, pipeline_status); - CHECK_EXPECTED(post_infer_queue_elem); - elements.push_back(post_infer_queue_elem.value()); - CHECK_SUCCESS_AS_EXPECTED(PipelinePad::link_pads(post_infer_elem.value(), post_infer_queue_elem.value())); - + auto hw_read_queue_element = add_pull_queue_element(output_stream, pipeline_status, elements, "PullQueueElement_hw_read", + shutdown_event, vstream_params); + CHECK_EXPECTED(hw_read_queue_element); + CHECK_SUCCESS_AS_EXPECTED(PipelinePad::link_pads(hw_read_element.value(), hw_read_queue_element.value())); + + auto post_infer_element = add_post_infer_element(output_stream, pipeline_status, elements, + "PostInferElement", vstream_params, shutdown_event); + CHECK_EXPECTED(post_infer_element); + CHECK_SUCCESS_AS_EXPECTED(PipelinePad::link_pads(hw_read_queue_element.value(), post_infer_element.value())); + auto user_buffer_queue_element = add_user_buffer_queue_element(output_stream, pipeline_status, elements, + "UserBufferQueueElement", shutdown_event, vstream_params); + CHECK_SUCCESS_AS_EXPECTED(PipelinePad::link_pads(post_infer_element.value(), user_buffer_queue_element.value())); output_stream->set_timeout(std::chrono::milliseconds(HAILO_INFINITE)); - hw_read_queue_elem->get()->set_timeout(std::chrono::milliseconds(HAILO_INFINITE)); - auto vstream = OutputVStream::create(vstream_info->second, vstream_params, post_infer_queue_elem.release(), std::move(elements), + hw_read_queue_element->get()->set_timeout(std::chrono::milliseconds(HAILO_INFINITE)); + auto vstream = OutputVStream::create(vstream_info->second, vstream_params, user_buffer_queue_element.release(), std::move(elements), std::move(pipeline_status), shutdown_event, core_op_activated_event, pipeline_latency_accumulator.release()); CHECK_EXPECTED(vstream); vstreams.emplace_back(vstream.release()); } else { output_stream->set_timeout(std::chrono::milliseconds(vstream_params.timeout_ms)); - auto vstream = OutputVStream::create(vstream_info->second, vstream_params, hw_read_elem.release(), std::move(elements), + auto vstream = OutputVStream::create(vstream_info->second, vstream_params, hw_read_element.release(), std::move(elements), std::move(pipeline_status), shutdown_event, core_op_activated_event, pipeline_latency_accumulator.release()); CHECK_EXPECTED(vstream); vstreams.emplace_back(vstream.release()); @@ -2071,6 +2311,91 @@ Expected> VStreamsBuilderUtils::create_outputs(std::s return vstreams; } +Expected> VStreamsBuilderUtils::create_output_post_process_softmax(std::shared_ptr output_stream, + const NameToVStreamParamsMap &vstreams_params_map, const hailo_vstream_info_t &output_vstream_info, const NetFlowElement &softmax_op) +{ + std::vector> elements; + std::vector vstreams; + + EventPtr core_op_activated_event = nullptr; + if (!output_stream->is_scheduled()) { + core_op_activated_event = output_stream->get_core_op_activated_event(); + } + + auto shutdown_event = Event::create_shared(Event::State::not_signalled); + CHECK_AS_EXPECTED(nullptr != shutdown_event, HAILO_OUT_OF_HOST_MEMORY); + + auto pipeline_status = make_shared_nothrow>(HAILO_SUCCESS); + CHECK_AS_EXPECTED(nullptr != pipeline_status, HAILO_OUT_OF_HOST_MEMORY); + + assert(!vstreams_params_map.empty()); + + // Note: In case of multiple values in vstreams_params_map (e.g. in the case of demux), we'll set the + // pipeline_elements_stats_flags for the hw_read_element as bitwise or of all the flags. + hailo_pipeline_elem_stats_flags_t hw_read_element_stats_flags = HAILO_PIPELINE_ELEM_STATS_NONE; + hailo_vstream_stats_flags_t hw_read_stream_stats_flags = HAILO_VSTREAM_STATS_NONE; + size_t buffer_pool_size = 0; + for (const auto &elem_name_params : vstreams_params_map) { + hw_read_element_stats_flags |= elem_name_params.second.pipeline_elements_stats_flags; + hw_read_stream_stats_flags |= elem_name_params.second.vstream_stats_flags; + buffer_pool_size += elem_name_params.second.queue_size; + } + + // TODO (HRT-4522): Support this measurement + CHECK_AS_EXPECTED(!(hw_read_stream_stats_flags & HAILO_VSTREAM_STATS_MEASURE_FPS), HAILO_NOT_IMPLEMENTED, + "Pipeline FPS statistics measurement is not implemented"); + + assert(1 == vstreams_params_map.size()); + auto op_input_format = softmax_op.op->inputs_metadata().begin()->second.format; + auto vstream_params = expand_vstream_params_autos_softmax(vstreams_params_map.begin()->second, op_input_format); + if (HAILO_FORMAT_FLAGS_QUANTIZED & vstream_params.user_buffer_format.flags) { + vstream_params.user_buffer_format.flags &= ~HAILO_FORMAT_FLAGS_QUANTIZED; + LOGGER__WARNING("Note: The output_vstream {} format flag is marked as quantized, which is not supported with {}. " + "flag has been automatically set to False.", softmax_op.output_vstream_info.name, softmax_op.op->get_name()); + } + + auto pipeline_latency_accumulator = create_pipeline_latency_accumulator(vstream_params); + CHECK_EXPECTED(pipeline_latency_accumulator); + + auto hw_read_element = add_hw_read_element(output_stream, pipeline_status, elements, "HwReadElement", shutdown_event, + buffer_pool_size, hw_read_element_stats_flags, hw_read_stream_stats_flags); + CHECK_EXPECTED(hw_read_element); + + auto hw_read_queue_element = add_pull_queue_element(output_stream, pipeline_status, elements, "PullQueueElement_hw_read", + shutdown_event, vstream_params); + CHECK_EXPECTED(hw_read_queue_element); + CHECK_SUCCESS_AS_EXPECTED(PipelinePad::link_pads(hw_read_element.value(), hw_read_queue_element.value())); + + auto post_infer_element = add_post_infer_element(output_stream, pipeline_status, elements, + "PostInferElement", vstream_params, shutdown_event); + CHECK_EXPECTED(post_infer_element); + CHECK_SUCCESS_AS_EXPECTED(PipelinePad::link_pads(hw_read_queue_element.value(), post_infer_element.value())); + + auto pre_softmax_queue_element = add_pull_queue_element(output_stream, pipeline_status, elements, "PullQueueElement_pre_softmax", + shutdown_event, vstream_params); + CHECK_SUCCESS_AS_EXPECTED(PipelinePad::link_pads(post_infer_element.value(), pre_softmax_queue_element.value())); + + auto softmax_element = add_softmax_element(output_stream, pipeline_status, elements, "SoftmaxPostProcessElement", + vstream_params, softmax_op); + CHECK_EXPECTED(softmax_element); + CHECK_SUCCESS_AS_EXPECTED(PipelinePad::link_pads(pre_softmax_queue_element.value(), softmax_element.value())); + auto user_buffer_queue_element = add_user_buffer_queue_element(output_stream, pipeline_status, elements, + "UserBufferQueueElement", shutdown_event, vstream_params); + CHECK_SUCCESS_AS_EXPECTED(PipelinePad::link_pads(softmax_element.value(), user_buffer_queue_element.value())); + output_stream->set_timeout(std::chrono::milliseconds(HAILO_INFINITE)); + hw_read_queue_element->get()->set_timeout(std::chrono::milliseconds(HAILO_INFINITE)); + auto vstream = OutputVStream::create(output_vstream_info, vstream_params, user_buffer_queue_element.release(), std::move(elements), + std::move(pipeline_status), shutdown_event, core_op_activated_event, pipeline_latency_accumulator.release()); + CHECK_EXPECTED(vstream); + vstreams.emplace_back(vstream.release()); + + for (const auto &curr_vstream : vstreams) { + LOGGER__INFO("{}", curr_vstream.get_pipeline_description()); + } + + return vstreams; +} + InputVStream VStreamsBuilderUtils::create_input(std::shared_ptr input_vstream) { return InputVStream(std::move(input_vstream)); @@ -2085,6 +2410,92 @@ static bool are_formats_equal(const hailo_format_t &format1, const hailo_format_ return ((format1.order == format2.order) && (format1.flags == format2.flags) && (format1.type == format2.type)); } +Expected> VStreamsBuilderUtils::create_output_vstreams_from_streams(const OutputStreamWithParamsVector &all_output_streams, + OutputStreamPtrVector &output_streams, const hailo_vstream_params_t &vstream_params, + const std::unordered_map> &post_process_ops, + const std::unordered_map &op_inputs_to_op_name, const std::map &output_vstream_infos_map) +{ + auto first_stream_info = output_streams[0]->get_info(); + if ((HAILO_FORMAT_ORDER_HAILO_NMS == first_stream_info.format.order) && + (first_stream_info.nms_info.is_defused)) { + // Case defuse NMS + return create_output_nms(output_streams, vstream_params, output_vstream_infos_map); + } else if (contains(op_inputs_to_op_name, static_cast(first_stream_info.name))) { + // Case post-process on host + auto &op_name = op_inputs_to_op_name.at(first_stream_info.name); + auto &op = post_process_ops.at(op_name); + switch (op.get()->op_type) { + case HAILO_NET_FLOW_OP_TYPE_NMS: + { + assert(1 <= op->op->outputs_metadata().size()); + auto updated_outputs_metadata = op->op->outputs_metadata(); + updated_outputs_metadata.begin()->second.format = vstream_params.user_buffer_format; + if (HAILO_FORMAT_ORDER_AUTO == updated_outputs_metadata.begin()->second.format.order) { + updated_outputs_metadata.begin()->second.format.order = HAILO_FORMAT_ORDER_HAILO_NMS; + } + if (HAILO_FORMAT_TYPE_AUTO == updated_outputs_metadata.begin()->second.format.type) { + updated_outputs_metadata.begin()->second.format.type = HAILO_FORMAT_TYPE_FLOAT32; + } + if (HAILO_FORMAT_FLAGS_QUANTIZED & updated_outputs_metadata.begin()->second.format.flags) { + updated_outputs_metadata.begin()->second.format.flags &= ~HAILO_FORMAT_FLAGS_QUANTIZED; + LOGGER__WARNING("Note: The output_vstream {} format flag is marked as quantized, which is not supported with {}. " + "flag has been automatically set to False.", op->output_vstream_info.name, op->op->get_name()); + } + + op->op->set_outputs_metadata(updated_outputs_metadata); + CHECK_SUCCESS_AS_EXPECTED(op->op->validate_metadata()); + return create_output_post_process_nms(output_streams, vstream_params, output_vstream_infos_map, *op); + } + + case HAILO_NET_FLOW_OP_TYPE_ARGMAX: + { + assert(output_streams.size() == 1); + NameToVStreamParamsMap name_to_vstream_params_map; + for (auto &output_stream : all_output_streams) { + if (output_stream.first->get_info().name == output_streams[0]->get_info().name) { + for (auto &vstream : output_stream.second) { + name_to_vstream_params_map.insert(vstream); + } + } + } + auto output_vstream_info = output_vstream_infos_map.at(op.get()->name); + return create_output_post_process_argmax(output_streams[0], name_to_vstream_params_map, output_vstream_info, *op); + } + + case HAILO_NET_FLOW_OP_TYPE_SOFTMAX: + { + assert(output_streams.size() == 1); + NameToVStreamParamsMap name_to_vstream_params_map; + for (auto &output_stream : all_output_streams) { + if (output_stream.first->get_info().name == output_streams[0]->get_info().name) { + for (auto &vstream : output_stream.second) { + name_to_vstream_params_map.insert(vstream); + } + } + } + auto output_vstream_info = output_vstream_infos_map.at(op.get()->name); + return create_output_post_process_softmax(output_streams[0], name_to_vstream_params_map, output_vstream_info, *op); + } + + default: + LOGGER__ERROR("op type {} of op {} is not in any of the supported post process OP types", op.get()->op_type, op_name); + return make_unexpected(HAILO_INVALID_OPERATION); + } + } else { + // All other cases + assert(output_streams.size() == 1); + NameToVStreamParamsMap name_to_vstream_params_map; + for (auto &output_stream : all_output_streams) { + if (output_stream.first->get_info().name == output_streams[0]->get_info().name) { + for (auto &vstream : output_stream.second) { + name_to_vstream_params_map.insert(vstream); + } + } + } + return create_outputs(output_streams[0], name_to_vstream_params_map, output_vstream_infos_map); + } +} + Expected> VStreamsBuilderUtils::create_output_nms(OutputStreamPtrVector &output_streams, hailo_vstream_params_t vstreams_params, const std::map &output_vstream_infos) @@ -2139,6 +2550,174 @@ Expected> VStreamsBuilderUtils::create_output_post_pr return vstreams; } +Expected> VStreamsBuilderUtils::add_hw_read_element(std::shared_ptr &output_stream, + std::shared_ptr> &pipeline_status, std::vector> &elements, + const std::string &element_name, EventPtr &shutdown_event, size_t buffer_pool_size, + const hailo_pipeline_elem_stats_flags_t &hw_read_element_stats_flags, const hailo_vstream_stats_flags_t &hw_read_stream_stats_flags) +{ + auto hw_read_elem = HwReadElement::create(output_stream, + PipelineObject::create_element_name(element_name, output_stream->name(), output_stream->get_info().index), + HAILO_INFINITE_TIMEOUT, buffer_pool_size, hw_read_element_stats_flags, hw_read_stream_stats_flags, shutdown_event, pipeline_status); + CHECK_EXPECTED(hw_read_elem); + elements.push_back(hw_read_elem.value()); + return hw_read_elem; +} + +Expected> VStreamsBuilderUtils::add_pull_queue_element(std::shared_ptr &output_stream, + std::shared_ptr> &pipeline_status, std::vector> &elements, + const std::string &element_name, EventPtr &shutdown_event, const hailo_vstream_params_t &vstream_params) +{ + auto pull_queue_elem = PullQueueElement::create( + PipelineObject::create_element_name(element_name, output_stream->name(), output_stream->get_info().index), + vstream_params, shutdown_event, pipeline_status); + CHECK_EXPECTED(pull_queue_elem); + elements.push_back(pull_queue_elem.value()); + return pull_queue_elem; +} + +Expected> VStreamsBuilderUtils::add_argmax_element(std::shared_ptr &output_stream, + std::shared_ptr> &pipeline_status, std::vector> &elements, + const std::string &element_name, hailo_vstream_params_t &vstream_params, const NetFlowElement &argmax_op) +{ + // Updating metadata according to user request. TODO: HRT-9737 + auto updated_outputs_metadata = argmax_op.op.get()->outputs_metadata(); + updated_outputs_metadata.begin()->second.format = vstream_params.user_buffer_format; + argmax_op.op.get()->set_outputs_metadata(updated_outputs_metadata); + CHECK_SUCCESS_AS_EXPECTED(argmax_op.op.get()->validate_metadata()); + // Updating metadata according to use request. TODO: HRT-9737 - End + auto argmax_element = ArgmaxPostProcessElement::create(argmax_op.op, + PipelineObject::create_element_name(element_name, output_stream->name(), output_stream->get_info().index), + vstream_params.pipeline_elements_stats_flags, pipeline_status); + CHECK_EXPECTED(argmax_element); + elements.push_back(argmax_element.value()); + return argmax_element; +} + +Expected> VStreamsBuilderUtils::add_softmax_element(std::shared_ptr &output_stream, + std::shared_ptr> &pipeline_status, std::vector> &elements, + const std::string &element_name, hailo_vstream_params_t &vstream_params, const NetFlowElement &softmax_op) +{ + // Updating metadata according to user request. TODO: HRT-9737 + // Currently softmax only supports inputs to be float32 and order NHWC or NC + auto updated_inputs_metadata = softmax_op.op.get()->inputs_metadata(); + updated_inputs_metadata.begin()->second.format = vstream_params.user_buffer_format; + softmax_op.op.get()->set_inputs_metadata(updated_inputs_metadata); + + auto updated_outputs_metadata = softmax_op.op.get()->outputs_metadata(); + updated_outputs_metadata.begin()->second.format = vstream_params.user_buffer_format; + softmax_op.op.get()->set_outputs_metadata(updated_outputs_metadata); + CHECK_SUCCESS_AS_EXPECTED(softmax_op.op.get()->validate_metadata()); + // Updating metadata according to use request. TODO: HRT-9737 - End + auto softmax_element = SoftmaxPostProcessElement::create(softmax_op.op, + PipelineObject::create_element_name(element_name, output_stream->name(), output_stream->get_info().index), + vstream_params.pipeline_elements_stats_flags, pipeline_status); + CHECK_EXPECTED(softmax_element); + elements.push_back(softmax_element.value()); + return softmax_element; +} + +Expected> VStreamsBuilderUtils::add_user_buffer_queue_element(std::shared_ptr &output_stream, + std::shared_ptr> &pipeline_status, std::vector> &elements, + const std::string &element_name, EventPtr &shutdown_event, const hailo_vstream_params_t &vstream_params) +{ + auto post_argmax_queue_element = UserBufferQueueElement::create( + PipelineObject::create_element_name(element_name, output_stream->name(), output_stream->get_info().index), + vstream_params, shutdown_event, pipeline_status); + CHECK_EXPECTED(post_argmax_queue_element); + elements.push_back(post_argmax_queue_element.value()); + return post_argmax_queue_element; +} + +Expected> VStreamsBuilderUtils::add_post_infer_element(std::shared_ptr &output_stream, + std::shared_ptr> &pipeline_status, std::vector> &elements, + const std::string &element_name, const hailo_vstream_params_t &vstream_params, EventPtr shutdown_event) +{ + auto post_infer_element = PostInferElement::create(output_stream->get_info().hw_shape, output_stream->get_info().format, + output_stream->get_info().shape, vstream_params.user_buffer_format, output_stream->get_info().quant_info, output_stream->get_info().nms_info, + PipelineObject::create_element_name(element_name, output_stream->name(), output_stream->get_info().index), + vstream_params, pipeline_status, shutdown_event); + CHECK_EXPECTED(post_infer_element); + elements.push_back(post_infer_element.value()); + return post_infer_element; +} + +Expected> VStreamsBuilderUtils::create_output_post_process_argmax(std::shared_ptr output_stream, + const NameToVStreamParamsMap &vstreams_params_map, const hailo_vstream_info_t &output_vstream_info, const NetFlowElement &argmax_op) +{ + std::vector> elements; + std::vector vstreams; + + EventPtr core_op_activated_event = nullptr; + if (!output_stream->is_scheduled()) { + core_op_activated_event = output_stream->get_core_op_activated_event(); + } + + auto shutdown_event = Event::create_shared(Event::State::not_signalled); + CHECK_AS_EXPECTED(nullptr != shutdown_event, HAILO_OUT_OF_HOST_MEMORY); + + auto pipeline_status = make_shared_nothrow>(HAILO_SUCCESS); + CHECK_AS_EXPECTED(nullptr != pipeline_status, HAILO_OUT_OF_HOST_MEMORY); + + assert(!vstreams_params_map.empty()); + + // Note: In case of multiple values in vstreams_params_map (e.g. in the case of demux), we'll set the + // pipeline_elements_stats_flags for the hw_read_element as bitwise or of all the flags. + hailo_pipeline_elem_stats_flags_t hw_read_element_stats_flags = HAILO_PIPELINE_ELEM_STATS_NONE; + hailo_vstream_stats_flags_t hw_read_stream_stats_flags = HAILO_VSTREAM_STATS_NONE; + size_t buffer_pool_size = 0; + for (const auto &elem_name_params : vstreams_params_map) { + hw_read_element_stats_flags |= elem_name_params.second.pipeline_elements_stats_flags; + hw_read_stream_stats_flags |= elem_name_params.second.vstream_stats_flags; + buffer_pool_size += elem_name_params.second.queue_size; + } + + // TODO (HRT-4522): Support this measurement + CHECK_AS_EXPECTED(!(hw_read_stream_stats_flags & HAILO_VSTREAM_STATS_MEASURE_FPS), HAILO_NOT_IMPLEMENTED, + "Pipeline FPS statistics measurement is not implemented"); + + auto hw_read_element = add_hw_read_element(output_stream, pipeline_status, elements, "HwReadElement", shutdown_event, + buffer_pool_size, hw_read_element_stats_flags, hw_read_stream_stats_flags); + CHECK_EXPECTED(hw_read_element); + + assert(1 == vstreams_params_map.size()); + auto op_input_format = argmax_op.op->inputs_metadata().begin()->second.format; + auto vstream_params = expand_vstream_params_autos_argmax(vstreams_params_map.begin()->second, op_input_format); + + auto hw_read_queue_element = add_pull_queue_element(output_stream, pipeline_status, elements, "PullQueueElement_hw_read", + shutdown_event, vstream_params); + CHECK_EXPECTED(hw_read_queue_element); + + CHECK_SUCCESS_AS_EXPECTED(PipelinePad::link_pads(hw_read_element.value(), hw_read_queue_element.value())); + + auto argmax_element = add_argmax_element(output_stream, pipeline_status, elements, "ArgmaxPostProcessElement", + vstream_params, argmax_op); + CHECK_EXPECTED(argmax_element); + + CHECK_SUCCESS_AS_EXPECTED(PipelinePad::link_pads(hw_read_queue_element.value(), argmax_element.value())); + + auto post_argmax_queue_element = add_user_buffer_queue_element(output_stream, pipeline_status, elements, + "UserBufferQueueElement_post_argmax", shutdown_event, vstream_params); + CHECK_EXPECTED(post_argmax_queue_element); + + CHECK_SUCCESS_AS_EXPECTED(PipelinePad::link_pads(argmax_element.value(), post_argmax_queue_element.value())); + + auto pipeline_latency_accumulator = create_pipeline_latency_accumulator(vstream_params); + CHECK_EXPECTED(pipeline_latency_accumulator); + + output_stream->set_timeout(std::chrono::milliseconds(HAILO_INFINITE)); + hw_read_queue_element->get()->set_timeout(std::chrono::milliseconds(HAILO_INFINITE)); + auto vstream = OutputVStream::create(output_vstream_info, vstream_params, post_argmax_queue_element.release(), std::move(elements), + std::move(pipeline_status), shutdown_event, core_op_activated_event, pipeline_latency_accumulator.release()); + CHECK_EXPECTED(vstream); + vstreams.emplace_back(vstream.release()); + + for (const auto ¤t_vstream : vstreams) { + LOGGER__INFO("{}", current_vstream.get_pipeline_description()); + } + + return vstreams; +} + hailo_status VStreamsBuilderUtils::add_demux(std::shared_ptr output_stream, NameToVStreamParamsMap &vstreams_params_map, std::vector> &&base_elements, std::vector &vstreams, std::shared_ptr hw_read_elem, EventPtr shutdown_event, std::shared_ptr> pipeline_status, @@ -2212,7 +2791,7 @@ hailo_status VStreamsBuilderUtils::add_demux(std::shared_ptr outpu auto post_infer_elem = PostInferElement::create(edge_info.hw_shape, edge_info.format, edge_info.shape, vstream_params.user_buffer_format, edge_info.quant_info, edge_info.nms_info, PipelineObject::create_element_name("PostInferElement", edge_info.name, edge_info.index), - vstream_params, pipeline_status); + vstream_params, pipeline_status, shutdown_event); CHECK_EXPECTED_AS_STATUS(post_infer_elem); current_vstream_elements.push_back(post_infer_elem.value()); CHECK_SUCCESS(PipelinePad::link_pads(demux_queue_elem.value(), post_infer_elem.value())); @@ -2267,7 +2846,7 @@ hailo_status VStreamsBuilderUtils::add_nms_fuse(OutputStreamPtrVector &output_st auto vstream_info = output_vstream_infos.find(fused_layer_name); CHECK(vstream_info != output_vstream_infos.end(), HAILO_NOT_FOUND, - "Failed to find vstream info of {}", fused_layer_name); + "Failed to find vstream info of {}. Could be due to use of old HEF. Try to re-compile network with newer Dataflow Compiler version", fused_layer_name); vstreams_params = expand_vstream_params_autos(first_defused_stream_info, vstreams_params); auto nms_elem = NmsMuxElement::create(nms_infos, @@ -2320,7 +2899,8 @@ hailo_status VStreamsBuilderUtils::add_nms_fuse(OutputStreamPtrVector &output_st auto post_infer_elem = PostInferElement::create({}, src_stream_format, {}, vstreams_params.user_buffer_format, vstream_info->second.quant_info, fused_layer_nms_info, - PipelineObject::create_element_name("PostInferElement", fused_layer_name, 0), vstreams_params, pipeline_status); + PipelineObject::create_element_name("PostInferElement", fused_layer_name, 0), vstreams_params, pipeline_status, + shutdown_event); CHECK_EXPECTED_AS_STATUS(post_infer_elem); elements.push_back(post_infer_elem.value()); diff --git a/hailort/libhailort/src/net_flow/pipeline/vstream_internal.hpp b/hailort/libhailort/src/net_flow/pipeline/vstream_internal.hpp index ef1126f..587d360 100644 --- a/hailort/libhailort/src/net_flow/pipeline/vstream_internal.hpp +++ b/hailort/libhailort/src/net_flow/pipeline/vstream_internal.hpp @@ -70,6 +70,7 @@ public: virtual hailo_status before_fork() { return HAILO_SUCCESS; }; virtual hailo_status after_fork_in_parent() { return HAILO_SUCCESS; }; virtual hailo_status after_fork_in_child() { return HAILO_SUCCESS; }; + virtual bool is_aborted() { return m_is_aborted; }; protected: BaseVStream(const hailo_vstream_info_t &vstream_info, const hailo_vstream_params_t &vstream_params, @@ -235,6 +236,9 @@ public: virtual hailo_status before_fork() override; virtual hailo_status after_fork_in_parent() override; virtual hailo_status after_fork_in_child() override; + virtual hailo_status stop_and_clear() override; + virtual hailo_status start_vstream() override; + virtual bool is_aborted() override; private: InputVStreamClient(std::unique_ptr client, uint32_t input_vstream_handle, hailo_format_t &&user_buffer_format, @@ -274,6 +278,9 @@ public: virtual hailo_status before_fork() override; virtual hailo_status after_fork_in_parent() override; virtual hailo_status after_fork_in_child() override; + virtual hailo_status stop_and_clear() override; + virtual hailo_status start_vstream() override; + virtual bool is_aborted() override; private: OutputVStreamClient(std::unique_ptr client, uint32_t outputs_vstream_handle, hailo_format_t &&user_buffer_format, @@ -323,22 +330,68 @@ public: static Expected> create(const hailo_3d_image_shape_t &src_image_shape, const hailo_format_t &src_format, const hailo_3d_image_shape_t &dst_image_shape, const hailo_format_t &dst_format, const hailo_quant_info_t &dst_quant_info, const hailo_nms_info_t &nms_info, const std::string &name, - hailo_pipeline_elem_stats_flags_t elem_flags, std::shared_ptr> pipeline_status); + hailo_pipeline_elem_stats_flags_t elem_flags, std::shared_ptr> pipeline_status, + std::chrono::milliseconds timeout, hailo_vstream_stats_flags_t vstream_flags, EventPtr shutdown_event, + size_t buffer_pool_size); static Expected> create(const hailo_3d_image_shape_t &src_image_shape, const hailo_format_t &src_format, const hailo_3d_image_shape_t &dst_image_shape, const hailo_format_t &dst_format, const hailo_quant_info_t &dst_quant_info, const hailo_nms_info_t &nms_info, - const std::string &name, const hailo_vstream_params_t &vstream_params, std::shared_ptr> pipeline_status); + const std::string &name, const hailo_vstream_params_t &vstream_params, std::shared_ptr> pipeline_status, EventPtr shutdown_event); PostInferElement(std::unique_ptr &&transform_context, const std::string &name, - DurationCollector &&duration_collector, std::shared_ptr> &&pipeline_status); + DurationCollector &&duration_collector, std::shared_ptr> &&pipeline_status, BufferPoolPtr buffer_pool, + std::chrono::milliseconds timeout); virtual ~PostInferElement() = default; virtual hailo_status run_push(PipelineBuffer &&buffer) override; virtual PipelinePad &next_pad() override; virtual std::string description() const override; + virtual std::vector get_queue_size_accumulators() override; protected: virtual Expected action(PipelineBuffer &&input, PipelineBuffer &&optional) override; private: std::unique_ptr m_transform_context; + BufferPoolPtr m_pool; + std::chrono::milliseconds m_timeout; +}; + +class ArgmaxPostProcessElement : public FilterElement +{ +public: + static Expected> create(std::shared_ptr argmax_op, + const std::string &name, hailo_pipeline_elem_stats_flags_t elem_flags, + std::shared_ptr> pipeline_status); + ArgmaxPostProcessElement(std::shared_ptr argmax_op, const std::string &name, + DurationCollector &&duration_collector, std::shared_ptr> &&pipeline_status); + virtual ~ArgmaxPostProcessElement() = default; + virtual hailo_status run_push(PipelineBuffer &&buffer) override; + virtual PipelinePad &next_pad() override; + virtual std::string description() const override; + +protected: + virtual Expected action(PipelineBuffer &&input, PipelineBuffer &&optional) override; + +private: + std::shared_ptr m_argmax_op; +}; + +class SoftmaxPostProcessElement : public FilterElement +{ +public: + static Expected> create(std::shared_ptr softmax_op, + const std::string &name, hailo_pipeline_elem_stats_flags_t elem_flags, + std::shared_ptr> pipeline_status); + SoftmaxPostProcessElement(std::shared_ptr softmax_op, const std::string &name, + DurationCollector &&duration_collector, std::shared_ptr> &&pipeline_status); + virtual ~SoftmaxPostProcessElement() = default; + virtual hailo_status run_push(PipelineBuffer &&buffer) override; + virtual PipelinePad &next_pad() override; + virtual std::string description() const override; + +protected: + virtual Expected action(PipelineBuffer &&input, PipelineBuffer &&optional) override; + +private: + std::shared_ptr m_softmax_op; }; class NmsPostProcessMuxElement : public BaseMuxElement @@ -429,11 +482,11 @@ public: virtual Expected run_pull(PipelineBuffer &&optional, const PipelinePad &source) override; virtual hailo_status execute_activate() override; virtual hailo_status execute_deactivate() override; - virtual hailo_status execute_post_deactivate() override; + virtual hailo_status execute_post_deactivate(bool should_clear_abort) override; virtual hailo_status execute_clear() override; virtual hailo_status execute_flush() override; virtual hailo_status execute_abort() override; - virtual hailo_status execute_resume() override; + virtual hailo_status execute_clear_abort() override; virtual hailo_status execute_wait_for_finish() override; uint32_t get_invalid_frames_count(); virtual std::string description() const override; @@ -461,11 +514,11 @@ public: virtual Expected run_pull(PipelineBuffer &&optional, const PipelinePad &source) override; virtual hailo_status execute_activate() override; virtual hailo_status execute_deactivate() override; - virtual hailo_status execute_post_deactivate() override; + virtual hailo_status execute_post_deactivate(bool should_clear_abort) override; virtual hailo_status execute_clear() override; virtual hailo_status execute_flush() override; virtual hailo_status execute_abort() override; - virtual hailo_status execute_resume() override; + virtual hailo_status execute_clear_abort() override; virtual hailo_status execute_wait_for_finish() override; virtual std::string description() const override; @@ -498,10 +551,33 @@ public: static Expected> create_output_nms(OutputStreamPtrVector &output_streams, hailo_vstream_params_t vstreams_params, const std::map &output_vstream_infos); + static Expected> create_output_vstreams_from_streams(const OutputStreamWithParamsVector &all_output_streams, + OutputStreamPtrVector &output_streams, const hailo_vstream_params_t &vstream_params, + const std::unordered_map> &post_process_ops, + const std::unordered_map &op_inputs_to_op_name, const std::map &output_vstream_infos_map); static Expected> create_output_post_process_nms(OutputStreamPtrVector &output_streams, hailo_vstream_params_t vstreams_params, const std::map &output_vstream_infos, const NetFlowElement &nms_op); + static Expected> add_hw_read_element(std::shared_ptr &output_stream, + std::shared_ptr> &pipeline_status, std::vector> &elements, + const std::string &element_name, EventPtr &shutdown_event, size_t buffer_pool_size, + const hailo_pipeline_elem_stats_flags_t &hw_read_element_stats_flags, const hailo_vstream_stats_flags_t &hw_read_stream_stats_flags); + static Expected> add_pull_queue_element(std::shared_ptr &output_stream, + std::shared_ptr> &pipeline_status, std::vector> &elements, + const std::string &element_name, EventPtr &shutdown_event, const hailo_vstream_params_t &vstream_params); + static Expected> add_argmax_element(std::shared_ptr &output_stream, + std::shared_ptr> &pipeline_status, std::vector> &elements, + const std::string &element_name, hailo_vstream_params_t &vstream_params, const NetFlowElement &argmax_op); + static Expected> add_softmax_element(std::shared_ptr &output_stream, + std::shared_ptr> &pipeline_status, std::vector> &elements, + const std::string &element_name, hailo_vstream_params_t &vstream_params, const NetFlowElement &softmax_op); + static Expected> add_user_buffer_queue_element(std::shared_ptr &output_stream, + std::shared_ptr> &pipeline_status, std::vector> &elements, + const std::string &element_name, EventPtr &shutdown_event, const hailo_vstream_params_t &vstream_params); + static Expected> add_post_infer_element(std::shared_ptr &output_stream, + std::shared_ptr> &pipeline_status, std::vector> &elements, + const std::string &element_name, const hailo_vstream_params_t &vstream_params, EventPtr shutdown_event); static hailo_status add_demux(std::shared_ptr output_stream, NameToVStreamParamsMap &vstreams_params_map, std::vector> &&elements, std::vector &vstreams, std::shared_ptr hw_read_elem, EventPtr shutdown_event, std::shared_ptr> pipeline_status, @@ -516,6 +592,12 @@ public: const std::map &output_vstream_infos, const NetFlowElement &nms_op); static Expected create_pipeline_latency_accumulator(const hailo_vstream_params_t &vstreams_params); + +private: + static Expected> create_output_post_process_argmax(std::shared_ptr output_stream, + const NameToVStreamParamsMap &vstreams_params_map, const hailo_vstream_info_t &output_vstream_info, const NetFlowElement &argmax_op); + static Expected> create_output_post_process_softmax(std::shared_ptr output_stream, + const NameToVStreamParamsMap &vstreams_params_map, const hailo_vstream_info_t &output_vstream_info, const NetFlowElement &softmax_op); }; } /* namespace hailort */ diff --git a/hailort/libhailort/src/network_group/network_group.cpp b/hailort/libhailort/src/network_group/network_group.cpp index bc09df0..2fa911f 100644 --- a/hailort/libhailort/src/network_group/network_group.cpp +++ b/hailort/libhailort/src/network_group/network_group.cpp @@ -27,6 +27,27 @@ namespace hailort { +Expected> ConfiguredNetworkGroup::duplicate_network_group_client(uint32_t handle, const std::string &network_group_name) +{ +#ifdef HAILO_SUPPORT_MULTI_PROCESS + auto net_group_client = ConfiguredNetworkGroupClient::duplicate_network_group_client(handle, network_group_name); + CHECK_EXPECTED(net_group_client); + + return std::shared_ptr(net_group_client.release()); +#else + (void)handle; + (void)network_group_name; + LOGGER__ERROR("`duplicate_network_group_client()` requires service compilation with HAILO_BUILD_SERVICE"); + return make_unexpected(HAILO_INVALID_OPERATION); +#endif // HAILO_SUPPORT_MULTI_PROCESS +} + +Expected ConfiguredNetworkGroup::get_client_handle() const +{ + LOGGER__ERROR("`get_client_handle()` is valid only when working with HailoRT Service!"); + return make_unexpected(HAILO_INVALID_OPERATION); +} + Expected> ConfiguredNetworkGroup::activate() { const auto network_group_params = HailoRTDefaults::get_active_network_group_params(); @@ -40,6 +61,11 @@ Expected> ConfiguredNetworkGroupBase::act } /* Network group base functions */ +Expected ConfiguredNetworkGroupBase::run_hw_infer_estimator() +{ + return get_core_op()->run_hw_infer_estimator(); +} + Expected ConfiguredNetworkGroupBase::get_latency_measurement(const std::string &network_name) { return get_core_op()->get_latency_measurement(network_name); @@ -48,12 +74,81 @@ Expected ConfiguredNetworkGroupBase::get_latency_measu Expected ConfiguredNetworkGroupBase::get_output_streams_from_vstream_names( const std::map &outputs_params) { - return get_core_op()->get_output_streams_from_vstream_names(outputs_params); + OutputStreamWithParamsVector results; + std::unordered_map outputs_edges_params; + for (auto &name_params_pair : outputs_params) { + auto stream_names = m_network_group_metadata.get_stream_names_from_vstream_name(name_params_pair.first); + CHECK_EXPECTED(stream_names); + + for (auto &stream_name : stream_names.value()) { + auto stream = get_shared_output_stream_by_name(stream_name); + CHECK_EXPECTED(stream); + if (stream.value()->get_info().is_mux) { + outputs_edges_params.emplace(name_params_pair); + } + else { + NameToVStreamParamsMap name_to_params = {name_params_pair}; + results.emplace_back(stream.value(), name_to_params); + } + } + } + // Add non mux streams to result + hailo_status status = add_mux_streams_by_edges_names(results, outputs_edges_params); + CHECK_SUCCESS_AS_EXPECTED(status); + + return results; +} + +// This function adds to results the OutputStreams that correspond to the edges in outputs_edges_params. +// If an edge name appears in outputs_edges_params then all of its predecessors must appear in outputs_edges_params as well, Otherwise, an error is returned. +// We use the set seen_edges in order to mark the edges already evaluated by one of its' predecessor. +hailo_status ConfiguredNetworkGroupBase::add_mux_streams_by_edges_names(OutputStreamWithParamsVector &results, + const std::unordered_map &outputs_edges_params) +{ + std::unordered_set seen_edges; + for (auto &name_params_pair : outputs_edges_params) { + if (seen_edges.end() != seen_edges.find(name_params_pair.first)) { + // Edge has already been seen by one of its predecessors + continue; + } + auto output_streams = get_output_streams_by_vstream_name(name_params_pair.first); + CHECK_EXPECTED_AS_STATUS(output_streams); + CHECK(output_streams->size() == 1, HAILO_INVALID_ARGUMENT, + "mux streams cannot be separated into multiple streams"); + auto output_stream = output_streams.release()[0]; + + // TODO: Find a better way to get the mux edges without creating OutputDemuxer + auto expected_demuxer = OutputDemuxer::create(*output_stream); + CHECK_EXPECTED_AS_STATUS(expected_demuxer); + + NameToVStreamParamsMap name_to_params; + for (auto &edge : expected_demuxer.value()->get_edges_stream_info()) { + auto edge_name_params_pair = outputs_edges_params.find(edge.name); + CHECK(edge_name_params_pair != outputs_edges_params.end(), HAILO_INVALID_ARGUMENT, + "All edges of stream {} must be in output vstream params. edge {} is missing.", + name_params_pair.first, edge.name); + seen_edges.insert(edge.name); + name_to_params.insert(*edge_name_params_pair); + } + results.emplace_back(output_stream, name_to_params); + } + return HAILO_SUCCESS; } Expected ConfiguredNetworkGroupBase::get_output_streams_by_vstream_name(const std::string &name) { - return get_core_op()->get_output_streams_by_vstream_name(name); + auto stream_names = m_network_group_metadata.get_stream_names_from_vstream_name(name); + CHECK_EXPECTED(stream_names); + + OutputStreamPtrVector output_streams; + output_streams.reserve(stream_names->size()); + for (const auto &stream_name : stream_names.value()) { + auto stream = get_shared_output_stream_by_name(stream_name); + CHECK_EXPECTED(stream); + output_streams.emplace_back(stream.value()); + } + + return output_streams; } Expected ConfiguredNetworkGroupBase::get_layer_info(const std::string &stream_name) @@ -63,10 +158,10 @@ Expected ConfiguredNetworkGroupBase::get_layer_info(const std::string ConfiguredNetworkGroupBase::ConfiguredNetworkGroupBase( const ConfigureNetworkParams &config_params, std::vector> &&core_ops, - std::vector> &&net_flow_ops) : + NetworkGroupMetadata &&metadata) : m_config_params(config_params), m_core_ops(std::move(core_ops)), - m_net_flow_ops(std::move(net_flow_ops)) + m_network_group_metadata(std::move(metadata)) {} // static func @@ -101,12 +196,12 @@ Expected> ConfiguredNetworkGroupBase::act const std::string &ConfiguredNetworkGroupBase::get_network_group_name() const { - return get_core_op_metadata()->core_op_name(); + return m_network_group_metadata.name(); } const std::string &ConfiguredNetworkGroupBase::name() const { - return get_core_op_metadata()->core_op_name(); + return m_network_group_metadata.name(); } hailo_status ConfiguredNetworkGroupBase::activate_low_level_streams(uint16_t dynamic_batch_size, bool resume_pending_stream_transfers) @@ -136,19 +231,32 @@ Expected ConfiguredNetworkGroupBase::get_stream_batch_size(const std:: return get_core_op()->get_stream_batch_size(stream_name); } -bool ConfiguredNetworkGroupBase::is_multi_context() const +Expected> ConfiguredNetworkGroupBase::get_sorted_output_names() { - return get_core_op()->is_multi_context(); + auto res = m_network_group_metadata.get_sorted_output_names(); + return res; } -const ConfigureNetworkParams ConfiguredNetworkGroupBase::get_config_params() const +Expected> ConfiguredNetworkGroupBase::get_stream_names_from_vstream_name(const std::string &vstream_name) { - return get_core_op()->get_config_params(); + auto res = m_network_group_metadata.get_stream_names_from_vstream_name(vstream_name); + return res; } Expected> ConfiguredNetworkGroupBase::get_vstream_names_from_stream_name(const std::string &stream_name) { - return get_core_op()->get_vstream_names_from_stream_name(stream_name); + auto res = m_network_group_metadata.get_vstream_names_from_stream_name(stream_name); + return res; +} + +bool ConfiguredNetworkGroupBase::is_multi_context() const +{ + return get_core_op()->is_multi_context(); +} + +const ConfigureNetworkParams ConfiguredNetworkGroupBase::get_config_params() const +{ + return get_core_op()->get_config_params(); } const SupportedFeatures &ConfiguredNetworkGroupBase::get_supported_features() @@ -234,56 +342,95 @@ hailo_status ConfiguredNetworkGroupBase::wait_for_activation(const std::chrono:: Expected>> ConfiguredNetworkGroupBase::get_output_vstream_groups() { - return get_core_op()->get_output_vstream_groups(); + std::vector> results; + + for (auto output_stream : get_output_streams()) { + auto vstreams_group = m_network_group_metadata.get_vstream_names_from_stream_name(output_stream.get().name()); + CHECK_EXPECTED(vstreams_group); + results.push_back(vstreams_group.release()); + } + + return results; } Expected>> ConfiguredNetworkGroupBase::make_output_vstream_params_groups( bool quantized, hailo_format_type_t format_type, uint32_t timeout_ms, uint32_t queue_size) { - return get_core_op()->make_output_vstream_params_groups(quantized, format_type, timeout_ms, queue_size); + auto params = make_output_vstream_params(quantized, format_type, timeout_ms, queue_size); + CHECK_EXPECTED(params); + + auto groups = get_output_vstream_groups(); + CHECK_EXPECTED(groups); + + std::vector> results(groups->size(), std::map()); + + size_t pipeline_group_index = 0; + for (const auto &group : groups.release()) { + for (const auto &name_pair : params.value()) { + if (contains(group, name_pair.first)) { + results[pipeline_group_index].insert(name_pair); + } + } + pipeline_group_index++; + } + + return results; } Expected> ConfiguredNetworkGroupBase::make_input_vstream_params( bool quantized, hailo_format_type_t format_type, uint32_t timeout_ms, uint32_t queue_size, const std::string &network_name) { - return get_core_op()->make_input_vstream_params(quantized, format_type, timeout_ms, queue_size, network_name); + auto input_vstream_infos = m_network_group_metadata.get_input_vstream_infos(network_name); + CHECK_EXPECTED(input_vstream_infos); + + std::map res; + auto status = Hef::Impl::fill_missing_vstream_params_with_default(res, input_vstream_infos.value(), quantized, + format_type, timeout_ms, queue_size); + CHECK_SUCCESS_AS_EXPECTED(status); + return res; } Expected> ConfiguredNetworkGroupBase::make_output_vstream_params( bool quantized, hailo_format_type_t format_type, uint32_t timeout_ms, uint32_t queue_size, const std::string &network_name) { - return get_core_op()->make_output_vstream_params(quantized, format_type, timeout_ms, queue_size, network_name); + auto output_vstream_infos = m_network_group_metadata.get_output_vstream_infos(network_name); + CHECK_EXPECTED(output_vstream_infos); + std::map res; + auto status = Hef::Impl::fill_missing_vstream_params_with_default(res, output_vstream_infos.value(), quantized, + format_type, timeout_ms, queue_size); + CHECK_SUCCESS_AS_EXPECTED(status); + return res; } Expected> ConfiguredNetworkGroupBase::get_network_infos() const { - return get_core_op()->get_network_infos(); + return m_network_group_metadata.get_network_infos(); } Expected> ConfiguredNetworkGroupBase::get_all_stream_infos( const std::string &network_name) const { - return get_core_op()->get_all_stream_infos(network_name); + return get_core_op_metadata()->get_all_stream_infos(network_name); } Expected> ConfiguredNetworkGroupBase::get_input_vstream_infos( const std::string &network_name) const { - return get_core_op()->get_input_vstream_infos(network_name); + return m_network_group_metadata.get_input_vstream_infos(network_name); } Expected> ConfiguredNetworkGroupBase::get_output_vstream_infos( const std::string &network_name) const { - return get_core_op()->get_output_vstream_infos(network_name); + return m_network_group_metadata.get_output_vstream_infos(network_name); } Expected> ConfiguredNetworkGroupBase::get_all_vstream_infos( const std::string &network_name) const { - return get_core_op()->get_all_vstream_infos(network_name); + return m_network_group_metadata.get_all_vstream_infos(network_name); } AccumulatorPtr ConfiguredNetworkGroupBase::get_activation_time_accumulator() const @@ -341,67 +488,49 @@ Expected> ConfiguredNetworkGroupBase::create_input_vst return vstreams; } -Expected> ConfiguredNetworkGroupBase::create_output_vstreams(const std::map &outputs_params) +Expected> ConfiguredNetworkGroupBase::create_output_vstreams(const std::map &vstreams_params) { std::vector vstreams; - vstreams.reserve(outputs_params.size()); - auto output_streams = get_output_streams_from_vstream_names(outputs_params); - CHECK_EXPECTED(output_streams); + vstreams.reserve(vstreams_params.size()); + auto all_output_streams_expected = get_output_streams_from_vstream_names(vstreams_params); + CHECK_EXPECTED(all_output_streams_expected); + auto all_output_streams = all_output_streams_expected.release(); auto output_vstream_infos = get_output_vstream_infos(); CHECK_EXPECTED(output_vstream_infos); auto output_vstream_infos_map = vstream_infos_vector_to_map(output_vstream_infos.release()); - // We iterate through all output streams, and if they are nms, we collect them together by their original stream name. - // We need this step because all nms output streams of the same original stream need to be fused together - - std::unordered_map> post_process_nms_ops; - std::set post_process_stream_inputs; - for (auto &op : m_net_flow_ops) { - post_process_nms_ops.insert({op->name, op}); - post_process_stream_inputs.insert(op->input_streams.begin(), op->input_streams.end()); - } - std::map> nms_op_output_streams; - std::map> nms_output_streams; - for (auto &stream_params_pair : output_streams.value()) { - if ((HAILO_FORMAT_ORDER_HAILO_NMS == stream_params_pair.first->get_info().format.order && stream_params_pair.first->get_info().nms_info.is_defused) && - (outputs_params.end() != outputs_params.find(stream_params_pair.first->get_info().nms_info.defuse_info.original_name))) { - auto original_name = stream_params_pair.first->get_info().nms_info.defuse_info.original_name; - nms_output_streams.emplace(original_name, std::pair( - OutputStreamPtrVector(), outputs_params.at(original_name))); - nms_output_streams[original_name].first.push_back(stream_params_pair.first); - } else if (post_process_stream_inputs.count(stream_params_pair.first->get_info().name)) { - for (auto &op : m_net_flow_ops) { - if (op->input_streams.count(stream_params_pair.first->get_info().name)) { - assert(op->op->outputs_metadata().size() == 1); - nms_op_output_streams.emplace(op->name, std::pair( - OutputStreamPtrVector(), outputs_params.at(op->op->outputs_metadata().begin()->first))); - nms_op_output_streams[op->name].first.push_back(stream_params_pair.first); - } - } - } else { - auto outputs = VStreamsBuilderUtils::create_outputs(stream_params_pair.first, stream_params_pair.second, output_vstream_infos_map); - CHECK_EXPECTED(outputs); - vstreams.insert(vstreams.end(), std::make_move_iterator(outputs->begin()), std::make_move_iterator(outputs->end())); + // Building DBs that connect output_vstreams, output_streams and ops. + // Note: Assuming each post process op has a unique output streams. + // In other words, not possible for an output stream to be connected to more than one op + std::unordered_map> post_process_ops; + std::unordered_map op_inputs_to_op_name; + for (auto &op : m_network_group_metadata.m_net_flow_ops) { + post_process_ops.insert({op->name, op}); + for (auto &input_stream : op->input_streams) { + op_inputs_to_op_name.insert({input_stream, op->name}); } } - for (auto &nms_output_stream_pair : nms_output_streams) { - auto outputs = VStreamsBuilderUtils::create_output_nms(nms_output_stream_pair.second.first, nms_output_stream_pair.second.second, - output_vstream_infos_map); - CHECK_EXPECTED(outputs); - vstreams.insert(vstreams.end(), std::make_move_iterator(outputs->begin()), std::make_move_iterator(outputs->end())); - } - for (auto &nms_output_stream_pair : nms_op_output_streams) { - auto op = post_process_nms_ops.at(nms_output_stream_pair.first); - auto outputs = VStreamsBuilderUtils::create_output_post_process_nms(nms_output_stream_pair.second.first, - nms_output_stream_pair.second.second, output_vstream_infos_map, - *op); + + // streams_added is a vector which holds all stream names which vstreams connected to them were already added (for demux cases) + std::vector streams_added; + for (auto &vstream_params : vstreams_params) { + auto output_streams = get_output_streams_by_vstream_name(vstream_params.first); + CHECK_EXPECTED(output_streams); + if (contains(streams_added, static_cast(output_streams.value()[0]->get_info().name))) { + continue; + } + for (auto &output_stream : output_streams.value()) { + streams_added.push_back(output_stream->get_info().name); + } + + auto outputs = VStreamsBuilderUtils::create_output_vstreams_from_streams(all_output_streams, output_streams.value(), vstream_params.second, + post_process_ops, op_inputs_to_op_name, output_vstream_infos_map); CHECK_EXPECTED(outputs); vstreams.insert(vstreams.end(), std::make_move_iterator(outputs->begin()), std::make_move_iterator(outputs->end())); } get_core_op()->set_vstreams_multiplexer_callbacks(vstreams); - return vstreams; } diff --git a/hailort/libhailort/src/network_group/network_group_internal.hpp b/hailort/libhailort/src/network_group/network_group_internal.hpp index 11c5513..31cb962 100644 --- a/hailort/libhailort/src/network_group/network_group_internal.hpp +++ b/hailort/libhailort/src/network_group/network_group_internal.hpp @@ -51,16 +51,17 @@ namespace hailort { +using stream_name_t = std::string; +using op_name_t = std::string; class ConfiguredNetworkGroupBase : public ConfiguredNetworkGroup { public: static Expected> create(const ConfigureNetworkParams &config_params, - std::vector> &&core_ops, std::vector> &&net_flow_ops) + std::vector> &&core_ops, NetworkGroupMetadata &&metadata) { auto net_group_ptr = std::shared_ptr(new (std::nothrow) - ConfiguredNetworkGroupBase(config_params, std::move(core_ops), std::move(net_flow_ops))); - // auto net_group_ptr = make_shared_nothrow(config_params, std::move(core_ops), std::move(net_flow_ops)); + ConfiguredNetworkGroupBase(config_params, std::move(core_ops), std::move(metadata))); CHECK_NOT_NULL_AS_EXPECTED(net_group_ptr, HAILO_OUT_OF_HOST_MEMORY); return net_group_ptr; @@ -118,16 +119,21 @@ public: virtual bool is_multi_context() const override; virtual const ConfigureNetworkParams get_config_params() const override; + virtual Expected run_hw_infer_estimator() override; + // TODO: HRT-9551 - Change to get_core_op_by_name() when multiple core_ops supported std::shared_ptr get_core_op() const; // TODO: HRT-9546 Remove const std::shared_ptr get_core_op_metadata() const; - Expected> get_vstream_names_from_stream_name(const std::string &stream_name); const SupportedFeatures &get_supported_features(); Expected get_stream_batch_size(const std::string &stream_name); + virtual Expected> get_sorted_output_names() override; + virtual Expected> get_stream_names_from_vstream_name(const std::string &vstream_name) override; + virtual Expected> get_vstream_names_from_stream_name(const std::string &stream_name) override; + virtual Expected> create_input_vstreams(const std::map &inputs_params) override; virtual Expected> create_output_vstreams(const std::map &outputs_params) override; @@ -204,7 +210,7 @@ public: private: ConfiguredNetworkGroupBase(const ConfigureNetworkParams &config_params, - std::vector> &&core_ops, std::vector> &&net_flow_ops); + std::vector> &&core_ops, NetworkGroupMetadata &&metadata); static uint16_t get_smallest_configured_batch_size(const ConfigureNetworkParams &config_params); hailo_status create_vdma_input_stream(Device &device, const std::string &stream_name, @@ -225,7 +231,7 @@ private: const ConfigureNetworkParams m_config_params; std::vector> m_core_ops; - std::vector> m_net_flow_ops; + NetworkGroupMetadata m_network_group_metadata; friend class VDeviceCoreOp; friend class VDeviceActivatedCoreOp; @@ -289,6 +295,12 @@ public: virtual bool is_multi_context() const override; virtual const ConfigureNetworkParams get_config_params() const override; + virtual Expected> get_sorted_output_names() override; + virtual Expected> get_stream_names_from_vstream_name(const std::string &vstream_name) override; + virtual Expected> get_vstream_names_from_stream_name(const std::string &stream_name) override; + + virtual Expected run_hw_infer_estimator() override; + virtual Expected> create_input_vstreams(const std::map &inputs_params); virtual Expected> create_output_vstreams(const std::map &outputs_params); @@ -296,7 +308,16 @@ public: virtual hailo_status after_fork_in_parent() override; virtual hailo_status after_fork_in_child() override; + virtual Expected get_client_handle() const override + { + auto val = m_handle; + return val; + }; + + static Expected> duplicate_network_group_client(uint32_t handle, const std::string &network_group_name); + private: + ConfiguredNetworkGroupClient(uint32_t handle, const std::string &network_group_name); hailo_status create_client(); std::unique_ptr m_client; diff --git a/hailort/libhailort/src/os/CMakeLists.txt b/hailort/libhailort/src/os/CMakeLists.txt index 4e52af3..8e8273c 100644 --- a/hailort/libhailort/src/os/CMakeLists.txt +++ b/hailort/libhailort/src/os/CMakeLists.txt @@ -8,7 +8,7 @@ elseif(UNIX) if (CMAKE_SYSTEM_NAME STREQUAL QNX) set(HAILO_FULL_OS_DIR ${HAILO_OS_DIR}/qnx) else() - set(HAILO_FULL_OS_DIR ${HAILO_OS_DIR}/unix) + set(HAILO_FULL_OS_DIR ${HAILO_OS_DIR}/linux) endif() else() message(FATAL_ERROR "Unexpeced platform target, stopping build") @@ -19,13 +19,12 @@ set(HAILO_OS_DIR ${HAILO_OS_DIR} PARENT_SCOPE) set(HAILO_FULL_OS_DIR ${HAILO_FULL_OS_DIR} PARENT_SCOPE) -set(files - ${HAILO_OS_DIR}/microsec_timer.cpp - ${HAILO_OS_DIR}/file_descriptor.cpp - ${HAILO_OS_DIR}/mmap_buffer.cpp - ${HAILO_OS_DIR}/hailort_driver.cpp - ${HAILO_FULL_OS_DIR}/event.cpp - ${HAILO_FULL_OS_DIR}/driver_scan.cpp -) +if(WIN32) + add_subdirectory(windows) +elseif(UNIX) + add_subdirectory(posix) +else() + message(FATAL_ERROR "Unexpeced platform target, stopping build") +endif() -set(HAILORT_CPP_OS_SOURCES ${files} PARENT_SCOPE) +set(HAILORT_CPP_SOURCES ${HAILORT_CPP_SOURCES} ${SRC_FILES} PARENT_SCOPE) \ No newline at end of file diff --git a/hailort/libhailort/src/os/hailort_driver.hpp b/hailort/libhailort/src/os/hailort_driver.hpp index ff2b3ab..7205d59 100755 --- a/hailort/libhailort/src/os/hailort_driver.hpp +++ b/hailort/libhailort/src/os/hailort_driver.hpp @@ -56,6 +56,7 @@ constexpr uint8_t MAX_H2D_CHANNEL_INDEX = 15; constexpr uint8_t MIN_D2H_CHANNEL_INDEX = MAX_H2D_CHANNEL_INDEX + 1; constexpr uint8_t MAX_D2H_CHANNEL_INDEX = 31; +constexpr size_t SIZE_OF_SINGLE_DESCRIPTOR = 0x10; // NOTE: don't change members from this struct without updating all code using it (platform specific) struct ChannelInterruptTimestamp { @@ -85,16 +86,22 @@ struct IrqData { using ChannelsBitmap = std::array; #if defined(__linux__) || defined(_MSC_VER) +// Unique handle returned from the driver. using vdma_mapped_buffer_driver_identifier = uintptr_t; #elif defined(__QNX__) -struct vdma_mapped_buffer_driver_identifier { - shm_handle_t shm_handle; - int shm_fd; -}; +// Identifier is the shared memory file descriptor. +using vdma_mapped_buffer_driver_identifier = int; #else #error "unsupported platform!" #endif // defined(__linux__) || defined(_MSC_VER) +struct DescriptorsListInfo { + uintptr_t handle; // Unique identifier for the driver. + uint64_t dma_address; + size_t desc_count; + void *user_address; +}; + class HailoRTDriver final { public: @@ -110,6 +117,11 @@ public: BOTH }; + enum class DmaSyncDirection { + TO_HOST = 0, + TO_DEVICE + }; + enum class DmaType { PCIE, DRAM @@ -136,7 +148,7 @@ public: using VdmaBufferHandle = size_t; - static Expected create(const std::string &dev_path); + static Expected create(const DeviceInfo &device_info); // TODO: HRT-7309 add implementation for Windows #if defined(__linux__) || defined(__QNX__) @@ -153,7 +165,7 @@ public: hailo_status write_vdma_channel_register(vdma::ChannelId channel_id, DmaDirection data_direction, size_t offset, size_t reg_size, uint32_t data); - hailo_status vdma_buffer_sync(VdmaBufferHandle buffer, DmaDirection sync_direction, size_t offset, size_t count); + hailo_status vdma_buffer_sync(VdmaBufferHandle buffer, DmaSyncDirection sync_direction, size_t offset, size_t count); hailo_status vdma_interrupts_enable(const ChannelsBitmap &channels_bitmap, bool enable_timestamps_measure); hailo_status vdma_interrupts_disable(const ChannelsBitmap &channel_id); @@ -197,18 +209,18 @@ public: hailo_status vdma_buffer_unmap(VdmaBufferHandle handle); /** - * Allocate vdma descriptors buffer that is accessable via kernel mode, user mode and the given board (using DMA). - * + * Allocate vdma descriptors list object that can bind to some buffer. Used for scatter gather vdma. + * * @param[in] desc_count - number of descriptors to allocate. The descriptor max size is DESC_MAX_SIZE. - * @return Upon success, returns Expected of a pair . - * Otherwise, returns Unexpected of ::hailo_status error. + * @param[in] is_circular - if true, the descriptors list can be used in a circular (and desc_count must be power + * of 2) */ - Expected> descriptors_list_create(size_t desc_count); - + Expected descriptors_list_create(size_t desc_count, bool is_circular); + /** - * Frees a vdma descriptors buffer allocated by 'create_descriptors_buffer'. + * Frees a vdma descriptors buffer allocated by 'descriptors_list_create'. */ - hailo_status descriptors_list_release(uintptr_t desc_handle); + hailo_status descriptors_list_release(const DescriptorsListInfo &descriptors_list_info); /** * Configure vdma channel descriptors to point to the given user address. @@ -233,15 +245,14 @@ public: hailo_status vdma_continuous_buffer_free(uintptr_t buffer_handle); /** - * The actual desc page size might be smaller than the once requested, depends on the host capabilities. + * Marks the device as used for vDMA operations. Only one open FD can be marked at once. + * The device is "unmarked" only on FD close. */ - uint16_t calc_desc_page_size(uint16_t requested_size) const + hailo_status mark_as_used(); + + const std::string &device_id() const { - if (m_desc_max_page_size < requested_size) { - LOGGER__WARNING("Requested desc page size ({}) is bigger than max on this host ({}).", - requested_size, m_desc_max_page_size); - } - return static_cast(std::min(static_cast(requested_size), static_cast(m_desc_max_page_size))); + return m_device_info.device_id; } inline DmaType dma_type() const @@ -251,21 +262,8 @@ public: FileDescriptor& fd() {return m_fd;} - const std::string &dev_path() const + inline bool allocate_driver_buffer() const { - return m_dev_path; - } - - hailo_status mark_as_used(); - -#ifdef __QNX__ - inline pid_t resource_manager_pid() const - { - return m_resource_manager_pid; - } -#endif // __QNX__ - - inline bool allocate_driver_buffer() const { return m_allocate_driver_buffer; } @@ -297,7 +295,12 @@ private: hailo_status read_memory_ioctl(MemoryType memory_type, uint64_t address, void *buf, size_t size); hailo_status write_memory_ioctl(MemoryType memory_type, uint64_t address, const void *buf, size_t size); - HailoRTDriver(const std::string &dev_path, FileDescriptor &&fd, hailo_status &status); + Expected> descriptors_list_create_ioctl(size_t desc_count, bool is_circular); + hailo_status descriptors_list_release_ioctl(uintptr_t desc_handle); + Expected descriptors_list_create_mmap(uintptr_t desc_handle, size_t desc_count); + hailo_status descriptors_list_create_munmap(void *address, size_t desc_count); + + HailoRTDriver(const DeviceInfo &device_info, FileDescriptor &&fd, hailo_status &status); bool is_valid_channel_id(const vdma::ChannelId &channel_id); bool is_valid_channels_bitmap(const ChannelsBitmap &bitmap) @@ -313,7 +316,7 @@ private: } FileDescriptor m_fd; - std::string m_dev_path; + DeviceInfo m_device_info; uint16_t m_desc_max_page_size; DmaType m_dma_type; bool m_allocate_driver_buffer; diff --git a/hailort/libhailort/src/os/mmap_buffer.hpp b/hailort/libhailort/src/os/mmap_buffer.hpp index e66cdfd..90c1572 100644 --- a/hailort/libhailort/src/os/mmap_buffer.hpp +++ b/hailort/libhailort/src/os/mmap_buffer.hpp @@ -26,6 +26,10 @@ public: static Expected create_shared_memory(size_t length); static Expected create_file_map(size_t length, FileDescriptor &file, uintptr_t offset); +#if defined(__QNX__) + static Expected create_file_map_nocache(size_t length, FileDescriptor &file, uintptr_t offset); +#endif /* defined(__QNX__) */ + MmapBufferImpl() : m_address(INVALID_ADDR), m_length(0), m_unmappable(false) {} ~MmapBufferImpl() @@ -51,6 +55,8 @@ public: return m_address; } + size_t size() const { return m_length; } + bool is_mapped() const { return (INVALID_ADDR != m_address); @@ -89,6 +95,15 @@ public: return MmapBuffer(std::move(mmap.release())); } +#if defined(__QNX__) + static Expected> create_file_map_nocache(size_t length, FileDescriptor &file, uintptr_t offset) + { + auto mmap = MmapBufferImpl::create_file_map_nocache(length, file, offset); + CHECK_EXPECTED(mmap); + return MmapBuffer(mmap.release()); + } +#endif /* defined(__QNX__) */ + MmapBuffer() = default; ~MmapBuffer() = default; @@ -106,6 +121,8 @@ public: return reinterpret_cast(m_mmap.address()); } + size_t size() const { return m_mmap.size(); } + template std::enable_if_t::value, U&> operator*() { diff --git a/hailort/libhailort/src/os/posix/CMakeLists.txt b/hailort/libhailort/src/os/posix/CMakeLists.txt new file mode 100644 index 0000000..2aa2e8a --- /dev/null +++ b/hailort/libhailort/src/os/posix/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.0.0) + +if (CMAKE_SYSTEM_NAME STREQUAL QNX) + add_subdirectory(qnx) +else() + add_subdirectory(linux) +endif() + +set(files + ${CMAKE_CURRENT_SOURCE_DIR}/microsec_timer.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/file_descriptor.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mmap_buffer.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/hailort_driver.cpp +) + +set(HAILORT_CPP_SOURCES ${HAILORT_CPP_SOURCES} ${files} PARENT_SCOPE) diff --git a/hailort/libhailort/src/os/posix/hailort_driver.cpp b/hailort/libhailort/src/os/posix/hailort_driver.cpp index 47b3a1e..4615f4d 100755 --- a/hailort/libhailort/src/os/posix/hailort_driver.cpp +++ b/hailort/libhailort/src/os/posix/hailort_driver.cpp @@ -107,20 +107,16 @@ const uintptr_t HailoRTDriver::INVALID_DRIVER_BUFFER_HANDLE_VALUE = INVALID_DRIV const size_t HailoRTDriver::INVALID_DRIVER_VDMA_MAPPING_HANDLE_VALUE = INVALID_DRIVER_HANDLE_VALUE; const uint8_t HailoRTDriver::INVALID_VDMA_CHANNEL_INDEX = INVALID_VDMA_CHANNEL; -Expected HailoRTDriver::create(const std::string &dev_path) +Expected HailoRTDriver::create(const DeviceInfo &device_info) { - hailo_status status = HAILO_UNINITIALIZED; + auto fd = FileDescriptor(open(device_info.dev_path.c_str(), O_RDWR)); + CHECK_AS_EXPECTED(fd >= 0, HAILO_DRIVER_FAIL, + "Failed to open device file {} with error {}", device_info.dev_path, errno); - auto fd = FileDescriptor(open(dev_path.c_str(), O_RDWR)); - if (0 > fd) { - LOGGER__ERROR("Failed to open board {}", dev_path); - return make_unexpected(HAILO_OPEN_FILE_FAILURE); - } + hailo_status status = HAILO_UNINITIALIZED; + HailoRTDriver object(device_info, std::move(fd), status); + CHECK_SUCCESS_AS_EXPECTED(status); - HailoRTDriver object(dev_path, std::move(fd), status); - if (HAILO_SUCCESS != status) { - return make_unexpected(status); - } return object; } @@ -155,9 +151,9 @@ static hailo_status validate_driver_version(const hailo_driver_info &driver_info return HAILO_SUCCESS; } -HailoRTDriver::HailoRTDriver(const std::string &dev_path, FileDescriptor &&fd, hailo_status &status) : +HailoRTDriver::HailoRTDriver(const DeviceInfo &device_info, FileDescriptor &&fd, hailo_status &status) : m_fd(std::move(fd)), - m_dev_path(dev_path), + m_device_info(device_info), m_allocate_driver_buffer(false) { hailo_driver_info driver_info = {}; @@ -429,13 +425,13 @@ hailo_status HailoRTDriver::write_memory_ioctl(MemoryType memory_type, uint64_t return HAILO_SUCCESS; } -hailo_status HailoRTDriver::vdma_buffer_sync(VdmaBufferHandle handle, DmaDirection sync_direction, size_t offset, size_t count) +hailo_status HailoRTDriver::vdma_buffer_sync(VdmaBufferHandle handle, DmaSyncDirection sync_direction, + size_t offset, size_t count) { #if defined(__linux__) - CHECK(sync_direction != DmaDirection::BOTH, HAILO_INVALID_ARGUMENT, "Can't sync vdma data both host and device"); hailo_vdma_buffer_sync_params sync_info{ .handle = handle, - .sync_type = (sync_direction == DmaDirection::H2D) ? HAILO_SYNC_FOR_DEVICE : HAILO_SYNC_FOR_HOST, + .sync_type = (sync_direction == DmaSyncDirection::TO_HOST) ? HAILO_SYNC_FOR_CPU : HAILO_SYNC_FOR_DEVICE, .offset = offset, .count = count }; @@ -623,12 +619,11 @@ hailo_status HailoRTDriver::reset_nn_core() return HAILO_SUCCESS; } - + +#if defined(__linux__) Expected HailoRTDriver::vdma_buffer_map(void *user_address, size_t required_size, DmaDirection data_direction, const vdma_mapped_buffer_driver_identifier &driver_buff_handle) { - -#if defined(__linux__) hailo_vdma_buffer_map_params map_user_buffer_info { .user_address = user_address, .size = required_size, @@ -636,29 +631,55 @@ Expected HailoRTDriver::vdma_buffer_map(void *u .allocated_buffer_handle = driver_buff_handle, .mapped_handle = 0 }; + + int err = 0; + auto status = hailo_ioctl(this->m_fd, HAILO_VDMA_BUFFER_MAP, &map_user_buffer_info, err); + if (HAILO_SUCCESS != status) { + LOGGER__ERROR("Failed to map user buffer with errno:{}", err); + return make_unexpected(HAILO_DRIVER_FAIL); + } + + return VdmaBufferHandle(map_user_buffer_info.mapped_handle); +} #elif defined( __QNX__) +Expected HailoRTDriver::vdma_buffer_map(void *user_address, size_t required_size, + DmaDirection data_direction, const vdma_mapped_buffer_driver_identifier &driver_buff_handle) +{ + // Mapping is done by the driver_buff_handle (shm file descriptor), and not by address. + (void)user_address; + + // Create shared memory handle to send to driver + shm_handle_t shm_handle; + int err = shm_create_handle(driver_buff_handle, m_resource_manager_pid, O_RDWR, + &shm_handle, 0); + if (0 != err) { + LOGGER__ERROR("Error creating shm object handle, errno is: {}", errno); + return make_unexpected(HAILO_INTERNAL_FAILURE); + } + hailo_vdma_buffer_map_params map_user_buffer_info { - .shared_memory_handle = driver_buff_handle.shm_handle, + .shared_memory_handle = shm_handle, .size = required_size, .data_direction = direction_to_dma_data_direction(data_direction), .allocated_buffer_handle = INVALID_DRIVER_HANDLE_VALUE, .mapped_handle = 0 }; - (void)user_address; -#else -#error "unsupported platform!" -#endif // __linux__ - - int err = 0; + // Note: The driver will accept the shm_handle, and will mmap it to its own address space. After the driver maps the + // the shm, calling shm_delete_handle is not needed (but can't harm on the otherhand). + // If the ioctl fails, we can't tell if the shm was mapped or not, so we delete it ourself. auto status = hailo_ioctl(this->m_fd, HAILO_VDMA_BUFFER_MAP, &map_user_buffer_info, err); if (HAILO_SUCCESS != status) { LOGGER__ERROR("Failed to map user buffer with errno:{}", err); + shm_delete_handle(shm_handle); return make_unexpected(HAILO_DRIVER_FAIL); } return VdmaBufferHandle(map_user_buffer_info.mapped_handle); } +#else +#error "unsupported platform!" +#endif // __linux__ hailo_status HailoRTDriver::vdma_buffer_unmap(VdmaBufferHandle handle) { @@ -676,9 +697,53 @@ hailo_status HailoRTDriver::vdma_buffer_unmap(VdmaBufferHandle handle) return HAILO_SUCCESS; } -Expected> HailoRTDriver::descriptors_list_create(size_t desc_count) +Expected HailoRTDriver::descriptors_list_create(size_t desc_count, bool is_circular) +{ + auto handle_to_dma_address_pair = descriptors_list_create_ioctl(desc_count, is_circular); + CHECK_EXPECTED(handle_to_dma_address_pair); + + const auto desc_handle = handle_to_dma_address_pair->first; + const auto dma_address = handle_to_dma_address_pair->second; + + auto user_address = descriptors_list_create_mmap(desc_handle, desc_count); + if (!user_address) { + auto status = descriptors_list_release_ioctl(desc_handle); + if (HAILO_SUCCESS != status) { + LOGGER__ERROR("Failed releasing descriptors list, status {}", status); + // continue + } + return make_unexpected(user_address.status()); + } + + return DescriptorsListInfo{desc_handle, dma_address, desc_count, user_address.release()}; +} + +hailo_status HailoRTDriver::descriptors_list_release(const DescriptorsListInfo &descriptors_list_info) { - hailo_desc_list_create_params create_desc_info {.desc_count = desc_count, .desc_handle = 0, .dma_address = 0 }; + hailo_status status = HAILO_SUCCESS; + + auto unmap_status = descriptors_list_create_munmap(descriptors_list_info.user_address, descriptors_list_info.desc_count); + if (HAILO_SUCCESS != unmap_status) { + LOGGER__ERROR("Descriptors list unmap failed with {}", unmap_status); + status = unmap_status; + // continue + } + + auto release_status = descriptors_list_release_ioctl(descriptors_list_info.handle); + if (HAILO_SUCCESS != release_status) { + LOGGER__ERROR("Descriptors list release status failed with {}", release_status); + status = release_status; + // continue + } + + return status; +} + +Expected> HailoRTDriver::descriptors_list_create_ioctl(size_t desc_count, bool is_circular) +{ + hailo_desc_list_create_params create_desc_info{}; + create_desc_info.desc_count = desc_count; + create_desc_info.is_circular = is_circular; int err = 0; auto status = hailo_ioctl(this->m_fd, HAILO_DESC_LIST_CREATE, &create_desc_info, err); @@ -690,7 +755,7 @@ Expected> HailoRTDriver::descriptors_list_create( return std::make_pair(create_desc_info.desc_handle, create_desc_info.dma_address); } -hailo_status HailoRTDriver::descriptors_list_release(uintptr_t desc_handle) +hailo_status HailoRTDriver::descriptors_list_release_ioctl(uintptr_t desc_handle) { int err = 0; auto status = hailo_ioctl(this->m_fd, HAILO_DESC_LIST_RELEASE, &desc_handle, err); @@ -699,9 +764,70 @@ hailo_status HailoRTDriver::descriptors_list_release(uintptr_t desc_handle) return HAILO_DRIVER_FAIL; } - return HAILO_SUCCESS; + return HAILO_SUCCESS; +} + +#if defined(__linux__) +Expected HailoRTDriver::descriptors_list_create_mmap(uintptr_t desc_handle, size_t desc_count) +{ + const size_t buffer_size = desc_count * SIZE_OF_SINGLE_DESCRIPTOR; + void *address = mmap(nullptr, buffer_size, PROT_WRITE | PROT_READ, MAP_SHARED, m_fd, (off_t)desc_handle); + if (MAP_FAILED == address) { + LOGGER__ERROR("Failed to map descriptors list buffer with errno: {}", errno); + return make_unexpected(HAILO_DRIVER_FAIL); + } + return address; +} + +hailo_status HailoRTDriver::descriptors_list_create_munmap(void *address, size_t desc_count) +{ + const size_t buffer_size = desc_count * SIZE_OF_SINGLE_DESCRIPTOR; + if (0 != munmap(address, buffer_size)) { + LOGGER__ERROR("munmap of address {}, length: {} failed with errno: {}", address, buffer_size, errno); + return HAILO_DRIVER_FAIL; + } + return HAILO_SUCCESS; } +#elif defined(__QNX__) + +Expected HailoRTDriver::descriptors_list_create_mmap(uintptr_t desc_handle, size_t desc_count) +{ + const size_t buffer_size = desc_count * SIZE_OF_SINGLE_DESCRIPTOR; + struct hailo_non_linux_desc_list_mmap_params map_vdma_list_params { + .desc_handle = desc_handle, + .size = buffer_size, + .user_address = nullptr, + }; + + int err = 0; + auto status = HailoRTDriver::hailo_ioctl(m_fd, HAILO_NON_LINUX_DESC_LIST_MMAP, &map_vdma_list_params, err); + if (HAILO_SUCCESS != status) { + LOGGER__ERROR("Mmap descriptors list ioctl failed with errno:{}", err); + return make_unexpected(HAILO_DRIVER_FAIL); + } + + void *address = mmap(nullptr, buffer_size, PROT_WRITE | PROT_READ | PROT_NOCACHE, MAP_SHARED | MAP_PHYS, NOFD, + (off_t)map_vdma_list_params.user_address); + CHECK_AS_EXPECTED(MAP_FAILED != address, HAILO_INTERNAL_FAILURE, "Failed to mmap buffer fd with errno:{}", errno); + + return address; +} + +hailo_status HailoRTDriver::descriptors_list_create_munmap(void *address, size_t desc_count) +{ + const size_t buffer_size = desc_count * SIZE_OF_SINGLE_DESCRIPTOR; + if (0 != munmap(address, buffer_size)) { + LOGGER__ERROR("munmap of address {}, length: {} failed with errno: {}", address, buffer_size, errno); + return HAILO_DRIVER_FAIL; + } + return HAILO_SUCCESS; +} + +#else +#error "unsupported platform!" +#endif + hailo_status HailoRTDriver::descriptors_list_bind_vdma_buffer(uintptr_t desc_handle, VdmaBufferHandle buffer_handle, uint16_t desc_page_size, uint8_t channel_index, uint32_t starting_desc) { @@ -764,6 +890,11 @@ Expected> HailoRTDriver::vdma_continuous_buffer_a int err = 0; auto status = hailo_ioctl(this->m_fd, HAILO_VDMA_CONTINUOUS_BUFFER_ALLOC, ¶ms, err); if (HAILO_SUCCESS != status) { + if (ENOMEM == err) { + LOGGER__WARN("Failed to allocate continuous buffer, size 0x{:x}. This failure means there is not a sufficient amount of CMA memory", + size); + return make_unexpected(HAILO_OUT_OF_HOST_CMA_MEMORY); + } LOGGER__ERROR("Failed allocate continuous buffer with errno:{}", err); return make_unexpected(HAILO_DRIVER_FAIL); } diff --git a/hailort/libhailort/src/os/posix/linux/CMakeLists.txt b/hailort/libhailort/src/os/posix/linux/CMakeLists.txt new file mode 100644 index 0000000..cffd810 --- /dev/null +++ b/hailort/libhailort/src/os/posix/linux/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.0.0) + +set(files + ${CMAKE_CURRENT_SOURCE_DIR}/driver_scan.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/event.cpp +) + +set(HAILORT_CPP_SOURCES ${HAILORT_CPP_SOURCES} ${files} PARENT_SCOPE) diff --git a/hailort/libhailort/src/os/posix/unix/driver_scan.cpp b/hailort/libhailort/src/os/posix/linux/driver_scan.cpp similarity index 100% rename from hailort/libhailort/src/os/posix/unix/driver_scan.cpp rename to hailort/libhailort/src/os/posix/linux/driver_scan.cpp diff --git a/hailort/libhailort/src/os/posix/unix/event.cpp b/hailort/libhailort/src/os/posix/linux/event.cpp similarity index 65% rename from hailort/libhailort/src/os/posix/unix/event.cpp rename to hailort/libhailort/src/os/posix/linux/event.cpp index 4c4525e..c9e753b 100644 --- a/hailort/libhailort/src/os/posix/unix/event.cpp +++ b/hailort/libhailort/src/os/posix/linux/event.cpp @@ -5,16 +5,13 @@ /** * @file event.cpp * @brief Event & Semaphore wrapper for Unix - * - * TODO: doc **/ #include "hailo/hailort.h" #include "hailo/event.hpp" #include "common/utils.hpp" - -#include "utils/event_internal.hpp" +#include "common/event_internal.hpp" #include #include @@ -24,10 +21,6 @@ namespace hailort { -Waitable::Waitable(underlying_waitable_handle_t handle) : - m_handle(handle) -{} - Waitable::~Waitable() { if (-1 != m_handle) { @@ -39,9 +32,9 @@ Waitable::Waitable(Waitable&& other) : m_handle(std::exchange(other.m_handle, -1)) {} -underlying_waitable_handle_t Waitable::get_underlying_handle() +hailo_status Waitable::wait_for_single_object(underlying_waitable_handle_t handle, std::chrono::milliseconds timeout) { - return m_handle; + return eventfd_poll(handle, timeout); } hailo_status Waitable::eventfd_poll(underlying_waitable_handle_t fd, std::chrono::milliseconds timeout) @@ -147,11 +140,6 @@ EventPtr Event::create_shared(const State& initial_state) return make_shared_nothrow(handle); } -hailo_status Event::wait(std::chrono::milliseconds timeout) -{ - return eventfd_poll(m_handle, timeout); -} - hailo_status Event::signal() { return eventfd_write(m_handle); @@ -201,22 +189,6 @@ SemaphorePtr Semaphore::create_shared(uint32_t initial_count) return make_shared_nothrow(handle); } -hailo_status Semaphore::wait(std::chrono::milliseconds timeout) -{ - // TODO: See SDK-16568 (might be necessary in the future) - hailo_status status = eventfd_poll(m_handle, timeout); - if (HAILO_TIMEOUT == status) { - LOGGER__INFO("eventfd_poll failed, status = {}", status); - return status; - } - CHECK_SUCCESS(status); - - status = eventfd_read(m_handle); - CHECK_SUCCESS(status); - - return HAILO_SUCCESS; -} - hailo_status Semaphore::signal() { return eventfd_write(m_handle); @@ -227,6 +199,11 @@ bool Semaphore::is_auto_reset() return true; } +hailo_status Semaphore::post_wait() +{ + return eventfd_read(m_handle); +} + underlying_waitable_handle_t Semaphore::open_semaphore_handle(uint32_t initial_count) { static const int SEMAPHORE = EFD_SEMAPHORE; @@ -237,63 +214,30 @@ underlying_waitable_handle_t Semaphore::open_semaphore_handle(uint32_t initial_c return handle; } -WaitOrShutdown::WaitOrShutdown(WaitablePtr waitable, EventPtr shutdown_event) : - m_waitable(waitable), - m_shutdown_event(shutdown_event), - m_wait_handle_array(create_wait_handle_array(waitable, shutdown_event)) -{} - -hailo_status WaitOrShutdown::wait(std::chrono::milliseconds timeout) +Expected WaitableGroup::wait_any(std::chrono::milliseconds timeout) { int poll_ret = -1; do { - poll_ret = poll(m_wait_handle_array.data(), m_wait_handle_array.size(), static_cast(timeout.count())); + poll_ret = poll(m_waitable_handles.data(), m_waitable_handles.size(), static_cast(timeout.count())); } while ((0 > poll_ret) && (EINTR == poll_ret)); if (0 == poll_ret) { LOGGER__TRACE("Timeout"); - return HAILO_TIMEOUT; - } - if (0 > poll_ret) { - LOGGER__ERROR("poll failed with errno={}", errno); - return HAILO_INTERNAL_FAILURE; - } - if ((0 == (m_wait_handle_array[WAITABLE_INDEX].revents & POLLIN)) && - (0 == (m_wait_handle_array[SHUTDOWN_INDEX].revents & POLLIN))) { - LOGGER__ERROR("Both pfds not in read state: waitable.revents={}, shutdown.revents={}", - m_wait_handle_array[WAITABLE_INDEX].revents, m_wait_handle_array[SHUTDOWN_INDEX].revents); - return HAILO_INTERNAL_FAILURE; + return make_unexpected(HAILO_TIMEOUT); } + CHECK_AS_EXPECTED(poll_ret > 0, HAILO_INTERNAL_FAILURE, "poll failed with errno={}", errno); - if (m_wait_handle_array[SHUTDOWN_INDEX].revents & POLLIN) { - return HAILO_SHUTDOWN_EVENT_SIGNALED; - } + for (size_t index = 0; index < m_waitable_handles.size(); index++) { + if (m_waitable_handles[index].revents & POLLIN) { + auto status = m_waitables[index].get().post_wait(); + CHECK_SUCCESS_AS_EXPECTED(status); - if (m_waitable->is_auto_reset() && (m_wait_handle_array[WAITABLE_INDEX].revents & POLLIN)) { - uint64_t dummy; - ssize_t read_ret = read(m_wait_handle_array[WAITABLE_INDEX].fd, &dummy, sizeof(dummy)); - if (sizeof(dummy) != read_ret) { - LOGGER__ERROR("read failed. bytes_read={}, expected={}, errno={}", read_ret, sizeof(dummy), errno); - return HAILO_INTERNAL_FAILURE; + return index; } } - return HAILO_SUCCESS; -} - -hailo_status WaitOrShutdown::signal() -{ - return m_waitable->signal(); -} - -WaitOrShutdown::WaitHandleArray WaitOrShutdown::create_wait_handle_array(WaitablePtr waitable, EventPtr shutdown_event) -{ - // Note the order! - WaitHandleArray pfds{{ - {shutdown_event->get_underlying_handle(), POLLIN, 0}, - {waitable->get_underlying_handle(), POLLIN, 0} - }}; - return pfds; + LOGGER__ERROR("None of the pollfd are in read state"); + return make_unexpected(HAILO_INTERNAL_FAILURE); } } /* namespace hailort */ diff --git a/hailort/libhailort/src/os/posix/mmap_buffer.cpp b/hailort/libhailort/src/os/posix/mmap_buffer.cpp index 0d2ff57..0939118 100644 --- a/hailort/libhailort/src/os/posix/mmap_buffer.cpp +++ b/hailort/libhailort/src/os/posix/mmap_buffer.cpp @@ -44,33 +44,19 @@ Expected MmapBufferImpl::create_shared_memory(size_t length) Expected MmapBufferImpl::create_file_map(size_t length, FileDescriptor &file, uintptr_t offset) { -#ifdef __linux__ void *address = mmap(nullptr, length, PROT_WRITE | PROT_READ, MAP_SHARED, file, (off_t)offset); CHECK_AS_EXPECTED(INVALID_ADDR != address, HAILO_INTERNAL_FAILURE, "Failed to mmap buffer fd with errno:{}", errno); -#elif defined(__QNX__) - - // TODO change name of struct - using this sturct because itis exact fields we need ro qnx mmap too (where user address is physical addr) - struct hailo_non_linux_desc_list_mmap_params map_vdma_list_params { - .desc_handle = offset, - .size = length, - .user_address = nullptr, - }; - - int err = 0; - auto status = HailoRTDriver::hailo_ioctl(file, HAILO_NON_LINUX_DESC_LIST_MMAP, &map_vdma_list_params, err); - if (HAILO_SUCCESS != status) { - LOGGER__ERROR("HAILO_NON_LINUX_DESC_LIST_MMAP failed with errno:{}", err); - return make_unexpected(HAILO_DRIVER_FAIL); - } + return MmapBufferImpl(address, length); +} - void *address = mmap(nullptr, length, PROT_WRITE | PROT_READ | PROT_NOCACHE, MAP_SHARED | MAP_PHYS, NOFD, (off_t)map_vdma_list_params.user_address); +#if defined(__QNX__) +Expected MmapBufferImpl::create_file_map_nocache(size_t length, FileDescriptor &file, uintptr_t offset) +{ + void *address = mmap(nullptr, length, PROT_WRITE | PROT_READ | PROT_NOCACHE, MAP_SHARED, file, (off_t)offset); CHECK_AS_EXPECTED(INVALID_ADDR != address, HAILO_INTERNAL_FAILURE, "Failed to mmap buffer fd with errno:{}", errno); -#else -#error "unsupported platform!" -#endif // __linux__ - return MmapBufferImpl(address, length); } +#endif /* defined(__QNX__) */ hailo_status MmapBufferImpl::unmap() { diff --git a/hailort/libhailort/src/os/posix/qnx/CMakeLists.txt b/hailort/libhailort/src/os/posix/qnx/CMakeLists.txt new file mode 100644 index 0000000..cffd810 --- /dev/null +++ b/hailort/libhailort/src/os/posix/qnx/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.0.0) + +set(files + ${CMAKE_CURRENT_SOURCE_DIR}/driver_scan.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/event.cpp +) + +set(HAILORT_CPP_SOURCES ${HAILORT_CPP_SOURCES} ${files} PARENT_SCOPE) diff --git a/hailort/libhailort/src/os/posix/qnx/event.cpp b/hailort/libhailort/src/os/posix/qnx/event.cpp index 9303b93..94f1617 100644 --- a/hailort/libhailort/src/os/posix/qnx/event.cpp +++ b/hailort/libhailort/src/os/posix/qnx/event.cpp @@ -14,8 +14,7 @@ #include "hailo/event.hpp" #include "common/utils.hpp" - -#include "utils/event_internal.hpp" +#include "common/event_internal.hpp" #include #include @@ -31,10 +30,6 @@ namespace hailort { -Waitable::Waitable(underlying_waitable_handle_t handle) : - m_handle(handle) -{} - Waitable::~Waitable() { if (INVALID_EVENT_HANDLE != m_handle) { @@ -49,11 +44,6 @@ Waitable::Waitable(Waitable&& other) : m_handle(std::exchange(other.m_handle, INVALID_EVENT_HANDLE)) {} -underlying_waitable_handle_t Waitable::get_underlying_handle() -{ - return m_handle; -} - hailo_status Waitable::wait_for_single_object(underlying_waitable_handle_t handle, std::chrono::milliseconds timeout) { const size_t timeout_ms = (timeout.count() > INT_MAX) ? INT_MAX : static_cast(timeout.count()); @@ -88,11 +78,6 @@ EventPtr Event::create_shared(const State& initial_state) return make_shared_nothrow(handle); } -hailo_status Event::wait(std::chrono::milliseconds timeout) -{ - return wait_for_single_object(m_handle, timeout); -} - hailo_status Event::signal() { const auto result = neosmart::SetEvent(m_handle); @@ -144,27 +129,6 @@ SemaphorePtr Semaphore::create_shared(uint32_t initial_count) return make_shared_nothrow(handle, initial_count); } -hailo_status Semaphore::wait(std::chrono::milliseconds timeout) -{ - auto wait_result = wait_for_single_object(m_handle, timeout); - if (HAILO_SUCCESS == wait_result) { - m_sem_mutex.lock(); - if (0 == m_count.load()) { - LOGGER__ERROR("Waiting on semaphore with 0 value"); - } - if (m_count > 0) { - m_count--; - } - // After decrementing the value of the semaphore - check if the new value is bigger than 0 and if it is signal the event - if (m_count > 0) { - neosmart::SetEvent(m_handle); - } - m_sem_mutex.unlock(); - } - - return wait_result; -} - hailo_status Semaphore::signal() { m_sem_mutex.lock(); @@ -208,71 +172,41 @@ Semaphore::Semaphore(Semaphore&& other) : other.m_sem_mutex.unlock(); } -WaitOrShutdown::WaitOrShutdown(WaitablePtr waitable, EventPtr shutdown_event) : - m_waitable(waitable), - m_shutdown_event(shutdown_event), - m_wait_handle_array(create_wait_handle_array(waitable, shutdown_event)) -{} +hailo_status Semaphore::post_wait() +{ + std::unique_lock lock(m_sem_mutex); + CHECK(m_count.load() > 0, HAILO_INTERNAL_FAILURE, "Wait returned on semaphore with 0 value"); -void Event::post_wait() -{} + m_count--; -void Semaphore::post_wait(){ - m_sem_mutex.lock(); - if (0 == m_count.load()) { - LOGGER__ERROR("Wait Returned on semaphore with 0 value"); - } - if (m_count > 0) { - m_count--; - } // After decrementing the value of the semaphore - check if the new value is bigger than 0 and if it is signal the event if (m_count > 0) { neosmart::SetEvent(m_handle); } - m_sem_mutex.unlock(); + + return HAILO_SUCCESS; } -hailo_status WaitOrShutdown::wait(std::chrono::milliseconds timeout) +Expected WaitableGroup::wait_any(std::chrono::milliseconds timeout) { int wait_index = -1; const uint64_t timeout_ms = (timeout.count() > INT_MAX) ? INT_MAX : static_cast(timeout.count()); - const auto wait_result = neosmart::WaitForMultipleEvents(m_wait_handle_array.data(), static_cast(m_wait_handle_array.size()), - false, timeout_ms, wait_index); - // If semaphore need to subtract from counter + const bool WAIT_FOR_ANY = false; + const auto wait_result = neosmart::WaitForMultipleEvents(m_waitable_handles.data(), + static_cast(m_waitable_handles.size()), WAIT_FOR_ANY, timeout_ms, wait_index); if (0 != wait_result) { if (ETIMEDOUT == wait_result) { - return HAILO_TIMEOUT; + return make_unexpected(HAILO_TIMEOUT); } else { LOGGER__ERROR("WaitForMultipleEvents Failed, error: {}", wait_result); - return HAILO_INTERNAL_FAILURE; + return make_unexpected(HAILO_INTERNAL_FAILURE); } } - - if (WAITABLE_INDEX == wait_index) { - // Meaning it can be a semaphore object - m_waitable->post_wait(); - return HAILO_SUCCESS; - } else if (SHUTDOWN_INDEX == wait_index) { - return HAILO_SHUTDOWN_EVENT_SIGNALED; - } else { - LOGGER__ERROR("Invalid event index signalled in WaitForMultipleEventsFailed, index: {}", wait_index); - return HAILO_INTERNAL_FAILURE; - } -} -hailo_status WaitOrShutdown::signal() -{ - return m_waitable->signal(); -} + auto status = m_waitables[wait_index].get().post_wait(); + CHECK_SUCCESS_AS_EXPECTED(status); -WaitOrShutdown::WaitHandleArray WaitOrShutdown::create_wait_handle_array(WaitablePtr waitable, EventPtr shutdown_event) -{ - // Note the order! - WaitHandleArray handles{ - shutdown_event->get_underlying_handle(), - waitable->get_underlying_handle() - }; - return handles; + return wait_index; } } /* namespace hailort */ diff --git a/hailort/libhailort/src/os/windows/CMakeLists.txt b/hailort/libhailort/src/os/windows/CMakeLists.txt new file mode 100644 index 0000000..bba4bed --- /dev/null +++ b/hailort/libhailort/src/os/windows/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.0.0) + +set(files + ${CMAKE_CURRENT_SOURCE_DIR}/microsec_timer.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/file_descriptor.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/mmap_buffer.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/hailort_driver.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/event.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/driver_scan.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/virtual_alloc_guard.cpp +) + +set(HAILORT_CPP_SOURCES ${HAILORT_CPP_SOURCES} ${files} PARENT_SCOPE) diff --git a/hailort/libhailort/src/os/windows/driver_scan.cpp b/hailort/libhailort/src/os/windows/driver_scan.cpp index 3870675..cec1bb6 100644 --- a/hailort/libhailort/src/os/windows/driver_scan.cpp +++ b/hailort/libhailort/src/os/windows/driver_scan.cpp @@ -150,7 +150,7 @@ Expected> list_devices() &guid, NULL, CM_GET_DEVICE_INTERFACE_LIST_PRESENT); - CHECK_AS_EXPECTED(cr == CR_SUCCESS && len >= 2, HAILO_PCIE_DRIVER_NOT_INSTALLED, + CHECK_AS_EXPECTED((cr == CR_SUCCESS) && (len > 0), HAILO_PCIE_DRIVER_NOT_INSTALLED, "Driver interface not found error {}", cr); std::string names_str; diff --git a/hailort/libhailort/src/os/windows/event.cpp b/hailort/libhailort/src/os/windows/event.cpp index ab1a35d..d13f29a 100644 --- a/hailort/libhailort/src/os/windows/event.cpp +++ b/hailort/libhailort/src/os/windows/event.cpp @@ -11,8 +11,7 @@ #include "hailo/event.hpp" #include "common/utils.hpp" - -#include "utils/event_internal.hpp" +#include "common/event_internal.hpp" #include #include @@ -21,10 +20,6 @@ namespace hailort { -Waitable::Waitable(underlying_waitable_handle_t handle) : - m_handle(handle) -{} - Waitable::~Waitable() { if (nullptr != m_handle) { @@ -36,11 +31,6 @@ Waitable::Waitable(Waitable&& other) : m_handle(std::exchange(other.m_handle, nullptr)) {} -underlying_waitable_handle_t Waitable::get_underlying_handle() -{ - return m_handle; -} - static DWORD timeout_millies(long long value) { DWORD millies = static_cast(value); @@ -89,11 +79,6 @@ EventPtr Event::create_shared(const State& initial_state) return make_shared_nothrow(handle); } -hailo_status Event::wait(std::chrono::milliseconds timeout) -{ - return wait_for_single_object(m_handle, timeout); -} - hailo_status Event::signal() { const auto result = SetEvent(m_handle); @@ -153,11 +138,6 @@ SemaphorePtr Semaphore::create_shared(uint32_t initial_count) return make_shared_nothrow(handle); } -hailo_status Semaphore::wait(std::chrono::milliseconds timeout) -{ - return wait_for_single_object(m_handle, timeout); -} - hailo_status Semaphore::signal() { static const LONG INCREMENT_BY_ONE = 1; @@ -176,6 +156,12 @@ bool Semaphore::is_auto_reset() return true; } +hailo_status Semaphore::post_wait() +{ + // On windows, after wait on semaphore the counters decrease automatically. + return HAILO_SUCCESS; +} + underlying_waitable_handle_t Semaphore::open_semaphore_handle(uint32_t initial_count) { static const LPSECURITY_ATTRIBUTES NO_INHERITANCE = nullptr; @@ -188,45 +174,24 @@ underlying_waitable_handle_t Semaphore::open_semaphore_handle(uint32_t initial_c return handle; } -WaitOrShutdown::WaitOrShutdown(WaitablePtr waitable, EventPtr shutdown_event) : - m_waitable(waitable), - m_shutdown_event(shutdown_event), - m_wait_handle_array(create_wait_handle_array(waitable, shutdown_event)) -{} - -hailo_status WaitOrShutdown::wait(std::chrono::milliseconds timeout) +Expected WaitableGroup::wait_any(std::chrono::milliseconds timeout) { DWORD wait_millies = timeout_millies(timeout.count()); - static const BOOL WAIT_FOR_ANY = false; - const auto wait_result = WaitForMultipleObjects(static_cast(m_wait_handle_array.size()), - m_wait_handle_array.data(), WAIT_FOR_ANY, wait_millies); - switch (wait_result) { - case WAIT_OBJECT_0 + WAITABLE_INDEX: - return HAILO_SUCCESS; - case WAIT_OBJECT_0 + SHUTDOWN_INDEX: - return HAILO_SHUTDOWN_EVENT_SIGNALED; - case WAIT_TIMEOUT: - return HAILO_TIMEOUT; - default: - LOGGER__ERROR("WaitForMultipleObjects returned {}, last_error={}", wait_result, GetLastError()); - return HAILO_INTERNAL_FAILURE; + const auto WAIT_OBJECT_N = WAIT_OBJECT_0 + m_waitable_handles.size(); + const bool WAIT_FOR_ANY = false; + const auto wait_result = WaitForMultipleObjects(static_cast(m_waitable_handles.size()), + m_waitable_handles.data(), WAIT_FOR_ANY, wait_millies); + if (wait_result == WAIT_TIMEOUT) { + return make_unexpected(HAILO_TIMEOUT); + } else if ((wait_result >= WAIT_OBJECT_0) && (wait_result < WAIT_OBJECT_N)) { + // Object is signaled. + // Note! On windows there is no need to call post_wait() because it is done automatically. + return wait_result - WAIT_OBJECT_0; + } else { + LOGGER__ERROR("WaitForMultipleObjects returned {}, last_error={}", wait_result, GetLastError()); + return make_unexpected(HAILO_INTERNAL_FAILURE); } } -hailo_status WaitOrShutdown::signal() -{ - return m_waitable->signal(); -} - -WaitOrShutdown::WaitHandleArray WaitOrShutdown::create_wait_handle_array(WaitablePtr waitable, EventPtr shutdown_event) -{ - // Note the order! - WaitHandleArray handles{ - shutdown_event->get_underlying_handle(), - waitable->get_underlying_handle() - }; - return handles; -} - } /* namespace hailort */ diff --git a/hailort/libhailort/src/os/windows/hailort_driver.cpp b/hailort/libhailort/src/os/windows/hailort_driver.cpp index 7707978..57b0db6 100644 --- a/hailort/libhailort/src/os/windows/hailort_driver.cpp +++ b/hailort/libhailort/src/os/windows/hailort_driver.cpp @@ -294,9 +294,9 @@ static hailo_status validate_driver_version(const hailo_driver_info &driver_info return HAILO_SUCCESS; } -HailoRTDriver::HailoRTDriver(const std::string &dev_path, FileDescriptor &&fd, hailo_status &status) : +HailoRTDriver::HailoRTDriver(const DeviceInfo &device_info, FileDescriptor &&fd, hailo_status &status) : m_fd(std::move(fd)), - m_dev_path(dev_path), + m_device_info(device_info), m_allocate_driver_buffer(false) { tCompatibleHailoIoctlData data = {}; @@ -353,17 +353,17 @@ Expected> HailoRTDriver::scan_devices() return devices_info; } -Expected HailoRTDriver::create(const std::string &dev_path) +Expected HailoRTDriver::create(const DeviceInfo &device_info) { hailo_status status = HAILO_UNINITIALIZED; - CDeviceFile f(dev_path); + CDeviceFile f(device_info.dev_path); if (!f.Present()) { - LOGGER__ERROR("Failed to open board {}", dev_path); + LOGGER__ERROR("Failed to open board {}", device_info.dev_path); return make_unexpected(HAILO_OPEN_FILE_FAILURE); } FileDescriptor fd(f.Detach()); - HailoRTDriver platform(dev_path, std::move(fd), status); + HailoRTDriver platform(device_info, std::move(fd), status); if (HAILO_SUCCESS != status) { return make_unexpected(status); } @@ -564,13 +564,13 @@ hailo_status HailoRTDriver::write_vdma_channel_register(vdma::ChannelId channel_ return HAILO_SUCCESS; } -hailo_status HailoRTDriver::vdma_buffer_sync(VdmaBufferHandle handle, DmaDirection sync_direction, size_t offset, size_t count) +hailo_status HailoRTDriver::vdma_buffer_sync(VdmaBufferHandle handle, DmaSyncDirection sync_direction, + size_t offset, size_t count) { - CHECK(sync_direction != DmaDirection::BOTH, HAILO_INVALID_ARGUMENT, "Can't sync vdma data both host and device"); tCompatibleHailoIoctlData data = {}; hailo_vdma_buffer_sync_params& sync_info = data.Buffer.VdmaBufferSync; sync_info.handle = handle; - sync_info.sync_type = (sync_direction == DmaDirection::H2D) ? HAILO_SYNC_FOR_DEVICE : HAILO_SYNC_FOR_HOST; + sync_info.sync_type = (sync_direction == DmaSyncDirection::TO_HOST) ? HAILO_SYNC_FOR_CPU : HAILO_SYNC_FOR_DEVICE; sync_info.offset = offset; sync_info.count = count; if (0 > ioctl(this->m_fd, HAILO_VDMA_BUFFER_SYNC, &data)) { @@ -762,13 +762,54 @@ hailo_status HailoRTDriver::vdma_buffer_unmap(VdmaBufferHandle handle) return HAILO_SUCCESS; } -Expected> HailoRTDriver::descriptors_list_create(size_t desc_count) +Expected HailoRTDriver::descriptors_list_create(size_t desc_count, bool is_circular) +{ + auto handle_to_dma_address_pair = descriptors_list_create_ioctl(desc_count, is_circular); + CHECK_EXPECTED(handle_to_dma_address_pair); + + const auto desc_handle = handle_to_dma_address_pair->first; + const auto dma_address = handle_to_dma_address_pair->second; + + auto user_address = descriptors_list_create_mmap(desc_handle, desc_count); + if (!user_address) { + auto status = descriptors_list_release_ioctl(desc_handle); + if (HAILO_SUCCESS != status) { + LOGGER__ERROR("Failed releasing descriptors list, status {}", status); + // continue + } + return make_unexpected(user_address.status()); + } + + return DescriptorsListInfo{desc_handle, dma_address, desc_count, user_address.release()}; +} + +hailo_status HailoRTDriver::descriptors_list_release(const DescriptorsListInfo &descriptors_list_info) +{ + hailo_status status = HAILO_SUCCESS; + + auto unmap_status = descriptors_list_create_munmap(descriptors_list_info.user_address, descriptors_list_info.desc_count); + if (HAILO_SUCCESS != unmap_status) { + LOGGER__ERROR("Descriptors list unmap failed with {}", unmap_status); + status = unmap_status; + // continue + } + + auto release_status = descriptors_list_release_ioctl(descriptors_list_info.handle); + if (HAILO_SUCCESS != release_status) { + LOGGER__ERROR("Descriptors list release status failed with {}", release_status); + status = release_status; + // continue + } + + return status; +} + +Expected> HailoRTDriver::descriptors_list_create_ioctl(size_t desc_count, bool is_circular) { tCompatibleHailoIoctlData data = {}; hailo_desc_list_create_params& create_desc_info = data.Buffer.DescListCreate; create_desc_info.desc_count = desc_count; - create_desc_info.desc_handle = 0; - create_desc_info.dma_address = 0; + create_desc_info.is_circular = is_circular; if (0 > ioctl(this->m_fd, HAILO_DESC_LIST_CREATE, &data)) { LOGGER__ERROR("Failed to create descriptors list with errno: {}", errno); @@ -778,10 +819,10 @@ Expected> HailoRTDriver::descriptors_list_create( return std::move(std::make_pair(create_desc_info.desc_handle, create_desc_info.dma_address)); } -hailo_status HailoRTDriver::descriptors_list_release(uintptr_t desc_handle) +hailo_status HailoRTDriver::descriptors_list_release_ioctl(uintptr_t desc_handle) { tCompatibleHailoIoctlData data = {}; - uintptr_t& release_desc_info = data.Buffer.DescListReleaseParam; + uintptr_t& release_desc_info = data.Buffer.DescListReleaseParam; release_desc_info = desc_handle; if (0 > ioctl(this->m_fd, HAILO_DESC_LIST_RELEASE, &data)) { LOGGER__ERROR("Failed to release descriptors list with errno: {}", errno); @@ -791,6 +832,26 @@ hailo_status HailoRTDriver::descriptors_list_release(uintptr_t desc_handle) return HAILO_SUCCESS; } +Expected HailoRTDriver::descriptors_list_create_mmap(uintptr_t desc_handle, size_t desc_count) +{ + tCompatibleHailoIoctlData data = {}; + data.Buffer.DescListMmap.desc_handle = desc_handle; + data.Buffer.DescListMmap.size = desc_count * SIZE_OF_SINGLE_DESCRIPTOR; + if (0 > ioctl(m_fd, HAILO_NON_LINUX_DESC_LIST_MMAP, &data)) { + LOGGER__ERROR("Failed to map physical memory with errno: {}", errno); + return make_unexpected(HAILO_DRIVER_FAIL); + } + + void *user_address = data.Buffer.DescListMmap.user_address; + return user_address; +} + +hailo_status HailoRTDriver::descriptors_list_create_munmap(void *, size_t ) +{ + // On windows, the unmap is done on the release ioctl + return HAILO_SUCCESS; +} + hailo_status HailoRTDriver::descriptors_list_bind_vdma_buffer(uintptr_t desc_handle, VdmaBufferHandle buffer_handle, uint16_t desc_page_size, uint8_t channel_index, uint32_t starting_desc) { @@ -841,19 +902,6 @@ hailo_status HailoRTDriver::reset_nn_core() return HAILO_NOT_IMPLEMENTED; } -Expected MmapBufferImpl::create_file_map(size_t length, FileDescriptor &file, uintptr_t offset) -{ - tCompatibleHailoIoctlData data = {}; - data.Buffer.DescListMmap.desc_handle = offset; - data.Buffer.DescListMmap.size = length; - if (0 > ioctl(file, HAILO_NON_LINUX_DESC_LIST_MMAP, &data)) { - LOGGER__ERROR("Failed to map physical memory with errno: {}", errno); - return make_unexpected(HAILO_DRIVER_FAIL); - } - // this mapping will be deleted automatically with the physical allocation - return MmapBufferImpl(data.Buffer.DescListMmap.user_address, length, false); -} - Expected HailoRTDriver::vdma_low_memory_buffer_alloc(size_t size) { (void) size; return make_unexpected(HAILO_INVALID_OPERATION); diff --git a/hailort/libhailort/src/os/windows/mmap_buffer.cpp b/hailort/libhailort/src/os/windows/mmap_buffer.cpp index 3107cc1..95391e9 100644 --- a/hailort/libhailort/src/os/windows/mmap_buffer.cpp +++ b/hailort/libhailort/src/os/windows/mmap_buffer.cpp @@ -14,19 +14,22 @@ namespace hailort void * const MmapBufferImpl::INVALID_ADDR = NULL; -Expected MmapBufferImpl::create_shared_memory(size_t length) +Expected MmapBufferImpl::create_shared_memory(size_t) { - void *address = VirtualAlloc(NULL, length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); - CHECK_AS_EXPECTED(INVALID_ADDR != address, HAILO_OUT_OF_HOST_MEMORY, "Failed to mmap buffer with error:{}", GetLastError()); - return MmapBufferImpl(address, length, true); + LOGGER__ERROR("Creating shared memory is not implemented on windows"); + return make_unexpected(HAILO_NOT_IMPLEMENTED); +} + +Expected MmapBufferImpl::create_file_map(size_t, FileDescriptor &, uintptr_t ) +{ + LOGGER__ERROR("Creating file mapping is not implemented on windows"); + return make_unexpected(HAILO_NOT_IMPLEMENTED); } hailo_status MmapBufferImpl::unmap() { - if (m_unmappable) { - VirtualFree(m_address, m_length, MEM_RELEASE); - } - return HAILO_SUCCESS; + LOGGER__ERROR("Unmapping is not implemented on windows"); + return HAILO_NOT_IMPLEMENTED; } } /* namespace hailort */ diff --git a/hailort/libhailort/src/os/windows/virtual_alloc_guard.cpp b/hailort/libhailort/src/os/windows/virtual_alloc_guard.cpp new file mode 100644 index 0000000..425454a --- /dev/null +++ b/hailort/libhailort/src/os/windows/virtual_alloc_guard.cpp @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file virtual_alloc_guard.cpp + * @brief Guard object for VirtualAlloc and VirtualFree + **/ + +#include "os/windows/virtual_alloc_guard.hpp" +#include "common/logger_macros.hpp" +#include "common/utils.hpp" + +namespace hailort +{ + +Expected VirtualAllocGuard::create(size_t size) +{ + hailo_status status = HAILO_UNINITIALIZED; + VirtualAllocGuard guard(size, status); + CHECK_SUCCESS_AS_EXPECTED(status); + return guard; +} + +VirtualAllocGuard::VirtualAllocGuard(size_t size, hailo_status &status) : + m_address(VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)), + m_size(size) +{ + if (nullptr == m_address) { + status = HAILO_OUT_OF_HOST_MEMORY; + return; + } + + status = HAILO_SUCCESS; +} + +VirtualAllocGuard::~VirtualAllocGuard() +{ + if (nullptr != m_address) { + // From msdn - when passing MEM_RELEASE to VirtualFree, 0 must be passed as size. + static constexpr size_t ZERO_SIZE = 0; + if (!VirtualFree(m_address, ZERO_SIZE, MEM_RELEASE)) { + LOGGER__ERROR("VirtualFree failed with error {}", GetLastError()); + } + } +} + +} /* namespace hailort */ diff --git a/hailort/libhailort/src/os/windows/virtual_alloc_guard.hpp b/hailort/libhailort/src/os/windows/virtual_alloc_guard.hpp new file mode 100644 index 0000000..d89c4ba --- /dev/null +++ b/hailort/libhailort/src/os/windows/virtual_alloc_guard.hpp @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file virtual_alloc_guard.hpp + * @brief Guard object for VirtualAlloc and VirtualFree (only for windows os). + **/ + +#ifndef _HAILO_VIRTUAL_ALLOC_GUARD_HPP_ +#define _HAILO_VIRTUAL_ALLOC_GUARD_HPP_ + +#include "hailo/expected.hpp" + +#include + +namespace hailort +{ + +class VirtualAllocGuard final { +public: + static Expected create(size_t size); + ~VirtualAllocGuard(); + + VirtualAllocGuard(const VirtualAllocGuard &other) = delete; + VirtualAllocGuard &operator=(const VirtualAllocGuard &other) = delete; + VirtualAllocGuard(VirtualAllocGuard &&other) : + m_address(std::exchange(other.m_address, nullptr)), + m_size(other.m_size) + {} + VirtualAllocGuard &operator=(VirtualAllocGuard &&other) = delete; + + void *address() { return m_address; } + size_t size() const { return m_size; } + +private: + VirtualAllocGuard(size_t size, hailo_status &status); + + void *m_address; + const size_t m_size; +}; + +} /* namespace hailort */ + +#endif /* _HAILO_VIRTUAL_ALLOC_GUARD_HPP_ */ diff --git a/hailort/libhailort/src/service/hailort_rpc_client.cpp b/hailort/libhailort/src/service/hailort_rpc_client.cpp index 30340aa..6f68357 100644 --- a/hailort/libhailort/src/service/hailort_rpc_client.cpp +++ b/hailort/libhailort/src/service/hailort_rpc_client.cpp @@ -23,7 +23,7 @@ hailo_status HailoRtRpcClient::client_keep_alive(uint32_t pid) keepalive_Request request; request.set_pid(pid); empty reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->client_keep_alive(&context, request, &reply); CHECK_GRPC_STATUS(status); return HAILO_SUCCESS; @@ -33,7 +33,7 @@ Expected HailoRtRpcClient::get_service_version() { get_service_version_Request request; get_service_version_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->get_service_version(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -58,7 +58,7 @@ Expected HailoRtRpcClient::VDevice_create(const hailo_vdevice_params_t proto_vdevice_params->set_group_id(params.group_id == nullptr ? "" : std::string(params.group_id)); VDevice_create_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->VDevice_create(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -72,19 +72,20 @@ Expected HailoRtRpcClient::VDevice_dup_handle(uint32_t pid, uint32_t h request.set_pid(pid); request.set_handle(handle); dup_handle_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->VDevice_dup_handle(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); return reply.handle(); } -hailo_status HailoRtRpcClient::VDevice_release(uint32_t handle) +hailo_status HailoRtRpcClient::VDevice_release(uint32_t handle, uint32_t pid) { Release_Request request; request.set_handle(handle); + request.set_pid(pid); Release_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->VDevice_release(&context, request, &reply); CHECK_GRPC_STATUS(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -122,7 +123,7 @@ Expected> HailoRtRpcClient::InputVStreams_create(uint32_t } VStreams_create_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->InputVStreams_create(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -135,13 +136,14 @@ Expected> HailoRtRpcClient::InputVStreams_create(uint32_t return input_vstreams_handles; } -hailo_status HailoRtRpcClient::InputVStream_release(uint32_t handle) +hailo_status HailoRtRpcClient::InputVStream_release(uint32_t handle, uint32_t pid) { Release_Request request; request.set_handle(handle); + request.set_pid(pid); Release_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->InputVStream_release(&context, request, &reply); CHECK_GRPC_STATUS(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -179,7 +181,7 @@ Expected> HailoRtRpcClient::OutputVStreams_create(uint32_t } VStreams_create_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->OutputVStreams_create(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -192,13 +194,14 @@ Expected> HailoRtRpcClient::OutputVStreams_create(uint32_t return output_vstreams_handles; } -hailo_status HailoRtRpcClient::OutputVStream_release(uint32_t handle) +hailo_status HailoRtRpcClient::OutputVStream_release(uint32_t handle, uint32_t pid) { Release_Request request; request.set_handle(handle); + request.set_pid(pid); Release_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->OutputVStream_release(&context, request, &reply); CHECK_GRPC_STATUS(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -212,7 +215,7 @@ Expected HailoRtRpcClient::InputVStream_dup_handle(uint32_t pid, uint3 request.set_pid(pid); request.set_handle(handle); dup_handle_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->InputVStream_dup_handle(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); return reply.handle(); @@ -224,7 +227,7 @@ Expected HailoRtRpcClient::OutputVStream_dup_handle(uint32_t pid, uint request.set_pid(pid); request.set_handle(handle); dup_handle_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->OutputVStream_dup_handle(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); return reply.handle(); @@ -274,7 +277,7 @@ Expected> HailoRtRpcClient::VDevice_configure(uint32_t vde } VDevice_configure_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->VDevice_configure(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -290,7 +293,7 @@ Expected> HailoRtRpcClient::VDevice_get_physical_device request.set_handle(handle); VDevice_get_physical_devices_ids_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->VDevice_get_physical_devices_ids(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -302,13 +305,30 @@ Expected> HailoRtRpcClient::VDevice_get_physical_device return result; } +Expected>> HailoRtRpcClient::VDevice_get_physical_devices(uint32_t handle) +{ + std::vector> devices; + + auto device_ids = VDevice_get_physical_devices_ids(handle); + CHECK_EXPECTED(device_ids); + devices.reserve(device_ids->size()); + + for (const auto &device_id : device_ids.value()) { + auto device = Device::create(device_id); + CHECK_EXPECTED(device); + devices.push_back(std::move(device.release())) ; + } + + return devices; +} + Expected HailoRtRpcClient::VDevice_get_default_streams_interface(uint32_t handle) { VDevice_get_default_streams_interface_Request request; request.set_handle(handle); VDevice_get_default_streams_interface_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->VDevice_get_default_streams_interface(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -324,19 +344,20 @@ Expected HailoRtRpcClient::ConfiguredNetworkGroup_dup_handle(uint32_t request.set_pid(pid); request.set_handle(handle); dup_handle_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_dup_handle(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); return reply.handle(); } -hailo_status HailoRtRpcClient::ConfiguredNetworkGroup_release(uint32_t handle) +hailo_status HailoRtRpcClient::ConfiguredNetworkGroup_release(uint32_t handle, uint32_t pid) { Release_Request request; request.set_handle(handle); + request.set_pid(pid); Release_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_release(&context, request, &reply); CHECK_GRPC_STATUS(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -381,7 +402,7 @@ Expected> HailoRtRpcClient::Config request.set_network_name(network_name); ConfiguredNetworkGroup_make_input_vstream_params_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_make_input_vstream_params(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -400,7 +421,7 @@ Expected>> HailoRtRpcC request.set_queue_size(queue_size); ConfiguredNetworkGroup_make_output_vstream_params_groups_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_make_output_vstream_params_groups(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -426,7 +447,7 @@ Expected> HailoRtRpcClient::Config request.set_network_name(network_name); ConfiguredNetworkGroup_make_output_vstream_params_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_make_output_vstream_params(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -464,7 +485,7 @@ Expected HailoRtRpcClient::ConfiguredNetworkGroup_name(uint32_t han request.set_handle(handle); ConfiguredNetworkGroup_name_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_name(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -479,7 +500,7 @@ Expected> HailoRtRpcClient::ConfiguredNetworkG request.set_handle(handle); ConfiguredNetworkGroup_get_network_infos_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_get_network_infos(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -503,7 +524,7 @@ Expected> HailoRtRpcClient::ConfiguredNetworkGr request.set_network_name(network_name); ConfiguredNetworkGroup_get_all_stream_infos_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_get_all_stream_infos(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -533,6 +554,8 @@ Expected> HailoRtRpcClient::ConfiguredNetworkGr proto_stream_info.nms_info().chunks_per_frame(), proto_stream_info.nms_info().is_defused(), nms_defuse_info, + proto_stream_info.nms_info().burst_size(), + static_cast(proto_stream_info.nms_info().burst_type()), }; hailo_format_t format{ static_cast(proto_stream_info.format().type()), @@ -571,7 +594,7 @@ Expected HailoRtRpcClient::ConfiguredNetworkGroup_get_ request.set_handle(handle); ConfiguredNetworkGroup_get_default_stream_interface_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_get_default_stream_interface(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -586,7 +609,7 @@ Expected>> HailoRtRpcClient::ConfiguredNetw request.set_handle(handle); ConfiguredNetworkGroup_get_output_vstream_groups_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_get_output_vstream_groups(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -660,7 +683,7 @@ Expected> HailoRtRpcClient::ConfiguredNetworkG request.set_network_name(network_name); ConfiguredNetworkGroup_get_vstream_infos_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_get_input_vstream_infos(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -676,7 +699,7 @@ Expected> HailoRtRpcClient::ConfiguredNetworkG request.set_network_name(network_name); ConfiguredNetworkGroup_get_vstream_infos_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_get_output_vstream_infos(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -692,7 +715,7 @@ Expected> HailoRtRpcClient::ConfiguredNetworkG request.set_network_name(network_name); ConfiguredNetworkGroup_get_vstream_infos_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_get_all_vstream_infos(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -705,7 +728,7 @@ Expected HailoRtRpcClient::ConfiguredNetworkGroup_is_scheduled(uint32_t ha ConfiguredNetworkGroup_is_scheduled_Request request; ConfiguredNetworkGroup_is_scheduled_Reply reply; request.set_handle(handle); - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_is_scheduled(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -722,7 +745,7 @@ hailo_status HailoRtRpcClient::ConfiguredNetworkGroup_set_scheduler_timeout(uint request.set_network_name(network_name); ConfiguredNetworkGroup_set_scheduler_timeout_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_set_scheduler_timeout(&context, request, &reply); CHECK_GRPC_STATUS(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -738,7 +761,7 @@ hailo_status HailoRtRpcClient::ConfiguredNetworkGroup_set_scheduler_threshold(ui request.set_network_name(network_name); ConfiguredNetworkGroup_set_scheduler_threshold_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_set_scheduler_threshold(&context, request, &reply); CHECK_GRPC_STATUS(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -754,7 +777,7 @@ hailo_status HailoRtRpcClient::ConfiguredNetworkGroup_set_scheduler_priority(uin request.set_network_name(network_name); ConfiguredNetworkGroup_set_scheduler_priority_Reply reply; - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_set_scheduler_priority(&context, request, &reply); CHECK_GRPC_STATUS(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -768,10 +791,13 @@ Expected HailoRtRpcClient::ConfiguredNetworkGroup_get_ ConfiguredNetworkGroup_get_latency_measurement_Reply reply; request.set_handle(handle); request.set_network_name(network_name); - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_get_latency_measurement(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); + if (HAILO_NOT_AVAILABLE == reply.status()) { + return make_unexpected(HAILO_NOT_AVAILABLE); + } CHECK_SUCCESS_AS_EXPECTED(static_cast(reply.status())); LatencyMeasurementResult result{ std::chrono::nanoseconds(reply.avg_hw_latency()) @@ -784,7 +810,7 @@ Expected HailoRtRpcClient::ConfiguredNetworkGroup_is_multi_context(uint32_ ConfiguredNetworkGroup_is_multi_context_Request request; ConfiguredNetworkGroup_is_multi_context_Reply reply; request.set_handle(handle); - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_is_multi_context(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -797,7 +823,7 @@ Expected HailoRtRpcClient::ConfiguredNetworkGroup_get_co ConfiguredNetworkGroup_get_config_params_Request request; ConfiguredNetworkGroup_get_config_params_Reply reply; request.set_handle(handle); - grpc::ClientContext context; + ClientContextWithTimeout context; grpc::Status status = m_stub->ConfiguredNetworkGroup_get_config_params(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); assert(reply.status() < HAILO_STATUS_COUNT); @@ -832,12 +858,65 @@ Expected HailoRtRpcClient::ConfiguredNetworkGroup_get_co return network_configure_params; } +Expected> HailoRtRpcClient::ConfiguredNetworkGroup_get_sorted_output_names(uint32_t handle) +{ + ConfiguredNetworkGroup_get_sorted_output_names_Request request; + ConfiguredNetworkGroup_get_sorted_output_names_Reply reply; + request.set_handle(handle); + ClientContextWithTimeout context; + grpc::Status status = m_stub->ConfiguredNetworkGroup_get_sorted_output_names(&context, request, &reply); + CHECK_GRPC_STATUS_AS_EXPECTED(status); + assert(reply.status() < HAILO_STATUS_COUNT); + CHECK_SUCCESS_AS_EXPECTED(static_cast(reply.status())); + std::vector result; + for (auto &name : reply.sorted_output_names()) { + result.push_back(name); + } + return result; +} + +Expected> HailoRtRpcClient::ConfiguredNetworkGroup_get_stream_names_from_vstream_name(uint32_t handle, const std::string &vstream_name) +{ + ConfiguredNetworkGroup_get_stream_names_from_vstream_name_Request request; + ConfiguredNetworkGroup_get_stream_names_from_vstream_name_Reply reply; + request.set_handle(handle); + request.set_vstream_name(vstream_name); + ClientContextWithTimeout context; + grpc::Status status = m_stub->ConfiguredNetworkGroup_get_stream_names_from_vstream_name(&context, request, &reply); + CHECK_GRPC_STATUS_AS_EXPECTED(status); + assert(reply.status() < HAILO_STATUS_COUNT); + CHECK_SUCCESS_AS_EXPECTED(static_cast(reply.status())); + std::vector result; + for (auto &name : reply.streams_names()) { + result.push_back(name); + } + return result; +} + +Expected> HailoRtRpcClient::ConfiguredNetworkGroup_get_vstream_names_from_stream_name(uint32_t handle, const std::string &stream_name) +{ + ConfiguredNetworkGroup_get_vstream_names_from_stream_name_Request request; + ConfiguredNetworkGroup_get_vstream_names_from_stream_name_Reply reply; + request.set_handle(handle); + request.set_stream_name(stream_name); + ClientContextWithTimeout context; + grpc::Status status = m_stub->ConfiguredNetworkGroup_get_vstream_names_from_stream_name(&context, request, &reply); + CHECK_GRPC_STATUS_AS_EXPECTED(status); + assert(reply.status() < HAILO_STATUS_COUNT); + CHECK_SUCCESS_AS_EXPECTED(static_cast(reply.status())); + std::vector result; + for (auto &name : reply.vstreams_names()) { + result.push_back(name); + } + return result; +} + hailo_status HailoRtRpcClient::InputVStream_write(uint32_t handle, const MemoryView &buffer) { InputVStream_write_Request request; request.set_handle(handle); request.set_data(buffer.data(), buffer.size()); - grpc::ClientContext context; + ClientContextWithTimeout context; InputVStream_write_Reply reply; grpc::Status status = m_stub->InputVStream_write(&context, request, &reply); CHECK_GRPC_STATUS(status); @@ -854,7 +933,7 @@ hailo_status HailoRtRpcClient::OutputVStream_read(uint32_t handle, MemoryView bu OutputVStream_read_Request request; request.set_handle(handle); request.set_size(static_cast(buffer.size())); - grpc::ClientContext context; + ClientContextWithTimeout context; OutputVStream_read_Reply reply; grpc::Status status = m_stub->OutputVStream_read(&context, request, &reply); CHECK_GRPC_STATUS(status); @@ -871,7 +950,7 @@ Expected HailoRtRpcClient::InputVStream_get_frame_size(uint32_t handle) { VStream_get_frame_size_Request request; request.set_handle(handle); - grpc::ClientContext context; + ClientContextWithTimeout context; VStream_get_frame_size_Reply reply; grpc::Status status = m_stub->InputVStream_get_frame_size(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); @@ -884,7 +963,7 @@ Expected HailoRtRpcClient::OutputVStream_get_frame_size(uint32_t handle) { VStream_get_frame_size_Request request; request.set_handle(handle); - grpc::ClientContext context; + ClientContextWithTimeout context; VStream_get_frame_size_Reply reply; grpc::Status status = m_stub->OutputVStream_get_frame_size(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); @@ -897,7 +976,7 @@ hailo_status HailoRtRpcClient::InputVStream_flush(uint32_t handle) { InputVStream_flush_Request request; request.set_handle(handle); - grpc::ClientContext context; + ClientContextWithTimeout context; InputVStream_flush_Reply reply; grpc::Status status = m_stub->InputVStream_flush(&context, request, &reply); CHECK_GRPC_STATUS(status); @@ -909,7 +988,7 @@ Expected HailoRtRpcClient::InputVStream_name(uint32_t handle) { VStream_name_Request request; request.set_handle(handle); - grpc::ClientContext context; + ClientContextWithTimeout context; VStream_name_Reply reply; grpc::Status status = m_stub->InputVStream_name(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); @@ -923,7 +1002,7 @@ Expected HailoRtRpcClient::OutputVStream_name(uint32_t handle) { VStream_name_Request request; request.set_handle(handle); - grpc::ClientContext context; + ClientContextWithTimeout context; VStream_name_Reply reply; grpc::Status status = m_stub->OutputVStream_name(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); @@ -937,7 +1016,7 @@ Expected HailoRtRpcClient::InputVStream_network_name(uint32_t handl { VStream_network_name_Request request; request.set_handle(handle); - grpc::ClientContext context; + ClientContextWithTimeout context; VStream_network_name_Reply reply; grpc::Status status = m_stub->InputVStream_network_name(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); @@ -951,7 +1030,7 @@ Expected HailoRtRpcClient::OutputVStream_network_name(uint32_t hand { VStream_network_name_Request request; request.set_handle(handle); - grpc::ClientContext context; + ClientContextWithTimeout context; VStream_network_name_Reply reply; grpc::Status status = m_stub->OutputVStream_network_name(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); @@ -965,7 +1044,7 @@ hailo_status HailoRtRpcClient::InputVStream_abort(uint32_t handle) { VStream_abort_Request request; request.set_handle(handle); - grpc::ClientContext context; + ClientContextWithTimeout context; VStream_abort_Reply reply; grpc::Status status = m_stub->InputVStream_abort(&context, request, &reply); CHECK_GRPC_STATUS(status); @@ -977,7 +1056,7 @@ hailo_status HailoRtRpcClient::OutputVStream_abort(uint32_t handle) { VStream_abort_Request request; request.set_handle(handle); - grpc::ClientContext context; + ClientContextWithTimeout context; VStream_abort_Reply reply; grpc::Status status = m_stub->OutputVStream_abort(&context, request, &reply); CHECK_GRPC_STATUS(status); @@ -989,7 +1068,7 @@ hailo_status HailoRtRpcClient::InputVStream_resume(uint32_t handle) { VStream_resume_Request request; request.set_handle(handle); - grpc::ClientContext context; + ClientContextWithTimeout context; VStream_resume_Reply reply; grpc::Status status = m_stub->InputVStream_resume(&context, request, &reply); CHECK_GRPC_STATUS(status); @@ -1001,7 +1080,7 @@ hailo_status HailoRtRpcClient::OutputVStream_resume(uint32_t handle) { VStream_resume_Request request; request.set_handle(handle); - grpc::ClientContext context; + ClientContextWithTimeout context; VStream_resume_Reply reply; grpc::Status status = m_stub->OutputVStream_resume(&context, request, &reply); CHECK_GRPC_STATUS(status); @@ -1009,11 +1088,59 @@ hailo_status HailoRtRpcClient::OutputVStream_resume(uint32_t handle) return static_cast(reply.status()); } +hailo_status HailoRtRpcClient::InputVStream_stop_and_clear(uint32_t handle) +{ + VStream_stop_and_clear_Request request; + request.set_handle(handle); + ClientContextWithTimeout context; + VStream_stop_and_clear_Reply reply; + grpc::Status status = m_stub->InputVStream_stop_and_clear(&context, request, &reply); + CHECK_GRPC_STATUS(status); + assert(reply.status() < HAILO_STATUS_COUNT); + return static_cast(reply.status()); +} + +hailo_status HailoRtRpcClient::OutputVStream_stop_and_clear(uint32_t handle) +{ + VStream_stop_and_clear_Request request; + request.set_handle(handle); + ClientContextWithTimeout context; + VStream_stop_and_clear_Reply reply; + grpc::Status status = m_stub->OutputVStream_stop_and_clear(&context, request, &reply); + CHECK_GRPC_STATUS(status); + assert(reply.status() < HAILO_STATUS_COUNT); + return static_cast(reply.status()); +} + +hailo_status HailoRtRpcClient::InputVStream_start_vstream(uint32_t handle) +{ + VStream_start_vstream_Request request; + request.set_handle(handle); + ClientContextWithTimeout context; + VStream_start_vstream_Reply reply; + grpc::Status status = m_stub->InputVStream_start_vstream(&context, request, &reply); + CHECK_GRPC_STATUS(status); + assert(reply.status() < HAILO_STATUS_COUNT); + return static_cast(reply.status()); +} + +hailo_status HailoRtRpcClient::OutputVStream_start_vstream(uint32_t handle) +{ + VStream_start_vstream_Request request; + request.set_handle(handle); + ClientContextWithTimeout context; + VStream_start_vstream_Reply reply; + grpc::Status status = m_stub->OutputVStream_start_vstream(&context, request, &reply); + CHECK_GRPC_STATUS(status); + assert(reply.status() < HAILO_STATUS_COUNT); + return static_cast(reply.status()); +} + Expected HailoRtRpcClient::InputVStream_get_user_buffer_format(uint32_t handle) { VStream_get_user_buffer_format_Request request; request.set_handle(handle); - grpc::ClientContext context; + ClientContextWithTimeout context; VStream_get_user_buffer_format_Reply reply; grpc::Status status = m_stub->InputVStream_get_user_buffer_format(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); @@ -1034,7 +1161,7 @@ Expected HailoRtRpcClient::OutputVStream_get_user_buffer_format( { VStream_get_user_buffer_format_Request request; request.set_handle(handle); - grpc::ClientContext context; + ClientContextWithTimeout context; VStream_get_user_buffer_format_Reply reply; grpc::Status status = m_stub->OutputVStream_get_user_buffer_format(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); @@ -1055,7 +1182,7 @@ Expected HailoRtRpcClient::InputVStream_get_info(uint32_t { VStream_get_info_Request request; request.set_handle(handle); - grpc::ClientContext context; + ClientContextWithTimeout context; VStream_get_info_Reply reply; grpc::Status status = m_stub->InputVStream_get_info(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); @@ -1068,7 +1195,7 @@ Expected HailoRtRpcClient::OutputVStream_get_info(uint32_t { VStream_get_info_Request request; request.set_handle(handle); - grpc::ClientContext context; + ClientContextWithTimeout context; VStream_get_info_Reply reply; grpc::Status status = m_stub->OutputVStream_get_info(&context, request, &reply); CHECK_GRPC_STATUS_AS_EXPECTED(status); @@ -1078,4 +1205,32 @@ Expected HailoRtRpcClient::OutputVStream_get_info(uint32_t return deserialize_vstream_info(info_proto); } +Expected HailoRtRpcClient::InputVStream_is_aborted(uint32_t handle) +{ + VStream_is_aborted_Request request; + request.set_handle(handle); + ClientContextWithTimeout context; + VStream_is_aborted_Reply reply; + grpc::Status status = m_stub->InputVStream_is_aborted(&context, request, &reply); + CHECK_GRPC_STATUS_AS_EXPECTED(status); + assert(reply.status() < HAILO_STATUS_COUNT); + CHECK_SUCCESS_AS_EXPECTED(static_cast(reply.status())); + auto is_aborted = reply.is_aborted(); + return is_aborted; +} + +Expected HailoRtRpcClient::OutputVStream_is_aborted(uint32_t handle) +{ + VStream_is_aborted_Request request; + request.set_handle(handle); + ClientContextWithTimeout context; + VStream_is_aborted_Reply reply; + grpc::Status status = m_stub->OutputVStream_is_aborted(&context, request, &reply); + CHECK_GRPC_STATUS_AS_EXPECTED(status); + assert(reply.status() < HAILO_STATUS_COUNT); + CHECK_SUCCESS_AS_EXPECTED(static_cast(reply.status())); + auto is_aborted = reply.is_aborted(); + return is_aborted; +} + } \ No newline at end of file diff --git a/hailort/libhailort/src/service/hailort_rpc_client.hpp b/hailort/libhailort/src/service/hailort_rpc_client.hpp index 5b393fd..231daa2 100644 --- a/hailort/libhailort/src/service/hailort_rpc_client.hpp +++ b/hailort/libhailort/src/service/hailort_rpc_client.hpp @@ -12,6 +12,7 @@ #include "hailo/hailort.h" #include "hailo/expected.hpp" +#include "hailo/device.hpp" #if defined(_MSC_VER) #pragma warning(push) @@ -33,6 +34,17 @@ namespace hailort { +// Higher then default-hrt-timeout so we can differentiate errors +static const std::chrono::milliseconds CONTEXT_TIMEOUT(HAILO_DEFAULT_VSTREAM_TIMEOUT_MS + 500); + +class ClientContextWithTimeout : public grpc::ClientContext { +public: + ClientContextWithTimeout() + { + set_deadline(std::chrono::system_clock::now() + CONTEXT_TIMEOUT); + } +}; + class HailoRtRpcClient final { public: HailoRtRpcClient(std::shared_ptr channel) @@ -43,13 +55,14 @@ public: Expected VDevice_create(const hailo_vdevice_params_t ¶ms, uint32_t pid); Expected VDevice_dup_handle(uint32_t pid, uint32_t handle); - hailo_status VDevice_release(uint32_t handle); + hailo_status VDevice_release(uint32_t handle, uint32_t pid); Expected> VDevice_get_physical_devices_ids(uint32_t handle); + Expected>> VDevice_get_physical_devices(uint32_t handle); Expected VDevice_get_default_streams_interface(uint32_t handle); Expected> VDevice_configure(uint32_t vdevice_handle, const Hef &hef, uint32_t pid, const NetworkGroupsParamsMap &configure_params={}); Expected ConfiguredNetworkGroup_dup_handle(uint32_t pid, uint32_t handle); - hailo_status ConfiguredNetworkGroup_release(uint32_t handle); + hailo_status ConfiguredNetworkGroup_release(uint32_t handle, uint32_t pid); Expected> ConfiguredNetworkGroup_make_input_vstream_params(uint32_t handle, bool quantized, hailo_format_type_t format_type, uint32_t timeout_ms, uint32_t queue_size, const std::string &network_name); @@ -75,15 +88,18 @@ public: Expected ConfiguredNetworkGroup_get_latency_measurement(uint32_t handle, const std::string &network_name); Expected ConfiguredNetworkGroup_is_multi_context(uint32_t handle); Expected ConfiguredNetworkGroup_get_config_params(uint32_t handle); + Expected> ConfiguredNetworkGroup_get_sorted_output_names(uint32_t handle); + Expected> ConfiguredNetworkGroup_get_stream_names_from_vstream_name(uint32_t handle, const std::string &vstream_name); + Expected> ConfiguredNetworkGroup_get_vstream_names_from_stream_name(uint32_t handle, const std::string &stream_name); Expected> InputVStreams_create(uint32_t net_group_handle, const std::map &inputs_params, uint32_t pid); Expected InputVStream_dup_handle(uint32_t pid, uint32_t handle); Expected OutputVStream_dup_handle(uint32_t pid, uint32_t handle); - hailo_status InputVStream_release(uint32_t handle); + hailo_status InputVStream_release(uint32_t handle, uint32_t pid); Expected> OutputVStreams_create(uint32_t net_group_handle, const std::map &output_params, uint32_t pid); - hailo_status OutputVStream_release(uint32_t handle); + hailo_status OutputVStream_release(uint32_t handle, uint32_t pid); hailo_status InputVStream_write(uint32_t handle, const MemoryView &buffer); hailo_status OutputVStream_read(uint32_t handle, MemoryView buffer); Expected InputVStream_get_frame_size(uint32_t handle); @@ -101,6 +117,10 @@ public: hailo_status OutputVStream_abort(uint32_t handle); hailo_status InputVStream_resume(uint32_t handle); hailo_status OutputVStream_resume(uint32_t handle); + hailo_status InputVStream_stop_and_clear(uint32_t handle); + hailo_status OutputVStream_stop_and_clear(uint32_t handle); + hailo_status InputVStream_start_vstream(uint32_t handle); + hailo_status OutputVStream_start_vstream(uint32_t handle); Expected InputVStream_get_user_buffer_format(uint32_t handle); Expected OutputVStream_get_user_buffer_format(uint32_t handle); @@ -108,6 +128,9 @@ public: Expected InputVStream_get_info(uint32_t handle); Expected OutputVStream_get_info(uint32_t handle); + Expected InputVStream_is_aborted(uint32_t handle); + Expected OutputVStream_is_aborted(uint32_t handle); + private: std::unique_ptr m_stub; }; diff --git a/hailort/libhailort/src/service/network_group_client.cpp b/hailort/libhailort/src/service/network_group_client.cpp index bb2db9c..0e2bf66 100644 --- a/hailort/libhailort/src/service/network_group_client.cpp +++ b/hailort/libhailort/src/service/network_group_client.cpp @@ -34,9 +34,25 @@ ConfiguredNetworkGroupClient::ConfiguredNetworkGroupClient(std::unique_ptr> ConfiguredNetworkGroupClient::duplicate_network_group_client(uint32_t handle, + const std::string &network_group_name) +{ + auto duplicated_net_group = std::shared_ptr(new (std::nothrow) ConfiguredNetworkGroupClient(handle, network_group_name)); + CHECK_ARG_NOT_NULL_AS_EXPECTED(duplicated_net_group); + auto status = duplicated_net_group->after_fork_in_child(); + CHECK_SUCCESS_AS_EXPECTED(status); + + return duplicated_net_group; +} + ConfiguredNetworkGroupClient::~ConfiguredNetworkGroupClient() { - auto reply = m_client->ConfiguredNetworkGroup_release(m_handle); + auto reply = m_client->ConfiguredNetworkGroup_release(m_handle, OsUtils::get_curr_pid()); if (reply != HAILO_SUCCESS) { LOGGER__CRITICAL("ConfiguredNetworkGroup_release failed with status: {}", reply); } @@ -65,9 +81,11 @@ hailo_status ConfiguredNetworkGroupClient::after_fork_in_child() { auto status = create_client(); CHECK_SUCCESS(status); + auto expected_dup_handle = m_client->ConfiguredNetworkGroup_dup_handle(OsUtils::get_curr_pid(), m_handle); CHECK_EXPECTED_AS_STATUS(expected_dup_handle); m_handle = expected_dup_handle.value(); + return HAILO_SUCCESS; } @@ -75,7 +93,7 @@ Expected> ConfiguredNetworkGroupClient::a const hailo_activate_network_group_params_t &/* network_group_params */) { LOGGER__WARNING("ConfiguredNetworkGroup::activate function is not supported when using multi-process service or HailoRT Scheduler."); - return make_unexpected(HAILO_NOT_IMPLEMENTED); + return make_unexpected(HAILO_INVALID_OPERATION); } /* Network group base functions */ @@ -160,7 +178,7 @@ Expected ConfiguredNetworkGroupClient::get_output_ hailo_status ConfiguredNetworkGroupClient::wait_for_activation(const std::chrono::milliseconds&) { LOGGER__WARNING("ConfiguredNetworkGroup::wait_for_activation function is not supported when using multi-process service or HailoRT Scheduler."); - return HAILO_NOT_IMPLEMENTED; + return HAILO_INVALID_OPERATION; } Expected>> ConfiguredNetworkGroupClient::get_output_vstream_groups() @@ -266,6 +284,12 @@ bool ConfiguredNetworkGroupClient::is_multi_context() const return reply.value(); } +Expected ConfiguredNetworkGroupClient::run_hw_infer_estimator() +{ + LOGGER__ERROR("ConfiguredNetworkGroupClient::run_hw_infer_estimator function is not supported when using multi-process service."); + return make_unexpected(HAILO_NOT_IMPLEMENTED); +} + const ConfigureNetworkParams ConfiguredNetworkGroupClient::get_config_params() const { auto reply = m_client->ConfiguredNetworkGroup_get_config_params(m_handle); @@ -276,6 +300,21 @@ const ConfigureNetworkParams ConfiguredNetworkGroupClient::get_config_params() c return reply.value(); } +Expected> ConfiguredNetworkGroupClient::get_sorted_output_names() +{ + return m_client->ConfiguredNetworkGroup_get_sorted_output_names(m_handle); +} + +Expected> ConfiguredNetworkGroupClient::get_stream_names_from_vstream_name(const std::string &vstream_name) +{ + return m_client->ConfiguredNetworkGroup_get_stream_names_from_vstream_name(m_handle, vstream_name); +} + +Expected> ConfiguredNetworkGroupClient::get_vstream_names_from_stream_name(const std::string &stream_name) +{ + return m_client->ConfiguredNetworkGroup_get_vstream_names_from_stream_name(m_handle, stream_name); +} + Expected> ConfiguredNetworkGroupClient::create_input_vstreams(const std::map &inputs_params) { auto reply = m_client->InputVStreams_create(m_handle, inputs_params, OsUtils::get_curr_pid()); diff --git a/hailort/libhailort/src/service/rpc_client_utils.hpp b/hailort/libhailort/src/service/rpc_client_utils.hpp index 965be9c..99d8444 100644 --- a/hailort/libhailort/src/service/rpc_client_utils.hpp +++ b/hailort/libhailort/src/service/rpc_client_utils.hpp @@ -3,7 +3,7 @@ * Distributed under the MIT license (https://opensource.org/licenses/MIT) **/ /** - * @file hailort_common.hpp + * @file rpc_client_utils.hpp * @brief Utility functions for rpc client communication **/ @@ -34,10 +34,14 @@ public: return instance; } - HailoRtRpcClientUtils() - : m_mutex(std::make_shared()) - , m_forking(false) - {} + HailoRtRpcClientUtils() : + m_mutex(std::make_shared()) + { + auto status = init_keep_alive_shutdown_event(); + if (HAILO_SUCCESS != status) { + LOGGER__ERROR("Failed to initialize RPC Client's keep-alive shutdown event with status {}", status); + } + } static Expected> create_client() { @@ -55,7 +59,7 @@ public: // Create client auto channel = grpc::CreateChannel(hailort::HAILORT_SERVICE_DEFAULT_ADDR, grpc::InsecureChannelCredentials()); auto client = make_unique_nothrow(channel); - CHECK(client != nullptr, HAILO_OUT_OF_HOST_MEMORY); + CHECK_NOT_NULL(client, HAILO_OUT_OF_HOST_MEMORY); // Check service version auto reply = client->get_service_version(); @@ -78,45 +82,39 @@ public: m_pid = OsUtils::get_curr_pid(); // Trigger client keep-alive - m_keep_alive_thread = make_unique_nothrow>([this] () { - return this->keep_alive(); - }); - CHECK(nullptr != m_keep_alive_thread, HAILO_OUT_OF_HOST_MEMORY); + status = start_keep_alive_thread(); + CHECK_SUCCESS(status); + m_initialized = true; } return HAILO_SUCCESS; } - hailo_status before_fork() + void before_fork() { - m_forking = true; - return m_keep_alive_thread->get(); + stop_keep_alive_thread(); } hailo_status after_fork_in_parent() { - m_forking = false; + m_keep_alive_shutdown_event->reset(); std::unique_lock lock(*m_mutex); if (m_initialized) { - // Trigger client keep-alive - m_keep_alive_thread = make_unique_nothrow>([this] () { - return this->keep_alive(); - }); + return start_keep_alive_thread(); } return HAILO_SUCCESS; } hailo_status after_fork_in_child() { - m_forking = false; m_mutex = std::make_shared(); + auto status = init_keep_alive_shutdown_event(); + CHECK_SUCCESS(status); + std::unique_lock lock(*m_mutex); if (m_initialized) { m_pid = OsUtils::get_curr_pid(); - // Trigger client keep-alive - m_keep_alive_thread = make_unique_nothrow>([this] () { - return this->keep_alive(); - }); + return start_keep_alive_thread(); } return HAILO_SUCCESS; } @@ -124,27 +122,59 @@ public: private: ~HailoRtRpcClientUtils() { - m_keep_alive_thread.release(); + stop_keep_alive_thread(); + } + + void stop_keep_alive_thread() + { + if (m_keep_alive_shutdown_event) { + (void)m_keep_alive_shutdown_event->signal(); + } + + m_keep_alive_thread.reset(); + } + + hailo_status start_keep_alive_thread() + { + m_keep_alive_thread = make_unique_nothrow>("SVC_KEEPALIVE", [this] () { + return this->keep_alive(); + }); + CHECK_NOT_NULL(m_keep_alive_thread, HAILO_OUT_OF_HOST_MEMORY); + return HAILO_SUCCESS; } hailo_status keep_alive() { auto channel = grpc::CreateChannel(hailort::HAILORT_SERVICE_DEFAULT_ADDR, grpc::InsecureChannelCredentials()); auto client = make_unique_nothrow(channel); - CHECK(client != nullptr, HAILO_OUT_OF_HOST_MEMORY); - while (!m_forking) { + CHECK_NOT_NULL(client, HAILO_OUT_OF_HOST_MEMORY); + + while (true) { + auto shutdown_status = m_keep_alive_shutdown_event->wait(hailort::HAILO_KEEPALIVE_INTERVAL / 2); + if (HAILO_TIMEOUT != shutdown_status) { + // shutdown event is signal (or we have another error) + return shutdown_status; + } + + // keep alive interval auto status = client->client_keep_alive(m_pid); CHECK_SUCCESS(status); - std::this_thread::sleep_for(hailort::HAILO_KEEPALIVE_INTERVAL / 2); } + } + + hailo_status init_keep_alive_shutdown_event() + { + m_keep_alive_shutdown_event = Event::create_shared(Event::State::not_signalled); + CHECK(nullptr != m_keep_alive_shutdown_event, HAILO_OUT_OF_HOST_MEMORY); + return HAILO_SUCCESS; } std::shared_ptr m_mutex; AsyncThreadPtr m_keep_alive_thread; bool m_initialized = false; - std::atomic m_forking; uint32_t m_pid; + EventPtr m_keep_alive_shutdown_event; }; } /* namespace hailort */ diff --git a/hailort/libhailort/src/stream_common/CMakeLists.txt b/hailort/libhailort/src/stream_common/CMakeLists.txt index 001d29e..cacbbb2 100644 --- a/hailort/libhailort/src/stream_common/CMakeLists.txt +++ b/hailort/libhailort/src/stream_common/CMakeLists.txt @@ -3,6 +3,7 @@ cmake_minimum_required(VERSION 3.0.0) set(SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/stream.cpp ${CMAKE_CURRENT_SOURCE_DIR}/stream_internal.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/nms_stream_reader.cpp ) set(HAILORT_CPP_SOURCES ${HAILORT_CPP_SOURCES} ${SRC_FILES} PARENT_SCOPE) diff --git a/hailort/libhailort/src/stream_common/async_common.hpp b/hailort/libhailort/src/stream_common/async_common.hpp new file mode 100644 index 0000000..31c39c8 --- /dev/null +++ b/hailort/libhailort/src/stream_common/async_common.hpp @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file async_common.hpp + * @brief Common types/functions for async api + **/ + +#ifndef _HAILO_ASYNC_COMMON_HPP_ +#define _HAILO_ASYNC_COMMON_HPP_ + +#include "hailo/stream.hpp" + +namespace hailort +{ + +// Internal function, wrapper to the user callbacks, accepts the callback status as an argument. +using InternalTransferDoneCallback = std::function; + +struct TransferRequest { + MemoryView buffer; + InternalTransferDoneCallback callback; + + // Optional pre-mapped user buffer. If set, mapped_buffer must be the same as the "buffer" + BufferPtr mapped_buffer = nullptr; +}; + +} /* namespace hailort */ + +#endif /* _HAILO_ASYNC_COMMON_HPP_ */ diff --git a/hailort/libhailort/src/stream_common/nms_stream_reader.cpp b/hailort/libhailort/src/stream_common/nms_stream_reader.cpp new file mode 100644 index 0000000..618be44 --- /dev/null +++ b/hailort/libhailort/src/stream_common/nms_stream_reader.cpp @@ -0,0 +1,300 @@ +/** + * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file nms_stream_reader.cpp + * @brief static class that helps receive and read the nms ouput stream according to the different burst mode, type and size. + * + * Explanation of state machine and logic: + * This class supports the following 5 nms cases: + * 1) Hailo-8 bbox mode (non burst mode) + * 2) Hailo-15 bbox mode + * 3) Hailo-8 Burst mode + * 4) Hailo-15 Burst per class mode + * 5) Hailo15 Burst per frame mode + * + * Lets explain each mode and the state machine of each mode: + * 1)-2) Hailo-8 bbox mode / Hailo-15 bbox mode - both work the same - they read bbox bbox from the nms core until a delimeter comes + * and expect to read the amount of delimeters as the same amount of number of classes (times num chunks if more than one chunk per frame). + * + * 3) Hailo8 Burst mode - Hailo 8 burst mode reads bursts in the size of burst-size and expects each burst to be made of x bboxes and + * then a delimeter and padding until the end of the burst - essentially what the state machine does here is read until the first delimeter + * and then expect padding until end of burts (in release mode we dont check that the rest of burst is padding and + * just go onto the next burst but in debug we validate that rest of burst is padding). NOTE: in Hailo-8 delimeter value and + * padding value are both 0xFFFFFFFFFFFFFFFF so essentially we read until first delimeter - and the every following delimeter + * in burst is padding. This mode also supports interrupt per frame - assuming burst size received from SDK is larger than max bboxes + 1 (for delimeter) + * we know there will be one burst per class and hence the output size will be num classes * burst size and we enable one interrupt per frame. + * + * 4) Hailo15 Burst per class mode - Hailo-15 Burst per class mode reads bursts in the size of burst size and expects the following order. + * x bboxes , followed by a delimeter, followed by an image delimeter, followed by padding until the end of the burst. The bbboxes, delimeter + * and image delimeter can all be in different bursts - so essentially the way the state machine works is the following: we read burst burst, + * in each burst we iterate over the bboxes until we find a delimeter - once after that we know how many bboxes there were for that class, + * and then we expect to see a following image delimeter after the delimeter, once we read the image delimeter we expect padding until the end of the + * burst (which we ensure in debug but not in release). NOTE: if a burst ends on a delimeter we need to read the next burst to get the image delimeter + * even in the case where the amount of delimeters we read is equal to the amount of classes - otherwise there is data still in the core + * that was not emptied and will be read as part of the next frame. This mode also supports interrupt per frame - assuming burst size received from SDK + * is larger than max bboxes + 2 (for image delimeter and delimeter) we know there will be one burst per class and hence the output size will be + * num classes * burst size and we enable one interrupt per frame. + * + * 5) Hailo15 Burst per frame mode - Hailo-15 Burst per frame mode reads bursts in the size of burst size and expects the following order. + * x bboxes , followed by a delimeter, for all the classes until the last class where the last delimeter should be followed by an image delimeter + * and padding until the end of the burst. The state machine works in the following way - we read burst burst, and for each time we reach a delimeter + * we save the amount of bboxes that were read for that class and keep reading the burst. NOTE: this is the only mode where there can be multiple + * delimeters per burst. Once we read the last delimeter (which we know from number classes) - we ensure there is a following image delimeter (which again + * can be in the following burst) and then assume the rest of the burst is padding (and in debug we verify that). NOTE: currently this mode is not + * supported in the sdk. + * + **/ + +#include "hailo/hailort.h" +#include "hailo/expected.hpp" +#include "stream_common/nms_stream_reader.hpp" +#include "src/hef/layer_info.hpp" + +namespace hailort +{ + +static void finish_reading_burst_update_state(NMSBurstState *burst_state, bool *can_stop_reading_burst, size_t *burst_index) +{ + *burst_state = NMSBurstState::NMS_BURST_STATE_WAITING_FOR_DELIMETER; + *burst_index = (*burst_index + 1); + *can_stop_reading_burst = true; +} + +// Function that implements the state machine of the 3 different nms burst modes based on the value of the current bbox and the current state. +hailo_status NMSStreamReader::advance_state_machine(NMSBurstState *burst_state, const uint64_t current_bbox, + const hailo_nms_burst_type_t burst_type, const uint32_t num_classes, size_t *num_delimeters_received, + bool *can_stop_reading_burst, const size_t burst_offset, const size_t burst_size, size_t *burst_index) +{ + switch(current_bbox) { + // This is also case for Hailo8 padding - seeing as they are same value + case NMS_DELIMITER: + { + // If we are in hailo8 per class mode - if we are in state waiting for delimeter - we received delimeter + // otherwise we must be in state waiting for padding - in which case we received padding. + if (HAILO_BURST_TYPE_H8_PER_CLASS == burst_type) { + CHECK_IN_DEBUG((NMSBurstState::NMS_BURST_STATE_WAITING_FOR_DELIMETER == (*burst_state)) || + (NMSBurstState::NMS_BURST_STATE_WAITING_FOR_PADDING == (*burst_state)), HAILO_NMS_BURST_INVALID_DATA, + "Invalid state, H8 NMS burst cannot receive delimeter while in state {}", (*burst_state)); + // To differentiate from H8 padding - where we should not increment amount of delimeters found + if ((*burst_state) == NMSBurstState::NMS_BURST_STATE_WAITING_FOR_DELIMETER) { + (*num_delimeters_received)++; + } +#ifdef NDEBUG + // In hailo8 burst mode - if is in state waiting for delimeter and got delimeter - rest will be padding and can skip + if ((*burst_state) == NMSBurstState::NMS_BURST_STATE_WAITING_FOR_DELIMETER) { + finish_reading_burst_update_state(burst_state, can_stop_reading_burst, burst_index); + break; + } +#endif + // In hailo8 mode after delimeter we expect padding until end of burst - seeing as h8 padding is same value + // Weather was in state wait for delimeter or state wait for padding - will always go to wait for padding until end of burst + *burst_state = NMSBurstState::NMS_BURST_STATE_WAITING_FOR_PADDING; + if (burst_offset == (burst_size - sizeof(current_bbox))) { + finish_reading_burst_update_state(burst_state, can_stop_reading_burst, burst_index); + } + break; + + } else if (HAILO_BURST_TYPE_H15_PER_CLASS == burst_type) { + CHECK_IN_DEBUG(NMSBurstState::NMS_BURST_STATE_WAITING_FOR_DELIMETER == (*burst_state), HAILO_NMS_BURST_INVALID_DATA, + "Invalid state, H15 Per class NMS burst cannot receive delimeter while in state {}", (*burst_state)); + (*num_delimeters_received)++; + *burst_state = NMSBurstState::NMS_BURST_STATE_WAITING_FOR_IMAGE_DELIMETER; + } else { + CHECK_IN_DEBUG(NMSBurstState::NMS_BURST_STATE_WAITING_FOR_DELIMETER == (*burst_state), HAILO_NMS_BURST_INVALID_DATA, + "Invalid state, H15 Per Frame NMS burst cannot receive delimeter while in state {}", (*burst_state)); + // in hailo15 per frame - if number of delimeter is same as num classes - we expect image delimeter next + // otherwise expect another delimeter + (*num_delimeters_received)++; + if (num_classes == (*num_delimeters_received)) { + *burst_state = NMSBurstState::NMS_BURST_STATE_WAITING_FOR_IMAGE_DELIMETER; + } + } + break; + } + + case NMS_IMAGE_DELIMITER: + { + CHECK_IN_DEBUG(HAILO_BURST_TYPE_H8_PER_CLASS != burst_type, HAILO_NMS_BURST_INVALID_DATA, + "Invalid state, H8 NMS burst cannot receive image delimeter"); + + CHECK_IN_DEBUG(NMSBurstState::NMS_BURST_STATE_WAITING_FOR_IMAGE_DELIMETER == (*burst_state), HAILO_NMS_BURST_INVALID_DATA, + "Invalid state, H15 NMS burst cannot receive image delimeter in state {}", (*burst_state)); + + // in both hailo15 per class and per frame - when receiving image delimeter we move to expecting padding + *burst_state = NMSBurstState::NMS_BURST_STATE_WAITING_FOR_PADDING; + +#ifdef NDEBUG + finish_reading_burst_update_state(burst_state, can_stop_reading_burst, burst_index); +#endif // NDEBUG + break; + } + + case NMS_H15_PADDING: + { + if ((HAILO_BURST_TYPE_H15_PER_CLASS == burst_type) || (HAILO_BURST_TYPE_H15_PER_FRAME == burst_type)) { + CHECK_IN_DEBUG(NMSBurstState::NMS_BURST_STATE_WAITING_FOR_PADDING == (*burst_state), HAILO_NMS_BURST_INVALID_DATA, + "Invalid state, H15 NMS burst cannot receive padding in state {}", (*burst_state)); + } + // In case of padding next state is wait for padding unless it is last padding of burst - then next state will be + // Wait for delimeter - will only get to this stage in debug - in release once image delimeter is read we ignore rest of + // burst seeing as it must be padding + if (burst_offset == (burst_size - sizeof(current_bbox))) { + finish_reading_burst_update_state(burst_state, can_stop_reading_burst, burst_index); + } + break; + } + } + + return HAILO_SUCCESS; +} + +hailo_status NMSStreamReader::read_nms_bbox_mode(OutputStream &stream, void *buffer, size_t offset) +{ + const uint32_t num_classes = stream.get_info().nms_info.number_of_classes; + const uint32_t chunks_per_frame = stream.get_info().nms_info.chunks_per_frame; + const size_t bbox_size = stream.get_info().nms_info.bbox_size; + + for (size_t delimeters_found = 0; delimeters_found < (num_classes * chunks_per_frame); delimeters_found++) { + nms_bbox_counter_t class_bboxes_count = 0; + nms_bbox_counter_t* class_bboxes_count_ptr = (nms_bbox_counter_t*)(reinterpret_cast(buffer) + offset); + offset += sizeof(*class_bboxes_count_ptr); + + while (true) { + MemoryView buffer_view(static_cast(buffer) + offset, bbox_size); + auto status = stream.read_impl(buffer_view); + if ((HAILO_STREAM_ABORTED_BY_USER == status) || + ((HAILO_STREAM_NOT_ACTIVATED == status))) { + return status; + } + CHECK_SUCCESS(status, "Failed reading nms bbox"); + const uint64_t current_bbox = *(uint64_t*)((uint8_t*)buffer + offset); + + if (NMS_IMAGE_DELIMITER == current_bbox) { + continue; + } + + if (NMS_DELIMITER == current_bbox) { + break; + } + + class_bboxes_count++; + CHECK_IN_DEBUG(class_bboxes_count <= stream.get_info().nms_info.max_bboxes_per_class, HAILO_INTERNAL_FAILURE, + "Data read from the device for the current class was size {}, max size is {}", class_bboxes_count, + stream.get_info().nms_info.max_bboxes_per_class); + offset += bbox_size; + } + + *class_bboxes_count_ptr = class_bboxes_count; + } + + return HAILO_SUCCESS; +} + +hailo_status NMSStreamReader::read_nms_burst_mode(OutputStream &stream, void *buffer, size_t offset, size_t buffer_size) +{ + NMSBurstState burst_state = NMSBurstState::NMS_BURST_STATE_WAITING_FOR_DELIMETER; + const uint32_t bbox_size = stream.get_info().nms_info.bbox_size; + const size_t burst_size = stream.get_layer_info().nms_info.burst_size * bbox_size; + const hailo_nms_burst_type_t burst_type = stream.get_layer_info().nms_info.burst_type; + const auto num_expected_delimeters = stream.get_info().nms_info.chunks_per_frame * stream.get_info().nms_info.number_of_classes; + // Transfer size if affected from if working in interrupt per burst or interrupt per frame + const size_t transfer_size = LayerInfoUtils::get_nms_layer_transfer_size(stream.get_layer_info()); + const bool is_interrupt_per_frame = (transfer_size > burst_size); + + CHECK(bbox_size == sizeof(uint64_t), HAILO_INTERNAL_FAILURE, + "Invalid Bbox size, must be 8 bytes received {}", bbox_size); + + CHECK(transfer_size <= buffer_size, HAILO_INTERNAL_FAILURE, "Invalid transfer size {}, Cannot be larger than buffer {}", + transfer_size, buffer_size); + + // Start writing bboxes at offset sizeof(nms_bbox_counter_t) - because the first sizeof(nms_bbox_counter_t) will be + // used to write amount of bboxes found for class 0 etc... + nms_bbox_counter_t class_bboxes_count = 0; + nms_bbox_counter_t* class_bboxes_count_ptr = (nms_bbox_counter_t*)(reinterpret_cast(buffer) + offset); + offset += sizeof(nms_bbox_counter_t); + + // Counter of number of delimeters found in frame + size_t delimeters_found = 0; + size_t burst_index = 0; + uint8_t *start_index_of_burst_in_buffer = nullptr; + while ((delimeters_found < num_expected_delimeters) || (NMSBurstState::NMS_BURST_STATE_WAITING_FOR_IMAGE_DELIMETER == burst_state)) { + // In interrupt per frame we read whole frame once (in first iteration) - then don't read in following loop iterations + // delimeters_found will always be 0 in first iteration - and in interrupt_per_frame will always be larger in following iterations + if (!is_interrupt_per_frame || (0 == delimeters_found)) { + assert(offset + transfer_size <= buffer_size); + start_index_of_burst_in_buffer = static_cast(buffer) + offset; + MemoryView buffer_view(start_index_of_burst_in_buffer, transfer_size); + auto status = stream.read_impl(buffer_view); + if ((HAILO_STREAM_ABORTED_BY_USER == status) || ((HAILO_STREAM_NOT_ACTIVATED == status))) { + return status; + } + CHECK_SUCCESS(status, "Failed reading nms burst"); + } + + // Flag that marks if we can stop reading burst and continue to next burst + bool can_stop_reading_burst = false; + // Iterate through burst and copy relevant data to user buffer + for (size_t burst_offset = 0; burst_offset < burst_size; burst_offset += bbox_size) { + uint64_t current_bbox = 0; + if (is_interrupt_per_frame) { + assert((burst_index * burst_size) + burst_offset < transfer_size); + current_bbox = *(uint64_t*)((uint8_t*)start_index_of_burst_in_buffer + (burst_index * burst_size) + burst_offset); + } else { + current_bbox = *(uint64_t*)((uint8_t*)start_index_of_burst_in_buffer + burst_offset); + } + + // If read delimeter - fill in information about num of bboxes found for the class (we also make sure that + // It is in state NMS_BURST_STATE_WAITING_FOR_DELIMETER because in hailo8 padding is same value) + if ((NMS_DELIMITER == current_bbox) && (NMSBurstState::NMS_BURST_STATE_WAITING_FOR_DELIMETER == burst_state)) { + *class_bboxes_count_ptr = class_bboxes_count; + class_bboxes_count_ptr = (nms_bbox_counter_t*)(reinterpret_cast(buffer) + offset); + class_bboxes_count = 0; + offset += sizeof(nms_bbox_counter_t); + } + + // Received delimeter can stop reading burst because rest of burst is image delimeter then padding + if ((NMS_DELIMITER == current_bbox) || (NMS_IMAGE_DELIMITER == current_bbox) || (NMS_H15_PADDING == current_bbox)) { + auto status = advance_state_machine(&burst_state, current_bbox, burst_type, stream.get_info().nms_info.number_of_classes, + &delimeters_found, &can_stop_reading_burst, burst_offset, burst_size, &burst_index); + CHECK_SUCCESS(status); + + if (can_stop_reading_burst) { + break; + } + continue; + } + + class_bboxes_count++; + CHECK_IN_DEBUG(class_bboxes_count <= stream.get_info().nms_info.max_bboxes_per_class, HAILO_INTERNAL_FAILURE, + "Data read from the device for the current class was size {}, max size is {}", class_bboxes_count, + stream.get_info().nms_info.max_bboxes_per_class); + + // Copy bbox to correct location in buffer + memcpy((static_cast(buffer) + offset), ¤t_bbox, sizeof(current_bbox)); + offset += bbox_size; + } + } + + return HAILO_SUCCESS; +} + +hailo_status NMSStreamReader::read_nms(OutputStream &stream, void *buffer, size_t offset, size_t size) +{ + hailo_status status = HAILO_UNINITIALIZED; + const bool burst_mode = (HAILO_BURST_TYPE_NO_BURST != stream.get_layer_info().nms_info.burst_type); + if (burst_mode) { + status = NMSStreamReader::read_nms_burst_mode(stream, buffer, offset, size); + } else { + status = NMSStreamReader::read_nms_bbox_mode(stream, buffer, offset); + } + if ((HAILO_STREAM_ABORTED_BY_USER == status) || (HAILO_STREAM_NOT_ACTIVATED == status)) { + return status; + } + CHECK_SUCCESS(status, "Failed reading nms"); + + return HAILO_SUCCESS; +} + +} /* namespace hailort */ \ No newline at end of file diff --git a/hailort/libhailort/src/stream_common/nms_stream_reader.hpp b/hailort/libhailort/src/stream_common/nms_stream_reader.hpp new file mode 100644 index 0000000..db5139c --- /dev/null +++ b/hailort/libhailort/src/stream_common/nms_stream_reader.hpp @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file nms_stream_reader.hpp + * @brief static class that helps receives and reads the nms ouput stream according to the differnet burst mode, type and size. + * + * For explanation on the different burst modes and types and state machine and logic of the class please check out the cpp. + * + **/ + +#ifndef _NMS_STREAM_READER_HPP_ +#define _NMS_STREAM_READER_HPP_ + +#include "hailo/stream.hpp" +#include "common/utils.hpp" +#include "hailo/hailort_common.hpp" + +namespace hailort +{ + +static constexpr uint32_t MAX_NMS_BURST_SIZE = 65536; +static const uint64_t NMS_DELIMITER = 0xFFFFFFFFFFFFFFFF; +static const uint64_t NMS_IMAGE_DELIMITER = 0xFFFFFFFFFFFFFFFE; +static const uint64_t NMS_H15_PADDING = 0xFFFFFFFFFFFFFFFD; + +enum class NMSBurstState { + NMS_BURST_STATE_WAITING_FOR_DELIMETER = 0, + NMS_BURST_STATE_WAITING_FOR_IMAGE_DELIMETER = 1, + NMS_BURST_STATE_WAITING_FOR_PADDING = 2, +}; + +class NMSStreamReader { +public: + static hailo_status read_nms(OutputStream &stream, void *buffer, size_t offset, size_t size); +private: + static hailo_status read_nms_bbox_mode(OutputStream &stream, void *buffer, size_t offset); + static hailo_status read_nms_burst_mode(OutputStream &stream, void *buffer, size_t offset, size_t buffer_size); + static hailo_status advance_state_machine(NMSBurstState *burst_state, const uint64_t current_bbox, + const hailo_nms_burst_type_t burst_type, const uint32_t num_classes, size_t *num_delimeters_received, + bool *can_stop_reading_burst, const size_t burst_offset, const size_t burst_size, size_t *burst_index); +}; + +} /* namespace hailort */ + +#endif /* _STREAM_INTERNAL_HPP_ */ \ No newline at end of file diff --git a/hailort/libhailort/src/stream_common/stream.cpp b/hailort/libhailort/src/stream_common/stream.cpp index df20eee..909fbbf 100644 --- a/hailort/libhailort/src/stream_common/stream.cpp +++ b/hailort/libhailort/src/stream_common/stream.cpp @@ -12,6 +12,7 @@ #include "hailo/hailort_common.hpp" #include "hailo/transform.hpp" #include "common/utils.hpp" +#include "stream_common/nms_stream_reader.hpp" #include @@ -25,25 +26,32 @@ hailo_status InputStream::flush() hailo_status InputStream::write(const MemoryView &buffer) { - CHECK((buffer.size() % get_info().hw_frame_size) == 0, HAILO_INVALID_ARGUMENT, - "write size {} must be a multiple of hw size {}", buffer.size(), get_info().hw_frame_size); + CHECK(buffer.size() == get_frame_size(), HAILO_INVALID_ARGUMENT, + "write size {} must be {}", buffer.size(), get_frame_size()); CHECK(((buffer.size() % HailoRTCommon::HW_DATA_ALIGNMENT) == 0), HAILO_INVALID_ARGUMENT, "Input must be aligned to {} (got {})", HailoRTCommon::HW_DATA_ALIGNMENT, buffer.size()); - - return sync_write_all_raw_buffer_no_transform_impl(const_cast(buffer.data()), 0, buffer.size()); + + return write_impl(buffer); } -hailo_status InputStream::wait_for_ready(size_t /* transfer_size */, std::chrono::milliseconds /* timeout */) +hailo_status InputStream::write(const void *buffer, size_t size) { - return HAILO_NOT_IMPLEMENTED; + return write(MemoryView::create_const(buffer, size)); } -hailo_status InputStream::write_async(std::shared_ptr /* buffer */, const TransferDoneCallback &/* user_callback */, void */* opaque */) +hailo_status InputStream::wait_for_async_ready(size_t /* transfer_size */, std::chrono::milliseconds /* timeout */) { + LOGGER__ERROR("wait_for_async_ready not implemented for sync API"); return HAILO_NOT_IMPLEMENTED; } +Expected InputStream::get_async_max_queue_size() const +{ + LOGGER__ERROR("get_async_max_queue_size not implemented for sync API"); + return make_unexpected(HAILO_NOT_IMPLEMENTED); +} + std::string InputStream::to_string() const { std::stringstream string_stream; @@ -60,76 +68,40 @@ EventPtr &InputStream::get_network_group_activated_event() hailo_status OutputStream::read_nms(void *buffer, size_t offset, size_t size) { - uint32_t num_of_classes = get_info().nms_info.number_of_classes; - uint32_t max_bboxes_per_class = get_info().nms_info.max_bboxes_per_class; - uint32_t chunks_per_frame = get_info().nms_info.chunks_per_frame; - size_t bbox_size = get_info().nms_info.bbox_size; - size_t transfer_size = bbox_size; - CHECK(size == get_info().hw_frame_size, HAILO_INSUFFICIENT_BUFFER, "On nms stream buffer size should be {} (given size {})", get_info().hw_frame_size, size); - for (uint32_t chunk_index = 0; chunk_index < chunks_per_frame; chunk_index++) { - for (uint32_t class_index = 0; class_index < num_of_classes; class_index++) { - nms_bbox_counter_t class_bboxes_count = 0; - nms_bbox_counter_t* class_bboxes_count_ptr = (nms_bbox_counter_t*)(reinterpret_cast(buffer) + offset); - offset += sizeof(*class_bboxes_count_ptr); - - // Read bboxes until reaching delimiter - for (;;) { - MemoryView buffer_view(static_cast(buffer) + offset, transfer_size); - auto expected_bytes_read = sync_read_raw_buffer(buffer_view); - if ((HAILO_STREAM_ABORTED_BY_USER == expected_bytes_read.status()) || - ((HAILO_STREAM_NOT_ACTIVATED == expected_bytes_read.status()))) { - return expected_bytes_read.status(); - } - CHECK_EXPECTED_AS_STATUS(expected_bytes_read, "Failed reading nms bbox"); - transfer_size = expected_bytes_read.release(); - CHECK(transfer_size == bbox_size, HAILO_INTERNAL_FAILURE, - "Data read from the device was size {}, should be bbox size {}", transfer_size, bbox_size); - - if (HailoRTCommon::NMS_DUMMY_DELIMITER == *(uint64_t*)((uint8_t*)buffer + offset)) { - continue; - } - - if (HailoRTCommon::NMS_DELIMITER == *(uint64_t*)((uint8_t*)buffer + offset)) { - break; - } - - class_bboxes_count++; - CHECK(class_bboxes_count <= max_bboxes_per_class, HAILO_INTERNAL_FAILURE, - "Data read from the device for the current class was size {}, max size is {}", class_bboxes_count, max_bboxes_per_class); - offset += bbox_size; - } - - *class_bboxes_count_ptr = class_bboxes_count; - } - } - return HAILO_SUCCESS; + return NMSStreamReader::read_nms((*this), buffer, offset, size); } hailo_status OutputStream::read(MemoryView buffer) { - CHECK((buffer.size() % get_info().hw_frame_size) == 0, HAILO_INVALID_ARGUMENT, - "Read size {} must be a multiple of hw size {}", buffer.size(), get_info().hw_frame_size); + CHECK(buffer.size() == get_frame_size(), HAILO_INVALID_ARGUMENT, "Read size {} must be {}", buffer.size(), + get_frame_size()); if (get_info().format.order == HAILO_FORMAT_ORDER_HAILO_NMS){ return read_nms(buffer.data(), 0, buffer.size()); } else { - return this->read_all(buffer); + return read_impl(buffer); } } -hailo_status OutputStream::wait_for_ready(size_t /* transfer_size */, std::chrono::milliseconds /* timeout */) +hailo_status OutputStream::read(void *buffer, size_t size) { - return HAILO_NOT_IMPLEMENTED; + return read(MemoryView(buffer, size)); } -hailo_status OutputStream::read_async(std::shared_ptr /* buffer */, const TransferDoneCallback &/* user_callback */, void */* opaque */) +hailo_status OutputStream::wait_for_async_ready(size_t /* transfer_size */, std::chrono::milliseconds /* timeout */) { + LOGGER__ERROR("wait_for_async_ready not implemented for sync API"); return HAILO_NOT_IMPLEMENTED; } +Expected OutputStream::get_async_max_queue_size() const +{ + LOGGER__ERROR("get_async_max_queue_size not implemented for sync API"); + return make_unexpected(HAILO_NOT_IMPLEMENTED); +} std::string OutputStream::to_string() const { diff --git a/hailort/libhailort/src/stream_common/stream_internal.cpp b/hailort/libhailort/src/stream_common/stream_internal.cpp index b3fb244..76a6421 100644 --- a/hailort/libhailort/src/stream_common/stream_internal.cpp +++ b/hailort/libhailort/src/stream_common/stream_internal.cpp @@ -27,6 +27,42 @@ InputStreamBase::InputStreamBase(const hailo_stream_info_t &stream_info, m_stream_info = stream_info; } +hailo_status InputStreamBase::write_async(BufferPtr buffer, const TransferDoneCallback &user_callback) +{ + CHECK_ARG_NOT_NULL(buffer); + CHECK_ARG_NOT_NULL(buffer->data()); + CHECK(buffer->size() == get_frame_size(), HAILO_INVALID_ARGUMENT, "Write size {} must be frame size {}", buffer->size(), + get_frame_size()); + + auto wrapped_callback = [buffer, user_callback](hailo_status status) { + user_callback(CompletionInfo{status, buffer->data(), buffer->size()}); + }; + return write_async(TransferRequest{MemoryView(*buffer), wrapped_callback, buffer}); +} + +hailo_status InputStreamBase::write_async(const MemoryView &buffer, const TransferDoneCallback &user_callback) +{ + CHECK_ARG_NOT_NULL(buffer.data()); + CHECK(buffer.size() == get_frame_size(), HAILO_INVALID_ARGUMENT, "Write size {} must be frame size {}", buffer.size(), + get_frame_size()); + + auto wrapped_callback = [buffer, user_callback](hailo_status status) { + user_callback(CompletionInfo{status, const_cast(buffer.data()), buffer.size()}); + }; + return write_async(TransferRequest{buffer, wrapped_callback}); +} + +hailo_status InputStreamBase::write_async(const void *buffer, size_t size, const TransferDoneCallback &user_callback) +{ + return write_async(MemoryView::create_const(buffer, size), user_callback); +} + +hailo_status InputStreamBase::write_async(TransferRequest &&) +{ + LOGGER__ERROR("write_async not implemented for sync API"); + return HAILO_NOT_IMPLEMENTED; +} + EventPtr &InputStreamBase::get_core_op_activated_event() { return m_core_op_activated_event; @@ -44,6 +80,42 @@ OutputStreamBase::OutputStreamBase(const LayerInfo &layer_info, const hailo_stre m_stream_info = stream_info; } +hailo_status OutputStreamBase::read_async(BufferPtr buffer, const TransferDoneCallback &user_callback) +{ + CHECK_ARG_NOT_NULL(buffer); + CHECK_ARG_NOT_NULL(buffer->data()); + CHECK(buffer->size() == get_frame_size(), HAILO_INVALID_ARGUMENT, "Read size {} must be frame size {}", buffer->size(), + get_frame_size()); + + auto wrapped_callback = [buffer, user_callback](hailo_status status) { + user_callback(CompletionInfo{status, const_cast(buffer->data()), buffer->size()}); + }; + return read_async(TransferRequest{MemoryView(*buffer), wrapped_callback, buffer}); +} + +hailo_status OutputStreamBase::read_async(MemoryView buffer, const TransferDoneCallback &user_callback) +{ + CHECK_ARG_NOT_NULL(buffer.data()); + CHECK(buffer.size() == get_frame_size(), HAILO_INVALID_ARGUMENT, "Read size {} must be frame size {}", buffer.size(), + get_frame_size()); + + auto wrapped_callback = [buffer, user_callback](hailo_status status) { + user_callback(CompletionInfo{status, const_cast(buffer.data()), buffer.size()}); + }; + return read_async(TransferRequest{buffer, wrapped_callback}); +} + +hailo_status OutputStreamBase::read_async(void *buffer, size_t size, const TransferDoneCallback &user_callback) +{ + return read_async(MemoryView(buffer, size), user_callback); +} + +hailo_status OutputStreamBase::read_async(TransferRequest &&) +{ + LOGGER__ERROR("read_async not implemented for sync API"); + return HAILO_NOT_IMPLEMENTED; +} + EventPtr &OutputStreamBase::get_core_op_activated_event() { return m_core_op_activated_event; diff --git a/hailort/libhailort/src/stream_common/stream_internal.hpp b/hailort/libhailort/src/stream_common/stream_internal.hpp index 27eff6b..b09340c 100644 --- a/hailort/libhailort/src/stream_common/stream_internal.hpp +++ b/hailort/libhailort/src/stream_common/stream_internal.hpp @@ -11,16 +11,23 @@ * * InputStream (External "interface") * |-- InputStreamBase (Base class) - * |-- VdmaInputStream + * |-- VdmaInputStreamBase + * |-- VdmaInputStream + * |-- VdmaAsyncInputStream * |-- EthernetInputStream * |-- MipiInputStream + * |-- VDeviceInputStreamBase + * |-- See vdevice_stream.hpp for subclasses * * * OutputStream (External "interface") * |-- OutputStreamBase (Base class) - * |-- VdmaOutputStream + * |-- VdmaOutputStreamBase + * |-- VdmaOutputStream + * |-- VdmaAsyncOutputStream * |-- EthernetOutputStream - * + * |-- VDeviceOutputStreamBase + * |-- See vdevice_stream.hpp for subclasses **/ #ifndef _STREAM_INTERNAL_HPP_ @@ -30,11 +37,14 @@ #include "hailo/event.hpp" #include "hailo/hailort_common.hpp" +#include "stream_common/async_common.hpp" #include "hef/hef_internal.hpp" #include "device_common/control_protocol.hpp" #include "hef/layer_info.hpp" #include "vdma/channel/boundary_channel.hpp" +using device_id_t = std::string; + namespace hailort { @@ -64,9 +74,9 @@ public: return m_nn_stream_config; }; - virtual hailo_status send_pending_buffer(size_t device_index = 0) + virtual hailo_status send_pending_buffer(const device_id_t &device_id) { - (void)device_index; + (void)device_id; return HAILO_INVALID_OPERATION; } @@ -74,16 +84,17 @@ public: { return make_unexpected(HAILO_INVALID_OPERATION); } - + virtual Expected get_pending_frames_count() const { return make_unexpected(HAILO_INVALID_OPERATION); } - virtual hailo_status register_interrupt_callback(const vdma::ProcessingCompleteCallback &/*callback*/) - { - return HAILO_INVALID_OPERATION; - } + virtual hailo_status write_async(BufferPtr buffer, const TransferDoneCallback &user_callback) override final; + virtual hailo_status write_async(const MemoryView &buffer, const TransferDoneCallback &user_callback) override final; + virtual hailo_status write_async(const void *buffer, size_t size, const TransferDoneCallback &user_callback) override final; + + virtual hailo_status write_async(TransferRequest &&transfer_request); CONTROL_PROTOCOL__nn_stream_config_t m_nn_stream_config; @@ -94,7 +105,14 @@ protected: { m_stream_info = LayerInfoUtils::get_stream_info_from_layer_info(layer_info); - const bool hw_padding_supported = HefConfigurator::is_hw_padding_supported(layer_info); + auto max_periph_bytes_from_hef = HefConfigurator::max_periph_bytes_value(stream_interface); + if (HAILO_SUCCESS != max_periph_bytes_from_hef.status()) { + status = max_periph_bytes_from_hef.status(); + return; + } + const auto max_periph_bytes = MIN(max_periph_bytes_from_hef.value(), layer_info.max_shmifo_size); + const bool hw_padding_supported = HefConfigurator::is_hw_padding_supported(layer_info, max_periph_bytes); + auto nn_stream_config = HefConfigurator::parse_nn_stream_config(layer_info, hw_padding_supported && (HAILO_STREAM_INTERFACE_MIPI != stream_interface)); // On MIPI networks, we don't want to use hw padding nn stream config. if(!nn_stream_config) { @@ -138,27 +156,41 @@ public: { return make_unexpected(HAILO_INVALID_OPERATION); } - + virtual Expected get_pending_frames_count() const { return make_unexpected(HAILO_INVALID_OPERATION); } - virtual hailo_status register_interrupt_callback(const vdma::ProcessingCompleteCallback &/*callback*/) + virtual hailo_status set_next_device_to_read(const device_id_t &device_id) { + (void)device_id; return HAILO_INVALID_OPERATION; } + virtual hailo_status read_async(BufferPtr buffer, const TransferDoneCallback &user_callback) override final; + virtual hailo_status read_async(MemoryView buffer, const TransferDoneCallback &user_callback) override final; + virtual hailo_status read_async(void *buffer, size_t size, const TransferDoneCallback &user_callback) override final; + + virtual hailo_status read_async(TransferRequest &&transfer_request); + CONTROL_PROTOCOL__nn_stream_config_t m_nn_stream_config; protected: - explicit OutputStreamBase(const LayerInfo &layer_info, + explicit OutputStreamBase(const LayerInfo &layer_info, hailo_stream_interface_t stream_interface, EventPtr &&core_op_activated_event, hailo_status &status) : m_layer_info(layer_info), m_core_op_activated_event(std::move(core_op_activated_event)) { m_stream_info = LayerInfoUtils::get_stream_info_from_layer_info(m_layer_info); - const bool hw_padding_supported = HefConfigurator::is_hw_padding_supported(m_layer_info); + auto max_periph_bytes_from_hef = HefConfigurator::max_periph_bytes_value(stream_interface); + if (HAILO_SUCCESS != max_periph_bytes_from_hef.status()) { + status = max_periph_bytes_from_hef.status(); + return; + } + const auto max_periph_bytes = MIN(max_periph_bytes_from_hef.value(), layer_info.max_shmifo_size); + const bool hw_padding_supported = HefConfigurator::is_hw_padding_supported(layer_info, max_periph_bytes); + auto nn_stream_config = HefConfigurator::parse_nn_stream_config(m_layer_info, hw_padding_supported); if(!nn_stream_config) { LOGGER__ERROR("Failed parse nn stream config"); diff --git a/hailort/libhailort/src/transform/transform.cpp b/hailort/libhailort/src/transform/transform.cpp index a983c95..54e5208 100644 --- a/hailort/libhailort/src/transform/transform.cpp +++ b/hailort/libhailort/src/transform/transform.cpp @@ -185,6 +185,7 @@ hailo_status transform__transpose_buffer(const void *src_ptr, const hailo_3d_ima switch (format.order) { case HAILO_FORMAT_ORDER_NHWC: + case HAILO_FORMAT_ORDER_RGB4: case HAILO_FORMAT_ORDER_NHW: case HAILO_FORMAT_ORDER_BAYER_RGB: case HAILO_FORMAT_ORDER_12_BIT_BAYER_RGB: @@ -445,8 +446,8 @@ static inline void transform__parse_and_copy_bbox (hailo_bbox_t *dst, uint64_t* void transform__d2h_NMS(const uint8_t *src_ptr, uint8_t *dst_ptr, const hailo_nms_info_t &nms_info, std::vector &chunk_offsets) { /* Validate arguments */ - ASSERT(NULL != src_ptr); - ASSERT(NULL != dst_ptr); + assert(NULL != src_ptr); + assert(NULL != dst_ptr); uint32_t num_of_classes = nms_info.number_of_classes; uint32_t bbox_size = nms_info.bbox_size; @@ -485,6 +486,7 @@ void transform__d2h_NMS(const uint8_t *src_ptr, uint8_t *dst_ptr, const hailo_nm // Add bbox from all chunks of current class src_offset = chunk_offsets[chunk_index]; class_bboxes_count = *((nms_bbox_counter_t*)((uint8_t*)src_ptr + src_offset)); + assert(class_bboxes_count <= nms_info.max_bboxes_per_class); *dst_bbox_counter = static_cast(*dst_bbox_counter + class_bboxes_count); src_offset += sizeof(nms_bbox_counter_t); @@ -739,8 +741,8 @@ hailo_status transform__d2h_argmax_NHCW_to_NHW(const T *src_ptr, const hailo_3d_ CHECK(dst_image_shape.features == 1, HAILO_INVALID_OPERATION, "NHCW_to_NHW argmax Transform is supported only when dst features ({}) is 1", dst_image_shape.features); - CHECK(src_image_shape.features < std::numeric_limits::max(), HAILO_INVALID_OPERATION, - "NHCW_to_NHW argmax Transform is supported only when src features ({}) is smaller than {}", + CHECK(src_image_shape.features <= std::numeric_limits::max(), HAILO_INVALID_OPERATION, + "NHCW_to_NHW argmax Transform is supported only when src features ({}) is equal/smaller than {}", src_image_shape.features, std::numeric_limits::max()); const auto src_row_size = src_image_shape.width * src_image_shape.features; @@ -828,7 +830,7 @@ hailo_status transform__h2d_RGB4_to_NHCW(const T *src_ptr, const hailo_3d_image_ const auto src_row_size = HailoRTCommon::align_to(row_size, RGB4_ALIGNMENT); const auto dst_row_size = dst_image_shape.width * dst_image_shape.features; - const auto pad_size = (dst_image_shape.width - src_image_shape.width) * dst_image_shape.features; + const auto pad_size = dst_image_shape.width - src_image_shape.width; uint32_t src_offset = 0; uint32_t dst_offset = 0; @@ -841,7 +843,7 @@ hailo_status transform__h2d_RGB4_to_NHCW(const T *src_ptr, const hailo_3d_image_ dst_offset = r * dst_row_size + f * dst_image_shape.width + c; dst_ptr[dst_offset] = src_ptr[src_offset]; } - /* pad feature to 8 elemnts */ + /* pad feature to 8 elements */ if (pad_size != 0) { dst_offset = r * dst_row_size + f * dst_image_shape.width + src_image_shape.width; std::fill_n(dst_ptr + dst_offset, pad_size, static_cast(0)); @@ -901,7 +903,11 @@ hailo_status FrameOutputTransformContext::quantize_stream(const void *dst_ptr) switch (m_dst_format.type) { case HAILO_FORMAT_TYPE_UINT8: if (HAILO_FORMAT_TYPE_UINT8 == m_src_format.type) { - Quantization::dequantize_output_buffer_in_place((uint8_t*)dst_ptr, shape_size, m_dst_quant_info); + if (m_are_all_qps_the_same) { + Quantization::dequantize_output_buffer_in_place((uint8_t*)dst_ptr, shape_size, m_dst_quant_info); + } else { + dequantize_output_by_feature((uint8_t*)dst_ptr, shape_size, m_quant_info_per_feature, m_quant_infos_rep_count); + } } else { return HAILO_INVALID_OPERATION; @@ -909,10 +915,18 @@ hailo_status FrameOutputTransformContext::quantize_stream(const void *dst_ptr) break; case HAILO_FORMAT_TYPE_UINT16: if (HAILO_FORMAT_TYPE_UINT8 == m_src_format.type) { - Quantization::dequantize_output_buffer_in_place((uint16_t*)dst_ptr, shape_size, m_dst_quant_info); + if (m_are_all_qps_the_same) { + Quantization::dequantize_output_buffer_in_place((uint16_t*)dst_ptr, shape_size, m_dst_quant_info); + } else { + dequantize_output_by_feature((uint16_t*)dst_ptr, shape_size, m_quant_info_per_feature, m_quant_infos_rep_count); + } } else if (HAILO_FORMAT_TYPE_UINT16 == m_src_format.type) { - Quantization::dequantize_output_buffer_in_place((uint16_t*)dst_ptr, shape_size, m_dst_quant_info); + if (m_are_all_qps_the_same) { + Quantization::dequantize_output_buffer_in_place((uint16_t*)dst_ptr, shape_size, m_dst_quant_info); + } else { + dequantize_output_by_feature((uint16_t*)dst_ptr, shape_size, m_quant_info_per_feature, m_quant_infos_rep_count); + } } else { return HAILO_INVALID_OPERATION; @@ -922,10 +936,18 @@ hailo_status FrameOutputTransformContext::quantize_stream(const void *dst_ptr) /* if output layer is argmax - do not rescale */ if (HAILO_FORMAT_ORDER_NHW != m_dst_format.order) { if (HAILO_FORMAT_TYPE_UINT8 == m_src_format.type) { - Quantization::dequantize_output_buffer_in_place((float32_t*)dst_ptr, shape_size, m_dst_quant_info); + if (m_are_all_qps_the_same) { + Quantization::dequantize_output_buffer_in_place((float32_t*)dst_ptr, shape_size, m_dst_quant_info); + } else { + dequantize_output_by_feature((float32_t*)dst_ptr, shape_size, m_quant_info_per_feature, m_quant_infos_rep_count); + } } else if (HAILO_FORMAT_TYPE_UINT16 == m_src_format.type) { - Quantization::dequantize_output_buffer_in_place((float32_t*)dst_ptr, shape_size, m_dst_quant_info); + if (m_are_all_qps_the_same) { + Quantization::dequantize_output_buffer_in_place((float32_t*)dst_ptr, shape_size, m_dst_quant_info); + } else { + dequantize_output_by_feature((float32_t*)dst_ptr, shape_size, m_quant_info_per_feature, m_quant_infos_rep_count); + } } else { return HAILO_INVALID_OPERATION; @@ -1795,7 +1817,50 @@ FrameOutputTransformContext::FrameOutputTransformContext(size_t src_frame_size, OutputTransformContext(src_frame_size, src_format, dst_frame_size, dst_format, dst_quant_info, should_quantize, should_transpose, should_reorder), m_src_image_shape(src_image_shape), m_dst_image_shape(dst_image_shape), m_transpose_buffer(std::move(transpose_buffer)) -{} +{ + std::vector dst_quant_infos = { dst_quant_info }; // TODO: Get vector from HEF + bool are_all_qps_the_same = true; + if (dst_quant_infos.size() > 1) { + for (const auto &quant_info : dst_quant_infos) { + if (0 != memcmp(&quant_info, &dst_quant_infos[0], sizeof(quant_info))) { + are_all_qps_the_same = false; + break; + } + } + } + m_are_all_qps_the_same = are_all_qps_the_same; + + switch (dst_format.order) { + case HAILO_FORMAT_ORDER_NHW: + case HAILO_FORMAT_ORDER_BAYER_RGB: + case HAILO_FORMAT_ORDER_12_BIT_BAYER_RGB: + case HAILO_FORMAT_ORDER_NCHW: + for (const auto &quant_info : dst_quant_infos) { + m_quant_info_per_feature.emplace_back(quant_info.qp_zp, quant_info.qp_scale); + } + m_quant_infos_rep_count = static_cast(dst_frame_size); + break; + case HAILO_FORMAT_ORDER_NHWC: + case HAILO_FORMAT_ORDER_FCR: + case HAILO_FORMAT_ORDER_F8CR: + case HAILO_FORMAT_ORDER_NC: + case HAILO_FORMAT_ORDER_RGB4: + for (const auto &quant_info : dst_quant_infos) { + m_quant_info_per_feature.emplace_back(quant_info.qp_zp, quant_info.qp_scale); + } + m_quant_infos_rep_count = 1; + break; + case HAILO_FORMAT_ORDER_NHCW: + for (const auto &quant_info : dst_quant_infos) { + m_quant_info_per_feature.emplace_back(quant_info.qp_zp, quant_info.qp_scale); + } + m_quant_infos_rep_count = dst_image_shape.width; + break; + default: + LOGGER__CRITICAL("Got unknown format order = {}", dst_format.order); + break; + } +} Expected> FrameOutputTransformContext::create(const hailo_3d_image_shape_t &src_image_shape, const hailo_format_t &src_format, const hailo_3d_image_shape_t &dst_image_shape, diff --git a/hailort/libhailort/src/transform/transform_internal.hpp b/hailort/libhailort/src/transform/transform_internal.hpp index b8ef52d..c1038a8 100644 --- a/hailort/libhailort/src/transform/transform_internal.hpp +++ b/hailort/libhailort/src/transform/transform_internal.hpp @@ -16,6 +16,7 @@ #include "hailo/buffer.hpp" #include "hailo/hef.hpp" #include "hailo/transform.hpp" +#include "hailo/quantization.hpp" #include "stream_common/stream_internal.hpp" #include "hef/layer_info.hpp" @@ -74,6 +75,14 @@ private: std::vector m_mux_infos; }; +struct QuantInfoForDequantize +{ + float32_t m_qp_zp; + float32_t m_qp_scale; + QuantInfoForDequantize(float32_t qp_zp, float32_t qp_scale) : m_qp_zp(qp_zp), m_qp_scale(qp_scale) + {} +}; + class HAILORTAPI FrameOutputTransformContext final : public OutputTransformContext { public: @@ -95,9 +104,26 @@ public: virtual std::string description() const override; private: + template + static inline void dequantize_output_by_feature(T *dst_ptr, uint32_t buffer_elements_count, + const std::vector &quant_infos, uint32_t repetition_count) + { + uint32_t elements_dequantized = 0; + while (elements_dequantized < buffer_elements_count) { + for (int32_t i = static_cast(quant_infos.size()) - 1; i >= 0; i--) { + Quantization::dequantize_output_buffer_in_place(dst_ptr, buffer_elements_count - repetition_count - elements_dequantized, + repetition_count, quant_infos[i].m_qp_zp, quant_infos[i].m_qp_scale); + elements_dequantized += repetition_count; + } + } + } + const hailo_3d_image_shape_t m_src_image_shape; const hailo_3d_image_shape_t m_dst_image_shape; Buffer m_transpose_buffer; + bool m_are_all_qps_the_same; + std::vector m_quant_info_per_feature; + uint32_t m_quant_infos_rep_count; }; class HAILORTAPI NMSOutputTransformContext final : public OutputTransformContext diff --git a/hailort/libhailort/src/utils/CMakeLists.txt b/hailort/libhailort/src/utils/CMakeLists.txt index c16a9a5..70cbfc1 100644 --- a/hailort/libhailort/src/utils/CMakeLists.txt +++ b/hailort/libhailort/src/utils/CMakeLists.txt @@ -4,6 +4,7 @@ set(SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/hailort_common.cpp ${CMAKE_CURRENT_SOURCE_DIR}/hailort_logger.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/buffer_storage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/buffer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/sensor_config_utils.cpp ) diff --git a/hailort/libhailort/src/utils/buffer.cpp b/hailort/libhailort/src/utils/buffer.cpp index 1430910..2148487 100644 --- a/hailort/libhailort/src/utils/buffer.cpp +++ b/hailort/libhailort/src/utils/buffer.cpp @@ -27,6 +27,8 @@ static void format_buffer(std::ostream& stream, const uint8_t *buffer, size_t si { assert(nullptr != buffer); + stream << "[addr = " << static_cast(buffer) << ", size = " << size << "]" << std::endl; + static const bool UPPERCASE = true; static const size_t BYTES_PER_LINE = 32; static const char *BYTE_DELIM = " "; @@ -35,67 +37,80 @@ static void format_buffer(std::ostream& stream, const uint8_t *buffer, size_t si stream << fmt::format("0x{:08X}", offset) << BYTE_DELIM; // 32 bit offset into a buffer should be enough stream << StringUtils::to_hex_string(buffer + offset, line_size, UPPERCASE, BYTE_DELIM) << std::endl; } - stream << "[size = " << std::dec << size << "]"; } Buffer::Buffer() : + m_storage(), m_data(nullptr), m_size(0) {} +Buffer::Buffer(BufferStoragePtr storage) : + m_storage(storage), + m_data(static_cast(m_storage->user_address())), + m_size(m_storage->size()) +{} + Buffer::Buffer(Buffer&& other) : - m_data(std::move(other.m_data)), + m_storage(std::move(other.m_storage)), + m_data(std::exchange(other.m_data, nullptr)), m_size(std::exchange(other.m_size, 0)) {} -Expected Buffer::create(size_t size) +Expected Buffer::create(size_t size, const BufferStorageParams ¶ms) { - std::unique_ptr data(new (std::nothrow) uint8_t[size]); - if (data == nullptr) { - LOGGER__ERROR("Failed allocating {} bytes", size); - return make_unexpected(HAILO_OUT_OF_HOST_MEMORY); - } + auto storage = BufferStorage::create(size, params); + CHECK_EXPECTED(storage); - return Buffer(std::move(data), size); + return Buffer(storage.release()); } -Expected Buffer::create(size_t size, uint8_t default_value) +Expected Buffer::create(size_t size, uint8_t default_value, const BufferStorageParams ¶ms) { - auto buffer = create(size); + auto buffer = create(size, params); CHECK_EXPECTED(buffer); - std::memset(static_cast(buffer->m_data.get()), default_value, size); + std::memset(static_cast(buffer->m_data), default_value, size); return buffer; } -Expected Buffer::create_shared(size_t size) +Expected Buffer::create_shared(size_t size, const BufferStorageParams ¶ms) { - auto buffer = Buffer::create(size); + auto buffer = Buffer::create(size, params); CHECK_EXPECTED(buffer); auto buffer_ptr = make_shared_nothrow(buffer.release()); CHECK_NOT_NULL_AS_EXPECTED(buffer_ptr, HAILO_OUT_OF_HOST_MEMORY); return buffer_ptr; } -Expected Buffer::create_shared(size_t size, uint8_t default_value) +Expected Buffer::create_shared(size_t size, uint8_t default_value, const BufferStorageParams ¶ms) { - auto buffer = Buffer::create(size, default_value); + auto buffer = Buffer::create(size, default_value, params); CHECK_EXPECTED(buffer); auto buffer_ptr = make_shared_nothrow(buffer.release()); CHECK_NOT_NULL_AS_EXPECTED(buffer_ptr, HAILO_OUT_OF_HOST_MEMORY); return buffer_ptr; } -Expected Buffer::create(const uint8_t *src, size_t size) +Expected Buffer::create_shared(const uint8_t *src, size_t size, const BufferStorageParams ¶ms) { - auto buffer = create(size); + auto buffer = Buffer::create(src, size, params); CHECK_EXPECTED(buffer); - std::memcpy(static_cast(buffer->m_data.get()), static_cast(src), size); + auto buffer_ptr = make_shared_nothrow(buffer.release()); + CHECK_NOT_NULL_AS_EXPECTED(buffer_ptr, HAILO_OUT_OF_HOST_MEMORY); + return buffer_ptr; +} + +Expected Buffer::create(const uint8_t *src, size_t size, const BufferStorageParams ¶ms) +{ + auto buffer = create(size, params); + CHECK_EXPECTED(buffer); + std::memcpy(static_cast(buffer->m_data), static_cast(src), size); return buffer; } -Expected Buffer::create(std::initializer_list init) +Expected Buffer::create(std::initializer_list init, const BufferStorageParams ¶ms) { - auto buffer = create(init.size()); + auto buffer = create(init.size(), params); CHECK_EXPECTED(buffer); size_t index = 0; for (const auto& n : init) { @@ -108,12 +123,13 @@ Expected Buffer::create(std::initializer_list init) Expected Buffer::copy() const { - return Buffer::create(m_data.get(), m_size); + return Buffer::create(m_data, m_size); } Buffer& Buffer::operator=(Buffer&& other) { - m_data = std::move(other.m_data); + m_storage = std::move(other.m_storage); + m_data = std::exchange(other.m_data, nullptr); m_size = std::exchange(other.m_size, 0); return *this; } @@ -123,7 +139,7 @@ bool Buffer::operator==(const Buffer& rhs) const if (m_size != rhs.m_size) { return false; } - return (0 == std::memcmp(data(), rhs.data(), m_size)); + return (0 == std::memcmp(m_data, rhs.m_data, m_size)); } bool Buffer::operator!=(const Buffer& rhs) const @@ -131,7 +147,7 @@ bool Buffer::operator!=(const Buffer& rhs) const if (m_size != rhs.m_size) { return true; } - return (0 != std::memcmp(data(), rhs.data(), m_size)); + return (0 != std::memcmp(m_data, rhs.m_data, m_size)); } uint8_t& Buffer::operator[](size_t pos) @@ -156,14 +172,19 @@ Buffer::iterator Buffer::end() return iterator(data() + m_size); } +BufferStorage &Buffer::storage() +{ + return *m_storage; +} + uint8_t* Buffer::data() noexcept { - return m_data.get(); + return m_data; } const uint8_t* Buffer::data() const noexcept { - return m_data.get(); + return m_data; } size_t Buffer::size() const noexcept @@ -171,22 +192,16 @@ size_t Buffer::size() const noexcept return m_size; } -uint8_t* Buffer::release() noexcept -{ - m_size = 0; - return m_data.release(); -} - std::string Buffer::to_string() const { for (size_t i = 0; i < m_size; i++) { if (m_data[i] == 0) { // We'll return a string that ends at the first null in the buffer - return std::string(reinterpret_cast(m_data.get())); + return std::string(reinterpret_cast(m_data)); } } - return std::string(reinterpret_cast(m_data.get()), m_size); + return std::string(reinterpret_cast(m_data), m_size); } // Note: This is a friend function @@ -226,11 +241,6 @@ uint64_t& Buffer::as_uint64() return as_type(); } -Buffer::Buffer(std::unique_ptr data, size_t size) : - m_data(std::move(data)), - m_size(size) - {} - MemoryView::MemoryView() : m_data(nullptr), m_size(0) diff --git a/hailort/libhailort/src/utils/buffer_storage.cpp b/hailort/libhailort/src/utils/buffer_storage.cpp new file mode 100644 index 0000000..2f94cb4 --- /dev/null +++ b/hailort/libhailort/src/utils/buffer_storage.cpp @@ -0,0 +1,345 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file buffer_storage.cpp + * @brief TODO: fill me (HRT-10026) + **/ + +#include "hailo/buffer_storage.hpp" +#include "hailo/hailort.h" +#include "hailo/vdevice.hpp" +#include "vdma/vdma_device.hpp" +#include "vdma/memory/dma_able_buffer.hpp" +#include "vdma/memory/mapped_buffer.hpp" +#include "common/utils.hpp" + +namespace hailort +{ + +// Checking ABI of hailo_dma_buffer_direction_t vs HailoRTDriver::DmaDirection +static_assert(HAILO_DMA_BUFFER_DIRECTION_H2D == (int)HailoRTDriver::DmaDirection::H2D, + "hailo_dma_buffer_direction_t must match HailoRTDriver::DmaDirection"); +static_assert(HAILO_DMA_BUFFER_DIRECTION_D2H == (int)HailoRTDriver::DmaDirection::D2H, + "hailo_dma_buffer_direction_t must match HailoRTDriver::DmaDirection"); +static_assert(HAILO_DMA_BUFFER_DIRECTION_BOTH == (int)HailoRTDriver::DmaDirection::BOTH, + "hailo_dma_buffer_direction_t must match HailoRTDriver::DmaDirection"); + +BufferStorageParams::HeapParams::HeapParams() +{} + +Expected BufferStorageParams::DmaMappingParams::create( + const hailo_buffer_dma_mapping_params_t ¶ms) +{ + CHECK_AS_EXPECTED((params.device == nullptr) || (params.vdevice == nullptr), HAILO_INVALID_ARGUMENT, + "Can't set both device and vdevice fields"); + return DmaMappingParams(params); +} + +BufferStorageParams::DmaMappingParams::DmaMappingParams(const hailo_buffer_dma_mapping_params_t ¶ms) : + device(reinterpret_cast(params.device)), + vdevice(reinterpret_cast(params.vdevice)), + data_direction(params.direction) +{} + +BufferStorageParams::DmaMappingParams::DmaMappingParams(Device &device, hailo_dma_buffer_direction_t data_direction) : + device(&device), + vdevice(nullptr), + data_direction(data_direction) +{} + +BufferStorageParams::DmaMappingParams::DmaMappingParams(VDevice &vdevice, hailo_dma_buffer_direction_t data_direction) : + device(nullptr), + vdevice(&vdevice), + data_direction(data_direction) +{} + +BufferStorageParams::DmaMappingParams::DmaMappingParams() : + device(nullptr), + vdevice(nullptr), + data_direction(HAILO_DMA_BUFFER_DIRECTION_MAX_ENUM) +{} + +Expected BufferStorageParams::create(const hailo_buffer_parameters_t ¶ms) +{ + BufferStorageParams result{}; + result.flags = params.flags; + + if (params.flags == HAILO_BUFFER_FLAGS_NONE) { + result.heap_params = HeapParams(); + } else if ((params.flags & HAILO_BUFFER_FLAGS_DMA) != 0) { + auto dma_mapping_params = DmaMappingParams::create(params.dma_mapping_params); + CHECK_EXPECTED(dma_mapping_params); + result.dma_mapping_params = dma_mapping_params.release(); + } else { + // TODO: HRT-10903 + LOGGER__ERROR("Buffer storage flags not currently supported {}", params.flags); + return make_unexpected(HAILO_NOT_IMPLEMENTED); + } + + return result; +} + +BufferStorageParams BufferStorageParams::create_dma() +{ + BufferStorageParams result{}; + result.flags = HAILO_BUFFER_FLAGS_DMA; + result.dma_mapping_params = DmaMappingParams(); + return result; +} + +BufferStorageParams BufferStorageParams::create_dma(Device &device, hailo_dma_buffer_direction_t data_direction) +{ + BufferStorageParams result{}; + result.flags = HAILO_BUFFER_FLAGS_DMA; + result.dma_mapping_params = DmaMappingParams(device, data_direction); + return result; +} + +BufferStorageParams BufferStorageParams::create_dma(VDevice &vdevice, hailo_dma_buffer_direction_t data_direction) +{ + BufferStorageParams result{}; + result.flags = HAILO_BUFFER_FLAGS_DMA; + result.dma_mapping_params = DmaMappingParams(vdevice, data_direction); + return result; +} + +BufferStorageParams::BufferStorageParams() : + flags(HAILO_BUFFER_FLAGS_NONE), + heap_params() +{} + +Expected BufferStorage::create(size_t size, const BufferStorageParams ¶ms) +{ + if (params.flags == HAILO_BUFFER_FLAGS_NONE) { + auto result = HeapStorage::create(size); + CHECK_EXPECTED(result); + return std::static_pointer_cast(result.release()); + } else if (0 != (params.flags & HAILO_BUFFER_FLAGS_DMA)) { + // TODO: check other flags here (HRT-10903) + auto &dma_mapping_params = params.dma_mapping_params; + + DmaStoragePtr storage = nullptr; + if ((dma_mapping_params.device != nullptr) && (dma_mapping_params.vdevice != nullptr)) { + LOGGER__ERROR("Can't map a buffer to both vdevice and device"); + return make_unexpected(HAILO_INVALID_ARGUMENT); + } else if (dma_mapping_params.device != nullptr) { + auto result = DmaStorage::create(size, dma_mapping_params.data_direction, + *dma_mapping_params.device); + CHECK_EXPECTED(result); + storage = result.release(); + } else if (dma_mapping_params.vdevice != nullptr) { + auto result = DmaStorage::create(size, dma_mapping_params.data_direction, + *dma_mapping_params.vdevice); + CHECK_EXPECTED(result); + storage = result.release(); + } else { + auto result = DmaStorage::create(size); + CHECK_EXPECTED(result); + storage = result.release(); + } + return std::static_pointer_cast(storage); + } + + // TODO: HRT-10903 + LOGGER__ERROR("Buffer storage flags not currently supported {}", params.flags); + return make_unexpected(HAILO_NOT_IMPLEMENTED); +} + +BufferStorage::BufferStorage(Type type) : + m_type(type) +{} + +BufferStorage::Type BufferStorage::type() const +{ + return m_type; +} + +Expected HeapStorage::create(size_t size) +{ + std::unique_ptr data(new (std::nothrow) uint8_t[size]); + CHECK_NOT_NULL_AS_EXPECTED(data, HAILO_OUT_OF_HOST_MEMORY); + + auto result = make_shared_nothrow(std::move(data), size); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); + + return result; +} + +HeapStorage::HeapStorage(std::unique_ptr data, size_t size) : + BufferStorage(Type::HEAP), + m_data(std::move(data)), + m_size(size) +{} + +HeapStorage::HeapStorage(HeapStorage&& other) noexcept : + BufferStorage(std::move(other)), + m_data(std::move(other.m_data)), + m_size(std::exchange(other.m_size, 0)) +{} + +size_t HeapStorage::size() const +{ + return m_size; +} + +void *HeapStorage::user_address() +{ + return m_data.get(); +} + +Expected HeapStorage::release() noexcept +{ + m_size = 0; + return m_data.release(); +} + +Expected HeapStorage::dma_map(Device &, hailo_dma_buffer_direction_t) +{ + LOGGER__ERROR("Heap allocated buffers can't be mapped to DMA"); + return make_unexpected(HAILO_INVALID_OPERATION); +} + +Expected HeapStorage::dma_map(HailoRTDriver &, hailo_dma_buffer_direction_t) +{ + LOGGER__ERROR("Heap allocated buffers can't be mapped to DMA"); + return make_unexpected(HAILO_INVALID_OPERATION); +} + +Expected HeapStorage::get_dma_mapped_buffer(const std::string &) +{ + LOGGER__ERROR("Mapped buffer is not supported for Heap allocated buffers"); + return make_unexpected(HAILO_INVALID_OPERATION); +} + +Expected DmaStorage::create(size_t size) +{ + static const auto ALLOCATE_BUFFER = nullptr; + return create(ALLOCATE_BUFFER, size); +} + +Expected DmaStorage::create(size_t size, + hailo_dma_buffer_direction_t data_direction, Device &device) +{ + static const auto ALLOCATE_BUFFER = nullptr; + return create(ALLOCATE_BUFFER, size, data_direction, + std::vector>{std::ref(device)}); +} + +Expected DmaStorage::create(size_t size, + hailo_dma_buffer_direction_t data_direction, VDevice &vdevice) +{ + static const auto ALLOCATE_BUFFER = nullptr; + auto physical_devices = vdevice.get_physical_devices(); + CHECK_EXPECTED(physical_devices); + return create(ALLOCATE_BUFFER, size, data_direction, physical_devices.release()); +} + +Expected DmaStorage::create_from_user_address(void *user_address, size_t size) +{ + return create(user_address, size); +} + +Expected DmaStorage::create_from_user_address(void *user_address, size_t size, + hailo_dma_buffer_direction_t data_direction, Device &device) +{ + CHECK_ARG_NOT_NULL_AS_EXPECTED(user_address); + return create(user_address, size, data_direction, + std::vector>{std::ref(device)}); +} + +Expected DmaStorage::create_from_user_address(void *user_address, size_t size, + hailo_dma_buffer_direction_t data_direction, VDevice &vdevice) +{ + CHECK_ARG_NOT_NULL_AS_EXPECTED(user_address); + auto physical_devices = vdevice.get_physical_devices(); + CHECK_EXPECTED(physical_devices); + return create(user_address, size, data_direction, physical_devices.release()); +} + +Expected DmaStorage::create(void *user_address, size_t size, + hailo_dma_buffer_direction_t data_direction, + std::vector> &&physical_devices) +{ + // TODO: HRT-10283 support sharing low memory buffers for DART and similar systems. + auto dma_able_buffer = vdma::DmaAbleBuffer::create(size, user_address); + CHECK_EXPECTED(dma_able_buffer); + + auto result = make_shared_nothrow(dma_able_buffer.release()); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); + + for (auto &device : physical_devices) { + auto is_new_mapping = result->dma_map(device, data_direction); + CHECK_EXPECTED(is_new_mapping); + CHECK_AS_EXPECTED(is_new_mapping.value(), HAILO_INTERNAL_FAILURE); + } + + return result; +} + +DmaStorage::DmaStorage(vdma::DmaAbleBufferPtr &&dma_able_buffer) : + BufferStorage(Type::DMA), + m_dma_able_buffer(std::move(dma_able_buffer)), + m_mappings() +{} + +size_t DmaStorage::size() const +{ + return m_dma_able_buffer->size(); +} + +void *DmaStorage::user_address() +{ + return m_dma_able_buffer->user_address(); +} + +Expected DmaStorage::release() noexcept +{ + return make_unexpected(HAILO_NOT_IMPLEMENTED); +} + +Expected DmaStorage::dma_map(Device &device, hailo_dma_buffer_direction_t data_direction) +{ + const auto device_type = device.get_type(); + CHECK_AS_EXPECTED(((Device::Type::INTEGRATED == device_type) || (Device::Type::PCIE == device_type)), + HAILO_INVALID_ARGUMENT, "Invalid device type (expected integrated/pcie, received {})", device_type); + VdmaDevice *vdma_device = reinterpret_cast(&device); + + return dma_map(vdma_device->get_driver(), data_direction); +} + +Expected DmaStorage::dma_map(HailoRTDriver &driver, hailo_dma_buffer_direction_t data_direction) +{ + CHECK_AS_EXPECTED(data_direction <= HAILO_DMA_BUFFER_DIRECTION_BOTH, HAILO_INVALID_ARGUMENT, + "Invalid data direction {}", data_direction); + + const auto &device_id = driver.device_id(); + auto find_result = m_mappings.find(device_id); + if (find_result != m_mappings.end()) { + // The buffer has been mapped => don't map it again + return Expected(false); // not a new mapping + } + + // The buffer hasn't been mapped => map it now + auto mapped_buffer = vdma::MappedBuffer::create_shared(driver, m_dma_able_buffer, + static_cast(data_direction)); + CHECK_EXPECTED(mapped_buffer); + + m_mappings.emplace(device_id, mapped_buffer.value()); + return Expected(true); // new mapping +} + +Expected DmaStorage::get_dma_mapped_buffer(const std::string &device_id) +{ + auto mapped_buffer = m_mappings.find(device_id); + if (mapped_buffer == m_mappings.end()) { + // Don't print error message here + LOGGER__INFO("Mapped buffer for {} not found", device_id); + return make_unexpected(HAILO_NOT_FOUND); + } + + return Expected(mapped_buffer->second); +} + +} /* namespace hailort */ diff --git a/hailort/libhailort/src/utils/exported_resource_manager.hpp b/hailort/libhailort/src/utils/exported_resource_manager.hpp new file mode 100644 index 0000000..a4d2d5d --- /dev/null +++ b/hailort/libhailort/src/utils/exported_resource_manager.hpp @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file exported_resource_manager.hpp + * @brief Holds resources that are exported via c-api + **/ + +#ifndef _HAILO_EXPORTED_RESOURCE_MANAGER_HPP_ +#define _HAILO_EXPORTED_RESOURCE_MANAGER_HPP_ + +#include "hailo/hailort.h" + +#include + +namespace hailort +{ + +// TODO: Merge ExportedResourceManager and SharedResourceManager (HRT-10317) +template> +class ExportedResourceManager final +{ +public: + static hailo_status register_resource(const Resource &resource, const Key &key) + { + return get_instance().register_resource_impl(resource, key); + } + + static Expected> get_resource(const Key &key) + { + return get_instance().get_resource_impl(key); + } + + static hailo_status unregister_resource(const Key &key) + { + return get_instance().unregister_resource_impl(key); + } + +private: + static ExportedResourceManager& get_instance() + { + static ExportedResourceManager instance; + return instance; + } + + hailo_status register_resource_impl(const Resource &resource, const Key &key) + { + std::lock_guard lock_guard(m_mutex); + + auto it = m_storage.find(key); + if (it != m_storage.end()) { + LOGGER__TRACE("There's already a resource registered under key {}", key); + return HAILO_INVALID_ARGUMENT; + } + + m_storage[key] = resource; + return HAILO_SUCCESS; + } + + Expected> get_resource_impl(const Key &key) + { + std::lock_guard lock_guard(m_mutex); + + auto it = m_storage.find(key); + if (it == m_storage.end()) { + LOGGER__TRACE("Key {} not found in resource manager", key); + return make_unexpected(HAILO_NOT_FOUND); + } + + return std::ref(it->second); + } + + hailo_status unregister_resource_impl(const Key &key) + { + std::lock_guard lock_guard(m_mutex); + + auto it = m_storage.find(key); + if (it == m_storage.end()) { + LOGGER__TRACE("Key {} not found in resource manager", key); + return HAILO_NOT_FOUND; + } + + m_storage.erase(it); + return HAILO_SUCCESS; + } + + std::mutex m_mutex; + std::unordered_map m_storage; +}; + +} /* namespace hailort */ + +#endif /* _HAILO_EXPORTED_RESOURCE_MANAGER_HPP_ */ diff --git a/hailort/libhailort/src/utils/hailort_common.cpp b/hailort/libhailort/src/utils/hailort_common.cpp index 7f59c47..7f8f17f 100644 --- a/hailort/libhailort/src/utils/hailort_common.cpp +++ b/hailort/libhailort/src/utils/hailort_common.cpp @@ -17,8 +17,6 @@ namespace hailort const uint32_t HailoRTCommon::BBOX_PARAMS; const uint32_t HailoRTCommon::MAX_DEFUSED_LAYER_COUNT; const size_t HailoRTCommon::HW_DATA_ALIGNMENT; -const uint64_t HailoRTCommon::NMS_DELIMITER; -const uint64_t HailoRTCommon::NMS_DUMMY_DELIMITER; Expected HailoRTCommon::to_device_id(const std::string &device_id) { diff --git a/hailort/libhailort/src/utils/hailort_logger.cpp b/hailort/libhailort/src/utils/hailort_logger.cpp index 3eda92f..f85abba 100644 --- a/hailort/libhailort/src/utils/hailort_logger.cpp +++ b/hailort/libhailort/src/utils/hailort_logger.cpp @@ -45,6 +45,7 @@ namespace hailort #define HAILORT_ANDROID_LOGGER_PATTERN ("%v") // Android logger will print only message (additional info are built-in) #define HAILORT_LOGGER_PATH_ENV_VAR ("HAILORT_LOGGER_PATH") +#define PERIODIC_LOGGER_FLUSH_TIME_IN_SECONDS (5) #ifdef _WIN32 #define PATH_SEPARATOR "\\" @@ -140,18 +141,24 @@ std::shared_ptr HailoRTLogger::create_file_sink(const std:: return make_shared_nothrow(); } + auto is_dir = Filesystem::is_directory(dir_path); + if (!is_dir) { + std::cerr << "HailoRT warning: Cannot create log file " << filename << "! Path " << dir_path << " is not valid." << std::endl; + return make_shared_nothrow(); + } + if (!is_dir.value()) { + std::cerr << "HailoRT warning: Cannot create log file " << filename << "! Path " << dir_path << " is not a directory." << std::endl; + return make_shared_nothrow(); + } + if (!Filesystem::is_path_accesible(dir_path)) { - std::cerr << "HailoRT warning: Cannot create log file " << filename - << "! Please check the directory " << dir_path << " write permissions." << std::endl; - // Create null sink instead (Will throw away its log) + std::cerr << "HailoRT warning: Cannot create log file " << filename << "! Please check the directory " << dir_path << " write permissions." << std::endl; return make_shared_nothrow(); } const auto file_path = dir_path + PATH_SEPARATOR + filename; if (Filesystem::does_file_exists(file_path) && !Filesystem::is_path_accesible(file_path)) { - std::cerr << "HailoRT warning: Cannot create log file " << filename - << "! Please check the file " << file_path << " write permissions." << std::endl; - // Create null sink instead (Will throw away its log) + std::cerr << "HailoRT warning: Cannot create log file " << filename << "! Please check the file " << file_path << " write permissions." << std::endl; return make_shared_nothrow(); } @@ -162,7 +169,7 @@ std::shared_ptr HailoRTLogger::create_file_sink(const std:: return make_shared_nothrow(file_path); } -HailoRTLogger::HailoRTLogger() : +HailoRTLogger::HailoRTLogger(spdlog::level::level_enum console_level, spdlog::level::level_enum file_level, spdlog::level::level_enum flush_level) : m_console_sink(make_shared_nothrow()), #ifdef __ANDROID__ m_main_log_file_sink(make_shared_nothrow(HAILORT_NAME)) @@ -171,6 +178,10 @@ HailoRTLogger::HailoRTLogger() : m_local_log_file_sink(create_file_sink(get_log_path(HAILORT_LOGGER_PATH_ENV_VAR), HAILORT_LOGGER_FILENAME, true)) #endif { + if ((nullptr == m_console_sink) || (nullptr == m_main_log_file_sink) || (nullptr == m_local_log_file_sink)) { + std::cerr << "Allocating memory on heap for logger sinks has failed! Please check if this host has enough memory. Writing to log will result in a SEGFAULT!" << std::endl; + return; + } #ifdef __ANDROID__ m_main_log_file_sink->set_pattern(HAILORT_ANDROID_LOGGER_PATTERN); @@ -179,31 +190,26 @@ HailoRTLogger::HailoRTLogger() : m_local_log_file_sink->set_pattern(HAILORT_LOCAL_FILE_LOGGER_PATTERN); #endif - // TODO: Handle null pointers for logger and sinks m_console_sink->set_pattern(HAILORT_CONSOLE_LOGGER_PATTERN); spdlog::sinks_init_list sink_list = { m_console_sink, m_main_log_file_sink, m_local_log_file_sink }; m_hailort_logger = make_shared_nothrow(HAILORT_NAME, sink_list.begin(), sink_list.end()); + if (nullptr == m_hailort_logger) { + std::cerr << "Allocating memory on heap for HailoRT logger has failed! Please check if this host has enough memory. Writing to log will result in a SEGFAULT!" << std::endl; + return; + } -#ifdef NDEBUG - set_levels(spdlog::level::warn, spdlog::level::info, spdlog::level::warn); -#else - set_levels(spdlog::level::warn, spdlog::level::debug, spdlog::level::debug); -#endif + set_levels(console_level, file_level, flush_level); spdlog::set_default_logger(m_hailort_logger); } -std::shared_ptr HailoRTLogger::logger() -{ - return m_hailort_logger; -} - -void HailoRTLogger::set_levels(spdlog::level::level_enum console_level, - spdlog::level::level_enum file_level, spdlog::level::level_enum flush_level) +void HailoRTLogger::set_levels(spdlog::level::level_enum console_level, spdlog::level::level_enum file_level, + spdlog::level::level_enum flush_level) { m_console_sink->set_level(console_level); m_main_log_file_sink->set_level(file_level); m_local_log_file_sink->set_level(file_level); m_hailort_logger->flush_on(flush_level); + spdlog::flush_every(std::chrono::seconds(PERIODIC_LOGGER_FLUSH_TIME_IN_SECONDS)); } diff --git a/hailort/libhailort/src/utils/hailort_logger.hpp b/hailort/libhailort/src/utils/hailort_logger.hpp index 5552d00..c40047e 100644 --- a/hailort/libhailort/src/utils/hailort_logger.hpp +++ b/hailort/libhailort/src/utils/hailort_logger.hpp @@ -17,30 +17,40 @@ #include "hailo/hailort.h" #include "common/logger_macros.hpp" +#include "common/utils.hpp" namespace hailort { class HailoRTLogger { public: - static HailoRTLogger& get_instance() +#ifdef NDEBUG + static std::unique_ptr &get_instance(spdlog::level::level_enum console_level = spdlog::level::warn, + spdlog::level::level_enum file_level = spdlog::level::info, spdlog::level::level_enum flush_level = spdlog::level::warn) +#else + static std::unique_ptr &get_instance(spdlog::level::level_enum console_level = spdlog::level::warn, + spdlog::level::level_enum file_level = spdlog::level::debug, spdlog::level::level_enum flush_level = spdlog::level::debug) +#endif { - static HailoRTLogger instance; + static std::unique_ptr instance = nullptr; + if (nullptr == instance) { + instance = make_unique_nothrow(console_level, file_level, flush_level); + } return instance; } + + HailoRTLogger(spdlog::level::level_enum console_level, spdlog::level::level_enum file_level, spdlog::level::level_enum flush_level); + ~HailoRTLogger() = default; HailoRTLogger(HailoRTLogger const&) = delete; void operator=(HailoRTLogger const&) = delete; - std::shared_ptr logger(); - void set_levels(spdlog::level::level_enum console_level, spdlog::level::level_enum file_level, - spdlog::level::level_enum flush_level); static std::string get_log_path(const std::string &path_env_var); static std::string get_main_log_path(); static std::shared_ptr create_file_sink(const std::string &dir_path, const std::string &filename, bool rotate); private: - HailoRTLogger(); static std::string parse_log_path(const char *log_path); + void set_levels(spdlog::level::level_enum console_level, spdlog::level::level_enum file_level, spdlog::level::level_enum flush_level); std::shared_ptr m_console_sink; diff --git a/hailort/libhailort/src/utils/profiler/CMakeLists.txt b/hailort/libhailort/src/utils/profiler/CMakeLists.txt index f5c91fa..56ab701 100644 --- a/hailort/libhailort/src/utils/profiler/CMakeLists.txt +++ b/hailort/libhailort/src/utils/profiler/CMakeLists.txt @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.0.0) set(SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/tracer.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/scheduler_profiler_handler.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/monitor_handler.cpp ) set(HAILORT_CPP_SOURCES ${HAILORT_CPP_SOURCES} ${SRC_FILES} PARENT_SCOPE) diff --git a/hailort/libhailort/src/utils/profiler/handler.hpp b/hailort/libhailort/src/utils/profiler/handler.hpp new file mode 100644 index 0000000..cfd8f41 --- /dev/null +++ b/hailort/libhailort/src/utils/profiler/handler.hpp @@ -0,0 +1,199 @@ +/** + * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file handler.hpp + * @brief Handlers base class for HailoRT tracer mechanism + **/ + +#ifndef _HAILO_HANDLER_HPP_ +#define _HAILO_HANDLER_HPP_ + +#include "hailo/hailort.h" +#include "hailo/stream.hpp" + +#include "vdevice/scheduler/scheduler_base.hpp" + +namespace hailort +{ + +struct Trace +{ + Trace(const std::string &name) + : name(name) + {} + + virtual ~Trace() = default; + + uint64_t timestamp = 0; + std::string name; +}; + +struct InitTrace : Trace +{ + InitTrace() : Trace("init") {} +}; + +struct CoreOpIdleTrace : Trace +{ + CoreOpIdleTrace(const device_id_t &device_id, scheduler_core_op_handle_t core_op_handle) + : Trace("core_op_idle"), device_id(device_id), core_op_handle(core_op_handle) + {} + + device_id_t device_id; + scheduler_core_op_handle_t core_op_handle; +}; + +struct AddDeviceTrace : Trace +{ + AddDeviceTrace(const device_id_t &device_id, const std::string &device_arch) + : Trace("add_device_trace"), device_id(device_id), device_arch(device_arch) + {} + + device_id_t device_id; + std::string device_arch; +}; + +struct SchedulerStartTrace : Trace +{ + SchedulerStartTrace(uint32_t device_count) + : Trace("scheduler_start"), device_count(device_count) + {} + + uint32_t device_count = 0; +}; + +struct AddCoreOpTrace : Trace +{ + AddCoreOpTrace(const device_id_t &device_id, const std::string &core_op_name, uint64_t timeout, uint32_t threshold, scheduler_core_op_handle_t handle, + bool is_nms) + : Trace("add_core_op"), device_id(device_id), core_op_name(core_op_name), timeout(timeout), threshold(threshold), core_op_handle(handle), is_nms(is_nms) + {} + + device_id_t device_id; + std::string core_op_name; + uint64_t timeout = 0; + uint32_t threshold = 0; + scheduler_core_op_handle_t core_op_handle = INVALID_CORE_OP_HANDLE; + bool is_nms; +}; + +struct CreateCoreOpInputStreamsTrace : Trace +{ + CreateCoreOpInputStreamsTrace(const device_id_t &device_id, const std::string &core_op_name, const std::string &stream_name, uint32_t queue_size) + : Trace("create_input_stream"), device_id(device_id), core_op_name(core_op_name), stream_name(stream_name), queue_size(queue_size) + {} + + device_id_t device_id; + std::string core_op_name; + std::string stream_name; + uint32_t queue_size; +}; + +struct CreateCoreOpOutputStreamsTrace : Trace +{ + CreateCoreOpOutputStreamsTrace(const device_id_t &device_id, const std::string &core_op_name, const std::string &stream_name, uint32_t queue_size) + : Trace("create_output_stream"), device_id(device_id), core_op_name(core_op_name), stream_name(stream_name), queue_size(queue_size) + {} + + device_id_t device_id; + std::string core_op_name; + std::string stream_name; + uint32_t queue_size; +}; + +struct WriteFrameTrace : Trace +{ + WriteFrameTrace(const device_id_t &device_id, scheduler_core_op_handle_t core_op_handle, const std::string &queue_name) + : Trace("write_frame"), device_id(device_id), core_op_handle(core_op_handle), queue_name(queue_name) + {} + + device_id_t device_id; + scheduler_core_op_handle_t core_op_handle; + std::string queue_name; +}; + +struct InputVdmaDequeueTrace : Trace +{ + InputVdmaDequeueTrace(const device_id_t &device_id, scheduler_core_op_handle_t core_op_handle, const std::string &queue_name) + : Trace("input_vdma_dequeue"), device_id(device_id), core_op_handle(core_op_handle), queue_name(queue_name) + {} + + device_id_t device_id; + scheduler_core_op_handle_t core_op_handle; + std::string queue_name; +}; + +struct ReadFrameTrace : Trace +{ + ReadFrameTrace(const device_id_t &device_id, scheduler_core_op_handle_t core_op_handle, const std::string &queue_name) + : Trace("read_frame"), device_id(device_id), core_op_handle(core_op_handle), queue_name(queue_name) + {} + + std::string device_id; + scheduler_core_op_handle_t core_op_handle; + std::string queue_name; +}; + +struct OutputVdmaEnqueueTrace : Trace +{ + OutputVdmaEnqueueTrace(const device_id_t &device_id, scheduler_core_op_handle_t core_op_handle, const std::string &queue_name, uint32_t frames) + : Trace("output_vdma_enqueue"), device_id(device_id), core_op_handle(core_op_handle), queue_name(queue_name), frames(frames) + {} + + device_id_t device_id; + scheduler_core_op_handle_t core_op_handle; + std::string queue_name; + uint32_t frames = 0; +}; + +struct ChooseCoreOpTrace : Trace +{ + ChooseCoreOpTrace(const device_id_t &device_id, scheduler_core_op_handle_t handle, bool threshold, bool timeout, core_op_priority_t priority) + : Trace("choose_core_op"), device_id(device_id), core_op_handle(handle), threshold(threshold), timeout(timeout), priority(priority) + {} + + device_id_t device_id; + scheduler_core_op_handle_t core_op_handle; + bool threshold = false; + bool timeout = false; + core_op_priority_t priority; +}; + +struct SwitchCoreOpTrace : Trace +{ + SwitchCoreOpTrace(const device_id_t &device_id, scheduler_core_op_handle_t handle) + : Trace("switch_core_op"), device_id(device_id), core_op_handle(handle) + {} + + device_id_t device_id; + scheduler_core_op_handle_t core_op_handle; +}; + +class Handler +{ +public: + virtual ~Handler() = default; + + virtual void handle_trace(const InitTrace&) {}; + virtual void handle_trace(const AddCoreOpTrace&) {}; + virtual void handle_trace(const CreateCoreOpInputStreamsTrace&) {}; + virtual void handle_trace(const CreateCoreOpOutputStreamsTrace&) {}; + virtual void handle_trace(const WriteFrameTrace&) {}; + virtual void handle_trace(const InputVdmaDequeueTrace&) {}; + virtual void handle_trace(const ReadFrameTrace&) {}; + virtual void handle_trace(const OutputVdmaEnqueueTrace&) {}; + virtual void handle_trace(const ChooseCoreOpTrace&) {}; + virtual void handle_trace(const SwitchCoreOpTrace&) {}; + virtual void handle_trace(const SchedulerStartTrace&) {}; + virtual void handle_trace(const CoreOpIdleTrace&) {}; + virtual void handle_trace(const AddDeviceTrace&) {}; + +}; + +struct JSON; + +} + +#endif /* _HAILO_HANDLER_HPP */ \ No newline at end of file diff --git a/hailort/libhailort/src/utils/profiler/monitor_handler.cpp b/hailort/libhailort/src/utils/profiler/monitor_handler.cpp new file mode 100644 index 0000000..79ee27a --- /dev/null +++ b/hailort/libhailort/src/utils/profiler/monitor_handler.cpp @@ -0,0 +1,341 @@ +/** + * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file monitor_handler.cpp + * @brief Implementation of the scheduler monitor handlers base with HailoRT tracer mechanism + **/ + +#include "monitor_handler.hpp" + +#include "common/logger_macros.hpp" +#include "common/os_utils.hpp" + +namespace hailort +{ +MonitorHandler::MonitorHandler() +{} + +MonitorHandler::~MonitorHandler() +{ + clear_monitor(); +} + +void MonitorHandler::clear_monitor() { + + if (m_is_monitor_currently_working) { + m_is_monitor_currently_working = false; + m_mon_shutdown_event->signal(); + if (m_mon_thread.joinable()) { + m_mon_thread.join(); + } + } + m_devices_info.clear(); + m_core_ops_info.clear(); +} + +void MonitorHandler::handle_trace(const SchedulerStartTrace &trace) +{ + m_device_count = trace.device_count; + start_mon(); +} + +void MonitorHandler::handle_trace(const CoreOpIdleTrace &trace) +{ + update_utilization_read_buffers_finished(trace.device_id, trace.core_op_handle, true); +} + +void MonitorHandler::handle_trace(const AddCoreOpTrace &trace) +{ + m_core_ops_info[trace.core_op_handle].utilization = 0; + m_core_ops_info[trace.core_op_handle].core_op_name = trace.core_op_name; + m_core_ops_info[trace.core_op_handle].is_nms = trace.is_nms; +} + +void MonitorHandler::handle_trace(const AddDeviceTrace &trace) +{ + DeviceInfo device_info(trace.device_id, trace.device_arch); + m_devices_info.emplace(trace.device_id, device_info); +} + +void MonitorHandler::handle_trace(const SwitchCoreOpTrace &trace) +{ + assert(contains(m_devices_info, trace.device_id)); + m_devices_info.at(trace.device_id).current_core_op_handle = trace.core_op_handle; +} + +void MonitorHandler::handle_trace(const CreateCoreOpInputStreamsTrace &trace) +{ + // TODO- HRT-10371 'if' should be removed, this is temporary solution since this trace is called out of the scheduler. + if (!m_is_monitor_currently_working) { return; } + auto core_op_handle = get_core_op_handle_by_name(trace.core_op_name); + assert(contains(m_core_ops_info, core_op_handle)); + m_core_ops_info[core_op_handle].input_streams_info[trace.stream_name] = StreamsInfo{trace.queue_size, 0}; +} + +void MonitorHandler::handle_trace(const CreateCoreOpOutputStreamsTrace &trace) +{ + // TODO- HRT-10371 'if' should be removed, this is temporary solution since this trace is called out of the scheduler. + if (!m_is_monitor_currently_working) { return; } + auto core_op_handle = get_core_op_handle_by_name(trace.core_op_name); + assert(contains(m_core_ops_info, core_op_handle)); + m_core_ops_info[core_op_handle].output_streams_info[trace.stream_name] = StreamsInfo{trace.queue_size, 0}; +} + +void MonitorHandler::handle_trace(const WriteFrameTrace &trace) +{ + assert(contains(m_core_ops_info, trace.core_op_handle)); + assert(contains(m_core_ops_info[trace.core_op_handle].input_streams_info, trace.queue_name)); + m_core_ops_info[trace.core_op_handle].input_streams_info[trace.queue_name].pending_frames_count++; +} + +void MonitorHandler::handle_trace(const ReadFrameTrace &trace) +{ + assert(contains(m_core_ops_info, trace.core_op_handle)); + assert(contains(m_core_ops_info[trace.core_op_handle].output_streams_info, trace.queue_name)); + m_core_ops_info[trace.core_op_handle].output_streams_info[trace.queue_name].pending_frames_count--; + m_core_ops_info[trace.core_op_handle].output_streams_info[trace.queue_name].total_frames_count++; +} + +void MonitorHandler::handle_trace(const OutputVdmaEnqueueTrace &trace) +{ + assert(contains(m_core_ops_info, trace.core_op_handle)); + assert(contains(m_core_ops_info[trace.core_op_handle].output_streams_info, trace.queue_name)); + m_core_ops_info[trace.core_op_handle].output_streams_info[trace.queue_name].pending_frames_count += trace.frames; +} + +void MonitorHandler::handle_trace(const InputVdmaDequeueTrace &trace) +{ + assert(contains(m_core_ops_info, trace.core_op_handle)); + assert(contains(m_core_ops_info[trace.core_op_handle].input_streams_info, trace.queue_name)); + m_core_ops_info[trace.core_op_handle].input_streams_info[trace.queue_name].pending_frames_count--; + update_utilization_send_started(trace.device_id); +} + +scheduler_core_op_handle_t MonitorHandler::get_core_op_handle_by_name(const std::string &name) +{ + for (const auto &core_op_info : m_core_ops_info) { + if (0 == core_op_info.second.core_op_name.compare(name)) { + return core_op_info.first; + } + } + return INVALID_CORE_OP_HANDLE; +} + +hailo_status MonitorHandler::start_mon() +{ +#if defined(__GNUC__) + + /* Clearing monitor members. Since the owner of monitor_handler is tracer, which is static, + the monitor may get rerun without destructor being called. */ + if (m_is_monitor_currently_working) { + clear_monitor(); + } + m_is_monitor_currently_working = true; + + m_mon_shutdown_event = Event::create_shared(Event::State::not_signalled); + m_last_measured_timestamp = std::chrono::steady_clock::now(); + CHECK(nullptr != m_mon_shutdown_event, HAILO_OUT_OF_HOST_MEMORY); + + auto tmp_file = open_temp_mon_file(); + CHECK_EXPECTED_AS_STATUS(tmp_file); + m_mon_tmp_output = tmp_file.release(); + + m_mon_thread = std::thread([this] () + { + while (true) { + auto status = m_mon_shutdown_event->wait(DEFAULT_SCHEDULER_MON_INTERVAL); + if (HAILO_TIMEOUT == status) { + dump_state(); + } else if (HAILO_SUCCESS == status) { + break; // shutdown_event was signaled + } else if (HAILO_SUCCESS != status) { + LOGGER__ERROR("Scheduler monitor failed with status {}", status); + return; + } + } + return; + }); + + return HAILO_SUCCESS; +#else + return HAILO_NOT_IMPLEMENTED; +#endif +} + +std::string get_curr_pid_as_str() +{ + return std::to_string(OsUtils::get_curr_pid()); +} + +#if defined(__GNUC__) +Expected> MonitorHandler::open_temp_mon_file() +{ + std::string file_name = get_curr_pid_as_str(); + auto tmp_file = TempFile::create(file_name, SCHEDULER_MON_TMP_DIR); + CHECK_EXPECTED(tmp_file); + + auto tmp_file_ptr = make_shared_nothrow(tmp_file.release()); + CHECK_AS_EXPECTED(nullptr != tmp_file_ptr, HAILO_OUT_OF_HOST_MEMORY); + + return tmp_file_ptr; +} + +void MonitorHandler::dump_state() +{ + auto file = LockedFile::create(m_mon_tmp_output->name(), "w"); + if (HAILO_SUCCESS != file.status()) { + LOGGER__ERROR("Failed to open and lock file {}, with status: {}", m_mon_tmp_output->name(), file.status()); + return; + } + + ProtoMon mon; + mon.set_pid(get_curr_pid_as_str()); + time_dependent_events_cycle_calc(); + log_monitor_networks_infos(mon); + log_monitor_device_infos(mon); + log_monitor_frames_infos(mon); + + clear_accumulators(); + + if (!mon.SerializeToFileDescriptor(file->get_fd())) { + LOGGER__ERROR("Failed to SerializeToFileDescriptor(), with errno: {}", errno); + } +} +#endif + +void MonitorHandler::time_dependent_events_cycle_calc() +{ + auto curr_time = std::chrono::steady_clock::now(); + m_last_measured_time_duration = std::chrono::duration_cast>(curr_time - m_last_measured_timestamp).count(); + + for (auto &device : m_devices_info) { + if (!device.second.device_has_drained_everything) { + update_utilization_read_buffers_finished(device.second.device_id, device.second.current_core_op_handle, false); + } + } + m_last_measured_timestamp = curr_time; +} + +void MonitorHandler::log_monitor_device_infos(ProtoMon &mon) +{ + for (auto const &device_info_pair : m_devices_info) { + auto device_info = device_info_pair.second; + auto curr_device_utilization = device_info.device_utilization_duration; + auto utilization_percentage = ((curr_device_utilization * 100) / m_last_measured_time_duration); + + auto device_infos = mon.add_device_infos(); + device_infos->set_device_id(device_info.device_id); + device_infos->set_utilization(utilization_percentage); + device_infos->set_device_arch(device_info.device_arch); + } +} + +void MonitorHandler::log_monitor_networks_infos(ProtoMon &mon) +{ + for (uint32_t core_op_handle = 0; core_op_handle < m_core_ops_info.size(); core_op_handle++) { + auto curr_core_op_utilization = m_core_ops_info[core_op_handle].utilization; + auto utilization = ((curr_core_op_utilization * 100) / m_last_measured_time_duration); + double min_fps = std::numeric_limits::max(); + + for (auto const &stream : m_core_ops_info[core_op_handle].output_streams_info) { + double fps = stream.second.total_frames_count / m_last_measured_time_duration; + min_fps = (fps < min_fps) ? fps : min_fps; + } + + auto net_info = mon.add_networks_infos(); + net_info->set_network_name(m_core_ops_info[core_op_handle].core_op_name); + net_info->set_utilization(utilization); + net_info->set_fps(min_fps); + } +} + +void MonitorHandler::log_monitor_frames_infos(ProtoMon &mon) +{ + for (uint32_t core_op_handle = 0; core_op_handle < m_core_ops_info.size(); core_op_handle++) { + assert(contains(m_core_ops_info, core_op_handle)); + auto net_frames_info = mon.add_net_frames_infos(); + for (auto const &stream : m_core_ops_info[core_op_handle].input_streams_info) { + net_frames_info->set_network_name(m_core_ops_info[core_op_handle].core_op_name); + auto stream_frames_info = net_frames_info->add_streams_frames_infos(); + stream_frames_info->set_stream_name(stream.first); + stream_frames_info->set_stream_direction(PROTO__STREAM_DIRECTION__HOST_TO_DEVICE); + stream_frames_info->set_buffer_frames_size(static_cast(stream.second.queue_size * m_device_count)); + stream_frames_info->set_pending_frames_count(static_cast(stream.second.pending_frames_count)); + } + + for (auto const &stream : m_core_ops_info[core_op_handle].output_streams_info) { + net_frames_info->set_network_name(m_core_ops_info[core_op_handle].core_op_name); + auto stream_frames_info = net_frames_info->add_streams_frames_infos(); + stream_frames_info->set_stream_name(stream.first); + stream_frames_info->set_stream_direction(PROTO__STREAM_DIRECTION__DEVICE_TO_HOST); + if (m_core_ops_info[core_op_handle].is_nms) { + stream_frames_info->set_pending_frames_count(SCHEDULER_MON_NAN_VAL); + stream_frames_info->set_buffer_frames_size(SCHEDULER_MON_NAN_VAL); + } else { + stream_frames_info->set_pending_frames_count(static_cast(stream.second.pending_frames_count)); + stream_frames_info->set_buffer_frames_size(static_cast(stream.second.queue_size * m_device_count)); + } + } + } +} + +void MonitorHandler::update_utilization_timers(const device_id_t &device_id, scheduler_core_op_handle_t core_op_handle) +{ + assert(contains(m_core_ops_info, core_op_handle)); + assert(contains(m_devices_info, device_id)); + + auto time_diff = std::chrono::duration_cast>( + std::chrono::steady_clock::now() - m_devices_info.at(device_id).last_measured_utilization_timestamp).count(); + + m_devices_info.at(device_id).device_utilization_duration += time_diff; + m_core_ops_info[core_op_handle].utilization += time_diff; +} + +void MonitorHandler::update_utilization_timestamp(const device_id_t &device_id) +{ + assert(contains(m_devices_info, device_id)); + m_devices_info.at(device_id).last_measured_utilization_timestamp = std::chrono::steady_clock::now(); +} + +void MonitorHandler::update_utilization_send_started(const device_id_t &device_id) +{ + assert(contains(m_devices_info, device_id)); + if (m_devices_info.at(device_id).device_has_drained_everything) { + update_device_drained_state(device_id, false); + update_utilization_timestamp(device_id); + } +} + +void MonitorHandler::update_device_drained_state(const device_id_t &device_id, bool state) +{ + assert(contains(m_devices_info, device_id)); + m_devices_info.at(device_id).device_has_drained_everything = state; +} + +void MonitorHandler::update_utilization_read_buffers_finished(const device_id_t &device_id, + scheduler_core_op_handle_t core_op_handle, bool is_drained_everything) +{ + update_utilization_timers(device_id, core_op_handle); + update_device_drained_state(device_id, is_drained_everything); + if (!is_drained_everything) { + update_utilization_timestamp(device_id); + } +} + +void MonitorHandler::clear_accumulators() +{ + for (auto &device_info : m_devices_info) { + device_info.second.device_utilization_duration = 0; + } + + for (auto &handle_core_op_pair : m_core_ops_info) { + for (auto &handle_streams_pair : handle_core_op_pair.second.output_streams_info) { + handle_streams_pair.second.total_frames_count = 0; + } + handle_core_op_pair.second.utilization = 0; + } +} + +} \ No newline at end of file diff --git a/hailort/libhailort/src/utils/profiler/monitor_handler.hpp b/hailort/libhailort/src/utils/profiler/monitor_handler.hpp new file mode 100644 index 0000000..e6c188c --- /dev/null +++ b/hailort/libhailort/src/utils/profiler/monitor_handler.hpp @@ -0,0 +1,135 @@ +/** + * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file monitor_handler.hpp + * @brief Implementation of the scheduler monitor handlers base with HailoRT tracer mechanism + **/ + +#ifndef _HAILO_MONITOR_HANDLER_HPP_ +#define _HAILO_MONITOR_HANDLER_HPP_ + +#include "handler.hpp" + +#include "hailo/hailort.h" +#include "hailo/expected.hpp" +#include "hailo/event.hpp" + +#include "common/filesystem.hpp" +#include "common/utils.hpp" + +#include "vdevice/scheduler/scheduler_base.hpp" + +#include +#include +#include + +#if defined(_MSC_VER) +#pragma warning(push) +#pragma warning(disable: 4244 4267 4127) +#else +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wconversion" +#endif +#include "scheduler_mon.pb.h" +#if defined(_MSC_VER) +#pragma warning( pop ) +#else +#pragma GCC diagnostic pop +#endif + +namespace hailort +{ + +#define SCHEDULER_MON_TMP_DIR ("/tmp/hmon_files/") +#define SCHEDULER_MON_ENV_VAR ("HAILO_MONITOR") +#define DEFAULT_SCHEDULER_MON_INTERVAL (std::chrono::seconds(1)) +#define SCHEDULER_MON_NAN_VAL (-1) + +using stream_name = std::string; + +struct DeviceInfo { + DeviceInfo(const device_id_t &device_id, const std::string &device_arch) : + device_id(device_id), device_arch(device_arch), device_has_drained_everything(true), + device_utilization_duration(0), last_measured_utilization_timestamp(std::chrono::steady_clock::now()), + current_core_op_handle(INVALID_CORE_OP_HANDLE) + {} + std::string device_id; + std::string device_arch; + bool device_has_drained_everything; + double device_utilization_duration; + std::chrono::time_point last_measured_utilization_timestamp; + scheduler_core_op_handle_t current_core_op_handle; +}; + +struct StreamsInfo { + uint32_t queue_size; + uint32_t pending_frames_count; + uint32_t total_frames_count = 0; +}; + +struct CoreOpInfo { + std::unordered_map input_streams_info; + std::unordered_map output_streams_info; + std::string core_op_name; + bool is_nms; + double utilization; +}; + +class MonitorHandler : public Handler +{ +public: + MonitorHandler(MonitorHandler const&) = delete; + void operator=(MonitorHandler const&) = delete; + + MonitorHandler(); + ~MonitorHandler(); + void clear_monitor(); + + virtual void handle_trace(const AddCoreOpTrace&) override; + virtual void handle_trace(const CreateCoreOpInputStreamsTrace&) override; + virtual void handle_trace(const CreateCoreOpOutputStreamsTrace&) override; + virtual void handle_trace(const WriteFrameTrace&) override; + virtual void handle_trace(const ReadFrameTrace&) override; + virtual void handle_trace(const InputVdmaDequeueTrace&) override; + virtual void handle_trace(const OutputVdmaEnqueueTrace&) override; + virtual void handle_trace(const SwitchCoreOpTrace&) override; + virtual void handle_trace(const SchedulerStartTrace&) override; + virtual void handle_trace(const CoreOpIdleTrace&) override; + virtual void handle_trace(const AddDeviceTrace&) override; + +private: + hailo_status start_mon(); +#if defined(__GNUC__) + Expected> open_temp_mon_file(); + void dump_state(); +#endif + void time_dependent_events_cycle_calc(); + void log_monitor_device_infos(ProtoMon &mon); + void log_monitor_networks_infos(ProtoMon &mon); + void log_monitor_frames_infos(ProtoMon &mon); + void update_utilization_timers(const device_id_t &device_id, scheduler_core_op_handle_t core_op_handle); + void update_utilization_timestamp(const device_id_t &device_id); + void update_utilization_send_started(const device_id_t &device_id); + void update_device_drained_state(const device_id_t &device_id, bool state); + void update_utilization_read_buffers_finished(const device_id_t &device_id, scheduler_core_op_handle_t core_op_hanle, bool is_drained_everything); + void clear_accumulators(); + scheduler_core_op_handle_t get_core_op_handle_by_name(const std::string &name); + + bool m_is_monitor_currently_working = false; + uint32_t m_device_count; + std::thread m_mon_thread; + EventPtr m_mon_shutdown_event; +#if defined(__GNUC__) + std::shared_ptr m_mon_tmp_output; +#endif + std::chrono::time_point m_last_measured_timestamp; + double m_last_measured_time_duration; + // TODO: Consider adding Accumulator classes for more info (min, max, mean, etc..) + std::unordered_map m_core_ops_info; + std::unordered_map m_devices_info; +}; +} + +#endif /* _MONITOR_HANDLER_HPP_ */ \ No newline at end of file diff --git a/hailort/libhailort/src/utils/profiler/scheduler_profiler_handler.cpp b/hailort/libhailort/src/utils/profiler/scheduler_profiler_handler.cpp new file mode 100644 index 0000000..86bd76b --- /dev/null +++ b/hailort/libhailort/src/utils/profiler/scheduler_profiler_handler.cpp @@ -0,0 +1,219 @@ +/** + * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file scheduler_profiler_handler.cpp + * @brief Implementation of the scheduler profiler handlers base with HailoRT tracer mechanism + **/ + +#include "scheduler_profiler_handler.hpp" + +#include "common/logger_macros.hpp" + +#include "utils/hailort_logger.hpp" + +#include +#include +#include +#include + +#include +#include + +#define SCHEDULER_PROFILER_NAME ("SchedulerProfiler") +#define SCHEDULER_PROFILER_LOGGER_FILENAME ("scheduler_profiler.json") +#define SCHEDULER_PROFILER_LOGGER_PATTERN ("%v") + +#define SCHEDULER_PROFILER_LOGGER_PATH ("SCHEDULER_PROFILER_LOGGER_PATH") + +namespace hailort +{ + +SchedulerProfilerHandler::SchedulerProfilerHandler(int64_t &start_time) +#ifndef __ANDROID__ + : m_file_sink(HailoRTLogger::create_file_sink(HailoRTLogger::get_log_path(SCHEDULER_PROFILER_LOGGER_PATH), SCHEDULER_PROFILER_LOGGER_FILENAME, false)), + m_first_write(true) +#endif +{ +#ifndef __ANDROID__ + spdlog::sinks_init_list sink_list = { m_file_sink }; + m_profiler_logger = make_shared_nothrow(SCHEDULER_PROFILER_NAME, sink_list.begin(), sink_list.end()); + m_file_sink->set_level(spdlog::level::level_enum::info); + m_file_sink->set_pattern(SCHEDULER_PROFILER_LOGGER_PATTERN); + std::stringstream ss; + ss << "{\"ns_since_epoch_zero_time\": \"" << start_time << "\",\n\"scheduler_actions\": [\n"; + m_profiler_logger->info(ss.str()); +#else + (void)start_time; +#endif +} + +SchedulerProfilerHandler::~SchedulerProfilerHandler() +{ + m_profiler_logger->info("]\n}"); +} + +struct JSON +{ + std::unordered_map members; + JSON(const std::initializer_list> &dict) : members{dict} {} + JSON(const std::unordered_map &dict) { + for (auto &pair : dict) { + members.insert({pair.first, std::to_string(pair.second)}); + } + } +}; + +template +std::string json_to_string(const T &val) { + return std::to_string(val); +} + +template<> +std::string json_to_string(const std::string &val) { + std::ostringstream os; + os << std::quoted(val); + return os.str(); +} + +template<> +std::string json_to_string(const bool &bool_val) { + return bool_val ? "true" : "false"; +} + +template<> +std::string json_to_string(const JSON &json_val) { + std::ostringstream os; + os << "{\n"; + size_t i = 0; + for (const auto &kv : json_val.members) { + ++i; + os << std::quoted(kv.first) << " : "; + os << kv.second; + if (i != json_val.members.size()) { + os << ",\n"; + } + } + os << "\n}"; + return os.str(); +} + +bool SchedulerProfilerHandler::comma() +{ + auto result = !m_first_write; + m_first_write = false; + return result; +} + +void SchedulerProfilerHandler::log(JSON json) +{ + m_profiler_logger->info("{}{}", comma() ? ",\n" : "", json_to_string(json)); +} + +void SchedulerProfilerHandler::handle_trace(const AddCoreOpTrace &trace) +{ + log(JSON({ + {"action", json_to_string(trace.name)}, + {"timestamp", json_to_string(trace.timestamp)}, + {"device_id", json_to_string(trace.device_id)}, + {"core_op_name", json_to_string(trace.core_op_name)}, + {"core_op_handle", json_to_string(trace.core_op_handle)}, + {"timeout", json_to_string((uint64_t)trace.timeout)}, + {"threshold", json_to_string((uint64_t)trace.threshold)} + })); +} + +void SchedulerProfilerHandler::handle_trace(const CreateCoreOpInputStreamsTrace &trace) +{ + log(JSON({ + {"action", json_to_string(trace.name)}, + {"timestamp", json_to_string(trace.timestamp)}, + {"device_id", json_to_string(trace.device_id)}, + {"core_op_name", json_to_string(trace.core_op_name)}, + {"stream_name", json_to_string(trace.stream_name)}, + {"queue_size", json_to_string(trace.queue_size)} + })); +} + +void SchedulerProfilerHandler::handle_trace(const CreateCoreOpOutputStreamsTrace &trace) +{ + log(JSON({ + {"action", json_to_string(trace.name)}, + {"timestamp", json_to_string(trace.timestamp)}, + {"device_id", json_to_string(trace.device_id)}, + {"core_op_name", json_to_string(trace.core_op_name)}, + {"stream_name", json_to_string(trace.stream_name)}, + {"queue_size", json_to_string(trace.queue_size)} + })); +} + +void SchedulerProfilerHandler::handle_trace(const WriteFrameTrace &trace) +{ + log(JSON({ + {"action", json_to_string(trace.name)}, + {"timestamp", json_to_string(trace.timestamp)}, + {"device_id", json_to_string(trace.device_id)}, + {"core_op_handle", json_to_string(trace.core_op_handle)}, + {"queue_name", json_to_string(trace.queue_name)} + })); +} + +void SchedulerProfilerHandler::handle_trace(const InputVdmaDequeueTrace &trace) +{ + log(JSON({ + {"action", json_to_string(trace.name)}, + {"timestamp", json_to_string(trace.timestamp)}, + {"device_id", json_to_string(trace.device_id)}, + {"core_op_handle", json_to_string(trace.core_op_handle)}, + {"queue_name", json_to_string(trace.queue_name)} + })); +} + +void SchedulerProfilerHandler::handle_trace(const ReadFrameTrace &trace) +{ + log(JSON({ + {"action", json_to_string(trace.name)}, + {"timestamp", json_to_string(trace.timestamp)}, + {"device_id", json_to_string(trace.device_id)}, + {"core_op_handle", json_to_string(trace.core_op_handle)}, + {"queue_name", json_to_string(trace.queue_name)} + })); +} + +void SchedulerProfilerHandler::handle_trace(const OutputVdmaEnqueueTrace &trace) +{ + log(JSON({ + {"action", json_to_string(trace.name)}, + {"timestamp", json_to_string(trace.timestamp)}, + {"device_id", json_to_string(trace.device_id)}, + {"core_op_handle", json_to_string(trace.core_op_handle)}, + {"queue_name", json_to_string(trace.queue_name)}, + {"frames", json_to_string(trace.frames)} + })); +} + +void SchedulerProfilerHandler::handle_trace(const ChooseCoreOpTrace &trace) +{ + log(JSON({ + {"action", json_to_string(trace.name)}, + {"timestamp", json_to_string(trace.timestamp)}, + {"device_id", json_to_string(trace.device_id)}, + {"chosen_core_op_handle", json_to_string(trace.core_op_handle)}, + {"threshold", json_to_string(trace.threshold)}, + {"timeout", json_to_string(trace.timeout)}, + {"priority", json_to_string(trace.priority)} + })); +} + +void SchedulerProfilerHandler::handle_trace(const SwitchCoreOpTrace &trace) +{ + log(JSON({ + {"action", json_to_string(trace.name)}, + {"timestamp", json_to_string(trace.timestamp)}, + {"device_id", json_to_string(trace.device_id)}, + {"core_op_handle", json_to_string(trace.core_op_handle)} + })); +} + +} \ No newline at end of file diff --git a/hailort/libhailort/src/utils/profiler/scheduler_profiler_handler.hpp b/hailort/libhailort/src/utils/profiler/scheduler_profiler_handler.hpp new file mode 100644 index 0000000..24178ae --- /dev/null +++ b/hailort/libhailort/src/utils/profiler/scheduler_profiler_handler.hpp @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file scheduler_profiler_handler.hpp + * @brief Implementation of the scheduler profiler handlers base with HailoRT tracer mechanism + **/ + +#ifndef _HAILO_SCHEDULER_PROFILER_HANDLER_HPP_ +#define _HAILO_SCHEDULER_PROFILER_HANDLER_HPP_ + +#include "hailo/hailort.h" + +#include "handler.hpp" + +namespace hailort +{ +class SchedulerProfilerHandler : public Handler +{ +public: + SchedulerProfilerHandler(SchedulerProfilerHandler const&) = delete; + void operator=(SchedulerProfilerHandler const&) = delete; + + SchedulerProfilerHandler(int64_t &start_time); + ~SchedulerProfilerHandler(); + + virtual void handle_trace(const AddCoreOpTrace&) override; + virtual void handle_trace(const CreateCoreOpInputStreamsTrace&) override; + virtual void handle_trace(const CreateCoreOpOutputStreamsTrace&) override; + virtual void handle_trace(const WriteFrameTrace&) override; + virtual void handle_trace(const InputVdmaDequeueTrace&) override; + virtual void handle_trace(const ReadFrameTrace&) override; + virtual void handle_trace(const OutputVdmaEnqueueTrace&) override; + virtual void handle_trace(const ChooseCoreOpTrace&) override; + virtual void handle_trace(const SwitchCoreOpTrace&) override; + +private: + void log(JSON json); + bool comma(); + + std::shared_ptr m_file_sink; + std::shared_ptr m_profiler_logger; + std::atomic m_first_write; +}; + +} + +#endif /* _SCHEDULER_PROFILER_HANDLER_HPP_ */ \ No newline at end of file diff --git a/hailort/libhailort/src/utils/profiler/tracer.cpp b/hailort/libhailort/src/utils/profiler/tracer.cpp index 175f67f..14fce8f 100644 --- a/hailort/libhailort/src/utils/profiler/tracer.cpp +++ b/hailort/libhailort/src/utils/profiler/tracer.cpp @@ -5,28 +5,12 @@ /** * @file tracer.cpp * @brief: Tracing mechanism for HailoRT + FW events - * **/ #include "common/utils.hpp" -#include "utils/hailort_logger.hpp" #include "utils/profiler/tracer.hpp" -#include -#include -#include -#include -#include -#include - - -#define SCHEDULER_PROFILER_NAME ("SchedulerProfiler") -#define SCHEDULER_PROFILER_LOGGER_FILENAME ("scheduler_profiler.json") -#define SCHEDULER_PROFILER_LOGGER_PATTERN ("%v") - -#define SCHEDULER_PROFILER_LOGGER_PATH ("SCHEDULER_PROFILER_LOGGER_PATH") - #define PROFILER_ENV_VAR ("HAILO_ENABLE_PROFILER") namespace hailort @@ -34,8 +18,14 @@ namespace hailort Tracer::Tracer() { - auto should_trace_env = std::getenv(PROFILER_ENV_VAR); - m_should_trace = ((nullptr != should_trace_env) && (strnlen(should_trace_env, 2) == 1) && (strncmp(should_trace_env, "1", 1) == 0)); + init_scheduler_profiler_handler(); + init_monitor_handler(); +} + +void Tracer::init_scheduler_profiler_handler() +{ + const char* env_var_name = PROFILER_ENV_VAR; + m_should_trace = is_env_variable_on(env_var_name); if (m_should_trace) { m_start_time = std::chrono::high_resolution_clock::now(); int64_t time_since_epoch = std::chrono::duration_cast(m_start_time.time_since_epoch()).count(); @@ -43,191 +33,13 @@ Tracer::Tracer() } } -SchedulerProfilerHandler::SchedulerProfilerHandler(int64_t &start_time) -#ifndef __ANDROID__ - : m_file_sink(HailoRTLogger::create_file_sink(HailoRTLogger::get_log_path(SCHEDULER_PROFILER_LOGGER_PATH), SCHEDULER_PROFILER_LOGGER_FILENAME, false)), - m_first_write(true) -#endif +void Tracer::init_monitor_handler() { -#ifndef __ANDROID__ - spdlog::sinks_init_list sink_list = { m_file_sink }; - m_profiler_logger = make_shared_nothrow(SCHEDULER_PROFILER_NAME, sink_list.begin(), sink_list.end()); - m_file_sink->set_level(spdlog::level::level_enum::info); - m_file_sink->set_pattern(SCHEDULER_PROFILER_LOGGER_PATTERN); - std::stringstream ss; - ss << "{\"ns_since_epoch_zero_time\": \"" << start_time << "\",\n\"scheduler_actions\": [\n"; - m_profiler_logger->info(ss.str()); -#else - (void)start_time; -#endif -} - -SchedulerProfilerHandler::~SchedulerProfilerHandler() -{ - m_profiler_logger->info("]\n}"); -} - -struct JSON -{ - std::unordered_map members; - JSON(const std::initializer_list> &dict) : members{dict} {} - JSON(const std::unordered_map &dict) { - for (auto &pair : dict) { - members.insert({pair.first, std::to_string(pair.second)}); - } - } -}; - -template -std::string json_to_string(const T &val) { - return std::to_string(val); -} - -template<> -std::string json_to_string(const std::string &val) { - std::ostringstream os; - os << std::quoted(val); - return os.str(); -} - -template<> -std::string json_to_string(const bool &bool_val) { - return bool_val ? "true" : "false"; -} - -template<> -std::string json_to_string(const JSON &json_val) { - std::ostringstream os; - os << "{\n"; - size_t i = 0; - for (const auto &kv : json_val.members) { - ++i; - os << std::quoted(kv.first) << " : "; - os << kv.second; - if (i != json_val.members.size()) { - os << ",\n"; - } + const char* env_var_name = SCHEDULER_MON_ENV_VAR; + m_should_monitor = is_env_variable_on(env_var_name); + if (m_should_monitor) { + m_handlers.push_back(std::make_unique()); } - os << "\n}"; - return os.str(); } -bool SchedulerProfilerHandler::comma() -{ - auto result = !m_first_write; - m_first_write = false; - return result; -} - -void SchedulerProfilerHandler::log(JSON json) -{ - m_profiler_logger->info("{}{}", comma() ? ",\n" : "", json_to_string(json)); -} - -void SchedulerProfilerHandler::handle_trace(const AddCoreOpTrace &trace) -{ - log(JSON({ - {"action", json_to_string(trace.name)}, - {"timestamp", json_to_string(trace.timestamp)}, - {"device_id", json_to_string(trace.device_id)}, - {"core_op_name", json_to_string(trace.core_op_name)}, - {"core_op_handle", json_to_string(trace.core_op_handle)}, - {"timeout", json_to_string((uint64_t)trace.timeout)}, - {"threshold", json_to_string((uint64_t)trace.threshold)} - })); -} - -void SchedulerProfilerHandler::handle_trace(const CreateCoreOpInputStreamsTrace &trace) -{ - log(JSON({ - {"action", json_to_string(trace.name)}, - {"timestamp", json_to_string(trace.timestamp)}, - {"device_id", json_to_string(trace.device_id)}, - {"core_op_name", json_to_string(trace.core_op_name)}, - {"stream_name", json_to_string(trace.stream_name)}, - {"queue_size", json_to_string(trace.queue_size)} - })); -} - -void SchedulerProfilerHandler::handle_trace(const CreateCoreOpOutputStreamsTrace &trace) -{ - log(JSON({ - {"action", json_to_string(trace.name)}, - {"timestamp", json_to_string(trace.timestamp)}, - {"device_id", json_to_string(trace.device_id)}, - {"core_op_name", json_to_string(trace.core_op_name)}, - {"stream_name", json_to_string(trace.stream_name)}, - {"queue_size", json_to_string(trace.queue_size)} - })); -} - -void SchedulerProfilerHandler::handle_trace(const WriteFrameTrace &trace) -{ - log(JSON({ - {"action", json_to_string(trace.name)}, - {"timestamp", json_to_string(trace.timestamp)}, - {"device_id", json_to_string(trace.device_id)}, - {"core_op_handle", json_to_string(trace.core_op_handle)}, - {"queue_name", json_to_string(trace.queue_name)} - })); -} - -void SchedulerProfilerHandler::handle_trace(const InputVdmaDequeueTrace &trace) -{ - log(JSON({ - {"action", json_to_string(trace.name)}, - {"timestamp", json_to_string(trace.timestamp)}, - {"device_id", json_to_string(trace.device_id)}, - {"core_op_handle", json_to_string(trace.core_op_handle)}, - {"queue_name", json_to_string(trace.queue_name)} - })); -} - -void SchedulerProfilerHandler::handle_trace(const ReadFrameTrace &trace) -{ - log(JSON({ - {"action", json_to_string(trace.name)}, - {"timestamp", json_to_string(trace.timestamp)}, - {"device_id", json_to_string(trace.device_id)}, - {"core_op_handle", json_to_string(trace.core_op_handle)}, - {"queue_name", json_to_string(trace.queue_name)} - })); -} - -void SchedulerProfilerHandler::handle_trace(const OutputVdmaEnqueueTrace &trace) -{ - log(JSON({ - {"action", json_to_string(trace.name)}, - {"timestamp", json_to_string(trace.timestamp)}, - {"device_id", json_to_string(trace.device_id)}, - {"core_op_handle", json_to_string(trace.core_op_handle)}, - {"queue_name", json_to_string(trace.queue_name)}, - {"frames", json_to_string(trace.frames)} - })); -} - -void SchedulerProfilerHandler::handle_trace(const ChooseCoreOpTrace &trace) -{ - log(JSON({ - {"action", json_to_string(trace.name)}, - {"timestamp", json_to_string(trace.timestamp)}, - {"device_id", json_to_string(trace.device_id)}, - {"chosen_core_op_handle", json_to_string(trace.core_op_handle)}, - {"threshold", json_to_string(trace.threshold)}, - {"timeout", json_to_string(trace.timeout)}, - {"priority", json_to_string(trace.priority)} - })); -} - -void SchedulerProfilerHandler::handle_trace(const SwitchCoreOpTrace &trace) -{ - log(JSON({ - {"action", json_to_string(trace.name)}, - {"timestamp", json_to_string(trace.timestamp)}, - {"device_id", json_to_string(trace.device_id)}, - {"core_op_handle", json_to_string(trace.core_op_handle)} - })); -} - - } diff --git a/hailort/libhailort/src/utils/profiler/tracer.hpp b/hailort/libhailort/src/utils/profiler/tracer.hpp index 369079f..35036aa 100644 --- a/hailort/libhailort/src/utils/profiler/tracer.hpp +++ b/hailort/libhailort/src/utils/profiler/tracer.hpp @@ -11,193 +11,11 @@ #define _HAILO_TRACER_HPP_ #include "hailo/hailort.h" -#include "common/logger_macros.hpp" - -#include "vdevice/scheduler/scheduler_base.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - +#include "scheduler_profiler_handler.hpp" +#include "monitor_handler.hpp" namespace hailort { - -struct Trace -{ - Trace(const std::string &name) - : name(name) - {} - - virtual ~Trace() = default; - - uint64_t timestamp = 0; - std::string name; -}; - -struct InitTrace : Trace -{ - InitTrace() : Trace("init") {} -}; - -struct AddCoreOpTrace : Trace -{ - AddCoreOpTrace(const std::string &device_id, const std::string &core_op_name, uint64_t timeout, uint32_t threshold, scheduler_core_op_handle_t handle) - : Trace("add_core_op"), device_id(device_id), core_op_name(core_op_name), timeout(timeout), threshold(threshold), core_op_handle(handle) - {} - - std::string device_id; - std::string core_op_name; - uint64_t timeout = 0; - uint32_t threshold = 0; - scheduler_core_op_handle_t core_op_handle = INVALID_CORE_OP_HANDLE; -}; - -struct CreateCoreOpInputStreamsTrace : Trace -{ - CreateCoreOpInputStreamsTrace(const std::string &device_id, const std::string &core_op_name, const std::string &stream_name, uint32_t queue_size) - : Trace("create_input_stream"), device_id(device_id), core_op_name(core_op_name), stream_name(stream_name), queue_size(queue_size) - {} - - std::string device_id; - std::string core_op_name; - std::string stream_name; - uint32_t queue_size; -}; - -struct CreateCoreOpOutputStreamsTrace : Trace -{ - CreateCoreOpOutputStreamsTrace(const std::string &device_id, const std::string &core_op_name, const std::string &stream_name, uint32_t queue_size) - : Trace("create_output_stream"), device_id(device_id), core_op_name(core_op_name), stream_name(stream_name), queue_size(queue_size) - {} - - std::string device_id; - std::string core_op_name; - std::string stream_name; - uint32_t queue_size; -}; - -struct WriteFrameTrace : Trace -{ - WriteFrameTrace(const std::string &device_id, scheduler_core_op_handle_t core_op_handle, const std::string &queue_name) - : Trace("write_frame"), device_id(device_id), core_op_handle(core_op_handle), queue_name(queue_name) - {} - - std::string device_id; - scheduler_core_op_handle_t core_op_handle; - std::string queue_name; -}; - -struct InputVdmaDequeueTrace : Trace -{ - InputVdmaDequeueTrace(const std::string &device_id, scheduler_core_op_handle_t core_op_handle, const std::string &queue_name) - : Trace("input_vdma_dequeue"), device_id(device_id), core_op_handle(core_op_handle), queue_name(queue_name) - {} - - std::string device_id; - scheduler_core_op_handle_t core_op_handle; - std::string queue_name; -}; - -struct ReadFrameTrace : Trace -{ - ReadFrameTrace(const std::string &device_id, scheduler_core_op_handle_t core_op_handle, const std::string &queue_name) - : Trace("read_frame"), device_id(device_id), core_op_handle(core_op_handle), queue_name(queue_name) - {} - - std::string device_id; - scheduler_core_op_handle_t core_op_handle; - std::string queue_name; -}; - -struct OutputVdmaEnqueueTrace : Trace -{ - OutputVdmaEnqueueTrace(const std::string &device_id, scheduler_core_op_handle_t core_op_handle, const std::string &queue_name, uint32_t frames) - : Trace("output_vdma_enqueue"), device_id(device_id), core_op_handle(core_op_handle), queue_name(queue_name), frames(frames) - {} - - std::string device_id; - scheduler_core_op_handle_t core_op_handle; - std::string queue_name; - uint32_t frames = 0; -}; - -struct ChooseCoreOpTrace : Trace -{ - ChooseCoreOpTrace(const std::string &device_id, scheduler_core_op_handle_t handle, bool threshold, bool timeout, core_op_priority_t priority) - : Trace("choose_core_op"), device_id(device_id), core_op_handle(handle), threshold(threshold), timeout(timeout), priority(priority) - {} - - std::string device_id; - scheduler_core_op_handle_t core_op_handle; - bool threshold = false; - bool timeout = false; - core_op_priority_t priority; -}; - -struct SwitchCoreOpTrace : Trace -{ - SwitchCoreOpTrace(const std::string &device_id, scheduler_core_op_handle_t handle) - : Trace("switch_core_op"), device_id(device_id), core_op_handle(handle) - {} - - std::string device_id; - scheduler_core_op_handle_t core_op_handle; -}; - -class Handler -{ -public: - virtual ~Handler() = default; - - virtual void handle_trace(const InitTrace&) {}; - virtual void handle_trace(const AddCoreOpTrace&) {}; - virtual void handle_trace(const CreateCoreOpInputStreamsTrace&) {}; - virtual void handle_trace(const CreateCoreOpOutputStreamsTrace&) {}; - virtual void handle_trace(const WriteFrameTrace&) {}; - virtual void handle_trace(const InputVdmaDequeueTrace&) {}; - virtual void handle_trace(const ReadFrameTrace&) {}; - virtual void handle_trace(const OutputVdmaEnqueueTrace&) {}; - virtual void handle_trace(const ChooseCoreOpTrace&) {}; - virtual void handle_trace(const SwitchCoreOpTrace&) {}; -}; - -struct JSON; - -class SchedulerProfilerHandler : public Handler -{ -public: - SchedulerProfilerHandler(SchedulerProfilerHandler const&) = delete; - void operator=(SchedulerProfilerHandler const&) = delete; - - SchedulerProfilerHandler(int64_t &start_time); - ~SchedulerProfilerHandler(); - - virtual void handle_trace(const AddCoreOpTrace&) override; - virtual void handle_trace(const CreateCoreOpInputStreamsTrace&) override; - virtual void handle_trace(const CreateCoreOpOutputStreamsTrace&) override; - virtual void handle_trace(const WriteFrameTrace&) override; - virtual void handle_trace(const InputVdmaDequeueTrace&) override; - virtual void handle_trace(const ReadFrameTrace&) override; - virtual void handle_trace(const OutputVdmaEnqueueTrace&) override; - virtual void handle_trace(const ChooseCoreOpTrace&) override; - virtual void handle_trace(const SwitchCoreOpTrace&) override; - -private: - void log(JSON json); - bool comma(); - - std::shared_ptr m_file_sink; - std::shared_ptr m_profiler_logger; - std::atomic m_first_write; -}; - class Tracer { public: @@ -210,6 +28,8 @@ public: private: Tracer(); + void init_monitor_handler(); + void init_scheduler_profiler_handler(); static Tracer& get_instance() { @@ -220,7 +40,7 @@ private: template void execute_trace(Args... trace_args) { - if (!m_should_trace) { + if ((!m_should_trace) && (!m_should_monitor)) { return; } @@ -233,6 +53,7 @@ private: } bool m_should_trace = false; + bool m_should_monitor = false; std::chrono::high_resolution_clock::time_point m_start_time; std::vector> m_handlers; }; diff --git a/hailort/libhailort/src/utils/shared_resource_manager.hpp b/hailort/libhailort/src/utils/shared_resource_manager.hpp index 9dfdd30..687f66e 100644 --- a/hailort/libhailort/src/utils/shared_resource_manager.hpp +++ b/hailort/libhailort/src/utils/shared_resource_manager.hpp @@ -37,6 +37,7 @@ struct ResourceRef { std::shared_ptr resource; }; +// TODO: Merge ExportedResourceManager and SharedResourceManager (HRT-10317) template class SharedResourceManager { diff --git a/hailort/libhailort/src/utils/thread_safe_queue.hpp b/hailort/libhailort/src/utils/thread_safe_queue.hpp index 6d8646f..3be5f5c 100644 --- a/hailort/libhailort/src/utils/thread_safe_queue.hpp +++ b/hailort/libhailort/src/utils/thread_safe_queue.hpp @@ -15,8 +15,7 @@ #include "common/utils.hpp" #include "common/logger_macros.hpp" - -#include "utils/event_internal.hpp" +#include "common/event_internal.hpp" // Define __unix__ for inclusion of readerwriterqueue.h because readerwriterqueue is implemented over POSIX standards // but checks __unix__ - otherwise QNX returns unsupported platform (need HAILO_UNDEF_UNIX_FLAG in order to undefine diff --git a/hailort/libhailort/src/vdevice/CMakeLists.txt b/hailort/libhailort/src/vdevice/CMakeLists.txt index cacefd2..f9535d3 100644 --- a/hailort/libhailort/src/vdevice/CMakeLists.txt +++ b/hailort/libhailort/src/vdevice/CMakeLists.txt @@ -6,11 +6,14 @@ set(SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/pipeline_multiplexer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/vdevice_stream.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/vdevice_native_stream.cpp ${CMAKE_CURRENT_SOURCE_DIR}/vdevice_stream_multiplexer_wrapper.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/callback_reorder_queue.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/scheduler/network_group_scheduler.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/scheduler/scheduler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/scheduler/scheduler_oracle.cpp ${CMAKE_CURRENT_SOURCE_DIR}/scheduler/scheduled_core_op_state.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/scheduler/scheduled_stream.cpp ${CMAKE_CURRENT_SOURCE_DIR}/scheduler/multi_device_scheduled_stream.cpp ) diff --git a/hailort/libhailort/src/vdevice/callback_reorder_queue.cpp b/hailort/libhailort/src/vdevice/callback_reorder_queue.cpp new file mode 100644 index 0000000..d2b1b42 --- /dev/null +++ b/hailort/libhailort/src/vdevice/callback_reorder_queue.cpp @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file callback_reorder_queue.cpp + **/ + +#include "callback_reorder_queue.hpp" + +namespace hailort +{ + +InternalTransferDoneCallback CallbackReorderQueue::wrap_callback(const InternalTransferDoneCallback &original) +{ + std::lock_guard lock_guard(m_queue_mutex); + const uint64_t current_callback_index = m_registered_callbacks++; + + return [this, original, current_callback_index](hailo_status status) { + // Push callback without calling it yet. + push_callback(std::make_pair(current_callback_index, [original, status]() { + return original(status); + })); + + // Then, call the queued callbacks in order (if there is ready callback). + call_queued_callbacks_in_order(); + }; +} + +void CallbackReorderQueue::cancel_last_callback() +{ + std::lock_guard lock_guard(m_queue_mutex); + assert(m_called_callbacks < m_registered_callbacks); + m_registered_callbacks--; +} + +void CallbackReorderQueue::push_callback(const Callback &callback) +{ + std::lock_guard lock_guard(m_queue_mutex); + assert(m_callbacks_queue.size() < m_max_size); + m_callbacks_queue.push(callback); +} + +void CallbackReorderQueue::call_queued_callbacks_in_order() +{ + // Allow only one thread to execute the callbacks. + std::lock_guard callbacks_lock(m_callbacks_mutex); + + while (auto callback = pop_ready_callback()) { + callback->second(); + } +} + +Expected CallbackReorderQueue::pop_ready_callback() +{ + std::lock_guard lock_guard(m_queue_mutex); + + if (m_callbacks_queue.empty()) { + return make_unexpected(HAILO_NOT_AVAILABLE); + } + + if (m_callbacks_queue.top().first != m_called_callbacks) { + // We need to wait until top() contains callback with index - m_called_callbacks. + return make_unexpected(HAILO_NOT_AVAILABLE); + } + + auto next_callback = m_callbacks_queue.top(); + m_callbacks_queue.pop(); + + m_called_callbacks++; + return next_callback; +} + +} /* namespace hailort */ diff --git a/hailort/libhailort/src/vdevice/callback_reorder_queue.hpp b/hailort/libhailort/src/vdevice/callback_reorder_queue.hpp new file mode 100644 index 0000000..e5df53e --- /dev/null +++ b/hailort/libhailort/src/vdevice/callback_reorder_queue.hpp @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file callback_reorder_queue.hpp + * @brief When using multiple devices with async API, we may get interrupt for some input/output stream out of order + * (For example - the second device may be faster than the first). + * To ensure the order of the callbacks, we put the callbacks in queue and call them in the same order inserted. + **/ + +#ifndef _HAILO_CALLBACK_REORDER_QUEUE_HPP_ +#define _HAILO_CALLBACK_REORDER_QUEUE_HPP_ + +#include "stream_common/async_common.hpp" + +#include +#include + +namespace hailort +{ + +class CallbackReorderQueue final { +public: + CallbackReorderQueue(size_t max_size) : + m_max_size(max_size), + m_callbacks_queue(compare_callbacks{}, make_queue_storage(m_max_size)) + {} + + // Wraps the given original callback so it will be called in the same wrap_callback order. + InternalTransferDoneCallback wrap_callback(const InternalTransferDoneCallback &original); + + // If some wrapped callback wasn't registered to some async API (for example because the queue is full), we need to + // remove the counters we added in `wrap_callback` (otherwise, next callback will wait forever). + // Note! + // * Call this function only after a `wrap_callback` was called. + // * Make sure the wrapped callback will never be called! (Otherwise counters will loss syncronization). + void cancel_last_callback(); + +private: + // must be called with m_lock held + void call_queued_callbacks_in_order(); + + // Each callback has a function pointer and its index + using Callback = std::pair>; + + void push_callback(const Callback &callback); + + // Pop next callback ready to be called. Can return HAILO_NOT_AVAILABLE if there is no callback ready. + Expected pop_ready_callback(); + + // We don't want to have any memory allocations in runtime, so we init the priority queue with a reserved vector. + static std::vector make_queue_storage(size_t max_size) + { + std::vector storage; + storage.reserve(max_size); + return storage; + } + + const size_t m_max_size; + + // Guards access to m_callbacks_queue and the counters. + std::mutex m_queue_mutex; + + // Increasing counter for the index on next register callback. We don't worry about overflow (Even if we assume + // extreme value of 1,000,000 per second) + uint64_t m_registered_callbacks = 0; + + // Amount of callback that have called. Because the callbacks are called in order, this counter contains the index + // of the next callback expected to be executed. + uint64_t m_called_callbacks = 0; + + struct compare_callbacks { + bool operator()(const Callback &a, const Callback &b) + { + // We want to pop the lower index first + return a.first > b.first; + } + }; + + // Callbacks are stored inside a priority_queue data-structure. + // The queue is sorted by the callbacks index (so we pop the callbacks with the smallest index first). + std::priority_queue, compare_callbacks> m_callbacks_queue; + + // This lock guarantee that only one thread is executing the callbacks. + std::mutex m_callbacks_mutex; +}; + +} /* namespace hailort */ + +#endif /* _HAILO_CALLBACK_REORDER_QUEUE_HPP_ */ diff --git a/hailort/libhailort/src/vdevice/pipeline_multiplexer.cpp b/hailort/libhailort/src/vdevice/pipeline_multiplexer.cpp index 3526662..c60476c 100644 --- a/hailort/libhailort/src/vdevice/pipeline_multiplexer.cpp +++ b/hailort/libhailort/src/vdevice/pipeline_multiplexer.cpp @@ -28,9 +28,11 @@ PipelineMultiplexer::PipelineMultiplexer() : m_written_streams_count(0), m_read_streams_count(0), m_next_to_read_after_drain(INVALID_CORE_OP_HANDLE) -{} +{ + assert(is_multiplexer_supported()); +} -bool PipelineMultiplexer::should_use_multiplexer() +bool PipelineMultiplexer::is_multiplexer_supported() { auto disable_multiplexer_env = std::getenv(DISABLE_MULTIPLEXER_ENV_VAR); if ((nullptr != disable_multiplexer_env) && (strnlen(disable_multiplexer_env, 2) == 1) && (strncmp(disable_multiplexer_env, "1", 1) == 0)) { @@ -120,7 +122,7 @@ hailo_status PipelineMultiplexer::wait_for_write(multiplexer_core_op_handle_t co m_is_waiting_to_write[core_op_handle] = true; hailo_status status = HAILO_SUCCESS; m_writing_cv.wait(lock, [this, core_op_handle, &status] { - if (!has_more_than_one_core_op_instance() || !should_use_multiplexer()) { + if (!has_more_than_one_core_op_instance()) { return true; } @@ -213,7 +215,7 @@ Expected PipelineMultiplexer::wait_for_read(multiplexer_core_op_handle hailo_status status = HAILO_SUCCESS; auto wait_res = m_reading_cv.wait_for(lock, timeout, [this, core_op_handle, stream_name, &drain_frames, &status] { - if (should_core_op_stop(core_op_handle)) { + if (m_should_core_op_stop[core_op_handle][stream_name]) { status = HAILO_STREAM_ABORTED_BY_USER; return true; // return true so that the wait will finish } diff --git a/hailort/libhailort/src/vdevice/pipeline_multiplexer.hpp b/hailort/libhailort/src/vdevice/pipeline_multiplexer.hpp index e781697..e9223aa 100644 --- a/hailort/libhailort/src/vdevice/pipeline_multiplexer.hpp +++ b/hailort/libhailort/src/vdevice/pipeline_multiplexer.hpp @@ -15,7 +15,7 @@ #include "common/barrier.hpp" -#include "vdevice/scheduler/network_group_scheduler.hpp" +#include "vdevice/scheduler/scheduler.hpp" #include #include @@ -24,7 +24,7 @@ namespace hailort { -#define DISABLE_MULTIPLEXER_ENV_VAR "HAILO_DISABLE_MULTIPLEXER" +#define DISABLE_MULTIPLEXER_ENV_VAR "HAILO_DISABLE_MULTIPLEXER_INTERNAL" using multiplexer_core_op_handle_t = uint32_t; using run_once_for_stream_handle_t = uint32_t; @@ -58,7 +58,7 @@ public: void set_can_output_vstream_read(multiplexer_core_op_handle_t core_op_handle, const std::string &vstream_name, bool can_read); - static bool should_use_multiplexer(); + static bool is_multiplexer_supported(); private: diff --git a/hailort/libhailort/src/vdevice/scheduler/multi_device_scheduled_stream.cpp b/hailort/libhailort/src/vdevice/scheduler/multi_device_scheduled_stream.cpp index 7ae77e9..d8d236c 100644 --- a/hailort/libhailort/src/vdevice/scheduler/multi_device_scheduled_stream.cpp +++ b/hailort/libhailort/src/vdevice/scheduler/multi_device_scheduled_stream.cpp @@ -14,7 +14,29 @@ namespace hailort { -hailo_status MultiDeviceScheduledInputStream::send_pending_buffer(size_t device_index) +Expected> MultiDeviceScheduledInputStream::create( + std::map> &&streams, + const scheduler_core_op_handle_t &core_op_handle, + EventPtr &&core_op_activated_event, + const LayerInfo &layer_info, + CoreOpsSchedulerWeakPtr core_ops_scheduler) +{ + auto buffer_frame_size = streams.begin()->second.get().get_buffer_frames_size(); + CHECK_EXPECTED(buffer_frame_size); + auto frame_size = streams.begin()->second.get().get_frame_size(); + auto buffers_queue_ptr = BuffersQueue::create_unique(frame_size, (streams.size() * buffer_frame_size.value())); + CHECK_EXPECTED(buffers_queue_ptr); + + auto status = HAILO_UNINITIALIZED; + auto stream = make_unique_nothrow(std::move(streams), + core_op_handle, std::move(core_op_activated_event), layer_info, + core_ops_scheduler, buffers_queue_ptr.release(), status); + CHECK_AS_EXPECTED((nullptr != stream), HAILO_OUT_OF_HOST_MEMORY); + CHECK_SUCCESS_AS_EXPECTED(status); + return stream; +} + +hailo_status MultiDeviceScheduledInputStream::send_pending_buffer(const device_id_t &device_id) { auto buffer = m_queue->front(get_timeout()); // Counting on scheduler to not allow paralle calls to this function if (HAILO_STREAM_ABORTED_BY_USER == buffer.status()) { @@ -22,7 +44,8 @@ hailo_status MultiDeviceScheduledInputStream::send_pending_buffer(size_t device_ return buffer.status(); } CHECK_EXPECTED_AS_STATUS(buffer); - auto status = m_streams[device_index].get().write_buffer_only(buffer.value()); + assert(contains(m_streams, device_id)); + auto status = m_streams.at(device_id).get().write_buffer_only(buffer.value()); if (HAILO_STREAM_ABORTED_BY_USER == status) { LOGGER__INFO("send_pending_buffer was aborted."); return status; @@ -30,38 +53,34 @@ hailo_status MultiDeviceScheduledInputStream::send_pending_buffer(size_t device_ CHECK_SUCCESS(status); m_queue->pop(); // Release buffer to free the queue for other dequeues - VdmaInputStream &vdma_input = static_cast(m_streams[device_index].get()); - return vdma_input.send_pending_buffer(); + auto &vdma_input = dynamic_cast(m_streams.at(device_id).get()); + return vdma_input.send_pending_buffer(device_id); } -Expected MultiDeviceScheduledInputStream::sync_write_raw_buffer(const MemoryView &buffer, +hailo_status MultiDeviceScheduledInputStream::write_impl(const MemoryView &buffer, const std::function &should_cancel) { - auto core_ops_scheduler = m_core_ops_scheduler.lock(); - CHECK_AS_EXPECTED(core_ops_scheduler, HAILO_INTERNAL_FAILURE); - - auto status = core_ops_scheduler->wait_for_write(m_core_op_handle, name(), get_timeout(), should_cancel); - if (HAILO_STREAM_ABORTED_BY_USER == status) { - LOGGER__INFO("Write to stream was aborted."); - return make_unexpected(status); + if (should_cancel()) { + return HAILO_STREAM_ABORTED_BY_USER; } - CHECK_SUCCESS_AS_EXPECTED(status); - status = m_queue->push(buffer, get_timeout()); + auto core_ops_scheduler = m_core_ops_scheduler.lock(); + CHECK(core_ops_scheduler, HAILO_INTERNAL_FAILURE); - auto write_finish_status = core_ops_scheduler->signal_write_finish(m_core_op_handle, name(), status != HAILO_SUCCESS); + auto status = m_queue->push(buffer, get_timeout()); if (HAILO_STREAM_ABORTED_BY_USER == status) { LOGGER__INFO("'push' was aborted."); - return make_unexpected(status); + return status; } - CHECK_SUCCESS_AS_EXPECTED(status); + CHECK_SUCCESS(status); + auto write_finish_status = core_ops_scheduler->signal_frame_pending_to_send(m_core_op_handle, name()); if (HAILO_STREAM_ABORTED_BY_USER == write_finish_status) { - return make_unexpected(write_finish_status); + return write_finish_status; } - CHECK_SUCCESS_AS_EXPECTED(write_finish_status); + CHECK_SUCCESS(write_finish_status); - return buffer.size(); + return HAILO_SUCCESS; } Expected MultiDeviceScheduledInputStream::get_pending_frames_count() const @@ -77,7 +96,8 @@ size_t MultiDeviceScheduledInputStream::get_queue_size() const hailo_status MultiDeviceScheduledInputStream::abort() { auto status = HAILO_SUCCESS; // Best effort - for (auto &stream : m_streams) { + for (const auto &pair : m_streams) { + auto &stream = pair.second; auto abort_status = stream.get().abort(); if (HAILO_SUCCESS != status) { LOGGER__ERROR("Failed to abort input stream. (status: {} device: {})", status, stream.get().get_dev_id()); @@ -101,7 +121,8 @@ hailo_status MultiDeviceScheduledInputStream::abort() hailo_status MultiDeviceScheduledInputStream::clear_abort() { auto status = HAILO_SUCCESS; // Best effort - for (auto &stream : m_streams) { + for (const auto &pair : m_streams) { + auto &stream = pair.second; auto clear_abort_status = stream.get().clear_abort(); if ((HAILO_SUCCESS != clear_abort_status) && (HAILO_STREAM_NOT_ACTIVATED != clear_abort_status)) { LOGGER__ERROR("Failed to clear abort input stream. (status: {} device: {})", clear_abort_status, stream.get().get_dev_id()); diff --git a/hailort/libhailort/src/vdevice/scheduler/multi_device_scheduled_stream.hpp b/hailort/libhailort/src/vdevice/scheduler/multi_device_scheduled_stream.hpp index ac8fe41..63eadf8 100644 --- a/hailort/libhailort/src/vdevice/scheduler/multi_device_scheduled_stream.hpp +++ b/hailort/libhailort/src/vdevice/scheduler/multi_device_scheduled_stream.hpp @@ -151,27 +151,35 @@ private: std::atomic_bool m_should_stop; }; -class MultiDeviceScheduledInputStream : public ScheduledInputStream { +// Stream used on scheduler input multiple device with SYNC api (On async api, the ScheduledAsyncInputStream handles +// both single and multiple devices). +class MultiDeviceScheduledInputStream : public ScheduledInputStreamBase { public: + static Expected> create( + std::map> &&streams, + const scheduler_core_op_handle_t &core_op_handle, + EventPtr &&core_op_activated_event, + const LayerInfo &layer_info, + CoreOpsSchedulerWeakPtr core_ops_scheduler); + MultiDeviceScheduledInputStream( - std::vector> &&streams, + std::map> &&streams, const scheduler_core_op_handle_t &core_op_handle, EventPtr &&core_op_activated_event, const LayerInfo &layer_info, CoreOpsSchedulerWeakPtr core_ops_scheduler, std::unique_ptr &&frames_queue, hailo_status &status) : - ScheduledInputStream(std::move(streams), core_op_handle, + ScheduledInputStreamBase(std::move(streams), core_op_handle, std::move(core_op_activated_event), layer_info, core_ops_scheduler, status), m_queue(std::move(frames_queue)) {} - virtual hailo_status send_pending_buffer(size_t device_index = 0) override; + virtual hailo_status send_pending_buffer(const device_id_t &device_id) override; virtual Expected get_pending_frames_count() const override; protected: - virtual Expected sync_write_raw_buffer(const MemoryView &buffer, - const std::function &should_cancel = []() { return false; }) override; + virtual hailo_status write_impl(const MemoryView &buffer, const std::function &should_cancel) override; virtual hailo_status abort() override; virtual hailo_status clear_abort() override; diff --git a/hailort/libhailort/src/vdevice/scheduler/network_group_scheduler.cpp b/hailort/libhailort/src/vdevice/scheduler/network_group_scheduler.cpp deleted file mode 100644 index cc7ace8..0000000 --- a/hailort/libhailort/src/vdevice/scheduler/network_group_scheduler.cpp +++ /dev/null @@ -1,1006 +0,0 @@ -/** - * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. - * Distributed under the MIT license (https://opensource.org/licenses/MIT) - **/ -/** - * TODO: Rename in a different PR - * @file network_group_scheduler.cpp - * @brief: Network scheduler - **/ - -#include "common/os_utils.hpp" - - -#include "vdevice/scheduler/network_group_scheduler.hpp" -#include "vdevice/vdevice_core_op.hpp" -#include "vdevice/scheduler/scheduler_oracle.hpp" -#include "vdevice/vdevice_stream_multiplexer_wrapper.hpp" -#include "hef/hef_internal.hpp" -#include "utils/profiler/tracer_macros.hpp" - -#include - - -namespace hailort -{ - -#define SINGLE_CONTEXT_BATCH_SIZE (1) -#define DEFAULT_BURST_SIZE (1) - -// TODO: use device handles instead device count -CoreOpsScheduler::CoreOpsScheduler(hailo_scheduling_algorithm_t algorithm, uint32_t device_count, std::vector &devices_bdf_id, - std::vector &devices_arch) : - SchedulerBase(algorithm, device_count, devices_bdf_id, devices_arch), - m_changing_current_batch_size(), - m_should_core_op_stop(), - m_before_read_write_mutex(), - m_core_ops_cvs(), - m_should_monitor(false) -#if defined(__GNUC__) - , m_mon_tmp_output() -#endif -{ - // TODO: HRT-7391 - Change scheduler monitor to work only when MON command is active - m_should_monitor = SchedulerMon::should_monitor(); - if (m_should_monitor) { - auto status = start_mon(); - if (HAILO_SUCCESS != status) { - LOGGER__ERROR("Failed to initiate hailo monitor of networks, with status {}", status); - } - } -} - -CoreOpsScheduler::~CoreOpsScheduler() -{ - for (auto device_info : m_devices) { - if (INVALID_CORE_OP_HANDLE != device_info->current_core_op_handle) { - auto current_core_op = m_scheduled_core_ops[device_info->current_core_op_handle]->get_core_op(); - auto current_core_op_bundle = std::dynamic_pointer_cast(current_core_op); - assert(nullptr != current_core_op_bundle); - auto vdma_core_op = current_core_op_bundle->get_core_op_by_device_index(device_info->device_id); - if (!vdma_core_op) { - LOGGER__ERROR("Error retrieving core-op in scheduler destructor"); - } else { - static const auto RESUME_PENDING_STREAM_TRANSFERS = true; - if (HAILO_SUCCESS != VdmaConfigManager::switch_core_op(vdma_core_op.value(), nullptr, 0, - RESUME_PENDING_STREAM_TRANSFERS)) { - LOGGER__ERROR("Error deactivating core-op when destroying scheduler"); - } - } - } - } - - if (m_should_monitor) { - m_should_monitor = false; - m_mon_shutdown_event->signal(); - if (m_mon_thread.joinable()) { - m_mon_thread.join(); - } - } -} - -Expected CoreOpsScheduler::create_round_robin(uint32_t device_count, std::vector &devices_bdf_id, std::vector &devices_arch) -{ - auto ptr = make_shared_nothrow(HAILO_SCHEDULING_ALGORITHM_ROUND_ROBIN, device_count, devices_bdf_id, devices_arch); - CHECK_AS_EXPECTED(nullptr != ptr, HAILO_OUT_OF_HOST_MEMORY); - - return ptr; -} - -std::string get_curr_pid_as_str() -{ - return std::to_string(OsUtils::get_curr_pid()); -} - -hailo_status CoreOpsScheduler::start_mon() -{ -#if defined(__GNUC__) - m_last_measured_timestamp = std::chrono::steady_clock::now(); - m_mon_shutdown_event = Event::create_shared(Event::State::not_signalled); - CHECK(nullptr != m_mon_shutdown_event, HAILO_OUT_OF_HOST_MEMORY); - auto device_count = get_device_count(); - for (uint32_t i = 0; i < device_count; i++) { - m_last_measured_utilization_timestamp[i] = {}; - m_device_has_drained_everything[i] = true; - m_device_utilization[i] = 0; - } - - auto tmp_file = open_temp_mon_file(); - CHECK_EXPECTED_AS_STATUS(tmp_file); - m_mon_tmp_output = tmp_file.release(); - - m_mon_thread = std::thread([this] () - { - while (m_should_monitor) { - auto status = m_mon_shutdown_event->wait(DEFAULT_SCHEDULER_MON_INTERVAL); - if (HAILO_TIMEOUT == status) { - dump_state(); - } else if (HAILO_SUCCESS != status) { - LOGGER__ERROR("Scheduler monitor failed with status {}", status); - return; - } - } - return; - }); - - return HAILO_SUCCESS; -#else - return HAILO_NOT_IMPLEMENTED; -#endif -} - -#if defined(__GNUC__) -Expected> CoreOpsScheduler::open_temp_mon_file() -{ - std::string file_name = get_curr_pid_as_str(); - auto tmp_file = TempFile::create(file_name, SCHEDULER_MON_TMP_DIR); - CHECK_EXPECTED(tmp_file); - - auto tmp_file_ptr = make_shared_nothrow(tmp_file.release()); - CHECK_AS_EXPECTED(nullptr != tmp_file_ptr, HAILO_OUT_OF_HOST_MEMORY); - - return tmp_file_ptr; -} - -void CoreOpsScheduler::dump_state() -{ - auto file = LockedFile::create(m_mon_tmp_output->name(), "w"); - if (HAILO_SUCCESS != file.status()) { - LOGGER__ERROR("Failed to open and lock file {}, with status: {}", m_mon_tmp_output->name(), file.status()); - return; - } - - ProtoMon mon; - mon.set_pid(get_curr_pid_as_str()); - time_dependent_events_cycle_calc(); - log_monitor_networks_infos(mon); - log_monitor_device_infos(mon); - log_monitor_frames_infos(mon); - - // Clear accumulators - for (auto &handle_core_op_utilization_pair : m_core_op_utilization) { - handle_core_op_utilization_pair.second = 0; - } - for (auto &handle_fps_pair : m_fps_accumulator) { - handle_fps_pair.second = 0; - } - for (auto &handle_device_utilization_pair: m_device_utilization) { - handle_device_utilization_pair.second = 0; - } - - if (!mon.SerializeToFileDescriptor(file->get_fd())) { - LOGGER__ERROR("Failed to SerializeToFileDescriptor(), with errno: {}", errno); - } -} -#endif - -std::string CoreOpsScheduler::get_core_op_name(const scheduler_core_op_handle_t &core_op_handle) -{ - assert(m_scheduled_core_ops.size() > core_op_handle); - return m_scheduled_core_ops[core_op_handle]->get_core_op_name(); -} - -// TODO: HRT-9804 - Change monitor to use the tracer design mechanism (curently this functions uses private members) -void CoreOpsScheduler::time_dependent_events_cycle_calc() -{ - auto curr_time = std::chrono::steady_clock::now(); - m_last_measured_time_duration = std::chrono::duration_cast>(curr_time - m_last_measured_timestamp).count(); - - for (auto device_info : m_devices) { - if (!m_device_has_drained_everything[device_info->device_id]) { - update_utilization_read_buffers_finished(device_info->device_id, device_info->current_core_op_handle, false); - } - } - - m_last_measured_timestamp = curr_time; -} - -void CoreOpsScheduler::log_monitor_device_infos(ProtoMon &mon) -{ - for (auto device_info : m_devices) { - assert(contains(m_device_utilization, device_info->device_id)); - auto curr_device_utilization = m_device_utilization[device_info->device_id]; - auto utilization_precentage = ((curr_device_utilization * 100) / m_last_measured_time_duration); - - auto device_infos = mon.add_device_infos(); - device_infos->set_device_id(device_info->device_bdf_id); - device_infos->set_utilization(utilization_precentage); - device_infos->set_device_arch(device_info->device_arch); - } -} - -void CoreOpsScheduler::log_monitor_networks_infos(ProtoMon &mon) -{ - for (uint32_t core_op_handle = 0; core_op_handle < m_core_op_utilization.size(); core_op_handle++) { - assert(contains(m_core_op_utilization, core_op_handle)); - auto curr_core_op_utilization = m_core_op_utilization[core_op_handle]; - auto utilization = ((curr_core_op_utilization * 100) / m_last_measured_time_duration); - auto outputs_count = static_cast(m_scheduled_core_ops[core_op_handle]->get_outputs_names().size()); - auto fps = static_cast((m_fps_accumulator[core_op_handle] / outputs_count) / m_last_measured_time_duration); - - auto net_info = mon.add_networks_infos(); - net_info->set_network_name(get_core_op_name(core_op_handle)); - net_info->set_utilization(utilization); - net_info->set_fps(fps); - } -} - -void CoreOpsScheduler::log_monitor_frames_infos(ProtoMon &mon) -{ - for (uint32_t core_op_handle = 0; core_op_handle < m_scheduled_core_ops.size(); core_op_handle++) { - auto net_frames_info = mon.add_net_frames_infos(); - net_frames_info->set_network_name(get_core_op_name(core_op_handle)); - - for (auto &stream_name : m_scheduled_core_ops[core_op_handle]->get_inputs_names()) { - auto stream_frames_info = net_frames_info->add_streams_frames_infos(); - stream_frames_info->set_stream_name(stream_name); - stream_frames_info->set_stream_direction(PROTO__STREAM_DIRECTION__HOST_TO_DEVICE); - auto status = set_h2d_frames_counters(core_op_handle, stream_name, *stream_frames_info); - if (HAILO_SUCCESS != status) { - LOGGER__ERROR("Failed to set stream's {} frames count, status = {}", stream_name, status); - continue; - } - } - - for (auto &stream_name : m_scheduled_core_ops[core_op_handle]->get_outputs_names()) { - auto stream_frames_info = net_frames_info->add_streams_frames_infos(); - stream_frames_info->set_stream_name(stream_name); - stream_frames_info->set_stream_direction(PROTO__STREAM_DIRECTION__DEVICE_TO_HOST); - auto status = set_d2h_frames_counters(core_op_handle, stream_name, *stream_frames_info); - if (HAILO_SUCCESS != status) { - LOGGER__ERROR("Failed to set stream's {} frames count, status = {}", stream_name, status); - continue; - } - } - } -} - -hailo_status CoreOpsScheduler::set_h2d_frames_counters(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, - ProtoMonStreamFramesInfo &stream_frames_info) -{ - assert(m_scheduled_core_ops.size() > core_op_handle); - auto current_cng = m_scheduled_core_ops[core_op_handle]->get_core_op(); - - auto input_stream = current_cng->get_input_stream_by_name(stream_name); - CHECK_EXPECTED_AS_STATUS(input_stream); - - InputStreamBase &vdevice_input = static_cast(input_stream->get()); - auto buffer_frames_size = vdevice_input.get_buffer_frames_size(); - if (HAILO_SUCCESS == buffer_frames_size.status()) { - stream_frames_info.set_buffer_frames_size(static_cast(buffer_frames_size.value())); - } else { - stream_frames_info.set_buffer_frames_size(SCHEDULER_MON_NAN_VAL); - } - - auto pending_frames_count = vdevice_input.get_pending_frames_count(); - if (HAILO_SUCCESS == pending_frames_count.status()) { - stream_frames_info.set_pending_frames_count(static_cast(pending_frames_count.value())); - } else { - stream_frames_info.set_pending_frames_count(SCHEDULER_MON_NAN_VAL); - } - - return HAILO_SUCCESS; -} - -hailo_status CoreOpsScheduler::set_d2h_frames_counters(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, - ProtoMonStreamFramesInfo &stream_frames_info) -{ - assert(m_scheduled_core_ops.size() > core_op_handle); - auto current_cng = m_scheduled_core_ops[core_op_handle]->get_core_op(); - - auto output_stream = current_cng->get_output_stream_by_name(stream_name); - CHECK_EXPECTED_AS_STATUS(output_stream); - - OutputStreamBase &vdevice_output = static_cast(output_stream->get()); - auto buffer_frames_size = vdevice_output.get_buffer_frames_size(); - if (HAILO_SUCCESS == buffer_frames_size.status()) { - stream_frames_info.set_buffer_frames_size(static_cast(buffer_frames_size.value())); - } else { - stream_frames_info.set_buffer_frames_size(SCHEDULER_MON_NAN_VAL); - } - - auto pending_frames_count = vdevice_output.get_pending_frames_count(); - if (HAILO_SUCCESS == pending_frames_count.status()) { - stream_frames_info.set_pending_frames_count(static_cast(pending_frames_count.value())); - } else { - stream_frames_info.set_pending_frames_count(SCHEDULER_MON_NAN_VAL); - } - - return HAILO_SUCCESS; -} - -Expected CoreOpsScheduler::add_core_op(std::shared_ptr added_cng) -{ - scheduler_core_op_handle_t core_op_handle = INVALID_CORE_OP_HANDLE; - { - std::unique_lock lock(m_before_read_write_mutex); - - core_op_handle = static_cast(m_scheduled_core_ops.size()); - TRACE(AddCoreOpTrace, "", added_cng->name(), DEFAULT_SCHEDULER_TIMEOUT.count(), DEFAULT_SCHEDULER_MIN_THRESHOLD, core_op_handle); - - auto stream_infos = added_cng->get_all_stream_infos(); - CHECK_EXPECTED(stream_infos); - - auto scheduled_core_op = ScheduledCoreOp::create(added_cng, stream_infos.value()); - CHECK_EXPECTED(scheduled_core_op); - - m_scheduled_core_ops.emplace_back(scheduled_core_op.release()); - - m_changing_current_batch_size[core_op_handle] = false; - - for (const auto &stream_info : stream_infos.value()) { - m_should_core_op_stop[core_op_handle][stream_info.name] = false; - } - - for (auto& device_info : m_devices) { - for (const auto &stream_info : stream_infos.value()) { - if (HAILO_H2D_STREAM == stream_info.direction) { - device_info->current_cycle_requested_transferred_frames_h2d[core_op_handle][stream_info.name] = 0; - } else { - device_info->current_cycle_finished_transferred_frames_d2h[core_op_handle][stream_info.name] = 0; - device_info->current_cycle_finished_read_frames_d2h[core_op_handle][stream_info.name] = 0; - } - } - } - - // Monitor members - m_core_op_utilization[core_op_handle] = 0; - m_fps_accumulator[core_op_handle] = 0; - - auto network_cvs = ScheduledCoreOpCV::create(added_cng); - CHECK_EXPECTED(network_cvs); - m_core_ops_cvs[core_op_handle] = network_cvs.release(); - m_core_op_priority[HAILO_SCHEDULER_PRIORITY_NORMAL].emplace_back(core_op_handle); - } - - return core_op_handle; -} - -bool CoreOpsScheduler::is_core_op_active(const scheduler_core_op_handle_t &core_op_handle) -{ - for (auto device_info : m_devices) { - if (core_op_handle == device_info->current_core_op_handle) { - return true; - } - } - - return false; -} - -bool CoreOpsScheduler::is_multi_device() -{ - return m_devices.size() > 1; -} - -hailo_status CoreOpsScheduler::wait_for_write(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, - const std::chrono::milliseconds &timeout, const std::function &should_cancel) -{ - { - std::unique_lock lock(m_before_read_write_mutex); - - hailo_status status = HAILO_SUCCESS; - auto wait_res = m_core_ops_cvs[core_op_handle]->wait_for(stream_name, lock, timeout, [this, core_op_handle, stream_name, &should_cancel, &status] { - - if (should_cancel()) { - status = HAILO_STREAM_ABORTED_BY_USER; - return true; // return true so that the wait will finish - } - - if (should_core_op_stop(core_op_handle)) { - status = HAILO_STREAM_ABORTED_BY_USER; - return true; // return true so that the wait will finish - } - - return m_scheduled_core_ops[core_op_handle]->can_stream_write(stream_name); - }); - CHECK(wait_res, HAILO_TIMEOUT, "{} (H2D) failed with status={}, timeout={}ms", stream_name, HAILO_TIMEOUT, timeout.count()); - if (HAILO_STREAM_ABORTED_BY_USER == status) { - return status; - } - CHECK_SUCCESS(status); - - m_scheduled_core_ops[core_op_handle]->mark_frame_sent(); - m_scheduled_core_ops[core_op_handle]->requested_write_frames().increase(stream_name); - } - - return HAILO_SUCCESS; -} - -hailo_status CoreOpsScheduler::signal_write_finish(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, - bool did_write_fail) -{ - { - std::unique_lock lock(m_before_read_write_mutex); - assert(m_scheduled_core_ops.size() > core_op_handle); - auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; - - if (did_write_fail) { - scheduled_core_op->requested_write_frames().decrease(stream_name); - return HAILO_SUCCESS; - } - - if (should_core_op_stop(core_op_handle)) { - return HAILO_STREAM_ABORTED_BY_USER; - } - - scheduled_core_op->finished_write_frames().increase(stream_name); - scheduled_core_op->requested_write_frames().decrease(stream_name); - - auto device_id = CoreOpsSchedulerOracle::get_avail_device(*this, core_op_handle); - if (INVALID_DEVICE_ID != device_id) { - auto status = switch_core_op(core_op_handle, device_id); - if (HAILO_STREAM_ABORTED_BY_USER == status) { - LOGGER__INFO("switch_core_op has failed with status=HAILO_STREAM_ABORTED_BY_USER"); - return status; - } - CHECK_SUCCESS(status); - } - - auto status = optimize_streaming_if_enabled(core_op_handle); - if (HAILO_STREAM_ABORTED_BY_USER == status) { - return status; - } - CHECK_SUCCESS(status); - } - - return HAILO_SUCCESS; -} - -hailo_status CoreOpsScheduler::switch_core_op(const scheduler_core_op_handle_t &core_op_handle, uint32_t device_id, bool /*keep_nn_config*/) -{ - auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; - auto curr_device_info = m_devices[device_id]; - - // initialize current cycle maps - for (const auto &name : scheduled_core_op->get_inputs_names()) { - curr_device_info->current_cycle_requested_transferred_frames_h2d[core_op_handle][name] = 0; - } - - for (const auto &name : scheduled_core_op->get_outputs_names()) { - curr_device_info->current_cycle_finished_transferred_frames_d2h[core_op_handle][name] = 0; - curr_device_info->current_cycle_finished_read_frames_d2h[core_op_handle][name] = 0; - } - - uint16_t batch_size = SINGLE_CONTEXT_BATCH_SIZE; - uint16_t burst_size = static_cast(scheduled_core_op->finished_write_frames_min_value()); - // In multi device finished write frame could be bigger then the vdma buffers we have, can be removed after dynamic desc binding. - if (is_multi_device()) { - burst_size = std::min(burst_size, get_min_avail_buffers_count(core_op_handle, device_id)); - // We limit the max burst size to (dev_count * max_batch) to keep former behavior (this was the buffer_pool size) - // TODO: remove this limitation and work with user-controlled max_burst_size - burst_size = std::min(burst_size, static_cast(scheduled_core_op->get_max_batch_size() * get_device_count())); - } - - if (scheduled_core_op->use_dynamic_batch_flow()) { - batch_size = std::min(static_cast(scheduled_core_op->finished_write_frames_min_value()), scheduled_core_op->get_max_batch_size()); - burst_size = batch_size; - } - - bool has_same_batch_size_as_previous = (curr_device_info->current_batch_size == batch_size); - curr_device_info->current_batch_size = batch_size; - - if (curr_device_info->current_core_op_handle != core_op_handle) { - curr_device_info->is_switching_core_op = false; - } - - if ((core_op_handle != curr_device_info->current_core_op_handle) || (!has_same_batch_size_as_previous)) { - assert(m_scheduled_core_ops.size() > core_op_handle); - auto next_active_cng = scheduled_core_op->get_core_op(); - auto next_active_cng_wrapper = std::dynamic_pointer_cast(next_active_cng); - assert(nullptr != next_active_cng_wrapper); - auto next_active_cng_expected = next_active_cng_wrapper->get_core_op_by_device_index(curr_device_info->device_id); - CHECK_EXPECTED_AS_STATUS(next_active_cng_expected); - - std::shared_ptr current_active_vdma_cng = nullptr; - if (curr_device_info->current_core_op_handle != INVALID_CORE_OP_HANDLE) { - auto current_active_cng = m_scheduled_core_ops[curr_device_info->current_core_op_handle]->get_core_op(); - auto current_active_cng_bundle = std::dynamic_pointer_cast(current_active_cng); - assert(nullptr != current_active_cng_bundle); - auto current_active_cng_expected = current_active_cng_bundle->get_core_op_by_device_index(curr_device_info->device_id); - CHECK_EXPECTED_AS_STATUS(current_active_cng_expected); - current_active_vdma_cng = current_active_cng_expected.release(); - } - - TRACE(SwitchCoreOpTrace, "", core_op_handle); - static const auto RESUME_PENDING_STREAM_TRANSFERS = true; - auto status = VdmaConfigManager::switch_core_op(current_active_vdma_cng, next_active_cng_expected.value(), batch_size, - - RESUME_PENDING_STREAM_TRANSFERS); - CHECK_SUCCESS(status, "Failed switching core-op"); - // Clear the ready_to_switch flag from old activation - scheduled_core_op->mark_unready_to_switch(); - - // Register to get interrupts - has to be after core-op is activated - for (auto &output_stream : next_active_cng_expected.value()->get_output_streams()) { - OutputStreamBase &vdevice_output = static_cast(output_stream.get()); - status = vdevice_output.register_interrupt_callback( - [this, name = output_stream.get().name(), format = vdevice_output.get_layer_info().format.order, scheduled_core_op, core_op_handle, device_id] - (uint32_t frames) { - auto should_notify_next = false; - { - std::unique_lock lock(m_before_read_write_mutex); - // In order to meet performance requirement we enable switch only after first frame is arrived. - // TODO: remove this hack / move it to oracle and add another scheduling algorithm for it - scheduled_core_op->mark_ready_to_switch(); - if (hailo_format_order_t::HAILO_FORMAT_ORDER_HAILO_NMS != format) { - TRACE(OutputVdmaEnqueueTrace, "", core_op_handle, name, frames); - // TODO: Remove d2h_finished_transferred_frames and use current_cycle_finished_transferred_frames_d2h instead - scheduled_core_op->d2h_finished_transferred_frames(name) += frames; - m_devices[device_id]->current_cycle_finished_transferred_frames_d2h[core_op_handle][name] += frames; - } - - auto has_drained_everything = has_core_op_drained_everything(core_op_handle, device_id); - - if (m_should_monitor) { - update_utilization_read_buffers_finished(device_id, core_op_handle, has_drained_everything); - } - - // If ng finished and we didnt choose next lets choose without checking threshold - if (!m_devices[device_id]->is_switching_core_op && has_drained_everything) { - auto was_chosen = choose_next_core_op(device_id, true); - if (!was_chosen) { - choose_next_core_op(device_id, false); - } - } - - if (m_devices[device_id]->is_switching_core_op && has_drained_everything) { - should_notify_next = true; - } - } - // Notify stream that new frame was accepted (wait_for_read) - m_core_ops_cvs[core_op_handle]->notify_one(name); - if (should_notify_next) { - auto next_core_op = m_devices[device_id]->next_core_op_handle; - // Notify all the threads of the next ng (wait_for_read) - m_core_ops_cvs[next_core_op]->notify_all(); - } - }); - CHECK_SUCCESS(status); - } - } - - scheduled_core_op->set_last_run_timestamp(std::chrono::steady_clock::now()); // Mark timestamp on activation - curr_device_info->current_core_op_handle = core_op_handle; - - // Finished switching batch size - m_changing_current_batch_size[core_op_handle] = false; - - auto status = send_all_pending_buffers(core_op_handle, device_id, burst_size); - if (HAILO_STREAM_ABORTED_BY_USER == status) { - LOGGER__INFO("send_all_pending_buffers has failed with status=HAILO_STREAM_ABORTED_BY_USER"); - return status; - } - CHECK_SUCCESS(status); - - return HAILO_SUCCESS; -} - -hailo_status CoreOpsScheduler::send_all_pending_buffers(const scheduler_core_op_handle_t &core_op_handle, uint32_t device_id, uint32_t burst_size) -{ - auto current_device_info = m_devices[device_id]; - if ((INVALID_CORE_OP_HANDLE == current_device_info->current_core_op_handle) || (current_device_info->current_core_op_handle != core_op_handle)) { - return HAILO_SUCCESS; - } - - auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; - - for (size_t i = 0; i < burst_size; i++) { - auto finished_send = false; - for (const auto &name : scheduled_core_op->get_inputs_names()) { - if (scheduled_core_op->finished_write_frames(name) == 0) { - finished_send = true; - break; - } - } - if (finished_send) { - break; - } - for (const auto &name : scheduled_core_op->get_inputs_names()) { - auto status = send_pending_buffer(core_op_handle, name, device_id); - if (HAILO_STREAM_ABORTED_BY_USER == status) { - LOGGER__INFO("send_pending_buffer has failed with status=HAILO_STREAM_ABORTED_BY_USER"); - return status; - } - CHECK_SUCCESS(status); - } - scheduled_core_op->push_device_index(device_id); - scheduled_core_op->set_last_device_index(device_id); - - if (m_should_monitor) { - update_utilization_send_started(device_id); - } - } - - return HAILO_SUCCESS; -} - -hailo_status CoreOpsScheduler::send_pending_buffer(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, - uint32_t device_id) -{ - assert(m_scheduled_core_ops.size() > core_op_handle); - auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; - - auto current_cng = scheduled_core_op->get_core_op(); - auto input_stream = current_cng->get_input_stream_by_name(stream_name); - CHECK_EXPECTED_AS_STATUS(input_stream); - - VDeviceInputStreamMultiplexerWrapper &vdevice_input = static_cast(input_stream->get()); - TRACE(InputVdmaDequeueTrace, "", core_op_handle, stream_name); - auto status = vdevice_input.send_pending_buffer(device_id); - if (HAILO_STREAM_ABORTED_BY_USER == status) { - LOGGER__INFO("send_pending_buffer has failed with status=HAILO_STREAM_ABORTED_BY_USER"); - return status; - } - CHECK_SUCCESS(status); - - m_devices[device_id]->current_cycle_requested_transferred_frames_h2d[core_op_handle][stream_name]++; - scheduled_core_op->finished_write_frames().decrease(stream_name); - - scheduled_core_op->h2d_finished_transferred_frames().increase(stream_name); - - if (should_core_op_stop(core_op_handle)) { - return HAILO_STREAM_ABORTED_BY_USER; - } - - return HAILO_SUCCESS; -} - -CoreOpsScheduler::ReadyInfo CoreOpsScheduler::is_core_op_ready(const scheduler_core_op_handle_t &core_op_handle, bool check_threshold) -{ - ReadyInfo result; - result.is_ready = false; - - if (should_core_op_stop(core_op_handle)) { - // Do not switch to an aborted core-op - return result; - } - - auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; - // Check if there arent any write requests - bool has_pending_writes = scheduled_core_op->finished_write_frames_min_value() > 0; - - // Check if there arent any read requests - bool has_pending_user_reads = false; - for (const auto &name : scheduled_core_op->get_outputs_names()) { - if (scheduled_core_op->requested_read_frames(name) > 0) { - has_pending_user_reads = true; - break; - } - } - - std::vector over_threshold; - over_threshold.reserve(scheduled_core_op->get_inputs_names().size()); - std::vector over_timeout; - over_timeout.reserve(scheduled_core_op->get_inputs_names().size()); - - if (check_threshold) { - for (const auto &name : scheduled_core_op->get_inputs_names()) { - auto threshold_exp = scheduled_core_op->get_threshold(name); - if (!threshold_exp) { - LOGGER__ERROR("Failed to get threshold for stream {}", name); - return result; - } - auto threshold = (DEFAULT_SCHEDULER_MIN_THRESHOLD == threshold_exp.value()) ? 1 : threshold_exp.value(); - auto timeout_exp = scheduled_core_op->get_timeout(); - if (!timeout_exp) { - LOGGER__ERROR("Failed to get timeout for stream {}", name); - return result; - } - auto timeout = timeout_exp.release(); - - // Check if there arent enough write requests to reach threshold and timeout didnt passed - auto write_requests = scheduled_core_op->requested_write_frames(name) + scheduled_core_op->finished_write_frames(name); - auto stream_over_threshold = write_requests >= threshold; - auto stream_over_timeout = timeout <= (std::chrono::steady_clock::now() - scheduled_core_op->get_last_run_timestamp()); - over_threshold.push_back(stream_over_threshold); - over_timeout.push_back(stream_over_timeout); - if (stream_over_threshold || stream_over_timeout) { - continue; - } else { - result.is_ready = false; - return result; - } - } - } - - result.threshold = std::all_of(over_threshold.begin(), over_threshold.end(), [](auto over) { return over; }); - result.timeout = std::all_of(over_timeout.begin(), over_timeout.end(), [](auto over) { return over; }); - result.is_ready = has_pending_writes && has_pending_user_reads; - - return result; -} - -Expected CoreOpsScheduler::wait_for_read(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, - const std::chrono::milliseconds &timeout) -{ - std::unique_lock lock(m_before_read_write_mutex); - - auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; - scheduled_core_op->requested_read_frames().increase(stream_name); - - hailo_status status = HAILO_SUCCESS; - auto wait_res = m_core_ops_cvs[core_op_handle]->wait_for(stream_name, lock, timeout, [this, core_op_handle, scheduled_core_op, stream_name, &status] { - - if (should_core_op_stop(core_op_handle)) { - status = HAILO_STREAM_ABORTED_BY_USER; - return true; // return true so that the wait will finish - } - - auto device_id = CoreOpsSchedulerOracle::get_avail_device(*this, core_op_handle); - if (INVALID_DEVICE_ID != device_id) { - status = switch_core_op(core_op_handle, device_id); - if (HAILO_SUCCESS != status) { - return true; // return true so that the wait will finish - } - } - - return scheduled_core_op->can_stream_read(stream_name); - }); - CHECK_AS_EXPECTED(wait_res, HAILO_TIMEOUT, "{} (D2H) failed with status={}, timeout={}ms", stream_name, HAILO_TIMEOUT, timeout.count()); - if (HAILO_STREAM_ABORTED_BY_USER == status) { - return make_unexpected(status); - } - CHECK_SUCCESS_AS_EXPECTED(status); - - scheduled_core_op->requested_read_frames().decrease(stream_name); - - return scheduled_core_op->pop_device_index(stream_name); -} - -hailo_status CoreOpsScheduler::signal_read_finish(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, uint32_t device_id) -{ - auto should_notify_next = false; - { - std::unique_lock lock(m_before_read_write_mutex); - - auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; - - scheduled_core_op->finished_read_frames().increase(stream_name); - m_devices[device_id]->current_cycle_finished_read_frames_d2h[core_op_handle][stream_name]++; - scheduled_core_op->d2h_finished_transferred_frames().decrease(stream_name); - m_fps_accumulator[core_op_handle]++; - - decrease_core_op_counters(core_op_handle); - - auto has_drained_everything = has_core_op_drained_everything(core_op_handle, device_id); - if (scheduled_core_op->is_nms() && has_drained_everything) { - // In NMS networks there is possibility that next wasn't choosen yet - choose_next_core_op(device_id, true); - - // If we didnt choose with treshold or timeout lets choose without treshold - if (!m_devices[device_id]->is_switching_core_op) { - choose_next_core_op(device_id, false); - } - - if (m_devices[device_id]->is_switching_core_op) { - should_notify_next = true; - } - - if (m_should_monitor) { - update_utilization_read_buffers_finished(device_id, core_op_handle, has_drained_everything); - } - } - } - - // Notify stream that frame was read and we have a space in the read buffers (wait_for_write) - m_core_ops_cvs[core_op_handle]->notify_all(); - - if (should_notify_next) { - // Notify all the threads of the next ng, for nms networks this is the only place we know the network was finished (wait_for_read) - m_core_ops_cvs[m_devices[device_id]->next_core_op_handle]->notify_all(); - } - - return HAILO_SUCCESS; -} - -void CoreOpsScheduler::decrease_core_op_counters(const scheduler_core_op_handle_t &core_op_handle) -{ - return m_scheduled_core_ops[core_op_handle]->decrease_current_core_op_counters(); -} - -bool CoreOpsScheduler::has_core_op_drained_everything(const scheduler_core_op_handle_t &core_op_handle, uint32_t device_id) -{ - if (INVALID_CORE_OP_HANDLE == core_op_handle) { - // If no core-op is running, consider it as drained - return true; - } - - if (core_op_all_streams_aborted(core_op_handle)) { - // We treat core-op as drained only if all streams are aborted - to make sure there aren't any ongoing transfers - return true; - } - - if ((!m_scheduled_core_ops[core_op_handle]->is_nms()) && (is_multi_device() || m_scheduled_core_ops[core_op_handle]->use_dynamic_batch_flow())) { - auto current_device_info = m_devices[device_id]; - auto max_transferred_h2d = get_max_value_of_unordered_map(current_device_info->current_cycle_requested_transferred_frames_h2d[core_op_handle]); - auto min_transferred_d2h = get_min_value_of_unordered_map(current_device_info->current_cycle_finished_transferred_frames_d2h[core_op_handle]); - - return (max_transferred_h2d == min_transferred_d2h); - } - - return m_scheduled_core_ops[core_op_handle]->has_core_op_drained_everything(); -} - -hailo_status CoreOpsScheduler::enable_stream(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name) -{ - { - std::unique_lock lock(m_before_read_write_mutex); - - if (!m_should_core_op_stop[core_op_handle][stream_name]) { - return HAILO_SUCCESS; - } - - m_should_core_op_stop[core_op_handle][stream_name] = false; - } - m_core_ops_cvs[core_op_handle]->notify_all(); - - return HAILO_SUCCESS; -} - -hailo_status CoreOpsScheduler::disable_stream(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name) -{ - { - std::unique_lock lock(m_before_read_write_mutex); - - if (m_should_core_op_stop[core_op_handle][stream_name]) { - return HAILO_SUCCESS; - } - - m_should_core_op_stop[core_op_handle][stream_name] = true; - } - m_core_ops_cvs[core_op_handle]->notify_all(); - - return HAILO_SUCCESS; -} - -hailo_status CoreOpsScheduler::set_timeout(const scheduler_core_op_handle_t &core_op_handle, const std::chrono::milliseconds &timeout, const std::string &/*network_name*/) -{ - // TODO: call in loop for set_timeout with the relevant stream-names (of the given network) - return m_scheduled_core_ops[core_op_handle]->set_timeout(timeout); -} - -hailo_status CoreOpsScheduler::set_threshold(const scheduler_core_op_handle_t &core_op_handle, uint32_t threshold, const std::string &/*network_name*/) -{ - // TODO: call in loop for set_timeout with the relevant stream-names (of the given network) - return m_scheduled_core_ops[core_op_handle]->set_threshold(threshold); -} - -hailo_status CoreOpsScheduler::set_priority(const scheduler_core_op_handle_t &core_op_handle, core_op_priority_t priority, const std::string &/*network_name*/) -{ - CHECK(priority <= HAILO_SCHEDULER_PRIORITY_MAX, HAILO_INVALID_ARGUMENT); - std::unique_lock lock(m_before_read_write_mutex); - auto old_priority = m_scheduled_core_ops[core_op_handle]->get_priority(); - auto &priority_vector = m_core_op_priority[old_priority]; - auto it = std::find(priority_vector.begin(), priority_vector.end(), core_op_handle); - CHECK(it != priority_vector.end(), HAILO_INTERNAL_FAILURE); - - priority_vector.erase(it); - m_scheduled_core_ops[core_op_handle]->set_priority(priority); - m_core_op_priority[priority].push_back(core_op_handle); - - return HAILO_SUCCESS; -} - -bool CoreOpsScheduler::choose_next_core_op(size_t device_id, bool check_threshold) -{ - if (!m_devices[device_id]->is_switching_core_op) { - return CoreOpsSchedulerOracle::choose_next_model(*this, m_devices[device_id]->device_id, check_threshold); - } - return false; -} - -bool CoreOpsScheduler::should_core_op_stop(const scheduler_core_op_handle_t &core_op_handle) -{ - for (const auto &name_flag_pair : m_should_core_op_stop[core_op_handle]) { - if (name_flag_pair.second) { - return true; - } - } - - return false; -} - -bool CoreOpsScheduler::core_op_all_streams_aborted(const scheduler_core_op_handle_t &core_op_handle) -{ - for (const auto &name_flag_pair : m_should_core_op_stop[core_op_handle]) { - if (!name_flag_pair.second) { - return false; - } - } - return true; -} - -void CoreOpsScheduler::notify_all() -{ - { - // Acquire mutex to make sure the notify_all will wake the blocking threads on the cv - std::unique_lock lock(m_before_read_write_mutex); - } - // TODO: consider notify only the relevant ng or stream - for (auto &cng_cvs : m_core_ops_cvs) { - cng_cvs.second->notify_all(); - } -} - -hailo_status CoreOpsScheduler::optimize_streaming_if_enabled(const scheduler_core_op_handle_t &core_op_handle) -{ - auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; - - if ((!scheduled_core_op->use_dynamic_batch_flow()) && !(scheduled_core_op->is_ready_to_switch() && - CoreOpsSchedulerOracle::should_stop_streaming(*this, scheduled_core_op->get_priority()))) { - for (uint32_t i = 0; i < m_devices.size(); i++) { - uint32_t index = scheduled_core_op->get_last_device_index() + i + 1; - index %= static_cast(m_devices.size()); - auto device_info = m_devices[index]; - // If multi device check for space in the vdma buffers, the send pending buffer is waitable in the current implementation. - // can be removed after dynamic descriptor binding support - if (device_info->current_core_op_handle == core_op_handle && - (!is_multi_device() || (get_min_avail_buffers_count(core_op_handle, device_info->device_id) >= DEFAULT_BURST_SIZE))) { - auto status = send_all_pending_buffers(core_op_handle, device_info->device_id, DEFAULT_BURST_SIZE); - if (HAILO_STREAM_ABORTED_BY_USER == status) { - LOGGER__INFO("send_all_pending_buffers has failed with status=HAILO_STREAM_ABORTED_BY_USER"); - return status; - } - CHECK_SUCCESS(status); - } - } - } - - return HAILO_SUCCESS; -} - -uint16_t CoreOpsScheduler::get_min_avail_buffers_count(const scheduler_core_op_handle_t &core_op_handle, uint32_t device_id) -{ - auto device_info = m_devices[device_id]; - auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; - - auto max_transferred_h2d = get_max_value_of_unordered_map(device_info->current_cycle_requested_transferred_frames_h2d[core_op_handle]); - auto min_d2h_frames = scheduled_core_op->is_nms() ? get_min_value_of_unordered_map(device_info->current_cycle_finished_read_frames_d2h[core_op_handle]) : - get_min_value_of_unordered_map(device_info->current_cycle_finished_transferred_frames_d2h[core_op_handle]); - auto ongoing_frames = static_cast(max_transferred_h2d - min_d2h_frames); - - uint16_t avail_buffers = static_cast(scheduled_core_op->get_min_input_buffers_count(get_device_count()) - ongoing_frames); - - return avail_buffers; -} - -void CoreOpsScheduler::update_utilization_timers(scheduler_device_idx_t device_id, scheduler_core_op_handle_t core_op_handle) -{ - assert(contains(m_core_op_utilization, core_op_handle)); - - auto time_diff = std::chrono::duration_cast>( - std::chrono::steady_clock::now() - m_last_measured_utilization_timestamp[device_id]).count(); - - m_device_utilization[device_id] += time_diff; - m_core_op_utilization[core_op_handle] += time_diff; -} - -void CoreOpsScheduler::update_utilization_timestamp(scheduler_device_idx_t device_id) -{ - m_last_measured_utilization_timestamp[device_id] = std::chrono::steady_clock::now(); -} - -void CoreOpsScheduler::update_utilization_send_started(scheduler_device_idx_t device_id) -{ - if (m_device_has_drained_everything[device_id]) { - update_device_drained_state(device_id, false); - update_utilization_timestamp(device_id); - } -} - -void CoreOpsScheduler::update_device_drained_state(scheduler_device_idx_t device_id, bool state) -{ - m_device_has_drained_everything[device_id] = state; -} - -void CoreOpsScheduler::update_utilization_read_buffers_finished(scheduler_device_idx_t device_id, - scheduler_core_op_handle_t core_op_handle, bool is_drained_everything) -{ - update_utilization_timers(device_id, core_op_handle); - update_device_drained_state(device_id, is_drained_everything); - if (!is_drained_everything) { - update_utilization_timestamp(device_id); - } -} - -} /* namespace hailort */ \ No newline at end of file diff --git a/hailort/libhailort/src/vdevice/scheduler/network_group_scheduler.hpp b/hailort/libhailort/src/vdevice/scheduler/network_group_scheduler.hpp deleted file mode 100644 index 253e2b9..0000000 --- a/hailort/libhailort/src/vdevice/scheduler/network_group_scheduler.hpp +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. - * Distributed under the MIT license (https://opensource.org/licenses/MIT) - **/ -/** - * @file network_group_scheduler.hpp - * @brief Class declaration for CoreOpsScheduler that schedules core-ops to be active depending on the scheduling algorithm. - **/ - -#ifndef _HAILO_NETWORK_GROUP_SCHEDULER_HPP_ -#define _HAILO_NETWORK_GROUP_SCHEDULER_HPP_ - -#include "hailo/hailort.h" -#include "hailo/expected.hpp" - -#include "common/utils.hpp" -#include "common/filesystem.hpp" - -#include "vdevice/scheduler/scheduler_mon.hpp" -#include "vdevice/scheduler/scheduled_core_op_state.hpp" -#include "vdevice/scheduler/scheduled_core_op_cv.hpp" -#include "vdevice/scheduler/scheduler_base.hpp" - - -namespace hailort -{ - -#define INVALID_CORE_OP_HANDLE (UINT32_MAX) -#define INVALID_DEVICE_ID (UINT32_MAX) - -using scheduler_core_op_handle_t = uint32_t; -using core_op_priority_t = uint8_t; -using scheduler_device_idx_t = uint32_t; - -class CoreOpsScheduler; -using CoreOpsSchedulerPtr = std::shared_ptr; - -// We use mostly weak pointer for the scheduler to prevent circular dependency of the pointers -using CoreOpsSchedulerWeakPtr = std::weak_ptr; - -using stream_name_t = std::string; - -class CoreOpsScheduler : public SchedulerBase -{ -public: - static Expected create_round_robin(uint32_t device_count, std::vector &devices_bdf_id, - std::vector &devices_arch); - CoreOpsScheduler(hailo_scheduling_algorithm_t algorithm, uint32_t device_count, std::vector &devices_bdf_id, - std::vector &devices_arch); - - virtual ~CoreOpsScheduler(); - CoreOpsScheduler(const CoreOpsScheduler &other) = delete; - CoreOpsScheduler &operator=(const CoreOpsScheduler &other) = delete; - CoreOpsScheduler &operator=(CoreOpsScheduler &&other) = delete; - CoreOpsScheduler(CoreOpsScheduler &&other) noexcept = delete; - - Expected add_core_op(std::shared_ptr added_core_op); - - hailo_status wait_for_write(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, - const std::chrono::milliseconds &timeout, const std::function &should_cancel); - hailo_status signal_write_finish(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, bool did_write_fail); - Expected wait_for_read(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, - const std::chrono::milliseconds &timeout); - hailo_status signal_read_finish(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, uint32_t device_id); - - hailo_status enable_stream(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name); - hailo_status disable_stream(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name); - - hailo_status set_timeout(const scheduler_core_op_handle_t &core_op_handle, const std::chrono::milliseconds &timeout, const std::string &network_name); - hailo_status set_threshold(const scheduler_core_op_handle_t &core_op_handle, uint32_t threshold, const std::string &network_name); - hailo_status set_priority(const scheduler_core_op_handle_t &core_op_handle, core_op_priority_t priority, const std::string &network_name); - - virtual ReadyInfo is_core_op_ready(const scheduler_core_op_handle_t &core_op_handle, bool check_threshold) override; - virtual bool has_core_op_drained_everything(const scheduler_core_op_handle_t &core_op_handle, uint32_t device_id) override; - - void notify_all(); - -protected: - bool choose_next_core_op(size_t device_id, bool check_threshold); - - std::unordered_map m_changing_current_batch_size; - std::unordered_map> m_should_core_op_stop; - -private: - hailo_status switch_core_op(const scheduler_core_op_handle_t &core_op_handle, uint32_t device_id, - bool keep_nn_config = false); - void reset_current_core_op_timestamps(uint32_t device_id); - - hailo_status send_all_pending_buffers(const scheduler_core_op_handle_t &core_op_handle, uint32_t device_id, uint32_t burst_size); - hailo_status send_pending_buffer(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, uint32_t device_id); - - void decrease_core_op_counters(const scheduler_core_op_handle_t &core_op_handle); - bool should_core_op_stop(const scheduler_core_op_handle_t &core_op_handle); - bool core_op_all_streams_aborted(const scheduler_core_op_handle_t &core_op_handle); - - std::string get_core_op_name(const scheduler_core_op_handle_t &core_op_handle); - bool is_core_op_active(const scheduler_core_op_handle_t &core_op_handle); - bool is_multi_device(); - hailo_status optimize_streaming_if_enabled(const scheduler_core_op_handle_t &network_group_handle); - uint16_t get_min_avail_buffers_count(const scheduler_core_op_handle_t &network_group_handle, uint32_t device_id); - - hailo_status start_mon(); - void time_dependent_events_cycle_calc(); - void log_monitor_device_infos(ProtoMon &mon); - void log_monitor_networks_infos(ProtoMon &mon); - void log_monitor_frames_infos(ProtoMon &mon); - void update_utilization_timers(scheduler_device_idx_t device_id, scheduler_core_op_handle_t core_op_handle); - void update_utilization_timestamp(scheduler_device_idx_t device_id); - void update_utilization_send_started(scheduler_device_idx_t device_id); - void update_device_drained_state(scheduler_device_idx_t device_id, bool state); - void update_utilization_read_buffers_finished(scheduler_device_idx_t device_id, scheduler_core_op_handle_t core_op_hanle, bool is_drained_everything); - hailo_status set_h2d_frames_counters(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, - ProtoMonStreamFramesInfo &stream_frames_info); - hailo_status set_d2h_frames_counters(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, - ProtoMonStreamFramesInfo &stream_frames_info); -#if defined(__GNUC__) - Expected> open_temp_mon_file(); - void dump_state(); -#endif - - std::vector> m_scheduled_core_ops; - std::mutex m_before_read_write_mutex; - std::unordered_map> m_core_ops_cvs; - - // Params for the scheduler MON - std::atomic_bool m_should_monitor; - std::thread m_mon_thread; - EventPtr m_mon_shutdown_event; -#if defined(__GNUC__) - std::shared_ptr m_mon_tmp_output; -#endif - std::chrono::time_point m_last_measured_timestamp; - double m_last_measured_time_duration; - std::unordered_map m_device_utilization; - std::unordered_map m_device_has_drained_everything; - std::unordered_map> m_last_measured_utilization_timestamp; - // TODO: Consider adding Accumulator classes for more info (min, max, mean, etc..) - std::unordered_map m_core_op_utilization; - std::unordered_map m_fps_accumulator; -}; - -} /* namespace hailort */ - -#endif /* _HAILO_NETWORK_GROUP_SCHEDULER_HPP_ */ diff --git a/hailort/libhailort/src/vdevice/scheduler/scheduled_core_op_cv.hpp b/hailort/libhailort/src/vdevice/scheduler/scheduled_core_op_cv.hpp index 9b6f8af..ef314a0 100644 --- a/hailort/libhailort/src/vdevice/scheduler/scheduled_core_op_cv.hpp +++ b/hailort/libhailort/src/vdevice/scheduler/scheduled_core_op_cv.hpp @@ -15,8 +15,6 @@ #include "common/utils.hpp" -#include "vdevice/scheduler/scheduler_mon.hpp" - #include diff --git a/hailort/libhailort/src/vdevice/scheduler/scheduled_core_op_state.cpp b/hailort/libhailort/src/vdevice/scheduler/scheduled_core_op_state.cpp index 037fb57..bc4fa21 100644 --- a/hailort/libhailort/src/vdevice/scheduler/scheduled_core_op_state.cpp +++ b/hailort/libhailort/src/vdevice/scheduler/scheduled_core_op_state.cpp @@ -19,26 +19,25 @@ namespace hailort #define SINGLE_CONTEXT_BATCH_SIZE (1) ScheduledCoreOp::ScheduledCoreOp(std::shared_ptr core_op, std::chrono::milliseconds timeout, - uint16_t max_batch_size, StreamInfoVector &stream_infos, std::string core_op_name) : + uint16_t max_batch_size, bool use_dynamic_batch_flow, StreamInfoVector &stream_infos, std::string core_op_name) : m_core_op(core_op), m_last_run_time_stamp(std::chrono::steady_clock::now()), m_timeout(std::move(timeout)), m_frame_was_sent(false), m_max_batch_size(max_batch_size), + m_use_dynamic_batch_flow(use_dynamic_batch_flow), m_priority(HAILO_SCHEDULER_PRIORITY_NORMAL), - m_last_device_index(INVALID_DEVICE_ID), + m_last_device_id(INVALID_DEVICE_ID), m_core_op_name(core_op_name), m_inputs_names(), m_outputs_names(), - m_is_nms(false), - m_ready_to_switch(false) + m_is_nms(false) { // Prepare empty counters for the added core-op for (const auto &stream_info : stream_infos) { m_min_threshold_per_stream[stream_info.name] = DEFAULT_SCHEDULER_MIN_THRESHOLD; if (HAILO_H2D_STREAM == stream_info.direction) { - m_requested_write_frames.insert(stream_info.name); - m_finished_write_frames.insert(stream_info.name); + m_pending_to_send_frames.insert(stream_info.name); m_h2d_finished_transferred_frames.insert(stream_info.name); m_inputs_names.push_back(stream_info.name); } else { @@ -46,7 +45,7 @@ ScheduledCoreOp::ScheduledCoreOp(std::shared_ptr core_op, std::chrono::m m_finished_read_frames.insert(stream_info.name); m_d2h_finished_transferred_frames.insert(stream_info.name); m_outputs_names.push_back(stream_info.name); - m_output_streams_read_orders[stream_info.name] = std::queue(); + if (HAILO_FORMAT_ORDER_HAILO_NMS == stream_info.format.order) { m_is_nms = true; } @@ -58,93 +57,44 @@ Expected> ScheduledCoreOp::create(std::shared_p { auto timeout = DEFAULT_SCHEDULER_TIMEOUT; - uint16_t max_batch_size = CONTROL_PROTOCOL__IGNORE_DYNAMIC_BATCH_SIZE; - if (added_core_op->get_supported_features().multi_context) { - auto batch_size = added_core_op->get_stream_batch_size(stream_infos[0].name); - CHECK_EXPECTED(batch_size); - if (batch_size.value() > SINGLE_CONTEXT_BATCH_SIZE) { - max_batch_size = batch_size.release(); - } - } - - return make_shared_nothrow(added_core_op, timeout, max_batch_size, stream_infos, added_core_op->name()); -} + auto batch_size_expected = added_core_op->get_stream_batch_size(stream_infos[0].name); + CHECK_EXPECTED(batch_size_expected); + auto max_batch_size = batch_size_expected.release(); -bool ScheduledCoreOp::has_enough_space_in_read_buffers(uint32_t ongoing_frames) -{ - auto output_streams = m_core_op->get_output_streams(); - for (auto &output_stream : output_streams) { - OutputStreamBase &vdevice_output = static_cast(output_stream.get()); - if (auto pending_frames_size = vdevice_output.get_buffer_frames_size()) { - if (pending_frames_size.value() <= ongoing_frames) { - return false; - } - // If couldnt get pending frames size and count (e.g. NMS layer), assume we have space - scheduler switch will prevent deadlocks here - } - } - return true; + // DEFAULT_BATCH_SIZE and SINGLE_CONTEXT_BATCH_SIZE support streaming and therfore we are not using dynamic batch flow + auto use_dynamic_batch_flow = added_core_op->get_supported_features().multi_context && (max_batch_size > SINGLE_CONTEXT_BATCH_SIZE); + return make_shared_nothrow(added_core_op, timeout, max_batch_size, use_dynamic_batch_flow, stream_infos, added_core_op->name()); } -uint16_t ScheduledCoreOp::get_min_input_buffers_count(uint32_t device_count) +uint16_t ScheduledCoreOp::get_min_input_buffers_count() { auto input_streams = m_core_op->get_input_streams(); uint16_t buffers_count = UINT16_MAX; for (auto &input_stream : input_streams) { InputStreamBase &vdevice_input = static_cast(input_stream.get()); if (auto pending_frames_size = vdevice_input.get_buffer_frames_size()) { - buffers_count = std::min(buffers_count, static_cast(pending_frames_size.value() / device_count)); + buffers_count = std::min(buffers_count, static_cast(pending_frames_size.value())); } } return buffers_count; } -bool ScheduledCoreOp::has_input_written_most_frames(const std::string &stream_name) -{ - auto total_writes = total_written_frames_count(); - return total_writes[stream_name] == get_max_value_of_unordered_map(total_writes); -} - -// TODO: Use get_pre_transfer_h2d_frames_count + get_h2d_transferred_frames_count -// TODO: Avoid returning map (malloc) -std::unordered_map ScheduledCoreOp::total_written_frames_count() -{ - std::unordered_map write_sum; - for (const auto &name : get_inputs_names()) { - write_sum[name] = m_requested_write_frames[name] + m_finished_write_frames[name] - + m_h2d_finished_transferred_frames[name]; - } - return write_sum; -} - -// TODO: Use max(m_d2h_finished_transferred_frames) == 0 instead -bool ScheduledCoreOp::has_pending_frames() +uint16_t ScheduledCoreOp::get_min_output_buffers_count() { - auto h2d_transferred_frames_count = m_h2d_finished_transferred_frames.get_max_value(); - for (const auto &name : get_outputs_names()) { - if (m_finished_read_frames[name] < h2d_transferred_frames_count) { - return true; + auto output_streams = m_core_op->get_output_streams(); + uint16_t buffers_count = UINT16_MAX; + for (auto &output_stream : output_streams) { + OutputStreamBase &vdevice_input = static_cast(output_stream.get()); + if (auto pending_frames_size = vdevice_input.get_buffer_frames_size()) { + buffers_count = std::min(buffers_count, static_cast(pending_frames_size.value())); } } - return false; -} - -bool ScheduledCoreOp::can_stream_read(const std::string &stream_name) -{ - return !m_output_streams_read_orders[stream_name].empty(); -} - -bool ScheduledCoreOp::can_stream_write(const std::string &stream_name) -{ - auto total_written_frames = total_written_frames_count()[stream_name]; - auto min_finished_read = finished_read_frames_min_value(); - auto ongoing_frames = (min_finished_read < total_written_frames) ? (total_written_frames - min_finished_read) : 0; - return has_enough_space_in_read_buffers(ongoing_frames); + return buffers_count; } - bool ScheduledCoreOp::use_dynamic_batch_flow() { - return (CONTROL_PROTOCOL__IGNORE_DYNAMIC_BATCH_SIZE != m_max_batch_size); + return m_use_dynamic_batch_flow; } bool ScheduledCoreOp::has_core_op_drained_everything() @@ -160,11 +110,10 @@ bool ScheduledCoreOp::has_core_op_drained_everything() void ScheduledCoreOp::decrease_current_core_op_counters() { - // Decrease only if counter is 2 or bigger because reaching 0 can cause states to change - if (!m_h2d_finished_transferred_frames.all_values_bigger_or_equal(2)) { + if (!m_h2d_finished_transferred_frames.all_values_bigger_or_equal(1)) { return; } - if (!m_finished_read_frames.all_values_bigger_or_equal(2)) { + if (!m_finished_read_frames.all_values_bigger_or_equal(1)) { return; } @@ -176,15 +125,6 @@ void ScheduledCoreOp::decrease_current_core_op_counters() } } -uint32_t ScheduledCoreOp::get_pre_transfer_h2d_frames_count() -{ - std::unordered_map write_sum; - for (const auto &name : get_inputs_names()) { - write_sum[name] = m_requested_write_frames[name] + m_finished_write_frames[name]; - } - return get_max_value_of_unordered_map(write_sum); -} - hailo_status ScheduledCoreOp::set_timeout(const std::chrono::milliseconds &timeout, const stream_name_t &stream_name) { CHECK(!m_frame_was_sent, HAILO_INVALID_OPERATION, @@ -199,7 +139,7 @@ hailo_status ScheduledCoreOp::set_timeout(const std::chrono::milliseconds &timeo hailo_status ScheduledCoreOp::set_threshold(uint32_t threshold, const stream_name_t &stream_name) { - CHECK((CONTROL_PROTOCOL__IGNORE_DYNAMIC_BATCH_SIZE == m_max_batch_size) || + CHECK(!use_dynamic_batch_flow() || (threshold <= m_max_batch_size), HAILO_INVALID_ARGUMENT, "Threshold must be equal or lower than the maximum batch size!"); CHECK(!m_frame_was_sent, HAILO_INVALID_OPERATION, @@ -226,14 +166,14 @@ void ScheduledCoreOp::set_priority(core_op_priority_t priority) m_priority = priority; } -uint32_t ScheduledCoreOp::get_last_device_index() +device_id_t ScheduledCoreOp::get_last_device() { - return m_last_device_index; + return m_last_device_id; } -void ScheduledCoreOp::set_last_device_index(uint32_t device_index) +void ScheduledCoreOp::set_last_device(const device_id_t &device_id) { - m_last_device_index = device_index; + m_last_device_id = device_id; } std::string ScheduledCoreOp::get_core_op_name() @@ -276,35 +216,26 @@ Expected ScheduledCoreOp::get_threshold(const stream_name_t &stream_na uint16_t ScheduledCoreOp::get_max_batch_size() { - if (!use_dynamic_batch_flow()) { - return SINGLE_CONTEXT_BATCH_SIZE; + if (CONTROL_PROTOCOL__IGNORE_DYNAMIC_BATCH_SIZE == m_max_batch_size) { + // In nms networks we dont know the output buffers count and therfore we are using the input buffer count + return is_nms() ? get_min_input_buffers_count() : get_min_output_buffers_count(); } return m_max_batch_size; } -Counter &ScheduledCoreOp::requested_write_frames() -{ - return m_requested_write_frames; -} - -std::atomic_uint32_t &ScheduledCoreOp::requested_write_frames(const stream_name_t &stream_name) -{ - return m_requested_write_frames[stream_name]; -} - -Counter &ScheduledCoreOp::finished_write_frames() +Counter &ScheduledCoreOp::pending_to_send_frames() { - return m_finished_write_frames; + return m_pending_to_send_frames; } -std::atomic_uint32_t &ScheduledCoreOp::finished_write_frames(const stream_name_t &stream_name) +std::atomic_uint32_t &ScheduledCoreOp::pending_to_send_frames(const stream_name_t &stream_name) { - return m_finished_write_frames[stream_name]; + return m_pending_to_send_frames[stream_name]; } -uint32_t ScheduledCoreOp::finished_write_frames_min_value() +uint32_t ScheduledCoreOp::pending_to_send_frames_min_value() { - return m_finished_write_frames.get_min_value(); + return m_pending_to_send_frames.get_min_value(); } Counter &ScheduledCoreOp::h2d_finished_transferred_frames() @@ -317,6 +248,11 @@ std::atomic_uint32_t &ScheduledCoreOp::h2d_finished_transferred_frames(const str return m_h2d_finished_transferred_frames[stream_name]; } +uint32_t ScheduledCoreOp::h2d_finished_transferred_frames_max_value() +{ + return m_h2d_finished_transferred_frames.get_max_value(); +} + Counter &ScheduledCoreOp::requested_read_frames() { return m_requested_read_frames; @@ -362,36 +298,4 @@ const std::vector &ScheduledCoreOp::get_outputs_names() return m_outputs_names; } -void ScheduledCoreOp::push_device_index(uint32_t device_index) -{ - for (auto& stream_name : get_outputs_names()) { - m_output_streams_read_orders[stream_name].push(device_index); - } -} - -uint32_t ScheduledCoreOp::pop_device_index(const stream_name_t &stream_name) -{ - assert(contains(m_output_streams_read_orders, stream_name)); - assert(!m_output_streams_read_orders[stream_name].empty()); - auto device_index = m_output_streams_read_orders[stream_name].front(); - m_output_streams_read_orders[stream_name].pop(); - - return device_index; -} - -bool ScheduledCoreOp::is_ready_to_switch() -{ - return m_ready_to_switch; -} - -void ScheduledCoreOp::mark_ready_to_switch() -{ - m_ready_to_switch = true; -} - -void ScheduledCoreOp::mark_unready_to_switch() -{ - m_ready_to_switch = false; -} - } /* namespace hailort */ diff --git a/hailort/libhailort/src/vdevice/scheduler/scheduled_core_op_state.hpp b/hailort/libhailort/src/vdevice/scheduler/scheduled_core_op_state.hpp index 29b50ae..e18eefa 100644 --- a/hailort/libhailort/src/vdevice/scheduler/scheduled_core_op_state.hpp +++ b/hailort/libhailort/src/vdevice/scheduler/scheduled_core_op_state.hpp @@ -3,7 +3,7 @@ * Distributed under the MIT license (https://opensource.org/licenses/MIT) **/ /** - * @file network_group_scheduler.hpp + * @file scheduler.hpp * @brief Class declaration for CoreOpsScheduler that schedules core-ops to be active depending on the scheduling algorithm. **/ @@ -18,6 +18,8 @@ #include "core_op/core_op.hpp" +#include "scheduler_base.hpp" + #include #include @@ -27,7 +29,7 @@ namespace hailort #define DEFAULT_SCHEDULER_TIMEOUT (std::chrono::milliseconds(0)) #define DEFAULT_SCHEDULER_MIN_THRESHOLD (0) -#define INVALID_DEVICE_ID (UINT32_MAX) +#define INVALID_DEVICE_ID (std::to_string(UINT32_MAX)) using stream_name_t = std::string; using core_op_priority_t = uint8_t; @@ -111,87 +113,70 @@ public: ScheduledCoreOp &operator=(ScheduledCoreOp &&other) = delete; ScheduledCoreOp(ScheduledCoreOp &&other) noexcept = delete; - bool has_enough_space_in_read_buffers(uint32_t ongoing_frames); - uint16_t get_min_input_buffers_count(uint32_t device_count); - bool has_input_written_most_frames(const std::string &stream_name); - std::unordered_map total_written_frames_count(); - bool has_pending_frames(); - bool can_stream_read(const std::string &stream_name); - bool can_stream_write(const std::string &stream_name); - bool use_dynamic_batch_flow(); - bool has_core_op_drained_everything(); - void decrease_current_core_op_counters(); - uint32_t get_pre_transfer_h2d_frames_count(); - - bool is_ready_to_switch(); - void mark_ready_to_switch(); - void mark_unready_to_switch(); - std::string get_core_op_name(); - std::shared_ptr get_core_op(); + const std::vector &get_outputs_names(); + const std::vector &get_inputs_names(); - void mark_frame_sent(); + uint16_t get_min_input_buffers_count(); + uint16_t get_min_output_buffers_count(); - std::chrono::time_point get_last_run_timestamp(); - void set_last_run_timestamp(const std::chrono::time_point ×tamp); + uint16_t get_max_batch_size(); + bool use_dynamic_batch_flow(); + bool has_core_op_drained_everything(); + + device_id_t get_last_device(); + void set_last_device(const device_id_t &device_id); Expected get_timeout(const stream_name_t &stream_name = ""); hailo_status set_timeout(const std::chrono::milliseconds &timeout, const stream_name_t &stream_name = ""); Expected get_threshold(const stream_name_t &stream_name); hailo_status set_threshold(uint32_t threshold, const stream_name_t &stream_name = ""); - core_op_priority_t get_priority(); void set_priority(core_op_priority_t priority); - uint32_t get_last_device_index(); - void set_last_device_index(uint32_t device_index); + std::chrono::time_point get_last_run_timestamp(); + void set_last_run_timestamp(const std::chrono::time_point ×tamp); - uint16_t get_max_batch_size(); + void mark_frame_sent(); + void decrease_current_core_op_counters(); - Counter &requested_write_frames(); - std::atomic_uint32_t &requested_write_frames(const stream_name_t &stream_name); - Counter &finished_write_frames(); - std::atomic_uint32_t &finished_write_frames(const stream_name_t &stream_name); - uint32_t finished_write_frames_min_value(); + Counter &pending_to_send_frames(); + std::atomic_uint32_t &pending_to_send_frames(const stream_name_t &stream_name); + uint32_t pending_to_send_frames_min_value(); Counter &h2d_finished_transferred_frames(); std::atomic_uint32_t &h2d_finished_transferred_frames(const stream_name_t &stream_name); + uint32_t h2d_finished_transferred_frames_max_value(); Counter &requested_read_frames(); std::atomic_uint32_t &requested_read_frames(const stream_name_t &stream_name); Counter &d2h_finished_transferred_frames(); std::atomic_uint32_t &d2h_finished_transferred_frames(const stream_name_t &stream_name); + Counter &finished_read_frames(); std::atomic_uint32_t &finished_read_frames(const stream_name_t &stream_name); uint32_t finished_read_frames_min_value(); - const std::vector &get_outputs_names(); - const std::vector &get_inputs_names(); bool is_nms() { return m_is_nms; } - void push_device_index(uint32_t device_index); - uint32_t pop_device_index(const stream_name_t &stream_name); - ScheduledCoreOp(std::shared_ptr core_op, std::chrono::milliseconds timeout, - uint16_t max_batch_size, StreamInfoVector &stream_infos, std::string core_op_name); + uint16_t max_batch_size, bool use_dynamic_batch_flow, StreamInfoVector &stream_infos, std::string core_op_name); private: std::shared_ptr m_core_op; - std::chrono::time_point m_last_run_time_stamp; std::chrono::milliseconds m_timeout; - std::atomic_bool m_frame_was_sent; uint16_t m_max_batch_size; + bool m_use_dynamic_batch_flow; - Counter m_requested_write_frames; // 'wait_for_write()' has been called - Counter m_finished_write_frames; // 'signal_finished_write()' has been called - frame is written in buffer (writes are a-sync) + Counter m_pending_to_send_frames; // 'signal_frame_pending_to_send()' has been called - frame is written in buffer (writes are a-sync) Counter m_h2d_finished_transferred_frames; // Frame has been transferred to device (intrpt was raised) @@ -204,19 +189,14 @@ private: core_op_priority_t m_priority; - std::atomic_uint32_t m_last_device_index; + device_id_t m_last_device_id; std::string m_core_op_name; std::vector m_inputs_names; std::vector m_outputs_names; - std::unordered_map> m_output_streams_read_orders; - bool m_is_nms; - - // TODO: Remove this flag when the old scheduling mode will be deprecated - std::atomic_bool m_ready_to_switch; }; } /* namespace hailort */ diff --git a/hailort/libhailort/src/vdevice/scheduler/scheduled_stream.cpp b/hailort/libhailort/src/vdevice/scheduler/scheduled_stream.cpp new file mode 100644 index 0000000..f1b64f4 --- /dev/null +++ b/hailort/libhailort/src/vdevice/scheduler/scheduled_stream.cpp @@ -0,0 +1,361 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file scheduled_stream.cpp + * @brief Internal stream implementation for scheduled streams + * + **/ + +#include "scheduled_stream.hpp" + +#include "utils/profiler/tracer_macros.hpp" + +namespace hailort +{ + +/** Input stream **/ +Expected> ScheduledInputStream::create( + std::map> &&streams, + const scheduler_core_op_handle_t &core_op_handle, + EventPtr &&core_op_activated_event, + const LayerInfo &layer_info, + CoreOpsSchedulerWeakPtr core_ops_scheduler) +{ + auto status = HAILO_UNINITIALIZED; + auto local_vdevice_stream = make_unique_nothrow(std::move(streams), + core_op_handle, std::move(core_op_activated_event), layer_info, + core_ops_scheduler, status); + CHECK_NOT_NULL_AS_EXPECTED(local_vdevice_stream, HAILO_OUT_OF_HOST_MEMORY); + CHECK_SUCCESS_AS_EXPECTED(status); + + return local_vdevice_stream; +} + +hailo_status ScheduledInputStreamBase::abort() +{ + return abort_impl(m_core_op_handle); +} + +hailo_status ScheduledInputStreamBase::abort_impl(scheduler_core_op_handle_t core_op_handle) +{ + auto status = HAILO_SUCCESS; // Best effort + assert(1 == m_streams.size()); + auto abort_status = m_streams.begin()->second.get().abort(); + if (HAILO_SUCCESS != status) { + LOGGER__ERROR("Failed to abort input stream. (status: {} device: {})", status, m_streams.begin()->second.get().get_dev_id()); + status = abort_status; + } + + auto core_ops_scheduler = m_core_ops_scheduler.lock(); + CHECK(core_ops_scheduler, HAILO_INTERNAL_FAILURE); + + auto disable_status = core_ops_scheduler->disable_stream(core_op_handle, name()); + if (HAILO_SUCCESS != disable_status) { + LOGGER__ERROR("Failed to disable stream in the core-op scheduler. (status: {})", disable_status); + status = disable_status; + } + + return status; +} + +hailo_status ScheduledInputStreamBase::clear_abort() +{ + return clear_abort_impl(m_core_op_handle); +} + +hailo_status ScheduledInputStreamBase::flush() +{ + auto core_ops_scheduler = m_core_ops_scheduler.lock(); + CHECK(core_ops_scheduler, HAILO_INTERNAL_FAILURE); + + auto status = core_ops_scheduler->flush_pending_buffers(m_core_op_handle, name(), get_timeout()); + if (HAILO_STREAM_ABORTED_BY_USER == status) { + LOGGER__INFO("Got HAILO_STREAM_ABORTED_BY_USER in flush of stream {}", name()); + return status; + } + CHECK_SUCCESS(status); + + return VDeviceInputStreamBase::flush(); +} + +hailo_status ScheduledInputStreamBase::clear_abort_impl(scheduler_core_op_handle_t core_op_handle) +{ + auto status = HAILO_SUCCESS; // Best effort + assert(1 == m_streams.size()); + auto clear_abort_status = m_streams.begin()->second.get().clear_abort(); + if ((HAILO_SUCCESS != clear_abort_status) && (HAILO_STREAM_NOT_ACTIVATED != clear_abort_status)) { + LOGGER__ERROR("Failed to clear abort input stream. (status: {} device: {})", clear_abort_status, m_streams.begin()->second.get().get_dev_id()); + status = clear_abort_status; + } + + auto core_ops_scheduler = m_core_ops_scheduler.lock(); + CHECK(core_ops_scheduler, HAILO_INTERNAL_FAILURE); + + auto enable_status = core_ops_scheduler->enable_stream(core_op_handle, name()); + if (HAILO_SUCCESS != enable_status) { + LOGGER__ERROR("Failed to enable stream in the core-op scheduler. (status: {})", enable_status); + status = enable_status; + } + + return status; +} + +hailo_status ScheduledInputStream::write_impl(const MemoryView &buffer, const std::function &should_cancel) +{ + auto core_ops_scheduler = m_core_ops_scheduler.lock(); + CHECK(core_ops_scheduler, HAILO_INTERNAL_FAILURE); + + assert(1 == m_streams.size()); + auto status = m_streams.begin()->second.get().write_buffer_only(buffer, should_cancel); + if (HAILO_SUCCESS != status) { + LOGGER__INFO("Write to stream has failed! status = {}", status); + return status; + } + + auto write_finish_status = core_ops_scheduler->signal_frame_pending_to_send(m_core_op_handle, name()); + if (HAILO_STREAM_ABORTED_BY_USER == write_finish_status) { + return write_finish_status; + } + CHECK_SUCCESS(write_finish_status); + + return HAILO_SUCCESS; +} + +Expected> ScheduledAsyncInputStream::create( + std::map> &&streams, + const scheduler_core_op_handle_t &core_op_handle, + EventPtr &&core_op_activated_event, + const LayerInfo &layer_info, + CoreOpsSchedulerWeakPtr core_ops_scheduler) +{ + auto max_queue_size_per_stream = streams.begin()->second.get().get_buffer_frames_size(); + CHECK_EXPECTED(max_queue_size_per_stream); + const auto max_queue_size = max_queue_size_per_stream.value() * streams.size(); + + auto status = HAILO_UNINITIALIZED; + auto local_vdevice_stream = make_unique_nothrow(std::move(streams), + core_op_handle, std::move(core_op_activated_event), layer_info, + core_ops_scheduler, max_queue_size, status); + CHECK_NOT_NULL_AS_EXPECTED(local_vdevice_stream, HAILO_OUT_OF_HOST_MEMORY); + CHECK_SUCCESS_AS_EXPECTED(status); + + return local_vdevice_stream; +} + +hailo_status ScheduledAsyncInputStream::send_pending_buffer(const device_id_t &device_id) +{ + // TODO HRT-10583 - allow option to remove reorder queue + auto pending_buffer = m_pending_buffers.dequeue(); + CHECK_EXPECTED_AS_STATUS(pending_buffer); + + pending_buffer->callback = m_callback_reorder_queue.wrap_callback(pending_buffer->callback); + assert(contains(m_streams, device_id)); + auto status = m_streams.at(device_id).get().write_async(pending_buffer.release()); + if (HAILO_SUCCESS != status) { + m_callback_reorder_queue.cancel_last_callback(); + } + return status; +} + +hailo_status ScheduledAsyncInputStream::wait_for_async_ready(size_t transfer_size, std::chrono::milliseconds timeout) +{ + (void)transfer_size; + return m_pending_buffers.wait_for_room(timeout); +} + +hailo_status ScheduledAsyncInputStream::write_async(TransferRequest &&transfer_request) +{ + auto core_ops_scheduler = m_core_ops_scheduler.lock(); + CHECK(core_ops_scheduler, HAILO_INTERNAL_FAILURE); + + auto status = m_pending_buffers.enqueue(std::move(transfer_request)); + CHECK_SUCCESS(status); + + auto write_finish_status = core_ops_scheduler->signal_frame_pending_to_send(m_core_op_handle, name()); + if (HAILO_STREAM_ABORTED_BY_USER == write_finish_status) { + return write_finish_status; + } + CHECK_SUCCESS(write_finish_status); + + return HAILO_SUCCESS; +} + +Expected ScheduledAsyncInputStream::get_async_max_queue_size() const +{ + return m_pending_buffers.max_size(); +} + + +hailo_status ScheduledAsyncInputStream::abort() +{ + m_pending_buffers.abort(); + return ScheduledInputStreamBase::abort(); +} + +hailo_status ScheduledAsyncInputStream::clear_abort() +{ + m_pending_buffers.clear_abort(); + return ScheduledInputStreamBase::clear_abort(); +} + +hailo_status ScheduledAsyncInputStream::write_impl(const MemoryView &, const std::function &) +{ + LOGGER__ERROR("Sync write is not supported by async streams"); + return HAILO_NOT_SUPPORTED; +} + +/** Output stream **/ +Expected> ScheduledOutputStream::create( + std::map> &&streams, + const scheduler_core_op_handle_t &core_op_handle, + const LayerInfo &layer_info, + EventPtr &&core_op_activated_event, + CoreOpsSchedulerWeakPtr core_ops_scheduler) +{ + auto status = HAILO_UNINITIALIZED; + auto stream = make_unique_nothrow(std::move(streams), core_op_handle, + layer_info, std::move(core_op_activated_event), core_ops_scheduler, status); + CHECK_NOT_NULL_AS_EXPECTED(stream, HAILO_OUT_OF_HOST_MEMORY); + CHECK_SUCCESS_AS_EXPECTED(status); + return stream; +} + +ScheduledOutputStream::ScheduledOutputStream( + std::map> &&streams, + const scheduler_core_op_handle_t &core_op_handle, + const LayerInfo &layer_info, + EventPtr &&core_op_activated_event, + CoreOpsSchedulerWeakPtr core_ops_scheduler, + hailo_status &status) : ScheduledOutputStreamBase(std::move(streams), core_op_handle, layer_info, + std::move(core_op_activated_event), core_ops_scheduler, status) + { + for (auto &stream_pair : m_streams) { + stream_pair.second.get().register_interrupt_callback( + [scheduler_weak=m_core_ops_scheduler, core_op_handle=m_core_op_handle, name=name(), device_id=stream_pair.first]() { + auto scheduler = scheduler_weak.lock(); + assert(scheduler); + scheduler->signal_frame_transferred_d2h(core_op_handle, name, device_id); + } + ); + } + } + +hailo_status ScheduledOutputStream::set_next_device_to_read(const device_id_t &device_id) +{ + std::lock_guard lock(m_device_read_order_mutex); + m_device_read_order.push(device_id); + return HAILO_SUCCESS; +} + +hailo_status ScheduledOutputStreamBase::abort() +{ + return abort_impl(m_core_op_handle); +} + +hailo_status ScheduledOutputStreamBase::abort_impl(scheduler_core_op_handle_t core_op_handle) +{ + auto status = HAILO_SUCCESS; // Best effort + for (const auto &pair : m_streams) { + auto &stream = pair.second; + auto abort_status = stream.get().abort(); + if (HAILO_SUCCESS != status) { + LOGGER__ERROR("Failed to abort output stream. (status: {} device: {})", status, stream.get().get_dev_id()); + status = abort_status; + } + } + auto core_ops_scheduler = m_core_ops_scheduler.lock(); + CHECK(core_ops_scheduler, HAILO_INTERNAL_FAILURE); + + auto disable_status = core_ops_scheduler->disable_stream(core_op_handle, name()); + if (HAILO_SUCCESS != disable_status) { + LOGGER__ERROR("Failed to disable stream in the core-op scheduler. (status: {})", disable_status); + status = disable_status; + } + + return status; +} + +hailo_status ScheduledOutputStreamBase::clear_abort() +{ + return clear_abort_impl(m_core_op_handle); +} + +hailo_status ScheduledOutputStreamBase::clear_abort_impl(scheduler_core_op_handle_t core_op_handle) +{ + auto status = HAILO_SUCCESS; // Best effort + for (const auto &pair : m_streams) { + auto &stream = pair.second; + auto clear_abort_status = stream.get().clear_abort(); + if ((HAILO_SUCCESS != clear_abort_status) && (HAILO_STREAM_NOT_ACTIVATED != clear_abort_status)) { + LOGGER__ERROR("Failed to clear abort output stream. (status: {} device: {})", clear_abort_status, stream.get().get_dev_id()); + status = clear_abort_status; + } + } + + auto core_ops_scheduler = m_core_ops_scheduler.lock(); + CHECK(core_ops_scheduler, HAILO_INTERNAL_FAILURE); + + auto enable_status = core_ops_scheduler->enable_stream(core_op_handle, name()); + if (HAILO_SUCCESS != enable_status) { + LOGGER__ERROR("Failed to enable stream in the core-op scheduler. (status: {})", enable_status); + status = enable_status; + } + + return status; +} + +hailo_status ScheduledOutputStream::read(MemoryView buffer) +{ + auto core_ops_scheduler = m_core_ops_scheduler.lock(); + CHECK(core_ops_scheduler, HAILO_INTERNAL_FAILURE); + + auto status = core_ops_scheduler->signal_frame_pending_to_read(m_core_op_handle, name()); + CHECK_SUCCESS(status); + + auto device_id = wait_for_read(); + if (HAILO_STREAM_ABORTED_BY_USER == device_id.status()) { + LOGGER__INFO("Read from stream was aborted."); + return device_id.status(); + } + CHECK_EXPECTED_AS_STATUS(device_id); + + assert(contains(m_streams, device_id.value())); + status = m_streams.at(device_id.value()).get().read(buffer); + if (HAILO_SUCCESS != status) { + LOGGER__INFO("Read from stream has failed! status = {}", status); + return status; + } + + status = core_ops_scheduler->signal_read_finish(m_core_op_handle, name(), device_id.value()); + if (HAILO_STREAM_ABORTED_BY_USER == status) { + return status; + } + CHECK_SUCCESS(status); + + return HAILO_SUCCESS; +} + +Expected ScheduledOutputStream::wait_for_read() +{ + auto core_ops_scheduler = m_core_ops_scheduler.lock(); + CHECK_AS_EXPECTED(core_ops_scheduler, HAILO_INTERNAL_FAILURE); + + auto status = core_ops_scheduler->wait_for_read(m_core_op_handle, name(), get_timeout(), [this]() { + std::lock_guard lock(m_device_read_order_mutex); + return !m_device_read_order.empty(); + }); + if (HAILO_STREAM_ABORTED_BY_USER == status) { + LOGGER__INFO("Read from stream was aborted."); + return make_unexpected(status); + } + CHECK_SUCCESS_AS_EXPECTED(status); + + std::lock_guard lock(m_device_read_order_mutex); + auto device_id = m_device_read_order.front(); + m_device_read_order.pop(); + return device_id; +} + +} /* namespace hailort */ diff --git a/hailort/libhailort/src/vdevice/scheduler/scheduled_stream.hpp b/hailort/libhailort/src/vdevice/scheduler/scheduled_stream.hpp index fb89a62..5aa530c 100644 --- a/hailort/libhailort/src/vdevice/scheduler/scheduled_stream.hpp +++ b/hailort/libhailort/src/vdevice/scheduler/scheduled_stream.hpp @@ -17,29 +17,29 @@ #include "stream_common/stream_internal.hpp" #include "vdevice/vdevice_internal.hpp" #include "vdevice/vdevice_stream.hpp" +#include "vdevice/callback_reorder_queue.hpp" #include "vdma/vdma_device.hpp" namespace hailort { -class ScheduledInputStream : public InputVDeviceBaseStream { + +class ScheduledInputStreamBase : public VDeviceInputStreamBase { public: - ScheduledInputStream( - std::vector> &&streams, + ScheduledInputStreamBase( + std::map> &&streams, const scheduler_core_op_handle_t &core_op_handle, EventPtr &&core_op_activated_event, const LayerInfo &layer_info, CoreOpsSchedulerWeakPtr core_ops_scheduler, hailo_status &status) : - InputVDeviceBaseStream(std::move(streams), std::move(core_op_activated_event), layer_info, status), + VDeviceInputStreamBase(std::move(streams), std::move(core_op_activated_event), layer_info, status), m_core_op_handle(core_op_handle), m_core_ops_scheduler(core_ops_scheduler) {} - virtual hailo_status abort() override; - virtual hailo_status clear_abort() override; - virtual bool is_scheduled() override { return true; }; + virtual bool is_scheduled() override final { return true; }; virtual void notify_all() override { @@ -50,18 +50,17 @@ public: } scheduler->notify_all(); - for (auto &stream : m_streams) { + for (const auto &pair : m_streams) { + auto &stream = pair.second; stream.get().notify_all(); } } -protected: - virtual Expected sync_write_raw_buffer(const MemoryView &buffer, - const std::function &should_cancel = []() { return false; }); - - Expected sync_write_raw_buffer_impl(const MemoryView &buffer, scheduler_core_op_handle_t core_op_handle, - const std::function &should_cancel); + virtual hailo_status abort() override; + virtual hailo_status clear_abort() override; + virtual hailo_status flush() override; +protected: scheduler_core_op_handle_t m_core_op_handle; CoreOpsSchedulerWeakPtr m_core_ops_scheduler; @@ -70,27 +69,179 @@ private: hailo_status clear_abort_impl(scheduler_core_op_handle_t core_op_handle); }; -class ScheduledOutputStream : public OutputVDeviceBaseStream { +class ScheduledInputStream : public ScheduledInputStreamBase { public: - ScheduledOutputStream( - std::vector> &&streams, + static Expected> create( + std::map> &&streams, const scheduler_core_op_handle_t &core_op_handle, + EventPtr &&core_op_activated_event, const LayerInfo &layer_info, + CoreOpsSchedulerWeakPtr core_ops_scheduler); + + ScheduledInputStream( + std::map> &&streams, + const scheduler_core_op_handle_t &core_op_handle, EventPtr &&core_op_activated_event, + const LayerInfo &layer_info, CoreOpsSchedulerWeakPtr core_ops_scheduler, hailo_status &status) : - OutputVDeviceBaseStream(std::move(streams), layer_info, std::move(core_op_activated_event), status), + ScheduledInputStreamBase(std::move(streams), core_op_handle, std::move(core_op_activated_event), layer_info, + core_ops_scheduler, status) + {} + +protected: + virtual hailo_status write_impl(const MemoryView &buffer, const std::function &should_cancel) override; +}; + +class TransferRequestsQueue final { +public: + TransferRequestsQueue(size_t max_size) : + m_max_size(max_size) + {} + + ~TransferRequestsQueue() + { + while (!m_queue.empty()) { + auto &request = m_queue.front(); + request.callback(HAILO_STREAM_ABORTED_BY_USER); + m_queue.pop(); + } + } + + TransferRequestsQueue(const TransferRequestsQueue &) = delete; + TransferRequestsQueue &operator=(const TransferRequestsQueue &) = delete; + + hailo_status wait_for_room(std::chrono::milliseconds timeout) + { + std::unique_lock lock(m_mutex); + auto result = m_dequeue_cv.wait_for(lock, timeout, + [&] { + return m_is_aborted || (m_queue.size() < m_max_size); + }); + if (!result) { + return HAILO_TIMEOUT; + } + if (m_is_aborted) { + return HAILO_STREAM_ABORTED_BY_USER; + } + return HAILO_SUCCESS; + } + + hailo_status enqueue(TransferRequest &&transfer_request) + { + std::unique_lock lock(m_mutex); + if (m_is_aborted) { + return HAILO_STREAM_ABORTED_BY_USER; + } + CHECK(m_queue.size() < m_max_size, HAILO_QUEUE_IS_FULL, "No space left in stream queue"); + m_queue.emplace(std::move(transfer_request)); + return HAILO_SUCCESS; + } + + Expected dequeue() + { + TransferRequest transfer_request{}; + { + std::unique_lock lock(m_mutex); + if (m_is_aborted) { + return make_unexpected(HAILO_STREAM_ABORTED_BY_USER); + } + CHECK_AS_EXPECTED(!m_queue.empty(), HAILO_INTERNAL_FAILURE, "Queue should not be empty"); + transfer_request = m_queue.front(); + m_queue.pop(); + } + m_dequeue_cv.notify_one(); + return transfer_request; + } + + void abort() + { + { + std::unique_lock lock(m_mutex); + m_is_aborted = true; + } + + m_dequeue_cv.notify_all(); + } + + void clear_abort() + { + std::unique_lock lock(m_mutex); + m_is_aborted = false; + } + + size_t max_size() const { return m_max_size; } + +private: + // TODO: use SpscQueue (HRT-10554) + const size_t m_max_size; + std::mutex m_mutex; + bool m_is_aborted = false; + std::condition_variable m_dequeue_cv; + std::queue m_queue; +}; + +class ScheduledAsyncInputStream : public ScheduledInputStreamBase { +public: + + static Expected> create( + std::map> &&streams, + const scheduler_core_op_handle_t &core_op_handle, + EventPtr &&core_op_activated_event, + const LayerInfo &layer_info, + CoreOpsSchedulerWeakPtr core_ops_scheduler); + + ScheduledAsyncInputStream( + std::map> &&streams, + const scheduler_core_op_handle_t &core_op_handle, + EventPtr &&core_op_activated_event, + const LayerInfo &layer_info, + CoreOpsSchedulerWeakPtr core_ops_scheduler, + size_t max_queue_size, + hailo_status &status) : + ScheduledInputStreamBase(std::move(streams), core_op_handle, std::move(core_op_activated_event), layer_info, + core_ops_scheduler, status), + m_pending_buffers(max_queue_size), + m_callback_reorder_queue(max_queue_size) // TODO HRT-1058 - use reorder queue only when needed + {} + + virtual hailo_status send_pending_buffer(const device_id_t &device_id) override; + virtual hailo_status wait_for_async_ready(size_t transfer_size, std::chrono::milliseconds timeout) override; + virtual hailo_status write_async(TransferRequest &&transfer_request) override; + virtual Expected get_async_max_queue_size() const override; + virtual hailo_status abort() override; + virtual hailo_status clear_abort() override; + +protected: + virtual hailo_status write_impl(const MemoryView &, const std::function &) override; + + // All buffers written by the user using write_async are first stored in this queue. + // When the scheduler decides to activate the network on a specific device, send_pending_buffer is called, and + // the buffers are sent to the underlying stream. + TransferRequestsQueue m_pending_buffers; + CallbackReorderQueue m_callback_reorder_queue; +}; + +class ScheduledOutputStreamBase : public VDeviceOutputStreamBase { +public: + ScheduledOutputStreamBase( + std::map> &&streams, + const scheduler_core_op_handle_t &core_op_handle, + const LayerInfo &layer_info, + EventPtr &&core_op_activated_event, + CoreOpsSchedulerWeakPtr core_ops_scheduler, + hailo_status &status) : + VDeviceOutputStreamBase(std::move(streams), layer_info, std::move(core_op_activated_event), status), m_core_op_handle(core_op_handle), m_core_ops_scheduler(core_ops_scheduler) {} + virtual bool is_scheduled() override { return true; }; + virtual hailo_status abort() override; virtual hailo_status clear_abort() override; - virtual bool is_scheduled() override { return true; }; protected: - virtual hailo_status read(MemoryView buffer) override; - hailo_status read_impl(MemoryView buffer, scheduler_core_op_handle_t core_op_handle); scheduler_core_op_handle_t m_core_op_handle; CoreOpsSchedulerWeakPtr m_core_ops_scheduler; @@ -100,6 +251,38 @@ private: hailo_status clear_abort_impl(scheduler_core_op_handle_t core_op_handle); }; + +class ScheduledOutputStream : public ScheduledOutputStreamBase { +public: + static Expected> create( + std::map> &&streams, + const scheduler_core_op_handle_t &core_op_handle, + const LayerInfo &layer_info, + EventPtr &&core_op_activated_event, + CoreOpsSchedulerWeakPtr core_ops_scheduler); + + ScheduledOutputStream( + std::map> &&streams, + const scheduler_core_op_handle_t &core_op_handle, + const LayerInfo &layer_info, + EventPtr &&core_op_activated_event, + CoreOpsSchedulerWeakPtr core_ops_scheduler, + hailo_status &status); + + virtual hailo_status set_next_device_to_read(const device_id_t &device_id) override; + +protected: + virtual hailo_status read(MemoryView buffer) override; + +private: + + // Returns device id to read from + Expected wait_for_read(); + + std::queue m_device_read_order; + std::mutex m_device_read_order_mutex; +}; + } /* namespace hailort */ #endif /* HAILO_SCHEDULED_STREAM_HPP_ */ diff --git a/hailort/libhailort/src/vdevice/scheduler/scheduler.cpp b/hailort/libhailort/src/vdevice/scheduler/scheduler.cpp new file mode 100644 index 0000000..c14f49a --- /dev/null +++ b/hailort/libhailort/src/vdevice/scheduler/scheduler.cpp @@ -0,0 +1,778 @@ +/** + * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file scheduler.cpp + * @brief: Network scheduler + **/ + +#include "common/os_utils.hpp" + + +#include "vdevice/scheduler/scheduler.hpp" +#include "vdevice/vdevice_core_op.hpp" +#include "vdevice/scheduler/scheduler_oracle.hpp" +#include "vdevice/vdevice_stream_multiplexer_wrapper.hpp" +#include "hef/hef_internal.hpp" +#include "utils/profiler/tracer_macros.hpp" + +#include + + +namespace hailort +{ + +#define SINGLE_CONTEXT_BATCH_SIZE (1) +#define DEFAULT_BURST_SIZE (1) + +// TODO: use device handles instead device count +CoreOpsScheduler::CoreOpsScheduler(hailo_scheduling_algorithm_t algorithm, std::vector &devices_ids, + std::vector &devices_arch) : + SchedulerBase(algorithm, devices_ids, devices_arch), + m_should_core_op_stop(), + m_before_read_write_mutex(), + m_core_ops_cvs(), + m_scheduler_cv() +{ + TRACE(SchedulerStartTrace, get_device_count()); + for (const auto &pair : m_devices) { + auto &device_info = pair.second; + TRACE(AddDeviceTrace, device_info->device_id, device_info->device_arch); + } + + m_is_running = true; + m_scheduler_thread = std::thread(&CoreOpsScheduler::worker_thread_main, this); + m_execute_worker_thread = true; +} + +CoreOpsScheduler::~CoreOpsScheduler() +{ + for (const auto &pair : m_devices) { + auto &device_info = pair.second; + if (INVALID_CORE_OP_HANDLE != device_info->current_core_op_handle) { + auto current_core_op = m_scheduled_core_ops[device_info->current_core_op_handle]->get_core_op(); + auto current_core_op_bundle = std::dynamic_pointer_cast(current_core_op); + assert(nullptr != current_core_op_bundle); + auto vdma_core_op = current_core_op_bundle->get_core_op_by_device_id(device_info->device_id); + if (!vdma_core_op) { + LOGGER__ERROR("Error retrieving core-op in scheduler destructor"); + } else { + if (HAILO_SUCCESS != VdmaConfigManager::deactivate_core_op(vdma_core_op.value())) { + LOGGER__ERROR("Error deactivating core-op when destroying scheduler"); + } + } + } + } + + // signal scheduler thread to stop and join + { + std::unique_lock lock(m_before_read_write_mutex); + m_is_running = false; + m_execute_worker_thread = true; + } + m_scheduler_cv.notify_one(); + if (m_scheduler_thread.joinable()) { + m_scheduler_thread.join(); + } +} + +Expected CoreOpsScheduler::create_round_robin(std::vector &devices_bdf_id, std::vector &devices_arch) +{ + auto ptr = make_shared_nothrow(HAILO_SCHEDULING_ALGORITHM_ROUND_ROBIN, devices_bdf_id, devices_arch); + CHECK_AS_EXPECTED(nullptr != ptr, HAILO_OUT_OF_HOST_MEMORY); + + return ptr; +} + +std::string CoreOpsScheduler::get_core_op_name(const scheduler_core_op_handle_t &core_op_handle) +{ + assert(m_scheduled_core_ops.size() > core_op_handle); + return m_scheduled_core_ops[core_op_handle]->get_core_op_name(); +} + +Expected CoreOpsScheduler::add_core_op(std::shared_ptr added_cng) +{ + scheduler_core_op_handle_t core_op_handle = INVALID_CORE_OP_HANDLE; + { + std::unique_lock lock(m_before_read_write_mutex); + core_op_handle = static_cast(m_scheduled_core_ops.size()); + + auto stream_infos = added_cng->get_all_stream_infos(); + CHECK_EXPECTED(stream_infos); + + auto scheduled_core_op = ScheduledCoreOp::create(added_cng, stream_infos.value()); + CHECK_EXPECTED(scheduled_core_op); + + bool is_nms = scheduled_core_op->get()->is_nms(); + TRACE(AddCoreOpTrace, "", added_cng->name(), DEFAULT_SCHEDULER_TIMEOUT.count(), DEFAULT_SCHEDULER_MIN_THRESHOLD, + core_op_handle, is_nms); + + m_scheduled_core_ops.emplace_back(scheduled_core_op.release()); + + + for (const auto &stream_info : stream_infos.value()) { + m_should_core_op_stop[core_op_handle][stream_info.name] = false; + } + + for (const auto &pair : m_devices) { + auto &device_info = pair.second; + for (const auto &stream_info : stream_infos.value()) { + if (HAILO_H2D_STREAM == stream_info.direction) { + device_info->current_cycle_requested_transferred_frames_h2d[core_op_handle][stream_info.name] = 0; + } else { + device_info->current_cycle_finished_transferred_frames_d2h[core_op_handle][stream_info.name] = 0; + device_info->pending_to_read_frames[core_op_handle][stream_info.name] = 0; + } + } + } + + auto network_cvs = ScheduledCoreOpCV::create(added_cng); + CHECK_EXPECTED(network_cvs); + m_core_ops_cvs[core_op_handle] = network_cvs.release(); + m_core_op_priority[HAILO_SCHEDULER_PRIORITY_NORMAL].emplace_back(core_op_handle); + } + + return core_op_handle; +} + +bool CoreOpsScheduler::is_core_op_active(const scheduler_core_op_handle_t &core_op_handle) +{ + for (const auto &pair : m_devices) { + auto &device_info = pair.second; + if (core_op_handle == device_info->current_core_op_handle) { + return true; + } + } + + return false; +} + +bool CoreOpsScheduler::is_multi_device() +{ + return m_devices.size() > 1; +} + +hailo_status CoreOpsScheduler::signal_frame_pending_to_send(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name) +{ + { + std::unique_lock lock(m_before_read_write_mutex); + assert(m_scheduled_core_ops.size() > core_op_handle); + auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; + + if (should_core_op_stop(core_op_handle)) { + return HAILO_STREAM_ABORTED_BY_USER; + } + + TRACE(WriteFrameTrace, "", core_op_handle, stream_name); + + m_scheduled_core_ops[core_op_handle]->mark_frame_sent(); + scheduled_core_op->pending_to_send_frames().increase(stream_name); + m_execute_worker_thread = true; + } + m_scheduler_cv.notify_one(); + + return HAILO_SUCCESS; +} + +hailo_status CoreOpsScheduler::switch_core_op(const scheduler_core_op_handle_t &core_op_handle, const device_id_t &device_id, bool /*keep_nn_config*/) +{ + auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; + assert(contains(m_devices, device_id)); + auto curr_device_info = m_devices[device_id]; + curr_device_info->is_switching_core_op = false; + + // initialize current cycle maps + for (const auto &name : scheduled_core_op->get_inputs_names()) { + curr_device_info->current_cycle_requested_transferred_frames_h2d[core_op_handle][name] = 0; + } + + for (const auto &name : scheduled_core_op->get_outputs_names()) { + curr_device_info->current_cycle_finished_transferred_frames_d2h[core_op_handle][name] = 0; + } + + uint16_t batch_size = std::min(scheduled_core_op->get_max_batch_size(), get_min_avail_buffers_count(core_op_handle, device_id)); + uint16_t hw_batch_size = SINGLE_CONTEXT_BATCH_SIZE; + + if (scheduled_core_op->use_dynamic_batch_flow()) { + batch_size = std::min(static_cast(scheduled_core_op->pending_to_send_frames_min_value()), batch_size); + hw_batch_size = batch_size; + } + + if (batch_size == 0) { + return HAILO_SUCCESS; + } + + bool has_same_hw_batch_size_as_previous = scheduled_core_op->use_dynamic_batch_flow() ? (curr_device_info->current_batch_size == batch_size) : true; + curr_device_info->current_batch_size = batch_size; + + if ((core_op_handle != curr_device_info->current_core_op_handle) || (!has_same_hw_batch_size_as_previous)) { + assert(m_scheduled_core_ops.size() > core_op_handle); + auto next_active_cng = scheduled_core_op->get_core_op(); + auto next_active_cng_wrapper = std::dynamic_pointer_cast(next_active_cng); + assert(nullptr != next_active_cng_wrapper); + auto next_active_cng_expected = next_active_cng_wrapper->get_core_op_by_device_id(curr_device_info->device_id); + CHECK_EXPECTED_AS_STATUS(next_active_cng_expected); + + std::shared_ptr current_active_vdma_cng = nullptr; + if (curr_device_info->current_core_op_handle != INVALID_CORE_OP_HANDLE) { + auto current_active_cng = m_scheduled_core_ops[curr_device_info->current_core_op_handle]->get_core_op(); + auto current_active_cng_bundle = std::dynamic_pointer_cast(current_active_cng); + assert(nullptr != current_active_cng_bundle); + auto current_active_cng_expected = current_active_cng_bundle->get_core_op_by_device_id(curr_device_info->device_id); + CHECK_EXPECTED_AS_STATUS(current_active_cng_expected); + current_active_vdma_cng = current_active_cng_expected.release(); + + // Flushing h2d channel in order to make sure we got all interrupts before switching the network. + for (auto &stream : current_active_vdma_cng->get_input_streams()) { + auto status = stream.get().flush(); + if (HAILO_STREAM_ABORTED_BY_USER == status) { + continue; + } + CHECK_SUCCESS(status); + } + } + + TRACE(SwitchCoreOpTrace, device_id, core_op_handle); + static const auto RESUME_PENDING_STREAM_TRANSFERS = true; + auto status = VdmaConfigManager::switch_core_op(current_active_vdma_cng, next_active_cng_expected.value(), hw_batch_size, + RESUME_PENDING_STREAM_TRANSFERS); + CHECK_SUCCESS(status, "Failed switching core-op"); + } + + scheduled_core_op->set_last_run_timestamp(std::chrono::steady_clock::now()); // Mark timestamp on activation + curr_device_info->current_core_op_handle = core_op_handle; + + auto status = send_all_pending_buffers(core_op_handle, device_id, batch_size); + if (HAILO_STREAM_ABORTED_BY_USER == status) { + LOGGER__INFO("send_all_pending_buffers has failed with status=HAILO_STREAM_ABORTED_BY_USER"); + return status; + } + CHECK_SUCCESS(status); + + return HAILO_SUCCESS; +} + +void CoreOpsScheduler::signal_read_finish_impl(const scheduler_core_op_handle_t &core_op_handle, + const std::string &stream_name, const device_id_t &device_id) +{ + TRACE(ReadFrameTrace, "", core_op_handle, stream_name); + + auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; + scheduled_core_op->requested_read_frames().decrease(stream_name); + scheduled_core_op->finished_read_frames().increase(stream_name); + scheduled_core_op->d2h_finished_transferred_frames().decrease(stream_name); + + if (m_devices[device_id]->pending_to_read_frames[core_op_handle][stream_name] > 0) { + m_devices[device_id]->pending_to_read_frames[core_op_handle][stream_name]--; + } + + decrease_core_op_counters(core_op_handle); + + auto has_drained_everything = has_core_op_drained_everything(core_op_handle, device_id); + if (scheduled_core_op->is_nms() && has_drained_everything) { + // In NMS networks there is possibility that next wasn't choosen yet + choose_next_core_op(device_id, true); + + // If we didn't choose with threshold or timeout lets choose without threshold + if (!m_devices[device_id]->is_switching_core_op) { + choose_next_core_op(device_id, false); + } + + if (has_drained_everything) { + TRACE(CoreOpIdleTrace, device_id, core_op_handle); + } + } + + m_execute_worker_thread = true; +} + +hailo_status CoreOpsScheduler::send_all_pending_buffers(const scheduler_core_op_handle_t &core_op_handle, const device_id_t &device_id, uint32_t burst_size) +{ + auto current_device_info = m_devices[device_id]; + if ((INVALID_CORE_OP_HANDLE == current_device_info->current_core_op_handle) || (current_device_info->current_core_op_handle != core_op_handle)) { + return HAILO_SUCCESS; + } + + auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; + + for (size_t i = 0; i < burst_size; i++) { + auto finished_send = false; + for (const auto &name : scheduled_core_op->get_inputs_names()) { + if (scheduled_core_op->pending_to_send_frames(name) == 0) { + finished_send = true; + break; + } + } + if (finished_send) { + break; + } + + for (const auto &name : scheduled_core_op->get_outputs_names()) { + auto output_stream = scheduled_core_op->get_core_op()->get_output_stream_by_name(name); + CHECK_EXPECTED_AS_STATUS(output_stream); + + auto &output_stream_base = static_cast(output_stream->get()); + auto status = output_stream_base.set_next_device_to_read(device_id); + CHECK_SUCCESS(status); + } + + for (const auto &name : scheduled_core_op->get_inputs_names()) { + auto status = send_pending_buffer(core_op_handle, name, device_id); + if (HAILO_STREAM_ABORTED_BY_USER == status) { + LOGGER__INFO("send_pending_buffer has failed with status=HAILO_STREAM_ABORTED_BY_USER"); + return status; + } + CHECK_SUCCESS(status); + } + scheduled_core_op->set_last_device(device_id); + } + + return HAILO_SUCCESS; +} + +hailo_status CoreOpsScheduler::send_pending_buffer(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, + const device_id_t &device_id) +{ + assert(m_scheduled_core_ops.size() > core_op_handle); + auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; + + auto current_cng = scheduled_core_op->get_core_op(); + auto input_stream = current_cng->get_input_stream_by_name(stream_name); + CHECK_EXPECTED_AS_STATUS(input_stream); + + auto &input_stream_base = static_cast(input_stream->get()); + auto status = input_stream_base.send_pending_buffer(device_id); + if (HAILO_STREAM_ABORTED_BY_USER == status) { + LOGGER__INFO("send_pending_buffer has failed with status=HAILO_STREAM_ABORTED_BY_USER"); + return status; + } + CHECK_SUCCESS(status); + + TRACE(InputVdmaDequeueTrace, device_id, core_op_handle, stream_name); + + m_devices[device_id]->current_cycle_requested_transferred_frames_h2d[core_op_handle][stream_name]++; + scheduled_core_op->pending_to_send_frames().decrease(stream_name); + // Notifying for flush + m_core_ops_cvs[core_op_handle]->notify_one(stream_name); + + scheduled_core_op->h2d_finished_transferred_frames().increase(stream_name); + + if (should_core_op_stop(core_op_handle)) { + return HAILO_STREAM_ABORTED_BY_USER; + } + + return HAILO_SUCCESS; +} + +CoreOpsScheduler::ReadyInfo CoreOpsScheduler::is_core_op_ready(const scheduler_core_op_handle_t &core_op_handle, bool check_threshold) +{ + ReadyInfo result; + result.is_ready = false; + + if (should_core_op_stop(core_op_handle)) { + // Do not switch to an aborted core-op + return result; + } + + auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; + // Check if there arent any write requests + const bool has_pending_writes = scheduled_core_op->pending_to_send_frames_min_value() > 0; + + // Check for read request on all the output streams + const bool has_avail_pending_to_read_buffers = get_min_avail_output_buffers(core_op_handle) > 0; + + std::vector over_threshold; + over_threshold.reserve(scheduled_core_op->get_inputs_names().size()); + std::vector over_timeout; + over_timeout.reserve(scheduled_core_op->get_inputs_names().size()); + + if (check_threshold) { + for (const auto &name : scheduled_core_op->get_inputs_names()) { + auto threshold_exp = scheduled_core_op->get_threshold(name); + if (!threshold_exp) { + LOGGER__ERROR("Failed to get threshold for stream {}", name); + return result; + } + auto threshold = (DEFAULT_SCHEDULER_MIN_THRESHOLD == threshold_exp.value()) ? 1 : threshold_exp.value(); + auto timeout_exp = scheduled_core_op->get_timeout(); + if (!timeout_exp) { + LOGGER__ERROR("Failed to get timeout for stream {}", name); + return result; + } + auto timeout = timeout_exp.release(); + + // Check if there arent enough write requests to reach threshold and timeout didnt passed + uint32_t write_requests = scheduled_core_op->pending_to_send_frames(name); + auto stream_over_threshold = write_requests >= threshold; + auto stream_over_timeout = timeout <= (std::chrono::steady_clock::now() - scheduled_core_op->get_last_run_timestamp()); + over_threshold.push_back(stream_over_threshold); + over_timeout.push_back(stream_over_timeout); + if (stream_over_threshold || stream_over_timeout) { + continue; + } else { + result.is_ready = false; + return result; + } + } + result.over_threshold = std::all_of(over_threshold.begin(), over_threshold.end(), [](auto over) { return over; }); + result.over_timeout = std::all_of(over_timeout.begin(), over_timeout.end(), [](auto over) { return over; }); + } + + result.is_ready = has_pending_writes && has_avail_pending_to_read_buffers; + + return result; +} + +hailo_status CoreOpsScheduler::wait_for_read(const scheduler_core_op_handle_t &core_op_handle, + const std::string &stream_name, const std::chrono::milliseconds &timeout, const std::function &predicate) +{ + std::unique_lock lock(m_before_read_write_mutex); + + hailo_status status = HAILO_SUCCESS; + auto wait_res = m_core_ops_cvs[core_op_handle]->wait_for(stream_name, lock, timeout, + [this, core_op_handle, predicate, &stream_name, &status] { + if (m_should_core_op_stop[core_op_handle][stream_name]) { + status = HAILO_STREAM_ABORTED_BY_USER; + return true; // return true so that the wait will finish + } + + return predicate(); + }); + CHECK(wait_res, HAILO_TIMEOUT, "{} (D2H) failed with status={}, timeout={}ms", stream_name, HAILO_TIMEOUT, timeout.count()); + if (HAILO_STREAM_ABORTED_BY_USER == status) { + return status; + } + CHECK_SUCCESS(status); + + return HAILO_SUCCESS; +} + +hailo_status CoreOpsScheduler::signal_frame_pending_to_read(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name) +{ + { + std::unique_lock lock(m_before_read_write_mutex); + + auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; + scheduled_core_op->requested_read_frames().increase(stream_name); + m_execute_worker_thread = true; + } + m_scheduler_cv.notify_one(); + + return HAILO_SUCCESS; +} + +void CoreOpsScheduler::signal_frame_transferred_d2h(const scheduler_core_op_handle_t &core_op_handle, + const std::string &stream_name, const device_id_t &device_id) +{ + { + std::unique_lock lock(m_before_read_write_mutex); + + auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; + if (!scheduled_core_op->is_nms()) { + TRACE(OutputVdmaEnqueueTrace, "", core_op_handle, stream_name, 1); + // TODO: Remove d2h_finished_transferred_frames and use current_cycle_finished_transferred_frames_d2h instead + scheduled_core_op->d2h_finished_transferred_frames().increase(stream_name); + m_devices[device_id]->pending_to_read_frames[core_op_handle][stream_name] += 1; + m_devices[device_id]->current_cycle_finished_transferred_frames_d2h[core_op_handle][stream_name] += 1; + } + + auto has_drained_everything = has_core_op_drained_everything(core_op_handle, device_id); + + if (has_drained_everything) { + TRACE(CoreOpIdleTrace, device_id, core_op_handle); + } + + // If ng finished and we didn't choose next lets choose without checking threshold + if (!m_devices[device_id]->is_switching_core_op && has_drained_everything) { + auto was_chosen = choose_next_core_op(device_id, true); + if (!was_chosen) { + choose_next_core_op(device_id, false); + } + } + + if (m_devices[device_id]->is_switching_core_op) { + m_execute_worker_thread = true; + } + } + + // Notify stream that new frame was accepted (wait_for read operation) + m_core_ops_cvs[core_op_handle]->notify_one(stream_name); + m_scheduler_cv.notify_one(); +} + +hailo_status CoreOpsScheduler::signal_read_finish(const scheduler_core_op_handle_t &core_op_handle, + const std::string &stream_name, const device_id_t &device_id) +{ + { + std::unique_lock lock(m_before_read_write_mutex); + signal_read_finish_impl(core_op_handle, stream_name, device_id); + } + m_scheduler_cv.notify_one(); + return HAILO_SUCCESS; +} + +void CoreOpsScheduler::decrease_core_op_counters(const scheduler_core_op_handle_t &core_op_handle) +{ + return m_scheduled_core_ops[core_op_handle]->decrease_current_core_op_counters(); +} + +bool CoreOpsScheduler::has_core_op_drained_everything(const scheduler_core_op_handle_t &core_op_handle, const device_id_t &device_id) +{ + if (core_op_all_streams_aborted(core_op_handle)) { + // We treat core-op as drained only if all streams are aborted - to make sure there aren't any ongoing transfers + return true; + } + + if (INVALID_CORE_OP_HANDLE == core_op_handle) { + // If no core-op is running, consider it as drained + return true; + } + + if ((!m_scheduled_core_ops[core_op_handle]->is_nms()) && (is_multi_device() || m_scheduled_core_ops[core_op_handle]->use_dynamic_batch_flow())) { + auto current_device_info = m_devices[device_id]; + auto max_transferred_h2d = get_max_value_of_unordered_map(current_device_info->current_cycle_requested_transferred_frames_h2d[core_op_handle]); + auto min_transferred_d2h = get_min_value_of_unordered_map(current_device_info->current_cycle_finished_transferred_frames_d2h[core_op_handle]); + + return (max_transferred_h2d == min_transferred_d2h); + } + + return m_scheduled_core_ops[core_op_handle]->has_core_op_drained_everything(); +} + +hailo_status CoreOpsScheduler::flush_pending_buffers(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, + const std::chrono::milliseconds &timeout) +{ + std::unique_lock lock(m_before_read_write_mutex); + + hailo_status status = HAILO_SUCCESS; + auto wait_res = m_core_ops_cvs[core_op_handle]->wait_for(stream_name, lock, timeout, + [this, core_op_handle, &stream_name, &status] { + if (should_core_op_stop(core_op_handle)) { + status = HAILO_STREAM_ABORTED_BY_USER; + return true; // return true so that the wait will finish + } + + assert(m_scheduled_core_ops.size() > core_op_handle); + auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; + auto pending = scheduled_core_op->pending_to_send_frames(stream_name).load(); + return (pending == 0); + }); + CHECK(wait_res, HAILO_TIMEOUT, "{} (H2D) failed with status={}, timeout={}ms", stream_name, HAILO_TIMEOUT, timeout.count()); + if (HAILO_STREAM_ABORTED_BY_USER == status) { + LOGGER__INFO("flush pending buffers was aborted in stream ={}", stream_name); + return status; + } + CHECK_SUCCESS(status); + + return HAILO_SUCCESS; +} + +hailo_status CoreOpsScheduler::enable_stream(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name) +{ + { + std::unique_lock lock(m_before_read_write_mutex); + + if (!m_should_core_op_stop[core_op_handle][stream_name]) { + return HAILO_SUCCESS; + } + + m_should_core_op_stop[core_op_handle][stream_name] = false; + } + m_core_ops_cvs[core_op_handle]->notify_all(); + + return HAILO_SUCCESS; +} + +hailo_status CoreOpsScheduler::disable_stream(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name) +{ + { + std::unique_lock lock(m_before_read_write_mutex); + + if (m_should_core_op_stop[core_op_handle][stream_name]) { + return HAILO_SUCCESS; + } + + m_should_core_op_stop[core_op_handle][stream_name] = true; + } + m_core_ops_cvs[core_op_handle]->notify_all(); + + return HAILO_SUCCESS; +} + +hailo_status CoreOpsScheduler::set_timeout(const scheduler_core_op_handle_t &core_op_handle, const std::chrono::milliseconds &timeout, const std::string &/*network_name*/) +{ + // TODO: call in loop for set_timeout with the relevant stream-names (of the given network) + return m_scheduled_core_ops[core_op_handle]->set_timeout(timeout); +} + +hailo_status CoreOpsScheduler::set_threshold(const scheduler_core_op_handle_t &core_op_handle, uint32_t threshold, const std::string &/*network_name*/) +{ + // TODO: call in loop for set_timeout with the relevant stream-names (of the given network) + return m_scheduled_core_ops[core_op_handle]->set_threshold(threshold); +} + +hailo_status CoreOpsScheduler::set_priority(const scheduler_core_op_handle_t &core_op_handle, core_op_priority_t priority, const std::string &/*network_name*/) +{ + CHECK(priority <= HAILO_SCHEDULER_PRIORITY_MAX, HAILO_INVALID_ARGUMENT); + std::unique_lock lock(m_before_read_write_mutex); + auto old_priority = m_scheduled_core_ops[core_op_handle]->get_priority(); + auto &priority_vector = m_core_op_priority[old_priority]; + auto it = std::find(priority_vector.begin(), priority_vector.end(), core_op_handle); + CHECK(it != priority_vector.end(), HAILO_INTERNAL_FAILURE); + + priority_vector.erase(it); + m_scheduled_core_ops[core_op_handle]->set_priority(priority); + m_core_op_priority[priority].push_back(core_op_handle); + + return HAILO_SUCCESS; +} + +bool CoreOpsScheduler::choose_next_core_op(const device_id_t &device_id, bool check_threshold) +{ + if (!m_devices[device_id]->is_switching_core_op) { + return CoreOpsSchedulerOracle::choose_next_model(*this, m_devices[device_id]->device_id, check_threshold) != INVALID_CORE_OP_HANDLE; + } + return false; +} + +bool CoreOpsScheduler::should_core_op_stop(const scheduler_core_op_handle_t &core_op_handle) +{ + for (const auto &name_flag_pair : m_should_core_op_stop[core_op_handle]) { + if (name_flag_pair.second) { + return true; + } + } + + return false; +} + +bool CoreOpsScheduler::core_op_all_streams_aborted(const scheduler_core_op_handle_t &core_op_handle) +{ + for (const auto &name_flag_pair : m_should_core_op_stop[core_op_handle]) { + if (!name_flag_pair.second) { + return false; + } + } + return true; +} + +void CoreOpsScheduler::notify_all() +{ + { + // Acquire mutex to make sure the notify_all will wake the blocking threads on the cv + std::unique_lock lock(m_before_read_write_mutex); + } + // TODO: consider notify only the relevant ng or stream + for (auto &cng_cvs : m_core_ops_cvs) { + cng_cvs.second->notify_all(); + } +} + +hailo_status CoreOpsScheduler::optimize_streaming_if_enabled(const scheduler_core_op_handle_t &core_op_handle) +{ + auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; + if (!scheduled_core_op->use_dynamic_batch_flow()) { + auto next_pair = m_devices.upper_bound(scheduled_core_op->get_last_device()); // Get last device and go to the next device in the map + if (m_devices.end() == next_pair){ // In case we reached to the end of the map - start from the beggining + next_pair = m_devices.begin(); + } + auto &device_info = next_pair->second; + if (device_info->current_core_op_handle == core_op_handle && !device_info->is_switching_core_op && + !CoreOpsSchedulerOracle::should_stop_streaming(*this, scheduled_core_op->get_priority(), device_info->device_id) && + (get_min_avail_buffers_count(core_op_handle, device_info->device_id) >= DEFAULT_BURST_SIZE)) { + auto status = send_all_pending_buffers(core_op_handle, device_info->device_id, DEFAULT_BURST_SIZE); + if (HAILO_STREAM_ABORTED_BY_USER == status) { + LOGGER__INFO("send_all_pending_buffers has failed with status=HAILO_STREAM_ABORTED_BY_USER"); + return status; + } + CHECK_SUCCESS(status); + } + } + return HAILO_SUCCESS; +} + +uint16_t CoreOpsScheduler::get_min_avail_buffers_count(const scheduler_core_op_handle_t &core_op_handle, const device_id_t &device_id) +{ + auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; + auto device_info = m_devices[device_id]; + + uint16_t avail_buffer_count = UINT16_MAX; + for (auto &output_stream : scheduled_core_op->get_core_op()->get_output_streams()) { + auto &vdevice_output = static_cast(output_stream.get()); + if (auto buffer_size_in_frames = vdevice_output.get_buffer_frames_size()) { + auto &pending_frames_in_buffer = device_info->pending_to_read_frames[core_op_handle][vdevice_output.name()]; + auto ongoing_frames = get_max_value_of_unordered_map(device_info->current_cycle_requested_transferred_frames_h2d[core_op_handle]) - + device_info->current_cycle_finished_transferred_frames_d2h[core_op_handle][vdevice_output.name()]; + assert(*buffer_size_in_frames >= (pending_frames_in_buffer + ongoing_frames)); + avail_buffer_count = std::min(avail_buffer_count, static_cast(*buffer_size_in_frames - pending_frames_in_buffer - ongoing_frames)); + } + } + + auto transferred_frames = get_max_value_of_unordered_map(device_info->current_cycle_requested_transferred_frames_h2d[core_op_handle]) - + get_min_value_of_unordered_map(device_info->current_cycle_finished_transferred_frames_d2h[core_op_handle]); + if (is_multi_device()) { + auto avail_input_buffer_count = static_cast((scheduled_core_op->get_min_input_buffers_count()) - transferred_frames); + avail_buffer_count = std::min(avail_input_buffer_count, avail_buffer_count); + } + + return avail_buffer_count; +} + +uint16_t CoreOpsScheduler::get_min_avail_output_buffers(const scheduler_core_op_handle_t &core_op_handle) +{ + auto scheduled_core_op = m_scheduled_core_ops[core_op_handle]; + auto sent_frames = scheduled_core_op->h2d_finished_transferred_frames_max_value() - + scheduled_core_op->finished_read_frames_min_value(); + + return static_cast((scheduled_core_op->get_min_output_buffers_count()) - sent_frames); +} + +void CoreOpsScheduler::worker_thread_main() +{ + OsUtils::set_current_thread_name("SCHEDULER"); + std::unique_lock lock(m_before_read_write_mutex); + while (m_is_running) { + + m_scheduler_cv.wait(lock, [this]() { + return m_execute_worker_thread.load(); + }); + m_execute_worker_thread = false; + + if (!m_is_running) { + break; + } + + for (uint32_t core_op_handle = 0; core_op_handle < m_scheduled_core_ops.size(); core_op_handle++) { + auto status = optimize_streaming_if_enabled(core_op_handle); + if (HAILO_STREAM_ABORTED_BY_USER == status) { + continue; + } + + if (HAILO_SUCCESS != status) { + if (m_is_running) { + LOGGER__ERROR("Scheduler thread failed with status={}", status); + } + break; + } + } + + auto oracle_decisions = CoreOpsSchedulerOracle::get_oracle_decisions(*this); + + for (const auto &run_params : oracle_decisions) { + auto status = switch_core_op(run_params.core_op_handle, run_params.device_id); + if (HAILO_STREAM_ABORTED_BY_USER == status) { + continue; + } + + if (HAILO_SUCCESS != status) { + if (m_is_running) { + LOGGER__ERROR("Scheduler thread failed with status={}", status); + } + break; + } + } + } +} + +} /* namespace hailort */ \ No newline at end of file diff --git a/hailort/libhailort/src/vdevice/scheduler/scheduler.hpp b/hailort/libhailort/src/vdevice/scheduler/scheduler.hpp new file mode 100644 index 0000000..c85c216 --- /dev/null +++ b/hailort/libhailort/src/vdevice/scheduler/scheduler.hpp @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file scheduler.hpp + * @brief Class declaration for CoreOpsScheduler that schedules core-ops to be active depending on the scheduling algorithm. + **/ + +#ifndef _HAILO_SCHEDULER_HPP_ +#define _HAILO_SCHEDULER_HPP_ + +#include "hailo/hailort.h" +#include "hailo/expected.hpp" + +#include "common/utils.hpp" +#include "common/filesystem.hpp" + +#include "vdevice/scheduler/scheduled_core_op_state.hpp" +#include "vdevice/scheduler/scheduled_core_op_cv.hpp" +#include "vdevice/scheduler/scheduler_base.hpp" + + +namespace hailort +{ + +#define INVALID_CORE_OP_HANDLE (UINT32_MAX) + +using scheduler_core_op_handle_t = uint32_t; +using core_op_priority_t = uint8_t; + +class CoreOpsScheduler; +using CoreOpsSchedulerPtr = std::shared_ptr; + +// We use mostly weak pointer for the scheduler to prevent circular dependency of the pointers +using CoreOpsSchedulerWeakPtr = std::weak_ptr; + +using stream_name_t = std::string; + +class CoreOpsScheduler : public SchedulerBase +{ +public: + static Expected create_round_robin(std::vector &devices_ids, + std::vector &devices_arch); + CoreOpsScheduler(hailo_scheduling_algorithm_t algorithm, std::vector &devices_ids, + std::vector &devices_arch); + + virtual ~CoreOpsScheduler(); + CoreOpsScheduler(const CoreOpsScheduler &other) = delete; + CoreOpsScheduler &operator=(const CoreOpsScheduler &other) = delete; + CoreOpsScheduler &operator=(CoreOpsScheduler &&other) = delete; + CoreOpsScheduler(CoreOpsScheduler &&other) noexcept = delete; + + Expected add_core_op(std::shared_ptr added_core_op); + + hailo_status signal_frame_pending_to_send(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name); + + hailo_status wait_for_read(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, + const std::chrono::milliseconds &timeout, const std::function &predicate); + + hailo_status signal_frame_pending_to_read(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name); + + void signal_frame_transferred_d2h(const scheduler_core_op_handle_t &core_op_handle, + const std::string &stream_name, const device_id_t &device_id); + hailo_status signal_read_finish(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, + const device_id_t &device_id); + + hailo_status enable_stream(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name); + hailo_status disable_stream(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name); + + hailo_status set_timeout(const scheduler_core_op_handle_t &core_op_handle, const std::chrono::milliseconds &timeout, const std::string &network_name); + hailo_status set_threshold(const scheduler_core_op_handle_t &core_op_handle, uint32_t threshold, const std::string &network_name); + hailo_status set_priority(const scheduler_core_op_handle_t &core_op_handle, core_op_priority_t priority, const std::string &network_name); + + virtual ReadyInfo is_core_op_ready(const scheduler_core_op_handle_t &core_op_handle, bool check_threshold) override; + virtual bool has_core_op_drained_everything(const scheduler_core_op_handle_t &core_op_handle, const device_id_t &device_id) override; + hailo_status flush_pending_buffers(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, const std::chrono::milliseconds &timeout); + + void notify_all(); + +protected: + bool choose_next_core_op(const device_id_t &device_id, bool check_threshold); + + std::unordered_map> m_should_core_op_stop; + +private: + hailo_status switch_core_op(const scheduler_core_op_handle_t &core_op_handle, const device_id_t &device_id, + bool keep_nn_config = false); + // Needs to be called with m_before_read_write_mutex held. + void signal_read_finish_impl(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, + const device_id_t &device_id); + + hailo_status send_all_pending_buffers(const scheduler_core_op_handle_t &core_op_handle, const device_id_t &device_id, uint32_t burst_size); + hailo_status send_pending_buffer(const scheduler_core_op_handle_t &core_op_handle, const std::string &stream_name, const device_id_t &device_id); + + void decrease_core_op_counters(const scheduler_core_op_handle_t &core_op_handle); + bool should_core_op_stop(const scheduler_core_op_handle_t &core_op_handle); + bool core_op_all_streams_aborted(const scheduler_core_op_handle_t &core_op_handle); + + std::string get_core_op_name(const scheduler_core_op_handle_t &core_op_handle); + bool is_core_op_active(const scheduler_core_op_handle_t &core_op_handle); + bool is_multi_device(); + + hailo_status optimize_streaming_if_enabled(const scheduler_core_op_handle_t &core_op_handle); + uint16_t get_min_avail_buffers_count(const scheduler_core_op_handle_t &core_op_handle, const device_id_t &device_id); + uint16_t get_min_avail_output_buffers(const scheduler_core_op_handle_t &core_op_handle); + + void worker_thread_main(); + + std::vector> m_scheduled_core_ops; + std::mutex m_before_read_write_mutex; + std::unordered_map> m_core_ops_cvs; + + std::atomic_bool m_is_running; + std::atomic_bool m_execute_worker_thread; + std::thread m_scheduler_thread; + std::condition_variable m_scheduler_cv; +}; +} /* namespace hailort */ + +#endif /* _HAILO_SCHEDULER_HPP_ */ diff --git a/hailort/libhailort/src/vdevice/scheduler/scheduler_base.hpp b/hailort/libhailort/src/vdevice/scheduler/scheduler_base.hpp index e9fc0a9..a8575f1 100644 --- a/hailort/libhailort/src/vdevice/scheduler/scheduler_base.hpp +++ b/hailort/libhailort/src/vdevice/scheduler/scheduler_base.hpp @@ -16,6 +16,8 @@ #include "common/utils.hpp" #include "common/filesystem.hpp" +#include "stream_common/stream_internal.hpp" + #include @@ -26,7 +28,6 @@ namespace hailort #define DEFAULT_SCHEDULER_MIN_THRESHOLD (0) #define INVALID_CORE_OP_HANDLE (UINT32_MAX) -#define INVALID_DEVICE_ID (UINT32_MAX) using scheduler_core_op_handle_t = uint32_t; using core_op_priority_t = uint8_t; @@ -34,10 +35,10 @@ using core_op_priority_t = uint8_t; using stream_name_t = std::string; struct ActiveDeviceInfo { - ActiveDeviceInfo(uint32_t device_id, const std::string &device_bdf_id, const std::string &device_arch) : + ActiveDeviceInfo(const device_id_t &device_id, const std::string &device_arch) : current_core_op_handle(INVALID_CORE_OP_HANDLE), next_core_op_handle(INVALID_CORE_OP_HANDLE), is_switching_core_op(false), current_batch_size(0), current_cycle_requested_transferred_frames_h2d(), current_cycle_finished_transferred_frames_d2h(), - current_cycle_finished_read_frames_d2h(), device_id(device_id), device_bdf_id(device_bdf_id), device_arch(device_arch) + pending_to_read_frames(), device_id(device_id), device_arch(device_arch) {} scheduler_core_op_handle_t current_core_op_handle; scheduler_core_op_handle_t next_core_op_handle; @@ -45,9 +46,8 @@ struct ActiveDeviceInfo { std::atomic_uint32_t current_batch_size; std::unordered_map> current_cycle_requested_transferred_frames_h2d; std::unordered_map> current_cycle_finished_transferred_frames_d2h; - std::unordered_map> current_cycle_finished_read_frames_d2h; - uint32_t device_id; - std::string device_bdf_id; + std::unordered_map> pending_to_read_frames; + device_id_t device_id; std::string device_arch; }; @@ -61,45 +61,53 @@ public: } struct ReadyInfo { - bool threshold = false; - bool timeout = false; + bool over_threshold = false; + bool over_timeout = false; bool is_ready = false; }; virtual ReadyInfo is_core_op_ready(const scheduler_core_op_handle_t &core_op_handle, bool check_threshold) = 0; - virtual bool has_core_op_drained_everything(const scheduler_core_op_handle_t &core_op_handle, uint32_t device_id) = 0; + virtual bool has_core_op_drained_everything(const scheduler_core_op_handle_t &core_op_handle, const device_id_t &device_id) = 0; virtual uint32_t get_device_count() const { return static_cast(m_devices.size()); } - virtual std::shared_ptr get_devices_info(uint32_t device_id) + virtual std::shared_ptr get_device_info(const device_id_t &device_id) { return m_devices[device_id]; } + + virtual std::map> &get_device_infos() + { + return m_devices; + } virtual std::map> get_core_op_priority_map() { return m_core_op_priority; } - virtual scheduler_core_op_handle_t get_last_choosen_core_op(core_op_priority_t priority) + virtual scheduler_core_op_handle_t get_next_core_op(core_op_priority_t priority) { - return m_last_choosen_core_op[priority]; + if (!contains(m_next_core_op, priority)) { + m_next_core_op[priority] = 0; + } + return m_next_core_op[priority]; } - virtual void set_last_choosen_core_op(const core_op_priority_t priority, const scheduler_core_op_handle_t &core_op_handle) + virtual void set_next_core_op(const core_op_priority_t priority, const scheduler_core_op_handle_t &core_op_handle) { - m_last_choosen_core_op[priority] = core_op_handle; + m_next_core_op[priority] = core_op_handle; } protected: - SchedulerBase(hailo_scheduling_algorithm_t algorithm, uint32_t device_count, std::vector &devices_bdf_id, + SchedulerBase(hailo_scheduling_algorithm_t algorithm, std::vector &devices_ids, std::vector &devices_arch) : m_algorithm(algorithm) { - for (uint32_t i = 0; i < device_count; i++) { - m_devices.push_back(make_shared_nothrow(i, devices_bdf_id[i], devices_arch[i])); + for (uint32_t i = 0; i < devices_ids.size(); i++) { + m_devices[devices_ids.at(i)] = make_shared_nothrow(devices_ids[i], devices_arch[i]); } }; @@ -109,11 +117,12 @@ protected: SchedulerBase &operator=(SchedulerBase &&other) = delete; SchedulerBase(SchedulerBase &&other) noexcept = delete; - std::vector> m_devices; + std::map> m_devices; + std::map> m_core_op_priority; hailo_scheduling_algorithm_t m_algorithm; - std::unordered_map m_last_choosen_core_op; + std::unordered_map m_next_core_op; }; } /* namespace hailort */ diff --git a/hailort/libhailort/src/vdevice/scheduler/scheduler_mon.hpp b/hailort/libhailort/src/vdevice/scheduler/scheduler_mon.hpp deleted file mode 100644 index 64fa99b..0000000 --- a/hailort/libhailort/src/vdevice/scheduler/scheduler_mon.hpp +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Copyright (c) 2022 Hailo Technologies Ltd. All rights reserved. - * Distributed under the MIT license (https://opensource.org/licenses/MIT) -**/ -/** - * @file scheduler_mon.hpp - * @brief Defines for scheduler monitor of networks. - **/ - -#ifndef _HAILO_SCHEDULER_MON_HPP_ -#define _HAILO_SCHEDULER_MON_HPP_ - -#include "hailo/hailort.h" - -#include "common/filesystem.hpp" - -#if defined(_MSC_VER) -#pragma warning(push) -#pragma warning(disable: 4244 4267 4127) -#else -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wconversion" -#endif -#include "scheduler_mon.pb.h" -#if defined(_MSC_VER) -#pragma warning( pop ) -#else -#pragma GCC diagnostic pop -#endif - -#include -#include - - -namespace hailort -{ - -#define SCHEDULER_MON_TMP_DIR ("/tmp/hmon_files/") -#define SCHEDULER_MON_ENV_VAR ("HAILO_MONITOR") -#define DEFAULT_SCHEDULER_MON_INTERVAL (std::chrono::seconds(1)) -#define SCHEDULER_MON_NAN_VAL (-1) - -class SchedulerMon -{ -public: - - static bool should_monitor() - { - #if defined(__GNUC__) - auto mon_var = std::getenv(SCHEDULER_MON_ENV_VAR); - return (mon_var != nullptr) && strncmp(mon_var, "1", 1) == 0; - #else - // TODO: HRT-7304 - Add support for windows - return false; - #endif - } -}; - -} /* namespace hailort */ - -#endif /* _HAILO_SCHEDULER_MON_HPP_ */ diff --git a/hailort/libhailort/src/vdevice/scheduler/scheduler_oracle.cpp b/hailort/libhailort/src/vdevice/scheduler/scheduler_oracle.cpp index b39ea3d..1d97c58 100644 --- a/hailort/libhailort/src/vdevice/scheduler/scheduler_oracle.cpp +++ b/hailort/libhailort/src/vdevice/scheduler/scheduler_oracle.cpp @@ -14,77 +14,46 @@ namespace hailort { -bool CoreOpsSchedulerOracle::choose_next_model(SchedulerBase &scheduler, uint32_t device_id, bool check_threshold) +scheduler_core_op_handle_t CoreOpsSchedulerOracle::choose_next_model(SchedulerBase &scheduler, const device_id_t &device_id, bool check_threshold) { - auto device_info = scheduler.get_devices_info(device_id); + auto device_info = scheduler.get_device_info(device_id); auto priority_map = scheduler.get_core_op_priority_map(); for (auto iter = priority_map.rbegin(); iter != priority_map.rend(); ++iter) { auto priority_group_size = iter->second.size(); for (uint32_t i = 0; i < priority_group_size; i++) { - uint32_t index = scheduler.get_last_choosen_core_op(iter->first) + i + 1; + uint32_t index = scheduler.get_next_core_op(iter->first) + i; index %= static_cast(priority_group_size); auto core_op_handle = iter->second[index]; - if (!is_core_op_active(scheduler, core_op_handle)) { - auto ready_info = scheduler.is_core_op_ready(core_op_handle, check_threshold); - if (ready_info.is_ready) { - TRACE(ChooseCoreOpTrace, "", core_op_handle, ready_info.threshold, ready_info.timeout, iter->first); - device_info->is_switching_core_op = true; - device_info->next_core_op_handle = core_op_handle; - scheduler.set_last_choosen_core_op(iter->first, index); - - return true; - } + auto ready_info = scheduler.is_core_op_ready(core_op_handle, check_threshold); + if (ready_info.is_ready) { + TRACE(ChooseCoreOpTrace, "", core_op_handle, ready_info.over_threshold, ready_info.over_timeout, iter->first); + device_info->is_switching_core_op = true; + device_info->next_core_op_handle = core_op_handle; + // Set next to run as next in round-robin + index = ((index + 1) % static_cast(priority_group_size)); + scheduler.set_next_core_op(iter->first, index); + return core_op_handle; } } } - return false; + return INVALID_CORE_OP_HANDLE; } -// TODO: return device handle instead index -uint32_t CoreOpsSchedulerOracle::get_avail_device(SchedulerBase &scheduler, scheduler_core_op_handle_t core_op_handle) -{ - const bool check_threshold = false; - auto device_count = scheduler.get_device_count(); - - // Check if should be next - /* Checking (INVALID_CORE_OP_HANDLE == m_current_core_op) for activating the first time the scheduler is running. - In this case we don't want to check threshold. */ - for (uint32_t device_index = 0; device_index < device_count; device_index++) { - auto active_device_info = scheduler.get_devices_info(device_index); - if (active_device_info->is_switching_core_op && scheduler.has_core_op_drained_everything(active_device_info->current_core_op_handle, active_device_info->device_id) && - (((INVALID_CORE_OP_HANDLE == active_device_info->current_core_op_handle) && - scheduler.is_core_op_ready(core_op_handle, check_threshold).is_ready) || - (active_device_info->next_core_op_handle == core_op_handle))) { - return active_device_info->device_id; - } - } - - // Check if device Idle - // We dont need to check if the core op is ready, because the device is idle and if we arrive here frame is already sent and as a space in the output buffer. - for (uint32_t device_index = 0; device_index < device_count; device_index++) { - auto active_device_info = scheduler.get_devices_info(device_index); - if (!active_device_info->is_switching_core_op && scheduler.has_core_op_drained_everything(active_device_info->current_core_op_handle, active_device_info->device_id)) { - return active_device_info->device_id; - } - } - - return INVALID_DEVICE_ID; -} - -bool CoreOpsSchedulerOracle::should_stop_streaming(SchedulerBase &scheduler, core_op_priority_t core_op_priority) +bool CoreOpsSchedulerOracle::should_stop_streaming(SchedulerBase &scheduler, core_op_priority_t core_op_priority, const device_id_t &device_id) { auto priority_map = scheduler.get_core_op_priority_map(); for (auto iter = priority_map.rbegin(); (iter != priority_map.rend()) && (iter->first >= core_op_priority); ++iter) { auto priority_group_size = iter->second.size(); for (uint32_t i = 0; i < priority_group_size; i++) { - uint32_t index = scheduler.get_last_choosen_core_op(iter->first) + i + 1; + uint32_t index = scheduler.get_next_core_op(iter->first) + i; index %= static_cast(priority_group_size); auto core_op_handle = iter->second[index]; // We dont want to stay with the same network group if there is a other qualified network group - if ((!is_core_op_active(scheduler, core_op_handle)) && scheduler.is_core_op_ready(core_op_handle, true).is_ready) { + if ((!is_core_op_active(scheduler, core_op_handle)) && scheduler.is_core_op_ready(core_op_handle, true).is_ready && + is_core_op_finished_batch(scheduler, device_id)) { return true; } } @@ -95,9 +64,9 @@ bool CoreOpsSchedulerOracle::should_stop_streaming(SchedulerBase &scheduler, cor bool CoreOpsSchedulerOracle::is_core_op_active(SchedulerBase &scheduler, scheduler_core_op_handle_t core_op_handle) { - auto device_count = scheduler.get_device_count(); - for (uint32_t device_index = 0; device_index < device_count; device_index++) { - auto active_device_info = scheduler.get_devices_info(device_index); + auto &devices = scheduler.get_device_infos(); + for (const auto &pair : devices) { + auto &active_device_info = pair.second; if (core_op_handle == active_device_info->current_core_op_handle) { return true; } @@ -106,4 +75,38 @@ bool CoreOpsSchedulerOracle::is_core_op_active(SchedulerBase &scheduler, schedul return false; } +bool CoreOpsSchedulerOracle::is_core_op_finished_batch(SchedulerBase &scheduler, const device_id_t &device_id) +{ + auto device_info = scheduler.get_device_info(device_id); + auto max_transferred_h2d = get_max_value_of_unordered_map(device_info->current_cycle_requested_transferred_frames_h2d[device_info->current_core_op_handle]); + + return device_info->current_batch_size <= max_transferred_h2d; +} + +std::vector CoreOpsSchedulerOracle::get_oracle_decisions(SchedulerBase &scheduler) +{ + auto &devices = scheduler.get_device_infos(); + std::vector oracle_decision; + + for (const auto &pair : devices) { + auto &active_device_info = pair.second; + + // Check if device is switching ng + if (active_device_info->is_switching_core_op) { + oracle_decision.push_back({active_device_info->next_core_op_handle, active_device_info->device_id}); + } + + // Check if device is idle + if (!active_device_info->is_switching_core_op && + scheduler.has_core_op_drained_everything(active_device_info->current_core_op_handle, active_device_info->device_id)) { + auto core_op_handle = choose_next_model(scheduler, active_device_info->device_id, false); + if (core_op_handle != INVALID_CORE_OP_HANDLE) { + oracle_decision.push_back({core_op_handle, active_device_info->device_id}); + } + } + } + + return oracle_decision; +} + } /* namespace hailort */ diff --git a/hailort/libhailort/src/vdevice/scheduler/scheduler_oracle.hpp b/hailort/libhailort/src/vdevice/scheduler/scheduler_oracle.hpp index 766bf45..fd09944 100644 --- a/hailort/libhailort/src/vdevice/scheduler/scheduler_oracle.hpp +++ b/hailort/libhailort/src/vdevice/scheduler/scheduler_oracle.hpp @@ -21,17 +21,23 @@ namespace hailort { +struct RunParams { + scheduler_core_op_handle_t core_op_handle; + device_id_t device_id; +}; + class CoreOpsSchedulerOracle { public: - static bool choose_next_model(SchedulerBase &scheduler, uint32_t device_id, bool check_threshold); - static uint32_t get_avail_device(SchedulerBase &scheduler, scheduler_core_op_handle_t core_op_handle); - static bool should_stop_streaming(SchedulerBase &scheduler, core_op_priority_t core_op_priority); + static scheduler_core_op_handle_t choose_next_model(SchedulerBase &scheduler, const device_id_t &device_id, bool check_threshold); + static std::vector get_oracle_decisions(SchedulerBase &scheduler); + static bool should_stop_streaming(SchedulerBase &scheduler, core_op_priority_t core_op_priority, const device_id_t &device_id); private: CoreOpsSchedulerOracle() {} // TODO: Consider returning a vector of devices (we can use this function in other places) static bool is_core_op_active(SchedulerBase &scheduler, scheduler_core_op_handle_t core_op_handle); + static bool is_core_op_finished_batch(SchedulerBase &scheduler, const device_id_t &device_id); }; } /* namespace hailort */ diff --git a/hailort/libhailort/src/vdevice/vdevice.cpp b/hailort/libhailort/src/vdevice/vdevice.cpp index 7030013..310892b 100644 --- a/hailort/libhailort/src/vdevice/vdevice.cpp +++ b/hailort/libhailort/src/vdevice/vdevice.cpp @@ -40,17 +40,23 @@ std::string SharedResourceManager::unique_key() static hailo_status validate_device_ids_match(const hailo_vdevice_params_t ¶ms, const std::set &old_ids) { - std::set new_ids; + const auto group_id_name = (nullptr == params.group_id ? "NULL" : params.group_id); + CHECK(old_ids.size() == static_cast(params.device_count), HAILO_INVALID_OPERATION, + "VDevice invalid device count for group_id {}", group_id_name); + for (uint32_t i = 0; i < params.device_count; i++) { - // TODO: maybe needs to normalize domain? - new_ids.insert(params.device_ids[i].id); + auto device_id_found = std::find_if(old_ids.begin(), old_ids.end(), + [&](const std::string &device_id) { + return Device::device_ids_equal(params.device_ids[i].id, device_id); + }); + CHECK(device_id_found != old_ids.end(), HAILO_INVALID_OPERATION, + "Device {} not used by group_id {}", params.device_ids[i].id, group_id_name); } - CHECK(old_ids == new_ids, HAILO_INVALID_OPERATION, "Different VDevice ids used by group_id {}", (nullptr == params.group_id ? "NULL" : params.group_id)); return HAILO_SUCCESS; } -hailo_status validate_same_vdevice(const hailo_vdevice_params_t ¶ms, const VDevice &vdevice) +static hailo_status validate_same_vdevice(const hailo_vdevice_params_t ¶ms, const VDevice &vdevice) { // Validate device ids if (params.device_ids != nullptr) { @@ -102,9 +108,6 @@ VDeviceHandle::~VDeviceHandle() Expected> VDeviceHandle::create(const hailo_vdevice_params_t ¶ms) { - auto status = VDeviceBase::validate_params(params); - CHECK_SUCCESS_AS_EXPECTED(status); - auto &manager = SharedResourceManager::get_instance(); auto create = [¶ms]() { return VDeviceBase::create(params); @@ -164,9 +167,10 @@ Expected VDeviceHandle::get_default_streams_interface( #ifdef HAILO_SUPPORT_MULTI_PROCESS -VDeviceClient::VDeviceClient(std::unique_ptr client, uint32_t handle) +VDeviceClient::VDeviceClient(std::unique_ptr client, uint32_t handle, std::vector> &&devices) : m_client(std::move(client)) , m_handle(handle) + , m_devices(std::move(devices)) {} VDeviceClient::~VDeviceClient() @@ -177,7 +181,7 @@ VDeviceClient::~VDeviceClient() // The vdevice in the service will destruct the ConfiguredNetworkGroupBase, // and then the ConfiguredNetworkGroupClient destructor will be called - causing double destruction on ConfiguredNetworkGroupBase. m_network_groups.clear(); - auto reply = m_client->VDevice_release(m_handle); + auto reply = m_client->VDevice_release(m_handle, OsUtils::get_curr_pid()); if (reply != HAILO_SUCCESS) { LOGGER__CRITICAL("VDevice_release failed!"); } @@ -233,7 +237,11 @@ Expected> VDeviceClient::create(const hailo_vdevice_par auto reply = client->VDevice_create(params, OsUtils::get_curr_pid()); CHECK_EXPECTED(reply); - auto client_vdevice = std::unique_ptr(new VDeviceClient(std::move(client), reply.value())); + auto handle = reply.value(); + auto devices = client->VDevice_get_physical_devices(handle); + CHECK_EXPECTED(devices); + + auto client_vdevice = std::unique_ptr(new VDeviceClient(std::move(client), handle, devices.release())); CHECK_AS_EXPECTED(client_vdevice != nullptr, HAILO_OUT_OF_HOST_MEMORY); return std::unique_ptr(std::move(client_vdevice)); @@ -263,8 +271,13 @@ Expected VDeviceClient::configure(Hef &hef, Expected>> VDeviceClient::get_physical_devices() const { - LOGGER__ERROR("ConfiguredNetworkGroup::get_physical_devices function is not supported when using multi-process service"); - return make_unexpected(HAILO_INVALID_OPERATION); + std::vector> devices_refs; + + for (auto &device : m_devices) { + devices_refs.push_back(*device); + } + + return devices_refs; } Expected> VDeviceClient::get_physical_devices_ids() const @@ -282,9 +295,15 @@ Expected VDeviceClient::get_default_streams_interface( Expected> VDevice::create(const hailo_vdevice_params_t ¶ms) { + auto status = VDeviceBase::validate_params(params); + CHECK_SUCCESS_AS_EXPECTED(status); + std::unique_ptr vdevice; + if (params.multi_process_service) { #ifdef HAILO_SUPPORT_MULTI_PROCESS + CHECK_AS_EXPECTED(params.scheduling_algorithm != HAILO_SCHEDULING_ALGORITHM_NONE, HAILO_INVALID_ARGUMENT, + "Multi-process service is supported only with HailoRT scheduler, please choose scheduling algorithm"); auto expected_vdevice = VDeviceClient::create(params); CHECK_EXPECTED(expected_vdevice); vdevice = expected_vdevice.release(); @@ -351,7 +370,8 @@ Expected> VDeviceBase::create(const hailo_vdevice_p device_archs.reserve(params.device_count); std::string vdevice_ids = "VDevice Infos:"; - for (const auto &device : devices) { + for (const auto &pair : devices) { + auto &device = pair.second; auto id_info_str = device->get_dev_id(); device_ids.emplace_back(id_info_str); auto device_arch = device->get_architecture(); @@ -366,7 +386,7 @@ Expected> VDeviceBase::create(const hailo_vdevice_p CoreOpsSchedulerPtr scheduler_ptr; if (HAILO_SCHEDULING_ALGORITHM_NONE != params.scheduling_algorithm) { if (HAILO_SCHEDULING_ALGORITHM_ROUND_ROBIN == params.scheduling_algorithm) { - auto core_ops_scheduler = CoreOpsScheduler::create_round_robin(params.device_count, device_ids, device_archs); + auto core_ops_scheduler = CoreOpsScheduler::create_round_robin(device_ids, device_archs); CHECK_EXPECTED(core_ops_scheduler); scheduler_ptr = core_ops_scheduler.release(); } else { @@ -395,34 +415,36 @@ Expected VDeviceBase::configure(Hef &hef, for (const auto &network_params_pair : local_config_params.value()) { std::vector> core_ops; + const bool use_multiplexer = should_use_multiplexer(network_params_pair.second); + std::shared_ptr identical_core_op = nullptr; - if (m_core_ops_scheduler && PipelineMultiplexer::should_use_multiplexer()) { + if (use_multiplexer) { for (auto &network_group : m_vdevice_core_ops) { - if ((network_group->equals(hef, network_params_pair)) && (1 == network_group->get_input_streams().size())) { - // TODO (HRT-8634): Support multi-inputs NGs (multi networks) + if (network_group->multiplexer_supported() && network_group->equals(hef, network_params_pair)) { identical_core_op = network_group; break; } } } - std::shared_ptr vdevice_netwrok_group = nullptr; + std::shared_ptr vdevice_network_group = nullptr; if (identical_core_op) { - auto vdevice_netwrok_group_exp = VDeviceCoreOp::duplicate(identical_core_op); - CHECK_EXPECTED(vdevice_netwrok_group_exp); + auto vdevice_network_group_exp = VDeviceCoreOp::duplicate(identical_core_op); + CHECK_EXPECTED(vdevice_network_group_exp); - vdevice_netwrok_group = vdevice_netwrok_group_exp.release(); - vdevice_netwrok_group->set_core_op_handle(identical_core_op->core_op_handle()); - vdevice_netwrok_group->create_vdevice_streams_from_duplicate(identical_core_op); + vdevice_network_group = vdevice_network_group_exp.release(); + vdevice_network_group->set_core_op_handle(identical_core_op->core_op_handle()); + auto status = vdevice_network_group->create_vdevice_streams_from_duplicate(identical_core_op); + CHECK_SUCCESS_AS_EXPECTED(status); } else { - auto vdevice_netwrok_group_expected = create_vdevice_network_group(hef, network_params_pair); - CHECK_EXPECTED(vdevice_netwrok_group_expected); - vdevice_netwrok_group = vdevice_netwrok_group_expected.release(); - m_vdevice_core_ops.push_back(vdevice_netwrok_group); + auto vdevice_network_group_expected = create_vdevice_network_group(hef, network_params_pair, use_multiplexer); + CHECK_EXPECTED(vdevice_network_group_expected); + vdevice_network_group = vdevice_network_group_expected.release(); + m_vdevice_core_ops.push_back(vdevice_network_group); } - core_ops.push_back(vdevice_netwrok_group); - auto net_flow_ops = hef.pimpl->post_process_ops(vdevice_netwrok_group->name()); - auto net_group_expected = ConfiguredNetworkGroupBase::create(network_params_pair.second, std::move(core_ops), std::move(net_flow_ops)); + core_ops.push_back(vdevice_network_group); + auto metadata = hef.pimpl->network_group_metadata(vdevice_network_group->name()); + auto net_group_expected = ConfiguredNetworkGroupBase::create(network_params_pair.second, std::move(core_ops), std::move(metadata)); CHECK_EXPECTED(net_group_expected); auto network_group_ptr = net_group_expected.release(); @@ -438,9 +460,10 @@ Expected VDeviceBase::configure(Hef &hef, Expected VDeviceBase::get_default_streams_interface() const { - auto stream_interface = m_devices[0]->get_default_streams_interface(); + auto stream_interface = m_devices.begin()->second.get()->get_default_streams_interface(); CHECK_EXPECTED(stream_interface); - for (auto &dev : m_devices) { + for (const auto &pair : m_devices) { + auto &dev = pair.second; auto current_stream_interface = dev->get_default_streams_interface(); CHECK_EXPECTED(current_stream_interface); CHECK_AS_EXPECTED(*current_stream_interface == *stream_interface, HAILO_INTERNAL_FAILURE, @@ -449,10 +472,9 @@ Expected VDeviceBase::get_default_streams_interface() return stream_interface.release(); } -Expected>> VDeviceBase::create_devices(const hailo_vdevice_params_t ¶ms) +Expected>> VDeviceBase::create_devices(const hailo_vdevice_params_t ¶ms) { - std::vector> devices; - devices.reserve(params.device_count); + std::map> devices; const bool user_specific_devices = (params.device_ids != nullptr); @@ -484,7 +506,7 @@ Expected>> VDeviceBase::create_devices(const } CHECK_SUCCESS_AS_EXPECTED(status); } - devices.emplace_back(device.release()); + devices[device_id] = device.release(); } CHECK_AS_EXPECTED(params.device_count == devices.size(), HAILO_OUT_OF_PHYSICAL_DEVICES, "Failed to create vdevice. there are not enough free devices. requested: {}, found: {}", @@ -513,7 +535,8 @@ Expected> VDeviceBase::get_device_ids(const hailo_vdevi Expected VDeviceBase::create_local_config_params(Hef &hef, const NetworkGroupsParamsMap &configure_params) { - for (auto &device : m_devices) { + for (const auto &pair : m_devices) { + auto &device = pair.second; auto status = dynamic_cast(*device).check_hef_is_compatible(hef); CHECK_SUCCESS_AS_EXPECTED(status); } @@ -521,7 +544,7 @@ Expected VDeviceBase::create_local_config_params(Hef &he auto local_config_params = configure_params; if (local_config_params.empty()) { // All stream iface should be the same - auto config_params_exp = m_devices[0]->create_configure_params(hef); + auto config_params_exp = m_devices.begin()->second->create_configure_params(hef); CHECK_EXPECTED(config_params_exp); local_config_params = config_params_exp.release(); } @@ -544,39 +567,77 @@ Expected VDeviceBase::create_local_config_params(Hef &he return local_config_params; } -Expected> VDeviceBase::create_vdevice_network_group(Hef &hef, const std::pair ¶ms) +Expected> VDeviceBase::create_vdevice_network_group(Hef &hef, + const std::pair ¶ms, bool use_multiplexer) { - std::vector> core_ops_bundle; // bundle of the same CoreOps for all devices - core_ops_bundle.reserve(m_devices.size()); + std::map>> core_ops_bundle; // configure all the devices to this ng and then push the core ops to bundle vector - for (auto &device : m_devices) { + for (const auto &pair : m_devices) { + auto &device = pair.second; auto ng_vector = device->configure(hef, { std::make_pair(params.first, params.second) }); CHECK_EXPECTED(ng_vector); assert(1 == ng_vector->size()); auto network_group_base = std::dynamic_pointer_cast(ng_vector.value()[0]); + + auto networks_info = network_group_base->get_network_infos(); + CHECK_EXPECTED(networks_info); + if (m_core_ops_scheduler && 1 < networks_info->size()) { + LOGGER__WARNING("Configuring '{}' which is a multi-networks model with scheduler enabled." + " The model will be scheduled only when all inputs and outputs of the network group will be ready", + network_group_base->name()); + } + auto ng_core_ops = network_group_base->get_core_ops(); + auto &core_ops_vector = core_ops_bundle.emplace(device->get_dev_id(), std::vector>{}).first->second; - core_ops_bundle.insert(core_ops_bundle.begin(), ng_core_ops.begin(), ng_core_ops.end()); + core_ops_vector.insert(core_ops_vector.begin(), ng_core_ops.begin(), ng_core_ops.end()); } - auto vdevice_netwrok_group_exp = VDeviceCoreOp::create(core_ops_bundle, m_core_ops_scheduler, hef.hash()); - CHECK_EXPECTED(vdevice_netwrok_group_exp); - auto vdevice_netwrok_group = vdevice_netwrok_group_exp.release(); + + auto vdevice_network_group_exp = VDeviceCoreOp::create(core_ops_bundle, m_core_ops_scheduler, hef.hash()); + CHECK_EXPECTED(vdevice_network_group_exp); + auto vdevice_network_group = vdevice_network_group_exp.release(); auto ng_handle = INVALID_CORE_OP_HANDLE; if (m_core_ops_scheduler) { - auto core_op_handle_exp = m_core_ops_scheduler->add_core_op(vdevice_netwrok_group); + auto core_op_handle_exp = m_core_ops_scheduler->add_core_op(vdevice_network_group); CHECK_EXPECTED(core_op_handle_exp); ng_handle = core_op_handle_exp.release(); } - vdevice_netwrok_group->set_core_op_handle(ng_handle); - auto status = vdevice_netwrok_group->create_vdevice_streams_from_config_params(make_shared_nothrow(), ng_handle); + vdevice_network_group->set_core_op_handle(ng_handle); + + std::shared_ptr multiplexer = nullptr; + if (use_multiplexer) { + multiplexer = make_shared_nothrow(); + CHECK_NOT_NULL_AS_EXPECTED(multiplexer, HAILO_OUT_OF_HOST_MEMORY); + } + + auto status = vdevice_network_group->create_vdevice_streams_from_config_params(multiplexer, ng_handle); CHECK_SUCCESS_AS_EXPECTED(status); - return vdevice_netwrok_group; + return vdevice_network_group; } +bool VDeviceBase::should_use_multiplexer(const ConfigureNetworkParams &network_params) +{ + const auto &stream_params_by_name = network_params.stream_params_by_name; + const auto input_counts = std::count_if(stream_params_by_name.begin(), stream_params_by_name.end(), + [](const std::pair &stream_params) { + return HAILO_H2D_STREAM == stream_params.second.direction; + }); + + const bool has_async_stream = std::any_of(stream_params_by_name.begin(), stream_params_by_name.end(), + [](const std::pair &stream_params) { + return 0 != (stream_params.second.flags & HAILO_STREAM_FLAGS_ASYNC); + }); + + return + PipelineMultiplexer::is_multiplexer_supported() && + m_core_ops_scheduler && + input_counts == 1 && // TODO (HRT-8634): Support multi-inputs NGs (multi networks) + !has_async_stream; // TODO (HRT-10557): Support async multiplexer +} } /* namespace hailort */ diff --git a/hailort/libhailort/src/vdevice/vdevice_core_op.cpp b/hailort/libhailort/src/vdevice/vdevice_core_op.cpp index a37c01d..18e3715 100644 --- a/hailort/libhailort/src/vdevice/vdevice_core_op.cpp +++ b/hailort/libhailort/src/vdevice/vdevice_core_op.cpp @@ -18,7 +18,7 @@ namespace hailort { Expected> VDeviceActivatedCoreOp::create( - std::vector> &core_ops, + std::map>> &core_ops, std::map> &input_streams, std::map> &output_streams, const hailo_activate_network_group_params_t &network_group_params, @@ -29,11 +29,14 @@ Expected> VDeviceActivatedCoreOp::create( auto status = HAILO_UNINITIALIZED; std::vector> activated_network_groups; activated_network_groups.reserve(core_ops.size()); - for (auto core_op : core_ops) { - auto ang = core_op->create_activated_network_group(network_group_params, dynamic_batch_size, - resume_pending_stream_transfers); - CHECK_EXPECTED(ang); - activated_network_groups.emplace_back(ang.release()); + for (const auto &pair : core_ops) { + auto &core_op_vector = pair.second; + for (auto &core_op : core_op_vector) { + auto ang = core_op->create_activated_network_group(network_group_params, dynamic_batch_size, + resume_pending_stream_transfers); + CHECK_EXPECTED(ang); + activated_network_groups.emplace_back(ang.release()); + } } auto ang = VDeviceActivatedCoreOp(std::move(activated_network_groups), input_streams, output_streams, network_group_params, core_op_activated_event, deactivation_time_accumulator, status); @@ -87,7 +90,7 @@ VDeviceActivatedCoreOp::VDeviceActivatedCoreOp(VDeviceActivatedCoreOp &&other) n } -Expected> VDeviceCoreOp::create(std::vector> core_ops, +Expected> VDeviceCoreOp::create(const std::map>> &core_ops, CoreOpsSchedulerWeakPtr core_ops_scheduler, const std::string &hef_hash) { auto status = HAILO_UNINITIALIZED; @@ -116,9 +119,9 @@ Expected> VDeviceCoreOp::duplicate(std::shared_pt } -VDeviceCoreOp::VDeviceCoreOp(std::vector> core_ops, +VDeviceCoreOp::VDeviceCoreOp(const std::map>> &core_ops, CoreOpsSchedulerWeakPtr core_ops_scheduler, const std::string &hef_hash, hailo_status &status) : - CoreOp(core_ops[0]->m_config_params, core_ops[0]->m_metadata, status), + CoreOp((core_ops.begin()->second)[0]->m_config_params, (core_ops.begin()->second)[0]->m_metadata, status), m_core_ops(std::move(core_ops)), m_core_ops_scheduler(core_ops_scheduler), m_scheduler_handle(INVALID_CORE_OP_HANDLE), @@ -129,21 +132,25 @@ VDeviceCoreOp::VDeviceCoreOp(std::vector> core_ops, Expected VDeviceCoreOp::get_default_streams_interface() { - auto first_streams_interface = m_core_ops[0]->get_default_streams_interface(); + auto first_streams_interface = (m_core_ops.begin()->second)[0]->get_default_streams_interface(); CHECK_EXPECTED(first_streams_interface); #ifndef NDEBUG // Check that all physical devices has the same interface - for (auto &core_op : m_core_ops) { - auto iface = core_op->get_default_streams_interface(); - CHECK_EXPECTED(iface); - CHECK_AS_EXPECTED(iface.value() == first_streams_interface.value(), HAILO_INTERNAL_FAILURE, - "Not all default stream interfaces are the same"); + for (const auto &pair : m_core_ops) { + auto &core_op_vector = pair.second; + for (auto &core_op : core_op_vector) { + auto iface = core_op->get_default_streams_interface(); + CHECK_EXPECTED(iface); + CHECK_AS_EXPECTED(iface.value() == first_streams_interface.value(), HAILO_INTERNAL_FAILURE, + "Not all default stream interfaces are the same"); + } } #endif return first_streams_interface; } -hailo_status VDeviceCoreOp::create_vdevice_streams_from_config_params(std::shared_ptr multiplexer, scheduler_core_op_handle_t scheduler_handle) +hailo_status VDeviceCoreOp::create_vdevice_streams_from_config_params(std::shared_ptr multiplexer, + scheduler_core_op_handle_t scheduler_handle) { // TODO - HRT-6931 - raise error on this case if (((m_config_params.latency & HAILO_LATENCY_MEASURE) == HAILO_LATENCY_MEASURE) && (1 < m_core_ops.size())) { @@ -183,8 +190,11 @@ hailo_status VDeviceCoreOp::create_vdevice_streams_from_config_params(std::share TRACE(CreateCoreOpInputStreamsTrace, "", name(), input_stream.first, (uint32_t)expected_queue_size.value()); } for (const auto &output_stream : m_output_streams) { - if ((hailo_format_order_t::HAILO_FORMAT_ORDER_HAILO_NMS == (static_cast(*output_stream.second).get_layer_info().format.order)) || - (HAILO_STREAM_INTERFACE_ETH == static_cast(*output_stream.second).get_interface())) { + if (hailo_format_order_t::HAILO_FORMAT_ORDER_HAILO_NMS == (static_cast(*output_stream.second).get_layer_info().format.order)) { + TRACE(CreateCoreOpOutputStreamsTrace, "", name(), output_stream.first, SCHEDULER_MON_NAN_VAL); + continue; + } + if (HAILO_STREAM_INTERFACE_ETH == static_cast(*output_stream.second).get_interface()) { continue; } auto expected_queue_size = static_cast(*output_stream.second).get_buffer_frames_size(); @@ -192,8 +202,10 @@ hailo_status VDeviceCoreOp::create_vdevice_streams_from_config_params(std::share TRACE(CreateCoreOpOutputStreamsTrace, "", name(), output_stream.first, (uint32_t)expected_queue_size.value()); } - auto status = m_multiplexer->add_core_op_instance(m_multiplexer_handle, *this); - CHECK_SUCCESS(status); + if (m_multiplexer) { + auto status = m_multiplexer->add_core_op_instance(m_multiplexer_handle, *this); + CHECK_SUCCESS(status); + } return HAILO_SUCCESS; } @@ -204,27 +216,36 @@ hailo_status VDeviceCoreOp::create_input_vdevice_stream_from_config_params(const auto edge_layer = get_layer_info(stream_name); CHECK_EXPECTED_AS_STATUS(edge_layer); - if (HailoRTCommon::is_vdma_stream_interface(stream_params.stream_interface)){ - std::vector> low_level_streams; - low_level_streams.reserve(m_core_ops.size()); - for (auto &core_op : m_core_ops) { - auto stream = core_op->get_input_stream_by_name(stream_name); - CHECK(stream, HAILO_INTERNAL_FAILURE); - low_level_streams.emplace_back(dynamic_cast(stream.release().get())); + if (HailoRTCommon::is_vdma_stream_interface(stream_params.stream_interface)) { + std::map> low_level_streams; + for (const auto &pair : m_core_ops) { + auto &device_id = pair.first; + auto &core_op_vector = pair.second; + for (auto &core_op : core_op_vector) { + auto stream = core_op->get_input_stream_by_name(stream_name); + CHECK(stream, HAILO_INTERNAL_FAILURE); + low_level_streams.emplace(device_id, dynamic_cast(stream.release().get())); + } } - auto input_stream = InputVDeviceBaseStream::create(std::move(low_level_streams), edge_layer.value(), - scheduler_handle, m_core_op_activated_event, m_core_ops_scheduler); + auto input_stream = VDeviceInputStreamBase::create(std::move(low_level_streams), stream_params, + edge_layer.value(), scheduler_handle, m_core_op_activated_event, m_core_ops_scheduler); CHECK_EXPECTED_AS_STATUS(input_stream); - auto input_stream_wrapper = VDeviceInputStreamMultiplexerWrapper::create(input_stream.release(), edge_layer->network_name, multiplexer, scheduler_handle); - CHECK_EXPECTED_AS_STATUS(input_stream_wrapper); - m_input_streams.insert(make_pair(stream_name, input_stream_wrapper.release())); + + if (multiplexer) { + auto input_stream_wrapper = VDeviceInputStreamMultiplexerWrapper::create(input_stream.release(), edge_layer->network_name, multiplexer, scheduler_handle); + CHECK_EXPECTED_AS_STATUS(input_stream_wrapper); + m_input_streams.insert(make_pair(stream_name, input_stream_wrapper.release())); + } else { + m_input_streams.insert(make_pair(stream_name, input_stream.release())); + } + } else { assert(1 == m_core_ops.size()); - auto stream = m_core_ops[0]->get_input_stream_by_name(stream_name); + auto stream = (m_core_ops.begin()->second)[0]->get_input_stream_by_name(stream_name); CHECK(stream, HAILO_INTERNAL_FAILURE); assert(1 == m_core_ops.size()); - assert(contains(m_core_ops[0]->m_input_streams, stream_name)); - m_input_streams.insert(make_pair(stream_name, m_core_ops[0]->m_input_streams.at(stream_name))); + assert(contains((m_core_ops.begin()->second)[0]->m_input_streams, stream_name)); + m_input_streams.insert(make_pair(stream_name, m_core_ops.begin()->second[0]->m_input_streams.at(stream_name))); } return HAILO_SUCCESS; @@ -237,23 +258,32 @@ hailo_status VDeviceCoreOp::create_output_vdevice_stream_from_config_params(cons CHECK_EXPECTED_AS_STATUS(edge_layer); if (HailoRTCommon::is_vdma_stream_interface(stream_params.stream_interface)) { - std::vector> low_level_streams; - low_level_streams.reserve(m_core_ops.size()); - for (auto &core_op : m_core_ops) { - auto stream = core_op->get_output_stream_by_name(stream_name); - CHECK(stream, HAILO_INTERNAL_FAILURE); - low_level_streams.emplace_back(dynamic_cast(stream.release().get())); + std::map> low_level_streams; + for (const auto &pair : m_core_ops) { + auto &device_id = pair.first; + auto &core_op_vector = pair.second; + for (auto &core_op : core_op_vector) { + auto stream = core_op->get_output_stream_by_name(stream_name); + CHECK(stream, HAILO_INTERNAL_FAILURE); + low_level_streams.emplace(device_id, dynamic_cast(stream.release().get())); + } } - auto output_stream = OutputVDeviceBaseStream::create(std::move(low_level_streams), edge_layer.value(), - scheduler_handle, m_core_op_activated_event, m_core_ops_scheduler); + auto output_stream = VDeviceOutputStreamBase::create(std::move(low_level_streams), stream_params, + edge_layer.value(), scheduler_handle, m_core_op_activated_event, m_core_ops_scheduler); CHECK_EXPECTED_AS_STATUS(output_stream); - auto output_stream_wrapper = VDeviceOutputStreamMultiplexerWrapper::create(output_stream.release(), edge_layer->network_name, multiplexer, scheduler_handle); - CHECK_EXPECTED_AS_STATUS(output_stream_wrapper); - m_output_streams.insert(make_pair(stream_name, output_stream_wrapper.release())); + + if (multiplexer) { + // We allow multiplexer only on scheduled streams. + auto output_stream_wrapper = VDeviceOutputStreamMultiplexerWrapper::create(output_stream.release(), edge_layer->network_name, multiplexer, scheduler_handle); + CHECK_EXPECTED_AS_STATUS(output_stream_wrapper); + m_output_streams.insert(make_pair(stream_name, output_stream_wrapper.release())); + } else { + m_output_streams.insert(make_pair(stream_name, output_stream.release())); + } } else { assert(1 == m_core_ops.size()); - assert(contains(m_core_ops[0]->m_output_streams, stream_name)); - m_output_streams.insert(make_pair(stream_name, m_core_ops[0]->m_output_streams.at(stream_name))); + assert(contains((m_core_ops.begin()->second)[0]->m_output_streams, stream_name)); + m_output_streams.insert(make_pair(stream_name, (m_core_ops.begin()->second)[0]->m_output_streams.at(stream_name))); } return HAILO_SUCCESS; @@ -266,6 +296,7 @@ hailo_status VDeviceCoreOp::create_vdevice_streams_from_duplicate(std::shared_pt LOGGER__WARNING("Latency measurement is not supported on more than 1 physical device."); } + assert(other->m_multiplexer != nullptr); m_multiplexer = other->m_multiplexer; m_multiplexer_handle = other->multiplexer_duplicates_count() + 1; @@ -347,7 +378,7 @@ hailo_status VDeviceCoreOp::set_scheduler_priority(uint8_t priority, const std:: Expected> VDeviceCoreOp::get_latency_meters() { - return m_core_ops[0]->get_latency_meters(); + return m_core_ops.begin()->second[0]->get_latency_meters(); } Expected VDeviceCoreOp::get_boundary_vdma_channel_by_stream_name(const std::string &stream_name) @@ -355,7 +386,7 @@ Expected VDeviceCoreOp::get_boundary_vdma_channel_by_s CHECK_AS_EXPECTED(1 == m_core_ops.size(), HAILO_INVALID_OPERATION, "get_boundary_vdma_channel_by_stream_name function is not supported on more than 1 physical device."); - return m_core_ops[0]->get_boundary_vdma_channel_by_stream_name(stream_name); + return m_core_ops.begin()->second[0]->get_boundary_vdma_channel_by_stream_name(stream_name); } void VDeviceCoreOp::set_vstreams_multiplexer_callbacks(std::vector &output_vstreams) @@ -376,10 +407,10 @@ void VDeviceCoreOp::set_vstreams_multiplexer_callbacks(std::vector> VDeviceCoreOp::get_core_op_by_device_index(uint32_t device_index) +Expected> VDeviceCoreOp::get_core_op_by_device_id(const device_id_t &device_id) { - CHECK_AS_EXPECTED(device_index < m_core_ops.size(), HAILO_INVALID_ARGUMENT); - auto core_op = std::dynamic_pointer_cast(m_core_ops[device_index]); + CHECK_AS_EXPECTED(m_core_ops.count(device_id), HAILO_INVALID_ARGUMENT); + auto core_op = std::dynamic_pointer_cast(m_core_ops[device_id][0]); CHECK_NOT_NULL_AS_EXPECTED(core_op, HAILO_INTERNAL_FAILURE); return core_op; } @@ -407,4 +438,11 @@ Expected> VDeviceCoreOp::create_activated return res; } +Expected VDeviceCoreOp::run_hw_infer_estimator() +{ + CHECK_AS_EXPECTED(1 == m_core_ops.size(), HAILO_INVALID_OPERATION, + "run_hw_infer_estimator function is not supported on more than 1 physical device."); + return m_core_ops.begin()->second[0]->run_hw_infer_estimator(); +} + } /* namespace hailort */ diff --git a/hailort/libhailort/src/vdevice/vdevice_core_op.hpp b/hailort/libhailort/src/vdevice/vdevice_core_op.hpp index 6fb1837..e93c5e8 100644 --- a/hailort/libhailort/src/vdevice/vdevice_core_op.hpp +++ b/hailort/libhailort/src/vdevice/vdevice_core_op.hpp @@ -16,7 +16,7 @@ #include "hailo/network_group.hpp" #include "hailo/vstream.hpp" -#include "vdevice/scheduler/network_group_scheduler.hpp" +#include "vdevice/scheduler/scheduler.hpp" #include "vdevice/pipeline_multiplexer.hpp" #include @@ -28,7 +28,7 @@ namespace hailort class VDeviceActivatedCoreOp : public ActivatedCoreOp { public: - static Expected> create(std::vector> &core_ops, + static Expected> create(std::map>> &core_ops, std::map> &input_streams, std::map> &output_streams, const hailo_activate_network_group_params_t &network_group_params, EventPtr core_op_activated_event, @@ -76,7 +76,7 @@ private: class VDeviceCoreOp : public CoreOp { public: - static Expected> create(std::vector> core_ops, + static Expected> create(const std::map>> &core_ops, CoreOpsSchedulerWeakPtr core_ops_scheduler, const std::string &hef_hash); static Expected> duplicate(std::shared_ptr other); @@ -112,10 +112,19 @@ public: return false; } - uint32_t multiplexer_duplicates_count() + uint32_t multiplexer_duplicates_count() const { - assert(m_multiplexer->instances_count() > 0); - return static_cast(m_multiplexer->instances_count() - 1); + if (m_multiplexer) { + assert(m_multiplexer->instances_count() > 0); + return static_cast(m_multiplexer->instances_count() - 1); + } else { + return 0; + } + } + + bool multiplexer_supported() const + { + return nullptr != m_multiplexer; } virtual Expected get_default_streams_interface() override; @@ -155,13 +164,15 @@ public: const hailo_activate_network_group_params_t &network_group_params, uint16_t dynamic_batch_size, bool resume_pending_stream_transfers) override; - Expected> get_core_op_by_device_index(uint32_t device_index); + Expected> get_core_op_by_device_id(const device_id_t &device_bdf_id); + + virtual Expected run_hw_infer_estimator() override; private: - VDeviceCoreOp(std::vector> core_ops, CoreOpsSchedulerWeakPtr core_ops_scheduler, + VDeviceCoreOp(const std::map>> &core_ops, CoreOpsSchedulerWeakPtr core_ops_scheduler, const std::string &hef_hash, hailo_status &status); - std::vector> m_core_ops; + std::map>> m_core_ops; CoreOpsSchedulerWeakPtr m_core_ops_scheduler; scheduler_core_op_handle_t m_scheduler_handle; multiplexer_core_op_handle_t m_multiplexer_handle; diff --git a/hailort/libhailort/src/vdevice/vdevice_internal.hpp b/hailort/libhailort/src/vdevice/vdevice_internal.hpp index 22c2948..a2ba698 100644 --- a/hailort/libhailort/src/vdevice/vdevice_internal.hpp +++ b/hailort/libhailort/src/vdevice/vdevice_internal.hpp @@ -27,7 +27,7 @@ #include "vdma/vdma_device.hpp" #include "vdma/vdma_config_manager.hpp" #include "vdevice/vdevice_core_op.hpp" -#include "vdevice/scheduler/network_group_scheduler.hpp" +#include "vdevice/scheduler/scheduler.hpp" #ifdef HAILO_SUPPORT_MULTI_PROCESS #include "service/hailort_rpc_client.hpp" @@ -55,7 +55,8 @@ public: { // Return Expected for future functionality std::vector> devices_refs; - for (auto &device : m_devices) { + for (const auto &pair : m_devices) { + auto &device = pair.second; devices_refs.push_back(*device); } return devices_refs; @@ -65,8 +66,9 @@ public: { std::vector device_ids; device_ids.reserve(m_devices.size()); - for (auto &device : m_devices) { - device_ids.push_back(device.get()->get_dev_id()); + for (const auto &pair : m_devices) { + auto &id = pair.first; + device_ids.push_back(id); } return device_ids; } @@ -82,16 +84,18 @@ public: static hailo_status validate_params(const hailo_vdevice_params_t ¶ms); private: - VDeviceBase(std::vector> &&devices, CoreOpsSchedulerPtr core_ops_scheduler) : + VDeviceBase(std::map> &&devices, CoreOpsSchedulerPtr core_ops_scheduler) : m_devices(std::move(devices)), m_core_ops_scheduler(core_ops_scheduler) {} - static Expected>> create_devices(const hailo_vdevice_params_t ¶ms); + static Expected>> create_devices(const hailo_vdevice_params_t ¶ms); static Expected> get_device_ids(const hailo_vdevice_params_t ¶ms); Expected create_local_config_params(Hef &hef, const NetworkGroupsParamsMap &configure_params); - Expected> create_vdevice_network_group(Hef &hef, const std::pair ¶ms); + Expected> create_vdevice_network_group(Hef &hef, + const std::pair ¶ms, bool use_multiplexer); + bool should_use_multiplexer(const ConfigureNetworkParams ¶ms); - std::vector> m_devices; + std::map> m_devices; CoreOpsSchedulerPtr m_core_ops_scheduler; std::vector> m_vdevice_core_ops; std::vector> m_network_groups; // TODO: HRT-9547 - Remove when ConfiguredNetworkGroup will be kept in global context @@ -124,12 +128,13 @@ public: virtual hailo_status after_fork_in_child() override; private: - VDeviceClient(std::unique_ptr client, uint32_t handle); + VDeviceClient(std::unique_ptr client, uint32_t handle, std::vector> &&devices); hailo_status create_client(); std::unique_ptr m_client; uint32_t m_handle; + std::vector> m_devices; std::vector> m_network_groups; }; diff --git a/hailort/libhailort/src/vdevice/vdevice_native_stream.cpp b/hailort/libhailort/src/vdevice/vdevice_native_stream.cpp new file mode 100644 index 0000000..2022c18 --- /dev/null +++ b/hailort/libhailort/src/vdevice/vdevice_native_stream.cpp @@ -0,0 +1,259 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file vdevice_native_stream.cpp + * @brief Internal stream implementation for native streams + * + **/ + +#include "vdevice_native_stream.hpp" + +namespace hailort { + +/** Input stream **/ +hailo_status VDeviceNativeInputStreamBase::abort() +{ + auto status = HAILO_SUCCESS; // Best effort + for (auto &pair: m_streams){ + auto &stream = pair.second; + auto abort_status = stream.get().abort(); + if (HAILO_SUCCESS != status) { + LOGGER__ERROR("Failed to abort input stream. (status: {} device: {})", status, stream.get().get_dev_id()); + status = abort_status; + } + } + return status; +} + +hailo_status VDeviceNativeInputStreamBase::clear_abort() +{ + auto status = HAILO_SUCCESS; // Best effort + for (auto &pair: m_streams){ + auto &stream = pair.second; + auto clear_abort_status = stream.get().clear_abort(); + if ((HAILO_SUCCESS != clear_abort_status) && (HAILO_STREAM_NOT_ACTIVATED != clear_abort_status)) { + LOGGER__ERROR("Failed to clear abort input stream. (status: {} device: {})", clear_abort_status, stream.get().get_dev_id()); + status = clear_abort_status; + } + } + + return status; +} + +Expected> VDeviceNativeInputStream::create( + std::map> &&streams, + EventPtr &&core_op_activated_event, + const LayerInfo &layer_info) +{ + auto status = HAILO_UNINITIALIZED; + auto stream = make_unique_nothrow(std::move(streams), + std::move(core_op_activated_event), layer_info, status); + CHECK_AS_EXPECTED((nullptr != stream), HAILO_OUT_OF_HOST_MEMORY); + CHECK_SUCCESS_AS_EXPECTED(status); + return stream; +} + +hailo_status VDeviceNativeInputStream::write_impl(const MemoryView &buffer, const std::function &should_cancel) +{ + if (should_cancel()) { + return HAILO_STREAM_ABORTED_BY_USER; + } + auto status = m_streams.at(m_next_transfer_stream).get().write_impl(buffer); + if (HAILO_SUCCESS != status) { + LOGGER__INFO("Write to stream has failed! status = {}", status); + return status; + } + + // Update m_next_transfer_stream only if 'batch' frames has been transferred + if (0 == (++m_acc_frames % m_streams.begin()->second.get().get_dynamic_batch_size())) { + auto it = m_streams.upper_bound(m_next_transfer_stream); + if (m_streams.end() == it) { + it = m_streams.begin(); + } + m_next_transfer_stream = it->first; + m_acc_frames = 0; + } + return HAILO_SUCCESS; +} + +Expected> VDeviceNativeAsyncInputStream::create( + std::map> &&streams, + EventPtr &&core_op_activated_event, + const LayerInfo &layer_info) +{ + auto max_queue_size_per_stream = streams.begin()->second.get().get_buffer_frames_size(); + CHECK_EXPECTED(max_queue_size_per_stream); + const auto max_queue_size = max_queue_size_per_stream.value() * streams.size(); + + auto status = HAILO_UNINITIALIZED; + auto stream = make_unique_nothrow(std::move(streams), + std::move(core_op_activated_event), layer_info, max_queue_size, status); + CHECK_AS_EXPECTED((nullptr != stream), HAILO_OUT_OF_HOST_MEMORY); + CHECK_SUCCESS_AS_EXPECTED(status); + return stream; +} + +hailo_status VDeviceNativeAsyncInputStream::wait_for_async_ready(size_t transfer_size, std::chrono::milliseconds timeout) +{ + return m_streams.at(m_next_transfer_stream).get().wait_for_async_ready(transfer_size, timeout); +} + +Expected VDeviceNativeAsyncInputStream::get_async_max_queue_size() const +{ + return Expected(m_max_queue_size); +} + +hailo_status VDeviceNativeAsyncInputStream::write_async(TransferRequest &&transfer_request) +{ + // TODO HRT-10583 - allow option to remove reorder queue + transfer_request.callback = m_callback_reorder_queue.wrap_callback(transfer_request.callback); + + auto status = m_streams.at(m_next_transfer_stream).get().write_async(std::move(transfer_request)); + if (HAILO_SUCCESS != status) { + m_callback_reorder_queue.cancel_last_callback(); + return status; + } + + // Update m_next_transfer_stream_index only if 'batch' frames has been transferred + if (0 == (++m_acc_frames % m_streams.begin()->second.get().get_dynamic_batch_size())) { + auto it = m_streams.upper_bound(m_next_transfer_stream); + if (m_streams.end() == it) { + it = m_streams.begin(); + } + m_next_transfer_stream = it->first; + m_acc_frames = 0; + } + return HAILO_SUCCESS; +} + +hailo_status VDeviceNativeAsyncInputStream::write_impl(const MemoryView &, const std::function &) +{ + LOGGER__ERROR("Sync write is not supported by async streams"); + return HAILO_INVALID_OPERATION; +} + +/** Output stream **/ +hailo_status VDeviceNativeOutputStreamBase::abort() +{ + auto status = HAILO_SUCCESS; // Best effort + for (const auto &pair : m_streams) { + auto &stream = pair.second; + auto abort_status = stream.get().abort(); + if (HAILO_SUCCESS != status) { + LOGGER__ERROR("Failed to abort output stream. (status: {} device: {})", status, stream.get().get_dev_id()); + status = abort_status; + } + } + + return status; +} + +hailo_status VDeviceNativeOutputStreamBase::clear_abort() +{ + auto status = HAILO_SUCCESS; // Best effort + for (const auto &pair : m_streams) { + auto &stream = pair.second; + auto clear_abort_status = stream.get().clear_abort(); + if ((HAILO_SUCCESS != clear_abort_status) && (HAILO_STREAM_NOT_ACTIVATED != clear_abort_status)) { + LOGGER__ERROR("Failed to clear abort output stream. (status: {} device: {})", clear_abort_status, stream.get().get_dev_id()); + status = clear_abort_status; + } + } + + return status; +} + +Expected> VDeviceNativeOutputStream::create( + std::map> &&streams, + EventPtr &&core_op_activated_event, + const LayerInfo &layer_info) +{ + auto status = HAILO_UNINITIALIZED; + auto stream = make_unique_nothrow(std::move(streams), + std::move(core_op_activated_event), layer_info, status); + CHECK_AS_EXPECTED((nullptr != stream), HAILO_OUT_OF_HOST_MEMORY); + CHECK_SUCCESS_AS_EXPECTED(status); + return stream; +} + +hailo_status VDeviceNativeOutputStream::read(MemoryView buffer) +{ + auto status = m_streams.at(m_next_transfer_stream).get().read(buffer); + if (HAILO_STREAM_ABORTED_BY_USER == status) { + // In case of aborted by user, don't show it as infer error + LOGGER__INFO("Stream aborted by user (device: {})", m_streams.at(m_next_transfer_stream).get().get_dev_id()); + return status; + } + CHECK_SUCCESS(status, "Read from stream has failed! status = {}", status); + + // Update m_next_transfer_stream_index only if 'batch' frames has been transferred + if (0 == (++m_acc_frames % m_streams.begin()->second.get().get_dynamic_batch_size())) { + auto it = m_streams.upper_bound(m_next_transfer_stream); + if (m_streams.end() == it) { + it = m_streams.begin(); + } + m_next_transfer_stream = it->first; + m_acc_frames = 0; + } + + return HAILO_SUCCESS; +} + +Expected> VDeviceNativeAsyncOutputStream::create( + std::map> &&streams, + EventPtr &&core_op_activated_event, + const LayerInfo &layer_info) +{ + auto max_queue_size_per_stream = streams.begin()->second.get().get_buffer_frames_size(); + CHECK_EXPECTED(max_queue_size_per_stream); + const auto max_queue_size = max_queue_size_per_stream.value() * streams.size(); + + auto status = HAILO_UNINITIALIZED; + auto stream = make_unique_nothrow(std::move(streams), + std::move(core_op_activated_event), layer_info, max_queue_size, status); + CHECK_AS_EXPECTED((nullptr != stream), HAILO_OUT_OF_HOST_MEMORY); + CHECK_SUCCESS_AS_EXPECTED(status); + return stream; +} + +hailo_status VDeviceNativeAsyncOutputStream::wait_for_async_ready(size_t transfer_size, std::chrono::milliseconds timeout) +{ + return m_streams.at(m_next_transfer_stream).get().wait_for_async_ready(transfer_size, timeout); +} + +Expected VDeviceNativeAsyncOutputStream::get_async_max_queue_size() const +{ + return Expected(m_max_queue_size); +} + +hailo_status VDeviceNativeAsyncOutputStream::read_async(TransferRequest &&transfer_request) +{ + // TODO HRT-10583 - allow option to remove reorder queue + transfer_request.callback = m_callback_reorder_queue.wrap_callback(transfer_request.callback); + auto status = m_streams.at(m_next_transfer_stream).get().read_async(std::move(transfer_request)); + if (HAILO_SUCCESS != status) { + m_callback_reorder_queue.cancel_last_callback(); + return status; + } + // Update m_next_transfer_stream_index only if 'batch' frames has been transferred + if (0 == (++m_acc_frames % m_streams.begin()->second.get().get_dynamic_batch_size())) { + auto it = m_streams.upper_bound(m_next_transfer_stream); + if (m_streams.end() == it) { + it = m_streams.begin(); + } + m_next_transfer_stream = it->first; + m_acc_frames = 0; + } + + return HAILO_SUCCESS; +} + +hailo_status VDeviceNativeAsyncOutputStream::read(MemoryView) +{ + LOGGER__ERROR("The read function is not supported by async streams"); + return HAILO_INVALID_OPERATION; +} + +} /* namespace hailort */ \ No newline at end of file diff --git a/hailort/libhailort/src/vdevice/vdevice_native_stream.hpp b/hailort/libhailort/src/vdevice/vdevice_native_stream.hpp index 5ddfe8c..61ce42c 100644 --- a/hailort/libhailort/src/vdevice/vdevice_native_stream.hpp +++ b/hailort/libhailort/src/vdevice/vdevice_native_stream.hpp @@ -16,48 +16,128 @@ #include "stream_common/stream_internal.hpp" #include "vdevice_stream.hpp" +#include "vdevice/callback_reorder_queue.hpp" namespace hailort { -class InputVDeviceNativeStream : public InputVDeviceBaseStream { + +class VDeviceNativeInputStreamBase : public VDeviceInputStreamBase { public: - InputVDeviceNativeStream( - std::vector> &&streams, + static Expected> create( + std::map> &&streams, + EventPtr &&core_op_activated_event, + const LayerInfo &layer_info); + + VDeviceNativeInputStreamBase( + std::map> &&streams, EventPtr &&core_op_activated_event, const LayerInfo &layer_info, hailo_status &status) : - InputVDeviceBaseStream(std::move(streams), std::move(core_op_activated_event), layer_info, status) + VDeviceInputStreamBase(std::move(streams), std::move(core_op_activated_event), layer_info, status) {} virtual hailo_status abort() override; virtual hailo_status clear_abort() override; virtual bool is_scheduled() override { return false; }; +}; + +class VDeviceNativeInputStream : public VDeviceNativeInputStreamBase { +public: + static Expected> create( + std::map> &&streams, + EventPtr &&core_op_activated_event, + const LayerInfo &layer_info); + + using VDeviceNativeInputStreamBase::VDeviceNativeInputStreamBase; protected: - virtual Expected sync_write_raw_buffer(const MemoryView &buffer, - const std::function &should_cancel = []() { return false; }) override; + virtual hailo_status write_impl(const MemoryView &buffer, const std::function &should_cancel) override;\ }; -class OutputVDeviceNativeStream : public OutputVDeviceBaseStream { +class VDeviceNativeAsyncInputStream : public VDeviceNativeInputStreamBase { public: - OutputVDeviceNativeStream( - std::vector> &&streams, + static Expected> create( + std::map> &&streams, + EventPtr &&core_op_activated_event, + const LayerInfo &layer_info); + + VDeviceNativeAsyncInputStream( + std::map> &&streams, + EventPtr &&core_op_activated_event, const LayerInfo &layer_info, + size_t max_queue_size, + hailo_status &status) : + VDeviceNativeInputStreamBase(std::move(streams), std::move(core_op_activated_event), layer_info, status), + m_callback_reorder_queue(max_queue_size), // TODO HRT-1058 - use reorder queue only when needed + m_max_queue_size(max_queue_size) + {} + + virtual hailo_status wait_for_async_ready(size_t transfer_size, std::chrono::milliseconds timeout) override; + virtual hailo_status write_async(TransferRequest &&transfer_request) override; + virtual Expected get_async_max_queue_size() const override; + +protected: + virtual hailo_status write_impl(const MemoryView &buffer, const std::function &should_cancel) override; + +private: + CallbackReorderQueue m_callback_reorder_queue; + const size_t m_max_queue_size; +}; + +class VDeviceNativeOutputStreamBase : public VDeviceOutputStreamBase { +public: + VDeviceNativeOutputStreamBase( + std::map> &&streams, EventPtr &&core_op_activated_event, + const LayerInfo &layer_info, hailo_status &status) : - OutputVDeviceBaseStream(std::move(streams), layer_info, std::move(core_op_activated_event), status) + VDeviceOutputStreamBase(std::move(streams), layer_info, std::move(core_op_activated_event), status) {} virtual hailo_status abort() override; virtual hailo_status clear_abort() override; virtual bool is_scheduled() override { return false; }; +}; -protected: - virtual hailo_status read(MemoryView buffer) override;; +class VDeviceNativeOutputStream : public VDeviceNativeOutputStreamBase { +public: + static Expected> create( + std::map> &&streams, + EventPtr &&core_op_activated_event, const LayerInfo &layer_info); + + using VDeviceNativeOutputStreamBase::VDeviceNativeOutputStreamBase; + virtual hailo_status read(MemoryView buffer) override; }; +class VDeviceNativeAsyncOutputStream : public VDeviceNativeOutputStreamBase { +public: + static Expected> create( + std::map> &&streams, + EventPtr &&core_op_activated_event, const LayerInfo &layer_info); + + VDeviceNativeAsyncOutputStream( + std::map> &&streams, + EventPtr &&core_op_activated_event, + const LayerInfo &layer_info, + size_t max_queue_size, + hailo_status &status) : + VDeviceNativeOutputStreamBase(std::move(streams), std::move(core_op_activated_event), layer_info, status), + m_callback_reorder_queue(max_queue_size), // TODO HRT-1058 - use reorder queue only when needed + m_max_queue_size(max_queue_size) + {} + + virtual hailo_status wait_for_async_ready(size_t transfer_size, std::chrono::milliseconds timeout) override; + virtual hailo_status read_async(TransferRequest &&transfer_request) override; + virtual Expected get_async_max_queue_size() const override; + virtual hailo_status read(MemoryView buffer) override; + +private: + CallbackReorderQueue m_callback_reorder_queue; + const size_t m_max_queue_size; + }; + } /* namespace hailort */ #endif /* HAILO_VDEVICE_NATIVE_STREAM_HPP_ */ diff --git a/hailort/libhailort/src/vdevice/vdevice_stream.cpp b/hailort/libhailort/src/vdevice/vdevice_stream.cpp index f50ec24..6123597 100644 --- a/hailort/libhailort/src/vdevice/vdevice_stream.cpp +++ b/hailort/libhailort/src/vdevice/vdevice_stream.cpp @@ -16,7 +16,6 @@ #include "common/utils.hpp" -#include "utils/profiler/tracer_macros.hpp" #include "vdevice/vdevice_stream.hpp" #include "vdevice/vdevice_native_stream.hpp" #include "vdevice/scheduler/multi_device_scheduled_stream.hpp" @@ -29,22 +28,8 @@ namespace hailort { -hailo_status InputVDeviceBaseStream::deactivate_stream() -{ - auto status = HAILO_SUCCESS; // Best effort - for (auto &stream : m_streams) { - auto deactivate_status = stream.get().deactivate_stream(); - if (HAILO_SUCCESS != deactivate_status) { - LOGGER__ERROR("Failed to deactivate input stream. (status: {} device: {})", deactivate_status, stream.get().get_dev_id()); - status = deactivate_status; - } - } - m_is_stream_activated = false; - return status; -} - /** Input stream **/ -InputVDeviceBaseStream::~InputVDeviceBaseStream() +VDeviceInputStreamBase::~VDeviceInputStreamBase() { // We want to stop the vdma channel before closing the stream in the firmware // because sending data to a closed stream may terminate the dma engine @@ -53,9 +38,10 @@ InputVDeviceBaseStream::~InputVDeviceBaseStream() } } -hailo_status InputVDeviceBaseStream::activate_stream(uint16_t dynamic_batch_size, bool resume_pending_stream_transfers) +hailo_status VDeviceInputStreamBase::activate_stream(uint16_t dynamic_batch_size, bool resume_pending_stream_transfers) { - for (auto &stream : m_streams) { + for (const auto &pair : m_streams) { + auto &stream = pair.second; auto status = stream.get().activate_stream(dynamic_batch_size, resume_pending_stream_transfers); if (HAILO_SUCCESS != status) { LOGGER__ERROR("Failed to activate input stream. (device: {})", stream.get().get_dev_id()); @@ -67,106 +53,118 @@ hailo_status InputVDeviceBaseStream::activate_stream(uint16_t dynamic_batch_size return HAILO_SUCCESS; } -hailo_status InputVDeviceBaseStream::sync_write_all_raw_buffer_no_transform_impl(void *buffer, size_t offset, size_t size) +hailo_status VDeviceInputStreamBase::deactivate_stream() { - ASSERT(NULL != buffer); - - return sync_write_raw_buffer(MemoryView(static_cast(buffer) + offset, size)).status(); + auto status = HAILO_SUCCESS; // Best effort + for (const auto &pair : m_streams) { + auto &stream = pair.second; + auto deactivate_status = stream.get().deactivate_stream(); + if (HAILO_SUCCESS != deactivate_status) { + LOGGER__ERROR("Failed to deactivate input stream. (status: {} device: {})", deactivate_status, stream.get().get_dev_id()); + status = deactivate_status; + } + } + m_is_stream_activated = false; + return status; } -hailo_status InputVDeviceBaseStream::send_pending_buffer(size_t device_index) +hailo_status VDeviceInputStreamBase::send_pending_buffer(const device_id_t &device_id) { assert(1 == m_streams.size()); - CHECK(0 == device_index, HAILO_INVALID_OPERATION); - VdmaInputStream &vdma_input = static_cast(m_streams[m_next_transfer_stream_index].get()); - return vdma_input.send_pending_buffer(); + auto &vdma_input = dynamic_cast(m_streams.at(m_next_transfer_stream).get()); + return vdma_input.send_pending_buffer(device_id); } -Expected InputVDeviceBaseStream::get_buffer_frames_size() const +Expected VDeviceInputStreamBase::get_buffer_frames_size() const { - size_t total_buffers_size = 0; - for (auto &stream : m_streams) { - auto stream_buffer_size = stream.get().get_buffer_frames_size(); - CHECK_EXPECTED(stream_buffer_size); - total_buffers_size += stream_buffer_size.value(); - } - - return total_buffers_size; + return m_streams.begin()->second.get().get_buffer_frames_size(); } -Expected InputVDeviceBaseStream::get_pending_frames_count() const +Expected VDeviceInputStreamBase::get_pending_frames_count() const { size_t total_pending_frames_count = 0; - for (auto &stream : m_streams) { + for (const auto &pair : m_streams) { + auto &stream = pair.second; auto stream_pending_frames_count = stream.get().get_pending_frames_count(); CHECK_EXPECTED(stream_pending_frames_count); total_pending_frames_count += stream_pending_frames_count.value(); } - return total_pending_frames_count; } -Expected> InputVDeviceBaseStream::create(std::vector> &&low_level_streams, - const LayerInfo &edge_layer, const scheduler_core_op_handle_t &core_op_handle, - EventPtr core_op_activated_event, CoreOpsSchedulerWeakPtr core_ops_scheduler) +Expected> VDeviceInputStreamBase::create( + std::map> &&low_level_streams, + const hailo_stream_parameters_t &stream_params, const LayerInfo &edge_layer, + const scheduler_core_op_handle_t &core_op_handle, EventPtr core_op_activated_event, + CoreOpsSchedulerWeakPtr core_ops_scheduler) { assert(0 < low_level_streams.size()); - auto status = HAILO_UNINITIALIZED; - - std::unique_ptr local_vdevice_stream; if (core_ops_scheduler.lock()) { - if (1 < low_level_streams.size()) { - auto buffer_frame_size = low_level_streams[0].get().get_buffer_frames_size(); - CHECK_EXPECTED(buffer_frame_size); - auto frame_size = low_level_streams[0].get().get_frame_size(); - auto buffers_queue_ptr = BuffersQueue::create_unique(frame_size, (low_level_streams.size() * buffer_frame_size.value())); - CHECK_EXPECTED(buffers_queue_ptr); - - local_vdevice_stream = make_unique_nothrow(std::move(low_level_streams), + if ((stream_params.flags & HAILO_STREAM_FLAGS_ASYNC) != 0) { + auto stream = ScheduledAsyncInputStream::create(std::move(low_level_streams), core_op_handle, std::move(core_op_activated_event), edge_layer, - core_ops_scheduler, buffers_queue_ptr.release(), status); + core_ops_scheduler); + CHECK_EXPECTED(stream); + return std::unique_ptr(stream.release()); } else { - local_vdevice_stream = make_unique_nothrow(std::move(low_level_streams), - core_op_handle, std::move(core_op_activated_event), edge_layer, - core_ops_scheduler, status); + if (1 < low_level_streams.size()) { + auto stream = MultiDeviceScheduledInputStream::create(std::move(low_level_streams), + core_op_handle, std::move(core_op_activated_event), edge_layer, + core_ops_scheduler); + CHECK_EXPECTED(stream); + return std::unique_ptr(stream.release()); + } else { + auto stream = ScheduledInputStream::create(std::move(low_level_streams), + core_op_handle, std::move(core_op_activated_event), edge_layer, + core_ops_scheduler); + CHECK_EXPECTED(stream); + return std::unique_ptr(stream.release()); + } } } else { - local_vdevice_stream = make_unique_nothrow(std::move(low_level_streams), - std::move(core_op_activated_event), edge_layer,status); - } - - CHECK_AS_EXPECTED((nullptr != local_vdevice_stream), HAILO_OUT_OF_HOST_MEMORY); - CHECK_SUCCESS_AS_EXPECTED(status); + if ((stream_params.flags & HAILO_STREAM_FLAGS_ASYNC) != 0) { + auto stream = VDeviceNativeAsyncInputStream::create(std::move(low_level_streams), + std::move(core_op_activated_event), edge_layer); + CHECK_EXPECTED(stream); + return std::unique_ptr(stream.release()); + } else { + auto stream = VDeviceNativeInputStream::create(std::move(low_level_streams), + std::move(core_op_activated_event), edge_layer); + CHECK_EXPECTED(stream); + return std::unique_ptr(stream.release()); + } - return local_vdevice_stream; + } } -hailo_status InputVDeviceBaseStream::set_timeout(std::chrono::milliseconds timeout) +hailo_status VDeviceInputStreamBase::set_timeout(std::chrono::milliseconds timeout) { - for (auto &stream : m_streams) { + for (const auto &pair : m_streams) { + auto &stream = pair.second; auto status = stream.get().set_timeout(timeout); CHECK_SUCCESS(status, "Failed to set timeout to input stream. (device: {})", stream.get().get_dev_id()); } return HAILO_SUCCESS; } -std::chrono::milliseconds InputVDeviceBaseStream::get_timeout() const +std::chrono::milliseconds VDeviceInputStreamBase::get_timeout() const { // All timeout values of m_streams should be the same - return m_streams[0].get().get_timeout(); + return m_streams.begin()->second.get().get_timeout(); } -hailo_stream_interface_t InputVDeviceBaseStream::get_interface() const +hailo_stream_interface_t VDeviceInputStreamBase::get_interface() const { // All interface values of m_streams should be the same - return m_streams[0].get().get_interface(); + return m_streams.begin()->second.get().get_interface(); } -hailo_status InputVDeviceBaseStream::flush() +hailo_status VDeviceInputStreamBase::flush() { auto status = HAILO_SUCCESS; // Best effort - for (auto &stream : m_streams) { + for (const auto &pair : m_streams) { + auto &stream = pair.second; auto flush_status = stream.get().flush(); if (HAILO_SUCCESS != status) { LOGGER__ERROR("Failed to flush input stream. (status: {} device: {})", status, stream.get().get_dev_id()); @@ -176,152 +174,17 @@ hailo_status InputVDeviceBaseStream::flush() return status; } -Expected ScheduledInputStream::sync_write_raw_buffer(const MemoryView &buffer, const std::function &should_cancel) -{ - return sync_write_raw_buffer_impl(buffer, m_core_op_handle, should_cancel); -} - -Expected InputVDeviceNativeStream::sync_write_raw_buffer(const MemoryView &buffer, const std::function &should_cancel) -{ - if (should_cancel()) { - return make_unexpected(HAILO_STREAM_ABORTED_BY_USER); - } - - auto expected_written_bytes = m_streams[m_next_transfer_stream_index].get().sync_write_raw_buffer(buffer); - if (HAILO_SUCCESS != expected_written_bytes.status()) { - LOGGER__INFO("Write to stream has failed! status = {}", expected_written_bytes.status()); - return make_unexpected(expected_written_bytes.status()); - } - auto written_bytes = expected_written_bytes.value(); - - // Update m_next_transfer_stream_index only if 'batch' frames has been transferred - if (0 == (++m_acc_frames % m_streams[0].get().get_dynamic_batch_size())) { - m_next_transfer_stream_index = static_cast((m_next_transfer_stream_index + 1) % m_streams.size()); - m_acc_frames = 0; - } - return written_bytes; -} - -Expected ScheduledInputStream::sync_write_raw_buffer_impl(const MemoryView &buffer, scheduler_core_op_handle_t core_op_handle, - const std::function &should_cancel) -{ - auto core_ops_scheduler = m_core_ops_scheduler.lock(); - CHECK_AS_EXPECTED(core_ops_scheduler, HAILO_INTERNAL_FAILURE); - - auto status = core_ops_scheduler->wait_for_write(core_op_handle, name(), get_timeout(), should_cancel); - if (HAILO_STREAM_ABORTED_BY_USER == status) { - LOGGER__INFO("Write to stream was aborted."); - return make_unexpected(status); - } - CHECK_SUCCESS_AS_EXPECTED(status); - - TRACE(WriteFrameTrace, "", core_op_handle, m_stream_info.name); - - assert(1 == m_streams.size()); - status = m_streams[0].get().write_buffer_only(buffer, should_cancel); - - auto write_finish_status = core_ops_scheduler->signal_write_finish(core_op_handle, name(), status != HAILO_SUCCESS); - if (HAILO_SUCCESS != status) { - LOGGER__INFO("Write to stream has failed! status = {}", status); - return make_unexpected(status); - } - - if (HAILO_STREAM_ABORTED_BY_USER == write_finish_status) { - return make_unexpected(write_finish_status); - } - CHECK_SUCCESS_AS_EXPECTED(write_finish_status); - - auto written_bytes = buffer.size(); - return written_bytes; -} - -hailo_status ScheduledInputStream::abort() -{ - return abort_impl(m_core_op_handle); -} - -hailo_status InputVDeviceNativeStream::abort() -{ - auto status = HAILO_SUCCESS; // Best effort - for (auto &stream : m_streams) { - auto abort_status = stream.get().abort(); - if (HAILO_SUCCESS != status) { - LOGGER__ERROR("Failed to abort input stream. (status: {} device: {})", status, stream.get().get_dev_id()); - status = abort_status; - } - } - - return status; -} - -hailo_status ScheduledInputStream::abort_impl(scheduler_core_op_handle_t core_op_handle) -{ - auto status = HAILO_SUCCESS; // Best effort - assert(1 == m_streams.size()); - auto abort_status = m_streams[0].get().abort(); - if (HAILO_SUCCESS != status) { - LOGGER__ERROR("Failed to abort input stream. (status: {} device: {})", status, m_streams[0].get().get_dev_id()); - status = abort_status; - } - - auto core_ops_scheduler = m_core_ops_scheduler.lock(); - CHECK(core_ops_scheduler, HAILO_INTERNAL_FAILURE); - - auto disable_status = core_ops_scheduler->disable_stream(core_op_handle, name()); - if (HAILO_SUCCESS != disable_status) { - LOGGER__ERROR("Failed to disable stream in the core-op scheduler. (status: {})", disable_status); - status = disable_status; - } - - return status; -} - -hailo_status ScheduledInputStream::clear_abort() +hailo_status VDeviceInputStreamBase::write_impl(const MemoryView &buffer) { - return clear_abort_impl(m_core_op_handle); -} - -hailo_status InputVDeviceNativeStream::clear_abort() -{ - auto status = HAILO_SUCCESS; // Best effort - for (auto &stream : m_streams) { - auto clear_abort_status = stream.get().clear_abort(); - if ((HAILO_SUCCESS != clear_abort_status) && (HAILO_STREAM_NOT_ACTIVATED != clear_abort_status)) { - LOGGER__ERROR("Failed to clear abort input stream. (status: {} device: {})", clear_abort_status, stream.get().get_dev_id()); - status = clear_abort_status; - } - } - - return status; -} - -hailo_status ScheduledInputStream::clear_abort_impl(scheduler_core_op_handle_t core_op_handle) -{ - auto status = HAILO_SUCCESS; // Best effort - assert(1 == m_streams.size()); - auto clear_abort_status = m_streams[0].get().clear_abort(); - if ((HAILO_SUCCESS != clear_abort_status) && (HAILO_STREAM_NOT_ACTIVATED != clear_abort_status)) { - LOGGER__ERROR("Failed to clear abort input stream. (status: {} device: {})", clear_abort_status, m_streams[0].get().get_dev_id()); - status = clear_abort_status; - } - - auto core_ops_scheduler = m_core_ops_scheduler.lock(); - CHECK(core_ops_scheduler, HAILO_INTERNAL_FAILURE); - - auto enable_status = core_ops_scheduler->enable_stream(core_op_handle, name()); - if (HAILO_SUCCESS != enable_status) { - LOGGER__ERROR("Failed to enable stream in the core-op scheduler. (status: {})", enable_status); - status = enable_status; - } - - return status; + return write_impl(buffer, []() { return false; }); } /** Output stream **/ -hailo_status OutputVDeviceBaseStream::deactivate_stream() +hailo_status VDeviceOutputStreamBase::deactivate_stream() { auto status = HAILO_SUCCESS; // Best effort - for (auto &stream : m_streams) { + for (const auto &pair : m_streams) { + auto &stream = pair.second; auto deactivate_status = stream.get().deactivate_stream(); if (HAILO_SUCCESS != status) { LOGGER__ERROR("Failed to deactivate output stream. (status: {} device: {})", status, stream.get().get_dev_id()); @@ -332,7 +195,7 @@ hailo_status OutputVDeviceBaseStream::deactivate_stream() return status; } -OutputVDeviceBaseStream::~OutputVDeviceBaseStream() +VDeviceOutputStreamBase::~VDeviceOutputStreamBase() { // We want to stop the vdma channel before closing the stream in the firmware // because sending data to a closed stream may terminate the dma engine @@ -341,9 +204,10 @@ OutputVDeviceBaseStream::~OutputVDeviceBaseStream() } } -hailo_status OutputVDeviceBaseStream::activate_stream(uint16_t dynamic_batch_size, bool resume_pending_stream_transfers) +hailo_status VDeviceOutputStreamBase::activate_stream(uint16_t dynamic_batch_size, bool resume_pending_stream_transfers) { - for (auto &stream : m_streams) { + for (const auto &pair : m_streams) { + auto &stream = pair.second; auto status = stream.get().activate_stream(dynamic_batch_size, resume_pending_stream_transfers); if (HAILO_SUCCESS != status) { LOGGER__ERROR("Failed to activate output stream. (device: {})", stream.get().get_dev_id()); @@ -355,222 +219,84 @@ hailo_status OutputVDeviceBaseStream::activate_stream(uint16_t dynamic_batch_siz return HAILO_SUCCESS; } -hailo_status OutputVDeviceBaseStream::read_all(MemoryView &/*buffer*/) +hailo_status VDeviceOutputStreamBase::read_impl(MemoryView &/*buffer*/) { - LOGGER__ERROR("read_all should not be called in vdevice flow"); + LOGGER__ERROR("read_impl should not be called in vdevice flow"); return HAILO_INTERNAL_FAILURE; } -Expected OutputVDeviceBaseStream::sync_read_raw_buffer(MemoryView &/*buffer*/) -{ - LOGGER__ERROR("sync_read_raw_buffer should not be called in vdevice flow"); - return make_unexpected(HAILO_INTERNAL_FAILURE); -} - -hailo_status ScheduledOutputStream::read(MemoryView buffer) -{ - return read_impl(buffer, m_core_op_handle); -} - -hailo_status OutputVDeviceNativeStream::read(MemoryView buffer) -{ - auto status = m_streams[m_next_transfer_stream_index].get().read(buffer); - if (HAILO_SUCCESS != status) { - LOGGER__INFO("Read from stream has failed! status = {}", status); - return status; - } - - // Update m_next_transfer_stream_index only if 'batch' frames has been transferred - if (0 == (++m_acc_frames % m_streams[0].get().get_dynamic_batch_size())) { - m_next_transfer_stream_index = static_cast((m_next_transfer_stream_index + 1) % m_streams.size()); - m_acc_frames = 0; - } - - return HAILO_SUCCESS; -} - -hailo_status ScheduledOutputStream::read_impl(MemoryView buffer, scheduler_core_op_handle_t core_op_handle) -{ - auto core_ops_scheduler = m_core_ops_scheduler.lock(); - CHECK(core_ops_scheduler, HAILO_INTERNAL_FAILURE); - - auto device_id = core_ops_scheduler->wait_for_read(core_op_handle, name(), get_timeout()); - if (HAILO_STREAM_ABORTED_BY_USER == device_id.status()) { - LOGGER__INFO("Read from stream was aborted."); - return device_id.status(); - } - CHECK_EXPECTED_AS_STATUS(device_id); - - TRACE(ReadFrameTrace, "", core_op_handle, m_stream_info.name); - auto status = m_streams[device_id.value()].get().read(buffer); - if (HAILO_SUCCESS != status) { - LOGGER__INFO("Read from stream has failed! status = {}", status); - return status; - } - - status = core_ops_scheduler->signal_read_finish(core_op_handle, name(), device_id.value()); - if (HAILO_STREAM_ABORTED_BY_USER == status) { - return status; - } - CHECK_SUCCESS(status); - - return HAILO_SUCCESS; -} - -Expected> OutputVDeviceBaseStream::create(std::vector> &&low_level_streams, - const LayerInfo &edge_layer, const scheduler_core_op_handle_t &core_op_handle, EventPtr core_op_activated_event, +Expected> VDeviceOutputStreamBase::create( + std::map> &&low_level_streams, + const hailo_stream_parameters_t &stream_params, const LayerInfo &edge_layer, + const scheduler_core_op_handle_t &core_op_handle, EventPtr core_op_activated_event, CoreOpsSchedulerWeakPtr core_ops_scheduler) { assert(0 < low_level_streams.size()); - auto status = HAILO_UNINITIALIZED; - - std::unique_ptr local_vdevice_stream; + if (core_ops_scheduler.lock()) { - local_vdevice_stream = make_unique_nothrow(std::move(low_level_streams), core_op_handle, - edge_layer, std::move(core_op_activated_event), core_ops_scheduler, status); + if ((stream_params.flags & HAILO_STREAM_FLAGS_ASYNC) != 0) { + LOGGER__ERROR("Async output streams are not supported with scheduler"); + return make_unexpected(HAILO_NOT_IMPLEMENTED); + } else { + auto stream = ScheduledOutputStream::create(std::move(low_level_streams), core_op_handle, + edge_layer, std::move(core_op_activated_event), core_ops_scheduler); + CHECK_EXPECTED(stream); + return std::unique_ptr(stream.release()); + } } else { - local_vdevice_stream = make_unique_nothrow(std::move(low_level_streams), edge_layer, - std::move(core_op_activated_event), status); + if ((stream_params.flags & HAILO_STREAM_FLAGS_ASYNC) != 0) { + auto stream = VDeviceNativeAsyncOutputStream::create(std::move(low_level_streams), + std::move(core_op_activated_event), edge_layer); + CHECK_EXPECTED(stream); + return std::unique_ptr(stream.release()); + } else { + auto stream = VDeviceNativeOutputStream::create(std::move(low_level_streams), + std::move(core_op_activated_event), edge_layer); + CHECK_EXPECTED(stream); + return std::unique_ptr(stream.release()); + } } - - CHECK_AS_EXPECTED((nullptr != local_vdevice_stream), HAILO_OUT_OF_HOST_MEMORY); - CHECK_SUCCESS_AS_EXPECTED(status); - - return local_vdevice_stream; } -hailo_status OutputVDeviceBaseStream::set_timeout(std::chrono::milliseconds timeout) +hailo_status VDeviceOutputStreamBase::set_timeout(std::chrono::milliseconds timeout) { - for (auto &stream : m_streams) { + for (const auto &pair : m_streams) { + auto &stream = pair.second; auto status = stream.get().set_timeout(timeout); CHECK_SUCCESS(status, "Failed to set timeout to output stream. (device: {})", stream.get().get_dev_id()); } return HAILO_SUCCESS; } -std::chrono::milliseconds OutputVDeviceBaseStream::get_timeout() const +std::chrono::milliseconds VDeviceOutputStreamBase::get_timeout() const { // All timeout values of m_streams should be the same - return m_streams[0].get().get_timeout(); + return m_streams.begin()->second.get().get_timeout(); } -hailo_stream_interface_t OutputVDeviceBaseStream::get_interface() const +hailo_stream_interface_t VDeviceOutputStreamBase::get_interface() const { // All interface values of m_streams should be the same - return m_streams[0].get().get_interface(); + return m_streams.begin()->second.get().get_interface(); } -hailo_status ScheduledOutputStream::abort() +Expected VDeviceOutputStreamBase::get_buffer_frames_size() const { - return abort_impl(m_core_op_handle); + return m_streams.begin()->second.get().get_buffer_frames_size(); } -hailo_status OutputVDeviceNativeStream::abort() -{ - auto status = HAILO_SUCCESS; // Best effort - for (auto &stream : m_streams) { - auto abort_status = stream.get().abort(); - if (HAILO_SUCCESS != status) { - LOGGER__ERROR("Failed to abort output stream. (status: {} device: {})", status, stream.get().get_dev_id()); - status = abort_status; - } - } - - return status; -} - -hailo_status ScheduledOutputStream::abort_impl(scheduler_core_op_handle_t core_op_handle) -{ - auto status = HAILO_SUCCESS; // Best effort - for (auto& stream : m_streams) { - auto abort_status = stream.get().abort(); - if (HAILO_SUCCESS != status) { - LOGGER__ERROR("Failed to abort output stream. (status: {} device: {})", status, stream.get().get_dev_id()); - status = abort_status; - } - } - - auto core_ops_scheduler = m_core_ops_scheduler.lock(); - CHECK(core_ops_scheduler, HAILO_INTERNAL_FAILURE); - - auto disable_status = core_ops_scheduler->disable_stream(core_op_handle, name()); - if (HAILO_SUCCESS != disable_status) { - LOGGER__ERROR("Failed to disable stream in the core-op scheduler. (status: {})", disable_status); - status = disable_status; - } - - return status; -} - -hailo_status ScheduledOutputStream::clear_abort() -{ - return clear_abort_impl(m_core_op_handle); -} - -hailo_status OutputVDeviceNativeStream::clear_abort() -{ - auto status = HAILO_SUCCESS; // Best effort - for (auto &stream : m_streams) { - auto clear_abort_status = stream.get().clear_abort(); - if ((HAILO_SUCCESS != clear_abort_status) && (HAILO_STREAM_NOT_ACTIVATED != clear_abort_status)) { - LOGGER__ERROR("Failed to clear abort output stream. (status: {} device: {})", clear_abort_status, stream.get().get_dev_id()); - status = clear_abort_status; - } - } - - return status; -} - -hailo_status ScheduledOutputStream::clear_abort_impl(scheduler_core_op_handle_t core_op_handle) -{ - auto status = HAILO_SUCCESS; // Best effort - for (auto& stream : m_streams) { - auto clear_abort_status = stream.get().clear_abort(); - if ((HAILO_SUCCESS != clear_abort_status) && (HAILO_STREAM_NOT_ACTIVATED != clear_abort_status)) { - LOGGER__ERROR("Failed to clear abort output stream. (status: {} device: {})", clear_abort_status, stream.get().get_dev_id()); - status = clear_abort_status; - } - } - - auto core_ops_scheduler = m_core_ops_scheduler.lock(); - CHECK(core_ops_scheduler, HAILO_INTERNAL_FAILURE); - - auto enable_status = core_ops_scheduler->enable_stream(core_op_handle, name()); - if (HAILO_SUCCESS != enable_status) { - LOGGER__ERROR("Failed to enable stream in the core-op scheduler. (status: {})", enable_status); - status = enable_status; - } - - return status; -} - -Expected OutputVDeviceBaseStream::get_buffer_frames_size() const -{ - size_t total_buffers_size = 0; - for (auto &stream : m_streams) { - auto stream_buffer_size = stream.get().get_buffer_frames_size(); - if (HAILO_NOT_AVAILABLE == stream_buffer_size.status()) { - return make_unexpected(HAILO_NOT_AVAILABLE); - } - CHECK_EXPECTED(stream_buffer_size); - total_buffers_size += stream_buffer_size.value(); - } - - return total_buffers_size; -} - -Expected OutputVDeviceBaseStream::get_pending_frames_count() const +Expected VDeviceOutputStreamBase::get_pending_frames_count() const { size_t total_pending_frames_count = 0; - for (auto &stream : m_streams) { + for (const auto &pair : m_streams) { + auto &stream = pair.second; auto stream_pending_frames_count = stream.get().get_pending_frames_count(); if (HAILO_NOT_AVAILABLE == stream_pending_frames_count.status()) { return make_unexpected(HAILO_NOT_AVAILABLE); } - CHECK_EXPECTED(stream_pending_frames_count); - total_pending_frames_count += stream_pending_frames_count.value(); + CHECK_EXPECTED(stream_pending_frames_count); + total_pending_frames_count += stream_pending_frames_count.value(); } - return total_pending_frames_count; } diff --git a/hailort/libhailort/src/vdevice/vdevice_stream.hpp b/hailort/libhailort/src/vdevice/vdevice_stream.hpp index e1aa294..c5cb88a 100644 --- a/hailort/libhailort/src/vdevice/vdevice_stream.hpp +++ b/hailort/libhailort/src/vdevice/vdevice_stream.hpp @@ -6,17 +6,25 @@ * @file vdevice_stream.hpp * @brief Internal stream implementation for VDevice * - * InputStream (External "interface") - * |-- InputStreamBase (Base class) - * |-- InputVDeviceBaseStream (Base class for vdevice streams) - * | |-- InputVDeviceNativeStream - * | |-- ScheduledInputStream + * InputStream (External "interface") + * |-- InputStreamBase (Base class) + * |-- VDeviceInputStreamBase (Base class for vdevice streams) + * | |-- VDeviceNativeInputStreamBase + * | | |-- VDeviceNativeInputStream (Sync api) + * | | |-- VDeviceNativeAsyncInputStream (Async api) + * | |-- ScheduledInputStreamBase + * | | |-- ScheduledInputStream (Sync api) + * | | |-- ScheduledAsyncInputStream (Async api) * - * OutputStream (External "interface") - * |-- OutputStreamBase (Base class) - * |-- OutputVDeviceBaseStream (Base class for vdevice streams) - * | |-- OutputVDeviceNativeStream - * | |-- ScheduledOutputStream + * OutputStream (External "interface") + * |-- OutputStreamBase (Base class) + * |-- VDeviceOutputStreamBase (Base class for vdevice streams) + * | |-- VDeviceNativeOutputStreamBase + * | | |-- VDeviceNativeOutputStream (Sync api) + * | | |-- VDeviceNativeAsyncOutputStream (Async api) + * | |-- ScheduledOutputStreamBase + * | | |-- ScheduledOutputStream (Sync api) + * | | |-- ScheduledAsyncOutputStream (Async api) **/ #ifndef HAILO_VDEVICE_STREAM_HPP_ @@ -34,14 +42,16 @@ namespace hailort { -class InputVDeviceBaseStream : public InputStreamBase { +class VDeviceInputStreamBase : public InputStreamBase { public: - static Expected> create(std::vector> &&low_level_streams, - const LayerInfo &edge_layer, const scheduler_core_op_handle_t &core_op_handle, - EventPtr core_op_activated_event, CoreOpsSchedulerWeakPtr core_ops_scheduler); + static Expected> create( + std::map> &&low_level_streams, + const hailo_stream_parameters_t &stream_params, const LayerInfo &edge_layer, + const scheduler_core_op_handle_t &core_op_handle, EventPtr core_op_activated_event, + CoreOpsSchedulerWeakPtr core_ops_scheduler); - virtual ~InputVDeviceBaseStream(); + virtual ~VDeviceInputStreamBase(); virtual hailo_status activate_stream(uint16_t dynamic_batch_size, bool resume_pending_stream_transfers) override; virtual hailo_status deactivate_stream() override; @@ -49,21 +59,13 @@ public: virtual std::chrono::milliseconds get_timeout() const override; virtual hailo_status set_timeout(std::chrono::milliseconds timeout) override; - virtual hailo_status send_pending_buffer(size_t device_index = 0) override; + virtual hailo_status send_pending_buffer(const device_id_t &device_id) override; virtual Expected get_buffer_frames_size() const override; virtual Expected get_pending_frames_count() const override; virtual bool is_scheduled() override = 0; virtual hailo_status abort() override = 0; virtual hailo_status clear_abort() override = 0; - - virtual hailo_status register_interrupt_callback(const vdma::ProcessingCompleteCallback &callback) override - { - for (auto &stream : m_streams) { - auto status = stream.get().register_interrupt_callback(callback); - CHECK_SUCCESS(status); - } - return HAILO_SUCCESS; - } + virtual hailo_status flush() override; virtual void notify_all() { @@ -72,43 +74,39 @@ public: } protected: - virtual hailo_status sync_write_all_raw_buffer_no_transform_impl(void *buffer, size_t offset, size_t size) override; - virtual Expected sync_write_raw_buffer(const MemoryView &buffer) override - { - return sync_write_raw_buffer(buffer, []() { return false; }); - } - virtual Expected sync_write_raw_buffer(const MemoryView &buffer, const std::function &should_cancel) = 0; + virtual hailo_status write_impl(const MemoryView &buffer) final override; + virtual hailo_status write_impl(const MemoryView &buffer, const std::function &should_cancel) = 0; - explicit InputVDeviceBaseStream( - std::vector> &&streams, + VDeviceInputStreamBase( + std::map> &&streams, EventPtr &&core_op_activated_event, const LayerInfo &layer_info, hailo_status &status) : - InputStreamBase(layer_info, streams[0].get().get_interface(), std::move(core_op_activated_event), status), + InputStreamBase(layer_info, streams.begin()->second.get().get_interface(), std::move(core_op_activated_event), status), m_streams(std::move(streams)), m_is_stream_activated(false), - m_next_transfer_stream_index(0), + m_next_transfer_stream(m_streams.begin()->first), m_acc_frames(0) {} - std::vector> m_streams; + std::map> m_streams; bool m_is_stream_activated; - uint32_t m_next_transfer_stream_index; + device_id_t m_next_transfer_stream; uint32_t m_acc_frames; private: friend class VDeviceInputStreamMultiplexerWrapper; - - virtual hailo_status flush() override; }; -class OutputVDeviceBaseStream : public OutputStreamBase { +class VDeviceOutputStreamBase : public OutputStreamBase { public: - virtual ~OutputVDeviceBaseStream(); + virtual ~VDeviceOutputStreamBase(); - static Expected> create(std::vector> &&low_level_streams, - const LayerInfo &edge_layer, const scheduler_core_op_handle_t &core_op_handle, - EventPtr core_op_activated_event, CoreOpsSchedulerWeakPtr core_ops_scheduler); + static Expected> create( + std::map> &&low_level_streams, + const hailo_stream_parameters_t &stream_params, const LayerInfo &edge_layer, + const scheduler_core_op_handle_t &core_op_handle, EventPtr core_op_activated_event, + CoreOpsSchedulerWeakPtr core_ops_scheduler); virtual hailo_status activate_stream(uint16_t dynamic_batch_size, bool resume_pending_stream_transfers) override; virtual hailo_status deactivate_stream() override; @@ -116,40 +114,30 @@ public: virtual std::chrono::milliseconds get_timeout() const override; virtual hailo_status set_timeout(std::chrono::milliseconds timeout) override; virtual Expected get_buffer_frames_size() const override; - virtual Expected get_pending_frames_count() const override; + virtual Expected get_pending_frames_count() const override; // Returns the accumulated pending frames virtual hailo_status abort() override = 0; virtual hailo_status clear_abort() override = 0; virtual bool is_scheduled() override = 0; - virtual hailo_status register_interrupt_callback(const vdma::ProcessingCompleteCallback &callback) override - { - for (auto &stream : m_streams) { - auto status = stream.get().register_interrupt_callback(callback); - CHECK_SUCCESS(status); - } - return HAILO_SUCCESS; - } - protected: - virtual Expected sync_read_raw_buffer(MemoryView &buffer) override; - - explicit OutputVDeviceBaseStream( - std::vector> &&streams, + VDeviceOutputStreamBase( + std::map> &&streams, const LayerInfo &layer_info, EventPtr &&core_op_activated_event, hailo_status &status) : - OutputStreamBase(layer_info, std::move(core_op_activated_event), status), + OutputStreamBase(layer_info, streams.begin()->second.get().get_interface(), + std::move(core_op_activated_event), status), m_streams(std::move(streams)), m_is_stream_activated(false), - m_next_transfer_stream_index(0), + m_next_transfer_stream(m_streams.begin()->first), m_acc_frames(0) {} - virtual hailo_status read_all(MemoryView &buffer) override; + virtual hailo_status read_impl(MemoryView &buffer) override final; - std::vector> m_streams; + std::map> m_streams; bool m_is_stream_activated; - uint32_t m_next_transfer_stream_index; + device_id_t m_next_transfer_stream; uint32_t m_acc_frames; private: diff --git a/hailort/libhailort/src/vdevice/vdevice_stream_multiplexer_wrapper.cpp b/hailort/libhailort/src/vdevice/vdevice_stream_multiplexer_wrapper.cpp index b9d9b00..1b7b0a1 100644 --- a/hailort/libhailort/src/vdevice/vdevice_stream_multiplexer_wrapper.cpp +++ b/hailort/libhailort/src/vdevice/vdevice_stream_multiplexer_wrapper.cpp @@ -40,19 +40,12 @@ hailo_status VDeviceInputStreamMultiplexerWrapper::abort() } *m_is_aborted = true; - if (is_scheduled()) { - auto status = m_multiplexer->disable_stream(m_core_op_multiplexer_handle, name()); - CHECK_SUCCESS(status); - - m_vdevice_input_stream->notify_all(); - - status = m_multiplexer->run_once_for_stream(name(), INPUT_RUN_ONCE_HANDLE__ABORT, m_core_op_multiplexer_handle); - CHECK_SUCCESS(status); + auto status = m_multiplexer->disable_stream(m_core_op_multiplexer_handle, name()); + CHECK_SUCCESS(status); - return HAILO_SUCCESS; - } + m_vdevice_input_stream->notify_all(); - auto status = m_vdevice_input_stream->abort(); + status = m_multiplexer->run_once_for_stream(name(), INPUT_RUN_ONCE_HANDLE__ABORT, m_core_op_multiplexer_handle); CHECK_SUCCESS(status); return HAILO_SUCCESS; @@ -65,32 +58,27 @@ hailo_status VDeviceInputStreamMultiplexerWrapper::clear_abort() } *m_is_aborted = false; - if (is_scheduled()) { - auto status = m_multiplexer->enable_stream(m_core_op_multiplexer_handle, name()); - CHECK_SUCCESS(status); - - status = m_multiplexer->run_once_for_stream(name(), INPUT_RUN_ONCE_HANDLE__CLEAR_ABORT, m_core_op_multiplexer_handle); - CHECK_SUCCESS(status); - - m_vdevice_input_stream->notify_all(); - - return HAILO_SUCCESS; - } + auto status = m_multiplexer->enable_stream(m_core_op_multiplexer_handle, name()); + CHECK_SUCCESS(status); - auto status = m_vdevice_input_stream->clear_abort(); + status = m_multiplexer->run_once_for_stream(name(), INPUT_RUN_ONCE_HANDLE__CLEAR_ABORT, m_core_op_multiplexer_handle); CHECK_SUCCESS(status); + m_vdevice_input_stream->notify_all(); + return HAILO_SUCCESS; } bool VDeviceInputStreamMultiplexerWrapper::is_scheduled() { - return m_vdevice_input_stream->is_scheduled(); + // Multiplexer can only work with scheduler + assert(m_vdevice_input_stream->is_scheduled()); + return true; } -hailo_status VDeviceInputStreamMultiplexerWrapper::send_pending_buffer(size_t device_index) +hailo_status VDeviceInputStreamMultiplexerWrapper::send_pending_buffer(const device_id_t &device_id) { - return m_vdevice_input_stream->send_pending_buffer(device_index); + return m_vdevice_input_stream->send_pending_buffer(device_id); } Expected VDeviceInputStreamMultiplexerWrapper::get_buffer_frames_size() const @@ -103,34 +91,23 @@ Expected VDeviceInputStreamMultiplexerWrapper::get_pending_frames_count( return m_vdevice_input_stream->get_pending_frames_count(); } -Expected VDeviceInputStreamMultiplexerWrapper::sync_write_raw_buffer(const MemoryView &buffer) +hailo_status VDeviceInputStreamMultiplexerWrapper::write_impl(const MemoryView &buffer) { - if (is_scheduled()) { - auto status = m_multiplexer->wait_for_write(m_core_op_multiplexer_handle); - if (HAILO_STREAM_ABORTED_BY_USER == status) { - return make_unexpected(status); - } - CHECK_SUCCESS_AS_EXPECTED(status); + auto status = m_multiplexer->wait_for_write(m_core_op_multiplexer_handle); + if (HAILO_STREAM_ABORTED_BY_USER == status) { + return status; } + CHECK_SUCCESS(status); - auto exp = m_vdevice_input_stream->sync_write_raw_buffer(buffer, [this]() { return m_is_aborted->load(); }); - if (is_scheduled()) { - auto status = m_multiplexer->signal_write_finish(m_core_op_multiplexer_handle, exp.status() != HAILO_SUCCESS); - CHECK_SUCCESS_AS_EXPECTED(status); - } - if (HAILO_STREAM_ABORTED_BY_USER == exp.status()) { - return make_unexpected(exp.status()); + auto write_status = m_vdevice_input_stream->write_impl(buffer, [this]() { return m_is_aborted->load(); }); + status = m_multiplexer->signal_write_finish(m_core_op_multiplexer_handle, write_status != HAILO_SUCCESS); + CHECK_SUCCESS(status); + if (HAILO_STREAM_ABORTED_BY_USER == write_status) { + return write_status; } - CHECK_EXPECTED(exp); - - return exp; -} - -hailo_status VDeviceInputStreamMultiplexerWrapper::sync_write_all_raw_buffer_no_transform_impl(void *buffer, size_t offset, size_t size) -{ - ASSERT(NULL != buffer); + CHECK_SUCCESS(write_status); - return sync_write_raw_buffer(MemoryView(static_cast(buffer) + offset, size)).status(); + return HAILO_SUCCESS; } hailo_status VDeviceInputStreamMultiplexerWrapper::set_timeout(std::chrono::milliseconds timeout) @@ -140,20 +117,14 @@ hailo_status VDeviceInputStreamMultiplexerWrapper::set_timeout(std::chrono::mill hailo_status VDeviceInputStreamMultiplexerWrapper::flush() { - if (is_scheduled()) { - auto status = m_multiplexer->run_once_for_stream(name(), INPUT_RUN_ONCE_HANDLE__FLUSH, m_core_op_multiplexer_handle); - CHECK_SUCCESS(status); - - return HAILO_SUCCESS; - } - - return m_vdevice_input_stream->flush(); + return m_multiplexer->run_once_for_stream(name(), INPUT_RUN_ONCE_HANDLE__FLUSH, m_core_op_multiplexer_handle); } -Expected> VDeviceInputStreamMultiplexerWrapper::create(std::shared_ptr vdevice_input_stream, +Expected> VDeviceInputStreamMultiplexerWrapper::create(std::shared_ptr vdevice_input_stream, std::string network_name, std::shared_ptr multiplexer, scheduler_core_op_handle_t core_ops_scheduler_handle, multiplexer_core_op_handle_t core_op_multiplexer_handle) { + assert(vdevice_input_stream->is_scheduled()); hailo_status status = HAILO_UNINITIALIZED; std::unique_ptr wrapper(new (std::nothrow) VDeviceInputStreamMultiplexerWrapper(vdevice_input_stream, network_name, multiplexer, core_ops_scheduler_handle, core_op_multiplexer_handle, status)); @@ -171,7 +142,7 @@ Expected> VDeviceInputStre return wrapper; } -VDeviceInputStreamMultiplexerWrapper::VDeviceInputStreamMultiplexerWrapper(std::shared_ptr &vdevice_input_stream, +VDeviceInputStreamMultiplexerWrapper::VDeviceInputStreamMultiplexerWrapper(std::shared_ptr &vdevice_input_stream, std::string network_name, std::shared_ptr multiplexer, scheduler_core_op_handle_t core_ops_scheduler_handle, multiplexer_core_op_handle_t core_op_multiplexer_handle, hailo_status &status) : InputStreamBase(vdevice_input_stream->get_info(), @@ -247,6 +218,11 @@ std::chrono::milliseconds VDeviceOutputStreamMultiplexerWrapper::get_timeout() c return m_vdevice_output_stream->get_timeout(); } +hailo_status VDeviceOutputStreamMultiplexerWrapper::set_next_device_to_read(const device_id_t &device_id) +{ + return m_vdevice_output_stream->set_next_device_to_read(device_id); +} + hailo_status VDeviceOutputStreamMultiplexerWrapper::abort() { if (*m_is_aborted) { @@ -254,17 +230,10 @@ hailo_status VDeviceOutputStreamMultiplexerWrapper::abort() } *m_is_aborted = true; - if (is_scheduled()) { - auto status = m_multiplexer->disable_stream(m_core_op_multiplexer_handle, name()); - CHECK_SUCCESS(status); - - status = m_multiplexer->run_once_for_stream(name(), OUTPUT_RUN_ONCE_HANDLE__ABORT, m_core_op_multiplexer_handle); - CHECK_SUCCESS(status); - - return HAILO_SUCCESS; - } + auto status = m_multiplexer->disable_stream(m_core_op_multiplexer_handle, name()); + CHECK_SUCCESS(status); - auto status = m_vdevice_output_stream->abort(); + status = m_multiplexer->run_once_for_stream(name(), OUTPUT_RUN_ONCE_HANDLE__ABORT, m_core_op_multiplexer_handle); CHECK_SUCCESS(status); return HAILO_SUCCESS; @@ -277,17 +246,10 @@ hailo_status VDeviceOutputStreamMultiplexerWrapper::clear_abort() } *m_is_aborted = false; - if (is_scheduled()) { - auto status = m_multiplexer->enable_stream(m_core_op_multiplexer_handle, name()); - CHECK_SUCCESS(status); - - status = m_multiplexer->run_once_for_stream(name(), OUTPUT_RUN_ONCE_HANDLE__CLEAR_ABORT, m_core_op_multiplexer_handle); - CHECK_SUCCESS(status); - - return HAILO_SUCCESS; - } + auto status = m_multiplexer->enable_stream(m_core_op_multiplexer_handle, name()); + CHECK_SUCCESS(status); - auto status = m_vdevice_output_stream->clear_abort(); + status = m_multiplexer->run_once_for_stream(name(), OUTPUT_RUN_ONCE_HANDLE__CLEAR_ABORT, m_core_op_multiplexer_handle); CHECK_SUCCESS(status); return HAILO_SUCCESS; @@ -295,7 +257,9 @@ hailo_status VDeviceOutputStreamMultiplexerWrapper::clear_abort() bool VDeviceOutputStreamMultiplexerWrapper::is_scheduled() { - return m_vdevice_output_stream->is_scheduled(); + // Multiplexer can only work with scheduler + assert(m_vdevice_output_stream->is_scheduled()); + return true; } Expected VDeviceOutputStreamMultiplexerWrapper::get_buffer_frames_size() const @@ -307,29 +271,22 @@ Expected VDeviceOutputStreamMultiplexerWrapper::get_pending_frames_count return m_vdevice_output_stream->get_pending_frames_count(); } -Expected VDeviceOutputStreamMultiplexerWrapper::sync_read_raw_buffer(MemoryView &buffer) -{ - return m_vdevice_output_stream->sync_read_raw_buffer(buffer); -} - -hailo_status VDeviceOutputStreamMultiplexerWrapper::read_all(MemoryView &buffer) +hailo_status VDeviceOutputStreamMultiplexerWrapper::read_impl(MemoryView &buffer) { - return m_vdevice_output_stream->read_all(buffer); + return m_vdevice_output_stream->read_impl(buffer); } hailo_status VDeviceOutputStreamMultiplexerWrapper::read(MemoryView buffer) { uint32_t frames_to_drain_count = 0; - if (is_scheduled()) { - auto expected_drain_count = m_multiplexer->wait_for_read(m_core_op_multiplexer_handle, name(), - m_vdevice_output_stream->get_timeout()); - if (HAILO_STREAM_ABORTED_BY_USER == expected_drain_count.status()) { - return expected_drain_count.status(); - } - CHECK_EXPECTED_AS_STATUS(expected_drain_count); - - frames_to_drain_count = expected_drain_count.release(); + auto expected_drain_count = m_multiplexer->wait_for_read(m_core_op_multiplexer_handle, name(), + m_vdevice_output_stream->get_timeout()); + if (HAILO_STREAM_ABORTED_BY_USER == expected_drain_count.status()) { + return expected_drain_count.status(); } + CHECK_EXPECTED_AS_STATUS(expected_drain_count); + + frames_to_drain_count = expected_drain_count.release(); for (uint32_t i = 0; i < frames_to_drain_count; i++) { auto status = m_vdevice_output_stream->read(buffer); @@ -345,10 +302,8 @@ hailo_status VDeviceOutputStreamMultiplexerWrapper::read(MemoryView buffer) } CHECK_SUCCESS(status); - if (is_scheduled()) { - status = m_multiplexer->signal_read_finish(); - CHECK_SUCCESS(status); - } + status = m_multiplexer->signal_read_finish(); + CHECK_SUCCESS(status); return HAILO_SUCCESS; } @@ -358,10 +313,11 @@ hailo_status VDeviceOutputStreamMultiplexerWrapper::set_timeout(std::chrono::mil return m_vdevice_output_stream->set_timeout(timeout); } -Expected> VDeviceOutputStreamMultiplexerWrapper::create(std::shared_ptr vdevice_output_stream, +Expected> VDeviceOutputStreamMultiplexerWrapper::create(std::shared_ptr vdevice_output_stream, std::string network_name, std::shared_ptr multiplexer, scheduler_core_op_handle_t core_ops_scheduler_handle, multiplexer_core_op_handle_t core_op_multiplexer_handle) { + assert(vdevice_output_stream->is_scheduled()); hailo_status status = HAILO_UNINITIALIZED; std::unique_ptr wrapper(new (std::nothrow) VDeviceOutputStreamMultiplexerWrapper(vdevice_output_stream, network_name, multiplexer, core_ops_scheduler_handle, core_op_multiplexer_handle, status)); @@ -378,7 +334,7 @@ Expected> VDeviceOutputSt return wrapper; } -VDeviceOutputStreamMultiplexerWrapper::VDeviceOutputStreamMultiplexerWrapper(std::shared_ptr &vdevice_output_stream, +VDeviceOutputStreamMultiplexerWrapper::VDeviceOutputStreamMultiplexerWrapper(std::shared_ptr &vdevice_output_stream, std::string network_name, std::shared_ptr multiplexer, scheduler_core_op_handle_t core_ops_scheduler_handle, multiplexer_core_op_handle_t core_op_multiplexer_handle, hailo_status &status) : OutputStreamBase(vdevice_output_stream->get_layer_info(), vdevice_output_stream->get_info(), diff --git a/hailort/libhailort/src/vdevice/vdevice_stream_multiplexer_wrapper.hpp b/hailort/libhailort/src/vdevice/vdevice_stream_multiplexer_wrapper.hpp index 0876d29..92e054b 100644 --- a/hailort/libhailort/src/vdevice/vdevice_stream_multiplexer_wrapper.hpp +++ b/hailort/libhailort/src/vdevice/vdevice_stream_multiplexer_wrapper.hpp @@ -34,7 +34,7 @@ enum output_run_once_handle_t { class VDeviceInputStreamMultiplexerWrapper : public InputStreamBase { public: virtual ~VDeviceInputStreamMultiplexerWrapper() = default; - static Expected> create(std::shared_ptr vdevice_input_stream, + static Expected> create(std::shared_ptr vdevice_input_stream, std::string network_name, std::shared_ptr multiplexer, scheduler_core_op_handle_t core_ops_scheduler_handle, multiplexer_core_op_handle_t core_op_multiplexer_handle = 0); Expected> clone(multiplexer_core_op_handle_t core_op_multiplexer_handle); @@ -49,28 +49,22 @@ public: virtual hailo_status clear_abort() override; virtual bool is_scheduled() override; - virtual hailo_status send_pending_buffer(size_t device_index = 0) override; + virtual hailo_status send_pending_buffer(const device_id_t &device_id) override; virtual Expected get_buffer_frames_size() const override; virtual Expected get_pending_frames_count() const override; - virtual hailo_status register_interrupt_callback(const vdma::ProcessingCompleteCallback &callback) override - { - return m_vdevice_input_stream->register_interrupt_callback(callback); - } - protected: - virtual Expected sync_write_raw_buffer(const MemoryView &buffer) override; - virtual hailo_status sync_write_all_raw_buffer_no_transform_impl(void *buffer, size_t offset, size_t size) override; + virtual hailo_status write_impl(const MemoryView &buffer) override; private: - VDeviceInputStreamMultiplexerWrapper(std::shared_ptr &vdevice_input_stream, + VDeviceInputStreamMultiplexerWrapper(std::shared_ptr &vdevice_input_stream, std::string network_name, std::shared_ptr multiplexer, scheduler_core_op_handle_t core_ops_scheduler_handle, multiplexer_core_op_handle_t core_op_multiplexer_handle, hailo_status &status); virtual hailo_status set_timeout(std::chrono::milliseconds timeout) override; virtual hailo_status flush() override; - - std::shared_ptr m_vdevice_input_stream; + + std::shared_ptr m_vdevice_input_stream; std::shared_ptr m_multiplexer; scheduler_core_op_handle_t m_core_ops_scheduler_handle; multiplexer_core_op_handle_t m_core_op_multiplexer_handle; @@ -83,7 +77,7 @@ class VDeviceOutputStreamMultiplexerWrapper : public OutputStreamBase { public: virtual ~VDeviceOutputStreamMultiplexerWrapper() noexcept = default; - static Expected> create(std::shared_ptr vdevice_output_stream, + static Expected> create(std::shared_ptr vdevice_output_stream, std::string network_name, std::shared_ptr multiplexer, scheduler_core_op_handle_t core_ops_scheduler_handle, multiplexer_core_op_handle_t core_op_multiplexer_handle = 0); Expected> clone(multiplexer_core_op_handle_t core_op_multiplexer_handle); @@ -94,30 +88,23 @@ public: virtual hailo_status deactivate_stream() override; virtual hailo_stream_interface_t get_interface() const override; virtual std::chrono::milliseconds get_timeout() const override; + virtual hailo_status set_next_device_to_read(const device_id_t &device_id) override; virtual hailo_status abort() override; virtual hailo_status clear_abort() override; virtual bool is_scheduled() override; virtual Expected get_buffer_frames_size() const override; virtual Expected get_pending_frames_count() const override; - virtual hailo_status register_interrupt_callback(const vdma::ProcessingCompleteCallback &callback) override - { - return m_vdevice_output_stream->register_interrupt_callback(callback); - } - -protected: - virtual Expected sync_read_raw_buffer(MemoryView &buffer) override; - private: - VDeviceOutputStreamMultiplexerWrapper(std::shared_ptr &vdevice_output_stream, + VDeviceOutputStreamMultiplexerWrapper(std::shared_ptr &vdevice_output_stream, std::string network_name, std::shared_ptr multiplexer, scheduler_core_op_handle_t core_ops_scheduler_handle, multiplexer_core_op_handle_t core_op_multiplexer_handle, hailo_status &status); virtual hailo_status set_timeout(std::chrono::milliseconds timeout) override; - virtual hailo_status read_all(MemoryView &buffer) override; + virtual hailo_status read_impl(MemoryView &buffer) override; virtual hailo_status read(MemoryView buffer) override; - std::shared_ptr m_vdevice_output_stream; + std::shared_ptr m_vdevice_output_stream; std::shared_ptr m_multiplexer; scheduler_core_op_handle_t m_core_ops_scheduler_handle; multiplexer_core_op_handle_t m_core_op_multiplexer_handle; diff --git a/hailort/libhailort/src/vdma/CMakeLists.txt b/hailort/libhailort/src/vdma/CMakeLists.txt index 4111464..2964998 100644 --- a/hailort/libhailort/src/vdma/CMakeLists.txt +++ b/hailort/libhailort/src/vdma/CMakeLists.txt @@ -21,11 +21,11 @@ set(SRC_FILES ${CMAKE_CURRENT_SOURCE_DIR}/memory/descriptor_list.cpp ${CMAKE_CURRENT_SOURCE_DIR}/memory/vdma_buffer.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/memory/dma_mapped_buffer.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/memory/mapped_buffer_impl.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/memory/mapped_buffer_factory.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/memory/mapped_buffer.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/memory/dma_able_buffer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/memory/sg_buffer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/memory/continuous_buffer.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/memory/buffer_requirements.cpp ) set(HAILORT_CPP_SOURCES ${HAILORT_CPP_SOURCES} ${SRC_FILES} PARENT_SCOPE) diff --git a/hailort/libhailort/src/vdma/channel/async_channel.cpp b/hailort/libhailort/src/vdma/channel/async_channel.cpp index 890390e..d104b39 100644 --- a/hailort/libhailort/src/vdma/channel/async_channel.cpp +++ b/hailort/libhailort/src/vdma/channel/async_channel.cpp @@ -44,34 +44,60 @@ AsyncChannel::AsyncChannel(vdma::ChannelId channel_id, Direction direction, Hail status = HAILO_SUCCESS; } -hailo_status AsyncChannel::transfer(std::shared_ptr buffer, const TransferDoneCallback &user_callback, void *opaque) -{ - CHECK_NOT_NULL(buffer, HAILO_INVALID_ARGUMENT); - CHECK(0 != buffer->size(), HAILO_INVALID_ARGUMENT); +hailo_status AsyncChannel::transfer_async(TransferRequest &&transfer_request) +{ + CHECK_ARG_NOT_NULL(transfer_request.buffer.data()); + CHECK(0 != transfer_request.buffer.size(), HAILO_INVALID_ARGUMENT, "Buffer is empty (size 0)"); + + auto is_new_mapping = true; + MappedBufferPtr mapped_buffer = nullptr; + if (transfer_request.mapped_buffer != nullptr) { + assert(transfer_request.buffer.data() == transfer_request.mapped_buffer->data()); + assert(transfer_request.buffer.size() == transfer_request.mapped_buffer->size()); + CHECK(transfer_request.mapped_buffer->storage().type() == BufferStorage::Type::DMA, HAILO_INVALID_ARGUMENT, + "Buffer must be dma-able (provided buffer type {})", transfer_request.mapped_buffer->storage().type()); + + // Map if not already mapped + const auto mapping_direction = (m_direction == Direction::H2D) ? HAILO_DMA_BUFFER_DIRECTION_H2D : HAILO_DMA_BUFFER_DIRECTION_D2H; + auto is_new_mapping_exp = transfer_request.mapped_buffer->storage().dma_map(m_driver, mapping_direction); + CHECK_EXPECTED_AS_STATUS(is_new_mapping_exp); + is_new_mapping = is_new_mapping_exp.release(); + + auto mapped_buffer_exp = transfer_request.mapped_buffer->storage().get_dma_mapped_buffer(m_driver.device_id()); + CHECK_EXPECTED_AS_STATUS(mapped_buffer_exp); + mapped_buffer = mapped_buffer_exp.release(); + } else { + auto mapped_buffer_exp = MappedBuffer::create_shared(m_driver, m_direction, + transfer_request.buffer.size(), transfer_request.buffer.data()); + CHECK_EXPECTED_AS_STATUS(mapped_buffer_exp); + mapped_buffer = mapped_buffer_exp.release(); + } + + if (!is_new_mapping) { + // The buffer has been previously mapped, so it needs to be sync'd from host to device. + // * If the buffer is mapped H2D/BOTH, then synchronize will make sure the device "sees" the most "up to date" + // version of the buffer. + // * If the buffer is mapped D2H, it might have been changed by the host between the time it was mapped and the + // current async transfer. Synchronizing will transfer ownership to the device, so that when the transfer is + // complete, the host will "see" an "up to date" version of the buffer. + auto status = mapped_buffer->synchronize(HailoRTDriver::DmaSyncDirection::TO_DEVICE); + CHECK_SUCCESS(status); + } std::lock_guard state_guard(m_state->mutex()); + if (!m_state->m_is_channel_activated) { + return HAILO_STREAM_NOT_ACTIVATED; + } if (m_state->m_is_aborted) { LOGGER__INFO("Tried to write to aborted channel {}", m_channel_id); return HAILO_STREAM_ABORTED_BY_USER; } - hailo_status status = HAILO_UNINITIALIZED; if (Direction::H2D == m_direction) { - status = transfer_h2d(buffer, user_callback, opaque); + return transfer_h2d(mapped_buffer, transfer_request.callback); } else { - status = transfer_d2h(buffer, user_callback, opaque); + return transfer_d2h(mapped_buffer, transfer_request.callback); } - - if (HAILO_STREAM_NOT_ACTIVATED == status) { - LOGGER__INFO("Transfer failed because Channel {} is not activated", m_channel_id); - return HAILO_STREAM_NOT_ACTIVATED; - } - if (HAILO_SUCCESS != status) { - LOGGER__ERROR("Transfer failed for channel {} with status {}", m_channel_id, status); - return status; - } - - return HAILO_SUCCESS; } hailo_status AsyncChannel::cancel_pending_transfers() @@ -79,10 +105,8 @@ hailo_status AsyncChannel::cancel_pending_transfers() std::lock_guard state_guard(m_state->mutex()); for (auto &pending_buffer_info : m_state->m_pending_buffers) { if (pending_buffer_info.on_transfer_done) { - pending_buffer_info.on_transfer_done(pending_buffer_info.buffer, - hailo_async_transfer_completion_info_t{HAILO_STREAM_NOT_ACTIVATED}, - pending_buffer_info.opaque); - // Release our references to user buffer, callback and opaque + pending_buffer_info.on_transfer_done(HAILO_STREAM_ABORTED_BY_USER); + // Release our references to user buffer and callback. pending_buffer_info = PendingBuffer{}; } else { LOGGER__WARNING("No transfer done callback found for transfer (channel {}); skipping", m_channel_id); @@ -105,7 +129,7 @@ hailo_status AsyncChannel::complete_channel_deactivation() return HAILO_SUCCESS; } -hailo_status AsyncChannel::transfer(void */* buf */, size_t /* count */) +hailo_status AsyncChannel::transfer_sync(void */* buf */, size_t /* count */, std::chrono::milliseconds /* timeout */) { return HAILO_NOT_IMPLEMENTED; } @@ -139,14 +163,18 @@ Expected AsyncChannel::get_d2h_pending_descs_count() return make_unexpected(HAILO_NOT_IMPLEMENTED); } -hailo_status AsyncChannel::transfer_d2h(std::shared_ptr buffer, const TransferDoneCallback &user_callback, void *opaque) +hailo_status AsyncChannel::transfer_d2h(MappedBufferPtr mapped_buffer, const InternalTransferDoneCallback &callback) { InterruptsDomain first_desc_interrupts_domain = InterruptsDomain::NONE; // Provide FW interrupt only in the end of the last transfer in the batch - InterruptsDomain last_desc_interrupts_domain = (m_state->m_accumulated_transfers + 1 == m_transfers_per_axi_intr) ? + InterruptsDomain last_desc_interrupts_domain = (m_state->m_accumulated_transfers + 1 == m_transfers_per_axi_intr) ? InterruptsDomain::BOTH : InterruptsDomain::HOST; - const auto status = prepare_descriptors(buffer, user_callback, opaque, first_desc_interrupts_domain, last_desc_interrupts_domain); + const auto status = prepare_descriptors(mapped_buffer, callback, first_desc_interrupts_domain, + last_desc_interrupts_domain); + if (HAILO_QUEUE_IS_FULL == status) { + return status; + } CHECK_SUCCESS(status); m_state->m_accumulated_transfers = (m_state->m_accumulated_transfers + 1) % m_transfers_per_axi_intr; @@ -154,7 +182,7 @@ hailo_status AsyncChannel::transfer_d2h(std::shared_ptr buffer, return HAILO_SUCCESS; } -hailo_status AsyncChannel::transfer_h2d(std::shared_ptr buffer, const TransferDoneCallback &user_callback, void *opaque) +hailo_status AsyncChannel::transfer_h2d(MappedBufferPtr mapped_buffer, const InternalTransferDoneCallback &callback) { // For h2d, only the host need to get transfer done interrupts InterruptsDomain last_desc_interrupts_domain = InterruptsDomain::HOST; @@ -162,46 +190,64 @@ hailo_status AsyncChannel::transfer_h2d(std::shared_ptr buffer, InterruptsDomain first_desc_interrupts_domain = (m_latency_meter != nullptr) ? InterruptsDomain::HOST : InterruptsDomain::NONE; - return prepare_descriptors(buffer, user_callback, opaque, first_desc_interrupts_domain, last_desc_interrupts_domain); + return prepare_descriptors(mapped_buffer, callback, first_desc_interrupts_domain, + last_desc_interrupts_domain); } -hailo_status AsyncChannel::prepare_descriptors(std::shared_ptr buffer, const TransferDoneCallback &user_callback, - void *opaque, InterruptsDomain first_desc_interrupts_domain, InterruptsDomain last_desc_interrupts_domain) +hailo_status AsyncChannel::prepare_descriptors(MappedBufferPtr mapped_buffer, + const InternalTransferDoneCallback &callback, InterruptsDomain first_desc_interrupts_domain, + InterruptsDomain last_desc_interrupts_domain) { - const auto desired_desc_num = m_desc_list->descriptors_in_buffer(buffer->size()); + assert(mapped_buffer != nullptr); + + const auto desired_desc_num = m_desc_list->descriptors_in_buffer(mapped_buffer->size()); CHECK(desired_desc_num <= MAX_DESCS_COUNT, HAILO_INTERNAL_FAILURE); const uint16_t desc_num = static_cast(desired_desc_num); - int num_available = get_num_available(); - int num_processed = CB_TAIL(m_state->m_descs); - int num_free = CB_AVAIL(m_state->m_descs, num_available, num_processed); + const auto num_available = get_num_available(); + const auto num_processed = CB_TAIL(m_state->m_descs); + const auto num_free = CB_AVAIL(m_state->m_descs, num_available, num_processed); if (num_free < desc_num) { - // TODO: do we want to block here? - return HAILO_OUT_OF_DESCRIPTORS; + return HAILO_QUEUE_IS_FULL; } - const auto status = m_desc_list->configure_to_use_buffer(*buffer, m_channel_id, num_available); + const auto status = m_desc_list->configure_to_use_buffer(*mapped_buffer, m_channel_id, num_available); CHECK_SUCCESS(status); + if (nullptr != m_latency_meter) { // Program first descriptor m_desc_list->program_single_descriptor((*m_desc_list)[num_available], m_desc_list->desc_page_size(), first_desc_interrupts_domain); } - auto actual_desc_count = m_desc_list->program_last_descriptor(buffer->size(), last_desc_interrupts_domain, - num_available, true); + auto actual_desc_count = m_desc_list->program_last_descriptor(mapped_buffer->size(), last_desc_interrupts_domain, + num_available); CHECK_EXPECTED_AS_STATUS(actual_desc_count, "Failed to program desc_list for channel {}", m_channel_id); assert (actual_desc_count.value() == desc_num); - int last_desc_avail = ((num_available + desc_num - 1) & m_state->m_descs.size_mask); + assert(desc_num > 0); + const auto last_desc_avail = static_cast((num_available + desc_num - 1) & m_state->m_descs.size_mask); + + const auto wrapped_callback = [this, mapped_buffer, callback](hailo_status callback_status) { + if (HAILO_SUCCESS != callback_status) { + // No need to sync, just forward the callback. + callback(callback_status); + return; + } - const auto callback = [this, user_callback](std::shared_ptr buffer, const hailo_async_transfer_completion_info_t &status, void *opaque) { - user_callback(buffer, status, opaque); + // The device may only change the contents of mapped_buffer, if it was mapped in Direction::D2H + // (not Direction::BOTH because channels are either D2H or H2D). Hence, we don't need to sync H2D + // buffers to the host (the host's "view" of the buffer is "up to date"). + if (m_direction == Direction::D2H) { + auto sync_status = mapped_buffer->synchronize(HailoRTDriver::DmaSyncDirection::TO_HOST); + if (sync_status != HAILO_SUCCESS) { + LOGGER__ERROR("Failed to sync buffer to host with status {}", sync_status); + callback_status = sync_status; + } + } - // opaque is only for the user callback - static constexpr void *NO_CONTEXT = nullptr; - m_transfer_done_callback(buffer, status, NO_CONTEXT); + callback(callback_status); }; - m_state->add_pending_buffer(num_available, last_desc_avail, m_direction, callback, buffer, opaque); + m_state->add_pending_buffer(num_available, last_desc_avail, m_direction, wrapped_callback, mapped_buffer); return inc_num_available(desc_num); } diff --git a/hailort/libhailort/src/vdma/channel/async_channel.hpp b/hailort/libhailort/src/vdma/channel/async_channel.hpp index a161ced..d2ae258 100644 --- a/hailort/libhailort/src/vdma/channel/async_channel.hpp +++ b/hailort/libhailort/src/vdma/channel/async_channel.hpp @@ -32,7 +32,7 @@ public: static Expected create(vdma::ChannelId channel_id, Direction direction, HailoRTDriver &driver, uint32_t descs_count, uint16_t desc_page_size, const std::string &stream_name = "", LatencyMeterPtr latency_meter = nullptr, uint16_t transfers_per_axi_intr = 1); - + AsyncChannel(vdma::ChannelId channel_id, Direction direction, HailoRTDriver &driver, uint32_t descs_count, uint16_t desc_page_size, const std::string &stream_name, LatencyMeterPtr latency_meter, uint16_t transfers_per_axi_intr, hailo_status &status); @@ -45,10 +45,10 @@ public: virtual hailo_status complete_channel_activation(uint32_t transfer_size, bool resume_pending_transfers) override; virtual hailo_status complete_channel_deactivation() override; - virtual hailo_status transfer(std::shared_ptr buffer, const TransferDoneCallback &user_callback, void *opaque) override; + virtual hailo_status transfer_async(TransferRequest &&transfer_request) override; virtual hailo_status cancel_pending_transfers() override; - virtual hailo_status transfer(void *buf, size_t count) override; + virtual hailo_status transfer_sync(void *buf, size_t count, std::chrono::milliseconds timeout) override; // TODO: don't want virtual hailo_status write_buffer(const MemoryView &buffer, std::chrono::milliseconds timeout, const std::function &should_cancel) override; @@ -65,10 +65,10 @@ public: virtual Expected get_d2h_pending_descs_count() override; private: - hailo_status transfer_d2h(std::shared_ptr buffer, const TransferDoneCallback &user_callback, void *opaque); - hailo_status transfer_h2d(std::shared_ptr buffer, const TransferDoneCallback &user_callback, void *opaque); - hailo_status prepare_descriptors(std::shared_ptr buffer, const TransferDoneCallback &user_callback, - void *opaque, InterruptsDomain first_desc_interrupts_domain, InterruptsDomain last_desc_interrupts_domain); + hailo_status transfer_d2h(MappedBufferPtr mapped_buffer, const InternalTransferDoneCallback &user_callback); + hailo_status transfer_h2d(MappedBufferPtr mapped_buffer, const InternalTransferDoneCallback &user_callback); + hailo_status prepare_descriptors(MappedBufferPtr mapped_buffer, const InternalTransferDoneCallback &user_callback, + InterruptsDomain first_desc_interrupts_domain, InterruptsDomain last_desc_interrupts_domain); }; } /* namespace vdma */ diff --git a/hailort/libhailort/src/vdma/channel/boundary_channel.cpp b/hailort/libhailort/src/vdma/channel/boundary_channel.cpp index c5652a8..9a298d0 100644 --- a/hailort/libhailort/src/vdma/channel/boundary_channel.cpp +++ b/hailort/libhailort/src/vdma/channel/boundary_channel.cpp @@ -81,10 +81,6 @@ BoundaryChannel::BoundaryChannel(Type type, vdma::ChannelId channel_id, Directio status = HAILO_INVALID_ARGUMENT; return; } - - m_transfer_done_callback = [this](std::shared_ptr, const hailo_async_transfer_completion_info_t &, void *) { - m_user_interrupt_callback(1); - }; } void BoundaryChannel::clear_pending_buffers_descriptors() @@ -103,22 +99,13 @@ void BoundaryChannel::clear_pending_buffers_descriptors() hailo_status BoundaryChannel::trigger_channel_completion(uint16_t hw_num_processed) { - size_t processed_no = 0; + PendingBuffersQueue completed_buffers{PENDING_BUFFERS_SIZE}; { // NOTE: right now, we can retake the 'completion' descriptor for a new transfer before handling the interrupt. // we should have our own pointers indicating whats free instead of reading from HW. - // TODO: consider calculating the last descriptor using the src_desc_avail and src_desc_proc instead of using - // status? - // TODO: we might free a pending buffer which we didn't get an interrupt for yet. we should still handle this - // situation correctly. - - std::lock_guard state_guard(m_state->mutex()); - // Although the hw_num_processed should be a number between 0 and m_descs.size-1, if m_desc.size < 0x10000 - // (the maximum desc size), the actual hw_num_processed is a number between 1 and m_descs.size. Therefore the - // value can be m_descs.size, in this case we change it to zero. - hw_num_processed = static_cast(hw_num_processed & m_state->m_descs.size_mask); + std::unique_lock state_guard(m_state->mutex()); if (m_state->m_is_aborted) { return HAILO_STREAM_ABORTED_BY_USER; @@ -128,6 +115,11 @@ hailo_status BoundaryChannel::trigger_channel_completion(uint16_t hw_num_process return HAILO_STREAM_NOT_ACTIVATED; } + // Although the hw_num_processed should be a number between 0 and m_descs.size-1, if m_desc.size < 0x10000 + // (the maximum desc size), the actual hw_num_processed is a number between 1 and m_descs.size. Therefore the + // value can be m_descs.size, in this case we change it to zero. + hw_num_processed = static_cast(hw_num_processed & m_state->m_descs.size_mask); + if (m_latency_meter != nullptr) { // The latency meter gets an updated hw_num_processed via a call to vdma_interrupts_read_timestamps // (the desc index of the last measured timestamp returned from that ioctl). Since update_latency_meter @@ -141,61 +133,38 @@ hailo_status BoundaryChannel::trigger_channel_completion(uint16_t hw_num_process hw_num_processed = latency_meter_hw_num_processed.value(); } - const auto last_num_processed = static_cast(CB_TAIL(m_state->m_descs)); + const auto previous_num_processed = static_cast(CB_TAIL(m_state->m_descs)); - // Calculate pending_buffers_count before iteration, because the iteration removes done transfers + // Calculate pending_buffers_count before iteration, because the iteration removes done transfers. const auto pending_buffers_count = m_state->m_pending_buffers.size(); for (size_t i = 0; i < pending_buffers_count; i++) { - auto &last_pending_buffer_info = m_state->m_pending_buffers.front(); - const auto last_desc_index = static_cast(last_pending_buffer_info.last_desc); - // Transfer is complete if its last descriptor is in [last_num_processed, hw_num_processed) or - // the the buffer is empty (hw_num_processed == get_num_available()) - const bool is_complete = is_desc_between(last_num_processed, hw_num_processed, last_desc_index) || - (hw_num_processed == get_num_available()); - - #ifndef NDEBUG - static constexpr auto STATUS_MASK = 0xFF; - static constexpr auto ERROR_BIT = 1; - const auto status = (*m_desc_list)[last_desc_index].RemainingPageSize_Status & STATUS_MASK; - CHECK(!is_bit_set(status, ERROR_BIT), HAILO_INTERNAL_FAILURE, - "Error while processing descriptor {} of DMA {} on board {}.", - last_desc_index, m_channel_id, m_driver.dev_path()); - - // status is read after hw_num_processed, so we want is_complete -> (status == 1). - assert(!is_complete || ((status & 0x1) == 1)); - #endif - - if (!is_complete) { + if (!is_complete(m_state->m_pending_buffers.front(), previous_num_processed, hw_num_processed)) { break; } - // Clear relevant descriptors from previous transfer - if (nullptr != m_latency_meter) { - const auto latency_desc_index = last_pending_buffer_info.latency_measure_desc; - m_desc_list->clear_descriptor(latency_desc_index); - } - m_desc_list->clear_descriptor(last_desc_index); - - _CB_SET(m_state->m_descs.tail, (last_pending_buffer_info.last_desc + 1) & m_state->m_descs.size_mask); - last_pending_buffer_info.on_transfer_done(last_pending_buffer_info.buffer, - hailo_async_transfer_completion_info_t{HAILO_SUCCESS}, last_pending_buffer_info.opaque); - processed_no++; + // Move item from pending_buffers to completed_buffers + completed_buffers.push_back(std::move(m_state->m_pending_buffers.front())); m_state->m_pending_buffers.pop_front(); } } - if (0 < processed_no) { + // completed_buffers were copied from m_pending_buffers inside the lock. Now we are free to process them and call + // the right completion callbacks without state mutex held. + for (auto &pending_buffer : completed_buffers) { + on_pending_buffer_irq(pending_buffer); + } + + if (!completed_buffers.empty()) { m_state->transfer_buffer_cv().notify_all(); } return HAILO_SUCCESS; } -hailo_status BoundaryChannel::register_interrupt_callback(const ProcessingCompleteCallback &callback) +void BoundaryChannel::register_interrupt_callback(const ProcessingCompleteCallback &callback) { std::lock_guard state_guard(m_state->mutex()); m_user_interrupt_callback = callback; - return HAILO_SUCCESS; } CONTROL_PROTOCOL__host_buffer_info_t BoundaryChannel::get_boundary_buffer_info(uint32_t transfer_size) @@ -247,20 +216,19 @@ hailo_status BoundaryChannel::activate(uint32_t transfer_size, bool resume_pendi hailo_status BoundaryChannel::deactivate() { std::unique_lock state_guard(m_state->mutex()); + { + CHECK(m_state->m_is_channel_activated, HAILO_INTERNAL_FAILURE, + "Vdma channel {} is not activated", m_channel_id); + m_state->m_is_channel_activated = false; - CHECK(m_state->m_is_channel_activated, HAILO_INTERNAL_FAILURE, - "Vdma channel {} is not activated", m_channel_id); - m_state->m_is_channel_activated = false; - - // Reset the user callback, so as not to keep objects provided by the user alive (they may lead to a chain of refs - // back to this channel causing it to be leaked). - // Note: PendingBuffers held by m_pending_buffers may still hold copies of the current m_transfer_done_callback, - // which in turn holds a reference to *this. Since we stop the m_wait_interrupts_thread there's no risk that - // these callbacks will be called and we don't need to reset this callback. - m_user_interrupt_callback = ignore_processing_complete; + // Note: PendingBuffers held by m_pending_buffers may still hold copies of the current m_transfer_done_callback, + // which in turn holds a reference to *this. Since we stop the m_wait_interrupts_thread there's no risk that + // these callbacks will be called and we don't need to reset this callback. - auto status = complete_channel_deactivation(); - CHECK_SUCCESS(status); + auto status = complete_channel_deactivation(); + CHECK_SUCCESS(status); + } + m_state->m_can_transfer_buffer_cv.notify_all(); return HAILO_SUCCESS; } @@ -270,6 +238,13 @@ BoundaryChannel::Type BoundaryChannel::type() const return m_type; } +hailo_status BoundaryChannel::set_transfers_per_axi_intr(uint16_t transfers_per_axi_intr) +{ + CHECK(0 != transfers_per_axi_intr, HAILO_INVALID_ARGUMENT, "Invalid transfers per axi interrupt"); + m_transfers_per_axi_intr = transfers_per_axi_intr; + return HAILO_SUCCESS; +} + hailo_status BoundaryChannel::flush(const std::chrono::milliseconds &timeout) { if (Direction::D2H == m_direction) { @@ -284,6 +259,10 @@ hailo_status BoundaryChannel::flush(const std::chrono::milliseconds &timeout) status = HAILO_STREAM_ABORTED_BY_USER; return true; // return true so that the wait will finish } + if (!m_state->m_is_channel_activated) { + status = HAILO_STREAM_NOT_ACTIVATED; + return true; // return true so that the wait will finish + } return m_state->m_pending_buffers.empty(); }); CHECK(was_successful, HAILO_TIMEOUT, "Got HAILO_TIMEOUT while waiting for channel {} interrupts on flush", m_channel_id); @@ -315,7 +294,7 @@ bool BoundaryChannel::has_room_in_desc_list(size_t buffer_size) if (desc_num == m_state->m_descs.size) { // Special case when the checking if the buffer is empty - return num_available == num_processed; + return num_available == num_processed; } int num_free = CB_AVAIL(m_state->m_descs, num_available, num_processed); @@ -326,8 +305,12 @@ bool BoundaryChannel::has_room_in_desc_list(size_t buffer_size) return true; } -hailo_status BoundaryChannel::wait(size_t buffer_size, std::chrono::milliseconds timeout) +hailo_status BoundaryChannel::wait(size_t buffer_size, std::chrono::milliseconds timeout, + bool stop_if_deactivated) { + std::unique_lock state_guard(m_state->mutex()); + assert(state_guard.owns_lock()); + const auto max_transfer_size = m_desc_list->desc_page_size() * m_desc_list->count(); CHECK(buffer_size < max_transfer_size, HAILO_INVALID_ARGUMENT, "Requested transfer size ({}) must be smaller than ({})", buffer_size, max_transfer_size); @@ -336,25 +319,73 @@ hailo_status BoundaryChannel::wait(size_t buffer_size, std::chrono::milliseconds std::bind(&BoundaryChannel::is_ready_for_transfer_h2d, this, buffer_size) : std::bind(&BoundaryChannel::is_ready_for_transfer_d2h, this, buffer_size); - std::unique_lock state_guard(m_state->mutex()); - hailo_status status = HAILO_SUCCESS; // Best effort - bool was_successful = m_state->transfer_buffer_cv().wait_for(state_guard, timeout, [this, is_ready_for_transfer, &status] () { - if (m_state->m_is_aborted) { - status = HAILO_STREAM_ABORTED_BY_USER; - return true; // return true so that the wait will finish - } + auto status = HAILO_SUCCESS; // Best effort + bool was_successful = m_state->transfer_buffer_cv().wait_for(state_guard, timeout, + [this, is_ready_for_transfer, stop_if_deactivated, &status] () { + if (m_state->m_is_aborted) { + status = HAILO_STREAM_ABORTED_BY_USER; + return true; // return true so that the wait will finish + } + if (stop_if_deactivated && !m_state->m_is_channel_activated) { + status = HAILO_STREAM_NOT_ACTIVATED; + return true; // return true so that the wait will finish + } - return is_ready_for_transfer(); - }); + return is_ready_for_transfer(); + } + ); CHECK(was_successful, HAILO_TIMEOUT, "Got HAILO_TIMEOUT while waiting for channel {} interrupts", m_channel_id); return status; } -hailo_status BoundaryChannel::set_transfers_per_axi_intr(uint16_t transfers_per_axi_intr) +bool BoundaryChannel::is_complete(const PendingBuffer &pending_buffer, uint16_t previous_num_processed, + uint16_t current_num_processed) { - CHECK(0 != transfers_per_axi_intr, HAILO_INVALID_ARGUMENT, "Invalid transfers per axi interrupt"); - m_transfers_per_axi_intr = transfers_per_axi_intr; - return HAILO_SUCCESS; + // Transfer is complete if its last descriptor is in [previous_num_processed, current_num_processed) or + // the the buffer is empty (previous_num_processed == get_num_available()) + return is_desc_between(previous_num_processed, current_num_processed, pending_buffer.last_desc) || + (current_num_processed == get_num_available()); +} + + +void BoundaryChannel::on_pending_buffer_irq(PendingBuffer &pending_buffer) +{ +#ifndef NDEBUG + auto &last_desc = (*m_desc_list)[pending_buffer.last_desc]; + if (!last_desc.is_done() || last_desc.is_error()) { + LOGGER__ERROR("Error while processing descriptor {} of DMA {} on device {} DESC_STATUS=0x{:x}.", + pending_buffer.last_desc, m_channel_id, m_driver.device_id(), last_desc.status()); + pending_buffer.on_transfer_done(HAILO_INTERNAL_FAILURE); + return; + } +#endif + + { + std::unique_lock state_guard(m_state->mutex()); + + // First, we want to call m_user_interrupt_callback. This callback is meant to be called right after we + // got an interrupt and before the user can read the frame or write a new frame. + // We call this callback inside the lock to make sure it wont be called when the channel is aborted. + if (!m_state->m_is_aborted) { + m_user_interrupt_callback(); + } + + // Then we increase desc num_proc (can happen only in this flow). After it is increased - + // 1. On D2H channels - the output can be read by the user. + // 2. On H2D channels - new input can be written to the buffer. + // Clear relevant descriptors from previous transfer + if (nullptr != m_latency_meter) { + m_desc_list->clear_descriptor(pending_buffer.latency_measure_desc); + } + m_desc_list->clear_descriptor(pending_buffer.last_desc); + + _CB_SET(m_state->m_descs.tail, (pending_buffer.last_desc + 1) & m_state->m_descs.size_mask); + } + + // Finally, we notify user callbacks registered with the transfer. + // We want to make sure that the callbacks are called after the descriptors can be reused (So the user will + // be able to start new transfer). + pending_buffer.on_transfer_done(HAILO_SUCCESS); } } /* namespace vdma */ diff --git a/hailort/libhailort/src/vdma/channel/boundary_channel.hpp b/hailort/libhailort/src/vdma/channel/boundary_channel.hpp index d578a24..6b7580e 100644 --- a/hailort/libhailort/src/vdma/channel/boundary_channel.hpp +++ b/hailort/libhailort/src/vdma/channel/boundary_channel.hpp @@ -32,7 +32,7 @@ namespace vdma { class BoundaryChannel; using BoundaryChannelPtr = std::shared_ptr; -using ProcessingCompleteCallback = std::function; +using ProcessingCompleteCallback = std::function; class BoundaryChannel : public ChannelBase { @@ -63,10 +63,15 @@ public: hailo_status deactivate(); Type type() const; + hailo_status set_transfers_per_axi_intr(uint16_t transfers_per_axi_intr); void clear_pending_buffers_descriptors(); hailo_status trigger_channel_completion(uint16_t hw_num_processed); - virtual hailo_status register_interrupt_callback(const ProcessingCompleteCallback &callback); + + // Register some new interrupt callback (and reset previous). + // Note - when reseting an old callback, it may still be called (until interrupts are stopped). + void register_interrupt_callback(const ProcessingCompleteCallback &callback); + CONTROL_PROTOCOL__host_buffer_info_t get_boundary_buffer_info(uint32_t transfer_size); virtual hailo_status abort(); virtual hailo_status clear_abort(); @@ -74,28 +79,31 @@ public: // For D2H channels, we don't buffer data // Hence there's nothing to be "flushed" and the function will return with HAILO_SUCCESS virtual hailo_status flush(const std::chrono::milliseconds &timeout); - virtual hailo_status wait(size_t buffer_size, std::chrono::milliseconds timeout); - hailo_status set_transfers_per_axi_intr(uint16_t transfers_per_axi_intr); - virtual hailo_status transfer(void *buf, size_t count) = 0; + // Blocks until buffer_size bytes can transferred to/from the channel or until timeout has elapsed. + // If stop_if_deactivated is true, this function will return HAILO_STREAM_NOT_ACTIVATED after deactivate() + // is called. Otherwise, this function can be used to access the buffer while the channel is not active. + hailo_status wait(size_t buffer_size, std::chrono::milliseconds timeout, bool stop_if_deactivated=false); + + // Transfers count bytes to/from buf via the channel. + // Blocks until the transfer can be registered or timeout has elapsed. Hence, calling 'wait(buffer_size, timeout)' + // prior to 'transfer(buf, buffer_size)' is redundant. + virtual hailo_status transfer_sync(void *buf, size_t count, std::chrono::milliseconds timeout) = 0; + // TODO: can write_buffer + send_pending_buffer move to BufferedChannel? (HRT-9105) // Either write_buffer + send_pending_buffer or transfer (h2d) should be used on a given channel, not both virtual hailo_status write_buffer(const MemoryView &buffer, std::chrono::milliseconds timeout, const std::function &should_cancel) = 0; virtual hailo_status send_pending_buffer() = 0; - - // TODO: move buffer? - // TODO: If the same callback is used for different buffers we need a way to tell the transfers appart - // - Passing buffer to callback could do the trick. However, what will happen if the same buffer has been transferred twice? - // - Maybe add a unique transfer_id? At least unique in the context of the maximum number of ongoing transfers - // TODO: What if there's no more room in desc list so the transfer can't be programmed? Should the function block - // - Maybe define that if more than max_concurrent_transfers() (based on a param passed to create) the function will return a failure? + // When the transfer is complete (i.e. data is written to/from buffer with a D2H/H2D channel) callback is called - // buffer can't be freed until callback is called - virtual hailo_status transfer(std::shared_ptr buffer, const TransferDoneCallback &user_callback, void *opaque) = 0; + // transfer_request.buffer can't be freed/changed until callback is called. + virtual hailo_status transfer_async(TransferRequest &&transfer_request) = 0; - // Calls all pending transfer callbacks (if they exist), marking them as canceled by passing hailo_async_transfer_completion_info_t{HAILO_STREAM_NOT_ACTIVATED}. - // Note: This function is to be called on a deactivated channel object. Calling on an active channel will lead to unexpected results + // Calls all pending transfer callbacks (if they exist), marking them as canceled by passing + // HAILO_STREAM_ABORTED_BY_USER as a status to the callbacks. + // Note: This function is to be called on a deactivated channel object. Calling on an active channel will lead to + // unexpected results virtual hailo_status cancel_pending_transfers() = 0; virtual void notify_all() = 0; @@ -117,7 +125,7 @@ public: virtual Expected get_d2h_pending_descs_count() = 0; protected: - static void ignore_processing_complete(uint32_t) {} + static void ignore_processing_complete() {} void stop_interrupts_thread(std::unique_lock &lock); virtual bool is_ready_for_transfer_h2d(size_t buffer_size); virtual bool is_ready_for_transfer_d2h(size_t buffer_size); @@ -127,12 +135,14 @@ protected: virtual hailo_status complete_channel_deactivation() = 0; const Type m_type; - TransferDoneCallback m_transfer_done_callback; ProcessingCompleteCallback m_user_interrupt_callback; uint16_t m_transfers_per_axi_intr; private: bool has_room_in_desc_list(size_t buffer_size); + bool is_complete(const PendingBuffer &pending_buffer, uint16_t previous_num_processed, + uint16_t current_num_processed); + void on_pending_buffer_irq(PendingBuffer &buffer); }; } /* namespace vdma */ diff --git a/hailort/libhailort/src/vdma/channel/buffered_channel.cpp b/hailort/libhailort/src/vdma/channel/buffered_channel.cpp index d1176ee..55602d0 100644 --- a/hailort/libhailort/src/vdma/channel/buffered_channel.cpp +++ b/hailort/libhailort/src/vdma/channel/buffered_channel.cpp @@ -12,8 +12,6 @@ #include "common/logger_macros.hpp" #include "vdma/channel/buffered_channel.hpp" -#include "vdma/memory/mapped_buffer_factory.hpp" -#include "vdma/memory/mapped_buffer_impl.hpp" #include "hw_consts.hpp" #include @@ -53,7 +51,7 @@ BufferedChannel::BufferedChannel(vdma::ChannelId channel_id, Direction direction return; } - auto mapped_buffer = create_mapped_buffer(descs_count, desc_page_size, direction, driver); + auto mapped_buffer = MappedBuffer::create_shared(driver, direction, descs_count * desc_page_size); if (!mapped_buffer) { LOGGER__ERROR("Failed building mapped vdma buffer"); status = mapped_buffer.status(); @@ -72,21 +70,6 @@ BufferedChannel::BufferedChannel(vdma::ChannelId channel_id, Direction direction status = HAILO_SUCCESS; } -Expected> BufferedChannel::create_mapped_buffer(uint32_t descs_count, uint16_t desc_page_size, - Direction direction, HailoRTDriver &driver) -{ - auto desc_page_size_value = driver.calc_desc_page_size(desc_page_size); - CHECK_AS_EXPECTED(is_powerof2(desc_page_size_value), HAILO_INVALID_ARGUMENT, "Descriptor page_size must be a power of two."); - - auto mapped_buffer_exp = MappedBufferFactory::create_mapped_buffer(descs_count * desc_page_size_value, direction, driver); - CHECK_EXPECTED(mapped_buffer_exp); - - auto mapped_buffer = make_shared_nothrow(mapped_buffer_exp.release()); - CHECK_NOT_NULL_AS_EXPECTED(mapped_buffer, HAILO_OUT_OF_HOST_MEMORY); - - return mapped_buffer; -} - hailo_status BufferedChannel::complete_channel_deactivation() { const auto status = store_channel_buffer_state(); @@ -189,20 +172,19 @@ hailo_status BufferedChannel::complete_channel_activation(uint32_t transfer_size } if ((Direction::D2H == m_direction) && (transfer_size != 0)) { - const auto transfers_in_buffer = get_transfers_count_in_buffer(transfer_size); + const auto max_transfers_in_buffer = get_transfers_count_in_buffer(transfer_size); + const auto transfers_in_buffer = std::min(max_transfers_in_buffer, m_state->m_pending_buffers.capacity()); const auto pending_descs = get_d2h_pending_descs_count(); const auto descs_in_transfer = m_desc_list->descriptors_in_buffer(transfer_size); const auto pending_transfers = pending_descs.value() / descs_in_transfer; // We prepare descs in advance for D2H channels: - // (1) The channel's buffer can store up to 'transfers_in_buffer' frames of size transfer_size - // (2) There are 'pending_transfers' frames from the previous channel activation (we assume that the same - // 'transfer_size' was used) - // (3) Hence, we have room for 'transfers_in_buffer - pending_transfers' frames in the buffer currently. - // (4) However, we can allow at most 'm_state->m_pending_buffers.capacity()' transfers. We can't store more than + // (1) The channel's buffer can store up to 'max_transfers_in_buffer' frames of size transfer_size + // (2) However, we can allow at most 'm_state->m_pending_buffers.capacity()' transfers. We can't store more than // that in the pending buffers circular array. - // (5) Hence, we'll take the minimum between (3) and (4). - const auto transfers_count = std::min(transfers_in_buffer - pending_transfers, - m_state->m_pending_buffers.capacity()); + // (3) There are 'pending_transfers' frames from the previous channel activation (we assume that the same + // 'transfer_size' was used) + // (4) Hence, we have room for 'min(transfers_in_buffer, pending_buffers.capacity()) - pending_transfers' frames in the buffer currently. + const auto transfers_count = transfers_in_buffer - pending_transfers; status = prepare_d2h_pending_descriptors(transfer_size, static_cast(transfers_count)); CHECK_SUCCESS(status); } @@ -210,32 +192,31 @@ hailo_status BufferedChannel::complete_channel_activation(uint32_t transfer_size return HAILO_SUCCESS; } -hailo_status BufferedChannel::transfer(void *buf, size_t count) +hailo_status BufferedChannel::transfer_sync(void *buf, size_t count, std::chrono::milliseconds timeout) { CHECK_NOT_NULL(buf, HAILO_INVALID_ARGUMENT); CHECK(0 != count, HAILO_INVALID_ARGUMENT); - std::lock_guard state_guard(m_state->mutex()); - if (m_state->m_is_aborted) { - LOGGER__INFO("Tried to write to aborted channel {}", m_channel_id); - return HAILO_STREAM_ABORTED_BY_USER; + auto status = wait(count, timeout); + if ((HAILO_STREAM_NOT_ACTIVATED == status) || (HAILO_STREAM_ABORTED_BY_USER == status)) { + LOGGER__INFO("wait failed because channel {} is not activated/aborted (status {})", m_channel_id, status); + return status; } + CHECK_SUCCESS(status, "wait failed with status {} (channel id: {}, timeout: {}ms)", status, m_channel_id, timeout.count()); - hailo_status status = HAILO_UNINITIALIZED; + std::unique_lock state_guard(m_state->mutex()); if (Direction::H2D == m_direction) { status = transfer_h2d(buf, count); } else { status = transfer_d2h(buf, count); } - if (HAILO_STREAM_NOT_ACTIVATED == status) { - LOGGER__INFO("Transfer failed because Channel {} is not activated", m_channel_id); - return HAILO_STREAM_NOT_ACTIVATED; - } - if (HAILO_SUCCESS != status) { - LOGGER__ERROR("Transfer failed for channel {} with status {}", m_channel_id, status); + if ((HAILO_STREAM_NOT_ACTIVATED == status) || (HAILO_STREAM_ABORTED_BY_USER == status)) { + LOGGER__INFO("transfer failed because channel {} is not activated/aborted (status {})", m_channel_id, status); return status; } + CHECK_SUCCESS(status, "transfer failed with status {} (channel id: {}, timeout: {}ms)", status, m_channel_id, timeout.count()); + return HAILO_SUCCESS; } @@ -263,20 +244,21 @@ hailo_status BufferedChannel::write_to_channel_buffer_cyclic(const MemoryView &b "Can't write {} bytes to channel buffer (channel buffer size {})", buffer.size(), m_channel_buffer->size()); + static const auto SYNC_TO_DEIVCE = HailoRTDriver::DmaSyncDirection::TO_DEVICE; const auto size_to_end = m_channel_buffer->size() - channel_buffer_write_offset; const auto first_chunk_size = std::min(size_to_end, buffer.size()); const auto first_chunk_addr = static_cast(m_channel_buffer->user_address()) + channel_buffer_write_offset; // Copy from buffer to m_channel_buffer and then synchronize std::memcpy(first_chunk_addr, buffer.data(), first_chunk_size); - auto status = m_channel_buffer->pimpl->synchronize(channel_buffer_write_offset, first_chunk_size); + auto status = m_channel_buffer->synchronize(channel_buffer_write_offset, first_chunk_size, SYNC_TO_DEIVCE); CHECK_SUCCESS(status); const auto remaining_size = buffer.size() - first_chunk_size; if (remaining_size > 0) { // Copy the remainder from buffer to m_channel_buffer and then synchronize std::memcpy(m_channel_buffer->user_address(), buffer.data() + first_chunk_size, remaining_size); - status = m_channel_buffer->pimpl->synchronize(0, remaining_size); + status = m_channel_buffer->synchronize(0, remaining_size, SYNC_TO_DEIVCE); CHECK_SUCCESS(status); } @@ -289,19 +271,20 @@ hailo_status BufferedChannel::read_from_channel_buffer_cyclic(uint8_t *dest_buff "Can't read {} bytes from channel buffer (channel buffer size {})", read_size, m_channel_buffer->size()); + static const auto SYNC_TO_HOST = HailoRTDriver::DmaSyncDirection::TO_HOST; const auto size_to_end = m_channel_buffer->size() - channel_buffer_read_offset; const auto first_chunk_size = std::min(size_to_end, read_size); const auto first_chunk_addr = static_cast(m_channel_buffer->user_address()) + channel_buffer_read_offset; // Synchronize m_channel_buffer and copy to dest_buffer - auto status = m_channel_buffer->pimpl->synchronize(channel_buffer_read_offset, first_chunk_size); + auto status = m_channel_buffer->synchronize(channel_buffer_read_offset, first_chunk_size, SYNC_TO_HOST); CHECK_SUCCESS(status); std::memcpy(dest_buffer, first_chunk_addr, first_chunk_size); const auto remaining_size = read_size - first_chunk_size; if (remaining_size > 0) { // Synchronize m_channel_buffer and copy remainder to dest_buffer - status = m_channel_buffer->pimpl->synchronize(0, remaining_size); + status = m_channel_buffer->synchronize(0, remaining_size, SYNC_TO_HOST); CHECK_SUCCESS(status); std::memcpy(dest_buffer + first_chunk_size, m_channel_buffer->user_address(), remaining_size); } @@ -431,7 +414,7 @@ hailo_status BufferedChannel::send_pending_buffer() return HAILO_SUCCESS; } -hailo_status BufferedChannel::transfer(std::shared_ptr, const TransferDoneCallback &, void *) +hailo_status BufferedChannel::transfer_async(TransferRequest &&) { return HAILO_NOT_IMPLEMENTED; } @@ -507,9 +490,9 @@ hailo_status BufferedChannel::prepare_descriptors(size_t transfer_size, Interrup assert(desired_desc_num <= MAX_DESCS_COUNT); uint16_t desc_num = static_cast(desired_desc_num); - int num_available = get_num_available(); - int num_processed = CB_TAIL(m_state->m_descs); - int num_free = CB_AVAIL(m_state->m_descs, num_available, num_processed); + const auto num_available = get_num_available(); + const auto num_processed = CB_TAIL(m_state->m_descs); + const auto num_free = CB_AVAIL(m_state->m_descs, num_available, num_processed); if (num_free < desc_num) { return HAILO_OUT_OF_DESCRIPTORS; } @@ -520,15 +503,16 @@ hailo_status BufferedChannel::prepare_descriptors(size_t transfer_size, Interrup first_desc_interrupts_domain); } auto actual_desc_count = m_desc_list->program_last_descriptor(transfer_size, last_desc_interrupts_domain, - num_available, true); + num_available); if (!actual_desc_count) { LOGGER__ERROR("Failed to program desc_list for channel {}", m_channel_id); return actual_desc_count.status(); } - assert (actual_desc_count.value() == desc_num); - int last_desc_avail = ((num_available + desc_num - 1) & m_state->m_descs.size_mask); + assert(actual_desc_count.value() == desc_num); + assert(desc_num > 0); + const auto last_desc_avail = static_cast((num_available + desc_num - 1) & m_state->m_descs.size_mask); - m_state->add_pending_buffer(num_available, last_desc_avail, m_direction, m_transfer_done_callback); + m_state->add_pending_buffer(num_available, last_desc_avail, m_direction); return inc_num_available(desc_num); } diff --git a/hailort/libhailort/src/vdma/channel/buffered_channel.hpp b/hailort/libhailort/src/vdma/channel/buffered_channel.hpp index d46ba7f..ac0d8c4 100644 --- a/hailort/libhailort/src/vdma/channel/buffered_channel.hpp +++ b/hailort/libhailort/src/vdma/channel/buffered_channel.hpp @@ -11,9 +11,9 @@ #ifndef _HAILO_VDMA_BUFFERED_CHANNEL_HPP_ #define _HAILO_VDMA_BUFFERED_CHANNEL_HPP_ -#include "hailo/hailort.h" -#include "hailo/dma_mapped_buffer.hpp" +#include "hailo/hailort.h" +#include "vdma/memory/mapped_buffer.hpp" #include "vdma/channel/boundary_channel.hpp" @@ -38,12 +38,15 @@ public: BufferedChannel &operator=(BufferedChannel &&other) = delete; virtual ~BufferedChannel() = default; - virtual hailo_status transfer(void *buf, size_t count) override; + // Writes/reads from the channel buffer. This function can work even if the channel is not activated (for example - + // reading data if it is ready). + virtual hailo_status transfer_sync(void *buf, size_t count, std::chrono::milliseconds timeout) override; // Either write_buffer + send_pending_buffer or transfer (h2d) should be used on a given channel, not both virtual hailo_status write_buffer(const MemoryView &buffer, std::chrono::milliseconds timeout, const std::function &should_cancel) override; virtual hailo_status send_pending_buffer() override; - virtual hailo_status transfer(std::shared_ptr, const TransferDoneCallback &, void *) override; + // TODO: merge with "transfer_sync(void *buf, size_t count)"? (HRT-10207) + virtual hailo_status transfer_async(TransferRequest &&) override; virtual hailo_status cancel_pending_transfers() override; virtual hailo_status complete_channel_activation(uint32_t transfer_size, bool resume_pending_transfers) override; virtual hailo_status complete_channel_deactivation() override; @@ -57,8 +60,6 @@ public: virtual void notify_all() override; private: - static Expected> create_mapped_buffer(uint32_t descs_count, uint16_t desc_page_size, - Direction direction, HailoRTDriver &driver); hailo_status transfer_h2d(void *buf, size_t count); hailo_status write_buffer_impl(const MemoryView &buffer); @@ -76,9 +77,9 @@ private: // TODO: m_channel_buffer gets bound to ChannelBase::m_desc_list meaning the desc in that list point to dma addrs // that back m_channel_buffer. Because ChannelBase gets dtor'd after BufferedChannel, m_channel_buffer ChannelBase::m_desc_list // will point to a freed buffer. This is ok because the channel objects only get dtor'd after they are deactivated by the fw. - // Might want to enforce this in hailort as well (e.g. desc lists can hold shared_ptrs to DmaMappedBuffer while they are bound). + // Might want to enforce this in hailort as well (e.g. desc lists can hold shared_ptrs to MappedBuffer while they are bound). // (HRT-9110) - std::shared_ptr m_channel_buffer; + std::shared_ptr m_channel_buffer; // Using CircularArray because it won't allocate or free memory wile pushing and popping. The fact that it is circular is not relevant here CircularArray m_pending_buffers_sizes; std::atomic_uint16_t m_pending_num_avail_offset; diff --git a/hailort/libhailort/src/vdma/channel/channel_base.cpp b/hailort/libhailort/src/vdma/channel/channel_base.cpp index 4c233fd..e872e73 100644 --- a/hailort/libhailort/src/vdma/channel/channel_base.cpp +++ b/hailort/libhailort/src/vdma/channel/channel_base.cpp @@ -41,12 +41,6 @@ ChannelBase::ChannelBase(vdma::ChannelId channel_id, Direction direction, HailoR return; } - if (descs_count > MAX_DESCS_COUNT) { - LOGGER__ERROR("Vdma channel descs_count mustn't be larger than {}", MAX_DESCS_COUNT); - status = HAILO_INVALID_ARGUMENT; - return; - } - auto state = VdmaChannelState::create(descs_count, (nullptr != m_latency_meter)); if(!state) { LOGGER__ERROR("Failed to create channel's state"); @@ -55,7 +49,6 @@ ChannelBase::ChannelBase(vdma::ChannelId channel_id, Direction direction, HailoR } m_state = state.release(); - // Allocate descriptor list (host side) status = allocate_descriptor_list(descs_count, desc_page_size); if (HAILO_SUCCESS != status) { LOGGER__ERROR("Failed to allocate Vdma buffer for channel transfer! status={}", status); @@ -134,6 +127,12 @@ uint16_t ChannelBase::get_num_available() return num_available; } +void ChannelBase::set_num_proc_value(uint16_t new_value) +{ + assert(new_value < m_state->m_descs.size); + _CB_SET(m_state->m_descs.tail, new_value); +} + Expected ChannelBase::get_hw_num_processed() { auto hw_num_processed = m_host_registers.get_num_processed(); @@ -153,10 +152,8 @@ ChannelBase::Direction ChannelBase::other_direction(Direction direction) hailo_status ChannelBase::allocate_descriptor_list(uint32_t descs_count, uint16_t desc_page_size) { - auto desc_page_size_value = m_driver.calc_desc_page_size(desc_page_size); - CHECK(is_powerof2(desc_page_size_value), HAILO_INVALID_ARGUMENT, "Descriptor page_size must be a power of two."); - - auto desc_list_exp = DescriptorList::create(descs_count, desc_page_size_value, m_driver); + static const bool CIRCULAR = true; + auto desc_list_exp = DescriptorList::create(descs_count, desc_page_size, CIRCULAR, m_driver); CHECK_EXPECTED_AS_STATUS(desc_list_exp); m_desc_list = make_shared_nothrow(desc_list_exp.release()); diff --git a/hailort/libhailort/src/vdma/channel/channel_base.hpp b/hailort/libhailort/src/vdma/channel/channel_base.hpp index 5f56b81..8ae5342 100644 --- a/hailort/libhailort/src/vdma/channel/channel_base.hpp +++ b/hailort/libhailort/src/vdma/channel/channel_base.hpp @@ -89,6 +89,7 @@ protected: Expected is_aborted(); hailo_status set_num_avail_value(uint16_t new_value); uint16_t get_num_available(); + void set_num_proc_value(uint16_t new_value); Expected get_hw_num_processed(); hailo_status inc_num_available(uint16_t value); static Direction other_direction(const Direction direction); diff --git a/hailort/libhailort/src/vdma/channel/channel_id.hpp b/hailort/libhailort/src/vdma/channel/channel_id.hpp index 2934456..09fb043 100644 --- a/hailort/libhailort/src/vdma/channel/channel_id.hpp +++ b/hailort/libhailort/src/vdma/channel/channel_id.hpp @@ -36,6 +36,12 @@ struct ChannelId { return std::make_pair(a.engine_index, a.channel_index) < std::make_pair(b.engine_index, b.channel_index); } + + // Allow channel Id's to be compared + friend bool operator==(const ChannelId &a, const ChannelId &b) + { + return ((a.channel_index == b.channel_index) && (a.engine_index == b.engine_index)); + } }; } /* namespace vdma */ diff --git a/hailort/libhailort/src/vdma/channel/channel_state.cpp b/hailort/libhailort/src/vdma/channel/channel_state.cpp index 0880f04..2afebb2 100644 --- a/hailort/libhailort/src/vdma/channel/channel_state.cpp +++ b/hailort/libhailort/src/vdma/channel/channel_state.cpp @@ -220,19 +220,19 @@ void VdmaChannelState::reset_previous_state_counters() m_d2h_read_desc_index_abs = 0; } -void VdmaChannelState::add_pending_buffer(uint32_t first_desc, uint32_t last_desc, HailoRTDriver::DmaDirection direction, - const TransferDoneCallback &on_transfer_done, std::shared_ptr buffer, void *opaque) +void VdmaChannelState::add_pending_buffer(uint16_t first_desc, uint16_t last_desc, HailoRTDriver::DmaDirection direction, + const InternalTransferDoneCallback &on_transfer_done, MappedBufferPtr mapped_buffer) { if (m_pending_buffers.full()) { // TODO- HRT-8900 : Fix log and check if should return error LOGGER__ERROR("no avail space"); } + PendingBuffer pending_buffer{}; pending_buffer.last_desc = last_desc; pending_buffer.latency_measure_desc = (direction == HailoRTDriver::DmaDirection::H2D) ? first_desc : last_desc; pending_buffer.on_transfer_done = on_transfer_done; - pending_buffer.buffer = buffer; - pending_buffer.opaque = opaque; + pending_buffer.mapped_buffer = mapped_buffer; m_pending_buffers.push_back(std::move(pending_buffer)); } diff --git a/hailort/libhailort/src/vdma/channel/channel_state.hpp b/hailort/libhailort/src/vdma/channel/channel_state.hpp index ece1e27..5bc964e 100644 --- a/hailort/libhailort/src/vdma/channel/channel_state.hpp +++ b/hailort/libhailort/src/vdma/channel/channel_state.hpp @@ -15,8 +15,8 @@ #include "hailo/hailort.h" #include "os/hailort_driver.hpp" #include "common/circular_buffer.hpp" -#include "hailo/dma_mapped_buffer.hpp" -#include "hailo/stream.hpp" +#include "vdma/memory/mapped_buffer.hpp" +#include "stream_common/async_common.hpp" #include #include @@ -30,13 +30,17 @@ namespace hailort { namespace vdma { struct PendingBuffer { - uint32_t last_desc; - uint32_t latency_measure_desc; - TransferDoneCallback on_transfer_done; - std::shared_ptr buffer; - void *opaque; + uint16_t last_desc; + uint16_t latency_measure_desc; + InternalTransferDoneCallback on_transfer_done; + MappedBufferPtr mapped_buffer; }; +// We use std::array for PendingBuffersQueue to avoid dynamic allocations allocations. We are doing it for two reasons: +// 1. It relies on memory shared between process (so we can't have dynamic allocation). +// 2. We put it on interrupt handler stack - we want to avoid allocations. +using PendingBuffersQueue = CircularArray>; + class ChannelBase; class BoundaryChannel; class AsyncChannel; @@ -91,6 +95,7 @@ using RecursiveSharedMutex = std::recursive_mutex; using SharedConditionVariable = std::condition_variable_any; #endif + class VdmaChannelState final { public: @@ -101,16 +106,19 @@ public: VdmaChannelState(VdmaChannelState &&other) = delete; ~VdmaChannelState() = default; + static void empty_transfer_done_callback(hailo_status){} + void reset_counters(); void reset_previous_state_counters(); // Each transfer on the channel is logged by a PendingBuffer: // - first_desc/last_desc - first and last descriptors of the transfer // - direction - transfer's direction // - on_transfer_done - callback to be called once the transfer is complete (i.e. when an interrupt is received on last_desc) - // - buffer - points to the vdma mapped buffer being transferred (may be null) - // - opaque - context to be transferred to the callback (may be null) - void add_pending_buffer(uint32_t first_desc, uint32_t last_desc, HailoRTDriver::DmaDirection direction, - const TransferDoneCallback &on_transfer_done, std::shared_ptr buffer = nullptr, void *opaque = nullptr); + // - context - transfer context + // - mapped_buffer - buffer's dma mapping (may be null) + void add_pending_buffer(uint16_t first_desc, uint16_t last_desc, HailoRTDriver::DmaDirection direction, + const InternalTransferDoneCallback &on_transfer_done = empty_transfer_done_callback, + MappedBufferPtr mapped_buffer = nullptr); RecursiveSharedMutex &mutex() { @@ -152,8 +160,7 @@ private: bool m_is_channel_activated; - // On pending buffer with must use std::array because it relays on the shared memory (and std::vector uses new malloc) - CircularArray> m_pending_buffers; + PendingBuffersQueue m_pending_buffers; // TODO: describe why we must have our own num_available and num_proc. // it's not just for efficiency but its critical to avoid a potential bug - see Avigail email. // TODO: Consider C11 stdatomic diff --git a/hailort/libhailort/src/vdma/channel/interrupts_dispatcher.cpp b/hailort/libhailort/src/vdma/channel/interrupts_dispatcher.cpp index 99ad909..a59699f 100644 --- a/hailort/libhailort/src/vdma/channel/interrupts_dispatcher.cpp +++ b/hailort/libhailort/src/vdma/channel/interrupts_dispatcher.cpp @@ -23,84 +23,110 @@ Expected> InterruptsDispatcher::create(std InterruptsDispatcher::InterruptsDispatcher(std::reference_wrapper driver) : m_driver(driver), - m_is_running(false), - m_channels_bitmap() + m_interrupts_thread([this] { wait_interrupts(); }) {} InterruptsDispatcher::~InterruptsDispatcher() { - if (m_is_running) { - stop(); + if (m_wait_context != nullptr) { + auto status = stop(); + if (status != HAILO_SUCCESS) { + LOGGER__ERROR("Failed stopping interrupts dispatcher on destructor"); + } + } + + if (m_interrupts_thread.joinable()) { + signal_thread_quit(); + m_interrupts_thread.join(); } } hailo_status InterruptsDispatcher::start(const ChannelsBitmap &channels_bitmap, bool enable_timestamp_measure, const ProcessIrqCallback &process_irq) { - CHECK(!m_is_running, HAILO_INVALID_OPERATION, "Interrupt thread already running"); - assert(m_channel_threads.empty()); - assert(m_channels_bitmap == ChannelsBitmap{}); + { + std::unique_lock lock(m_mutex); + CHECK(m_wait_context == nullptr, HAILO_INVALID_OPERATION, "Interrupt thread already running"); - m_channels_bitmap = channels_bitmap; + auto wait_context = make_unique_nothrow(WaitContext{channels_bitmap, process_irq}); + CHECK_NOT_NULL(wait_context, HAILO_OUT_OF_HOST_MEMORY); + m_wait_context = std::move(wait_context); - auto status = m_driver.get().vdma_interrupts_enable(m_channels_bitmap, enable_timestamp_measure); - CHECK_SUCCESS(status, "Failed to enable vdma interrupts"); - - // Setting m_is_running will allow the threads to run - m_is_running = true; - m_channel_threads.emplace_back([this, process_irq]() { - // m_channels_bitmap may be changed by InterruptsDispatcher::stop. To avoid wait for 0 channels, - // we use copy of m_channels_bitmap. - ChannelsBitmap channels_bitmap_local = m_channels_bitmap; - wait_interrupts(channels_bitmap_local, process_irq); - }); + auto status = m_driver.get().vdma_interrupts_enable(m_wait_context->bitmap, enable_timestamp_measure); + CHECK_SUCCESS(status, "Failed to enable vdma interrupts"); + } + m_cond.notify_one(); return HAILO_SUCCESS; } hailo_status InterruptsDispatcher::stop() { - CHECK(m_is_running, HAILO_INVALID_OPERATION, "Interrupts thread not started"); - assert(!m_channel_threads.empty()); - assert(m_channels_bitmap != ChannelsBitmap{}); + std::unique_lock lock(m_mutex); + CHECK(m_wait_context != nullptr, HAILO_INVALID_OPERATION, "Interrupt thread not running"); - // Signal threads to stop execution - m_is_running = false; + // Nullify wait context so the thread will pause + const auto bitmap = m_wait_context->bitmap; + m_wait_context = nullptr; // Calling disable interrupts will cause the vdma_interrupts_wait to return. - auto status = m_driver.get().vdma_interrupts_disable(m_channels_bitmap); + auto status = m_driver.get().vdma_interrupts_disable(bitmap); CHECK_SUCCESS(status, "Failed to disable vdma interrupts"); - m_channels_bitmap = ChannelsBitmap{}; - for (auto &thread : m_channel_threads) { - if (thread.joinable()) { - thread.join(); - } - } - m_channel_threads.clear(); + // Needs to make sure that the interrupts thread is disabled. + // The wait is needed because otherwise, on a fast stop() and start(), the next start() may accept + // interrupts from previous run. + m_cond.wait(lock, [&]{ return m_thread_state == ThreadState::not_active; }); return HAILO_SUCCESS; } -void InterruptsDispatcher::wait_interrupts(const ChannelsBitmap &channels_bitmap, const ProcessIrqCallback &process_irq) +void InterruptsDispatcher::wait_interrupts() { OsUtils::set_current_thread_name("CHANNEL_INTR"); - while (m_is_running) { + + std::unique_lock lock(m_mutex); + while (true) { + + m_thread_state = ThreadState::not_active; + m_cond.notify_one(); // Wake up stop() + + m_cond.wait(lock, [&]{ return m_should_quit || (m_wait_context != nullptr); }); + if (m_should_quit) { + break; + } + + m_thread_state = ThreadState::active; + auto wait_context = *m_wait_context; + // vdma_interrupts_wait is a blocking function that returns in this scenarios: // 1. We got a new interrupts, irq_data will be passed to the process_irq callback // 2. vdma_interrupts_disable will be called, vdma_interrupts_wait will return with an empty list. // 3. Other error returns - shouldn't really happen, we exit the interrupt thread. - auto irq_data = m_driver.get().vdma_interrupts_wait(channels_bitmap); + lock.unlock(); + auto irq_data = m_driver.get().vdma_interrupts_wait(wait_context.bitmap); + lock.lock(); + if (!irq_data.has_value()) { LOGGER__ERROR("Interrupt thread exit with {}", irq_data.status()); break; } if (irq_data->channels_count > 0) { - process_irq(irq_data.release()); + wait_context.process_irq(irq_data.release()); } } } +void InterruptsDispatcher::signal_thread_quit() +{ + { + std::unique_lock lock(m_mutex); + assert(m_thread_state == ThreadState::not_active); + m_should_quit = true; + } + m_cond.notify_one(); +} + } /* namespace vdma */ } /* namespace hailort */ diff --git a/hailort/libhailort/src/vdma/channel/interrupts_dispatcher.hpp b/hailort/libhailort/src/vdma/channel/interrupts_dispatcher.hpp index c02f428..b039e41 100644 --- a/hailort/libhailort/src/vdma/channel/interrupts_dispatcher.hpp +++ b/hailort/libhailort/src/vdma/channel/interrupts_dispatcher.hpp @@ -13,11 +13,11 @@ #include "os/hailort_driver.hpp" #include #include +#include namespace hailort { namespace vdma { - /// When needed, creates thread (or threads) that waits for interrupts on all channels. class InterruptsDispatcher final { public: @@ -33,19 +33,40 @@ public: InterruptsDispatcher(InterruptsDispatcher &&) = delete; InterruptsDispatcher &operator=(InterruptsDispatcher &&) = delete; - // TODO: HRT-9590 remove interrupt_thread_per_channel, use it by default hailo_status start(const ChannelsBitmap &channels_bitmap, bool enable_timestamp_measure, const ProcessIrqCallback &process_irq); hailo_status stop(); private: - void wait_interrupts(const ChannelsBitmap &channels_bitmap, const ProcessIrqCallback &process_irq); + void wait_interrupts(); + void signal_thread_quit(); + + struct WaitContext { + ChannelsBitmap bitmap; + ProcessIrqCallback process_irq; + }; + + enum class ThreadState { + // The interrupts thread is actually waiting for interrupts + active, + + // The interrupts thread is done waiting for interrupts, it is waiting to be active. + not_active, + }; + + std::mutex m_mutex; + std::condition_variable m_cond; const std::reference_wrapper m_driver; - std::atomic m_is_running; - ChannelsBitmap m_channels_bitmap; - std::vector m_channel_threads; + + ThreadState m_thread_state = ThreadState::not_active; + // When m_wait_context is not nullptr, the thread should start waiting for interrupts. + std::unique_ptr m_wait_context; + + // m_should_quit is used to quit the thread (called on destruction) + bool m_should_quit = false; + std::thread m_interrupts_thread; }; } /* namespace vdma */ diff --git a/hailort/libhailort/src/vdma/integrated/integrated_device.cpp b/hailort/libhailort/src/vdma/integrated/integrated_device.cpp index b6406d5..c574d3d 100644 --- a/hailort/libhailort/src/vdma/integrated/integrated_device.cpp +++ b/hailort/libhailort/src/vdma/integrated/integrated_device.cpp @@ -28,19 +28,19 @@ Expected> IntegratedDevice::create() { hailo_status status = HAILO_UNINITIALIZED; - auto driver = HailoRTDriver::create(INTEGRATED_NNC_DRIVER_PATH); + const HailoRTDriver::DeviceInfo device_info {INTEGRATED_NNC_DRIVER_PATH, DEVICE_ID}; + auto driver = HailoRTDriver::create(device_info); CHECK_EXPECTED(driver, "Failed to initialize HailoRTDriver"); - auto device = std::unique_ptr(new (std::nothrow) IntegratedDevice(driver.release(), status, DEVICE_ID)); + auto device = std::unique_ptr(new (std::nothrow) IntegratedDevice(driver.release(), status)); CHECK_AS_EXPECTED((nullptr != device), HAILO_OUT_OF_HOST_MEMORY); CHECK_SUCCESS_AS_EXPECTED(status, "Failed creating IntegratedDevice"); return device; } - -IntegratedDevice::IntegratedDevice(HailoRTDriver &&driver, hailo_status &status, const std::string &device_id) : - VdmaDevice::VdmaDevice(std::move(driver), Device::Type::INTEGRATED, device_id) +IntegratedDevice::IntegratedDevice(HailoRTDriver &&driver, hailo_status &status) : + VdmaDevice::VdmaDevice(std::move(driver), Device::Type::INTEGRATED) { status = update_fw_state(); if (HAILO_SUCCESS != status) { @@ -51,10 +51,6 @@ IntegratedDevice::IntegratedDevice(HailoRTDriver &&driver, hailo_status &status, status = HAILO_SUCCESS; } -Expected IntegratedDevice::get_architecture() const { - return Expected(m_device_architecture); -} - hailo_status IntegratedDevice::reset_impl(CONTROL_PROTOCOL__reset_type_t reset_type) { if (CONTROL_PROTOCOL__RESET_TYPE__NN_CORE == reset_type) { diff --git a/hailort/libhailort/src/vdma/integrated/integrated_device.hpp b/hailort/libhailort/src/vdma/integrated/integrated_device.hpp index 5bb07fe..856994b 100644 --- a/hailort/libhailort/src/vdma/integrated/integrated_device.hpp +++ b/hailort/libhailort/src/vdma/integrated/integrated_device.hpp @@ -24,12 +24,11 @@ namespace hailort class IntegratedDevice : public VdmaDevice { public: - virtual ~IntegratedDevice() = default; static bool is_loaded(); static Expected> create(); - virtual Expected get_architecture() const override; - virtual const char* get_dev_id() const override {return DEVICE_ID;} + virtual ~IntegratedDevice() = default; + Expected read_log(MemoryView &buffer, hailo_cpu_id_t cpu_id); virtual bool is_stream_interface_supported(const hailo_stream_interface_t &stream_interface) const override @@ -53,7 +52,7 @@ protected: virtual hailo_status reset_impl(CONTROL_PROTOCOL__reset_type_t reset_type) override; private: - IntegratedDevice(HailoRTDriver &&driver, hailo_status &status, const std::string &device_id); + IntegratedDevice(HailoRTDriver &&driver, hailo_status &status); }; diff --git a/hailort/libhailort/src/vdma/memory/buffer_requirements.cpp b/hailort/libhailort/src/vdma/memory/buffer_requirements.cpp new file mode 100644 index 0000000..c48ecb3 --- /dev/null +++ b/hailort/libhailort/src/vdma/memory/buffer_requirements.cpp @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file buffer_requirements.cpp + **/ + +#include "buffer_requirements.hpp" +#include "vdma/memory/descriptor_list.hpp" +#include "utils.h" + +namespace hailort { +namespace vdma { + +// Minimum size of ccb buffers in descriptors, taken from the CCB spec. +static constexpr uint32_t MIN_CCB_DESCRIPTORS_COUNT = 16; + +Expected BufferSizesRequirements::get_sg_buffer_requirements_single_transfer( + uint16_t max_desc_page_size, uint16_t min_batch_size, uint16_t max_batch_size, uint32_t transfer_size, + bool is_circular, const bool force_default_page_size) +{ + // First, get the result for the min size + auto results = get_sg_buffer_requirements_multiple_transfers(max_desc_page_size, min_batch_size, + {transfer_size}, is_circular, force_default_page_size); + CHECK_EXPECTED(results); + + // In order to fetch all descriptors, the amount of active descs is lower by one that the amount + // of descs given (Otherwise we won't be able to determine if the buffer is empty or full). + // Therefore we add 1 in order to compensate. + const uint32_t descs_per_transfer = DIV_ROUND_UP(transfer_size, results->desc_page_size()); + uint32_t descs_count = std::min((descs_per_transfer * max_batch_size) + 1, MAX_DESCS_COUNT); + if (is_circular) { + descs_count = get_nearest_powerof_2(descs_count, MIN_DESCS_COUNT); + } + + return BufferSizesRequirements{ descs_count, results->desc_page_size() }; +} + +Expected BufferSizesRequirements::get_sg_buffer_requirements_multiple_transfers( + uint16_t max_desc_page_size, uint16_t batch_size, const std::vector &transfer_sizes, + bool is_circular, const bool force_default_page_size) +{ + const uint16_t initial_desc_page_size = force_default_page_size ? + DEFAULT_DESC_PAGE_SIZE : find_initial_desc_page_size(transfer_sizes); + + CHECK_AS_EXPECTED(max_desc_page_size <= MAX_DESC_PAGE_SIZE, HAILO_INTERNAL_FAILURE, + "max_desc_page_size given {} is bigger than hw max desc page size {}", + max_desc_page_size, MAX_DESC_PAGE_SIZE); + CHECK_AS_EXPECTED(MIN_DESC_PAGE_SIZE <= max_desc_page_size, HAILO_INTERNAL_FAILURE, + "max_desc_page_size given {} is lower that hw min desc page size {}", + max_desc_page_size, MIN_DESC_PAGE_SIZE); + + const uint16_t min_desc_page_size = MIN_DESC_PAGE_SIZE; + CHECK_AS_EXPECTED(initial_desc_page_size <= max_desc_page_size, HAILO_INTERNAL_FAILURE, + "Initial descriptor page size ({}) is larger than maximum descriptor page size ({})", + initial_desc_page_size, max_desc_page_size); + CHECK_AS_EXPECTED(initial_desc_page_size >= min_desc_page_size, HAILO_INTERNAL_FAILURE, + "Initial descriptor page size ({}) is smaller than minimum descriptor page size ({})", + initial_desc_page_size, min_desc_page_size); + + // Defined as uint32_t to prevent overflow (as we multiply it by two in each iteration of the while loop bellow) + uint32_t local_desc_page_size = initial_desc_page_size; + + uint32_t descs_count = get_required_descriptor_count(transfer_sizes, initial_desc_page_size); + // Too many descriptors; try a larger desc_page_size which will lead to less descriptors used + while ((descs_count * batch_size) > (MAX_DESCS_COUNT - 1)) { + CHECK_AS_EXPECTED(IS_FIT_IN_UINT16(local_desc_page_size << 1), HAILO_INTERNAL_FAILURE, + "Descriptor page size needs to fit in 16B"); + local_desc_page_size = static_cast(local_desc_page_size << 1); + + CHECK_AS_EXPECTED(local_desc_page_size <= max_desc_page_size, HAILO_OUT_OF_DESCRIPTORS, + "Network shapes and batch size exceeds driver descriptors capabilities. " + "Required descriptors count: {}, max allowed on the driver: {}. " + "(A common cause for this error could be the batch size - which is {}).", + (batch_size * descs_count), (MAX_DESCS_COUNT - 1), batch_size); + + descs_count = get_required_descriptor_count(transfer_sizes, static_cast(local_desc_page_size)); + } + + // Found desc_page_size and descs_count + const auto desc_page_size = static_cast(local_desc_page_size); + if (initial_desc_page_size != desc_page_size) { + LOGGER__WARNING("Desc page size value ({}) is not optimal for performance.", desc_page_size); + } + + if (is_circular) { + // The length of a descriptor list is always a power of 2. Therefore, on circular buffers the hw will have to + // access all descriptors. + descs_count = get_nearest_powerof_2(descs_count, MIN_DESCS_COUNT); + CHECK_AS_EXPECTED(descs_count <= MAX_DESCS_COUNT, HAILO_OUT_OF_DESCRIPTORS); + } + + return BufferSizesRequirements{descs_count, desc_page_size}; +} + +Expected BufferSizesRequirements::get_ccb_buffer_requirements_single_transfer(uint16_t batch_size, + uint32_t transfer_size, bool is_circular) +{ + const uint16_t desc_page_size = DEFAULT_DESC_PAGE_SIZE; + const auto desc_per_transfer = DIV_ROUND_UP(transfer_size, desc_page_size); + auto descs_count = desc_per_transfer * batch_size; + descs_count = std::max(descs_count, MIN_CCB_DESCRIPTORS_COUNT); + if (is_circular) { + // The first 12 channels in D2H CCB ("regular channels") requires that the amount of descriptors will be a power + // of 2. + // We can optimize it by checking that channel index is one of the last 4 channels ("enhanced channels"), or + // even allocate those indexes. + // Meanwhile however, we always use power of 2 + descs_count = get_nearest_powerof_2(descs_count, MIN_CCB_DESCRIPTORS_COUNT); + } + + return BufferSizesRequirements{descs_count, desc_page_size}; +} + + +uint16_t BufferSizesRequirements::find_initial_desc_page_size(const std::vector &transfer_sizes) +{ + const auto max_transfer_size = *std::max_element(transfer_sizes.begin(), transfer_sizes.end()); + // Note: If the pages pointed to by the descriptors are copied in their entirety, then DEFAULT_DESC_PAGE_SIZE + // is the optimal value. For transfer_sizes smaller than DEFAULT_DESC_PAGE_SIZE using smaller descriptor page + // sizes will save memory consuption without harming performance. In the case of nms for example, only one bbox + // is copied from each page. Hence, we'll use MIN_DESC_PAGE_SIZE for nms. + const uint16_t initial_desc_page_size = (DEFAULT_DESC_PAGE_SIZE > max_transfer_size) ? + static_cast(get_nearest_powerof_2(max_transfer_size, MIN_DESC_PAGE_SIZE)) : + DEFAULT_DESC_PAGE_SIZE; + if (DEFAULT_DESC_PAGE_SIZE != initial_desc_page_size) { + LOGGER__INFO("Using non-default initial_desc_page_size of {}, due to a small transfer size ({})", + initial_desc_page_size, max_transfer_size); + } + return initial_desc_page_size; +} + +uint32_t BufferSizesRequirements::get_required_descriptor_count(const std::vector &transfer_sizes, + uint16_t desc_page_size) +{ + uint32_t desc_count = 0; + for (auto &transfer_size : transfer_sizes) { + desc_count += DIV_ROUND_UP(transfer_size, desc_page_size); + } + // One extra descriptor is needed, because the amount of available descriptors is (desc_count - 1) + return desc_count + 1; +} + +} /* namespace vdma */ +} /* namespace hailort */ diff --git a/hailort/libhailort/src/vdma/memory/buffer_requirements.hpp b/hailort/libhailort/src/vdma/memory/buffer_requirements.hpp new file mode 100644 index 0000000..03568f8 --- /dev/null +++ b/hailort/libhailort/src/vdma/memory/buffer_requirements.hpp @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file buffer_requirements.hpp + * @brief Calculate all vdma buffer size requirements, including actual size, amount of descriptors and the actual desc + * count. + **/ + +#ifndef _HAILO_BUFFER_REQUIREMENTS_HPP_ +#define _HAILO_BUFFER_REQUIREMENTS_HPP_ + +#include "hailo/expected.hpp" + +#include +#include +#include + + +namespace hailort { +namespace vdma { + +class BufferSizesRequirements final { +public: + BufferSizesRequirements(uint32_t descs_count, uint16_t desc_page_size) : + m_descs_count(descs_count), + m_desc_page_size(desc_page_size) + { + assert(m_descs_count > 0); + assert(m_desc_page_size > 0); + } + + uint32_t descs_count() const { return m_descs_count; } + uint16_t desc_page_size() const { return m_desc_page_size; } + uint32_t buffer_size() const { return m_descs_count * m_desc_page_size; } + + static Expected get_sg_buffer_requirements_single_transfer(uint16_t max_desc_page_size, + uint16_t min_batch_size, uint16_t max_batch_size, uint32_t transfer_size, bool is_circular, + const bool force_default_page_size); + static Expected get_sg_buffer_requirements_multiple_transfers(uint16_t max_desc_page_size, + uint16_t batch_size, const std::vector &transfer_sizes, bool is_circular, + const bool force_default_page_size); + + static Expected get_ccb_buffer_requirements_single_transfer(uint16_t batch_size, + uint32_t transfer_size, bool is_circular); + +private: + static uint16_t find_initial_desc_page_size(const std::vector &transfer_sizes); + static uint32_t get_required_descriptor_count(const std::vector &transfer_sizes, uint16_t desc_page_size); + + const uint32_t m_descs_count; + const uint16_t m_desc_page_size; +}; + +} /* namespace vdma */ +} /* namespace hailort */ + +#endif /* _HAILO_BUFFER_REQUIREMENTS_HPP_ */ diff --git a/hailort/libhailort/src/vdma/memory/continuous_buffer.cpp b/hailort/libhailort/src/vdma/memory/continuous_buffer.cpp index bff2809..beac646 100644 --- a/hailort/libhailort/src/vdma/memory/continuous_buffer.cpp +++ b/hailort/libhailort/src/vdma/memory/continuous_buffer.cpp @@ -12,20 +12,16 @@ namespace hailort { namespace vdma { -// Minimum size of ccb buffers in descriptors, taken from the CCB spec. -#define MIN_CCB_DESCRIPTORS_COUNT (16) - -static uint32_t align(uint32_t size, uint32_t align) -{ - assert(is_powerof2(align)); - const uint32_t mask = align - 1; - return (size + mask) & ~mask; -} - Expected ContinuousBuffer::create(size_t size, HailoRTDriver &driver) { auto result = driver.vdma_continuous_buffer_alloc(size); - CHECK_EXPECTED(result, "Failed allocating continuous buffer, size {}", size); + /* Don't print error here since this might be expected error that the libhailoRT can recover from + (out of host memory). If it's not the case, there is a print in hailort_driver.cpp file */ + if (HAILO_OUT_OF_HOST_CMA_MEMORY == result.status()) { + return make_unexpected(result.status()); + } else { + CHECK_EXPECTED(result); + } uintptr_t handle = 0; uint64_t dma_address = 0; @@ -41,23 +37,6 @@ Expected ContinuousBuffer::create(size_t size, HailoRTDriver & return ContinuousBuffer(size, driver, handle, dma_address, mmap.release()); } -uint32_t ContinuousBuffer::get_buffer_size(uint32_t buffer_size) -{ - const uint16_t page_size = DEFAULT_DESC_PAGE_SIZE; - const auto aligned_buffer_size = align(buffer_size, page_size); - - const uint32_t min_buffer_size = page_size * MIN_CCB_DESCRIPTORS_COUNT; - return std::max(aligned_buffer_size, min_buffer_size); -} - -uint32_t ContinuousBuffer::get_buffer_size_desc_power2(uint32_t buffer_size) -{ - const uint16_t page_size = DEFAULT_DESC_PAGE_SIZE; - const auto descriptors_in_buffer = DIV_ROUND_UP(buffer_size, page_size); - const auto actual_descriptors_count = get_nearest_powerof_2(descriptors_in_buffer, MIN_CCB_DESCRIPTORS_COUNT); - return actual_descriptors_count * page_size; -} - ContinuousBuffer::~ContinuousBuffer() { if (0 != m_handle) { @@ -96,7 +75,7 @@ uint32_t ContinuousBuffer::descs_count() const return descriptors_in_buffer(m_size); } -hailo_status ContinuousBuffer::read(void *buf_dst, size_t count, size_t offset, bool /* should_sync */) +hailo_status ContinuousBuffer::read(void *buf_dst, size_t count, size_t offset) { CHECK((count + offset) <= m_size, HAILO_INSUFFICIENT_BUFFER, "Requested size {} from offset {} is more than the buffer size {}", count, offset, m_size); @@ -117,11 +96,10 @@ hailo_status ContinuousBuffer::write(const void *buf_src, size_t count, size_t o } Expected ContinuousBuffer::program_descriptors(size_t transfer_size, InterruptsDomain last_desc_interrupts_domain, - size_t desc_offset, bool is_circular) + size_t desc_offset) { (void)last_desc_interrupts_domain; (void)desc_offset; - (void)is_circular; // The descriptors in continuous mode are programmed by the hw, nothing to do here. return descriptors_in_buffer(transfer_size); diff --git a/hailort/libhailort/src/vdma/memory/continuous_buffer.hpp b/hailort/libhailort/src/vdma/memory/continuous_buffer.hpp index 58afefb..57b3ed5 100644 --- a/hailort/libhailort/src/vdma/memory/continuous_buffer.hpp +++ b/hailort/libhailort/src/vdma/memory/continuous_buffer.hpp @@ -22,10 +22,6 @@ class ContinuousBuffer final : public VdmaBuffer { public: static Expected create(size_t size, HailoRTDriver &driver); - static uint32_t get_buffer_size(uint32_t buffer_size); - // Get buffer size with the requirment that the amount of descriptors is a power of 2. - static uint32_t get_buffer_size_desc_power2(uint32_t buffer_size); - ContinuousBuffer(const ContinuousBuffer &) = delete; ContinuousBuffer& operator=(const ContinuousBuffer &) = delete; ContinuousBuffer& operator=(ContinuousBuffer &&) = delete; @@ -51,11 +47,11 @@ public: virtual uint16_t desc_page_size() const override; virtual uint32_t descs_count() const override; - virtual hailo_status read(void *buf_dst, size_t count, size_t offset, bool should_sync) override; + virtual hailo_status read(void *buf_dst, size_t count, size_t offset) override; virtual hailo_status write(const void *buf_src, size_t count, size_t offset) override; virtual Expected program_descriptors(size_t transfer_size, InterruptsDomain last_desc_interrupts_domain, - size_t desc_offset, bool is_circular) override; + size_t desc_offset) override; virtual hailo_status reprogram_device_interrupts_for_end_of_batch(size_t transfer_size, uint16_t batch_size, InterruptsDomain new_interrupts_domain) override; diff --git a/hailort/libhailort/src/vdma/memory/descriptor_list.cpp b/hailort/libhailort/src/vdma/memory/descriptor_list.cpp index 995a27c..a264098 100644 --- a/hailort/libhailort/src/vdma/memory/descriptor_list.cpp +++ b/hailort/libhailort/src/vdma/memory/descriptor_list.cpp @@ -8,7 +8,6 @@ **/ #include "vdma/memory/descriptor_list.hpp" -#include "vdma/memory/mapped_buffer_impl.hpp" #include "utils.h" @@ -32,117 +31,90 @@ namespace hailort { namespace vdma { -Expected DescriptorList::create(uint32_t desc_count, uint16_t requested_desc_page_size, +Expected DescriptorList::create(uint32_t desc_count, uint16_t desc_page_size, bool is_circular, HailoRTDriver &driver) { hailo_status status = HAILO_UNINITIALIZED; - auto desc_page_size_value = driver.calc_desc_page_size(requested_desc_page_size); - DescriptorList object(desc_count, driver, desc_page_size_value, status); + assert(desc_page_size <= driver.desc_max_page_size()); + + CHECK_AS_EXPECTED(desc_count <= MAX_DESCS_COUNT, HAILO_INVALID_ARGUMENT, + "descs_count {} must be smaller/equal to {}", desc_count, MAX_DESCS_COUNT); + + DescriptorList object(desc_count, desc_page_size, is_circular, driver, status); if (HAILO_SUCCESS != status) { return make_unexpected(status); } - // No need to initialize descripotrs here because they are initialized in driver in hailo_vdma_program_descriptor() + // No need to initialize descriptors here because they are initialized in driver in hailo_vdma_program_descriptor() return object; } -DescriptorList::DescriptorList(uint32_t desc_count, HailoRTDriver &driver, uint16_t desc_page_size, - hailo_status &status) : - m_mapped_list(), - m_count(desc_count), - m_depth(0), - m_desc_handle(0), - m_dma_address(0), +DescriptorList::DescriptorList(uint32_t desc_count, uint16_t desc_page_size, bool is_circular, HailoRTDriver &driver, + hailo_status &status) : + m_desc_list_info(), + m_is_circular(is_circular), m_driver(driver), m_desc_page_size(desc_page_size) { - if (!is_powerof2(desc_count)) { - LOGGER__ERROR("Descriptor count ({}) must be power of 2", desc_count); + if (m_is_circular && !is_powerof2(desc_count)) { + LOGGER__ERROR("Descriptor count ({}) for circular descriptor list must be power of 2", desc_count); status = HAILO_INVALID_ARGUMENT; return; } - auto depth = calculate_desc_list_depth(desc_count); - if (!depth) { - status = depth.status(); - return; - } - m_depth = depth.value(); - - auto desc_handle_phys_addr_pair = m_driver.descriptors_list_create(desc_count); - if (!desc_handle_phys_addr_pair) { - status = desc_handle_phys_addr_pair.status(); + auto desc_list_info = m_driver.descriptors_list_create(desc_count, m_is_circular); + if (!desc_list_info) { + status = desc_list_info.status(); return; } - m_desc_handle = desc_handle_phys_addr_pair->first; - m_dma_address = desc_handle_phys_addr_pair->second; - - auto mapped_list = MmapBuffer::create_file_map(desc_count * sizeof(VdmaDescriptor), m_driver.fd(), m_desc_handle); - if (!mapped_list) { - LOGGER__ERROR("Failed to memory map descriptors. desc handle: {:X}", m_desc_handle); - status = mapped_list.status(); - return; - } + m_desc_list_info = desc_list_info.release(); - m_mapped_list = mapped_list.release(); status = HAILO_SUCCESS; } DescriptorList::~DescriptorList() { - if (HAILO_SUCCESS != m_mapped_list.unmap()) { - LOGGER__ERROR("Failed to release descriptors mapping"); - } - - // Note: The descriptors_list is freed by the desc_handle (no need to use the phys_address to free) - if (0 != m_desc_handle) { - if(HAILO_SUCCESS != m_driver.descriptors_list_release(m_desc_handle)) { - LOGGER__ERROR("Failed to release descriptor list {}", m_desc_handle); + if (0 != m_desc_list_info.handle) { + auto status = m_driver.descriptors_list_release(m_desc_list_info); + if(HAILO_SUCCESS != status) { + LOGGER__ERROR("Failed to release descriptor list {} with status {}", m_desc_list_info.handle, status); } } } -DescriptorList::DescriptorList(DescriptorList &&other) noexcept : - m_mapped_list(std::move(other.m_mapped_list)), - m_count(std::move(other.m_count)), - m_depth(std::move(other.m_depth)), - m_desc_handle(std::exchange(other.m_desc_handle, 0)), - m_dma_address(std::exchange(other.m_dma_address, 0)), +DescriptorList::DescriptorList(DescriptorList &&other) noexcept : + m_desc_list_info(), + m_is_circular(std::move(other.m_is_circular)), m_driver(other.m_driver), - m_desc_page_size(other.m_desc_page_size) {} - -Expected DescriptorList::calculate_desc_list_depth(size_t count) + m_desc_page_size(other.m_desc_page_size) { - // Calculate log2 of m_count (by finding the offset of the MSB) - uint32_t depth = 0; - while (count >>= 1) { - ++depth; - } - CHECK_AS_EXPECTED(IS_FIT_IN_UINT8(depth), HAILO_INTERNAL_FAILURE, "Calculated desc_list_depth is too big: {}", depth); - return static_cast(depth); + m_desc_list_info.handle = std::exchange(other.m_desc_list_info.handle, 0); + m_desc_list_info.dma_address = std::exchange(other.m_desc_list_info.dma_address, 0); + m_desc_list_info.desc_count = std::move(other.m_desc_list_info.desc_count); + m_desc_list_info.user_address = std::exchange(other.m_desc_list_info.user_address, nullptr); } -hailo_status DescriptorList::configure_to_use_buffer(DmaMappedBuffer& buffer, ChannelId channel_id, uint32_t starting_desc) +hailo_status DescriptorList::configure_to_use_buffer(MappedBuffer& buffer, ChannelId channel_id, uint32_t starting_desc) { - const auto desc_list_capacity = m_desc_page_size * m_count; + const auto desc_list_capacity = m_desc_page_size * count(); CHECK(buffer.size() <= desc_list_capacity, HAILO_INVALID_ARGUMENT, "Can't bind a buffer larger than the descriptor list's capacity. Buffer size {}, descriptor list capacity {}", buffer.size(), desc_list_capacity); - return m_driver.descriptors_list_bind_vdma_buffer(m_desc_handle, buffer.pimpl->handle(), m_desc_page_size, + return m_driver.descriptors_list_bind_vdma_buffer(m_desc_list_info.handle, buffer.handle(), m_desc_page_size, channel_id.channel_index, starting_desc); } Expected DescriptorList::program_last_descriptor(size_t transfer_size, - InterruptsDomain last_desc_interrupts_domain, size_t desc_offset, bool is_circular) + InterruptsDomain last_desc_interrupts_domain, size_t desc_offset) { assert(transfer_size > 0); const auto required_descriptors = descriptors_in_buffer(transfer_size); // Required_descriptors + desc_offset can't reach m_count. - if ((!is_circular) && ((required_descriptors + desc_offset) > m_count)){ - LOGGER__ERROR("Requested transfer size ({}) result in more descriptors than available ({})", transfer_size, m_count); + if ((!m_is_circular) && ((required_descriptors + desc_offset) > count())){ + LOGGER__ERROR("Requested transfer size ({}) result in more descriptors than available ({})", transfer_size, count()); return make_unexpected(HAILO_OUT_OF_DESCRIPTORS); } @@ -150,7 +122,7 @@ Expected DescriptorList::program_last_descriptor(size_t transfer_size, /* write residue page with the remaining buffer size*/ auto resuide = transfer_size - (required_descriptors - 1) * m_desc_page_size; assert(IS_FIT_IN_UINT16(resuide)); - size_t last_desc = (desc_offset + required_descriptors - 1) & (m_count - 1); + size_t last_desc = (desc_offset + required_descriptors - 1) % count(); program_single_descriptor((*this)[last_desc], static_cast(resuide), last_desc_interrupts_domain); return std::move(static_cast(required_descriptors)); @@ -159,8 +131,8 @@ Expected DescriptorList::program_last_descriptor(size_t transfer_size, hailo_status DescriptorList::reprogram_descriptor_interrupts_domain(size_t desc_index, InterruptsDomain interrupts_domain) { - if (desc_index >= m_count){ - LOGGER__ERROR("Requested desc (index={}) exceeds the number of descriptors in the list ({})", desc_index, m_count); + if (desc_index >= count()){ + LOGGER__ERROR("Requested desc (index={}) exceeds the number of descriptors in the list ({})", desc_index, count()); return HAILO_OUT_OF_DESCRIPTORS; } reprogram_single_descriptor_interrupts_domain((*this)[desc_index], interrupts_domain); @@ -189,111 +161,6 @@ uint32_t DescriptorList::calculate_descriptors_count(uint32_t buffer_size, uint1 return get_nearest_powerof_2(descs_count, MIN_DESCS_COUNT); } -Expected> DescriptorList::get_desc_buffer_sizes_for_single_transfer( - const HailoRTDriver &driver, uint16_t min_batch_size, uint16_t max_batch_size, uint32_t transfer_size) -{ - // Note: If the pages pointed to by the descriptors are copied in their entirety, then DEFAULT_DESC_PAGE_SIZE - // is the optimal value. For transfer_sizes smaller than DEFAULT_DESC_PAGE_SIZE using smaller descriptor page - // sizes will save memory consuption without harming performance. In the case of nms for example, only one bbox - // is copied from each page. Hence, we'll use MIN_DESC_PAGE_SIZE for nms. - const uint32_t initial_desc_page_size = (DEFAULT_DESC_PAGE_SIZE > transfer_size) ? - get_nearest_powerof_2(transfer_size, MIN_DESC_PAGE_SIZE) : DEFAULT_DESC_PAGE_SIZE; - if (DEFAULT_DESC_PAGE_SIZE != initial_desc_page_size) { - LOGGER__INFO("Using non-default initial_desc_page_size of {}, due to a small transfer size ({})", - initial_desc_page_size, transfer_size); - } - CHECK_AS_EXPECTED(IS_FIT_IN_UINT16(initial_desc_page_size), HAILO_INTERNAL_FAILURE, - "Descriptor page size needs to fit in 16B"); - - return get_desc_buffer_sizes_for_single_transfer_impl(driver, min_batch_size, max_batch_size, transfer_size, - static_cast(initial_desc_page_size)); -} - -Expected> DescriptorList::get_desc_buffer_sizes_for_multiple_transfers( - const HailoRTDriver &driver, uint16_t batch_size, const std::vector &transfer_sizes) -{ - return get_desc_buffer_sizes_for_multiple_transfers_impl(driver, batch_size, transfer_sizes, - DEFAULT_DESC_PAGE_SIZE); -} - -Expected> DescriptorList::get_desc_buffer_sizes_for_single_transfer_impl( - const HailoRTDriver &driver, uint16_t min_batch_size, uint16_t max_batch_size, uint32_t transfer_size, - uint16_t initial_desc_page_size) -{ - auto results = DescriptorList::get_desc_buffer_sizes_for_multiple_transfers_impl(driver, min_batch_size, - {transfer_size}, initial_desc_page_size); - CHECK_EXPECTED(results); - - auto page_size = results->first; - - auto desc_count = std::min(MAX_DESCS_COUNT, - DescriptorList::calculate_descriptors_count(transfer_size, max_batch_size, page_size)); - - return std::make_pair(page_size, desc_count); -} - -Expected> DescriptorList::get_desc_buffer_sizes_for_multiple_transfers_impl( - const HailoRTDriver &driver, uint16_t batch_size, const std::vector &transfer_sizes, - uint16_t initial_desc_page_size) -{ - const uint16_t min_desc_page_size = driver.calc_desc_page_size(MIN_DESC_PAGE_SIZE); - const uint16_t max_desc_page_size = driver.calc_desc_page_size(MAX_DESC_PAGE_SIZE); - // Defined as uint32_t to prevent overflow (as we multiply it by two in each iteration of the while loop bellow) - uint32_t local_desc_page_size = driver.calc_desc_page_size(initial_desc_page_size); - CHECK_AS_EXPECTED(IS_FIT_IN_UINT16(local_desc_page_size), HAILO_INTERNAL_FAILURE, - "Descriptor page size needs to fit in 16B"); - CHECK_AS_EXPECTED(local_desc_page_size <= max_desc_page_size, HAILO_INTERNAL_FAILURE, - "Initial descriptor page size ({}) is larger than maximum descriptor page size ({})", - local_desc_page_size, max_desc_page_size); - CHECK_AS_EXPECTED(local_desc_page_size >= min_desc_page_size, HAILO_INTERNAL_FAILURE, - "Initial descriptor page size ({}) is smaller than minimum descriptor page size ({})", - local_desc_page_size, min_desc_page_size); - - uint32_t acc_desc_count = get_descriptors_count_needed(transfer_sizes, static_cast(local_desc_page_size)); - - // Too many descriptors; try a larger desc_page_size which will lead to less descriptors used - while ((acc_desc_count * batch_size) > (MAX_DESCS_COUNT - 1)) { - local_desc_page_size <<= 1; - - CHECK_AS_EXPECTED(local_desc_page_size <= max_desc_page_size, HAILO_OUT_OF_DESCRIPTORS, - "Network shapes and batch size exceeds driver descriptors capabilities. " - "Required descriptors count: {}, max allowed on the driver: {}. " - "(A common cause for this error could be the batch size - which is {}).", - (batch_size * acc_desc_count), (MAX_DESCS_COUNT - 1), batch_size); - - CHECK_AS_EXPECTED(IS_FIT_IN_UINT16(local_desc_page_size), HAILO_INTERNAL_FAILURE, - "Descriptor page size needs to fit in 16B"); - - acc_desc_count = get_descriptors_count_needed(transfer_sizes, static_cast(local_desc_page_size)); - } - - // Found desc_page_size and acc_desc_count - const auto desc_page_size = static_cast(local_desc_page_size); - - // Find descs_count - const auto descs_count = get_nearest_powerof_2(acc_desc_count, MIN_DESCS_COUNT); - CHECK_AS_EXPECTED(descs_count <= MAX_DESCS_COUNT, HAILO_OUT_OF_DESCRIPTORS); - - if (initial_desc_page_size != desc_page_size) { - LOGGER__WARNING("Desc page size value ({}) is not optimal for performance.", desc_page_size); - } - - return std::make_pair(desc_page_size, descs_count); -} - -uint32_t DescriptorList::get_descriptors_count_needed(const std::vector &transfer_sizes, - uint16_t desc_page_size) -{ - uint32_t desc_count = 0; - for (auto &transfer_size : transfer_sizes) { - desc_count += descriptors_in_buffer(transfer_size, desc_page_size); - } - - // One extra descriptor is needed, because the amount of available descriptors is (desc_count - 1) - desc_count += 1; - return desc_count; -} - uint32_t DescriptorList::get_interrupts_bitmask(InterruptsDomain interrupts_domain) { uint32_t host_bitmask = 0; @@ -353,7 +220,7 @@ void DescriptorList::reprogram_single_descriptor_interrupts_domain(VdmaDescripto // Set the IRQ control bits to zero // Make all edits to the local variable local_pagesize_desc_ctrl that is on the stack to save read/writes to DDR auto local_pagesize_desc_ctrl = (descriptor.PageSize_DescControl & ~DESC_IRQ_MASK); - + if (InterruptsDomain::NONE == interrupts_domain) { // Nothing else to do descriptor.PageSize_DescControl = local_pagesize_desc_ctrl; diff --git a/hailort/libhailort/src/vdma/memory/descriptor_list.hpp b/hailort/libhailort/src/vdma/memory/descriptor_list.hpp index 6800b32..25c3c35 100644 --- a/hailort/libhailort/src/vdma/memory/descriptor_list.hpp +++ b/hailort/libhailort/src/vdma/memory/descriptor_list.hpp @@ -11,11 +11,13 @@ #define _HAILO_VDMA_DESCRIPTOR_LIST_HPP_ #include "hailo/expected.hpp" -#include "hailo/dma_mapped_buffer.hpp" +#include "hailo/hailort_common.hpp" #include "common/utils.hpp" #include "vdma/channel/channel_id.hpp" +#include "vdma/memory/mapped_buffer.hpp" + #include "os/hailort_driver.hpp" #include "os/mmap_buffer.hpp" @@ -35,14 +37,13 @@ static_assert(DEFAULT_DESC_COUNT <= MAX_DESCS_COUNT && DEFAULT_DESC_COUNT >= MIN "DEFAULT_DESC_COUNT not in range"); // From PLDA's vDMA controller reference: -// - Addresses of pages pointed to by vDMA descriptors need to be on a 64B boundry. +// - Addresses of pages pointed to by vDMA descriptors need to be on a 64B boundary. // Hence, we require a minimum page size of 64B. // - G_PAGE_SIZE_MAX dictates the maximum desc page size: // max_page_size = 2 ^ (G_PAGE_SIZE_MAX - 1) // In our case max_page_size = 2 ^ (13 - 1) = 4096 -#define MIN_DESC_PAGE_SIZE (64u) -// TODO: Calculate from G_PAGE_SIZE_MAX (I.e. read the reg etc.) -#define MAX_DESC_PAGE_SIZE (4096u) +static constexpr uint16_t MIN_DESC_PAGE_SIZE = 64; +static constexpr uint16_t MAX_DESC_PAGE_SIZE = 4096; static constexpr uint16_t DEFAULT_DESC_PAGE_SIZE = 512; static_assert(is_powerof2(MIN_DESC_PAGE_SIZE), "MIN_DESC_PAGE_SIZE must be a power of 2"); @@ -53,15 +54,40 @@ static_assert(is_powerof2(DEFAULT_DESC_PAGE_SIZE), "DEFAULT_DESC_PAGE_SIZE must static_assert(DEFAULT_DESC_PAGE_SIZE > 0, "DEFAULT_DESC_PAGE_SIZE must be larger then 0"); -struct VdmaDescriptor +static constexpr auto DESCRIPTOR_STATUS_MASK = 0xFF; +static constexpr auto DESCRIPTOR_STATUS_DONE_BIT = 0; +static constexpr auto DESCRIPTOR_STATUS_ERROR_BIT = 1; + +struct VdmaDescriptor { + // Struct layout is taken from PLDA spec for vDMA, and cannot be changed. uint32_t PageSize_DescControl; uint32_t AddrL_rsvd_DataID; uint32_t AddrH; uint32_t RemainingPageSize_Status; + +#ifndef NDEBUG + // Easy accessors (only on debug since we mark DESC_STATUS_REQ and DESC_STATUS_REQ_ERR are set only on debug). + uint8_t status() const + { + return RemainingPageSize_Status & DESCRIPTOR_STATUS_MASK; + } + + bool is_done() const + { + return is_bit_set(status(), DESCRIPTOR_STATUS_DONE_BIT); + } + + bool is_error() const + { + return is_bit_set(status(), DESCRIPTOR_STATUS_ERROR_BIT); + } +#endif /* NDEBUG */ }; -enum class InterruptsDomain +static_assert(SIZE_OF_SINGLE_DESCRIPTOR == sizeof(VdmaDescriptor), "Invalid size of descriptor"); + +enum class InterruptsDomain { NONE = 0, DEVICE = 1 << 0, @@ -82,7 +108,7 @@ inline bool device_interuptes_enabled(InterruptsDomain interrupts_domain) class DescriptorList { public: - static Expected create(uint32_t desc_count, uint16_t requested_desc_page_size, + static Expected create(uint32_t desc_count, uint16_t desc_page_size, bool is_circular, HailoRTDriver &driver); ~DescriptorList(); @@ -92,25 +118,21 @@ public: DescriptorList(DescriptorList &&other) noexcept; DescriptorList &operator=(DescriptorList &&other) = delete; - uint8_t depth() const - { - return m_depth; - } - uint32_t count() const { - return m_count; + assert(m_desc_list_info.desc_count <= std::numeric_limits::max()); + return static_cast(m_desc_list_info.desc_count); } uint64_t dma_address() const { - return m_dma_address; + return m_desc_list_info.dma_address; } VdmaDescriptor& operator[](size_t i) { - assert(i < m_count); - return m_mapped_list[i]; + assert(i < count()); + return desc_list()[i]; } uint16_t desc_page_size() const @@ -120,23 +142,23 @@ public: uintptr_t handle() const { - return m_desc_handle; + return m_desc_list_info.handle; } uint16_t max_transfers(uint32_t transfer_size) { // We need to keep at least 1 free desc at all time. - return static_cast((m_count - 1) / descriptors_in_buffer(transfer_size)); + return static_cast((count() - 1) / descriptors_in_buffer(transfer_size)); } // Map descriptors starting at offset to the start of buffer, wrapping around the descriptor list as needed // On hailo8, we allow configuring buffer without specific channel index (default is INVALID_VDMA_CHANNEL_INDEX). - hailo_status configure_to_use_buffer(DmaMappedBuffer& buffer, ChannelId channel_id, uint32_t starting_desc = 0); + hailo_status configure_to_use_buffer(MappedBuffer& buffer, ChannelId channel_id, uint32_t starting_desc = 0); // All descritors are initialized to have size of m_desc_page_size - so all we do is set the last descritor for the // Interrupt - and then after transfer has finished clear the previously used first and last decsriptors. // This saves us write/ reads to the desscriptor list which is DMA memory. Expected program_last_descriptor(size_t transfer_size, InterruptsDomain last_desc_interrupts_domain, - size_t desc_offset, bool is_circular); + size_t desc_offset); void program_single_descriptor(VdmaDescriptor &descriptor, uint16_t page_size, InterruptsDomain interrupts_domain); hailo_status reprogram_descriptor_interrupts_domain(size_t desc_index, InterruptsDomain interrupts_domain); void clear_descriptor(const size_t desc_index); @@ -144,31 +166,19 @@ public: uint32_t descriptors_in_buffer(size_t buffer_size) const; static uint32_t descriptors_in_buffer(size_t buffer_size, uint16_t desc_page_size); static uint32_t calculate_descriptors_count(uint32_t buffer_size, uint16_t batch_size, uint16_t desc_page_size); - static Expected> get_desc_buffer_sizes_for_single_transfer(const HailoRTDriver &driver, - uint16_t min_batch_size, uint16_t max_batch_size, uint32_t transfer_size); - static Expected> get_desc_buffer_sizes_for_multiple_transfers(const HailoRTDriver &driver, - uint16_t batch_size, const std::vector &transfer_sizes); private: - DescriptorList(uint32_t desc_count, HailoRTDriver &driver, uint16_t desc_page_size, hailo_status &status); + DescriptorList(uint32_t desc_count, uint16_t desc_page_size, bool is_circular, HailoRTDriver &driver, + hailo_status &status); + + VdmaDescriptor *desc_list() { return reinterpret_cast(m_desc_list_info.user_address); } + uint32_t get_interrupts_bitmask(InterruptsDomain interrupts_domain); void reprogram_single_descriptor_interrupts_domain(VdmaDescriptor &descriptor, InterruptsDomain interrupts_domain); - static Expected calculate_desc_list_depth(size_t count); - // Note: initial_desc_page_size should be the optimal descriptor page size. - static Expected> get_desc_buffer_sizes_for_single_transfer_impl( - const HailoRTDriver &driver, uint16_t min_batch_size, uint16_t max_batch_size, uint32_t transfer_size, - uint16_t initial_desc_page_size); - static Expected> get_desc_buffer_sizes_for_multiple_transfers_impl( - const HailoRTDriver &driver, uint16_t batch_size, const std::vector &transfer_sizes, - uint16_t initial_desc_page_size); - static uint32_t get_descriptors_count_needed(const std::vector &transfer_sizes, - uint16_t desc_page_size); - - MmapBuffer m_mapped_list; - uint32_t m_count; - uint8_t m_depth; - uintptr_t m_desc_handle; - uint64_t m_dma_address; + + + DescriptorsListInfo m_desc_list_info; + const bool m_is_circular; HailoRTDriver &m_driver; const uint16_t m_desc_page_size; }; diff --git a/hailort/libhailort/src/vdma/memory/dma_able_buffer.cpp b/hailort/libhailort/src/vdma/memory/dma_able_buffer.cpp new file mode 100644 index 0000000..6b7d16b --- /dev/null +++ b/hailort/libhailort/src/vdma/memory/dma_able_buffer.cpp @@ -0,0 +1,267 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file dma_able_buffer.cpp + * @brief A Buffer that can be mapped to some device for dma operations. + * See hpp for more information. + **/ + +#include "dma_able_buffer.hpp" +#include "common/os_utils.hpp" + +#if defined(_MSC_VER) +#include "os/windows/virtual_alloc_guard.hpp" +#endif /* defined(_MSC_VER) */ + + +#if defined(__QNX__) +#include +#endif + +namespace hailort { +namespace vdma { + +#if defined(__linux__) || defined(_MSC_VER) + +// User buffer. This class does not own the buffer. +class UserAllocatedDmaAbleBuffer : public DmaAbleBuffer { +public: + static Expected create(void *user_address, size_t size) + { + CHECK_AS_EXPECTED(0 == (reinterpret_cast(user_address) % OsUtils::get_page_size()), + HAILO_INVALID_ARGUMENT, "User address mapped as dma must be paged aligned (page size {})", + OsUtils::get_page_size()); + + auto buffer = make_shared_nothrow(user_address, size); + CHECK_NOT_NULL_AS_EXPECTED(buffer, HAILO_OUT_OF_HOST_MEMORY); + + return std::static_pointer_cast(buffer); + } + + UserAllocatedDmaAbleBuffer(void *user_address, size_t size) : + m_size(size), + m_user_address(user_address) + {} + + virtual size_t size() const override { return m_size; } + virtual void *user_address() override { return m_user_address; } + virtual vdma_mapped_buffer_driver_identifier buffer_identifier() override { return HailoRTDriver::INVALID_DRIVER_BUFFER_HANDLE_VALUE; } + +private: + const size_t m_size; + void *m_user_address; +}; + + +#if defined(__linux__) +class PageAlignedDmaAbleBuffer : public DmaAbleBuffer { +public: + static Expected create(size_t size) + { + // Shared memory to allow python fork. + auto mmapped_buffer = MmapBuffer::create_shared_memory(size); + CHECK_EXPECTED(mmapped_buffer); + + auto buffer = make_shared_nothrow(mmapped_buffer.release()); + CHECK_NOT_NULL_AS_EXPECTED(buffer, HAILO_OUT_OF_HOST_MEMORY); + return std::static_pointer_cast(buffer); + } + + PageAlignedDmaAbleBuffer(MmapBuffer &&mmapped_buffer) : + m_mmapped_buffer(std::move(mmapped_buffer)) + {} + + virtual void* user_address() override { return m_mmapped_buffer.address(); } + virtual size_t size() const override { return m_mmapped_buffer.size(); } + virtual vdma_mapped_buffer_driver_identifier buffer_identifier() override { return HailoRTDriver::INVALID_DRIVER_BUFFER_HANDLE_VALUE; } + +private: + // Using mmap instead of aligned_alloc to enable MEM_SHARE flag - used for multi-process fork. + MmapBuffer m_mmapped_buffer; +}; + +#elif defined(_MSC_VER) +class PageAlignedDmaAbleBuffer : public DmaAbleBuffer { +public: + static Expected create(size_t size) + { + auto memory_guard = VirtualAllocGuard::create(size); + CHECK_EXPECTED(memory_guard); + + auto buffer = make_shared_nothrow(memory_guard.release()); + CHECK_NOT_NULL_AS_EXPECTED(buffer, HAILO_OUT_OF_HOST_MEMORY); + return std::static_pointer_cast(buffer); + } + + PageAlignedDmaAbleBuffer(VirtualAllocGuard &&memory_guard) : + m_memory_guard(std::move(memory_guard)) + {} + + virtual size_t size() const override { return m_memory_guard.size(); } + virtual void *user_address() override { return m_memory_guard.address(); } + virtual vdma_mapped_buffer_driver_identifier buffer_identifier() override { return HailoRTDriver::INVALID_DRIVER_BUFFER_HANDLE_VALUE; } + +private: + VirtualAllocGuard m_memory_guard; +}; +#else +#error "unsupported platform!" +#endif + +// Allocate low memory buffer using HailoRTDriver. +class DriverAllocatedDmaAbleBuffer : public DmaAbleBuffer { +public: + static Expected create(HailoRTDriver &driver, size_t size) + { + auto driver_buffer_handle = driver.vdma_low_memory_buffer_alloc(size); + CHECK_EXPECTED(driver_buffer_handle); + + auto mmapped_buffer = MmapBuffer::create_file_map(size, driver.fd(), driver_buffer_handle.value()); + if (!mmapped_buffer) { + auto free_status = driver.vdma_low_memory_buffer_free(driver_buffer_handle.value()); + if (HAILO_SUCCESS != free_status) { + LOGGER__ERROR("Failed free vdma low memory with status {}", free_status); + // Continue + } + + return make_unexpected(mmapped_buffer.status()); + } + CHECK_EXPECTED(mmapped_buffer); + + auto buffer = make_shared_nothrow(driver, driver_buffer_handle.value(), + mmapped_buffer.release()); + CHECK_NOT_NULL_AS_EXPECTED(buffer, HAILO_OUT_OF_HOST_MEMORY); + return std::static_pointer_cast(buffer); + } + + DriverAllocatedDmaAbleBuffer(HailoRTDriver &driver, vdma_mapped_buffer_driver_identifier driver_allocated_buffer_id, + MmapBuffer &&mmapped_buffer) : + m_driver(driver), + m_driver_allocated_buffer_id(driver_allocated_buffer_id), + m_mmapped_buffer(std::move(mmapped_buffer)) + {} + + DriverAllocatedDmaAbleBuffer(const DriverAllocatedDmaAbleBuffer &) = delete; + DriverAllocatedDmaAbleBuffer &operator=(const DriverAllocatedDmaAbleBuffer &) = delete; + + ~DriverAllocatedDmaAbleBuffer() + { + auto status = m_mmapped_buffer.unmap(); + if (HAILO_SUCCESS != status) { + LOGGER__ERROR("Failed to unmap buffer"); + // continue + } + + status = m_driver.vdma_low_memory_buffer_free(m_driver_allocated_buffer_id); + if (HAILO_SUCCESS != status) { + LOGGER__ERROR("Failed to free low memory buffer"); + // continue + } + } + + virtual void* user_address() override { return m_mmapped_buffer.address(); } + virtual size_t size() const override { return m_mmapped_buffer.size(); } + virtual vdma_mapped_buffer_driver_identifier buffer_identifier() override { return m_driver_allocated_buffer_id; } + +private: + HailoRTDriver &m_driver; + const vdma_mapped_buffer_driver_identifier m_driver_allocated_buffer_id; + + MmapBuffer m_mmapped_buffer; +}; + +Expected DmaAbleBuffer::create(size_t size, void *user_address) +{ + if (nullptr != user_address) { + return UserAllocatedDmaAbleBuffer::create(user_address, size); + } else { + return PageAlignedDmaAbleBuffer::create(size); + } +} + +Expected DmaAbleBuffer::create(HailoRTDriver &driver, size_t size, void *user_address) +{ + if ((nullptr == user_address) && driver.allocate_driver_buffer()) { + return DriverAllocatedDmaAbleBuffer::create(driver, size); + } else { + // The driver is not needed. + return create(size, user_address); + } +} + +#elif defined(__QNX__) + +class SharedMemoryDmaAbleBuffer : public DmaAbleBuffer { +public: + + static Expected create(size_t size) + { + auto shm_fd = open_shared_memory_fd(size); + CHECK_EXPECTED(shm_fd); + + auto mmapped_buffer = MmapBuffer::create_file_map_nocache(size, shm_fd.value(), 0); + CHECK_EXPECTED(mmapped_buffer); + + auto buffer = make_shared_nothrow(shm_fd.release(), mmapped_buffer.release()); + CHECK_NOT_NULL_AS_EXPECTED(buffer, HAILO_OUT_OF_HOST_MEMORY); + return std::static_pointer_cast(buffer); + } + + SharedMemoryDmaAbleBuffer(FileDescriptor &&shm_fd, MmapBuffer &&mmapped_buffer) : + m_shm_fd(std::move(shm_fd)), + m_mmapped_buffer(std::move(mmapped_buffer)) + {} + + virtual void *user_address() override { return m_mmapped_buffer.address(); } + virtual size_t size() const override { return m_mmapped_buffer.size(); } + virtual vdma_mapped_buffer_driver_identifier buffer_identifier() override { return m_shm_fd; } + +private: + + static Expected open_shared_memory_fd(size_t size) + { + static const int INVALID_FD = -1; + static const char* VDMA_BUFFER_TYPE_MEMORY_NAME = "/memory/below4G/ram/below1G"; + + FileDescriptor type_mem_fd = posix_typed_mem_open(VDMA_BUFFER_TYPE_MEMORY_NAME, O_RDWR, POSIX_TYPED_MEM_ALLOCATE); + CHECK_AS_EXPECTED(INVALID_FD != type_mem_fd, HAILO_FILE_OPERATION_FAILURE, + "Error getting fd from typed memory of type {}, errno {}", VDMA_BUFFER_TYPE_MEMORY_NAME, errno); + + FileDescriptor shm_fd = shm_open(SHM_ANON, O_RDWR | O_CREAT, 0777); + CHECK_AS_EXPECTED(INVALID_FD != shm_fd, HAILO_FILE_OPERATION_FAILURE, + "Error creating shm object, errno is: {}", errno); + + // backs the shared memory object with physical memory. After calling shm_tl, the type_mem_fd can be released. + int err = shm_ctl(shm_fd, SHMCTL_ANON | SHMCTL_TYMEM, (uint64_t)type_mem_fd, size); + CHECK_AS_EXPECTED(-1 != err, HAILO_FILE_OPERATION_FAILURE, + "Error backing shm object in physical memory, errno is: {}", errno); + + return shm_fd; + } + + // Initialization dependency + FileDescriptor m_shm_fd; + MmapBuffer m_mmapped_buffer; +}; + +Expected DmaAbleBuffer::create(size_t size, void *user_address) +{ + CHECK_AS_EXPECTED(nullptr == user_address, HAILO_NOT_SUPPORTED, "Mapping user address is not supported on QNX"); + return SharedMemoryDmaAbleBuffer::create(size); +} + +Expected DmaAbleBuffer::create(HailoRTDriver &driver, size_t size, void *user_address) +{ + // qnx don't need the driver for the allocation + (void)driver; + return DmaAbleBuffer::create(size, user_address); +} + +#else +#error "unsupported platform!" +#endif + +} /* namespace vdma */ +} /* namespace hailort */ diff --git a/hailort/libhailort/src/vdma/memory/dma_able_buffer.hpp b/hailort/libhailort/src/vdma/memory/dma_able_buffer.hpp new file mode 100644 index 0000000..66e1c70 --- /dev/null +++ b/hailort/libhailort/src/vdma/memory/dma_able_buffer.hpp @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file dma_able_buffer.hpp + * @brief A Buffer that can be mapped to some device for dma operations. + * There are several options for that buffer: + * 1. No allocation - The user gives its own buffer pointer and address. The buffer must be page aligned. + * 2. Normal allocation - page aligned allocation. This is the default option for linux and windows. + * 3. Driver allocation - On some platforms, default user mode memory allocation is not DMAAble. To overcome + * this, we allocate the buffer in a low memory using hailort driver. We check it querying + * HailoRTDriver::allocate_driver_buffer(). + * 4. QNX shared memory allocation - for qnx, in order to pass the driver to the resources manager, we need to + * create a shared memory object, and pass an handle to it in the mapping. TODO: HRT-10298 implement this. + **/ + +#ifndef _HAILO_DMA_ABLE_BUFFER_HPP_ +#define _HAILO_DMA_ABLE_BUFFER_HPP_ + +#include "hailo/expected.hpp" +#include "os/hailort_driver.hpp" +#include "os/mmap_buffer.hpp" + +namespace hailort { +namespace vdma { + +class DmaAbleBuffer; +using DmaAbleBufferPtr = std::shared_ptr; + +class DmaAbleBuffer { +public: + // If user_address is not nullptr, allocation is not needed. + static Expected create(size_t size, void *user_address = nullptr); + + // The driver is used only if driver.allocate_driver_buffer is true, and that the user address is nullptr. + static Expected create(HailoRTDriver &driver, size_t size, void *user_address = nullptr); + + DmaAbleBuffer() = default; + DmaAbleBuffer(DmaAbleBuffer &&other) = delete; + DmaAbleBuffer(const DmaAbleBuffer &other) = delete; + DmaAbleBuffer &operator=(const DmaAbleBuffer &other) = delete; + DmaAbleBuffer &operator=(DmaAbleBuffer &&other) = delete; + virtual ~DmaAbleBuffer() = default; + + virtual void* user_address() = 0; + virtual size_t size() const = 0; + virtual vdma_mapped_buffer_driver_identifier buffer_identifier() = 0; +}; + +} /* namespace vdma */ +} /* namespace hailort */ + +#endif /* _HAILO_DMA_ABLE_BUFFER_HPP_ */ diff --git a/hailort/libhailort/src/vdma/memory/dma_mapped_buffer.cpp b/hailort/libhailort/src/vdma/memory/dma_mapped_buffer.cpp deleted file mode 100644 index bd3a270..0000000 --- a/hailort/libhailort/src/vdma/memory/dma_mapped_buffer.cpp +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. - * Distributed under the MIT license (https://opensource.org/licenses/MIT) -**/ -/** - * @file vmda_mapped_buffer.cpp - * @brief Vdma mapped buffer implementation - **/ - -#include "hailo/dma_mapped_buffer.hpp" - -#include "vdma/memory/mapped_buffer_impl.hpp" -#include "vdma/vdma_device.hpp" - - -namespace hailort { - -static Expected convert_flags_to_driver_enum(hailo_vdma_buffer_direction_flags_t data_direction) -{ - static const auto BOTH_DIRECTIONS = HAILO_VDMA_BUFFER_DIRECTION_FLAGS_H2D | HAILO_VDMA_BUFFER_DIRECTION_FLAGS_D2H; - if ((data_direction & BOTH_DIRECTIONS) == BOTH_DIRECTIONS) { - return HailoRTDriver::DmaDirection::BOTH; - } - - if ((data_direction & HAILO_VDMA_BUFFER_DIRECTION_FLAGS_H2D) == HAILO_VDMA_BUFFER_DIRECTION_FLAGS_H2D) { - return HailoRTDriver::DmaDirection::H2D; - } - - if ((data_direction & HAILO_VDMA_BUFFER_DIRECTION_FLAGS_D2H) == HAILO_VDMA_BUFFER_DIRECTION_FLAGS_D2H) { - return HailoRTDriver::DmaDirection::D2H; - } - - return make_unexpected(HAILO_INVALID_ARGUMENT); -} - -// TODO: this should maybe be a vdevice (for mapping buffers to multiple devs) -// TODO: a helper function for the cast to VdmaDevice -Expected DmaMappedBuffer::create(size_t size, - hailo_vdma_buffer_direction_flags_t data_direction_flags, Device &device) -{ - static const auto ALLOCATE_BUFFER = nullptr; - return create(ALLOCATE_BUFFER, size, data_direction_flags, device); -} - -Expected DmaMappedBuffer::create_from_user_address(void *user_address, size_t size, - hailo_vdma_buffer_direction_flags_t data_direction_flags, Device &device) -{ - CHECK_ARG_NOT_NULL_AS_EXPECTED(user_address); - return create(user_address, size, data_direction_flags, device); -} - -Expected DmaMappedBuffer::create(void *user_address, size_t size, - hailo_vdma_buffer_direction_flags_t data_direction_flags, Device &device) -{ - const auto device_type = device.get_type(); - CHECK_AS_EXPECTED(((Device::Type::INTEGRATED == device_type) || (Device::Type::PCIE == device_type)), - HAILO_INVALID_ARGUMENT, "Invalid device type (expected integrated/pcie, received {})", device_type); - VdmaDevice *vdma_device = reinterpret_cast(&device); - - auto data_direction = convert_flags_to_driver_enum(data_direction_flags); - CHECK_EXPECTED(data_direction, "Invalid direction flags received {}", data_direction_flags); - - auto pimpl_exp = Impl::create(vdma_device->get_driver(), data_direction.release(), size, user_address); - CHECK_EXPECTED(pimpl_exp); - - auto pimpl = make_unique_nothrow(pimpl_exp.release()); - CHECK_NOT_NULL_AS_EXPECTED(pimpl, HAILO_OUT_OF_HOST_MEMORY); - - return DmaMappedBuffer(std::move(pimpl)); -} - -DmaMappedBuffer::DmaMappedBuffer(std::unique_ptr pimpl) : - pimpl(std::move(pimpl)) -{} - -// Note: These can't be defined in the header due to the use of pimpl (it'll cause a compilation error) -DmaMappedBuffer::DmaMappedBuffer(DmaMappedBuffer &&other) noexcept = default; -DmaMappedBuffer::~DmaMappedBuffer() = default; - -void *DmaMappedBuffer::user_address() -{ - return pimpl->user_address(); -} - -size_t DmaMappedBuffer::size() const -{ - return pimpl->size(); -} - -hailo_status DmaMappedBuffer::synchronize() -{ - static constexpr auto BUFFER_START = 0; - return pimpl->synchronize(BUFFER_START, size()); -} - -} /* namespace hailort */ diff --git a/hailort/libhailort/src/vdma/memory/mapped_buffer.cpp b/hailort/libhailort/src/vdma/memory/mapped_buffer.cpp new file mode 100644 index 0000000..b179fbb --- /dev/null +++ b/hailort/libhailort/src/vdma/memory/mapped_buffer.cpp @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) +**/ +/** + * @file mapped_buffer.cpp + * @brief Vdma mapped buffer implementation + **/ + +#include "mapped_buffer.hpp" + +#include "vdma/vdma_device.hpp" + + +namespace hailort { +namespace vdma { + +Expected MappedBuffer::create(HailoRTDriver &driver, + std::shared_ptr buffer, HailoRTDriver::DmaDirection data_direction) +{ + auto status = HAILO_UNINITIALIZED; + auto result = MappedBuffer(driver, buffer, data_direction, status); + CHECK_SUCCESS_AS_EXPECTED(status); + + return result; +} + +Expected MappedBuffer::create_shared(HailoRTDriver &driver, std::shared_ptr buffer, + HailoRTDriver::DmaDirection data_direction) +{ + auto dma_mapped_buffer = create(driver, buffer, data_direction); + CHECK_EXPECTED(dma_mapped_buffer); + + auto result = make_shared_nothrow(dma_mapped_buffer.release()); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); + + return result; +} + +Expected MappedBuffer::create(HailoRTDriver &driver, + HailoRTDriver::DmaDirection data_direction, size_t size, void *user_address) +{ + auto buffer = DmaAbleBuffer::create(driver, size, user_address); + CHECK_EXPECTED(buffer); + + return create(driver, buffer.release(), data_direction); +} + +Expected MappedBuffer::create_shared(HailoRTDriver &driver, + HailoRTDriver::DmaDirection data_direction, size_t size, void *user_address) +{ + auto dma_mapped_buffer = create(driver, data_direction, size, user_address); + CHECK_EXPECTED(dma_mapped_buffer); + + auto result = make_shared_nothrow(dma_mapped_buffer.release()); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); + + return result; +} + +MappedBuffer::MappedBuffer(HailoRTDriver &driver, std::shared_ptr buffer, + HailoRTDriver::DmaDirection data_direction, hailo_status &status) : + m_driver(driver), + m_buffer(buffer), + m_mapping_handle(HailoRTDriver::INVALID_DRIVER_VDMA_MAPPING_HANDLE_VALUE), + m_data_direction(data_direction) +{ + auto expected_handle = driver.vdma_buffer_map(m_buffer->user_address(), m_buffer->size(), m_data_direction, + m_buffer->buffer_identifier()); + if (!expected_handle) { + LOGGER__ERROR("Mapping address {} to dma failed", m_buffer->user_address()); + status = expected_handle.status(); + return; + } + + m_mapping_handle = expected_handle.release(); + status = HAILO_SUCCESS; +} + +MappedBuffer::~MappedBuffer() +{ + if (HailoRTDriver::INVALID_DRIVER_VDMA_MAPPING_HANDLE_VALUE != m_mapping_handle) { + m_driver.vdma_buffer_unmap(m_mapping_handle); + m_mapping_handle = HailoRTDriver::INVALID_DRIVER_VDMA_MAPPING_HANDLE_VALUE; + } +} + +MappedBuffer::MappedBuffer(MappedBuffer &&other) noexcept : + m_driver(other.m_driver), + m_buffer(std::move(other.m_buffer)), + m_mapping_handle(std::exchange(other.m_mapping_handle, HailoRTDriver::INVALID_DRIVER_VDMA_MAPPING_HANDLE_VALUE)), + m_data_direction(other.m_data_direction) +{} + +void* MappedBuffer::user_address() +{ + return m_buffer->user_address(); +} + +size_t MappedBuffer::size() const +{ + return m_buffer->size(); +} + +HailoRTDriver::VdmaBufferHandle MappedBuffer::handle() +{ + return m_mapping_handle; +} + +hailo_status MappedBuffer::synchronize(HailoRTDriver::DmaSyncDirection sync_direction) +{ + static constexpr auto BUFFER_START = 0; + return synchronize(BUFFER_START, size(), sync_direction); +} + +hailo_status MappedBuffer::synchronize(size_t offset, size_t count, HailoRTDriver::DmaSyncDirection sync_direction) +{ + CHECK(offset + count <= size(), HAILO_INVALID_ARGUMENT, + "Synchronizing {} bytes starting at offset {} will overflow (buffer size {})", + offset, count, size()); + return m_driver.vdma_buffer_sync(m_mapping_handle, sync_direction, offset, count); +} + +} /* namespace vdma */ +} /* namespace hailort */ diff --git a/hailort/libhailort/src/vdma/memory/mapped_buffer.hpp b/hailort/libhailort/src/vdma/memory/mapped_buffer.hpp new file mode 100644 index 0000000..2fc876b --- /dev/null +++ b/hailort/libhailort/src/vdma/memory/mapped_buffer.hpp @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. + * Distributed under the MIT license (https://opensource.org/licenses/MIT) + **/ +/** + * @file mapped_buffer.hpp + * @brief The mapped buffer that is continuous in virtual memory, but not on physical memory. + * We map the buffer to the IOMMU. + * + * The buffer can be used only with the help of a descriptors list that contains pointers to a physical + * continuous "dma pages". + * + * There are 2 options to allocated the buffer: + * 1. User mode allocation - the user mode calls `malloc` or `mmap` to allocate the buffer, then + * using HailoRTDriver we map the driver to the IOMMU (and pin the pages to avoid pagigs). + * This is the default option + * 2. Kernel mode allocation - on some systems, the user mode doesn't allocate the memory in a "dma-able" address, + * so we need to allocate the pages in driver. + **/ + +#ifndef _HAILO_DMA_MAPPED_BUFFER_HPP_ +#define _HAILO_DMA_MAPPED_BUFFER_HPP_ + +#include "hailo/expected.hpp" +#include "os/hailort_driver.hpp" +#include "vdma/memory/dma_able_buffer.hpp" + +#include + + +namespace hailort { +namespace vdma { + + +class MappedBuffer; +using MappedBufferPtr = std::shared_ptr; + +class MappedBuffer final +{ +public: + // Maps the given DmaAbleBuffer in the right direction. + static Expected create(HailoRTDriver &driver, std::shared_ptr buffer, + HailoRTDriver::DmaDirection data_direction); + static Expected create_shared(HailoRTDriver &driver, std::shared_ptr buffer, + HailoRTDriver::DmaDirection data_direction); + + // If user_address is nullptr, a buffer of size 'size' will be allocated and mapped to dma in 'data_direction' + // Otherwise, the buffer pointed to by user_address will be mapped to dma in 'data_direction' + static Expected create(HailoRTDriver &driver, HailoRTDriver::DmaDirection data_direction, + size_t size, void *user_address = nullptr); + static Expected create_shared(HailoRTDriver &driver, HailoRTDriver::DmaDirection data_direction, + size_t size, void *user_address = nullptr); + + + MappedBuffer(MappedBuffer &&other) noexcept; + MappedBuffer(const MappedBuffer &other) = delete; + MappedBuffer &operator=(const MappedBuffer &other) = delete; + MappedBuffer &operator=(MappedBuffer &&other) = delete; + ~MappedBuffer(); + + size_t size() const; + void *user_address(); + HailoRTDriver::VdmaBufferHandle handle(); + hailo_status synchronize(HailoRTDriver::DmaSyncDirection sync_direction); + // TODO: validate that offset is cache aligned (HRT-9811) + hailo_status synchronize(size_t offset, size_t count, HailoRTDriver::DmaSyncDirection sync_direction); + +private: + MappedBuffer(HailoRTDriver &driver, std::shared_ptr buffer, HailoRTDriver::DmaDirection data_direction, + hailo_status &status); + + HailoRTDriver &m_driver; + std::shared_ptr m_buffer; + HailoRTDriver::VdmaBufferHandle m_mapping_handle; + const HailoRTDriver::DmaDirection m_data_direction; +}; + +} /* namespace vdma */ +} /* namespace hailort */ + +#endif /* _HAILO_DMA_MAPPED_BUFFER_HPP_ */ \ No newline at end of file diff --git a/hailort/libhailort/src/vdma/memory/mapped_buffer_factory.cpp b/hailort/libhailort/src/vdma/memory/mapped_buffer_factory.cpp deleted file mode 100644 index 095243e..0000000 --- a/hailort/libhailort/src/vdma/memory/mapped_buffer_factory.cpp +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. - * Distributed under the MIT license (https://opensource.org/licenses/MIT) -**/ -/** - * @file mapped_buffer_factory.cpp - * @brief Static utility class for creating DmaMappedBuffers internally in hailort - **/ - -#include "vdma/memory/mapped_buffer_factory.hpp" -#include "vdma/memory/mapped_buffer_impl.hpp" - -namespace hailort -{ -namespace vdma -{ - -Expected MappedBufferFactory::create_mapped_buffer(size_t size, - HailoRTDriver::DmaDirection data_direction, HailoRTDriver &driver) -{ - auto pimpl_exp = DmaMappedBuffer::Impl::create(driver, data_direction, size); - CHECK_EXPECTED(pimpl_exp); - - auto pimpl = make_unique_nothrow(pimpl_exp.release()); - CHECK_NOT_NULL_AS_EXPECTED(pimpl, HAILO_OUT_OF_HOST_MEMORY); - return DmaMappedBuffer(std::move(pimpl)); -} - -} /* namespace vdma */ -} /* namespace hailort */ diff --git a/hailort/libhailort/src/vdma/memory/mapped_buffer_factory.hpp b/hailort/libhailort/src/vdma/memory/mapped_buffer_factory.hpp deleted file mode 100644 index 8cad51f..0000000 --- a/hailort/libhailort/src/vdma/memory/mapped_buffer_factory.hpp +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2023 Hailo Technologies Ltd. All rights reserved. - * Distributed under the MIT license (https://opensource.org/licenses/MIT) -**/ -/** - * @file mapped_buffer_factory.hpp - * @brief Static utility class for creating DmaMappedBuffers internally in hailort - **/ - -#ifndef _HAILO_MAPPED_BUFFER_FACTORY_HPP_ -#define _HAILO_MAPPED_BUFFER_FACTORY_HPP_ - -#include "hailo/hailort.h" -#include "hailo/dma_mapped_buffer.hpp" -#include "os/hailort_driver.hpp" - -namespace hailort -{ -namespace vdma -{ - -class MappedBufferFactory -{ -public: - MappedBufferFactory() = delete; - static Expected create_mapped_buffer(size_t size, - HailoRTDriver::DmaDirection data_direction, HailoRTDriver &driver); -}; - -} /* namespace vdma */ -} /* namespace hailort */ - -#endif /* _HAILO_MAPPED_BUFFER_FACTORY_HPP_ */ diff --git a/hailort/libhailort/src/vdma/memory/mapped_buffer_impl.cpp b/hailort/libhailort/src/vdma/memory/mapped_buffer_impl.cpp deleted file mode 100644 index 2d7193f..0000000 --- a/hailort/libhailort/src/vdma/memory/mapped_buffer_impl.cpp +++ /dev/null @@ -1,279 +0,0 @@ -/** - * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. - * Distributed under the MIT license (https://opensource.org/licenses/MIT) - **/ -/** - * @file mapped_buffer_impl.cpp - * @brief Dma mapped buffer pimpl class implementation - **/ -#include "mapped_buffer_impl.hpp" - -namespace hailort { - -#if defined(__linux__) || defined(_MSC_VER) - -Expected DmaMappedBuffer::Impl::create(HailoRTDriver &driver, - HailoRTDriver::DmaDirection data_direction, size_t size, void *user_address) -{ - if (nullptr != user_address) { - // User allocated buffer - create an empty MmapBuffer (it doesn't hold the buffer) - auto status = HAILO_UNINITIALIZED; - auto result = DmaMappedBuffer::Impl(HailoRTDriver::INVALID_DRIVER_BUFFER_HANDLE_VALUE, size, - data_direction, user_address, MmapBuffer(), driver, status); - CHECK_SUCCESS_AS_EXPECTED(status); - - return result; - } else if (driver.allocate_driver_buffer()) { - // Allocate buffer via driver - auto driver_buffer_handle = driver.vdma_low_memory_buffer_alloc(size); - CHECK_EXPECTED(driver_buffer_handle); - - uintptr_t driver_buff_handle = driver_buffer_handle.release(); - - auto mapped_buffer = MmapBuffer::create_file_map(size, driver.fd(), driver_buff_handle); - CHECK_EXPECTED(mapped_buffer); - - auto status = HAILO_UNINITIALIZED; - auto result = DmaMappedBuffer::Impl(driver_buff_handle, size, data_direction, mapped_buffer.release(), - driver, status); - CHECK_SUCCESS_AS_EXPECTED(status); - - return result; - } else { - // Standard userspace allocation - auto mapped_buffer = MmapBuffer::create_shared_memory(size); - CHECK_EXPECTED(mapped_buffer); - - auto status = HAILO_UNINITIALIZED; - auto result = DmaMappedBuffer::Impl(HailoRTDriver::INVALID_DRIVER_BUFFER_HANDLE_VALUE, size, - data_direction, mapped_buffer.release(), driver, status); - CHECK_SUCCESS_AS_EXPECTED(status); - - return result; - } -} - -DmaMappedBuffer::Impl::Impl(vdma_mapped_buffer_driver_identifier driver_allocated_buffer_id, - size_t size, HailoRTDriver::DmaDirection data_direction, void *user_address, - MmapBuffer &&mapped_buffer, HailoRTDriver &driver, hailo_status &status) : - m_driver(driver), - m_driver_allocated_buffer_id(driver_allocated_buffer_id), - m_mapping_handle(HailoRTDriver::INVALID_DRIVER_VDMA_MAPPING_HANDLE_VALUE), - m_mapped_buffer(std::move(mapped_buffer)), - m_size(size), - m_data_direction(data_direction), - m_user_address(user_address) -{ - if (m_mapped_buffer.is_mapped() && (m_user_address != m_mapped_buffer.address())) { - status = HAILO_INVALID_ARGUMENT; - return; - } - - auto expected_handle = driver.vdma_buffer_map(m_user_address, m_size, m_data_direction, - m_driver_allocated_buffer_id); - if (!expected_handle) { - status = expected_handle.status(); - return; - } - - m_mapping_handle = expected_handle.release(); - status = HAILO_SUCCESS; -} - -DmaMappedBuffer::Impl::Impl(vdma_mapped_buffer_driver_identifier driver_allocated_buffer_id, - size_t size, HailoRTDriver::DmaDirection data_direction, - MmapBuffer &&mapped_buffer, HailoRTDriver &driver, hailo_status &status) : - Impl(driver_allocated_buffer_id, size, data_direction, mapped_buffer.address(), std::move(mapped_buffer), driver, status) -{} - -DmaMappedBuffer::Impl::Impl(Impl &&other) noexcept : - m_driver(other.m_driver), - m_driver_allocated_buffer_id(std::exchange(other.m_driver_allocated_buffer_id, HailoRTDriver::INVALID_DRIVER_BUFFER_HANDLE_VALUE)), - m_mapping_handle(std::exchange(other.m_mapping_handle, HailoRTDriver::INVALID_DRIVER_VDMA_MAPPING_HANDLE_VALUE)), - m_mapped_buffer(std::move(other.m_mapped_buffer)), - m_size(std::move(other.m_size)), - m_data_direction(std::move(other.m_data_direction)), - m_user_address(std::move(other.m_user_address)) -{} - -DmaMappedBuffer::Impl::~Impl() -{ - if (HailoRTDriver::INVALID_DRIVER_VDMA_MAPPING_HANDLE_VALUE != m_mapping_handle) { - m_driver.vdma_buffer_unmap(m_mapping_handle); - m_mapping_handle = HailoRTDriver::INVALID_DRIVER_VDMA_MAPPING_HANDLE_VALUE; - } - - if (HailoRTDriver::INVALID_DRIVER_BUFFER_HANDLE_VALUE != m_driver_allocated_buffer_id) { - m_driver.vdma_low_memory_buffer_free(m_driver_allocated_buffer_id); - m_driver_allocated_buffer_id = HailoRTDriver::INVALID_DRIVER_BUFFER_HANDLE_VALUE; - } -} - -void* DmaMappedBuffer::Impl::user_address() -{ - return m_user_address; -} - -size_t DmaMappedBuffer::Impl::size() const -{ - return m_size; -} - -HailoRTDriver::VdmaBufferHandle DmaMappedBuffer::Impl::handle() -{ - return m_mapping_handle; -} - -hailo_status DmaMappedBuffer::Impl::synchronize(size_t offset, size_t count) -{ - CHECK(offset + count <= size(), HAILO_INVALID_ARGUMENT, - "Synchronizing {} bytes starting at offset {} will overflow (buffer size {})", - offset, count, size()); - return m_driver.vdma_buffer_sync(m_mapping_handle, m_data_direction, offset, count); -} - -#elif defined(__QNX__) - -#include - -const int DmaMappedBuffer::Impl::INVALID_FD = -1; -const shm_handle_t DmaMappedBuffer::Impl::INVALID_HANDLE = (shm_handle_t)-1; -const char* DmaMappedBuffer::Impl::VDMA_BUFFER_TYPE_MEMORY_NAME = "/memory/below4G/ram/below1G"; - -Expected DmaMappedBuffer::Impl::create(HailoRTDriver &driver, - HailoRTDriver::DmaDirection data_direction, size_t size, void *user_address) -{ - // TODO: HRT-9508 - CHECK_AS_EXPECTED(user_address == nullptr, HAILO_NOT_IMPLEMENTED, "User allocated buffers not supported on qnx"); - - // Destructor of type_mem_fd will close fd - FileDescriptor type_mem_fd(posix_typed_mem_open(VDMA_BUFFER_TYPE_MEMORY_NAME, O_RDWR, POSIX_TYPED_MEM_ALLOCATE)); - if (INVALID_FD == type_mem_fd) { - LOGGER__ERROR("Error getting fd from typed memory of type {}, errno {}\n", VDMA_BUFFER_TYPE_MEMORY_NAME, - errno); - return make_unexpected(HAILO_INTERNAL_FAILURE); - } - - vdma_mapped_buffer_driver_identifier driver_buff_handle; - driver_buff_handle.shm_fd = shm_open(SHM_ANON, O_RDWR | O_CREAT, 0777); - CHECK_AS_EXPECTED(INVALID_FD != driver_buff_handle.shm_fd, HAILO_INTERNAL_FAILURE, - "Error creating shm object, errno is: {}", errno); - - // backs the shared memory object with physical memory - int err = shm_ctl(driver_buff_handle.shm_fd, SHMCTL_ANON | SHMCTL_TYMEM, (uint64_t)type_mem_fd, - size); - if (-1 == err) { - LOGGER__ERROR("Error backing shm object in physical memory, errno is: {}", errno); - close(driver_buff_handle.shm_fd); - return make_unexpected(HAILO_INTERNAL_FAILURE); - } - - // Create shared memory handle to send to driver - err = shm_create_handle(driver_buff_handle.shm_fd, driver.resource_manager_pid(), O_RDWR, - &driver_buff_handle.shm_handle, 0); - if (0 != err) { - LOGGER__ERROR("Error creating shm object handle, errno is: {}", errno); - close(driver_buff_handle.shm_fd); - return make_unexpected(HAILO_INTERNAL_FAILURE); - } - - void *address = mmap(0, size, PROT_WRITE | PROT_READ | PROT_NOCACHE, MAP_SHARED, driver_buff_handle.shm_fd, 0); - if (MAP_FAILED == address) { - LOGGER__ERROR("Failed to mmap buffer with errno:{}", errno); - shm_delete_handle(driver_buff_handle.shm_handle); - close(driver_buff_handle.shm_fd); - return make_unexpected(HAILO_OUT_OF_HOST_MEMORY); - } - - hailo_status status = HAILO_UNINITIALIZED; - auto result = DmaMappedBuffer::Impl(address, size, data_direction, driver_buff_handle.shm_handle, - driver_buff_handle.shm_fd, driver, status); - if (HAILO_SUCCESS != status) { - LOGGER__ERROR("Failed to map buffer to vdma"); - munmap(address, size); - shm_delete_handle(driver_buff_handle.shm_handle); - close(driver_buff_handle.shm_fd); - return make_unexpected(status); - } - - return result; -} - -DmaMappedBuffer::Impl::Impl(void *addr, size_t size, HailoRTDriver::DmaDirection data_direction, - shm_handle_t shm_handle, int shm_fd, HailoRTDriver &driver, hailo_status &status) : - m_driver(driver), - m_address(addr), - m_size(size), - m_data_direction(data_direction) -{ - m_driver_allocated_buffer_id.shm_handle = shm_handle; - m_driver_allocated_buffer_id.shm_fd = shm_fd; - - auto expected_handle = driver.vdma_buffer_map(addr, size, data_direction, m_driver_allocated_buffer_id); - if (!expected_handle) { - status = expected_handle.status(); - return; - } - - m_mapping_handle = expected_handle.release(); - status = HAILO_SUCCESS; -} - -DmaMappedBuffer::Impl::Impl(Impl &&other) noexcept : - m_driver(other.m_driver), - m_address(std::exchange(other.m_address, nullptr)), - m_size(std::move(other.m_size)), - m_data_direction(std::move(other.m_data_direction)), - m_mapping_handle(std::exchange(other.m_mapping_handle, HailoRTDriver::INVALID_DRIVER_VDMA_MAPPING_HANDLE_VALUE)) -{ - m_driver_allocated_buffer_id.shm_handle = std::exchange(other.m_driver_allocated_buffer_id.shm_handle, INVALID_HANDLE); - m_driver_allocated_buffer_id.shm_fd = std::exchange(other.m_driver_allocated_buffer_id.shm_fd, INVALID_FD); -} - -DmaMappedBuffer::Impl::~Impl() -{ - if (HailoRTDriver::INVALID_DRIVER_VDMA_MAPPING_HANDLE_VALUE != m_mapping_handle) { - m_driver.vdma_buffer_unmap(m_mapping_handle); - m_mapping_handle = HailoRTDriver::INVALID_DRIVER_VDMA_MAPPING_HANDLE_VALUE; - } - - if (nullptr != m_address) { - if (0 != munmap(m_address, m_size)) { - LOGGER__ERROR("Error unmapping memory at address {}, Errno: {}", m_address, errno); - } - } - - if (INVALID_FD != m_driver_allocated_buffer_id.shm_fd) { - if (0 != close(m_driver_allocated_buffer_id.shm_fd)) { - LOGGER__ERROR("Error closing shared memory fd, Errno: {}", errno); - } - } -} - -void* DmaMappedBuffer::Impl::user_address() -{ - return m_address; -} -size_t DmaMappedBuffer::Impl::size() const -{ - return m_size; -} - -HailoRTDriver::VdmaBufferHandle DmaMappedBuffer::Impl::handle() -{ - return m_mapping_handle; -} - -hailo_status DmaMappedBuffer::Impl::synchronize(size_t offset, size_t count) -{ - CHECK(offset + count <= size(), HAILO_INVALID_ARGUMENT, - "Synchronizing {} bytes starting at offset {} will overflow (buffer size {})", - offset, count, size()); - return m_driver.vdma_buffer_sync(m_mapping_handle, m_data_direction, offset, count); -} - -#else -#error "unsupported platform!" -#endif // defined(__linux__) || defined(_MSC_VER) - -} /* namespace hailort */ \ No newline at end of file diff --git a/hailort/libhailort/src/vdma/memory/mapped_buffer_impl.hpp b/hailort/libhailort/src/vdma/memory/mapped_buffer_impl.hpp deleted file mode 100644 index 7643db8..0000000 --- a/hailort/libhailort/src/vdma/memory/mapped_buffer_impl.hpp +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright (c) 2020-2022 Hailo Technologies Ltd. All rights reserved. - * Distributed under the MIT license (https://opensource.org/licenses/MIT) - **/ -/** - * @file mapped_buffer_impl.hpp - * @brief Vdma mapped buffer pimpl class defintion - **/ -#ifndef _HAILO_VDMA_MAPPED_BUFFER_IMPL_HPP_ -#define _HAILO_VDMA_MAPPED_BUFFER_IMPL_HPP_ - -#include "hailo/dma_mapped_buffer.hpp" -#include "os/mmap_buffer.hpp" -#include "os/hailort_driver.hpp" -#include "hailo/expected.hpp" - -namespace hailort { - -#if defined(__linux__) || defined(_MSC_VER) - -class DmaMappedBuffer::Impl final { -public: - // If user_address is nullptr, a buffer of size 'size' will be allocated and mapped to dma in 'data_direction' - // Otherwise, the buffer pointed to by user_address will be mapped to dma in 'data_direction' - static Expected create(HailoRTDriver &driver, HailoRTDriver::DmaDirection data_direction, - size_t size, void *user_address = nullptr); - - Impl(Impl &&other) noexcept; - Impl(const Impl &other) = delete; - Impl &operator=(const Impl &other) = delete; - Impl &operator=(Impl &&other) = delete; - ~Impl(); - - void* user_address(); - size_t size() const; - HailoRTDriver::VdmaBufferHandle handle(); - // TODO: validate that offset is cache aligned (HRT-9811) - hailo_status synchronize(size_t offset, size_t count); - -private: - Impl(vdma_mapped_buffer_driver_identifier driver_allocated_buffer_id, size_t size, - HailoRTDriver::DmaDirection data_direction, void *user_address, MmapBuffer &&mapped_buffer, - HailoRTDriver &driver, hailo_status &status); - Impl(vdma_mapped_buffer_driver_identifier driver_allocated_buffer_id, size_t size, - HailoRTDriver::DmaDirection data_direction, MmapBuffer &&mapped_buffer, HailoRTDriver &driver, - hailo_status &status); - - HailoRTDriver &m_driver; - vdma_mapped_buffer_driver_identifier m_driver_allocated_buffer_id; - HailoRTDriver::VdmaBufferHandle m_mapping_handle; - MmapBuffer m_mapped_buffer; - const size_t m_size; - const HailoRTDriver::DmaDirection m_data_direction; - void *const m_user_address; -}; - -#elif defined(__QNX__) - -// TODO: merge qnx and non-qnx impls (HRT-9508) -class DmaMappedBuffer::Impl final { -public: - static Expected create(HailoRTDriver &driver, HailoRTDriver::DmaDirection data_direction, - size_t size, void *user_address = nullptr); - - Impl(const Impl &other) = delete; - Impl &operator=(const Impl &other) = delete; - Impl &operator=(Impl &&other) = delete; - Impl(Impl &&other) noexcept; - ~Impl(); - - void* user_address(); - size_t size() const; - HailoRTDriver::VdmaBufferHandle handle(); - hailo_status synchronize(size_t offset, size_t count); - -private: - Impl(void *addr, size_t size, HailoRTDriver::DmaDirection data_direction, - shm_handle_t shm_handle, int shm_fd, HailoRTDriver &driver, hailo_status &status); - - static const int INVALID_FD; - static const shm_handle_t INVALID_HANDLE; - static const char* VDMA_BUFFER_TYPE_MEMORY_NAME; - - HailoRTDriver &m_driver; - void *m_address; - const size_t m_size; - const HailoRTDriver::DmaDirection m_data_direction; - vdma_mapped_buffer_driver_identifier m_driver_allocated_buffer_id; - HailoRTDriver::VdmaBufferHandle m_mapping_handle; -}; - -#else -#error "unsupported platform!" -#endif // defined(__linux__) || defined(_MSC_VER) - -} /* namespace hailort */ - -#endif /* _HAILO_VDMA_MAPPED_BUFFER_IMPL_HPP_ */ \ No newline at end of file diff --git a/hailort/libhailort/src/vdma/memory/sg_buffer.cpp b/hailort/libhailort/src/vdma/memory/sg_buffer.cpp index 9d6b97b..28a6973 100644 --- a/hailort/libhailort/src/vdma/memory/sg_buffer.cpp +++ b/hailort/libhailort/src/vdma/memory/sg_buffer.cpp @@ -9,28 +9,23 @@ #include "vdma/memory/sg_buffer.hpp" #include "vdma/channel/channel_id.hpp" -#include "vdma/memory/mapped_buffer_factory.hpp" namespace hailort { namespace vdma { Expected SgBuffer::create(HailoRTDriver &driver, size_t size, uint32_t desc_count, uint16_t desc_page_size, - HailoRTDriver::DmaDirection data_direction, ChannelId channel_id) + bool is_circular, HailoRTDriver::DmaDirection data_direction, ChannelId channel_id) { CHECK_AS_EXPECTED(size <= (desc_count * desc_page_size), HAILO_INTERNAL_FAILURE, "Requested buffer size {} must be smaller than {}", size, (desc_count * desc_page_size)); CHECK_AS_EXPECTED((size % desc_page_size) == 0, HAILO_INTERNAL_FAILURE, "SgBuffer size must be a multiple of descriptors page size (size {})", size); - auto mapped_buffer_exp = MappedBufferFactory::create_mapped_buffer(size, - data_direction, driver); - CHECK_EXPECTED(mapped_buffer_exp); + auto mapped_buffer = MappedBuffer::create_shared(driver, data_direction, size); + CHECK_EXPECTED(mapped_buffer); - auto mapped_buffer = make_shared_nothrow(mapped_buffer_exp.release()); - CHECK_NOT_NULL_AS_EXPECTED(mapped_buffer, HAILO_OUT_OF_HOST_MEMORY); - - auto desc_list_exp = DescriptorList::create(desc_count, desc_page_size, driver); + auto desc_list_exp = DescriptorList::create(desc_count, desc_page_size, is_circular, driver); CHECK_EXPECTED(desc_list_exp); auto desc_list = make_shared_nothrow(desc_list_exp.release()); @@ -38,13 +33,13 @@ Expected SgBuffer::create(HailoRTDriver &driver, size_t size, uint32_t assert((desc_count * desc_page_size) <= std::numeric_limits::max()); - auto status = desc_list->configure_to_use_buffer(*mapped_buffer, channel_id); + auto status = desc_list->configure_to_use_buffer(*mapped_buffer.value(), channel_id); CHECK_SUCCESS_AS_EXPECTED(status); - return SgBuffer(mapped_buffer, desc_list); + return SgBuffer(mapped_buffer.release(), desc_list); } -SgBuffer::SgBuffer(std::shared_ptr mapped_buffer, std::shared_ptr desc_list) : +SgBuffer::SgBuffer(std::shared_ptr mapped_buffer, std::shared_ptr desc_list) : m_mapped_buffer(mapped_buffer), m_desc_list(desc_list) {} @@ -69,33 +64,15 @@ uint32_t SgBuffer::descs_count() const return static_cast(m_desc_list->count()); } -uint8_t SgBuffer::depth() const -{ - return m_desc_list->depth(); -} - -std::shared_ptr SgBuffer::get_desc_list() -{ - return m_desc_list; -} - -// TODO: Remove after HRT-7838 -void* SgBuffer::get_user_address() -{ - return m_mapped_buffer->user_address(); -} - -hailo_status SgBuffer::read(void *buf_dst, size_t count, size_t offset, bool should_sync) +hailo_status SgBuffer::read(void *buf_dst, size_t count, size_t offset) { CHECK(count + offset <= m_mapped_buffer->size(), HAILO_INSUFFICIENT_BUFFER); if (count == 0) { return HAILO_SUCCESS; } - if (should_sync) { - const auto status = m_mapped_buffer->synchronize(); - CHECK_SUCCESS(status, "Failed synching SgBuffer buffer on read"); - } + const auto status = m_mapped_buffer->synchronize(offset, count, HailoRTDriver::DmaSyncDirection::TO_HOST); + CHECK_SUCCESS(status, "Failed synching SgBuffer buffer on read"); const auto src_addr = static_cast(m_mapped_buffer->user_address()) + offset; memcpy(buf_dst, src_addr, count); @@ -112,16 +89,16 @@ hailo_status SgBuffer::write(const void *buf_src, size_t count, size_t offset) const auto dst_addr = static_cast(m_mapped_buffer->user_address()) + offset; std::memcpy(dst_addr, buf_src, count); - const auto status = m_mapped_buffer->synchronize(); + const auto status = m_mapped_buffer->synchronize(offset, count, HailoRTDriver::DmaSyncDirection::TO_DEVICE); CHECK_SUCCESS(status, "Failed synching SgBuffer buffer on write"); return HAILO_SUCCESS; } Expected SgBuffer::program_descriptors(size_t transfer_size, InterruptsDomain last_desc_interrupts_domain, - size_t desc_offset, bool is_circular) + size_t desc_offset) { - return m_desc_list->program_last_descriptor(transfer_size, last_desc_interrupts_domain, desc_offset, is_circular); + return m_desc_list->program_last_descriptor(transfer_size, last_desc_interrupts_domain, desc_offset); } hailo_status SgBuffer::reprogram_device_interrupts_for_end_of_batch(size_t transfer_size, uint16_t batch_size, diff --git a/hailort/libhailort/src/vdma/memory/sg_buffer.hpp b/hailort/libhailort/src/vdma/memory/sg_buffer.hpp index 36e2041..bb13157 100644 --- a/hailort/libhailort/src/vdma/memory/sg_buffer.hpp +++ b/hailort/libhailort/src/vdma/memory/sg_buffer.hpp @@ -7,19 +7,18 @@ * @brief Scatter-gather vdma buffer, from the user-mode point of view the buffer is continuous, * but not from the physical-memory point of view. * The sg buffer contains 2 parts: - * - DmaMappedBuffer - the actual buffer stores the data. - * - Descriptors list - each descritpor points to a single "dma page" in the DmaMappedBuffer. + * - MappedBuffer - the actual buffer stores the data. + * - Descriptors list - each descritpor points to a single "dma page" in the MappedBuffer. * The hw accept the descriptors list address and parses it to get the actual data. **/ #ifndef _HAILO_VDMA_SG_BUFFER_HPP_ #define _HAILO_VDMA_SG_BUFFER_HPP_ -#include "hailo/dma_mapped_buffer.hpp" - #include "os/hailort_driver.hpp" #include "vdma/memory/vdma_buffer.hpp" #include "vdma/memory/descriptor_list.hpp" +#include "vdma/memory/mapped_buffer.hpp" namespace hailort { @@ -28,7 +27,7 @@ namespace vdma { class SgBuffer final : public VdmaBuffer { public: static Expected create(HailoRTDriver &driver, size_t size, uint32_t desc_count, uint16_t desc_page_size, - HailoRTDriver::DmaDirection data_direction, vdma::ChannelId channel_id); + bool is_circular, HailoRTDriver::DmaDirection data_direction, vdma::ChannelId channel_id); virtual ~SgBuffer() = default; @@ -46,25 +45,20 @@ public: virtual uint64_t dma_address() const override; virtual uint16_t desc_page_size() const override; virtual uint32_t descs_count() const override; - uint8_t depth() const; - - std::shared_ptr get_desc_list(); - // TODO: Remove after HRT-7838 - void *get_user_address(); - virtual hailo_status read(void *buf_dst, size_t count, size_t offset, bool should_sync) override; + virtual hailo_status read(void *buf_dst, size_t count, size_t offset) override; virtual hailo_status write(const void *buf_src, size_t count, size_t offset) override; virtual Expected program_descriptors(size_t transfer_size, InterruptsDomain last_desc_interrupts_domain, - size_t desc_offset, bool is_circular) override; + size_t desc_offset) override; virtual hailo_status reprogram_device_interrupts_for_end_of_batch(size_t transfer_size, uint16_t batch_size, InterruptsDomain new_interrupts_domain) override; private: - SgBuffer(std::shared_ptr mapped_buffer, std::shared_ptr desc_list); + SgBuffer(std::shared_ptr mapped_buffer, std::shared_ptr desc_list); // Initialization Dependency: The descriptor list points into the mapped buffer so it must be freed before it - std::shared_ptr m_mapped_buffer; + std::shared_ptr m_mapped_buffer; std::shared_ptr m_desc_list; }; diff --git a/hailort/libhailort/src/vdma/memory/vdma_buffer.hpp b/hailort/libhailort/src/vdma/memory/vdma_buffer.hpp index 78171ab..d11393f 100644 --- a/hailort/libhailort/src/vdma/memory/vdma_buffer.hpp +++ b/hailort/libhailort/src/vdma/memory/vdma_buffer.hpp @@ -48,11 +48,11 @@ public: return static_cast(DIV_ROUND_UP(buffer_size, page_size)); } - virtual hailo_status read(void *buf_dst, size_t count, size_t offset, bool should_sync = true) = 0; + virtual hailo_status read(void *buf_dst, size_t count, size_t offset) = 0; virtual hailo_status write(const void *buf_src, size_t count, size_t offset) = 0; virtual Expected program_descriptors(size_t transfer_size, InterruptsDomain last_desc_interrupts_domain, - size_t desc_offset, bool is_circular) = 0; + size_t desc_offset) = 0; virtual hailo_status reprogram_device_interrupts_for_end_of_batch(size_t transfer_size, uint16_t batch_size, InterruptsDomain new_interrupts_domain) = 0; diff --git a/hailort/libhailort/src/vdma/pcie/pcie_device.cpp b/hailort/libhailort/src/vdma/pcie/pcie_device.cpp index 2fe9e93..88fe6b2 100644 --- a/hailort/libhailort/src/vdma/pcie/pcie_device.cpp +++ b/hailort/libhailort/src/vdma/pcie/pcie_device.cpp @@ -52,8 +52,10 @@ Expected> PcieDevice::create() // Take the first device auto scan_result = scan(); CHECK_EXPECTED(scan_result, "Failed scanning pcie devices"); - CHECK_AS_EXPECTED(scan_result->size() == 1, HAILO_INVALID_OPERATION, - "Expected only 1 PCIe device. Pass `hailo_pcie_device_info_t` to create a specific PCIe device"); + CHECK_AS_EXPECTED(scan_result->size() >= 1, HAILO_INVALID_OPERATION, + "There are no PCIe devices on the system"); + + // choose first device return create(scan_result->at(0)); } @@ -62,15 +64,11 @@ Expected> PcieDevice::create(const hailo_pcie_device auto device_info = find_device_info(pcie_device_info); CHECK_EXPECTED(device_info); - auto pcie_device_info_str = pcie_device_info_to_string(pcie_device_info); - CHECK_EXPECTED(pcie_device_info_str); - - auto driver = HailoRTDriver::create(device_info->dev_path); + auto driver = HailoRTDriver::create(*device_info); CHECK_EXPECTED(driver); hailo_status status = HAILO_UNINITIALIZED; - auto device = std::unique_ptr(new (std::nothrow) PcieDevice(driver.release(), pcie_device_info, status, - pcie_device_info_str.release())); + auto device = std::unique_ptr(new (std::nothrow) PcieDevice(driver.release(), status)); CHECK_AS_EXPECTED((nullptr != device), HAILO_OUT_OF_HOST_MEMORY); CHECK_SUCCESS_AS_EXPECTED(status, "Failed creating PcieDevice"); return device; @@ -130,10 +128,16 @@ Expected PcieDevice::pcie_device_info_to_string(const hailo_pcie_de return std::string(device_string); } -PcieDevice::PcieDevice(HailoRTDriver &&driver, const hailo_pcie_device_info_t &device_info, hailo_status &status, - const std::string &device_id) : - VdmaDevice::VdmaDevice(std::move(driver), Device::Type::PCIE, device_id), - m_device_info(device_info) +bool PcieDevice::pcie_device_infos_equal(const hailo_pcie_device_info_t &first, const hailo_pcie_device_info_t &second) +{ + const bool bdf_equal = (first.bus == second.bus) && (first.device == second.device) && (first.func == second.func); + const bool domain_equal = (HAILO_PCIE_ANY_DOMAIN == first.domain) || (HAILO_PCIE_ANY_DOMAIN == second.domain) || + (first.domain == second.domain); + return bdf_equal && domain_equal; +} + +PcieDevice::PcieDevice(HailoRTDriver &&driver, hailo_status &status) : + VdmaDevice::VdmaDevice(std::move(driver), Device::Type::PCIE) { if (driver.is_fw_loaded()) { status = update_fw_state(); @@ -146,8 +150,6 @@ PcieDevice::PcieDevice(HailoRTDriver &&driver, const hailo_pcie_device_info_t &d m_is_control_version_supported = false; } - m_device_id = device_id; - status = HAILO_SUCCESS; } @@ -176,11 +178,6 @@ hailo_status PcieDevice::direct_read_memory(uint32_t address, void *buffer, uint return m_driver.read_memory(HailoRTDriver::MemoryType::DIRECT_MEMORY, address, buffer, size); } -const char *PcieDevice::get_dev_id() const -{ - return m_device_id.c_str(); -} - hailo_status PcieDevice::reset_impl(CONTROL_PROTOCOL__reset_type_t reset_type) { hailo_status status = HAILO_UNINITIALIZED; @@ -210,7 +207,7 @@ hailo_status PcieDevice::reset_impl(CONTROL_PROTOCOL__reset_type_t reset_type) // TODO: fix logic with respect to is_expecting_response, implement wait_for_wakeup(); if (HAILO_SUCCESS == status) { status = Control::parse_and_validate_response(response_buffer, (uint32_t)(response_size), &header, - &payload, &request); + &payload, &request, *this); CHECK_SUCCESS(status); CHECK(is_expecting_response, HAILO_INTERNAL_FAILURE, "Recived valid response from FW for control who is not expecting one."); } else if ((HAILO_FW_CONTROL_FAILURE == status) && (!is_expecting_response)){ diff --git a/hailort/libhailort/src/vdma/pcie/pcie_device.hpp b/hailort/libhailort/src/vdma/pcie/pcie_device.hpp index ed909e0..6bca191 100644 --- a/hailort/libhailort/src/vdma/pcie/pcie_device.hpp +++ b/hailort/libhailort/src/vdma/pcie/pcie_device.hpp @@ -30,6 +30,7 @@ public: static Expected parse_pcie_device_info(const std::string &device_info_str, bool log_on_failure); static Expected pcie_device_info_to_string(const hailo_pcie_device_info_t &device_info); + static bool pcie_device_infos_equal(const hailo_pcie_device_info_t &first, const hailo_pcie_device_info_t &second); virtual ~PcieDevice() = default; @@ -55,20 +56,10 @@ public: void set_is_control_version_supported(bool value); virtual Expected get_architecture() const override; - const hailo_pcie_device_info_t get_device_info() const - { - return m_device_info; - } - virtual const char* get_dev_id() const override; - private: - PcieDevice(HailoRTDriver &&driver, const hailo_pcie_device_info_t &device_info, hailo_status &status, - const std::string &device_id); + PcieDevice(HailoRTDriver &&driver, hailo_status &status); static Expected find_device_info(const hailo_pcie_device_info_t &pcie_device_info); - - const hailo_pcie_device_info_t m_device_info; - std::string m_device_id; }; } /* namespace hailort */ diff --git a/hailort/libhailort/src/vdma/vdma_async_stream.cpp b/hailort/libhailort/src/vdma/vdma_async_stream.cpp index 1b55e27..22392f2 100644 --- a/hailort/libhailort/src/vdma/vdma_async_stream.cpp +++ b/hailort/libhailort/src/vdma/vdma_async_stream.cpp @@ -10,6 +10,7 @@ #include "hailo/hailort_common.hpp" #include "vdma/vdma_async_stream.hpp" +#include "common/os_utils.hpp" namespace hailort @@ -27,27 +28,47 @@ VdmaAsyncInputStream::VdmaAsyncInputStream(VdmaDevice &device, vdma::BoundaryCha return; } + if (channel->type() != vdma::BoundaryChannel::Type::ASYNC) { + LOGGER__ERROR("Can't create a async vdma stream with a non async channel. Received channel type {}", channel->type()); + status = HAILO_INVALID_ARGUMENT; + return; + } + status = HAILO_SUCCESS; } -Expected VdmaAsyncInputStream::sync_write_raw_buffer(const MemoryView &) +hailo_status VdmaAsyncInputStream::wait_for_async_ready(size_t transfer_size, std::chrono::milliseconds timeout) +{ + const bool STOP_IF_DEACTIVATED = true; + return m_channel->wait(transfer_size, timeout, STOP_IF_DEACTIVATED); +} + +Expected VdmaAsyncInputStream::get_async_max_queue_size() const { - return make_unexpected(HAILO_NOT_IMPLEMENTED); + return get_buffer_frames_size(); } -hailo_status VdmaAsyncInputStream::sync_write_all_raw_buffer_no_transform_impl(void *, size_t, size_t) +hailo_status VdmaAsyncInputStream::write_buffer_only(const MemoryView &, const std::function &) { - return HAILO_NOT_IMPLEMENTED; + LOGGER__ERROR("The write_buffer_only function is not supported by async streams"); + return HAILO_INVALID_OPERATION; } -hailo_status VdmaAsyncInputStream::wait_for_ready(size_t transfer_size, std::chrono::milliseconds timeout) +hailo_status VdmaAsyncInputStream::send_pending_buffer(const device_id_t &) { - return m_channel->wait(transfer_size, timeout); + LOGGER__ERROR("The send_pending_buffer function is not supported by async streams"); + return HAILO_INVALID_OPERATION; } -hailo_status VdmaAsyncInputStream::write_async(std::shared_ptr buffer, const TransferDoneCallback &user_callback, void *opaque) +hailo_status VdmaAsyncInputStream::write_async(TransferRequest &&transfer_request) { - return m_channel->transfer(buffer, user_callback, opaque); + return m_channel->transfer_async(std::move(transfer_request)); +} + +hailo_status VdmaAsyncInputStream::write_impl(const MemoryView &) +{ + LOGGER__ERROR("Sync write is not supported by async streams"); + return HAILO_INVALID_OPERATION; } /** Output stream **/ @@ -64,27 +85,246 @@ VdmaAsyncOutputStream::VdmaAsyncOutputStream(VdmaDevice &device, vdma::BoundaryC return; } + if (channel->type() != vdma::BoundaryChannel::Type::ASYNC) { + LOGGER__ERROR("Can't create an async vdma stream with a non async channel. Received channel type {}", channel->type()); + status = HAILO_INVALID_ARGUMENT; + return; + } + + status = HAILO_SUCCESS; +} + +hailo_status VdmaAsyncOutputStream::wait_for_async_ready(size_t transfer_size, std::chrono::milliseconds timeout) +{ + const bool STOP_IF_DEACTIVATED = true; + return m_channel->wait(transfer_size, timeout, STOP_IF_DEACTIVATED); +} + +Expected VdmaAsyncOutputStream::get_async_max_queue_size() const +{ + return get_buffer_frames_size(); +} + +hailo_status VdmaAsyncOutputStream::read_impl(MemoryView &) +{ + LOGGER__ERROR("Sync read is not supported by async streams"); + return HAILO_INVALID_OPERATION; +} + +hailo_status VdmaAsyncOutputStream::read_async(TransferRequest &&transfer_request) +{ + return m_channel->transfer_async(std::move(transfer_request)); +} + +/** Output nms stream **/ +VdmaAsyncOutputNmsStream::VdmaAsyncOutputNmsStream(VdmaDevice &device, vdma::BoundaryChannelPtr channel, + const LayerInfo &edge_layer, EventPtr core_op_activated_event, + uint16_t batch_size, std::chrono::milliseconds transfer_timeout, + hailo_stream_interface_t interface, hailo_status &status) : + VdmaOutputStreamBase(device, channel, edge_layer, core_op_activated_event, batch_size, + transfer_timeout, interface, status), + m_queue_max_size(channel->get_transfers_count_in_buffer(get_info().hw_frame_size)), + m_queue_mutex(), + m_abort_mutex(), + m_queue_cond(), + m_queue(), + m_stream_aborted(false), + m_should_quit(false), + m_worker_thread([this] { process_transfer_requests(); }) +{ + // Check status for base class c'tor + if (HAILO_SUCCESS != status) { + return; + } + + if (edge_layer.format.order != HAILO_FORMAT_ORDER_HAILO_NMS) { + // This shouldn't happen + LOGGER__ERROR("Can't create NMS vdma async output stream if edge layer order isn't NMS. Order received {}", + edge_layer.format.order); + status = HAILO_INTERNAL_FAILURE; + return; + } + + // TODO: after adding NMS single int, we can create an async channel for async nms output stream (HRT-10553) + if (channel->type() != vdma::BoundaryChannel::Type::BUFFERED) { + LOGGER__ERROR("Can't create an async nms vdma stream with a non buffered channel. Received channel type {}", channel->type()); + status = HAILO_INVALID_ARGUMENT; + return; + } + status = HAILO_SUCCESS; } -Expected VdmaAsyncOutputStream::sync_read_raw_buffer(MemoryView &) +VdmaAsyncOutputNmsStream::~VdmaAsyncOutputNmsStream() +{ + // VdmaAsyncOutputNmsStream::deactivate_stream() calls VdmaOutputStreamBase::deactivate_stream(). + // Because this dtor (i.e. ~VdmaAsyncOutputNmsStream()) is called before ~VdmaOutputStreamBase(), calling + // VdmaOutputStreamBase::deactivate_stream() inside VdmaAsyncOutputNmsStream::deactivate_stream() will work. + if (this->is_stream_activated) { + const auto status = deactivate_stream(); + if (HAILO_SUCCESS != status) { + LOGGER__ERROR("Failed to deactivate stream with error status {}", status); + } + } + + if (m_worker_thread.joinable()) { + signal_thread_quit(); + m_worker_thread.join(); + } +} + +hailo_status VdmaAsyncOutputNmsStream::wait_for_async_ready(size_t transfer_size, std::chrono::milliseconds timeout) { - return make_unexpected(HAILO_NOT_IMPLEMENTED); + CHECK(transfer_size == get_info().hw_frame_size, HAILO_INSUFFICIENT_BUFFER, + "On nms stream transfer_size should be {} (given size {})", get_info().hw_frame_size, transfer_size); + std::unique_lock lock(m_queue_mutex); + auto result = m_queue_cond.wait_for(lock, timeout, + [&]{ return m_should_quit || m_stream_aborted || (m_queue.size() < m_queue_max_size); }); + if (result) { + if (m_should_quit) { + return HAILO_STREAM_NOT_ACTIVATED; + } + return m_stream_aborted ? HAILO_STREAM_ABORTED_BY_USER : HAILO_SUCCESS; + } + return HAILO_TIMEOUT; +} + +Expected VdmaAsyncOutputNmsStream::get_async_max_queue_size() const +{ + return Expected(m_queue_max_size); +} + +hailo_status VdmaAsyncOutputNmsStream::read_async(TransferRequest &&transfer_request) +{ + { + std::lock_guard lock(m_queue_mutex); + CHECK(!m_stream_aborted, HAILO_STREAM_ABORTED_BY_USER); + CHECK(m_queue.size() < m_queue_max_size, HAILO_QUEUE_IS_FULL, "No space left in nms queue"); + + m_queue.emplace(std::move(transfer_request)); + } + m_queue_cond.notify_one(); + return HAILO_SUCCESS; +} + +hailo_status VdmaAsyncOutputNmsStream::read(MemoryView /* buffer */) +{ + // We need to override read() since VdmaAsyncOutputNmsStream impl's read_impl. This will cause read() to succeed, + // however this isn't desired for async streams. + LOGGER__ERROR("The read function is not supported by async streams"); + return HAILO_INVALID_OPERATION; +} + +hailo_status VdmaAsyncOutputNmsStream::abort() +{ + std::unique_lock lock(m_abort_mutex); + const auto status = VdmaOutputStreamBase::abort(); + CHECK_SUCCESS(status); + + m_stream_aborted = true; + + return HAILO_SUCCESS; +} + +hailo_status VdmaAsyncOutputNmsStream::clear_abort() +{ + std::unique_lock lock(m_abort_mutex); + const auto status = VdmaOutputStreamBase::clear_abort(); + CHECK_SUCCESS(status); + + m_stream_aborted = false; + + return HAILO_SUCCESS; +} + +hailo_status VdmaAsyncOutputNmsStream::read_impl(MemoryView &buffer) +{ + CHECK((buffer.size() % HailoRTCommon::HW_DATA_ALIGNMENT) == 0, HAILO_INVALID_ARGUMENT, + "Size must be aligned to {} (got {})", HailoRTCommon::HW_DATA_ALIGNMENT, buffer.size()); + + return m_channel->transfer_sync(buffer.data(), buffer.size(), m_transfer_timeout); +} + +hailo_status VdmaAsyncOutputNmsStream::deactivate_stream() +{ + std::unique_lock lock(m_queue_mutex); + + // abort is called because read_nms may block on a non-aborted channel + auto status = abort(); + CHECK_SUCCESS(status); + + // Now for every transfer processed in process_transfer_requests(), we'll pass HAILO_STREAM_ABORTED_BY_USER to the + // callback. + status = VdmaOutputStreamBase::deactivate_stream(); + CHECK_SUCCESS(status); + + // Block until all transfers have been emptied from the queue + auto result = m_queue_cond.wait_for(lock, m_transfer_timeout, [&]{ return m_queue.empty(); }); + CHECK(result, HAILO_TIMEOUT, "Timeout while deactivating async nms output stream"); + + return HAILO_SUCCESS; } -hailo_status VdmaAsyncOutputStream::read_all(MemoryView &) +hailo_status VdmaAsyncOutputNmsStream::activate_stream(uint16_t dynamic_batch_size, bool resume_pending_stream_transfers) { - return HAILO_NOT_IMPLEMENTED; + std::unique_lock lock(m_queue_mutex); + auto status = VdmaOutputStreamBase::activate_stream(dynamic_batch_size, resume_pending_stream_transfers); + CHECK_SUCCESS(status); + + status = clear_abort(); + CHECK_SUCCESS(status); + + return HAILO_SUCCESS; } -hailo_status VdmaAsyncOutputStream::wait_for_ready(size_t transfer_size, std::chrono::milliseconds timeout) +Expected VdmaAsyncOutputNmsStream::get_buffer_frames_size() const { - return m_channel->wait(transfer_size, timeout); + return Expected(m_queue_max_size); } -hailo_status VdmaAsyncOutputStream::read_async(std::shared_ptr buffer, const TransferDoneCallback &user_callback, void *opaque) +void VdmaAsyncOutputNmsStream::signal_thread_quit() { - return m_channel->transfer(buffer, user_callback, opaque); + { + std::unique_lock lock(m_queue_mutex); + m_should_quit = true; + } + m_queue_cond.notify_all(); +} + +void VdmaAsyncOutputNmsStream::process_transfer_requests() +{ + static const size_t FROM_START_OF_BUFFER = 0; + OsUtils::set_current_thread_name("ASYNC_NMS"); + + while (true) { + std::unique_lock lock(m_queue_mutex); + m_queue_cond.wait(lock, [&]{ return m_should_quit || !m_queue.empty(); }); + if (m_should_quit) { + break; + } + + auto transfer_request = m_queue.front(); + m_queue.pop(); + + lock.unlock(); + auto status = read_nms(transfer_request.buffer.data(), FROM_START_OF_BUFFER, transfer_request.buffer.size()); + lock.lock(); + + if (!this->is_stream_activated) { + LOGGER__TRACE("Stream is not active (previous status {})", status); + transfer_request.callback(HAILO_STREAM_ABORTED_BY_USER); + } else if (status != HAILO_SUCCESS) { + // TODO: timeout? stream aborted? (HRT-10513) + transfer_request.callback(status); + } else { + transfer_request.callback(HAILO_SUCCESS); + } + + lock.unlock(); + + // We notify after calling the callback, so that deactivate_stream() will block until the queue is empty + all callbacks have been called + m_queue_cond.notify_one(); + } } } /* namespace hailort */ diff --git a/hailort/libhailort/src/vdma/vdma_async_stream.hpp b/hailort/libhailort/src/vdma/vdma_async_stream.hpp index 5086c55..eb48c17 100644 --- a/hailort/libhailort/src/vdma/vdma_async_stream.hpp +++ b/hailort/libhailort/src/vdma/vdma_async_stream.hpp @@ -17,6 +17,12 @@ #include "vdma/vdma_stream_base.hpp" #include "vdma/vdma_device.hpp" #include "vdma/channel/async_channel.hpp" +#include "vdevice/scheduler/scheduled_core_op_state.hpp" + +#include +#include +#include +#include namespace hailort @@ -31,12 +37,16 @@ public: hailo_status &status); virtual ~VdmaAsyncInputStream() = default; - virtual hailo_status wait_for_ready(size_t transfer_size, std::chrono::milliseconds timeout) override; - virtual hailo_status write_async(std::shared_ptr buffer, const TransferDoneCallback &user_callback, void *opaque); + virtual hailo_status wait_for_async_ready(size_t transfer_size, std::chrono::milliseconds timeout) override; + virtual Expected get_async_max_queue_size() const override; -private: - virtual Expected sync_write_raw_buffer(const MemoryView &buffer) override; - virtual hailo_status sync_write_all_raw_buffer_no_transform_impl(void *buffer, size_t offset, size_t size) override; + virtual hailo_status write_buffer_only(const MemoryView &buffer, const std::function &should_cancel) override; + virtual hailo_status send_pending_buffer(const device_id_t &device_id) override; + + virtual hailo_status write_async(TransferRequest &&transfer_request) override; + +protected: + virtual hailo_status write_impl(const MemoryView &buffer) override; }; class VdmaAsyncOutputStream : public VdmaOutputStreamBase @@ -48,14 +58,53 @@ public: hailo_status &status); virtual ~VdmaAsyncOutputStream() = default; - virtual hailo_status wait_for_ready(size_t transfer_size, std::chrono::milliseconds timeout) override; - virtual hailo_status read_async(std::shared_ptr buffer, const TransferDoneCallback &user_callback, void *opaque = nullptr) override; + virtual hailo_status wait_for_async_ready(size_t transfer_size, std::chrono::milliseconds timeout) override; + virtual Expected get_async_max_queue_size() const override; -private: - virtual Expected sync_read_raw_buffer(MemoryView &buffer); - virtual hailo_status read_all(MemoryView &buffer) override; +protected: + virtual hailo_status read_impl(MemoryView &buffer) override; + virtual hailo_status read_async(TransferRequest &&transfer_request) override; }; +// NMS requires multiple reads from the device + parsing the output. Hence, a background thread is needed. +// This class opens a worker thread that processes nms transfers, signalling the user's callback upon completion. +// read_async adds transfer requests to a producer-consumer queue +class VdmaAsyncOutputNmsStream : public VdmaOutputStreamBase +{ +public: + VdmaAsyncOutputNmsStream(VdmaDevice &device, vdma::BoundaryChannelPtr channel, const LayerInfo &edge_layer, + EventPtr core_op_activated_event, uint16_t batch_size, + std::chrono::milliseconds transfer_timeout, hailo_stream_interface_t interface, + hailo_status &status); + virtual ~VdmaAsyncOutputNmsStream(); + + virtual hailo_status wait_for_async_ready(size_t transfer_size, std::chrono::milliseconds timeout) override; + virtual Expected get_async_max_queue_size() const override; + virtual hailo_status read(MemoryView buffer) override; + virtual hailo_status abort() override; + virtual hailo_status clear_abort() override; + +private: + virtual hailo_status read_impl(MemoryView &buffer) override; + virtual hailo_status read_async(TransferRequest &&transfer_request) override; + virtual hailo_status deactivate_stream() override; + virtual hailo_status activate_stream(uint16_t dynamic_batch_size, bool resume_pending_stream_transfers) override; + virtual Expected get_buffer_frames_size() const override; + + void signal_thread_quit(); + void process_transfer_requests(); + + // TODO: use SpscQueue (HRT-10554) + const size_t m_queue_max_size; + std::mutex m_queue_mutex; + std::mutex m_abort_mutex; + std::condition_variable m_queue_cond; + std::queue m_queue; + std::atomic_bool m_stream_aborted; + // m_should_quit is used to quit the thread (called on destruction) + bool m_should_quit; + std::thread m_worker_thread; +}; } /* namespace hailort */ diff --git a/hailort/libhailort/src/vdma/vdma_config_core_op.cpp b/hailort/libhailort/src/vdma/vdma_config_core_op.cpp index 88c0968..c0f8020 100644 --- a/hailort/libhailort/src/vdma/vdma_config_core_op.cpp +++ b/hailort/libhailort/src/vdma/vdma_config_core_op.cpp @@ -40,7 +40,7 @@ hailo_status VdmaConfigCoreOp::activate_impl(uint16_t dynamic_batch_size, bool r m_active_core_op_holder.set(*this); - status = m_resources_manager->set_inter_context_channels_dynamic_batch_size(dynamic_batch_size); + status = m_resources_manager->set_dynamic_batch_size(dynamic_batch_size); CHECK_SUCCESS(status, "Failed to set inter-context channels dynamic batch size."); status = m_resources_manager->enable_state_machine(dynamic_batch_size); @@ -165,4 +165,9 @@ Expected VdmaConfigCoreOp::get_boundary_vdma_channel_b return m_resources_manager->get_boundary_vdma_channel_by_stream_name(stream_name); } +Expected VdmaConfigCoreOp::run_hw_infer_estimator() +{ + return m_resources_manager->run_hw_only_infer(); +} + } /* namespace hailort */ diff --git a/hailort/libhailort/src/vdma/vdma_config_core_op.hpp b/hailort/libhailort/src/vdma/vdma_config_core_op.hpp index 740d4a3..821ff92 100644 --- a/hailort/libhailort/src/vdma/vdma_config_core_op.hpp +++ b/hailort/libhailort/src/vdma/vdma_config_core_op.hpp @@ -73,6 +73,7 @@ public: virtual hailo_status set_scheduler_timeout(const std::chrono::milliseconds &timeout, const std::string &network_name) override; virtual hailo_status set_scheduler_threshold(uint32_t threshold, const std::string &network_name) override; virtual hailo_status set_scheduler_priority(uint8_t priority, const std::string &network_name) override; + virtual Expected run_hw_infer_estimator() override; virtual ~VdmaConfigCoreOp() = default; VdmaConfigCoreOp(const VdmaConfigCoreOp &other) = delete; diff --git a/hailort/libhailort/src/vdma/vdma_config_manager.cpp b/hailort/libhailort/src/vdma/vdma_config_manager.cpp index 0bf8d4f..1d96c51 100644 --- a/hailort/libhailort/src/vdma/vdma_config_manager.cpp +++ b/hailort/libhailort/src/vdma/vdma_config_manager.cpp @@ -14,7 +14,7 @@ namespace hailort { hailo_status VdmaConfigManager::switch_core_op(std::shared_ptr current_active_core_op, - std::shared_ptr next_core_op, const uint16_t batch_size, bool resume_pending_stream_transfers) + std::shared_ptr next_core_op, const uint16_t batch_size, const bool resume_pending_stream_transfers) { static const auto RESET_NN_CONFIG = false; CHECK((nullptr != current_active_core_op) || (nullptr != next_core_op), HAILO_INVALID_ARGUMENT); @@ -33,6 +33,7 @@ hailo_status VdmaConfigManager::switch_core_op(std::shared_ptr auto status = current_active_core_op->deactivate_host_resources(); CHECK_SUCCESS(status, "Failed deactivating current core-op"); + // TODO HRT-10799 Fix when enabling batch switch flow for hailo15 // TODO: In mercury we need to reset after deactivate. This will be fixed in MSW-762 and the "if" will be removed // when we make the nn_manager responsible to reset the nn-core. if (Device::Type::INTEGRATED == current_active_core_op->get_resources_manager()->get_device().get_type()) { @@ -52,4 +53,12 @@ hailo_status VdmaConfigManager::switch_core_op(std::shared_ptr return HAILO_SUCCESS; } +hailo_status VdmaConfigManager::deactivate_core_op(std::shared_ptr current_active_core_op) +{ + static const auto RESUME_PENDING_STREAM_TRANSFERS = true; + static const uint16_t DEACTIVATE_BATCH_SIZE = 0; + const std::shared_ptr DEACTIVATE_NEXT_CORE_OP = nullptr; + return switch_core_op(current_active_core_op, DEACTIVATE_NEXT_CORE_OP, DEACTIVATE_BATCH_SIZE, RESUME_PENDING_STREAM_TRANSFERS); +} + } /* namespace hailort */ diff --git a/hailort/libhailort/src/vdma/vdma_config_manager.hpp b/hailort/libhailort/src/vdma/vdma_config_manager.hpp index c42b6a8..c20b1e0 100644 --- a/hailort/libhailort/src/vdma/vdma_config_manager.hpp +++ b/hailort/libhailort/src/vdma/vdma_config_manager.hpp @@ -27,7 +27,9 @@ public: VdmaConfigManager() = delete; static hailo_status switch_core_op(std::shared_ptr current_active_core_op, - std::shared_ptr next_core_op, const uint16_t batch_size, bool resume_pending_stream_transfers); + std::shared_ptr next_core_op, const uint16_t batch_size, const bool resume_pending_stream_transfers); + + static hailo_status deactivate_core_op(std::shared_ptr current_active_core_op); }; } /* namespace hailort */ diff --git a/hailort/libhailort/src/vdma/vdma_device.cpp b/hailort/libhailort/src/vdma/vdma_device.cpp index c560a65..a3791a5 100644 --- a/hailort/libhailort/src/vdma/vdma_device.cpp +++ b/hailort/libhailort/src/vdma/vdma_device.cpp @@ -31,11 +31,11 @@ static constexpr std::chrono::milliseconds DEFAULT_TIMEOUT(1000); static constexpr std::chrono::milliseconds DEFAULT_TIMEOUT(50000); #endif /* ifndef HAILO_EMULATOR */ -VdmaDevice::VdmaDevice(HailoRTDriver &&driver, Device::Type type, const std::string &device_id) : +VdmaDevice::VdmaDevice(HailoRTDriver &&driver, Device::Type type) : DeviceBase::DeviceBase(type), m_driver(std::move(driver)), m_is_configured(false) { - activate_notifications(device_id); + activate_notifications(get_dev_id()); } Expected> VdmaDevice::create(const std::string &device_id) @@ -109,6 +109,26 @@ hailo_status VdmaDevice::fw_interact_impl(uint8_t *request_buffer, size_t reques return HAILO_SUCCESS; } +hailo_status VdmaDevice::clear_configured_apps() +{ + static const auto DONT_KEEP_NN_CONFIG_DURING_RESET = false; + auto status = Control::reset_context_switch_state_machine(*this, DONT_KEEP_NN_CONFIG_DURING_RESET); + CHECK_SUCCESS(status); + + // In case of mercury need to reset nn core before activating network group to clear prior nn core state + if (Device::Type::INTEGRATED == get_type()) { + // On core device, the nn_manager is not responsible to reset the nn-core so + // we use the SCU control for that. + status = m_driver.reset_nn_core(); + CHECK_SUCCESS(status); + } + + status = Control::clear_configured_apps(*this); + CHECK_SUCCESS(status, "Failed to clear configured network groups with status {}", status); + + return HAILO_SUCCESS; +} + Expected VdmaDevice::add_hef(Hef &hef, const NetworkGroupsParamsMap &configure_params) { auto status = mark_as_used(); @@ -118,21 +138,9 @@ Expected VdmaDevice::add_hef(Hef &hef, const Netwo // TODO: Do we need this control after fixing HRT-7519? // Reset context_switch state machine - it may have been in an active state if a previous VdmaDevice // wasn't dtor'd (due to SIGKILL for example) - static const auto REMOVE_NN_CONFIG_DURING_RESET = false; - status = Control::reset_context_switch_state_machine(*this, REMOVE_NN_CONFIG_DURING_RESET); + status = clear_configured_apps(); CHECK_SUCCESS_AS_EXPECTED(status); - // In case of mercury need to reset nn core before activating network group to clear prior nn core state - if (Device::Type::INTEGRATED == get_type()) { - // On core device, the nn_manager is not responsible to reset the nn-core so - // we use the SCU control for that. - status = reset(HAILO_RESET_DEVICE_MODE_NN_CORE); - CHECK_SUCCESS_AS_EXPECTED(status); - } - - status = Control::clear_configured_apps(*this); - CHECK_SUCCESS_AS_EXPECTED(status, "Failed to clear configured network groups with status {}", status); - assert(nullptr == m_vdma_interrupts_dispatcher); auto interrupts_dispatcher = vdma::InterruptsDispatcher::create(std::ref(m_driver)); CHECK_EXPECTED(interrupts_dispatcher); @@ -185,8 +193,8 @@ Expected> VdmaDevice::create_configured_ m_core_ops.emplace_back(core_op_ptr); // TODO: HRT-8875 - auto net_flow_ops = hef.pimpl->post_process_ops(core_op_metadata->core_op_name()); - auto network_group_expected = ConfiguredNetworkGroupBase::create(config_params, std::move(core_ops), std::move(net_flow_ops)); + auto metadata = hef.pimpl->network_group_metadata(core_op_metadata->core_op_name()); + auto network_group_expected = ConfiguredNetworkGroupBase::create(config_params, std::move(core_ops), std::move(metadata)); CHECK_EXPECTED(network_group_expected); auto network_group_ptr = network_group_expected.release(); @@ -215,11 +223,6 @@ hailo_reset_device_mode_t VdmaDevice::get_default_reset_mode() return HAILO_RESET_DEVICE_MODE_SOFT; } -uint16_t VdmaDevice::get_default_desc_page_size() const -{ - return m_driver.calc_desc_page_size(vdma::DEFAULT_DESC_PAGE_SIZE); -} - hailo_status VdmaDevice::mark_as_used() { return m_driver.mark_as_used(); @@ -238,9 +241,9 @@ VdmaDevice::~VdmaDevice() LOGGER__WARNING("Stopping notification thread ungracefully"); } if (m_is_configured) { - status = Control::clear_configured_apps(*this); + status = clear_configured_apps(); if (HAILO_SUCCESS != status) { - LOGGER__ERROR("Failed to clear configured core-ops with status {}", status); + LOGGER__WARNING("clear configured apps ended with status {}", status); } } } @@ -334,10 +337,7 @@ Expected>> VdmaDevice::create_core_o // TODO: decide about core_op names - align with the Compiler auto core_op_metadata = hef.pimpl->get_core_op_metadata(network_group_name, partial_clusters_layout_bitmap); CHECK_EXPECTED(core_op_metadata); - - auto core_op_metadata_ptr = make_shared_nothrow(core_op_metadata.release()); - CHECK_AS_EXPECTED(nullptr != core_op_metadata_ptr, HAILO_OUT_OF_HOST_MEMORY); - core_ops_metadata_ptrs.emplace_back(core_op_metadata_ptr); + core_ops_metadata_ptrs.emplace_back(core_op_metadata.release()); } return core_ops_metadata_ptrs; diff --git a/hailort/libhailort/src/vdma/vdma_device.hpp b/hailort/libhailort/src/vdma/vdma_device.hpp index 5aea085..6d5eea7 100644 --- a/hailort/libhailort/src/vdma/vdma_device.hpp +++ b/hailort/libhailort/src/vdma/vdma_device.hpp @@ -32,19 +32,24 @@ public: virtual hailo_status wait_for_wakeup() override; virtual void increment_control_sequence() override; virtual hailo_reset_device_mode_t get_default_reset_mode() override; - uint16_t get_default_desc_page_size() const; - hailo_status mark_as_used(); virtual Expected read_log(MemoryView &buffer, hailo_cpu_id_t cpu_id) override; - HailoRTDriver &get_driver() { + HailoRTDriver &get_driver() + { return std::ref(m_driver); }; + virtual const char* get_dev_id() const override final + { + // m_driver.device_id() is reference. Hence, returning c_str is safe. + return m_driver.device_id().c_str(); + }; + ExpectedRef get_vdma_interrupts_dispatcher(); protected: - VdmaDevice(HailoRTDriver &&driver, Type type, const std::string &device_id); + VdmaDevice(HailoRTDriver &&driver, Type type); virtual Expected read_notification() override; virtual hailo_status disable_notifications() override; @@ -55,7 +60,7 @@ protected: HailoRTDriver m_driver; std::vector> m_core_ops; std::vector> m_network_groups; // TODO: HRT-9547 - Remove when ConfiguredNetworkGroup will be kept in global context - + // The vdma interrupts dispatcher contains a callback with a reference to the current activated network group // (reference to the ResourcesManager). Hence, it must be destructed before the networks groups are destructed. std::unique_ptr m_vdma_interrupts_dispatcher; @@ -68,6 +73,7 @@ private: std::vector> &core_ops, Hef &hef, const ConfigureNetworkParams &config_params, uint8_t network_group_index); + hailo_status clear_configured_apps(); Expected create_networks_group_vector(Hef &hef, const NetworkGroupsParamsMap &configure_params); Expected>> create_core_ops_metadata(Hef &hef, const std::string &network_group_name, uint32_t partial_clusters_layout_bitmap); diff --git a/hailort/libhailort/src/vdma/vdma_stream.cpp b/hailort/libhailort/src/vdma/vdma_stream.cpp index 59ab85b..b3f387c 100644 --- a/hailort/libhailort/src/vdma/vdma_stream.cpp +++ b/hailort/libhailort/src/vdma/vdma_stream.cpp @@ -27,30 +27,18 @@ VdmaInputStream::VdmaInputStream(VdmaDevice &device, vdma::BoundaryChannelPtr ch return; } + if (channel->type() != vdma::BoundaryChannel::Type::BUFFERED) { + LOGGER__ERROR("Can't create a vdma stream with a non buffered channel. Received channel type {}", channel->type()); + status = HAILO_INVALID_ARGUMENT; + return; + } + status = HAILO_SUCCESS; } -Expected VdmaInputStream::sync_write_raw_buffer(const MemoryView &buffer) +hailo_status VdmaInputStream::write_impl(const MemoryView &buffer) { - hailo_status status = HAILO_UNINITIALIZED; - - status = m_channel->wait(buffer.size(), m_channel_timeout); - if ((status == HAILO_STREAM_ABORTED_BY_USER) || (status == HAILO_STREAM_NOT_ACTIVATED)) { - return make_unexpected(status); - } - CHECK_AS_EXPECTED(HAILO_TIMEOUT != status, HAILO_TIMEOUT, - "{} (H2D) failed with status={} (timeout={}ms)", name(), HAILO_TIMEOUT, m_channel_timeout.count()); - CHECK_SUCCESS_AS_EXPECTED(status); - - status = m_channel->transfer((void*)buffer.data(), buffer.size()); - if ((status == HAILO_STREAM_ABORTED_BY_USER) || (status == HAILO_STREAM_NOT_ACTIVATED)) { - return make_unexpected(status); - } - CHECK_AS_EXPECTED(HAILO_TIMEOUT != status, HAILO_TIMEOUT, - "{} (H2D) failed with status={} (timeout={}ms)", name(), HAILO_TIMEOUT, m_channel_timeout.count()); - CHECK_SUCCESS_AS_EXPECTED(status); - - return buffer.size(); + return m_channel->transfer_sync((void*)buffer.data(), buffer.size(), m_channel_timeout); } hailo_status VdmaInputStream::write_buffer_only(const MemoryView &buffer, @@ -60,10 +48,10 @@ hailo_status VdmaInputStream::write_buffer_only(const MemoryView &buffer, return m_channel->write_buffer(buffer, m_channel_timeout, should_cancel); } -hailo_status VdmaInputStream::send_pending_buffer(size_t device_index) +hailo_status VdmaInputStream::send_pending_buffer(const device_id_t &device_id) { + (void)device_id; std::unique_lock lock(m_send_pending_mutex); - CHECK(0 == device_index, HAILO_INVALID_OPERATION); hailo_status status = m_channel->wait(get_frame_size(), m_channel_timeout); if ((HAILO_STREAM_ABORTED_BY_USER == status) || (HAILO_STREAM_NOT_ACTIVATED == status)) { return status; @@ -75,60 +63,34 @@ hailo_status VdmaInputStream::send_pending_buffer(size_t device_index) return m_channel->send_pending_buffer(); } -hailo_status VdmaInputStream::sync_write_all_raw_buffer_no_transform_impl(void *buffer, size_t offset, size_t size) -{ - ASSERT(NULL != buffer); - - return sync_write_raw_buffer(MemoryView(static_cast(buffer) + offset, size)).status(); -} - /** Output stream **/ VdmaOutputStream::VdmaOutputStream(VdmaDevice &device, vdma::BoundaryChannelPtr channel, const LayerInfo &edge_layer, EventPtr core_op_activated_event, uint16_t batch_size, std::chrono::milliseconds transfer_timeout, hailo_stream_interface_t interface, hailo_status &status) : - VdmaOutputStreamBase(device, channel, edge_layer, core_op_activated_event, batch_size, transfer_timeout, interface, status), - m_read_mutex() + VdmaOutputStreamBase(device, channel, edge_layer, core_op_activated_event, batch_size, transfer_timeout, interface, status) { // Check status for base class c'tor if (HAILO_SUCCESS != status) { return; } - status = HAILO_SUCCESS; -} - -Expected VdmaOutputStream::sync_read_raw_buffer(MemoryView &buffer) -{ - hailo_status status = HAILO_UNINITIALIZED; - - status = m_channel->wait(buffer.size(), m_transfer_timeout); - if ((status == HAILO_STREAM_ABORTED_BY_USER) || (status == HAILO_STREAM_NOT_ACTIVATED)) { - return make_unexpected(status); - } - CHECK_AS_EXPECTED(HAILO_TIMEOUT != status, HAILO_TIMEOUT, - "{} (D2H) failed with status={} (timeout={}ms)", name(), HAILO_TIMEOUT, m_transfer_timeout.count()); - CHECK_SUCCESS_AS_EXPECTED(status); - - status = m_channel->transfer(buffer.data(), buffer.size()); - if ((status == HAILO_STREAM_NOT_ACTIVATED) || (status == HAILO_STREAM_ABORTED_BY_USER)) { - return make_unexpected(status); + if (channel->type() != vdma::BoundaryChannel::Type::BUFFERED) { + LOGGER__ERROR("Can't create a vdma stream with a non buffered channel. Received channel type {}", channel->type()); + status = HAILO_INVALID_ARGUMENT; + return; } - CHECK_AS_EXPECTED(HAILO_TIMEOUT != status, HAILO_TIMEOUT, - "{} (D2H) failed with status={} (timeout={}ms)", name(), HAILO_TIMEOUT, m_transfer_timeout.count()); - CHECK_SUCCESS_AS_EXPECTED(status); - return buffer.size(); + status = HAILO_SUCCESS; } -hailo_status VdmaOutputStream::read_all(MemoryView &buffer) +hailo_status VdmaOutputStream::read_impl(MemoryView &buffer) { - std::unique_lock lock(m_read_mutex); - CHECK((buffer.size() % HailoRTCommon::HW_DATA_ALIGNMENT) == 0, HAILO_INVALID_ARGUMENT, + CHECK((buffer.size() % HailoRTCommon::HW_DATA_ALIGNMENT) == 0, HAILO_INVALID_ARGUMENT, "Size must be aligned to {} (got {})", HailoRTCommon::HW_DATA_ALIGNMENT, buffer.size()); - return sync_read_raw_buffer(buffer).status(); + return m_channel->transfer_sync(buffer.data(), buffer.size(), m_transfer_timeout); } } /* namespace hailort */ diff --git a/hailort/libhailort/src/vdma/vdma_stream.hpp b/hailort/libhailort/src/vdma/vdma_stream.hpp index ec6f41e..bd4329f 100644 --- a/hailort/libhailort/src/vdma/vdma_stream.hpp +++ b/hailort/libhailort/src/vdma/vdma_stream.hpp @@ -17,6 +17,7 @@ #include "vdma/vdma_stream_base.hpp" #include "vdma/vdma_device.hpp" #include "vdma/channel/boundary_channel.hpp" +#include "vdevice/scheduler/scheduled_core_op_state.hpp" namespace hailort @@ -31,23 +32,14 @@ public: hailo_status &status); virtual ~VdmaInputStream() = default; - hailo_status write_buffer_only(const MemoryView &buffer, const std::function &should_cancel = []() { return false; }); - hailo_status send_pending_buffer(size_t device_index = 0); - - void notify_all() - { - return m_channel->notify_all(); - } + virtual hailo_status write_buffer_only(const MemoryView &buffer, const std::function &should_cancel = []() { return false; }) override; + virtual hailo_status send_pending_buffer(const device_id_t &device_id) override; private: - virtual Expected sync_write_raw_buffer(const MemoryView &buffer) override; - virtual hailo_status sync_write_all_raw_buffer_no_transform_impl(void *buffer, size_t offset, size_t size) override; + virtual hailo_status write_impl(const MemoryView &buffer) override; std::mutex m_write_only_mutex; std::mutex m_send_pending_mutex; - - friend class InputVDeviceBaseStream; - friend class InputVDeviceNativeStream; }; class VdmaOutputStream : public VdmaOutputStreamBase @@ -60,12 +52,7 @@ public: virtual ~VdmaOutputStream() = default; private: - virtual Expected sync_read_raw_buffer(MemoryView &buffer); - virtual hailo_status read_all(MemoryView &buffer) override; - - std::mutex m_read_mutex; - - friend class OutputVDeviceBaseStream; + virtual hailo_status read_impl(MemoryView &buffer) override; }; diff --git a/hailort/libhailort/src/vdma/vdma_stream_base.cpp b/hailort/libhailort/src/vdma/vdma_stream_base.cpp index 542c438..0f5189d 100644 --- a/hailort/libhailort/src/vdma/vdma_stream_base.cpp +++ b/hailort/libhailort/src/vdma/vdma_stream_base.cpp @@ -24,7 +24,7 @@ static bool validate_device_interface_compatibility(hailo_stream_interface_t int case Device::Type::PCIE: interface_valid = (HAILO_STREAM_INTERFACE_PCIE == interface); break; - + case Device::Type::INTEGRATED: interface_valid = (HAILO_STREAM_INTERFACE_INTEGRATED == interface); break; @@ -48,29 +48,20 @@ Expected> VdmaInputStreamBase::create(hailo { CHECK_AS_EXPECTED(validate_device_interface_compatibility(interface, device.get_type()), HAILO_INTERNAL_FAILURE); + hailo_status status = HAILO_UNINITIALIZED; + std::shared_ptr result = nullptr; if ((stream_params.flags & HAILO_STREAM_FLAGS_ASYNC) != 0) { - CHECK_AS_EXPECTED(channel->type() == vdma::BoundaryChannel::Type::ASYNC, HAILO_INVALID_ARGUMENT, - "Can't create a async vdma stream with a non async channel. Received channel type {}", channel->type()); - - hailo_status status = HAILO_UNINITIALIZED; - auto result = make_shared_nothrow(device, channel, edge_layer, core_op_activated_event, + result = make_shared_nothrow(device, channel, edge_layer, core_op_activated_event, batch_size, DEFAULT_TRANSFER_TIMEOUT, interface, status); - CHECK_SUCCESS_AS_EXPECTED(status); - CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); - - return std::static_pointer_cast(result); } else { - CHECK_AS_EXPECTED(channel->type() == vdma::BoundaryChannel::Type::BUFFERED, HAILO_INVALID_ARGUMENT, - "Can't create a vdma stream with a non buffered channel. Received channel type {}", channel->type()); - - hailo_status status = HAILO_UNINITIALIZED; - auto result = make_shared_nothrow(device, channel, edge_layer, core_op_activated_event, + result = make_shared_nothrow(device, channel, edge_layer, core_op_activated_event, batch_size, DEFAULT_TRANSFER_TIMEOUT, interface, status); - CHECK_SUCCESS_AS_EXPECTED(status); - CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); - - return std::static_pointer_cast(result); } + + // Check that the creation of the various subclasses succeeded + CHECK_SUCCESS_AS_EXPECTED(status); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); + return result; } VdmaInputStreamBase::VdmaInputStreamBase(VdmaDevice &device, vdma::BoundaryChannelPtr channel, @@ -134,7 +125,7 @@ hailo_status VdmaInputStreamBase::clear_abort() hailo_status VdmaInputStreamBase::flush() { - const auto dynamic_batch_size = (CONTROL_PROTOCOL__IGNORE_DYNAMIC_BATCH_SIZE == m_dynamic_batch_size) ? + const auto dynamic_batch_size = (CONTROL_PROTOCOL__IGNORE_DYNAMIC_BATCH_SIZE == m_dynamic_batch_size) ? 1 : m_dynamic_batch_size; return m_channel->flush(m_channel_timeout * dynamic_batch_size); } @@ -158,16 +149,8 @@ hailo_status VdmaInputStreamBase::deactivate_stream() return HAILO_SUCCESS; } - // Flush is best effort - auto status = m_channel->flush(VDMA_FLUSH_TIMEOUT); - if (HAILO_STREAM_ABORTED_BY_USER == status) { - LOGGER__INFO("Flush input_channel is not needed because channel was aborted. (channel {})", m_channel->get_channel_id()); - status = HAILO_SUCCESS; - } else if (HAILO_SUCCESS != status) { - LOGGER__ERROR("Failed to flush input_channel. (status {} channel {})", status, m_channel->get_channel_id()); - } - status = m_channel->deactivate(); + auto status = m_channel->deactivate(); if (HAILO_SUCCESS != status) { LOGGER__ERROR("Failed to stop channel with status {}", status); } @@ -201,11 +184,6 @@ Expected VdmaInputStreamBase::get_pending_frames_count() const return m_channel->get_h2d_pending_frames_count(); } -hailo_status VdmaInputStreamBase::register_interrupt_callback(const vdma::ProcessingCompleteCallback &callback) -{ - return m_channel->register_interrupt_callback(callback); -} - hailo_status VdmaInputStreamBase::set_dynamic_batch_size(uint16_t dynamic_batch_size) { // TODO: use std::max in the configure stage @@ -218,7 +196,7 @@ hailo_status VdmaInputStreamBase::set_dynamic_batch_size(uint16_t dynamic_batch_ CHECK(dynamic_batch_size <= m_max_batch_size, HAILO_INVALID_ARGUMENT, "Dynamic batch size ({}) must be <= than the configured batch size ({})", dynamic_batch_size, m_max_batch_size); - + if (CONTROL_PROTOCOL__IGNORE_DYNAMIC_BATCH_SIZE == dynamic_batch_size) { LOGGER__TRACE("Received CONTROL_PROTOCOL__IGNORE_DYNAMIC_BATCH_SIZE == dynamic_batch_size; " "Leaving previously set value of {}", m_dynamic_batch_size); @@ -240,36 +218,34 @@ Expected> VdmaOutputStreamBase::create(hai { CHECK_AS_EXPECTED(validate_device_interface_compatibility(interface, device.get_type()), HAILO_INTERNAL_FAILURE); + hailo_status status = HAILO_UNINITIALIZED; + std::shared_ptr result = nullptr; + // TODO: after adding NMS single int, we can create an async channel for async nms output stream (HRT-10553) if ((stream_params.flags & HAILO_STREAM_FLAGS_ASYNC) != 0) { - CHECK_AS_EXPECTED(channel->type() == vdma::BoundaryChannel::Type::ASYNC, HAILO_INVALID_ARGUMENT, - "Can't create a async vdma stream with a non async channel. Received channel type {}", channel->type()); - - hailo_status status = HAILO_UNINITIALIZED; - auto result = make_shared_nothrow(device, channel, edge_layer, core_op_activated_event, - batch_size, DEFAULT_TRANSFER_TIMEOUT, interface, status); - CHECK_SUCCESS_AS_EXPECTED(status); - CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); - - return std::static_pointer_cast(result); + if (edge_layer.format.order == HAILO_FORMAT_ORDER_HAILO_NMS) { + result = make_shared_nothrow(device, channel, edge_layer, core_op_activated_event, + batch_size, DEFAULT_TRANSFER_TIMEOUT, interface, status); + } else { + result = make_shared_nothrow(device, channel, edge_layer, core_op_activated_event, + batch_size, DEFAULT_TRANSFER_TIMEOUT, interface, status); + } } else { - CHECK_AS_EXPECTED(channel->type() == vdma::BoundaryChannel::Type::BUFFERED, HAILO_INVALID_ARGUMENT, - "Can't create a vdma stream with a non buffered channel. Received channel type {}", channel->type()); + result = make_shared_nothrow(device, channel, edge_layer, core_op_activated_event, + batch_size, DEFAULT_TRANSFER_TIMEOUT, interface, status); + } - hailo_status status = HAILO_UNINITIALIZED; - auto result = make_shared_nothrow(device, channel, edge_layer, core_op_activated_event, - batch_size, DEFAULT_TRANSFER_TIMEOUT, interface, status); - CHECK_SUCCESS_AS_EXPECTED(status); - CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); + // Check that the creation of the various subclasses succeeded + CHECK_SUCCESS_AS_EXPECTED(status); + CHECK_NOT_NULL_AS_EXPECTED(result, HAILO_OUT_OF_HOST_MEMORY); - return std::static_pointer_cast(result); - } + return result; } VdmaOutputStreamBase::VdmaOutputStreamBase(VdmaDevice &device, vdma::BoundaryChannelPtr channel, const LayerInfo &edge_layer, EventPtr core_op_activated_event, uint16_t batch_size, std::chrono::milliseconds transfer_timeout, hailo_stream_interface_t interface, hailo_status &status) : - OutputStreamBase(edge_layer, std::move(core_op_activated_event), status), + OutputStreamBase(edge_layer, interface, std::move(core_op_activated_event), status), m_device(&device), m_channel(std::move(channel)), m_interface(interface), @@ -277,7 +253,7 @@ VdmaOutputStreamBase::VdmaOutputStreamBase(VdmaDevice &device, vdma::BoundaryCha m_transfer_timeout(transfer_timeout), m_max_batch_size(batch_size), m_dynamic_batch_size(batch_size), - m_transfer_size(get_transfer_size(m_stream_info)) + m_transfer_size(get_transfer_size(m_stream_info, get_layer_info())) { // Check status for base class c'tor if (HAILO_SUCCESS != status) { @@ -353,9 +329,9 @@ hailo_status VdmaOutputStreamBase::activate_stream(uint16_t dynamic_batch_size, return HAILO_SUCCESS; } -hailo_status VdmaOutputStreamBase::register_interrupt_callback(const vdma::ProcessingCompleteCallback &callback) +void VdmaOutputStreamBase::register_interrupt_callback(const vdma::ProcessingCompleteCallback &callback) { - return m_channel->register_interrupt_callback(callback); + m_channel->register_interrupt_callback(callback); } hailo_status VdmaOutputStreamBase::deactivate_stream() @@ -373,11 +349,9 @@ hailo_status VdmaOutputStreamBase::deactivate_stream() return HAILO_SUCCESS; } -uint32_t VdmaOutputStreamBase::get_transfer_size(const hailo_stream_info_t &stream_info) +uint32_t VdmaOutputStreamBase::get_transfer_size(const hailo_stream_info_t &stream_info, const LayerInfo &layer_info) { - // The ppu outputs one bbox per vdma buffer in the case of nms - return (HAILO_FORMAT_ORDER_HAILO_NMS == stream_info.format.order) ? - stream_info.nms_info.bbox_size : stream_info.hw_frame_size; + return LayerInfoUtils::get_stream_transfer_size(stream_info, layer_info); } hailo_status VdmaOutputStreamBase::set_dynamic_batch_size(uint16_t dynamic_batch_size) @@ -392,7 +366,7 @@ hailo_status VdmaOutputStreamBase::set_dynamic_batch_size(uint16_t dynamic_batch CHECK(dynamic_batch_size <= m_max_batch_size, HAILO_INVALID_ARGUMENT, "Dynamic batch size ({}) must be <= than the configured batch size ({})", dynamic_batch_size, m_max_batch_size); - + if (CONTROL_PROTOCOL__IGNORE_DYNAMIC_BATCH_SIZE == dynamic_batch_size) { LOGGER__TRACE("Received CONTROL_PROTOCOL__IGNORE_DYNAMIC_BATCH_SIZE == dynamic_batch_size; " "Leaving previously set value of {}", m_dynamic_batch_size); diff --git a/hailort/libhailort/src/vdma/vdma_stream_base.hpp b/hailort/libhailort/src/vdma/vdma_stream_base.hpp index a7cde98..9569f24 100644 --- a/hailort/libhailort/src/vdma/vdma_stream_base.hpp +++ b/hailort/libhailort/src/vdma/vdma_stream_base.hpp @@ -20,7 +20,6 @@ namespace hailort { -constexpr std::chrono::seconds VDMA_FLUSH_TIMEOUT(10); class VdmaInputStreamBase : public InputStreamBase { public: @@ -41,7 +40,14 @@ public: Expected get_buffer_state(); virtual Expected get_buffer_frames_size() const override; virtual Expected get_pending_frames_count() const override; - virtual hailo_status register_interrupt_callback(const vdma::ProcessingCompleteCallback &callback) override; + + virtual hailo_status write_buffer_only(const MemoryView &buffer, const std::function &should_cancel = []() { return false; }) = 0; + virtual hailo_status send_pending_buffer(const device_id_t &device_id) = 0; + + void notify_all() + { + m_channel->notify_all(); + } protected: VdmaInputStreamBase(VdmaDevice &device, vdma::BoundaryChannelPtr channel, const LayerInfo &edge_layer, @@ -53,6 +59,9 @@ protected: virtual hailo_status deactivate_stream() override; hailo_status set_dynamic_batch_size(uint16_t dynamic_batch_size); + friend class VDeviceInputStreamBase; + friend class VDeviceNativeInputStream; + VdmaDevice *m_device; vdma::BoundaryChannelPtr m_channel; const hailo_stream_interface_t m_interface; @@ -81,7 +90,7 @@ public: virtual Expected get_buffer_frames_size() const override; virtual Expected get_pending_frames_count() const override; - virtual hailo_status register_interrupt_callback(const vdma::ProcessingCompleteCallback &callback); + void register_interrupt_callback(const vdma::ProcessingCompleteCallback &callback); protected: VdmaOutputStreamBase(VdmaDevice &device, vdma::BoundaryChannelPtr channel, const LayerInfo &edge_layer, @@ -91,9 +100,11 @@ protected: virtual hailo_status activate_stream(uint16_t dynamic_batch_size, bool resume_pending_stream_transfers) override; virtual hailo_status deactivate_stream() override; - static uint32_t get_transfer_size(const hailo_stream_info_t &stream_info); + static uint32_t get_transfer_size(const hailo_stream_info_t &stream_info, const LayerInfo &layer_info); hailo_status set_dynamic_batch_size(uint16_t dynamic_batch_size); + friend class VDeviceOutputStreamBase; + VdmaDevice *m_device; vdma::BoundaryChannelPtr m_channel; const hailo_stream_interface_t m_interface; @@ -102,7 +113,6 @@ protected: const uint16_t m_max_batch_size; uint16_t m_dynamic_batch_size; const uint32_t m_transfer_size; - std::mutex m_read_mutex; }; diff --git a/hailort/pre_build/external/CMakeLists.txt b/hailort/pre_build/external/CMakeLists.txt index fd34495..1c7438e 100644 --- a/hailort/pre_build/external/CMakeLists.txt +++ b/hailort/pre_build/external/CMakeLists.txt @@ -16,7 +16,7 @@ function(git_clone proj repo tag) ) endfunction() -git_clone(pybind11 https://github.com/pybind/pybind11.git 80dc998efced8ceb2be59756668a7e90e8bef917) +include(${CMAKE_CURRENT_LIST_DIR}/../../libhailort/bindings/python/externals/pybind11.cmake) git_clone(Catch2 https://github.com/catchorg/Catch2.git c4e3767e265808590986d5db6ca1b5532a7f3d13) git_clone(CLI11 https://github.com/hailo-ai/CLI11.git f1644f15f219303b7ad670732c21018a1e6f0e11) git_clone(spdlog https://github.com/gabime/spdlog.git e2789531912a5c6ab28a90387f97c52963eec08a) diff --git a/hailort/rpc/hailort_rpc.proto b/hailort/rpc/hailort_rpc.proto index 3df851f..873c9ba 100644 --- a/hailort/rpc/hailort_rpc.proto +++ b/hailort/rpc/hailort_rpc.proto @@ -33,6 +33,9 @@ service ProtoHailoRtRpc { rpc ConfiguredNetworkGroup_get_latency_measurement (ConfiguredNetworkGroup_get_latency_measurement_Request) returns (ConfiguredNetworkGroup_get_latency_measurement_Reply) {} rpc ConfiguredNetworkGroup_is_multi_context (ConfiguredNetworkGroup_is_multi_context_Request) returns (ConfiguredNetworkGroup_is_multi_context_Reply) {} rpc ConfiguredNetworkGroup_get_config_params(ConfiguredNetworkGroup_get_config_params_Request) returns (ConfiguredNetworkGroup_get_config_params_Reply) {} + rpc ConfiguredNetworkGroup_get_sorted_output_names(ConfiguredNetworkGroup_get_sorted_output_names_Request) returns (ConfiguredNetworkGroup_get_sorted_output_names_Reply) {} + rpc ConfiguredNetworkGroup_get_stream_names_from_vstream_name(ConfiguredNetworkGroup_get_stream_names_from_vstream_name_Request) returns (ConfiguredNetworkGroup_get_stream_names_from_vstream_name_Reply) {} + rpc ConfiguredNetworkGroup_get_vstream_names_from_stream_name(ConfiguredNetworkGroup_get_vstream_names_from_stream_name_Request) returns (ConfiguredNetworkGroup_get_vstream_names_from_stream_name_Reply) {} rpc InputVStreams_create (VStream_create_Request) returns (VStreams_create_Reply) {} rpc InputVStream_dup_handle (dup_handle_Request) returns (dup_handle_Reply) {} @@ -51,12 +54,18 @@ service ProtoHailoRtRpc { rpc OutputVStream_network_name (VStream_network_name_Request) returns (VStream_network_name_Reply) {} rpc InputVStream_abort (VStream_abort_Request) returns (VStream_abort_Reply) {} rpc OutputVStream_abort (VStream_abort_Request) returns (VStream_abort_Reply) {} + rpc InputVStream_stop_and_clear (VStream_stop_and_clear_Request) returns (VStream_stop_and_clear_Reply) {} + rpc InputVStream_start_vstream (VStream_start_vstream_Request) returns (VStream_start_vstream_Reply) {} + rpc OutputVStream_stop_and_clear (VStream_stop_and_clear_Request) returns (VStream_stop_and_clear_Reply) {} + rpc OutputVStream_start_vstream (VStream_start_vstream_Request) returns (VStream_start_vstream_Reply) {} rpc InputVStream_resume (VStream_resume_Request) returns (VStream_resume_Reply) {} rpc OutputVStream_resume (VStream_resume_Request) returns (VStream_resume_Reply) {} rpc InputVStream_get_user_buffer_format (VStream_get_user_buffer_format_Request) returns (VStream_get_user_buffer_format_Reply) {} rpc OutputVStream_get_user_buffer_format (VStream_get_user_buffer_format_Request) returns (VStream_get_user_buffer_format_Reply) {} rpc InputVStream_get_info (VStream_get_info_Request) returns (VStream_get_info_Reply) {} rpc OutputVStream_get_info (VStream_get_info_Request) returns (VStream_get_info_Reply) {} + rpc InputVStream_is_aborted (VStream_is_aborted_Request) returns (VStream_is_aborted_Reply) {} + rpc OutputVStream_is_aborted (VStream_is_aborted_Request) returns (VStream_is_aborted_Reply) {} } message empty {} @@ -107,6 +116,7 @@ message VDevice_create_Reply { message Release_Request { uint32 handle = 1; + uint32 pid = 2; } message Release_Reply { @@ -159,6 +169,17 @@ message ProtoNmsDefuseInfo { string original_name = 2; } +enum ProtoNmsBurstType { + // No burst + PROTO_NMS_BURST_TYPE_NO_BURST = 0; + // No image delimiter, burst per class + PROTO_NMS_BURST_TYPE_H8_PER_CLASS = 1; + // Image delimiter and burst per class + PROTO_NMS_BURST_TYPE_H15_PER_CLASS = 2; + // Image delimiter and burst per image + PROTO_NMS_BURST_TYPE_H15_PER_FRAME = 3; +} + message ProtoNmsInfo { uint32 number_of_classes = 1; uint32 max_bboxes_per_class = 2; @@ -166,6 +187,8 @@ message ProtoNmsInfo { uint32 chunks_per_frame = 4; bool is_defused = 5; ProtoNmsDefuseInfo defuse_info = 6; + uint32 burst_size = 7; + ProtoNmsBurstType burst_type = 8; } message ProtoQuantInfo { @@ -438,6 +461,35 @@ message ConfiguredNetworkGroup_get_config_params_Reply { ProtoConfigureNetworkParams params = 2; } +message ConfiguredNetworkGroup_get_sorted_output_names_Request { + uint32 handle = 1; +} + +message ConfiguredNetworkGroup_get_sorted_output_names_Reply { + uint32 status = 1; + repeated string sorted_output_names = 2; +} + +message ConfiguredNetworkGroup_get_stream_names_from_vstream_name_Request { + uint32 handle = 1; + string vstream_name = 2; +} + +message ConfiguredNetworkGroup_get_stream_names_from_vstream_name_Reply { + uint32 status = 1; + repeated string streams_names = 2; +} + +message ConfiguredNetworkGroup_get_vstream_names_from_stream_name_Request { + uint32 handle = 1; + string stream_name = 2; +} + +message ConfiguredNetworkGroup_get_vstream_names_from_stream_name_Reply { + uint32 status = 1; + repeated string vstreams_names = 2; +} + message InputVStream_write_Request { uint32 handle = 1; bytes data = 2; @@ -500,6 +552,22 @@ message VStream_abort_Reply { uint32 status = 1; } +message VStream_stop_and_clear_Request { + uint32 handle = 1; +} + +message VStream_stop_and_clear_Reply { + uint32 status = 1; +} + +message VStream_start_vstream_Request { + uint32 handle = 1; +} + +message VStream_start_vstream_Reply { + uint32 status = 1; +} + message VStream_resume_Request { uint32 handle = 1; } @@ -524,4 +592,13 @@ message VStream_get_info_Request { message VStream_get_info_Reply { uint32 status = 1; ProtoVStreamInfo vstream_info = 2; +} + +message VStream_is_aborted_Request { + uint32 handle = 1; +} + +message VStream_is_aborted_Reply { + uint32 status = 1; + bool is_aborted = 2; } \ No newline at end of file diff --git a/hailort/scripts/download_firmware_eth.cmd b/hailort/scripts/download_firmware_eth.cmd index 043b312..db9c3ff 100644 --- a/hailort/scripts/download_firmware_eth.cmd +++ b/hailort/scripts/download_firmware_eth.cmd @@ -2,7 +2,7 @@ @ECHO OFF set BASE_URI=https://hailo-hailort.s3.eu-west-2.amazonaws.com -set HRT_VERSION=4.13.0 +set HRT_VERSION=4.14.0 set FW_DIR=Hailo8/%HRT_VERSION%/FW set FW=hailo8_fw.%HRT_VERSION%_eth.bin diff --git a/hailort/scripts/download_firmware_eth.sh b/hailort/scripts/download_firmware_eth.sh index 70c8886..d02e0c5 100755 --- a/hailort/scripts/download_firmware_eth.sh +++ b/hailort/scripts/download_firmware_eth.sh @@ -2,7 +2,7 @@ set -e readonly BASE_URI="https://hailo-hailort.s3.eu-west-2.amazonaws.com" -readonly HRT_VERSION=4.13.0 +readonly HRT_VERSION=4.14.0 readonly FW_AWS_DIR="Hailo8/${HRT_VERSION}/FW" readonly FW="hailo8_fw.${HRT_VERSION}_eth.bin" diff --git a/hailort/scripts/download_hefs.cmd b/hailort/scripts/download_hefs.cmd index 332c9f1..944367e 100644 --- a/hailort/scripts/download_hefs.cmd +++ b/hailort/scripts/download_hefs.cmd @@ -1,12 +1,12 @@ :: cmd @ECHO OFF set BASE_URI=https://hailo-hailort.s3.eu-west-2.amazonaws.com -set HRT_VERSION=4.13.0 +set HRT_VERSION=4.14.0 set REMOTE_HEF_DIR=Hailo8/%HRT_VERSION%/HEFS set LOCAL_EXAMPLES_HEF_DIR=..\libhailort\examples\hefs -set LOCAL_TUTORIALS_HEF_DIR=..\libhailort\bindings\python\platform\tutorials\hefs +set LOCAL_TUTORIALS_HEF_DIR=..\libhailort\bindings\python\platform\hailo_tutorials\hefs set EXAMPLES_HEFS=(multi_network_shortcut_net.hef shortcut_net.hef) -set TUTORIALS_HEFS=(resnet_v1_18.hef) +set TUTORIALS_HEFS=(resnet_v1_18.hef shortcut_net.hef) if not exist %LOCAL_EXAMPLES_HEF_DIR% mkdir %LOCAL_EXAMPLES_HEF_DIR% if not exist %LOCAL_TUTORIALS_HEF_DIR% mkdir %LOCAL_TUTORIALS_HEF_DIR% diff --git a/hailort/scripts/download_hefs.sh b/hailort/scripts/download_hefs.sh index 489367f..ff56bd0 100755 --- a/hailort/scripts/download_hefs.sh +++ b/hailort/scripts/download_hefs.sh @@ -2,16 +2,17 @@ set -e readonly BASE_URI="https://hailo-hailort.s3.eu-west-2.amazonaws.com" -readonly HRT_VERSION=4.13.0 +readonly HRT_VERSION=4.14.0 readonly REMOTE_HEF_DIR="Hailo8/${HRT_VERSION}/HEFS" readonly LOCAL_EXAMPLES_HEF_DIR="../libhailort/examples/hefs" -readonly LOCAL_TUTORIALS_HEF_DIR="../libhailort/bindings/python/platform/tutorials/hefs/" +readonly LOCAL_TUTORIALS_HEF_DIR="../libhailort/bindings/python/platform/hailo_tutorials/hefs" readonly EXAMPLES_HEFS=( "shortcut_net.hef" "multi_network_shortcut_net.hef" ) readonly TUTORIALS_HEFS=( "resnet_v1_18.hef" + "shortcut_net.hef" ) function create_hef_dir(){ diff --git a/hailort/tools/hailo15-scripts/hailo15_env_vars.sh b/hailort/tools/hailo15-scripts/hailo15_env_vars.sh new file mode 100644 index 0000000..2756ad1 --- /dev/null +++ b/hailort/tools/hailo15-scripts/hailo15_env_vars.sh @@ -0,0 +1,8 @@ +#! /bin/bash +set -e + +# Environment declarations +script_directory=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd) +local_platform_sw_path="$script_directory"/../../../ +h15="10.0.0.1" +ssh-copy-id root@$h15 \ No newline at end of file diff --git a/hailort/tools/hailo15-scripts/load_driver.sh b/hailort/tools/hailo15-scripts/load_driver.sh new file mode 100755 index 0000000..0e4e414 --- /dev/null +++ b/hailort/tools/hailo15-scripts/load_driver.sh @@ -0,0 +1,13 @@ +#! /bin/bash +set -e + +# Include Environment declarations +script_directory=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd) +source "$script_directory"/hailo15_env_vars.sh + +cd $local_platform_sw_path +./install.sh comp build_integrated_nnc_driver --image-path /local/bkc/v0.29-build-2023-05-07 +path="$local_platform_sw_path"/hailort/drivers/linux/integrated_nnc/hailo_integrated_nnc.ko +scp $path root@$h15:/lib/modules/5.15.32-yocto-standard/kernel/drivers/misc/hailo_integrated_nnc.ko + +ssh root@$h15 "modprobe -r hailo_integrated_nnc && modprobe hailo_integrated_nnc" diff --git a/hailort/tools/hailo15-scripts/load_firmware.sh b/hailort/tools/hailo15-scripts/load_firmware.sh new file mode 100755 index 0000000..c568630 --- /dev/null +++ b/hailort/tools/hailo15-scripts/load_firmware.sh @@ -0,0 +1,11 @@ +#! /bin/bash +set -e + +# Include Environment declarations +script_directory=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd) +source "$script_directory"/hailo15_env_vars.sh + +cd $local_platform_sw_path +./install.sh comp build_fw --fw vpu --hw-arch hailo15 +scp firmware/vpu_firmware/build/hailo15_nnc_fw.bin root@$h15:/lib/firmware/hailo/hailo15_nnc_fw.bin +ssh root@$h15 "modprobe -r hailo_integrated_nnc && modprobe hailo_integrated_nnc" diff --git a/hailort/tools/hailo15-scripts/load_hrt.sh b/hailort/tools/hailo15-scripts/load_hrt.sh new file mode 100755 index 0000000..8c6947d --- /dev/null +++ b/hailort/tools/hailo15-scripts/load_hrt.sh @@ -0,0 +1,14 @@ +#! /bin/bash +set -e + +# Include Environment declarations +script_directory=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd) +source "$script_directory"/hailo15_env_vars.sh + +cd $local_platform_sw_path +./build.sh -aaarch64 -brelease install + +scp lib/linux.aarch64.release/libhailort.* root@$h15:/usr/lib/ +scp bin/linux.aarch64.release/hailortcli root@$h15:/usr/bin/ +scp bin/linux.aarch64.release/debalex root@$h15:/usr/bin/ +scp bin/linux.aarch64.release/board_tests root@$h15:/usr/bin/ diff --git a/hailort/tools/hailo15-scripts/load_pcr.sh b/hailort/tools/hailo15-scripts/load_pcr.sh new file mode 100755 index 0000000..4123852 --- /dev/null +++ b/hailort/tools/hailo15-scripts/load_pcr.sh @@ -0,0 +1,12 @@ +#! /bin/bash +set -e + +# Include Environment declarations +script_directory=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd) +source "$script_directory"/hailo15_env_vars.sh + +cd $local_platform_sw_path +# Compile PCR +./install.sh comp build_infra_tools --arch aarch64 --build-hailort --build-type release + +scp platform_internals/hailo_platform_internals/low_level_tools/build/linux.aarch64.release/pcr/pcr root@$h15:/usr/bin/ diff --git a/hailort/tools/hailo15-scripts/read_log.sh b/hailort/tools/hailo15-scripts/read_log.sh new file mode 100755 index 0000000..5c05a0f --- /dev/null +++ b/hailort/tools/hailo15-scripts/read_log.sh @@ -0,0 +1,15 @@ +#! /bin/bash +set -e + +# Include Environment declarations +script_directory=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd) +source "$script_directory"/hailo15_env_vars.sh + +cd $local_platform_sw_path +source hailo_platform_venv/bin/activate +ssh root@$h15 "hailortcli fw-logger /tmp/fw_log.dat" +scp root@$h15:/tmp/fw_log.dat /tmp +ssh root@$h15 "rm /tmp/fw_log.dat" + +python ./platform_internals/hailo_platform_internals/tools/firmware/parse_tracelog.py --fw vpu --core-log-entries firmware/vpu_firmware/build/hailo15_nnc_fw_*_log_entries.csv --core-only --raw-input-file /tmp/fw_log.dat + diff --git a/hailort/tools/hailo15-scripts/sanity_infer.sh b/hailort/tools/hailo15-scripts/sanity_infer.sh new file mode 100755 index 0000000..0393549 --- /dev/null +++ b/hailort/tools/hailo15-scripts/sanity_infer.sh @@ -0,0 +1,8 @@ +#! /bin/bash +set -e + +# Include Environment declarations +script_directory=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd) +source "$script_directory"/hailo15_env_vars.sh + +ssh root@$h15 "hailortcli run /etc/hailo/hefs/hailo15/shortcut_net/28_28_3/shortcut_net.hef -c 1" diff --git a/hailort/tools/hailo15-scripts/update_hrt_and_infer.sh b/hailort/tools/hailo15-scripts/update_hrt_and_infer.sh new file mode 100755 index 0000000..4e8c93d --- /dev/null +++ b/hailort/tools/hailo15-scripts/update_hrt_and_infer.sh @@ -0,0 +1,23 @@ +#! /bin/bash +set -e + +# Include Environment declarations +script_directory=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd) +source "$script_directory"/hailo15_env_vars.sh + +# Build hailo15 artifacts +/bin/bash "$script_directory"/load_hrt.sh + +# Build hailo15 PCR +/bin/bash "$script_directory"/load_pcr.sh + +# Build hailo15 fw +cd $local_platform_sw_path +./install.sh comp build_fw --fw vpu --hw-arch hailo15 +scp firmware/vpu_firmware/build/hailo15_nnc_fw.bin root@$h15:/lib/firmware/hailo/hailo15_nnc_fw.bin + +# Build integrated_nnc (hailo15) driver +/bin/bash "$script_directory"/load_driver.sh + +# Run sanity infer +/bin/bash "$script_directory"/sanity_infer.sh diff --git a/hailort/tools/hw_debug/CMakeLists.txt b/hailort/tools/hw_debug/CMakeLists.txt index 5fd1d77..eeea604 100644 --- a/hailort/tools/hw_debug/CMakeLists.txt +++ b/hailort/tools/hw_debug/CMakeLists.txt @@ -12,8 +12,6 @@ set(FILES ${HAILO_OS_DIR}/hailort_driver.cpp ${HAILO_OS_DIR}/file_descriptor.cpp ${HAILO_FULL_OS_DIR}/driver_scan.cpp - # TODO: HRT-3816 remove mmap header - ${HAILO_OS_DIR}/mmap_buffer.cpp ) if(WIN32) @@ -51,3 +49,8 @@ if(READLINE_INCLUDE_DIR AND READLINE_LIBRARY) else() message(WARNING "Could not find readline library. To better UI, please install it by calling `sudo apt install libreadline6-dev`") endif() + +install(TARGETS debalex + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) +cli11_install_completion_file(debalex) \ No newline at end of file diff --git a/hailort/tools/hw_debug/main.cpp b/hailort/tools/hw_debug/main.cpp index 74d33c4..0d0bfac 100644 --- a/hailort/tools/hw_debug/main.cpp +++ b/hailort/tools/hw_debug/main.cpp @@ -92,7 +92,7 @@ static std::vector get_available_device_ids() return device_ids; } -std::string get_device_filepath(const std::string &device_id) +HailoRTDriver::DeviceInfo get_device_info(const std::string &device_id) { auto scan_results = HailoRTDriver::scan_devices(); if (!scan_results) { @@ -107,13 +107,13 @@ std::string get_device_filepath(const std::string &device_id) throw std::runtime_error("Requested device not found"); } - return device_found->dev_path; + return *device_found; } std::shared_ptr create_driver_object(const std::string &device_id) { - auto device_path = get_device_filepath(device_id); - auto hailort_driver = HailoRTDriver::create(device_path); + auto device_info = get_device_info(device_id); + auto hailort_driver = HailoRTDriver::create(device_info); if (!hailort_driver) { throw std::runtime_error("Failed create hailort driver object"); } -- 2.34.1