1use 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#[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
83const MAX_GROUPS: u32 = 65_535;
85const ELEMENT_THREADS: u32 = 256;
87const STATS_GROUPS: u32 = 1024;
89const COMPACT_CHUNK: u32 = 4096;
92const MAX_COMPACT_BLOCKS: u32 = 65_536;
94const HIST_THREADS: u32 = 1024;
97const HIST_TARGET_GROUPS: u32 = 1024;
100const HIST_MIN_ROWS_PER_BLOCK: u32 = 1024;
102const HIST_FLOAT_MAX_ROWS_PER_BLOCK: u32 = 8192;
104const LEAF_SUM_ROWS: u32 = REDUCE_THREADS * 16;
106const MFB_CHUNK_ROWS: usize = 65_536;
108
109struct DatasetBuffers<B> {
111 num_rows: u32,
112 bins: B,
114 groups: B,
116 layouts: Vec<GroupLayout>,
117 scores: B,
118}
119
120struct HistUnits<B> {
122 units: B,
123 col_map: B,
124 count: u32,
125}
126
127struct RankSetup<B> {
129 query_bounds: B,
130 queries: B,
131 classes: [(u32, u32); 3],
133 inverse_max_dcg: B,
134 label_gain: B,
135 discount: B,
136 sigmoid_table: B,
138 params: RankParams,
139 host_queries: Vec<usize>,
141 host_bounds: Vec<u32>,
142 host_label: Vec<f32>,
143 host_weight: Option<Vec<f32>>,
144}
145
146struct 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 dummy: B,
162 grad: B,
163 hess: B,
164 packed: B,
165 partition: B,
166 scratch_rows: B,
167 split_flags: B,
169 block_counts: B,
170 scan_total: B,
171 stats_partials: B,
172 hist: B,
174 quant_units: HistUnits<B>,
175 float_units: HistUnits<B>,
176 leaf_partials: Scratch<B>,
177 leaf_jobs: Scratch<B>,
179 nodes: Scratch<B>,
180 leaf_values: Scratch<B>,
181 rank: Option<RankSetup<B>>,
182 quant: Option<QuantSpec>,
183 deferred_tree: Option<u32>,
185}
186
187pub struct GpuBackend<D: GpuDevice> {
189 state: Option<Loaded<D::Buffer>>,
190 compiled: bool,
191 device: D,
192}
193
194impl<D: GpuDevice> GpuBackend<D> {
195 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 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 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
244fn groups_for(n: u32, threads: u32) -> u32 {
246 n.div_ceil(threads).clamp(1, MAX_GROUPS)
247}
248
249fn 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
285fn 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
336fn 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 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
376fn 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
441fn 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
461fn 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
548fn 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
609fn 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
616fn 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
622fn 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 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 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 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 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 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 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 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 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 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 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(©),
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
1113fn 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 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 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 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 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 self.encode_deferred_tree()?;
1386 self.device.flush()?;
1387 Ok(scores)
1388 }
1389}