Hello NVIDIA team and community,
I would appreciate some guidance on a GPU memory reuse change in our NITROS image pipeline. I am trying to understand the intended design and whether we should use an existing mechanism before maintaining a custom implementation.
Environment and test scope
-
Jetson AGX Orin
-
Jetson Linux / L4T R36.4.3, CUDA 12.6
-
Kernel:
5.15.148-rt-tegra,PREEMPT_RT -
ROS 2 Humble
-
Isaac ROS / NITROS version and commit: [please insert exact version/commit]
-
System configuration: three RealSense cameras—front-top, front-bottom, and rear—with separate camera containers.
-
Evaluated path: front-top camera only, using 1280 Ă— 720 RGB8 at 30 FPS.
-
The front-top container was restricted to logical CPU 5 (
CpusetCpus=5), shared by the camera and other processes inside that container.
The relevant pipeline is:
RealSense → sensor_msgs::msg::Image → NITROS image adapter → H.264 encoder
The change concerns GPU image allocation in the ROS-to-NITROS adapter, rather than the encoder’s output pool. The results below do not represent a controlled three-camera comparison or validation of the change across all cameras.
Observation and motivation
Our saved pre-change adapter calls cudaMallocAsync for each incoming image and cudaFreeAsync from its cleanup callback.
CPU profiling showed samples in this allocation path, including underlying Jetson nvmap allocation work. We investigated whether retaining completed image allocations could reduce this overhead during continuous streaming.
I understand that cudaMallocAsync already uses a CUDA memory pool. However, we did not capture the effective pool release threshold during the original measurement. We therefore cannot establish whether pool configuration or synchronization behavior caused the observed overhead.
The saved baseline may also contain local changes, so I do not want to attribute this behavior to an unmodified upstream release without confirming that.
Experimental change
We added a cache at the image adapter boundary:
-
Reuse an idle buffer only when its byte size matches the requested image size.
-
Call
cudaMallocwhen no matching buffer is available. -
Hold the allocation through a shared-ownership lease attached to the image buffer’s external deleter.
-
Preserve the existing
NitrosBufferGPU-event cleanup mechanism. The intended behavior is to recycle only after all registered GPU reads and writes have completed. -
Retain up to 64 MiB of idle buffers per process/device, freeing excess buffers. This does not limit live frames or total process memory.
-
Keep the host-to-device image copy.
The intent is to reduce repeated allocation work while preserving the existing buffer lifetime requirements.
Preliminary evidence and limitations
Short profiling captures on logical CPU 5 recorded:
| Stack path | Before: 3,465 samples | After: 3,121 samples |
|---|---|---|
cudaMallocAsync |
19.77% | 0 samples |
NITROS image convert_to_custom |
21.39% | 2.60% |
nvmap_alloc_handle, all sources |
18.41% | 2.02% |
These are inclusive stack-sample percentages on the sampled CPU, not allocation counts or whole-system CPU utilization. Overlapping paths should not be added together. Replacing the adapter’s async allocation calls also means their disappearance alone does not establish a performance benefit.
The captures were taken at different times on a shared system, and another camera-path change was also present. We have not completed an isolated comparison against a properly configured CUDA async pool, a comparison with a non-RT kernel, or full multi-camera endurance testing.
Questions
-
Preferred mechanism: For fixed-resolution image streaming, should we first investigate the existing CUDA pool and
cudaMemPoolAttrReleaseThreshold, or is there a NITROS-provided image/buffer pool we should use? Which settings and synchronization points would you check first? -
Ownership and correctness: Is retaining an allocation through the external deleter compatible with the intended
NitrosBufferlifecycle? What should we verify for safe recycling with multiple subscribers, different CUDA streams, and node shutdown? -
RT kernel and CPU affinity: Are there additional considerations for allocation, cleanup, or profiling under
PREEMPT_RTwith a camera container restricted to one logical CPU? Would you recommend checking kernel configuration or relaxing CPU affinity before drawing conclusions about allocator overhead? We have not established either as the cause. -
Tradeoffs and validation: What drawbacks should we evaluate compared with the native async allocator—for example, synchronization on cache misses, memory retention across separate camera processes, or interference with other GPU workloads? Which measurements would make the comparison most useful?
I have read the CUDA stream-ordered allocator explanation and the discussion on GPU memory management with NITROS nodes. I am still learning how these mechanisms fit together in a running NITROS pipeline.
If using or configuring the existing mechanisms is the better approach, I would be happy to simplify our implementation. Any corrections or pointers would be greatly appreciated.
Thank you for your time and guidance.