Skip to content

[c++] fix Booster.refit() segfault with linear_tree on single-class data#7233

Open
FranciscoRMendes wants to merge 4 commits into
lightgbm-org:mainfrom
FranciscoRMendes:fix/issue-6792-clean
Open

[c++] fix Booster.refit() segfault with linear_tree on single-class data#7233
FranciscoRMendes wants to merge 4 commits into
lightgbm-org:mainfrom
FranciscoRMendes:fix/issue-6792-clean

Conversation

@FranciscoRMendes

@FranciscoRMendes FranciscoRMendes commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Fixes the segfault in Booster.refit() reported in #6792.

When refitting a linear_tree=True model on data whose label has a single class, some features end up constant in that subset. LightGBM filters out constant features during Dataset construction, so Dataset.InnerFeatureIndex() returns -1 for them.
CalculateLinear was then calling FeatureBinMapper(-1) — an invalid index — which caused the segfault.

The fix skips features where InnerFeatureIndex() returns -1 in CalculateLinear. I added a leaf_old_coeff_idx vector to track each surviving feature's position in the stored coefficient array, so the decay blend (decay_rate * old_coeff + ...) still reads the
right slot when features are skipped.

I have tested this by running test_refit_linear_tree_single_class_no_segfault which confirms refitting on single-class data no longer crashes.

Closes #6792

@jameslamb jameslamb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Bug 2: Spurious UserWarning about categorical_feature keyword in params

We already have a fix for #6793 in #7124, please remove changes related to that from this PR. And if you're curious, please do look at the changeset in that PR... quite a lot more than just removing the parameters is needed.

@FranciscoRMendes FranciscoRMendes changed the title [python-package/c++] fix Booster.refit() segfault and spurious warning with linear_tree [c++] fix Booster.refit() segfault with linear_tree on single-class data Apr 21, 2026
@FranciscoRMendes

Copy link
Copy Markdown
Contributor Author

Bug 2: Spurious UserWarning about categorical_feature keyword in params

We already have a fix for #6793 in #7124, please remove changes related to that from this PR. And if you're curious, please do look at the changeset in that PR... quite a lot more than just removing the parameters is needed.

oops my bad. I thought this would be an easy fix for me, turns out I was wrong.

@FranciscoRMendes

Copy link
Copy Markdown
Contributor Author

removed changes for the other unrelated bug

@jameslamb jameslamb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just one quick comment, will review more thoroughly when I can.

if weight is not None:
if _is_pyarrow_array(weight):
if pa_compute.all(pa_compute.equal(weight, 1)).as_py():
if pa_compute.all(pa_compute.equal(weight, 1)).as_py(): # type: ignore[attr-defined]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
if pa_compute.all(pa_compute.equal(weight, 1)).as_py(): # type: ignore[attr-defined]

Please remove this and update this branch, this was fixed in #7236

And in the future, when you find yourself making changes that seem unrelated to the purpose of the PR, please consider proactively writing a small comment explaining them. Including the relevant warnings / error messages in plaintext is also helpful, because it allows others to find similar issues from GitHub search.

For this case, I did that here: #7238

Doing that before maintainers have to ask "wait, what is this about?" cuts out a review cycle, which helps us all work a bit more efficiently.

…g with linear trees (lightgbm-org#6792)

Two bugs are fixed here:

1. Segfault in CalculateLinear when refitting a linear_tree model on data
   whose label contains only one class.  Features that are constant in the
   refit dataset are filtered out during Dataset construction, so
   InnerFeatureIndex() returns -1 for them.  The code then passed -1 directly
   to FeatureBinMapper(), causing an out-of-bounds crash.

   Fix: skip features whose InnerFeatureIndex() returns -1, and track their
   original position in the leaf's feature list so that the old coefficient
   used for the decay blend is taken from the correct slot.

2. Spurious "categorical_feature keyword found in params" UserWarning emitted
   by Dataset construction inside Booster.refit() when the model was loaded
   from a text file.  The model text file embeds categorical_feature in the
   stored parameter string, which ends up in Booster.params and was forwarded
   verbatim as the params= argument to Dataset.  Dataset.__init__ already
   accepts categorical_feature as an explicit argument, so having it in params
   triggers the duplicate-keyword warning.

   Fix: strip all categorical_feature aliases from the params dict that is
   passed to Dataset inside refit(), since categorical_feature is already
   forwarded as the dedicated Dataset argument.
@FranciscoRMendes

FranciscoRMendes commented May 18, 2026

Copy link
Copy Markdown
Contributor Author

I did three things,

  1. Guard against -1 feature index (if (feat < 0) continue;) — this is the direct crash fix. When a feature is constant in the refit data, InnerFeatureIndex() returns -1, and passing that to FeatureBinMapper() was the segfault.
  2. leaf_old_coeff_idx vector — tracks each surviving feature's original position in the raw feature list. This is necessary because once you start skipping features, position i in the new feature list no longer matches position i in the old coefficient array.
  3. Decay-blend index fix — uses old_coeff_idx[i] instead of i when blending old and new coefficients, so the right old coefficient is read even when some features were skipped.

Without (2) and (3), the guard in (1) would fix the crash but silently use the wrong old coefficients during refit.

Conflict resolution accidentally removed the _choose_param_value block
for categorical_feature that upstream gained via lightgbm-org#7124. Restore it so
basic.py matches upstream exactly.
@BelixRogner

This comment was marked as spam.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[python-package] Booster.refit() segfaults when label only contains 1 class

3 participants