1use std::collections::HashMap;
12use std::sync::{Arc, Mutex};
13
14use crate::gpu::{GpuBufferImpl, GpuCompilerImpl, GpuContextImpl, GpuError, GpuKernelImpl};
15
16mod ffi;
17mod memory_pool;
18
19use ffi::{ClBuffer, ClContext, ClKernel, ClProgram, ClQueue};
20use memory_pool::OpenCLMemoryPool;
21
22#[allow(dead_code)]
24const ADAM_KERNEL_OPENCL: &str = r#"
25__kernel void adam_update_f32(
26 __global float* params, __global const float* grads, __global float* m, __global float* v,
27 const float lr,
28 const float beta1,
29 const float beta2,
30 const float eps,
31 const float weight_decay,
32 const float bias_correction1,
33 const float bias_correction2,
34 const int n
35) {
36 const int idx = get_global_id(0);
37
38 if (idx < n) {
39 float grad = grads[idx];
40
41 // Apply weight decay
42 if (weight_decay > 0.0f) {
43 grad += weight_decay * params[idx];
44 }
45
46 // Update biased first moment estimate
47 m[idx] = beta1 * m[idx] + (1.0f - beta1) * grad;
48
49 // Update biased second raw moment estimate
50 v[idx] = beta2 * v[idx] + (1.0f - beta2) * grad * grad;
51
52 // Compute bias-corrected moment estimates
53 float m_hat = m[idx] / bias_correction1;
54 float v_hat = v[idx] / bias_correction2;
55
56 // Update parameters
57 params[idx] -= lr * m_hat / (sqrt(v_hat) + eps);
58 }
59}
60"#;
61
62#[allow(dead_code)]
63const GEMM_KERNEL_OPENCL: &str = r#"
64__kernel void gemm_f32(
65 __global const float* A, __global const float* B, __global float* C,
66 const int M,
67 const int N,
68 const int K,
69 const float alpha,
70 const float beta
71) {
72 const int row = get_global_id(0);
73 const int col = get_global_id(1);
74
75 if (row < M && col < N) {
76 float sum = 0.0f;
77 for (int k = 0; k < K; k++) {
78 sum += A[row * K + k] * B[k * N + col];
79 }
80 C[row * N + col] = alpha * sum + beta * C[row * N + col];
81 }
82}
83"#;
84
85pub struct OpenCLContext {
87 device: ffi::cl_device_id,
89 context: Arc<ClContext>,
90 queue: Arc<ClQueue>,
91 compiled_kernels: Arc<Mutex<HashMap<String, OpenCLKernel>>>,
92 memory_pool: Arc<Mutex<OpenCLMemoryPool>>,
93}
94
95unsafe impl Send for OpenCLContext {}
101unsafe impl Sync for OpenCLContext {}
102
103impl OpenCLContext {
104 pub fn new() -> Result<Self, GpuError> {
106 let api = ffi::api().ok_or_else(|| GpuError::BackendNotAvailable("OpenCL".to_string()))?;
107
108 let platforms = api.platform_ids()?;
109 if platforms.is_empty() {
110 return Err(GpuError::Other("No OpenCL platforms found".to_string()));
111 }
112
113 let device_ids = api.device_ids(ffi::CL_DEVICE_TYPE_GPU)?;
114 if device_ids.is_empty() {
115 return Err(GpuError::Other("No OpenCL GPU devices found".to_string()));
116 }
117 let device = device_ids[0];
118
119 let context = ClContext(api.create_context(device)?);
120 let queue = ClQueue(api.create_command_queue(context.0, device)?);
121
122 Ok(Self {
123 device,
124 context: Arc::new(context),
125 queue: Arc::new(queue),
126 compiled_kernels: Arc::new(Mutex::new(HashMap::new())),
127 memory_pool: Arc::new(Mutex::new(OpenCLMemoryPool::new(1024 * 1024 * 1024))), })
129 }
130
131 pub fn is_available() -> bool {
133 ffi::api().is_some_and(|api| {
134 api.device_ids(ffi::CL_DEVICE_TYPE_GPU)
135 .is_ok_and(|devices| !devices.is_empty())
136 })
137 }
138
139 fn compile_kernel_internal(&self, source: &str, name: &str) -> Result<OpenCLKernel, GpuError> {
141 let api = ffi::api().ok_or_else(|| GpuError::BackendNotAvailable("OpenCL".to_string()))?;
142
143 let program = ClProgram(api.build_program(self.context.0, self.device, source)?);
144 let kernel = ClKernel(api.create_kernel(program.0, name)?);
145
146 Ok(OpenCLKernel {
147 program,
148 kernel,
149 queue: Arc::clone(&self.queue),
150 name: name.to_string(),
151 })
152 }
153
154 pub fn allocate_device_memory(&self, size: usize) -> Result<ClBuffer, GpuError> {
156 let api = ffi::api().ok_or_else(|| GpuError::BackendNotAvailable("OpenCL".to_string()))?;
157 let mem = api.create_buffer(self.context.0, ffi::CL_MEM_READ_WRITE, size)?;
158 Ok(ClBuffer { mem, size })
159 }
160}
161
162impl GpuContextImpl for OpenCLContext {
163 fn create_buffer(&self, size: usize) -> Arc<dyn GpuBufferImpl> {
164 if let Ok(mut pool) = self.memory_pool.lock() {
166 if let Some(buffer) = pool.allocate(size) {
167 return Arc::new(OpenCLBuffer {
168 buffer: Some(buffer),
169 queue: Arc::clone(&self.queue),
170 size,
171 memory_pool: Arc::clone(&self.memory_pool),
172 });
173 }
174 }
175
176 match self.allocate_device_memory(size) {
178 Ok(buffer) => Arc::new(OpenCLBuffer {
179 buffer: Some(buffer),
180 queue: Arc::clone(&self.queue),
181 size,
182 memory_pool: Arc::clone(&self.memory_pool),
183 }),
184 Err(e) => {
185 eprintln!(
187 "Warning: OpenCL buffer allocation failed ({e}), creating CPU fallback buffer"
188 );
189 Arc::new(OpenCLCpuFallbackBuffer {
190 data: vec![0u8; size],
191 size,
192 memory_pool: Arc::clone(&self.memory_pool),
193 })
194 }
195 }
196 }
197
198 fn create_compiler(&self) -> Arc<dyn GpuCompilerImpl> {
199 Arc::new(OpenCLCompiler {
200 context: Arc::new(OpenCLContext {
201 device: self.device,
202 context: Arc::clone(&self.context),
203 queue: Arc::clone(&self.queue),
204 compiled_kernels: Arc::clone(&self.compiled_kernels),
205 memory_pool: Arc::clone(&self.memory_pool),
206 }),
207 })
208 }
209}
210
211struct OpenCLKernel {
213 #[allow(dead_code)]
216 program: ClProgram,
217 kernel: ClKernel,
218 queue: Arc<ClQueue>,
219 #[allow(dead_code)]
220 name: String,
221}
222
223struct OpenCLCompiler {
225 context: Arc<OpenCLContext>,
226}
227
228impl GpuCompilerImpl for OpenCLCompiler {
229 fn compile(&self, source: &str) -> Result<Arc<dyn GpuKernelImpl>, GpuError> {
230 let kernel = self.context.compile_kernel_internal(source, "kernel")?;
231 let name = kernel.name.clone();
232 if let Ok(mut kernels) = self.context.compiled_kernels.lock() {
234 kernels.insert(name.clone(), kernel);
235 }
236 Ok(Arc::new(OpenCLKernelHandle {
237 kernel_name: name,
238 compiled_kernels: Arc::clone(&self.context.compiled_kernels),
239 params: Arc::new(Mutex::new(Vec::new())),
240 }))
241 }
242
243 fn compile_typed(
244 &self,
245 name: &str,
246 _input_type: std::any::TypeId,
247 _output_type: std::any::TypeId,
248 ) -> Result<Arc<dyn GpuKernelImpl>, GpuError> {
249 Err(GpuError::KernelCompilationError(format!(
258 "compile_typed has no generated OpenCL source for kernel '{name}'; use \
259 GpuCompiler::compile with real OpenCL C source, or GpuContext::get_kernel for \
260 registry-backed named kernels"
261 )))
262 }
263}
264
265struct OpenCLKernelHandle {
267 kernel_name: String,
268 compiled_kernels: Arc<Mutex<HashMap<String, OpenCLKernel>>>,
269 params: Arc<Mutex<Vec<(String, KernelParam)>>>,
272}
273
274enum KernelParam {
275 Buffer(Arc<dyn GpuBufferImpl>),
276 U32(u32),
277 I32(i32),
278 F32(f32),
279 F64(f64),
280}
281
282impl OpenCLKernelHandle {
283 fn set_param(&self, name: &str, param: KernelParam) {
285 if let Ok(mut params) = self.params.lock() {
286 if let Some(slot) = params.iter_mut().find(|(n, _)| n == name) {
287 slot.1 = param;
288 } else {
289 params.push((name.to_string(), param));
290 }
291 }
292 }
293}
294
295impl GpuKernelImpl for OpenCLKernelHandle {
296 fn set_buffer(&self, name: &str, buffer: &Arc<dyn GpuBufferImpl>) {
297 self.set_param(name, KernelParam::Buffer(Arc::clone(buffer)));
298 }
299
300 fn set_u32(&self, name: &str, value: u32) {
301 self.set_param(name, KernelParam::U32(value));
302 }
303
304 fn set_i32(&self, name: &str, value: i32) {
305 self.set_param(name, KernelParam::I32(value));
306 }
307
308 fn set_f32(&self, name: &str, value: f32) {
309 self.set_param(name, KernelParam::F32(value));
310 }
311
312 fn set_f64(&self, name: &str, value: f64) {
313 self.set_param(name, KernelParam::F64(value));
314 }
315
316 fn dispatch(&self, workgroups: [u32; 3]) {
317 let Some(api) = ffi::api() else {
320 return;
321 };
322 let Ok(kernels) = self.compiled_kernels.lock() else {
323 return;
324 };
325 let Some(kernel) = kernels.get(&self.kernel_name) else {
326 return;
327 };
328 let Ok(params) = self.params.lock() else {
329 return;
330 };
331
332 let kernel_handle = kernel.kernel.0;
333
334 for (index, (_name, param)) in params.iter().enumerate() {
336 let index = index as ffi::cl_uint;
337 let bind = match param {
338 KernelParam::Buffer(buffer) => {
339 match buffer.as_any().downcast_ref::<OpenCLBuffer>() {
340 Some(cl_buffer) => match cl_buffer.mem_handle() {
341 Some(mem) => api.set_arg_mem(kernel_handle, index, &mem),
344 None => Ok(()),
345 },
346 None => Ok(()),
348 }
349 }
350 KernelParam::U32(value) => {
351 api.set_arg_bytes(kernel_handle, index, &value.to_ne_bytes())
352 }
353 KernelParam::I32(value) => {
354 api.set_arg_bytes(kernel_handle, index, &value.to_ne_bytes())
355 }
356 KernelParam::F32(value) => {
357 api.set_arg_bytes(kernel_handle, index, &value.to_ne_bytes())
358 }
359 KernelParam::F64(value) => {
360 api.set_arg_bytes(kernel_handle, index, &value.to_ne_bytes())
361 }
362 };
363 if bind.is_err() {
364 return;
365 }
366 }
367
368 let global = [workgroups[0] as usize];
370 let local = [64usize];
371 if api
372 .enqueue_nd_range(kernel.queue.0, kernel_handle, &global, Some(&local))
373 .is_err()
374 {
375 return;
376 }
377 let _ = api.finish(kernel.queue.0);
378 }
379}
380
381struct OpenCLBuffer {
383 buffer: Option<ClBuffer>,
385 queue: Arc<ClQueue>,
386 size: usize,
387 memory_pool: Arc<Mutex<OpenCLMemoryPool>>,
388}
389
390impl OpenCLBuffer {
391 fn mem_handle(&self) -> Option<ffi::cl_mem> {
393 self.buffer.as_ref().map(|b| b.mem)
394 }
395}
396
397impl GpuBufferImpl for OpenCLBuffer {
398 fn size(&self) -> usize {
399 self.size
400 }
401
402 unsafe fn copy_from_host(&self, data: *const u8, size: usize) {
403 if size > self.size {
404 return;
405 }
406 let Some(api) = ffi::api() else {
407 return;
408 };
409 let Some(buffer) = self.buffer.as_ref() else {
410 return;
411 };
412 let data_slice = std::slice::from_raw_parts(data, size);
413 if let Err(e) = api.enqueue_write(self.queue.0, buffer.mem, 0, data_slice) {
414 eprintln!("Warning: OpenCL write buffer failed: {e}");
415 }
416 }
417
418 unsafe fn copy_to_host(&self, data: *mut u8, size: usize) {
419 if size > self.size {
420 return;
421 }
422 let Some(api) = ffi::api() else {
423 return;
424 };
425 let Some(buffer) = self.buffer.as_ref() else {
426 return;
427 };
428 let data_slice = std::slice::from_raw_parts_mut(data, size);
429 if let Err(e) = api.enqueue_read(self.queue.0, buffer.mem, 0, data_slice) {
430 eprintln!("Warning: OpenCL read buffer failed: {e}");
431 }
432 }
433
434 fn as_any(&self) -> &dyn std::any::Any {
435 self
436 }
437}
438
439impl Drop for OpenCLBuffer {
440 fn drop(&mut self) {
441 if let Some(buffer) = self.buffer.take() {
444 if let Ok(mut pool) = self.memory_pool.lock() {
445 pool.deallocate(buffer);
446 }
447 }
448 }
449}
450
451struct OpenCLCpuFallbackBuffer {
454 data: Vec<u8>,
455 size: usize,
456 #[allow(dead_code)]
457 memory_pool: Arc<Mutex<OpenCLMemoryPool>>,
458}
459
460impl GpuBufferImpl for OpenCLCpuFallbackBuffer {
461 fn size(&self) -> usize {
462 self.size
463 }
464
465 unsafe fn copy_from_host(&self, data: *const u8, size: usize) {
466 if size > self.size {
467 eprintln!("Warning: OpenCL CPU fallback buffer copy_from_host size mismatch");
468 return;
469 }
470
471 let _data_slice = std::slice::from_raw_parts(data, size);
473 eprintln!("Warning: CPU fallback buffer copy_from_host called (size: {size})");
476 }
477
478 unsafe fn copy_to_host(&self, data: *mut u8, size: usize) {
479 if size > self.size {
480 eprintln!("Warning: OpenCL CPU fallback buffer copy_to_host size mismatch");
481 return;
482 }
483
484 let data_slice = std::slice::from_raw_parts_mut(data, size);
486 let copy_size = size.min(self.data.len());
487 data_slice[..copy_size].copy_from_slice(&self.data[..copy_size]);
488
489 eprintln!("Warning: CPU fallback buffer copy_to_host called (size: {size})");
490 }
491
492 fn device_ptr(&self) -> u64 {
493 self.data.as_ptr() as u64
494 }
495
496 fn as_any(&self) -> &dyn std::any::Any {
497 self
498 }
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504
505 #[test]
510 fn ffi_api_probe_does_not_panic_and_is_stable() {
511 let available = ffi::api().is_some();
512 println!("OpenCL ICD loadable: {available}");
513 assert_eq!(available, ffi::api().is_some());
514 }
515
516 #[test]
519 fn is_available_does_not_panic() {
520 let available = OpenCLContext::is_available();
521 println!("OpenCLContext::is_available() = {available}");
522 }
523
524 #[test]
527 fn context_new_degrades_gracefully() {
528 match OpenCLContext::new() {
529 Ok(_ctx) => println!("OpenCL context created (GPU device present)"),
530 Err(e) => println!("OpenCL unavailable, graceful error: {e}"),
531 }
532 }
533
534 #[test]
537 fn gpu_context_opencl_degrades_gracefully() {
538 use crate::gpu::{GpuBackend, GpuContext};
539
540 let _ = GpuBackend::OpenCL.is_available();
542
543 match GpuContext::new(GpuBackend::OpenCL) {
546 Ok(_ctx) => println!("GpuContext(OpenCL) created"),
547 Err(e) => println!("GpuContext(OpenCL) graceful error: {e}"),
548 }
549 }
550}