Repository navigation
Exp/tiled hist mip - #124
Merged
Merged
Conversation
Collaborator
Author
|
Timings on dedicated compute comparing this PR to 0.7.0
|
Port of 6c6bc21 (exp/tiled-hist, on main) to feat/model-identical-perf. With tiled_histograms on, the booster keeps a second copy of the binned data in tiles of up to 32 columns, each storing its rows' bins together. Nodes with at least 8 tasks (tiles x row segments of 2048+ rows, at most 16 segments) build histograms row by row: each task sums a tile's columns over a segment into a scratch histogram with an overflow slot per column, and the segments are added in a fixed order, so results don't depend on the thread count. Other nodes use the column-wise fill. Tiles reach depth-wise levels and LossGuide splits alike. Sampled trees read rows in place instead of using RowSubset. Off by default. Exposed in Python as tiled_histograms; perf_phases gains --tiled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The tiled fill reads every column of the tiles it touches, so with colsample_bytree 0.1 on 800 columns it made fits 14-18% slower. Histograms now use it only when they use at least a quarter of those tiles' columns; on 250k x 800 the crossover is between colsample 0.2 (+2%) and 0.3 (-7%). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jinlow
force-pushed
the
exp/tiled-hist-mip
branch
from
October 13, 2026 12:29
3874616 to
2bfc671
Compare
jinlow
approved these changes
Oct 13, 2026
jinlow
left a comment
Owner
There was a problem hiding this comment.
Very cool, and makes sense why this would lead to speed ups. Also agree with having it opt-in because of the extra memory is uses over the default.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Opt-in tiled, row-wise histograms (stacked on #123)
Summary
Adds an opt-in
tiled_histogramssetting (set_tiled_histogramsin Rust,tiled_histograms=Truein Python). When it's on, large nodes build histograms row by row from a tiled copy of the binned data, instead of a column at a time.main: with Feat/model identical perf #123 underneath, most cases are 38–51% faster thanmain.Where the gains come from
1. The column-wise fill is limited by scattered reads
The default fill (
fill_feature_histogram) loops over features. For each feature it walks the node's rows and reads each row's bin from that feature's column.2. A tile puts a row's bins in one cache line
TiledBinskeeps a second copy of the bins, split into tiles of up to 32 consecutive columns and stored row by row within each tile. 32u16bins are 64 bytes, one cache line.histmethod blocks columns by 32 the same way (kColBlockSizeinsrc/common/hist_util.cc) when a full-row histogram doesn't fit in cache.3. Each task's histogram fits in cache
Tiles are capped at 32 columns and 8,192 bins. A task's scratch histogram (f64 gradient and hessian sums) is then at most about 130 KB and stays in L2.
Why not plain row-wise: a row-wise fill over all columns doesn't fit. At 800 columns each thread's histogram is about 3.3 MB.
Kernel probe at 250k × 800 (histogram time per tree):
Larger tiles were slower at every width tested. Histogram time per tree (ms) on
main, summed over a tree's nodes (mainbuilds nodes one after another):4. Enough parallel work for any node, with fixed results
A task is one tile × one row segment.
LossGuidesplit) has work for 16 threads.5. Kernel details that made the difference
The first version barely helped: it saved 0–22 ms of tree time per iteration on the cases below. The final kernel saves 14–83 ms. The changes:
+inf). Those slots are dropped at the end, as the column-wise fill drops those values.Measured: the saving is all in building trees
Tree time per iteration, base vs. this PR (
perf_phases --mode phases, depth 6, 15 iterations):Over 30 iterations, this accounts for nearly all of the end-to-end savings below (for example, 2.3 s of the 2.5 s at 4M × 50). Other phases run the same code, with two exceptions:
The saving doesn't depend on #123
The same change on
mainsaves about the same time per fit:mainResults
Median fit time over 3 interleaved rounds:
mainmainmainfrom this PRLossGuide64 leavescolsample_bytree0.5colsample_bytree0.1With the missing branch (1M × 50, 30 iterations, one run), tiling cuts the fit from 2.57 to 1.88 s with the same log loss.
Where it doesn't help
Small data. At 100k rows few nodes have enough work for tiles, so most histograms stay column-wise.
Few sampled columns on wide data. A tile row is read whole, so with few sampled columns most of each read is wasted. Histograms fall back to column-wise when they use under a quarter of the columns in the tiles they touch (second commit). Crossover at 250k × 800, without the fallback:
colsample_bytreeAt 0.1 the fallback means tiles are never used, but the tile copy is still built, which costs 6% vs. Feat/model identical perf #123.
Costs
Commits
18a0c61TiledBins(inbinning), the tiled fill andHistogramMatrix::build, which chooses between the tiled and column-wise fills. Tiles are passed throughTree::fit_with_tilesto depth-wise levels andLossGuidesplits. Adds thetiled_histogramsparameter in Rust and Python, and--tiledforperf_phases.3874616colsample_bytreefallback above).API notes
GradientBooster::tiled_histogramsfield. It defaults to off when loading older models.set_tiled_histograms,Tree::fit_with_tilesandbinning::TiledBins.Tree::fitis unchanged.Splitter::split_nodeandhandle_split_infotake an extratiles: Option<&TiledBins>argument.tiled_histogramsargument. It's included inget_params()and survives save and load.Testing
Rust:
cargo testpasses 71 tests, 7 of them new:cargo fmt --checkpasses; clippy reports no new warnings.Python: 122 tests pass, including the new
test_tiled_histograms.ruff checkandruff format --checkpass.Golden checks:
scripts/perf_golden.pywith tiling off matches Feat/model identical perf #123 on all 15 configurations.Not done
colsample_bytreeis below 0.25, which removes the 6% cost above.nbins <= 256, which would halve the tile memory and bandwidth.Benchmark setup
examples/perf_phases.rs --mode fit, depth 6, learning rate 0.1, 256 bins, one 50k-row evaluation set scored every iteration. 30 iterations (100 at 100k rows, 40 with GOSS); median of 3 interleaved rounds.scripts/make_perf_data.py: continuous, discrete and zero-inflated columns, with missing values in about 30% of columns.maincolumn:mainwith tiling off, plus a--colsampleflag forperf_phases. Its models are byte-identical tomain.To reproduce:
python scripts/make_perf_data.py --out /tmp/forust-perf/w50_4m --rows 4000000 --eval-rows 50000 --cols 50 cargo run --release --example perf_phases -- --mode fit --data /tmp/forust-perf/w50_4m \ --iterations 30 --max-depth 6 --tiled true