Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions g3doc/user_guide/rdma.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ RDMA, allowing the NIC to transfer data directly to and from GPU memory.

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

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

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

* **EFA needs a dma-buf-capable NCCL plugin.** Because GPUDirect works only
through dma-buf (above), AWS EFA requires `aws-ofi-nccl` v1.19.2 or newer:
earlier releases hard-disable dma-buf on EFA device generations 1-3 and
register GPU memory by virtual address instead, which is not supported.

* **Single-container sandboxes only.** The RDMA devices must be declared in
the OCI spec of the sandbox's root container. Deployments where the devices
appear only in a sub-container's spec — such as a Kubernetes pod where the
Expand Down
24 changes: 24 additions & 0 deletions pkg/abi/ib/ib.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const (
UVERBS_OBJECT_PD = 1
UVERBS_OBJECT_CQ = 3
UVERBS_OBJECT_QP = 4
UVERBS_OBJECT_AH = 6
UVERBS_OBJECT_MR = 7
UVERBS_OBJECT_ASYNC_EVENT = 16
)
Expand All @@ -106,6 +107,11 @@ const (
UVERBS_METHOD_QP_CREATE = 0
UVERBS_METHOD_QP_DESTROY = 1

// enum uverbs_methods_ah. AH creation uses the legacy write path
// (IB_USER_VERBS_CMD_CREATE_AH via INVOKE_WRITE); only destroy has a
// modern object method.
UVERBS_METHOD_AH_DESTROY = 0

// enum uverbs_methods_mr.
UVERBS_METHOD_MR_DESTROY = 1
UVERBS_METHOD_REG_DMABUF_MR = 4
Expand Down Expand Up @@ -160,6 +166,9 @@ const (
// UVERBS_ATTR_DESTROY_PD_HANDLE is the handle attribute ID for uverbs_attrs_destroy_pd_cmd_attr_ids.
const UVERBS_ATTR_DESTROY_PD_HANDLE = 0

// enum uverbs_attrs_ah_destroy_ids.
const UVERBS_ATTR_DESTROY_AH_HANDLE = 0

// enum uverbs_attrs_reg_mr_cmd_attr_ids.
const (
UVERBS_ATTR_REG_MR_HANDLE = 0
Expand Down Expand Up @@ -274,6 +283,21 @@ const (
MLX5_IB_ATTR_UAR_OBJ_DESTROY_HANDLE = 0x1000
)

// EFA driver-namespace method/attr IDs from include/uapi/rdma/efa-abi.h. EFA
// extends the standard UVERBS_OBJECT_MR with a query method returning the
// interconnect IDs an RDMA-read/write source MR must advertise to peers.
const (
// enum efa_mr_methods.
EFA_IB_METHOD_MR_QUERY = 0x1000

// enum efa_query_mr_attrs.
EFA_IB_ATTR_QUERY_MR_HANDLE = 0x1000
EFA_IB_ATTR_QUERY_MR_RESP_IC_ID_VALIDITY = 0x1001
EFA_IB_ATTR_QUERY_MR_RESP_RECV_IC_ID = 0x1002
EFA_IB_ATTR_QUERY_MR_RESP_RDMA_READ_IC_ID = 0x1003
EFA_IB_ATTR_QUERY_MR_RESP_RDMA_RECV_IC_ID = 0x1004
)

// Legacy write(2)-path command numbers (enum ib_uverbs_write_cmds,
// include/uapi/rdma/ib_user_verbs.h), as carried by the INVOKE_WRITE
// WRITE_CMD attribute.
Expand Down
11 changes: 8 additions & 3 deletions pkg/rdma/collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ var ibAttrNames = []string{
// table repopulates when netdevs move namespaces and acquire addresses;
// link state and rate can change on retrain).
var portLiveAttrNames = []string{
"state", "phys_state", "rate", "lid", "sm_lid", "sm_sl",
"state", "phys_state", "rate", "lid", "lid_mask_count", "sm_lid", "sm_sl",
}

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

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

Expand Down
5 changes: 5 additions & 0 deletions pkg/rdma/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ type PCINode struct {
// Attrs maps attribute file name to contents (verbatim, including any
// trailing newline).
Attrs map[string]string `json:"attrs"`
// Config is the raw PCI config space ("config" file), or nil if absent.
// hwloc (used by aws-ofi-nccl for NCCL topology) reads it to recover the
// PCI-bridge bus-number registers and PCIe link attributes; without it
// hwloc cannot reconstruct the bridge hierarchy.
Config []byte `json:"config,omitempty"`
}

// Port is the per-IB-port state. Attributes split into static (immutable
Expand Down
22 changes: 22 additions & 0 deletions pkg/sentry/devices/rdmaproxy/efaproxy/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
load("//tools:defs.bzl", "go_library")

package(
default_applicable_licenses = ["//:license"],
licenses = ["notice"],
)

go_library(
name = "efaproxy",
srcs = [
"efaproxy.go",
],
visibility = [
"//pkg/sentry:internal",
"//runsc:__subpackages__",
],
deps = [
"//pkg/abi/ib",
"//pkg/sentry/devices/rdmaproxy",
"//pkg/sentry/kernel",
],
)
71 changes: 71 additions & 0 deletions pkg/sentry/devices/rdmaproxy/efaproxy/efaproxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2026 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package efaproxy implements the rdmaproxy.Driver plug-in for AWS Elastic
// Fabric Adapter (EFA) devices bound to the host `efa` kernel driver.
//
// To make this driver available, Init() must be called.
package efaproxy

import (
"gvisor.dev/gvisor/pkg/abi/ib"
"gvisor.dev/gvisor/pkg/sentry/devices/rdmaproxy"
"gvisor.dev/gvisor/pkg/sentry/kernel"
)

// driverName matches the DRIVER= field of
// /sys/class/infiniband/<ibdev>/device/uevent for EFA adapters. runsc looks
// this up via rdmaproxy.LookupDriver and attaches the resulting driver to the
// corresponding uverbs device.
const driverName = "efa"

// efaDriver is the rdmaproxy.Driver implementation for EFA adapters.
type efaDriver struct{}

// Name implements rdmaproxy.Driver.Name.
func (efaDriver) Name() string { return driverName }

// PrepareCreateDMA implements rdmaproxy.Driver.PrepareCreateDMA. EFA CQ/QP
// CREATE command structs (efa_ibv_create_cq / efa_ibv_create_qp) carry no
// userspace buffer pointers. The host kernel allocates the work-queue and
// doorbell memory and hands it back through mmap keys in the CREATE *response*
// (q_mmap_key, rq_mmap_key, sq_db_mmap_key, ...); userspace then mmap()s the
// uverbs FD at those offsets, which the rdmaproxy core already forwards to the
// host FD verbatim. There is thus no app memory to mirror or rewrite at CREATE
// time, so PrepareCreateDMA is a no-op.
func (efaDriver) PrepareCreateDMA(t *kernel.Task, uhwIn []byte) (*rdmaproxy.PinnedDMABufs, error) {
return nil, nil
}

// Schemas implements rdmaproxy.Driver.Schemas. EFA extends the standard MR
// object with a query method returning the interconnect IDs a source MR must
// advertise for RDMA read/write; libfabric's EFA provider issues it during
// endpoint setup. All attributes are handles or fixed scalars, so no address
// translation is needed.
func (efaDriver) Schemas() map[uint32]*rdmaproxy.MethodSchema {
return map[uint32]*rdmaproxy.MethodSchema{
rdmaproxy.SchemaKey(ib.UVERBS_OBJECT_MR, ib.EFA_IB_METHOD_MR_QUERY): {
Attrs: map[uint16]rdmaproxy.AttrType{
ib.EFA_IB_ATTR_QUERY_MR_HANDLE: rdmaproxy.AttrIdr,
ib.EFA_IB_ATTR_QUERY_MR_RESP_IC_ID_VALIDITY: rdmaproxy.AttrPtrOut,
ib.EFA_IB_ATTR_QUERY_MR_RESP_RECV_IC_ID: rdmaproxy.AttrPtrOut,
ib.EFA_IB_ATTR_QUERY_MR_RESP_RDMA_READ_IC_ID: rdmaproxy.AttrPtrOut,
ib.EFA_IB_ATTR_QUERY_MR_RESP_RDMA_RECV_IC_ID: rdmaproxy.AttrPtrOut,
},
},
}
}

// Init registers the EFA driver plug-in with the rdmaproxy core.
func Init() { rdmaproxy.RegisterDriver(efaDriver{}) }
7 changes: 7 additions & 0 deletions pkg/sentry/devices/rdmaproxy/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@ func buildSchemas() map[uint32]*MethodSchema {
ib.UVERBS_ATTR_DESTROY_PD_HANDLE: AttrIdr,
},
},
// AH creation rides the legacy write path (DmaInvokeWrite); only
// destroy has a modern object method.
SchemaKey(ib.UVERBS_OBJECT_AH, ib.UVERBS_METHOD_AH_DESTROY): {
Attrs: map[uint16]AttrType{
ib.UVERBS_ATTR_DESTROY_AH_HANDLE: AttrIdr,
},
},
SchemaKey(ib.UVERBS_OBJECT_MR, ib.UVERBS_METHOD_REG_MR): {
Dma: DmaMRReg, HandleAttr: ib.UVERBS_ATTR_REG_MR_HANDLE,
Attrs: map[uint16]AttrType{
Expand Down
19 changes: 3 additions & 16 deletions pkg/sentry/fsimpl/proc/tasks_files.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs"
"gvisor.dev/gvisor/pkg/sentry/kernel"
"gvisor.dev/gvisor/pkg/sentry/kernel/auth"
"gvisor.dev/gvisor/pkg/sentry/kernel/version"
"gvisor.dev/gvisor/pkg/sentry/ktime"
"gvisor.dev/gvisor/pkg/sentry/usage"
"gvisor.dev/gvisor/pkg/sentry/vfs"
Expand Down Expand Up @@ -394,8 +395,7 @@ func (*versionData) Generate(ctx context.Context, buf *bytes.Buffer) error {
// FIXME(mpratt): Using Version from the init task SyscallTable
// disregards the different version a task may have (e.g., in a uts
// namespace).
ver := kernelVersion(ctx)
fmt.Fprintf(buf, "%s version %s %s\n", ver.Sysname, ver.Release, ver.Version)
fmt.Fprintf(buf, "%s version %s %s\n", version.LinuxSysname, version.LinuxRelease(), version.LinuxVersion)
return nil
}

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

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

// kernelVersion returns the kernel version.
func kernelVersion(ctx context.Context) kernel.Version {
k := kernel.KernelFromContext(ctx)
init := k.GlobalInit()
if init == nil {
// Attempted to read before the init Task is created. This can
// only occur during startup, which should never need to read
// this file.
panic("Attempted to read version before initial Task is available")
}
return init.Leader().SyscallTable().Version
}

// devicesData backs /proc/devices.
//
// +stateify savable
Expand Down
2 changes: 1 addition & 1 deletion pkg/sentry/fsimpl/proc/tasks_sys.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func (fs *filesystem) newSysDir(ctx context.Context, root *auth.Credentials, k *
"keys": fs.newStaticDir(ctx, root, map[string]kernfs.Inode{
"maxkeys": fs.newMaxKeySizeFile(ctx, k, root),
}),
"osrelease": fs.newInode(ctx, root, 0444, newStaticFile(version.LinuxRelease)),
"osrelease": fs.newInode(ctx, root, 0444, newStaticFile(version.LinuxRelease())),
"ostype": fs.newInode(ctx, root, 0444, newStaticFile(version.LinuxSysname)),
"version": fs.newInode(ctx, root, 0444, newStaticFile(version.LinuxVersion)),
}),
Expand Down
44 changes: 44 additions & 0 deletions pkg/sentry/fsimpl/sys/rdma.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ type rdmaSysfsDirs struct {
class map[string]kernfs.Inode
// busPCIDevices contains the /sys/bus/pci/devices symlinks.
busPCIDevices map[string]kernfs.Inode
// busPCIDrivers maps a kernel driver name to its
// /sys/bus/pci/drivers/<driver> directory of bound-device back-symlinks.
busPCIDrivers map[string]kernfs.Inode
// node is the /sys/devices/system/node subtree, or nil.
node kernfs.Inode
}
Expand Down Expand Up @@ -105,6 +108,26 @@ func (fs *filesystem) newRDMASysfs(ctx context.Context, creds *auth.Credentials,
classNet := map[string]string{} // netdev -> symlink target
classPCIBus := map[string]string{} // bus ("0000:0c") -> symlink target

// driverByLeaf maps a leaf PCI function path to its kernel driver name
// (from the DRIVER= line of the leaf's uevent). Used to synthesize the
// device/driver symlink and /sys/bus/pci/drivers tree that libfabric's
// EFA provider resolves during device discovery.
driverByLeaf := map[string]string{}
for i := range snap.Devices {
leaf := snap.Devices[i].LeafPCI
for _, n := range snap.PCINodes {
if n.Path != leaf {
continue
}
for _, line := range strings.Split(n.Attrs["uevent"], "\n") {
if drv, ok := strings.CutPrefix(line, "DRIVER="); ok && rdma.SafeName(drv) {
driverByLeaf[leaf] = drv
}
}
}
}
classPCIDrivers := map[string][]string{} // driver -> leaf PCI paths bound to it

// 1. The canonical PCI hierarchy with per-level static attributes, plus
// the "subsystem" symlink every PCI device carries. NCCL and other
// consumers classify a directory as a PCI device by following
Expand Down Expand Up @@ -135,12 +158,21 @@ func (fs *filesystem) newRDMASysfs(ctx context.Context, creds *auth.Credentials,
if _, ok := d.files["local_cpulist"]; ok {
d.files["local_cpulist"] = cpuListString(cores)
}
// Raw PCI config space (binary). hwloc reads it to rebuild the PCI
// bridge hierarchy; without it aws-ofi-nccl's NCCL topology write fails.
if n.Config != nil {
d.files["config"] = string(n.Config)
}
// Root complexes (pciXXXX:YY) carry no subsystem link and sit on
// no parent bus; only function directories (BDFs) do.
if rdma.IsBDF(path.Base(n.Path)) {
// depth of n.Path below /sys == number of "../" to reach /sys.
depth := strings.Count(n.Path, "/") + 1
d.symlinks["subsystem"] = strings.Repeat("../", depth) + "bus/pci"
if drv, ok := driverByLeaf[n.Path]; ok {
d.symlinks["driver"] = strings.Repeat("../", depth) + "bus/pci/drivers/" + drv
classPCIDrivers[drv] = append(classPCIDrivers[drv], n.Path)
}
fs.addPCIBus(root, n.Path, classPCIBus)
}
}
Expand Down Expand Up @@ -211,6 +243,7 @@ func (fs *filesystem) newRDMASysfs(ctx context.Context, creds *auth.Credentials,
devices: map[string]kernfs.Inode{},
class: map[string]kernfs.Inode{},
busPCIDevices: map[string]kernfs.Inode{},
busPCIDrivers: map[string]kernfs.Inode{},
}
devicesTree, ok := root.children["devices"]
if !ok {
Expand Down Expand Up @@ -240,6 +273,17 @@ func (fs *filesystem) newRDMASysfs(ctx context.Context, creds *auth.Credentials,
}
}

// /sys/bus/pci/drivers/<driver>/<bdf> back-symlinks (the inverse of the
// device/driver links added above). libfabric's EFA provider realpath's
// the driver dir to confirm the bound driver during discovery.
for drv, leaves := range classPCIDrivers {
entries := map[string]kernfs.Inode{}
for _, leaf := range leaves {
entries[path.Base(leaf)] = kernfs.NewStaticSymlink(ctx, creds, linux.UNNAMED_MAJOR, fs.devMinor, fs.NextIno(), "../../../../"+leaf)
}
out.busPCIDrivers[drv] = fs.newDir(ctx, creds, defaultSysDirMode, entries)
}

if snap.NUMA != nil {
out.node = fs.buildNUMA(ctx, creds, snap.NUMA, cores)
}
Expand Down
17 changes: 13 additions & 4 deletions pkg/sentry/fsimpl/sys/sys.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ func (fsType FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt
productName := ""
busSub := make(map[string]kernfs.Inode) // /sys/bus
pciDevices := make(map[string]kernfs.Inode) // /sys/bus/pci/devices
pciDrivers := make(map[string]kernfs.Inode) // /sys/bus/pci/drivers
kernelSub := kernelDir(ctx, fs, creds) // /sys/kernel
if opts.InternalData != nil {
idata := opts.InternalData.(*InternalData)
Expand Down Expand Up @@ -204,15 +205,23 @@ func (fsType FilesystemType) GetFilesystem(ctx context.Context, vfsObj *vfs.Virt
for name, sub := range rdmaDirs.busPCIDevices {
pciDevices[name] = sub
}
for name, sub := range rdmaDirs.busPCIDrivers {
pciDrivers[name] = sub
}
if rdmaDirs.node != nil {
systemSub["node"] = rdmaDirs.node
}
}
}
if len(pciDevices) > 0 {
busSub["pci"] = fs.newDir(ctx, creds, defaultSysDirMode, map[string]kernfs.Inode{
"devices": fs.newDir(ctx, creds, defaultSysDirMode, pciDevices),
})
if len(pciDevices) > 0 || len(pciDrivers) > 0 {
pciSub := map[string]kernfs.Inode{}
if len(pciDevices) > 0 {
pciSub["devices"] = fs.newDir(ctx, creds, defaultSysDirMode, pciDevices)
}
if len(pciDrivers) > 0 {
pciSub["drivers"] = fs.newDir(ctx, creds, defaultSysDirMode, pciDrivers)
}
busSub["pci"] = fs.newDir(ctx, creds, defaultSysDirMode, pciSub)
}
devicesSub["system"] = fs.newDir(ctx, creds, defaultSysDirMode, systemSub)

Expand Down
Loading