fuel_dispatch/ranker/candidate.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! `Candidate` — one (kernel, placement, precision, cost) bundle that
3//! the optimizer ranker is considering at a graph decision point.
4//!
5//! Phase 1.1 of the picker-work arc. A candidate is what the
6//! ranker filters and ranks. Today's
7//! [`BindingEntry`](crate::kernel::BindingEntry) is the
8//! per-(op, dtypes, backend)-key cousin; a `Candidate` is the
9//! per-decision-point view that adds placement (`BackendId` ×
10//! `DeviceLocation`), the `OpParams` the kernel will see at runtime,
11//! and (in later phases) coupling info that says "this candidate's
12//! cost depends on what alternative wins at adjacent decision points."
13//!
14//! Phase 1.1 ships the shape with `coupling` as an empty `Vec`.
15//! Phase 2 populates it when transfer-op insertion lands.
16
17use fuel_ir::DeviceLocation;
18use fuel_ir::probe::BackendId;
19
20use crate::fused::{CostEstimate, PrecisionGuarantee};
21use crate::kernel::{KernelCaps, KernelRef, OpParams};
22
23/// One concrete dispatch alternative the ranker is considering for a
24/// graph decision point. Pre-resolved at plan time — by the time a
25/// `Candidate` exists, the binding-table lookup has happened and the
26/// `KernelRef` is in hand.
27///
28/// All fields are explicit on purpose: every successor phase (cost
29/// composition, executor consumption, runtime selection) reads from
30/// these fields by name, and adding a field is preferable to adding
31/// a derived getter.
32///
33/// # Where it comes from
34///
35/// Phase 1.2 (`compile_plan` successor) builds candidates by walking
36/// [`crate::topology::SystemTopology`]-equivalent metadata: for each
37/// `DeviceLocation` the graph node may run on, enumerate co-located
38/// `BackendId`s, then for each `(op_kind, dtypes, backend)` triple
39/// pull every entry from [`crate::kernel::KernelBindingTable::lookup_alternatives`].
40/// Each entry contributes one `Candidate`.
41#[derive(Clone, Debug)]
42pub struct Candidate {
43 /// Pre-resolved kernel function pointer.
44 pub kernel: KernelRef,
45 /// Capability flags the executor reads to decide auto-Contiguize.
46 pub caps: KernelCaps,
47 /// Which backend this candidate runs on.
48 pub backend: BackendId,
49 /// Specific device within `backend`.
50 pub device: DeviceLocation,
51 /// What this kernel claims about its numerical behavior. Hard
52 /// filters consume this to enforce user precision floors and
53 /// tolerance budgets.
54 pub precision: PrecisionGuarantee,
55 /// Layer-1 static cost estimate (FLOPs + bandwidth + launch
56 /// overhead) computed against the decision point's input shapes.
57 /// Phase 1.4's cost composition reads this; Phase 3's Judge
58 /// integration refines it with Layer-2 measurements.
59 pub static_cost: CostEstimate,
60 /// Planner Stage-2 inbound-transfer term: estimated nanoseconds
61 /// to move this decision point's inputs from their committed
62 /// residencies onto `device`, summed over every input whose
63 /// residency differs from `device`. Populated by
64 /// [`super::cost::apply_inbound_transfer_costs`] when a
65 /// `TransferEstimator` is threaded through `PlanOptions`; zero
66 /// when all inputs are co-resident, residency is unknown, or no
67 /// estimator is configured. Kept separate from `static_cost` so
68 /// Layer-2 Judge refinement (which REPLACES the kernel-time
69 /// estimate) never clobbers the transfer term — ranking adds the
70 /// two (`composite_ns(static_cost) + inbound_transfer_ns`).
71 pub inbound_transfer_ns: u64,
72 /// The op-specific parameter payload that will accompany this
73 /// kernel call. Cloned here so the candidate is self-contained
74 /// for plan-time reasoning. Most ops carry `OpParams::None`.
75 pub op_params: OpParams,
76 /// Cost adjustments contingent on adjacent decision points'
77 /// resolutions — e.g. "this CUDA candidate's cost is +2ms if the
78 /// downstream join falls back to CPU." Empty in Phase 1.1; Phase
79 /// 2 populates it when transfer-op cost coupling lands. See
80 /// architecture v1.0 §04 "Coupling between decisions" for the
81 /// long-term shape.
82 pub coupling: Vec<CouplingAdjustment>,
83 /// Diagnostic tag identifying the kernel's implementation source
84 /// (e.g. `"portable-cpu"`, `"aocl"`, `"mkl"`, `"cublas"`).
85 /// Copied from the `BindingEntry::kernel_source` of the binding
86 /// this candidate was enumerated from. Never used for dispatch —
87 /// the picker distinguishes candidates by `kernel` (function-
88 /// pointer identity). Surfaces in Judge telemetry and selector
89 /// diagnostics so logs can name which kernel won at a decision
90 /// point even when several share `(backend, device)`.
91 pub kernel_source: &'static str,
92 /// Content-derived revision of this candidate's kernel build, copied
93 /// from the same `BindingEntry::kernel_revision_hash` as
94 /// `kernel_source`. Together they form the variant-granular identity
95 /// the Judge oracle keys on, so the cost composer's
96 /// `measured_latency_ns` lookup hits the cell measured for THIS
97 /// build rather than a sibling that shares the `kernel_source` tag.
98 /// `0` for synthetic candidates and untracked kernels.
99 pub kernel_revision_hash: u64,
100}
101
102/// One conditional cost contribution attached to a [`Candidate`].
103/// Phase 1.1 ships the struct shape with an inert default; Phase 2's
104/// transfer-op insertion is the first session that constructs real
105/// instances.
106///
107/// The conditional is intentionally minimal — `condition` describes
108/// the predicate ("downstream decision point N picks a non-co-located
109/// backend") and `delta_cost` is the additive correction. Phase 2
110/// will likely grow richer predicates (e.g. specific adjacent-node
111/// alternative identity); the placeholder is good enough to let the
112/// surrounding types stabilize.
113#[derive(Clone, Debug, Default, PartialEq, Eq)]
114pub struct CouplingAdjustment {
115 /// Opaque tag describing what conditions this adjustment fires
116 /// under. Phase 2 will replace this with a richer predicate type;
117 /// the current `String` is a stand-in to keep Phase 1.1 from
118 /// over-committing on a representation that Phase 2's needs will
119 /// shape.
120 pub condition: String,
121 /// Additive nanosecond correction to the candidate's static cost
122 /// when `condition` holds.
123 pub delta_ns: i64,
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use fuel_ir::Layout;
130 use fuel_ir::Result;
131 use std::sync::{Arc, RwLock};
132
133 use fuel_memory::Storage;
134
135 fn noop_kernel(
136 _inputs: &[Arc<RwLock<Storage>>],
137 _outputs: &mut [Arc<RwLock<Storage>>],
138 _layouts: &[Layout],
139 _params: &OpParams,
140 ) -> Result<()> {
141 Ok(())
142 }
143
144 /// Smoke: candidate construction populates every required field.
145 #[test]
146 fn candidate_construction() {
147 let c = Candidate {
148 kernel: noop_kernel,
149 caps: KernelCaps::empty(),
150 backend: BackendId::Cpu,
151 device: DeviceLocation::Cpu,
152 precision: PrecisionGuarantee::PRIMITIVE_DETERMINISTIC_CPU,
153 static_cost: CostEstimate {
154 flops: 100,
155 bytes_moved: 400,
156 kernel_overhead_ns: 50,
157 },
158 inbound_transfer_ns: 0,
159 op_params: OpParams::None,
160 coupling: Vec::new(),
161 kernel_source: "",
162 kernel_revision_hash: 0,
163 };
164 assert_eq!(c.backend, BackendId::Cpu);
165 assert_eq!(c.static_cost.flops, 100);
166 assert!(c.coupling.is_empty());
167 }
168
169 /// Smoke: `CouplingAdjustment::default` is inert (empty condition,
170 /// zero delta). Phase 2 populates real instances.
171 #[test]
172 fn coupling_adjustment_default_is_inert() {
173 let a = CouplingAdjustment::default();
174 assert!(a.condition.is_empty());
175 assert_eq!(a.delta_ns, 0);
176 }
177}