Skip to navigation

Core Runtime Benchmarks

View as Markdown

The SDK ships its runtime performance coverage in-tree under the top-level benchmark/ directory, alongside its correctness tests. This page separates public transport-through-Graph measurements from implementation microbenchmarks used by SDK developers. Channel-focused transport results are covered on Channel Benchmarks. Application sensor-to-actuation measurements are not yet available and remain unpublished.

You will learn
  • What one controlled x86_64 run measured through a running Graph.
  • Which internal suites help SDK developers diagnose runtime costs locally.
Controlled x86_64 results

The EA2 tables report medians across seven repetitions on one controlled x86_64 workstation. Single-process latency rows use at least 10,000 measured samples and report p50, p99, and p99.9. Interprocess latency rows use 1,000 warmup plus at least 1,000 measured samples and deliberately stop at p99. Throughput is unpaced. The 1:1 and chain latency rows use 200 µs pacing and queue depth 1; fanout latency uses queue depth 2. Throughput also uses queue depth 2.

Transport through a running Graph

The controlled suite reports comparable payload and process-topology dimensions where matched HoloMQ, Channel, and Graph rows exist. Coverage is not identical across all three layers. These are independent measurement boundaries rather than additive substages: do not subtract one row from another to infer framework overhead.

Three solid boxes represent separate HoloMQ transport, typed Channel facade, and running Graph measurements. A dashed fourth box marks future application end-to-end measurements that are not yet published.HoloMQ transportindependent boundaryChannel facadeindependent boundaryRunning Graphindependent boundaryApplication E2Enot yet measuredCompare matched rows; do not subtract independently timed boundaries.

Single-process Graph transport latency

TopologyPayloadStorageApplication payloadp50p99p99.9Notes
Single-process 1:1BenchmarkPod64Inline64 B6.560 µs7.722 µs8.057 µsSleeping wait set
Single-process 1:1schema::TimeTInline16 B6.533 µs7.131 µs7.575 µsGenerated-schema encode and decode
Single-process 1:1TensorPinned host1 MiB7.154 µs7.928 µs8.557 µsDescriptor-only; backing pools initialized during warmup
Single-process 1:1TensorDevice1 MiB8.558 µs9.572 µs10.244 µsDescriptor-only; backing pools initialized during warmup
Single-process 1:1runtime_benchmark::MultiTensorMessageTPageable host2 MiB9.856 µs10.584 µs11.167 µsTwo rank-1 uint8 Tensor fields; independent preinitialized pools
Single-process 1:1schema::ImageTPageable host1 MiB8.499 µs9.228 µs9.579 µsGenerated schema with preinitialized Tensor backing
Single-process 1:1schema::ImageTPageable host1 MiB8.189 µs8.854 µs9.131 µsAs above; the sink borrows each sample with selected_sample() instead of an owning copy
Single-process 1:2, shared partitionBenchmarkPod64Inline64 B5.058 µs5.367 µs5.495 µsShared operator partition; direct-retained fanout; eventless; pump-free
Single-process 1:2, separate partitionsBenchmarkPod64Inline64 B8.362 µs9.095 µs9.395 µsOne source partition and one partition per subscriber; direct-retained fanout; eventless; pump-free
Single-process 1:4, shared partitionBenchmarkPod64Inline64 B9.617 µs10.001 µs10.163 µsShared operator partition; direct-retained fanout; eventless; pump-free
Single-process 1:4, separate partitionsBenchmarkPod64Inline64 B10.529 µs11.100 µs11.313 µsOne source partition and one partition per subscriber; direct-retained fanout; eventless; pump-free
Single-process 10-operator chainBenchmarkPod64Inline64 B33.985 µs35.113 µs36.192 µsSource, eight relays, and sink

Interprocess Graph transport latency

TopologyPayloadStorageApplication payloadp50p99Notes
Two-process 1:1BenchmarkPod64Inline64 B16.324 µs17.143 µsSleeping wait set
Two-process 1:1schema::TimeTInline16 B16.566 µs17.554 µsGenerated-schema encode and decode
Two-process 1:1TensorPinned host1 MiB22.200 µs23.303 µsPreinitialized backing; host writer, so no readiness fence
Two-process 1:1TensorPinned host1 MiB26.995 µs28.336 µsPreinitialized backing; CUDA stream writer, so the consumer waits on its readiness fence
Two-process 1:1TensorDevice1 MiB26.964 µs28.199 µsPreinitialized backing
Two-process 1:1schema::ImageTPageable host1 MiB21.567 µs22.697 µsGenerated schema with preinitialized Tensor backing
Two-process 1:1schema::ImageTPinned host1 MiB24.010 µs25.199 µsGenerated schema with preinitialized Tensor backing; host writer, so no readiness fence
Two-process 1:1schema::ImageTPinned host1 MiB28.878 µs30.129 µsGenerated schema with preinitialized Tensor backing; CUDA stream writer, so the consumer waits on its readiness fence
Two-process 1:1schema::ImageTDevice1 MiB28.950 µs30.350 µsGenerated schema with preinitialized Tensor backing
Two-process 1:1runtime_benchmark::MultiTensorMessageTPageable host2 MiB24.516 µs25.547 µsTwo rank-1 uint8 Tensor fields; independent preinitialized pools
Three-process 1:2BenchmarkPod64Inline64 B20.111 µs25.754 µsFanout
Three-process chainBenchmarkPod64Inline64 B32.961 µs34.478 µsProducer, relay, and consumer

The table’s latency boundary is publication_to_receive: its source timestamp is taken immediately before emit(), after any producer-side Tensor loan acquisition and writes, and its sink timestamp is taken immediately after receive(). The docs preset uses descriptor-only raw Tensor and multi-Tensor rows. Their compiler-owned pool backing is initialized during warmup, so no backing memset occurs during measured publications; the rows isolate descriptor publication, Runtime execution, retention, and receive. The full callback and Tensor phase timings remain available in the artifacts, and the runner retains a full_write profile for separate producer-work diagnostics.

runtime_benchmark::MultiTensorMessageT contains one uint64 sequence plus two independent rank-1 uint8 pageable-host Tensor fields, first and second. Each field is 1 MiB and has its own compiler-owned pool, for a 2 MiB application payload. The interprocess 1:2 row uses queue depth 2 and 5 ms pacing to provide the flow-control headroom required by that topology.

The p99 and p99.9 values are descriptive. Seven repetitions characterize this controlled run but do not establish a tail-latency acceptance bound. The interprocess rows use at least 1,000 measured samples, which support p99 but not a consistently useful p99.9 estimate, so the interprocess table omits that column.

Single-process Graph throughput

TopologyPayloadStorageApplication payloadLogical messages/sAggregate deliveries/sNotes
Single-process 1:1BenchmarkPod64Inline64 B197,837197,837Sleeping wait set
Single-process 1:1schema::TimeTInline16 B273,673273,673Generated-schema encode and decode
Single-process 1:1TensorPinned host1 MiB193,925193,925Descriptor-only; backing pools initialized during warmup
Single-process 1:1TensorDevice1 MiB120,092120,092Descriptor-only; backing pools initialized during warmup
Single-process 1:1runtime_benchmark::MultiTensorMessageTPageable host2 MiB98,63898,638Two rank-1 uint8 Tensor fields; independent preinitialized pools
Single-process 1:1schema::ImageTPageable host1 MiB135,504135,504Generated schema with preinitialized Tensor backing
Single-process 1:1schema::ImageTPageable host1 MiB147,535147,535As above; the sink borrows each sample with selected_sample() instead of an owning copy
Single-process 1:2, shared partitionBenchmarkPod64Inline64 B168,961337,922Shared operator partition; direct-retained fanout; eventless; pump-free
Single-process 1:2, separate partitionsBenchmarkPod64Inline64 B204,854409,707One source partition and one partition per subscriber; direct-retained fanout; eventless; pump-free
Single-process 1:4, shared partitionBenchmarkPod64Inline64 B95,505382,022Shared operator partition; direct-retained fanout; eventless; pump-free
Single-process 1:4, separate partitionsBenchmarkPod64Inline64 B161,003644,010One source partition and one partition per subscriber; direct-retained fanout; eventless; pump-free
Single-process 10-operator chainBenchmarkPod64Inline64 B34,23634,236Source, eight relays, and sink

Interprocess Graph throughput

TopologyPayloadStorageApplication payloadLogical messages/sAggregate deliveries/sNotes
Two-process 1:1BenchmarkPod64Inline64 B149,946149,945Sleeping wait set
Two-process 1:1schema::TimeTInline16 B157,342157,341Generated-schema encode and decode
Two-process 1:1TensorPinned host1 MiB79,65479,646Preinitialized backing; host writer, so no readiness fence
Two-process 1:1TensorPinned host1 MiB73,67373,670Preinitialized backing; CUDA stream writer, so the consumer waits on its readiness fence
Two-process 1:1TensorDevice1 MiB73,76473,755Preinitialized backing
Two-process 1:1schema::ImageTPageable host1 MiB111,980111,976Generated schema with preinitialized Tensor backing
Two-process 1:1schema::ImageTPinned host1 MiB80,37180,358Generated schema with preinitialized Tensor backing; host writer, so no readiness fence
Two-process 1:1schema::ImageTPinned host1 MiB66,64266,640Generated schema with preinitialized Tensor backing; CUDA stream writer, so the consumer waits on its readiness fence
Two-process 1:1schema::ImageTDevice1 MiB58,76558,760Generated schema with preinitialized Tensor backing
Two-process 1:1runtime_benchmark::MultiTensorMessageTPageable host2 MiB80,42480,420Two rank-1 uint8 Tensor fields; independent preinitialized pools
Three-process 1:2BenchmarkPod64Inline64 B125,174250,343Fanout
Three-process chainBenchmarkPod64Inline64 B88,99388,991Producer, relay, and consumer

The tables preserve logical publications/s and aggregate deliveries/s as separate metrics. For fanout, one logical publication produces one delivery per subscriber, so aggregate delivery rate scales with subscriber count. The small 1:1 difference between the rates is measurement-window alignment, not data loss. The paired single-process fanout rows compare one shared operator partition with separate source and subscriber partitions. Both arrangements measure the default process-local direct-retained fanout: the source’s emit() retains the sample for every subscriber directly, so neither needs an event wake or a channel-pump thread. The separate rows also give every subscriber its own partition on its own pinned CPU, which adds partition-owner wake and scheduling costs while allowing subscriber callbacks to run in parallel. The Notes column records the delivery path and wake mechanism present in each measurement.

For representative 1:1 rows, the running Graph measured 6.533–9.856 µs single-process and 16.324–28.950 µs interprocess p50 latency. This boundary includes Runtime wake-up, controller and any channel-pump coordination, readiness selection, operator activation, and receive materialization. It is therefore expected to cost more than the lower-level HoloMQ and Channel measurements, but the boundaries are independently timed and must not be subtracted to infer one internal stage.

Provenance and caveats

These measurements are evidence of cost structure on this machine, not performance guarantees for other hardware or application graphs.

FieldValue
Run date and artifact digest2026-10-16; summary.json SHA-256 7efd47e7021ef97ae92414946859ddadc48d584fdcb0358799ef930aa6b7fd5a
CPU and topologyx86_64; recorded topology covers 7 physical cores on 1 socket; SMT sibling topology reported
Selected CPUsLogical CPUs 2, 3, 4, 5, 6, 7, 8, 9; topology not recorded for CPU 8
GPUNVIDIA RTX A6000 (driver 595.71.05)
OS and kernelLinux 6.8.0-139-generic on x86_64
BuildRelease; c++ (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0; Build cuda_13.0.r13.0/compiler.36424714_0
System controlsperformance governor reported for CPUs with recorded topology; ASLR and CPU idle-state status are not recorded in suite artifacts
Preset and repetitionsdocs-core-runtime; 7 repetitions
Run-to-run gate10% maximum primary-metric coefficient of variation; 48/48 rows passed
Single-process latencyAt least 10,000 measured samples; 1,000 warmup; p50, p99, and p99.9
Interprocess latency1,000 measured samples; 1,000 warmup; p50 and p99
Payload validationDisabled in timed production rows; structural and completion checks retained

Host checks confirmed that logical CPUs 2–9 were kernel-isolated (isolcpus, nohz_full, rcu_nocbs), their policies used the performance governor, and C1, C1E, and C6 were disabled. The suite artifact records the governor but does not yet capture CPU isolation or idle-state controls, so the generated provenance row remains conservative. The RTX A6000 also drove the workstation display, and GPU utilization was 6% immediately before the run, so GPU-sensitive rows may include that background load.

The two-process pinned-host Tensor and schema::ImageT rows come in pairs. The host-writer rows fill each buffer on the CPU, so the consumer has no readiness fence to wait for. The CUDA-stream-writer rows fill it on the producer’s stream, as a GPU producer would, so each slot returns to the pool only after the consumer’s stream passes that fence; the device rows share that handoff. Compare pinned-host rows only with the same writer. Both writers run below pageable-host schema::ImageT because a pinned-host or device consumer orders each slot’s release after its own CUDA stream, which costs CPU time on every sample. See Pinned-host readiness follows the writer for the contract and for when slot return limits throughput.

Reproducing the tables

Run from the SDK source root on a native Linux x86_64 benchmark host. Measurements on other hardware need their own provenance. First save the current CPU configuration and apply the benchmark controls:

sudo ./benchmark/configure_cpu_performance.sh \
apply /tmp/holoscan-cpu-performance.state

Then run the rows used by this page in the SDK build container. launch starts in the selected target’s build directory, so the benchmark uses --build-dir .. The explicit x86_64 target matches the setarch command and published hardware:

QA_CPUS=$(./benchmark/configure_cpu_performance.sh recommend 8)
RESULTS_DIR="transport-suite-docs-core-runtime-$(date +%Y%m%d-%H%M%S)"
./run launch --arch x86_64 \
--security-opt=seccomp=unconfined \
--run-cmd "setarch x86_64 -R \
python3 ../benchmark/run_transport_benchmark_suite.py \
--build-dir . \
--output-dir '${RESULTS_DIR}' \
--preset docs-core-runtime \
--cpus ${QA_CPUS}"
Reduced-core systems

The docs-core-runtime preset also accepts four or more distinct physical cores. It runs every row whose operator and support-thread roles fit and records the remaining rows as topology-based skips. The generated core-runtime-tables.md is marked as partial; updating this public page with --update-doc still requires the complete eight-core, 48-row run.

After the benchmark finishes, use the same host shell to generate the tables from the preserved artifacts and update this page in place. Query the directory for the same target instead of reconstructing its name:

BUILD_DIR=$(./run get_build_dir --arch x86_64)
python3 benchmark/summarize_runtime_benchmarks.py \
"${BUILD_DIR}/${RESULTS_DIR}" \
--update-doc docs/benchmarks_core_runtime.mdx

Finally, restore the saved host CPU configuration:

sudo ./benchmark/configure_cpu_performance.sh \
restore /tmp/holoscan-cpu-performance.state

See Runtime benchmark hardware tuning for GPU clock controls, Jetson controls, verification, and recovery commands.

Internal suite inventory

SuiteExecutable / directoryWhat it measures
Dispatcherbenchmark/runtime (runtime_dispatcher_benchmark)One preallocated dispatcher freeze/select/complete cycle over 1/8/64 ready candidates; readiness notification publish/drain/retire; one complete blocking run(plan) activation including session lifecycle and teardown.
Runtime channel sessionbenchmark/runtime (runtime_channel_benchmark)Channel-session activation prepare/retire cycles, readiness polls over retained bursts (up to 1,024 samples), and witness retirement as graph-wide channel counts scale.
Runtime payloadbenchmark/runtime (runtime_payload_benchmark)Steady-state compiled-graph catalog publish/receive: encode, framing, submission, decode, and Sample<T> construction for POD, schema::TimeT, schema::StringT, and nested schema::ImuT payloads; reports wire size and per-sample allocations.
Prepared schema Graphbenchmark/runtime (runtime_prepared_schema_graph_benchmark)A running IMU source-transform-sink Graph in owning, borrowed, and prepared-transform modes, over direct and forced-HoloMQ routes with tracing off/on. Reports receive, scratch, publication, and whole-callback allocations separately, callback cost per transform hop, and source-to-sink p50/p95/p99. Its full-run queue pacing differs from the descriptor Graph benchmark’s retirement acknowledgments, so their latency percentiles are not comparable.
Temporal contractbenchmark/temporal_contractThe payload-independent temporal cache and selection engines: cache push, Exact and LatestWithin selection, cancellation, commit, and DrainAll batch paths at retained depths from 1 to 1,024.
Graph authoringbenchmark/graphCold graph-authoring scaling: operator declarations, linear chains, fan-out DAGs with cycle/fan-in validation, snapshot construction and canonical hashing, and nested reusable graphs.
Endpoint catalogbenchmark/endpoint_catalogCold endpoint-catalog construction and normalization: builder plus canonical build up to the schema maximum of 4,096 endpoints, and POD layout validation and digest derivation up to 1,024 fields.
Timebenchmark/timeClock-registry and clock-conversion paths (ClockRegistry, ManualClock, affine clock conversions).
Runtime aggregatebenchmark/runtime_aggregate (runtime_aggregate_benchmark)The cumulative runtime scenario set: it sequences selected cases from the dispatcher, compiled-channel, POD, and temporal suites (dispatch, multi-rate fresh/stalled scenarios, and more) through one CTest entry.
Hello scaffoldbenchmark/core_helloThe minimal Google Benchmark example against holoscan::core; the template for adding new suites.

Running the suites locally

All commands run from the repository root that contains the benchmark/ directory. The ./run bench helper executes inside the Holoscan build container with a Release build by default, rebuilds any stale benchmark targets, and runs the binaries directly so Google Benchmark output keeps its normal formatting:

./run bench --help # full command reference and commented examples
./run bench --list # list registered benchmarks (CTest name + short alias)
./run bench # build what is missing and run all benchmarks

You can target one suite by alias or CTest name — for example ./run bench --name core_hello and ./run bench --name BENCHMARK_CORE_HELLO are equivalent. Suites are also registered with CTest under the benchmark label, so ctest -L benchmark runs them from an existing build tree.

Benchmark defaults keep runtimes short enough for frequent developer and CI smoke runs. For stable local measurements, use the reversible host tuning helper and explicit CPU placement described above, close background workloads, and use a production-quality preset.

Recap

  • Controlled representative 1:1 Graph p50 latency ranged from 6.533–9.856 µs in one process and 16.324–28.950 µs between two processes.
  • Single-process rows report p99.9 from at least 10,000 measurements; interprocess rows use at least 1,000 measurements and stop at p99.
  • The generated tables compare shared and separate-partition 1:2 and 1:4 fanout, include a POD 10-operator single-process chain, and cover a generated schema with two preallocated Tensor fields.
  • HoloMQ, Channel, and Graph measurements are independent boundaries; compare matched rows and do not subtract them.
  • The internal microbenchmark inventory remains available for local diagnosis and CI smoke coverage; it is not the public transport result table.