#![allow(missing_docs)]
#![cfg(all(feature = "single-cell", feature = "gpu"))]
use ann_search_rs::gpu::grid_2d;
use ann_search_rs::gpu::tensor::GpuTensor;
use cubecl::prelude::*;
use faer::Mat;
use rand::{Rng, SeedableRng, rngs::SmallRng};
use rayon::prelude::*;
use rustc_hash::FxHashMap;
use std::time::Instant;
use thousands::Separable;
use crate::gpu::{WORKGROUP_32, WORKGROUP_128};
use crate::prelude::*;
use crate::single_cell::mc_analysis::scenic_metacells::{
batch_genes_in_memory, build_tf_quantised_store, extract_target_column,
};
use crate::single_cell::sc_analysis::scenic::*;
use crate::single_cell::sc_utils::utils_tree::*;
pub struct ScenicGpuParams {
pub wave_byte_budget: usize,
}
impl Default for ScenicGpuParams {
fn default() -> Self {
Self {
wave_byte_budget: 4 * 1024 * 1024 * 1024,
}
}
}
const N_BINS: u32 = 256;
const DEFAULT_WAVE_SIZE: usize = 32;
const MAX_PLANES_PER_CUBE: u32 = 32;
const SMEM_HIST_SLOTS: u32 = 2176;
const RF_UNROLL: u32 = 16;
const INVALID_NODE: u32 = u32::MAX;
const _: () = assert!(MULTI_OUTPUT_BATCH <= WORKGROUP_128 as usize);
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments)]
pub fn sample_node_features(
tree_seeds: &Tensor<u32>,
node_features: &mut Tensor<u32>,
wave_size: u32,
n_active_nodes: u32,
k_feats: u32,
n_features: u32,
level: u32,
#[comptime] wg_size: u32,
) {
let node = CUBE_POS_X + CUBE_POS_Y * CUBE_COUNT_X;
let tree = CUBE_POS_Z;
if node >= n_active_nodes {
terminate!();
}
if tree >= wave_size {
terminate!();
}
let tx = UNIT_POS_X;
let seed = tree_seeds[tree as usize];
let base = ((tree * n_active_nodes + node) * k_feats) as usize;
let mut slot: u32 = tx;
while slot < k_feats {
let mut h = seed;
h = hash_mix(h ^ level);
h = hash_mix(h ^ node);
h = hash_mix(h ^ slot);
node_features[base + slot as usize] = h % n_features;
slot += wg_size;
}
}
#[cube]
#[allow(clippy::assign_op_pattern)]
fn atomic_add_f32_bits(ptr: &Atomic<u32>, delta: f32) {
let mut old_bits: u32 = Atomic::load(ptr);
let mut done: u32 = 0u32;
while done == 0u32 {
let old_f = f32::from_bits(old_bits);
let new_bits = (old_f + delta).to_bits();
let observed = Atomic::compare_exchange_weak(ptr, old_bits, new_bits);
if observed == old_bits {
done = 1u32;
} else {
old_bits = observed;
}
}
}
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments)]
pub fn build_hist_privatised(
feature_data: &Tensor<u32>,
sy_offsets: &Tensor<u32>,
sy_target_indices: &Tensor<u32>,
sy_values: &Tensor<f32>,
node_sample_offsets: &Tensor<u32>,
node_sample_ids: &Tensor<u32>,
node_sample_mults: &Tensor<u32>,
node_features: &Tensor<u32>,
hist_counts: &mut Tensor<Atomic<u32>>,
hist_y_sums: &mut Tensor<Atomic<u32>>,
hist_y_sum_sqs: &mut Tensor<Atomic<u32>>,
n_samples: u32,
wave_size: u32,
n_active_nodes: u32,
k_feats: u32,
n_targets: u32,
#[comptime] wg_size: u32,
) {
let slot = CUBE_POS_X;
let node = CUBE_POS_Y;
let tree = CUBE_POS_Z;
if slot >= k_feats {
terminate!();
}
if node >= n_active_nodes {
terminate!();
}
if tree >= wave_size {
terminate!();
}
let tx = UNIT_POS_X;
let feat = node_features[((tree * n_active_nodes + node) * k_feats + slot) as usize];
let count_base = (((tree * n_active_nodes + node) * k_feats + slot) * N_BINS) as usize;
let sum_base =
((((tree * n_active_nodes + node) * k_feats + slot) * N_BINS) * n_targets) as usize;
let mut zb: u32 = tx;
while zb < N_BINS {
Atomic::store(&hist_counts[count_base + zb as usize], 0u32);
zb += wg_size;
}
let sum_slots = N_BINS * n_targets;
let mut zs: u32 = tx;
while zs < sum_slots {
Atomic::store(&hist_y_sums[sum_base + zs as usize], 0u32);
Atomic::store(&hist_y_sum_sqs[sum_base + zs as usize], 0u32);
zs += wg_size;
}
sync_cube();
let obase = (tree * (n_active_nodes + 1u32)) as usize;
let lo = node_sample_offsets[obase + node as usize];
let hi = node_sample_offsets[obase + (node + 1u32) as usize];
let gbase = (tree * n_samples) as usize;
let mut j: u32 = lo + tx;
while j < hi {
let s = node_sample_ids[gbase + j as usize];
let mult = node_sample_mults[gbase + j as usize];
let bin = feature_bin(feature_data, feat * n_samples + s);
Atomic::fetch_add(&hist_counts[count_base + bin as usize], mult);
let mult_f = f32::cast_from(mult);
let bin_base = sum_base + (bin * n_targets) as usize;
let off_s = sy_offsets[s as usize];
let off_e = sy_offsets[(s + 1u32) as usize];
let mut c = off_s;
while c < off_e {
let k = sy_target_indices[c as usize];
let v = sy_values[c as usize];
let mv = mult_f * v;
atomic_add_f32_bits(&hist_y_sums[bin_base + k as usize], mv);
atomic_add_f32_bits(&hist_y_sum_sqs[bin_base + k as usize], mv * v);
c += 1u32;
}
j += wg_size;
}
}
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments)]
pub fn merge_hist(
hist_counts: &Tensor<u32>,
hist_y_sums: &Tensor<u32>,
hist_y_sum_sqs: &Tensor<u32>,
node_counts: &mut Tensor<u32>,
node_y_sums: &mut Tensor<f32>,
node_y_sum_sqs: &mut Tensor<f32>,
wave_size: u32,
n_active_nodes: u32,
k_feats: u32,
n_targets: u32,
#[comptime] wg_size: u32,
) {
let node = CUBE_POS_X + CUBE_POS_Y * CUBE_COUNT_X;
let tree = CUBE_POS_Z;
if node >= n_active_nodes {
terminate!();
}
if tree >= wave_size {
terminate!();
}
let tx = UNIT_POS_X;
let count_base = (((tree * n_active_nodes + node) * k_feats) * N_BINS) as usize;
let sum_base = ((((tree * n_active_nodes + node) * k_feats) * N_BINS) * n_targets) as usize;
let stats_out = ((tree * n_active_nodes + node) * n_targets) as usize;
if tx == 0u32 {
let mut total: u32 = 0u32;
let mut b: u32 = 0u32;
while b < N_BINS {
total += hist_counts[count_base + b as usize];
b += 1u32;
}
node_counts[(tree * n_active_nodes + node) as usize] = total;
}
let mut k: u32 = tx;
while k < n_targets {
let mut sum: f32 = 0f32;
let mut ssq: f32 = 0f32;
let mut b: u32 = 0u32;
while b < N_BINS {
let bin_base = sum_base + (b * n_targets) as usize;
sum += f32::from_bits(hist_y_sums[bin_base + k as usize]);
ssq += f32::from_bits(hist_y_sum_sqs[bin_base + k as usize]);
b += 1u32;
}
node_y_sums[stats_out + k as usize] = sum;
node_y_sum_sqs[stats_out + k as usize] = ssq;
k += wg_size;
}
}
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments)]
pub fn prefix_sum_bins(
hist_counts: &Tensor<u32>,
hist_y_sums: &Tensor<u32>,
hist_y_sum_sqs: &Tensor<u32>,
cum_counts: &mut Tensor<u32>,
cum_y_sums: &mut Tensor<f32>,
cum_y_sum_sqs: &mut Tensor<f32>,
slot_min_bin: &mut Tensor<u32>,
slot_max_bin: &mut Tensor<u32>,
wave_size: u32,
n_active_nodes: u32,
k_feats: u32,
n_targets: u32,
#[comptime] wg_size: u32,
) {
let slot = CUBE_POS_X;
let node = CUBE_POS_Y;
let tree = CUBE_POS_Z;
if slot >= k_feats {
terminate!();
}
if node >= n_active_nodes {
terminate!();
}
if tree >= wave_size {
terminate!();
}
let tx = UNIT_POS_X;
let slot_flat = ((tree * n_active_nodes + node) * k_feats + slot) as usize;
let count_base = slot_flat * N_BINS as usize;
let sum_base =
((((tree * n_active_nodes + node) * k_feats + slot) * N_BINS) * n_targets) as usize;
if tx == 0u32 {
let mut min_b: u32 = 0u32;
let mut has_min: u32 = 0u32;
let mut max_b: u32 = 0u32;
let first = hist_counts[count_base];
cum_counts[count_base] = first;
if first > 0u32 {
min_b = 0u32;
has_min = 1u32;
max_b = 0u32;
}
let mut run: u32 = first;
let mut b: u32 = 1u32;
while b < N_BINS {
let curr = count_base + b as usize;
let hc = hist_counts[curr];
run += hc;
cum_counts[curr] = run;
if hc > 0u32 {
if has_min == 0u32 {
min_b = b;
has_min = 1u32;
}
max_b = b;
}
b += 1u32;
}
if has_min == 1u32 {
slot_min_bin[slot_flat] = min_b;
slot_max_bin[slot_flat] = max_b;
} else {
slot_min_bin[slot_flat] = 0u32;
slot_max_bin[slot_flat] = 0u32;
}
}
let mut k: u32 = tx;
while k < n_targets {
let mut run_s = f32::from_bits(hist_y_sums[sum_base + k as usize]);
let mut run_q = f32::from_bits(hist_y_sum_sqs[sum_base + k as usize]);
cum_y_sums[sum_base + k as usize] = run_s;
cum_y_sum_sqs[sum_base + k as usize] = run_q;
let mut b: u32 = 1u32;
while b < N_BINS {
let curr_s = sum_base + (b * n_targets) as usize + k as usize;
run_s += f32::from_bits(hist_y_sums[curr_s]);
run_q += f32::from_bits(hist_y_sum_sqs[curr_s]);
cum_y_sums[curr_s] = run_s;
cum_y_sum_sqs[curr_s] = run_q;
b += 1u32;
}
k += wg_size;
}
}
#[cube]
fn feature_bin(feature_data: &Tensor<u32>, idx: u32) -> u32 {
let word = feature_data[(idx / 4u32) as usize];
(word >> ((idx % 4u32) * 8u32)) & 255u32
}
#[cube]
#[allow(clippy::assign_op_pattern)]
fn hash_mix(x: u32) -> u32 {
let mut h = x;
h ^= h >> 16u32;
h = h * 2246822507u32;
h ^= h >> 13u32;
h = h * 3266489909u32;
h ^= h >> 16u32;
h
}
#[cube]
fn argmax_takes_mate(cur_valid: u32, cur_score: f32, mate_valid: u32, mate_score: f32) -> u32 {
let mut take: u32 = 0u32;
if mate_valid == 1u32 {
if cur_valid == 0u32 {
take = 1u32;
} else {
if mate_score > cur_score {
take = 1u32;
}
}
}
take
}
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments, clippy::collapsible_if)]
pub fn evaluate_splits_rf(
cum_counts: &Tensor<u32>,
cum_y_sums: &Tensor<f32>,
cum_y_sum_sqs: &Tensor<f32>,
node_counts: &Tensor<u32>,
node_y_sums: &Tensor<f32>,
node_y_sum_sqs: &Tensor<f32>,
node_features: &Tensor<u32>,
slot_min_bin: &Tensor<u32>,
slot_max_bin: &Tensor<u32>,
split_feature: &mut Tensor<u32>,
split_threshold: &mut Tensor<u32>,
split_n_left: &mut Tensor<u32>,
wave_size: u32,
n_active_nodes: u32,
k_feats: u32,
n_targets: u32,
min_samples_leaf: u32,
#[comptime] wg_size: u32,
) {
let node = CUBE_POS_X + CUBE_POS_Y * CUBE_COUNT_X;
let tree = CUBE_POS_Z;
if node >= n_active_nodes {
terminate!();
}
if tree >= wave_size {
terminate!();
}
let tx = UNIT_POS_X;
let node_flat = (tree * n_active_nodes + node) as usize;
let n = node_counts[node_flat];
let stats_base = node_flat * n_targets as usize;
let nf = f32::cast_from(n);
let mut s_parent_var = SharedMemory::<f32>::new(1usize);
if tx == 0u32 {
let mut pv: f32 = 0f32;
if n >= 2u32 {
let mut k: u32 = 0u32;
while k < n_targets {
let s = node_y_sums[stats_base + k as usize];
let ss = node_y_sum_sqs[stats_base + k as usize];
let mean = s / nf;
let v = ss / nf - mean * mean;
pv += f32::max(v, 0f32);
k += 1u32;
}
}
s_parent_var[0] = pv;
}
sync_cube();
let parent_var_sum = s_parent_var[0];
if n < 2u32 * min_samples_leaf {
if tx == 0u32 {
split_feature[node_flat] = INVALID_NODE;
split_threshold[node_flat] = 0u32;
split_n_left[node_flat] = 0u32;
}
terminate!();
}
if parent_var_sum <= 0f32 {
if tx == 0u32 {
split_feature[node_flat] = INVALID_NODE;
split_threshold[node_flat] = 0u32;
split_n_left[node_flat] = 0u32;
}
terminate!();
}
let thresholds_per_slot = N_BINS - 1u32;
let n_candidates = k_feats * thresholds_per_slot;
let mut best_score: f32 = 0f32;
let mut best_slot: u32 = 0u32;
let mut best_thr: u32 = 0u32;
let mut best_n_left: u32 = 0u32;
let mut best_valid: u32 = 0u32;
let mut c: u32 = tx;
while c < n_candidates {
let slot = c / thresholds_per_slot;
let thr = c % thresholds_per_slot;
let slot_flat = ((tree * n_active_nodes + node) * k_feats + slot) as usize;
let min_bin = slot_min_bin[slot_flat];
let max_bin = slot_max_bin[slot_flat];
let in_range = max_bin > min_bin && thr >= min_bin && thr < max_bin;
if in_range {
let count_base = (((tree * n_active_nodes + node) * k_feats + slot) * N_BINS) as usize;
let sum_base =
((((tree * n_active_nodes + node) * k_feats + slot) * N_BINS) * n_targets) as usize;
let n_left = cum_counts[count_base + thr as usize];
let n_right = n - n_left;
let ok_left = n_left >= min_samples_leaf;
let ok_right = n_right >= min_samples_leaf;
if ok_left && ok_right {
let nl = f32::cast_from(n_left);
let nr = f32::cast_from(n_right);
let inv_nl = 1f32 / nl;
let inv_nr = 1f32 / nr;
let wl = nl / nf;
let wr = nr / nf;
let mut score: f32 = 0f32;
let bin_base = sum_base + (thr * n_targets) as usize;
let mut k: u32 = 0u32;
while k < n_targets {
let sy = node_y_sums[stats_base + k as usize];
let ssq = node_y_sum_sqs[stats_base + k as usize];
let mean_p = sy / nf;
let var_p = ssq / nf - mean_p * mean_p;
let syl = cum_y_sums[bin_base + k as usize];
let ssyl = cum_y_sum_sqs[bin_base + k as usize];
let mean_l = syl * inv_nl;
let var_l = ssyl * inv_nl - mean_l * mean_l;
let syr = sy - syl;
let ssyr = ssq - ssyl;
let mean_r = syr * inv_nr;
let var_r = ssyr * inv_nr - mean_r * mean_r;
let vp = f32::max(var_p, 0f32);
let vl = f32::max(var_l, 0f32);
let vr = f32::max(var_r, 0f32);
score += vp - wl * vl - wr * vr;
k += 1u32;
}
if score > best_score {
best_score = score;
best_slot = slot;
best_thr = thr;
best_n_left = n_left;
best_valid = 1u32;
}
}
}
c += wg_size;
}
let mut s_score = SharedMemory::<f32>::new(WORKGROUP_128 as usize);
let mut s_slot = SharedMemory::<u32>::new(WORKGROUP_128 as usize);
let mut s_thr = SharedMemory::<u32>::new(WORKGROUP_128 as usize);
let mut s_nl = SharedMemory::<u32>::new(WORKGROUP_128 as usize);
let mut s_valid = SharedMemory::<u32>::new(WORKGROUP_128 as usize);
s_score[tx as usize] = best_score;
s_slot[tx as usize] = best_slot;
s_thr[tx as usize] = best_thr;
s_nl[tx as usize] = best_n_left;
s_valid[tx as usize] = best_valid;
sync_cube();
if tx < 64u32 {
let mate = tx + 64u32;
let take = argmax_takes_mate(
s_valid[tx as usize],
s_score[tx as usize],
s_valid[mate as usize],
s_score[mate as usize],
);
if take == 1u32 {
s_score[tx as usize] = s_score[mate as usize];
s_slot[tx as usize] = s_slot[mate as usize];
s_thr[tx as usize] = s_thr[mate as usize];
s_nl[tx as usize] = s_nl[mate as usize];
s_valid[tx as usize] = s_valid[mate as usize];
}
}
sync_cube();
if tx < 32u32 {
let mate = tx + 32u32;
let take = argmax_takes_mate(
s_valid[tx as usize],
s_score[tx as usize],
s_valid[mate as usize],
s_score[mate as usize],
);
if take == 1u32 {
s_score[tx as usize] = s_score[mate as usize];
s_slot[tx as usize] = s_slot[mate as usize];
s_thr[tx as usize] = s_thr[mate as usize];
s_nl[tx as usize] = s_nl[mate as usize];
s_valid[tx as usize] = s_valid[mate as usize];
}
}
sync_cube();
if tx < 16u32 {
let mate = tx + 16u32;
let take = argmax_takes_mate(
s_valid[tx as usize],
s_score[tx as usize],
s_valid[mate as usize],
s_score[mate as usize],
);
if take == 1u32 {
s_score[tx as usize] = s_score[mate as usize];
s_slot[tx as usize] = s_slot[mate as usize];
s_thr[tx as usize] = s_thr[mate as usize];
s_nl[tx as usize] = s_nl[mate as usize];
s_valid[tx as usize] = s_valid[mate as usize];
}
}
sync_cube();
if tx < 8u32 {
let mate = tx + 8u32;
let take = argmax_takes_mate(
s_valid[tx as usize],
s_score[tx as usize],
s_valid[mate as usize],
s_score[mate as usize],
);
if take == 1u32 {
s_score[tx as usize] = s_score[mate as usize];
s_slot[tx as usize] = s_slot[mate as usize];
s_thr[tx as usize] = s_thr[mate as usize];
s_nl[tx as usize] = s_nl[mate as usize];
s_valid[tx as usize] = s_valid[mate as usize];
}
}
sync_cube();
if tx < 4u32 {
let mate = tx + 4u32;
let take = argmax_takes_mate(
s_valid[tx as usize],
s_score[tx as usize],
s_valid[mate as usize],
s_score[mate as usize],
);
if take == 1u32 {
s_score[tx as usize] = s_score[mate as usize];
s_slot[tx as usize] = s_slot[mate as usize];
s_thr[tx as usize] = s_thr[mate as usize];
s_nl[tx as usize] = s_nl[mate as usize];
s_valid[tx as usize] = s_valid[mate as usize];
}
}
sync_cube();
if tx < 2u32 {
let mate = tx + 2u32;
let take = argmax_takes_mate(
s_valid[tx as usize],
s_score[tx as usize],
s_valid[mate as usize],
s_score[mate as usize],
);
if take == 1u32 {
s_score[tx as usize] = s_score[mate as usize];
s_slot[tx as usize] = s_slot[mate as usize];
s_thr[tx as usize] = s_thr[mate as usize];
s_nl[tx as usize] = s_nl[mate as usize];
s_valid[tx as usize] = s_valid[mate as usize];
}
}
sync_cube();
if tx < 1u32 {
let mate = tx + 1u32;
let take = argmax_takes_mate(
s_valid[tx as usize],
s_score[tx as usize],
s_valid[mate as usize],
s_score[mate as usize],
);
if take == 1u32 {
s_score[tx as usize] = s_score[mate as usize];
s_slot[tx as usize] = s_slot[mate as usize];
s_thr[tx as usize] = s_thr[mate as usize];
s_nl[tx as usize] = s_nl[mate as usize];
s_valid[tx as usize] = s_valid[mate as usize];
}
}
sync_cube();
let winner_valid = s_valid[0];
let winner_slot = s_slot[0];
let winner_thr = s_thr[0];
let winner_n_left = s_nl[0];
if tx == 0u32 {
if winner_valid == 1u32 {
let feat =
node_features[((tree * n_active_nodes + node) * k_feats + winner_slot) as usize];
split_feature[node_flat] = feat;
split_threshold[node_flat] = winner_thr;
split_n_left[node_flat] = winner_n_left;
} else {
split_feature[node_flat] = INVALID_NODE;
split_threshold[node_flat] = 0u32;
split_n_left[node_flat] = 0u32;
}
}
}
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments)]
pub fn finalise_split_stats_rf(
feature_data: &Tensor<u32>,
y_dense: &Tensor<f32>,
node_sample_offsets: &Tensor<u32>,
node_sample_ids: &Tensor<u32>,
node_sample_mults: &Tensor<u32>,
split_feature: &Tensor<u32>,
split_threshold: &Tensor<u32>,
split_y_sums_l: &mut Tensor<f32>,
split_y_sum_sqs_l: &mut Tensor<f32>,
n_samples: u32,
wave_size: u32,
n_active_nodes: u32,
n_targets: u32,
) {
let node = CUBE_POS_X + CUBE_POS_Y * CUBE_COUNT_X;
let tree = CUBE_POS_Z;
if node >= n_active_nodes {
terminate!();
}
if tree >= wave_size {
terminate!();
}
let mut s_tile_id = SharedMemory::<u32>::new(WORKGROUP_128 as usize);
let mut s_tile_mult = SharedMemory::<u32>::new(WORKGROUP_128 as usize);
let mut ys = Array::<f32>::new(RF_UNROLL as usize);
let mut ms = Array::<u32>::new(RF_UNROLL as usize);
let tx = UNIT_POS_X;
let node_flat = (tree * n_active_nodes + node) as usize;
let feat = split_feature[node_flat];
if feat == INVALID_NODE {
terminate!();
}
let thr = split_threshold[node_flat];
let obase = (tree * (n_active_nodes + 1u32)) as usize;
let lo = node_sample_offsets[obase + node as usize];
let hi = node_sample_offsets[obase + (node + 1u32) as usize];
let gbase = (tree * n_samples) as usize;
let mut acc_s: f32 = 0f32;
let mut acc_q: f32 = 0f32;
let mut base: u32 = lo;
while base < hi {
let idx = base + tx;
if idx < hi {
let s = node_sample_ids[gbase + idx as usize];
s_tile_id[tx as usize] = s;
if feature_bin(feature_data, feat * n_samples + s) <= thr {
s_tile_mult[tx as usize] = node_sample_mults[gbase + idx as usize];
} else {
s_tile_mult[tx as usize] = 0u32;
}
}
sync_cube();
let mut cnt = hi - base;
if cnt > CUBE_DIM_X {
cnt = CUBE_DIM_X;
}
if tx < n_targets {
let mut i: u32 = 0u32;
while i + (RF_UNROLL - 1u32) < cnt {
#[unroll]
for u in 0..RF_UNROLL {
ms[u as usize] = s_tile_mult[(i + u) as usize];
ys[u as usize] =
y_dense[(s_tile_id[(i + u) as usize] * n_targets + tx) as usize];
}
#[unroll]
for u in 0..RF_UNROLL {
let mf = f32::cast_from(ms[u as usize]);
acc_s += mf * ys[u as usize];
acc_q += mf * ys[u as usize] * ys[u as usize];
}
i += RF_UNROLL;
}
while i < cnt {
let mf = f32::cast_from(s_tile_mult[i as usize]);
let y = y_dense[(s_tile_id[i as usize] * n_targets + tx) as usize];
acc_s += mf * y;
acc_q += mf * y * y;
i += 1u32;
}
}
sync_cube();
base += CUBE_DIM_X;
}
if tx < n_targets {
let out = node_flat * n_targets as usize + tx as usize;
split_y_sums_l[out] = acc_s;
split_y_sum_sqs_l[out] = acc_q;
}
}
pub fn pick_gpu_bins(n_targets: usize) -> u32 {
let mut bins = N_BINS;
while bins > 1 && (bins as usize) * (n_targets + 1) > SMEM_HIST_SLOTS as usize {
bins /= 2;
}
bins
}
pub fn fused_rf_smem_bytes() -> usize {
(SMEM_HIST_SLOTS as usize * 4)
+ (N_BINS as usize * 4)
+ (WORKGROUP_128 as usize * 7 * 4)
+ (2 * 4)
}
fn fused_rf_viable<R: Runtime>(client: &ComputeClient<R>) -> bool {
client.properties().hardware.max_shared_memory_size >= fused_rf_smem_bytes()
}
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments, clippy::collapsible_if)]
pub fn build_score_rf_fused(
feature_data: &Tensor<u32>,
y_dense: &Tensor<f32>,
node_sample_offsets: &Tensor<u32>,
node_sample_ids: &Tensor<u32>,
node_sample_mults: &Tensor<u32>,
node_features: &Tensor<u32>,
node_counts: &Tensor<u32>,
node_y_sums: &Tensor<f32>,
node_y_sum_sqs: &Tensor<f32>,
slot_best_score: &mut Tensor<f32>,
slot_best_thr: &mut Tensor<u32>,
slot_best_n_left: &mut Tensor<u32>,
slot_best_valid: &mut Tensor<u32>,
n_samples: u32,
wave_size: u32,
n_active_nodes: u32,
k_feats: u32,
n_targets: u32,
n_bins_gpu: u32,
bin_shift: u32,
min_samples_leaf: u32,
#[comptime] wg_size: u32,
) {
let slot = CUBE_POS_X;
let node = CUBE_POS_Y;
let tree = CUBE_POS_Z;
if slot >= k_feats {
terminate!();
}
if node >= n_active_nodes {
terminate!();
}
if tree >= wave_size {
terminate!();
}
let mut s_hist = SharedMemory::<f32>::new(SMEM_HIST_SLOTS as usize);
let mut s_cnt = SharedMemory::<u32>::new(N_BINS as usize);
let mut s_score = SharedMemory::<f32>::new(WORKGROUP_128 as usize);
let mut s_bin = SharedMemory::<u32>::new(WORKGROUP_128 as usize);
let mut s_nl = SharedMemory::<u32>::new(WORKGROUP_128 as usize);
let mut s_valid = SharedMemory::<u32>::new(WORKGROUP_128 as usize);
let mut s_pq = SharedMemory::<f32>::new(2usize);
let mut s_tile_id = SharedMemory::<u32>::new(WORKGROUP_128 as usize);
let mut s_tile_mult = SharedMemory::<u32>::new(WORKGROUP_128 as usize);
let mut s_tile_bin = SharedMemory::<u32>::new(WORKGROUP_128 as usize);
let mut ys = Array::<f32>::new(RF_UNROLL as usize);
let mut bs = Array::<u32>::new(RF_UNROLL as usize);
let mut ms = Array::<u32>::new(RF_UNROLL as usize);
let tx = UNIT_POS_X;
let node_flat = (tree * n_active_nodes + node) as usize;
let slot_flat = ((tree * n_active_nodes + node) * k_feats + slot) as usize;
let stats_base = node_flat * n_targets as usize;
let n = node_counts[node_flat];
if n < 2u32 || n < 2u32 * min_samples_leaf {
if tx == 0u32 {
slot_best_score[slot_flat] = 0f32;
slot_best_thr[slot_flat] = 0u32;
slot_best_n_left[slot_flat] = 0u32;
slot_best_valid[slot_flat] = 0u32;
}
terminate!();
}
let stride = n_targets + 1u32;
let feat = node_features[slot_flat];
let hist_len = n_bins_gpu * stride;
let mut z: u32 = tx;
while z < hist_len {
s_hist[z as usize] = 0f32;
z += wg_size;
}
let mut zc: u32 = tx;
while zc < n_bins_gpu {
s_cnt[zc as usize] = 0u32;
zc += wg_size;
}
sync_cube();
let obase = (tree * (n_active_nodes + 1u32)) as usize;
let lo = node_sample_offsets[obase + node as usize];
let hi = node_sample_offsets[obase + (node + 1u32) as usize];
let gbase = (tree * n_samples) as usize;
let mut base: u32 = lo;
while base < hi {
let idx = base + tx;
if idx < hi {
let s = node_sample_ids[gbase + idx as usize];
s_tile_id[tx as usize] = s;
s_tile_mult[tx as usize] = node_sample_mults[gbase + idx as usize];
s_tile_bin[tx as usize] = feature_bin(feature_data, feat * n_samples + s) >> bin_shift;
}
sync_cube();
let mut cnt = hi - base;
if cnt > CUBE_DIM_X {
cnt = CUBE_DIM_X;
}
let mut i: u32 = 0u32;
while i + (RF_UNROLL - 1u32) < cnt {
#[unroll]
for u in 0..RF_UNROLL {
bs[u as usize] = s_tile_bin[(i + u) as usize];
ms[u as usize] = s_tile_mult[(i + u) as usize];
}
if tx < n_targets {
#[unroll]
for u in 0..RF_UNROLL {
ys[u as usize] =
y_dense[(s_tile_id[(i + u) as usize] * n_targets + tx) as usize];
}
#[unroll]
for u in 0..RF_UNROLL {
s_hist[(bs[u as usize] * stride + tx) as usize] +=
f32::cast_from(ms[u as usize]) * ys[u as usize];
}
}
if tx == 0u32 {
#[unroll]
for u in 0..RF_UNROLL {
s_cnt[bs[u as usize] as usize] += ms[u as usize];
}
}
i += RF_UNROLL;
}
while i < cnt {
let b = s_tile_bin[i as usize];
if tx < n_targets {
let mf = f32::cast_from(s_tile_mult[i as usize]);
let y = y_dense[(s_tile_id[i as usize] * n_targets + tx) as usize];
s_hist[(b * stride + tx) as usize] += mf * y;
}
if tx == 0u32 {
s_cnt[b as usize] += s_tile_mult[i as usize];
}
i += 1u32;
}
sync_cube();
base += CUBE_DIM_X;
}
if tx < n_targets {
let mut run: f32 = 0f32;
let mut b: u32 = 0u32;
while b < n_bins_gpu {
run += s_hist[(b * stride + tx) as usize];
s_hist[(b * stride + tx) as usize] = run;
b += 1u32;
}
}
if tx == 0u32 {
let mut runc: u32 = 0u32;
let mut b: u32 = 0u32;
while b < n_bins_gpu {
runc += s_cnt[b as usize];
s_cnt[b as usize] = runc;
b += 1u32;
}
let nf = f32::cast_from(n);
let mut p: f32 = 0f32;
let mut q: f32 = 0f32;
let mut k: u32 = 0u32;
while k < n_targets {
let sy = node_y_sums[stats_base + k as usize];
let ssq = node_y_sum_sqs[stats_base + k as usize];
q += ssq;
let mean = sy / nf;
p += f32::max(ssq / nf - mean * mean, 0f32);
k += 1u32;
}
s_pq[0] = p;
s_pq[1] = q;
}
sync_cube();
let parent_var = s_pq[0];
let q_tot = s_pq[1];
if parent_var <= 0f32 {
if tx == 0u32 {
slot_best_score[slot_flat] = 0f32;
slot_best_thr[slot_flat] = 0u32;
slot_best_n_left[slot_flat] = 0u32;
slot_best_valid[slot_flat] = 0u32;
}
terminate!();
}
let mut best_g: f32 = 0f32;
let mut best_b: u32 = 0u32;
let mut best_nl: u32 = 0u32;
let mut best_valid: u32 = 0u32;
let mut b: u32 = tx;
while b + 1u32 < n_bins_gpu {
let n_left = s_cnt[b as usize];
let n_right = n - n_left;
if n_left >= min_samples_leaf {
if n_right >= min_samples_leaf {
let nl = f32::cast_from(n_left);
let nr = f32::cast_from(n_right);
let mut sl: f32 = 0f32;
let mut sr: f32 = 0f32;
let mut k: u32 = 0u32;
while k < n_targets {
let cl = s_hist[(b * stride + k) as usize];
let cr = node_y_sums[stats_base + k as usize] - cl;
sl += cl * cl;
sr += cr * cr;
k += 1u32;
}
let g = sl / nl + sr / nr;
if best_valid == 0u32 || g > best_g {
best_g = g;
best_b = b;
best_nl = n_left;
best_valid = 1u32;
}
}
}
b += wg_size;
}
s_score[tx as usize] = best_g;
s_bin[tx as usize] = best_b;
s_nl[tx as usize] = best_nl;
s_valid[tx as usize] = best_valid;
sync_cube();
let mut red: u32 = (wg_size / 2u32).runtime();
while red > 0u32 {
if tx < red {
let mate = tx + red;
let take = argmax_takes_mate(
s_valid[tx as usize],
s_score[tx as usize],
s_valid[mate as usize],
s_score[mate as usize],
);
if take == 1u32 {
s_score[tx as usize] = s_score[mate as usize];
s_bin[tx as usize] = s_bin[mate as usize];
s_nl[tx as usize] = s_nl[mate as usize];
s_valid[tx as usize] = s_valid[mate as usize];
}
}
sync_cube();
red /= 2u32;
}
if tx == 0u32 {
let nf = f32::cast_from(n);
let score = parent_var - q_tot / nf + s_score[0] / nf;
if s_valid[0] == 1u32 && score > 0f32 {
slot_best_score[slot_flat] = score;
slot_best_thr[slot_flat] = (s_bin[0] << bin_shift) | ((1u32 << bin_shift) - 1u32);
slot_best_n_left[slot_flat] = s_nl[0];
slot_best_valid[slot_flat] = 1u32;
} else {
slot_best_score[slot_flat] = 0f32;
slot_best_thr[slot_flat] = 0u32;
slot_best_n_left[slot_flat] = 0u32;
slot_best_valid[slot_flat] = 0u32;
}
}
}
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments)]
pub fn reduce_slot_winners(
slot_best_score: &Tensor<f32>,
slot_best_thr: &Tensor<u32>,
slot_best_n_left: &Tensor<u32>,
slot_best_valid: &Tensor<u32>,
node_features: &Tensor<u32>,
split_feature: &mut Tensor<u32>,
split_threshold: &mut Tensor<u32>,
split_n_left: &mut Tensor<u32>,
wave_size: u32,
n_active_nodes: u32,
k_feats: u32,
) {
let node = CUBE_POS_X + CUBE_POS_Y * CUBE_COUNT_X;
let tree = CUBE_POS_Z;
if node >= n_active_nodes {
terminate!();
}
if tree >= wave_size {
terminate!();
}
if UNIT_POS_X != 0u32 {
terminate!();
}
let node_flat = (tree * n_active_nodes + node) as usize;
let base = node_flat * k_feats as usize;
let mut best_score: f32 = 0f32;
let mut best_slot: u32 = 0u32;
let mut found: u32 = 0u32;
let mut s: u32 = 0u32;
while s < k_feats {
if slot_best_valid[base + s as usize] == 1u32 {
let sc = slot_best_score[base + s as usize];
if found == 0u32 || sc > best_score {
best_score = sc;
best_slot = s;
found = 1u32;
}
}
s += 1u32;
}
if found == 1u32 {
split_feature[node_flat] = node_features[base + best_slot as usize];
split_threshold[node_flat] = slot_best_thr[base + best_slot as usize];
split_n_left[node_flat] = slot_best_n_left[base + best_slot as usize];
} else {
split_feature[node_flat] = INVALID_NODE;
split_threshold[node_flat] = 0u32;
split_n_left[node_flat] = 0u32;
}
}
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments, clippy::collapsible_if)]
pub fn scan_slot_bin_range(
feature_data: &Tensor<u32>,
sample_to_node: &Tensor<u32>,
sample_multiplicity: &Tensor<u32>,
node_features: &Tensor<u32>,
node_counts: &Tensor<u32>,
slot_min_bin: &mut Tensor<u32>,
slot_max_bin: &mut Tensor<u32>,
n_samples: u32,
wave_size: u32,
n_active_nodes: u32,
k_feats: u32,
#[comptime] wg_size: u32,
) {
let slot = CUBE_POS_X;
let node = CUBE_POS_Y;
let tree = CUBE_POS_Z;
if slot >= k_feats {
terminate!();
}
if node >= n_active_nodes {
terminate!();
}
if tree >= wave_size {
terminate!();
}
let slot_flat = ((tree * n_active_nodes + node) * k_feats + slot) as usize;
let tx = UNIT_POS_X;
if node_counts[(tree * n_active_nodes + node) as usize] == 0u32 {
if tx == 0u32 {
slot_min_bin[slot_flat] = 0u32;
slot_max_bin[slot_flat] = 0u32;
}
terminate!();
}
let feat = node_features[slot_flat];
let s2n_base = (tree * n_samples) as usize;
let n_bins = N_BINS.runtime();
let mut lo: u32 = n_bins;
let mut hi: u32 = 0u32;
let mut s: u32 = tx;
while s < n_samples {
if sample_to_node[s2n_base + s as usize] == node {
if sample_multiplicity[s2n_base + s as usize] > 0u32 {
let b = feature_bin(feature_data, feat * n_samples + s);
if b < lo {
lo = b;
}
if b > hi {
hi = b;
}
}
}
s += wg_size;
}
let mut s_lo = SharedMemory::<u32>::new(WORKGROUP_128 as usize);
let mut s_hi = SharedMemory::<u32>::new(WORKGROUP_128 as usize);
s_lo[tx as usize] = lo;
s_hi[tx as usize] = hi;
sync_cube();
let mut stride: u32 = (wg_size / 2u32).runtime();
while stride > 0u32 {
if tx < stride {
let a = s_lo[(tx + stride) as usize];
if a < s_lo[tx as usize] {
s_lo[tx as usize] = a;
}
let b = s_hi[(tx + stride) as usize];
if b > s_hi[tx as usize] {
s_hi[tx as usize] = b;
}
}
sync_cube();
stride /= 2u32;
}
if tx == 0u32 {
if s_lo[0] >= n_bins {
slot_min_bin[slot_flat] = 0u32;
slot_max_bin[slot_flat] = 0u32;
} else {
slot_min_bin[slot_flat] = s_lo[0];
slot_max_bin[slot_flat] = s_hi[0];
}
}
}
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments, clippy::collapsible_if)]
pub fn accumulate_split_stats_et(
feature_data: &Tensor<u32>,
y_dense: &Tensor<f32>,
sample_to_node: &Tensor<u32>,
sample_multiplicity: &Tensor<u32>,
node_features: &Tensor<u32>,
node_counts: &Tensor<u32>,
tree_seeds: &Tensor<u32>,
slot_min_bin: &Tensor<u32>,
slot_max_bin: &Tensor<u32>,
cand_thr: &mut Tensor<u32>,
cand_n_left: &mut Tensor<u32>,
cand_y_sums_l: &mut Tensor<f32>,
cand_y_sum_sqs_l: &mut Tensor<f32>,
n_samples: u32,
wave_size: u32,
n_active_nodes: u32,
k_feats: u32,
n_targets: u32,
n_thresholds: u32,
level: u32,
#[comptime] use_plane: bool,
) {
let cand = CUBE_POS_X;
let node = CUBE_POS_Y;
let tree = CUBE_POS_Z;
let n_cands = k_feats * n_thresholds;
if cand >= n_cands {
terminate!();
}
if node >= n_active_nodes {
terminate!();
}
if tree >= wave_size {
terminate!();
}
let slot = cand / n_thresholds;
let ti = cand % n_thresholds;
let tx = UNIT_POS_X;
let node_flat = (tree * n_active_nodes + node) as usize;
let slot_flat = ((tree * n_active_nodes + node) * k_feats + slot) as usize;
let cand_flat = ((tree * n_active_nodes + node) * n_cands + cand) as usize;
let min_bin = slot_min_bin[slot_flat];
let max_bin = slot_max_bin[slot_flat];
if node_counts[node_flat] == 0u32 || max_bin <= min_bin {
if tx == 0u32 {
cand_thr[cand_flat] = 0u32;
cand_n_left[cand_flat] = 0u32;
}
terminate!();
}
let mut seed_mix = tree_seeds[tree as usize];
seed_mix = hash_mix(seed_mix ^ level);
seed_mix = hash_mix(seed_mix ^ node);
seed_mix = hash_mix(seed_mix ^ slot);
seed_mix = hash_mix(seed_mix ^ ti);
seed_mix = hash_mix(seed_mix ^ 2654435769u32);
let thr = min_bin + (seed_mix % (max_bin - min_bin));
let feat = node_features[slot_flat];
let s2n_base = (tree * n_samples) as usize;
let mut s_sum = SharedMemory::<f32>::new(WORKGROUP_128 as usize);
let mut s_sq = SharedMemory::<f32>::new(WORKGROUP_128 as usize);
let mut s_cnt = SharedMemory::<u32>::new(WORKGROUP_128 as usize);
let mut s_id = SharedMemory::<u32>::new(WORKGROUP_128 as usize);
let mut s_mult = SharedMemory::<u32>::new(WORKGROUP_128 as usize);
let mut s_ptot = SharedMemory::<u32>::new(MAX_PLANES_PER_CUBE as usize);
let mut acc_s: f32 = 0f32;
let mut acc_q: f32 = 0f32;
let mut acc_n: u32 = 0u32;
if use_plane {
let lane = UNIT_POS_PLANE;
let plane_id = UNIT_POS_X / PLANE_DIM;
let n_planes = CUBE_DIM_X / PLANE_DIM;
let n_blocks = n_samples.div_ceil(CUBE_DIM_X);
let mut blk: u32 = 0u32;
while blk < n_blocks {
let s = blk * CUBE_DIM_X + tx;
let mut hit: u32 = 0u32;
let mut mult: u32 = 0u32;
if s < n_samples {
if sample_to_node[s2n_base + s as usize] == node {
let m = sample_multiplicity[s2n_base + s as usize];
if m > 0u32 {
if feature_bin(feature_data, feat * n_samples + s) <= thr {
hit = 1u32;
mult = m;
}
}
}
}
let p_excl = plane_exclusive_sum(hit);
let p_tot = plane_sum(hit);
if lane == 0u32 {
s_ptot[plane_id as usize] = p_tot;
}
sync_cube();
let mut base: u32 = 0u32;
let mut cnt: u32 = 0u32;
let mut i: u32 = 0u32;
while i < n_planes {
let t = s_ptot[i as usize];
if i < plane_id {
base += t;
}
cnt += t;
i += 1u32;
}
if hit == 1u32 {
s_id[(base + p_excl) as usize] = s;
s_mult[(base + p_excl) as usize] = mult;
}
sync_cube();
if tx < n_targets {
let mut j: u32 = 0u32;
while j < cnt {
let sid = s_id[j as usize];
let mf = f32::cast_from(s_mult[j as usize]);
let y = y_dense[(sid * n_targets + tx) as usize];
acc_s += mf * y;
acc_q += mf * y * y;
j += 1u32;
}
}
if tx == 0u32 {
let mut j: u32 = 0u32;
while j < cnt {
acc_n += s_mult[j as usize];
j += 1u32;
}
}
sync_cube();
blk += 1u32;
}
if tx < n_targets {
let out = cand_flat * n_targets as usize + tx as usize;
cand_y_sums_l[out] = acc_s;
cand_y_sum_sqs_l[out] = acc_q;
}
if tx == 0u32 {
cand_thr[cand_flat] = thr;
cand_n_left[cand_flat] = acc_n;
}
} else {
let n_stripes = (CUBE_DIM_X / n_targets).max(1u32);
let stripe = tx / n_targets;
let k = tx % n_targets;
if stripe < n_stripes {
let mut s: u32 = stripe;
while s < n_samples {
if sample_to_node[s2n_base + s as usize] == node {
let mult = sample_multiplicity[s2n_base + s as usize];
if mult > 0u32 {
if feature_bin(feature_data, feat * n_samples + s) <= thr {
let mf = f32::cast_from(mult);
let y = y_dense[(s * n_targets + k) as usize];
acc_s += mf * y;
acc_q += mf * y * y;
acc_n += mult;
}
}
}
s += n_stripes;
}
}
s_sum[tx as usize] = acc_s;
s_sq[tx as usize] = acc_q;
s_cnt[tx as usize] = acc_n;
sync_cube();
if tx < n_targets {
let mut tot_s: f32 = 0f32;
let mut tot_q: f32 = 0f32;
let mut p: u32 = 0u32;
while p < n_stripes {
tot_s += s_sum[(p * n_targets + tx) as usize];
tot_q += s_sq[(p * n_targets + tx) as usize];
p += 1u32;
}
let out = cand_flat * n_targets as usize + tx as usize;
cand_y_sums_l[out] = tot_s;
cand_y_sum_sqs_l[out] = tot_q;
}
if tx == 0u32 {
let mut tot_n: u32 = 0u32;
let mut p: u32 = 0u32;
while p < n_stripes {
tot_n += s_cnt[(p * n_targets) as usize];
p += 1u32;
}
cand_thr[cand_flat] = thr;
cand_n_left[cand_flat] = tot_n;
}
}
}
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments, clippy::collapsible_if, unused_assignments)]
pub fn evaluate_splits_et_direct(
node_counts: &Tensor<u32>,
node_y_sums: &Tensor<f32>,
node_y_sum_sqs: &Tensor<f32>,
node_features: &Tensor<u32>,
cand_thr: &Tensor<u32>,
cand_n_left: &Tensor<u32>,
cand_y_sums_l: &Tensor<f32>,
cand_y_sum_sqs_l: &Tensor<f32>,
split_feature: &mut Tensor<u32>,
split_threshold: &mut Tensor<u32>,
split_n_left: &mut Tensor<u32>,
split_y_sums_l: &mut Tensor<f32>,
split_y_sum_sqs_l: &mut Tensor<f32>,
wave_size: u32,
n_active_nodes: u32,
k_feats: u32,
n_targets: u32,
n_thresholds: u32,
min_samples_leaf: u32,
#[comptime] wg_size: u32,
#[comptime] use_plane: bool,
) {
let node = CUBE_POS_X + CUBE_POS_Y * CUBE_COUNT_X;
let tree = CUBE_POS_Z;
if node >= n_active_nodes {
terminate!();
}
if tree >= wave_size {
terminate!();
}
let tx = UNIT_POS_X;
let node_flat = (tree * n_active_nodes + node) as usize;
let n = node_counts[node_flat];
let stats_base = node_flat * n_targets as usize;
let nf = f32::cast_from(n);
let n_cands = k_feats * n_thresholds;
let mut s_parent_var = SharedMemory::<f32>::new(1usize);
if tx == 0u32 {
let mut pv: f32 = 0f32;
if n >= 2u32 {
let mut k: u32 = 0u32;
while k < n_targets {
let s = node_y_sums[stats_base + k as usize];
let ss = node_y_sum_sqs[stats_base + k as usize];
let mean = s / nf;
let v = ss / nf - mean * mean;
pv += f32::max(v, 0f32);
k += 1u32;
}
}
s_parent_var[0] = pv;
}
sync_cube();
let parent_var_sum = s_parent_var[0];
if n < 2u32 * min_samples_leaf {
if tx == 0u32 {
split_feature[node_flat] = INVALID_NODE;
split_threshold[node_flat] = 0u32;
split_n_left[node_flat] = 0u32;
}
terminate!();
}
if parent_var_sum <= 0f32 {
if tx == 0u32 {
split_feature[node_flat] = INVALID_NODE;
split_threshold[node_flat] = 0u32;
split_n_left[node_flat] = 0u32;
}
terminate!();
}
let mut best_score: f32 = 0f32;
let mut best_cand: u32 = 0u32;
let mut best_n_left: u32 = 0u32;
let mut best_valid: u32 = 0u32;
let mut c: u32 = tx;
while c < n_cands {
let cand_flat = (node_flat as u32 * n_cands + c) as usize;
let n_left = cand_n_left[cand_flat];
let n_right = n - n_left;
if n_left >= min_samples_leaf && n_right >= min_samples_leaf {
let nl = f32::cast_from(n_left);
let nr = f32::cast_from(n_right);
let inv_nl = 1f32 / nl;
let inv_nr = 1f32 / nr;
let wl = nl / nf;
let wr = nr / nf;
let mut score: f32 = 0f32;
let cbase = cand_flat * n_targets as usize;
let mut k: u32 = 0u32;
while k < n_targets {
let sy = node_y_sums[stats_base + k as usize];
let ssq = node_y_sum_sqs[stats_base + k as usize];
let mean_p = sy / nf;
let var_p = ssq / nf - mean_p * mean_p;
let syl = cand_y_sums_l[cbase + k as usize];
let ssyl = cand_y_sum_sqs_l[cbase + k as usize];
let mean_l = syl * inv_nl;
let var_l = ssyl * inv_nl - mean_l * mean_l;
let syr = sy - syl;
let ssyr = ssq - ssyl;
let mean_r = syr * inv_nr;
let var_r = ssyr * inv_nr - mean_r * mean_r;
let vp = f32::max(var_p, 0f32);
let vl = f32::max(var_l, 0f32);
let vr = f32::max(var_r, 0f32);
score += vp - wl * vl - wr * vr;
k += 1u32;
}
if score > best_score {
best_score = score;
best_cand = c;
best_n_left = n_left;
best_valid = 1u32;
}
}
c += wg_size;
}
let mut s_score = SharedMemory::<f32>::new(WORKGROUP_32 as usize);
let mut s_cand = SharedMemory::<u32>::new(WORKGROUP_32 as usize);
let mut s_nl = SharedMemory::<u32>::new(WORKGROUP_32 as usize);
let mut s_valid = SharedMemory::<u32>::new(WORKGROUP_32 as usize);
let mut winner_valid: u32 = 0u32;
let mut winner_cand: u32 = 0u32;
let mut winner_n_left: u32 = 0u32;
if use_plane {
let winner_score = plane_max(best_score);
let mut rank: u32 = PLANE_DIM;
if best_valid == 1u32 && best_score == winner_score {
rank = UNIT_POS_PLANE;
}
let winner_lane = plane_min(rank);
let mut src: u32 = 0u32;
if winner_lane < PLANE_DIM {
src = winner_lane;
winner_valid = 1u32;
}
winner_cand = plane_shuffle(best_cand, src);
winner_n_left = plane_shuffle(best_n_left, src);
} else {
s_score[tx as usize] = best_score;
s_cand[tx as usize] = best_cand;
s_nl[tx as usize] = best_n_left;
s_valid[tx as usize] = best_valid;
sync_cube();
let mut stride: u32 = (wg_size / 2u32).runtime();
while stride > 0u32 {
if tx < stride {
let mate = tx + stride;
let take = argmax_takes_mate(
s_valid[tx as usize],
s_score[tx as usize],
s_valid[mate as usize],
s_score[mate as usize],
);
if take == 1u32 {
s_score[tx as usize] = s_score[mate as usize];
s_cand[tx as usize] = s_cand[mate as usize];
s_nl[tx as usize] = s_nl[mate as usize];
s_valid[tx as usize] = s_valid[mate as usize];
}
}
sync_cube();
stride /= 2u32;
}
winner_valid = s_valid[0];
winner_cand = s_cand[0];
winner_n_left = s_nl[0];
}
if tx == 0u32 {
if winner_valid == 1u32 {
let winner_slot = winner_cand / n_thresholds;
let feat =
node_features[((tree * n_active_nodes + node) * k_feats + winner_slot) as usize];
split_feature[node_flat] = feat;
split_threshold[node_flat] =
cand_thr[(node_flat as u32 * n_cands + winner_cand) as usize];
split_n_left[node_flat] = winner_n_left;
} else {
split_feature[node_flat] = INVALID_NODE;
split_threshold[node_flat] = 0u32;
split_n_left[node_flat] = 0u32;
}
}
if winner_valid == 1u32 {
let cbase = (node_flat as u32 * n_cands + winner_cand) as usize * n_targets as usize;
let mut k: u32 = tx;
while k < n_targets {
split_y_sums_l[stats_base + k as usize] = cand_y_sums_l[cbase + k as usize];
split_y_sum_sqs_l[stats_base + k as usize] = cand_y_sum_sqs_l[cbase + k as usize];
k += wg_size;
}
}
}
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments)]
pub fn init_root_stats(
y_dense: &Tensor<f32>,
sample_multiplicity: &Tensor<u32>,
node_counts: &mut Tensor<u32>,
node_y_sums: &mut Tensor<f32>,
node_y_sum_sqs: &mut Tensor<f32>,
n_samples: u32,
wave_size: u32,
n_active_nodes: u32,
n_targets: u32,
#[comptime] wg_size: u32,
) {
let tree = CUBE_POS_X;
if tree >= wave_size {
terminate!();
}
let tx = UNIT_POS_X;
let s2n_base = (tree * n_samples) as usize;
let node_flat = (tree * n_active_nodes) as usize;
if tx == 0u32 {
let mut tot: u32 = 0u32;
let mut s: u32 = 0u32;
while s < n_samples {
tot += sample_multiplicity[s2n_base + s as usize];
s += 1u32;
}
node_counts[node_flat] = tot;
}
let mut k: u32 = tx;
while k < n_targets {
let mut acc_s: f32 = 0f32;
let mut acc_q: f32 = 0f32;
let mut s: u32 = 0u32;
while s < n_samples {
let mult = sample_multiplicity[s2n_base + s as usize];
if mult > 0u32 {
let mf = f32::cast_from(mult);
let y = y_dense[(s * n_targets + k) as usize];
acc_s += mf * y;
acc_q += mf * y * y;
}
s += 1u32;
}
node_y_sums[node_flat * n_targets as usize + k as usize] = acc_s;
node_y_sum_sqs[node_flat * n_targets as usize + k as usize] = acc_q;
k += wg_size;
}
}
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments)]
pub fn reassign_samples(
feature_data: &Tensor<u32>,
split_feature: &Tensor<u32>,
split_threshold: &Tensor<u32>,
left_child_id: &Tensor<u32>,
right_child_id: &Tensor<u32>,
sample_to_node: &mut Tensor<u32>,
n_samples: u32,
n_features: u32,
wave_size: u32,
n_active_nodes: u32,
) {
let s = CUBE_POS_X * WORKGROUP_128 + UNIT_POS_X + CUBE_POS_Y * CUBE_COUNT_X * WORKGROUP_128;
let tree = CUBE_POS_Z;
if s >= n_samples {
terminate!();
}
if tree >= wave_size {
terminate!();
}
let s2n_idx = (tree * n_samples + s) as usize;
let node = sample_to_node[s2n_idx];
if node == INVALID_NODE {
terminate!();
}
if node >= n_active_nodes {
sample_to_node[s2n_idx] = INVALID_NODE;
terminate!();
}
let node_flat = (tree * n_active_nodes + node) as usize;
let feat = split_feature[node_flat];
if feat == INVALID_NODE {
sample_to_node[s2n_idx] = INVALID_NODE;
terminate!();
}
if feat >= n_features {
sample_to_node[s2n_idx] = INVALID_NODE;
terminate!();
}
let thr = split_threshold[node_flat];
let bin = feature_bin(feature_data, feat * n_samples + s);
if bin <= thr {
sample_to_node[s2n_idx] = left_child_id[node_flat];
} else {
sample_to_node[s2n_idx] = right_child_id[node_flat];
}
}
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments)]
pub fn accumulate_importance(
node_counts: &Tensor<u32>,
node_y_sums: &Tensor<f32>,
node_y_sum_sqs: &Tensor<f32>,
split_feature: &Tensor<u32>,
split_n_left: &Tensor<u32>,
split_y_sums_l: &Tensor<f32>,
split_y_sum_sqs_l: &Tensor<f32>,
batch_importances: &mut Tensor<Atomic<u32>>,
wave_size: u32,
n_active_nodes: u32,
n_targets: u32,
n_total: u32,
#[comptime] wg_size: u32,
) {
let node = CUBE_POS_X + CUBE_POS_Y * CUBE_COUNT_X;
let tree = CUBE_POS_Z;
if node >= n_active_nodes {
terminate!();
}
if tree >= wave_size {
terminate!();
}
let tx = UNIT_POS_X;
let node_flat = (tree * n_active_nodes + node) as usize;
let feat = split_feature[node_flat];
if feat == INVALID_NODE {
terminate!();
}
let n = node_counts[node_flat];
let n_left = split_n_left[node_flat];
let n_right = n - n_left;
let nf = f32::cast_from(n);
let nl = f32::cast_from(n_left);
let nr = f32::cast_from(n_right);
let weight = nf / f32::cast_from(n_total);
let stats_base = node_flat * n_targets as usize;
let imp_base = (feat * n_targets) as usize;
let mut k: u32 = tx;
while k < n_targets {
let sy = node_y_sums[stats_base + k as usize];
let ssq = node_y_sum_sqs[stats_base + k as usize];
let syl = split_y_sums_l[stats_base + k as usize];
let ssyl = split_y_sum_sqs_l[stats_base + k as usize];
let syr = sy - syl;
let ssyr = ssq - ssyl;
let mean_p = sy / nf;
let var_p = ssq / nf - mean_p * mean_p;
let mean_l = syl / nl;
let var_l = ssyl / nl - mean_l * mean_l;
let mean_r = syr / nr;
let var_r = ssyr / nr - mean_r * mean_r;
let vp = f32::max(var_p, 0f32);
let vl = f32::max(var_l, 0f32);
let vr = f32::max(var_r, 0f32);
let reduction = f32::max(vp - (nl / nf) * vl - (nr / nf) * vr, 0f32);
let contribution = weight * reduction;
atomic_add_f32_bits(&batch_importances[imp_base + k as usize], contribution);
k += wg_size;
}
}
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments)]
pub fn compute_child_ids(
split_feature: &Tensor<u32>,
node_counts: &Tensor<u32>,
left_child_id: &mut Tensor<u32>,
right_child_id: &mut Tensor<u32>,
wave_size: u32,
n_active_nodes: u32,
) {
let tree = CUBE_POS_X;
if tree >= wave_size {
terminate!();
}
if UNIT_POS_X != 0u32 {
terminate!();
}
let base = (tree * n_active_nodes) as usize;
let mut next_w: u32 = 0u32;
let mut node: u32 = 0u32;
while node < n_active_nodes {
let idx = base + node as usize;
let feat = split_feature[idx];
if feat != INVALID_NODE {
let n = node_counts[idx];
if n > 0u32 {
left_child_id[idx] = next_w;
right_child_id[idx] = next_w + 1u32;
next_w += 2u32;
} else {
left_child_id[idx] = INVALID_NODE;
right_child_id[idx] = INVALID_NODE;
}
} else {
left_child_id[idx] = INVALID_NODE;
right_child_id[idx] = INVALID_NODE;
}
node += 1u32;
}
}
#[cube(launch_unchecked)]
pub fn zero_node_stats(
node_counts: &mut Tensor<u32>,
node_y_sums: &mut Tensor<f32>,
node_y_sum_sqs: &mut Tensor<f32>,
wave_size: u32,
n_active_nodes: u32,
n_targets: u32,
#[comptime] wg_size: u32,
) {
let node = CUBE_POS_X + CUBE_POS_Y * CUBE_COUNT_X;
let tree = CUBE_POS_Z;
if node >= n_active_nodes {
terminate!();
}
if tree >= wave_size {
terminate!();
}
let tx = UNIT_POS_X;
let node_flat = (tree * n_active_nodes + node) as usize;
if tx == 0u32 {
node_counts[node_flat] = 0u32;
}
let stats_base = node_flat * n_targets as usize;
let mut k: u32 = tx;
while k < n_targets {
node_y_sums[stats_base + k as usize] = 0f32;
node_y_sum_sqs[stats_base + k as usize] = 0f32;
k += wg_size;
}
}
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments)]
pub fn propagate_child_stats(
split_feature: &Tensor<u32>,
split_n_left: &Tensor<u32>,
split_y_sums_l: &Tensor<f32>,
split_y_sum_sqs_l: &Tensor<f32>,
left_child_id: &Tensor<u32>,
right_child_id: &Tensor<u32>,
node_counts: &Tensor<u32>,
node_y_sums: &Tensor<f32>,
node_y_sum_sqs: &Tensor<f32>,
next_counts: &mut Tensor<u32>,
next_y_sums: &mut Tensor<f32>,
next_y_sum_sqs: &mut Tensor<f32>,
wave_size: u32,
n_active_nodes: u32,
n_active_next: u32,
n_targets: u32,
#[comptime] wg_size: u32,
) {
let node = CUBE_POS_X + CUBE_POS_Y * CUBE_COUNT_X;
let tree = CUBE_POS_Z;
if node >= n_active_nodes {
terminate!();
}
if tree >= wave_size {
terminate!();
}
let node_flat = (tree * n_active_nodes + node) as usize;
if split_feature[node_flat] == INVALID_NODE {
terminate!();
}
let lid = left_child_id[node_flat];
let rid = right_child_id[node_flat];
if lid == INVALID_NODE {
terminate!();
}
if lid >= n_active_next {
terminate!();
}
if rid >= n_active_next {
terminate!();
}
let tx = UNIT_POS_X;
let n = node_counts[node_flat];
let n_left = split_n_left[node_flat];
let l_flat = (tree * n_active_next + lid) as usize;
let r_flat = (tree * n_active_next + rid) as usize;
if tx == 0u32 {
next_counts[l_flat] = n_left;
next_counts[r_flat] = n - n_left;
}
let stats_base = node_flat * n_targets as usize;
let l_base = l_flat * n_targets as usize;
let r_base = r_flat * n_targets as usize;
let mut k: u32 = tx;
while k < n_targets {
let sy = node_y_sums[stats_base + k as usize];
let ssq = node_y_sum_sqs[stats_base + k as usize];
let syl = split_y_sums_l[stats_base + k as usize];
let ssyl = split_y_sum_sqs_l[stats_base + k as usize];
next_y_sums[l_base + k as usize] = syl;
next_y_sum_sqs[l_base + k as usize] = ssyl;
next_y_sums[r_base + k as usize] = sy - syl;
next_y_sum_sqs[r_base + k as usize] = ssq - ssyl;
k += wg_size;
}
}
#[cube(launch_unchecked)]
pub fn init_sample_to_node(
sample_multiplicity: &Tensor<u32>,
sample_to_node: &mut Tensor<u32>,
n_samples: u32,
wave_size: u32,
) {
let s = CUBE_POS_X * WORKGROUP_128 + UNIT_POS_X + CUBE_POS_Y * CUBE_COUNT_X * WORKGROUP_128;
let tree = CUBE_POS_Z;
if s >= n_samples {
terminate!();
}
if tree >= wave_size {
terminate!();
}
let idx = (tree * n_samples + s) as usize;
if sample_multiplicity[idx] > 0u32 {
sample_to_node[idx] = 0u32;
} else {
sample_to_node[idx] = INVALID_NODE;
}
}
#[cube(launch_unchecked)]
pub fn zero_node_sample_counts(
node_sample_counts: &mut Tensor<Atomic<u32>>,
wave_size: u32,
n_active_nodes: u32,
#[comptime] wg_size: u32,
) {
let tree = CUBE_POS_X;
if tree >= wave_size {
terminate!();
}
let base = (tree * n_active_nodes) as usize;
let mut n: u32 = UNIT_POS_X;
while n < n_active_nodes {
Atomic::store(&node_sample_counts[base + n as usize], 0u32);
n += wg_size;
}
}
#[cube(launch_unchecked)]
#[allow(clippy::collapsible_if)]
pub fn count_node_samples(
sample_to_node: &Tensor<u32>,
sample_multiplicity: &Tensor<u32>,
node_sample_counts: &mut Tensor<Atomic<u32>>,
n_samples: u32,
wave_size: u32,
n_active_nodes: u32,
) {
let s = CUBE_POS_X * WORKGROUP_128 + UNIT_POS_X + CUBE_POS_Y * CUBE_COUNT_X * WORKGROUP_128;
let tree = CUBE_POS_Z;
if s >= n_samples {
terminate!();
}
if tree >= wave_size {
terminate!();
}
let idx = (tree * n_samples + s) as usize;
let node = sample_to_node[idx];
if node < n_active_nodes {
if sample_multiplicity[idx] > 0u32 {
Atomic::fetch_add(
&node_sample_counts[(tree * n_active_nodes + node) as usize],
1u32,
);
}
}
}
#[cube(launch_unchecked)]
pub fn scan_node_offsets(
node_sample_counts: &mut Tensor<Atomic<u32>>,
node_sample_offsets: &mut Tensor<u32>,
wave_size: u32,
n_active_nodes: u32,
) {
let tree = CUBE_POS_X;
if tree >= wave_size {
terminate!();
}
if UNIT_POS_X != 0u32 {
terminate!();
}
let cbase = (tree * n_active_nodes) as usize;
let obase = (tree * (n_active_nodes + 1u32)) as usize;
node_sample_offsets[obase] = 0u32;
let mut run: u32 = 0u32;
let mut n: u32 = 0u32;
while n < n_active_nodes {
let c = Atomic::load(&node_sample_counts[cbase + n as usize]);
Atomic::store(&node_sample_counts[cbase + n as usize], 0u32);
run += c;
node_sample_offsets[obase + (n + 1u32) as usize] = run;
n += 1u32;
}
}
#[cube(launch_unchecked)]
#[allow(clippy::too_many_arguments)]
pub fn scatter_node_samples(
sample_to_node: &Tensor<u32>,
sample_multiplicity: &Tensor<u32>,
node_sample_offsets: &Tensor<u32>,
node_sample_counts: &mut Tensor<Atomic<u32>>,
node_sample_ids: &mut Tensor<u32>,
node_sample_mults: &mut Tensor<u32>,
n_samples: u32,
wave_size: u32,
n_active_nodes: u32,
) {
let s = CUBE_POS_X * WORKGROUP_128 + UNIT_POS_X + CUBE_POS_Y * CUBE_COUNT_X * WORKGROUP_128;
let tree = CUBE_POS_Z;
if s >= n_samples {
terminate!();
}
if tree >= wave_size {
terminate!();
}
let idx = (tree * n_samples + s) as usize;
let node = sample_to_node[idx];
if node < n_active_nodes {
let mult = sample_multiplicity[idx];
if mult > 0u32 {
let slot = Atomic::fetch_add(
&node_sample_counts[(tree * n_active_nodes + node) as usize],
1u32,
);
let pos = node_sample_offsets[(tree * (n_active_nodes + 1u32) + node) as usize] + slot;
let out = (tree * n_samples + pos) as usize;
node_sample_ids[out] = s;
node_sample_mults[out] = mult;
}
}
}
#[allow(clippy::too_many_arguments)]
fn launch_sample_features<R: Runtime>(
client: &ComputeClient<R>,
tree_seeds: &GpuTensor<R, u32>,
node_features: &GpuTensor<R, u32>,
wave_size: usize,
n_active_nodes: usize,
k_feats: usize,
n_features: usize,
level: u32,
) {
unsafe {
sample_node_features::launch_unchecked::<R>(
client,
CubeCount::Static(n_active_nodes as u32, 1, wave_size as u32),
CubeDim::new_1d(WORKGROUP_128),
tree_seeds.clone().into_tensor_arg(),
node_features.clone().into_tensor_arg(),
wave_size as u32,
n_active_nodes as u32,
k_feats as u32,
n_features as u32,
level,
WORKGROUP_128,
);
}
}
#[allow(clippy::too_many_arguments)]
fn launch_gather_node_samples<R: Runtime>(
client: &ComputeClient<R>,
sample_to_node: &GpuTensor<R, u32>,
sample_multiplicity: &GpuTensor<R, u32>,
node_sample_counts: &GpuTensor<R, u32>,
node_sample_offsets: &GpuTensor<R, u32>,
node_sample_ids: &GpuTensor<R, u32>,
node_sample_mults: &GpuTensor<R, u32>,
n_samples: usize,
wave_size: usize,
n_active_nodes: usize,
) {
let n_wgs = (n_samples as u32).div_ceil(WORKGROUP_128);
let (gx, gy) = grid_2d(n_wgs.max(1));
unsafe {
zero_node_sample_counts::launch_unchecked::<R>(
client,
CubeCount::Static(wave_size as u32, 1, 1),
CubeDim::new_1d(WORKGROUP_128),
node_sample_counts.clone().into_tensor_arg(),
wave_size as u32,
n_active_nodes as u32,
WORKGROUP_128,
);
count_node_samples::launch_unchecked::<R>(
client,
CubeCount::Static(gx, gy, wave_size as u32),
CubeDim::new_1d(WORKGROUP_128),
sample_to_node.clone().into_tensor_arg(),
sample_multiplicity.clone().into_tensor_arg(),
node_sample_counts.clone().into_tensor_arg(),
n_samples as u32,
wave_size as u32,
n_active_nodes as u32,
);
scan_node_offsets::launch_unchecked::<R>(
client,
CubeCount::Static(wave_size as u32, 1, 1),
CubeDim::new_1d(1),
node_sample_counts.clone().into_tensor_arg(),
node_sample_offsets.clone().into_tensor_arg(),
wave_size as u32,
n_active_nodes as u32,
);
scatter_node_samples::launch_unchecked::<R>(
client,
CubeCount::Static(gx, gy, wave_size as u32),
CubeDim::new_1d(WORKGROUP_128),
sample_to_node.clone().into_tensor_arg(),
sample_multiplicity.clone().into_tensor_arg(),
node_sample_offsets.clone().into_tensor_arg(),
node_sample_counts.clone().into_tensor_arg(),
node_sample_ids.clone().into_tensor_arg(),
node_sample_mults.clone().into_tensor_arg(),
n_samples as u32,
wave_size as u32,
n_active_nodes as u32,
);
}
}
#[allow(clippy::too_many_arguments)]
fn launch_rf_fused<R: Runtime>(
client: &ComputeClient<R>,
feature_data: &GpuTensor<R, u32>,
y_dense: &GpuTensor<R, f32>,
node_sample_offsets: &GpuTensor<R, u32>,
node_sample_ids: &GpuTensor<R, u32>,
node_sample_mults: &GpuTensor<R, u32>,
node_features: &GpuTensor<R, u32>,
node_counts: &GpuTensor<R, u32>,
node_y_sums: &GpuTensor<R, f32>,
node_y_sum_sqs: &GpuTensor<R, f32>,
slot_best_score: &GpuTensor<R, f32>,
slot_best_thr: &GpuTensor<R, u32>,
slot_best_n_left: &GpuTensor<R, u32>,
slot_best_valid: &GpuTensor<R, u32>,
split_feature: &GpuTensor<R, u32>,
split_threshold: &GpuTensor<R, u32>,
split_n_left: &GpuTensor<R, u32>,
n_samples: usize,
wave_size: usize,
n_active_nodes: usize,
k_feats: usize,
n_targets: usize,
min_samples_leaf: usize,
) {
let n_bins_gpu = pick_gpu_bins(n_targets);
let bin_shift = (N_BINS / n_bins_gpu).trailing_zeros();
debug_assert!(
n_bins_gpu * (n_targets as u32 + 1) <= SMEM_HIST_SLOTS,
"fused RF histogram wants {} slots of {SMEM_HIST_SLOTS}",
n_bins_gpu * (n_targets as u32 + 1)
);
let (gx, gy) = grid_2d((n_active_nodes as u32).max(1));
unsafe {
build_score_rf_fused::launch_unchecked::<R>(
client,
CubeCount::Static(k_feats as u32, n_active_nodes as u32, wave_size as u32),
CubeDim::new_1d(WORKGROUP_128),
feature_data.clone().into_tensor_arg(),
y_dense.clone().into_tensor_arg(),
node_sample_offsets.clone().into_tensor_arg(),
node_sample_ids.clone().into_tensor_arg(),
node_sample_mults.clone().into_tensor_arg(),
node_features.clone().into_tensor_arg(),
node_counts.clone().into_tensor_arg(),
node_y_sums.clone().into_tensor_arg(),
node_y_sum_sqs.clone().into_tensor_arg(),
slot_best_score.clone().into_tensor_arg(),
slot_best_thr.clone().into_tensor_arg(),
slot_best_n_left.clone().into_tensor_arg(),
slot_best_valid.clone().into_tensor_arg(),
n_samples as u32,
wave_size as u32,
n_active_nodes as u32,
k_feats as u32,
n_targets as u32,
n_bins_gpu,
bin_shift,
min_samples_leaf as u32,
WORKGROUP_128,
);
reduce_slot_winners::launch_unchecked::<R>(
client,
CubeCount::Static(gx, gy, wave_size as u32),
CubeDim::new_1d(1),
slot_best_score.clone().into_tensor_arg(),
slot_best_thr.clone().into_tensor_arg(),
slot_best_n_left.clone().into_tensor_arg(),
slot_best_valid.clone().into_tensor_arg(),
node_features.clone().into_tensor_arg(),
split_feature.clone().into_tensor_arg(),
split_threshold.clone().into_tensor_arg(),
split_n_left.clone().into_tensor_arg(),
wave_size as u32,
n_active_nodes as u32,
k_feats as u32,
);
}
}
#[allow(clippy::too_many_arguments)]
fn launch_finalise_split_stats_rf<R: Runtime>(
client: &ComputeClient<R>,
feature_data: &GpuTensor<R, u32>,
y_dense: &GpuTensor<R, f32>,
node_sample_offsets: &GpuTensor<R, u32>,
node_sample_ids: &GpuTensor<R, u32>,
node_sample_mults: &GpuTensor<R, u32>,
split_feature: &GpuTensor<R, u32>,
split_threshold: &GpuTensor<R, u32>,
split_y_sums_l: &GpuTensor<R, f32>,
split_y_sum_sqs_l: &GpuTensor<R, f32>,
n_samples: usize,
wave_size: usize,
n_active_nodes: usize,
n_targets: usize,
) {
let (gx, gy) = grid_2d((n_active_nodes as u32).max(1));
unsafe {
finalise_split_stats_rf::launch_unchecked::<R>(
client,
CubeCount::Static(gx, gy, wave_size as u32),
CubeDim::new_1d(WORKGROUP_128),
feature_data.clone().into_tensor_arg(),
y_dense.clone().into_tensor_arg(),
node_sample_offsets.clone().into_tensor_arg(),
node_sample_ids.clone().into_tensor_arg(),
node_sample_mults.clone().into_tensor_arg(),
split_feature.clone().into_tensor_arg(),
split_threshold.clone().into_tensor_arg(),
split_y_sums_l.clone().into_tensor_arg(),
split_y_sum_sqs_l.clone().into_tensor_arg(),
n_samples as u32,
wave_size as u32,
n_active_nodes as u32,
n_targets as u32,
);
}
}
#[allow(clippy::too_many_arguments)]
fn launch_build_hist<R: Runtime>(
client: &ComputeClient<R>,
feature_data: &GpuTensor<R, u32>,
sy_offsets: &GpuTensor<R, u32>,
sy_target_indices: &GpuTensor<R, u32>,
sy_values: &GpuTensor<R, f32>,
node_sample_offsets: &GpuTensor<R, u32>,
node_sample_ids: &GpuTensor<R, u32>,
node_sample_mults: &GpuTensor<R, u32>,
node_features: &GpuTensor<R, u32>,
hist_counts: &GpuTensor<R, u32>,
hist_y_sums: &GpuTensor<R, u32>,
hist_y_sum_sqs: &GpuTensor<R, u32>,
n_samples: usize,
wave_size: usize,
n_active_nodes: usize,
k_feats: usize,
n_targets: usize,
) {
unsafe {
build_hist_privatised::launch_unchecked::<R>(
client,
CubeCount::Static(k_feats as u32, n_active_nodes as u32, wave_size as u32),
CubeDim::new_1d(WORKGROUP_128),
feature_data.clone().into_tensor_arg(),
sy_offsets.clone().into_tensor_arg(),
sy_target_indices.clone().into_tensor_arg(),
sy_values.clone().into_tensor_arg(),
node_sample_offsets.clone().into_tensor_arg(),
node_sample_ids.clone().into_tensor_arg(),
node_sample_mults.clone().into_tensor_arg(),
node_features.clone().into_tensor_arg(),
hist_counts.clone().into_tensor_arg(),
hist_y_sums.clone().into_tensor_arg(),
hist_y_sum_sqs.clone().into_tensor_arg(),
n_samples as u32,
wave_size as u32,
n_active_nodes as u32,
k_feats as u32,
n_targets as u32,
WORKGROUP_128,
);
}
}
#[allow(clippy::too_many_arguments)]
fn launch_merge_hist<R: Runtime>(
client: &ComputeClient<R>,
hist_counts: &GpuTensor<R, u32>,
hist_y_sums: &GpuTensor<R, u32>,
hist_y_sum_sqs: &GpuTensor<R, u32>,
node_counts: &GpuTensor<R, u32>,
node_y_sums: &GpuTensor<R, f32>,
node_y_sum_sqs: &GpuTensor<R, f32>,
wave_size: usize,
n_active_nodes: usize,
k_feats: usize,
n_targets: usize,
) {
let (gx, gy) = grid_2d((n_active_nodes as u32).max(1));
unsafe {
merge_hist::launch_unchecked::<R>(
client,
CubeCount::Static(gx, gy, wave_size as u32),
CubeDim::new_1d(WORKGROUP_128),
hist_counts.clone().into_tensor_arg(),
hist_y_sums.clone().into_tensor_arg(),
hist_y_sum_sqs.clone().into_tensor_arg(),
node_counts.clone().into_tensor_arg(),
node_y_sums.clone().into_tensor_arg(),
node_y_sum_sqs.clone().into_tensor_arg(),
wave_size as u32,
n_active_nodes as u32,
k_feats as u32,
n_targets as u32,
WORKGROUP_128,
);
}
}
#[allow(clippy::too_many_arguments)]
fn launch_prefix_sum<R: Runtime>(
client: &ComputeClient<R>,
hist_counts: &GpuTensor<R, u32>,
hist_y_sums: &GpuTensor<R, u32>,
hist_y_sum_sqs: &GpuTensor<R, u32>,
cum_counts: &GpuTensor<R, u32>,
cum_y_sums: &GpuTensor<R, f32>,
cum_y_sum_sqs: &GpuTensor<R, f32>,
slot_min_bin: &GpuTensor<R, u32>,
slot_max_bin: &GpuTensor<R, u32>,
wave_size: usize,
n_active_nodes: usize,
k_feats: usize,
n_targets: usize,
) {
unsafe {
prefix_sum_bins::launch_unchecked::<R>(
client,
CubeCount::Static(k_feats as u32, n_active_nodes as u32, wave_size as u32),
CubeDim::new_1d(WORKGROUP_128),
hist_counts.clone().into_tensor_arg(),
hist_y_sums.clone().into_tensor_arg(),
hist_y_sum_sqs.clone().into_tensor_arg(),
cum_counts.clone().into_tensor_arg(),
cum_y_sums.clone().into_tensor_arg(),
cum_y_sum_sqs.clone().into_tensor_arg(),
slot_min_bin.clone().into_tensor_arg(),
slot_max_bin.clone().into_tensor_arg(),
wave_size as u32,
n_active_nodes as u32,
k_feats as u32,
n_targets as u32,
WORKGROUP_128,
);
}
}
fn plane_compact_viable<R: Runtime>(client: &ComputeClient<R>, wg_size: u32) -> bool {
let hw = &client.properties().hardware;
let plane = hw.plane_size_min;
plane == hw.plane_size_max
&& plane > 0
&& wg_size.is_multiple_of(plane)
&& wg_size / plane <= MAX_PLANES_PER_CUBE
}
fn plane_argmax_viable<R: Runtime>(client: &ComputeClient<R>, wg_size: u32) -> bool {
let hw = &client.properties().hardware;
hw.plane_size_min == wg_size && hw.plane_size_max == wg_size
}
#[allow(clippy::too_many_arguments)]
fn launch_evaluate_splits_rf<R: Runtime>(
client: &ComputeClient<R>,
cum_counts: &GpuTensor<R, u32>,
cum_y_sums: &GpuTensor<R, f32>,
cum_y_sum_sqs: &GpuTensor<R, f32>,
node_counts: &GpuTensor<R, u32>,
node_y_sums: &GpuTensor<R, f32>,
node_y_sum_sqs: &GpuTensor<R, f32>,
node_features: &GpuTensor<R, u32>,
slot_min_bin: &GpuTensor<R, u32>,
slot_max_bin: &GpuTensor<R, u32>,
split_feature: &GpuTensor<R, u32>,
split_threshold: &GpuTensor<R, u32>,
split_n_left: &GpuTensor<R, u32>,
wave_size: usize,
n_active_nodes: usize,
k_feats: usize,
n_targets: usize,
min_samples_leaf: usize,
) {
let (gx, gy) = grid_2d((n_active_nodes as u32).max(1));
unsafe {
evaluate_splits_rf::launch_unchecked::<R>(
client,
CubeCount::Static(gx, gy, wave_size as u32),
CubeDim::new_1d(WORKGROUP_128),
cum_counts.clone().into_tensor_arg(),
cum_y_sums.clone().into_tensor_arg(),
cum_y_sum_sqs.clone().into_tensor_arg(),
node_counts.clone().into_tensor_arg(),
node_y_sums.clone().into_tensor_arg(),
node_y_sum_sqs.clone().into_tensor_arg(),
node_features.clone().into_tensor_arg(),
slot_min_bin.clone().into_tensor_arg(),
slot_max_bin.clone().into_tensor_arg(),
split_feature.clone().into_tensor_arg(),
split_threshold.clone().into_tensor_arg(),
split_n_left.clone().into_tensor_arg(),
wave_size as u32,
n_active_nodes as u32,
k_feats as u32,
n_targets as u32,
min_samples_leaf as u32,
WORKGROUP_128,
);
}
}
#[allow(clippy::too_many_arguments)]
fn launch_reassign<R: Runtime>(
client: &ComputeClient<R>,
feature_data: &GpuTensor<R, u32>,
split_feature: &GpuTensor<R, u32>,
split_threshold: &GpuTensor<R, u32>,
left_child_id: &GpuTensor<R, u32>,
right_child_id: &GpuTensor<R, u32>,
sample_to_node: &GpuTensor<R, u32>,
n_samples: usize,
n_features: usize,
wave_size: usize,
n_active_nodes: usize,
) {
let n_wgs = (n_samples as u32).div_ceil(WORKGROUP_128);
let (gx, gy) = grid_2d(n_wgs.max(1));
unsafe {
reassign_samples::launch_unchecked::<R>(
client,
CubeCount::Static(gx, gy, wave_size as u32),
CubeDim::new_1d(WORKGROUP_128),
feature_data.clone().into_tensor_arg(),
split_feature.clone().into_tensor_arg(),
split_threshold.clone().into_tensor_arg(),
left_child_id.clone().into_tensor_arg(),
right_child_id.clone().into_tensor_arg(),
sample_to_node.clone().into_tensor_arg(),
n_samples as u32,
n_features as u32,
wave_size as u32,
n_active_nodes as u32,
);
}
}
#[allow(clippy::too_many_arguments)]
fn launch_accumulate_importance<R: Runtime>(
client: &ComputeClient<R>,
node_counts: &GpuTensor<R, u32>,
node_y_sums: &GpuTensor<R, f32>,
node_y_sum_sqs: &GpuTensor<R, f32>,
split_feature: &GpuTensor<R, u32>,
split_n_left: &GpuTensor<R, u32>,
split_y_sums_l: &GpuTensor<R, f32>,
split_y_sum_sqs_l: &GpuTensor<R, f32>,
batch_importances: &GpuTensor<R, u32>,
wave_size: usize,
n_active_nodes: usize,
n_targets: usize,
n_total: usize,
) {
let (gx, gy) = grid_2d((n_active_nodes as u32).max(1));
unsafe {
accumulate_importance::launch_unchecked::<R>(
client,
CubeCount::Static(gx, gy, wave_size as u32),
CubeDim::new_1d(WORKGROUP_128),
node_counts.clone().into_tensor_arg(),
node_y_sums.clone().into_tensor_arg(),
node_y_sum_sqs.clone().into_tensor_arg(),
split_feature.clone().into_tensor_arg(),
split_n_left.clone().into_tensor_arg(),
split_y_sums_l.clone().into_tensor_arg(),
split_y_sum_sqs_l.clone().into_tensor_arg(),
batch_importances.clone().into_tensor_arg(),
wave_size as u32,
n_active_nodes as u32,
n_targets as u32,
n_total as u32,
WORKGROUP_128,
);
}
}
#[allow(clippy::too_many_arguments)]
fn launch_compute_child_ids<R: Runtime>(
client: &ComputeClient<R>,
split_feature: &GpuTensor<R, u32>,
node_counts: &GpuTensor<R, u32>,
left_child_id: &GpuTensor<R, u32>,
right_child_id: &GpuTensor<R, u32>,
wave_size: usize,
n_active_nodes: usize,
) {
unsafe {
compute_child_ids::launch_unchecked::<R>(
client,
CubeCount::Static(wave_size as u32, 1, 1),
CubeDim::new_1d(1),
split_feature.clone().into_tensor_arg(),
node_counts.clone().into_tensor_arg(),
left_child_id.clone().into_tensor_arg(),
right_child_id.clone().into_tensor_arg(),
wave_size as u32,
n_active_nodes as u32,
);
}
}
#[allow(clippy::too_many_arguments)]
fn launch_scan_slot_bin_range<R: Runtime>(
client: &ComputeClient<R>,
feature_data: &GpuTensor<R, u32>,
sample_to_node: &GpuTensor<R, u32>,
sample_multiplicity: &GpuTensor<R, u32>,
node_features: &GpuTensor<R, u32>,
node_counts: &GpuTensor<R, u32>,
slot_min_bin: &GpuTensor<R, u32>,
slot_max_bin: &GpuTensor<R, u32>,
n_samples: usize,
wave_size: usize,
n_active_nodes: usize,
k_feats: usize,
) {
unsafe {
scan_slot_bin_range::launch_unchecked::<R>(
client,
CubeCount::Static(k_feats as u32, n_active_nodes as u32, wave_size as u32),
CubeDim::new_1d(WORKGROUP_128),
feature_data.clone().into_tensor_arg(),
sample_to_node.clone().into_tensor_arg(),
sample_multiplicity.clone().into_tensor_arg(),
node_features.clone().into_tensor_arg(),
node_counts.clone().into_tensor_arg(),
slot_min_bin.clone().into_tensor_arg(),
slot_max_bin.clone().into_tensor_arg(),
n_samples as u32,
wave_size as u32,
n_active_nodes as u32,
k_feats as u32,
WORKGROUP_128,
);
}
}
#[allow(clippy::too_many_arguments)]
fn launch_accumulate_split_stats_et<R: Runtime>(
client: &ComputeClient<R>,
feature_data: &GpuTensor<R, u32>,
y_dense: &GpuTensor<R, f32>,
sample_to_node: &GpuTensor<R, u32>,
sample_multiplicity: &GpuTensor<R, u32>,
node_features: &GpuTensor<R, u32>,
node_counts: &GpuTensor<R, u32>,
tree_seeds: &GpuTensor<R, u32>,
slot_min_bin: &GpuTensor<R, u32>,
slot_max_bin: &GpuTensor<R, u32>,
cand_thr: &GpuTensor<R, u32>,
cand_n_left: &GpuTensor<R, u32>,
cand_y_sums_l: &GpuTensor<R, f32>,
cand_y_sum_sqs_l: &GpuTensor<R, f32>,
n_samples: usize,
wave_size: usize,
n_active_nodes: usize,
k_feats: usize,
n_targets: usize,
n_thresholds: usize,
level: u32,
) {
unsafe {
accumulate_split_stats_et::launch_unchecked::<R>(
client,
CubeCount::Static(
(k_feats * n_thresholds) as u32,
n_active_nodes as u32,
wave_size as u32,
),
CubeDim::new_1d(WORKGROUP_128),
feature_data.clone().into_tensor_arg(),
y_dense.clone().into_tensor_arg(),
sample_to_node.clone().into_tensor_arg(),
sample_multiplicity.clone().into_tensor_arg(),
node_features.clone().into_tensor_arg(),
node_counts.clone().into_tensor_arg(),
tree_seeds.clone().into_tensor_arg(),
slot_min_bin.clone().into_tensor_arg(),
slot_max_bin.clone().into_tensor_arg(),
cand_thr.clone().into_tensor_arg(),
cand_n_left.clone().into_tensor_arg(),
cand_y_sums_l.clone().into_tensor_arg(),
cand_y_sum_sqs_l.clone().into_tensor_arg(),
n_samples as u32,
wave_size as u32,
n_active_nodes as u32,
k_feats as u32,
n_targets as u32,
n_thresholds as u32,
level,
plane_compact_viable(client, WORKGROUP_128),
);
}
}
#[allow(clippy::too_many_arguments)]
fn launch_evaluate_splits_et_direct<R: Runtime>(
client: &ComputeClient<R>,
node_counts: &GpuTensor<R, u32>,
node_y_sums: &GpuTensor<R, f32>,
node_y_sum_sqs: &GpuTensor<R, f32>,
node_features: &GpuTensor<R, u32>,
cand_thr: &GpuTensor<R, u32>,
cand_n_left: &GpuTensor<R, u32>,
cand_y_sums_l: &GpuTensor<R, f32>,
cand_y_sum_sqs_l: &GpuTensor<R, f32>,
split_feature: &GpuTensor<R, u32>,
split_threshold: &GpuTensor<R, u32>,
split_n_left: &GpuTensor<R, u32>,
split_y_sums_l: &GpuTensor<R, f32>,
split_y_sum_sqs_l: &GpuTensor<R, f32>,
wave_size: usize,
n_active_nodes: usize,
k_feats: usize,
n_targets: usize,
n_thresholds: usize,
min_samples_leaf: usize,
) {
let (gx, gy) = grid_2d((n_active_nodes as u32).max(1));
unsafe {
evaluate_splits_et_direct::launch_unchecked::<R>(
client,
CubeCount::Static(gx, gy, wave_size as u32),
CubeDim::new_1d(WORKGROUP_32),
node_counts.clone().into_tensor_arg(),
node_y_sums.clone().into_tensor_arg(),
node_y_sum_sqs.clone().into_tensor_arg(),
node_features.clone().into_tensor_arg(),
cand_thr.clone().into_tensor_arg(),
cand_n_left.clone().into_tensor_arg(),
cand_y_sums_l.clone().into_tensor_arg(),
cand_y_sum_sqs_l.clone().into_tensor_arg(),
split_feature.clone().into_tensor_arg(),
split_threshold.clone().into_tensor_arg(),
split_n_left.clone().into_tensor_arg(),
split_y_sums_l.clone().into_tensor_arg(),
split_y_sum_sqs_l.clone().into_tensor_arg(),
wave_size as u32,
n_active_nodes as u32,
k_feats as u32,
n_targets as u32,
n_thresholds as u32,
min_samples_leaf as u32,
WORKGROUP_32,
plane_argmax_viable(client, WORKGROUP_32),
);
}
}
#[allow(clippy::too_many_arguments)]
fn launch_init_root_stats<R: Runtime>(
client: &ComputeClient<R>,
y_dense: &GpuTensor<R, f32>,
sample_multiplicity: &GpuTensor<R, u32>,
node_counts: &GpuTensor<R, u32>,
node_y_sums: &GpuTensor<R, f32>,
node_y_sum_sqs: &GpuTensor<R, f32>,
n_samples: usize,
wave_size: usize,
n_active_nodes: usize,
n_targets: usize,
) {
unsafe {
init_root_stats::launch_unchecked::<R>(
client,
CubeCount::Static(wave_size as u32, 1, 1),
CubeDim::new_1d(WORKGROUP_128),
y_dense.clone().into_tensor_arg(),
sample_multiplicity.clone().into_tensor_arg(),
node_counts.clone().into_tensor_arg(),
node_y_sums.clone().into_tensor_arg(),
node_y_sum_sqs.clone().into_tensor_arg(),
n_samples as u32,
wave_size as u32,
n_active_nodes as u32,
n_targets as u32,
WORKGROUP_128,
);
}
}
fn launch_zero_node_stats<R: Runtime>(
client: &ComputeClient<R>,
node_counts: &GpuTensor<R, u32>,
node_y_sums: &GpuTensor<R, f32>,
node_y_sum_sqs: &GpuTensor<R, f32>,
wave_size: usize,
n_active_nodes: usize,
n_targets: usize,
) {
let (gx, gy) = grid_2d((n_active_nodes as u32).max(1));
unsafe {
zero_node_stats::launch_unchecked::<R>(
client,
CubeCount::Static(gx, gy, wave_size as u32),
CubeDim::new_1d(WORKGROUP_128),
node_counts.clone().into_tensor_arg(),
node_y_sums.clone().into_tensor_arg(),
node_y_sum_sqs.clone().into_tensor_arg(),
wave_size as u32,
n_active_nodes as u32,
n_targets as u32,
WORKGROUP_128,
);
}
}
#[allow(clippy::too_many_arguments)]
fn launch_propagate_child_stats<R: Runtime>(
client: &ComputeClient<R>,
split_feature: &GpuTensor<R, u32>,
split_n_left: &GpuTensor<R, u32>,
split_y_sums_l: &GpuTensor<R, f32>,
split_y_sum_sqs_l: &GpuTensor<R, f32>,
left_child_id: &GpuTensor<R, u32>,
right_child_id: &GpuTensor<R, u32>,
node_counts: &GpuTensor<R, u32>,
node_y_sums: &GpuTensor<R, f32>,
node_y_sum_sqs: &GpuTensor<R, f32>,
next_counts: &GpuTensor<R, u32>,
next_y_sums: &GpuTensor<R, f32>,
next_y_sum_sqs: &GpuTensor<R, f32>,
wave_size: usize,
n_active_nodes: usize,
n_active_next: usize,
n_targets: usize,
) {
let (gx, gy) = grid_2d((n_active_nodes as u32).max(1));
unsafe {
propagate_child_stats::launch_unchecked::<R>(
client,
CubeCount::Static(gx, gy, wave_size as u32),
CubeDim::new_1d(WORKGROUP_128),
split_feature.clone().into_tensor_arg(),
split_n_left.clone().into_tensor_arg(),
split_y_sums_l.clone().into_tensor_arg(),
split_y_sum_sqs_l.clone().into_tensor_arg(),
left_child_id.clone().into_tensor_arg(),
right_child_id.clone().into_tensor_arg(),
node_counts.clone().into_tensor_arg(),
node_y_sums.clone().into_tensor_arg(),
node_y_sum_sqs.clone().into_tensor_arg(),
next_counts.clone().into_tensor_arg(),
next_y_sums.clone().into_tensor_arg(),
next_y_sum_sqs.clone().into_tensor_arg(),
wave_size as u32,
n_active_nodes as u32,
n_active_next as u32,
n_targets as u32,
WORKGROUP_128,
);
}
}
fn launch_init_sample_to_node<R: Runtime>(
client: &ComputeClient<R>,
sample_multiplicity: &GpuTensor<R, u32>,
sample_to_node: &GpuTensor<R, u32>,
n_samples: usize,
wave_size: usize,
) {
let n_wgs = (n_samples as u32).div_ceil(WORKGROUP_128);
let (gx, gy) = grid_2d(n_wgs.max(1));
unsafe {
init_sample_to_node::launch_unchecked::<R>(
client,
CubeCount::Static(gx, gy, wave_size as u32),
CubeDim::new_1d(WORKGROUP_128),
sample_multiplicity.clone().into_tensor_arg(),
sample_to_node.clone().into_tensor_arg(),
n_samples as u32,
wave_size as u32,
);
}
}
struct WaveState<R: Runtime> {
sample_to_node: GpuTensor<R, u32>,
node_features: GpuTensor<R, u32>,
hist_counts: GpuTensor<R, u32>,
hist_y_sums: GpuTensor<R, u32>,
hist_y_sum_sqs: GpuTensor<R, u32>,
cum_counts: GpuTensor<R, u32>,
cum_y_sums: GpuTensor<R, f32>,
cum_y_sum_sqs: GpuTensor<R, f32>,
slot_min_bin: GpuTensor<R, u32>,
slot_max_bin: GpuTensor<R, u32>,
node_sample_counts: GpuTensor<R, u32>,
node_sample_offsets: GpuTensor<R, u32>,
node_sample_ids: GpuTensor<R, u32>,
node_sample_mults: GpuTensor<R, u32>,
slot_best_score: GpuTensor<R, f32>,
slot_best_thr: GpuTensor<R, u32>,
slot_best_n_left: GpuTensor<R, u32>,
slot_best_valid: GpuTensor<R, u32>,
node_counts: GpuTensor<R, u32>,
node_y_sums: GpuTensor<R, f32>,
node_y_sum_sqs: GpuTensor<R, f32>,
node_counts_alt: GpuTensor<R, u32>,
node_y_sums_alt: GpuTensor<R, f32>,
node_y_sum_sqs_alt: GpuTensor<R, f32>,
split_feature: GpuTensor<R, u32>,
split_threshold: GpuTensor<R, u32>,
split_n_left: GpuTensor<R, u32>,
split_y_sums_l: GpuTensor<R, f32>,
split_y_sum_sqs_l: GpuTensor<R, f32>,
cand_thr: GpuTensor<R, u32>,
cand_n_left: GpuTensor<R, u32>,
cand_y_sums_l: GpuTensor<R, f32>,
cand_y_sum_sqs_l: GpuTensor<R, f32>,
left_child_id: GpuTensor<R, u32>,
right_child_id: GpuTensor<R, u32>,
wave_size: usize,
max_active_nodes: usize,
n_samples: usize,
k_feats: usize,
n_targets: usize,
}
impl<R: Runtime> WaveState<R> {
#[allow(clippy::too_many_arguments)]
fn allocate(
client: &ComputeClient<R>,
wave_size: usize,
max_active_nodes: usize,
n_samples: usize,
k_feats: usize,
n_targets: usize,
n_thresholds: usize,
layout: WaveLayout,
) -> Self {
let use_et = layout == WaveLayout::ExtraTrees;
let use_hist = layout == WaveLayout::RandomForestHist;
let hist_counts_len = if use_hist {
wave_size * max_active_nodes * k_feats * N_BINS as usize
} else {
1
};
let hist_sums_len = if use_hist {
hist_counts_len * n_targets
} else {
1
};
let node_stats_len = wave_size * max_active_nodes * n_targets;
let n_cands = k_feats * n_thresholds;
let cand_len = if use_et {
wave_size * max_active_nodes * n_cands
} else {
1
};
let cand_stats_len = if use_et { cand_len * n_targets } else { 1 };
let gather_node_len = if use_et {
1
} else {
wave_size * max_active_nodes
};
let gather_offset_len = if use_et {
1
} else {
wave_size * (max_active_nodes + 1)
};
let gather_id_len = if use_et { 1 } else { wave_size * n_samples };
let slot_best_len = if use_et {
1
} else {
wave_size * max_active_nodes * k_feats
};
Self {
sample_to_node: GpuTensor::empty(vec![wave_size * n_samples], client),
node_features: GpuTensor::empty(vec![wave_size * max_active_nodes * k_feats], client),
hist_counts: GpuTensor::empty(vec![hist_counts_len], client),
hist_y_sums: GpuTensor::empty(vec![hist_sums_len], client),
hist_y_sum_sqs: GpuTensor::empty(vec![hist_sums_len], client),
cum_counts: GpuTensor::empty(vec![hist_counts_len], client),
cum_y_sums: GpuTensor::empty(vec![hist_sums_len], client),
cum_y_sum_sqs: GpuTensor::empty(vec![hist_sums_len], client),
slot_min_bin: GpuTensor::empty(vec![wave_size * max_active_nodes * k_feats], client),
slot_max_bin: GpuTensor::empty(vec![wave_size * max_active_nodes * k_feats], client),
node_sample_counts: GpuTensor::empty(vec![gather_node_len], client),
node_sample_offsets: GpuTensor::empty(vec![gather_offset_len], client),
node_sample_ids: GpuTensor::empty(vec![gather_id_len], client),
node_sample_mults: GpuTensor::empty(vec![gather_id_len], client),
slot_best_score: GpuTensor::empty(vec![slot_best_len], client),
slot_best_thr: GpuTensor::empty(vec![slot_best_len], client),
slot_best_n_left: GpuTensor::empty(vec![slot_best_len], client),
slot_best_valid: GpuTensor::empty(vec![slot_best_len], client),
node_counts: GpuTensor::empty(vec![wave_size * max_active_nodes], client),
node_y_sums: GpuTensor::empty(vec![node_stats_len], client),
node_y_sum_sqs: GpuTensor::empty(vec![node_stats_len], client),
node_counts_alt: GpuTensor::empty(vec![wave_size * max_active_nodes], client),
node_y_sums_alt: GpuTensor::empty(vec![node_stats_len], client),
node_y_sum_sqs_alt: GpuTensor::empty(vec![node_stats_len], client),
split_feature: GpuTensor::empty(vec![wave_size * max_active_nodes], client),
split_threshold: GpuTensor::empty(vec![wave_size * max_active_nodes], client),
split_n_left: GpuTensor::empty(vec![wave_size * max_active_nodes], client),
split_y_sums_l: GpuTensor::empty(vec![node_stats_len], client),
split_y_sum_sqs_l: GpuTensor::empty(vec![node_stats_len], client),
cand_thr: GpuTensor::empty(vec![cand_len], client),
cand_n_left: GpuTensor::empty(vec![cand_len], client),
cand_y_sums_l: GpuTensor::empty(vec![cand_stats_len], client),
cand_y_sum_sqs_l: GpuTensor::empty(vec![cand_stats_len], client),
left_child_id: GpuTensor::empty(vec![wave_size * max_active_nodes], client),
right_child_id: GpuTensor::empty(vec![wave_size * max_active_nodes], client),
wave_size,
max_active_nodes,
n_samples,
k_feats,
n_targets,
}
}
}
pub fn viable_max_active_nodes(
max_depth: usize,
n_samples: usize,
min_samples_leaf: usize,
) -> usize {
let depth_cap = 1usize << max_depth.min(20);
let leaf_cap = if min_samples_leaf == 0 {
depth_cap
} else {
(n_samples / (2 * min_samples_leaf)).max(1)
};
depth_cap.min(leaf_cap).max(1)
}
pub fn wave_byte_cost(
wave_size: usize,
max_active_nodes: usize,
k_feats: usize,
n_targets: usize,
) -> usize {
let counts_slots = wave_size * max_active_nodes * k_feats * N_BINS as usize;
let sums_slots = counts_slots * n_targets;
(counts_slots * 2 * 4) + (sums_slots * 4 * 4)
}
pub fn wave_byte_cost_et(
wave_size: usize,
max_active_nodes: usize,
k_feats: usize,
n_targets: usize,
n_thresholds: usize,
) -> usize {
let cand_slots = wave_size * max_active_nodes * k_feats * n_thresholds;
(cand_slots * 2 * 4) + (cand_slots * n_targets * 2 * 4)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WaveLayout {
ExtraTrees,
RandomForestFused,
RandomForestHist,
}
pub fn wave_byte_cost_rf_fused(
wave_size: usize,
max_active_nodes: usize,
k_feats: usize,
n_targets: usize,
n_samples: usize,
) -> usize {
let slot_slots = wave_size * max_active_nodes * k_feats;
let node_stats = wave_size * max_active_nodes * n_targets;
let sample_slots = wave_size * n_samples;
(slot_slots * 5 * 4) + (node_stats * 6 * 4) + (sample_slots * 3 * 4)
}
#[allow(clippy::too_many_arguments)]
pub fn pick_wave_size(
max_active_nodes: usize,
k_feats: usize,
n_targets: usize,
n_trees_target: usize,
wave_byte_budget: usize,
n_thresholds: usize,
layout: WaveLayout,
n_samples: usize,
max_binding_bytes: usize,
) -> Result<usize, BixverseErrors> {
let cost = |w: usize| match layout {
WaveLayout::ExtraTrees => {
wave_byte_cost_et(w, max_active_nodes, k_feats, n_targets, n_thresholds)
}
WaveLayout::RandomForestFused => {
wave_byte_cost_rf_fused(w, max_active_nodes, k_feats, n_targets, n_samples)
}
WaveLayout::RandomForestHist => wave_byte_cost(w, max_active_nodes, k_feats, n_targets),
};
let largest = |w: usize| {
let slots = match layout {
WaveLayout::ExtraTrees => w * max_active_nodes * k_feats * n_thresholds * n_targets,
WaveLayout::RandomForestFused => (w * n_samples).max(w * max_active_nodes * n_targets),
WaveLayout::RandomForestHist => {
w * max_active_nodes * k_feats * N_BINS as usize * n_targets
}
};
slots * 4
};
let fits = |w: usize| cost(w) <= wave_byte_budget && largest(w) <= max_binding_bytes;
let mut w = DEFAULT_WAVE_SIZE.min(n_trees_target.max(1));
while w > 1 {
if fits(w) {
return Ok(w);
}
w /= 2;
}
if !fits(1) {
return Err(BixverseErrors::InvalidArgument(format!(
"GPU SCENIC: even wave_size=1 does not fit (needs {} MB total vs a \
{} MB budget, largest binding {} MB vs a {} MB device limit; \
nodes={}, k_feats={}, n_targets={}). Reduce max_depth or \
n_features_split.",
cost(1) / (1024 * 1024),
wave_byte_budget / (1024 * 1024),
largest(1) / (1024 * 1024),
max_binding_bytes / (1024 * 1024),
max_active_nodes,
k_feats,
n_targets,
)));
}
Ok(1)
}
struct SparseYGpu<R: Runtime> {
offsets: GpuTensor<R, u32>,
target_indices: GpuTensor<R, u32>,
values: GpuTensor<R, f32>,
}
fn upload_dense_y<R: Runtime>(
sy: &SparseYBatch,
n_samples: usize,
n_targets: usize,
scratch: &mut Vec<f32>,
client: &ComputeClient<R>,
) -> GpuTensor<R, f32> {
scratch.clear();
scratch.resize(n_samples * n_targets, 0.0);
for s in 0..n_samples {
let lo = sy.offsets[s] as usize;
let hi = sy.offsets[s + 1] as usize;
for j in lo..hi {
scratch[s * n_targets + sy.target_indices[j] as usize] = sy.values[j];
}
}
GpuTensor::<R, f32>::from_slice(scratch, vec![n_samples * n_targets], client)
}
impl<R: Runtime> SparseYGpu<R> {
fn upload(sy: &SparseYBatch, n_samples: usize, client: &ComputeClient<R>) -> Self {
let offsets = GpuTensor::<R, u32>::from_slice(&sy.offsets, vec![n_samples + 1], client);
let target_indices_u32: Vec<u32> = sy.target_indices.iter().map(|&i| i as u32).collect();
let nnz = target_indices_u32.len().max(1);
let (target_indices, values) = if target_indices_u32.is_empty() {
(
GpuTensor::<R, u32>::from_slice(&[0u32], vec![1], client),
GpuTensor::<R, f32>::from_slice(&[0.0f32], vec![1], client),
)
} else {
(
GpuTensor::<R, u32>::from_slice(&target_indices_u32, vec![nnz], client),
GpuTensor::<R, f32>::from_slice(&sy.values, vec![nnz], client),
)
};
Self {
offsets,
target_indices,
values,
}
}
}
#[allow(clippy::too_many_arguments)]
fn run_wave_bfs<R: Runtime>(
client: &ComputeClient<R>,
feature_data_gpu: &GpuTensor<R, u32>,
sy_gpu: Option<&SparseYGpu<R>>,
y_dense_gpu: &GpuTensor<R, f32>,
state: &WaveState<R>,
sample_multiplicity_gpu: &GpuTensor<R, u32>,
tree_seeds: &GpuTensor<R, u32>,
config: &dyn TreeRegressorConfig,
batch_importances_gpu: &GpuTensor<R, u32>,
n_features: usize,
max_depth: usize,
min_samples_leaf: usize,
n_thresholds: usize,
n_total: usize,
) -> Result<(), BixverseErrors> {
let wave_size = state.wave_size;
let n_samples = state.n_samples;
let k_feats = state.k_feats;
let n_targets = state.n_targets;
let max_active_nodes = state.max_active_nodes;
let use_et = config.random_threshold();
let use_fused_rf = !use_et && fused_rf_viable(client);
launch_init_sample_to_node(
client,
sample_multiplicity_gpu,
&state.sample_to_node,
n_samples,
wave_size,
);
for depth in 0..max_depth {
let depth_cap = 1usize.checked_shl(depth as u32).unwrap_or(usize::MAX);
let n_active_nodes = depth_cap.min(max_active_nodes).max(1);
let n_active_next = depth_cap.saturating_mul(2).min(max_active_nodes).max(1);
let (cur_counts, cur_y_sums, cur_y_sum_sqs) = if depth % 2 == 0 {
(
&state.node_counts,
&state.node_y_sums,
&state.node_y_sum_sqs,
)
} else {
(
&state.node_counts_alt,
&state.node_y_sums_alt,
&state.node_y_sum_sqs_alt,
)
};
let (nxt_counts, nxt_y_sums, nxt_y_sum_sqs) = if depth % 2 == 0 {
(
&state.node_counts_alt,
&state.node_y_sums_alt,
&state.node_y_sum_sqs_alt,
)
} else {
(
&state.node_counts,
&state.node_y_sums,
&state.node_y_sum_sqs,
)
};
launch_sample_features(
client,
tree_seeds,
&state.node_features,
wave_size,
n_active_nodes,
k_feats,
n_features,
depth as u32,
);
let at_max_depth = depth + 1 >= max_depth;
if use_et {
if depth == 0 {
launch_init_root_stats(
client,
y_dense_gpu,
sample_multiplicity_gpu,
cur_counts,
cur_y_sums,
cur_y_sum_sqs,
n_samples,
wave_size,
n_active_nodes,
n_targets,
);
}
launch_scan_slot_bin_range(
client,
feature_data_gpu,
&state.sample_to_node,
sample_multiplicity_gpu,
&state.node_features,
cur_counts,
&state.slot_min_bin,
&state.slot_max_bin,
n_samples,
wave_size,
n_active_nodes,
k_feats,
);
launch_accumulate_split_stats_et(
client,
feature_data_gpu,
y_dense_gpu,
&state.sample_to_node,
sample_multiplicity_gpu,
&state.node_features,
cur_counts,
tree_seeds,
&state.slot_min_bin,
&state.slot_max_bin,
&state.cand_thr,
&state.cand_n_left,
&state.cand_y_sums_l,
&state.cand_y_sum_sqs_l,
n_samples,
wave_size,
n_active_nodes,
k_feats,
n_targets,
n_thresholds,
depth as u32,
);
launch_evaluate_splits_et_direct(
client,
cur_counts,
cur_y_sums,
cur_y_sum_sqs,
&state.node_features,
&state.cand_thr,
&state.cand_n_left,
&state.cand_y_sums_l,
&state.cand_y_sum_sqs_l,
&state.split_feature,
&state.split_threshold,
&state.split_n_left,
&state.split_y_sums_l,
&state.split_y_sum_sqs_l,
wave_size,
n_active_nodes,
k_feats,
n_targets,
n_thresholds,
min_samples_leaf,
);
} else if use_fused_rf {
if depth == 0 {
launch_init_root_stats(
client,
y_dense_gpu,
sample_multiplicity_gpu,
cur_counts,
cur_y_sums,
cur_y_sum_sqs,
n_samples,
wave_size,
n_active_nodes,
n_targets,
);
}
launch_gather_node_samples(
client,
&state.sample_to_node,
sample_multiplicity_gpu,
&state.node_sample_counts,
&state.node_sample_offsets,
&state.node_sample_ids,
&state.node_sample_mults,
n_samples,
wave_size,
n_active_nodes,
);
launch_rf_fused(
client,
feature_data_gpu,
y_dense_gpu,
&state.node_sample_offsets,
&state.node_sample_ids,
&state.node_sample_mults,
&state.node_features,
cur_counts,
cur_y_sums,
cur_y_sum_sqs,
&state.slot_best_score,
&state.slot_best_thr,
&state.slot_best_n_left,
&state.slot_best_valid,
&state.split_feature,
&state.split_threshold,
&state.split_n_left,
n_samples,
wave_size,
n_active_nodes,
k_feats,
n_targets,
min_samples_leaf,
);
launch_finalise_split_stats_rf(
client,
feature_data_gpu,
y_dense_gpu,
&state.node_sample_offsets,
&state.node_sample_ids,
&state.node_sample_mults,
&state.split_feature,
&state.split_threshold,
&state.split_y_sums_l,
&state.split_y_sum_sqs_l,
n_samples,
wave_size,
n_active_nodes,
n_targets,
);
} else {
let sy = sy_gpu.expect("RandomForest path requires the sparse Y upload");
launch_gather_node_samples(
client,
&state.sample_to_node,
sample_multiplicity_gpu,
&state.node_sample_counts,
&state.node_sample_offsets,
&state.node_sample_ids,
&state.node_sample_mults,
n_samples,
wave_size,
n_active_nodes,
);
launch_build_hist(
client,
feature_data_gpu,
&sy.offsets,
&sy.target_indices,
&sy.values,
&state.node_sample_offsets,
&state.node_sample_ids,
&state.node_sample_mults,
&state.node_features,
&state.hist_counts,
&state.hist_y_sums,
&state.hist_y_sum_sqs,
n_samples,
wave_size,
n_active_nodes,
k_feats,
n_targets,
);
if depth == 0 {
launch_merge_hist(
client,
&state.hist_counts,
&state.hist_y_sums,
&state.hist_y_sum_sqs,
cur_counts,
cur_y_sums,
cur_y_sum_sqs,
wave_size,
n_active_nodes,
k_feats,
n_targets,
);
}
launch_prefix_sum(
client,
&state.hist_counts,
&state.hist_y_sums,
&state.hist_y_sum_sqs,
&state.cum_counts,
&state.cum_y_sums,
&state.cum_y_sum_sqs,
&state.slot_min_bin,
&state.slot_max_bin,
wave_size,
n_active_nodes,
k_feats,
n_targets,
);
launch_evaluate_splits_rf(
client,
&state.cum_counts,
&state.cum_y_sums,
&state.cum_y_sum_sqs,
cur_counts,
cur_y_sums,
cur_y_sum_sqs,
&state.node_features,
&state.slot_min_bin,
&state.slot_max_bin,
&state.split_feature,
&state.split_threshold,
&state.split_n_left,
wave_size,
n_active_nodes,
k_feats,
n_targets,
min_samples_leaf,
);
launch_finalise_split_stats_rf(
client,
feature_data_gpu,
y_dense_gpu,
&state.node_sample_offsets,
&state.node_sample_ids,
&state.node_sample_mults,
&state.split_feature,
&state.split_threshold,
&state.split_y_sums_l,
&state.split_y_sum_sqs_l,
n_samples,
wave_size,
n_active_nodes,
n_targets,
);
}
launch_accumulate_importance(
client,
cur_counts,
cur_y_sums,
cur_y_sum_sqs,
&state.split_feature,
&state.split_n_left,
&state.split_y_sums_l,
&state.split_y_sum_sqs_l,
batch_importances_gpu,
wave_size,
n_active_nodes,
n_targets,
n_total,
);
if at_max_depth {
continue;
}
launch_compute_child_ids(
client,
&state.split_feature,
cur_counts,
&state.left_child_id,
&state.right_child_id,
wave_size,
n_active_nodes,
);
launch_zero_node_stats(
client,
nxt_counts,
nxt_y_sums,
nxt_y_sum_sqs,
wave_size,
n_active_next,
n_targets,
);
launch_propagate_child_stats(
client,
&state.split_feature,
&state.split_n_left,
&state.split_y_sums_l,
&state.split_y_sum_sqs_l,
&state.left_child_id,
&state.right_child_id,
cur_counts,
cur_y_sums,
cur_y_sum_sqs,
nxt_counts,
nxt_y_sums,
nxt_y_sum_sqs,
wave_size,
n_active_nodes,
n_active_next,
n_targets,
);
launch_reassign(
client,
feature_data_gpu,
&state.split_feature,
&state.split_threshold,
&state.left_child_id,
&state.right_child_id,
&state.sample_to_node,
n_samples,
n_features,
wave_size,
n_active_nodes,
);
}
Ok(())
}
fn report_batch_progress(done: usize, total: usize, start: Instant) {
let pct = done * 100 / total;
let prev_pct = (done - 1) * 100 / total;
if pct / 10 > prev_pct / 10 || done == total {
println!(
" Progress: {}% ({}/{} batches dispatched, {:.2?} elapsed)",
pct,
done,
total,
start.elapsed()
);
}
}
#[allow(clippy::too_many_arguments)]
pub fn fit_scenic_batches_gpu<R: Runtime>(
batches: &[&[SparseAxis<u32, f32>]],
batch_seeds: &[usize],
feature_matrix: &QuantisedStore,
n_samples: usize,
config: &dyn TreeRegressorConfig,
device: R::Device,
params: &ScenicGpuParams,
verbose: usize,
) -> Result<Vec<Vec<f32>>, BixverseErrors> {
debug_assert_eq!(
batches.len(),
batch_seeds.len(),
"fit_scenic_batches_gpu: batches.len() must equal batch_seeds.len()"
);
let n_features = feature_matrix.n_features;
let n_trees = config.n_trees();
let n_features_split = resolve_n_features_split(config.n_features_split(), n_features);
let k_feats = n_features_split.min(n_features).max(1);
let max_depth = config.max_depth().unwrap_or(usize::MAX).min(20);
let min_samples_leaf = config.min_samples_leaf().max(1);
let n_thresholds = config.n_thresholds().max(1);
let use_et = config.random_threshold();
let max_active_nodes = viable_max_active_nodes(max_depth, n_samples, min_samples_leaf);
let n_sub = if let Some(frac) = config.subsample_frac() {
((n_samples as f32 * frac).round() as usize).max(2 * min_samples_leaf)
} else if config.subsample_rate() >= 1.0 {
n_samples
} else {
((n_samples as f32 * config.subsample_rate()).round() as usize).max(2 * min_samples_leaf)
};
let subsample_needed = n_sub < n_samples;
let bootstrap = config.bootstrap();
let n_targets_total: usize = batches.iter().map(|b| b.len()).sum();
let mut result: Vec<Vec<f32>> = vec![Vec::new(); n_targets_total];
if batches.is_empty() || n_targets_total == 0 || n_trees == 0 {
return Ok(result);
}
let verbosity = parse_verbosity_level(verbose);
let total_batches = batches.iter().filter(|b| !b.is_empty()).count();
let client = R::client(&device);
let wave_layout = if use_et {
WaveLayout::ExtraTrees
} else if fused_rf_viable(&client) {
WaveLayout::RandomForestFused
} else {
WaveLayout::RandomForestHist
};
let n_bins_total = n_features * n_samples;
let n_words = n_bins_total.div_ceil(4);
let max_binding = client.properties().memory.max_page_size as usize;
if n_words * 4 > max_binding {
return Err(BixverseErrors::InvalidArgument(format!(
"GPU SCENIC: the packed feature tensor needs {} MB but the device \
accepts at most {} MB per binding (n_features={}, n_samples={}). \
Subsample cells or reduce the TF set.",
n_words * 4 / (1024 * 1024),
max_binding / (1024 * 1024),
n_features,
n_samples,
)));
}
let mut packed = vec![0u32; n_words];
for (i, &b) in feature_matrix.data.iter().enumerate() {
packed[i / 4] |= (b as u32) << ((i % 4) * 8);
}
let feature_data_gpu = GpuTensor::<R, u32>::from_slice(&packed, vec![n_words], &client);
drop(packed);
let mut deferred: Vec<(usize, usize, GpuTensor<R, u32>)> = Vec::with_capacity(batches.len());
let mut dense_scratch: Vec<f32> = Vec::new();
let start_dispatch = Instant::now();
let mut batches_done = 0usize;
let mut flat_offset = 0usize;
for (chunk, &batch_seed) in batches.iter().zip(batch_seeds.iter()) {
let batch_n_targets = chunk.len();
if batch_n_targets == 0 {
continue;
}
let sparse_y = SparseYBatch::from_targets(chunk, n_samples)?;
let sy_gpu = if wave_layout == WaveLayout::RandomForestHist {
Some(SparseYGpu::upload(&sparse_y, n_samples, &client))
} else {
None
};
let y_dense_gpu = upload_dense_y(
&sparse_y,
n_samples,
batch_n_targets,
&mut dense_scratch,
&client,
);
let wave_size = pick_wave_size(
max_active_nodes,
k_feats,
batch_n_targets,
n_trees,
params.wave_byte_budget,
n_thresholds,
wave_layout,
n_samples,
client.properties().memory.max_page_size as usize,
)?;
let state = WaveState::allocate(
&client,
wave_size,
max_active_nodes,
n_samples,
k_feats,
batch_n_targets,
n_thresholds,
wave_layout,
);
let batch_importances_gpu = GpuTensor::<R, u32>::from_slice(
&vec![0u32; n_features * batch_n_targets],
vec![n_features * batch_n_targets],
&client,
);
let mut tree_idx = 0usize;
while tree_idx < n_trees {
let this_wave = std::cmp::min(wave_size, n_trees - tree_idx);
let seeds_host: Vec<u32> = (tree_idx..tree_idx + this_wave)
.map(|t| tree_seed(batch_seed, t) as u32)
.collect();
let tree_seeds_gpu =
GpuTensor::<R, u32>::from_slice(&seeds_host, vec![this_wave], &client);
let mut mult_host = vec![0u32; this_wave * n_samples];
if subsample_needed {
for w in 0..this_wave {
let mut rng = SmallRng::seed_from_u64(tree_seed(batch_seed, tree_idx + w));
let row_base = w * n_samples;
if bootstrap {
for _ in 0..n_sub {
let idx = rng.random_range(0..n_samples);
mult_host[row_base + idx] += 1;
}
} else {
let mut buf: Vec<u32> = vec![0u32; n_samples];
init_and_split(&mut buf, n_samples, n_sub, &mut rng);
for i in 0..n_sub {
let idx = buf[i] as usize;
mult_host[row_base + idx] = 1;
}
}
}
} else {
mult_host.fill(1);
}
let mult_gpu =
GpuTensor::<R, u32>::from_slice(&mult_host, vec![this_wave * n_samples], &client);
let effective_state = if this_wave == wave_size {
None
} else {
Some(WaveState::allocate(
&client,
this_wave,
max_active_nodes,
n_samples,
k_feats,
batch_n_targets,
n_thresholds,
wave_layout,
))
};
let use_state = effective_state.as_ref().unwrap_or(&state);
run_wave_bfs(
&client,
&feature_data_gpu,
sy_gpu.as_ref(),
&y_dense_gpu,
use_state,
&mult_gpu,
&tree_seeds_gpu,
config,
&batch_importances_gpu,
n_features,
max_depth,
min_samples_leaf,
n_thresholds,
n_sub,
)?;
tree_idx += this_wave;
}
deferred.push((flat_offset, batch_n_targets, batch_importances_gpu));
flat_offset += batch_n_targets;
batches_done += 1;
if verbosity.normal_verbosity() {
report_batch_progress(batches_done, total_batches, start_dispatch);
}
}
if verbosity.detailed_verbosity() {
println!(
" All {} batches dispatched in {:.2?}, draining the queue",
total_batches,
start_dispatch.elapsed()
);
}
for (target_offset, batch_n_targets, imp_gpu) in deferred {
let bits = imp_gpu.read(&client)?;
let batch_imp: Vec<f32> = bits.iter().map(|&b| f32::from_bits(b)).collect();
for k in 0..batch_n_targets {
let mut per_target = vec![0.0f32; n_features];
for f in 0..n_features {
per_target[f] = batch_imp[f * batch_n_targets + k];
}
normalise_importances(&mut per_target);
result[target_offset + k] = per_target;
}
}
Ok(result)
}
pub fn fit_multi_trees_gpu<R: Runtime>(
targets: &[SparseAxis<u32, f32>],
feature_matrix: &QuantisedStore,
n_samples: usize,
config: &dyn TreeRegressorConfig,
seed: usize,
device: R::Device,
params: &ScenicGpuParams,
) -> Result<Vec<Vec<f32>>, BixverseErrors> {
if targets.is_empty() {
return Ok(Vec::new());
}
let batches: Vec<&[SparseAxis<u32, f32>]> = targets.chunks(MULTI_OUTPUT_BATCH).collect();
let batch_seeds: Vec<usize> = vec![seed; batches.len()];
fit_scenic_batches_gpu::<R>(
&batches,
&batch_seeds,
feature_matrix,
n_samples,
config,
device,
params,
0,
)
}
pub fn fit_extra_trees_gpu_single<R: Runtime>(
sparse_y: &SparseYBatch,
feature_matrix: &QuantisedStore,
n_samples: usize,
config: &ExtraTreesConfig,
seed: usize,
device: R::Device,
params: &ScenicGpuParams,
) -> Result<Vec<Vec<f32>>, BixverseErrors> {
let n_targets = sparse_y_infer_n_targets(sparse_y);
let mut cols_indices: Vec<Vec<usize>> = vec![Vec::new(); n_targets];
let mut cols_values: Vec<Vec<f32>> = vec![Vec::new(); n_targets];
for cell in 0..n_samples {
let s = sparse_y.offsets[cell] as usize;
let e = sparse_y.offsets[cell + 1] as usize;
for j in s..e {
let t = sparse_y.target_indices[j] as usize;
let v = sparse_y.values[j];
cols_indices[t].push(cell);
cols_values[t].push(v);
}
}
let axes: Vec<SparseAxis<u32, f32>> = cols_indices
.into_iter()
.zip(cols_values)
.map(|(idx, vs)| SparseAxis::<u32, f32>::new_csc(idx, Vec::new(), Some(vs), n_samples))
.collect();
let mut cfg = config.clone();
cfg.n_trees = 1;
fit_multi_trees_gpu::<R>(&axes, feature_matrix, n_samples, &cfg, seed, device, params)
}
fn sparse_y_infer_n_targets(sy: &SparseYBatch) -> usize {
let mut max = 0u32;
for &t in &sy.target_indices {
if t as u32 > max {
max = t as u32;
}
}
(max as usize) + 1
}
const BATCH_SEED_STRIDE: usize = 2_654_435_761;
fn learner_name(learner: &RegressionLearner) -> &'static str {
match learner {
RegressionLearner::ExtraTrees(_) => "ExtraTrees",
RegressionLearner::RandomForest(_) => "RandomForest",
RegressionLearner::GradientBoosting(_) => "GradientBoosting",
}
}
fn tree_config_of(params: &ScenicParams) -> &dyn TreeRegressorConfig {
match ¶ms.regression_learner {
RegressionLearner::ExtraTrees(cfg) => cfg,
RegressionLearner::RandomForest(cfg) => cfg,
RegressionLearner::GradientBoosting(_) => {
unreachable!("GBM must be rejected before reaching tree_config_of")
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn run_scenic_grn_gpu<R>(
f_path: &str,
cell_indices: &[usize],
gene_indices: &[usize],
tf_indices: &[usize],
scenic_params: &ScenicParams,
gpu_params: &ScenicGpuParams,
seed: usize,
device: R::Device,
verbose: usize,
) -> Result<Mat<f32>, BixverseErrors>
where
R: Runtime,
{
if matches!(
scenic_params.regression_learner,
RegressionLearner::GradientBoosting(_)
) {
return Err(BixverseErrors::GpuNotSupportedForLearner {
learner: learner_name(&scenic_params.regression_learner),
});
}
let verbosity = parse_verbosity_level(verbose);
let setup = scenic_common_setup(f_path, cell_indices, tf_indices, verbose)?;
let n_genes = gene_indices.len();
let n_multi_output = scenic_params
.gene_batch_size
.unwrap_or(MULTI_OUTPUT_BATCH)
.min(MULTI_OUTPUT_BATCH);
let strategy = parse_gene_batch_strategy(
&scenic_params.gene_batch_strategy,
scenic_params.n_pcs,
scenic_params.n_subsample,
)
.unwrap_or(GeneBatchStrategy::Random);
let batches = batch_genes(
f_path,
gene_indices,
cell_indices,
n_multi_output,
&strategy,
seed,
verbose,
)?;
let ordered_genes: Vec<usize> = batches.iter().flatten().copied().collect();
let gene_id_to_pos: FxHashMap<usize, usize> = gene_indices
.iter()
.enumerate()
.map(|(pos, &gid)| (gid, pos))
.collect();
let start_gene_read = Instant::now();
let mut all_sparse_cols: Vec<SparseAxis<u32, f32>> = Vec::with_capacity(n_genes);
for (iter, chunk) in ordered_genes.chunks(SCENIC_GENE_CHUNK_SIZE).enumerate() {
let mut gene_chunks: Vec<CscGeneChunk> = setup.reader.read_gene_parallel(chunk)?;
gene_chunks.par_iter_mut().for_each(|c| {
c.filter_selected_cells(&setup.cell_set);
});
for gc in gene_chunks.iter() {
all_sparse_cols.push(gc.to_sparse_axis(setup.n_cells));
}
if verbosity.detailed_verbosity() {
println!(
" Read gene chunk {}/{} ({} genes)",
iter + 1,
ordered_genes.len().div_ceil(SCENIC_GENE_CHUNK_SIZE),
all_sparse_cols.len(),
);
}
}
if verbosity.normal_verbosity() {
println!(
"Read and filtered {} target genes in {:.2?}",
n_genes,
start_gene_read.elapsed()
);
}
let mut col_batches: Vec<&[SparseAxis<u32, f32>]> = Vec::with_capacity(batches.len());
let mut offset = 0usize;
for b in &batches {
col_batches.push(&all_sparse_cols[offset..offset + b.len()]);
offset += b.len();
}
let total_batches = col_batches.len();
let learner_name = learner_name(&scenic_params.regression_learner);
let config = tree_config_of(scenic_params);
if verbosity.normal_verbosity() {
println!(
"Running SCENIC GPU ({}) on {} genes ({} TFs, {} cells, {} batches of up to {})",
learner_name, n_genes, setup.n_tfs, setup.n_cells, total_batches, n_multi_output,
);
}
let start_fit = Instant::now();
let batch_seeds: Vec<usize> = (0..total_batches)
.map(|i| seed.wrapping_add(i.wrapping_mul(BATCH_SEED_STRIDE)))
.collect();
let flat_imp = fit_scenic_batches_gpu::<R>(
&col_batches,
&batch_seeds,
&setup.tf_data,
setup.n_cells,
config,
device,
gpu_params,
verbose,
)?;
let flat_gene_ids: Vec<usize> = batches.iter().flatten().copied().collect();
let mut importance_scores: Vec<Vec<f32>> = vec![Vec::new(); n_genes];
for (imp, gene_id) in flat_imp.into_iter().zip(flat_gene_ids.iter()) {
let pos = gene_id_to_pos[gene_id];
importance_scores[pos] = imp;
}
if verbosity.normal_verbosity() {
println!(
"SCENIC GPU ({}) GRN inference complete in {:.2?} (fit {:.2?})",
learner_name,
setup.start_total.elapsed(),
start_fit.elapsed()
);
}
Ok(Mat::from_fn(n_genes, setup.n_tfs, |i, j| {
if j < importance_scores[i].len() {
importance_scores[i][j]
} else {
0.0
}
}))
}
#[allow(clippy::too_many_arguments)]
pub fn run_scenic_grn_streaming_gpu<R>(
f_path: &str,
cell_indices: &[usize],
gene_indices: &[usize],
tf_indices: &[usize],
scenic_params: &ScenicParams,
gpu_params: &ScenicGpuParams,
seed: usize,
device: R::Device,
verbose: usize,
) -> Result<Mat<f32>, BixverseErrors>
where
R: Runtime,
{
if matches!(
scenic_params.regression_learner,
RegressionLearner::GradientBoosting(_)
) {
return Err(BixverseErrors::GpuNotSupportedForLearner {
learner: learner_name(&scenic_params.regression_learner),
});
}
let verbosity = parse_verbosity_level(verbose);
let setup = scenic_common_setup(f_path, cell_indices, tf_indices, verbose)?;
let n_genes = gene_indices.len();
let n_multi_output = scenic_params
.gene_batch_size
.unwrap_or(MULTI_OUTPUT_BATCH)
.min(MULTI_OUTPUT_BATCH);
let strategy = parse_gene_batch_strategy(
&scenic_params.gene_batch_strategy,
scenic_params.n_pcs,
scenic_params.n_subsample,
)
.unwrap_or(GeneBatchStrategy::Random);
let batches = batch_genes(
f_path,
gene_indices,
cell_indices,
n_multi_output,
&strategy,
seed,
verbose,
)?;
let io_groups = group_batches_for_io(&batches, SCENIC_GENE_CHUNK_SIZE);
let gene_id_to_pos: FxHashMap<usize, usize> = gene_indices
.iter()
.enumerate()
.map(|(pos, &gid)| (gid, pos))
.collect();
let learner_name = learner_name(&scenic_params.regression_learner);
let config = tree_config_of(scenic_params);
let total_io_chunks = io_groups.len();
let mut importance_scores: Vec<Vec<f32>> = vec![Vec::new(); n_genes];
let mut global_batch_offset: usize = 0;
let mut genes_processed: usize = 0;
if verbosity.normal_verbosity() {
println!(
"Running SCENIC GPU ({}, streaming) on {} genes ({} TFs, {} cells, {} I/O chunks, batches of {})",
learner_name,
n_genes.separate_with_underscores(),
setup.n_tfs.separate_with_underscores(),
setup.n_cells.separate_with_underscores(),
total_io_chunks,
n_multi_output,
);
}
for (chunk_idx, &(g_start, g_end)) in io_groups.iter().enumerate() {
let start_chunk = Instant::now();
let group = &batches[g_start..g_end];
let io_chunk: Vec<usize> = group.iter().flatten().copied().collect();
let start_io = Instant::now();
let mut gene_chunks: Vec<CscGeneChunk> = setup.reader.read_gene_parallel(&io_chunk)?;
gene_chunks.par_iter_mut().for_each(|c| {
c.filter_selected_cells(&setup.cell_set);
});
let sparse_columns: Vec<SparseAxis<u32, f32>> = gene_chunks
.iter()
.map(|c| c.to_sparse_axis(setup.n_cells))
.collect();
drop(gene_chunks);
if verbosity.normal_verbosity() {
println!(
" Chunk {}/{}: loaded and filtered {} genes in {:.2?}",
chunk_idx + 1,
total_io_chunks,
io_chunk.len(),
start_io.elapsed()
);
}
let mut col_offsets = Vec::with_capacity(group.len() + 1);
let mut off = 0usize;
col_offsets.push(0);
for b in group {
off += b.len();
col_offsets.push(off);
}
let n_batches_this_chunk = group.len();
let start_fit = Instant::now();
let chunk_col_batches: Vec<&[SparseAxis<u32, f32>]> = (0..n_batches_this_chunk)
.map(|i| &sparse_columns[col_offsets[i]..col_offsets[i + 1]])
.collect();
let chunk_batch_seeds: Vec<usize> = (0..n_batches_this_chunk)
.map(|i| seed.wrapping_add((global_batch_offset + i).wrapping_mul(BATCH_SEED_STRIDE)))
.collect();
let flat_imp = fit_scenic_batches_gpu::<R>(
&chunk_col_batches,
&chunk_batch_seeds,
&setup.tf_data,
setup.n_cells,
config,
device.clone(),
gpu_params,
verbose,
)?;
let chunk_flat_gene_ids: Vec<usize> = group.iter().flatten().copied().collect();
for (imp, gene_id) in flat_imp.into_iter().zip(chunk_flat_gene_ids.iter()) {
let pos = gene_id_to_pos[gene_id];
importance_scores[pos] = imp;
}
global_batch_offset += n_batches_this_chunk;
genes_processed += io_chunk.len();
if verbosity.detailed_verbosity() {
println!(
" Chunk {}: fit {} batches in {:.2?}",
chunk_idx + 1,
n_batches_this_chunk,
start_fit.elapsed()
);
}
if verbosity.normal_verbosity() {
println!(
" Chunk {}/{}: {}/{} genes done in {:.2?} (fit: {:.2?})",
chunk_idx + 1,
total_io_chunks,
genes_processed,
n_genes,
start_chunk.elapsed(),
start_fit.elapsed()
);
}
}
if verbosity.normal_verbosity() {
println!(
"SCENIC GPU ({}) GRN inference (streaming) complete in {:.2?}",
learner_name,
setup.start_total.elapsed()
);
}
Ok(Mat::from_fn(n_genes, setup.n_tfs, |i, j| {
if j < importance_scores[i].len() {
importance_scores[i][j]
} else {
0.0
}
}))
}
#[allow(clippy::too_many_arguments)]
pub fn run_scenic_grn_in_memory_gpu<R, T>(
expr_csc: &CompressedSparseData2<T, f32>,
tf_indices: &[usize],
scenic_params: &ScenicParams,
gpu_params: &ScenicGpuParams,
seed: usize,
device: R::Device,
verbose: usize,
) -> Result<Mat<f32>, BixverseErrors>
where
R: Runtime,
T: BixverseNumeric + Copy + Into<u32> + Sync,
{
if matches!(
scenic_params.regression_learner,
RegressionLearner::GradientBoosting(_)
) {
return Err(BixverseErrors::GpuNotSupportedForLearner {
learner: learner_name(&scenic_params.regression_learner),
});
}
let verbosity = parse_verbosity_level(verbose);
let csc_owned;
let csc: &CompressedSparseData2<T, f32> = match expr_csc.cs_type {
CompressedSparseFormat::Csc => expr_csc,
CompressedSparseFormat::Csr => {
csc_owned = expr_csc.transform();
&csc_owned
}
};
let start_total = Instant::now();
let n_cells = csc.shape.0;
let n_genes = csc.shape.1;
let n_tfs = tf_indices.len();
let start_quant = Instant::now();
let tf_data = build_tf_quantised_store(csc, tf_indices, n_cells);
if verbosity.normal_verbosity() {
println!(
"Quantised TF store (n: {}) in: {:.2?}",
n_tfs.separate_with_underscores(),
start_quant.elapsed()
);
}
let n_multi_output = scenic_params
.gene_batch_size
.unwrap_or(MULTI_OUTPUT_BATCH)
.min(MULTI_OUTPUT_BATCH);
let strategy = parse_gene_batch_strategy(
&scenic_params.gene_batch_strategy,
scenic_params.n_pcs,
scenic_params.n_subsample,
)
.unwrap_or(GeneBatchStrategy::Random);
let ordered_genes = batch_genes_in_memory(csc, n_multi_output, &strategy, seed, verbose)?;
let start_extract = Instant::now();
let all_sparse_cols: Vec<SparseAxis<u32, f32>> = ordered_genes
.par_iter()
.map(|&g| extract_target_column(csc, g, n_cells))
.collect();
if verbosity.normal_verbosity() {
println!(
"Extracted {} target columns in {:.2?}",
n_genes,
start_extract.elapsed()
);
}
let id_batches: Vec<&[usize]> = ordered_genes.chunks(n_multi_output).collect();
let col_batches: Vec<&[SparseAxis<u32, f32>]> =
all_sparse_cols.chunks(n_multi_output).collect();
let total_batches = col_batches.len();
let learner_name = learner_name(&scenic_params.regression_learner);
let config = tree_config_of(scenic_params);
if verbosity.normal_verbosity() {
println!(
"Running SCENIC GPU ({}, in-memory) on {} genes ({} TFs, {} cells, {} batches of up to {})",
learner_name, n_genes, n_tfs, n_cells, total_batches, n_multi_output,
);
}
let start_fit = Instant::now();
let batch_seeds: Vec<usize> = (0..total_batches)
.map(|i| seed.wrapping_add(i.wrapping_mul(BATCH_SEED_STRIDE)))
.collect();
let flat_imp = fit_scenic_batches_gpu::<R>(
&col_batches,
&batch_seeds,
&tf_data,
n_cells,
config,
device,
gpu_params,
verbose,
)?;
let flat_gene_ids: Vec<usize> = id_batches.iter().flat_map(|b| b.iter().copied()).collect();
let mut importance_scores: Vec<Vec<f32>> = vec![Vec::new(); n_genes];
for (imp, &gene_id) in flat_imp.into_iter().zip(flat_gene_ids.iter()) {
importance_scores[gene_id] = imp;
}
if verbosity.normal_verbosity() {
println!(
"SCENIC GPU ({}, in-memory) GRN inference complete in {:.2?} (fit {:.2?})",
learner_name,
start_total.elapsed(),
start_fit.elapsed()
);
}
Ok(Mat::from_fn(n_genes, n_tfs, |i, j| {
if j < importance_scores[i].len() {
importance_scores[i][j]
} else {
0.0
}
}))
}