Skip to content

Change to scikit-learn tree node type breaks treelite.sklearn.export_model #696

Description

@betatim

scikit-learn 1.10.0.dev0 (the nightly version of scikit-learn) uses a different node type to 1.9.0.

For example in cuml's nightly we get failures related to this https://github.com/NVIDIA/cuml/actions/runs/33059973711/job/98476087166

I've picked out one test failure and put it in the detail below. The test itself is integration/test_rf_classifier.py::test_oob_score

A minimal reproducer with the nightly version of scikit-learn:

import sklearn
import treelite.sklearn
from sklearn.datasets import make_classification
from sklearn.ensemble import RandomForestClassifier

print(f"sklearn={sklearn.__version__} treelite={treelite.__version__}")

X, y = make_classification(n_samples=100, n_features=4, random_state=0)
model = RandomForestClassifier(n_estimators=2, max_depth=3).fit(X, y)

tl_model = treelite.sklearn.import_model(model)
treelite.sklearn.export_model(tl_model)

scikit-learn/scikit-learn#33972 is the PR that made the relevant change.

Full test failure

________________________________ test_oob_score ________________________________
[gw1] linux -- Python 3.14.7 /pyenv/versions/3.14.7/bin/python
Traceback (most recent call last):
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/_pytest/runner.py", line 361, in from_call
    result: TResult | None = func()
                             ~~~~^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/_pytest/runner.py", line 250, in <lambda>
    lambda: runtest_hook(item=item, **kwds),
            ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/pluggy/_hooks.py", line 512, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/pluggy/_callers.py", line 167, in _multicall
    raise exception
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/pluggy/_callers.py", line 139, in _multicall
    teardown.throw(exception)
    ~~~~~~~~~~~~~~^^^^^^^^^^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/_pytest/logging.py", line 865, in pytest_runtest_call
    yield
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/pluggy/_callers.py", line 139, in _multicall
    teardown.throw(exception)
    ~~~~~~~~~~~~~~^^^^^^^^^^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/_pytest/capture.py", line 900, in pytest_runtest_call
    return (yield)
            ^^^^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/pluggy/_callers.py", line 139, in _multicall
    teardown.throw(exception)
    ~~~~~~~~~~~~~~^^^^^^^^^^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/pluggy/_callers.py", line 53, in run_old_style_hookwrapper
    return result.get_result()
           ~~~~~~~~~~~~~~~~~^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/pluggy/_result.py", line 103, in get_result
    raise exc.with_traceback(tb)
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/pluggy/_callers.py", line 38, in run_old_style_hookwrapper
    res = yield
          ^^^^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/pluggy/_callers.py", line 139, in _multicall
    teardown.throw(exception)
    ~~~~~~~~~~~~~~^^^^^^^^^^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/_pytest/skipping.py", line 268, in pytest_runtest_call
    return (yield)
            ^^^^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/pluggy/_callers.py", line 121, in _multicall
    res = hook_impl.function(*args)
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/_pytest/runner.py", line 184, in pytest_runtest_call
    item.runtest()
    ~~~~~~~~~~~~^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/_pytest/python.py", line 1707, in runtest
    self.ihook.pytest_pyfunc_call(pyfuncitem=self)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/pluggy/_hooks.py", line 512, in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
           ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/pluggy/_manager.py", line 120, in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
           ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/pluggy/_callers.py", line 167, in _multicall
    raise exception
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/pluggy/_callers.py", line 121, in _multicall
    res = hook_impl.function(*args)
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/_pytest/python.py", line 167, in pytest_pyfunc_call
    result = testfunction(**testargs)
  File "/__w/cuml/cuml/python/cuml/cuml_accel_tests/integration/test_rf_classifier.py", line 238, in test_oob_score
    assert isinstance(clf.oob_score_, float)
                      ^^^^^^^^^^^^^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/cuml/accel/estimator_proxy.py", line 587, in __getattr__
    self._sync_attrs_to_cpu()
    ~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/cuml/accel/estimator_proxy.py", line 333, in _sync_attrs_to_cpu
    self._gpu._sync_attrs_to_cpu(self._cpu)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/cuml/internals/outputs.py", line 818, in inner
    res = func(*args, **kwargs)
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/cuml/internals/interop.py", line 185, in _sync_attrs_to_cpu
    attrs = self._attrs_to_cpu(model)
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/cuml/ensemble/randomforestclassifier.py", line 196, in _attrs_to_cpu
    attrs = super()._attrs_to_cpu(model)
  File "cuml/ensemble/randomforest_common.pyx", line 288, in cuml.ensemble.randomforest_common.BaseRandomForestModel._attrs_to_cpu
    sk_model = treelite.sklearn.export_model(tl_model)
    ^^^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/treelite/sklearn/exporter.py", line 236, in export_model
    _export_tree(
    ~~~~~~~~~~~~^
        model,
        ^^^^^^
    ...<5 lines>...
        subestimator_class=subestimator_class,
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/pyenv/versions/3.14.7/lib/python3.14/site-packages/treelite/sklearn/exporter.py", line 133, in _export_tree
    tree.__setstate__(state)
    ~~~~~~~~~~~~~~~~~^^^^^^^
  File "sklearn/tree/_tree.pyx", line 896, in sklearn.tree._tree.Tree.__setstate__
  File "sklearn/tree/_tree.pyx", line 1655, in sklearn.tree._tree._check_node_ndarray
    raise ValueError(
ValueError: node array from the pickle has an incompatible dtype:
- expected: {'names': ['left_child', 'right_child', 'feature', 'threshold', 'left_cat_bitset', 'impurity', 'n_node_samples', 'weighted_n_node_samples', 'missing_go_to_left', 'split_kind'], 'formats': ['<i8', '<i8', '<i8', '<f8', ('<u4', (8,)), '<f8', '<i8', '<f8', 'u1', 'i1'], 'offsets': [0, 8, 16, 24, 32, 64, 72, 80, 88, 89], 'itemsize': 96, 'aligned': True}
- got     : {'names': ['left_child', 'right_child', 'feature', 'threshold', 'left_cat_bitset', 'impurity', 'n_node_samples', 'weighted_n_node_samples', 'missing_go_to_left'], 'formats': ['<i8', '<i8', '<i8', '<f8', ('<u4', (8,)), '<f8', '<i8', '<f8', 'u1'], 'offsets': [0, 8, 16, 24, 32, 64, 72, 80, 88], 'itemsize': 96, 'aligned': True}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions