Files
llmgw/internal/agent/discovery/gpu_fabric.go
T
Ilker ManapandClaude Opus 5.5 20479a2d5f Find multi-node NVLink domains, place across them, and show them as racks
A GB200 NVL72 rack or an NVLink Switch system is a scale-up island: its
GPUs reach each other over NVLink whatever node they are in. Each GPU
now reports its fabric from nvidia-smi -q - state, status, ClusterUUID,
CliqueId, bandwidth - at registration and once a minute after, since a
fabric manager restarting drops every card out at once; and each node
reports nvidia-imex and its channels, without which the domain's GPUs
cannot share memory across nodes. A single machine's NVSwitch, which
reports a cluster of zeros, is no domain.

A replica spanning nodes is placed inside one domain first, on nodes
whose every card is joined and whose IMEX is up, and its shards are
told NCCL_MNNVL_ENABLE=1; InfiniBand is the fallback.

/admin/nvlink draws each domain as a rack - the NVLink Switch band on
top, light running down a backplane, trays of GPU chips that glow when
joined, pulse red when not, turn amber on degraded bandwidth - and
lists what keeps a tray out. A node's page shows its GPU-by-GPU NVLink
matrix, each card's NUMA node and nearest adapter, and its domain and
IMEX. doctor checks joining, bandwidth, IMEX and its channel; metrics
llmgw_gpu_nvlink_joined and llmgw_imex_up; /api/v1/nvlink.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 09:07:59 +02:00

161 lines
4.6 KiB
Go

package discovery
import (
"context"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
// GPUFabric is a GPU's place in a multi-node NVLink domain, as the driver
// reports it: the NVL72 rack, or the NVLink Switch system, it belongs to.
type GPUFabric struct {
// State is Completed once the fabric manager has joined the GPU to its
// domain; In Progress, Not Started or Not Supported otherwise.
State string `json:"state"`
// Status is Success, or the reason the join failed.
Status string `json:"status,omitempty"`
// ClusterUUID and CliqueID name the domain: GPUs sharing both can reach
// each other over NVLink whatever node they are in.
ClusterUUID string `json:"cluster_uuid,omitempty"`
CliqueID string `json:"clique_id,omitempty"`
// Bandwidth is Full, or how the domain's NVLink is degraded.
Bandwidth string `json:"bandwidth,omitempty"`
}
// Domain is the NVLink domain the GPU is joined to, empty when it is in none
// that crosses nodes: a card with no fabric, one not joined, or one whose
// cluster is all zeros, which is what a single machine's NVSwitch reports.
func (f *GPUFabric) Domain() string {
if f == nil || !strings.EqualFold(f.State, "Completed") {
return ""
}
u := strings.ToLower(strings.TrimSpace(f.ClusterUUID))
if u == "" || strings.Trim(u, "0-") == "" {
return ""
}
return u + "/" + f.CliqueID
}
// ParseGPUFabric reads the Fabric sections of `nvidia-smi -q`, by the PCI
// address each GPU's section starts with.
func ParseGPUFabric(out string) map[string]GPUFabric {
res := map[string]GPUFabric{}
var bus string
fabricIndent := -1
var cur *GPUFabric
flush := func() {
if bus != "" && cur != nil && cur.State != "" {
res[bus] = *cur
}
cur, fabricIndent = nil, -1
}
for _, line := range strings.Split(out, "\n") {
trim := strings.TrimSpace(line)
indent := len(line) - len(strings.TrimLeft(line, " \t"))
if strings.HasPrefix(line, "GPU ") && indent == 0 {
flush()
bus = strings.ToLower(strings.TrimSpace(strings.TrimPrefix(line, "GPU ")))
continue
}
if trim == "Fabric" {
flush()
cur, fabricIndent = &GPUFabric{}, indent
continue
}
if cur == nil {
continue
}
if trim != "" && indent <= fabricIndent {
flush()
continue
}
k, v, ok := strings.Cut(trim, ":")
if !ok {
continue
}
k, v = strings.TrimSpace(k), strings.TrimSpace(v)
if strings.EqualFold(v, "N/A") {
v = ""
}
switch k {
case "State":
cur.State = v
case "Status":
cur.Status = v
case "ClusterUUID", "Cluster UUID":
cur.ClusterUUID = v
case "CliqueId", "Clique ID":
cur.CliqueID = v
case "Bandwidth":
cur.Bandwidth = v
}
}
flush()
return res
}
// DiscoverGPUFabric runs `nvidia-smi -q` for the Fabric sections. Nothing
// is reported on a driver or a card without them.
func DiscoverGPUFabric(ctx context.Context, binary string) (map[string]GPUFabric, error) {
if binary == "" {
if _, err := exec.LookPath("nvidia-smi"); err != nil {
return nil, nil
}
binary = "nvidia-smi"
}
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, binary, "-q").Output()
if err != nil {
return nil, err
}
return ParseGPUFabric(string(out)), nil
}
// ApplyFabric puts each GPU's fabric on it, matched by PCI address.
func ApplyFabric(gpus []GPU, fab map[string]GPUFabric) {
for i := range gpus {
if f, ok := fab[strings.ToLower(gpus[i].PCIBusID)]; ok {
f := f
gpus[i].Fabric = &f
}
}
}
// IMEX is the service that lets GPUs in different nodes of one NVLink
// domain share memory: without it, multi-node NVLink carries nothing.
type IMEX struct {
// State is systemd's word for nvidia-imex: active, inactive, or empty
// where it is not installed.
State string `json:"state,omitempty"`
// Channels is how many IMEX channels exist; a process needs one.
Channels int `json:"channels"`
}
// DiscoverIMEX reads the IMEX service and its channels.
func DiscoverIMEX(ctx context.Context) IMEX {
var m IMEX
if entries, err := os.ReadDir("/dev/nvidia-caps-imex-channels"); err == nil {
m.Channels = len(entries)
}
if _, err := exec.LookPath("systemctl"); err == nil {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
out, _ := exec.CommandContext(ctx, "systemctl", "is-active", "nvidia-imex").Output()
m.State = strings.TrimSpace(string(out))
if m.State == "inactive" {
// "inactive" is also what systemd says of a unit that does not
// exist; the binary says which.
if _, err := exec.LookPath("nvidia-imex"); err != nil {
if _, err := os.Stat(filepath.Join("/usr/bin", "nvidia-imex")); err != nil {
m.State = ""
}
}
}
}
return m
}