AEGLOSLAB 엔비디아 기술 스택

제9부 로보틱스

9.7 Isaac ROS I — NITROS·GPU 가속 ROS 2

9.7.1 이 장에서 배우는 것 / 선수 지식

9.5(DeepStream)·9.6(VPI)에서 로봇 비전·추론을 엔진에 가속하는 법을 봤다. 그런데 로봇 소프트웨어는 대부분 ROS 2(노드·토픽·서비스) 위에 구성된다. 문제는, ROS 2의 기본 전송이 CPU·호스트 메모리·직렬화를 전제로 설계되어, GPU 가속 노드를 여러 개 이으면 단계마다 호스트 복사(H2D/D2H)와 직렬화가 개입해 PCIe·CPU가 병목이 된다는 것이다. 이 장은 Isaac ROS가 이를 푸는 핵심 메커니즘 — NITROS(NVIDIA Isaac Transport for ROS) 가 노드 사이로 GPU 버퍼 핸들을 zero-copy로 흘리는 구조 — 를 판다. 실제 그래프·노드 구성 실습은 9.8.

핵심 통찰: GPU 가속의 적은 연산이 아니라 데이터 이동이다. 한 노드가 GPU로 잘 동작해도, ROS 2 토픽으로 결과를 넘기는 순간 GPU→호스트(D2H)로 내리고 직렬화해 보내면, 다음 노드는 다시 호스트→GPU(H2D)로 전송한다. N단계 가속 그래프는 N번의 왕복 복사 + 직렬화를 치른다 — 1080p·4K 영상이라면 이 복사가 추론 자체보다 비용이 커진다. NITROS는 ROS 2의 Type Adaptation·Type Negotiation(Humble 이후 도입, 정확한 배포판은 버전 의존·확인 필요) 위에서, 인접 가속 노드가 GPU 버퍼를 가리키는 핸들만 주고받게 해 가속 그래프 내부를 zero-copy로 만든다 — 복사는 그래프 경계(진입·이탈)에서 한 번씩만.

배우는 것:

  • 복사의 문제 — 표준 ROS 2 + GPU 노드 조합이 단계마다 H2D/D2H·직렬화로 병목이 되는 메커니즘.
  • ROS 2 토대 — intra-process 통신, Type Adaptation(REP-2007), Type Negotiation(REP-2009).
  • NITROS — ROS 메시지 ⟷ GPU 핸들 적응, 인접 노드 타입 협상, zero-copy 전달과 경계 폴백.
  • GXF(Graph Execution Framework) — NITROS 노드 속의 실행 그래프(코드렛·엔티티·텐서), ROS ⟷ GXF 다리.
  • Isaac ROS 지형 — isaac_ros_common·isaac_ros_nitros·GEM(인지·추론·SLAM·매니퓰레이션).

선수 지식: 9.3(컨테이너·배포)·9.4(Jetson 통합 메모리·전력)·2.4(메모리 모델: H2D/D2H)·2.10(스트림)·5.2(TensorRT)·9.6(VPI 비전 가속). ROS 2(노드·토픽·DDS·executor·composable node)의 기초는 가정한다(BK-02). 그래프·노드 실습은 9.8, 인지(cuVSLAM·nvblox)는 9.9, 매니퓰레이션(cuMotion)은 9.10.

정직: Isaac ROS의 패키지명·노드 플러그인 클래스명·지원 메시지 타입·요구 ROS 2 배포판은 릴리스 의존이 크다(GEM 추가·개명·재편이 잦다). 이 장은 NITROS가 왜·어떻게 zero-copy를 만드는가의 원리를 가르치고, 구체 패키지·플러그인명·버전은 "그 릴리스 문서로 확인". 코드·launch는 개념 골격이다(클래스명은 예시).

9.7.2 왜 ROS 2에 GPU 가속이 어려운가 — 복사의 문제

ROS 2의 기본 통신은 DDS(데이터 분산 서비스) 위의 발행/구독이다. 메시지(sensor_msgs/Image 등)는 CDR로 직렬화되어 전송되고, 데이터는 기본적으로 호스트(CPU) 메모리에 있다. 이 모델은 분산·이식성에는 훌륭하지만, GPU 가속 비전·추론 파이프라인에는 정면으로 불리하다.

전형적 인지 그래프 — 카메라 → 왜곡 보정(rectify) → 크기 변환(resize) → DNN 추론(TensorRT) → 후처리 — 를 노드마다 따로 GPU 가속하면:

표준 ROS 2 + GPU 노드 — 가속을 노드마다 독립적으로:

Image Image·DDS 호스트복사 + CDR Image·DDS 호스트복사 + CDR Tensor·DDS 호스트복사 + CDR camera (CPU) rectify H2D·GPU·D2H + 직렬화 resize H2D·GPU·D2H + 직렬화 DNN H2D·GPU·D2H + 직렬화 postproc H2D·GPU·D2H

매 단계 비용: ① D2H(GPU→호스트) ② 직렬화(CDR) ③ DDS 전송 ④ 역직렬화 ⑤ H2D(호스트→GPU). N단계면 호스트 왕복 복사 약 2N회 + 직렬화/역직렬화 2N회 — PCIe·memcpy·CPU가 병목.

  • 호스트 왕복: GPU에서 만든 결과를 다음 노드에 넘기려면 D2H로 전송해야 하고(직렬화·전송 대상은 호스트 메모리), 다음 노드는 H2D로 다시 전송한다. 한 단계당 D2H+H2D 한 쌍. 4단계면 영상이 GPU↔호스트를 8번 오간다 — 정작 연산은 GPU에서 끝났는데도.
  • 직렬화 세금: DDS는 메시지를 CDR로 직렬화/역직렬화한다. 작은 제어 메시지엔 무해하나, 수 MB 크기의 영상엔 CPU 시간·메모리 대역폭을 소모한다.
  • 대역폭 산수(1.9 정신): 1080p RGB8 한 장 ≈ 1920×1080×3 ≈ 6.2 MB. 30 fps면 한 방향 복사만 ~186 MB/s. 4단계 naive(단계당 D2H+H2D = 8 복사/프레임)면 ~1.5 GB/s의 호스트 복사 트래픽 + 직렬화 — 영상이 4K(≈24.9 MB)면 ~6 GB/s. (개략값, 환경 의존.)
  • 결과: GPU 점유율은 낮은데 레이턴시는 높고 CPU는 바쁘다. "GPU로 가속했는데 왜 안 빠르지?"의 전형 — 데이터 이동이 연산 시간을 압도한다. 엣지(Jetson, 9.4)의 제한된 전력·대역폭에선 특히 치명적.

핵심: 표준 ROS 2는 호스트 메모리·직렬화 전송이라, GPU 가속 노드를 이으면 단계마다 D2H→직렬화→H2D가 개입해 데이터 이동이 병목이 된다. 연산을 가속해도 그래프가 안 빨라진다. 해법은 GPU에 데이터를 둔 채 노드 사이로 핸들만 넘기는 것 — 그것이 NITROS다.

9.7.3 ROS 2가 주는 토대 — intra-process·Type Adaptation·Type Negotiation

NITROS는 자동으로 가속되는 것이 아니라, ROS 2(Humble 이후, 시점 의존 확인 필요)가 도입한 세 가지 표준 기능 위에 선다. 이를 알아야 NITROS가 보인다.

① Intra-process 통신. 여러 노드를 같은 프로세스에 composable node(컴포넌트) 로 적재하면, ROS 2는 토픽 전달을 포인터 전달로 단축한다 — 직렬화·DDS·복사 없이 같은 주소 공간에서 메시지 객체의 포인터를 넘긴다. 단, 이 포인터는 여전히 호스트 메모리 객체를 가리킨다(GPU 버퍼가 아니다). NITROS의 zero-copy는 이 intra-process 경로를 전제로 한다 — 그래서 가속 노드들은 하나의 component container에 함께 적재되어야 한다.

② Type Adaptation (REP-2007). 노드가 ROS 메시지 타입(sensor_msgs/Image) 대신 자신에게 편한 커스텀 타입(예: GPU 버퍼 핸들을 감싼 타입)으로 콜백을 받게 하는 기능. ROS는 경계에서만 커스텀 타입 ⟷ ROS 메시지를 변환한다. 인접한 두 노드가 같은 커스텀(적응) 타입을 쓰면, 그 사이에선 ROS 메시지로 되돌리지 않고 커스텀 타입을 직접 주고받는다.

③ Type Negotiation (REP-2009). 발행자와 구독자가 시동 시 어떤 타입으로 통신할지 협상하는 기능. 가속 노드 체인은 협상을 통해 공통 GPU 타입을 골라 end-to-end로 쓰고, 비가속(표준) 노드를 만나는 경계에서는 표준 ROS 메시지로 폴백한다(폴백).

세 토대가 합쳐지는 그림:

intra-process · 같은 컨테이너 — 내부: zero-copy 핸들 sensor_msgs/Image 경계: 표준 메시지 Image 경계: 표준 메시지 표준 노드 NITROS A 협상된 GPU 타입 NITROS B NITROS C 표준 노드
  • Negotiation: 시동 시 A·B·C가 '우리끼리는 NITROS GPU 타입으로' 합의.
  • Adaptation: 경계에서만 sensor_msgs/Image ⟷ NITROS 타입 변환(H2D는 진입, D2H는 이탈 1회씩).
  • Intra-proc: A·B·C가 같은 프로세스라 핸들(포인터) 전달이 실제로 zero-copy.
  • 세 개가 모두 있어야 zero-copy가 성립한다: 협상(공통 GPU 타입 합의) + 적응(경계만 변환) + 동일 프로세스(포인터 전달 가능). 하나라도 빠지면(예: 노드를 별도 프로세스로) 표준 경로로 폴백해 복사가 부활한다.
  • 이 기능들은 NVIDIA 전용이 아니라 ROS 2 표준이다(ROS 2 Hardware Acceleration Working Group 맥락). NITROS는 이 표준의 NVIDIA GPU 구현이다.

핵심: NITROS는 ① intra-process(같은 컨테이너) ② Type Adaptation(경계만 ROS 메시지 변환) ③ Type Negotiation(인접 노드가 공통 GPU 타입 합의) 라는 ROS 2 표준 위에 선다. 셋이 맞물려야 가속 그래프 내부가 zero-copy가 되고, 하나라도 어긋나면 표준 복사 경로로 조용히 폴백한다 — "왜 안 빠른가"의 90%가 여기다.

9.7.4 NITROS — GPU 버퍼를 zero-copy로 흘린다

NITROS(NVIDIA Isaac Transport for ROS) 는 위 세 토대의 NVIDIA 구현이다. 핵심 자료구조는 NITROS 타입 — GPU 버퍼(정확히는 뒤에 나올 GXF 텐서)를 가리키는 핸들을 감싼 적응 타입이다. 예: NitrosImage, NitrosTensorList, NitrosCameraInfo, NitrosDisparityImage 등(이름·종류는 릴리스 의존, 확인 필요).

동작:

NITROS 발행/구독의 실제(개념):

발행 구독 NITROS 노드 A GPU 버퍼(영상/ 텐서)를 GXF가 소유 발행 = NitrosImage (핸들) 전달 토픽 NITROS 노드 B 같은 GPU 버퍼 핸들을 수신 복사 0회: 포인터만 (intra-process)
  • 협상이 성공하고 둘이 같은 컨테이너면 전달되는 것은 'GPU 버퍼를 가리키는 핸들'이다.
  • ros2 topic 인터페이스는 그대로(토픽명·QoS) — 표준 ROS 노드처럼 보이나 내부는 GPU 핸들.
  • 경계 변환(adaptation): 그래프 진입(카메라 드라이버 같은 표준 노드 → 첫 NITROS 노드)에서 sensor_msgs/Image → NitrosImage로 H2D 1회. 그래프 이탈(마지막 NITROS 노드 → 표준 노드/시각화)에서 NitrosImage → sensor_msgs/Image로 D2H 1회. 내부는 0회. 4단계든 10단계든 호스트 왕복은 진입·이탈 단 2회.
  • 투명성: NITROS 토픽도 일반 ROS 2 토픽이다 — 토픽명·QoS·ros2 topic list가 그대로다. 디버깅으로 ros2 topic echo를 걸면 그 지점에서 경계 변환(D2H)이 유발되니, 운영에선 그런 탭을 빼야 한다(함정, 9.7.10).
  • 협상 실패 시 폴백: 인접 노드가 NITROS 타입을 모르거나(표준 노드), 별도 프로세스라 intra-process가 안 되면, NITROS는 표준 sensor_msgs/Image로 폴백해 동작은 보장하되 복사가 부활한다. 기능은 멀쩡한데 성능만 표준 수준 — 조용한 성능 회귀라 로그·프로파일로 확인해야 한다.

대역폭 비교(9.7.2의 4단계 1080p30 예):

구성프레임당 호스트 복사복사 트래픽(1080p30)직렬화
naive(표준 ROS 2 + GPU 노드)단계당 D2H+H2D = 8 복사/프레임≈ 1.5 GB/s매 단계
NITROS(한 컨테이너·협상 성공)진입 H2D 1 + 이탈 D2H 1 = 2 복사/프레임 (내부 0)≈ 0.37 GB/s거의 제거

→ 호스트 복사 트래픽 약 4배↓, 직렬화 거의 제거, 레이턴시↓ (개략값·환경 의존).

견해: NITROS의 본질은 "가속 그래프 내부에서 데이터를 GPU에 묶어두고 핸들만 흘린다" 이다. 복사는 그래프 경계(진입 H2D·이탈 D2H)에서만. 표준 ROS 2 인터페이스(토픽·QoS)를 그대로 쓰되 내부 전송만 교체한 것 — 그래서 기존 ROS 2 그래프에 점진 도입이 된다(가속 구간만 NITROS 노드로). 단, zero-copy는 같은 컨테이너·협상 성공이 전제 — 깨지면 조용히 표준 복사로 폴백한다.

9.7.5 GXF — NITROS 노드 속의 실행 그래프

NITROS 노드를 열어 보면, 그 안에는 보통 GXF(Graph Execution Framework) 그래프가 들어 있다. Isaac ROS의 가속 GEM(아래)은 대부분 GXF로 구현된 코드렛을 ROS 2 노드로 감싼 것이다.

NITROS 노드의 내부 계층(위에서 아래로):

ROS 2 노드 (rclcpp) — 토픽·파라미터·라이프사이클 ← 응용이 보는 얼굴 NITROS — Type Adaptation/Negotiation (ROS ⟷ GXF 다리) ← 메시지 ⟷ GXF 텐서 GXF 그래프 — entity·component·codelet·scheduler ← 실제 GPU 연산 그래프 예: 코드렛이 CUDA/cuDNN/TensorRT/VPI 커널 호출 CUDA · cuDNN · TensorRT · VPI · cuVSLAM … (1.11·3.2·5.2·9.6)
  • GXF 개념: GXF는 엔티티(entity) 가 컴포넌트(component) 를 담고, 코드렛(codelet) 이 실행 단위(tick)이며, 스케줄러가 이를 구동하는 컴포넌트 기반 실행 프레임워크다. 데이터는 GXF 텐서(GPU 메모리)로 흐른다. DeepStream(9.5)도 GXF 기반 그래프를 쓴다(버전 의존) — Isaac ROS와 공통 실행 엔진을 공유하는 셈.
  • 왜 GXF인가: 검증된 가속 코드렛(영상 변환·DNN·SLAM)을 ROS와 독립적으로 재사용하고, GXF 텐서 핸들을 NITROS로 ROS 토픽에 노출하기 위해서다. NITROS 타입의 핸들이 가리키는 실체가 바로 GXF 텐서(GPU 버퍼) 다.
  • 응용 입장: 보통은 GXF를 직접 만질 필요 없다. ROS 2 노드로서 토픽을 잇고 파라미터를 주면 된다(9.8). GXF는 "그 안에서 어떻게 GPU 그래프가 실행되는가"의 답이다. 단, 커스텀 가속 노드를 만들거나 깊은 튜닝을 할 때 GXF가 드러난다.

핵심: Isaac ROS 가속 노드 = GXF 그래프(실제 GPU 연산) 를 NITROS(ROS⟷GXF 다리) 로 감싼 ROS 2 노드다. NITROS 핸들이 가리키는 것은 GXF 텐서(GPU 버퍼). 응용은 ROS 2 노드로만 보면 되지만, "내부에서 무엇이 실행되는가·왜 zero-copy가 되는가"의 답은 GXF다. DeepStream(9.5)과 같은 실행 엔진 계보.

9.7.6 Isaac ROS 패키지 지형

Isaac ROS는 하나의 묶음이 아니라 isaac_ros_* 패키지(GEM) 모음이다. 큰 축만(이름·구성은 릴리스 의존, 확인 필요):

분류패키지역할
토대(transport·환경)isaac_ros_common개발 컨테이너(Docker)·빌드 환경·공통 유틸 (9.3 컨테이너)
isaac_ros_nitrosNITROS 전송: 적응 타입(NitrosImage/TensorList 등)·협상
isaac_ros_gxfGXF 런타임·확장 (NITROS 노드의 실행 엔진)
인지·전처리 GEMisaac_ros_image_procrectify·resize·색공간 변환 (VPI/CUDA 가속, 9.6)
isaac_ros_apriltagAprilTag 검출 (GPU)
isaac_ros_stereo_image_proc스테레오 disparity (9.6)
추론 GEMisaac_ros_tensor_rtTensorRT 추론 노드 (5.2)
isaac_ros_tritonTriton 추론 노드 (5.11)
isaac_ros_dnn_image_encoderDNN 입력 전처리(정규화·레이아웃) → NitrosTensorList
고수준 GEM(다음 장들)isaac_ros_visual_slamcuVSLAM (9.9)
isaac_ros_nvbloxnvblox 3D 재구성 (9.9)
isaac_ros_cumotioncuMotion 모션 플래닝 (9.10)
(pose estimation·object detection·depth 등 다수)
  • isaac_ros_common: 거의 모든 작업의 출발점 — Jetson/x86 공통의 개발용 Docker 컨테이너(9.3)와 빌드 스크립트를 준다. Isaac ROS는 컨테이너 안에서 빌드·실행이 권장된다(드라이버·CUDA·VPI·TensorRT 버전 정합, 9.2·9.3).
  • GEM = "GPU 가속 모듈": 각 GEM은 NITROS로 입출력을 GPU 핸들로 노출하는 가속 노드(들)의 묶음. 같은 컨테이너에 적재해 이으면 GEM 사이가 zero-copy.
  • 이 부의 지도: 이 장(9.7)은 전송 구조(NITROS·GXF), 9.8는 그래프·노드 실습, 9.9은 인지(cuVSLAM·nvblox), 9.10은 매니퓰레이션(cuMotion) — 모두 위 GEM들이다.

견해: Isaac ROS를 "NITROS(전송) + GXF(실행) 토대 위에 구축된 가속 GEM 카탈로그"로 보면 길을 잃지 않는다. isaac_ros_common으로 컨테이너 환경을 구성하고(9.3), 필요한 GEM(image_proc·tensor_rt·visual_slam…)을 한 컨테이너에 적재해 토픽으로 이으면, 그 사이가 zero-copy 가속 그래프가 된다. 이름·구성은 릴리스마다 바뀌니 그 시점 문서로.

9.7.7 워크스루 — 가속 인지 그래프의 데이터 흐름

NITROS zero-copy의 전제 조건을 코드로 명시해 본다. 핵심은 노드들을 하나의 component container에 composable node로 적재하는 것이다(별도 노드로 따로 실행하면 협상·intra-process가 깨져 복사로 폴백).

# 가속 인지 그래프: 한 프로세스(component container)에 NITROS 노드를 함께 적재
#   → intra-process + 타입 협상으로 GEM 사이 GPU 버퍼 zero-copy
#   (개념 골격 — 패키지·플러그인 클래스명·파라미터는 릴리스 의존, 반드시 확인)
import launch
from launch_ros.actions import ComposableNodeContainer
from launch_ros.descriptions import ComposableNode

def generate_launch_description():
    rectify = ComposableNode(
        package='isaac_ros_image_proc',
        plugin='nvidia::isaac_ros::image_proc::RectifyNode',   # 예시 클래스명
        name='rectify',
        remappings=[('image_raw', '/camera/image'),
                    ('camera_info', '/camera/camera_info')],
    )
    resize = ComposableNode(
        package='isaac_ros_image_proc',
        plugin='nvidia::isaac_ros::image_proc::ResizeNode',
        name='resize',
        parameters=[{'output_width': 640, 'output_height': 640}],
        remappings=[('image', '/rectify/image')],
    )
    encoder = ComposableNode(   # DNN 입력 텐서로 인코딩 → NitrosTensorList
        package='isaac_ros_dnn_image_encoder',
        plugin='nvidia::isaac_ros::dnn_inference::DnnImageEncoderNode',
        name='encoder',
        parameters=[{'network_image_width': 640, 'network_image_height': 640}],
    )
    tensor_rt = ComposableNode(
        package='isaac_ros_tensor_rt',
        plugin='nvidia::isaac_ros::dnn_inference::TensorRTNode',
        name='tensor_rt',
        parameters=[{'engine_file_path': '/models/model.plan',   # 5.2 빌드 산출
                     'input_binding_names': ['images'],
                     'output_binding_names': ['output']}],
    )
    # ★ 핵심: 위 4개를 같은 컨테이너에 적재해야 zero-copy
    container = ComposableNodeContainer(
        name='perception_container',
        namespace='',
        package='rclcpp_components',
        executable='component_container_mt',   # 멀티스레드 컨테이너(병렬 콜백)
        composable_node_descriptions=[rectify, resize, encoder, tensor_rt],
        output='screen',
    )
    return launch.LaunchDescription([container])

실행과 관찰:

   1) isaac_ros_common 컨테이너에서 빌드 (9.3):
        colcon build --packages-up-to isaac_ros_tensor_rt  # 예시
        source install/setup.bash
   2) launch 실행:
        ros2 launch my_perception perception.launch.py
   3) 토픽은 평범한 ROS 2 — 구조 확인:
        ros2 node list ; ros2 topic list
        ros2 topic hz /tensor_rt/output     # 처리율(개략, 환경 의존)
   4) 가속 여부 진단:
        · jtop/tegrastats(9.4): GPU 점유율↑ + CPU memcpy↓ 인지
        · Nsight Systems(2.13): H2D/D2H가 진입·이탈에만 찍히는지(내부 0)

데이터 흐름(이 그래프):

같은 컨테이너·협상 성공 — 내부: GPU 핸들만, 복사 0 경계 진입: H2D 1회 sensor_msgs/Image → NitrosImage /tensor_rt/output Tensor 경계 이탈: D2H 1회 필요 시 /camera/image (sensor_msgs/Image, CPU) rectify resize NitrosImage encoder NitrosImage tensor_rt NitrosTensorList 후처리/표준 노드
  • 반례(폴백 유발): 위 4노드를 Node(독립 프로세스)로 따로 실행하면 — intra-process가 깨져 단계마다 직렬화·H2D/D2H로 폴백한다. 같은 launch라도 별도 컨테이너로 분할하면 그 경계마다 복사. zero-copy를 원하면 한 컨테이너(9.7.3).
  • 검증 포인트(2.13 Nsight Systems): 프로파일에서 cudaMemcpy(H2D/D2H)가 그래프 진입·이탈에만 나타나고 내부 단계엔 없어야 한다. 단계마다 memcpy가 보이면 협상 실패/프로세스 분리를 의심.

견해: NITROS 가속 그래프의 실습은 "노드를 한 component container에 composable로 적재" 가 9할이다 — 이것이 intra-process를 켜고 협상을 가능케 해 GEM 사이를 zero-copy로 만든다. 평범한 ROS 2 토픽으로 보이지만(점진 도입 가능), 내부는 GPU 핸들 전달. Nsight·jtop으로 "내부에 memcpy가 없는지" 를 확인하는 것이 실질적 가속의 증거다.

9.7.8 중간 점검 (체크포인트)

  • [ ] 표준 ROS 2 + GPU 노드 조합이 단계마다 D2H→직렬화→H2D로 데이터 이동 병목이 되는 이유를 설명할 수 있다.
  • [ ] NITROS가 intra-process + Type Adaptation(REP-2007) + Type Negotiation(REP-2009) 위에 서고, 셋이 다 있어야 zero-copy임을 안다.
  • [ ] NITROS가 가속 그래프 내부는 GPU 핸들(NitrosImage/TensorList)만 전달하고 복사는 경계(진입 H2D·이탈 D2H)에서만 함을 안다.
  • [ ] GXF가 NITROS 노드 속 실제 실행 그래프(코드렛·텐서)이고 NITROS가 ROS⟷GXF 다리임을 안다.
  • [ ] zero-copy의 전제(같은 component container·협상 성공)와, 깨지면 표준 복사로 조용히 폴백함을 안다.

증상→의심: 가속 그래프인데 느림·CPU 높음 → 노드가 별도 프로세스/별도 컨테이너 → intra-process·협상 실패 → 한 component_container_mt에 적재(9.7.7). 단계마다 cudaMemcpy(Nsight) → 표준(비-NITROS) 노드가 중간에 섞임 → Isaac ROS GEM으로 통일. ros2 topic echo 켜니 그래프가 느려짐 → 그 지점 경계 D2H 유발 → 운영에선 디버그 탭 제거(9.7.10). 협상이 표준 타입으로 폴백 → 인접 노드 타입 비호환/버전 불일치 → GEM·ROS 배포판 버전 확인. 멀티스레드 컨테이너인데 한 코어만 → executor 스레드·콜백 그룹 설정. 빌드·런타임 라이브러리 충돌 → isaac_ros_common 컨테이너 밖에서 빌드(9.3) → 컨테이너 안에서.

9.7.9 트레이드오프·대안

결정트레이드오프
NITROS(Isaac ROS) vs 표준 ROS 2 image_procGPU zero-copy·가속 vs 단순·이식·CPU 무난
단일 컨테이너(intra-process) vs 멀티 프로세스zero-copy·저지연 vs 격리·내결함성(한 노드가 중단돼도 분리)
Type adaptation vs 수동 H2D/D2H자동·경계만 복사 vs 명시적 제어·디버그 용이
Isaac ROS GEM vs DeepStream(9.5)ROS 2 생태계·로봇 그래프 vs 비디오 분석 파이프라인 특화
GXF 코드렛 래핑 vs 순수 rclcpp 노드검증된 가속·재사용 vs 투명·단순·디버그

견해: 로봇 인지/추론 그래프를 ROS 2 위에서 GPU로 가속하려면 NITROS(Isaac ROS)가 정답에 가깝다 — 표준 ROS 2 노드로 GPU를 쓰면 단계마다 복사로 가속이 상쇄된다. 단, 단일 컨테이너 강제라는 비용이 있다: 한 노드가 죽으면 컨테이너 전체가 영향받아 내결함성·격리가 약해진다 — 안전 중요 시스템(8.8~8.11)은 가속 그래프를 격리 경계로 적절히 분할하고(경계마다 복사 감수) 안전 모니터는 분리하는 설계가 낫다. 비디오 분석 일색이면 DeepStream(9.5)이, ROS 로봇 그래프면 Isaac ROS가 맞다 — 둘 다 GXF 계보라 개념은 통한다.

비엔비디아 정직: ROS 2 가속은 NVIDIA만의 것이 아니다. Type Adaptation·Type Negotiation·intra-process는 ROS 2 표준(Hardware Acceleration WG)이라, 다른 가속기(FPGA 등)·벤더도 같은 토대 위에 구현할 수 있다. NITROS의 강점은 CUDA/TensorRT/VPI/cuVSLAM 가속 GEM 카탈로그 + Jetson/x86 공통 + GXF 코드렛 재사용이다. 순수 CPU·소규모 그래프거나 GPU가 없으면 표준 image_proc로 충분하다 — 고해상도·고프레임·다단계 GPU 파이프라인일수록 NITROS의 zero-copy 이득이 커진다.

9.7.10 흔한 함정

  • 노드를 따로(독립 프로세스) 실행. intra-process가 깨져 zero-copy 무산 → 한 component_container_mt에 composable로 적재(9.7.3·9.7.7).
  • 비-NITROS 노드를 중간에 삽입. 표준 image_proc(CPU)나 커스텀 노드를 사이에 넣으면 그 경계마다 D2H/H2D 부활 → 가속 구간은 Isaac ROS GEM으로 통일하고, 표준 노드는 그래프 양 끝(경계)에만.
  • 운영에서 ros2 topic echo/rqt 켜둠. 디버그 탭이 경계 변환(D2H)을 유발해 그래프를 느리게 함 → 디버깅 때만, 운영 시 제거.
  • single-thread 컨테이너로 병렬 기대. component_container(단일 스레드)면 콜백이 직렬화됨 → component_container_mt + 콜백 그룹(reentrant)로 병렬.
  • 협상 실패를 못 보고 넘어감. 비호환·버전 불일치면 표준 타입으로 조용히 폴백해 기능은 되는데 느림 → 시동 로그·Nsight(2.13)로 협상 결과·내부 memcpy 확인.
  • Jetson의 통합 메모리(9.4)를 오해. Jetson은 물리적 통합 메모리라 "H2D"의 의미가 dGPU와 다르나(복사 없이 매핑 가능), 직렬화 제거·핸들 전달의 이점은 그대로 — NITROS는 여전히 유효.
  • 컨테이너 밖에서 빌드. 드라이버·CUDA·VPI·TensorRT 버전 충돌 → isaac_ros_common 개발 컨테이너에서 빌드·실행(9.2·9.3).
  • QoS 불일치. NITROS도 ROS 2 QoS를 따름 → 센서 스트림은 보통 best-effort·적절한 depth로 맞춤(불일치면 구독 누락).
  • 패키지·플러그인명·타입을 버전 불문 복붙. Isaac ROS는 GEM 개명·재편이 잦음 → 그 릴리스 문서로 클래스명·파라미터 확인.

9.7.11 연습문제

연습 1 (복사의 문제). 표준 ROS 2 노드로 GPU 가속을 한 4단계 인지 그래프가 왜 비효율인지, 복사·직렬화 횟수로 설명하고, NITROS가 정확히 무엇을 없애는지 답하라.

정답

표준 ROS 2는 메시지를 호스트 메모리에 두고 DDS로 직렬화(CDR) 전송한다. GPU 가속 노드는 ① 입력을 받으려면 H2D(호스트→GPU)로 전송하고 ② GPU에서 연산 ③ 결과를 다음 노드에 넘기려면 D2H(GPU→호스트)로 전송해 ④ 직렬화해 발행한다. 다음 노드가 또 ①~④를 반복. 즉 한 단계당 D2H+H2D 한 쌍 + 직렬화/역직렬화 한 쌍. 4단계면 영상이 GPU↔호스트를 약 8번(2N) 오가고 직렬화도 그만큼. 1080p RGB8(≈6.2 MB)·30fps면 호스트 복사만 ~1.5 GB/s(개략), 4K면 ~6 GB/s — 정작 연산은 GPU에서 끝났는데 데이터 이동(PCIe·memcpy)·CPU 직렬화가 병목이 되어 "가속했는데 안 빠른" 상태가 된다. NITROS가 없애는 것: 가속 그래프 내부의 모든 D2H/H2D와 직렬화 — 인접 NITROS 노드는 GPU 버퍼 핸들(NitrosImage/TensorList)만 주고받으므로 내부 복사 0. 복사는 그래프 경계(표준 노드↔가속 그래프)에서만 진입 H2D 1회·이탈 D2H 1회. 4단계든 10단계든 호스트 왕복은 2회로 고정 → 복사 트래픽 ~4배↓, 직렬화 거의 제거, 레이턴시↓. 전제: 노드들이 같은 component container에 있고(intra-process) 협상이 성공해야 한다 — 아니면 표준 복사로 폴백.

연습 2 (적응·협상). Type Adaptation(REP-2007)과 Type Negotiation(REP-2009)의 각각의 역할을 구분하고, 둘이 합쳐 어떻게 end-to-end zero-copy와 경계 폴백을 동시에 만드는지 설명하라.

정답

Type Adaptation(REP-2007): 노드가 ROS 메시지 타입(sensor_msgs/Image) 대신 자신에게 편한 커스텀 타입(GPU 버퍼 핸들을 감싼 NitrosImage 등)으로 콜백을 받고 발행하게 하는 기능. ROS는 필요할 때만(경계) 커스텀 타입 ⟷ ROS 메시지를 변환한다. 즉 "이 노드는 GPU 핸들로 일한다"를 선언하는 메커니즘. Type Negotiation(REP-2009): 발행자·구독자가 시동 시 어떤 타입으로 통신할지 합의하는 메커니즘. 한 토픽에 여러 후보 타입이 있을 때 인접 노드가 공통의 최적 타입(가속 노드끼리는 GPU 타입)을 고른다. 합쳐지는 방식: 가속 노드 체인 A→B→C가 시동 시 협상해 "우리끼리는 NITROS GPU 타입" 으로 합의(negotiation) → 그러면 A·B·C는 적응 타입(GPU 핸들)을 ROS 메시지로 되돌리지 않고 직접 전달(adaptation) → 같은 컨테이너(intra-process)면 이는 포인터 전달 = zero-copy. 한편 체인의 경계(앞단의 표준 카메라 노드, 뒷단의 표준 시각화 노드)는 NITROS 타입을 모르므로, 협상이 표준 sensor_msgs/Image로 떨어지고(negotiation 폴백) 적응 계층이 거기서만 변환(H2D 진입/D2H 이탈). 결과: 가속 구간은 GPU 타입으로 zero-copy, 경계는 표준 메시지로 자동 폴백 — 표준 ROS 2 그래프에 가속 구간만 삽입해도 동작하고(점진 도입), 가속 구간 내부만 빨라진다. 협상이 다 표준으로 떨어지면(비호환·프로세스 분리) zero-copy가 사라지므로, 협상 결과 확인이 성능 보장의 핵심 요인.

연습 3 (대역폭 산수). rectify→resize→DNN→postproc 4단계 그래프가 1080p RGB8·30fps를 처리한다. (a) naive(표준 ROS 2 + GPU 노드)와 (b) NITROS(한 컨테이너)의 프레임당 호스트 복사 횟수와 초당 복사 트래픽을 개략 계산하고 차이를 해석하라.

정답

1080p RGB8 한 장 ≈ 1920×1080×3 ≈ 6.2 MB(텐서 단계는 채널·크기가 달라 다르지만 개략 동급으로 둠). 30fps. (a) naive: 단계마다 입력 H2D 1 + 출력 D2H 1 = 단계당 2 복사. 4단계 → 8 복사/프레임. 트래픽 ≈ 8 × 6.2 MB × 30 ≈ ~1.5 GB/s + 매 단계 직렬화/역직렬화(CPU·메모리 대역폭 추가). (b) NITROS: 경계 진입 H2D 1 + 경계 이탈 D2H 1 = 2 복사/프레임(내부 0). 트래픽 ≈ 2 × 6.2 MB × 30 ≈ ~0.37 GB/s, 직렬화는 경계만. 해석: 호스트 복사 트래픽 약 4배↓(8→2), 직렬화 거의 제거, 단계 수가 늘수록(N) naive는 2N로 선형 증가하나 NITROS는 2로 고정 — 그래프가 깊을수록 이득이 커진다. 4K(≈24.9 MB)면 naive ~6 GB/s vs NITROS ~1.5 GB/s로 절대 격차가 더 벌어진다. 게다가 복사는 PCIe·memcpy 레이턴시와 CPU 점유를 만들어, GPU 점유율은 낮은데 지연·CPU는 높은 패턴을 naive에서 보게 된다. (모두 개략값·환경 의존 — 실제는 Nsight(2.13)로 측정.) 단, Jetson(9.4)은 통합 메모리라 "복사"가 매핑으로 줄 수 있어 절대 수치는 다르지만, 직렬화 제거·핸들 전달의 구조적 이득은 동일.

연습 4 (진단). "Isaac ROS로 가속 그래프를 구성했는데 표준보다 느리고 CPU만 바쁘다." 원인 후보 4가지와 각각의 진단·해결을 제시하라.

정답

① 노드가 별도 프로세스/별도 컨테이너로 실행됨 — intra-process가 꺼져 협상이 표준 타입으로 폴백, 단계마다 직렬화·복사. 진단: ros2 node list로 컨테이너 구성 확인, Nsight(2.13)에서 단계마다 cudaMemcpy. 해결: 4노드를 하나의 component_container_mt 에 composable로 적재(9.7.7). ② 비-NITROS 노드가 중간에 섞임 — 표준 image_proc(CPU)나 커스텀 노드가 사이에 있어 그 경계마다 D2H/H2D. 진단: 토픽 그래프(rqt)로 표준 메시지 구간 식별. 해결: 가속 구간을 Isaac ROS GEM으로 통일, 표준 노드는 양 끝으로. ③ 협상 실패(버전·타입 비호환) — GEM·ROS 배포판 버전이 안 맞아 NITROS 타입 합의 실패, 조용히 표준 폴백. 진단: 시동 로그의 협상 메시지. 해결: 버전 정합(isaac_ros_common 컨테이너, 9.3), 호환 GEM. ④ single-thread 컨테이너/QoS 문제 — component_container(단일 스레드)라 콜백 직렬화로 코어 하나만, 또는 QoS 불일치로 재전송·드롭. 진단: jtop으로 코어 점유 편중, QoS 경고. 해결: component_container_mt + 콜백 그룹, 센서 QoS(best-effort) 정합. 공통 진단 도구: jtop/tegrastats(9.4)로 GPU 점유·CPU memcpy 패턴, Nsight Systems(2.13)로 내부 단계에 memcpy가 있는지 — 있으면 zero-copy가 깨진 것. 부수로 ⑤ 운영 중 ros2 topic echo 같은 디버그 탭이 경계 D2H를 유발하는 경우도.

연습 5 (프로세스 경계 설계). NITROS zero-copy는 왜 프로세스 경계를 못 넘는가? 그럼에도 안전·격리 때문에 멀티 프로세스가 필요하다면, 가속 그래프를 어떻게 설계해야 손해가 최소인가?

정답

왜 못 넘는가: NITROS의 zero-copy는 intra-process 통신(같은 주소 공간에서 메시지 객체 포인터 전달) 위에 선다. GPU 버퍼 핸들도 결국 그 프로세스의 주소 공간에서 유효한 핸들이라, 다른 프로세스로 포인터를 넘겨도 그쪽에선 가리킬 수 없다. 프로세스 간으로 GPU 메모리를 실제로 공유하려면 CUDA IPC나 공유 메모리·메모리 핸들 export 같은 별도 장치가 필요한데, 이는 NITROS의 기본 표준 경로가 아니다. 그래서 프로세스 경계에선 협상이 표준 ROS 메시지로 폴백(직렬화·복사). 설계 지침: ① 가속 그래프를 격리 단위로 묶기 — 한 가속 파이프라인(예: 비전 전처리→DNN→후처리)을 하나의 컨테이너(프로세스) 로 두어 그 안은 zero-copy로 유지하고, 프로세스 경계는 자연스러운 모듈 경계(인지 ↔ 계획 ↔ 제어)에만 둔다 — 경계 수를 최소화. ② 경계는 데이터가 작아지는 지점에 — 큰 영상은 가속 그래프 안에 가두고, 프로세스 경계로는 압축된 결과(검출 박스·포즈·점유 격자 같은 작은 메시지)만 넘기면 직렬화·복사 비용이 작다(영상 6 MB vs 검출 결과 수 KB). ③ 안전 모니터·치명 노드는 분리 — 안전 중요 노드(8.8~8.11)는 가속 그래프와 다른 프로세스로 격리해, 가속 컨테이너가 중단돼도 안전 기능은 살아 있게(내결함성). 경계 비용은 작은 메시지라 감수. ④ 꼭 큰 데이터를 프로세스 경계로 넘겨야 하면 — CUDA IPC/공유 메모리 기반 전송(별도 구현·검증 필요)을 검토하되 복잡도·동기화 비용을 따진다. 요지: zero-copy 이득은 "큰 데이터를 한 프로세스 안에 가두고, 프로세스 경계는 작은 데이터·격리 경계에만" 두는 설계로 지킨다 — NITROS의 단일 컨테이너 강제(9.7.9)와 안전 격리(8.8) 사이의 균형.

9.7.12 요약 · 다음 장

  • GPU 가속의 적은 데이터 이동. 표준 ROS 2는 호스트 메모리·직렬화 전송이라, GPU 가속 노드를 이으면 단계마다 D2H→직렬화→H2D로 PCIe·CPU가 병목 — "가속했는데 안 빠른" 전형(9.7.2).
  • NITROS = ROS 2 표준 3종의 NVIDIA 구현. intra-process(같은 컨테이너) + Type Adaptation(REP-2007, 경계만 변환) + Type Negotiation(REP-2009, 인접 노드 공통 GPU 타입 합의) 가 맞물려, 가속 그래프 내부를 GPU 핸들(NitrosImage/TensorList)만 흘리는 zero-copy로(복사는 경계 진입·이탈 2회만).
  • GXF가 그 속의 실행 엔진. Isaac ROS 가속 노드 = GXF 그래프(코드렛·텐서) 를 NITROS로 감싼 ROS 2 노드. NITROS 핸들이 가리키는 실체가 GXF 텐서. DeepStream(9.5)과 같은 계보.
  • 전제가 깨지면 조용히 폴백. 별도 프로세스·비호환·중간 표준 노드면 표준 복사로 떨어져 기능은 되나 느림 — Nsight(2.13)·jtop(9.4)로 "내부에 memcpy 없는지" 확인이 실질적 가속의 증거.
  • 지형: isaac_ros_common(컨테이너 환경)·isaac_ros_nitros(전송)·GEM(image_proc·tensor_rt·visual_slam·cumotion…). 점진 도입 가능(가속 구간만 NITROS). 정직: 패키지·플러그인명·타입·요구 배포판은 릴리스 의존 — 그 시점 문서로 확인.

다음 장: 9.8 Isaac ROS II — 그래프·노드 실습. NITROS·GXF의 구조를 봤으니, 이제 실제 그래프를 구성·실행한다 — composable node 적재, 토픽 리매핑·파라미터, 가속 GEM(image_proc·tensor_rt 등)을 이어 인지 파이프라인을 직접 기동하고, 협상·zero-copy를 프로파일로 검증한다. 이 장의 "왜·무엇"을 9.8에서 "어떻게"로.