1use alloc::vec;
9use alloc::vec::Vec;
10
11use crate::histogram::BinEdges;
12
13#[derive(Debug, Clone)]
16pub struct FeatureHistogram {
17 pub grad_sums: Vec<f64>,
22 pub hess_sums: Vec<f64>,
24 pub counts: Vec<u64>,
26 pub edges: BinEdges,
28 decay_scale: f64,
38}
39
40impl FeatureHistogram {
41 pub fn new(edges: BinEdges) -> Self {
43 let n = edges.n_bins();
44 Self {
45 grad_sums: vec![0.0; n],
46 hess_sums: vec![0.0; n],
47 counts: vec![0; n],
48 edges,
49 decay_scale: 1.0,
50 }
51 }
52
53 #[inline]
58 pub fn accumulate(&mut self, value: f64, gradient: f64, hessian: f64) {
59 let bin = self.edges.find_bin(value);
60 self.grad_sums[bin] += gradient;
61 self.hess_sums[bin] += hessian;
62 self.counts[bin] += 1;
63 }
64
65 pub fn total_gradient(&self) -> f64 {
69 let raw = {
70 #[cfg(feature = "simd")]
71 {
72 crate::histogram::simd::sum_f64(&self.grad_sums)
73 }
74 #[cfg(not(feature = "simd"))]
75 {
76 self.grad_sums.iter().sum::<f64>()
77 }
78 };
79 raw * self.decay_scale
80 }
81
82 pub fn total_hessian(&self) -> f64 {
86 let raw = {
87 #[cfg(feature = "simd")]
88 {
89 crate::histogram::simd::sum_f64(&self.hess_sums)
90 }
91 #[cfg(not(feature = "simd"))]
92 {
93 self.hess_sums.iter().sum::<f64>()
94 }
95 };
96 raw * self.decay_scale
97 }
98
99 pub fn total_count(&self) -> u64 {
101 self.counts.iter().sum()
102 }
103
104 #[inline]
106 pub fn n_bins(&self) -> usize {
107 self.edges.n_bins()
108 }
109
110 #[inline]
124 pub fn accumulate_with_decay(&mut self, value: f64, gradient: f64, hessian: f64, alpha: f64) {
125 self.decay_scale *= alpha;
126 let inv_scale = 1.0 / self.decay_scale;
127 let bin = self.edges.find_bin(value);
128 self.grad_sums[bin] += gradient * inv_scale;
129 self.hess_sums[bin] += hessian * inv_scale;
130 self.counts[bin] += 1;
131
132 if self.decay_scale < 1e-100 {
136 self.materialize_decay();
137 }
138 }
139
140 #[inline]
146 pub fn materialize_decay(&mut self) {
147 if (self.decay_scale - 1.0).abs() > f64::EPSILON {
148 for i in 0..self.grad_sums.len() {
149 self.grad_sums[i] *= self.decay_scale;
150 self.hess_sums[i] *= self.decay_scale;
151 }
152 self.decay_scale = 1.0;
153 }
154 }
155
156 pub fn reset(&mut self) {
158 self.grad_sums.fill(0.0);
159 self.hess_sums.fill(0.0);
160 self.counts.fill(0);
161 self.decay_scale = 1.0;
162 }
163
164 pub fn subtract(&self, child: &FeatureHistogram) -> FeatureHistogram {
176 debug_assert_eq!(
177 self.n_bins(),
178 child.n_bins(),
179 "cannot subtract histograms with different bin counts"
180 );
181 let n = self.n_bins();
182 let s_self = self.decay_scale;
183 let s_child = child.decay_scale;
184
185 let scales_trivial =
187 (s_self - 1.0).abs() <= f64::EPSILON && (s_child - 1.0).abs() <= f64::EPSILON;
188
189 #[cfg(feature = "simd")]
190 {
191 if scales_trivial {
192 let mut grad_sums = vec![0.0; n];
193 let mut hess_sums = vec![0.0; n];
194 let mut counts = vec![0u64; n];
195 crate::histogram::simd::subtract_f64(
196 &self.grad_sums,
197 &child.grad_sums,
198 &mut grad_sums,
199 );
200 crate::histogram::simd::subtract_f64(
201 &self.hess_sums,
202 &child.hess_sums,
203 &mut hess_sums,
204 );
205 crate::histogram::simd::subtract_u64(&self.counts, &child.counts, &mut counts);
206 return FeatureHistogram {
207 grad_sums,
208 hess_sums,
209 counts,
210 edges: self.edges.clone(),
211 decay_scale: 1.0,
212 };
213 }
214 }
215
216 let _ = scales_trivial; let mut grad_sums = Vec::with_capacity(n);
219 let mut hess_sums = Vec::with_capacity(n);
220 let mut counts = Vec::with_capacity(n);
221
222 for i in 0..n {
223 grad_sums.push(self.grad_sums[i] * s_self - child.grad_sums[i] * s_child);
224 hess_sums.push(self.hess_sums[i] * s_self - child.hess_sums[i] * s_child);
225 counts.push(self.counts[i].saturating_sub(child.counts[i]));
226 }
227
228 FeatureHistogram {
229 grad_sums,
230 hess_sums,
231 counts,
232 edges: self.edges.clone(),
233 decay_scale: 1.0,
234 }
235 }
236}
237
238#[derive(Debug, Clone)]
243pub struct LeafHistograms {
244 pub histograms: Vec<FeatureHistogram>,
246}
247
248impl LeafHistograms {
249 pub fn new(edges_per_feature: &[BinEdges]) -> Self {
251 let histograms = edges_per_feature
252 .iter()
253 .map(|edges| FeatureHistogram::new(edges.clone()))
254 .collect();
255 Self { histograms }
256 }
257
258 pub fn accumulate(&mut self, features: &[f64], gradient: f64, hessian: f64) {
264 debug_assert_eq!(
265 features.len(),
266 self.histograms.len(),
267 "feature count mismatch: got {} features but have {} histograms",
268 features.len(),
269 self.histograms.len(),
270 );
271 for (hist, &value) in self.histograms.iter_mut().zip(features.iter()) {
272 hist.accumulate(value, gradient, hessian);
273 }
274 }
275
276 pub fn accumulate_with_decay(
280 &mut self,
281 features: &[f64],
282 gradient: f64,
283 hessian: f64,
284 alpha: f64,
285 ) {
286 debug_assert_eq!(
287 features.len(),
288 self.histograms.len(),
289 "feature count mismatch: got {} features but have {} histograms",
290 features.len(),
291 self.histograms.len(),
292 );
293 for (hist, &value) in self.histograms.iter_mut().zip(features.iter()) {
294 hist.accumulate_with_decay(value, gradient, hessian, alpha);
295 }
296 }
297
298 pub fn total_count(&self) -> u64 {
303 self.histograms.first().map_or(0, |h| h.total_count())
304 }
305
306 #[inline]
308 pub fn n_features(&self) -> usize {
309 self.histograms.len()
310 }
311
312 pub fn materialize_decay(&mut self) {
317 for hist in &mut self.histograms {
318 hist.materialize_decay();
319 }
320 }
321
322 pub fn reset(&mut self) {
324 for hist in &mut self.histograms {
325 hist.reset();
326 }
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use crate::histogram::BinEdges;
334
335 fn four_bin_edges() -> BinEdges {
338 BinEdges {
339 edges: vec![2.0, 4.0, 6.0],
340 }
341 }
342
343 #[test]
344 fn feature_histogram_new_is_zeroed() {
345 let h = FeatureHistogram::new(four_bin_edges());
346 assert_eq!(h.n_bins(), 4);
347 assert_eq!(h.total_gradient(), 0.0);
348 assert_eq!(h.total_hessian(), 0.0);
349 assert_eq!(h.total_count(), 0);
350 }
351
352 #[test]
353 fn feature_histogram_accumulate_single() {
354 let mut h = FeatureHistogram::new(four_bin_edges());
355 h.accumulate(3.0, 1.5, 0.5);
357 assert_eq!(h.counts[1], 1);
358 assert_eq!(h.grad_sums[1], 1.5);
359 assert_eq!(h.hess_sums[1], 0.5);
360 assert_eq!(h.total_count(), 1);
361 }
362
363 #[test]
364 fn feature_histogram_accumulate_multiple() {
365 let mut h = FeatureHistogram::new(four_bin_edges());
366 h.accumulate(1.0, 0.1, 0.01);
368 h.accumulate(0.5, 0.2, 0.02);
369 h.accumulate(2.0, 0.3, 0.03);
371 h.accumulate(5.0, 0.4, 0.04);
373 h.accumulate(7.0, 0.5, 0.05);
375 h.accumulate(8.0, 0.6, 0.06);
376
377 assert_eq!(h.counts, vec![2, 1, 1, 2]);
378 assert_eq!(h.total_count(), 6);
379
380 let total_grad = 0.1 + 0.2 + 0.3 + 0.4 + 0.5 + 0.6;
381 assert!((h.total_gradient() - total_grad).abs() < 1e-12);
382
383 let total_hess = 0.01 + 0.02 + 0.03 + 0.04 + 0.05 + 0.06;
384 assert!((h.total_hessian() - total_hess).abs() < 1e-12);
385 }
386
387 #[test]
388 fn feature_histogram_subtraction_trick() {
389 let edges = four_bin_edges();
390 let mut parent = FeatureHistogram::new(edges.clone());
391 let mut child = FeatureHistogram::new(edges);
392
393 parent.accumulate(1.0, 0.1, 0.01);
395 parent.accumulate(3.0, 0.2, 0.02);
396 parent.accumulate(5.0, 0.3, 0.03);
397 parent.accumulate(7.0, 0.4, 0.04);
398 parent.accumulate(1.5, 0.5, 0.05);
399 parent.accumulate(5.5, 0.6, 0.06);
400
401 child.accumulate(1.0, 0.1, 0.01);
403 child.accumulate(5.0, 0.3, 0.03);
404 child.accumulate(7.0, 0.4, 0.04);
405
406 let sibling = parent.subtract(&child);
407
408 assert_eq!(sibling.total_count(), 3);
410 assert_eq!(sibling.counts[0], 1);
412 assert!((sibling.grad_sums[0] - 0.5).abs() < 1e-12);
413 assert!((sibling.hess_sums[0] - 0.05).abs() < 1e-12);
414 assert_eq!(sibling.counts[1], 1);
416 assert!((sibling.grad_sums[1] - 0.2).abs() < 1e-12);
417 assert_eq!(sibling.counts[2], 1);
419 assert!((sibling.grad_sums[2] - 0.6).abs() < 1e-12);
420 assert_eq!(sibling.counts[3], 0);
422 assert!((sibling.grad_sums[3]).abs() < 1e-12);
423 }
424
425 #[test]
426 fn feature_histogram_reset() {
427 let mut h = FeatureHistogram::new(four_bin_edges());
428 h.accumulate(1.0, 1.0, 1.0);
429 h.accumulate(3.0, 2.0, 2.0);
430 assert_eq!(h.total_count(), 2);
431
432 h.reset();
433 assert_eq!(h.total_count(), 0);
434 assert_eq!(h.total_gradient(), 0.0);
435 assert_eq!(h.total_hessian(), 0.0);
436 assert_eq!(h.n_bins(), 4); }
438
439 #[test]
440 fn leaf_histograms_multi_feature() {
441 let edges_f0 = BinEdges { edges: vec![5.0] }; let edges_f1 = BinEdges {
443 edges: vec![2.0, 4.0, 6.0],
444 }; let mut leaf = LeafHistograms::new(&[edges_f0, edges_f1]);
447 assert_eq!(leaf.n_features(), 2);
448
449 leaf.accumulate(&[3.0, 5.0], 1.0, 0.1);
451 leaf.accumulate(&[7.0, 1.0], 2.0, 0.2);
453 leaf.accumulate(&[3.0, 3.0], 3.0, 0.3);
455
456 assert_eq!(leaf.total_count(), 3);
457
458 let h0 = &leaf.histograms[0];
460 assert_eq!(h0.counts, vec![2, 1]); assert!((h0.total_gradient() - 6.0).abs() < 1e-12);
462 assert!((h0.total_hessian() - 0.6).abs() < 1e-12);
463
464 let h1 = &leaf.histograms[1];
466 assert_eq!(h1.counts, vec![1, 1, 1, 0]);
467 assert!((h1.total_gradient() - 6.0).abs() < 1e-12);
468 }
469
470 #[test]
471 fn leaf_histograms_reset() {
472 let edges = BinEdges { edges: vec![3.0] };
473 let mut leaf = LeafHistograms::new(&[edges.clone(), edges]);
474 leaf.accumulate(&[1.0, 5.0], 1.0, 1.0);
475 assert_eq!(leaf.total_count(), 1);
476
477 leaf.reset();
478 assert_eq!(leaf.total_count(), 0);
479 assert_eq!(leaf.n_features(), 2);
480 }
481
482 #[test]
483 fn leaf_histograms_empty() {
484 let leaf = LeafHistograms::new(&[]);
485 assert_eq!(leaf.n_features(), 0);
486 assert_eq!(leaf.total_count(), 0);
487 }
488
489 #[test]
490 fn single_edge_histogram() {
491 let edges = BinEdges { edges: vec![5.0] };
493 let mut h = FeatureHistogram::new(edges);
494 assert_eq!(h.n_bins(), 2);
495
496 h.accumulate(3.0, 1.0, 0.1);
497 h.accumulate(5.0, 2.0, 0.2); h.accumulate(7.0, 3.0, 0.3);
499
500 assert_eq!(h.counts, vec![1, 2]);
501 assert!((h.grad_sums[0] - 1.0).abs() < 1e-12);
502 assert!((h.grad_sums[1] - 5.0).abs() < 1e-12);
503 }
504
505 #[test]
506 fn no_edges_single_bin() {
507 let edges = BinEdges { edges: vec![] };
509 let mut h = FeatureHistogram::new(edges);
510 assert_eq!(h.n_bins(), 1);
511
512 h.accumulate(42.0, 1.0, 0.5);
513 h.accumulate(-100.0, 2.0, 0.3);
514
515 assert_eq!(h.counts, vec![2]);
516 assert!((h.total_gradient() - 3.0).abs() < 1e-12);
517 assert!((h.total_hessian() - 0.8).abs() < 1e-12);
518 }
519
520 #[test]
521 fn accumulate_with_decay_recent_dominates() {
522 let edges = BinEdges {
524 edges: vec![3.0, 7.0],
525 };
526 let mut h = FeatureHistogram::new(edges);
527 let alpha = 0.9;
528
529 for _ in 0..100 {
531 h.accumulate_with_decay(1.0, 1.0, 1.0, alpha);
532 }
533
534 for _ in 0..50 {
536 h.accumulate_with_decay(8.0, 1.0, 1.0, alpha);
537 }
538
539 h.materialize_decay();
541
542 assert!(
544 h.grad_sums[2] > h.grad_sums[0],
545 "recent bin should dominate: bin2={} > bin0={}",
546 h.grad_sums[2],
547 h.grad_sums[0],
548 );
549 }
550
551 #[test]
552 fn lazy_decay_matches_eager() {
553 let edges = BinEdges {
556 edges: vec![3.0, 6.0],
557 }; let alpha = 0.95;
559
560 let mut lazy = FeatureHistogram::new(edges.clone());
562
563 let n = 3;
566 let mut eager_grad = vec![0.0; n];
567 let mut eager_hess = vec![0.0; n];
568
569 let samples: Vec<(f64, f64, f64)> = vec![
570 (1.0, 0.5, 1.0), (4.0, -0.3, 0.8), (1.0, 0.7, 1.2), (8.0, -1.0, 0.5), (5.0, 0.2, 0.9), (1.0, 0.1, 1.1), (8.0, 0.4, 0.6), (4.0, -0.5, 1.0), ];
579
580 let edge_vals = [3.0, 6.0];
581 for &(value, gradient, hessian) in &samples {
582 for i in 0..n {
584 eager_grad[i] *= alpha;
585 eager_hess[i] *= alpha;
586 }
587 let bin = if value <= edge_vals[0] {
588 0
589 } else if value <= edge_vals[1] {
590 1
591 } else {
592 2
593 };
594 eager_grad[bin] += gradient;
595 eager_hess[bin] += hessian;
596
597 lazy.accumulate_with_decay(value, gradient, hessian, alpha);
599 }
600
601 lazy.materialize_decay();
603
604 for i in 0..n {
605 assert!(
606 (lazy.grad_sums[i] - eager_grad[i]).abs() < 1e-10,
607 "grad_sums[{}]: lazy={}, eager={}",
608 i,
609 lazy.grad_sums[i],
610 eager_grad[i],
611 );
612 assert!(
613 (lazy.hess_sums[i] - eager_hess[i]).abs() < 1e-10,
614 "hess_sums[{}]: lazy={}, eager={}",
615 i,
616 lazy.hess_sums[i],
617 eager_hess[i],
618 );
619 }
620 }
621
622 #[test]
623 fn lazy_decay_total_gradient_without_materialize() {
624 let edges = BinEdges { edges: vec![5.0] }; let alpha = 0.9;
628 let mut h = FeatureHistogram::new(edges);
629
630 h.accumulate_with_decay(1.0, 1.0, 1.0, alpha); h.accumulate_with_decay(7.0, 1.0, 1.0, alpha); let total = h.total_gradient();
636 assert!(
637 (total - 1.9).abs() < 1e-10,
638 "total_gradient should account for decay_scale: got {}",
639 total,
640 );
641 }
642
643 #[test]
644 fn materialize_is_idempotent() {
645 let edges = BinEdges { edges: vec![5.0] };
646 let mut h = FeatureHistogram::new(edges);
647 let alpha = 0.95;
648
649 for _ in 0..50 {
650 h.accumulate_with_decay(1.0, 1.0, 1.0, alpha);
651 }
652
653 h.materialize_decay();
654 let grad_after_first = h.grad_sums.clone();
655
656 h.materialize_decay(); assert_eq!(
658 h.grad_sums, grad_after_first,
659 "second materialize should be a no-op"
660 );
661 }
662
663 #[test]
664 fn lazy_decay_renormalization() {
665 let edges = BinEdges { edges: vec![5.0] };
667 let mut h = FeatureHistogram::new(edges);
668 let alpha = 0.5; for _ in 0..500 {
671 h.accumulate_with_decay(1.0, 1.0, 1.0, alpha);
672 }
673
674 let total = h.total_gradient();
676 assert!(
677 total.is_finite(),
678 "gradient should be finite after renormalization, got {}",
679 total
680 );
681 assert!(total > 0.0, "gradient should be positive, got {}", total);
682
683 assert!(
685 (total - 2.0).abs() < 0.1,
686 "total gradient should converge to ~2.0, got {}",
687 total,
688 );
689 }
690}