Skip to navigation

Lifecycle and Restart in Depth

View as Markdown

A sensor pipeline needs coordination at several levels. Cameras must finish initialization before fusion can compute. Fusion needs suitable capture timestamps. A failed device may need to restart while other operators retain their state. Each requirement has its own mechanism.

This walkthrough follows two cameras, a fusion operator and an unrelated monitor. It derives their behavior from lifecycle declarations, then connects that model to the shipped camera + IMU example. The stereo diagrams are illustrative predictions, not a recorded application trace. For runnable callback and restart examples, see examples/lifecycle_callbacks/cpp and examples/experimental/restart_control.

You will learn
  • Why fixed stages and optional hooks work together
  • Why initial startup waits for every operator, while restart callbacks have a smaller scope
  • How lifecycle edges determine restart membership and the effective entry stage
  • What pauses, what gets reset, and what a successful or failed restart preserves
  • How lifecycle readiness differs from sensor synchronization and temporal selection

Beta. Lifecycle restart and active parameter-update APIs may change in upcoming releases. Receiver-boundary handling and temporal timing details called out below describe the current implementation. They do not establish scheduling latency or hardware synchronization guarantees.

Declarations, a compiled plan, and a running session

Constructors, setup() and contract() describe an operator. Authoring and compilation can replay them, so keep resource acquisition and externally visible work in lifecycle callbacks. Compilation validates the declarations and freezes stage checkpoints, same-stage ordering graphs, restart authority and closures. No lifecycle callback runs during compilation.

A new RunSession reconstructs runtime operator instances and bindings, performs startup, then admits computation. A targeted restart reuses those instances. Stopping and creating another session reconstructs fresh instances instead.

Author and Compile happen before any session. Replayable declarations become a frozen plan, and no lifecycle callback runs yet. The plan starts a RunSession, drawn as a band. Inside the band, the session reconstructs operators and bindings, runs startup through every stage until ready, and then computes. A restart runs at a control boundary and reuses the same instances. The restart returns to compute on Published or RolledBack. FailedStopped ends the run. Stop tears down and joins the session. Optionally, creating another RunSession reconstructs fresh instances from the same plan.AuthordeclarationsCompilefrozen planno lifecycle callbacks yetplanRunSessionReconstruct operators and bindingsStartup: every stage, then readyComputeRestart: same instancesruns at a control boundaryreturns on Published or RolledBackFailedStopped ends the runStop: teardown and joincreate another sessionNext RunSessionfresh instances from the same plan

A successful or rolled-back restart returns to computation within the same session. A session can also stop without restarting. Terminal stop tears down and joins the session.

Why a fixed set of stages?

A stage is a shared ordering and cleanup checkpoint. The runtime knows which stages precede computation, which forward suffix a restart must repeat, and which completed work is eligible for cleanup. Application-specific device state belongs inside the operator and its hooks.

StageTypical application work
kConfigureValidate selected configuration and derive cold intent
kDiscoverQuery capabilities, modes or dimensions
kAllocateAcquire the resources owned by this lifecycle pass
kWarmUpPrepare those resources for steady execution
kArmBind generation-scoped readiness and external capabilities
kStartEnable active behavior
kStopQuiesce active use
kReleaseRelease owned resources

These are useful responsibilities, not an assertion that every shipped operator uses every stage. A simple operator may do all its setup and cleanup in start()/stop(). Advanced hooks have the signature LifecycleStatus method(LifecycleContext&) noexcept.

An operator with no hooks writes no lifecycle code. Its checkpoints still take part in stage ordering and complete as skipped. You may omit an optional hook. If you register it, its failure still affects execution. kRetryableFailure classifies a failure but does not schedule an automatic retry. See Lifecycle callbacks for registration, context services and status values.

Two cameras: separate data edges from lifecycle edges

Left and Right send frames to Fusion. Monitor has no data connection. Fixed nominal rates keep rate-derived restart dependencies out of this first example.

Three views of the same four operators in fixed positions: Left, Fusion and Right in a row, and Monitor below Right. Data: Left and Right each send payloads to Fusion. Monitor has no data connection. Arm ordering: Left and Right each precede Fusion at Arm. Left, Right and Fusion register Arm hooks. Monitor has no Arm hook, so its Arm checkpoint is skipped. Stop ordering, a coupled variation used only if Fusion must stop first: Fusion precedes Left and Right at Stop.DataArm orderingArm hookStop ordering: coupled variationonly if Fusion must stop firstno Arm hook: skippedLeftFusionRightMonitorLeftFusionRightMonitorArmArmLeftFusionRightMonitorStopStop

Solid arrows carry payloads. Dashed arrows order lifecycle checkpoints at one named stage. Each stage has its own graph. Neither kind transfers resource ownership or synchronizes camera exposure. The Stop view is a deliberate variation, separate from the baseline example.

Now declare that each camera produces an armed fact, and Fusion requires both facts at kArm. The following declaration fragments omit ports, class definitions and callback bodies. LeftCamera, RightCamera and Fusion are illustrative application classes.

// Shared dependency keys, declared at namespace scope.
inline constexpr holoscan::LifecycleDependencyKey kLeftArmed{"left.armed"};
inline constexpr holoscan::LifecycleDependencyKey kRightArmed{"right.armed"};
// In LeftCamera::setup(): RightCamera produces kRightArmed instead.
spec.lifecycle()
.stage(holoscan::LifecycleStage::kAllocate, &LeftCamera::allocate)
.stage(holoscan::LifecycleStage::kArm, &LeftCamera::arm)
.stage(holoscan::LifecycleStage::kStop, &LeftCamera::stop_camera)
.stage(holoscan::LifecycleStage::kRelease, &LeftCamera::release)
.produces(holoscan::LifecycleStage::kArm, kLeftArmed);
// In Fusion::setup():
spec.lifecycle()
.stage(holoscan::LifecycleStage::kArm, &Fusion::arm)
.requires_dependency(holoscan::LifecycleStage::kArm, kLeftArmed)
.requires_dependency(holoscan::LifecycleStage::kArm, kRightArmed);

Monitor registers no hooks. A dependency fact is a compiled ordering declaration, not a runtime message or a promise to poll until a string key appears. Missing or duplicate producers and same-stage cycles are rejected during compilation.

The global barriers already order different stages, so declared dependencies connect checkpoints within one stage.

Startup is stage-major

Every operator completes Configure before any enters Discover, and so on through Start. Within a stage, callbacks execute serially in stable topological order, on their assigned execution lanes.

A grid of six startup stages, from Configure down to Start, for four operators: Left, Right, Fusion and Monitor. Every operator finishes a stage before any operator enters the next one. Callbacks run only for registered hooks: Left and Right at Allocate, and Left, Right and Fusion at Arm. Every other checkpoint, including all of Monitor’s, completes as skipped without calling operator code. Arm has two levels. Left and Right are in the first level, together with Monitor’s skipped checkpoint, and Fusion follows both cameras in the second level. Callbacks run one at a time, level by level. After Start and readiness setup finish, compute and restart ingress open.LeftRightFusionMonitorConfigureDiscoverAllocateWarmUpArmStartAfter Start and readiness setup:compute and restart ingress opencallbackskipped checkpointOne callback at a time, level by level

The filled marks are the five startup hooks declared above. An empty mark is a successful skipped checkpoint. At Arm, both camera callbacks precede Fusion’s callback. Monitor has no dependency edge and skips its checkpoint.

Can Left compute while Fusion is still in Arm? No. Initial startup completes the last Start checkpoint and readiness/source initialization before opening compute dispatch and restart ingress. Once running, ordinary activations follow their own contracts.

Three meanings of “in sync”

RequirementMechanismWhat it does not establish
All operators ready before computationLifecycle stage barriers and ordering factsSimultaneous physical sensor capture
Cameras expose together or share a calibrated time baseDevice triggering, synchronization and timestamp/clock integrationWhich queued samples fusion selects
Fusion receives a suitable pair or intervalTemporal trigger, selectors and missing policyHardware synchronization or equal capture rates

For stereo data, OnAligned with Exact member selectors matches FIFO heads within an inclusive capture-time spread. For camera + IMU, OnWindow with BracketedWindow selects a complete neighborhood around consecutive camera anchors. Neither follows automatically from an Arm dependency. See Temporal contracts and Advanced temporal patterns.

Derive restart membership before its entry stage

The compiler follows lifecycle edges forward, across every stage, to freeze each target’s callback closure. This includes Stop/Release ordering edges and any rate-derived dependencies. The closure is independent of the requested entry stage. Data-flow edges alone do not establish callback membership.

The second panel previews the deliberate Stop ordering described below.

Two panels with the same operator positions. Left, Fusion and Right sit in a row, and Monitor sits below Right with no edges. Baseline: Left and Right each have an Arm edge into Fusion. Following lifecycle edges forward from Left reaches Fusion only. Right’s edge points into Fusion and is not followed backward. C is Left and Fusion. Right and Monitor are outside C. Coupled variation: Fusion must stop before both cameras, so Stop edges run from Fusion to Left and from Fusion to Right. Forward from Left, the Arm edge reaches Fusion and Fusion’s Stop edge reaches Right. C is Left, Fusion and Right. Monitor is outside C.followed forward from LeftRestart Left: baselineC: Left, Fusion. Outside C: Right, MonitorRestart Left: Fusion stops firstC: Left, Fusion, Right. Outside C: MonitorLeftFusionRightMonitorArmArmLeftFusionRightMonitorArmArmStopStop

With only the Arm ordering, a Left restart has C = {Left, Fusion}. Right’s incoming edge to Fusion does not run backwards. Monitor is outside C, and this baseline has no direct data consumer outside C.

Next, the compiler compares the requested stage with forward-stage dependencies whose two endpoints are inside C. An earlier internal edge widens the entry stage. An incoming dependency from an outside provider does not alone widen it.

Target / requested stageCallback closureEffective entry
Left / StartLeft, FusionArm
Fusion / StartFusionStart
Left / DiscoverLeft, FusionDiscover

The receipt keeps the requested stage. inspect()/await() report the effective stage. The plan’s resolution records explain the widening reason.

Couple cameras only for a real ordering requirement

Suppose Fusion must stop using both cameras’ resources before either camera stops. Add Fusion → Left and Fusion → Right at Stop. Those are separate from the camera → Fusion edges at Arm.

Each stage graph is acyclic. Across stages, forward reachability now connects all three operators, so restarting any one includes Left, Right and Fusion. A Start request widens to Arm. This is a consequence of real lifetime ordering, not a separate restart-group API. Declare dependencies for actual lifetime ordering.

Who pauses and who receives callbacks?

Return to the baseline graph with only Arm dependencies. Add a Detector that consumes Fusion’s output through an ordinary same-process data edge, with no lifecycle dependency. Give Detector hooks of its own so that “no callbacks during a Left restart” is meaningful.

Left, Fusion and Right sit in a row. Detector sits below Fusion and Monitor below Right. Left and Right each send data to Fusion and have an Arm edge into Fusion. Fusion sends data down to Detector, with no lifecycle edge. Restarting Left follows the Arm edge forward to Fusion, so C is Left and Fusion. Detector directly consumes data from a C member without belonging to C, so B is Detector. U is Right and Monitor. While the transaction runs, local dispatch pauses for C, B and U. First, at the fence, C and B have pending candidates canceled and frozen sources retired. Then lifecycle callbacks run for C members only. If publication succeeds, it resets C and B selectors and advances their Tag generations. U keeps its Tag. If the transaction rolls back instead, Tags stay unchanged and canceled candidates are not replayed.Restart Left with a data-only DetectorLeftFusionRightDetectorMonitorArmArmdatadatadataC: Left, FusionB: DetectorU: Right, MonitorLocal dispatch pauses for C, B and UFence: cancel pending candidatesand retire frozen sourcesLifecycle: C members onlyOn success: reset selectorsand advance Tag generationsRolledBack: Tags unchanged, canceledcandidates are not replayedCBCCB

Detector’s data edge adds boundary handling without adding lifecycle callbacks.

Set and membershipDuring the transactionOn successful publication
C: target and compiled callback dependentsCancel pending candidates and retire frozen sources at the fence, then run lifecycle callbacksReset selectors/readiness and advance Tag generation
B: direct same-process consumers of C outputs, outside CCancel pending candidates and retire frozen sources at the fence. No lifecycle callbacksReset selectors/readiness and advance Tag generation
U: remaining operatorsLocal dispatch pauses with the session. No lifecycle callbacksRetain Tag generation

Current boundary behavior. B includes ordinary data consumers, not just explicitly authored channel boundaries. It is a direct-consumer expansion, not recursive traversal of every downstream operator. Its pending-work and generation handling may change in upcoming releases. Do not rely on B keeping its pending input selection or activation generation through a restart.

The runtime waits until every local owner reaches a control boundary with no activation in progress. It then prepares the transaction, runs callbacks for C, and resumes owners only after a resumable outcome. An unrelated operator is not reconstructed, but uninterrupted progress is not promised. This is a local session boundary, not distributed quiescence of interprocess neighbors.

Preparation completes before C’s lifecycle callbacks:

OrderRuntime work
1Check that C/B receivers are not draining end-of-stream and their completion remains open
2Cancel and join the shared channel service, if used
3Cancel C/B pending candidates and retire their frozen sources
4Begin the tensor restart when tensor resources exist, then prepare rate state

A failure here produces kFailedStopped and fails the run before any callback. The channel service is shared across the session. Even a B receiver near end-of-stream can fail the fence.

Which stages repeat?

The executor uses the effective entry stage. Compatible queued requests can coalesce into a union closure with the earliest shared entry stage. Several receipts can therefore identify the same transaction.

Effective entryPre-restart teardownForward suffix
kStartStopStart
kArmStopArm → Start
kWarmUpStopWarmUp → Arm → Start
kAllocateStop → ReleaseAllocate → WarmUp → Arm → Start
kDiscoverStop → ReleaseDiscover → Allocate → WarmUp → Arm → Start
kConfigureStop → ReleaseConfigure → Discover → Allocate → WarmUp → Arm → Start

A Start-entry transaction omits Release. That does not guarantee that Stop preserves application resources: an operator may release resources in Stop and recreate them in Start. Declare important teardown ordering at Stop/Release explicitly. Ordinary teardown follows those stage graphs. Rollback unwinds completed candidate work in reverse completion order. Independent Stop-peer ordering in initial/final session transitions need not be the restart graph’s ordering.

Choose the control operation

NeedOperationResult
Recover with current configurationExplicit lifecycle restartRe-enter the effective suffix in the same session
Change a runtime-mutable scalarParameter updatePublish at an admitted next-compute boundary without a lifecycle restart
Change a lifecycle-mutable scalarParameter update plus lifecycle transactionPublish the candidate only after the transaction succeeds
Replace topology, contracts or unsupported configurationCompile an appropriate plan and start a new sessionFresh runtime instances and startup
End executionRequest stop and joinEnd this session

Dedicated restart needs compile-time transaction capacity and explicit self, external or requester-to-target grants. A restriction-only control handle cannot add authority absent from the plan. Lifecycle-mutable parameter fields use parameter-origin authority after admission. They do not require the dedicated restart grant. Parameter control requires update/scope authority and its own supported profile. Keep the restart and parameter profile matrices separate.

compute() can submit a nonblocking request and hand its receipt to off-callback control code. Only control code outside runtime callbacks uses a LifecycleControlHandle to inspect or await it. LifecycleContext cannot submit or await restarts. Acceptance does not mean completion. A caller-side wait timeout does not cancel the transaction.

See Restart control for the complete APIs and Runtime parameter updates for update-sequence/result observation. Parameter updates do not return dedicated restart receipts. Atomic parameter batches are same-operator only: restarting both cameras does not atomically change both cameras’ parameter values.

A model change has resource ownership

Consider an application-defined Detector with a scalar uint32_t model_id that selects entries 0 and 1 in a fixed compatible catalog. Declare the field with .range(0U, 1U) and .lifecycle_mutable(holoscan::LifecycleStage::kAllocate). Allocate owns model resources, WarmUp prepares them, Stop quiesces active use, and Release destroys the owned resource set. This is a design example, not a claim that the stock inference operator implements this contract.

For Detector’s own update, C is {Detector} if it has no outgoing lifecycle or rate dependencies. With no downstream consumer, B is empty. The incoming Fusion data edge does not pull the cameras or Fusion into the closure. Detector was in B for a Left restart. Membership depends on the target.

An out-of-range model ID is rejected with ParamUpdateStatus::kOutOfBounds before lifecycle submission. To reason about restoration, instead use a valid catalog ID whose Allocate succeeds but WarmUp fails:

  1. Pre-restart Stop and Release use the old published value.
  2. Candidate Allocate succeeds, but candidate WarmUp fails.
  3. Candidate cleanup invokes Release for the completed allocation. The failed WarmUp callback must clean its own unrecorded partial acquisition with RAII.
  4. RestoreOld reruns Allocate → WarmUp → Arm → Start using the old value.
  5. If cleanup, restoration and runtime recovery succeed, the update rolls back and the run continues with the old published configuration.

Restoration loads the old resource again because teardown already released it. There is no uninterrupted fallback instance or automatic reversal of external device effects. Stock InferenceOp does not expose a lifecycle-mutable model-catalog selector. Its Stop/Start restart reinitializes the same configuration. A different model map or port contract needs supported reconfiguration or a new plan/session.

Name the counter before asking whether it resets

IdentityOn kPublishedOn kRolledBack
RunGenerationIdSame RunSessionSame RunSession
ParamGenerationAdvances for each participating parameter candidate. A dedicated restart alone need not change itOld published values and generations remain
Tag.generation / Tag.sequenceC/B generations advance and their activation sequences restart. U keeps its generationC/B generations remain unchanged
SampleMetadata::sequenceRuntime-owned output sequence continues across the restartRuntime-owned output sequence continues

External endpoints retain their own publication-sequence authority. These counters describe different state and should be observed separately.

Pre-restart teardown sees old published parameters. Restart-forward sees the candidate for a patched owner and retained current values for dependency-only members. Cleanup sees the view that created the work. Restoration sees the old view. Final teardown sees the then-current published view. Readiness, rate and resource generations are further identities, not aliases for one universal epoch.

Failure is not always recoverable

A restart transaction after every owner has paused at the control boundary. Fence and runtime preparation come first. Then pre-restart teardown runs Stop, plus Release when the effective entry is Allocate or earlier. Then the candidate forward suffix runs. If it succeeds, runtime and candidate publication follow, and the result is kPublished: dispatch resumes, C and B Tag generations advance, and any parameter changes become published. If the forward suffix fails, completed candidate work is cleaned up, the old suffix is rerun with the old values and runtime services are recovered. The result is kRolledBack: the old lifecycle and values resume, Tags stay unchanged, a warning is reported and canceled work is not replayed. Fence and preparation, teardown, publication, cleanup, and restoration with recovery each carry a stop sign. If any of them fails, the result is kFailedStopped: the run fails, parameter candidates are retired, and partial publication may already have changed internal state.Owners paused at the control boundaryFence and runtime preparationTeardown: Stop, plus Release whenthe entry is Allocate or earlierCandidate forward suffixsucceedsfailsRuntime andcandidatepublicationClean upcompletedcandidate workRerun the oldsuffix, recoverthe runtimeDispatch resumesC/B Tags advanceParameter changesbecome publishedOld lifecycle andvalues resumeTags unchangedWarning, no replaykFailedStopped: any marked step failsThe run fails. Parameter candidates areretired, and partial publication mayalready have changed internal state.kPublishedkRolledBack

A stop sign marks a step whose failure ends the run. Restoration re-executes the old suffix after completed candidate work has been cleaned up. It does not undo arbitrary device effects. Only participating parameter candidates change values on publication.

Failure or outcomeWhat follows
Fence/runtime preparation failsFailedStopped before lifecycle callbacks
Pre-restart teardown failsFailedStopped without a forward suffix or restoration
Forward callback failsUnwind completed candidate work, then attempt old suffix restoration if cleanup succeeds
Cleanup, restoration, runtime recovery or publication failsFailedStopped and a failed run
kPublishedResume local dispatch. Publish participating parameter candidates and advance C/B receiver generations
kRolledBackContinue with old lifecycle epoch and published parameters, unchanged C/B Tag generations, and a warning diagnostic

RolledBack does not restore canceled candidates or queue contents. Its warning carries detail.transaction_id. FailedStopped ends the run without relaunching owners. A parameter candidate is retired without publication on that path. Partial runtime publication may already have changed internal state, so a failed run does not guarantee intact generations.

A stop request cancels queued work and closes ingress. An already executing dedicated restart retains its actual outcome before terminal stop. Parameter-origin work has an additional stop-before-publication fence.

Inspect the plan, then observe the right result

QuestionEvidence to inspect
Which checkpoints have callbacks?lifecycle_checkpoints(), including callback_present and topological level
Which same-stage edges exist?lifecycle_dependencies()
Who belongs to a target’s closure?lifecycle_restart_closures()
Why was the entry stage widened?Each closure record’s stage_resolutions in lifecycle_restart_closures()
Was a dedicated restart accepted and how did it finish?Receipt, then control-handle inspect() / await()
Did a parameter candidate publish or roll back?Parameter control result for its accepted update sequence
Which activation generation was observed?Compare ExecutionContext::tag() before and after each transaction

The public plan does not expose every runtime receiver-binding record. For a plain downstream consumer, derive the data relationship from the graph and observe Tags and callback absence. See Plan tooling for inspection.

For a restart that seems broader than expected, check forward lifecycle reachability before the requested stage. Lifecycle-selected nominal rates can add closure dependencies. A fixed-rate example does not exercise that path. If resources need rebuilding earlier, declare the actual Allocate ordering or lifecycle-mutable stage. Camera mode changes are device-specific: the owning operator must quiesce acquisition, invalidate the right resources and restart with valid timestamps. Restart does not promise physical exposure alignment. Keep such reconfiguration outside a CUDA capture region, which rejects restart targets or closure members.

Check your prediction

Try answering these questions from the graph:

QuestionPrediction
Can Left compute while Fusion is still in Arm?No. Initial compute waits for global startup
Does restarting Left include Right?The baseline callback closure is Left/Fusion
What changes after adding the Stop edges?The closure includes both cameras and Fusion
Does a data-only Detector receive callbacks?No. It receives boundary handling

For a camera/IMU pipeline, predict the interval samples, missing policy and end-of-stream behavior using the worked bracket.

Recap

  • Fixed stages provide shared ordering, restart suffixes and cleanup rules. Sparse hooks let each operator implement only its relevant work.
  • Startup is global and stage-major. Restart callback scope is compiled from forward dependencies, then its effective entry stage is resolved.
  • Local dispatch pauses at the control boundary. Callback members, direct boundary consumers and remaining operators have different restart effects.
  • Publication, restoration and failed-stop outcomes preserve different state. Name each parameter, Tag and output counter explicitly.
  • Lifecycle readiness, hardware synchronization and temporal data selection solve different problems. Declare each requirement through its own mechanism.