use rayon::prelude::*;
use std::ops::ControlFlow;
use super::margins::MarginCaches;
use super::prepare::{Prepared, TrainContext, TreeSample};
use super::row_sampling::{make_column_sampler, sample_rows};
use crate::config::{BoosterKind, Boulevard, TrainingParams};
use crate::data::DMatrix;
use crate::error::Result;
use crate::inference::BoulevardInfo;
use crate::model::BoostedModel;
use crate::objective::GradPair;
use crate::rng::Rng;
use crate::tree::RegTree;
use crate::tree::reuse::ReuseSet;
use crate::tree::sampler::ColumnSampler;
const ROW_CHUNK: usize = 4096;
pub(crate) const BOULEVARD_SALT: u64 = 0xB0_07E7;
fn settings(params: &TrainingParams) -> Boulevard {
match params.booster {
BoosterKind::Boulevard(boulevard) => boulevard,
_ => Boulevard::default(),
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Schedule {
pub(crate) dropout: f64,
pub(crate) learning_rate: f64,
pub(crate) truncation: Option<f64>,
pub(crate) parallel: usize,
pub(crate) seed: u64,
pub(crate) salt: u64,
}
impl Schedule {
pub(crate) fn from_params(params: &TrainingParams) -> Self {
Schedule {
dropout: settings(params).dropout(),
learning_rate: params.eta,
truncation: settings(params).truncation(),
parallel: params.num_parallel_tree,
seed: params.seed,
salt: BOULEVARD_SALT,
}
}
pub(crate) fn from_info(info: &BoulevardInfo, parallel: usize, salt: u64) -> Self {
Schedule {
dropout: info.dropout,
learning_rate: info.learning_rate,
truncation: info.truncation,
parallel,
seed: info.seed,
salt,
}
}
pub(crate) fn scale(&self, rounds: usize) -> f64 {
let b = rounds.max(1) as f64;
if self.parallel > 1 {
1.0 / b
} else {
(1.0 + self.learning_rate * (1.0 - self.dropout)) / b
}
}
fn truncate(&self, v: f64) -> f64 {
match self.truncation {
Some(m) => v.clamp(-m, m),
None => v,
}
}
}
pub(crate) struct RoundRequest<'a> {
pub(crate) index: usize,
pub(crate) first_slot: usize,
pub(crate) offsets: &'a [Vec<f64>],
pub(crate) rng: &'a mut Rng,
}
pub(crate) struct Recursion {
schedule: Schedule,
n: usize,
rounds: usize,
per_tree: Vec<Vec<f32>>,
slots: Vec<Vec<f64>>,
}
impl Recursion {
pub(crate) fn new(schedule: Schedule, n: usize) -> Self {
Recursion {
schedule,
n,
rounds: 0,
per_tree: Vec::new(),
slots: vec![vec![0.0; n]; schedule.parallel],
}
}
pub(crate) fn scale(&self) -> f64 {
self.schedule.scale(self.rounds)
}
pub(crate) fn step(
&mut self,
mut fit: impl FnMut(RoundRequest) -> Result<Vec<Vec<f32>>>,
) -> Result<()> {
let s = self.schedule;
let b = self.rounds;
let mut rng = Rng::new(s.seed ^ (b as u64).wrapping_mul(0x9E37_79B9) ^ s.salt);
if s.parallel == 1 {
let offset = self.dropout_offset(&mut rng);
let preds = fit(RoundRequest {
index: b,
first_slot: 0,
offsets: std::slice::from_ref(&offset),
rng: &mut rng,
})?;
let preds = preds.into_iter().next().unwrap_or_default();
add_rows(&mut self.slots[0], &preds);
if s.dropout > 0.0 {
self.per_tree.push(preds);
}
} else if b == 0 {
let mut running = vec![0.0; self.n];
for k in 0..s.parallel {
let offset: Vec<f64> = running.iter().map(|&v| s.truncate(v)).collect();
let preds = fit(RoundRequest {
index: 0,
first_slot: k,
offsets: std::slice::from_ref(&offset),
rng: &mut rng,
})?;
let preds = preds.into_iter().next().unwrap_or_default();
add_rows(&mut running, &preds);
add_rows(&mut self.slots[k], &preds);
}
} else {
let inv = 1.0 / b as f64;
let slots = &self.slots;
let offsets: Vec<Vec<f64>> = (0..s.parallel)
.map(|k| {
let mut out = vec![0.0; self.n];
out.par_chunks_mut(ROW_CHUNK)
.enumerate()
.for_each(|(c, chunk)| {
let start = c * ROW_CHUNK;
for (i, o) in chunk.iter_mut().enumerate() {
let row = start + i;
let mut sum = 0.0;
for (l, slot) in slots.iter().enumerate() {
if l != k {
sum += slot[row];
}
}
*o = s.truncate(sum * inv);
}
});
out
})
.collect();
let preds = fit(RoundRequest {
index: b,
first_slot: 0,
offsets: &offsets,
rng: &mut rng,
})?;
for (slot, p) in self.slots.iter_mut().zip(&preds) {
add_rows(slot, p);
}
}
self.rounds += 1;
Ok(())
}
fn dropout_offset(&self, rng: &mut Rng) -> Vec<f64> {
let s = self.schedule;
let b = self.rounds;
if b == 0 {
return vec![0.0; self.n];
}
let factor = s.learning_rate / b as f64;
let total = &self.slots[0];
if s.dropout == 0.0 {
return total.iter().map(|&v| s.truncate(factor * v)).collect();
}
let kept: Vec<bool> = (0..b).map(|_| rng.f64() >= s.dropout).collect();
let n_kept = kept.iter().filter(|&&k| k).count();
let from_kept = n_kept <= b - n_kept;
let trees: Vec<&[f32]> = self
.per_tree
.iter()
.zip(&kept)
.filter(|&(_, &k)| k == from_kept)
.map(|(t, _)| t.as_slice())
.collect();
let mut out = vec![0.0; self.n];
out.par_chunks_mut(ROW_CHUNK)
.enumerate()
.for_each(|(c, chunk)| {
let start = c * ROW_CHUNK;
for (i, o) in chunk.iter_mut().enumerate() {
let row = start + i;
let partial: f64 = trees.iter().map(|t| f64::from(t[row])).sum();
let sum = if from_kept {
partial
} else {
total[row] - partial
};
*o = s.truncate(factor * sum);
}
});
out
}
}
fn add_rows(sum: &mut [f64], preds: &[f32]) {
for (s, &p) in sum.iter_mut().zip(preds) {
*s += f64::from(p);
}
}
pub(crate) fn tree_rows(tree: &RegTree, data: &DMatrix) -> Vec<f32> {
(0..data.n_rows())
.into_par_iter()
.with_min_len(1024)
.map(|row| tree.predict_row(data, row))
.collect()
}
pub(super) struct BoostState<'m, 'a> {
pub(super) model: &'m mut BoostedModel,
pub(super) margins: &'m mut MarginCaches<'a>,
pub(super) reuse: &'m mut Option<ReuseSet>,
}
pub(super) fn boost(
run: &TrainContext,
prepared: &Prepared,
state: BoostState<'_, '_>,
rounds: usize,
mut after_round: impl FnMut(usize, &MarginCaches) -> ControlFlow<()>,
) -> Result<()> {
let TrainContext {
params,
dtrain,
info,
objective,
..
} = *run;
debug_assert!(matches!(params.booster, BoosterKind::Boulevard(_)));
let BoostState {
model,
margins,
reuse,
} = state;
let n = dtrain.n_rows();
let mu = model.base_scores()[0];
let mut recursion = Recursion::new(Schedule::from_params(params), n);
let mut eval_sums: Vec<Vec<f64>> = margins.evals.iter().map(|m| vec![0.0; m.len()]).collect();
let eval_sets: Vec<&DMatrix> = margins.eval_data().collect();
for round in 0..rounds {
recursion.step(|request| {
let RoundRequest { offsets, rng, .. } = request;
let gpairs: Vec<Vec<GradPair>> = offsets
.iter()
.map(|offset| {
let margins: Vec<f32> =
offset.iter().map(|&o| (f64::from(mu) + o) as f32).collect();
let mut gpair = vec![GradPair::default(); n];
objective.gradient_info(&margins, info, &mut gpair);
gpair
})
.collect();
let rows: Vec<Vec<u32>> = gpairs
.iter()
.map(|_| sample_rows(n, params, run.rows, rng))
.collect();
let mut samplers: Vec<_> = gpairs
.iter()
.map(|_| make_column_sampler(dtrain, params, rng))
.collect();
prepared.fill_approx_cache(run, &gpairs[0]);
let build = |k: usize, sampler: &mut ColumnSampler, reuse: Option<&mut ReuseSet>| {
let sample = TreeSample {
gpair: &gpairs[k],
rows: &rows[k],
forest_index: None,
};
prepared.build_tree(run, sample, sampler, reuse, 0, false).0
};
let trees: Vec<RegTree> = if gpairs.len() > 1
&& reuse.is_none()
&& params.device == crate::config::Device::Cpu
&& rayon::current_num_threads() > 1
{
samplers
.par_iter_mut()
.enumerate()
.map(|(k, sampler)| build(k, sampler, None))
.collect()
} else {
samplers
.iter_mut()
.enumerate()
.map(|(k, sampler)| build(k, sampler, reuse.as_mut()))
.collect()
};
let mut preds = Vec::with_capacity(trees.len());
for tree in trees {
preds.push(tree_rows(&tree, dtrain));
for (sum, data) in eval_sums.iter_mut().zip(&eval_sets) {
add_rows(sum, &tree_rows(&tree, data));
}
model.push_tree_weighted(tree, 1.0);
}
Ok(preds)
})?;
let scale = recursion.scale();
for (margins, sum) in margins.evals.iter_mut().zip(&eval_sums) {
for (m, &s) in margins.iter_mut().zip(sum) {
*m = (f64::from(mu) + scale * s) as f32;
}
}
if after_round(round, &*margins).is_break() {
break;
}
}
model.scale_all_leaves(recursion.scale() as f32);
model.set_boulevard(Some(BoulevardInfo {
dropout: settings(params).dropout(),
learning_rate: params.eta,
subsample: params.subsample,
reg_lambda: params.lambda,
truncation: settings(params).truncation(),
seed: params.seed,
intercept_from_labels: params.base_score.is_none(),
}));
Ok(())
}