1use 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#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct ExecutionBackend {
17 pub mode: AccelerationMode,
19 pub worker_count: usize,
21 pub preferred_batch_size: usize,
23 pub vector_width_bits: usize,
25}
26
27impl ExecutionBackend {
28 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 pub const fn name(&self) -> &'static str {
48 self.mode.as_str()
49 }
50
51 pub fn scale_f32_slice(&self, input: &[f32], scale: f32) -> Vec<f32> {
53 if input.is_empty() {
54 return Vec::new();
55 }
56
57 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 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 let (chunks, remainder) = input.as_chunks::<8>();
90 let mut out_ptr = output;
91
92 for chunk in chunks {
93 let v0 = chunk[0] * scale;
94 let v1 = chunk[1] * scale;
95 let v2 = chunk[2] * scale;
96 let v3 = chunk[3] * scale;
97 let v4 = chunk[4] * scale;
98 let v5 = chunk[5] * scale;
99 let v6 = chunk[6] * scale;
100 let v7 = chunk[7] * scale;
101
102 out_ptr.write(v0);
103 out_ptr.add(1).write(v1);
104 out_ptr.add(2).write(v2);
105 out_ptr.add(3).write(v3);
106 out_ptr.add(4).write(v4);
107 out_ptr.add(5).write(v5);
108 out_ptr.add(6).write(v6);
109 out_ptr.add(7).write(v7);
110
111 out_ptr = out_ptr.add(8);
112 }
113
114 for (i, &src) in remainder.iter().enumerate() {
115 out_ptr.add(i).write(src * scale);
116 }
117}
118
119unsafe fn scale_vectorized(input: &[f32], output: *mut f32, scale: f32) {
120 #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
121 {
122 scale_x86_256(input, output, scale);
123 }
124 #[cfg(target_arch = "aarch64")]
125 {
126 scale_neon(input, output, scale);
127 }
128 #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))]
129 {
130 scale_scalar(input, output, scale);
131 }
132}
133
134unsafe fn parallel_scale_scalar(input: &[f32], output: *mut f32, scale: f32, worker_count: usize) {
135 let worker_count = worker_count.max(1);
136 const MIN_PARALLEL_LEN: usize = 65_536;
139 if worker_count <= 1 || input.len() < MIN_PARALLEL_LEN {
140 scale_vectorized(input, output, scale);
141 return;
142 }
143
144 let chunk_size = input.len().div_ceil(worker_count).max(1);
145 std::thread::scope(|scope| {
146 for (i, in_chunk) in input.chunks(chunk_size).enumerate() {
147 let out_ptr = SendPtr(output.add(i * chunk_size));
148 scope.spawn(move || {
149 let p = out_ptr;
150 scale_vectorized(in_chunk, p.0, scale);
151 });
152 }
153 });
154}
155
156#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
157unsafe fn scale_x86_256(input: &[f32], output: *mut f32, scale: f32) {
158 if std::is_x86_feature_detected!("avx2") {
159 scale_x86_256_impl(input, output, scale)
160 } else {
161 scale_scalar(input, output, scale)
162 }
163}
164
165#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
166unsafe fn scale_x86_256(input: &[f32], output: *mut f32, scale: f32) {
167 scale_scalar(input, output, scale)
168}
169
170#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
171unsafe fn scale_x86_512(input: &[f32], output: *mut f32, scale: f32) {
172 scale_x86_256(input, output, scale)
174}
175
176#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
177unsafe fn scale_x86_512(input: &[f32], output: *mut f32, scale: f32) {
178 scale_scalar(input, output, scale)
179}
180
181#[cfg(target_arch = "aarch64")]
182unsafe fn scale_neon(input: &[f32], output: *mut f32, scale: f32) {
183 scale_neon_impl(input, output, scale)
184}
185
186#[cfg(not(target_arch = "aarch64"))]
187unsafe fn scale_neon(input: &[f32], output: *mut f32, scale: f32) {
188 scale_scalar(input, output, scale)
189}
190
191#[cfg(target_arch = "x86_64")]
192#[target_feature(enable = "avx2")]
193unsafe fn scale_x86_256_impl(input: &[f32], output: *mut f32, scale: f32) {
194 use std::arch::x86_64::{_mm256_loadu_ps, _mm256_mul_ps, _mm256_set1_ps, _mm256_storeu_ps};
195
196 let scale_vec = _mm256_set1_ps(scale);
197 let mut index = 0usize;
198 while index + 32 <= input.len() {
202 let input_vec0 = _mm256_loadu_ps(input.as_ptr().add(index));
203 let input_vec1 = _mm256_loadu_ps(input.as_ptr().add(index + 8));
204 let input_vec2 = _mm256_loadu_ps(input.as_ptr().add(index + 16));
205 let input_vec3 = _mm256_loadu_ps(input.as_ptr().add(index + 24));
206
207 let output_vec0 = _mm256_mul_ps(input_vec0, scale_vec);
208 let output_vec1 = _mm256_mul_ps(input_vec1, scale_vec);
209 let output_vec2 = _mm256_mul_ps(input_vec2, scale_vec);
210 let output_vec3 = _mm256_mul_ps(input_vec3, scale_vec);
211
212 _mm256_storeu_ps(output.add(index), output_vec0);
213 _mm256_storeu_ps(output.add(index + 8), output_vec1);
214 _mm256_storeu_ps(output.add(index + 16), output_vec2);
215 _mm256_storeu_ps(output.add(index + 24), output_vec3);
216
217 index += 32;
218 }
219 while index + 8 <= input.len() {
221 let input_vec = _mm256_loadu_ps(input.as_ptr().add(index));
222 let output_vec = _mm256_mul_ps(input_vec, scale_vec);
223 _mm256_storeu_ps(output.add(index), output_vec);
224 index += 8;
225 }
226 scale_scalar(&input[index..], output.add(index), scale);
227}
228
229#[cfg(target_arch = "x86")]
230#[target_feature(enable = "avx2")]
231unsafe fn scale_x86_256_impl(input: &[f32], output: *mut f32, scale: f32) {
232 use std::arch::x86::{_mm256_loadu_ps, _mm256_mul_ps, _mm256_set1_ps, _mm256_storeu_ps};
233
234 let scale_vec = _mm256_set1_ps(scale);
235 let mut index = 0usize;
236 while index + 32 <= input.len() {
240 let input_vec0 = _mm256_loadu_ps(input.as_ptr().add(index));
241 let input_vec1 = _mm256_loadu_ps(input.as_ptr().add(index + 8));
242 let input_vec2 = _mm256_loadu_ps(input.as_ptr().add(index + 16));
243 let input_vec3 = _mm256_loadu_ps(input.as_ptr().add(index + 24));
244
245 let output_vec0 = _mm256_mul_ps(input_vec0, scale_vec);
246 let output_vec1 = _mm256_mul_ps(input_vec1, scale_vec);
247 let output_vec2 = _mm256_mul_ps(input_vec2, scale_vec);
248 let output_vec3 = _mm256_mul_ps(input_vec3, scale_vec);
249
250 _mm256_storeu_ps(output.add(index), output_vec0);
251 _mm256_storeu_ps(output.add(index + 8), output_vec1);
252 _mm256_storeu_ps(output.add(index + 16), output_vec2);
253 _mm256_storeu_ps(output.add(index + 24), output_vec3);
254
255 index += 32;
256 }
257 while index + 8 <= input.len() {
259 let input_vec = _mm256_loadu_ps(input.as_ptr().add(index));
260 let output_vec = _mm256_mul_ps(input_vec, scale_vec);
261 _mm256_storeu_ps(output.add(index), output_vec);
262 index += 8;
263 }
264 scale_scalar(&input[index..], output.add(index), scale);
265}
266
267#[cfg(target_arch = "aarch64")]
268unsafe fn scale_neon_impl(input: &[f32], output: *mut f32, scale: f32) {
269 use std::arch::aarch64::{vdupq_n_f32, vld1q_f32, vmulq_f32, vst1q_f32};
270
271 let scale_vec = vdupq_n_f32(scale);
272 let mut index = 0usize;
273 while index + 16 <= input.len() {
277 let input_vec0 = vld1q_f32(input.as_ptr().add(index));
278 let input_vec1 = vld1q_f32(input.as_ptr().add(index + 4));
279 let input_vec2 = vld1q_f32(input.as_ptr().add(index + 8));
280 let input_vec3 = vld1q_f32(input.as_ptr().add(index + 12));
281
282 let output_vec0 = vmulq_f32(input_vec0, scale_vec);
283 let output_vec1 = vmulq_f32(input_vec1, scale_vec);
284 let output_vec2 = vmulq_f32(input_vec2, scale_vec);
285 let output_vec3 = vmulq_f32(input_vec3, scale_vec);
286
287 vst1q_f32(output.add(index), output_vec0);
288 vst1q_f32(output.add(index + 4), output_vec1);
289 vst1q_f32(output.add(index + 8), output_vec2);
290 vst1q_f32(output.add(index + 12), output_vec3);
291
292 index += 16;
293 }
294 while index + 4 <= input.len() {
296 let input_vec = vld1q_f32(input.as_ptr().add(index));
297 let output_vec = vmulq_f32(input_vec, scale_vec);
298 vst1q_f32(output.add(index), output_vec);
299 index += 4;
300 }
301 scale_scalar(&input[index..], output.add(index), scale);
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use crate::protocol::NodeResources;
308
309 #[test]
310 fn backend_selection_reflects_acceleration_mode() {
311 let profile = RuntimeProfile {
312 node_resources: NodeResources::new("node-a", 24.0, 64.0, "8.9", None),
313 logical_cores: 16,
314 recommended_workers: 8,
315 acceleration_mode: AccelerationMode::Gpu,
316 gpu_backend: crate::host::GpuBackend::Cuda,
317 xdp_supported: true,
318 detection_source: String::from("test"),
319 probe_mode: crate::host::ProbeMode::Fast,
320 };
321
322 let backend = ExecutionBackend::from_runtime_profile(&profile);
323 assert_eq!(backend.name(), "GPU");
324 assert_eq!(backend.vector_width_bits, 512);
325 }
326
327 #[test]
328 fn backend_executes_scaled_transform() {
329 let profile = RuntimeProfile {
330 node_resources: NodeResources::new("node-a", 0.0, 64.0, "cpu", None),
331 logical_cores: 8,
332 recommended_workers: 4,
333 acceleration_mode: AccelerationMode::Avx2,
334 gpu_backend: crate::host::GpuBackend::Cpu,
335 xdp_supported: true,
336 detection_source: String::from("test"),
337 probe_mode: crate::host::ProbeMode::Fast,
338 };
339
340 let backend = ExecutionBackend::from_runtime_profile(&profile);
341 let output = backend.scale_f32_slice(&[1.0, 2.0, 3.5], 2.0);
342 assert_eq!(output, vec![2.0, 4.0, 7.0]);
343 }
344}