Skip to content

Commit 52c2525

Browse files
authored
PERF Faster GradientHistogramBooster by improving the binning algorithm's binary search (scikit-learn#34194)
1 parent aeadb51 commit 52c2525

2 files changed

Lines changed: 54 additions & 15 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Fitting in :class:`sklearn.ensemble.HistGradientBoostingClassifier` and :class:`sklearn.ensemble.HistGradientBoostingRegressor` was optimized to be faster, by speeding up assignment of bins.
2+
By :user:`Itamar Turner-Trauring <itamarst>`.

sklearn/ensemble/_hist_gradient_boosting/_binning.pyx

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Authors: The scikit-learn developers
22
# SPDX-License-Identifier: BSD-3-Clause
33

4+
from math import log2, ceil
5+
46
from cython.parallel import prange
57
from libc.math cimport isnan
68

@@ -49,18 +51,23 @@ def _map_to_bins(const X_DTYPE_C [:, :] data,
4951

5052
cdef void _map_col_to_bins(
5153
const X_DTYPE_C [:] data,
52-
const X_DTYPE_C [:] binning_thresholds,
54+
const X_DTYPE_C [::1] binning_thresholds,
5355
const uint8_t is_categorical,
5456
const uint8_t missing_values_bin_idx,
5557
int n_threads,
56-
X_BINNED_DTYPE_C [:] binned
58+
X_BINNED_DTYPE_C [::1] binned
5759
):
5860
"""Binary search to find the bin index for each value in the data."""
5961
cdef:
6062
int i
61-
int left
62-
int right
63-
int middle
63+
uint8_t n_iter_bin_search
64+
size_t n_bin_thresholds
65+
66+
n_bin_thresholds = len(binning_thresholds)
67+
if n_bin_thresholds == 0:
68+
n_iter_bin_search = 0
69+
else:
70+
n_iter_bin_search = int(ceil(log2(n_bin_thresholds)))
6471

6572
for i in prange(data.shape[0], schedule='static', nogil=True,
6673
num_threads=n_threads):
@@ -73,13 +80,43 @@ cdef void _map_col_to_bins(
7380
binned[i] = missing_values_bin_idx
7481
else:
7582
# for known values, use binary search
76-
left, right = 0, binning_thresholds.shape[0]
77-
while left < right:
78-
# equal to (right + left - 1) // 2 but avoids overflow
79-
middle = left + (right - left - 1) // 2
80-
if data[i] <= binning_thresholds[middle]:
81-
right = middle
82-
else:
83-
left = middle + 1
84-
85-
binned[i] = left
83+
binned[i] = _binary_search(
84+
data[i],
85+
binning_thresholds,
86+
n_bin_thresholds,
87+
n_iter_bin_search
88+
)
89+
90+
91+
cdef inline size_t _binary_search(
92+
X_DTYPE_C value,
93+
const X_DTYPE_C [::1] binning_thresholds,
94+
size_t size,
95+
uint8_t n_iterations
96+
) noexcept nogil:
97+
cdef:
98+
size_t left
99+
size_t half
100+
size_t middle
101+
size_t remaining_size
102+
103+
# This implementation is designed to minimize branch mispredictions. See:
104+
# https://pvk.ca/Blog/2012/07/03/binary-search-star-eliminates-star-branch-mispredictions/
105+
# https://pvk.ca/Blog/2015/11/29/retrospective-on-binary-search-and-on-compression-slash-compilation/
106+
left = 0
107+
remaining_size = size
108+
109+
# Fixed number of loops, instead of less-predictable while loop:
110+
for _ in range(n_iterations):
111+
half = remaining_size / 2
112+
middle = left + half
113+
# Try for cmov instead of branch; see
114+
# https://en.algorithmica.org/hpc/pipelining/branchless/ for details:
115+
left = middle if (binning_thresholds[middle] < value) else left
116+
remaining_size -= half
117+
118+
# Try for cmov instead of branch:
119+
left = left + 1 if (
120+
(left < size) and (value > binning_thresholds[left])
121+
) else left
122+
return left

0 commit comments

Comments
 (0)