@@ -5,17 +5,23 @@ SPDX-License-Identifier: Apache-2.0
55package hamicore
66
77import (
8+ "bytes"
89 "context"
910 "encoding/json"
1011 "fmt"
1112 "strconv"
1213 "strings"
14+ "time"
1315
1416 . "github.com/onsi/ginkgo/v2"
1517 . "github.com/onsi/gomega"
18+ dto "github.com/prometheus/client_model/go"
19+ "github.com/prometheus/common/expfmt"
20+ "github.com/prometheus/common/model"
1621 v1 "k8s.io/api/core/v1"
1722 "k8s.io/apimachinery/pkg/api/resource"
1823 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+ "k8s.io/client-go/kubernetes"
1925 "k8s.io/utils/ptr"
2026
2127 kaiv1binder "github.com/kai-scheduler/KAI-scheduler/pkg/apis/kai/v1/binder"
@@ -32,13 +38,51 @@ import (
3238
3339const (
3440 kaiResourceIsolatorWebhookName = "kai-resource-isolator-mutating"
41+ kaiResourceIsolatorNamespace = "kai-resource-isolator"
42+ kaiVGPUMonitorDaemonSetName = "kai-resource-isolator-monitor"
43+ kaiVGPUMonitorLabel = "app.kubernetes.io/component=kai-vgpu-monitor"
44+ kaiVGPUMonitorMetricsPort = "9394"
3545 binderDeploymentName = "binder"
3646 binderDeploymentNamespace = "kai-scheduler"
3747 binderPluginsFlag = "--plugins"
3848 cudaImage = "nvidia/cuda:12.6.0-base-ubuntu22.04"
39- gpuMemoryRequestMiB = 2000
49+ // devel image is needed so the metrics workload can nvcc a tiny cudaMalloc hold binary.
50+ cudaDevelImage = "nvidia/cuda:12.6.0-devel-ubuntu22.04"
51+ gpuMemoryRequestMiB = 2000
52+ cudaAllocHoldMiB = 64
53+ vgpuMetricsTimeout = 60 * time .Second
54+ vgpuMetricsInterval = 5 * time .Second
55+ hamiVGPUMemoryUsed = "hami_vgpu_memory_used_bytes"
4056)
4157
58+ // Compiles and runs a small CUDA alloc that holds memory so kai-vgpu-monitor can
59+ // scrape hami_vgpu_memory_used_bytes > 0. Kept as a container entrypoint (not
60+ // ExecInPod) so the allocation outlives the ready wait.
61+ const cudaAllocHoldScript = `set -euo pipefail
62+ cat >/tmp/hold.cu <<'EOF'
63+ #include <cuda_runtime.h>
64+ #include <stdio.h>
65+ #include <unistd.h>
66+ int main(void) {
67+ void *p = NULL;
68+ size_t bytes = (size_t)64 * 1024 * 1024;
69+ cudaError_t err = cudaMalloc(&p, bytes);
70+ if (err != cudaSuccess) {
71+ fprintf(stderr, "cudaMalloc failed: %s\n", cudaGetErrorString(err));
72+ return 1;
73+ }
74+ printf("allocated %zu bytes at %p\n", bytes, p);
75+ fflush(stdout);
76+ for (;;) {
77+ sleep(3600);
78+ }
79+ return 0;
80+ }
81+ EOF
82+ nvcc -O0 -o /tmp/hold /tmp/hold.cu
83+ exec /tmp/hold
84+ `
85+
4286var _ = Describe ("HAMi-core resource isolation" , Ordered , func () {
4387 var testCtx * testcontext.TestContext
4488
@@ -191,6 +235,45 @@ var _ = Describe("HAMi-core resource isolation", Ordered, func() {
191235 "nvidia-smi visible memory (%d MiB) should match CUDA_DEVICE_MEMORY_LIMIT (%d MiB)" ,
192236 visibleMemMiB , limitMiB )
193237 })
238+
239+ It ("gpu-memory: kai-vgpu-monitor reports hami_vgpu_memory_used_bytes > 0" ,
240+ Label (labels .ReservationPod ), func (ctx context.Context ) {
241+ if ! isKaiVGPUMonitorInstalled (ctx , testCtx .KubeClientset ) {
242+ Skip (fmt .Sprintf (
243+ "kai-vgpu-monitor DaemonSet %q not found in namespace %q; " +
244+ "install via hack/third_party_integrations/deploy_isolator.sh" ,
245+ kaiVGPUMonitorDaemonSetName , kaiResourceIsolatorNamespace ,
246+ ))
247+ }
248+
249+ pod := rd .CreatePodObject (testCtx .Queues [0 ], v1.ResourceRequirements {})
250+ pod .Annotations [constants .GpuMemory ] = strconv .Itoa (gpuMemoryRequestMiB )
251+ pod .Spec .Containers [0 ].Image = cudaDevelImage
252+ pod .Spec .Containers [0 ].Command = []string {"bash" , "-c" }
253+ pod .Spec .Containers [0 ].Args = []string {cudaAllocHoldScript }
254+
255+ _ , err := rd .CreatePod (ctx , testCtx .KubeClientset , pod )
256+ Expect (err ).NotTo (HaveOccurred ())
257+ wait .ForPodReady (ctx , testCtx .ControllerClient , pod )
258+
259+ pod , err = rd .GetPod (ctx , testCtx .KubeClientset , pod .Namespace , pod .Name )
260+ Expect (err ).NotTo (HaveOccurred ())
261+ Expect (pod .Spec .NodeName ).NotTo (BeEmpty (), "pod should be scheduled to a node" )
262+ containerName := pod .Spec .Containers [0 ].Name
263+
264+ By ("waiting for kai-vgpu-monitor to expose hami_vgpu_memory_used_bytes for the workload" )
265+ Eventually (func (g Gomega ) {
266+ used , scrapeErr := scrapeHamiVGPUMemoryUsedBytes (
267+ ctx , testCtx .KubeClientset , pod .Spec .NodeName , pod .Namespace , pod .Name , containerName ,
268+ )
269+ g .Expect (scrapeErr ).NotTo (HaveOccurred ())
270+ GinkgoLogr .Info ("hami_vgpu_memory_used_bytes" ,
271+ "namespace" , pod .Namespace , "pod" , pod .Name , "container" , containerName , "bytes" , used )
272+ g .Expect (used ).To (BeNumerically (">" , 0 ),
273+ "expected hami_vgpu_memory_used_bytes > 0 for %s/%s container %s (held ~%d MiB)" ,
274+ pod .Namespace , pod .Name , containerName , cudaAllocHoldMiB )
275+ }, vgpuMetricsTimeout , vgpuMetricsInterval ).Should (Succeed ())
276+ })
194277})
195278
196279func printNvidiaSmi (ctx context.Context , testCtx * testcontext.TestContext , pod * v1.Pod ) {
@@ -262,3 +345,88 @@ func hamiCoreEnabledInPluginsJSON(raw string) bool {
262345 }
263346 return ptr .Deref (cfg .Enabled , false )
264347}
348+
349+ func isKaiVGPUMonitorInstalled (ctx context.Context , client kubernetes.Interface ) bool {
350+ _ , err := client .AppsV1 ().DaemonSets (kaiResourceIsolatorNamespace ).
351+ Get (ctx , kaiVGPUMonitorDaemonSetName , metav1.GetOptions {})
352+ return err == nil
353+ }
354+
355+ func scrapeHamiVGPUMemoryUsedBytes (
356+ ctx context.Context ,
357+ client kubernetes.Interface ,
358+ nodeName , namespace , podName , containerName string ,
359+ ) (float64 , error ) {
360+ monitorPod , err := findMonitorPodOnNode (ctx , client , nodeName )
361+ if err != nil {
362+ return 0 , err
363+ }
364+
365+ raw , err := client .CoreV1 ().Pods (monitorPod .Namespace ).
366+ ProxyGet ("http" , monitorPod .Name , kaiVGPUMonitorMetricsPort , "/metrics" , nil ).
367+ DoRaw (ctx )
368+ if err != nil {
369+ return 0 , fmt .Errorf ("proxy GET /metrics from monitor pod %s/%s: %w" ,
370+ monitorPod .Namespace , monitorPod .Name , err )
371+ }
372+
373+ parser := expfmt .NewTextParser (model .UTF8Validation )
374+ families , err := parser .TextToMetricFamilies (bytes .NewReader (raw ))
375+ if err != nil {
376+ return 0 , fmt .Errorf ("parse prometheus metrics: %w" , err )
377+ }
378+
379+ family , ok := families [hamiVGPUMemoryUsed ]
380+ if ! ok {
381+ return 0 , fmt .Errorf ("metric %s not present in monitor scrape" , hamiVGPUMemoryUsed )
382+ }
383+
384+ want := map [string ]string {
385+ "namespace" : namespace ,
386+ "pod" : podName ,
387+ "container" : containerName ,
388+ }
389+ for _ , metric := range family .GetMetric () {
390+ if ! metricHasLabels (metric , want ) {
391+ continue
392+ }
393+ if metric .GetGauge () == nil {
394+ return 0 , fmt .Errorf ("metric %s for %v is not a gauge" , hamiVGPUMemoryUsed , want )
395+ }
396+ return metric .GetGauge ().GetValue (), nil
397+ }
398+ return 0 , fmt .Errorf ("metric %s with labels %v not found" , hamiVGPUMemoryUsed , want )
399+ }
400+
401+ func findMonitorPodOnNode (ctx context.Context , client kubernetes.Interface , nodeName string ) (* v1.Pod , error ) {
402+ pods , err := client .CoreV1 ().Pods (kaiResourceIsolatorNamespace ).List (ctx , metav1.ListOptions {
403+ LabelSelector : kaiVGPUMonitorLabel ,
404+ FieldSelector : "spec.nodeName=" + nodeName ,
405+ })
406+ if err != nil {
407+ return nil , fmt .Errorf ("list kai-vgpu-monitor pods on node %s: %w" , nodeName , err )
408+ }
409+ for i := range pods .Items {
410+ pod := & pods .Items [i ]
411+ if rd .IsPodReady (pod ) {
412+ return pod , nil
413+ }
414+ }
415+ if len (pods .Items ) == 0 {
416+ return nil , fmt .Errorf ("no kai-vgpu-monitor pods scheduled on node %s" , nodeName )
417+ }
418+ return nil , fmt .Errorf ("kai-vgpu-monitor pod(s) on node %s are not Ready" , nodeName )
419+ }
420+
421+ func metricHasLabels (metric * dto.Metric , want map [string ]string ) bool {
422+ got := make (map [string ]string , len (metric .GetLabel ()))
423+ for _ , label := range metric .GetLabel () {
424+ got [label .GetName ()] = label .GetValue ()
425+ }
426+ for k , v := range want {
427+ if got [k ] != v {
428+ return false
429+ }
430+ }
431+ return true
432+ }
0 commit comments