Skip to content

Commit 5ba79f3

Browse files
committed
Fix DRA kind e2e setup and queue resource accounting
Signed-off-by: xuwentao <cutenear1993@yahoo.com>
1 parent 6275b05 commit 5ba79f3

24 files changed

Lines changed: 1605 additions & 529 deletions

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ e2e-test-cronjob: images
224224
E2E_TYPE=CRONJOB ./hack/run-e2e-kind.sh
225225

226226
e2e-test-dra: images
227-
E2E_TYPE=DRA FEATURE_GATES="DynamicResourceAllocation=true" ./hack/run-e2e-kind.sh
227+
E2E_TYPE=DRA FEATURE_GATES="DynamicResourceAllocation=true,DRAConsumableCapacity=true" ./hack/run-e2e-kind.sh
228228

229229
e2e-test-hypernode: images
230230
E2E_TYPE=HYPERNODE ./hack/run-e2e-kind.sh

docs/design/capacity-dra-support.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ In practice:
151151

152152
These two responsibilities are complementary rather than overlapping.
153153

154+
For consumable capacity, Volcano accounts the logical total. A request for two devices with `8Gi` of memory each consumes `16Gi` of the queue's `memory.deviceclass/<DeviceClass>` quota. This does not solve physical fragmentation: if the cluster has two `8Gi` devices, a single Pod that needs `10Gi` on one device can still pass queue quota while failing the final DRA allocation.
155+
154156
## Root Queue and Total Resource View
155157

156158
For DRA resources, the cluster-wide total can come from actual cluster state or from an administrator’s explicit queue configuration.

docs/user-guide/how_to_use_dra_quota.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,14 @@ data:
5353
- name: drf
5454
- name: predicates
5555
- name: capacity
56+
arguments:
57+
capacity.DynamicResourceAllocationEnable: true
58+
capacity.DRAConsumableCapacityEnable: true
5659
- name: nodeorder
5760
```
5861
62+
The capacity plugin arguments are optional. When they are not set, Volcano uses the corresponding Kubernetes feature gate values as defaults.
63+
5964
## Queue Key Formats
6065
6166
Use the following keys in `spec.capability`, `spec.deserved`, and `spec.guarantee`.
@@ -67,6 +72,8 @@ Use the following keys in `spec.capability`, `spec.deserved`, and `spec.guarante
6772

6873
These keys can coexist with normal resources such as `cpu`, `memory`, and `nvidia.com/gpu`.
6974

75+
Consumable-capacity requests are counted per allocated device. For example, a ResourceClaim requesting `count: 2` and `memory: 8Gi` consumes `16Gi` of the queue key `memory.deviceclass/<DeviceClass>`.
76+
7077
## Queue Configuration Examples
7178

7279
### Whole-Card GPU Queue
@@ -303,6 +310,8 @@ The following request styles are not the main quota-accounting target:
303310

304311
If your environment relies heavily on those modes, evaluate behavior carefully before treating them as strict queue quota signals.
305312

313+
Queue quota is a logical total-resource check. It does not guarantee that the requested capacity can be placed on a specific physical device layout. For example, a queue may have `16Gi` of total GPU memory quota, while one Pod requesting `10Gi` still cannot fit onto any single `8Gi` device. Kubernetes DRA and the installed driver make the final physical allocation decision.
314+
306315
## Best Practices
307316

308317
1. Use `ResourceClaimTemplate` for per-Pod allocation lifecycle

hack/e2e-kind-config.yaml

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,49 @@ nodes:
1818
apiVersion: kubelet.config.k8s.io/v1beta1
1919
kind: KubeletConfiguration
2020
containerLogMaxSize: "50Mi"
21+
featureGates:
22+
DynamicResourceAllocation: true
23+
DRAConsumableCapacity: true
2124
- |
2225
kind: ClusterConfiguration
2326
apiServer:
2427
extraArgs:
2528
runtime-config: "admissionregistration.k8s.io/v1beta1"
2629
enable-admission-plugins: "MutatingAdmissionPolicy"
30+
feature-gates: "MutatingAdmissionPolicy=true,DynamicResourceAllocation=true,DRAConsumableCapacity=true"
31+
controllerManager:
32+
extraArgs:
33+
feature-gates: "MutatingAdmissionPolicy=true,DynamicResourceAllocation=true,DRAConsumableCapacity=true"
2734
# the four workers
2835
- role: worker
36+
kubeadmConfigPatches:
37+
- |
38+
apiVersion: kubelet.config.k8s.io/v1beta1
39+
kind: KubeletConfiguration
40+
featureGates:
41+
DynamicResourceAllocation: true
42+
DRAConsumableCapacity: true
43+
- role: worker
44+
kubeadmConfigPatches:
45+
- |
46+
apiVersion: kubelet.config.k8s.io/v1beta1
47+
kind: KubeletConfiguration
48+
featureGates:
49+
DynamicResourceAllocation: true
50+
DRAConsumableCapacity: true
2951
- role: worker
52+
kubeadmConfigPatches:
53+
- |
54+
apiVersion: kubelet.config.k8s.io/v1beta1
55+
kind: KubeletConfiguration
56+
featureGates:
57+
DynamicResourceAllocation: true
58+
DRAConsumableCapacity: true
3059
- role: worker
31-
- role: worker
60+
kubeadmConfigPatches:
61+
- |
62+
apiVersion: kubelet.config.k8s.io/v1beta1
63+
kind: KubeletConfiguration
64+
featureGates:
65+
DynamicResourceAllocation: true
66+
DRAConsumableCapacity: true

hack/lib/install.sh

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ function kind-up-cluster {
1919
check-kind
2020

2121
echo "Running kind: [kind create cluster ${CLUSTER_CONTEXT[*]} ${KIND_OPT}]"
22-
kind create cluster "${CLUSTER_CONTEXT[@]}" ${KIND_OPT}
22+
kind create cluster "${CLUSTER_CONTEXT[@]}" ${KIND_OPT} || exit 1
2323

2424
echo
2525
check-images
@@ -33,6 +33,32 @@ function kind-up-cluster {
3333
if [[ "${E2E_TYPE}" == "AGENTSCHEDULER" ]]; then
3434
kind load docker-image ${IMAGE_PREFIX}/vc-agent-scheduler:${TAG} "${CLUSTER_CONTEXT[@]}" --nodes ${CLUSTER_CONTEXT[1]}-control-plane
3535
fi
36+
if [[ "${E2E_TYPE}" == "DRA" || "${E2E_TYPE}" == "ALL" ]]; then
37+
ensure-dra-test-images
38+
fi
39+
}
40+
41+
function ensure-dra-test-images {
42+
local dra_images=(
43+
"nginx:1.29.3-alpine"
44+
"registry.k8s.io/kwok/kwok:v0.7.0"
45+
"registry.k8s.io/sig-storage/hostpathplugin:v1.16.1"
46+
)
47+
48+
echo
49+
echo "Ensuring DRA test images are available locally"
50+
for image in "${dra_images[@]}"; do
51+
if ! docker image inspect "${image}" >/dev/null 2>&1; then
52+
echo "Pulling image ${image} ..."
53+
docker pull "${image}" >/dev/null || exit 1
54+
fi
55+
done
56+
57+
echo
58+
echo "Loading DRA test images into kind cluster"
59+
for image in "${dra_images[@]}"; do
60+
kind load docker-image "${image}" "${CLUSTER_CONTEXT[@]}" || exit 1
61+
done
3662
}
3763

3864
# check if the required images exist
@@ -81,6 +107,17 @@ function check-kind {
81107
if [[ $? -ne 0 ]]; then
82108
echo "Installing kind ..."
83109
GOOS=${OS} go install sigs.k8s.io/kind@v0.31.0
110+
local bin_path
111+
bin_path=$(go env GOBIN)
112+
if [[ -z "${bin_path}" ]]; then
113+
bin_path="$(go env GOPATH)/bin"
114+
fi
115+
export PATH="${bin_path}:${PATH}"
116+
if ! command -v kind >/dev/null 2>&1; then
117+
echo -e "\033[31mERROR\033[0m: kind installation completed but the binary is still not available on PATH"
118+
exit 1
119+
fi
120+
echo -n "Using kind, version: " && kind version
84121
else
85122
echo -n "Found kind, version: " && kind version
86123
fi

hack/run-e2e-kind.sh

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export LOG_LEVEL=3
2424
export CLEANUP_CLUSTER=${CLEANUP_CLUSTER:-1}
2525
export E2E_TYPE=${E2E_TYPE:-"ALL"}
2626
export ARTIFACTS_PATH=${ARTIFACTS_PATH:-"${VK_ROOT}/volcano-e2e-logs"}
27+
DRA_GINKGO_FOCUS=${DRA_GINKGO_FOCUS:-"DRA (Quota )?E2E Test"}
2728
mkdir -p "$ARTIFACTS_PATH"
2829

2930
NAMESPACE=${NAMESPACE:-volcano-system}
@@ -379,7 +380,7 @@ case ${E2E_TYPE} in
379380
KUBECONFIG=${KUBECONFIG} GOOS=${OS} ginkgo -r --skip="\[sig-.*\]" --slow-spec-threshold='30s' --progress ./test/e2e/schedulingaction/
380381
KUBECONFIG=${KUBECONFIG} GOOS=${OS} ginkgo -r --slow-spec-threshold='30s' --progress ./test/e2e/vcctl/
381382
KUBECONFIG=${KUBECONFIG} GOOS=${OS} ginkgo -r --slow-spec-threshold='30s' --progress ./test/e2e/cronjob/
382-
KUBECONFIG=${KUBECONFIG} GOOS=${OS} ginkgo -r --slow-spec-threshold='30s' --progress --focus="DRA E2E Test" ./test/e2e/dra/
383+
KUBECONFIG=${KUBECONFIG} GOOS=${OS} ginkgo -r --slow-spec-threshold='30s' --progress --focus="${DRA_GINKGO_FOCUS}" ./test/e2e/dra/
383384
KUBECONFIG=${KUBECONFIG} GOOS=${OS} ginkgo -r --slow-spec-threshold='30s' --progress ./test/e2e/admission/
384385
KUBECONFIG=${KUBECONFIG} GOOS=${OS} ginkgo -r --slow-spec-threshold='30s' --progress ./test/e2e/hypernode/
385386
;;
@@ -414,7 +415,7 @@ case ${E2E_TYPE} in
414415
;;
415416
"DRA")
416417
echo "Running dra e2e suite..."
417-
KUBECONFIG=${KUBECONFIG} GOOS=${OS} ginkgo -v -r --slow-spec-threshold='30s' --progress --focus="DRA E2E Test" ./test/e2e/dra/
418+
KUBECONFIG=${KUBECONFIG} GOOS=${OS} ginkgo -v -r --slow-spec-threshold='30s' --progress --focus="${DRA_GINKGO_FOCUS}" ./test/e2e/dra/
418419
;;
419420
"ADMISSION_POLICY")
420421
echo "Running admission policy e2e suite..."

pkg/scheduler/api/job_info.go

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,40 +1446,19 @@ func (ji *JobInfo) GetMinDRAResources() map[string]*DRAResource {
14461446
}
14471447

14481448
result := make(map[string]*DRAResource)
1449-
processedRoles := make(map[string]struct{})
1450-
1451-
// Since DRA requests can vary per task/pod, we aggregate them based on TaskMinAvailable
1452-
for _, task := range ji.Tasks {
1453-
if task.DRAResreq == nil {
1454-
continue
1455-
}
1456-
1457-
// Calculate how many times this task type needs to run
1458-
taskType := task.TaskRole
1459-
minNum, ok := ji.TaskMinAvailable[taskType]
1460-
if !ok || minNum <= 0 {
1461-
continue
1462-
}
1463-
if _, seen := processedRoles[taskType]; seen {
1464-
continue
1465-
}
1466-
processedRoles[taskType] = struct{}{}
1467-
1468-
for deviceClass, res := range task.DRAResreq {
1449+
addResource := func(res map[string]*DRAResource, times int32) {
1450+
for deviceClass, request := range res {
14691451
if _, exists := result[deviceClass]; !exists {
14701452
result[deviceClass] = &DRAResource{
14711453
Count: 0,
14721454
Capacity: make(map[string]resource.Quantity),
14731455
}
14741456
}
14751457

1476-
result[deviceClass].Count += res.Count * int64(minNum)
1477-
for dim, cap := range res.Capacity {
1458+
result[deviceClass].Count += request.Count * int64(times)
1459+
for dim, cap := range request.Capacity {
14781460
totalCap := cap.DeepCopy()
1479-
// resource.Quantity has no Multiply func, so we parse memory/cpu as MilliValues
1480-
// For exact values we can just use set
1481-
// Since Quantity can represent fractional, we will loop to add
1482-
for i := int32(0); i < minNum-1; i++ {
1461+
for i := int32(0); i < times-1; i++ {
14831462
totalCap.Add(cap)
14841463
}
14851464

@@ -1493,6 +1472,62 @@ func (ji *JobInfo) GetMinDRAResources() map[string]*DRAResource {
14931472
}
14941473
}
14951474

1475+
if len(ji.TaskMinAvailable) == 0 {
1476+
minAvailable := ji.MinAvailable
1477+
if minAvailable <= 0 {
1478+
return nil
1479+
}
1480+
1481+
tasks := make([]*TaskInfo, 0, len(ji.Tasks))
1482+
for _, task := range ji.Tasks {
1483+
if task.DRAResreq != nil {
1484+
tasks = append(tasks, task)
1485+
}
1486+
}
1487+
sort.Slice(tasks, func(i, j int) bool {
1488+
if tasks[i].Namespace != tasks[j].Namespace {
1489+
return tasks[i].Namespace < tasks[j].Namespace
1490+
}
1491+
if tasks[i].Name != tasks[j].Name {
1492+
return tasks[i].Name < tasks[j].Name
1493+
}
1494+
return tasks[i].UID < tasks[j].UID
1495+
})
1496+
1497+
for i, task := range tasks {
1498+
if int32(i) >= minAvailable {
1499+
break
1500+
}
1501+
addResource(task.DRAResreq, 1)
1502+
}
1503+
1504+
if len(result) == 0 {
1505+
return nil
1506+
}
1507+
return result
1508+
}
1509+
1510+
// Since DRA requests can vary per task/pod, we aggregate them based on TaskMinAvailable
1511+
processedRoles := make(map[string]struct{})
1512+
for _, task := range ji.Tasks {
1513+
if task.DRAResreq == nil {
1514+
continue
1515+
}
1516+
1517+
// Calculate how many times this task type needs to run
1518+
taskType := task.TaskRole
1519+
minNum, ok := ji.TaskMinAvailable[taskType]
1520+
if !ok || minNum <= 0 {
1521+
continue
1522+
}
1523+
if _, seen := processedRoles[taskType]; seen {
1524+
continue
1525+
}
1526+
processedRoles[taskType] = struct{}{}
1527+
1528+
addResource(task.DRAResreq, minNum)
1529+
}
1530+
14961531
if len(result) == 0 {
14971532
return nil
14981533
}

pkg/scheduler/api/job_info_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,3 +718,70 @@ func TestParseMinMemberInfoChanged(t *testing.T) {
718718
})
719719
}
720720
}
721+
722+
func TestGetMinDRAResourcesIdempotent(t *testing.T) {
723+
job := NewJobInfo("test-job")
724+
job.TaskMinAvailable["worker"] = 2
725+
job.Tasks[TaskID("worker-0")] = &TaskInfo{
726+
UID: TaskID("worker-0"),
727+
Name: "worker-0",
728+
TaskRole: "worker",
729+
DRAResreq: map[string]*DRAResource{
730+
"gpu.com": {
731+
Count: 1,
732+
Capacity: map[string]resource.Quantity{
733+
"memory": resource.MustParse("8Gi"),
734+
},
735+
},
736+
},
737+
}
738+
739+
expected := map[string]*DRAResource{
740+
"gpu.com": {
741+
Count: 2,
742+
Capacity: map[string]resource.Quantity{
743+
"memory": resource.MustParse("16Gi"),
744+
},
745+
},
746+
}
747+
748+
assert.Equal(t, expected, job.GetMinDRAResources())
749+
assert.Equal(t, expected, job.GetMinDRAResources())
750+
assert.Equal(t, map[string]int32{"worker": 2}, job.TaskMinAvailable)
751+
}
752+
753+
func TestGetMinDRAResourcesFallbackWithoutTaskMinAvailable(t *testing.T) {
754+
job := NewJobInfo("test-job")
755+
job.MinAvailable = 2
756+
job.Tasks[TaskID("task-b")] = &TaskInfo{
757+
UID: TaskID("task-b"),
758+
Namespace: "ns",
759+
Name: "task-b",
760+
DRAResreq: map[string]*DRAResource{
761+
"gpu.com": {Count: 1},
762+
},
763+
}
764+
job.Tasks[TaskID("task-a")] = &TaskInfo{
765+
UID: TaskID("task-a"),
766+
Namespace: "ns",
767+
Name: "task-a",
768+
DRAResreq: map[string]*DRAResource{
769+
"gpu.com": {Count: 2},
770+
},
771+
}
772+
job.Tasks[TaskID("task-c")] = &TaskInfo{
773+
UID: TaskID("task-c"),
774+
Namespace: "ns",
775+
Name: "task-c",
776+
DRAResreq: map[string]*DRAResource{
777+
"gpu.com": {Count: 4},
778+
},
779+
}
780+
781+
assert.Equal(t, map[string]*DRAResource{
782+
"gpu.com": {
783+
Count: 3,
784+
Capacity: map[string]resource.Quantity{},
785+
},
786+
}, job.GetMinDRAResources())
787+
}

0 commit comments

Comments
 (0)