package metrics // GPU telemetry, the way btop and intel_gpu_top get it without a vendor library: the // device from sysfs, and who is using it from DRM fdinfo. // // - The device: /sys/class/drm/card with a driver we know to be a GPU. Clock from the // driver's frequency files, temperature/power/fan from the hwmon under the device, and // how busy it is from the one counter every Intel GT exports without root — RC6 idle // residency, so busy = 100 − idle. amdgpu says it outright (gpu_busy_percent). // - The clients: every process holding /dev/dri/* open has a drm-engine- running // total (nanoseconds) in /proc//fdinfo/; the delta between two reads is that // process's share of each engine, and the sum across clients is the engine's load. Other // users' fdinfo is unreadable, so when the daemon isn't root it keeps one root child // (`hamlab drm-clients`) that does the walk, asked over a pipe — one sudo per daemon // start, not one per second. // // A GPU with no counter of either kind (Asahi, today) is still listed: a name is better // than silence about hardware the machine has. import ( "bufio" "encoding/json" "io" "log" "os" "os/exec" "path/filepath" "regexp" "sort" "strconv" "strings" "sync" "time" "hamlab/internal/priv" ) // GPU is one graphics device as a Sample reports it. type GPU struct { Name string `json:"name"` Driver string `json:"driver"` Bus string `json:"bus,omitempty"` // PCI address; empty for a platform device Integrated bool `json:"integrated,omitempty"` Usage float64 `json:"usage"` // % busy UsageSrc string `json:"usage_src,omitempty"` // "idle" (100 − idle residency), "busy" (the driver's own %), "clients" (the busiest engine); "" when nothing measures it Engines map[string]float64 `json:"engines,omitempty"` // render/video/copy/enhance… % from the clients, normalised by engine count Temp float64 `json:"temp,omitempty"` TempSrc string `json:"temp_src,omitempty"` // "gpu" from the device's own sensor, "package" the CPU die an integrated GPU shares PowerW float64 `json:"power_w,omitempty"` PowerCapW float64 `json:"power_cap_w,omitempty"` FanRPM int `json:"fan_rpm,omitempty"` MHz float64 `json:"mhz,omitempty"` // actual clock (0 when power-gated) ReqMHz float64 `json:"req_mhz,omitempty"` // what the driver asked for MaxMHz float64 `json:"max_mhz,omitempty"` MemUsed uint64 `json:"mem_used,omitempty"` // VRAM used, or the clients' resident bytes where the driver has no VRAM counter MemTotal uint64 `json:"mem_total,omitempty"` Clients []GPUClient `json:"clients"` ClientsOK bool `json:"clients_ok"` // false when fdinfo of other users' processes could not be read Stalled bool `json:"stalled,omitempty"` // a sysfs read of this card never returned; its readings are paused until restart } // GPUClient is one process with the device open. type GPUClient struct { PID int `json:"pid"` Name string `json:"name"` Usage float64 `json:"usage"` // its busiest engine, % Engines map[string]float64 `json:"engines,omitempty"` Mem uint64 `json:"mem,omitempty"` // resident bytes } // gpuDrivers are DRM drivers that drive a GPU, not just a scanout (simpledrm, ast, the // Apple display coprocessor) — those have no engines and nothing to say. var gpuDrivers = map[string]bool{"i915": true, "xe": true, "amdgpu": true, "radeon": true, "nouveau": true, "nvidia-drm": true, "nvidia": true, "asahi": true, "panfrost": true, "panthor": true, "v3d": true, "msm": true, "etnaviv": true, "lima": true, "powervr": true, "virtio_gpu": true} type gpuDev struct { card, dev, key, bus, driver, name, hwmon string integrated, stalled bool prevIdle, prevEnergy float64 prevAt time.Time nv []GPUClient // nvidia: the process table from the last read (pmon), fdinfo says nothing there } type gpuState struct { devs []*gpuDev listedAt time.Time clients map[string]clientPrev // device key + client id → engine totals at the previous scan last []drmFD // the newest client walk scan func() ([]drmFD, bool) // the walk; nil is scanClients (tests stand a fake list in) helper *drmHelper ok bool // the last walk saw every process } type clientPrev struct { ns map[string]uint64 at time.Time } const gpuRelist = time.Minute // how often the card list is re-read: hotplug is rare, the readdir is cheap // gpus is the GPU section of a sample. cpuTemp stands in for an integrated GPU's // temperature: it is the same die. func (c *Collector) gpus(now time.Time, cpuTemp float64) []GPU { g := &c.gpu if g.devs == nil || now.Sub(g.listedAt) >= gpuRelist { g.devs, g.listedAt = listGPUs(g.devs), now } if len(g.devs) == 0 { return []GPU{} } if g.clients == nil { g.clients = map[string]clientPrev{} } // The clients are read on every sample, like everything else in it: their engine shares // move on the same tick as the device's own numbers, at whatever rate is set. scan := c.scanClients if g.scan != nil { scan = g.scan } g.last, g.ok = scan() out := make([]GPU, 0, len(g.devs)) live := map[string]bool{} for _, d := range g.devs { x := GPU{Name: d.name, Driver: d.driver, Bus: d.bus, Integrated: d.integrated, Clients: []GPUClient{}, ClientsOK: g.ok} d.readWithin(&x, now) if x.Temp == 0 && d.integrated && cpuTemp > 0 { x.Temp, x.TempSrc = cpuTemp, "package" } // the clients: one entry per client id, under the first pid that holds it (a child // that inherited the fd shares the client) byID := map[string]*GPUClient{} var order []string engines := map[string]float64{} var resident uint64 if d.driver == "nvidia" { // no engine counters in its fdinfo; nvidia-smi pmon is the client list x.Clients, x.ClientsOK = append(x.Clients, d.nv...), true } for _, f := range g.last { if d.driver == "nvidia" { break } fi := parseFdinfo(f.Info) if fi.pdev != "" && fi.pdev != d.bus || fi.pdev == "" && fi.driver != d.driver { continue } key := d.key + "/" + fi.client live[key] = true if byID[fi.client] != nil { continue } cl := &GPUClient{PID: f.PID, Name: f.Comm, Mem: fi.mem} byID[fi.client] = cl order = append(order, fi.client) resident += fi.mem if prev, ok := g.clients[key]; ok { if dt := now.Sub(prev.at).Nanoseconds(); dt > 0 { for e, ns := range fi.engines { p, ok := prev.ns[e] if !ok || ns < p { continue } pct := float64(ns-p) / float64(dt) * 100 if n := fi.capacity[e]; n > 1 { pct /= float64(n) } pct = round1(pct) if cl.Engines == nil { cl.Engines = map[string]float64{} } cl.Engines[e] = pct engines[e] += pct if pct > cl.Usage { cl.Usage = pct } } } } g.clients[key] = clientPrev{ns: fi.engines, at: now} } for _, id := range order { x.Clients = append(x.Clients, *byID[id]) } sort.SliceStable(x.Clients, func(i, j int) bool { return x.Clients[i].Usage > x.Clients[j].Usage }) if len(engines) > 0 { x.Engines = map[string]float64{} for e, v := range engines { if v > 100 { v = 100 } x.Engines[e] = round1(v) } } if x.UsageSrc == "" && len(x.Engines) > 0 { for _, v := range x.Engines { if v > x.Usage { x.Usage = v } } x.UsageSrc = "clients" } if x.MemTotal == 0 && x.MemUsed == 0 { x.MemUsed = resident } out = append(out, x) } for k := range g.clients { // a client that closed the device if !live[k] { delete(g.clients, k) } } return out } // listGPUs enumerates the cards, keeping the counters of those already known. func listGPUs(known []*gpuDev) []*gpuDev { ents, _ := os.ReadDir("/sys/class/drm") old := map[string]*gpuDev{} for _, d := range known { old[d.card] = d } out := []*gpuDev{} for _, e := range ents { n := e.Name() if !strings.HasPrefix(n, "card") || strings.Contains(n, "-") { continue } card := "/sys/class/drm/" + n dev, err := filepath.EvalSymlinks(card + "/device") if err != nil { continue } driver := ueventValue(dev+"/uevent", "DRIVER") if driver == "" { // some drivers hang the DRIVER key off the parent driver = filepath.Base(readLink(dev + "/driver")) } if !gpuDrivers[driver] { continue } if d, ok := old[card]; ok && d.dev == dev { out = append(out, d) continue } d := &gpuDev{card: card, dev: dev, driver: driver} if pci := ueventValue(dev+"/uevent", "PCI_SLOT_NAME"); pci != "" { d.bus, d.key = pci, pci // an on-die GPU hangs off root bus 0 (Intel's 00:02.0, an AMD APU's 00:08.0 bridge) d.integrated = strings.HasPrefix(pci, "0000:00:") } else { d.key = "driver:" + driver d.integrated = true } if hw, _ := filepath.Glob(dev + "/hwmon/hwmon*"); len(hw) > 0 { d.hwmon = hw[0] } d.name = gpuName(dev, d.bus, driver) out = append(out, d) } sort.Slice(out, func(i, j int) bool { return out[i].card < out[j].card }) return out } // RenderNodeName is the GPU behind a render node ("/dev/dri/renderD128" → "Intel Arc A380"), // named the same way the Processes tab names it; "" when sysfs has nothing for it. func RenderNodeName(node string) string { dev, err := filepath.EvalSymlinks("/sys/class/drm/" + filepath.Base(node) + "/device") if err != nil { return "" } driver := ueventValue(dev+"/uevent", "DRIVER") if driver == "" { driver = filepath.Base(readLink(dev + "/driver")) } return gpuName(dev, ueventValue(dev+"/uevent", "PCI_SLOT_NAME"), driver) } // RenderNodeIntegrated reports a render node whose GPU sits on the CPU die (PCI root bus 0, // or no PCI bus at all), as opposed to a card on the bus. func RenderNodeIntegrated(node string) bool { dev, err := filepath.EvalSymlinks("/sys/class/drm/" + filepath.Base(node) + "/device") if err != nil { return true } pci := ueventValue(dev+"/uevent", "PCI_SLOT_NAME") return pci == "" || strings.HasPrefix(pci, "0000:00:") } var gpuVendors = map[string]string{"0x8086": "Intel", "0x1002": "AMD", "0x10de": "NVIDIA", "0x1af4": "Virtio", "0x106b": "Apple"} var ( revRe = regexp.MustCompile(`\s*\(rev [^)]*\)`) bracketRe = regexp.MustCompile(`\[([^\]]+)\]`) ) // gpuName is "Intel Arc A380": from the embedded PCI ID table first, so the name is the // same on every machine; from lspci for a card newer than the table; and failing both, // the vendor and the raw device ID. func gpuName(dev, bus, driver string) string { if bus != "" { vid := strings.TrimPrefix(strings.TrimSpace(readStr(dev+"/vendor")), "0x") did := strings.TrimPrefix(strings.TrimSpace(readStr(dev+"/device")), "0x") if vn, dn := pciName(vid, did); dn != "" { return lspciName(vn + " " + dn) } if out, ok := runOut("lspci", "-s", bus); ok { if _, desc, found := strings.Cut(out, ": "); found { return lspciName(desc) } } } vendor := gpuVendors[strings.TrimSpace(readStr(dev+"/vendor"))] if compat := readStr(dev + "/of_node/compatible"); compat != "" { // "apple,agx-t8103\0apple,agx-g13g" first := strings.SplitN(compat, "\x00", 2)[0] if v, model, ok := strings.Cut(first, ","); ok { if vendor == "" && v != "" { vendor = strings.ToUpper(v[:1]) + v[1:] } return vendor + " GPU (" + model + ")" } } if vendor == "" && driver != "" { vendor = strings.ToUpper(driver[:1]) + driver[1:] } if id := strings.TrimPrefix(strings.TrimSpace(readStr(dev+"/device")), "0x"); id != "" { return strings.TrimSpace(vendor + " GPU " + id) } return strings.TrimSpace(vendor + " GPU") } // lspciName turns "Intel Corporation DG2 [Arc A380] (rev 05)" into "Intel Arc A380". func lspciName(desc string) string { desc = revRe.ReplaceAllString(strings.TrimSpace(desc), "") vendor := "" for _, v := range []struct{ long, short string }{{"Intel Corporation", "Intel"}, {"Advanced Micro Devices, Inc. [AMD/ATI]", "AMD"}, {"Advanced Micro Devices, Inc. [AMD]", "AMD"}, {"NVIDIA Corporation", "NVIDIA"}, {"Red Hat, Inc.", "Virtio"}} { if strings.HasPrefix(desc, v.long) { vendor = v.short desc = strings.TrimSpace(desc[len(v.long):]) break } } if vendor == "" { // vendor not recognised: its first word if i := strings.IndexByte(desc, ' '); i > 0 { vendor, desc = desc[:i], strings.TrimSpace(desc[i+1:]) } } if m := bracketRe.FindStringSubmatch(desc); m != nil { desc = m[1] } return strings.TrimSpace(vendor + " " + desc) } // readWithin is read with a deadline. A sysfs read that does not come back must not take // the whole sampler with it — every graph on every page of this machine stops — so the // device is marked stalled, its goroutine abandoned, and the rest of the sample goes on. const gpuReadWithin = 3 * time.Second func (d *gpuDev) readWithin(x *GPU, now time.Time) { if d.stalled { x.Stalled = true return } done := make(chan GPU, 1) go func() { y := *x d.read(&y, now) done <- y }() select { case y := <-done: *x = y case <-time.After(gpuReadWithin): d.stalled, x.Stalled = true, true log.Printf("gpu: %s (%s): a sysfs read did not return within %s; its readings are paused until restart", d.name, d.card, gpuReadWithin) } } // read fills the device-level numbers: clock, idle/busy, hwmon. func (d *gpuDev) read(x *GPU, now time.Time) { dt := now.Sub(d.prevAt).Seconds() switch d.driver { case "nvidia": d.readNvidia(x) case "i915": x.MHz = readF(d.card + "/gt_act_freq_mhz") x.ReqMHz = readF(d.card + "/gt_cur_freq_mhz") x.MaxMHz = readF(d.card + "/gt_max_freq_mhz") idle, ok := readInt(d.card + "/gt/gt0/rc6_residency_ms") if !ok { idle, ok = readInt(d.card + "/power/rc6_residency_ms") } if ok { d.busyFromIdle(x, float64(idle), dt) } case "xe": gt := d.dev + "/tile0/gt0" x.MHz = readF(gt + "/freq0/act_freq") x.ReqMHz = readF(gt + "/freq0/cur_freq") x.MaxMHz = readF(gt + "/freq0/max_freq") if idle, ok := readInt(gt + "/gtidle/idle_residency_ms"); ok { d.busyFromIdle(x, float64(idle), dt) } case "amdgpu": if v, ok := readInt(d.dev + "/gpu_busy_percent"); ok { x.Usage, x.UsageSrc = float64(v), "busy" } x.MemUsed = uint64(readF(d.dev + "/mem_info_vram_used")) x.MemTotal = uint64(readF(d.dev + "/mem_info_vram_total")) // "1: 1800Mhz *" — the starred level is the current one, the last the highest for _, line := range strings.Split(readStr(d.dev+"/pp_dpm_sclk"), "\n") { f := strings.Fields(line) if len(f) < 2 { continue } mhz := leadingNum(f[1]) x.MaxMHz = mhz if strings.HasSuffix(line, "*") { x.MHz = mhz } } } if d.hwmon != "" { if t := readF(d.hwmon + "/temp1_input"); t > 0 { x.Temp, x.TempSrc = t/1000, "gpu" } if p := readF(d.hwmon + "/power1_input"); p > 0 { x.PowerW = round1(p / 1e6) } else if p := readF(d.hwmon + "/power1_average"); p > 0 { x.PowerW = round1(p / 1e6) } else if e := readF(d.hwmon + "/energy1_input"); e > 0 { // µJ, cumulative if d.prevEnergy > 0 && e >= d.prevEnergy && dt > 0 && !d.prevAt.IsZero() { x.PowerW = round1((e - d.prevEnergy) / 1e6 / dt) } d.prevEnergy = e } if p := readF(d.hwmon + "/power1_cap"); p > 0 { x.PowerCapW = p / 1e6 } else if p := readF(d.hwmon + "/power1_max"); p > 0 { x.PowerCapW = p / 1e6 } x.FanRPM = int(readF(d.hwmon + "/fan1_input")) } d.prevAt = now } // busyFromIdle: the GT's idle residency is a running ms total; the share of the interval // it did not cover is how busy the device was. func (d *gpuDev) busyFromIdle(x *GPU, idle, dt float64) { x.UsageSrc = "idle" if d.prevAt.IsZero() || dt <= 0 || idle < d.prevIdle { d.prevIdle = idle return } busy := 100 - (idle-d.prevIdle)/(dt*1000)*100 if busy < 0 { busy = 0 } if busy > 100 { busy = 100 } x.Usage = round1(busy) d.prevIdle = idle } // ---- DRM fdinfo: who has the device open ---- // drmFD is one client of a DRM device: the fdinfo text of one open fd, with its owner. type drmFD struct { PID int `json:"pid"` Comm string `json:"comm"` Info string `json:"info"` } type fdinfo struct { driver, pdev, client string engines map[string]uint64 // ns, cumulative capacity map[string]int mem uint64 // bytes } func parseFdinfo(s string) fdinfo { fi := fdinfo{engines: map[string]uint64{}, capacity: map[string]int{}} var resident, memory uint64 for _, line := range strings.Split(s, "\n") { k, v, ok := strings.Cut(line, ":") if !ok || !strings.HasPrefix(k, "drm-") { continue } v = strings.TrimSpace(v) switch { case k == "drm-driver": fi.driver = v case k == "drm-pdev": fi.pdev = v case k == "drm-client-id": fi.client = v case strings.HasPrefix(k, "drm-engine-capacity-"): fi.capacity[k[len("drm-engine-capacity-"):]], _ = strconv.Atoi(v) case strings.HasPrefix(k, "drm-engine-"): n, _ := strconv.ParseUint(strings.TrimSuffix(v, " ns"), 10, 64) fi.engines[k[len("drm-engine-"):]] = n case strings.HasPrefix(k, "drm-resident-"): resident += kib(v) case strings.HasPrefix(k, "drm-memory-"): memory += kib(v) } } fi.mem = resident if fi.mem == 0 { fi.mem = memory } return fi } // kib parses "52624 KiB" (or MiB, or a bare count of bytes) to bytes. func kib(v string) uint64 { f := strings.Fields(v) if len(f) == 0 { return 0 } n, _ := strconv.ParseUint(f[0], 10, 64) if len(f) > 1 { switch f[1] { case "KiB": n <<= 10 case "MiB": n <<= 20 case "GiB": n <<= 30 } } return n } // drmScan walks /proc for every fd open on /dev/dri/*, one entry per client per process. // Run as root it sees everyone; as a user, only that user's processes. func drmScan() (out []drmFD, complete bool) { ents, err := os.ReadDir("/proc") if err != nil { return []drmFD{}, false } complete = true out = []drmFD{} for _, e := range ents { pid, err := strconv.Atoi(e.Name()) if err != nil { continue } fds, err := os.ReadDir("/proc/" + e.Name() + "/fd") if err != nil { if !os.IsNotExist(err) { complete = false } continue } seen := map[string]bool{} comm := "" for _, fd := range fds { target, err := os.Readlink("/proc/" + e.Name() + "/fd/" + fd.Name()) if err != nil || !strings.HasPrefix(target, "/dev/dri/") { continue } info := readStr("/proc/" + e.Name() + "/fdinfo/" + fd.Name()) _, rest, ok := strings.Cut(info, "drm-client-id:") if !ok { continue } id := strings.TrimSpace(strings.SplitN(rest, "\n", 2)[0]) if seen[id] { continue } seen[id] = true if comm == "" { comm = strings.TrimSpace(readStr("/proc/" + e.Name() + "/comm")) } out = append(out, drmFD{PID: pid, Comm: comm, Info: info}) } } sort.SliceStable(out, func(i, j int) bool { return out[i].PID < out[j].PID }) return out, complete } // scanClients walks in-process when that sees everything (root), else through the helper. func (c *Collector) scanClients() ([]drmFD, bool) { if priv.Mode() == "root" || !priv.Available() { return drmScan() } if c.gpu.helper == nil { c.gpu.helper = &drmHelper{} } if fds, ok := c.gpu.helper.scan(); ok { return fds, true } return drmScan() // the helper is down: our own processes are better than nothing } // drmHelper is the root child that answers "scan" requests. One line in, one JSON line out. type drmHelper struct { mu sync.Mutex cmd *exec.Cmd in io.WriteCloser out *bufio.Reader failedAt time.Time } const helperRetry = 30 * time.Second func (h *drmHelper) scan() ([]drmFD, bool) { h.mu.Lock() defer h.mu.Unlock() if h.cmd == nil { if !h.failedAt.IsZero() && time.Since(h.failedAt) < helperRetry { return nil, false } if !h.start() { h.failedAt = time.Now() return nil, false } } // a stuck child must not stall the sampler: it is killed, and tried again later cmd := h.cmd stop := time.AfterFunc(5*time.Second, func() { cmd.Process.Kill() }) defer stop.Stop() if _, err := io.WriteString(h.in, "\n"); err != nil { h.fail() return nil, false } line, err := h.out.ReadBytes('\n') if err != nil { h.fail() return nil, false } var fds []drmFD if json.Unmarshal(line, &fds) != nil { h.fail() return nil, false } if fds == nil { fds = []drmFD{} } return fds, true } func (h *drmHelper) start() bool { self, err := os.Executable() if err != nil { return false } full := priv.Wrap(self, "drm-clients") cmd := exec.Command(full[0], full[1:]...) in, err := cmd.StdinPipe() if err != nil { return false } out, err := cmd.StdoutPipe() if err != nil { return false } if err := cmd.Start(); err != nil { return false } h.cmd, h.in, h.out = cmd, in, bufio.NewReaderSize(out, 1<<20) return true } func (h *drmHelper) fail() { if h.cmd != nil { h.in.Close() h.cmd.Process.Kill() go h.cmd.Wait() } h.cmd, h.in, h.out = nil, nil, nil h.failedAt = time.Now() } // DRMClientsHelper is `hamlab drm-clients`: for every line read on stdin, one JSON line of // drmFD on stdout. Ends when stdin does — when the daemon that started it has gone. func DRMClientsHelper(stdin io.Reader, stdout io.Writer) int { sc := bufio.NewScanner(stdin) w := bufio.NewWriter(stdout) for sc.Scan() { fds, _ := drmScan() b, _ := json.Marshal(fds) w.Write(b) w.WriteByte('\n') if w.Flush() != nil { return 1 } } return 0 } // ---- small helpers ---- func readF(p string) float64 { v, err := strconv.ParseFloat(strings.TrimSpace(readStr(p)), 64) if err != nil { return 0 } return v } func readLink(p string) string { t, err := os.Readlink(p) if err != nil { return "" } return t } func ueventValue(p, key string) string { for _, line := range strings.Split(readStr(p), "\n") { if k, v, ok := strings.Cut(line, "="); ok && k == key { return strings.TrimSpace(v) } } return "" } // The proprietary NVIDIA driver exports no busy counter in sysfs and no engine totals in // fdinfo, so the device and its processes come from nvidia-smi — two short invocations per // sample (~20 ms and ~75 ms on a GTX 965M), no root needed. The PCI address selects the // card, so two NVIDIA GPUs stay apart. func (d *gpuDev) readNvidia(x *GPU) { if out, ok := runOut("nvidia-smi", "--id="+d.bus, "--query-gpu=utilization.gpu,utilization.encoder,utilization.decoder,memory.used,memory.total,clocks.sm,clocks.max.sm,temperature.gpu,power.draw,power.limit", "--format=csv,noheader,nounits"); ok { parseNvidiaQuery(x, out) } if out, ok := runOut("nvidia-smi", "pmon", "-c", "1", "-s", "um", "--id="+d.bus); ok { d.nv = parseNvidiaPmon(out) } } // nvNum is one nvidia-smi CSV field; "[N/A]" (no sensor, no limit) is "nothing". func nvNum(s string) (float64, bool) { s = strings.TrimSpace(s) if s == "" || strings.Contains(s, "N/A") { return 0, false } v, err := strconv.ParseFloat(s, 64) return v, err == nil } // parseNvidiaQuery fills a GPU from one --query-gpu line in the field order readNvidia asks // for: gpu%, encoder%, decoder%, mem used MiB, mem total MiB, SM MHz, max SM MHz, °C, W, W cap. func parseNvidiaQuery(x *GPU, line string) { f := strings.Split(strings.TrimSpace(line), ",") if len(f) < 10 { return } if v, ok := nvNum(f[0]); ok { x.Usage, x.UsageSrc = v, "busy" x.Engines = map[string]float64{"render": v} enc, okE := nvNum(f[1]) dec, okD := nvNum(f[2]) if okE || okD { x.Engines["video"] = max(enc, dec) } } if v, ok := nvNum(f[3]); ok { x.MemUsed = uint64(v) << 20 } if v, ok := nvNum(f[4]); ok { x.MemTotal = uint64(v) << 20 } if v, ok := nvNum(f[5]); ok { x.MHz = v } if v, ok := nvNum(f[6]); ok { x.MaxMHz = v } if v, ok := nvNum(f[7]); ok && v > 0 { x.Temp, x.TempSrc = v, "gpu" } if v, ok := nvNum(f[8]); ok { x.PowerW = round1(v) } if v, ok := nvNum(f[9]); ok { x.PowerCapW = v } } // parseNvidiaPmon reads `nvidia-smi pmon -c 1 -s um`: a header naming the columns, then one // row per process — "-" where a process has no share. Columns vary between driver versions, // so they are taken by name. Busiest first, like the fdinfo clients. func parseNvidiaPmon(out string) []GPUClient { col := map[string]int{} var clients []GPUClient for _, line := range strings.Split(out, "\n") { f := strings.Fields(line) if len(f) == 0 { continue } if f[0] == "#" { if len(f) > 1 && f[1] == "gpu" { // "# gpu pid type sm mem enc dec jpg ofa fb ccpm command" col = map[string]int{} for i, name := range f[1:] { col[name] = i } } continue } get := func(name string) (float64, bool) { i, ok := col[name] if !ok || i >= len(f) || f[i] == "-" { return 0, false } v, err := strconv.ParseFloat(f[i], 64) return v, err == nil } pid, ok := get("pid") if !ok || pid <= 0 { continue } cl := GPUClient{PID: int(pid), Engines: map[string]float64{}} if i, ok := col["command"]; ok && i < len(f) { cl.Name = strings.Join(f[i:], " ") } if v, ok := get("sm"); ok { cl.Engines["render"] = v } enc, okE := get("enc") dec, okD := get("dec") if okE || okD { cl.Engines["video"] = max(enc, dec) } for _, v := range cl.Engines { if v > cl.Usage { cl.Usage = v } } if len(cl.Engines) == 0 { cl.Engines = nil } if v, ok := get("fb"); ok { cl.Mem = uint64(v) << 20 } clients = append(clients, cl) } sort.SliceStable(clients, func(i, j int) bool { return clients[i].Usage > clients[j].Usage }) return clients }