silvic_core/backend/mod.rs
1//! The device operations the learner drives. A backend holds the training set (dataset 0) and the validation sets
2//! (1..) in its own memory, their f32 scores, the training gradients and a partition array of training row indices
3//! in which every leaf of the tree being grown is a contiguous range.
4//!
5//! Histograms come back in the host layout (`2 * total_bins` f64, `[g, h]` per bin): integer counts of quantized
6//! units in quantized mode, real sums in float mode. Everything a backend computes from quantized inputs is integer
7//! and exact, so every backend returns the same numbers for the same inputs.
8
9mod cpu;
10
11pub use cpu::CpuBackend;
12
13use crate::data::BinnedDataset;
14use crate::error::Result;
15use crate::quantize::QuantSpec;
16
17/// A leaf's rows: `partition[begin..begin + count]`.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub struct LeafRange {
20 pub begin: u32,
21 pub count: u32,
22}
23
24/// A split as the device applies it: rows whose bin of the feature at (`group`, `column`) is set in `left_bins` go
25/// left. Numerical splits set bins `0..=threshold` and, when missing values go left, the NaN bin.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct BinSplit {
28 pub group: u32,
29 pub column: u32,
30 pub left_bins: [u32; 8],
31}
32
33impl BinSplit {
34 #[inline]
35 pub fn goes_left(&self, bin: u8) -> bool {
36 (self.left_bins[usize::from(bin) / 32] >> (bin % 32)) & 1 == 1
37 }
38}
39
40/// One node of a tree in device form. Children `>= 0` are nodes, `< 0` are `!leaf`.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct DeviceNode {
43 pub split: BinSplit,
44 pub left: i32,
45 pub right: i32,
46}
47
48/// A trained tree as the device adds it to scores: `leaf_values` are already shrunk and exclude the first tree's bias.
49#[derive(Debug, Clone, PartialEq)]
50pub struct DeviceTree {
51 /// Empty for a single-leaf tree.
52 pub nodes: Vec<DeviceNode>,
53 pub leaf_values: Vec<f32>,
54}
55
56/// Objective-specific inputs the gradient computation needs beyond labels, weights and query boundaries.
57#[derive(Debug, Clone, PartialEq)]
58pub enum ObjectiveSetup {
59 Regression,
60 Binary {
61 sigmoid: f64,
62 },
63 LambdaRank {
64 sigmoid: f64,
65 norm: bool,
66 truncation_level: u32,
67 label_gain: Vec<f64>,
68 /// Per training query (LightGBM `inverse_max_dcgs_`).
69 inverse_max_dcg: Vec<f64>,
70 },
71}
72
73/// Local maxima for the quantization scales; the learner reduces them over ranks.
74#[derive(Debug, Clone, Copy, PartialEq, Default)]
75pub struct GradientStats {
76 pub max_abs_grad: f64,
77 pub max_hess: f64,
78}
79
80/// Which training rows the next tree sees.
81#[derive(Debug, Clone, Copy, PartialEq)]
82pub enum BagSpec {
83 All,
84 /// Rows with `quantize::in_bag(seed, round, global_row, fraction)`.
85 Bernoulli {
86 seed: u32,
87 round: u32,
88 fraction: f64,
89 },
90}
91
92/// A child of a split leaf.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum Side {
95 Left,
96 Right,
97}
98
99impl Side {
100 /// The child's rows after `leaf` was partitioned with `left` rows going left.
101 pub fn range(self, leaf: LeafRange, left: u32) -> LeafRange {
102 match self {
103 Side::Left => LeafRange {
104 begin: leaf.begin,
105 count: left,
106 },
107 Side::Right => LeafRange {
108 begin: leaf.begin + left,
109 count: leaf.count - left,
110 },
111 }
112 }
113}
114
115pub trait Backend {
116 /// Human-readable device name for logs (`cpu (14 threads)`, `Apple M4 Pro (metal)`).
117 fn name(&self) -> String;
118
119 /// The pool host-side parallel work runs on when `num_threads` is set; backends without host-side parallelism
120 /// ignore it. Called before `load`.
121 fn set_thread_pool(&mut self, pool: std::sync::Arc<rayon::ThreadPool>) {
122 let _ = pool;
123 }
124
125 /// Takes the training set and validation sets; called once, before anything else.
126 fn load(&mut self, train: &BinnedDataset, valid: &[&BinnedDataset], objective: &ObjectiveSetup) -> Result<()>;
127
128 /// Sets every score of `dataset` to `value`.
129 fn init_scores(&mut self, dataset: usize, value: f64) -> Result<()>;
130
131 /// Computes f32 gradients and hessians of the training scores; returns their local maxima.
132 fn compute_gradients(&mut self) -> Result<GradientStats>;
133
134 /// Quantizes the gradients and hessians with `spec` (`quantize::quantize_row`, keyed by global row id).
135 fn quantize(&mut self, spec: &QuantSpec) -> Result<()>;
136
137 /// Refills the partition with the bagged training rows in ascending order; returns their count.
138 fn reset_partition(&mut self, bag: &BagSpec) -> Result<u32>;
139
140 /// Histograms from now on only need the bins of the features with `used[feature]` (LightGBM `is_feature_used`:
141 /// the tree's feature mask and the parent's splittable features); the other bins may hold anything. Every rank
142 /// passes the same mask. Backends may ignore it.
143 fn set_histogram_features(&mut self, used: &[bool]) {
144 let _ = used;
145 }
146
147 /// Histogram of the rows of `leaf` into `out` (`2 * total_bins`, overwritten): quantized units when `quantized`,
148 /// else float sums of gradients and hessians.
149 fn histogram(&mut self, leaf: LeafRange, quantized: bool, out: &mut [f64]) -> Result<()>;
150
151 /// Stable in-place partition of `leaf`'s rows by `split`; returns the number of rows going left.
152 fn partition(&mut self, leaf: LeafRange, split: &BinSplit) -> Result<u32>;
153
154 /// `partition`, then the histogram of the `side` child into `out`; returns the left count. A device overrides it
155 /// to run both without waiting for the count in between: the side is chosen before partitioning (from the
156 /// split's estimated counts, the same on every rank), so the histogram's row range can stay on the device.
157 fn partition_and_histogram(
158 &mut self,
159 leaf: LeafRange,
160 split: &BinSplit,
161 side: Side,
162 quantized: bool,
163 out: &mut [f64],
164 ) -> Result<u32> {
165 let left = self.partition(leaf, split)?;
166 self.histogram(side.range(leaf, left), quantized, out)?;
167 Ok(left)
168 }
169
170 /// Exact fixed-point sums `[sum rint(g * scale), sum rint(h * scale)]` of each leaf's float gradients
171 /// (`quantize::to_fixed`), for leaf renewal.
172 fn leaf_sums(&mut self, leaves: &[LeafRange], grad_scale: f32, hess_scale: f32) -> Result<Vec<[i64; 2]>>;
173
174 /// Adds `tree` to the scores of every dataset, traversing binned rows.
175 fn add_tree(&mut self, tree: &DeviceTree) -> Result<()>;
176
177 /// The current scores of `dataset`.
178 fn scores(&mut self, dataset: usize) -> Result<Vec<f32>>;
179}