ibverbs 2026.7.17__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.
- ibverbs-2026.7.17/CLAUDE.md +131 -0
- ibverbs-2026.7.17/LICENSE +28 -0
- ibverbs-2026.7.17/MANIFEST.in +3 -0
- ibverbs-2026.7.17/PKG-INFO +226 -0
- ibverbs-2026.7.17/README.md +201 -0
- ibverbs-2026.7.17/pyproject.toml +67 -0
- ibverbs-2026.7.17/setup.cfg +4 -0
- ibverbs-2026.7.17/setup.py +67 -0
- ibverbs-2026.7.17/src/ibverbs/__init__.py +86 -0
- ibverbs-2026.7.17/src/ibverbs/_ibverbs.c +60521 -0
- ibverbs-2026.7.17/src/ibverbs/_ibverbs.pyx +1735 -0
- ibverbs-2026.7.17/src/ibverbs/_libverbs.pxd +251 -0
- ibverbs-2026.7.17/src/ibverbs/_version.py +1 -0
- ibverbs-2026.7.17/src/ibverbs/cuda.py +244 -0
- ibverbs-2026.7.17/src/ibverbs/enums.py +214 -0
- ibverbs-2026.7.17/src/ibverbs/helpers.py +156 -0
- ibverbs-2026.7.17/src/ibverbs.egg-info/PKG-INFO +226 -0
- ibverbs-2026.7.17/src/ibverbs.egg-info/SOURCES.txt +33 -0
- ibverbs-2026.7.17/src/ibverbs.egg-info/dependency_links.txt +1 -0
- ibverbs-2026.7.17/src/ibverbs.egg-info/requires.txt +7 -0
- ibverbs-2026.7.17/src/ibverbs.egg-info/top_level.txt +1 -0
- ibverbs-2026.7.17/tests/_rc.py +105 -0
- ibverbs-2026.7.17/tests/conftest.py +108 -0
- ibverbs-2026.7.17/tests/test_comp_channel.py +68 -0
- ibverbs-2026.7.17/tests/test_cq.py +55 -0
- ibverbs-2026.7.17/tests/test_cuda_helpers.py +103 -0
- ibverbs-2026.7.17/tests/test_device.py +82 -0
- ibverbs-2026.7.17/tests/test_enums.py +61 -0
- ibverbs-2026.7.17/tests/test_gpudirect.py +146 -0
- ibverbs-2026.7.17/tests/test_helpers.py +104 -0
- ibverbs-2026.7.17/tests/test_import.py +13 -0
- ibverbs-2026.7.17/tests/test_loopback.py +176 -0
- ibverbs-2026.7.17/tests/test_pd_mr.py +104 -0
- ibverbs-2026.7.17/tests/test_qp.py +114 -0
- ibverbs-2026.7.17/tests/test_two_device.py +109 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# CLAUDE.md — ibverbs
|
|
2
|
+
|
|
3
|
+
Guidance for working on the `ibverbs` package (low-level libibverbs bindings).
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
A Cython wrapper over `libibverbs`. The goal is a **faithful, low-level** verbs
|
|
8
|
+
API in Python that a higher-level RDMA library can be built on — not a
|
|
9
|
+
transport itself. Keep it thin: one Python object per verbs object, minimal
|
|
10
|
+
policy, no hidden threads, no runtime dependencies, and **no torch/CUDA
|
|
11
|
+
linkage** (GPUDirect works via raw addresses / dma-buf fds).
|
|
12
|
+
|
|
13
|
+
## Layout
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
ibverbs/
|
|
17
|
+
pyproject.toml # build (setuptools+Cython), metadata, pytest markers
|
|
18
|
+
setup.py # Extension built via `pkg-config libibverbs`
|
|
19
|
+
src/ibverbs/
|
|
20
|
+
__init__.py # public re-exports + __version__
|
|
21
|
+
_libverbs.pxd # `cdef extern` declarations of <infiniband/verbs.h>
|
|
22
|
+
_ibverbs.pyx # all cdef classes + posting logic (the real binding)
|
|
23
|
+
enums.py # IntEnum/IntFlag mirrors of the rdma-core ABI
|
|
24
|
+
helpers.py # QPInfo, RC connect helpers, reg_tensor (pure Python)
|
|
25
|
+
cuda.py # optional GPUDirect: register_tensor/GpuMR (lazy libcuda)
|
|
26
|
+
tests/
|
|
27
|
+
conftest.py # device/port fixtures, RoCE GID selection, markers
|
|
28
|
+
_rc.py # HostBuffer + Endpoint + connected-pair helpers
|
|
29
|
+
test_*.py # unit + integration (see README feature table)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Build & test
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Dev venv lives at repo-root .venv (Python 3.14, has torch+cython+pytest+numpy).
|
|
36
|
+
uv pip install --python ../.venv -e . --no-build-isolation # rebuild after .pyx/.pxd edits
|
|
37
|
+
../.venv/bin/python -m pytest -rs -q # run the suite
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- Editing `.py` (enums/helpers/tests): no rebuild needed.
|
|
41
|
+
- Editing `.pyx` / `.pxd`: **must rebuild** (`uv pip install -e . --no-build-isolation`).
|
|
42
|
+
- The Cython-generated `_ibverbs.c` and the `.so` are git-ignored.
|
|
43
|
+
|
|
44
|
+
## Linkage model: dlopen, not direct link (portability)
|
|
45
|
+
|
|
46
|
+
The extension is compiled against the rdma-core headers but is **not** linked
|
|
47
|
+
against `libibverbs`. `setup.py` uses `pkg-config --cflags-only-I` for the
|
|
48
|
+
header path and links only `libdl`. At import, `_load_libibverbs()` in
|
|
49
|
+
`_ibverbs.pyx` `dlopen`s `libibverbs.so.1` (RTLD_NOW|RTLD_GLOBAL) and `dlsym`s
|
|
50
|
+
each exported verb into a typed function pointer (`_v_*`). Consequences:
|
|
51
|
+
|
|
52
|
+
- The `.so`'s only `NEEDED` is `libc` — no external dep for `auditwheel`, so a
|
|
53
|
+
clean manylinux wheel; missing libibverbs → clean `ImportError`.
|
|
54
|
+
- `ibv_reg_dmabuf_mr` is loaded **optionally** (may be NULL on rdma-core < 34);
|
|
55
|
+
`PD.reg_dmabuf_mr` raises a clear error if it's unavailable.
|
|
56
|
+
- Built as **abi3** (Limited API, floor 3.9) → one wheel for CPython 3.9+.
|
|
57
|
+
- Only the five `static inline` data-path verbs
|
|
58
|
+
(`ibv_post_send/recv`, `ibv_poll_cq`, `ibv_req_notify_cq`,
|
|
59
|
+
`ibv_post_srq_recv`) are still declared extern-from-header: they dispatch
|
|
60
|
+
through the provider op table and reference no exported symbol, so they
|
|
61
|
+
compile inline with zero symbol resolution (the fast path stays fast). This
|
|
62
|
+
is why a pure-ctypes binding is a poor fit and we use a compiled extension.
|
|
63
|
+
|
|
64
|
+
When adding a verb: declare its function-pointer type (`fp_*`), a `_v_*`
|
|
65
|
+
global, and a `_req_sym`/`dlsym` line in `_load_libibverbs`, then call `_v_*`.
|
|
66
|
+
|
|
67
|
+
## How the C API is declared (`_libverbs.pxd`)
|
|
68
|
+
|
|
69
|
+
- Structs are declared **partially** — only the fields we touch. Cython trusts
|
|
70
|
+
the real header for layout, so this is safe even for stack-allocated structs.
|
|
71
|
+
- Enum-typed fields/params are declared as plain `int` (C converts implicitly);
|
|
72
|
+
this keeps the file small and robust across rdma-core versions.
|
|
73
|
+
- `ibv_reg_mr` and `ibv_query_port` are **macros** in the header, so we never
|
|
74
|
+
reference them by name: we `dlsym` `ibv_reg_mr_iova2` (called with
|
|
75
|
+
`iova == addr`) and the raw `ibv_query_port` symbol (called with a zeroed
|
|
76
|
+
attr struct, which is what the header's `___ibv_query_port` compat shim does).
|
|
77
|
+
- `ibv_send_wr.wr` is a C **union** (`rdma` / `atomic` / `ud`); declared with
|
|
78
|
+
named nested `cdef struct`s that are never emitted (extern), only used for
|
|
79
|
+
field-access type-checking.
|
|
80
|
+
|
|
81
|
+
## Conventions in `_ibverbs.pyx`
|
|
82
|
+
|
|
83
|
+
- **RAII:** each `cdef class` frees its handle in `__dealloc__` and in an
|
|
84
|
+
idempotent `close()`; all are context managers.
|
|
85
|
+
- **Lifetime:** children hold a Python reference to their parent (MR→PD,
|
|
86
|
+
QP→PD+CQs+SRQ, CQ→Context/CompChannel) so the GC can't free a parent first.
|
|
87
|
+
- **GIL:** build C structs while holding the GIL, then wrap only the actual
|
|
88
|
+
`ibv_*` call in `with nogil:`. Coercing a Python object inside `nogil` is a
|
|
89
|
+
compile error — convert to a C local first.
|
|
90
|
+
- **Errors:** verbs failures raise `VerbsError(OSError)` carrying `errno`; a bad
|
|
91
|
+
*work completion* status does **not** raise (it's returned in the `WC`);
|
|
92
|
+
`WC.raise_for_status()` is opt-in.
|
|
93
|
+
- **Byte order:** `imm_data` crosses the wire in network order — `htonl` on
|
|
94
|
+
send, `ntohl` on completion, so Python always sees host order.
|
|
95
|
+
- **CQ ↔ event mapping:** `create_cq` passes `<void*>cq` as the CQ context, so
|
|
96
|
+
`CompChannel.get_cq_event()` can hand back the exact Python `CQ`.
|
|
97
|
+
|
|
98
|
+
## Adding a wrapped verb
|
|
99
|
+
|
|
100
|
+
1. Declare the C signature/struct fields in `_libverbs.pxd` (partial is fine).
|
|
101
|
+
2. Add the method/class in `_ibverbs.pyx`; follow the RAII + GIL + error rules
|
|
102
|
+
above; add any new enum values to `enums.py`.
|
|
103
|
+
3. Re-export new public names in `__init__.py` (and its `__all__`).
|
|
104
|
+
4. Write a test in `tests/` — a unit test if it needs no wire, otherwise add to
|
|
105
|
+
the loopback/integration set. Rebuild, then `pytest`.
|
|
106
|
+
|
|
107
|
+
## RDMA gotchas learned here (don't regress these)
|
|
108
|
+
|
|
109
|
+
- **RoCE GID selection matters.** Link-local (`fe80::`) GIDs do **not** loop
|
|
110
|
+
back or route to themselves on this fabric; use a global **RoCE v2** GID.
|
|
111
|
+
`tests/conftest.find_roce_gid` scores candidates accordingly.
|
|
112
|
+
- **Union aliasing.** In `post_send`, only populate the `wr` union member for
|
|
113
|
+
the actual opcode — writing `atomic.*` over `rdma.*` clobbers the rkey.
|
|
114
|
+
- **`char*` comparison.** Comparing device names must use `strcmp`, not `==`
|
|
115
|
+
(which compares pointers in Cython).
|
|
116
|
+
- **GPUDirect here.** `nvidia_peermem` may fail to load (kernel/driver skew);
|
|
117
|
+
the dma-buf path (`reg_dmabuf_mr` + `cuMemGetHandleForAddressRange`) is the
|
|
118
|
+
reliable route and needs no kernel module. `ibverbs.cuda.register_tensor`
|
|
119
|
+
wraps it — it stays torch-free (duck-typed on `data_ptr()`) and imports no
|
|
120
|
+
CUDA at build. Requirements learned the hard way: torch memory must be
|
|
121
|
+
VMM-backed (`PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True`); the dma-buf
|
|
122
|
+
export needs the **base page-aligned down and length page-aligned up**
|
|
123
|
+
(`register_tensor` handles this via `offset`/`iova`); and `ibv_mr.addr` is
|
|
124
|
+
**not** meaningful for dma-buf MRs, so `GpuMR` carries the real device VA.
|
|
125
|
+
- Loopback (two QPs on one port) is a real NIC round-trip and is the primary
|
|
126
|
+
integration signal; two-NIC tests skip when the NICs aren't mutually routable.
|
|
127
|
+
|
|
128
|
+
## Hardware this was validated on
|
|
129
|
+
|
|
130
|
+
12× Mellanox mlx5 NICs (RoCEv2, Ethernet link layer), 8× NVIDIA H100, CUDA
|
|
131
|
+
driver 580.x, rdma-core 61, kernel 6.16.
|
|
@@ -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,226 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ibverbs
|
|
3
|
+
Version: 2026.7.17
|
|
4
|
+
Summary: Low-level Pythonic bindings for libibverbs (RDMA), with GPUDirect support
|
|
5
|
+
Author: Tristan Rice
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
Project-URL: Homepage, https://github.com/tristanr/rdma4py
|
|
8
|
+
Project-URL: Repository, https://github.com/tristanr/rdma4py
|
|
9
|
+
Keywords: rdma,ibverbs,infiniband,roce,gpudirect,networking
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Programming Language :: Cython
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: System :: Networking
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
21
|
+
Requires-Dist: numpy; extra == "test"
|
|
22
|
+
Provides-Extra: gpu
|
|
23
|
+
Requires-Dist: torch; extra == "gpu"
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# ibverbs
|
|
27
|
+
|
|
28
|
+
Low-level, Pythonic bindings for **libibverbs** (RDMA), designed as a
|
|
29
|
+
foundation for building high-performance RDMA libraries in Python — including
|
|
30
|
+
**GPUDirect** transfers to/from GPU memory.
|
|
31
|
+
|
|
32
|
+
The bindings are a thin, faithful wrapper over the verbs API (device, PD, MR,
|
|
33
|
+
CQ, QP, SRQ, AH, work requests, completions, async events) plus a small,
|
|
34
|
+
optional set of RC connection helpers. They are written in Cython so the data
|
|
35
|
+
path (`post_send` / `post_recv` / `poll`) compiles to direct C calls and
|
|
36
|
+
releases the GIL, and so the `static inline` verbs fast-path functions are
|
|
37
|
+
called correctly (they can't be reached through `dlsym`).
|
|
38
|
+
|
|
39
|
+
- **No runtime dependencies.** Only libibverbs, which is `dlopen`ed at import.
|
|
40
|
+
- **No torch / CUDA linkage.** GPUDirect works by registering an integer
|
|
41
|
+
device address or an exported dma-buf fd; CUDA stays entirely in your code.
|
|
42
|
+
- **One `abi3` wheel for all of CPython 3.9+** on Linux.
|
|
43
|
+
|
|
44
|
+
## Portability
|
|
45
|
+
|
|
46
|
+
The extension does **not** link `libibverbs`. It is compiled against the
|
|
47
|
+
rdma-core headers (for struct layouts and the `static inline` data-path verbs)
|
|
48
|
+
but resolves the exported verbs at import time with `dlopen`/`dlsym`. As a
|
|
49
|
+
result:
|
|
50
|
+
|
|
51
|
+
- The compiled module's only `NEEDED` library is `libc` — no external
|
|
52
|
+
dependency for `auditwheel`, so a single **manylinux** wheel is portable
|
|
53
|
+
across distros.
|
|
54
|
+
- It is built against the **CPython Limited API (abi3)**, so one wheel works on
|
|
55
|
+
CPython **3.9 through 3.14+** — no per-version builds.
|
|
56
|
+
- A missing `libibverbs` yields a clean `ImportError`, not a loader crash.
|
|
57
|
+
- Newer verbs are optional: `ibv_reg_dmabuf_mr` (rdma-core ≥ 34) is loaded if
|
|
58
|
+
present and only errors if you actually call `reg_dmabuf_mr`, so the wheel
|
|
59
|
+
still imports on older systems.
|
|
60
|
+
|
|
61
|
+
At runtime you only need `libibverbs.so.1` (any `rdma-core` from the last
|
|
62
|
+
several years). The data path (`post_send`/`poll`/…) stays compiled inline and
|
|
63
|
+
dispatches through the provider op table, so `dlopen` costs nothing on the hot
|
|
64
|
+
path.
|
|
65
|
+
|
|
66
|
+
## Requirements
|
|
67
|
+
|
|
68
|
+
- Linux with an RDMA-capable NIC (tested on Mellanox/NVIDIA **mlx5**, RoCEv2).
|
|
69
|
+
- **Runtime:** `libibverbs.so.1` (`rdma-core` — `libibverbs1` on Debian/Ubuntu,
|
|
70
|
+
`libibverbs` on RHEL/Fedora). No compiler or headers needed to *use* a wheel.
|
|
71
|
+
- **Build from source only:** a C compiler, Cython, and the `rdma-core`
|
|
72
|
+
development headers (`libibverbs-dev` / `rdma-core-devel`).
|
|
73
|
+
|
|
74
|
+
## Install
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install ibverbs # prebuilt abi3 manylinux wheel (once published)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Building from source (needs the rdma-core dev headers + a compiler):
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install "Cython>=3.0" "setuptools>=77" wheel
|
|
84
|
+
pip install ./ibverbs # or: pip install -e ./ibverbs
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Quickstart
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import ibverbs as ib
|
|
91
|
+
|
|
92
|
+
# 1. Open a device and set up resources.
|
|
93
|
+
dev = ib.get_device_list()[0]
|
|
94
|
+
ctx = dev.open()
|
|
95
|
+
pd = ctx.alloc_pd()
|
|
96
|
+
cq = ctx.create_cq(64)
|
|
97
|
+
|
|
98
|
+
# 2. Register memory (host or GPU address — any integer VA works).
|
|
99
|
+
import numpy as np
|
|
100
|
+
buf = np.zeros(4096, dtype=np.uint8)
|
|
101
|
+
access = ib.AccessFlags.LOCAL_WRITE | ib.AccessFlags.REMOTE_WRITE | ib.AccessFlags.REMOTE_READ
|
|
102
|
+
mr = pd.reg_mr(buf.ctypes.data, buf.nbytes, access)
|
|
103
|
+
|
|
104
|
+
# 3. Create a reliable-connected QP.
|
|
105
|
+
qp = pd.create_qp(ib.QPInitAttr(send_cq=cq, recv_cq=cq, qp_type=ib.QPType.RC))
|
|
106
|
+
|
|
107
|
+
# 4. Exchange connection info with the peer out-of-band, then connect.
|
|
108
|
+
port = 1
|
|
109
|
+
port_attr = ctx.query_port(port)
|
|
110
|
+
gid = ctx.query_gid(port, gid_index) # pick a routable RoCEv2 GID
|
|
111
|
+
local = ib.local_qp_info(qp, port_attr, gid, port=port, psn=0)
|
|
112
|
+
# ... send local.to_bytes() to peer, receive remote_bytes ...
|
|
113
|
+
remote = ib.QPInfo.from_bytes(remote_bytes)
|
|
114
|
+
ib.connect_rc(qp, remote, port=port, sgid_index=gid_index, access=access)
|
|
115
|
+
|
|
116
|
+
# 5. Post an RDMA write and reap the completion.
|
|
117
|
+
qp.post_send(ib.SendWR(
|
|
118
|
+
wr_id=1, sg_list=[ib.SGE(mr, 4096)], opcode=ib.WROpcode.RDMA_WRITE,
|
|
119
|
+
send_flags=ib.SendFlags.SIGNALED, remote_addr=peer_addr, rkey=peer_rkey))
|
|
120
|
+
for wc in cq.poll(16):
|
|
121
|
+
wc.raise_for_status()
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Every resource is a context manager and frees its handle on `close()` /
|
|
125
|
+
garbage collection; children hold references to their parents, so destruction
|
|
126
|
+
order is always safe.
|
|
127
|
+
|
|
128
|
+
## GPUDirect with torch tensors
|
|
129
|
+
|
|
130
|
+
The library never imports torch or links CUDA. The optional `ibverbs.cuda`
|
|
131
|
+
helper (which only lazily `dlopen`s `libcuda`) registers a CUDA tensor for RDMA
|
|
132
|
+
in one call — handling the dma-buf export and page alignment for you:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import os
|
|
136
|
+
# torch's CUDA memory must be VMM-backed to be dma-buf exportable. Set this
|
|
137
|
+
# BEFORE torch initializes CUDA:
|
|
138
|
+
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
|
|
139
|
+
|
|
140
|
+
import torch
|
|
141
|
+
import ibverbs as ib
|
|
142
|
+
import ibverbs.cuda
|
|
143
|
+
|
|
144
|
+
src = torch.arange(4096, dtype=torch.float32, device="cuda:0")
|
|
145
|
+
dst = torch.zeros(4096, dtype=torch.float32, device="cuda:0")
|
|
146
|
+
|
|
147
|
+
access = ib.AccessFlags.LOCAL_WRITE | ib.AccessFlags.REMOTE_WRITE
|
|
148
|
+
src_mr = ib.cuda.register_tensor(pd, src, access) # retains src until close()
|
|
149
|
+
dst_mr = ib.cuda.register_tensor(pd, dst, access)
|
|
150
|
+
|
|
151
|
+
# RDMA-write one GPU buffer into another, with no host staging on the data path.
|
|
152
|
+
torch.cuda.synchronize(src.device) # source-producing CUDA work must be done
|
|
153
|
+
qp.post_send(ib.SendWR(
|
|
154
|
+
wr_id=1, sg_list=[src_mr.sge()], opcode=ib.WROpcode.RDMA_WRITE,
|
|
155
|
+
send_flags=ib.SendFlags.SIGNALED,
|
|
156
|
+
remote_addr=dst_mr.addr, rkey=dst_mr.rkey))
|
|
157
|
+
for wc in qp.send_cq.poll(16):
|
|
158
|
+
wc.raise_for_status()
|
|
159
|
+
|
|
160
|
+
# On the receiver, after the peer has signaled that its write is complete,
|
|
161
|
+
# order the inbound NIC writes before launching CUDA work that consumes dst.
|
|
162
|
+
with torch.cuda.device(dst.device):
|
|
163
|
+
ib.cuda.flush_gpudirect_writes()
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`GpuMR` wraps the `MR` with the correct device address (`ibv_mr.addr` is not
|
|
167
|
+
meaningful for dma-buf MRs), retains the tensor allocation until `close()`, and
|
|
168
|
+
exposes `.sge()`, `.addr`, `.lkey`, `.rkey`.
|
|
169
|
+
|
|
170
|
+
CUDA work and NIC work are separate ordering domains. Synchronize the stream
|
|
171
|
+
that produced an outbound tensor before posting it. For inbound `SEND`, RDMA
|
|
172
|
+
read, or RDMA write, wait for the corresponding completion or protocol-level
|
|
173
|
+
notification, then call `flush_gpudirect_writes()` in the destination CUDA
|
|
174
|
+
context before consuming the tensor. A one-sided RDMA write does not create a
|
|
175
|
+
remote CQ entry by itself; use write-with-immediate or an out-of-band message
|
|
176
|
+
to notify the receiver.
|
|
177
|
+
|
|
178
|
+
Under the hood there are two registration paths, chosen automatically:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
# dma-buf fd (default; no kernel module needed):
|
|
182
|
+
mr = pd.reg_dmabuf_mr(offset, length, iova=device_va, fd=dmabuf_fd, access=access)
|
|
183
|
+
# raw device pointer (requires the nvidia_peermem kernel module):
|
|
184
|
+
mr = pd.reg_mr(tensor.data_ptr(), nbytes, access)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
For a **host** (CPU) torch tensor or numpy array, `ib.reg_tensor(pd, tensor,
|
|
188
|
+
access)` registers it directly and retains the allocation. Both tensor helpers
|
|
189
|
+
require contiguous, non-empty tensors; split any single SGE larger than
|
|
190
|
+
`2**32 - 1` bytes into multiple entries. `tests/test_gpudirect.py` performs real
|
|
191
|
+
GPU-to-GPU RDMA writes, reads, and sends verified with `torch.equal`.
|
|
192
|
+
|
|
193
|
+
## Feature coverage
|
|
194
|
+
|
|
195
|
+
| Area | Supported |
|
|
196
|
+
|------|-----------|
|
|
197
|
+
| Device / port / GID query | ✅ `get_device_list`, `Context.query_device/query_port/query_gid` |
|
|
198
|
+
| Protection domains | ✅ `alloc_pd` |
|
|
199
|
+
| Memory regions | ✅ `reg_mr`, `reg_dmabuf_mr` (GPUDirect) |
|
|
200
|
+
| Completion queues | ✅ `create_cq`, `poll`, comp channels + `req_notify`/`ack_events` |
|
|
201
|
+
| Queue pairs | ✅ RC / UC / UD; `modify`, `query`, `to_init`/`to_rtr`/`to_rts` |
|
|
202
|
+
| Work requests | ✅ SEND(/_IMM), RDMA_WRITE(/_IMM), RDMA_READ, ATOMIC_CMP_AND_SWP, ATOMIC_FETCH_AND_ADD, scatter/gather, inline/signaled/fenced/solicited flags |
|
|
203
|
+
| Shared receive queues | ✅ `create_srq`, `post_recv`, `modify`, `query` |
|
|
204
|
+
| Address handles | ✅ `create_ah` (UD) |
|
|
205
|
+
| Async events | ✅ `get_async_event` / `ack_async_event`, `async_fd` |
|
|
206
|
+
| Connection helpers | ✅ `QPInfo`, `local_qp_info`, `connect_rc` |
|
|
207
|
+
|
|
208
|
+
Out of scope for v1 (candidates for later): the extended `ibv_wr_*` / `qp_ex`
|
|
209
|
+
post API, device memory (`ibv_alloc_dm`), memory windows, and flow steering.
|
|
210
|
+
|
|
211
|
+
## Testing
|
|
212
|
+
|
|
213
|
+
The suite exercises real hardware and skips features the host lacks:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
pip install -e "./ibverbs[test,gpu]" # pytest, numpy, torch
|
|
217
|
+
cd ibverbs && pytest -rs # -rs shows skip reasons
|
|
218
|
+
pytest -m "not gpu" # skip GPUDirect tests
|
|
219
|
+
pytest -m integration # only real-hardware tests
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Markers: `integration` (needs an RDMA NIC), `gpu` (needs CUDA + torch).
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
BSD-3-Clause. See the repository `LICENSE`.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# ibverbs
|
|
2
|
+
|
|
3
|
+
Low-level, Pythonic bindings for **libibverbs** (RDMA), designed as a
|
|
4
|
+
foundation for building high-performance RDMA libraries in Python — including
|
|
5
|
+
**GPUDirect** transfers to/from GPU memory.
|
|
6
|
+
|
|
7
|
+
The bindings are a thin, faithful wrapper over the verbs API (device, PD, MR,
|
|
8
|
+
CQ, QP, SRQ, AH, work requests, completions, async events) plus a small,
|
|
9
|
+
optional set of RC connection helpers. They are written in Cython so the data
|
|
10
|
+
path (`post_send` / `post_recv` / `poll`) compiles to direct C calls and
|
|
11
|
+
releases the GIL, and so the `static inline` verbs fast-path functions are
|
|
12
|
+
called correctly (they can't be reached through `dlsym`).
|
|
13
|
+
|
|
14
|
+
- **No runtime dependencies.** Only libibverbs, which is `dlopen`ed at import.
|
|
15
|
+
- **No torch / CUDA linkage.** GPUDirect works by registering an integer
|
|
16
|
+
device address or an exported dma-buf fd; CUDA stays entirely in your code.
|
|
17
|
+
- **One `abi3` wheel for all of CPython 3.9+** on Linux.
|
|
18
|
+
|
|
19
|
+
## Portability
|
|
20
|
+
|
|
21
|
+
The extension does **not** link `libibverbs`. It is compiled against the
|
|
22
|
+
rdma-core headers (for struct layouts and the `static inline` data-path verbs)
|
|
23
|
+
but resolves the exported verbs at import time with `dlopen`/`dlsym`. As a
|
|
24
|
+
result:
|
|
25
|
+
|
|
26
|
+
- The compiled module's only `NEEDED` library is `libc` — no external
|
|
27
|
+
dependency for `auditwheel`, so a single **manylinux** wheel is portable
|
|
28
|
+
across distros.
|
|
29
|
+
- It is built against the **CPython Limited API (abi3)**, so one wheel works on
|
|
30
|
+
CPython **3.9 through 3.14+** — no per-version builds.
|
|
31
|
+
- A missing `libibverbs` yields a clean `ImportError`, not a loader crash.
|
|
32
|
+
- Newer verbs are optional: `ibv_reg_dmabuf_mr` (rdma-core ≥ 34) is loaded if
|
|
33
|
+
present and only errors if you actually call `reg_dmabuf_mr`, so the wheel
|
|
34
|
+
still imports on older systems.
|
|
35
|
+
|
|
36
|
+
At runtime you only need `libibverbs.so.1` (any `rdma-core` from the last
|
|
37
|
+
several years). The data path (`post_send`/`poll`/…) stays compiled inline and
|
|
38
|
+
dispatches through the provider op table, so `dlopen` costs nothing on the hot
|
|
39
|
+
path.
|
|
40
|
+
|
|
41
|
+
## Requirements
|
|
42
|
+
|
|
43
|
+
- Linux with an RDMA-capable NIC (tested on Mellanox/NVIDIA **mlx5**, RoCEv2).
|
|
44
|
+
- **Runtime:** `libibverbs.so.1` (`rdma-core` — `libibverbs1` on Debian/Ubuntu,
|
|
45
|
+
`libibverbs` on RHEL/Fedora). No compiler or headers needed to *use* a wheel.
|
|
46
|
+
- **Build from source only:** a C compiler, Cython, and the `rdma-core`
|
|
47
|
+
development headers (`libibverbs-dev` / `rdma-core-devel`).
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install ibverbs # prebuilt abi3 manylinux wheel (once published)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Building from source (needs the rdma-core dev headers + a compiler):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install "Cython>=3.0" "setuptools>=77" wheel
|
|
59
|
+
pip install ./ibverbs # or: pip install -e ./ibverbs
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quickstart
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import ibverbs as ib
|
|
66
|
+
|
|
67
|
+
# 1. Open a device and set up resources.
|
|
68
|
+
dev = ib.get_device_list()[0]
|
|
69
|
+
ctx = dev.open()
|
|
70
|
+
pd = ctx.alloc_pd()
|
|
71
|
+
cq = ctx.create_cq(64)
|
|
72
|
+
|
|
73
|
+
# 2. Register memory (host or GPU address — any integer VA works).
|
|
74
|
+
import numpy as np
|
|
75
|
+
buf = np.zeros(4096, dtype=np.uint8)
|
|
76
|
+
access = ib.AccessFlags.LOCAL_WRITE | ib.AccessFlags.REMOTE_WRITE | ib.AccessFlags.REMOTE_READ
|
|
77
|
+
mr = pd.reg_mr(buf.ctypes.data, buf.nbytes, access)
|
|
78
|
+
|
|
79
|
+
# 3. Create a reliable-connected QP.
|
|
80
|
+
qp = pd.create_qp(ib.QPInitAttr(send_cq=cq, recv_cq=cq, qp_type=ib.QPType.RC))
|
|
81
|
+
|
|
82
|
+
# 4. Exchange connection info with the peer out-of-band, then connect.
|
|
83
|
+
port = 1
|
|
84
|
+
port_attr = ctx.query_port(port)
|
|
85
|
+
gid = ctx.query_gid(port, gid_index) # pick a routable RoCEv2 GID
|
|
86
|
+
local = ib.local_qp_info(qp, port_attr, gid, port=port, psn=0)
|
|
87
|
+
# ... send local.to_bytes() to peer, receive remote_bytes ...
|
|
88
|
+
remote = ib.QPInfo.from_bytes(remote_bytes)
|
|
89
|
+
ib.connect_rc(qp, remote, port=port, sgid_index=gid_index, access=access)
|
|
90
|
+
|
|
91
|
+
# 5. Post an RDMA write and reap the completion.
|
|
92
|
+
qp.post_send(ib.SendWR(
|
|
93
|
+
wr_id=1, sg_list=[ib.SGE(mr, 4096)], opcode=ib.WROpcode.RDMA_WRITE,
|
|
94
|
+
send_flags=ib.SendFlags.SIGNALED, remote_addr=peer_addr, rkey=peer_rkey))
|
|
95
|
+
for wc in cq.poll(16):
|
|
96
|
+
wc.raise_for_status()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Every resource is a context manager and frees its handle on `close()` /
|
|
100
|
+
garbage collection; children hold references to their parents, so destruction
|
|
101
|
+
order is always safe.
|
|
102
|
+
|
|
103
|
+
## GPUDirect with torch tensors
|
|
104
|
+
|
|
105
|
+
The library never imports torch or links CUDA. The optional `ibverbs.cuda`
|
|
106
|
+
helper (which only lazily `dlopen`s `libcuda`) registers a CUDA tensor for RDMA
|
|
107
|
+
in one call — handling the dma-buf export and page alignment for you:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
import os
|
|
111
|
+
# torch's CUDA memory must be VMM-backed to be dma-buf exportable. Set this
|
|
112
|
+
# BEFORE torch initializes CUDA:
|
|
113
|
+
os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
|
|
114
|
+
|
|
115
|
+
import torch
|
|
116
|
+
import ibverbs as ib
|
|
117
|
+
import ibverbs.cuda
|
|
118
|
+
|
|
119
|
+
src = torch.arange(4096, dtype=torch.float32, device="cuda:0")
|
|
120
|
+
dst = torch.zeros(4096, dtype=torch.float32, device="cuda:0")
|
|
121
|
+
|
|
122
|
+
access = ib.AccessFlags.LOCAL_WRITE | ib.AccessFlags.REMOTE_WRITE
|
|
123
|
+
src_mr = ib.cuda.register_tensor(pd, src, access) # retains src until close()
|
|
124
|
+
dst_mr = ib.cuda.register_tensor(pd, dst, access)
|
|
125
|
+
|
|
126
|
+
# RDMA-write one GPU buffer into another, with no host staging on the data path.
|
|
127
|
+
torch.cuda.synchronize(src.device) # source-producing CUDA work must be done
|
|
128
|
+
qp.post_send(ib.SendWR(
|
|
129
|
+
wr_id=1, sg_list=[src_mr.sge()], opcode=ib.WROpcode.RDMA_WRITE,
|
|
130
|
+
send_flags=ib.SendFlags.SIGNALED,
|
|
131
|
+
remote_addr=dst_mr.addr, rkey=dst_mr.rkey))
|
|
132
|
+
for wc in qp.send_cq.poll(16):
|
|
133
|
+
wc.raise_for_status()
|
|
134
|
+
|
|
135
|
+
# On the receiver, after the peer has signaled that its write is complete,
|
|
136
|
+
# order the inbound NIC writes before launching CUDA work that consumes dst.
|
|
137
|
+
with torch.cuda.device(dst.device):
|
|
138
|
+
ib.cuda.flush_gpudirect_writes()
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`GpuMR` wraps the `MR` with the correct device address (`ibv_mr.addr` is not
|
|
142
|
+
meaningful for dma-buf MRs), retains the tensor allocation until `close()`, and
|
|
143
|
+
exposes `.sge()`, `.addr`, `.lkey`, `.rkey`.
|
|
144
|
+
|
|
145
|
+
CUDA work and NIC work are separate ordering domains. Synchronize the stream
|
|
146
|
+
that produced an outbound tensor before posting it. For inbound `SEND`, RDMA
|
|
147
|
+
read, or RDMA write, wait for the corresponding completion or protocol-level
|
|
148
|
+
notification, then call `flush_gpudirect_writes()` in the destination CUDA
|
|
149
|
+
context before consuming the tensor. A one-sided RDMA write does not create a
|
|
150
|
+
remote CQ entry by itself; use write-with-immediate or an out-of-band message
|
|
151
|
+
to notify the receiver.
|
|
152
|
+
|
|
153
|
+
Under the hood there are two registration paths, chosen automatically:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# dma-buf fd (default; no kernel module needed):
|
|
157
|
+
mr = pd.reg_dmabuf_mr(offset, length, iova=device_va, fd=dmabuf_fd, access=access)
|
|
158
|
+
# raw device pointer (requires the nvidia_peermem kernel module):
|
|
159
|
+
mr = pd.reg_mr(tensor.data_ptr(), nbytes, access)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
For a **host** (CPU) torch tensor or numpy array, `ib.reg_tensor(pd, tensor,
|
|
163
|
+
access)` registers it directly and retains the allocation. Both tensor helpers
|
|
164
|
+
require contiguous, non-empty tensors; split any single SGE larger than
|
|
165
|
+
`2**32 - 1` bytes into multiple entries. `tests/test_gpudirect.py` performs real
|
|
166
|
+
GPU-to-GPU RDMA writes, reads, and sends verified with `torch.equal`.
|
|
167
|
+
|
|
168
|
+
## Feature coverage
|
|
169
|
+
|
|
170
|
+
| Area | Supported |
|
|
171
|
+
|------|-----------|
|
|
172
|
+
| Device / port / GID query | ✅ `get_device_list`, `Context.query_device/query_port/query_gid` |
|
|
173
|
+
| Protection domains | ✅ `alloc_pd` |
|
|
174
|
+
| Memory regions | ✅ `reg_mr`, `reg_dmabuf_mr` (GPUDirect) |
|
|
175
|
+
| Completion queues | ✅ `create_cq`, `poll`, comp channels + `req_notify`/`ack_events` |
|
|
176
|
+
| Queue pairs | ✅ RC / UC / UD; `modify`, `query`, `to_init`/`to_rtr`/`to_rts` |
|
|
177
|
+
| Work requests | ✅ SEND(/_IMM), RDMA_WRITE(/_IMM), RDMA_READ, ATOMIC_CMP_AND_SWP, ATOMIC_FETCH_AND_ADD, scatter/gather, inline/signaled/fenced/solicited flags |
|
|
178
|
+
| Shared receive queues | ✅ `create_srq`, `post_recv`, `modify`, `query` |
|
|
179
|
+
| Address handles | ✅ `create_ah` (UD) |
|
|
180
|
+
| Async events | ✅ `get_async_event` / `ack_async_event`, `async_fd` |
|
|
181
|
+
| Connection helpers | ✅ `QPInfo`, `local_qp_info`, `connect_rc` |
|
|
182
|
+
|
|
183
|
+
Out of scope for v1 (candidates for later): the extended `ibv_wr_*` / `qp_ex`
|
|
184
|
+
post API, device memory (`ibv_alloc_dm`), memory windows, and flow steering.
|
|
185
|
+
|
|
186
|
+
## Testing
|
|
187
|
+
|
|
188
|
+
The suite exercises real hardware and skips features the host lacks:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
pip install -e "./ibverbs[test,gpu]" # pytest, numpy, torch
|
|
192
|
+
cd ibverbs && pytest -rs # -rs shows skip reasons
|
|
193
|
+
pytest -m "not gpu" # skip GPUDirect tests
|
|
194
|
+
pytest -m integration # only real-hardware tests
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Markers: `integration` (needs an RDMA NIC), `gpu` (needs CUDA + torch).
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
BSD-3-Clause. See the repository `LICENSE`.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "Cython>=3.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ibverbs"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Low-level Pythonic bindings for libibverbs (RDMA), with GPUDirect 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 = ["rdma", "ibverbs", "infiniband", "roce", "gpudirect", "networking"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: POSIX :: Linux",
|
|
19
|
+
"Programming Language :: Cython",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Topic :: System :: Networking",
|
|
22
|
+
]
|
|
23
|
+
# No runtime dependencies: the library only needs libibverbs at load time.
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.dynamic]
|
|
27
|
+
version = { attr = "ibverbs._version.__version__" }
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
test = ["pytest>=7", "numpy"]
|
|
31
|
+
gpu = ["torch"]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/tristanr/rdma4py"
|
|
35
|
+
Repository = "https://github.com/tristanr/rdma4py"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools]
|
|
38
|
+
package-dir = { "" = "src" }
|
|
39
|
+
include-package-data = false
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
markers = [
|
|
46
|
+
"integration: exercises real RDMA hardware (loopback / two-NIC)",
|
|
47
|
+
"gpu: requires a CUDA GPU and torch (GPUDirect)",
|
|
48
|
+
]
|
|
49
|
+
testpaths = ["tests"]
|
|
50
|
+
|
|
51
|
+
# Single abi3 wheel per platform (works on CPython 3.9+). The extension is
|
|
52
|
+
# built against rdma-core headers but dlopens libibverbs at runtime, so the
|
|
53
|
+
# build image only needs the -devel headers; the wheel has no external NEEDED
|
|
54
|
+
# library and is trivially manylinux-compliant.
|
|
55
|
+
[tool.cibuildwheel]
|
|
56
|
+
# cibuildwheel selects a CPython build environment; setup.py retags the
|
|
57
|
+
# resulting extension as cp39-abi3 via Py_LIMITED_API.
|
|
58
|
+
build = "cp39-*"
|
|
59
|
+
build-frontend = "build"
|
|
60
|
+
skip = ["*-musllinux*"]
|
|
61
|
+
test-requires = ["pytest", "numpy"]
|
|
62
|
+
# Hardware/GPU tests self-skip when no NIC/CUDA is present in CI.
|
|
63
|
+
test-command = "pytest -rs -m 'not gpu' {package}/tests"
|
|
64
|
+
|
|
65
|
+
[tool.cibuildwheel.linux]
|
|
66
|
+
# manylinux images are AlmaLinux-based (dnf/yum).
|
|
67
|
+
before-all = "dnf install -y rdma-core-devel || yum install -y rdma-core-devel"
|