Hành trình lấy chứng nhận Kubernetes AI Conformance cho Viettel AI Platform

Đi lần lượt từng yêu cầu của bản v1.35: chuẩn đòi hỏi gì, và bọn mình đã chứng minh gì cho Viettel AI Platform.

8 min readkubernetescncfai-conformancegpudra

Read in English.

Nhiều người nghĩ Kubernetes AI Conformance là một bộ test, chạy qua là có chứng nhận. Thực ra với bản v1.35, nó là một file YAML checklist kèm mấy file Markdown evidence do chính mình tự viết, CNCF review rồi đăng công khai trên GitHub cho ai cũng đọc được. Vậy nên thứ đáng đọc là evidence, không phải cái badge.

Tháng 4/2026 team mình đưa Viettel AI Platform qua chương trình này trên Kubernetes v1.35. Mình đã nhắc tới kết quả trong bài recap KCD Vietnam, còn bài này là bản đầy đủ: đi qua từng yêu cầu, chuẩn hỏi gì, bọn mình đưa ra cái gì, và những chỗ phải nối nhiều thành phần mới chạy được.

Kubernetes v1.35.3, containerd 2.2.2 và 1.7.28, NVIDIA GPU Operator với driver 580.126.09, Kueue v0.10.2. Số liệu trong repo kiểm tra ngày 25/10/2026.

Logo Certified Kubernetes AI Platform, lấy từ repo artwork của CNCF

Muốn có badge AI thì phải conformant trước đã

Bạn không bắt đầu từ chương trình AI được, vì platform phải Kubernetes Conformant trước, và checklist AI đòi đúng cái link đó ở phần metadata.

Kết quả conformance gốc của bọn mình vào repo cncf/k8s-conformance ngày 8/4, tức là một tuần sau khi pull request AI đã mở, nên PR AI phải chờ. Lần chạy Ginkgo mất khoảng hai tiếng:

SUCCESS! -- 441 Passed | 0 Failed | 0 Pending | 6914 Skipped
PASS

Ginkgo ran 1 suite in 2h1m0.033057564s

Pull request AI (#85) mở ngày 1/4 và được merge ngày 22/4 theo giờ Hà Nội. Chứng nhận có hiệu lực một năm.

cncf/k8s-ai-conformance#85: Viettel AI Platform: AI Conformance for v1.35, đã merge

Checklist v1.35 có mười hai mục, chín mục MUST và ba mục SHOULD. Mỗi mục có một trường status, một danh sách file evidence và một dòng ghi chú. Evidence của bọn mình là chín file Markdown, toàn là lệnh kubectl kèm output.

Evidence lấy từ hai node bare-metal Ubuntu 22.04 có GPU NVIDIA L40S và L40. Chứng nhận tính theo từng sản phẩm và từng cấu hình, nên đó chính là cấu hình được mô tả.

Tới ngày 25/10/2026, thư mục v1.35/ có 23 platform, còn cả repo có 40 platform khác nhau trải từ v1.33 tới v1.37. Viettel là vendor duy nhất từ Việt Nam trong danh sách đó.

Nhóm Certified Kubernetes - AI Platform trên CNCF Landscape, ngày 25/10/2026

DRA: API v1 có sẵn ngay từ đầu

Yêu cầu: “Support Dynamic Resource Allocation (DRA) APIs to enable more flexible and fine-grained resource requests beyond simple counts.”

Trên v1.35 thì resource.k8s.io/v1 đã GA và bật mặc định, nên cụm kubeadm không cần thêm flag nào cũng serve được:

$ kubectl api-resources --api-group=resource.k8s.io
NAME                     SHORTNAMES   APIVERSION           NAMESPACED   KIND
deviceclasses                         resource.k8s.io/v1   false        DeviceClass
resourceclaims                        resource.k8s.io/v1   true         ResourceClaim
resourceclaimtemplates                resource.k8s.io/v1   true         ResourceClaimTemplate
resourceslices                        resource.k8s.io/v1   false        ResourceSlice

Bọn mình tạo một ResourceClaimTemplate xin một device thuộc class gpu.nvidia.com, cùng một pod tham chiếu tới nó qua spec.resourceClaims:

apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: single-gpu
  namespace: dra-test
spec:
  spec:
    devices:
      requests:
      - name: req-0
        exactly:
          deviceClassName: gpu.nvidia.com
          allocationMode: ExactCount
          count: 1

API server nhận cả hai. Mà để ý block exactly nhé: ở bản v1, một device request hoặc là exactly với đúng một lựa chọn, hoặc là firstAvailable với một danh sách lựa chọn xếp theo thứ tự ưu tiên.

Thêm một chi tiết cho ai đang xem trước các bản sau: checklist v1.36 và v1.37 không còn mục dra_support riêng nữa.

Driver và runtime: phần nhàm chán để GPU Operator lo

Mục này là SHOULD, tức là platform nên có “a verifiable mechanism” để đảm bảo mọi node có accelerator đều có đúng driver và đúng cấu hình container runtime.

GPU Operator cài driver, một DaemonSet toolkit ghi cấu hình containerd, rồi một pod CUDA validator chạy tới Completed trên từng node:

nvidia-container-toolkit-daemonset-q7jhl   1/1   Running     0   5h8m
nvidia-container-toolkit-daemonset-rgqwj   1/1   Running     0   28h
nvidia-cuda-validator-7hghr                0/1   Completed   0   27h
nvidia-cuda-validator-x72v5                0/1   Completed   0   5h7m

Hai node chạy hai bản containerd khác major (2.2.2 và 1.7.28), và validator vẫn chạy xong trên cả hai.

GPU sharing là một chủ đề riêng, mình đã kể cùng HAMi và số đo trong bài recap KCD.

Gateway API: Envoy Gateway lo phần route

Yêu cầu: có một implementation Gateway API cho traffic inference, “which enables capabilities like weighted traffic splitting, header-based routing (for OpenAI protocol headers), and optional integration with service meshes.”

Bọn mình dùng Envoy Gateway làm implementation, phía trên là Envoy AI Gateway (giờ là Agent Router) lo LLM routing, chọn model backend và rate limit theo token. API group được serve đầy đủ, có cả ListenerSet và TLSRoute ở bản v1.

Test tạo một GatewayClass, một Gateway, một HTTPRoute match theo host name và một backend Python nhỏ. Khi Gateway báo PROGRAMMED: True thì gửi một request qua nó:

$ curl -v -H 'Host: test.example.local' http://<gateway-ip>:80/
> GET / HTTP/1.1
> Host: test.example.local
...
< HTTP/1.1 200 OK
< server: BaseHTTP/0.6 Python/3.11.15
...
Hello from Viettel AI Platform

Request đi vào Envoy, khớp HTTPRoute theo header Host, rồi tới được Service của backend.

Gang scheduling: Kueue nhận cả Job một lần

Yêu cầu: cài và chạy được “at least one gang scheduling solution that ensures all-or-nothing scheduling for distributed AI workloads (e.g. Kueue, Volcano, etc.)”

Bọn mình cài Kueue v0.10.2 từ release manifest, tạo một ResourceFlavor, một ClusterQueue quota 10 CPU và 10Gi memory, một LocalQueue, rồi tạo một Job có parallelism: 3 gắn label tên queue.

Vậy all-or-nothing nằm ở đâu? Kueue suspend Job ngay khi tạo, sinh ra một Workload đại diện cho cả bộ pod, và chỉ unsuspend khi cả bộ vừa quota. Nghĩa là quyết định “tất cả hoặc không” xảy ra lúc admission, trước khi scheduler nhìn thấy pod nào.

$ kubectl get workloads -n gang-scheduling-test
NAME                      QUEUE          RESERVED IN    ADMITTED   FINISHED   AGE
job-gang-test-job-b942a   gang-test-lq   gang-test-cq   True       True       83s

Ba pod ghi log thời điểm bắt đầu là 09:29:23, 09:29:24 và 09:29:25, và cả ba đều chạy xong.

Một mẹo nếu bạn dựng tiếp trên cái này: Kueue admit theo quota chứ không theo node còn trống, nên nếu cần các pod thực sự khởi động cùng nhau thì xem thêm waitForPodsReady.

Cluster autoscaling: chữ “nếu” quan trọng với bare metal

Yêu cầu mở đầu bằng một điều kiện: “If the platform provides a cluster autoscaler or an equivalent mechanism, it must be able to scale up/down node groups containing specific accelerator types based on pending pods.”

Trên bare metal thì không có node group nào để scale. Một server GPU mới join bằng kubeadm join, sau đó GPU Operator cài driver, cấu hình runtime và advertise GPU lên node:

NODE         GPU   CPU
gpu-node-1   2     96
gpu-node-2   2     96

Với kiểu triển khai này thì autoscaling diễn ra ở mức pod, cũng chính là yêu cầu tiếp theo.

Pod autoscaling: từ DCGM tới HPA

Yêu cầu: HPA phải chạy đúng với pod dùng accelerator, “including the ability to scale these Pods based on custom metrics relevant to AI/ML workloads.”

Làm CPU trước thì đơn giản: cài metrics-server, thêm --kubelet-insecure-tls vì kubelet trên mấy node bare-metal này dùng certificate mà metrics-server không verify được, rồi tạo tải lên một web server nhỏ. HPA tăng từ 1 lên 10 replica ở mức cpu: 436%/20%, rồi về lại 1 khi hết tải.

Sau đó tới GPU metric. Commit ngày 17/4 mở rộng phần này để cho thấy scale-out dưới tải GPU thật: một vòng lặp PyTorch nhân hai ma trận 8192×8192.

Pipeline có bốn phần: DCGM exporter gắn vào mỗi series GPU label namespace, pod và container của pod đang giữ device đó, Prometheus scrape các pod exporter, rồi Prometheus Adapter biến series đó thành custom metric:

rules:
  custom:
    - seriesQuery: 'DCGM_FI_DEV_GPU_UTIL{namespace!="",pod!=""}'
      resources:
        overrides:
          namespace: {resource: "namespace"}
          pod: {resource: "pod"}
      name:
        matches: "DCGM_FI_DEV_GPU_UTIL"
        as: "dcgm_fi_dev_gpu_util"
      metricsQuery: 'avg_over_time(DCGM_FI_DEV_GPU_UTIL{<<.LabelMatchers>>}[2m])'

Còn HPA thì target nó như một metric kiểu Pods với averageValue: "50". Pod đứng ở 100 nên:

Events:
  Normal  SuccessfulRescale  38s   horizontal-pod-autoscaler  New size: 2; reason: pods metric dcgm_fi_dev_gpu_util above target
  Normal  SuccessfulRescale  8s    horizontal-pod-autoscaler  New size: 4; reason: pods metric dcgm_fi_dev_gpu_util above target

Có hai chi tiết trong rule đó làm phần việc chính. namespace!="",pod!="" loại các GPU không có pod nào giữ, nên card rảnh không kéo trung bình xuống. Và DCGM_FI_DEV_GPU_UTIL là số đo ở mức device, nó khớp gọn với một pod ở đây vì mỗi pod giữ trọn một GPU.

Accelerator metrics: đọc thẳng endpoint gốc

Yêu cầu: ít nhất một giải pháp metrics có utilization và memory theo từng accelerator, thêm nhiệt độ, công suất và interconnect bandwidth nếu phần cứng có, ở định dạng chuẩn mà máy đọc được.

DCGM exporter đi kèm GPU Operator và nghe ở port 9400. Bọn mình curl nó từ trong cụm và đăng nguyên các dòng output, ở đây cắt bớt:

DCGM_FI_DEV_SM_CLOCK{gpu="0",...,modelName="NVIDIA L40S",Hostname="gpu-node-1",DCGM_FI_DRIVER_VERSION="580.126.09"} 210
DCGM_FI_DEV_GPU_TEMP{gpu="0",...,modelName="NVIDIA L40S",...} 32
DCGM_FI_DEV_POWER_USAGE{gpu="0",...,modelName="NVIDIA L40S",...} 34.156000
DCGM_FI_DEV_GPU_UTIL{gpu="0",...,modelName="NVIDIA L40S",...} 0
DCGM_FI_DEV_FB_FREE{gpu="0",...,modelName="NVIDIA L40S",...} 45457
DCGM_FI_PROF_PIPE_TENSOR_ACTIVE{gpu="0",...,modelName="NVIDIA L40S",...} 0.000000

Series nào cũng mang label DCGM_FI_DRIVER_VERSION, nên đây cũng là một cách kiểm chéo miễn phí cho mục driver ở trên.

AI service metrics: Alloy thu thập, Prometheus cấp số cho HPA

Yêu cầu: một hệ thống monitoring có thể tự discover và thu metrics từ các workload expose theo định dạng chuẩn, ví dụ Prometheus exposition.

Grafana Alloy lo phần thu thập chung: /metrics của KServe controller ở port 8080, /stats/prometheus của Envoy cho AI gateway, và DCGM exporter. Một Prometheus server scrape DCGM exporter cho Prometheus Adapter, rồi Adapter serve custom.metrics.k8s.io cho HPA ở phần trên.

Secure accelerator access: pod không xin GPU thì không thấy gì

Yêu cầu: quyền truy cập accelerator từ container phải được “properly isolated and mediated by the Kubernetes resource management framework (device plugin or DRA) and container runtime.”

Test gồm hai pod, trong đó pod đầu không xin GPU:

=== Test: Pod WITHOUT GPU request ===
PASS: No GPU devices visible (expected)
nvidia-smi check:
sh: 8: nvidia-smi: not found
PASS: nvidia-smi not available (expected)

Container đó thậm chí không có cả binary nvidia-smi. Vì sao vậy? Container toolkit chỉ inject device node và file driver cho những device mà device plugin đã cấp, nên không xin thì không có gì được inject.

Pod thứ hai xin nvidia.com/gpu: 1:

=== Test: Pod WITH GPU request ===
GPU 0: NVIDIA L40 (UUID: GPU-c3c7096e-db9d-322f-efd9-510971f3d33c)
---
/dev/nvidia-modeset
/dev/nvidia-uvm
/dev/nvidia-uvm-tools
/dev/nvidia1
/dev/nvidiactl

Chỉ có một device node GPU là nvidia1 cùng mấy device điều khiển. Node đó còn nvidia0 nữa nhưng pod không nhìn thấy.

Robust controller: KServe, có cả webhook

Yêu cầu: ít nhất một AI operator phức tạp có CRD cài được và chạy ổn định, tức là pod chạy, webhook trả lời, custom resource được reconcile.

Bọn mình dùng KServe: bảy CRD, trong đó có LLMInferenceService với llmisvc-controller-manager riêng, một mutating và bảy validating webhook configuration, chín ClusterServingRuntime đã reconcile từ HuggingFace tới TensorFlow Serving. Sau đó tạo một InferenceService:

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: sklearn-iris
  namespace: kserve-test
spec:
  predictor:
    sklearn:
      storageUri: "gs://kfserving-examples/models/sklearn/1.0/model"

Validating webhook nhận resource này và controller nhận nó vào vòng reconcile. Vậy là đủ cả ba thứ yêu cầu nêu ra: pod của controller, webhook và một custom resource được reconcile.

Vòng sau sẽ khác

Từ v1.37 chương trình khuyến nghị chạy bộ test AI Conformance của upstream và đính kèm junit.xml hoặc e2e.log cho các mục mà bộ test có. Hiện bộ test đã có test cho gang scheduling, cluster autoscaling, driver và runtime management, và AI service metrics. Test runner kiểm cùng một thứ theo cùng một cách cho mọi platform.

Badge chỉ là một cái logo trên trang landscape, còn evidence là mười file trên GitHub mà ai cũng đọc được. Của bọn mình ở đây. Nếu bạn đang chuẩn bị submission và kẹt ở mục nào thì cứ đọc thử rồi email mình nhé.