diff --git a/.github/install_bazel.sh b/.github/install_bazel.sh index afdd8db8..bb910d8b 100644 --- a/.github/install_bazel.sh +++ b/.github/install_bazel.sh @@ -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 diff --git a/.github/workflows/test_bindings.yml b/.github/workflows/test_bindings.yml index c0e1c9af..98fa7e1c 100644 --- a/.github/workflows/test_bindings.yml +++ b/.github/workflows/test_bindings.yml @@ -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 ] diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e8c80740..d3c4630e 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -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 }} diff --git a/BUILD.bazel b/BUILD.bazel index 64f86eed..99616163 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -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 = { diff --git a/bazel/benchmark_deps.bzl b/bazel/benchmark_deps.bzl index 03f8ca42..8c786fbc 100644 --- a/bazel/benchmark_deps.bzl +++ b/bazel/benchmark_deps.bzl @@ -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(): diff --git a/bindings/python/google_benchmark/BUILD b/bindings/python/google_benchmark/BUILD index 3c1561f4..89ec76e0 100644 --- a/bindings/python/google_benchmark/BUILD +++ b/bindings/python/google_benchmark/BUILD @@ -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", ], ) diff --git a/bindings/python/google_benchmark/benchmark.cc b/bindings/python/google_benchmark/benchmark.cc index 5614b928..991da5a5 100644 --- a/bindings/python/google_benchmark/benchmark.cc +++ b/bindings/python/google_benchmark/benchmark.cc @@ -2,19 +2,16 @@ #include "benchmark/benchmark.h" -#include -#include -#include +#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 Initialize(const std::vector& argv) { // The `argv` pointers here become invalid when this function returns, but @@ -38,14 +35,15 @@ std::vector Initialize(const std::vector& 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_(m, "TimeUnit") + nb::enum_(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_(m, "BigO") + nb::enum_(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_(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_(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_ py_counter(m, "Counter"); + nb::class_ py_counter(m, "Counter"); - py::enum_(py_counter, "Flags") + nb::enum_(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_(py_counter, "OneK") + nb::enum_(py_counter, "OneK") .value("kIs1000", Counter::OneK::kIs1000) .value("kIs1024", Counter::OneK::kIs1024) .export_values(); py_counter - .def(py::init(), - 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(), + 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::implicitly_convertible(); + .def_readwrite("oneK", &Counter::oneK) + .def(nb::init_implicit()); - py::bind_map(m, "UserCounters"); + nb::implicitly_convertible(); + + nb::bind_map(m, "UserCounters"); using benchmark::State; - py::class_(m, "State") + nb::class_(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); diff --git a/bindings/python/nanobind.BUILD b/bindings/python/nanobind.BUILD new file mode 100644 index 00000000..9a8d6a04 --- /dev/null +++ b/bindings/python/nanobind.BUILD @@ -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"], +) diff --git a/bindings/python/pybind11.BUILD b/bindings/python/pybind11.BUILD deleted file mode 100644 index bc833500..00000000 --- a/bindings/python/pybind11.BUILD +++ /dev/null @@ -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"], -) diff --git a/setup.py b/setup.py index 0f486362..2388f59b 100644 --- a/setup.py +++ b/setup.py @@ -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",