AEGLOSLAB 엔비디아 기술 스택

제9부 로보틱스

9.8 Isaac ROS II — 그래프·노드 실습

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

9.7에서 NITROS·GXF의 구조(왜 표준 ROS 2 + GPU 노드가 복사로 병목이 되고, 어떻게 NITROS가 GPU 핸들만 흘려 zero-copy를 만드는가)를 봤다. 이 장은 그 "왜·무엇"을 "어떻게"로 옮긴다 — 실제로 개발 컨테이너를 기동하고, composable node를 하나의 container에 적재해 가속 인지 그래프를 직접 조립·실행하고, 협상과 zero-copy가 실제로 일어났는지 프로파일로 검증한다. 인지·매니퓰레이션 같은 고수준 GEM(9.9·9.10)을 쓰기 전, 모든 Isaac ROS 그래프의 공통 실습 골격을 여기서 익힌다.

핵심 통찰: zero-copy는 "어떻게 기동하느냐"로 켜지거나 꺼진다. 9.7에서 봤듯 NITROS의 zero-copy는 ① 노드가 같은 component container에 있고 ② 협상이 성공해야 성립한다. 그래서 Isaac ROS 실습의 9할은 "노드를 어떤 그릇(container)에, 어떤 토픽 배선(remapping)으로, 어떤 executor·QoS로 적재하는가"다 — 코드 한 줄(Node vs ComposableNode)이 가속을 좌우한다. 그리고 기동한 뒤에는 반드시 검증한다: 가속 그래프는 안 빨라도 멀쩡히 도니까(조용한 폴백), Nsight·jtop으로 "내부에 memcpy가 없는지" 봐야 확정된다.

배우는 것:

  • 개발 환경 — isaac_ros_common 컨테이너·워크스페이스·colcon build(왜 컨테이너 안에서).
  • composable node·container — ComposableNode·ComposableNodeContainer·component_container_mt, 런타임 적재(ros2 component).
  • launch로 조립 — remapping(토픽 배선)·parameter·namespace·launch 인자·include 합성.
  • 인지 파이프라인 실습 — 카메라 → rectify → resize → encoder → TensorRT 추론을 한 그래프로 기동.
  • 검증 — ros2 node/topic/component/param·rqt_graph, Nsight Systems(2.13)·jtop(9.4)로 zero-copy·협상 확인.
  • executor·콜백그룹·QoS — 멀티스레드 병렬성과 센서 스트림 QoS 정합.

선수 지식: 9.7(NITROS·GXF·협상)·9.2·9.3(JetPack·L4T·컨테이너)·5.2(TensorRT 엔진 빌드)·2.13(Nsight Systems)·9.4(jtop·tegrastats·통합 메모리)·9.6(VPI/image_proc 가속). ROS 2 launch·노드·토픽·QoS 기초는 가정(BK-02). 인지(cuVSLAM·nvblox)는 9.9, 매니퓰레이션(cuMotion)은 9.10 — 모두 이 장의 골격 위에 선다.

정직: Isaac ROS의 패키지명·플러그인 클래스명·토픽 인터페이스·파라미터·컨테이너 스크립트·요구 ROS 2 배포판은 릴리스 의존이 크다(GEM 개명·재편·API 변경 잦음). 이 장의 코드·명령·launch·클래스명은 개념 골격(예시) 이다 — 실제 이름·인자는 그 릴리스의 Isaac ROS 문서로 확인하라. 가르치는 것은 "어떤 구조로 그래프를 조립·검증하는가"의 절차·원리이지 특정 버전의 정확한 API가 아니다. 출력 수치(처리율·점유율)는 환경 의존 개략값.

9.8.2 개발 환경 — isaac_ros_common 컨테이너·워크스페이스

Isaac ROS는 개발용 Docker 컨테이너 안에서 빌드·실행이 강하게 권장된다. 이유는 9.2·9.3에서 본 그대로 — 드라이버·CUDA·VPI·TensorRT·ROS 2 버전이 한 묶음으로 정합해야 하고, 컨테이너 밖에서 빌드하면 호스트의 라이브러리 버전과 충돌하기 쉽다(함정, 9.8.10).

워크스페이스 레이아웃(권장 구조, 경로·이름은 릴리스 의존):

isaac_ros-dev/ — ${ISAAC_ ROS_WS} src/ isaac_ros_ common/ — 컨테이너·빌드 토대(여기 스크립트로 진입) isaac_ros_ nitros/ — NITROS 전송 (9.7) isaac_ros_ image_proc/ — rectify·resize· 색공간(VPI/ CUDA 가속, 9.6) isaac_ros_dnn_ inference/ — encoder· tensor_rt·triton 노드(5.2·5.11) my_perception/ — 내 launch· config·모델 build/ · install/ · log/ — colcon 산출(컨테이너 안에서 생성)
# 1) 개발 컨테이너 진입(개념 — 스크립트명·태그는 릴리스 확인)
cd ${ISAAC_ROS_WS}/src/isaac_ros_common
./scripts/run_dev.sh ${ISAAC_ROS_WS}
#   → 호스트 ws를 /workspaces/isaac_ros-dev에 마운트, --gpus all,
#     Jetson이면 Argus/카메라/디바이스 노드 노출, X11 등 설정을 자동화한 래퍼

# 2) 컨테이너 안에서 빌드(필요 패키지까지만 — 전체 빌드는 느림)
cd /workspaces/isaac_ros-dev
colcon build --symlink-install --packages-up-to isaac_ros_tensor_rt
source install/setup.bash
  • run_dev.sh(또는 동등 스크립트)가 하는 일: GPU 노출(--gpus all 또는 Jetson 런타임), 워크스페이스 마운트, 디바이스(카메라·Argus)·디스플레이 전달, 정합된 베이스 이미지 선택. 호스트엔 ROS/CUDA를 설치하지 않아도 된다 — 컨테이너가 환경을 들고 있다.
  • --packages-up-to: 의존 그래프상 그 패키지까지만 빌드. Isaac ROS 전체는 크니, 필요한 GEM까지만 빌드해 시간을 아낀다. --symlink-install은 launch·config 수정 시 재빌드 없이 반영.
  • Jetson 주의(9.4): 통합 메모리·전력 모드(nvpmodel)·jetson_clocks가 빌드/실행 성능에 영향. 빌드는 부하가 크다 — 스왑·전력 모드 확인.
  • colcon build가 끝나면 반드시 source install/setup.bash — 안 하면 ros2 launch가 패키지를 못 찾는다(흔한 첫 실수).

핵심: Isaac ROS 실습은 isaac_ros_common 컨테이너 진입 → 필요 GEM까지 colcon build → source 가 매번의 출발점이다. 컨테이너가 드라이버·CUDA·TensorRT·ROS 2를 정합된 한 묶음으로 들고 있어, 컨테이너 밖 빌드의 버전 의존성 충돌(dependency hell)(9.2·9.3)을 피한다. 경로·스크립트명은 릴리스마다 다르니 그 시점 문서로.

9.8.3 composable node·container — zero-copy의 그릇

9.7에서 강조했다: NITROS zero-copy는 노드들이 같은 프로세스(component container)에 있을 때만 성립한다. 그 "그릇"을 정확히 이해하자.

ROS 2 노드는 두 가지로 띄울 수 있다:

① 표준 노드(독립 프로세스) ② composable node — 한 component container(한 프로세스) DDS·직렬화· 복사 노드 A (프로세스) 노드 B (프로세스) A B C
  • ① 프로세스 경계를 넘으므로 zero-copy 불가 — 노드 간은 DDS·직렬화·복사.
  • ② A·B·C가 같은 주소공간(intra-process 포인터 전달) → NITROS zero-copy.
  • ComposableNode: 노드를 공유 라이브러리(.so) 안의 컴포넌트로 만든 것. rclcpp_components가 등록(register)한 플러그인 클래스를 런타임에 컨테이너에 적재한다. 별도 실행파일이 아니라 컨테이너에 삽입하는 부품이다.
  • ComposableNodeContainer: 컴포넌트들을 담는 프로세스(그릇). 종류:
    • component_container — 단일 스레드 executor. 콜백이 직렬화됨(한 코어).
    • component_container_mt — 멀티스레드 executor. 콜백 병렬 가능(9.8.7). 가속 그래프는 보통 이것을 쓴다.
    • component_container_isolated — 컴포넌트별 executor 격리(릴리스 의존).
  • 런타임 적재/조회(launch 없이도):
ros2 run rclcpp_components component_container_mt      # 빈 컨테이너 기동하고
ros2 component load /ComponentManager \
    isaac_ros_image_proc nvidia::isaac_ros::image_proc::RectifyNode   # 적재(클래스명 예시)
ros2 component list                                    # 적재된 컴포넌트 확인
  • 왜 중요한가: 같은 launch라도 노드를 Node(독립 프로세스)로 쓰거나 별도 컨테이너로 분리하면 intra-process가 깨져 NITROS가 표준 복사로 폴백한다(9.7.3). zero-copy를 원하는 가속 노드는 반드시 하나의 container에. 격리·내결함성을 위해 일부러 분할할 수도 있으나, 그것은 그 경계의 복사를 감수하는 의사결정이다(9.7.9).

핵심: ComposableNode(부품) + ComposableNodeContainer(그릇) 가 zero-copy의 물리적 전제다 — 같은 컨테이너 = 같은 프로세스 = 포인터 전달 가능. 가속 노드는 하나의 component_container_mt에 모은다. Node로 따로 기동하거나 컨테이너를 분리하면 NITROS는 조용히 표준 복사로 폴백한다(9.7).

9.8.4 launch로 그래프 조립 — remapping·parameter·namespace

이제 launch 파일로 노드를 잇는다. 핵심 도구는 remapping(토픽 배선)·parameter(노드 설정)·namespace(이름 공간) 다.

# my_perception/launch/perception.launch.py
# 가속 인지 그래프: 카메라→rectify→resize→encoder→TensorRT, 한 컨테이너에 적재
# (개념 골격 — 패키지·플러그인 클래스명·remap 키·파라미터는 릴리스 의존, 반드시 확인)
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import ComposableNodeContainer
from launch_ros.descriptions import ComposableNode

def generate_launch_description():
    engine_arg = DeclareLaunchArgument(
        'engine', default_value='/models/model.plan',     # 5.2 trtexec 산출
        description='TensorRT 엔진 경로(Jetson 타깃 빌드, 5.2·9.2)')

    rectify = ComposableNode(
        package='isaac_ros_image_proc',
        plugin='nvidia::isaac_ros::image_proc::RectifyNode',   # 예시 클래스명
        name='rectify',
        remappings=[('image_raw', '/camera/image_raw'),        # 입력 ← 카메라
                    ('camera_info', '/camera/camera_info'),
                    ('image_rect', 'rect/image')])             # 출력 → rect/image

    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', 'rect/image'),                   # ← rectify 출력
                    ('resize/image', 'net/image')])            # → net/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,
                     'image_mean': [0.5, 0.5, 0.5], 'image_stddev': [0.5, 0.5, 0.5]}],
        remappings=[('image', 'net/image'),
                    ('encoded_tensor', 'tensor_pub')])

    tensor_rt = ComposableNode(
        package='isaac_ros_tensor_rt',
        plugin='nvidia::isaac_ros::dnn_inference::TensorRTNode',
        name='tensor_rt',
        parameters=[{'engine_file_path': LaunchConfiguration('engine'),
                     'input_tensor_names': ['input_tensor'],
                     'input_binding_names': ['images'],         # ONNX 바인딩명과 일치(5.2)
                     'output_tensor_names': ['output_tensor'],
                     'output_binding_names': ['output']}],
        remappings=[('tensor_pub', 'tensor_pub'),
                    ('tensor_sub', 'inference_out')])

    # ★ 핵심: 네 노드를 같은 컨테이너에 → intra-process + 협상 → zero-copy(9.7)
    container = ComposableNodeContainer(
        name='perception_container', namespace='',
        package='rclcpp_components', executable='component_container_mt',
        composable_node_descriptions=[rectify, resize, encoder, tensor_rt],
        output='screen')

    return LaunchDescription([engine_arg, container])
  • remapping = 토픽 배선: 각 노드는 일반적 토픽명(image, image_raw)으로 구독/발행한다. remapping으로 실제 토픽명을 이어붙여 그래프를 만든다 — rectify의 출력 image_rect를 rect/image로, resize의 입력 image를 rect/image로 → 둘이 연결. remap 키(좌변)는 노드가 기대하는 이름이라 릴리스 문서로 확인.
  • parameter = 노드 설정: 크기·정규화 통계·엔진 경로 등. tensor_rt의 input_binding_names/output_binding_names는 ONNX 모델의 바인딩명과 정확히 일치해야 한다(5.2) — 틀리면 추론이 실패하거나 조용히 잘못된 텐서.
  • launch 인자(DeclareLaunchArgument): 엔진 경로처럼 바뀌는 값을 외부에서 주입 → ros2 launch ... engine:=/models/yolo.plan.
  • 합성(include): 카메라 드라이버·시각화는 별도 launch로 두고 IncludeLaunchDescription으로 합치면 재사용·교체가 쉽다(예: 실카메라 ↔ rosbag 재생 교체).

토픽 흐름(remapping이 만든 그래프):

rect/image net/image tensor_pub inference_out /camera/image_raw rectify resize encoder tensor_rt ...

견해: launch에서 remapping은 "배선", parameter는 "설정", namespace는 "이름 충돌 회피" 다. 가속 그래프 조립은 결국 ① 노드를 한 container에 모으고 ② remapping으로 출력↔입력 토픽을 이어붙이는 일. 바인딩명(5.2)·remap 키는 릴리스·모델 의존이라 문서로 정확히 맞춰야 추론이 산다 — 여기 오타가 "그래프는 뜨는데 결과가 없다"의 단골 원인.

9.8.5 워크스루 — 인지 파이프라인 기동하고 들여다보기

조립한 그래프를 실제로 기동하고 표준 ROS 2 도구로 들여다본다.

# 0) 컨테이너 안, 빌드·소스 완료 가정(9.8.2), 엔진 준비(5.2 trtexec로 .plan 빌드)
#    + 카메라 또는 rosbag 재생이 /camera/image_raw를 발행 중이어야 함

# 1) 그래프 실행
ros2 launch my_perception perception.launch.py engine:=/models/model.plan

# 2) 구조 확인
ros2 node list                       # /rectify /resize /encoder /tensor_rt (+ 컨테이너)
ros2 component list                  # perception_container에 적재된 4개 컴포넌트 확인 ★
ros2 topic list                      # rect/image net/image tensor_pub inference_out ...

# 3) 흐름·속도 확인(개략, 환경 의존)
ros2 topic hz /inference_out         # 추론 출력 처리율
ros2 topic hz /camera/image_raw      # 입력 프레임율과 비교(드롭 여부)

# 4) 그래프 시각화
rqt_graph                            # 노드·토픽 연결을 눈으로(배선 오류 즉시 발견)

# 5) 파라미터 점검
ros2 param list /tensor_rt
ros2 param get /resize output_width
구분신호 / 판정
기대(정상)ros2 component list → 4개 컴포넌트가 '한' 컨테이너(perception_container)에 ★
rqt_graph → rectify→resize→encoder→tensor_rt가 끊김 없이 연결
ros2 topic hz /inference_out → 카메라 프레임율에 근접(큰 드롭 없음)
이상 신호component list가 비거나 컴포넌트가 여러 컨테이너로 흩어짐 → zero-copy 안 됨(9.8.6)
rqt_graph에 끊긴 토픽 → remapping 오타(9.8.4)
inference_out이 안 나옴 → 바인딩명 불일치(5.2)·엔진 경로·입력 미발행
  • ros2 component list가 실질적 체크포인트: 네 컴포넌트가 하나의 컨테이너에 있어야 NITROS가 협상·zero-copy를 한다. 흩어져 있으면(또는 Node로 떴으면) 표준 복사 폴백이다.
  • 입력이 없으면 아무것도 안 나온다: 카메라 드라이버(Jetson Argus·RealSense 등)나 ros2 bag play로 /camera/image_raw가 발행돼야 한다. 개발 초기엔 rosbag 재생으로 결정적 입력을 주는 것이 편하다(9.8.11 연습5).
  • ros2 topic echo는 디버깅에만: 토픽 내용을 보려고 echo를 걸면 그 지점에서 경계 변환(D2H)이 유발돼 그래프가 느려진다(9.7.4). 운영에선 빼라.

견해: 그래프를 기동한 뒤의 첫 세 명령은 ros2 component list(한 컨테이너에 모였나)·rqt_graph(배선이 이어졌나)·ros2 topic hz(흐르나·드롭하나) 다. 이 셋이 "구조·연결·흐름"을 한 번에 보여 준다. 결과가 안 나오면 대개 배선(remapping)·바인딩명(5.2)·입력 미발행 중 하나 — echo로 들여다보되 운영에선 빼라.

9.8.6 협상·zero-copy 검증 — 실질적 가속의 증거

9.7의 핵심 경고를 실습으로 확인한다: 가속 그래프는 zero-copy가 깨져도 멀쩡히 실행된다(표준 복사로 폴백). 그래서 "느린데 왜인지 모름"을 피하려면 검증해야 한다.

① Nsight Systems(2.13) — 내부에 memcpy가 없는지. 이것이 zero-copy의 결정적 증거다.

# 그래프를 Nsight로 프로파일(CUDA·NVTX·OS 런타임 트레이스)
nsys profile -t cuda,nvtx,osrt -o perception_prof \
    ros2 launch my_perception perception.launch.py engine:=/models/model.plan
# (또는 떠 있는 프로세스에 attach — nsys 버전별 방법 확인)

타임라인에서 확인:

정상(zero-copy) 이상(폴백) H2D rectify · resize · encoder · TensorRT 커널들 D2H rectify H2D..D2H resize H2D..D2H encoder H2D..D2H ...
  • 정상: cudaMemcpy(H2D)는 진입 1회, D2H는 이탈 1회 — 내부 단계엔 memcpy 없음 ★.
  • 이상: 단계마다 cudaMemcpy 쌍 → 협상 실패/프로세스 분리(9.7.3).

② jtop / tegrastats(9.4) — 엔진 점유·CPU 부담.

jtop          # GPU 점유율↑(연산 중), CPU의 memcpy·직렬화 부담↓ 이면 가속 정상
              # GPU는 유휴 상태인데 CPU만 바쁘면 → 데이터 이동 병목(폴백 의심)
tegrastats    # 텍스트 버전(GPU·CPU·EMC 메모리 대역·전력)

③ 협상 결과 로그 — 어떤 타입으로 합의됐나.

# 디버그 로그로 NITROS 협상 결과 확인(로그 형식은 릴리스 의존)
# 주의: ros2 launch는 --ros-args를 직접 안 받는다(그것은 ros2 run용). 디버그 레벨은
#       launch 파일의 Node(arguments=['--ros-args','--log-level','debug'])로 설정한다.
ros2 launch my_perception perception.launch.py 2>&1 | grep -i nitros
#   → 인접 노드가 NITROS GPU 타입으로 합의했는지 vs 표준 sensor_msgs로 폴백했는지
  • 세 증거의 관계: Nsight가 "내부 memcpy 없음"을 보이고(①), jtop이 "GPU 점유·CPU 한가"를 보이고(②), 로그가 "NITROS 타입 합의"를 보이면(③) zero-copy가 실제로 성립한다. 하나라도 어긋나면 폴백을 의심.
  • 폴백의 흔한 원인(9.7·9.8.10): 노드가 별도 프로세스/컨테이너, 중간에 비-NITROS 노드, GEM·ROS 버전 비호환, ros2 topic echo 같은 디버그 탭.
  • Jetson 특수성(9.4): 통합 메모리라 "H2D"가 dGPU와 의미가 다르다(물리 복사 없이 매핑 가능) — Nsight 해석 시 유의하되, 직렬화 제거·핸들 전달의 이득은 그대로.

견해: "기동했다 ≠ 가속됐다." Isaac ROS 그래프는 zero-copy가 깨져도 동작하므로(조용한 폴백, 9.7), Nsight(내부 memcpy 부재)·jtop(GPU 점유·CPU 한가)·협상 로그 세 가지로 검증하는 습관이 핵심 역량이다. 이 검증을 건너뛰면 "GPU 샀는데 왜 안 빠르지"의 함정에 그대로 빠진다.

9.8.7 executor·콜백그룹·QoS — 병렬성과 센서 스트림

가속 그래프가 여러 노드를 한 컨테이너에서 실행하므로, executor(스레드)·콜백그룹·QoS 설정이 처리율·연결성을 좌우한다.

executor·콜백그룹. component_container_mt는 멀티스레드 executor를 쓴다. 하지만 한 노드의 콜백들은 기본적으로 mutually exclusive(상호 배타) 콜백 그룹이라 동시 실행되지 않는다.

컨테이너 / 콜백 그룹동작
component_container (단일)모든 콜백 직렬 → 한 코어. 깊은 파이프라인이 한 코어에 묶임
component_container_mt (멀티스레드 executor)단, 콜백 그룹에 따라:
└ 기본(mutually exclusive 그룹)같은 노드 콜백은 직렬, 노드 간은 병렬 가능
└ Reentrant 콜백 그룹같은 노드 콜백도 병렬(스레드 안전 책임은 코드에)

→ 깊은 가속 그래프는 component_container_mt + 노드별 병렬로 처리율↑.

  • 증상→처방: 멀티 코어인데 한 코어만 100% → component_container(단일)로 떴거나 콜백 그룹이 직렬 → component_container_mt + 필요 시 reentrant 콜백 그룹.
  • 단, 스레드를 늘린다고 항상 빨라지진 않는다 — GPU가 병목이면 CPU 스레드는 의미 없다(2.13으로 병목부터 확인).

QoS — 센서 스트림 정합. ROS 2는 발행자·구독자 QoS가 호환돼야 연결된다. 카메라 같은 고빈도 스트림은 보통 SensorDataQoS(best-effort·keep_last·작은 depth) 를 쓴다.

항목내용
카메라(발행)best-effort, keep_last, depth=5 (실시간·최신 우선, 드롭 허용)
구독자가 reliable로 받으려 하면QoS 비호환 → '연결 안 됨'(조용히)
처방센서 토픽 구독은 SensorDataQoS로 맞춤. depth는 지연↔버퍼 트레이드오프
  • 흔한 함정: 카메라는 best-effort인데 노드가 reliable로 구독 → 연결이 안 되는데 에러도 조용함 → ros2 topic hz가 0, rqt_graph에 연결은 보이나 데이터 없음. QoS 불일치 경고 로그 확인.
  • best-effort는 네트워크·부하 시 프레임을 드롭하지만 실시간 인지엔 최신 프레임이 더 중요하다(오래된 프레임으로 추론하면 위험, 8.10). depth는 작게.

핵심: 깊은 가속 그래프는 component_container_mt + (필요 시) reentrant 콜백 그룹으로 노드를 병렬화해 처리율을 낸다(한 코어 묶임 회피). 그리고 센서 토픽은 SensorDataQoS(best-effort)로 정합 — reliable과 섞으면 조용히 연결이 끊긴다. 둘 다 "조용한 실패"라 topic hz·QoS 로그로 확인.

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

  • [ ] isaac_ros_common 컨테이너 진입 → colcon build --packages-up-to → source 의 개발 흐름을 안다(9.2·9.3).
  • [ ] ComposableNode(부품)·ComposableNodeContainer(그릇)·component_container_mt 로 가속 노드를 한 프로세스에 모으는 이유(zero-copy 전제, 9.7)를 안다.
  • [ ] launch에서 remapping(배선)·parameter(설정)·launch 인자로 그래프를 조립하고, 바인딩명(5.2) 을 모델과 맞출 줄 안다.
  • [ ] 기동한 그래프를 ros2 component list·rqt_graph·topic hz 로 구조·연결·흐름 점검할 줄 안다.
  • [ ] Nsight(내부 memcpy 부재)·jtop(GPU 점유·CPU 한가)·협상 로그 로 zero-copy를 검증할 줄 안다.
  • [ ] component_container_mt·콜백그룹(병렬)과 SensorDataQoS(센서 정합)를 설정할 줄 안다.

증상→의심: ros2 component list에 컴포넌트가 흩어짐/비어 있음 → Node로 떴거나 컨테이너 분리 → 한 component_container_mt에. topic hz=0인데 rqt엔 연결 → QoS 비호환(best-effort↔reliable) → SensorDataQoS. 그래프는 뜨는데 inference_out 없음 → 바인딩명 불일치(5.2)·엔진 경로·입력 미발행 → 이름·경로·카메라/bag 확인. 한 코어만 100% → 단일 컨테이너/직렬 콜백 → _mt·reentrant. Nsight에 단계마다 memcpy → 협상 폴백 → 컨테이너·버전·중간 표준노드 점검. 빌드/런타임 라이브러리 충돌 → 컨테이너 밖에서 빌드 → 컨테이너 안에서(9.3).

9.8.9 트레이드오프·대안

결정트레이드오프
단일 container(_mt) vs 다중 containerzero-copy·저지연 vs 격리·내결함성(한 노드가 중단돼도 분리, 9.7.9)
component_container_mt vs component_container병렬 처리율 vs 단순·결정적 직렬 실행
reentrant vs mutually-exclusive 콜백그룹콜백 병렬 vs 스레드 안전 단순(상태 보호 불필요)
launch 합성(include) vs 단일 거대 launch재사용·교체(카메라↔bag) vs 한 파일 단순
실카메라 vs rosbag 재생(개발)실환경 vs 결정적·반복 가능·회귀(8.14·7.7)
--packages-up-to vs 전체 빌드빠른 빌드 vs 모든 GEM 가용

견해: 개발 단계에선 rosbag 재생 + 단일 component_container_mt + launch 합성이 가장 생산적이다 — 결정적 입력(bag)으로 반복·회귀하고(8.14), zero-copy를 유지하며(단일 컨테이너), 카메라/모델을 launch 인자·include로 교체한다. 운영·안전 단계에선 9.7.9처럼 가속 그래프를 격리 경계로 적절히 분리하고(경계 복사 감수) 안전 모니터를 분리하는 설계로 옮긴다 — 개발의 "한 컨테이너 zero-copy"와 운영의 "격리·내결함성"은 다른 최적점이다.

비엔비디아 정직: composable node·container·executor·QoS·remapping·rosbag은 모두 ROS 2 표준 메커니즘이다 — Isaac ROS 전용이 아니다. 그래서 이 장의 그래프 조립·검증 절차는 일반 ROS 2 가속 그래프(다른 벤더·CPU 노드)에도 그대로 적용된다. Isaac ROS가 더하는 것은 NITROS 전송 + 가속 GEM일 뿐, 조립·디버깅 도구(ros2 node/topic/component, rqt_graph, launch)는 표준이다.

9.8.10 흔한 함정

  • 컨테이너 밖에서 빌드. 호스트의 CUDA·TensorRT·ROS 버전과 충돌 → isaac_ros_common 컨테이너 안에서 빌드·실행(9.2·9.3).
  • source install/setup.bash를 빼먹음. ros2 launch가 패키지를 못 찾음 → 빌드 후 매 셸에서 source.
  • 노드를 Node(독립 프로세스)로 기동함. zero-copy 무산 → ComposableNode + 한 ComposableNodeContainer(9.7·9.8.3).
  • remapping 오타·바인딩명 불일치. 그래프는 뜨는데 결과가 없음 → rqt_graph로 배선 확인, input/output_binding_names를 ONNX와 일치(5.2).
  • QoS 비호환(best-effort↔reliable). 연결이 조용히 안 됨(topic hz=0) → 센서 토픽은 SensorDataQoS(9.8.7).
  • component_container(단일)로 병렬 기대. 한 코어만 → component_container_mt + 콜백 그룹.
  • 운영에서 ros2 topic echo/rqt 켜둠. 경계 D2H 유발로 느려짐 → 디버깅 때만(9.7.4).
  • 검증 없이 "가속됐다" 가정. 폴백이어도 동작하므로 모름 → Nsight·jtop·협상 로그로 확인(9.8.6).
  • 입력(카메라/bag) 없이 그래프만 기동함. 아무것도 안 나옴 → 드라이버/ros2 bag play 먼저.
  • 호스트 엔진을 Jetson에 그대로 사용. TensorRT 엔진은 GPU·TRT 버전 종속 → Jetson 타깃에서 재빌드(5.2·9.2).
  • 패키지·플러그인명·remap 키를 버전 불문 복붙. GEM 개명·API 변경 잦음 → 그 릴리스 문서로.

9.8.11 연습문제

연습 1 (그릇이 가속을 정한다). 같은 4노드 인지 그래프를 (a) 각 노드 Node로 따로 launch, (b) 하나의 component_container_mt에 ComposableNode로 적재 — 두 방식의 데이터 경로와 성능이 왜 다른지 9.7과 연결해 설명하고, 어느 명령으로 차이를 확인하는지 답하라.

정답

(a) 독립 프로세스(Node): 각 노드가 별도 프로세스라 주소 공간이 다르다. 노드 간 토픽 전달은 DDS를 거쳐 직렬화→전송→역직렬화되고, GPU 가속 노드는 입력을 받으려 H2D, 출력을 넘기려 D2H 해야 한다(9.7.2). NITROS의 GPU 핸들은 그 프로세스에서만 유효하므로(9.7 연습5), 협상은 표준 sensor_msgs로 폴백 → 단계마다 복사·직렬화 → 데이터 이동 병목. (b) 한 컨테이너(ComposableNode): 네 노드가 같은 프로세스라 intra-process 포인터 전달이 가능하고, 시동 시 협상으로 NITROS GPU 타입에 합의 → 가속 그래프 내부는 GPU 핸들만 zero-copy, 복사는 경계(진입 H2D·이탈 D2H) 2회만(9.7.3·9.7.4). 결과: (b)가 호스트 복사·직렬화·레이턴시·CPU 부담에서 크게 유리(4단계 1080p30 기준 호스트 복사 ~4배↓, 9.7.4). 차이 확인: ① ros2 component list — (b)는 4컴포넌트가 한 컨테이너에, (a)는 컴포넌트 없음(독립 노드). ② Nsight Systems(2.13) — (a)는 단계마다 cudaMemcpy 쌍, (b)는 진입·이탈에만. ③ jtop(9.4) — (a)는 CPU memcpy 부담↑·GPU 점유↓, (b)는 반대. ④ ros2 topic hz /inference_out — (b)가 더 높은 처리율·낮은 드롭. 핵심: Node냐 ComposableNode냐, 한 컨테이너냐라는 launch 선택이 zero-copy를 켜고 끈다.

연습 2 (배선 설계). 카메라(/cam/image_raw,/cam/camera_info) → rectify → resize(640²) → encoder → TensorRT 그래프를 launch의 remapping으로 설계하라. 각 노드의 입·출력 토픽명을 명시하고, 그래프가 끊기지 않는지 검증하는 명령을 답하라.

정답

remapping으로 출력↔입력 토픽명을 이어붙인다(키 좌변은 노드 기대 이름, 우변은 실제 토픽 — 릴리스 확인): rectify: 입력 image_raw←/cam/image_raw, camera_info←/cam/camera_info; 출력 image_rect→rect/image. resize: 입력 image←rect/image; 출력 resize/image→net/image; 파라미터 output_width=640,output_height=640. encoder: 입력 image←net/image; 출력 encoded_tensor→tensor_pub; 파라미터 network_image_width/height=640, mean·stddev(모델 학습 전처리와 일치). tensor_rt: 입력 tensor_pub←tensor_pub; 출력 tensor_sub→inference_out; 파라미터 engine_file_path, input_binding_names/output_binding_names(ONNX 바인딩명과 정확히 일치, 5.2). 흐름: /cam/image_raw →[rectify]→ rect/image →[resize]→ net/image →[encoder]→ tensor_pub →[tensor_rt]→ inference_out. 검증: ① rqt_graph — 네 노드가 위 토픽으로 끊김 없이 연결됐는지(끊긴 화살표=remap 오타). ② ros2 topic list — rect/image net/image tensor_pub inference_out 존재. ③ ros2 topic hz /inference_out>0 — 끝까지 흐르는지. ④ ros2 component list — 넷이 한 컨테이너(zero-copy). 결과가 없으면: 바인딩명(5.2)·엔진 경로·입력(카메라/bag) 발행·remap 키를 점검. 네 노드는 하나의 component_container_mt에 적재(zero-copy 전제).

연습 3 (zero-copy 검증 절차). "그래프를 띄웠는데 정말 zero-copy로 가속되는지 확신이 안 선다." Nsight·jtop·ros2 component list·협상 로그를 써서 단계적 검증 절차와 각 단계의 합격/불합격 판정을 제시하라.

정답

① 구조 — ros2 component list: 가속 노드들이 하나의 컨테이너에 적재됐는지. 합격=한 컨테이너에 전부, 불합격=흩어짐/비어 있음(→Node로 떴거나 컨테이너 분리 → zero-copy 불가, 한 _mt로 모음). ② 연결 — rqt_graph/topic hz: 토픽이 끊김 없이 이어지고 inference_out이 입력 프레임율에 근접해 흐르는지. 불합격=끊긴 토픽(remap 오타)·hz 0(QoS·입력·바인딩명). ③ 협상 로그 — --log-level debug | grep -i nitros: 인접 노드가 NITROS GPU 타입으로 합의했는지 vs 표준 sensor_msgs로 폴백했는지. 합격=NITROS 타입 합의, 불합격=표준 폴백(→비호환·버전·중간 표준노드). ④ 결정적 증거 — Nsight Systems(2.13): nsys profile -t cuda,nvtx,osrt로 타임라인 확인 — cudaMemcpy(H2D/D2H)가 그래프 진입·이탈에만 있고 내부 단계엔 없어야 합격. 단계마다 memcpy 쌍이 보이면 불합격(폴백). ⑤ 자원 — jtop/tegrastats(9.4): GPU 점유율↑·CPU memcpy 부담↓ 합격; GPU는 유휴 상태인데 CPU만 바쁘면 데이터 이동 병목(폴백) 불합격. 종합 판정: ①~⑤가 모두 합격이어야 zero-copy 확정. 하나라도 불합격이면 그 단계의 처방(컨테이너 통합·remap 수정·버전 정합·QoS·중간 표준노드 제거)을 적용 후 재검증. Jetson은 통합 메모리라 ④의 H2D 해석에 유의(물리 복사 없이 매핑 가능)하되 ①②③⑤로 보완. 요지: "기동했다≠가속됐다" — 폴백이어도 동작하므로 이 절차로 증명해야 한다.

연습 4 (병렬성). 가속 그래프를 띄웠더니 CPU 8코어 중 한 코어만 100%이고 처리율이 낮다. 원인 후보와 진단·해결을 제시하고, 스레드를 늘려도 안 빨라질 수 있는 경우를 설명하라.

정답

원인 후보: ① component_container(단일 스레드)로 기동함 — 모든 콜백이 한 executor 스레드에서 직렬 → 한 코어. 진단: launch의 executable 확인. 해결: component_container_mt. ② 콜백 그룹이 mutually exclusive — _mt라도 한 노드의 콜백이 직렬, 또는 그래프가 사실상 직렬 의존이라 병렬화 안 됨. 진단: 노드별 코어 사용. 해결: 적절히 reentrant 콜백 그룹(상태 보호 책임은 코드에), 노드 분리. ③ GPU/특정 단계 병목 — CPU가 아니라 GPU(또는 한 고부하 단계)가 병목이면 CPU 스레드를 늘려도 무의미. 진단: Nsight(2.13)·jtop(9.4) 로 병목이 GPU인지 CPU인지. 스레드를 늘려도 안 빨라지는 경우: 병목이 GPU 연산이거나(TensorRT 단계가 GPU를 포화시킴), 데이터 의존이 직렬(파이프라인이 본질적으로 단계 순차)이거나, 메모리 대역(1.9)이 한계면 CPU 스레드 추가는 효과가 없다 — 이땐 모델 최적화(양자화·배치, 5.4·5.8)·엔진 분업(DLA, 8.4)·해상도/프레임율 조정이 답. 핵심: 먼저 Nsight로 병목을 찾고(CPU 직렬 vs GPU 포화 vs 메모리), CPU 직렬이면 _mt+콜백그룹, GPU/메모리면 모델·엔진 차원의 최적화. "느리면 스레드부터 늘린다"는 병목을 모르고 하는 무익한 작업이 되기 쉽다.

연습 5 (QoS·결정적 개발). (a) 카메라는 best-effort QoS인데 첫 노드가 reliable로 구독해 연결이 안 된다 — 진단과 수정. (b) 실카메라 없이 그래프를 결정적으로 개발·회귀하려면 어떻게 하는가?

정답

(a) QoS 불일치: ROS 2는 발행자·구독자 QoS가 호환돼야 연결된다. 카메라는 보통 SensorDataQoS(best-effort·keep_last·작은 depth)로 발행하는데, 구독자가 reliable을 요구하면 비호환이라 연결이 안 되는데 에러는 조용하다(경고 로그만). 진단: ros2 topic hz가 0인데 rqt_graph엔 노드·토픽이 보임 → QoS 의심 → 노드 QoS 설정·QoS 불일치 경고 로그 확인. 수정: 센서 토픽 구독을 SensorDataQoS(best-effort) 로 맞춘다(노드 파라미터 또는 코드). depth는 작게(최신 프레임 우선 — 오래된 프레임 추론은 위험, 8.10). best-effort는 부하 시 프레임을 드롭하지만 실시간 인지엔 최신성이 더 중요. (b) 결정적 개발 — rosbag 재생: 실카메라 대신 ros2 bag record로 센서 스트림(영상·camera_info·TF·clock)을 한 번 녹화해 두고, 개발·테스트 때 ros2 bag play로 재생한다. 이점: ① 결정적·반복 가능 — 같은 입력으로 매번 같은 그래프 동작 → 회귀 테스트(8.14 데이터 엔진·7.7 플라이휠 정신). ② 실환경 불필요 — 카메라·로봇 없이 개발. ③ 엣지 케이스 보존 — 실패한 장면을 bag으로 모아 재현·수정. 주의: bag의 QoS도 원본과 맞춰 재생(불일치면 (a)와 같은 문제), 타임스탬프·use_sim_time·/clock 정합(시간 동기 8.7), 영상이 크면 bag 용량·디스크 대역 고려. launch를 합성(include) 으로 작성해 두면 카메라 launch ↔ bag play를 인자로 교체 쉽다(9.8.9). 운영 검증은 bag(결정적) + 실카메라(실환경)를 병행한다 — bag만으론 실시간·실센서 특성을 모두 반영하지 못한다.

9.8.12 요약 · 다음 장

  • 개발 흐름: isaac_ros_common 컨테이너 진입 → colcon build --packages-up-to → source (9.2·9.3). 컨테이너가 드라이버·CUDA·TensorRT·ROS 2를 정합으로 들고 있어 버전 충돌을 피한다.
  • zero-copy의 그릇: ComposableNode(부품) + ComposableNodeContainer(component_container_mt) 에 가속 노드를 모아야 intra-process·협상이 성립(9.7). Node로 따로 기동하거나 컨테이너를 분리하면 표준 복사로 폴백.
  • 조립: launch의 remapping(배선)·parameter(설정)·인자로 카메라→rectify→resize→encoder→TensorRT를 잇는다. 바인딩명(5.2)·remap 키를 모델·릴리스에 맞추는 것이 "결과가 나오느냐"를 가른다.
  • 검증이 핵심: 가속 그래프는 폴백이어도 동작하므로 ros2 component list(한 컨테이너)·Nsight(내부 memcpy 부재, 2.13)·jtop(GPU 점유·CPU 한가, 9.4)·협상 로그 로 zero-copy를 증명한다. "기동했다≠가속됐다."
  • 병렬성·QoS: component_container_mt+콜백그룹으로 처리율(단, GPU 병목이면 무의미 — Nsight로 병목부터), 센서 토픽은 SensorDataQoS(best-effort)로 정합(reliable과 섞으면 조용히 끊김). 개발은 rosbag 재생으로 결정적·반복. 정직: 패키지·클래스명·API는 릴리스 의존.

다음 장: 9.9 Isaac 인지 — cuVSLAM·nvblox. 이제 가속 그래프를 조립·검증할 줄 알았으니, 그 위에 구축하는 고수준 인지 GEM 으로 간다 — 카메라·IMU로 로봇의 위치를 추정하는 cuVSLAM(9.6 VPI 특징·스테레오 전단 위에서)과, 깊이로 3D 점유 지도를 재구성하는 nvblox. NITROS로 이어진 zero-copy 그래프가 SLAM·매핑이라는 실제 로봇 인지로.