Skip to main content

compile_plan

Function compile_plan 

Source
pub fn compile_plan(
    graph: &Graph,
    order: &[NodeId],
    bindings_table: &KernelBindingTable,
    options: &PlanOptions<'_>,
) -> Result<ExecutionPlan>
Expand description

Build an ExecutionPlan from a topologically-ordered node sequence + binding table + options.

For each node in order:

  • If op_to_op_kind(&node.op) returns None (view ops, Op::Const, ops not yet wired into dispatch), the node gets no entry — view-only adoption + const adoption flow through the legacy paths unchanged.
  • Op::Copy / Op::Move nodes get no entry either (picker-arc step 4a): their kernel backend is residency-determined (the SOURCE backend), so transfer-kernel resolution stays with the executor’s compile_node path keyed by the bridge-maintained source-backend stamp.
  • Otherwise:
    1. Resolve (op_kind, dtypes, device) from the graph + the options (device via Graph::placement, then PlanOptions::pinned_device, then default_device_for(target_backend) for legacy stamped graphs).
    2. Build the placement list — either via options.placements_for_device (cross-backend mode) or [(target_backend, device)] (legacy single-backend mode).
    3. Enumerate candidates via enumerate_candidates.
    4. Apply the default filter chain (precision floor + strided-input pref + bit-stable pref).
    5. If populate_costs, compute static costs via the capabilities lookup — plus, when a PlanOptions::transfer_estimator is configured, the Stage-2 inbound-transfer term per candidate — and rank ascending by composite cost.
    6. Retain the per-ending-device Pareto frontier + crowding cap (AlternativeSet::retain_per_device_frontier, keep = KEEP_PER_DEVICE).
    7. If the set is empty after filtering, return Error::NoBackendForOp (fail-fast at plan time, not deep in executor dispatch).
    8. Insert into plan.alternatives.

§Stage-2 residency threading

order is topological, so each node’s producers are planned — and their placements committed — before the node itself ranks. The walk maintains a per-node residency view mirroring the bridge’s effective_placements priority: residency-declaring ops (Op::Copy/Move/Alloc targets) → explicit Graph::placement → caller-supplied input residency (PlanOptions::input_residency, consts + persistent cache slots) → the plan’s own committed winner → backend stamp → view-op pass-through inheritance. Inputs whose residency resolves feed the inbound-transfer pricing; unknown residency prices no term (conservative — an unpriceable edge neither justifies nor penalizes a move).

§Stage-3 carry-forward placement DP

Relaxed nodes whose admissible candidates span ≥ 2 devices defer their winner choice to the carry-forward DP (crate::ranker::placement_dp): the walk opens a state row per such node (best[d] = accumulated cost arriving with output on d), extends rows along the dominant chain (view-shaped pass-throughs alias through), merges joins per-device by the cheaper producer state, prices the exit crossing back to the realize target on terminal rows, and backtracks the cheapest terminal state into per-node device commitments. Each committed row’s AlternativeSet is then priced, ranked, and pruned to the committed device — preserving the Stage-2 single-device-set invariant the residency stitch depends on.

Gating: a row only opens when the node is relaxed (estimator + fallback enumerator configured, no explicit placement, not destructive, not already-resident) AND its candidates span more than one device. Single-device plans never open a row and take the Stage-2 greedy path bit-identically — zero overhead beyond an empty-map check per node.

Documented debt (see the placement_dp module docs): fan-out rows chain into their FIRST consumer only (later consumers price that edge as unknown); diamonds re-anchor the second branch as a fresh chain; hard-pinned consumers of an open row close it toward their device at walk time, which prices the crossing but cannot revisit it later.