use objc2::runtime::Sel;
use objc2_core_ml::{MLModelConfiguration, MLOptimizationHints, MLSpecializationStrategy};
use objc2_foundation::NSString;
use crate::{ComputeUnits, LoadError};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FunctionName<'a>(&'a str);
impl<'a> FunctionName<'a> {
#[inline]
pub const fn new(name: &'a str) -> Option<Self> {
if name.is_empty() {
None
} else {
Some(Self(name))
}
}
#[inline]
pub const fn as_str(&self) -> &'a str {
self.0
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, derive_more::Display)]
#[display("{}", self.as_str())]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub enum Specialization {
#[default]
Default,
FastPrediction,
}
impl Specialization {
#[inline]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Default => "default",
Self::FastPrediction => "fast_prediction",
}
}
#[inline]
const fn to_raw(self) -> MLSpecializationStrategy {
match self {
Self::Default => MLSpecializationStrategy::Default,
Self::FastPrediction => MLSpecializationStrategy::FastPrediction,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, derive_more::Display)]
#[display("{}", self.as_str())]
#[non_exhaustive]
pub enum LoadOption {
LowPrecisionGpuAccumulation,
Specialization,
}
impl LoadOption {
#[inline]
pub const fn as_str(&self) -> &'static str {
match self {
Self::LowPrecisionGpuAccumulation => "low_precision_gpu_accumulation",
Self::Specialization => "specialization",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
pub struct LoadOptions {
compute: ComputeUnits,
#[cfg_attr(feature = "serde", serde(default))]
low_precision_gpu_accumulation: bool,
#[cfg_attr(feature = "serde", serde(default))]
specialization: Specialization,
}
impl LoadOptions {
#[inline]
pub const fn new(compute: ComputeUnits) -> Self {
Self {
compute,
low_precision_gpu_accumulation: false,
specialization: Specialization::Default,
}
}
#[inline]
pub const fn compute(&self) -> ComputeUnits {
self.compute
}
#[must_use]
#[inline]
pub const fn with_compute(mut self, compute: ComputeUnits) -> Self {
self.set_compute(compute);
self
}
#[inline]
pub const fn set_compute(&mut self, compute: ComputeUnits) -> &mut Self {
self.compute = compute;
self
}
#[inline]
pub const fn low_precision_gpu_accumulation(&self) -> bool {
self.low_precision_gpu_accumulation
}
#[must_use]
#[inline]
pub const fn with_low_precision_gpu_accumulation(mut self, allow: bool) -> Self {
self.set_low_precision_gpu_accumulation(allow);
self
}
#[inline]
pub const fn set_low_precision_gpu_accumulation(&mut self, allow: bool) -> &mut Self {
self.low_precision_gpu_accumulation = allow;
self
}
#[inline]
pub const fn specialization(&self) -> Specialization {
self.specialization
}
#[must_use]
#[inline]
pub const fn with_specialization(mut self, specialization: Specialization) -> Self {
self.set_specialization(specialization);
self
}
#[inline]
pub const fn set_specialization(&mut self, specialization: Specialization) -> &mut Self {
self.specialization = specialization;
self
}
}
impl core::fmt::Display for LoadOptions {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"compute={},low_precision_gpu_accumulation={},specialization={}",
self.compute, self.low_precision_gpu_accumulation, self.specialization
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Receiver {
Configuration,
OptimizationHints,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Sends<'a> {
function: Option<FunctionName<'a>>,
low_precision_gpu_accumulation: bool,
specialization: Option<Specialization>,
}
#[cfg(test)]
impl<'a> Sends<'a> {
const fn function(&self) -> Option<FunctionName<'a>> {
self.function
}
const fn low_precision_gpu_accumulation(&self) -> bool {
self.low_precision_gpu_accumulation
}
const fn specialization(&self) -> Option<Specialization> {
self.specialization
}
}
fn plan_sends<'a>(
function: Option<FunctionName<'a>>,
options: &LoadOptions,
mut responds: impl FnMut(Receiver, Sel) -> bool,
) -> Result<Sends<'a>, LoadError> {
if function.is_some() && !responds(Receiver::Configuration, objc2::sel!(setFunctionName:)) {
return Err(LoadError::FunctionsUnsupported);
}
let low_precision_gpu_accumulation = options.low_precision_gpu_accumulation();
if low_precision_gpu_accumulation
&& !responds(
Receiver::Configuration,
objc2::sel!(setAllowLowPrecisionAccumulationOnGPU:),
)
{
return Err(LoadError::OptionUnsupported(
LoadOption::LowPrecisionGpuAccumulation,
));
}
let specialization = match options.specialization() {
Specialization::Default => None,
chosen @ Specialization::FastPrediction => {
if !responds(Receiver::Configuration, objc2::sel!(setOptimizationHints:))
|| !responds(
Receiver::OptimizationHints,
objc2::sel!(setSpecializationStrategy:),
)
{
return Err(LoadError::OptionUnsupported(LoadOption::Specialization));
}
Some(chosen)
}
};
Ok(Sends {
function,
low_precision_gpu_accumulation,
specialization,
})
}
pub(super) fn configure(
configuration: &MLModelConfiguration,
function: Option<FunctionName<'_>>,
options: &LoadOptions,
) -> Result<(), LoadError> {
use objc2::{ClassType, runtime::NSObjectProtocol};
let sends = plan_sends(function, options, |receiver, selector| match receiver {
Receiver::Configuration => configuration.respondsToSelector(selector),
Receiver::OptimizationHints => MLOptimizationHints::class().responds_to(selector),
})?;
unsafe { apply(configuration, &sends) };
Ok(())
}
unsafe fn apply(configuration: &MLModelConfiguration, sends: &Sends<'_>) {
if let Some(function) = sends.function {
let name = NSString::from_str(function.as_str());
unsafe { configuration.setFunctionName(Some(&name)) };
}
if sends.low_precision_gpu_accumulation {
unsafe { configuration.setAllowLowPrecisionAccumulationOnGPU(true) };
}
if let Some(specialization) = sends.specialization {
let hints = unsafe { MLOptimizationHints::new() };
unsafe { hints.setSpecializationStrategy(specialization.to_raw()) };
unsafe { configuration.setOptimizationHints(&hints) };
}
}
#[cfg(test)]
mod tests;