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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)