(pipeline-is-a-temporal-producer-consumer-nest)= # Pipelines and stage boundaries These are language contracts and design extensions, not a promise of complete target support. Use the [executable examples](kernels.md) for current C++ spelling and [implementation coverage](../performance/tile/implementation.md) for admitted lowering. The [implemented software-prefetch path](../internals/tile/lowering.md#implemented-native-software-prefetch-path) is narrower than the scheduling model below; hardware async transfer and warp specialization remain future work. ```{contents} On this page :local: :depth: 2 ``` Pipeline belongs to Execution because it organizes when one logical spatial sub-hierarchy runs. It is not another memory hierarchy. Its natural parent is therefore visible in the same syntax as any other nest: ~~~cpp PipelinePolicy policy{.window = 2, .interval = 1}; for (auto &nest : parallel(grid_shape)) { for (auto &subnest : nest.parallel(subnest_shape)) { for (auto &k : subnest.pipeline(iteration_space, policy)) { k.stage("produce"); // Producer operations for k.index(). k.stage("consume"); // Consumer operations for k.index(). } } } ~~~ There is one logical pipeline instance per `subnest` coordinate. Moving the pipeline outside that `parallel` changes the semantics to one pipeline shared by the parent nest; it is not merely formatting. The pipeline loop adds a temporal coordinate but does not deepen the spatial execution frontier. Like `parallel` and `serial`, the C++ range executes its body exactly once during capture. Dereference creates `PipelineOp`, pushes its body, and returns a non-copyable iteration handle; `k.index()` is its staged coordinate. Increment closes the region after discovering carried values. A multidimensional domain returns a tuple-like `index()`. Generated code executes all logical iterations through a prologue, steady state, and epilogue chosen by scheduling. ## Stage boundaries are lexical A pipeline is a repeated producer/consumer graph, not a loop with only an `interval` annotation: ~~~text Pipeline = (IterationSet I, StageSet S, StageOrder = 1 Issue(stage s, iteration i) = i * interval + theta(s) Issue(op in s, i) = Issue(s, i) + delta(op) Issue(sc, i + distance) >= Issue(sp, i) + latency(e) Schedule(op, i) = (Issue(op, i), anchor(op), frontier(op), Active(op), engine(op)) Version(materialized edge or MemoryState, i) -> VersionCoord ~~~ `theta(s)` places a logical stage in the modulo schedule and `delta(op)` orders operations within it. `window` bounds the scheduling window; it is not the number of logical stage segments and is not blindly copied to every buffer depth. `interval` belongs in `PipelinePolicy` because it is a primary temporal constraint. `window = 0` lets the planner choose, `window = 1` disables cross-iteration overlap, and `window = N` permits at most N in-flight iterations without promising to fill the window. `interval` must be positive; its default is 1. The compiler derives: - prologue, steady state, and epilogue; - async-copy and compute issue points; - per-edge live version count; - storage ring indices for materialized edges; - barriers, events, waits, and fence scopes; - resource pressure and legality. Only a materialized edge's `VersionCoord` enters its `AddressMap`. Pure SSA edges do not acquire fictitious memory versions. ### Issue, completion and release are different events The schedule equation above abbreviates a supported protocol, not a license to implement asynchronous readiness with a guessed fixed delay. An atom's issue need not complete its writes or stop its reads. Consumer readiness and storage reuse therefore have different prerequisites: ```text consumer A completes(v) --+ producer completes(v) --< +--> release(v) consumer B completes(v) --+ | v overwrite the same slot ``` Both consumers wait for the relevant producer completion/publication. Reuse waits for every actual reader of that version to release it, including reads still in flight after their textual last use. Unrelated consumers do not join that release. Versions sharing a physical ring slot must have nonoverlapping live intervals; a missing protocol or progress guarantee makes the plan illegal, however attractive its predicted latency. Copy elimination may remove data movement but must preserve any still-needed publication or release edges. The compiler derives these from visible SSA and memory effects; opaque atoms supply effect/protocol contracts. These are internal obligations, not required user `owned_by`/event annotations. The [planner](../internals/tile/planner.md#formal-finite-optimization-problem) owns the resource-constrained formulation and its [Cypress/Tawa comparisons](../internals/tile/related-work.md) explain the prior mechanisms informing it. ```{figure} ../../_static/tile/pipeline-timeline.svg :alt: Three pipeline iterations overlap across load, compute, and store engines while their memory versions remain live. :width: 100% The scheduler overlaps stage instances subject to dependence latency; storage version count follows liveness rather than source stage count. ```