Switch bindings implementation to `nanobind` (#1526)

* End support for Python 3.7, update cibuildwheel and publish actions

Removes Python 3.7 from the support matrix, since it does not support
PEP590 vectorcalls.

Bumps the `cibuildwheel` and `pypa-publish` actions to their latest
available versions respectively.

* Add nanobind to the Bazel dependencies, add a BUILD file

The build file builds nanobind as a static `cc_library`. Currently,
the git SHA points to HEAD, since some necessary features have not
been included in a release yet.

* Delete pybind11 BUILD file

* Switch bindings implementation to nanobind

Switches over the binding tool to `nanobind` from `pybind11`. Most
changes in the build setup itself were drop-in replacements of existing
code changed to nanobind names, no new concepts needed to be
implemented.

Sets the minimum required macOS to 10.14 for full C++17 support. Also,
to avoid ambiguities in Bazel, build for macOS 11 on Mac ARM64.

* Use Bazel select for linker options

Guards against unknown linker option errors by selecting required
linker options for nanobind only on macOS, where they are relevant.

Other changes:
* Bump cibuildwheel action to v2.12.0
* Bump Bazel for aarch64 linux wheels to 6.0.0
* Remove C++17 flag from build files since it is present in setup.py `bazel build` command
* Bump nanobind commit to current HEAD (TBD: Bump to next stable release)

* Unbreak Windows builds of nanobind-based bindings

Guards compiler options behind a new `select` macro choosing between
MSVC and not MSVC.

Other changes:
* Inject the proper C++17 standard cxxopt in the `setup.py` build
command.
* Bump nanobind to current HEAD.
* Make `macos` a benchmark-wide condition, with public visibility to
allow its use in the nanobind BUILD file.

* Fall back to `nb::implicitly_convertible` for Counter construction

Since `benchmark::Counter` only has a constructor for `double`,
the nanobind `nb::init_implicit` template cannot be used. Therefore,
to support implicit construction from ints, we fall back to the
`nb::implicitly_convertible` template instead.
This commit is contained in:
Nicholas Junge 2023-02-06 14:07:17 +01:00 committed by GitHub
parent f59d021ebc
commit 80a3c5e4d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 154 additions and 106 deletions

View File

@ -5,7 +5,7 @@ if ! bazel version; then
fi
echo "Installing wget and downloading $arch Bazel binary from GitHub releases."
yum install -y wget
wget "https://github.com/bazelbuild/bazel/releases/download/5.2.0/bazel-5.2.0-linux-$arch" -O /usr/local/bin/bazel
wget "https://github.com/bazelbuild/bazel/releases/download/6.0.0/bazel-6.0.0-linux-$arch" -O /usr/local/bin/bazel
chmod +x /usr/local/bin/bazel
else
# bazel is installed for the correct architecture

View File

@ -11,6 +11,7 @@ jobs:
name: Test GBM Python bindings on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]

View File

@ -14,10 +14,10 @@ jobs:
- name: Check out repo
uses: actions/checkout@v3
- name: Install Python 3.9
uses: actions/setup-python@v3
- name: Install Python 3.11
uses: actions/setup-python@v4
with:
python-version: 3.9
python-version: 3.11
- name: Build and check sdist
run: |
@ -46,11 +46,11 @@ jobs:
platforms: all
- name: Build wheels on ${{ matrix.os }} using cibuildwheel
uses: pypa/cibuildwheel@v2.9.0
uses: pypa/cibuildwheel@v2.12.0
env:
CIBW_BUILD: 'cp37-* cp38-* cp39-* cp310-* cp311-*'
CIBW_SKIP: "cp37-*-arm64 *-musllinux_*"
# TODO: Build ppc64le using some other trick
CIBW_BUILD: 'cp38-* cp39-* cp310-* cp311-*'
CIBW_SKIP: "*-musllinux_*"
CIBW_TEST_SKIP: "*-macosx_arm64"
CIBW_ARCHS_LINUX: x86_64 aarch64
CIBW_ARCHS_MACOS: x86_64 arm64
CIBW_ARCHS_WINDOWS: AMD64
@ -73,7 +73,7 @@ jobs:
name: dist
path: dist
- uses: pypa/gh-action-pypi-publish@v1.5.0
- uses: pypa/gh-action-pypi-publish@v1.6.4
with:
user: __token__
password: ${{ secrets.PYPI_PASSWORD }}

View File

@ -18,6 +18,12 @@ config_setting(
visibility = [":__subpackages__"],
)
config_setting(
name = "macos",
constraint_values = ["@platforms//os:macos"],
visibility = ["//visibility:public"],
)
config_setting(
name = "perfcounters",
define_values = {

View File

@ -44,13 +44,13 @@ def benchmark_deps():
tag = "release-1.11.0",
)
if "pybind11" not in native.existing_rules():
http_archive(
name = "pybind11",
build_file = "@//bindings/python:pybind11.BUILD",
sha256 = "eacf582fa8f696227988d08cfc46121770823839fe9e301a20fbce67e7cd70ec",
strip_prefix = "pybind11-2.10.0",
urls = ["https://github.com/pybind/pybind11/archive/v2.10.0.tar.gz"],
if "nanobind" not in native.existing_rules():
git_repository(
name = "nanobind",
remote = "https://github.com/wjakob/nanobind.git",
commit = "fe3ecb800a7a3e8023e8ee77167a6241591e0b8b",
build_file = "@//bindings/python:nanobind.BUILD",
recursive_init_submodules = True,
)
if "libpfm" not in native.existing_rules():

View File

@ -6,7 +6,6 @@ py_library(
visibility = ["//visibility:public"],
deps = [
":_benchmark",
# pip; absl:app
],
)
@ -17,10 +16,13 @@ py_extension(
"-fexceptions",
"-fno-strict-aliasing",
],
features = ["-use_header_modules"],
features = [
"-use_header_modules",
"-parse_headers",
],
deps = [
"//:benchmark",
"@pybind11",
"@nanobind",
"@python_headers",
],
)

View File

@ -2,19 +2,16 @@
#include "benchmark/benchmark.h"
#include <map>
#include <string>
#include <vector>
#include "nanobind/nanobind.h"
#include "nanobind/operators.h"
#include "nanobind/stl/bind_map.h"
#include "nanobind/stl/string.h"
#include "nanobind/stl/vector.h"
#include "pybind11/operators.h"
#include "pybind11/pybind11.h"
#include "pybind11/stl.h"
#include "pybind11/stl_bind.h"
PYBIND11_MAKE_OPAQUE(benchmark::UserCounters);
NB_MAKE_OPAQUE(benchmark::UserCounters);
namespace {
namespace py = ::pybind11;
namespace nb = nanobind;
std::vector<std::string> Initialize(const std::vector<std::string>& argv) {
// The `argv` pointers here become invalid when this function returns, but
@ -38,14 +35,15 @@ std::vector<std::string> Initialize(const std::vector<std::string>& argv) {
}
benchmark::internal::Benchmark* RegisterBenchmark(const char* name,
py::function f) {
nb::callable f) {
return benchmark::RegisterBenchmark(
name, [f](benchmark::State& state) { f(&state); });
}
PYBIND11_MODULE(_benchmark, m) {
NB_MODULE(_benchmark, m) {
using benchmark::TimeUnit;
py::enum_<TimeUnit>(m, "TimeUnit")
nb::enum_<TimeUnit>(m, "TimeUnit")
.value("kNanosecond", TimeUnit::kNanosecond)
.value("kMicrosecond", TimeUnit::kMicrosecond)
.value("kMillisecond", TimeUnit::kMillisecond)
@ -53,74 +51,74 @@ PYBIND11_MODULE(_benchmark, m) {
.export_values();
using benchmark::BigO;
py::enum_<BigO>(m, "BigO")
nb::enum_<BigO>(m, "BigO")
.value("oNone", BigO::oNone)
.value("o1", BigO::o1)
.value("oN", BigO::oN)
.value("oNSquared", BigO::oNSquared)
.value("oNCubed", BigO::oNCubed)
.value("oLogN", BigO::oLogN)
.value("oNLogN", BigO::oLogN)
.value("oNLogN", BigO::oNLogN)
.value("oAuto", BigO::oAuto)
.value("oLambda", BigO::oLambda)
.export_values();
using benchmark::internal::Benchmark;
py::class_<Benchmark>(m, "Benchmark")
// For methods returning a pointer tor the current object, reference
// return policy is used to ask pybind not to take ownership oof the
nb::class_<Benchmark>(m, "Benchmark")
// For methods returning a pointer to the current object, reference
// return policy is used to ask nanobind not to take ownership of the
// returned object and avoid calling delete on it.
// https://pybind11.readthedocs.io/en/stable/advanced/functions.html#return-value-policies
//
// For methods taking a const std::vector<...>&, a copy is created
// because a it is bound to a Python list.
// https://pybind11.readthedocs.io/en/stable/advanced/cast/stl.html
.def("unit", &Benchmark::Unit, py::return_value_policy::reference)
.def("arg", &Benchmark::Arg, py::return_value_policy::reference)
.def("args", &Benchmark::Args, py::return_value_policy::reference)
.def("range", &Benchmark::Range, py::return_value_policy::reference,
py::arg("start"), py::arg("limit"))
.def("unit", &Benchmark::Unit, nb::rv_policy::reference)
.def("arg", &Benchmark::Arg, nb::rv_policy::reference)
.def("args", &Benchmark::Args, nb::rv_policy::reference)
.def("range", &Benchmark::Range, nb::rv_policy::reference,
nb::arg("start"), nb::arg("limit"))
.def("dense_range", &Benchmark::DenseRange,
py::return_value_policy::reference, py::arg("start"),
py::arg("limit"), py::arg("step") = 1)
.def("ranges", &Benchmark::Ranges, py::return_value_policy::reference)
nb::rv_policy::reference, nb::arg("start"),
nb::arg("limit"), nb::arg("step") = 1)
.def("ranges", &Benchmark::Ranges, nb::rv_policy::reference)
.def("args_product", &Benchmark::ArgsProduct,
py::return_value_policy::reference)
.def("arg_name", &Benchmark::ArgName, py::return_value_policy::reference)
nb::rv_policy::reference)
.def("arg_name", &Benchmark::ArgName, nb::rv_policy::reference)
.def("arg_names", &Benchmark::ArgNames,
py::return_value_policy::reference)
nb::rv_policy::reference)
.def("range_pair", &Benchmark::RangePair,
py::return_value_policy::reference, py::arg("lo1"), py::arg("hi1"),
py::arg("lo2"), py::arg("hi2"))
nb::rv_policy::reference, nb::arg("lo1"), nb::arg("hi1"),
nb::arg("lo2"), nb::arg("hi2"))
.def("range_multiplier", &Benchmark::RangeMultiplier,
py::return_value_policy::reference)
.def("min_time", &Benchmark::MinTime, py::return_value_policy::reference)
nb::rv_policy::reference)
.def("min_time", &Benchmark::MinTime, nb::rv_policy::reference)
.def("min_warmup_time", &Benchmark::MinWarmUpTime,
py::return_value_policy::reference)
nb::rv_policy::reference)
.def("iterations", &Benchmark::Iterations,
py::return_value_policy::reference)
nb::rv_policy::reference)
.def("repetitions", &Benchmark::Repetitions,
py::return_value_policy::reference)
nb::rv_policy::reference)
.def("report_aggregates_only", &Benchmark::ReportAggregatesOnly,
py::return_value_policy::reference, py::arg("value") = true)
nb::rv_policy::reference, nb::arg("value") = true)
.def("display_aggregates_only", &Benchmark::DisplayAggregatesOnly,
py::return_value_policy::reference, py::arg("value") = true)
nb::rv_policy::reference, nb::arg("value") = true)
.def("measure_process_cpu_time", &Benchmark::MeasureProcessCPUTime,
py::return_value_policy::reference)
nb::rv_policy::reference)
.def("use_real_time", &Benchmark::UseRealTime,
py::return_value_policy::reference)
nb::rv_policy::reference)
.def("use_manual_time", &Benchmark::UseManualTime,
py::return_value_policy::reference)
nb::rv_policy::reference)
.def(
"complexity",
(Benchmark * (Benchmark::*)(benchmark::BigO)) & Benchmark::Complexity,
py::return_value_policy::reference,
py::arg("complexity") = benchmark::oAuto);
nb::rv_policy::reference,
nb::arg("complexity") = benchmark::oAuto);
using benchmark::Counter;
py::class_<Counter> py_counter(m, "Counter");
nb::class_<Counter> py_counter(m, "Counter");
py::enum_<Counter::Flags>(py_counter, "Flags")
nb::enum_<Counter::Flags>(py_counter, "Flags")
.value("kDefaults", Counter::Flags::kDefaults)
.value("kIsRate", Counter::Flags::kIsRate)
.value("kAvgThreads", Counter::Flags::kAvgThreads)
@ -132,28 +130,29 @@ PYBIND11_MODULE(_benchmark, m) {
.value("kAvgIterationsRate", Counter::Flags::kAvgIterationsRate)
.value("kInvert", Counter::Flags::kInvert)
.export_values()
.def(py::self | py::self);
.def(nb::self | nb::self);
py::enum_<Counter::OneK>(py_counter, "OneK")
nb::enum_<Counter::OneK>(py_counter, "OneK")
.value("kIs1000", Counter::OneK::kIs1000)
.value("kIs1024", Counter::OneK::kIs1024)
.export_values();
py_counter
.def(py::init<double, Counter::Flags, Counter::OneK>(),
py::arg("value") = 0., py::arg("flags") = Counter::kDefaults,
py::arg("k") = Counter::kIs1000)
.def(py::init([](double value) { return Counter(value); }))
.def(nb::init<double, Counter::Flags, Counter::OneK>(),
nb::arg("value") = 0., nb::arg("flags") = Counter::kDefaults,
nb::arg("k") = Counter::kIs1000)
.def("__init__", ([](Counter *c, double value) { new (c) Counter(value); }))
.def_readwrite("value", &Counter::value)
.def_readwrite("flags", &Counter::flags)
.def_readwrite("oneK", &Counter::oneK);
py::implicitly_convertible<py::float_, Counter>();
py::implicitly_convertible<py::int_, Counter>();
.def_readwrite("oneK", &Counter::oneK)
.def(nb::init_implicit<double>());
py::bind_map<benchmark::UserCounters>(m, "UserCounters");
nb::implicitly_convertible<nb::int_, Counter>();
nb::bind_map<benchmark::UserCounters>(m, "UserCounters");
using benchmark::State;
py::class_<State>(m, "State")
nb::class_<State>(m, "State")
.def("__bool__", &State::KeepRunning)
.def_property_readonly("keep_running", &State::KeepRunning)
.def("pause_timing", &State::PauseTiming)
@ -168,15 +167,16 @@ PYBIND11_MODULE(_benchmark, m) {
.def_property("items_processed", &State::items_processed,
&State::SetItemsProcessed)
.def("set_label", (void (State::*)(const char*)) & State::SetLabel)
.def("range", &State::range, py::arg("pos") = 0)
.def("range", &State::range, nb::arg("pos") = 0)
.def_property_readonly("iterations", &State::iterations)
.def_property_readonly("name", &State::name)
.def_readwrite("counters", &State::counters)
.def_property_readonly("thread_index", &State::thread_index)
.def_property_readonly("threads", &State::threads);
m.def("Initialize", Initialize);
m.def("RegisterBenchmark", RegisterBenchmark,
py::return_value_policy::reference);
nb::rv_policy::reference);
m.def("RunSpecifiedBenchmarks",
[]() { benchmark::RunSpecifiedBenchmarks(); });
m.def("ClearRegisteredBenchmarks", benchmark::ClearRegisteredBenchmarks);

View File

@ -0,0 +1,52 @@
config_setting(
name = "msvc_compiler",
flag_values = {"@bazel_tools//tools/cpp:compiler": "msvc-cl"},
)
cc_library(
name = "nanobind",
hdrs = glob(
include = [
"include/nanobind/*.h",
"include/nanobind/stl/*.h",
"include/nanobind/detail/*.h",
],
exclude = [],
),
srcs = [
"include/nanobind/stl/detail/nb_dict.h",
"include/nanobind/stl/detail/nb_list.h",
"include/nanobind/stl/detail/traits.h",
"ext/robin_map/include/tsl/robin_map.h",
"ext/robin_map/include/tsl/robin_hash.h",
"ext/robin_map/include/tsl/robin_growth_policy.h",
"ext/robin_map/include/tsl/robin_set.h",
"src/buffer.h",
"src/common.cpp",
"src/error.cpp",
"src/implicit.cpp",
"src/nb_enum.cpp",
"src/nb_func.cpp",
"src/nb_internals.cpp",
"src/nb_internals.h",
"src/nb_type.cpp",
"src/tensor.cpp",
"src/trampoline.cpp",
],
copts = select({
":msvc_compiler": [],
"//conditions:default": [
"-fexceptions",
"-Os", # size optimization
"-flto", # enable LTO
],
}),
linkopts = select({
"@com_github_google_benchmark//:macos": ["-undefined suppress", "-flat_namespace"],
"//conditions:default": [],
}),
includes = ["include", "ext/robin_map/include"],
deps = ["@python_headers"],
visibility = ["//visibility:public"],
)

View File

@ -1,20 +0,0 @@
cc_library(
name = "pybind11",
hdrs = glob(
include = [
"include/pybind11/*.h",
"include/pybind11/detail/*.h",
],
exclude = [
"include/pybind11/common.h",
"include/pybind11/eigen.h",
],
),
copts = [
"-fexceptions",
"-Wno-undefined-inline",
"-Wno-pragma-once-outside-header",
],
includes = ["include"],
visibility = ["//visibility:public"],
)

View File

@ -88,23 +88,31 @@ class BuildBazelExtension(build_ext.build_ext):
bazel_argv = [
"bazel",
"build",
str(ext.bazel_target),
ext.bazel_target,
f"--symlink_prefix={temp_path / 'bazel-'}",
f"--compilation_mode={'dbg' if self.debug else 'opt'}",
# C++17 is required by nanobind
f"--cxxopt={'/std:c++17' if IS_WINDOWS else '-std=c++17'}",
]
if IS_WINDOWS:
# Link with python*.lib.
for library_dir in self.library_dirs:
bazel_argv.append("--linkopt=/LIBPATH:" + library_dir)
elif IS_MAC and platform.machine() == "x86_64":
bazel_argv.append("--macos_minimum_os=10.9")
elif IS_MAC:
if platform.machine() == "x86_64":
# C++17 needs macOS 10.14 at minimum
bazel_argv.append("--macos_minimum_os=10.14")
# ARCHFLAGS is always set by cibuildwheel before macOS wheel builds.
archflags = os.getenv("ARCHFLAGS", "")
if "arm64" in archflags:
bazel_argv.append("--cpu=darwin_arm64")
bazel_argv.append("--macos_cpus=arm64")
# cross-compilation for Mac ARM64 on GitHub Mac x86 runners.
# ARCHFLAGS is set by cibuildwheel before macOS wheel builds.
archflags = os.getenv("ARCHFLAGS", "")
if "arm64" in archflags:
bazel_argv.append("--cpu=darwin_arm64")
bazel_argv.append("--macos_cpus=arm64")
elif platform.machine() == "arm64":
bazel_argv.append("--macos_minimum_os=11.0")
self.spawn(bazel_argv)
@ -146,7 +154,6 @@ setuptools.setup(
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",