sim_lib_interference_compute/
preflight.rs1use std::{f64::consts::PI, fmt};
4
5use sim_kernel::Symbol;
6use sim_lib_numbers_tensor::{
7 TensorExecutorCard, add_op_symbol, cos_op_symbol, div_op_symbol, exp_op_symbol, mul_op_symbol,
8 sin_op_symbol, sqrt_op_symbol, sub_op_symbol,
9};
10
11pub const REQUIRED_TENSOR_OPERATION_NAMES: [&str; 8] = [
13 "tensor/op/add",
14 "tensor/op/sub",
15 "tensor/op/mul",
16 "tensor/op/div",
17 "tensor/op/sqrt",
18 "tensor/op/exp",
19 "tensor/op/sin",
20 "tensor/op/cos",
21];
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum PreflightCheck {
26 Configuration,
28 Operator,
30 DType,
32 Shape,
34 Singularity,
36 ForwardPlane,
38 Denominator,
40 ResultAllocation,
42 PhaseBudget,
44 GeometryBudget,
46 RoundoffBudget,
48 Constant,
50 Execution,
52}
53
54#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct LoweringError {
57 check: PreflightCheck,
58 detail: String,
59}
60
61impl LoweringError {
62 pub(crate) fn new(check: PreflightCheck, detail: impl Into<String>) -> Self {
63 Self {
64 check,
65 detail: detail.into(),
66 }
67 }
68
69 pub fn check(&self) -> PreflightCheck {
71 self.check
72 }
73
74 pub fn detail(&self) -> &str {
76 &self.detail
77 }
78}
79
80impl fmt::Display for LoweringError {
81 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82 write!(formatter, "{:?} preflight: {}", self.check, self.detail)
83 }
84}
85
86impl std::error::Error for LoweringError {}
87
88#[derive(Clone, Copy, Debug, PartialEq)]
90pub struct PhaseBudget {
91 pub max_abs_residual_phase_rad: f64,
93 pub max_predicted_geometry_error_rad: f64,
95 pub max_predicted_roundoff_error_rad: f64,
97}
98
99impl PhaseBudget {
100 pub fn new(
102 max_abs_residual_phase_rad: f64,
103 max_predicted_geometry_error_rad: f64,
104 max_predicted_roundoff_error_rad: f64,
105 ) -> Result<Self, LoweringError> {
106 let budget = Self {
107 max_abs_residual_phase_rad,
108 max_predicted_geometry_error_rad,
109 max_predicted_roundoff_error_rad,
110 };
111 budget.validate()?;
112 Ok(budget)
113 }
114
115 pub(crate) fn validate(self) -> Result<(), LoweringError> {
116 require_finite_positive(
117 "max_abs_residual_phase_rad",
118 self.max_abs_residual_phase_rad,
119 )?;
120 if self.max_abs_residual_phase_rad > PI {
121 return Err(LoweringError::new(
122 PreflightCheck::Configuration,
123 format!(
124 "max_abs_residual_phase_rad {} exceeds pi",
125 self.max_abs_residual_phase_rad
126 ),
127 ));
128 }
129 require_finite_positive(
130 "max_predicted_geometry_error_rad",
131 self.max_predicted_geometry_error_rad,
132 )?;
133 require_finite_positive(
134 "max_predicted_roundoff_error_rad",
135 self.max_predicted_roundoff_error_rad,
136 )
137 }
138}
139
140impl Default for PhaseBudget {
141 fn default() -> Self {
142 Self {
143 max_abs_residual_phase_rad: PI,
144 max_predicted_geometry_error_rad: 1.0e-4,
145 max_predicted_roundoff_error_rad: 1.0e-4,
146 }
147 }
148}
149
150#[derive(Clone, Copy, Debug, PartialEq, Eq)]
152pub struct TileProfile {
153 pub supports_f32: bool,
155 pub max_elements_per_tile: usize,
157 pub max_segment_bytes: u64,
159 pub max_segments_per_tensor: usize,
161 pub max_tensor_bytes: u64,
163 pub max_result_bytes: u64,
165}
166
167impl TileProfile {
168 pub(crate) fn validate(self) -> Result<(), LoweringError> {
169 if !self.supports_f32 {
170 return Err(LoweringError::new(
171 PreflightCheck::DType,
172 "selected Tensor profile does not support numbers/f32",
173 ));
174 }
175 for (name, value) in [
176 ("max_elements_per_tile", self.max_elements_per_tile),
177 ("max_segments_per_tensor", self.max_segments_per_tensor),
178 ] {
179 if value == 0 {
180 return Err(LoweringError::new(
181 PreflightCheck::Configuration,
182 format!("{name} must be non-zero"),
183 ));
184 }
185 }
186 for (name, value, minimum) in [
187 ("max_segment_bytes", self.max_segment_bytes, 4_u64),
188 ("max_tensor_bytes", self.max_tensor_bytes, 4),
189 ("max_result_bytes", self.max_result_bytes, 8),
190 ] {
191 if value < minimum {
192 return Err(LoweringError::new(
193 PreflightCheck::Configuration,
194 format!("{name} must be at least {minimum}"),
195 ));
196 }
197 }
198 Ok(())
199 }
200
201 pub(crate) fn effective_element_limit(self) -> Result<usize, LoweringError> {
202 self.validate()?;
203 let segment_elements = self.max_segment_bytes / 4;
204 let segmented = segment_elements
205 .checked_mul(u64::try_from(self.max_segments_per_tensor).map_err(|_| {
206 LoweringError::new(
207 PreflightCheck::ResultAllocation,
208 "segment count does not fit u64",
209 )
210 })?)
211 .ok_or_else(|| {
212 LoweringError::new(
213 PreflightCheck::ResultAllocation,
214 "segmented element limit overflowed",
215 )
216 })?;
217 let tensor_elements = self.max_tensor_bytes / 4;
218 let limit = u64::try_from(self.max_elements_per_tile)
219 .unwrap_or(u64::MAX)
220 .min(segmented)
221 .min(tensor_elements);
222 usize::try_from(limit).map_err(|_| {
223 LoweringError::new(
224 PreflightCheck::ResultAllocation,
225 "effective tile element limit does not fit usize",
226 )
227 })
228 }
229}
230
231impl Default for TileProfile {
232 fn default() -> Self {
233 Self {
234 supports_f32: true,
235 max_elements_per_tile: 262_144,
236 max_segment_bytes: 64 * 1024,
237 max_segments_per_tensor: 16,
238 max_tensor_bytes: 16 * 1024 * 1024,
239 max_result_bytes: 256 * 1024 * 1024,
240 }
241 }
242}
243
244pub(crate) fn required_operation_symbols() -> [Symbol; 8] {
245 [
246 add_op_symbol(),
247 sub_op_symbol(),
248 mul_op_symbol(),
249 div_op_symbol(),
250 sqrt_op_symbol(),
251 exp_op_symbol(),
252 sin_op_symbol(),
253 cos_op_symbol(),
254 ]
255}
256
257pub(crate) fn validate_executor(card: &TensorExecutorCard) -> Result<(), LoweringError> {
258 for required in required_operation_symbols() {
259 if !card
260 .operations
261 .iter()
262 .any(|available| available == &required)
263 {
264 return Err(LoweringError::new(
265 PreflightCheck::Operator,
266 format!("executor {} does not advertise {required}", card.symbol),
267 ));
268 }
269 }
270 Ok(())
271}
272
273pub(crate) fn finite_nonzero_f32(name: &str, value: f64) -> Result<f32, LoweringError> {
274 let lowered = finite_f32(name, value)?;
275 if lowered == 0.0 {
276 Err(LoweringError::new(
277 PreflightCheck::Constant,
278 format!("{name} underflowed to zero in f32"),
279 ))
280 } else {
281 Ok(lowered)
282 }
283}
284
285pub(crate) fn finite_f32(name: &str, value: f64) -> Result<f32, LoweringError> {
286 let lowered = value as f32;
287 if value.is_finite() && lowered.is_finite() {
288 Ok(lowered)
289 } else {
290 Err(LoweringError::new(
291 PreflightCheck::Constant,
292 format!("{name} {value} is not representable as finite f32"),
293 ))
294 }
295}
296
297fn require_finite_positive(name: &str, value: f64) -> Result<(), LoweringError> {
298 if value.is_finite() && value > 0.0 {
299 Ok(())
300 } else {
301 Err(LoweringError::new(
302 PreflightCheck::Configuration,
303 format!("{name} must be finite and positive"),
304 ))
305 }
306}