bosk 0.1.0

Pure-Rust LightGBM inference: parses the text model format directly — no FFI, zero deps. Optional ONNX Runtime and CatBoost backends behind the same small Model trait.
Documentation

bosk

A bosk is a small thicket of trees — here, an ensemble of decision trees.

Gradient-boosting model inference behind one small trait — load a trained LightGBM, ONNX, or CatBoost model and call predict.

The headline is the LightGBM backend: it parses the .lgb text format directly, with no C dependency and no FFI to lib_lightgbm. Building with --no-default-features gives you a LightGBM predictor with zero dependencies — trivial to cross-compile, embed, or drop into a musl target.

use bosk::{load_model, Model};

let model = load_model(std::path::Path::new("model.lgb"))?;
let p = model.predict(&features)?;           // probability for a binary classifier
let ps = model.predict_batch(&flat, n)?;     // row-major batch, n features per row

Backends

Extension Backend Feature flag Extra deps
.lgb / .txt pure-Rust LightGBM text parser always on none (zero deps)
.onnx ONNX Runtime via ort onnx (default) prebuilt ONNX Runtime
.cbm CatBoost via catboost-rust catboost (default) native CatBoost

load_model auto-detects the format by extension; supported_extensions() reports what the current build can load. The default build enables onnx and catboost. For the zero-C-dependency LightGBM-only build:

bosk = { version = "0.1", default-features = false }

A .cbm loaded through load_model is assumed to be a binary classifier (the CatBoost C API does not expose the trained loss function). For a CatBoost regression or ranking model, construct the backend explicitly: CatBoostModel::load(path, Output::Raw).

The Model trait

pub trait Model: Send + Sync {
    fn predict(&self, features: &[f64]) -> bosk::Result<f64>;
    fn predict_batch(&self, flat: &[f64], n_features: usize) -> bosk::Result<Vec<f64>> { /* default */ }
}

predict_batch takes the samples row-major in one flat slice — no per-row allocation — and backends override it with a single native batched run (ONNX, CatBoost). predict checks the feature count against the model (LightGBM's max_feature_idx, CatBoost's float-feature count, the ONNX graph's declared input dimension when static) and refuses a wrong-length vector with Error::FeatureCount instead of silently treating the tail as missing. On the LightGBM path that refusal is the only possible error — evaluation itself cannot fail once the model is loaded and validated. Where an infallible call matters more than the length check, hold a concrete LgbModel and use predict_unchecked (out-of-range feature indices are then evaluated as missing; the objective transform is still applied — it is not a raw-score prediction).

LightGBM coverage

The pure-Rust parser reproduces LightGBM's own predictions bit-for-bit (verified against LightGBM 4.6 across the full objective matrix):

  • numerical splits with full missing-value semantics (default direction + missing type: none/zero/NaN, including the kZeroThreshold zero band);
  • categorical splits (bitset lookup), including unseen, negative, and NaN categories;
  • random-forest averaging (average_output);
  • the output transform of every single-output objective: binary (honoring its sigmoid parameter), cross_entropy, cross_entropy_lambda, poisson / gamma / tweedie (exponential link), the regression family (including reg_sqrt), and ranking (raw scores).

Models it cannot evaluate faithfully are rejected at load with Error::Unsupported rather than mispredicted silently: multiclass (num_class > 1), linear trees (is_linear=1), unrecognised objectives, and unrecognised objective tokens (a token like sqrt can change the output transform, so an unknown one is never skipped). The token whitelist is audited against every LightGBM release from 2.1 through 4.6 — the complete vocabulary those versions can write is sqrt, sigmoid:, and num_class: — so a token refusal can only occur for a model written by a future LightGBM version. Each tree's structure is validated at load — including that the child pointers form an actual tree, so a corrupted file cannot send predict into an infinite loop or a panic.

Testing

cargo test --no-default-features   # pure-Rust LightGBM path + parity fixtures
cargo test                         # + onnx and catboost backend parity tests

Every backend is tested against its reference implementation: the LightGBM parity fixtures assert the pure-Rust prediction matches LightGBM 4.6 to 1e-9 (including missing-value, zero-as-missing, categorical, and reg_sqrt models), the CatBoost fixtures pin Output::Probability / Output::Raw against CatBoost 1.2.10, and the ONNX fixture checks the exported graph end to end. See tests/fixtures/README.md to regenerate fixtures.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.