Tutorial 23 - Extended Attributes On The GPU

Extended attributes are no different from other types of attributes with respect to where their memory will be located. The difference is that there is a slightly different API for accessing their data, illustrating by these examples.

This node also illustrates the new concept of having a node create an ABI function override that handles the runtime type resolution of extended attribute types. In this case when any of the two input attributes or one output attribute become resolved then the other two attributes are resolved to the same type, if possible.

OgnTutorialCpuGpuExtended.ogn

The ogn file shows the implementation of a node named “omni.graph.tutorials.CpuGpuExtended” with an input ‘any’ attribute on the CPU, an input ‘any’ attribute on the GPU, and an output whose memory location is decided at runtime by a boolean.

 1{
 2    "CpuGpuExtended": {
 3        "version": 1,
 4        "categories": "tutorials",
 5        "description": [
 6            "This is a tutorial node. It exercises functionality for accessing data in extended attributes that",
 7            "are on the GPU as well as those whose CPU/GPU location is decided at runtime. The compute",
 8            "adds the two inputs 'gpuData' and 'cpuData' together, placing the result in `cpuGpuSum`, whose",
 9            "memory location is determined by the 'gpu' flag.",
10            "This node is identical to OgnTutorialCpuGpuExtendedPy.ogn, except is is implemented in C++."
11        ],
12        "tags": ["tutorial", "extended", "gpu"],
13        "uiName": "Tutorial Node: CPU/GPU Extended Attributes",
14        "inputs": {
15             "cpuData": {
16                "type": "any",
17                "description": "Input attribute whose data always lives on the CPU",
18                "uiName": "CPU Input Attribute"
19             },
20             "gpuData": {
21                 "type": "any",
22                 "memoryType": "cuda",
23                 "description": "Input attribute whose data always lives on the GPU",
24                 "uiName": "GPU Input Attribute"
25             },
26             "gpu": {
27                 "type": "bool",
28                 "description": "If true then put the sum on the GPU, otherwise put it on the CPU",
29                 "uiName": "Results To GPU"
30             }
31        },
32        "outputs": {
33            "cpuGpuSum": {
34                "type": "any",
35                "memoryType": "any",
36                "description": [
37                    "This is the attribute with the selected data. If the 'gpu' attribute is set to true then this",
38                    "attribute's contents will be entirely on the GPU, otherwise it will be on the CPU."
39                ],
40                "uiName": "Sum"
41            }
42        },
43        "tests": [
44            {
45                "inputs:cpuData": {
46                    "type": "pointf[3][]",
47                    "value": [
48                        [ 1.0, 2.0, 3.0 ],
49                        [ 4.0, 5.0, 6.0 ]
50                    ]
51                },
52                "inputs:gpuData": {
53                    "type": "pointf[3][]",
54                    "value": [
55                        [ 7.0, 8.0, 9.0 ],
56                        [ 10.0, 11.0, 12.0 ]
57                    ]
58                },
59                "inputs:gpu": false,
60                "outputs:cpuGpuSum": {
61                    "type": "pointf[3][]",
62                    "value": [
63                        [ 8.0, 10.0, 12.0 ],
64                        [ 14.0, 16.0, 18.0 ]
65                    ]
66                }
67            },
68            {
69                "inputs:cpuData": {
70                    "type": "pointf[3][]",
71                    "value": [ [ 4.0, 5.0, 6.0 ] ]
72                },
73                "inputs:gpuData": {
74                    "type": "pointf[3][]",
75                    "value": [ [ 7.0, 8.0, 9.0 ] ]
76                },
77                "inputs:gpu": true,
78                "outputs:cpuGpuSum": {
79                    "type": "pointf[3][]",
80                    "value": [ [ 11.0, 13.0, 15.0 ] ]
81                }
82            }
83        ]
84    }
85}

OgnTutorialCpuGpuExtended.cpp

The cpp file contains the implementation of the compute method. It sums two inputs on either the CPU or GPU based on the input boolean. For simplicity only the float[3][] attribute type is processed, with all others resulting in a compute failure.

  1// SPDX-FileCopyrightText: Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
  2// SPDX-License-Identifier: LicenseRef-NvidiaProprietary
  3//
  4// NVIDIA CORPORATION, its affiliates and licensors retain all intellectual
  5// property and proprietary rights in and to this material, related
  6// documentation and any modifications thereto. Any use, reproduction,
  7// disclosure or distribution of this material and related documentation
  8// without an express license agreement from NVIDIA CORPORATION or
  9// its affiliates is strictly prohibited.
 10#include <OgnTutorialCpuGpuExtendedDatabase.h>
 11extern "C" void cpuGpuSumCPU(float const (*p1)[3], float const (*p2)[3], float (*sums)[3], size_t);
 12extern "C" void cpuGpuSumGPU(float const (**p1)[3], float const (**p2)[3], float (**sums)[3]);
 13
 14namespace omni
 15{
 16namespace graph
 17{
 18namespace tutorials
 19{
 20
 21// Only the pointf[3][] type is accepted. Make a shortcut to the type information describing it for comparison
 22core::Type acceptedType(core::BaseDataType::eFloat, 3, 1, core::AttributeRole::ePosition);
 23
 24// Template for reporting type inconsistencies. The data types are different for the attributes but the checks are
 25// the same so this avoids duplication
 26template <typename P1, typename P2, typename P3>
 27bool verifyDataTypes(OgnTutorialCpuGpuExtendedDatabase& db, const P1& points1, const P2& points2, P3& sums, const char* type)
 28{
 29    if (!points1)
 30    {
 31        db.logWarning("Skipping compute - The %s attribute was not a valid pointf[3][]", type);
 32    }
 33    else if (!points2)
 34    {
 35        db.logWarning("Skipping compute - The %s attribute was not a valid pointf[3][]", type);
 36    }
 37    else if (!sums)
 38    {
 39        db.logWarning("Skipping compute - The %s output attribute was not a valid pointf[3][]", type);
 40    }
 41    else if (points1.size() != points2.size())
 42    {
 43        db.logWarning(
 44            "Skipping compute - Point arrays are different sizes (%zu and %zu)", points1.size(), points2.size());
 45    }
 46    else
 47    {
 48        sums.resize(points1.size());
 49        return true;
 50    }
 51    return false;
 52}
 53
 54class OgnTutorialCpuGpuExtended
 55{
 56    bool m_allAttributesResolved{ false };
 57
 58public:
 59    static bool compute(OgnTutorialCpuGpuExtendedDatabase& db)
 60    {
 61        if (!db.sharedState<OgnTutorialCpuGpuExtended>().m_allAttributesResolved)
 62        {
 63            db.logWarning("All types are not yet resolved. Cannot run the compute.");
 64            return false;
 65        }
 66
 67        const auto& gpu = db.inputs.gpu();
 68        const auto cpuData = db.inputs.cpuData();
 69        const auto gpuData = db.inputs.gpuData();
 70        auto cpuGpuSum = db.outputs.cpuGpuSum();
 71
 72        if ((cpuData.type() != acceptedType) || (gpuData.type() != acceptedType) || (cpuGpuSum.type() != acceptedType))
 73        {
 74            db.logWarning("Skipping compute - All of the attributes do not have the accepted resolved type pointf[3][]");
 75            return false;
 76        }
 77
 78        if (gpu)
 79        {
 80            // Computation on the GPU has been requested so get the GPU versions of the attribute data
 81            const auto points1 = cpuData.getGpu<float[][3]>();
 82            const auto points2 = gpuData.get<float[][3]>();
 83            auto sums = cpuGpuSum.getGpu<float[][3]>();
 84            if (!verifyDataTypes(db, points1, points2, sums, "GPU"))
 85            {
 86                return false;
 87            }
 88            cpuGpuSumGPU(points1(), points2(), sums());
 89        }
 90        else
 91        {
 92            // Computation on the CPU has been requested so get the CPU versions of the attribute data
 93            const auto points1 = cpuData.get<float[][3]>();
 94            const auto points2 = gpuData.getCpu<float[][3]>();
 95            auto sums = cpuGpuSum.getCpu<float[][3]>();
 96            if (!verifyDataTypes(db, points1, points2, sums, "CPU"))
 97            {
 98                return false;
 99            }
100            cpuGpuSumCPU(points1->data(), points2->data(), sums->data(), points1.size());
101        }
102        return true;
103    }
104
105    static void onConnectionTypeResolve(const NodeObj& nodeObj)
106    {
107        // If any one type is resolved the others should resolve to the same type. Calling this helper function
108        // makes that happen automatically. If it returns false then the resolution failed for some reason. The
109        // node's user data, which is just a copy of this class, is used to keep track of the resolution state so
110        // that the compute method can quickly exit when the types are not resolved.
111        AttributeObj attributes[3]{ nodeObj.iNode->getAttributeByToken(nodeObj, inputs::cpuData.token()),
112                                    nodeObj.iNode->getAttributeByToken(nodeObj, inputs::gpuData.token()),
113                                    nodeObj.iNode->getAttributeByToken(nodeObj, outputs::cpuGpuSum.token()) };
114        auto& state = OgnTutorialCpuGpuExtendedDatabase::sSharedState<OgnTutorialCpuGpuExtended>(nodeObj);
115        state.m_allAttributesResolved = nodeObj.iNode->resolveCoupledAttributes(nodeObj, attributes, 3);
116    }
117};
118
119REGISTER_OGN_NODE()
120
121} // namespace tutorials
122} // namespace graph
123} // namespace omni

OgnTutorialCpuGpuExtendedPy.py

The py file contains the same algorithm as the C++ node, with the node implementation language being different.

 1"""
 2Implementation of the Python node accessing extended attributes whose memory location is determined at runtime.
 3"""
 4
 5import omni.graph.core as og
 6
 7# Only one type of data is handled by the compute - pointf[3][]
 8POINT_ARRAY_TYPE = og.Type(og.BaseDataType.FLOAT, tuple_count=3, array_depth=1, role=og.AttributeRole.POSITION)
 9
10
11class OgnTutorialCpuGpuExtendedPy:
12    """Exercise GPU access for extended attributes through a Python OmniGraph node"""
13
14    @staticmethod
15    def compute(db) -> bool:
16        """Implements the same algorithm as the C++ node OgnTutorialCpuGpuExtended.cpp.
17
18        It follows the same code pattern for easier comparison, though in practice you would probably code Python
19        nodes differently from C++ nodes to take advantage of the strengths of each language.
20        """
21        # Find and verify the attributes containing the points
22        if db.attributes.inputs.cpuData.get_resolved_type() != POINT_ARRAY_TYPE:
23            db.log_warning("Skipping compute - CPU attribute type did not resolve to pointf[3][]")
24            return False
25        if db.attributes.inputs.gpuData.get_resolved_type() != POINT_ARRAY_TYPE:
26            db.log_warning("Skipping compute - GPU attribute type did not resolve to pointf[3][]")
27            return False
28        if db.attributes.outputs.cpuGpuSum.get_resolved_type() != POINT_ARRAY_TYPE:
29            db.log_warning("Skipping compute - Sum attribute type did not resolve to pointf[3][]")
30            return False
31
32        # Put accessors into local variables for convenience
33        gpu_data = db.inputs.gpuData
34        cpu_data = db.inputs.cpuData
35        sums = db.outputs.cpuGpuSum
36
37        # Mismatched sizes cannot be computed
38        if gpu_data.size != cpu_data.size:
39            db.log_warning(f"Skipping compute - Point arrays are different sizes ({gpu_data.size} and {cpu_data.size})")
40
41        # Set the size to what is required for the dot product calculation
42        sums.size = cpu_data.size
43
44        # Use the correct data access based on whether the output is supposed to be on the GPU or not
45        if db.inputs.gpu:
46            # The second line is how the values would be extracted if Python supported GPU data extraction.
47            # When it does this tutorial will be updated
48            sums.cpu_value = cpu_data.value + gpu_data.cpu_value
49            # sums.gpu_value = cpu_data.gpu_value + gpu_data.value
50        else:
51            sums.cpu_value = cpu_data.value + gpu_data.cpu_value
52        return True
53
54    @staticmethod
55    def on_connection_type_resolve(node: og.Node) -> None:
56        """Whenever any of the inputs or the output get a resolved type the others should get the same resolution"""
57        attribs = [
58            node.get_attribute("inputs:cpuData"),
59            node.get_attribute("inputs:gpuData"),
60            node.get_attribute("outputs:cpuGpuSum"),
61        ]
62        og.resolve_fully_coupled(attribs)