Building from source¶
You only need this page if a prebuilt wheel does not cover you (Install), you want the bonsai command-line binary, or you are developing bonsai itself.
What a build produces¶
Three artifacts come out of this repo, and they land in separate build trees so the plain CPU tree stays pristine while sanitizer and CUDA variants live beside it:
| artifact | command | lands at | who needs it |
|---|---|---|---|
bonsai CLI binary |
make build |
build/src/bonsai |
anyone using the command line; the wheel does not include it |
| C++ test binary | make test |
build/tests/ (run via ctest) |
contributors |
_bonsai Python module (dev tree) |
make python |
build/python/bonsai/ |
developing against the Python API |
_bonsai Python module (installed) |
pip install . |
your environment's site-packages | using a source-built package like a wheel |
| CUDA variants of all of the above | make build-cuda / python-cuda |
build-cuda/... |
GPU training from a source build |
The trees: build/ (CPU), build-asan/ (sanitizers), build-cuda/ (GPU). make clean removes all three.
Toolchain¶
bonsai is C++23 built with LLVM clang and libc++. The floor is LLVM 20 (std::print, std::mdspan, and float std::from_chars land in libc++ 20; clang 19 relaxed an OpenMP restriction on capturing structured bindings, so clang 18 fails twice over); development and CI run LLVM 21, which is the tested configuration. libc++ rather than libstdc++ because Ubuntu 22.04's system libstdc++ (GCC 11) predates the C++23 pieces above. CMake 3.25+ and Ninja round out the requirements; every C++ dependency (Catch2, CLI11, tomlplusplus, nlohmann/json) is vendored by CMake FetchContent, so there is nothing else to install.
On Ubuntu:
wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh && sudo bash /tmp/llvm.sh 21 && \
sudo apt-get install -y libc++-21-dev libc++abi-21-dev libomp-21-dev \
clang-format-21 clang-tidy-21 clang-tools-21 ninja-build
On macOS:
brew install llvm@21 libomp ninja cmake
The Makefile finds the toolchain at /usr/lib/llvm-21/bin (Linux) or homebrew's llvm@21 keg, falling back to plain llvm (macOS); point it elsewhere with make LLVM_BIN=/path/to/llvm/bin ....
The make-target map¶
Every make target, generated from the Makefile so it never drifts, is on the Make targets page. make help prints the common ones in the terminal.
OpenMP is required, loudly¶
Configure fails hard if OpenMP is missing. This is deliberate: a silent serial fallback once shipped builds that trained different (valid, but not reproducible) model bits than parallel ones and manufactured a phantom cross-architecture bug (decision 60). On macOS, homebrew's libomp is keg-only and CMake is given the hint automatically; on Linux, libomp-21-dev comes with the toolchain block above. A deliberately serial build is -DBONSAI_OPENMP=OFF. BONSAI_OPENMP_STATIC=ON (what wheels use) links the runtime statically so the Python module cannot deadlock against xgboost/lightgbm's bundled OpenMP; on Linux runners no static libomp exists, so the wheel pipeline vendors a private dynamic copy instead (#134). The ASan tree runs in CI only on Linux; homebrew LLVM's ASan deadlocks on current macOS.
The Python module, two ways¶
Installed, wheel-style. pip install . drives scikit-build-core through the same CMake project; pass CMake options with -C:
pip install . -C cmake.define.BONSAI_CUDA=ON -C cmake.define.BONSAI_CUDA_ARCH=sm_120
That example is a local CUDA wheel for one GPU architecture. Requires nanobind and numpy at build time (pip fetches them as build requirements automatically).
Dev tree. make python builds the extension into build/python/bonsai without installing anything; use it with PYTHONPATH=build/python python .... make python-cuda is the same into build-cuda/python. Iterating on C++ then re-running Python tests is fastest this way; pip install . re-resolves the whole build each time.
CUDA builds¶
The CUDA backend needs a CUDA 12.x toolkit at build time only (clang cannot target CUDA 13; hosts shipping 13 need a 12.x side-install pointed at with -DCUDAToolkit_ROOT=...). make build-cuda compiles with BONSAI_CUDA_ARCH=native, which probes the local device, so build on the machine with the GPU or pass an explicit architecture. The knobs, all CMake cache variables:
| variable | meaning | default |
|---|---|---|
BONSAI_CUDA_ARCH |
--offload-arch target(s); a semicolon list builds a fat binary |
native |
BONSAI_CUDA_PTX_ARCH |
embed PTX for this single arch only (forward-JIT floor) | clang's per-arch default |
BONSAI_CUDA_STATIC_RUNTIME |
link cudart statically (what release wheels do) | OFF |
How the backend works (device-resident training, the one kernel TU compiled by the project's own clang) is guide chapter 10 and the architecture note; the wider scaling study lives in the benchmarks README.
When the build fails¶
| symptom | cause | fix |
|---|---|---|
clang 18: errors in <charconv> or OpenMP capture errors |
libc++ < 20 lacks float from_chars; clang < 19 rejects OpenMP capture of structured bindings |
use LLVM 20+, ideally 21 |
| CUDA TU fails with texture-header errors | CUDA 13 toolkit; clang cannot target it | install a CUDA 12.x toolkit, set CUDAToolkit_ROOT |
| macOS link errors about string hash symbols | SDK libc++ older than the compiler's headers | handled in CMakeLists (links the keg's libc++); update homebrew llvm if it recurs |
| configure error: OpenMP not found | intentional, see above | install libomp, or -DBONSAI_OPENMP=OFF deliberately |
CUDA configure fails probing native |
no GPU visible on the build machine | pass -DBONSAI_CUDA_ARCH=sm_80 (or your target) |
Extending bonsai¶
Extending requires a source tree; the recipes below say which files a new component touches.
A stateless grower or sampler is two edits: add the type to the registry typelist and a string to the name table, and the dispatch table, bonsai info listing, and parametric registry tests all expand automatically.
A new objective is about six file touches: the typelist entry, the name, three trait specializations that static assertions demand by name (link function, task kind, default metrics), the implementation itself, and a config field if it carries parameters (plus its TOML section). The parametric tests then cover every (objective, grower, sampler) combination without new test code.
Stateful components implement the same concepts as classes constructed from Config; a new tree type (beyond DenseTree/ObliviousTree) is the one extension that ripples, since model IO versioning is involved. The machinery that makes all this work is the dispatch note.