forked from kubernetes-sigs/dra-driver-nvidia-gpu
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvfio-device.go
More file actions
376 lines (330 loc) · 11.4 KB
/
Copy pathvfio-device.go
File metadata and controls
376 lines (330 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
/*
Copyright The Kubernetes 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
https://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 main
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"sync"
"time"
"k8s.io/klog/v2"
)
const (
kernelIommuGroupPath = "/sys/kernel/iommu_groups"
vfioPciModule = "vfio_pci"
vfioPciDriver = "vfio-pci"
nvidiaDriver = "nvidia"
hostRoot = "/host-root"
sysModulePath = "/sys/module"
pciDevicesPath = "/sys/bus/pci/devices"
vfioDevicesRoot = "/dev/vfio"
vfioDevicesPath = "/dev/vfio/devices"
iommuDevicePath = "/dev/iommu"
nvidiaPersistencedSocketPath = "/run/nvidia-persistenced/socket"
unbindFromDriverScript = "/usr/bin/unbind_from_driver.sh"
bindToDriverScript = "/usr/bin/bind_to_driver.sh"
gpuFreeCheckInterval = 1 * time.Second
gpuFreeCheckTimeout = 60 * time.Second
)
type VfioPciManager struct {
sync.Mutex
containerDriverRoot string
hostDriverRoot string
driver string
nvlib *deviceLib
nvidiaEnabled bool
}
func NewVfioPciManager(containerDriverRoot string, hostDriverRoot string, nvlib *deviceLib, nvidiaEnabled bool) (*VfioPciManager, error) {
if loaded, err := checkVfioPCIModuleLoaded(); err == nil {
if !loaded {
err = loadVfioPciModule()
if err != nil {
return nil, fmt.Errorf("failed to load vfio_pci module: %w", err)
}
}
} else {
return nil, fmt.Errorf("error checking if vfio_pci module is loaded: %w", err)
}
iommuEnabled, err := checkIommuEnabled()
if err != nil {
return nil, fmt.Errorf("error checking if IOMMU is enabled: %w", err)
}
if !iommuEnabled {
return nil, fmt.Errorf("IOMMU is not enabled in the kernel")
}
vm := &VfioPciManager{
containerDriverRoot: containerDriverRoot,
hostDriverRoot: hostDriverRoot,
driver: vfioPciDriver,
nvlib: nvlib,
nvidiaEnabled: nvidiaEnabled,
}
return vm, nil
}
// WaitForGPUFree does a best effort scan of the GPU clients running on the host and
// waits for them to exit on their own.
//
// This polls the GPU's /dev/nvidia* device node in the driver installation path on
// the host periodically to see if any process has open fds to it. This acts as a
// limited safety net to ensure that we don't mistakenly try to unbind a GPU from
// the nvidia driver while it is busy.
// Note: Here, we can only check if there are any GPU clients running on the host rootfs
// where the driver is installed. If you have containerized GPU clients that work
// with their own view of the device nodes, we will not able to detect it.
func (vm *VfioPciManager) WaitForGPUFree(ctx context.Context, info *VfioDeviceInfo) error {
if info.parent == nil {
return nil
}
timeout := time.After(gpuFreeCheckTimeout)
ticker := time.NewTicker(gpuFreeCheckInterval)
defer ticker.Stop()
gpuDeviceNode := filepath.Join(vm.hostDriverRoot, "dev", fmt.Sprintf("nvidia%d", info.parent.minor))
var err error
for {
select {
case <-timeout:
return fmt.Errorf("timed out waiting for gpu to be free: %w", err)
case <-ticker.C:
out, cmdErr := execCommandWithChroot(hostRoot, "fuser", []string{gpuDeviceNode}) //nolint:gosec
if cmdErr != nil {
// fuser returns exit code 1 if no process is using the device.
if exitErr, ok := cmdErr.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
return nil
}
err = fmt.Errorf("unexpected error checking if gpu device %q is free: %w", info.PciBusID, cmdErr)
klog.V(6).Infof("[DEBUG] %s", err.Error())
continue
}
err = fmt.Errorf("gpu device %q has open fds by process(es): %q", info.PciBusID, string(out))
klog.V(6).Infof("[DEBUG] %s", err.Error())
}
}
}
// Verify there are no VFs on the GPU.
func (vm *VfioPciManager) verifyDisabledVFs(pciBusID string) error {
gpu, err := vm.nvlib.nvpci.GetGPUByPciBusID(pciBusID)
if err != nil {
return err
}
if gpu == nil {
return fmt.Errorf("no GPU found at PCI bus ID %q", pciBusID)
}
// PhysicalFunction is nil for GPUs that do not support SR-IOV (e.g. T400).
// A nil PhysicalFunction means no VFs can exist, so it is safe to proceed.
if gpu.SriovInfo.PhysicalFunction == nil {
return nil
}
numVFs := gpu.SriovInfo.PhysicalFunction.NumVFs
if numVFs > 0 {
return fmt.Errorf("gpu has %d VFs, cannot unbind", numVFs)
}
return nil
}
// Configure binds the GPU to the vfio-pci driver.
func (vm *VfioPciManager) Configure(ctx context.Context, info *VfioDeviceInfo) error {
driver, err := getDriver(pciDevicesPath, info.PciBusID)
if err != nil {
return fmt.Errorf("error getting driver details for GPU %q: %w", info.PciBusID, err)
}
// Skip if the GPU is already bound to the vfio-pci driver.
if driver == vm.driver {
return nil
}
// Only support vfio-pci or nvidia (if vm.nvidiaEnabled) driver.
if !vm.nvidiaEnabled || driver != nvidiaDriver {
return fmt.Errorf("GPU %q is bound to %q driver, expected %q or %q", info.PciBusID, driver, vm.driver, nvidiaDriver)
}
// Disable GPU Persistence Mode.
err = vm.disableGPUPersistenceMode(info.PciBusID)
if err != nil {
return fmt.Errorf("error disabling persistence mode for GPU %q: %w", info.PciBusID, err)
}
// Wait for other GPU clients to evacuate.
err = vm.WaitForGPUFree(ctx, info)
if err != nil {
return fmt.Errorf("error waiting for GPU %q to be free: %w", info.PciBusID, err)
}
// Verify SRIOV VFs are disabled on the GPU.
err = vm.verifyDisabledVFs(info.PciBusID)
if err != nil {
return fmt.Errorf("error verifying disabled VFs: %w", err)
}
// Change the GPU driver to vfio-pci.
err = vm.changeDriver(info.PciBusID, vm.driver)
if err != nil {
return fmt.Errorf("error changing driver for GPU %q: %w", info.PciBusID, err)
}
return nil
}
// Unconfigure binds the GPU to the nvidia driver.
func (vm *VfioPciManager) Unconfigure(ctx context.Context, info *VfioDeviceInfo) error {
// Do nothing if we dont expect to switch to nvidia driver.
if !vm.nvidiaEnabled {
return nil
}
// Change the GPU driver to nvidia.
err := vm.changeDriver(info.PciBusID, nvidiaDriver)
if err != nil {
return fmt.Errorf("error changing driver for GPU %q: %w", info.PciBusID, err)
}
// Enable GPU Persistence Mode.
err = vm.enableGPUPersistenceMode(info.PciBusID)
if err != nil {
return fmt.Errorf("error enabling persistence mode for GPU %q: %w", info.PciBusID, err)
}
return nil
}
// Get the current driver the GPU is bound to.
func getDriver(pciDevicesPath, pciAddress string) (string, error) {
driverPath, err := os.Readlink(filepath.Join(pciDevicesPath, pciAddress, "driver"))
if err != nil {
return "", err
}
_, driver := filepath.Split(driverPath)
return driver, nil
}
// Change the driver the GPU is bound to.
func (vm *VfioPciManager) changeDriver(pciAddress, driver string) error {
currentDriver, err := getDriver(pciDevicesPath, pciAddress)
if err != nil {
return fmt.Errorf("error getting driver details for GPU %q: %w", pciAddress, err)
}
// Skip if the GPU is already bound to the desired driver.
if currentDriver == driver {
return nil
}
err = vm.unbindFromDriver(pciAddress)
if err != nil {
return err
}
err = vm.bindToDriver(pciAddress, driver)
if err != nil {
return err
}
return nil
}
// Unbind the GPU from the driver it is bound to.
func (vm *VfioPciManager) unbindFromDriver(pciAddress string) error {
out, err := execCommand(unbindFromDriverScript, []string{pciAddress}) //nolint:gosec
if err != nil {
klog.Errorf("Attempting to unbind %s from its driver failed; stdout: %s, err: %v", pciAddress, string(out), err)
return err
}
return nil
}
// Bind the GPU to the given driver.
func (vm *VfioPciManager) bindToDriver(pciAddress, driver string) error {
out, err := execCommand(bindToDriverScript, []string{pciAddress, driver}) //nolint:gosec
if err != nil {
klog.Errorf("Attempting to bind %s to %s driver failed; stdout: %s, err: %v", pciAddress, driver, string(out), err)
return err
}
return nil
}
// Enable GPU Persistence Mode.
func (vm *VfioPciManager) enableGPUPersistenceMode(pciAddress string) error {
// Obtain a lock to serialize persistence mode operations.
// This is a cautious approach to avoid any NVML race conditions.
vm.Lock()
defer vm.Unlock()
return vm.nvlib.enableGPUPersistenceMode(pciAddress)
}
// Disable GPU Persistence Mode.
func (vm *VfioPciManager) disableGPUPersistenceMode(pciAddress string) error {
// Obtain a lock to serialize persistence mode operations.
// This is a cautious approach to avoid any NVML race conditions.
vm.Lock()
defer vm.Unlock()
// We dont need to toggle persistence mode if nvidia-persistenced is not running.
klog.V(4).Infof("Checking if nvidia-persistenced is running: %s", filepath.Join(vm.containerDriverRoot, nvidiaPersistencedSocketPath))
_, err := os.Stat(filepath.Join(vm.containerDriverRoot, nvidiaPersistencedSocketPath))
if err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("error checking if nvidia-persistenced is running: %w", err)
}
klog.V(4).Infof("nvidia-persistenced is not running; nothing to do...")
return nil
}
err = vm.nvlib.disableGPUPersistenceMode(pciAddress)
if err != nil {
return fmt.Errorf("error disabling persistence mode for GPU %q: %w", pciAddress, err)
}
return nil
}
// Check if the vfio_pci module is loaded.
func checkVfioPCIModuleLoaded() (bool, error) {
f, err := os.Stat(filepath.Join(hostRoot, sysModulePath, vfioPciModule))
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, fmt.Errorf("failed to check if vfio_pci module is loaded: %w", err)
}
if !f.IsDir() {
return false, nil
}
return true, nil
}
// Load the vfio_pci module.
func loadVfioPciModule() error {
_, err := execCommandWithChroot(hostRoot, "modprobe", []string{vfioPciModule}) //nolint:gosec
if err != nil {
return err
}
return nil
}
// Check if IOMMU is enabled.
func checkIommuEnabled() (bool, error) {
f, err := os.Open(filepath.Join(hostRoot, kernelIommuGroupPath))
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, err
}
defer f.Close()
_, err = f.Readdirnames(1)
if err == io.EOF {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
// Check if IOMMUFD is enabled.
// We correlate the IOMMUFD support with the presence of the /dev/iommu API device.
func checkIommuFDEnabled() (bool, error) {
_, err := os.Stat(filepath.Join(hostRoot, iommuDevicePath))
if err != nil {
if os.IsNotExist(err) {
klog.Infof("IOMMUFD is not enabled, /dev/iommu device node does not exist")
return false, nil
}
return false, fmt.Errorf("error checking if iommu device node exists: %w", err)
}
return true, nil
}
// Execute a command with chroot.
func execCommandWithChroot(fsRoot, cmd string, args []string) ([]byte, error) {
chrootArgs := []string{fsRoot, cmd}
chrootArgs = append(chrootArgs, args...)
return exec.Command("chroot", chrootArgs...).CombinedOutput()
}
// Execute a command.
func execCommand(cmd string, args []string) ([]byte, error) {
return exec.Command(cmd, args...).CombinedOutput()
}