FAQ
Quick answers to the questions we hear most often about Holoscan SDK 5.x — from developers arriving fresh and from developers arriving from 4.x, where many familiar names (Application, schedulers, Conditions, GXF) no longer exist. Each answer is deliberately short and links to the page that owns the full story.
If you are migrating from 4.x, start with Migrating from 4.x; if you are new to the SDK, start with Core concepts.
Naming and concepts
Why is the method called contract()?
Because it declares a temporal contract between your operator and the
runtime: what the runtime must satisfy before it invokes compute(). A
contract names the trigger (what causes an activation, such as OnEach{camera}
or OnClock{.period = 33ms}), the input selection (which samples participate,
such as LatestWithin{.max_age = 5ms}), and the missing-data policy (what
happens when an input is absent or stale, such as ReuseLast or Skip). It
is a contract in the everyday sense — both sides agree on terms up front, the
compiler checks them, and the runtime enforces them — which is what lets
compute() stay entirely oblivious to timing. See
Temporal contracts.
Why errors-as-values instead of exceptions?
Fallible runtime APIs return holoscan::expected<T, Error> instead of
throwing, for four reasons: failure is visible in the signature (you cannot
forget to handle it — the results are [[nodiscard]]); returning an error is
an ordinary value return with deterministic cost, whereas stack unwinding is
expensive and hard to bound on real-time paths; a code-only Error (say,
kNotReady on a hot polling path) allocates nothing; and value-channel code
works even in builds with exceptions disabled. Exceptions still exist at
genuine boundaries — graph authoring throws on programming errors, by
design. Branch on ErrorCode, and use HOLOSCAN_TRY to propagate concisely.
See Errors as values.
Where did the schedulers go?
There is no user-selectable scheduler in 5.x — no Greedy, Multi-Thread, or
Event-Based Scheduler to choose between. Instead, each operator’s
contract() declares when it should run, compile() lowers those
declarations into an execution plan, and the runtime’s dispatcher executes
that plan. Scheduling intent moved from a global runtime knob into a
per-operator, statically checkable declaration. See
Core concepts and
Temporal contracts.
Why is there no Application class?
5.x separates the three phases that Application used to bundle. You
compose a holoscan::Graph (add operators with graph.op<T>(...), wire
them with add_flow), compile it with holoscan::compile(graph) into an
ExecutionPlan, and run the plan with holoscan::run_async(plan), which
returns a RunSession you control. Each phase has its own failure style:
authoring throws, compile() returns a plan you check with plan.ok(), and
a run fails closed. Because the phases are separate values, you can compile
once and run many times, inspect a rejected plan’s diagnostics, or hold
multiple sessions. See
Composing a graph
and Running and stopping.
What replaced Conditions (CountCondition, PeriodicCondition, …)?
Triggers and selectors inside contract(). A 4.x PeriodicCondition becomes
OnClock{.period = ...}; message-availability conditions become the default
OnAnyInput{} or an explicit OnEach{port}; alignment of multiple inputs
becomes OnAligned{...}. Unlike Conditions, these are typed value objects
that the compiler validates as part of the plan rather than runtime components
attached to an operator. See
Temporal contracts.
What are the “three declarations” of an operator?
setup() declares structure (ports and parameters), contract() declares
time (trigger, selection, missing-data policy), and compute() does the work
— nothing else. Keeping the three concerns in three methods is what makes
operators testable and reusable across graphs with different timing. See
Operator anatomy.
Practical
Why does my run fail with a clock-binding diagnostic?
Ordinary input- and clock-triggered pipelines must select a default clock.
If it is absent, compile() rejects the plan with TEMPORAL_CLOCK_UNDECLARED.
Declare and select it before compiling:
Python can use hs.Graph("pipeline", default_clock=hs.RealtimeClock).
Readiness-only sources may be clock-free. RUNTIME_CLOCK_BINDING_UNAVAILABLE
instead means an admitted plan’s required clock could not be bound at startup.
See Time and clocks.
Why am I getting kResourceExhausted?
Because 5.x is bounded by construction: tensor pools are preallocated, queues
have declared depths, and per-compute emit counts are finite. When a bounded
resource has no free capacity, the SDK does not silently allocate more — it
returns Error{kResourceExhausted} so you can react (drop, retry, or treat
it as a real sizing bug). The usual causes are a pool sized too small for the
pipeline’s in-flight depth, or a slot held longer than intended (for example,
a tensor loan that was never committed). See
Understanding failures
and Payload type rules.
Why can’t I use int as a port type?
Because int (like size_t and long) is a platform-dependent alias, and a
port type is a wire contract that must mean the same thing everywhere. The
in-process scalar catalog is a closed set of fixed-width types: bool,
uint8_t through int64_t, float, and double. Write
holoscan::Input<std::int32_t>, not holoscan::Input<int>. Structured types
go through schema payloads, and tensors through Tensor ports — not through
ad-hoc structs. See
Payload type rules.
How do I synchronize CUDA streams between operators?
You don’t — not on the hot path. Stream ordering is automatic: committing a
tensor records its readiness, and a Tensor you receive is already ordered on
your compute stream, so you launch kernels immediately.
Never call cudaStreamSynchronize (or cudaDeviceSynchronize) on the hot
path. It stalls the pipeline and defeats the SDK’s asynchronous ordering.
If you need CPU access to tensor data, request host access explicitly — that
is the sanctioned, visible synchronization point.
See Tensors 101 and Tensors in depth.
How do I stop a running pipeline cleanly?
run_async(plan) returns a RunSession; call session.request_stop() and
then session.wait(). The session’s termination() tells you how the run
ended (for example RunTermination::kStopped for a requested stop). There is
no deadlock-based shutdown idiom as in 4.x — stopping is an explicit,
first-class request. See
Running and stopping
and Restart control.
How does an operator finish the whole run successfully?
Inside compute(), call context.request_completion(). The session reports
RunTermination::kRequestedCompleted, distinct from natural finite
kCompleted, and blocking run() returns zero. The request is immediate: it
closes new callback admission for the whole session, but does not guarantee
EOS, source closure, downstream drain, or queued delivery. Never retain
ExecutionContext; it is valid only for the callback.
There is no kRequestedStopped because kStopped already denotes the explicit
stop category; HSDK 5 has no separate natural-stop success state. It also has
no operator-local deactivate/readiness-removal API or deadlock-as-success rule.
Count-bounded EOS and drain semantics are separate from this request. See
Running and stopping.
Why does compile() return a rejected plan instead of throwing?
A rejected graph is an expected outcome, not an exceptional one — so
compile() reports it as a value. Check plan.ok() and, when it is false,
read plan.diagnostics(): each diagnostic names what was rejected (an invalid
trigger/selector combination, over-subscribed queue capacity, a type mismatch
across a flow) before anything runs. Failing at compile time, with a full
list of problems, beats failing one bug at a time at runtime. See
Compile and check.
Can I use the channel client API inside my graph?
No.
One graph, one vocabulary. Inside a graph, operators communicate through
typed ports (Input<T>/Output<T>) wired with add_flow() — the runtime
materializes the channels for you. The ChannelRuntime client API exists
only for standalone processes outside any graph. Never mix the two: using
ChannelRuntime inside an operator bypasses the compiled plan and its
guarantees.
See Interprocess channels and Standalone channel clients.
Scope: languages, platforms, operators
Is Python supported?
Yes. Holoscan SDK 5.x provides a Python 3.12+ application-authoring API for defining Python operators, composing them with registered C++ operators, compiling inspectable plans, and controlling run sessions. It includes reusable graphs, graph policy, graph-owned YAML/JSON configuration, scalar and inline-schema external topics, Tensor interop, typed exceptions, registry proxies, and typing stubs.
Python operators subclass holoscan.Operator and declare typed ports, a
temporal contract, and a compute() callback. The current surface does not
expose C++ managed-signal ownership. Python applications import registered
operator providers and manage process signals themselves. Start with the
Python guide and use the
Python migration guide
rather than inferring compatibility from 4.x packages.
Which platforms are supported?
Linux on x86_64 and arm64 with NVIDIA GPUs. The authoritative, per-release platform and CUDA matrix lives on SDK installation — check that page rather than assuming a 4.x-era matrix carries over.
Where are the domain operators — inference, visualization, video I/O?
In Holoscan Modules. 5.x separates the
core runtime — the graph/compile/run machinery, temporal contracts, payloads,
tensors, and channels — from domain operators, which ship as separately
selectable modules on top of it. This release carries three core modules:
Inference (InferenceOp),
Vision (nine CV-CUDA and NPP operators),
and Visualization
(HolovizOp). For other capabilities, author your own operators against the core API or
look for a community module on
HoloHub.
What is the difference between a Holoscan Core Module and a Holoscan External Module?
Both are libraries that extend the SDK with operators, subgraphs, and/or schema. Core modules live in the SDK repository, are released on the SDK cadence as separate, optional Debian packages and Python wheels, and are supported through the SDK release process. External modules live in a separate project, are published by the community or by hardware partners on their own cadence, and are supported by their maintainers. See Core modules and ecosystem modules.
How do I publish my own module?
Scaffold it from the Holoscan CLI module template
(holoscan create my-module), follow the HoloHub
Create a Holoscan module tutorial, and list it in the HoloHub Modules
catalog. If you want to propose a module for the SDK itself, read the
core module development guide
first. See Build your own module.
Is there anything like distributed applications / fragments?
Cross-process pipelines are built from interprocess channels: processes exchange typed payloads over named external topics rather than partitioning one application object into fragments. See External topics and Interprocess channels.
Migration
I have a 4.x application. Where do I start?
Read Migrating from 4.x
first — it maps each 4.x concept (Application, Fragment, Conditions,
schedulers, make_operator) to its 5.x counterpart and walks a real port.
Then work through the getting-started sequence from
Core concepts:
the mental model (compose → compile → run, errors as values, contracts) is
worth absorbing before you translate code line by line.
Do my 4.x operators or GXF extensions work in 5.x?
No. 5.x is a new runtime, not an API revision — there is no GXF underneath,
no entity/codelet layer, and no compatibility shim for 4.x operator classes.
Operators are re-authored around the three declarations (setup() /
contract() / compute()), which is usually a simplification: scheduling
Conditions become contract triggers, and receive/emit boilerplate becomes
typed Input<T>/Output<T> calls returning expected. See
Migrating from 4.x
and Operator anatomy.
Where should I ask a question this page doesn’t answer?
Check the Glossary for unfamiliar terms, the Understanding failures page for any diagnostic the runtime printed, and the Holoscan by example tour for working code to compare against.