use std::sync::Arc;
use rayon::ThreadPool;
use crate::backend::{Backend, BagSpec, ObjectiveSetup};
use crate::collective::Collective;
use crate::config::Params;
use crate::data::BinnedDataset;
use crate::error::{Result, SilvicError};
use crate::learner::{GrownTree, Learner, TreeSpec};
use crate::metric::{self, EvalResult};
use crate::model::{Model, ModelObjective};
use crate::objective;
use crate::quantize::QuantSpec;
use crate::replay;
use crate::split::{self, HistScale, K_EPSILON};
use crate::tree::Tree;
#[derive(Debug, Clone, PartialEq)]
pub struct IterationReport<'a> {
pub iteration: usize,
pub evals: &'a [EvalResult],
head: &'a Model,
init_trees: &'a [Tree],
trees: &'a [Tree],
}
impl IterationReport<'_> {
pub fn model(&self) -> Model {
let mut trees = Vec::with_capacity(self.init_trees.len() + self.trees.len());
trees.extend_from_slice(self.init_trees);
trees.extend_from_slice(self.trees);
Model {
trees,
..self.head.clone()
}
}
}
pub fn train(
params: &Params,
train: &BinnedDataset,
valid: &[(&str, &BinnedDataset)],
backend: &mut dyn Backend,
collective: &mut dyn Collective,
on_iteration: &mut dyn FnMut(&IterationReport<'_>) -> Result<()>,
) -> Result<Model> {
train_from(params, None, train, valid, backend, collective, on_iteration)
}
pub fn train_from(
params: &Params,
init_model: Option<&Model>,
train: &BinnedDataset,
valid: &[(&str, &BinnedDataset)],
backend: &mut dyn Backend,
collective: &mut dyn Collective,
on_iteration: &mut dyn FnMut(&IterationReport<'_>) -> Result<()>,
) -> Result<Model> {
let pool = thread_pool(params.num_threads)?;
if let Some(pool) = &pool {
backend.set_thread_pool(Arc::clone(pool));
}
let setup = objective::setup(params, train)?;
let init_trees: &[Tree] = init_model.map_or(&[], |m| &m.trees);
let head = model_head(params, &setup, train, init_model)?;
let valid_sets: Vec<&BinnedDataset> = valid.iter().map(|(_, data)| *data).collect();
backend.load(train, &valid_sets, &setup)?;
let init_score = install(pool.as_deref(), || {
objective::init_score(params, train, &mut *collective)
})?;
let init_score = if init_score.abs() > K_EPSILON { init_score } else { 0.0 };
for dataset in 0..=valid.len() {
backend.init_scores(dataset, init_score)?;
}
for (index, tree) in init_trees.iter().enumerate() {
let bias = if index == 0 { init_score } else { 0.0 };
backend.add_tree(&replay::device_tree(tree, train, bias)?)?;
}
tracing::debug!(backend = %backend.name(), init_score, init_trees = init_trees.len(), "training started");
let mut trainer = Trainer {
params,
train,
valid,
learner: Learner::new(params, &train.features, train.total_bins, pool.as_deref()),
pool: pool.as_deref(),
constant_hessian: objective::constant_hessian(&setup, train.weight.as_deref()),
init_score,
prior_trees: init_trees.len(),
trees: Vec::new(),
};
let start = init_trees.len();
for iteration in start..start + params.num_iterations {
if !trainer.train_one_iter(backend, collective, iteration)? {
break;
}
let evals = trainer.evaluate(backend, collective)?;
tracing::debug!(
iteration,
leaves = trainer.trees.last().map_or(0, |t| t.num_leaves),
?evals,
"tree added"
);
on_iteration(&IterationReport {
iteration,
evals: &evals,
head: &head,
init_trees,
trees: &trainer.trees,
})?;
}
let mut model = head;
model.trees = init_trees.to_vec();
model.trees.append(&mut trainer.trees);
Ok(model)
}
fn model_head(
params: &Params,
setup: &ObjectiveSetup,
train: &BinnedDataset,
init_model: Option<&Model>,
) -> Result<Model> {
let objective = match setup {
ObjectiveSetup::Regression => ModelObjective::Regression,
ObjectiveSetup::Binary { sigmoid } => ModelObjective::Binary { sigmoid: *sigmoid },
ObjectiveSetup::LambdaRank { .. } => ModelObjective::LambdaRank,
};
let mut parameters = params.model_parameters();
if let Some(init) = init_model {
if init.objective != objective {
return Err(SilvicError::config(format!(
"init_model has objective {:?}, the parameters train {objective:?}",
init.objective
)));
}
if init.num_features() != train.features.len() {
return Err(SilvicError::config(format!(
"init_model has {} features, the training data has {}",
init.num_features(),
train.features.len()
)));
}
let total = init.num_trees() + params.num_iterations;
if let Some((_, value)) = parameters.iter_mut().find(|(key, _)| key == "num_iterations") {
*value = total.to_string();
}
}
Ok(Model {
objective,
feature_names: train.features.iter().map(|f| f.name.clone()).collect(),
feature_infos: train.features.iter().map(|f| f.bins.info_string()).collect(),
trees: Vec::new(),
parameters,
pandas_categorical: init_model.and_then(|m| m.pandas_categorical.clone()),
})
}
fn thread_pool(num_threads: usize) -> Result<Option<Arc<ThreadPool>>> {
if num_threads == 0 {
return Ok(None);
}
rayon::ThreadPoolBuilder::new()
.num_threads(num_threads)
.thread_name(|index| format!("silvic-{index}"))
.build()
.map(|pool| Some(Arc::new(pool)))
.map_err(|e| SilvicError::config(format!("cannot start {num_threads} threads: {e}")))
}
fn install<R: Send>(pool: Option<&ThreadPool>, op: impl FnOnce() -> R + Send) -> R {
match pool {
Some(pool) => pool.install(op),
None => op(),
}
}
struct Trainer<'a> {
params: &'a Params,
train: &'a BinnedDataset,
valid: &'a [(&'a str, &'a BinnedDataset)],
learner: Learner<'a>,
pool: Option<&'a ThreadPool>,
constant_hessian: Option<f64>,
init_score: f64,
prior_trees: usize,
trees: Vec<Tree>,
}
impl Trainer<'_> {
fn train_one_iter(
&mut self,
backend: &mut dyn Backend,
collective: &mut dyn Collective,
iteration: usize,
) -> Result<bool> {
let params = self.params;
let stats = backend.compute_gradients()?;
let mut maxima = [stats.max_abs_grad, stats.max_hess];
let scale = if params.use_quantized_grad {
collective.allreduce_max_f64(&mut maxima)?;
let spec = QuantSpec::new(
maxima[0],
maxima[1],
self.constant_hessian,
params.num_grad_quant_bins,
params.stochastic_rounding,
params.data_random_seed,
iteration as u32,
);
backend.quantize(&spec)?;
HistScale {
grad: spec.grad_scale,
hess: spec.hess_scale,
}
} else {
HistScale::UNIT
};
let local_rows = backend.reset_partition(&self.bag(iteration))?;
let mut root_count = [i64::from(local_rows)];
collective.allreduce_sum_i64(&mut root_count)?;
let feature_mask = split::feature_mask(
&self.train.features,
params.feature_fraction,
params.feature_fraction_seed,
iteration as u32,
);
let spec = TreeSpec {
quantized: params.use_quantized_grad,
scale,
feature_mask: &feature_mask,
root: crate::backend::LeafRange {
begin: 0,
count: local_rows,
},
root_count: root_count[0],
};
let mut grown = self.learner.grow(backend, collective, spec)?;
if grown.tree.num_leaves == 1 {
self.single_leaf(iteration);
return Ok(false);
}
if params.use_quantized_grad && params.quant_train_renew_leaf {
self.learner
.renew_leaves(backend, collective, &mut grown, maxima[0], maxima[1])?;
}
self.add(backend, grown)?;
Ok(true)
}
fn bag(&self, iteration: usize) -> BagSpec {
let params = self.params;
if params.bagging_fraction < 1.0 && params.bagging_freq > 0 {
BagSpec::Bernoulli {
seed: params.bagging_seed,
round: (iteration / params.bagging_freq) as u32,
fraction: params.bagging_fraction,
}
} else {
BagSpec::All
}
}
fn single_leaf(&mut self, iteration: usize) {
tracing::debug!(iteration, "no split with positive gain; training stops");
if self.prior_trees + self.trees.len() == 0 {
let mut tree = Tree::new();
tree.as_constant(self.init_score);
self.trees.push(tree);
}
}
fn add(&mut self, backend: &mut dyn Backend, mut grown: GrownTree) -> Result<()> {
grown.tree.shrink(self.params.learning_rate);
let mut device = grown.device_tree();
if self.prior_trees + self.trees.len() == 0 && self.init_score != 0.0 {
grown.tree.add_bias(self.init_score);
device.leaf_values = replay::leaf_values(&grown.tree, self.init_score);
}
backend.add_tree(&device)?;
self.trees.push(grown.tree);
Ok(())
}
fn evaluate(&self, backend: &mut dyn Backend, collective: &mut dyn Collective) -> Result<Vec<EvalResult>> {
let mut evals = Vec::new();
for (index, (name, data)) in self.valid.iter().enumerate() {
let scores = backend.scores(index + 1)?;
let collective = &mut *collective;
evals.extend(install(self.pool, || {
metric::evaluate(self.params, name, &scores, data, collective)
})?);
}
Ok(evals)
}
}