Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b34dde5
single threading
cakedev0 Aug 13, 2026
1ab226b
Merge remote-tracking branch 'upstream/main' into hgb/use_threads_if
cakedev0 Aug 13, 2026
bda77cf
Merge remote-tracking branch 'upstream/main' into hgb/use_threads_if
cakedev0 Aug 13, 2026
68e1aae
fix
cakedev0 Aug 14, 2026
185c1a0
TST Add regression test for max_features<1 crash in HGB splitter
cakedev0 Aug 14, 2026
49c8feb
grower policy
cakedev0 Aug 14, 2026
2d30995
simplify & extend policy
cakedev0 Aug 14, 2026
288802f
fix
cakedev0 Aug 14, 2026
35d03d9
Merge remote-tracking branch 'upstream/main' into hgb/use_threads_if
cakedev0 Aug 14, 2026
1806261
sequential in other parts
cakedev0 Aug 14, 2026
5277077
Merge branch 'hgb/use_threads_if' into hgb/both_threads_optim
cakedev0 Aug 14, 2026
0766990
iter
cakedev0 Aug 19, 2026
2d50316
explain formula
cakedev0 Aug 19, 2026
77a15ff
iter active wait (no good for now)
cakedev0 Aug 19, 2026
166dfd6
fix
cakedev0 Aug 19, 2026
469443b
proper active wait adaptation
cakedev0 Aug 19, 2026
577e13e
Merge remote-tracking branch 'upstream/main' into hgb/adapt_to_active…
cakedev0 Aug 19, 2026
adf5efa
fixes
cakedev0 Aug 19, 2026
5181beb
avoid use threads if in loss
cakedev0 Aug 19, 2026
27d3e13
test heuristic
cakedev0 Aug 19, 2026
497bf5d
Merge remote-tracking branch 'upstream/main' into hgb/adapt_to_active…
cakedev0 Aug 20, 2026
bc29313
WIP
cakedev0 Sep 3, 2026
8c718c2
reactivate _get_heurirstic_optimal_n_threads
cakedev0 Sep 8, 2026
18703ab
Merge remote-tracking branch 'upstream/main' into hgb/only_uniformly_…
cakedev0 Sep 9, 2026
f15bd11
int n threads
cakedev0 Sep 9, 2026
ef25634
fix tests
cakedev0 Sep 9, 2026
0d79f81
cleaning diff
cakedev0 Sep 9, 2026
1d2343c
cleaning diff
cakedev0 Sep 9, 2026
0d4c4b9
fix test
cakedev0 Sep 9, 2026
1ded562
changelog
cakedev0 Sep 9, 2026
b38ba01
add active wait detection and use it in _get_heurirstic_optimal_n_thr…
cakedev0 Sep 9, 2026
5123c96
more use_threads_if
cakedev0 Sep 9, 2026
218900b
fix heuristic
cakedev0 Sep 10, 2026
de2f2f7
remove unrelated test
cakedev0 Sep 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
- :class:`ensemble.HistGradientBoostingClassifier` and
:class:`ensemble.HistGradientBoostingRegressor` now pick a more sensible
number of threads to use during `fit`, based on the size of the dataset,
instead of always using every available thread. This avoids thread
management overhead dominating the actual work on small datasets, which
can lead to up to ~10x speed-ups when the number of threads is
unconstrained (e.g. no `OMP_NUM_THREADS` or similar limit set).
By :user:`Arthur Lacote <cakedev0>`.
36 changes: 27 additions & 9 deletions sklearn/ensemble/_hist_gradient_boosting/binning.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,14 @@ class _BinMapper(TransformerMixin, BaseEstimator):
Pass an int for reproducible output across multiple
function calls.
See :term:`Glossary <random_state>`.
n_threads : int, default=None
Number of OpenMP threads to use. `_openmp_effective_n_threads` is called
to determine the effective number of threads use, which takes cgroups CPU
quotes into account. See the docstring of `_openmp_effective_n_threads`
for details.
max_n_threads : int, default=None
Upper bound on the number of OpenMP threads to use.
`_openmp_effective_n_threads` is called with this value to determine
the effective number of threads available, which takes cgroups CPU
quotas into account (see its docstring for details). The number of
threads actually used is then sized down further based on the amount
of data to bin, to avoid parallelizing workloads too small to
benefit from it.

Attributes
----------
Expand Down Expand Up @@ -202,14 +205,14 @@ def __init__(
is_categorical=None,
known_categories=None,
random_state=None,
n_threads=None,
max_n_threads=None,
):
self.n_bins = n_bins
self.subsample = subsample
self.is_categorical = is_categorical
self.known_categories = known_categories
self.random_state = random_state
self.n_threads = n_threads
self.max_n_threads = max_n_threads

def fit(self, X, y=None, sample_weight=None):
"""Fit data X by computing the binning thresholds.
Expand Down Expand Up @@ -286,7 +289,20 @@ def fit(self, X, y=None, sample_weight=None):
self.bin_thresholds_ = [None] * n_features
n_bins_non_missing = [None] * n_features

non_cat_thresholds = Parallel(n_jobs=self.n_threads, backend="threading")(
n_features_to_bin = sum(
not self.is_categorical_[f_idx] for f_idx in range(n_features)
)
max_n_threads = _openmp_effective_n_threads(self.max_n_threads)
n_threads = max(
1,
min(
# starting joblib threads is expensive
(n_features_to_bin * X.shape[0]) // 1_000_000,
max_n_threads,
),
)

non_cat_thresholds = Parallel(n_jobs=n_threads, backend="threading")(
delayed(_find_binning_thresholds)(
X[:, f_idx], max_bins, sample_weight=sample_weight
)
Expand Down Expand Up @@ -339,7 +355,9 @@ def transform(self, X):
"to transform()".format(self.n_bins_non_missing_.shape[0], X.shape[1])
)

n_threads = _openmp_effective_n_threads(self.n_threads)
max_n_threads = _openmp_effective_n_threads(self.max_n_threads)
n_threads = max(1, min(round(X.shape[0] / 2000), max_n_threads))

binned = np.zeros_like(X, dtype=X_BINNED_DTYPE, order="F")
_map_to_bins(
X,
Expand Down
55 changes: 51 additions & 4 deletions sklearn/ensemble/_hist_gradient_boosting/gradient_boosting.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# SPDX-License-Identifier: BSD-3-Clause

import itertools
import math
from abc import ABC, abstractmethod
from contextlib import contextmanager, nullcontext, suppress
from functools import partial
Expand Down Expand Up @@ -41,7 +42,10 @@
from sklearn.preprocessing import FunctionTransformer, LabelEncoder, OrdinalEncoder
from sklearn.utils import check_random_state, compute_sample_weight, resample
from sklearn.utils._missing import is_scalar_nan
from sklearn.utils._openmp_helpers import _openmp_effective_n_threads
from sklearn.utils._openmp_helpers import (
_openmp_effective_n_threads,
_openmp_uses_active_wait,
)
from sklearn.utils._param_validation import Interval, RealNotInt, StrOptions
from sklearn.utils.multiclass import check_classification_targets
from sklearn.utils.validation import (
Expand Down Expand Up @@ -523,7 +527,7 @@ def fit(

# `_openmp_effective_n_threads` is used to take cgroups CPU quotes
# into account when determine the maximum number of threads to use.
n_threads = _openmp_effective_n_threads()
max_n_threads = _openmp_effective_n_threads()

if isinstance(self.loss, str):
self._loss = self._get_loss(sample_weight=sample_weight)
Expand Down Expand Up @@ -598,7 +602,7 @@ def fit(
is_categorical=self._is_categorical_remapped,
known_categories=known_categories,
random_state=self._random_seed,
n_threads=n_threads,
max_n_threads=max_n_threads,
)
X_binned_train = self._bin_data(
X_train, sample_weight_train, is_training_data=True
Expand All @@ -610,6 +614,14 @@ def fit(
else:
X_binned_val = None

n_samples, n_features = X_binned_train.shape

n_threads = self._get_heurirstic_optimal_n_threads(
max_n_threads,
n_samples,
n_features,
)

# Uses binned data to check for missing values
has_missing_values = (
(X_binned_train == self._bin_mapper.missing_values_bin_idx_)
Expand All @@ -620,7 +632,6 @@ def fit(
if self.verbose:
print("Fitting gradient boosted rounds:")

n_samples = X_binned_train.shape[0]
scoring_is_predefined_string = self.scoring in _SCORERS
need_raw_predictions_val = X_binned_val is not None and (
scoring_is_predefined_string or self.scoring == "loss"
Expand Down Expand Up @@ -955,6 +966,42 @@ def fit(
del self._in_fit # hard delete so we're sure it can't be used anymore
return self

@staticmethod
def _get_heurirstic_optimal_n_threads(max_n_threads, n_samples, n_features):
"""
Using the maximum number of available threads regardless of the size of
the workload can be counter-productive: parallelizing over very few
features or samples adds thread-management overhead that outweighs the
benefit. This balances ``max_n_threads`` against ``n_features`` (so that
threads are not left idle or unevenly loaded) and against ``n_samples``
(so that small datasets use fewer threads).
"""
active_wait = _openmp_uses_active_wait()
# For very small problems, multi-threading is always counter-productively
min_workload = 20_000 if active_wait else 2_000_000
if n_samples * n_features <= min_workload:
return 1

# Empircally, HGB almost always scales counter-productively past 64 threads
max_n_threads = min(max_n_threads, 64)
if not active_wait and n_samples * n_features <= 20_000_000:
max_n_threads = min(max_n_threads, 4)

# Compute the per-thread chunk size first, then derive how many threads
# are actually needed to cover n_features with that chunk size: this can
# be lower than max_n_threads, avoiding threads with little to no work.
n_features_per_thread = math.ceil(n_features / max_n_threads)
n_threads_for_features = math.ceil(n_features / n_features_per_thread)

if not active_wait:
return n_threads_for_features

# Very empirical: more samples warrant more threads:
n_threads_for_samples = min(0.1 * math.pow(n_samples, 1 / 3), max_n_threads)
heuristic_n_threads = max(n_threads_for_features, n_threads_for_samples)

return round(heuristic_n_threads)

def _is_fitted(self):
return len(getattr(self, "_predictors", [])) > 0

Expand Down
16 changes: 8 additions & 8 deletions sklearn/ensemble/_hist_gradient_boosting/grower.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from sklearn.ensemble._hist_gradient_boosting.predictor import TreePredictor
from sklearn.ensemble._hist_gradient_boosting.splitting import Splitter
from sklearn.utils._bitset import set_raw_bitset_from_binned_bitset
from sklearn.utils._openmp_helpers import _openmp_effective_n_threads


class TreeNode:
Expand Down Expand Up @@ -212,11 +211,12 @@ class TreeGrower:
shrinkage : float, default=1.
The shrinkage parameter to apply to the leaves values, also known as
learning rate.
n_threads : int, default=None
Number of OpenMP threads to use. `_openmp_effective_n_threads` is called
to determine the effective number of threads use, which takes cgroups CPU
quotes into account. See the docstring of `_openmp_effective_n_threads`
for details.
n_threads : int
Number of OpenMP threads to use. Callers are responsible for resolving
this to an actual thread count (e.g. via `_openmp_effective_n_threads`)
and for sizing it down to avoid parallelizing workloads that are too
small to benefit from it (e.g. few features or samples) before calling
this class.

Attributes
----------
Expand Down Expand Up @@ -261,14 +261,14 @@ def __init__(
feature_fraction_per_split=1.0,
rng=np.random.default_rng(),
shrinkage=1.0,
n_threads=None,
*,
n_threads,
):
self._validate_parameters(
X_binned,
min_gain_to_split,
min_hessian_to_split,
)
n_threads = _openmp_effective_n_threads(n_threads)

if n_bins_non_missing is None:
n_bins_non_missing = n_bins - 1
Expand Down
Loading
Loading