FrozenArray
Take independent typed snapshots, share immutable numerical payloads across repetitions and inspect explicit memory and transport limits.
ImplementedRequires Aner 0.0.4 or newer. Native shared memory mappings are available on Linux and macOS. Current Windows packages use bounded private process copies and typed wire transport instead of native shared memory mappings.
array.snapshot() creates a FrozenArray<T> containing an independent immutable copy of the array's live numerical elements. parallel.repeat tasks share that backing buffer within each process instead of copying it for every iteration. Cluster repetitions distribute each distinct frozen payload once per compatible host and map native read only shared memory into its worker processes. Platforms or namespaces without that capability retain the bounded per process copy transport. Ordinary Array, DynamicArray, NDArray, List, and class parameters retain their existing private copy behavior.
import aner.parallel
fn simulate(parameters: List, iteration: Int64, seed: Int64) -> Float64 {
let input = cast<FrozenArray<Float64>>(parameters.get(0))
return input[iteration % input.len()] + Float64(iteration)
}
let initial = Array<Float64>(size: 1000, fill: 2.0)
let input = initial.snapshot()
initial[0] = 99.0
let results = parallel.repeat(iterations: 1000, task: simulate,
parameters: List(input), execution: parallel.local(), seed: 42)
print(input[0]) // 2.0
print(results.value(0, 0)) // 2.0
print(results.value(999, 0)) // 1001.0
print(results.summary())The complete example runs without external data or services. A snapshot is explicit: taking it copies the input once; assigning its handle and passing it to local repetitions share its immutable storage. Mutable parameter graphs and small shape/owner descriptors are still cloned. This feature removes repeated numerical payload copies, rather than all task allocation.
Types and access
Snapshots support all eight fixed width integer dtypes, Float32, and Float64. They preserve values and precision, including the full UInt64 range. Bool, String, Number, Any, units, nested buffers, and object elements are outside this milestone. No additional import is needed to create or read a snapshot. FrozenArray<T> requires one explicit numerical dtype when used in an annotation or cast.
FrozenArray is contextual: existing user classes, functions, variables, and generic parameters with that name remain valid. A same named user class takes precedence in explicit type annotations; values inferred from .snapshot() still retain their native immutable type.
Array and DynamicArray snapshots have shape List(length). DynamicArray copies only live elements, excluding unused reserved capacity. NDArray snapshots retain their shape, rank, row major order, and element strides. Empty axes remain empty. Their rank and element limits follow existing numerical array limits.
| Operation | Result | Cost |
|---|---|---|
source.snapshot() | FrozenArray<T> | O(n) time and new numerical payload; shape metadata O(rank). |
input.len() | Int64 | O(1), total element count. |
input.is_empty() | Bool | O(1). |
input.rank() | Int64 | O(1), number of dimensions. |
input.shape() | List | O(rank), independent metadata copy. |
input.strides() | List | O(rank), independent element stride copy. |
input.bytes() | Int64 | O(1), numerical payload bytes only. |
input.get(index: Int64) | T | O(1), flat row major index. |
input[i, j, ...] | T | O(rank), exactly one coordinate per axis. |
type(input) / type_of(input) | String | Reports FrozenArray<T>. |
dtype(input) | String | Reports the concrete numerical dtype. |
There is no direct FrozenArray constructor and no setter, resize, sorting, reshape, or mutable conversion method. Indexed and compound assignment are rejected by checking. Boxing in Any preserves the frozen type; a cast to a mutable array fails rather than granting write access. The native heap also rejects attempts to use mutable array operations on a frozen descriptor.
Ownership and lifetime
The source stays mutable. Every source alias, including shared NDArray reshapes, continues to refer to the original storage. Changes to those aliases cannot affect the new snapshot. The frozen backing is held by const shared owners, and no mutable source descriptor refers to it.
Copies of a frozen handle in Lists, classes, Optional/Result values, or task parameter graphs retain the same immutable backing. Each task gets its own managed descriptors and mutable state. Frozen backing survives source collection and task completion while a caller or another task still retains it; the final owner releases it. Failed tasks and cancellation retain the existing joined, all or failure repetition behavior.
Frozen inputs work with parallel.local and enrolled CPU processes selected by parallel.cluster. A cluster run binds each distinct immutable input once per selected worker process before scheduling its first task, importing it on a cache miss. Repeated handles in the admitted List tree refer to that registration, and later batches reuse it. Compatible processes on the same host map one native read only shared segment for each backing. The first worker receives and copies the payload into that segment once; peers attach without allocating their own numerical payload. Different hosts use separate segments. Native capability and namespace checks select the existing private copy path when a shared segment cannot be used. Addresses remain private to each process.
The remote parameter contract remains narrower than the local heap clone: numeric values, Bool, String, FrozenArray, completed OutputResults and nested List trees are admitted. Immutable aliases are supported; shared mutable List aliases, cycles, mutable input arrays, classes and Optional/Result wrappers remain rejected. A task can still construct its own private mutable arrays. Each call clears its checked state and bindings when it ends, while the owning program or native Session keeps one admitted worker cohort and a bounded immutable input cache for unchanged ordered worker definitions. Later calls, including calls to other hosts, can bind cached inputs without repeating payload or producer metadata transfers, shared staging or numerical copies. Each worker retains at most 64 cache entries and 64 MiB of native storage, plus 1 MiB of serialized producer metadata, with least recently used eviction; active call input admission has its own 64 MiB bound. Owner only segment names are unlinked after registration, before tasks run; mapped storage remains live until its owners release it. Program return, Session reset/destruction, changed worker definitions, execution failures and cancellation reap the cohort and release its cache and mappings. Idle cohorts expire when another cluster call begins after 300 seconds. Name cleanup uses live workers and bounded reconnection to approved hosts; it is best effort if a host becomes unreachable before names are unlinked.
See the shared cluster example and cluster limits and accounting. Shared immutable input does not authorize concurrent mutation of ordinary array buffers or Tensor graphs.
Memory admission and accounting
Snapshot construction admits its complete numerical payload under the existing 64 MiB array budget before copying it. The source allocation and snapshot coexist and both count toward that heap's budget. Object and shape limits also apply. A failed allocation or admission leaves the source unchanged.
When a frozen parameter is cloned, each receiving heap charges its visible array payload once per distinct backing buffer, regardless of how many aliases retain it. The shared payload does not consume the 16 MiB parameter copy quota because it is not copied; managed descriptor metadata still consumes that quota. Ordinary mutable arrays retain the previous copy and admission rules. Heap accounting describes logical retained bytes, not a total process RSS limit; summing each task's visible shared bytes would count the same physical allocation repeatedly.
Completed OutputResults<T> can also be submitted directly as immutable inputs without taking another snapshot; see completed output reuse. Their shared retained storage includes the producing run's coverage and row controls as well as numerical payload. They use repetition input limits rather than the ordinary frozen array heap budget.
For local repetitions and cluster repetitions using immutable inputs or typed outputs, results.summary() includes:
| Field | Meaning |
|---|---|
shared_input_bytes | Unique FrozenArray and completed result numerical payload reachable from the submitted parameter graph, counted once across aliases. |
shared_input_buffers | Number of distinct immutable backings, including empty buffers. |
shared_input_retained_bytes | Unique retained native input storage: frozen payload and completed result payload, coverage and row controls. |
parameter_snapshot_bytes | Logical private descriptor and payload bytes charged by the initial parameter clone; excludes shared immutable numerical backing. |
parameter_snapshot_array_copy_bytes | Numerical array payload copied in that initial clone, excluding immutable numerical backing. |
task_array_copy_bytes | Sum of numerical array payload bytes copied into all iteration heaps, excluding immutable numerical backing. |
input_accounting_scope | Describes these logical measurements and exclusions. The older scalar/List cluster route without immutable numerical inputs retains its existing unavailable input accounting label. |
The shared fields describe retained submitted input, not cumulative snapshot construction elsewhere in the program. Each explicit .snapshot() creates its own backing and is counted separately if submitted. Copy totals include mutable array capacity according to its existing clone contract; temporary arrays created by the task body are outside these input copy totals.
Existing max_bytes, result_bytes, and managed_bytes retain their result buffer meanings. Input metadata does not consume a caller's result only budget. Allocator overhead, interpreter frames, task workspace, unrelated native values, and whole process RSS are outside these counters. The counters expose sharing and copied input traffic; they are not a complete job memory ceiling or a speedup measurement.
For a cluster run, shared_input_bytes and shared_input_buffers describe the coordinator's unique submitted backings. cluster_input_payload_bytes counts numerical bytes sent once per native host segment or private fallback import. cluster_input_shared_host_bytes and cluster_input_shared_segments count unique shared storage per host; cluster_input_shared_mapping_bytes and cluster_input_shared_mappings sum the corresponding process views. A 1 MiB frozen allocation submitted under three aliases to four compatible processes on one host therefore reports 1 MiB of input traffic, one shared segment, four mappings and zero cluster_worker_input_payload_copy_bytes. The one copy into shared storage is reported by cluster_input_shared_staging_copy_bytes. A fallback process retains its own numerical copy, reported by cluster_worker_private_input_bytes and cluster_worker_input_payload_copy_bytes. The host/mapping payload fields use logical numerical lengths. cluster_input_shared_allocation_bytes and cluster_input_shared_mapping_allocation_bytes separately count segment headers and operating system page padding; kernel bookkeeping remains outside these counters. Repeated task batches add no input payload traffic.
cluster_coordinator_input_wire_bytes reports the extra canonical input wire cache held during the run; it equals the unique immutable numerical payload bytes. cluster_worker_peak_input_wire_bytes sums each process's largest incoming input frame; attachments to an existing segment carry no numerical payload. cluster_input_transfers counts shared creation attempts and private imports, including empty backings, while cluster_input_bindings counts descriptors registered across all workers. Completed inputs retain serialized producer metadata, reported by cluster_coordinator_input_metadata_bytes and cluster_input_metadata_bytes. cluster_worker_input_retained_bytes sums visible mapping lengths and private allocations; it can count one physical shared segment repeatedly. A recoverable shared memory registration failure may require additional payload or metadata traffic before private import; these attempts remain included in transfer counters.
The coordinator retains one canonical wire copy per unique input backing during the run, adding shared_input_bytes to its logical input storage. Each worker admits at most 4,094 immutable descriptor bindings and 64 MiB of combined retained immutable storage, subject to checked shape and object limits. Initial admission conservatively includes completed result coverage and row controls so the same inputs remain valid through the private fallback path; native mapped imports need only their numerical payload. Ordinary FrozenArray inputs also keep their array heap limits. Canonical numerical transport retains the exact dtype and represented bits, including UInt64 values, floating signed zero and subnormals. Frozen floating buffers preserve nonfinite entries as numerical bits; tasks must still produce finite outputs. Unsupported parameter types, nonfinite scalar source parameters, malformed descriptors, mismatched payload lengths and budget failures abort the run. Transport and worker workspace counters remain separate from coordinator output storage; see the full field reference.