8000 MAINT: Make npy_half a strong type by serge-sans-paille · Pull Request #21487 · numpy/numpy · GitHub
[go: up one dir, main page]

Skip to content
< 8000 div class="application-main " data-commit-hovercards-enabled data-discussion-hovercards-enabled data-issue-and-pr-hovercards-enabled data-project-hovercards-enabled >

MAINT: Make npy_half a strong type #21487

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

Conversation

serge-sans-paille
Copy link
Contributor

Instead of using a type alias, make npy_half a struct.
As a consequence, cleanup npy_half usage to always reference function declared in
numpy/halffloat.h. This avoids vodoo incantation in files that should now
nothing of npy_half internal.

Some of numpy_half manipulating functions are promoted to inline to avoid
performance regression.

Copy link
Member
@seberg seberg left a comment

Choose a reason for hiding this comment

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

Some of numpy_half manipulating functions are promoted to inline to avoid
performance regression.

This is worrying me. Is the calling convention for this struct different from the uint16? Because npymath is a library, so the symbols must remain ABI compatible.

Further, the npy_half definition is public. I think we can only modify it if explicitly enabled by the user. I would suggest creating a #define NPY_HALF_IS_STRONG definition (which we always use internally) and documenting it, then use an #ifdef.

The main concern right now is the ABI compatibility within npy_math though.

@seberg
Copy link
Member
seberg commented May 10, 2022

Hmmmpf, I can't see way to work around any possible ABI issues. I think we need to keep the npymath symbols unmodified (using uint16), although, I am willing to bet that those are the only public symbols that pass by value.

Might it work to use macro "aliases" to expand calls to the original (possibly now inlined) functions and add the value.bits? So, all half functions would be macros – at least when NPY_HALF_IS_STRONG is set – and all function definitions will keep using uint16 (we can think about adding a npy_half_bits alias if we like when NPY_HALF_IS_STRONG is set, but not sure it matters)

@eric-wieser
Copy link
Member

Can you elaborate on why you think this is an ABI breakage?

@seberg
Copy link
Member
seberg commented May 10, 2022

It definitely isn't on x64. But is a small struct guaranteed to be passed by value in the same register as the equivalent small integer type?
Maybe that is the wrong question though and the question should be whether there is any actual system with a different calling convention, and the answer is likely "no". (Or maybe I just need to dig into the C standard.)

@eric-wieser
Copy link
Member

Ouch, I hadn't considered that the behavior could be platform specific. I guess that's the type of thing that config_test could check for?

@serge-sans-paille serge-sans-paille force-pushed the feature/npy_half-strong-type branch from f804432 to 62868e4 Compare May 10, 2022 12:49
@charris charris changed the title Make npy_half a strong type MAINT: Make npy_half a strong type May 10, 2022
@serge-sans-paille
Copy link
Contributor Author

I 100% agree, this would not break ABI compatibility on X86_64 linux machines, but would break on other archs / and on Windows that passes all struct argument on the stack. I'll submit a patch that maintain ABI compatibility soon.

@serge-sans-paille serge-sans-paille force-pushed the feature/npy_half-strong-type branch from 4670308 to e0d9b39 Compare May 10, 2022 19:02
@serge-sans-paille
Copy link
Contributor Author

@seberg please tell me if you're fine with current approach: it keeps old symbols while not making them available in the header file, so that each recompiled file will use the new interface. However, the old symbols are still compiled in in the .c so that old programs won't stop working with the new ABI.

extern int npy_half_signbit(npy_half h);
extern npy_half npy_half_neg(npy_half h);
extern npy_half npy_half_abs(npy_half h);
extern npy_half npy_half_pos(npy_half h);
Copy link
Member

Choose a reason for hiding this comment

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

All of these seem fine to move into the header. Maybe just for the sake of it: can we add an (since NumPy 1.24, 2022-05) to the comment so in 5-10 years someone might decide to delete it without too much worry?

We want them as part of the ABI "for backwards compatibility".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment updated, but we don't want these declarations in the header, this leads to multiple definitions. We want them once in this compilation unit.

@serge-sans-paille serge-sans-paille force-pushed the feature/npy_half-strong-type branch from e0d9b39 to 073d38e Compare May 10, 2022 19:56
@serge-sans-paille
Copy link
Contributor Author

The error seems unrelated, otherwise good for review :-)

Copy link
Member
@seberg seberg left a comment

Choose a reason for hiding this comment

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

Not sure if I like the NPY_HALF_INTERNAL_API macro/name (mainly, it doesn't really seem to safe any characters compared to just using the new name?).
And, users will actually be using the "internal" one now...

Since users will be switched over, this is a C-API break. Now the problem I see is, that I am unsure how you would write code that will compile fine against both an old and a new NumPy version. Not being able to do that would be inconvenient.

So, I still think it might be good to introduce an "opt-in" to using the struct version? And probably keep the default at not using it (except for NumPy itself). Or am I missing an easy transition here?

#define NPY_HALF_NEGONE (npy_half){0xbc00u}
#define NPY_HALF_PINF (npy_half){0x7c00u}
#define NPY_HALF_NINF (npy_half){0xfc00u}
#define NPY_HALF_NAN (npy_half){0x7e00u}
Copy link
Member

Choose a reason for hiding this comment

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

Not sure about binding, but may need extra backets to write NPY_HALF_ZERO.bits?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct!

@serge-sans-paille
Copy link
Contributor Author

Not sure if I like the NPY_HALF_INTERNAL_API macro/name (mainly, it doesn't really seem to safe any characters compared to just using the new name?). And, users will actually be using the "internal" one now...
ok, so maybe NPY_HALF_NEW_API ? the goal is not to save characters but to make it explicit that we have two implementations, and a consistent naming scheme.

Since users will be switched over, this is a C-API break. Now the problem I see is, that I am unsure how you would write code that will compile fine against both an old and a new NumPy version. Not being able to do that would be inconvenient.

Yeah, it's a C-API break. In most situation, a recompilation should be just fine, but in some cases adding a few cast / .bits might be needed. Does Numpy have any strong guarentee to that respect?

So, I still think it might be good to introduce an "opt-in" to using the struct version? And probably keep the default at not using it (except for NumPy itself). Or am I missing an easy transition here?

In that case, maybe we should just have two headers, halffloat.h that's backward compatible and halfloat_strong.h that's used internally and relies on struct?

@seberg
Copy link
Member
seberg commented May 11, 2022

I am not worried about the API break itself. Users can keep up and there are not many half users. But such a user should have an easy way to adapt their code so that it will comile against both old and new NumPy versions (and that should be explained in a release note).

Two headers may be a good solution, I am not sure right now where halffloat.h is included (I expected it gets included into the main headers?). But splitting things into two files is probably a good organization in any case.

@serge-sans-paille
Copy link
Contributor Author

I am not worried about the API break itself. Users can keep up and there are not many half users. But such a user should have an easy way to adapt their code so that it will comile against both old and new NumPy versions (and that should be explained in a release note).

ok, and to achieve that a macro-controlled API should be just fine. I understand you want that macro to activate old API by default? If we do so what would be the incentive for client code to move to new API?

@seberg
Copy link
Member
seberg commented May 11, 2022

Honestly, I didn't think about it enough. I am also OK to default to the new definition (modulo any pushback that comes up).
The advantage of sticking with the old one by default for now is that people may only have to adapt once (rather than enforce old version now, and new version later). And it is a bit less to worry about right now ;).

@se
8000
rge-sans-paille serge-sans-paille force-pushed the feature/npy_half-strong-type branch 2 times, most recently from 796b9dd to 75a5855 Compare May 16, 2022 12:34
@serge-sans-paille
Copy link
Contributor Author

@seberg: if you agree with this version, I'll had a release note and a test case for the legacy API

@seberg
Copy link
Member
seberg commented May 17, 2022

I think I am happy with this. Just to summarize for others, the proposal is to:

  • Change npy_half to be a struct, with a single uint16 bits field. (C-API break)
  • Retain ABI compatibility with old versions (name-mangling for the new API, so old symbols remain unchanged).
  • Allow 3rd party code use #define NPY_USE_LEGACY_HALF at import to keep using the old definition. (Backwards compatible API by opt-in, the thought is that npy_half is probably not used all that much, so adding such a guard seems OK)

Macro magic will do the rest ;).

@mattip
Copy link
Member
mattip commented May 17, 2022

Sounds reasonable

@serge-sans-paille serge-sans-paille force-pushed the feature/npy_half-strong-type branch from 75a5855 to a8ea24f Compare May 17, 2022 20:39
@serge-sans-paille
Copy link
Contributor Author

Update done, with the appropriate test case (that detected a few errors in the legacy ABI ;-))
I don't know where I should provide the release note.

@serge-sans-paille serge-sans-paille force-pushed the feature/npy_half-strong-type branch from a8ea24f to cc85dcb Compare May 17, 2022 20:41
@seberg
Copy link
Member
seberg commented May 19, 2022

@serge-sans-paille you add a "fragment" file into doc/release/upcoming_changes, there is a small README in that folder as well. The name will be something like <pr-number>.compatibility.rst (or maybe c_api as category)

Instead of using a type alias, make npy_half a struct.

As a consequence, cleanup npy_half usage to always reference function declared in
numpy/halffloat.h. This avoids vodoo incantation in files that should now
nothing of npy_half internal.

Some of numpy_half manipulating functions are promoted to inline to avoid
performance regression, but symbols are still generated.

This is an ABI breakage.

In order to keep ABI compatibility, older symbols are still built, and new
symbols are prefixed by `npy_strongly_typed_half_` instead of `npy_half_`. This
change should be invisible to the end-user as soon as they include numpy/halffloat.h.

If for any reason, new applications still need to be compiled against the legacy
API, NPY_USE_LEGACY_HALF must be defined prior to including numpy/halffloat.h.
This is not used by any numpy internal code.

A new test, numpy/core/src/npymath/_half_legacy_tests.c, tests that all legacy
symbols are till available.
@serge-sans-paille serge-sans-paille force-pushed the feature/npy_half-strong-type branch from cc85dcb to d76db1b Compare May 19, 2022 04:34
@serge-sans-paille
Copy link
Contributor Author

@seberg : done!

@seberg
Copy link
Member
seberg commented May 19, 2022

Thanks, I don't feel like pushing this in before branching, but bug me if I don't follow-up/review this soon after branching happened :).

@serge-sans-paille
Copy link
Contributor Author

@seberg is now the good time? :-)

@seberg
Copy link
Member
seberg commented Jun 17, 2022

It would be, but I am now a bit confused about the fact that npymath is not dynamically linked at all and what the heck we should do about its future :(.
So, unless you have clarity on that not really mattering much, I suspect we should discuss in a meeting with e.g. Matti. Unfortunately, with SciPy (and general availability), I am not sure when would be a good time within the next few weeks... Maybe right after SciPy in a triage call?

@seberg
Copy link
Member
seberg commented Apr 28, 2023

As valiant as this PR was. @seiko2plus do you think your half PR largely supersedes this?

I realize that this (by default) actually changed the public typedef of npy_half, which has its advantages, but I wonder if we should shelve that for the time being until npymath is no more (in its current form).

@seiko2plus
Copy link
Member

do you think your half PR largely supersedes this?

IMHO, defining half-precision as struct rather than an alias of uint16 is only C++ matters, distinct a data-type in general is necessary for type deducing, overloading and meta programming. So yes having #23298 is more than enough for our internal use.

@seberg
Copy link
Member
seberg commented May 3, 2023

OK, lets close it then for now. I guess there might still be some interesting tidbits here, but there is also the moving part that we probably want to get rid of npymath in some form, so that whole changeset is also outdated.

@seberg seberg closed this May 3, 2023
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.

5 participants
0