Skip to main content

tract_linalg/
x86_64_fma.rs

1use crate::Ops;
2use crate::frame::element_wise::ElementWiseKer;
3use crate::frame::reduce::{MapReduceKer, ReduceKer};
4use crate::x86_64_fma::softmax::x86_64_avx512_softmax2_fastcompact_f16_64n;
5use crate::x86_64_fma::softmax::x86_64_fma_softmax2_fastcompact_f32_32n;
6
7pub mod mmm;
8
9mod amd_avx512_linear;
10mod amd_fma_linear;
11mod intel_avx512_linear;
12mod intel_avx512_mmv_linear;
13mod intel_fma_linear;
14
15/// CPU vendor, the axis (with the AVX-512-vs-FMA tier, decided by which plug runs)
16/// that selects a per-target `LinearCostModel`. `TRACT_X86_KIND=intel|amd|other`
17/// overrides the CPUID probe (for forcing a cohort under emulation or in CI).
18#[derive(PartialEq, Clone, Copy)]
19pub(crate) enum Vendor {
20    Intel,
21    Amd,
22    Other,
23}
24
25pub(crate) fn vendor() -> Vendor {
26    if let Ok(k) = std::env::var("TRACT_X86_KIND") {
27        return match k.as_str() {
28            "intel" => Vendor::Intel,
29            "amd" => Vendor::Amd,
30            _ => Vendor::Other,
31        };
32    }
33    // `unsafe` is required on the MSRV (1.91); newer rustc deems it redundant.
34    #[allow(unused_unsafe)]
35    let id = unsafe { std::arch::x86_64::__cpuid(0) };
36    let mut s = [0u8; 12];
37    s[0..4].copy_from_slice(&id.ebx.to_le_bytes());
38    s[4..8].copy_from_slice(&id.edx.to_le_bytes());
39    s[8..12].copy_from_slice(&id.ecx.to_le_bytes());
40    match &s {
41        b"GenuineIntel" => Vendor::Intel,
42        b"AuthenticAMD" => Vendor::Amd,
43        _ => Vendor::Other,
44    }
45}
46
47pub mod act;
48pub mod act_f16;
49pub mod act_f16_fp16;
50
51pub mod amx;
52pub mod amx_bf16;
53pub mod avxvnni;
54pub mod by_scalar;
55pub mod erf;
56#[cfg(tract_avx512vnni)]
57pub mod fma_width;
58pub mod max;
59pub mod min;
60pub mod panel_extract;
61pub mod rms_norm;
62pub mod softmax;
63
64const AVX: fn() -> bool = || is_x86_feature_detected!("avx");
65const AVX2: fn() -> bool = || is_x86_feature_detected!("avx2");
66const FMA: fn() -> bool = || is_x86_feature_detected!("fma");
67const AVX512F: fn() -> bool = || is_x86_feature_detected!("avx512f");
68#[cfg(tract_avx512vnni)]
69const AVX512VNNI: fn() -> bool = || is_x86_feature_detected!("avx512vnni");
70
71tanh_impl!(f32, fma_tanh_f32, 8, 8, is_x86_feature_detected!("fma"));
72sigmoid_impl!(f32, fma_sigmoid_f32, 8, 8, is_x86_feature_detected!("fma"));
73silu_impl!(f32, fma_silu_f32, 8, 8, is_x86_feature_detected!("fma"));
74
75// AVX-without-FMA ports of the fma kernels above (each vfmadd132ps expanded
76// to an in-place vmulps+vaddps pair) for CPUs outside the fma tier.
77tanh_impl!(f32, avx_tanh_f32, 8, 8, is_x86_feature_detected!("avx"));
78sigmoid_impl!(f32, avx_sigmoid_f32, 8, 8, is_x86_feature_detected!("avx"));
79
80// AVX-512 (zmm, 16-wide) variants. The assembly lives in x86_64/avx512/; the
81// main loop handles 64 lanes (4 zmm) per iteration with a 16-lane tail, so
82// nr()=16 (any multiple of 16 is safe).
83tanh_impl!(f32, avx512_tanh_f32, 16, 16, is_x86_feature_detected!("avx512f"));
84sigmoid_impl!(f32, avx512_sigmoid_f32, 16, 16, is_x86_feature_detected!("avx512f"));
85silu_impl!(f32, avx512_silu_f32, 16, 16, is_x86_feature_detected!("avx512f"));
86
87fn plug_avx2(_ops: &mut Ops) {}
88
89/// Element-wise kernels for AVX-capable CPUs outside the fma tier: the
90/// mul_by_scalar / max / min asm is plain AVX, and sigmoid / tanh have
91/// dedicated mul+add ports. softmax uses fma asm and keeps its generic
92/// fallback on this tier.
93fn plug_avx(ops: &mut Ops) {
94    ops.sigmoid_f32 = Box::new(|| avx_sigmoid_f32::ew());
95    ops.tanh_f32 = Box::new(|| avx_tanh_f32::ew());
96
97    ops.mul_by_scalar_f32 = Box::new(|| by_scalar::x86_64_avx_f32_mul_by_scalar_32n::ew());
98    ops.max_f32 = Box::new(|| max::x86_64_fma_max_f32_32n::red());
99    ops.min_f32 = Box::new(|| min::x86_64_fma_min_f32_32n::red());
100
101    log::info!("sigmoid_f32, tanh_f32, mul_by_scalar_f32, max_f32, min_f32: x86_64/avx activated");
102}
103
104fn plug_fma(ops: &mut Ops) {
105    panel_extract::plug(ops);
106
107    ops.sigmoid_f32 = Box::new(|| fma_sigmoid_f32::ew());
108    ops.tanh_f32 = Box::new(|| fma_tanh_f32::ew());
109    ops.silu_f32 = Box::new(|| fma_silu_f32::ew());
110
111    ops.mul_by_scalar_f32 = Box::new(|| by_scalar::x86_64_avx_f32_mul_by_scalar_32n::ew());
112    ops.max_f32 = Box::new(|| max::x86_64_fma_max_f32_32n::red());
113    ops.min_f32 = Box::new(|| min::x86_64_fma_min_f32_32n::red());
114    ops.softmax2_fastcompact_f32 = Box::new(|| x86_64_fma_softmax2_fastcompact_f32_32n::red());
115
116    log::info!("sigmoid_f32, tanh_f32, silu_f32: x86_64/fma activated");
117}
118
119/// On hosts that also support AVX-512_FP16 (Sapphire Rapids / Granite Rapids /
120/// later, and recent Xeon-D / consumer parts), upgrade the f16 element-wise
121/// kernels from the f32-roundtrip implementations in `act_f16.rs` to the
122/// native f16 implementations in `act_f16_fp16.rs` where the native path is
123/// actually faster on this uarch. We benched each op against its f32-roundtrip
124/// equivalent on Sapphire Rapids and only plug in the ones that win:
125///
126///   hardswish_f16:  8.71 → 31.6 Gelem/s  (3.62× native) — plug in
127///   leaky_relu_f16: 9.44 →  5.85 Gelem/s (0.62× native — regression) — keep
128///                   the f32-roundtrip version from act_f16.rs. The native
129///                   kernel exists in act_f16_fp16.rs for future revisits but
130///                   is not wired here.
131fn plug_avx512fp16(ops: &mut Ops) {
132    ops.hardswish_f16 = Box::new(|| act_f16_fp16::x86_64_avx512fp16_hardswish_f16_128n::ew());
133
134    log::info!("hardswish_f16: x86_64/avx512fp16 native activated");
135}
136
137fn plug_avx512f(ops: &mut Ops) {
138    ops.sigmoid_f32 = Box::new(|| avx512_sigmoid_f32::ew());
139    ops.tanh_f32 = Box::new(|| avx512_tanh_f32::ew());
140    ops.hardswish_f32 = Box::new(|| act::x86_64_avx512_hardswish_f32_64n::ew());
141    ops.leaky_relu_f32 = Box::new(|| act::x86_64_avx512_leaky_relu_f32_64n::ew());
142    ops.silu_f32 = Box::new(|| avx512_silu_f32::ew());
143    ops.gelu_f32 = Box::new(|| act::x86_64_avx512_gelu_f32_16n::ew());
144
145    ops.sigmoid_f16 = Box::new(|| act_f16::x86_64_avx512_sigmoid_f16_16n::ew());
146    ops.tanh_f16 = Box::new(|| act_f16::x86_64_avx512_tanh_f16_16n::ew());
147    ops.hardswish_f16 = Box::new(|| act_f16::x86_64_avx512_hardswish_f16_64n::ew());
148    ops.leaky_relu_f16 = Box::new(|| act_f16::x86_64_avx512_leaky_relu_f16_64n::ew());
149    ops.silu_f16 = Box::new(|| act_f16::x86_64_avx512_silu_f16_16n::ew());
150    ops.gelu_f16 = Box::new(|| act_f16::x86_64_avx512_gelu_f16_16n::ew());
151
152    ops.max_f32 = Box::new(|| max::x86_64_avx512_max_f32_64n::red());
153    ops.softmax2_fastcompact_f32 =
154        Box::new(|| softmax::x86_64_avx512_softmax2_fastcompact_f32_64n::red());
155    ops.softmax2_fastcompact_f16 = Box::new(|| x86_64_avx512_softmax2_fastcompact_f16_64n::red());
156
157    ops.erf_f32 = Box::new(|| erf::x86_64_avx512_erf_f32_64n::ew());
158
159    ops.rms_norm_f32 = Box::new(rms_norm::rms_norm_f32);
160
161    log::info!(
162        "sigmoid_f32, tanh_f32, hardswish_f32, leaky_relu_f32, \
163         silu_f32, gelu_f32, \
164         sigmoid_f16, tanh_f16, hardswish_f16, leaky_relu_f16, \
165         silu_f16, gelu_f16, \
166         max_f32, softmax2_fastcompact_f32, softmax2_fastcompact_f16, erf_f32, \
167         rms_norm_f32: x86_64/avx512f activated"
168    );
169}
170
171pub fn plug(ops: &mut Ops) {
172    mmm::plug(ops);
173    if is_x86_feature_detected!("avx")
174        && !(is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma"))
175    {
176        plug_avx(ops);
177    }
178    if is_x86_feature_detected!("avx2") {
179        plug_avx2(ops);
180        if is_x86_feature_detected!("fma") {
181            plug_fma(ops);
182            if is_x86_feature_detected!("avx512f") {
183                plug_avx512f(ops);
184                if is_x86_feature_detected!("avx512fp16") {
185                    plug_avx512fp16(ops);
186                }
187            }
188        }
189    }
190}