Repository: rapidsai/kvikio (language: python)

Issue:
Forward-merge branch-24.06 into branch-24.08
Forward-merge triggered by push to branch-24.06 that creates a PR to keep branch-24.08 up-to-date. If this PR is unable to be immediately merged due to conflicts, it will remain open for the team to manually merge. See [forward-merger docs](https://docs.rapids.ai/maintainers/forward-merger/) for more info.
add wheel output
closes #250 

I'm not sure if it makes sense to split the wheel into one dedicated wheel for the shared library, and another for the python wrapper. That's what @vyasr has been doing on other libraries, like https://github.com/rapidsai/rmm/pull/1529. If it makes sense here, I'll add that.
Adding wheels for KvikIO
Would be useful to have wheels for KvikIO as well. This could be helpful for users coming from `pip` (like some Dask-CUDA users). Raising this to track adding them.

Note: There may be some things that need to be fixed as part of this like issue ( https://github.com/rapidsai/kvikio/issues/233 )
[FEA]: Add NVTX annotations to public kvikio functions
As part of benchmarking, I was confused why the _first_ usage of IO in libcudf was so slow (even for a tiny file), but then I realised that it was because we were initializing the kvikio cuFileAPI instance.

This would have been easy to spot if kvikio had nvtx annotations that could indicate what it was doing.
add wheel output
closes #250 

I'm not sure if it makes sense to split the wheel into one dedicated wheel for the shared library, and another for the python wrapper. That's what @vyasr has been doing on other libraries, like https://github.com/rapidsai/rmm/pull/1529. If it makes sense here, I'll add that.
Moving from `pynvml` to `nvidia-ml-py`
Currently this is making use of `pynvml` in a few places:

https://github.com/rapidsai/kvikio/blob/c152f633779d8c41791a3b76f9f678f926a9847d/python/benchmarks/single-node-io.py#L248-L250

However we would like to move to `nvidia-ml-py` in the future. Raising this issue to track this work


Discussion / hints:

Yeah for consistency with the rest of RAPIDS using separate C++ and Python wheels is the way to go. We'll be able to leverage that by using the libkvikio wheel during the libcudf wheel build since libcudf needs libkvikio headers. You can see that in [your cudf PR](https://github.com/rapidsai/cudf/pull/15483) those are [currently being bundled into the libcudf wheel](https://github.com/rapidsai/cudf/actions/runs/8714266105/job/23904261719?pr=15483#step:9:2552) which is not ideal. Using the libkvikio wheel from this PR instead will fix that.
Error in wheel build:

```
2024-04-23T14:25:13.7290506Z   CMake Debug Log at /tmp/pip-build-env-c44ywnqk/overlay/lib/python3.11/site-packages/cmake/data/share/cmake-3.29/Modules/FindCUDAToolkit.cmake:1093 (find_library):
2024-04-23T14:25:13.7290723Z     find_library called with the following settings:
2024-04-23T14:25:13.7291226Z 
2024-04-23T14:25:13.7292425Z       VAR: CUDA_nvgraph_LIBRARY
2024-04-23T14:25:13.7293410Z       NAMES: "nvgraph"
2024-04-23T14:25:13.7294445Z       Documentation: Path to a library.
2024-04-23T14:25:13.7295560Z       Framework
2024-04-23T14:25:13.7296475Z         Only Search Frameworks: 0
2024-04-23T14:25:13.7297410Z         Search Frameworks Last: 0
2024-04-23T14:25:13.7298380Z         Search Frameworks First: 0
2024-04-23T14:25:13.7299296Z       AppBundle
2024-04-23T14:25:13.7300297Z         Only Search AppBundle: 0
2024-04-23T14:25:13.7301260Z         Search AppBundle Last: 0
2024-04-23T14:25:13.7302243Z         Search AppBundle First: 0
2024-04-23T14:25:13.7303230Z       CMAKE_FIND_USE_CMAKE_PATH: 1
2024-04-23T14:25:13.7304297Z       CMAKE_FIND_USE_CMAKE_ENVIRONMENT_PATH: 1
2024-04-23T14:25:13.7305256Z       CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH: 1
2024-04-23T14:25:13.7306202Z       CMAKE_FIND_USE_CMAKE_SYSTEM_PATH: 1
2024-04-23T14:25:13.7307195Z       CMAKE_FIND_USE_INSTALL_PREFIX: 1
2024-04-23T14:25:13.7308173Z 
2024-04-23T14:25:13.7309466Z     find_library considered the following locations:
2024-04-23T14:25:13.7310161Z 
2024-04-23T14:25:13.7311636Z       /pyenv/versions/3.11.9/lib/python3.11/site-packages/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7312856Z       /tmp/pip-build-env-c44ywnqk/overlay/lib/python3.11/site-packages/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7313631Z       /usr/local/cuda-11.8/targets/sbsa-linux/lib/stubs/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7314756Z       /usr/local/cuda-11.8/targets/sbsa-linux/lib/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7315623Z       /usr/local/cuda/targets/sbsa-linux/lib/stubs/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7316798Z       /usr/local/cuda/targets/sbsa-linux/lib/stubs/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7317635Z       /usr/local/cuda/targets/sbsa-linux/lib/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7318804Z       /tmp/pip-build-env-c44ywnqk/overlay/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7319505Z       /pyenv/versions/3.11.9/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7320537Z       /pyenv/libexec/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7321656Z       /pyenv/plugins/python-build/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7322702Z       /pyenv/plugins/pyenv-virtualenv/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7323693Z       /pyenv/plugins/pyenv-update/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7324697Z       /pyenv/plugins/pyenv-doctor/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7325540Z       /usr/share/Modules/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7326723Z       /opt/rh/gcc-toolset-11/root/usr/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7327383Z       /pyenv/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7328500Z       /pyenv/shims/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7329497Z       /usr/local/cuda/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7330462Z       /usr/local/sbin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7331416Z       /usr/local/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7332371Z       /usr/sbin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7333400Z       /usr/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7334331Z       /sbin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7335402Z       /bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7336379Z       /usr/local/lib64/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7337336Z       /usr/local/lib/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7338301Z       /usr/local/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7339339Z       /usr/lib64/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7340462Z       /usr/lib/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7341307Z       /usr/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7342246Z       /lib64/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7343187Z       /lib/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7344763Z       /tmp/pip-build-env-c44ywnqk/overlay/lib/python3.11/site-packages/cmake/data/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7345173Z       /opt/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7346285Z       /usr/lib64/X11/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7347077Z 
2024-04-23T14:25:13.7348338Z     The item was not found.
```
Feel free to add this to the [devcontainer build job](https://github.com/rapidsai/kvikio/blob/7b0231c8241164c8a0511cd737e7098cb7ccea3e/.github/workflows/pr.yaml#L67-L70) to test the path adjustment in the devcontainers before merging:
```yaml
      build_command: |
        sudo yq -i '.repos[2].python[0].sub_dir = "python/kvikio"' /opt/rapids-build-utils/manifest.yaml;
        rapids-generate-scripts;
```
Youll need to add a line [like this](https://github.com/rapidsai/rmm/blob/branch-24.06/python/librmm/pyproject.toml#L55) indicating that the wheels built for libkvikio are compatible with any Python version. Without that, scikit-build assumes that the wheels are compiled against Python and are therefore only compatible with the minor version of Python that you built against (in this case, Python 3.11).
Finally have passing tests here. @vyasr please take one more pass on this.
For anyone new to this thread, the failures in the devcontainers builds are expected right now. The plan to proceed is to get approval from ops and kvikio code owners on this PR, then merge the related devcontainers PR (https://github.com/rapidsai/devcontainers/pull/283), then re-run CI here. Assuming all goes well with that re-run, this PR will be ready to merge.
Once we have approvals, the final steps to merge will be
1. Cleaning out the special logic in this PR for running devcontainer jobs without the upstream updates
2. Merging the devcontainer PR
3. Rerunning CI here to make sure everything passes
I think this would be great!


Yeah for consistency with the rest of RAPIDS using separate C++ and Python wheels is the way to go. We'll be able to leverage that by using the libkvikio wheel during the libcudf wheel build since libcudf needs libkvikio headers. You can see that in [your cudf PR](https://github.com/rapidsai/cudf/pull/15483) those are [currently being bundled into the libcudf wheel](https://github.com/rapidsai/cudf/actions/runs/8714266105/job/23904261719?pr=15483#step:9:2552) which is not ideal. Using the libkvikio wheel from this PR instead will fix that.
Error in wheel build:

```
2024-04-23T14:25:13.7290506Z   CMake Debug Log at /tmp/pip-build-env-c44ywnqk/overlay/lib/python3.11/site-packages/cmake/data/share/cmake-3.29/Modules/FindCUDAToolkit.cmake:1093 (find_library):
2024-04-23T14:25:13.7290723Z     find_library called with the following settings:
2024-04-23T14:25:13.7291226Z 
2024-04-23T14:25:13.7292425Z       VAR: CUDA_nvgraph_LIBRARY
2024-04-23T14:25:13.7293410Z       NAMES: "nvgraph"
2024-04-23T14:25:13.7294445Z       Documentation: Path to a library.
2024-04-23T14:25:13.7295560Z       Framework
2024-04-23T14:25:13.7296475Z         Only Search Frameworks: 0
2024-04-23T14:25:13.7297410Z         Search Frameworks Last: 0
2024-04-23T14:25:13.7298380Z         Search Frameworks First: 0
2024-04-23T14:25:13.7299296Z       AppBundle
2024-04-23T14:25:13.7300297Z         Only Search AppBundle: 0
2024-04-23T14:25:13.7301260Z         Search AppBundle Last: 0
2024-04-23T14:25:13.7302243Z         Search AppBundle First: 0
2024-04-23T14:25:13.7303230Z       CMAKE_FIND_USE_CMAKE_PATH: 1
2024-04-23T14:25:13.7304297Z       CMAKE_FIND_USE_CMAKE_ENVIRONMENT_PATH: 1
2024-04-23T14:25:13.7305256Z       CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH: 1
2024-04-23T14:25:13.7306202Z       CMAKE_FIND_USE_CMAKE_SYSTEM_PATH: 1
2024-04-23T14:25:13.7307195Z       CMAKE_FIND_USE_INSTALL_PREFIX: 1
2024-04-23T14:25:13.7308173Z 
2024-04-23T14:25:13.7309466Z     find_library considered the following locations:
2024-04-23T14:25:13.7310161Z 
2024-04-23T14:25:13.7311636Z       /pyenv/versions/3.11.9/lib/python3.11/site-packages/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7312856Z       /tmp/pip-build-env-c44ywnqk/overlay/lib/python3.11/site-packages/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7313631Z       /usr/local/cuda-11.8/targets/sbsa-linux/lib/stubs/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7314756Z       /usr/local/cuda-11.8/targets/sbsa-linux/lib/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7315623Z       /usr/local/cuda/targets/sbsa-linux/lib/stubs/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7316798Z       /usr/local/cuda/targets/sbsa-linux/lib/stubs/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7317635Z       /usr/local/cuda/targets/sbsa-linux/lib/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7318804Z       /tmp/pip-build-env-c44ywnqk/overlay/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7319505Z       /pyenv/versions/3.11.9/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7320537Z       /pyenv/libexec/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7321656Z       /pyenv/plugins/python-build/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7322702Z       /pyenv/plugins/pyenv-virtualenv/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7323693Z       /pyenv/plugins/pyenv-update/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7324697Z       /pyenv/plugins/pyenv-doctor/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7325540Z       /usr/share/Modules/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7326723Z       /opt/rh/gcc-toolset-11/root/usr/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7327383Z       /pyenv/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7328500Z       /pyenv/shims/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7329497Z       /usr/local/cuda/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7330462Z       /usr/local/sbin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7331416Z       /usr/local/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7332371Z       /usr/sbin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7333400Z       /usr/bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7334331Z       /sbin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7335402Z       /bin/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7336379Z       /usr/local/lib64/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7337336Z       /usr/local/lib/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7338301Z       /usr/local/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7339339Z       /usr/lib64/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7340462Z       /usr/lib/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7341307Z       /usr/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7342246Z       /lib64/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7343187Z       /lib/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7344763Z       /tmp/pip-build-env-c44ywnqk/overlay/lib/python3.11/site-packages/cmake/data/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7345173Z       /opt/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7346285Z       /usr/lib64/X11/libnvgraph(\.so|\.a)
2024-04-23T14:25:13.7347077Z 
2024-04-23T14:25:13.7348338Z     The item was not found.
```
Feel free to add this to the [devcontainer build job](https://github.com/rapidsai/kvikio/blob/7b0231c8241164c8a0511cd737e7098cb7ccea3e/.github/workflows/pr.yaml#L67-L70) to test the path adjustment in the devcontainers before merging:
```yaml
      build_command: |
        sudo yq -i '.repos[2].python[0].sub_dir = "python/kvikio"' /opt/rapids-build-utils/manifest.yaml;
        rapids-generate-scripts;
```
Youll need to add a line [like this](https://github.com/rapidsai/rmm/blob/branch-24.06/python/librmm/pyproject.toml#L55) indicating that the wheels built for libkvikio are compatible with any Python version. Without that, scikit-build assumes that the wheels are compiled against Python and are therefore only compatible with the minor version of Python that you built against (in this case, Python 3.11).
Finally have passing tests here. @vyasr please take one more pass on this.
For anyone new to this thread, the failures in the devcontainers builds are expected right now. The plan to proceed is to get approval from ops and kvikio code owners on this PR, then merge the related devcontainers PR (https://github.com/rapidsai/devcontainers/pull/283), then re-run CI here. Assuming all goes well with that re-run, this PR will be ready to merge.
Once we have approvals, the final steps to merge will be
1. Cleaning out the special logic in this PR for running devcontainer jobs without the upstream updates
2. Merging the devcontainer PR
3. Rerunning CI here to make sure everything passes


Fix patch:
diff --git a/.devcontainer/cuda11.8-conda/devcontainer.json b/.devcontainer/cuda11.8-conda/devcontainer.json
index e9319b8902..1f9d029417 100644
--- a/.devcontainer/cuda11.8-conda/devcontainer.json
+++ b/.devcontainer/cuda11.8-conda/devcontainer.json
@@ -5,12 +5,17 @@
     "args": {
       "CUDA": "11.8",
       "PYTHON_PACKAGE_MANAGER": "conda",
-      "BASE": "rapidsai/devcontainers:24.06-cpp-llvm16-cuda11.8-mambaforge-ubuntu22.04"
+      "BASE": "rapidsai/devcontainers:24.08-cpp-cuda11.8-mambaforge-ubuntu22.04"
     }
   },
+  "runArgs": [
+    "--rm",
+    "--name",
+    "${localEnv:USER:anon}-rapids-${localWorkspaceFolderBasename}-24.08-cuda11.8-conda"
+  ],
   "hostRequirements": {"gpu": "optional"},
   "features": {
-    "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils:24.6": {}
+    "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils:24.8": {}
   },
   "overrideFeatureInstallOrder": [
     "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils"
diff --git a/.devcontainer/cuda11.8-pip/devcontainer.json b/.devcontainer/cuda11.8-pip/devcontainer.json
index 4af320f9c4..1472bd95bb 100644
--- a/.devcontainer/cuda11.8-pip/devcontainer.json
+++ b/.devcontainer/cuda11.8-pip/devcontainer.json
@@ -5,12 +5,17 @@
     "args": {
       "CUDA": "11.8",
       "PYTHON_PACKAGE_MANAGER": "pip",
-      "BASE": "rapidsai/devcontainers:24.06-cpp-cuda11.8-ubuntu22.04"
+      "BASE": "rapidsai/devcontainers:24.08-cpp-cuda11.8-ubuntu22.04"
     }
   },
+  "runArgs": [
+    "--rm",
+    "--name",
+    "${localEnv:USER:anon}-rapids-${localWorkspaceFolderBasename}-24.08-cuda11.8-pip"
+  ],
   "hostRequirements": {"gpu": "optional"},
   "features": {
-    "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils:24.6": {}
+    "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils:24.8": {}
   },
   "overrideFeatureInstallOrder": [
     "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils"
diff --git a/.devcontainer/cuda12.2-conda/devcontainer.json b/.devcontainer/cuda12.5-conda/devcontainer.json
similarity index 82%
rename from .devcontainer/cuda12.2-conda/devcontainer.json
rename to .devcontainer/cuda12.5-conda/devcontainer.json
index 275dd0f15a..b815b954b9 100644
--- a/.devcontainer/cuda12.2-conda/devcontainer.json
+++ b/.devcontainer/cuda12.5-conda/devcontainer.json
@@ -3,19 +3,24 @@
     "context": "${localWorkspaceFolder}/.devcontainer",
     "dockerfile": "${localWorkspaceFolder}/.devcontainer/Dockerfile",
     "args": {
-      "CUDA": "12.2",
+      "CUDA": "12.5",
       "PYTHON_PACKAGE_MANAGER": "conda",
-      "BASE": "rapidsai/devcontainers:24.06-cpp-mambaforge-ubuntu22.04"
+      "BASE": "rapidsai/devcontainers:24.08-cpp-mambaforge-ubuntu22.04"
     }
   },
+  "runArgs": [
+    "--rm",
+    "--name",
+    "${localEnv:USER:anon}-rapids-${localWorkspaceFolderBasename}-24.08-cuda12.5-conda"
+  ],
   "hostRequirements": {"gpu": "optional"},
   "features": {
-    "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils:24.6": {}
+    "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils:24.8": {}
   },
   "overrideFeatureInstallOrder": [
     "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils"
   ],
-  "initializeCommand": ["/bin/bash", "-c", "mkdir -m 0755 -p ${localWorkspaceFolder}/../.{aws,cache,config,conda/pkgs,conda/${localWorkspaceFolderBasename}-cuda12.2-envs}"],
+  "initializeCommand": ["/bin/bash", "-c", "mkdir -m 0755 -p ${localWorkspaceFolder}/../.{aws,cache,config,conda/pkgs,conda/${localWorkspaceFolderBasename}-cuda12.5-envs}"],
   "postAttachCommand": ["/bin/bash", "-c", "if [ ${CODESPACES:-false} = 'true' ]; then . devcontainer-utils-post-attach-command; . rapids-post-attach-command; fi"],
   "workspaceFolder": "/home/coder",
   "workspaceMount": "source=${localWorkspaceFolder},target=/home/coder/kvikio,type=bind,consistency=consistent",
@@ -24,7 +29,7 @@
     "source=${localWorkspaceFolder}/../.cache,target=/home/coder/.cache,type=bind,consistency=consistent",
     "source=${localWorkspaceFolder}/../.config,target=/home/coder/.config,type=bind,consistency=consistent",
     "source=${localWorkspaceFolder}/../.conda/pkgs,target=/home/coder/.conda/pkgs,type=bind,consistency=consistent",
-    "source=${localWorkspaceFolder}/../.conda/${localWorkspaceFolderBasename}-cuda12.2-envs,target=/home/coder/.conda/envs,type=bind,consistency=consistent"
+    "source=${localWorkspaceFolder}/../.conda/${localWorkspaceFolderBasename}-cuda12.5-envs,target=/home/coder/.conda/envs,type=bind,consistency=consistent"
   ],
   "customizations": {
     "vscode": {
diff --git a/.devcontainer/cuda12.2-pip/devcontainer.json b/.devcontainer/cuda12.5-pip/devcontainer.json
similarity index 81%
rename from .devcontainer/cuda12.2-pip/devcontainer.json
rename to .devcontainer/cuda12.5-pip/devcontainer.json
index 44e1fae2a8..b4996728db 100644
--- a/.devcontainer/cuda12.2-pip/devcontainer.json
+++ b/.devcontainer/cuda12.5-pip/devcontainer.json
@@ -3,19 +3,24 @@
     "context": "${localWorkspaceFolder}/.devcontainer",
     "dockerfile": "${localWorkspaceFolder}/.devcontainer/Dockerfile",
     "args": {
-      "CUDA": "12.2",
+      "CUDA": "12.5",
       "PYTHON_PACKAGE_MANAGER": "pip",
-      "BASE": "rapidsai/devcontainers:24.06-cpp-cuda12.2-ubuntu22.04"
+      "BASE": "rapidsai/devcontainers:24.08-cpp-cuda12.5-ubuntu22.04"
     }
   },
+  "runArgs": [
+    "--rm",
+    "--name",
+    "${localEnv:USER:anon}-rapids-${localWorkspaceFolderBasename}-24.08-cuda12.5-pip"
+  ],
   "hostRequirements": {"gpu": "optional"},
   "features": {
-    "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils:24.6": {}
+    "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils:24.8": {}
   },
   "overrideFeatureInstallOrder": [
     "ghcr.io/rapidsai/devcontainers/features/rapids-build-utils"
   ],
-  "initializeCommand": ["/bin/bash", "-c", "mkdir -m 0755 -p ${localWorkspaceFolder}/../.{aws,cache,config/pip,local/share/${localWorkspaceFolderBasename}-cuda12.2-venvs}"],
+  "initializeCommand": ["/bin/bash", "-c", "mkdir -m 0755 -p ${localWorkspaceFolder}/../.{aws,cache,config/pip,local/share/${localWorkspaceFolderBasename}-cuda12.5-venvs}"],
   "postAttachCommand": ["/bin/bash", "-c", "if [ ${CODESPACES:-false} = 'true' ]; then . devcontainer-utils-post-attach-command; . rapids-post-attach-command; fi"],
   "workspaceFolder": "/home/coder",
   "workspaceMount": "source=${localWorkspaceFolder},target=/home/coder/kvikio,type=bind,consistency=consistent",
@@ -23,7 +28,7 @@
     "source=${localWorkspaceFolder}/../.aws,target=/home/coder/.aws,type=bind,consistency=consistent",
     "source=${localWorkspaceFolder}/../.cache,target=/home/coder/.cache,type=bind,consistency=consistent",
     "source=${localWorkspaceFolder}/../.config,target=/home/coder/.config,type=bind,consistency=consistent",
-    "source=${localWorkspaceFolder}/../.local/share/${localWorkspaceFolderBasename}-cuda12.2-venvs,target=/home/coder/.local/share/venvs,type=bind,consistency=consistent"
+    "source=${localWorkspaceFolder}/../.local/share/${localWorkspaceFolderBasename}-cuda12.5-venvs,target=/home/coder/.local/share/venvs,type=bind,consistency=consistent"
   ],
   "customizations": {
     "vscode": {
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index d17e14bd92..b237a67888 100755
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -10,7 +10,14 @@ python/            @rapidsai/kvikio-python-codeowners
 build.sh           @rapidsai/kvikio-cmake-codeowners
 python/setup.py    @rapidsai/kvikio-cmake-codeowners @rapidsai/kvikio-python-codeowners
 
-#build/ops code owners
-.github/           @rapidsai/ops-codeowners
-ci/                @rapidsai/ops-codeowners
-conda/             @rapidsai/ops-codeowners
+#CI code owners
+/.github/                @rapidsai/ci-codeowners
+/ci/                     @rapidsai/ci-codeowners
+/.pre-commit-config.yaml @rapidsai/ci-codeowners
+
+#packaging code owners
+/.devcontainer/    @rapidsai/packaging-codeowners
+/conda/            @rapidsai/packaging-codeowners
+/dependencies.yaml @rapidsai/packaging-codeowners
+/build.sh          @rapidsai/packaging-codeowners
+pyproject.toml     @rapidsai/packaging-codeowners
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 4e6cca8727..7b826f0cb4 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -28,7 +28,7 @@ concurrency:
 jobs:
   cpp-build:
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@branch-24.06
+    uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@branch-24.08
     with:
       build_type: ${{ inputs.build_type || 'branch' }}
       branch: ${{ inputs.branch }}
@@ -37,7 +37,7 @@ jobs:
   python-build:
     needs: [cpp-build]
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@branch-24.06
+    uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@branch-24.08
     with:
       build_type: ${{ inputs.build_type || 'branch' }}
       branch: ${{ inputs.branch }}
@@ -46,7 +46,7 @@ jobs:
   upload-conda:
     needs: [cpp-build, python-build]
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/conda-upload-packages.yaml@branch-24.06
+    uses: rapidsai/shared-workflows/.github/workflows/conda-upload-packages.yaml@branch-24.08
     with:
       build_type: ${{ inputs.build_type || 'branch' }}
       branch: ${{ inputs.branch }}
@@ -56,7 +56,7 @@ jobs:
     if: github.ref_type == 'branch'
     needs: python-build
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.06
+    uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.08
     with:
       arch: "amd64"
       branch: ${{ inputs.branch }}
@@ -66,3 +66,45 @@ jobs:
       node_type: "gpu-v100-latest-1"
       run_script: "ci/build_docs.sh"
       sha: ${{ inputs.sha }}
+  wheel-build-cpp:
+    secrets: inherit
+    uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.08
+    with:
+      matrix_filter: group_by([.ARCH, (.CUDA_VER|split(".")|map(tonumber)|.[0])]) | map(max_by(.PY_VER|split(".")|map(tonumber)))
+      build_type: ${{ inputs.build_type || 'branch' }}
+      branch: ${{ inputs.branch }}
+      sha: ${{ inputs.sha }}
+      date: ${{ inputs.date }}
+      script: ci/build_wheel_cpp.sh
+  wheel-build-python:
+    needs: wheel-build-cpp
+    secrets: inherit
+    uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.08
+    with:
+      build_type: ${{ inputs.build_type || 'branch' }}
+      branch: ${{ inputs.branch }}
+      sha: ${{ inputs.sha }}
+      date: ${{ inputs.date }}
+      script: ci/build_wheel_python.sh
+  wheel-publish-cpp:
+    needs: wheel-build-cpp
+    secrets: inherit
+    uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.08
+    with:
+      build_type: ${{ inputs.build_type || 'branch' }}
+      branch: ${{ inputs.branch }}
+      sha: ${{ inputs.sha }}
+      date: ${{ inputs.date }}
+      package-name: libkvikio
+      package-type: cpp
+  wheel-publish-python:
+    needs: wheel-build-python
+    secrets: inherit
+    uses: rapidsai/shared-workflows/.github/workflows/wheels-publish.yaml@branch-24.08
+    with:
+      build_type: ${{ inputs.build_type || 'branch' }}
+      branch: ${{ inputs.branch }}
+      sha: ${{ inputs.sha }}
+      date: ${{ inputs.date }}
+      package-name: kvikio
+      package-type: python
diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml
index 2e38cf9245..f4dc586470 100644
--- a/.github/workflows/pr.yaml
+++ b/.github/workflows/pr.yaml
@@ -19,39 +19,42 @@ jobs:
       - conda-python-tests
       - docs-build
       - devcontainer
+      - wheel-cpp-build
+      - wheel-python-build
+      - wheel-python-tests
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/pr-builder.yaml@branch-24.06
+    uses: rapidsai/shared-workflows/.github/workflows/pr-builder.yaml@branch-24.08
   checks:
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/checks.yaml@branch-24.06
+    uses: rapidsai/shared-workflows/.github/workflows/checks.yaml@branch-24.08
   conda-cpp-build:
     needs: checks
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@branch-24.06
+    uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-build.yaml@branch-24.08
     with:
       build_type: pull-request
   conda-cpp-tests:
     needs: conda-cpp-build
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@branch-24.06
+    uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@branch-24.08
     with:
       build_type: pull-request
   conda-python-build:
     needs: conda-cpp-build
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@branch-24.06
+    uses: rapidsai/shared-workflows/.github/workflows/conda-python-build.yaml@branch-24.08
     with:
       build_type: pull-request
   conda-python-tests:
     needs: conda-python-build
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.06
+    uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.08
     with:
       build_type: pull-request
   docs-build:
     needs: conda-python-build
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.06
+    uses: rapidsai/shared-workflows/.github/workflows/custom-job.yaml@branch-24.08
     with:
       build_type: pull-request
       node_type: "gpu-v100-latest-1"
@@ -60,11 +63,32 @@ jobs:
       run_script: "ci/build_docs.sh"
   devcontainer:
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/build-in-devcontainer.yaml@branch-24.06
+    uses: rapidsai/shared-workflows/.github/workflows/build-in-devcontainer.yaml@branch-24.08
     with:
       arch: '["amd64"]'
-      cuda: '["12.2"]'
+      cuda: '["12.5"]'
       build_command: |
         sccache -z;
         build-all --verbose;
         sccache -s;
+  wheel-cpp-build:
+    secrets: inherit
+    uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.08
+    with:
+      matrix_filter: group_by([.ARCH, (.CUDA_VER|split(".")|map(tonumber)|.[0])]) | map(max_by(.PY_VER|split(".")|map(tonumber)))
+      build_type: pull-request
+      script: ci/build_wheel_cpp.sh
+  wheel-python-build:
+    needs: wheel-cpp-build
+    secrets: inherit
+    uses: rapidsai/shared-workflows/.github/workflows/wheels-build.yaml@branch-24.08
+    with:
+      build_type: pull-request
+      script: ci/build_wheel_python.sh
+  wheel-python-tests:
+    needs: wheel-python-build
+    secrets: inherit
+    uses: rapidsai/shared-workflows/.github/workflows/wheels-test.yaml@branch-24.08
+    with:
+      build_type: pull-request
+      script: ci/test_wheel.sh
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index be56a05348..f794394103 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -18,7 +18,7 @@ repos:
     hooks:
       - id: black
         files: python/.*
-        args: ["--config", "python/pyproject.toml"]
+        args: ["--config", "python/kvikio/pyproject.toml"]
   - repo: https://github.com/PyCQA/flake8
     rev: 6.0.0
     hooks:
@@ -37,11 +37,11 @@ repos:
     hooks:
       - id: mypy
         additional_dependencies: [types-cachetools]
-        args: ["--config-file=python/pyproject.toml",
-                "python/kvikio",
-                "python/tests",
-                "python/examples",
-                "python/benchmarks"]
+        args: ["--config-file=python/kvikio/pyproject.toml",
+                "python/kvikio/kvikio",
+                "python/kvikio/tests",
+                "python/kvikio/examples"
+              ]
         pass_filenames: false
   - repo: https://github.com/pre-commit/mirrors-clang-format
     rev: v16.0.6
@@ -81,13 +81,8 @@ repos:
           (?x)^(
             ^CHANGELOG.md$
           )
-  - repo: https://github.com/rapidsai/dependency-file-generator
-    rev: v1.8.0
-    hooks:
-      - id: rapids-dependency-file-generator
-        args: ["--clean"]
   - repo: https://github.com/rapidsai/pre-commit-hooks
-    rev: v0.0.3
+    rev: v0.2.0
     hooks:
       - id: verify-copyright
         files: |
@@ -95,6 +90,12 @@ repos:
               [.](cmake|cpp|cu|cuh|h|hpp|sh|pxd|py|pyx)$|
               CMakeLists[.]txt$|
               meta[.]yaml$
+      - id: verify-alpha-spec
+  - repo: https://github.com/rapidsai/dependency-file-generator
+    rev: v1.13.11
+    hooks:
+      - id: rapids-dependency-file-generator
+        args: ["--clean"]
 
 default_language_version:
       python: python3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index be1064ccb5..e86398d239 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,36 @@
+# kvikio 24.08.00 (7 Aug 2024)
+
+## 🐛 Bug Fixes
+
+- fix compat_mode check typo ([#421](https://github.com/rapidsai/kvikio/pull/421)) [@madsbk](https://github.com/madsbk)
+- Fix `O_DIRECT` in `defined` ([#416](https://github.com/rapidsai/kvikio/pull/416)) [@jakirkham](https://github.com/jakirkham)
+- cufile-wrapper: include the cuda-wrapper ([#413](https://github.com/rapidsai/kvikio/pull/413)) [@madsbk](https://github.com/madsbk)
+- Rename `.devcontainer`s for CUDA 12.5 ([#406](https://github.com/rapidsai/kvikio/pull/406)) [@jakirkham](https://github.com/jakirkham)
+- Use `#ifdef` to guard `O_DIRECT` usage ([#404](https://github.com/rapidsai/kvikio/pull/404)) [@jakirkham](https://github.com/jakirkham)
+- Properly quote items in __all__ ([#399](https://github.com/rapidsai/kvikio/pull/399)) [@KyleFromNVIDIA](https://github.com/KyleFromNVIDIA)
+
+## 🛠️ Improvements
+
+- Benchmarks clean up ([#424](https://github.com/rapidsai/kvikio/pull/424)) [@madsbk](https://github.com/madsbk)
+- split up CUDA-suffixed dependencies in dependencies.yaml ([#411](https://github.com/rapidsai/kvikio/pull/411)) [@jameslamb](https://github.com/jameslamb)
+- Don&#39;t open with `O_DIRECT` when in compat mode ([#410](https://github.com/rapidsai/kvikio/pull/410)) [@madsbk](https://github.com/madsbk)
+- Fetch and use BS thread_pool ([#408](https://github.com/rapidsai/kvikio/pull/408)) [@madsbk](https://github.com/madsbk)
+- Use workflow branch 24.08 again ([#407](https://github.com/rapidsai/kvikio/pull/407)) [@KyleFromNVIDIA](https://github.com/KyleFromNVIDIA)
+- Enable cuFile on ARM ([#405](https://github.com/rapidsai/kvikio/pull/405)) [@jakirkham](https://github.com/jakirkham)
+- Build and test with CUDA 12.5.1 ([#401](https://github.com/rapidsai/kvikio/pull/401)) [@KyleFromNVIDIA](https://github.com/KyleFromNVIDIA)
+- skip CMake 3.30.0 ([#400](https://github.com/rapidsai/kvikio/pull/400)) [@jameslamb](https://github.com/jameslamb)
+- Use verify-alpha-spec hook ([#398](https://github.com/rapidsai/kvikio/pull/398)) [@KyleFromNVIDIA](https://github.com/KyleFromNVIDIA)
+- Avoid --find-links in wheel jobs ([#397](https://github.com/rapidsai/kvikio/pull/397)) [@jameslamb](https://github.com/jameslamb)
+- Adding NVTX annotations ([#395](https://github.com/rapidsai/kvikio/pull/395)) [@madsbk](https://github.com/madsbk)
+- Clean up error handling ([#394](https://github.com/rapidsai/kvikio/pull/394)) [@madsbk](https://github.com/madsbk)
+- Move content of the Python pkg to subdirectory python/kvikio/ ([#392](https://github.com/rapidsai/kvikio/pull/392)) [@hcho3](https://github.com/hcho3)
+- Adopt CI/packaging codeowners ([#391](https://github.com/rapidsai/kvikio/pull/391)) [@bdice](https://github.com/bdice)
+- Remove text builds of documentation ([#390](https://github.com/rapidsai/kvikio/pull/390)) [@vyasr](https://github.com/vyasr)
+- use rapids-build-backend ([#389](https://github.com/rapidsai/kvikio/pull/389)) [@jameslamb](https://github.com/jameslamb)
+- remove unnecessary &#39;setuptools&#39; dependency ([#385](https://github.com/rapidsai/kvikio/pull/385)) [@jameslamb](https://github.com/jameslamb)
+- add --rm and --name to devcontainer run args ([#370](https://github.com/rapidsai/kvikio/pull/370)) [@trxcllnt](https://github.com/trxcllnt)
+- add wheel output ([#369](https://github.com/rapidsai/kvikio/pull/369)) [@msarahan](https://github.com/msarahan)
+
 # kvikio 24.06.00 (5 Jun 2024)
 
 ## 🐛 Bug Fixes
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 44babdddd8..5dbbcb0ade 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -23,61 +23,6 @@ More information can be found at: [Contributor Code of Conduct](https://docs.rap
 
 ## Code contributions
 
-### Requirements
-
-To install users should have a working Linux machine with CUDA Toolkit
-installed (v11.4+) and a working compiler toolchain (C++17 and cmake).
-
-#### C++
-
-The C++ bindings are header-only and depends on CUDA Driver and Runtime API.
-In order to build and run the example code, CMake is required.
-
-#### Python
-
-The Python packages depends on the following packages:
-
-* Cython
-* Pip
-* Setuptools
-
-For testing:
-* pytest
-* cupy
-
-### Build KvikIO from source
-
-#### C++
-To build the C++ example, go to the `cpp` subdiretory and run:
-```
-mkdir build
-cd build
-cmake ..
-make
-```
-Then run the example:
-```
-./examples/basic_io
-```
-
-#### Python
-
-To build and install the extension, go to the `python` subdiretory and run:
-```
-python -m pip install .
-```
-One might have to define `CUDA_HOME` to the path to the CUDA installation.
-
-In order to test the installation, run the following:
-```
-pytest tests/
-```
-
-And to test performance, run the following:
-```
-python benchmarks/single-node-io.py
-```
-
 ### Code Formatting
 
 #### Using pre-commit hooks
diff --git a/VERSION b/VERSION
index 0bff6981a3..ec8489fda9 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-24.06.00
+24.08.00
diff --git a/build.sh b/build.sh
index fc4b1d6df1..ca9de72a49 100755
--- a/build.sh
+++ b/build.sh
@@ -151,7 +151,7 @@ fi
 # Build and install the kvikio Python package
 if (( NUMARGS == 0 )) || hasArg kvikio; then
     echo "building kvikio..."
-    cd ${REPODIR}/python
+    cd ${REPODIR}/python/kvikio
     SKBUILD_CMAKE_ARGS="-DCMAKE_PREFIX_PATH=${INSTALL_PREFIX};-DCMAKE_LIBRARY_PATH=${LIBKVIKIO_BUILD_DIR};${EXTRA_CMAKE_ARGS}" \
-        python -m pip install --no-build-isolation --no-deps .
+        python -m pip install --no-build-isolation --no-deps --config-settings rapidsai.disable-cuda=true .
 fi
diff --git a/ci/build_docs.sh b/ci/build_docs.sh
index 7bd14e4973..af1e23ae3c 100755
--- a/ci/build_docs.sh
+++ b/ci/build_docs.sh
@@ -8,7 +8,7 @@ rapids-logger "Create test conda environment"
 
 rapids-dependency-file-generator \
   --output conda \
-  --file_key docs \
+  --file-key docs \
   --matrix "cuda=${RAPIDS_CUDA_VERSION%.*};arch=$(arch);py=${RAPIDS_PY_VERSION}" | tee env.yaml
 
 rapids-mamba-retry env create --yes -f env.yaml -n docs
@@ -40,10 +40,8 @@ popd
 rapids-logger "Build Python docs"
 pushd docs
 sphinx-build -b dirhtml source _html -W
-sphinx-build -b text source _text -W
-mkdir -p "${RAPIDS_DOCS_DIR}/kvikio/"{html,txt}
+mkdir -p "${RAPIDS_DOCS_DIR}/kvikio/"html
 mv _html/* "${RAPIDS_DOCS_DIR}/kvikio/html"
-mv _text/* "${RAPIDS_DOCS_DIR}/kvikio/txt"
 popd
 
 rapids-upload-docs
diff --git a/ci/build_wheel_cpp.sh b/ci/build_wheel_cpp.sh
new file mode 100755
index 0000000000..9893474da3
--- /dev/null
+++ b/ci/build_wheel_cpp.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+# Copyright (c) 2023-2024, NVIDIA CORPORATION.
+
+set -euo pipefail
+
+package_name="libkvikio"
+package_dir="python/libkvikio"
+
+source rapids-configure-sccache
+source rapids-date-string
+
+rapids-generate-version > ./VERSION
+
+cd "${package_dir}"
+
+python -m pip install wheel
+# libkvikio is a header-only C++ library with no Python code, so
+# it is entirely platform-agnostic. We cannot use auditwheel for
+# retagging since it has no extension modules, so we use `wheel`
+# directly instead.
+python -m pip wheel . -w dist -vvv --no-deps --disable-pip-version-check
+python -m wheel tags --platform any dist/* --remove
+
+RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen ${RAPIDS_CUDA_VERSION})"
+RAPIDS_PY_WHEEL_NAME="${package_name}_${RAPIDS_PY_CUDA_SUFFIX}" rapids-upload-wheels-to-s3 cpp dist
diff --git a/ci/build_wheel_python.sh b/ci/build_wheel_python.sh
new file mode 100755
index 0000000000..a75ef5b08b
--- /dev/null
+++ b/ci/build_wheel_python.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+# Copyright (c) 2023-2024, NVIDIA CORPORATION.
+
+set -euo pipefail
+
+package_name="kvikio"
+package_dir="python/kvikio"
+
+source rapids-configure-sccache
+source rapids-date-string
+
+RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen ${RAPIDS_CUDA_VERSION})"
+
+rapids-generate-version > ./VERSION
+
+CPP_WHEELHOUSE=$(RAPIDS_PY_WHEEL_NAME="libkvikio_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-from-s3 cpp /tmp/libkvikio_dist)
+
+cd "${package_dir}"
+
+# ensure 'kvikio' wheel builds always use the 'libkvikio' just built in the same CI run
+#
+# using env variable PIP_CONSTRAINT is necessary to ensure the constraints
+# are used when creating the isolated build environment
+echo "libkvikio-${RAPIDS_PY_CUDA_SUFFIX} @ file://$(echo ${CPP_WHEELHOUSE}/libkvikio_*.whl)" > ./constraints.txt
+
+PIP_CONSTRAINT="${PWD}/constraints.txt" \
+    python -m pip wheel . -w dist -vvv --no-deps --disable-pip-version-check
+
+mkdir -p final_dist
+python -m auditwheel repair -w final_dist dist/*
+
+RAPIDS_PY_WHEEL_NAME="${package_name}_${RAPIDS_PY_CUDA_SUFFIX}" rapids-upload-wheels-to-s3 final_dist
diff --git a/ci/check_style.sh b/ci/check_style.sh
index d154742114..a17065c1f9 100755
--- a/ci/check_style.sh
+++ b/ci/check_style.sh
@@ -8,7 +8,7 @@ rapids-logger "Create checks conda environment"
 
 rapids-dependency-file-generator \
   --output conda \
-  --file_key checks \
+  --file-key checks \
   --matrix "cuda=${RAPIDS_CUDA_VERSION%.*};arch=$(arch);py=${RAPIDS_PY_VERSION}" | tee env.yaml
 
 rapids-mamba-retry env create --yes -f env.yaml -n checks
diff --git a/ci/release/update-version.sh b/ci/release/update-version.sh
index c3042dc06b..585c9d7697 100755
--- a/ci/release/update-version.sh
+++ b/ci/release/update-version.sh
@@ -36,6 +36,18 @@ function sed_runner() {
 # Centralized version file update
 echo "${NEXT_FULL_TAG}" > VERSION
 
+DEPENDENCIES=(
+  libkvikio
+)
+for DEP in "${DEPENDENCIES[@]}"; do
+  for FILE in dependencies.yaml conda/environments/*.yaml; do
+    sed_runner "/-.* ${DEP}\(-cu[[:digit:]]\{2\}\)\{0,1\}==/ s/==.*/==${NEXT_SHORT_TAG_PEP440}.*,>=0.0.0a0/g" "${FILE}"
+  done
+  for FILE in python/*/pyproject.toml; do
+    sed_runner "/\"${DEP}==/ s/==.*\"/==${NEXT_SHORT_TAG_PEP440}.*,>=0.0.0a0\"/g" ${FILE}
+  done
+done
+
 # CI files
 for FILE in .github/workflows/*.yaml; do
   sed_runner "/shared-workflows/ s/@.*/@branch-${NEXT_SHORT_TAG}/g" "${FILE}"
@@ -45,6 +57,7 @@ done
 find .devcontainer/ -type f -name devcontainer.json -print0 | while IFS= read -r -d '' filename; do
     sed_runner "s@rapidsai/devcontainers:[0-9.]*@rapidsai/devcontainers:${NEXT_SHORT_TAG}@g" "${filename}"
     sed_runner "s@rapidsai/devcontainers/features/rapids-build-utils:[0-9.]*@rapidsai/devcontainers/features/rapids-build-utils:${NEXT_SHORT_TAG_PEP440}@" "${filename}"
+    sed_runner "s@rapids-\${localWorkspaceFolderBasename}-[0-9.]*@rapids-\${localWorkspaceFolderBasename}-${NEXT_SHORT_TAG}@g" "${filename}"
 done
 
 # The example of a downstream project
diff --git a/conda/environments/all_cuda-118_arch-aarch64.yaml b/conda/environments/all_cuda-118_arch-aarch64.yaml
new file mode 100644
index 0000000000..6e2e085874
--- /dev/null
+++ b/conda/environments/all_cuda-118_arch-aarch64.yaml
@@ -0,0 +1,38 @@
+# This file is generated by `rapids-dependency-file-generator`.
+# To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`.
+channels:
+- rapidsai
+- rapidsai-nightly
+- conda-forge
+- nvidia
+dependencies:
+- c-compiler
+- cmake>=3.26.4,!=3.30.0
+- cuda-python>=11.7.1,<12.0a0
+- cuda-version=11.8
+- cudatoolkit
+- cupy>=12.0.0
+- cxx-compiler
+- cython>=3.0.0
+- dask>=2022.05.2
+- doxygen=1.9.1
+- gcc_linux-aarch64=11.*
+- ninja
+- numcodecs <0.12.0
+- numpy>=1.23,<2.0a0
+- numpydoc
+- nvcc_linux-aarch64=11.8
+- nvcomp==3.0.6
+- packaging
+- pre-commit
+- pytest
+- pytest-cov
+- python>=3.9,<3.12
+- rapids-build-backend>=0.3.0,<0.4.0.dev0
+- scikit-build-core>=0.7.0
+- sphinx
+- sphinx-click
+- sphinx_rtd_theme
+- sysroot_linux-aarch64=2.17
+- zarr
+name: all_cuda-118_arch-aarch64
diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml
index 03c84b29a7..1cbc7c0a80 100644
--- a/conda/environments/all_cuda-118_arch-x86_64.yaml
+++ b/conda/environments/all_cuda-118_arch-x86_64.yaml
@@ -7,7 +7,7 @@ channels:
 - nvidia
 dependencies:
 - c-compiler
-- cmake>=3.26.4
+- cmake>=3.26.4,!=3.30.0
 - cuda-python>=11.7.1,<12.0a0
 - cuda-version=11.8
 - cudatoolkit
@@ -30,6 +30,7 @@ dependencies:
 - pytest
 - pytest-cov
 - python>=3.9,<3.12
+- rapids-build-backend>=0.3.0,<0.4.0.dev0
 - scikit-build-core>=0.7.0
 - sphinx
 - sphinx-click
diff --git a/conda/environments/all_cuda-125_arch-aarch64.yaml b/conda/environments/all_cuda-125_arch-aarch64.yaml
new file mode 100644
index 0000000000..4fcd7aff02
--- /dev/null
+++ b/conda/environments/all_cuda-125_arch-aarch64.yaml
@@ -0,0 +1,38 @@
+# This file is generated by `rapids-dependency-file-generator`.
+# To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`.
+channels:
+- rapidsai
+- rapidsai-nightly
+- conda-forge
+- nvidia
+dependencies:
+- c-compiler
+- cmake>=3.26.4,!=3.30.0
+- cuda-nvcc
+- cuda-python>=12.0,<13.0a0
+- cuda-version=12.5
+- cupy>=12.0.0
+- cxx-compiler
+- cython>=3.0.0
+- dask>=2022.05.2
+- doxygen=1.9.1
+- gcc_linux-aarch64=11.*
+- libcufile-dev
+- ninja
+- numcodecs <0.12.0
+- numpy>=1.23,<2.0a0
+- numpydoc
+- nvcomp==3.0.6
+- packaging
+- pre-commit
+- pytest
+- pytest-cov
+- python>=3.9,<3.12
+- rapids-build-backend>=0.3.0,<0.4.0.dev0
+- scikit-build-core>=0.7.0
+- sphinx
+- sphinx-click
+- sphinx_rtd_theme
+- sysroot_linux-aarch64=2.17
+- zarr
+name: all_cuda-125_arch-aarch64
diff --git a/conda/environments/all_cuda-122_arch-x86_64.yaml b/conda/environments/all_cuda-125_arch-x86_64.yaml
similarity index 84%
rename from conda/environments/all_cuda-122_arch-x86_64.yaml
rename to conda/environments/all_cuda-125_arch-x86_64.yaml
index e1a8f31c5b..d19547dc34 100644
--- a/conda/environments/all_cuda-122_arch-x86_64.yaml
+++ b/conda/environments/all_cuda-125_arch-x86_64.yaml
@@ -7,10 +7,10 @@ channels:
 - nvidia
 dependencies:
 - c-compiler
-- cmake>=3.26.4
+- cmake>=3.26.4,!=3.30.0
 - cuda-nvcc
 - cuda-python>=12.0,<13.0a0
-- cuda-version=12.2
+- cuda-version=12.5
 - cupy>=12.0.0
 - cxx-compiler
 - cython>=3.0.0
@@ -28,10 +28,11 @@ dependencies:
 - pytest
 - pytest-cov
 - python>=3.9,<3.12
+- rapids-build-backend>=0.3.0,<0.4.0.dev0
 - scikit-build-core>=0.7.0
 - sphinx
 - sphinx-click
 - sphinx_rtd_theme
 - sysroot_linux-64=2.17
 - zarr
-name: all_cuda-122_arch-x86_64
+name: all_cuda-125_arch-x86_64
diff --git a/conda/recipes/kvikio/conda_build_config.yaml b/conda/recipes/kvikio/conda_build_config.yaml
index e2c908c9f3..4055133583 100644
--- a/conda/recipes/kvikio/conda_build_config.yaml
+++ b/conda/recipes/kvikio/conda_build_config.yaml
@@ -17,7 +17,7 @@ c_stdlib_version:
   - "2.17"
 
 cmake_version:
-  - ">=3.26.4"
+  - ">=3.26.4,!=3.30.0"
 
 nvcomp_version:
   - "=3.0.6"
diff --git a/conda/recipes/kvikio/meta.yaml b/conda/recipes/kvikio/meta.yaml
index 78e844aff4..2f2e308ae7 100644
--- a/conda/recipes/kvikio/meta.yaml
+++ b/conda/recipes/kvikio/meta.yaml
@@ -54,7 +54,6 @@ requirements:
     - {{ stdlib("c") }}
   host:
     - python
-    - setuptools
     - pip
     - cython >=3.0.0
     {% if cuda_major == "11" %}
@@ -62,6 +61,7 @@ requirements:
     {% endif %}
     - cuda-version ={{ cuda_version }}
     - nvcomp {{ nvcomp_version }}
+    - rapids-build-backend >=0.3.0,<0.4.0.dev0
     - scikit-build-core >=0.7.0
     - libkvikio ={{ version }}
   run:
diff --git a/conda/recipes/libkvikio/conda_build_config.yaml b/conda/recipes/libkvikio/conda_build_config.yaml
index 33fc5a7f7a..9cf2923599 100644
--- a/conda/recipes/libkvikio/conda_build_config.yaml
+++ b/conda/recipes/libkvikio/conda_build_config.yaml
@@ -5,7 +5,7 @@ cxx_compiler_version:
   - 11
 
 cmake_version:
-  - ">=3.26.4"
+  - ">=3.26.4,!=3.30.0"
 
 cuda_compiler:
   - cuda-nvcc
diff --git a/conda/recipes/libkvikio/meta.yaml b/conda/recipes/libkvikio/meta.yaml
index c1253dc26c..186c373f56 100644
--- a/conda/recipes/libkvikio/meta.yaml
+++ b/conda/recipes/libkvikio/meta.yaml
@@ -50,7 +50,7 @@ requirements:
     - libcufile {{ cuda11_libcufile_host_version }}      # [linux64]
     - libcufile-dev {{ cuda11_libcufile_host_version }}  # [linux64]
     {% else %}
-    - libcufile-dev  # [linux64]
+    - libcufile-dev  # [linux]
     {% endif %}
 
 outputs:
@@ -67,7 +67,7 @@ outputs:
         - {{ compiler('cuda11') }}
         {% else %}
         - {{ compiler('cuda') }}
-        - libcufile-dev  # [linux64]
+        - libcufile-dev  # [linux]
         {% endif %}
     requirements:
       build:
@@ -81,7 +81,7 @@ outputs:
         - libcufile {{ cuda11_libcufile_run_version }}      # [linux64]
         - libcufile-dev {{ cuda11_libcufile_run_version }}  # [linux64]
         {% else %}
-        - libcufile-dev  # [linux64]
+        - libcufile-dev  # [linux]
         {% endif %}
     test:
         commands:
@@ -104,7 +104,7 @@ outputs:
         {% else %}
         - {{ compiler('cuda') }}
         - cuda-cudart-dev
-        - libcufile-dev  # [linux64]
+        - libcufile-dev  # [linux]
         {% endif %}
     requirements:
       build:
@@ -116,7 +116,7 @@ outputs:
         - libcufile {{ cuda11_libcufile_run_version }}  # [linux64]
         {% else %}
         - cuda-cudart-dev
-        - libcufile-dev  # [linux64]
+        - libcufile-dev  # [linux]
         {% endif %}
       run:
         - {{ pin_compatible('cuda-version', max_pin='x', min_pin='x') }}
@@ -125,7 +125,7 @@ outputs:
         - libcufile {{ cuda11_libcufile_run_version }}  # [linux64]
         {% else %}
         - cuda-cudart
-        - libcufile  # [linux64]
+        - libcufile  # [linux]
         {% endif %}
     about:
       home: https://rapids.ai
diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt
index b1ac294d7a..f4f3f13109 100644
--- a/cpp/CMakeLists.txt
+++ b/cpp/CMakeLists.txt
@@ -80,6 +80,9 @@ else()
   message(STATUS "Found cuFile Stream API: ${cuFile_STREAM_API_FOUND}")
 endif()
 
+include(cmake/thirdparty/get_nvtx.cmake)
+include(cmake/thirdparty/get_thread_pool.cmake)
+
 # library targets
 add_library(kvikio INTERFACE)
 add_library(kvikio::kvikio ALIAS kvikio)
@@ -95,7 +98,11 @@ add_library(kvikio::kvikio ALIAS kvikio)
 
 # Enable CUDA in KvikIO
 if(CUDAToolkit_FOUND)
-  target_link_libraries(kvikio INTERFACE $<BUILD_LOCAL_INTERFACE:CUDA::toolkit>)
+  if(CUDA_STATIC_RUNTIME)
+    target_link_libraries(kvikio INTERFACE $<BUILD_LOCAL_INTERFACE:CUDA::cudart_static>)
+  else()
+    target_link_libraries(kvikio INTERFACE $<BUILD_LOCAL_INTERFACE:CUDA::cudart>)
+  endif()
   target_compile_definitions(kvikio INTERFACE $<BUILD_LOCAL_INTERFACE:KVIKIO_CUDA_FOUND>)
 else()
   message(WARNING "Building KvikIO without CUDA")
@@ -121,9 +128,9 @@ target_include_directories(
   kvikio INTERFACE "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
                    "$<INSTALL_INTERFACE:include>"
 )
-
-target_link_libraries(kvikio INTERFACE Threads::Threads)
-target_link_libraries(kvikio INTERFACE ${CMAKE_DL_LIBS})
+target_link_libraries(
+  kvikio INTERFACE Threads::Threads ${CMAKE_DL_LIBS} nvtx3::nvtx3-cpp BS::thread_pool
+)
 target_compile_features(kvikio INTERFACE cxx_std_17)
 
 # optionally build examples
@@ -167,7 +174,11 @@ if(NOT already_set_kvikio)
 
   find_package(CUDAToolkit QUIET)
   if(CUDAToolkit_FOUND)
-    target_link_libraries(kvikio::kvikio INTERFACE CUDA::toolkit)
+    if(CUDA_STATIC_RUNTIME)
+      target_link_libraries(kvikio::kvikio INTERFACE CUDA::cudart_static)
+    else()
+      target_link_libraries(kvikio::kvikio INTERFACE CUDA::cudart)
+    endif()
     target_compile_definitions(kvikio::kvikio INTERFACE KVIKIO_CUDA_FOUND)
   else()
     message(WARNING "Building KvikIO without CUDA")
diff --git a/cpp/cmake/thirdparty/get_nvtx.cmake b/cpp/cmake/thirdparty/get_nvtx.cmake
new file mode 100644
index 0000000000..ee4fc02f69
--- /dev/null
+++ b/cpp/cmake/thirdparty/get_nvtx.cmake
@@ -0,0 +1,25 @@
+# =============================================================================
+# Copyright (c) 2024, NVIDIA CORPORATION.
+#
+# 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.
+# =============================================================================
+
+# Need to call rapids_cpm_nvtx3 to get support for an installed version of nvtx3 and to support
+# installing it ourselves
+function(find_and_configure_nvtx)
+  include(${rapids-cmake-dir}/cpm/nvtx3.cmake)
+
+  # Find or install nvtx3
+  rapids_cpm_nvtx3(BUILD_EXPORT_SET kvikio-exports INSTALL_EXPORT_SET kvikio-exports)
+
+endfunction()
+
+find_and_configure_nvtx()
diff --git a/cpp/cmake/thirdparty/get_thread_pool.cmake b/cpp/cmake/thirdparty/get_thread_pool.cmake
new file mode 100644
index 0000000000..3faf21366a
--- /dev/null
+++ b/cpp/cmake/thirdparty/get_thread_pool.cmake
@@ -0,0 +1,25 @@
+# =============================================================================
+# Copyright (c) 2024, NVIDIA CORPORATION.
+#
+# 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.
+# =============================================================================
+
+# Need to call rapids_cpm_bs_thread_pool to get support for an installed version of thread-pool and
+# to support installing it ourselves
+function(find_and_configure_thread_pool)
+  include(${rapids-cmake-dir}/cpm/bs_thread_pool.cmake)
+
+  # Find or install thread-pool
+  rapids_cpm_bs_thread_pool(BUILD_EXPORT_SET kvikio-exports INSTALL_EXPORT_SET kvikio-exports)
+
+endfunction()
+
+find_and_configure_thread_pool()
diff --git a/cpp/doxygen/main_page.md b/cpp/doxygen/main_page.md
index 5de65f9fc9..2b404f835e 100644
--- a/cpp/doxygen/main_page.md
+++ b/cpp/doxygen/main_page.md
@@ -37,8 +37,8 @@ Install the **stable release** from the ``rapidsai`` channel with the following:
 mamba install -c rapidsai -c conda-forge libkvikio
 # Create new environment (CUDA 11.8)
 mamba create -n libkvikio-env -c rapidsai -c conda-forge cuda-version=11.8 libkvikio
-# Create new environment (CUDA 12.0)
-mamba create -n libkvikio-env -c rapidsai -c conda-forge cuda-version=12.0 libkvikio
+# Create new environment (CUDA 12.5)
+mamba create -n libkvikio-env -c rapidsai -c conda-forge cuda-version=12.5 libkvikio
 ```
 
 Install the **nightly release** from the ``rapidsai-nightly`` channel with the following:
@@ -48,8 +48,8 @@ Install the **nightly release** from the ``rapidsai-nightly`` channel with the f
 mamba install -c rapidsai-nightly -c conda-forge libkvikio
 # Create new environment (CUDA 11.8)
 mamba create -n libkvikio-env -c rapidsai-nightly -c conda-forge python=3.11 cuda-version=11.8 libkvikio
-# Create new environment (CUDA 12.0)
-mamba create -n libkvikio-env -c rapidsai-nightly -c conda-forge python=3.11 cuda-version=12.0 libkvikio
+# Create new environment (CUDA 12.5)
+mamba create -n libkvikio-env -c rapidsai-nightly -c conda-forge python=3.11 cuda-version=12.5 libkvikio
 ```
 
 ---
diff --git a/cpp/examples/basic_io.cpp b/cpp/examples/basic_io.cpp
index 24aa2440a4..3a4ab892ad 100644
--- a/cpp/examples/basic_io.cpp
+++ b/cpp/examples/basic_io.cpp
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2021-2023, NVIDIA CORPORATION.
+ * Copyright (c) 2021-2024, NVIDIA CORPORATION.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -254,8 +254,8 @@ int main()
     // After synchronizing `stream`, we can read the number of bytes written
     check(cudaStreamSynchronize(stream) == cudaSuccess);
     // Note, `*bytes_done_p` might be negative, which indicate an IO error thus we
-    // use `CUFILE_CHECK_STREAM_IO` to check for errors.
-    CUFILE_CHECK_STREAM_IO(bytes_done_p);
+    // use `CUFILE_CHECK_BYTES_DONE` to check for errors.
+    CUFILE_CHECK_BYTES_DONE(*bytes_done_p);
     check(*bytes_done_p == SIZE);
     cout << "File async write: " << *bytes_done_p << endl;
 
@@ -263,7 +263,7 @@ int main()
     *bytes_done_p = 0;
     f_handle.read_async(c_dev, &io_size, &f_off, &d_off, bytes_done_p, stream);
     check(cudaStreamSynchronize(stream) == cudaSuccess);
-    CUFILE_CHECK_STREAM_IO(bytes_done_p);
+    CUFILE_CHECK_BYTES_DONE(*bytes_done_p);
     check(*bytes_done_p == SIZE);
     cout << "File async read: " << *bytes_done_p << endl;
     check(cudaFreeHost((void*)bytes_done_p) == cudaSuccess);
diff --git a/cpp/include/kvikio/cufile_config.hpp b/cpp/include/kvikio/cufile_config.hpp
new file mode 100644
index 0000000000..bfffb1eb05
--- /dev/null
+++ b/cpp/include/kvikio/cufile_config.hpp
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2024, NVIDIA CORPORATION.
+ *
+ * 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.
+ */
+#pragma once
+
+#include <cstdlib>
+#include <filesystem>
+#include <string>
+
+namespace kvikio {
+namespace detail {
+
+[[nodiscard]] inline const std::string lookup_config_path()
+{
+  const char* env = std::getenv("CUFILE_ENV_PATH_JSON");
+  if (env != nullptr && std::filesystem::exists(env)) { return env; }
+  if (std::filesystem::exists("/etc/cufile.json")) { return "/etc/cufile.json"; }
+  return std::string();
+}
+
+}  // namespace detail
+
+/**
+ * @brief Get the filepath to cuFile's config file (`cufile.json`) or the empty string
+ *
+ * This lookup is cached.
+ *
+ * @return The filepath to the cufile.json file or the empty string if it isn't found.
+ */
+[[nodiscard]] inline const std::string config_path()
+{
+  static const std::string ret = detail::lookup_config_path();
+  return ret;
+}
+
+}  // namespace kvikio
diff --git a/cpp/include/kvikio/defaults.hpp b/cpp/include/kvikio/defaults.hpp
index d2ee6b8d91..c192763ccd 100644
--- a/cpp/include/kvikio/defaults.hpp
+++ b/cpp/include/kvikio/defaults.hpp
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022-2023, NVIDIA CORPORATION.
+ * Copyright (c) 2022-2024, NVIDIA CORPORATION.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -23,8 +23,9 @@
 #include <string>
 #include <utility>
 
+#include <BS_thread_pool.hpp>
+
 #include <kvikio/shim/cufile.hpp>
-#include <kvikio/thread_pool.hpp>
 
 namespace kvikio {
 namespace detail {
@@ -78,7 +79,7 @@ inline bool getenv_or(std::string_view env_var_name, bool default_val)
  */
 class defaults {
  private:
-  kvikio::third_party::thread_pool _thread_pool{get_num_threads_from_env()};
+  BS::thread_pool _thread_pool{get_num_threads_from_env()};
   bool _compat_mode;
   std::size_t _task_size;
   std::size_t _gds_threshold;
@@ -166,10 +167,7 @@ class defaults {
    *
    * @return The the default thread pool instance.
    */
-  [[nodiscard]] static kvikio::third_party::thread_pool& thread_pool()
-  {
-    return instance()->_thread_pool;
-  }
+  [[nodiscard]] static BS::thread_pool& thread_pool() { return instance()->_thread_pool; }
 
   /**
    * @brief Get the number of threads in the default thread pool.
diff --git a/cpp/include/kvikio/error.hpp b/cpp/include/kvikio/error.hpp
index 873bb13e0e..e84ebd770c 100644
--- a/cpp/include/kvikio/error.hpp
+++ b/cpp/include/kvikio/error.hpp
@@ -15,6 +15,7 @@
  */
 #pragma once
 
+#include <cstring>
 #include <exception>
 #include <system_error>
 
@@ -61,37 +62,41 @@ struct CUfileException : public std::runtime_error {
   GET_CUFILE_TRY_MACRO(__VA_ARGS__, CUFILE_TRY_2, CUFILE_TRY_1) \
   (__VA_ARGS__)
 #define GET_CUFILE_TRY_MACRO(_1, _2, NAME, ...) NAME
-#define CUFILE_TRY_2(_call, _exception_type)                                     \
-  do {                                                                           \
-    CUfileError_t const error = (_call);                                         \
-    if (error.err != CU_FILE_SUCCESS) {                                          \
-      if (error.err == CU_FILE_CUDA_DRIVER_ERROR) {                              \
-        CUresult const cuda_error = error.cu_err;                                \
-        CUDA_DRIVER_TRY(cuda_error);                                             \
-      }                                                                          \
-      throw(_exception_type){std::string{"cuFile error at: "} + __FILE__ + ":" + \
-                             KVIKIO_STRINGIFY(__LINE__) + ": " +                 \
-                             cufileop_status_error(error.err)};                  \
-    }                                                                            \
+#define CUFILE_TRY_2(_call, _exception_type)                                             \
+  do {                                                                                   \
+    CUfileError_t const error = (_call);                                                 \
+    if (error.err != CU_FILE_SUCCESS) {                                                  \
+      if (error.err == CU_FILE_CUDA_DRIVER_ERROR) {                                      \
+        CUresult const cuda_error = error.cu_err;                                        \
+        CUDA_DRIVER_TRY(cuda_error);                                                     \
+      }                                                                                  \
+      throw(_exception_type){std::string{"cuFile error at: "} + __FILE__ + ":" +         \
+                             KVIKIO_STRINGIFY(__LINE__) + ": " +                         \
+                             cufileop_status_error((CUfileOpError)std::abs(error.err))}; \
+    }                                                                                    \
   } while (0)
 #define CUFILE_TRY_1(_call) CUFILE_TRY_2(_call, kvikio::CUfileException)
 #endif
 
-#ifndef CUFILE_CHECK_STREAM_IO
-#define CUFILE_CHECK_STREAM_IO(...)                                  \
-  GET_CUFILE_CHECK_STREAM_IO_MACRO(                                  \
-    __VA_ARGS__, CUFILE_CHECK_STREAM_IO_2, CUFILE_CHECK_STREAM_IO_1) \
+#ifndef CUFILE_CHECK_BYTES_DONE
+#define CUFILE_CHECK_BYTES_DONE(...)                                   \
+  GET_CUFILE_CHECK_BYTES_DONE_MACRO(                                   \
+    __VA_ARGS__, CUFILE_CHECK_BYTES_DONE_2, CUFILE_CHECK_BYTES_DONE_1) \
   (__VA_ARGS__)
-#define GET_CUFILE_CHECK_STREAM_IO_MACRO(_1, _2, NAME, ...) NAME
-#define CUFILE_CHECK_STREAM_IO_2(_nbytes_done, _exception_type)                            \
-  do {                                                                                     \
-    auto const _nbytes = *(_nbytes_done);                                                  \
-    if (_nbytes < 0) {                                                                     \
-      throw(_exception_type){std::string{"cuFile error at: "} + __FILE__ + ":" +           \
-                             KVIKIO_STRINGIFY(__LINE__) + ": " + std::to_string(_nbytes)}; \
-    }                                                                                      \
+#define GET_CUFILE_CHECK_BYTES_DONE_MACRO(_1, _2, NAME, ...) NAME
+#define CUFILE_CHECK_BYTES_DONE_2(_nbytes_done, _exception_type)                  \
+  do {                                                                            \
+    auto const _nbytes = (_nbytes_done);                                          \
+    if (_nbytes < 0) {                                                            \
+      auto const err = std::abs(_nbytes);                                         \
+      auto const msg = (err > CUFILEOP_BASE_ERR)                                  \
+                         ? std::string(cufileop_status_error((CUfileOpError)err)) \
+                         : std::string(std::strerror(err));                       \
+      throw(_exception_type){std::string{"cuFile error at: "} + __FILE__ + ":" +  \
+                             KVIKIO_STRINGIFY(__LINE__) + ": " + msg};            \
+    }                                                                             \
   } while (0)
-#define CUFILE_CHECK_STREAM_IO_1(_call) CUFILE_CHECK_STREAM_IO_2(_call, kvikio::CUfileException)
+#define CUFILE_CHECK_BYTES_DONE_1(_call) CUFILE_CHECK_BYTES_DONE_2(_call, kvikio::CUfileException)
 #endif
 
 }  // namespace kvikio
diff --git a/cpp/include/kvikio/file_handle.hpp b/cpp/include/kvikio/file_handle.hpp
index daa717f060..80779c5282 100644
--- a/cpp/include/kvikio/file_handle.hpp
+++ b/cpp/include/kvikio/file_handle.hpp
@@ -25,10 +25,12 @@
 #include <iostream>
 #include <numeric>
 #include <optional>
+#include <stdexcept>
 #include <system_error>
 #include <utility>
 
 #include <kvikio/buffer.hpp>
+#include <kvikio/cufile_config.hpp>
 #include <kvikio/defaults.hpp>
 #include <kvikio/error.hpp>
 #include <kvikio/parallel_operation.hpp>
@@ -46,6 +48,9 @@ namespace detail {
  * @param flags The flags
  * @param o_direct Append O_DIRECT to the open flags
  * @return oflags
+ *
+ * @throw std::invalid_argument if the specified flags are not supported.
+ * @throw std::invalid_argument if `o_direct` is true, but `O_DIRECT` is not supported.
  */
 inline int open_fd_parse_flags(const std::string& flags, bool o_direct)
 {
@@ -65,7 +70,13 @@ inline int open_fd_parse_flags(const std::string& flags, bool o_direct)
     default: throw std::invalid_argument("Unknown file open flag");
   }
   file_flags |= O_CLOEXEC;
-  if (o_direct) { file_flags |= O_DIRECT; }
+  if (o_direct) {
+#if defined(O_DIRECT)
+    file_flags |= O_DIRECT;
+#else
+    throw std::invalid_argument("'o_direct' flag unsupported on this platform");
+#endif
+  }
   return file_flags;
 }
 
@@ -163,12 +174,20 @@ class FileHandle {
       _initialized{true},
       _compat_mode{compat_mode}
   {
+    if (_compat_mode) {
+      return;  // Nothing to do in compatibility mode
+    }
+
+    // Try to open the file with the O_DIRECT flag. Fall back to compatibility mode, if it fails.
     try {
       _fd_direct_on = detail::open_fd(file_path, flags, true, mode);
     } catch (const std::system_error&) {
-      _compat_mode = true;  // Fall back to compat mode if we cannot open the file with O_DIRECT
+      _compat_mode = true;
+    } catch (const std::invalid_argument&) {
+      _compat_mode = true;
     }
 
+    // Create a cuFile handle, if not in compatibility mode
     if (!_compat_mode) {
       CUfileDescr_t desc{};  // It is important to set to zero!
       desc.type = CU_FILE_HANDLE_TYPE_OPAQUE_FD;
@@ -305,15 +324,10 @@ class FileHandle {
     if (_compat_mode) {
       return posix_device_read(_fd_direct_off, devPtr_base, size, file_offset, devPtr_offset);
     }
+    KVIKIO_NVTX_FUNC_RANGE("cufileRead()", size);
     ssize_t ret = cuFileAPI::instance().Read(
       _handle, devPtr_base, size, convert_size2off(file_offset), convert_size2off(devPtr_offset));
-    if (ret == -1) {
-      throw std::system_error(errno, std::generic_category(), "Unable to read file");
-    }
-    if (ret < -1) {
-      throw CUfileException(std::string{"cuFile error at: "} + __FILE__ + ":" +
-                            KVIKIO_STRINGIFY(__LINE__) + ": " + CUFILE_ERRSTR(ret));
-    }
+    CUFILE_CHECK_BYTES_DONE(ret);
     return ret;
   }
 
@@ -351,6 +365,7 @@ class FileHandle {
     if (_compat_mode) {
       return posix_device_write(_fd_direct_off, devPtr_base, size, file_offset, devPtr_offset);
     }
+    KVIKIO_NVTX_FUNC_RANGE("cufileWrite()", size);
     ssize_t ret = cuFileAPI::instance().Write(
       _handle, devPtr_base, size, convert_size2off(file_offset), convert_size2off(devPtr_offset));
     if (ret == -1) {
@@ -526,7 +541,9 @@ class FileHandle {
                   ssize_t* bytes_read_p,
                   CUstream stream)
   {
-    if (kvikio::is_batch_and_stream_available() && !_compat_mode) {
+    // When checking for availability, we also check if cuFile's config file exist. This is because
+    // even when the stream API is available, it doesn't work if no config file exist.
+    if (kvikio::is_batch_and_stream_available() && !_compat_mode && !config_path().empty()) {
       CUFILE_TRY(cuFileAPI::instance().ReadAsync(
         _handle, devPtr_base, size_p, file_offset_p, devPtr_offset_p, bytes_read_p, stream));
       return;
@@ -616,7 +633,9 @@ class FileHandle {
                    ssize_t* bytes_written_p,
                    CUstream stream)
   {
-    if (kvikio::is_batch_and_stream_available() && !_compat_mode) {
+    // When checking for availability, we also check if cuFile's config file exist. This is because
+    // even when the stream API is available, it doesn't work if no config file exist.
+    if (kvikio::is_batch_and_stream_available() && !_compat_mode && !config_path().empty()) {
       CUFILE_TRY(cuFileAPI::instance().WriteAsync(
         _handle, devPtr_base, size_p, file_offset_p, devPtr_offset_p, bytes_written_p, stream));
       return;
diff --git a/cpp/include/kvikio/parallel_operation.hpp b/cpp/include/kvikio/parallel_operation.hpp
index 4312e988dc..f345333c4f 100644
--- a/cpp/include/kvikio/parallel_operation.hpp
+++ b/cpp/include/kvikio/parallel_operation.hpp
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2021-2022, NVIDIA CORPORATION.
+ * Copyright (c) 2021-2024, NVIDIA CORPORATION.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -28,6 +28,18 @@
 
 namespace kvikio {
 
+namespace detail {
+
+template <typename F, typename T>
+std::future<std::size_t> submit_task(
+  F op, T buf, std::size_t size, std::size_t file_offset, std::size_t devPtr_offset)
+{
+  return defaults::thread_pool().submit_task(
+    [=] { return op(buf, size, file_offset, devPtr_offset); });
+}
+
+}  // namespace detail
+
 /**
  * @brief Apply read or write operation in parallel.
  *
@@ -52,7 +64,7 @@ std::future<std::size_t> parallel_io(F op,
 
   // Single-task guard
   if (task_size >= size || page_size >= size) {
-    return defaults::thread_pool().submit(op, buf, size, file_offset, devPtr_offset);
+    return detail::submit_task(op, buf, size, file_offset, devPtr_offset);
   }
 
   // We know an upper bound of the total number of tasks
@@ -61,16 +73,14 @@ std::future<std::size_t> parallel_io(F op,
 
   // 1) Submit `task_size` sized tasks
   while (size >= task_size) {
-    tasks.push_back(defaults::thread_pool().submit(op, buf, task_size, file_offset, devPtr_offset));
+    tasks.push_back(detail::submit_task(op, buf, task_size, file_offset, devPtr_offset));
     file_offset += task_size;
     devPtr_offset += task_size;
     size -= task_size;
   }
 
   // 2) Submit a task for the remainder
-  if (size > 0) {
-    tasks.push_back(defaults::thread_pool().submit(op, buf, size, file_offset, devPtr_offset));
-  }
+  if (size > 0) { tasks.push_back(detail::submit_task(op, buf, size, file_offset, devPtr_offset)); }
 
   // Finally, we sum the result of all tasks.
   auto gather_tasks = [](std::vector<std::future<std::size_t>>&& tasks) -> std::size_t {
diff --git a/cpp/include/kvikio/posix_io.hpp b/cpp/include/kvikio/posix_io.hpp
index 9f2bbd4fd6..cba399a449 100644
--- a/cpp/include/kvikio/posix_io.hpp
+++ b/cpp/include/kvikio/posix_io.hpp
@@ -189,7 +189,7 @@ ssize_t posix_host_io(int fd, const void* buf, size_t count, off_t offset, bool
       const std::string name = IsReadOperation ? "pread" : "pwrite";
       if (errno == EBADF) {
         throw CUfileException{std::string{"POSIX error on " + name + " at: "} + __FILE__ + ":" +
-                              KVIKIO_STRINGIFY(__LINE__) + ": unsupported file open flags"};
+                              KVIKIO_STRINGIFY(__LINE__) + ": Operation not permitted"};
       }
       throw CUfileException{std::string{"POSIX error on " + name + " at: "} + __FILE__ + ":" +
                             KVIKIO_STRINGIFY(__LINE__) + ": " + strerror(errno)};
@@ -273,6 +273,7 @@ std::size_t posix_device_io(int fd,
 inline std::size_t posix_host_read(
   int fd, void* buf, std::size_t size, std::size_t file_offset, bool partial)
 {
+  KVIKIO_NVTX_FUNC_RANGE("posix_host_read()", size);
   return detail::posix_host_io<true>(fd, buf, size, convert_size2off(file_offset), partial);
 }
 
@@ -292,6 +293,7 @@ inline std::size_t posix_host_read(
 inline std::size_t posix_host_write(
   int fd, const void* buf, std::size_t size, std::size_t file_offset, bool partial)
 {
+  KVIKIO_NVTX_FUNC_RANGE("posix_host_write()", size);
   return detail::posix_host_io<false>(fd, buf, size, convert_size2off(file_offset), partial);
 }
 
@@ -314,6 +316,7 @@ inline std::size_t posix_device_read(int fd,
                                      std::size_t file_offset,
                                      std::size_t devPtr_offset)
 {
+  KVIKIO_NVTX_FUNC_RANGE("posix_device_read()", size);
   return detail::posix_device_io<true>(fd, devPtr_base, size, file_offset, devPtr_offset);
 }
 
@@ -336,6 +339,7 @@ inline std::size_t posix_device_write(int fd,
                                       std::size_t file_offset,
                                       std::size_t devPtr_offset)
 {
+  KVIKIO_NVTX_FUNC_RANGE("posix_device_write()", size);
   return detail::posix_device_io<false>(fd, devPtr_base, size, file_offset, devPtr_offset);
 }
 
diff --git a/cpp/include/kvikio/shim/cufile_h_wrapper.hpp b/cpp/include/kvikio/shim/cufile_h_wrapper.hpp
index f9bd7dc100..33c3fee9a2 100644
--- a/cpp/include/kvikio/shim/cufile_h_wrapper.hpp
+++ b/cpp/include/kvikio/shim/cufile_h_wrapper.hpp
@@ -17,6 +17,8 @@
 
 #include <sys/types.h>
 
+#include <kvikio/shim/cuda_h_wrapper.hpp>
+
 /**
  * In order to support compilation when `cufile.h` isn't available, we
  * wrap all use of cufile in a `#ifdef KVIKIO_CUFILE_FOUND` guard.
@@ -34,6 +36,7 @@
 using CUfileHandle_t = void*;
 using CUfileOpError  = int;
 #define CUFILE_ERRSTR(x)          ("KvikIO not compiled with cuFile.h")
+#define CUFILEOP_BASE_ERR         5000
 #define CU_FILE_SUCCESS           0
 #define CU_FILE_CUDA_DRIVER_ERROR 1
 
diff --git a/cpp/include/kvikio/stream.hpp b/cpp/include/kvikio/stream.hpp
index 6b573fcb06..9c0564ab27 100644
--- a/cpp/include/kvikio/stream.hpp
+++ b/cpp/include/kvikio/stream.hpp
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2023, NVIDIA CORPORATION.
+ * Copyright (c) 2023-2024, NVIDIA CORPORATION.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -132,9 +132,9 @@ class StreamFuture {
       CUDA_DRIVER_TRY(cudaAPI::instance().StreamSynchronize(_stream));
     }
 
-    CUFILE_CHECK_STREAM_IO(&_val->bytes_done);
-    // At this point, we know `*_val->bytes_done` is a positive value otherwise
-    // CUFILE_CHECK_STREAM_IO() would have raised an exception.
+    CUFILE_CHECK_BYTES_DONE(_val->bytes_done);
+    // At this point, we know `_val->bytes_done` is a positive value otherwise
+    // CUFILE_CHECK_BYTES_DONE() would have raised an exception.
     return static_cast<std::size_t>(_val->bytes_done);
   }
 
diff --git a/cpp/include/kvikio/thread_pool.hpp b/cpp/include/kvikio/thread_pool.hpp
deleted file mode 100644
index 008f119486..0000000000
--- a/cpp/include/kvikio/thread_pool.hpp
+++ /dev/null
@@ -1,461 +0,0 @@
-#pragma once
-
-/**
- * @file thread_pool.hpp
- * @author Barak Shoshany (baraksh@gmail.com) (http://baraksh.com)
- * @version 2.0.0
- * @date 2021-08-14
- * @copyright Copyright (c) 2021 Barak Shoshany. Licensed under the MIT license. If you use this
- * library in published research, please cite it as follows:
- *  - Barak Shoshany, "A C++17 Thread Pool for High-Performance Scientific Computing",
- * doi:10.5281/zenodo.4742687, arXiv:2105.00613 (May 2021)
- *
- * @brief A C++17 thread pool for high-performance scientific computing.
- * @details A modern C++17-compatible thread pool implementation, built from scratch with
- * high-performance scientific computing in mind. The thread pool is implemented as a single
- * lightweight and self-contained class, and does not have any dependencies other than the C++17
- * standard library, thus allowing a great degree of portability. In particular, this implementation
- * does not utilize OpenMP or any other high-level multithreading APIs, and thus gives the
- * programmer precise low-level control over the details of the parallelization, which permits more
- * robust optimizations. The thread pool was extensively tested on both AMD and Intel CPUs with up
- * to 40 cores and 80 threads. Other features include automatic generation of futures and easy
- * parallelization of loops. Two helper classes enable synchronizing printing to an output stream by
- * different threads and measuring execution time for benchmarking purposes. Please visit the GitHub
- * repository at https://github.com/bshoshany/thread-pool for documentation and updates, or to
- * submit feature requests and bug reports.
- */
-
-#define THREAD_POOL_VERSION "v2.0.0 (2021-08-14)"
-
-#include <atomic>      // std::atomic
-#include <chrono>      // std::chrono
-#include <cstdint>     // std::int_fast64_t, std::uint_fast32_t
-#include <functional>  // std::function
-#include <future>      // std::future, std::promise
-#include <iostream>    // std::cout, std::ostream
-#include <memory>      // std::shared_ptr, std::unique_ptr
-#include <mutex>       // std::mutex, std::scoped_lock
-#include <queue>       // std::queue
-#include <thread>      // std::this_thread, std::thread
-#include <type_traits>  // std::common_type_t, std::decay_t, std::enable_if_t, std::is_void_v, std::invoke_result_t
-#include <utility>  // std::move
-
-// ============================================================================================= //
-//                                    Begin class thread_pool                                    //
-
-namespace kvikio::third_party {
-
-/**
- * @brief A C++17 thread pool class. The user submits tasks to be executed into a queue. Whenever a
- * thread becomes available, it pops a task from the queue and executes it. Each task is
- * automatically assigned a future, which can be used to wait for the task to finish executing
- * and/or obtain its eventual return value.
- */
-class thread_pool {
-  typedef std::uint_fast32_t ui32;
-  typedef std::uint_fast64_t ui64;
-
- public:
-  // ============================
-  // Constructors and destructors
-  // ============================
-
-  /**
-   * @brief Construct a new thread pool.
-   *
-   * @param _thread_count The number of threads to use. The default value is the total number of
-   * hardware threads available, as reported by the implementation. With a hyperthreaded CPU, this
-   * will be twice the number of CPU cores. If the argument is zero, the default value will be used
-   * instead.
-   */
-  thread_pool(const ui32& _thread_count = std::thread::hardware_concurrency())
-    : thread_count(_thread_count ? _thread_count : std::thread::hardware_concurrency()),
-      threads(new std::thread[_thread_count ? _thread_count : std::thread::hardware_concurrency()])
-  {
-    create_threads();
-  }
-
-  /**
-   * @brief Destruct the thread pool. Waits for all tasks to complete, then destroys all threads.
-   * Note that if the variable paused is set to true, then any tasks still in the queue will never
-   * be executed.
-   */
-  ~thread_pool()
-  {
-    wait_for_tasks();
-    running = false;
-    destroy_threads();
-  }
-
-  // =======================
-  // Public member functions
-  // =======================
-
-  /**
-   * @brief Get the number of tasks currently waiting in the queue to be executed by the threads.
-   *
-   * @return The number of queued tasks.
-   */
-  ui64 get_tasks_queued() const
-  {
-    const std::scoped_lock lock(queue_mutex);
-    return tasks.size();
-  }
-
-  /**
-   * @brief Get the number of tasks currently being executed by the threads.
-   *
-   * @return The number of running tasks.
-   */
-  ui32 get_tasks_running() const { return tasks_total - (ui32)get_tasks_queued(); }
-
-  /**
-   * @brief Get the total number of unfinished tasks - either still in the queue, or running in a
-   * thread.
-   *
-   * @return The total number of tasks.
-   */
-  ui32 get_tasks_total() const { return tasks_total; }
-
-  /**
-   * @brief Get the number of threads in the pool.
-   *
-   * @return The number of threads.
-   */
-  ui32 get_thread_count() const { return thread_count; }
-
-  /**
-   * @brief Parallelize a loop by splitting it into blocks, submitting each block separately to the
-   * thread pool, and waiting for all blocks to finish executing. The user supplies a loop function,
-   * which will be called once per block and should iterate over the block's range.
-   *
-   * @tparam T1 The type of the first index in the loop. Should be a signed or unsigned integer.
-   * @tparam T2 The type of the index after the last index in the loop. Should be a signed or
-   * unsigned integer. If T1 is not the same as T2, a common type will be automatically inferred.
-   * @tparam F The type of the function to loop through.
-   * @param first_index The first index in the loop.
-   * @param index_after_last The index after the last index in the loop. The loop will iterate from
-   * first_index to (index_after_last - 1) inclusive. In other words, it will be equivalent to "for
-   * (T i = first_index; i < index_after_last; i++)". Note that if first_index == index_after_last,
-   * the function will terminate without doing anything.
-   * @param loop The function to loop through. Will be called once per block. Should take exactly
-   * two arguments: the first index in the block and the index after the last index in the block.
-   * loop(start, end) should typically involve a loop of the form "for (T i = start; i < end; i++)".
-   * @param num_blocks The maximum number of blocks to split the loop into. The default is to use
-   * the number of threads in the pool.
-   */
-  template <typename T1, typename T2, typename F>
-  void parallelize_loop(const T1& first_index,
-                        const T2& index_after_last,
-                        const F& loop,
-                        ui32 num_blocks = 0)
-  {
-    typedef std::common_type_t<T1, T2> T;
-    T the_first_index = (T)first_index;
-    T last_index      = (T)index_after_last;
-    if (the_first_index == last_index) return;
-    if (last_index < the_first_index) {
-      T temp          = last_index;
-      last_index      = the_first_index;
-      the_first_index = temp;
-    }
-    last_index--;
-    if (num_blocks == 0) num_blocks = thread_count;
-    ui64 total_size = (ui64)(last_index - the_first_index + 1);
-    ui64 block_size = (ui64)(total_size / num_blocks);
-    if (block_size == 0) {
-      block_size = 1;
-      num_blocks = (ui32)total_size > 1 ? (ui32)total_size : 1;
-    }
-    std::atomic<ui32> blocks_running = 0;
-    for (ui32 t = 0; t < num_blocks; t++) {
-      T start = ((T)(t * block_size) + the_first_index);
-      T end =
-        (t == num_blocks - 1) ? last_index + 1 : ((T)((t + 1) * block_size) + the_first_index);
-      blocks_running++;
-      push_task([start, end, &loop, &blocks_running] {
-        loop(start, end);
-        blocks_running--;
-      });
-    }
-    while (blocks_running != 0) {
-      sleep_or_yield();
-    }
-  }
-
-  /**
-   * @brief Push a function with no arguments or return value into the task queue.
-   *
-   * @tparam F The type of the function.
-   * @param task The function to push.
-   */
-  template <typename F>
-  void push_task(const F& task)
-  {
-    tasks_total++;
-    {
-      const std::scoped_lock lock(queue_mutex);
-      tasks.push(std::function<void()>(task));
-    }
-  }
-
-  /**
-   * @brief Push a function with arguments, but no return value, into the task queue.
-   * @details The function is wrapped inside a lambda in order to hide the arguments, as the tasks
-   * in the queue must be of type std::function<void()>, so they cannot have any arguments or return
-   * value. If no arguments are provided, the other overload will be used, in order to avoid the
-   * (slight) overhead of using a lambda.
-   *
-   * @tparam F The type of the function.
-   * @tparam A The types of the arguments.
-   * @param task The function to push.
-   * @param args The arguments to pass to the function.
-   */
-  template <typename F, typename... A>
-  void push_task(const F& task, const A&... args)
-  {
-    push_task([task, args...] { task(args...); });
-  }
-
-  /**
-   * @brief Reset the number of threads in the pool. Waits for all currently running tasks to be
-   * completed, then destroys all threads in the pool and creates a new thread pool with the new
-   * number of threads. Any tasks that were waiting in the queue before the pool was reset will then
-   * be executed by the new threads. If the pool was paused before resetting it, the new pool will
-   * be paused as well.
-   *
-   * @param _thread_count The number of threads to use. The default value is the total number of
-   * hardware threads available, as reported by the implementation. With a hyperthreaded CPU, this
-   * will be twice the number of CPU cores. If the argument is zero, the default value will be used
-   * instead.
-   */
-  void reset(const ui32& _thread_count = std::thread::hardware_concurrency())
-  {
-    bool was_paused = paused;
-    paused          = true;
-    wait_for_tasks();
-    running = false;
-    destroy_threads();
-    thread_count = _thread_count ? _thread_count : std::thread::hardware_concurrency();
-    threads.reset(new std::thread[thread_count]);
-    paused  = was_paused;
-    running = true;
-    create_threads();
-  }
-
-  /**
-   * @brief Submit a function with zero or more arguments and no return value into the task queue,
-   * and get an std::future<bool> that will be set to true upon completion of the task.
-   *
-   * @tparam F The type of the function.
-   * @tparam A The types of the zero or more arguments to pass to the function.
-   * @param task The function to submit.
-   * @param args The zero or more arguments to pass to the function.
-   * @return A future to be used later to check if the function has finished its execution.
-   */
-  template <typename F,
-            typename... A,
-            typename = std::enable_if_t<
-              std::is_void_v<std::invoke_result_t<std::decay_t<F>, std::decay_t<A>...>>>>
-  std::future<bool> submit(const F& task, const A&... args)
-  {
-    std::shared_ptr<std::promise<bool>> task_promise(new std::promise<bool>);
-    std::future<bool> future = task_promise->get_future();
-    push_task([task, args..., task_promise] {
-      try {
-        task(args...);
-        task_promise->set_value(true);
-      } catch (...) {
-        try {
-          task_promise->set_exception(std::current_exception());
-        } catch (...) {
-        }
-      }
-    });
-    return future;
-  }
-
-  /**
-   * @brief Submit a function with zero or more arguments and a return value into the task queue,
-   * and get a future for its eventual returned value.
-   *
-   * @tparam F The type of the function.
-   * @tparam A The types of the zero or more arguments to pass to the function.
-   * @tparam R The return type of the function.
-   * @param task The function to submit.
-   * @param args The zero or more arguments to pass to the function.
-   * @return A future to be used later to obtain the function's returned value, waiting for it to
-   * finish its execution if needed.
-   */
-  template <typename F,
-            typename... A,
-            typename R = std::invoke_result_t<std::decay_t<F>, std::decay_t<A>...>,
-            typename   = std::enable_if_t<!std::is_void_v<R>>>
-  std::future<R> submit(const F& task, const A&... args)
-  {
-    std::shared_ptr<std::promise<R>> task_promise(new std::promise<R>);
-    std::future<R> future = task_promise->get_future();
-    push_task([task, args..., task_promise] {
-      try {
-        task_promise->set_value(task(args...));
-      } catch (...) {
-        try {
-          task_promise->set_exception(std::current_exception());
-        } catch (...) {
-        }
-      }
-    });
-    return future;
-  }
-
-  /**
-   * @brief Wait for tasks to be completed. Normally, this function waits for all tasks, both those
-   * that are currently running in the threads and those that are still waiting in the queue.
-   * However, if the variable paused is set to true, this function only waits for the currently
-   * running tasks (otherwise it would wait forever). To wait for a specific task, use submit()
-   * instead, and call the wait() member function of the generated future.
-   */
-  void wait_for_tasks()
-  {
-    while (true) {
-      if (!paused) {
-        if (tasks_total == 0) break;
-      } else {
-        if (get_tasks_running() == 0) break;
-      }
-      sleep_or_yield();
-    }
-  }
-
-  // ===========
-  // Public data
-  // ===========
-
-  /**
-   * @brief An atomic variable indicating to the workers to pause. When set to true, the workers
-   * temporarily stop popping new tasks out of the queue, although any tasks already executed will
-   * keep running until they are done. Set to false again to resume popping tasks.
-   */
-  std::atomic<bool> paused = false;
-
-  /**
-   * @brief The duration, in microseconds, that the worker function should sleep for when it cannot
-   * find any tasks in the queue. If set to 0, then instead of sleeping, the worker function will
-   * execute std::this_thread::yield() if there are no tasks in the queue. The default value is
-   * 1000.
-   */
-  ui32 sleep_duration = 1000;
-
- private:
-  // ========================
-  // Private member functions
-  // ========================
-
-  /**
-   * @brief Create the threads in the pool and assign a worker to each thread.
-   */
-  void create_threads()
-  {
-    for (ui32 i = 0; i < thread_count; i++) {
-      threads[i] = std::thread(&thread_pool::worker, this);
-    }
-  }
-
-  /**
-   * @brief Destroy the threads in the pool by joining them.
-   */
-  void destroy_threads()
-  {
-    for (ui32 i = 0; i < thread_count; i++) {
-      threads[i].join();
-    }
-  }
-
-  /**
-   * @brief Try to pop a new task out of the queue.
-   *
-   * @param task A reference to the task. Will be populated with a function if the queue is not
-   * empty.
-   * @return true if a task was found, false if the queue is empty.
-   */
-  bool pop_task(std::function<void()>& task)
-  {
-    const std::scoped_lock lock(queue_mutex);
-    if (tasks.empty())
-      return false;
-    else {
-      task = std::move(tasks.front());
-      tasks.pop();
-      return true;
-    }
-  }
-
-  /**
-   * @brief Sleep for sleep_duration microseconds. If that variable is set to zero, yield instead.
-   *
-   */
-  void sleep_or_yield()
-  {
-    if (sleep_duration)
-      std::this_thread::sleep_for(std::chrono::microseconds(sleep_duration));
-    else
-      std::this_thread::yield();
-  }
-
-  /**
-   * @brief A worker function to be assigned to each thread in the pool. Continuously pops tasks out
-   * of the queue and executes them, as long as the atomic variable running is set to true.
-   */
-  void worker()
-  {
-    while (running) {
-      std::function<void()> task;
-      if (!paused && pop_task(task)) {
-        task();
-        tasks_total--;
-      } else {
-        sleep_or_yield();
-      }
-    }
-  }
-
-  // ============
-  // Private data
-  // ============
-
-  /**
-   * @brief A mutex to synchronize access to the task queue by different threads.
-   */
-  mutable std::mutex queue_mutex = {};
-
-  /**
-   * @brief An atomic variable indicating to the workers to keep running. When set to false, the
-   * workers permanently stop working.
-   */
-  std::atomic<bool> running = true;
-
-  /**
-   * @brief A queue of tasks to be executed by the threads.
-   */
-  std::queue<std::function<void()>> tasks = {};
-
-  /**
-   * @brief The number of threads in the pool.
-   */
-  ui32 thread_count;
-
-  /**
-   * @brief A smart pointer to manage the memory allocated for the threads.
-   */
-  std::unique_ptr<std::thread[]> threads;
-
-  /**
-   * @brief An atomic variable to keep track of the total number of unfinished tasks - either still
-   * in the queue, or running in a thread.
-   */
-  std::atomic<ui32> tasks_total = 0;
-};
-}  // namespace kvikio::third_party
-
-//                                     End class thread_pool                                     //
-// ============================================================================================= //
diff --git a/cpp/include/kvikio/utils.hpp b/cpp/include/kvikio/utils.hpp
index a136aad8c2..918983a919 100644
--- a/cpp/include/kvikio/utils.hpp
+++ b/cpp/include/kvikio/utils.hpp
@@ -23,6 +23,8 @@
 #include <optional>
 #include <tuple>
 
+#include <nvtx3/nvtx3.hpp>
+
 #include <kvikio/error.hpp>
 #include <kvikio/shim/cuda.hpp>
 
@@ -264,4 +266,47 @@ inline bool is_future_done(const T& future)
   return future.wait_for(std::chrono::seconds(0)) != std::future_status::timeout;
 }
 
+/**
+ * @brief Tag type for libkvikio's NVTX domain.
+ */
+struct libkvikio_domain {
+  static constexpr char const* name{"libkvikio"};
+};
+
+// Macro overloads of KVIKIO_NVTX_FUNC_RANGE
+#define KVIKIO_NVTX_FUNC_RANGE_1() NVTX3_FUNC_RANGE_IN(libkvikio_domain)
+#define KVIKIO_NVTX_FUNC_RANGE_2(msg, val)                    \
+  nvtx3::scoped_range_in<libkvikio_domain> _kvikio_nvtx_range \
+  {                                                           \
+    nvtx3::event_attributes                                   \
+    {                                                         \
+      msg, nvtx3::payload { val }                             \
+    }                                                         \
+  }
+#define GET_KVIKIO_NVTX_FUNC_RANGE_MACRO(_1, _2, NAME, ...) NAME
+
+/**
+ * @brief Convenience macro for generating an NVTX range in the `libkvikio` domain
+ * from the lifetime of a function.
+ *
+ * Takes two arguments (message, payload) or no arguments, in which case the name
+ * of the immediately enclosing function returned by `__func__` is used.
+ *
+ * Example:
+ * ```
+ * void some_function1(){
+ *    KVIKIO_NVTX_FUNC_RANGE("my function", 42);
+ *    ...
+ * }
+ * void some_function2(){
+ *    KVIKIO_NVTX_FUNC_RANGE();  // The name `some_function2` is used
+ *    ...
+ * }
+ * ```
+ */
+#define KVIKIO_NVTX_FUNC_RANGE(...)                                  \
+  GET_KVIKIO_NVTX_FUNC_RANGE_MACRO(                                  \
+    __VA_ARGS__, KVIKIO_NVTX_FUNC_RANGE_2, KVIKIO_NVTX_FUNC_RANGE_1) \
+  (__VA_ARGS__)
+
 }  // namespace kvikio
diff --git a/cpp/scripts/run-cmake-format.sh b/cpp/scripts/run-cmake-format.sh
index fe6a96d770..963fc7359b 100755
--- a/cpp/scripts/run-cmake-format.sh
+++ b/cpp/scripts/run-cmake-format.sh
@@ -1,6 +1,5 @@
 #!/bin/bash
-
-# Copyright (c) 2021-2023, NVIDIA CORPORATION.
+# Copyright (c) 2021-2024, NVIDIA CORPORATION.
 
 # This script is a wrapper for cmakelang that may be used with pre-commit. The
 # wrapping is necessary because RAPIDS libraries split configuration for
@@ -45,6 +44,7 @@ fi
 
 DEFAULT_FORMAT_FILE_LOCATIONS=(
   "${KVIKIO_BUILD_DIR:-${HOME}}/_deps/rapids-cmake-src/cmake-format-rapids-cmake.json"
+  "${KVIKIO_BUILD_DIR:-cpp/build}/latest/_deps/rapids-cmake-src/cmake-format-rapids-cmake.json"
 )
 
 if [ -z ${RAPIDS_CMAKE_FORMAT_FILE:+PLACEHOLDER} ]; then
diff --git a/dependencies.yaml b/dependencies.yaml
index 1e17c924f0..16f059de19 100644
--- a/dependencies.yaml
+++ b/dependencies.yaml
@@ -3,16 +3,19 @@ files:
   all:
     output: conda
     matrix:
-      cuda: ["11.8", "12.2"]
-      arch: [x86_64]
+      cuda: ["11.8", "12.5"]
+      arch: [aarch64, x86_64]
     includes:
-      - build
+      - build-universal
+      - build-cpp
+      - build-py-wrapper
       - checks
       - cuda
       - cuda_version
       - depends_on_cupy
       - docs
       - py_version
+      - rapids_build_skbuild
       - run
       - test_python
   test_cpp:
@@ -38,22 +41,48 @@ files:
       - py_version
   py_build:
     output: pyproject
-    pyproject_dir: python
+    pyproject_dir: python/kvikio
     extras:
       table: build-system
     includes:
-      - build
+      - rapids_build_skbuild
+  py_build_cpp_wheel:
+    output: pyproject
+    pyproject_dir: python/libkvikio
+    extras:
+      table: build-system
+    includes:
+      - rapids_build_skbuild
   py_run:
     output: pyproject
-    pyproject_dir: python
+    pyproject_dir: python/kvikio
     extras:
       table: project
     includes:
       - depends_on_cupy
       - run
+  py_wheel_cpp:
+    output: pyproject
+    pyproject_dir: python/libkvikio
+    extras:
+      table: tool.rapids-build-backend
+      key: requires
+    includes:
+      - build-universal
+  py_wheel_python:
+    output: pyproject
+    pyproject_dir: python/kvikio
+    extras:
+      table: tool.rapids-build-backend
+      key: requires
+    includes:
+      - build-universal
+      - build-cpp
+      - build-py-wrapper
+      - build-use-libkvikio-wheel
   py_optional_test:
     output: pyproject
-    pyproject_dir: python
+    pyproject_dir: python/kvikio
     extras:
       table: project.optional-dependencies
       key: test
@@ -65,21 +94,18 @@ channels:
   - conda-forge
   - nvidia
 dependencies:
-  build:
+  build-universal:
     common:
       - output_types: [conda, requirements, pyproject]
         packages:
-          - cmake>=3.26.4
-          - cython>=3.0.0
+          - cmake>=3.26.4,!=3.30.0
           - ninja
+  build-cpp:
+    common:
       - output_types: conda
         packages:
           - c-compiler
           - cxx-compiler
-          - scikit-build-core>=0.7.0
-      - output_types: [requirements, pyproject]
-        packages:
-          - scikit-build-core[pyproject]>=0.7.0
     specific:
       - output_types: conda
         matrices:
@@ -109,6 +135,30 @@ dependencies:
               cuda: "12.*"
             packages:
               - cuda-nvcc
+  build-use-libkvikio-wheel:
+    common:
+      - output_types: conda
+        packages: &libkvikio_packages
+          - libkvikio==24.8.*,>=0.0.0a0
+    specific:
+      - output_types: [requirements, pyproject]
+        matrices:
+          - matrix:
+              cuda: "12.*"
+              cuda_suffixed: "true"
+            packages:
+              - libkvikio-cu12==24.8.*,>=0.0.0a0
+          - matrix:
+              cuda: "11.*"
+              cuda_suffixed: "true"
+            packages:
+              - libkvikio-cu11==24.8.*,>=0.0.0a0
+          - {matrix: null, packages: *libkvikio_packages}
+  build-py-wrapper:
+    common:
+      - output_types: [conda, requirements, pyproject]
+        packages:
+          - cython>=3.0.0
   checks:
     common:
       - output_types: [conda, requirements]
@@ -142,6 +192,10 @@ dependencies:
               cuda: "12.2"
             packages:
               - cuda-version=12.2
+          - matrix:
+              cuda: "12.5"
+            packages:
+              - cuda-version=12.5
   cuda:
     common:
       - output_types: conda
@@ -159,14 +213,14 @@ dependencies:
             packages:
       - output_types: conda
         matrices:
-          - matrix:
-              arch: aarch64
-            packages:
           - matrix:
               cuda: "12.*"
-              arch: x86_64
             packages:
               - libcufile-dev
+          - matrix:
+              arch: aarch64
+              cuda: "11.*"
+            packages:
           - matrix:
               cuda: "11.8"
               arch: x86_64
@@ -238,6 +292,17 @@ dependencies:
           - matrix:
             packages:
               - python>=3.9,<3.12
+  rapids_build_skbuild:
+    common:
+      - output_types: [conda, requirements, pyproject]
+        packages:
+          - rapids-build-backend>=0.3.0,<0.4.0.dev0
+      - output_types: conda
+        packages:
+          - scikit-build-core>=0.7.0
+      - output_types: [requirements, pyproject]
+        packages:
+          - scikit-build-core[pyproject]>=0.7.0
   run:
     common:
       - output_types: [conda, requirements, pyproject]
diff --git a/docs/source/install.rst b/docs/source/install.rst
index 3c821cf75f..3e7985401c 100644
--- a/docs/source/install.rst
+++ b/docs/source/install.rst
@@ -17,8 +17,8 @@ Install the **stable release** from the ``rapidsai`` channel like:
   mamba install -c rapidsai -c conda-forge kvikio
   # Create new environment (CUDA 11.8)
   mamba create -n kvikio-env -c rapidsai -c conda-forge python=3.11 cuda-version=11.8 kvikio
-  # Create new environment (CUDA 12.2)
-  mamba create -n kvikio-env -c rapidsai -c conda-forge python=3.11 cuda-version=12.2 kvikio
+  # Create new environment (CUDA 12.5)
+  mamba create -n kvikio-env -c rapidsai -c conda-forge python=3.11 cuda-version=12.5 kvikio
 
 Install the **nightly release** from the ``rapidsai-nightly`` channel like:
 
@@ -28,8 +28,8 @@ Install the **nightly release** from the ``rapidsai-nightly`` channel like:
   mamba install -c rapidsai-nightly -c conda-forge kvikio
   # Create new environment (CUDA 11.8)
   mamba create -n kvikio-env -c rapidsai-nightly -c conda-forge python=3.11 cuda-version=11.8 kvikio
-  # Create new environment (CUDA 12.2)
-  mamba create -n kvikio-env -c rapidsai-nightly -c conda-forge python=3.11 cuda-version=12.2 kvikio
+  # Create new environment (CUDA 12.5)
+  mamba create -n kvikio-env -c rapidsai-nightly -c conda-forge python=3.11 cuda-version=12.5 kvikio
 
 
 .. note::
@@ -45,8 +45,8 @@ In order to setup a development environment run:
 
   # CUDA 11.8
   mamba env create --name kvikio-dev --file conda/environments/all_cuda-118_arch-x86_64.yaml
-  # CUDA 12.2
-  mamba env create --name kvikio-dev --file conda/environments/all_cuda-122_arch-x86_64.yaml
+  # CUDA 12.5
+  mamba env create --name kvikio-dev --file conda/environments/all_cuda-125_arch-x86_64.yaml
 
 To build and install the extension run:
 
@@ -61,11 +61,11 @@ In order to test the installation, run the following:
 
 .. code-block::
 
-  pytest tests/
+  pytest python/kvikio/tests/
 
 
 And to test performance, run the following:
 
 .. code-block::
 
-  python benchmarks/single-node-io.py
+  python python/kvikio/kvikio/benchmarks/single_node_io.py
diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst
index 0b1f3e8a68..01e644fb2a 100644
--- a/docs/source/quickstart.rst
+++ b/docs/source/quickstart.rst
@@ -23,13 +23,13 @@ In order to open a file, use KvikIO's filehandle :py:meth:`kvikio.cufile.CuFile`
 
   # Use contexmanager
   c = cupy.empty_like(a)
-  with kvikio.CuFile(path, "r") as f:
+  with kvikio.CuFile("test-file", "r") as f:
       f.read(c)
   assert all(a == c)
 
   # Non-blocking read
   d = cupy.empty_like(a)
-  with kvikio.CuFile(path, "r") as f:
+  with kvikio.CuFile("test-file", "r") as f:
       future1 = f.pread(d[:50])
       future2 = f.pread(d[50:], file_offset=d[:50].nbytes)
       future1.get()  # Wait for first read
diff --git a/docs/source/zarr.rst b/docs/source/zarr.rst
index f2a697d525..5b63ffd8b7 100644
--- a/docs/source/zarr.rst
+++ b/docs/source/zarr.rst
@@ -11,5 +11,5 @@ The following is an example of how to use the convenience function :py:meth:`kvi
 to create a new Zarr array and how open an existing Zarr array.
 
 
-.. literalinclude:: ../../python/examples/zarr_cupy_nvcomp.py
+.. literalinclude:: ../../python/kvikio/examples/zarr_cupy_nvcomp.py
     :language: python
diff --git a/python/.gitattributes b/python/.gitattributes
deleted file mode 100644
index 2656ae48af..0000000000
--- a/python/.gitattributes
+++ /dev/null
@@ -1,1 +0,0 @@
-kvikio/_version.py export-subst
diff --git a/python/README.md b/python/README.md
deleted file mode 120000
index 32d46ee883..0000000000
--- a/python/README.md
+++ /dev/null
@@ -1,1 +0,0 @@
-../README.md
\ No newline at end of file
diff --git a/python/.coveragerc b/python/kvikio/.coveragerc
similarity index 100%
rename from python/.coveragerc
rename to python/kvikio/.coveragerc
diff --git a/python/CMakeLists.txt b/python/kvikio/CMakeLists.txt
similarity index 78%
rename from python/CMakeLists.txt
rename to python/kvikio/CMakeLists.txt
index 039daf7ce9..9217645637 100644
--- a/python/CMakeLists.txt
+++ b/python/kvikio/CMakeLists.txt
@@ -14,7 +14,7 @@
 
 cmake_minimum_required(VERSION 3.26.4 FATAL_ERROR)
 
-include(../cpp/cmake/rapids_config.cmake)
+include(../../cpp/cmake/rapids_config.cmake)
 include(rapids-cpm)
 rapids_cpm_init()
 include(rapids-cuda)
@@ -31,7 +31,7 @@ option(FIND_KVIKIO_CPP
 )
 
 # TODO: Should we symlink FindcuFile.cmake into python/cmake? find cuFile
-include(../cpp/cmake/Modules/FindcuFile.cmake)
+include(../../cpp/cmake/Modules/FindcuFile.cmake)
 
 if(FIND_KVIKIO_CPP)
   find_package(KvikIO "${RAPIDS_VERSION}")
@@ -41,9 +41,10 @@ endif()
 
 find_package(CUDAToolkit REQUIRED)
 
+set(cython_lib_dir kvikio)
+
 if(NOT KvikIO_FOUND)
-  add_subdirectory(../cpp kvikio-cpp)
-  set(cython_lib_dir kvikio)
+  add_subdirectory(../../cpp kvikio-cpp)
   install(TARGETS kvikio DESTINATION ${cython_lib_dir})
 endif()
 
@@ -51,4 +52,12 @@ include(rapids-cython-core)
 rapids_cython_init()
 
 add_subdirectory(cmake)
+
+# It would be better to factor nvcomp out into its own wheel. Until that is available, we vendor it
+# here.
+install_aliased_imported_targets(
+  TARGETS nvcomp::nvcomp nvcomp::nvcomp_gdeflate nvcomp::nvcomp_bitcomp DESTINATION
+  ${cython_lib_dir}/_lib
+)
+
 add_subdirectory(kvikio/_lib)
diff --git a/python/kvikio/README.md b/python/kvikio/README.md
new file mode 120000
index 0000000000..fe84005413
--- /dev/null
+++ b/python/kvikio/README.md
@@ -0,0 +1,1 @@
+../../README.md
\ No newline at end of file
diff --git a/python/kvikio/VERSION b/python/kvikio/VERSION
deleted file mode 120000
index 558194c5a5..0000000000
--- a/python/kvikio/VERSION
+++ /dev/null
@@ -1,1 +0,0 @@
-../../VERSION
\ No newline at end of file
diff --git a/python/cli/gpu_compressor.py b/python/kvikio/cli/gpu_compressor.py
similarity index 100%
rename from python/cli/gpu_compressor.py
rename to python/kvikio/cli/gpu_compressor.py
diff --git a/python/cmake/CMakeLists.txt b/python/kvikio/cmake/CMakeLists.txt
similarity index 84%
rename from python/cmake/CMakeLists.txt
rename to python/kvikio/cmake/CMakeLists.txt
index 4249773480..fa94bc3f8e 100644
--- a/python/cmake/CMakeLists.txt
+++ b/python/kvikio/cmake/CMakeLists.txt
@@ -1,5 +1,5 @@
 # =============================================================================
-# Copyright (c) 2022-2023, NVIDIA CORPORATION.
+# Copyright (c) 2022-2024, NVIDIA CORPORATION.
 #
 # 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
@@ -13,3 +13,5 @@
 # =============================================================================
 
 include(thirdparty/get_nvcomp.cmake)
+# Needed for install_aliased_imported_targets
+include(thirdparty/WheelHelpers.cmake)
diff --git a/python/kvikio/cmake/thirdparty/WheelHelpers.cmake b/python/kvikio/cmake/thirdparty/WheelHelpers.cmake
new file mode 100644
index 0000000000..3abe98a064
--- /dev/null
+++ b/python/kvikio/cmake/thirdparty/WheelHelpers.cmake
@@ -0,0 +1,59 @@
+# =============================================================================
+# Copyright (c) 2022-2024, NVIDIA CORPORATION.
+#
+# 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.
+# =============================================================================
+include_guard(GLOBAL)
+
+# Making libraries available inside wheels by installing the associated targets.
+function(install_aliased_imported_targets)
+  list(APPEND CMAKE_MESSAGE_CONTEXT "install_aliased_imported_targets")
+
+  set(options "")
+  set(one_value "DESTINATION")
+  set(multi_value "TARGETS")
+  cmake_parse_arguments(_ "${options}" "${one_value}" "${multi_value}" ${ARGN})
+
+  message(VERBOSE "Installing targets '${__TARGETS}' into lib_dir '${__DESTINATION}'")
+
+  foreach(target IN LISTS __TARGETS)
+
+    if(NOT TARGET ${target})
+      message(VERBOSE "No target named ${target}")
+      continue()
+    endif()
+
+    get_target_property(alias_target ${target} ALIASED_TARGET)
+    if(alias_target)
+      set(target ${alias_target})
+    endif()
+
+    get_target_property(is_imported ${target} IMPORTED)
+    if(NOT is_imported)
+      # If the target isn't imported, install it into the wheel
+      install(TARGETS ${target} DESTINATION ${__DESTINATION})
+      message(VERBOSE "install(TARGETS ${target} DESTINATION ${__DESTINATION})")
+    else()
+      # If the target is imported, make sure it's global
+      get_target_property(type ${target} TYPE)
+      if(${type} STREQUAL "UNKNOWN_LIBRARY")
+        install(FILES $<TARGET_FILE:${target}> DESTINATION ${__DESTINATION})
+        message(VERBOSE "install(FILES $<TARGET_FILE:${target}> DESTINATION ${__DESTINATION})")
+      else()
+        install(IMPORTED_RUNTIME_ARTIFACTS ${target} DESTINATION ${__DESTINATION})
+        message(
+          VERBOSE
+          "install(IMPORTED_RUNTIME_ARTIFACTS $<TARGET_FILE:${target}> DESTINATION ${__DESTINATION})"
+        )
+      endif()
+    endif()
+  endforeach()
+endfunction()
diff --git a/python/cmake/thirdparty/get_nvcomp.cmake b/python/kvikio/cmake/thirdparty/get_nvcomp.cmake
similarity index 100%
rename from python/cmake/thirdparty/get_nvcomp.cmake
rename to python/kvikio/cmake/thirdparty/get_nvcomp.cmake
diff --git a/python/examples/hello_world.py b/python/kvikio/examples/hello_world.py
similarity index 100%
rename from python/examples/hello_world.py
rename to python/kvikio/examples/hello_world.py
diff --git a/python/examples/zarr_cupy_nvcomp.py b/python/kvikio/examples/zarr_cupy_nvcomp.py
similarity index 100%
rename from python/examples/zarr_cupy_nvcomp.py
rename to python/kvikio/examples/zarr_cupy_nvcomp.py
diff --git a/python/kvikio/kvikio/VERSION b/python/kvikio/kvikio/VERSION
new file mode 120000
index 0000000000..d62dc733ef
--- /dev/null
+++ b/python/kvikio/kvikio/VERSION
@@ -0,0 +1,1 @@
+../../../VERSION
\ No newline at end of file
diff --git a/python/kvikio/__init__.py b/python/kvikio/kvikio/__init__.py
similarity index 86%
rename from python/kvikio/__init__.py
rename to python/kvikio/kvikio/__init__.py
index 5186df50f8..5e2704adfa 100644
--- a/python/kvikio/__init__.py
+++ b/python/kvikio/kvikio/__init__.py
@@ -2,7 +2,7 @@
 # See file LICENSE for terms.
 
 from ._lib import libkvikio  # type: ignore
-from ._version import __version__  # noqa: F401
+from ._version import __git_commit__, __version__  # noqa: F401
 from .cufile import CuFile  # noqa: F401
 
 
diff --git a/python/kvikio/_lib/CMakeLists.txt b/python/kvikio/kvikio/_lib/CMakeLists.txt
similarity index 100%
rename from python/kvikio/_lib/CMakeLists.txt
rename to python/kvikio/kvikio/_lib/CMakeLists.txt
diff --git a/python/kvikio/_lib/__init__.pxd b/python/kvikio/kvikio/_lib/__init__.pxd
similarity index 100%
rename from python/kvikio/_lib/__init__.pxd
rename to python/kvikio/kvikio/_lib/__init__.pxd
diff --git a/python/kvikio/_lib/__init__.py b/python/kvikio/kvikio/_lib/__init__.py
similarity index 100%
rename from python/kvikio/_lib/__init__.py
rename to python/kvikio/kvikio/_lib/__init__.py
diff --git a/python/kvikio/_lib/arr.pxd b/python/kvikio/kvikio/_lib/arr.pxd
similarity index 100%
rename from python/kvikio/_lib/arr.pxd
rename to python/kvikio/kvikio/_lib/arr.pxd
diff --git a/python/kvikio/_lib/arr.pyi b/python/kvikio/kvikio/_lib/arr.pyi
similarity index 100%
rename from python/kvikio/_lib/arr.pyi
rename to python/kvikio/kvikio/_lib/arr.pyi
diff --git a/python/kvikio/_lib/arr.pyx b/python/kvikio/kvikio/_lib/arr.pyx
similarity index 100%
rename from python/kvikio/_lib/arr.pyx
rename to python/kvikio/kvikio/_lib/arr.pyx
diff --git a/python/kvikio/_lib/kvikio_cxx_api.pxd b/python/kvikio/kvikio/_lib/kvikio_cxx_api.pxd
similarity index 100%
rename from python/kvikio/_lib/kvikio_cxx_api.pxd
rename to python/kvikio/kvikio/_lib/kvikio_cxx_api.pxd
diff --git a/python/kvikio/_lib/libkvikio.pyx b/python/kvikio/kvikio/_lib/libkvikio.pyx
similarity index 100%
rename from python/kvikio/_lib/libkvikio.pyx
rename to python/kvikio/kvikio/_lib/libkvikio.pyx
diff --git a/python/kvikio/_lib/libnvcomp.pyx b/python/kvikio/kvikio/_lib/libnvcomp.pyx
similarity index 100%
rename from python/kvikio/_lib/libnvcomp.pyx
rename to python/kvikio/kvikio/_lib/libnvcomp.pyx
diff --git a/python/kvikio/_lib/libnvcomp_ll.pyx b/python/kvikio/kvikio/_lib/libnvcomp_ll.pyx
similarity index 100%
rename from python/kvikio/_lib/libnvcomp_ll.pyx
rename to python/kvikio/kvikio/_lib/libnvcomp_ll.pyx
diff --git a/python/kvikio/_lib/nvcomp_cxx_api.pxd b/python/kvikio/kvikio/_lib/nvcomp_cxx_api.pxd
similarity index 100%
rename from python/kvikio/_lib/nvcomp_cxx_api.pxd
rename to python/kvikio/kvikio/_lib/nvcomp_cxx_api.pxd
diff --git a/python/kvikio/_lib/nvcomp_ll_cxx_api.pxd b/python/kvikio/kvikio/_lib/nvcomp_ll_cxx_api.pxd
similarity index 100%
rename from python/kvikio/_lib/nvcomp_ll_cxx_api.pxd
rename to python/kvikio/kvikio/_lib/nvcomp_ll_cxx_api.pxd
diff --git a/python/kvikio/kvikio/_version.py b/python/kvikio/kvikio/_version.py
new file mode 100644
index 0000000000..a5171f19f4
--- /dev/null
+++ b/python/kvikio/kvikio/_version.py
@@ -0,0 +1,30 @@
+# Copyright (c) 2023-2024, NVIDIA CORPORATION.
+#
+# 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.
+
+import importlib.resources
+
+__version__ = (
+    importlib.resources.files(__package__).joinpath("VERSION").read_text().strip()
+)
+try:
+    __git_commit__ = (
+        importlib.resources.files(__package__)
+        .joinpath("GIT_COMMIT")
+        .read_text()
+        .strip()
+    )
+except FileNotFoundError:
+    __git_commit__ = ""
+
+__all__ = ["__git_commit__", "__version__"]
diff --git a/python/kvikio/kvikio/benchmarks/__init__.py b/python/kvikio/kvikio/benchmarks/__init__.py
new file mode 100644
index 0000000000..8586c47db2
--- /dev/null
+++ b/python/kvikio/kvikio/benchmarks/__init__.py
@@ -0,0 +1,2 @@
+# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.
+# See file LICENSE for terms.
diff --git a/python/benchmarks/single-node-io.py b/python/kvikio/kvikio/benchmarks/single_node_io.py
similarity index 82%
rename from python/benchmarks/single-node-io.py
rename to python/kvikio/kvikio/benchmarks/single_node_io.py
index 72b57300cc..4d47a80791 100644
--- a/python/benchmarks/single-node-io.py
+++ b/python/kvikio/kvikio/benchmarks/single_node_io.py
@@ -1,10 +1,8 @@
-# Copyright (c) 2021-2023, NVIDIA CORPORATION. All rights reserved.
+# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved.
 # See file LICENSE for terms.
 
 import argparse
 import contextlib
-import os
-import os.path
 import pathlib
 import shutil
 import statistics
@@ -17,6 +15,7 @@
 
 import kvikio
 import kvikio.defaults
+from kvikio.benchmarks.utils import parse_directory, pprint_sys_info
 
 
 def get_zarr_compressors() -> Dict[str, Any]:
@@ -260,53 +259,10 @@ def main(args):
     cupy.arange(10)  # Make sure CUDA is initialized
 
     kvikio.defaults.num_threads_reset(args.nthreads)
-    props = kvikio.DriverProperties()
-    try:
-        import pynvml.smi
-
-        nvsmi = pynvml.smi.nvidia_smi.getInstance()
-    except ImportError:
-        gpu_name = "Unknown (install pynvml)"
-        mem_total = gpu_name
-        bar1_total = gpu_name
-    else:
-        info = nvsmi.DeviceQuery()["gpu"][0]
-        gpu_name = f"{info['product_name']} (dev #0)"
-        mem_total = format_bytes(
-            parse_bytes(
-                str(info["fb_memory_usage"]["total"]) + info["fb_memory_usage"]["unit"]
-            )
-        )
-        bar1_total = format_bytes(
-            parse_bytes(
-                str(info["bar1_memory_usage"]["total"])
-                + info["bar1_memory_usage"]["unit"]
-            )
-        )
-    gds_version = "N/A (Compatibility Mode)"
-    if props.is_gds_available:
-        gds_version = f"v{props.major_version}.{props.minor_version}"
-    gds_config_json_path = os.path.realpath(
-        os.getenv("CUFILE_ENV_PATH_JSON", "/etc/cufile.json")
-    )
 
     print("Roundtrip benchmark")
     print("----------------------------------")
-    if kvikio.defaults.compat_mode():
-        print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
-        print("   WARNING - KvikIO compat mode   ")
-        print("      libcufile.so not used       ")
-        print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
-    elif not props.is_gds_available:
-        print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
-        print("   WARNING - cuFile compat mode   ")
-        print("         GDS not enabled          ")
-        print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
-    print(f"GPU               | {gpu_name}")
-    print(f"GPU Memory Total  | {mem_total}")
-    print(f"BAR1 Memory Total | {bar1_total}")
-    print(f"GDS driver        | {gds_version}")
-    print(f"GDS config.json   | {gds_config_json_path}")
+    pprint_sys_info()
     print("----------------------------------")
     print(f"nbytes            | {args.nbytes} bytes ({format_bytes(args.nbytes)})")
     print(f"4K aligned        | {args.nbytes % 4096 == 0}")
@@ -345,16 +301,6 @@ def pprint_api_res(name, samples):
 
 
 if __name__ == "__main__":
-
-    def parse_directory(x):
-        if x is None:
-            return x
-        else:
-            p = pathlib.Path(x)
-            if not p.is_dir():
-                raise argparse.ArgumentTypeError("Must be a directory")
-            return p
-
     parser = argparse.ArgumentParser(description="Roundtrip benchmark")
     parser.add_argument(
         "-n",
@@ -380,10 +326,10 @@ def parse_directory(x):
         help="Number of runs per API (default: %(default)s).",
     )
     parser.add_argument(
-        "--no-pre-register-buffer",
+        "--pre-register-buffer",
         action="store_true",
         default=False,
-        help="Disable pre-register of device buffer",
+        help="Enable pre-register of device buffer",
     )
     parser.add_argument(
         "-t",
@@ -413,7 +359,6 @@ def parse_directory(x):
     )
 
     args = parser.parse_args()
-    args.pre_register_buffer = args.no_pre_register_buffer is False
     if "all" in args.api:
         args.api = tuple(API.keys())
 
diff --git a/python/kvikio/kvikio/benchmarks/utils.py b/python/kvikio/kvikio/benchmarks/utils.py
new file mode 100644
index 0000000000..69375b8c21
--- /dev/null
+++ b/python/kvikio/kvikio/benchmarks/utils.py
@@ -0,0 +1,88 @@
+# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.
+# See file LICENSE for terms.
+
+from __future__ import annotations
+
+import argparse
+import os
+import os.path
+import pathlib
+import subprocess
+
+from dask.utils import format_bytes
+
+import kvikio
+import kvikio.defaults
+
+
+def drop_vm_cache() -> None:
+    """Tells the Linux kernel to drop the page, inode, and dentry caches
+
+    See <https://linux-mm.org/Drop_Caches>
+    """
+    subprocess.check_output(["sudo /sbin/sysctl vm.drop_caches=3"], shell=True)
+
+
+def pprint_sys_info() -> None:
+    """Pretty print system information"""
+
+    props = kvikio.DriverProperties()
+    try:
+        import pynvml
+
+        pynvml.nvmlInit()
+        dev = pynvml.nvmlDeviceGetHandleByIndex(0)
+    except ImportError:
+        gpu_name = "Unknown (install nvidia-ml-py)"
+        mem_total = gpu_name
+        bar1_total = gpu_name
+    else:
+        gpu_name = f"{pynvml.nvmlDeviceGetName(dev)} (dev #0)"
+        mem_total = format_bytes(pynvml.nvmlDeviceGetMemoryInfo(dev).total)
+        bar1_total = format_bytes(pynvml.nvmlDeviceGetBAR1MemoryInfo(dev).bar1Total)
+    gds_version = "N/A (Compatibility Mode)"
+    if props.is_gds_available:
+        gds_version = f"v{props.major_version}.{props.minor_version}"
+    gds_config_json_path = os.path.realpath(
+        os.getenv("CUFILE_ENV_PATH_JSON", "/etc/cufile.json")
+    )
+
+    if kvikio.defaults.compat_mode():
+        print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
+        print("   WARNING - KvikIO compat mode   ")
+        print("      libcufile.so not used       ")
+        print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
+    elif not props.is_gds_available:
+        print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
+        print("   WARNING - cuFile compat mode   ")
+        print("         GDS not enabled          ")
+        print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
+    print(f"GPU               | {gpu_name}")
+    print(f"GPU Memory Total  | {mem_total}")
+    print(f"BAR1 Memory Total | {bar1_total}")
+    print(f"GDS driver        | {gds_version}")
+    print(f"GDS config.json   | {gds_config_json_path}")
+
+
+def parse_directory(x: str | None) -> pathlib.Path | None:
+    """Given an argparse argument, return a dir path.
+
+    None are passed through untouched.
+    Raise argparse.ArgumentTypeError if `x` isn't a directory (or None).
+
+    Parameters
+    ----------
+    x
+        argparse argument
+
+    Returns
+    -------
+    The directory path or None
+    """
+    if x is None:
+        return x
+    else:
+        p = pathlib.Path(x)
+        if not p.is_dir():
+            raise argparse.ArgumentTypeError("Must be a directory")
+        return p
diff --git a/python/benchmarks/zarr-io.py b/python/kvikio/kvikio/benchmarks/zarr_io.py
similarity index 75%
rename from python/benchmarks/zarr-io.py
rename to python/kvikio/kvikio/benchmarks/zarr_io.py
index 983c735364..fc226c2263 100644
--- a/python/benchmarks/zarr-io.py
+++ b/python/kvikio/kvikio/benchmarks/zarr_io.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2023, NVIDIA CORPORATION. All rights reserved.
+# Copyright (c) 2023-2024, NVIDIA CORPORATION. All rights reserved.
 # See file LICENSE for terms.
 
 import argparse
@@ -8,7 +8,6 @@
 import pathlib
 import shutil
 import statistics
-import subprocess
 import tempfile
 from time import perf_counter as clock
 from typing import ContextManager, Union
@@ -22,6 +21,7 @@
 import kvikio
 import kvikio.defaults
 import kvikio.zarr
+from kvikio.benchmarks.utils import drop_vm_cache, parse_directory, pprint_sys_info
 
 if not kvikio.zarr.supported:
     raise RuntimeError(f"requires Zarr >={kvikio.zarr.MINIMUM_ZARR_VERSION}")
@@ -32,11 +32,6 @@
 }
 
 
-def drop_vm_cache(args):
-    if args.drop_vm_cache:
-        subprocess.check_output(["sudo /sbin/sysctl vm.drop_caches=3"], shell=True)
-
-
 def create_src_data(args):
     return cupy.random.random(args.nelem, dtype=args.dtype)
 
@@ -51,7 +46,8 @@ def run_kvikio(args):
     src = create_src_data(args)
 
     # Write
-    drop_vm_cache(args)
+    if args.drop_vm_cache:
+        drop_vm_cache()
     t0 = clock()
     z = zarr.create(
         shape=(args.nelem,),
@@ -66,7 +62,8 @@ def run_kvikio(args):
     write_time = clock() - t0
 
     # Read
-    drop_vm_cache(args)
+    if args.drop_vm_cache:
+        drop_vm_cache()
     t0 = clock()
     res = z[:]
     read_time = clock() - t0
@@ -85,7 +82,8 @@ def run_posix(args):
     src = create_src_data(args)
 
     # Write
-    drop_vm_cache(args)
+    if args.drop_vm_cache:
+        drop_vm_cache()
     t0 = clock()
     z = zarr.create(
         shape=(args.nelem,),
@@ -100,7 +98,8 @@ def run_posix(args):
     write_time = clock() - t0
 
     # Read
-    drop_vm_cache(args)
+    if args.drop_vm_cache:
+        drop_vm_cache()
     t0 = clock()
     res = cupy.asarray(z[:])
     read_time = clock() - t0
@@ -120,57 +119,14 @@ def main(args):
     cupy.arange(10)  # Make sure CUDA is initialized
 
     kvikio.defaults.num_threads_reset(args.nthreads)
-    props = kvikio.DriverProperties()
-    try:
-        import pynvml.smi
-
-        nvsmi = pynvml.smi.nvidia_smi.getInstance()
-    except ImportError:
-        gpu_name = "Unknown (install pynvml)"
-        mem_total = gpu_name
-        bar1_total = gpu_name
-    else:
-        info = nvsmi.DeviceQuery()["gpu"][0]
-        gpu_name = f"{info['product_name']} (dev #0)"
-        mem_total = format_bytes(
-            parse_bytes(
-                str(info["fb_memory_usage"]["total"]) + info["fb_memory_usage"]["unit"]
-            )
-        )
-        bar1_total = format_bytes(
-            parse_bytes(
-                str(info["bar1_memory_usage"]["total"])
-                + info["bar1_memory_usage"]["unit"]
-            )
-        )
-    gds_version = "N/A (Compatibility Mode)"
-    if props.is_gds_available:
-        gds_version = f"v{props.major_version}.{props.minor_version}"
-    gds_config_json_path = os.path.realpath(
-        os.getenv("CUFILE_ENV_PATH_JSON", "/etc/cufile.json")
-    )
     drop_vm_cache_msg = str(args.drop_vm_cache)
     if not args.drop_vm_cache:
         drop_vm_cache_msg += " (use --drop-vm-cache for better accuracy!)"
     chunksize = args.chunksize * args.dtype.itemsize
 
-    print("Roundtrip benchmark")
+    print("Zarr-IO Benchmark")
     print("----------------------------------")
-    if kvikio.defaults.compat_mode():
-        print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
-        print("   WARNING - KvikIO compat mode   ")
-        print("      libcufile.so not used       ")
-        print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
-    elif not props.is_gds_available:
-        print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
-        print("   WARNING - cuFile compat mode   ")
-        print("         GDS not enabled          ")
-        print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
-    print(f"GPU               | {gpu_name}")
-    print(f"GPU Memory Total  | {mem_total}")
-    print(f"BAR1 Memory Total | {bar1_total}")
-    print(f"GDS driver        | {gds_version}")
-    print(f"GDS config.json   | {gds_config_json_path}")
+    pprint_sys_info()
     print("----------------------------------")
     print(f"nbytes            | {args.nbytes} bytes ({format_bytes(args.nbytes)})")
     print(f"chunksize         | {chunksize} bytes ({format_bytes(chunksize)})")
@@ -211,16 +167,6 @@ def pprint_api_res(name, samples):
 
 
 if __name__ == "__main__":
-
-    def parse_directory(x):
-        if x is None:
-            return x
-        else:
-            p = pathlib.Path(x)
-            if not p.is_dir():
-                raise argparse.ArgumentTypeError("Must be a directory")
-            return p
-
     parser = argparse.ArgumentParser(description="Roundtrip benchmark")
     parser.add_argument(
         "-n",
diff --git a/python/kvikio/cufile.py b/python/kvikio/kvikio/cufile.py
similarity index 100%
rename from python/kvikio/cufile.py
rename to python/kvikio/kvikio/cufile.py
diff --git a/python/kvikio/defaults.py b/python/kvikio/kvikio/defaults.py
similarity index 100%
rename from python/kvikio/defaults.py
rename to python/kvikio/kvikio/defaults.py
diff --git a/python/kvikio/numcodecs.py b/python/kvikio/kvikio/numcodecs.py
similarity index 100%
rename from python/kvikio/numcodecs.py
rename to python/kvikio/kvikio/numcodecs.py
diff --git a/python/kvikio/numpy.py b/python/kvikio/kvikio/numpy.py
similarity index 100%
rename from python/kvikio/numpy.py
rename to python/kvikio/kvikio/numpy.py
diff --git a/python/kvikio/nvcomp.py b/python/kvikio/kvikio/nvcomp.py
similarity index 100%
rename from python/kvikio/nvcomp.py
rename to python/kvikio/kvikio/nvcomp.py
diff --git a/python/kvikio/nvcomp_codec.py b/python/kvikio/kvikio/nvcomp_codec.py
similarity index 100%
rename from python/kvikio/nvcomp_codec.py
rename to python/kvikio/kvikio/nvcomp_codec.py
diff --git a/python/kvikio/zarr.py b/python/kvikio/kvikio/zarr.py
similarity index 100%
rename from python/kvikio/zarr.py
rename to python/kvikio/kvikio/zarr.py
diff --git a/python/pyproject.toml b/python/kvikio/pyproject.toml
similarity index 77%
rename from python/pyproject.toml
rename to python/kvikio/pyproject.toml
index 743c832ece..824641964d 100644
--- a/python/pyproject.toml
+++ b/python/kvikio/pyproject.toml
@@ -1,14 +1,12 @@
-# Copyright (c) 2021-2022, NVIDIA CORPORATION. All rights reserved.
+# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved.
 # See file LICENSE for terms.
 
 [build-system]
-build-backend = "scikit_build_core.build"
+build-backend = "rapids_build_backend.build"
 requires = [
-    "cmake>=3.26.4",
-    "cython>=3.0.0",
-    "ninja",
+    "rapids-build-backend>=0.3.0,<0.4.0.dev0",
     "scikit-build-core[pyproject]>=0.7.0",
-] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../dependencies.yaml and run `rapids-dependency-file-generator`.
+] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`.
 
 [project]
 name = "kvikio"
@@ -26,7 +24,7 @@ dependencies = [
     "numpy>=1.23,<2.0a0",
     "packaging",
     "zarr",
-] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../dependencies.yaml and run `rapids-dependency-file-generator`.
+] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`.
 classifiers = [
     "Intended Audience :: Developers",
     "Topic :: Database",
@@ -44,7 +42,7 @@ test = [
     "dask>=2022.05.2",
     "pytest",
     "pytest-cov",
-] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../dependencies.yaml and run `rapids-dependency-file-generator`.
+] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`.
 
 [project.urls]
 Homepage = "https://github.com/rapidsai/kvikio"
@@ -109,6 +107,17 @@ ignore_missing_imports = true
 [project.entry-points."numcodecs.codecs"]
 nvcomp_batch = "kvikio.nvcomp_codec:NvCompBatchCodec"
 
+[tool.rapids-build-backend]
+build-backend = "scikit_build_core.build"
+dependencies-file = "../../dependencies.yaml"
+matrix-entry = "cuda_suffixed=true"
+requires = [
+    "cmake>=3.26.4,!=3.30.0",
+    "cython>=3.0.0",
+    "libkvikio==24.8.*,>=0.0.0a0",
+    "ninja",
+] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`.
+
 [tool.scikit-build]
 build-dir = "build/{wheel_tag}"
 cmake.build-type = "Release"
diff --git a/python/libkvikio/CMakeLists.txt b/python/libkvikio/CMakeLists.txt
new file mode 100644
index 0000000000..278e09f462
--- /dev/null
+++ b/python/libkvikio/CMakeLists.txt
@@ -0,0 +1,43 @@
+# =============================================================================
+# Copyright (c) 2024, NVIDIA CORPORATION.
+#
+# 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.
+# =============================================================================
+
+cmake_minimum_required(VERSION 3.26.4 FATAL_ERROR)
+
+include(../../cpp/cmake/rapids_config.cmake)
+include(rapids-cpm)
+include(rapids-cuda)
+rapids_cuda_init_architectures(libkvikio-python)
+rapids_cpm_init()
+
+project(
+  libkvikio-python
+  VERSION "${RAPIDS_VERSION}"
+  LANGUAGES CXX
+)
+
+# Check if kvikio is already available. If so, it's the user's responsibility to ensure that the
+# CMake package is also available at build time of the Python kvikio package.
+find_package(kvikio "${RAPIDS_VERSION}")
+
+if(kvikio_FOUND)
+  return()
+endif()
+
+unset(kvikio_FOUND)
+
+set(KvikIO_BUILD_EXAMPLES OFF)
+set(KvikIO_BUILD_TESTS OFF)
+set(CUDA_STATIC_RUNTIME ON)
+
+add_subdirectory(../../cpp kvikio-cpp)
diff --git a/python/libkvikio/LICENSE b/python/libkvikio/LICENSE
new file mode 120000
index 0000000000..30cff7403d
--- /dev/null
+++ b/python/libkvikio/LICENSE
@@ -0,0 +1,1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/python/libkvikio/README.md b/python/libkvikio/README.md
new file mode 120000
index 0000000000..fe84005413
--- /dev/null
+++ b/python/libkvikio/README.md
@@ -0,0 +1,1 @@
+../../README.md
\ No newline at end of file
diff --git a/python/libkvikio/libkvikio/VERSION b/python/libkvikio/libkvikio/VERSION
new file mode 120000
index 0000000000..d62dc733ef
--- /dev/null
+++ b/python/libkvikio/libkvikio/VERSION
@@ -0,0 +1,1 @@
+../../../VERSION
\ No newline at end of file
diff --git a/python/kvikio/_version.py b/python/libkvikio/libkvikio/__init__.py
similarity index 74%
rename from python/kvikio/_version.py
rename to python/libkvikio/libkvikio/__init__.py
index 82fc478d5c..995cd1027d 100644
--- a/python/kvikio/_version.py
+++ b/python/libkvikio/libkvikio/__init__.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2023-2024, NVIDIA CORPORATION.
+# Copyright (c) 2024, NVIDIA CORPORATION.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -12,9 +12,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import importlib.resources
+from libkvikio._version import __git_commit__, __version__
 
-__version__ = (
-    importlib.resources.files("kvikio").joinpath("VERSION").read_text().strip()
-)
-__git_commit__ = ""
+__all__ = ["__git_commit__", "__version__"]
diff --git a/python/libkvikio/libkvikio/_version.py b/python/libkvikio/libkvikio/_version.py
new file mode 100644
index 0000000000..8b67326806
--- /dev/null
+++ b/python/libkvikio/libkvikio/_version.py
@@ -0,0 +1,30 @@
+# Copyright (c) 2024, NVIDIA CORPORATION.
+#
+# 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.
+
+import importlib.resources
+
+__version__ = (
+    importlib.resources.files(__package__).joinpath("VERSION").read_text().strip()
+)
+try:
+    __git_commit__ = (
+        importlib.resources.files(__package__)
+        .joinpath("GIT_COMMIT")
+        .read_text()
+        .strip()
+    )
+except FileNotFoundError:
+    __git_commit__ = ""
+
+__all__ = ["__version__", "__git_commit__"]
diff --git a/python/libkvikio/pyproject.toml b/python/libkvikio/pyproject.toml
new file mode 100644
index 0000000000..b40b42dbbd
--- /dev/null
+++ b/python/libkvikio/pyproject.toml
@@ -0,0 +1,56 @@
+# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.
+# See file LICENSE for terms.
+
+[build-system]
+build-backend = "rapids_build_backend.build"
+requires = [
+    "rapids-build-backend>=0.3.0,<0.4.0.dev0",
+    "scikit-build-core[pyproject]>=0.7.0",
+] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`.
+
+[project]
+name = "libkvikio"
+dynamic = ["version"]
+description = "KvikIO - GPUDirect Storage (C++)"
+readme = { file = "README.md", content-type = "text/markdown" }
+authors = [
+    { name = "NVIDIA Corporation" },
+]
+license = { text = "Apache 2.0" }
+classifiers = [
+    "Intended Audience :: Developers",
+    "Topic :: Database",
+    "Topic :: Scientific/Engineering",
+    "License :: OSI Approved :: Apache Software License",
+    "Programming Language :: Python",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+]
+
+[project.urls]
+Homepage = "https://github.com/rapidsai/kvikio"
+
+[tool.scikit-build]
+build-dir = "build/{wheel_tag}"
+cmake.build-type = "Release"
+cmake.minimum-version = "3.26.4"
+ninja.make-fallback = true
+sdist.exclude = ["*tests*"]
+sdist.reproducible = true
+wheel.packages = ["libkvikio"]
+wheel.py-api = "py3"
+
+[tool.scikit-build.metadata.version]
+provider = "scikit_build_core.metadata.regex"
+input = "libkvikio/VERSION"
+regex = "(?P<value>.*)"
+
+[tool.rapids-build-backend]
+build-backend = "scikit_build_core.build"
+dependencies-file = "../../dependencies.yaml"
+matrix-entry = "cuda_suffixed=true"
+requires = [
+    "cmake>=3.26.4,!=3.30.0",
+    "ninja",
+] # This list was generated by `rapids-dependency-file-generator`. To make changes, edit ../../dependencies.yaml and run `rapids-dependency-file-generator`.


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index f8fb008f20..959c63a39f 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -16,7 +16,7 @@ on:
 jobs:
   cpp-tests:
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@branch-24.06
+    uses: rapidsai/shared-workflows/.github/workflows/conda-cpp-tests.yaml@branch-24.08
     with:
       build_type: nightly
       branch: ${{ inputs.branch }}
@@ -24,7 +24,7 @@ jobs:
       sha: ${{ inputs.sha }}
   python-tests:
     secrets: inherit
-    uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.06
+    uses: rapidsai/shared-workflows/.github/workflows/conda-python-tests.yaml@branch-24.08
     with:
       build_type: nightly
       branch: ${{ inputs.branch }}
diff --git a/ci/run_pytests.sh b/ci/run_pytests.sh
index 73de68d03b..b2c93dbe56 100755
--- a/ci/run_pytests.sh
+++ b/ci/run_pytests.sh
@@ -4,6 +4,6 @@
 set -euo pipefail
 
 # Support invoking run_pytests.sh outside the script directory
-cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")"/../python/
+cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")"/../python/kvikio
 
 pytest --cache-clear --verbose "$@" tests
diff --git a/ci/test_cpp.sh b/ci/test_cpp.sh
index fdc6584a14..e7de3e68f2 100755
--- a/ci/test_cpp.sh
+++ b/ci/test_cpp.sh
@@ -8,7 +8,7 @@ set -euo pipefail
 rapids-logger "Generate C++ testing dependencies"
 rapids-dependency-file-generator \
   --output conda \
-  --file_key test_cpp \
+  --file-key test_cpp \
   --matrix "cuda=${RAPIDS_CUDA_VERSION%.*};arch=$(arch)" | tee env.yaml
 
 rapids-mamba-retry env create --yes -f env.yaml -n test
diff --git a/ci/test_python.sh b/ci/test_python.sh
index ecd2bea44e..fccbcb4728 100755
--- a/ci/test_python.sh
+++ b/ci/test_python.sh
@@ -11,7 +11,7 @@ cd "$(dirname "$(realpath "${BASH_SOURCE[0]}")")"/../
 rapids-logger "Generate Python testing dependencies"
 rapids-dependency-file-generator \
   --output conda \
-  --file_key test_python \
+  --file-key test_python \
   --matrix "cuda=${RAPIDS_CUDA_VERSION%.*};arch=$(arch);py=${RAPIDS_PY_VERSION}" | tee env.yaml
 
 rapids-mamba-retry env create --yes -f env.yaml -n test
diff --git a/ci/test_wheel.sh b/ci/test_wheel.sh
new file mode 100755
index 0000000000..94a31b04b6
--- /dev/null
+++ b/ci/test_wheel.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+# Copyright (c) 2023-2024, NVIDIA CORPORATION.
+
+set -eou pipefail
+
+RAPIDS_PY_CUDA_SUFFIX="$(rapids-wheel-ctk-name-gen ${RAPIDS_CUDA_VERSION})"
+WHEELHOUSE="${PWD}/dist/"
+RAPIDS_PY_WHEEL_NAME="kvikio_${RAPIDS_PY_CUDA_SUFFIX}" rapids-download-wheels-from-s3 python "${WHEELHOUSE}"
+
+python -m pip install "$(echo ${WHEELHOUSE}/kvikio_${RAPIDS_PY_CUDA_SUFFIX}*.whl)[test]"
+
+python -m pytest ./python/kvikio/tests
diff --git a/python/tests/conftest.py b/python/kvikio/tests/conftest.py
similarity index 100%
rename from python/tests/conftest.py
rename to python/kvikio/tests/conftest.py
diff --git a/python/tests/test_async_io.py b/python/kvikio/tests/test_async_io.py
similarity index 95%
rename from python/tests/test_async_io.py
rename to python/kvikio/tests/test_async_io.py
index 27a9f22e05..194a921ec5 100644
--- a/python/tests/test_async_io.py
+++ b/python/kvikio/tests/test_async_io.py
@@ -30,7 +30,7 @@ def test_read_write(tmp_path, size):
     assert f.raw_write_async(a, stream.ptr).check_bytes_done() == a.nbytes
 
     # Try to read file opened in write-only mode
-    with pytest.raises(RuntimeError, match="unsupported file open flags"):
+    with pytest.raises(RuntimeError, match="Operation not permitted"):
         # The exception is raised when we call the raw_read_async API.
         future_stream = f.raw_read_async(a, stream.ptr)
         future_stream.check_bytes_done()
diff --git a/python/tests/test_basic_io.py b/python/kvikio/tests/test_basic_io.py
similarity index 98%
rename from python/tests/test_basic_io.py
rename to python/kvikio/tests/test_basic_io.py
index 71932828c5..49c3bb544d 100644
--- a/python/tests/test_basic_io.py
+++ b/python/kvikio/tests/test_basic_io.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2021-2023, NVIDIA CORPORATION. All rights reserved.
+# Copyright (c) 2021-2024, NVIDIA CORPORATION. All rights reserved.
 # See file LICENSE for terms.
 
 import os
@@ -36,7 +36,7 @@ def test_read_write(tmp_path, xp, gds_threshold, size, nthreads, tasksize):
             assert f.write(a) == a.nbytes
 
             # Try to read file opened in write-only mode
-            with pytest.raises(RuntimeError, match="unsupported file open flags"):
+            with pytest.raises(RuntimeError, match="Operation not permitted"):
                 f.read(a)
 
             # Close file
diff --git a/python/kvikio/tests/test_benchmarks.py b/python/kvikio/tests/test_benchmarks.py
new file mode 100644
index 0000000000..3bdaf6613e
--- /dev/null
+++ b/python/kvikio/tests/test_benchmarks.py
@@ -0,0 +1,80 @@
+# Copyright (c) 2022-2024, NVIDIA CORPORATION. All rights reserved.
+# See file LICENSE for terms.
+
+import os
+import os.path
+import sys
+from pathlib import Path
+
+import pytest
+
+benchmarks_path = (
+    Path(os.path.realpath(__file__)).parent.parent / "kvikio" / "benchmarks"
+)
+pytest.importorskip("cupy")
+pytest.importorskip("dask")
+
+
+@pytest.mark.parametrize(
+    "api",
+    [
+        "cufile",
+        "posix",
+        "cufile-mfma",
+        "cufile-mf",
+        "cufile-ma",
+        "zarr",
+    ],
+)
+def test_single_node_io(run_cmd, tmp_path, api):
+    """Test benchmarks/single_node_io.py"""
+
+    if "zarr" in api:
+        kz = pytest.importorskip("kvikio.zarr")
+        if not kz.supported:
+            pytest.skip(f"requires Zarr >={kz.MINIMUM_ZARR_VERSION}")
+
+    retcode = run_cmd(
+        cmd=[
+            sys.executable or "python",
+            "single_node_io.py",
+            "-n",
+            "1MiB",
+            "-d",
+            str(tmp_path),
+            "--api",
+            api,
+        ],
+        cwd=benchmarks_path,
+    )
+    assert retcode == 0
+
+
+@pytest.mark.parametrize(
+    "api",
+    [
+        "kvikio",
+        "posix",
+    ],
+)
+def test_zarr_io(run_cmd, tmp_path, api):
+    """Test benchmarks/zarr_io.py"""
+
+    kz = pytest.importorskip("kvikio.zarr")
+    if not kz.supported:
+        pytest.skip(f"requires Zarr >={kz.MINIMUM_ZARR_VERSION}")
+
+    retcode = run_cmd(
+        cmd=[
+            sys.executable or "python",
+            "zarr_io.py",
+            "-n",
+            "1MiB",
+            "-d",
+            str(tmp_path),
+            "--api",
+            api,
+        ],
+        cwd=benchmarks_path,
+    )
+    assert retcode == 0
diff --git a/python/tests/test_defaults.py b/python/kvikio/tests/test_defaults.py
similarity index 100%
rename from python/tests/test_defaults.py
rename to python/kvikio/tests/test_defaults.py
diff --git a/python/tests/test_examples.py b/python/kvikio/tests/test_examples.py
similarity index 100%
rename from python/tests/test_examples.py
rename to python/kvikio/tests/test_examples.py
diff --git a/python/tests/test_numpy.py b/python/kvikio/tests/test_numpy.py
similarity index 100%
rename from python/tests/test_numpy.py
rename to python/kvikio/tests/test_numpy.py
diff --git a/python/tests/test_nvcomp.py b/python/kvikio/tests/test_nvcomp.py
similarity index 100%
rename from python/tests/test_nvcomp.py
rename to python/kvikio/tests/test_nvcomp.py
diff --git a/python/tests/test_nvcomp_codec.py b/python/kvikio/tests/test_nvcomp_codec.py
similarity index 100%
rename from python/tests/test_nvcomp_codec.py
rename to python/kvikio/tests/test_nvcomp_codec.py
diff --git a/python/kvikio/tests/test_version.py b/python/kvikio/tests/test_version.py
new file mode 100644
index 0000000000..840d6624c9
--- /dev/null
+++ b/python/kvikio/tests/test_version.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.
+# See file LICENSE for terms.
+
+import kvikio
+
+
+def test_version_constants_are_populated():
+    # __git_commit__ will only be non-empty in a built distribution
+    assert isinstance(kvikio.__git_commit__, str)
+
+    # __version__ should always be non-empty
+    assert isinstance(kvikio.__version__, str)
+    assert len(kvikio.__version__) > 0
diff --git a/python/tests/test_zarr.py b/python/kvikio/tests/test_zarr.py
similarity index 100%
rename from python/tests/test_zarr.py
rename to python/kvikio/tests/test_zarr.py
diff --git a/python/tests/test_benchmarks.py b/python/tests/test_benchmarks.py
deleted file mode 100644
index ee0321d40a..0000000000
--- a/python/tests/test_benchmarks.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# Copyright (c) 2022-2023, NVIDIA CORPORATION. All rights reserved.
-# See file LICENSE for terms.
-
-import os
-import os.path
-import sys
-from pathlib import Path
-
-import pytest
-
-benchmarks_path = Path(os.path.realpath(__file__)).parent / ".." / "benchmarks"
-
-pytest.importorskip("cupy")
-pytest.importorskip("dask")
-
-
-@pytest.mark.parametrize(
-    "api",
-    [
-        "cufile",
-        "posix",
-        "cufile-mfma",
-        "cufile-mf",
-        "cufile-ma",
-        "zarr",
-    ],
-)
-def test_single_node_io(run_cmd, tmp_path, api):
-    """Test benchmarks/single-node-io.py"""
-
-    if "zarr" in api:
-        kz = pytest.importorskip("kvikio.zarr")
-        if not kz.supported:
-            pytest.skip(f"requires Zarr >={kz.MINIMUM_ZARR_VERSION}")
-
-    retcode = run_cmd(
-        cmd=[
-            sys.executable or "python",
-            "single-node-io.py",
-            "-n",
-            "1MiB",
-            "-d",
-            str(tmp_path),
-            "--api",
-            api,
-        ],
-        cwd=benchmarks_path,
-    )
-    assert retcode == 0