dcgm_saturation.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. package kubemodel
  2. import (
  3. "time"
  4. "github.com/opencost/opencost/core/pkg/model/kubemodel"
  5. "github.com/opencost/opencost/core/pkg/source"
  6. )
  7. // DCGM device saturation hydration. The saturation queries are
  8. // container-attributed series (dcgm-exporter duplicates each device-level
  9. // value onto every pod sharing the device), so hydration reduces them to
  10. // one value per device UUID. MIG instances surface as their own UUIDs when
  11. // the exporter runs in MIG mode, so slices hydrate as distinct devices and
  12. // PodUsages preserves the container-to-slice association.
  13. // dcgmSaturationFutures holds the in-flight saturation queries for one
  14. // computeDCGMDevices pass, mirroring the bundle used by ComputeAllocation.
  15. type dcgmSaturationFutures struct {
  16. throttleViolation *source.QueryGroupFuture[source.GPUSaturationResult]
  17. throttleReason *source.QueryGroupFuture[source.GPUSaturationResult]
  18. memoryUsedAvg *source.QueryGroupFuture[source.GPUSaturationResult]
  19. memoryUsedMax *source.QueryGroupFuture[source.GPUSaturationResult]
  20. memoryPressure *source.QueryGroupFuture[source.GPUSaturationResult]
  21. xidErrorCount *source.QueryGroupFuture[source.GPUSaturationResult]
  22. dramActiveAvg *source.QueryGroupFuture[source.GPUSaturationResult]
  23. dramActiveMax *source.QueryGroupFuture[source.GPUSaturationResult]
  24. smActiveAvg *source.QueryGroupFuture[source.GPUSaturationResult]
  25. smOccupancyAvg *source.QueryGroupFuture[source.GPUSaturationResult]
  26. pcieTxBytesAvg *source.QueryGroupFuture[source.GPUSaturationResult]
  27. pcieRxBytesAvg *source.QueryGroupFuture[source.GPUSaturationResult]
  28. nvlinkTxBytesAvg *source.QueryGroupFuture[source.GPUSaturationResult]
  29. nvlinkRxBytesAvg *source.QueryGroupFuture[source.GPUSaturationResult]
  30. }
  31. func startDCGMSaturationQueries(grp *source.QueryGroup, metrics source.MetricsQuerier, start, end time.Time) *dcgmSaturationFutures {
  32. return &dcgmSaturationFutures{
  33. throttleViolation: source.WithGroup(grp, metrics.QueryGPUThrottleViolationRatio(start, end)),
  34. throttleReason: source.WithGroup(grp, metrics.QueryGPUThrottleReasonRatio(start, end)),
  35. memoryUsedAvg: source.WithGroup(grp, metrics.QueryGPUMemoryUsedRatioAvg(start, end)),
  36. memoryUsedMax: source.WithGroup(grp, metrics.QueryGPUMemoryUsedRatioMax(start, end)),
  37. memoryPressure: source.WithGroup(grp, metrics.QueryGPUMemoryPressureRatio(start, end)),
  38. xidErrorCount: source.WithGroup(grp, metrics.QueryGPUXIDErrorCount(start, end)),
  39. dramActiveAvg: source.WithGroup(grp, metrics.QueryGPUDRAMActiveAvg(start, end)),
  40. dramActiveMax: source.WithGroup(grp, metrics.QueryGPUDRAMActiveMax(start, end)),
  41. smActiveAvg: source.WithGroup(grp, metrics.QueryGPUSMActiveAvg(start, end)),
  42. smOccupancyAvg: source.WithGroup(grp, metrics.QueryGPUSMOccupancyAvg(start, end)),
  43. pcieTxBytesAvg: source.WithGroup(grp, metrics.QueryGPUPCIeTxBytesAvg(start, end)),
  44. pcieRxBytesAvg: source.WithGroup(grp, metrics.QueryGPUPCIeRxBytesAvg(start, end)),
  45. nvlinkTxBytesAvg: source.WithGroup(grp, metrics.QueryGPUNVLinkTxBytesAvg(start, end)),
  46. nvlinkRxBytesAvg: source.WithGroup(grp, metrics.QueryGPUNVLinkRxBytesAvg(start, end)),
  47. }
  48. }
  49. // awaitAndApply awaits every saturation query and reduces the results onto
  50. // the device map. Per-future errors are recorded in the query group by
  51. // Await, matching how the rest of computeDCGMDevices treats its queries.
  52. func (f *dcgmSaturationFutures) awaitAndApply(deviceMap map[string]*kubemodel.DCGMDevice) {
  53. if f == nil {
  54. return
  55. }
  56. resThrottleViolation, _ := f.throttleViolation.Await()
  57. resThrottleReason, _ := f.throttleReason.Await()
  58. resMemoryUsedAvg, _ := f.memoryUsedAvg.Await()
  59. resMemoryUsedMax, _ := f.memoryUsedMax.Await()
  60. resMemoryPressure, _ := f.memoryPressure.Await()
  61. resXIDErrorCount, _ := f.xidErrorCount.Await()
  62. resDRAMActiveAvg, _ := f.dramActiveAvg.Await()
  63. resDRAMActiveMax, _ := f.dramActiveMax.Await()
  64. resSMActiveAvg, _ := f.smActiveAvg.Await()
  65. resSMOccupancyAvg, _ := f.smOccupancyAvg.Await()
  66. resPCIeTxBytesAvg, _ := f.pcieTxBytesAvg.Await()
  67. resPCIeRxBytesAvg, _ := f.pcieRxBytesAvg.Await()
  68. resNVLinkTxBytesAvg, _ := f.nvlinkTxBytesAvg.Await()
  69. resNVLinkRxBytesAvg, _ := f.nvlinkRxBytesAvg.Await()
  70. applyDeviceThrottleRatios(deviceMap, resThrottleViolation, func(sat *kubemodel.DCGMDeviceSaturation) map[string]float64 {
  71. if sat.ThrottleViolationRatios == nil {
  72. sat.ThrottleViolationRatios = make(map[string]float64)
  73. }
  74. return sat.ThrottleViolationRatios
  75. })
  76. applyDeviceThrottleRatios(deviceMap, resThrottleReason, func(sat *kubemodel.DCGMDeviceSaturation) map[string]float64 {
  77. if sat.ThrottleReasonRatios == nil {
  78. sat.ThrottleReasonRatios = make(map[string]float64)
  79. }
  80. return sat.ThrottleReasonRatios
  81. })
  82. applyDeviceSaturationScalar(deviceMap, resMemoryUsedAvg, func(sat *kubemodel.DCGMDeviceSaturation, v float64) { sat.MemoryUsedRatioAvg = &v })
  83. applyDeviceSaturationScalar(deviceMap, resMemoryUsedMax, func(sat *kubemodel.DCGMDeviceSaturation, v float64) { sat.MemoryUsedRatioMax = &v })
  84. applyDeviceSaturationScalar(deviceMap, resMemoryPressure, func(sat *kubemodel.DCGMDeviceSaturation, v float64) { sat.MemoryPressureRatio = &v })
  85. applyDeviceSaturationScalar(deviceMap, resXIDErrorCount, func(sat *kubemodel.DCGMDeviceSaturation, v float64) { sat.XIDErrorCount = &v })
  86. applyDeviceSaturationScalar(deviceMap, resDRAMActiveAvg, func(sat *kubemodel.DCGMDeviceSaturation, v float64) { sat.DRAMActiveAvg = &v })
  87. applyDeviceSaturationScalar(deviceMap, resDRAMActiveMax, func(sat *kubemodel.DCGMDeviceSaturation, v float64) { sat.DRAMActiveMax = &v })
  88. applyDeviceSaturationScalar(deviceMap, resSMActiveAvg, func(sat *kubemodel.DCGMDeviceSaturation, v float64) { sat.SMActiveAvg = &v })
  89. applyDeviceSaturationScalar(deviceMap, resSMOccupancyAvg, func(sat *kubemodel.DCGMDeviceSaturation, v float64) { sat.SMOccupancyAvg = &v })
  90. applyDeviceSaturationScalar(deviceMap, resPCIeTxBytesAvg, func(sat *kubemodel.DCGMDeviceSaturation, v float64) { sat.PCIeTxBytesAvg = &v })
  91. applyDeviceSaturationScalar(deviceMap, resPCIeRxBytesAvg, func(sat *kubemodel.DCGMDeviceSaturation, v float64) { sat.PCIeRxBytesAvg = &v })
  92. applyDeviceSaturationScalar(deviceMap, resNVLinkTxBytesAvg, func(sat *kubemodel.DCGMDeviceSaturation, v float64) { sat.NVLinkTxBytesAvg = &v })
  93. applyDeviceSaturationScalar(deviceMap, resNVLinkRxBytesAvg, func(sat *kubemodel.DCGMDeviceSaturation, v float64) { sat.NVLinkRxBytesAvg = &v })
  94. }
  95. // ensureDeviceSaturation lazily creates the saturation struct, so devices
  96. // with no signals keep Saturation nil (absence is never zero).
  97. func ensureDeviceSaturation(device *kubemodel.DCGMDevice) *kubemodel.DCGMDeviceSaturation {
  98. if device.Saturation == nil {
  99. device.Saturation = &kubemodel.DCGMDeviceSaturation{}
  100. }
  101. return device.Saturation
  102. }
  103. // applyDeviceSaturationScalar reduces container-attributed results to one
  104. // value per device UUID. Sharing containers carry duplicates of the same
  105. // device-level value, so last-write-wins is exact; results for UUIDs the
  106. // info query did not report are skipped.
  107. func applyDeviceSaturationScalar(deviceMap map[string]*kubemodel.DCGMDevice, results []*source.GPUSaturationResult, set func(sat *kubemodel.DCGMDeviceSaturation, value float64)) {
  108. for _, res := range results {
  109. device, ok := deviceMap[res.UUID]
  110. if !ok || len(res.Data) == 0 {
  111. continue
  112. }
  113. set(ensureDeviceSaturation(device), res.Data[0].Value)
  114. }
  115. }
  116. // applyDeviceThrottleRatios reduces reason-labeled throttle results onto the
  117. // device's reason map.
  118. func applyDeviceThrottleRatios(deviceMap map[string]*kubemodel.DCGMDevice, results []*source.GPUSaturationResult, ratios func(sat *kubemodel.DCGMDeviceSaturation) map[string]float64) {
  119. for _, res := range results {
  120. device, ok := deviceMap[res.UUID]
  121. if !ok || len(res.Data) == 0 || res.Reason == "" {
  122. continue
  123. }
  124. ratios(ensureDeviceSaturation(device))[res.Reason] = res.Data[0].Value
  125. }
  126. }
  127. // dcgmDeviceMetricFutures holds the device-level metric queries backing
  128. // DeviceInfo / DevicePerformance: power, temperature, device-level compute
  129. // utilization, and framebuffer used. Unlike saturation these are not
  130. // feature-gated: they are core device telemetry from the default
  131. // dcgm-exporter configuration.
  132. type dcgmDeviceMetricFutures struct {
  133. powerAvg *source.QueryGroupFuture[source.GPUDeviceMetricResult]
  134. tempAvg *source.QueryGroupFuture[source.GPUDeviceMetricResult]
  135. usageAvg *source.QueryGroupFuture[source.GPUDeviceMetricResult]
  136. usageMax *source.QueryGroupFuture[source.GPUDeviceMetricResult]
  137. memoryUsedAvg *source.QueryGroupFuture[source.GPUDeviceMetricResult]
  138. memoryUsedMax *source.QueryGroupFuture[source.GPUDeviceMetricResult]
  139. }
  140. func startDCGMDeviceMetricQueries(grp *source.QueryGroup, metrics source.MetricsQuerier, start, end time.Time) *dcgmDeviceMetricFutures {
  141. return &dcgmDeviceMetricFutures{
  142. powerAvg: source.WithGroup(grp, metrics.QueryGPUDevicePowerAvg(start, end)),
  143. tempAvg: source.WithGroup(grp, metrics.QueryGPUDeviceTempAvg(start, end)),
  144. usageAvg: source.WithGroup(grp, metrics.QueryGPUDeviceUsageAvg(start, end)),
  145. usageMax: source.WithGroup(grp, metrics.QueryGPUDeviceUsageMax(start, end)),
  146. memoryUsedAvg: source.WithGroup(grp, metrics.QueryGPUDeviceMemoryUsedAvg(start, end)),
  147. memoryUsedMax: source.WithGroup(grp, metrics.QueryGPUDeviceMemoryUsedMax(start, end)),
  148. }
  149. }
  150. const fbMiB = 1024 * 1024
  151. // awaitAndApply reduces the device-level metrics onto the device map,
  152. // scaling to the model's units: GR_ENGINE_ACTIVE ratio to percent (0-100),
  153. // FB_USED MiB to bytes.
  154. func (f *dcgmDeviceMetricFutures) awaitAndApply(deviceMap map[string]*kubemodel.DCGMDevice) {
  155. if f == nil {
  156. return
  157. }
  158. resPower, _ := f.powerAvg.Await()
  159. resTemp, _ := f.tempAvg.Await()
  160. resUsageAvg, _ := f.usageAvg.Await()
  161. resUsageMax, _ := f.usageMax.Await()
  162. resMemAvg, _ := f.memoryUsedAvg.Await()
  163. resMemMax, _ := f.memoryUsedMax.Await()
  164. applyDeviceMetric(deviceMap, resPower, func(d *kubemodel.DCGMDevice, v float64) { d.PowerWatts = v })
  165. applyDeviceMetric(deviceMap, resTemp, func(d *kubemodel.DCGMDevice, v float64) { d.TemperatureCelsius = v })
  166. applyDeviceMetric(deviceMap, resUsageAvg, func(d *kubemodel.DCGMDevice, v float64) { d.ComputeUtilizationAvg = v * 100 })
  167. applyDeviceMetric(deviceMap, resUsageMax, func(d *kubemodel.DCGMDevice, v float64) { d.ComputeUtilizationMax = v * 100 })
  168. applyDeviceMetric(deviceMap, resMemAvg, func(d *kubemodel.DCGMDevice, v float64) { d.MemoryUsedBytesAvg = v * fbMiB })
  169. applyDeviceMetric(deviceMap, resMemMax, func(d *kubemodel.DCGMDevice, v float64) { d.MemoryUsedBytesMax = v * fbMiB })
  170. }
  171. // applyDeviceMetric reduces device-keyed results onto device fields;
  172. // unknown UUIDs and empty results are skipped.
  173. func applyDeviceMetric(deviceMap map[string]*kubemodel.DCGMDevice, results []*source.GPUDeviceMetricResult, set func(d *kubemodel.DCGMDevice, value float64)) {
  174. for _, res := range results {
  175. device, ok := deviceMap[res.UUID]
  176. if !ok || len(res.Data) == 0 {
  177. continue
  178. }
  179. set(device, res.Data[0].Value)
  180. }
  181. }