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

register c++ half-precision floating point as numpy.float16 #1776

Closed
ericxsun opened this issue Apr 28, 2019 · 14 comments
Closed

register c++ half-precision floating point as numpy.float16 #1776

ericxsun opened this issue Apr 28, 2019 · 14 comments

Comments

@ericxsun
Copy link

ericxsun commented Apr 28, 2019

Issue description

how could I pass numpy.float16 into c++ (in c++, I used the half.hpp as the float16 type)? ( what is the right way to bind numpy.float16 and half float defined in half.hpp)

Reproducible example code

depend on half.hpp

in test_half_pybind.cc:

#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
#include <pybind11/stl.h>

// half_float::half behaviors like float, but with different precision
#include <half.hpp> 

namespace py = pybind11;

void test_print_half(half_float::half *points, int sz) {
  for (int i = 0; i < sz; ++i) {
    py::print(points[i]);
  }
}

PYBIND11_MODULE(test_half_pybind, m) {
  m.def(
    "test_print_half"
    [](
      std::vector<??numpy::float16??> arr,
      int sz
    ) {
      test_print_half((half_float::half *)arr.data(), sz);
    },
    py::arg("arr"),
    py::arg("sz")
  );
}

in python side

>>> from test_half_pybind import *
>>> import numpy as np
>>> a = np.array([1.1, 2.1, 5.012], dtype='float16')
>>> a
array([1.1 , 2.1 , 5.01], dtype=float16)

>>> test_print_half(a, a.size)

any help or reference will be highly appreciated.

@eacousineau
Copy link
Contributor

I'm not sure what your above code would do if it were just focusing on float or double.
I would first recommend you make a working example using an already bound type, say float; you can use the NumPy buffer protocol:
https://pybind11.readthedocs.io/en/stable/advanced/pycpp/numpy.html
Or for simplicity, use Eigen:
https://pybind11.readthedocs.io/en/stable/advanced/cast/eigen.html

From looking at the question marks in your repro, I think what you need is to tell your C++ translation unit about half by specializing npy_format_descriptor<>.

Example specialization for char[N]:

template <size_t N> struct npy_format_descriptor<char[N]> { PYBIND11_DECL_CHAR_FMT };

It's not succinctly documented, but I think you just need to define name and dtype() fields.

@a-sevin
Copy link

a-sevin commented May 2, 2019

I also work with fp16 in my C++ libraries but I have to wrote a lot of lambda functions (for getter/setter from/to fp32 conversion for example)...
It would be very appreciated that pybind11 can directly use fp16 without any conversion.

@eacousineau
Copy link
Contributor

Can you post a link to your code that requires conversions to/from fp32?

And have you tried implementing the npy_format_descriptor that I mentioned?
Writing this now, though, I remember that you will need to also write a type_caster itself that will return the type you want - easiest way may be to leverage NumPy array casting, but for a scalar (if possible).

@a-sevin
Copy link

a-sevin commented May 5, 2019

I can post a code if it's really needed but it can be confusing due to the architecture... But the idea is MyClass has a function void MyClass<half>::set_arg(const std::vector<half> &new_arg) and, basically, what I do is :

  myClassHalf.def(
    "set_arg"
    [](MyClass<half> &myclass, std::vector<float> &arr ) {
      std::vector<half> new_arg(arr.size());
      std::copy(arr.begin(), arr.end(), new_arg.begin());
      myclass.set_arg(new_arg);
    },
    py::arg("arr")
  );

It's not elegant but it works. I didn't try the npy_format_descriptor implementation because it think I'll spend to much time to understand the mechanism and I'm already late in my project...

@eacousineau
Copy link
Contributor

Aye, yeah, your manual inline type caster will do the trick for ya if you're on a tight deadline.

Another alternative, that's somewhere between your inline workaround and the npy_format_descriptor, is to write your own type_caster<half>:
https://pybind11.readthedocs.io/en/stable/advanced/cast/custom.html

While the examples there use the direct Python C API, you could instead use pybind11s C++ / Python interface to simplify things.

Here's a quick 3-min hack that might address your usage (though I haven't tested it):

namespace pybind11 { namespace detail {

template <> struct type_caster<half> {
public:
    PYBIND11_TYPE_CASTER(half, _("half"));
    using float_caster = type_caster<float>;

    bool load(handle src, bool convert) {
        float_caster caster;
        if (caster.load(src, convert)) {
            this->value = half(float(caster));  // Implicit cast defined by `type_caster`.
            return true;

        }
        return false;
    }
    static handle cast(half src, return_value_policy policy, handle parent) {
        return float_caster::cast(float(src), policy, parent);
    }
};

}} // namespace pybind11::detail

Then you can just directly bind any APIs that use half, e.g.:

myClassHalf.def("set_arg", &MyClass<half>::set_arg, py::arg("arr"));

@eacousineau
Copy link
Contributor

Actually, got curious / overly invested, made a solution that should work for you in #1785:
https://github.com/eacousineau/repro/blob/5e6acf6/python/pybind11/custom_tests/test_numpy_issue1785.cc#L49-L52

@a-sevin
Copy link

a-sevin commented May 11, 2019

Thanks for your help, it's really appreciated. I will try it in the next days

@eacousineau
Copy link
Contributor

Welcome!

And, er, brain fart: The solution related to #1785 would only work if you successfully register half as a NumPy dtype.
The other workaround above, though, should get ya what you need if you want to skip the NumPy shenanigans.

@ericxsun
Copy link
Author

ericxsun commented May 13, 2019

@eacousineau Thanks a lot.

However with type_caster, it casts half to float and versa. In python side, when using np.int, np.float16, np.float32, or even np.float64, it acts the same.

#include <pybind11/pybind11.h>
#include <half.h>

using half = half_float::half;

namespace pybind11 { namespace detail {
template <> struct type_caster<half> {
public:
    PYBIND11_TYPE_CASTER(half, _("half"));
    using float_caster = type_caster<float>;

    bool load(handle src, bool convert) {
        float_caster caster;
        if (caster.load(src, convert)) {
            this->value = half(float(caster));  // Implicit cast defined by `type_caster`.
            return true;

        }
        return false;
    }
    static handle cast(half src, return_value_policy policy, handle parent) {
        return float_caster::cast(float(src), policy, parent);
    }
};

}} // namespace pybind11::detail


PYBIND11_MODULE(bind_half_np_pybind, m) {
  m.def("overload", [](float16 x) { return "float16"; });
}

py-side

>>> from bind_half_np_pybind import overload
>>> overload(np.float16(1.0))
u'float16'
>>> overload(np.int(1))
u'float16'
>>> overload(np.float64(1.0))
u'float16'

That's not the exact same thing what we want.

register half as a NumPy dtype, as you mentioned, is the real thing we need, but I've not figure out how to do this. Appreciated lots.

Another working on your test code. Same error NumPy type info missing for N10half_float4halfE as what did before.

@eacousineau
Copy link
Contributor

eacousineau commented May 14, 2019

However with type_caster, it casts half to float and versa [...]

That's the whole point of it being a short-term workaround ;) It's not terribly efficient.

That's not the exact same thing what we want.

Er, I have no idea what you actually want... Can you post what you expect the output to be? You've only provided one C++ overload, so you'll only get one type. Do you want it to overload for multiple types? (If so, why?)

register half as a NumPy dtype, as you mentioned, is the real thing we need, but I've not figure out how to do this. Appreciated lots. [...] Same error [...]

You've convinced me to do it for ya :P (just because the NumPy stuff is a bit esoteric haha).

Here's the working code:
Code + Output
And here's what I meant by specializing npy_descriptor_format:
Snippet

Related header for npy_scalar_caster: https://github.com/eacousineau/repro/blob/43407e3/python/pybind11/custom_tests/pybind11_numpy_scalar.h

@ericxsun
Copy link
Author

ericxsun commented May 15, 2019

@eacousineau Thanks so much. you save my life.

Another question with refactoring ur code Code.

m.def("make_array", []() {
    py::array_t<float16> x{2};
    x.mutable_at(0) = float16{1.};
    x.mutable_at(1) = float16{10.};
    return x;
  });
  m.def("make_scalar", []() { return float16{2.}; });
  m.def("return_array", [](py::array_t<float16> x, int sz) {
    py::buffer_info buf = x.request();
    float16 *ptr = (float16 *)buf.ptr;

    for (int i = 0; i < sz; ++i) {
      ptr[i] += (float16)(i);
    }
  });

  m.def("return_array", [](py::array_t<float> x, int sz) {
    py::buffer_info buf = x.request();
    float *ptr = (float *)buf.ptr;

    for (int i = 0; i < sz; ++i) {
      ptr[i] *= (float)(i);
    }
  });

in py-side

>>> import numpy as np
>>> a = np.array([1.0, 2.0, 3.0], dtype=np.float16)
>>> b = np.array([1.0, 2.0, 3.0], dtype=np.float32)
>>> c = np.array([1, 2, 3], dtype=np.int)
>>> d = np.array(['x', 'b'])
>>>
>>> return_array(a, a.size)
array([1., 3., 5.], dtype=float16)  # the right

>>> return_array(b, b.size)
array([0., 2., 6.], dtype=float32)  # the right

>>> return_array(c, c.size)
array([1., 2., 3.], dtype=int)  # should it raise an TypeError like the following?

>>> return_array(d, d.size)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    return_array(d, d.size)
TypeError: return_array(): incompatible function arguments. Th
e following argument types are supported:
    1. (arg0: numpy.ndarray[float16], arg1: int) -> None
    2. (arg0: numpy.ndarray[float32], arg1: int) -> None
Invoked with: array(['x', 'b'], dtype='|S1'), 2

When calling func without definitions for a given type of the parameters, it should raise an exception. However, as showing in the pyside code, calling with dtype=int,no exceptions raised

@ericxsun ericxsun changed the title bind numpy.float16 to c++ half_float register c++ half-precision floating point as numpy.float16 May 15, 2019
@ericxsun ericxsun changed the title register c++ half-precision floating point as numpy.float16 register half as a NumPy dtype (float16) May 15, 2019
@ericxsun ericxsun changed the title register half as a NumPy dtype (float16) register c++ half-precision floating point as numpy.float16 May 15, 2019
@eacousineau
Copy link
Contributor

Meta: If you have an issue, can you be more explicit about what the problem is, rather than just posting code + errors with no text? (Sorry, I'm picky about these things!)

Also, this looks like an entirely different question; if I covered your main question about float16, would you be able to ask your new question on Gitter or StackOverflow?
Looking at the current docs, it looks like they don't describe much what char[] looks like directly, so opening an issue about the docs lacking would do the trick ;)
https://pybind11.readthedocs.io/en/stable/advanced/pycpp/numpy.html

And can you either reformulate this issue to have an action, like "Update docs to show example of [...]", or mebbe close it if it's resolved?

@a-sevin
Copy link

a-sevin commented May 15, 2019

I get it work ! Thanks a lot but I have to make some changes on npy_format_descriptor:

  • declare name as a function
  • declare format to return a py::buffer_info with the .def_buffer method
// Kinda following: https://github.com/pybind/pybind11/blob/9bb3313162c0b856125e481ceece9d8faa567716/include/pybind11/numpy.h#L1000
template <>
struct npy_format_descriptor<float16> {
  static pybind11::dtype dtype() {
    handle ptr = npy_api::get().PyArray_DescrFromType_(NPY_FLOAT16);
    return reinterpret_borrow<pybind11::dtype>(ptr);
  }
  static std::string format() {
    // following: https://docs.python.org/3/library/struct.html#format-characters
    return "e";
  }
  static constexpr auto name() {
    return _("float16");
  }
};

@ericxsun
Copy link
Author

Thanks very much mainly solved my problem

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

No branches or pull requests

3 participants