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

Skip to content

FIX avoid non-inlined isnan() calls in tree/HGB hot loops - #34876

Open
cakedev0 wants to merge 8 commits into
scikit-learn:mainfrom
cakedev0:fix-isnan-inlining-hot-loops
Open

FIX avoid non-inlined isnan() calls in tree/HGB hot loops#34876
cakedev0 wants to merge 8 commits into
scikit-learn:mainfrom
cakedev0:fix-isnan-inlining-hot-loops

Conversation

@cakedev0

@cakedev0 cakedev0 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Reference Issues/PRs

Fixes #34869

What does this implement/fix? Explain your changes.

As suggested in the issue, I replaced isnan(value) with the self-inequality NaN test (value != value) where relevant.

value != value is mathematically equivalent to isnan(value) for IEEE 754 floats and is pure comparison. There's nothing to inline, on any compiler or libc, ever.

Benchmarked against the exact real conda-forge toolchain, ExtraTreesRegressor.fit, same case as #34869:

build (exact conda-forge toolchain) fit median
unfixed (main) 3292ms
fixed (this PR) 2398ms
reference: real conda-forge published 1.9.0 3257ms
reference: real PyPI 1.9.0 2154ms

The unfixed rebuild also lands right on the real published package's number (3292 vs 3257ms), confirming the reproduction is faithful.
A small residual gap remains (2398 vs 2154ms) that isn't explained by this specific bug, maybe noise. Anyway, not chasing it further here.

AI usage disclosure

I used AI assistance for every steps.

cakedev0 and others added 2 commits September 3, 2026 09:41
isnan() is supposed to always be inlined by the compiler (C11
type-generic macro -> __builtin_isnan*), but on some libc versions
(e.g. glibc 2.17, used by conda-forge's sysroot for binary
compatibility) it expands to a call to non-builtin, non-inlinable
internal symbols (__isnanf/__isnan) instead. This turns a 2-instruction
comparison into a real out-of-line function call, sitting in the
hottest loops of tree building (DensePartitioner.find_min_max,
.sort_samples_and_feature_values) and HGB binning/prediction.

Replace isnan(value) with the self-inequality NaN test
(value != value), which is mathematically equivalent and is pure
comparison: there is nothing to inline, on any compiler or libc.

See scikit-learn#34869 for the full investigation (this explains most of the
reported ExtraTrees fit slowdown on conda-forge builds vs PyPI).
@cakedev0
cakedev0 marked this pull request as ready for review September 3, 2026 11:57

@ogrisel ogrisel 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.

LGTM.

Comment thread sklearn/tree/_partitioner.pyx Outdated
Comment thread sklearn/tree/_partitioner.pyx Outdated
Comment thread sklearn/tree/_partitioner.pyx Outdated
Comment thread sklearn/tree/_partitioner.pyx Outdated
Comment thread sklearn/ensemble/_hist_gradient_boosting/_binning.pyx Outdated
Comment thread sklearn/ensemble/_hist_gradient_boosting/_predictor.pyx Outdated
Comment thread sklearn/tree/_utils.pxd Outdated
@lesteve

lesteve commented Sep 4, 2026

Copy link
Copy Markdown
Member

Would defining our own isnan inline function in a well-chosen .pxd work in terms of performance and be a bit more explicit about the intent as well as less verbose? I.e. something like this (with a better name 😉):

cdef inline bint more_likely_inlined_isnan(double x) noexcept:
    return x != x

Maybe there are still some cases where it wouldn't be inlined but chatting with an AI (chAItting 😁) seems to tell me that our own inline Cython function is more likely to be inlined than lib.math.isnan.

@cakedev0

cakedev0 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

That's a very good idea, and very safe for inlining yes.

There is sklearn/utils/_typedefs.pxd that's already imported everywhere. It's not really a typedef but it's a bit a type things, so I'd say it's not too weird to put it there.

How to name it? isnan might collide so maybe not a good idea; is_nan, is_nan_inlined, ...?

@ogrisel

ogrisel commented Sep 4, 2026

Copy link
Copy Markdown
Member

isnan is fine. Just add a docstring that explains why it's better to use this than calling the libm library function.

@cakedev0

cakedev0 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

I added isnan in sklearn/utils/_typedefs.pxd and verified it still fixes the slowdown ✅

@cakedev0

cakedev0 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Apparently, there is a risk that aggressive floating-point optimization flags remove completely x != x, so I switched for bit-mask (no visible performance impact).

I added a test too, it'll fail if one day someone switches to x != x + fast-math flag.

@lesteve

lesteve commented Sep 4, 2026

Copy link
Copy Markdown
Member

Does using npy_isnan also fix the problem (from numpy cimport npy_isnan)?

If that works, then I guess it's preferable to rely on numpy to do this kind of low-level tricks, rather than rolling our own "solution".

Apparently numpy use the compiler builtin isnan if it exists, and AI is telling me it does at least for gcc and clang (MSVC maybe not).
https://github.com/numpy/numpy/blob/8fecb3b1c054b5eae955b30c334cdf471bdd53ee/numpy/_core/include/numpy/npy_math.h#L212-L218
In particular this comment seems quite relevant for the problem at hand 😉

/* use builtins to avoid function calls in tight loops

@cakedev0

cakedev0 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

My AI says:

I dug into this — the suggestion doesn't actually hold up for scikit-learn. Here's why:

What numpy's npy_isnan does:

#ifdef HAVE___BUILTIN_ISNAN
    #define npy_isnan(x) __builtin_isnan(x)
#else
    #define npy_isnan(x) isnan(x)
#endif

HAVE___BUILTIN_ISNAN only gets defined via numpy's private npy_config.h, which is generated during numpy's own build and is explicitly not shipped in the public headers (numpy/_core/include/numpy/) that downstream packages like scikit-learn compile against. I confirmed this two ways:

  1. npy_config.h doesn't exist anywhere under numpy.get_include().
  2. Grepped scikit-learn's actual ninja build args for every -DHAVE... flag — none are set.

So for scikit-learn, npy_isnan(x) unconditionally expands to plain isnan(x) — i.e. it's a no-op rename of exactly what we already have via libc.math.isnan. I verified this by preprocessing numpy's header with sklearn's real include paths.

@lesteve

lesteve commented Sep 4, 2026

Copy link
Copy Markdown
Member

OK thanks for trying, I still have a small preference for having a different name for our custom isnan. This would make it more easily greppable and easier to differentiate vs libm isnan.

Happy to be overriden on this one, if I am the only one who thinks this 😉. I did see above @ogrisel thinks using the isnan name is fine.

Suggestions:

  • sklearn_isnan (following a bit the np_isnan name)
  • inlinable_isnan
  • something better?

We could also copy what numpy does with using compiler builtin isnan maybe?

@ogrisel

ogrisel commented Sep 4, 2026

Copy link
Copy Markdown
Member

Fine with either.

@cakedev0
cakedev0 marked this pull request as draft September 7, 2026 09:08
@cakedev0

cakedev0 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

I'm putting back this PR as draft:

  • for modern compilation chain this bitmask approach is likely a bit slower
  • there seem to be some movement on the conda-forge side, they might bump glibc for everyone soon (edit: not soon). I'll keep track of where discussions go there.
  • We can also bump it for our recipe only (around here) before the next release.

@cakedev0

Copy link
Copy Markdown
Contributor Author

I chose inlinable_isnan, I like self-explanatory names.

As discussed IRL yesterday, let's do this fix rather than the glibc bump.

@cakedev0
cakedev0 marked this pull request as ready for review September 11, 2026 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Investigation: conda-forge scikit-learn slower than PyPI (ExtraTrees case)

3 participants