start.sh 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098
  1. #!/usr/bin/env bash
  2. # start.sh — DeepSeek-V4.1-Flash on 3× DGX Spark (GB10 / SM121)
  3. #
  4. # Upstream: https://github.com/0xSero/deepseek-v4.1-flash-4x-rtx-pro-6000
  5. # 4× RTX PRO 6000 Blackwell, TP4/EP4, native weights, NVMe Engram, DSpark.
  6. # This wrapper remaps that stack onto the 3-Spark triangle:
  7. # spark1 10.0.0.1 (mia) — rank 0, API :8888
  8. # spark2 10.0.0.2 (zurih)
  9. # spark3 10.0.0.3 (zurih)
  10. # TP=3 EP=3, RoCE NCCL, Engram on NVMe. spark2/spark3 read spark1 weights
  11. # over NFSv4 on ConnectX (vllm-fn-nfs / dsv41-nfs) — no local 476 GiB copy.
  12. #
  13. # Usage:
  14. # ./start.sh doctor → image → share → serve (default)
  15. # ./start.sh doctor connectivity + GPU + checkpoint report
  16. # ./start.sh pull pull lmsysorg/sglang:dev-dsv41 on all 3 nodes
  17. # ./start.sh build docker build the Engram overlay image everywhere
  18. # ./start.sh download hf download the pinned V4.1-Flash revision
  19. # ./start.sh share export spark1 checkpoint; NFS volumes on spark2/3
  20. # ./start.sh mount alias for share (legacy)
  21. # ./start.sh serve launch workers then head
  22. # ./start.sh stop tear down all 3 ranks
  23. # ./start.sh status containers + API
  24. # ./start.sh logs [N] tail head boot logs
  25. # ./start.sh logs worker<N> [lines] (worker1, worker2, ...)
  26. # ./start.sh smoke arithmetic (+ optional tools/vision)
  27. #
  28. set -euo pipefail
  29. ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  30. cd "$ROOT"
  31. # Profiles: start.sh reads .env (3 Sparks, TP3); start-tp4.sh points ENV_FILE at
  32. # .env.tp4 (4 Sparks, TP4) and gives that profile its own state/log dirs.
  33. ENV_FILE="${ENV_FILE:-$ROOT/.env}"
  34. ENV_EXAMPLE="${ENV_EXAMPLE:-$ROOT/.env.example}"
  35. if [[ ! -f "$ENV_FILE" ]]; then
  36. [[ -f "$ENV_EXAMPLE" ]] || { echo "missing $(basename "$ENV_EXAMPLE")" >&2; exit 1; }
  37. cp "$ENV_EXAMPLE" "$ENV_FILE"
  38. echo "[dsv41] wrote $(basename "$ENV_FILE") from $(basename "$ENV_EXAMPLE") — edit IPs if needed"
  39. fi
  40. set -a
  41. # shellcheck disable=SC1091
  42. source "$ENV_FILE"
  43. set +a
  44. HEAD_IP="${HEAD_IP:-10.0.0.1}"
  45. # Workers: WORKER_IPS="10.0.0.2 10.0.0.3 ..." (space or comma separated) and
  46. # optionally WORKER_HOSTS="spark2 spark3 ..." (ssh names); the legacy
  47. # WORKER1_IP/WORKER2_IP/WORKER1_HOST/WORKER2_HOST pairs still work for 3 nodes.
  48. _list() { tr ',' ' ' <<<"$1"; }
  49. if [[ -n "${WORKER_IPS:-}" ]]; then
  50. read -r -a WORKER_IPS <<<"$(_list "$WORKER_IPS")"
  51. else
  52. WORKER_IPS=("${WORKER1_IP:-10.0.0.2}" "${WORKER2_IP:-10.0.0.3}")
  53. fi
  54. if [[ -n "${WORKER_HOSTS:-}" ]]; then
  55. read -r -a WORKER_HOSTS <<<"$(_list "$WORKER_HOSTS")"
  56. else
  57. WORKER_HOSTS=()
  58. for _i in "${!WORKER_IPS[@]}"; do
  59. _v="WORKER$((_i + 1))_HOST"
  60. WORKER_HOSTS+=("${!_v:-${WORKER_IPS[$_i]}}")
  61. done
  62. fi
  63. [[ ${#WORKER_HOSTS[@]} -eq ${#WORKER_IPS[@]} ]] || { echo "WORKER_HOSTS and WORKER_IPS differ in length" >&2; exit 1; }
  64. WORKER1_IP="${WORKER_IPS[0]}"; WORKER2_IP="${WORKER_IPS[1]:-}" # legacy names, still read by files/nfs-share.sh
  65. WORKER_USER="${WORKER_USER:-zurih}"
  66. SSH_IDENTITY="${SSH_IDENTITY:-$HOME/.ssh/id_ed25519_shared}"
  67. FABRIC_IFACE="${FABRIC_IFACE:-enp1s0f1np1}"
  68. GLOO_SOCKET_IFNAME="${GLOO_SOCKET_IFNAME:-enP7s7}"
  69. NCCL_SOCKET_IFNAME="${NCCL_SOCKET_IFNAME:-enP7s7}"
  70. IB_HCA="${IB_HCA:-rocep1s0f0,rocep1s0f1}"
  71. NCCL_NET="${NCCL_NET:-IB}"
  72. NCCL_IB_DISABLE="${NCCL_IB_DISABLE:-0}"
  73. NCCL_P2P_DISABLE="${NCCL_P2P_DISABLE:-1}"
  74. NCCL_SHM_DISABLE="${NCCL_SHM_DISABLE:-1}"
  75. NCCL_DEBUG="${NCCL_DEBUG:-WARN}"
  76. NCCL_HOST_DIR="${NCCL_HOST_DIR:-$HOME/nccl-2.30.7}"
  77. NCCL_CONTAINER_DIR="${NCCL_CONTAINER_DIR:-/nccl}"
  78. # Container path of the base image's pip-installed NCCL. A patched host build
  79. # can be overlaid here (NCCL_OVERLAY_PIP) instead of being pushed onto
  80. # LD_LIBRARY_PATH, which leaves two NCCL runtimes visible and makes DeepEP's
  81. # check_nccl_so() abort before NCCL is ever initialised.
  82. NCCL_PIP_SO="${NCCL_PIP_SO:-/opt/sglang/lib/python3.12/site-packages/nvidia/nccl/lib/libnccl.so.2}"
  83. # Overlay the patched library; defaults to the switchless-ring switch below,
  84. # so it can also be enabled on its own with NCCL_OVERLAY_PIP=1.
  85. NCCL_SWITCHLESS_RING_ONLY="${NCCL_SWITCHLESS_RING_ONLY:-0}"
  86. NCCL_OVERLAY_PIP="${NCCL_OVERLAY_PIP:-$NCCL_SWITCHLESS_RING_ONLY}"
  87. MODEL_DIR="${MODEL_DIR:-$HOME/NewModels/DeepSeek-V4.1-Flash}"
  88. COMMON_MODEL="${COMMON_MODEL:-/var/tmp/DeepSeek-V4.1-Flash}"
  89. HF_REPO="${HF_REPO:-deepseek-ai/DeepSeek-V4.1-Flash}"
  90. HF_REVISION="${HF_REVISION:-fb2764a5cf321eaa5070ca8f9e892818f477c16d}"
  91. EXPECTED_SHARDS="${EXPECTED_SHARDS:-48}"
  92. BASE_IMAGE="${BASE_IMAGE:-lmsysorg/sglang:dev-dsv41@sha256:3dbc313030a6ef2c5d7de8ecf48e9aece722694a82182cb618cc82b588816349}" # same pin as the Dockerfile FROM
  93. IMAGE="${IMAGE:-dsv41-3x-spark:local}"
  94. # Honoured only when start-tp4.sh has set DSV41_LAUNCHER=tp4. ./start.sh always
  95. # builds Dockerfile and does not read these. A TP4 profile that runs one of the
  96. # canary images sets BUILD_DOCKERFILE so `build` produces $IMAGE from that recipe;
  97. # BUILD_ARGS carries the rest, e.g. --build-arg PIP_INDEX=<mirror>/simple.
  98. BUILD_DOCKERFILE="${BUILD_DOCKERFILE:-Dockerfile}"
  99. BUILD_ARGS="${BUILD_ARGS:-}"
  100. HEAD_CTN="${HEAD_CTN:-dsv41-head}"
  101. WORKER_CTN="${WORKER_CTN:-dsv41-worker}"
  102. WORKER_DIR="${WORKER_DIR:-/home/${WORKER_USER}/dsv41-3x-spark}"
  103. NNODES="${NNODES:-$(( ${#WORKER_IPS[@]} + 1 ))}"
  104. [[ "$NNODES" -eq $(( ${#WORKER_IPS[@]} + 1 )) ]] || { echo "NNODES=$NNODES but ${#WORKER_IPS[@]} workers are configured" >&2; exit 1; }
  105. TP_SIZE="${TP_SIZE:-3}"
  106. EP_SIZE="${EP_SIZE:-$TP_SIZE}"
  107. DIST_PORT="${DIST_PORT:-20000}"
  108. PORT="${PORT:-8888}"
  109. OFFLOAD_MODE="${OFFLOAD_MODE:-nvme}"
  110. DSV41_CACHE_GIB="${DSV41_CACHE_GIB:-4}"
  111. # Engram misses are serviced by a pool: the GB10 NVMe does ~3.5k IOPS at QD=1 and
  112. # ~112k at QD=64, and the callback blocks the compute stream the whole time.
  113. DSV41_IO_THREADS="${DSV41_IO_THREADS:-96}"
  114. DSV41_RESIDENT_SCALES="${DSV41_RESIDENT_SCALES:-0}"
  115. DSV41_CACHE_WAYS="${DSV41_CACHE_WAYS:-4}"
  116. DSV41_STATS_SECONDS="${DSV41_STATS_SECONDS:-60}"
  117. # DSpark's ragged-verify scheduler is inert without a profiled SPS cost table.
  118. # Build one against a running server with:
  119. # python -m sglang.benchmark.dspark_sps_profiler
  120. # and drop it at $STATE_DIR/dspark_sps.json; every rank needs its own copy.
  121. DSPARK_SPS_TABLE="${DSPARK_SPS_TABLE:-/state/dspark_sps.json}"
  122. DSPARK_STS_TABLE="${DSPARK_STS_TABLE:-/state/dspark_sts.json}"
  123. # Node-local repacked Engram shards (see scripts/pack_engram.py). ~68 GiB/node.
  124. ENGRAM_DIR="${ENGRAM_DIR:-$HOME/dsv41-engram}"
  125. WORKER_ENGRAM_DIR="${WORKER_ENGRAM_DIR:-$WORKER_DIR/engram}"
  126. DSV41_PACKED_DIR="${DSV41_PACKED_DIR:-/engram}"
  127. CONTEXT_LENGTH="${CONTEXT_LENGTH:-409600}"
  128. MEM_FRACTION_STATIC="${MEM_FRACTION_STATIC:-0.95}"
  129. # Rank 0 also carries the HTTP server, tokenizer and detokenizer (~1.3 GiB the
  130. # workers never pay) and serves the checkpoint over NFS, so it runs out of host
  131. # RAM first -- and on GB10 host RAM is GPU memory. Give it its own budget.
  132. HEAD_MEM_FRACTION_STATIC="${HEAD_MEM_FRACTION_STATIC:-$MEM_FRACTION_STATIC}"
  133. MAX_RUNNING_REQUESTS="${MAX_RUNNING_REQUESTS:-4}"
  134. CHUNKED_PREFILL_SIZE="${CHUNKED_PREFILL_SIZE:-2048}"
  135. MAX_TOTAL_TOKENS="${MAX_TOTAL_TOKENS:-320000}"
  136. SPEC_ALGO="${SPEC_ALGO:-DSPARK}"
  137. DSPARK_BLOCK_SIZE="${DSPARK_BLOCK_SIZE:-3}"
  138. # The TP4 decode adapters are loaded in the image but off. Turning them on
  139. # made spark3 drop its weights at FlashInfer autotune: that pass is a
  140. # target-verify forward, rank 2 is the padded shard, and wo_a DROP has already
  141. # replaced those parameters with a 1-element stand-in while Engram prefetch
  142. # and the verify remap run inside the profiled forward.
  143. if [[ "$TP_SIZE" == "3" ]]; then
  144. DSV41_WO_A_W8="${DSV41_WO_A_W8:-0}"
  145. DSV41_WO_A_W8_MID="${DSV41_WO_A_W8_MID:-0}"
  146. DSV41_WO_A_W8_DROP="${DSV41_WO_A_W8_DROP:-0}"
  147. DSV41_DRAFT_HEAD_FP8="${DSV41_DRAFT_HEAD_FP8:-0}"
  148. DSV41_VERIFY_CAP="${DSV41_VERIFY_CAP:-0}"
  149. DSV41_DRAFT_TAU="${DSV41_DRAFT_TAU:-1}"
  150. DSV41_BLOCK_VERIFY="${DSV41_BLOCK_VERIFY:-0}"
  151. DSV41_ENGRAM_PREFETCH="${DSV41_ENGRAM_PREFETCH:-0}"
  152. DSV41_FOLDED_FENCE="${DSV41_FOLDED_FENCE:-0}"
  153. else
  154. DSV41_WO_A_W8="${DSV41_WO_A_W8:-0}"
  155. DSV41_WO_A_W8_MID="${DSV41_WO_A_W8_MID:-0}"
  156. DSV41_WO_A_W8_DROP="${DSV41_WO_A_W8_DROP:-0}"
  157. DSV41_DRAFT_HEAD_FP8="${DSV41_DRAFT_HEAD_FP8:-0}"
  158. DSV41_VERIFY_CAP="${DSV41_VERIFY_CAP:-0}"
  159. DSV41_DRAFT_TAU="${DSV41_DRAFT_TAU:-1}"
  160. DSV41_BLOCK_VERIFY="${DSV41_BLOCK_VERIFY:-0}"
  161. DSV41_ENGRAM_PREFETCH="${DSV41_ENGRAM_PREFETCH:-0}"
  162. DSV41_FOLDED_FENCE="${DSV41_FOLDED_FENCE:-0}"
  163. fi
  164. SERVED_MODEL_NAME="${SERVED_MODEL_NAME:-deepseek-v4.1-flash}"
  165. SKIP_PREPARE="${SKIP_PREPARE:-1}"
  166. SKIP_VERIFY="${SKIP_VERIFY:-1}"
  167. SKIP_SMOKE="${SKIP_SMOKE:-0}"
  168. SMOKE_QUICK="${SMOKE_QUICK:-1}"
  169. API_KEY="${API_KEY:-}"
  170. STATE_DIR="${STATE_DIR:-$ROOT/state}"
  171. LOG_DIR="${LOG_DIR:-$ROOT/logs}"
  172. SERVE_LOG="${SERVE_LOG:-$LOG_DIR/dsv41.log}"
  173. REMOTE_PY="$ROOT/scripts/remote.py"
  174. NFS_VOLUME="${NFS_VOLUME:-dsv41-weights}"
  175. NFS_SHARE="${NFS_SHARE:-1}"
  176. _abs() { readlink -f "$1" 2>/dev/null || echo "$1"; }
  177. MODEL_DIR="$(_abs "$MODEL_DIR")"
  178. SSH_IDENTITY="$(_abs "$SSH_IDENTITY")"
  179. NCCL_HOST_DIR="$(_abs "$NCCL_HOST_DIR")"
  180. RED=$'\033[0;31m'; GREEN=$'\033[0;32m'; YELLOW=$'\033[1;33m'; NC=$'\033[0m'
  181. info() { echo "${GREEN}[+]${NC} $*"; }
  182. warn() { echo "${YELLOW}[!]${NC} $*"; }
  183. err() { echo "${RED}[x]${NC} $*" >&2; }
  184. die() { err "$@"; exit 1; }
  185. mkdir -p "$LOG_DIR" "$STATE_DIR"
  186. # shellcheck source=files/nfs-share.sh
  187. source "$ROOT/files/nfs-share.sh"
  188. # shellcheck source=files/nccl.sh
  189. source "$ROOT/files/nccl.sh"
  190. remote_on() {
  191. local host="$1"; shift
  192. local to_args=()
  193. if [[ "${1:-}" == "--timeout" ]]; then
  194. to_args=(--timeout "$2"); shift 2
  195. elif [[ -n "${REMOTE_TIMEOUT:-}" ]]; then
  196. to_args=(--timeout "$REMOTE_TIMEOUT")
  197. fi
  198. python3 "$REMOTE_PY" --env-file "$ENV_FILE" \
  199. --host "$host" --user "$WORKER_USER" \
  200. --identity "$SSH_IDENTITY" \
  201. "${to_args[@]}" \
  202. "bash -lc $(printf '%q' "$*")"
  203. }
  204. remote_ok_on() { remote_on "$@" >/dev/null 2>&1; }
  205. ssh_rsync_e() {
  206. printf 'ssh -i %q -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new' \
  207. "$SSH_IDENTITY"
  208. }
  209. ensure_ssh_keys() {
  210. local pub host
  211. pub=$(cat "${SSH_IDENTITY}.pub" 2>/dev/null || true)
  212. [[ -n "$pub" ]] || die "Missing ${SSH_IDENTITY}.pub"
  213. for host in "${WORKER_HOSTS[@]}"; do
  214. remote_on "$host" "mkdir -p ~/.ssh && chmod 700 ~/.ssh
  215. touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys
  216. grep -qxF '$pub' ~/.ssh/authorized_keys || echo '$pub' >> ~/.ssh/authorized_keys"
  217. done
  218. }
  219. model_src() {
  220. if [[ -L "$COMMON_MODEL" ]]; then
  221. readlink -f "$COMMON_MODEL"
  222. elif [[ -d "$COMMON_MODEL" ]]; then
  223. echo "$COMMON_MODEL"
  224. else
  225. echo "$MODEL_DIR"
  226. fi
  227. }
  228. api_key() {
  229. # Empty / dummy / off → no --api-key (Spark 1 / Pi probe /v1/models without auth).
  230. local v="${API_KEY:-}"
  231. case "${v,,}" in
  232. ''|none|off|dummy|0) echo "" ;;
  233. *) echo "$v" ;;
  234. esac
  235. }
  236. gid_index_local() {
  237. local ip="$1" hex hca g i
  238. hex=$(printf '%02x%02x:%02x%02x' $(echo "$ip" | tr . ' '))
  239. local IFS=','
  240. for hca in $IB_HCA; do
  241. hca="${hca// /}"
  242. [[ -d /sys/class/infiniband/"$hca" ]] || continue
  243. for g in /sys/class/infiniband/"$hca"/ports/1/gids/*; do
  244. i=${g##*/}
  245. [[ "$(cat /sys/class/infiniband/"$hca"/ports/1/gid_attrs/types/"$i" 2>/dev/null)" == "RoCE v2" ]] || continue
  246. case "$(cat "$g")" in *ffff:"$hex") echo "$i"; return 0;; esac
  247. done
  248. done
  249. return 1
  250. }
  251. gid_index_remote() {
  252. local host="$1" ip="$2" hex
  253. hex=$(printf '%02x%02x:%02x%02x' $(echo "$ip" | tr . ' '))
  254. remote_on "$host" "for hca in \$(echo $IB_HCA | tr ',' ' '); do
  255. [ -d /sys/class/infiniband/\$hca ] || continue;
  256. for g in /sys/class/infiniband/\$hca/ports/1/gids/*; do
  257. i=\${g##*/};
  258. [ \"\$(cat /sys/class/infiniband/\$hca/ports/1/gid_attrs/types/\$i 2>/dev/null)\" = 'RoCE v2' ] || continue;
  259. case \$(cat \$g) in *ffff:$hex) echo \$i; exit 0;; esac;
  260. done;
  261. done"
  262. }
  263. docker_common_args() {
  264. local -n _a=$1
  265. local src hip gid
  266. src=$(model_src)
  267. hip="$2"
  268. gid="$3"
  269. [[ -d "$src" ]] || die "model mount src missing: $src"
  270. _a+=(
  271. --network host --ipc host --privileged --cap-add IPC_LOCK --gpus all
  272. --shm-size "${SHM_SIZE:-32g}"
  273. --ulimit "memlock=-1:-1" --ulimit stack=67108864
  274. --device /dev/infiniband:/dev/infiniband
  275. -v "$src:/models/DeepSeek-V4.1-Flash"
  276. -v "$STATE_DIR:/state"
  277. -v "$HOME/.cache:/root/.cache"
  278. -e "OFFLOAD_MODE=$OFFLOAD_MODE"
  279. -e "DSV41_CACHE_GIB=$DSV41_CACHE_GIB"
  280. -e "DSV41_SERIAL_WEIGHT_LOAD=${DSV41_SERIAL_WEIGHT_LOAD:-0}"
  281. -e "DSV41_IO_THREADS=$DSV41_IO_THREADS"
  282. -e "DSV41_RESIDENT_SCALES=$DSV41_RESIDENT_SCALES"
  283. -e "DSV41_CACHE_WAYS=$DSV41_CACHE_WAYS"
  284. -e "DSV41_STATS_SECONDS=$DSV41_STATS_SECONDS"
  285. -v "$ENGRAM_DIR:/engram"
  286. -e "DSV41_PACKED_DIR=$DSV41_PACKED_DIR"
  287. -e "DSPARK_SPS_TABLE=$DSPARK_SPS_TABLE"
  288. -e "DSPARK_STS_TABLE=$DSPARK_STS_TABLE"
  289. -e "DSV41_SOURCE=/models/DeepSeek-V4.1-Flash"
  290. -e "MODEL_PATH=/models/DeepSeek-V4.1-Flash"
  291. -e "STATE_PATH=/state"
  292. -e "SERVER_PORT=$PORT"
  293. -e "NNODES=$NNODES"
  294. -e "TP_SIZE=$TP_SIZE"
  295. -e "EP_SIZE=$EP_SIZE"
  296. -e "DIST_INIT_ADDR=${HEAD_IP}:${DIST_PORT}"
  297. -e "CONTEXT_LENGTH=$CONTEXT_LENGTH"
  298. -e "MEM_FRACTION_STATIC=$HEAD_MEM_FRACTION_STATIC"
  299. -e "MAX_RUNNING_REQUESTS=$MAX_RUNNING_REQUESTS"
  300. -e "CHUNKED_PREFILL_SIZE=$CHUNKED_PREFILL_SIZE"
  301. -e "MAX_TOTAL_TOKENS=$MAX_TOTAL_TOKENS"
  302. -e "CUDA_GRAPH_MAX_BS_DECODE=$MAX_RUNNING_REQUESTS"
  303. -e "SPEC_ALGO=$SPEC_ALGO"
  304. -e "DSPARK_BLOCK_SIZE=$DSPARK_BLOCK_SIZE"
  305. -e "DSV41_WO_A_W8=$DSV41_WO_A_W8"
  306. -e "DSV41_WO_A_W8_MID=$DSV41_WO_A_W8_MID"
  307. -e "DSV41_WO_A_W8_DROP=$DSV41_WO_A_W8_DROP"
  308. -e "DSV41_DRAFT_HEAD_FP8=$DSV41_DRAFT_HEAD_FP8"
  309. -e "DSV41_VERIFY_CAP=$DSV41_VERIFY_CAP"
  310. -e "DSV41_DRAFT_TAU=$DSV41_DRAFT_TAU"
  311. -e "DSV41_BLOCK_VERIFY=$DSV41_BLOCK_VERIFY"
  312. -e "DSV41_ENGRAM_PREFETCH=$DSV41_ENGRAM_PREFETCH"
  313. -e "DSV41_FOLDED_FENCE=$DSV41_FOLDED_FENCE"
  314. -e "DSV41_AUTOTUNE_KEEP=${DSV41_AUTOTUNE_KEEP:-0}"
  315. -e "DSV41_WO_A_W8_DRAFT=${DSV41_WO_A_W8_DRAFT:-0}"
  316. -e "DSV41_ENGRAM_PREFETCH_CHECK=${DSV41_ENGRAM_PREFETCH_CHECK:-0}"
  317. -e "SERVED_MODEL_NAME=$SERVED_MODEL_NAME"
  318. -e "SKIP_PREPARE=$SKIP_PREPARE"
  319. -e "SKIP_VERIFY=$SKIP_VERIFY"
  320. -e "WARMUP=${WARMUP:-1}"
  321. -e "HOST=0.0.0.0"
  322. -e "NCCL_NET=$NCCL_NET"
  323. -e "NCCL_IB_DISABLE=$NCCL_IB_DISABLE"
  324. -e "NCCL_IB_HCA=$IB_HCA"
  325. -e "NCCL_SOCKET_IFNAME=$NCCL_SOCKET_IFNAME"
  326. -e "GLOO_SOCKET_IFNAME=$GLOO_SOCKET_IFNAME"
  327. -e "NCCL_P2P_DISABLE=$NCCL_P2P_DISABLE"
  328. -e "NCCL_SHM_DISABLE=$NCCL_SHM_DISABLE"
  329. -e "NCCL_CROSS_NIC=${NCCL_CROSS_NIC:-1}"
  330. -e "NCCL_IB_MERGE_NICS=${NCCL_IB_MERGE_NICS:-0}"
  331. -e "NCCL_IB_SUBNET_AWARE_ROUTING=${NCCL_IB_SUBNET_AWARE_ROUTING:-1}"
  332. -e "NCCL_CUMEM_ENABLE=0"
  333. -e "NCCL_DEBUG=$NCCL_DEBUG"
  334. -e "NCCL_BUFFSIZE=${NCCL_BUFFSIZE:-4194304}"
  335. -e "NCCL_LL128_BUFFSIZE=${NCCL_LL128_BUFFSIZE:--2}"
  336. -e "NCCL_PROTO=${NCCL_PROTO:-LL,LL128,Simple}"
  337. -e "NCCL_MAX_NCHANNELS=${NCCL_MAX_NCHANNELS:-32}"
  338. -e "NCCL_DEBUG_SUBSYS=${NCCL_DEBUG_SUBSYS:-INIT}"
  339. -e "DSV41_MXFP8_BACKEND=${DSV41_MXFP8_BACKEND:-b12x}"
  340. -e "SGLANG_FLASHINFER_MOE_FUSED_FINALIZE=${SGLANG_FLASHINFER_MOE_FUSED_FINALIZE:-1}"
  341. -e "SGLANG_DSV41_REASONING_EFFORT=${SGLANG_DSV41_REASONING_EFFORT:-75}"
  342. -e "DSV41_MAX_NEW_TOKENS=${DSV41_MAX_NEW_TOKENS:-32768}"
  343. -e "DSV41_LOOP_ABORT=${DSV41_LOOP_ABORT:-1}"
  344. -e "DSV41_LOOP_NGRAM=${DSV41_LOOP_NGRAM:-32}"
  345. -e "DSV41_LOOP_REPEATS=${DSV41_LOOP_REPEATS:-4}"
  346. -e "DSV41_LOOP_LINE_REPEATS=${DSV41_LOOP_LINE_REPEATS:-8}"
  347. -e "PYTORCH_CUDA_ALLOC_CONF=${PYTORCH_CUDA_ALLOC_CONF:-expandable_segments:False}"
  348. -e "CUDA_DEVICE_ORDER=PCI_BUS_ID"
  349. -e "SGLANG_ENABLE_DSV41_ENGRAM_HOST_TABLE=0"
  350. -e "DSV41_TP_PAD=${DSV41_TP_PAD:-1}"
  351. -e "VLLM_HOST_IP=$hip"
  352. -e "HOST_IP=$hip"
  353. -e "NCCL_IB_GID_INDEX=$gid"
  354. )
  355. if [[ -n "${API_KEY}" ]]; then
  356. _a+=(-e "API_KEY=$API_KEY")
  357. fi
  358. if [[ -n "${EXTRA_SGLANG_ARGS:-}" ]]; then
  359. _a+=(-e "EXTRA_SGLANG_ARGS=$EXTRA_SGLANG_ARGS")
  360. fi
  361. # Unset means the engine's AUTO (decided from min free memory across ranks); only forward an explicit mode.
  362. if [[ -n "${SGLANG_DSPARK_FOLDED_SAMPLING:-}" ]]; then
  363. _a+=(-e "SGLANG_DSPARK_FOLDED_SAMPLING=$SGLANG_DSPARK_FOLDED_SAMPLING")
  364. fi
  365. switchless_ring_args _a
  366. nccl_mount_args _a || die "NCCL mount setup failed"
  367. # start-tp4.sh only. ./start.sh does not pass these and does not read EXTRA_CONTAINER_ENV.
  368. if [[ "${DSV41_LAUNCHER:-}" == tp4 ]]; then
  369. _a+=(
  370. -e "DSV41_LAUNCHER=tp4"
  371. -e "DSV41_SHARED_PAD_K=${DSV41_SHARED_PAD_K:-0}"
  372. -e "DSV41_SHARED_PAD_BUF_ROWS=${DSV41_SHARED_PAD_BUF_ROWS:-256}"
  373. )
  374. extra_container_env_args _a
  375. fi
  376. }
  377. # EXTRA_CONTAINER_ENV="KEY=VALUE KEY2=VALUE2" (whitespace separated). Called only from the
  378. # start-tp4.sh path. Container env for the head and every worker, so the TP4 profile's
  379. # switches are one .env line. A key the launcher already passes is replaced in place, not
  380. # repeated: a repeated -e leaves both entries, and which one a process reads depends on it.
  381. extra_container_env_args() {
  382. local -n _xa=$1
  383. local -a _items=()
  384. local _kv _i _found
  385. read -r -a _items <<<"${EXTRA_CONTAINER_ENV:-}"
  386. for _kv in "${_items[@]}"; do
  387. [[ "$_kv" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]] || die "EXTRA_CONTAINER_ENV: '$_kv' is not KEY=VALUE"
  388. _found=0
  389. for _i in "${!_xa[@]}"; do
  390. if [[ "${_xa[$_i]}" == -e && "${_xa[$((_i + 1))]:-}" == "${_kv%%=*}="* ]]; then
  391. _xa[$((_i + 1))]="$_kv"
  392. _found=1
  393. fi
  394. done
  395. [[ "$_found" == 1 ]] || _xa+=(-e "$_kv")
  396. done
  397. }
  398. # Every rank builds its own planner, so the SPS/STS calibration has to exist on
  399. # every node -- the head's copy in $STATE_DIR is the source of truth.
  400. push_spec_tables() {
  401. local name local_path host
  402. for name in dspark_sps.json dspark_sts.json; do
  403. local_path="$STATE_DIR/$name"
  404. for host in "${WORKER_IPS[@]}"; do
  405. if [[ -f "$local_path" ]]; then
  406. local payload
  407. payload=$(base64 -w0 <"$local_path")
  408. # Every rank derives its verify schedule from its own copy of this file.
  409. # A rank that disagrees with the others builds a different verify shape
  410. # and the TP collective hangs, so a failed push is fatal, not a warning.
  411. remote_on "$host" "mkdir -p $(printf '%q' "$WORKER_DIR/state") && printf %s $(printf '%q' "$payload") | base64 -d > $(printf '%q' "$WORKER_DIR/state/$name")" \
  412. || die "could not push $name to $host: ranks would disagree on the DSpark verify schedule"
  413. info "pushed $name to $host"
  414. else
  415. # Likewise for a stale copy left from an earlier run.
  416. remote_on "$host" "rm -f $(printf '%q' "$WORKER_DIR/state/$name")" \
  417. || die "could not clear a stale $name on $host"
  418. fi
  419. done
  420. done
  421. }
  422. worker_env_lines() {
  423. local wip="$1" wgid="$2" rank="$3"
  424. local -a worker_args=(
  425. -e "NODE_RANK=$rank"
  426. -e "NNODES=$NNODES"
  427. -e "TP_SIZE=$TP_SIZE"
  428. -e "EP_SIZE=$EP_SIZE"
  429. -e "DIST_INIT_ADDR=$HEAD_IP:$DIST_PORT"
  430. -e "OFFLOAD_MODE=$OFFLOAD_MODE"
  431. -e "DSV41_CACHE_GIB=$DSV41_CACHE_GIB"
  432. -e "DSV41_IO_THREADS=$DSV41_IO_THREADS"
  433. -e "DSV41_SERIAL_WEIGHT_LOAD=${DSV41_SERIAL_WEIGHT_LOAD:-0}"
  434. -e "DSV41_RESIDENT_SCALES=$DSV41_RESIDENT_SCALES"
  435. -e "DSV41_CACHE_WAYS=$DSV41_CACHE_WAYS"
  436. -e "DSV41_STATS_SECONDS=$DSV41_STATS_SECONDS"
  437. -v "$WORKER_ENGRAM_DIR:/engram"
  438. -e "DSV41_PACKED_DIR=$DSV41_PACKED_DIR"
  439. -e "DSPARK_SPS_TABLE=$DSPARK_SPS_TABLE"
  440. -e "DSPARK_STS_TABLE=$DSPARK_STS_TABLE"
  441. -e "DSV41_SOURCE=/models/DeepSeek-V4.1-Flash"
  442. -e "MODEL_PATH=/models/DeepSeek-V4.1-Flash"
  443. -e "STATE_PATH=/state"
  444. -e "SERVER_PORT=$PORT"
  445. -e "HOST=0.0.0.0"
  446. -e "CONTEXT_LENGTH=$CONTEXT_LENGTH"
  447. -e "MEM_FRACTION_STATIC=$MEM_FRACTION_STATIC"
  448. -e "MAX_RUNNING_REQUESTS=$MAX_RUNNING_REQUESTS"
  449. -e "CHUNKED_PREFILL_SIZE=$CHUNKED_PREFILL_SIZE"
  450. -e "MAX_TOTAL_TOKENS=$MAX_TOTAL_TOKENS"
  451. -e "CUDA_GRAPH_MAX_BS_DECODE=$MAX_RUNNING_REQUESTS"
  452. -e "SPEC_ALGO=$SPEC_ALGO"
  453. -e "DSPARK_BLOCK_SIZE=$DSPARK_BLOCK_SIZE"
  454. -e "DSV41_WO_A_W8=$DSV41_WO_A_W8"
  455. -e "DSV41_WO_A_W8_MID=$DSV41_WO_A_W8_MID"
  456. -e "DSV41_WO_A_W8_DROP=$DSV41_WO_A_W8_DROP"
  457. -e "DSV41_DRAFT_HEAD_FP8=$DSV41_DRAFT_HEAD_FP8"
  458. -e "DSV41_VERIFY_CAP=$DSV41_VERIFY_CAP"
  459. -e "DSV41_DRAFT_TAU=$DSV41_DRAFT_TAU"
  460. -e "DSV41_BLOCK_VERIFY=$DSV41_BLOCK_VERIFY"
  461. -e "DSV41_ENGRAM_PREFETCH=$DSV41_ENGRAM_PREFETCH"
  462. -e "DSV41_FOLDED_FENCE=$DSV41_FOLDED_FENCE"
  463. -e "DSV41_AUTOTUNE_KEEP=${DSV41_AUTOTUNE_KEEP:-0}"
  464. -e "DSV41_WO_A_W8_DRAFT=${DSV41_WO_A_W8_DRAFT:-0}"
  465. -e "DSV41_ENGRAM_PREFETCH_CHECK=${DSV41_ENGRAM_PREFETCH_CHECK:-0}"
  466. -e "SERVED_MODEL_NAME=$SERVED_MODEL_NAME"
  467. -e "SKIP_PREPARE=1"
  468. -e "SKIP_VERIFY=1"
  469. -e "SKIP_SMOKE=1"
  470. -e "NCCL_NET=$NCCL_NET"
  471. -e "NCCL_IB_DISABLE=$NCCL_IB_DISABLE"
  472. -e "NCCL_IB_HCA=$IB_HCA"
  473. -e "NCCL_SOCKET_IFNAME=$NCCL_SOCKET_IFNAME"
  474. -e "GLOO_SOCKET_IFNAME=$GLOO_SOCKET_IFNAME"
  475. -e "NCCL_P2P_DISABLE=$NCCL_P2P_DISABLE"
  476. -e "NCCL_SHM_DISABLE=$NCCL_SHM_DISABLE"
  477. -e "NCCL_CROSS_NIC=${NCCL_CROSS_NIC:-1}"
  478. -e "NCCL_IB_MERGE_NICS=${NCCL_IB_MERGE_NICS:-0}"
  479. -e "NCCL_IB_SUBNET_AWARE_ROUTING=${NCCL_IB_SUBNET_AWARE_ROUTING:-1}"
  480. -e "NCCL_CUMEM_ENABLE=0"
  481. -e "NCCL_DEBUG=$NCCL_DEBUG"
  482. -e "NCCL_BUFFSIZE=${NCCL_BUFFSIZE:-4194304}"
  483. -e "NCCL_LL128_BUFFSIZE=${NCCL_LL128_BUFFSIZE:--2}"
  484. -e "NCCL_PROTO=${NCCL_PROTO:-LL,LL128,Simple}"
  485. -e "NCCL_MAX_NCHANNELS=${NCCL_MAX_NCHANNELS:-32}"
  486. -e "NCCL_DEBUG_SUBSYS=${NCCL_DEBUG_SUBSYS:-INIT}"
  487. -e "DSV41_MXFP8_BACKEND=${DSV41_MXFP8_BACKEND:-b12x}"
  488. -e "SGLANG_FLASHINFER_MOE_FUSED_FINALIZE=${SGLANG_FLASHINFER_MOE_FUSED_FINALIZE:-1}"
  489. -e "SGLANG_DSV41_REASONING_EFFORT=${SGLANG_DSV41_REASONING_EFFORT:-75}"
  490. -e "DSV41_MAX_NEW_TOKENS=${DSV41_MAX_NEW_TOKENS:-32768}"
  491. -e "DSV41_LOOP_ABORT=${DSV41_LOOP_ABORT:-1}"
  492. -e "DSV41_LOOP_NGRAM=${DSV41_LOOP_NGRAM:-32}"
  493. -e "DSV41_LOOP_REPEATS=${DSV41_LOOP_REPEATS:-4}"
  494. -e "DSV41_LOOP_LINE_REPEATS=${DSV41_LOOP_LINE_REPEATS:-8}"
  495. -e "PYTORCH_CUDA_ALLOC_CONF=${PYTORCH_CUDA_ALLOC_CONF:-expandable_segments:False}"
  496. -e "NCCL_IB_GID_INDEX=$wgid"
  497. -e "CUDA_DEVICE_ORDER=PCI_BUS_ID"
  498. -e "SGLANG_ENABLE_DSV41_ENGRAM_HOST_TABLE=0"
  499. -e "DSV41_TP_PAD=${DSV41_TP_PAD:-1}"
  500. -e "HOST_IP=$wip"
  501. -e "VLLM_HOST_IP=$wip"
  502. )
  503. if [[ -n "${SGLANG_DSPARK_FOLDED_SAMPLING:-}" ]]; then
  504. worker_args+=(-e "SGLANG_DSPARK_FOLDED_SAMPLING=$SGLANG_DSPARK_FOLDED_SAMPLING")
  505. fi
  506. switchless_ring_args worker_args
  507. if [[ "${DSV41_LAUNCHER:-}" == tp4 ]]; then
  508. worker_args+=(
  509. -e "DSV41_LAUNCHER=tp4"
  510. -e "DSV41_SHARED_PAD_K=${DSV41_SHARED_PAD_K:-0}"
  511. -e "DSV41_SHARED_PAD_BUF_ROWS=${DSV41_SHARED_PAD_BUF_ROWS:-256}"
  512. )
  513. extra_container_env_args worker_args
  514. fi
  515. printf ' %q ' "${worker_args[@]}"
  516. printf '\\\n'
  517. }
  518. # Validate every rank; never stop at the first missing worker image.
  519. # These probes run disposable containers without network or GPU access.
  520. preflight_all_nodes() {
  521. nccl_validate_config || return 1
  522. [[ "$NCCL_OVERLAY_PIP" == 1 || "$NCCL_SWITCHLESS_RING_ONLY" == 1 ]] || return 0
  523. local h gid failed=0
  524. RING_WORKER_GIDS=()
  525. if RING_GID_HEAD=$(nccl_preflight); then
  526. info "NCCL preflight head OK${RING_GID_HEAD:+ (ACTIVE HCAs: $IB_HCA, RoCEv2 GID: $RING_GID_HEAD)}"
  527. else
  528. warn 'NCCL preflight failed on head'; failed=1
  529. fi
  530. for h in "${WORKER_HOSTS[@]}"; do
  531. if gid=$(remote_on "$h" "set -e
  532. $(nccl_worker_settings)
  533. nccl_preflight"); then
  534. gid="${gid//$'\r'/}"
  535. gid="${gid#"${gid%%[![:space:]]*}"}"
  536. gid="${gid%"${gid##*[![:space:]]}"}"
  537. if [[ "$NCCL_SWITCHLESS_RING_ONLY" == 1 && ! "$gid" =~ ^[0-9]+$ ]]; then
  538. warn "NCCL preflight $h returned an invalid GID index"; failed=1
  539. continue
  540. fi
  541. RING_WORKER_GIDS+=("$gid")
  542. info "NCCL preflight $h OK${gid:+ (ACTIVE HCAs: $IB_HCA, RoCEv2 GID: $gid)}"
  543. else
  544. warn "NCCL preflight failed on $h"; failed=1
  545. fi
  546. done
  547. return "$failed"
  548. }
  549. cmd_doctor() {
  550. echo "=== doctor (DeepSeek-V4.1-Flash · ${NNODES}× Spark · TP${TP_SIZE} · SGLang) ==="
  551. echo "head: $(hostname) / $(whoami) @ $HEAD_IP"
  552. echo "workers: ${WORKER_USER}@${WORKER_HOSTS[*]} (${WORKER_IPS[*]})"
  553. echo "image: $IMAGE (base $BASE_IMAGE)"
  554. echo "model: $MODEL_DIR"
  555. echo "parallel: nnodes=$NNODES TP=$TP_SIZE EP=$EP_SIZE ctx=$CONTEXT_LENGTH util=$MEM_FRACTION_STATIC (head $HEAD_MEM_FRACTION_STATIC)"
  556. echo "offload: $OFFLOAD_MODE cache=${DSV41_CACHE_GIB}GiB spec=$SPEC_ALGO-$DSPARK_BLOCK_SIZE port=$PORT"
  557. echo "decode: wo_a_w8=$DSV41_WO_A_W8 mid=$DSV41_WO_A_W8_MID drop=$DSV41_WO_A_W8_DROP draft_head_fp8=$DSV41_DRAFT_HEAD_FP8 verify_cap=$DSV41_VERIFY_CAP draft_tau=$DSV41_DRAFT_TAU block_verify=$DSV41_BLOCK_VERIFY engram_prefetch=$DSV41_ENGRAM_PREFETCH folded_fence=$DSV41_FOLDED_FENCE"
  558. echo
  559. local ok=0
  560. command -v docker >/dev/null || { warn "docker missing on head"; ok=1; }
  561. command -v nvidia-smi >/dev/null && info "GPU: $(nvidia-smi -L | head -1)" || { warn "nvidia-smi missing"; ok=1; }
  562. [[ -f "$SSH_IDENTITY" ]] && info "SSH key $SSH_IDENTITY" || { warn "SSH key missing"; ok=1; }
  563. local host_arch
  564. host_arch=$(uname -m)
  565. info "host arch: $host_arch (GB10 expects aarch64)"
  566. if docker image inspect "$IMAGE" >/dev/null 2>&1; then
  567. info "overlay image present ($(docker image inspect -f '{{.Architecture}}' "$IMAGE"))"
  568. else
  569. warn "image $IMAGE missing — ./start.sh build (pulls $BASE_IMAGE)"
  570. fi
  571. if [[ -f "$MODEL_DIR/config.json" ]]; then
  572. local n
  573. n=$(find "$MODEL_DIR" -maxdepth 1 -name 'model-*-of-*.safetensors' 2>/dev/null | wc -l | tr -d ' ')
  574. if [[ "${n:-0}" -ge "$EXPECTED_SHARDS" ]]; then
  575. info "checkpoint OK ($n / $EXPECTED_SHARDS shards, $(du -sh "$MODEL_DIR" | awk '{print $1}'))"
  576. else
  577. warn "partial checkpoint ($n / $EXPECTED_SHARDS) — ./start.sh download"
  578. ok=1
  579. fi
  580. else
  581. warn "checkpoint missing — ./start.sh download"
  582. ok=1
  583. fi
  584. if [[ "$OFFLOAD_MODE" == "ram" ]]; then
  585. warn "RAM Engram mode pins ~189 GiB into unified memory — will not fit on Spark. Use nvme."
  586. ok=1
  587. fi
  588. if [[ "$NFS_SHARE" == "1" ]]; then
  589. if nfs_rpc_ready 127.0.0.1; then
  590. info "NFSv4 listening on head"
  591. else
  592. warn "NFSv4 not listening yet — ./start.sh share will start or reuse the exporter"
  593. fi
  594. info "weights: workers read $NFS_VOLUME over NFSv4; head reads $MODEL_DIR"
  595. else
  596. info "weights: NFS_SHARE=0 — head reads $MODEL_DIR; workers use local volume $NFS_VOLUME"
  597. fi
  598. if [[ "$TP_SIZE" -eq 3 ]]; then
  599. info "TP3 note: heads=64, o_groups=8, vocab=129280 are not divisible by 3; adapter/tp3_pad.py pads them"
  600. info " (heads 64→96, groups 8→12, draft experts 128→129). experts=384 divides. Rank 2 holds padded shards only."
  601. info " 2 Sparks cannot hold the MXFP4 experts (290 GiB / 2 = 145 GiB > 121 GiB)."
  602. elif [[ "$TP_SIZE" -eq 4 ]]; then
  603. info "TP4 note: heads, o_groups, draft experts and vocab all divide by 4; no padding (DSV41_TP_PAD=${DSV41_TP_PAD:-0})."
  604. fi
  605. preflight_all_nodes || ok=1
  606. if nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv,noheader 2>/dev/null | grep -q .; then
  607. warn "GPU already has compute apps:"
  608. nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv
  609. warn "Stop the other stack (e.g. ./start.sh stop in glm-5.3-flash-sm120) before serving, or FORCE=1"
  610. fi
  611. local h
  612. for h in "${WORKER_HOSTS[@]}"; do
  613. local host_report
  614. if host_report=$(remote_on "$h" "hostname" 2>&1); then
  615. info "SSH $h OK → ${host_report//$'\r'/}"
  616. remote_on "$h" "command -v docker >/dev/null && nvidia-smi -L | head -1 && test -d /dev/infiniband && echo IB_OK" \
  617. || { warn "docker/GPU/IB check failed on $h"; ok=1; }
  618. if ! nfs_worker_has_model "$h"; then
  619. warn "$h: weights missing/incomplete or volume $NFS_VOLUME unavailable (NFS_SHARE=$NFS_SHARE)"
  620. ok=1
  621. fi
  622. else
  623. err "SSH to $h FAILED: $host_report"
  624. ok=1
  625. fi
  626. done
  627. if [[ "$ok" -ne 0 ]]; then
  628. if [[ "${DOCTOR_STRICT:-1}" == "1" && "${1:-}" == "strict" ]]; then
  629. die "doctor found blocking issues"
  630. fi
  631. warn "doctor: issues found"
  632. return 1
  633. fi
  634. info "doctor: ready"
  635. }
  636. cmd_download() {
  637. info "=== download $HF_REPO @$HF_REVISION → $MODEL_DIR ==="
  638. mkdir -p "$MODEL_DIR"
  639. local n
  640. n=$(find "$MODEL_DIR" -maxdepth 1 -name 'model-*-of-*.safetensors' 2>/dev/null | wc -l | tr -d ' ')
  641. if [[ -f "$MODEL_DIR/config.json" && "${n:-0}" -ge "$EXPECTED_SHARDS" ]]; then
  642. info "checkpoint already present ($n safetensors) — skip"
  643. else
  644. command -v hf >/dev/null || die "hf CLI missing (pip install -U huggingface_hub[cli])"
  645. hf download "$HF_REPO" --revision "$HF_REVISION" --local-dir "$MODEL_DIR"
  646. fi
  647. ln -sfn "$MODEL_DIR" "$COMMON_MODEL"
  648. info "head: $COMMON_MODEL → $MODEL_DIR"
  649. }
  650. cmd_pull() {
  651. info "=== pull $BASE_IMAGE on all nodes ==="
  652. ensure_ssh_keys
  653. docker pull --platform linux/arm64 "$BASE_IMAGE"
  654. local h
  655. for h in "${WORKER_HOSTS[@]}"; do
  656. info "pulling on $h ..."
  657. remote_on "$h" --timeout "${PULL_TIMEOUT:-0}" \
  658. "docker pull --platform linux/arm64 $(printf '%q' "$BASE_IMAGE")"
  659. done
  660. info "base image present on all nodes"
  661. }
  662. cmd_build() {
  663. local dockerfile="Dockerfile" build_args=""
  664. if [[ "${DSV41_LAUNCHER:-}" == tp4 ]]; then
  665. info "=== build $IMAGE from $ROOT ($BUILD_DOCKERFILE) ==="
  666. dockerfile="$BUILD_DOCKERFILE"
  667. case "$dockerfile" in /*) die "BUILD_DOCKERFILE must be inside the repository (got $dockerfile)";; esac
  668. [[ -f "$ROOT/$dockerfile" ]] || die "no such Dockerfile: $ROOT/$dockerfile"
  669. build_args="$BUILD_ARGS"
  670. else
  671. info "=== build $IMAGE from $ROOT ==="
  672. fi
  673. if ! docker image inspect "$BASE_IMAGE" >/dev/null 2>&1; then
  674. cmd_pull
  675. fi
  676. # shellcheck disable=SC2086 # build_args is a list of docker build flags on the TP4 path
  677. if [[ "${DSV41_LAUNCHER:-}" == tp4 ]]; then
  678. docker build -f "$ROOT/$dockerfile" -t "$IMAGE" $build_args "$ROOT"
  679. else
  680. docker build -t "$IMAGE" "$ROOT"
  681. fi
  682. local img_arch
  683. img_arch=$(docker image inspect -f '{{.Architecture}}' "$IMAGE")
  684. [[ "$img_arch" == "arm64" ]] || die "expected arm64 image, got $img_arch"
  685. info "head built $IMAGE arch=$img_arch"
  686. ensure_ssh_keys
  687. local h
  688. for h in "${WORKER_HOSTS[@]}"; do
  689. info "rsync recipe → $h:$WORKER_DIR"
  690. remote_on "$h" "mkdir -p $(printf '%q' "$WORKER_DIR")"
  691. if [[ "${DSV41_LAUNCHER:-}" == tp4 ]]; then
  692. # rsync-tp4
  693. # Anchored on purpose: an unanchored `models` also matches .../sglang/srt/models (and
  694. # `engram` any engram/ package), and an excluded directory is never transferred, so the
  695. # worker's copy goes stale. runtime/fetched/ is host-side staging, never built from.
  696. rsync -aH --delete --exclude '.env' --exclude '.env.tp4' \
  697. --exclude '/state' --exclude '/state-tp4' \
  698. --exclude '/logs' --exclude '/logs-tp4' --exclude '/models' \
  699. --exclude '/engram' --exclude '/runtime/fetched' \
  700. -e "$(ssh_rsync_e)" \
  701. "$ROOT/" "${WORKER_USER}@${h}:${WORKER_DIR}/"
  702. # end-rsync-tp4
  703. info "docker build on $h ..."
  704. remote_on "$h" --timeout "${BUILD_TIMEOUT:-0}" \
  705. "docker image inspect $(printf '%q' "$BASE_IMAGE") >/dev/null || docker pull --platform linux/arm64 $(printf '%q' "$BASE_IMAGE")
  706. cd $(printf '%q' "$WORKER_DIR") && docker build -f $(printf '%q' "$dockerfile") -t $(printf '%q' "$IMAGE") $build_args ."
  707. else
  708. # rsync-start
  709. rsync -aH --delete --exclude '.env' --exclude '.env.tp4' --exclude 'state' --exclude 'state-tp4' \
  710. --exclude 'logs' --exclude 'logs-tp4' --exclude 'models' \
  711. --exclude 'engram' \
  712. -e "$(ssh_rsync_e)" \
  713. "$ROOT/" "${WORKER_USER}@${h}:${WORKER_DIR}/"
  714. # end-rsync-start
  715. info "docker build on $h ..."
  716. remote_on "$h" --timeout "${BUILD_TIMEOUT:-0}" \
  717. "docker image inspect $(printf '%q' "$BASE_IMAGE") >/dev/null || docker pull --platform linux/arm64 $(printf '%q' "$BASE_IMAGE")
  718. cd $(printf '%q' "$WORKER_DIR") && docker build -t $(printf '%q' "$IMAGE") ."
  719. fi
  720. done
  721. info "overlay image on all 3 nodes"
  722. }
  723. cmd_share() {
  724. [[ "$NFS_SHARE" == 1 ]] || { info "NFS_SHARE=0 — keeping local worker volumes"; return 0; }
  725. info "=== share spark1 checkpoint over NFSv4 on ConnectX ==="
  726. [[ -f "$MODEL_DIR/config.json" ]] || die "no checkpoint — ./start.sh download"
  727. ln -sfn "$MODEL_DIR" "$COMMON_MODEL"
  728. ensure_ssh_keys
  729. nfs_ensure_server
  730. nfs_publish_model
  731. local i=0 h
  732. for h in "${WORKER_HOSTS[@]}"; do
  733. nfs_ensure_worker_volume "$h" "$i"
  734. if nfs_worker_has_model "$h"; then
  735. info "$h: $NFS_VOLUME has config.json"
  736. else
  737. die "$h cannot see the checkpoint over NFS. Check ACL (10.0.23.0/24 for spark3) and docker logs of the nfs exporter."
  738. fi
  739. i=$((i + 1))
  740. done
  741. info "spark2 + spark3 read $MODEL_DIR from spark1 over NFS (no local copy)"
  742. }
  743. cmd_mount() { cmd_share; }
  744. cmd_sync() {
  745. die "rsync of this 476 GiB checkpoint will not fit spark3. Use ./start.sh share (NFSv4 from spark1)."
  746. }
  747. _busy_gpu() {
  748. nvidia-smi --query-compute-apps=pid --format=csv,noheader 2>/dev/null | grep -q '[0-9]'
  749. }
  750. cmd_serve() {
  751. nccl_validate_config || die "invalid NCCL/loader configuration"
  752. DOCTOR_STRICT=0 cmd_doctor || true
  753. [[ -f "$MODEL_DIR/config.json" ]] || cmd_download
  754. ln -sfn "$MODEL_DIR" "$COMMON_MODEL"
  755. if _busy_gpu && [[ "${FORCE:-0}" != "1" ]]; then
  756. die "GPU is busy (glm53-exl3 or similar). Stop the other stack, or FORCE=1 ./start.sh serve"
  757. fi
  758. if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then
  759. cmd_build
  760. fi
  761. local h
  762. for h in "${WORKER_HOSTS[@]}"; do
  763. if ! remote_ok_on "$h" "docker image inspect $(printf '%q' "$IMAGE") >/dev/null 2>&1"; then
  764. info "image missing on $h — building"
  765. cmd_build
  766. break
  767. fi
  768. done
  769. preflight_all_nodes || die "NCCL preflight failed; see docs/switchless-ring.md"
  770. if [[ "$NFS_SHARE" == 1 ]]; then
  771. cmd_share
  772. else
  773. local_model_has_weights || die "head: $MODEL_DIR is missing/incomplete; NFS_SHARE=0 requires a complete local checkpoint"
  774. for h in "${WORKER_HOSTS[@]}"; do
  775. nfs_worker_has_model "$h" || die "$h: local volume $NFS_VOLUME is missing/incomplete. Provision local weights (docs/switchless-ring.md); NFS_SHARE=0 disables NFS setup."
  776. done
  777. fi
  778. API_KEY="$(api_key)"
  779. export API_KEY
  780. if [[ -n "$API_KEY" ]]; then
  781. (umask 077; printf '%s\n' "$API_KEY" > "$STATE_DIR/api-key")
  782. chmod 600 "$STATE_DIR/api-key"
  783. else
  784. rm -f "$STATE_DIR/api-key"
  785. fi
  786. local GID_HEAD gi g
  787. local -a WORKER_GIDS=()
  788. if [[ "$NCCL_SWITCHLESS_RING_ONLY" == 1 ]]; then
  789. GID_HEAD="$RING_GID_HEAD"
  790. WORKER_GIDS=("${RING_WORKER_GIDS[@]}")
  791. else
  792. GID_HEAD=$(gid_index_local "$HEAD_IP" 2>/dev/null || true)
  793. GID_HEAD="${GID_HEAD:-${NCCL_IB_GID_INDEX:-3}}"
  794. for gi in "${!WORKER_IPS[@]}"; do
  795. g=$(gid_index_remote "${WORKER_HOSTS[$gi]}" "${WORKER_IPS[$gi]}" | tr -d '\r' || true)
  796. WORKER_GIDS+=("${g:-${NCCL_IB_GID_INDEX:-3}}")
  797. done
  798. fi
  799. info "RoCEv2 GID indexes: head=$GID_HEAD workers=${WORKER_GIDS[*]}"
  800. docker rm -f "$HEAD_CTN" >/dev/null 2>&1 || true
  801. for h in "${WORKER_HOSTS[@]}"; do
  802. remote_on "$h" "docker rm -f $(printf '%q' "$WORKER_CTN") >/dev/null 2>&1 || true" || true
  803. done
  804. push_spec_tables
  805. info "Starting workers (ranks 1..${#WORKER_IPS[@]}) first..."
  806. local idx=0 wip wgid rank
  807. for h in "${WORKER_HOSTS[@]}"; do
  808. wip="${WORKER_IPS[$idx]}"
  809. wgid="${WORKER_GIDS[$idx]}"
  810. rank=$((idx + 1))
  811. remote_on "$h" "
  812. set -e
  813. docker volume inspect $(printf '%q' "$NFS_VOLUME") >/dev/null
  814. test -d /dev/infiniband
  815. mkdir -p $(printf '%q' "$WORKER_DIR/state") $(printf '%q' "$WORKER_DIR/logs")
  816. $(nccl_worker_settings)
  817. nccl_args=()
  818. nccl_mount_args nccl_args
  819. docker run -d --name $(printf '%q' "$WORKER_CTN") \
  820. --network host --ipc host --privileged --cap-add IPC_LOCK --gpus all \
  821. --shm-size $(printf '%q' "${SHM_SIZE:-32g}") \
  822. --ulimit memlock=-1:-1 --ulimit stack=67108864 \
  823. --device /dev/infiniband:/dev/infiniband \
  824. -v $(printf '%q' "$NFS_VOLUME:/models/DeepSeek-V4.1-Flash:ro") \
  825. -v $(printf '%q' "$WORKER_DIR/state:/state") \
  826. -v \"\$HOME/.cache:/root/.cache\" \
  827. \"\${nccl_args[@]}\" \\
  828. $(worker_env_lines "$wip" "$wgid" "$rank")
  829. -e $(printf '%q' "API_KEY=$API_KEY") \\
  830. -e $(printf '%q' "EXTRA_SGLANG_ARGS=${EXTRA_SGLANG_ARGS:-}") \\
  831. $(printf '%q' "$IMAGE") run
  832. "
  833. idx=$((idx + 1))
  834. done
  835. info "Starting head (rank 0, API :$PORT)..."
  836. local -a head_args=()
  837. docker_common_args head_args "$HEAD_IP" "$GID_HEAD"
  838. local head_cid=""
  839. head_cid=$(docker run -d --name "$HEAD_CTN" --restart=no \
  840. "${head_args[@]}" \
  841. -e NODE_RANK=0 \
  842. -e SKIP_SMOKE="$SKIP_SMOKE" \
  843. -e SMOKE_QUICK="$SMOKE_QUICK" \
  844. "$IMAGE" run)
  845. [[ -n "$head_cid" ]] || die "docker run produced no container id"
  846. info "head cid=${head_cid:0:12}"
  847. mkdir -p "$LOG_DIR"
  848. : >"$SERVE_LOG"
  849. _stop_logtail() {
  850. local p
  851. p=$(cat "$LOG_DIR/logtail.pid" 2>/dev/null || true)
  852. rm -f "$LOG_DIR/logtail.pid"
  853. if [[ -n "${p:-}" ]]; then
  854. # Children first (docker logs + tee), while they are still parented to the
  855. # subshell; killing the subshell first reparents them and they outlive us,
  856. # which kept the log streaming onto the terminal after "this script is done".
  857. pkill -TERM -P "$p" 2>/dev/null || true
  858. kill -TERM "$p" 2>/dev/null || true
  859. fi
  860. # Only the follower started by this script (exact command lines); never the engine.
  861. pkill -TERM -f "docker logs -f --tail=20 ${HEAD_CTN}" 2>/dev/null || true
  862. pkill -TERM -f "tee -a ${SERVE_LOG}" 2>/dev/null || true
  863. }
  864. # Live engine log on this TTY, also copied to $SERVE_LOG. Stopped when we
  865. # return to the shell (ready or fail) — the container keeps running.
  866. info "streaming engine log (returns to shell when the engine is up, smoke-tested and warmed up, or the head dies)..."
  867. ( docker logs -f --tail=20 "$HEAD_CTN" 2>&1 | tee -a "$SERVE_LOG" ) &
  868. echo $! >"$LOG_DIR/logtail.pid"
  869. trap '_stop_logtail' EXIT
  870. _dump_head_fail() {
  871. _stop_logtail
  872. trap - EXIT
  873. local st oom errstr
  874. st=$(docker inspect -f '{{.State.Status}}' "$HEAD_CTN" 2>/dev/null || echo missing)
  875. oom=$(docker inspect -f '{{.State.OOMKilled}}' "$HEAD_CTN" 2>/dev/null || echo '?')
  876. errstr=$(docker inspect -f '{{.State.Error}}' "$HEAD_CTN" 2>/dev/null || echo '')
  877. echo
  878. err "head is ${st} (oom=${oom}) cid=${head_cid:0:12}"
  879. [[ -n "$errstr" ]] && err "docker: $errstr"
  880. echo "---- $SERVE_LOG (tail) ----"
  881. tail -n 120 "$SERVE_LOG" 2>/dev/null || true
  882. echo "---- workers ----"
  883. local wh
  884. for wh in "${WORKER_HOSTS[@]}"; do
  885. echo "== $wh =="
  886. remote_on "$wh" "docker ps -a --filter name=$WORKER_CTN --format '{{.Names}} {{.Status}}'; docker logs --tail=40 $WORKER_CTN 2>/dev/null | tail -40" || true
  887. done
  888. }
  889. local i=0 st
  890. while (( i < ${READY_TIMEOUT:-360} )); do
  891. # Ready = boot.py printed its banner, i.e. /health answered AND the smoke test
  892. # and warm-up passed (WARMUP=0 / SKIP_SMOKE=1 skip them; then it is /health alone).
  893. if docker logs "$HEAD_CTN" 2>&1 | grep -q "^Ready: API on port ${PORT}" \
  894. || { [[ "${SKIP_SMOKE:-0}" == "1" ]] && curl -fsS --max-time 3 "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; }; then
  895. _stop_logtail
  896. trap - EXIT
  897. # Keep the engine log flowing into $SERVE_LOG after we return, detached from
  898. # this terminal (the TTY follower above is what was stopped). Without this the
  899. # file ends at readiness and a later failure leaves no trace once the
  900. # container is removed.
  901. ( setsid docker logs -f --since 1s "$HEAD_CTN" >>"$SERVE_LOG" 2>&1 </dev/null & ) 2>/dev/null
  902. echo
  903. info "API is up on :$PORT (smoke + warm-up passed) — engine keeps running, this script is done."
  904. cmd_status
  905. echo
  906. echo " curl http://$HEAD_IP:$PORT/v1/chat/completions \\"
  907. if [[ -n "$API_KEY" ]]; then
  908. echo " -H 'Authorization: Bearer ***' -H 'Content-Type: application/json' \\"
  909. else
  910. echo " -H 'Content-Type: application/json' \\"
  911. fi
  912. echo " -d '{\"model\":\"$SERVED_MODEL_NAME\",\"messages\":[{\"role\":\"user\",\"content\":\"What is 19 + 23?\"}],\"max_tokens\":32,\"chat_template_kwargs\":{\"thinking\":false}}'"
  913. echo
  914. [[ -n "$API_KEY" ]] && echo " key: $STATE_DIR/api-key" || echo " auth: none (no API key)"
  915. echo " logs: ./start.sh logs | ./start.sh logs -f | ./start.sh logs worker1"
  916. echo " stop: ./stop.sh"
  917. return 0
  918. fi
  919. st=$(docker inspect -f '{{.State.Status}}' "$HEAD_CTN" 2>/dev/null || echo missing)
  920. case "$st" in
  921. running|created|restarting) ;;
  922. *)
  923. _dump_head_fail
  924. exit 1
  925. ;;
  926. esac
  927. i=$((i + 1))
  928. sleep 10
  929. done
  930. _stop_logtail
  931. trap - EXIT
  932. die "timed out waiting for :$PORT — ./start.sh logs"
  933. }
  934. cmd_stop() {
  935. exec "$ROOT/stop.sh" "$@"
  936. }
  937. cmd_status() {
  938. echo "== head =="
  939. docker ps --filter "name=$HEAD_CTN" --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}' || true
  940. echo
  941. local h
  942. for h in "${WORKER_HOSTS[@]}"; do
  943. echo "== worker $h =="
  944. remote_on "$h" "docker ps --filter $(printf '%q' "name=$WORKER_CTN") --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'" || warn "status SSH $h failed"
  945. # Look inside $NFS_VOLUME, not at $COMMON_MODEL: the latter is a head-side symlink
  946. # to MODEL_DIR and never exists on a worker, so the old check reported
  947. # weights:MISSING on healthy workers in the NFS_SHARE=0 profile.
  948. if nfs_worker_has_model "$h"; then echo "weights:OK ($NFS_VOLUME)"; else echo "weights:MISSING ($NFS_VOLUME)"; fi
  949. echo
  950. done
  951. echo "== API =="
  952. local key
  953. key=$(cat "$STATE_DIR/api-key" 2>/dev/null || true)
  954. if curl -fsS --max-time 5 "http://127.0.0.1:${PORT}/v1/models" \
  955. || { [[ -n "$key" ]] && curl -fsS --max-time 5 -H "Authorization: Bearer $key" "http://127.0.0.1:${PORT}/v1/models"; }; then
  956. echo
  957. else
  958. echo "(not responding on :$PORT)"
  959. fi
  960. }
  961. cmd_logs() {
  962. local who="${1:-head}" lines="${2:-120}"
  963. case "$who" in
  964. ''|head|[0-9]*)
  965. [[ "$who" =~ ^[0-9]+$ ]] && lines="$who"
  966. docker logs --tail="$lines" "$HEAD_CTN" 2>&1 || tail -n "$lines" "$SERVE_LOG"
  967. ;;
  968. worker*|w[0-9]*)
  969. local n="${who#worker}"; n="${n#w}"; n="${n:-1}"
  970. [[ "$n" =~ ^[0-9]+$ && "$n" -ge 1 && "$n" -le ${#WORKER_HOSTS[@]} ]] || die "no such worker: $who (1..${#WORKER_HOSTS[@]})"
  971. remote_on "${WORKER_HOSTS[$((n - 1))]}" "docker logs --tail=$lines $WORKER_CTN" || true
  972. ;;
  973. -f|follow)
  974. docker logs -f "$HEAD_CTN"
  975. ;;
  976. *)
  977. docker logs --tail="$lines" "$HEAD_CTN" 2>&1 || true
  978. ;;
  979. esac
  980. }
  981. cmd_smoke() {
  982. local key auth=()
  983. key=$(cat "$STATE_DIR/api-key" 2>/dev/null || true)
  984. [[ -n "$key" ]] && auth=(-H "Authorization: Bearer $key")
  985. info "smoke: 19+23 (thinking off)"
  986. curl -fsS --max-time 180 "http://127.0.0.1:${PORT}/v1/chat/completions" \
  987. "${auth[@]}" -H 'Content-Type: application/json' \
  988. -d "{\"model\":\"$SERVED_MODEL_NAME\",\"temperature\":0,\"max_tokens\":32,\"chat_template_kwargs\":{\"thinking\":false},\"messages\":[{\"role\":\"user\",\"content\":\"What is 19 + 23? Reply only with the number.\"}]}"
  989. echo
  990. }
  991. usage() {
  992. sed -n '2,24p' "$0" | tr -d '#'
  993. }
  994. CMD="${1:-serve}"
  995. shift || true
  996. # Repack each rank's owned Engram rows onto node-local NVMe (~68 GiB/node).
  997. # A miss then costs one local read instead of two, and on a worker it stops
  998. # being an NFS round trip to the head. Idempotent: complete shards are skipped.
  999. cmd_pack() {
  1000. local src
  1001. src=$(model_src)
  1002. info "packing Engram shards: head $ENGRAM_DIR, workers $WORKER_ENGRAM_DIR"
  1003. mkdir -p "$ENGRAM_DIR"
  1004. docker run --rm --network host \
  1005. -v "$src:/models/DeepSeek-V4.1-Flash:ro" \
  1006. -v "$ENGRAM_DIR:/engram" \
  1007. -e DSV41_SOURCE=/models/DeepSeek-V4.1-Flash \
  1008. --entrypoint python3 "$IMAGE" \
  1009. /opt/dsv41/scripts/pack_engram.py --rank 0 --tp "$TP_SIZE" --out /engram \
  1010. || die "pack failed on head"
  1011. local idx=1 host
  1012. for host in "${WORKER_IPS[@]}"; do
  1013. info "packing rank $idx on $host..."
  1014. remote_on "$host" "mkdir -p $WORKER_ENGRAM_DIR && docker run --rm --network host \
  1015. -v $NFS_VOLUME:/models/DeepSeek-V4.1-Flash:ro \
  1016. -v $WORKER_ENGRAM_DIR:/engram \
  1017. -e DSV41_SOURCE=/models/DeepSeek-V4.1-Flash \
  1018. --entrypoint python3 $IMAGE \
  1019. /opt/dsv41/scripts/pack_engram.py --rank $idx --tp $TP_SIZE --out /engram" \
  1020. || die "pack failed on $host"
  1021. idx=$((idx + 1))
  1022. done
  1023. info "Engram shards packed on all 3 nodes"
  1024. }
  1025. case "$CMD" in
  1026. serve|start|"") cmd_serve "$@" ;;
  1027. doctor) cmd_doctor strict ;;
  1028. pull) cmd_pull ;;
  1029. build) cmd_build ;;
  1030. pack) cmd_pack ;;
  1031. download) cmd_download ;;
  1032. share|mount) cmd_share ;;
  1033. sync) cmd_sync ;;
  1034. stop) cmd_stop "$@" ;;
  1035. status) cmd_status ;;
  1036. logs) cmd_logs "$@" ;;
  1037. smoke) cmd_smoke ;;
  1038. -h|--help|help) usage ;;
  1039. *) die "unknown command: $CMD (try ./start.sh help)" ;;
  1040. esac