Perception › ROS 2 implementation › Acronym glossary
Middleware and real-time engineering
The architecture names the nodes and their deadlines. This tutorial explains what happens between two callbacks, why QoS is a safety choice, and exactly when an image or point cloud moves without a copy. Numbers below are design budgets, not measured guarantees; validate them on the target hardware and ROS distribution.
1 · Follow a message through ROS 2
A node is a collection of entities, not a thread or process. A context initializes the middleware; a container holds multiple nodes in one process. Performance diagnosis starts at the correct layer: CDR fragmentation/transport for a multi-megabyte cloud, executor dispatch for callback jitter, CUDA event dependencies for inference stalls.
| rmw | Reason to select | Qualification question |
|---|---|---|
| Cyclone DDS | Configurable peers, PSMX/iceoryx shared-memory path | Is this build's PSMX actually enabled and sized for the samples? |
| Fast DDS | Widely deployed; SHM transport and async publisher option | Does its selected type/transport loan memory, or merely serialize into SHM? |
| Connext DDS | QoS and observability tooling | Licensing and target availability? |
| Zenoh rmw | Router-based discovery, useful across networks | Are its release-specific QoS and SHM guarantees tested? |
Constrain discovery on the vehicle network; pin the rmw version and test large-message transport under packet loss. One participant per node is not a portable assumption: participant ownership is implementation/version-specific.
Time contract: stamp at hardware capture (PTP/trigger), use ROS time for message stamps and bag replay, a monotonic steady clock for latency and watchdog durations, and never assume cross-topic DDS ordering. For six cameras plus a LiDAR sweep, exact sync is brittle without common triggers; approximate sync may wait a whole period. Sweep-anchored sync picks the nearest image within a bounded slop and carries a valid-view mask. Cache static TF from map → odom → base_link → sensors at configuration; never block a model callback on TF. Use continuous odom for derivatives: map → odom can jump on loop closure.
2 · Design QoS per link, not per project
Ask five questions: (1) Is a late sample worse than a missing one? (2) How large is it, and can it cross a process boundary without fragments? (3) Must a restarted subscriber receive the previous value? (4) What depth absorbs producer jitter without retaining stale work? (5) Which deadline, lifespan and liveliness events expose failure? KEEP_LAST(1) bounds a queue but silently drops data: publish sequence-gap counters.
| Link (nominal) | Reliability / history / durability | Deadline · lifespan | Trade-off / failure if wrong |
|---|---|---|---|
| 6× camera raw (10–30 Hz, 2–6 MB) | Best effort / last 1–2 / volatile | 1.5 periods · 100 ms | Reliable retransmission of a fragmented stale frame blocks the next. |
| CameraInfo (on change) | Reliable / last 1 / transient local | — | Late joiners need intrinsics; validate stamp and calibration revision. |
| LiDAR full sweeps (10–20 Hz, 3–10 MB) | Best effort / last 1 / volatile | 1.5 periods · 150 ms | Keep-last 10 builds an old-sweep backlog; 1 trades completeness for age. |
| LiDAR raw packets (~kHz, small) | Best effort / last 100–200 / volatile | — · 50 ms | Small bursts need depth; track missing azimuth slices. |
| Radar tracks (10–20 Hz) | Best effort or reliable / last 5 / volatile | 2 periods · 200 ms | Independent AEB fallback may warrant reliable on a separate executor. |
| IMU (100–1000 Hz) | Best effort / last 10–50 / volatile | 2 periods · 20 ms | Integrator needs ordered history; track gaps. Batched IMU reduces callback count. |
| GNSS fix / velocity (1–10 Hz) | Reliable / last 5 / volatile | 2 periods · 1 s | Few small fixes; reject poor status/covariance despite successful delivery. |
| Wheel/VIO odom (50–200 Hz) | Best effort or reliable / last 10 / volatile | 2 periods · 100 ms | Freshest for control; history for state estimation. |
| Dynamic TF | Reliable / last 100 / volatile | — | Transform buffers need history for stamped lookup. |
| Static TF | Reliable / sufficient depth / transient local | — | Restarted nodes need extrinsics; check multiple-publisher retention. |
| SensorFrame / GPU tensors (intra-process) | Best effort / last 1 / volatile | 120 ms · bounded | Never latch process-local CUDA pointers or let them escape the container. |
| Detections3D (10 Hz) | Reliable / last 5 / volatile | 150 ms · 300 ms | Useful for tracker, but consumer must reject stale timestamps. |
| TrackArray (10 Hz) | Reliable / last 5 / volatile (optional transient local) | 150 ms · 500 ms | Latch helps restart but may deliver an obsolete world state; check age. |
| BEV segmentation / occupancy (5–10 Hz) | Best effort / last 1 / volatile | 250 ms · 500 ms | Large self-contained grids supersede earlier ones. |
| Static map (on change) | Reliable / last 1 / transient local | — | Large but late subscribers require it; budget writer-cache memory. |
| Actuator command (50–200 Hz) | Reliable / last 1 / volatile | 1.2 periods · 2 periods | Also use an independent wall/steady-time actuator watchdog; DDS events may be delayed. |
| Joint states (100–500 Hz) | Best effort / last 10 / volatile | 2 periods · 50 ms | Latest state wins. |
| Diagnostics / rosout (1–10 Hz) | Reliable / last 10–100 / volatile | — | Important evidence but never on the model executor. |
| Parameter events / service endpoints | Service/default reliable profile | Not a streaming freshness contract | Keep configuration off the critical path. |
ros2 topic echo, inspect offered QoS before debugging drivers.ros2 topic info /lidar/points --verbose
ros2 topic echo /lidar/points --qos-reliability best_effort --qos-depth 1 --once
Wire requested/offered deadline, liveliness, and incompatible-QoS event callbacks into health_node. A deadline is a signal, not a hard scheduling guarantee. A manual liveliness lease must be asserted after completed inference, not before a GPU hang; separately poll the CUDA completion event. Size DDS resource limits, socket buffers and fragment limits, and prefer async publishing if large inter-process serialization would block a callback.
3 · Executors: dispatch is a scheduling decision
An executor waits on subscriptions, timers, services, clients and guard conditions then runs ready callbacks. The usual single/multithreaded executors do not promise strict priority, fairness or a worst-case dispatch time. Dispatch details (including timer ordering) change across distributions; measure and test on your pinned ROS version. A one-thread executor serializes everything; a multithreaded executor permits parallel groups, but its default mutually-exclusive group serializes all callbacks within each node.
| Choice | Good for | Cost / caution |
|---|---|---|
| SingleThreadedExecutor | Small deterministic eval container | One blocking callback delays all others. |
| MultiThreadedExecutor (explicit thread count) | Independent camera, LiDAR and head groups | Not a priority scheduler; unlimited threads contend with CUDA and DDS. |
| StaticSingleThreaded / EventsExecutor | Fixed/high-entity-count graph, when supported | Distribution/version availability and semantics require benchmarking. |
| Dedicated callback-group executors / WaitSet | Different criticalities, explicit ordering | More lifecycle and shutdown plumbing; verify bounded work per turn. |
Put a stateful tracker and each single-context TensorRT stage into MutuallyExclusive groups; use Reentrant only where concurrent callbacks have independent storage. Explicitly routed groups must be created with automatically_add_to_executor_with_node=false, then attached by add_callback_group(group, node_base) before spinning. Put only remaining default groups into the housekeeping executor; do not add the same node indiscriminately to multiple executors.
auto gpu_group = node->create_callback_group(
rclcpp::CallbackGroupType::MutuallyExclusive, false);
gpu_exec.add_callback_group(gpu_group, node->get_node_base_interface());
std::thread gpu_thread([&] {
set_affinity({6,7,8,9}); // implemented in perception_common/thread_utils.hpp
gpu_exec.spin();
});
Failure drills: same-group synchronous service call deadlocks; default groups unexpectedly serialize; an overactive timer starves sensor work; depth-one queues hide skips; reentrant tracker corrupts identities; engine deserialization or TF wait inside a callback freezes the pipeline; implicit hardware-concurrency thread counts thrash an embedded SoC. Use traces and counters to demonstrate each fix.
4 · One process, multiple nodes and threads
Register each component with the component macro and forward the incoming NodeOptions to its base constructor: remaps, parameter overrides and intra-process configuration otherwise disappear. For bring-up, use component_container_mt plus use_intra_process_comms=true; for bounded executor ownership use the custom perception_container_main with three executors. Dynamic components allow loading heads at runtime; the custom main makes allocation, affinities and explicit group routing inspectable.
| Architecture | Gain | Risk / boundary |
|---|---|---|
| Separate process per model stage | Fault isolation | CDR copies, duplicate GPU contexts, discovery and memory costs. |
| One perception container | Pointer handoffs, shared CUDA pool/context and parallel streams | Any segfault kills all stages; watchdog + process supervisor required. |
| Separate driver, GNC, eval, logger | Privilege, scheduling and failure isolation | Cross-process results must be age-checked; heavy images need SHM or optimized intake. |
Lifecycle bring-up: configure calibration, deserialize engines and allocate fixed pools before motion; activate publishers only after checks pass; on GPU fault deactivate, emit FAULT, then attempt cleanup/reconfigure while the GNC process owns safe stop. Hot-swapping a detector without checking input/output contracts and vehicle state is not safe merely because dynamic loading is available.
5 · Four different meanings of “zero copy”
| Mechanism | Scope | Preconditions | What still copies? |
|---|---|---|---|
| A · intra-process ownership move | Same process | unique_ptr publish, single owning subscriber, volatile, intra-process enabled | Adding another owning subscriber or publishing by const reference may copy. |
| B · intra-process read-only share | Same process, fan-out | Const shared callbacks, same allocation and refcounted lifetime | An exclusive/mutating subscriber requires a copy. |
| C · rmw loan / SHM | Cross process, if supported | Actually loanable fixed-size/POD message, matching rmw/SHM configuration | Dynamic ROS sequences, rmw fallback, CPU→GPU transfer. |
| D · TypeAdapter / GPU handle | GPU data shared inside process | Device buffer owner, process guard, stream completion event, explicit wire conversion | Ingest H2D (unless true camera DMA/import), any external debug subscriber conversion. |
A/B: a moved image has the same object address at publisher and owning subscriber. With multiple read-only consumers prefer a shared ownership callback; never retain a reference beyond callback lifetime. Volatile durability is necessary for the intra-process hot path; depth-one can discard an unprocessed frame, so count it. An external subscriber may cause a second serialized publication even while internal delivery is copy-free; never promise no serialization just because the nodes are composed.
auto image = std::make_unique<sensor_msgs::msg::Image>();
auto* before = image.get();
pub->publish(std::move(image)); // compare 'before' with owning callback's m.get()
D: the GPU adapter carries a pool-owned CUDA allocation, dimensions, encoding, producer PID and readiness event. The consumer calls cudaStreamWaitEvent(consumer_stream, ready_event), not device-wide synchronization. An address alone is not inter-process transferable: use CUDA IPC/NvSci with ownership protocol if needed, or stage into a safe wire type. On integrated-GPU SoCs evaluate mapped pinned/NvSci buffers; on discrete GPUs budget H2D and host pinning. “GPU-direct camera” requires a capture path that imports DMA buffers into the device; wrapping a CPU image in a pointer does not eliminate H2D.
Image or PointCloud2 at a chosen rate; that conversion intentionally copies.6 · Loaned messages and the driver boundary
For a separate privileged LiDAR driver, small packets can cross via standard messages. If a decoded full sweep must cross processes without a CPU copy, use an rmw-supported, fixed-capacity point array with num_points; for a 1920×1080 RGB image the fixed RGB8 array needs 6,220,800 bytes. Standard Image.data and PointCloud2.data are dynamically sized, so borrow_loaned_message() cannot be assumed to provide true middleware-backed zero copy for them. can_loan_messages() and transport instrumentation distinguish a real loan from heap fallback.
| Fixed-capacity POD | Standard dynamic ROS message |
|---|---|
| SHM loan may avoid serialization and second CPU copy | Excellent rviz, bag and ecosystem interoperability |
| Worst-case chunk × history depth; ABI/capacity constraints | Uses actual payload size; may serialize/copy across process |
| Custom converter required for tooling | Easy replay and recording |
Prefer the decoder inside the perception container: de-skew each point with its time offset and ego odometry, then upload/decode into GPU tensors once. A fixed-capacity packet or sweep sample is useful only when measured cross-process cost justifies custom types. Fast DDS SHM + async publish and Cyclone PSMX/iceoryx both require release-specific configuration, sufficient chunk pools and verified fallback behavior; SHM transport is not synonymous with a middleware loan.
7 · Services, actions, parameters and lifecycle
| Need | Mechanism | Why not another mechanism? |
|---|---|---|
| Camera frames, tracks, health | Topics + per-link QoS | Services serialize requests and have no streaming freshness contract. |
| Reset evaluation counters or retrieve one snapshot | Short service in housekeeping/eval executor | Never wait synchronously inside a model callback. |
| Recalibrate / load a long-running model with feedback | Action or managed lifecycle transition while parked | A service timeout cannot safely cancel prolonged work. |
| Calibration / map for a restarted subscriber | Reliable transient-local topic (versioned) | Subscriber must validate stamp/revision before use. |
| Model path, point-cloud layout | Read-only startup parameter; change via deactivate/configure | Changing tensor geometry in-flight corrupts buffers. |
| Detection threshold, camera slop | Validated dynamic parameter with atomic snapshot | Validate ranges and log version; avoid partial mid-frame changes. |
Declare parameters with descriptors, ranges and read-only restrictions; validate through an on-set callback, snapshot a consistent configuration at the frame boundary, and version changes in recordings. Use configure → activate → deactivate → cleanup lifecycle transitions to prevent engine compilation inside an active deadline. Do not call spin_until_future_complete from a callback whose response needs the same single-threaded/mutually-exclusive executor; use asynchronous client callbacks or a separately routed group.
8 · Make a real-time claim falsifiable
Preallocate CUDA, host and message pools; prebuild TRT engines; lock critical memory and prefault stacks; avoid filesystem/allocations/blocking TF in callbacks. Pin critical threads and prioritize GNC separately from GPU work; SCHED_FIFO requires explicit privileges and bounded callback execution or it can starve the entire system. Use stream events for camera/LiDAR overlap and serialize the fuser only when its inputs are ready. Keep only the newest pending sweep when inference exceeds its 100 ms period; drop and count the rest rather than accumulating latency.
| Claim | Measurement / fail condition |
|---|---|
| One-process move | Publisher/subscriber pointer identity in the one-owner case; refcounted fan-out separately. |
| Real SHM loan | can_loan_messages(), rmw transport introspection and no serialize/copy trace. |
| No GPU intermediate copy | Nsight timeline: H2D at ingest only unless DMA import; no BEV tensor D2H/H2D. |
| Bounded freshness | p50/p95/p99/p99.9 capture-to-track age and drop rate in hot/cold, loss and thermal tests. |
| Controlled degradation | Stop one camera, hang GPU, restart planner, corrupt TF and impose disk load; verify GNC response. |
Compare ROS tracepoints (publish → callback), CUDA stream events, PTP/hardware timestamps and steady-clock age. An executor hop microbenchmark is not a total system deadline proof. Also measure detection quality conditioned on dropped frames; a stack that meets 95 ms by discarding hard frames has not solved the vehicle problem.