Skip to main content

bittensor_core/pow/
opencl.rs

1//! Minimal dynamically loaded OpenCL 1.2 host. Kernels use bounded buffers and
2//! one in-order queue per device; every answer is recomputed on the CPU.
3
4use super::{seal, validate_batch, Solution, PREFIX_LEN};
5use libloading::Library;
6use sp_core::U256;
7use std::{
8    ffi::{c_char, c_void},
9    ptr,
10    sync::Arc,
11};
12
13type Handle = *mut c_void;
14type Status = i32;
15type Notify = Option<unsafe extern "C" fn(*const c_char, *const c_void, usize, *mut c_void)>;
16
17macro_rules! opencl_api {
18    ($( $name:ident : $ty:ty ),* $(,)?) => {
19        #[allow(non_snake_case)]
20        struct Api {
21            $( $name: $ty, )*
22            // Function pointers must never outlive the loaded library.
23            _library: Library,
24        }
25        impl Api {
26            fn load() -> Result<Arc<Self>, String> {
27                #[cfg(target_os = "windows")]
28                let names = ["OpenCL.dll"];
29                #[cfg(target_os = "macos")]
30                let names = ["/System/Library/Frameworks/OpenCL.framework/OpenCL"];
31                #[cfg(not(any(target_os = "windows", target_os = "macos")))]
32                let names = ["libOpenCL.so.1", "libOpenCL.so"];
33                let mut last = String::new();
34                for name in names {
35                    // SAFETY: load the vendor's system OpenCL runtime. Typed
36                    // symbols match the OpenCL 1.2 C ABI and library is retained.
37                    match unsafe { Library::new(name) } {
38                        Ok(library) => {
39                            return unsafe {
40                                Ok(Arc::new(Self {
41                                    $( $name: *library.get::<$ty>(concat!(stringify!($name), "\0").as_bytes())
42                                        .map_err(|error| format!("OpenCL symbol: {error}"))?, )*
43                                    _library: library,
44                                }))
45                            };
46                        }
47                        Err(error) => last = error.to_string(),
48                    }
49                }
50                Err(format!("OpenCL GPU runtime unavailable: {last}"))
51            }
52        }
53    };
54}
55
56opencl_api! {
57    clGetPlatformIDs: unsafe extern "C" fn(u32, *mut Handle, *mut u32) -> Status,
58    clGetDeviceIDs: unsafe extern "C" fn(Handle, u64, u32, *mut Handle, *mut u32) -> Status,
59    clGetDeviceInfo: unsafe extern "C" fn(Handle, u32, usize, *mut c_void, *mut usize) -> Status,
60    clCreateContext: unsafe extern "C" fn(*const isize, u32, *const Handle, Notify, *mut c_void, *mut Status) -> Handle,
61    clCreateCommandQueue: unsafe extern "C" fn(Handle, Handle, u64, *mut Status) -> Handle,
62    clCreateProgramWithSource: unsafe extern "C" fn(Handle, u32, *const *const c_char, *const usize, *mut Status) -> Handle,
63    clBuildProgram: unsafe extern "C" fn(Handle, u32, *const Handle, *const c_char, Option<unsafe extern "C" fn(Handle, *mut c_void)>, *mut c_void) -> Status,
64    clGetProgramBuildInfo: unsafe extern "C" fn(Handle, Handle, u32, usize, *mut c_void, *mut usize) -> Status,
65    clCreateKernel: unsafe extern "C" fn(Handle, *const c_char, *mut Status) -> Handle,
66    clCreateBuffer: unsafe extern "C" fn(Handle, u64, usize, *mut c_void, *mut Status) -> Handle,
67    clSetKernelArg: unsafe extern "C" fn(Handle, u32, usize, *const c_void) -> Status,
68    clEnqueueWriteBuffer: unsafe extern "C" fn(Handle, Handle, u32, usize, usize, *const c_void, u32, *const Handle, *mut Handle) -> Status,
69    clEnqueueReadBuffer: unsafe extern "C" fn(Handle, Handle, u32, usize, usize, *mut c_void, u32, *const Handle, *mut Handle) -> Status,
70    clEnqueueNDRangeKernel: unsafe extern "C" fn(Handle, Handle, u32, *const usize, *const usize, *const usize, u32, *const Handle, *mut Handle) -> Status,
71    clFlush: unsafe extern "C" fn(Handle) -> Status,
72    clFinish: unsafe extern "C" fn(Handle) -> Status,
73    clReleaseMemObject: unsafe extern "C" fn(Handle) -> Status,
74    clReleaseKernel: unsafe extern "C" fn(Handle) -> Status,
75    clReleaseProgram: unsafe extern "C" fn(Handle) -> Status,
76    clReleaseCommandQueue: unsafe extern "C" fn(Handle) -> Status,
77    clReleaseContext: unsafe extern "C" fn(Handle) -> Status,
78}
79
80fn check(status: Status, operation: &str) -> Result<(), String> {
81    if status == 0 {
82        Ok(())
83    } else {
84        Err(format!("OpenCL {operation} failed ({status})"))
85    }
86}
87
88#[derive(Debug, Clone)]
89pub struct GpuDevice {
90    pub id: usize,
91    pub name: String,
92    pub vendor: String,
93}
94
95fn device_text(api: &Api, device: Handle, param: u32) -> Result<String, String> {
96    let mut size = 0;
97    // SAFETY: size query and a bounded allocation for the OpenCL device string.
98    unsafe {
99        check(
100            (api.clGetDeviceInfo)(device, param, 0, ptr::null_mut(), &mut size),
101            "device info size",
102        )?;
103        if size == 0 || size > 65_536 {
104            return Err("invalid OpenCL device info size".into());
105        }
106        let mut bytes = vec![0u8; size];
107        check(
108            (api.clGetDeviceInfo)(
109                device,
110                param,
111                size,
112                bytes.as_mut_ptr().cast(),
113                ptr::null_mut(),
114            ),
115            "device info",
116        )?;
117        Ok(String::from_utf8_lossy(&bytes)
118            .trim_end_matches('\0')
119            .to_owned())
120    }
121}
122
123fn discover(api: &Api) -> Result<Vec<(Handle, GpuDevice)>, String> {
124    let mut count = 0;
125    // SAFETY: OpenCL writes at most the requested number of handles into the
126    // allocated vectors. Platform/device handles belong to the loaded runtime.
127    unsafe {
128        let status = (api.clGetPlatformIDs)(0, ptr::null_mut(), &mut count);
129        if status == -1001 {
130            return Ok(Vec::new());
131        }
132        check(status, "platform discovery")?;
133        if count > 64 {
134            return Err("too many OpenCL platforms".into());
135        }
136        if count == 0 {
137            return Ok(Vec::new());
138        }
139        let mut platforms = vec![ptr::null_mut(); count as usize];
140        check(
141            (api.clGetPlatformIDs)(count, platforms.as_mut_ptr(), ptr::null_mut()),
142            "platform discovery",
143        )?;
144        let mut result = Vec::new();
145        for platform in platforms {
146            let mut n = 0;
147            let status = (api.clGetDeviceIDs)(platform, 4, 0, ptr::null_mut(), &mut n);
148            if status == -1 {
149                continue;
150            } // CL_DEVICE_NOT_FOUND
151            check(status, "GPU discovery")?;
152            if n > 64 {
153                return Err("too many OpenCL GPUs on one platform".into());
154            }
155            if n == 0 {
156                continue;
157            }
158            let mut devices = vec![ptr::null_mut(); n as usize];
159            check(
160                (api.clGetDeviceIDs)(platform, 4, n, devices.as_mut_ptr(), ptr::null_mut()),
161                "GPU discovery",
162            )?;
163            for device in devices {
164                let id = result.len();
165                result.push((
166                    device,
167                    GpuDevice {
168                        id,
169                        name: device_text(api, device, 0x102b)?,
170                        vendor: device_text(api, device, 0x102c)?,
171                    },
172                ));
173            }
174        }
175        Ok(result)
176    }
177}
178
179/// Discovery returns an empty list when the driver/runtime is absent. Explicit
180/// miner construction returns a descriptive error instead of silently using CPU.
181pub fn gpu_devices() -> Result<Vec<GpuDevice>, String> {
182    let api = match Api::load() {
183        Ok(api) => api,
184        Err(_) => return Ok(Vec::new()),
185    };
186    Ok(discover(&api)?.into_iter().map(|(_, info)| info).collect())
187}
188
189struct Worker {
190    api: Arc<Api>,
191    info: GpuDevice,
192    context: Handle,
193    queue: Handle,
194    program: Handle,
195    kernel: Handle,
196    vector_kernel: Handle,
197    buffers: [Handle; 5],
198}
199
200// SAFETY: each worker owns its context, in-order queue and buffers. The public
201// miner requires &mut self for work, so no queue/resource is accessed concurrently.
202unsafe impl Send for Worker {}
203
204impl Worker {
205    fn new(api: Arc<Api>, device: Handle, info: GpuDevice) -> Result<Self, String> {
206        let mut worker = Self {
207            api,
208            info,
209            context: ptr::null_mut(),
210            queue: ptr::null_mut(),
211            program: ptr::null_mut(),
212            kernel: ptr::null_mut(),
213            vector_kernel: ptr::null_mut(),
214            buffers: [ptr::null_mut(); 5],
215        };
216        let mut status = 0;
217        let source = include_str!("kernel.cl");
218        let source_ptr = source.as_ptr().cast::<c_char>();
219        let length = source.len();
220        // SAFETY: handles are retained in Worker and released even on partial
221        // initialization failure. All source strings and lengths remain valid.
222        unsafe {
223            worker.context = (worker.api.clCreateContext)(
224                ptr::null(),
225                1,
226                &device,
227                None,
228                ptr::null_mut(),
229                &mut status,
230            );
231            check(status, "context")?;
232            worker.queue =
233                (worker.api.clCreateCommandQueue)(worker.context, device, 0, &mut status);
234            check(status, "queue")?;
235            worker.program = (worker.api.clCreateProgramWithSource)(
236                worker.context,
237                1,
238                &source_ptr,
239                &length,
240                &mut status,
241            );
242            check(status, "program")?;
243            let mut status = (worker.api.clBuildProgram)(
244                worker.program,
245                1,
246                &device,
247                c"-cl-std=CL1.2".as_ptr(),
248                None,
249                ptr::null_mut(),
250            );
251            if status != 0 {
252                let mut size = 0;
253                (worker.api.clGetProgramBuildInfo)(
254                    worker.program,
255                    device,
256                    0x1183,
257                    0,
258                    ptr::null_mut(),
259                    &mut size,
260                );
261                let mut bytes = vec![0u8; size.min(65_536)];
262                if !bytes.is_empty() {
263                    (worker.api.clGetProgramBuildInfo)(
264                        worker.program,
265                        device,
266                        0x1183,
267                        bytes.len(),
268                        bytes.as_mut_ptr().cast(),
269                        ptr::null_mut(),
270                    );
271                }
272                return Err(format!(
273                    "OpenCL kernel build failed ({status}): {}",
274                    String::from_utf8_lossy(&bytes)
275                ));
276            }
277            worker.kernel =
278                (worker.api.clCreateKernel)(worker.program, c"mine".as_ptr(), &mut status);
279            check(status, "kernel")?;
280            worker.vector_kernel =
281                (worker.api.clCreateKernel)(worker.program, c"seal_vector".as_ptr(), &mut status);
282            check(status, "vector kernel")?;
283            for (buffer, size) in worker.buffers.iter_mut().zip([PREFIX_LEN, 32, 4, 8, 32]) {
284                *buffer = (worker.api.clCreateBuffer)(
285                    worker.context,
286                    1,
287                    size,
288                    ptr::null_mut(),
289                    &mut status,
290                );
291                check(status, "buffer")?;
292            }
293        }
294        worker.self_test()?;
295        Ok(worker)
296    }
297
298    fn self_test(&self) -> Result<(), String> {
299        let mut prefix = b"subtensor-pow-register-v1".to_vec();
300        prefix.extend_from_slice(&1u16.to_le_bytes());
301        prefix.extend_from_slice(&[1; 32]);
302        prefix.extend_from_slice(&[2; 32]);
303        prefix.extend_from_slice(&[3; 32]);
304        for nonce in [0u64, 1, u64::MAX] {
305            self.write(self.buffers[0], &prefix)?;
306            // SAFETY: each argument is copied into the retained vector kernel.
307            // The one-workitem kernel writes exactly 32 bytes to its output.
308            unsafe {
309                check(
310                    (self.api.clSetKernelArg)(
311                        self.vector_kernel,
312                        0,
313                        std::mem::size_of::<Handle>(),
314                        (&self.buffers[0] as *const Handle).cast(),
315                    ),
316                    "vector prefix",
317                )?;
318                check(
319                    (self.api.clSetKernelArg)(
320                        self.vector_kernel,
321                        1,
322                        8,
323                        (&nonce as *const u64).cast(),
324                    ),
325                    "vector nonce",
326                )?;
327                check(
328                    (self.api.clSetKernelArg)(
329                        self.vector_kernel,
330                        2,
331                        std::mem::size_of::<Handle>(),
332                        (&self.buffers[4] as *const Handle).cast(),
333                    ),
334                    "vector output",
335                )?;
336                let global = 1usize;
337                check(
338                    (self.api.clEnqueueNDRangeKernel)(
339                        self.queue,
340                        self.vector_kernel,
341                        1,
342                        ptr::null(),
343                        &global,
344                        ptr::null(),
345                        0,
346                        ptr::null(),
347                        ptr::null_mut(),
348                    ),
349                    "vector launch",
350                )?;
351            }
352            let mut gpu = [0u8; 32];
353            self.read(self.buffers[4], &mut gpu)?;
354            if gpu != seal(&prefix, nonce)? {
355                return Err(format!(
356                    "OpenCL device {} failed registration hash self-test",
357                    self.info.name
358                ));
359            }
360        }
361        Ok(())
362    }
363
364    fn write(&self, buffer: Handle, bytes: &[u8]) -> Result<(), String> {
365        // SAFETY: buffers were allocated to fixed matching sizes, blocking write
366        // completes before the byte slice can leave scope.
367        unsafe {
368            check(
369                (self.api.clEnqueueWriteBuffer)(
370                    self.queue,
371                    buffer,
372                    1,
373                    0,
374                    bytes.len(),
375                    bytes.as_ptr().cast(),
376                    0,
377                    ptr::null(),
378                    ptr::null_mut(),
379                ),
380                "write",
381            )
382        }
383    }
384    fn read(&self, buffer: Handle, bytes: &mut [u8]) -> Result<(), String> {
385        // SAFETY: blocking reads into live, correctly sized host buffers.
386        unsafe {
387            check(
388                (self.api.clEnqueueReadBuffer)(
389                    self.queue,
390                    buffer,
391                    1,
392                    0,
393                    bytes.len(),
394                    bytes.as_mut_ptr().cast(),
395                    0,
396                    ptr::null(),
397                    ptr::null_mut(),
398                ),
399                "read",
400            )
401        }
402    }
403    fn arg<T>(&self, index: u32, value: &T) -> Result<(), String> {
404        // SAFETY: OpenCL copies each argument's bytes before returning.
405        unsafe {
406            check(
407                (self.api.clSetKernelArg)(
408                    self.kernel,
409                    index,
410                    std::mem::size_of::<T>(),
411                    (value as *const T).cast(),
412                ),
413                "kernel argument",
414            )
415        }
416    }
417    fn launch(
418        &self,
419        prefix: &[u8],
420        limit: &[u8; 32],
421        start: u64,
422        attempts: u32,
423    ) -> Result<(), String> {
424        self.write(self.buffers[0], prefix)?;
425        self.write(self.buffers[1], limit)?;
426        self.write(self.buffers[2], &0u32.to_ne_bytes())?;
427        self.write(self.buffers[3], &0u64.to_ne_bytes())?;
428        self.arg(0, &self.buffers[0])?;
429        self.arg(1, &self.buffers[1])?;
430        self.arg(2, &start)?;
431        self.arg(3, &attempts)?;
432        self.arg(4, &self.buffers[2])?;
433        self.arg(5, &self.buffers[3])?;
434        let global = (attempts as usize).min(65_536);
435        // SAFETY: kernel arguments and buffers have been initialized; the
436        // runtime chooses a compatible local workgroup size.
437        unsafe {
438            check(
439                (self.api.clEnqueueNDRangeKernel)(
440                    self.queue,
441                    self.kernel,
442                    1,
443                    ptr::null(),
444                    &global,
445                    ptr::null(),
446                    0,
447                    ptr::null(),
448                    ptr::null_mut(),
449                ),
450                "launch",
451            )?;
452            check((self.api.clFlush)(self.queue), "flush")
453        }
454    }
455    fn answer(&self) -> Result<Option<u64>, String> {
456        let mut found = [0u8; 4];
457        self.read(self.buffers[2], &mut found)?;
458        if u32::from_ne_bytes(found) == 0 {
459            return Ok(None);
460        }
461        let mut nonce = [0u8; 8];
462        self.read(self.buffers[3], &mut nonce)?;
463        Ok(Some(u64::from_ne_bytes(nonce)))
464    }
465}
466
467impl Drop for Worker {
468    fn drop(&mut self) {
469        // SAFETY: release only successfully created handles. Finishing the
470        // queue ensures outstanding kernels no longer reference freed buffers.
471        unsafe {
472            if !self.queue.is_null() {
473                (self.api.clFinish)(self.queue);
474            }
475            for buffer in self.buffers {
476                if !buffer.is_null() {
477                    (self.api.clReleaseMemObject)(buffer);
478                }
479            }
480            if !self.vector_kernel.is_null() {
481                (self.api.clReleaseKernel)(self.vector_kernel);
482            }
483            if !self.kernel.is_null() {
484                (self.api.clReleaseKernel)(self.kernel);
485            }
486            if !self.program.is_null() {
487                (self.api.clReleaseProgram)(self.program);
488            }
489            if !self.queue.is_null() {
490                (self.api.clReleaseCommandQueue)(self.queue);
491            }
492            if !self.context.is_null() {
493                (self.api.clReleaseContext)(self.context);
494            }
495        }
496    }
497}
498
499/// Persistent per-device contexts. Every device receives a disjoint nonce
500/// interval in each bounded batch, and launches are flushed before results wait.
501pub struct GpuMiner {
502    workers: Vec<Worker>,
503}
504
505impl GpuMiner {
506    pub fn new(device_ids: Option<&[usize]>) -> Result<Self, String> {
507        let api = Api::load()?;
508        let devices = discover(&api)?;
509        if devices.is_empty() {
510            return Err("no usable OpenCL GPUs found; install the GPU vendor's OpenCL driver or select CPU mining".into());
511        }
512        if let Some(ids) = device_ids {
513            if ids.is_empty() || ids.len() > 32 {
514                return Err("select 1..32 GPU devices".into());
515            }
516            let mut unique = ids.to_vec();
517            unique.sort_unstable();
518            unique.dedup();
519            if unique.len() != ids.len() || ids.iter().any(|id| *id >= devices.len()) {
520                return Err("GPU device ids must be unique discovered device ids".into());
521            }
522        } else if devices.len() > 32 {
523            return Err("more than 32 GPUs found; select at most 32".into());
524        }
525        let mut workers = Vec::new();
526        for (device, info) in devices {
527            if device_ids.is_none_or(|ids| ids.contains(&info.id)) {
528                workers.push(Worker::new(Arc::clone(&api), device, info)?);
529            }
530        }
531        Ok(Self { workers })
532    }
533    pub fn devices(&self) -> Vec<GpuDevice> {
534        self.workers
535            .iter()
536            .map(|worker| worker.info.clone())
537            .collect()
538    }
539
540    pub fn mine(
541        &mut self,
542        prefix: &[u8],
543        difficulty: u64,
544        start: u64,
545        attempts_per_device: u32,
546    ) -> Result<Option<Solution>, String> {
547        let limit = validate_batch(prefix, difficulty, attempts_per_device)?;
548        let bytes = limit.to_little_endian();
549        for (index, worker) in self.workers.iter().enumerate() {
550            let offset = (index as u64).wrapping_mul(u64::from(attempts_per_device));
551            worker.launch(
552                prefix,
553                &bytes,
554                start.wrapping_add(offset),
555                attempts_per_device,
556            )?;
557        }
558        let mut solution = None;
559        // Drain every queue before returning, including when another GPU won.
560        for (index, worker) in self.workers.iter().enumerate() {
561            if let Some(nonce) = worker.answer()? {
562                let device_start =
563                    start.wrapping_add((index as u64).wrapping_mul(u64::from(attempts_per_device)));
564                if nonce.wrapping_sub(device_start) >= u64::from(attempts_per_device) {
565                    return Err("GPU returned a nonce outside its assigned range".into());
566                }
567                let work = seal(prefix, nonce)?;
568                if U256::from_little_endian(&work) > limit {
569                    return Err("GPU returned an invalid registration proof".into());
570                }
571                if solution.is_none() {
572                    solution = Some((nonce, work));
573                }
574            }
575        }
576        Ok(solution)
577    }
578}