Standalone Python Channel Clients
Python processes that do not host a Holoscan graph can publish and consume the
same holomq:// topics as C++ processes. The holoscan.channel module wraps
the public C++ ChannelRuntime endpoint families while inferring payload types
from the graph’s endpoint catalog.
This page follows the same lifecycle as the
C++ standalone client guide,
then covers Python-specific cancellation, asyncio, generated dataclasses,
and Tensor array protocols.
Developer preview. Standalone Python channel clients are a developer preview and may change in upcoming releases. Descriptor-backed schemas and Tensor catalogs are SDK-build-specific; run graph and client processes from matching SDK builds.
- How to load a graph-produced endpoint catalog and create a client runtime
- How payload inference, publication, receive, EOS, and local close work
- Which publication failures are safe to retry
- How blocking cancellation and descriptor-based
asynciowaits differ - How to publish Tensor-like objects or fill a bounded zero-copy loan
- How generated dataclasses carry multiple read-only Tensor fields
When to use a standalone client
Use hs.channel.Runtime in a process outside a graph: a sensor daemon feeding
an imported topic, a recorder consuming an exported topic, or a middleware
bridge. Inside an Operator, continue to use typed ports and add_flow().
Creating a standalone runtime inside an operator bypasses the admitted plan.
The shipped Python sensor and recorder variants demonstrate this process boundary in External clients.
Creating the runtime
A runtime owns the transport context and all endpoints created from it. A context manager gives the required destruction order and publishes ordered EOS from live publishers when the block exits normally:
An exceptional context exit abandons live publishers without publishing EOS,
as does object destruction. This avoids fabricating successful stream
completion while unwinding an error. The runtime must also outlive received
Tensors, derived views, and asynchronous GPU work that touches their storage.
Creation failures and endpoint operations raise the matching hs.errors
exception; the exception retains the C++ error code and stable token.
Obtaining an endpoint catalog
The graph process saves its compiled boundary:
The standalone process loads that exact file:
EndpointCatalog.parse(text) accepts canonical JSON already held in memory,
and compatibility_with() compares two catalogs. Do not reconstruct payload
identity or capacity from selected JSON fields. The immutable EndpointView
exposes the normalized scalar, schema, bounds, attach, clock, and Tensor facts
used by endpoint inference.
Creating publishers and subscribers
Catalog forms infer the registered payload from the endpoint:
An optional payload_type= checks the inferred type and raises
IncompatibleError on a mismatch. It does not override the catalog.
Catalog-less Python-to-Python channels require an explicit payload and bounded options:
Name-based endpoints are useful when no graph exists. Tensor publishers still require a catalog because their memory placement, shape, and pool capacity are part of the admitted boundary contract.
Emitting and receiving samples
emit() returns the exact recipient count. try_receive() is nonblocking and
returns None when no sample is available; receive() blocks up to one
relative timeout and returns a Sample:
Timeouts accept float seconds, datetime.timedelta, or hs.time.Duration.
Each call computes one absolute steady-clock deadline; readiness wakes never
restart its budget. With timeout=None, Publisher.emit() makes one C++
attempt and exposes NotReadyError or BackpressuredError directly.
Producer EOS and local subscriber shutdown
The two close operations have different authority:
Call publisher.close() after the final accepted publication, or leave its
owning Runtime context normally; both request ordered producer EOS.
Exceptional context exit, object destruction, process exit, silence, and
subscriber close do not imply EOS. A finite input-driven graph drains accepted
work and propagates EOS to its exports, where the subscriber observes
ClosedError after the final sample:
Calls on one endpoint are externally serialized. Cancel and join a thread that is waiting on a subscriber before closing that subscriber or its runtime.
Reporting capture-clock health
If the graph’s import advertises clock-health reports, the standalone publisher submits them in the same order as data and EOS:
Refresh a required healthy report within the catalog’s maximum age. A client whose catalog does not advertise the capability gets a checked contract error instead of silently publishing unused evidence.
Attach and retry discipline
Cross-process attachment is asynchronous. wait_for_subscribers(timeout) is
5 ms polling sugar over update_connections() and the latest
has_subscribers snapshot. It is useful before the first publication but is
not proof that a later publication was delivered.
Passing timeout= to emit() applies the standard bounded retry loop. It
retries only NotReadyError and BackpressuredError, which prove that
submission did not begin. PublicationPartialError and
PublicationIndeterminateError may represent accepted delivery; retrying can
duplicate a sample. Leave the loop on those and every other error.
Waiting for samples
Use receive(timeout, cancel=...) to wait and consume, or
wait(timeout, cancel=...) to wait without consuming. Both release the GIL for
the complete blocking interval:
Cancellation raises HoloscanInterruptedError and leaves the subscriber
usable. A timeout raises HoloscanTimeoutError. Only one blocking wait or
readiness owner may claim a subscriber at a time; another attempt raises
InvalidArgumentError.
For a custom event loop, subscriber.readiness() returns a context-managed
ReadinessHandle. fileno() is suitable for select/poll; readable means a
sample or terminal condition may be available. Acknowledge first, then drain
with try_receive() until it returns None so a wake racing the drain remains
pending. Detaching releases the subscriber’s wait-owner slot.
Using asyncio
hs.channel.aio.Subscriber registers the readiness descriptor with
loop.add_reader(). The normal catalog path creates no helper thread:
The shipped recorder exposes this exact path as recorder.py --async; its
CTest runs the async Python subscriber in a separate process against the C++
relay graph and rejects any Python helper thread.
Task cancellation removes the reader and detaches the handle. A name-based
subscriber created without event wake raises NotSupportedError when asked
for readiness; only then does the adapter fall back to an executor-backed
blocking wait. hs.channel.aio.Publisher.emit() uses asyncio.sleep() for the
same pre-submit-only retry discipline without blocking the event loop.
Publishing and receiving Tensors
The copy path accepts NumPy, CuPy, PyTorch, and other Tensor-like values through
DLPack, __cuda_array_interface__, or __array_interface__ according to the
catalog’s foreign-emit policy:
For a bounded endpoint-owned allocation, fill a TensorLoan and then consume
it with emit():
For CUDA-device memory, pass a CuPy stream to write(stream=stream) and create
the writable device view through DLPack inside that phase. Pool exhaustion
raises ResourceExhaustedError; it never allocates a fallback buffer. A loan
is single-use and can be abandoned explicitly with cancel(). Every emit
attempt consumes the loan, including an attempt that returns an error, so do
not retry the same loan. A synchronous or asynchronous loan emit with a
non-None retry timeout is rejected before the loan is consumed.
Generated descriptor-backed schemas use their Python dataclasses directly.
Tensor fields accept Tensor-like objects on emit and return read-only
hs.Tensor objects on receive:
The generated module loads its compiled payload registration adapter. Graph and client processes must use matching SDK builds because Tensor descriptors are SDK-private wire records. A CUDA device must be available for catalog Tensor endpoints even when their backing memory is host-resident.
Error and retry reference
Recap
- Create
hs.channel.Runtimeonly in a process outside a graph and keep it alive longer than every endpoint and received Tensor. - Share the graph’s exact endpoint catalog. Catalog endpoint forms infer the payload; name-based forms require an explicit payload.
emit(timeout=...)retries only proven pre-submit outcomes against one deadline. Publisher close and normal Runtime-context exit request ordered EOS; subscriber close is local cleanup.- Blocking waits release the GIL and support
CancelToken;asynciouses the readiness descriptor without threads when event wake is available. - Tensor-like emit is the policy-controlled copy path.
TensorLoanis the bounded endpoint-owned path, and every received Tensor is read-only.
See the generated Python API reference under holoscan.channel for the exact
facade signatures.