Skip to main content

sim_lib_interference_compute/
preflight.rs

1//! Public budgets, provider limits, and fail-closed diagnostics.
2
3use 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
11/// Stable names of the only open Tensor operations emitted by this lowering.
12pub 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/// The validation boundary that refused a lowering request.
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum PreflightCheck {
26    /// A budget or provider limit was internally invalid.
27    Configuration,
28    /// The selected executor does not advertise a required operation.
29    Operator,
30    /// The selected profile does not admit canonical `numbers/f32`.
31    DType,
32    /// A Tensor shape, element count, or tile layout was invalid.
33    Shape,
34    /// A point sample lies at or inside its exclusion radius.
35    Singularity,
36    /// A plane-wave sample lies behind its forward plane.
37    ForwardPlane,
38    /// A normalized-distance or gain denominator was not safely positive.
39    Denominator,
40    /// A Tensor or final result would exceed an allocation limit.
41    ResultAllocation,
42    /// Residual phase would exceed its declared interval.
43    PhaseBudget,
44    /// Predicted geometry error would exceed its declared limit.
45    GeometryBudget,
46    /// Predicted arithmetic roundoff would exceed its declared limit.
47    RoundoffBudget,
48    /// A host-derived constant could not be represented safely as `f32`.
49    Constant,
50    /// An accepted canonical Tensor operation failed.
51    Execution,
52}
53
54/// A deterministic lowering diagnostic.
55#[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    /// Returns the boundary that rejected or failed the request.
70    pub fn check(&self) -> PreflightCheck {
71        self.check
72    }
73
74    /// Returns the stable human-readable diagnostic detail.
75    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/// Accuracy limits applied to every source on every tile.
89#[derive(Clone, Copy, Debug, PartialEq)]
90pub struct PhaseBudget {
91    /// Largest absolute phase argument an executor may receive.
92    pub max_abs_residual_phase_rad: f64,
93    /// Largest predicted phase error from `f64` geometry lowered to local f32.
94    pub max_predicted_geometry_error_rad: f64,
95    /// Largest predicted phase error from the f32 arithmetic sequence.
96    pub max_predicted_roundoff_error_rad: f64,
97}
98
99impl PhaseBudget {
100    /// Constructs and validates a phase budget.
101    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/// Provider and allocation limits used to partition and admit a plane.
151#[derive(Clone, Copy, Debug, PartialEq, Eq)]
152pub struct TileProfile {
153    /// Whether the selected provider admits canonical `numbers/f32`.
154    pub supports_f32: bool,
155    /// Largest logical element count for one tile Tensor.
156    pub max_elements_per_tile: usize,
157    /// Largest byte count in one resident segment.
158    pub max_segment_bytes: u64,
159    /// Largest segment count for one tile Tensor.
160    pub max_segments_per_tensor: usize,
161    /// Largest total byte count for one tile Tensor.
162    pub max_tensor_bytes: u64,
163    /// Largest combined byte count for the final real and imaginary planes.
164    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}