Skip to main content

silvic_gpu/
backend.rs

1//! `silvic_core::Backend` over any [`GpuDevice`]: the training set's binned rows, gradients, quantized gradients and
2//! partition live in device memory, and every operation is a few kernel launches from `kernels/kernels.kern`.
3//!
4//! Host synchronization is limited to the results the learner reads, one wait each: gradient maxima, the bag count,
5//! leaf sums, scores, and per split one wait for partition and the child's histogram together
6//! (`partition_and_histogram`: the histogram kernel reads the child's rows from the device-side left count).
7//! Everything else (gradients, quantization, tree updates) is queued. The training set's update by a new tree is
8//! deferred until validation scores are read and then started without waiting, so it overlaps the host's metric
9//! evaluation.
10//!
11//! Quantized histograms, partitions, bags, leaf sums and score updates are integer or the same f32 operations as
12//! `silvic_core::CpuBackend`, so they agree exactly; lambdarank and binary gradients agree to f32 rounding. Queries
13//! longer than the largest lambdarank kernel holds are computed on the host with
14//! `silvic_core::objective::lambdarank_query`.
15
16use bytemuck::{Pod, Zeroable};
17use rayon::prelude::*;
18use silvic_core::backend::{Backend, BagSpec, BinSplit, DeviceTree, GradientStats, LeafRange, ObjectiveSetup, Side};
19use silvic_core::data::{BinnedDataset, FeatureGroup};
20use silvic_core::objective::{self, lambdarank::SigmoidTable};
21use silvic_core::quantize::QuantSpec;
22use silvic_core::{Result, SilvicError};
23
24use crate::device::{Arg, GpuDevice};
25use crate::params::{
26    kernel_source, BagParams, CopyParams, FillParams, GradParams, GroupLayout, HistParams, HistUnit, LeafSumParams,
27    PartitionParams, QuantParams, RankParams, ScanParams, StatsParams, TreeNode, TreeParams, HIST_FLOAT_BINS,
28    HIST_QUANT_BINS, HIST_SIDE_LEAF, HIST_SIDE_LEFT, HIST_SIDE_RIGHT, LAMBDARANK_CLASSES, REDUCE_THREADS, SCAN_THREADS,
29};
30
31/// The kernels of `kernels.kern`, in [`KERNEL_NAMES`] order.
32#[derive(Debug, Clone, Copy)]
33enum Kernel {
34    FillU32,
35    IotaU32,
36    CopyU32,
37    GradRegression,
38    GradBinary,
39    GradStats,
40    LambdarankSmall,
41    LambdarankMedium,
42    LambdarankLarge,
43    Quantize,
44    BagCount,
45    ScanBlocks,
46    BagScatter,
47    PartitionCount,
48    PartitionScatter,
49    HistQuant,
50    HistFloat,
51    LeafSums,
52    AddTree,
53}
54
55const KERNEL_NAMES: [&str; 19] = [
56    "fill_u32",
57    "iota_u32",
58    "copy_u32",
59    "grad_regression",
60    "grad_binary",
61    "grad_stats",
62    "lambdarank_small",
63    "lambdarank_medium",
64    "lambdarank_large",
65    "quantize",
66    "bag_count",
67    "scan_blocks",
68    "bag_scatter",
69    "partition_count",
70    "partition_scatter",
71    "hist_quant",
72    "hist_float",
73    "leaf_sums",
74    "add_tree",
75];
76
77const LAMBDARANK_KERNELS: [Kernel; 3] = [
78    Kernel::LambdarankSmall,
79    Kernel::LambdarankMedium,
80    Kernel::LambdarankLarge,
81];
82
83/// Threadgroups of a grid-stride kernel: enough to fill any current GPU, within every API's grid limit.
84const MAX_GROUPS: u32 = 65_535;
85/// Threads of the elementwise kernels.
86const ELEMENT_THREADS: u32 = 256;
87/// Threadgroups of `grad_stats`; the host reduces this many partials.
88const STATS_GROUPS: u32 = 1024;
89/// Fewest rows one compaction (bag, partition) threadgroup handles; more when a leaf needs more than
90/// `MAX_COMPACT_BLOCKS` such chunks.
91const COMPACT_CHUNK: u32 = 4096;
92/// Most threadgroups of one compaction: the length of the block-count buffer `scan_blocks` scans.
93const MAX_COMPACT_BLOCKS: u32 = 65_536;
94/// Threads of the histogram kernels, when the device allows that many: with 32 KiB of threadgroup memory per
95/// threadgroup, a full-size group hides the most memory latency (measured on an M4 Pro: 1024 beats 512 and 256).
96const HIST_THREADS: u32 = 1024;
97/// Histogram threadgroups (row blocks times work units) a launch aims for: enough to occupy the GPU, few enough
98/// that zeroing and flushing the shared histograms stays small next to the row work.
99const HIST_TARGET_GROUPS: u32 = 1024;
100/// Fewest rows of a histogram row block, below which flushing dominates.
101const HIST_MIN_ROWS_PER_BLOCK: u32 = 1024;
102/// Most rows of a float histogram row block, bounding each f32 partial sum.
103const HIST_FLOAT_MAX_ROWS_PER_BLOCK: u32 = 8192;
104/// Rows of one `leaf_sums` threadgroup.
105const LEAF_SUM_ROWS: u32 = REDUCE_THREADS * 16;
106/// Rows per task when counting each column's most frequent bin at load.
107const MFB_CHUNK_ROWS: usize = 65_536;
108
109/// A dataset's device buffers.
110struct DatasetBuffers<B> {
111    num_rows: u32,
112    /// Every group's row-major bins, one group after another.
113    bins: B,
114    /// `GroupLayout` per group.
115    groups: B,
116    layouts: Vec<GroupLayout>,
117    scores: B,
118}
119
120/// Histogram work units of the training set for one shared-histogram capacity.
121struct HistUnits<B> {
122    units: B,
123    col_map: B,
124    count: u32,
125}
126
127/// Lambdarank inputs, with queries sorted into the kernels' size classes.
128struct RankSetup<B> {
129    query_bounds: B,
130    queries: B,
131    /// `(first index into queries, number of queries)` per entry of `LAMBDARANK_CLASSES`.
132    classes: [(u32, u32); 3],
133    inverse_max_dcg: B,
134    label_gain: B,
135    discount: B,
136    /// LightGBM's pair sigmoid table as f32.
137    sigmoid_table: B,
138    params: RankParams,
139    /// Queries longer than every kernel class holds, computed on the host.
140    host_queries: Vec<usize>,
141    host_bounds: Vec<u32>,
142    host_label: Vec<f32>,
143    host_weight: Option<Vec<f32>>,
144}
145
146/// A device buffer that grows to the largest size asked of it.
147struct Scratch<B> {
148    buffer: B,
149    capacity: usize,
150}
151
152struct Loaded<B> {
153    datasets: Vec<DatasetBuffers<B>>,
154    num_rows: u32,
155    row_offset: u64,
156    total_bins: usize,
157    objective: ObjectiveSetup,
158    label: B,
159    weight: Option<B>,
160    /// Bound in place of an absent weight buffer.
161    dummy: B,
162    grad: B,
163    hess: B,
164    packed: B,
165    partition: B,
166    scratch_rows: B,
167    /// One byte per row of the leaf being partitioned: whether it goes left.
168    split_flags: B,
169    block_counts: B,
170    scan_total: B,
171    stats_partials: B,
172    /// `2 * total_bins` i32 (quantized) or f32 (float) histogram.
173    hist: B,
174    quant_units: HistUnits<B>,
175    float_units: HistUnits<B>,
176    leaf_partials: Scratch<B>,
177    /// `(begin, end)` partition positions of each `leaf_sums` threadgroup.
178    leaf_jobs: Scratch<B>,
179    nodes: Scratch<B>,
180    leaf_values: Scratch<B>,
181    rank: Option<RankSetup<B>>,
182    quant: Option<QuantSpec>,
183    /// Node count of the last tree when its update of the training scores is not encoded yet (`add_tree`).
184    deferred_tree: Option<u32>,
185}
186
187/// `silvic_core::Backend` over any [`GpuDevice`].
188pub struct GpuBackend<D: GpuDevice> {
189    state: Option<Loaded<D::Buffer>>,
190    compiled: bool,
191    device: D,
192}
193
194impl<D: GpuDevice> GpuBackend<D> {
195    /// Wraps `device`; kernels compile on [`GpuBackend::compile`] or the first `load`.
196    pub fn new(device: D) -> Self {
197        GpuBackend {
198            state: None,
199            compiled: false,
200            device,
201        }
202    }
203
204    pub fn device(&self) -> &D {
205        &self.device
206    }
207
208    /// Compiles the kernels for the device, once.
209    pub fn compile(&mut self) -> Result<()> {
210        if !self.compiled {
211            self.device.compile(&kernel_source(D::PRELUDE), &KERNEL_NAMES)?;
212            self.compiled = true;
213        }
214        Ok(())
215    }
216
217    /// The training gradients and hessians of the last `compute_gradients`, read back for inspection.
218    pub fn gradients(&self) -> Result<(Vec<f32>, Vec<f32>)> {
219        let state = self.loaded()?;
220        let n = state.num_rows as usize;
221        Ok((
222            download_vec(&self.device, &state.grad, 0, n)?,
223            download_vec(&self.device, &state.hess, 0, n)?,
224        ))
225    }
226
227    fn loaded(&self) -> Result<&Loaded<D::Buffer>> {
228        self.state
229            .as_ref()
230            .ok_or_else(|| SilvicError::device("backend used before load"))
231    }
232}
233
234fn launch<D: GpuDevice>(
235    device: &D,
236    kernel: Kernel,
237    grid: [u32; 2],
238    threads: u32,
239    args: &[Arg<'_, D::Buffer>],
240) -> Result<()> {
241    device.launch(kernel as usize, [grid[0], grid[1], 1], [threads, 1, 1], args)
242}
243
244/// Threadgroups of a grid-stride kernel over `n` items.
245fn groups_for(n: u32, threads: u32) -> u32 {
246    n.div_ceil(threads).clamp(1, MAX_GROUPS)
247}
248
249/// A parameter struct argument.
250fn params<B, T: Pod>(value: &T) -> Arg<'_, B> {
251    Arg::Bytes(bytemuck::bytes_of(value))
252}
253
254fn new_buffer<D: GpuDevice>(device: &D, data: &[u8]) -> Result<D::Buffer> {
255    let buffer = device.alloc(data.len())?;
256    if !data.is_empty() {
257        device.upload(&buffer, 0, data)?;
258    }
259    Ok(buffer)
260}
261
262fn zeroed_vec<T: Pod + Zeroable>(len: usize) -> Vec<T> {
263    vec![T::zeroed(); len]
264}
265
266fn download_vec<D: GpuDevice, T: Pod>(
267    device: &D,
268    buffer: &D::Buffer,
269    offset_items: usize,
270    len: usize,
271) -> Result<Vec<T>> {
272    let mut out: Vec<T> = zeroed_vec(len);
273    device.download(
274        buffer,
275        offset_items * std::mem::size_of::<T>(),
276        bytemuck::cast_slice_mut(&mut out),
277    )?;
278    Ok(out)
279}
280
281fn split_u64(value: u64) -> (u32, u32) {
282    (value as u32, (value >> 32) as u32)
283}
284
285/// `value` as an f32 pair `(hi, lo)` with `hi + lo` within ~2^-48 relative of it, for double-float kernel arithmetic.
286fn split_f64(value: f64) -> (f32, f32) {
287    let hi = value as f32;
288    (hi, (value - f64::from(hi)) as f32)
289}
290
291fn rows_u32(rows: usize) -> Result<u32> {
292    u32::try_from(rows).map_err(|_| SilvicError::unsupported(format!("{rows} rows on one device (at most 2^32 - 1)")))
293}
294
295fn upload_dataset<D: GpuDevice>(device: &D, data: &BinnedDataset) -> Result<DatasetBuffers<D::Buffer>> {
296    let num_rows = rows_u32(data.num_rows)?;
297    let mut layouts = Vec::with_capacity(data.groups.len());
298    let mut offset = 0u64;
299    for group in &data.groups {
300        let len = data.num_rows as u64 * u64::from(group.row_stride);
301        if group.data.len() as u64 != len || group.row_stride % 4 != 0 {
302            return Err(SilvicError::data(format!(
303                "feature group of {} bytes for {} rows of stride {}",
304                group.data.len(),
305                data.num_rows,
306                group.row_stride
307            )));
308        }
309        let (base_lo, base_hi) = split_u64(offset);
310        layouts.push(GroupLayout {
311            base_lo,
312            base_hi,
313            row_stride: group.row_stride,
314            pad: 0,
315        });
316        offset += len;
317    }
318    let total =
319        usize::try_from(offset).map_err(|_| SilvicError::unsupported("binned data exceeds the address space"))?;
320    let bins = device.alloc(total)?;
321    for (group, layout) in data.groups.iter().zip(&layouts) {
322        let base = (u64::from(layout.base_hi) << 32) | u64::from(layout.base_lo);
323        if !group.data.is_empty() {
324            device.upload(&bins, base as usize, &group.data)?;
325        }
326    }
327    Ok(DatasetBuffers {
328        num_rows,
329        groups: new_buffer(device, bytemuck::cast_slice(&layouts))?,
330        layouts,
331        bins,
332        scores: device.alloc(data.num_rows * 4)?,
333    })
334}
335
336/// The most frequent bin of every column of `group` (LightGBM's `most_freq_bin`).
337fn most_frequent_bins(group: &FeatureGroup) -> Vec<u8> {
338    let stride = group.row_stride as usize;
339    if stride == 0 {
340        return Vec::new();
341    }
342    let counts = group
343        .data
344        .par_chunks(stride * MFB_CHUNK_ROWS)
345        .map(|rows| {
346            let mut counts = vec![0u32; stride * 256];
347            for row in rows.chunks_exact(stride) {
348                for (column, &bin) in row.iter().enumerate() {
349                    counts[column * 256 + usize::from(bin)] += 1;
350                }
351            }
352            counts
353        })
354        .reduce(
355            || vec![0u32; stride * 256],
356            |mut total, part| {
357                total.iter_mut().zip(part).for_each(|(t, p)| *t += p);
358                total
359            },
360        );
361    counts
362        .as_chunks::<256>()
363        .0
364        .iter()
365        .map(|column| {
366            // The lowest of equally frequent bins.
367            let best = column
368                .iter()
369                .enumerate()
370                .max_by_key(|&(bin, &count)| (count, std::cmp::Reverse(bin)));
371            best.map_or(0, |(bin, _)| bin as u8)
372        })
373        .collect()
374}
375
376/// Splits every group's features (in bin order) into runs whose bins fit `max_bins`. A `col_map` entry packs the
377/// column's unit-relative bin offset with its most frequent bin in bits 16..24 (`kernels.kern`, `hist_*`).
378fn hist_units(data: &BinnedDataset, max_bins: u32, most_frequent: &[Vec<u8>]) -> Result<(Vec<HistUnit>, Vec<i32>)> {
379    let mut units = Vec::new();
380    let mut col_map = Vec::new();
381    let mut words_base = 0u64;
382    for (group, group_mfb) in data.groups.iter().zip(most_frequent) {
383        let mut features = group.features.clone();
384        features.sort_by_key(|&f| data.features[f].hist_offset);
385        let mut start = 0;
386        while start < features.len() {
387            let first = data.features[features[start]].hist_offset;
388            let mut end = start;
389            let mut extent = 0;
390            while end < features.len() {
391                let meta = &data.features[features[end]];
392                let next_extent = meta.hist_offset + meta.num_bins - first;
393                if next_extent > max_bins {
394                    break;
395                }
396                extent = next_extent;
397                end += 1;
398            }
399            if end == start {
400                return Err(SilvicError::data(format!("a feature has more than {max_bins} bins")));
401            }
402            let cols_begin = col_map.len();
403            col_map.resize(cols_begin + group.row_stride as usize, -1);
404            for &feature in &features[start..end] {
405                let meta = &data.features[feature];
406                let column = meta.column as usize;
407                col_map[cols_begin + column] =
408                    ((meta.hist_offset - first) | (u32::from(group_mfb[column]) << 16)) as i32;
409            }
410            let (words_base_lo, words_base_hi) = split_u64(words_base);
411            units.push(HistUnit {
412                words_base_lo,
413                words_base_hi,
414                row_words: group.row_stride / 4,
415                bin_begin: first,
416                num_bins: extent,
417                cols_begin: cols_begin as u32,
418                pad: [0; 2],
419            });
420            start = end;
421        }
422        words_base += data.num_rows as u64 * u64::from(group.row_stride / 4);
423    }
424    Ok((units, col_map))
425}
426
427fn upload_units<D: GpuDevice>(
428    device: &D,
429    data: &BinnedDataset,
430    max_bins: u32,
431    most_frequent: &[Vec<u8>],
432) -> Result<HistUnits<D::Buffer>> {
433    let (units, col_map) = hist_units(data, max_bins, most_frequent)?;
434    Ok(HistUnits {
435        units: new_buffer(device, bytemuck::cast_slice(&units))?,
436        col_map: new_buffer(device, bytemuck::cast_slice(&col_map))?,
437        count: units.len() as u32,
438    })
439}
440
441/// `objective::lambdarank::discount` (LightGBM `DCGCalculator::GetDiscount`) for every rank a lambdarank kernel
442/// holds.
443fn discounts() -> Vec<f32> {
444    let ranks = LAMBDARANK_CLASSES[LAMBDARANK_CLASSES.len() - 1].0 as usize;
445    (0..ranks)
446        .map(|rank| objective::lambdarank::discount(rank) as f32)
447        .collect()
448}
449
450fn check_rank_labels(label: &[f32], label_gain: &[f64]) -> Result<()> {
451    let valid = |&value: &f32| value >= 0.0 && value.fract() == 0.0 && (value as usize) < label_gain.len();
452    match label.iter().find(|value| !valid(value)) {
453        Some(value) => Err(SilvicError::data(format!(
454            "lambdarank label {value} is not an integer index into label_gain ({} entries)",
455            label_gain.len()
456        ))),
457        None => Ok(()),
458    }
459}
460
461/// Queries per lambdarank kernel class, and the queries no class holds.
462fn classify_queries(bounds: &[u32]) -> ([Vec<u32>; 3], Vec<usize>) {
463    let mut classes: [Vec<u32>; 3] = Default::default();
464    let mut host = Vec::new();
465    for query in 0..bounds.len() - 1 {
466        let size = bounds[query + 1] - bounds[query];
467        match LAMBDARANK_CLASSES.iter().position(|&(capacity, _)| size <= capacity) {
468            Some(class) => classes[class].push(query as u32),
469            None => host.push(query),
470        }
471    }
472    (classes, host)
473}
474
475fn rank_setup<D: GpuDevice>(
476    device: &D,
477    train: &BinnedDataset,
478    setup: &ObjectiveSetup,
479) -> Result<Option<RankSetup<D::Buffer>>> {
480    let ObjectiveSetup::LambdaRank {
481        sigmoid,
482        norm,
483        truncation_level,
484        label_gain,
485        inverse_max_dcg,
486    } = setup
487    else {
488        return Ok(None);
489    };
490    let bounds = train
491        .query_boundaries
492        .as_deref()
493        .ok_or_else(|| SilvicError::data("lambdarank needs query boundaries"))?;
494    if bounds.len() != inverse_max_dcg.len() + 1 || bounds.last().copied() != Some(train.num_rows as u32) {
495        return Err(SilvicError::data(
496            "query boundaries do not match the rows and inverse max DCGs",
497        ));
498    }
499    check_rank_labels(&train.label, label_gain)?;
500    let (lists, host_queries) = classify_queries(bounds);
501    let mut classes = [(0u32, 0u32); 3];
502    let mut queries = Vec::with_capacity(lists.iter().map(Vec::len).sum());
503    for (class, list) in classes.iter_mut().zip(&lists) {
504        *class = (queries.len() as u32, list.len() as u32);
505        queries.extend_from_slice(list);
506    }
507    let gains: Vec<f32> = label_gain.iter().map(|&g| g as f32).collect();
508    let inverse: Vec<f32> = inverse_max_dcg.iter().map(|&v| v as f32).collect();
509    let table = SigmoidTable::shared(*sigmoid);
510    let table_values: Vec<f32> = table.values().iter().map(|&v| v as f32).collect();
511    let keep_host = !host_queries.is_empty();
512    Ok(Some(RankSetup {
513        query_bounds: new_buffer(device, bytemuck::cast_slice(bounds))?,
514        queries: new_buffer(device, bytemuck::cast_slice(&queries))?,
515        classes,
516        inverse_max_dcg: new_buffer(device, bytemuck::cast_slice(&inverse))?,
517        label_gain: new_buffer(device, bytemuck::cast_slice(&gains))?,
518        discount: new_buffer(device, bytemuck::cast_slice(&discounts()))?,
519        sigmoid_table: new_buffer(device, bytemuck::cast_slice(&table_values))?,
520        params: RankParams {
521            query_begin: 0,
522            num_queries: 0,
523            has_weight: u32::from(train.weight.is_some()),
524            norm: u32::from(*norm),
525            truncation_level: *truncation_level,
526            sigmoid: *sigmoid as f32,
527            sigmoid_min_hi: split_f64(table.min_input()).0,
528            sigmoid_min_lo: split_f64(table.min_input()).1,
529            sigmoid_factor_hi: split_f64(table.idx_factor()).0,
530            sigmoid_factor_lo: split_f64(table.idx_factor()).1,
531            sigmoid_last: (table.values().len() - 1) as u32,
532            pad: 0,
533        },
534        host_queries,
535        host_bounds: if keep_host { bounds.to_vec() } else { Vec::new() },
536        host_label: if keep_host { train.label.clone() } else { Vec::new() },
537        host_weight: if keep_host { train.weight.clone() } else { None },
538    }))
539}
540
541fn scratch<D: GpuDevice>(device: &D, capacity: usize, item_bytes: usize) -> Result<Scratch<D::Buffer>> {
542    Ok(Scratch {
543        buffer: device.alloc(capacity * item_bytes)?,
544        capacity,
545    })
546}
547
548/// Grows `scratch` to hold `len` items (contents are not kept).
549fn ensure_capacity<D: GpuDevice>(
550    device: &D,
551    scratch: &mut Scratch<D::Buffer>,
552    len: usize,
553    item_bytes: usize,
554) -> Result<()> {
555    if len > scratch.capacity {
556        let capacity = len.next_power_of_two();
557        scratch.buffer = device.alloc(capacity * item_bytes)?;
558        scratch.capacity = capacity;
559    }
560    Ok(())
561}
562
563fn load_train<D: GpuDevice>(
564    device: &D,
565    train: &BinnedDataset,
566    datasets: Vec<DatasetBuffers<D::Buffer>>,
567    objective: &ObjectiveSetup,
568) -> Result<Loaded<D::Buffer>> {
569    let n = train.num_rows;
570    if train.label.len() != n || train.weight.as_ref().is_some_and(|w| w.len() != n) {
571        return Err(SilvicError::data("label or weight length differs from the row count"));
572    }
573    let most_frequent: Vec<Vec<u8>> = train.groups.iter().map(most_frequent_bins).collect();
574    Ok(Loaded {
575        num_rows: rows_u32(n)?,
576        row_offset: train.row_offset,
577        total_bins: train.total_bins,
578        objective: objective.clone(),
579        label: new_buffer(device, bytemuck::cast_slice(&train.label))?,
580        weight: train
581            .weight
582            .as_ref()
583            .map(|w| new_buffer(device, bytemuck::cast_slice(w)))
584            .transpose()?,
585        dummy: device.alloc(4)?,
586        grad: device.alloc(n * 4)?,
587        hess: device.alloc(n * 4)?,
588        packed: device.alloc(n * 4)?,
589        partition: device.alloc(n * 4)?,
590        scratch_rows: device.alloc(n * 4)?,
591        split_flags: device.alloc(n)?,
592        block_counts: device.alloc(MAX_COMPACT_BLOCKS as usize * 4)?,
593        scan_total: device.alloc(4)?,
594        stats_partials: device.alloc(STATS_GROUPS as usize * 8)?,
595        hist: device.alloc(train.total_bins * 8)?,
596        quant_units: upload_units(device, train, HIST_QUANT_BINS, &most_frequent)?,
597        float_units: upload_units(device, train, HIST_FLOAT_BINS, &most_frequent)?,
598        leaf_partials: scratch(device, 1024, 16)?,
599        leaf_jobs: scratch(device, 1024, 8)?,
600        nodes: scratch(device, 64, std::mem::size_of::<TreeNode>())?,
601        leaf_values: scratch(device, 64, 4)?,
602        rank: rank_setup(device, train, objective)?,
603        quant: None,
604        deferred_tree: None,
605        datasets,
606    })
607}
608
609/// Rows per threadgroup of a compaction over `count` rows: a multiple of the threadgroup size, so that at most
610/// `MAX_COMPACT_BLOCKS` chunks cover the rows.
611fn compact_chunk(count: u32) -> u32 {
612    let chunk = count.div_ceil(MAX_COMPACT_BLOCKS).max(COMPACT_CHUNK);
613    chunk.div_ceil(SCAN_THREADS) * SCAN_THREADS
614}
615
616/// Rows per histogram row block of a `count`-row leaf over `units` work units, at most `cap`.
617fn hist_rows_per_block(count: u32, units: u32, cap: u32) -> u32 {
618    let blocks = HIST_TARGET_GROUPS.div_ceil(units.max(1));
619    count.div_ceil(blocks).max(HIST_MIN_ROWS_PER_BLOCK).min(cap).max(1)
620}
621
622/// Rows one packed shared histogram takes before an i16 half could overflow: `QuantSpec::max_rows_per_packed_block`,
623/// except that a constant hessian adds exactly 1 per row, so only the gradient half bounds it.
624fn packed_block_rows(spec: &QuantSpec) -> u32 {
625    let rows = if spec.constant_hessian {
626        (i32::from(i16::MAX) / spec.grad_max_q.max(1)) as u32
627    } else {
628        spec.max_rows_per_packed_block()
629    };
630    rows.max(1)
631}
632
633impl<B> Loaded<B> {
634    fn check_leaf(&self, leaf: LeafRange) -> Result<()> {
635        match leaf.begin.checked_add(leaf.count) {
636            Some(end) if end <= self.num_rows => Ok(()),
637            _ => Err(SilvicError::device(format!(
638                "leaf {}+{} outside the {} training rows",
639                leaf.begin, leaf.count, self.num_rows
640            ))),
641        }
642    }
643
644    fn check_hist_out(&self, out: &[f64]) -> Result<()> {
645        if out.len() == 2 * self.total_bins {
646            Ok(())
647        } else {
648            Err(SilvicError::device(format!(
649                "histogram buffer of {} values for {} bins",
650                out.len(),
651                self.total_bins
652            )))
653        }
654    }
655
656    fn weight_or_dummy(&self) -> &B {
657        self.weight.as_ref().unwrap_or(&self.dummy)
658    }
659
660    fn dataset(&self, dataset: usize) -> Result<&DatasetBuffers<B>> {
661        self.datasets
662            .get(dataset)
663            .ok_or_else(|| SilvicError::device(format!("dataset {dataset} was not loaded")))
664    }
665}
666
667impl<D: GpuDevice> GpuBackend<D> {
668    fn pointwise_gradients(&self, kernel: Kernel, sigmoid: f32) -> Result<()> {
669        let state = self.loaded()?;
670        let p = GradParams {
671            n: state.num_rows,
672            has_weight: u32::from(state.weight.is_some()),
673            sigmoid,
674            pad: 0,
675        };
676        let train = state.dataset(0)?;
677        launch(
678            &self.device,
679            kernel,
680            [groups_for(state.num_rows, ELEMENT_THREADS), 1],
681            ELEMENT_THREADS,
682            &[
683                Arg::Buffer(&train.scores),
684                Arg::Buffer(&state.label),
685                Arg::Buffer(state.weight_or_dummy()),
686                Arg::Buffer(&state.grad),
687                Arg::Buffer(&state.hess),
688                params(&p),
689            ],
690        )
691    }
692
693    fn lambdarank_gradients(&self) -> Result<()> {
694        let state = self.loaded()?;
695        let rank = state
696            .rank
697            .as_ref()
698            .ok_or_else(|| SilvicError::device("lambdarank was not set up"))?;
699        let train = state.dataset(0)?;
700        for (class, &(query_begin, num_queries)) in rank.classes.iter().enumerate() {
701            if num_queries == 0 {
702                continue;
703            }
704            let p = RankParams {
705                query_begin,
706                num_queries,
707                ..rank.params
708            };
709            launch(
710                &self.device,
711                LAMBDARANK_KERNELS[class],
712                [num_queries.min(MAX_GROUPS), 1],
713                LAMBDARANK_CLASSES[class].1,
714                &[
715                    Arg::Buffer(&train.scores),
716                    Arg::Buffer(&state.label),
717                    Arg::Buffer(state.weight_or_dummy()),
718                    Arg::Buffer(&rank.query_bounds),
719                    Arg::Buffer(&rank.queries),
720                    Arg::Buffer(&rank.inverse_max_dcg),
721                    Arg::Buffer(&rank.label_gain),
722                    Arg::Buffer(&rank.discount),
723                    Arg::Buffer(&rank.sigmoid_table),
724                    Arg::Buffer(&state.grad),
725                    Arg::Buffer(&state.hess),
726                    params(&p),
727                ],
728            )?;
729        }
730        self.host_lambdarank(state, rank)
731    }
732
733    /// `objective::lambdarank_query` for the queries too long for the kernels: their scores come down, their
734    /// gradients go back up.
735    fn host_lambdarank(&self, state: &Loaded<D::Buffer>, rank: &RankSetup<D::Buffer>) -> Result<()> {
736        let train = state.dataset(0)?;
737        for &query in &rank.host_queries {
738            let start = rank.host_bounds[query] as usize;
739            let end = rank.host_bounds[query + 1] as usize;
740            let scores: Vec<f32> = download_vec(&self.device, &train.scores, start, end - start)?;
741            let mut grad = vec![0f32; end - start];
742            let mut hess = vec![0f32; end - start];
743            let weight = rank.host_weight.as_ref().map(|w| &w[start..end]);
744            objective::lambdarank_query(
745                &state.objective,
746                query,
747                &scores,
748                &rank.host_label[start..end],
749                weight,
750                &mut grad,
751                &mut hess,
752            );
753            self.device
754                .upload(&state.grad, start * 4, bytemuck::cast_slice(&grad))?;
755            self.device
756                .upload(&state.hess, start * 4, bytemuck::cast_slice(&hess))?;
757        }
758        Ok(())
759    }
760
761    fn gradient_stats(&self) -> Result<GradientStats> {
762        let state = self.loaded()?;
763        let groups = state.num_rows.div_ceil(REDUCE_THREADS).clamp(1, STATS_GROUPS);
764        let p = StatsParams {
765            n: state.num_rows,
766            pad: [0; 3],
767        };
768        launch(
769            &self.device,
770            Kernel::GradStats,
771            [groups, 1],
772            REDUCE_THREADS,
773            &[
774                Arg::Buffer(&state.grad),
775                Arg::Buffer(&state.hess),
776                Arg::Buffer(&state.stats_partials),
777                params(&p),
778            ],
779        )?;
780        let partials: Vec<f32> = download_vec(&self.device, &state.stats_partials, 0, 2 * groups as usize)?;
781        let mut stats = GradientStats::default();
782        for pair in partials.as_chunks::<2>().0 {
783            stats.max_abs_grad = stats.max_abs_grad.max(f64::from(pair[0]));
784            stats.max_hess = stats.max_hess.max(f64::from(pair[1]));
785        }
786        Ok(stats)
787    }
788
789    /// Encodes `add_tree` for `dataset` with the uploaded tree of `num_nodes` nodes.
790    fn encode_add_tree(&self, state: &Loaded<D::Buffer>, dataset: usize, num_nodes: u32) -> Result<()> {
791        let data = state.dataset(dataset)?;
792        let p = TreeParams {
793            n: data.num_rows,
794            num_nodes,
795            pad: [0; 2],
796        };
797        launch(
798            &self.device,
799            Kernel::AddTree,
800            [groups_for(data.num_rows, ELEMENT_THREADS), 1],
801            ELEMENT_THREADS,
802            &[
803                Arg::Buffer(&data.bins),
804                Arg::Buffer(&data.groups),
805                Arg::Buffer(&state.nodes.buffer),
806                Arg::Buffer(&state.leaf_values.buffer),
807                Arg::Buffer(&data.scores),
808                params(&p),
809            ],
810        )
811    }
812
813    /// Encodes the training set's update by the last tree, when `add_tree` deferred it.
814    fn encode_deferred_tree(&mut self) -> Result<()> {
815        let Some(num_nodes) = self.state.as_mut().and_then(|state| state.deferred_tree.take()) else {
816            return Ok(());
817        };
818        self.encode_add_tree(self.loaded()?, 0, num_nodes)
819    }
820
821    fn hist_threads(&self) -> u32 {
822        HIST_THREADS.min(self.device.max_threads_per_group())
823    }
824
825    fn fill(&self, buffer: &D::Buffer, n: u32, value: u32) -> Result<()> {
826        let p = FillParams { n, value };
827        launch(
828            &self.device,
829            Kernel::FillU32,
830            [groups_for(n, ELEMENT_THREADS), 1],
831            ELEMENT_THREADS,
832            &[Arg::Buffer(buffer), params(&p)],
833        )
834    }
835
836    /// Exclusive scan of the first `blocks` block counts; the total lands in `scan_total`.
837    fn scan_block_counts(&self, state: &Loaded<D::Buffer>, blocks: u32) -> Result<()> {
838        let p = ScanParams { n: blocks, pad: [0; 3] };
839        launch(
840            &self.device,
841            Kernel::ScanBlocks,
842            [1, 1],
843            SCAN_THREADS,
844            &[
845                Arg::Buffer(&state.block_counts),
846                Arg::Buffer(&state.scan_total),
847                params(&p),
848            ],
849        )
850    }
851
852    fn read_scan_total(&self, state: &Loaded<D::Buffer>) -> Result<u32> {
853        let total: Vec<u32> = download_vec(&self.device, &state.scan_total, 0, 1)?;
854        Ok(total[0])
855    }
856
857    fn bernoulli_bag(&self, seed: u32, round: u32, fraction: f64) -> Result<u32> {
858        let state = self.loaded()?;
859        let n = state.num_rows;
860        if n == 0 {
861            return Ok(0);
862        }
863        let chunk = compact_chunk(n);
864        let blocks = n.div_ceil(chunk);
865        let (row_offset_lo, row_offset_hi) = split_u64(state.row_offset);
866        let p = BagParams {
867            n,
868            chunk,
869            row_offset_lo,
870            row_offset_hi,
871            seed,
872            round,
873            threshold: bag_threshold(fraction),
874            pad: 0,
875        };
876        launch(
877            &self.device,
878            Kernel::BagCount,
879            [blocks, 1],
880            SCAN_THREADS,
881            &[Arg::Buffer(&state.block_counts), params(&p)],
882        )?;
883        self.scan_block_counts(state, blocks)?;
884        launch(
885            &self.device,
886            Kernel::BagScatter,
887            [blocks, 1],
888            SCAN_THREADS,
889            &[
890                Arg::Buffer(&state.block_counts),
891                Arg::Buffer(&state.partition),
892                params(&p),
893            ],
894        )?;
895        self.read_scan_total(state)
896    }
897
898    /// Encodes the histogram of `leaf`'s rows (`side` `HIST_SIDE_LEAF`), or of one child of `leaf` after a partition
899    /// encoded just before (the kernel reads the left count from `scan_total`). Quantized launches must cover at
900    /// most `rows_per_launch` rows.
901    fn encode_histogram(&self, state: &Loaded<D::Buffer>, leaf: LeafRange, side: u32, quantized: bool) -> Result<()> {
902        let (kernel, units, cap) = if quantized {
903            let spec = state
904                .quant
905                .ok_or_else(|| SilvicError::device("quantized histogram before quantize"))?;
906            (Kernel::HistQuant, &state.quant_units, packed_block_rows(&spec))
907        } else {
908            (Kernel::HistFloat, &state.float_units, HIST_FLOAT_MAX_ROWS_PER_BLOCK)
909        };
910        // A child's size is unknown until the partition runs: blocks are sized for half the parent and the grid
911        // covers all of it.
912        let expected = if side == HIST_SIDE_LEAF {
913            leaf.count
914        } else {
915            leaf.count.div_ceil(2)
916        };
917        let rows_per_block = hist_rows_per_block(expected, units.count, cap);
918        let p = HistParams {
919            begin: leaf.begin,
920            count: leaf.count,
921            rows_per_block,
922            side,
923        };
924        self.fill(&state.hist, (2 * state.total_bins) as u32, 0)?;
925        let grid = [leaf.count.div_ceil(rows_per_block), units.count];
926        let bins = &state.datasets[0].bins;
927        let tail = [
928            Arg::Buffer(bins),
929            Arg::Buffer(&units.units),
930            Arg::Buffer(&units.col_map),
931            Arg::Buffer(&state.hist),
932            Arg::Buffer(&state.scan_total),
933            params(&p),
934        ];
935        let args: Vec<Arg<'_, D::Buffer>> = if quantized {
936            [Arg::Buffer(&state.partition), Arg::Buffer(&state.packed)]
937                .into_iter()
938                .chain(tail)
939                .collect()
940        } else {
941            [
942                Arg::Buffer(&state.partition),
943                Arg::Buffer(&state.grad),
944                Arg::Buffer(&state.hess),
945            ]
946            .into_iter()
947            .chain(tail)
948            .collect()
949        };
950        launch(&self.device, kernel, grid, self.hist_threads(), &args)
951    }
952
953    /// Adds the histogram the last `encode_histogram` built into `out`.
954    fn read_histogram(&self, state: &Loaded<D::Buffer>, quantized: bool, out: &mut [f64]) -> Result<()> {
955        let bins = 2 * state.total_bins;
956        if quantized {
957            let sums: Vec<i32> = download_vec(&self.device, &state.hist, 0, bins)?;
958            out.iter_mut().zip(&sums).for_each(|(out, &sum)| *out += f64::from(sum));
959        } else {
960            let sums: Vec<f32> = download_vec(&self.device, &state.hist, 0, bins)?;
961            out.iter_mut().zip(&sums).for_each(|(out, &sum)| *out += f64::from(sum));
962        }
963        Ok(())
964    }
965
966    /// Rows one histogram launch may cover: a quantized bin's global i32 sum is at most `rows * max_q` in magnitude.
967    fn rows_per_launch(&self, state: &Loaded<D::Buffer>, quantized: bool) -> Result<u32> {
968        if !quantized {
969            return Ok(u32::MAX);
970        }
971        let spec = state
972            .quant
973            .ok_or_else(|| SilvicError::device("quantized histogram before quantize"))?;
974        Ok(i32::MAX as u32 / spec.grad_max_q.max(spec.hess_max_q).max(1) as u32)
975    }
976
977    /// The histogram of `leaf` into `out`, in as many launches as `rows_per_launch` needs.
978    fn leaf_histogram(
979        &self,
980        state: &Loaded<D::Buffer>,
981        leaf: LeafRange,
982        quantized: bool,
983        out: &mut [f64],
984    ) -> Result<()> {
985        let rows_per_launch = self.rows_per_launch(state, quantized)?;
986        out.fill(0.0);
987        let end = leaf.begin + leaf.count;
988        let mut begin = leaf.begin;
989        while begin < end {
990            let count = (end - begin).min(rows_per_launch);
991            self.encode_histogram(state, LeafRange { begin, count }, HIST_SIDE_LEAF, quantized)?;
992            self.read_histogram(state, quantized, out)?;
993            begin += count;
994        }
995        Ok(())
996    }
997
998    /// Encodes the stable partition of `leaf` (non-empty) by `split`; the left count lands in `scan_total`.
999    fn encode_partition(&self, state: &Loaded<D::Buffer>, leaf: LeafRange, split: &BinSplit) -> Result<()> {
1000        let p = self.partition_params(state, leaf, split)?;
1001        let blocks = leaf.count.div_ceil(p.chunk);
1002        let bins = &state.datasets[0].bins;
1003        launch(
1004            &self.device,
1005            Kernel::PartitionCount,
1006            [blocks, 1],
1007            SCAN_THREADS,
1008            &[
1009                Arg::Buffer(&state.partition),
1010                Arg::Buffer(bins),
1011                Arg::Buffer(&state.block_counts),
1012                Arg::Buffer(&state.split_flags),
1013                params(&p),
1014            ],
1015        )?;
1016        self.scan_block_counts(state, blocks)?;
1017        launch(
1018            &self.device,
1019            Kernel::PartitionScatter,
1020            [blocks, 1],
1021            SCAN_THREADS,
1022            &[
1023                Arg::Buffer(&state.partition),
1024                Arg::Buffer(&state.split_flags),
1025                Arg::Buffer(&state.block_counts),
1026                Arg::Buffer(&state.scan_total),
1027                Arg::Buffer(&state.scratch_rows),
1028                params(&p),
1029            ],
1030        )?;
1031        let copy = CopyParams {
1032            begin: leaf.begin,
1033            count: leaf.count,
1034            pad: [0; 2],
1035        };
1036        launch(
1037            &self.device,
1038            Kernel::CopyU32,
1039            [groups_for(leaf.count, ELEMENT_THREADS), 1],
1040            ELEMENT_THREADS,
1041            &[
1042                Arg::Buffer(&state.scratch_rows),
1043                Arg::Buffer(&state.partition),
1044                params(&copy),
1045            ],
1046        )
1047    }
1048
1049    fn partition_params(
1050        &self,
1051        state: &Loaded<D::Buffer>,
1052        leaf: LeafRange,
1053        split: &BinSplit,
1054    ) -> Result<PartitionParams> {
1055        let layout = state.datasets[0].layouts.get(split.group as usize).ok_or_else(|| {
1056            SilvicError::device(format!(
1057                "split on group {} of {}",
1058                split.group,
1059                state.datasets[0].layouts.len()
1060            ))
1061        })?;
1062        if split.column >= layout.row_stride {
1063            return Err(SilvicError::device(format!(
1064                "split on column {} of a {}-byte row",
1065                split.column, layout.row_stride
1066            )));
1067        }
1068        Ok(PartitionParams {
1069            begin: leaf.begin,
1070            count: leaf.count,
1071            chunk: compact_chunk(leaf.count),
1072            row_stride: layout.row_stride,
1073            group_base_lo: layout.base_lo,
1074            group_base_hi: layout.base_hi,
1075            column: split.column,
1076            pad: 0,
1077            left_bins: split.left_bins,
1078        })
1079    }
1080
1081    fn tree_nodes(&self, state: &Loaded<D::Buffer>, tree: &DeviceTree) -> Result<Vec<TreeNode>> {
1082        let layouts = &state.datasets[0].layouts;
1083        let child_ok = |node: usize, child: i32| {
1084            if child >= 0 {
1085                (child as usize) > node && (child as usize) < tree.nodes.len()
1086            } else {
1087                ((!child) as usize) < tree.leaf_values.len()
1088            }
1089        };
1090        tree.nodes
1091            .iter()
1092            .enumerate()
1093            .map(|(index, node)| {
1094                let split = &node.split;
1095                let layout_ok = layouts
1096                    .get(split.group as usize)
1097                    .is_some_and(|l| split.column < l.row_stride);
1098                if !layout_ok || !child_ok(index, node.left) || !child_ok(index, node.right) {
1099                    return Err(SilvicError::device(format!("malformed device tree node {index}")));
1100                }
1101                Ok(TreeNode {
1102                    group: split.group,
1103                    column: split.column,
1104                    left: node.left,
1105                    right: node.right,
1106                    left_bins: split.left_bins,
1107                })
1108            })
1109            .collect()
1110    }
1111}
1112
1113/// `ceil(fraction * 2^24)`: a draw `k / 2^24` is below `fraction` exactly when `k` is below this.
1114fn bag_threshold(fraction: f64) -> u32 {
1115    const DRAWS: f64 = 16_777_216.0;
1116    if fraction.is_nan() || fraction <= 0.0 {
1117        0
1118    } else if fraction >= 1.0 {
1119        DRAWS as u32
1120    } else {
1121        (fraction * DRAWS).ceil() as u32
1122    }
1123}
1124
1125impl<D: GpuDevice> Backend for GpuBackend<D> {
1126    fn name(&self) -> String {
1127        self.device.name()
1128    }
1129
1130    fn load(&mut self, train: &BinnedDataset, valid: &[&BinnedDataset], objective: &ObjectiveSetup) -> Result<()> {
1131        self.compile()?;
1132        self.state = None;
1133        let device = &self.device;
1134        let datasets = std::iter::once(train)
1135            .chain(valid.iter().copied())
1136            .map(|data| upload_dataset(device, data))
1137            .collect::<Result<Vec<_>>>()?;
1138        if datasets.iter().any(|d| d.layouts.len() != datasets[0].layouts.len()) {
1139            return Err(SilvicError::data(
1140                "validation sets must share the training set's feature groups",
1141            ));
1142        }
1143        self.state = Some(load_train(device, train, datasets, objective)?);
1144        Ok(())
1145    }
1146
1147    fn init_scores(&mut self, dataset: usize, value: f64) -> Result<()> {
1148        if dataset == 0 {
1149            // A deferred tree update would land on the refilled scores.
1150            if let Some(state) = self.state.as_mut() {
1151                state.deferred_tree = None;
1152            }
1153        }
1154        let state = self.loaded()?;
1155        let data = state.dataset(dataset)?;
1156        self.fill(&data.scores, data.num_rows, (value as f32).to_bits())
1157    }
1158
1159    fn compute_gradients(&mut self) -> Result<GradientStats> {
1160        self.encode_deferred_tree()?;
1161        let state = self.loaded()?;
1162        match state.objective {
1163            ObjectiveSetup::Regression => self.pointwise_gradients(Kernel::GradRegression, 1.0)?,
1164            ObjectiveSetup::Binary { sigmoid } => self.pointwise_gradients(Kernel::GradBinary, sigmoid as f32)?,
1165            ObjectiveSetup::LambdaRank { .. } => self.lambdarank_gradients()?,
1166        }
1167        self.gradient_stats()
1168    }
1169
1170    fn quantize(&mut self, spec: &QuantSpec) -> Result<()> {
1171        let state = self.loaded()?;
1172        let (row_offset_lo, row_offset_hi) = split_u64(state.row_offset);
1173        let p = QuantParams {
1174            n: state.num_rows,
1175            row_offset_lo,
1176            row_offset_hi,
1177            inv_grad_scale: spec.inv_grad_scale,
1178            inv_hess_scale: spec.inv_hess_scale,
1179            grad_max_q: spec.grad_max_q,
1180            hess_max_q: spec.hess_max_q,
1181            constant_hessian: u32::from(spec.constant_hessian),
1182            stochastic: u32::from(spec.stochastic),
1183            seed: spec.seed,
1184            iteration: spec.iteration,
1185            pad: 0,
1186        };
1187        launch(
1188            &self.device,
1189            Kernel::Quantize,
1190            [groups_for(state.num_rows, ELEMENT_THREADS), 1],
1191            ELEMENT_THREADS,
1192            &[
1193                Arg::Buffer(&state.grad),
1194                Arg::Buffer(&state.hess),
1195                Arg::Buffer(&state.packed),
1196                params(&p),
1197            ],
1198        )?;
1199        if let Some(state) = self.state.as_mut() {
1200            state.quant = Some(*spec);
1201        }
1202        Ok(())
1203    }
1204
1205    fn reset_partition(&mut self, bag: &BagSpec) -> Result<u32> {
1206        match *bag {
1207            BagSpec::All => {
1208                let state = self.loaded()?;
1209                let p = FillParams {
1210                    n: state.num_rows,
1211                    value: 0,
1212                };
1213                launch(
1214                    &self.device,
1215                    Kernel::IotaU32,
1216                    [groups_for(state.num_rows, ELEMENT_THREADS), 1],
1217                    ELEMENT_THREADS,
1218                    &[Arg::Buffer(&state.partition), params(&p)],
1219                )?;
1220                Ok(state.num_rows)
1221            }
1222            BagSpec::Bernoulli { seed, round, fraction } => self.bernoulli_bag(seed, round, fraction),
1223        }
1224    }
1225
1226    fn histogram(&mut self, leaf: LeafRange, quantized: bool, out: &mut [f64]) -> Result<()> {
1227        let state = self.loaded()?;
1228        state.check_hist_out(out)?;
1229        state.check_leaf(leaf)?;
1230        if leaf.count == 0 || state.total_bins == 0 {
1231            out.fill(0.0);
1232            return Ok(());
1233        }
1234        self.leaf_histogram(state, leaf, quantized, out)
1235    }
1236
1237    fn partition(&mut self, leaf: LeafRange, split: &BinSplit) -> Result<u32> {
1238        let state = self.loaded()?;
1239        state.check_leaf(leaf)?;
1240        if leaf.count == 0 {
1241            return Ok(0);
1242        }
1243        self.encode_partition(state, leaf, split)?;
1244        self.read_scan_total(state)
1245    }
1246
1247    /// Partition and the child's histogram in one command buffer: the histogram kernel takes the child's rows from
1248    /// the device-side left count, so the host waits once.
1249    fn partition_and_histogram(
1250        &mut self,
1251        leaf: LeafRange,
1252        split: &BinSplit,
1253        side: Side,
1254        quantized: bool,
1255        out: &mut [f64],
1256    ) -> Result<u32> {
1257        let state = self.loaded()?;
1258        state.check_hist_out(out)?;
1259        state.check_leaf(leaf)?;
1260        if leaf.count == 0 || state.total_bins == 0 || leaf.count > self.rows_per_launch(state, quantized)? {
1261            let left = self.partition(leaf, split)?;
1262            self.histogram(side.range(leaf, left), quantized, out)?;
1263            return Ok(left);
1264        }
1265        self.encode_partition(state, leaf, split)?;
1266        let side = match side {
1267            Side::Left => HIST_SIDE_LEFT,
1268            Side::Right => HIST_SIDE_RIGHT,
1269        };
1270        self.encode_histogram(state, leaf, side, quantized)?;
1271        out.fill(0.0);
1272        self.read_histogram(state, quantized, out)?;
1273        self.read_scan_total(state)
1274    }
1275
1276    fn leaf_sums(&mut self, leaves: &[LeafRange], grad_scale: f32, hess_scale: f32) -> Result<Vec<[i64; 2]>> {
1277        let blocks: Vec<u32> = leaves.iter().map(|leaf| leaf.count.div_ceil(LEAF_SUM_ROWS)).collect();
1278        let total_blocks: usize = blocks.iter().map(|&b| b as usize).sum();
1279        let mut jobs = Vec::with_capacity(2 * total_blocks);
1280        for leaf in leaves {
1281            self.loaded()?.check_leaf(*leaf)?;
1282            let end = leaf.begin + leaf.count;
1283            for start in (leaf.begin..end).step_by(LEAF_SUM_ROWS as usize) {
1284                jobs.extend([start, (start + LEAF_SUM_ROWS).min(end)]);
1285            }
1286        }
1287        let state = self
1288            .state
1289            .as_mut()
1290            .ok_or_else(|| SilvicError::device("backend used before load"))?;
1291        ensure_capacity(&self.device, &mut state.leaf_partials, total_blocks, 16)?;
1292        ensure_capacity(&self.device, &mut state.leaf_jobs, total_blocks, 8)?;
1293        let state = self.loaded()?;
1294        let partials: Vec<i64> = if total_blocks == 0 {
1295            Vec::new()
1296        } else {
1297            self.device
1298                .upload(&state.leaf_jobs.buffer, 0, bytemuck::cast_slice(&jobs))?;
1299            let p = LeafSumParams {
1300                grad_scale,
1301                hess_scale,
1302                pad: [0; 2],
1303            };
1304            launch(
1305                &self.device,
1306                Kernel::LeafSums,
1307                [total_blocks as u32, 1],
1308                REDUCE_THREADS,
1309                &[
1310                    Arg::Buffer(&state.partition),
1311                    Arg::Buffer(&state.grad),
1312                    Arg::Buffer(&state.hess),
1313                    Arg::Buffer(&state.leaf_jobs.buffer),
1314                    Arg::Buffer(&state.leaf_partials.buffer),
1315                    params(&p),
1316                ],
1317            )?;
1318            download_vec(&self.device, &state.leaf_partials.buffer, 0, 2 * total_blocks)?
1319        };
1320        let mut sums = Vec::with_capacity(leaves.len());
1321        let mut pairs = partials.as_chunks::<2>().0.iter();
1322        for &leaf_blocks in &blocks {
1323            let sum = pairs
1324                .by_ref()
1325                .take(leaf_blocks as usize)
1326                .fold([0i64; 2], |acc, pair| [acc[0] + pair[0], acc[1] + pair[1]]);
1327            sums.push(sum);
1328        }
1329        Ok(sums)
1330    }
1331
1332    fn add_tree(&mut self, tree: &DeviceTree) -> Result<()> {
1333        let state = self.loaded()?;
1334        if tree.leaf_values.len() != tree.nodes.len() + 1 {
1335            return Err(SilvicError::device(format!(
1336                "device tree with {} nodes and {} leaves",
1337                tree.nodes.len(),
1338                tree.leaf_values.len()
1339            )));
1340        }
1341        let nodes = self.tree_nodes(state, tree)?;
1342        // The previous tree's update of the training scores reads the node buffers about to be overwritten.
1343        self.encode_deferred_tree()?;
1344        let state = self
1345            .state
1346            .as_mut()
1347            .ok_or_else(|| SilvicError::device("backend used before load"))?;
1348        ensure_capacity(
1349            &self.device,
1350            &mut state.nodes,
1351            nodes.len(),
1352            std::mem::size_of::<TreeNode>(),
1353        )?;
1354        ensure_capacity(&self.device, &mut state.leaf_values, tree.leaf_values.len(), 4)?;
1355        let state = self.loaded()?;
1356        if !nodes.is_empty() {
1357            self.device
1358                .upload(&state.nodes.buffer, 0, bytemuck::cast_slice(&nodes))?;
1359        }
1360        self.device
1361            .upload(&state.leaf_values.buffer, 0, bytemuck::cast_slice(&tree.leaf_values))?;
1362        let num_nodes = nodes.len() as u32;
1363        for dataset in 1..state.datasets.len() {
1364            self.encode_add_tree(state, dataset, num_nodes)?;
1365        }
1366        if state.datasets.len() == 1 {
1367            return self.encode_add_tree(state, 0, num_nodes);
1368        }
1369        // The training set's update waits until the validation scores are read (or the training scores are next
1370        // used), so reading validation scores does not wait for the much larger training set.
1371        if let Some(state) = self.state.as_mut() {
1372            state.deferred_tree = Some(num_nodes);
1373        }
1374        Ok(())
1375    }
1376
1377    fn scores(&mut self, dataset: usize) -> Result<Vec<f32>> {
1378        if dataset == 0 {
1379            self.encode_deferred_tree()?;
1380        }
1381        let state = self.loaded()?;
1382        let data = state.dataset(dataset)?;
1383        let scores = download_vec(&self.device, &data.scores, 0, data.num_rows as usize)?;
1384        // Start the training set's update now: it runs while the host evaluates these scores.
1385        self.encode_deferred_tree()?;
1386        self.device.flush()?;
1387        Ok(scores)
1388    }
1389}