Skip to content
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

Extend HPy call API #251

Merged
merged 16 commits into from
Apr 13, 2023
Merged

Extend HPy call API #251

merged 16 commits into from
Apr 13, 2023

Conversation

fangerer
Copy link
Contributor

@fangerer fangerer commented Oct 19, 2021

As discussed in #122 in this month's dev call, this PR adds APIs for calling functions and methods.
We don't want to add all the specific API functions like in the C API but we want to provide functions for two general use cases:

  • Call some callable or method by using some existing arguments tuple or keywords dict (probably created in Python land)
  • Call some callable or method by crafting the arguments in C.

Hence, this PR adds following:

  • HPy_CallMethodTupleDict/ HPy_CallMethodTupleDict_s:
    Call a method using an argument tuple and a keyword dict.
    The name is given as Python unicode object (first variant) or as C string (suffix _s).
  • HPy_CallVectorDict:
    Call a callable object using a handle array and a keyword dict.
  • HPy_CallMethodVectorDict:
    Call a method using a handle array and a keyword dict.

I'm still not sure about the best way how to specify the keywords.
CPython 3.9 basically provides both variants: providing them as Python dict or providing an array of C strings (and the values are appended to the arguments array).
For now, I've implemented the dict variant because I think it's not very common to craft and pass keyword args in C.
According to an analysis of the top4000, the most frequently used functions are PyObject_CallMethod and PyObject_CallFunction and none of those allows to pass keyword arguments.

@hodgestar
Copy link
Contributor

The previous discussion of some of the options: #122

@fangerer
Copy link
Contributor Author

🤦 Thanks, @hodgestar, completely overlooked this issue.

@fangerer fangerer changed the title WIP: Add HPy_CallMethod Add method call and vector call functions. Nov 10, 2021
@fangerer
Copy link
Contributor Author

@hodgestar @rlamy @antocuni @mattip This should be ready for a second review round (actually, the first serious one).

@hodgestar
Copy link
Contributor

@fangerer PyObject_Vectorcall is unfortunately not a simple call in CPython -- it's a calling interface to an underlying PyObject protocol https://docs.python.org/3/c-api/call.html#the-vectorcall-protocol.

For example, nargsf is not the number of arguments, but rather the number of arguments XORed with a flag and if the flag is set then args is not just the pointer to the start of an array of arguments, but a pointer to the second argument of an array instead (but only in the case of PyObject_Vectorcall and not PyObject_VectorcallMethod.

PyObject_VectorcallDict is not fast, and so neither HPy_CallVectorDict or HPy_CallVectorMethod can be fast either. The "fast" calls are PyObject_Vectorcall and PyObject_VectorcallMethod which can call the underlying C function directly without having to repack arguments.

If we plan to implement the vectorcall protocol, we should probably add it to objects too and match the semantics and name the methods to match (e.g. HPyVectorcall_XXX).

If we don't plan to implement the vectorcall protocol, we should probably not use vector in our API function names and should probably choose more sensible semantics.

Hopefully this doesn't mess with your plans too much -- I discovered all of this while writing up #122 ages ago, but I don't think it made it into the issue write up very clearly. :/

@fangerer
Copy link
Contributor Author

@hodgestar Thanks for your comments.

PyObject_Vectorcall is unfortunately not a simple call in CPython

Sure, but it's the API with the lowest overhead if tp_vectorcall is provided (by the user; not the generic one), right?
That's why I used it to implemented ctx_Call*VectorDict. We can improve that by testing if PyType_HasFeature(type, Py_TPFLAGS_HAVE_VECTORCALL) and then decide how to call it.

For example, nargsf is not the number of arguments, but rather the number of arguments XORed with a flag and if the flag is set then args is not just the pointer to the start of an array of arguments, but a pointer to the second argument of an array instead (but only in the case of PyObject_Vectorcall and not PyObject_VectorcallMethod.

I'm aware of that and I'm sure that we agreed on not not doing the same.

PyObject_VectorcallDict is not fast

It's still fast if no keyword arguments were specified because it then doesn't need to unpack anything. Sorry, I forgot to add some comments about this. According to the analysis of the top4000 packages (https://paste.opendev.org/show/bPLIEafAmoHq5prb9F8f/), the most frequently used call functions are PyObject_CallMethod and PyObject_CallFunction and neither of them allows to pass keyword args. So, my assumption basically was that keyword args are rarely provided.
Anyway, that's something I wanted to discuss: should we use a signature similar to PyObject_Vectorcall (i.e. having a const char *kwnames instead of the dict) or should we pass keywords as dict?

If we plan to implement the vectorcall protocol, we should probably add it to objects too and match the semantics and name the methods to match (e.g. HPyVectorcall_XXX).
If we don't plan to implement the vectorcall protocol, we should probably not use vector in our API function names and should probably choose more sensible semantics.

Good question. I'm not sure if it makes sense to implement the vectorcall protocol. CPython also uses it internally but PyPy and Graalpython don't. Furthermore, our HPyFunc_KEYWORDS signature is already compatible to HPy_CallMethodVectorDict. So, I thought this would be a good fit. I suggest to remove the Vector from the names.

Hopefully this doesn't mess with your plans too much -- I discovered all of this while writing up #122 ages ago, but I don't think it made it into the issue write up very clearly. :/

Don't worry, I just try to make some progress. There is no need to rush such things.

for(int i=0; i<nargs; i++) {
uh_args[i] = DHPy_unwrap(dctx, dh_args[i]);
}
return DHPy_open(dctx, HPy_CallMethodVectorDict(get_info(dctx)->uctx, DHPy_unwrap(dctx, dh_receiver), DHPy_unwrap(dctx, dh_name), uh_args, nargs, DHPy_unwrap(dctx, dh_kw)));
Copy link
Collaborator

Choose a reason for hiding this comment

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

nitpick: we don't have a official style guidelines (maybe we should?) but I think that 178 chars is definitely too long for a single line :)

hpy/devel/include/hpy/cpython/misc.h Show resolved Hide resolved

#if PY_VERSION_HEX >= 0x03080000
PyObject **py_args = (PyObject **) alloca(nargs * sizeof(PyObject *));
_harr2pyarr(py_args, 0, args, nargs);
Copy link
Collaborator

Choose a reason for hiding this comment

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

see my comment above. On the CPython ABI, this should be a noop cast

py_args = (PyObject **) alloca((nargs + 1) * sizeof(PyObject *));
py_args[0] = _h2py(receiver);
_harr2pyarr(py_args, 1, args, nargs);
result = PyObject_VectorcallMethod(_h2py(name), py_args, nargs + 1, NULL);
Copy link
Collaborator

Choose a reason for hiding this comment

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

same comment as above about _harr2pyarr on CPython ABI, but this is trickier (impossible?) because of the first argument.

PEP 590/VectorCall tries to solve a similar issue by using PY_VECTORCALL_ARGUMENTS_OFFSET which allows to mutate the first argument of the vector.

However, I think that the only way to take advantage of this would be to refactor the C signature of HPy_CallMethodVectorDict to pass receiver as the args[0]. But the for consistency we should probably change also the signatue of our HPyFunc_KEYWORDS methods... what a mess 🤦‍♂️ .

hpy/devel/src/runtime/ctx_call.c Show resolved Hide resolved
)
for receiver, args_tuple, kw in test_args:
assert mod.call(receiver, *args_tuple, kw=kw) == receiver(*args_tuple, **kw)

Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe it's just me, but I find this style of testing very difficult to read. To start with the comment # (receiver, method, args_tuple) looks wrong.
I think the following is orders or magnitude easier to read.

assert mod.call(dict, dict(a=0, b=1), kw={}) == dict((dict(a=0, b=1)) # or even == {'a': 0, 'b': 1}
assert mod.call(dict, kw=dict(a=0, b=1)) == dict(a=0, b=1)

If the code above is not equivalent to the original code, it's a good sign that the original code is too complex to read 😅.

Feel free to ignore this suggestion though.

@antocuni
Copy link
Collaborator

I left some comments inline in the review. However, this probably requires a much deeper discussion.
@hodgestar already mentioned the discussion in #122 , but we already had more discussions in #147 , in particular #147 (comment).
Probably it would be best to keep the discussions about API design in the proper issue and keep the PRs for discussing only implementation details. Let's continue the discussion on #122.

@wjakob
Copy link

wjakob commented Nov 10, 2022

Hi all -- just a quick bit of feedback after having been pointed to this PR by @mattip.

I looks like HPy at this point provides most of the facilities that would be needed to create an extra backend for nanobind (A hobby project of mine for C++ <-> Python interop, it's basically a simpler & faster version of pybind11), which would be very cool.

However: nanobind derives a quite significant part of its speedup through the ability to receive and perform vector calls. As mentioned by @hodgestar above, the API proposed here involves quite a bit of boxing/unboxing and doesn't expose the fast way of performing calls.

In response to @fangerer 's comment:

Good question. I'm not sure if it makes sense to implement the vectorcall protocol.

My answer would be a resounding YES!! 😊

@fangerer
Copy link
Contributor Author

@wjakob thanks for you comment and I agree.

Since this PR is already a bit older, I'm not sure if I remember all the details correctly. The vectorcall protocol as such makes, for sure, sense in CPython. My question, if it makes sense, was because in HPy we would have the possibility to define a different protocol. But then we again need to do (costly) conversions in some cases which we should avoid.

looks like HPy at this point provides most of the facilities that would be needed to create an extra backend for nanobind

I, of course, know nanobind and would love to see an HPy backend for it.

I will revisit this PR in the near future but this PR alone won't be enough (since it's just about an object calling API like https://docs.python.org/3/c-api/call.html#object-calling-api). We will also need to have the ability to define types that implement the vectorcall protocol (i.e. specify tp_vectorcall_offset).

@wjakob
Copy link

wjakob commented Nov 11, 2022

Great, I am happy to hear it.

We will also need to have the ability to define types that implement the vectorcall protocol (i.e. specify tp_vectorcall_offset).

While object call protocol is helpful for callbacks and, e.g., to extend C++ classes in Python and override virtual functions there, those are IMO less common use cases. Binding code mostly involves C or C++ code called from Python, so it's the tp_vectorcall_offset part that is the truly performance-critical part of the vector call protocol. (For example, nanobind routes all Python->C++ function calls through a custom function object that sets tp_vectorcall_offset)

@mattip
Copy link
Contributor

mattip commented Jan 31, 2023

This has conflicts

@fangerer fangerer changed the title Add method call and vector call functions. Extend HPy call API Apr 3, 2023
@fangerer
Copy link
Contributor Author

fangerer commented Apr 3, 2023

Big update on this PR: After PR #390 was merged, it now makes sense to introduce a call API that aligns with the signature of HPyFunc_keywords. Therefore, I've introduced HPy_Call and HPy_CallMethod that uses the same calling convention and can (in CPython ABI mode) directly be mapped to PyObject_Vectorcall and PyObject_VectorcallMethod, respectively.

Copy link
Collaborator

@antocuni antocuni left a comment

Choose a reason for hiding this comment

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

LGTM!

hpy/devel/include/hpy/cpython/misc.h Outdated Show resolved Hide resolved
hpy/devel/include/hpy/cpython/misc.h Outdated Show resolved Hide resolved
hpy/devel/include/hpy/cpython/misc.h Show resolved Hide resolved
hpy/devel/include/hpy/inline_helpers.h Outdated Show resolved Hide resolved
hpy/devel/src/runtime/ctx_call.c Outdated Show resolved Hide resolved
hpy/devel/src/runtime/ctx_call.c Outdated Show resolved Hide resolved
hpy/devel/src/runtime/ctx_call.c Show resolved Hide resolved
@fangerer fangerer merged commit 0e6c6fc into hpyproject:master Apr 13, 2023
@fangerer fangerer deleted the fa/call_method branch April 13, 2023 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants