Skip to main content

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}