use std::{f64::consts::PI, fmt};
use sim_kernel::Symbol;
use sim_lib_numbers_tensor::{
TensorExecutorCard, add_op_symbol, cos_op_symbol, div_op_symbol, exp_op_symbol, mul_op_symbol,
sin_op_symbol, sqrt_op_symbol, sub_op_symbol,
};
pub const REQUIRED_TENSOR_OPERATION_NAMES: [&str; 8] = [
"tensor/op/add",
"tensor/op/sub",
"tensor/op/mul",
"tensor/op/div",
"tensor/op/sqrt",
"tensor/op/exp",
"tensor/op/sin",
"tensor/op/cos",
];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PreflightCheck {
Configuration,
Operator,
DType,
Shape,
Singularity,
ForwardPlane,
Denominator,
ResultAllocation,
PhaseBudget,
GeometryBudget,
RoundoffBudget,
Constant,
Execution,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LoweringError {
check: PreflightCheck,
detail: String,
}
impl LoweringError {
pub(crate) fn new(check: PreflightCheck, detail: impl Into<String>) -> Self {
Self {
check,
detail: detail.into(),
}
}
pub fn check(&self) -> PreflightCheck {
self.check
}
pub fn detail(&self) -> &str {
&self.detail
}
}
impl fmt::Display for LoweringError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{:?} preflight: {}", self.check, self.detail)
}
}
impl std::error::Error for LoweringError {}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PhaseBudget {
pub max_abs_residual_phase_rad: f64,
pub max_predicted_geometry_error_rad: f64,
pub max_predicted_roundoff_error_rad: f64,
}
impl PhaseBudget {
pub fn new(
max_abs_residual_phase_rad: f64,
max_predicted_geometry_error_rad: f64,
max_predicted_roundoff_error_rad: f64,
) -> Result<Self, LoweringError> {
let budget = Self {
max_abs_residual_phase_rad,
max_predicted_geometry_error_rad,
max_predicted_roundoff_error_rad,
};
budget.validate()?;
Ok(budget)
}
pub(crate) fn validate(self) -> Result<(), LoweringError> {
require_finite_positive(
"max_abs_residual_phase_rad",
self.max_abs_residual_phase_rad,
)?;
if self.max_abs_residual_phase_rad > PI {
return Err(LoweringError::new(
PreflightCheck::Configuration,
format!(
"max_abs_residual_phase_rad {} exceeds pi",
self.max_abs_residual_phase_rad
),
));
}
require_finite_positive(
"max_predicted_geometry_error_rad",
self.max_predicted_geometry_error_rad,
)?;
require_finite_positive(
"max_predicted_roundoff_error_rad",
self.max_predicted_roundoff_error_rad,
)
}
}
impl Default for PhaseBudget {
fn default() -> Self {
Self {
max_abs_residual_phase_rad: PI,
max_predicted_geometry_error_rad: 1.0e-4,
max_predicted_roundoff_error_rad: 1.0e-4,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TileProfile {
pub supports_f32: bool,
pub max_elements_per_tile: usize,
pub max_segment_bytes: u64,
pub max_segments_per_tensor: usize,
pub max_tensor_bytes: u64,
pub max_result_bytes: u64,
}
impl TileProfile {
pub(crate) fn validate(self) -> Result<(), LoweringError> {
if !self.supports_f32 {
return Err(LoweringError::new(
PreflightCheck::DType,
"selected Tensor profile does not support numbers/f32",
));
}
for (name, value) in [
("max_elements_per_tile", self.max_elements_per_tile),
("max_segments_per_tensor", self.max_segments_per_tensor),
] {
if value == 0 {
return Err(LoweringError::new(
PreflightCheck::Configuration,
format!("{name} must be non-zero"),
));
}
}
for (name, value, minimum) in [
("max_segment_bytes", self.max_segment_bytes, 4_u64),
("max_tensor_bytes", self.max_tensor_bytes, 4),
("max_result_bytes", self.max_result_bytes, 8),
] {
if value < minimum {
return Err(LoweringError::new(
PreflightCheck::Configuration,
format!("{name} must be at least {minimum}"),
));
}
}
Ok(())
}
pub(crate) fn effective_element_limit(self) -> Result<usize, LoweringError> {
self.validate()?;
let segment_elements = self.max_segment_bytes / 4;
let segmented = segment_elements
.checked_mul(u64::try_from(self.max_segments_per_tensor).map_err(|_| {
LoweringError::new(
PreflightCheck::ResultAllocation,
"segment count does not fit u64",
)
})?)
.ok_or_else(|| {
LoweringError::new(
PreflightCheck::ResultAllocation,
"segmented element limit overflowed",
)
})?;
let tensor_elements = self.max_tensor_bytes / 4;
let limit = u64::try_from(self.max_elements_per_tile)
.unwrap_or(u64::MAX)
.min(segmented)
.min(tensor_elements);
usize::try_from(limit).map_err(|_| {
LoweringError::new(
PreflightCheck::ResultAllocation,
"effective tile element limit does not fit usize",
)
})
}
}
impl Default for TileProfile {
fn default() -> Self {
Self {
supports_f32: true,
max_elements_per_tile: 262_144,
max_segment_bytes: 64 * 1024,
max_segments_per_tensor: 16,
max_tensor_bytes: 16 * 1024 * 1024,
max_result_bytes: 256 * 1024 * 1024,
}
}
}
pub(crate) fn required_operation_symbols() -> [Symbol; 8] {
[
add_op_symbol(),
sub_op_symbol(),
mul_op_symbol(),
div_op_symbol(),
sqrt_op_symbol(),
exp_op_symbol(),
sin_op_symbol(),
cos_op_symbol(),
]
}
pub(crate) fn validate_executor(card: &TensorExecutorCard) -> Result<(), LoweringError> {
for required in required_operation_symbols() {
if !card
.operations
.iter()
.any(|available| available == &required)
{
return Err(LoweringError::new(
PreflightCheck::Operator,
format!("executor {} does not advertise {required}", card.symbol),
));
}
}
Ok(())
}
pub(crate) fn finite_nonzero_f32(name: &str, value: f64) -> Result<f32, LoweringError> {
let lowered = finite_f32(name, value)?;
if lowered == 0.0 {
Err(LoweringError::new(
PreflightCheck::Constant,
format!("{name} underflowed to zero in f32"),
))
} else {
Ok(lowered)
}
}
pub(crate) fn finite_f32(name: &str, value: f64) -> Result<f32, LoweringError> {
let lowered = value as f32;
if value.is_finite() && lowered.is_finite() {
Ok(lowered)
} else {
Err(LoweringError::new(
PreflightCheck::Constant,
format!("{name} {value} is not representable as finite f32"),
))
}
}
fn require_finite_positive(name: &str, value: f64) -> Result<(), LoweringError> {
if value.is_finite() && value > 0.0 {
Ok(())
} else {
Err(LoweringError::new(
PreflightCheck::Configuration,
format!("{name} must be finite and positive"),
))
}
}