8000 MAINT:Translate the entirety of cephes into C++ by steppi · Pull Request #20390 · scipy/scipy · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

MAINT:Translate the entirety of cephes into C++ #20390

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 100 commits into from
Apr 20, 2024

Conversation

steppi
Copy link
Contributor
@steppi steppi commented Apr 4, 2024

Update: I've rewritten this PR message since the original I wrote when this was a work-in-progress is no longer relevant.

This PR translates the entirety of Cephes into C++ capable of running on CUDA through conditional compilation. This PR is rebased on #20405 which is rebased on #20089 which is rebased on #19954, so though still gargantuan, the true diff is a little smaller than what is seen now.

Most of this was fairly straightforward, there was just a lot of it. One challenge was translating double-double arithmetic from cephes/dd_real.c and friends (actually from the qd extended precision package) into C++. I chose to do this in a more modern style with operator overloading.

The old cephes has been removed entirely. I wrote C wrappers for use in Cython and C files which have not been translated to C++. special_c_wrappers.cpp for special functions, and dd_real_wrappers.cpp for double double arithmetic, which is used in _cunity.pxd and which has test code written in Cython.

You have your work cut out for you @izaid. Thanks for agreeing to review this in your time between terms.

@steppi steppi requested a review from person142 as a code owner April 4, 2024 19:59
@github-actions github-actions bot added scipy.special C/C++ Items related to the internal C/C++ code base Cython Issues with the internal Cython code base Meson Items related to the introduction of Meson as the new build system for SciPy maintenance Items related to regular maintenance tasks labels Apr 4, 2024
@steppi steppi marked this pull request as draft April 4, 2024 20:00
@steppi steppi removed the request for review from person142 April 4, 2024 20:00
@steppi
Copy link
Contributor Author
steppi commented Apr 4, 2024

This is rebased on the branch for #20089 which is in turn rebased on the branch for #19954. If someone merges those, the diff on this one will shrink a little. cc @rlucas7, @tirthasheshpatel, @rgommers

@steppi
Copy link
Contributor Author
steppi commented Apr 4, 2024

I’ll investigate more closely later, but it seems like the failures in stats on Linuxes 32 bit and aarch64 would be ok to resolve with tolerance bumps. Some of them are exact equality checks, though maybe there’s a reason for that.

@rlucas7
Copy link
Member
rlucas7 commented Apr 5, 2024 via email

@steppi
Copy link
Contributor Author
steppi commented Apr 5, 2024

I’ll take a look this weekend.

Thanks. I appreciate it.

@steppi steppi marked this pull request as ready for review April 6, 2024 03:58
@steppi steppi changed the title [WIP] MAINT:Translate the entirety of cephes into C++ MAINT:Translate the entirety of cephes into C++ Apr 6, 2024
@steppi
Copy link
Contributor Author
steppi commented Apr 6, 2024

I've completely excised the old C cephes and all tests pass locally. What remains is

  1. Figure out if the couple of failing stats tests can just have their tolerances bumped.
  2. Add a healthy number of comments explaining why some things are structured as they are. Particularly for wrappers for calling cephes functions from C and Cython, and any time where cephes double double arithmetic is used outside of cephes.

The trickiest thing in this PR was probably getting the double double arithmetic to work. I opted to write it in a more modern style using operator overloading. Fortunately there was already direct testing for the double double arithmetic, because these tests failed at first despite all tests for functions passing.

Phew. That is the largest diff I've ever worked on.

@steppi
Copy link
Contributor Author
steppi commented Apr 7, 2024

I've tried to rebase this on #20405, but things won't compile due to unsuccessful resolution of conflicts with #20403, which I merged today. @izaid, can you submit a PR to this branch fixing these conflicts? I could probably figure it out, but I'm running out of steam. The issue seems to be with using old cephes functions which no longer exist in the new ufunc infrastructure.

Here's a sample of the errors I get when I try to compile.

../scipy/special/_special_ufuncs.cpp: In function ‘PyObject* PyInit__special_ufuncs()’:
../scipy/special/_special_ufuncs.cpp:184:10: error: invalid ‘static_cast’ from type ‘int(double, double*, double*, double*, 
8000
double*)’ to type ‘func_d_dpdpdpdp_t’ {aka ‘void (*)(double, double*, double*, double*, double*)’}
  184 |         {static_cast<func_d_dpdpdpdp_t>(special::airy_wrap), static_cast<func_D_DpDpDpDp_t>(special::cairy_wrap)}, 4,
      |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
../scipy/special/_special_ufuncs.cpp:184:62: error: invalid ‘static_cast’ from type ‘int(std::complex<double>, std::complex<double>*, std::complex<double>*, std::complex<double>*, std::complex<double>*)’ to type ‘func_D_DpDpDpDp_t’ {aka ‘void (*)(std::complex<double>, std::complex<double>*, std::complex<double>*, std::complex<double>*, std::complex<double>*)’}
  184 |         {static_cast<func_d_dpdpdpdp_t>(special::airy_wrap), static_cast<func_D_DpDpDpDp_t>(special::cairy_wrap)}, 4,
      |                                                              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
../scipy/special/_special_ufuncs.cpp:183:38: error: no matching function for call to ‘SpecFun_NewUFunc(<brace-enclosed initializer list>, int, const char [5], const char*&)’
  183 |     PyObject *airy = SpecFun_NewUFunc(
      |                      ~~~~~~~~~~~~~~~~^
  184 |         {static_cast<func_d_dpdpdpdp_t>(special::airy_wrap), static_cast<func_D_DpDpDpDp_t>(special::cairy_wrap)}, 4,
      |         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  185 |         "airy", airy_doc);
      |         ~~~~~~~~~~~~~~~~~             
In file included from ../scipy/special/_special_ufuncs.cpp:17:
../scipy/special/ufunc.h:220:11: note: candidate: ‘PyObject* SpecFun_NewUFunc(std::initializer_list<SpecFun_Func>, int, const char*, const char*)’
  220 | PyObject *SpecFun_NewUFunc(std::initializer_list<SpecFun_Func> func, int nout, const char *name, const char *doc) {
      |           ^~~~~~~~~~~~~~~~
../scipy/special/ufunc.h:220:64: note:   no known conversion for argument 1 from ‘<brace-enclosed initializer list>’ to ‘std::initializer_list<SpecFun_Func>’
  220 | PyObject *SpecFun_NewUFunc(std::initializer_list<SpecFun_Func> func, int nout, const char *name, const char *doc) {
      |                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~
../scipy/special/ufunc.h:239:11: note: candidate: ‘PyObject* SpecFun_NewUFunc(std::initializer_list<SpecFun_Func>, const char*, const char*)’
  239 | PyObject *SpecFun_NewUFunc(std::initializer_list<SpecFun_Func> func, const char *name, const char *doc) {
      |           ^~~~~~~~~~~~~~~~
../scipy/special/ufunc.h:239:11: note:   candidate expects 3 arguments, 4 provided
../scipy/special/_special_ufuncs.cpp:188:41: error: invalid ‘static_cast’ from type ‘int(double, double*, double*, double*, double*)’ to type ‘func_d_dpdpdpdp_t’ {aka ‘void (*)(double, double*, double*, double*, double*)’}
  188 |     PyObject *airye = SpecFun_NewUFunc({static_cast<func_d_dpdpdpdp_t>(special::cairy_wrap_e_real),
      |                                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
../scipy/special/_special_ufuncs.cpp:189:41: error: invalid ‘static_cast’ from type ‘int(std::complex<double>, std::complex<double>*, std::complex<double>*, std::complex<double>*, std::complex<double>*)’ to type ‘func_D_DpDpDpDp_t’ {aka ‘void (*)(std::complex<double>, std::complex<double>*, std::complex<double>*, std::complex<double>*, std::complex<double>*)’}
  189 |                                         static_cast<func_D_DpDpDpDp_t>(special::cairy_wrap_e)},
      |                                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
../scipy/special/_special_ufuncs.cpp:188:39: error: no matching function for call to ‘SpecFun_NewUFunc(<brace-enclosed initializer list>, int, const char [6], const char*&)’
  188 |     PyObject *airye = SpecFun_NewUFunc({static_cast<func_d_dpdpdpdp_t>(special::cairy_wrap_e_real),
      |                       ~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  189 |                                         static_cast<func_D_DpDpDpDp_t>(special::cairy_wrap_e)},
      |                                         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  190 |                                        4, "airye", airye_doc);
      |                                        ~~~~~~~~~~~~~~~~~~~~~~
In file included from ../scipy/special/_special_ufuncs.cpp:17:
../scipy/special/ufunc.h:220:11: note: candidate: ‘PyObject* SpecFun_NewUFunc(std::initializer_list<SpecFun_Func>, int, const char*, const char*)’
  220 | PyObject *SpecFun_NewUFunc(std::initializer_list<SpecFun_Func> func, int nout, const char *name, const char *doc) {
      |           ^~~~~~~~~~~~~~~~
../scipy/special/ufunc.h:220:64: note:   no known conversion for argument 1 from ‘<brace-enclosed initializer list>’ to ‘std::initializer_list<SpecFun_Func>’
  220 | PyObject *SpecFun_NewUFunc(std::initializer_list<SpecFun_Func> func, int nout, const char *name, const char *doc) {
      |                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~
../scipy/special/ufunc.h:239:11: note: candidate: ‘PyObject* SpecFun_NewUFunc(std::initializer_list<SpecFun_Func>, const char*, const char*)’
  239 | PyObject *SpecFun_NewUFunc(std::initializer_list<SpecFun_Func> func, const char *name, const char *doc) {
      |           ^~~~~~~~~~~~~~~~
../scipy/special/ufunc.h:239:11: note:   candidate expects 3 arguments, 4 provided
../scipy/special/_special_ufuncs.cpp:276:35: error: ‘cephes_iv’ was not declared in this scope
  276 |         {static_cast<func_dd_d_t>(cephes_iv), static_cast<func_dD_D_t>(special::cbesi_wrap)}, "iv", iv_doc);
      |                                   ^~~~~~~~~
../scipy/special/_special_ufuncs.cpp:275:36: error: no matching function for call to ‘SpecFun_NewUFunc(<brace-enclosed initializer list>, const char [3], const char*&)’
  275 |     PyObject *iv = SpecFun_NewUFunc(
      |                    ~~~~~~~~~~~~~~~~^
  276 |         {static_cast<func_dd_d_t>(cephes_iv), static_cast<func_dD_D_t>(special::cbesi_wrap)}, "iv", iv_doc);
      |         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from ../scipy/special/_special_ufuncs.cpp:17:
../scipy/special/ufunc.h:220:11: note: candidate: ‘PyObject* SpecFun_NewUFunc(std::initializer_list<SpecFun_Func>, int, const char*, const char*)’
  220 | PyObject *SpecFun_NewUFunc(std::initializer_list<SpecFun_Func> func, int nout, const char *name, const char *doc) {
      |           ^~~~~~~~~~~~~~~~
../scipy/special/ufunc.h:220:11: note:   candidate expects 4 arguments, 3 provided
../scipy/special/ufunc.h:239:11: note: candidate: ‘PyObject* SpecFun_NewUFunc(std::initializer_list<SpecFun_Func>, const char*, const char*)’
  239 | PyObject *SpecFun_NewUFunc(std::initializer_list<SpecFun_Func> func, const char *name, const char *doc) {
      |           ^~~~~~~~~~~~~~~~
../scipy/special/ufunc.h:239:64: note:   no known conversion for argument 1 from ‘<brace-enclosed initializer list>’ to ‘std::initializer_list<SpecFun_Func>’
  239 | PyObject *SpecFun_NewUFunc(std::initializer_list<SpecFun_Func> func, const char *name, const char *doc) {
      |                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~
[59/85] Compiling C++ object scipy/special/_ufuncs_cxx.cpython-311-x86_64-linux-gnu.so.p/meson-generated__ufuncs_cxx.cpp.o
ninja: build stopped: subcommand failed.
Build failed!

@izaid
Copy link
Contributor
izaid commented Apr 7, 2024

I've tried to rebase this on #20405, but things won't compile due to unsuccessful resolution of conflicts with #20403, which I merged today. @izaid, can you submit a PR to this branch fixing these conflicts? I could probably figure it out, but I'm running out of steam. The issue seems to be with using old cephes functions which no longer exist in the new ufunc infrastructure.

Here's a sample of the errors I get when I try to compile.

Think we fixed this now.

@steppi
Copy link
Contributor Author
steppi commented Apr 7, 2024

Think we fixed this now.

Yes, fixed thanks to your help. I missed that this morning when editing out comments that were no longer relevant.

@ilayn
Copy link
Member
ilayn commented Apr 7, 2024

One detail I am starting to see is that we are using inline keyword everywhere. Though the compiler is free to ignore it or inline it when there is no inline, we don't need to force it all the time because it has the risk of bloating the binaries due to repetitive code expansion. Just FYI. And I did a similar mistake in the Cython code too but I think we don't need repeat the mistake again.

@ilayn
Copy link
Member
ilayn commented Apr 7, 2024

I think it is a bit too late but If you also use the git's rename option then it would be possible to get a smaller diff and also a chance to still see the git history.

@izaid
Copy link
Contributor
izaid commented Apr 7, 2024

One detail I am starting to see is that we are using inline keyword everywhere. Though the compiler is free to ignore it or inline it when there is no inline, we don't need to force it all the time because it has the risk of bloating the binaries due to repetitive code expansion. Just FYI. And I did a similar mistake in the Cython code too but I think we don't need repeat the mistake again.

Thanks @ilayn! Good point, but it actually does have a use in C++. It allows a function to appear in two different translation units, see the first answer here for instance https://stackoverflow.com/questions/29796264/is-there-still-a-use-for-inline
We are doing it by design. :)

@steppi
Copy link
Contributor Author
steppi commented Apr 7, 2024

One detail I am starting to see is that we are using inline keyword everywhere. Though the compiler is free to ignore it or inline it when there is no inline, we don't need to force it all the time because it has the risk of bloating the binaries due to repetitive code expansion. Just FYI. And I did a similar mistake in the Cython code too but I think we don't need repeat the mistake again.

I think making everything inline is necessary to avoid symbol clashes in a header only library. I was getting errors in some cases when leaving them out, in particular for functions which are used in ufuncs but also called in C code through wrappers. Things need to be header only so they can work on CUDA, but I've been trying to think of ways to mitigate increase in binary size.

Update: @izaid beat me to it.

@izaid
Copy link
Contributor
izaid commented Apr 7, 2024

One detail I am starting to see is that we are using inline keyword everywhere. Though the compiler is free to ignore it or inline it when there is no inline, we don't need to force it all the time because it has the risk of bloating the binaries due to repetitive code expansion. Just FYI. And I did a similar mistake in the Cython code too but I think we don't need repeat the mistake again.

I think making everything inline is necessary to avoid symbol clashes in a header only library. I was getting errors in some cases when leaving them out, in particular for functions which are used in ufuncs but also called in C code through wrappers. Things need to be header only so they can work on CUDA, but I've been trying to think of ways to mitigate increase in binary size.

Update: @izaid beat me to it.

I believe there are ways to have it instantiate only once. But if we put all the ufuncs in a single Python extension like _special_ufuncs.cpp, we won't need that anyway.

@steppi
Copy link
Contributor Author
steppi commented Apr 7, 2024

I think it is a bit too late but If you also use the git's rename option then it would be possible to get a smaller diff and also a chance to still see the git history.

I don't think it's too late actually. While I was working on this, I had to keep the old cephes around, and didn't think of the now obvious idea of backing up each cephes file before I translated it, doing git mv, and then replacing the cephes C file with its backup. Even before you mentioned it, I've thought about backing up the new C++ files, starting over, and doing git mv, and then immediately replacing each with its updated C++ version. It wouldn't be anywhere near as tedious as translating 80+ files from cephes, so I'd be happy to do it if it will make review easier.

Before I do that though, I want to try to fix the failing tests on 32 bit Linux, but just haven't had time to debug yet.

#include "cephes/dd_idefs.h"


/* Computes fl(a+b) and err(a+b). */
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we should change anything here, but I'm curious if we really need volatile.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did some research to find why the volatile was there, and apparently it is to prevent aggressive optimizations from altering the results, which may have been more of a problem back then. Now it's only an issue if someone compiles with --ffastmath I think.

/* multiply by power of 2 */
x = std::ldexp(x, n);

return (x);
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't know why there are brackets here, but also there are a lot of historical things that got carried over from cephes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's just the coding style used in cephes. It was probably popular at the time. When I first started, I may have got rid of them, but after a while I just starting leaving all of the return statements unchanged.

@steppi
Copy link
Contributor Author
steppi commented Apr 19, 2024

The failures look unrelated. I think this is good to merge.

cc @rgommers

Copy link
Contributor
@izaid izaid left a comment

Choose a reason for hiding this comment

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

All looks good to me, this is a huge amount of work!

Copy link
Member
@rgommers rgommers left a comment

Choose a reason for hiding this comment

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

Thanks @steppi and @izaid, this looks like a large amount of very nice work. I went through the whole diff (that alone was a bit of work:) ), resolved comments that were addressed, and checked the build time impact (which seems fine).

There was a single unaddressed comments about static variables left - please have a look at that. Aside from that it seems good to go, so I'll hit the approve button here.

Related: when testing I found a build warning (with GCC 12) that came from a previous PR - it'd be nice to address that in a follow-up:

In file included from ../scipy/special/_special_ufuncs.cpp:15:
../scipy/special/special/gamma.h: In function 'std::complex<_Tp> special::gamma(std::complex<_Tp>) [with T = float]':
../scipy/special/special/gamma.h:19:44: warning: infinite recursion detected [-Winfinite-recursion]
   19 | SPECFUN_HOST_DEVICE inline std::complex<T> gamma(std::complex<T> z) {
      |                                            ^~~~~
../scipy/special/special/gamma.h:20:17: note: recursive call
   20 |     return gamma(z);
      |            ~~~~~^~~

double ak, bk, akl, bkl;
int sign, doa, dob, nflg, k, s, tk, tkp1, m;
static double u[8];
static double ai, aip, bi, bip;
Copy link
Member

Choose a reason for hiding this comment

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

@steppi this is still unaddressed

@lucascolley lucascolley added this to the 1.14.0 milestone Apr 19, 2024
@steppi
Copy link
Contributor Author
steppi commented Apr 19, 2024

Related: when testing I found a build warning (with GCC 12) that came from a previous PR - it'd be nice to address that in a follow-up:

Ah yes, in scipy/special/special/loggamma.h there's a complex valued gamma implementation with signature

std::complex<double> gamma(std::complex<double> z)

that lives in the special namespace.

@izaid's intention in scipy/special/special/gamma.h here:

template <typename T>
SPECFUN_HOST_DEVICE inline std::complex<T> gamma(std::complex<T> z) {
return gamma(z);
}

is to write a templated version, (also in the special namespace), which can handle 32 bit float input in the ufuncs, by passing the inputs to the double version, but that the resolution works correctly seems like it might be based on undefined behavior. @izaid, what do you think we should do here? Just explicitly write a gamma with signature

std::complex<float> gamma(std::complex<float> z)

maybe?

@steppi
Copy link
Contributor Author
steppi commented Apr 19, 2024

Thanks @rgommers! I appreciate that you went to the length of checking the full diff. Good catch that I missed the comment about the static variables.

@izaid
Copy link
Contributor
izaid commented Apr 19, 2024

Related: when testing I found a build warning (with GCC 12) that came from a previous PR - it'd be nice to address that in a follow-up:

Ah yes, in scipy/special/special/loggamma.h there's a complex valued gamma implementation with signature

std::complex<double> gamma(std::complex<double> z)

that lives in the special namespace.

@izaid's intention in scipy/special/special/gamma.h here:

template <typename T>
SPECFUN_HOST_DEVICE inline std::complex<T> gamma(std::complex<T> z) {
return gamma(z);
}

is to write a templated version, (also in the special namespace), which can handle 32 bit float input in the ufuncs, by passing the inputs to the double version, but that the resolution works correctly seems like it might be based on undefined behavior. @izaid, what do you think we should do here? Just explicitly write a gamma with signature

std::complex<float> gamma(std::complex<float> z)

maybe?

Yeah, that should just be calling our complex gamma function. I can fix it in another PR, it's unrelated to anything here.

@rgommers rgommers merged commit f305bdf into scipy:main Apr 20, 2024
@rgommers
Copy link
Member

All right - merged! Thanks a lot @steppi and @izaid 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C/C++ Items related to the internal C/C++ code base Cython Issues with the internal Cython code base maintenance Items related to regular maintenance tasks Meson Items related to the introduction of Meson as the new build system for SciPy scipy.special
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants
0