Skip to content

Commit

Permalink
DLTensor progress
Browse files Browse the repository at this point in the history
  • Loading branch information
wjakob committed Apr 5, 2022
1 parent a22ca65 commit 5d92f75
Show file tree
Hide file tree
Showing 21 changed files with 1,797 additions and 577 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:

- name: Install Python dependencies
run: |
python -m pip install pytest pytest-github-actions-annotate-failures
python -m pip install pytest pytest-github-actions-annotate-failures numpy
- name: Configure
run: >
Expand Down
91 changes: 29 additions & 62 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,10 @@ Removed features include:
- ● PyPy support is gone. (PyPy requires many workaround in _pybind11_ that
complicate the its internals. Making PyPy interoperate with _nanobind_ will
likely require changes to the PyPy CPython emulation layer.)
- ◑ Eigen and NumPy integration have been removed.
- ◑ NumPy integration was replaced by a more general ``nb::tensor<>``
integration that supports CPU/GPU tensors produced by various frameworks
(NumPy, PyTorch, TensorFlow, JAX, ..).
- ◑ Eigen integration was removed.
- ◑ Buffer protocol functionality was removed.
- ◑ Nested exceptions are not supported.
- ◑ Features to facilitate pickling and unpickling were removed.
Expand Down Expand Up @@ -240,6 +243,11 @@ improvements for developers:
when the binding code of those C++ types hasn't yet run. _nanobind_ does not
pre-render function docstrings, they are created on the fly when queried.

- _nanobind_ has [greatly
improved](https://github.com/wjakob/nanobind/blob/master/docs/tensor.md)
support for exchanging tensor data structures with modern array programming
frameworks.

### Dependencies

_nanobind_ depends on recent versions of everything:
Expand Down Expand Up @@ -467,6 +475,19 @@ changes are detailed below.

- **New features.**

- **Unified DLPack/Buffer protocol integration**: _nanobind_ can retrieve and
return tensors using two standard protocols:
[DLPack](https://github.com/dmlc/dlpack), and the the [buffer
protocol](https://docs.python.org/3/c-api/buffer.html). This enables
zero-copy data exchange of CPU and GPU tensors with array programming
frameworks including [NumPy](https://numpy.org),
[PyTorch](https://pytorch.org), [TensorFlow](https://www.tensorflow.org),
[JAX](https://jax.readthedocs.io), etc.

Details on using this feature can be found
[here](https://github.com/wjakob/nanobind/blob/master/docs/tensor.md).


- **Supplemental type data**: _nanobind_ can store supplemental data along
with registered types. This information is co-located with the Python type
object. An example use of this fairly advanced feature are libraries that
Expand All @@ -488,75 +509,21 @@ changes are detailed below.

- **Low-level interface**: _nanobind_ exposes a low-level interface to
provide fine-grained control over the sequence of steps that instantiates a
Python object wrapping a C++ instance. An example is shown below:

```cpp
/* Look up the Python type object associated with a C++ class named `MyClass`.
Requires a previous nb::class_<> binding declaration, otherwise this line
will return a NULL pointer (this can be checked via py_type.is_valid()). */
nb::handle py_type = nb::type<MyClass>();

// Type metadata can also be queried in the other direction
assert(py_type.is_valid() && // Did the type lookup work?
nb::type_check(py_type) &&
nb::type_size(py_type) == sizeof(MyClass) && // nanobind knows the size+alignment
nb::type_align(py_type) == alignof(MyClass) &&
nb::type_info(py_type) == typeid(MyClass)); // Query C++ RTTI record

/* Allocate an uninitialized Python instance of this type. Nanobind will
refuse to pass this (still unitialized) object to bound C++ functions */
nb::object py_inst = nb::inst_alloc(py_type);
assert(nb::inst_check(py_inst) && py_inst.type().is(py_type) && !nb::inst_ready(py_inst));

/* For POD types, the following line zero-initializes the object and marks
it as ready. Alternatively, the next lines show how to perform a fancy
object initialization using the C++ constructor */
// nb::inst_zero(py_inst);

// Get a C++ pointer to the uninitialized instance data
MyClass *ptr = nb::inst_ptr<MyClass>(py_inst);

// Perform an in-place construction of the C++ object
new (ptr) MyClass();

/* Mark the Python object as ready. When reference count reaches zero,
nanobind will automatically call the destructor (MyClass::~MyClass). */
nb::inst_mark_ready(py_inst);
assert(nb::inst_ready(py_inst));

/* Alternatively, we can force-call the destructor and transition the
instance back to non-ready status. The instance could then be reused
by initializing it yet again. */
nb::inst_destruct(py_inst);
assert(!nb::inst_ready(py_inst));

/* We can copy- or move-construct 'py_inst' from another instance of the
same type. This calls the C++ copy or move constructor and transitions
'py_inst' back to 'ready' status. Note that this is equivalent to calling
an in-place version of these constructors above but compiles to more
compact code (the 'nb::class_<MyClass>' declaration has already created
bindings for both constructors, and this simply calls those bindings). */
// nb::inst_copy(/* dst = */ py_inst, /* src = */ some_other_instance);
// nb::inst_move(/* dst = */ py_inst, /* src = */ some_other_instance);
```
Python object wrapping a C++ instance. Like the above point, this is useful
when writing generic binding code that manipulates _nanobind_-based objects
of various types.

Note that these functions are all _unsafe_ in the sense that they do not
verify that their input arguments are valid. This is done for performance
reasons, and such checks (if needed) are therefore the responsibility of
the caller. Functions labeled `nb::type_*` should only be called with
_nanobind_ type objects, and functions labeled `nb::inst_*` should only be
called with _nanobind_ instance objects. The functions `nb::type_check()`
and `nb::inst_check()` accept any Python object and test whether something
is a _nanobind_ type or instance object.
Details on using this feature can be found
[here](https://github.com/wjakob/nanobind/blob/master/docs/lowlevel.md).

- **Python type wrappers**: The `nb::handle_of<T>` type behaves just like the
`nb::handle` class and wraps a `PyObject *` pointer. However, when binding
a function that takes such an argument, _nanobind_ will only call the
associated function overload when the underlying Python object wraps a C++
`T` instance.
instance of type `T`.

- **Raw docstrings**: In cases where absolute control over docstrings is
required (for example, so that they can be parsed by a tool like
required (for example, so that complex cases can be parsed by a tool like
[Sphinx](https://www.sphinx-doc.org)), the ``nb::raw_doc`` attribute can be
specified to functions. In this case, _nanobind_ will _skip_ generation of
a combined docstring that enumerates overloads along with type information.
Expand Down
8 changes: 8 additions & 0 deletions cmake/nanobind-config.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,20 @@ function (nanobuild_build_library TARGET_NAME TARGET_TYPE)
${NB_DIR}/include/nanobind/nb_traits.h
${NB_DIR}/include/nanobind/nb_types.h
${NB_DIR}/include/nanobind/trampoline.h
${NB_DIR}/include/nanobind/tensor.h
${NB_DIR}/include/nanobind/operators.h
${NB_DIR}/include/nanobind/stl/shared_ptr.h
${NB_DIR}/include/nanobind/stl/unique_ptr.h
${NB_DIR}/include/nanobind/stl/string.h
${NB_DIR}/include/nanobind/stl/tuple.h
${NB_DIR}/include/nanobind/stl/pair.h
${NB_DIR}/include/nanobind/stl/function.h

${NB_DIR}/src/internals.h
${NB_DIR}/src/buffer.h
${NB_DIR}/src/internals.cpp
${NB_DIR}/src/common.cpp
${NB_DIR}/src/tensor.cpp
${NB_DIR}/src/nb_func.cpp
${NB_DIR}/src/nb_type.cpp
${NB_DIR}/src/nb_enum.cpp
Expand Down
67 changes: 67 additions & 0 deletions docs/lowlevel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# **Low-level instance interface**

_nanobind_ exposes a low-level interface to provide fine-grained control over
the sequence of steps that instantiates a Python object wrapping a C++
instance. Like the above point, this is useful when writing generic binding
code that manipulates _nanobind_-based objects of various types.

An example is shown below:

```cpp
/* Look up the Python type object associated with a C++ class named `MyClass`.
Requires a previous nb::class_<> binding declaration, otherwise this line
will return a NULL pointer (this can be checked via py_type.is_valid()). */
nb::handle py_type = nb::type<MyClass>();

// Type metadata can also be queried in the other direction
assert(py_type.is_valid() && // Did the type lookup work?
nb::type_check(py_type) &&
nb::type_size(py_type) == sizeof(MyClass) && // nanobind knows the size+alignment
nb::type_align(py_type) == alignof(MyClass) &&
nb::type_info(py_type) == typeid(MyClass)); // Query C++ RTTI record

/* Allocate an uninitialized Python instance of this type. Nanobind will
refuse to pass this (still unitialized) object to bound C++ functions */
nb::object py_inst = nb::inst_alloc(py_type);
assert(nb::inst_check(py_inst) && py_inst.type().is(py_type) && !nb::inst_ready(py_inst));

/* For POD types, the following line zero-initializes the object and marks
it as ready. Alternatively, the next lines show how to perform a fancy
object initialization using the C++ constructor */
// nb::inst_zero(py_inst);

// Get a C++ pointer to the uninitialized instance data
MyClass *ptr = nb::inst_ptr<MyClass>(py_inst);

// Perform an in-place construction of the C++ object
new (ptr) MyClass();

/* Mark the Python object as ready. When reference count reaches zero,
nanobind will automatically call the destructor (MyClass::~MyClass). */
nb::inst_mark_ready(py_inst);
assert(nb::inst_ready(py_inst));

/* Alternatively, we can force-call the destructor and transition the
instance back to non-ready status. The instance could then be reused
by initializing it yet again. */
nb::inst_destruct(py_inst);
assert(!nb::inst_ready(py_inst));

/* We can copy- or move-construct 'py_inst' from another instance of the
same type. This calls the C++ copy or move constructor and transitions
'py_inst' back to 'ready' status. Note that this is equivalent to calling
an in-place version of these constructors above but compiles to more
compact code (the 'nb::class_<MyClass>' declaration has already created
bindings for both constructors, and this simply calls those bindings). */
// nb::inst_copy(/* dst = */ py_inst, /* src = */ some_other_instance);
// nb::inst_move(/* dst = */ py_inst, /* src = */ some_other_instance);
```
Note that these functions are all _unsafe_ in the sense that they do not
verify that their input arguments are valid. This is done for performance
reasons, and such checks (if needed) are therefore the responsibility of
the caller. Functions labeled `nb::type_*` should only be called with
_nanobind_ type objects, and functions labeled `nb::inst_*` should only be
called with _nanobind_ instance objects. The functions `nb::type_check()`
and `nb::inst_check()` accept any Python object and test whether something
is a _nanobind_ type or instance object.
Loading

0 comments on commit 5d92f75

Please sign in to comment.