github.com/jaypipes/ghw@v0.21.1/pkg/gpu/gpu_linux.go (about) 1 // Use and distribution licensed under the Apache license version 2. 2 // 3 // See the COPYING file in the root project directory for full text. 4 // 5 6 package gpu 7 8 import ( 9 "os" 10 "path/filepath" 11 "regexp" 12 "strconv" 13 "strings" 14 15 "github.com/jaypipes/ghw/pkg/context" 16 "github.com/jaypipes/ghw/pkg/linuxpath" 17 "github.com/jaypipes/ghw/pkg/pci" 18 "github.com/jaypipes/ghw/pkg/topology" 19 "github.com/jaypipes/ghw/pkg/util" 20 ) 21 22 const ( 23 validPCIAddress = `\b(0{0,4}:[[:xdigit:]]{2}:[[:xdigit:]]{2}\.[[:xdigit:]]:?\w*)` 24 ) 25 26 var reValidPCIAddress = regexp.MustCompile(validPCIAddress) 27 28 const ( 29 _WARN_NO_SYS_CLASS_DRM = ` 30 /sys/class/drm does not exist on this system (likely the host system is a 31 virtual machine or container with no graphics). Therefore, 32 GPUInfo.GraphicsCards will be an empty array. 33 ` 34 ) 35 36 func (i *Info) load() error { 37 // In Linux, each graphics card is listed under the /sys/class/drm 38 // directory as a symbolic link named "cardN", where N is a zero-based 39 // index of the card in the system. "DRM" stands for Direct Rendering 40 // Manager and is the Linux subsystem that is responsible for graphics I/O 41 // 42 // Each card may have multiple symbolic 43 // links in this directory representing the interfaces from the graphics 44 // card over a particular wire protocol (HDMI, DisplayPort, etc). These 45 // symbolic links are named cardN-<INTERFACE_TYPE>-<DISPLAY_ID>. For 46 // instance, on one of my local workstations with an NVIDIA GTX 1050ti 47 // graphics card with one HDMI, one DisplayPort, and one DVI interface to 48 // the card, I see the following in /sys/class/drm: 49 // 50 // $ ll /sys/class/drm/ 51 // total 0 52 // drwxr-xr-x 2 root root 0 Jul 16 11:50 ./ 53 // drwxr-xr-x 75 root root 0 Jul 16 11:50 ../ 54 // lrwxrwxrwx 1 root root 0 Jul 16 11:50 card0 -> ../../devices/pci0000:00/0000:00:03.0/0000:03:00.0/drm/card0/ 55 // lrwxrwxrwx 1 root root 0 Jul 16 11:50 card0-DP-1 -> ../../devices/pci0000:00/0000:00:03.0/0000:03:00.0/drm/card0/card0-DP-1/ 56 // lrwxrwxrwx 1 root root 0 Jul 16 11:50 card0-DVI-D-1 -> ../../devices/pci0000:00/0000:00:03.0/0000:03:00.0/drm/card0/card0-DVI-D-1/ 57 // lrwxrwxrwx 1 root root 0 Jul 16 11:50 card0-HDMI-A-1 -> ../../devices/pci0000:00/0000:00:03.0/0000:03:00.0/drm/card0/card0-HDMI-A-1/ 58 // 59 // In this routine, we are only interested in the first link (card0), which 60 // we follow to gather information about the actual device from the PCI 61 // subsystem (we query the modalias file of the PCI device's sysfs 62 // directory using the `ghw.PCIInfo.GetDevice()` function. 63 paths := linuxpath.New(i.ctx) 64 links, err := os.ReadDir(paths.SysClassDRM) 65 if err != nil { 66 i.ctx.Warn(_WARN_NO_SYS_CLASS_DRM) 67 return nil 68 } 69 cards := make([]*GraphicsCard, 0) 70 for _, link := range links { 71 lname := link.Name() 72 if !strings.HasPrefix(lname, "card") { 73 continue 74 } 75 if strings.ContainsRune(lname, '-') { 76 continue 77 } 78 // Grab the card's zero-based integer index 79 lnameBytes := []byte(lname) 80 cardIdx, err := strconv.Atoi(string(lnameBytes[4:])) 81 if err != nil { 82 cardIdx = -1 83 } 84 85 // Calculate the card's PCI address by looking at the symbolic link's 86 // target 87 lpath := filepath.Join(paths.SysClassDRM, lname) 88 dest, err := os.Readlink(lpath) 89 if err != nil { 90 continue 91 } 92 pathParts := strings.Split(dest, "/") 93 // The PCI address of the graphics card is the *last* PCI address in 94 // the filepath... 95 pciAddress := "" 96 for x := len(pathParts) - 1; x >= 0; x-- { 97 part := pathParts[x] 98 if reValidPCIAddress.MatchString(part) { 99 pciAddress = part 100 break 101 } 102 } 103 if pciAddress == "" { 104 continue 105 } 106 card := &GraphicsCard{ 107 Address: pciAddress, 108 Index: cardIdx, 109 } 110 cards = append(cards, card) 111 } 112 gpuFillNUMANodes(i.ctx, cards) 113 gpuFillPCIDevice(i.ctx, cards) 114 i.GraphicsCards = cards 115 return nil 116 } 117 118 // Loops through each GraphicsCard struct and attempts to fill the DeviceInfo 119 // attribute with PCI device information 120 func gpuFillPCIDevice(ctx *context.Context, cards []*GraphicsCard) { 121 pci, err := pci.New(context.WithContext(ctx)) 122 if err != nil { 123 ctx.Warn("failed to PCI device database: %s", err) 124 return 125 } 126 for _, card := range cards { 127 if card.DeviceInfo == nil { 128 card.DeviceInfo = pci.GetDevice(card.Address) 129 } 130 } 131 } 132 133 // Loops through each GraphicsCard struct and find which NUMA node the card is 134 // affined to, setting the GraphicsCard.Node field accordingly. If the host 135 // system is not a NUMA system, the Node field will be set to nil. 136 func gpuFillNUMANodes(ctx *context.Context, cards []*GraphicsCard) { 137 paths := linuxpath.New(ctx) 138 topo, err := topology.New(context.WithContext(ctx)) 139 if err != nil { 140 // Problem getting topology information so just set the graphics card's 141 // node to nil 142 for _, card := range cards { 143 if topo.Architecture != topology.ArchitectureNUMA { 144 card.Node = nil 145 } 146 } 147 return 148 } 149 for _, card := range cards { 150 // Each graphics card on a NUMA system will have a pseudo-file 151 // called /sys/class/drm/card$CARD_INDEX/device/numa_node which 152 // contains the NUMA node that the card is affined to 153 cardIndexStr := strconv.Itoa(card.Index) 154 fpath := filepath.Join( 155 paths.SysClassDRM, 156 "card"+cardIndexStr, 157 "device", 158 "numa_node", 159 ) 160 nodeIdx := util.SafeIntFromFile(ctx, fpath) 161 if nodeIdx == -1 { 162 continue 163 } 164 for _, node := range topo.Nodes { 165 if nodeIdx == int(node.ID) { 166 card.Node = node 167 } 168 } 169 } 170 }