#[cfg(test)]
mod tests;
use super::traits::{BackendCapabilities, BackendType, ComputeOp};
use crate::gpu::{detect_gpu, DetectionOptions};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SelectionStrategy {
PreferGpu,
PreferSimd,
#[default]
Automatic,
Threshold {
min_flops: u64,
},
}
impl SelectionStrategy {
#[must_use]
pub fn threshold(min_flops: u64) -> Self {
Self::Threshold { min_flops }
}
#[must_use]
pub fn description(&self) -> &str {
match self {
Self::PreferGpu => "prefer GPU",
Self::PreferSimd => "prefer SIMD",
Self::Automatic => "automatic",
Self::Threshold { .. } => "threshold-based",
}
}
}
#[derive(Debug, Clone)]
pub struct SelectorConfig {
pub strategy: SelectionStrategy,
pub gpu_options: DetectionOptions,
pub gpu_threshold_flops: u64,
pub max_gpu_memory: u64,
pub gpu_dispatch_overhead_us: u32,
}
impl Default for SelectorConfig {
fn default() -> Self {
Self {
strategy: SelectionStrategy::default(),
gpu_options: DetectionOptions::default(),
gpu_threshold_flops: 100_000, max_gpu_memory: 256 * 1024 * 1024, gpu_dispatch_overhead_us: 100, }
}
}
impl SelectorConfig {
#[must_use]
pub fn for_inference() -> Self {
Self {
strategy: SelectionStrategy::Automatic,
gpu_options: DetectionOptions::for_inference(),
gpu_threshold_flops: 1_000_000, max_gpu_memory: 1024 * 1024 * 1024, gpu_dispatch_overhead_us: 50,
}
}
#[must_use]
pub fn prefer_gpu() -> Self {
Self {
strategy: SelectionStrategy::PreferGpu,
..Default::default()
}
}
#[must_use]
pub fn prefer_simd() -> Self {
Self {
strategy: SelectionStrategy::PreferSimd,
..Default::default()
}
}
#[must_use]
pub fn with_strategy(mut self, strategy: SelectionStrategy) -> Self {
self.strategy = strategy;
self
}
#[must_use]
pub fn with_gpu_threshold(mut self, flops: u64) -> Self {
self.gpu_threshold_flops = flops;
self
}
#[must_use]
pub fn with_max_gpu_memory(mut self, bytes: u64) -> Self {
self.max_gpu_memory = bytes;
self
}
}
#[derive(Debug)]
pub struct BackendSelector {
config: SelectorConfig,
simd_caps: BackendCapabilities,
gpu_caps: Option<BackendCapabilities>,
gpu_available: bool,
}
impl BackendSelector {
#[must_use]
pub fn new(config: SelectorConfig) -> Self {
let simd_caps = BackendCapabilities::simd();
let gpu_result = detect_gpu(&config.gpu_options);
let gpu_caps = if gpu_result.available {
Some(BackendCapabilities::gpu(
true,
gpu_result.capabilities.limits.max_buffer_size,
gpu_result
.capabilities
.limits
.max_compute_invocations_per_workgroup,
gpu_result.capabilities.supports_f16,
))
} else {
None
};
Self {
config,
simd_caps,
gpu_available: gpu_result.available,
gpu_caps,
}
}
#[must_use]
pub fn default_config() -> Self {
Self::new(SelectorConfig::default())
}
#[must_use]
pub fn config(&self) -> &SelectorConfig {
&self.config
}
#[must_use]
pub fn gpu_available(&self) -> bool {
self.gpu_available
}
#[must_use]
pub fn simd_capabilities(&self) -> &BackendCapabilities {
&self.simd_caps
}
#[must_use]
pub fn gpu_capabilities(&self) -> Option<&BackendCapabilities> {
self.gpu_caps.as_ref()
}
pub fn select<O: ComputeOp>(&self, op: &O) -> BackendSelection {
let flops = op.estimated_flops();
let memory = op.memory_requirement() as u64;
match self.config.strategy {
SelectionStrategy::PreferGpu => {
if self.gpu_available && memory <= self.config.max_gpu_memory {
BackendSelection::gpu("PreferGpu strategy")
} else if self.gpu_available {
BackendSelection::simd("Memory exceeds GPU limit")
} else {
BackendSelection::simd("GPU not available")
}
}
SelectionStrategy::PreferSimd => BackendSelection::simd("PreferSimd strategy"),
SelectionStrategy::Threshold { min_flops } => {
if !self.gpu_available {
return BackendSelection::simd("GPU not available");
}
if memory > self.config.max_gpu_memory {
return BackendSelection::simd("Memory exceeds GPU limit");
}
if flops >= min_flops {
BackendSelection::gpu("FLOPs exceed threshold")
} else {
BackendSelection::simd("FLOPs below threshold")
}
}
SelectionStrategy::Automatic => self.select_automatic(flops, memory),
}
}
fn select_automatic(&self, flops: u64, memory: u64) -> BackendSelection {
if !self.gpu_available {
return BackendSelection::simd("GPU not available");
}
if memory > self.config.max_gpu_memory {
return BackendSelection::simd("Memory exceeds GPU limit");
}
let gpu_worthwhile = self.is_gpu_worthwhile(flops, memory);
if gpu_worthwhile {
BackendSelection::gpu("Large workload benefits from GPU")
} else {
BackendSelection::simd("Small workload better on CPU")
}
}
fn is_gpu_worthwhile(&self, flops: u64, memory: u64) -> bool {
if flops < self.config.gpu_threshold_flops {
return false;
}
if let Some(gpu_caps) = &self.gpu_caps {
if !gpu_caps.can_handle(memory) {
return false;
}
}
true
}
pub fn select_batch<O: ComputeOp>(&self, ops: &[O]) -> BackendSelection {
if ops.is_empty() {
return BackendSelection::simd("No operations");
}
let total_flops: u64 = ops.iter().map(|o| o.estimated_flops()).sum();
let max_memory: u64 = ops
.iter()
.map(|o| o.memory_requirement() as u64)
.max()
.unwrap_or(0);
self.select_automatic(total_flops, max_memory)
}
#[cfg(test)]
pub(crate) fn with_simulated_gpu(config: SelectorConfig, max_memory: u64) -> Self {
let simd_caps = BackendCapabilities::simd();
let gpu_caps = Some(BackendCapabilities::gpu(true, max_memory, 256, true));
Self {
config,
simd_caps,
gpu_available: true,
gpu_caps,
}
}
#[must_use]
pub fn summary(&self) -> String {
use std::fmt::Write;
let mut s = format!(
"Backend Selector ({})\n",
self.config.strategy.description()
);
let _ = writeln!(
s,
" SIMD: parallelism={}, score={:.1}",
self.simd_caps.max_parallelism, self.simd_caps.performance_score
);
if let Some(gpu) = &self.gpu_caps {
let _ = writeln!(
s,
" GPU: parallelism={}, score={:.1}, f16={}",
gpu.max_parallelism, gpu.performance_score, gpu.supports_f16
);
} else {
s.push_str(" GPU: not available\n");
}
s
}
}
#[derive(Debug, Clone)]
pub struct BackendSelection {
pub backend: BackendType,
pub reason: String,
}
impl BackendSelection {
#[must_use]
pub fn gpu(reason: impl Into<String>) -> Self {
Self {
backend: BackendType::Gpu,
reason: reason.into(),
}
}
#[must_use]
pub fn simd(reason: impl Into<String>) -> Self {
Self {
backend: BackendType::Simd,
reason: reason.into(),
}
}
#[must_use]
pub fn is_gpu(&self) -> bool {
self.backend.is_gpu()
}
#[must_use]
pub fn is_simd(&self) -> bool {
self.backend.is_cpu()
}
}
impl std::fmt::Display for BackendSelection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.backend, self.reason)
}
}