Coverage for src/lilbee/providers/fleet/gpu_backends/fdinfo.py: 100%

75 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-14 11:46 +0000

1"""Generic DRM fdinfo GPU-utilization reader (no root, no vendor tool). 

2 

3Modern kernels publish per-client GPU engine-busy counters under 

4``/proc/<pid>/fdinfo/<fd>`` for any DRM driver (i915, xe, amdgpu, ...). Each open 

5DRM file reports monotonic nanosecond counters per engine:: 

6 

7 drm-driver: i915 

8 drm-pdev: 0000:00:02.0 

9 drm-engine-render: 9288864723 ns 

10 drm-engine-video: 0 ns 

11 

12Utilization is the busiest engine's share of wall-clock time over a short window: 

13we snapshot the counters, wait, snapshot again, and divide the delta by the 

14elapsed time. Counters are summed across every DRM client of the matching driver, 

15so a workload split across processes still reads correctly. 

16 

17This needs no elevated privileges (a process can read its own and same-user 

18clients' fdinfo) and no extra tool, but the engine counters only exist on kernels 

19new enough to publish them (i915 landed them in 5.19 for execlists submission, 

206.5 where GuC submission is active); older kernels omit the 

21``drm-engine-*`` lines and this reader reports nothing, letting a caller fall back. 

22""" 

23 

24from __future__ import annotations 

25 

26import time 

27from pathlib import Path 

28 

29_PROC = Path("/proc") 

30_ENGINE_PREFIX = "drm-engine-" 

31_DRIVER_KEY = "drm-driver" 

32_DEFAULT_INTERVAL_S = 0.2 

33 

34 

35def read_drm_util( 

36 driver: str, 

37 interval_s: float = _DEFAULT_INTERVAL_S, 

38 proc: Path = _PROC, 

39) -> int | None: 

40 """Busiest-engine utilization percent for *driver*'s GPU, or None. 

41 

42 Returns None when the kernel publishes no engine counters for this driver 

43 (too old, or no active DRM clients), so the caller can fall back to a tool. 

44 """ 

45 first = _snapshot(driver, proc) 

46 if first is None: 

47 return None 

48 time.sleep(interval_s) 

49 second = _snapshot(driver, proc) 

50 if second is None: 

51 return None 

52 busy1, wall1 = first 

53 busy2, wall2 = second 

54 elapsed = wall2 - wall1 

55 if elapsed <= 0: 

56 return None 

57 peak = 0.0 

58 for engine, ns2 in busy2.items(): 

59 delta = ns2 - busy1.get(engine, 0) 

60 if delta > 0: 

61 peak = max(peak, delta / elapsed) 

62 return round(min(peak, 1.0) * 100) 

63 

64 

65def _snapshot(driver: str, proc: Path) -> tuple[dict[str, int], int] | None: 

66 """Sum engine-busy ns per engine across all DRM clients of *driver*. 

67 

68 Returns (totals_by_engine, monotonic_ns), or None when no client publishes 

69 engine counters for this driver. 

70 """ 

71 totals: dict[str, int] = {} 

72 found = False 

73 try: 

74 pids = [entry for entry in proc.iterdir() if entry.name.isdigit()] 

75 except OSError: 

76 return None 

77 for pid in pids: 

78 for engine, ns in _client_engine_ns(pid, driver): 

79 totals[engine] = totals.get(engine, 0) + ns 

80 found = True 

81 if not found: 

82 return None 

83 return totals, time.monotonic_ns() 

84 

85 

86def _client_engine_ns(pid: Path, driver: str) -> list[tuple[str, int]]: 

87 """(engine, ns) pairs from every *driver* DRM client fd under this pid.""" 

88 pairs: list[tuple[str, int]] = [] 

89 try: 

90 entries = list((pid / "fdinfo").iterdir()) 

91 except OSError: 

92 return pairs 

93 for entry in entries: 

94 try: 

95 text = entry.read_text(encoding="utf-8") 

96 except OSError: 

97 continue 

98 if not _driver_matches(text, driver): 

99 continue 

100 for line in text.splitlines(): 

101 if line.startswith(_ENGINE_PREFIX): 

102 engine, ns = _parse_engine_line(line) 

103 if ns is not None: 

104 pairs.append((engine, ns)) 

105 return pairs 

106 

107 

108def _driver_matches(text: str, driver: str) -> bool: 

109 """True when the fdinfo names *driver* on its drm-driver line.""" 

110 for line in text.splitlines(): 

111 if line.startswith(_DRIVER_KEY): 

112 _, _, val = line.partition(":") 

113 return val.strip() == driver 

114 return False 

115 

116 

117def _parse_engine_line(line: str) -> tuple[str, int | None]: 

118 """Parse 'drm-engine-render:\\t123 ns' into ('render', 123).""" 

119 key, _, val = line.partition(":") 

120 engine = key[len(_ENGINE_PREFIX) :] 

121 fields = val.split() 

122 if not fields: 

123 return engine, None 

124 try: 

125 return engine, int(fields[0]) 

126 except ValueError: 

127 return engine, None