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)