Skip to content

Commit 8328766

Browse files
ayushr2atoniolo76
andcommitted
Add AWS EFA support to rdmaproxy
Add an efaproxy driver plug-in for AWS Elastic Fabric Adapter (EFA) devices bound to the host `efa` driver. EFA's driver-private uverbs ABI carries no userspace buffer pointers on CQ/QP CREATE: queue memory is kernel-allocated and handed back via mmap keys in the CREATE response, which the core already forwards to the host FD verbatim. PrepareCreateDMA is therefore a no-op; the plug-in exists to register the "efa" driver name and to model EFA's MR_QUERY driver-namespace method (used by libfabric to fetch RDMA read/write interconnect IDs). Model the standard AH object's destroy method (UVERBS_OBJECT_AH): EFA (SRD) creates address handles via the legacy write path and destroys them through the modern object, which the core must allow. Extend the virtual RDMA sysfs for libfabric/hwloc topology discovery: - device/driver symlink and /sys/bus/pci/drivers/<driver> tree, which libfabric's EFA provider resolves during device discovery. - Mirror each PCI node's raw config space ("config" file). hwloc (used by aws-ofi-nccl to build the NCCL topology) reads it to recover the PCI bridge bus-number registers; without it hwloc cannot reconstruct the bridge hierarchy and aws-ofi-nccl aborts its topology write. - Serve the per-port lid_mask_count attribute alongside the other port attributes libibverbs reads from sysfs; ib_core creates it on every port. The sysfs integration tests cover the new driver tree and config mirroring. Advertise a >= 5.12 kernel release in RDMA-enabled sandboxes so aws-ofi-nccl enables dmabuf-based GPU memory registration (ibv_reg_dmabuf_mr), which the proxy handles via the existing REG_DMABUF_MR path. Sandboxes without RDMA keep the current 4.19 release to limit the bump's blast radius; a later change can make the newer release the default. Co-authored-by: Alessio Ricci Toniolo <alessio@modal.com>
1 parent 359e356 commit 8328766

20 files changed

Lines changed: 267 additions & 94 deletions

File tree

g3doc/user_guide/rdma.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ memory.
2828

2929
RDMA support is under active development. The following limitations apply:
3030

31-
* **Mellanox NICs only.** Only Mellanox ConnectX (`mlx5`) adapters are
32-
currently supported. Support for additional vendors is planned.
31+
* **Mellanox and AWS EFA NICs only.** Only Mellanox ConnectX (`mlx5`)
32+
adapters and AWS Elastic Fabric Adapters (`efa`) are currently supported.
33+
Support for additional vendors is planned.
3334

3435
* **Host kernel 5.12 or newer.** `rdmaproxy` proxies the modern
3536
`RDMA_VERBS_IOCTL` interface only; the legacy `write(2)` command
@@ -40,6 +41,11 @@ RDMA support is under active development. The following limitations apply:
4041
through the dma-buf mechanism, which is the modern default. The legacy
4142
`nvidia-peermem` kernel-module path is not supported.
4243

44+
* **EFA needs a dma-buf-capable NCCL plugin.** Because GPUDirect works only
45+
through dma-buf (above), AWS EFA requires `aws-ofi-nccl` v1.19.2 or newer:
46+
earlier releases hard-disable dma-buf on EFA device generations 1-3 and
47+
register GPU memory by virtual address instead, which is not supported.
48+
4349
* **Single-container sandboxes only.** The RDMA devices must be declared in
4450
the OCI spec of the sandbox's root container. Deployments where the
4551
devices appear only in a sub-container's spec — such as a Kubernetes pod

pkg/abi/ib/ib.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ const (
8181
UVERBS_OBJECT_PD = 1
8282
UVERBS_OBJECT_CQ = 3
8383
UVERBS_OBJECT_QP = 4
84+
UVERBS_OBJECT_AH = 6
8485
UVERBS_OBJECT_MR = 7
8586
UVERBS_OBJECT_ASYNC_EVENT = 16
8687
)
@@ -106,6 +107,11 @@ const (
106107
UVERBS_METHOD_QP_CREATE = 0
107108
UVERBS_METHOD_QP_DESTROY = 1
108109

110+
// enum uverbs_methods_ah. AH creation uses the legacy write path
111+
// (IB_USER_VERBS_CMD_CREATE_AH via INVOKE_WRITE); only destroy has a
112+
// modern object method.
113+
UVERBS_METHOD_AH_DESTROY = 0
114+
109115
// enum uverbs_methods_mr.
110116
UVERBS_METHOD_MR_DESTROY = 1
111117
UVERBS_METHOD_REG_DMABUF_MR = 4
@@ -160,6 +166,9 @@ const (
160166
// enum uverbs_attrs_destroy_pd_cmd_attr_ids.
161167
const UVERBS_ATTR_DESTROY_PD_HANDLE = 0
162168

169+
// enum uverbs_attrs_ah_destroy_ids.
170+
const UVERBS_ATTR_DESTROY_AH_HANDLE = 0
171+
163172
// enum uverbs_attrs_reg_mr_cmd_attr_ids.
164173
const (
165174
UVERBS_ATTR_REG_MR_HANDLE = 0
@@ -274,6 +283,21 @@ const (
274283
MLX5_IB_ATTR_UAR_OBJ_DESTROY_HANDLE = 0x1000
275284
)
276285

286+
// EFA driver-namespace method/attr IDs from include/uapi/rdma/efa-abi.h. EFA
287+
// extends the standard UVERBS_OBJECT_MR with a query method returning the
288+
// interconnect IDs an RDMA-read/write source MR must advertise to peers.
289+
const (
290+
// enum efa_mr_methods.
291+
EFA_IB_METHOD_MR_QUERY = 0x1000
292+
293+
// enum efa_query_mr_attrs.
294+
EFA_IB_ATTR_QUERY_MR_HANDLE = 0x1000
295+
EFA_IB_ATTR_QUERY_MR_RESP_IC_ID_VALIDITY = 0x1001
296+
EFA_IB_ATTR_QUERY_MR_RESP_RECV_IC_ID = 0x1002
297+
EFA_IB_ATTR_QUERY_MR_RESP_RDMA_READ_IC_ID = 0x1003
298+
EFA_IB_ATTR_QUERY_MR_RESP_RDMA_RECV_IC_ID = 0x1004
299+
)
300+
277301
// Legacy write(2)-path command numbers (enum ib_uverbs_write_cmds,
278302
// include/uapi/rdma/ib_user_verbs.h), as carried by the INVOKE_WRITE
279303
// WRITE_CMD attribute.

pkg/rdma/collect.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ var ibAttrNames = []string{
5555
// table repopulates when netdevs move namespaces and acquire addresses;
5656
// link state and rate can change on retrain).
5757
var portLiveAttrNames = []string{
58-
"state", "phys_state", "rate", "lid", "sm_lid", "sm_sl",
58+
"state", "phys_state", "rate", "lid", "lid_mask_count", "sm_lid", "sm_sl",
5959
}
6060

6161
// Per-port attributes that are fixed for the sandbox lifetime.
@@ -182,13 +182,18 @@ func Collect(sysRoot string, uverbs []UverbsSpec) (*Snapshot, error) {
182182
}
183183
}
184184

185-
// Materialize every PCI node with its static attributes.
185+
// Materialize every PCI node with its static attributes and config space.
186186
for p := range pciPaths {
187187
attrs, err := readAttrs(path.Join(sysRoot, p), pciAttrNames)
188188
if err != nil {
189189
return nil, fmt.Errorf("PCI node %q: %w", p, err)
190190
}
191-
s.PCINodes = append(s.PCINodes, PCINode{Path: p, Attrs: attrs})
191+
// config is best-effort: root complexes and some bridges lack it.
192+
config, err := os.ReadFile(path.Join(sysRoot, p, "config"))
193+
if err != nil && !errors.Is(err, fs.ErrNotExist) {
194+
return nil, fmt.Errorf("reading PCI config of %q: %w", p, err)
195+
}
196+
s.PCINodes = append(s.PCINodes, PCINode{Path: p, Attrs: attrs, Config: config})
192197
}
193198
sort.Slice(s.PCINodes, func(i, j int) bool { return s.PCINodes[i].Path < s.PCINodes[j].Path })
194199

pkg/rdma/snapshot.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ type PCINode struct {
4040
// Attrs maps attribute file name to contents (verbatim, including any
4141
// trailing newline).
4242
Attrs map[string]string `json:"attrs"`
43+
// Config is the raw PCI config space ("config" file), or nil if absent.
44+
// hwloc (used by aws-ofi-nccl for NCCL topology) reads it to recover the
45+
// PCI-bridge bus-number registers and PCIe link attributes; without it
46+
// hwloc cannot reconstruct the bridge hierarchy.
47+
Config []byte `json:"config,omitempty"`
4348
}
4449

4550
// Port is the per-IB-port state. Attributes split into static (immutable
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
load("//tools:defs.bzl", "go_library")
2+
3+
package(
4+
default_applicable_licenses = ["//:license"],
5+
licenses = ["notice"],
6+
)
7+
8+
go_library(
9+
name = "efaproxy",
10+
srcs = [
11+
"efaproxy.go",
12+
],
13+
visibility = [
14+
"//pkg/sentry:internal",
15+
"//runsc:__subpackages__",
16+
],
17+
deps = [
18+
"//pkg/abi/ib",
19+
"//pkg/sentry/devices/rdmaproxy",
20+
"//pkg/sentry/kernel",
21+
],
22+
)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2026 The gVisor Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package efaproxy implements the rdmaproxy.Driver plug-in for AWS Elastic
16+
// Fabric Adapter (EFA) devices bound to the host `efa` kernel driver.
17+
//
18+
// To make this driver available, Init() must be called.
19+
package efaproxy
20+
21+
import (
22+
"gvisor.dev/gvisor/pkg/abi/ib"
23+
"gvisor.dev/gvisor/pkg/sentry/devices/rdmaproxy"
24+
"gvisor.dev/gvisor/pkg/sentry/kernel"
25+
)
26+
27+
// driverName matches the DRIVER= field of
28+
// /sys/class/infiniband/<ibdev>/device/uevent for EFA adapters. runsc looks
29+
// this up via rdmaproxy.LookupDriver and attaches the resulting driver to the
30+
// corresponding uverbs device.
31+
const driverName = "efa"
32+
33+
// efaDriver is the rdmaproxy.Driver implementation for EFA adapters.
34+
type efaDriver struct{}
35+
36+
// Name implements rdmaproxy.Driver.Name.
37+
func (efaDriver) Name() string { return driverName }
38+
39+
// PrepareCreateDMA implements rdmaproxy.Driver.PrepareCreateDMA. EFA CQ/QP
40+
// CREATE command structs (efa_ibv_create_cq / efa_ibv_create_qp) carry no
41+
// userspace buffer pointers. The host kernel allocates the work-queue and
42+
// doorbell memory and hands it back through mmap keys in the CREATE *response*
43+
// (q_mmap_key, rq_mmap_key, sq_db_mmap_key, ...); userspace then mmap()s the
44+
// uverbs FD at those offsets, which the rdmaproxy core already forwards to the
45+
// host FD verbatim. There is thus no app memory to mirror or rewrite at CREATE
46+
// time, so PrepareCreateDMA is a no-op.
47+
func (efaDriver) PrepareCreateDMA(t *kernel.Task, uhwIn []byte) (*rdmaproxy.PinnedDMABufs, error) {
48+
return nil, nil
49+
}
50+
51+
// Schemas implements rdmaproxy.Driver.Schemas. EFA extends the standard MR
52+
// object with a query method returning the interconnect IDs a source MR must
53+
// advertise for RDMA read/write; libfabric's EFA provider issues it during
54+
// endpoint setup. All attributes are handles or fixed scalars, so no address
55+
// translation is needed.
56+
func (efaDriver) Schemas() map[uint32]*rdmaproxy.MethodSchema {
57+
return map[uint32]*rdmaproxy.MethodSchema{
58+
rdmaproxy.SchemaKey(ib.UVERBS_OBJECT_MR, ib.EFA_IB_METHOD_MR_QUERY): {
59+
Attrs: map[uint16]rdmaproxy.AttrType{
60+
ib.EFA_IB_ATTR_QUERY_MR_HANDLE: rdmaproxy.AttrIdr,
61+
ib.EFA_IB_ATTR_QUERY_MR_RESP_IC_ID_VALIDITY: rdmaproxy.AttrPtrOut,
62+
ib.EFA_IB_ATTR_QUERY_MR_RESP_RECV_IC_ID: rdmaproxy.AttrPtrOut,
63+
ib.EFA_IB_ATTR_QUERY_MR_RESP_RDMA_READ_IC_ID: rdmaproxy.AttrPtrOut,
64+
ib.EFA_IB_ATTR_QUERY_MR_RESP_RDMA_RECV_IC_ID: rdmaproxy.AttrPtrOut,
65+
},
66+
},
67+
}
68+
}
69+
70+
// Init registers the EFA driver plug-in with the rdmaproxy core.
71+
func Init() { rdmaproxy.RegisterDriver(efaDriver{}) }

pkg/sentry/devices/rdmaproxy/schema.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,13 @@ func buildSchemas() map[uint32]*MethodSchema {
200200
ib.UVERBS_ATTR_DESTROY_PD_HANDLE: AttrIdr,
201201
},
202202
},
203+
// AH creation rides the legacy write path (DmaInvokeWrite); only
204+
// destroy has a modern object method.
205+
SchemaKey(ib.UVERBS_OBJECT_AH, ib.UVERBS_METHOD_AH_DESTROY): {
206+
Attrs: map[uint16]AttrType{
207+
ib.UVERBS_ATTR_DESTROY_AH_HANDLE: AttrIdr,
208+
},
209+
},
203210
SchemaKey(ib.UVERBS_OBJECT_MR, ib.UVERBS_METHOD_REG_MR): {
204211
Dma: DmaMRReg, HandleAttr: ib.UVERBS_ATTR_REG_MR_HANDLE,
205212
Attrs: map[uint16]AttrType{

pkg/sentry/fsimpl/proc/tasks_files.go

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs"
2828
"gvisor.dev/gvisor/pkg/sentry/kernel"
2929
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
30+
"gvisor.dev/gvisor/pkg/sentry/kernel/version"
3031
"gvisor.dev/gvisor/pkg/sentry/ktime"
3132
"gvisor.dev/gvisor/pkg/sentry/usage"
3233
"gvisor.dev/gvisor/pkg/sentry/vfs"
@@ -394,8 +395,7 @@ func (*versionData) Generate(ctx context.Context, buf *bytes.Buffer) error {
394395
// FIXME(mpratt): Using Version from the init task SyscallTable
395396
// disregards the different version a task may have (e.g., in a uts
396397
// namespace).
397-
ver := kernelVersion(ctx)
398-
fmt.Fprintf(buf, "%s version %s %s\n", ver.Sysname, ver.Release, ver.Version)
398+
fmt.Fprintf(buf, "%s version %s %s\n", version.LinuxSysname, version.LinuxRelease(), version.LinuxVersion)
399399
return nil
400400
}
401401

@@ -442,23 +442,10 @@ var _ dynamicInode = (*cmdLineData)(nil)
442442

443443
// Generate implements vfs.DynamicByteSource.Generate.
444444
func (*cmdLineData) Generate(ctx context.Context, buf *bytes.Buffer) error {
445-
fmt.Fprintf(buf, "BOOT_IMAGE=/vmlinuz-%s-gvisor quiet\n", kernelVersion(ctx).Release)
445+
fmt.Fprintf(buf, "BOOT_IMAGE=/vmlinuz-%s quiet\n", version.LinuxRelease())
446446
return nil
447447
}
448448

449-
// kernelVersion returns the kernel version.
450-
func kernelVersion(ctx context.Context) kernel.Version {
451-
k := kernel.KernelFromContext(ctx)
452-
init := k.GlobalInit()
453-
if init == nil {
454-
// Attempted to read before the init Task is created. This can
455-
// only occur during startup, which should never need to read
456-
// this file.
457-
panic("Attempted to read version before initial Task is available")
458-
}
459-
return init.Leader().SyscallTable().Version
460-
}
461-
462449
// devicesData backs /proc/devices.
463450
//
464451
// +stateify savable

pkg/sentry/fsimpl/proc/tasks_sys.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func (fs *filesystem) newSysDir(ctx context.Context, root *auth.Credentials, k *
7474
"keys": fs.newStaticDir(ctx, root, map[string]kernfs.Inode{
7575
"maxkeys": fs.newMaxKeySizeFile(ctx, k, root),
7676
}),
77-
"osrelease": fs.newInode(ctx, root, 0444, newStaticFile(version.LinuxRelease)),
77+
"osrelease": fs.newInode(ctx, root, 0444, newStaticFile(version.LinuxRelease())),
7878
"ostype": fs.newInode(ctx, root, 0444, newStaticFile(version.LinuxSysname)),
7979
"version": fs.newInode(ctx, root, 0444, newStaticFile(version.LinuxVersion)),
8080
}),

pkg/sentry/fsimpl/sys/rdma.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ type rdmaSysfsDirs struct {
5858
class map[string]kernfs.Inode
5959
// busPCIDevices contains the /sys/bus/pci/devices symlinks.
6060
busPCIDevices map[string]kernfs.Inode
61+
// busPCIDrivers maps a kernel driver name to its
62+
// /sys/bus/pci/drivers/<driver> directory of bound-device back-symlinks.
63+
busPCIDrivers map[string]kernfs.Inode
6164
// node is the /sys/devices/system/node subtree, or nil.
6265
node kernfs.Inode
6366
}
@@ -105,6 +108,26 @@ func (fs *filesystem) newRDMASysfs(ctx context.Context, creds *auth.Credentials,
105108
classNet := map[string]string{} // netdev -> symlink target
106109
classPCIBus := map[string]string{} // bus ("0000:0c") -> symlink target
107110

111+
// driverByLeaf maps a leaf PCI function path to its kernel driver name
112+
// (from the DRIVER= line of the leaf's uevent). Used to synthesize the
113+
// device/driver symlink and /sys/bus/pci/drivers tree that libfabric's
114+
// EFA provider resolves during device discovery.
115+
driverByLeaf := map[string]string{}
116+
for i := range snap.Devices {
117+
leaf := snap.Devices[i].LeafPCI
118+
for _, n := range snap.PCINodes {
119+
if n.Path != leaf {
120+
continue
121+
}
122+
for _, line := range strings.Split(n.Attrs["uevent"], "\n") {
123+
if drv, ok := strings.CutPrefix(line, "DRIVER="); ok && rdma.SafeName(drv) {
124+
driverByLeaf[leaf] = drv
125+
}
126+
}
127+
}
128+
}
129+
classPCIDrivers := map[string][]string{} // driver -> leaf PCI paths bound to it
130+
108131
// 1. The canonical PCI hierarchy with per-level static attributes, plus
109132
// the "subsystem" symlink every PCI device carries. NCCL and other
110133
// consumers classify a directory as a PCI device by following
@@ -135,12 +158,21 @@ func (fs *filesystem) newRDMASysfs(ctx context.Context, creds *auth.Credentials,
135158
if _, ok := d.files["local_cpulist"]; ok {
136159
d.files["local_cpulist"] = cpuListString(cores)
137160
}
161+
// Raw PCI config space (binary). hwloc reads it to rebuild the PCI
162+
// bridge hierarchy; without it aws-ofi-nccl's NCCL topology write fails.
163+
if n.Config != nil {
164+
d.files["config"] = string(n.Config)
165+
}
138166
// Root complexes (pciXXXX:YY) carry no subsystem link and sit on
139167
// no parent bus; only function directories (BDFs) do.
140168
if rdma.IsBDF(path.Base(n.Path)) {
141169
// depth of n.Path below /sys == number of "../" to reach /sys.
142170
depth := strings.Count(n.Path, "/") + 1
143171
d.symlinks["subsystem"] = strings.Repeat("../", depth) + "bus/pci"
172+
if drv, ok := driverByLeaf[n.Path]; ok {
173+
d.symlinks["driver"] = strings.Repeat("../", depth) + "bus/pci/drivers/" + drv
174+
classPCIDrivers[drv] = append(classPCIDrivers[drv], n.Path)
175+
}
144176
fs.addPCIBus(root, n.Path, classPCIBus)
145177
}
146178
}
@@ -211,6 +243,7 @@ func (fs *filesystem) newRDMASysfs(ctx context.Context, creds *auth.Credentials,
211243
devices: map[string]kernfs.Inode{},
212244
class: map[string]kernfs.Inode{},
213245
busPCIDevices: map[string]kernfs.Inode{},
246+
busPCIDrivers: map[string]kernfs.Inode{},
214247
}
215248
devicesTree, ok := root.children["devices"]
216249
if !ok {
@@ -240,6 +273,17 @@ func (fs *filesystem) newRDMASysfs(ctx context.Context, creds *auth.Credentials,
240273
}
241274
}
242275

276+
// /sys/bus/pci/drivers/<driver>/<bdf> back-symlinks (the inverse of the
277+
// device/driver links added above). libfabric's EFA provider realpath's
278+
// the driver dir to confirm the bound driver during discovery.
279+
for drv, leaves := range classPCIDrivers {
280+
entries := map[string]kernfs.Inode{}
281+
for _, leaf := range leaves {
282+
entries[path.Base(leaf)] = kernfs.NewStaticSymlink(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), "../../../../"+leaf)
283+
}
284+
out.busPCIDrivers[drv] = fs.newDir(ctx, creds, defaultSysDirMode, entries)
285+
}
286+
243287
if snap.NUMA != nil {
244288
out.node = fs.buildNUMA(ctx, creds, snap.NUMA, cores)
245289
}

0 commit comments

Comments
 (0)