use crate::Matrix;
use crate::bin::Bin;
use crate::data::{FloatData, JaggedMatrix};
use rayon::{ThreadPool, prelude::*};
use std::cell::UnsafeCell;
#[derive(Debug)]
pub struct FeatureHistogramOwned {
pub data: Vec<Bin>,
}
impl FeatureHistogramOwned {
pub fn empty_from_cuts(cuts: &[f64], is_const_hess: bool) -> Self {
let mut histogram: Vec<Bin> = Vec::with_capacity(cuts.len());
if is_const_hess {
histogram.push(Bin::empty_const_hess(0, f64::NAN));
histogram.extend(
cuts[..(cuts.len() - 1)]
.iter()
.enumerate()
.map(|(it, c)| Bin::empty_const_hess(it as u16 + 1, *c)),
);
} else {
histogram.push(Bin::empty(0, f64::NAN));
histogram.extend(
cuts[..(cuts.len() - 1)]
.iter()
.enumerate()
.map(|(it, c)| Bin::empty(it as u16 + 1, *c)),
);
}
FeatureHistogramOwned { data: histogram }
}
pub fn empty(max_bin: u16, is_const_hess: bool) -> Self {
let mut histogram: Vec<Bin> = Vec::with_capacity(max_bin.into());
if is_const_hess {
histogram.push(Bin::empty_const_hess(0, f64::NAN));
histogram.extend((0..(max_bin + 1)).map(|i| Bin::empty_const_hess(i + 1, f64::NAN)));
} else {
histogram.push(Bin::empty(0, f64::NAN));
histogram.extend((0..(max_bin + 1)).map(|i| Bin::empty(i + 1, f64::NAN)));
}
FeatureHistogramOwned { data: histogram }
}
}
#[derive(Copy, Clone, Debug)]
pub struct FeatureHistogram<'a> {
pub data: &'a [UnsafeCell<Bin>],
}
unsafe impl<'a> Send for FeatureHistogram<'a> {}
unsafe impl<'a> Sync for FeatureHistogram<'a> {}
impl<'a> FeatureHistogram<'a> {
pub fn new(hist: &'a mut [Bin]) -> Self {
let ptr = hist as *mut [Bin] as *const [UnsafeCell<Bin>];
Self { data: unsafe { &*ptr } }
}
pub unsafe fn update(&self, feature: &[u16], sorted_grad: &[f32], sorted_hess: Option<&[f32]>, index: &[usize]) {
unsafe {
let n_bins = self.data.len();
let n = index.len();
const MAX_FLAT_BINS: usize = 300;
if n < 64 || n_bins > MAX_FLAT_BINS {
match sorted_hess {
Some(sorted_hess) => {
self.data.iter().for_each(|b| {
let bin = b.get().as_mut().unwrap_unchecked();
bin.g_folded = [f32::ZERO; 5];
bin.h_folded = [f32::ZERO; 5];
bin.counts = [0; 5];
});
for k in 0..n {
let i = *index.get_unchecked(k);
let b = self.data.get_unchecked(*feature.get_unchecked(i) as usize).get();
let bin = b.as_mut().unwrap_unchecked();
let fold = i % 5;
*bin.g_folded.get_unchecked_mut(fold) += *sorted_grad.get_unchecked(i);
*bin.h_folded.get_unchecked_mut(fold) += *sorted_hess.get_unchecked(i);
*bin.counts.get_unchecked_mut(fold) += 1;
}
}
None => {
self.data.iter().for_each(|b| {
let bin = b.get().as_mut().unwrap_unchecked();
bin.g_folded = [f32::ZERO; 5];
bin.counts = [0; 5];
bin.h_folded = [f32::ZERO; 5];
});
for k in 0..n {
let i = *index.get_unchecked(k);
let b = self.data.get_unchecked(*feature.get_unchecked(i) as usize).get();
let bin = b.as_mut().unwrap_unchecked();
let fold = i % 5;
*bin.g_folded.get_unchecked_mut(fold) += *sorted_grad.get_unchecked(i);
*bin.counts.get_unchecked_mut(fold) += 1;
}
}
}
return;
}
const MAX_FLAT: usize = 300 * 5; debug_assert!(n_bins * 5 <= MAX_FLAT, "n_bins {} exceeds flat buffer capacity", n_bins);
let flat_len = n_bins * 5;
let mut flat_grad_storage: std::mem::MaybeUninit<[f32; MAX_FLAT]> = std::mem::MaybeUninit::uninit();
let mut flat_counts_storage: std::mem::MaybeUninit<[u32; MAX_FLAT]> = std::mem::MaybeUninit::uninit();
let gp = flat_grad_storage.as_mut_ptr() as *mut f32;
let cp = flat_counts_storage.as_mut_ptr() as *mut u32;
core::ptr::write_bytes(gp, 0, flat_len);
core::ptr::write_bytes(cp, 0, flat_len);
let flat_grad = core::slice::from_raw_parts_mut(gp, flat_len);
let flat_counts = core::slice::from_raw_parts_mut(cp, flat_len);
match sorted_hess {
Some(sorted_hess) => {
let mut flat_hess_storage: std::mem::MaybeUninit<[f32; MAX_FLAT]> = std::mem::MaybeUninit::uninit();
let hp = flat_hess_storage.as_mut_ptr() as *mut f32;
core::ptr::write_bytes(hp, 0, flat_len);
let flat_hess = core::slice::from_raw_parts_mut(hp, flat_len);
#[cfg(target_arch = "x86_64")]
{
use core::arch::x86_64::{_MM_HINT_T0, _MM_HINT_T1, _mm_prefetch};
const PF_FAR: usize = 16;
const PF_NEAR: usize = 4;
let n = index.len();
for k in 0..n {
if k + PF_FAR < n {
let far = *index.get_unchecked(k + PF_FAR);
_mm_prefetch(feature.as_ptr().add(far) as *const i8, _MM_HINT_T1);
_mm_prefetch(sorted_grad.as_ptr().add(far) as *const i8, _MM_HINT_T1);
_mm_prefetch(sorted_hess.as_ptr().add(far) as *const i8, _MM_HINT_T1);
}
if k + PF_NEAR < n {
let near = *index.get_unchecked(k + PF_NEAR);
_mm_prefetch(feature.as_ptr().add(near) as *const i8, _MM_HINT_T0);
_mm_prefetch(sorted_grad.as_ptr().add(near) as *const i8, _MM_HINT_T0);
_mm_prefetch(sorted_hess.as_ptr().add(near) as *const i8, _MM_HINT_T0);
}
let i = *index.get_unchecked(k);
let bin_idx = *feature.get_unchecked(i) as usize;
let slot = bin_idx * 5 + (i % 5);
*flat_grad.get_unchecked_mut(slot) += *sorted_grad.get_unchecked(i);
*flat_hess.get_unchecked_mut(slot) += *sorted_hess.get_unchecked(i);
*flat_counts.get_unchecked_mut(slot) += 1;
}
}
#[cfg(not(target_arch = "x86_64"))]
{
for k in 0..index.len() {
let i = *index.get_unchecked(k);
let bin_idx = *feature.get_unchecked(i) as usize;
let slot = bin_idx * 5 + (i % 5);
*flat_grad.get_unchecked_mut(slot) += *sorted_grad.get_unchecked(i);
*flat_hess.get_unchecked_mut(slot) += *sorted_hess.get_unchecked(i);
*flat_counts.get_unchecked_mut(slot) += 1;
}
}
for b_idx in 0..n_bins {
let bin = self.data.get_unchecked(b_idx).get().as_mut().unwrap_unchecked();
let base = b_idx * 5;
bin.g_folded.copy_from_slice(flat_grad.get_unchecked(base..base + 5));
bin.h_folded.copy_from_slice(flat_hess.get_unchecked(base..base + 5));
bin.counts.copy_from_slice(flat_counts.get_unchecked(base..base + 5));
}
}
None => {
#[cfg(target_arch = "x86_64")]
{
use core::arch::x86_64::{_MM_HINT_T0, _MM_HINT_T1, _mm_prefetch};
const PF_FAR: usize = 16;
const PF_NEAR: usize = 4;
let n = index.len();
for k in 0..n {
if k + PF_FAR < n {
let far = *index.get_unchecked(k + PF_FAR);
_mm_prefetch(feature.as_ptr().add(far) as *const i8, _MM_HINT_T1);
_mm_prefetch(sorted_grad.as_ptr().add(far) as *const i8, _MM_HINT_T1);
}
if k + PF_NEAR < n {
let near = *index.get_unchecked(k + PF_NEAR);
_mm_prefetch(feature.as_ptr().add(near) as *const i8, _MM_HINT_T0);
_mm_prefetch(sorted_grad.as_ptr().add(near) as *const i8, _MM_HINT_T0);
}
let i = *index.get_unchecked(k);
let bin_idx = *feature.get_unchecked(i) as usize;
let slot = bin_idx * 5 + (i % 5);
*flat_grad.get_unchecked_mut(slot) += *sorted_grad.get_unchecked(i);
*flat_counts.get_unchecked_mut(slot) += 1;
}
}
#[cfg(not(target_arch = "x86_64"))]
{
for k in 0..index.len() {
let i = *index.get_unchecked(k);
let bin_idx = *feature.get_unchecked(i) as usize;
let slot = bin_idx * 5 + (i % 5);
*flat_grad.get_unchecked_mut(slot) += *sorted_grad.get_unchecked(i);
*flat_counts.get_unchecked_mut(slot) += 1;
}
}
for b_idx in 0..n_bins {
let bin = self.data.get_unchecked(b_idx).get().as_mut().unwrap_unchecked();
let base = b_idx * 5;
bin.g_folded.copy_from_slice(flat_grad.get_unchecked(base..base + 5));
bin.counts.copy_from_slice(flat_counts.get_unchecked(base..base + 5));
bin.h_folded = [f32::ZERO; 5];
}
}
}
}
}
pub unsafe fn update_cuts(&self, cuts: &[f64]) {
unsafe {
let cuts_mod = &cuts[..(cuts.len() - 1)];
self.data.iter().enumerate().for_each(|(i, b)| {
let bin = b.get().as_mut().unwrap();
if i == 0 {
bin.cut_value = f64::NAN;
} else {
bin.cut_value = *cuts_mod.get(i - 1).unwrap_or(&f64::NAN);
}
});
}
}
}
#[derive(Debug)]
pub struct NodeHistogramOwned {
pub data: Vec<FeatureHistogramOwned>,
}
impl NodeHistogramOwned {
pub fn empty_from_cuts(cuts: &JaggedMatrix<f64>, col_index: &[usize], is_const_hess: bool, parallel: bool) -> Self {
let histograms: Vec<FeatureHistogramOwned> = if parallel {
col_index
.par_iter()
.map(|col| FeatureHistogramOwned::empty_from_cuts(cuts.get_col(*col), is_const_hess))
.collect()
} else {
col_index
.iter()
.map(|col| FeatureHistogramOwned::empty_from_cuts(cuts.get_col(*col), is_const_hess))
.collect()
};
NodeHistogramOwned { data: histograms }
}
pub fn empty(max_bin: u16, col_amount: usize, is_const_hess: bool, parallel: bool) -> Self {
let histograms: Vec<FeatureHistogramOwned> = if parallel {
(0..col_amount)
.collect::<Vec<_>>()
.par_iter()
.map(|_col| FeatureHistogramOwned::empty(max_bin, is_const_hess))
.collect()
} else {
(0..col_amount)
.map(|_col| FeatureHistogramOwned::empty(max_bin, is_const_hess))
.collect()
};
NodeHistogramOwned { data: histograms }
}
}
pub struct HistogramArena {
bins: Vec<Bin>,
col_bin_counts: Vec<usize>,
n_nodes: usize,
}
impl HistogramArena {
pub fn from_cuts(cuts: &JaggedMatrix<f64>, col_index: &[usize], _is_const_hess: bool, n_nodes: usize) -> Self {
let col_cuts: Vec<&[f64]> = col_index.iter().map(|&col| cuts.get_col(col)).collect();
let col_bin_counts: Vec<usize> = col_cuts.iter().map(|c| c.len()).collect();
let bins_per_node: usize = col_bin_counts.iter().sum();
let total_bins = bins_per_node * n_nodes;
let mut bins: Vec<Bin> = Self::alloc_zeroed_bins(total_bins);
let mut template_num: Vec<u16> = Vec::with_capacity(bins_per_node);
let mut template_cv: Vec<f64> = Vec::with_capacity(bins_per_node);
for col_cuts_slice in &col_cuts {
let cuts_mod = &col_cuts_slice[..(col_cuts_slice.len() - 1)];
template_num.push(0);
template_cv.push(f64::NAN);
for (it, c) in cuts_mod.iter().enumerate() {
template_num.push(it as u16 + 1);
template_cv.push(*c);
}
}
bins.par_chunks_mut(bins_per_node).for_each(|node_bins| {
for (bin, (&num, &cv)) in node_bins.iter_mut().zip(template_num.iter().zip(template_cv.iter())) {
bin.num = num;
bin.cut_value = cv;
}
});
HistogramArena {
bins,
col_bin_counts,
n_nodes,
}
}
pub fn from_fixed(max_bin: u16, col_amount: usize, _is_const_hess: bool, n_nodes: usize) -> Self {
let bins_per_feature = max_bin as usize + 2;
let col_bin_counts: Vec<usize> = vec![bins_per_feature; col_amount];
let bins_per_node = bins_per_feature * col_amount;
let total_bins = bins_per_node * n_nodes;
let mut bins: Vec<Bin> = Self::alloc_zeroed_bins(total_bins);
let mut template_num: Vec<u16> = Vec::with_capacity(bins_per_node);
let mut template_cv: Vec<f64> = Vec::with_capacity(bins_per_node);
for _col in 0..col_amount {
template_num.push(0);
template_cv.push(f64::NAN);
for i in 0..(max_bin + 1) {
template_num.push(i + 1);
template_cv.push(f64::NAN);
}
}
bins.par_chunks_mut(bins_per_node).for_each(|node_bins| {
for (bin, (&num, &cv)) in node_bins.iter_mut().zip(template_num.iter().zip(template_cv.iter())) {
bin.num = num;
bin.cut_value = cv;
}
});
HistogramArena {
bins,
col_bin_counts,
n_nodes,
}
}
fn alloc_zeroed_bins(count: usize) -> Vec<Bin> {
if count == 0 {
return Vec::new();
}
unsafe {
let layout = std::alloc::Layout::array::<Bin>(count).unwrap();
let ptr = std::alloc::alloc_zeroed(layout) as *mut Bin;
if ptr.is_null() {
std::alloc::handle_alloc_error(layout);
}
Vec::from_raw_parts(ptr, count, count)
}
}
pub fn as_node_histograms(&mut self) -> Vec<NodeHistogram<'_>> {
let mut result: Vec<NodeHistogram<'_>> = Vec::with_capacity(self.n_nodes);
let n_cols = self.col_bin_counts.len();
let all_cells: &[UnsafeCell<Bin>] = unsafe {
let ptr = self.bins.as_ptr() as *const UnsafeCell<Bin>;
std::slice::from_raw_parts(ptr, self.bins.len())
};
let mut offset = 0;
for _node in 0..self.n_nodes {
let mut features: Vec<FeatureHistogram<'_>> = Vec::with_capacity(n_cols);
for &n_bins in &self.col_bin_counts {
features.push(FeatureHistogram {
data: &all_cells[offset..offset + n_bins],
});
offset += n_bins;
}
result.push(NodeHistogram { data: features });
}
result
}
}
#[derive(Debug)]
pub struct NodeHistogram<'a> {
pub data: Vec<FeatureHistogram<'a>>,
}
impl<'a> NodeHistogram<'a> {
pub fn from_owned(hist: &'a mut NodeHistogramOwned) -> NodeHistogram<'a> {
let histograms = hist
.data
.iter_mut()
.map(|f| FeatureHistogram::new(&mut f.data))
.collect();
NodeHistogram { data: histograms }
}
pub fn from_parent_child(hist_tree: &[NodeHistogram], root_num: usize, child_num: usize, update_num: usize) {
unsafe {
let root_hist = &hist_tree.get_unchecked(root_num).data;
let child_hist = &hist_tree.get_unchecked(child_num).data;
let update_hist = &hist_tree.get_unchecked(update_num).data;
root_hist
.iter()
.zip(child_hist.iter())
.zip(update_hist.iter())
.for_each(|((root_feat_hist, child_feat_hist), update_feat_hist)| {
root_feat_hist
.data
.iter()
.zip(child_feat_hist.data.iter())
.zip(update_feat_hist.data.iter())
.for_each(|((root_bin, child_bin), update_bin)| {
Bin::from_parent_child(root_bin.get(), child_bin.get(), update_bin.get())
})
});
}
}
pub fn from_parent_two_children(
hist_tree: &[NodeHistogram],
root_num: usize,
first_num: usize,
second_num: usize,
update_num: usize,
) {
unsafe {
let root_hist = &hist_tree.get_unchecked(root_num).data;
let first_hist = &hist_tree.get_unchecked(first_num).data;
let second_hist = &hist_tree.get_unchecked(second_num).data;
let update_hist = &hist_tree.get_unchecked(update_num).data;
root_hist
.iter()
.zip(first_hist.iter())
.zip(second_hist.iter())
.zip(update_hist.iter())
.for_each(
|(((root_feat_hist, first_feat_hist), second_feat_hist), update_feat_hist)| {
root_feat_hist
.data
.iter()
.zip(first_feat_hist.data.iter())
.zip(second_feat_hist.data.iter())
.zip(update_feat_hist.data.iter())
.for_each(|(((root_bin, first_bin), second_bin), update_bin)| {
Bin::from_parent_two_children(
root_bin.get(),
first_bin.get(),
second_bin.get(),
update_bin.get(),
)
})
},
);
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn update_cuts(hist: &NodeHistogram, col_index: &[usize], cuts: &JaggedMatrix<f64>, parallel: bool) {
if parallel {
hist.data
.par_iter()
.zip(col_index.par_iter())
.for_each(|(h, i)| unsafe { h.update_cuts(cuts.get_col(*i)) })
} else {
hist.data
.iter()
.zip(col_index.iter())
.for_each(|(h, i)| unsafe { h.update_cuts(cuts.get_col(*i)) })
}
}
#[allow(clippy::too_many_arguments)]
pub fn update_histogram(
hist: &NodeHistogram,
start: usize,
stop: usize,
data: &Matrix<u16>,
grad: &[f32],
hess: Option<&[f32]>,
index: &[usize],
col_index: &[usize],
pool: &ThreadPool,
_sort: bool,
) {
let sorted_grad = grad;
let sorted_hess = hess;
unsafe {
let n_samples = stop - start;
if pool.current_num_threads() > 1 && n_samples >= 512 {
pool.scope(|s| {
for (i, &col) in col_index.iter().enumerate().take(hist.data.len()) {
let h = hist.data.get_unchecked(i);
let feature = data.get_col(col); s.spawn(|_| {
h.update(feature, sorted_grad, sorted_hess, &index[start..stop]);
});
}
});
} else {
col_index.iter().enumerate().for_each(|(i, col)| {
hist.data
.get_unchecked(i)
.update(data.get_col(*col), sorted_grad, sorted_hess, &index[start..stop]);
});
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn update_histogram_and_subtract(
hist_tree: &[NodeHistogram],
parent_num: usize,
child_num: usize,
update_num: usize,
start: usize,
stop: usize,
data: &Matrix<u16>,
grad: &[f32],
hess: Option<&[f32]>,
index: &[usize],
col_index: &[usize],
pool: &ThreadPool,
) {
let sorted_grad = grad;
let sorted_hess = hess;
unsafe {
let child_hist = hist_tree.get_unchecked(child_num);
let parent_hist = hist_tree.get_unchecked(parent_num);
let update_hist = hist_tree.get_unchecked(update_num);
let n_samples = stop - start;
if pool.current_num_threads() > 1 && n_samples >= 512 {
pool.scope(|s| {
for (i, &col) in col_index.iter().enumerate().take(child_hist.data.len()) {
let ch = child_hist.data.get_unchecked(i);
let ph = parent_hist.data.get_unchecked(i);
let uh = update_hist.data.get_unchecked(i);
let feature = data.get_col(col);
s.spawn(move |_| {
ch.update(feature, sorted_grad, sorted_hess, &index[start..stop]);
ph.data.iter().zip(ch.data.iter()).zip(uh.data.iter()).for_each(
|((parent_cell, child_cell), update_cell)| {
Bin::from_parent_child(parent_cell.get(), child_cell.get(), update_cell.get())
},
);
});
}
});
} else {
col_index.iter().enumerate().for_each(|(i, col)| {
child_hist.data.get_unchecked(i).update(
data.get_col(*col),
sorted_grad,
sorted_hess,
&index[start..stop],
);
});
NodeHistogram::from_parent_child(hist_tree, parent_num, child_num, update_num);
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn update_two_histograms_and_subtract(
hist_tree: &[NodeHistogram],
parent_num: usize,
first_num: usize,
first_start: usize,
first_stop: usize,
second_num: usize,
second_start: usize,
second_stop: usize,
update_num: usize,
data: &Matrix<u16>,
grad: &[f32],
hess: Option<&[f32]>,
index: &[usize],
col_index: &[usize],
pool: &ThreadPool,
) {
let sorted_grad = grad;
let sorted_hess = hess;
unsafe {
let first_hist = hist_tree.get_unchecked(first_num);
let second_hist = hist_tree.get_unchecked(second_num);
let parent_hist = hist_tree.get_unchecked(parent_num);
let update_hist = hist_tree.get_unchecked(update_num);
let n_samples = (first_stop - first_start) + (second_stop - second_start);
if pool.current_num_threads() > 1 && n_samples >= 512 {
pool.scope(|s| {
for (i, &col) in col_index.iter().enumerate().take(first_hist.data.len()) {
let fh = first_hist.data.get_unchecked(i);
let sh = second_hist.data.get_unchecked(i);
let ph = parent_hist.data.get_unchecked(i);
let uh = update_hist.data.get_unchecked(i);
let feature = data.get_col(col);
s.spawn(move |_| {
fh.update(feature, sorted_grad, sorted_hess, &index[first_start..first_stop]);
sh.update(feature, sorted_grad, sorted_hess, &index[second_start..second_stop]);
ph.data
.iter()
.zip(fh.data.iter())
.zip(sh.data.iter())
.zip(uh.data.iter())
.for_each(|(((pc, fc), sc), uc)| {
Bin::from_parent_two_children(pc.get(), fc.get(), sc.get(), uc.get())
});
});
}
});
} else {
col_index.iter().enumerate().for_each(|(i, col)| {
first_hist.data.get_unchecked(i).update(
data.get_col(*col),
sorted_grad,
sorted_hess,
&index[first_start..first_stop],
);
second_hist.data.get_unchecked(i).update(
data.get_col(*col),
sorted_grad,
sorted_hess,
&index[second_start..second_stop],
);
});
NodeHistogram::from_parent_two_children(hist_tree, parent_num, first_num, second_num, update_num);
}
}
}
#[cfg(test)]
mod tests {
use crate::Matrix;
use crate::binning::bin_matrix;
use crate::data::JaggedMatrix;
use crate::histogram::{
FeatureHistogram, FeatureHistogramOwned, HistogramArena, NodeHistogram, NodeHistogramOwned, update_cuts,
update_histogram,
};
use crate::objective::{Objective, ObjectiveFunction};
use approx::assert_relative_eq;
use rayon::ThreadPoolBuilder;
use std::collections::HashSet;
use std::fs;
#[test]
fn test_simple_histogram() {
let objective_function = Objective::LogLoss;
let nbins = 90;
let data_vec: Vec<f64> = (0..100).map(|i| i as f64).collect();
let y: Vec<f64> = (0..100).map(|i| i as f64).collect();
let data = Matrix::new(&data_vec, data_vec.len(), 1);
let b = bin_matrix(&data, None, nbins, f64::NAN, None).unwrap();
let bdata = Matrix::new(&b.binned_data, data.rows, data.cols);
let y_avg = y.iter().sum::<f64>() / y.len() as f64;
let yhat = vec![y_avg; y.len()];
let (g, h) = objective_function.gradient(&y, &yhat, None, None);
let col = 0;
let mut hist_feat_owned = FeatureHistogramOwned::empty_from_cuts(b.cuts.get_col(col), false);
let hist_feat = FeatureHistogram::new(&mut hist_feat_owned.data);
unsafe { hist_feat.update(bdata.get_col(col), &g, h.as_deref(), &bdata.index) };
let mut f = bdata.get_col(col).to_owned();
println!("histogram:");
println!("{:?}", hist_feat);
println!("histogram.cuts:");
println!(
"{:?}",
hist_feat
.data
.iter()
.map(|b| unsafe { b.get().as_ref().unwrap().cut_value })
.collect::<Vec<_>>()
);
println!("feature_data:");
println!("{:?}", &data.get_col(col));
println!("feature_data_bin_indices:");
println!("{:?}", &bdata.get_col(col));
println!("data_indices:");
println!("{:?}", &bdata.index);
println!("cuts:");
println!("{:?}", &b.cuts.get_col(col));
f.sort();
f.dedup();
println!("f:");
println!("{:?}", &f);
println!("{:?}", &f.len());
println!("{:?}", &hist_feat.data.len());
assert_eq!(f.len() + 1, hist_feat.data.len());
println!("b.cuts:");
println!("{:?}", &b.cuts);
println!("b.nunique:");
println!("{:?}", &b.nunique);
}
#[test]
fn test_single_histogram() {
let objective_function = Objective::LogLoss;
let nbins = 10;
let file =
fs::read_to_string("resources/contiguous_no_missing.csv").expect("Something went wrong reading the file");
let data_vec: Vec<f64> = file.lines().map(|x| x.parse::<f64>().unwrap()).collect();
let data = Matrix::new(&data_vec, 891, 5);
let b = bin_matrix(&data, None, nbins, f64::NAN, None).unwrap();
let bdata = Matrix::new(&b.binned_data, data.rows, data.cols);
let y: Vec<f64> = file.lines().map(|x| x.parse::<f64>().unwrap()).collect();
let yhat = vec![0.5; y.len()];
let (g, h) = objective_function.gradient(&y, &yhat, None, None);
let col_index: Vec<usize> = (0..data.cols).collect();
let mut hist_init_owned = NodeHistogramOwned::empty_from_cuts(&b.cuts, &col_index, true, false);
let mut hist_init = NodeHistogram::from_owned(&mut hist_init_owned);
let col = 1;
unsafe {
hist_init
.data
.get_mut(col)
.unwrap()
.update(bdata.get_col(col), &g, h.as_deref(), &bdata.index)
};
let mut f = bdata.get_col(col).to_owned();
println!("histogram:");
println!("{:?}", hist_init.data.get(col).unwrap());
println!("feature_data:");
println!("{:?}", &data.get_col(col));
println!("feature_data_bin_indices:");
println!("{:?}", &bdata.get_col(col));
println!("data_indices:");
println!("{:?}", &bdata.index);
println!("cuts:");
println!("{:?}", &b.cuts.get_col(col));
f.sort();
f.dedup();
println!("f:");
println!("{:?}", &f);
println!("{:?}", &f.len());
println!("{:?}", &hist_init.data.get(col).unwrap().data.len());
assert_eq!(f.len() + 1, hist_init.data.get(col).unwrap().data.len());
}
#[test]
fn test_histogram_categorical() {
let objective_function = Objective::LogLoss;
let file =
fs::read_to_string("resources/titanic_train_flat.csv").expect("Something went wrong reading the file");
let n_rows = 712;
let n_columns = 13;
let n_lines = n_columns * n_rows;
let data_vec: Vec<f64> = file
.lines()
.take(n_lines)
.map(|x| x.trim().parse::<f64>().unwrap_or(f64::NAN))
.collect();
let data = Matrix::new(&data_vec, n_rows, n_columns);
let b = bin_matrix(
&data,
None,
256,
f64::NAN,
Some(&HashSet::from([0, 3, 4, 6, 7, 8, 10, 11])),
)
.unwrap();
let bdata = Matrix::new(&b.binned_data, data.rows, data.cols);
let y: Vec<f64> = file.lines().map(|x| x.parse::<f64>().unwrap_or(f64::NAN)).collect();
let yhat = vec![0.5; y.len()];
let (g, h) = objective_function.gradient(&y, &yhat, None, None);
let col_index: Vec<usize> = (0..data.cols).collect();
let mut hist_init_owned = NodeHistogramOwned::empty_from_cuts(&b.cuts, &col_index, false, false);
let hist_init = NodeHistogram::from_owned(&mut hist_init_owned);
let col = 0;
let pool = rayon::ThreadPoolBuilder::new().num_threads(2).build().unwrap();
update_histogram(
&hist_init,
0,
bdata.index.len(),
&bdata,
&g,
h.as_deref(),
&bdata.index,
&col_index,
&pool,
false,
);
let mut f = bdata.get_col(col).to_owned();
println!("histogram:");
println!("{:?}", unsafe { hist_init.data.get_unchecked(col) });
println!("cuts:");
println!("{:?}", &b.cuts.get_col(col));
f.sort();
f.dedup();
println!("f:");
println!("{:?}", &f);
println!("{:?}", &f.len());
println!("{:?}", unsafe { hist_init.data.get_unchecked(col) }.data.len());
assert_eq!(f.len() + 1, unsafe { hist_init.data.get_unchecked(col) }.data.len());
}
#[test]
fn test_histogram_parallel() {
let objective_function = Objective::LogLoss;
let file =
fs::read_to_string("resources/titanic_train_flat.csv").expect("Something went wrong reading the file");
let n_rows = 712;
let n_columns = 13;
let n_lines = n_columns * n_rows;
let data_vec: Vec<f64> = file
.lines()
.take(n_lines)
.map(|x| x.trim().parse::<f64>().unwrap_or(f64::NAN))
.collect();
let data = Matrix::new(&data_vec, n_rows, n_columns);
let b = bin_matrix(&data, None, 256, f64::NAN, Some(&HashSet::from([1]))).unwrap();
let bdata = Matrix::new(&b.binned_data, data.rows, data.cols);
let y: Vec<f64> = file.lines().map(|x| x.parse::<f64>().unwrap_or(f64::NAN)).collect();
let yhat = vec![0.5; y.len()];
let (g, h) = objective_function.gradient(&y, &yhat, None, None);
let col_index: Vec<usize> = (0..data.cols).collect();
let mut hist_init_owned1 = NodeHistogramOwned::empty_from_cuts(&b.cuts, &col_index, false, false);
let hist_init1 = NodeHistogram::from_owned(&mut hist_init_owned1);
let mut hist_init_owned2 = NodeHistogramOwned::empty_from_cuts(&b.cuts, &col_index, false, false);
let hist_init2 = NodeHistogram::from_owned(&mut hist_init_owned2);
let col = 1;
let pool1 = rayon::ThreadPoolBuilder::new().num_threads(1).build().unwrap();
let pool2 = rayon::ThreadPoolBuilder::new().num_threads(2).build().unwrap();
update_histogram(
&hist_init1,
0,
bdata.index.len(),
&bdata,
&g,
h.as_deref(),
&bdata.index,
&col_index,
&pool1,
false,
);
update_histogram(
&hist_init2,
0,
bdata.index.len(),
&bdata,
&g,
h.as_deref(),
&bdata.index,
&col_index,
&pool2,
false,
);
let bins1 = unsafe { &hist_init_owned1.data.get_unchecked(col).data };
let bins2 = unsafe { &hist_init_owned2.data.get_unchecked(col).data };
println!("{:?}", bins1[0].g_folded);
println!("{:?}", bins2[0].g_folded);
bins1.iter().zip(bins2.iter()).for_each(|(b1, b2)| {
b1.g_folded.iter().zip(b2.g_folded.iter()).for_each(|(g1, g2)| {
assert_relative_eq!(g1, g2);
});
b1.h_folded.iter().zip(b2.h_folded.iter()).for_each(|(h1, h2)| {
assert_relative_eq!(h1, h2);
});
});
}
#[test]
fn test_histogram_constructors_and_arena() {
let nbins = 10;
let col_amount = 3;
let is_const_hess = true;
let parallel = false;
let fh_owned = FeatureHistogramOwned::empty(nbins, is_const_hess);
assert_eq!(fh_owned.data.len(), (nbins + 2) as usize);
let fh_owned_var = FeatureHistogramOwned::empty(nbins, false);
assert_eq!(fh_owned_var.data.len(), (nbins + 2) as usize);
let nh_owned = NodeHistogramOwned::empty(nbins, col_amount, is_const_hess, parallel);
assert_eq!(nh_owned.data.len(), col_amount);
assert_eq!(nh_owned.data[0].data.len(), (nbins + 2) as usize);
let nh_owned_par = NodeHistogramOwned::empty(nbins, col_amount, is_const_hess, true);
assert_eq!(nh_owned_par.data.len(), col_amount);
let n_nodes = 2;
let mut arena = HistogramArena::from_fixed(nbins, col_amount, is_const_hess, n_nodes);
let n_hists = arena.as_node_histograms();
assert_eq!(n_hists.len(), n_nodes);
assert_eq!(n_hists[0].data.len(), col_amount);
let cuts_vec = vec![vec![0.1, 0.5, 0.9], vec![1.0, 2.0], vec![0.5]];
let cuts = JaggedMatrix::from_vecs(&cuts_vec);
let col_index = vec![0, 1, 2];
let mut arena_cuts = HistogramArena::from_cuts(&cuts, &col_index, is_const_hess, n_nodes);
let n_hists_cuts = arena_cuts.as_node_histograms();
assert_eq!(n_hists_cuts.len(), n_nodes);
assert_eq!(n_hists_cuts[0].data.len(), col_amount);
update_cuts(&n_hists_cuts[0], &col_index, &cuts, false);
update_cuts(&n_hists_cuts[1], &col_index, &cuts, true);
}
#[test]
fn test_histogram_subtraction_ternary() {
let nbins = 10;
let n_rows = 1000;
let data_vec: Vec<f64> = (0..n_rows).map(|i| i as f64).collect();
let data = Matrix::new(&data_vec, n_rows, 1);
let b = bin_matrix(&data, None, nbins, f64::NAN, None).unwrap();
let bdata = Matrix::new(&b.binned_data, data.rows, data.cols);
let y = vec![0.0; n_rows];
let yhat = vec![0.5; n_rows];
let (g, h) = Objective::LogLoss.gradient(&y, &yhat, None, None);
let col_index = vec![0];
let mut arena = HistogramArena::from_cuts(&b.cuts, &col_index, false, 4);
let hist_tree = arena.as_node_histograms();
update_histogram(
&hist_tree[0],
0,
n_rows,
&bdata,
&g,
h.as_deref(),
&bdata.index,
&col_index,
&ThreadPoolBuilder::new().build().unwrap(),
false,
);
let pool = ThreadPoolBuilder::new().num_threads(2).build().unwrap();
use crate::histogram::update_two_histograms_and_subtract;
update_two_histograms_and_subtract(
&hist_tree,
0, 1, 0,
300,
2, 300,
600,
3, &bdata,
&g,
h.as_deref(),
&bdata.index,
&col_index,
&pool,
);
let p_bin = unsafe { hist_tree[0].data[0].data[0].get().as_ref().unwrap() };
let f_bin = unsafe { hist_tree[1].data[0].data[0].get().as_ref().unwrap() };
let s_bin = unsafe { hist_tree[2].data[0].data[0].get().as_ref().unwrap() };
let u_bin = unsafe { hist_tree[3].data[0].data[0].get().as_ref().unwrap() };
assert_relative_eq!(
p_bin.g_folded.iter().sum::<f32>(),
f_bin.g_folded.iter().sum::<f32>()
+ s_bin.g_folded.iter().sum::<f32>()
+ u_bin.g_folded.iter().sum::<f32>(),
epsilon = 1e-4
);
}
}