use std::ops::AddAssign;
use std::sync::{Arc, RwLock};
use ec_gpu::GpuName;
use ff::PrimeField;
use group::{prime::PrimeCurveAffine, Group};
use log::{error, info};
use rust_gpu_tools::{program_closures, Device, Program};
use yastl::Scope;
use crate::{
error::{EcError, EcResult},
threadpool::Worker,
};
const MAX_WINDOW_SIZE: usize = 10;
const LOCAL_WORK_SIZE: usize = 128;
const MEMORY_PADDING: f64 = 0.2f64;
const AMPERE: u32 = 8;
const fn div_ceil(a: usize, b: usize) -> usize {
if a % b == 0 {
a / b
} else {
(a / b) + 1
}
}
const fn work_units(compute_units: u32, compute_capabilities: Option<(u32, u32)>) -> usize {
match compute_capabilities {
Some((AMPERE, _)) => LOCAL_WORK_SIZE * compute_units as usize * 2,
_ => LOCAL_WORK_SIZE * compute_units as usize,
}
}
pub struct SingleMultiexpKernel<'a, G>
where
G: PrimeCurveAffine,
{
program: Program,
n: usize,
work_units: usize,
maybe_abort: Option<&'a (dyn Fn() -> bool + Send + Sync)>,
_phantom: std::marker::PhantomData<G::Scalar>,
}
fn calc_chunk_size<G>(mem: u64, work_units: usize) -> usize
where
G: PrimeCurveAffine,
G::Scalar: PrimeField,
{
let aff_size = std::mem::size_of::<G>();
let exp_size = exp_size::<G::Scalar>();
let proj_size = std::mem::size_of::<G::Curve>();
let max_memory = ((mem as f64) * (1f64 - MEMORY_PADDING)) as usize;
let term_size = aff_size + exp_size;
let max_buckets_per_work_unit = 1 << MAX_WINDOW_SIZE;
let buckets_size = work_units * max_buckets_per_work_unit * proj_size;
let results_size = work_units * proj_size;
(max_memory - buckets_size - results_size) / term_size
}
fn exp_size<F: PrimeField>() -> usize {
std::mem::size_of::<F::Repr>()
}
impl<'a, G> SingleMultiexpKernel<'a, G>
where
G: PrimeCurveAffine + GpuName,
{
pub fn create(
program: Program,
device: &Device,
maybe_abort: Option<&'a (dyn Fn() -> bool + Send + Sync)>,
) -> EcResult<Self> {
let mem = device.memory();
let compute_units = device.compute_units();
let compute_capability = device.compute_capability();
let work_units = work_units(compute_units, compute_capability);
let chunk_size = calc_chunk_size::<G>(mem, work_units);
Ok(SingleMultiexpKernel {
program,
n: chunk_size,
work_units,
maybe_abort,
_phantom: std::marker::PhantomData,
})
}
pub fn multiexp(
&self,
bases: &[G],
exponents: &[<G::Scalar as PrimeField>::Repr],
) -> EcResult<G::Curve> {
assert_eq!(bases.len(), exponents.len());
if let Some(maybe_abort) = &self.maybe_abort {
if maybe_abort() {
return Err(EcError::Aborted);
}
}
let window_size = self.calc_window_size(bases.len());
let num_windows = div_ceil(256, window_size);
let num_groups = self.work_units / num_windows;
let bucket_len = 1 << window_size;
let closures = program_closures!(|program, _arg| -> EcResult<Vec<G::Curve>> {
let base_buffer = program.create_buffer_from_slice(bases)?;
let exp_buffer = program.create_buffer_from_slice(exponents)?;
let bucket_buffer =
unsafe { program.create_buffer::<G::Curve>(self.work_units * bucket_len)? };
let result_buffer = unsafe { program.create_buffer::<G::Curve>(self.work_units)? };
let global_work_size = div_ceil(num_windows * num_groups, LOCAL_WORK_SIZE);
let kernel_name = format!("{}_multiexp", G::name());
let kernel = program.create_kernel(&kernel_name, global_work_size, LOCAL_WORK_SIZE)?;
kernel
.arg(&base_buffer)
.arg(&bucket_buffer)
.arg(&result_buffer)
.arg(&exp_buffer)
.arg(&(bases.len() as u32))
.arg(&(num_groups as u32))
.arg(&(num_windows as u32))
.arg(&(window_size as u32))
.run()?;
let mut results = vec![G::Curve::identity(); self.work_units];
program.read_into_buffer(&result_buffer, &mut results)?;
Ok(results)
});
let results = self.program.run(closures, ())?;
let mut acc = G::Curve::identity();
let mut bits = 0;
let exp_bits = exp_size::<G::Scalar>() * 8;
for i in 0..num_windows {
let w = std::cmp::min(window_size, exp_bits - bits);
for _ in 0..w {
acc = acc.double();
}
for g in 0..num_groups {
acc.add_assign(&results[g * num_windows + i]);
}
bits += w; }
Ok(acc)
}
fn calc_window_size(&self, num_terms: usize) -> usize {
let window_size = ((div_ceil(num_terms, self.work_units) as f64).log2() as usize) + 2;
std::cmp::min(window_size, MAX_WINDOW_SIZE)
}
}
pub struct MultiexpKernel<'a, G>
where
G: PrimeCurveAffine,
{
kernels: Vec<SingleMultiexpKernel<'a, G>>,
}
impl<'a, G> MultiexpKernel<'a, G>
where
G: PrimeCurveAffine + GpuName,
{
pub fn create(programs: Vec<Program>, devices: &[&Device]) -> EcResult<Self> {
Self::create_optional_abort(programs, devices, None)
}
pub fn create_with_abort(
programs: Vec<Program>,
devices: &[&Device],
maybe_abort: &'a (dyn Fn() -> bool + Send + Sync),
) -> EcResult<Self> {
Self::create_optional_abort(programs, devices, Some(maybe_abort))
}
fn create_optional_abort(
programs: Vec<Program>,
devices: &[&Device],
maybe_abort: Option<&'a (dyn Fn() -> bool + Send + Sync)>,
) -> EcResult<Self> {
let kernels: Vec<_> = programs
.into_iter()
.zip(devices.iter())
.filter_map(|(program, device)| {
let device_name = program.device_name().to_string();
let kernel = SingleMultiexpKernel::create(program, device, maybe_abort);
if let Err(ref e) = kernel {
error!(
"Cannot initialize kernel for device '{}'! Error: {}",
device_name, e
);
}
kernel.ok()
})
.collect();
if kernels.is_empty() {
return Err(EcError::Simple("No working GPUs found!"));
}
info!("Multiexp: {} working device(s) selected.", kernels.len());
for (i, k) in kernels.iter().enumerate() {
info!(
"Multiexp: Device {}: {} (Chunk-size: {})",
i,
k.program.device_name(),
k.n
);
}
Ok(MultiexpKernel { kernels })
}
pub fn parallel_multiexp<'s>(
&'s mut self,
scope: &Scope<'s>,
bases: &'s [G],
exps: &'s [<G::Scalar as PrimeField>::Repr],
results: &'s mut [G::Curve],
error: Arc<RwLock<EcResult<()>>>,
) {
let num_devices = self.kernels.len();
let num_exps = exps.len();
let chunk_size = ((num_exps as f64) / (num_devices as f64)).ceil() as usize;
for (((bases, exps), kern), result) in bases
.chunks(chunk_size)
.zip(exps.chunks(chunk_size))
.zip(self.kernels.iter_mut())
.zip(results.iter_mut())
{
let error = error.clone();
scope.execute(move || {
let mut acc = G::Curve::identity();
for (bases, exps) in bases.chunks(kern.n).zip(exps.chunks(kern.n)) {
if error.read().unwrap().is_err() {
break;
}
match kern.multiexp(bases, exps) {
Ok(result) => acc.add_assign(&result),
Err(e) => {
*error.write().unwrap() = Err(e);
break;
}
}
}
if error.read().unwrap().is_ok() {
*result = acc;
}
});
}
}
pub fn multiexp(
&mut self,
pool: &Worker,
bases_arc: Arc<Vec<G>>,
exps: Arc<Vec<<G::Scalar as PrimeField>::Repr>>,
skip: usize,
) -> EcResult<G::Curve> {
let bases = &bases_arc[skip..(skip + exps.len())];
let exps = &exps[..];
let mut results = Vec::new();
let error = Arc::new(RwLock::new(Ok(())));
pool.scoped(|s| {
results = vec![G::Curve::identity(); self.kernels.len()];
self.parallel_multiexp(s, bases, exps, &mut results, error.clone());
});
Arc::try_unwrap(error)
.expect("only one ref left")
.into_inner()
.unwrap()?;
let mut acc = G::Curve::identity();
for r in results {
acc.add_assign(&r);
}
Ok(acc)
}
pub fn num_kernels(&self) -> usize {
self.kernels.len()
}
}