use std::fmt;
use std::str::FromStr;
use crate::e40::BUCKETS;
pub const NEURAL_ENGINE_MAX_TOKENS: usize = 128;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum CoreMlComputeUnit {
All,
CpuAndNe,
CpuAndGpu,
CpuOnly,
}
impl CoreMlComputeUnit {
pub const fn name(self) -> &'static str {
match self {
Self::All => "all",
Self::CpuAndNe => "cpu_and_ne",
Self::CpuAndGpu => "cpu_and_gpu",
Self::CpuOnly => "cpu_only",
}
}
}
impl fmt::Display for CoreMlComputeUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("invalid Core ML compute units: {0}")]
pub struct CoreMlUnitsParseError(String);
impl FromStr for CoreMlComputeUnit {
type Err = CoreMlUnitsParseError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let key: String = text
.chars()
.filter(|c| !matches!(c, '-' | '_' | ' '))
.map(|c| c.to_ascii_lowercase())
.collect();
match key.as_str() {
"all" => Ok(Self::All),
"cpuandne" | "cpuandneuralengine" | "ne" => Ok(Self::CpuAndNe),
"cpuandgpu" | "gpu" => Ok(Self::CpuAndGpu),
"cpuonly" | "cpu" => Ok(Self::CpuOnly),
_ => Err(CoreMlUnitsParseError(format!(
"`{}` is not one of all, cpu_and_ne, cpu_and_gpu, cpu_only",
text.trim()
))),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CoreMlUnits {
by_length: [CoreMlComputeUnit; BUCKETS.len()],
}
impl CoreMlUnits {
pub fn per_length_default() -> Self {
let mut by_length = [CoreMlComputeUnit::CpuAndNe; BUCKETS.len()];
for (unit, length) in by_length.iter_mut().zip(BUCKETS) {
if length > NEURAL_ENGINE_MAX_TOKENS {
*unit = CoreMlComputeUnit::CpuAndGpu;
}
}
Self { by_length }
}
pub fn uniform(unit: CoreMlComputeUnit) -> Self {
Self {
by_length: [unit; BUCKETS.len()],
}
}
pub fn with_length(mut self, length: usize, unit: CoreMlComputeUnit) -> Option<Self> {
let index = BUCKETS.iter().position(|bucket| *bucket == length)?;
self.by_length[index] = unit;
Some(self)
}
pub fn unit_for(&self, length: usize) -> Option<CoreMlComputeUnit> {
BUCKETS
.iter()
.position(|bucket| *bucket == length)
.map(|index| self.by_length[index])
}
pub fn entries(&self) -> impl Iterator<Item = (usize, CoreMlComputeUnit)> + '_ {
BUCKETS.iter().copied().zip(self.by_length.iter().copied())
}
pub fn is_cpu_only(&self) -> bool {
self.by_length
.iter()
.all(|unit| *unit == CoreMlComputeUnit::CpuOnly)
}
}
impl From<CoreMlComputeUnit> for CoreMlUnits {
fn from(unit: CoreMlComputeUnit) -> Self {
Self::uniform(unit)
}
}
impl Default for CoreMlUnits {
fn default() -> Self {
Self::per_length_default()
}
}
impl fmt::Display for CoreMlUnits {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (position, (length, unit)) in self.entries().enumerate() {
if position > 0 {
f.write_str(",")?;
}
write!(f, "{length}={unit}")?;
}
Ok(())
}
}
impl FromStr for CoreMlUnits {
type Err = CoreMlUnitsParseError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let text = text.trim();
let fail = |why: String| CoreMlUnitsParseError(why);
if text.is_empty() {
return Err(fail("empty specification".into()));
}
let key = text.to_ascii_lowercase().replace(['-', ' '], "_");
if key == "default" || key == "per_length" {
return Ok(Self::per_length_default());
}
if !text.contains('=') {
return text.parse().map(Self::uniform);
}
let mut units = Self::per_length_default();
let mut seen = Vec::new();
for pair in text.split(',') {
let (length, unit) = pair
.split_once('=')
.ok_or_else(|| fail(format!("`{}` is not length=unit", pair.trim())))?;
let length: usize = length
.trim()
.parse()
.map_err(|_| fail(format!("`{}` is not a token length", length.trim())))?;
if seen.contains(&length) {
return Err(fail(format!("length {length} is given twice")));
}
seen.push(length);
units = units.with_length(length, unit.parse()?).ok_or_else(|| {
fail(format!(
"{length} is not a function length (one of {BUCKETS:?})"
))
})?;
}
Ok(units)
}
}
#[cfg(test)]
mod tests {
use super::*;
use CoreMlComputeUnit::{All, CpuAndGpu, CpuAndNe, CpuOnly};
#[test]
fn the_default_map_is_ne_up_to_128_and_gpu_above() {
let units = CoreMlUnits::per_length_default();
let entries: Vec<_> = units.entries().collect();
assert_eq!(
entries,
vec![
(32, CpuAndNe),
(64, CpuAndNe),
(128, CpuAndNe),
(256, CpuAndGpu),
(512, CpuAndGpu),
]
);
assert_eq!(units, CoreMlUnits::default());
assert_eq!(units.unit_for(128), Some(CpuAndNe));
assert_eq!(units.unit_for(256), Some(CpuAndGpu));
assert_eq!(units.unit_for(48), None, "embed_48 is not served");
assert!(!units.is_cpu_only());
}
#[test]
fn a_uniform_map_forces_one_unit() {
for unit in [All, CpuAndNe, CpuAndGpu, CpuOnly] {
let units = CoreMlUnits::uniform(unit);
assert!(units.entries().all(|(_, u)| u == unit));
}
assert!(CoreMlUnits::uniform(CpuOnly).is_cpu_only());
assert!(!CoreMlUnits::uniform(All).is_cpu_only());
}
#[test]
fn unit_names_parse_loosely_and_round_trip() {
for unit in [All, CpuAndNe, CpuAndGpu, CpuOnly] {
assert_eq!(unit.name().parse::<CoreMlComputeUnit>(), Ok(unit));
assert_eq!(unit.to_string(), unit.name());
}
assert_eq!("CPU-and-NE".parse::<CoreMlComputeUnit>(), Ok(CpuAndNe));
assert_eq!(" cpuAndGpu ".parse::<CoreMlComputeUnit>(), Ok(CpuAndGpu));
assert_eq!("cpu".parse::<CoreMlComputeUnit>(), Ok(CpuOnly));
assert!("ane".parse::<CoreMlComputeUnit>().is_err());
assert!("".parse::<CoreMlComputeUnit>().is_err());
}
#[test]
fn override_strings_parse() {
assert_eq!(
"default".parse::<CoreMlUnits>().unwrap(),
CoreMlUnits::per_length_default()
);
assert_eq!(
"cpu_only".parse::<CoreMlUnits>().unwrap(),
CoreMlUnits::uniform(CpuOnly)
);
assert_eq!(
"ALL".parse::<CoreMlUnits>().unwrap(),
CoreMlUnits::uniform(All)
);
let mixed: CoreMlUnits = "256=cpu_only, 32=all".parse().unwrap();
assert_eq!(mixed.unit_for(32), Some(All));
assert_eq!(
mixed.unit_for(64),
Some(CpuAndNe),
"unlisted keeps the default"
);
assert_eq!(mixed.unit_for(256), Some(CpuOnly));
assert_eq!(mixed.unit_for(512), Some(CpuAndGpu));
}
#[test]
fn bad_override_strings_are_refused() {
for bad in [
"",
" ",
"fast",
"48=all",
"256=fast",
"x=all",
"256",
"256=all,256=cpu_only",
"256=all,,",
"all,512=all",
] {
assert!(
bad.parse::<CoreMlUnits>().is_err(),
"`{bad}` must not parse"
);
}
}
#[test]
fn display_parses_back_to_the_same_map() {
let units = CoreMlUnits::per_length_default();
assert_eq!(
units.to_string(),
"32=cpu_and_ne,64=cpu_and_ne,128=cpu_and_ne,256=cpu_and_gpu,512=cpu_and_gpu"
);
assert_eq!(units.to_string().parse::<CoreMlUnits>().unwrap(), units);
let forced = CoreMlUnits::uniform(All);
assert_eq!(forced.to_string().parse::<CoreMlUnits>().unwrap(), forced);
}
}