The launch arguments had been read from documentation and never from an
engine. Now they have been:
vllm 0.29.0 --host --port --tensor-parallel-size --served-model-name
--distributed-executor-backend all accepted
llama.cpp b10991 --model --host --port --metrics --alias --parallel
--n-gpu-layers all accepted
Getting there found three faults in the checker rather than in llmgw.
It checked every command's flags against the engine's help, but a shard can be
several programs — vLLM under Ray is `ray start` and then `vllm serve` — and
they have different vocabularies. It reported --head and --num-gpus as
missing, which is true of vllm and irrelevant, because ray is what receives
them. Each command is now checked against its own help, and one that cannot be
run is reported as skipped rather than failed.
vLLM does not give up its whole help for --help: it prints a summary of
section names and expects --help=all. Checked against the summary, all five
flags looked missing. The warning about truncated help did not fire, because
the summary happens to mention --help= while listing how to ask for more.
And vLLM refuses to print anything at all on a machine with no accelerator
unless VLLM_TARGET_DEVICE says what to assume. Asking an engine about its
flags should not require the hardware it runs on.
Two things remain unchecked and are written down as such: the Ray sidecar,
because vLLM 0.29 does not install Ray, and SGLang, because it is not
installed anywhere. `launch_mode: mp` avoids Ray entirely and is worth
preferring until someone checks it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
252 lines
7.8 KiB
Go
252 lines
7.8 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"gitea.mynodes.xyz/ilker/llmgw/internal/agent/engines"
|
|
)
|
|
|
|
func newEngineCheckCmd() *cobra.Command {
|
|
var (
|
|
help string
|
|
bin string
|
|
nodes int
|
|
tp int
|
|
perNode int
|
|
)
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "engine-check <engine>",
|
|
Short: "Check that an engine still accepts the flags llmgw builds for it",
|
|
Long: "llmgw's launch arguments are read from each engine's documentation, and\n" +
|
|
"engines rename and remove flags between versions. When that happens the\n" +
|
|
"failure arrives minutes into a model load, in an engine's own error\n" +
|
|
"message, on a node nobody was watching.\n\n" +
|
|
"This asks the engine itself. It runs the engine's --help and checks every\n" +
|
|
"flag llmgw would pass against it, which needs no GPU — a CLI prints its\n" +
|
|
"help on any machine. Run it after upgrading an engine.\n\n" +
|
|
"With --help-output it reads a saved --help instead of running anything,\n" +
|
|
"so the check works from a laptop against help captured on a node.",
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
name := args[0]
|
|
reg := engines.NewRegistry(engines.Config{})
|
|
eng, err := reg.Get(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// The binary lives where the agent's configuration says, which is
|
|
// rarely on PATH: a check that cannot find the engine reports
|
|
// nothing rather than reporting a problem.
|
|
cfg := engines.Config{}
|
|
if bin != "" {
|
|
switch name {
|
|
case "llamacpp":
|
|
cfg.LlamaCpp.Bin = bin
|
|
case "vllm":
|
|
cfg.VLLM.Bin = bin
|
|
case "sglang":
|
|
cfg.SGLang.Python = bin
|
|
case "sim":
|
|
cfg.Sim.Bin = bin
|
|
}
|
|
reg = engines.NewRegistry(cfg)
|
|
if eng, err = reg.Get(name); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
spec := representativeShard(name, nodes, tp, perNode)
|
|
cmds, err := eng.Commands(spec)
|
|
if err != nil {
|
|
return fmt.Errorf("building a command for %s: %w", name, err)
|
|
}
|
|
|
|
out, errOut := cmd.OutOrStdout(), cmd.ErrOrStderr()
|
|
var checked, skipped int
|
|
var unknown []string
|
|
|
|
// Per command, and that distinction is the whole point. A shard can
|
|
// be several processes — vLLM under Ray is `ray start` and then
|
|
// `vllm serve` — and they are different programs with different
|
|
// vocabularies. Checking the sidecar's flags against the engine's
|
|
// help reports --head and --num-gpus as missing, which is true of
|
|
// vllm and irrelevant, because ray is what receives them.
|
|
for i, c := range cmds {
|
|
fmt.Fprintf(errOut, " %s %s\n", c.Path, strings.Join(c.Args, " "))
|
|
|
|
text := ""
|
|
switch {
|
|
case help != "" && i == len(cmds)-1:
|
|
// A supplied help belongs to the engine, which is the last
|
|
// command; the sidecars have their own and we do not have it.
|
|
text = help
|
|
case help != "":
|
|
skipped++
|
|
fmt.Fprintf(errOut, " skipped: --help-output describes the engine, not %s\n", c.Name)
|
|
continue
|
|
default:
|
|
var err error
|
|
if text, err = runHelp(cmd.Context(), c); err != nil {
|
|
skipped++
|
|
fmt.Fprintf(errOut, " skipped: %v\n", err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// A truncated help produces confident nonsense: every flag below
|
|
// the cut looks missing. Real help documents --help itself,
|
|
// which is the cheapest signal that we have all of it.
|
|
if !mentions(text, "--help") {
|
|
fmt.Fprintf(errOut, " warning: this help does not document --help, "+
|
|
"which usually means it was truncated; flags below the cut will look missing\n")
|
|
}
|
|
|
|
seen := map[string]bool{}
|
|
for _, a := range c.Args {
|
|
flag, _, _ := strings.Cut(a, "=")
|
|
if !strings.HasPrefix(flag, "--") || seen[flag] {
|
|
continue
|
|
}
|
|
seen[flag] = true
|
|
checked++
|
|
if !mentions(text, flag) {
|
|
unknown = append(unknown, c.Name+": "+flag)
|
|
}
|
|
}
|
|
}
|
|
sort.Strings(unknown)
|
|
|
|
fmt.Fprintf(out, "%s: %d flags checked", name, checked)
|
|
if skipped > 0 {
|
|
fmt.Fprintf(out, ", %d commands skipped", skipped)
|
|
}
|
|
fmt.Fprintln(out)
|
|
|
|
if len(unknown) == 0 {
|
|
if checked == 0 {
|
|
return fmt.Errorf("nothing was checked; install the engine here or pass --help-output")
|
|
}
|
|
fmt.Fprintln(out, "every flag llmgw builds is accepted")
|
|
return nil
|
|
}
|
|
fmt.Fprintf(out, "\n%d flags not mentioned by the program they are passed to:\n", len(unknown))
|
|
for _, f := range unknown {
|
|
fmt.Fprintf(out, " %s\n", f)
|
|
}
|
|
return fmt.Errorf("%s: %d flags are not accepted", name, len(unknown))
|
|
},
|
|
}
|
|
|
|
f := cmd.Flags()
|
|
f.StringVar(&help, "help-output", "", "a saved --help to check against, instead of running the engine")
|
|
f.StringVar(&bin, "bin", "", "where the engine lives, when it is not on PATH")
|
|
f.IntVar(&nodes, "nodes", 2, "shape the command for a replica of this many nodes")
|
|
f.IntVar(&tp, "tp", 8, "tensor parallel degree")
|
|
f.IntVar(&perNode, "gpus-per-node", 4, "GPUs each node contributes")
|
|
return cmd
|
|
}
|
|
|
|
// representativeShard is the widest command llmgw would ever build, because a
|
|
// flag that only appears in the multi-node case is exactly the one nobody
|
|
// notices has been renamed.
|
|
func representativeShard(engine string, nodes, tp, perNode int) engines.ShardSpec {
|
|
if nodes < 1 {
|
|
nodes = 1
|
|
}
|
|
return engines.ShardSpec{
|
|
ReplicaID: "check", ShardID: "check-r0", Rank: 0, NNodes: nodes,
|
|
LeaderAddr: "10.0.0.1", LeaderPort: 20001,
|
|
GPUIndices: seq(perNode), HCAs: []string{"mlx5_0"},
|
|
MgmtInterface: "eth0", HTTPPort: 20000,
|
|
Profile: engines.Profile{
|
|
Name: "check", Engine: engine, TP: tp, Nodes: nodes, GPUsPerNode: perNode,
|
|
ModelPath: "/models/x", ServedModelName: "x",
|
|
},
|
|
}
|
|
}
|
|
|
|
func seq(n int) []int {
|
|
out := make([]int, n)
|
|
for i := range out {
|
|
out[i] = i
|
|
}
|
|
return out
|
|
}
|
|
|
|
// helpArgs is how each program is asked for its *complete* help.
|
|
//
|
|
// Not every program gives it all up for --help. vLLM prints a summary listing
|
|
// section names and tells you to ask again per section; checking against that
|
|
// reports every flag as missing, confidently and wrongly. This is exactly what
|
|
// happened the first time this ran against a real vLLM.
|
|
var helpArgs = map[string][]string{
|
|
"vllm": {"--help=all"},
|
|
}
|
|
|
|
// runHelp asks one program to describe itself.
|
|
func runHelp(ctx context.Context, c engines.Command) (string, error) {
|
|
// Everything up to the first flag, plus the help request. Passing the real
|
|
// arguments would start the engine.
|
|
var args []string
|
|
for _, a := range c.Args {
|
|
if strings.HasPrefix(a, "-") {
|
|
break
|
|
}
|
|
args = append(args, a)
|
|
}
|
|
ask, ok := helpArgs[filepath.Base(c.Path)]
|
|
if !ok {
|
|
ask = []string{"--help"}
|
|
}
|
|
args = append(args, ask...)
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
|
defer cancel()
|
|
|
|
ec := exec.CommandContext(ctx, c.Path, args...)
|
|
// vLLM refuses to do anything at all on a machine with no accelerator,
|
|
// including print its help, unless it is told what to assume. Asking an
|
|
// engine about its flags should not require the hardware it runs on.
|
|
ec.Env = append(ec.Environ(), "VLLM_TARGET_DEVICE=cpu")
|
|
|
|
raw, err := ec.CombinedOutput()
|
|
if err != nil && len(raw) == 0 {
|
|
return "", fmt.Errorf("could not run %s: %w (install it here, or pass --help-output)",
|
|
c.Path, err)
|
|
}
|
|
return string(raw), nil
|
|
}
|
|
|
|
// mentions reports whether the help text documents a flag.
|
|
//
|
|
// Matched on a word boundary, so --port does not satisfy --port-range and a
|
|
// missing flag cannot hide behind a longer one that happens to contain it.
|
|
func mentions(help, flag string) bool {
|
|
for i := 0; ; {
|
|
j := strings.Index(help[i:], flag)
|
|
if j < 0 {
|
|
return false
|
|
}
|
|
end := i + j + len(flag)
|
|
if end == len(help) || !isFlagChar(help[end]) {
|
|
return true
|
|
}
|
|
i = end
|
|
}
|
|
}
|
|
|
|
func isFlagChar(b byte) bool {
|
|
return b == '-' || b == '_' || (b >= 'a' && b <= 'z') ||
|
|
(b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
|
|
}
|