1use std::sync::mpsc;
9use std::thread;
10
11use num_complex::Complex64;
12
13use crate::operator::LinearOperator;
14use crate::{QmbedError, Result};
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18#[non_exhaustive]
19pub enum Accelerator {
20 Cpu,
22 Gpu { device: usize },
24}
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31#[non_exhaustive]
32pub struct ExecutionProfile {
33 pub accelerator: Accelerator,
35 pub ranks: usize,
37 pub threads_per_rank: usize,
39}
40
41impl ExecutionProfile {
42 pub const fn serial() -> Self {
44 Self::local_cpu(1)
45 }
46
47 pub const fn throughput(threads: usize) -> Self {
49 Self::local_cpu(threads)
50 }
51
52 pub const fn local_cpu(threads: usize) -> Self {
54 Self {
55 accelerator: Accelerator::Cpu,
56 ranks: 1,
57 threads_per_rank: threads,
58 }
59 }
60
61 pub const fn distributed_cpu(ranks: usize, threads_per_rank: usize) -> Self {
66 Self {
67 accelerator: Accelerator::Cpu,
68 ranks,
69 threads_per_rank,
70 }
71 }
72
73 pub const fn local_gpu(device: usize, threads: usize) -> Self {
75 Self {
76 accelerator: Accelerator::Gpu { device },
77 ranks: 1,
78 threads_per_rank: threads,
79 }
80 }
81
82 pub fn validate(self) -> Result<Self> {
84 if self.ranks == 0 || self.threads_per_rank == 0 {
85 return Err(QmbedError::InvalidOptions(
86 "execution profiles require at least one rank and one thread per rank".into(),
87 ));
88 }
89 Ok(self)
90 }
91}
92
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95pub struct RuntimeCapabilities {
96 pub accelerator: Accelerator,
98 pub ranks: usize,
100 pub threads_per_rank: usize,
102}
103
104pub trait RuntimeBuffer: Send + Sync {
106 fn len(&self) -> usize;
108
109 fn is_empty(&self) -> bool {
111 self.len() == 0
112 }
113}
114
115pub trait Runtime: Send + Sync {
120 type Buffer: RuntimeBuffer;
122
123 fn capabilities(&self) -> RuntimeCapabilities;
125 fn zeros(&self, length: usize) -> Result<Self::Buffer>;
127 fn upload(&self, values: &[Complex64]) -> Result<Self::Buffer>;
129 fn to_host(&self, buffer: &Self::Buffer) -> Result<Vec<Complex64>>;
131 fn fill(&self, buffer: &mut Self::Buffer, value: Complex64) -> Result<()>;
133 fn axpy(&self, alpha: Complex64, input: &Self::Buffer, output: &mut Self::Buffer)
135 -> Result<()>;
136 fn scale(&self, alpha: Complex64, buffer: &mut Self::Buffer) -> Result<()>;
138 fn dotc(&self, left: &Self::Buffer, right: &Self::Buffer) -> Result<Complex64>;
140
141 fn norm(&self, buffer: &Self::Buffer) -> Result<f64> {
143 Ok(self.dotc(buffer, buffer)?.re.max(0.0).sqrt())
144 }
145}
146
147pub trait RuntimeLinearOperator<R>
149where
150 R: Runtime,
151{
152 fn runtime_shape(&self) -> (usize, usize);
154 fn apply_on(&self, runtime: &R, input: &R::Buffer, output: &mut R::Buffer) -> Result<()>;
156}
157
158pub trait RuntimeAdjointLinearOperator<R>: RuntimeLinearOperator<R>
165where
166 R: Runtime,
167{
168 fn apply_adjoint_on(
170 &self,
171 runtime: &R,
172 input: &R::Buffer,
173 output: &mut R::Buffer,
174 ) -> Result<()>;
175}
176
177#[derive(Clone, Debug, PartialEq)]
179pub struct CpuBuffer {
180 values: Vec<Complex64>,
181}
182
183impl CpuBuffer {
184 pub fn as_slice(&self) -> &[Complex64] {
186 &self.values
187 }
188
189 pub fn as_mut_slice(&mut self) -> &mut [Complex64] {
191 &mut self.values
192 }
193}
194
195impl RuntimeBuffer for CpuBuffer {
196 fn len(&self) -> usize {
197 self.values.len()
198 }
199}
200
201#[derive(Clone, Copy, Debug, Eq, PartialEq)]
203pub struct CpuRuntime {
204 profile: ExecutionProfile,
205}
206
207impl CpuRuntime {
208 pub fn new(threads: usize) -> Result<Self> {
210 Self::from_profile(ExecutionProfile::local_cpu(threads))
211 }
212
213 pub fn from_profile(profile: ExecutionProfile) -> Result<Self> {
218 let profile = profile.validate()?;
219 if profile.accelerator != Accelerator::Cpu {
220 return Err(QmbedError::UnsupportedBackend(
221 "the built-in runtime is CPU-only; install a GPU runtime implementation".into(),
222 ));
223 }
224 if profile.ranks != 1 {
225 return Err(QmbedError::UnsupportedBackend(
226 "the built-in runtime is single-rank; install an MPI runtime implementation".into(),
227 ));
228 }
229 Ok(Self { profile })
230 }
231
232 pub const fn profile(self) -> ExecutionProfile {
234 self.profile
235 }
236
237 pub fn map_ordered<T, U, F>(&self, items: &[T], operation: F) -> Result<Vec<U>>
242 where
243 T: Sync,
244 U: Send,
245 F: Fn(&T) -> Result<U> + Sync,
246 {
247 if items.is_empty() {
248 return Ok(Vec::new());
249 }
250 let workers = self.profile.threads_per_rank.min(items.len());
251 let mut ordered = (0..items.len()).map(|_| None).collect::<Vec<_>>();
252 thread::scope(|scope| {
253 let (sender, receiver) = mpsc::channel();
254 let mut handles = Vec::with_capacity(workers);
255 for worker in 0..workers {
256 let sender = sender.clone();
257 let operation = &operation;
258 handles.push(scope.spawn(move || {
259 for index in (worker..items.len()).step_by(workers) {
260 if sender.send((index, operation(&items[index]))).is_err() {
261 break;
262 }
263 }
264 }));
265 }
266 drop(sender);
267 for (index, result) in receiver {
268 ordered[index] = Some(result);
269 }
270 if handles.into_iter().any(|handle| handle.join().is_err()) {
271 return Err(QmbedError::UnsupportedBackend(
272 "a CPU runtime worker panicked".into(),
273 ));
274 }
275 let mut output = Vec::with_capacity(items.len());
276 for result in ordered {
277 let result = result.ok_or_else(|| {
278 QmbedError::UnsupportedBackend(
279 "a CPU runtime worker did not return a result".into(),
280 )
281 })?;
282 output.push(result?);
283 }
284 Ok(output)
285 })
286 }
287}
288
289impl Runtime for CpuRuntime {
290 type Buffer = CpuBuffer;
291
292 fn capabilities(&self) -> RuntimeCapabilities {
293 RuntimeCapabilities {
294 accelerator: Accelerator::Cpu,
295 ranks: 1,
296 threads_per_rank: self.profile.threads_per_rank,
297 }
298 }
299
300 fn zeros(&self, length: usize) -> Result<Self::Buffer> {
301 Ok(CpuBuffer {
302 values: vec![Complex64::new(0.0, 0.0); length],
303 })
304 }
305
306 fn upload(&self, values: &[Complex64]) -> Result<Self::Buffer> {
307 Ok(CpuBuffer {
308 values: values.to_vec(),
309 })
310 }
311
312 fn to_host(&self, buffer: &Self::Buffer) -> Result<Vec<Complex64>> {
313 Ok(buffer.values.clone())
314 }
315
316 fn fill(&self, buffer: &mut Self::Buffer, value: Complex64) -> Result<()> {
317 buffer.values.fill(value);
318 Ok(())
319 }
320
321 fn axpy(
322 &self,
323 alpha: Complex64,
324 input: &Self::Buffer,
325 output: &mut Self::Buffer,
326 ) -> Result<()> {
327 equal_lengths(input, output)?;
328 for (target, source) in output.values.iter_mut().zip(&input.values) {
329 *target += alpha * *source;
330 }
331 Ok(())
332 }
333
334 fn scale(&self, alpha: Complex64, buffer: &mut Self::Buffer) -> Result<()> {
335 for value in &mut buffer.values {
336 *value *= alpha;
337 }
338 Ok(())
339 }
340
341 fn dotc(&self, left: &Self::Buffer, right: &Self::Buffer) -> Result<Complex64> {
342 equal_lengths(left, right)?;
343 Ok(left
344 .values
345 .iter()
346 .zip(&right.values)
347 .map(|(left, right)| left.conj() * *right)
348 .sum())
349 }
350}
351
352impl<O> RuntimeLinearOperator<CpuRuntime> for O
353where
354 O: LinearOperator + ?Sized,
355{
356 fn runtime_shape(&self) -> (usize, usize) {
357 self.shape()
358 }
359
360 fn apply_on(
361 &self,
362 _runtime: &CpuRuntime,
363 input: &CpuBuffer,
364 output: &mut CpuBuffer,
365 ) -> Result<()> {
366 self.apply(input.as_slice(), output.as_mut_slice())
367 }
368}
369
370impl<O> RuntimeAdjointLinearOperator<CpuRuntime> for O
371where
372 O: LinearOperator + ?Sized,
373{
374 fn apply_adjoint_on(
375 &self,
376 _runtime: &CpuRuntime,
377 input: &CpuBuffer,
378 output: &mut CpuBuffer,
379 ) -> Result<()> {
380 self.apply_adjoint(input.as_slice(), output.as_mut_slice())
381 }
382}
383
384fn equal_lengths(left: &impl RuntimeBuffer, right: &impl RuntimeBuffer) -> Result<()> {
385 if left.len() != right.len() {
386 return Err(QmbedError::DimensionMismatch(format!(
387 "runtime vector lengths differ: {} and {}",
388 left.len(),
389 right.len()
390 )));
391 }
392 Ok(())
393}