iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
Kubernetes

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

【Day 14】GPU 高效能網路:從節點內到跨節點,再到 Kubernetes

  • 分享至 

  • xImage
  •  

訓練從來不是一張卡的事。每張卡各算自己那一份,算完要把梯度湊起來,湊完才能走下一步。所以今天換一個問題:卡跟卡之間怎麼講話。

順序由近到遠,分三層:

這一層在問什麼
第一層 同一台機器裡,兩張卡之間有多遠
第二層 資料跨出機器的時候,走什麼
第三層 那些網卡要怎麼交到 Pod 手上

第一層:同一台機器裡,兩張卡有多遠

nvidia-smi topo -m 是 Nvidia 用來查拓樸關係的指令,白話來說,這段指令可以理解成用來查「這台機器裡,每張 GPU 彼此之間是怎麼連的?」。

一台八卡機器的輸出大概長這樣:

        GPU0  GPU1  GPU2  GPU3  GPU4  GPU5  GPU6  GPU7  CPU Affinity
GPU0     X    NV1   NV1   NV2   NV2   SYS   SYS   SYS   0-19,40-59
GPU1    NV1    X    NV2   NV1   SYS   NV2   SYS   SYS   0-19,40-59
GPU2    NV1   NV2    X    NV2   SYS   SYS   NV1   SYS   0-19,40-59
GPU3    NV2   NV1   NV2    X    SYS   SYS   SYS   NV1   0-19,40-59
GPU4    NV2   SYS   SYS   SYS    X    NV1   NV1   NV2   20-39,60-79
GPU5    SYS   NV2   SYS   SYS   NV1    X    NV2   NV1   20-39,60-79
GPU6    SYS   SYS   NV1   SYS   NV1   NV2    X    NV2   20-39,60-79
GPU7    SYS   SYS   SYS   NV1   NV2   NV1   NV2    X    20-39,60-79

整理成由近到遠:

代號 這條連線經過什麼
NV# NVLink,# 是幾條綁在一起(NV2 比 NV1 寬一倍)
PIX 單一個 PCIe switch
PXB 多個 PCIe switch,但還沒碰到 host bridge
PHB 經過 PCIe host bridge,也就是 CPU
NODE 跨越不同 host bridge 之間的互連,但還在同一個 NUMA 節點內
SYS 跨 NUMA,要走 CPU 之間的互連(QPI/UPI)

NUMA 節點指的是一顆 CPU 加上它直接管的記憶體與 PCIe 插槽。兩顆 CPU 的機器有兩個 NUMA 節點,跨過去就要走 CPU 之間的那條線。

這台機器長什麼樣

矩陣裡有一個很清楚的圖樣:GPU0 到 GPU3 之間全是 NV,GPU4 到 GPU7 之間也全是 NV,但跨組就變成 SYS。

最後那欄印證了這個分組:前四張卡的 CPU affinity 是 0-19,40-59,後四張是 20-39,60-79。這台機器有兩顆 CPU,八張卡分成兩半,組內拉了 NVLink,跨組要繞 QPI/UPI。

同一台機器裡,GPU0 到 GPU1 和 GPU0 到 GPU5,是兩種完全不同的距離。

而 PHB 那一列的說明點出了為什麼這件事重要:到了那個距離,連線要經過 CPU,資料不再是 GPU 直接對 GPU。

NVLink 與 NVSwitch

矩陣裡的 NV1、NV2 就是最近的那一種連法。

NVLink 是 GPU 對 GPU 的直連,不經過 PCIe,也不經過 CPU。以第五代 NVLink 為例,搭配 Blackwell 架構(B200),每張 GPU 的 NVLink 頻寬是 1,800 GB/s,最多 18 條連結。那個 NV# 的井字號就是「幾條 NVLink 綁在一起」,所以 NV2 的頻寬是 NV1 的兩倍。

但只靠直連的話,8 張卡要兩兩都通,每張卡得拉 7 條線,卡再多就不可行了。而且就算拉得動,那 18 條連結分給 7 個鄰居,每個只剩 2 條多,頻寬被切得很碎。

NVSwitch 解決的是這個:每張卡的 18 條 NVLink 全部接到交換機上,由交換機決定當下要把頻寬給誰。任兩張卡之間都能用到全速,不是幾張卡共用一份。拓撲從「實體上能拉幾條線」的問題,變成交換的問題。

第五代的 NVSwitch 更把 NVLink 的連線延伸到機器之外,讓一個 NVLink domain 涵蓋數百張 GPU。GB200 NVL72 就是這樣:72 張 GPU 被當成一個加速器用,每一對 GPU 之間都是 1,800 GB/s。這叫 MNNVL(Multi-Node NVLink),也是第三層那個 nvidia.com/gpu.clique 標籤與 ComputeDomain CRD 存在的理由。

P2P:GPU 直接讀寫對方的記憶體

距離之所以重要,是因為它決定了能不能用 P2P。

P2P 是 CUDA 的直接存取:一張卡上的 kernel 直接讀寫另一張卡的記憶體位址,中間不經過主機記憶體。它可以走 NVLink,也可以走 PCIe,重點是那條路要能直接定址。

一旦要跨越 CPU 的 host bridge,或更糟、跨到另一顆 CPU 去,這條路就不成立或不划算了。

NCCL 用一條門檻線決定要不要用 P2P

NCCL 是 NVIDIA 的多 GPU 通訊函式庫,後面會詳談。它有一個環境變數 NCCL_P2P_LEVEL,定義「多遠以內才用 P2P」:

值 意思
LOC 永遠不用 P2P
NVL 有 NVLink 才用
PIX 同一個 PCI switch 以內
PXB 經過多個 PCI switch 也算
PHB 同一個 NUMA 節點以內,流量會經過 CPU
SYS 跨 NUMA 也照用,可能要走 QPI/UPI

這些值和 topo -m 的圖例幾乎是同一套詞彙,只差一個 NODE。

兩份合起來看:topo -m 說這條連線經過了什麼,NCCL_P2P_LEVEL 說多遠以內還值得用 P2P。

走不了近路就往下掉

P2P 用不上的時候,NCCL 還有兩條後備道路,而且有明確的順序:

P2P    GPU 直接對 GPU(NVLink 或 PCIe)
 ↓     用不了的話
SHM    繞主機記憶體中轉
 ↓     也被關掉的話
NET    走網路(InfiniBand 或 IP socket)

SHM 是作業系統層級的共享記憶體,用的是主機記憶體(一般講的 RAM,和 GPU 上的記憶體是兩回事)。發送端先把資料從 GPU 記憶體複製到一塊主機記憶體,接收端再複製進自己的 GPU 記憶體,兩次複製都要經過 PCIe,這就是它比 P2P 慢的原因。

兩個相關的環境變數分別關掉其中一層:NCCL_P2P_DISABLE 關掉 P2P,NCCL_SHM_DISABLE 關掉 SHM。

有一件事容易誤會:NET 不是跨機器專用的。 同一台機器裡兩顆 CPU 之間的通訊,只要 SHM 被關掉,NCCL 一樣會走網路,資料繞出去再繞回來。跨機器時 NET 是唯一選項,單機時它是最後的後備。


第二層:跨出機器

前面講的 P2P、SHM,都只在一台機器裡有效。一旦資料要送到另一台機器,就只剩最後一條路:走網路。

而 GPU 叢集之所以要特別講網路,是因為一般的網路太慢,不是線太細,是資料在兩端各繞了一大圈。這一節要看的就是怎麼把那個圈拿掉,以及做這件事的兩種網路。

RDMA 是一種能力,不是一種網路

RDMA 讓資料直接在兩端的應用程式之間傳輸,作業系統不介入,兩邊的 CPU 幾乎不耗資源。

傳統作法為什麼慢?因為應用程式碰不到網路,一切都得經過作業系統。資料要從應用程式的緩衝區被搬到網路堆疊、再送上線路,接收端還要反過來做一遍。這兩趟搬運是 CPU 花週期做的,RDMA 把它們拿掉。

「能力」這個詞要記住:RDMA 不是一個協定,而是一種能力。接下來兩節講的是這個能力跑在哪種網路上。

InfiniBand:把不丟包做進連結層

一般的網路會丟包。線路塞住了、交換機的緩衝區滿了,封包就被丟掉,再由 TCP 那一層偵測並重傳。這在瀏覽網頁上沒問題,但 RDMA 之所以能比 TCP 輕量,正是因為底下那層已經保證了不丟包、不亂序,所以它需要一個不丟包的網路,也就是所謂的無損網路(lossless fabric)。嚴格說,定義是封包不會例行性地被丟棄,不是絕不丟包。

InfiniBand 就是為此而生的。它是一套完整的網路協定,從實體層到傳輸層都自己定義,靠網卡(HCA)上的 RDMA 直接搬資料,CPU 不介入。

一個 IB 網路裡有三種角色:每台機器上的 HCA、負責轉封包的交換機,以及一個叫子網管理器(SM)的軟體。

不丟包怎麼做到

IB 的無損來自連結層的流控:每條連結的接收端會給發送端 credit,告訴它現在可以收多少資料。最關鍵的規則是,接收端宣告的 credit 如果不足以裝下整則訊息,發送端就一個位元組都不送。

因為連結層保證了不超送、也保證按序送達,上層就省事了:IB 的傳輸層不需要重排封包,也不需要靠丟包來試探該送多少。

對照乙太網:乙太網是先送再說、塞住了丟掉、丟了再由 TCP 補救。IB 是送之前先問過對面有沒有位子。TCP 的擁塞控制本質上是撞牆才知道牆在哪,IB 是事先問過。

路徑由一個中央實體算好

子網管理器(SM)負責設定整個網路。它會為每一對端點預先算好路徑,把轉發決策事先寫進交換機,而不是等封包到了才查表決定。連結掉線或新增時,也由它重新設定。

那它會不會是單點故障?不會。一個網路裡可以有多個 SM,但同一時間只有一個在工作,其餘的持有它的副本並盯著它;工作中的那個倒了,其中一個會接手。

這樣做換到的好處是平行路徑可以全部用上,而且路徑是連線建立時就算好的,傳輸時不必再付代價。

對照乙太網:乙太網為了避免迴圈,要靠 spanning tree 關掉多餘的平行連結。NVIDIA 的說法是,spanning tree 是乙太網最好也最糟的元素之一,如今已成了一種負擔,因為它限制了節點之間能有幾條平行路徑。

RoCE:把 IB 的傳輸層搬到乙太網

IB 自成一個世界:專屬的交換機、網卡、線材,和既有的乙太網設備不通。RoCE 要解的就是這個——RDMA over Converged Ethernet,把 IB 的傳輸層封裝進乙太網封包,跑在一般的乙太網設備上。前面講的那一套沒有被丟掉,只是外面換了一層殼。

它有兩個版本。舊的 RoCEv1 活在第二層,過不了路由器;後來的 RoCEv2 改成套一層 IP 加 UDP,封包就穿得過路由器了。

無損得自己做出來

RoCE 的定義裡有一個容易滑過去的詞:它提供的是「在無損的乙太網上」的低延遲傳輸。

但乙太網本來就會丟包。IB 的無損來自連結層的 credit 流控,乙太網沒有這個東西。

所以那個「無損」得自己在交換機那一側設定出來。

RoCE 不是插上網路線就有 RDMA,它假設你已經把那張乙太網做成無損的了。

GPUDirect RDMA:網卡直接對 GPU 記憶體

RDMA 已經讓網卡直接讀寫應用程式的記憶體。GPUDirect RDMA 再更進一步:網卡直接讀寫 GPU 的記憶體,連主機記憶體那一站都省了。

但它有一個條件:網卡和 GPU 最好在同一個 PCIe root complex 底下,不然效能會掉。

怎麼查?還是 nvidia-smi topo -m。在有 RDMA 網卡的機器上,那個矩陣會把網卡(mlx5_0 這類)一起列進去,所以你會直接看到某張 GPU 和某張網卡之間是 PIX 還是 SYS。

第一層那張距離表,量的不只是 GPU 對 GPU,也包括 GPU 對網卡。


實驗:跑一次 all-reduce,量出沒有 RDMA 的代價

先認識 NCCL

多張卡一起訓練,彼此要交換資料。最常見的是 AllReduce:把所有 GPU 算出來的梯度加總,再把結果送回每一張卡。NCCL 就是做這件事的函式庫。

同一個 AllReduce 有不同的做法。Ring 讓資料沿著一個環走一圈加總,再走一圈送回去;Tree 是兩兩一組先加,像一棵樹收斂到頂點,再反過來分送下去。

注意這和前面講的路徑是兩回事:Ring 和 Tree 是運算的順序,P2P、SHM、NET 是資料走哪條線。兩者獨立,常被混在一起講。

怎麼讓它把選擇印出來

NCCL 的日誌分兩層控制。NCCL_DEBUG 決定詳細程度,通常設 INFO;但 NCCL_DEBUG_SUBSYS 沒設定時預設只印初始化、啟動協商、環境變數那三類,看不到選路。

要看選路得指定子系統,相關的是 P2P、SHM、NET、GRAPH、TUNING。開啟之後會看到:

# 走 P2P
NCCL INFO Channel 00/0 : 0[0] -> 1[1] via P2P/CUMEM

# 走 SHM
NCCL INFO Channel 00 : 0[0] -> 1[1] via SHM/direct/direct

# 走 NET
NCCL INFO Channel 00/0 : 0[0] -> 1[1] [send] via NET/Socket/0

這三行就是第一層那個順序的實際樣子。

實驗環境

環境是 Day 1 開的兩台 EC2 組成的真卡叢集。兩台之間沒有 NVLink、沒有 RDMA 網卡,只有一般的乙太網。

先確認判準

判準一:兩個 Pod 要落在不同節點。 排到同一台就走 shared memory,那不是要量的東西。用 podAntiAffinity 強制分開:

affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels: { app: nccl }
        topologyKey: kubernetes.io/hostname
kubectl -n nccl get pods -o custom-columns='NAME:.metadata.name,NODE:.spec.nodeName'
NAME      NODE
nccl-r0   ip-172-31-30-5
nccl-r1   ip-172-31-26-39

兩台不一樣,過。

判準二:確認 NCCL 真的走網路。 這是實驗一要看的。

rank 怎麼拿到卡

每個 Pod 用 ResourceClaimTemplate 各自要一張:

apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: one-gpu
  namespace: nccl
spec:
  spec:
    devices:
      requests:
        - name: gpu
          exactly:
            deviceClassName: gpu.nvidia.com

用 template 而不是共用一個 claim,否則兩個 Pod 會指向同一份。

kubectl -n nccl get resourceclaims -o custom-columns='NAME:.metadata.name,DEVICE:.status.allocation.devices.results[*].device,POD:.status.reservedFor[*].name'
NAME                DEVICE   POD
nccl-r0-gpu-458l2   gpu-0    nccl-r0
nccl-r1-gpu-tq7xc   gpu-0    nccl-r1

兩個都叫 gpu-0,但那是兩台機器上的兩張卡。

順帶一提,這個叢集的 node.status.allocatable 裡 nvidia.com/gpu 是 <none>。那不是壞了,是對的:走 DRA 的卡不會出現在 allocatable 裡,那一欄是 device plugin 的地盤。同一張卡,device plugin 讓它變成一個整數,DRA 讓它變成一個帶名字的物件。

實驗一:NCCL 選了哪條路

kubectl -n nccl logs nccl-r0 | grep -E "libibverbs|NET plugin|NET/Socket|Using network|via NET"
NCCL INFO Failed to open libibverbs.so[.1]
NCCL INFO transport/net_ib/init.cc:370 -> 3
NCCL INFO Failed to initialize NET plugin IB
NCCL INFO NET/Socket : Using [0]eth0:10.42.1.22<0>
NCCL INFO Using network Socket
NCCL INFO Channel 00/0 : 0[0] -> 1[0] [send] via NET/Socket/0
NCCL INFO Channel 01/0 : 0[0] -> 1[0] [send] via NET/Socket/0

NCCL 先試了 InfiniBand,開不了 libibverbs.so,初始化失敗,然後安靜地退回 socket。沒有警告、沒有錯誤,程式照跑。

「訓練變慢,結果發現它一直在走 TCP」是很常見的狀況,而這就是現場。不開 NCCL_DEBUG=INFO 完全看不到。

再看另外兩行:

kubectl -n nccl logs nccl-r0 | grep -E "GDR|Trees"
NCCL INFO Trees [0] 1/-1/-1->0->-1 [1] -1/-1/-1->0->1
NCCL INFO Connected all rings, use ring PXN 0 GDR 0

GDR 0 是 GPUDirect RDMA 沒開。 前面那條 PCIe root 限制,在這裡根本輪不到它上場。

Trees [...] 是 NCCL 實際建出來的樹狀結構,也就是前面講的那個演算法。

實驗二:量出來是多少

腳本從 8 bytes 跑到 1 GB,每次乘 2,前五次暖身不計時。

kubectl -n nccl logs nccl-r0 | grep -vE "NCCL|^$" | tail -32

節錄幾列:

     size(B)    time(us)  algbw(GB/s)  busbw(GB/s)
           8       272.6        0.000        0.000
         128       268.1        0.000        0.000
        8192       322.5        0.025        0.025
      524288      1301.6        0.403        0.403
     4194304      5531.8        0.758        0.758
    33554432     50556.6        0.664        0.664
   268435456    219399.3        1.224        1.224
  1073741824    935943.5        1.147        1.147

峰值 busbw 是 1.224 GB/s,在 256 MB 那一列。

三個觀察。

一、busbw 和 algbw 完全相同。 不是算錯。all-reduce 的修正係數是 2(n−1)/n,兩個 rank 代入就是 1,這個公式在兩個 rank 時什麼都沒修正。rank 多起來才看得出差別。

二、小訊息那幾列時間幾乎不動。 8 bytes 要 272 µs,128 bytes 要 268 µs。這個區間量到的不是頻寬,是一趟來回要多久。

三、大訊息才看得出頻寬。 從幾百 KB 開始 busbw 才爬上去,最後停在 1.2 GB/s 附近。

這個數字是什麼概念

前面講的第五代 NVLink 是每張 GPU 1,800 GB/s,InfiniBand NDR 單埠是 50 GB/s。這幾個數字性質不同(前兩個是連結規格,我們這個是實測的 busbw),但量級差距說明了一切:這裡的瓶頸不是 GPU,是那條一般的乙太網。


第三層:Kubernetes 怎麼把網卡交出去

前面兩層講的是硬體本身的能力。網卡要怎麼交到 Pod 手上,是另一個問題。

NVLink 在 Kubernetes 裡的形狀

前面講的 NVL72,在 Kubernetes 裡是一個節點標籤加一個 CRD。

標籤:gpu.clique

GPU Feature Discovery 會在每個 GPU 節點貼上一個 Clique ID,表示這個 NVLink domain 裡哪些 GPU 實體上通得到彼此。

NODE    LABEL                  CLIQUE
node1   nvidia.com/gpu.clique  9277d399-0674-44a9-b64e-d85bb19ce2b0.32766
node2   nvidia.com/gpu.clique  9277d399-0674-44a9-b64e-d85bb19ce2b0.32766

值的格式是「叢集 UUID 加 Clique ID」。分群是在 NVSwitch 那一層做的,不是節點層,節點只是繼承結果,所以同一個節點上的 GPU 保證有相同的一組值。

這個標籤還有前提:GPU Operator 要等 NVML 回報 fabric 狀態是 GPU_FABRIC_STATE_COMPLETED 才會寫上。沒有跨節點 NVLink 的機器(例如 DGX B200、B300)永遠到不了那個狀態,標籤根本不會出現。

CRD:ComputeDomain

ComputeDomain 保證組內的 Pod 之間 MNNVL 通得到,同時和組外的 Pod 隔離。它的位置跟著工作負載走,壽命也綁在工作負載上,工作結束就消失。

底下是 NVIDIA 的 DRA driver 在調度 IMEX 的 daemon、domain 與 channel,使用者只碰 ComputeDomain 這個 CRD。

舊路:device plugin 加 Multus

為什麼需要 Multus

Kubernetes 裡一個 Pod 通常只有一張網路介面(loopback 之外)。Multus 讓 Pod 可以有多張,作法是自己當一個 meta-plugin:它是 CNI plugin,但工作是去呼叫其他 CNI plugin。

Multus 不配裝置,它只是讀帳

Multus 不分配裝置。分配是 device plugin 加 kubelet 的事,而且以容器為單位。

kubelet 把分配結果記成「(Pod, 容器, 資源名) 對應到一串裝置 ID」。Multus 從 kubelet 的 pod-resources API 讀這份帳,把同一個資源名底下各容器的清單串成一份 Pod 層級的列表,需要時取出還沒用過的 ID,寫進下游 CNI 設定的 deviceID。

整條鏈:

  1. device plugin 把網卡 advertise 成一個資源名
  2. Pod 在容器的 resources.requests 裡要它,kubelet 分配裝置
  3. NAD 用 annotation 宣告自己要用哪個資源名
  4. Multus 去 kubelet 讀「誰拿到哪些裝置 ID」
  5. Multus 把裝置 ID 寫進下游 CNI 的 deviceID

NAD(NetworkAttachmentDefinition)長這樣:

apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  name: sriov-net-a
  annotations:
    k8s.v1.cni.cncf.io/resourceName: intel.com/sriov_net_A
spec:
  config: '{ "cniVersion": "1.0.0", "name": "sriov-net-a", "type": "sriov" }'

那行 annotation 就是第 3 步。

RDMA shared device plugin

RDMA 網卡這邊,Mellanox 的 device plugin 用一份設定描述要發布什麼:

{
  "resourceName": "hca_shared_devices_a",
  "resourcePrefix": "example_prefix",
  "rdmaHcaMax": 1000,
  "devices": ["ib0", "ib1"]
}

resourcePrefix 不填預設是 rdma,所以資源名就是 rdma/hca_shared_devices_a。挑裝置除了直接列 devices,也可以用 vendors、deviceIDs、drivers、ifNames、linkTypes 篩。

rdmaHcaMax 是必填,指這個資源能提供的 RDMA 資源數量上限。它是上限計數,不是實體張數:設 1000 不代表有一千張卡,是同一張 HCA 最多能被一千個容器共用。

SR-IOV

SR-IOV 讓一張實體網卡被多個 Pod 共用,效能接近原生。它的 operator 有四個元件:

元件 位置 做什麼
Operator 控制平面 管理 CR、產生節點設定
Operator Webhook 控制平面 驗證 SriovNetworkNodePolicy 與 SriovOperatorConfig
Resource Injector 控制平面 mutating webhook,依 Pod 的網路 annotation 自動注入資源請求
Config Daemon 每個節點 發現硬體、設定介面、管理 VF 建立

Resource Injector 值得記:使用者只寫網路 annotation,資源請求是 webhook 補上去的。

新路:DRA

DraNet 是一個 Kubernetes 網路驅動,用 DRA 提供高效能網路。它透過 DRA API 和 kubelet 溝通,透過 NRI 和 container runtime 溝通;Pod 的 network namespace 一建立,runtime 就用 gRPC 呼叫它去做網路設定。

這個設計有一個重要後果:它和既有的 CNI plugin 完全相容,不取代 CNI。Pod 原本那張 eth0 還是 CNI 給的。

一張網卡在 ResourceSlice 裡長什麼樣

- basic:
    attributes:
      dra.net/ifName:      { string: gpu7rdma0 }
      dra.net/mac:         { string: 9a:41:2e:4f:86:16 }
      dra.net/mtu:         { int: 8896 }
      dra.net/numaNode:    { int: 1 }
      resource.kubernetes.io/numaNode: { int: 1 }
      dra.net/pciAddressBus:      { string: c8 }
      dra.net/pciAddressDevice:   { string: "00" }
      dra.net/pciAddressDomain:   { string: "0000" }
      dra.net/pciAddressFunction: { string: "0" }
      dra.net/pciVendor:   { string: Mellanox Technologies }
      dra.net/rdma:        { bool: true }
      dra.net/sriov:       { bool: false }
      dra.net/state:       { string: up }
      dra.net/type:        { string: device }
      dra.net/virtual:     { bool: false }
  name: gpu7rdma0

對照一下:device plugin 那邊,一張 RDMA 網卡是 rdma/hca_shared_devices_a: 1,一個整數;這邊是十幾個帶型別的屬性,包含它是不是 RDMA、是不是 VF、在哪個 NUMA、PCI 位址的四段。

挑 RDMA 網卡就是一行 CEL:

- cel:
    expression: device.attributes["dra.net"].rdma == true

那條 PCIe root 限制,變成一行約束

前面 GPUDirect RDMA 那個「網卡和 GPU 最好在同一個 PCIe root complex 底下」,在 DRA 裡長這樣:

requests:
- name: gpu
  exactly: { deviceClassName: gpu.nvidia.com, count: 1 }
- name: efa
  exactly: { deviceClassName: efa.networking.k8s.aws, count: 1 }
constraints:
- requests: [gpu, efa]
  matchAttribute: resource.kubernetes.io/pcieRoot

一行 matchAttribute,把硬體的拓撲限制變成宣告式的約束。

這行值多少?DraNet 自己量過,同樣的工作負載對齊之後,all-gather 和 all-reduce 的 busbw 都快了將近六成(約 29 GB/s 提升到約 46 GB/s)。前面說「不同 root 效能會掉」,這就是掉多少。(這是專案自己的數據,不是中立的第三方量測。)

一個還沒解決的問題

matchAttribute 要求兩邊的屬性名一模一樣。但現在不同的 DRA driver 各用各的名字在發布 NUMA 資訊:NVIDIA 的 GPU 叫 numa、AMD 叫 numaNode、DraNet 叫 dra.net/numaNode、CPU 那個 driver 又叫 dra.cpu/numaNodeID。

名字對不上,就寫不出跨 driver 的對齊約束。上游正在推 KEP-6072 把這件事標準化,讓大家都用 resource.kubernetes.io/numaNode。


小結

今天講的其實就是一件事:兩張卡離多遠,決定了資料怎麼走。

同一台機器裡,有 NVLink 就走 NVLink,沒有就走 PCIe,再遠一點就得繞 CPU。NCCL 會照這個順序往下試,走得通哪條就走哪條。跨出機器之後只剩網路,而 IB 和 RoCE 就是把網路做快的兩種方式。

實驗跑出來 1.224 GB/s,這是最慢的那條路。日誌裡還有一行很值得記:它先試了 IB,開不起來,然後默默改走 TCP,什麼都沒說。

至於 Kubernetes 這邊,網卡的處境跟 GPU 一模一樣。device plugin 只能給你一個數字,DRA 能告訴你這張卡是不是 RDMA、在哪個 NUMA、PCI 位址是什麼。有了這些,才寫得出「GPU 和網卡要在同一個 PCIe root 底下」這種要求。


參考資料

NVIDIA — NVLink & NVLink Switch
NCCL — Environment Variables
NCCL — Logging
NVIDIA/nccl-tests、Performance
NVIDIA — InfiniBand FAQ
NVIDIA MLNX-OS — Subnet Manager
NVIDIA — RDMA over Converged Ethernet (RoCE)
NVIDIA GPU Operator — GPUDirect RDMA and GPUDirect Storage
NVIDIA — GPUDirect RDMA User Manual
NVIDIA GPU Operator — DRA Driver
NVIDIA/k8s-dra-driver-gpu
NVIDIA Topograph — Node Labels and Annotations
k8snetworkplumbingwg/multus-cni、Device resource assignment
Mellanox/k8s-rdma-shared-dev-plugin
k8snetworkplumbingwg/sriov-network-operator
kubernetes-sigs/dranet、文件站
KEP-6072 — DRA Standard numaNode Attribute


上一篇
【Day 13】Slinky:當 HPC 的老將 Slurm 走進 Kubernetes
下一篇
【Day 15】Kubernetes 上的 vLLM:從 KV cache 到 prefill 與 decode
系列文
凌晨四點,女友帶著 GPU 來我家學習 Kubernetes:打造 K8s AI Infra 的 30 夜 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言