Skip to main content

ghostlink_core/
accelerator.rs

1//! Runtime-selected execution backend helpers.
2//!
3//! This module turns the detected acceleration mode into a concrete execution
4//! profile with backend-specific chunk sizing and parallel slice execution.
5
6use crate::host::{AccelerationMode, RuntimeProfile};
7use std::mem::{ManuallyDrop, MaybeUninit};
8
9#[derive(Copy, Clone)]
10struct SendPtr(*mut f32);
11unsafe impl Send for SendPtr {}
12unsafe impl Sync for SendPtr {}
13
14/// Concrete execution backend derived from the runtime profile.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct ExecutionBackend {
17    /// Selected acceleration mode.
18    pub mode: AccelerationMode,
19    /// Number of worker slices used for execution.
20    pub worker_count: usize,
21    /// Preferred batch size per worker.
22    pub preferred_batch_size: usize,
23    /// Logical vector width used when chunking the inner loop.
24    pub vector_width_bits: usize,
25}
26
27impl ExecutionBackend {
28    /// Select an execution backend from the detected runtime profile.
29    pub fn from_runtime_profile(profile: &RuntimeProfile) -> Self {
30        let (preferred_batch_size, vector_width_bits) = match profile.acceleration_mode {
31            AccelerationMode::Gpu => (4096, 512),
32            AccelerationMode::Avx512 => (2048, 512),
33            AccelerationMode::Avx2 => (1024, 256),
34            AccelerationMode::Neon => (1024, 128),
35            AccelerationMode::Generic => (512, 64),
36        };
37
38        Self {
39            mode: profile.acceleration_mode,
40            worker_count: profile.recommended_workers.max(1),
41            preferred_batch_size,
42            vector_width_bits,
43        }
44    }
45
46    /// Human-readable backend name.
47    pub const fn name(&self) -> &'static str {
48        self.mode.as_str()
49    }
50
51    /// Scale a slice using the selected backend's worker and chunk sizing.
52    pub fn scale_f32_slice(&self, input: &[f32], scale: f32) -> Vec<f32> {
53        if input.is_empty() {
54            return Vec::new();
55        }
56
57        // Allocate an uninitialized buffer of MaybeUninit<f32> to avoid zero-filling memset overhead.
58        // SAFETY: MaybeUninit<f32> allows uninitialized memory, so calling set_len on Vec<MaybeUninit<f32>> is sound.
59        let mut output: Vec<MaybeUninit<f32>> = Vec::with_capacity(input.len());
60        unsafe {
61            output.set_len(input.len());
62        }
63
64        let out_ptr = output.as_mut_ptr() as *mut f32;
65
66        unsafe {
67            match self.mode {
68                AccelerationMode::Gpu => {
69                    parallel_scale_scalar(input, out_ptr, scale, self.worker_count)
70                }
71                AccelerationMode::Avx512 => scale_x86_512(input, out_ptr, scale),
72                AccelerationMode::Avx2 => scale_x86_256(input, out_ptr, scale),
73                AccelerationMode::Neon => scale_neon(input, out_ptr, scale),
74                AccelerationMode::Generic => scale_scalar(input, out_ptr, scale),
75            }
76        }
77
78        // SAFETY: Every element in 0..input.len() has been fully initialized by the backend scaling function above.
79        // Using ManuallyDrop + Vec::from_raw_parts avoids transmute between repr(Rust) Vec types.
80        let mut md = ManuallyDrop::new(output);
81        unsafe { Vec::from_raw_parts(md.as_mut_ptr() as *mut f32, md.len(), md.capacity()) }
82    }
83}
84
85unsafe fn scale_scalar(input: &[f32], output: *mut f32, scale: f32) {
86    for (i, &src) in input.iter().enumerate() {
87        output.add(i).write(src * scale);
88    }
89}
90
91unsafe fn parallel_scale_scalar(input: &[f32], output: *mut f32, scale: f32, worker_count: usize) {
92    let worker_count = worker_count.max(1);
93    // Thread fan-out can dominate runtime for moderate tensor sizes.
94    // Keep GPU fallback scalar in-process unless there is enough work.
95    const MIN_PARALLEL_LEN: usize = 65_536;
96    if worker_count <= 1 || input.len() < MIN_PARALLEL_LEN {
97        scale_scalar(input, output, scale);
98        return;
99    }
100
101    let chunk_size = input.len().div_ceil(worker_count).max(1);
102    std::thread::scope(|scope| {
103        for (i, in_chunk) in input.chunks(chunk_size).enumerate() {
104            let out_ptr = SendPtr(output.add(i * chunk_size));
105            scope.spawn(move || {
106                let p = out_ptr;
107                scale_scalar(in_chunk, p.0, scale);
108            });
109        }
110    });
111}
112
113#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
114unsafe fn scale_x86_256(input: &[f32], output: *mut f32, scale: f32) {
115    if std::is_x86_feature_detected!("avx2") {
116        scale_x86_256_impl(input, output, scale)
117    } else {
118        scale_scalar(input, output, scale)
119    }
120}
121
122#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
123unsafe fn scale_x86_256(input: &[f32], output: *mut f32, scale: f32) {
124    scale_scalar(input, output, scale)
125}
126
127#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
128unsafe fn scale_x86_512(input: &[f32], output: *mut f32, scale: f32) {
129    // Keep AVX-512 mode API-compatible on stable Rust by falling back to AVX2/scalar.
130    scale_x86_256(input, output, scale)
131}
132
133#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
134unsafe fn scale_x86_512(input: &[f32], output: *mut f32, scale: f32) {
135    scale_scalar(input, output, scale)
136}
137
138#[cfg(target_arch = "aarch64")]
139unsafe fn scale_neon(input: &[f32], output: *mut f32, scale: f32) {
140    scale_neon_impl(input, output, scale)
141}
142
143#[cfg(not(target_arch = "aarch64"))]
144unsafe fn scale_neon(input: &[f32], output: *mut f32, scale: f32) {
145    scale_scalar(input, output, scale)
146}
147
148#[cfg(target_arch = "x86_64")]
149#[target_feature(enable = "avx2")]
150unsafe fn scale_x86_256_impl(input: &[f32], output: *mut f32, scale: f32) {
151    use std::arch::x86_64::{_mm256_loadu_ps, _mm256_mul_ps, _mm256_set1_ps, _mm256_storeu_ps};
152
153    let scale_vec = _mm256_set1_ps(scale);
154    let mut index = 0usize;
155    // Unroll 4x to process 32 floats (four 256-bit AVX2 registers) per loop iteration.
156    // This allows modern CPU out-of-order execution pipelines to execute multiple independent
157    // vector multiplications in parallel, avoiding dependency stalls and loop overhead.
158    while index + 32 <= input.len() {
159        let input_vec0 = _mm256_loadu_ps(input.as_ptr().add(index));
160        let input_vec1 = _mm256_loadu_ps(input.as_ptr().add(index + 8));
161        let input_vec2 = _mm256_loadu_ps(input.as_ptr().add(index + 16));
162        let input_vec3 = _mm256_loadu_ps(input.as_ptr().add(index + 24));
163
164        let output_vec0 = _mm256_mul_ps(input_vec0, scale_vec);
165        let output_vec1 = _mm256_mul_ps(input_vec1, scale_vec);
166        let output_vec2 = _mm256_mul_ps(input_vec2, scale_vec);
167        let output_vec3 = _mm256_mul_ps(input_vec3, scale_vec);
168
169        _mm256_storeu_ps(output.add(index), output_vec0);
170        _mm256_storeu_ps(output.add(index + 8), output_vec1);
171        _mm256_storeu_ps(output.add(index + 16), output_vec2);
172        _mm256_storeu_ps(output.add(index + 24), output_vec3);
173
174        index += 32;
175    }
176    // Clean up remaining vectors of size 8
177    while index + 8 <= input.len() {
178        let input_vec = _mm256_loadu_ps(input.as_ptr().add(index));
179        let output_vec = _mm256_mul_ps(input_vec, scale_vec);
180        _mm256_storeu_ps(output.add(index), output_vec);
181        index += 8;
182    }
183    scale_scalar(&input[index..], output.add(index), scale);
184}
185
186#[cfg(target_arch = "x86")]
187#[target_feature(enable = "avx2")]
188unsafe fn scale_x86_256_impl(input: &[f32], output: *mut f32, scale: f32) {
189    use std::arch::x86::{_mm256_loadu_ps, _mm256_mul_ps, _mm256_set1_ps, _mm256_storeu_ps};
190
191    let scale_vec = _mm256_set1_ps(scale);
192    let mut index = 0usize;
193    // Unroll 4x to process 32 floats (four 256-bit AVX2 registers) per loop iteration.
194    // This allows modern CPU out-of-order execution pipelines to execute multiple independent
195    // vector multiplications in parallel, avoiding dependency stalls and loop overhead.
196    while index + 32 <= input.len() {
197        let input_vec0 = _mm256_loadu_ps(input.as_ptr().add(index));
198        let input_vec1 = _mm256_loadu_ps(input.as_ptr().add(index + 8));
199        let input_vec2 = _mm256_loadu_ps(input.as_ptr().add(index + 16));
200        let input_vec3 = _mm256_loadu_ps(input.as_ptr().add(index + 24));
201
202        let output_vec0 = _mm256_mul_ps(input_vec0, scale_vec);
203        let output_vec1 = _mm256_mul_ps(input_vec1, scale_vec);
204        let output_vec2 = _mm256_mul_ps(input_vec2, scale_vec);
205        let output_vec3 = _mm256_mul_ps(input_vec3, scale_vec);
206
207        _mm256_storeu_ps(output.add(index), output_vec0);
208        _mm256_storeu_ps(output.add(index + 8), output_vec1);
209        _mm256_storeu_ps(output.add(index + 16), output_vec2);
210        _mm256_storeu_ps(output.add(index + 24), output_vec3);
211
212        index += 32;
213    }
214    // Clean up remaining vectors of size 8
215    while index + 8 <= input.len() {
216        let input_vec = _mm256_loadu_ps(input.as_ptr().add(index));
217        let output_vec = _mm256_mul_ps(input_vec, scale_vec);
218        _mm256_storeu_ps(output.add(index), output_vec);
219        index += 8;
220    }
221    scale_scalar(&input[index..], output.add(index), scale);
222}
223
224#[cfg(target_arch = "aarch64")]
225unsafe fn scale_neon_impl(input: &[f32], output: *mut f32, scale: f32) {
226    use std::arch::aarch64::{vdupq_n_f32, vld1q_f32, vmulq_f32, vst1q_f32};
227
228    let scale_vec = vdupq_n_f32(scale);
229    let mut index = 0usize;
230    // Unroll 4x to process 16 floats (four 128-bit NEON registers) per loop iteration.
231    // This allows modern CPU out-of-order execution pipelines to execute multiple independent
232    // vector multiplications in parallel, avoiding dependency stalls and loop overhead.
233    while index + 16 <= input.len() {
234        let input_vec0 = vld1q_f32(input.as_ptr().add(index));
235        let input_vec1 = vld1q_f32(input.as_ptr().add(index + 4));
236        let input_vec2 = vld1q_f32(input.as_ptr().add(index + 8));
237        let input_vec3 = vld1q_f32(input.as_ptr().add(index + 12));
238
239        let output_vec0 = vmulq_f32(input_vec0, scale_vec);
240        let output_vec1 = vmulq_f32(input_vec1, scale_vec);
241        let output_vec2 = vmulq_f32(input_vec2, scale_vec);
242        let output_vec3 = vmulq_f32(input_vec3, scale_vec);
243
244        vst1q_f32(output.add(index), output_vec0);
245        vst1q_f32(output.add(index + 4), output_vec1);
246        vst1q_f32(output.add(index + 8), output_vec2);
247        vst1q_f32(output.add(index + 12), output_vec3);
248
249        index += 16;
250    }
251    // Clean up remaining vectors of size 4
252    while index + 4 <= input.len() {
253        let input_vec = vld1q_f32(input.as_ptr().add(index));
254        let output_vec = vmulq_f32(input_vec, scale_vec);
255        vst1q_f32(output.add(index), output_vec);
256        index += 4;
257    }
258    scale_scalar(&input[index..], output.add(index), scale);
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use crate::protocol::NodeResources;
265
266    #[test]
267    fn backend_selection_reflects_acceleration_mode() {
268        let profile = RuntimeProfile {
269            node_resources: NodeResources::new("node-a", 24.0, 64.0, "8.9", None),
270            logical_cores: 16,
271            recommended_workers: 8,
272            acceleration_mode: AccelerationMode::Gpu,
273            gpu_backend: crate::host::GpuBackend::Cuda,
274            xdp_supported: true,
275            detection_source: String::from("test"),
276            probe_mode: crate::host::ProbeMode::Fast,
277        };
278
279        let backend = ExecutionBackend::from_runtime_profile(&profile);
280        assert_eq!(backend.name(), "GPU");
281        assert_eq!(backend.vector_width_bits, 512);
282    }
283
284    #[test]
285    fn backend_executes_scaled_transform() {
286        let profile = RuntimeProfile {
287            node_resources: NodeResources::new("node-a", 0.0, 64.0, "cpu", None),
288            logical_cores: 8,
289            recommended_workers: 4,
290            acceleration_mode: AccelerationMode::Avx2,
291            gpu_backend: crate::host::GpuBackend::Cpu,
292            xdp_supported: true,
293            detection_source: String::from("test"),
294            probe_mode: crate::host::ProbeMode::Fast,
295        };
296
297        let backend = ExecutionBackend::from_runtime_profile(&profile);
298        let output = backend.scale_f32_slice(&[1.0, 2.0, 3.5], 2.0);
299        assert_eq!(output, vec![2.0, 4.0, 7.0]);
300    }
301}