Skip to main content

PlanOptions

Struct PlanOptions 

Source
pub struct PlanOptions<'env> {
    pub precision_requirement: PrecisionRequirement,
    pub populate_costs: bool,
    pub placements_for_device: Option<&'env (dyn Fn(DeviceLocation) -> Vec<BackendId> + 'env)>,
    pub pinned_device: Option<DeviceLocation>,
    pub fallback_placements_for: Option<&'env (dyn Fn(DeviceLocation) -> Vec<(BackendId, DeviceLocation)> + 'env)>,
    pub capabilities_for: Option<&'env CapabilitiesLookup<'env>>,
    pub judge: Option<&'env dyn JudgeOracle>,
    pub transfer_estimator: Option<&'env dyn TransferEstimator>,
    pub require_transfer_pricing: bool,
    pub input_residency: Option<&'env (dyn Fn(NodeId) -> Option<DeviceLocation> + 'env)>,
    pub reuse_plan: Option<&'env ExecutionPlan>,
    pub allow_cost_placement: bool,
}
Expand description

Plan-time configuration the caller hands to compile_plan.

Most fields have sensible defaults via PlanOptions::new + chained builders. The two callback fields are required for non-trivial usage — the 'env lifetime ties them to the caller’s environment.

§placements_for_device

When Some, called per kernel-bearing node with the node’s target DeviceLocation and expected to return every BackendId the picker is allowed to consider at that device. This is the cross-co-located-backend integration point — callers wire it to SystemTopology::backends_for(device) to unlock the AOCL/MKL/CPU competition story.

When None, the planner uses single-backend placement: only (target_backend, placement) from the graph node’s side-table. Matches the pre-1.5 picker’s pinned-backend behavior; useful as a transitional default and for tests.

§pinned_device

Picker-arc step 4a. When Some, nodes WITHOUT an explicit graph placement resolve their decision device to this location — the realize call’s pinned DEVICE. This replaces the bridge’s pre-plan monolithic per-node set_target_backend loop: the graph no longer needs target_backend stamped before planning; the plan’s per-node winner is stamped back onto the graph AFTER planning (fuel-tensor::pipelined_bridge::stamp_plan_backends).

Resolution priority per node: explicit Graph::placement (the scheduler’s per-node assignment) → pinned_device → default_device_for(target_backend) (legacy stamped graphs) → error.

§fallback_placements_for

Picker-arc step 4b, relaxed by planner Stage 2. The closure supplies the OFF-DEVICE placements the picker may consider beyond the node’s decision device. Two admission regimes:

  • Priced (Stage 2) — when a Self::transfer_estimator is configured (with populate_costs + a capabilities lookup), off-device placements ALWAYS enumerate alongside the decision device’s, and every candidate carries an inbound-transfer term; locality emerges from the pricing (ties break toward the decision device via the rank’s stable sort). After the rank the set is pruned to the winner’s device. Nodes with an explicit Graph::placement are HARD pins — they stay on the legacy regime below.
  • Legacy (unpriced) — without an estimator, the closure is consulted only when the decision device can’t deliver: empty enumeration for the (op, dtypes), or (Stage-2 fix) a hard filter chain that rejects every decision-device registration.

Constraints enforced by compile_plan in both regimes:

  • Destructive ops never fall back (Op::destructive_input() is Some): in-place mutation semantics don’t survive moving the op away from the device that owns its mutation target; those ops keep the plan-time NoBackendForOp error.
  • Each NODE’s surviving alternative set lives on one device (legacy fallback sets freeze to their single ranked winner; relaxed sets prune to that node’s winner’s device): the residency stitch (Op::Copy insertion) is a graph rewrite computed from the static winner, so a dispatch-time selector (the Op::Branch arm picker) must not be able to swap in a sibling on a different device whose inputs were never copied there. This is a per-node invariant on the candidate set — NOT a graph-global “everything realizes on one device” claim. Step E A4a-1 confirmed (CireSnave 2026-06-29) device placement is per-node: the surviving graph is almost never single-device, and even a path between decision points may span devices. The planner already honors that — each node’s decision device is Graph::placement → pinned_device → stamp-default (independently per node), stamp_plan_backends writes each node’s own winner backend, and insert_residency_copies bridges every resulting cross-device boundary edge. A single-device plan is just the degenerate case where every node’s per-node device happens to coincide; this prune is a no-op there, so single-device realize stays byte-identical.

§capabilities_for

Required when populate_costs is true (the default). The closure resolves a BackendId to its BackendCapabilities — callers typically wire it to SystemTopology::capabilities(...) or the CapabilityRegistry. Returns None for backends not in the topology (their candidates retain default zero cost).

Fields§

§precision_requirement: PrecisionRequirement

Hard precision floor the picker enforces. Default: empty (unconstrained — every candidate passes the precision filter).

§populate_costs: bool

Whether to invoke compute_static_costs + rank after filtering. Default: true. Disable for tests that just want to verify enumeration + filtering without cost machinery.

§placements_for_device: Option<&'env (dyn Fn(DeviceLocation) -> Vec<BackendId> + 'env)>

Cross-co-located-backend placement enumerator. See struct docs.

§pinned_device: Option<DeviceLocation>

Realize-call device pin (picker-arc step 4a). See struct docs.

§fallback_placements_for: Option<&'env (dyn Fn(DeviceLocation) -> Vec<(BackendId, DeviceLocation)> + 'env)>

Off-device fallback enumerator for missing-impl ops (picker-arc step 4b). See struct docs.

§capabilities_for: Option<&'env CapabilitiesLookup<'env>>

Capabilities lookup. Required when populate_costs is true.

§judge: Option<&'env dyn JudgeOracle>

Optional Phase 3 Layer-2 oracle. When Some AND populate_costs is true, the cost composer refines each candidate’s Layer-1 estimate with the Judge’s measured latency at that cell. Candidates the oracle hasn’t measured keep the Layer-1 estimate (silent fallback — absence ≠ zero).

§transfer_estimator: Option<&'env dyn TransferEstimator>

Planner Stage-2 transfer-cost oracle. When Some AND populate_costs is true (with a capabilities lookup), the cost composer adds a per-candidate inbound-transfer term: the sum over the node’s inputs of estimate_transfer_ns(input residency, candidate device, input bytes), for every input whose residency is known at plan time. Producer residencies are committed along the topo walk (producers rank before consumers); graph-input residencies come from Self::input_residency.

Also the gate for the Stage-2 admission relax: with pricing active, off-device candidates ALWAYS enumerate (priced) for nodes without an explicit Graph::placement — locality emerges from the numbers, not from the legacy missing-impl-only fallback gate.

Production callers wire this to SystemTopology::estimate_transfer_ns (Stage-1 TransferCalibration behind it); unit tests use synthetic estimators — never live calibration.

§require_transfer_pricing: bool

Guard against silently planning cost-based placement WITHOUT transfer pricing. When true, compile_plan fails fast with an error if Self::transfer_estimator is None — i.e. the caller has declared “I intend to price transfer; if the estimator isn’t wired, that’s a bug, not a silent revert to latency-greedy.”

Default false so the many unit tests that legitimately plan unpriced (see compile_plan_without_estimator_prices_nothing) are unaffected. The PRODUCTION realize entry (fuel-core’s build_optimized_graph) sets this whenever cost-based cross-device placement is active, so a future edit that drops the .with_transfer_estimator(...) wiring — or a new production planner that forgets it — fails loudly at plan-build time instead of quietly pricing every transfer at zero. Opt-in rather than default-on because “cost placement, unpriced” is a supported mode for callers that ask for it; production simply never does.

§input_residency: Option<&'env (dyn Fn(NodeId) -> Option<DeviceLocation> + 'env)>

Residency resolver for graph INPUTS whose bytes already exist outside the plan — Op::Const storages uploaded by the const cache and persistent initial slots (InferenceContext). Consulted while threading residency through the plan walk, with the same priority the bridge’s effective_placements gives cached storages: after residency-declaring ops and explicit placements, before the plan’s own winner. None for a node means “not a resident input” — its residency resolves through the remaining rules (or stays unknown, in which case no transfer term fires on its edges).

§reuse_plan: Option<&'env ExecutionPlan>

Planner Stage 4: incremental extension over a previously compiled plan. Nodes already covered by reuse_plan (an AlternativeSet exists for them) skip enumeration / filter / cost / DP entirely — their stored set is cloned into the new plan and their residency commits as the stored winner’s device (mirroring the priority-4 “plan winner” rule), so the delta’s pricing sees the same residency picture the original walk committed.

The caller (the plan store) is responsible for only reusing plans built under the SAME topology generation and pinned device — compile_plan does not re-validate the reused sets. Reused candidates keep their original inbound_transfer_ns terms; a residency shift between builds is repriced only for delta nodes (acceptable drift — placement correctness is re-stitched per realize by the bridge’s residency passes).

§allow_cost_placement: bool

Gate for the Stage-2 relaxed cross-device placement (cost-based off-device enumeration). Default: true — a node without an explicit Graph::placement may be priced onto (and moved to) a cheaper sibling device, the “plan IS the graph” auto-placement.

Set false to keep every node on its pinned/decision device: the relaxed branch is suppressed, so no off-device candidate is enumerated on cost grounds.

This is what a reference / single-device realize wants. A CPU-pinned oracle must genuinely run on CPU — never cost-relocated to the very backend it validates — and by the always-built coverage commitment (fuel-cpu-backend ships a bit_stable_on_same_hardware kernel for every primitive op) the CPU decision device can always supply a kernel, so suppressing off-device placement never strands a node. A single-op parity probe pinned to backend B likewise stays on B regardless of op size. Explicit Graph::placement is a hard pin either way; this flag governs only the un-pinned nodes.

Implementations§

Source§

impl<'env> PlanOptions<'env>

Source

pub fn new() -> Self

Builder constructor; same fields as Default.

Source

pub fn with_precision_requirement(self, req: PrecisionRequirement) -> Self

Set the precision requirement hard filter.

Source

pub fn without_cost_population(self) -> Self

Disable cost computation + ranking. Sets populate_costs = false; alternatives end up in enumerator order, useful for tests that don’t care about ranking.

Source

pub fn with_placements_for_device( self, f: &'env (dyn Fn(DeviceLocation) -> Vec<BackendId> + 'env), ) -> Self

Attach a co-located-backend enumerator. Wire to SystemTopology::backends_for(device) for production.

Source

pub fn with_pinned_device(self, device: DeviceLocation) -> Self

Pin the realize call’s device. Nodes without an explicit graph placement enumerate their candidates at this location — no per-node target_backend stamp needed before planning. Picker-arc step 4a.

Source

pub fn without_cost_placement(self) -> Self

Suppress the Stage-2 relaxed cross-device placement (see Self::allow_cost_placement): every un-pinned node stays on its decision device; only the missing-impl fallback may still go off-device. Use for a reference / single-device realize.

Source

pub fn require_transfer_pricing(self) -> Self

Declare that this plan MUST price transfer: compile_plan errors if no Self::transfer_estimator is set. Production realize wires this whenever cost-based cross-device placement is active, so a dropped or forgotten .with_transfer_estimator(...) fails loudly at plan-build time rather than silently reverting to latency-greedy placement (transfers priced at zero). See Self::require_transfer_pricing.

Source

pub fn with_fallback_placements_for( self, f: &'env (dyn Fn(DeviceLocation) -> Vec<(BackendId, DeviceLocation)> + 'env), ) -> Self

Attach an off-device fallback enumerator. With a Stage-2 transfer estimator configured, off-device placements always enumerate, priced; without one, the closure is consulted only when the decision device can’t deliver (no impl, or every impl hard-filtered). Wire to the SystemTopology device list for production. Picker-arc step 4b + planner Stage 2 — see struct docs for the destructive-op, hard-pin, and single-device constraints.

Source

pub fn with_capabilities_for(self, f: &'env CapabilitiesLookup<'env>) -> Self

Attach a capabilities lookup. Required when populate_costs is true.

Source

pub fn with_judge(self, judge: &'env dyn JudgeOracle) -> Self

Attach a Layer-2 Judge oracle. When set, compile_plan’s cost composer refines Layer-1 estimates with measured latencies per JudgeOracle::measured_latency_ns.

Source

pub fn with_transfer_estimator(self, est: &'env dyn TransferEstimator) -> Self

Attach a Stage-2 transfer-cost oracle. See the Self::transfer_estimator field docs — enables inbound- transfer pricing AND the priced off-device admission relax. Production wires SystemTopology::estimate_transfer_ns; tests use synthetic estimators.

Source

pub fn with_input_residency( self, f: &'env (dyn Fn(NodeId) -> Option<DeviceLocation> + 'env), ) -> Self

Attach a residency resolver for already-resident graph inputs (consts + persistent cache slots). See the Self::input_residency field docs.

Source

pub fn with_reuse_plan(self, base: &'env ExecutionPlan) -> Self

Attach a base plan for incremental extension (planner Stage 4). See the Self::reuse_plan field docs.

Trait Implementations§

Source§

impl Default for PlanOptions<'_>

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

§

impl<'env> !RefUnwindSafe for PlanOptions<'env>

§

impl<'env> !Send for PlanOptions<'env>

§

impl<'env> !Sync for PlanOptions<'env>

§

impl<'env> !UnwindSafe for PlanOptions<'env>

§

impl<'env> Freeze for PlanOptions<'env>

§

impl<'env> Unpin for PlanOptions<'env>

§

impl<'env> UnsafeUnpin for PlanOptions<'env>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more