Skip to main content

hyalite/
backend.rs

1//! Which compute backend a [`Database`](crate::Database) resolves to, and how to override it.
2//!
3//! Reporting *and* controlling the backend is load-bearing, not cosmetic: a benchmark number is
4//! meaningless without knowing which kernel ran, the CI matrix must be able to force each backend
5//! in turn to exercise the determinism contract, and a downstream tool (rustar) will log the
6//! resolved backend so a reproducibility question can be answered from the log rather than by
7//! guessing at the user's CPU. See `handover.md` §4 and §7.
8//!
9//! # Availability in M0
10//!
11//! All four backend *names* exist so the override API and env var do not churn when the SIMD
12//! kernels land, but only [`Backend::Scalar`] is **implemented and available** in M0. Forcing any
13//! other backend returns [`Error::BackendUnavailable`](crate::Error::BackendUnavailable) today;
14//! it will simply start succeeding once that kernel exists and the CPU supports it.
15
16use crate::error::{Error, Result};
17use core::fmt;
18
19/// The environment variable that overrides backend selection at [`build`](crate::DatabaseBuilder::build).
20pub const BACKEND_ENV_VAR: &str = "HYALITE_BACKEND";
21
22/// The alignment backend actually used, or one that could be requested.
23///
24/// `#[non_exhaustive]` — more tiers may be added without a breaking change.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26#[non_exhaustive]
27pub enum Backend {
28    /// The portable scalar reference kernel. Always available; the correctness oracle.
29    Scalar,
30    /// x86-64 SSE4.1 (16 lanes @ i8). Not yet implemented in M0.
31    Sse41,
32    /// x86-64 AVX2 (32 lanes @ i8). Not yet implemented in M0.
33    Avx2,
34    /// aarch64 NEON (16 lanes @ i8). Not yet implemented in M0.
35    Neon,
36}
37
38impl Backend {
39    /// The canonical lowercase name, matching what [`BackendChoice::parse`] accepts.
40    #[must_use]
41    pub const fn name(self) -> &'static str {
42        match self {
43            Backend::Scalar => "scalar",
44            Backend::Sse41 => "sse4.1",
45            Backend::Avx2 => "avx2",
46            Backend::Neon => "neon",
47        }
48    }
49
50    /// The SIMD lane count (number of `i8` lanes) for this backend, or `None` for the scalar
51    /// backend, which does not use the inter-sequence lane kernel.
52    #[must_use]
53    pub(crate) fn simd_lanes(self) -> Option<usize> {
54        match self {
55            Backend::Scalar => None,
56            Backend::Sse41 | Backend::Neon => Some(16),
57            Backend::Avx2 => Some(32),
58        }
59    }
60
61    /// Whether this backend is implemented and usable on the current build/CPU. Gated on both the
62    /// target architecture and runtime CPU-feature detection.
63    ///
64    /// Availability is *not* the whole story for a given database: even an available SIMD backend
65    /// is only *used* when the database is SIMD-eligible (i8 width, small alphabet). That extra
66    /// gate lives in `DatabaseBuilder::build`.
67    #[must_use]
68    pub fn is_available(self) -> bool {
69        match self {
70            Backend::Scalar => true,
71            Backend::Sse41 => sse41_detected(),
72            Backend::Avx2 => avx2_detected(),
73            Backend::Neon => neon_available(),
74        }
75    }
76}
77
78#[cfg(target_arch = "x86_64")]
79fn sse41_detected() -> bool {
80    std::is_x86_feature_detected!("sse4.1")
81}
82
83#[cfg(not(target_arch = "x86_64"))]
84fn sse41_detected() -> bool {
85    false
86}
87
88#[cfg(target_arch = "x86_64")]
89fn avx2_detected() -> bool {
90    std::is_x86_feature_detected!("avx2")
91}
92
93#[cfg(not(target_arch = "x86_64"))]
94fn avx2_detected() -> bool {
95    false
96}
97
98// NEON is mandatory on aarch64 — always present, no runtime detection needed (handover §5).
99#[cfg(target_arch = "aarch64")]
100fn neon_available() -> bool {
101    true
102}
103
104#[cfg(not(target_arch = "aarch64"))]
105fn neon_available() -> bool {
106    false
107}
108
109impl fmt::Display for Backend {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        f.write_str(self.name())
112    }
113}
114
115/// How a [`Database`](crate::Database) should pick its backend: detect automatically, or force a
116/// specific one (for the CI matrix and benchmarking).
117#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
118pub enum BackendChoice {
119    /// Pick the fastest available backend at build time.
120    #[default]
121    Auto,
122    /// Force a specific backend; [`build`](crate::DatabaseBuilder::build) fails with
123    /// [`Error::BackendUnavailable`] if it is not available.
124    Force(Backend),
125}
126
127impl BackendChoice {
128    /// Parse a backend choice from a string (case-insensitive). Accepts `auto`, `scalar`,
129    /// `sse4.1`/`sse41`, `avx2`, and `neon`.
130    ///
131    /// # Errors
132    ///
133    /// [`Error::InvalidBackendName`] if the string matches none of those.
134    pub fn parse(s: &str) -> Result<Self> {
135        match s.trim().to_ascii_lowercase().as_str() {
136            "auto" => Ok(BackendChoice::Auto),
137            "scalar" => Ok(BackendChoice::Force(Backend::Scalar)),
138            "sse4.1" | "sse41" => Ok(BackendChoice::Force(Backend::Sse41)),
139            "avx2" => Ok(BackendChoice::Force(Backend::Avx2)),
140            "neon" => Ok(BackendChoice::Force(Backend::Neon)),
141            _ => Err(Error::InvalidBackendName {
142                name: s.to_string(),
143            }),
144        }
145    }
146}
147
148/// The fastest available backend, in descending preference: AVX2, SSE4.1 (x86-64), NEON
149/// (aarch64), then scalar.
150fn detect_best() -> Backend {
151    if Backend::Avx2.is_available() {
152        Backend::Avx2
153    } else if Backend::Sse41.is_available() {
154        Backend::Sse41
155    } else if Backend::Neon.is_available() {
156        Backend::Neon
157    } else {
158        Backend::Scalar
159    }
160}
161
162/// Resolve a [`BackendChoice`] to a concrete, available [`Backend`].
163///
164/// # Errors
165///
166/// [`Error::BackendUnavailable`] if a forced backend is not available.
167pub(crate) fn resolve(choice: BackendChoice) -> Result<Backend> {
168    match choice {
169        BackendChoice::Auto => Ok(detect_best()),
170        BackendChoice::Force(backend) => {
171            if backend.is_available() {
172                Ok(backend)
173            } else {
174                Err(Error::BackendUnavailable { backend })
175            }
176        }
177    }
178}
179
180/// The backend choice requested via the [`BACKEND_ENV_VAR`] environment variable, if any.
181/// Reading is safe; the variable is consulted once at build time.
182///
183/// # Errors
184///
185/// [`Error::InvalidBackendName`] if the variable is set to an unrecognised or non-Unicode value.
186pub(crate) fn choice_from_env() -> Result<Option<BackendChoice>> {
187    match std::env::var(BACKEND_ENV_VAR) {
188        Ok(s) if s.trim().is_empty() => Ok(None),
189        Ok(s) => BackendChoice::parse(&s).map(Some),
190        Err(std::env::VarError::NotPresent) => Ok(None),
191        Err(std::env::VarError::NotUnicode(_)) => Err(Error::InvalidBackendName {
192            name: "<non-unicode>".to_string(),
193        }),
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn availability_matches_arch_and_cpu() {
203        // Scalar is always available; x86 SIMD tracks the CPU; NEON is baseline on aarch64.
204        assert!(Backend::Scalar.is_available());
205        assert_eq!(Backend::Sse41.is_available(), sse41_detected());
206        assert_eq!(Backend::Avx2.is_available(), avx2_detected());
207        assert_eq!(Backend::Neon.is_available(), neon_available());
208        #[cfg(not(target_arch = "x86_64"))]
209        {
210            assert!(!Backend::Sse41.is_available());
211            assert!(!Backend::Avx2.is_available());
212        }
213        #[cfg(not(target_arch = "aarch64"))]
214        assert!(!Backend::Neon.is_available());
215    }
216
217    #[test]
218    fn name_round_trips_through_parse_for_every_backend() {
219        for b in [
220            Backend::Scalar,
221            Backend::Sse41,
222            Backend::Avx2,
223            Backend::Neon,
224        ] {
225            assert_eq!(
226                BackendChoice::parse(b.name()).unwrap(),
227                BackendChoice::Force(b)
228            );
229            assert_eq!(b.to_string(), b.name());
230        }
231    }
232
233    #[test]
234    fn parse_accepts_aliases_and_is_case_insensitive() {
235        assert_eq!(BackendChoice::parse("auto").unwrap(), BackendChoice::Auto);
236        assert_eq!(BackendChoice::parse("AUTO").unwrap(), BackendChoice::Auto);
237        assert_eq!(
238            BackendChoice::parse("  SSE41 ").unwrap(),
239            BackendChoice::Force(Backend::Sse41)
240        );
241        assert_eq!(
242            BackendChoice::parse("sse4.1").unwrap(),
243            BackendChoice::Force(Backend::Sse41)
244        );
245        assert_eq!(
246            BackendChoice::parse("Avx2").unwrap(),
247            BackendChoice::Force(Backend::Avx2)
248        );
249    }
250
251    #[test]
252    fn parse_rejects_unknown_names() {
253        for bad in ["", "sse2", "ssse3", "avx512", "gpu", "x"] {
254            let err = BackendChoice::parse(bad).unwrap_err();
255            assert_eq!(
256                err,
257                Error::InvalidBackendName {
258                    name: bad.to_string()
259                }
260            );
261        }
262    }
263
264    #[test]
265    fn resolve_auto_picks_an_available_backend() {
266        let b = resolve(BackendChoice::Auto).unwrap();
267        assert!(b.is_available());
268        // Auto prefers SSE4.1 when the CPU supports it, else scalar.
269        assert_eq!(b, detect_best());
270    }
271
272    #[test]
273    fn resolve_forcing_scalar_always_succeeds() {
274        assert_eq!(
275            resolve(BackendChoice::Force(Backend::Scalar)).unwrap(),
276            Backend::Scalar
277        );
278    }
279
280    #[test]
281    fn resolve_forcing_a_backend_tracks_its_availability() {
282        for b in [Backend::Sse41, Backend::Avx2, Backend::Neon] {
283            let got = resolve(BackendChoice::Force(b));
284            if b.is_available() {
285                assert_eq!(got.unwrap(), b);
286            } else {
287                assert_eq!(got.unwrap_err(), Error::BackendUnavailable { backend: b });
288            }
289        }
290    }
291}