Skip to content

[c++] SHAP computation crashes if run after model.update() or in training callback #7488

Open

Description

@ron-wettenstein

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_:

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:

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.0

Additional 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

  1. changed the title [-]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[/+] on Oct 6, 2026
  2. jameslamb commented on Oct 6, 2026

    @jameslamb
    Member

    Thanks for the excellent write-up! Definitely seems like a bug, we'd welcome a PR with a proposed fix if you have time.

  3. ron-wettenstein commented on Oct 24, 2026

    @ron-wettenstein
    Author

    Added a PR that solves the bug. See #7501 .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions