Skip to main content

lance_core/utils/
cpu.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::fmt;
5use std::sync::LazyLock;
6
7/// A level of SIMD support for some feature.
8///
9/// `#[non_exhaustive]` so future tiers (e.g. AVX-512 BF16, AMX) can be added
10/// without breaking external `match` consumers.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum SimdSupport {
14    None,
15    Neon,
16    Sse,
17    /// AVX (256-bit float ops) without FMA.
18    ///
19    /// This tier does not imply that AVX2 is absent: selecting [`Self::Avx2`]
20    /// requires both AVX2 and FMA, so a host with AVX2 but no FMA selects this
21    /// tier. Intel Sandy Bridge / Ivy Bridge are the typical hosts.
22    Avx,
23    /// AVX + FMA but no AVX2.
24    /// AMD Piledriver / Steamroller / FX-7500.
25    AvxFma,
26    /// AVX2 + FMA. Intel Haswell / AMD Excavator and later.
27    ///
28    /// Selecting this tier asserts FMA is present: the kernels it dispatches to
29    /// are `#[target_feature(enable = "avx,fma")]`.
30    Avx2,
31    Avx512,
32    Avx512FP16,
33    Lsx,
34    Lasx,
35}
36
37impl fmt::Display for SimdSupport {
38    /// Formats the tier name in lowercase, matching pyarrow's
39    /// `runtime_info().simd_level` convention.
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        let name = match self {
42            Self::None => "none",
43            Self::Neon => "neon",
44            Self::Sse => "sse",
45            Self::Avx => "avx",
46            Self::AvxFma => "avx_fma",
47            Self::Avx2 => "avx2",
48            Self::Avx512 => "avx512",
49            Self::Avx512FP16 => "avx512_fp16",
50            Self::Lsx => "lsx",
51            Self::Lasx => "lasx",
52        };
53        f.write_str(name)
54    }
55}
56
57/// Snapshot of the SIMD tier lance dispatches to on the current host, plus the
58/// raw CPU features detected for diagnostic purposes.
59///
60/// Mirrors the role of `pyarrow.runtime_info()`: a single, cheap call users can
61/// make to verify which SIMD tier the runtime selected and what underlying
62/// features the host advertises. Obtain one with [`simd_info()`].
63#[derive(Debug, Clone)]
64pub struct SimdInfo {
65    /// The SIMD tier lance dispatches to at runtime on this host.
66    pub tier: SimdSupport,
67    /// The architecture name (e.g. "x86_64", "aarch64", "loongarch64").
68    pub target_arch: &'static str,
69    /// Raw CPU feature flags detected on this host (x86_64 only; empty on
70    /// other architectures). Each entry is a feature name like "avx2",
71    /// "fma", "avx512f", "popcnt", etc.
72    pub host_features: Vec<&'static str>,
73}
74
75/// Returns a snapshot of the SIMD tier lance is using on this host along with
76/// the raw CPU feature flags that drove the decision.
77///
78/// Useful for performance debugging and giving users a way to verify which
79/// dispatch tier they are hitting without rebuilding lance. See [`SimdInfo`]
80/// for the meaning of each field and [`SimdSupport`] for the tier values.
81///
82/// # Examples
83///
84/// ```
85/// use lance_core::utils::cpu::simd_info;
86///
87/// let info = simd_info();
88/// println!("dispatching to {} on {}", info.tier, info.target_arch);
89/// ```
90pub fn simd_info() -> SimdInfo {
91    SimdInfo {
92        tier: *SIMD_SUPPORT,
93        target_arch: std::env::consts::ARCH,
94        host_features: detect_host_features(),
95    }
96}
97
98#[cfg(target_arch = "x86_64")]
99fn detect_host_features() -> Vec<&'static str> {
100    // Each call must be inline: `is_x86_feature_detected!` does its own custom
101    // input parsing and rejects feature names received via a `macro_rules!`
102    // `:literal` metavariable on some toolchains.
103    let mut features = Vec::with_capacity(17);
104    if is_x86_feature_detected!("sse2") {
105        features.push("sse2");
106    }
107    if is_x86_feature_detected!("sse3") {
108        features.push("sse3");
109    }
110    if is_x86_feature_detected!("ssse3") {
111        features.push("ssse3");
112    }
113    if is_x86_feature_detected!("sse4.1") {
114        features.push("sse4.1");
115    }
116    if is_x86_feature_detected!("sse4.2") {
117        features.push("sse4.2");
118    }
119    if is_x86_feature_detected!("popcnt") {
120        features.push("popcnt");
121    }
122    if is_x86_feature_detected!("avx") {
123        features.push("avx");
124    }
125    if is_x86_feature_detected!("avx2") {
126        features.push("avx2");
127    }
128    if is_x86_feature_detected!("fma") {
129        features.push("fma");
130    }
131    if is_x86_feature_detected!("f16c") {
132        features.push("f16c");
133    }
134    if is_x86_feature_detected!("bmi1") {
135        features.push("bmi1");
136    }
137    if is_x86_feature_detected!("bmi2") {
138        features.push("bmi2");
139    }
140    if is_x86_feature_detected!("avx512f") {
141        features.push("avx512f");
142    }
143    if is_x86_feature_detected!("avx512bw") {
144        features.push("avx512bw");
145    }
146    if is_x86_feature_detected!("avx512cd") {
147        features.push("avx512cd");
148    }
149    if is_x86_feature_detected!("avx512dq") {
150        features.push("avx512dq");
151    }
152    if is_x86_feature_detected!("avx512vl") {
153        features.push("avx512vl");
154    }
155    features
156}
157
158#[cfg(not(target_arch = "x86_64"))]
159fn detect_host_features() -> Vec<&'static str> {
160    Vec::new()
161}
162
163/// Support for SIMD operations
164pub static SIMD_SUPPORT: LazyLock<SimdSupport> = LazyLock::new(|| {
165    #[cfg(all(target_arch = "aarch64", any(target_os = "ios", target_os = "tvos")))]
166    {
167        // AArch64 iOS/tvOS has NEON; fp16 arithmetic is available on modern targets.
168        SimdSupport::Neon
169    }
170    #[cfg(all(
171        target_arch = "aarch64",
172        not(any(target_os = "ios", target_os = "tvos"))
173    ))]
174    {
175        if aarch64::has_neon_f16_support() {
176            SimdSupport::Neon
177        } else {
178            SimdSupport::None
179        }
180    }
181    #[cfg(target_arch = "x86_64")]
182    {
183        if x86::has_avx512() {
184            if x86::has_avx512_f16_support() {
185                SimdSupport::Avx512FP16
186            } else {
187                SimdSupport::Avx512
188            }
189        } else if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
190            // FMA is checked explicitly: every kernel selected for this tier is
191            // `#[target_feature(enable = "avx,fma")]`, and AVX2 does not imply
192            // FMA in the ISA. Every shipping AVX2 part has FMA, so this only
193            // guards against a host that would otherwise take an FMA kernel
194            // without FMA.
195            SimdSupport::Avx2
196        } else if is_x86_feature_detected!("avx") && is_x86_feature_detected!("fma") {
197            // AMD Piledriver / Steamroller / FX-7500: 256-bit float ops + FMA but no AVX2.
198            SimdSupport::AvxFma
199        } else if is_x86_feature_detected!("avx") {
200            // This includes a possible AVX2 host without FMA because the Avx2
201            // tier above requires both features.
202            SimdSupport::Avx
203        } else {
204            SimdSupport::None
205        }
206    }
207    #[cfg(target_arch = "loongarch64")]
208    {
209        if loongarch64::has_lasx_support() {
210            SimdSupport::Lasx
211        } else if loongarch64::has_lsx_support() {
212            SimdSupport::Lsx
213        } else {
214            SimdSupport::None
215        }
216    }
217    #[cfg(not(any(
218        target_arch = "aarch64",
219        target_arch = "x86_64",
220        target_arch = "loongarch64"
221    )))]
222    {
223        SimdSupport::None
224    }
225});
226
227#[cfg(target_arch = "x86_64")]
228mod x86 {
229    use core::arch::x86_64::__cpuid;
230
231    #[inline]
232    fn check_flag(x: usize, position: u32) -> bool {
233        x & (1 << position) != 0
234    }
235
236    pub fn has_avx512_f16_support() -> bool {
237        // this macro does many OS checks/etc. to determine if allowed to use AVX512
238        if !has_avx512() {
239            return false;
240        }
241
242        // EAX=7, ECX=0: Extended Features (includes AVX512)
243        // More info on calling CPUID can be found here (section 1.4)
244        // https://www.intel.com/content/dam/develop/external/us/en/documents/architecture-instruction-set-extensions-programming-reference.pdf
245        // __cpuid is safe in nightly but unsafe in stable, allow both
246        #[allow(unused_unsafe)]
247        let ext_cpuid_result = unsafe { __cpuid(7) };
248        check_flag(ext_cpuid_result.edx as usize, 23)
249    }
250
251    pub fn has_avx512() -> bool {
252        is_x86_feature_detected!("avx512f")
253    }
254}
255
256// Inspired by https://github.com/RustCrypto/utils/blob/master/cpufeatures/src/aarch64.rs
257// aarch64 doesn't have userspace feature detection built in, so we have to call
258// into OS-specific functions to check for features.
259
260#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
261mod aarch64 {
262    pub fn has_neon_f16_support() -> bool {
263        // Maybe we can assume it's there?
264        true
265    }
266}
267
268#[cfg(all(target_arch = "aarch64", target_os = "linux"))]
269mod aarch64 {
270    pub fn has_neon_f16_support() -> bool {
271        // See: https://github.com/rust-lang/libc/blob/7ce81ca7aeb56aae7ca0237ef9353d58f3d7d2f1/src/unix/linux_like/linux/gnu/b64/aarch64/mod.rs#L533
272        let flags = unsafe { libc::getauxval(libc::AT_HWCAP) };
273        flags & libc::HWCAP_FPHP != 0
274    }
275}
276
277#[cfg(all(target_arch = "aarch64", target_os = "windows"))]
278mod aarch64 {
279    pub fn has_neon_f16_support() -> bool {
280        // https://github.com/lance-format/lance/issues/2411
281        false
282    }
283}
284
285#[cfg(target_arch = "loongarch64")]
286mod loongarch64 {
287    pub fn has_lsx_support() -> bool {
288        // See: https://github.com/rust-lang/libc/blob/7ce81ca7aeb56aae7ca0237ef9353d58f3d7d2f1/src/unix/linux_like/linux/gnu/b64/loongarch64/mod.rs#L263
289        let flags = unsafe { libc::getauxval(libc::AT_HWCAP) };
290        flags & libc::HWCAP_LOONGARCH_LSX != 0
291    }
292    pub fn has_lasx_support() -> bool {
293        // See: https://github.com/rust-lang/libc/blob/7ce81ca7aeb56aae7ca0237ef9353d58f3d7d2f1/src/unix/linux_like/linux/gnu/b64/loongarch64/mod.rs#L264
294        let flags = unsafe { libc::getauxval(libc::AT_HWCAP) };
295        flags & libc::HWCAP_LOONGARCH_LASX != 0
296    }
297}
298
299#[cfg(all(target_arch = "aarch64", target_os = "android"))]
300mod aarch64 {
301    pub fn has_neon_f16_support() -> bool {
302        false
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use rstest::rstest;
310
311    #[test]
312    fn simd_info_exposes_tier() {
313        let info = simd_info();
314        assert_eq!(info.target_arch, std::env::consts::ARCH);
315        // Tier should match the detected SIMD support.
316        assert_eq!(info.tier, *SIMD_SUPPORT);
317    }
318
319    #[cfg(target_arch = "x86_64")]
320    #[test]
321    fn simd_info_features_include_baseline() {
322        let info = simd_info();
323        // The x86_64 ABI mandates SSE2, so it must always be present on this
324        // architecture.
325        assert!(info.host_features.contains(&"sse2"));
326    }
327
328    #[cfg(not(target_arch = "x86_64"))]
329    #[test]
330    fn simd_info_features_empty_off_x86_64() {
331        let info = simd_info();
332        assert!(info.host_features.is_empty());
333    }
334
335    /// The `Avx2` and `AvxFma` tiers both dispatch to kernels declared
336    /// `#[target_feature(enable = "avx,fma")]`, so neither may be selected on a
337    /// host without FMA. AVX2 does not imply FMA in the ISA, so the detection
338    /// checks it explicitly. (`Avx512*` is excluded: its kernels declare
339    /// `avx512f`, which is what `has_avx512` verifies.)
340    #[cfg(target_arch = "x86_64")]
341    #[test]
342    fn avx_fma_tiers_are_only_selected_when_fma_is_detected() {
343        if matches!(*SIMD_SUPPORT, SimdSupport::Avx2 | SimdSupport::AvxFma) {
344            assert!(
345                is_x86_feature_detected!("fma"),
346                "tier {} dispatches to avx,fma kernels but the host has no FMA",
347                *SIMD_SUPPORT
348            );
349        }
350    }
351
352    #[rstest]
353    #[case::none(SimdSupport::None, "none")]
354    #[case::neon(SimdSupport::Neon, "neon")]
355    #[case::sse(SimdSupport::Sse, "sse")]
356    #[case::avx(SimdSupport::Avx, "avx")]
357    #[case::avx_fma(SimdSupport::AvxFma, "avx_fma")]
358    #[case::avx2(SimdSupport::Avx2, "avx2")]
359    #[case::avx512(SimdSupport::Avx512, "avx512")]
360    #[case::avx512_fp16(SimdSupport::Avx512FP16, "avx512_fp16")]
361    #[case::lsx(SimdSupport::Lsx, "lsx")]
362    #[case::lasx(SimdSupport::Lasx, "lasx")]
363    fn simd_support_display_matches_lowercase_convention(
364        #[case] tier: SimdSupport,
365        #[case] expected: &str,
366    ) {
367        assert_eq!(tier.to_string(), expected);
368    }
369}