Explorar o código

docs: rewrite README to match current implementation (sm121 binary, all commands, serve flags, recipe fields, mikrotik/registry config, build flow)

Alexander Korolev hai 1 mes
pai
achega
ecf4102e7e
Modificáronse 1 ficheiros con 155 adicións e 166 borrados
  1. 155 166
      README.md

+ 155 - 166
README.md

@@ -1,241 +1,230 @@
-# sm121-launcher
+# sm121
 
-Launch vLLM / SGLang inference clusters across DGX Spark machines via SSH.
+Launch vLLM / SGLang inference clusters across a DGX Spark fleet over SSH.
+Docker is the default deployment target; tmux is a first-class alternative
+(`deploy = "tmux"` in the recipe).
 
-## Quick Install
+## Install
 
 ```bash
-# From source
 cargo install --path .
-
-# Or download a pre-built binary (requires Rust toolchain to build)
-cargo build --release
-# Binary at target/release/sm121-launcher
+# or: cargo build --release   → target/release/sm121
 ```
 
-## Quick Start
+## Quick start
 
 ```bash
-# Create config.toml (see Configuration below)
-# Launch a recipe
-sm121-launcher serve --recipe deepseek-v4-flash
-
-# Follow logs
-sm121-launcher logs --recipe deepseek-v4-flash --follow
-
-# Stop containers
-sm121-launcher stop --recipe deepseek-v4-flash
+sm121 config init-global            # create the global config if missing
+sm121 -d serve --recipe deepseek-v4-flash   # dry-run: print the commands
+sm121 serve --recipe deepseek-v4-flash      # launch (waits for the engine)
+sm121 logs --recipe deepseek-v4-flash --follow
+sm121 bench --recipe deepseek-v4-flash --label smoke   # tool-eval-bench
+sm121 stop --recipe deepseek-v4-flash  # stops ALL nodes, verifies each one
+sm121 health                          # cluster-level health (nodes + switches)
 ```
 
+`-d`/`--dry-run` and `-c/--config <path>` are top-level flags:
+`sm121 -d serve ...`, `sm121 -c /other/config.toml serve ...`.
+
 ## Commands
 
 | Command | Description |
 |---------|-------------|
-| `serve --recipe <name>` | Launch recipe on cluster |
-| `logs --recipe <name> [--follow]` | Show container logs |
-| `stop --recipe <name>` | Stop and remove containers |
-| `health [--cluster <name>]` | Check cluster health |
+| `serve --recipe <name>` | Launch; records the run in the ledger, waits for the engine (`/v1/models`, up to 30 min; `--no-wait` skips) |
+| `stop --recipe <name>` | Stop **all** nodes in parallel, verify each (60 s poll); `--no-wait` opts out |
+| `logs --recipe <name> [--follow]` | Container logs; follow = liveness watch (30 s probes, 2 misses = died) |
+| `bench --recipe <name>` | `tool-eval-bench` against a live serve; reports auto-land in `recipes/<lane>/reports/` (+ run package: recipe copy + manifest) |
+| `report [--serve]` | Table of tested recipes; `--serve` = interactive HTML (SSE, in-TOML navigation) |
+| `health [--cluster <name>]` | Cluster-level health: UP only when all nodes reachable **and** blocking extras (HA switches, MikroTik routers) are on |
 | `doctor --recipe <name> [--repair]` | Check/repair node prerequisites |
-| `exec --cluster <name> -- <command>` | Run command on cluster nodes |
-| `exec --cluster <name> --alias <name>` | Run predefined alias |
-| `power off --cluster <name> [--node <name>]` | Shutdown + HA turn_off |
-| `power on --cluster <name> [--node <name>]` | HA turn_on (wake nodes) |
-| `power switch --from <src> --to <tgt>` | Switch clusters |
-| `tmux serve --recipe <name>` | Launch via tmux (no Docker) |
-| `tmux status --recipe <name>` | Check tmux session health |
-| `tmux logs --recipe <name>` | Capture tmux pane output |
-| `init-host-keys` | Auto-write SSH host keys |
-| `-d` / `--dry-run` | Preview commands without executing |
-
-### Cluster selection
+| `exec --cluster <name> -- <cmd>` | Run a shell command on cluster nodes; `--alias <name>` runs a predefined one |
+| `build --recipe <name> --build-node <n>` | Build the recipe's image from `docker-images/<dir>/` on a node (clone-at-pin) |
+| `publish --recipe <name> [--push]` | Self-contained recipe+image bundle; `--push` also pushes the image under a versioned tag |
+| `registry` | Browse/install recipes from the public registry |
+| `runlock list / close <recipe>` | Offline run-ledger management (per-recipe `.lock`) |
+| `web` | Local dashboard (axum + SSE): recipes, topology map, LED board, jobs, serve/stop jobs |
+| `power off/on/switch` | Power management via Home Assistant + MikroTik RouterOS (see below) |
+| `tmux serve/status/logs` | tmux deployment target (no Docker) |
+| `init-host-keys` | Auto-discover SSH host keys into config |
+| `config path / init-global` | Config resolution helpers |
+| `-d` / `--dry-run` | Print instead of execute (all commands) |
+
+### `serve` flags (cluster selection & overrides)
 
 ```bash
-sm121-launcher serve --recipe deepseek-v4-flash              # auto (smallest match)
-sm121-launcher serve --recipe glm51 --cluster 4nodes-switch  # exact name
-sm121-launcher serve --recipe glm51 --cluster 4             # prefix match
-sm121-launcher serve --recipe glm51 --cluster 2             # picks 2nodes-dac
+sm121 serve --recipe R --cluster auto            # smallest cluster that fits
+sm121 serve --recipe R --cluster 4               # exact name, prefix, or auto
+sm121 serve --recipe R --solo                    # first free node, single-node run
+sm121 serve --recipe R --force                   # skip occupancy check (co-serve)
+sm121 serve --recipe R --fit=2                   # scale to N nodes, TP tracks
+sm121 serve --recipe R --mirror                  # per-node local HF mirror (no NAS at load)
+sm121 serve --recipe R --clear                   # rm -rf host extra_mounts dirs first
+sm121 serve --recipe R --build-node dgx1         # build the image first (if stale)
+sm121 serve --recipe R --sync-image              # push the image to launch nodes first
 ```
 
-Prefix must be unambiguous — error if multiple clusters match.
+Cluster selection: exact name, unambiguous prefix (`4` → `4nodes-switch`), or
+`auto` (smallest cluster with NCCL config + matching `required_nodes`).
 
 ## Recipes
 
-Recipes are TOML files in `recipes/<name>/recipe.toml`, resolved relative
-to the current directory (then `~/.config/sm121-launcher/recipes/`).
-This repo is the source of truth: a recipe and its image build context
-(`docker-images/<dir>/` — Dockerfile, patches, version pins) are
-versioned together, since the served image must match the recipe's
-revision exactly. `sm121 publish` flattens both into a self-contained
-bundle; the registry data repo <https://git.kilork.org/sm121/recipes>
-stores those published bundles (discovery/sharing artifact, never the
-source).
+Recipes are TOML: `recipes/<name>/recipe.toml`, resolved from `./recipes`
+first, then each `recipe_sources` entry in the config. This repo is the
+source of truth: a recipe and its image build context (`docker-images/<dir>/`)
+are versioned together — the served image must match the recipe revision.
+`sm121 publish` flattens both into a self-contained bundle; the public
+registry <https://git.kilork.org/sm121/recipes> stores those bundles only.
 
-### Creating a recipe
+### Core fields
 
 ```toml
-engine = "vllm"                        # or "sglang" (default: "vllm")
-name = "My Model"
-docker_image = "my-image:tag"
+engine = "vllm"                        # or "sglang"
+name = "My Model"                      # display name
+docker_image = "jovian-judgement:r35-glm53"
+docker_image_dir = "jovian-judgement"  # build dir under docker-images/ (default: recipe name)
 model = "org/model-name"
-required_nodes = 2                     # optional
-tensor_parallel_size = 2               # TP across nodes
-pipeline_parallel_size = 1             # vLLM only, 0 = auto (equals nnodes)
+parent_model = "org/base-model"        # optional; report grouping
+port = 30000
+required_nodes = 2                     # optional; also caps the launch set
+solo = false                           # optional; single-node on first free node
+deploy = "docker"                      # or "tmux"
+tensor_parallel_size = 2
+pipeline_parallel_size = 1             # vLLM only, 0 = auto
 distributed_backend = "mp"             # "mp" (default) or "ray"
-shm_size = "64gb"                      # optional, only added when set
+shm_size = "64gb"                      # optional
+entrypoint = ["vllm", "serve"]         # optional; used by docker and tmux
 
-entrypoint = ["vllm", "serve"]         # optional, appended after image
+args = ["--kv-cache-dtype", "fp8"]     # engine args, passed through verbatim
 
-args = [
-    "--kv-cache-dtype", "fp8",
-    "--max-model-len", "32768",
-]
+[env]
+KEY = "value"                          # wins over launcher defaults (last -e wins)
+PORT = { from = "port" }               # value resolved from a recipe field at serve time
 
-before_serve = [                       # recipe-level pre-serve hooks
-    { command = "echo ready", on_failure = "abort" },
+before_serve = [
+    "sudo nvidia-smi -lgc",            # bare string = raw command
+    { command = "echo hi", on_failure = "warn" },
+    { script = "stop_dgx_services" },  # built-in host script alias (embedded in binary)
 ]
-
-[env]
-VLLM_LOGGING_COLOR = "1"              # override any launcher default
+after_stop = [{ script = "restore_dgx_services" }]
 ```
 
-## Configuration
+Full spec — `extra_mounts` (aliases, HF `repo` path refs, `local_files`
+hardlinks), `composed_model`, `[mods]` (in-container patches before the
+engine), `venv`/`venv_setup` (tmux), `build_profile` + `[build.versions]`
+(image pin overrides) — is in [`AGENTS.md`](AGENTS.md#recipe-format-recipesnamerecipetoml).
 
-The launcher reads `config.toml` from the current directory,
-falling back to `~/.config/sm121-launcher/config.toml`.
+## Image builds
 
-### Minimal
+A recipe's `docker_image` is built **on demand** (`build` or
+`serve --build-node`): the image dir is uploaded to the node, built in a tmux
+session, and stamped with a `sm121.checksum` label (recursive SHA-256 of the
+dir); later serves skip the build when the label matches. Builds are
+clone-at-pin: every input is a pinned `KEY=VAL` in `versions.env`, staged by
+`fetch-deps.sh` and verified against the committed `locks/deps.lock`
+(commit SHAs / gitlink SHAs / blob fingerprint) — any drift aborts the build.
 
-```toml
-ssh_user = "kilork"
-ssh_key_path = "/path/to/ssh/key"
+Pins can be overridden without editing the image dir: a `build_profile`
+references `profiles/<name>.env` in the image dir; precedence:
+`versions.env` < profile < recipe `[build.versions]`.
 
-[[clusters]]
-name = "4nodes-switch"
-master_ib_ip = "192.168.200.15"
-nodes = [
-    { name = "dgx1", ssh_ip = "10.0.0.1", ib_ip = "192.168.200.15", rank = 0 },
-    { name = "dgx2", ssh_ip = "10.0.0.2", ib_ip = "192.168.200.13", rank = 1 },
-    { name = "dgx3", ssh_ip = "10.0.0.3", ib_ip = "192.168.200.17", rank = 2 },
-    { name = "dgx4", ssh_ip = "10.0.0.4", ib_ip = "192.168.200.19", rank = 3 },
-]
-```
+With `[registry]` in the config, a successful build pushes the image under a
+versioned tag (`{base}/{image}:<checksum>`, + `:latest`); a local publication
+ledger skips duplicate pushes unless `--force`. `publish --push` pushes
+without a build. See [`AGENTS.md`](AGENTS.md#image-registry-pushregistry).
 
-### With Home Assistant (power management)
+## Configuration
+
+Config resolution: `-c/--config <path>` > `./config.toml` > the global
+config (`~/.config/sm121/config.toml` on Linux,
+`~/Library/Application Support/sm121/config.toml` on macOS —
+`sm121 config path`). The global config exists so `sm121` works from any
+cwd; it may carry no clusters of its own — `cluster_sources` files provide
+them (merged by name, file wins).
 
 ```toml
+ssh_user = "kilork"
+ssh_key_path = "/path/to/key"
+
+recipe_sources = ["/path/to/more/recipes"]    # extra recipe dirs (after ./recipes)
+cluster_sources = ["/path/to/clusters.toml"]  # extra cluster defs
+
 [[clusters]]
 name = "4nodes-switch"
-master_ib_ip = "192.168.200.15"
-extra_entities = ["switch.network_switch"]   # cluster-level switches
+master_ib_ip = "192.168.200.15"               # optional; default: rank-0 node's ib_ip
 nodes = [
     { name = "dgx1", ssh_ip = "10.0.0.1", ib_ip = "192.168.200.15", rank = 0, entity = "switch.dgx1" },
     { name = "dgx2", ssh_ip = "10.0.0.2", ib_ip = "192.168.200.13", rank = 1, entity = "switch.dgx2" },
-    { name = "dgx3", ssh_ip = "10.0.0.2", ib_ip = "192.168.200.17", rank = 2, entity = "switch.dgx3" },
-    { name = "dgx4", ssh_ip = "10.0.0.2", ib_ip = "192.168.200.19", rank = 3, entity = "switch.dgx4" },
+]
+extra_entities = [
+    "switch.network",                          # bare string = blocking (off → cluster down)
+    { entity = "switch.fan", blocks = false }, # toggled + shown, never gates up/down
 ]
 
 [nccl."4nodes-switch"]
 ib_hca = "rocep1s0f0,roceP2p1s0f0"
 socket_ifname = "enp1s0f0np0"
-ib_gid_index = 3
+ib_gid_index = 3          # optional: omit → auto-detect RoCE-v2 IPv4 GID per node at serve
 ib_disable = 0
 
 [home_assistant]
-url = "http://homeassistant.local:8123"
+url = "http://ha.lan:8123"
 token = "long-lived-access-token"
 shutdown_delay_seconds = 30
 
-[exec_aliases]
-gpu-lock = "sudo nvidia-smi -lgc 300,2300"
-shutdown = "sudo shutdown now"
-
-[before_serve."4nodes-switch"]
-commands = [
-    "sudo nvidia-smi -lgc 300,2300",
-    { command = "sudo nvidia-smi -pm 1", on_failure = "warn" },
-]
-```
-
-### NCCL config
-
-Required for each cluster. Controls InfiniBand/RoCE communication:
-
-| Field | Example | Purpose |
-|-------|---------|---------|
-| `ib_hca` | `rocep1s0f0,roceP2p1s0f0` | RDMA devices |
-| `socket_ifname` | `enp1s0f0np0` | Network interface |
-| `ib_gid_index` | `3` | RoCE GID index |
-| `ib_disable` | `0` | Keep IB enabled |
-
-## Power Management
+[mikrotik]                 # RouterOS devices probed via API (health) + graceful shutdown
+address = "192.168.88.39"
+name = "SW2-MUC"           # must match a cluster entity name
+username = "sm121"
+password = "..."
+on_entity = "switch.sw6_mt"   # optional HA relay used for power on; absent → manual reminder
 
-Requires `[home_assistant]` in config.toml.
+[registry]                 # optional: image push target (see Image builds above)
+base = "ghcr.io/kilork"
 
-```bash
-# Graceful shutdown + wait + turn off smart plugs
-sm121-launcher power off --cluster 4nodes-switch
-
-# Wake nodes via smart plugs
-sm121-launcher power on --cluster 4nodes-switch
-
-# Switch clusters (disable 4 → enable 2)
-sm121-launcher power switch --from 4 --to 2
+[exec_aliases]
+gpu-lock = "sudo nvidia-smi -lgc 300,2300"
 
-# Single node
-sm121-launcher power off --cluster 4nodes-switch --node dgx1
+[mount_aliases]            # host paths by name for recipe [[extra_mounts]]
+jit_cache = "/mnt/nas/ai/jit"
 
-# Dry-run
-sm121-launcher -d power off --cluster 4nodes-switch
+[before_serve."*"]         # global: every cluster; per-cluster key supplements
+commands = ["sudo nvidia-smi -lgc 0,2000"]
 ```
 
-**Power-off flow**: SSH `sudo shutdown now` → wait `shutdown_delay_seconds` → HA `turn_off` entity → HA `turn_off` cluster extras.
+## Power management
 
-**Power-on flow**: HA `turn_on` (no SSH, assumes smart plug wakes the machine).
+`power off`: SSH `sudo shutdown now` → wait `shutdown_delay_seconds` →
+HA `turn_off` per-node entities + cluster extras; MikroTik devices get a
+graceful RouterOS `system shutdown` instead of a plain relay cut (and are
+probed by `health` via a RouterOS API login — a dead router takes the
+cluster down like any dead switch). `power on`: HA `turn_on` (no SSH;
+MikroTik powers up through its `on_entity` relay, or a manual reminder).
+`power switch --from A --to B`: disable only what `A` has, enable all of `B`.
 
-**Switch flow**: disable nodes/extras only in source → enable all in target.
+> ⚠️ A cluster may be serving live traffic (possibly the model serving this
+> very session). Always `health` and confirm before `stop` / `power off`.
 
-## Environment Variables
+## Web dashboard
 
-The launcher sets these automatically. Recipe `[env]` overrides any.
-
-### Auto-set (always)
-
-| Env var | Source | Description |
-|---------|--------|-------------|
-| `NCCL_IB_DISABLE` | `nccl.<name>.ib_disable` | InfiniBand on/off |
-| `NCCL_IB_HCA` | `nccl.<name>.ib_hca` | RDMA devices |
-| `NCCL_SOCKET_IFNAME` | `nccl.<name>.socket_ifname` | NCCL control interface |
-| `GLOO_SOCKET_IFNAME` | same | Gloo (PyTorch) interface |
-| `TP_SOCKET_IFNAME` | same | vLLM TP coordinator |
-| `MN_IF_NAME` | same | Multi-node interface |
-| `UCX_NET_DEVICES` | same | UCX devices |
-| `OMPI_MCA_btl_tcp_if_include` | same | OpenMPI TCP interface |
-| `NCCL_IB_GID_INDEX` | `nccl.<name>.ib_gid_index` | RoCE GID |
-| `VLLM_HOST_IP` / `SGLANG_WORKER_IP` | `node.ib_ip` | Engine host IP |
-| `HF_HOME` | `/root/.cache/huggingface` | HF cache (volume-mounted) |
-| `OMP_NUM_THREADS` | `4` | OpenMP threads |
-| `NCCL_IGNORE_CPU_AFFINITY` | `1` | Don't pin NCCL threads |
-| `NCCL_NET` | `IB` | NCCL IB plugin |
-| `NCCL_DEBUG` | `WARN` | NCCL log level |
-| `VLLM_LOGGING_COLOR` | `1` | Colored logs (head only) |
-| `TERM` | `xterm-256color` | Terminal type (head only) |
-
-### Ray backend (when `distributed_backend = "ray"`)
-
-`RAY_ADDRESS`, `RAY_PORT`, `RAY_NODE_IP_ADDRESS`, `RAY_OVERRIDE_NODE_IP_ADDRESS`
+`sm121 web` — local axum + SSE dashboard: the recipes panel (the `report
+--serve` table, with run-ledger state and in-TOML navigation), a physical
+topology map (grid, 200G/10G fabric, HA glyphs, cluster frames), a
+cluster-level LED board, and serve/stop/power jobs with live status.
 
 ## Internationalization
 
-Messages are localized. Auto-detects system locale.
+Messages are localized (Fluent FTL in `locales/{en,ru,es,it,fr,de}/`),
+auto-detected from the system locale:
 
 ```bash
-# Override locale (works on both macOS and Linux)
-LANG=ru_RU.UTF-8 sm121-launcher serve --recipe deepseek-v4-flash
+LANG=ru_RU.UTF-8 sm121 serve --recipe deepseek-v4-flash
 ```
 
-Available: English (fallback), Russian, Spanish, Italian, French, German.
-
-## Project Links
+## Further reading
 
-- **Development docs**: [`DEVELOPMENT.md`](DEVELOPMENT.md)
-- **Agent reference**: [`AGENTS.md`](AGENTS.md)
+- **[`AGENTS.md`](AGENTS.md)** — full operational reference: recipe format,
+  image builds/pins, env vars, mods, composed models, registry, NCCL
+  auto-detect, power, gotchas.
+- **[`DEVELOPMENT.md`](DEVELOPMENT.md)** — architecture, module layout, dev workflow.
+- **`specs/tech-architecture/`** — measured lane states and findings.
+- **`history/night-shifts/`** — shift logs.