From eb80b180bcad656980af173739c0826bdc9028d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Wed, 19 Aug 2026 08:53:01 +0000 Subject: [PATCH 1/8] build: use latest nanobind in split mode --- pyproject.toml | 11 ++++++--- src/monoprop/bindings/CMakeLists.txt | 7 +++--- uv.lock | 37 ++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index de384db6..c6892e02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,10 @@ [build-system] requires = [ - "nanobind>=2.13.0,<3", + "mpi4py>=4.1.0", + "nanobind-backend>=1.0.0.dev1", + "nanobind>=3.0.0.dev1", "scikit-build-core>=1.0.3,<2", "setuptools-scm>=8", - "mpi4py>=4.1.0", ] build-backend = "scikit_build_core.build" @@ -37,7 +38,7 @@ classifiers = [ "Typing :: Typed", ] dynamic = ["version"] -dependencies = ["numpy>=2", "msgpack>=1.0.0"] +dependencies = ["msgpack>=1.0.0", "nanobind-backend>=1.0.0.dev1", "numpy>=2"] [project.urls] @@ -325,7 +326,9 @@ flake8-annotations.allow-star-arg-any = true "packages/monoprop-bench-tools/src/**" = ["ANN401", "T201"] # cupy comes from an optional, GPU-only extra, so it is imported inside the # functions that need it (PLC0415). -"packages/monoprop-bench-tools/src/monoprop_bench_tools/memory/gpu.py" = ["PLC0415"] +"packages/monoprop-bench-tools/src/monoprop_bench_tools/memory/gpu.py" = [ + "PLC0415", +] [tool.ruff.lint.pycodestyle] # maximum line length to allow for line-length violations within documentation, diff --git a/src/monoprop/bindings/CMakeLists.txt b/src/monoprop/bindings/CMakeLists.txt index e18ede35..e2ae235c 100644 --- a/src/monoprop/bindings/CMakeLists.txt +++ b/src/monoprop/bindings/CMakeLists.txt @@ -104,9 +104,10 @@ configure_file( ) nanobind_add_module(_core - STABLE_ABI # Perform a stable ABI build - NB_STATIC # Compile the core nanobind library as a static library - NOMINSIZE # Don’t perform optimizations to minimize binary size + NB_SUPPRESS_WARNINGS # suppress warnings from nanobind and Python headers + NOMINSIZE # don’t perform optimizations to minimize binary size + LTO # perform link-time optimization + BACKEND_MODULE nanobind_backend ${CMAKE_CURRENT_BINARY_DIR}/bindings.cpp ${_bind_cpps} # List of generated binders ) diff --git a/uv.lock b/uv.lock index 727e7f35..3ef42ef6 100644 --- a/uv.lock +++ b/uv.lock @@ -1044,6 +1044,7 @@ name = "monoprop" source = { editable = "." } dependencies = [ { name = "msgpack" }, + { name = "nanobind-backend" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] @@ -1112,6 +1113,7 @@ workspace-test = [ requires-dist = [ { name = "mpi4py", marker = "extra == 'mpi'", specifier = ">=4.1.0" }, { name = "msgpack", specifier = ">=1.0.0" }, + { name = "nanobind-backend", specifier = ">=1.0.0.dev1" }, { name = "numpy", specifier = ">=2" }, { name = "qiskit", marker = "extra == 'qiskit'", specifier = ">=2.0.0" }, ] @@ -1313,6 +1315,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" }, ] +[[package]] +name = "nanobind-backend" +version = "1.0.0.dev2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/dd/618f711fe586cfa561ef1ca1ffe9fc70a3fa718b4dfae4bfe89e77156a7e/nanobind_backend-1.0.0.dev2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:47dff4118e2f3515fedf208d2cdc303cf15ebb86950235d0414a3cfd143963f7", size = 78563, upload-time = "2026-08-18T23:19:14.98Z" }, + { url = "https://files.pythonhosted.org/packages/9b/22/ea632eceb673ecf0a4e1516b874ffc8e62edd0a2572646ca7e4b8ccbc5be/nanobind_backend-1.0.0.dev2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84fe97884be9f0f4aebf4ec3a7e9e81f3a2795bec8d30fc7f1e9f7f87a2516ea", size = 74768, upload-time = "2026-08-18T23:19:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/cf/41/f168216c7cec240ab8ef951eaba7660c7b23278565136e37f45b6ce6da62/nanobind_backend-1.0.0.dev2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19e7d0eedea02def03627285383d5b85b3b43ab07e54dbbb58624cc51d224f0", size = 96791, upload-time = "2026-08-18T23:19:16.948Z" }, + { url = "https://files.pythonhosted.org/packages/b0/12/837240bcc05850277c9d6f3d883c25c11ad389ac29e8c7d32faf3041f7ca/nanobind_backend-1.0.0.dev2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:345fb9e1db5ce294cb54d11c36e759990af8c9892c658e32ad3fd1ddf96350e8", size = 104609, upload-time = "2026-08-18T23:19:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/f6/71/6d41fbdb7bd001ccc8a7a7eabaae4fc37e290e7cd38ba2bf1fdd44e04ea0/nanobind_backend-1.0.0.dev2-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:4aadd7816c533fc137ed9080a5cd17341dc32ec99be719327d50eac774c5b7a2", size = 97445, upload-time = "2026-08-18T23:19:18.945Z" }, + { url = "https://files.pythonhosted.org/packages/93/5b/6f1bcc443a43abc878d42186d75884d05703ca01b2d45486315b40afb5eb/nanobind_backend-1.0.0.dev2-cp311-cp311-win_amd64.whl", hash = "sha256:e1961484b6e786995973224de16abdcb337365a090dd8645734b2bb902689e4b", size = 263282, upload-time = "2026-08-18T23:19:20.036Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/0ad49fad7597ae935155698c38d5f13c9f37eb8480247d9324eeff5d1096/nanobind_backend-1.0.0.dev2-cp311-cp311-win_arm64.whl", hash = "sha256:242aa6c017c21645046fe71eb5edfaa820812e9afbdae58465b15c6db16218b8", size = 433465, upload-time = "2026-08-18T23:19:21.154Z" }, + { url = "https://files.pythonhosted.org/packages/55/e1/f8f107ee7c3c99f1f7d593c6b4fb0cf0d197db23b9869e9d769ef3d7ded7/nanobind_backend-1.0.0.dev2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:95ee0d77daf6ce8b9e5c561d8648653f48c30d97a2f700a65a3cfee0d917d968", size = 78269, upload-time = "2026-08-18T23:19:22.192Z" }, + { url = "https://files.pythonhosted.org/packages/91/33/0d2efa368d2a98c1cb9739fdbb9c38f732aedd45027f87372e1c5478810f/nanobind_backend-1.0.0.dev2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3ccc27ae8cdfa1d967cfeea4d08f1c3c45581f3f7fb5557d0e315bbe747ee90e", size = 73468, upload-time = "2026-08-18T23:19:23.207Z" }, + { url = "https://files.pythonhosted.org/packages/1b/55/e33ce6d69c5ca46b7c7045e2c73a69d9a002e43616246c86e971b01de4be/nanobind_backend-1.0.0.dev2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71e8f20a919be7aff9b21be4fd7bab419e9a684fca19a422d13390bbe58f48da", size = 95124, upload-time = "2026-08-18T23:19:24.262Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/fb2014d22072f4a24c3834ec2927a6f9d454306c285c0bb9efc966f82231/nanobind_backend-1.0.0.dev2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f69249022a660d230d1ce546ef928dbad5036abe275e9c70a4cc0871b25d74ee", size = 103911, upload-time = "2026-08-18T23:19:25.25Z" }, + { url = "https://files.pythonhosted.org/packages/15/e4/278fed7f401d9ca3cefe8798a5fe1ca2fad506c279eebae9a8357174cc18/nanobind_backend-1.0.0.dev2-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:aa4fbea9c02da00ee6b978e258234bcec5599240c5c01ca1c58a08e3baeb5f61", size = 96775, upload-time = "2026-08-18T23:19:26.278Z" }, + { url = "https://files.pythonhosted.org/packages/fd/64/6463943e068144f3680aaa78f143778e1d1b41735096006b5cf7a5ad07aa/nanobind_backend-1.0.0.dev2-cp312-cp312-win_amd64.whl", hash = "sha256:6377e3cbcfadd461371aa217997ee830f44a555d78b10a54ebf38de925f81beb", size = 262792, upload-time = "2026-08-18T23:19:27.275Z" }, + { url = "https://files.pythonhosted.org/packages/5a/65/62b457974ee4521b36379e918dac28f61f20dcf93d28d1036d16293baf5e/nanobind_backend-1.0.0.dev2-cp312-cp312-win_arm64.whl", hash = "sha256:c285983e891a768b2c8b314a87cf8c2ad6c9641250027db2c582d39ac1dad3b8", size = 432785, upload-time = "2026-08-18T23:19:28.372Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b2/201d8ae18106f1653e1bd88195e970771030e4c52d0645824b172bca8164/nanobind_backend-1.0.0.dev2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:9b7265f4dd550421f3eb1237b95efab6cac6f475bf016df5f533a80daf1d1ecc", size = 78400, upload-time = "2026-08-18T23:19:29.551Z" }, + { url = "https://files.pythonhosted.org/packages/e1/59/bbde4ff11d9302448fb141978ee3bf566e93b351ddcdced849b039965d46/nanobind_backend-1.0.0.dev2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8dccc94d918ba2862b756aa97d51fb264938617327b6a88e37be5cf7d0b3f4a", size = 73550, upload-time = "2026-08-18T23:19:30.437Z" }, + { url = "https://files.pythonhosted.org/packages/f8/78/980c0ea297c3a36980500044779f7cecb52c727fcd5b99e9e29538852288/nanobind_backend-1.0.0.dev2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6e972ed1ca7388b98b8ddd56b4015c1655c9f4fcfbdf0ad33036eb029403c85", size = 95299, upload-time = "2026-08-18T23:19:31.498Z" }, + { url = "https://files.pythonhosted.org/packages/ed/db/9d98ff9e2eb36ef04369204428f271f29b87f468662bfeeb04290a3d12cc/nanobind_backend-1.0.0.dev2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613cc718ca9b8b0d04b6db96f631e073f0815470e65df312a65c917e4e47a05", size = 104187, upload-time = "2026-08-18T23:19:32.522Z" }, + { url = "https://files.pythonhosted.org/packages/27/49/6812ac28cfbac94926eb8ec0983c3042f7e883aaf5b6ca6ce7b3b6956293/nanobind_backend-1.0.0.dev2-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:dcf6e6e83eb042105cd4c23403bb9a8cee1e57c0ae92c4c23489260ec9fb270f", size = 96995, upload-time = "2026-08-18T23:19:33.524Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a7/58df6ddc9df4b956c1f99edd411a204d1602b6c2b083c0a884ab53ea3c8c/nanobind_backend-1.0.0.dev2-cp313-cp313-win_amd64.whl", hash = "sha256:626da70a09cb934a23d842fc738d19bc452dd34e700a1ce219a86c7a71f2fa99", size = 262809, upload-time = "2026-08-18T23:19:34.413Z" }, + { url = "https://files.pythonhosted.org/packages/48/27/7bf69293129bd64721f5d760fe0dea0818b88ec5d0affcbb78098cde2007/nanobind_backend-1.0.0.dev2-cp313-cp313-win_arm64.whl", hash = "sha256:18fb598374683b34cf7054174fee9fc364efc3773f43339af03ca09c8273bda0", size = 432732, upload-time = "2026-08-18T23:19:35.383Z" }, + { url = "https://files.pythonhosted.org/packages/25/6b/32166c460ef62b0bd3348da21eafbf80bc8608ca95917a75fc92109c9821/nanobind_backend-1.0.0.dev2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c579c89752c86dfa6d64abeae01661c158d9b9b9425db0d9bde6eba39ba3fb4c", size = 78500, upload-time = "2026-08-18T23:19:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/e4/cb/e4c2be41879d9690f8721d3c4860a9cdb0a136402776efd5814b779c9dc0/nanobind_backend-1.0.0.dev2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ebea21fae4f5e99f47b6ba82df2703633d8c54c5724b21d4d337f29fa5d94177", size = 73626, upload-time = "2026-08-18T23:19:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9d/2e8e17086b92e68d8ecb6444ec1806ce27e23fd23262e033104d5f0caaab/nanobind_backend-1.0.0.dev2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7766050e0437fd18fceb26db114a6f1d333d8026cff492dacd9107d20ea2d55", size = 95915, upload-time = "2026-08-18T23:19:38.473Z" }, + { url = "https://files.pythonhosted.org/packages/ab/55/7a0aaf671582d1d9369b4e1f11b49f05deba878f831bcdc820d129b0d946/nanobind_backend-1.0.0.dev2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4c9df40baa871dafa767d481dffc1dd50f105adbdf738f16189165faf5fd346", size = 104531, upload-time = "2026-08-18T23:19:39.485Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f9/807e763a029488486f6104a5545dd045b04cc127fc32b693b93560fdc577/nanobind_backend-1.0.0.dev2-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:c65b222cdd04e8b5059254c0a61ceb1b775da27270b45853c6c4efd155d1f223", size = 96880, upload-time = "2026-08-18T23:19:40.435Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7f/abb959beb6a3e6f09e8dd310b0fa48d8ac5f724d49651e2d6cec40a04cc4/nanobind_backend-1.0.0.dev2-cp314-cp314-win_amd64.whl", hash = "sha256:253a304bd26781ebc849e3d6722b39fac22322e41aca3fe03ad89c5775c8ed3b", size = 271814, upload-time = "2026-08-18T23:19:41.404Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/47d5b3498d1688c9559bd80862e735751591401ab5a967f5d4a4638214a1/nanobind_backend-1.0.0.dev2-cp314-cp314-win_arm64.whl", hash = "sha256:e9955fd670b317a1f97d33d5ff96e200ae3c5594846e7492dcb69abf6aa98ef9", size = 448575, upload-time = "2026-08-18T23:19:42.49Z" }, +] + [[package]] name = "nbclient" version = "0.11.0" From 231bf14c8c6905173a14894400c907f265d9d4e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Wed, 19 Aug 2026 09:02:25 +0000 Subject: [PATCH 2/8] build(python): :zap: prune cibuildwheel build matrix --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c6892e02..6fd1b74c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -114,8 +114,8 @@ provider = "scikit_build_core.metadata.setuptools_scm" minimum-version = "build-system.requires" # setuptools-style build caching in a local directory build-dir = "build/{state}/{build_type}" -# build stable ABI wheels for CPython 3.12+ -wheel.py-api = "cp312" +# build stable ABI wheels for CPython 3.10 +wheel.py-api = "cp310" logging.level = "INFO" # read CMake version from CMakeLists.txt cmake.version = "CMakeLists.txt" @@ -370,7 +370,7 @@ reportMatchNotExhaustive = "information" [tool.cibuildwheel] build-frontend = "uv" build-verbosity = 1 -build = ["cp311-*", "cp312-*", "cp313-*", "cp314-*"] +build = ["cp311"] skip = ["*-musllinux_*"] test-command = "python -m pytest {package}/tests" test-groups = ["test"] From ac9fc2140ad570a6911ff0b37435cfdbc27d0e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Wed, 19 Aug 2026 09:05:59 +0000 Subject: [PATCH 3/8] build: set wheel.py-api to cp311 3.10 is not supported --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6fd1b74c..b95cb7a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -114,8 +114,8 @@ provider = "scikit_build_core.metadata.setuptools_scm" minimum-version = "build-system.requires" # setuptools-style build caching in a local directory build-dir = "build/{state}/{build_type}" -# build stable ABI wheels for CPython 3.10 -wheel.py-api = "cp310" +# build stable ABI wheels for CPython 3.11 +wheel.py-api = "cp311" logging.level = "INFO" # read CMake version from CMakeLists.txt cmake.version = "CMakeLists.txt" From 189dfeca97c15b5ffda0fa2af5e2ec4f3f452193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Wed, 19 Aug 2026 09:08:18 +0000 Subject: [PATCH 4/8] fix(cibuildwheel): build selector needs wildcard --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b95cb7a1..12afe6db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -370,7 +370,7 @@ reportMatchNotExhaustive = "information" [tool.cibuildwheel] build-frontend = "uv" build-verbosity = 1 -build = ["cp311"] +build = ["cp311-*"] skip = ["*-musllinux_*"] test-command = "python -m pytest {package}/tests" test-groups = ["test"] From 9c45aad36397a7d16b19087482729ab5ee18ac09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Wed, 19 Aug 2026 09:27:54 +0000 Subject: [PATCH 5/8] fix(cibuildwheel): recover testing against all supported python versions --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 12afe6db..ece329a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -368,9 +368,9 @@ reportMatchNotExhaustive = "information" [tool.cibuildwheel] +build = "cp3??-*" build-frontend = "uv" build-verbosity = 1 -build = ["cp311-*"] skip = ["*-musllinux_*"] test-command = "python -m pytest {package}/tests" test-groups = ["test"] From b9b9becca95bd561383d4d046f94385dfb0db2ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Wed, 19 Aug 2026 10:20:22 +0000 Subject: [PATCH 6/8] perf: use improved nanobind tuple creation methods --- src/monoprop/bindings/binder.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/monoprop/bindings/binder.h b/src/monoprop/bindings/binder.h index 5728aca5..9aad5003 100644 --- a/src/monoprop/bindings/binder.h +++ b/src/monoprop/bindings/binder.h @@ -202,11 +202,11 @@ auto bind_monomial_propagator(nb::module_ &mod) -> void { [](MonomialPropagator &self, const VecD ¶meters, double atol) -> nb::dict { nb::dict py_result; for (const auto &[indices, coeff] : self.evolved_operator_terms(parameters, atol)) { - nb::list key; + nb::tuple_builder key(indices.size()); for (const auto &i : indices) { - key.append(i); + key.put(i); } - py_result[nb::tuple(key)] = coeff; + py_result[key.commit()] = coeff; } if (!self.schrodinger() && std::abs(self.core_term()) >= atol) { From 43d61b958550a254d77f11471b7322535dbb60d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Di=20Remigio=20Eik=C3=A5s?= Date: Wed, 19 Aug 2026 10:23:10 +0000 Subject: [PATCH 7/8] feat(benches): :chart_with_upwards_trend: add microbenchmarks to measure binding overhead Assisted-by: GitHubCopilot:gpt-5.6.sol --- AGENTS.md | 7 +- README.md | 4 +- benches/README.md | 15 ++- benches/bench_bindings.py | 117 ++++++++++++++++++ benches/conftest.py | 10 ++ docs/content/docs/benchmarks.mdx | 19 ++- .../src/monoprop_bench_tools/report.py | 14 ++- .../monoprop-bench-tools/tests/test_report.py | 16 +++ src/monoprop/__init__.py | 2 + src/monoprop/bindings/bindings.cpp.in | 1 + 10 files changed, 198 insertions(+), 7 deletions(-) create mode 100644 benches/bench_bindings.py diff --git a/AGENTS.md b/AGENTS.md index 8993acbe..6aea5b0b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,8 +56,11 @@ The root `pyproject.toml` declares a `[tool.uv.workspace]`, so one `uv.lock` cov wheels, so it is a standalone uv project with its own `uv.lock`. Run it with `cd packages/bench-third-party && uv sync`, never from the root environment. - `benches/` — monoprop's own benchmark suite (`conftest.py`, `bench_*.py`, `results/`). It stays in - the repository and imports the tools package. Benchmark names are Bencher's history key, so they - must not move with a library release; that is why the suite is not in `monoprop-bench-tools`. + the repository and imports the tools package. `bench_bindings.py` contains serial-only boundary + microbenchmarks; run it on a fixed host and Release build. Artifacts record Python, compiled + nanobind frontend, and runtime backend versions. Benchmark names are Bencher's history key, so + they must not move with a library release; that is why the suite is not in + `monoprop-bench-tools`. Dependency groups follow from that split: `test` is monoprop's own suite only (cibuildwheel installs it against a built wheel, so it must not reference a workspace member), `workspace-test` adds diff --git a/README.md b/README.md index afab6abb..0074f35b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,9 @@ and against [`MajoranaPropagation.jl`](https://github.com/SparqleSim/MajoranaPro Head to our [benchmarks page](https://docs.monoprop.algorithmiq.tech/benchmarks) for more details. Every commit on `main` also runs the internal benchmark suite, tracked over time -with [Bencher](https://bencher.dev/) to catch performance regressions. +with [Bencher](https://bencher.dev/) to catch propagation and Python/C++ binding +regressions. Reports record the Python, nanobind frontend, and runtime backend +versions for reproducible comparisons. 📖 **Full documentation:** diff --git a/benches/README.md b/benches/README.md index 781bf27f..91354b3a 100644 --- a/benches/README.md +++ b/benches/README.md @@ -5,16 +5,27 @@ The `monoprop` repository includes a pytest suite measuring the **time** and The suite is separated from the test suite and can be run with `just bench`. See below for more detailed instructions. -See the the [Benchmarks](../docs/content/docs/benchmarks.mdx) section of the +See the [Benchmarks](../docs/content/docs/benchmarks.mdx) section of the documentation for detailed instructions. ## What lives where This directory holds only monoprop's own benchmarks — `conftest.py` (the fixtures -and the results schema), `bench_random.py`, `bench_models.py`, and `results/`. +and the results schema), `bench_random.py`, `bench_models.py`, `bench_bindings.py`, +and `results/`. The binding microbenchmarks are serial-only and isolate Python/C++ +call and return-conversion costs from propagation work. Run them directly with: + +```bash +uv run pytest benches/bench_bindings.py --benchmark-only +``` + Benchmark names are the key [Bencher](https://bencher.dev/) stores history under, so they stay here rather than moving with a library release. +Each run records its Python version, the nanobind version compiled into the +extension, and the installed `nanobind-backend` version. Keep the host, Release +build, and benchmark command fixed when comparing binding revisions. + Everything reusable is in the `monoprop-bench-tools` package ([`../packages/monoprop-bench-tools`](../packages/monoprop-bench-tools)): the memory instrumentation, the model builders, and the two renderers that turn a diff --git a/benches/bench_bindings.py b/benches/bench_bindings.py new file mode 100644 index 00000000..a8c0beb4 --- /dev/null +++ b/benches/bench_bindings.py @@ -0,0 +1,117 @@ +# Copyright 2026 Algorithmiq +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Microbenchmarks isolating Python/C++ binding overhead.""" + +from __future__ import annotations + +from itertools import combinations, islice + +import pytest + +import monoprop +from monoprop import MajoranaPropagator +from monoprop.fermi import MajoranaOperator + +BOUNDARY_ROUNDS = 20 +CHEAP_CALL_ITERATIONS = 10_000 +FUNCTIONAL_ITERATIONS = 1_000 +LARGE_TERM_COUNT = 1_024 +NUM_MODES = 32 +OUTPUT_ATOL = 1e-300 + + +@pytest.fixture(scope="module", autouse=True) +def _serial_only(bench_comm): + if bench_comm is not None and bench_comm.Get_size() > 1: + pytest.skip("binding microbenchmarks are serial-only") + + +@pytest.fixture(scope="module") +def binding_propagators(): + """Build fixed small and large propagators outside timed regions.""" + small = MajoranaPropagator( + MajoranaOperator({(0, 1, 2, 3): 1.0}, NUM_MODES), + [], + cutoff=2 * NUM_MODES, + ) + large_terms = dict.fromkeys( + islice(combinations(range(2 * NUM_MODES), 4), LARGE_TERM_COUNT), 1.0 + ) + large = MajoranaPropagator( + MajoranaOperator(large_terms, NUM_MODES), + [], + cutoff=2 * NUM_MODES, + ) + return {"small": small, "large": large} + + +def test_binding_is_antihermitian_positional(benchmark): + """Benchmark positional free-function dispatch.""" + result = benchmark.pedantic( + monoprop.is_antihermitian, + args=([0, 1],), + rounds=BOUNDARY_ROUNDS, + iterations=CHEAP_CALL_ITERATIONS, + ) + assert result is True + + +def test_binding_is_antihermitian_keyword(benchmark): + """Benchmark keyword free-function dispatch.""" + result = benchmark.pedantic( + monoprop.is_antihermitian, + kwargs={"indices": [0, 1]}, + rounds=BOUNDARY_ROUNDS, + iterations=CHEAP_CALL_ITERATIONS, + ) + assert result is True + + +def test_binding_size(benchmark, binding_propagators): + """Benchmark a bound method with a scalar return value.""" + simulator = binding_propagators["small"]._simulator + result = benchmark.pedantic( + simulator.size, + rounds=BOUNDARY_ROUNDS, + iterations=CHEAP_CALL_ITERATIONS, + ) + assert result == 1 + + +def test_binding_expectation_value_functional(benchmark, binding_propagators): + """Benchmark calls through an already-created functional.""" + functional = binding_propagators["small"].expectation_value_functional() + result = benchmark.pedantic( + functional, + args=([],), + rounds=BOUNDARY_ROUNDS, + iterations=FUNCTIONAL_ITERATIONS, + ) + assert isinstance(result, float) + + +@pytest.mark.parametrize("operator_size", ["small", "large"]) +def test_binding_evolved_operator(benchmark, binding_propagators, operator_size): + """Benchmark raw evolved-operator dictionary conversion.""" + simulator = binding_propagators[operator_size]._simulator + iterations = 100 if operator_size == "small" else 1 + expected_terms = 1 if operator_size == "small" else LARGE_TERM_COUNT + result = benchmark.pedantic( + simulator.evolved_operator, + args=([], OUTPUT_ATOL), + rounds=BOUNDARY_ROUNDS, + iterations=iterations, + ) + assert len(result) == expected_terms diff --git a/benches/conftest.py b/benches/conftest.py index b469ca41..c19b4cb1 100644 --- a/benches/conftest.py +++ b/benches/conftest.py @@ -34,8 +34,10 @@ import json import os +import platform import socket from dataclasses import asdict, fields +from importlib.metadata import PackageNotFoundError, version from pathlib import Path from typing import TYPE_CHECKING, Any @@ -160,6 +162,11 @@ def pytest_addoption(parser: pytest.Parser) -> None: def _meta() -> dict[str, Any]: """Return this run's configuration metadata for the report.""" + try: + nanobind_backend_version = version("nanobind-backend") + except PackageNotFoundError: + nanobind_backend_version = "not installed" + return { "label": os.environ.get("monoprop_BENCH_LABEL", "?"), # noqa: SIM112 "ranks": _size(), @@ -170,6 +177,9 @@ def _meta() -> dict[str, Any]: "monoprop_version": monoprop.__version__, "monoprop_variant": monoprop.__variant__, "monoprop_compiler_flags": monoprop.__compiler_flags__, + "python_version": platform.python_version(), + "nanobind_version": monoprop.__nanobind_version__, + "nanobind_backend_version": nanobind_backend_version, } diff --git a/docs/content/docs/benchmarks.mdx b/docs/content/docs/benchmarks.mdx index 937f1f96..22def8ce 100644 --- a/docs/content/docs/benchmarks.mdx +++ b/docs/content/docs/benchmarks.mdx @@ -446,11 +446,28 @@ aborts if MPI is missing; extra args are forwarded to `mpiexec` (as root, add Override any config field via `---` (e.g. `--pauli-num-layers 30`); `just bench --help` lists all. +**Bindings** (`bench_bindings.py`; fixed, serial-only) — low-work calls that +isolate Python/C++ boundary costs: positional and keyword dispatch, a bound +method, an already-created expectation-value functional, and raw evolved-operator +conversion for one and 1,024 terms. These are microbenchmarks of binding overhead, +not propagation throughput. Run only this group with: + +```bash +uv run pytest benches/bench_bindings.py --benchmark-only +``` + +For an upgrade comparison, use Release builds on one pinned host and keep the +interpreter and command fixed. Compare the old dependency, the new dependency +before wrapper edits, and the new dependency after wrapper edits to separate +automatic nanobind improvements from source-level changes. + ### Output Each run writes `results/time-