1use std::sync::Arc;
16
17use rayon::ThreadPool;
18
19use crate::backend::{Backend, BagSpec, ObjectiveSetup};
20use crate::collective::Collective;
21use crate::config::Params;
22use crate::data::BinnedDataset;
23use crate::error::{Result, SilvicError};
24use crate::learner::{GrownTree, Learner, TreeSpec};
25use crate::metric::{self, EvalResult};
26use crate::model::{Model, ModelObjective};
27use crate::objective;
28use crate::quantize::QuantSpec;
29use crate::replay;
30use crate::split::{self, HistScale, K_EPSILON};
31use crate::tree::Tree;
32
33#[derive(Debug, Clone, PartialEq)]
35pub struct IterationReport<'a> {
36 pub iteration: usize,
38 pub evals: &'a [EvalResult],
39 head: &'a Model,
40 init_trees: &'a [Tree],
41 trees: &'a [Tree],
42}
43
44impl IterationReport<'_> {
45 pub fn model(&self) -> Model {
48 let mut trees = Vec::with_capacity(self.init_trees.len() + self.trees.len());
49 trees.extend_from_slice(self.init_trees);
50 trees.extend_from_slice(self.trees);
51 Model {
52 trees,
53 ..self.head.clone()
54 }
55 }
56}
57
58pub fn train(
64 params: &Params,
65 train: &BinnedDataset,
66 valid: &[(&str, &BinnedDataset)],
67 backend: &mut dyn Backend,
68 collective: &mut dyn Collective,
69 on_iteration: &mut dyn FnMut(&IterationReport<'_>) -> Result<()>,
70) -> Result<Model> {
71 train_from(params, None, train, valid, backend, collective, on_iteration)
72}
73
74pub fn train_from(
84 params: &Params,
85 init_model: Option<&Model>,
86 train: &BinnedDataset,
87 valid: &[(&str, &BinnedDataset)],
88 backend: &mut dyn Backend,
89 collective: &mut dyn Collective,
90 on_iteration: &mut dyn FnMut(&IterationReport<'_>) -> Result<()>,
91) -> Result<Model> {
92 let pool = thread_pool(params.num_threads)?;
93 if let Some(pool) = &pool {
94 backend.set_thread_pool(Arc::clone(pool));
95 }
96 let setup = objective::setup(params, train)?;
97 let init_trees: &[Tree] = init_model.map_or(&[], |m| &m.trees);
98 let head = model_head(params, &setup, train, init_model)?;
99 let valid_sets: Vec<&BinnedDataset> = valid.iter().map(|(_, data)| *data).collect();
100 backend.load(train, &valid_sets, &setup)?;
101 let init_score = install(pool.as_deref(), || {
102 objective::init_score(params, train, &mut *collective)
103 })?;
104 let init_score = if init_score.abs() > K_EPSILON { init_score } else { 0.0 };
106 for dataset in 0..=valid.len() {
107 backend.init_scores(dataset, init_score)?;
108 }
109 for (index, tree) in init_trees.iter().enumerate() {
110 let bias = if index == 0 { init_score } else { 0.0 };
111 backend.add_tree(&replay::device_tree(tree, train, bias)?)?;
112 }
113 tracing::debug!(backend = %backend.name(), init_score, init_trees = init_trees.len(), "training started");
114 let mut trainer = Trainer {
115 params,
116 train,
117 valid,
118 learner: Learner::new(params, &train.features, train.total_bins, pool.as_deref()),
119 pool: pool.as_deref(),
120 constant_hessian: objective::constant_hessian(&setup, train.weight.as_deref()),
121 init_score,
122 prior_trees: init_trees.len(),
123 trees: Vec::new(),
124 };
125 let start = init_trees.len();
126 for iteration in start..start + params.num_iterations {
127 if !trainer.train_one_iter(backend, collective, iteration)? {
128 break;
129 }
130 let evals = trainer.evaluate(backend, collective)?;
131 tracing::debug!(
132 iteration,
133 leaves = trainer.trees.last().map_or(0, |t| t.num_leaves),
134 ?evals,
135 "tree added"
136 );
137 on_iteration(&IterationReport {
138 iteration,
139 evals: &evals,
140 head: &head,
141 init_trees,
142 trees: &trainer.trees,
143 })?;
144 }
145 let mut model = head;
146 model.trees = init_trees.to_vec();
147 model.trees.append(&mut trainer.trees);
148 Ok(model)
149}
150
151fn model_head(
153 params: &Params,
154 setup: &ObjectiveSetup,
155 train: &BinnedDataset,
156 init_model: Option<&Model>,
157) -> Result<Model> {
158 let objective = match setup {
159 ObjectiveSetup::Regression => ModelObjective::Regression,
160 ObjectiveSetup::Binary { sigmoid } => ModelObjective::Binary { sigmoid: *sigmoid },
161 ObjectiveSetup::LambdaRank { .. } => ModelObjective::LambdaRank,
162 };
163 let mut parameters = params.model_parameters();
164 if let Some(init) = init_model {
165 if init.objective != objective {
166 return Err(SilvicError::config(format!(
167 "init_model has objective {:?}, the parameters train {objective:?}",
168 init.objective
169 )));
170 }
171 if init.num_features() != train.features.len() {
172 return Err(SilvicError::config(format!(
173 "init_model has {} features, the training data has {}",
174 init.num_features(),
175 train.features.len()
176 )));
177 }
178 let total = init.num_trees() + params.num_iterations;
179 if let Some((_, value)) = parameters.iter_mut().find(|(key, _)| key == "num_iterations") {
180 *value = total.to_string();
181 }
182 }
183 Ok(Model {
184 objective,
185 feature_names: train.features.iter().map(|f| f.name.clone()).collect(),
186 feature_infos: train.features.iter().map(|f| f.bins.info_string()).collect(),
187 trees: Vec::new(),
188 parameters,
189 pandas_categorical: init_model.and_then(|m| m.pandas_categorical.clone()),
190 })
191}
192
193fn thread_pool(num_threads: usize) -> Result<Option<Arc<ThreadPool>>> {
194 if num_threads == 0 {
195 return Ok(None);
196 }
197 rayon::ThreadPoolBuilder::new()
198 .num_threads(num_threads)
199 .thread_name(|index| format!("silvic-{index}"))
200 .build()
201 .map(|pool| Some(Arc::new(pool)))
202 .map_err(|e| SilvicError::config(format!("cannot start {num_threads} threads: {e}")))
203}
204
205fn install<R: Send>(pool: Option<&ThreadPool>, op: impl FnOnce() -> R + Send) -> R {
206 match pool {
207 Some(pool) => pool.install(op),
208 None => op(),
209 }
210}
211
212struct Trainer<'a> {
213 params: &'a Params,
214 train: &'a BinnedDataset,
215 valid: &'a [(&'a str, &'a BinnedDataset)],
216 learner: Learner<'a>,
217 pool: Option<&'a ThreadPool>,
218 constant_hessian: Option<f64>,
219 init_score: f64,
220 prior_trees: usize,
222 trees: Vec<Tree>,
223}
224
225impl Trainer<'_> {
226 fn train_one_iter(
228 &mut self,
229 backend: &mut dyn Backend,
230 collective: &mut dyn Collective,
231 iteration: usize,
232 ) -> Result<bool> {
233 let params = self.params;
234 let stats = backend.compute_gradients()?;
235 let mut maxima = [stats.max_abs_grad, stats.max_hess];
236 let scale = if params.use_quantized_grad {
237 collective.allreduce_max_f64(&mut maxima)?;
238 let spec = QuantSpec::new(
239 maxima[0],
240 maxima[1],
241 self.constant_hessian,
242 params.num_grad_quant_bins,
243 params.stochastic_rounding,
244 params.data_random_seed,
245 iteration as u32,
246 );
247 backend.quantize(&spec)?;
248 HistScale {
249 grad: spec.grad_scale,
250 hess: spec.hess_scale,
251 }
252 } else {
253 HistScale::UNIT
254 };
255 let local_rows = backend.reset_partition(&self.bag(iteration))?;
256 let mut root_count = [i64::from(local_rows)];
257 collective.allreduce_sum_i64(&mut root_count)?;
258 let feature_mask = split::feature_mask(
259 &self.train.features,
260 params.feature_fraction,
261 params.feature_fraction_seed,
262 iteration as u32,
263 );
264 let spec = TreeSpec {
265 quantized: params.use_quantized_grad,
266 scale,
267 feature_mask: &feature_mask,
268 root: crate::backend::LeafRange {
269 begin: 0,
270 count: local_rows,
271 },
272 root_count: root_count[0],
273 };
274 let mut grown = self.learner.grow(backend, collective, spec)?;
275 if grown.tree.num_leaves == 1 {
276 self.single_leaf(iteration);
277 return Ok(false);
278 }
279 if params.use_quantized_grad && params.quant_train_renew_leaf {
280 self.learner
281 .renew_leaves(backend, collective, &mut grown, maxima[0], maxima[1])?;
282 }
283 self.add(backend, grown)?;
284 Ok(true)
285 }
286
287 fn bag(&self, iteration: usize) -> BagSpec {
289 let params = self.params;
290 if params.bagging_fraction < 1.0 && params.bagging_freq > 0 {
291 BagSpec::Bernoulli {
292 seed: params.bagging_seed,
293 round: (iteration / params.bagging_freq) as u32,
294 fraction: params.bagging_fraction,
295 }
296 } else {
297 BagSpec::All
298 }
299 }
300
301 fn single_leaf(&mut self, iteration: usize) {
304 tracing::debug!(iteration, "no split with positive gain; training stops");
305 if self.prior_trees + self.trees.len() == 0 {
306 let mut tree = Tree::new();
307 tree.as_constant(self.init_score);
308 self.trees.push(tree);
309 }
310 }
311
312 fn add(&mut self, backend: &mut dyn Backend, mut grown: GrownTree) -> Result<()> {
314 grown.tree.shrink(self.params.learning_rate);
315 let mut device = grown.device_tree();
316 if self.prior_trees + self.trees.len() == 0 && self.init_score != 0.0 {
317 grown.tree.add_bias(self.init_score);
318 device.leaf_values = replay::leaf_values(&grown.tree, self.init_score);
320 }
321 backend.add_tree(&device)?;
322 self.trees.push(grown.tree);
323 Ok(())
324 }
325
326 fn evaluate(&self, backend: &mut dyn Backend, collective: &mut dyn Collective) -> Result<Vec<EvalResult>> {
327 let mut evals = Vec::new();
328 for (index, (name, data)) in self.valid.iter().enumerate() {
329 let scores = backend.scores(index + 1)?;
330 let collective = &mut *collective;
331 evals.extend(install(self.pool, || {
332 metric::evaluate(self.params, name, &scores, data, collective)
333 })?);
334 }
335 Ok(evals)
336 }
337}