Skip to content

perf(data)!: bin once and share: cached dual-layout index, row views, bin-routed margins, binned matrices, borrowed arrays - #169

Merged
brndnmtthws merged 16 commits into
mainfrom
perf/bin-once-share
Oct 20, 2026
Merged

brndnmtthws merged 16 commits into
mainfrom
perf/bin-once-share

Conversation

@brndnmtthws

@brndnmtthws brndnmtthws commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

What

Bin each matrix's feature values once and share the result: across trainings of a matrix and its metadata copies, across cross-validation folds and other row selections, and across concurrent trainings. Plus a matrix type that trains from the bins alone (XGBoost's QuantileDMatrix).

Against main (#163, #164, #165, and #166 are merged; this branch supersedes closed #168, whose base branch the #164 merge deleted).

  1. Dual-layout index, streamed cuts (perf(data)): one parallel pass over 1,024-row blocks writes CSR global row bins (u16 up to 65,536 total bins) and, for dense indexes and sparse ones at least half full, feature-major local columns (u8 up to 256 bins per feature, else u16/u32) with a missing-entry bitmap. Cuts stream dense rows in 65,536-row transposed blocks. Every builder keeps its exact per-bin addition order, and sum_order still keys on the old is_dense rule, so no sum changes.
  2. Bin cache (perf(data)): DMatrix::hist_index(max_bin) caches the index with the values, keyed by max_bin, weights, feature types and missing value. The first caller builds it and concurrent callers wait (a parallel build runs on its own pool, so waiting rayon workers cannot deadlock). Clones and with_* copies share it; a request drops the other keys' built indexes nothing else holds (the cache keeps the latest binning asked for, and those a training or binned matrix still uses), and the rest is freed with the values.
  3. Row views (feat(data)!): select_rows returns a view (a row map over the shared storage, with only the metadata gathered). ViewCuts::Own (the default) cuts the view's own rows, bit-identical to a copy of them. ViewCuts::Source instead gathers the view's rows of the source's index, so a source and its views share one binning pass. Python: DMatrix.slice(rows, cuts="own" | "source"). CV folds are views.
  4. Bin-routed margins (perf(training)): sampled rounds and exact leaves route every training row through the finished tree by its bins (BinnedTree) instead of reading scattered raw f32 rows. This happens only when the cuts were sketched from every row (SketchedRows::All); otherwise rows are routed by value, as before. Prediction also skips loading row blocks when the tree range is empty, which every training's initial margins did.
  5. Binned matrices (feat(data)): data::BinnedMatrix / hessboost.BinnedMatrix keeps the index and metadata and drops the values. Trainer::new and train take impl Into<TrainingData> (a DMatrix or a BinnedMatrix), and training on the bins is training on the matrix, bit for bit. Everything that would read values is refused through an allow-list over refuse_changes_from, so options added later are refused until vetted: another max_bin, continuation, device = cuda, tree_method approx/exact, non-gbtree boosters, linear leaves, refresh, SGLB, and reuse penalties. Binning refuses rows with zero weight, whose values the cuts never saw.
  6. Borrowed arrays (feat(python)): DMatrix(x, copy=False) reads a read-only, C-contiguous, aligned float32 array in place, so a memory-mapped .npy (np.load(..., mmap_mode="r")) stays file-backed; the matrix and every copy or slice of it keep the array alive (a BinnedMatrix keeps only the bins). Refused: writeable arrays, non-contiguous ones, arrays whose base chain (followed through memory views and the buffer protocol) can still write the buffer, and buffers another Rust extension holds a mutable borrow of. The core API is DMatrix::from_dense_shared(Arc<dyn AsRef<[f32]> + Send + Sync>, ..); the extension's one unsafe block (python/src/borrowed.rs, unsafe_code lowered from forbid to deny with that block allowed) turns the kept-alive numpy buffer into the slice. The contract, documented on copy: the array, and every array sharing its memory, stays unchanged while a matrix borrows it — numpy offers no way to enforce that (any WRITEABLE flag can be set again), so it is the caller's promise, as in every zero-copy numpy bridge.

Numbers

4M x 173 panel (41.6% NaN), Neoverse-V3, cores pinned; before = 53862434 (the branch this was stacked on: main with #163-#165), after = this branch merged with main. Another workload shared the cores during these runs: memory is unaffected, and the per-round rows are the fastest of 12 runs interleaved with the baseline.

measurement before after
1-round train, 1 thread: first / again / copy+set_info 15.46 / 15.82 / 16.30 s 10.74 / 0.48 / 0.47 s
same, 8 threads 3.22 / 2.53 / 2.29 s 1.80 / 0.15 / 0.13 s
same, 16 threads 1.95 / 1.98 / 1.98 s 0.84 / 0.09 / 0.09 s
q10 quantile round, 8 threads (warm) 84.1 ms 74.9 ms
q10 with exact leaves, 8 threads 98.9 ms 88.2 ms
4 heads x 4 threads sharing one matrix, 10 rounds: peak / time 13.10 GiB / 7.4 s 4.60 GiB / 3.8 s
8 heads x 4 threads 23.54 GiB / 10.6 s 5.10 GiB / 3.9 s
trial (85% of rows + eval set) peak, 8 threads 7.60 GiB (numpy copies) 4.16 GiB (views, own cuts); 5.67 GiB (source cuts)
refit on all rows: peak / time 5.55 GiB / 4.24 s 4.52 GiB / 3.22 s (own); 4.49 GiB / 1.62 s (source: index reused)
BinnedMatrix of the panel, then drop the DMatrix, then 20 rounds — index 1.42 GiB; training peak 1.55 GiB, 1.60 s
4 / 8 heads on one matrix borrowing the memory-mapped array (copy=False) 13.10 / 23.54 GiB 1.99 / 2.54 GiB
trial / refit peaks on views of a borrowing matrix 7.60 / 5.55 GiB 1.59 / 1.96 GiB (own cuts); 3.10 / 1.91 GiB (source cuts)

When copying, the 4- and 8-head peaks are one value copy (2.64 GiB) + one index (1.42 GiB) + about 0.12 GiB per head; borrowing drops the copy, since the memory-mapped pages are file-backed and not counted. A profile of 20 sampled rounds puts the bin-routed margin update at 4.5% of samples, against about 10.5% for the raw walk it replaces (DMatrix::get + predict_row + goes_left). The skipped row loads for an empty tree range were another 7.4%.

Bit identity

Model and margin hashes for 7 configurations (vector quantiles, weighted and dense; 2-target squared error by node; vector lossguide; scalar quantile; scalar squared), each at 1, 4, and 16 threads, are identical to #164 after every commit. tests/binned_matrix.rs trains every option binned training accepts on both the DMatrix and its BinnedMatrix and compares model bytes and eval histories: sampled rows and columns, lossguide, forests, GOSS, quantized gradients with leaf renewal, extra trees, path smoothing, monotone and interaction constraints, exact leaves, vector leaves, multiclass, ranking, and a sparse index without columns, with symmetric trees.

Design notes

  • A columns-only layout (no CSR rows) was tried first: setup fell from 2.43 s to 1.68 s, but rounds went from 82.5 ms to 131 ms at 8 threads. Gathering a node's scattered rows touches one cache line per row per feature. The dual layout keeps row gathers on CSR rows and single-column streaming on 8-bit columns.
  • Routing a row by its bins equals routing it by value only for a row the cuts were sketched from. A zero-weight row's value can lie beyond the last cut (it then clamps into the last bin), and an unseen category clamps to bin 0. DMatrix::sketched_rows (All / Weighted / Partial) replaces the weights-only check. It sees through source-cut views: a view that gives weight to rows its source weighs zero gets Partial, so its training margins route by value. That keeps "train k + j rounds" equal to "train k, then continue for j" (tests/row_views.rs).
  • perf(multi): sweep vector-leaf histograms by feature on sparse indexes #163's sweep policy (each node's thread share, sweep_pays) is carried onto the new column layout in the merge of feat(training): opt-in exact quantile leaves (leaf_estimation_method = exact) #164's head (de492643).

Review

An independent review found five defects, all around source-cut provenance; 0e74a6a8 fixes them and adds a regression test for each:

  • A binned matrix of a source-cut view that weighs rows its unweighted source sketched read values it no longer had.
  • A forked child could hang on a bin cache caught mid-build; it now bypasses inherited caches.
  • Budget mode followed bin partitions for weighted rows the source never sketched; it now refuses such views.
  • with_feature_types on a source-cut view contradicted its cuts; it is now refused.
  • SGLB leaf renewal read those partitions too; it now routes such rows by value.

The re-review of the fixes found nothing further.

A second review (3380d8fe) fixed:

  • Approximate online updates of a source-cut view: the replay state recut the view's own rows, so the model's splits (on its source's cuts) were regrown on every update. It now reads the index the model was trained on (tests/online.rs).
  • The bin cache kept one index per key ever requested, so a max_bin search or bootstrap-weighted copies of one matrix accumulated indexes until the values were freed. Every request now drops the other keys' built indexes nothing else holds (fb2ceb11, after Codex noted that dropping them only on a new key's build kept a wave of concurrently claimed keys).
  • approx's sorted cut summaries held every feature's (value, weight) pairs at once (8 bytes per cell); they are gathered and sorted one column per worker again. Categorical cuts merged the whole distinct prefix every 65,536-row block (quadratic for high-cardinality features); each block's distinct values are now appended and sorted once. Both bit for bit as before.
  • Metal built the global columns before checking the 4 GiB interleaved-copy limit (an oversized sparse dataset allocated first, then was refused); the check comes first again.
  • GHistIndex::from_dmatrix checks the cut table's width in release builds (unsafe consumers index rows by it).
  • Binned continuation is refused as IncompatibleModel, like every other continuation refusal.
  • copy=False accepted a read-only array whose base exposes no buffer (an as_strided result), whose memory may be writeable; it is refused now. The stub's __all__ gains BinnedMatrix, the Python README no longer lists QuantileDMatrix as missing, and docs (BinnedMatrix::weights, max_bin for categorical features, the extension's unsafe note) and tests (the binned CUDA refusal, a source-cut test that tells source from own cuts) are fixed.

Codex's security review of that head noted that resize(refcheck=False) frees an owning array's buffer whatever refers to it. 66d1c194 pins the borrowed array, and every array it views, with weak references, which numpy's resize refuses while a matrix reads the buffer (smoke-checked on numpy 2.5.3: refused while borrowed or sliced, allowed once only a BinnedMatrix remains; numpy 2.5's stubs deprecate in-place resize, so ty rejects a permanent test of it). Writes after setflags(write=True) stay the caller's documented promise: numpy cannot enforce them.

Breaking changes

  • DMatrix::select_rows returns a view that keeps the source's missing value; it no longer converts the selection to NaN-missing.
  • Trainer::new and train take impl Into<TrainingData<'_>>. &DMatrix callers compile unchanged; callers passing &&DMatrix need one deref.
  • Python: Objective is generic over the training matrix type (Objective[M]).

The 0.4.0 bump in #165 covers these.

GHistIndex::column_bins() is Some only for dense indexes, so any matrix
with missing values sent every vector-leaf (multi_output_tree) node
through the serial CSR loop, whatever the thread count.

NodeHistograms now sweeps the feature-major copy whenever the index keeps
one: dense columns, or missing_columns() skipping the width's sentinel.
Tasks of four pool features read each row's split gradients once, and only
the tree's colsample_bytree pool is built (every node's candidate features
are a subset of it). Each bin still adds its rows in one ascending chain,
so models are bit-identical at every thread count.
…= exact)

reg:quantileerror and reg:absoluteerror grow their structure on smooth
surrogates and keep Newton leaves by default. LeafEstimation::Exact
(CatBoost's leaf_estimation_method) sets output j of every leaf to eta
times the alpha_j residual quantile (the median for absolute error) of the
training rows the finished tree routes there: every row, sampled or not,
by raw value, with the intercept's interpolated or weighted-step quantile.
A positive max_delta_step bounds the quantile; lambda, alpha, gamma and
min_child_weight act on the structure only.

Scalar and vector leaves, several levels, label matrices and sample
weights are supported; the all-row partition also adds the tree to the
training margins. dart, gblinear, Boulevard, EBMs, monotone constraints,
linear_tree, path_smooth, quantized leaf renewal, SGLB and refresh are
refused. Python takes it as a params key, the scikit-learn estimators as a
constructor parameter. The default (Newton) path is unchanged bit for bit.
GHistIndex keeps its two layouts, both written by one parallel pass over
1,024-row blocks instead of binning rows and then transposing them:

- rows: each row's present entries as global bins (u16 up to 65,536
  bins, else u32), CSR-shaped, which row-wise histogram gathers read;
- columns (dense indexes and sparse ones at least half full): each
  feature's bins local to the feature, feature-major, in u8 up to 256
  bins per feature (the default max_bin), else u16/u32, plus one bit per
  entry marking missing values, which partitions, vector-leaf sweeps,
  budget/quantized column paths and the GPU uploads read.

The columns were global u16 with a u16::MAX sentinel; local bins with a
separate bitmap fit 256-bin features in a byte. HistCuts streams dense
rows through 65,536-row transposed blocks (every sketch still takes its
feature's values in row order) instead of transposing the whole matrix,
and CSR input goes through its CSC view a feature at a time as before.

Every histogram keeps its per-bin addition order (sum_order is keyed on
the dense flag as before), so models are bit for bit unchanged.
A DMatrix and its metadata copies (with_labels, with_weights, Python's
copy + set_info) share their feature values; they now also share the
histogram indexes binned from them. DMatrix::hist_index(max_bin) keys a
BinCache by max_bin, sample weights, feature types and missing value, and
hist training, EBM pair ranking and budget training read their index
from it, so repeated and concurrent trainings of one matrix bin once. The
index is freed with the values; new values (dense_values_mut, map_values,
select_rows) start a new cache.

The first request for a key builds it and concurrent requests wait. A
parallel build runs on a pool of its own and publishes from there: a
builder waiting on its own rayon joins may steal a waiter (two trainings
of one matrix started from one pool), which would otherwise wait on its
own stack forever. Serial builds (small matrices, one thread) never enter
rayon and run on the caller; a builder that cannot start threads builds
unshared and its waiters build their own.

A cache hit is the index a miss builds, so training is bit for bit
unchanged.
…n's margins

With `leaf_estimation_method = exact`, `multi_output_tree`, and
`num_parallel_tree > 1`, each vector-leaf tree was added to the training
margins before the next one was fitted, so later trees of the iteration took
their residual quantiles against margins that already held the earlier ones
while their gradients came from the iteration's start. The vector path now
grows all of an iteration's trees before any joins the margins, as the
scalar path does; Newton leaves never read the margins, so their models are
unchanged.

The exact-leaf reference tests now cover forests (scalar and vector leaves,
`eta / num_parallel_tree`, outputs grouped by forest) and every builder
(hist depth-wise and symmetric, approx, exact).
`DMatrix::select_rows` now returns a view: the selected rows' metadata is
gathered, the feature values stay shared with the source and are read
through a composing row map. Raw-value readers go through the row-mapped
`DenseRows`/`CsrRows` accessors, and writing a view's values
(`dense_values_mut`) copies only its rows. Cross-validation folds,
target-statistics folds and every other internal selection are views.

`select_rows_with_cuts(rows, ViewCuts)` picks the view's histogram cuts:
`ViewCuts::Own` (the default) cuts the view's rows, so training on a view
is training on a copy of its rows, bit for bit; `ViewCuts::Source`
gathers the view's rows of the source's cached index
(`GHistIndex::select_rows`), so a source and its views share one binning
pass and split on the same thresholds. Python: `DMatrix.slice(rows,
cuts="own" | "source")`.

The routing guard of the `hist` builder's row partitions is now
`DMatrix::sketched_rows`: a source-cut view of rows its source weighs zero
holds values its cuts never saw, so those rows' training margins route by
value (and linear leaves and device rounds keep off the partitions), which
keeps training equal to continuing it.

BREAKING CHANGE: `select_rows` keeps the source's missing value instead of
converting the selection to NaN-missing.
A sampled round's margin update and exact leaves' row routing walked every
training row through the new tree by raw value, reading scattered row-major
`f32`s. When the index's cuts were sketched from every row (no zero sample
weight, `SketchedRows::All`), a row's bins route it through any tree split
on those cuts as its values do, so both now walk the index's narrow feature
columns instead (`tree::binned::BinnedTree`, `margins::RowRouting`); every
row still receives one addition of the same leaf value, so models are
unchanged bit for bit. Trees whose thresholds fall inside a bin
(`HistCuts::bins_left_of`), linear leaves, refreshed trees, sparse indexes
without columns, and eval sets keep routing by value.

`HistCuts::categories_left` replaces the builder's private
`category_left`, now shared by partitions, device growth, and the walker.

Prediction no longer loads every row block when summing an empty tree
range, which every training's initial margins did.
`data::BinnedMatrix` (Python `hessboost.BinnedMatrix`) keeps a matrix's
`hist` index and metadata and drops the values, as XGBoost's
`QuantileDMatrix` does: `BinnedMatrix::new(&dmatrix, max_bin)` takes the
index the matrix's own training builds (shared with it, so binning after a
training costs nothing), and training on it is training on the matrix, bit
for bit. On the 4M x 173 panel the index takes 1.4 GiB against 2.6 GiB of
values, and 20 rounds of quantile training peak at 1.56 GiB once the
`DMatrix` is dropped.

`Trainer::new` and `train` take `impl Into<TrainingData>`, a `DMatrix` or a
`BinnedMatrix`. Training refuses what would read the values it dropped:
another `max_bin`, continuation, `device = cuda` (whose recovery routes rows
by value), and every option outside an allow-list of what `hist` growth on
bins serves (`validate_binned_request` over `refuse_changes_from`, so a new
option is refused until it is vetted). Binning refuses zero-weight rows,
whose values the cuts never saw. The weights and feature types that shaped
the cuts are fixed; labels, groups, margins, bounds, and feature weights
can be set. The values-free storage (`Storage::Bins`) panics if a value is
ever read, so a missed refusal fails loudly.

Training rows route through finished trees by their bins on every index
layout now: `BinnedTree` also walks a sparse index's row bins.

Python: `train` is generic over the matrix, and a custom objective receives
it (`Objective[M]`); prediction, eval sets, and `cv` refuse a
`BinnedMatrix` with a `TypeError`. `DMatrix` and `BinnedMatrix` share their
metadata readers (`_Metadata`).
…eaves

# Conflicts:
#	AGENTS.md
#	src/data/sort.rs
#	src/tree/builder/multi.rs
Review fixes for the bin sharing:

- A binned matrix's rows are all sketched (`SketchedRows::All`): binning
  proved it, and its weights are fixed. Re-deriving it from the weights
  failed for a source-cut view that weighs rows its unweighted source
  sketched anyway, sending its training to read values it no longer has.
- A forked child bypasses the bin caches it inherits: the fork may have
  caught one mid-build (its builder thread did not fork) or with a lock
  held, either of which would hang the child's first training.
- Budget mode refuses a source-cut view that weighs rows its source never
  sketched: its margins and loss decrements follow the builder's
  partitions, which route those rows by bin, not value.
- A source-cut view keeps its source's feature types, which its cuts were
  computed for (`with_feature_types` refuses others).
- SGLB leaf re-estimation reads the builder's partitions under the same
  guard as the margins, routing rows by value for such views (scalar and
  vector leaves).
`DMatrix(x, copy=False)` reads a read-only, C-contiguous, aligned float32
numpy array in place instead of copying it: a memory-mapped `.npy`
(`np.load(..., mmap_mode="r")`) then stays file-backed, and the matrix, its
copies, and its slices keep the array alive. Refused (`HessboostError`):
writeable arrays, non-contiguous ones, arrays whose base chain (through
memory views, and the buffer-protocol owner at its end) can still write the
buffer, and buffers another Rust extension holds a mutable borrow of;
other types and dtypes are refused (`TypeError`). The caller keeps the
array, and every array sharing its memory, unchanged while a matrix
borrows it: numpy lets any WRITEABLE flag be set again, so that is a
contract, as in every zero-copy numpy bridge.

The core gains `DMatrix::from_dense_shared(Arc<dyn AsRef<[f32]> + Send +
Sync>, ..)`: dense storage holds owned values or a caller's buffer through
its owner (`DenseValues`), checked once, and copied on write. The
extension's one `unsafe` block, `python/src/borrowed.rs`, turns the kept-
alive numpy buffer into that slice; `unsafe_code` is now `deny` with that
block alone allowed.

On the 4M x 173 panel, 4 and 8 heads training on one borrowing matrix
peak at 2.01 and 2.55 GiB (4.54 and 5.13 GiB copying), and the
trial-then-refit flow on its views at 1.57 / 1.91 GiB.
# Conflicts:
#	AGENTS.md
#	src/training/exact_leaves.rs
#	src/training/multi_output.rs
#	src/training/round.rs
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-20T20:56:58.275920Z fb2ceb1 Manual request
🔒 Security Review ✅ Completed 2026-10-20T20:57:59.767610Z fb2ceb1 Manual request

Security findings

Advisory findings (1)

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Review fixes for the bin-once-share changes:

- Online: the approximate mode's replay state now reads the index the
  model was trained on (`DMatrix::hist_index`). It recut the matrix's own
  rows, so a `ViewCuts::Source` view's splits, found on its source's cuts,
  were not cuts of the replay state: every update regrew them on the
  view's own cuts and refused values below the source's top cut.
- Bin cache: a matrix keeps its latest binning, not one index for every
  `max_bin`, weighting, or feature typing ever trained. A new key replaces
  the built ones (those being built stay for their waiters), so a
  `max_bin` search or bootstrap-weighted copies no longer pile up indexes
  until the values are freed.
- Cuts: `approx`'s sorted summaries transpose the rows once and gather and
  sort one column of `(value, weight)` pairs per worker again, instead of
  holding every feature's pairs at once (8 bytes per cell). Categorical
  cuts append each block's distinct values and sort them once, instead of
  merging the whole prefix into a new vector every block (quadratic for
  high-cardinality features). Both bit for bit as before.
- `GHistIndex::from_dmatrix` checks that the cut table covers the data's
  features in release builds too: the unsafe histogram loops and the
  device backends index rows by its width.
- Metal: the interleaved copy's 4 GiB limit is checked before the global
  columns it is built from are materialized, as main did for sparse
  indexes, so an oversized dataset is refused without the allocation.
- Binned training: continuation is refused as `IncompatibleModel`, as
  every other continuation refusal is.
- Python: `DMatrix(copy=False)` also refuses an array whose base exposes
  no buffer to ask (an `as_strided` result, a capsule), which may hide
  writeable memory; a memory view without an exporter is asked itself.
  The stub exports `BinnedMatrix`; the README no longer lists
  `QuantileDMatrix` as missing and states that `DMatrix` copies unless
  `copy=False`.
- Docs and tests: `BinnedMatrix`'s `weights` and `max_bin` docs, the
  extension's `unsafe` note, the binned CUDA refusal, and a source-cut test
  whose fixture tells source cuts from own cuts.
@brndnmtthws

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: 3380d8fe1e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ Codex Security Review · Automatically triggered

Here are some automated security review suggestions for this pull request.

Reviewed commit: 3380d8fe1e

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Comment thread python/src/borrowed.rs Resolved
`resize(refcheck=False)` reallocates, and so frees, an owning array's
buffer whatever still refers to it, so a `DMatrix(copy=False)` over the
array (or a view of it) could read freed memory. numpy refuses to resize
an array anything holds a weak reference to: `BorrowedArray` now holds one
for the array and every array it is a view of, for as long as a matrix
reads the buffer. Writes, and replacing an array's state in place, stay
the caller's documented promise.
@brndnmtthws

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 66d1c19403

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/data/bin_cache.rs Resolved
@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 66d1c19403

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

A request dropped the other built indexes only when it built a new key,
so binnings claimed together (concurrent trainings under several
`max_bin`s or weightings) stayed cached after their trainings finished,
as long as later requests hit one of them. Every request now drops the
other keys' built indexes nothing besides the cache holds: the cache
keeps the latest binning asked for, the ones a training or binned matrix
still uses (sharing those costs no memory), and those being built.
@brndnmtthws

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: fb2ceb1179

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: fb2ceb1179

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@brndnmtthws
brndnmtthws merged commit 470b86a into main Oct 20, 2026
38 checks passed
@brndnmtthws
brndnmtthws deleted the perf/bin-once-share branch October 20, 2026 21:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant