Skip to main content

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}