fuel_dispatch/ranker/profile_oracle.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! `ProfileJudgeOracle` — the concrete [`JudgeOracle`](super::judge::JudgeOracle)
3//! over the Judge's measured profile data.
4//!
5//! Moved from `fuel-core::judge::oracle` (fuel-core dissolution, plan item
6//! 8): it had zero coupling to `fuel-core`'s `Tensor`/`Device` or to the
7//! rest of `judge/mod.rs`, only to this crate's own `ranker::judge` types
8//! plus `fuel_ir`. The `Judge` struct that PRODUCES a `ProfileReport` stays
9//! in `fuel-tensor::judge` (it builds synthetic `Tensor`s to time kernels);
10//! this adapter only CONSUMES one, so the dependency-cycle concern the old
11//! doc comment described no longer applies to this file.
12//!
13//! # Data source: `ProfileReport`, not `DispatchTable`
14//!
15//! The Judge produces two artifacts:
16//!
17//! - [`ProfileReport`] — every raw measurement: one entry per
18//! `(op, dtype, size_class, backend, device, kernel_source)` cell,
19//! including LOSING alternatives (per-alternative measurement,
20//! commit `1ba99650`).
21//! - `DispatchTable` — winners only, indexed per criterion.
22//!
23//! The cost composer's Layer-2 refinement needs latencies for EVERY
24//! candidate it ranks — a losing-but-close alternative must carry its
25//! own measured number, not inherit the winner's. So the adapter is
26//! built from the report, not the table.
27//!
28//! # Key semantics
29//!
30//! Exact-match on all five axes. `kernel_source` is part of the key:
31//! AOCL / MKL / portable-cpu siblings at the same
32//! `(op, dtype, size_class, BackendId::Cpu)` cell each resolve to
33//! their own latency, and an unmeasured sibling misses (`None`)
34//! rather than borrowing a sibling's number.
35//!
36//! The trait key carries no device axis; when a report holds the same
37//! cell measured on multiple devices (distinct equivalence classes of
38//! one backend), the adapter keeps the MINIMUM latency — "the best
39//! this backend has demonstrated" — which is deterministic regardless
40//! of entry order. Within one equivalence class the replicated
41//! entries share a latency, so the min is a no-op there.
42
43use super::judge::{HashMapJudge, JudgeOracle};
44use fuel_ir::DType;
45use fuel_ir::dispatch::{OpKind, PROFILE_REPORT_VERSION, ProfileReport, SizeClass};
46use fuel_ir::probe::BackendId;
47
48/// [`JudgeOracle`] adapter over a [`ProfileReport`]. Build once per
49/// report (cache lifecycle in `fuel_tensor::judge::cache`, which stays
50/// alongside the `Judge` that produces the report), query per candidate
51/// from the optimizer ranker's cost composer.
52#[derive(Debug, Default, Clone)]
53pub struct ProfileJudgeOracle {
54 inner: HashMapJudge,
55}
56
57impl ProfileJudgeOracle {
58 /// Index every entry of `report` for exact-match lookup.
59 ///
60 /// Defensive version gate: a report whose `version` differs from
61 /// [`PROFILE_REPORT_VERSION`] produces an EMPTY oracle (every
62 /// lookup misses → Layer-1 static costs stand). The persisted-load
63 /// path already filters stale schemas (`ProfileReport::load`
64 /// returns `Ok(None)`), but an in-memory report of a foreign
65 /// schema must not feed the cost composer either — pre-v2 reports
66 /// were ambiguous about which kernel sibling they timed.
67 pub fn from_report(report: &ProfileReport) -> Self {
68 let mut inner = HashMapJudge::new();
69 if report.version != PROFILE_REPORT_VERSION {
70 return Self { inner };
71 }
72 for e in &report.entries {
73 // `device.backend` is a redundant co-encoding of the `backend`
74 // axis; a divergence means a producer stamped a device identity
75 // that disagrees with the backend it measured on. Nothing downstream
76 // re-derives one from the other, so this is the only thing keeping
77 // the two in step — catch a producer bug at its source, in tests.
78 debug_assert_eq!(
79 e.backend, e.device.backend,
80 "ProfileEntry backend ({:?}) and device.backend ({:?}) disagree",
81 e.backend, e.device.backend
82 );
83 let prev = inner.measured_latency_ns(
84 e.op,
85 e.dtype,
86 e.size_class,
87 e.backend,
88 &e.kernel_source,
89 e.kernel_revision_hash,
90 );
91 // Keep the minimum across GENUINE duplicate keys — multi-device
92 // entries at ONE `(op, dtype, size_class, backend, kernel_source,
93 // kernel_revision_hash)` cell (see module docs). Distinct
94 // revisions are distinct keys, so variant siblings no longer
95 // collapse here: they land in separate cells and each keeps its
96 // own measured latency.
97 if prev.is_none_or(|p| e.latency_ns < p) {
98 inner.insert(
99 e.op,
100 e.dtype,
101 e.size_class,
102 e.backend,
103 &e.kernel_source,
104 e.latency_ns,
105 e.kernel_revision_hash,
106 );
107 }
108 }
109 Self { inner }
110 }
111
112 /// Number of distinct `(op, dtype, size_class, backend,
113 /// kernel_source)` cells indexed.
114 pub fn len(&self) -> usize {
115 self.inner.len()
116 }
117
118 /// Is the oracle empty (no measurements — every lookup misses)?
119 pub fn is_empty(&self) -> bool {
120 self.inner.is_empty()
121 }
122}
123
124impl JudgeOracle for ProfileJudgeOracle {
125 fn measured_latency_ns(
126 &self,
127 op: OpKind,
128 dtype: DType,
129 size_class: SizeClass,
130 backend: BackendId,
131 kernel_source: &str,
132 kernel_revision_hash: u64,
133 ) -> Option<u64> {
134 self.inner.measured_latency_ns(
135 op,
136 dtype,
137 size_class,
138 backend,
139 kernel_source,
140 kernel_revision_hash,
141 )
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use fuel_ir::dispatch::ProfileEntry;
149
150 /// Minimal equivalence key for a fixture entry — fixtures never load
151 /// through the hardware gate, so the vendor/device ids are
152 /// irrelevant, only the backend. Mirrors
153 /// `fuel_tensor::judge::test_equiv_key`, which stays `pub(crate)` there
154 /// and so can't be reused across the crate boundary.
155 fn test_equiv_key(backend: BackendId) -> fuel_ir::probe::EquivalenceKey {
156 fuel_ir::probe::EquivalenceKey {
157 backend,
158 vendor_id: 0,
159 device_id: 0,
160 compute_capability: None,
161 driver_version: String::new(),
162 }
163 }
164
165 fn entry(
166 backend: BackendId,
167 op: OpKind,
168 size: u32,
169 device_index: u32,
170 latency: u64,
171 kernel_source: &str,
172 ) -> ProfileEntry {
173 ProfileEntry {
174 op,
175 dtype: DType::F32,
176 size_class: SizeClass(size),
177 backend,
178 device_index,
179 latency_ns: latency,
180 iterations: 7,
181 max_rel_error: 1e-6,
182 kernel_source: kernel_source.to_string(),
183 kernel_revision_hash: 0,
184 device: test_equiv_key(backend),
185 }
186 }
187
188 #[test]
189 fn empty_report_misses_everywhere() {
190 let report = ProfileReport {
191 version: PROFILE_REPORT_VERSION,
192 entries: vec![],
193 };
194 let oracle = ProfileJudgeOracle::from_report(&report);
195 assert!(oracle.is_empty());
196 assert!(
197 oracle
198 .measured_latency_ns(
199 OpKind::MatMul,
200 DType::F32,
201 SizeClass(16),
202 BackendId::Cpu,
203 "",
204 0,
205 )
206 .is_none()
207 );
208 }
209
210 /// Two entries at the same `(op, dtype, size_class, backend)`
211 /// cell differing ONLY in `kernel_source` resolve to distinct
212 /// latencies — the sibling-collision bug class the per-alt
213 /// remediation arc fixed must stay fixed here.
214 #[test]
215 fn sibling_kernel_sources_do_not_collide() {
216 let report = ProfileReport {
217 version: PROFILE_REPORT_VERSION,
218 entries: vec![
219 entry(
220 BackendId::Cpu,
221 OpKind::MatMul,
222 16,
223 0,
224 1_000_000,
225 "portable-cpu",
226 ),
227 entry(BackendId::Cpu, OpKind::MatMul, 16, 0, 200_000, "aocl"),
228 ],
229 };
230 let oracle = ProfileJudgeOracle::from_report(&report);
231 assert_eq!(oracle.len(), 2);
232 let cell = |src: &str| {
233 oracle.measured_latency_ns(
234 OpKind::MatMul,
235 DType::F32,
236 SizeClass(16),
237 BackendId::Cpu,
238 src,
239 0,
240 )
241 };
242 assert_eq!(cell("portable-cpu"), Some(1_000_000));
243 assert_eq!(cell("aocl"), Some(200_000));
244 // An unmeasured sibling misses — no fallthrough to either
245 // measured sibling.
246 assert!(cell("mkl").is_none());
247 assert!(cell("").is_none());
248 }
249
250 /// Same key measured on two devices (distinct equivalence
251 /// classes of one backend) keeps the minimum, independent of
252 /// entry order.
253 #[test]
254 fn duplicate_keys_keep_minimum_latency() {
255 let report = ProfileReport {
256 version: PROFILE_REPORT_VERSION,
257 entries: vec![
258 entry(BackendId::Cuda, OpKind::MatMul, 20, 0, 300_000, "cublas"),
259 entry(BackendId::Cuda, OpKind::MatMul, 20, 1, 900_000, "cublas"),
260 ],
261 };
262 let oracle = ProfileJudgeOracle::from_report(&report);
263 assert_eq!(oracle.len(), 1);
264 assert_eq!(
265 oracle.measured_latency_ns(
266 OpKind::MatMul,
267 DType::F32,
268 SizeClass(20),
269 BackendId::Cuda,
270 "cublas",
271 0,
272 ),
273 Some(300_000),
274 );
275
276 // Reverse the entry order — same answer.
277 let reversed = ProfileReport {
278 version: PROFILE_REPORT_VERSION,
279 entries: vec![
280 entry(BackendId::Cuda, OpKind::MatMul, 20, 1, 900_000, "cublas"),
281 entry(BackendId::Cuda, OpKind::MatMul, 20, 0, 300_000, "cublas"),
282 ],
283 };
284 let oracle = ProfileJudgeOracle::from_report(&reversed);
285 assert_eq!(
286 oracle.measured_latency_ns(
287 OpKind::MatMul,
288 DType::F32,
289 SizeClass(20),
290 BackendId::Cuda,
291 "cublas",
292 0,
293 ),
294 Some(300_000),
295 );
296 }
297
298 /// Misses stay misses on every non-matching axis: backend,
299 /// dtype, size_class, op.
300 #[test]
301 fn miss_on_any_axis_returns_none() {
302 let report = ProfileReport {
303 version: PROFILE_REPORT_VERSION,
304 entries: vec![entry(BackendId::Cpu, OpKind::MatMul, 16, 0, 1_000, "")],
305 };
306 let oracle = ProfileJudgeOracle::from_report(&report);
307 let hit = oracle.measured_latency_ns(
308 OpKind::MatMul,
309 DType::F32,
310 SizeClass(16),
311 BackendId::Cpu,
312 "",
313 0,
314 );
315 assert_eq!(hit, Some(1_000));
316 assert!(
317 oracle
318 .measured_latency_ns(
319 OpKind::MatMul,
320 DType::F32,
321 SizeClass(16),
322 BackendId::Cuda,
323 "",
324 0,
325 )
326 .is_none()
327 );
328 assert!(
329 oracle
330 .measured_latency_ns(
331 OpKind::MatMul,
332 DType::F64,
333 SizeClass(16),
334 BackendId::Cpu,
335 "",
336 0,
337 )
338 .is_none()
339 );
340 assert!(
341 oracle
342 .measured_latency_ns(
343 OpKind::MatMul,
344 DType::F32,
345 SizeClass(17),
346 BackendId::Cpu,
347 "",
348 0,
349 )
350 .is_none()
351 );
352 assert!(
353 oracle
354 .measured_latency_ns(
355 OpKind::AddElementwise,
356 DType::F32,
357 SizeClass(16),
358 BackendId::Cpu,
359 "",
360 0,
361 )
362 .is_none()
363 );
364 }
365
366 /// A report with a foreign schema version produces an empty
367 /// oracle — Layer-1 static costs stand.
368 #[test]
369 fn version_mismatch_yields_empty_oracle() {
370 let report = ProfileReport {
371 version: PROFILE_REPORT_VERSION + 1,
372 entries: vec![entry(BackendId::Cpu, OpKind::MatMul, 16, 0, 1_000, "")],
373 };
374 let oracle = ProfileJudgeOracle::from_report(&report);
375 assert!(oracle.is_empty());
376 }
377
378 /// Type-level proof of the production plumb: the adapter slots
379 /// into `PlanOptions::with_judge` as a `&dyn JudgeOracle`.
380 #[test]
381 fn adapter_attaches_to_plan_options() {
382 let report = ProfileReport {
383 version: PROFILE_REPORT_VERSION,
384 entries: vec![entry(BackendId::Cpu, OpKind::MatMul, 16, 0, 1_000, "aocl")],
385 };
386 let oracle = ProfileJudgeOracle::from_report(&report);
387 let opts = crate::plan::PlanOptions::new().with_judge(&oracle);
388 let queried = opts
389 .judge
390 .expect("with_judge sets the oracle")
391 .measured_latency_ns(
392 OpKind::MatMul,
393 DType::F32,
394 SizeClass(16),
395 BackendId::Cpu,
396 "aocl",
397 0,
398 );
399 assert_eq!(queried, Some(1_000));
400 }
401
402 /// Two entries at the SAME `(op, dtype, size, backend, kernel_source)`
403 /// cell differing ONLY in `kernel_revision_hash` — both NON-ZERO — occupy
404 /// TWO distinct oracle cells via the REAL cache path (`from_report`).
405 /// This is the variant-sibling collapse `kernel_revision_hash` exists to
406 /// prevent: forty tilings of one provider share the `kernel_source` tag
407 /// (`"baracuda"`), so without the revision in the key they collapse into
408 /// one cell and thirty-nine become invisible with no error. Both revisions
409 /// are nonzero deliberately — a `0`-vs-nonzero test would pass while real
410 /// siblings still collapsed. Born-red: before the revision joined the key
411 /// this produced `len() == 1` and both lookups returned the min latency.
412 #[test]
413 fn sibling_kernel_revisions_do_not_collide_via_report() {
414 let mk = |rev: u64, latency: u64| ProfileEntry {
415 op: OpKind::MatMul,
416 dtype: DType::F32,
417 size_class: SizeClass(16),
418 backend: BackendId::Cuda,
419 device_index: 0,
420 latency_ns: latency,
421 iterations: 7,
422 max_rel_error: 1e-6,
423 kernel_source: "baracuda".to_string(),
424 kernel_revision_hash: rev,
425 device: test_equiv_key(BackendId::Cuda),
426 };
427 let report = ProfileReport {
428 version: PROFILE_REPORT_VERSION,
429 entries: vec![
430 mk(0x1111_1111_1111_1111, 5_000),
431 mk(0x2222_2222_2222_2222, 9_000),
432 ],
433 };
434 let oracle = ProfileJudgeOracle::from_report(&report);
435 assert_eq!(
436 oracle.len(),
437 2,
438 "distinct non-zero revisions are distinct cells"
439 );
440 let cell = |rev: u64| {
441 oracle.measured_latency_ns(
442 OpKind::MatMul,
443 DType::F32,
444 SizeClass(16),
445 BackendId::Cuda,
446 "baracuda",
447 rev,
448 )
449 };
450 assert_eq!(cell(0x1111_1111_1111_1111), Some(5_000));
451 assert_eq!(cell(0x2222_2222_2222_2222), Some(9_000));
452 // An untracked (0) revision at the same tag is its OWN cell — no
453 // fallthrough to either measured sibling.
454 assert!(cell(0).is_none());
455 }
456}