1use silvic_core::data::{DenseData, DenseMatrix, Layout};
4use silvic_core::model::{Model, ModelObjective};
5use silvic_core::tree::{Tree, CATEGORICAL_MASK};
6use silvic_core::{Result, SilvicError};
7
8use crate::runtime::{Buffer, DType, Device, Execution, TensorSpec};
9
10const TILE_ROWS: usize = 32;
11const META_FIELDS: usize = 9;
12const VALUE_FIELDS: usize = 3;
13
14struct PackedForest {
15 metadata: Vec<i32>,
16 values: Vec<f32>,
17 categories: Vec<u32>,
18 records: usize,
19}
20
21fn threshold_f32(value: f64) -> f32 {
22 let rounded = value as f32;
23 if f64::from(rounded) > value {
24 rounded.next_down()
25 } else {
26 rounded
27 }
28}
29
30fn device_index(value: usize, what: &str) -> Result<i32> {
31 i32::try_from(value)
32 .map_err(|_| SilvicError::model(format!("Neuron {what} exceeds signed 32-bit device addressing")))
33}
34
35impl PackedForest {
36 fn new(model: &Model) -> Result<Self> {
37 device_index(model.num_features(), "feature count")?;
38 device_index(model.trees.len(), "tree count")?;
39 let mut packed = Self {
40 metadata: Vec::new(),
41 values: Vec::new(),
42 categories: vec![0],
43 records: 0,
44 };
45 for (tree_index, tree) in model.trees.iter().enumerate() {
46 let nodes = checked_preorder(tree, model.num_features())
47 .map_err(|message| SilvicError::model(format!("tree {tree_index}: {message}")))?;
48 let category_base = packed.categories.len();
49 let category_end = category_base
50 .checked_add(tree.cat_threshold.len())
51 .ok_or_else(|| SilvicError::model("Neuron category storage overflows usize"))?;
52 device_index(category_end, "category storage")?;
53 packed.categories.extend_from_slice(&tree.cat_threshold);
54 if nodes.is_empty() {
55 packed.push(
56 [0, -1, -1, 0, 0, 0, 0, tree_index as i32, 1],
57 [f32::NAN, tree.leaf_value[0] as f32, tree.leaf_value[0] as f32],
58 )?;
59 continue;
60 }
61 for (position, &node) in nodes.iter().enumerate() {
62 let categorical = tree.num_cat > 0 && tree.decision_type[node] & CATEGORICAL_MASK != 0;
63 let (category_start, category_len) = if categorical {
64 let category = tree.threshold[node] as usize;
65 let start = tree.cat_boundaries[category] as usize;
66 let end = tree.cat_boundaries[category + 1] as usize;
67 (
68 device_index(category_base + start, "category offset")?,
69 device_index(end - start, "category length")?,
70 )
71 } else {
72 (0, 0)
73 };
74 let leaf = |child: i32| {
75 if child < 0 {
76 tree.leaf_value[!child as usize] as f32
77 } else {
78 0.0
79 }
80 };
81 packed.push(
82 [
83 node as i32,
84 tree.left_child[node],
85 tree.right_child[node],
86 tree.split_feature[node],
87 i32::from(tree.decision_type[node] as u8 & !(CATEGORICAL_MASK as u8)) | i32::from(categorical),
88 category_start,
89 category_len,
90 tree_index as i32,
91 i32::from(position + 1 == nodes.len()),
92 ],
93 [
94 threshold_f32(tree.threshold[node]),
95 leaf(tree.left_child[node]),
96 leaf(tree.right_child[node]),
97 ],
98 )?;
99 }
100 }
101 if packed.records == 0 {
103 packed.push([0, -1, -1, 0, 0, 0, 0, 0, 1], [f32::NAN, 0.0, 0.0])?;
104 }
105 Ok(packed)
106 }
107
108 fn push(&mut self, metadata: [i32; META_FIELDS], values: [f32; VALUE_FIELDS]) -> Result<()> {
109 let records = self
110 .records
111 .checked_add(1)
112 .ok_or_else(|| SilvicError::model("Neuron forest record count overflows usize"))?;
113 let elements = records
114 .checked_mul(META_FIELDS)
115 .ok_or_else(|| SilvicError::model("Neuron forest metadata size overflows usize"))?;
116 device_index(elements, "forest metadata storage")?;
117 self.metadata.extend_from_slice(&metadata);
118 self.values.extend_from_slice(&values);
119 self.records = records;
120 Ok(())
121 }
122}
123
124fn checked_preorder(tree: &Tree, features: usize) -> std::result::Result<Vec<usize>, String> {
127 if tree.leaf_value.is_empty() {
128 return Err("no leaf values".into());
129 }
130 if tree.num_leaves <= 1 {
131 return Ok(Vec::new());
132 }
133 let nodes = tree.num_leaves - 1;
134 if nodes > i32::MAX as usize {
135 return Err("node count exceeds signed 32-bit device addressing".into());
136 }
137 if [
138 tree.split_feature.len(),
139 tree.threshold.len(),
140 tree.decision_type.len(),
141 tree.left_child.len(),
142 tree.right_child.len(),
143 ]
144 .iter()
145 .any(|&len| len < nodes)
146 || tree.leaf_value.len() < tree.num_leaves
147 {
148 return Err(format!("node arrays shorter than num_leaves={}", tree.num_leaves));
149 }
150 let mut seen = vec![false; nodes];
151 let mut stack = vec![0i32];
152 let mut preorder = Vec::new();
153 while let Some(child) = stack.pop() {
154 if child < 0 {
155 if !child as usize >= tree.num_leaves {
156 return Err(format!("child {child} names an out-of-range leaf"));
157 }
158 continue;
159 }
160 let node = child as usize;
161 if node >= nodes || seen[node] {
162 return Err(format!("node {node} is out of range or reached twice"));
163 }
164 seen[node] = true;
165 let feature = tree.split_feature[node];
166 if usize::try_from(feature).map_or(true, |feature| feature >= features) {
167 return Err(format!("node {node} splits on feature {feature} of {features}"));
168 }
169 if tree.num_cat > 0 && tree.decision_type[node] & CATEGORICAL_MASK != 0 {
170 let threshold = tree.threshold[node];
171 let category = threshold as usize;
172 let valid =
173 threshold >= 0.0 && category < tree.num_cat && category < tree.cat_boundaries.len().saturating_sub(1);
174 if !valid {
175 return Err(format!("node {node} names categorical bitset {threshold}"));
176 }
177 let start = tree.cat_boundaries[category];
178 let end = tree.cat_boundaries[category + 1];
179 if start < 0 || end < start || end as usize > tree.cat_threshold.len() {
180 return Err(format!("node {node} names categorical bitset {threshold}"));
181 }
182 }
183 preorder.push(node);
184 stack.push(tree.right_child[node]);
185 stack.push(tree.left_child[node]);
186 }
187 Ok(preorder)
188}
189
190fn check_matrix(rows: &DenseMatrix<'_>, features: usize) -> Result<()> {
191 if rows.cols < features {
192 return Err(SilvicError::config(format!(
193 "the matrix has {} columns, the model reads {features} features",
194 rows.cols
195 )));
196 }
197 let elements = rows
198 .rows
199 .checked_mul(rows.cols)
200 .ok_or_else(|| SilvicError::config("Neuron prediction matrix dimensions overflow usize"))?;
201 let available = match rows.data {
202 DenseData::F32(values) => values.len(),
203 DenseData::F64(values) => values.len(),
204 };
205 if elements > available || rows.rows > isize::MAX as usize / std::mem::size_of::<f32>() {
206 return Err(SilvicError::config(
207 "Neuron prediction matrix storage is too short or output is too large",
208 ));
209 }
210 Ok(())
211}
212
213pub struct NeuronPredictor {
216 input: Buffer,
217 prefix: Buffer,
218 output: Buffer,
219 execution: Execution,
220 input_tile: Vec<f32>,
221 output_tile: [f32; TILE_ROWS],
222 num_features: usize,
223 num_trees: usize,
224 objective: ModelObjective,
225}
226
227impl NeuronPredictor {
228 pub fn new(model: &Model, ordinal: Option<usize>) -> Result<Self> {
230 let packed = PackedForest::new(model)?;
231 let input_elements = model
232 .num_features()
233 .max(1)
234 .checked_mul(TILE_ROWS)
235 .ok_or_else(|| SilvicError::model("Neuron prediction tile size overflows usize"))?;
236 device_index(input_elements, "input tile storage")?;
237 let device = Device::new(ordinal)?;
238 let metadata = device.upload(&packed.metadata)?;
239 let values = device.upload(&packed.values)?;
240 let categories = device.upload(&packed.categories)?;
241 let input_tile = vec![0.0f32; input_elements];
242 let input = device.upload(&input_tile)?;
243 let prefix = device.upload(&[model.trees.len() as i32])?;
244 let output = device.allocate(TILE_ROWS * std::mem::size_of::<f32>())?;
245 let inputs = [
246 TensorSpec::new("input", DType::F32, vec![1, input_elements]),
247 TensorSpec::new("metadata", DType::I32, vec![1, packed.metadata.len()]),
248 TensorSpec::new("values", DType::F32, vec![1, packed.values.len()]),
249 TensorSpec::new("categories", DType::U32, vec![packed.categories.len(), 1]),
250 TensorSpec::new("prefix", DType::I32, vec![1, 1]),
251 ];
252 let outputs = [TensorSpec::new("scores", DType::F32, vec![1, TILE_ROWS])];
253 let kernel = device.compile(
254 include_str!("../kernels/serving.py"),
255 "score",
256 &serde_json::json!({"RECORDS": packed.records, "ROWS": TILE_ROWS}),
257 &inputs,
258 &outputs,
259 )?;
260 let execution = kernel.bind(
261 &[input.clone(), metadata, values, categories, prefix.clone()],
262 std::slice::from_ref(&output),
263 )?;
264 Ok(Self {
265 input,
266 prefix,
267 output,
268 execution,
269 input_tile,
270 output_tile: [0.0; TILE_ROWS],
271 num_features: model.num_features(),
272 num_trees: model.trees.len(),
273 objective: model.objective.clone(),
274 })
275 }
276
277 pub fn num_trees(&self) -> usize {
278 self.num_trees
279 }
280
281 pub fn num_features(&self) -> usize {
282 self.num_features
283 }
284
285 pub fn predict_raw(&mut self, rows: &DenseMatrix<'_>, num_trees: Option<usize>, out: &mut [f32]) -> Result<()> {
289 check_matrix(rows, self.num_features)?;
290 if out.len() != rows.rows {
291 return Err(SilvicError::config("Neuron prediction requires one output per row"));
292 }
293 if rows.rows == 0 {
294 return Ok(());
295 }
296 self.prefix
297 .write(&[num_trees.unwrap_or(self.num_trees).min(self.num_trees) as i32])?;
298 for first in (0..rows.rows).step_by(TILE_ROWS) {
299 let count = (rows.rows - first).min(TILE_ROWS);
300 for feature in 0..self.num_features {
301 let tile = &mut self.input_tile[feature * TILE_ROWS..(feature + 1) * TILE_ROWS];
302 for (lane, value) in tile[..count].iter_mut().enumerate() {
303 let row = first + lane;
304 let index = match rows.layout {
305 Layout::RowMajor => row * rows.cols + feature,
306 Layout::ColumnMajor => feature * rows.rows + row,
307 };
308 *value = match rows.data {
309 DenseData::F32(values) => values[index],
310 DenseData::F64(values) => values[index] as f32,
311 };
312 }
313 tile[count..].fill(0.0);
314 }
315 self.input.write(&self.input_tile)?;
316 self.execution.execute()?;
317 self.output.read(&mut self.output_tile)?;
318 out[first..first + count].copy_from_slice(&self.output_tile[..count]);
319 }
320 Ok(())
321 }
322
323 pub fn predict(&mut self, rows: &DenseMatrix<'_>, raw_score: bool, num_trees: Option<usize>) -> Result<Vec<f32>> {
325 check_matrix(rows, self.num_features)?;
326 let mut out = vec![0.0f32; rows.rows];
327 self.predict_raw(rows, num_trees, &mut out)?;
328 if !raw_score {
329 if let ModelObjective::Binary { sigmoid } = self.objective {
330 let sigmoid = sigmoid as f32;
331 for value in &mut out {
332 *value = 1.0f32 / (1.0f32 + (-sigmoid * *value).exp());
333 }
334 }
335 }
336 Ok(out)
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 fn model(trees: Vec<Tree>) -> Model {
345 Model {
346 objective: ModelObjective::Regression,
347 feature_names: vec!["f0".into()],
348 feature_infos: vec!["none".into()],
349 trees,
350 parameters: Vec::new(),
351 pandas_categorical: None,
352 }
353 }
354
355 #[test]
356 fn thresholds_round_toward_negative_infinity() {
357 for exact in [
358 -f64::MAX,
359 -1.0e-300,
360 -0.0,
361 0.0,
362 1.0e-300,
363 0.1,
364 f64::from(1.0f32.next_up()).next_down(),
365 f64::MAX,
366 f64::NEG_INFINITY,
367 f64::INFINITY,
368 ] {
369 let rounded = threshold_f32(exact);
370 assert!(f64::from(rounded) <= exact);
371 if rounded != f32::INFINITY {
372 assert!(f64::from(rounded.next_up()) > exact || exact == f64::INFINITY);
373 }
374 }
375 assert!(threshold_f32(f64::NAN).is_nan());
376 assert_eq!(threshold_f32(-0.0).to_bits(), (-0.0f32).to_bits());
377 }
378
379 #[test]
380 fn malformed_models_fail_before_device_creation() {
381 let mut tree = Tree::new();
382 tree.leaf_value.clear();
383 assert!(PackedForest::new(&model(vec![tree])).is_err());
384 let mut tree = Tree::new();
385 tree.num_leaves = 2;
386 assert!(PackedForest::new(&model(vec![tree])).is_err());
387 let mut tree = Tree::new();
388 tree.num_leaves = 2;
389 tree.split_feature = vec![0];
390 tree.threshold = vec![0.0];
391 tree.decision_type = vec![0];
392 tree.left_child = vec![0];
393 tree.right_child = vec![-2];
394 tree.leaf_value = vec![1.0, 2.0];
395 assert!(PackedForest::new(&model(vec![tree.clone()])).is_err());
396 tree.left_child[0] = -3;
397 assert!(PackedForest::new(&model(vec![tree.clone()])).is_err());
398 tree.left_child[0] = -1;
399 tree.split_feature[0] = -1;
400 assert!(PackedForest::new(&model(vec![tree.clone()])).is_err());
401 tree.split_feature[0] = 0;
402 tree.num_cat = 1;
403 tree.decision_type[0] = 1;
404 tree.cat_boundaries = vec![0, 2];
405 tree.cat_threshold = vec![u32::MAX];
406 assert!(PackedForest::new(&model(vec![tree.clone()])).is_err());
407 tree.cat_boundaries[1] = 1;
408 assert!(PackedForest::new(&model(vec![tree])).is_ok());
409 }
410
411 #[test]
412 fn invalid_matrix_boundaries_are_checked_without_device() {
413 for matrix in [
414 DenseMatrix {
415 data: DenseData::F32(&[]),
416 rows: 1,
417 cols: 0,
418 layout: Layout::RowMajor,
419 },
420 DenseMatrix {
421 data: DenseData::F32(&[]),
422 rows: 1,
423 cols: 1,
424 layout: Layout::ColumnMajor,
425 },
426 DenseMatrix {
427 data: DenseData::F64(&[]),
428 rows: usize::MAX,
429 cols: 2,
430 layout: Layout::RowMajor,
431 },
432 DenseMatrix {
433 data: DenseData::F64(&[]),
434 rows: isize::MAX as usize,
435 cols: 0,
436 layout: Layout::RowMajor,
437 },
438 ] {
439 assert!(check_matrix(&matrix, 1).is_err());
440 }
441 assert!(check_matrix(
442 &DenseMatrix {
443 data: DenseData::F32(&[]),
444 rows: 100,
445 cols: 0,
446 layout: Layout::RowMajor
447 },
448 0
449 )
450 .is_ok());
451 assert!(check_matrix(
452 &DenseMatrix {
453 data: DenseData::F64(&[]),
454 rows: 0,
455 cols: 1,
456 layout: Layout::ColumnMajor
457 },
458 1
459 )
460 .is_ok());
461 }
462}