Просмотр исходного кода

Update device and include in bingen

Sean Holcomb 2 недель назад
Родитель
Сommit
de27688051

+ 0 - 89
core/pkg/compute/kubemodel/dcgmdevice.go

@@ -1,89 +0,0 @@
-package kubemodel
-
-import (
-	"time"
-
-	"github.com/opencost/opencost/core/pkg/log"
-	"github.com/opencost/opencost/core/pkg/model/kubemodel"
-	"github.com/opencost/opencost/core/pkg/source"
-)
-
-func (km *KubeModel) computeDCGMDevices(kms *kubemodel.KubeModelSet, start, end time.Time) error {
-	grp := source.NewQueryGroup()
-	metrics := km.ds.Metrics()
-
-	dcgmInfoFuture := source.WithGroup(grp, metrics.QueryDCGMDeviceInfo(start, end))
-	dcgmUptimeFuture := source.WithGroup(grp, metrics.QueryDCGMDeviceUptime(start, end))
-	dcgmUsageAvgFuture := source.WithGroup(grp, metrics.QueryDCGMContainerUsageAvg(start, end))
-	dcgmUsageMaxFuture := source.WithGroup(grp, metrics.QueryDCGMContainerUsageMax(start, end))
-
-	deviceMap := make(map[string]*kubemodel.DCGMDevice)
-
-	dcgmInfoResult, _ := dcgmInfoFuture.Await()
-	for _, res := range dcgmInfoResult {
-		if res.UUID == "" {
-			continue
-		}
-		if _, ok := deviceMap[res.UUID]; ok {
-			continue
-		}
-		deviceMap[res.UUID] = &kubemodel.DCGMDevice{
-			UUID:      res.UUID,
-			Device:    res.Device,
-			ModelName: res.ModelName,
-			PodUsages: make(map[string]kubemodel.DCGMPod),
-		}
-	}
-
-	dcgmUptimeResult, _ := dcgmUptimeFuture.Await()
-	for _, res := range dcgmUptimeResult {
-		d, ok := deviceMap[res.UUID]
-		if !ok {
-			log.Warnf("DCGM uptime result for unknown device UUID '%s'", res.UUID)
-			continue
-		}
-		s, e := res.GetStartEnd(start, end, km.ds.Resolution())
-		d.Start = s
-		d.End = e
-	}
-
-	dcgmUsageAvgResult, _ := dcgmUsageAvgFuture.Await()
-	for _, res := range dcgmUsageAvgResult {
-		device, ok := deviceMap[res.UUID]
-		if !ok || res.PodUID == "" || res.Container == "" {
-			continue
-		}
-		pod, ok := device.PodUsages[res.PodUID]
-		if !ok {
-			pod = kubemodel.DCGMPod{ContainerUsages: make(map[string]kubemodel.DCGMContainer)}
-		}
-		c := pod.ContainerUsages[res.Container]
-		c.UsageAvg = res.Value
-		pod.ContainerUsages[res.Container] = c
-		device.PodUsages[res.PodUID] = pod
-	}
-
-	dcgmUsageMaxResult, _ := dcgmUsageMaxFuture.Await()
-	for _, res := range dcgmUsageMaxResult {
-		device, ok := deviceMap[res.UUID]
-		if !ok || res.PodUID == "" || res.Container == "" {
-			continue
-		}
-		pod, ok := device.PodUsages[res.PodUID]
-		if !ok {
-			pod = kubemodel.DCGMPod{ContainerUsages: make(map[string]kubemodel.DCGMContainer)}
-		}
-		c := pod.ContainerUsages[res.Container]
-		c.UsageMax = res.Value
-		pod.ContainerUsages[res.Container] = c
-		device.PodUsages[res.PodUID] = pod
-	}
-
-	for _, device := range deviceMap {
-		if err := kms.RegisterDCGMDevice(device); err != nil {
-			log.Warnf("Failed to register DCGM device: %s", err.Error())
-		}
-	}
-
-	return nil
-}

+ 83 - 0
core/pkg/compute/kubemodel/device.go

@@ -0,0 +1,83 @@
+package kubemodel
+
+import (
+	"time"
+
+	"github.com/opencost/opencost/core/pkg/log"
+	"github.com/opencost/opencost/core/pkg/model/kubemodel"
+	"github.com/opencost/opencost/core/pkg/source"
+)
+
+func (km *KubeModel) computeDevices(kms *kubemodel.KubeModelSet, start, end time.Time) error {
+	grp := source.NewQueryGroup()
+	metrics := km.ds.Metrics()
+
+	infoFuture := source.WithGroup(grp, metrics.QueryDCGMDeviceInfo(start, end))
+	uptimeFuture := source.WithGroup(grp, metrics.QueryDCGMDeviceUptime(start, end))
+	usageAvgFuture := source.WithGroup(grp, metrics.QueryDCGMContainerUsageAvg(start, end))
+	usageMaxFuture := source.WithGroup(grp, metrics.QueryDCGMContainerUsageMax(start, end))
+
+	deviceMap := make(map[string]*kubemodel.Device)
+
+	infoResult, _ := infoFuture.Await()
+	for _, res := range infoResult {
+		if res.UUID == "" {
+			continue
+		}
+		if _, ok := deviceMap[res.UUID]; ok {
+			continue
+		}
+		deviceMap[res.UUID] = &kubemodel.Device{
+			UUID:      res.UUID,
+			Device:    res.Device,
+			ModelName: res.ModelName,
+		}
+	}
+
+	uptimeResult, _ := uptimeFuture.Await()
+	for _, res := range uptimeResult {
+		d, ok := deviceMap[res.UUID]
+		if !ok {
+			log.Warnf("DCGM uptime result for unknown device UUID '%s'", res.UUID)
+			continue
+		}
+		s, e := res.GetStartEnd(start, end, km.ds.Resolution())
+		d.Start = s
+		d.End = e
+	}
+
+	for _, device := range deviceMap {
+		if err := kms.RegisterDevice(device); err != nil {
+			log.Warnf("Failed to register device: %s", err.Error())
+		}
+	}
+
+	setUsage := func(res *source.DCGMDeviceContainerUsageResult, apply func(*kubemodel.DeviceUsage)) {
+		if res.PodUID == "" || res.Container == "" {
+			return
+		}
+		key := (&kubemodel.Container{PodUID: res.PodUID, Name: res.Container}).GetKey()
+		container, ok := kms.Containers[key]
+		if !ok {
+			return
+		}
+		if container.DeviceUsages == nil {
+			container.DeviceUsages = make(map[string]kubemodel.DeviceUsage)
+		}
+		usage := container.DeviceUsages[res.UUID]
+		apply(&usage)
+		container.DeviceUsages[res.UUID] = usage
+	}
+
+	usageAvgResult, _ := usageAvgFuture.Await()
+	for _, res := range usageAvgResult {
+		setUsage(res, func(u *kubemodel.DeviceUsage) { u.UsageAvg = res.Value })
+	}
+
+	usageMaxResult, _ := usageMaxFuture.Await()
+	for _, res := range usageMaxResult {
+		setUsage(res, func(u *kubemodel.DeviceUsage) { u.UsageMax = res.Value })
+	}
+
+	return nil
+}

+ 71 - 31
core/pkg/compute/kubemodel/dcgmdevice_test.go → core/pkg/compute/kubemodel/device_test.go

@@ -11,22 +11,25 @@ import (
 	"github.com/opencost/opencost/core/pkg/source"
 )
 
-func TestComputeDCGMDevices(t *testing.T) {
+func TestComputeDevices(t *testing.T) {
 	start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
 	end := start.Add(time.Hour)
 
 	tests := []struct {
-		name      string
-		overrides map[string]any
-		want      map[string]*kubemodel.DCGMDevice
+		name         string
+		overrides    map[string]any
+		containers   map[string]*kubemodel.Container
+		wantDevices  map[string]*kubemodel.Device
+		wantUsageKey string
+		wantUsage    *kubemodel.DeviceUsage
 	}{
 		{
-			name:      "no data returns empty dcgm device map",
-			overrides: map[string]any{},
-			want:      map[string]*kubemodel.DCGMDevice{},
+			name:        "no data returns empty device map",
+			overrides:   map[string]any{},
+			wantDevices: map[string]*kubemodel.Device{},
 		},
 		{
-			name: "basic dcgm device info and uptime",
+			name: "basic device info and uptime",
 			overrides: map[string]any{
 				source.QueryDCGMDeviceInfo: []*source.DCGMDeviceInfoResult{
 					{UUID: "GPU-abc123", Device: "nvidia0", ModelName: "A100"},
@@ -35,28 +38,27 @@ func TestComputeDCGMDevices(t *testing.T) {
 					{UUID: "GPU-abc123", First: start, Last: end},
 				},
 			},
-			want: map[string]*kubemodel.DCGMDevice{
+			wantDevices: map[string]*kubemodel.Device{
 				"GPU-abc123": {
 					UUID:      "GPU-abc123",
 					Device:    "nvidia0",
 					ModelName: "A100",
 					Start:     start,
 					End:       end,
-					PodUsages: map[string]kubemodel.DCGMPod{},
 				},
 			},
 		},
 		{
-			name: "dcgm device without uptime is not registered",
+			name: "device without uptime is not registered",
 			overrides: map[string]any{
 				source.QueryDCGMDeviceInfo: []*source.DCGMDeviceInfoResult{
 					{UUID: "GPU-abc123", Device: "nvidia0", ModelName: "A100"},
 				},
 			},
-			want: map[string]*kubemodel.DCGMDevice{},
+			wantDevices: map[string]*kubemodel.Device{},
 		},
 		{
-			name: "dcgm device with empty uuid is skipped",
+			name: "device with empty uuid is skipped",
 			overrides: map[string]any{
 				source.QueryDCGMDeviceInfo: []*source.DCGMDeviceInfoResult{
 					{UUID: "", Device: "nvidia0", ModelName: "A100"},
@@ -65,10 +67,31 @@ func TestComputeDCGMDevices(t *testing.T) {
 					{UUID: "GPU-abc123", First: start, Last: end},
 				},
 			},
-			want: map[string]*kubemodel.DCGMDevice{},
+			wantDevices: map[string]*kubemodel.Device{},
 		},
 		{
-			name: "dcgm container usage avg and max are populated",
+			name: "duplicate device info entries use first occurrence",
+			overrides: map[string]any{
+				source.QueryDCGMDeviceInfo: []*source.DCGMDeviceInfoResult{
+					{UUID: "GPU-abc123", Device: "nvidia0", ModelName: "A100"},
+					{UUID: "GPU-abc123", Device: "nvidia0-dup", ModelName: "A100-dup"},
+				},
+				source.QueryDCGMDeviceUptime: []*source.DCGMDeviceUptimeResult{
+					{UUID: "GPU-abc123", First: start, Last: end},
+				},
+			},
+			wantDevices: map[string]*kubemodel.Device{
+				"GPU-abc123": {
+					UUID:      "GPU-abc123",
+					Device:    "nvidia0",
+					ModelName: "A100",
+					Start:     start,
+					End:       end,
+				},
+			},
+		},
+		{
+			name: "container usage avg and max are applied to a registered container",
 			overrides: map[string]any{
 				source.QueryDCGMDeviceInfo: []*source.DCGMDeviceInfoResult{
 					{UUID: "GPU-abc123", Device: "nvidia0", ModelName: "A100"},
@@ -83,22 +106,20 @@ func TestComputeDCGMDevices(t *testing.T) {
 					{UUID: "GPU-abc123", PodUID: "pod-1", Container: "training", Value: 0.95},
 				},
 			},
-			want: map[string]*kubemodel.DCGMDevice{
+			containers: map[string]*kubemodel.Container{
+				"pod-1/training": {PodUID: "pod-1", Name: "training"},
+			},
+			wantDevices: map[string]*kubemodel.Device{
 				"GPU-abc123": {
 					UUID:      "GPU-abc123",
 					Device:    "nvidia0",
 					ModelName: "A100",
 					Start:     start,
 					End:       end,
-					PodUsages: map[string]kubemodel.DCGMPod{
-						"pod-1": {
-							ContainerUsages: map[string]kubemodel.DCGMContainer{
-								"training": {UsageAvg: 0.75, UsageMax: 0.95},
-							},
-						},
-					},
 				},
 			},
+			wantUsageKey: "pod-1/training",
+			wantUsage:    &kubemodel.DeviceUsage{UsageAvg: 0.75, UsageMax: 0.95},
 		},
 		{
 			name: "usage with empty pod uid or container is ignored",
@@ -114,36 +135,41 @@ func TestComputeDCGMDevices(t *testing.T) {
 					{UUID: "GPU-abc123", PodUID: "pod-1", Container: "", Value: 0.5},
 				},
 			},
-			want: map[string]*kubemodel.DCGMDevice{
+			containers: map[string]*kubemodel.Container{
+				"pod-1/training": {PodUID: "pod-1", Name: "training"},
+			},
+			wantDevices: map[string]*kubemodel.Device{
 				"GPU-abc123": {
 					UUID:      "GPU-abc123",
 					Device:    "nvidia0",
 					ModelName: "A100",
 					Start:     start,
 					End:       end,
-					PodUsages: map[string]kubemodel.DCGMPod{},
 				},
 			},
+			wantUsageKey: "pod-1/training",
+			wantUsage:    nil,
 		},
 		{
-			name: "duplicate device info entries use first occurrence",
+			name: "usage for an unregistered container is ignored",
 			overrides: map[string]any{
 				source.QueryDCGMDeviceInfo: []*source.DCGMDeviceInfoResult{
 					{UUID: "GPU-abc123", Device: "nvidia0", ModelName: "A100"},
-					{UUID: "GPU-abc123", Device: "nvidia0-dup", ModelName: "A100-dup"},
 				},
 				source.QueryDCGMDeviceUptime: []*source.DCGMDeviceUptimeResult{
 					{UUID: "GPU-abc123", First: start, Last: end},
 				},
+				source.QueryDCGMContainerUsageAvg: []*source.DCGMDeviceContainerUsageResult{
+					{UUID: "GPU-abc123", PodUID: "pod-1", Container: "training", Value: 0.75},
+				},
 			},
-			want: map[string]*kubemodel.DCGMDevice{
+			wantDevices: map[string]*kubemodel.Device{
 				"GPU-abc123": {
 					UUID:      "GPU-abc123",
 					Device:    "nvidia0",
 					ModelName: "A100",
 					Start:     start,
 					End:       end,
-					PodUsages: map[string]kubemodel.DCGMPod{},
 				},
 			},
 		},
@@ -162,11 +188,25 @@ func TestComputeDCGMDevices(t *testing.T) {
 			require.NoError(t, err)
 
 			kms := kubemodel.NewKubeModelSet(start, end)
+			if tt.containers != nil {
+				kms.Containers = tt.containers
+			}
 
-			err = km.computeDCGMDevices(kms, start, end)
+			err = km.computeDevices(kms, start, end)
 			require.NoError(t, err)
 
-			assert.Equal(t, tt.want, kms.DCGMDevices)
+			assert.Equal(t, tt.wantDevices, kms.Devices)
+
+			if tt.wantUsageKey != "" {
+				c, ok := kms.Containers[tt.wantUsageKey]
+				require.True(t, ok)
+				if tt.wantUsage == nil {
+					assert.Empty(t, c.DeviceUsages)
+				} else {
+					require.NotNil(t, c.DeviceUsages)
+					assert.Equal(t, *tt.wantUsage, c.DeviceUsages["GPU-abc123"])
+				}
+			}
 		})
 	}
 }

+ 1 - 1
core/pkg/compute/kubemodel/kubemodel.go

@@ -104,6 +104,6 @@ func (km *KubeModel) computeFuncs(start, end time.Time) []computeFunc {
 		km.computePersistentVolumeClaims,
 		km.computePods,
 		km.computeContainers,
-		//km.computeDCGMDevices,
+		km.computeDevices,
 	}
 }

+ 1 - 1
core/pkg/compute/kubemodel/kubemodel_test.go

@@ -185,7 +185,7 @@ func TestComputeKubeModelSet(t *testing.T) {
 				assert.NotEmpty(t, kms.Services)
 				assert.NotEmpty(t, kms.PersistentVolumes)
 				assert.NotEmpty(t, kms.PersistentVolumeClaims)
-				//assert.NotEmpty(t, kms.DCGMDevices)
+				assert.NotEmpty(t, kms.Devices)
 			},
 		},
 	}

+ 1 - 1
core/pkg/model/kubemodel/bingen.go

@@ -22,4 +22,4 @@ package kubemodel
 
 // @bingen:define[string]:github.com/opencost/opencost/core/pkg/cloud.Provider
 
-//go:generate bingen -package=kubemodel -version=2
+//go:generate bingen -package=kubemodel -version=3

+ 22 - 12
core/pkg/model/kubemodel/container.go

@@ -7,18 +7,28 @@ import (
 
 // @bingen:generate:Container
 type Container struct {
-	PodUID                string             `json:"podUid"`
-	Name                  string             `json:"name"`
-	ResourceRequests      ResourceQuantities `json:"resourceRequests"`
-	ResourceLimits        ResourceQuantities `json:"resourceLimits"`
-	CPUCoreAllocationAvg  float64            `json:"cpuCoreAllocationAvg"`
-	CPUCoreUsageAvg       float64            `json:"cpuCoreUsageAvg"`
-	CPUCoreUsageMax       float64            `json:"cpuCoreUsageMax"`
-	RAMBytesAllocationAvg float64            `json:"ramBytesAllocationAvg"`
-	RAMBytesUsageAvg      float64            `json:"ramBytesUsageAvg"`
-	RAMBytesUsageMax      float64            `json:"ramBytesUsageMax"`
-	Start                 time.Time          `json:"start"`
-	End                   time.Time          `json:"end"`
+	PodUID                string                 `json:"podUid"`
+	Name                  string                 `json:"name"`
+	ResourceRequests      ResourceQuantities     `json:"resourceRequests"`
+	ResourceLimits        ResourceQuantities     `json:"resourceLimits"`
+	CPUCoreAllocationAvg  float64                `json:"cpuCoreAllocationAvg"`
+	CPUCoreUsageAvg       float64                `json:"cpuCoreUsageAvg"`
+	CPUCoreUsageMax       float64                `json:"cpuCoreUsageMax"`
+	RAMBytesAllocationAvg float64                `json:"ramBytesAllocationAvg"`
+	RAMBytesUsageAvg      float64                `json:"ramBytesUsageAvg"`
+	RAMBytesUsageMax      float64                `json:"ramBytesUsageMax"`
+	DeviceUsages          map[string]DeviceUsage `json:"deviceUsages"` // @bingen:field[version=3]
+	Start                 time.Time              `json:"start"`
+	End                   time.Time              `json:"end"`
+}
+
+// DeviceUsage holds usage metrics for a single container/device pairing. The shape is
+// vendor-agnostic, but the only populating source currently implemented is the DCGM exporter.
+// It is keyed by Device.UUID under Container.DeviceUsages.
+// @bingen:generate:DeviceUsage
+type DeviceUsage struct {
+	UsageAvg float64 `json:"usageAvg"`
+	UsageMax float64 `json:"usageMax"`
 }
 
 func (c *Container) GetKey() string {

+ 0 - 66
core/pkg/model/kubemodel/dcgm.go

@@ -1,66 +0,0 @@
-package kubemodel
-
-import (
-	"fmt"
-	"time"
-)
-
-// DCGMDevice holds recording from the DCGM exporter which provides identification and usage metrics for
-// Nvidia gpu. These Nvidia devices can be incorporated into the cluster via k8s Device Plugin API or DRAs.
-// While the DCGM exporter does provide unique identifiers for the containers that it is reporting metrics on,
-// It is split out here to provide some isolation from the rest of the KubeModel which represent universal structures
-// from the k8s API. It is left to the end user to interpret the relationships to the rest of the cluster based on
-// container unique identifiers
-// @bingen:generate:DCGMDevice
-type DCGMDevice struct {
-	UUID      string             `json:"uuid"`
-	Start     time.Time          `json:"start"`
-	End       time.Time          `json:"end"`
-	Device    string             `json:"device"`
-	ModelName string             `json:"modelName"`
-	PodUsages map[string]DCGMPod `json:"podUsages"`
-}
-
-// @bingen:generate:DCGMPod
-type DCGMPod struct {
-	ContainerUsages map[string]DCGMContainer `json:"container-usages"`
-}
-
-// @bingen:generate:DCGMContainer
-type DCGMContainer struct {
-	UsageAvg float64 `json:"usageAvg"`
-	UsageMax float64 `json:"usageMax"`
-}
-
-func (d *DCGMDevice) ValidateDCGMDevice(window Window) error {
-	if d.UUID == "" {
-		return fmt.Errorf("UUID is missing for DCGMDevice with device '%s'", d.Device)
-	}
-
-	if err := checkWindow(window, d.Start, d.End); err != nil {
-		return err
-	}
-
-	return nil
-}
-
-// RegisterDCGMDevice validates and adds a DCGMDevice to the set, keyed by UUID.
-func (kms *KubeModelSet) RegisterDCGMDevice(device *DCGMDevice) error {
-	if err := device.ValidateDCGMDevice(kms.Window); err != nil {
-		err = fmt.Errorf("RegisterDCGMDevice: invalid dcgm device: %w", err)
-		kms.Error(err)
-		return err
-	}
-
-	if _, ok := kms.DCGMDevices[device.UUID]; !ok {
-		if kms.Cluster == nil {
-			kms.Warnf("RegisterDCGMDevice: Cluster is nil")
-		}
-
-		kms.DCGMDevices[device.UUID] = device
-
-		kms.Metadata.ObjectCount++
-	}
-
-	return nil
-}

+ 52 - 0
core/pkg/model/kubemodel/device.go

@@ -0,0 +1,52 @@
+package kubemodel
+
+import (
+	"fmt"
+	"time"
+)
+
+// Device holds identification for an accelerator device (e.g. an Nvidia GPU) attached to the
+// cluster via the k8s Device Plugin API or DRAs. The shape is vendor-agnostic, but the only
+// populating source currently implemented is the DCGM exporter. Usage of a Device by a specific
+// container is recorded on Container.DeviceUsages, keyed by Device.UUID.
+// @bingen:generate:Device
+type Device struct {
+	UUID      string    `json:"uuid"`
+	Start     time.Time `json:"start"`
+	End       time.Time `json:"end"`
+	Device    string    `json:"device"`
+	ModelName string    `json:"modelName"`
+}
+
+func (d *Device) ValidateDevice(window Window) error {
+	if d.UUID == "" {
+		return fmt.Errorf("UUID is missing for Device with device '%s'", d.Device)
+	}
+
+	if err := checkWindow(window, d.Start, d.End); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// RegisterDevice validates and adds a Device to the set, keyed by UUID.
+func (kms *KubeModelSet) RegisterDevice(device *Device) error {
+	if err := device.ValidateDevice(kms.Window); err != nil {
+		err = fmt.Errorf("RegisterDevice: invalid device: %w", err)
+		kms.Error(err)
+		return err
+	}
+
+	if _, ok := kms.Devices[device.UUID]; !ok {
+		if kms.Cluster == nil {
+			kms.Warnf("RegisterDevice: Cluster is nil")
+		}
+
+		kms.Devices[device.UUID] = device
+
+		kms.Metadata.ObjectCount++
+	}
+
+	return nil
+}

+ 20 - 20
core/pkg/model/kubemodel/dcgm_test.go → core/pkg/model/kubemodel/device_test.go

@@ -7,35 +7,35 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestValidateDCGMDevice(t *testing.T) {
+func TestValidateDevice(t *testing.T) {
 	start := time.Now().UTC().Truncate(time.Hour)
 	end := start.Add(time.Hour)
 	window := Window{Start: start, End: end}
 
 	tests := []struct {
 		name    string
-		device  *DCGMDevice
+		device  *Device
 		wantErr string
 	}{
 		{
 			name:    "empty UUID",
-			device:  &DCGMDevice{Device: "GPU-0", Start: start, End: end},
-			wantErr: "UUID is missing for DCGMDevice with device 'GPU-0'",
+			device:  &Device{Device: "GPU-0", Start: start, End: end},
+			wantErr: "UUID is missing for Device with device 'GPU-0'",
 		},
 		{
 			name:    "outside window",
-			device:  &DCGMDevice{UUID: "gpu-uuid", Device: "GPU-0", Start: start.Add(-time.Hour), End: end},
+			device:  &Device{UUID: "gpu-uuid", Device: "GPU-0", Start: start.Add(-time.Hour), End: end},
 			wantErr: checkWindow(window, start.Add(-time.Hour), end).Error(),
 		},
 		{
 			name:   "valid",
-			device: &DCGMDevice{UUID: "gpu-uuid", Device: "GPU-0", Start: start, End: end},
+			device: &Device{UUID: "gpu-uuid", Device: "GPU-0", Start: start, End: end},
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			err := tt.device.ValidateDCGMDevice(window)
+			err := tt.device.ValidateDevice(window)
 			if tt.wantErr != "" {
 				require.EqualError(t, err, tt.wantErr)
 			} else {
@@ -45,12 +45,12 @@ func TestValidateDCGMDevice(t *testing.T) {
 	}
 }
 
-func TestRegisterDCGMDevice(t *testing.T) {
+func TestRegisterDevice(t *testing.T) {
 	start := time.Now().UTC().Truncate(time.Hour)
 	end := start.Add(time.Hour)
 
-	newDevice := func(uuid, device string) *DCGMDevice {
-		return &DCGMDevice{UUID: uuid, Device: device, Start: start, End: end}
+	newDevice := func(uuid, device string) *Device {
+		return &Device{UUID: uuid, Device: device, Start: start, End: end}
 	}
 	withCluster := func(kms *KubeModelSet) {
 		kms.RegisterCluster(&Cluster{UID: "cluster-uid", Start: start, End: end})
@@ -59,18 +59,18 @@ func TestRegisterDCGMDevice(t *testing.T) {
 	tests := []struct {
 		name    string
 		setup   func(*KubeModelSet)
-		device  *DCGMDevice
+		device  *Device
 		wantErr string
 		want    *KubeModelSet
 	}{
 		{
 			name:    "validation failure",
-			device:  &DCGMDevice{UUID: "", Device: "GPU-0", Start: start, End: end},
-			wantErr: "RegisterDCGMDevice: invalid dcgm device: UUID is missing for DCGMDevice with device 'GPU-0'",
+			device:  &Device{UUID: "", Device: "GPU-0", Start: start, End: end},
+			wantErr: "RegisterDevice: invalid device: UUID is missing for Device with device 'GPU-0'",
 			want: func() *KubeModelSet {
 				kms := NewKubeModelSet(start, end)
 				kms.Metadata.Diagnostics = []Diagnostic{
-					{Level: DiagnosticLevelError, Message: "RegisterDCGMDevice: invalid dcgm device: UUID is missing for DCGMDevice with device 'GPU-0'"},
+					{Level: DiagnosticLevelError, Message: "RegisterDevice: invalid device: UUID is missing for Device with device 'GPU-0'"},
 				}
 				return kms
 			}(),
@@ -80,10 +80,10 @@ func TestRegisterDCGMDevice(t *testing.T) {
 			device: newDevice("gpu-uuid", "GPU-0"),
 			want: func() *KubeModelSet {
 				kms := NewKubeModelSet(start, end)
-				kms.DCGMDevices["gpu-uuid"] = newDevice("gpu-uuid", "GPU-0")
+				kms.Devices["gpu-uuid"] = newDevice("gpu-uuid", "GPU-0")
 				kms.Metadata.ObjectCount = 1
 				kms.Metadata.Diagnostics = []Diagnostic{
-					{Level: DiagnosticLevelWarning, Message: "RegisterDCGMDevice: Cluster is nil"},
+					{Level: DiagnosticLevelWarning, Message: "RegisterDevice: Cluster is nil"},
 				}
 				return kms
 			}(),
@@ -95,7 +95,7 @@ func TestRegisterDCGMDevice(t *testing.T) {
 			want: func() *KubeModelSet {
 				kms := NewKubeModelSet(start, end)
 				withCluster(kms)
-				kms.DCGMDevices["gpu-uuid"] = newDevice("gpu-uuid", "GPU-0")
+				kms.Devices["gpu-uuid"] = newDevice("gpu-uuid", "GPU-0")
 				kms.Metadata.ObjectCount = 1
 				return kms
 			}(),
@@ -104,13 +104,13 @@ func TestRegisterDCGMDevice(t *testing.T) {
 			name: "duplicate registration is a no-op",
 			setup: func(kms *KubeModelSet) {
 				withCluster(kms)
-				kms.RegisterDCGMDevice(newDevice("gpu-uuid", "GPU-0"))
+				kms.RegisterDevice(newDevice("gpu-uuid", "GPU-0"))
 			},
 			device: newDevice("gpu-uuid", "GPU-1"),
 			want: func() *KubeModelSet {
 				kms := NewKubeModelSet(start, end)
 				withCluster(kms)
-				kms.DCGMDevices["gpu-uuid"] = newDevice("gpu-uuid", "GPU-0")
+				kms.Devices["gpu-uuid"] = newDevice("gpu-uuid", "GPU-0")
 				kms.Metadata.ObjectCount = 1
 				return kms
 			}(),
@@ -124,7 +124,7 @@ func TestRegisterDCGMDevice(t *testing.T) {
 				tt.setup(kms)
 			}
 
-			err := kms.RegisterDCGMDevice(tt.device)
+			err := kms.RegisterDevice(tt.device)
 
 			if tt.wantErr != "" {
 				require.EqualError(t, err, tt.wantErr)

+ 3 - 3
core/pkg/model/kubemodel/kubemodel.go

@@ -24,7 +24,7 @@ type KubeModelSet struct {
 	PersistentVolumeClaims map[string]*PersistentVolumeClaim `json:"pvcs"`              // @bingen:field[version=2]
 	Pods                   map[string]*Pod                   `json:"pods"`              // @bingen:field[version=2]
 	Containers             map[string]*Container             `json:"containers"`        // @bingen:field[version=2]
-	DCGMDevices            map[string]*DCGMDevice            `json:"dcgmDevices"`       // @bingen:field[ignore]
+	Devices                map[string]*Device                `json:"devices"`           // @bingen:field[version=3]
 }
 
 func NewKubeModelSet(start time.Time, end time.Time) *KubeModelSet {
@@ -48,7 +48,7 @@ func NewKubeModelSet(start time.Time, end time.Time) *KubeModelSet {
 		ReplicaSets:            map[string]*ReplicaSet{},
 		Namespaces:             map[string]*Namespace{},
 		Nodes:                  map[string]*Node{},
-		DCGMDevices:            map[string]*DCGMDevice{},
+		Devices:                map[string]*Device{},
 		Pods:                   map[string]*Pod{},
 		PersistentVolumeClaims: map[string]*PersistentVolumeClaim{},
 		ResourceQuotas:         map[string]*ResourceQuota{},
@@ -74,7 +74,7 @@ func (kms *KubeModelSet) IsEmpty() bool {
 		len(kms.ReplicaSets) == 0 &&
 		len(kms.Namespaces) == 0 &&
 		len(kms.Nodes) == 0 &&
-		len(kms.DCGMDevices) == 0 &&
+		len(kms.Devices) == 0 &&
 		len(kms.Pods) == 0 &&
 		len(kms.PersistentVolumeClaims) == 0 &&
 		len(kms.ResourceQuotas) == 0 &&

Разница между файлами не показана из-за своего большого размера
+ 358 - 447
core/pkg/model/kubemodel/kubemodel_codecs.go


+ 1 - 2
core/pkg/model/kubemodel/kubemodel_helpers_test.go

@@ -59,6 +59,5 @@ func KubeModelSetEquals(t *testing.T, this, that *KubeModelSet) {
 	require.Equal(t, this.PersistentVolumeClaims, that.PersistentVolumeClaims)
 	require.Equal(t, this.Services, that.Services)
 	require.Equal(t, this.PersistentVolumes, that.PersistentVolumes)
-	// DCGM is ignored by bingen
-	// require.Equal(t, this.DCGMDevices, that.DCGMDevices)
+	require.Equal(t, this.Devices, that.Devices)
 }

+ 9 - 11
core/pkg/model/kubemodel/mock.go

@@ -236,21 +236,19 @@ func NewMockKubeModelSet(start, end time.Time) *KubeModelSet {
 		End:             end,
 	})
 
-	// --- DCGMDevice ---
-	kms.RegisterDCGMDevice(&DCGMDevice{
+	// --- Device ---
+	kms.RegisterDevice(&Device{
 		UUID:      "GPU-abc123def-456-789",
 		Device:    "0",
 		ModelName: "Tesla T4",
-		PodUsages: map[string]DCGMPod{
-			"pod-uid": {
-				ContainerUsages: map[string]DCGMContainer{
-					"app": {UsageAvg: 0.65, UsageMax: 0.92},
-				},
-			},
-		},
-		Start: start,
-		End:   end,
+		Start:     start,
+		End:       end,
 	})
+	if c, ok := kms.Containers["pod-uid/app"]; ok {
+		c.DeviceUsages = map[string]DeviceUsage{
+			"GPU-abc123def-456-789": {UsageAvg: 0.65, UsageMax: 0.92},
+		}
+	}
 
 	// --- Diagnostics ---
 	kms.Error(errMock("mock error"))

Некоторые файлы не были показаны из-за большого количества измененных файлов