Skip to main content

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}