Files
llmgw/internal/cli/agent.go
T
ilkerandClaude Opus 5 55417546fc Phase 2 (2/2): NVML backend behind a build tag
NVML is the only source of NVLink topology, and that topology is what keeps a
tensor-parallel group off the slow path inside a node. It needs cgo, so it sits
behind the nvml tag and the default build keeps using nvidia-smi.

Discover() picks the backend and falls back to nvidia-smi when NVML fails,
logging the fallback rather than performing it silently: on a node that is meant
to have working NVML, a quiet downgrade hides a broken driver installation and
costs the scheduler its topology.

CI now compiles the tagged build. That immediately paid for itself: ErrNoNVML
was declared only in the untagged file, so the tests did not compile under
-tags nvml. It is now declared where both builds can see it.

Not verified: the NVML path has never run against a driver, because there is no
GPU machine available. The NVLink domains it reports should be treated as
unverified until someone runs `llmgw agent inventory` on a real node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 02:05:25 +02:00

127 lines
4.0 KiB
Go

package cli
import (
"encoding/json"
"fmt"
"os"
"text/tabwriter"
"github.com/spf13/cobra"
"gitea.mynodes.xyz/ilker/llmgw/internal/agent/discovery"
)
func newAgentCmd() *cobra.Command {
var configPath string
cmd := &cobra.Command{
Use: "agent",
Short: "Node agent: discovery and engine supervision (phase 2)",
Long: "Runs on every GPU node. It discovers the hardware, starts and supervises\n" +
"inference engines, and reports to the control plane.\n\n" +
"The control plane connection is not built yet, so for now the useful\n" +
"subcommand is `agent inventory`.",
RunE: func(_ *cobra.Command, _ []string) error {
return errNotImplemented
},
}
cmd.Flags().StringVar(&configPath, "config", "agent.yaml", "configuration file")
cmd.AddCommand(newInventoryCmd())
return cmd
}
func newInventoryCmd() *cobra.Command {
var (
asJSON bool
smiBin string
ibRoot string
skipGPU bool
useNVML bool
)
cmd := &cobra.Command{
Use: "inventory",
Short: "Show this node's GPUs and InfiniBand adapters",
Long: "Reports what the scheduler would see on this node: GPU classes, health,\n" +
"and the fabric each adapter is attached to.\n\n" +
"Run it before adding a node to the cluster — it is the fastest way to find\n" +
"a card another process is already using, or a port with no subnet manager.",
RunE: func(cmd *cobra.Command, _ []string) error {
inv, err := discovery.Discover(cmd.Context(), discovery.Options{
NvidiaSMI: smiBin,
IBRoot: ibRoot,
PreferNVML: useNVML,
SkipGPUs: skipGPU,
}, logger())
if err != nil {
// Not fatal: a node whose GPUs cannot be read still has IB
// worth showing, and the error is itself the useful output.
fmt.Fprintf(os.Stderr, "%v\n", err)
inv.Source = "unavailable"
}
if asJSON {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(inv)
}
printInventory(inv)
return nil
},
}
f := cmd.Flags()
f.BoolVar(&asJSON, "json", false, "emit JSON")
f.StringVar(&smiBin, "nvidia-smi", "", "path to nvidia-smi")
f.StringVar(&ibRoot, "ib-root", discovery.SysfsRoot, "sysfs InfiniBand directory")
f.BoolVar(&skipGPU, "no-gpu", false, "skip GPU discovery")
f.BoolVar(&useNVML, "nvml", true, "prefer the NVML backend when this build has one (it is the only source of NVLink topology)")
return cmd
}
func printInventory(inv discovery.Inventory) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "GPUs (%d, via %s)\n", len(inv.GPUs), inv.Source)
if len(inv.GPUs) == 0 {
fmt.Fprintln(w, " none")
} else {
fmt.Fprintln(w, " IDX\tCLASS\tNAME\tMEM\tCC\tHEALTH")
for _, g := range inv.GPUs {
health := "ok"
if !g.Health.OK() {
health = g.Health.Problems[0]
}
fmt.Fprintf(w, " %d\t%s\t%s\t%d MiB\t%s\t%s\n",
g.Index, g.Class(), g.Name, g.MemoryTotalMB, g.ComputeCapability, health)
}
fmt.Fprintf(w, " classes: %v\n", inv.Classes())
if !discovery.NVMLSupported() {
fmt.Fprintln(w, " NVLink topology unavailable: rebuild with -tags nvml.")
fmt.Fprintln(w, " Without it, placement cannot keep a tensor-parallel group inside")
fmt.Fprintln(w, " one NVLink domain (plan §6.2).")
}
}
fmt.Fprintf(w, "\nInfiniBand adapters (%d)\n", len(inv.HCAs))
if len(inv.HCAs) == 0 {
// Worth stating plainly: such a node is its own single-node island and
// can never host a multi-node replica.
fmt.Fprintln(w, " none — this node cannot host multi-node replicas")
} else {
fmt.Fprintln(w, " HCA\tPORT\tSTATE\tRATE\tLINK\tGID PREFIX\tNUMA")
for _, h := range inv.HCAs {
for _, p := range h.Ports {
fmt.Fprintf(w, " %s\t%d\t%s\t%d Gb/s\t%s\t%s\t%d\n",
h.Name, p.Num, p.State, p.RateGbps, p.LinkLayer, p.GIDPrefix, h.NUMANode)
}
}
fmt.Fprintln(w, "\n Fabric identity is not derived from the GID prefix by default:")
fmt.Fprintln(w, " OpenSM's default is fe80:: on every fabric, so a shared prefix proves")
fmt.Fprintln(w, " nothing. Label fabrics explicitly in agent.yaml (plan §5.2).")
}
_ = w.Flush()
}