Files
2026-08-11 22:49:18 -07:00

327 lines
9.2 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 dockerutil provides utility functions for GPU tests.
package dockerutil
import (
"context"
"flag"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"testing"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
// Needed for go:embed
_ "embed"
)
// Flags.
var (
setCOSGPU = flag.Bool("cos-gpu", false, "set to configure GPU settings for COS, as opposed to Docker")
)
//go:embed run_sniffer_copy
var runSnifferBinary []byte
const (
// ioctlSnifferMountPath is the in-container path at which the ioctl sniffer is mounted.
ioctlSnifferMountPath = "/ioctl_sniffer"
)
const (
// AllGPUCapabilitiesEnv is the environment variable that enables all NVIDIA
// GPU capabilities within a container.
AllGPUCapabilitiesEnv = "NVIDIA_DRIVER_CAPABILITIES=all"
// DefaultGPUCapabilities are the driver capabilities enabled by default.
DefaultGPUCapabilities = "compute,utility"
)
// GPURunOpts returns Docker run options with GPU support enabled.
func GPURunOpts(sniffGPUOpts SniffGPUOpts) (RunOpts, error) {
var mounts []mount.Mount
if sniffGPUOpts.DisableSnifferReason == "" {
// Extract the sniffer binary to a temporary location.
runSniffer, err := os.CreateTemp("", "run_sniffer.*")
if err != nil {
return RunOpts{}, fmt.Errorf("failed to create temporary file: %w", err)
}
if _, err := runSniffer.Write(runSnifferBinary); err != nil {
return RunOpts{}, fmt.Errorf("failed to write to temporary file: %w", err)
}
if err := runSniffer.Sync(); err != nil {
return RunOpts{}, fmt.Errorf("failed to sync temporary file: %w", err)
}
if err := runSniffer.Chmod(0o555); err != nil {
return RunOpts{}, fmt.Errorf("failed to chmod temporary file: %w", err)
}
if err := runSniffer.Close(); err != nil {
return RunOpts{}, fmt.Errorf("failed to close temporary file: %w", err)
}
sniffGPUOpts.runSniffer = runSniffer
mounts = append(mounts, mount.Mount{
Source: runSniffer.Name(),
Target: ioctlSnifferMountPath,
Type: mount.TypeBind,
ReadOnly: true,
})
}
gpuEnv := []string{"NVIDIA_DRIVER_CAPABILITIES=" + sniffGPUOpts.GPUCapabilities()}
if !*setCOSGPU {
return RunOpts{
Env: gpuEnv,
DeviceRequests: []container.DeviceRequest{
{
Count: -1,
Capabilities: [][]string{{"gpu"}},
Options: map[string]string{},
},
},
Mounts: mounts,
sniffGPUOpts: &sniffGPUOpts,
}, nil
}
// COS has specific settings since it has a custom installer for GPU drivers.
// See: https://cloud.google.com/container-optimized-os/docs/how-to/run-gpus#install-driver
devices := []container.DeviceMapping{}
var nvidiaDevices []string
for i := 0; true; i++ {
devicePath := fmt.Sprintf("/dev/nvidia%d", i)
if _, err := os.Stat(devicePath); err != nil {
break
}
nvidiaDevices = append(nvidiaDevices, devicePath)
}
nvidiaDevices = append(nvidiaDevices, "/dev/nvidia-uvm", "/dev/nvidiactl")
for _, device := range nvidiaDevices {
devices = append(devices, container.DeviceMapping{
PathOnHost: device,
PathInContainer: device,
CgroupPermissions: "rwm",
})
}
for _, nvidiaBin := range []string{
"/home/kubernetes/bin/nvidia/bin",
"/var/lib/nvidia/bin",
} {
if st, err := os.Stat(nvidiaBin); err == nil && st.IsDir() {
mounts = append(mounts, mount.Mount{
Source: nvidiaBin,
Target: "/usr/local/nvidia/bin",
Type: mount.TypeBind,
ReadOnly: true,
})
break
}
}
for _, nvidiaLib64 := range []string{
"/home/kubernetes/bin/nvidia/lib64",
"/var/lib/nvidia/lib64",
} {
if st, err := os.Stat(nvidiaLib64); err == nil && st.IsDir() {
mounts = append(mounts, mount.Mount{
Source: nvidiaLib64,
Target: "/usr/local/nvidia/lib64",
Type: mount.TypeBind,
ReadOnly: true,
})
sniffGPUOpts.addLDPath = "/usr/local/nvidia/lib64"
break
}
}
return RunOpts{
Env: gpuEnv,
Mounts: mounts,
Devices: devices,
sniffGPUOpts: &sniffGPUOpts,
}, nil
}
// SniffGPUOpts dictates options to sniffer GPU workloads.
type SniffGPUOpts struct {
// If set, explains why the sniffer should be disabled for this test.
// If unset or empty, the sniffer is enabled.
DisableSnifferReason string
// If true, the test will not fail even when the workload calls incompatible
// ioctls. Useful for debugging.
// TODO(b/340955577): Should be converted to a flag and removed from this
// struct once all GPU tests have no incompatible ioctls.
AllowIncompatibleIoctl bool
// The set of GPU capabilities exposed to the container.
// If unset, defaults to `DefaultGPUCapabilities`.
Capabilities string
// If set, add the given directory to the ld cache.
// Must be a directory visible from within the container.
addLDPath string
// The fields below are set internally.
runSniffer *os.File
}
// GPUCapabilities returns the set of GPU capabilities meant to be
// exposed to the container.
func (sgo *SniffGPUOpts) GPUCapabilities() string {
if sgo.Capabilities == "" {
return DefaultGPUCapabilities
}
return sgo.Capabilities
}
// prepend prepends the sniffer arguments to the given command.
func (sgo *SniffGPUOpts) prepend(argv []string) []string {
if sgo.DisableSnifferReason != "" {
return argv
}
snifferArgv := []string{
ioctlSnifferMountPath,
// TODO(eperot): Add flag to enforce capability set here once implemented.
}
if !sgo.AllowIncompatibleIoctl {
snifferArgv = append(snifferArgv, "--enforce_compatibility=INSTANT")
}
if sgo.addLDPath != "" {
snifferArgv = append(snifferArgv, fmt.Sprintf("--add_ld_path=%s", sgo.addLDPath))
}
return append(snifferArgv, argv...)
}
func (sgo *SniffGPUOpts) cleanup() error {
if sgo.DisableSnifferReason != "" {
return nil // Sniffer disabled, so nothing to clean up.
}
if err := os.Remove(sgo.runSniffer.Name()); err != nil {
return fmt.Errorf("failed to unlink temporary file %q: %w", sgo.runSniffer.Name(), err)
}
return nil
}
// NumGPU crudely estimates the number of NVIDIA GPUs on the host.
func NumGPU() int {
numGPU := 0
for {
_, err := os.Stat(fmt.Sprintf("/dev/nvidia%d", numGPU))
if err != nil {
break
}
numGPU++
}
return numGPU
}
// CudaVersion represents a cuda version.
type CudaVersion struct {
Major int64
Minor int64
}
// IsAtLeast returns true if the cuda version is at least as new as the other
// cuda version.
func (c *CudaVersion) IsAtLeast(other *CudaVersion) bool {
if c.Major > other.Major {
return true
}
if c.Major < other.Major {
return false
}
return c.Minor >= other.Minor
}
func (c *CudaVersion) String() string {
return fmt.Sprintf("%d.%d", c.Major, c.Minor)
}
// MustParseCudaVersion returns a new CudaVersion from a string.
func MustParseCudaVersion(version string) *CudaVersion {
v, err := ParseCudaVersion(version)
if err != nil {
panic(err.Error())
}
return v
}
// ParseCudaVersion returns a new CudaVersion from a string.
func ParseCudaVersion(version string) (*CudaVersion, error) {
parts := strings.Split(version, ".")
if len(parts) != 2 {
return nil, fmt.Errorf("invalid cuda version: %q", version)
}
major, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse major version %q: %v", parts[0], err)
}
minor, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse minor version %q: %v", parts[1], err)
}
return &CudaVersion{Major: major, Minor: minor}, nil
}
var cudaRE = regexp.MustCompile(`CUDA\s*(?:UMD\s*)?Version\s*:\s*(\d+)\.(\d+)`)
// NewCudaVersionFromOutput returns a new CudaVersion from the output of nvidia-smi.
func NewCudaVersionFromOutput(out string) (*CudaVersion, error) {
parts := cudaRE.FindStringSubmatch(out)
if len(parts) != 3 {
return nil, fmt.Errorf("CUDA version not found in output %q", out)
}
major, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse major version %q: %v", parts[1], err)
}
minor, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse minor version %q: %v", parts[2], err)
}
return &CudaVersion{Major: major, Minor: minor}, err
}
// MaxSuportedCUDAVersion returns the maximum supported by the host machine.
func MaxSuportedCUDAVersion(ctx context.Context, t *testing.T) (*CudaVersion, error) {
c := MakeContainer(ctx, t)
defer c.CleanUp(ctx)
opts, err := GPURunOpts(SniffGPUOpts{
DisableSnifferReason: "Get CUDA Version",
Capabilities: "all",
})
if err != nil {
return nil, fmt.Errorf("could not create opts: %w", err)
}
opts.Image = "gpu/cuda-tests"
out, err := c.Run(ctx, opts, "nvidia-smi")
if err != nil {
return nil, fmt.Errorf("failed to run container: %w", err)
}
return NewCudaVersionFromOutput(out)
}