nvidia.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. //go:build linux
  2. // +build linux
  3. package nvidia
  4. import (
  5. "fmt"
  6. "strings"
  7. "sync"
  8. "time"
  9. "unsafe"
  10. "github.com/NVIDIA/go-nvml/pkg/nvml"
  11. )
  12. // requestMutex prevents overlapping NVML requests to avoid slowdowns
  13. var requestMutex sync.Mutex
  14. // convertCString converts a C-style char array to a Go string
  15. func convertCString(cstr [32]int8) string {
  16. n := 0
  17. for i := 0; i < len(cstr); i++ {
  18. if cstr[i] == 0 {
  19. break
  20. }
  21. n++
  22. }
  23. return string((*[32]byte)(unsafe.Pointer(&cstr[0]))[:n])
  24. }
  25. // GPUDevice represents an NVIDIA GPU device
  26. type GPUDevice struct {
  27. Index int
  28. Handle nvml.Device
  29. Name string
  30. PCIBusID string
  31. Memory uint64 // Total memory in bytes
  32. UUID string
  33. }
  34. // GPUMetrics contains current GPU metrics
  35. type GPUMetrics struct {
  36. PowerDraw float64 // Watts
  37. PerformanceLevel string // P0, P8, etc.
  38. MemoryUsage float64 // Percentage
  39. GPUUtilization int // Percentage
  40. MemoryUtilization int // Percentage
  41. Temperature int // Celsius
  42. }
  43. // Init initializes the NVML library
  44. func Init() error {
  45. requestMutex.Lock()
  46. defer requestMutex.Unlock()
  47. ret := nvml.Init()
  48. if ret != nvml.SUCCESS {
  49. return fmt.Errorf("failed to initialize NVML: %s", nvml.ErrorString(ret))
  50. }
  51. return nil
  52. }
  53. // Shutdown shuts down the NVML library
  54. func Shutdown() error {
  55. requestMutex.Lock()
  56. defer requestMutex.Unlock()
  57. ret := nvml.Shutdown()
  58. if ret != nvml.SUCCESS {
  59. return fmt.Errorf("failed to shutdown NVML: %s", nvml.ErrorString(ret))
  60. }
  61. return nil
  62. }
  63. // GetGPUDevices returns all available GPU devices
  64. func GetGPUDevices() ([]GPUDevice, error) {
  65. requestMutex.Lock()
  66. defer requestMutex.Unlock()
  67. count, ret := nvml.DeviceGetCount()
  68. if ret != nvml.SUCCESS {
  69. return nil, fmt.Errorf("failed to get device count: %s", nvml.ErrorString(ret))
  70. }
  71. devices := make([]GPUDevice, count)
  72. for i := 0; i < count; i++ {
  73. device, ret := nvml.DeviceGetHandleByIndex(i)
  74. if ret != nvml.SUCCESS {
  75. return nil, fmt.Errorf("failed to get device handle for index %d: %s", i, nvml.ErrorString(ret))
  76. }
  77. // Get device name
  78. name, ret := device.GetName()
  79. if ret != nvml.SUCCESS {
  80. return nil, fmt.Errorf("failed to get device name: %s", nvml.ErrorString(ret))
  81. }
  82. // Get PCI Bus ID
  83. pciInfo, ret := device.GetPciInfo()
  84. if ret != nvml.SUCCESS {
  85. return nil, fmt.Errorf("failed to get PCI info: %s", nvml.ErrorString(ret))
  86. }
  87. // Get memory info
  88. memInfo, ret := device.GetMemoryInfo()
  89. if ret != nvml.SUCCESS {
  90. return nil, fmt.Errorf("failed to get memory info: %s", nvml.ErrorString(ret))
  91. }
  92. // Get UUID
  93. uuid, ret := device.GetUUID()
  94. if ret != nvml.SUCCESS {
  95. return nil, fmt.Errorf("failed to get device UUID: %s", nvml.ErrorString(ret))
  96. }
  97. devices[i] = GPUDevice{
  98. Index: i,
  99. Handle: device,
  100. Name: name,
  101. PCIBusID: convertCString(pciInfo.BusId),
  102. Memory: memInfo.Total,
  103. UUID: uuid,
  104. }
  105. }
  106. return devices, nil
  107. }
  108. // GetGPUMetrics retrieves current metrics for a GPU device with timeout protection
  109. func GetGPUMetrics(device GPUDevice) (GPUMetrics, error) {
  110. // Use a timeout channel to prevent hanging requests
  111. done := make(chan struct {
  112. metrics GPUMetrics
  113. err error
  114. }, 1)
  115. go func() {
  116. metrics, err := getGPUMetricsInternal(device)
  117. done <- struct {
  118. metrics GPUMetrics
  119. err error
  120. }{metrics, err}
  121. }()
  122. select {
  123. case result := <-done:
  124. return result.metrics, result.err
  125. case <-time.After(10 * time.Second):
  126. return GPUMetrics{}, fmt.Errorf("timeout getting GPU metrics for device %s", device.Name)
  127. }
  128. }
  129. // getGPUMetricsInternal performs the actual NVML calls with mutex protection
  130. func getGPUMetricsInternal(device GPUDevice) (GPUMetrics, error) {
  131. requestMutex.Lock()
  132. defer requestMutex.Unlock()
  133. metrics := GPUMetrics{}
  134. // Get power draw
  135. power, ret := device.Handle.GetPowerUsage()
  136. if ret == nvml.SUCCESS {
  137. metrics.PowerDraw = float64(power) / 1000.0 // Convert mW to W
  138. } else if ret != nvml.ERROR_NOT_SUPPORTED {
  139. return metrics, fmt.Errorf("failed to get power usage: %s", nvml.ErrorString(ret))
  140. }
  141. // Get performance state
  142. perfState, ret := device.Handle.GetPerformanceState()
  143. if ret == nvml.SUCCESS {
  144. metrics.PerformanceLevel = fmt.Sprintf("P%d", int(perfState))
  145. } else if ret != nvml.ERROR_NOT_SUPPORTED {
  146. return metrics, fmt.Errorf("failed to get performance state: %s", nvml.ErrorString(ret))
  147. }
  148. // Get memory usage
  149. memInfo, ret := device.Handle.GetMemoryInfo()
  150. if ret == nvml.SUCCESS {
  151. metrics.MemoryUsage = float64(memInfo.Used) / float64(memInfo.Total) * 100.0
  152. } else {
  153. return metrics, fmt.Errorf("failed to get memory info: %s", nvml.ErrorString(ret))
  154. }
  155. // Get utilization rates
  156. utilization, ret := device.Handle.GetUtilizationRates()
  157. if ret == nvml.SUCCESS {
  158. metrics.GPUUtilization = int(utilization.Gpu)
  159. metrics.MemoryUtilization = int(utilization.Memory)
  160. } else if ret != nvml.ERROR_NOT_SUPPORTED {
  161. return metrics, fmt.Errorf("failed to get utilization rates: %s", nvml.ErrorString(ret))
  162. }
  163. // Get temperature
  164. temperature, ret := device.Handle.GetTemperature(nvml.TEMPERATURE_GPU)
  165. if ret == nvml.SUCCESS {
  166. metrics.Temperature = int(temperature)
  167. } else if ret != nvml.ERROR_NOT_SUPPORTED {
  168. return metrics, fmt.Errorf("failed to get temperature: %s", nvml.ErrorString(ret))
  169. }
  170. return metrics, nil
  171. }
  172. // GetShortPCIBusID formats PCI Bus ID from 00000000:04:00.0 to 00:04:00.0
  173. func GetShortPCIBusID(pciBusID string) string {
  174. // Split by colon to separate domain:bus:device.function
  175. parts := strings.Split(pciBusID, ":")
  176. if len(parts) >= 3 {
  177. // Format: domain should be 2 digits instead of 8
  178. domain := parts[0]
  179. if len(domain) > 2 {
  180. // Take the last 2 characters of the domain
  181. domain = domain[len(domain)-2:]
  182. }
  183. // Return formatted as domain:bus:device.function
  184. return fmt.Sprintf("%s:%s:%s", domain, parts[1], parts[2])
  185. }
  186. // If format is unexpected, return as-is
  187. return pciBusID
  188. }
  189. // GetDeviceID generates a unique device identifier for MQTT topics
  190. func GetDeviceID(device GPUDevice) string {
  191. // Format PCI Bus ID to short format and remove unwanted characters
  192. shortPCIBusID := GetShortPCIBusID(device.PCIBusID)
  193. deviceID := strings.Replace(shortPCIBusID, ":", "_", -1)
  194. deviceID = strings.Replace(deviceID, ".", "_", -1)
  195. // Add GPU UUID suffix (first 8 characters) to ensure uniqueness across different machines
  196. uuidSuffix := strings.Replace(device.UUID, "-", "", -1)
  197. if len(uuidSuffix) > 8 {
  198. uuidSuffix = uuidSuffix[:8]
  199. }
  200. return strings.ToLower(fmt.Sprintf("%s_%s", deviceID, uuidSuffix))
  201. }
  202. // GetDeviceDisplayName generates a display name in the format: {HOSTNAME} {PCI ID} - NVIDIA {MODEL} {VRAM}
  203. func GetDeviceDisplayName(device GPUDevice, hostname string) string {
  204. vramGB := float64(device.Memory) / (1024 * 1024 * 1024)
  205. // Extract model name from device name (remove "NVIDIA" prefix if present)
  206. modelName := device.Name
  207. if strings.HasPrefix(strings.ToUpper(modelName), "NVIDIA ") {
  208. modelName = strings.TrimPrefix(modelName, "NVIDIA ")
  209. }
  210. // Use short format PCI Bus ID (00:04:00.0 instead of 00000000:04:00.0)
  211. shortPCIBusID := GetShortPCIBusID(device.PCIBusID)
  212. return fmt.Sprintf("%s %s - NVIDIA %s %.0fGB", hostname, shortPCIBusID, modelName, vramGB)
  213. }
  214. // GetNVMLVersion returns the NVML version information
  215. func GetNVMLVersion() (string, error) {
  216. requestMutex.Lock()
  217. defer requestMutex.Unlock()
  218. version, ret := nvml.SystemGetNVMLVersion()
  219. if ret != nvml.SUCCESS {
  220. return "", fmt.Errorf("failed to get NVML version: %s", nvml.ErrorString(ret))
  221. }
  222. return version, nil
  223. }
  224. // GetDriverVersion returns the NVIDIA driver version
  225. func GetDriverVersion() (string, error) {
  226. requestMutex.Lock()
  227. defer requestMutex.Unlock()
  228. version, ret := nvml.SystemGetDriverVersion()
  229. if ret != nvml.SUCCESS {
  230. return "", fmt.Errorf("failed to get driver version: %s", nvml.ErrorString(ret))
  231. }
  232. return version, nil
  233. }
  234. // IsDeviceAvailable checks if a GPU device is still available and responsive
  235. func IsDeviceAvailable(device GPUDevice) bool {
  236. requestMutex.Lock()
  237. defer requestMutex.Unlock()
  238. // Try to get device name as a simple health check
  239. _, ret := device.Handle.GetName()
  240. return ret == nvml.SUCCESS
  241. }