pub const STREAM_GRADIENT: u32 = 0;
pub const STREAM_HESSIAN: u32 = 1;
pub const STREAM_BAGGING: u32 = 2;
#[inline]
pub fn mix32(mut x: u32) -> u32 {
x ^= x >> 16;
x = x.wrapping_mul(0x7feb_352d);
x ^= x >> 15;
x = x.wrapping_mul(0x846c_a68b);
x ^= x >> 16;
x
}
#[inline]
pub fn uniform(seed: u32, stream: u32, iteration: u32, row: u64) -> f32 {
let row_hash = mix32((row as u32) ^ mix32((row >> 32) as u32));
let h = mix32(seed ^ mix32(stream ^ mix32(iteration ^ row_hash)));
(h >> 8) as f32 * (1.0 / 16_777_216.0)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct QuantSpec {
pub inv_grad_scale: f32,
pub inv_hess_scale: f32,
pub grad_scale: f64,
pub hess_scale: f64,
pub grad_max_q: i32,
pub hess_max_q: i32,
pub constant_hessian: bool,
pub stochastic: bool,
pub seed: u32,
pub iteration: u32,
}
impl QuantSpec {
pub fn new(
max_abs_grad: f64,
max_hess: f64,
constant_hessian: Option<f64>,
num_bins: u32,
stochastic: bool,
seed: u32,
iteration: u32,
) -> QuantSpec {
let grad_max_q = (num_bins / 2) as i32;
let hess_max_q = num_bins as i32;
let grad_scale = positive_or_one(max_abs_grad / f64::from(grad_max_q));
let hess_scale = match constant_hessian {
Some(value) => positive_or_one(value),
None => positive_or_one(max_hess / f64::from(hess_max_q)),
};
QuantSpec {
inv_grad_scale: (1.0 / grad_scale) as f32,
inv_hess_scale: (1.0 / hess_scale) as f32,
grad_scale,
hess_scale,
grad_max_q,
hess_max_q,
constant_hessian: constant_hessian.is_some(),
stochastic,
seed,
iteration,
}
}
pub fn max_rows_per_packed_block(&self) -> u32 {
(32_767 / self.grad_max_q.max(self.hess_max_q)) as u32
}
}
fn positive_or_one(value: f64) -> f64 {
if value > 0.0 && value.is_finite() {
value
} else {
1.0
}
}
#[inline]
pub fn quantize_row(spec: &QuantSpec, grad: f32, hess: f32, row: u64) -> i32 {
let g_q = quantize_grad(spec, grad, row);
let h_q = quantize_hess(spec, hess, row);
h_q * 65_536 + g_q
}
#[inline]
fn quantize_grad(spec: &QuantSpec, grad: f32, row: u64) -> i32 {
let q = if spec.stochastic {
let u = uniform(spec.seed, STREAM_GRADIENT, spec.iteration, row);
if grad >= 0.0 {
grad.mul_add(spec.inv_grad_scale, u).trunc()
} else {
grad.mul_add(spec.inv_grad_scale, -u).trunc()
}
} else {
(grad * spec.inv_grad_scale).round_ties_even()
};
(q as i32).clamp(-spec.grad_max_q, spec.grad_max_q)
}
#[inline]
fn quantize_hess(spec: &QuantSpec, hess: f32, row: u64) -> i32 {
if spec.constant_hessian {
return 1;
}
let q = if spec.stochastic {
let u = uniform(spec.seed, STREAM_HESSIAN, spec.iteration, row);
hess.mul_add(spec.inv_hess_scale, u).trunc()
} else {
(hess * spec.inv_hess_scale).round_ties_even()
};
(q as i32).clamp(0, spec.hess_max_q)
}
#[inline]
pub fn unpack(packed: i32) -> (i32, i32) {
let g = i32::from(packed as u32 as u16 as i16);
(g, (packed - g) >> 16)
}
#[inline]
pub fn in_bag(bagging_seed: u32, bag_round: u32, row: u64, fraction: f64) -> bool {
f64::from(uniform(bagging_seed, STREAM_BAGGING, bag_round, row)) < fraction
}
pub fn fixed_point_scale(max_abs: f64) -> f32 {
if !max_abs.is_finite() || max_abs <= 0.0 {
return 1.0;
}
let exponent = 30 - max_abs.log2().ceil() as i32;
2f32.powi(exponent.clamp(-126, 127))
}
#[inline]
pub fn to_fixed(value: f32, scale: f32) -> i64 {
(value * scale).round_ties_even() as i64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uniform_is_in_unit_interval_and_row_keyed() {
let draws: Vec<f32> = (0..10_000).map(|row| uniform(7, STREAM_GRADIENT, 3, row)).collect();
assert!(draws.iter().all(|u| (0.0..1.0).contains(u)));
let mean = draws.iter().map(|&u| f64::from(u)).sum::<f64>() / draws.len() as f64;
assert!((mean - 0.5).abs() < 0.02, "mean {mean}");
assert_ne!(
uniform(7, STREAM_GRADIENT, 3, 1 << 32),
uniform(7, STREAM_GRADIENT, 3, 0)
);
}
#[test]
fn stochastic_rounding_is_unbiased() {
let spec = QuantSpec::new(2.0, 1.0, None, 4, true, 11, 0);
let grad = 0.3f32;
let sum: i64 = (0..200_000u64)
.map(|row| i64::from(unpack(quantize_row(&spec, grad, 0.5, row)).0))
.sum();
let mean = sum as f64 / 200_000.0 * spec.grad_scale;
assert!((mean - 0.3).abs() < 0.01, "mean {mean}");
}
#[test]
fn pack_round_trips_negative_gradients() {
for g in -32..=32 {
for h in 0..=64 {
assert_eq!(unpack(h * 65_536 + g), (g, h));
}
}
}
#[test]
fn constant_hessian_counts_rows() {
let spec = QuantSpec::new(1.0, 1.0, Some(1.0), 4, true, 1, 0);
assert_eq!(unpack(quantize_row(&spec, -0.7, 1.0, 5)).1, 1);
}
#[test]
fn fixed_point_scale_bounds_magnitude() {
let scale = fixed_point_scale(3.7);
assert!(f64::from(3.7f32 * scale) <= f64::from(1u32 << 30));
assert!(f64::from(3.7f32 * scale) > f64::from(1u32 << 29));
assert_eq!(fixed_point_scale(0.0), 1.0);
}
}