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