mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
Passing --device-map to cuda-checkpoint isn't implemented yet, but see comment in runsc/boot/nvproxy.go:getNvproxyDeviceRemapIDs(). PiperOrigin-RevId: 937715462
391 lines
15 KiB
Go
391 lines
15 KiB
Go
// Copyright 2023 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 nvproxy
|
|
|
|
import (
|
|
goContext "context"
|
|
"fmt"
|
|
"path/filepath"
|
|
|
|
"golang.org/x/sys/unix"
|
|
"gvisor.dev/gvisor/pkg/abi/nvgpu"
|
|
"gvisor.dev/gvisor/pkg/context"
|
|
"gvisor.dev/gvisor/pkg/devutil"
|
|
"gvisor.dev/gvisor/pkg/fdnotifier"
|
|
"gvisor.dev/gvisor/pkg/log"
|
|
"gvisor.dev/gvisor/pkg/sentry/vfs"
|
|
)
|
|
|
|
type saveRestoreGoogleContextID int
|
|
|
|
const (
|
|
// CtxDeviceRemapping is a Context.Value key for a DeviceRemapping.
|
|
CtxDeviceRemapping saveRestoreGoogleContextID = iota
|
|
)
|
|
|
|
// DeviceRemappingFromContext returns the DeviceRemapping associated with ctx,
|
|
// or nil if no such DeviceRemapping exists.
|
|
func DeviceRemappingFromContext(ctx goContext.Context) *DeviceRemapping {
|
|
if r := ctx.Value(CtxDeviceRemapping); r != nil {
|
|
return r.(*DeviceRemapping)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// AnyFrontendDevicesOpen returns true if at least one "frontend device"
|
|
// (/dev/nvidia# or /dev/nvidiactl) has at least one open file descriptor.
|
|
func AnyFrontendDevicesOpen(vfsObj *vfs.VirtualFilesystem) bool {
|
|
nvp := nvproxyFromVFS(vfsObj)
|
|
if nvp == nil {
|
|
return false
|
|
}
|
|
nvp.fdsMu.Lock()
|
|
defer nvp.fdsMu.Unlock()
|
|
return len(nvp.frontendFDs) != 0
|
|
}
|
|
|
|
// DeviceRemapID identifies a device for remapping across save/restore.
|
|
type DeviceRemapID struct {
|
|
// Minor is the minor device number. This is assigned by
|
|
// kernel-open/nvidia/nv-pci.c:nv_pci_probe() =>
|
|
// kernel-open/nvidia/nv.c:nv_linux_add_device_locked().
|
|
Minor uint32 `json:"minor"`
|
|
|
|
// PCIVendorID and PCIDeviceID are PCI vendor and device IDs respectively.
|
|
// These are properties of the underlying hardware.
|
|
PCIVendorID uint16 `json:"pci_vendor_id"`
|
|
PCIDeviceID uint16 `json:"pci_device_id"`
|
|
|
|
// GPUID is nv_state_t::gpu_id. This is generated by
|
|
// src/nvidia/arch/nvalloc/unix/src/osinit.c:RmInitPrivateState() =>
|
|
// nv_generate_id_from_pci_info().
|
|
GPUID uint32 `json:"gpu_id"`
|
|
|
|
// DeviceInstance is the "broadcast device instance number", or
|
|
// equivalently the value of NV0080_ALLOC_PARAMETERS::deviceId, or
|
|
// equivalently the index of this device in OBJGPUMGR::pGpuGrpTable. This
|
|
// is assigned by
|
|
// src/nvidia/src/kernel/gpu_mgr/gpu_mgr.c:gpumgrCreateDevice() =>
|
|
// gpumgrAllocDeviceInstance().
|
|
DeviceInstance uint32 `json:"device_instance"`
|
|
|
|
// SubDeviceInstance is the "unicast subdevice instance number", or
|
|
// equivalently the value of NV2080_ALLOC_PARAMETERS::subDeviceId, or
|
|
// equivalently the value of OBJGPU::subdeviceInstance. This is set by an
|
|
// invocation of NV2080_CTRL_CMD_GPU_SET_SDM.
|
|
SubDeviceInstance uint32 `json:"subdevice_instance"`
|
|
|
|
// UUID is the "UUID", including the "GPU-" prefix, as printed by
|
|
// `nvidia-smi -L`. This is provided by the GSP, and retrieved and
|
|
// stringified by src/nvidia/src/kernel/gpu/gpu.c:gpuGetGidInfo_IMPL().
|
|
UUID string `json:"uuid"`
|
|
|
|
// If DevGoferClient is not nil, it provides access to the represented
|
|
// device.
|
|
DevGoferClient *devutil.GoferClient `json:"-"`
|
|
}
|
|
|
|
// String implements fmt.Stringer.String.
|
|
func (id *DeviceRemapID) String() string {
|
|
return fmt.Sprintf("{Minor:%d PCIVendorID:0x%04x PCIDeviceID:0x%04x GPUID:%#x DeviceInstance:%d SubDeviceInstance:%d UUID:%s}", id.Minor, id.PCIVendorID, id.PCIDeviceID, id.GPUID, id.DeviceInstance, id.SubDeviceInstance, id.UUID)
|
|
}
|
|
|
|
// CheckDevicesRemappable checks that the set of devices in ids can be saved.
|
|
func CheckDevicesRemappable(ids []DeviceRemapID) error {
|
|
if len(ids) == 0 {
|
|
return nil
|
|
}
|
|
// Check that all hardware is identical. MakeDeviceRemapping currently
|
|
// relies on this property during both save and restore to avoid having to
|
|
// remap based on hardware identity. This property is satisfied on all
|
|
// known GCE GPU instance types.
|
|
for _, id := range ids[1:] {
|
|
if ids[0].PCIVendorID != id.PCIVendorID || ids[0].PCIDeviceID != id.PCIDeviceID {
|
|
return fmt.Errorf("can't remap with heterogeneous GPUs: devices %v and %v have different PCI vendor/device IDs", ids[0], id)
|
|
}
|
|
}
|
|
// Check that all subdevice instances are 0, which implies that each device
|
|
// has a distinct device instance. This is currently relied upon to avoid
|
|
// having to preserve device-subdevice relationships in
|
|
// MakeDeviceRemapping(), or change them in nvproxy.afterLoadImpl(). This
|
|
// should be the case when using the OSS kernel driver, since
|
|
// src/nvidia/src/kernel/gpu_mgr/gpu_mgr.c:gpumgrCreateDevice() returns
|
|
// NV_ERR_INVALID_REQUEST if !gpumgrIsSubDeviceCountOne() (based on the
|
|
// code structure, there is likely redacted handling for the
|
|
// !gpumgrIsSubDeviceCountOne() case that is only present in the
|
|
// proprietary driver).
|
|
for _, id := range ids {
|
|
if id.SubDeviceInstance != 0 {
|
|
return fmt.Errorf("device %v has non-zero subdevice instance; all devices: %v", id, ids)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeviceRemapping specifies the remapping from a set of saved devices to a set
|
|
// of devices available during restore.
|
|
type DeviceRemapping struct {
|
|
// XByY maps Y to X.
|
|
NewDeviceByOld map[*DeviceRemapID]*DeviceRemapID
|
|
OldDeviceByMinor map[uint32]*DeviceRemapID
|
|
OldDeviceByDeviceInstance map[uint32]*DeviceRemapID
|
|
}
|
|
|
|
// MakeDeviceRemapping produces a DeviceRemapping. It takes ownership of oldIDs
|
|
// and newIDs.
|
|
//
|
|
// Preconditions: CheckDevicesRemappable(oldIDs) == nil.
|
|
func MakeDeviceRemapping(oldIDs, newIDs []DeviceRemapID) (*DeviceRemapping, error) {
|
|
if len(oldIDs) > len(newIDs) {
|
|
return nil, fmt.Errorf("can't remap %d saved devices to %d restored devices", len(oldIDs), len(newIDs))
|
|
}
|
|
if len(oldIDs) == 0 {
|
|
return nil, nil
|
|
}
|
|
if err := CheckDevicesRemappable(newIDs); err != nil {
|
|
return nil, err
|
|
}
|
|
dr := &DeviceRemapping{
|
|
NewDeviceByOld: make(map[*DeviceRemapID]*DeviceRemapID),
|
|
OldDeviceByMinor: make(map[uint32]*DeviceRemapID),
|
|
OldDeviceByDeviceInstance: make(map[uint32]*DeviceRemapID),
|
|
}
|
|
for i := range oldIDs {
|
|
oldID := &oldIDs[i]
|
|
newID := &newIDs[i]
|
|
dr.NewDeviceByOld[oldID] = newID
|
|
dr.OldDeviceByMinor[oldID.Minor] = oldID
|
|
dr.OldDeviceByDeviceInstance[oldID.DeviceInstance] = oldID
|
|
}
|
|
return dr, nil
|
|
}
|
|
|
|
// beforeSave is invoked by stateify.
|
|
func (nvp *nvproxy) beforeSave() {
|
|
// No need to hold any locks since the sandbox is paused.
|
|
// This avoids lock inversion between nvp.clientsMu and rootClient.objsMu.
|
|
for _, client := range nvp.clients {
|
|
for _, obj := range client.resources {
|
|
if _, ok := obj.impl.(restorableObjectImpl); !ok {
|
|
panic(fmt.Sprintf("nvproxy: object handle %v:%v (class %v, type %T) is not restorable", client.handle, obj.handle, obj.class, obj.impl))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// afterLoad is invoked by stateify.
|
|
func (nvp *nvproxy) afterLoad(ctx goContext.Context) {
|
|
Init()
|
|
abiEntry, ok := abis[nvp.version]
|
|
if !ok {
|
|
panic(fmt.Sprintf("driver version %q not found in abis map", nvp.version))
|
|
}
|
|
nvp.abi = abiEntry.cons()
|
|
if dr := DeviceRemappingFromContext(ctx); dr != nil {
|
|
// Remap frontendFDs to their new frontendDevices.
|
|
// frontendFD.vfsfd.VirtualDentry() will still point to the original
|
|
// dentry, so e.g. /proc/[pid]/fd/ will be incorrect, but we don't have
|
|
// a way to get the correct dentry, which may not even exist
|
|
// (frontendFDs might have been opened from application-created device
|
|
// special files in any filesystem not mounted nodev).
|
|
func() {
|
|
nvp.fdsMu.Lock()
|
|
defer nvp.fdsMu.Unlock()
|
|
for fd := range nvp.frontendFDs {
|
|
oldMinor := fd.dev.minor
|
|
if oldMinor > nvgpu.NV_MINOR_DEVICE_NUMBER_REGULAR_MAX {
|
|
continue
|
|
}
|
|
oldDev, ok := dr.OldDeviceByMinor[oldMinor]
|
|
if !ok {
|
|
panic(fmt.Sprintf("nvproxy: remapping does not contain a saved device with minor number %d: %+v", oldMinor, dr))
|
|
}
|
|
fd.dev = nvp.regularDevs[dr.NewDeviceByOld[oldDev].Minor]
|
|
}
|
|
}()
|
|
// Remap objects to their new devices.
|
|
// Calls to CheckDevicesRemappable() already checked that all subdevice
|
|
// instances are 0, so we don't need to update any NV20_SUBDEVICE_0
|
|
// objects.
|
|
for _, client := range nvp.clients {
|
|
for _, obj := range client.resources {
|
|
if obj.class == nvgpu.NV01_DEVICE_0 {
|
|
obj := obj.impl.(*rmAllocObject)
|
|
var allocParams nvgpu.NV0080_ALLOC_PARAMETERS
|
|
// src/nvidia/src/kernel/gpu/device.c:deviceConstruct_IMPL()
|
|
// treats NULL allocParams with class NV01_DEVICE_0 as
|
|
// equivalent to (what Go calls) the zero value.
|
|
if len(obj.params.allocParams) != 0 {
|
|
allocParams.UnmarshalBytes(obj.params.allocParams)
|
|
}
|
|
oldDev, ok := dr.OldDeviceByDeviceInstance[allocParams.DeviceID]
|
|
if !ok {
|
|
panic(fmt.Sprintf("nvproxy: remapping does not contain a saved device with device instance %d: %+v", allocParams.DeviceID, dr))
|
|
}
|
|
allocParams.DeviceID = dr.NewDeviceByOld[oldDev].DeviceInstance
|
|
if size := allocParams.SizeBytes(); len(obj.params.allocParams) < size {
|
|
obj.params.allocParams = make([]byte, size)
|
|
}
|
|
allocParams.MarshalBytes(obj.params.allocParams)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ensure that frontendFDs have host FDs before restoring objects that may
|
|
// use them.
|
|
func() {
|
|
nvp.fdsMu.Lock()
|
|
defer nvp.fdsMu.Unlock()
|
|
for fd := range nvp.frontendFDs {
|
|
fd.load(ctx)
|
|
}
|
|
}()
|
|
|
|
// Produce a topological sort of all live objects so that we can restore
|
|
// each object before its dependents. Algorithm adapted from Cormen et al.,
|
|
// "Introduction to Algorithms", 3rd ed., 2009, Sec. 22.4 "Topological
|
|
// sort".
|
|
objsVisited := make(map[*object]bool) // false => discovered, true => finished
|
|
var objsSorted []*object
|
|
var dfsConsider func(obj *object)
|
|
dfsConsider = func(obj *object) {
|
|
if finished, ok := objsVisited[obj]; ok {
|
|
if !finished {
|
|
panic(fmt.Sprintf("nvproxy: circular dependency through object handle %v:%v (class %v)", obj.client.handle, obj.handle, obj.class))
|
|
}
|
|
return
|
|
}
|
|
objsVisited[obj] = false
|
|
for dep := range obj.deps {
|
|
dfsConsider(dep)
|
|
}
|
|
objsVisited[obj] = true
|
|
objsSorted = append(objsSorted, obj)
|
|
}
|
|
// No need to hold any locks since the sandbox is paused.
|
|
// This avoids lock inversion between nvp.clientsMu and rootClient.objsMu.
|
|
for _, client := range nvp.clients {
|
|
for _, obj := range client.resources {
|
|
dfsConsider(obj)
|
|
}
|
|
}
|
|
|
|
// Restore objects in sorted order.
|
|
var depHs []nvgpu.Handle
|
|
for _, obj := range objsSorted {
|
|
for dep := range obj.deps {
|
|
depHs = append(depHs, dep.handle)
|
|
}
|
|
// This type assertion was checked in nvp.beforeSave().
|
|
if err := obj.impl.(restorableObjectImpl).Restore(ctx); err != nil {
|
|
panic(fmt.Sprintf("nvproxy: failed to restore object handle %v:%v (class %v, type %T, dependencies %v): %v", obj.client.handle, obj.handle, obj.class, obj.impl, depHs, err))
|
|
}
|
|
if log.IsLogging(log.Debug) {
|
|
log.Debugf("nvproxy: restored object handle %v:%v (class %v, type %T, dependencies %v)", obj.client.handle, obj.handle, obj.class, obj.impl, depHs)
|
|
}
|
|
// Reuse slice across iterations.
|
|
depHs = depHs[:0]
|
|
}
|
|
}
|
|
|
|
func (fd *frontendFD) load(ctx goContext.Context) {
|
|
basename := fd.dev.basename()
|
|
fd.hostFD = openHostDevFileForRestore(ctx, basename, fd.dev.nvp.useDevGofer, fd.containerName, fd.vfsfd.StatusFlags())
|
|
if err := fdnotifier.AddFD(fd.hostFD, &fd.internalQueue); err != nil {
|
|
panic(fmt.Sprintf("fdnotifier.AddFD(%d) failed for %s device: %v", fd.hostFD, basename, err))
|
|
}
|
|
fd.memmapFile.SetFD(int(fd.hostFD))
|
|
}
|
|
|
|
// beforeSave is invoked by stateify.
|
|
func (fd *frontendFD) beforeSave() {
|
|
// no-op
|
|
}
|
|
|
|
// afterLoad is invoked by stateify.
|
|
func (fd *frontendFD) afterLoad(ctx goContext.Context) {
|
|
// no-op: nvproxy.afterLoad() => frontendFD.load() restores fd.hostFD
|
|
}
|
|
|
|
// beforeSave is invoked by stateify.
|
|
func (fd *uvmFD) beforeSave() {
|
|
// no-op
|
|
}
|
|
|
|
// afterLoad is invoked by stateify.
|
|
func (fd *uvmFD) afterLoad(ctx goContext.Context) {
|
|
fd.hostFD = openHostDevFileForRestore(ctx, "nvidia-uvm", fd.dev.nvp.useDevGofer, fd.containerName, fd.vfsfd.StatusFlags())
|
|
if err := fdnotifier.AddFD(fd.hostFD, &fd.queue); err != nil {
|
|
panic(fmt.Sprintf("fdnotifier.AddFD(%d) failed for UVM device: %v", fd.hostFD, err))
|
|
}
|
|
fd.memmapFile.SetFD(int(fd.hostFD))
|
|
}
|
|
|
|
// openHostDevFileForRestore differs from openHostDevFile in the following ways:
|
|
// - The context passed to restore is not bound to a specific container, so
|
|
// devutil.CtxDevGoferClient is not set. Instead, a container name is passed by the
|
|
// caller.
|
|
// - For the same reason, openHostDevFileForRestore does not return a container name.
|
|
// - Restore is unable to propagate errors except via panics, so
|
|
// openHostDevFileForRestore panics on failure rather than restoring an error.
|
|
func openHostDevFileForRestore(ctx goContext.Context, relpath string, useDevGofer bool, containerName string, openFlags uint32) int32 {
|
|
if useDevGofer {
|
|
clientProvider := devutil.GoferClientProviderFromContext(ctx)
|
|
if clientProvider == nil {
|
|
panic("devutil.CtxDevGoferClientProvider is not set")
|
|
}
|
|
devClient := clientProvider.GetDevGoferClient(containerName)
|
|
hostFD, err := devClient.OpenAt(ctx.(context.Context), relpath, openFlags)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to open device gofer %s: %v", relpath, err))
|
|
}
|
|
return int32(hostFD)
|
|
}
|
|
abspath := filepath.Join("/dev", relpath)
|
|
hostFD, err := unix.Openat(-1, abspath, int(openFlags&unix.O_ACCMODE|unix.O_NOFOLLOW), 0)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to open host %s: %v", abspath, err))
|
|
}
|
|
return int32(hostFD)
|
|
}
|
|
|
|
type restorableObjectImpl interface {
|
|
objectImpl
|
|
|
|
// Restore recreates the represented object in the given driver FD.
|
|
Restore(ctx goContext.Context) error
|
|
}
|
|
|
|
// Restore implements restorableObjectImpl.Restore.
|
|
func (o *rmAllocObject) Restore(ctx goContext.Context) error {
|
|
return o.params.restore()
|
|
}
|
|
|
|
// Restore implements restorableObjectImpl.Restore.
|
|
func (c *rootClient) Restore(ctx goContext.Context) error {
|
|
// For root clients, the requested handle need to be passed as hRoot rather
|
|
// than hObjectNew; see
|
|
// src/nvidia/src/kernel/rmapi/entry_points.c:_nv04AllocWithAccessSecInfo()
|
|
// => src/nvidia/src/kernel/rmapi/alloc_free.c:rmapiAllocWithSecInfo()
|
|
// (renamed to hClient and *phObject respectively) => _rmAlloc() =>
|
|
// src/nvidia/src/libraries/resserv/src/rs_server.c:serverAllocResource()
|
|
// => serverAllocClient() => _serverCreateEntryAndLockForNewClient().
|
|
// RS_COMPATABILITY_MODE is defined to 1 in src/nvidia/Makefile.
|
|
temp := c.params
|
|
temp.ioctlParams.HRoot = c.handle
|
|
return temp.restore()
|
|
}
|