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});
214
215#[cfg(target_arch = "x86_64")]
216mod x86 {
217    use core::arch::x86_64::__cpuid;
218
219    #[inline]
220    fn check_flag(x: usize, position: u32) -> bool {
221        x & (1 << position) != 0
222    }
223
224    pub fn has_avx512_f16_support() -> bool {
225        // this macro does many OS checks/etc. to determine if allowed to use AVX512
226        if !has_avx512() {
227            return false;
228        }
229
230        // EAX=7, ECX=0: Extended Features (includes AVX512)
231        // More info on calling CPUID can be found here (section 1.4)
232        // https://www.intel.com/content/dam/develop/external/us/en/documents/architecture-instruction-set-extensions-programming-reference.pdf
233        // __cpuid is safe in nightly but unsafe in stable, allow both
234        #[allow(unused_unsafe)]
235        let ext_cpuid_result = unsafe { __cpuid(7) };
236        check_flag(ext_cpuid_result.edx as usize, 23)
237    }
238
239    pub fn has_avx512() -> bool {
240        is_x86_feature_detected!("avx512f")
241    }
242}
243
244// Inspired by https://github.com/RustCrypto/utils/blob/master/cpufeatures/src/aarch64.rs
245// aarch64 doesn't have userspace feature detection built in, so we have to call
246// into OS-specific functions to check for features.
247
248#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
249mod aarch64 {
250    pub fn has_neon_f16_support() -> bool {
251        // Maybe we can assume it's there?
252        true
253    }
254}
255
256#[cfg(all(target_arch = "aarch64", target_os = "linux"))]
257mod aarch64 {
258    pub fn has_neon_f16_support() -> bool {
259        // See: https://github.com/rust-lang/libc/blob/7ce81ca7aeb56aae7ca0237ef9353d58f3d7d2f1/src/unix/linux_like/linux/gnu/b64/aarch64/mod.rs#L533
260        let flags = unsafe { libc::getauxval(libc::AT_HWCAP) };
261        flags & libc::HWCAP_FPHP != 0
262    }
263}
264
265#[cfg(all(target_arch = "aarch64", target_os = "windows"))]
266mod aarch64 {
267    pub fn has_neon_f16_support() -> bool {
268        // https://github.com/lance-format/lance/issues/2411
269        false
270    }
271}
272
273#[cfg(target_arch = "loongarch64")]
274mod loongarch64 {
275    pub fn has_lsx_support() -> bool {
276        // See: https://github.com/rust-lang/libc/blob/7ce81ca7aeb56aae7ca0237ef9353d58f3d7d2f1/src/unix/linux_like/linux/gnu/b64/loongarch64/mod.rs#L263
277        let flags = unsafe { libc::getauxval(libc::AT_HWCAP) };
278        flags & libc::HWCAP_LOONGARCH_LSX != 0
279    }
280    pub fn has_lasx_support() -> bool {
281        // See: https://github.com/rust-lang/libc/blob/7ce81ca7aeb56aae7ca0237ef9353d58f3d7d2f1/src/unix/linux_like/linux/gnu/b64/loongarch64/mod.rs#L264
282        let flags = unsafe { libc::getauxval(libc::AT_HWCAP) };
283        flags & libc::HWCAP_LOONGARCH_LASX != 0
284    }
285}
286
287#[cfg(all(target_arch = "aarch64", target_os = "android"))]
288mod aarch64 {
289    pub fn has_neon_f16_support() -> bool {
290        false
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use rstest::rstest;
298
299    #[test]
300    fn simd_info_exposes_tier() {
301        let info = simd_info();
302        assert_eq!(info.target_arch, std::env::consts::ARCH);
303        // Tier should match the detected SIMD support.
304        assert_eq!(info.tier, *SIMD_SUPPORT);
305    }
306
307    #[cfg(target_arch = "x86_64")]
308    #[test]
309    fn simd_info_features_include_baseline() {
310        let info = simd_info();
311        // The x86_64 ABI mandates SSE2, so it must always be present on this
312        // architecture.
313        assert!(info.host_features.contains(&"sse2"));
314    }
315
316    #[cfg(not(target_arch = "x86_64"))]
317    #[test]
318    fn simd_info_features_empty_off_x86_64() {
319        let info = simd_info();
320        assert!(info.host_features.is_empty());
321    }
322
323    /// The `Avx2` and `AvxFma` tiers both dispatch to kernels declared
324    /// `#[target_feature(enable = "avx,fma")]`, so neither may be selected on a
325    /// host without FMA. AVX2 does not imply FMA in the ISA, so the detection
326    /// checks it explicitly. (`Avx512*` is excluded: its kernels declare
327    /// `avx512f`, which is what `has_avx512` verifies.)
328    #[cfg(target_arch = "x86_64")]
329    #[test]
330    fn avx_fma_tiers_are_only_selected_when_fma_is_detected() {
331        if matches!(*SIMD_SUPPORT, SimdSupport::Avx2 | SimdSupport::AvxFma) {
332            assert!(
333                is_x86_feature_detected!("fma"),
334                "tier {} dispatches to avx,fma kernels but the host has no FMA",
335                *SIMD_SUPPORT
336            );
337        }
338    }
339
340    #[rstest]
341    #[case::none(SimdSupport::None, "none")]
342    #[case::neon(SimdSupport::Neon, "neon")]
343    #[case::sse(SimdSupport::Sse, "sse")]
344    #[case::avx(SimdSupport::Avx, "avx")]
345    #[case::avx_fma(SimdSupport::AvxFma, "avx_fma")]
346    #[case::avx2(SimdSupport::Avx2, "avx2")]
347    #[case::avx512(SimdSupport::Avx512, "avx512")]
348    #[case::avx512_fp16(SimdSupport::Avx512FP16, "avx512_fp16")]
349    #[case::lsx(SimdSupport::Lsx, "lsx")]
350    #[case::lasx(SimdSupport::Lasx, "lasx")]
351    fn simd_support_display_matches_lowercase_convention(
352        #[case] tier: SimdSupport,
353        #[case] expected: &str,
354    ) {
355        assert_eq!(tier.to_string(), expected);
356    }
357}