dd training callbacks, feature validation, and update XGBoost API integration' or 'Implement callback mechanism and refactor gradient/hessian passing with array interfaces - #12
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds iteration-aware custom-objective training, per-round callbacks with early-stop, JSON array-interface marshaling for gradients/hessians, feature-name count validation, and tests updated to use the new BinaryError metric. ChangesBooster & Training
DMatrix & IO
Sequence Diagram(s)sequenceDiagram
participant Trainer as "Trainer"
participant Booster as "Booster"
participant Obj as "Custom Objective"
participant Eval as "Evaluator"
participant Callback as "Training Callback"
Trainer->>Booster: train(dtrain, params with callbacks)
loop per iteration
Booster->>Obj: compute objective(dtrain, iteration)
Obj-->>Booster: gradients, hessians
Booster->>Booster: make_array_interface(grad, hess)
Booster->>Eval: evaluate(dtrain)
Eval-->>Booster: evaluation_results
Booster->>Callback: invoke callback(&CallbackEnv)
alt callback returns false
Callback-->>Booster: false
Booster-->>Trainer: stop training early
else callback returns true
Callback-->>Booster: true
Booster->>Booster: XGBoosterTrainOneIter(..., iteration)
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/booster.rs (1)
233-243: Custom eval results are stored under the wrong key.
dmat_eval_resultsis documented as dataset → metric, but the custom eval path insertseval_nameas the outer key anddmat_nameas the inner key, swapping axes. This produces incorrect output (e.g.,custom-train) and makes callback data inconsistent.🐛 Fix key placement
- let eval_results = dmat_eval_results - .entry(eval_name.to_string()) - .or_insert_with(IndexMap::new); - eval_results.insert(dmat_name.to_string(), eval_result); + let eval_results = dmat_eval_results + .entry(dmat_name.to_string()) + .or_insert_with(IndexMap::new); + eval_results.insert(eval_name.to_string(), eval_result);src/dmatrix.rs (1)
167-221: Design clarity improvement: Document that CSR/CSC inputs are copied by XGBoost.The current implementation passes raw pointers to XGBoost's C API, which could appear unsafe without documentation. However, XGBoost copies the data during the FFI call (consistent with
from_densewhich uses the identical pattern), making the borrowed-slice API safe. Add a documentation comment explicitly stating that input buffers are copied and can be safely dropped after the call, or consider adding a safety note similar tonew_with_cached_dmats()inBooster.
🤖 Fix all issues with AI agents
In `@src/dmatrix.rs`:
- Around line 258-265: The code in load_binary currently uses
path.as_ref().to_string_lossy(), which can silently alter non‑UTF8 paths; change
it to require valid UTF‑8 by using path.as_ref().to_str() and return an error if
it returns None (or map to a Result) before building the JSON config; then use
the obtained &str (escape backslashes/quotes as you already do) to create the
JSON config and CString, keeping the rest (config_cstr and
XGDMatrixCreateFromURI call) unchanged — reference symbols: load_binary, path,
escaped_path, config, config_cstr, and XGDMatrixCreateFromURI.
- Around line 12-46: The current make_array_interface_usize produces
platform-dependent typestr and can yield 4-byte types on 32-bit targets, which
breaks XGBoost's expectation of 64-bit CSR/CSC indices; change the API to use
fixed-width u64: replace or overload make_array_interface_usize to accept &[u64]
(or add new make_array_interface_u64) and ensure callers that pass indptr and
indices convert/collect their values to Vec<u64> before calling (update call
sites that currently pass indptr/indices slices), then emit typestr "<u8"
unconditionally so the array interface always encodes 8-byte unsigned indices.
♻️ Duplicate comments (1)
src/dmatrix.rs (1)
409-415: Array-interface lifetime/typing verification (same as above).
🧹 Nitpick comments (3)
Cargo.toml (1)
11-24: Confirm MSRV and prerelease dependency intent.Edition 2024 (Line 11) raises MSRV and libc 1.0.0-alpha.2 is a pre-release. Please confirm the minimum supported Rust version is documented/CI-updated and that the alpha dependency is intentional; otherwise consider a stable libc version.
src/booster.rs (2)
13-28: Optional: centralize array-interface helper to reduce duplication.
make_array_interfaceduplicates the f32 helper insrc/dmatrix.rs. Consider a shared utility to keep the spec in one place.
516-543: Consider validating features for the standard update path too.
validate_featuresis only invoked inboost(custom objective). If the goal is to guard all training flows, call it fromupdateor at the start of thetrainloop.
|
Hey @marcomq, I was wondering if you could take a look at this PR as I use it in one of my projects. I don't believe it breaks anything as the tests still pass. Thanks |
marcomq
left a comment
There was a problem hiding this comment.
Thank you very much for the PR. It looks good and seems to be a good step forward.
However, it might introduce some breaking changes and might have performance impact as it internally uses now different functions that work with strings. These from_csc / from_dense functions are usually called in the inner loop and as we don't have performance benchmarks yet, I would prefer to not apply these changes silently.
Can you try to create new separate functions for the changed from_csc / from_dense, so it doesn't affect current functionality.
| /// # Errors | ||
| /// | ||
| /// Returns an error if the path contains non-UTF8 characters. | ||
| pub fn load_binary<P: AsRef<Path>>(path: P) -> XGBResult<Self> { |
There was a problem hiding this comment.
This is a breaking change as it uses another C function that uses urls instead of files. For consistency, we should also use a new function here too. Let's keep the old function as it is and add load_binary_from_uri
| documentation = "https://docs.rs/xgboost_lib" | ||
| readme = "README.md" | ||
| edition = "2021" | ||
| edition = "2024" |
There was a problem hiding this comment.
This is modern, but might be a breaking change for others. Can we keep it at 2021?
| log = "0.4" | ||
| tempfile = "3.15" | ||
| indexmap = "2.7" | ||
| libc = "1.0.0-alpha.2" |
There was a problem hiding this comment.
I don't feel good about using an alpha version. Is this really necessary?
| tempfile = "3.15" | ||
| indexmap = "2.7" | ||
| libc = "1.0.0-alpha.2" | ||
| derive_builder = "0.20.2" |
There was a problem hiding this comment.
xgboost is used as a library. Just keep "0.20" / "0.4". Rust will automatically try to select the latest most compatible version.
Only update it if necessary or breaking change.
In your later app, you can specify a more recent version, if you like to.
| let data_cstr = ffi::CString::new(data_interface).unwrap(); | ||
| let config = ffi::CString::new(r#"{"missing": NaN}"#).unwrap(); | ||
|
|
||
| xgb_call!(xgboost_sys::XGDMatrixCreateFromCSR( |
There was a problem hiding this comment.
Generally a good change.
But this looks like it may affect performance negatively as we are now allocating new strings for all the input. We must avoid undocumented perforformance regressions. Please create a new function, maybe from_dense_new or similar.
| let data_cstr = ffi::CString::new(data_interface).unwrap(); | ||
| let config = ffi::CString::new(r#"{"missing": NaN}"#).unwrap(); | ||
|
|
||
| xgb_call!(xgboost_sys::XGDMatrixCreateFromCSC( |
There was a problem hiding this comment.
Same here, this may have a negative performance impact.
…ost_sys api without hurting performance
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/dmatrix.rs (1)
169-202: Breaking API change not addressed per maintainer feedback.The maintainer previously requested keeping the old function and adding a new one (e.g.,
from_csr_new) to avoid undocumented performance regressions from the new string allocations. The signature change from&[usize]to&[u64]is also a breaking change for existing callers.Consider either:
- Restoring the old
from_csrimplementation and addingfrom_csr_v2or similar- Adding a deprecation notice and migration guide if replacing is intentional
🤖 Fix all issues with AI agents
In `@benches/dmatrix_benchmark.rs`:
- Line 240: Update the printed threshold text so it matches the library logic:
change the println call that currently outputs "Threshold: 5000 non-zeros
(single-thread below, multi-thread above)" to state 30000 non-zeros instead;
locate and edit the println! invocation in benches/dmatrix_benchmark.rs (the
line that prints the threshold message) to reflect "Threshold: 30000 non-zeros
(single-thread below, multi-thread above)".
- Around line 122-130: The SINGLE_THREAD_THRESHOLD in from_csr_auto_tuned is set
to 5000 which diverges from the library implementation and bench_table.rs;
update the threshold constant used in from_csr_auto_tuned to 30000 so it matches
DMatrix::from_csr's behavior and the example benchmark; ensure the constant name
SINGLE_THREAD_THRESHOLD remains unchanged and that any logic using it continues
to select single-thread mode for sizes below 30000.
- Around line 306-310: The hardcoded 5000 threshold in the optimal selection
logic (the expression setting optimal using nnz, single_us, multi_us) must be
replaced with a named constant and updated to the correct threshold value;
introduce a const (e.g., OPTIMAL_NNZ_THRESHOLD) near the benchmarking constants
and use it in the condition: if (nnz < OPTIMAL_NNZ_THRESHOLD && single_us <=
multi_us) || (nnz >= OPTIMAL_NNZ_THRESHOLD && multi_us <= single_us) so the
threshold can be changed centrally and set to the updated value required by the
benchmark.
- Around line 329-333: There is a duplicate main when feature "print_table" is
enabled: the standalone fn main calling print_comparison_table conflicts with
the main generated by the Criterion macro; to fix it, gate one of them with cfg
so only one main is compiled — for example add #[cfg(not(feature =
"print_table"))] around the Criterion entry (criterion_main!(benches)) or
alternatively add #[cfg(feature = "print_table")] to the table-printer main;
reference the standalone main that calls print_comparison_table and the
criterion_main!(benches) macro when applying the cfg.
In `@BENCHMARK_VERIFICATION.md`:
- Around line 30-38: The fenced code block showing expected output (the block
starting with "Rows | NNZ | Deprecated (us)" in BENCHMARK_VERIFICATION.md) lacks
a language specifier; update that fenced block to include a language tag such as
"text" or "plaintext" (e.g., change ``` to ```text) so the static analysis
requirement for annotated code blocks is satisfied and the expected-output block
is treated as plain text.
In `@examples/basic/src/main.rs`:
- Around line 68-74: The printed "error=" value is actually computing accuracy
(num_correct as f32 / preds.len() as f32) which is misleading; update the
calculation in the println to use the true error rate by computing 1.0 -
(num_correct as f32 / preds.len() as f32) or (preds.len() - num_correct) as f32
/ preds.len() so the label "error=" matches the value (refer to the variables
num_correct and preds and the println! call).
In `@examples/bench_deprecated.rs`:
- Around line 12-13: The file is missing imports for ptr::null_mut and
Instant::now; add the appropriate imports (for example import std::ptr or
std::ptr::null_mut and std::time::Instant) so calls to ptr::null_mut() and
Instant::now() resolve. Update the top of the file near the existing extern
crate xgboost_sys declaration to include these imports so functions referencing
ptr::null_mut() and Instant::now() compile.
- Around line 54-56: The FFI call to xgboost_sys::XGDMatrixCreateFromCSREx is
incorrectly casting a Vec<u64> pointer to *const u32 (the indices variable)
causing misinterpreted memory; fix by converting or recreating indices as
Vec<u32> (or mapping indices.iter().map(|v| *v as u32).collect() immediately
before the call) and pass that u32 buffer's pointer to XGDMatrixCreateFromCSREx,
ensuring the indptr/indices types match the expected C signature.
- Around line 53-63: The call to XGDMatrixCreateFromCSREx is passing mismatched
pointer types and ignoring its return code: cast indptr.as_ptr() to *const
size_t, ensure indices is a &[u32] (or perform a checked conversion from &[u64]
to Vec<u32] to avoid truncation) and pass indices.as_ptr() as *const u32, keep
data.as_ptr(), indptr.len(), data.len(), num_cols and &mut handle as before,
then capture the returned int (e.g., let ret =
xgboost_sys::XGDMatrixCreateFromCSREx(...)) and check if ret != 0 and handle or
propagate the error (panic or map to Result) instead of ignoring it.
In `@examples/bench_table.rs`:
- Around line 75-86: The call to the FFI function XGDMatrixCreateFromCSR
currently ignores its return code and may leave handle null; update the code to
check the function's return value and verify handle is non-null before returning
or continuing. Specifically, capture the return code from
XGDMatrixCreateFromCSR, handle non-zero/error codes by returning an Err or
panicking with a clear message, and if the call appears to fail ensure you do
not call XGDMatrixFree on a null handle; alternatively return a Result from the
surrounding function so callers can handle failures instead of unconditionally
returning handle.
♻️ Duplicate comments (3)
Cargo.toml (2)
15-19: Prefer semver-compatible ranges over exact version pinning for library dependencies.Exact version pinning (e.g.,
libc = "0.2.180") in a library crate can cause dependency resolution conflicts for downstream users. Consider using semver ranges like"0.2"or"0.4"to allow Cargo to select compatible versions, as mentioned in previous review feedback.♻️ Suggested change
-libc = "0.2.180" -derive_builder = "0.20.2" -log = "0.4.29" -tempfile = "3.24.0" -indexmap = "2.13.0" +libc = "0.2" +derive_builder = "0.20" +log = "0.4" +tempfile = "3" +indexmap = "2"
11-11: Rust edition 2024 may cause compatibility issues for downstream users.Edition 2024 is very recent and may introduce breaking changes for users who haven't upgraded their toolchains. For a library crate, consider keeping
edition = "2021"for broader compatibility, as mentioned in previous review feedback.src/dmatrix.rs (1)
216-249: Same breaking change concern asfrom_csr.This has the same issue - the signature change and new allocations weren't addressed per the maintainer's feedback requesting a new function to preserve backward compatibility.
🧹 Nitpick comments (4)
examples/bench_table.rs (1)
34-59: Code duplication with benches/dmatrix_benchmark.rs.The
generate_sparse_datafunction is duplicated verbatim acrossexamples/bench_table.rs,benches/dmatrix_benchmark.rs, andexamples/bench_deprecated.rs. Consider extracting this to a shared module (e.g., abench_utilsmodule) to improve maintainability.examples/bench_deprecated.rs (1)
15-41: Duplicatedgenerate_sparse_datafunction.This is the third copy of this function (also in
examples/bench_table.rsandbenches/dmatrix_benchmark.rs). As noted earlier, consider extracting to a shared utility module.benches/dmatrix_benchmark.rs (2)
18-36: Consider extracting shared array interface helpers.These helper functions are duplicated from
src/dmatrix.rs. Consider extracting them to a shared internal module to avoid drift and reduce maintenance burden.
171-177: Consider adjusting test cases for 30k threshold.With the library's threshold of 30000, only "large_50k_nnz" and "xlarge_100k_nnz" would trigger multi-threaded mode in auto-tuned. Consider adding a test case near the 30k boundary (e.g., 25k and 35k nnz) to better validate the threshold behavior.
| You should see output like: | ||
| ``` | ||
| Rows | NNZ | Deprecated (us) | ||
| ---------+----------+---------------- | ||
| 100 | 979 | 56.00 | ||
| 1000 | 10037 | 487.00 | ||
| 2000 | 19849 | 980.00 | ||
| ... | ||
| ``` |
There was a problem hiding this comment.
Add language specifier to fenced code blocks.
Per static analysis, fenced code blocks should have a language specified. For expected output, use text or plaintext.
📝 Suggested fix
You should see output like:
-```
+```text
Rows | NNZ | Deprecated (us)
---------+----------+----------------
100 | 979 | 56.00📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| You should see output like: | |
| ``` | |
| Rows | NNZ | Deprecated (us) | |
| ---------+----------+---------------- | |
| 100 | 979 | 56.00 | |
| 1000 | 10037 | 487.00 | |
| 2000 | 19849 | 980.00 | |
| ... | |
| ``` | |
| You should see output like: |
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
35-35: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
In `@BENCHMARK_VERIFICATION.md` around lines 30 - 38, The fenced code block
showing expected output (the block starting with "Rows | NNZ | Deprecated (us)"
in BENCHMARK_VERIFICATION.md) lacks a language specifier; update that fenced
block to include a language tag such as "text" or "plaintext" (e.g., change ```
to ```text) so the static analysis requirement for annotated code blocks is
satisfied and the expected-output block is treated as plain text.
|
|
||
| extern crate xgboost_sys; |
There was a problem hiding this comment.
Missing required imports.
The file uses ptr::null_mut() and Instant::now() but doesn't import std::ptr or std::time::Instant. This will cause compilation errors.
🐛 Proposed fix
extern crate xgboost_sys;
+
+use std::ptr;
+use std::time::Instant;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| extern crate xgboost_sys; | |
| extern crate xgboost_sys; | |
| use std::ptr; | |
| use std::time::Instant; |
🤖 Prompt for AI Agents
In `@examples/bench_deprecated.rs` around lines 12 - 13, The file is missing
imports for ptr::null_mut and Instant::now; add the appropriate imports (for
example import std::ptr or std::ptr::null_mut and std::time::Instant) so calls
to ptr::null_mut() and Instant::now() resolve. Update the top of the file near
the existing extern crate xgboost_sys declaration to include these imports so
functions referencing ptr::null_mut() and Instant::now() compile.
| unsafe { | ||
| xgboost_sys::XGDMatrixCreateFromCSREx( | ||
| indptr.as_ptr(), | ||
| indices.as_ptr() as *const u32, | ||
| data.as_ptr(), | ||
| indptr.len(), | ||
| data.len(), | ||
| num_cols, | ||
| &mut handle, | ||
| ); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "XGDMatrixCreateFromCSREx" --type rust -A 10Repository: marcomq/rust-xgboost
Length of output: 3036
🏁 Script executed:
fd -e rs -e toml | xargs rg -l "XGDMatrixCreateFromCSREx" | head -20Repository: marcomq/rust-xgboost
Length of output: 92
🏁 Script executed:
# Search for xgboost-sys as a dependency
rg "xgboost.sys|xgboost_sys" --type toml -A 5Repository: marcomq/rust-xgboost
Length of output: 600
🏁 Script executed:
# Search for function definition in bindings or lib files
rg -n "fn XGDMatrixCreateFromCSREx|extern.*XGDMatrixCreateFromCSREx" -A 5Repository: marcomq/rust-xgboost
Length of output: 46
🏁 Script executed:
# Explore xgboost-sys directory structure
find xgboost-sys -name "*.rs" | head -20Repository: marcomq/rust-xgboost
Length of output: 107
🏁 Script executed:
# Search specifically in xgboost-sys for the function
rg -n "XGDMatrixCreateFromCSREx" xgboost-sys/ -A 5Repository: marcomq/rust-xgboost
Length of output: 46
🏁 Script executed:
# Read xgboost-sys lib.rs to understand bindings
cat xgboost-sys/src/lib.rsRepository: marcomq/rust-xgboost
Length of output: 1057
🏁 Script executed:
# Check build.rs to see how bindings are generated
head -100 xgboost-sys/build.rsRepository: marcomq/rust-xgboost
Length of output: 4529
🏁 Script executed:
# Find the C header file
find . -name "c_api.h" -o -name "c_api.h*"Repository: marcomq/rust-xgboost
Length of output: 46
🏁 Script executed:
# Search for XGDMatrixCreateFromCSREx in header files
rg -n "XGDMatrixCreateFromCSREx" --type c --type h -B 2 -A 10Repository: marcomq/rust-xgboost
Length of output: 212
🏁 Script executed:
# Search for any documentation or comments mentioning parameter types
rg -n "indptr|indices|XGDMatrix" examples/bench_deprecated.rs -B 5 -A 5Repository: marcomq/rust-xgboost
Length of output: 4411
🏁 Script executed:
# Look for type definitions in the codebase
rg -n "size_t|bindex_t" --type rustRepository: marcomq/rust-xgboost
Length of output: 46
🏁 Script executed:
# Check how other XGDMatrix functions handle return values
rg -n "XGDMatrix.*unsafe" -A 3 examples/Repository: marcomq/rust-xgboost
Length of output: 46
🌐 Web query:
XGDMatrixCreateFromCSREx XGBoost C API signature parameters
💡 Result:
int XGDMatrixCreateFromCSREx(
const size_t* indptr,
const unsigned* indices,
const float* data,
size_t nindptr,
size_t nelem,
size_t num_col,
DMatrixHandle* out
);[1]
Parameters
indptr: CSR row pointer array (length =nindptr). [2]indices: CSR column indices for each non-zero (length =nelem). [2]data: CSR non-zero values (length =nelem). [2]nindptr: number of rows + 1. [2]nelem: number of non-zero elements. [2]num_col: number of columns (if0, XGBoost will try to infer it). [2]out: outputDMatrixHandle(created DMatrix). [2]
Note: XGDMatrixCreateFromCSREx is deprecated since XGBoost 2.0.0; the recommended API is XGDMatrixCreateFromCSR() (array-interface based). [1]
🏁 Script executed:
# Search for any comments or documentation files about the deprecated API
rg -n "CSR|deprecated" --type md -A 5Repository: marcomq/rust-xgboost
Length of output: 7608
🏁 Script executed:
# Look at the actual function signature from test cases if they exist
rg -n "XGDMatrixCreateFromCSREx|XGDMatrixCreateFromCSR" -B 10 -A 5Repository: marcomq/rust-xgboost
Length of output: 17944
Fix type mismatches and add error handling for deprecated API call.
The XGDMatrixCreateFromCSREx function expects const size_t* indptr and const unsigned* indices, but the code passes:
indptr.as_ptr()whereindptris&[u64]— size_t is platform-dependent (32-bit on some platforms, 64-bit on others)indices.as_ptr()as&[u64]cast to*const u32— loses data when converting u64 to u32
Additionally, the function returns an error code (int) that is completely ignored. Cast both arrays to the correct pointer types and check the return value:
Example fix
unsafe {
let ret = xgboost_sys::XGDMatrixCreateFromCSREx(
indptr.as_ptr() as *const size_t,
indices.as_ptr() as *const u32, // or validate no data loss
data.as_ptr(),
indptr.len(),
data.len(),
num_cols,
&mut handle,
);
if ret != 0 {
panic!("XGDMatrixCreateFromCSREx failed with error code: {}", ret);
}
}🤖 Prompt for AI Agents
In `@examples/bench_deprecated.rs` around lines 53 - 63, The call to
XGDMatrixCreateFromCSREx is passing mismatched pointer types and ignoring its
return code: cast indptr.as_ptr() to *const size_t, ensure indices is a &[u32]
(or perform a checked conversion from &[u64] to Vec<u32] to avoid truncation)
and pass indices.as_ptr() as *const u32, keep data.as_ptr(), indptr.len(),
data.len(), num_cols and &mut handle as before, then capture the returned int
(e.g., let ret = xgboost_sys::XGDMatrixCreateFromCSREx(...)) and check if ret !=
0 and handle or propagate the error (panic or map to Result) instead of ignoring
it.
| xgboost_sys::XGDMatrixCreateFromCSREx( | ||
| indptr.as_ptr(), | ||
| indices.as_ptr() as *const u32, |
There was a problem hiding this comment.
Dangerous pointer cast from u64* to u32*.
The indices vector contains u64 values, but line 56 casts the pointer to *const u32. This reinterprets the memory layout incorrectly—each 64-bit index will be read as two 32-bit values, producing garbage data and potential memory access violations.
Either generate indices as Vec<u32> or convert the data before the FFI call.
🐛 Proposed fix - convert indices to u32
#[cfg(feature = "deprecated_api")]
fn from_csr_deprecated(
indptr: &[u64],
indices: &[u64],
data: &[f32],
num_cols: usize,
num_rows: usize,
) -> xgboost_sys::DMatrixHandle {
let mut handle = ptr::null_mut();
+
+ // Convert indices to u32 for deprecated API
+ let indices_u32: Vec<u32> = indices.iter().map(|&x| x as u32).collect();
unsafe {
xgboost_sys::XGDMatrixCreateFromCSREx(
indptr.as_ptr(),
- indices.as_ptr() as *const u32,
+ indices_u32.as_ptr(),
data.as_ptr(),
indptr.len(),
data.len(),
num_cols,
&mut handle,
);
}
handle
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| xgboost_sys::XGDMatrixCreateFromCSREx( | |
| indptr.as_ptr(), | |
| indices.as_ptr() as *const u32, | |
| #[cfg(feature = "deprecated_api")] | |
| fn from_csr_deprecated( | |
| indptr: &[u64], | |
| indices: &[u64], | |
| data: &[f32], | |
| num_cols: usize, | |
| num_rows: usize, | |
| ) -> xgboost_sys::DMatrixHandle { | |
| let mut handle = ptr::null_mut(); | |
| // Convert indices to u32 for deprecated API | |
| let indices_u32: Vec<u32> = indices.iter().map(|&x| x as u32).collect(); | |
| unsafe { | |
| xgboost_sys::XGDMatrixCreateFromCSREx( | |
| indptr.as_ptr(), | |
| indices_u32.as_ptr(), | |
| data.as_ptr(), | |
| indptr.len(), | |
| data.len(), | |
| num_cols, | |
| &mut handle, | |
| ); | |
| } | |
| handle | |
| } |
🤖 Prompt for AI Agents
In `@examples/bench_deprecated.rs` around lines 54 - 56, The FFI call to
xgboost_sys::XGDMatrixCreateFromCSREx is incorrectly casting a Vec<u64> pointer
to *const u32 (the indices variable) causing misinterpreted memory; fix by
converting or recreating indices as Vec<u32> (or mapping indices.iter().map(|v|
*v as u32).collect() immediately before the call) and pass that u32 buffer's
pointer to XGDMatrixCreateFromCSREx, ensuring the indptr/indices types match the
expected C signature.
| unsafe { | ||
| xgboost_sys::XGDMatrixCreateFromCSR( | ||
| indptr_json.as_ptr(), | ||
| indices_json.as_ptr(), | ||
| data_json.as_ptr(), | ||
| num_cols as xgboost_sys::bst_ulong, | ||
| config.as_ptr(), | ||
| &mut handle, | ||
| ); | ||
| } | ||
| handle | ||
| } |
There was a problem hiding this comment.
Missing error handling for XGBoost FFI calls.
The return value of XGDMatrixCreateFromCSR is not checked. If the call fails, handle remains null, which could cause undefined behavior when passed to XGDMatrixFree. Consider checking the return code or at least asserting the handle is non-null after creation.
🛠️ Suggested fix
unsafe {
- xgboost_sys::XGDMatrixCreateFromCSR(
+ let ret = xgboost_sys::XGDMatrixCreateFromCSR(
indptr_json.as_ptr(),
indices_json.as_ptr(),
data_json.as_ptr(),
num_cols as xgboost_sys::bst_ulong,
config.as_ptr(),
&mut handle,
);
+ assert!(ret == 0 && !handle.is_null(), "DMatrix creation failed");
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| unsafe { | |
| xgboost_sys::XGDMatrixCreateFromCSR( | |
| indptr_json.as_ptr(), | |
| indices_json.as_ptr(), | |
| data_json.as_ptr(), | |
| num_cols as xgboost_sys::bst_ulong, | |
| config.as_ptr(), | |
| &mut handle, | |
| ); | |
| } | |
| handle | |
| } | |
| unsafe { | |
| let ret = xgboost_sys::XGDMatrixCreateFromCSR( | |
| indptr_json.as_ptr(), | |
| indices_json.as_ptr(), | |
| data_json.as_ptr(), | |
| num_cols as xgboost_sys::bst_ulong, | |
| config.as_ptr(), | |
| &mut handle, | |
| ); | |
| assert!(ret == 0 && !handle.is_null(), "DMatrix creation failed"); | |
| } | |
| handle | |
| } |
🤖 Prompt for AI Agents
In `@examples/bench_table.rs` around lines 75 - 86, The call to the FFI function
XGDMatrixCreateFromCSR currently ignores its return code and may leave handle
null; update the code to check the function's return value and verify handle is
non-null before returning or continuing. Specifically, capture the return code
from XGDMatrixCreateFromCSR, handle non-zero/error codes by returning an Err or
panicking with a clear message, and if the call appears to fail ensure you do
not call XGDMatrixFree on a null handle; alternatively return a Result from the
surrounding function so callers can handle failures instead of unconditionally
returning handle.
| pub fn load_binary<P: AsRef<Path>>(path: P) -> XGBResult<Self> { | ||
| debug!("Loading DMatrix from: {}", path.as_ref().display()); | ||
| let mut handle = ptr::null_mut(); | ||
| let fname = crate::path_to_c_str(path); | ||
| xgb_call!(xgboost_sys::XGDMatrixCreateFromFile(fname.as_ptr(), 1, &mut handle)).unwrap(); | ||
| // Use XGDMatrixCreateFromURI with a JSON config specifying the URI | ||
| // Binary format is auto-detected, no format parameter needed | ||
| let path_str = path | ||
| .as_ref() | ||
| .to_str() | ||
| .ok_or_else(|| XGBError::new("Path contains non-UTF8 characters"))?; | ||
| // Escape backslashes and quotes for valid JSON | ||
| let escaped_path = path_str.replace('\\', "\\\\").replace('"', "\\\""); | ||
| let config = format!(r#"{{"uri": "{}", "silent": 1}}"#, escaped_path); | ||
| let config_cstr = ffi::CString::new(config).unwrap(); | ||
| xgb_call!(xgboost_sys::XGDMatrixCreateFromURI(config_cstr.as_ptr(), &mut handle))?; | ||
| DMatrix::new(handle) | ||
| } |
There was a problem hiding this comment.
Function renamed per maintainer request not implemented.
The maintainer requested keeping the old load_binary function and adding a new load_binary_from_uri function since this uses a different C function (URI-based). The current implementation replaces the old behavior, which is a breaking change.
Update dev-dependency to the latest patch release of criterion to include recent performance improvements and bug fixes.
XGBoost requires the gradient and hessian to be declared as a 2D [num_row, n_targets] array for multi-target boosters (e.g., distributional models). This patch updates make_array_interface to emit the correct shape when n_targets > 1, inferred from the gradient buffer length relative to the number of rows.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/booster.rs (2)
244-253:⚠️ Potential issue | 🟠 Major | ⚡ Quick winStore custom evals under the dataset key.
dmat_eval_resultsisdmat_name -> eval_name -> scoreeverywhere else, but this block inserts custom metrics ascustom -> dmat_name -> score. That makes callback data inconsistent and printscustom-traininstead oftrain-custom.💡 Proposed fix
if let Some(eval_fn) = params.custom_evaluation_fn { let eval_name = "custom"; for (dmat, dmat_name) in eval_sets { let margin = bst.predict_margin(dmat)?; let eval_result = eval_fn(&margin, dmat); let eval_results = dmat_eval_results - .entry(eval_name.to_string()) + .entry(dmat_name.to_string()) .or_insert_with(IndexMap::new); - eval_results.insert(dmat_name.to_string(), eval_result); + eval_results.insert(eval_name.to_string(), eval_result); } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/booster.rs` around lines 244 - 253, The custom evaluation block currently inserts results under the evaluation-name key ("custom") causing the map shape to be eval_name -> dmat_name -> score; change it to store under the dataset key so the shape matches the rest (dmat_name -> eval_name -> score): inside the loop over eval_sets (where params.custom_evaluation_fn, eval_sets, bst.predict_margin, eval_result, dmat_name and dmat_eval_results are used) replace the entry lookup that uses eval_name as the top-level key with one that uses dmat_name.to_string() and then insert eval_name.to_string() -> eval_result into that inner IndexMap so callbacks and prints become dataset-first (e.g., "train-custom" instead of "custom-train").
328-330:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse raw margins for the custom objective callback.
XGBoost custom objectives are defined over raw predictions / leaf weights, not transformed outputs. The
predict()method applies link function transformations, producing incorrect gradients and Hessians for objectives with a non-identity link.predict_margin()returns raw margins and is the correct source forobjective_fn.💡 Proposed fix
- let pred = self.predict(dtrain)?; - let (gradient, hessian) = objective_fn(&pred.to_vec(), dtrain); + let pred = self.predict_margin(dtrain)?; + let (gradient, hessian) = objective_fn(&pred, dtrain); self.boost(dtrain, iteration, &gradient, &hessian)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/booster.rs` around lines 328 - 330, The code is using transformed predictions from self.predict() to compute gradients/hessians for objective_fn, but custom objectives must use raw margins; replace the call to self.predict(...) with self.predict_margin(...) so objective_fn receives raw predictions (margins) before boost is called (keep passing the resulting Vec to objective_fn and then call self.boost(dtrain, iteration, &gradient, &hessian) as before).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/booster.rs`:
- Around line 244-253: The custom evaluation block currently inserts results
under the evaluation-name key ("custom") causing the map shape to be eval_name
-> dmat_name -> score; change it to store under the dataset key so the shape
matches the rest (dmat_name -> eval_name -> score): inside the loop over
eval_sets (where params.custom_evaluation_fn, eval_sets, bst.predict_margin,
eval_result, dmat_name and dmat_eval_results are used) replace the entry lookup
that uses eval_name as the top-level key with one that uses
dmat_name.to_string() and then insert eval_name.to_string() -> eval_result into
that inner IndexMap so callbacks and prints become dataset-first (e.g.,
"train-custom" instead of "custom-train").
- Around line 328-330: The code is using transformed predictions from
self.predict() to compute gradients/hessians for objective_fn, but custom
objectives must use raw margins; replace the call to self.predict(...) with
self.predict_margin(...) so objective_fn receives raw predictions (margins)
before boost is called (keep passing the resulting Vec to objective_fn and then
call self.boost(dtrain, iteration, &gradient, &hessian) as before).
- update_custom: pass predictions by reference instead of redundantly cloning the already-owned Vec on every boosting round - set_feature_info: keep CStrings alive for the FFI call instead of leaking them via into_raw() with no matching from_raw() - validate_features: count feature names via a new num_feature_names() helper that reads only the count, instead of allocating an owned String for every name each iteration Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous commit recorded the file as 755 due to executable-bit noise from the external-drive filesystem; restore it to a normal source mode. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- libc 0.2.180 -> 0.2.186 - log 0.4.29 -> 0.4.30 - tempfile 3.24.0 -> 3.27.0 - indexmap 2.13.0 -> 2.14.0 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…h, zero-alloc FFI strings - Add predict_from_dense/predict_from_csr for inplace prediction (no DMatrix round-trip): 1.8–3× faster for serving workloads - Add from_dense_quantile/from_csr_quantile (QuantileDMatrix via callback API): ~4× lower memory for hist tree method - Hybrid dispatch in DMatrix::from_dense: XGDMatrixCreateFromMat_omp above 50k elements, ~5.5× faster than original at 2.5M elements - Replace PredictOption bitmask with static CStr predict_config constants (zero-alloc hot path) - Replace KEY_* &str statics with &CStr literals in dmatrix.rs (eliminates per-call CString::new) - Add predict_borrowed/predict_margin_borrowed returning borrowed slices; wire into custom-obj and eval loops (no copy per round) - Add reset(), serialize_to_buffer(), unserialize_from_buffer() for serving/checkpointing - Coalesce per-round stdout into single println! via format buffer - Add predict_benchmark.rs bench suite; extend dmatrix_benchmark.rs with Mat_omp and array-interface groups - 34 lib tests + 6 doctests passing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eInfo, document nthread tuning Passing a cached proxy to XGBoosterPredictFromDense/FromCSR instead of null avoids XGBoost allocating an internal DMatrixProxy per call (~18% of single-row latency on a single-thread booster; 6.9 -> 5.5 us measured on xgboost 3.0.0). Quantile-matrix labels now go through XGDMatrixSetInfoFromInterface, removing the per-construction deprecation warning. Small-batch serving latency is documented as OpenMP-dispatch-bound (linear in nthread; pin nthread=1 below ~1000 rows). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… metadata to fork - Submodule bumped to dmlc/xgboost v3.2.0; previous setup ran 3.1.3 bindgen headers against downloaded 3.0.0 binaries (silent version skew). Measured vs the old prebuilt: batch predict -65%, training rounds -55%. - local_build is the default feature: cmake Release (-O3, was RelWithDebInfo) with ninja when available, so headers and runtime cannot disagree. - use_prebuilt_xgb now downloads from this fork's release assets (built by the new release-libs.yml workflow on all four targets) and falls back to the legacy 3.0.0 binaries with a warning until a release is published. web_copy now fails on HTTP errors instead of writing error pages to disk. - Crate metadata, README, and CI point at agene0001/rust-xgboost; crate versions bumped to 3.2.0; committed 3.0.0 binaries removed from the repo. - Windows CI stays on the prebuilt path; other platforms build from source. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ault-features
xgboost-sys is now depended on with default-features = false; previously
building xgb with --no-default-features --features use_prebuilt_xgb still
enabled the sys crate's default local_build, so Windows CI downloaded the
prebuilt binaries and then attempted a cmake source build. That build also
exposed a second bug: std canonicalize yields a \\?\ extended-length path
on Windows which breaks CMake's file(GLOB) ("No SOURCES given to target:
xgboost"); xgb_root now goes through dunce::canonicalize.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…>/lib, not the build dir The main library (libxgboost.so/.dylib, xgboost.dll) is emitted into xgboost-sys/xgboost/lib by xgboost's CMake; only libdmlc.a lands under the build directory. Search both roots and fail with an explicit message when a file is missing instead of cp's empty-operand error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The DLL lands in cmake's out dir (local_build) or target/<profile>/deps (use_prebuilt_xgb) — neither is on the Windows loader's search path, so binaries run outside `cargo run` (which patches PATH with the link-search dirs) failed with "xgboost.dll was not found". Copy it into the profile root at build time, where the final executables are emitted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generalizes the Windows-only DLL staging to Linux (libxgboost.so) and macOS (libxgboost.dylib), on both the local_build and use_prebuilt_xgb paths. The local_build copy probes bin/, lib/, and lib64/ since cmake's output layout varies by platform and distro. Staging alone only fixes direct execution on Windows, where the loader searches the exe's directory. On Linux/macOS the binary also needs an $ORIGIN/@loader_path rpath, which Cargo cannot propagate from a dependency's build script — documented in the README with build.rs and .cargo/config.toml snippets for binary crates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ld flags
Removes the last per-call heap allocations on the inplace prediction hot
path and adds buffer-reuse APIs so a warm serving loop allocates nothing
on the Rust side.
- booster.rs: build array-interface JSON into a fixed 192-byte stack
buffer (CBuf) instead of format! + CString. predict_from_dense did one
String+CString per call; predict_from_csr did three. The owned
predict_from_{dense,csr} now delegate to shared raw helpers.
- Add predict_from_dense_into / predict_from_csr_into, which write
predictions into a caller-provided &mut Vec<f32> (cleared, capacity
reused) rather than returning a fresh Vec.
- tests/zero_alloc.rs: counting global allocator proves the warm _into
loop performs zero Rust-side allocations across 100 dense/CSR calls,
with bit-identical results to the owned variants.
- xgboost-sys/build.rs: opt-in XGB_BUILD_NATIVE (-march/-mcpu=native)
and XGB_BUILD_IPO (CMake IPO/ThinLTO) for the local_build C++ compile,
off by default for portable binaries. IPO is passed explicitly ON/OFF
so unsetting the env var reverts the cmake cache. rerun-if-env-changed
wired for both.
- benches: new serving_nthread1 group (stable single-thread serving
latency, owned vs _into, dense+CSR at 1/16/100 rows).
- README: document the serving-path guidance and build flags; fix stale
claims (default is local_build, not use_prebuilt_xgb; XGBoost 3.2.0).
All 44 tests pass, including against a NATIVE+IPO build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The counting allocator used a process-global AtomicUsize, but libtest runs tests on parallel threads (and prints progress from its own), so other threads' allocations could land inside a test's measurement window. On the slower Windows/Linux debug CI builds the two tests overlapped and failed with small spurious counts (16 and 4 per 100 calls — well under one per call, the signature of cross-thread bleed rather than a real wrapper allocation). - Count allocations in a const-initialized thread_local Cell instead, so each test measures only its own thread. try_with in the allocator hook so it can never panic during thread teardown. - Harden the property: each test now runs an allocator-hammering background thread during the measured window, making immunity to concurrent allocation deterministic instead of scheduling luck. - Silence pre-existing CI warnings: declare deprecated_api/print_table as expected cfg values via [lints.rust] unexpected_cfgs, and gate generate_sparse_data / print_comparison_table under the same features as their only callers (fixes the dead_code warning). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Make inplace predict serving path allocation-free; add opt-in C++ bui…
Correctness:
- save_buffer passed a non-NUL-terminated String pointer as a C string
to XGBoosterSaveModelToBuffer (heap overread); use static CStr configs.
- predict_margin config carried training:true from the legacy call,
applying random tree-dropout per call on DART models; now false,
matching Python's predict(output_margin=True).
- parse_eval_string cross-matched eval-set names that prefix another
("val"/"val2"); require the '-' separator.
- predict_from_dense / DMatrix::from_dense{,_quantile} silently truncated
when values.len() wasn't divisible by num_rows and panicked on
num_rows=0; both now return an error.
- DMatrix::slice segfaulted on out-of-bounds indices (the C API doesn't
bounds-check); validate before the FFI call.
- XGBoost returns a null data pointer for empty predictions (0-row
DMatrix): predict_raw asserted non-null (panic), and the naive fix
would be from_raw_parts(null, 0) UB. All three raw predict paths now
share predict_output_slices, which maps null+empty-shape to &[].
- predict_leaf/contributions/interactions divided by zero on 0-row
matrices.
Performance:
- boost() built its gradient/hessian array-interface JSON with
format!+CString (4 allocations per custom-objective round); build it
in the existing CBuf stack buffers instead, byte-identical output.
Proven by the new alloc-count test (exactly 2 allocs/round: the
objective's own grad/hess Vecs) and a custom-vs-builtin logistic
equivalence test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
XGBoost silently ignores unknown parameter keys (validate_parameters defaults to false), so stale names fail silently. Audited every emitted key/value against the bundled 3.2.0 source: - verbose emitted "silent", removed as a parameter in 1.0 — the toggle did nothing. Now emits "verbosity" (0/2), so it works. - predictor (removed 2.0) and sketch_eps (removed 1.7) were dead keys emitted on every run; removed (replacements: device, max_bin). - base_score was unconditionally sent as 0.5, silently disabling 3.x's automatic intercept estimation (boost_from_average) on every model. Now Option<f32> defaulting to unset, matching Python defaults. BEHAVIOR CHANGE: models trained without an explicit base_score now boost from the estimated intercept. Golden-value tests pin 0.5. - Removed variants that 3.x rejects at Configure time: gpu_exact / gpu_hist tree methods (string From impl shims them to exact/hist), distcol / grow_local_histmaker / grow_skmaker updaters (added the modern grow_quantile_histmaker), and the four gpu:* objectives. - TweedieLogLoss emitted bare "tweedie-nloglik", which XGBoost rejects; now takes rho and emits "tweedie-nloglik@rho". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…guide - unsafe impl Send for Booster and DMatrix: the C API keeps all return buffers in thread-local maps keyed by learner/DMatrix pointer (LearnerAPIThreadLocalStore), so handles have no thread affinity and a loaded model can move into worker threads. Sync stays unimplemented: concurrent &self predictions would race on the cached inplace proxy. - predict_into / predict_margin_into: buffer-reuse variants for the DMatrix path, matching the inplace *_into APIs; steady-state batch scoring is now allocation-free (pinned by a new zero_alloc test). - Record the per_round_overhead bench conclusion: the validate_features feature-info FFI getter costs ~37 ns vs ~2 ms per boosting round, so caching it is not worth doing. - docs/SERVING.md: consumer-facing guide covering XGB_BUILD_NATIVE/IPO, nthread=1 for small batches, inplace prediction, buffer reuse, the Send-not-Sync threading model, and QuantileDMatrix training. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Build-flag env vars must be pinned in the final binary's workspace (.cargo/config.toml does not travel with dependencies), and libraries wrapping this crate should document the batch-size/threading regime they tuned for so top-level consumers know which side of the ~1000-row nthread crossover they are on. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Distributional-regression callers (gradientlss) carry the training margin across rounds and compute grad/hess from it, so update_custom's internal predict is a wasted prediction-cache read per round for them — boost is the entry point they need, and it was already safe to expose (multi-target shape inference included). New tests pin down the QuantileDMatrix contract those callers rely on: base_margin and weights are settable post-construction (only the feature data is frozen by binning), a 0-tree multi-target predict returns exactly the base margin, and pub boost + cached predict round-trips build a real multi-target model on a quantile matrix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…direct) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Param/API audit for 3.2.0 -> 3.3.0 (diffed the bundled C++ between tags): - C-API surface unchanged: no entry points added or removed (only doc edits deprecating the GPU-only max_quantile_blocks config key, unused here). - No wrapped parameter was renamed or removed; all pinned-value tests pass unchanged, so numerics on our paths are identical. - New in 3.3: reg:expectileerror objective (expectile_alpha list param) and expectile metric. Not yet in the Objective enum (reachable via set_param, like reg:quantileerror); candidate follow-up. - booster=dart is deprecated and internally remapped to gbtree (DART folded into the tree booster); booster=gblinear is deprecated with removal planned. Both still work — documented on BoosterType, and a new test pins that they keep training so a future bump that removes them fails loudly. - gputreeshap is no longer a submodule of xgboost at 3.3.0. build.rs: watch include/xgboost/version_config.h so a submodule version bump triggers a C++ rebuild — without this, cargo silently reuses the previous version's cached libxgboost (bit this bump; caught because the build finished in seconds). Prebuilt release URL moved to the v3.3.0 tag, which the release-libs workflow populates when the tag is pushed. Crate versions follow the bundled XGBoost: 3.3.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…arness Point the xgboost submodule at agene0001/xgboost branch serving-patches-3.3.0: v3.3.0 plus two patches driven by profiling the single-row nthread=1 inplace-predict loop (examples/profile_predict.rs, committed here as the reusable harness): - context.cc: cache the two per-call std::regex constructions in device parsing and fast-path device=cpu. DMatrixProxy::SetArray re-inits its context every inplace predict, so this was ~27% of single-row latency. - cpu_predictor.cc: skip the per-tree MaxDepth() precalculation when n_samples <= 1; DispatchArrayLayout never reads it for size-1 blocks. Another ~21% of single-row latency. Combined: 7.43us -> 4.31us per single-row call (-42%), bit-identical predictions, full test suite green in debug and release. Both patches are also staged as branches off dmlc master on the fork (fix-device-parse-regex, fix-single-row-depth-precalc) for upstream PRs; when upstream ships them, repoint the submodule at the official tag and drop this divergence.
…3 parity)
- Objective::RegQuantile(Vec<f32>) / RegExpectile(Vec<f32>): first-class
variants for reg:quantileerror and the new-in-3.3 reg:expectileerror,
emitting the alpha list in the [a,b,...] form ParamArray parses. One
prediction column per alpha; builder validates alphas non-empty and in
(0, 1). Objective loses Copy (Vec payloads) — Clone remains.
- DMatrix::set/get_feature_types + set/get_feature_names via
XGDMatrix{Set,Get}StrFeatureInfo. Marking a column "c" enables
categorical splits (on by default since XGBoost 3.3); previously the
wrapper had no way to use categorical features at all.
- Tests: multi-quantile/expectile output shape and column ordering,
builder validation, and a categorical training test asserting the
split separates the informative category.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
serving-patches-3.3.0 gains a third patch (also submitted upstream as a follow-up to dmlc/xgboost#12311): parse object keys as plain strings, insert with moves, construct unescaped string literals in one piece, and move array elements. Single-row inplace predict: 4665 -> 4272 ns/call (~9%), bit-identical predictions; JSON model loading shares the same reader. Full test suite green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- [profile.release] lto="thin" + codegen-units=1. The hot path is inside
libxgboost so the Rust-side gain is small, but it is free and removes a
confound when comparing this wrapper against other bindings. bench
inherits release, so criterion measures shipped codegen.
- XGB_BUILD_NATIVE now defaults ON: local_build compiles the C++ on the
machine that runs it, so "portable across CPUs" protected nobody by
default while making every default build (and forgotten-env benchmark)
measure the slow path. Cross-machine deploys opt out with =0. Release
CI assets are unaffected (they invoke cmake directly, not build.rs).
- HIDE_CXX_SYMBOLS=ON for the shared build: the C API keeps its explicit
visibility("default"), so linking is unaffected; cross-TU calls inside
libxgboost stop being interposable on Linux ELF.
- XGB_CMAKE_DEFINES="KEY=VAL;..." passthrough for perf experiments
(USE_OPENMP=OFF, BUILD_STATIC_LIB=ON, ...) without editing build.rs.
- Watch xgboost/src and xgboost/include for rebuilds: watching only
version_config.h silently measured stale code after hand-edits to the
carried C++ patches.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Correctness / FFI hardening: - predict_matrix: reject interior-NUL configs (from_bytes_with_nul) instead of silently truncating at the first NUL. - validate_features uses XGBoosterGetNumFeature instead of counting feature *names* (valid for models without names; skip-on-error keeps behavior). - feature-name/type getters and setters return Err on interior-NUL / non-UTF-8 (possible from a model written by another binding) instead of panicking. - set_objective validates alphas (shared with the builder), closing a bypass that surfaced as an opaque C++ CHECK at the first update. - Multi-output doc fixes on predict/predict_into; pin predict_margin_into in tests/zero_alloc.rs (the last uncovered predict-into variant). - Document the set_feature_types stale-gradient-index footgun. Data-ingestion APIs (fill gaps that forced user-side full copies): - from_dense/from_csr/from_csc_with_missing: custom missing sentinel dropped during XGBoost's own ingestion pass; uniform finite-or-NaN contract. Booster::set_inplace_predict_missing for the serving path (config built once; hot path stays allocation-free). - from_dense_quantile_ref / from_csr_quantile_ref: eval sets share the training matrix's bin cuts (Python's QuantileDMatrix(ref=...)). - from_dense_f64: <f8 array-interface ingestion; f64->f32 narrowing folds into XGBoost's mandatory copy. Training config surface (the loop is at ceiling; the gaps were around it): - TrainingParameters.eval_period + verbose_eval: evaluation sets no longer force a full prediction pass + stdout print every round. - BoosterParameters.device (Device enum): GPU training is now reachable through typed params; the removed gpu_hist/gpu_exact tree methods warn loudly instead of silently mapping to CPU. - SamplingMethod + max_cat_to_onehot/max_cat_threshold typed params. - Rewrite stale TreeMethod docs (auto == hist since 2.0; exact is legacy). - SERVING.md/README: document the measured levers (max_bin, eval cadence, QuantileDMatrix as a memory win not a speed win, GPU guidance). - examples/profile_train.rs: reusable training profiling harness. Full test suite green (53 lib + 5 zero-alloc + 6 doc). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
serving-patches-3.3.0 gains a fourth patch: CacheManager fell through to compiled cache defaults (L1=32KB, L2=1MB) on Apple silicon, where the real figures are 128KB/64KB L1d and 16MB/4MB L2 (M3 Pro P/E cores). The hist tree method sizes histogram row blocks and picks the row-vs-column build kernel from these, so blocks were sized several times too small and the kernel-selection threshold sat at ~838KB instead of ~3.4MB. The patch reads hw.perflevel<N>.l1dcachesize/l2cachesize via sysctlbyname (min across performance levels), isolated by preprocessor to non-x86_64 macOS. Measured (hist, depth 6, 100k rows, min round of 3 runs): 64 cols 8.17->7.39 ms/round; 512 cols 68.99->63.88 ms/round. Full test suite green. Also submitted upstream as dmlc/xgboost#12362 (branch fix-macos-cache-detection on the fork); repoint to the official tag and drop this divergence when it merges. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-pins the submodule onto the revert of 791afcace. That patch was a measured regression, not the ~5-9% win claimed when it landed in e1d38fd: the original number came from comparing builds batched (all-A then all-B) with min-of-min round times, which on this machine confounds the build difference with background-load and thermal drift. Re-measured interleaved (variant alternated every iteration, rebuild between each run): no difference at 500k x 64 (median round 28.14 vs 28.19 ms, 6 pairs), and the patch loses all 4 pairs at 200k x 512 -- median round ~172 vs ~135 ms, total ~7567 vs ~6996 ms, i.e. ~12-28% slower where it actually flips histogram kernel selection. Root cause of the bad premise: hw.perflevel*.l2cachesize is the cluster-shared L2, but ReadByColumn treats L2 as a roughly per-thread budget, so the "real" figure overestimates per-thread capacity and picks the row-wise kernel when the working set does not fit. The conservative 1MB default is closer to the effective per-core budget. Upstream PR dmlc/xgboost#12362 closed with the same correction. The branch keeps the three merged-upstream serving patches (device regex, single-row depth precalc, JSON parser allocations) unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Upstream XGBoost's CMake links the shared library into the SOURCE tree (<xgboost>/lib) by default. Under cargo that directory is shared by every profile and every concurrent cargo invocation using the same checkout, so two builds racing through the ninja link step fail with LNK1104: cannot open file '...\xgboost-sys\xgboost\lib\xgboost.dll' (observed: a background `cargo build` overlapping `cargo run --profile production` right after a pin bump, when the native lib had to be rebuilt). Cargo's target-dir lock does not cover the git checkout, so nothing serializes the two linkers. Fix: define KEEP_BUILD_ARTIFACTS_IN_BINARY_DIR=ON (upstream option since CMakeLists.txt line 73) so the link output stays under cmake's per-OUT_DIR binary dir. The install step still places the runtime library in <dst>/bin and the import/static libs in <dst>/lib, which is exactly where the link-search paths and the staging candidates already look — verified on Windows: source tree stays clean, xgboost.dll lands in out/bin + staged next to the executables, xgboost.lib/dmlc.lib in out/lib, and `cargo test -p xgb --no-run` links end to end. The define sits before the XGB_CMAKE_DEFINES escape hatch, so it can still be overridden per build if anyone ever wants the old layout back. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Windows CI silently tested the 3.3 wrapper against XGBoost 3.0.0 binaries for over a week: the v3.3.0 release was never published, so build.rs fell back to the legacy assets and the skew only surfaced as an "Unknown objective function: reg:expectileerror" test failure. Three invariants were maintained by hand; now each fails loudly instead. - The release tag is derived from CARGO_PKG_VERSION rather than hardcoded, so it cannot drift from the crate version on a bump. - A missing release asset is a hard error naming the workflow to run, instead of a silent downgrade. The XGBoost 3.0.0 fallback mixes ABIs against the bundled headers, so it is now opt-in via XGB_ALLOW_LEGACY_PREBUILT=1 (still warns loudly when used). - build.rs warns when the crate version disagrees with the bundled submodule's version_config.h — a submodule bump that forgot the Cargo.toml bump would otherwise point at the wrong release tag. A warning, not a panic, so an intentional skew or a user-supplied XGBOOST_LIB_DIR stays buildable. release-libs.yml gains a `verify` job that resolves the tag once and gates the build on crate version == root version == bundled XGBoost == tag (previously the dispatch input defaulted to a stale "v3.2.0" literal, which would have published assets under a tag build.rs never looks up). The release job additionally refuses to publish an incomplete asset set, since a missing platform breaks only that platform's consumers. Verified: skew warning fires on a simulated submodule bump; the prebuilt path hard-errors with the derived tag when no release exists; the opt-in fallback still works. Full suite green (53 + 5 + 6). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Upstream's three new commits (through 86ad557) rebuild the prebuilt path around SHA-256-pinned artifacts fetched from marcomq/rust-xgboost at tag v3.0.5, switch the downloader from reqwest to ureq+sha2, and restore use_prebuilt_xgb as the default feature. None of that can be adopted here. This fork bundles XGBoost 3.3.0 (the submodule points at agene0001/xgboost) and publishes its own release assets, with the tag derived from CARGO_PKG_VERSION so it cannot drift from the crate version. Upstream's checksums are for 3.0.5 binaries and can never match an asset published from this fork, and linking 3.0.x libraries against 3.3.0 headers is exactly the version skew that fetch_lib already refuses by design - it previously surfaced as an "Unknown objective function: reg:expectile" failure at run time rather than as a build error. Resolved by keeping this fork's xgboost-sys/build.rs, Cargo.toml versions and feature defaults (default = local_build) in full. Upstream touched nothing outside that scheme, so the merged tree is unchanged from the previous branch tip; the merge records the shared history so these conflicts do not resurface on the next sync. Also dropped upstream's release tooling that came in with the merge (.github/workflows/prebuilt-libs.yml, scripts/upload-release-libs.sh): it publishes v3.0.5 binaries under the scheme this fork does not use, duplicating the existing release-libs.yml. Upstream's content-hash verification is worth having and is ported onto this fork's downloader in a follow-up commit.
Ports the one idea worth keeping from upstream's competing prebuilt scheme onto this fork's downloader. error_for_status already stops a 404 page being written out as a library, but it cannot catch a truncated transfer, a body mangled by an intercepting proxy, or a release asset re-uploaded with different contents under a tag that was already consumed. None of those are hypothetical for assets fetched over the network on every clean build. The digest is checked before the bytes are written, so a rejected download never lands on disk where a later build would find the file present and skip the fetch entirely. A mismatch returns an error rather than panicking, so fetch_lib treats a bad asset the same as a missing one and its existing fallback still applies; the panic message now carries the underlying cause so a mismatch is not reported as "the release was never published". Digests for all eight v3.3.0 assets are recorded. An asset with no entry is downloaded with a warning rather than failing the build, so adding a platform does not need a digest up front; XGB_REQUIRE_CHECKSUMS=1 turns that into an error for CI. Also adds XGBOOST_LIB_URL to point the same flat <platform>-<file> layout at a mirror, and declares rerun-if-env-changed for the lookup variables so a build actually reruns when they change. Verified: assets re-downloaded from a clean deps/ pass verification and the crate's 64 tests pass against the result; corrupting a recorded digest fails the build with the mismatch as the reported cause. Corrected two stale README claims while documenting this: the crate builds against XGBoost 3.3.0, not 3.2.0, and local_build is the default feature here, not use_prebuilt_xgb.
Fixed deprecated function warnings and added TODO features. Also bumped up version numbers and edition
Summary by CodeRabbit
New Features
Added
Fixed
Documentation