iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
Kubernetes

凌晨四點,女友帶著 GPU 來我家學習 Kubernetes:打造 K8s AI Infra 的 30 夜系列 第 27 篇

【Day 27】NVIDIA Dynamo 與 Grove:另一套推理框架,和它底下另一種 Kubernetes 工作負載 API

  • 分享至 

  • xImage
  •  

幾台各自獨立的 vLLM,前面要有一層決定每一發請求去哪一台。多個角色的 Pod,要一起建、一起更新、一起擴縮。這兩件事前面都處理過,用的是 llm-d,以及 LeaderWorkerSet 和 DisaggregatedSet。

今天兩層都換成 NVIDIA 的版本。Dynamo 是推理框架,Grove 是它底下那層工作負載的 API,兩者不在同一層。

要看的是一份宣告長出哪些物件,以及一個請求怎麼被兩個 Pod 分著做。


Dynamo 是推理框架,把多個引擎組成一套多節點的服務

Dynamo 跑在 vLLM、SGLang、TensorRT-LLM 上面,不取代它們,而是把它們組成一套多節點的推理系統。

層 誰 做什麼
引擎 vLLM 載入模型、管 KV cache、算 token
推理框架 Dynamo 前端入口、挑 worker、搬 KV
工作負載 API Grove 把多個角色寫成一個物件,交給排程器

它提供這幾項能力:

能力 做什麼
prefill 和 decode 分離 兩個階段各自成為可以獨立擴縮的 GPU 池
KV 感知的路由 按 worker 的負載和 KV cache 的重疊程度決定請求去哪一個
KV Block Manager 把 KV cache 從 GPU 往 CPU、SSD、遠端儲存搬
Planner 按 SLA 做自動擴縮,先量工作負載再決定每個池要多大
ModelExpress 透過 NIXL 和 NVLink 把模型權重從 GPU 直接送到 GPU

執行時的角色有四個。Frontend 收請求並正規化,對外是 OpenAI 格式的 HTTP endpoint;Router 按負載和 KV 重疊挑 worker;prefill worker 算 prompt 的 KV;decode worker 產出 token。prefill 算完的 KV 由 NIXL 送到 decode 那一邊。

Frontend 怎麼知道有哪些 worker,靠的是 DynamoWorkerMetadata 和 EndpointSlice,不用另外架一個註冊中心。

使用者寫的物件是 DynamoGraphDeployment,裡面是一個 components 清單,每個 component 一個角色。角色由 type 決定,可以是 frontend、worker、prefill、decode、planner、epp。


Grove 是工作負載 API,用一份宣告描述整套推理服務

Grove 是一個 Kubernetes API,用一份宣告描述一套 AI 推理工作負載。它不處理模型和 token,處理的是這些 Pod 要怎麼被排、擺在哪、按什麼順序起來,而這四件事 Kubernetes 原生沒有:

成組排程 多節點的模型實例要求 Pod 一起被排,而且要分層,必要的元件以全有全無為單位一起啟動
拓樸感知的擺放 按網路擺,例如擺在同一個 NVLink 網域裡,把通訊成本壓下來
啟動順序 明確的順序,避免元件亂序啟動造成的失敗
分層擴縮 擴縮分成多層,而 PodCliqueSet 的副本在擴的時候會照拓樸分散

它的物件有這幾個:

kind 是什麼
PodCliqueSet 最上層,一組會被一起管理、一起擺放的元件
PodClique 一個角色的一群 Pod,角色例如 leader、worker、frontend
PodCliqueScalingGroup 會一起擴縮、一起被當成一個 gang 排的幾個 PodClique
PodGang 排程器的 API,定義一個成組排程的單位
PodGangMap PodGang 和 PodCliqueSet 的對應

PodGang 在 scheduler.grove.io,其他幾個在 grove.io。

Dynamo 的 operator 把 DynamoGraphDeployment 翻成這些物件。


實驗環境

兩台 EC2 g6e.2xlarge,各一張 L40S 46068 MiB,k3s v1.36.4,GPU 由 DRA 分配,沒有 device plugin。API 版本 nvidia.com/v1beta1。

這一輪走 Dynamo 自己的 Frontend 拓樸,沒有裝 Gateway API,也沒有裝 GAIE。

KAI Scheduler 保持 chart 的預設,沒有裝。所以後面清單裡那個 PodGang 物件會建出來,但這個叢集裡沒有任何東西會照它的 minReplicas 擋 Pod。

裝 platform chart

export NAMESPACE=dynamo-system
export RELEASE_VERSION=1.5.1

helm fetch https://helm.ngc.nvidia.com/nvidia/ai-dynamo/charts/dynamo-platform-$RELEASE_VERSION.tgz

helm install dynamo-platform dynamo-platform-$RELEASE_VERSION.tgz \
  --namespace $NAMESPACE \
  --create-namespace \
  --set "global.grove.install=true"

chart 打包了六樣東西,預設只裝 operator

Chart.yaml 的依賴有 operator、NATS、etcd、KAI Scheduler、Grove、snapshot 六個,而 values.yaml 裡除了 operator 之外全部是 install: false。所以不下那個 --set,Grove 不會出現。

兩個預設值跟服務發現有關:

預設 影響
global.etcd.install: false 服務發現不經 etcd
dynamo-operator.discoveryBackend: kubernetes 走 Kubernetes API

NATS 也是選用的,預設的請求和事件平面走 TCP 和 ZMQ。

生效的設定跟你傳進去的值不一樣

helm get values -a 顯示的是 grove: { enabled: false, install: true },而 operator 的設定裡是 enabled: true。operator 的 OperatorConfiguration 在 render 的時候把 install 也算進 enabled,那個計算不會回寫 helm 的值。

kubectl -n dynamo-system get cm dynamo-platform-dynamo-operator-config \
  -o jsonpath='{.data.config\.yaml}' | python3 -c "
import sys,yaml
print(yaml.safe_load(sys.stdin.read())['orchestrators'])
"
{'kaiScheduler': {'enabled': False}, 'volcanoScheduler': {'enabled': False}, 'grove': {'enabled': True, 'terminationDelay': '4h'}}

namespace 和 secret

kubectl create namespace dynamo-demo
kubectl -n dynamo-demo create secret generic hf-token-secret --from-literal=HF_TOKEN=$HF_TOKEN
secret/hf-token-secret created

Qwen3-0.6B 不是需要授權的模型,token 的內容不重要,但這個 secret 必須存在,因為容器用 envFrom.secretRef 引用它。

卡用手寫的 claim

這個叢集沒有 device plugin,而 Dynamo 沒有提供一個寫一行就幫你生 claim 的便利層,所以 ResourceClaimTemplate 和兩處引用都要自己寫。podTemplate.spec 是一個 PodSpec,pod 層的 resourceClaims 塞得進去。

手寫不等於繞過 operator。operator 的 DRA 那段程式裡有一個 gpuCountFromClaimSpec(),它會從你寫的 claim 反推這個 Pod 要幾張卡,所以那份樣板是 operator 看得懂的輸入。

examples/backends/vllm/deploy/xpu/ 底下有八份 DRA 範例,agg 四份(本體、router、router 加 kv_approx、tracing),disagg 四份(本體、router、planner、tracing)。

而它們寫給的是另一家的卡。其中 disagg_xpu_dra.yaml 裡是 deviceClassName: gpu.intel.com、worker 映像 vllm-runtime-xpu、VLLM_TARGET_DEVICE: xpu、kv_buffer_device: "xpu"。

而這一份的形狀剛好是 Frontend 加一個 decode 加一個 prefill,各要一張卡。以它為底改五處:

改了什麼 為什麼
deviceClassName 換成 gpu.nvidia.com 這個叢集的 DeviceClass
worker 映像換成 vllm-runtime:1.5.1 Frontend 本來就是這個
刪掉 VLLM_TARGET_DEVICE: xpu 不是那家的卡
kv_buffer_device 換成 cuda 同上
加 VLLM_NIXL_SIDE_CHANNEL_HOST 讀 status.podIP vLLM 的預設值是 localhost,跨節點握手要真實位址

一個 prefill、一個 decode、一個 Frontend 的那份宣告

apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: gpu-template
spec:
  spec:
    devices:
      requests:
        - name: gpu
          exactly:
            deviceClassName: gpu.nvidia.com
            count: 1
---
apiVersion: nvidia.com/v1beta1
kind: DynamoGraphDeployment
metadata:
  name: vllm-disagg-dra
spec:
  components:
  - name: Frontend
    podTemplate:
      spec:
        containers:
        - envFrom:
          - secretRef:
              name: hf-token-secret
          image: nvcr.io/nvidia/ai-dynamo/vllm-runtime:1.5.1
          name: main
    replicas: 1
    type: frontend
  - name: VllmDecodeWorker
    podTemplate:
      spec:
        containers:
        - args:
          - --model
          - Qwen/Qwen3-0.6B
          - --disaggregation-mode
          - decode
          - --kv-transfer-config
          - '{"kv_connector":"NixlConnector","kv_role":"kv_consumer","kv_buffer_device":"cuda"}'
          - --block-size
          - "64"
          command:
          - python3
          - -m
          - dynamo.vllm
          env:
          - name: VLLM_NIXL_SIDE_CHANNEL_HOST
            valueFrom:
              fieldRef:
                fieldPath: status.podIP
          envFrom:
          - secretRef:
              name: hf-token-secret
          image: nvcr.io/nvidia/ai-dynamo/vllm-runtime:1.5.1
          name: main
          resources:
            claims:
            - name: gpu
            requests:
              ephemeral-storage: 2Gi
          workingDir: /workspace/examples/backends/vllm
        resourceClaims:
        - name: gpu
          resourceClaimTemplateName: gpu-template
    replicas: 1
    type: decode
  - name: VllmPrefillWorker
    podTemplate:
      spec:
        containers:
        - args:
          - --model
          - Qwen/Qwen3-0.6B
          - --disaggregation-mode
          - prefill
          - --kv-transfer-config
          - '{"kv_connector":"NixlConnector","kv_role":"kv_both","kv_buffer_device":"cuda"}'
          - --block-size
          - "64"
          command:
          - python3
          - -m
          - dynamo.vllm
          env:
          - name: VLLM_NIXL_SIDE_CHANNEL_HOST
            valueFrom:
              fieldRef:
                fieldPath: status.podIP
          envFrom:
          - secretRef:
              name: hf-token-secret
          image: nvcr.io/nvidia/ai-dynamo/vllm-runtime:1.5.1
          name: main
          resources:
            claims:
            - name: gpu
            requests:
              ephemeral-storage: 2Gi
          workingDir: /workspace/examples/backends/vllm
        resourceClaims:
        - name: gpu
          resourceClaimTemplateName: gpu-template
    replicas: 1
    type: prefill

resources 裡沒有任何 GPU 資源,卡靠容器層的 resources.claims 和 pod 層的 resourceClaims 兩處引用那份樣板。kv_role 照範例,decode 是 kv_consumer,prefill 是 kv_both。

kubectl apply -n dynamo-demo -f ~/dyn/02-disagg-dra.yaml
resourceclaimtemplate.resource.k8s.io/gpu-template created
dynamographdeployment.nvidia.com/vllm-disagg-dra created

一、一份宣告展開成什麼

kubectl -n dynamo-demo get dynamographdeployment,podcliqueset,podcliquescalinggroup,podclique,podgang,podgangmap,all,resourceclaim
NAME                                               READY   BACKEND   AGE
dynamographdeployment.nvidia.com/vllm-disagg-dra   True              9m46s

NAME                                    REPLICAS   AVAILABLE   UPDATED   PCLQS-UPDATED   PCLQS-TOTAL   PCSGS-UPDATED   PCSGS-TOTAL   AGE
podcliqueset.grove.io/vllm-disagg-dra   1          1           1                                                                     9m45s

NAME                                                     MINAVAIL   REPLICAS   READY   SCHEDULED   UPDATED   AGE
podclique.grove.io/vllm-disagg-dra-0-frontend            1          1          1       1           1         9m35s
podclique.grove.io/vllm-disagg-dra-0-vllmdecodeworker    1          1          1       1           1         9m35s
podclique.grove.io/vllm-disagg-dra-0-vllmprefillworker   1          1          1       1           1         9m35s

NAME                                                               PHASE     AGE
podgang.scheduler.grove.io/vllm-disagg-dra-0-1791654674281274298   Pending   9m36s

NAME                                    AGE
podgangmap.grove.io/vllm-disagg-dra-0   9m46s

NAME                                            READY   STATUS    RESTARTS   AGE
pod/vllm-disagg-dra-0-frontend-v57lz            1/1     Running   0          9m35s
pod/vllm-disagg-dra-0-vllmdecodeworker-hfvzb    1/1     Running   0          9m35s
pod/vllm-disagg-dra-0-vllmprefillworker-tn669   1/1     Running   0          9m35s

NAME                                        TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/vllm-disagg-dra-0                   ClusterIP   None            <none>        <none>     9m46s
service/vllm-disagg-dra-frontend            ClusterIP   10.43.168.38    <none>        8000/TCP   9m46s
service/vllm-disagg-dra-vllmdecodeworker    ClusterIP   10.43.222.187   <none>        9090/TCP   9m46s
service/vllm-disagg-dra-vllmprefillworker   ClusterIP   10.43.128.69    <none>        9090/TCP   9m46s

NAME                                                                                STATE                AGE
resourceclaim.resource.k8s.io/vllm-disagg-dra-0-vllmdecodeworker-hfvzb-gpu-5rnz4    allocated,reserved   9m36s
resourceclaim.resource.k8s.io/vllm-disagg-dra-0-vllmprefillworker-tn669-gpu-5pmcl   allocated,reserved   9m36s

get all 裡沒有 Deployment

all 這個集合涵蓋 Deployment、ReplicaSet、StatefulSet,而這個 namespace 裡一個都沒有,只有 Pod 和 Service。

四個 Service 也都不是宣告裡寫的:一個 headless 的 vllm-disagg-dra-0,加上三個 component 各一個。

PCSGS-UPDATED 和 PCSGS-TOTAL 兩欄是空的,PodCliqueScalingGroup 要在宣告裡寫出來才會建。

PodGang 的 PHASE 停在 Pending,而三個 Pod 都在跑。這個叢集沒有裝支援 PodGang 的排程器,所以沒有任何東西會去讀它。

Pod 的 owner 是 PodClique

kubectl -n dynamo-demo get podcliqueset,podclique,podgang,podgangmap,pod -o json | python3 -c "
import json,sys
for it in json.load(sys.stdin)['items']:
    m=it['metadata']
    o=m.get('ownerReferences') or []
    own=', '.join('%s/%s'%(x['kind'],x['name']) for x in o) or '(none)'
    print('%-14s %-44s owner: %s' % (it['kind'], m['name'], own))
"
PodCliqueSet   vllm-disagg-dra                              owner: DynamoGraphDeployment/vllm-disagg-dra
PodClique      vllm-disagg-dra-0-frontend                   owner: PodCliqueSet/vllm-disagg-dra
PodClique      vllm-disagg-dra-0-vllmdecodeworker           owner: PodCliqueSet/vllm-disagg-dra
PodClique      vllm-disagg-dra-0-vllmprefillworker          owner: PodCliqueSet/vllm-disagg-dra
PodGang        vllm-disagg-dra-0-1791654674281274298        owner: PodCliqueSet/vllm-disagg-dra
PodGangMap     vllm-disagg-dra-0                            owner: PodCliqueSet/vllm-disagg-dra
Pod            vllm-disagg-dra-0-frontend-v57lz             owner: PodClique/vllm-disagg-dra-0-frontend
Pod            vllm-disagg-dra-0-vllmdecodeworker-hfvzb     owner: PodClique/vllm-disagg-dra-0-vllmdecodeworker
Pod            vllm-disagg-dra-0-vllmprefillworker-tn669    owner: PodClique/vllm-disagg-dra-0-vllmprefillworker

Pod 的上一層就是 PodClique,中間沒有既有的工作負載物件。

PodClique、PodGang、PodGangMap 三個都是 PodCliqueSet 的直接子代,互為兄弟,不是一條鏈。

PodGang 的每一組各帶自己的 minReplicas

kubectl -n dynamo-demo get podgang -o yaml | sed -n '/^  spec:/,/^  status:/p'
  spec:
    podgroups:
    - minReplicas: 1
      name: vllm-disagg-dra-0-frontend
      podReferences:
      - name: vllm-disagg-dra-0-frontend-v57lz
        namespace: dynamo-demo
    - minReplicas: 1
      name: vllm-disagg-dra-0-vllmdecodeworker
      podReferences:
      - name: vllm-disagg-dra-0-vllmdecodeworker-hfvzb
        namespace: dynamo-demo
    - minReplicas: 1
      name: vllm-disagg-dra-0-vllmprefillworker
      podReferences:
      - name: vllm-disagg-dra-0-vllmprefillworker-tn669
        namespace: dynamo-demo
  status:

三個 podgroup 各帶自己的 minReplicas,而且 Pod 的名字直接列在裡面。該擋誰、擋幾個都寫好了,缺的是會讀它的那個排程器。

兩張卡各給一個 worker

kubectl -n dynamo-demo get pod -o wide
NAME                                        READY   STATUS    RESTARTS   AGE    IP            NODE
vllm-disagg-dra-0-frontend-v57lz            1/1     Running   0          3m1s   10.42.1.228   ip-172-31-30-5
vllm-disagg-dra-0-vllmdecodeworker-hfvzb    1/1     Running   0          3m1s   10.42.0.157   ip-172-31-26-39
vllm-disagg-dra-0-vllmprefillworker-tn669   1/1     Running   0          3m1s   10.42.1.229   ip-172-31-30-5

prefill 和 decode 落在不同節點,所以後面那次 KV 傳輸是跨節點的。

前面那份清單裡只有兩個 ResourceClaim,各屬於一個 worker。Frontend 沒有 claim,它不算東西。

一個入口,而它的設定裡沒有模型

Frontend 那個 component 的容器只有 image 和 envFrom,沒有 args,整份宣告裡模型只出現在兩個 worker 的參數上。

kubectl -n dynamo-demo port-forward svc/vllm-disagg-dra-frontend 8000:8000 >/tmp/pf.log 2>&1 &
echo $! > /tmp/pf.pid
sleep 5
curl -s localhost:8000/v1/models | python3 -m json.tool
{
    "object": "list",
    "data": [
        {
            "id": "Qwen/Qwen3-0.6B",
            "object": "model",
            "created": 1791654992,
            "owned_by": "nvidia",
            "context_window": 40960
        }
    ]
}

Frontend 知道有這個模型,是兩個 worker 自己註冊進去的。宣告裡沒有任何一行把 worker 的位址告訴 Frontend,而這個叢集也沒有 etcd。


二、一個請求出現在兩個 Pod

先把兩個 worker 的 log 接出來,記下目前的行數當基準。

kubectl -n dynamo-demo logs -f vllm-disagg-dra-0-vllmprefillworker-tn669 > /tmp/pre.log 2>&1 &
echo $! > /tmp/pre.pid
kubectl -n dynamo-demo logs -f vllm-disagg-dra-0-vllmdecodeworker-hfvzb > /tmp/dec.log 2>&1 &
echo $! > /tmp/dec.pid
sleep 3
wc -l /tmp/pre.log /tmp/dec.log
   238 /tmp/pre.log
   208 /tmp/dec.log
   446 total

送一發請求,prompt 裡放一個好認的字串。

curl -si -X POST localhost:8000/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"Qwen/Qwen3-0.6B","messages":[{"role":"user","content":"MARKER-7f3a9c Summarize in one sentence: Dynamic Resource Allocation lets a Kubernetes workload claim attached devices through DeviceClass and ResourceClaimTemplate objects instead of asking for an opaque integer count of vendor resources, which means the scheduler can reason about device attributes, topology and sharing rather than a single number."}],"max_tokens":32}'
HTTP/1.1 200 OK
content-type: application/json
transfer-encoding: chunked
date: Sat, 10 Oct 2026 17:57:50 GMT

{"id":"chatcmpl-36a56007-19b3-4f62-b01e-5586e3f6aead","choices":[{"index":0,"message":{"content":"<think>\nOkay, let's see. The user wants a one-sentence summary of the MARKER-7f3a9c information. The key points","refusal":null,"role":"assistant"},"finish_reason":"length","logprobs":null}],"created":1791655070,"model":"Qwen/Qwen3-0.6B","object":"chat.completion","usage":{"prompt_tokens":73,"completion_tokens":32,"total_tokens":105,"prompt_tokens_details":{"cached_tokens":0}}}

回應裡沒有 server 這個 header,也沒有 system_fingerprint。吐回應的是 Dynamo 自己的 frontend,vLLM 在它後面。

prefill 那一側

tail -n +239 /tmp/pre.log
2026-10-10T17:57:50.695158Z  INFO handle_payload: dynamo_runtime::pipeline::network::ingress::push_handler: request received request_id=36a56007-19b3-4f62-b01e-5586e3f6aead otel.kind="server" request_id="36a56007-19b3-4f62-b01e-5586e3f6aead" component="prefill" endpoint="generate" namespace="dynamo-demo-vllm-disagg-dra-93940418" instance_id=5791056154302409
2026-10-10T17:57:50.707957Z  INFO handle_payload: dynamo_runtime::pipeline::network::ingress::push_handler: request completed request_id=36a56007-19b3-4f62-b01e-5586e3f6aead otel.kind="server" request_id="36a56007-19b3-4f62-b01e-5586e3f6aead" component="prefill" endpoint="generate" namespace="dynamo-demo-vllm-disagg-dra-93940418" instance_id=5791056154302409
2026-10-10T17:57:55.116649Z  INFO loggers.log: Engine 000: Avg prompt throughput: 7.4 tokens/s, Avg generation throughput: 0.2 tokens/s, Running: 0 reqs, Waiting: 0 reqs, GPU KV cache usage: 0.1%, Prefix cache hit rate: 0.0%, External prefix cache hit rate: 0.0%
2026-10-10T17:57:55.116804Z  INFO metrics.log: KV Transfer metrics: Num successful transfers=0, Avg xfer time (ms)=0, P90 xfer time (ms)=0, Avg post time (ms)=0, P90 post time (ms)=0, Avg MB per transfer=0, Throughput (MB/s)=0, Avg number of descriptors=0

component 是 prefill,請求收到也完成了。

decode 那一側

tail -n +209 /tmp/dec.log
2026-10-10T17:57:50.709035Z  INFO handle_payload: dynamo_runtime::pipeline::network::ingress::push_handler: request received request_id=36a56007-19b3-4f62-b01e-5586e3f6aead otel.kind="server" request_id="36a56007-19b3-4f62-b01e-5586e3f6aead" component="backend" endpoint="generate" namespace="dynamo-demo-vllm-disagg-dra-93940418" instance_id=1519627598993914
(EngineCore pid=187) 2026-10-10T17:57:50.713892Z  INFO base_worker._nixl_handshake: NIXL compatibility check passed (hash: 818f3314a8adad82f286256c842e7585c39d4d414e13c6e03a733f4ecd53deda)
(EngineCore pid=187) 2026-10-10T17:57:50.714096Z  INFO base_worker.add_remote_agent: Transfer plan: TransferTopology(tp_ratio=1, num_kv_heads=8, local_tp=1, remote_tp=1, remote_pp=0, local_rank=0, remote_block_len=262144)
2026-10-10T17:57:50.984974Z  INFO handle_payload: dynamo_runtime::pipeline::network::ingress::push_handler: request completed request_id=36a56007-19b3-4f62-b01e-5586e3f6aead otel.kind="server" request_id="36a56007-19b3-4f62-b01e-5586e3f6aead" component="backend" endpoint="generate" namespace="dynamo-demo-vllm-disagg-dra-93940418" instance_id=1519627598993914
2026-10-10T17:57:56.559651Z  INFO loggers.log: Engine 000: Avg prompt throughput: 0.1 tokens/s, Avg generation throughput: 3.3 tokens/s, Running: 0 reqs, Waiting: 0 reqs, GPU KV cache usage: 0.0%, Prefix cache hit rate: 0.0%, External prefix cache hit rate: 75.3%
2026-10-10T17:57:56.560545Z  INFO metrics.log: KV Transfer metrics: Num successful transfers=1, Avg xfer time (ms)=74.382, P90 xfer time (ms)=74.382, Avg post time (ms)=0.402, P90 post time (ms)=0.402, Avg MB per transfer=14.0, Throughput (MB/s)=188.218, Avg number of descriptors=56.0

這兩段 log 說了四件事

同一個 request_id 出現在兩個 Pod 上,而 instance_id 不同。一個請求確實被兩個 worker 分著做。

順序是 prefill 先完成,decode 才收到。prefill 在 .707957 完成,decode 在 .709035 收到。

NIXL 的握手是 Dynamo 做的。decode 那邊印出相容性檢查通過,以及一份 TransferTopology。宣告裡沒有任何一行把 prefill 的位址告訴 decode。

傳輸的計數記在拉的那一邊。整段時間裡 prefill 的 Num successful transfers 是 0,decode 是 1,單次 14.0 MB。decode 的 External prefix cache hit rate 從 0 跳到 75.3%,那一段就是 prefill 送過來的 KV。

kill $(cat /tmp/pre.pid) $(cat /tmp/dec.pid) $(cat /tmp/pf.pid)

收尾

kubectl delete -n dynamo-demo -f ~/dyn/02-disagg-dra.yaml
kubectl delete namespace dynamo-demo
helm uninstall dynamo-platform -n dynamo-system

小結

Dynamo 做的事是把一套分離式推理的接線收進宣告裡。一個 type: frontend 當入口,一個 prefill 一個 decode 在後面,兩個 worker 自己註冊讓 Frontend 找得到,NIXL 的握手和 KV 的搬運也是它自己接的。宣告裡沒有 proxy、沒有 Service、沒有互相的位址。

一個請求在兩個 Pod 的 log 裡用同一個 request_id 出現,instance_id 不同,傳輸的計數只記在拉的那一邊。分離這件事不是推論出來的,是兩邊的 log 對起來的。

Grove 是這套東西底下的工作負載 API。它生出來的 Pod 不屬於任何 Deployment、ReplicaSet 或 StatefulSet,PodClique 直接擁有 Pod。多角色的階層在它這裡是 API 的一部分,不是用好幾層既有物件疊出來的。

而 PodGang 是給排程器看的。這個叢集沒有裝支援它的排程器,所以它建出來就停在 Pending,而工作負載照樣跑完。物件在不等於那件事在發生。


參考資料

ai-dynamo/dynamo
Overall Architecture - NVIDIA Dynamo
DynamoGraphDeployment Reference - NVIDIA Dynamo
Grove - NVIDIA Dynamo
dynamo platform chart README
ai-dynamo/grove
kai-scheduler/KAI-Scheduler
Disaggregated Prefilling, vLLM
Dynamic Resource Allocation - Kubernetes


上一篇
【Day 26】KServe LLMInferenceService:一份宣告在 Kubernetes 上展開成 vLLM 與 DRA 物件
下一篇
【Day 28】Kubernetes GPU 叢集的指標:硬體、叢集、引擎三層
系列文
凌晨四點,女友帶著 GPU 來我家學習 Kubernetes:打造 K8s AI Infra 的 30 夜 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言