1use 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 _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 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 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 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 } 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
179pub 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
200unsafe 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 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 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 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 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 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 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 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
499pub 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 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}