Skip to content

Releases: daniel-m-campos/bonsai

v1.5.4

Choose a tag to compare

@daniel-m-campos daniel-m-campos released this 31 Jul 02:15

The benchmark harness becomes a tool, and its first campaign ships in the ledger.

Added

  • The unified bench CLI: python -m bonsai.bench run --spec <name-or-path> executes declarative cell specs (explicit cells or generators, variants, threads, per-device repeats, gates, timeout caps) over the existing worker protocol; plan prints the expansion without fitting, variants prints the registry, and spec-mode runs resume by default (finished rows skip, failures re-attempt). The iso_volume generator holds rows x cols constant and sweeps aspect ratio. Custom cell ladders no longer require source edits.
  • Campaign specs ship in the wheel: the committed specs are package data under bench/specs/, --spec resolves a bare name against them (python -m bonsai.bench run --spec iso-volume-2026-08 works from any install; a filesystem path still wins), and python -m bonsai.bench specs lists what ships.
  • Measured peak device memory: a sampler thread (NVML, nvidia-smi fallback) records per-process and whole-device VRAM peaks into a dev_mem field on every GPU row, on error and OOM rows too, with the sampling interval recorded so rows never claim more precision than sampled. --data-cache DIR memoizes the synthetic generator as memmapped .npy files (bit-identical fits, verified).
  • One variant registry (bonsai.bench.variants): canonical names, per-suite membership, and the historical alias spellings in one table; params.bonsai_core closes the last hand-built config path. Worker children report the reference-library versions they imported, so rows finally carry them.
  • A committed pod campaign driver (scripts/pod_bench_driver.sh): clone-or-fetch, Blackwell-aware CUDA 12.8 side-install, build, run a committed spec, resume-safe.

Measured

  • The iso-volume shape frontier (decision 91, RTX PRO 6000 Blackwell 96GB): bonsai's CUDA growers fastest at every cell of the 2^31 and 2^33 constant-volume ladders, near-flat across the tall half where the references vary 1.5-2x. The instrument's first catches: XGBoost-GPU dies at 32k x 65536 having allocated 33.4GB of a 96GB card, and CatBoost-GPU reserves 90.2GB at every shape. At p ~ 2x n the oblivious grower holds test r2 .873 where depthwise-family growers fall to .815.

Docs

  • The results ledger is per-suite pages: the old URL is now a landing with perf-first division summaries linking one generated page per study; the README states each division in one table; navigation gains section landings and door cards.

v1.5.3

Choose a tag to compare

@daniel-m-campos daniel-m-campos released this 30 Jul 19:01

The wide-data wall falls on both processors, and the standings prove it.

Changed

  • One tiled fill replaces the row-wise/feature-parallel pair (decision 89): the binned mirror moved to a column-block-tiled layout (2048-feature blocks) and the histogram fill runs tiles outer, rows inner, so the live scatter target is one block's histograms at any width. In an interleaved same-pod A/B the tiled fill beat the row path at its best cell (326 vs 369s at 1M x 4096), beat feature-parallel at its best cell (442 vs 514s at 131k x 16384), and tied at 16M x 100. Models are bit-identical to 1.5.2 at every width, so the 24MB routing threshold and its cache-size follow-up are retired outright. Evidence: benchmarks/wide-cpu-hist-2026-07.md.
  • The bin mapper radix-sorts its cut subsample: an LSD byte-radix on the order-preserving key transform replaces std::sort above 2k samples, cutting mapper fit from 2.85 to 2.12s at 131k x 4096 on an M2 (the mapper cost 11.5s of a 54.9s GPU fit at 16k features, decision 90's price list). Cuts and models are byte-identical; small columns keep std::sort.

Measured

  • The CUDA wide wall was stale data (decision 90): the recorded ~5x wide-GPU deficit dated to 2026-07-08 code, before the device-resident objective landed. On current main, same pod, bonsai's CUDA growers lead every wide cell against both CatBoost-GPU and XGBoost-GPU at 3-4x less host memory.
  • Six-variant cols re-baseline (decision 90 follow-up): one pod, four cells from 1M x 100 to 131k x 16384; bonsai CUDA fastest at every width (50.5s vs 71.3/77.3s at the widest) and the wide standings now carry their chart in the ledger.

Docs

  • Engine chapter E6 epilogue: how the tiled mirror dissolves the wall the chapter builds, with the A/B that proved it.

v1.5.2

Choose a tag to compare

@daniel-m-campos daniel-m-campos released this 30 Jul 13:22

Wide data on CPU gets 2.6-5.8x faster, from the first production field report.

Changed

  • Ultra-wide CPU fits route through the feature-parallel fill (decision 88, issue #217): u8 levels whose selected-histogram footprint exceeds 24MB (more than ~12k features at 255 bins) stop using the row-wise fill, whose per-row scatter breaks every cache at that width. Same-pod at 131k x 16384, t=16: depthwise 1019s to 379s (LightGBM-CPU parity), leafwise 2591s to 445s (a 7x deficit collapses to 1.2x), at 2.7x less peak memory than LightGBM (18.8 vs 50.1GB). Narrow and mid-width fits are code-identical (fixed-input models byte-identical); above the threshold, models change bytes at identical accuracy and become bit-identical at any thread count. The threshold was set by an interleaved same-pod A/B; the cache-size-aware refinement is recorded on issue #217. Evidence: benchmarks/wide-cpu-hist-2026-07.md.

Docs

  • Engine track chapter E6, the wide-data wall: the two fills as one arithmetic with the scatter on opposite sides, the footprint table behind the cache cliff, and the two refuted theories on the way to the shipped constant.

v1.5.1

Choose a tag to compare

@daniel-m-campos daniel-m-campos released this 30 Jul 02:23

Package hygiene and benchmark fairness, driven by the first field reports from production use.

Changed

  • The Python package is properly moduled: __init__.py (849 lines of implementation) is now a ~50-line public surface over estimators.py, _compat.py (the XGBoost translation tables), and _coerce.py. Public API unchanged (same nine names); pickles from earlier versions load bit-identically. The lite design review behind it: docs/reviews/2026-07-30-design-review-python.md.
  • Benchmark harness fairness fixes (adversarial review, docs/reviews/2026-07-30-review-benchmark-fairness.md): the CatBoost borders-vs-bins fencepost now lives in one place (catboost_core, guard-tested) after call-site translations drifted three ways; XGBoost and LightGBM no longer run one bin short of bonsai in the airline and gpu-pareto suites; grinsztajn pins bonsai to the references' thread count. A rented-GPU re-check of both airline knob shapes confirmed no published cell depended on the drift (deltas within ±0.0022 of zero, all cited leads stand). The protocol page now states the classification leaf-floor caveats.

Docs

  • Running the benchmarks (docs/use/benchmarks.md): the installed-wheel and source-tree paths side by side, all four suites documented (the airline ladder for the first time), and where result rows land.

v1.5.0

Choose a tag to compare

@daniel-m-campos daniel-m-campos released this 29 Jul 21:47

On PyPI as bonsai-gbt, and the XGBoost drop-in surface.

Added

  • PyPI via trusted publishing: pip install bonsai-gbt with no --find-links. Releases upload through OIDC (no stored tokens); the CUDA-enabled linux x86_64 wheel reaches the index only after the rented-GPU release gate passes, and a TestPyPI rehearsal path (workflow dispatch) proved the flow end to end before the first real upload.
  • The XGBoost drop-in surface: the canonical XGBRegressor/XGBClassifier script runs with only the class name swapped. New aliases min_child_weight (tree.min_child_hess, the same minimum hessian mass per child), gamma, subsample (switches on the bernoulli sampler), and device="cuda" (picks the matching CUDA grower); XGBoost objective strings on both estimators (reg:squarederror, reg:quantileerror with quantile_alpha, binary:logistic, multi:softprob, ...); eval_set in the list-of-tuples form; evals_result(), best_iteration, and best_score backed by a per-round eval history (in-memory only, the model format is untouched; squared error is presented as rmse, the exact root); save_model/load_model/apply/iteration_range; n_features_in_. The full swap table and the deliberate differences: Switching from XGBoost.

Measured

  • XGBoost 3.3 recheck (decision 87): every published standing survives against the 2026-07-21 release on a same-pod three-arm ladder; the one real competitor gain (wide-CPU histogram tiling, 2x at 1M x 4096) flips no published cell. The 16M host-memory ratio (22.1 vs 6.9GB) reproduced on a second host.

v1.4.0 — the device-resident objective ships

Choose a tag to compare

@daniel-m-campos daniel-m-campos released this 19 Jul 13:14

GPU fits 15 to 25% faster through the device-resident objective, and the documentation rebuilt around results.

Added

  • Device-resident objective for CUDA fits (decision 77, issue #171): eligible fits (MSE, LogLoss, or Poisson; sample weights supported; no DART; row sampling off or Bernoulli) keep labels and scores on the GPU, derive each tree's gradients there, and fuse the score update into the tree epilogue, so per tree nothing crosses the bus in either direction. Measured same-pod: 20-25% faster fits at 16M rows, 15-16% at 64M, with identical accuracy (full-data resident models are bit-identical to the host-objective GPU models). Ineligible fits and BONSAI_HOST_OBJECTIVE=1 take the unchanged host path; CPU models are byte-identical.
  • CUDA device selection (issue #158): parallel.device_id places a cuda_* grower's whole fit (ingest and training) on a specific GPU, so multi-GPU hosts can pin concurrent fits to different cards without CUDA_VISIBLE_DEVICES. 0 is the default device and behavior-preserving; an out-of-range id, or a nonzero id on a CUDA-less build, raises ConfigError. Placement only: model bits are unaffected and the knob is not persisted in the model artifact. CPU growers ignore it (the sampler.subsample convention for regime-scoped knobs). First step of the single-node multi-GPU track (architecture doc 19).
  • Explicit bin edges on Dataset (decision 73, architecture doc 18): bonsai.Dataset(X, y, bin_edges={col: edges}) bins listed columns at user-supplied cut points (regulatory bands, clinical thresholds, incumbent-scheme parity) instead of fitted quantiles. The edges travel inside the model artifact like fitted cuts, so predict, save, and load work on raw values with no external transform; k edges give k+1 splittable bands plus the NaN-only missing bin. Unlisted columns fit exactly as before (byte-identical models when the argument is omitted).

Docs

  • The site rebuilt around five doors (issue #179): results-first standings with three drift-gated generated references (parameters, make targets, the GBT timeline), the Engine track teaching five real perf campaigns case-method, an honest What-to-use-when including competitor recommendations, CI-enforced style and CI-executed examples, and the Design core from system map to determinism contract.

Fixed

  • Weighted fits no longer pay a serial per-tree weight multiply: the host gradient weighting loop is now parallel (elementwise, bitwise-identical models); at 16M rows it was costing roughly 80ms per boosting round of single-threaded work on every weighted fit.
  • The missing bin is NaN-only on every fitting path (decision 74, issue #155): fitted cuts now end with a FLT_MAX top-band closer, so finite values above the last cut (a stride path's top tail, a capped column's heavy maximum, rows beyond the bin sample) get a real splittable bin instead of training as missing, which also removes a train/predict routing skew for those rows. Models change only where a leak existed; capped-column synthetics recover up to half the lost variance, and on the re-validated Grinsztajn standings bonsai's mean rank improves 1.73 to 1.44 with 36 of 55 outright wins and no last-place finishes (evidence: benchmarks/missing-bin-closer-2026-07.md).

v1.3.0 — GPU training from a 2.3MB pip install

Choose a tag to compare

@daniel-m-campos daniel-m-campos released this 14 Jul 18:01

GPU training from a 2.3MB pip install, and the benchmark harness ships in the package.

Added

  • CUDA in the linux x86_64 wheel (decision 70): GPU training out of the box on any NVIDIA driver R525+, SASS for sm_70 through sm_120 plus a compute_90 PTX forward-JIT floor, cudart statically linked. The whole backend costs 2.33MB of wheel (vs ~300MB for xgboost's GPU wheel) and behaves exactly like a CPU wheel on GPU-less machines. This wheel was validated on rented GPU hardware before it attached; the byte-identity model-hash gate runs across all three wheel platforms on every build.
  • Runtime docker image: ghcr.io/daniel-m-campos/bonsai:cuda with the CUDA wheel preinstalled, RunPod-ready (sshd entrypoint); the release gate boots this exact image, so the image and the wheel are validated together.
  • bonsai.bench in the wheel (decision 69): pip install bonsai-gbt[bench] reproduces the published benchmark tables; python -m bonsai.bench.grinsztajn out.jsonl --report re-runs the external standings suite. Normative rules in the benchmark charter.

Fixed

  • Model.n_classes reports 0 unless the model was trained with the softmax objective.
  • Linux wheels vendored a dynamic libomp while claiming static linkage; the workflow now documents the vendoring honestly (#134).

Changed

  • CLI fit no longer runs the binning pass on validation sets (#119): per-iteration eval reads features and labels only, so the pass was pure waste in every early-stopping run.

Full changelog: CHANGELOG.md

v1.3.0rc1 — CUDA wheels dry run

Pre-release

Choose a tag to compare

@daniel-m-campos daniel-m-campos released this 14 Jul 17:14

Dry run of the automated CUDA release flow (issue #99, decision 70): full wheel fan, hash-compare, CPU attach, candidate runtime image, RunPod validation gate, promote. Pre-release; artifacts here are for gate verification, not use. The first validate-cuda attempt is expected to fail until the GHCR bonsai package is flipped public.

v1.2.0 — wheels, reusable Dataset, classifier fixes

Choose a tag to compare

@daniel-m-campos daniel-m-campos released this 14 Jul 01:00

Install without a toolchain, reuse binning across fits, and a round of classifier correctness fixes surfaced by an adversarial post-release review.

Prebuilt wheels are attached below — Linux x86_64/aarch64 (manylinux_2_35, Ubuntu 22.04+/Debian 12+) and macOS arm64, Python 3.9–3.13. pip install the wheel for your platform; no LLVM/CMake needed. CPU-only — GPU training remains a source build.

Added

  • Python 3.9 supportrequires-python lowered to 3.9; full binding suite verified on CPython 3.9.25.
  • Prebuilt wheels on GitHub Releases — libc++ vendored into the wheel, OpenMP statically linked; every wheel smoke-tested in a clean venv (fit, predict_proba, Dataset, save/from_file) before shipping.
  • Reusable pre-binned bonsai.Dataset — bin once, train many: ds = bonsai.Dataset(X, y); bonsai.train(params, ds) skips the per-fit bin pass across a hyperparameter search or CV loop, bit-identical to fitting from (X, y). On GPU the resident-matrix upload-skip cache now fires across fits. Bin settings are sealed at construction — bin_mapper.* overrides are rejected whether they arrive as params or inside a config file.
  • Multiclass predict_proba(n, K) row-wise softmax probabilities; completes BonsaiClassifier.
  • xgboost/lightgbm-style constructor aliases on both estimators: n_estimators, num_leaves, random_state, n_jobs, reg_lambda, reg_alpha, max_bin, min_child_samples, colsample_bytree.
  • Model.objective_name / Model.n_classes read-only properties.

Fixed

  • Multiclass sample_weight was silently ignored — the softmax gradient/hessian loop never applied per-row weights; a weighted 3-class fit was bit-identical to unweighted. Weights now scale grad/hess; unweighted fits are unchanged bit-for-bit.
  • BonsaiClassifier.from_file crashed predict/predict_proba — class metadata was only set by fit. Restored from the saved model as encoded ids 0..K-1 (xgboost's load_model convention; pickle preserves original label values), and non-classifier models are rejected instead of mislabeled.
  • eval_set labels absent from the training classes now raise instead of silently corrupting the validation metric and early stopping. NaN labels are rejected like sklearn.

Docs

  • README: superseded benchmark content removed; stale facts corrected.

Full details in CHANGELOG.md.

https://claude.ai/code/session_01SfgWZ7ftZV7aJJh87NXR72

v1.1.0 — crown-week release

Choose a tag to compare

@daniel-m-campos daniel-m-campos released this 13 Jul 19:24

The crown-week release: measured parity-or-better against xgboost, lightgbm, and catboost, plus a scikit-learn-shaped Python surface.

Added

  • BonsaiClassifier — sklearn-style classifier over the engine's logloss (binary) and softmax (multiclass) objectives. Binary predict_proba; multiclass predict returns labels (multiclass predict_proba is a tracked follow-up). Arbitrary label types are encoded/decoded via classes_.
  • scikit-learn estimator compatibility for BonsaiRegressor/BonsaiClassifierget_params/set_params/score, and drop-in use in clone, Pipeline, GridSearchCV, cross_val_score, and pickle — implemented without a scikit-learn runtime dependency (import bonsai never imports sklearn).
  • sample_weight on BonsaiRegressor.fit / bonsai.train (sklearn convention) — per-row weighting of gradients and hessians.
  • OrderedTargetEncoder — leak-free ordered target statistics for categorical features, including cross=2 pair encodings (decision 58; guide chapter 13).
  • Poisson regression objective (closes #44).

Changed / Performance

  • Binning: one shared row sample for the whole matrix instead of a per-feature reservoir pass — 24× faster mapper-fit at 16M, quality-neutral (decision 64).
  • CPU fill loop software prefetch — the 16M-row fit now ties xgboost-hist (decision 61).
  • Fresh same-pod re-baseline (decision 62–64): bonsai's GPU oblivious grower now edges catboost and beats xgboost-GPU at 16M at matched accuracy and ~3× less host memory, and holds the fastest slot at every row scale.

Fixed

  • GPU oblivious grower carried a split-selection defect (a missing port of the issue-#60 fix) that silently cost ~0.011 test r² at depth ≥ 5; now matches its CPU twin exactly (decision 63).
  • Cross-architecture bit determinism: models are byte-identical across arm64/x86-64 at a fixed thread count — no floating-point contraction on the host plane (decisions 59–60), enforced by a cross-arch CI gate.
  • OpenMP build variance can no longer be silent — a missing OpenMP is now a hard configure error, not a quiet serial fallback that changed model bits (decision 60).
  • Quality-campaign correctness fixes: count-weighted cuts for heavy-value columns (#63/decision 57), one cut per distinct value on duplicate-heavy columns (#61), infeasible frontier nodes contribute zero gain rather than vetoing the split (#60), and the true diagonal softmax hessian p(1−p) (#62).

Docs

  • New guide material (categoricals, structure-vs-scheme) and a claims-and-proofs table in the README linking every performance/quality claim to a reproducible run and the decision that records it.
  • Note: pyproject.toml had drifted to 0.6.0 after the v1.0.0 tag; corrected to 1.1.0 here.