Skip to content

Migrate from pybind11 to nanobind - #1707

Open
sea-bass wants to merge 4 commits into
ros2:rollingfrom
sea-bass:nanobind-migration
Open

Migrate from pybind11 to nanobind#1707
sea-bass wants to merge 4 commits into
ros2:rollingfrom
sea-bass:nanobind-migration

Conversation

@sea-bass

@sea-bass sea-bass commented Aug 1, 2026

Copy link
Copy Markdown

Description

Following a thread on ROS Discourse, where I suggested upgrading from pybind11 to nanobind, I figured I'd give this a try.

Relevant context: https://nanobind.readthedocs.io/en/latest/why.html

Some additional changes besides pure porting:

  • Note that nanobind offers a CMake macro for Python type stub generation, so we don't have to manually update it anymore.
  • There were several places in the binding where it was specified that an argument could be None; however, nanobind provides for-free bindings for this if you just make it a std::optional on the C++ side.

Note that nanobind-dev as a dependency only transitioned from pip to apt on Ubuntu 24.04, but the 24.04 version is too old for stub generation. So I think on Lyrical onwards this PR is good to go. My recommendation would be not to backport this, though, and simply go forward-looking.

Is this user-facing behavior change?

It shouldn't be for the end user, unless there's a bug (or an improvement!) that is noticeable.

Did you use Generative AI?

Yes, Claude Fable 5.

I of course read through all the changes (except for the autogenerated .pyi stub files), identified areas of simplification/inefficiency, and iterated.

@sea-bass
sea-bass force-pushed the nanobind-migration branch from 5ae45f6 to 3c25f73 Compare August 1, 2026 18:37
@sea-bass
sea-bass marked this pull request as ready for review August 1, 2026 18:49
@mergify

mergify Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@sea-bass
sea-bass force-pushed the nanobind-migration branch from 884952a to 21f8a4d Compare August 1, 2026 18:50
@mjcarroll

Copy link
Copy Markdown
Member

Interesting stuff. Did you see any performance improvements out of the gate with this?

@sea-bass

sea-bass commented Aug 6, 2026

Copy link
Copy Markdown
Author

Interesting stuff. Did you see any performance improvements out of the gate with this?

Yes! I kicked off a pretty rough vibe-benchmark while working on the PR and the general takeaway was twofold:

  1. Anything that relies on the C API under the hood doing the heavy lifting is pretty much the same.
  2. The more bindings-heavy calls like getting the current time saw up to 3-4x speedup due to nanobind usage.

Other less performance related improvements to reiterate are that binary size is smaller and .pyi stub generation is automatic.

@sloretz

sloretz commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

I think we would absolutely consider merging this if the benefits are significant.

  • How much faster at runtime is it?
  • How much smaller are the binaries?

Note that nanobind-dev as a dependency only transitioned from pip to apt on Ubuntu 24.04, but the 24.04 version is too old for stub generation

For a change this big, I would recommend against backporting, but it could go into Rolling and beyond. Lyrical and Rolling currently share target platforms. Looking at the Lyrical target platforms, do these platforms all have a new enough nanobind version?

  • Ubuntu Resolute (26.04) (Seems like yes)
  • Windows 11 (via pixi / conda-forge)
  • macOS (via pixi / conda-forge)
  • Debian Trixie
  • OpenEmbedded

.pyi stub generation is automatic.

If stub generation is automatic, do we need rclpy/rclpy/impl/_rclpy_nanobind.pyi, or can we generate it?

@sloretz sloretz removed their assignment Aug 6, 2026
@sea-bass

sea-bass commented Aug 6, 2026

Copy link
Copy Markdown
Author

When I find some more time I will post some numbers on perf and binary size. In the meantime...

Looking at the Lyrical target platforms, do these platforms all have a new enough nanobind version?

  • Ubuntu Resolute (26.04) (Seems like yes)
  • Windows 11 (via pixi / conda-forge)
  • macOS (via pixi / conda-forge)
  • Debian Trixie
  • OpenEmbedded

Yes on the first 4 -- I believe nanobind 2.0 is the cutoff, so I see 2.5.0 and newer on all the others.

How do I check OpenEmbedded? My cursory search only had python3-nanobind show up but not nanobind-dev, which is the one we need. But I also don't really know where to look, to be honest.

EDIT: Looking at rosdistro we may just need to copy how pybind11-dev did it, by adding this to the nanobind-dev key, and we should be good.

openembedded: [python3-nanobind@meta-python]

However, there are still a bunch of other platforms like rhel, etc. that don't yet have a nanobind-dev key so we probably want to add those to rosdistro pretty extensively so we don't take down rclpy support for existing platforms...

If stub generation is automatic, do we need rclpy/rclpy/impl/_rclpy_nanobind.pyi, or can we generate it?

It is generated! But it's also versioned with the code. This way people get IDE support more easily.

Additionally, it lends itself to testing. For example, in my repo that uses nanobind we have a CI check that builds the code and runs the tests (as normal) and then does a git diff to check whether the user updated the stubs by building the source code on their machine.

@sea-bass

sea-bass commented Aug 6, 2026

Copy link
Copy Markdown
Author

Update, I left Claude running a benchmark in the background and it produced a nice report: https://claude.ai/code/artifact/f5514863-a0c8-42fe-81a7-e7bb6442eb04

These are the "headline numbers" I got in my CLI:

  ┌───────────────────────────────────┬──────────────────────────┬────────────────────┬──────────────┐
  │              Metric               │    pybind11 (de802f7)    │ nanobind (21f8a4d) │    Change    │
  ├───────────────────────────────────┼──────────────────────────┼────────────────────┼──────────────┤
  │ Extension .so                     │                1,484 KiB │            738 KiB │       −50.2% │
  ├───────────────────────────────────┼──────────────────────────┼────────────────────┼──────────────┤
  │ .text (generated code)            │                1,071 KiB │            412 KiB │       −61.5% │
  ├───────────────────────────────────┼──────────────────────────┼────────────────────┼──────────────┤
  │ Geomean over 79 method benchmarks │                        — │                  — │ 1.92× faster │
  ├───────────────────────────────────┼──────────────────────────┼────────────────────┼──────────────┤
  │ Executor spin_once (ready timer)  │                  48.8 µs │            33.9 µs │        1.44× │
  ├───────────────────────────────────┼──────────────────────────┼────────────────────┼──────────────┤
  │ Service round-trip (loopback)     │                 121.3 µs │            88.8 µs │        1.37× │
  ├───────────────────────────────────┼──────────────────────────┼────────────────────┼──────────────┤
  │ import rclpy / cold start / RSS   │ ~55 ms / 125 ms / 66 MiB │               same │       ~1.00× │
  └───────────────────────────────────┴──────────────────────────┴────────────────────┴──────────────┘

quite encouraging TBH!

Signed-off-by: Sebastian Castro <sebas.a.castro@gmail.com>
Signed-off-by: Sebastian Castro <sebas.a.castro@gmail.com>
Signed-off-by: Sebastian Castro <sebas.a.castro@gmail.com>
@sea-bass
sea-bass force-pushed the nanobind-migration branch from 21f8a4d to 6ec8dcc Compare August 7, 2026 13:02
@skyegalaxy

Copy link
Copy Markdown
Member

@skyegalaxy to put on the client library WG agenda

@skyegalaxy skyegalaxy self-assigned this Aug 13, 2026
@jmachowinski

Copy link
Copy Markdown
Contributor

We discussed this and we are all in favor of moving forward with this.
One thing that came up though was that we should merge this together with also switching the rosidl_py bindings to nanobind.
@sea-bass Would you be interested to look into this as well ?

@sea-bass

Copy link
Copy Markdown
Author

Awesome! I could take a look. It is this repo?

https://github.com/ros2/rosidl/tree/rolling/rosidl_buffer_py

@jmachowinski

Copy link
Copy Markdown
Contributor

https://github.com/ros2/rosidl_python/tree/rolling/rosidl_generator_py

That is the generator. It generates the bindings.

ros2/rosidl_python#193
This PR might collide with this work. It has been stale for years but now we got movement again...

I also have some experimental code around that speed up the existing implementation by ~3x if using pybind that is not that intrusive...

@jmachowinski

Copy link
Copy Markdown
Contributor

ros2/rosidl_python#265

I guess the same optimizations might work for nanobind as well.

@sea-bass

Copy link
Copy Markdown
Author

Cool! Would it make sense to land those 2 PRs to rosidl_python first, and then we can start a nanobind migration there?

I guess we probably don't want to merge this until both repos are in workable states w nanobind?

@sea-bass

sea-bass commented Aug 21, 2026

Copy link
Copy Markdown
Author

Sorry, I just glanced at those repos and I don't think I understand.

Is there anything there that currently uses pybind11 which would need porting to nanobind? I don't see any binding code at all in the repo or the linked PR. Just CPython stuff which is a level of abstraction down.

Is the idea to change the CPython usage there to nanobind instead?

@jmachowinski

Copy link
Copy Markdown
Contributor

Pulls: #1707
Gist: https://gist.githubusercontent.com/jmachowinski/9d0c4e3cc66fb17a604ca57948ab8d39/raw/cb405781297f6b2de4383911b0c56b059129d710/ros2.repos
BUILD args:
TEST args:
ROS Distro: rolling
Job: ci_launcher
ci_launcher ran: https://ci.ros2.org/job/ci_launcher/20161

  • Linux Build Status
  • Linux-aarch64 Build Status
  • Linux-rhel Build Status
  • Windows Build Status

@jmachowinski

Copy link
Copy Markdown
Contributor

You are right. I was under the assumption that the 'other' python bindings would use the same framework, but this obviously is not the case.
I think then we are good to go here. I started the CI to see if everything works as expected.

@jmachowinski

Copy link
Copy Markdown
Contributor

The CI does not find nanobind, are we missing nanobind-dev as a key for rosdep ?

@sea-bass

sea-bass commented Aug 22, 2026

Copy link
Copy Markdown
Author

I added nanobind-dev to the package.xml, is CI calling rosdep install / is there a cached set of dependencies being used? Reading the jenkins logs it's still installing pybind11 via apt?

Here's the rosdistro entries: https://github.com/ros/rosdistro/blob/82eefa281fd5702f1869408c66606d395cd635ee/rosdep/base.yaml#L8554

Also re: finding it as a key there may be some missing OSes in our non tier 1 platforms. See: #1707 (comment)

@jmachowinski

Copy link
Copy Markdown
Contributor

@mjcarroll @claraberendsen any idea what's going on with the nanobind dependency ?

@InvincibleRMC

InvincibleRMC commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

These new autogenerated stubs have some regressions around generics. I will check out if nanobind has a nice way of handling it.

@sea-bass

sea-bass commented Aug 26, 2026

Copy link
Copy Markdown
Author

These new autogenerated stubs have some regressions around generics. I will check out if nanobind has a nice way of handling it.

@InvincibleRMC maybe we can use nb::sig() / nb::typed() as shown here?

https://nanobind.readthedocs.io/en/latest/api_core.html#_CPPv4N8nanobind3sigE
https://nanobind.readthedocs.io/en/latest/api_core.html#_CPPv4I0DpEN8nanobind5typedE

Combined with this if needed: https://nanobind.readthedocs.io/en/latest/typing.html#pattern-files

@InvincibleRMC

Copy link
Copy Markdown
Contributor

@InvincibleRMC maybe we can use nb::sig() / nb::typed() as shown here?

Yeah nb::sig will work, but it means a large amount of the .pyi file is going to be hand maintained (just in cpp instead of .pyi)

Signed-off-by: Sebastian Castro <sebas.a.castro@gmail.com>
@sea-bass

sea-bass commented Aug 26, 2026

Copy link
Copy Markdown
Author

A large amount is better than the whole thing, I guess?

I've just pushed up a commit that proves this out, and it's... not bad, and also not that substantial a portion of the CPP binding code. There are some gaps still, compared to the manual stub curation, but I really would like to hold on to the hope that this can be somewhat automated.

Let me know your thoughts?

@sea-bass
sea-bass requested a review from jmachowinski August 26, 2026 02:59
claraberendsen added a commit to ros2/ros2 that referenced this pull request Aug 26, 2026
claraberendsen added a commit to ros2/ros2 that referenced this pull request Aug 26, 2026
@claraberendsen

Copy link
Copy Markdown
Contributor

@sea-bass @jmachowinski CI uses a custom set of dependencies defined on ros2/ci and pixi.toml for Windows. It doesn't use rosdep so there is some manual steps to test this:
Here are the changes needed:

Two things to note: on conda-forge there isn't available the exact version as on Resolute so I bumped it to the next one (feel free to modify/point me to what version we want). Is this package available for RHEL? Otherwise CI for it will fail.

And a fresh run/rebuild of ci with those branches as base:

  • ci_linux: Build Status
  • ci_linux_aarch: Build Status
  • ci_windows: Build Status

@claraberendsen

Copy link
Copy Markdown
Contributor

Rebuild with side-by-side installation of pybind and nanobind. Windows hasn't triggered yet so the previous build should pick up the changes already.

  • ci_linux: Build Status
  • ci_linux_aarch: Build Status

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.

7 participants