mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
Containers with /dev/infiniband/uverbs* devices need a sysfs surface for device discovery: libibverbs enumerates /sys/class/infiniband_verbs, and NCCL builds its hardware topology by resolving /sys/class symlinks with realpath() and walking the PCI hierarchy. This adds a --rdmaproxy flag which will gate all RDMA support. For now it only wires up the sysfs topology: runsc snapshots the host sysfs closure derived from the spec's uverbs devices (ibdevs, netdevs, PCI ancestor chains, GPU functions, NUMA nodes) before pivot_root, and the sentry constructs a path-identical virtual tree: one canonical /sys/devices/pci... subtree per device, with /sys/class/* and /sys/bus/pci/devices as symlink farms matching the kernel layout. Per-port dynamic state (RoCE GID tables, link state, counters) is served live through targeted read-only bind mounts of the per-ibdev ports subtrees; identity and PCI attributes are a static snapshot, since the collecting process and the constructed sandbox tree do not observe the same sysfs mount (the RDMA netdevs move into the sandbox network namespace only after the sandbox is created). Each PCI function directory also gets a subsystem symlink into a synthesized /sys/bus/pci, and every node's bus gets a pci_bus entry: NCCL classifies a directory as a PCI device by following its subsystem symlink, and aborts topology discovery without it. The NUMA topology is not mirrored: the sentry advertises a single node holding every sandbox CPU, consistent with its single-node memory model (set_mempolicy's maxNodes == 1) and its single-socket /proc/cpuinfo view. Host node cpumasks name CPUs the sandbox does not have, which breaks libnuma/hwloc consumers whenever the sandbox CPU count is below the host's (numactl node binding, which NUMA-aware stacks exec in front of worker subprocesses, kills the child). The PCI numa_node, local_cpus and local_cpulist attributes are rewritten to match the single-node view. The verbs device proxy that makes RDMA usable end-to-end is not part of this change; --rdmaproxy is a work in progress and RDMA is not yet functional. Co-authored-by: Alessio Ricci Toniolo <[email protected]>
129 lines
3.2 KiB
Go
129 lines
3.2 KiB
Go
// 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 rdma
|
|
|
|
import (
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
)
|
|
|
|
func TestSafeName(t *testing.T) {
|
|
for name, want := range map[string]bool{
|
|
"uverbs0": true,
|
|
"mlx5_0": true,
|
|
"0000:0c:00.0": true,
|
|
"eth0.100": true,
|
|
"enp12s0f0np0": true,
|
|
"a.b": true,
|
|
"0": true,
|
|
"": false,
|
|
".": false,
|
|
"..": false,
|
|
".hidden": false,
|
|
"-flag": false,
|
|
"_x": false,
|
|
"a/b": false,
|
|
"a b": false,
|
|
"a\n": false,
|
|
"a\x00b": false,
|
|
} {
|
|
if got := SafeName(name); got != want {
|
|
t.Errorf("SafeName(%q) = %v, want %v", name, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsBDF(t *testing.T) {
|
|
for name, want := range map[string]bool{
|
|
"0000:0c:00.0": true,
|
|
"0000:ff:1f.7": true,
|
|
// Synthetic domains above 0xffff (e.g. Intel VMD) print wider than
|
|
// %04x.
|
|
"10000:e0:06.0": true,
|
|
"0000:0c:00": false,
|
|
"0000:0c:00.8": false, // function is 0-7
|
|
"0000:0C:00.0": false, // the kernel prints lowercase hex
|
|
"000:0c:00.0": false,
|
|
"pci0000:0c": false,
|
|
"0000:0c:00.0/": false,
|
|
} {
|
|
if got := IsBDF(name); got != want {
|
|
t.Errorf("IsBDF(%q) = %v, want %v", name, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPCIRootRE(t *testing.T) {
|
|
for name, want := range map[string]bool{
|
|
"pci0000:07": true,
|
|
"pci10000:e0": true,
|
|
"pci0000": false,
|
|
"0000:07": false,
|
|
"pci0000:07:00": false,
|
|
"pci0000:0G": false,
|
|
} {
|
|
if got := pciRootRE.MatchString(name); got != want {
|
|
t.Errorf("pciRootRE.MatchString(%q) = %v, want %v", name, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAddWithAncestors(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
leaf string
|
|
want []string // nil means an error is expected
|
|
}{
|
|
{
|
|
leaf: "devices/pci0000:07/0000:07:01.0/0000:0c:00.0",
|
|
want: []string{
|
|
"devices/pci0000:07",
|
|
"devices/pci0000:07/0000:07:01.0",
|
|
"devices/pci0000:07/0000:07:01.0/0000:0c:00.0",
|
|
},
|
|
},
|
|
{
|
|
leaf: "devices/pci10000:e0/10000:e0:06.0",
|
|
want: []string{
|
|
"devices/pci10000:e0",
|
|
"devices/pci10000:e0/10000:e0:06.0",
|
|
},
|
|
},
|
|
{leaf: "devices/pci0000:07/virtual0/0000:0c:00.0", want: nil},
|
|
{leaf: "sys/devices/pci0000:07", want: nil},
|
|
} {
|
|
set := make(map[string]bool)
|
|
err := addWithAncestors(set, tc.leaf)
|
|
if tc.want == nil {
|
|
if err == nil {
|
|
t.Errorf("addWithAncestors(%q) succeeded, want error", tc.leaf)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Errorf("addWithAncestors(%q) failed: %v", tc.leaf, err)
|
|
continue
|
|
}
|
|
var got []string
|
|
for p := range set {
|
|
got = append(got, p)
|
|
}
|
|
sort.Strings(got)
|
|
if !reflect.DeepEqual(got, tc.want) {
|
|
t.Errorf("addWithAncestors(%q) = %v, want %v", tc.leaf, got, tc.want)
|
|
}
|
|
}
|
|
}
|