Skip to content

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 21 Oct 01:25
· 23 commits to main since this release
Immutable release. Only release title and notes can be modified.
eb63f38

hessboost v0.4.0

hessboost v0.4.0 introduces bin-once shared histogram storage (BinnedMatrix, zero-copy borrowed dense arrays, and row-view slicing), native model averaging, out-of-fold cross-validation with leakage-safe per-fold stopping, exact quantile and median leaf estimation, faster complete-tree row prediction and sparse multi-output histogram sweeps, and PEP 803 abi3t wheels for CPython 3.15+. (v0.3.1 was bumped in repository history without being tagged or published and is folded into v0.4.0.)

Full Changelog: v0.3.0...v0.4.0

Breaking Changes

  • Rust Cross-Validation API (#165): CrossValidation::early_stopping_rounds(n) is replaced by CrossValidation::early_stopping(CvStopping). Use CvStopping::fold_mean(n) for the previous fold-mean stopping behavior. CrossValidation::run() now returns CvReport instead of Vec<CvResult>; read .results for the fold-mean history.
  • Python 3.12 Minimum & Typing (#162): CPython 3.11 is no longer supported (installers on Python 3.11 remain on 0.3.0), and CPython 3.15+ requires numpy >= 2.5. Public Python type aliases are now defined with PEP 695 type statements (TypeAliasType instances at runtime).
  • Row Selection Returns Views (#169): Rust DMatrix::select_rows now returns a row-mapped view sharing backing storage instead of copying a new matrix. Training with default ViewCuts::Own remains bit-for-bit identical to training on a copied matrix; ViewCuts::Source opts into reusing the source matrix's bins and split thresholds.

Highlights Since v0.3.0

Data & Shared Binning

  • Bin Once and Share (#169): Feature values are binned once and reused across repeated trainings of a matrix and its metadata copies, CV folds, row selections, and concurrent trainings, producing bit-identical model and margin hashes (verified across 7 configurations at 1, 4, and 16 threads).
  • BinnedMatrix (#169): New data::BinnedMatrix (Python hessboost.BinnedMatrix) retains the histogram index and metadata while dropping raw feature values, training bit-for-bit identically to DMatrix. Accepted only by train; refuses options that require raw feature values (differing max_bin, continuation, CUDA, approx/exact tree methods, non-gbtree boosters, linear leaves, refresh, SGLB, and reuse penalties).
  • Row Views & Source Cuts (#169): DMatrix::select_rows (Python DMatrix.slice(rows, cuts="own" | "source")) creates a storage-sharing view. ViewCuts::Own (default) computes quantile cuts on the selected rows just as a copy would; ViewCuts::Source gathers the source matrix's bins to share a single binning pass and split thresholds.
  • Zero-Copy Dense Borrowing (#169, #170): Python DMatrix(x, copy=False) and Rust DMatrix::from_dense_shared borrow read-only, C-contiguous, aligned float32 arrays in place, keeping memory-mapped .npy files file-backed. Arrays whose base chain reaches an ownerless memory view (e.g., ctypes.memoryview_at or PyMemoryView_FromMemory) are refused so borrowed buffers cannot be freed while in use (#170).
  • Memory & Throughput Gains (#169): On a 4M × 173 panel, single-thread 1-round training drops from 15.46s (first) / 15.82s (repeat) to 10.74s (first) / 0.48s (repeat); multi-head concurrent training peak memory drops from 23.54 GiB to 5.10 GiB (8 heads × 4 threads) and from 13.10 GiB to 4.60 GiB (4 heads); full-row refit drops from 5.55 GiB / 4.24s to 4.49 GiB / 1.62s with ViewCuts::Source.

Model Averaging & Cross-Validation

  • Native Model Averaging (#165): BoostedModel::average (Python Booster.average) averages ensembles directly in margin space by dividing leaf values by member count, concatenating trees, and averaging intercepts. Refuses gblinear, model shrinkage, DART with non-unit weights, Boulevard, EBM, and best_iteration.
  • Per-Fold Stopping & Out-of-Fold Margins (#165, #167): CvStopping::per_fold(patience, PurgedTail) (Python cross_validate(..., per_fold_stopping=PurgedTail)) performs leakage-safe early stopping per fold on a purged chronological tail (computing the cutoff offset in f64, rounding up to a whole tick, and comparing integer offsets exactly) and then retrains each fold on all of its rows. CrossValidation::run() returns CvReport containing .results (fold-mean history), .folds (per-fold test rows, round counts, scores, margins, and tail split), and CvReport::out_of_fold() to assemble per-row OOF margins. CrossValidation::history_only() (used by hessboost.cv and Python cv) discards fold models.
  • Intercept & Residual-Scale Speedups (#165): Faster quantile/absolute-error residual-scale pass and a stable radix sort for the intercept, bit-for-bit unchanged.

Exact Quantile & Median Leaves

  • LeafEstimation::Exact (#164): Opt-in leaf_estimation (LeafEstimation::Exact or flat key leaf_estimation_method="exact"; default Newton is bit-identical to previous releases). For reg:quantileerror and reg:absoluteerror, tree structure is grown from surrogate gradients, and each leaf output is set to eta / num_parallel_tree times the alpha-quantile (median for reg:absoluteerror) of the leaf's residuals across all training rows routed to that leaf, whether sampled or not.
  • Validation & Guards (#164): Refused for other objectives and for DART, gblinear, Boulevard, EBM, monotone constraints, linear_tree, path_smooth, quantized leaf renewal, SGLB/posterior sampling, process_type="update", and online/budget modes.
  • Convergence (#164): On the bands benchmark at eta = 0.06, q90 achieves lower pinball loss (1.44137 vs. 1.44271) in 3.0× fewer rounds, q10 matches pinball loss in 3.5× fewer rounds, and training both bands takes roughly half the wall time (22.4s vs. 43.3s).

Prediction, Multi-Output Trees & Metadata

  • Complete-Tree Row Walks (#166): Single-row prediction methods (predict_row, predict_row_into, and margin forms) are 2–3× faster, allocation-free on warm calls, and bit-for-bit identical to predict: 48.4 → 18.7 µs warm and 109.1 → 33.0 µs cold on a 4,421-tree ensemble; 132.7 → 45.6 µs warm and 270.2 → 72.5 µs cold on an 11,349-tree ensemble.
  • Typed Tree Metadata (#166): Added RegTree::depth, RegTree::has_categorical_splits, BoostedModel::tree_outputs(), and Python TreeInfo.depth for external tree walkers.
  • Sparse Vector-Leaf Histograms (#163): Multi-output tree nodes (multi_strategy="multi_output_tree") now build histograms by feature sweep on sparse indexes as well as dense, with per-node thread shares and bit-identical output across thread counts. On a 1M × 64 dataset (40% missing, 2 targets, depth 8), training time drops from 107 ms to 56 ms at 16 threads and from 105 ms to 41 ms at 64 threads.

Python Packaging

  • PEP 803 abi3t Wheels (#162): Ships a single abi3t wheel per platform for CPython 3.15+ covering both GIL-enabled and free-threaded builds as well as future 3.15+ releases (free-threaded 3.14 continues to use its dedicated cp314t wheel).

Migration Guide (v0.3.0 -> v0.4.0)

1) Rust Cross-Validation API (CvStopping and CvReport)

Replace early_stopping_rounds(patience) with early_stopping(CvStopping::fold_mean(patience)), and read .results from the returned CvReport when you need Vec<CvResult> (or call .history_only() before .run() if you do not need retained fold models):

use hessboost::training::{CrossValidation, CvStopping, Fold, PurgedTail};
use std::num::NonZeroUsize;

let patience = NonZeroUsize::new(20).unwrap();

// Before (v0.3.0)
let results: Vec<CvResult> = CrossValidation::new(
    &params,
    &data,
    5,
    Fold::k_fold(data.n_rows(), 3, 0)?,
)
.early_stopping_rounds(patience)
.run()?;

// After (v0.4.0) — fold-mean stopping (the previous behavior)
let report = CrossValidation::new(
    &params,
    &data,
    5,
    Fold::k_fold(data.n_rows(), 3, 0)?,
)
.early_stopping(CvStopping::fold_mean(patience))
.run()?;
let results = report.results;

// v0.4.0 — leakage-safe per-fold stopping and out-of-fold margins
let tail = PurgedTail::new(&decision_at, &label_end, 0.2)?;
let report = CrossValidation::new(
    &params,
    &data,
    5,
    Fold::forward_chaining(data.n_rows(), 3, 3)?,
)
.early_stopping(CvStopping::per_fold(patience, tail))
.run()?;
let oof = report.out_of_fold()?;

(In Python, early_stopping_rounds is unchanged for fold-mean mode; pass per_fold_stopping=PurgedTail to cross_validate for per-fold stopping.)

2) Python Version Floor (CPython 3.12+)

Upgrade environments running hessboost >= 0.4.0 to CPython 3.12 or newer (and numpy >= 2.5 on CPython 3.15+). Environments remaining on CPython 3.11 stay on 0.3.0:

# pyproject.toml
requires-python = ">=3.12"
dependencies = [
    "hessboost>=0.4.0",
    "numpy>=2.5; python_version >= '3.15'",
]

3) DMatrix::select_rows Returns a View

DMatrix::select_rows now returns a row-mapped view sharing the parent matrix's storage instead of allocating a copied matrix. Default ViewCuts::Own computes cuts on the selected rows and is bit-for-bit identical to training on a copied matrix; no changes are needed unless you want to share the source matrix's bins and split thresholds via ViewCuts::Source (or dmat.slice(rows, cuts="source") in Python).

Pull Requests by Category

Features

  • perf(data)!: bin once and share (#169)
  • feat!: model averaging, out-of-fold CV, per-fold stopping (#165)
  • feat: exact quantile leaves (#164)
  • perf(predict): complete-tree row walks (#166)
  • perf(multi): vector-leaf histograms on sparse indexes (#163)
  • feat(python)!: abi3t wheels, CPython 3.12 floor (#162)

Bug Fixes

  • fix(python): refuse copy=False over ownerless memory (#170)

Misc

  • docs(cv): PurgedTail cutoff (#167)
  • Test fixture deduplication and zstd compression (#161, #160)
  • chore(release): v0.3.1 (#159)