Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/dra-example-kubeletplugin/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ func (d *driver) PrepareResourceClaims(ctx context.Context, claims []*resourceap
return result, nil
}

func (d *driver) prepareResourceClaim(_ context.Context, claim *resourceapi.ResourceClaim) kubeletplugin.PrepareResult {
preparedPBs, err := d.state.Prepare(claim)
func (d *driver) prepareResourceClaim(ctx context.Context, claim *resourceapi.ResourceClaim) kubeletplugin.PrepareResult {
preparedPBs, err := d.state.Prepare(ctx, claim)
if err != nil {
return kubeletplugin.PrepareResult{
Err: fmt.Errorf("error preparing devices for claim %v: %w", claim.UID, err),
Expand Down
74 changes: 71 additions & 3 deletions cmd/dra-example-kubeletplugin/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@
package main

import (
"context"
"encoding/json"
"fmt"
"slices"
"sync"

resourceapi "k8s.io/api/resource/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
resourceapply "k8s.io/client-go/applyconfigurations/resource/v1"
"k8s.io/klog/v2"
drapbv1 "k8s.io/kubelet/pkg/apis/dra/v1beta1"
"k8s.io/kubernetes/pkg/kubelet/checkpointmanager"

Expand Down Expand Up @@ -61,6 +66,7 @@ type DeviceState struct {
cdi *CDIHandler
allocatable AllocatableDevices
checkpointManager checkpointmanager.CheckpointManager
config *Config
}

func NewDeviceState(config *Config) (*DeviceState, error) {
Expand Down Expand Up @@ -88,6 +94,7 @@ func NewDeviceState(config *Config) (*DeviceState, error) {
cdi: cdi,
allocatable: allocatable,
checkpointManager: checkpointManager,
config: config,
}

checkpoints, err := state.checkpointManager.ListCheckpoints()
Expand All @@ -109,7 +116,7 @@ func NewDeviceState(config *Config) (*DeviceState, error) {
return state, nil
}

func (s *DeviceState) Prepare(claim *resourceapi.ResourceClaim) ([]*drapbv1.Device, error) {
func (s *DeviceState) Prepare(ctx context.Context, claim *resourceapi.ResourceClaim) ([]*drapbv1.Device, error) {
s.Lock()
defer s.Unlock()

Expand All @@ -125,7 +132,7 @@ func (s *DeviceState) Prepare(claim *resourceapi.ResourceClaim) ([]*drapbv1.Devi
return preparedClaims[claimUID].GetDevices(), nil
}

preparedDevices, err := s.prepareDevices(claim)
preparedDevices, err := s.prepareDevices(ctx, claim)
if err != nil {
return nil, fmt.Errorf("prepare failed: %v", err)
}
Expand Down Expand Up @@ -173,7 +180,7 @@ func (s *DeviceState) Unprepare(claimUID string) error {
return nil
}

func (s *DeviceState) prepareDevices(claim *resourceapi.ResourceClaim) (PreparedDevices, error) {
func (s *DeviceState) prepareDevices(ctx context.Context, claim *resourceapi.ResourceClaim) (PreparedDevices, error) {
if claim.Status.Allocation == nil {
return nil, fmt.Errorf("claim not yet allocated")
}
Expand All @@ -196,13 +203,20 @@ func (s *DeviceState) prepareDevices(claim *resourceapi.ResourceClaim) (Prepared
Config: configapi.DefaultGpuConfig(),
})

// build device status
var devicesStatus []*resourceapply.AllocatedDeviceStatusApplyConfiguration

// Look through the configs and figure out which one will be applied to
// each device allocation result based on their order of precedence.
configResultsMap := make(map[runtime.Object][]*resourceapi.DeviceRequestAllocationResult)
for _, result := range claim.Status.Allocation.Devices.Results {
if _, exists := s.allocatable[result.Device]; !exists {
return nil, fmt.Errorf("requested GPU is not allocatable: %v", result.Device)
}

deviceStatus := s.buildDeviceStatus(result)
devicesStatus = append(devicesStatus, deviceStatus)

for _, c := range slices.Backward(configs) {
if len(c.Requests) == 0 || slices.Contains(c.Requests, result.Request) {
configResultsMap[c.Config] = append(configResultsMap[c.Config], &result)
Expand All @@ -211,6 +225,11 @@ func (s *DeviceState) prepareDevices(claim *resourceapi.ResourceClaim) (Prepared
}
}

klog.Infof("Adding device attribute to claim %s/%s", claim.Namespace, claim.Name)
if err := s.applyDeviceStatus(ctx, claim.Namespace, claim.Name, devicesStatus...); err != nil {
klog.Warningf("Failed to update device attributes for claim %s/%s: %v", claim.Namespace, claim.Name, err)
}

// Normalize, validate, and apply all configs associated with devices that
// need to be prepared. Track container edits generated from applying the
// config to the set of device allocation results.
Expand Down Expand Up @@ -380,3 +399,52 @@ func GetOpaqueDeviceConfigs(

return resultConfigs, nil
}

func (s *DeviceState) buildDeviceStatus(res resourceapi.DeviceRequestAllocationResult) *resourceapply.AllocatedDeviceStatusApplyConfiguration {
dn := res.Device
deviceInfo := make(map[string]resourceapi.DeviceAttribute)

if d, ok := s.allocatable[dn]; ok {
if uuid, ok := d.Attributes["uuid"]; ok {
deviceInfo["uuid"] = uuid
}
if model, ok := d.Attributes["model"]; ok {
deviceInfo["model"] = model
}
if driverVersion, ok := d.Attributes["driverVersion"]; ok {
deviceInfo["driverVersion"] = driverVersion
}
}

jsonBytes, err := json.Marshal(deviceInfo)
if err != nil {
klog.Errorf("Failed to marshal device data: %v", err)
jsonBytes = []byte("{}")
}
data := runtime.RawExtension{
Raw: jsonBytes,
}

return resourceapply.AllocatedDeviceStatus().
WithDevice(dn).
WithDriver(res.Driver).
WithPool(res.Pool).
// WithData records per-allocation metadata used for monitoring and debugging:
// - Pod→GPU mapping: makes it easier to see which GPU a given pod is using,
// which is not readily available elsewhere.
// - Device attributes (e.g. UUID, model, driverVersion): remain available
// even if the device is later removed from a ResourceSlice (for example,
// because it becomes unhealthy), so past allocations can still be
// correlated with later health or scheduling issues.
WithData(data)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my own understanding, who or what generally consumes this data?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, this seems useful for monitoring and debugging when admin want to know pod to gpu mapping, which GPU is being used by a particular pod. right now this info is not readily available.

}

func (s *DeviceState) applyDeviceStatus(ctx context.Context, ns, name string, devices ...*resourceapply.AllocatedDeviceStatusApplyConfiguration) error {
claim := resourceapply.ResourceClaim(name, ns).
WithStatus(resourceapply.ResourceClaimStatus().WithDevices(devices...))

opts := metav1.ApplyOptions{FieldManager: consts.DriverName, Force: true}

_, err := s.config.coreclient.ResourceV1().ResourceClaims(ns).ApplyStatus(ctx, claim, opts)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there some lightweight verification we can add to the e2e tests? At least something to verify that something like the condition or one of the attributes is set on even one of the examples would make sure this doesn't totally break later.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure.

return err
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ rules:
- apiGroups: ["resource.k8s.io"]
resources: ["resourceclaims"]
verbs: ["get"]
- apiGroups: ["resource.k8s.io"]
resources: ["resourceclaims/status"]
verbs: ["patch", "update"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get"]
Expand Down
48 changes: 47 additions & 1 deletion test/e2e/e2e.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
!/usr/bin/env bash

# Copyright 2024 The Kubernetes Authors.
#
Expand Down Expand Up @@ -84,6 +84,49 @@ function gpu-partition-count-from-logs {
echo "$logs" | sed -nE "s/^declare -x GPU_DEVICE_${id}_PARTITION_COUNT=\"(.+)\"$/\1/p"
}

function verify-resourceclaim-device-status() {
local ns="$1"
echo "=== Verifying ResourceClaim device data in namespace ${ns} ==="

local claim=""
for i in {1..30}; do
claim="$(kubectl get resourceclaim -n "${ns}" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)"
if [[ -n "${claim}" ]]; then
break
fi
sleep 1
done

if [[ -z "${claim}" ]]; then
echo "ERROR: no ResourceClaim found in namespace ${ns}"
exit 1
fi

echo "Found ResourceClaim ${ns}/${claim}, checking status.devices[0].data ..."

local uuid
uuid="$(kubectl get resourceclaim "${claim}" -n "${ns}" \
-o jsonpath='{.status.devices[0].data.uuid.string}')"

local driver_version
driver_version="$(kubectl get resourceclaim "${claim}" -n "${ns}" \
-o jsonpath='{.status.devices[0].data.driverVersion.version}')"

if [[ -z "${uuid}" ]]; then
echo "ERROR: ResourceClaim ${ns}/${claim} is missing .status.devices[0].data.uuid.string"
kubectl get resourceclaim "${claim}" -n "${ns}" -o yaml
exit 1
fi

if [[ -z "${driver_version}" ]]; then
echo "ERROR: ResourceClaim ${ns}/${claim} is missing .status.devices[0].data.driverVersion.version"
kubectl get resourceclaim "${claim}" -n "${ns}" -o yaml
exit 1
fi

echo "OK: ResourceClaim ${ns}/${claim} has device data (uuid=${uuid}, driverVersion=${driver_version})"
}

declare -a observed_gpus
function gpu-already-seen {
local gpu="$1"
Expand All @@ -101,6 +144,9 @@ if [ $gpu_test_1 != 2 ]; then
exit 1
fi

# Verify that at least one ResourceClaim in gpu-test1 has device data
verify-resourceclaim-device-status "gpu-test1"

gpu_test1_pod0_ctr0_logs=$(kubectl logs -n gpu-test1 pod0 -c ctr0)
gpu_test1_pod0_ctr0_gpus=$(gpus-from-logs "$gpu_test1_pod0_ctr0_logs")
gpu_test1_pod0_ctr0_gpus_count=$(echo "$gpu_test1_pod0_ctr0_gpus" | wc -w | tr -d ' ')
Expand Down