Skip to content

Independent adaptive block schedules

The FlepiMoP2 provider can discover a separate accepted mesh for each independent block, then replay those meshes through compact JAX scans. Select the policy explicitly; block_schedule: shared preserves the existing shared-mesh default.

engine:
  module: flepimop2.engine.op_engine
  state_change: flow
  config:
    method: dopri5
    adaptive: true
    state_layout: block
    block_axis: loc
    block_schedule: independent
    block_batching: length
    block_batch_size: 16

The system must publish the compiler's proven block axis, block stepper and block-template shapes. Every state template must carry that axis. The provider uses the published runtime parameter-axis positions: local parameters are sliced at that position, while shared parameters are broadcast. Initial-condition parameters participate in the same contract. A declaration of state_layout alone supplies no proof. op_system rejects reductions, mixing and operators across the proposed block axis during compilation. Supported typed operators within a block remain part of its RHS.

This full-solve path requires real float32/float64 packed JAX states and deterministic explicit Euler, Heun, RK4 or DOPRI5. Independent meshes require compact replay (auto or compact); an explicit unrolled request is rejected. Current and minimum CPU qualification uses JAX 0.12.0.1 and 0.4.38. The minimum joint provider/compiler NumPy requirement is 2.2.6. Existing provider packing can promote integer or lower-precision input arrays to its real state dtype; complex packed states are rejected before discovery or preparation. CPU tests cover the stated first-derivative transforms. This does not qualify Torch full-solve transforms, GPU execution, implicit integration or controller derivatives.

Discover and validate

from flepimop2.engine.op_engine import IndependentBlockSchedule

discovered = engine.run_adaptive(
    system, times, initial_state, params, model_state=model_state
)
schedule = discovered.schedule
assert isinstance(schedule, IndependentBlockSchedule)
print(schedule.step_counts)  # accepted steps, in compiler block-axis order
print(schedule.batches(engine.config))  # original indices in each replay batch

replayed = engine.run_adaptive(
    system, times, initial_state, params,
    model_state=model_state, schedule=schedule,
)
replayed.require_accurate()  # host-side decision, outside JAX transforms
per_block = replayed.block_replay_diagnostics

IndependentBlockSchedule.blocks contains one ordinary AdaptiveSchedule per block, including its interval steps, numerical controls and context. The bank binds the structural model and each mesh's axis position. Changed controls, output grid, structural context or a reordered mesh bank are rejected. Each block discovers with the maximum scaled cell error within that block. Appending an unrelated stiff block cannot change another block's controller reduction. Declared forcing jumps are honored in every discovery and replay plan.

Replay returns per-block arrays for accuracy flags, maximum scaled local error, inaccurate-step counts and actual accepted-step counts, plus the existing scalar aggregate diagnostics. These arrays preserve original block order. The default local-error threshold is replay_error_factor: 1.25; this is a freshness check, not a global trajectory-error certificate. Independently check the observable accuracy needed by the consumer.

Parameters and initial-state values remain dynamic and are not hashed. A compatible mesh can become inaccurate when those values change. Use the schedule-aware entry point to inspect diagnostics and refresh affected regimes at an outer orchestration boundary. engine.run() and prepared executors return only the trajectory and do not raise on a traced accuracy flag. The prepared cache includes every block mesh; its internal_step_count is total accepted work across blocks. Clear and rebuild prepared objects if system internals change. Gradients are conditional on the frozen meshes, with shared-parameter contributions summed across blocks.

Batching, storage and compilation

Policy Replay behavior
block_schedule: shared Discover a conservative full-state mesh, then vmap that mesh across all blocks. Appropriate when workloads are similar or staging cost dominates.
independent, block_batching: length Group exactly equal total accepted-step counts. Every member carries its own step times, sizes and requested-output slots. Interval meshes need not agree.
independent, block_batching: serial Replay one member per batch as a qualification/reference option.

block_batch_size limits each independent bucket. There are no padded ODE steps between unequal-length members. Chunk checkpointing can add inactive scan slots inside a checkpoint chunk, as in shared replay. replay_checkpoint: step|chunk requires adaptive_replay: compact and trades reverse-mode storage for recomputation; it does not change a mesh.

Discovery currently visits blocks serially using the existing bounded compiled controller. It includes per-block tracing/compilation and host mesh extraction; that setup can cost more than shared discovery. Replay constructs one vmapped scan per batch. Many distinct lengths or a small batch limit increase graph and compilation costs. Shapes, batch membership and mesh contents are static within a prepared executor; changed shapes/meshes require a new plan and compilation. Changing batching without changing numerical controls can reuse the mesh bank.

Mesh metadata and replay-plan constants use storage proportional to the sum of accepted lengths. Discovery needs one block's bounded controller buffer at a time. Primal replay carries current block states, requested outputs and small error summaries; it does not return the full internal-step history. Flattening and restoring block order allocate requested-output arrays. The batch limit bounds a batch's concurrent working state; it does not bound total output, compiled constants or reverse-mode residual storage. Measure those separately.

Reproduce the CPU measurements

From flepimop2-op_engine, after uv sync --dev:

JAX_PLATFORMS=cpu .venv/bin/python -m benchmarks.independent_schedules \
  --blocks 4 16 --workers-per-case 3 --repeats 7 \
  --output /tmp/independent-blocks.jsonl

Each policy runs in a fresh CPU process, pinned to one available logical CPU on Linux. Receipts include source hashes, dependency versions, configuration, per-block work and buckets, all warm timings, model setup, discovery, preparation, compilation and first-call costs. Both discovery and replay must pass the same analytic pointwise gate before a row is marked successful; local replay diagnostics must also pass. The workload has repeated slow-decay blocks and one fast-decay block. It qualifies this bounded workload, without selecting application defaults.

Compiler memory-analysis fields are estimates of argument, result, temporary and aliased bytes for the compiled primal. Process peak RSS includes imports, discovery and compilation; neither quantity is a measured device peak or a reverse-mode memory bound. More block steps saved need not mean a proportional wall-time or memory improvement. Compare the complete lifecycle and the intended number of replays before selecting a policy.

Recorded bounded CPU result

The retained 18 worker receipts and summary pass all common accuracy and replay gates. Measurements use an Intel Core Ultra 7 265U, one logical CPU, float32, JAX 0.12.0.1 and NumPy 2.5.4. Each block has four state cells; setup and dispatch are significant for this cheap RHS.

Blocks Policy Block-steps Discovery (s) Compile (s) Warm replay (µs) Compiler temporary estimate (bytes)
4 shared 212 0.427 0.217 13.3 1048
4 independent-length 62 1.098 0.739 31.5 1496
4 independent-serial 62 1.104 0.666 32.1 2520
16 shared 848 0.546 0.342 43.0 2584
16 independent-length 98 2.737 0.828 46.5 3672
16 independent-serial 98 3.035 2.784 77.9 8664

Independent scheduling saves 3.4×/8.7× accepted block work at 4/16 blocks, but increases discovery and compilation costs and does not improve warm latency or compiler temporary estimates in this workload. Length batching is less costly than serial replay at 16 blocks. These results support retaining the shared default and measuring a consumer’s complete lifecycle before adopting independent meshes; they establish no application speedup.