fuel_ir/backend.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Backend capability **data** types.
3//!
4//! After step 8 of the backend-agnostic refactor (2026-04-30), the static
5//! `BackendStorage`/`BackendDevice` generic traits were deleted; every backend
6//! implements `DynBackendStorage`/`DynBackendDevice` directly. As of B0.3 the
7//! object-safe contract *traits* (`HostStorage`, `BackendStorage`,
8//! `BackendCapabilityProvider`, `BackendRuntime`) moved to the
9//! `fuel-backend-contract` crate (it sits above this vocabulary crate, below the
10//! backends). What remains here is the **data** they traffic in: the capability
11//! advertisement ([`BackendCapabilities`] / [`SubstrateClass`] / [`TransferPath`])
12//! and the predictive [`FitStatus`].
13use crate::{DType, DeviceLocation, dispatch::OpKind, probe::BackendId};
14use std::collections::HashSet;
15
16// =============================================================================
17// Phase 7.5 A4 — capability advertisement
18// =============================================================================
19
20/// Classifies the underlying allocator / pointer namespace a backend's
21/// storage lives in. Two backends share a substrate class **iff their
22/// storage variants are byte-compatible on the same device** — calling
23/// one backend's kernel after the other's is a vtable swap, not a data
24/// copy.
25///
26/// SystemTopology consumes this for its `shares_storage` predicate
27/// (Phase 7.6 system-topology session, 2026-05-30). When both backends
28/// declare the same `SubstrateClass` *and* target the same
29/// [`DeviceLocation`], they can freely interleave on the same `Storage`
30/// handle without an Op::Copy.
31///
32/// Forward-extension: a new backend that introduces a new pointer
33/// namespace adds a new variant. The enum is `#[non_exhaustive]` so
34/// downstream pattern matches stay forward-compatible.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36#[non_exhaustive]
37pub enum SubstrateClass {
38 /// Host-addressable bytes — the CPU trio (Cpu / Aocl / Mkl) plus
39 /// `Reference` all share this. A storage allocated as
40 /// `CpuStorageBytes` flows through any of them without a copy.
41 HostBytes,
42 /// CUDA untyped device buffer. Two CUDA backends on the same
43 /// `gpu_id` share; CUDA on `gpu_id=0` vs `gpu_id=1` do not.
44 CudaUntyped,
45 /// Vulkan device buffer (`VkBuffer`). Two Vulkan backends on the
46 /// same physical device share; Vulkan vs CUDA on the same silicon
47 /// do not (different pointer namespaces, separate allocators).
48 VulkanBuffer,
49 /// Metal device buffer (`MTLBuffer`). Reserved for future Metal
50 /// backend wiring.
51 MetalBuffer,
52}
53
54/// How bytes can move from one device to another. Backends advertise
55/// the paths they support as the source; Router consumes this to
56/// pick the cheapest available path between two specific devices.
57///
58/// Bandwidth + latency estimates land in A5 alongside Router
59/// integration; for A4 the variants exist as discriminators only.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub enum TransferPath {
62 /// No transfer needed (same device instance).
63 SameDevice,
64 /// Direct peer-to-peer (CUDA P2P, NVLink, AMD Infinity Fabric,
65 /// GPUDirect). Requires explicit enablement on both ends.
66 Peer,
67 /// Zero-copy via shared memory mapping (UMA, ResizableBAR,
68 /// dma-buf). Common on integrated GPUs and Apple Silicon.
69 SharedMemory,
70 /// Bulk transfer engine (cudaMemcpy, vkCmdCopyBuffer + staging
71 /// buffer). The default for cross-device GPU↔GPU on standard
72 /// hardware.
73 DeviceCopy,
74 /// Through CPU as intermediary. Universal fallback — every
75 /// backend supports this. Highest latency but always available.
76 HostStaging,
77}
78
79/// What a backend can do — advertised once at registration, consumed
80/// by Router during dispatch planning.
81///
82/// The fields are facts: which (op, dtype) pairs the backend has
83/// kernels for, what alignment its allocations satisfy, what
84/// transfer paths it can use, etc. Routing policy (which backend
85/// gets which op given input residency) lives in Router, not here.
86#[derive(Debug, Clone)]
87pub struct BackendCapabilities {
88 /// Identifies the backend variant (Cpu / Cuda / Vulkan / ...).
89 pub backend_id: BackendId,
90 /// Identifies the specific device within the backend's family
91 /// (e.g., GPU 0 vs GPU 1 for CUDA).
92 pub device_location: DeviceLocation,
93 /// Set of `(op, dtype)` pairs this backend has kernels for.
94 /// Routing checks `contains(&(op, dtype))` before dispatching.
95 pub op_dtype_support: HashSet<(OpKind, DType)>,
96 /// Required alignment in bytes for storage allocations on this
97 /// backend. Router pads/repacks if a source storage doesn't
98 /// meet the destination's alignment.
99 pub required_alignment: usize,
100 /// Smallest addressable unit in bits. Most CPUs/GPUs are byte-
101 /// addressable (8); some accelerators are 32-bit-only or
102 /// 128-bit-vector-only. Router routes around granularity
103 /// limits or refuses to route there.
104 pub access_granularity_bits: u32,
105 /// Outbound transfer paths this backend supports as the source.
106 /// Each entry is the destination's `DeviceLocation` plus the
107 /// path that connects them. Router builds a transfer matrix
108 /// from these entries at registration.
109 pub transfer_paths: Vec<(DeviceLocation, TransferPath)>,
110 /// Allocator / pointer-namespace class. SystemTopology's
111 /// `shares_storage(a, b)` predicate is true iff both backends
112 /// declare the same class **and** target the same device. CPU
113 /// trio all declare [`SubstrateClass::HostBytes`]; CUDA declares
114 /// [`SubstrateClass::CudaUntyped`]; Vulkan declares
115 /// [`SubstrateClass::VulkanBuffer`].
116 pub storage_substrate: SubstrateClass,
117 /// Peak sustained compute throughput as a Layer-1 cost prior, in
118 /// FLOPs per nanosecond (numerically GFLOP/s). The ranker's
119 /// `composite_ns` divides a kernel's FLOP count by this to estimate
120 /// compute time, so a higher value models a genuinely faster
121 /// backend — the placement DP prefers it for large parallel work
122 /// while transfer cost still keeps small ops local. The historical
123 /// neutral prior was 1.0 for every backend (1 FLOP ≈ 1 ns); real
124 /// per-backend tiers replace it, and the Judge's Layer-2 latency
125 /// measurements refine the effective figure per (op, dtype, size)
126 /// cell. Never zero/negative — consumers guard and fall back to the
127 /// CPU prior if it is.
128 pub compute_throughput_flops_per_ns: f64,
129 /// Peak sustained memory bandwidth as a Layer-1 cost prior, in bytes
130 /// per nanosecond (numerically GB/s). `composite_ns` divides
131 /// bytes-moved by this to estimate memory-transfer time. Historical
132 /// neutral prior: 4.0 for every backend.
133 pub mem_bandwidth_bytes_per_ns: f64,
134}
135
136/// Predictive fit-check result for a projected allocation. Consumed
137/// by `BackendRuntime::would_fit` (in `fuel-backend-contract`) and read
138/// by Picker 2's pressure-aware selectors.
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub enum FitStatus {
141 /// Allocation projected to fit comfortably (post-alloc usage
142 /// below the pressure threshold, default 0.85 of total).
143 Comfortable,
144 /// Allocation projected to fit but leaves the device tight
145 /// (post-alloc usage above the pressure threshold). Selectors
146 /// should prefer a less-loaded co-located backend if available.
147 Tight,
148 /// Allocation projected NOT to fit (size > available bytes).
149 /// Selectors should pick a different backend; planner-level
150 /// surfaces a typed error if no alternative.
151 WontFit,
152 /// Backend cannot answer — `available_bytes` returned `None`.
153 /// Selectors fall back to static cost.
154 Unknown,
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::DeviceLocation;
161
162 /// Smoke: BackendCapabilities can be constructed and queried.
163 #[test]
164 fn capability_construction_round_trip() {
165 let caps = BackendCapabilities {
166 backend_id: BackendId::Cpu,
167 device_location: DeviceLocation::Cpu,
168 op_dtype_support: [
169 (OpKind::MatMul, DType::F32),
170 (OpKind::MatMul, DType::F64),
171 (OpKind::AddElementwise, DType::F32),
172 ]
173 .into_iter()
174 .collect(),
175 required_alignment: 64,
176 access_granularity_bits: 8,
177 transfer_paths: vec![(DeviceLocation::Cpu, TransferPath::SameDevice)],
178 storage_substrate: SubstrateClass::HostBytes,
179 compute_throughput_flops_per_ns: 1.0,
180 mem_bandwidth_bytes_per_ns: 4.0,
181 };
182 assert_eq!(caps.backend_id, BackendId::Cpu);
183 assert_eq!(caps.required_alignment, 64);
184 assert_eq!(caps.access_granularity_bits, 8);
185 assert!(
186 caps.op_dtype_support
187 .contains(&(OpKind::MatMul, DType::F32))
188 );
189 assert!(
190 !caps
191 .op_dtype_support
192 .contains(&(OpKind::MatMul, DType::BF16))
193 );
194 }
195
196 /// Smoke: TransferPath enum is comparable + hashable.
197 #[test]
198 fn transfer_path_traits() {
199 let a = TransferPath::SameDevice;
200 let b = TransferPath::SameDevice;
201 let c = TransferPath::HostStaging;
202 assert_eq!(a, b);
203 assert_ne!(a, c);
204 // Hashable — can be a key in HashSet/HashMap.
205 let mut set = HashSet::new();
206 set.insert(TransferPath::Peer);
207 set.insert(TransferPath::Peer); // dup
208 set.insert(TransferPath::DeviceCopy);
209 assert_eq!(set.len(), 2);
210 }
211}