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_estimatoris configured (withpopulate_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 explicitGraph::placementare 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()isSome): in-place mutation semantics don’t survive moving the op away from the device that owns its mutation target; those ops keep the plan-timeNoBackendForOperror. - 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::Copyinsertion) is a graph rewrite computed from the static winner, so a dispatch-time selector (theOp::Brancharm 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 isGraph::placement→pinned_device→ stamp-default (independently per node),stamp_plan_backendswrites each node’s own winner backend, andinsert_residency_copiesbridges 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: PrecisionRequirementHard precision floor the picker enforces. Default: empty (unconstrained — every candidate passes the precision filter).
populate_costs: boolWhether 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: boolGuard 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: boolGate 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>
impl<'env> PlanOptions<'env>
Sourcepub fn with_precision_requirement(self, req: PrecisionRequirement) -> Self
pub fn with_precision_requirement(self, req: PrecisionRequirement) -> Self
Set the precision requirement hard filter.
Sourcepub fn without_cost_population(self) -> Self
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.
Sourcepub fn with_placements_for_device(
self,
f: &'env (dyn Fn(DeviceLocation) -> Vec<BackendId> + 'env),
) -> Self
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.
Sourcepub fn with_pinned_device(self, device: DeviceLocation) -> Self
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.
Sourcepub fn without_cost_placement(self) -> Self
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.
Sourcepub fn require_transfer_pricing(self) -> Self
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.
Sourcepub fn with_fallback_placements_for(
self,
f: &'env (dyn Fn(DeviceLocation) -> Vec<(BackendId, DeviceLocation)> + 'env),
) -> Self
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.
Sourcepub fn with_capabilities_for(self, f: &'env CapabilitiesLookup<'env>) -> Self
pub fn with_capabilities_for(self, f: &'env CapabilitiesLookup<'env>) -> Self
Attach a capabilities lookup. Required when populate_costs
is true.
Sourcepub fn with_judge(self, judge: &'env dyn JudgeOracle) -> Self
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.
Sourcepub fn with_transfer_estimator(self, est: &'env dyn TransferEstimator) -> Self
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.
Sourcepub fn with_input_residency(
self,
f: &'env (dyn Fn(NodeId) -> Option<DeviceLocation> + 'env),
) -> Self
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.
Sourcepub fn with_reuse_plan(self, base: &'env ExecutionPlan) -> Self
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§
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> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
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