diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..d41c6a3e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug, help wanted +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Platform Information (please complete the following information):** + - Holoscan SDK Version [e.g. 2.1.0] +- Architecture: [x86_64, arm64] + - OS: [Ubuntu, RHEL, IGX SW OS] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..dcb4a88a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement, help wanted +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/questions-and-support.md b/.github/ISSUE_TEMPLATE/questions-and-support.md new file mode 100644 index 00000000..66def445 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/questions-and-support.md @@ -0,0 +1,17 @@ +--- +name: Questions and Support +about: Request information on Holoscan SDK features and best practices +title: '' +labels: help wanted +assignees: '' + +--- + +## Please describe your question +Ask about Holoscan SDK features or best practices. For example, "Does Holoscan SDK support v4l2 compatible cameras?" +  +## Please specify what Holoscan SDK version you are using +latest + +## Please add any details about your platform or use case +For instance, "IGX devkit" diff --git a/.gitignore b/.gitignore index b8d8ffb8..598a012d 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,9 @@ _deps runtime_docker/install runtime_docker/*.txt +# file autogenerated by setuptools_scm +_version.py + # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. diff --git a/.vscode/launch.json b/.vscode/launch.json index 3724ee49..2977a916 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -157,7 +157,7 @@ ] }, { - "name": "(gdb) examples/conditions/asynchronous/cpp/ping_async", + "name": "(gdb) examples/conditions/asynchronous/cpp", "type": "cppdbg", "request": "launch", "program": "${command:cmake.buildDirectory}/examples/conditions/asynchronous/cpp/ping_async", @@ -184,7 +184,98 @@ ] }, { - "name": "(gdb) examples/conditions/periodic/cpp/periodic_ping", + "name": "(gdb) examples/conditions/asynchronous/python", + "type": "cppdbg", + "request": "launch", + "program": "/usr/bin/bash", + "args": [ + "${workspaceFolder}/${env:HOLOSCAN_PUBLIC_FOLDER}/.vscode/debug_python", + "${workspaceFolder}/${env:HOLOSCAN_PUBLIC_FOLDER}/examples/conditions/asynchronous/python/ping_async.py", + ], + "stopAtEntry": false, + "cwd": "${command:cmake.buildDirectory}", + "environment": [ + { + "name": "HOLOSCAN_LOG_LEVEL", + "value": "DEBUG" + }, + { + "name": "PYTHONPATH", + "value": "${command:cmake.buildDirectory}/python/lib" + }, + ], + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, + { + "name": "(gdb) examples/conditions/expiring_message/cpp", + "type": "cppdbg", + "request": "launch", + "program": "${command:cmake.buildDirectory}/examples/conditions/expiring_message/cpp/ping_expiring_message", + "args": [], + "stopAtEntry": false, + "cwd": "${command:cmake.buildDirectory}", + "environment": [ + { + "name": "HOLOSCAN_LOG_LEVEL", + "value": "INFO" + }, + { + "name": "HOLOSCAN_LOG_FORMAT", + "value": "long" + }, + ], + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, + { + "name": "(gdb) examples/conditions/expiring_message/python", + "type": "cppdbg", + "request": "launch", + "program": "/usr/bin/bash", + "args": [ + "${workspaceFolder}/${env:HOLOSCAN_PUBLIC_FOLDER}/.vscode/debug_python", + "${workspaceFolder}/${env:HOLOSCAN_PUBLIC_FOLDER}/examples/conditions/expiring_message/python/ping_expiring_message.py", + ], + "stopAtEntry": false, + "cwd": "${command:cmake.buildDirectory}", + "environment": [ + { + "name": "HOLOSCAN_LOG_LEVEL", + "value": "INFO" + }, + { + "name": "HOLOSCAN_LOG_FORMAT", + "value": "long" + }, + { + "name": "PYTHONPATH", + "value": "${command:cmake.buildDirectory}/python/lib" + }, + ], + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + }, + { + "name": "(gdb) examples/conditions/periodic/cpp", "type": "cppdbg", "request": "launch", "program": "${command:cmake.buildDirectory}/examples/conditions/periodic/cpp/periodic_ping", diff --git a/CODE_OF_CONDUCT b/CODE_OF_CONDUCT new file mode 100644 index 00000000..27995902 --- /dev/null +++ b/CODE_OF_CONDUCT @@ -0,0 +1,4 @@ +We observe the [RAPIDS Contributor Covenant Code of Conduct](https://docs.rapids.ai/resources/conduct/) +to foster a positive Holoscan community. + +Please report any instances of conduct violations to holoscan-conduct@nvidia.com. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd278284..b57125f2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,66 @@ -The Holoscan SDK is released on GitHub to better support the community and facilitate feedback. +# Contribute to Holoscan SDK -If you find any issues with the SDK please report them using GitHub [Issues](https://github.com/nvidia-holoscan/holoscan-sdk/issues). +Welcome to Holoscan SDK! We're glad that you're considering contributing to the platform. + +Holoscan SDK is released on GitHub as open source software to better support the community and facilitate feedback. By +contributing you agree to follow our [code of conduct](./CODE_OF_CONDUCT). + +## Reporting Feedback + +Community feedback helps us improve the Holoscan SDK platform to better meet user needs. We use GitHub [Issues](https://github.com/nvidia-holoscan/holoscan-sdk/issues) to track feedback and problems over time, as well as to provide +limited support to Holoscan SDK users. + +Consider reviewing existing issues or opening a new issue if you: +- Have a question about using a Holoscan SDK feature +- Notice errors or unexpected behavior coming from Holoscan SDK +- Have an idea for a change that might benefit other users + +When reporting an error, please include relevant details that will help our team investigate the issue. Details might include: +- A summary of the problem +- The behavior you have observed +- The behavior you expected +- Details about your PC, including the architecture (x86_64 or arm64) and GPU +- The version of Holoscan SDK where you observed the problem +- Any relevant logs or images to help investigate the issue + +You can also refer to the Holoscan SDK [NVIDIA Developer Forums](https://forums.developer.nvidia.com/c/healthcare/holoscan-sdk/) for support questions and community discussions. + +## Suggesting Changes + +Users are welcome to suggest code changes to Holoscan SDK in the form of [Issues](https://github.com/nvidia-holoscan/holoscan-sdk/issues) or [Pull Requests](https://github.com/nvidia-holoscan/holoscan-sdk/pulls). + +### Issues + +Please open a new [issue](https://github.com/nvidia-holoscan/holoscan-sdk/issues) if you'd like to request a new feature +or propose a feature design. You can tag a specific community maintainer in your post with "@", or we'll update +when we've had a chance to review your post. + +### Pull Requests + +While we primarily develop Holoscan SDK internally, we also accept external contributions that help move the platform +forward. We typically favor contributions that aim to fix an existing issue or improve documentation, but we'll also integrate new features +and enhancements when they're aligned with the Holoscan SDK vision. Please check in with the development team to propose your idea before spending time working on new features. This will help prevent duplicate effort and avoid spending time on features that would be unlikely to be merged. We'll typically refer new operators for contribution +to the downstream [HoloHub](https://github.com/nvidia-holoscan/holohub) community project. + +You might contribute to Holoscan to help us address fixes earlier in our development cycle, or to suggest improvements to Holoscan SDK that you believe would broadly benefit the Holoscan community. + +Holoscan SDK follows a monthly release process that includes internal quality assurance. To add a fix or feature, +we request that you [fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) Holoscan SDK, develop in a branch, and submit your change as a [pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) against the latest Holoscan SDK release commit. If we accept your submission after external discussion, we will integrate those changes within our internal development and credit you in Git commit history. Any changes that we accept from community contributions will undergo quality assurance testing before they are included in the next Holoscan SDK release. + +**Note**: We recommend that new GitHub users read GitHub's [Getting Started](https://docs.github.com/en/get-started/start-your-journey) guide before opening their first pull request. + +## Tracking Development + +We take all community feedback into consideration. If we don't believe a proposed change aligns with our direction +for Holoscan SDK, or if we don't expect we can prioritize a task within a reasonable timeframe, we'll let you +know by appropriately labeling or closing the issue or pull request with an explanatory comment. + +For items that we do plan to pursue or integrate, we use +[GitHub Milestones](https://github.com/nvidia-holoscan/holoscan-sdk/milestones) +to communicate our release planning. We will add community issues and pull requests to the approximate monthly release +milestone when we expect to pursue that development. Issues that we mark as "needs triage" are not part of a milestone +and will be revisited once each month to determine our priorities. + +## Additional Information + +Please refer to the project [README](/README.md) document for additional developer information. Happy coding! diff --git a/DEVELOP.md b/DEVELOP.md index df13cc4d..b4eab4d0 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -84,7 +84,7 @@ To build the Holoscan SDK on a local environment, the following versions of dev | CUDA | 12.2 | Core SDK | base | | gRPC | 1.54.2 | Core SDK | grpc-builder | | UCX | 1.15.0 | Core SDK | ucx-builder | -| GXF | 3.1 | Core SDK | gxf-downloader | +| GXF | 4.0 | Core SDK | gxf-downloader | | MOFED | 23.07 | ConnectX | mofed-installer | | TensorRT | 8.6.1 | Inference operator | base | | ONNX Runtime | 1.15.1 | Inference operator | onnxruntime-downloader | diff --git a/README.md b/README.md index 2ff38d1c..085eeb8e 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,12 @@ We appreciate community discussion and feedback in support of Holoscan platform - Direct questions to the [NVIDIA Support Forum](https://forums.developer.nvidia.com/c/healthcare/holoscan-sdk/320/all). - Enter SDK issues on the [SDK GitHub Issues board](https://github.com/nvidia-holoscan/holoscan-sdk/issues). +## Contributing to Holoscan SDK + +Holoscan SDK is developed internally and released as open source software. We welcome community contributions +and may include them in Holoscan SDK releases at our discretion. Please refer to the Holoscan SDK +[Contributing Guide](/CONTRIBUTING.md) for more information. + ## Additional Notes ### Relation to NVIDIA Clara diff --git a/VERSION b/VERSION index 50aea0e7..e3a4f193 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.0 \ No newline at end of file +2.2.0 \ No newline at end of file diff --git a/docs/api/holoscan_cpp_api.md b/docs/api/holoscan_cpp_api.md index 019004d6..66910916 100644 --- a/docs/api/holoscan_cpp_api.md +++ b/docs/api/holoscan_cpp_api.md @@ -189,7 +189,7 @@ - {ref}`exhale_class_classholoscan_1_1Tensor` - {ref}`exhale_class_classholoscan_1_1TensorMap` -- {ref}`exhale_struct_structholoscan_1_1DLManagedTensorCtx` +- {ref}`exhale_struct_structholoscan_1_1DLManagedTensorContext` - {ref}`exhale_class_classholoscan_1_1DLManagedMemoryBuffer` ##### Functions diff --git a/docs/components/conditions.md b/docs/components/conditions.md index 03aea753..11fa57e4 100644 --- a/docs/components/conditions.md +++ b/docs/components/conditions.md @@ -20,6 +20,7 @@ The following table shows various states of the scheduling status of an operator By default, operators are always `READY`, meaning they are scheduled to continuously execute their `compute()` method. To change that behavior, some condition classes can be passed to the constructor of an operator. There are various conditions currently supported in the Holoscan SDK: - MessageAvailableCondition +- ExpiringMessageAvailableCondition - DownstreamMessageAffordableCondition - CountCondition - BooleanCondition @@ -46,6 +47,14 @@ An optional parameter for this condition is `front_stage_max_size`, the maximum If this parameter is set, the condition will only allow execution if the number of messages in the queue does not exceed this count. It can be used for operators which do not consume all messages from the queue. +## ExpiringMessageAvailableCondition + +An operator associated with `ExpiringMessageAvailableCondition` ({cpp:class}`C++ `/{py:class}`Python `) is executed when the first message received in the associated queue is expiring or when there are enough messages in the queue. +This condition is associated with a specific input or output port of an operator through the `condition()` method on the return value (IOSpec) of the OperatorSpec's `input()` or `output()` method. + +The parameters ``max_batch_size`` and ``max_delay_ns`` dictate the maximum number of messages to be batched together and the maximum delay from first message to wait before executing the entity respectively. +Please note that `ExpiringMessageAvailableCondition` requires that the input messages sent to any port using this condition must contain a timestamp. This means that the upstream operator has to emit using a timestamp . + ## DownstreamMessageAffordableCondition The `DownstreamMessageAffordableCondition` ({cpp:class}`C++ `/{py:class}`Python `) condition specifies that an operator shall be executed if the input port of the downstream operator for a given output port can accept new messages. @@ -57,6 +66,7 @@ The minimum number of messages that permits the execution of the operator is spe An operator associated with `CountCondition` ({cpp:class}`C++ `/{py:class}`Python `) is executed for a specific number of times specified using its `count` parameter. The scheduling status of the operator associated with this condition can either be in `READY` or `NEVER` state. The scheduling status reaches the `NEVER` state when the operator has been executed `count` number of times. +The `count` parameter can be set to a negative value to indicate that the operator should be executed an infinite number of times (default: `1`). ## BooleanCondition diff --git a/docs/conf.py b/docs/conf.py index c684f39c..1602440e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,24 +1,24 @@ """ - SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - SPDX-License-Identifier: Apache-2.0 +SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: Apache-2.0 - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 +http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. - Configuration file for the Sphinx documentation builder. +Configuration file for the Sphinx documentation builder. - This file only contains a selection of the most common options. For a full - list see the documentation: - https://www.sphinx-doc.org/en/master/usage/configuration.html +This file only contains a selection of the most common options. For a full +list see the documentation: +https://www.sphinx-doc.org/en/master/usage/configuration.html """ # noqa: E501 import os import textwrap @@ -82,7 +82,7 @@ numfig = True # -- Options for graphviz output --------------------------------------------- -graphviz_output_format = "svg" +graphviz_output_format = "png" # -- Options for HTML output ------------------------------------------------- diff --git a/docs/holoscan_create_app.md b/docs/holoscan_create_app.md index 161775b1..7070805b 100644 --- a/docs/holoscan_create_app.md +++ b/docs/holoscan_create_app.md @@ -292,6 +292,8 @@ load_extensions(context, exts) To be discoverable, paths to these shared libraries need to either be absolute, relative to your working directory, installed in the `lib/gxf_extensions` folder of the holoscan package, or listed under the `HOLOSCAN_LIB_PATH` or `LD_LIBRARY_PATH` environment variables. ::: +Please see other examples in the [system tests](https://github.com/nvidia-holoscan/holoscan-sdk/blob/main/tests/system/loading_gxf_extension.cpp) in the Holoscan SDK repository. + (configuring-app-operators)= ### Configuring operators @@ -510,6 +512,149 @@ def compose(self): ``` ```` + +(configuring-app-operator-native-resources)= + +#### Native resource creation + +The resources bundled with the SDK are wrapping an underlying GXF component. However, it is also possible to define a "native" resource without any need to create and wrap an underlying GXF component. Such a resource can also be passed conditionally to an operator in the same way as the resources created in the previous section. + +For example: + +`````{tab-set} +````{tab-item} C++ +To create a native resource, implement a class that inherits from {cpp:class}`Resource ` + +```{code-block} cpp +namespace holoscan { + +class MyNativeResource : public holoscan::Resource { + public: + HOLOSCAN_RESOURCE_FORWARD_ARGS_SUPER(MyNativeResource, Resource) + + MyNativeResource() = default; + + // add any desired parameters in the setup method + // (a single string parameter is shown here for illustration) + void setup(ComponentSpec& spec) override { + spec.param(message_, "message", "Message string", "Message String", std::string("test message")); + } + + // add any user-defined methods (these could be called from an Operator's compute method) + std::string message() { return message_.get(); } + + private: + Parameter message_; +}; +} // namespace: holoscan +``` + +The `setup` method can be used to define any parameters needed by the resource. + +This resource can be used with a C++ operator, just like any other resource. For example, an operator could have a parameter holding a shared pointer to `MyNativeResource` as below. + +```{code-block} cpp +private: + +class MyOperator : public holoscan::Operator { + public: + HOLOSCAN_OPERATOR_FORWARD_ARGS(MyOperator) + + MyOperator() = default; + + void setup(OperatorSpec& spec) override { + spec.param(message_resource_, "message_resource", "message resource", + "resource printing a message"); + } + + void compute(InputContext&, OutputContext& op_output, ExecutionContext&) override { + HOLOSCAN_LOG_TRACE("MyOp::compute()"); + + // get a resource based on its name (this assumes the app author named the resource "message_resource") + auto res = resource("message_resource"); + if (!res) { + throw std::runtime_error("resource named 'message_resource' not found!"); + } + + // call a method on the retrieved resource class + auto message = res->message(); + + }; + +private: + Parameter message_resource_; +} +``` +The `compute` method above demonstrates how the templated `resource` method can be used to retrieve a resource. + + +and the resource could be created and passed via a named argument in the usual way +```{code-block} cpp + +// example code for within Application::compose (or Fragment::compose) + + auto message_resource = make_resource( + "message_resource", holoscan::Arg("message", "hello world"); + + auto my_op = std::make_operator( + "my_op", holoscan::Arg("message_resource", message_resource)); +``` + +As with GXF-based resources, it is also possible to pass a native resource as a positional argument to the operator constructor. + +For a concreate example of native resource use in a real application, see the [volume_rendering_xr application](https://github.com/nvidia-holoscan/holohub/blob/main/applications/volume_rendering_xr/main.cpp) on Holohub. This application uses a native [XrSession resource](https://github.com/nvidia-holoscan/holohub/blob/main/operators/XrFrameOp/xr_session.hpp) type which corresponds to a single OpenXR session. This single "session" resource can then be shared by both the `XrBeginFrameOp` and `XrEndFrameOp` operators. + +```` +````{tab-item} Python +To create a native resource, implement a class that inherits from {py:class}`Resource `. + +```{code-block} python +class MyNativeResource(Resource): + def __init__(self, fragment, message="test message", *args, **kwargs): + self.message = message + super().__init__(fragment, *args, **kwargs) + + # Could optionally define Parameter as in C++ via spec.param as below. + # Here, we chose instead to pass message as an argument to __init__ above. + # def setup(self, spec: ComponentSpec): + # spec.param("message", "test message") + + # define a custom method + def message(self): + return self.message +``` + +The below shows how some custom operator could use such a resource in its compute method + +```{code-block} python +class MyOperator(Operator): + def compute(self, op_input, op_output, context): + resource = self.resource("message_resource") + if resource is None: + raise ValueError("expected message resource not found") + assert isinstance(resource, MyNativeResource) + + print(f"message = {resource.message()") +``` + +where this native resource could have been created and passed positionally to `MyOperator` as follows + +```{code-block} python + +# example code within Application.compose (or Fragment.compose) + + message_resource = MyNativeResource( + fragment=self, message="hello world", name="message_resource") + + # pass the native resource as a positional argument to MyOperator + my_op = MyOperator(fragment=self, message_resource) +``` +```` +````` + +There is a minimal example of native resource use in the [examples/native](https://github.com/nvidia-holoscan/holoscan-sdk/blob/main/examples/native/) folder. + + (configuring-app-scheduler)= ### Configuring the scheduler @@ -518,14 +663,16 @@ The [scheduler](./components/schedulers.md) controls how the application schedul The default scheduler is a single-threaded [`GreedyScheduler`](./components/schedulers.md#greedy-scheduler). An application can be configured to use a different scheduler `Scheduler` ({cpp:class}`C++ `/{py:class}`Python `) or change the parameters from the default scheduler, using the `scheduler()` function ({cpp:func}`C++ `/{py:func}`Python `). -For example, if an application needs to run multiple operators in parallel, the [`MultiThreadScheduler`](./components/schedulers.md#multithreadscheduler) or [`EventBasedScheduler`](./components/schedulers.md#eventbasedscheduler) can instead be used. The difference between the two is that the MultiThreadScheduler is based on actively polling operators to determine if they are ready to execute, while the EventBasedScheduler will instead wait for an event indicating that an operator is ready to execute. +For example, if an application needs to run multiple operators in parallel, the [`MultiThreadScheduler`](./components/schedulers.md#multithread-scheduler) or [`EventBasedScheduler`](./components/schedulers.md#event-based-scheduler) can instead be used. The difference between the two is that the MultiThreadScheduler is based on actively polling operators to determine if they are ready to execute, while the EventBasedScheduler will instead wait for an event indicating that an operator is ready to execute. The code snippet belows shows how to set and configure a non-default scheduler: `````{tab-set} ````{tab-item} C++ + - We create an instance of a {ref}`holoscan::Scheduler ` derived class by using the {cpp:func}`~holoscan::Fragment::make_scheduler` function. Like operators, parameters can come from explicit {cpp:class}`~holoscan::Arg`s or {cpp:class}`~holoscan::ArgList`, or from a YAML configuration. - The {cpp:func}`~holoscan::Fragment::scheduler` method assigns the scheduler to be used by the application. + ```{code-block} cpp :emphasize-lines: 2-7 :name: holoscan-config-scheduler-cpp @@ -539,10 +686,13 @@ auto scheduler = app->make_scheduler( app->scheduler(scheduler); app->run(); ``` + ```` + ````{tab-item} Python - We create an instance of a `Scheduler` class in the {py:mod}`~holoscan.schedulers` module. Like operators, parameters can come from an explicit {py:class}`~holoscan.core.Arg` or {py:class}`~holoscan.core.ArgList`, or from a YAML configuration. - The {py:func}`~holoscan.core.Fragment.scheduler` method assigns the scheduler to be used by the application. + ```{code-block} python :emphasize-lines: 2-8 :name: holoscan-config-scheduler-python diff --git a/docs/holoscan_create_operator.md b/docs/holoscan_create_operator.md index ca5473c8..517c6fe0 100644 --- a/docs/holoscan_create_operator.md +++ b/docs/holoscan_create_operator.md @@ -419,7 +419,7 @@ class PingRxOp : public holoscan::ops::GXFOperator { The Holoscan SDK provides built-in data types called **{ref}`Domain Objects`**, defined in the `include/holoscan/core/domain` directory. For example, the {cpp:class}`holoscan::Tensor` is a Domain Object class that is used to represent a multi-dimensional array of data, which can be used directly by `OperatorSpec`, `InputContext`, and `OutputContext`. :::{tip} -This {cpp:class}`holoscan::Tensor` class is a wrapper around the {cpp:class}`~holoscan::DLManagedTensorCtx` struct holding a [DLManagedTensor](https://dmlc.github.io/dlpack/latest/c_api.html#_CPPv415DLManagedTensor) object. As such, it provides a primary interface to access Tensor data and is interoperable with other frameworks that support the [DLPack interface](https://dmlc.github.io/dlpack/latest/). +This {cpp:class}`holoscan::Tensor` class is a wrapper around the {cpp:class}`~holoscan::DLManagedTensorContext` struct holding a [DLManagedTensor](https://dmlc.github.io/dlpack/latest/c_api.html#c.DLManagedTensor) object. As such, it provides a primary interface to access Tensor data and is interoperable with other frameworks that support the [DLPack interface](https://dmlc.github.io/dlpack/latest/). ::: :::{warning} @@ -800,9 +800,17 @@ There are no differences in CMake between using a GXF operator and [using a nati ### Interoperability between GXF and native C++ operators To support sending or receiving tensors to and from operators (both GXF and native C++ operators), the Holoscan SDK provides the C++ classes below: -- A class template called {cpp:class}`holoscan::MyMap` which inherits from `std::unordered_map>`. The template parameter `T` can be any type, and it is used to specify the type of the `std::shared_ptr` objects stored in the map. -- -A {cpp:class}`holoscan::TensorMap` class defined as a specialization of `holoscan::Map` for the {cpp:class}`holoscan::Tensor` type. +- A class template called {cpp:class}`holoscan::Map` which inherits from `std::unordered_map>`. The template parameter `T` can be any type, and it is used to specify the type of the `std::shared_ptr` objects stored in the map. +- A {cpp:class}`holoscan::TensorMap` class defined as a specialization of `holoscan::Map` for the {cpp:class}`holoscan::Tensor` type. + +When a message with a {cpp:class}`holoscan::TensorMap` is emitted from a native C++ operator, +the message object is always converted to a {cpp:class}`holoscan::gxf::Entity` object and sent to the +downstream operator. + +Then, if the sent GXF Entity object holds only Tensor object(s) as its components, the downstream operator can receive the message data as a {cpp:class}`holoscan::TensorMap` object instead of a {cpp:class}`holoscan::gxf::Entity` object. + +[{numref}`fig-holoscan-tensor-interoperability`](fig-holoscan-tensor-interoperability) shows the relationship between the {cpp:class}`holoscan::gxf::Entity` and {cpp:class}`nvidia::gxf:Entity` classes and the relationship +between the {cpp:class}`holoscan::Tensor` and {cpp:class}`nvidia::gxf::Tensor` classes. :::{figure-md} fig-holoscan-tensor-interoperability :align: center @@ -813,18 +821,27 @@ A {cpp:class}`holoscan::TensorMap` class defined as a specialization of `holosca Supporting Tensor Interoperability ::: -Consider the following example, where `GXFSendTensorOp` and `GXFReceiveTensorOp` are GXF operators, and where `ProcessTensorOp` is a C++ native operator: +Both {cpp:class}`holoscan::gxf::Tensor` and {cpp:class}`nvidia::gxf::Tensor` are interoperable with each other because they are wrappers around the same underlying {cpp:class}`~holoscan::DLManagedTensorContext` struct holding a [DLManagedTensor](https://dmlc.github.io/dlpack/latest/c_api.html#c.DLManagedTensor) object. + +The `holoscan::TensorMap` class is used to store multiple tensors in a map, where each tensor is associated with a unique key. The `holoscan::TensorMap` class is used to pass multiple tensors between operators, and it is used in the same way as a `std::unordered_map>` object. + +Since both {cpp:class}`holoscan::TensorMap` and {cpp:class}`holoscan::gxf::Entity` objects hold tensors which are interoperable, the message data between GXF and native C++ operators are also interoperable. + +[{numref}`fig-tensor-interop-between-cpp-native-op-and-gxf-op`](fig-tensor-interop-between-cpp-native-op-and-gxf-op) illustrates the use of the `holoscan::TensorMap` class to pass multiple tensors between operators. The `GXFSendTensorOp` operator sends a `nvidia::gxf::Entity` object (containing a `nvidia::gxf::Tensor` object as a GXF component named "tensor") to the `ProcessTensorOp` operator, which processes the tensors and then forwards the processed tensors to the `GXFReceiveTensorOp` operator. + +Consider the following example, where `GXFSendTensorOp` and `GXFReceiveTensorOp` are GXF operators, and where `ProcessTensorOp` is a Holoscan native operator in C++: ```{digraph} interop +:name: fig-tensor-interop-between-cpp-native-op-and-gxf-op :align: center :caption: The tensor interoperability between C++ native operator and GXF operator rankdir="LR" node [shape=record]; - source [label="GXFSendTensorOp| |signal(out) : Tensor"]; + source [label="GXFSendTensorOp| |signal(out) : gxf::Entity"]; process [label="ProcessTensorOp| [in]in : TensorMap | out(out) : TensorMap "]; - sink [label="GXFReceiveTensorOp| [in]signal : Tensor | "]; + sink [label="GXFReceiveTensorOp| [in]signal : gxf::Entity | "]; source->process [label="signal...in"] process->sink [label="out...signal"] @@ -835,14 +852,14 @@ The following code shows how to implement `ProcessTensorOp`'s `compute()` method ```{code-block} cpp :caption: examples/tensor_interop/cpp/tensor_interop.cpp :linenos: true -:lineno-start: 81 -:emphasize-lines: 4,6,20,21,24 +:lineno-start: 86 +:emphasize-lines: 4,6,8,12,17,22,23,26 -void compute(InputContext& op_input, OutputContext& op_output, + void compute(InputContext& op_input, OutputContext& op_output, ExecutionContext& context) override { // The type of `in_message` is 'holoscan::TensorMap'. auto in_message = op_input.receive("in").value(); - // the type of out_message is TensorMap + // The type of out_message is TensorMap TensorMap out_message; for (auto& [key, tensor] : in_message) { // Process with 'tensor' here. @@ -1488,3 +1505,8 @@ This would define To learn more about overriding connectors and/or conditions there is a [multi_branch_pipeline](https://github.com/nvidia-holoscan/holoscan-sdk/blob/main/examples/multi_branch_pipeline) example which overrides default conditions to allow two branches of a pipeline to run at different frame rates. There is also an example of increasing the queue sizes available in [this Python queue policy test application](https://github.com/nvidia-holoscan/holoscan-sdk/blob/main/python/tests/system/test_application_with_repeated_emit_on_same_port.py). +### Using the Holoscan SDK with Other Libraries + +The Holoscan SDK enables seamless integration with various powerful, GPU-accelerated libraries to build efficient, high-performance pipelines. + +Please refer to the [Best Practices to Integrate External Libraries into Holoscan Pipelines](https://github.com/nvidia-holoscan/holohub/blob/main/tutorials/integrate_external_libs_into_pipeline/README.md) tutorial in the [HoloHub](https://github.com/nvidia-holoscan/holohub) repository for detailed examples and more information on Holoscan's tensor interoperability and handling CUDA libraries in the pipeline. This includes [CUDA Python](https://github.com/NVIDIA/cuda-python), [CuPy](https://cupy.dev/), [MatX](https://github.com/NVIDIA/MatX) for C++, [cuCIM](https://github.com/rapidsai/cucim), [CV-CUDA](https://github.com/CVCUDA/CV-CUDA), and [OpenCV](https://opencv.org/) for integration into Holoscan applications. diff --git a/docs/holoscan_create_operator_python_bindings.md b/docs/holoscan_create_operator_python_bindings.md index d9a02204..176cdb85 100644 --- a/docs/holoscan_create_operator_python_bindings.md +++ b/docs/holoscan_create_operator_python_bindings.md @@ -418,10 +418,11 @@ namespace holoscan { */ template <> struct emitter_receiver> { - static void emit(py::object& data, const std::string& name, PyOutputContext& op_output) { + static void emit(py::object& data, const std::string& name, PyOutputContext& op_output, + const int64_t acq_timestamp = -1) { auto input_spec = data.cast>(); py::gil_scoped_release release; - op_output.emit>(input_spec, name.c_str()); + op_output.emit>(input_spec, name.c_str(), acq_timestamp); return; } @@ -494,7 +495,7 @@ When creating Python bindings for an Operator on Holohub, the [pybind11_add_holo For types for which Pybind11's default casting between C++ and Python is adequate, it is not necessary to explicitly define the `emitter_receiver` class as shown in step 1. This is true because there are a couple of [default implementations](https://github.com/nvidia-holoscan/holoscan-sdk/blob/v2.1.0/python/holoscan/core/emitter_receiver_registry.hpp) for `emitter_receiver` and `emitter_receiver>` that already cover common cases. The default emitter_receiver works for the `std::vector` type shown above, which is why the code shown for illustration there is [not found within the operator's bindings](https://github.com/nvidia-holoscan/holoscan-sdk/blob/main/python/holoscan/operators/holoviz/holoviz.cpp). In that case one could immediately implement `register_types` from step 2 without having to explicitly create an `emitter_receiver` class. -An example where the default `emitter_receiver` would not work is the custom one defined by the SDK for `pybind11::dict`. In this case, to provide convenient emit of multiple tensors via passing a `dict[holoscan::Tensor]` to `op_output.emit` we have special handling of Python dictionaries. The dictionary is inspected and if all keys are strings and all values are tensor-like objects, a single C++ `nvidia::gxf::Entity` containing all of the tensors as an `nvidia::gxf::Tensor` is emitted. If the dictionary is not a tensor map, then it is just emitted as a shared pointer to the Python dict object. The `emitter_receiver` implementations used for the core SDK are defined in [emitter_receivers.hpp](https://github.com/nvidia-holoscan/holoscan-sdk/blob/v2.1.0/python/holoscan/core/emitter_receivers.hpp). These can serve as a reference when creating new ones for additional types. +An example where the default `emitter_receiver` would not work is the custom one defined by the SDK for `pybind11::dict`. In this case, to provide convenient emit of multiple tensors via passing a `dict[holoscan::Tensor]` to `op_output.emit` we have special handling of Python dictionaries. The dictionary is inspected and if all keys are strings and all values are tensor-like objects, a single C++ `nvidia::gxf::Entity` containing all of the tensors as an `nvidia::gxf::Tensor` is emitted. If the dictionary is not a tensor map, then it is just emitted as a shared pointer to the Python dict object. The `emitter_receiver` implementations used for the core SDK are defined in [emitter_receivers.hpp](https://github.com/nvidia-holoscan/holoscan-sdk/blob/v2.2.0/python/holoscan/core/emitter_receivers.hpp). These can serve as a reference when creating new ones for additional types. #### Runtime behavior of emit and receive @@ -523,7 +524,7 @@ Only types registered with the SDK can be specified by name in this third argume #### Table of types registered by the core SDK -The list of types that are registered with the SDK's `EmitterReceiverRegistry` are given in the table below. +The list of types that are registered with the SDK's `EmitterReceiverRegistry` are given in the table below. C++ Type | name in the EmitterReceiverRegistry -------------------------------------------------------|------------------------------------------- diff --git a/docs/holoscan_packager.md b/docs/holoscan_packager.md index 23ca55f1..06192ce1 100644 --- a/docs/holoscan_packager.md +++ b/docs/holoscan_packager.md @@ -95,6 +95,8 @@ The NGC container has the CLI installed already, no additional steps are require :::{tip} The packager feature is also illustrated in the [cli_packager](https://github.com/nvidia-holoscan/holoscan-sdk/tree/main/examples/cli_packager) and [video_replayer_distributed]() examples. + +Additional arguments are required when launching the container to enable the packaging of Holoscan applications inside the NGC Holoscan container. Please see the [NGC Holoscan container](https://catalog.ngc.nvidia.com/orgs/nvidia/teams/clara-holoscan/containers/holoscan) page for additional details. ::: 1. Ensure to use the [HAP environment variables](./cli/hap.md#table-of-environment-variables) wherever possible when accessing data. For example: @@ -225,6 +227,35 @@ The packager feature is also illustrated in the [cli_packager](https://github.co holoscan package --platform x64-workstation --tag my-awesome-app --config /path/to/my/awesome/application/config.yaml /path/to/my/awesome/application/ ``` +### Common Issues When Using Holoscan Packager + +#### DNS Name Resolution Error + +The Holoscan Packager may be unable to resolve hostnames in specific networking environments and may show errors similar to the following: + +``` +curl: (6) Could not resolve host: github.com. +Failed to establish a new connection:: [Errno -3] Temporary failure in name solution... +``` + +To resolve these errors, edit the `/etc/docker/daemon.json` file to include `dns` and `dns-serach` fields as follows: + +```json +{ + "default-runtime": "nvidia", + "runtimes": { + "nvidia": { + "args": [], + "path": "nvidia-container-runtime" + } + }, + "dns": ["IP-1", "IP-n"], + "dns-search": ["DNS-SERVER-1", "DNS-SERVER-n"] +} +``` + +You may need to consult your IT team and replace `IP-x` and `DNS-SERVER-x` with the provided values. + ## Run a packaged application The packaged Holoscan application container image can run with the [Holoscan App Runner](./cli/run.md): diff --git a/docs/images/holoscan_tensor_interoperability.png b/docs/images/holoscan_tensor_interoperability.png index 8f7f8ed1..7ec577fa 100644 Binary files a/docs/images/holoscan_tensor_interoperability.png and b/docs/images/holoscan_tensor_interoperability.png differ diff --git a/docs/inference.md b/docs/inference.md index 00765f09..2f92697b 100644 --- a/docs/inference.md +++ b/docs/inference.md @@ -93,6 +93,12 @@ Required parameters and related features available with the Holoscan Inference M - If the `temporal_map` is absent in the parameter set, all models are inferred for all the frames. - All models are not mandatory in the `temporal_map`. The missing models are inferred per frame. - Temporal map based inferencing is supported for all backends. + - `activation_map`: Dynamic inferencing can be enabled with this parameter. It is populated in the parameter set and is updated at runtime. + - Each entry in `activation_map` has a unique keyword representing the model (same as used in `model_path_map` and `pre_processor_map`), and activation state as the value. Activation state represents whether the model will be used for inferencing or not on a given frame. Any model(s) with a value of 1 will be active and will be used for inference, and any model(s) with a value of 0 will not run. The activation map must be initialized in the parameter set for all the models that need to be activated or deactivated dynamically. + - When the activation state is 0 for a particular model in the `activation_map`, the inference operator will not launch the inference for the model and will emits the last inferred result for the model. + - If the `activation_map` is absent in the parameter set, all of the models are inferred for all frames. + - All models are not mandatory in the `activation_map`. The missing models are active on every frame. + - Activation map based dynamic inferencing is supported for all backends. - `backend_map`: Multiple backends can be used in the same application with this parameter. - Each entry in `backend_map` has a unique keyword representing the model (same as used in `model_path_map`), and the `backend` as the value. - A sample backend_map is shown below. In the example, model_1 uses the `tensorRT` backend, and model 2 and model 3 uses the `torch` backend for inference. diff --git a/docs/sdk_installation.md b/docs/sdk_installation.md index 95ee5d58..916b10ca 100644 --- a/docs/sdk_installation.md +++ b/docs/sdk_installation.md @@ -17,7 +17,7 @@ Set up your developer kit: Developer Kit | User Guide | OS | GPU Mode ------------- | ---------- | --- | --- -[NVIDIA IGX Orin][igx] | [Guide][igx-guide] | [IGX Software][igx-sw] 1.0 DP | iGPU **or*** dGPU +[NVIDIA IGX Orin][igx] | [Guide][igx-guide] | [IGX Software][igx-sw] 1.0 Production Release | iGPU **or*** dGPU [NVIDIA Jetson AGX Orin and Orin Nano][jetson-orin] | [Guide][jetson-guide] | [JetPack][jp] 6.0 | iGPU [NVIDIA Clara AGX][clara-agx]
_Only supporting the NGC container_ | [Guide][clara-guide] | [HoloPack][sdkm] 1.2 | iGPU **or*** dGPU @@ -81,11 +81,11 @@ We provide multiple ways to install and run the Holoscan SDK: ````{tab-item} NGC Container - **dGPU** (x86_64, IGX Orin dGPU, Clara AGX dGPU, GH200) ```bash - docker pull nvcr.io/nvidia/clara-holoscan/holoscan:v1.0.3-dgpu + docker pull nvcr.io/nvidia/clara-holoscan/holoscan:v2.2.0-dgpu ``` - **iGPU** (Jetson, IGX Orin iGPU, Clara AGX iGPU) ```bash - docker pull nvcr.io/nvidia/clara-holoscan/holoscan:v1.0.3-igpu + docker pull nvcr.io/nvidia/clara-holoscan/holoscan:v2.2.0-igpu ``` See details and usage instructions on [NGC][container]. ```` diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index e77b9b86..0405ee9f 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -33,6 +33,7 @@ add_subdirectory(ping_custom_op) add_subdirectory(ping_multi_port) add_subdirectory(ping_distributed) add_subdirectory(ping_vector) +add_subdirectory(python_decorator) add_subdirectory(resources) add_subdirectory(tensor_interop) add_subdirectory(v4l2_camera) diff --git a/examples/README.md b/examples/README.md index 2845b78d..ca235981 100644 --- a/examples/README.md +++ b/examples/README.md @@ -72,6 +72,10 @@ The following examples illustrate the use of specific resource classes that can * [**Clock**](resources/clock): demonstrate assignment of a user-configured clock to the Holoscan SDK scheduler and how its runtime methods can be accessed from an operator's compute method. +## Decorator-based Python API + +* [**Python Functions as Operators**](python_decorator): demonstrates how to use a decorator to convert a Python function into an Operator. + ## Visualization * [**Holoviz**](holoviz): display overlays of various geometric primitives diff --git a/examples/conditions/CMakeLists.txt b/examples/conditions/CMakeLists.txt index bfdf68ba..bc240d35 100644 --- a/examples/conditions/CMakeLists.txt +++ b/examples/conditions/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,4 +14,6 @@ # limitations under the License. add_subdirectory(asynchronous) +add_subdirectory(expiring_message) add_subdirectory(periodic) + diff --git a/examples/conditions/expiring_message/CMakeLists.txt b/examples/conditions/expiring_message/CMakeLists.txt new file mode 100644 index 00000000..91b8d28e --- /dev/null +++ b/examples/conditions/expiring_message/CMakeLists.txt @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +add_subdirectory(cpp) +add_subdirectory(python) + +file(RELATIVE_PATH app_relative_dest_path ${CMAKE_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) + +install( + FILES README.md + DESTINATION "${app_relative_dest_path}" + COMPONENT "holoscan-examples" +) diff --git a/examples/conditions/expiring_message/README.md b/examples/conditions/expiring_message/README.md new file mode 100644 index 00000000..1a8882a5 --- /dev/null +++ b/examples/conditions/expiring_message/README.md @@ -0,0 +1,51 @@ +# Holoscan::ExpiringMessageAvailableCondition + +This example demonstrates how to use Holoscan::ExpiringMessageAvailableCondition. + +*Visit the [SDK User Guide](https://docs.nvidia.com/holoscan/sdk-user-guide/components/conditions.html) to learn more about the ExpiringMessageAvailable Condition.* + +## C++ API + +This example has two operators involved: + 1. a transmitter that a transmitter, set to transmit a string message `Periodic ping...` on port `out`. This operator is configured to be executed 8 times each subsequent message is sent only after a period of 10 milliseconds has elapsed. + 2. a receiver that waits for a 5 messages to be batched together to call compute. If 5 messages have not arrived by a specified interval of 1 second, compute will be called at that time. + +Note that the `ExpiringMessageAvailableCondition` added to the input port of the receive operator requires that the message sent by the output port of the transmit operator attaches a timestamp. This timestamp is needed to be able to enforce the `max_delay_ns` timeout interval used by the condition. + + +### Build instructions + +Built with the SDK, see instructions from the top level README. + +### Run instructions + +First, go in your `build` or `install` directory (automatically done by `./run launch`). + +Then, run: +```bash +./examples/conditions/expiring_message/cpp/ping_expiring_message +``` + +## Python API + +This example demonstrates the use of ExpiringMessageAvailableCondition using python API. This is a simple ping application with two operators connected using add_flow(). + +There are two operators involved in this example: + 1. a transmitter that on each tick, transmits an integer to the "out" port. This operator is configured to be executed 8 times each subsequent message is sent only after a period of 10 milliseconds has elapsed. + 3. a receiver that will wait for 5 messages from the "in" port before it will call compute. If 5 messages have not arrived by the specified interval of 1 second, compute will be called at that time. + +Note that the `ExpiringMessageAvailableCondition` added to the input port of the receive operator requires that the message sent by the output port of the transmit operator attaches a timestamp. This timestamp is needed to be able to enforce the `max_delay_ns` timeout interval used by the condition. + +### Build instructions + +Built with the SDK, see instructions from the top level README. + +### Run instructions + +First, go in your `build` or `install` directory (automatically done by `./run launch`). + +Then, run the following command. + +```bash +python3 ./examples/conditions/expiring_message/python/ping_expiring_message.py +``` diff --git a/examples/conditions/expiring_message/cpp/CMakeLists.min.txt b/examples/conditions/expiring_message/cpp/CMakeLists.min.txt new file mode 100644 index 00000000..b37d3562 --- /dev/null +++ b/examples/conditions/expiring_message/cpp/CMakeLists.min.txt @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the \"License\"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an \"AS IS\" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.20) +project(ping_expiring_message CXX) + +# Finds the package holoscan +find_package(holoscan REQUIRED CONFIG + PATHS "/opt/nvidia/holoscan" "/workspace/holoscan-sdk/install") + +add_executable(ping_expiring_message + ping_expiring_message.cpp +) + +target_link_libraries(ping_expiring_message + PRIVATE + holoscan::core +) + +# Copy config file to the build tree +add_custom_target(ping_expiring_message_yaml + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/ping_expiring_message.yaml" ${CMAKE_CURRENT_BINARY_DIR} +) +add_dependencies(ping_expiring_message ping_expiring_message_yaml) + +# Testing +if(BUILD_TESTING) + add_test(NAME EXAMPLE_CPP_PING_EXPIRING_MESSAGE_TEST + COMMAND ping_expiring_message + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + set_tests_properties(EXAMPLE_CPP_PING_EXPIRING_MESSAGE_TEST PROPERTIES + PASS_REGULAR_EXPRESSION "Rx message received: Expiring message ping") +endif() diff --git a/examples/conditions/expiring_message/cpp/CMakeLists.txt b/examples/conditions/expiring_message/cpp/CMakeLists.txt new file mode 100644 index 00000000..553a0e36 --- /dev/null +++ b/examples/conditions/expiring_message/cpp/CMakeLists.txt @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Create examples +add_executable(ping_expiring_message + ping_expiring_message.cpp +) +target_link_libraries(ping_expiring_message + PRIVATE + holoscan::core +) + +# Copy config file to the build tree +add_custom_target(ping_expiring_message_yaml + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/ping_expiring_message.yaml" ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "ping_expiring_message.yaml" + BYPRODUCTS "ping_expiring_message.yaml" +) +add_dependencies(ping_expiring_message ping_expiring_message_yaml) + +# Install examples + +# Set the install RPATH based on the location of the Holoscan SDK libraries +# The GXF extensions are loaded by the GXF libraries - no need to include here +file(RELATIVE_PATH install_lib_relative_path ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/${HOLOSCAN_INSTALL_LIB_DIR}) +set_target_properties(ping_expiring_message PROPERTIES INSTALL_RPATH "\$ORIGIN/${install_lib_relative_path}") + +# Install following the relative folder path +file(RELATIVE_PATH app_relative_dest_path ${CMAKE_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) + +if(HOLOSCAN_INSTALL_EXAMPLE_SOURCE) +# Install the source +install(FILES ping_expiring_message.cpp + DESTINATION "${app_relative_dest_path}" + COMPONENT holoscan-examples +) + +# Install the minimal CMakeLists.txt file +install(FILES CMakeLists.min.txt + RENAME "CMakeLists.txt" + DESTINATION "${app_relative_dest_path}" + COMPONENT holoscan-examples +) +endif() + +# Install the compiled example +install(TARGETS ping_expiring_message + DESTINATION "${app_relative_dest_path}" + COMPONENT holoscan-examples +) + +# Install the configuration file +install(FILES + "${CMAKE_CURRENT_SOURCE_DIR}/ping_expiring_message.yaml" + DESTINATION "${app_relative_dest_path}" + COMPONENT holoscan-examples +) + +# Testing +if(HOLOSCAN_BUILD_TESTS) + add_test(NAME EXAMPLE_CPP_PING_EXPIRING_MESSAGE_TEST + COMMAND ping_expiring_message + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + set_tests_properties(EXAMPLE_CPP_PING_EXPIRING_MESSAGE_TEST PROPERTIES + PASS_REGULAR_EXPRESSION "Rx message received: ExpiringMessageAvailable ping: 8") +endif() diff --git a/examples/conditions/expiring_message/cpp/ping_expiring_message.cpp b/examples/conditions/expiring_message/cpp/ping_expiring_message.cpp new file mode 100644 index 00000000..e1ee40c6 --- /dev/null +++ b/examples/conditions/expiring_message/cpp/ping_expiring_message.cpp @@ -0,0 +1,126 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "holoscan/holoscan.hpp" +#include "holoscan/core/conditions/gxf/expiring_message.hpp" + +namespace holoscan::ops { + +class PingTxOp : public Operator { + public: + HOLOSCAN_OPERATOR_FORWARD_ARGS(PingTxOp) + + PingTxOp() = default; + + void setup(OperatorSpec& spec) override { spec.output>("out"); } + + void compute(InputContext&, OutputContext& op_output, ExecutionContext&) override { + auto value = + std::make_shared(fmt::format("ExpiringMessageAvailable ping: {}", index_)); + ++index_; + + // retrieve the scheduler used for this application via it's fragment + auto scheduler = fragment_->scheduler(); + // To get the clock we currently have to cast the scheduler to gxf::GXFScheduler. + // TODO: Refactor C++ lib so the clock method is on Scheduler rather than GXFScheduler. + // That would allow us to avoid this dynamic_pointer_cast, but might require adding + // renaming Clock->GXFClock and then adding a new holoscan::Clock independent of GXF. + auto gxf_scheduler = std::dynamic_pointer_cast(scheduler); + auto clock = gxf_scheduler->clock(); + auto timestamp = clock->timestamp(); + + // emitting a timestamp is necessary for this port to be connected to an input port that is + // using a ExpiringMessageAvailableCondition + op_output.emit(value, "out", timestamp); + }; + + private: + int index_ = 1; +}; + +class PingRxOp : public Operator { + public: + HOLOSCAN_OPERATOR_FORWARD_ARGS(PingRxOp) + + PingRxOp() = default; + + void setup(OperatorSpec& spec) override { + ArgList expiring_message_arglist{Arg("max_batch_size", static_cast(5)), + Arg("max_delay_ns", static_cast(1'000'000'000))}; + spec.input>("in") + .connector(IOSpec::ConnectorType::kDoubleBuffer, + Arg("capacity", static_cast(5)), + Arg("policy", static_cast(1))) + .condition(ConditionType::kExpiringMessageAvailable, expiring_message_arglist); + } + + void compute(InputContext& op_input, OutputContext&, ExecutionContext&) override { + auto in_value = op_input.receive>("in"); + + HOLOSCAN_LOG_INFO("PingRxOp::compute() called"); + + while (in_value) { + auto message = in_value.value(); + if (message) { + HOLOSCAN_LOG_INFO("Rx message received: {}", message->c_str()); + } else { + HOLOSCAN_LOG_INFO("Rx message received: nullptr"); + } + in_value = op_input.receive>("in"); + } + }; +}; + +} // namespace holoscan::ops + +class App : public holoscan::Application { + public: + void compose() override { + using namespace holoscan; + using namespace std::chrono_literals; + // Configure the operators. Here we use CountCondition to terminate + // execution after a specific number of messages have been sent. + // PeriodicCondition is used so that each subsequent message is + // sent only after a period of 10 milliseconds has elapsed. + auto tx = make_operator( + "tx", + make_condition("count-condition", 8), + make_condition("periodic-condition", 0.01s)); + + auto rx = make_operator("rx"); + + add_flow(tx, rx); + } +}; + +int main(int argc, char** argv) { + auto app = holoscan::make_application(); + + // Get the configuration + auto config_path = std::filesystem::canonical(argv[0]).parent_path(); + config_path /= std::filesystem::path("ping_expiring_message.yaml"); + app->config(config_path); + auto& tracker = app->track(0, 0, 0); + tracker.enable_logging(); + app->run(); + tracker.print(); + return 0; +} diff --git a/examples/conditions/expiring_message/cpp/ping_expiring_message.yaml b/examples/conditions/expiring_message/cpp/ping_expiring_message.yaml new file mode 100644 index 00000000..8469d54d --- /dev/null +++ b/examples/conditions/expiring_message/cpp/ping_expiring_message.yaml @@ -0,0 +1,18 @@ +%YAML 1.2 +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +extensions: + - libgxf_std.so diff --git a/examples/conditions/expiring_message/python/CMakeLists.txt b/examples/conditions/expiring_message/python/CMakeLists.txt new file mode 100644 index 00000000..279cec97 --- /dev/null +++ b/examples/conditions/expiring_message/python/CMakeLists.txt @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Get relative folder path for the app +file(RELATIVE_PATH app_relative_dest_path ${CMAKE_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) + +# Copy native operator ping application +add_custom_target(python_ping_expiring_message ALL + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/ping_expiring_message.py" ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "ping_expiring_message.py" + BYPRODUCTS "ping_expiring_message,py" +) + +# Install the app +install(FILES + "${CMAKE_CURRENT_SOURCE_DIR}/ping_expiring_message.py" + DESTINATION "${app_relative_dest_path}" + COMPONENT "holoscan-examples" +) + +# Testing +if(HOLOSCAN_BUILD_TESTS) + add_test(NAME EXAMPLE_PYTHON_PING_EXPIRING_MESSAGE + COMMAND python3 ping_expiring_message.py + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + set_tests_properties(EXAMPLE_PYTHON_PING_EXPIRING_MESSAGE PROPERTIES + PASS_REGULAR_EXPRESSION "ExpiringMessageAvailable ping: 8" + ) +endif() \ No newline at end of file diff --git a/examples/conditions/expiring_message/python/ping_expiring_message.py b/examples/conditions/expiring_message/python/ping_expiring_message.py new file mode 100644 index 00000000..fbdb091e --- /dev/null +++ b/examples/conditions/expiring_message/python/ping_expiring_message.py @@ -0,0 +1,126 @@ +""" + SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + SPDX-License-Identifier: Apache-2.0 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" # noqa: E501 + +from holoscan.conditions import CountCondition, PeriodicCondition +from holoscan.core import Application, ConditionType, IOSpec, Operator, OperatorSpec +from holoscan.schedulers import GreedyScheduler + + +class PingTxOp(Operator): + """Simple transmitter operator. + + On each tick, it transmits an integer to the "out" port. + + **==Named Outputs==** + + out : int + An index value that increments by one on each call to `compute`. The starting value is + 1. + """ + + def __init__(self, fragment, *args, **kwargs): + self.index = 1 + # Need to call the base class constructor last + super().__init__(fragment, *args, **kwargs) + + def setup(self, spec: OperatorSpec): + spec.output("out") + + def compute(self, op_input, op_output, context): + # we can retrieve the scheduler used for this application via it's fragment + scheduler = self.fragment.scheduler() + + # The scheduler's clock is available as a parameter. + # The clock object has methods to retrieve timestamps. + + # To get the clock we currently have to set the scheduler manually in the main method. + # TODO: Refactor C++ lib so the clock method is on Scheduler rather than GXFScheduler. # noqa: E501, FIX002 + # That would allow us to access 'clock' attribute without manually setting scheduler, + # but might require adding renaming Clock->GXFClock and then adding + # a new holoscan::Clock independent of GXF. + clock = scheduler.clock + + print(f"Sending message: {self.index}") + + # Note that we must set acq_timestamp so that the timestamp required by + # the ExpiringMessageAvailableCondition on the downstream operator will + # be found. + op_output.emit(self.index, "out", acq_timestamp=clock.timestamp()) + self.index += 1 + + +class PingRxOp(Operator): + """Simple receiver operator. + + This is an example of a native operator with one input port. + On each tick, it receives up to 5 batches of messages from the "in" port. + If 5 messages are not received within 1 second, the operator will be triggered to process. + + **==Named Inputs==** + + in : any + A received value. + """ + + def __init__(self, fragment, *args, **kwargs): + # Need to call the base class constructor last + super().__init__(fragment, *args, **kwargs) + + def setup(self, spec: OperatorSpec): + spec.input("in").connector( + IOSpec.ConnectorType.DOUBLE_BUFFER, + capacity=5, + policy=1, + ).condition( + # Set the enum value corresponding to ExpiringMessageAvailableCondition + ConditionType.EXPIRING_MESSAGE_AVAILABLE, + max_batch_size=5, + max_delay_ns=1_000_000_000, + ) + + def compute(self, op_input, op_output, context): + message = op_input.receive("in") + print("PingRxOp.compute() called") + while message: + print(f"ExpiringMessageAvailable ping: {message}") + message = op_input.receive("in") + + +# Now define a simple application using the operators defined above + + +class MyPingApp(Application): + def compose(self): + # Configure the operators. Here we use CountCondition to terminate + # execution after a specific number of messages have been sent. + # PeriodicCondition is used so that each subsequent message is + # sent only after a period of 10 milliseconds has elapsed. + tx = PingTxOp(self, CountCondition(self, 8), PeriodicCondition(self, 10_000_000), name="tx") + rx = PingRxOp(self, name="rx") + + # Connect the operators into the workflow: tx -> rx + self.add_flow(tx, rx) + + +def main(): + app = MyPingApp() + app.scheduler(GreedyScheduler(fragment=app, name="greedy")) + app.run() + + +if __name__ == "__main__": + main() diff --git a/examples/holoviz/cpp/holoviz_geometry.cpp b/examples/holoviz/cpp/holoviz_geometry.cpp index 1eb86c2e..53fa7df9 100644 --- a/examples/holoviz/cpp/holoviz_geometry.cpp +++ b/examples/holoviz/cpp/holoviz_geometry.cpp @@ -307,10 +307,8 @@ class HolovizGeometryApp : public holoscan::Application { label_coords_spec.text_ = {"label_1", "label_2"}; label_coords_spec.priority_ = priority++; - auto visualizer = make_operator("holoviz", - Arg("width", 854u), - Arg("height", 480u), - Arg("tensors", input_spec)); + auto visualizer = make_operator( + "holoviz", Arg("width", 854u), Arg("height", 480u), Arg("tensors", input_spec)); // Define the workflow: source -> holoviz add_flow(source, visualizer, {{"outputs", "receivers"}}); @@ -326,7 +324,7 @@ int main(int argc, char** argv) { // Parse args struct option long_options[] = { {"help", no_argument, 0, 'h'}, {"count", required_argument, 0, 'c'}, {0, 0, 0, 0}}; - uint64_t count; + uint64_t count = 0; while (true) { int option_index = 0; const int c = getopt_long(argc, argv, "hc:", long_options, &option_index); diff --git a/examples/import_gxf_components/cpp/import_gxf_components.cpp b/examples/import_gxf_components/cpp/import_gxf_components.cpp index f7e58116..0e6f6b4e 100644 --- a/examples/import_gxf_components/cpp/import_gxf_components.cpp +++ b/examples/import_gxf_components/cpp/import_gxf_components.cpp @@ -31,7 +31,8 @@ #include "./receive_tensor_gxf.hpp" #include "./send_tensor_gxf.hpp" -#include "holoscan/core/resources/gxf/gxf_component_resource.hpp" +// Include the following header files to use GXFCodeletOp and HOLOSCAN_WRAP_GXF_CODELET_AS_OPERATOR +// macro. #include "holoscan/operators/gxf_codelet/gxf_codelet.hpp" #ifdef CUDA_TRY diff --git a/examples/multi_branch_pipeline/README.md b/examples/multi_branch_pipeline/README.md index ef0557b4..32fec735 100644 --- a/examples/multi_branch_pipeline/README.md +++ b/examples/multi_branch_pipeline/README.md @@ -49,7 +49,7 @@ For the C++ application, the scheduler to be used can be set via the `scheduler` ## Python API -- `multi_branch_pipeline.py`: This example is the same as described for the C++ application above. The primary difference is that instead of using a YAML file for the configuration variables, all values are set via the command line. Call the script below with the `--help` option to get a full description of the command line parameters. By default a polling-based multithread scheduler will be used, but if `--event-based` is specified, the event-based multithread scheduler will be used instead. +- `multi_branch_pipeline.py`: This example is the same as described for the C++ application above. The primary difference is that instead of using a YAML file for the configuration variables, all values are set via the command line. Call the script below with the `--help` option to get a full description of the command line parameters. By default a polling-based multithread scheduler will be used, but if `--event_based` is specified, the event-based multithread scheduler will be used instead. ### Build instructions diff --git a/examples/multithread/README.md b/examples/multithread/README.md index 364a3d98..437f6d98 100644 --- a/examples/multithread/README.md +++ b/examples/multithread/README.md @@ -36,7 +36,7 @@ For the C++ application, the scheduler to be used can be set via the `scheduler` ## Python API -- `multithread.py`: This example demonstrates how to configure and use a multi-threaded scheduler instead of the default single-threaded one. It involves three operators as described for the C++ API example described above. The primary difference is that instead of using a YAML file for the configuration variables, all values are set via the command line. Call the script below with the `--help` option to get a full description of the command line parameters. By default a polling-based multithread scheduler will be used, but if `--event-based` is specified, the event-based multithread scheduler will be used instead. +- `multithread.py`: This example demonstrates how to configure and use a multi-threaded scheduler instead of the default single-threaded one. It involves three operators as described for the C++ API example described above. The primary difference is that instead of using a YAML file for the configuration variables, all values are set via the command line. Call the script below with the `--help` option to get a full description of the command line parameters. By default a polling-based multithread scheduler will be used, but if `--event_based` is specified, the event-based multithread scheduler will be used instead. ### Build instructions @@ -49,5 +49,5 @@ First, go in your `build` or `install` directory (automatically done by `./run l Then, run the app with the options of your choice. For example, to use 8 worker threads to run 32 delay operators with delays ranging linearly from 0.2 to (0.2 + 0.05 * 31), one would set: ```bash -python3 ./examples/multithread/python/multithread.py --threads 8 --num_delay_ops 32 --delay 0.2 --delay_step 0.05 --event-based +python3 ./examples/multithread/python/multithread.py --threads 8 --num_delay_ops 32 --delay 0.2 --delay_step 0.05 --event_based ``` diff --git a/examples/multithread/cpp/multithread.cpp b/examples/multithread/cpp/multithread.cpp index 802f89fa..30de54a4 100644 --- a/examples/multithread/cpp/multithread.cpp +++ b/examples/multithread/cpp/multithread.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include "holoscan/holoscan.hpp" @@ -75,7 +76,7 @@ class DelayOp : public Operator { } if (!silent) { HOLOSCAN_LOG_INFO("{}: sending new value ({})", name(), new_value); } op_output.emit(new_value, "out_val"); - op_output.emit(nm, "out_name"); + op_output.emit(std::move(nm), "out_name"); }; private: @@ -134,7 +135,7 @@ class App : public holoscan::Application { auto rx = make_operator("rx", silent_); for (int i = 0; i < num_delays_; ++i) { std::string delay_name = fmt::format("mx{}", i); - auto del_op = make_operator(delay_name, + auto del_op = make_operator(std::move(delay_name), Arg{"delay", delay_ + delay_step_ * i}, Arg{"increment", i}, Arg{"silent", silent_}); diff --git a/examples/ping_distributed/cpp/ping_distributed.cpp b/examples/ping_distributed/cpp/ping_distributed.cpp index dc87b5ab..0e4e7ab7 100644 --- a/examples/ping_distributed/cpp/ping_distributed.cpp +++ b/examples/ping_distributed/cpp/ping_distributed.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -115,7 +115,7 @@ std::optional get_boolean_arg(std::vector args, const std::st std::optional get_int32_arg(std::vector args, const std::string& name) { auto loc = std::find(args.begin(), args.end(), name); - if ((loc != std::end(args)) && (loc++ != std::end(args))) { + if ((loc != std::end(args)) && (++loc != std::end(args))) { try { return std::stoi(*loc); } catch (std::exception& e) { @@ -125,10 +125,9 @@ std::optional get_int32_arg(std::vector args, const std::s } return {}; } - std::optional get_int64_arg(std::vector args, const std::string& name) { auto loc = std::find(args.begin(), args.end(), name); - if ((loc != std::end(args)) && (loc++ != std::end(args))) { + if ((loc != std::end(args)) && (++loc != std::end(args))) { try { return std::stoll(*loc); } catch (std::exception& e) { @@ -141,7 +140,7 @@ std::optional get_int64_arg(std::vector args, const std::s std::optional get_str_arg(std::vector args, const std::string& name) { auto loc = std::find(args.begin(), args.end(), name); - if ((loc != std::end(args)) && (loc++ != std::end(args))) { return *loc; } + if ((loc != std::end(args)) && (++loc != std::end(args))) { return *loc; } return {}; } @@ -165,7 +164,7 @@ int main() { auto app = holoscan::make_application(); // Parse args that are defined for all applications. - auto& remaining_args = app->argv(); + std::vector& remaining_args = app->argv(); // Parse any additional supported arguments bool tensor_on_gpu = get_boolean_arg(remaining_args, "--gpu").value_or(false); diff --git a/examples/python_decorator/CMakeLists.txt b/examples/python_decorator/CMakeLists.txt new file mode 100644 index 00000000..e663d25a --- /dev/null +++ b/examples/python_decorator/CMakeLists.txt @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Get relative folder path for the app +file(RELATIVE_PATH app_relative_dest_path ${CMAKE_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) + +# Copy video_replayer application file +add_custom_target(python_decorator_example ALL + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/video_replayer.py" ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "video_replayer.py" + BYPRODUCTS "video_replayer.py" +) + +# Install the app +install(FILES + "${CMAKE_CURRENT_SOURCE_DIR}/video_replayer.py" + DESTINATION "${app_relative_dest_path}" + COMPONENT "holoscan-examples" +) + +# Testing +if(HOLOSCAN_BUILD_TESTS) + add_test(NAME EXAMPLE_PYTHON_DECORATOR_INTEROP_TEST + COMMAND python3 video_replayer.py + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + set_tests_properties(EXAMPLE_PYTHON_DECORATOR_INTEROP_TEST PROPERTIES + DEPENDS "video_replayer.py" + PASS_REGULAR_EXPRESSION "Reach end of file or playback count reaches to the limit. Stop ticking." + ) +endif() diff --git a/examples/python_decorator/README.md b/examples/python_decorator/README.md new file mode 100644 index 00000000..d015d01f --- /dev/null +++ b/examples/python_decorator/README.md @@ -0,0 +1,41 @@ +# Using a Function Decorator to Build Python Operators + +This is an example of mixed use of native Python operators and wrapped C++ operators. In this example, instead of explicitly creating a Python operator from inheriting from `holoscan.core.Operator`, we instead demonstrate how `holoscan.core.decorator` can be used to decorator an existing function, turning it into an Operator. + +## Data + +The following dataset is used by this example: +[📦️ (NGC) Sample RacerX Video Data](https://catalog.ngc.nvidia.com/orgs/nvidia/teams/clara-holoscan/resources/holoscan_racerx_video/files?version=20231009). + +## Python Run instructions + +* **using python wheel**: + ```bash + # [Prerequisite] Download NGC dataset above to `DATA_DIR` + export HOLOSCAN_INPUT_PATH= + # [Prerequisite] Download example .py file below to `APP_DIR` + # [Optional] Start the virtualenv where holoscan is installed + python3 /video_replayer.py + ``` +* **using deb package install**: + ```bash + sudo /opt/nvidia/holoscan/examples/download_example_data + export HOLOSCAN_INPUT_PATH=/opt/nvidia/holoscan/data + export PYTHONPATH=/opt/nvidia/holoscan/python/lib + python3 /opt/nvidia/holoscan/examples/python_decorator/video_replayer.py + ``` +* **from NGC container**: + ```bash + python3 /opt/nvidia/holoscan/examples/python_decorator/video_replayer.py + ``` +* **source (dev container)**: + ```bash + ./run launch # optional: append `install` for install tree + python3 ./examples/python_decorator/video_replayer.py + ``` +* **source (local env)**: + ```bash + export PYTHONPATH=${BUILD_OR_INSTALL_DIR}/python/lib + export HOLOSCAN_INPUT_PATH=${SRC_DIR}/data + python3 ${BUILD_OR_INSTALL_DIR}/examples/python_decorator/video_replayer.py + ``` diff --git a/examples/python_decorator/video_replayer.py b/examples/python_decorator/video_replayer.py new file mode 100644 index 00000000..0ef77c20 --- /dev/null +++ b/examples/python_decorator/video_replayer.py @@ -0,0 +1,92 @@ +""" + SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + SPDX-License-Identifier: Apache-2.0 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" # noqa: E501 + +import os + +from holoscan.core import Application +from holoscan.decorator import Input, Output, create_op +from holoscan.operators import HolovizOp, VideoStreamReplayerOp + +sample_data_path = os.environ.get("HOLOSCAN_INPUT_PATH", "../data") + + +@create_op( + inputs="tensor", + outputs="out_tensor", +) +def invert(tensor): + tensor = 255 - tensor + return tensor + + +@create_op(inputs=Input("in", arg_map="tensor"), outputs=Output("out", tensor_names=("frame",))) +def tensor_info(tensor): + print(f"tensor from 'in' port: shape = {tensor.shape}, " f"dtype = {tensor.dtype.name}") + return tensor + + +class VideoReplayerApp(Application): + """Example of an application that uses the operators defined above. + + This application has the following operators: + + - VideoStreamReplayerOp + - HolovizOp + + The VideoStreamReplayerOp reads a video file and sends the frames to the HolovizOp. + The HolovizOp displays the frames. + """ + + def compose(self): + video_dir = os.path.join(sample_data_path, "racerx") + if not os.path.exists(video_dir): + raise ValueError(f"Could not find video data: {video_dir=}") + + # Define the replayer and holoviz operators + replayer = VideoStreamReplayerOp( + self, + name="replayer", + directory=video_dir, + basename="racerx", + frame_rate=0, # as specified in timestamps + repeat=False, # default: false + realtime=True, # default: true + count=40, # default: 0 (no frame count restriction) + ) + invert_op = invert(self, name="image_invert") + info_op = tensor_info(self, name="tensor_info") + visualizer = HolovizOp( + self, + name="holoviz", + width=854, + height=480, + # name="frame" to match Output argument to create_op for tensor_info + tensors=[dict(name="frame", type="color", opacity=1.0, priority=0)], + ) + # Define the workflow + self.add_flow(replayer, invert_op, {("output", "tensor")}) + self.add_flow(invert_op, info_op, {("out_tensor", "in")}) + self.add_flow(info_op, visualizer, {("out", "receivers")}) + + +def main(): + app = VideoReplayerApp() + app.run() + + +if __name__ == "__main__": + main() diff --git a/examples/resources/CMakeLists.txt b/examples/resources/CMakeLists.txt index 07c05014..01485913 100644 --- a/examples/resources/CMakeLists.txt +++ b/examples/resources/CMakeLists.txt @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,3 +14,4 @@ # limitations under the License. add_subdirectory(clock) +add_subdirectory(native) diff --git a/examples/resources/native/CMakeLists.txt b/examples/resources/native/CMakeLists.txt new file mode 100644 index 00000000..91b8d28e --- /dev/null +++ b/examples/resources/native/CMakeLists.txt @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +add_subdirectory(cpp) +add_subdirectory(python) + +file(RELATIVE_PATH app_relative_dest_path ${CMAKE_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) + +install( + FILES README.md + DESTINATION "${app_relative_dest_path}" + COMPONENT "holoscan-examples" +) diff --git a/examples/resources/native/README.md b/examples/resources/native/README.md new file mode 100644 index 00000000..24cbff8f --- /dev/null +++ b/examples/resources/native/README.md @@ -0,0 +1,47 @@ +# Native Resource Example +`` +This example demonstrates how a C++ `holoscan::Resource` (or Python `holoscan.core.Resource`) can be created without wrapping an underlying GXF Component. + +## C++ Run instructions + +* **using deb package install or NGC container**: + ```bash + /opt/nvidia/holoscan/examples/resources/native/cpp/native_resource + ``` +* **source (dev container)**: + ```bash + ./run launch # optional: append `install` for install tree + ./examples/resources/native/cpp/native_resource + ``` +* **source (local env)**: + ```bash + ${BUILD_OR_INSTALL_DIR}/examples/resources/native/cpp/native_resource + ``` + +## Python Run instructions + +* **using python wheel**: + ```bash + # [Prerequisite] Download example .py file below to `APP_DIR` + # [Optional] Start the virtualenv where holoscan is installed + python3 /native_resource.py + ``` +* **using deb package install**: + ```bash + export PYTHONPATH=/opt/nvidia/holoscan/python/lib + python3 /opt/nvidia/holoscan/examples/resources/native/python/native_resource.py + ``` +* **from NGC container**: + ```bash + python3 /opt/nvidia/holoscan/examples/resources/native/python/native_resource.py + ``` +* **source (dev container)**: + ```bash + ./run launch # optional: append `install` for install tree + python3 ./examples/resources/native/python/native_resource.py + ``` +* **source (local env)**: + ```bash + export PYTHONPATH=${BUILD_OR_INSTALL_DIR}/python/lib + python3 ${BUILD_OR_INSTALL_DIR}/examples/resources/native/python/native_resource.py + ``` diff --git a/examples/resources/native/cpp/CMakeLists.min.txt b/examples/resources/native/cpp/CMakeLists.min.txt new file mode 100644 index 00000000..41867a23 --- /dev/null +++ b/examples/resources/native/cpp/CMakeLists.min.txt @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the \"License\"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an \"AS IS\" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.20) +project(holoscan_native_resource CXX) + +# Finds the package holoscan +find_package(holoscan REQUIRED CONFIG + PATHS "/opt/nvidia/holoscan" "/workspace/holoscan-sdk/install") + +add_executable(native_resource + native_resource.cpp +) + +target_link_libraries(native_resource + PRIVATE + holoscan::core +) + +# Testing +if(BUILD_TESTING) + add_test(NAME EXAMPLE_RESOURCES_CPP_NATIVE_RESOURCE_TEST + COMMAND native_resource + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + set_tests_properties( + EXAMPLE_RESOURCES_CPP_NATIVE_RESOURCE_TEST PROPERTIES + PASS_REGULAR_EXPRESSION "string_native_resource.string_param: test_string" + PASS_REGULAR_EXPRESSION "hardcoded_native_resource.string_param: hardcoded_string" + PASS_REGULAR_EXPRESSION "empty_native_resource.string_param: ''" + FAIL_REGULAR_EXPRESSION "error" + FAIL_REGULAR_EXPRESSION "Exception occurred" + ) +endif() diff --git a/examples/resources/native/cpp/CMakeLists.txt b/examples/resources/native/cpp/CMakeLists.txt new file mode 100644 index 00000000..8ef384d7 --- /dev/null +++ b/examples/resources/native/cpp/CMakeLists.txt @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Create examples +add_executable(native_resource + native_resource.cpp +) +target_link_libraries(native_resource + PUBLIC + holoscan::core +) + +# Install examples + +# Set the install RPATH based on the location of the Holoscan SDK libraries +# The GXF extensions are loaded by the GXF libraries - no need to include here +file(RELATIVE_PATH install_lib_relative_path ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/${HOLOSCAN_INSTALL_LIB_DIR}) +set_target_properties(native_resource PROPERTIES INSTALL_RPATH "\$ORIGIN/${install_lib_relative_path}") + +# Install following the relative folder path +file(RELATIVE_PATH app_relative_dest_path ${CMAKE_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) + +if(HOLOSCAN_INSTALL_EXAMPLE_SOURCE) +# Install the source +install(FILES native_resource.cpp + DESTINATION "${app_relative_dest_path}" + COMPONENT holoscan-examples +) + +# Install the minimal CMakeLists.txt file +install(FILES CMakeLists.min.txt + RENAME "CMakeLists.txt" + DESTINATION "${app_relative_dest_path}" + COMPONENT holoscan-examples +) +endif() + +# Install the compiled example +install(TARGETS native_resource + DESTINATION "${app_relative_dest_path}" + COMPONENT holoscan-examples +) + +# Testing +if(HOLOSCAN_BUILD_TESTS) + add_test(NAME EXAMPLE_RESOURCES_CPP_NATIVE_RESOURCE_TEST + COMMAND native_resource + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + set_tests_properties( + EXAMPLE_RESOURCES_CPP_NATIVE_RESOURCE_TEST PROPERTIES + PASS_REGULAR_EXPRESSION "string_native_resource.string_param: test_string" + PASS_REGULAR_EXPRESSION "hardcoded_native_resource.string_param: hardcoded_string" + PASS_REGULAR_EXPRESSION "empty_native_resource.string_param: ''" + FAIL_REGULAR_EXPRESSION "error" + FAIL_REGULAR_EXPRESSION "Exception occurred" + ) +endif() diff --git a/tests/system/native_resource_minimal_app.cpp b/examples/resources/native/cpp/native_resource.cpp similarity index 74% rename from tests/system/native_resource_minimal_app.cpp rename to examples/resources/native/cpp/native_resource.cpp index 30e42682..720a4dcf 100644 --- a/tests/system/native_resource_minimal_app.cpp +++ b/examples/resources/native/cpp/native_resource.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,16 +15,11 @@ * limitations under the License. */ -#include - +#include #include #include -#include "../config.hpp" - -static HoloscanTestConfig test_config; - namespace holoscan { class MinimalNativeResource : public holoscan::Resource { @@ -88,26 +83,11 @@ class MinimalNativeResourceApp : public holoscan::Application { } }; -TEST(MinimalNativeResourceApp, TestMinimalNativeResourceApp) { - auto app = make_application(); - - const std::string config_file = test_config.get_test_data_file("minimal.yaml"); - app->config(config_file); - - // capture output so that we can check that the expected value is present - testing::internal::CaptureStderr(); +} // namespace holoscan +int main(int argc, char** argv) { + auto app = holoscan::make_application(); app->run(); - std::string log_output = testing::internal::GetCapturedStderr(); - EXPECT_TRUE(log_output.find("string_native_resource.string_param: test_string") != - std::string::npos) - << log_output; - EXPECT_TRUE(log_output.find("hardcoded_native_resource.string_param: hardcoded_string") != - std::string::npos) - << log_output; - EXPECT_TRUE(log_output.find("empty_native_resource.string_param: ''") != std::string::npos) - << log_output; + return 0; } - -} // namespace holoscan diff --git a/examples/resources/native/python/CMakeLists.txt b/examples/resources/native/python/CMakeLists.txt new file mode 100644 index 00000000..210c2956 --- /dev/null +++ b/examples/resources/native/python/CMakeLists.txt @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Get relative folder path for the app +file(RELATIVE_PATH app_relative_dest_path ${CMAKE_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) + +# Copy native operator ping application +add_custom_target(python_native_resource ALL + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/native_resource.py" ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS "native_resource.py" + BYPRODUCTS "native_resource.py" +) + +# Install the app +install(FILES + "${CMAKE_CURRENT_SOURCE_DIR}/native_resource.py" + DESTINATION "${app_relative_dest_path}" + COMPONENT "holoscan-examples" +) + +# Testing +if(HOLOSCAN_BUILD_TESTS) + add_test(NAME EXAMPLE_RESOURCES_PYTHON_NATIVE_RESOURCE_TEST + COMMAND python3 native_resource.py + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + set_tests_properties(EXAMPLE_RESOURCES_PYTHON_NATIVE_RESOURCE_TEST PROPERTIES + PASS_REGULAR_EXPRESSION "native resource setup method called" + PASS_REGULAR_EXPRESSION "MinimalOp compute method called" + FAIL_REGULAR_EXPRESSION "error" + FAIL_REGULAR_EXPRESSION "Exception occurred" + ) +endif() diff --git a/examples/resources/native/python/native_resource.py b/examples/resources/native/python/native_resource.py new file mode 100644 index 00000000..adae6a80 --- /dev/null +++ b/examples/resources/native/python/native_resource.py @@ -0,0 +1,80 @@ +""" + SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + SPDX-License-Identifier: Apache-2.0 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" # noqa: E501 + +from holoscan.conditions import CountCondition +from holoscan.core import Application, ComponentSpec, Operator, Resource + + +class NativeResource(Resource): + def __init__(self, fragment, msg="test", *args, **kwargs): + self.msg = msg + super().__init__(fragment, *args, **kwargs) + + def setup(self, spec: ComponentSpec): + print("** native resource setup method called **") + + def get_message(self): + return self.msg + + +class MinimalOp(Operator): + def __init__(self, *args, expected_message="test", **kwargs): + self.expected_message = expected_message + # Need to call the base class constructor last + super().__init__(*args, **kwargs) + + def compute(self, op_input, op_output, context): + print("** MinimalOp compute method called **") + resource = self.resource("msg_resource") + assert isinstance(resource, NativeResource) + + # can call a custom method implemented for the resource + msg = resource.get_message() + assert msg == self.expected_message + + # test case when no resource with the specified name exists + nonexistent_resource = self.resource("nonexistent") + assert nonexistent_resource is None + + # test retrieving all resources + resources = self.resources + assert isinstance(resources, dict) + assert len(resources) == 1 + assert isinstance(resources["msg_resource"], NativeResource) + + +class MinimalNativeResourceApp(Application): + def compose(self): + msg = "native resource message" + native_resource = NativeResource(self, msg=msg, name="msg_resource") + mx = MinimalOp( + self, + CountCondition(self, 1), + native_resource, + expected_message=msg, + name="mx", + ) + self.add_operator(mx) + + +def main(): + app = MinimalNativeResourceApp() + app.run() + + +if __name__ == "__main__": + main() diff --git a/examples/v4l2_camera/cpp/v4l2_camera.cpp b/examples/v4l2_camera/cpp/v4l2_camera.cpp index e2773d6d..c4f396b2 100644 --- a/examples/v4l2_camera/cpp/v4l2_camera.cpp +++ b/examples/v4l2_camera/cpp/v4l2_camera.cpp @@ -43,9 +43,9 @@ class App : public holoscan::Application { if (key_exists(from_config("source"), "width") && key_exists(from_config("source"), "height")) { // width and height given, use BlockMemoryPool (better latency) - const int width = from_config("source.width").as(); - const int height = from_config("source.height").as(); - const int n_channels = 4; + const uint64_t width = from_config("source.width").as(); + const uint64_t height = from_config("source.height").as(); + const uint8_t n_channels = 4; uint64_t block_size = width * height * n_channels; auto allocator = make_resource("pool", 0, block_size, 1); diff --git a/include/holoscan/core/application.hpp b/include/holoscan/core/application.hpp index 4be1ace5..f0a83d7c 100644 --- a/include/holoscan/core/application.hpp +++ b/include/holoscan/core/application.hpp @@ -28,12 +28,14 @@ #include "./fragment.hpp" -#include "./app_driver.hpp" #include "./app_worker.hpp" #include "./cli_parser.hpp" namespace holoscan { +// forward declaration +class AppDriver; + /** * @brief Utility function to create an application. * @@ -343,7 +345,7 @@ class Application : public Fragment { CLIParser cli_parser_; ///< The command line parser. std::vector argv_; ///< The command line arguments after processing flags. - std::unique_ptr fragment_graph_; ///< The fragment connection graph. + std::shared_ptr fragment_graph_; ///< The fragment connection graph. std::shared_ptr app_driver_; ///< The application driver. std::shared_ptr app_worker_; ///< The application worker. diff --git a/include/holoscan/core/codec_registry.hpp b/include/holoscan/core/codec_registry.hpp index 905a5108..7cfdb209 100644 --- a/include/holoscan/core/codec_registry.hpp +++ b/include/holoscan/core/codec_registry.hpp @@ -257,7 +257,7 @@ class CodecRegistry { * @param overwrite if true, any existing codec with matching codec_name will be overwritten. */ template - void add_codec(std::pair codec, const std::string& codec_name, + void add_codec(std::pair& codec, const std::string& codec_name, bool overwrite = true) { auto index = std::type_index(typeid(typeT)); add_codec(index, codec, codec_name, overwrite); @@ -271,7 +271,7 @@ class CodecRegistry { * @param codec_name the name of the codec to add. * @param overwrite if true, any existing codec with matching codec_name will be overwritten. */ - void add_codec(const std::type_index& index, std::pair codec, + void add_codec(const std::type_index& index, std::pair& codec, const std::string& codec_name, bool overwrite = true) { auto name_search = name_to_index_map_.find(codec_name); if (name_search != name_to_index_map_.end()) { diff --git a/include/holoscan/core/component_spec-inl.hpp b/include/holoscan/core/component_spec-inl.hpp index 718501d9..bcd9bcf5 100644 --- a/include/holoscan/core/component_spec-inl.hpp +++ b/include/holoscan/core/component_spec-inl.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,8 +21,6 @@ #include #include -#include "./component_spec.hpp" - #include "./argument_setter.hpp" #include "./executors/gxf/gxf_parameter_adaptor.hpp" diff --git a/include/holoscan/core/condition.hpp b/include/holoscan/core/condition.hpp index a9567843..accc6ba2 100644 --- a/include/holoscan/core/condition.hpp +++ b/include/holoscan/core/condition.hpp @@ -98,6 +98,7 @@ namespace holoscan { // Forward declarations class Operator; +class Resource; // Note: Update `IOSpec::to_yaml_node()` if you add new condition types enum class ConditionType { @@ -109,6 +110,7 @@ enum class ConditionType { kBoolean, ///< nvidia::gxf::BooleanSchedulingTerm kPeriodic, ///< nvidia::gxf::PeriodicSchedulingTerm kAsynchronous, ///< nvidia::gxf::AsynchronousSchedulingTerm + kExpiringMessageAvailable, ///< nvidia::gxf::ExpiringMessageAvailableSchedulingTerm }; /** @@ -196,6 +198,27 @@ class Condition : public Component { using Component::add_arg; + /** + * @brief Add a resource to the condition. + * + * @param arg The resource to add. + */ + void add_arg(const std::shared_ptr& arg); + + /** + * @brief Add a resource to the condition. + * + * @param arg The resource to add. + */ + void add_arg(std::shared_ptr&& arg); + + /** + * @brief Get the resources of the condition. + * + * @return The resources of the condition. + */ + std::unordered_map>& resources() { return resources_; } + /** * @brief Define the condition specification. * @@ -217,6 +240,9 @@ class Condition : public Component { using Component::reset_graph_entities; bool is_initialized_ = false; ///< Whether the condition is initialized. + + std::unordered_map> + resources_; ///< The resources used by the condition. }; } // namespace holoscan diff --git a/include/holoscan/core/conditions/gxf/expiring_message.hpp b/include/holoscan/core/conditions/gxf/expiring_message.hpp new file mode 100644 index 00000000..467bb212 --- /dev/null +++ b/include/holoscan/core/conditions/gxf/expiring_message.hpp @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef HOLOSCAN_CORE_CONDITIONS_GXF_EXPIRING_MESSAGE_HPP +#define HOLOSCAN_CORE_CONDITIONS_GXF_EXPIRING_MESSAGE_HPP + +#include + +#include "../../gxf/gxf_condition.hpp" +#include "../../resources/gxf/clock.hpp" +#include "../../resources/gxf/realtime_clock.hpp" + +namespace holoscan { + +class ExpiringMessageAvailableCondition : public gxf::GXFCondition { + public: + HOLOSCAN_CONDITION_FORWARD_ARGS_SUPER(ExpiringMessageAvailableCondition, GXFCondition) + + ExpiringMessageAvailableCondition() = default; + + explicit ExpiringMessageAvailableCondition(int64_t max_batch_size) + : max_batch_size_(max_batch_size) {} + + ExpiringMessageAvailableCondition(int64_t max_batch_size, int64_t max_delay_ns) + : max_batch_size_(max_batch_size), max_delay_ns_(max_delay_ns) {} + + template + explicit ExpiringMessageAvailableCondition(int64_t max_batch_size, + std::chrono::duration max_delay) + : max_batch_size_(max_batch_size) { + max_delay_ns_ = std::chrono::duration_cast(max_delay).count(); + } + + const char* gxf_typename() const override { + return "nvidia::gxf::ExpiringMessageAvailableSchedulingTerm"; + } + + void receiver(std::shared_ptr receiver) { receiver_ = receiver; } + std::shared_ptr receiver() { return receiver_.get(); } + + void setup(ComponentSpec& spec) override; + + void initialize() override; + + void max_batch_size(int64_t max_batch_size); + int64_t max_batch_size() { return max_batch_size_; } + + /** + * @brief Set max delay. + * + * Note that calling this method doesn't affect the behavior of the condition once the condition + * is initialized. + * + * @param max_delay_ns The integer representing max delay in nanoseconds. + */ + void max_delay(int64_t max_delay_ns); + + /** + * @brief Set max delay. + * + * Note that calling this method doesn't affect the behavior of the condition once the + * condition is initialized. + * + * @param max_delay_duration The max delay of type `std::chrono::duration`. + */ + template + void max_delay(std::chrono::duration max_delay_duration) { + int64_t max_delay_ns = + std::chrono::duration_cast(max_delay_duration).count(); + max_delay(max_delay_ns); + } + + /** + * @brief Get max delay in nano seconds. + * + * @return The minimum time which needs to elapse between two executions (in nano seconds) + */ + int64_t max_delay_ns(); + + nvidia::gxf::ExpiringMessageAvailableSchedulingTerm* get() const; + + // TODO(GXF4): Expected setReceiver(Handle value) + + private: + // TODO(GXF4): this is now a std::set> receivers_ + Parameter> receiver_; + Parameter max_batch_size_; + Parameter max_delay_ns_; + Parameter> clock_; +}; + +} // namespace holoscan + +#endif /* HOLOSCAN_CORE_CONDITIONS_GXF_EXPIRING_MESSAGE_HPP */ diff --git a/include/holoscan/core/conditions/gxf/message_available.hpp b/include/holoscan/core/conditions/gxf/message_available.hpp index 1ea924fb..2126eb0b 100644 --- a/include/holoscan/core/conditions/gxf/message_available.hpp +++ b/include/holoscan/core/conditions/gxf/message_available.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/include/holoscan/core/config.hpp b/include/holoscan/core/config.hpp index 08b9957d..8c355ff9 100644 --- a/include/holoscan/core/config.hpp +++ b/include/holoscan/core/config.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -48,9 +48,12 @@ class Config { HOLOSCAN_LOG_WARN("Config file '{}' doesn't exist", config_file); } } - virtual ~Config() = default; + // Delete the copy constructor and assignment operator to prevent copying. + Config(const Config&) = delete; + Config& operator=(const Config&) = delete; + /** * @brief Get the path to the configuration file. * diff --git a/include/holoscan/core/domain/tensor.hpp b/include/holoscan/core/domain/tensor.hpp index 7abe0d9f..706c6e95 100644 --- a/include/holoscan/core/domain/tensor.hpp +++ b/include/holoscan/core/domain/tensor.hpp @@ -43,7 +43,7 @@ using DLManagedMemoryBuffer = nvidia::gxf::DLManagedMemoryBuffer; * * The Tensor class is a wrapper around the DLManagedTensorContext struct that holds the * DLManagedTensor object. - * (https://dmlc.github.io/dlpack/latest/c_api.html#_CPPv415DLManagedTensor). + * (https://dmlc.github.io/dlpack/latest/c_api.html#c.DLManagedTensor). * * This class provides a primary interface to access Tensor data and is interoperable with other * frameworks that support DLManagedTensor. diff --git a/include/holoscan/core/endpoint.hpp b/include/holoscan/core/endpoint.hpp index ec3585b3..dcadc01c 100644 --- a/include/holoscan/core/endpoint.hpp +++ b/include/holoscan/core/endpoint.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -119,7 +119,7 @@ class Endpoint : public Resource { } private: - nvidia::gxf::Endpoint* gxf_endpoint_; + nvidia::gxf::Endpoint* gxf_endpoint_ = nullptr; }; } // namespace holoscan diff --git a/include/holoscan/core/execution_context.hpp b/include/holoscan/core/execution_context.hpp index 11221ec3..9f362261 100644 --- a/include/holoscan/core/execution_context.hpp +++ b/include/holoscan/core/execution_context.hpp @@ -35,6 +35,8 @@ class ExecutionContext { */ ExecutionContext() = default; + virtual ~ExecutionContext() = default; + /** * @brief Get the input context. * diff --git a/include/holoscan/core/executor.hpp b/include/holoscan/core/executor.hpp index 9fe75006..8be0c761 100644 --- a/include/holoscan/core/executor.hpp +++ b/include/holoscan/core/executor.hpp @@ -51,6 +51,10 @@ class Executor { explicit Executor(Fragment* fragment) : fragment_(fragment) {} virtual ~Executor() = default; + // Delete the copy constructor and assignment operator to prevent copying. + Executor(const Executor&) = delete; + Executor& operator=(const Executor&) = delete; + /** * @brief Run the graph. * diff --git a/include/holoscan/core/executors/gxf/gxf_executor.hpp b/include/holoscan/core/executors/gxf/gxf_executor.hpp index 803c4c7b..73153d75 100644 --- a/include/holoscan/core/executors/gxf/gxf_executor.hpp +++ b/include/holoscan/core/executors/gxf/gxf_executor.hpp @@ -89,7 +89,7 @@ class GXFExecutor : public holoscan::Executor { /** * @brief Set the context. * - * For GXF, GXFExtensionManager(gxf_extension_manager_) is initialized with the context. + * For GXF, GXFExtensionManager(extension_manager_) is initialized with the context. * * @param context The context. */ @@ -220,11 +220,8 @@ class GXFExecutor : public holoscan::Executor { ///< initializing a new operator if this is 0. gxf_uid_t op_cid_ = 0; ///< The GXF component ID of the operator. Create new component for ///< initializing a new operator if this is 0. - std::shared_ptr gxf_extension_manager_; ///< The GXF extension manager. nvidia::gxf::Extension* gxf_holoscan_extension_ = nullptr; ///< The GXF holoscan extension. - /// The flag to indicate whether the extensions are loaded. - bool is_extensions_loaded_ = false; /// The flag to indicate whether the GXF graph is initialized. bool is_gxf_graph_initialized_ = false; /// The flag to indicate whether the GXF graph is activated. diff --git a/include/holoscan/core/fragment.hpp b/include/holoscan/core/fragment.hpp index abd11ae7..a2e465ec 100644 --- a/include/holoscan/core/fragment.hpp +++ b/include/holoscan/core/fragment.hpp @@ -188,6 +188,13 @@ class Fragment { */ Config& config(); + /** + * @brief Get the shared pointer to the configuration of the fragment. + * + * @return The shared pointer to the configuration of the fragment. + */ + std::shared_ptr config_shared(); + /** * @brief Get the graph of the fragment. * @@ -195,6 +202,13 @@ class Fragment { */ OperatorGraph& graph(); + /** + * @brief Get the shared pointer to the graph of the fragment. + * + * @return The shared pointer to the graph of the fragment. + */ + std::shared_ptr graph_shared(); + /** * @brief Get the executor of the fragment. * @@ -202,6 +216,13 @@ class Fragment { */ Executor& executor(); + /** + * @brief Get the shared pointer to the executor of the fragment. + * + * @return The shared pointer to the executor of the fragment. + */ + std::shared_ptr executor_shared(); + /** * @brief Get the scheduler used by the executor * @@ -656,8 +677,8 @@ class Fragment { } template - std::unique_ptr make_graph() { - return std::make_unique(); + std::shared_ptr make_graph() { + return std::make_shared(); } template @@ -666,20 +687,23 @@ class Fragment { } template - std::unique_ptr make_executor(ArgsT&&... args) { - return std::make_unique(std::forward(args)...); + std::shared_ptr make_executor(ArgsT&&... args) { + return std::make_shared(std::forward(args)...); } /// Cleanup helper that will by called by GXFExecutor prior to GxfContextDestroy. void reset_graph_entities(); + /// Load the GXF extensions specified in the configuration. + void load_extensions_from_config(); + // Note: Maintain the order of declarations (executor_ and graph_) to ensure proper destruction // of the executor's context. std::string name_; ///< The name of the fragment. Application* app_ = nullptr; ///< The application that this fragment belongs to. std::shared_ptr config_; ///< The configuration of the fragment. std::shared_ptr executor_; ///< The executor for the fragment. - std::unique_ptr graph_; ///< The graph of the fragment. + std::shared_ptr graph_; ///< The graph of the fragment. std::shared_ptr scheduler_; ///< The scheduler used by the executor std::shared_ptr network_context_; ///< The network_context used by the executor std::shared_ptr data_flow_tracker_; ///< The DataFlowTracker for the fragment diff --git a/include/holoscan/core/graph.hpp b/include/holoscan/core/graph.hpp index ca5da60d..b883766a 100644 --- a/include/holoscan/core/graph.hpp +++ b/include/holoscan/core/graph.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -61,6 +61,10 @@ class Graph { Graph() = default; virtual ~Graph() = default; + // Delete the copy constructor and assignment operator to prevent copying. + Graph(const Graph&) = delete; + Graph& operator=(const Graph&) = delete; + /** * @brief Add the node to the graph. * diff --git a/include/holoscan/core/gxf/entity.hpp b/include/holoscan/core/gxf/entity.hpp index 7bb0e9db..5646914e 100644 --- a/include/holoscan/core/gxf/entity.hpp +++ b/include/holoscan/core/gxf/entity.hpp @@ -19,6 +19,7 @@ #define HOLOSCAN_CORE_GXF_ENTITY_HPP #include +#include // Entity definition // Since it has code that causes a warning as an error, we disable it here. @@ -47,7 +48,7 @@ class Entity : public nvidia::gxf::Entity { public: Entity() = default; explicit Entity(const nvidia::gxf::Entity& other) : nvidia::gxf::Entity(other) {} - explicit Entity(nvidia::gxf::Entity&& other) : nvidia::gxf::Entity(other) {} + explicit Entity(nvidia::gxf::Entity&& other) : nvidia::gxf::Entity(std::move(other)) {} // Creates a new entity static Entity New(ExecutionContext* context); @@ -122,7 +123,7 @@ class Entity : public nvidia::gxf::Entity { auto handle = nvidia::gxf::Handle::Create(context(), cid); nvidia::gxf::Tensor* tensor_ptr = handle->get(); - // Copy the member data (std::shared_ptr) from the Tensor to the + // Copy the member data (std::shared_ptr) from the Tensor to the // nvidia::gxf::Tensor *tensor_ptr = nvidia::gxf::Tensor(data->dl_ctx()); } diff --git a/include/holoscan/core/gxf/gxf_component.hpp b/include/holoscan/core/gxf/gxf_component.hpp index 2763c247..18f964ed 100644 --- a/include/holoscan/core/gxf/gxf_component.hpp +++ b/include/holoscan/core/gxf/gxf_component.hpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include "../parameter.hpp" @@ -55,7 +56,7 @@ class GXFComponent { std::shared_ptr gxf_graph_entity() { return gxf_graph_entity_; } void gxf_graph_entity(std::shared_ptr graph_entity) { - gxf_graph_entity_ = graph_entity; + gxf_graph_entity_ = std::move(graph_entity); } void* gxf_cptr() { return gxf_cptr_; } diff --git a/include/holoscan/core/gxf/gxf_execution_context.hpp b/include/holoscan/core/gxf/gxf_execution_context.hpp index 99cc919c..6fac2020 100644 --- a/include/holoscan/core/gxf/gxf_execution_context.hpp +++ b/include/holoscan/core/gxf/gxf_execution_context.hpp @@ -52,6 +52,7 @@ class GXFExecutionContext : public holoscan::ExecutionContext { GXFExecutionContext(gxf_context_t context, std::shared_ptr gxf_input_context, std::shared_ptr gxf_output_context); + ~GXFExecutionContext() override = default; /** * @brief Get the GXF input context. * diff --git a/include/holoscan/core/gxf/gxf_io_context.hpp b/include/holoscan/core/gxf/gxf_io_context.hpp index 3963f188..ea98f64a 100644 --- a/include/holoscan/core/gxf/gxf_io_context.hpp +++ b/include/holoscan/core/gxf/gxf_io_context.hpp @@ -97,7 +97,8 @@ class GXFOutputContext : public OutputContext { protected: void emit_impl(std::any data, const char* name = nullptr, - OutputType out_type = OutputType::kSharedPointer) override; + OutputType out_type = OutputType::kSharedPointer, + const int64_t acq_timestamp = -1) override; }; } // namespace holoscan::gxf diff --git a/include/holoscan/core/io_context.hpp b/include/holoscan/core/io_context.hpp index 030d7004..59920f6b 100644 --- a/include/holoscan/core/io_context.hpp +++ b/include/holoscan/core/io_context.hpp @@ -39,6 +39,10 @@ namespace holoscan { +// To indicate that data is not available for the input port +struct NoMessageType {}; +constexpr NoMessageType kNoReceivedMessage; + static inline std::string get_well_formed_name( const char* name, const std::unordered_map>& io_list) { std::string well_formed_name; @@ -293,29 +297,13 @@ class InputContext { // If it is not a vector then try to get the input directly and convert for respective data // type for an input auto value = receive_impl(name); - // If the received data is nullptr, then check whether nullptr or empty holoscan::gxf::Entity - // can be sent - if (value.type() == typeid(nullptr_t)) { - HOLOSCAN_LOG_DEBUG("nullptr is received from the input port with name '{}'", name); - // If it is a shared pointer, or raw pointer then return nullptr because it might be a valid - // nullptr - if constexpr (holoscan::is_shared_ptr_v) { - return nullptr; - } else if constexpr (std::is_pointer_v) { - return nullptr; - } - // If it's holoscan::gxf::Entity then return an error message - if constexpr (is_one_of_derived_v) { - auto error_message = fmt::format( - "Null received in place of nvidia::gxf::Entity or derived type for input {}", name); - return make_unexpected( - holoscan::RuntimeError(holoscan::ErrorCode::kReceiveError, error_message.c_str())); - } else if constexpr (is_one_of_derived_v) { - auto error_message = fmt::format( - "Null received in place of holoscan::TensorMap or derived type for input {}", name); - return make_unexpected( - holoscan::RuntimeError(holoscan::ErrorCode::kReceiveError, error_message.c_str())); - } + // If no message is received, return an error message + if (value.type() == typeid(NoMessageType)) { + HOLOSCAN_LOG_DEBUG("No message is received from the input port with name '{}'", name); + auto error_message = + fmt::format("No message is received from the input port with name '{}'", name); + return make_unexpected( + holoscan::RuntimeError(holoscan::ErrorCode::kReceiveError, error_message.c_str())); } try { // Check if the types of value and DataT are the same or not @@ -323,6 +311,17 @@ class InputContext { DataT return_value = std::any_cast(value); return return_value; } catch (const std::bad_any_cast& e) { + // If the received data is nullptr, check whether the sent value was nullptr + if (value.type() == typeid(nullptr_t)) { + HOLOSCAN_LOG_DEBUG("nullptr is received from the input port with name '{}'", name); + // If it is a shared pointer or raw pointer, then return nullptr + if constexpr (holoscan::is_shared_ptr_v) { + return nullptr; + } else if constexpr (std::is_pointer_v) { + return nullptr; + } + } + // If it is of the type of holoscan::gxf::Entity then show a specific error message if constexpr (is_one_of_derived_v) { auto error_message = fmt::format( @@ -504,11 +503,14 @@ class OutputContext { * @tparam DataT The type of the data to send. * @param data The shared pointer to the data. * @param name The name of the output port. + * @param acq_timestamp The time when the message is acquired. For instance, this would generally + * be the timestamp of the camera when it captures an image. */ template >> - void emit(std::shared_ptr& data, const char* name = nullptr) { - emit_impl(data, name); + void emit(std::shared_ptr& data, const char* name = nullptr, + const int64_t acq_timestamp = -1) { + emit_impl(data, name, OutputType::kSharedPointer, acq_timestamp); } /** @@ -559,17 +561,19 @@ class OutputContext { * @tparam DataT The type of the data to send. It should be `holoscan::gxf::Entity`. * @param data The entity object to send (`holoscan::gxf::Entity`). * @param name The name of the output port. + * @param acq_timestamp The time when the message is acquired. For instance, this would generally + * be the timestamp of the camera when it captures an image. */ template >> - void emit(DataT& data, const char* name = nullptr) { + void emit(DataT& data, const char* name = nullptr, const int64_t acq_timestamp = -1) { // if it is the same as nvidia::gxf::Entity then just pass it to emit_impl if constexpr (holoscan::is_one_of_v) { - emit_impl(data, name, OutputType::kGXFEntity); + emit_impl(data, name, OutputType::kGXFEntity, acq_timestamp); } else { // Convert it to nvidia::gxf::Entity and then pass it to emit_impl // Otherwise, we will lose the type information and cannot cast appropriately in emit_impl - emit_impl(nvidia::gxf::Entity(data), name, OutputType::kGXFEntity); + emit_impl(nvidia::gxf::Entity(data), name, OutputType::kGXFEntity, acq_timestamp); } } @@ -621,17 +625,20 @@ class OutputContext { * (std::shared_ptr) or the GXF Entity (holoscan::gxf::Entity) type. * @param data The entity object to send (as `std::any`). * @param name The name of the output port. + * @param acq_timestamp The time when the message is acquired. For instance, this would generally + * be the timestamp of the camera when it captures an image. */ template >> - void emit(DataT data, const char* name = nullptr) { - emit_impl(data, name, OutputType::kAny); + void emit(DataT data, const char* name = nullptr, const int64_t acq_timestamp = -1) { + emit_impl(std::move(data), name, OutputType::kAny, acq_timestamp); } - void emit(holoscan::TensorMap& data, const char* name = nullptr) { + void emit(holoscan::TensorMap& data, const char* name = nullptr, + const int64_t acq_timestamp = -1) { auto out_message = holoscan::gxf::Entity::New(execution_context_); for (auto& [key, tensor] : data) { out_message.add(tensor, key.c_str()); } - emit(out_message, name); + emit(out_message, name, acq_timestamp); } protected: @@ -645,12 +652,10 @@ class OutputContext { * @param name The name of the output port. * @param out_type The type of the message data. */ - virtual void emit_impl(std::any data, const char* name = nullptr, - OutputType out_type = OutputType::kSharedPointer) { - (void)data; - (void)name; - (void)out_type; - } + virtual void emit_impl([[maybe_unused]] std::any data, + [[maybe_unused]] const char* name = nullptr, + [[maybe_unused]] OutputType out_type = OutputType::kSharedPointer, + [[maybe_unused]] const int64_t acq_timestamp = -1) {} ExecutionContext* execution_context_ = nullptr; ///< The execution context that is associated with. diff --git a/include/holoscan/core/io_spec.hpp b/include/holoscan/core/io_spec.hpp index b0cfb171..087cdeae 100644 --- a/include/holoscan/core/io_spec.hpp +++ b/include/holoscan/core/io_spec.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,6 +35,7 @@ #include "./conditions/gxf/downstream_affordable.hpp" #include "./conditions/gxf/periodic.hpp" #include "./conditions/gxf/message_available.hpp" +#include "./conditions/gxf/expiring_message.hpp" #include "./resources/gxf/double_buffer_receiver.hpp" #include "./resources/gxf/double_buffer_transmitter.hpp" #include "./resources/gxf/ucx_receiver.hpp" @@ -172,6 +173,11 @@ class IOSpec { conditions_.emplace_back( type, std::make_shared(std::forward(args)...)); break; + case ConditionType::kExpiringMessageAvailable: + conditions_.emplace_back( + type, + std::make_shared(std::forward(args)...)); + break; case ConditionType::kDownstreamMessageAffordable: conditions_.emplace_back( type, @@ -199,7 +205,7 @@ class IOSpec { * * @param connector The connector (transmitter or receiver) of this input/output. */ - void connector(std::shared_ptr connector) { connector_ = connector; } + void connector(std::shared_ptr connector) { connector_ = std::move(connector); } /** * @brief Add a connector (receiver/transmitter) to this input/output. diff --git a/include/holoscan/core/resources/gxf/gxf_component_resource.hpp b/include/holoscan/core/resources/gxf/gxf_component_resource.hpp index a545584a..603a0fe9 100644 --- a/include/holoscan/core/resources/gxf/gxf_component_resource.hpp +++ b/include/holoscan/core/resources/gxf/gxf_component_resource.hpp @@ -74,7 +74,7 @@ namespace holoscan { explicit class_name(ArgT&& arg, ArgsT&&... args) \ : ::holoscan::GXFComponentResource(gxf_typename, std::forward(arg), \ std::forward(args)...) {} \ - class_name() = default; \ + class_name() : ::holoscan::GXFComponentResource(gxf_typename) {} \ }; /** diff --git a/include/holoscan/core/services/common/virtual_operator.hpp b/include/holoscan/core/services/common/virtual_operator.hpp index 4cdd616d..5ae7af8d 100644 --- a/include/holoscan/core/services/common/virtual_operator.hpp +++ b/include/holoscan/core/services/common/virtual_operator.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -42,7 +42,7 @@ class VirtualOperator : public holoscan::Operator { template >> VirtualOperator(StringT port_name, IOSpec::ConnectorType connector_type, ArgList arg_list) - : port_name_(port_name), connector_type_(connector_type), arg_list_(arg_list) { + : port_name_(std::move(port_name)), connector_type_(connector_type), arg_list_(arg_list) { operator_type_ = OperatorType::kVirtual; } diff --git a/include/holoscan/core/system/gpu_resource_monitor.hpp b/include/holoscan/core/system/gpu_resource_monitor.hpp index 2ef6ee7b..e5186a80 100644 --- a/include/holoscan/core/system/gpu_resource_monitor.hpp +++ b/include/holoscan/core/system/gpu_resource_monitor.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -231,8 +231,8 @@ class GPUResourceMonitor { bool init_nvml(); bool init_cuda_runtime(); - void shutdown_nvml(); - void shutdown_cuda_runtime(); + void shutdown_nvml() noexcept; + void shutdown_cuda_runtime() noexcept; void* handle_ = nullptr; ///< The handle of the GPU resource monitor void* cuda_handle_ = nullptr; ///< The handle of the CUDA Runtime library diff --git a/include/holoscan/holoscan.hpp b/include/holoscan/holoscan.hpp index 2042e645..d1a1d13c 100644 --- a/include/holoscan/holoscan.hpp +++ b/include/holoscan/holoscan.hpp @@ -51,23 +51,24 @@ #include "./core/network_contexts/gxf/ucx_context.hpp" // Resources -#include "./core/resources/gxf/clock.hpp" #include "./core/resources/gxf/block_memory_pool.hpp" -#include "./core/resources/gxf/manual_clock.hpp" +#include "./core/resources/gxf/clock.hpp" +#include "./core/resources/gxf/cuda_stream_pool.hpp" #include "./core/resources/gxf/double_buffer_receiver.hpp" #include "./core/resources/gxf/double_buffer_transmitter.hpp" +#include "./core/resources/gxf/gxf_component_resource.hpp" +#include "./core/resources/gxf/manual_clock.hpp" #include "./core/resources/gxf/realtime_clock.hpp" -#include "./core/resources/gxf/cuda_stream_pool.hpp" #include "./core/resources/gxf/serialization_buffer.hpp" #include "./core/resources/gxf/std_component_serializer.hpp" #include "./core/resources/gxf/std_entity_serializer.hpp" -#include "./core/resources/gxf/unbounded_allocator.hpp" #include "./core/resources/gxf/ucx_component_serializer.hpp" #include "./core/resources/gxf/ucx_entity_serializer.hpp" #include "./core/resources/gxf/ucx_holoscan_component_serializer.hpp" #include "./core/resources/gxf/ucx_receiver.hpp" #include "./core/resources/gxf/ucx_serialization_buffer.hpp" #include "./core/resources/gxf/ucx_transmitter.hpp" +#include "./core/resources/gxf/unbounded_allocator.hpp" // Schedulers #include "./core/schedulers/gxf/event_based_scheduler.hpp" diff --git a/include/holoscan/operators/aja_source/aja_source.hpp b/include/holoscan/operators/aja_source/aja_source.hpp index bb0ee972..25ae4968 100644 --- a/include/holoscan/operators/aja_source/aja_source.hpp +++ b/include/holoscan/operators/aja_source/aja_source.hpp @@ -112,8 +112,8 @@ class AJASourceOp : public holoscan::Operator { // internal state CNTV2Card device_; - NTV2DeviceID device_id_; - NTV2VideoFormat video_format_; + NTV2DeviceID device_id_ = DEVICE_ID_NOTFOUND; + NTV2VideoFormat video_format_ = NTV2_FORMAT_1080p_6000_A; NTV2PixelFormat pixel_format_ = NTV2_FBF_ABGR; bool use_tsi_ = false; bool is_kona_hdmi_ = false; diff --git a/include/holoscan/operators/bayer_demosaic/bayer_demosaic.hpp b/include/holoscan/operators/bayer_demosaic/bayer_demosaic.hpp index c969d33f..49519e0a 100644 --- a/include/holoscan/operators/bayer_demosaic/bayer_demosaic.hpp +++ b/include/holoscan/operators/bayer_demosaic/bayer_demosaic.hpp @@ -15,8 +15,8 @@ * limitations under the License. */ -#ifndef HOLOSCAN_OPERATORS_BAYER_DEMOSAIC_HPP -#define HOLOSCAN_OPERATORS_BAYER_DEMOSAIC_HPP +#ifndef HOLOSCAN_OPERATORS_BAYER_DEMOSAIC_BAYER_DEMOSAIC_HPP +#define HOLOSCAN_OPERATORS_BAYER_DEMOSAIC_BAYER_DEMOSAIC_HPP #include @@ -123,8 +123,9 @@ class BayerDemosaicOp : public holoscan::Operator { NppStreamContext npp_stream_ctx_{}; - NppiInterpolationMode npp_bayer_interp_mode_; - NppiBayerGridPosition npp_bayer_grid_pos_; + // defaults here will be overridden later by parameter defaults in setup method + NppiInterpolationMode npp_bayer_interp_mode_ = NPPI_INTER_UNDEFINED; + NppiBayerGridPosition npp_bayer_grid_pos_ = NPPI_BAYER_GBRG; nvidia::gxf::MemoryBuffer device_scratch_buffer_; @@ -133,4 +134,4 @@ class BayerDemosaicOp : public holoscan::Operator { } // namespace holoscan::ops -#endif /* HOLOSCAN_OPERATORS_BAYER_DEMOSAIC_HPP */ +#endif /* HOLOSCAN_OPERATORS_BAYER_DEMOSAIC_BAYER_DEMOSAIC_HPP */ diff --git a/include/holoscan/operators/holoviz/holoviz.hpp b/include/holoscan/operators/holoviz/holoviz.hpp index 91ae2dea..4d00b022 100644 --- a/include/holoscan/operators/holoviz/holoviz.hpp +++ b/include/holoscan/operators/holoviz/holoviz.hpp @@ -498,9 +498,9 @@ class HolovizOp : public Operator { std::vector lut_; std::vector initial_input_spec_; CudaStreamHandler cuda_stream_handler_; - bool render_buffer_input_enabled_; - bool render_buffer_output_enabled_; - bool camera_pose_output_enabled_; + bool render_buffer_input_enabled_ = false; + bool render_buffer_output_enabled_ = false; + bool camera_pose_output_enabled_ = false; bool is_first_tick_ = true; std::array camera_eye_cur_; //< current camera eye position diff --git a/include/holoscan/operators/inference/inference.hpp b/include/holoscan/operators/inference/inference.hpp index c1c0246b..e87d60ab 100644 --- a/include/holoscan/operators/inference/inference.hpp +++ b/include/holoscan/operators/inference/inference.hpp @@ -69,8 +69,10 @@ namespace holoscan::ops { * - **pre_processor_map**: Pre processed data to model map. * - **device_map**: Mapping of model (`DataMap`) to GPU ID for inference. Optional. * - **backend_map**: Mapping of model (`DataMap`) to backend type for inference. - * - **temporal_map**: Mapping of model (`DataMap`) to a frame delay for model inference. Optional. * Backend options: `"trt"` or `"torch"`. Optional. + * - **temporal_map**: Mapping of model (`DataMap`) to a frame delay for model inference. Optional. + * - **activation_map**: Mapping of model (`DataMap`) to a activation state for model inference. + * Optional. * - **in_tensor_names**: Input tensors (`std::vector`). Optional. * - **out_tensor_names**: Output tensors (`std::vector`). Optional. * - **infer_on_cpu**: Whether to run the computation on the CPU instead of GPU. Optional @@ -151,6 +153,9 @@ class InferenceOp : public holoscan::Operator { /// @brief Map with key as model name and value as frame delay for model inference Parameter temporal_map_; + /// @brief Map with key as model name and value as an activation state for model inference + Parameter activation_map_; + /// @brief Input tensor names Parameter> in_tensor_names_; diff --git a/include/holoscan/operators/segmentation_postprocessor/segmentation_postprocessor.hpp b/include/holoscan/operators/segmentation_postprocessor/segmentation_postprocessor.hpp index 6ed17971..e0a6a1db 100644 --- a/include/holoscan/operators/segmentation_postprocessor/segmentation_postprocessor.hpp +++ b/include/holoscan/operators/segmentation_postprocessor/segmentation_postprocessor.hpp @@ -77,8 +77,9 @@ class SegmentationPostprocessorOp : public Operator { ExecutionContext& context) override; private: - NetworkOutputType network_output_type_value_; - DataFormat data_format_value_; + // Defaults set here will be overridden by parameters defined in the setup method + NetworkOutputType network_output_type_value_ = NetworkOutputType::kSoftmax; + DataFormat data_format_value_ = DataFormat::kHWC; Parameter in_; Parameter out_; diff --git a/include/holoscan/operators/v4l2_video_capture/v4l2_video_capture.hpp b/include/holoscan/operators/v4l2_video_capture/v4l2_video_capture.hpp index 0df55093..f51ac0fa 100644 --- a/include/holoscan/operators/v4l2_video_capture/v4l2_video_capture.hpp +++ b/include/holoscan/operators/v4l2_video_capture/v4l2_video_capture.hpp @@ -89,6 +89,7 @@ namespace holoscan::ops { * int64_t row_stride = (row_bytes % 256 == 0) ? row_bytes : ((row_bytes / 256 + 1) * 256); * return height_even * row_stride; * } + * ``` */ class V4L2VideoCaptureOp : public Operator { public: @@ -133,12 +134,12 @@ class V4L2VideoCaptureOp : public Operator { void* ptr; size_t length; }; - Buffer* buffers_; + Buffer* buffers_ = nullptr; int fd_ = -1; - uint32_t width_use_; - uint32_t height_use_; - uint32_t pixel_format_use_; + uint32_t width_use_{0}; + uint32_t height_use_{0}; + uint32_t pixel_format_use_{V4L2_PIX_FMT_RGBA32}; }; } // namespace holoscan::ops diff --git a/include/holoscan/operators/video_stream_recorder/video_stream_recorder.hpp b/include/holoscan/operators/video_stream_recorder/video_stream_recorder.hpp index 2287d3b5..b7e5d220 100644 --- a/include/holoscan/operators/video_stream_recorder/video_stream_recorder.hpp +++ b/include/holoscan/operators/video_stream_recorder/video_stream_recorder.hpp @@ -75,7 +75,7 @@ class VideoStreamRecorderOp : public holoscan::Operator { // File stream for binary data nvidia::gxf::FileStream binary_file_stream_; // Offset into binary file - size_t binary_file_offset_; + size_t binary_file_offset_{0}; }; } // namespace holoscan::ops diff --git a/modules/holoinfer/src/include/holoinfer.hpp b/modules/holoinfer/src/include/holoinfer.hpp index 8a0be6dd..4f05e5b3 100644 --- a/modules/holoinfer/src/include/holoinfer.hpp +++ b/modules/holoinfer/src/include/holoinfer.hpp @@ -48,12 +48,11 @@ class _HOLOSCAN_EXTERNAL_API_ InferContext { * Executes the inference * Toolkit supports one input per model, in float32 type * - * @param preprocess_data_map Map of model names as key mapped to the preprocessed input data - * @param output_data_map Map of tensor names as key mapped to the inferred data + * @param inference_specs Pointer to inference specifications * * @return InferStatus with appropriate holoinfer_code and message. */ - InferStatus execute_inference(DataMap& preprocess_data_map, DataMap& output_data_map); + InferStatus execute_inference(std::shared_ptr& inference_specs); /** * Gets output dimension per model diff --git a/modules/holoinfer/src/include/holoinfer_buffer.hpp b/modules/holoinfer/src/include/holoinfer_buffer.hpp index 3909aeb5..adfd8565 100644 --- a/modules/holoinfer/src/include/holoinfer_buffer.hpp +++ b/modules/holoinfer/src/include/holoinfer_buffer.hpp @@ -207,6 +207,7 @@ struct InferenceSpecs { * @param inference_map Map with model name as key, output tensor names in vector form as value * @param device_map Map with model name as key, GPU ID for inference as value * @param temporal_map Map with model name as key, frame number to skip for inference as value + * @param activation_map Map with key as model name and activation state for inference as value * @param is_engine_path Input path to model is trt engine * @param oncpu Perform inference on CPU * @param parallel_proc Perform parallel inference of multiple models @@ -217,8 +218,9 @@ struct InferenceSpecs { InferenceSpecs(const std::string& backend, const Mappings& backend_map, const Mappings& model_path_map, const MultiMappings& pre_processor_map, const MultiMappings& inference_map, const Mappings& device_map, - const Mappings& temporal_map, bool is_engine_path, bool oncpu, bool parallel_proc, - bool use_fp16, bool cuda_buffer_in, bool cuda_buffer_out) + const Mappings& temporal_map, const Mappings& activation_map, bool is_engine_path, + bool oncpu, bool parallel_proc, bool use_fp16, bool cuda_buffer_in, + bool cuda_buffer_out) : backend_type_(backend), backend_map_(backend_map), model_path_map_(model_path_map), @@ -226,6 +228,7 @@ struct InferenceSpecs { inference_map_(inference_map), device_map_(device_map), temporal_map_(temporal_map), + activation_map_(activation_map), is_engine_path_(is_engine_path), oncuda_(!oncpu), parallel_processing_(parallel_proc), @@ -257,6 +260,22 @@ struct InferenceSpecs { */ Mappings get_temporal_map() const { return temporal_map_; } + /** + * @brief Get the Activation map + * @return Mappings data + */ + Mappings get_activation_map() const { return activation_map_; } + + /** + * @brief Set the Activation map + * @param activation_map Map that will be used to update the activation_map_ of specs. + */ + void set_activation_map(const Mappings& activation_map) { + for (const auto& [key, value] : activation_map) { + if (activation_map_.find(key) != activation_map.end()) { activation_map_.at(key) = value; } + } + } + /// @brief Backend type (for all models) std::string backend_type_{""}; @@ -278,6 +297,9 @@ struct InferenceSpecs { /// @brief Map with key as model name and frame number to skip for inference as value Mappings temporal_map_; + /// @brief Map with key as model name and activation state for inference as value + Mappings activation_map_; + /// @brief Flag showing if input model path is path to engine files bool is_engine_path_ = false; diff --git a/modules/holoinfer/src/include/holoinfer_constants.hpp b/modules/holoinfer/src/include/holoinfer_constants.hpp index c5bf73ee..ed5fda55 100644 --- a/modules/holoinfer/src/include/holoinfer_constants.hpp +++ b/modules/holoinfer/src/include/holoinfer_constants.hpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -69,6 +69,10 @@ class _HOLOSCAN_EXTERNAL_API_ InferStatus { void set_message(const std::string& _m) { _message = _m; } void display_message() const { switch (_code) { + case holoinfer_code::H_WARNING: { + HOLOSCAN_LOG_WARN(_message); + break; + } case holoinfer_code::H_SUCCESS: default: { HOLOSCAN_LOG_INFO(_message); diff --git a/modules/holoinfer/src/infer/onnx/core.cpp b/modules/holoinfer/src/infer/onnx/core.cpp index 28a76937..4e18715f 100644 --- a/modules/holoinfer/src/infer/onnx/core.cpp +++ b/modules/holoinfer/src/infer/onnx/core.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -138,6 +138,10 @@ holoinfer_datatype OnnxInferImpl::get_holoinfer_datatype(ONNXTensorElementDataTy return holoinfer_datatype::h_Int8; case ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32: return holoinfer_datatype::h_Int32; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64: + return holoinfer_datatype::h_Int64; + case ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT8: + return holoinfer_datatype::h_UInt8; default: return holoinfer_datatype::h_Unsupported; } @@ -244,7 +248,14 @@ Ort::Value OnnxInferImpl::create_tensor(const std::shared_ptr& input return create_tensor_core(input_buffer, dims, memory_info_); case holoinfer_datatype::h_Int32: return create_tensor_core(input_buffer, dims, memory_info_); + case holoinfer_datatype::h_Int64: + return create_tensor_core(input_buffer, dims, memory_info_); + case holoinfer_datatype::h_UInt8: + return create_tensor_core(input_buffer, dims, memory_info_); default: { + HOLOSCAN_LOG_INFO( + "Onnxruntime backend is supported with following data types: float, int8, int32, int64, " + "uint8"); HOLOSCAN_LOG_ERROR("Unsupported datatype in Onnx backend tensor creation."); return Ort::Value(nullptr); } @@ -268,8 +279,17 @@ void OnnxInferImpl::transfer_to_output(std::vector>& case holoinfer_datatype::h_Int32: transfer_to_host(output_buffer[index], output_tensors_[index], output_tensor_size); break; + case holoinfer_datatype::h_Int64: + transfer_to_host(output_buffer[index], output_tensors_[index], output_tensor_size); + break; + case holoinfer_datatype::h_UInt8: + transfer_to_host(output_buffer[index], output_tensors_[index], output_tensor_size); + break; default: - throw std::runtime_error("Unsupported datatype"); + HOLOSCAN_LOG_INFO( + "Onnxruntime backend is supported with following data types: float, int8, int32, int64, " + "uint8"); + throw std::runtime_error("Unsupported datatype in output transfer with onnxrt backend."); } } diff --git a/modules/holoinfer/src/infer/torch/core.cpp b/modules/holoinfer/src/infer/torch/core.cpp index 083f4c53..fee5395e 100644 --- a/modules/holoinfer/src/infer/torch/core.cpp +++ b/modules/holoinfer/src/infer/torch/core.cpp @@ -161,10 +161,15 @@ torch::Tensor TorchInferImpl::create_tensor(const std::shared_ptr& i case holoinfer_datatype::h_Int32: return create_tensor_core( input_buffer, dims, torch::kI32, infer_device_, input_device_, cstream); + case holoinfer_datatype::h_Int64: + return create_tensor_core( + input_buffer, dims, torch::kI64, infer_device_, input_device_, cstream); case holoinfer_datatype::h_UInt8: return create_tensor_core( input_buffer, dims, torch::kUInt8, infer_device_, input_device_, cstream); default: { + HOLOSCAN_LOG_INFO( + "Torch backend is supported with following data types: float, int8, int32, int64, uint8"); HOLOSCAN_LOG_ERROR("Unsupported datatype in Torch backend tensor creation."); return torch::empty({0}); } @@ -281,7 +286,16 @@ InferStatus TorchInferImpl::transfer_to_output( infer_device_, output_device_, cstream); + case holoinfer_datatype::h_UInt8: + return transfer_from_tensor(output_buffer[index], + out_torch_tensor, + output_dims_[index], + infer_device_, + output_device_, + cstream); default: + HOLOSCAN_LOG_INFO( + "Torch backend is supported with following data types: float, int8, int32, int64, uint8"); return InferStatus(holoinfer_code::H_ERROR, "Unsupported datatype for transfer."); } } @@ -541,7 +555,7 @@ InferStatus TorchInfer::do_inference(const std::vectoroutput_tensors_[a]; - auto status = impl_->transfer_to_output(output_buffer, current_tensor, a); + auto status = impl_->transfer_to_output(output_buffer, std::move(current_tensor), a); HOLOSCAN_LOG_ERROR("Transfer of Tensor {} failed in inferece core.", impl_->output_names_[a]); return status; diff --git a/modules/holoinfer/src/infer/trt/core.cpp b/modules/holoinfer/src/infer/trt/core.cpp index 72c86bab..5e3e91c8 100644 --- a/modules/holoinfer/src/infer/trt/core.cpp +++ b/modules/holoinfer/src/infer/trt/core.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -165,7 +165,12 @@ bool TrtInfer::initialize_parameters() { holoinfer_type = holoinfer_datatype::h_Int8; break; } + case nvinfer1::DataType::kUINT8: { + holoinfer_type = holoinfer_datatype::h_UInt8; + break; + } default: { + HOLOSCAN_LOG_INFO("TensorRT backend supports float, int8, int32, uint8 data types."); HOLOSCAN_LOG_ERROR("Data type not supported."); return false; } @@ -199,6 +204,7 @@ bool TrtInfer::initialize_parameters() { } } break; default: { + HOLOSCAN_LOG_INFO("All tensors must have dimension size between 2 and 4."); throw std::runtime_error("Dimension size not supported: " + std::to_string(dims.nbDims)); } diff --git a/modules/holoinfer/src/manager/infer_manager.cpp b/modules/holoinfer/src/manager/infer_manager.cpp index 273014bd..1b297eb9 100644 --- a/modules/holoinfer/src/manager/infer_manager.cpp +++ b/modules/holoinfer/src/manager/infer_manager.cpp @@ -227,9 +227,7 @@ InferStatus ManagerInfer::set_inference_params(std::shared_ptr& switch (current_backend) { case holoinfer_backend::h_trt: { if (inference_specs->use_fp16_ && inference_specs->is_engine_path_) { - status.set_message( - "WARNING: Engine files are the input, fp16 check/conversion is ignored"); - status.display_message(); + HOLOSCAN_LOG_WARN("Engine files are the input, fp16 check/conversion is ignored"); } if (!inference_specs->oncuda_) { status.set_message("ERROR: TRT backend supports inference on GPU only"); @@ -320,6 +318,7 @@ InferStatus ManagerInfer::set_inference_params(std::shared_ptr& if (!new_torch_infer) { HOLOSCAN_LOG_ERROR(dlerror()); status.set_message("Torch context setup failure."); + dlclose(handle); return status; } dlclose(handle); @@ -681,10 +680,20 @@ InferStatus ManagerInfer::run_core_inference(const std::string& model_name, return InferStatus(); } -InferStatus ManagerInfer::execute_inference(DataMap& permodel_preprocess_data, - DataMap& permodel_output_data) { +InferStatus ManagerInfer::execute_inference(std::shared_ptr& inference_specs) { InferStatus status = InferStatus(); + auto permodel_preprocess_data = inference_specs->data_per_tensor_; + auto permodel_output_data = inference_specs->output_per_model_; + + if (permodel_preprocess_data.size() == 0) { + status.set_code(holoinfer_code::H_ERROR); + status.set_message("Inference manager, Error: Data map empty for inferencing"); + return status; + } + + auto activation_map = inference_specs->get_activation_map(); + if (frame_counter_++ == UINT_MAX - 1) { frame_counter_ = 0; } if (infer_param_.size() == 0) { @@ -701,8 +710,28 @@ InferStatus ManagerInfer::execute_inference(DataMap& permodel_preprocess_data, std::map> inference_futures; s_time = std::chrono::steady_clock::now(); for (const auto& [model_instance, _] : infer_param_) { + bool process_model = true; + + if (activation_map.find(model_instance) != activation_map.end()) { + try { + auto activation_value = std::stoul(activation_map.at(model_instance)); + HOLOSCAN_LOG_INFO("Activation value: {} for Model: {}", activation_value, model_instance); + if (activation_value > 1) { + HOLOSCAN_LOG_WARN("Activation map can have either a value of 0 or 1 for a model."); + HOLOSCAN_LOG_WARN("Activation map value is ignored for model {}", model_instance); + } + if (activation_value == 0) { process_model = false; } + } catch (std::invalid_argument const& ex) { + HOLOSCAN_LOG_WARN("Invalid argument in activation map: {}", ex.what()); + HOLOSCAN_LOG_WARN("Activation map value is ignored for model {}", model_instance); + } catch (std::out_of_range const& ex) { + HOLOSCAN_LOG_WARN("Invalid range in activation map: {}", ex.what()); + HOLOSCAN_LOG_WARN("Activation map value is ignored for model {}", model_instance); + } + } + auto temporal_id = infer_param_.at(model_instance)->get_temporal_id(); - if (frame_counter_ % temporal_id == 0) { + if (process_model && (frame_counter_ % temporal_id == 0)) { if (!parallel_processing_) { InferStatus infer_status = run_core_inference(model_instance, permodel_preprocess_data, permodel_output_data); @@ -769,7 +798,7 @@ InferContext::InferContext() { } catch (const std::bad_alloc&) { throw; } } -InferStatus InferContext::execute_inference(DataMap& data_map, DataMap& output_data_map) { +InferStatus InferContext::execute_inference(std::shared_ptr& inference_specs) { InferStatus status = InferStatus(); if (g_managers.find(unique_id_) == g_managers.end()) { @@ -781,12 +810,7 @@ InferStatus InferContext::execute_inference(DataMap& data_map, DataMap& output_d try { g_manager = g_managers.at(unique_id_); - if (data_map.size() == 0) { - status.set_code(holoinfer_code::H_ERROR); - status.set_message("Inference manager, Error: Data map empty for inferencing"); - return status; - } - status = g_manager->execute_inference(data_map, output_data_map); + status = g_manager->execute_inference(inference_specs); } catch (const std::exception& e) { status.set_code(holoinfer_code::H_ERROR); status.set_message(std::string("Inference manager, Error in inference setup: ") + e.what()); diff --git a/modules/holoinfer/src/manager/infer_manager.hpp b/modules/holoinfer/src/manager/infer_manager.hpp index a78944bd..0ee3cce0 100644 --- a/modules/holoinfer/src/manager/infer_manager.hpp +++ b/modules/holoinfer/src/manager/infer_manager.hpp @@ -71,12 +71,11 @@ class ManagerInfer { /** * @brief Prepares and launches single/multiple inference * - * @param preprocess_data_map Input DataMap with model name as key and DataBuffer as value - * @param output_data_map Output DataMap with tensor name as key and DataBuffer as value + * @param inference_specs specifications for inference * * @return InferStatus with appropriate code and message */ - InferStatus execute_inference(DataMap& preprocess_data_map, DataMap& output_data_map); + InferStatus execute_inference(std::shared_ptr& inference_specs); /** * @brief Executes Core inference for a particular model and generates inferred data diff --git a/modules/holoinfer/src/params/infer_param.hpp b/modules/holoinfer/src/params/infer_param.hpp index ecfb4df1..75ec2a74 100644 --- a/modules/holoinfer/src/params/infer_param.hpp +++ b/modules/holoinfer/src/params/infer_param.hpp @@ -48,7 +48,7 @@ class Params { std::string model_file_path_; std::string instance_name_; int device_id_; - unsigned int temporal_id_; + unsigned int temporal_id_ = 0; std::vector in_tensor_names_; std::vector out_tensor_names_; }; diff --git a/python/holoscan/CMakeLists.txt b/python/holoscan/CMakeLists.txt index d0db6904..3fa6bc2c 100644 --- a/python/holoscan/CMakeLists.txt +++ b/python/holoscan/CMakeLists.txt @@ -87,6 +87,15 @@ add_custom_target(holoscan-python-pyinit ) add_dependencies(holoscan-python holoscan-python-pyinit) +# custom target for top-level decorator.py file is copied +set(CMAKE_PYBIND11_DECORATORS_PY_FILE ${CMAKE_CURRENT_LIST_DIR}/decorator.py) +add_custom_target(holoscan-python-decorator + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_PYBIND11_DECORATORS_PY_FILE}" "${HOLOSCAN_PYTHON_MODULE_BINARY_DIR}/" + DEPENDS "${CMAKE_PYBIND11_DECORATORS_PY_FILE}" +) +add_dependencies(holoscan-python holoscan-python-decorator) + + # copy Holoscan Python CLI module set(HOLOSCAN_PYTHON_CLI_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cli) add_custom_target(holoscan-python-cli @@ -99,10 +108,6 @@ add_dependencies(holoscan-python holoscan-python-cli) add_subdirectory(cli) add_subdirectory(conditions) add_subdirectory(core) -target_link_libraries(core_python - PRIVATE holoscan::core - PRIVATE AJA::ajantv2 # need this to parse NTV2Channel enum from kwargs -) add_subdirectory(executors) add_subdirectory(graphs) add_subdirectory(gxf) diff --git a/python/holoscan/__init__.py b/python/holoscan/__init__.py index 0b2763e7..e89865ee 100644 --- a/python/holoscan/__init__.py +++ b/python/holoscan/__init__.py @@ -18,7 +18,12 @@ # We import cli, core and gxf to make sure they're available before other modules that rely on them from . import cli, core, gxf -__all__ = ["as_tensor", "cli", "core", "gxf"] +try: + from ._version import __version__ +except ImportError: + __version__ = "unknown version" + +__all__ = ["__version__", "as_tensor", "cli", "core", "gxf"] def as_tensor(obj): @@ -92,6 +97,7 @@ def as_tensor(obj): # Other modules are exposed to the public API but will only be lazily loaded _EXTRA_MODULES = [ "conditions", + "decorator", "executors", "graphs", "logger", diff --git a/python/holoscan/cli/common/artifact_sources.py b/python/holoscan/cli/common/artifact_sources.py index 1ee53986..fb7232bf 100644 --- a/python/holoscan/cli/common/artifact_sources.py +++ b/python/holoscan/cli/common/artifact_sources.py @@ -39,7 +39,7 @@ class ArtifactSources: def __init__(self) -> None: self._logger = logging.getLogger("common") - self._supported_holoscan_versions = ["2.0.0", "2.1.0"] + self._supported_holoscan_versions = ["2.0.0", "2.1.0", "2.2.0"] @property def holoscan_versions(self) -> List[str]: diff --git a/python/holoscan/conditions/CMakeLists.txt b/python/holoscan/conditions/CMakeLists.txt index f0b6dbd3..b799cffe 100644 --- a/python/holoscan/conditions/CMakeLists.txt +++ b/python/holoscan/conditions/CMakeLists.txt @@ -20,5 +20,6 @@ holoscan_pybind11_module(conditions count.cpp downstream_message_affordable.cpp message_available.cpp + expiring_message.cpp periodic.cpp ) diff --git a/python/holoscan/conditions/__init__.py b/python/holoscan/conditions/__init__.py index e974bab2..69f6f4d3 100644 --- a/python/holoscan/conditions/__init__.py +++ b/python/holoscan/conditions/__init__.py @@ -20,6 +20,7 @@ holoscan.conditions.BooleanCondition holoscan.conditions.CountCondition holoscan.conditions.DownstreamMessageAffordableCondition + holoscan.conditions.ExpiringMessageAvailableCondition holoscan.conditions.MessageAvailableCondition holoscan.conditions.PeriodicCondition """ @@ -30,6 +31,7 @@ BooleanCondition, CountCondition, DownstreamMessageAffordableCondition, + ExpiringMessageAvailableCondition, MessageAvailableCondition, PeriodicCondition, ) @@ -40,6 +42,7 @@ "BooleanCondition", "CountCondition", "DownstreamMessageAffordableCondition", + "ExpiringMessageAvailableCondition", "MessageAvailableCondition", "PeriodicCondition", ] diff --git a/python/holoscan/conditions/conditions.cpp b/python/holoscan/conditions/conditions.cpp index 4f911798..3b620c08 100644 --- a/python/holoscan/conditions/conditions.cpp +++ b/python/holoscan/conditions/conditions.cpp @@ -30,6 +30,7 @@ void init_count(py::module_&); void init_periodic(py::module_&); void init_downstream_message_affordable(py::module_&); void init_message_available(py::module_&); +void init_expiring_message_available(py::module_&); PYBIND11_MODULE(_conditions, m) { m.doc() = R"pbdoc( @@ -44,5 +45,6 @@ PYBIND11_MODULE(_conditions, m) { init_periodic(m); init_downstream_message_affordable(m); init_message_available(m); + init_expiring_message_available(m); } // PYBIND11_MODULE } // namespace holoscan diff --git a/python/holoscan/conditions/count.cpp b/python/holoscan/conditions/count.cpp index 900fbdad..cc24f07c 100644 --- a/python/holoscan/conditions/count.cpp +++ b/python/holoscan/conditions/count.cpp @@ -66,7 +66,7 @@ class PyCountCondition : public CountCondition { void init_count(py::module_& m) { py::class_>( m, "CountCondition", doc::CountCondition::doc_CountCondition) - .def(py::init(), + .def(py::init(), "fragment"_a, "count"_a = 1L, "name"_a = "noname_count_condition"s, diff --git a/python/holoscan/conditions/expiring_message.cpp b/python/holoscan/conditions/expiring_message.cpp new file mode 100644 index 00000000..fbce4109 --- /dev/null +++ b/python/holoscan/conditions/expiring_message.cpp @@ -0,0 +1,157 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include +#include +#include + +#include "./expiring_message_pydoc.hpp" +#include "holoscan/core/component_spec.hpp" +#include "holoscan/core/conditions/gxf/expiring_message.hpp" +#include "holoscan/core/fragment.hpp" +#include "holoscan/core/gxf/gxf_resource.hpp" +#include "holoscan/core/resources/gxf/realtime_clock.hpp" + +using std::string_literals::operator""s; +using pybind11::literals::operator""_a; + +#define STRINGIFY(x) #x +#define MACRO_STRINGIFY(x) STRINGIFY(x) + +namespace py = pybind11; + +namespace holoscan { + +/* Trampoline classes for handling Python kwargs + * + * These add a constructor that takes a Fragment for which to initialize the condition. + * The explicit parameter list and default arguments take care of providing a Pythonic + * kwarg-based interface with appropriate default values matching the condition's + * default parameters in the C++ API `setup` method. + * + * The sequence of events in this constructor is based on Fragment::make_condition + */ + +class PyExpiringMessageAvailableCondition : public ExpiringMessageAvailableCondition { + public: + /* Inherit the constructors */ + using ExpiringMessageAvailableCondition::ExpiringMessageAvailableCondition; + + // Define a constructor that fully initializes the object. + PyExpiringMessageAvailableCondition( + Fragment* fragment, + // std::shared_ptr receiver, + int64_t max_batch_size, int64_t max_delay_ns, std::shared_ptr clock = nullptr, + const std::string& name = "noname_expiring_message_available_condition") + : ExpiringMessageAvailableCondition(max_batch_size, max_delay_ns) { + name_ = name; + fragment_ = fragment; + if (clock) { + this->add_arg(Arg{"clock", clock}); + } else { + this->add_arg(Arg{"clock", fragment_->make_resource("realtime_clock")}); + } + spec_ = std::make_shared(fragment); + // receiver = receiver; // e.g. DoubleBufferReceiver + setup(*spec_.get()); + } + + template + PyExpiringMessageAvailableCondition( + Fragment* fragment, + // std::shared_ptr receiver, + int64_t max_batch_size, std::chrono::duration recess_period_duration, + std::shared_ptr clock = nullptr, + const std::string& name = "noname_expiring_message_available_condition") + : ExpiringMessageAvailableCondition(max_batch_size, recess_period_duration) { + name_ = name; + fragment_ = fragment; + if (clock) { + this->add_arg(Arg{"clock", clock}); + } else { + this->add_arg(Arg{"clock", fragment_->make_resource("realtime_clock")}); + } + spec_ = std::make_shared(fragment); + // receiver = receiver; // e.g. DoubleBufferReceiver + setup(*spec_.get()); + } +}; + +void init_expiring_message_available(py::module_& m) { + py::class_>( + m, + "ExpiringMessageAvailableCondition", + doc::ExpiringMessageAvailableCondition::doc_ExpiringMessageAvailableCondition) + // TODO: sphinx API doc build complains if more than one ExpiringMessageAvailableCondition + // init method has a docstring specified. For now just set the docstring for the + // overload using datetime.timedelta for the max_delay. + .def(py::init, const std::string&>(), + "fragment"_a, + "max_batch_size"_a, + "max_delay_ns"_a, + "clock"_a = py::none(), + "name"_a = "noname_expiring_message_available_condition"s, + doc::ExpiringMessageAvailableCondition::doc_ExpiringMessageAvailableCondition) + .def(py::init, + const std::string&>(), + "fragment"_a, + "max_batch_size"_a, + "max_delay_ns"_a, + "clock"_a = py::none(), + "name"_a = "noname_expiring_message_available_condition"s, + doc::ExpiringMessageAvailableCondition::doc_ExpiringMessageAvailableCondition) + .def_property_readonly("gxf_typename", + &ExpiringMessageAvailableCondition::gxf_typename, + doc::ExpiringMessageAvailableCondition::doc_gxf_typename) + .def_property("receiver", + py::overload_cast<>(&ExpiringMessageAvailableCondition::receiver), + py::overload_cast>( + &ExpiringMessageAvailableCondition::receiver), + doc::ExpiringMessageAvailableCondition::doc_receiver) + .def_property("max_batch_size", + py::overload_cast<>(&ExpiringMessageAvailableCondition::max_batch_size), + py::overload_cast(&ExpiringMessageAvailableCondition::max_batch_size), + doc::ExpiringMessageAvailableCondition::doc_max_batch_size) + .def("max_delay", + static_cast( + &ExpiringMessageAvailableCondition::max_delay), + doc::ExpiringMessageAvailableCondition::doc_max_delay) + .def("max_delay", + static_cast( + &ExpiringMessageAvailableCondition::max_delay), + doc::ExpiringMessageAvailableCondition::doc_max_delay) + .def("max_delay_ns", + &ExpiringMessageAvailableCondition::max_delay_ns, + doc::ExpiringMessageAvailableCondition::doc_max_delay_ns) + .def("setup", + &ExpiringMessageAvailableCondition::setup, + doc::ExpiringMessageAvailableCondition::doc_setup) + .def("initialize", + &ExpiringMessageAvailableCondition::initialize, + doc::ExpiringMessageAvailableCondition::doc_initialize); +} +} // namespace holoscan diff --git a/python/holoscan/conditions/expiring_message_pydoc.hpp b/python/holoscan/conditions/expiring_message_pydoc.hpp new file mode 100644 index 00000000..db977ab1 --- /dev/null +++ b/python/holoscan/conditions/expiring_message_pydoc.hpp @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef PYHOLOSCAN_CONDITIONS_EXPIRINGMESSAGE_AVAILABLE_PYDOC_HPP +#define PYHOLOSCAN_CONDITIONS_EXPIRINGMESSAGE_AVAILABLE_PYDOC_HPP + +#include + +#include "../macros.hpp" + +namespace holoscan::doc { + +namespace ExpiringMessageAvailableCondition { + +PYDOC(ExpiringMessageAvailableCondition, R"doc( +Condition that tries to wait for specified number of messages in receiver. +When the first message in the queue mature after specified delay since arrival it would fire +regardless. + +Parameters +---------- +fragment : holoscan.core.Fragment + The fragment the condition will be associated with +max_batch_size : int + The maximum number of messages to be batched together. +max_delay_ns: int + Maximum delay in nano seconds. + The maximum delay from first message to wait before submitting workload anyway. +clock : holoscan.resources.Clock or None, optional + The clock used by the scheduler to define the flow of time. If None, a default-constructed + `holoscan.resources.RealtimeClock` will be used. +name : str, optional + The name of the condition. +)doc") + +PYDOC(gxf_typename, R"doc( +The GXF type name of the condition. + +Returns +------- +str + The GXF type name of the condition +)doc") + +PYDOC(setup, R"doc( +Define the component specification. + +Parameters +---------- +spec : holoscan.core.ComponentSpec + Component specification associated with the condition. +)doc") + +PYDOC(receiver, R"doc( +The receiver associated with the condition. +)doc") + +PYDOC(max_batch_size, R"doc( +The maximum number of messages to be batched together. +)doc") + +PYDOC(max_delay, R"doc( +The maximum delay from first message to wait before submitting workload anyway. +)doc") + +PYDOC(max_delay_ns, R"doc( +The maximum delay from first message to wait before submitting workload anyway. +)doc") + +PYDOC(initialize, R"doc( +Initialize the condition + +This method is called only once when the condition is created for the first +time, and uses a light-weight initialization. +)doc") + +} // namespace ExpiringMessageAvailableCondition + +} // namespace holoscan::doc + +#endif /* PYHOLOSCAN_CONDITIONS_MESSAGE_AVAILABLE_PYDOC_HPP */ diff --git a/python/holoscan/core/__init__.py b/python/holoscan/core/__init__.py index 8c1e3e9b..64eb18f5 100644 --- a/python/holoscan/core/__init__.py +++ b/python/holoscan/core/__init__.py @@ -90,8 +90,8 @@ from ._core import PyRegistryContext as _RegistryContext from ._core import PyOperatorSpec as OperatorSpec from ._core import PyTensor as Tensor +from ._core import Resource as _Resource from ._core import ( - Resource, Scheduler, arg_to_py_object, arglist_to_kwargs, @@ -303,6 +303,32 @@ def stop(self): Operator.__init__.__doc__ = _Operator.__init__.__doc__ +class Resource(_Resource): + def __init__(self, fragment, *args, **kwargs): + if not isinstance(fragment, _Fragment): + raise ValueError( + "The first argument to an Resource's constructor must be the Fragment " + "(Application) to which it belongs." + ) + # It is recommended to not use super() + # (https://pybind11.readthedocs.io/en/stable/advanced/classes.html#overriding-virtual-functions-in-python) + _Resource.__init__(self, self, fragment, *args, **kwargs) + # Create a PyComponentSpec object and pass it to the C++ API + spec = ComponentSpec(fragment=self.fragment, component=self) + self.spec = spec + # Call setup method in PyResource class + self.setup(spec) + + def setup(self, spec: ComponentSpec): + """Default implementation of setup method.""" + pass + + +# copy docstrings defined in core_pydoc.hpp +Resource.__doc__ = _Resource.__doc__ +Resource.__init__.__doc__ = _Resource.__init__.__doc__ + + class Tracker: """Context manager to add data flow tracking to an application.""" diff --git a/python/holoscan/core/component.cpp b/python/holoscan/core/component.cpp index c05b28f7..5db222ca 100644 --- a/python/holoscan/core/component.cpp +++ b/python/holoscan/core/component.cpp @@ -18,10 +18,12 @@ #include #include +#include #include #include -#include +#include +#include "component.hpp" #include "component_pydoc.hpp" #include "holoscan/core/arg.hpp" #include "holoscan/core/component.hpp" @@ -33,78 +35,33 @@ namespace py = pybind11; namespace holoscan { -class PyComponentSpec : public ComponentSpec { - public: - /* Inherit the constructors */ - using ComponentSpec::ComponentSpec; - - // Override the constructor to get the py::object for the Python class - explicit PyComponentSpec(Fragment* fragment = nullptr, py::object op = py::none()) - : ComponentSpec(fragment), py_op_(op) {} - - // TOIMPROVE: Should we parse headline and description from kwargs or just - // add them to the function signature? - void py_param(const std::string& name, const py::object& default_value, const ParameterFlag& flag, - const py::kwargs& kwargs) { - using std::string_literals::operator""s; - - bool is_receivers = false; - std::string headline{""s}; - std::string description{""s}; - for (const auto& [name, value] : kwargs) { - std::string param_name = name.cast(); - if (param_name == "headline") { - headline = value.cast(); - } else if (param_name == "description") { - description = value.cast(); - } else { - throw std::runtime_error("unsupported kwarg: "s + param_name); - } +// TOIMPROVE: Should we parse headline and description from kwargs or just +// add them to the function signature? +void PyComponentSpec::py_param(const std::string& name, const py::object& default_value, + const ParameterFlag& flag, const py::kwargs& kwargs) { + using std::string_literals::operator""s; + + bool is_receivers = false; + std::string headline{""s}; + std::string description{""s}; + for (const auto& [nm, value] : kwargs) { + std::string param_name = nm.cast(); + if (param_name == "headline") { + headline = value.cast(); + } else if (param_name == "description") { + description = value.cast(); + } else { + throw std::runtime_error("unsupported kwarg: "s + param_name); } - - // Create parameter object - py_params_.emplace_back(py_op()); - - // Register parameter - auto& parameter = py_params_.back(); - param(parameter, name.c_str(), headline.c_str(), description.c_str(), default_value, flag); - } - - py::object py_op() const { return py_op_; } - - std::list>& py_params() { return py_params_; } - - private: - py::object py_op_ = py::none(); - // NOTE: we use std::list instead of std::vector because we register the address of Parameter - // object to the GXF framework. The address of a std::vector element may change when the vector is - // resized. - std::list> py_params_; -}; - -class PyComponentBase : public ComponentBase { - public: - /* Inherit the constructors */ - using ComponentBase::ComponentBase; - - /* Trampolines (need one for each virtual function) */ - void initialize() override { - /* , , , */ - PYBIND11_OVERRIDE(void, ComponentBase, initialize); } -}; -class PyComponent : public Component { - public: - /* Inherit the constructors */ - using Component::Component; + // Create parameter object + py_params_.emplace_back(py_component()); - /* Trampolines (need one for each virtual function) */ - void initialize() override { - /* , , , */ - PYBIND11_OVERRIDE(void, Component, initialize); - } -}; + // Register parameter + auto& parameter = py_params_.back(); + param(parameter, name.c_str(), headline.c_str(), description.c_str(), default_value, flag); +} void init_component(py::module_& m) { py::class_>( @@ -128,10 +85,10 @@ void init_component(py::module_& m) { .value("DYNAMIC", ParameterFlag::kDynamic); py::class_>( - m, "PyComponentSpec", R"doc(Operator specification class.)doc") + m, "PyComponentSpec", R"doc(Component specification class.)doc") .def(py::init(), "fragment"_a, - "op"_a = py::none(), + "component"_a = py::none(), doc::ComponentSpec::doc_ComponentSpec) .def("param", &PyComponentSpec::py_param, diff --git a/python/holoscan/core/component.hpp b/python/holoscan/core/component.hpp new file mode 100644 index 00000000..e0695a30 --- /dev/null +++ b/python/holoscan/core/component.hpp @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef PYHOLOSCAN_CORE_COMPONENT_HPP +#define PYHOLOSCAN_CORE_COMPONENT_HPP + +#include + +#include +#include +#include + +#include "holoscan/core/component.hpp" +#include "holoscan/core/component_spec.hpp" +#include "holoscan/core/fragment.hpp" +#include "holoscan/core/parameter.hpp" +#include "io_context.hpp" + +namespace py = pybind11; + +namespace holoscan { + +void init_component(py::module_&); + +class PyComponentSpec : public ComponentSpec { + public: + /* Inherit the constructors */ + using ComponentSpec::ComponentSpec; + + // Override the constructor to get the py::object for the Python class + explicit PyComponentSpec(Fragment* fragment = nullptr, py::object component = py::none()) + : ComponentSpec(fragment), py_component_(component) {} + + void py_param(const std::string& name, const py::object& default_value, const ParameterFlag& flag, + const py::kwargs& kwargs); + + py::object py_component() const { return py_component_; } + + std::list>& py_params() { return py_params_; } + + private: + py::object py_component_ = py::none(); + // NOTE: we use std::list instead of std::vector because we register the address of Parameter + // object to the GXF framework. The address of a std::vector element may change when the vector is + // resized. + std::list> py_params_; +}; + +class PyComponentBase : public ComponentBase { + public: + /* Inherit the constructors */ + using ComponentBase::ComponentBase; + + /* Trampolines (need one for each virtual function) */ + void initialize() override { + /* , , , */ + PYBIND11_OVERRIDE(void, ComponentBase, initialize); + } +}; + +class PyComponent : public Component { + public: + /* Inherit the constructors */ + using Component::Component; + + /* Trampolines (need one for each virtual function) */ + void initialize() override { + /* , , , */ + PYBIND11_OVERRIDE(void, Component, initialize); + } +}; + +} // namespace holoscan + +#endif /* PYHOLOSCAN_CORE_COMPONENT_HPP */ diff --git a/python/holoscan/core/component_pydoc.hpp b/python/holoscan/core/component_pydoc.hpp index a4c04018..7b545c1b 100644 --- a/python/holoscan/core/component_pydoc.hpp +++ b/python/holoscan/core/component_pydoc.hpp @@ -99,14 +99,12 @@ flag: holoscan.core.ParameterFlag, optional Notes ----- -This method is intended to be called within the `setup` method of an Operator. +This method is intended to be called within the `setup` method of a Component, Condition or +Resource. -In general, for native Python operators, it is not necessary to call `param` to register a -parameter with the class. Instead, one can just directly add parameters to the Python operator +In general, for native Python resources, it is not necessary to call `param` to register a +parameter with the class. Instead, one can just directly add parameters to the Python resource class (e.g. For example, directly assigning ``self.param_name = value`` in __init__.py). - -The one case which cannot be implemented without a call to `param` is adding a multi-receiver port -to an operator via a parameter with ``kind="receivers"`` set. )doc") } // namespace ComponentSpec diff --git a/python/holoscan/core/condition.cpp b/python/holoscan/core/condition.cpp index 6e982570..da00914f 100644 --- a/python/holoscan/core/condition.cpp +++ b/python/holoscan/core/condition.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -98,7 +98,10 @@ void init_condition(py::module_& m) { .value("MESSAGE_AVAILABLE", ConditionType::kMessageAvailable) .value("DOWNSTREAM_MESSAGE_AFFORDABLE", ConditionType::kDownstreamMessageAffordable) .value("COUNT", ConditionType::kCount) - .value("BOOLEAN", ConditionType::kBoolean); + .value("BOOLEAN", ConditionType::kBoolean) + .value("PERIODIC", ConditionType::kPeriodic) + .value("ASYNCHRONOUS", ConditionType::kAsynchronous) + .value("EXPIRING_MESSAGE_AVAILABLE", ConditionType::kExpiringMessageAvailable); py::class_>( m, "Condition", doc::Condition::doc_Condition) diff --git a/python/holoscan/core/emitter_receiver_registry.hpp b/python/holoscan/core/emitter_receiver_registry.hpp index 51b0a29b..e4699f8c 100644 --- a/python/holoscan/core/emitter_receiver_registry.hpp +++ b/python/holoscan/core/emitter_receiver_registry.hpp @@ -50,10 +50,11 @@ namespace holoscan { */ template struct emitter_receiver { - static void emit(py::object& data, const std::string& name, PyOutputContext& op_output) { + static void emit(py::object& data, const std::string& name, PyOutputContext& op_output, + const int64_t acq_timestamp = -1) { auto cpp_type = data.cast(); py::gil_scoped_release release; - op_output.emit(cpp_type, name.c_str()); + op_output.emit(std::move(cpp_type), name.c_str(), acq_timestamp); return; } @@ -67,10 +68,11 @@ struct emitter_receiver { */ template struct emitter_receiver> { - static void emit(py::object& data, const std::string& name, PyOutputContext& op_output) { + static void emit(py::object& data, const std::string& name, PyOutputContext& op_output, + const int64_t acq_timestamp = -1) { auto cpp_obj = std::make_shared(data.cast()); py::gil_scoped_release release; - op_output.emit>(cpp_obj, name.c_str()); + op_output.emit>(std::move(cpp_obj), name.c_str(), acq_timestamp); return; } static py::object receive(std::any result, const std::string& name, PyInputContext& op_input) { @@ -90,7 +92,8 @@ class EmitterReceiverRegistry { /** * @brief Function type for emitting a data type */ - using EmitFunc = std::function; + using EmitFunc = std::function; /** * @brief Function type for receiving a data type @@ -105,7 +108,8 @@ class EmitterReceiverRegistry { inline static EmitFunc none_emit = []([[maybe_unused]] py::object& data, [[maybe_unused]] const std::string& name, - [[maybe_unused]] PyOutputContext& op_output) -> void { + [[maybe_unused]] PyOutputContext& op_output, + [[maybe_unused]] const int64_t acq_timestamp = -1) -> void { HOLOSCAN_LOG_ERROR( "Unable to emit message (op: '{}', port: '{}')", op_output.op()->name(), name); return; @@ -138,13 +142,15 @@ class EmitterReceiverRegistry { * @param data The Python object corresponding to the data of typeT. * @param name The name of the entity emitted. * @param op_output The PyOutputContext used to emit the data. + * @param acq_timestamp The acquisition timestamp of the data. */ template - static void emit(py::object& data, const std::string& name, PyOutputContext& op_output) { + static void emit(py::object& data, const std::string& name, PyOutputContext& op_output, + const int64_t acq_timestamp = -1) { auto& instance = get_instance(); const std::type_index index = std::type_index(typeid(typeT)); const EmitFunc& func = instance.get_emitter(index); - return func(data, name, op_output); + return func(data, name, op_output, acq_timestamp); } /** diff --git a/python/holoscan/core/emitter_receivers.hpp b/python/holoscan/core/emitter_receivers.hpp index 84324e74..b570996b 100644 --- a/python/holoscan/core/emitter_receivers.hpp +++ b/python/holoscan/core/emitter_receivers.hpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include "../gxf/entity.hpp" @@ -158,10 +159,11 @@ py::object gxf_entity_to_py_object(holoscan::gxf::Entity in_entity) { */ template <> struct emitter_receiver { - static void emit(py::object& data, const std::string& name, PyOutputContext& op_output) { + static void emit(py::object& data, const std::string& name, PyOutputContext& op_output, + const int64_t acq_timestamp = -1) { py::gil_scoped_release release; auto entity = gxf::Entity(static_cast(data.cast())); - op_output.emit(entity, name.c_str()); + op_output.emit(entity, name.c_str(), acq_timestamp); return; } static py::object receive(std::any result, const std::string& name, PyInputContext& op_input) { @@ -179,14 +181,15 @@ struct emitter_receiver { */ template <> struct emitter_receiver { - static void emit(py::object& data, const std::string& name, PyOutputContext& op_output) { + static void emit(py::object& data, const std::string& name, PyOutputContext& op_output, + const int64_t acq_timestamp = -1) { // unused (emit type is holoscan::PyEntity, not holoscan::gxf:Entity) return; } static py::object receive(std::any result, const std::string& name, PyInputContext& op_input) { auto in_entity = std::any_cast(result); - return gxf_entity_to_py_object(in_entity); + return gxf_entity_to_py_object(std::move(in_entity)); } }; @@ -209,7 +212,8 @@ struct emitter_receiver { */ template <> struct emitter_receiver { - static void emit(py::object& data, const std::string& name, PyOutputContext& op_output) { + static void emit(py::object& data, const std::string& name, PyOutputContext& op_output, + const int64_t acq_timestamp = -1) { HOLOSCAN_LOG_DEBUG("py_emit: tensor-like over UCX connector"); // For tensor-like data, we should create an entity and transmit using the holoscan::Tensor // serializer. cloudpickle fails to serialize PyTensor and we want to avoid using it anyways @@ -237,7 +241,7 @@ struct emitter_receiver { py_entity.py_add(py_tensor_obj, "#holoscan: tensor"); } py::gil_scoped_release release2; - op_output.emit(py_entity, name.c_str()); + op_output.emit(py_entity, name.c_str(), acq_timestamp); return; } static py::object receive(std::any result, const std::string& name, PyInputContext& op_input) { @@ -255,7 +259,8 @@ struct emitter_receiver { */ template <> struct emitter_receiver { - static void emit(py::object& data, const std::string& name, PyOutputContext& op_output) { + static void emit(py::object& data, const std::string& name, PyOutputContext& op_output, + const int64_t acq_timestamp = -1) { bool is_tensormap = true; auto dict_obj = data.cast(); @@ -285,14 +290,15 @@ struct emitter_receiver { py_entity.py_add(py_tensor_obj, key.c_str()); } py::gil_scoped_release release2; - op_output.emit(py_entity, name.c_str()); + op_output.emit(py_entity, name.c_str(), acq_timestamp); return; } else { // If the dict is not a TensorMap, pass it as a Python object HOLOSCAN_LOG_DEBUG("py_emit: dict, but not a tensormap"); auto data_ptr = std::make_shared(data); py::gil_scoped_release release; - op_output.emit>(data_ptr, name.c_str()); + op_output.emit>( + std::move(data_ptr), name.c_str(), acq_timestamp); return; } } @@ -309,11 +315,13 @@ struct emitter_receiver { */ template <> struct emitter_receiver> { - static void emit(py::object& data, const std::string& name, PyOutputContext& op_output) { + static void emit(py::object& data, const std::string& name, PyOutputContext& op_output, + const int64_t acq_timestamp = -1) { // Emit everything else as a Python object. auto data_ptr = std::make_shared(data); py::gil_scoped_release release; - op_output.emit>(data_ptr, name.c_str()); + op_output.emit>( + std::move(data_ptr), name.c_str(), acq_timestamp); return; } @@ -330,14 +338,15 @@ struct emitter_receiver> { */ template <> struct emitter_receiver { - static void emit(py::object& data, const std::string& name, PyOutputContext& op_output) { + static void emit(py::object& data, const std::string& name, PyOutputContext& op_output, + const int64_t acq_timestamp = -1) { // use cloudpickle to serialize as a string py::module_ cloudpickle = py::module_::import("cloudpickle"); py::bytes serialized = cloudpickle.attr("dumps")(data); py::gil_scoped_release release; - auto serialized_str = serialized.cast(); - CloudPickleSerializedObject serialized_obj{serialized_str}; - op_output.emit(serialized_obj, name.c_str()); + CloudPickleSerializedObject serialized_obj{serialized.cast()}; + op_output.emit( + std::move(serialized_obj), name.c_str(), acq_timestamp); return; } @@ -356,8 +365,9 @@ struct emitter_receiver { */ template <> struct emitter_receiver { - static void emit(py::object& data, const std::string& name, PyOutputContext& op_output) { - op_output.emit(nullptr, name.c_str()); + static void emit(py::object& data, const std::string& name, PyOutputContext& op_output, + const int64_t acq_timestamp = -1) { + op_output.emit(nullptr, name.c_str(), acq_timestamp); return; } static py::object receive(std::any result, const std::string& name, PyInputContext& op_input) { diff --git a/python/holoscan/core/execution_context.cpp b/python/holoscan/core/execution_context.cpp index 7c5770cd..224b178b 100644 --- a/python/holoscan/core/execution_context.cpp +++ b/python/holoscan/core/execution_context.cpp @@ -20,6 +20,7 @@ #include #include +#include #include "execution_context_pydoc.hpp" #include "holoscan/core/execution_context.hpp" @@ -45,7 +46,7 @@ PyExecutionContext::PyExecutionContext(gxf_context_t context, std::shared_ptr& py_output_context, py::object op) : gxf::GXFExecutionContext(context, py_input_context, py_output_context), - py_op_(op), + py_op_(std::move(op)), py_input_context_(py_input_context), py_output_context_(py_output_context) {} diff --git a/python/holoscan/core/fragment.cpp b/python/holoscan/core/fragment.cpp index 40668149..e6fca4dc 100644 --- a/python/holoscan/core/fragment.cpp +++ b/python/holoscan/core/fragment.cpp @@ -76,10 +76,10 @@ void init_fragment(py::module_& m) { "prefix"_a = "", doc::Fragment::doc_config_kwargs) .def("config", py::overload_cast&>(&Fragment::config)) - .def("config", py::overload_cast<>(&Fragment::config)) + .def("config", &Fragment::config_shared) .def("config_keys", &Fragment::config_keys, doc::Fragment::doc_config_keys) - .def_property_readonly("graph", &Fragment::graph, doc::Fragment::doc_graph) - .def_property_readonly("executor", &Fragment::executor, doc::Fragment::doc_executor) + .def_property_readonly("graph", &Fragment::graph_shared, doc::Fragment::doc_graph) + .def_property_readonly("executor", &Fragment::executor_shared, doc::Fragment::doc_executor) .def( "from_config", [](Fragment& fragment, const std::string& key) { diff --git a/python/holoscan/core/gil_guarded_pyobject.hpp b/python/holoscan/core/gil_guarded_pyobject.hpp index 3dd75695..5c968e56 100644 --- a/python/holoscan/core/gil_guarded_pyobject.hpp +++ b/python/holoscan/core/gil_guarded_pyobject.hpp @@ -20,6 +20,8 @@ #include +#include + namespace py = pybind11; namespace holoscan { @@ -45,9 +47,25 @@ class GILGuardedPyObject { ~GILGuardedPyObject() { // Acquire GIL before destroying the PyObject - py::gil_scoped_acquire scope_guard; - py::handle handle = obj_.release(); - if (handle) { handle.dec_ref(); } + try { + py::gil_scoped_acquire scope_guard; + py::handle handle = obj_.release(); + if (handle) { handle.dec_ref(); } + } catch (py::error_already_set& eas) { + // Discard any Python error using Python APIs + // https://pybind11.readthedocs.io/en/stable/advanced/exceptions.html#handling-unraisable-exceptions + try { + // ignore potential runtime_error from release() call internal to discard_as_unraisable + eas.discard_as_unraisable(__func__); + } catch (...) {} + } catch (const std::exception& e) { + // catch and print info on any C++ exception raised in the destructor + try { + HOLOSCAN_LOG_ERROR("error in ~GILGuardedPyObject: {}", e.what()); + } catch (...) { + // ignore any fmt::format exception thrown by HOLOSCAN_LOG_ERROR + } + } } private: diff --git a/python/holoscan/core/io_context.cpp b/python/holoscan/core/io_context.cpp index 9c6ff5d9..2c32e7b0 100644 --- a/python/holoscan/core/io_context.cpp +++ b/python/holoscan/core/io_context.cpp @@ -152,7 +152,7 @@ py::object PyInputContext::py_receive(const std::string& name) { } else { auto maybe_result = receive(name.c_str()); if (!maybe_result.has_value()) { - HOLOSCAN_LOG_ERROR("Unable to receive input (std::any) with name '{}'", name); + HOLOSCAN_LOG_DEBUG("Unable to receive input (std::any) with name '{}'", name); return py::none(); } auto result = maybe_result.value(); @@ -164,7 +164,7 @@ py::object PyInputContext::py_receive(const std::string& name) { } void PyOutputContext::py_emit(py::object& data, const std::string& name, - const std::string& emitter_name) { + const std::string& emitter_name, int64_t acq_timestamp) { // Note:: Issue 4206197 // In the UcxTransmitter::sync_io_abi(), while popping an entity from the queue, // Runtime::GxfEntityRefCountDec() on the entity can be called (which locks 'ref_count_mutex_'). @@ -188,7 +188,7 @@ void PyOutputContext::py_emit(py::object& data, const std::string& name, if (!emitter_name.empty()) { HOLOSCAN_LOG_DEBUG("py_emit: emitting a {}", emitter_name); const auto& emit_func = registry.get_emitter(emitter_name); - emit_func(data, name, *this); + emit_func(data, name, *this, acq_timestamp); return; } @@ -196,7 +196,7 @@ void PyOutputContext::py_emit(py::object& data, const std::string& name, if (py::isinstance(data)) { HOLOSCAN_LOG_DEBUG("py_emit: emitting a holoscan::PyEntity"); const auto& emit_func = registry.get_emitter(typeid(holoscan::PyEntity)); - emit_func(data, name, *this); + emit_func(data, name, *this, acq_timestamp); return; } @@ -211,7 +211,7 @@ void PyOutputContext::py_emit(py::object& data, const std::string& name, "py_emit: emitting a std::vector object"); const auto& emit_func = registry.get_emitter(typeid(std::vector)); - emit_func(data, name, *this); + emit_func(data, name, *this, acq_timestamp); return; } } @@ -220,7 +220,7 @@ void PyOutputContext::py_emit(py::object& data, const std::string& name, // handle pybind11::dict separately from other Python types for special TensorMap treatment if (py::isinstance(data)) { const auto& emit_func = registry.get_emitter(typeid(pybind11::dict)); - emit_func(data, name, *this); + emit_func(data, name, *this, acq_timestamp); return; } @@ -259,7 +259,7 @@ void PyOutputContext::py_emit(py::object& data, const std::string& name, if (is_distributed_app && is_tensor_like(data)) { HOLOSCAN_LOG_DEBUG("py_emit: emitting a tensor-like object over a UCX connector"); const auto& emit_func = registry.get_emitter(typeid(holoscan::Tensor)); - emit_func(data, name, *this); + emit_func(data, name, *this, acq_timestamp); return; } @@ -271,7 +271,7 @@ void PyOutputContext::py_emit(py::object& data, const std::string& name, // broadcast codelet was inserted. HOLOSCAN_LOG_DEBUG("py_emit: emitting a std::shared_ptr"); const auto& emit_func = registry.get_emitter(typeid(std::shared_ptr)); - emit_func(data, name, *this); + emit_func(data, name, *this, acq_timestamp); return; } @@ -303,7 +303,12 @@ void init_io_context(py::module_& m) { py::class_>( m, "PyOutputContext", R"doc(Output context class.)doc") - .def("emit", &PyOutputContext::py_emit, "data"_a, "name"_a, "emitter_name"_a = ""); + .def("emit", + &PyOutputContext::py_emit, + "data"_a, + "name"_a, + "emitter_name"_a = "", + "acq_timestamp"_a = -1); // register a cloudpickle-based serializer for Python objects register_py_object_codec(); @@ -352,11 +357,13 @@ void init_io_context(py::module_& m) { PyInputContext::PyInputContext(ExecutionContext* execution_context, Operator* op, std::unordered_map>& inputs, py::object py_op) - : gxf::GXFInputContext::GXFInputContext(execution_context, op, inputs), py_op_(py_op) {} + : gxf::GXFInputContext::GXFInputContext(execution_context, op, inputs), + py_op_(std::move(py_op)) {} PyOutputContext::PyOutputContext(ExecutionContext* execution_context, Operator* op, std::unordered_map>& outputs, py::object py_op) - : gxf::GXFOutputContext::GXFOutputContext(execution_context, op, outputs), py_op_(py_op) {} + : gxf::GXFOutputContext::GXFOutputContext(execution_context, op, outputs), + py_op_(std::move(py_op)) {} } // namespace holoscan diff --git a/python/holoscan/core/io_context.hpp b/python/holoscan/core/io_context.hpp index f72d490b..db6f2ff4 100644 --- a/python/holoscan/core/io_context.hpp +++ b/python/holoscan/core/io_context.hpp @@ -66,7 +66,8 @@ class PyOutputContext : public gxf::GXFOutputContext { std::unordered_map>& outputs, py::object py_op); - void py_emit(py::object& data, const std::string& name, const std::string& emitter_name = ""); + void py_emit(py::object& data, const std::string& name, const std::string& emitter_name = "", + const int64_t acq_timestamp = -1); private: py::object py_op_ = py::none(); diff --git a/python/holoscan/core/io_spec.cpp b/python/holoscan/core/io_spec.cpp index 5d92068e..6e4e4a0c 100644 --- a/python/holoscan/core/io_spec.cpp +++ b/python/holoscan/core/io_spec.cpp @@ -22,6 +22,7 @@ #include #include +#include #include "holoscan/core/condition.hpp" #include "holoscan/core/io_spec.hpp" @@ -56,31 +57,38 @@ void init_io_spec(py::module_& m) { "name"_a, "io_type"_a, doc::IOSpec::doc_IOSpec) - .def_property_readonly("name", &IOSpec::name, doc::IOSpec::doc_name) + .def_property_readonly( + "name", &IOSpec::name, doc::IOSpec::doc_name, py::return_value_policy::reference_internal) .def_property_readonly("io_type", &IOSpec::io_type, doc::IOSpec::doc_io_type) .def_property_readonly( "connector_type", &IOSpec::connector_type, doc::IOSpec::doc_connector_type) - .def_property_readonly("conditions", &IOSpec::conditions, doc::IOSpec::doc_conditions) + .def_property_readonly("conditions", + &IOSpec::conditions, + doc::IOSpec::doc_conditions, + py::return_value_policy::reference_internal) .def( "condition", - [](IOSpec& io_spec, const ConditionType& kind, const py::kwargs& kwargs) { + // Note: The return type needs to be specified explicitly because pybind11 can't deduce it + [](IOSpec& io_spec, const ConditionType& kind, const py::kwargs& kwargs) -> IOSpec& { return io_spec.condition(kind, kwargs_to_arglist(kwargs)); }, - doc::IOSpec::doc_condition) + doc::IOSpec::doc_condition, + py::return_value_policy::reference_internal) // TODO: sphinx API doc build complains if more than one connector // method has a docstring specified. For now just set the docstring for the // first overload only and add information about the rest in the Notes section. .def( "connector", - [](IOSpec& io_spec, const IOSpec::ConnectorType& kind, const py::kwargs& kwargs) { - return io_spec.connector(kind, kwargs_to_arglist(kwargs)); - }, - doc::IOSpec::doc_connector) + // Note: The return type needs to be specified explicitly because pybind11 can't deduce it + [](IOSpec& io_spec, const IOSpec::ConnectorType& kind, const py::kwargs& kwargs) + -> IOSpec& { return io_spec.connector(kind, kwargs_to_arglist(kwargs)); }, + doc::IOSpec::doc_connector, + py::return_value_policy::reference_internal) // using lambdas for overloaded connector methods because py::overload_cast didn't work .def("connector", [](IOSpec& io_spec) { return io_spec.connector(); }) .def("connector", [](IOSpec& io_spec, std::shared_ptr connector) { - return io_spec.connector(connector); + return io_spec.connector(std::move(connector)); }) .def("__repr__", // use py::object and obj.cast to avoid a segfault if object has not been initialized diff --git a/python/holoscan/core/kwarg_handling.cpp b/python/holoscan/core/kwarg_handling.cpp index e7271461..0232a34c 100644 --- a/python/holoscan/core/kwarg_handling.cpp +++ b/python/holoscan/core/kwarg_handling.cpp @@ -29,7 +29,6 @@ #include "holoscan/core/condition.hpp" #include "holoscan/core/io_spec.hpp" #include "holoscan/core/resource.hpp" -#include "holoscan/operators/aja_source/ntv2channel.hpp" #include "kwarg_handling.hpp" #include "kwarg_handling_pydoc.hpp" @@ -106,7 +105,7 @@ void set_vector_arg_via_numpy_array(const py::array& obj, Arg& out) { out = yaml_node; } else if (obj.attr("ndim").cast() == 2) { YAML::Node yaml_node = YAML::Load("[]"); // Create an empty sequence - for (auto item : obj) { + for (const auto& item : obj) { YAML::Node inner_yaml_node = YAML::Load("[]"); // Create an empty sequence for (const auto& inner_item : item) { inner_yaml_node.push_back(cast_to_yaml_node(inner_item)); @@ -131,10 +130,10 @@ void set_vector_arg_via_py_sequence(const py::sequence& seq, Arg& out) { // Handle list of list and other sequence of sequence types. std::vector> v; v.reserve(static_cast(py::len(seq))); - for (auto item : seq) { + for (const auto& item : seq) { std::vector vv; vv.reserve(static_cast(py::len(item))); - for (auto inner_item : item) { vv.push_back(inner_item.cast()); } + for (const auto& inner_item : item) { vv.push_back(inner_item.cast()); } v.push_back(vv); } out = v; @@ -143,7 +142,7 @@ void set_vector_arg_via_py_sequence(const py::sequence& seq, Arg& out) { std::vector v; size_t length = py::len(seq); v.reserve(length); - for (auto item : seq) v.push_back(item.cast()); + for (const auto& item : seq) v.push_back(item.cast()); out = v; } } else { @@ -151,7 +150,7 @@ void set_vector_arg_via_py_sequence(const py::sequence& seq, Arg& out) { if (py::isinstance(first_item) && !py::isinstance(first_item)) { // Handle list of list and other sequence of sequence types. YAML::Node yaml_node = YAML::Load("[]"); // Create an empty sequence - for (auto item : seq) { + for (const auto& item : seq) { YAML::Node inner_yaml_node = YAML::Load("[]"); // Create an empty sequence for (const auto& inner_item : item) { inner_yaml_node.push_back(cast_to_yaml_node(inner_item)); @@ -266,11 +265,11 @@ py::object vector_arg_to_py_object(Arg& arg) { py::object yaml_node_to_py_object(YAML::Node node) { if (node.IsSequence()) { py::list list; - for (auto item : node) { list.append(yaml_node_to_py_object(item)); } + for (const auto& item : node) { list.append(yaml_node_to_py_object(item)); } return list; } else if (node.IsMap()) { py::dict dict; - for (auto item : node) { + for (const auto& item : node) { dict[py::str(item.first.as())] = yaml_node_to_py_object(item.second); } return dict; @@ -294,10 +293,6 @@ py::object yaml_node_to_py_object(YAML::Node node) { } // Check if it is a string. { - // special case for string -> AJASourceOp NTV2Channel enum - NTV2Channel aja_t; - if (YAML::convert::decode(node, aja_t)) { return py::cast(aja_t); } - std::string t; if (YAML::convert::decode(node, t)) { return py::str(t); } } @@ -428,7 +423,7 @@ ArgList kwargs_to_arglist(const py::kwargs& kwargs) { // There is currently no option to choose conversion to kArray instead of kNative. ArgList arglist; if (kwargs) { - for (auto& [name, handle] : kwargs) { + for (const auto& [name, handle] : kwargs) { arglist.add(py_object_to_arg(handle.cast(), name.cast())); } /// .. do something with kwargs diff --git a/python/holoscan/core/operator.cpp b/python/holoscan/core/operator.cpp index efb34d3e..e417139a 100644 --- a/python/holoscan/core/operator.cpp +++ b/python/holoscan/core/operator.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include "gil_guarded_pyobject.hpp" @@ -124,6 +125,16 @@ void init_operator(py::module_& m) { .def_property_readonly("resources", &Operator::resources, doc::Operator::doc_resources) .def_property_readonly( "operator_type", &Operator::operator_type, doc::Operator::doc_operator_type) + .def( + "resource", + [](Operator& op, const py::str& name) -> std::optional { + auto resources = op.resources(); + auto res = resources.find(name); + if (res == resources.end()) { return py::none(); } + return py::cast(res->second); + }, + "name"_a, + doc::Operator::doc_resource) .def("add_arg", py::overload_cast(&Operator::add_arg), "arg"_a, @@ -182,7 +193,7 @@ void init_operator(py::module_& m) { } PyOperatorSpec::PyOperatorSpec(Fragment* fragment, py::object op) - : OperatorSpec(fragment), py_op_(op) {} + : OperatorSpec(fragment), py_op_(std::move(op)) {} void PyOperatorSpec::py_param(const std::string& name, const py::object& default_value, const ParameterFlag& flag, const py::kwargs& kwargs) { @@ -191,8 +202,8 @@ void PyOperatorSpec::py_param(const std::string& name, const py::object& default bool is_receivers = false; std::string headline{""s}; std::string description{""s}; - for (const auto& [name, value] : kwargs) { - std::string param_name = name.cast(); + for (const auto& [kw_name, value] : kwargs) { + std::string param_name = kw_name.cast(); if (param_name == "headline") { headline = value.cast(); } else if (param_name == "description") { diff --git a/python/holoscan/core/operator_pydoc.hpp b/python/holoscan/core/operator_pydoc.hpp index 4706d8b3..aaee65f0 100644 --- a/python/holoscan/core/operator_pydoc.hpp +++ b/python/holoscan/core/operator_pydoc.hpp @@ -189,6 +189,20 @@ PYDOC(resources, R"doc( Resources associated with the operator. )doc") +PYDOC(resource, R"doc( +Resources associated with the operator. + +Parameters +---------- +name : str +The name of the resource to retrieve + +Returns +------- +holoscan.core.Resource or None + The resource with the given name. If no resource with the given name is found, None is returned. +)doc") + PYDOC(add_arg_Arg, R"doc( Add an argument to the component. )doc") diff --git a/python/holoscan/core/resource.cpp b/python/holoscan/core/resource.cpp index 61426008..0d744162 100644 --- a/python/holoscan/core/resource.cpp +++ b/python/holoscan/core/resource.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +21,7 @@ #include #include +#include "component.hpp" #include "holoscan/core/arg.hpp" #include "holoscan/core/component.hpp" #include "holoscan/core/component_spec.hpp" @@ -42,9 +43,14 @@ class PyResource : public Resource { // Define a kwargs-based constructor that can create an ArgList // for passing on to the variadic-template based constructor. - PyResource(const py::args& args, const py::kwargs& kwargs) : Resource() { + PyResource(py::object resource, Fragment* fragment, const py::args& args, + const py::kwargs& kwargs) + : Resource() { using std::string_literals::operator""s; + py_resource_ = resource; + fragment_ = fragment; + int n_fragments = 0; for (auto& item : args) { py::object arg_value = item.cast(); @@ -81,6 +87,12 @@ class PyResource : public Resource { } } + // Override spec() method + std::shared_ptr py_shared_spec() { + auto spec_ptr = spec_shared(); + return std::static_pointer_cast(spec_ptr); + } + /* Trampolines (need one for each virtual function) */ void initialize() override { /* , , , */ @@ -90,12 +102,19 @@ class PyResource : public Resource { /* , , , */ PYBIND11_OVERRIDE(void, Resource, setup, spec); } + + private: + py::object py_resource_ = py::none(); }; void init_resource(py::module_& m) { - py::class_>( - m, "Resource", doc::Resource::doc_Resource) - .def(py::init(), doc::Resource::doc_Resource_args_kwargs) + // note: added py::dynamic_attr() to allow dynamically adding attributes in a Python subclass + py::class_> resource_class( + m, "Resource", py::dynamic_attr(), doc::Resource::doc_Resource_args_kwargs); + + resource_class + .def(py::init(), + doc::Resource::doc_Resource_args_kwargs) .def_property("name", py::overload_cast<>(&Resource::name, py::const_), (Resource & (Resource::*)(const std::string&)&)&Resource::name, @@ -110,6 +129,8 @@ void init_resource(py::module_& m) { &Resource::initialize, doc::Resource::doc_initialize) // note: virtual function .def_property_readonly("description", &Resource::description, doc::Resource::doc_description) + .def_property_readonly( + "resource_type", &Resource::resource_type, doc::Resource::doc_resource_type) .def( "__repr__", [](const py::object& obj) { @@ -119,6 +140,10 @@ void init_resource(py::module_& m) { return std::string(""); }, R"doc(Return repr(self).)doc"); + + py::enum_(resource_class, "ResourceType") + .value("NATIVE", Resource::ResourceType::kNative) + .value("GXF", Resource::ResourceType::kGXF); } } // namespace holoscan diff --git a/python/holoscan/core/resource_pydoc.hpp b/python/holoscan/core/resource_pydoc.hpp index c259d520..8edaf080 100644 --- a/python/holoscan/core/resource_pydoc.hpp +++ b/python/holoscan/core/resource_pydoc.hpp @@ -95,6 +95,13 @@ PYDOC(description, R"doc( YAML formatted string describing the resource. )doc") +PYDOC(resource_type, R"doc( +Resource type. + +`holoscan.core.Resource.ResourceType` enum representing the type of +the operator. The two types currently implemented are NATIVE and GXF. +)doc") + } // namespace Resource } // namespace holoscan::doc diff --git a/python/holoscan/core/tensor.cpp b/python/holoscan/core/tensor.cpp index 9eb79fd1..9e7e403d 100644 --- a/python/holoscan/core/tensor.cpp +++ b/python/holoscan/core/tensor.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include "dl_converter.hpp" @@ -212,7 +213,9 @@ LazyDLManagedTensorDeleter::LazyDLManagedTensorDeleter() { } LazyDLManagedTensorDeleter::~LazyDLManagedTensorDeleter() { - release(); + try { + release(); + } catch (const std::exception& e) {} // ignore potential fmt::v8::format_error } void LazyDLManagedTensorDeleter::add(DLManagedTensor* dl_managed_tensor_ptr) { @@ -283,7 +286,10 @@ void LazyDLManagedTensorDeleter::release() { std::this_thread::yield(); } HOLOSCAN_LOG_DEBUG("LazyDLManagedTensorDeleter thread stopped"); - s_stop = false; + { + std::lock_guard lock(s_mutex); + s_stop = false; + } } } @@ -594,7 +600,7 @@ py::capsule PyTensor::dlpack(const py::object& obj, py::object stream) { // Do not copy 'obj' or a shared pointer here in the lambda expression's initializer, otherwise // the refcount of it will be increased by 1 and prevent the object from being destructed. Use a // raw pointer here instead. - return py_dlpack(tensor.get(), stream); + return py_dlpack(tensor.get(), std::move(stream)); } py::tuple PyTensor::dlpack_device(const py::object& obj) { diff --git a/python/holoscan/decorator.py b/python/holoscan/decorator.py new file mode 100644 index 00000000..f16e61fa --- /dev/null +++ b/python/holoscan/decorator.py @@ -0,0 +1,577 @@ +""" + SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + SPDX-License-Identifier: Apache-2.0 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" # noqa: E501 + +import ast +import inspect +import textwrap +from dataclasses import dataclass, field + +# Need Python 3.9 to use builtin tuple and dict directly instead of typing.Tuple, typing.Dict +from typing import Any, Dict, Optional, Tuple, Union + +import cupy as cp +import numpy as np + +from holoscan.conditions import BooleanCondition, CountCondition +from holoscan.core import ( + Condition, + ConditionType, + Fragment, + IOSpec, + Operator, + OperatorSpec, + Resource, +) +from holoscan.core._core import Fragment as FragmentBase +from holoscan.core._core import Tensor as TensorBase + +__all__ = ["Input", "Output", "create_op"] + + +def _is_tensor_like(obj): + if ( + (hasattr(obj, "__dlpack__") and hasattr(obj, "__dlpack_device__")) + or hasattr(obj, "__cuda_array_interface__") + or hasattr(obj, "__array_interface__") + ): + return True + return False + + +def _as_python_tensor(tensor): + if hasattr(tensor, "__array_interface__") or ( + hasattr(tensor, "__dlpack_device__") and tensor.__dlpack_device__()[0] == 1 + ): + return np.asarray(tensor) + else: + return cp.asarray(tensor) + + +@dataclass +class Input: + """Class for specifying an input port and how the received value maps to a function's arguments. + + Parameters + ---------- + name : str + The name of the input port. + arg_map: str or dict[str, str] + If `arg_map` is a str, the Python object received by the input port is passed to the + function argument specified by `arg_map`. If `arg_map` is a dict, the input is assumed to be + a TensorMap (dictionary of tensors). In this case the keys of the dict are the tensor names + and the values are the names of the function arguments that the tensors map to. + condition_type : holoscan.core.ConditionType, optional + The condition type for the input port. + condition_kwargs : dict[str, Any], optional + The keywords passed onto the condition specified by `condition_type`. + connector_type : holoscan.core.IOSpec.ConnectorType, optional + The connector type for the input port. + connector_kwargs : dict[str, Any], optional + The keywords passed onto the connector specified by `connector_type`. + """ + + name: str + arg_map: Optional[Union[str, dict[str, str]]] = () + condition_type: Optional[ConditionType] = None + condition_kwargs: Dict[str, Any] = field(default_factory=dict) + connector_type: Optional[IOSpec.ConnectorType] = None + connector_kwargs: Dict[str, Any] = field(default_factory=dict) + + def create_input(self, spec: OperatorSpec) -> IOSpec: + iospec = spec.input(self.name) + if self.condition_type is not None: + iospec = iospec.condition(self.condition_type, **self.condition_kwargs) + if self.connector_type is not None: + iospec = iospec.connector(self.connector_type, **self.connector_kwargs) + + +@dataclass +class Output: + """Class for specifying an output port and how the received value maps to a function's + arguments. + + Parameters + ---------- + name : str + The name of the input port. + tensor_names: tuple(str) or None + If None, whatever Python object the func outputs is emitted on the output port. If a tuple + of strings is provided it is assumed that the func returns a dictionary of tensors. The + names in the tuple specify which tensors in the dict will be transmitted on the output + port. There is no need to specify `tensor_names` if all tensors in a dict returned by the + function are to be transmitted. + condition_type : holoscan.core.ConditionType, optional + The condition type for the input port. + condition_kwargs : dict[str, Any], optional + The keywords passed onto the condition specified by `condition_type`. + connector_type : holoscan.core.IOSpec.ConnectorType, optional + The connector type for the input port. + connector_kwargs : dict[str, Any], optional + The keywords passed onto the connector specified by `connector_type`. + """ + + name: str + tensor_names: Optional[Tuple[str]] = () + condition_type: Optional[ConditionType] = None + condition_kwargs: Dict[str, Any] = field(default_factory=dict) + connector_type: Optional[IOSpec.ConnectorType] = None + connector_kwargs: Dict[str, Any] = field(default_factory=dict) + + def create_output(self, spec: OperatorSpec) -> IOSpec: + iospec = spec.output(self.name) + if self.condition_type is not None: + iospec = iospec.condition(self.condition_type, **self.condition_kwargs) + if self.connector_type is not None: + iospec = iospec.connector(self.connector_type, **self.connector_kwargs) + + +def _as_input(input_: Union[str, Input]): + """Cast str to Output object.""" + if isinstance(input_, str): + return Input(input_, arg_map=input_) + elif not isinstance(input_, Input): + return ValueError("`inputs` must be a single port name or Input object or a tuple of these") + return input_ + + +def _as_output(output: Union[str, Output]): + """Cast str to Output object.""" + if isinstance(output, str): + return Output(output) + elif not isinstance(output, Output): + return ValueError( + "`outputs` must be a single port name or Output object or a tuple of these" + ) + return output + + +def _has_function_returns_value(func): + """Check if the provided function has any return statements returning a value.""" + + class ReturnVisitor(ast.NodeVisitor): + def __init__(self): + self.returns_value = False + + def visit_Return(self, node): # noqa: N802 + # check if the return statement has a value + if node.value is not None: + self.returns_value = True + return + + self.generic_visit(node) + + def visit_ClassDef(self, node): # noqa: N802 + return + + def visit_FunctionDef(self, node): # noqa: N802 + return + + def visit_AsyncFunctionDef(self, node): # noqa: N802 + return + + def visit(self, node): + if self.returns_value: + return + super().visit(node) + + # parse the source code into an AST + source_code = inspect.getsource(func) + # deindent the text if it is indented + source_code = textwrap.dedent(source_code) + tree = ast.parse(source_code) + # initialize the visitor + visitor = ReturnVisitor() + # walk the AST + for node in ast.walk(tree): + if isinstance(node, ast.FunctionDef) and node.name == func.__name__: + visitor.generic_visit(node) + break + return visitor.returns_value + + +def create_op( + function_or_class=None, + inputs: Union[str, Input, Tuple[Union[str, Input]]] = (), + outputs: Union[str, Output, Tuple[Union[str, Output]]] = (), + cast_tensors=True, +): + """Decorator for creating an operator from a function or a class. + + When the decorator is used on a class, the class must have a `__call__` method that will be + used as the operator function. + + inputs : str, Input, or Tuple[str | Input], optional + If a str is provided, it is assumed to be the name of the input port and that the function + has a variable matching that port name to which the object received on the port will be + connected. If the port name does not match the name of the variable in the function + signature, or if there are multiple tensors to be mapped to multiple objects, use an Input + argument. A tuple of str or Input objects can be provided to specify multiple input ports. + The default of an empty tuple corresponds to no input ports. + outputs : str, Output, or Tuple[str | Output], optional + If a str is provided, any value returned by the function will be emitted on an output port + of that name. Otherwise, an Output object can be provided in the case that the function + returns multiple outputs that should be split across multiple ports. + cast_tensors : bool, optional + If True, automatically cast any tensor-like input to a NumPy or CuPy array (for host and + device tensors, respectively). If set to False, these will be left as `holoscan.Tensor` and + the user will have to cast to the desired type within the body of the decorated function or + class. + + Notes + ----- + Another case where using `Input` or `Output` objects is necessary is if the user wishes to + override the default connector or condition types for the port. + """ + # used to store the class object if the decorator is used on a class + class_obj = None + # used to determine if the decorator was used without args + is_without_args = function_or_class is not None + + # convert scalars to tuple + if isinstance(inputs, (str, Input)): + inputs = (inputs,) + # convert any str in the tuple to an Input object + inputs = tuple(_as_input(i) for i in inputs) + + if isinstance(outputs, (str, Output)): + outputs = (outputs,) + # convert any str in the tuple to an Output object + outputs = tuple(_as_output(o) for o in outputs) + + if not isinstance(outputs, tuple): + raise ValueError( + "`outputs` must be a single port name or Output object or a tuple of these" + ) + + def decorator(func_or_cls): + nonlocal function_or_class, class_obj + + def make_class(*args, **kwargs): + if "fragment" in kwargs: + fragment = kwargs.pop("fragment") + elif len(args) and isinstance(args[0], FragmentBase): + fragment, args = args[0], args[1:] + else: + raise ValueError( + "fragment must be provided via kwarg or as the first positional argument" + ) + + # frame = inspect.currentframe() + # args_names, _, _, locals_dict = inspect.getargvalues(frame) + # print(f"{args_names=}, {locals_dict=}") + + class DynamicOp(Operator): + def __init__( + self, + fragment: FragmentBase, + *args, + inputs, + outputs, + cast_tensors=cast_tensors, + **kwargs, + ): + self.func = func_or_cls + self.input_objs = inputs + self.output_objs = outputs + self.is_generator = inspect.isgeneratorfunction(self.func) + self.gen_obj = None + self.cast_tensors = cast_tensors + + # remove conditions and resources from *args + condition_args = tuple(a for a in args if isinstance(a, Condition)) + resource_args = tuple(a for a in args if isinstance(a, Resource)) + args = tuple(a for a in args if not isinstance(a, (Condition, Resource))) + self.func_args = args + + # add a boolean condition to prevent triggering if the function is a generator + # and the iteration is complete + if self.is_generator: + condition_args = condition_args + ( + BooleanCondition(fragment, name="_generator_func"), + ) + + # set name kwarg to self.func.__name__ if not provided + name = kwargs.pop("name", self.func.__name__) + + argspec = inspect.getfullargspec(self.func) + + # remove self from argspec.args if the decorator is used on a class + if class_obj: + argspec = argspec._replace(args=argspec.args[1:]) + self.class_obj = class_obj + + self.func_argspec = argspec + + # populate inputs and outputs with defaults if decorator was used without args + if is_without_args: + self.input_objs = tuple(Input(name, arg_map=name) for name in argspec.args) + # configure the output port if the function contains return statements + # (in this case, the port name will be left empty) + if _has_function_returns_value(function_or_class): + self.output_objs = tuple((Output(""),)) + + # populate all arguments not provided with defaults + if argspec.kwonlydefaults is not None: + for k in argspec.kwonlyargs: + if k not in kwargs and k in argspec.kwonlydefaults: + kwargs[k] = argspec.kwonlydefaults[k] + + # store a list of what ports map to what function arguments + self.input_mappings = {} + for input_obj in self.input_objs: + # store what argument(s) this input maps to + self.input_mappings[input_obj.name] = input_obj.arg_map + + # sets self.dynamic_kwargs and self.fixed_kwargs + self._set_fixed_and_dynamic_kwargs(kwargs) + + # get the type annotations dict for the function (not currently used) + # self.func_annotations = inspect.get_annotations(self.func) + self.func_annotations = self.func.__annotations__ + + super().__init__(fragment, *condition_args, *resource_args, name=name) + + def _set_fixed_and_dynamic_kwargs(self, kwargs): + """Split provided kwargs into those which are "fixed" and those which are + "dynamic". + + Here "dynamic" refers to function arguments that are obtained from input + ports. The keys for self.dynamic_kwargs are determined here, but the values + are initialized to None. Actual values get set during each `compute` call. + + "fixed" refers to other keyword arguments to the function that don't change + across calls. + """ + self.dynamic_kwargs = {} + for input_map in self.input_mappings.values(): + if isinstance(input_map, str): + self._add_dynamic_arg(input_map, kwargs) + elif isinstance(input_map, dict): + for arg_name in input_map.values(): + self._add_dynamic_arg(arg_name, kwargs) + self.fixed_kwargs = kwargs + + # store any positional args with specified defaults in fixed_kwargs instead + argspec = self.func_argspec + if argspec.defaults is not None: + n_default_positional = len(argspec.defaults) + if n_default_positional > 0: + self.func_args = self.func_args[:-n_default_positional] + n_required_positional = len(argspec.args) - len(argspec.defaults) + for k, v in zip(argspec.args[n_required_positional:], argspec.defaults): + # don't overwrite any kwargs that were provided + if k not in self.fixed_kwargs: + self.fixed_kwargs[k] = v + + # Now that all args with defaults are in self.fixed_kwargs we can check if any + # of the required arguments were not specified + required_args = set(argspec.args) | set(argspec.kwonlyargs) + if argspec.kwonlydefaults is not None: + required_args -= set(argspec.kwonlydefaults.keys()) + for arg in required_args: + if arg not in self.fixed_kwargs and arg not in self.dynamic_kwargs: + raise ValueError(f"required argument, '{arg}', has not been specified") + + def _add_dynamic_arg(self, arg_name, kwargs): + """helper function for _set_fixed_and_dynamic_kwargs""" + if arg_name in self.dynamic_kwargs: + raise ValueError( + "duplicate specification of mapping to function kwarg: '{arg_name}'" + ) + self.dynamic_kwargs[arg_name] = None + try: + kwargs.pop(arg_name) + except KeyError as e: + argspec = self.func_argspec + if arg_name not in argspec.kwonlyargs + argspec.args: + msg = ( + f"Provided func does not have an arg or kwarg named '{arg_name}'." + " The provided wrapped function has" + f" positional args: {argspec.args}" + f" and keyword-only args: {argspec.kwonlyargs}" + ) + raise KeyError(msg) from e + return + + # # not used by the Application, but can be useful to test the call + # def __call__(self, *args, **kwargs): + # print(f"{self.msg=}") + # return self.func(*self.func_args, *args, **self.fixed_kwargs, **kwargs) + + def setup(self, spec: OperatorSpec): + for input_obj in self.input_objs: + input_obj.create_input(spec) + + self.output_tensor_map = {} + for output_obj in self.output_objs: + output_obj.create_output(spec) + self.output_tensor_map[output_obj.name] = output_obj.tensor_names + + def compute(self, op_input, op_output, context): + for port_name, arg_map in self.input_mappings.items(): + print(f"input {port_name=}, {arg_map=}") + msg = op_input.receive(port_name) + if isinstance(arg_map, str): + # print(f"{msg=}") + if isinstance(msg, dict): + try: + # try tensor based on matching name + msg = msg[arg_map] + except KeyError as e: + # use tensor regardless of name if only one is present + tensors = tuple( + v for k, v in msg.items() if isinstance(v, TensorBase) + ) + if len(tensors) == 1: + msg = tensors[0] + elif len(tensors) > 1: + raise ValueError( + "More than one tensor found in port, but none has " + f"name {arg_map}" + ) from e + + # cast holoscan.Tensor to cp.asarray(Tensor) here or require the user + # to do it in the provided func? + if self.cast_tensors and isinstance(msg, TensorBase): + msg = _as_python_tensor(msg) + + self.dynamic_kwargs[arg_map] = msg + elif isinstance(arg_map, dict): + for tensor_name, arg_name in arg_map.items(): + try: + val = msg[tensor_name] + except KeyError as e: + raise KeyError( + f"key with name '{tensor_name}' not found in input dict" + ) from e + if self.cast_tensors and isinstance(val, TensorBase): + val = _as_python_tensor(val) + self.dynamic_kwargs[arg_name] = val + + if self.is_generator: + if self.gen_obj is None: + out = self.func( + *self.func_args, **self.fixed_kwargs, **self.dynamic_kwargs + ) + self.gen_obj = out + try: + out = next(self.gen_obj) + except StopIteration: + # disable the condition to prevent further calls + self.conditions["_generator_func"].disable_tick() + return + else: + out = self.func(*self.func_args, **self.fixed_kwargs, **self.dynamic_kwargs) + + for port_name, tensor_names in self.output_tensor_map.items(): + if tensor_names is None or len(tensor_names) == 0: + if _is_tensor_like(out): + # emit as dict of tensor-like objects + out = {"": out} + op_output.emit(out, port_name) + elif len(tensor_names) == 1: + name = tensor_names[0] + if _is_tensor_like(out): + # emit as dict of tensor-like objects + out = {name: out} + op_output.emit(out, port_name) + else: + if name not in out: + raise ValueError( + f"tensor with name '{name}' not found in function output" + ) + op_output.emit({name: out[name]}, port_name) + else: + out_tensors = {} + for name in tensor_names: + if name not in out: + raise ValueError( + f"tensor with name '{name}' not found in function output" + ) + out_tensors[name] = out[name] + # print(f"outputting tensors named: {tuple(out_tensors.keys())} on + # port {port_name}") + # print(f"tensormap emit of {out_tensors=}") + op_output.emit(out_tensors, port_name) + + op = DynamicOp(fragment, *args, inputs=inputs, outputs=outputs, **kwargs) + + def _to_camel_case(name): + """Convert name to camel case""" + parts = name.split("_") + return "".join(p.capitalize() for p in parts) + + # manually update instead of using functools.update_wrapper(op, func_or_cls) because: + # - don't want to overwrite __doc__ with func.__doc__ + # - want to use name instead of func.__name__ + if class_obj: + class_name = class_obj.__class__.__name__ + op.__name__ = class_name + "Op" if not class_name.endswith("Op") else class_name + else: + op.__name__ = _to_camel_case(func_or_cls.__name__) + "Op" + op.__qualname__ = op.__name__ + op.__module__ = func_or_cls.__module__ + return op + + def init_class(*args, **kwargs): + nonlocal class_obj, function_or_class + # create an instance of the class (using function_or_class as the class) + class_obj = function_or_class(*args, **kwargs) + # use the class's __call__ method as the operator function + if not callable(class_obj): + raise ValueError( + f"{function_or_class} must have a __call__ method to be used as an operator" + ) + function_or_class = class_obj.__call__ + return decorator(function_or_class) + + if func_or_cls is None: + return decorator + + # check if the decorator was used on a class first + if inspect.isclass(func_or_cls): # if isinstance(func_or_cls, type): + function_or_class = func_or_cls + return init_class + + if callable(func_or_cls): + return make_class + + raise Exception(f"Invalid usage of decorator for {func_or_cls}") + + return decorator(function_or_class) + + +""" +Remove example code from below this point +""" + +if False: + f = Fragment() + + @create_op( + inputs=Input("image", arg_map={"tensor": "image"}), + outputs="out", + ) + def scale_image(image: cp.ndarray, *, value: float = 1.0): + """Operator that scales an image by a specified value""" + return image * value + + # ScaleOp operator with non-default value + scale_op = scale_image(f, CountCondition(f, count=10), value=5.0, name="scale") + + assert scale_op.__name__ == "ScaleImageOp" + assert scale_op.name == "scale" diff --git a/python/holoscan/operators/aja_source/aja_source.cpp b/python/holoscan/operators/aja_source/aja_source.cpp index 5b278cdc..d5a6a5be 100644 --- a/python/holoscan/operators/aja_source/aja_source.cpp +++ b/python/holoscan/operators/aja_source/aja_source.cpp @@ -16,10 +16,13 @@ */ #include +#include #include #include #include +#include +#include #include "../operator_util.hpp" #include "./pydoc.hpp" @@ -39,6 +42,29 @@ namespace py = pybind11; namespace holoscan::ops { +namespace { + +static std::unordered_map const NTV2ChannelMapping = { + {"NTV2_CHANNEL1", NTV2Channel::NTV2_CHANNEL1}, + {"NTV2_CHANNEL2", NTV2Channel::NTV2_CHANNEL2}, + {"NTV2_CHANNEL3", NTV2Channel::NTV2_CHANNEL3}, + {"NTV2_CHANNEL4", NTV2Channel::NTV2_CHANNEL4}, + {"NTV2_CHANNEL5", NTV2Channel::NTV2_CHANNEL5}, + {"NTV2_CHANNEL6", NTV2Channel::NTV2_CHANNEL6}, + {"NTV2_CHANNEL7", NTV2Channel::NTV2_CHANNEL7}, + {"NTV2_CHANNEL8", NTV2Channel::NTV2_CHANNEL8}}; + +static NTV2Channel ToNTV2Channel(const std::string& value) { + auto it = NTV2ChannelMapping.find(value); + if (it != NTV2ChannelMapping.end()) { + return it->second; + } else { + return NTV2Channel::NTV2_CHANNEL_INVALID; + } +} + +} // namespace + /* Trampoline class for handling Python kwargs * * These add a constructor that takes a Fragment for which to initialize the operator. @@ -55,22 +81,31 @@ class PyAJASourceOp : public AJASourceOp { using AJASourceOp::AJASourceOp; // Define a constructor that fully initializes the object. - PyAJASourceOp(Fragment* fragment, const py::args& args, const std::string& device = "0"s, - NTV2Channel channel = NTV2Channel::NTV2_CHANNEL1, uint32_t width = 1920, - uint32_t height = 1080, uint32_t framerate = 60, bool rdma = false, - bool enable_overlay = false, - NTV2Channel overlay_channel = NTV2Channel::NTV2_CHANNEL2, bool overlay_rdma = true, - const std::string& name = "aja_source") + PyAJASourceOp( + Fragment* fragment, const py::args& args, const std::string& device = "0"s, + const std::variant channel = NTV2Channel::NTV2_CHANNEL1, + uint32_t width = 1920, uint32_t height = 1080, uint32_t framerate = 60, bool rdma = false, + bool enable_overlay = false, + const std::variant overlay_channel = NTV2Channel::NTV2_CHANNEL2, + bool overlay_rdma = true, const std::string& name = "aja_source") : AJASourceOp(ArgList{Arg{"device", device}, - Arg{"channel", channel}, Arg{"width", width}, Arg{"height", height}, Arg{"framerate", framerate}, Arg{"rdma", rdma}, Arg{"enable_overlay", enable_overlay}, - Arg{"overlay_channel", overlay_channel}, Arg{"overlay_rdma", overlay_rdma}}) { add_positional_condition_and_resource_args(this, args); + if (std::holds_alternative(channel)) { + this->add_arg(Arg("channel", ToNTV2Channel(std::get(channel)))); + } else { + this->add_arg(Arg("channel", std::get(channel))); + } + if (std::holds_alternative(overlay_channel)) { + this->add_arg(Arg("overlay_channel", ToNTV2Channel(std::get(overlay_channel)))); + } else { + this->add_arg(Arg("overlay_channel", std::get(overlay_channel))); + } name_ = name; fragment_ = fragment; spec_ = std::make_shared(fragment); @@ -104,13 +139,13 @@ PYBIND11_MODULE(_aja_source, m) { .def(py::init, uint32_t, uint32_t, uint32_t, bool, bool, - NTV2Channel, + const std::variant, bool, const std::string&>(), "fragment"_a, diff --git a/python/holoscan/operators/bayer_demosaic/bayer_demosaic.cpp b/python/holoscan/operators/bayer_demosaic/bayer_demosaic.cpp index d36c8604..e5694eb6 100644 --- a/python/holoscan/operators/bayer_demosaic/bayer_demosaic.cpp +++ b/python/holoscan/operators/bayer_demosaic/bayer_demosaic.cpp @@ -89,7 +89,7 @@ PYBIND11_MODULE(_bayer_demosaic, m) { py::class_>( m, "BayerDemosaicOp", doc::BayerDemosaicOp::doc_BayerDemosaicOp) - .def(py::init<>(), doc::BayerDemosaicOp::doc_BayerDemosaicOp) + .def(py::init<>()) .def(py::init, diff --git a/python/holoscan/operators/gxf_codelet/gxf_codelet.cpp b/python/holoscan/operators/gxf_codelet/gxf_codelet.cpp index 728f57f3..beea2253 100644 --- a/python/holoscan/operators/gxf_codelet/gxf_codelet.cpp +++ b/python/holoscan/operators/gxf_codelet/gxf_codelet.cpp @@ -100,7 +100,7 @@ PYBIND11_MODULE(_gxf_codelet, m) { py::class_>( m, "GXFCodeletOp", doc::GXFCodeletOp::doc_GXFCodeletOp) - .def(py::init<>(), doc::GXFCodeletOp::doc_GXFCodeletOp) + .def(py::init<>()) .def(py::init& in_tensor_names, const std::vector& out_tensor_names, bool infer_on_cpu = false, @@ -130,10 +131,19 @@ class PyInferenceOp : public InferenceOp { } py::dict temporal_map_infer = temporal_map.cast(); - for (auto& [key, value] : temporal_map_infer) { temporal_map_infer[key] = py::str(value); } + for (auto& [key, value] : temporal_map_infer) { + if (!py::isinstance(value)) { temporal_map_infer[key] = py::str(value); } + } + + py::dict activation_map_infer = activation_map.cast(); + for (auto& [key, value] : activation_map_infer) { + if (!py::isinstance(value)) { activation_map_infer[key] = py::str(value); } + } py::dict device_map_infer = device_map.cast(); - for (auto& [key, value] : device_map_infer) { device_map_infer[key] = py::str(value); } + for (auto& [key, value] : device_map_infer) { + if (!py::isinstance(value)) { device_map_infer[key] = py::str(value); } + } // convert from Python dict to InferenceOp::DataVecMap auto inference_map_datavecmap = _dict_to_inference_datavecmap(inference_map_dict); @@ -148,6 +158,9 @@ class PyInferenceOp : public InferenceOp { auto temporal_datamap = _dict_to_inference_datamap(temporal_map_infer); this->add_arg(Arg("temporal_map", temporal_datamap)); + auto activation_datamap = _dict_to_inference_datamap(activation_map_infer); + this->add_arg(Arg("activation_map", activation_datamap)); + auto backend_datamap = _dict_to_inference_datamap(backend_map.cast()); this->add_arg(Arg("backend_map", backend_datamap)); @@ -183,6 +196,7 @@ PYBIND11_MODULE(_inference, m) { py::dict, py::dict, py::dict, + py::dict, const std::vector&, const std::vector&, bool, @@ -202,6 +216,7 @@ PYBIND11_MODULE(_inference, m) { "pre_processor_map"_a, "device_map"_a = py::dict(), "temporal_map"_a = py::dict(), + "activation_map"_a = py::dict(), "backend_map"_a = py::dict(), "in_tensor_names"_a = std::vector{}, "out_tensor_names"_a = std::vector{}, diff --git a/python/holoscan/operators/inference/pydoc.hpp b/python/holoscan/operators/inference/pydoc.hpp index b26ab665..4455bd12 100644 --- a/python/holoscan/operators/inference/pydoc.hpp +++ b/python/holoscan/operators/inference/pydoc.hpp @@ -73,6 +73,8 @@ device_map : dict[str, int], optional Mapping of model to GPU ID for inference. temporal_map : dict[str, int], optional Mapping of model to frame delay for inference. +activation_map : dict[str, int], optional + Mapping of model to activation state for inference. backend_map : dict[str, str], optional Mapping of model to backend type for inference. Backend options: ``"trt"`` or ``"torch"`` in_tensor_names : sequence of str, optional diff --git a/python/holoscan/operators/operator_util.hpp b/python/holoscan/operators/operator_util.hpp index 8033d33b..9db7711c 100644 --- a/python/holoscan/operators/operator_util.hpp +++ b/python/holoscan/operators/operator_util.hpp @@ -156,10 +156,10 @@ void set_vector_arg_via_py_sequence(const py::sequence& seq, Arg& out) { // Handle list of list and other sequence of sequence types. std::vector> v; v.reserve(static_cast(py::len(seq))); - for (auto item : seq) { + for (const auto& item : seq) { std::vector vv; vv.reserve(static_cast(py::len(item))); - for (auto inner_item : item) { vv.push_back(inner_item.cast()); } + for (const auto& inner_item : item) { vv.push_back(inner_item.cast()); } v.push_back(vv); } out = v; @@ -176,7 +176,7 @@ void set_vector_arg_via_py_sequence(const py::sequence& seq, Arg& out) { if (py::isinstance(first_item) && !py::isinstance(first_item)) { // Handle list of list and other sequence of sequence types. YAML::Node yaml_node = YAML::Load("[]"); // Create an empty sequence - for (auto item : seq) { + for (const auto& item : seq) { YAML::Node inner_yaml_node = YAML::Load("[]"); // Create an empty sequence for (const auto& inner_item : item) { inner_yaml_node.push_back(cast_to_yaml_node(inner_item)); diff --git a/python/holoscan/resources/__init__.py b/python/holoscan/resources/__init__.py index ef7a900d..137e5cde 100644 --- a/python/holoscan/resources/__init__.py +++ b/python/holoscan/resources/__init__.py @@ -119,7 +119,7 @@ def __init__(self, fragment, *args, **kwargs): # (https://pybind11.readthedocs.io/en/stable/advanced/classes.html#overriding-virtual-functions-in-python) _GXFComponentResource.__init__(self, self, fragment, *args, **kwargs) # Create a PyGXFComponentResourceSpec object and pass it to the C++ API - spec = ComponentSpec(fragment=self.fragment, op=self) + spec = ComponentSpec(fragment=self.fragment, component=self) self.spec = spec # Call setup method in the derived class self.setup(spec) diff --git a/python/holoscan/resources/gxf_component_resource.cpp b/python/holoscan/resources/gxf_component_resource.cpp index d084ce70..1673dbbf 100644 --- a/python/holoscan/resources/gxf_component_resource.cpp +++ b/python/holoscan/resources/gxf_component_resource.cpp @@ -93,7 +93,7 @@ void init_gxf_component_resource(py::module_& m) { gxf::GXFResource, std::shared_ptr>( m, "GXFComponentResource", doc::GXFComponentResource::doc_GXFComponentResource) - .def(py::init<>(), doc::GXFComponentResource::doc_GXFComponentResource) + .def(py::init<>()) .def(py::init +spec: +""" + in repr(cond) + ) + + # assert no warnings or errors logged + captured = capfd.readouterr() + assert "error" not in captured.err + assert "warning" not in captured.err + + def test_default_initialization(self, app): + ExpiringMessageAvailableCondition(app, 1, 4) + + def test_positional_initialization(self, app): + ExpiringMessageAvailableCondition(app, 1, 4, RealtimeClock(app, name="clock"), "expiring") + + class TestPeriodicCondition: def test_kwarg_based_initialization(self, app, capfd): name = "periodic" diff --git a/python/tests/unit/test_core.py b/python/tests/unit/test_core.py index fd50e930..72fbf246 100644 --- a/python/tests/unit/test_core.py +++ b/python/tests/unit/test_core.py @@ -27,7 +27,6 @@ ArgType, CLIOptions, Component, - ComponentSpec, Condition, ConditionType, Config, @@ -48,6 +47,7 @@ io_type_registry, py_object_to_arg, ) +from holoscan.core._core import ComponentSpec as ComponentSpecBase from holoscan.core._core import OperatorSpec as OperatorSpecBase from holoscan.core._core import ParameterFlag, PyOperatorSpec from holoscan.executors import GXFExecutor @@ -192,14 +192,14 @@ def test_dynamic_attribute_not_allowed(self): obj.custom_attribute = 5 -class TestComponentSpec: +class TestComponentSpecBase: def test_init(self, fragment): - c = ComponentSpec(fragment) + c = ComponentSpecBase(fragment) assert c.params == {} assert c.fragment is fragment def test_dynamic_attribute_not_allowed(self, fragment): - obj = ComponentSpec(fragment) + obj = ComponentSpecBase(fragment) with pytest.raises(AttributeError): obj.custom_attribute = 5 @@ -268,7 +268,7 @@ def test_initialize(self): c.initialize() def test_setup(self, fragment): - spec = ComponentSpec(fragment=fragment) + spec = ComponentSpecBase(fragment=fragment) c = Condition() c.setup(spec) @@ -280,55 +280,55 @@ def test_dynamic_attribute_not_allowed(self): class TestResource: def test_init(self, fragment): - r = Resource() + r = Resource(fragment) assert r.name == "" - assert r.fragment is None + assert r.fragment is fragment + assert r.resource_type == Resource.ResourceType.NATIVE - def test_init_with_kwargs(self): - r = Resource(a=5, b=(13.7, 15.2), c="abcd") + def test_init_with_kwargs(self, fragment): + r = Resource(fragment, a=5, b=(13.7, 15.2), c="abcd") assert r.name == "" - assert r.fragment is None + assert r.fragment is fragment assert len(r.args) == 3 - def test_init_with_name_and_kwargs(self): + def test_init_with_name_and_kwargs(self, fragment): # name provided by kwarg - r = Resource(name="r2", a=5, b=(13.7, 15.2), c="abcd") + r = Resource(fragment, name="r2", a=5, b=(13.7, 15.2), c="abcd") assert r.name == "r2" - assert r.fragment is None + assert r.fragment is fragment assert len(r.args) == 3 - def test_name(self): - r = Resource() + def test_name(self, fragment): + r = Resource(fragment) r.name = "res1" assert r.name == "res1" - r = Resource(name="res3") + r = Resource(fragment, name="res3") assert r.name == "res3" def test_fragment(self, fragment): - r = Resource() - assert r.fragment is None + r = Resource(fragment) + assert r.fragment is fragment # not allowed to assign fragment with pytest.raises(AttributeError): r.fragment = fragment - def test_add_arg(self): - r = Resource() + def test_add_arg(self, fragment): + r = Resource(fragment) r.add_arg(Arg("a1")) - def test_initialize(self): - r = Resource() + def test_initialize(self, fragment): + r = Resource(fragment) r.initialize() def test_setup(self, fragment): - spec = ComponentSpec(fragment=fragment) - r = Resource() + spec = ComponentSpecBase(fragment=fragment) + r = Resource(fragment) r.setup(spec) - def test_dynamic_attribute_not_allowed(self): - obj = Resource() - with pytest.raises(AttributeError): - obj.custom_attribute = 5 + def test_dynamic_attribute_allowed(self, fragment): + obj = Resource(fragment) + obj.custom_attribute = 5 class TestOperatorSpecBase: @@ -405,6 +405,61 @@ def test_input_connector_ucx(self, fragment, capfd, kwargs): assert iospec.io_type == IOSpec.IOType.INPUT assert isinstance(iospec.connector(), UcxReceiver) + def test_input_connector_and_condition(self, fragment, capfd): + c = OperatorSpecBase(fragment) + iospec = c.input("in").connector( + IOSpec.ConnectorType.DOUBLE_BUFFER, + capacity=5, + policy=1, + ) + b = iospec.condition( + ConditionType.EXPIRING_MESSAGE_AVAILABLE, + max_batch_size=5, + max_delay_n=1_000_000_000, + ) + + assert iospec == b + + assert isinstance(iospec, IOSpec) + assert iospec.name == "in" + assert iospec.io_type == IOSpec.IOType.INPUT + assert isinstance(iospec.connector(), DoubleBufferReceiver) + + assert len(iospec.conditions) == 1 + assert iospec.conditions[0][0] == ConditionType.EXPIRING_MESSAGE_AVAILABLE + assert iospec.conditions[0][1] is not None + + assert c.inputs["in"] == iospec + assert len(c.inputs["in"].conditions) == 1 + + def test_input_condition_and_connector(self, fragment, capfd): + c = OperatorSpecBase(fragment) + iospec = ( + c.input("in") + .condition( + ConditionType.EXPIRING_MESSAGE_AVAILABLE, + max_batch_size=5, + max_delay_n=1_000_000_000, + ) + .connector( + IOSpec.ConnectorType.DOUBLE_BUFFER, + capacity=5, + policy=1, + ) + ) + assert isinstance(iospec, IOSpec) + assert iospec.name == "in" + assert iospec.io_type == IOSpec.IOType.INPUT + assert isinstance(iospec.connector(), DoubleBufferReceiver) + + assert len(iospec.conditions) == 1 + assert iospec.conditions[0][0] == ConditionType.EXPIRING_MESSAGE_AVAILABLE + assert iospec.conditions[0][1] is not None + + assert c.inputs["in"] == iospec + + assert len(c.inputs["in"].conditions) == 1 + def test_output(self, fragment, capfd): c = OperatorSpecBase(fragment) iospec = c.output() diff --git a/python/tests/unit/test_decorator.py b/python/tests/unit/test_decorator.py new file mode 100644 index 00000000..ea2a0e3e --- /dev/null +++ b/python/tests/unit/test_decorator.py @@ -0,0 +1,346 @@ +""" + SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + SPDX-License-Identifier: Apache-2.0 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" # noqa: E501 +import pytest + +from holoscan.core import Application, ConditionType, IOSpec, Operator +from holoscan.decorator import Input, Output, create_op +from holoscan.operators import PingRxOp, PingTxOp + + +class TestInput: + def test_input(self): + obj = Input("in") + assert obj.name == "in" + assert not obj.arg_map + + def test_input_string_arg_map(self): + obj = Input("in", arg_map="x") + assert obj.arg_map == "x" + + def test_input_dict_arg_map(self): + obj = Input("in", arg_map={"image": "x", "mask": "m"}) + assert isinstance(obj.arg_map, dict) + assert len(obj.arg_map.keys()) == 2 + + def test_create_input_with_condition(self): + app = Application() + op = PingRxOp(fragment=app, name="rx") + + # initially PingRxOp has one input port named "in" with a default connector + assert tuple(op.spec.inputs.keys()) == ("in",) + assert op.spec.inputs["in"].connector_type == IOSpec.ConnectorType.DEFAULT + + # add a new port with NONE condition via Input.create_input + obj = Input("in2", condition_type=ConditionType.NONE, condition_kwargs={}) + obj.create_input(op.spec) + in_spec = op.spec.inputs["in2"] + assert len(in_spec.conditions) == 1 + assert in_spec.conditions[0][0] == ConditionType.NONE + + # add a new port with MESSAGE_AVAILABLE condition via Input.create_input + obj = Input( + "in3", condition_type=ConditionType.MESSAGE_AVAILABLE, condition_kwargs=dict(min_size=2) + ) + obj.create_input(op.spec) + in_spec = op.spec.inputs["in3"] + assert len(in_spec.conditions) == 1 + cond_type, cond_obj = in_spec.conditions[0] + assert cond_type == ConditionType.MESSAGE_AVAILABLE + assert len(cond_obj.args) == 1 + assert cond_obj.args[0].name == "min_size" + assert "value: 2" in cond_obj.args[0].description + + def test_create_input_with_connector(self): + app = Application() + op = PingRxOp(fragment=app, name="rx") + + # initially PingRxOp has one input port named "in" with a default connector + assert tuple(op.spec.inputs.keys()) == ("in",) + assert op.spec.inputs["in"].connector_type == IOSpec.ConnectorType.DEFAULT + + obj = Input( + "in2", + connector_type=IOSpec.ConnectorType.DOUBLE_BUFFER, + connector_kwargs=dict(capacity=2, policy=1), + ) + obj.create_input(op.spec) + in_spec = op.spec.inputs["in2"] + assert in_spec.connector_type == IOSpec.ConnectorType.DOUBLE_BUFFER + connector = in_spec.connector() + assert len(connector.args) == 2 + assert connector.args[0].name == "capacity" + assert "value: 2" in connector.args[0].description + assert connector.args[1].name == "policy" + assert "value: 1" in connector.args[1].description + + +class TestOutput: + def test_output(self): + obj = Output("out") + assert obj.name == "out" + assert obj.tensor_names == () + + def test_output_tensor_names(self): + tensor_names = ("x", "waveform") + obj = Output("out", tensor_names=tensor_names) + assert obj.tensor_names == tensor_names + + def test_create_output_with_condition(self): + app = Application() + op = PingTxOp(fragment=app, name="rx") + + # initially PingTxOp has one output port named "out" with a default connector + assert tuple(op.spec.outputs.keys()) == ("out",) + assert op.spec.outputs["out"].connector_type == IOSpec.ConnectorType.DEFAULT + + # add a new port with NONE condition via Output.create_output + obj = Output("out2", condition_type=ConditionType.NONE, condition_kwargs={}) + obj.create_output(op.spec) + in_spec = op.spec.outputs["out2"] + assert len(in_spec.conditions) == 1 + assert in_spec.conditions[0][0] == ConditionType.NONE + + # add a new port with MESSAGE_AVAILABLE condition via Output.create_output + obj = Output( + "out3", + condition_type=ConditionType.DOWNSTREAM_MESSAGE_AFFORDABLE, + condition_kwargs=dict(min_size=2), + ) + obj.create_output(op.spec) + in_spec = op.spec.outputs["out3"] + assert len(in_spec.conditions) == 1 + cond_type, cond_obj = in_spec.conditions[0] + assert cond_type == ConditionType.DOWNSTREAM_MESSAGE_AFFORDABLE + assert len(cond_obj.args) == 1 + assert cond_obj.args[0].name == "min_size" + assert "value: 2" in cond_obj.args[0].description + + def test_create_output_with_connector(self): + app = Application() + op = PingTxOp(fragment=app, name="rx") + + # initially PingTxOp has one output port named "out" with a default connector + assert tuple(op.spec.outputs.keys()) == ("out",) + assert op.spec.outputs["out"].connector_type == IOSpec.ConnectorType.DEFAULT + + obj = Output( + "out2", + connector_type=IOSpec.ConnectorType.DOUBLE_BUFFER, + connector_kwargs=dict(capacity=2, policy=1), + ) + obj.create_output(op.spec) + in_spec = op.spec.outputs["out2"] + assert in_spec.connector_type == IOSpec.ConnectorType.DOUBLE_BUFFER + connector = in_spec.connector() + assert len(connector.args) == 2 + assert connector.args[0].name == "capacity" + assert "value: 2" in connector.args[0].description + assert connector.args[1].name == "policy" + assert "value: 1" in connector.args[1].description + + +class TestCreateOp: + def test_create_op_no_args_func(self): + @create_op() + def func_no_args(): + pass + + with pytest.raises(ValueError, match="fragment must be provided"): + func_no_args() + + # pass fragment positionally + app = Application() + my_op = func_no_args(app) + # __name__ will be a camelcase version of the function name + assert my_op.__name__ == "FuncNoArgsOp" + assert my_op.name == "func_no_args" + + # pass fragment and name via kwarg + my_op2 = func_no_args(fragment=app, name="my-op") + assert my_op2.__name__ == "FuncNoArgsOp" + assert my_op2.name == "my-op" + + def test_create_op_input_not_specified(self): + @create_op() + def func_one_positional_arg(image): + return image + + app = Application() + with pytest.raises(ValueError, match="required argument, 'image', has not been specified"): + func_one_positional_arg(app) + + @pytest.mark.parametrize( + "inputs", + [ + "image", + ("image",), + Input("image", arg_map="image"), + Input("image", arg_map={"tensor": "image"}), + (Input("image", arg_map={"tensor": "image"}),), + ], + ) + def test_create_op_inputs_specified(self, inputs): + @create_op(inputs=inputs) + def func_one_positional_arg(image): + return image + + # pass fragment positionally + app = Application() + my_op = func_one_positional_arg(app) + # __name__ will be a camelcase version of the function name + assert my_op.__name__ == "FuncOnePositionalArgOp" + assert my_op.name == "func_one_positional_arg" + assert "image" in my_op.dynamic_kwargs + assert "image" not in my_op.fixed_kwargs + + @pytest.mark.parametrize( + "inputs, exception_type, expected_error_message", + [ + ("tensor", KeyError, "Provided func does not have an arg or kwarg named 'tensor'"), + (("tensor",), KeyError, "Provided func does not have an arg or kwarg named 'tensor'"), + # image not in destinations for arg_map + ( + Input("image", arg_map=()), + ValueError, + "required argument, 'image', has not been specified", + ), + ( + Input("image", arg_map={"tensor": "tensor"}), + KeyError, + "Provided func does not have an arg or kwarg named 'tensor'", + ), + ( + (Input("image", arg_map={"tensor": "tensor"}),), + KeyError, + "Provided func does not have an arg or kwarg named 'tensor'", + ), + ], + ) + def test_create_op_invalid_input_name(self, inputs, exception_type, expected_error_message): + @create_op(inputs=inputs) + def func_one_positional_arg(image): + return image + + app = Application() + with pytest.raises(exception_type, match=expected_error_message): + func_one_positional_arg(app) + + @pytest.mark.parametrize("input_as_tuple", [False, True]) + def test_create_op_inputs_specified_via_input_obj(self, input_as_tuple): + port_name = "image_in" + inputs = Input(port_name, {"image": "image"}, condition_type=ConditionType.NONE) + if input_as_tuple: + inputs = (inputs,) + + @create_op(inputs=inputs) + def func_one_positional_arg(image): + return image + + # pass fragment positionally + app = Application() + my_op = func_one_positional_arg(app) + assert "image" in my_op.dynamic_kwargs + assert "image" not in my_op.fixed_kwargs + + cond_type, cond_obj = my_op.spec.inputs[port_name].conditions[0] + assert cond_obj is None + assert cond_type == ConditionType.NONE + + def test_create_op_inputs_keyword_only_arg(self): + @create_op(inputs="image", outputs="image") + def func_one_positional_arg(image, x=5, *, y=7): + return image + + # pass fragment positionally + app = Application() + my_op = func_one_positional_arg(app, y=12) + assert "image" in my_op.dynamic_kwargs + assert "x" in my_op.fixed_kwargs + assert my_op.fixed_kwargs["x"] == 5 + assert "y" in my_op.fixed_kwargs + assert my_op.fixed_kwargs["y"] == 12 + + def test_create_op_generator_func(self): + @create_op(inputs="image", outputs="image") + def int_generator(image, *, count=10): + yield from range(count) + + # pass fragment positionally + app = Application() + my_op = int_generator(app) + assert my_op.fixed_kwargs["count"] == 10 + + my_op = int_generator(app, count=1) + assert my_op.fixed_kwargs["count"] == 1 + + @pytest.mark.parametrize("explicit_inputs_and_outputs", [False, True]) + def test_create_op_from_class(self, explicit_inputs_and_outputs): + if explicit_inputs_and_outputs: + + @create_op(inputs="image", outputs="out") + class TensorGenerator: + def __init__(self, start_index=5): + self.counter = start_index - 1 + + def __call__(self, image, *, msg="hello"): + print(f"{msg}: {image.shape=}") + self.counter += 1 + return self.counter + else: + + @create_op + class TensorGenerator: + def __init__(self, start_index=5): + self.counter = start_index - 1 + + def __call__(self, image, *, msg="hello"): + print(f"{msg}: {image.shape=}") + self.counter += 1 + return self.counter + + # pass fragment positionally + app = Application() + start_index = 10 + my_op = TensorGenerator(start_index=start_index)(app, name="class_op") + + assert isinstance(my_op, Operator) + # verify input port name + inputs = my_op.spec.inputs + assert len(inputs) == 1 + assert inputs["image"].name == "image" + + # verify output port name + outputs = my_op.spec.outputs + assert len(outputs) == 1 + if explicit_inputs_and_outputs: + assert "out" in outputs + else: + assert "" in outputs + + # check internal state of the wrapped class + assert my_op.class_obj.counter == start_index - 1 + assert my_op.class_obj.__class__.__name__ == "TensorGenerator" + + # check names + assert my_op.name == "class_op" + assert my_op.__name__ == "TensorGeneratorOp" + + # verify kwargs based on __call__ function signature + assert len(my_op.dynamic_kwargs) == 1 + assert "image" in my_op.dynamic_kwargs + assert len(my_op.fixed_kwargs) == 1 + assert "msg" in my_op.fixed_kwargs diff --git a/python/tests/unit/test_gxf.py b/python/tests/unit/test_gxf.py index 9ff26490..5baa85c8 100644 --- a/python/tests/unit/test_gxf.py +++ b/python/tests/unit/test_gxf.py @@ -17,7 +17,9 @@ import pytest -from holoscan.core import Component, Condition, Resource, _Operator +from holoscan.core import Component, Condition +from holoscan.core import _Operator as OperatorBase +from holoscan.core import _Resource as ResourceBase from holoscan.gxf import ( Entity, GXFComponent, @@ -102,13 +104,13 @@ class TestGXFResource(TestGXFComponent): def test_type(self): r = GXFResource() assert isinstance(r, Component) - assert isinstance(r, Resource) + assert isinstance(r, ResourceBase) assert isinstance(r, GXFComponent) - def test_dynamic_attribute_not_allowed(self): + def test_dynamic_attribute_allowed(self): + # parent Resource class allows dynamic attributes obj = GXFResource() - with pytest.raises(AttributeError): - obj.custom_attribute = 5 + obj.custom_attribute = 5 class TestGXFInputContext: @@ -166,7 +168,7 @@ def test_eid(self): def test_type(self): op = GXFOperator() - assert isinstance(op, _Operator) + assert isinstance(op, OperatorBase) def test_dynamic_attribute_allowed(self): obj = GXFOperator() diff --git a/python/tests/unit/test_network_context.py b/python/tests/unit/test_network_context.py index 24d74c8a..ec6f01e6 100644 --- a/python/tests/unit/test_network_context.py +++ b/python/tests/unit/test_network_context.py @@ -15,7 +15,8 @@ limitations under the License. """ # noqa: E501 -from holoscan.core import ComponentSpec, NetworkContext +from holoscan.core import NetworkContext +from holoscan.core._core import ComponentSpec as ComponentSpecBase from holoscan.gxf import GXFNetworkContext from holoscan.network_contexts import UcxContext from holoscan.resources import UcxEntitySerializer @@ -26,9 +27,7 @@ def test_default_init(self, app): e = UcxContext(app) assert isinstance(e, GXFNetworkContext) assert isinstance(e, NetworkContext) - # The 'e.spec' is from the binder (ComponentSpec), and 'ComponentSpec' actually - # derives from PyComponentSpec, which inherits from ComponentSpec. - assert issubclass(ComponentSpec, type(e.spec)) + assert issubclass(ComponentSpecBase, type(e.spec)) def test_init_kwargs(self, app): entity_serializer = UcxEntitySerializer( diff --git a/python/tests/unit/test_operators_native.py b/python/tests/unit/test_operators_native.py index 7edaf00e..239fdcc4 100644 --- a/python/tests/unit/test_operators_native.py +++ b/python/tests/unit/test_operators_native.py @@ -359,6 +359,29 @@ def test_kwarg_based_initialization(self, app, config_file, capfd): assert "error" not in captured.err assert "warning" not in captured.err + def test_channel_kwarg_string_variant(self, app, config_file, capfd): + app.config(config_file) + name = "source" + op = AJASourceOp( + fragment=app, + name=name, + channel="NTV2_CHANNEL1", + width=1920, + height=1080, + rdma=True, + enable_overlay=False, + overlay_channel="NTV2_CHANNEL2", + overlay_rdma=True, + ) + assert isinstance(op, _Operator) + assert op.operator_type == Operator.OperatorType.NATIVE + assert f"name: {name}" in repr(op) + + # assert no warnings or errors logged + captured = capfd.readouterr() + assert "error" not in captured.err + assert "warning" not in captured.err + def test_initialization_from_yaml(self, app, config_file, capfd): app.config(config_file) name = "source" diff --git a/python/tests/unit/test_resources.py b/python/tests/unit/test_resources.py index 2efd0ad5..7215c755 100644 --- a/python/tests/unit/test_resources.py +++ b/python/tests/unit/test_resources.py @@ -15,8 +15,10 @@ limitations under the License. """ # noqa: E501 -from holoscan.core import Resource +from holoscan.core import ComponentSpec, Resource +from holoscan.core import _Resource as ResourceBase from holoscan.gxf import GXFResource +from holoscan.operators import PingTxOp from holoscan.resources import ( Allocator, BlockMemoryPool, @@ -54,7 +56,7 @@ def test_kwarg_based_initialization(self, app, capfd): ) assert isinstance(pool, Allocator) assert isinstance(pool, GXFResource) - assert isinstance(pool, Resource) + assert isinstance(pool, ResourceBase) assert pool.id == -1 assert pool.gxf_typename == "nvidia::gxf::BlockMemoryPool" @@ -74,7 +76,7 @@ def test_default_initialization(self, app, capfd): pool = CudaStreamPool(fragment=app, name=name) assert isinstance(pool, Allocator) assert isinstance(pool, GXFResource) - assert isinstance(pool, Resource) + assert isinstance(pool, ResourceBase) assert pool.id == -1 assert pool.gxf_typename == "nvidia::gxf::CudaStreamPool" assert f"name: {name}" in repr(pool) @@ -96,7 +98,7 @@ def test_kwarg_based_initialization(self, app, capfd): ) assert isinstance(pool, Allocator) assert isinstance(pool, GXFResource) - assert isinstance(pool, Resource) + assert isinstance(pool, ResourceBase) assert pool.id == -1 assert pool.gxf_typename == "nvidia::gxf::CudaStreamPool" assert f"name: {name}" in repr(pool) @@ -118,7 +120,7 @@ def test_kwarg_based_initialization(self, app, capfd): ) assert isinstance(alloc, Allocator) assert isinstance(alloc, GXFResource) - assert isinstance(alloc, Resource) + assert isinstance(alloc, ResourceBase) assert alloc.id == -1 assert alloc.gxf_typename == "nvidia::gxf::UnboundedAllocator" assert f"name: {name}" in repr(alloc) @@ -143,15 +145,18 @@ def test_kwarg_based_initialization(self, app, capfd): ) assert isinstance(r, Receiver) assert isinstance(r, GXFResource) - assert isinstance(r, Resource) + assert isinstance(r, ResourceBase) assert r.id == -1 assert r.gxf_typename == "nvidia::gxf::DoubleBufferReceiver" + r.initialize() # manually initialize so we can check resource_type + assert r.resource_type == Resource.ResourceType.GXF assert f"name: {name}" in repr(r) - # assert no warnings or errors logged + # assert no unexpected warnings or errors logged captured = capfd.readouterr() assert "error" not in captured.err - assert "warning" not in captured.err + # expect one warning due to manually calling initialize() above + assert captured.err.count("warning") < 2 def test_default_initialization(self, app): DoubleBufferReceiver(app) @@ -168,15 +173,18 @@ def test_kwarg_based_initialization(self, app, capfd): ) assert isinstance(r, Transmitter) assert isinstance(r, GXFResource) - assert isinstance(r, Resource) + assert isinstance(r, ResourceBase) assert r.id == -1 assert r.gxf_typename == "nvidia::gxf::DoubleBufferTransmitter" + r.initialize() # manually initialize so we can check resource_type + assert r.resource_type == Resource.ResourceType.GXF assert f"name: {name}" in repr(r) - # assert no warnings or errors logged + # assert no unexpected warnings or errors logged captured = capfd.readouterr() assert "error" not in captured.err - assert "warning" not in captured.err + # expect one warning due to manually calling initialize() above + assert captured.err.count("warning") < 2 def test_default_initialization(self, app): DoubleBufferTransmitter(app) @@ -190,7 +198,7 @@ def test_kwarg_based_initialization(self, app, capfd): name=name, ) assert isinstance(r, GXFResource) - assert isinstance(r, Resource) + assert isinstance(r, ResourceBase) assert r.id == -1 assert r.gxf_typename == "nvidia::gxf::StdComponentSerializer" assert f"name: {name}" in repr(r) @@ -212,7 +220,7 @@ def test_kwarg_based_initialization(self, app, capfd): name=name, ) assert isinstance(r, GXFResource) - assert isinstance(r, Resource) + assert isinstance(r, ResourceBase) assert r.id == -1 assert r.gxf_typename == "nvidia::gxf::StdEntitySerializer" assert f"name: {name}" in repr(r) @@ -236,7 +244,7 @@ def test_kwarg_based_initialization(self, app, capfd): ) assert isinstance(clk, Clock) assert isinstance(clk, GXFResource) - assert isinstance(clk, Resource) + assert isinstance(clk, ResourceBase) assert clk.id == -1 assert clk.gxf_typename == "nvidia::gxf::ManualClock" assert f"name: {name}" in repr(clk) @@ -262,7 +270,7 @@ def test_kwarg_based_initialization(self, app, capfd): ) assert isinstance(clk, Clock) assert isinstance(clk, GXFResource) - assert isinstance(clk, Resource) + assert isinstance(clk, ResourceBase) assert clk.id == -1 assert clk.gxf_typename == "nvidia::gxf::RealtimeClock" assert f"name: {name}" in repr(clk) @@ -286,7 +294,7 @@ def test_kwarg_based_initialization(self, app, capfd): name=name, ) assert isinstance(res, GXFResource) - assert isinstance(res, Resource) + assert isinstance(res, ResourceBase) assert res.id == -1 # -1 because initialize() isn't called assert res.gxf_typename == "nvidia::gxf::SerializationBuffer" assert f"name: {name}" in repr(res) @@ -307,7 +315,7 @@ def test_kwarg_based_initialization(self, app, capfd): name=name, ) assert isinstance(res, GXFResource) - assert isinstance(res, Resource) + assert isinstance(res, ResourceBase) assert res.id == -1 assert res.gxf_typename == "nvidia::gxf::UcxSerializationBuffer" assert f"name: {name}" in repr(res) @@ -327,7 +335,7 @@ def test_kwarg_based_initialization(self, app, capfd): name=name, ) assert isinstance(res, GXFResource) - assert isinstance(res, Resource) + assert isinstance(res, ResourceBase) assert res.id == -1 assert res.gxf_typename == "nvidia::gxf::UcxComponentSerializer" assert f"name: {name}" in repr(res) @@ -347,7 +355,7 @@ def test_kwarg_based_initialization(self, app, capfd): name=name, ) assert isinstance(res, GXFResource) - assert isinstance(res, Resource) + assert isinstance(res, ResourceBase) assert res.id == -1 assert res.gxf_typename == "nvidia::gxf::UcxHoloscanComponentSerializer" assert f"name: {name}" in repr(res) @@ -367,7 +375,7 @@ def test_intialization_default_serializers(self, app, capfd): name=name, ) assert isinstance(res, GXFResource) - assert isinstance(res, Resource) + assert isinstance(res, ResourceBase) assert res.id == -1 assert res.gxf_typename == "nvidia::gxf::UcxEntitySerializer" assert f"name: {name}" in repr(res) @@ -397,7 +405,7 @@ def test_kwarg_based_initialization(self, app, capfd): name=name, ) assert isinstance(res, GXFResource) - assert isinstance(res, Resource) + assert isinstance(res, ResourceBase) assert isinstance(res, Receiver) assert res.id == -1 assert res.gxf_typename == "nvidia::gxf::UcxReceiver" @@ -431,7 +439,7 @@ def test_kwarg_based_initialization(self, app, capfd): name=name, ) assert isinstance(res, GXFResource) - assert isinstance(res, Resource) + assert isinstance(res, ResourceBase) assert isinstance(res, Transmitter) assert res.id == -1 assert res.gxf_typename == "nvidia::gxf::UcxTransmitter" @@ -441,3 +449,31 @@ def test_kwarg_based_initialization(self, app, capfd): captured = capfd.readouterr() assert "error" not in captured.err assert "warning" not in captured.err + + +class DummyNativeResource(Resource): + def __init__(self, fragment, *args, **kwargs): + super().__init__(fragment, *args, **kwargs) + + def setup(self, spec: ComponentSpec): + pass + + +class TestNativeResource: + def test_native_resource_to_operator(self, app): + """Tests passing a native resource as a positional argument to an operator.""" + tx = PingTxOp( + app, + DummyNativeResource(fragment=app, name="native_resource"), + name="tx", + ) + tx.initialize() + + # verify that the resource is included in the operator's description + resource_repr = """ +resources: + - id: -1 + name: native_resource +""" + assert resource_repr in tx.__repr__() + assert resource_repr in tx.description diff --git a/python/tests/unit/test_schedulers.py b/python/tests/unit/test_schedulers.py index 0981b33b..2c62376b 100644 --- a/python/tests/unit/test_schedulers.py +++ b/python/tests/unit/test_schedulers.py @@ -17,7 +17,8 @@ import pytest -from holoscan.core import ComponentSpec, Scheduler +from holoscan.core import Scheduler +from holoscan.core._core import ComponentSpec as ComponentSpecBase from holoscan.gxf import GXFScheduler from holoscan.resources import ManualClock, RealtimeClock from holoscan.schedulers import EventBasedScheduler, GreedyScheduler, MultiThreadScheduler @@ -28,9 +29,7 @@ def test_default_init(self, app): scheduler = GreedyScheduler(app) assert isinstance(scheduler, GXFScheduler) assert isinstance(scheduler, Scheduler) - # The 'scheduler.spec' is from the binder (ComponentSpec), and 'ComponentSpec' actually - # derives from PyComponentSpec, which inherits from ComponentSpec. - assert issubclass(ComponentSpec, type(scheduler.spec)) + assert issubclass(ComponentSpecBase, type(scheduler.spec)) @pytest.mark.parametrize("ClockClass", [ManualClock, RealtimeClock]) def test_init_kwargs(self, app, ClockClass): # noqa: N803 @@ -83,9 +82,7 @@ def test_default_init(self, app): scheduler = MultiThreadScheduler(app) assert isinstance(scheduler, GXFScheduler) assert isinstance(scheduler, Scheduler) - # The 'scheduler.spec' is from the binder (ComponentSpec), and 'ComponentSpec' actually - # derives from PyComponentSpec, which inherits from ComponentSpec. - assert issubclass(ComponentSpec, type(scheduler.spec)) + assert issubclass(ComponentSpecBase, type(scheduler.spec)) @pytest.mark.parametrize("ClockClass", [ManualClock, RealtimeClock]) def test_init_kwargs(self, app, ClockClass): # noqa: N803 @@ -146,9 +143,7 @@ def test_default_init(self, app): scheduler = EventBasedScheduler(app) assert isinstance(scheduler, GXFScheduler) assert isinstance(scheduler, Scheduler) - # The 'scheduler.spec' is from the binder (ComponentSpec), and 'ComponentSpec' actually - # derives from PyComponentSpec, which inherits from ComponentSpec. - assert issubclass(ComponentSpec, type(scheduler.spec)) + assert issubclass(ComponentSpecBase, type(scheduler.spec)) @pytest.mark.parametrize("ClockClass", [ManualClock, RealtimeClock]) def test_init_kwargs(self, app, ClockClass): # noqa: N803 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 030191e4..ef74fc47 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -129,6 +129,7 @@ add_holoscan_library(core core/conditions/gxf/downstream_affordable.cpp core/conditions/gxf/periodic.cpp core/conditions/gxf/message_available.cpp + core/conditions/gxf/expiring_message.cpp core/config.cpp core/dataflow_tracker.cpp core/domain/tensor.cpp diff --git a/src/common/logger/spdlog_logger.cpp b/src/common/logger/spdlog_logger.cpp index 9f34526d..419d63f9 100644 --- a/src/common/logger/spdlog_logger.cpp +++ b/src/common/logger/spdlog_logger.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include namespace spdlog { @@ -40,13 +41,13 @@ class ansicolor_file_sink : public ansicolor_sink { } // namespace sinks -static inline std::shared_ptr create_file_logger(std::string name, FILE* file) { +static inline std::shared_ptr create_file_logger(const std::string name, FILE* file) { // Do not register to spdlog registry spdlog::details::registry::instance().set_automatic_registration(false); return spdlog::synchronous_factory::template create< spdlog::sinks::ansicolor_file_sink>( - name, file, spdlog::color_mode::automatic); + std::move(name), file, spdlog::color_mode::automatic); } } // namespace spdlog diff --git a/src/core/app_driver.cpp b/src/core/app_driver.cpp index b8353228..cdff6bec 100644 --- a/src/core/app_driver.cpp +++ b/src/core/app_driver.cpp @@ -255,8 +255,8 @@ void AppDriver::run() { HOLOSCAN_LOG_ERROR("Send interrupt once more to terminate immediately"); SignalHandler::unregister_signal_handler(context, signum); // Register the global signal handler. - SignalHandler::register_global_signal_handler(signum, [](int signum) { - (void)signum; + SignalHandler::register_global_signal_handler(signum, [](int sig) { + (void)sig; HOLOSCAN_LOG_ERROR("Interrupted by user (global signal handler)"); exit(1); }); @@ -271,13 +271,13 @@ void AppDriver::run() { app_status_ = AppStatus::kError; // Stop the driver server - driver_server_->stop(); + if (driver_server_) { driver_server_->stop(); } // Do not wait for the driver server to stop because it will block the main thread }; SignalHandler::register_signal_handler(app_->executor().context(), SIGINT, sig_handler); SignalHandler::register_signal_handler(app_->executor().context(), SIGTERM, sig_handler); } - driver_server_->wait(); + if (driver_server_) { driver_server_->wait(); } } } @@ -963,11 +963,11 @@ void AppDriver::check_fragment_schedule(const std::string& worker_address) { std::unordered_set not_participated_workers(worker_addresses.begin(), worker_addresses.end()); for (auto& [fragment_name, worker_id] : schedule) { not_participated_workers.erase(worker_id); } - for (const auto& worker_address : not_participated_workers) { - HOLOSCAN_LOG_INFO("{}' does not participate in the schedule", worker_address); - auto& worker_client = driver_server_->connect_to_worker(worker_address); + for (const auto& worker_addr : not_participated_workers) { + HOLOSCAN_LOG_INFO("{}' does not participate in the schedule", worker_addr); + auto& worker_client = driver_server_->connect_to_worker(worker_addr); worker_client->terminate_worker(AppWorkerTerminationCode::kCancelled); - driver_server_->close_worker_connection(worker_address); + driver_server_->close_worker_connection(worker_addr); } auto& fragment_graph = app_->fragment_graph(); @@ -984,6 +984,7 @@ void AppDriver::check_fragment_schedule(const std::string& worker_address) { } // collect port information from each worker (must be before the collect_connections call) + bool is_port_info_collected = true; for (const auto& [worker_id, fragment_names] : id_to_fragment_names_vector) { auto& worker_client = driver_server_->connect_to_worker(worker_id); @@ -995,12 +996,25 @@ void AppDriver::check_fragment_schedule(const std::string& worker_address) { HOLOSCAN_LOG_ERROR( "Failed to retrieve port info for all fragments scheduled on worker with id '{}'", worker_id); + is_port_info_collected = false; break; } } // Merge the collected port info into the app driver's port information map all_fragment_port_map_->merge(scheduled_fragments_port_info); } + if (!is_port_info_collected) { + HOLOSCAN_LOG_ERROR("Unable to collect port information from workers"); + // Terminate all worker and close all worker connections + terminate_all_workers(AppWorkerTerminationCode::kFailure); + + // Set app status to error + app_status_ = AppStatus::kError; + + // Stop the driver server + driver_server_->stop(); + return; + } // (populates index_to_port_map_, index_to_ip_map_, connection_map_, receiver_port_map_) if (!collect_connections(fragment_graph)) { HOLOSCAN_LOG_ERROR("Cannot collect connections"); } diff --git a/src/core/app_worker.cpp b/src/core/app_worker.cpp index 987e46b3..f70e7747 100644 --- a/src/core/app_worker.cpp +++ b/src/core/app_worker.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -106,7 +106,19 @@ bool AppWorker::execute_fragments( } // Compose scheduled fragments - for (auto& fragment : scheduled_fragments) { fragment->compose_graph(); } + for (auto& fragment : scheduled_fragments) { + try { + fragment->compose_graph(); + } catch (const std::exception& exception) { + HOLOSCAN_LOG_ERROR( + "Failed to compose fragment graph '{}': {}", fragment->name(), exception.what()); + // Notify the worker server that the worker execution is finished with failure + termination_code_ = AppWorkerTerminationCode::kFailure; + submit_message( + WorkerMessage{AppWorker::WorkerMessageCode::kNotifyWorkerExecutionFinished, {}}); + return false; + } + } // Add the UCX network context for (auto& fragment : scheduled_fragments) { diff --git a/src/core/application.cpp b/src/core/application.cpp index cbfd16bf..7d2f01ad 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -330,8 +330,13 @@ void Application::compose_graph() { HOLOSCAN_LOG_DEBUG("The application({}) has already been composed. Skipping...", name()); return; } - is_composed_ = true; + + // Load extensions from the config file before composing the graph. + // (The GXFCodeletOp and GXFComponentResource classes are required to access the underlying GXF + // types in the setup() method when composing a graph.) + load_extensions_from_config(); compose(); + is_composed_ = true; } void Application::set_scheduler_for_fragments(std::vector& target_fragments) { diff --git a/src/core/cli_parser.cpp b/src/core/cli_parser.cpp index 3422d236..3e628a6a 100644 --- a/src/core/cli_parser.cpp +++ b/src/core/cli_parser.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +20,7 @@ #include #include #include +#include #include #include "CLI/Config.hpp" @@ -30,7 +31,7 @@ namespace holoscan { void CLIParser::initialize(std::string app_description, std::string app_version) { // Set the application description and version. - app_.description(app_description); + app_.description(std::move(app_description)); app_.set_version_flag("--version", app_version, "Show the version of the application."); if (!is_initialized_) { diff --git a/src/core/condition.cpp b/src/core/condition.cpp index 9d47d5b7..8dfc147a 100644 --- a/src/core/condition.cpp +++ b/src/core/condition.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,7 +17,10 @@ #include "holoscan/core/condition.hpp" +#include +#include #include "holoscan/core/component_spec.hpp" +#include "holoscan/core/resource.hpp" namespace holoscan { @@ -31,4 +34,26 @@ YAML::Node Condition::to_yaml_node() const { return node; } +void Condition::add_arg(const std::shared_ptr& arg) { + if (resources_.find(arg->name()) != resources_.end()) { + HOLOSCAN_LOG_ERROR( + "Resource '{}' already exists in the condition. Please specify a unique " + "name when creating a Resource instance.", + arg->name()); + } else { + resources_[arg->name()] = arg; + } +} + +void Condition::Condition::add_arg(std::shared_ptr&& arg) { + if (resources_.find(arg->name()) != resources_.end()) { + HOLOSCAN_LOG_ERROR( + "Resource '{}' already exists in the condition. Please specify a unique " + "name when creating a Resource instance.", + arg->name()); + } else { + resources_[arg->name()] = std::move(arg); + } +} + } // namespace holoscan diff --git a/src/core/conditions/gxf/asynchronous.cpp b/src/core/conditions/gxf/asynchronous.cpp index 61d65802..07b19dff 100644 --- a/src/core/conditions/gxf/asynchronous.cpp +++ b/src/core/conditions/gxf/asynchronous.cpp @@ -25,13 +25,7 @@ namespace holoscan { AsynchronousCondition::AsynchronousCondition(const std::string& name, nvidia::gxf::AsynchronousSchedulingTerm* term) - : GXFCondition(name, term) { - if (term) { - // no parameters to configure for this condition type - } else { - HOLOSCAN_LOG_ERROR("AsynchronousCondition: term is null"); - } -} + : GXFCondition(name, term) {} nvidia::gxf::AsynchronousSchedulingTerm* AsynchronousCondition::get() const { return static_cast(gxf_cptr_); diff --git a/src/core/conditions/gxf/expiring_message.cpp b/src/core/conditions/gxf/expiring_message.cpp new file mode 100644 index 00000000..b63fe9ef --- /dev/null +++ b/src/core/conditions/gxf/expiring_message.cpp @@ -0,0 +1,96 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "holoscan/core/conditions/gxf/expiring_message.hpp" +#include "holoscan/core/component_spec.hpp" +#include "holoscan/core/fragment.hpp" + +namespace holoscan { + +void ExpiringMessageAvailableCondition::setup(ComponentSpec& spec) { + spec.param(receiver_, + "receiver", + "Queue channel", + "The scheduling term permits execution if this channel has at least a given number of " + "messages available."); + spec.param(clock_, "clock", "Clock", "The clock to be used to get the time from."); + spec.param(max_batch_size_, + "max_batch_size", + "Maximum Batch Size", + "The maximum number of messages to be batched together"); + spec.param(max_delay_ns_, + "max_delay_ns", + "Maximum delay in nano seconds.", + "The maximum delay from first message to wait before submitting workload"); +} + +nvidia::gxf::ExpiringMessageAvailableSchedulingTerm* ExpiringMessageAvailableCondition::get() + const { + return static_cast(gxf_cptr_); +} + +void ExpiringMessageAvailableCondition::initialize() { + // Set up prerequisite parameters before calling Scheduler::initialize() + auto frag = fragment(); + + // Find if there is an argument for 'clock' + auto has_clock = std::find_if( + args().begin(), args().end(), [](const auto& arg) { return (arg.name() == "clock"); }); + // Create the clock if there was no argument provided. + if (has_clock == args().end()) { + clock_ = frag->make_resource("expiring_message__realtime_clock"); + clock_->gxf_cname(clock_->name().c_str()); + if (gxf_eid_ != 0) { clock_->gxf_eid(gxf_eid_); } + add_arg(clock_.get()); + } + + // parent class initialize() call must be after the argument additions above + GXFCondition::initialize(); +} + +void ExpiringMessageAvailableCondition::max_batch_size(int64_t max_batch_size) { + auto expiring_message = get(); + if (expiring_message) { + // expiring_message->setMaxBatchSize(max_batch_size); + GxfParameterSetInt64(gxf_context_, gxf_cid_, "max_batch_size", max_batch_size); + } + max_batch_size_ = max_batch_size; +} + +void ExpiringMessageAvailableCondition::max_delay(int64_t max_delay_ns) { + auto expiring_message = get(); + + if (expiring_message) { + HOLOSCAN_LOG_INFO("Setting max delay to {}", max_delay_ns); + // expiring_message->setMaxDelayNs(max_delay_ns); + GxfParameterSetInt64(gxf_context_, gxf_cid_, "max_delay_ns", max_delay_ns); + } + max_delay_ns_ = max_delay_ns; +} + +int64_t ExpiringMessageAvailableCondition::max_delay_ns() { + auto expiring_message = get(); + if (expiring_message) { + int64_t max_delay_ns = 0; + // int64_t max_delay_ns = expiring_message->maxDelayNs(); + GxfParameterGetInt64(gxf_context_, gxf_cid_, "max_delay_ns", &max_delay_ns); + if (max_delay_ns != max_delay_ns_) { max_delay_ns_ = max_delay_ns; } + } + return max_delay_ns_; +} + +} // namespace holoscan diff --git a/src/core/dataflow_tracker.cpp b/src/core/dataflow_tracker.cpp index 6840e4bd..a617f786 100644 --- a/src/core/dataflow_tracker.cpp +++ b/src/core/dataflow_tracker.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include "holoscan/core/dataflow_tracker.hpp" @@ -43,7 +44,7 @@ void DataFlowTracker::end_logging() { if (!logger_ofstream_.is_open()) return; // Write out the remaining messages from the log buffer and close ofstream - for (auto it : buffered_messages_) { logger_ofstream_ << it << "\n"; } + for (const auto& it : buffered_messages_) { logger_ofstream_ << it << "\n"; } logger_ofstream_.close(); } @@ -51,9 +52,9 @@ void DataFlowTracker::print() const { std::cout << "Data Flow Tracking Results:\n"; std::cout << "Total paths: " << all_path_metrics_.size() << "\n\n"; int i = 0; - for (auto it : all_path_metrics_) { + for (const auto& it : all_path_metrics_) { std::cout << "Path " << ++i << ": " << it.first << "\n"; - for (auto it2 : it.second->metrics) { + for (const auto& it2 : it.second->metrics) { std::cout << metricToString.at(it2.first) << ": " << it2.second << "\n"; } std::cout << "\n"; @@ -61,7 +62,7 @@ void DataFlowTracker::print() const { std::cout << "Number of source messages [format: source operator->transmitter name: number of " "messages]:\n"; - for (auto it : source_messages_) { std::cout << it.first << ": " << it.second << "\n"; } + for (const auto& it : source_messages_) { std::cout << it.first << ": " << it.second << "\n"; } std::cout.flush(); // flush standard output; otherwise output may not be printed } @@ -153,7 +154,7 @@ int DataFlowTracker::get_num_paths() { std::vector DataFlowTracker::get_path_strings() { std::vector all_pathstrings; all_pathstrings.reserve(all_path_metrics_.size()); - for (auto it : all_path_metrics_) { all_pathstrings.push_back(it.first); } + for (const auto& it : all_path_metrics_) { all_pathstrings.push_back(it.first); } return all_pathstrings; } @@ -185,7 +186,8 @@ void DataFlowTracker::set_skip_latencies(int threshold) { void DataFlowTracker::enable_logging(std::string filename, uint64_t num_buffered_messages) { is_file_logging_enabled_ = true; this->num_buffered_messages_ = num_buffered_messages; - logger_filename_ = filename; + logger_filename_ = std::move(filename); + std::scoped_lock lock(buffered_messages_mutex_); buffered_messages_.reserve(this->num_buffered_messages_); logfile_messages_ = 0; } @@ -193,18 +195,17 @@ void DataFlowTracker::enable_logging(std::string filename, uint64_t num_buffered void DataFlowTracker::write_to_logfile(std::string text) { if (!text.empty() && is_file_logging_enabled_) { if (!logger_ofstream_.is_open()) { logger_ofstream_.open(logger_filename_); } - buffered_messages_mutex_.lock(); + std::scoped_lock lock(buffered_messages_mutex_); buffered_messages_.push_back(std::to_string(++logfile_messages_) + ":\n" + text); // When the vector's size is equal to buffered number of messages, // flush out the buffer to file // and clear the vector to re-reserve the memory if (buffered_messages_.size() == num_buffered_messages_) { - for (auto it : buffered_messages_) { logger_ofstream_ << it << "\n"; } + for (const auto& it : buffered_messages_) { logger_ofstream_ << it << "\n"; } logger_ofstream_ << std::flush; buffered_messages_.clear(); buffered_messages_.reserve(num_buffered_messages_); } - buffered_messages_mutex_.unlock(); } } diff --git a/src/core/executors/gxf/gxf_executor.cpp b/src/core/executors/gxf/gxf_executor.cpp index a7f6fffb..e9099f53 100644 --- a/src/core/executors/gxf/gxf_executor.cpp +++ b/src/core/executors/gxf/gxf_executor.cpp @@ -40,6 +40,7 @@ #include "holoscan/core/condition.hpp" #include "holoscan/core/conditions/gxf/downstream_affordable.hpp" #include "holoscan/core/conditions/gxf/message_available.hpp" +#include "holoscan/core/conditions/gxf/expiring_message.hpp" #include "holoscan/core/config.hpp" #include "holoscan/core/domain/tensor.hpp" #include "holoscan/core/errors.hpp" @@ -249,7 +250,7 @@ GXFExecutor::GXFExecutor(holoscan::Fragment* fragment, bool create_gxf_context) HOLOSCAN_GXF_CALL_FATAL(GxfContextCreate(&context_)); // } own_gxf_context_ = true; - gxf_extension_manager_ = std::make_shared(context_); + extension_manager_ = std::make_shared(context_); // Register extensions for holoscan (GXFWrapper codelet) register_extensions(); @@ -273,12 +274,22 @@ GXFExecutor::~GXFExecutor() { if (own_gxf_context_) { auto frag_name_display = fragment_->name(); if (!frag_name_display.empty()) { frag_name_display = "[" + frag_name_display + "] "; } - HOLOSCAN_LOG_INFO("{}Destroying context", frag_name_display); + try { + HOLOSCAN_LOG_INFO("{}Destroying context", frag_name_display); + } catch (const std::exception& e) {} // Unregister signal handlers if any - SignalHandler::unregister_signal_handler(context_, SIGINT); - SignalHandler::unregister_signal_handler(context_, SIGTERM); - HOLOSCAN_GXF_CALL(GxfContextDestroy(context_)); + try { + SignalHandler::unregister_signal_handler(context_, SIGINT); + SignalHandler::unregister_signal_handler(context_, SIGTERM); + } catch (const std::exception& e) { + try { + HOLOSCAN_LOG_ERROR("Failed to unregister signal handlers: {}", e.what()); + } catch (const std::exception& e) {} + } + try { + HOLOSCAN_GXF_CALL(GxfContextDestroy(context_)); + } catch (const std::exception& e) {} } // Delete GXF Holoscan Extension @@ -310,10 +321,11 @@ void GXFExecutor::initialize_gxf_resources( void GXFExecutor::add_operator_to_entity_group(gxf_context_t context, gxf_uid_t entity_group_gid, std::shared_ptr op) { auto graph_entity = op->graph_entity(); - gxf_uid_t op_eid = graph_entity->eid(); if (!graph_entity) { HOLOSCAN_LOG_ERROR("null GraphEntity found during add_operator_to_entity_group"); + return; } + gxf_uid_t op_eid = graph_entity->eid(); HOLOSCAN_LOG_DEBUG("Adding operator eid '{}' to entity group '{}'", op_eid, entity_group_gid); HOLOSCAN_GXF_CALL_FATAL(GxfUpdateEntityGroup(context, entity_group_gid, op_eid)); } @@ -348,11 +360,11 @@ void GXFExecutor::interrupt() { void GXFExecutor::context(void* context) { context_ = context; - gxf_extension_manager_ = std::make_shared(context_); + extension_manager_ = std::make_shared(context_); } std::shared_ptr GXFExecutor::extension_manager() { - return gxf_extension_manager_; + return extension_manager_; } namespace { @@ -580,6 +592,22 @@ void GXFExecutor::create_input_port(Fragment* fragment, gxf_context_t gxf_contex message_available_condition->add_to_graph_entity(op); break; } + case ConditionType::kExpiringMessageAvailable: { + std::shared_ptr expiring_message_available_condition = + std::dynamic_pointer_cast(condition); + // Note: GraphEntity::addSchedulingTerm requires a unique name here + std::string cond_name = + fmt::format("__{}_{}_cond_{}", op->name(), rx_name, condition_index); + expiring_message_available_condition->receiver(connector); + expiring_message_available_condition->name(cond_name); + expiring_message_available_condition->fragment(fragment); + auto rx_condition_spec = std::make_shared(fragment); + expiring_message_available_condition->setup(*rx_condition_spec); + expiring_message_available_condition->spec(std::move(rx_condition_spec)); + // Add to the same entity as the operator and initialize + expiring_message_available_condition->add_to_graph_entity(op); + break; + } case ConditionType::kNone: // No condition break; @@ -866,7 +894,7 @@ void create_virtual_operators_and_connections( std::string source_address_str(source_address); auto [ip, _] = holoscan::CLIOptions::parse_address(source_address_str, "0.0.0.0", "0"); // Convert port string 'port' to int32_t. - source_ip = ip; + source_ip = std::move(ip); } for (auto& [op, port_map] : connection_map) { @@ -1052,7 +1080,6 @@ void GXFExecutor::connect_broadcast_to_previous_op( // Create a transmitter based on the prev_connector_type. switch (prev_connector_type) { - case IOSpec::ConnectorType::kDefault: case IOSpec::ConnectorType::kDoubleBuffer: { // We don't create a AnnotatedDoubleBufferTransmitter even if DFFT is on because // we don't want to annotate a message at the Broadcast component. @@ -1685,17 +1712,6 @@ bool GXFExecutor::initialize_gxf_graph(OperatorGraph& graph) { // multiple threads are setting up the graph. static std::mutex gxf_execution_mutex; - // Load extensions from config file only if not already loaded, - // to avoid unnecessary loading on multiple run() calls. - if (!is_extensions_loaded_) { - HOLOSCAN_LOG_INFO("Loading extensions from configs..."); - // Load extensions from config file if exists. - for (const auto& yaml_node : fragment_->config().yaml_nodes()) { - gxf_extension_manager_->load_extensions_from_yaml(yaml_node); - } - is_extensions_loaded_ = true; - } - { // Lock the GXF context for execution std::scoped_lock lock{gxf_execution_mutex}; @@ -1776,7 +1792,7 @@ bool GXFExecutor::initialize_gxf_graph(OperatorGraph& graph) { dfft_collector_ptr->data_flow_tracker(fragment_->data_flow_tracker()); // Identify leaf and root operators and add to the DFFTCollector object - for (auto op : graph.get_nodes()) { + for (auto& op : graph.get_nodes()) { if (op->is_leaf()) { dfft_collector_ptr->add_leaf_op(op.get()); } else if (op->is_root() || op->is_user_defined_root()) { @@ -1794,8 +1810,7 @@ bool GXFExecutor::initialize_gxf_graph(OperatorGraph& graph) { network_context->gxf_eid(eid); network_context->initialize(); - std::string entity_group_name = "network_entity_group"; - auto entity_group_gid = ::holoscan::gxf::add_entity_group(context_, entity_group_name); + auto entity_group_gid = ::holoscan::gxf::add_entity_group(context_, "network_entity_group"); int32_t gpu_id = static_cast(AppDriver::get_int_env_var("HOLOSCAN_UCX_DEVICE_ID", 0)); @@ -1834,7 +1849,7 @@ bool GXFExecutor::initialize_gxf_graph(OperatorGraph& graph) { GxfUpdateEntityGroup(context, entity_group_gid, gxf_network_context->gxf_eid())); // Loop through all operators and add any operators with a UCX port to the entity group - auto operator_graph = static_cast(fragment_->graph()); + auto& operator_graph = static_cast(fragment_->graph()); for (auto& node : operator_graph.get_nodes()) { auto op_spec = node->spec(); bool already_added = false; @@ -1872,7 +1887,7 @@ bool GXFExecutor::initialize_gxf_graph(OperatorGraph& graph) { "UCX-based connection found, but there is no NetworkContext."}; // Raise an error if any operator has a UCX connector. - auto operator_graph = static_cast(fragment_->graph()); + auto& operator_graph = static_cast(fragment_->graph()); for (auto& node : operator_graph.get_nodes()) { if (node->has_ucx_connector()) { throw std::runtime_error(ucx_error_msg); } } @@ -1912,8 +1927,8 @@ void GXFExecutor::run_gxf_graph() { HOLOSCAN_LOG_ERROR("Send interrupt once more to terminate immediately"); SignalHandler::unregister_signal_handler(context, signum); // Register the global signal handler. - SignalHandler::register_global_signal_handler(signum, [](int signum) { - (void)signum; + SignalHandler::register_global_signal_handler(signum, [](int sig) { + (void)sig; HOLOSCAN_LOG_ERROR("Interrupted by user (global signal handler)"); exit(1); }); @@ -1977,18 +1992,19 @@ void GXFExecutor::register_extensions() { // Register the default GXF extensions for (auto& gxf_extension_file_name : kDefaultGXFExtensions) { - gxf_extension_manager_->load_extension(gxf_extension_file_name); + extension_manager_->load_extension(gxf_extension_file_name); } // Register the default Holoscan GXF extensions for (auto& gxf_extension_file_name : kDefaultHoloscanGXFExtensions) { - gxf_extension_manager_->load_extension(gxf_extension_file_name); + extension_manager_->load_extension(gxf_extension_file_name); } // Register the GXF extension that provides the native operators gxf_tid_t gxf_wrapper_tid{0xd4e7c16bcae741f8, 0xa5eb93731de9ccf6}; + auto gxf_extension_manager = std::dynamic_pointer_cast(extension_manager_); - if (!gxf_extension_manager_->is_extension_loaded(gxf_wrapper_tid)) { + if (gxf_extension_manager && !gxf_extension_manager->is_extension_loaded(gxf_wrapper_tid)) { GXFExtensionRegistrar extension_factory( context_, "HoloscanSdkInternalExtension", @@ -2206,10 +2222,10 @@ void GXFExecutor::add_component_args_to_graph_entity( if (container_type == ArgContainerType::kNative) { if (element_type == ArgElementType::kCondition) { auto condition = std::any_cast>(arg.value()); - add_condition_to_graph_entity(condition, graph_entity); + add_condition_to_graph_entity(std::move(condition), graph_entity); } else if (element_type == ArgElementType::kResource) { auto resource = std::any_cast>(arg.value()); - add_resource_to_graph_entity(resource, graph_entity); + add_resource_to_graph_entity(std::move(resource), graph_entity); } else if (element_type == ArgElementType::kIOSpec) { auto io_spec = std::any_cast(arg.value()); add_iospec_to_graph_entity(io_spec, graph_entity); diff --git a/src/core/fragment.cpp b/src/core/fragment.cpp index a4714e97..7a0210e3 100644 --- a/src/core/fragment.cpp +++ b/src/core/fragment.cpp @@ -104,6 +104,10 @@ void Fragment::config(std::shared_ptr& config) { } Config& Fragment::config() { + return *config_shared(); +} + +std::shared_ptr Fragment::config_shared() { if (!config_) { // If the application is executed with `--config` option or HOLOSCAN_CONFIG_PATH environment // variable, we take the config file from there. @@ -112,7 +116,7 @@ Config& Fragment::config() { if (!config_path.empty() && config_path.size() > 0) { HOLOSCAN_LOG_DEBUG("Loading config from '{}' (through --config option)", config_path); config_ = make_config(config_path.c_str()); - return *config_; + return config_; } else { const char* env_value = std::getenv("HOLOSCAN_CONFIG_PATH"); if (env_value != nullptr && env_value[0] != '\0') { @@ -120,24 +124,31 @@ Config& Fragment::config() { "Loading config from '{}' (through HOLOSCAN_CONFIG_PATH environment variable)", env_value); config_ = make_config(env_value); - return *config_; + return config_; } } } - config_ = make_config(); } - return *config_; + return config_; } OperatorGraph& Fragment::graph() { + return *graph_shared(); +} + +std::shared_ptr Fragment::graph_shared() { if (!graph_) { graph_ = make_graph(); } - return *graph_; + return graph_; } Executor& Fragment::executor() { + return *executor_shared(); +} + +std::shared_ptr Fragment::executor_shared() { if (!executor_) { executor_ = make_executor(); } - return *executor_; + return executor_; } void Fragment::scheduler(const std::shared_ptr& scheduler) { @@ -484,8 +495,13 @@ void Fragment::compose_graph() { HOLOSCAN_LOG_DEBUG("The fragment({}) has already been composed. Skipping...", name()); return; } - is_composed_ = true; + + // Load extensions from the config file before composing the graph. + // (The GXFCodeletOp and GXFComponentResource classes are required to access the underlying GXF + // types in the setup() method when composing a graph.) + load_extensions_from_config(); compose(); + is_composed_ = true; // Protect against the case where no add_operator or add_flow calls were made if (!graph_) { @@ -505,7 +521,7 @@ FragmentPortMap Fragment::port_info() const { return fragment_port_info; } std::vector operators = graph_->get_nodes(); - for (auto op : operators) { + for (auto& op : operators) { HOLOSCAN_LOG_TRACE("\toperator: {}", name_, op->name()); OperatorSpec* op_spec = op->spec(); @@ -549,4 +565,12 @@ void Fragment::reset_graph_entities() { if (gxf_network_context) { gxf_network_context->reset_graph_entities(); } } +void Fragment::load_extensions_from_config() { + HOLOSCAN_LOG_INFO("Loading extensions from configs..."); + // Load any extensions that may be present in the config file + for (const auto& yaml_node : config().yaml_nodes()) { + executor().extension_manager()->load_extensions_from_yaml(yaml_node); + } +} + } // namespace holoscan diff --git a/src/core/gxf/gxf_condition.cpp b/src/core/gxf/gxf_condition.cpp index fea5184d..7e8a7268 100644 --- a/src/core/gxf/gxf_condition.cpp +++ b/src/core/gxf/gxf_condition.cpp @@ -31,6 +31,12 @@ namespace holoscan::gxf { GXFCondition::GXFCondition(const std::string& name, nvidia::gxf::SchedulingTerm* term) { + if (term == nullptr) { + std::string err_msg = + fmt::format("SchedulingTerm pointer is null. Cannot initialize GXFCondition '{}'", name); + HOLOSCAN_LOG_ERROR(err_msg); + throw std::runtime_error(err_msg); + } id_ = term->cid(); name_ = name; gxf_context_ = term->context(); diff --git a/src/core/gxf/gxf_execution_context.cpp b/src/core/gxf/gxf_execution_context.cpp index 58d34f68..5d2b9970 100644 --- a/src/core/gxf/gxf_execution_context.cpp +++ b/src/core/gxf/gxf_execution_context.cpp @@ -19,6 +19,7 @@ #include "holoscan/core/gxf/gxf_execution_context.hpp" #include +#include #include "holoscan/core/gxf/gxf_operator.hpp" @@ -36,7 +37,8 @@ GXFExecutionContext::GXFExecutionContext(gxf_context_t context, Operator* op) { GXFExecutionContext::GXFExecutionContext(gxf_context_t context, std::shared_ptr gxf_input_context, std::shared_ptr gxf_output_context) - : gxf_input_context_(gxf_input_context), gxf_output_context_(gxf_output_context) { + : gxf_input_context_(std::move(gxf_input_context)), + gxf_output_context_(std::move(gxf_output_context)) { context_ = context; } diff --git a/src/core/gxf/gxf_extension_manager.cpp b/src/core/gxf/gxf_extension_manager.cpp index ebe55ddd..5ee2410c 100644 --- a/src/core/gxf/gxf_extension_manager.cpp +++ b/src/core/gxf/gxf_extension_manager.cpp @@ -110,7 +110,9 @@ bool GXFExtensionManager::load_extension(const std::string& file_name, bool no_e HOLOSCAN_LOG_DEBUG("Trying extension {} found in search path {}", base_name.c_str(), candidate_parent_path.c_str()); - handle = dlopen(candidate_path.c_str(), RTLD_LAZY | RTLD_NODELETE); + if (handle == nullptr) { + handle = dlopen(candidate_path.c_str(), RTLD_LAZY | RTLD_NODELETE); + } if (handle != nullptr) { HOLOSCAN_LOG_DEBUG("Loaded extension {} from search path '{}'", base_name.c_str(), diff --git a/src/core/gxf/gxf_io_context.cpp b/src/core/gxf/gxf_io_context.cpp index f998b022..90bb6e11 100644 --- a/src/core/gxf/gxf_io_context.cpp +++ b/src/core/gxf/gxf_io_context.cpp @@ -62,6 +62,10 @@ gxf_context_t GXFInputContext::gxf_context() const { bool GXFInputContext::empty_impl(const char* name) { std::string input_name = holoscan::get_well_formed_name(name, inputs_); auto it = inputs_.find(input_name); + if (it == inputs_.end()) { + HOLOSCAN_LOG_ERROR("The input port with name {} is not found", input_name); + return false; + } auto receiver = get_gxf_receiver(it->second); return receiver->size() == 0; } @@ -118,7 +122,7 @@ std::any GXFInputContext::receive_impl(const char* name, bool no_error_message) auto entity = receiver->receive(); if (!entity || entity.value().is_null()) { - return nullptr; // to indicate that there is no data + return kNoReceivedMessage; // to indicate that there is no data } auto message = entity.value().get(); @@ -147,7 +151,8 @@ gxf_context_t GXFOutputContext::gxf_context() const { return nullptr; } -void GXFOutputContext::emit_impl(std::any data, const char* name, OutputType out_type) { +void GXFOutputContext::emit_impl(std::any data, const char* name, OutputType out_type, + const int64_t acq_timestamp) { std::string output_name = holoscan::get_well_formed_name(name, outputs_); auto it = outputs_.find(output_name); @@ -217,7 +222,11 @@ void GXFOutputContext::emit_impl(std::any data, const char* name, OutputType out buffer.value()->set_value(data); // Publish the Entity object. // TODO(gbae): Check error message - static_cast(tx_ptr)->publish(std::move(gxf_entity.value())); + if (acq_timestamp != -1) { + static_cast(tx_ptr)->publish(gxf_entity.value(), acq_timestamp); + } else { + static_cast(tx_ptr)->publish(std::move(gxf_entity.value())); + } break; } case OutputType::kGXFEntity: { @@ -225,7 +234,11 @@ void GXFOutputContext::emit_impl(std::any data, const char* name, OutputType out try { auto gxf_entity = std::any_cast(data); // TODO(gbae): Check error message - static_cast(tx_ptr)->publish(std::move(gxf_entity)); + if (acq_timestamp != -1) { + static_cast(tx_ptr)->publish(gxf_entity, acq_timestamp); + } else { + static_cast(tx_ptr)->publish(std::move(gxf_entity)); + } } catch (const std::bad_any_cast& e) { HOLOSCAN_LOG_ERROR("Unable to cast to gxf::Entity: {}", e.what()); } diff --git a/src/core/gxf/gxf_resource.cpp b/src/core/gxf/gxf_resource.cpp index 4db484b6..7717993e 100644 --- a/src/core/gxf/gxf_resource.cpp +++ b/src/core/gxf/gxf_resource.cpp @@ -33,6 +33,12 @@ namespace holoscan::gxf { GXFResource::GXFResource(const std::string& name, nvidia::gxf::Component* component) { + if (component == nullptr) { + std::string err_msg = + fmt::format("Component pointer is null. Cannot initialize GXFResource '{}'", name); + HOLOSCAN_LOG_ERROR(err_msg); + throw std::runtime_error(err_msg); + } id_ = component->cid(); name_ = name; gxf_context_ = component->context(); @@ -178,7 +184,8 @@ bool GXFResource::handle_dev_id(std::optional& dev_id_value) { // Create an EntityGroup to associate the GPUDevice with this resource std::string entity_group_name = fmt::format("{}_eid{}_dev_id{}_group", name(), gxf_eid_, device_id); - auto entity_group_gid = ::holoscan::gxf::add_entity_group(gxf_context_, entity_group_name); + auto entity_group_gid = + ::holoscan::gxf::add_entity_group(gxf_context_, std::move(entity_group_name)); // Add GPUDevice component to the same entity as this resource // TODO (GXF4): requested an addResource method to handle nvidia::gxf::ResourceBase types diff --git a/src/core/messagelabel.cpp b/src/core/messagelabel.cpp index 59b288b6..3fc8c87d 100644 --- a/src/core/messagelabel.cpp +++ b/src/core/messagelabel.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -59,7 +59,7 @@ void MessageLabel::print_all() { std::string MessageLabel::to_string() const { auto msg_buf = fmt::memory_buffer(); - for (auto it : message_paths) { + for (auto& it : message_paths) { fmt::format_to(std::back_inserter(msg_buf), "{}", to_string(it)); } return fmt::to_string(msg_buf); @@ -67,7 +67,7 @@ std::string MessageLabel::to_string() const { std::string MessageLabel::to_string(MessageLabel::TimestampedPath path) { auto msg_buf = fmt::memory_buffer(); - for (auto it : path) { + for (auto& it : path) { if (!it.operator_ptr) { HOLOSCAN_LOG_ERROR("MessageLabel::to_string - Operator pointer is null"); } else { @@ -132,7 +132,7 @@ MessageLabel::TimestampedPath MessageLabel::get_path(int index) { std::string MessageLabel::get_path_name(int index) { auto pathstring = fmt::memory_buffer(); - for (auto oplabel : message_paths[index]) { + for (auto& oplabel : message_paths[index]) { if (!oplabel.operator_ptr) { HOLOSCAN_LOG_ERROR( "MessageLabel::get_path_name - Operator pointer is null. Path until now: {}.", diff --git a/src/core/operator.cpp b/src/core/operator.cpp index 3595e7e8..7439c4d8 100644 --- a/src/core/operator.cpp +++ b/src/core/operator.cpp @@ -88,9 +88,9 @@ holoscan::MessageLabel Operator::get_consolidated_input_label() { if (this->input_message_labels.size()) { // Flatten the message_paths in input_message_labels into a single MessageLabel - for (auto it : this->input_message_labels) { + for (auto& it : this->input_message_labels) { MessageLabel everyinput = it.second; - for (auto p : everyinput.paths()) { m.add_new_path(p); } + for (auto& p : everyinput.paths()) { m.add_new_path(p); } } } else { // Root operator if (!this->is_root() && !this->is_user_defined_root()) { diff --git a/src/core/resources/gxf/annotated_double_buffer_receiver.cpp b/src/core/resources/gxf/annotated_double_buffer_receiver.cpp index ec03ca86..7fd6ec8a 100644 --- a/src/core/resources/gxf/annotated_double_buffer_receiver.cpp +++ b/src/core/resources/gxf/annotated_double_buffer_receiver.cpp @@ -32,7 +32,7 @@ gxf_result_t AnnotatedDoubleBufferReceiver::receive_abi(gxf_uid_t* uid) { static gxf_tid_t message_label_tid = GxfTidNull(); if (message_label_tid == GxfTidNull()) { - GxfComponentTypeId(context(), "holoscan::MessageLabel", &message_label_tid); + HOLOSCAN_GXF_CALL(GxfComponentTypeId(context(), "holoscan::MessageLabel", &message_label_tid)); } if (gxf::has_component(context(), *uid, message_label_tid, "message_label")) { diff --git a/src/core/resources/gxf/dfft_collector.cpp b/src/core/resources/gxf/dfft_collector.cpp index c1eacf7c..0760d115 100644 --- a/src/core/resources/gxf/dfft_collector.cpp +++ b/src/core/resources/gxf/dfft_collector.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -27,7 +27,10 @@ namespace holoscan { gxf_result_t DFFTCollector::on_execute_abi(gxf_uid_t eid, uint64_t timestamp, gxf_result_t code) { - if (!data_flow_tracker_) { HOLOSCAN_LOG_ERROR("data_flow_tracker_ is null in DFFTCollector."); } + if (!data_flow_tracker_) { + HOLOSCAN_LOG_ERROR("data_flow_tracker_ is null in DFFTCollector."); + return GXF_FAILURE; + } // Get handle to entity auto entity = nvidia::gxf::Entity::Shared(context(), eid); @@ -66,7 +69,7 @@ gxf_result_t DFFTCollector::on_execute_abi(gxf_uid_t eid, uint64_t timestamp, gx } else if (root_ops_.find(codelet_id) != root_ops_.end()) { holoscan::Operator* cur_op = root_ops_[codelet_id]; - for (auto it : cur_op->num_published_messages_map()) { + for (auto& it : cur_op->num_published_messages_map()) { data_flow_tracker_->update_source_messages_number(it.first, it.second); } } diff --git a/src/core/schedulers/gxf/greedy_scheduler.cpp b/src/core/schedulers/gxf/greedy_scheduler.cpp index 49c85e46..9e5665ab 100644 --- a/src/core/schedulers/gxf/greedy_scheduler.cpp +++ b/src/core/schedulers/gxf/greedy_scheduler.cpp @@ -74,7 +74,7 @@ void GreedyScheduler::initialize() { // Find if there is an argument for 'clock' auto has_clock = std::find_if( args().begin(), args().end(), [](const auto& arg) { return (arg.name() == "clock"); }); - // Create the BooleanCondition if there is no argument provided. + // Create the clock if there was no argument provided. if (has_clock == args().end()) { clock_ = frag->make_resource("greedy_scheduler__realtime_clock"); clock_->gxf_cname(clock_->name().c_str()); diff --git a/src/core/services/app_driver/client.cpp b/src/core/services/app_driver/client.cpp index 30d165d7..82499a3e 100644 --- a/src/core/services/app_driver/client.cpp +++ b/src/core/services/app_driver/client.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -50,12 +50,13 @@ bool AppDriverClient::fragment_allocation(const std::string& worker_ip, // Creating AvailableSystemResource and adding it to the request - float cpu_memory = cpuinfo.memory_total / 1024 / 1024 / 1024; /// convert to GiB - float cpu_shared_memory = cpuinfo.shared_memory_total / 1024 / 1024; /// convert to MiB + float cpu_memory = cpuinfo.memory_total / 1024.0 / 1024.0 / 1024.0; /// convert to GiB + float cpu_shared_memory = cpuinfo.shared_memory_total / 1024.0 / 1024.0; /// convert to MiB float gpu_memory = std::numeric_limits::max(); // Calculate the minimum GPU memory among all GPUs for (const auto& gpu : gpuinfo) { - gpu_memory = std::min(gpu_memory, static_cast(gpu.memory_total / 1024 / 1024 / 1024)); + gpu_memory = + std::min(gpu_memory, static_cast(gpu.memory_total / 1024.0 / 1024.0 / 1024.0)); } // Format float value with one decimal place and convert it to string diff --git a/src/core/services/app_driver/service_impl.cpp b/src/core/services/app_driver/service_impl.cpp index 407b2ea5..ff79cdfa 100644 --- a/src/core/services/app_driver/service_impl.cpp +++ b/src/core/services/app_driver/service_impl.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +20,7 @@ #include #include #include +#include #include "holoscan/core/app_driver.hpp" #include "holoscan/logger/logger.hpp" @@ -170,7 +171,7 @@ std::string AppDriverServiceImpl::parse_ip_from_peer(const std::string& peer) { // for IPv6 addresses true); // enclose IPv6 addresses in brackets - return ip; + return std::move(ip); } std::string AppDriverServiceImpl::parse_port_from_peer(const std::string& peer) { diff --git a/src/core/services/app_worker/client.cpp b/src/core/services/app_worker/client.cpp index ce0e8b24..f4ea3a4a 100644 --- a/src/core/services/app_worker/client.cpp +++ b/src/core/services/app_worker/client.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -42,7 +42,7 @@ AppWorkerClient::AppWorkerClient(const std::string& worker_address, // Assign the extracted IP to worker_ip_. We don't need to enclose IPv6 in brackets for this use // case. - worker_ip_ = extracted_ip; + worker_ip_ = std::move(extracted_ip); } const std::string& AppWorkerClient::ip_address() const { diff --git a/src/core/signal_handler.cpp b/src/core/signal_handler.cpp index 0de24f77..bbf96781 100644 --- a/src/core/signal_handler.cpp +++ b/src/core/signal_handler.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -119,9 +119,9 @@ void SignalHandler::install_signal_handler_impl(int signal) { return; } - for (auto& [signal, handler] : old_signal_handlers_) { - HOLOSCAN_LOG_DEBUG("Installing signal handler for signal {}", signal); - sigaction(signal, &signal_handler_, nullptr); // can ignore storing old handler + for (auto& [sig, handler] : old_signal_handlers_) { + HOLOSCAN_LOG_DEBUG("Installing signal handler for signal {}", sig); + sigaction(sig, &signal_handler_, nullptr); // can ignore storing old handler } } diff --git a/src/core/system/cpu_resource_monitor.cpp b/src/core/system/cpu_resource_monitor.cpp index 968d014f..634b2398 100644 --- a/src/core/system/cpu_resource_monitor.cpp +++ b/src/core/system/cpu_resource_monitor.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -81,8 +81,6 @@ static void get_proc_meminfo(uint64_t* stats) { case 3: matched = sscanf(line, "MemAvailable: %lu kB", &stats[2]); break; - default: - break; } if (matched != 1) { HOLOSCAN_LOG_ERROR( @@ -184,9 +182,13 @@ CPUInfo& CPUResourceMonitor::update(CPUInfo& cpu_info, uint64_t metric_flags) { cpu_info.memory_total = mem_info[0] * 1024; cpu_info.memory_free = mem_info[1] * 1024; cpu_info.memory_available = mem_info[2] * 1024; - cpu_info.memory_usage = static_cast(1.0 - (static_cast(mem_info[2]) / - static_cast(mem_info[0]))) * - 100.0f; + double memory_total = static_cast(mem_info[0]); + if (memory_total <= 0) { + throw std::runtime_error( + fmt::format("Invalid cpu_info.memory_total value: {}", memory_total)); + } + cpu_info.memory_usage = + static_cast(1.0 - (static_cast(mem_info[2]) / memory_total)) * 100.0f; } if (metric_flags & CPUMetricFlag::SHARED_MEMORY_USAGE) { diff --git a/src/core/system/gpu_resource_monitor.cpp b/src/core/system/gpu_resource_monitor.cpp index 820b2fd7..e227e096 100644 --- a/src/core/system/gpu_resource_monitor.cpp +++ b/src/core/system/gpu_resource_monitor.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -517,7 +517,7 @@ bool GPUResourceMonitor::bind_cuda_runtime_methods() { cudaDeviceGetPCIBusId = reinterpret_cast(dlsym(cuda_handle_, "cudaDeviceGetPCIBusId")); // for cudaMemGetInfo method - cudaMemGetInfo = reinterpret_cast(dlsym(handle_, "cudaMemGetInfo")); + cudaMemGetInfo = reinterpret_cast(dlsym(cuda_handle_, "cudaMemGetInfo")); if (cudaGetErrorString == nullptr || cudaGetDeviceCount == nullptr || cudaGetDeviceProperties == nullptr || cudaDeviceGetPCIBusId == nullptr || @@ -605,17 +605,19 @@ bool GPUResourceMonitor::init_cuda_runtime() { return true; } -void GPUResourceMonitor::shutdown_nvml() { +void GPUResourceMonitor::shutdown_nvml() noexcept { if (handle_) { - nvml::nvmlReturn_t result = nvmlShutdown(); - if (result != 0) { HOLOSCAN_LOG_ERROR("Could not shutdown NVML"); } + if (nvmlShutdown) { + nvml::nvmlReturn_t result = nvmlShutdown(); + if (result != 0) { HOLOSCAN_LOG_ERROR("Could not shutdown NVML"); } + } dlclose(handle_); handle_ = nullptr; is_cached_ = false; } } -void GPUResourceMonitor::shutdown_cuda_runtime() { +void GPUResourceMonitor::shutdown_cuda_runtime() noexcept { if (cuda_handle_) { dlclose(cuda_handle_); cuda_handle_ = nullptr; diff --git a/src/logger/logger.cpp b/src/logger/logger.cpp index 757e17bb..2eda5444 100644 --- a/src/logger/logger.cpp +++ b/src/logger/logger.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include "common/logger/spdlog_logger.hpp" @@ -89,7 +90,7 @@ void set_log_pattern(std::string pattern) { bool is_overridden_by_env = false; // https://spdlog.docsforge.com/v1.x/0.faq/#colors-do-not-appear-when-using-custom-format - Logger::set_pattern(pattern, &is_overridden_by_env); + Logger::set_pattern(std::move(pattern), &is_overridden_by_env); if (is_overridden_by_env) { const char* env_p = std::getenv("HOLOSCAN_LOG_FORMAT"); @@ -153,7 +154,7 @@ void Logger::set_pattern(std::string pattern, bool* is_overridden_by_env) { if (env_p) { std::string env_pattern; std::string log_pattern = env_p; - env_pattern = get_concrete_log_pattern(log_pattern); + env_pattern = get_concrete_log_pattern(std::move(log_pattern)); if (is_overridden_by_env) { *is_overridden_by_env = true; } diff --git a/src/operators/aja_source/aja_source.cpp b/src/operators/aja_source/aja_source.cpp index b6f74d6b..b1496d51 100644 --- a/src/operators/aja_source/aja_source.cpp +++ b/src/operators/aja_source/aja_source.cpp @@ -461,7 +461,7 @@ void AJASourceOp::compute(InputContext& op_input, OutputContext& op_output, nvidia::gxf::VideoBufferInfo info{width_, height_, video_type.value, - color_planes, + std::move(color_planes), nvidia::gxf::SurfaceLayout::GXF_SURFACE_LAYOUT_PITCH_LINEAR}; if (enable_overlay_) { diff --git a/src/operators/bayer_demosaic/bayer_demosaic.cpp b/src/operators/bayer_demosaic/bayer_demosaic.cpp index 00b03810..15f6e637 100644 --- a/src/operators/bayer_demosaic/bayer_demosaic.cpp +++ b/src/operators/bayer_demosaic/bayer_demosaic.cpp @@ -101,6 +101,13 @@ void BayerDemosaicOp::initialize() { Operator::initialize(); npp_bayer_interp_mode_ = static_cast(bayer_interp_mode_.get()); + if (npp_bayer_interp_mode_ != NPPI_INTER_UNDEFINED) { + // according to NPP docs only NPPI_INTER_UNDEFINED is supported for Bayer demosaic + // https://docs.nvidia.com/cuda/archive/12.2.0/npp/group__image__color__debayer.html + throw std::runtime_error(fmt::format("Unsupported bayer_interp_mode: {}. Must be 0", + static_cast(npp_bayer_interp_mode_))); + } + npp_bayer_grid_pos_ = static_cast(bayer_grid_pos_.get()); } diff --git a/src/operators/format_converter/format_converter.cpp b/src/operators/format_converter/format_converter.cpp index b6ebf8a5..0375874d 100644 --- a/src/operators/format_converter/format_converter.cpp +++ b/src/operators/format_converter/format_converter.cpp @@ -336,9 +336,10 @@ void FormatConverterOp::compute(InputContext& op_input, OutputContext& op_output // If the buffer is in host memory, copy it to a device (GPU) buffer // as needed for the NPP resize/convert operations. - if (in_memory_storage_type == nvidia::gxf::MemoryStorageType::kSystem) { + if (in_memory_storage_type == nvidia::gxf::MemoryStorageType::kSystem || + in_memory_storage_type == nvidia::gxf::MemoryStorageType::kHost) { uint32_t element_size = nvidia::gxf::PrimitiveTypeSize(in_primitive_type); - size_t buffer_size = rows * columns * in_channels * element_size; + size_t buffer_size = static_cast(rows) * columns * in_channels * element_size; if (buffer_size > device_scratch_buffer_->size()) { device_scratch_buffer_->resize( pool.value(), buffer_size, nvidia::gxf::MemoryStorageType::kDevice); @@ -403,9 +404,10 @@ void FormatConverterOp::compute(InputContext& op_input, OutputContext& op_output stride_string)); } - if (in_memory_storage_type == nvidia::gxf::MemoryStorageType::kSystem) { + if (in_memory_storage_type == nvidia::gxf::MemoryStorageType::kSystem || + in_memory_storage_type == nvidia::gxf::MemoryStorageType::kHost) { uint32_t element_size = nvidia::gxf::PrimitiveTypeSize(in_primitive_type); - size_t buffer_size = rows * columns * in_channels * element_size; + size_t buffer_size = static_cast(rows) * columns * in_channels * element_size; if (buffer_size > device_scratch_buffer_->size()) { device_scratch_buffer_->resize( @@ -577,7 +579,7 @@ nvidia::gxf::Expected FormatConverterOp::resizeImage( auto pool = nvidia::gxf::Handle::Create(frag->executor().context(), pool_->gxf_cid()); - uint64_t buffer_size = resize_width * resize_height * channels; + uint64_t buffer_size = static_cast(resize_width) * resize_height * channels; resize_buffer_->resize(pool.value(), buffer_size, nvidia::gxf::MemoryStorageType::kDevice); } @@ -752,7 +754,7 @@ void FormatConverterOp::convertTensorFormat( auto pool = nvidia::gxf::Handle::Create(frag->executor().context(), pool_->gxf_cid()); - uint64_t buffer_size = rows * columns * 3; // 4 channels -> 3 channels + uint64_t buffer_size = static_cast(rows) * columns * 3; // 4 channels -> 3 channels channel_buffer_->resize(pool.value(), buffer_size, nvidia::gxf::MemoryStorageType::kDevice); } diff --git a/src/operators/inference/inference.cpp b/src/operators/inference/inference.cpp index 6b6e7486..4dfb5c9a 100644 --- a/src/operators/inference/inference.cpp +++ b/src/operators/inference/inference.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include "holoscan/core/execution_context.hpp" @@ -49,7 +50,7 @@ struct YAML::convert { for (YAML::const_iterator it = node.begin(); it != node.end(); ++it) { std::string key = it->first.as(); std::string value = it->second.as(); - datamap.insert(key, value); + datamap.insert(key, std::move(value)); } } catch (const std::exception& e) { HOLOSCAN_LOG_ERROR(e.what()); @@ -96,7 +97,7 @@ struct YAML::convert { key); HOLOSCAN_LOG_WARN("Single I/O per model supported in backward compatibility mode."); std::string value = it->second.as(); - datavmap.insert(key, {value}); + datavmap.insert(key, {std::move(value)}); } break; case YAML::NodeType::Sequence: { std::vector value = it->second.as>(); @@ -139,6 +140,11 @@ void InferenceOp::setup(OperatorSpec& spec) { "Model Keyword with associated frame execution delay", "Frame delay for model inference.", DataMap()); + spec.param(activation_map_, + "activation_map", + "Model Keyword with associated model inference activation", + "Activation of model inference (1 = active, 0 = inactive).", + DataMap()); spec.param(pre_processor_map_, "pre_processor_map", "Pre processor setting per model", @@ -199,6 +205,7 @@ void InferenceOp::start() { inference_map_.get().get_map(), device_map_.get().get_map(), temporal_map_.get().get_map(), + activation_map_.get().get_map(), is_engine_path_.get(), infer_on_cpu_.get(), parallel_inference_.get(), @@ -250,8 +257,10 @@ void InferenceOp::compute(InputContext& op_input, OutputContext& op_output, // Execute inference and populate output buffer in inference specifications HoloInfer::TimePoint s_time, e_time; HoloInfer::timer_init(s_time); - auto status = holoscan_infer_context_->execute_inference(inference_specs_->data_per_tensor_, - inference_specs_->output_per_model_); + + inference_specs_->set_activation_map(activation_map_.get().get_map()); + + auto status = holoscan_infer_context_->execute_inference(inference_specs_); HoloInfer::timer_init(e_time); HoloInfer::timer_check(s_time, e_time, "Inference Operator: Inference execution"); if (status.get_code() != HoloInfer::holoinfer_code::H_SUCCESS) { diff --git a/src/operators/inference_processor/inference_processor.cpp b/src/operators/inference_processor/inference_processor.cpp index 5c6f9723..643363fb 100644 --- a/src/operators/inference_processor/inference_processor.cpp +++ b/src/operators/inference_processor/inference_processor.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,6 +19,7 @@ #include #include +#include #include #include "holoscan/core/execution_context.hpp" @@ -89,7 +90,7 @@ struct YAML::convert { HOLOSCAN_LOG_INFO( "Converting mappings for tensor {} to vector for backward compatibility.", key); std::string value = it->second.as(); - datavmap.insert(key, {value}); + datavmap.insert(key, {std::move(value)}); } break; case YAML::NodeType::Sequence: { std::vector value = it->second.as>(); diff --git a/src/operators/v4l2_video_capture/v4l2_video_capture.cpp b/src/operators/v4l2_video_capture/v4l2_video_capture.cpp index 2d1bfe3c..2c2e5192 100644 --- a/src/operators/v4l2_video_capture/v4l2_video_capture.cpp +++ b/src/operators/v4l2_video_capture/v4l2_video_capture.cpp @@ -165,10 +165,10 @@ void V4L2VideoCaptureOp::compute(InputContext& op_input, OutputContext& op_outpu buf.length, nvidia::gxf::MemoryStorageType::kHost, read_buf.ptr, - [buffer = buf, fd = fd_, device = device_](void*) mutable { + [buffer = buf, fd = fd_, &device_ = device_](void*) mutable { if (ioctl(fd, VIDIOC_QBUF, &buffer) < 0) { - throw std::runtime_error( - fmt::format("Failed to queue buffer {} on {}", buffer.index, device.get().c_str())); + throw std::runtime_error(fmt::format( + "Failed to queue buffer {} on {}", buffer.index, device_.get().c_str())); } return nvidia::gxf::Success; }); @@ -208,7 +208,9 @@ void V4L2VideoCaptureOp::v4l2_initialize() { struct v4l2_capability caps; CLEAR(caps); - ioctl(fd_, VIDIOC_QUERYCAP, &caps); + if (ioctl(fd_, VIDIOC_QUERYCAP, &caps)) { + throw std::runtime_error("ioctl VIDIOC_QUERYCAP failed"); + } if (!(caps.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { throw std::runtime_error("No V4l2 Video capture node"); } diff --git a/src/operators/video_stream_recorder/video_stream_recorder.cpp b/src/operators/video_stream_recorder/video_stream_recorder.cpp index 53836f1e..0b3402d6 100644 --- a/src/operators/video_stream_recorder/video_stream_recorder.cpp +++ b/src/operators/video_stream_recorder/video_stream_recorder.cpp @@ -19,6 +19,7 @@ #include #include +#include #include "gxf/core/expected.hpp" #include "gxf/serialization/entity_serializer.hpp" @@ -109,14 +110,18 @@ VideoStreamRecorderOp::~VideoStreamRecorderOp() { nvidia::gxf::Expected result = binary_file_stream_.close(); if (!result) { auto code = nvidia::gxf::ToResultCode(result); - HOLOSCAN_LOG_ERROR("Failed to close binary_file_stream_ with code: {}", code); + try { + HOLOSCAN_LOG_ERROR("Failed to close binary_file_stream_ with code: {}", code); + } catch (std::exception& e) {} } // Close index file stream result = index_file_stream_.close(); if (!result) { auto code = nvidia::gxf::ToResultCode(result); - HOLOSCAN_LOG_ERROR("Failed to close index_file_stream_ with code: {}", code); + try { + HOLOSCAN_LOG_ERROR("Failed to close index_file_stream_ with code: {}", code); + } catch (std::exception& e) {} } } @@ -154,7 +159,7 @@ void VideoStreamRecorderOp::compute(InputContext& op_input, OutputContext& op_ou auto entity_serializer = nvidia::gxf::Handle::Create( context.context(), vs_serializer->gxf_cid()); nvidia::gxf::Expected size = - entity_serializer.value()->serializeEntity(entity, &binary_file_stream_); + entity_serializer.value()->serializeEntity(std::move(entity), &binary_file_stream_); if (!size) { auto code = nvidia::gxf::ToResultCode(size); throw std::runtime_error(fmt::format("Failed to serialize entity with code {}", code)); diff --git a/src/utils/cuda_stream_handler.cpp b/src/utils/cuda_stream_handler.cpp index 92070aec..9f84f523 100644 --- a/src/utils/cuda_stream_handler.cpp +++ b/src/utils/cuda_stream_handler.cpp @@ -34,7 +34,9 @@ CudaStreamHandler::~CudaStreamHandler() { for (auto&& event : cuda_events_) { const cudaError_t result = cudaEventDestroy(event); if (cudaSuccess != result) { - HOLOSCAN_LOG_ERROR("Failed to destroy CUDA event: %s", cudaGetErrorString(result)); + try { + HOLOSCAN_LOG_ERROR("Failed to destroy CUDA event: %s", cudaGetErrorString(result)); + } catch (std::exception& e) {} } } cuda_events_.clear(); diff --git a/src/utils/holoinfer_utils.cpp b/src/utils/holoinfer_utils.cpp index fffa56d4..731dc871 100644 --- a/src/utils/holoinfer_utils.cpp +++ b/src/utils/holoinfer_utils.cpp @@ -121,6 +121,7 @@ gxf_result_t get_data_per_model(InputContext& op_input, const std::vector>("receivers").value(); for (unsigned int i = 0; i < in_tensors.size(); ++i) { // nvidia::gxf::Handle in_tensor; + HOLOSCAN_LOG_DEBUG("Extracting data from tensor {}", in_tensors[i]); std::shared_ptr in_tensor; cudaStream_t cstream = 0; for (unsigned int j = 0; j < messages.size(); ++j) { @@ -151,10 +152,10 @@ gxf_result_t get_data_per_model(InputContext& op_input, const std::vectoradd_fragment(fragment); // First call to graph creates a FlowGraph object - // F.graph() returns a pointer to the abstract Graph base class so use + // F.graph() returns a reference to the abstract Graph base class so use // static_cast here - FragmentFlowGraph G = static_cast(app->fragment_graph()); + FragmentFlowGraph& G = static_cast(app->fragment_graph()); // verify that the operator was added to the graph auto nodes = G.get_nodes(); @@ -168,9 +168,9 @@ TEST(Application, TestAddFlow) { app->add_flow(fragment1, fragment2, {{"blur_image", "sharpen_image"}}); // First call to graph creates a FlowGraph object - // F.graph() returns a pointer to the abstract Graph base class so use + // F.graph() returns a reference to the abstract Graph base class so use // static_cast here - FragmentFlowGraph G = static_cast(app->fragment_graph()); + FragmentFlowGraph& G = static_cast(app->fragment_graph()); // verify that the fragments and edges were added to the graph auto nodes = G.get_nodes(); diff --git a/tests/core/arg.cpp b/tests/core/arg.cpp index 4f3d2137..1395bc91 100644 --- a/tests/core/arg.cpp +++ b/tests/core/arg.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -29,8 +29,12 @@ #include #include "../config.hpp" +#include "holoscan/core/conditions/gxf/boolean.hpp" #include "holoscan/core/fragment.hpp" +#include "holoscan/core/io_spec.hpp" +#include "holoscan/core/operator_spec.hpp" #include "holoscan/core/parameter.hpp" +#include "holoscan/core/resources/gxf/unbounded_allocator.hpp" using namespace std::string_literals; @@ -217,16 +221,26 @@ TEST(Arg, TestArgHandleType) { check_arg_types(A, ArgElementType::kHandle, ArgContainerType::kVector); } -TEST(Arg, TestOtherEnums) { - // TODO: add parametrized test cases above for these. +TEST(Arg, TestCondition) { + Fragment F; + auto bool_cond = F.make_condition("boolean"s, Arg{"enable_tick", true}); + Arg condition_arg{"bool cond", bool_cond}; + check_arg_types(condition_arg, ArgElementType::kCondition, ArgContainerType::kNative); +} + +TEST(Arg, TestResource) { + Fragment F; + auto allocator = F.make_resource("unbounded"); + Arg resource_arg{"allocator", allocator}; + check_arg_types(resource_arg, ArgElementType::kResource, ArgContainerType::kNative); +} - // For now, just check that enum entries exist for: - // kIOSpec (holoscan::IOSpec*) - // kCondition (std::shared_ptr) - // kResource (std::shared_ptr) - ArgElementType T = ArgElementType::kIOSpec; - T = ArgElementType::kCondition; - T = ArgElementType::kResource; +TEST(Arg, TestIOSpec) { + OperatorSpec op_spec = OperatorSpec(); + IOSpec spec = + IOSpec(&op_spec, std::string("a"), IOSpec::IOType::kInput, &typeid(holoscan::gxf::Entity)); + Arg spec_arg{"iospec", &spec}; + check_arg_types(spec_arg, ArgElementType::kIOSpec, ArgContainerType::kNative); } TEST(Arg, TestArgDescription) { diff --git a/tests/core/argument_setter.cpp b/tests/core/argument_setter.cpp index c2d0fc87..e2792303 100644 --- a/tests/core/argument_setter.cpp +++ b/tests/core/argument_setter.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,8 +33,12 @@ namespace holoscan { TEST(ArgumentSetter, TestArgumentSetterInstance) { ArgumentSetter instance = ArgumentSetter::get_instance(); + // call static ensure_type method ArgumentSetter::ensure_type; - // ArgumentSetter::ensure_type>; // will fail to compile + + // get the setter corresponding to float + float f = 1.0; + auto func = instance.get_argument_setter(typeid(f)); } } // namespace holoscan diff --git a/tests/core/condition.cpp b/tests/core/condition.cpp index cde5aa3e..b30545a1 100644 --- a/tests/core/condition.cpp +++ b/tests/core/condition.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -34,8 +34,8 @@ TEST(Condition, TestConditionName) { EXPECT_EQ(C.name(), ""); C.name("my_name"); - Fragment* f; - C.fragment(f); + Fragment f; + C.fragment(&f); EXPECT_EQ(C.name(), "my_name"); } @@ -45,41 +45,40 @@ TEST(Condition, TestConditionFragment) { Condition C = Condition(); EXPECT_EQ(C.fragment(), nullptr); - Fragment* f; - C.fragment(f); - EXPECT_EQ(C.fragment(), f); + Fragment f; + C.fragment(&f); + EXPECT_EQ(C.fragment(), &f); } TEST(Condition, TestConditionSpec) { // initialization Condition C = Condition(); - Fragment* f; + Fragment f; - C.spec(std::make_shared(f)); + C.spec(std::make_shared(&f)); } TEST(Condition, TestConditionChainedAssignments) { // initialization Condition C; - Fragment *f1, *f2, *f3; + Fragment f1, f2, f3; - C.fragment(f1).name("name1"); - EXPECT_EQ(C.fragment(), f1); + C.fragment(&f1).name("name1"); + EXPECT_EQ(C.fragment(), &f1); EXPECT_EQ(C.name(), "name1"); - C.name("name2").fragment(f2); - EXPECT_EQ(C.fragment(), f2); + C.name("name2").fragment(&f2); + EXPECT_EQ(C.fragment(), &f2); EXPECT_EQ(C.name(), "name2"); - C.spec(std::make_shared(f3)).name("name3").fragment(f3); - EXPECT_EQ(C.fragment(), f3); + C.spec(std::make_shared(&f3)).name("name3").fragment(&f3); + EXPECT_EQ(C.fragment(), &f3); EXPECT_EQ(C.name(), "name3"); } TEST(Condition, TestConditionSpecFragmentNull) { // initialization Condition C = Condition(); - Fragment* f; // component spec can take in nullptr fragment C.spec(std::make_shared()); diff --git a/tests/core/condition_classes.cpp b/tests/core/condition_classes.cpp index e8420c78..95228b76 100644 --- a/tests/core/condition_classes.cpp +++ b/tests/core/condition_classes.cpp @@ -35,6 +35,7 @@ #include "holoscan/core/conditions/gxf/downstream_affordable.hpp" #include "holoscan/core/conditions/gxf/periodic.hpp" #include "holoscan/core/conditions/gxf/message_available.hpp" +#include "holoscan/core/conditions/gxf/expiring_message.hpp" #include "holoscan/core/config.hpp" #include "holoscan/core/executor.hpp" #include "holoscan/core/graph.hpp" @@ -207,6 +208,19 @@ TEST(ConditionClasses, TestMessageAvailableCondition) { EXPECT_TRUE(condition->description().find("name: " + name) != std::string::npos); } +TEST(ConditionClasses, TestExpiringMessageAvailableCondition) { + Fragment F; + const std::string name{"expiring-message-available-condition"}; + ArgList arglist{Arg{"min_size", 1L}, Arg{"front_stage_max_size", 2L}}; + auto condition = F.make_condition(name, arglist); + EXPECT_EQ(condition->name(), name); + EXPECT_EQ(typeid(condition), + typeid(std::make_shared(arglist))); + EXPECT_EQ(std::string(condition->gxf_typename()), + "nvidia::gxf::ExpiringMessageAvailableSchedulingTerm"s); + EXPECT_TRUE(condition->description().find("name: " + name) != std::string::npos); +} + TEST(ConditionClasses, TestMessageAvailableConditionDefaultConstructor) { Fragment F; auto condition = F.make_condition(); @@ -351,7 +365,8 @@ TEST_F(ConditionClassesWithGXFContext, TestPeriodicConditionInitializeWithArg) { const GxfEntityCreateInfo entity_create_info = {"dummy_entity", GXF_ENTITY_CREATE_PROGRAM_BIT}; gxf_uid_t eid = 0; gxf_result_t code; - GxfCreateEntity(context, &entity_create_info, &eid); + code = GxfCreateEntity(context, &entity_create_info, &eid); + ASSERT_EQ(code, GXF_SUCCESS); std::vector> conditions; for (auto& pair : pairs) { diff --git a/tests/core/dataflow_tracker.cpp b/tests/core/dataflow_tracker.cpp index 4d194168..0aa0d5cd 100644 --- a/tests/core/dataflow_tracker.cpp +++ b/tests/core/dataflow_tracker.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -248,7 +248,7 @@ TEST(DataFlowTracker, GetPathStrings) { auto paths = tracker.get_path_strings(); - for (auto path : paths) { + for (const auto& path : paths) { ASSERT_TRUE(std::find(path_strings.begin(), path_strings.end(), path) != path_strings.end()); } } diff --git a/tests/core/fragment.cpp b/tests/core/fragment.cpp index 268234d4..4489a7ee 100644 --- a/tests/core/fragment.cpp +++ b/tests/core/fragment.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -84,7 +84,7 @@ TEST(Fragment, TestFragmentConfig) { const std::string config_file = test_config.get_test_data_file("app_config.yaml"); F.config(config_file); - Config C = F.config(); + Config& C = F.config(); ASSERT_TRUE(C.config_file() == config_file); ArgList args = F.from_config("format_converter_replayer"s); @@ -112,7 +112,7 @@ TEST(Fragment, TestFragmentConfigNestedArgs) { const std::string config_file = test_config.get_test_data_file("app_config.yaml"); F.config(config_file); - Config C = F.config(); + Config& C = F.config(); ASSERT_TRUE(C.config_file() == config_file); // can directly access a specific argument under the "aja" section @@ -126,7 +126,7 @@ TEST(Fragment, TestFragmentConfigNestedArgs) { TEST(Fragment, TestConfigUninitializedWarning) { Fragment F; - Config C = F.config(); + Config& C = F.config(); EXPECT_EQ(C.config_file(), ""); } @@ -138,7 +138,7 @@ TEST(Fragment, TestFragmentFromConfigNonexistentKey) { const std::string config_file = test_config.get_test_data_file("app_config.yaml"); F.config(config_file); - Config C = F.config(); + Config& C = F.config(); ASSERT_TRUE(C.config_file() == config_file); ArgList args = F.from_config("non-existent"s); EXPECT_EQ(args.size(), 0); @@ -169,9 +169,9 @@ TEST(Fragment, TestFragmentGraph) { Fragment F; // First call to graph creates a FlowGraph object - // F.graph() returns a pointer to the abstract Graph base class so use + // F.graph() returns a reference to the abstract Graph base class so use // static_cast here - OperatorFlowGraph G = static_cast(F.graph()); + OperatorFlowGraph& G = static_cast(F.graph()); } TEST(Fragment, TestAddOperator) { @@ -181,9 +181,9 @@ TEST(Fragment, TestAddOperator) { F.add_operator(op); // First call to graph creates a FlowGraph object - // F.graph() returns a pointer to the abstract Graph base class so use + // F.graph() returns a reference to the abstract Graph base class so use // static_cast here - OperatorFlowGraph G = static_cast(F.graph()); + OperatorFlowGraph& G = static_cast(F.graph()); // verify that the operator was added to the graph auto nodes = G.get_nodes(); @@ -212,9 +212,9 @@ TEST(Fragment, TestAddFlow) { F.add_flow(tx, rx, {{"out", "in"}}); // First call to graph creates a FlowGraph object - // F.graph() returns a pointer to the abstract Graph base class so use + // F.graph() returns a reference to the abstract Graph base class so use // static_cast here - OperatorFlowGraph G = static_cast(F.graph()); + OperatorFlowGraph& G = static_cast(F.graph()); // verify that the operators and edges were added to the graph auto nodes = G.get_nodes(); @@ -243,7 +243,7 @@ TEST(Fragment, TestOperatorOrder) { F.add_flow(tx, rx, {{"out", "in"}}); F.add_flow(tx2, rx2, {{"out", "in"}}); - OperatorFlowGraph G = static_cast(F.graph()); + OperatorFlowGraph& G = static_cast(F.graph()); auto order = G.get_nodes(); const std::vector expected_order = {"tx2", "tx", "rx", "rx2"}; @@ -256,7 +256,7 @@ TEST(Fragment, TestFragmentExecutor) { Fragment F; // First call to executor generates an Executor object - Executor E = F.executor(); + Executor& E = F.executor(); // this fragment is associated with the executor that was created EXPECT_EQ(E.fragment(), &F); @@ -271,7 +271,6 @@ TEST(Fragment, TestFragmentMoveAssignment) { // can only move assign (copy assignment operator has been deleted) F = std::move(G); EXPECT_EQ(F.name(), "G"); - EXPECT_EQ(G.name(), ""); } // Fragment::make_condition is tested elsewhere in condition_classes.cpp diff --git a/tests/core/resource.cpp b/tests/core/resource.cpp index f3df16d6..8cb484a6 100644 --- a/tests/core/resource.cpp +++ b/tests/core/resource.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022-2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -35,8 +35,8 @@ TEST(Resource, TestResourceName) { EXPECT_EQ(R.name(), ""); R.name("my_name"); - Fragment* f; - R.fragment(f); + Fragment f; + R.fragment(&f); EXPECT_EQ(R.name(), "my_name"); } @@ -46,23 +46,23 @@ TEST(Resource, TestResourceFragment) { Resource R = Resource(); EXPECT_EQ(R.fragment(), nullptr); - Fragment* f; - R.fragment(f); - EXPECT_EQ(R.fragment(), f); + Fragment f; + R.fragment(&f); + EXPECT_EQ(R.fragment(), &f); } TEST(Resource, TestResourceSpec) { // initialization Resource R = Resource(); - Fragment* f; + Fragment f; - R.spec(std::make_shared(f)); + R.spec(std::make_shared(&f)); } TEST(Resource, TestResourceSpecFragmentNull) { // initialization Resource R = Resource(); - Fragment* f; + Fragment f; // component spec can take in nullptr fragment R.spec(std::make_shared()); @@ -71,18 +71,18 @@ TEST(Resource, TestResourceSpecFragmentNull) { TEST(Resource, TestResourceChainedAssignments) { // initialization Resource R; - Fragment *f1, *f2, *f3; + Fragment f1, f2, f3; - R.fragment(f1).name("name1"); - EXPECT_EQ(R.fragment(), f1); + R.fragment(&f1).name("name1"); + EXPECT_EQ(R.fragment(), &f1); EXPECT_EQ(R.name(), "name1"); - R.name("name2").fragment(f2); - EXPECT_EQ(R.fragment(), f2); + R.name("name2").fragment(&f2); + EXPECT_EQ(R.fragment(), &f2); EXPECT_EQ(R.name(), "name2"); - R.spec(std::make_shared(f3)).name("name3").fragment(f3); - EXPECT_EQ(R.fragment(), f3); + R.spec(std::make_shared(&f3)).name("name3").fragment(&f3); + EXPECT_EQ(R.fragment(), &f3); EXPECT_EQ(R.name(), "name3"); } diff --git a/tests/data/loading_gxf_extension.yaml b/tests/data/loading_gxf_extension.yaml new file mode 100644 index 00000000..a1735125 --- /dev/null +++ b/tests/data/loading_gxf_extension.yaml @@ -0,0 +1,19 @@ +%YAML 1.2 +# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +--- +extensions: +- libgxf_cuda.so +- libgxf_sample.so diff --git a/tests/holoinfer/inference/test_core.cpp b/tests/holoinfer/inference/test_core.cpp index 3df1307b..754c2436 100644 --- a/tests/holoinfer/inference/test_core.cpp +++ b/tests/holoinfer/inference/test_core.cpp @@ -117,6 +117,7 @@ void HoloInferTests::setup_specifications() { inference_map, device_map, temporal_map, + activation_map, is_engine_path, infer_on_cpu, parallel_inference, @@ -145,7 +146,7 @@ HoloInfer::InferStatus HoloInferTests::prepare_for_inference() { auto status = create_specifications(); - for (const auto td : in_tensor_dimensions) { + for (const auto& td : in_tensor_dimensions) { auto db = std::make_shared(); size_t buffer_size = std::accumulate(td.second.begin(), td.second.end(), 1, std::multiplies()); @@ -163,8 +164,7 @@ HoloInfer::InferStatus HoloInferTests::do_inference() { try { if (!holoscan_infer_context_) { return status; } - return holoscan_infer_context_->execute_inference(inference_specs_->data_per_tensor_, - inference_specs_->output_per_model_); + return holoscan_infer_context_->execute_inference(inference_specs_); } catch (...) { std::cout << "Exception occurred in inference.\n"; return status; diff --git a/tests/holoinfer/inference/test_core.hpp b/tests/holoinfer/inference/test_core.hpp index 48ef8e3b..37651fd7 100644 --- a/tests/holoinfer/inference/test_core.hpp +++ b/tests/holoinfer/inference/test_core.hpp @@ -71,6 +71,7 @@ class HoloInferTests { std::map device_map = {{"model_1", "0"}, {"model_2", "0"}}; std::map temporal_map = {{"model_1", "1"}, {"model_2", "1"}}; + std::map activation_map = {{"model_1", "1"}, {"model_2", "1"}}; std::map backend_map; diff --git a/tests/operators/operator_classes.cpp b/tests/operators/operator_classes.cpp index b8f78621..06944625 100644 --- a/tests/operators/operator_classes.cpp +++ b/tests/operators/operator_classes.cpp @@ -162,7 +162,9 @@ TEST_F(OperatorClassesWithGXFContext, TestVideoStreamRecorderOp) { TEST_F(OperatorClassesWithGXFContext, TestVideoStreamReplayerOp) { const std::string name{"replayer"}; - const std::string sample_data_path = std::string(std::getenv("HOLOSCAN_INPUT_PATH")); + auto in_path = std::getenv("HOLOSCAN_INPUT_PATH"); + if (!in_path) { GTEST_SKIP() << "Skipping test due to undefined HOLOSCAN_INPUT_PATH env var"; } + const std::string sample_data_path = std::string(in_path); ArgList args{ Arg{"directory", sample_data_path + "/racerx"s}, Arg{"basename", "racerx"s}, diff --git a/tests/operators/segmentation_postprocessor/test_postprocessor.cpp b/tests/operators/segmentation_postprocessor/test_postprocessor.cpp index 6d65ff5a..08f606de 100644 --- a/tests/operators/segmentation_postprocessor/test_postprocessor.cpp +++ b/tests/operators/segmentation_postprocessor/test_postprocessor.cpp @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-FileCopyrightText: Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -123,14 +123,50 @@ static const uint8_t kArgmaxOutputData[] = { 4, 1, 0, 2, 2, 1, 0, 3, 3, 4, 2, 2, 4, 0, 0, 0, 2, 4, 4, 2, 0, 0, 2, 2, 3, 0, 0, 2, 2, 2, 1, 3, 2, 1, 0, 4, 3, 1, 0, 4, 2, 4, 4, 1, 4, 3, 3, 0, 4, 2, 4, 1, 1, 1, 3, 1, 4, 1, 3, 0, 2, 0}; -TEST(SegmentationPostprocessor, Argmax) { - holoscan::ops::segmentation_postprocessor::Shape shape; - shape.height = 19; - shape.width = 10; - shape.channels = 5; +// The fixture for testing the Segmentation Postprocessor +class SegmentationPostprocessorTest : public testing::Test { + protected: + void SetUp() override { + shape.height = 19; + shape.width = 10; + shape.channels = 5; + + // Allocate device memory needed by the tests + cuda_status = cudaMalloc(reinterpret_cast(&device_input_data), input_data_size); + if (cuda_status == cudaSuccess) { + cuda_status = cudaMalloc(reinterpret_cast(&device_output_data), output_data_size); + } + // Copy input data to the device + if (cuda_status == cudaSuccess) { + cuda_status = cudaMemcpy(device_input_data, kArgmaxInputData, input_data_size, + cudaMemcpyHostToDevice); + } + } + + void TearDown() override { + // Free any device memory allocated during SetUp + if (device_input_data) { + cudaFree(device_input_data); + } + if (device_output_data) { + cudaFree(device_output_data); + } + } + + // Any members defined here can be directly accessed from the test case + holoscan::ops::segmentation_postprocessor::Shape shape; const uint32_t input_data_size = sizeof(kArgmaxInputData); const uint32_t output_data_size = sizeof(kArgmaxOutputData); + cudaError_t cuda_status; + float* device_input_data = nullptr; + holoscan::ops::segmentation_postprocessor::output_type_t* device_output_data = nullptr; +}; + + +TEST_F(SegmentationPostprocessorTest, Argmax) { + // check that no CUDA errors occurred during SetUp() + ASSERT_EQ(cudaSuccess, cuda_status); ASSERT_EQ(input_data_size, shape.height * shape.width * shape.channels * sizeof(float)); ASSERT_EQ(output_data_size, @@ -139,15 +175,6 @@ TEST(SegmentationPostprocessor, Argmax) { holoscan::ops::segmentation_postprocessor::output_type_t host_output_data[output_data_size] = {}; - float* device_input_data = nullptr; - holoscan::ops::segmentation_postprocessor::output_type_t* device_output_data = nullptr; - ASSERT_EQ(cudaSuccess, cudaMalloc(reinterpret_cast(&device_input_data), input_data_size)); - ASSERT_EQ(cudaSuccess, - cudaMalloc(reinterpret_cast(&device_output_data), output_data_size)); - - ASSERT_EQ( - cudaSuccess, - cudaMemcpy(device_input_data, kArgmaxInputData, input_data_size, cudaMemcpyHostToDevice)); ASSERT_EQ(cudaSuccess, cudaMemset(device_output_data, 0, output_data_size)); holoscan::ops::segmentation_postprocessor::cuda_postprocess( @@ -161,9 +188,6 @@ TEST(SegmentationPostprocessor, Argmax) { cudaSuccess, cudaMemcpy(host_output_data, device_output_data, output_data_size, cudaMemcpyDeviceToHost)); - ASSERT_EQ(cudaSuccess, cudaFree(device_input_data)); - ASSERT_EQ(cudaSuccess, cudaFree(device_output_data)); - for (uint32_t i = 0; i < output_data_size / sizeof(host_output_data[0]); i++) { ASSERT_EQ(kArgmaxOutputData[i], host_output_data[i]) << "Failed at index: " << i; } diff --git a/tests/system/distributed/distributed_app.cpp b/tests/system/distributed/distributed_app.cpp index f68a6775..207a0071 100644 --- a/tests/system/distributed/distributed_app.cpp +++ b/tests/system/distributed/distributed_app.cpp @@ -54,7 +54,8 @@ TEST(DistributedApp, TestTwoMultiInputsOutputsFragmentsApp) { app->run(); std::string log_output = testing::internal::GetCapturedStderr(); - EXPECT_TRUE(log_output.find("received count: 10") != std::string::npos); + EXPECT_TRUE(log_output.find("received count: 10") != std::string::npos) << "===LogMessage===\n" + << log_output; } TEST(DistributedApp, TestTwoMultipleSingleOutputOperatorsApp) { diff --git a/tests/system/distributed/ucx_message_serialization_ping_app.cpp b/tests/system/distributed/ucx_message_serialization_ping_app.cpp index d926a165..9e278070 100644 --- a/tests/system/distributed/ucx_message_serialization_ping_app.cpp +++ b/tests/system/distributed/ucx_message_serialization_ping_app.cpp @@ -147,8 +147,8 @@ TEST_P(UcxMessageTypeParmeterizedTestFixture, TestUcxMessageSerializationApp) { if (message_type == MessageType::VEC_DOUBLE_LARGE) { // message is larger than kDefaultUcxSerializationBufferSize // set HOLOSCAN_UCX_SERIALIZATION_BUFFER_SIZE to a value large enough to hold the data - const char* buffer_size = std::to_string(10 * 1024 * 1024).c_str(); - setenv("HOLOSCAN_UCX_SERIALIZATION_BUFFER_SIZE", buffer_size, 1); + std::string buffer_size = std::to_string(10 * 1024 * 1024); + setenv("HOLOSCAN_UCX_SERIALIZATION_BUFFER_SIZE", buffer_size.c_str(), 1); } HOLOSCAN_LOG_INFO("Creating UcxMessageSerializationApp for type: {}", diff --git a/tests/system/loading_gxf_extension.cpp b/tests/system/loading_gxf_extension.cpp new file mode 100644 index 00000000..1cf205cf --- /dev/null +++ b/tests/system/loading_gxf_extension.cpp @@ -0,0 +1,175 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include +#include + +#include "../config.hpp" + +static HoloscanTestConfig test_config; + +namespace holoscan { + +// Do not pollute holoscan namespace with utility classes +namespace { + +/////////////////////////////////////////////////////////////////////////////// +// Utility Resources/Operators +/////////////////////////////////////////////////////////////////////////////// + +HOLOSCAN_WRAP_GXF_COMPONENT_AS_RESOURCE(MyCudaStreamPool, "nvidia::gxf::CudaStreamPool") +HOLOSCAN_WRAP_GXF_CODELET_AS_OPERATOR(HelloWorldOp, "nvidia::gxf::HelloWorld") + +/////////////////////////////////////////////////////////////////////////////// +// Utility Applications +/////////////////////////////////////////////////////////////////////////////// + +class LoadInsideComposeApp : public holoscan::Application { + public: + using Application::Application; + + void compose() override { + auto extension_manager = executor().extension_manager(); + extension_manager->load_extension("libgxf_cuda.so"); + extension_manager->load_extension("libgxf_sample.so"); + + auto pool = make_resource("pool"); + auto hello = make_operator("hello", make_condition(10)); + add_operator(hello); + } +}; + +class DummyApp : public holoscan::Application { + public: + using Application::Application; + + void compose() override { + auto pool = make_resource("pool"); + auto hello = make_operator("hello", make_condition(10)); + add_operator(hello); + } +}; + +} // namespace + +/////////////////////////////////////////////////////////////////////////////// +// Tests +/////////////////////////////////////////////////////////////////////////////// + +TEST(Extensions, LoadInsideComposeMethod) { + auto app = make_application(); + + // Capture stderr output to check for specific error messages + testing::internal::CaptureStderr(); + + app->run(); + + std::string log_output = testing::internal::GetCapturedStderr(); + // Check that log_output has 10 instances of "Hello world" + auto pos = log_output.find("Hello world"); + int count = 0; + while (pos != std::string::npos) { + count++; + pos = log_output.find("Hello world", pos + 1); + } + EXPECT_EQ(count, 10) << "Expected to find 10 instances of 'Hello world' in log output, but found " + << count << ".\nLog output:\n" + << log_output; +} + +TEST(Extensions, LoadOutsideApp) { + auto app = make_application(); + + // Load the extensions outside of the application before calling run() method + auto& executor = app->executor(); + auto extension_manager = executor.extension_manager(); + extension_manager->load_extension("libgxf_cuda.so"); + extension_manager->load_extension("libgxf_sample.so"); + + // Capture stderr output to check for specific error messages + testing::internal::CaptureStderr(); + + app->run(); + + std::string log_output = testing::internal::GetCapturedStderr(); + // Check that log_output has 10 instances of "Hello world" + auto pos = log_output.find("Hello world"); + int count = 0; + while (pos != std::string::npos) { + count++; + pos = log_output.find("Hello world", pos + 1); + } + EXPECT_EQ(count, 10) << "Expected to find 10 instances of 'Hello world' in log output, but found " + << count << ".\nLog output:\n" + << log_output; +} + +TEST(Extensions, LoadFromConfigFile) { + auto app = make_application(); + + const std::string config_file = test_config.get_test_data_file("loading_gxf_extension.yaml"); + app->config(config_file); + + // Capture stderr output to check for specific error messages + testing::internal::CaptureStderr(); + + app->run(); + + std::string log_output = testing::internal::GetCapturedStderr(); + // Check that log_output has 10 instances of "Hello world" + auto pos = log_output.find("Hello world"); + int count = 0; + while (pos != std::string::npos) { + count++; + pos = log_output.find("Hello world", pos + 1); + } + EXPECT_EQ(count, 10) << "Expected to find 10 instances of 'Hello world' in log output, but found " + << count << ".\nLog output:\n" + << log_output; +} + +TEST(Extensions, LoadFromConfigFileAfterAccessingExecutor) { + auto app = make_application(); + // Access executor before calling config() or run() method to see if it works + app->executor().context(); + + const std::string config_file = test_config.get_test_data_file("loading_gxf_extension.yaml"); + app->config(config_file); + + // Capture stderr output to check for specific error messages + testing::internal::CaptureStderr(); + + app->run(); + + std::string log_output = testing::internal::GetCapturedStderr(); + // Check that log_output has 10 instances of "Hello world" + auto pos = log_output.find("Hello world"); + int count = 0; + while (pos != std::string::npos) { + count++; + pos = log_output.find("Hello world", pos + 1); + } + EXPECT_EQ(count, 10) << "Expected to find 10 instances of 'Hello world' in log output, but found " + << count << ".\nLog output:\n" + << log_output; +} + +} // namespace holoscan