Repository navigation
[c++] SHAP computation crashes if run after model.update() or in training callback #7488
Description
Description
Call predict(..., pred_contrib=True) on a Booster, train that Booster for more iterations, then call predict(..., pred_contrib=True) again. The second call fails with
[LightGBM] [Fatal] Check failed: (max_depth_) >= (0) at ...\include\LightGBM/tree.h, line 673 .
This crashes the session.
Another way to hit this is a callback that computes SHAP values during training (second example below).
This is a regression in v4.6.0. v4.5.0 works.
Bug Investigation
#6778 (the fix for #5482) changed GBDT::InitPredict() to call Tree::RecomputeMaxDepth() only once per Booster, guarded by models_initialized_:
Lines 438 to 449 in 439136e
| if (is_pred_contrib && !models_initialized_) { | |
| std::lock_guard<std::mutex> lock(instance_mutex_); | |
| if (models_initialized_) | |
| return; | |
| #pragma omp parallel for num_threads(OMP_NUM_THREADS()) schedule(static) | |
| for (int i = 0; i < static_cast<int>(models_.size()); ++i) { | |
| models_[i]->RecomputeMaxDepth(); | |
| } | |
| models_initialized_ = true; | |
| } |
models_initialized_ is never reset when trees are added. New trees start with max_depth_ = -1, so this check in Tree::PredictContrib() fails for every tree trained after the first pred_contrib call:
LightGBM/include/LightGBM/tree.h
Line 673 in 439136e
| CHECK_GE(max_depth_, 0); |
Reproducible example
import lightgbm as lgb
import numpy as np
X = np.random.default_rng(0).normal(size=(1_000, 5))
y = X[:, 0] + X[:, 1]
bst = lgb.Booster({"objective": "regression", "verbose": -1}, lgb.Dataset(X, y))
bst.update()
bst.predict(X, pred_contrib=True) # works
bst.update()
bst.predict(X, pred_contrib=True) # Check failed: (max_depth_) >= (0)The same failure with lgb.train() and a callback, reusing X and y. It fails at the second iteration:
def log_shap(env):
env.model.predict(X, pred_contrib=True)
lgb.train(
{"objective": "regression", "verbose": -1},
lgb.Dataset(X, y),
num_boost_round=3,
callbacks=[log_shap],
)Environment info
You can run this code in this Colab notebook:
https://colab.research.google.com/drive/1GFgssf483AIeh-CjOK0BejSeMHAJe6Gh?usp=sharing
Also reproduced this locally on Windows 11, Python 3.12.7.
The code crashes on 4.7.0 and 4.6.0. On 4.5.0 the code works and doesn't crash.
Command(s) you used to install LightGBM
pip install lightgbm==4.7.0Additional Comments
I recently started working on improving the TreeSHAP algorithm in LightGBM. See #7477 .
I stumbled upon this bug when asking Claude to help me get familiar with the current code.
If you want, I can try to tackle this as part of the SHAP computation change.
I have a feeling though it requires changes in other files and may affect other flows other than SHAP computation.
This Claude suggested:
In my tests, the failed check kills the Python process instead of raising LightGBMError. That looks like the separate ThreadExceptionHelper::ReThrow() problem that #7366 fixes.
Possible fix: set max_depth_ when a tree is built or loaded, instead of in InitPredict(). Tree::Split() already maintains leaf_depth_. This would also stop InitPredict() from modifying trees at predict time, which is what #5482 was about. Alternatively, reset models_initialized_ whenever trees are added (TrainOneIter(), MergeFrom(), ...).
Activity
[-]SHAP computation crashes if run after model.update() or in training callback[/-][+][c++] SHAP computation crashes if run after model.update() or in training callback[/+]jameslamb commentedon Oct 6, 2026
Thanks for the excellent write-up! Definitely seems like a bug, we'd welcome a PR with a proposed fix if you have time.
ron-wettenstein commentedon Oct 24, 2026
Added a PR that solves the bug. See #7501 .