Skip to main content

silvic_core/data/
mod.rs

1//! Binned training data: every feature value replaced by a bin index (a byte), features packed into groups whose
2//! bins fit one GPU threadgroup's shared histogram.
3//!
4//! Bin mappers follow LightGBM 3.3.5 `BinMapper` semantics where they reach the model file: a numerical value goes
5//! to the first bin whose upper bound is `>= value`, and a split "bins `<= t` go left" is written with threshold
6//! `upper_bounds[t]`, so `value <= threshold` at prediction time agrees with training for every value. NaN has its
7//! own bin when the training sample had NaN (`MissingType::NaN`); otherwise NaN bins as 0.0, as LightGBM predicts
8//! with `MissingType::None`. Categorical bin 0 collects negative, NaN, unseen and dropped categories, which LightGBM
9//! sends right.
10
11mod bin_mapper;
12mod binning;
13mod build;
14#[cfg(test)]
15mod parity_tests;
16mod sample;
17
18pub use bin_mapper::BinMapper;
19pub use build::{DatasetInput, DenseData, DenseMatrix, Layout};
20
21/// Bins one feature group may hold: a packed i32 per bin fills 32 KiB, Metal's threadgroup memory limit.
22pub const GROUP_BIN_BUDGET: u32 = 8192;
23
24#[derive(Debug, Clone, PartialEq)]
25pub struct FeatureMeta {
26    pub name: String,
27    pub bins: BinMapper,
28    /// Bins in the histogram, the NaN bin included; 0 for a trivial feature.
29    pub num_bins: u32,
30    /// The feature's group; meaningless for a trivial feature.
31    pub group: u32,
32    /// Byte offset of the feature in its group's row.
33    pub column: u32,
34    /// Index of the feature's bin 0 in the flat histogram.
35    pub hist_offset: u32,
36}
37
38impl FeatureMeta {
39    pub fn is_trivial(&self) -> bool {
40        self.num_bins == 0
41    }
42
43    pub fn is_categorical(&self) -> bool {
44        matches!(self.bins, BinMapper::Categorical { .. })
45    }
46}
47
48#[derive(Debug, Clone, PartialEq)]
49pub struct FeatureGroup {
50    /// Indices into `BinnedDataset::features`, in column order.
51    pub features: Vec<usize>,
52    /// Bytes per row: the feature count rounded up to a multiple of 4, padding bytes zero.
53    pub row_stride: u32,
54    /// Index of the group's first bin in the flat histogram; a group's bins are contiguous there.
55    pub hist_offset: u32,
56    pub total_bins: u32,
57    /// Row-major bins: `data[row * row_stride + column]`.
58    pub data: Vec<u8>,
59}
60
61#[derive(Debug, Clone, PartialEq)]
62pub struct BinnedDataset {
63    pub num_rows: usize,
64    /// Global id of local row 0: rows of lower ranks come first. Keys the random draws of quantization and bagging.
65    pub row_offset: u64,
66    /// Every input feature in input order, trivial ones included.
67    pub features: Vec<FeatureMeta>,
68    pub groups: Vec<FeatureGroup>,
69    pub total_bins: usize,
70    pub label: Vec<f32>,
71    pub weight: Option<Vec<f32>>,
72    /// `num_queries + 1` row boundaries; queries are contiguous runs of rows.
73    pub query_boundaries: Option<Vec<u32>>,
74}
75
76impl BinnedDataset {
77    pub fn num_queries(&self) -> usize {
78        self.query_boundaries.as_ref().map_or(0, |b| b.len() - 1)
79    }
80
81    /// The bin of `feature` at `row`.
82    #[inline]
83    pub fn bin(&self, feature: usize, row: usize) -> u8 {
84        let meta = &self.features[feature];
85        let group = &self.groups[meta.group as usize];
86        group.data[row * group.row_stride as usize + meta.column as usize]
87    }
88}