Typed output and resident pipelines
Write checked numerical output regions, reuse immutable results and explicitly collect resident cluster results.
ImplementedRequires Aner 0.0.4 or newer. Native shared memory output and direct mappings are available on Linux and macOS. Current Windows packages use bounded private output buffers and typed wire transport instead of native shared memory mappings.
By default, parallel.repeat allocates one numerical result buffer before tasks start and gives each task exclusive access to its own contiguous section. Local tasks write retained output directly. Compatible cluster workers on the same host share one native output payload and write their assigned global ranges into it. Workers sharing the coordinator's host return completion metadata without serializing or transporting numerical output. Other hosts send completed mapped ranges through bounded typed frames, without a numerical serialization buffer. Unavailable or isolated native namespaces use private batch payloads. After every task completes, the caller receives immutable OutputResults<T> in iteration order, with the exact numerical dtype and shape.
import aner.parallel
fn simulate(parameters: List, iteration: Int64, seed: Int64,
output: OutputRegion<Float32>) -> Unit {
var step = 0
while step < 8 {
output[step, 0] = Float32(iteration)
output[step, 1] = Float32(step)
step += 1
}
}
let history = parallel.repeat(iterations: 1000, task: simulate,
parameters: List(), output_shape: List(8, 2), seed: 2026)
print(history.shape()) // List(1000, 8, 2)
print(history[999, 7, 1]) // Float32 value 7.0Run the complete example to combine shared frozen inputs with typed output regions. Existing three parameter tasks returning a scalar or numeric List keep their existing SimulationResults API, argument labels, defaults and result budgets.
Submission and layout
The task must be a directly named top level function with this exact shape:
fn task(parameters: List, iteration: Int64, seed: Int64,
output: OutputRegion<T>) -> Unit { ... }The numerical dtype T is inferred from the fourth task parameter. There is no dtype argument and no generic parallel.repeat<T> call. Supported dtypes are Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64, Float32 and Float64. Values retain their exact dtype, including the full UInt64 range; results are not converted to Float64. Bool, String, Number, Any, units and nested element types are not output dtypes.
The typed overload is:
parallel.repeat(iterations, task, parameters, output_shape,
execution: parallel.local(), seed: 42, max_bytes: 67108864,
resident: false)The first four arguments are required. Optional named arguments can be supplied independently and named arguments may be reordered. parameters remains an isolated List graph: each task gets private mutable state, while immutable FrozenArray and completed OutputResults<T> numerical payloads are shared within a process. Compatible cluster processes on the same host map one native read only segment per backing, with private copy transport available when native sharing is unavailable. Cluster parameters retain the narrower admitted List tree contract described in the cluster reference.
iterations ranges from 1 through 1,000,000. output_shape contains 1 to 7 nonnegative integer dimensions, each at most 10,000,000, subject to checked product and memory limits. Zero length dimensions are valid. A task sees only these axes. The completed result prepends an iteration axis, giving a maximum result rank of eight. Storage is contiguous and row major. Empty outputs use canonical zero strides.
For example, iterations: 1000 and output_shape: List(8, 2) allocate 16 cells per task and publish shape List(1000, 8, 2). Task i can address only its own [8, 2] region; it never receives an address or index into another task's region. Shaped brackets require exactly one coordinate per axis. get and set instead use a flat row major index within the region.
Region methods and lifecycle
| Operation | Result | Cost |
|---|---|---|
output[index, ...] / output.get(index) | Exact T from an initialized cell | O(rank) / O(1) |
output[index, ...] = value / output.set(index, value) | Unit | O(rank) / O(1) |
output.fill(value) | Unit; initializes every region cell | O(cells) |
output.len() / output.is_empty() | Int64 / Bool | O(1) |
output.rank() / output.bytes() | Int64 | O(1) |
output.shape() / output.strides() | Detached Int64 List | O(rank) time and output space |
Writes require the declared element dtype after the language's checked contextual conversions. Integer conversions must fit the destination. Use explicit Float32(value) when rounding to Float32 is intended; overflow and nonfinite values fail. Indices and values are validated before mutation. Compound assignment reads an existing cell, so that cell must already be initialized. Writing a cell more than once is allowed. The runtime checks per cell initialization rather than assuming zero filled storage represents a valid result.
Every cell must be initialized before the task returns Unit. Reading an unwritten cell or returning with incomplete output fails the batch. Nonfinite writes, overflow, out of region access, task exceptions and execution budget exhaustion also fail the batch. Task failures are reported as R2803 with the iteration and original diagnostic; ordinary invalid result access uses the array diagnostic R3001.
Helpers may receive and return region handles. Task local aliases through Any, Lists, Optional/Result wrappers and classes refer to the same scoped capability. They cannot extend its write lifetime. Each capability belongs to the executing task thread and is revoked when that task completes or fails. Native access checks the active capability and bounds; no raw pointer, resize, reshape, mutable array cast or ownership transfer is exposed. Existing transitive task validation rejects global captures, global writes, nested repetitions and unsupported effects.
The executor joins active tasks before publishing a result or releasing failed run storage. A failed batch publishes no partial result. The caller can read completed output only after parallel.repeat returns; there is no live reading or partial publication API. Tasks write disjoint numerical and initialization cells without a shared mutex for each cell. Scheduling, row ownership and publication still require synchronization.
For native cluster output, each worker batch receives only its assigned global range through the checked runtime. Its tasks keep the same bounded OutputRegion<T> interface, with private initialization coverage and lease controls. A task cannot address another iteration's row through that interface. The worker joins its tasks and surrenders the mutable payload mapping before reporting completion. The coordinator validates the assigned range and its completed values, then marks directly mapped rows complete without copying them. Other host workers stream only their completed ranges from the read only mapping; the coordinator validates and copies those received bytes into its own final range. After every writer has finished, native segment names are removed and the retained result payload is sealed read only before immutable publication. A completion message alone cannot make an incomplete or invalid region a successful result.
Completed results
| Operation | Result | Cost |
|---|---|---|
results[index, ...] / results.get(index) | Exact T; full result coordinates / flat index | O(rank) / O(1) |
results.value(iteration, column) | Exact T; flat column within one region | O(1) |
results.rows() / results.cols() | Repetition count / flat cells per region | O(1) |
results.len() / results.is_empty() / results.rank() / results.bytes() | Size metadata | O(1) |
results.shape() / results.strides() | Detached Int64 List | O(rank) time and output space |
results.seed() / results.seed(iteration) | Master / derived task seed | O(1) |
results.workers() / results.seconds() | Admitted workers / executor seconds | O(1) |
results.summary() | JSON execution and memory metadata | O(summary bytes) |
results.is_resident() | Bool; authoritative output remains on cluster workers | O(1) |
results.collect() | Immutable OutputResults<T> in coordinator memory; an alias for ordinary results | O(payload bytes + iterations + workers) for resident results, O(1) otherwise |
type_of(results) reports OutputResults<T> and dtype(results) reports T. Any boxing and checked casts preserve the native type and exact dtype. OutputRegion and OutputResults are contextual source names: preexisting same named classes, functions, variables and generic parameters remain valid. An inferred native result retains its native identity even when a same named user class exists.
Completed results share native ownership when assigned or returned. Ordinary collected results remain valid after worker interpreters are destroyed. Resident results depend on their owning worker cohort; see resident output pipelines. They cannot be mutated, constructed directly, reshaped or cast to a mutable Array/NDArray. This milestone does not add Float64 reductions, CSV/List exports or mutable conversions to OutputResults. The existing SimulationResults methods remain available for legacy tasks.
Reuse completed outputs as inputs
Pass a completed OutputResults<T> directly in the next call's parameters. No snapshot or conversion is needed. A checked helper can accept its exact native type:
fn read_history(history: OutputResults<Float32>, iteration: Int64) -> Float32 {
return history[iteration, 7, 1]
}
fn summarize(parameters: List, iteration: Int64, seed: Int64,
output: OutputRegion<Float32>) -> Unit {
let history = cast<OutputResults<Float32>>(parameters.get(0))
output[0] = read_history(history, iteration)
}
let next = parallel.repeat(iterations: 1000, task: summarize,
parameters: List(history), output_shape: List(1),
execution: parallel.local())
print(next[999, 0]) // Float32 value 7.0Run the complete two stage example locally or replace its execution policy with an enrolled cluster.
Local parameter snapshots and iteration heaps retain the same immutable native allocation. They copy bounded handle metadata without copying numerical payload, coverage or row controls. Aliases through nested Lists, Any, Optional/Result, Dictionary values and ordinary classes retain the same backing. Mutable surrounding objects remain private to each task. A completed source stays readable and can be submitted repeatedly after its producing task interpreters or function scopes have ended. Local completed result retention is bounded to 1 GiB per submitted parameter graph, separately from the 16 MiB copied parameter quota and each new output's max_bytes.
parallel.cluster accepts completed results directly or inside its admitted List trees. Each distinct backing is registered once per worker on a cache miss and shared by its task threads across batches. When a result already owns a sealed native shared memory payload, compatible workers on the coordinator's host map that original storage through a read only descriptor capability. No numerical payload is restaged, transported or copied, and no second input segment or coordinator wire cache is allocated. Descriptor aliases reuse one backing and process mapping. Later calls with the same cluster definitions reuse the admitted workers and their bounded immutable input cache; a cache hit needs no new descriptor or producer metadata transfer. The original output name stays unlinked throughout reuse.
Private completed results and FrozenArray inputs use one new read only segment per compatible host: the first process receives the payload once and peers map it without per worker numerical payload copies. Other hosts receive their own segment on a cache miss; retained workers can reuse their cached input on later calls without another payload transfer. Unsupported platforms, isolated namespaces and unavailable capability resources retain bounded transport and private import paths. An existing mapped result supplies a borrowed immutable numerical span to these transport paths, avoiding a coordinator serialization buffer even when direct descriptor reuse is unavailable. Transfer preserves exact dtype and shape, including empty axes, UInt64 values, signed zero and subnormals. All result readers retain the original producer's metadata: master and derived seeds, admitted workers, timing and the complete summary() string. An imported result remains OutputResults<T>; it is not converted to a FrozenArray. Results produced on a cluster may be inputs to later local or cluster calls. Three parameter numeric tasks can read these inputs and keep the existing SimulationResults API.
The native capability broker is private to one submission and transfers only read only descriptors for admitted immutable inputs. It closes before task execution. Workers validate descriptor access, ownership, storage format, size and shape before registration; they cannot obtain a writer mapping through this capability. Producer ownership, coordinator coverage and row controls stay alive with the original result, while worker imports retain only the immutable numerical mapping and producer metadata. Empty results reuse the same ownership capability with zero payload pages.
The cluster's existing narrower parameter graph remains in force: List aliases/cycles, Optional/Result wrappers, Dictionary values and nominal classes are not newly admitted remotely. Completed outputs remain immutable, and active or expired OutputRegion writer capabilities are still rejected from input graphs. Failed runs publish no reusable result.
Resident output pipelines
Set resident: true on a typed cluster call to leave completed numerical output on its producing workers. The returned value is still OutputResults<T>, with the full global shape, exact dtype, seeds and producer summary. The coordinator holds bounded descriptors and producer metadata; it does not allocate or collect the numerical result. Each worker retains one immutable contiguous shard, written directly by its task regions. This works across remote hosts without collecting output and sending it back for the next stage.
let history = parallel.repeat(iterations: 1000, task: simulate,
parameters: List(), output_shape: List(8, 2),
execution: cluster, seed: 2026, resident: true)
let next = parallel.repeat(iterations: 1000, task: summarize,
parameters: List(history), output_shape: List(1),
execution: cluster, resident: true)
let local = next.collect()
print(local[999, 0])Run the complete resident pipeline after preparing your private cluster configuration. The resident flag defaults to false and is available only for the four parameter typed output overload. Producing resident output requires a cluster CPU context. An ordinary local or collected result's collect() returns an alias to the same immutable allocation, without copying its numerical data.
Resident producing calls divide the global iteration range into balanced contiguous partitions by configured process order. Every later call consuming a resident input uses that same partition and worker affinity, even when its own resident flag is false and its output is collected normally. The iteration count, owning cohort generation and partition layout must match. Multiple resident inputs must be co partitioned. Changing task code, seed, region shape or ordinary frozen parameters remains supported; numerical resident input is neither collected nor redistributed. Each worker receives bounded original producer metadata and attaches its existing native shard to fresh checked call state. Repeated aliases bind the same shard once.
Task code indexes resident inputs using the original global iteration identity:
let history = cast<OutputResults<Float32>>(parameters.get(0))
output[0] = history[iteration, 7, 1]Metadata readers expose the full global result. Numerical get, value and bracket reads on a worker may access only rows stored in that worker's partition. Access to a different partition fails clearly; there is no hidden network read or automatic redistribution. Algorithms requiring other workers' rows must collect explicitly and submit the collected result through the ordinary immutable input path. On the coordinator, numerical reads of a resident result fail with a request to collect first. Shape, strides, dtype, size, seeds, worker count, timing and summary remain available without collection.
Printing a resident result describes its type, global shape and numerical byte count without previewing cells. The development Inspector also reads metadata only: it marks the result resident: true and leaves its numerical preview empty, even when value previews are enabled. Display and inspection do not collect shards or create network traffic.
collect() explicitly gathers every shard into its global iteration range and returns an ordinary immutable result. It preserves exact represented values and producer provenance. Collection leaves the resident source usable for further calls and does not rerun tasks. It is a coordinator operation: calling collect() inside an isolated repetition task is rejected by the checker (E2803). A lost worker or stale owner capability fails collection rather than returning incomplete data.
The collected result has a fresh coordinator summary that includes cluster_resident_collected_bytes, received numerical output and collection copy accounting. Its seed, producer execution metrics, nodes and input metrics are preserved. Each resident input attachment sends its original global producer metadata separately, with the existing 16 KiB per binding bound; resident store metadata snapshots count local worker summaries rather than that transmitted global metadata. The original resident result's summary stays unchanged. output_residency reports workers for a resident coordinator handle and coordinator for ordinary collected storage.
Each worker's authoritative resident store admits at most 64 shards and 64 MiB of native retained storage, including payload, initialization coverage and row controls, plus 1 MiB of stored local worker summary metadata. This is separate from the immutable input LRU cache, active call inputs and new output admission. Live resident results are pinned and are never evicted to make room for another result. Capacity is reserved before tasks start; insufficient capacity fails admission. Empty numerical regions still consume entry and control/metadata capacity.
Resident execution uses one bounded generated partition program per worker; the existing 1 MiB generated source guard applies. It does not silently stream multiple unbounded resident batches. Producer summary reservation is conservatively 16 KiB per store entry, and metadata snapshots report the actual stored local summary bytes.
A coordinator resident descriptor reports the complete logical payload through bytes() and result_bytes, with managed_bytes, coverage_bytes, region_control_bytes and retained_output_bytes all zero because it owns no numerical allocation. cluster_resident_output_retained_bytes and related resident fields report the actual worker storage. Fixed handle and shape metadata are excluded from these logical native counters. Worker side immutable views keep global size/shape metadata while retained native input accounting describes their actual local shard. Resident attachments also contribute their actual shard allocation once per distinct resident identity and worker to cluster_worker_input_retained_bytes and cluster_worker_private_input_bytes. Those views overlap cluster_resident_output_retained_bytes; binding does not create another native allocation.
Copies, Any/List aliases and task inputs share the resident owner's lifetime. When actual managed heap collection releases the final coordinator alias, its owner queues a remote release; a subsequent pool acquisition flushes queued releases before admitting new output. Session cell completion is a collection boundary. Leaving a source scope alone does not immediately collect unreachable language descriptors, so a large standalone loop can reach the 64 entry pinned store limit before collection or teardown. Live resident results prevent the usual 300 second idle expiry. Program return, Session reset/destruction, a changed worker configuration, worker loss, execution failure or cancellation releases the cohort and invalidates its resident handles. A stale handle cannot be reused by a replacement worker. Collected results keep their own local immutable storage through these events.
Memory accounting and admission
max_bytes is a positive per run logical output allocation budget. The default is 64 MiB (67,108,864 bytes) and the maximum is 1 GiB (1,073,741,824 bytes). Admission checks all output allocations before tasks start:
- Exact packed numerical payload: iterations × cells per region × dtype width.
- One initialization coverage byte per output cell.
- Per row control for ownership and completion, using the actual native control size.
Ordinary results retain coverage and region control allocations along with their numerical payload. Resident workers retain those allocations in their bounded authoritative stores. The counters report this storage honestly; they do not imply that temporary control storage has been freed. Publication does not copy the completed coordinator payload or create a second result array. Native cluster output shares one full logical output payload per participating host and allocates only coverage and lease controls for each worker batch. Coordinator host workers reuse the final retained payload directly. Other host and private fallback collection copies accepted worker payload into its final assigned region; those copies are reported separately. Native segment headers, operating system page padding and other host output segments are reported outside the coordinator's logical max_bytes admission.
| Summary field | Meaning |
|---|---|
result_bytes | Exact packed numerical payload bytes. Same value as bytes(). |
coverage_bytes | Per cell initialization tracking allocation. |
region_control_bytes | Per row native ownership/completion allocation. |
managed_bytes | Sum of payload, coverage and region control allocations; admitted against max_bytes. |
retained_output_bytes | Retained native output allocation, equal to managed_bytes in this implementation. |
task_output_copy_bytes | Zero: task code writes its region directly. Binary fallback collection is reported by cluster_received_output_bytes; native shared output requires no numerical output frame. |
shared_input_bytes / shared_input_buffers | Unique reachable FrozenArray and completed result numerical payload bytes/backings, deduplicated across aliases. |
shared_input_retained_bytes | Unique retained native input allocation: FrozenArray payload plus completed result payload, coverage and row controls. |
parameter_snapshot_bytes | Logical copied initial parameter graph storage. |
parameter_snapshot_array_copy_bytes | Mutable array payload copied into the initial parameter snapshot. |
task_array_copy_bytes | Aggregate mutable array payload copied into iteration heaps. |
These counters describe logical owned allocations and copying, not RSS or a process wide memory quota. Fixed state and shape metadata, handle descriptors, allocator overhead, interpreter task heaps and executor workspace are outside max_bytes. Input accounting remains separate from output admission. The ordinary array heap also retains its own limits for ordinary Array/FrozenArray storage; output allocation follows the explicit repetition output budget.
The input wire cache and import staging are explicit: cluster_coordinator_input_wire_bytes counts unique owned serialized numerical payload, while cluster_coordinator_input_borrowed_bytes counts existing mapped numerical spans used without a serialization copy. Borrowed spans are retained source views, not additional payload allocations. cluster_worker_peak_input_wire_bytes sums each process's largest incoming metadata plus payload frame. cluster_coordinator_input_metadata_bytes counts serialized producer metadata retained for unique completed inputs, and cluster_input_metadata_bytes counts those metadata bytes sent across workers. cluster_worker_input_retained_bytes sums visible mapping lengths and private allocations across worker processes; completed result coverage and controls exist only in private fallback imports. These allocations are separate from response buffers and the new output's max_bytes.
cluster_input_shared_host_bytes, cluster_input_shared_segments and cluster_input_shared_allocation_bytes count newly created input segments. Existing source storage reused through descriptors is counted separately by cluster_input_shared_reused_bytes, cluster_input_shared_reused_buffers and cluster_input_shared_reused_allocation_bytes. Those reused bytes are already part of shared_input_retained_bytes and are not a new physical allocation. cluster_input_shared_mappings, cluster_input_shared_mapping_bytes and cluster_input_shared_mapping_allocation_bytes include all worker views, with their reused subset reported by the corresponding cluster_input_shared_reused_mappings, cluster_input_shared_reused_mapping_bytes and cluster_input_shared_reused_mapping_allocation_bytes. Allocation counters include native headers and operating system page padding; empty backings require no mapped pages. Aliases and batches never multiply backing counts or process mappings.
With direct reuse, input payload traffic, new input segments, segment staging copies and worker private payload copies are zero. Producer metadata and descriptor registration still require bounded transport. Newly created host segments report their one creation copy through cluster_input_shared_staging_copy_bytes. cluster_worker_private_input_bytes and cluster_worker_input_payload_copy_bytes describe private fallback imports. Segment names are private and unlinked before task execution; successful completion clears current call bindings while bounded cache ownership can retain immutable mappings. Failure, cancellation and interpreter teardown release the worker cohort and its cache while the source result remains owned by its caller. If a host becomes unreachable during initialization, name cleanup uses live workers or bounded reconnection and remains best effort. A source result's producer summary describes its original execution; the consuming call's summary accounts the source's physical retained input storage. If compatible workers use an ABI with a different native control size, the preserved producer summary still describes the producer; worker retained input accounting describes the actual imported native allocation.
Execution scope
Cluster calls in one program execution or native Session keep one worker cohort for identical ordered worker definitions. Changing task source, parameters or seed starts fresh checked call state in those processes. The immutable input cache retains up to 64 entries, 64 MiB native storage and 1 MiB serialized producer metadata per worker, using least recently used eviction. The authoritative resident output store separately pins up to 64 shards, 64 MiB native storage and 1 MiB local worker summary metadata; it does not evict live outputs. The active call's 64 MiB input admission is independent. A changed worker definition, idle expiry on the next call after 300 seconds when no live resident output pins it, failure or cancellation discards the cohort; program return, session reset and session destruction reap it. Workers acknowledge call completion only after dropping all output writers, so parked processes cannot mutate published results. See cluster lifecycle and accounting.
The consuming summary reports new process starts and reuse, cache hits/misses, stores/skips, eviction, retained/peak cache storage and producer metadata. A warm immutable input hit sends no numerical or producer metadata, creates no segment, copies no payload and does not increment the direct descriptor reuse counters. End of call cache ownership can overlap the current call input storage counters; these are logical views, not allocations to add into an RSS estimate. Cache keys include kind, exact dtype, shape, numerical bytes and complete producer metadata, preserving the original producer summary even when another result has equal numerical values.
Ordinary typed output supports CPU contexts from parallel.local and parallel.cluster; resident output production requires parallel.cluster. The following collection behavior describes the default resident: false mode. On a cluster, each process gets checked task/helper source and immutable input registrations once, then executes bounded batches. Compatible processes on a host share its output payload. Coordinator host workers write their assigned rows directly into the final retained payload and return row identities and accounting without numerical output. Other host workers stream completed mapped ranges through exact typed frames without a serialization buffer; unavailable native namespaces use private batch payloads. The coordinator checks run identity, dtype, shape, assigned iteration range and complete accounting before committing rows, including finite value checks and payload length checks on binary frames. Results are published only after all selected workers finish, all rows are present and native writer mappings are revoked. Scheduling and completion order cannot change result order or task seed derivation.
For ordinary collected output, the coordinator preallocates and admits the complete final output against max_bytes before launching workers. Resident output uses worker shard allocation and independent store admission instead of allocating a complete coordinator payload. Other host shared output segments, worker batch coverage/controls, private fallback payloads and bounded transport frames are additional memory, reported separately by the cluster accounting fields. Native output avoids private worker payload and serialization buffer allocations. Coordinator host writers also avoid numerical output transport and collection copies. Other host workers continue to send bounded output frames; native output does not expose cross computer addresses. Up to 64 MiB of combined retained immutable input is registered per worker. Initial admission conservatively includes completed result payload, coverage and controls so private fallback remains possible; mapped completed inputs retain only numerical payload. The native transport bounds each numerical payload to 64 MiB and each completed input's serialized producer metadata to 16 KiB, with separate bounded headers/summaries and the existing 1 MiB generated source guard. Task failures, incomplete regions, malformed replies, lost workers and cancellation abort all sessions without publishing partial output. See the complete example.
GPU placement, exposing native pointers and concurrent remote mutation of caller arrays remain outside this API. Worker count remains subject to the existing CPU admission policy and does not guarantee speedup.
See the Native parallel computing module reference for the shared API.