nvmeof 2026.7.21__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nvmeof-2026.7.21/LICENSE +28 -0
- nvmeof-2026.7.21/PKG-INFO +142 -0
- nvmeof-2026.7.21/README.md +116 -0
- nvmeof-2026.7.21/pyproject.toml +47 -0
- nvmeof-2026.7.21/setup.cfg +4 -0
- nvmeof-2026.7.21/src/nvmeof/__init__.py +27 -0
- nvmeof-2026.7.21/src/nvmeof/_version.py +1 -0
- nvmeof-2026.7.21/src/nvmeof/controller.py +314 -0
- nvmeof-2026.7.21/src/nvmeof/protocol.py +306 -0
- nvmeof-2026.7.21/src/nvmeof/rdma.py +297 -0
- nvmeof-2026.7.21/src/nvmeof.egg-info/PKG-INFO +142 -0
- nvmeof-2026.7.21/src/nvmeof.egg-info/SOURCES.txt +17 -0
- nvmeof-2026.7.21/src/nvmeof.egg-info/dependency_links.txt +1 -0
- nvmeof-2026.7.21/src/nvmeof.egg-info/requires.txt +8 -0
- nvmeof-2026.7.21/src/nvmeof.egg-info/top_level.txt +1 -0
- nvmeof-2026.7.21/tests/test_integration.py +66 -0
- nvmeof-2026.7.21/tests/test_namespace.py +40 -0
- nvmeof-2026.7.21/tests/test_protocol.py +109 -0
- nvmeof-2026.7.21/tests/test_queue.py +130 -0
nvmeof-2026.7.21/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Tristan Rice
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nvmeof
|
|
3
|
+
Version: 2026.7.21
|
|
4
|
+
Summary: NVMe over Fabrics RDMA initiator with direct GPU memory support
|
|
5
|
+
Author: Tristan Rice
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Documentation, https://d4l3k.github.io/rdma4py/nvmeof/
|
|
8
|
+
Project-URL: Homepage, https://github.com/d4l3k/rdma4py
|
|
9
|
+
Project-URL: Repository, https://github.com/d4l3k/rdma4py
|
|
10
|
+
Keywords: nvme,nvmeof,rdma,ibverbs,gpudirect,gpu
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: System :: Hardware
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: ibverbs>=0.1.0
|
|
20
|
+
Provides-Extra: test
|
|
21
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
22
|
+
Requires-Dist: numpy; extra == "test"
|
|
23
|
+
Provides-Extra: gpu
|
|
24
|
+
Requires-Dist: torch; extra == "gpu"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# nvmeof
|
|
28
|
+
|
|
29
|
+
`nvmeof` is a userspace NVMe over Fabrics RDMA initiator for Python. It is a
|
|
30
|
+
separate package layered on
|
|
31
|
+
[`ibverbs`](https://github.com/d4l3k/rdma4py/tree/main/ibverbs) because NVMe controller and
|
|
32
|
+
namespace policy do not belong in low-level verbs bindings.
|
|
33
|
+
|
|
34
|
+
The data path uses NVMe/RDMA keyed SGL descriptors that point at an existing
|
|
35
|
+
ibverbs MR. The target transfers blocks directly to or from that MR, including
|
|
36
|
+
CUDA memory registered by `ibverbs.cuda`; there is no host staging buffer in
|
|
37
|
+
the NVMe-to-RDMA-to-GPU path.
|
|
38
|
+
|
|
39
|
+
## Why this does not bundle SPDK
|
|
40
|
+
|
|
41
|
+
SPDK is a strong choice for a managed native storage service, but it includes
|
|
42
|
+
an environment abstraction, hugepage/driver setup, native plugins, and a large
|
|
43
|
+
dependency graph. Embedding it would not produce a self-contained, portable
|
|
44
|
+
`pip`/manylinux wheel. This package instead implements the small NVMe/RDMA host
|
|
45
|
+
protocol directly over the existing portable `ibverbs` ABI3 extension.
|
|
46
|
+
|
|
47
|
+
`nvmeof` itself is a pure-Python `py3-none-any` wheel. Its `ibverbs` dependency
|
|
48
|
+
is an ABI3 manylinux wheel that `dlopen`s the host's `libibverbs.so.1` and,
|
|
49
|
+
only when NVMe/RDMA is used, `librdmacm.so.1`.
|
|
50
|
+
|
|
51
|
+
## Requirements
|
|
52
|
+
|
|
53
|
+
- Linux, an RDMA NIC, and a reachable NVMe/RDMA target.
|
|
54
|
+
- `libibverbs.so.1`, `librdmacm.so.1`, and the matching RDMA provider.
|
|
55
|
+
- For GPU I/O: CUDA, an HCA with GPUDirect RDMA, and dma-buf support or the
|
|
56
|
+
`nvidia_peermem` module.
|
|
57
|
+
- The target must support keyed SGL data block descriptors. Separate namespace
|
|
58
|
+
metadata and DH-HMAC-CHAP are not currently supported.
|
|
59
|
+
|
|
60
|
+
On Debian/Ubuntu the runtime packages are typically:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
sudo apt-get install libibverbs1 librdmacm1 ibverbs-providers
|
|
64
|
+
python -m pip install nvmeof
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Host memory
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import nvmeof
|
|
71
|
+
|
|
72
|
+
with nvmeof.Controller.connect(
|
|
73
|
+
"192.0.2.20",
|
|
74
|
+
"nqn.2026-07.io.example:storage",
|
|
75
|
+
source="192.0.2.21",
|
|
76
|
+
) as controller:
|
|
77
|
+
namespace = controller.namespace(1)
|
|
78
|
+
with controller.allocate(128 * 4096) as buffer:
|
|
79
|
+
namespace.read(buffer, slba=0, blocks=128)
|
|
80
|
+
payload = buffer.read()
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`controller.register(array_or_cpu_tensor)` registers an existing contiguous
|
|
84
|
+
host allocation instead.
|
|
85
|
+
|
|
86
|
+
## Direct GPU memory
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import os
|
|
90
|
+
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
|
|
91
|
+
|
|
92
|
+
import torch
|
|
93
|
+
import nvmeof
|
|
94
|
+
|
|
95
|
+
tensor = torch.empty(128 * 4096, dtype=torch.uint8, device="cuda:0")
|
|
96
|
+
|
|
97
|
+
with nvmeof.Controller.connect(
|
|
98
|
+
"192.0.2.20", "nqn.2026-07.io.example:storage"
|
|
99
|
+
) as controller:
|
|
100
|
+
namespace = controller.namespace(1)
|
|
101
|
+
with controller.register_gpu(tensor) as gpu_mr:
|
|
102
|
+
# Target NVMe -> target RDMA WRITE -> local GPU memory.
|
|
103
|
+
# Namespace.read flushes completed GPUDirect writes before returning.
|
|
104
|
+
with torch.cuda.device(tensor.device):
|
|
105
|
+
namespace.read(gpu_mr, slba=0, blocks=128)
|
|
106
|
+
|
|
107
|
+
# Local GPU memory -> target RDMA READ -> target NVMe.
|
|
108
|
+
# Namespace.write synchronizes the current CUDA context first.
|
|
109
|
+
with torch.cuda.device(tensor.device):
|
|
110
|
+
namespace.write(gpu_mr, slba=1024, blocks=128)
|
|
111
|
+
namespace.flush()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The CUDA context that owns the tensor must be current while issuing GPU I/O.
|
|
115
|
+
The registered MR must be closed before its controller.
|
|
116
|
+
|
|
117
|
+
`source` is optional. Set it to an address on the intended initiator HCA when
|
|
118
|
+
the host has multiple RDMA NICs. This makes an NVMe -> target NIC -> initiator
|
|
119
|
+
NIC -> GPU route explicit and prevents route selection from silently choosing
|
|
120
|
+
an HCA with poor GPU PCIe locality.
|
|
121
|
+
|
|
122
|
+
Reads and writes larger than the target's MDTS are split into multiple NVMe
|
|
123
|
+
commands while retaining direct placement in the same registered buffer. The
|
|
124
|
+
target used for the repository benchmarks advertises a 1 MiB MDTS; 4, 16, and
|
|
125
|
+
64 MiB GPU tensors were verified end to end.
|
|
126
|
+
|
|
127
|
+
## Scope
|
|
128
|
+
|
|
129
|
+
The first implementation provides dynamic controller connection, controller
|
|
130
|
+
enablement, Identify Controller/Namespace, one I/O queue, synchronous and
|
|
131
|
+
low-level asynchronous command submission, block read/write splitting, and
|
|
132
|
+
flush. It deliberately does not claim filesystem semantics, multipath,
|
|
133
|
+
reconnect, keep-alive, protection information, authentication, or target
|
|
134
|
+
functionality. KATO is therefore negotiated as zero.
|
|
135
|
+
|
|
136
|
+
Real-target tests are opt-in through `NVME4PY_TARGET` and
|
|
137
|
+
`NVME4PY_SUBSYSTEM_NQN`. Set `NVME4PY_SOURCE` to bind the initiator to a
|
|
138
|
+
specific HCA address. The GPU round-trip test also requires
|
|
139
|
+
`NVME4PY_DESTRUCTIVE=1` and must only be used with a disposable namespace.
|
|
140
|
+
|
|
141
|
+
Measured multi-QP bandwidth, latency, QP scaling, and PCIe locality results
|
|
142
|
+
are in [`BENCHMARKS.md`](BENCHMARKS.md).
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# nvmeof
|
|
2
|
+
|
|
3
|
+
`nvmeof` is a userspace NVMe over Fabrics RDMA initiator for Python. It is a
|
|
4
|
+
separate package layered on
|
|
5
|
+
[`ibverbs`](https://github.com/d4l3k/rdma4py/tree/main/ibverbs) because NVMe controller and
|
|
6
|
+
namespace policy do not belong in low-level verbs bindings.
|
|
7
|
+
|
|
8
|
+
The data path uses NVMe/RDMA keyed SGL descriptors that point at an existing
|
|
9
|
+
ibverbs MR. The target transfers blocks directly to or from that MR, including
|
|
10
|
+
CUDA memory registered by `ibverbs.cuda`; there is no host staging buffer in
|
|
11
|
+
the NVMe-to-RDMA-to-GPU path.
|
|
12
|
+
|
|
13
|
+
## Why this does not bundle SPDK
|
|
14
|
+
|
|
15
|
+
SPDK is a strong choice for a managed native storage service, but it includes
|
|
16
|
+
an environment abstraction, hugepage/driver setup, native plugins, and a large
|
|
17
|
+
dependency graph. Embedding it would not produce a self-contained, portable
|
|
18
|
+
`pip`/manylinux wheel. This package instead implements the small NVMe/RDMA host
|
|
19
|
+
protocol directly over the existing portable `ibverbs` ABI3 extension.
|
|
20
|
+
|
|
21
|
+
`nvmeof` itself is a pure-Python `py3-none-any` wheel. Its `ibverbs` dependency
|
|
22
|
+
is an ABI3 manylinux wheel that `dlopen`s the host's `libibverbs.so.1` and,
|
|
23
|
+
only when NVMe/RDMA is used, `librdmacm.so.1`.
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
- Linux, an RDMA NIC, and a reachable NVMe/RDMA target.
|
|
28
|
+
- `libibverbs.so.1`, `librdmacm.so.1`, and the matching RDMA provider.
|
|
29
|
+
- For GPU I/O: CUDA, an HCA with GPUDirect RDMA, and dma-buf support or the
|
|
30
|
+
`nvidia_peermem` module.
|
|
31
|
+
- The target must support keyed SGL data block descriptors. Separate namespace
|
|
32
|
+
metadata and DH-HMAC-CHAP are not currently supported.
|
|
33
|
+
|
|
34
|
+
On Debian/Ubuntu the runtime packages are typically:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
sudo apt-get install libibverbs1 librdmacm1 ibverbs-providers
|
|
38
|
+
python -m pip install nvmeof
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Host memory
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import nvmeof
|
|
45
|
+
|
|
46
|
+
with nvmeof.Controller.connect(
|
|
47
|
+
"192.0.2.20",
|
|
48
|
+
"nqn.2026-07.io.example:storage",
|
|
49
|
+
source="192.0.2.21",
|
|
50
|
+
) as controller:
|
|
51
|
+
namespace = controller.namespace(1)
|
|
52
|
+
with controller.allocate(128 * 4096) as buffer:
|
|
53
|
+
namespace.read(buffer, slba=0, blocks=128)
|
|
54
|
+
payload = buffer.read()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`controller.register(array_or_cpu_tensor)` registers an existing contiguous
|
|
58
|
+
host allocation instead.
|
|
59
|
+
|
|
60
|
+
## Direct GPU memory
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import os
|
|
64
|
+
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
|
|
65
|
+
|
|
66
|
+
import torch
|
|
67
|
+
import nvmeof
|
|
68
|
+
|
|
69
|
+
tensor = torch.empty(128 * 4096, dtype=torch.uint8, device="cuda:0")
|
|
70
|
+
|
|
71
|
+
with nvmeof.Controller.connect(
|
|
72
|
+
"192.0.2.20", "nqn.2026-07.io.example:storage"
|
|
73
|
+
) as controller:
|
|
74
|
+
namespace = controller.namespace(1)
|
|
75
|
+
with controller.register_gpu(tensor) as gpu_mr:
|
|
76
|
+
# Target NVMe -> target RDMA WRITE -> local GPU memory.
|
|
77
|
+
# Namespace.read flushes completed GPUDirect writes before returning.
|
|
78
|
+
with torch.cuda.device(tensor.device):
|
|
79
|
+
namespace.read(gpu_mr, slba=0, blocks=128)
|
|
80
|
+
|
|
81
|
+
# Local GPU memory -> target RDMA READ -> target NVMe.
|
|
82
|
+
# Namespace.write synchronizes the current CUDA context first.
|
|
83
|
+
with torch.cuda.device(tensor.device):
|
|
84
|
+
namespace.write(gpu_mr, slba=1024, blocks=128)
|
|
85
|
+
namespace.flush()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The CUDA context that owns the tensor must be current while issuing GPU I/O.
|
|
89
|
+
The registered MR must be closed before its controller.
|
|
90
|
+
|
|
91
|
+
`source` is optional. Set it to an address on the intended initiator HCA when
|
|
92
|
+
the host has multiple RDMA NICs. This makes an NVMe -> target NIC -> initiator
|
|
93
|
+
NIC -> GPU route explicit and prevents route selection from silently choosing
|
|
94
|
+
an HCA with poor GPU PCIe locality.
|
|
95
|
+
|
|
96
|
+
Reads and writes larger than the target's MDTS are split into multiple NVMe
|
|
97
|
+
commands while retaining direct placement in the same registered buffer. The
|
|
98
|
+
target used for the repository benchmarks advertises a 1 MiB MDTS; 4, 16, and
|
|
99
|
+
64 MiB GPU tensors were verified end to end.
|
|
100
|
+
|
|
101
|
+
## Scope
|
|
102
|
+
|
|
103
|
+
The first implementation provides dynamic controller connection, controller
|
|
104
|
+
enablement, Identify Controller/Namespace, one I/O queue, synchronous and
|
|
105
|
+
low-level asynchronous command submission, block read/write splitting, and
|
|
106
|
+
flush. It deliberately does not claim filesystem semantics, multipath,
|
|
107
|
+
reconnect, keep-alive, protection information, authentication, or target
|
|
108
|
+
functionality. KATO is therefore negotiated as zero.
|
|
109
|
+
|
|
110
|
+
Real-target tests are opt-in through `NVME4PY_TARGET` and
|
|
111
|
+
`NVME4PY_SUBSYSTEM_NQN`. Set `NVME4PY_SOURCE` to bind the initiator to a
|
|
112
|
+
specific HCA address. The GPU round-trip test also requires
|
|
113
|
+
`NVME4PY_DESTRUCTIVE=1` and must only be used with a disposable namespace.
|
|
114
|
+
|
|
115
|
+
Measured multi-QP bandwidth, latency, QP scaling, and PCIe locality results
|
|
116
|
+
are in [`BENCHMARKS.md`](BENCHMARKS.md).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nvmeof"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "NVMe over Fabrics RDMA initiator with direct GPU memory support"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "BSD-3-Clause"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Tristan Rice" }]
|
|
14
|
+
keywords = ["nvme", "nvmeof", "rdma", "ibverbs", "gpudirect", "gpu"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: POSIX :: Linux",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: System :: Hardware",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["ibverbs>=0.1.0"]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
test = ["pytest>=7", "numpy"]
|
|
26
|
+
gpu = ["torch"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.dynamic]
|
|
29
|
+
version = { attr = "nvmeof._version.__version__" }
|
|
30
|
+
|
|
31
|
+
[tool.setuptools]
|
|
32
|
+
package-dir = { "" = "src" }
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
markers = [
|
|
39
|
+
"integration: exercises a real NVMe/RDMA target",
|
|
40
|
+
"gpu: requires CUDA and GPUDirect RDMA",
|
|
41
|
+
]
|
|
42
|
+
testpaths = ["tests"]
|
|
43
|
+
|
|
44
|
+
[project.urls]
|
|
45
|
+
Documentation = "https://d4l3k.github.io/rdma4py/nvmeof/"
|
|
46
|
+
Homepage = "https://github.com/d4l3k/rdma4py"
|
|
47
|
+
Repository = "https://github.com/d4l3k/rdma4py"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Userspace NVMe over Fabrics RDMA with direct host and GPU I/O."""
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .controller import Controller, Namespace
|
|
5
|
+
from .protocol import (
|
|
6
|
+
Completion,
|
|
7
|
+
ControllerInfo,
|
|
8
|
+
NamespaceInfo,
|
|
9
|
+
NVME_RDMA_PORT,
|
|
10
|
+
NVMeStatusError,
|
|
11
|
+
)
|
|
12
|
+
from .rdma import HostBuffer, QueueFullError, RDMAQueue, Request
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"__version__",
|
|
16
|
+
"Completion",
|
|
17
|
+
"Controller",
|
|
18
|
+
"ControllerInfo",
|
|
19
|
+
"HostBuffer",
|
|
20
|
+
"Namespace",
|
|
21
|
+
"NamespaceInfo",
|
|
22
|
+
"NVMeStatusError",
|
|
23
|
+
"NVME_RDMA_PORT",
|
|
24
|
+
"QueueFullError",
|
|
25
|
+
"RDMAQueue",
|
|
26
|
+
"Request",
|
|
27
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2026.7.21"
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""High-level NVMe-oF controller and namespace APIs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
import ibverbs as ib
|
|
9
|
+
|
|
10
|
+
from . import protocol as p
|
|
11
|
+
from .rdma import HostBuffer, RDMAQueue
|
|
12
|
+
|
|
13
|
+
_DATA_ACCESS = (
|
|
14
|
+
ib.AccessFlags.LOCAL_WRITE
|
|
15
|
+
| ib.AccessFlags.REMOTE_WRITE
|
|
16
|
+
| ib.AccessFlags.REMOTE_READ
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _is_gpu_memory(buffer) -> bool:
|
|
21
|
+
tensor = getattr(buffer, "tensor", None)
|
|
22
|
+
return tensor is not None and bool(getattr(tensor, "is_cuda", True))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Controller:
|
|
26
|
+
"""A userspace NVMe-oF/RDMA controller with one direct I/O queue."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
host: str,
|
|
31
|
+
subsystem_nqn: str,
|
|
32
|
+
*,
|
|
33
|
+
port=p.NVME_RDMA_PORT,
|
|
34
|
+
host_id=None,
|
|
35
|
+
host_nqn=None,
|
|
36
|
+
queue_depth=128,
|
|
37
|
+
keep_alive_ms=0,
|
|
38
|
+
timeout=30.0,
|
|
39
|
+
source=None,
|
|
40
|
+
):
|
|
41
|
+
self.host = host
|
|
42
|
+
self.port = int(port)
|
|
43
|
+
self.source = source
|
|
44
|
+
self.subsystem_nqn = subsystem_nqn
|
|
45
|
+
self.host_id = uuid.UUID(str(host_id)) if host_id else uuid.uuid4()
|
|
46
|
+
self.host_nqn = host_nqn or (
|
|
47
|
+
"nqn.2014-08.org.nvmexpress:uuid:%s" % self.host_id
|
|
48
|
+
)
|
|
49
|
+
self.keep_alive_ms = int(keep_alive_ms)
|
|
50
|
+
if self.keep_alive_ms != 0:
|
|
51
|
+
raise NotImplementedError(
|
|
52
|
+
"non-zero keep-alive requires a background command worker"
|
|
53
|
+
)
|
|
54
|
+
self.timeout = float(timeout)
|
|
55
|
+
self.admin = None
|
|
56
|
+
self.io = None
|
|
57
|
+
self.controller_id = None
|
|
58
|
+
self.capabilities = None
|
|
59
|
+
self.info = None
|
|
60
|
+
self.max_transfer_bytes = None
|
|
61
|
+
try:
|
|
62
|
+
self._connect(int(queue_depth))
|
|
63
|
+
except Exception:
|
|
64
|
+
try:
|
|
65
|
+
self.close()
|
|
66
|
+
except OSError:
|
|
67
|
+
pass
|
|
68
|
+
raise
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def connect(cls, host: str, subsystem_nqn: str, **kwargs) -> "Controller":
|
|
72
|
+
return cls(host, subsystem_nqn, **kwargs)
|
|
73
|
+
|
|
74
|
+
def _connect(self, requested_depth: int) -> None:
|
|
75
|
+
if requested_depth < 2 or requested_depth > p.MAX_QUEUE_DEPTH:
|
|
76
|
+
raise ValueError("queue_depth must be between 2 and 256")
|
|
77
|
+
self.admin = RDMAQueue(
|
|
78
|
+
self.host,
|
|
79
|
+
self.port,
|
|
80
|
+
qid=0,
|
|
81
|
+
depth=p.ADMIN_QUEUE_DEPTH,
|
|
82
|
+
source=self.source,
|
|
83
|
+
)
|
|
84
|
+
with HostBuffer(self.admin.pd, 1024) as data:
|
|
85
|
+
data.write(
|
|
86
|
+
p.connect_data(self.host_id, self.subsystem_nqn, self.host_nqn, 0xFFFF)
|
|
87
|
+
)
|
|
88
|
+
completion = self.admin.execute(
|
|
89
|
+
p.fabrics_connect(0, p.ADMIN_QUEUE_DEPTH, data, self.keep_alive_ms),
|
|
90
|
+
data_owner=data,
|
|
91
|
+
timeout=self.timeout,
|
|
92
|
+
)
|
|
93
|
+
self.controller_id = completion.result & 0xFFFF
|
|
94
|
+
auth_required = completion.result & ((1 << 17) | (1 << 18))
|
|
95
|
+
if auth_required:
|
|
96
|
+
raise NotImplementedError("NVMe DH-HMAC-CHAP authentication is required")
|
|
97
|
+
|
|
98
|
+
self.capabilities = self._property_get(p.REG_CAP, 8)
|
|
99
|
+
mpsmin = (self.capabilities >> 48) & 0xF
|
|
100
|
+
self.controller_page_size = 1 << (12 + mpsmin)
|
|
101
|
+
css = (self.capabilities >> 37) & 0xFF
|
|
102
|
+
cc = p.CC_ENABLE | p.CC_IOSQES | p.CC_IOCQES | (mpsmin << p.CC_MPS_SHIFT)
|
|
103
|
+
if css & (1 << 6):
|
|
104
|
+
cc |= p.CC_CSS_CSI
|
|
105
|
+
self._property_set(p.REG_CC, cc)
|
|
106
|
+
self._wait_ready()
|
|
107
|
+
|
|
108
|
+
with HostBuffer(self.admin.pd, 4096) as identify:
|
|
109
|
+
self.admin.execute(
|
|
110
|
+
p.identify(identify, controller=True),
|
|
111
|
+
data_owner=identify,
|
|
112
|
+
timeout=self.timeout,
|
|
113
|
+
)
|
|
114
|
+
self.info = p.ControllerInfo.from_bytes(identify.read())
|
|
115
|
+
if self.info.controller_id != self.controller_id:
|
|
116
|
+
raise RuntimeError("Connect and Identify returned different controller IDs")
|
|
117
|
+
if not (self.info.sgls & (1 << 2)):
|
|
118
|
+
raise RuntimeError("NVMe/RDMA target does not advertise keyed SGL support")
|
|
119
|
+
if self.info.icdoff:
|
|
120
|
+
raise RuntimeError("non-zero NVMe in-capsule data offsets are unsupported")
|
|
121
|
+
|
|
122
|
+
queue_limit = (self.capabilities & 0xFFFF) + 1
|
|
123
|
+
if self.info.max_commands:
|
|
124
|
+
queue_limit = min(queue_limit, self.info.max_commands)
|
|
125
|
+
depth = min(requested_depth, queue_limit, p.MAX_QUEUE_DEPTH)
|
|
126
|
+
queue_count = (0 << 16) | 0 # Request one submission and one completion queue.
|
|
127
|
+
completion = self.admin.execute(
|
|
128
|
+
p.set_features(p.FEAT_NUMBER_OF_QUEUES, queue_count),
|
|
129
|
+
timeout=self.timeout,
|
|
130
|
+
)
|
|
131
|
+
if min(completion.result & 0xFFFF, completion.result >> 16) + 1 < 1:
|
|
132
|
+
raise RuntimeError("NVMe target did not allocate an I/O queue")
|
|
133
|
+
|
|
134
|
+
self.io = RDMAQueue(
|
|
135
|
+
self.host,
|
|
136
|
+
self.port,
|
|
137
|
+
qid=1,
|
|
138
|
+
depth=depth,
|
|
139
|
+
controller_id=self.controller_id,
|
|
140
|
+
source=self.source,
|
|
141
|
+
)
|
|
142
|
+
with HostBuffer(self.io.pd, 1024) as data:
|
|
143
|
+
data.write(
|
|
144
|
+
p.connect_data(
|
|
145
|
+
self.host_id,
|
|
146
|
+
self.subsystem_nqn,
|
|
147
|
+
self.host_nqn,
|
|
148
|
+
self.controller_id,
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
self.io.execute(
|
|
152
|
+
p.fabrics_connect(1, depth, data),
|
|
153
|
+
data_owner=data,
|
|
154
|
+
timeout=self.timeout,
|
|
155
|
+
)
|
|
156
|
+
mdts = p.MAX_KEYED_SGL_LENGTH
|
|
157
|
+
if self.info.mdts:
|
|
158
|
+
mdts = min(mdts, self.controller_page_size << self.info.mdts)
|
|
159
|
+
self.max_transfer_bytes = mdts
|
|
160
|
+
|
|
161
|
+
def _property_get(self, offset: int, size: int) -> int:
|
|
162
|
+
return self.admin.execute(
|
|
163
|
+
p.property_get(offset, size), timeout=self.timeout
|
|
164
|
+
).result
|
|
165
|
+
|
|
166
|
+
def _property_set(self, offset: int, value: int, size: int = 4) -> None:
|
|
167
|
+
self.admin.execute(p.property_set(offset, value, size), timeout=self.timeout)
|
|
168
|
+
|
|
169
|
+
def _wait_ready(self) -> None:
|
|
170
|
+
timeout = ((self.capabilities >> 24) & 0xFF) + 1
|
|
171
|
+
deadline = time.monotonic() + max(0.5, timeout * 0.5)
|
|
172
|
+
while True:
|
|
173
|
+
csts = self._property_get(p.REG_CSTS, 4)
|
|
174
|
+
if csts & p.CSTS_FATAL:
|
|
175
|
+
raise RuntimeError("NVMe controller entered fatal status")
|
|
176
|
+
if csts & p.CSTS_READY:
|
|
177
|
+
return
|
|
178
|
+
if time.monotonic() >= deadline:
|
|
179
|
+
raise TimeoutError("NVMe controller did not become ready")
|
|
180
|
+
time.sleep(0.01)
|
|
181
|
+
|
|
182
|
+
def identify_namespace(self, nsid: int) -> "Namespace":
|
|
183
|
+
nsid = int(nsid)
|
|
184
|
+
if nsid <= 0 or nsid > self.info.namespace_count:
|
|
185
|
+
raise ValueError("namespace ID is outside the controller's range")
|
|
186
|
+
with HostBuffer(self.admin.pd, 4096) as identify:
|
|
187
|
+
self.admin.execute(
|
|
188
|
+
p.identify(identify, nsid=nsid),
|
|
189
|
+
data_owner=identify,
|
|
190
|
+
timeout=self.timeout,
|
|
191
|
+
)
|
|
192
|
+
info = p.NamespaceInfo.from_bytes(nsid, identify.read())
|
|
193
|
+
if info.metadata_size:
|
|
194
|
+
raise NotImplementedError(
|
|
195
|
+
"namespaces with separate metadata are unsupported"
|
|
196
|
+
)
|
|
197
|
+
return Namespace(self, info)
|
|
198
|
+
|
|
199
|
+
namespace = identify_namespace
|
|
200
|
+
|
|
201
|
+
def allocate(self, length: int) -> HostBuffer:
|
|
202
|
+
"""Allocate host memory registered for direct NVMe/RDMA I/O."""
|
|
203
|
+
return HostBuffer(self.io.pd, length, _DATA_ACCESS)
|
|
204
|
+
|
|
205
|
+
def register(self, tensor):
|
|
206
|
+
"""Register contiguous host tensor/array memory for NVMe/RDMA I/O."""
|
|
207
|
+
return ib.reg_tensor(self.io.pd, tensor, _DATA_ACCESS)
|
|
208
|
+
|
|
209
|
+
def register_gpu(self, tensor):
|
|
210
|
+
"""Register CUDA tensor memory for direct NVMe/RDMA I/O."""
|
|
211
|
+
import ibverbs.cuda as ibcuda
|
|
212
|
+
|
|
213
|
+
return ibcuda.register_tensor(self.io.pd, tensor, _DATA_ACCESS)
|
|
214
|
+
|
|
215
|
+
def close(self) -> None:
|
|
216
|
+
error = None
|
|
217
|
+
if self.io is not None:
|
|
218
|
+
try:
|
|
219
|
+
self.io.close()
|
|
220
|
+
self.io = None
|
|
221
|
+
except OSError as exc:
|
|
222
|
+
error = exc
|
|
223
|
+
if self.admin is not None:
|
|
224
|
+
try:
|
|
225
|
+
self.admin.close()
|
|
226
|
+
self.admin = None
|
|
227
|
+
except OSError as exc:
|
|
228
|
+
if error is None:
|
|
229
|
+
error = exc
|
|
230
|
+
if error is not None:
|
|
231
|
+
raise error
|
|
232
|
+
|
|
233
|
+
def __enter__(self):
|
|
234
|
+
return self
|
|
235
|
+
|
|
236
|
+
def __exit__(self, *exc):
|
|
237
|
+
self.close()
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class Namespace:
|
|
242
|
+
"""An NVM namespace accessed through the controller's direct I/O queue."""
|
|
243
|
+
|
|
244
|
+
def __init__(self, controller: Controller, info: p.NamespaceInfo):
|
|
245
|
+
self.controller = controller
|
|
246
|
+
self.info = info
|
|
247
|
+
self.nsid = info.nsid
|
|
248
|
+
self.lba_size = info.lba_size
|
|
249
|
+
|
|
250
|
+
def _transfer(
|
|
251
|
+
self, opcode: int, buffer, slba: int, blocks=None, buffer_offset: int = 0
|
|
252
|
+
) -> None:
|
|
253
|
+
slba = int(slba)
|
|
254
|
+
buffer_offset = int(buffer_offset)
|
|
255
|
+
available = int(buffer.length) - buffer_offset
|
|
256
|
+
if blocks is None:
|
|
257
|
+
if available % self.lba_size:
|
|
258
|
+
raise ValueError("buffer length is not a whole number of LBAs")
|
|
259
|
+
blocks = available // self.lba_size
|
|
260
|
+
blocks = int(blocks)
|
|
261
|
+
if blocks <= 0 or slba < 0 or slba + blocks > self.info.size_lbas:
|
|
262
|
+
raise ValueError("I/O range is outside the namespace")
|
|
263
|
+
total = blocks * self.lba_size
|
|
264
|
+
if buffer_offset < 0 or total > available:
|
|
265
|
+
raise ValueError("I/O range is outside the registered buffer")
|
|
266
|
+
|
|
267
|
+
gpu = _is_gpu_memory(buffer)
|
|
268
|
+
if gpu and opcode == p.OPC_WRITE:
|
|
269
|
+
import ibverbs.cuda as ibcuda
|
|
270
|
+
|
|
271
|
+
ibcuda.synchronize()
|
|
272
|
+
|
|
273
|
+
max_blocks = min(65536, self.controller.max_transfer_bytes // self.lba_size)
|
|
274
|
+
if max_blocks == 0:
|
|
275
|
+
raise RuntimeError(
|
|
276
|
+
"namespace LBA size exceeds the controller transfer limit"
|
|
277
|
+
)
|
|
278
|
+
remaining = blocks
|
|
279
|
+
offset = buffer_offset
|
|
280
|
+
current_lba = slba
|
|
281
|
+
while remaining:
|
|
282
|
+
count = min(remaining, max_blocks)
|
|
283
|
+
command = p.rw_command(
|
|
284
|
+
opcode,
|
|
285
|
+
self.nsid,
|
|
286
|
+
current_lba,
|
|
287
|
+
count,
|
|
288
|
+
buffer,
|
|
289
|
+
lba_size=self.lba_size,
|
|
290
|
+
buffer_offset=offset,
|
|
291
|
+
)
|
|
292
|
+
self.controller.io.execute(
|
|
293
|
+
command, data_owner=buffer, timeout=self.controller.timeout
|
|
294
|
+
)
|
|
295
|
+
current_lba += count
|
|
296
|
+
offset += count * self.lba_size
|
|
297
|
+
remaining -= count
|
|
298
|
+
|
|
299
|
+
if gpu and opcode == p.OPC_READ:
|
|
300
|
+
import ibverbs.cuda as ibcuda
|
|
301
|
+
|
|
302
|
+
ibcuda.flush_gpudirect_writes()
|
|
303
|
+
|
|
304
|
+
def read(self, buffer, slba: int, blocks=None, *, buffer_offset: int = 0) -> None:
|
|
305
|
+
"""Read namespace LBAs directly into a registered host/GPU buffer."""
|
|
306
|
+
self._transfer(p.OPC_READ, buffer, slba, blocks, buffer_offset)
|
|
307
|
+
|
|
308
|
+
def write(self, buffer, slba: int, blocks=None, *, buffer_offset: int = 0) -> None:
|
|
309
|
+
"""Write namespace LBAs directly from a registered host/GPU buffer."""
|
|
310
|
+
self._transfer(p.OPC_WRITE, buffer, slba, blocks, buffer_offset)
|
|
311
|
+
|
|
312
|
+
def flush(self) -> None:
|
|
313
|
+
command = p._command(p.OPC_FLUSH, self.nsid)
|
|
314
|
+
self.controller.io.execute(bytes(command), timeout=self.controller.timeout)
|