entrypoint.sh 56 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032
  1. #!/bin/bash
  2. set -euo pipefail
  3. # ---------------------------------------------------------------------------
  4. # Serve GLM-5.3-Flash (NVFP4) across the ConnectX-linked GX10 boxes. TP and PP
  5. # come from the environment, so the same image serves the TP=2/PP=2 pair-of-
  6. # pairs and a flat TP=4 across one fabric.
  7. #
  8. # Deliberately simpler than ds4-flash/entrypoint.sh in one respect: it loads
  9. # PLAIN SAFETENSORS and builds no sharded-state cache. That costs boot time and
  10. # buys correctness margin -- the sharded-state path needs the modelopt
  11. # IDEMPOTENT-GUARD patch, because a cache is dumped AFTER the NVFP4
  12. # kernel-format transform and re-running that transform on load permutes the
  13. # fused gate/up halves and serves fluent nonsense with no error. This image is
  14. # built on upstream's per-model base and carries none of our patches, so the
  15. # safe load is the one that runs the transform exactly once.
  16. #
  17. # Adopt the sharded-state cache only together with that patch. See
  18. # ds4-flash/vllm-spark.patch.
  19. # ---------------------------------------------------------------------------
  20. TP="${TP:-4}"
  21. MTP="${MTP:-1}"
  22. # TP=RING4 is TP=4 on four boxes cabled in a loop with no switch. mentat
  23. # places the ranks in cable order, and fabric_ring.py sets each rank's NCCL
  24. # and arx devices from the neighbours mentat gives it.
  25. FABRIC_LAYOUT=mesh
  26. if [[ "$TP" == RING4 ]]; then
  27. TP=4; FABRIC_LAYOUT=ring
  28. export MENTAT_CLAIM_LAYOUT=ring
  29. fi
  30. # --- per-TP defaults ---------------------------------------------------------
  31. # Each rank holds 1/TP of the weights and of every request's KDA state, so
  32. # the memory left for KV, and how many requests fit in it, depend on TP. These
  33. # fill in whatever .env leaves out. TP=4 is the measured four-box setup. TP=2
  34. # is two boxes with 89.6 GiB of weights per rank. Halving the batched-token
  35. # budget to 8192 shrinks the activation peak by ~2.8 GiB on the head, at
  36. # ~9% of prefill speed, and an 8 GiB KV pin then leaves the head 1.4 GiB free
  37. # at its lowest. At 16384 an 8 GiB pin got a worker OOM-killed mid-prefill.
  38. # Each rank carries half the KDA heads, so a request's states are twice as big.
  39. # RecoverSSM (recoverssm.yaml) keeps one state per request instead of 1 + k:
  40. # 64 run at TP=4, the most whose k=7 steps fit the largest captured graph,
  41. # and 16 at TP=2. It also removes the state
  42. # writes adaptive-k's step cost used to include, so it gets its own prior,
  43. # fitted on code and prose at two concurrencies with k forced to 2 and 7.
  44. _rs=0; [[ "${VLLM_GLM5NEXT_RECOVERSSM:-0}" == 1 ]] && _rs=1
  45. case "$TP" in
  46. 4) # KV_EXTRA_MEMORY (bytes) raises the default pin. dispram.yaml sets 2 GiB,
  47. # the RAM kindling-spark-os's 64 KiB kernel returns.
  48. : "${KV_CACHE_MEMORY:=$(( 27917287424 + ${KV_EXTRA_MEMORY:-0} ))}" "${MAX_MODEL_LEN:=524288}"
  49. (( _rs )) && : "${MAX_NUM_SEQS:=64}" "${VLLM_ADAPTIVE_K_MODEL:=26.4,0.660,0.484}"
  50. # With the drafter's KV in its own pool (fixes.yaml), ~50 requests' KDA
  51. # states fit in the pool; without it, 32.
  52. [[ "${VLLM_GLM5NEXT_DRAFT_POOL:-0}" == 1 ]] && : "${MAX_NUM_SEQS:=50}"
  53. : "${VLLM_ADAPTIVE_K_MODEL:=27.0,0.635,0.542}" ;;
  54. 3) # Three boxes on the zero-padded checkpoint (experimental/tp3). 26 GiB of
  55. # KV had the host OOM-killer take rank 0 during graph capture; 12 GiB
  56. # holds 1.4M tokens (1.7M with RecoverSSM). Block 3456 becomes 3584 in
  57. # vLLM: 14% mamba page padding, where the 4608 it picks from 2304 pads 46%
  58. # and coarsens the prefix cache.
  59. : "${KV_CACHE_MEMORY:=12884901888}" "${MAX_MODEL_LEN:=524288}" "${BLOCK_SIZE:=3456}"
  60. export BLOCK_SIZE
  61. # With RecoverSSM, 64 requests at k=7 fill the 512-token graph cap as at
  62. # TP=4: +24% aggregate at 48 streams over 32, single-stream unchanged.
  63. (( _rs )) && : "${MAX_NUM_SEQS:=64}"
  64. # What the online fit converged to at TP=3 with RecoverSSM on (3% mean
  65. # error; the two three-box sets agreed within 2%).
  66. : "${VLLM_ADAPTIVE_K_MODEL:=27.5,0.795,0.50}" ;;
  67. 6) # Six boxes on the same zero-padded checkpoint as TP=3 (66 / 66 / 2304
  68. # split as 11 heads and 384 MoE columns per rank). Weights are ~1/6 per
  69. # rank, so the TP=4 KV pin fits. With 11 KDA heads a rank's mamba page is
  70. # ~1573 tokens of attention page: block 2304 pads it 46%, 1792 (the
  71. # smallest multiple of 256 above it) 14%, for a 1.7% bigger pool and
  72. # 17% faster cached TTFT.
  73. : "${KV_CACHE_MEMORY:=27917287424}" "${MAX_MODEL_LEN:=524288}" "${BLOCK_SIZE:=1792}"
  74. export BLOCK_SIZE
  75. (( _rs )) && : "${MAX_NUM_SEQS:=64}"
  76. # What the online fit converged to at TP=6 with RecoverSSM on (1% error).
  77. : "${VLLM_ADAPTIVE_K_MODEL:=19.3,0.50,0.40}" ;;
  78. 2) : "${KV_CACHE_MEMORY:=8589934592}" "${MAX_MODEL_LEN:=163840}" "${MAX_NUM_BATCHED_TOKENS:=8192}"
  79. # A rank reads twice the expert weights it does at TP=4, so the
  80. # per-expert cost is held at twice TP=4's and the rest fitted.
  81. (( _rs )) && : "${MAX_NUM_SEQS:=16}" "${VLLM_ADAPTIVE_K_MODEL:=32.1,1.32,0.427}"
  82. : "${MAX_NUM_SEQS:=4}"
  83. : "${VLLM_ADAPTIVE_K_MODEL:=35.0,1.27,0.8}" ;;
  84. *) echo "FATAL: TP=$TP; this recipe is tuned for TP=4 or TP=RING4 (four boxes), TP=6 (six), TP=3 (three) or TP=2 (two)" >&2; exit 1 ;;
  85. esac
  86. export VLLM_ADAPTIVE_K_MODEL
  87. # SPEC_METHOD picks the drafter: dflash (the separate DFlash2 draft model at
  88. # DFLASH_MODEL, which must be mounted on every node; k must be 7, its block
  89. # size minus one, and the model refuses anything else), mtp (the checkpoint's
  90. # own head, num_nextn_predict_layers: 1), or none. MTP=0 is the older off
  91. # switch and still turns speculation off whichever method is named. Resolved
  92. # here, above the cache tag, because the tag hashes it.
  93. #
  94. # dflash is what serves: 109.8 / 88.8 / 52.6 tok/s on structured / code / prose
  95. # with v6, where MTP gave 57.2 / 54.4 / 45.6 with v4 (both 2026-09-23). Judge a drafter
  96. # on acceptance LENGTH, not acceptance rate. Set here rather than through
  97. # EXTRA_ARGS, which is one string that a compose override restating it
  98. # silently replaces.
  99. SPEC_METHOD="${SPEC_METHOD:-dflash}"
  100. [[ "$MTP" == "0" ]] && SPEC_METHOD=none
  101. # With neither ROLE nor HEAD_HOST set, the boxes elect a head once all TP
  102. # agents have registered (discover.sh). HEAD_HOST alone fixes the head, and
  103. # each box's role follows from whether it holds that address.
  104. . "$(dirname "$0")/discover.sh"
  105. _elect=""
  106. [[ -z "${ROLE:-}" && -z "${HEAD_HOST:-}" ]] && _elect=1
  107. MODEL="${MODEL_DIR:-/models/glm-5.3-flash-nvfp4}"
  108. SERVED="${SERVED_NAME:-glm53}"
  109. # --- cluster networking: everything rides the ConnectX link -----------------
  110. # No literal address appears here or in the compose file. The boxes are NOT
  111. # symmetric (one carries its link on enp1s0f1np1, the rest on enp1s0f0np0), so
  112. # any hardcoded interface name is wrong somewhere whichever you pick.
  113. #
  114. # VLLM_HOST_IP is this node's LAN address: mentat identifies a node by it, and
  115. # the agent and daemon must agree on one string. Address prefixes
  116. # (CLUSTER_SUBNET, FABRIC_SUBNETS) name the fabric. Whatever .env leaves out
  117. # comes from the local mentatd: its lan-tagged address, and one prefix per
  118. # rdma-tagged address.
  119. #
  120. # An uncabled port powers down completely -- no PCI device, no
  121. # /sys/class/infiniband entry. That is not a missing driver and no amount of
  122. # modprobe fixes it, so a node reports only the ports it actually has.
  123. discover_network
  124. CLUSTER_SUBNET="${CLUSTER_SUBNET:-${FABRIC_SUBNETS%% *}}"
  125. export VLLM_HOST_IP
  126. # Gloo needs an EXACT interface name -- NCCL_SOCKET_IFNAME takes a prefix, this
  127. # does not. Left unset, Gloo binds 127.0.0.1 and the cluster fails silently at
  128. # rendezvous, so derive it from whichever interface actually owns VLLM_HOST_IP.
  129. # Bounded, unlike the fabric wait above: the address is already up, so the
  130. # interface and its RDMA device are local state that either settles in seconds
  131. # or is broken. Waiting past that hides the fault instead of reporting it.
  132. if [[ -z "${GLOO_SOCKET_IFNAME:-}" ]]; then
  133. _waited=0
  134. while :; do
  135. GLOO_SOCKET_IFNAME=$(ip -o -4 addr show 2>/dev/null \
  136. | awk -v ip="$VLLM_HOST_IP" '$4 ~ "^"ip"/" {print $2; exit}')
  137. [[ -n "$GLOO_SOCKET_IFNAME" ]] && break
  138. if (( _waited >= ${ROCE_SETTLE_S:-60} )); then
  139. echo "FATAL: no interface holds VLLM_HOST_IP=$VLLM_HOST_IP after ${_waited}s" >&2
  140. ip -br addr show >&2; exit 1
  141. fi
  142. sleep 5; _waited=$(( _waited + 5 ))
  143. done
  144. fi
  145. export GLOO_SOCKET_IFNAME
  146. # --- fabric ports: which RoCE devices carry the cluster, at which GID -------
  147. # FABRIC_SUBNETS names one address prefix per cabled port. A second entry puts
  148. # NCCL on both PCIe roots of the ConnectX-7, which ib_write_bw measured at 196
  149. # Gb/s against 112 for one root alone. It is opt-in because the second root's
  150. # registrations are GPU-resident and land AFTER vLLM profiles, so they eat the
  151. # headroom a long prefill needs rather than the KV cache: allocations sat at
  152. # 111.41 GiB against the 104.6 GiB GPU_MEM_UTIL=0.86 budgets. At TP=2 that
  153. # bought nothing -- the per-token allreduce is ~720 KB, a fraction of a
  154. # millisecond against a 45 ms token -- so weigh it only at TP>2.
  155. #
  156. # Keyed on the subnet rather than on VLLM_HOST_IP because the two are no longer
  157. # the same address. mentat identifies a node by its LAN address and the agent
  158. # and daemon must agree on one string, so VLLM_HOST_IP is the LAN one, and no
  159. # RoCE GID will ever match it.
  160. FABRIC_SUBNETS="${FABRIC_SUBNETS:-$CLUSTER_SUBNET}"
  161. # Echoes "<rdma-device> <gid-index>" for the port holding an address in $1.
  162. # Returns 1 when this node has not cabled that port, or its GID has yet to
  163. # appear.
  164. fabric_port() {
  165. local prefix="$1" found addr ifname dev="" hex i t g d n
  166. found=$(ip -o -4 addr show 2>/dev/null \
  167. | awk -v p="$prefix" '$4 ~ "^"p {split($4,a,"/"); print $2, a[1]; exit}')
  168. [[ -n "$found" ]] || return 1
  169. ifname="${found%% *}"; addr="${found##* }"
  170. for d in /sys/class/infiniband/*; do
  171. for n in "$d"/ports/1/gid_attrs/ndevs/*; do
  172. [[ -f "$n" ]] || continue
  173. [[ "$(cat "$n" 2>/dev/null)" == "$ifname" ]] || continue
  174. dev="$(basename "$d")"; break 2
  175. done
  176. done
  177. [[ -n "$dev" ]] || return 1
  178. # Slots 0/1 hold the driver's MAC-derived GIDs whatever the IP configuration,
  179. # so match this port's own static address in its IPv4-mapped form, v2 only.
  180. hex=$(printf '%s' "$addr" | awk -F. '{printf "%02x%02x:%02x%02x", $1,$2,$3,$4}')
  181. for i in $(seq 0 15); do
  182. t=$(cat "/sys/class/infiniband/$dev/ports/1/gid_attrs/types/$i" 2>/dev/null) || continue
  183. g=$(cat "/sys/class/infiniband/$dev/ports/1/gids/$i" 2>/dev/null) || continue
  184. [[ "$t" == "RoCE v2" && "$g" == *"ffff:$hex" ]] || continue
  185. printf '%s %s\n' "$dev" "$i"; return 0
  186. done
  187. return 1
  188. }
  189. # There is no correct constant for the GID index. The table is keyed by
  190. # (address, RoCE version), slots are allocated first-free and freed in place, so
  191. # the index for one address differs per node AND per boot. Observed, not
  192. # theorised: on 2026-08-26 one node held the right entry at 6 and its peer at
  193. # 5, because a `nmcli con delete` / `add` had left the first a hole at slot 3. Reboot
  194. # it with the static profile already in place and the table comes up dense,
  195. # moving that 6 to 5, at which point a pinned 6 names an empty slot and NCCL
  196. # fails every TP init with "unhandled system error".
  197. if [[ -z "${NCCL_IB_HCA:-}" || -z "${NCCL_IB_GID_INDEX:-}" ]]; then
  198. _waited=0
  199. while :; do
  200. _hcas=""; _gid=""; _mismatch=""
  201. for _p in $FABRIC_SUBNETS; do
  202. _r=$(fabric_port "$_p") || continue
  203. _d="${_r%% *}"; _i="${_r##* }"
  204. [[ -n "$_gid" && "$_i" != "$_gid" ]] && _mismatch="$_d at $_i, expected $_gid"
  205. _gid="${_gid:-$_i}"
  206. _hcas="${_hcas:+$_hcas,}$_d"
  207. done
  208. [[ -n "$_hcas" ]] && break
  209. if (( _waited >= ${ROCE_SETTLE_S:-60} )); then
  210. echo "FATAL: no RoCE v2 GID for any of: $FABRIC_SUBNETS" >&2
  211. ip -br addr show >&2
  212. ls /sys/class/infiniband/ >&2 || echo "(no /sys/class/infiniband at all)" >&2
  213. exit 1
  214. fi
  215. sleep 5; _waited=$(( _waited + 5 ))
  216. done
  217. # NCCL_IB_GID_INDEX applies to every device in NCCL_IB_HCA, so a device whose
  218. # index differs gets asked for a GID it does not have and QP setup dies with
  219. # "local GID ::". One IPv4 per fabric interface keeps them aligned; refuse the
  220. # list rather than hand NCCL one that cannot work.
  221. # A ring gives each cable its own subnet, and fabric_ring.py picks each
  222. # device's GID by address instead.
  223. if [[ -n "$_mismatch" && "$FABRIC_LAYOUT" == mesh ]]; then
  224. echo "FATAL: fabric ports disagree on GID index ($_mismatch)." >&2
  225. echo "Each fabric interface must carry exactly one IPv4 address." >&2
  226. exit 1
  227. fi
  228. NCCL_IB_HCA="${NCCL_IB_HCA:-$_hcas}"
  229. NCCL_IB_GID_INDEX="${NCCL_IB_GID_INDEX:-$_gid}"
  230. echo "fabric: NCCL_IB_HCA=$NCCL_IB_HCA gid=$NCCL_IB_GID_INDEX (subnets: $FABRIC_SUBNETS)"
  231. else
  232. echo "fabric: NCCL_IB_HCA=$NCCL_IB_HCA gid=$NCCL_IB_GID_INDEX (both pinned; derivation skipped)"
  233. fi
  234. export NCCL_IB_HCA NCCL_IB_GID_INDEX
  235. # The EXACT interface holding VLLM_HOST_IP, not a prefix. This is only the
  236. # out-of-band bootstrap path -- the IB devices above carry the data -- and a
  237. # prefix matches every interface that happens to share it. Once the fabric
  238. # grew a second cable, "enp1s0f" matched both the head link and an unrelated
  239. # one, and NCCL bootstrapped toward the wrong wire: "Connection closed by
  240. # remote peer spark-head".
  241. export NCCL_SOCKET_IFNAME="${NCCL_SOCKET_IFNAME:-$GLOO_SOCKET_IFNAME}"
  242. # Channel count. NCCL picks 64 here, while every published GB10 recipe pins
  243. # 4-12. 8 was chosen on 2026-09-06 while the fabric was stuck at 12 Gb/s, before
  244. # a power drain fixed it; NCCL's own choice has not been re-tested since.
  245. export NCCL_MAX_NCHANNELS="${NCCL_MAX_NCHANNELS:-8}"
  246. # INFO prints the devices NCCL actually selected, which is the only way to
  247. # confirm both roots are in use.
  248. export NCCL_DEBUG="${NCCL_DEBUG:-INFO}"
  249. # --- preflight: things that make the stack slow or fragile without failing --
  250. # One table, WARN rows first to read, never fatal. PREFLIGHT=0 skips it.
  251. preflight() {
  252. local rows=() warns=0 dev nd mtu rate state phys cur max tp kv need avail swap fs free apps
  253. # Fields are split on the unit separator: fix text can hold "|" (echo 3 | sudo tee).
  254. local sep=$'\x1f'
  255. row() { rows+=("$1$sep$2$sep$3"); [[ "$3" == WARN ]] && warns=$(( warns + 1 )); return 0; }
  256. local -a devs; IFS=, read -r -a devs <<< "$(sed -E 's/^[=^]+//; s/:[0-9]+//g' <<< "$NCCL_IB_HCA")"
  257. (( ${#devs[@]} >= 2 )) && row "fabric devices" "${NCCL_IB_HCA}" ok \
  258. || row "fabric devices" "${NCCL_IB_HCA} (one PCIe root tops out near 110 Gb/s; set FABRIC_SUBNETS to both)" WARN
  259. local roots; roots=$(for dev in "${devs[@]}"; do basename "$(readlink -f "/sys/class/infiniband/$dev/device")" | cut -d: -f1-2; done | sort -u | wc -l)
  260. (( ${#devs[@]} < 2 || roots >= 2 )) || row "PCIe roots" "all fabric devices share one root" WARN
  261. for dev in "${devs[@]}"; do
  262. local p=/sys/class/infiniband/$dev
  263. [[ -d $p ]] || { row "$dev" "no such device" WARN; continue; }
  264. nd=$(ls "$p/device/net" 2>/dev/null | head -1)
  265. state=$(cut -d' ' -f2 "$p/ports/1/state"); phys=$(cut -d' ' -f2- "$p/ports/1/phys_state")
  266. [[ $state == ACTIVE ]] && row "$dev link" "$state, $phys" ok || row "$dev link" "$state, $phys" WARN
  267. rate=$(cut -d' ' -f1 "$p/ports/1/rate")
  268. (( rate >= 200 )) && row "$dev rate" "$(cat "$p/ports/1/rate")" ok || row "$dev rate" "$(cat "$p/ports/1/rate") (expected 200 Gb/sec)" WARN
  269. # RoCE's path MTU is the largest IB MTU that fits the netdev's: 9000 gives
  270. # 4096, 1500 gives 1024. arx and arxbig follow it, and smaller packets cost
  271. # bandwidth everywhere.
  272. mtu=$(cat "/sys/class/net/$nd/mtu" 2>/dev/null || echo 0)
  273. (( mtu >= 4200 )) && row "$nd MTU" "$mtu" ok || row "$nd MTU" "$mtu (RoCE path MTU below 4096; set 9000 end to end)" WARN
  274. cur="$(cut -d' ' -f1-2 "$p/device/current_link_speed") x$(cat "$p/device/current_link_width")"
  275. max="$(cut -d' ' -f1-2 "$p/device/max_link_speed") x$(cat "$p/device/max_link_width")"
  276. [[ $cur == "$max" ]] && row "$dev PCIe" "$cur" ok \
  277. || row "$dev PCIe" "$cur of $max (degraded link; a full power drain has fixed this before)" WARN
  278. # Each up/down is two changes; more than a few since boot means a flapping link.
  279. local flaps; flaps=$(cat "/sys/class/net/$nd/carrier_changes" 2>/dev/null || echo 0)
  280. (( flaps <= 4 )) || row "$nd flaps" "$flaps carrier changes since boot (check the cable or transceiver)" WARN
  281. local c v bad=""
  282. for c in local_ack_timeout_err packet_seq_err out_of_sequence; do
  283. v=$(cat "$p/ports/1/hw_counters/$c" 2>/dev/null || echo 0)
  284. (( v > 0 )) && bad+="$c=$v "
  285. done
  286. [[ -z $bad ]] || row "$dev retransmits" "${bad% } (since driver load; drops, likely no PFC)" info
  287. done
  288. # RDMA pins its buffers; a memlock limit fails registration or NCCL's setup.
  289. [[ "$(ulimit -l)" == unlimited ]] && row "memlock limit" "unlimited" ok \
  290. || row "memlock limit" "$(ulimit -l) KiB (RDMA registration needs unlimited: ulimits memlock -1)" WARN
  291. # GPU: a power-delivery or thermal event caps clocks for the whole boot.
  292. local ev; ev=$(nvidia-smi --query-gpu=clocks_event_reasons.hw_slowdown,clocks_event_reasons.hw_power_brake_slowdown,clocks_event_reasons.hw_thermal_slowdown,clocks_event_reasons.sw_thermal_slowdown,temperature.gpu --format=csv,noheader 2>/dev/null)
  293. if [[ -z $ev ]]; then row "GPU" "nvidia-smi unavailable" WARN
  294. elif awk -F', ' '{for (i = 1; i < NF; i++) if ($i == "Active") f = 1} END {exit !f}' <<< "$ev"; then
  295. row "GPU slowdown" "$ev (hw, power brake, hw thermal, sw thermal, C)" WARN
  296. else row "GPU slowdown" "none, ${ev##*, } C" ok; fi
  297. apps=$(nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv,noheader 2>/dev/null | tr '\n' ';')
  298. [[ -z $apps ]] && row "other GPU processes" "none" ok || row "other GPU processes" "${apps%;}" WARN
  299. # Host memory: ~182 GiB of weights split TP ways, the pinned KV, and ~30 GiB
  300. # more (CUDA graphs, the activation reserve, NCCL and RDMA buffers, the
  301. # processes themselves). TP=2 with 8 GiB of KV came to ~123 GiB and the OOM
  302. # killer took a worker mid-prefill.
  303. tp=${TP:-4}; kv=$(( ${KV_CACHE_MEMORY:-27917287424} >> 30 ))
  304. need=$(( 182 / tp + kv + 30 ))
  305. avail=$(( $(awk '/MemAvailable/ {print $2}' /proc/meminfo) >> 20 ))
  306. (( avail >= need )) && row "host memory" "${avail} GiB free, ~${need} GiB needed" ok \
  307. || row "host memory" "${avail} GiB free, ~${need} GiB needed (stop other models; lower KV_CACHE_MEMORY)" WARN
  308. # MemAvailable counts the page cache as free, but on GB10 CUDA allocations
  309. # can fail before the kernel reclaims it: NCCL's init died "out of memory"
  310. # with ~113 GB cached after a weight copy. Drop the model files' own cached
  311. # pages (no privilege needed), then say how to drop the rest.
  312. unfree() { echo $(( $(awk '/MemFree/ {print $2}' /proc/meminfo) >> 20 )); }
  313. if (( $(unfree) < need )); then
  314. python3 - "$MODEL" "${DFLASH_MODEL:-/models/glm-5.3-flash-dflash2}" "${VLLM_WEIGHT_SNAPSHOT_DIR:-}" <<'PY' 2>/dev/null || true
  315. import os, sys
  316. for root in filter(None, sys.argv[1:]):
  317. for dirpath, _, files in os.walk(root):
  318. for name in files:
  319. try:
  320. fd = os.open(os.path.join(dirpath, name), os.O_RDONLY)
  321. os.posix_fadvise(fd, 0, 0, os.POSIX_FADV_DONTNEED)
  322. os.close(fd)
  323. except OSError:
  324. pass
  325. PY
  326. fi
  327. local memfree; memfree=$(unfree)
  328. local cached; cached=$(( $(awk '/^Cached:/ {print $2}' /proc/meminfo) >> 20 ))
  329. (( memfree >= need )) && row "page cache" "${cached} GiB cached, ${memfree} GiB free" ok \
  330. || row "page cache" "${cached} GiB cached, ${memfree} GiB free, ~${need} GiB needed (GPU allocations may fail: on the host, sync; echo 3 | sudo tee /proc/sys/vm/drop_caches)" WARN
  331. swap=$(( ( $(awk '/SwapTotal/ {print $2}' /proc/meminfo) - $(awk '/SwapFree/ {print $2}' /proc/meminfo) ) >> 20 ))
  332. (( swap < 1 )) || row "swap in use" "${swap} GiB (the GPU shares this memory; paging stalls it)" WARN
  333. fs=$(findmnt -n -o FSTYPE -T "${MODEL_DIR:-/models/glm-5.3-flash-nvfp4}" 2>/dev/null)
  334. [[ $fs == nfs* || $fs == cifs || $fs == fuse* ]] && row "model filesystem" "$fs (every rank reads it all; use local disk)" WARN \
  335. || row "model filesystem" "${fs:-unknown}" ok
  336. # The first boot at a TP size writes a weight snapshot (snapshot.yaml).
  337. if [[ -n "${VLLM_WEIGHT_SNAPSHOT_DIR:-}" ]]; then
  338. free=$(df -BG --output=avail "${CACHE_ROOT:-/root/.cache}" 2>/dev/null | tail -1 | tr -dc 0-9)
  339. ver=$(sed -n 's/^SNAPSHOT_VERSION = \([0-9]*\).*/\1/p' \
  340. /usr/local/lib/python3.12/dist-packages/vllm/model_executor/model_loader/weight_snapshot.py 2>/dev/null)
  341. if compgen -G "$VLLM_WEIGHT_SNAPSHOT_DIR/v${ver}-target-tp*of${tp}-*" >/dev/null; then row "weight snapshot" "present for TP=$tp" ok
  342. elif (( ${free:-0} >= 182 / tp + 5 )); then row "weight snapshot" "none yet for TP=$tp, ${free} GiB free" ok
  343. else row "weight snapshot" "none yet for TP=$tp, ${free:-?} GiB free, needs ~$(( 182 / tp )) GiB" WARN; fi
  344. fi
  345. # A desktop session holds GPU memory and CPU on the shared memory, and these
  346. # boxes ship with GDM enabled. systemd lists running units in /run/systemd/units.
  347. if [[ -d /host/systemd-units ]]; then
  348. local dm; dm=$(ls /host/systemd-units 2>/dev/null \
  349. | sed -n 's/^invocation:\(gdm3\?\|lightdm\|sddm\|display-manager\)\.service$/\1/p' | head -1)
  350. [[ -z $dm ]] && row "desktop session" "none" ok \
  351. || row "desktop session" "$dm is running (sudo systemctl set-default multi-user.target && sudo systemctl isolate multi-user.target)" WARN
  352. fi
  353. grep -q '^search \.$' /etc/resolv.conf 2>/dev/null && row "resolv.conf" "'search .' frozen in; bare hostnames fail (restart the container)" WARN
  354. local on; on=$(overlays | tr '\n' ' ')
  355. [[ -n $on ]] && row "overlays" "$on" ok || row "overlays" "none: stock kernels" WARN
  356. echo "preflight ($warns warning$([[ $warns == 1 ]] || echo s)):"
  357. printf '%s\n' "${rows[@]}" | awk -F"$sep" '$3 == "WARN" {printf " %-5s %-26s %s\n", $3, $1, $2}'
  358. printf '%s\n' "${rows[@]}" | awk -F"$sep" '$3 != "WARN" {printf " %-5s %-26s %s\n", $3, $1, $2}'
  359. }
  360. # The experimental/ overlays this container has mounted, one name per line.
  361. # Each one mounts a file the image does not ship, except sp, which only sets
  362. # its environment.
  363. overlays() {
  364. local v=/usr/local/lib/python3.12/dist-packages/vllm
  365. [[ -e $v/distributed/device_communicators/arx.py ]] && echo arx
  366. [[ -e $v/model_executor/model_loader/weight_snapshot.py ]] && echo snapshot
  367. [[ -e $v/v1/core/sched/adaptive_k.py ]] && echo adaptive-k
  368. [[ -e $v/model_executor/model_loader/dense_fp8.py ]] && echo fp8
  369. [[ -e $v/model_executor/layers/fused_moe/megamoe_vllm.py ]] && echo megamoe
  370. [[ -e $v/v1/attention/backends/mla/gb10_sparse_mla.py ]] && echo fixes
  371. [[ -n "${VLLM_GLM_SP_TP:-}" ]] && echo sp
  372. [[ -e $v/models/glm5next/common/recoverssm.py ]] && echo recoverssm
  373. return 0
  374. }
  375. [[ "${PREFLIGHT:-1}" == 1 ]] && preflight || true
  376. # Without any overlay the stack runs vLLM's stock kernels at about half the
  377. # README's decode speed, and nothing else says so. Refuse to start instead.
  378. if [[ -z "$(overlays)" && "${ALLOW_STOCK:-0}" != 1 ]]; then
  379. echo "FATAL: no experimental overlays are mounted, so this would run stock kernels at about half the" \
  380. "README's speed. Start the stack with ./glm53 up -d (README step 5), or set ALLOW_STOCK=1" \
  381. "in compose/.env to run stock on purpose." >&2
  382. exit 1
  383. fi
  384. # --- worker memory ---------------------------------------------------------
  385. # Torch's caching allocator never hands a freed block back to the OS, and on
  386. # unified memory that block is host memory. The sparse indexer scores
  387. # chunk x context/kpool, so a session whose context keeps growing asks for a
  388. # slightly bigger block each step and strands the last one.
  389. # expandable_segments:True grows one segment instead of laddering.
  390. export PYTORCH_CUDA_ALLOC_CONF="${PYTORCH_CUDA_ALLOC_CONF:-expandable_segments:True}"
  391. # Hard ceiling on one worker's device memory, as a fraction of the 121.6 GiB,
  392. # read by spark_mem_trace.py. Keep it above GPU_MEM_UTIL by enough for a full
  393. # prefill's indexer scratch, and below 1.0 by whatever the host needs to keep
  394. # forking: past this a request raises OutOfMemoryError instead of the node
  395. # wedging. 0.92 is ~111.9 GiB against GPU_MEM_UTIL's ~107.
  396. export TORCH_MEM_FRACTION="${TORCH_MEM_FRACTION:-0.92}"
  397. if [[ -n "$_elect" ]]; then
  398. # Every box registers with its own daemon. The daemon relays to mentat's
  399. # head, and every group lives on the head.
  400. RAY_ADDRESS="${RAY_ADDRESS:-127.0.0.1:6379}"
  401. export RAY_ADDRESS
  402. else
  403. RAY_ADDRESS="${RAY_ADDRESS:-${HEAD_HOST:?set HEAD_HOST to the head node address}:6379}"
  404. export RAY_ADDRESS
  405. # One head per group. A worker left at ROLE=head starts a second driver:
  406. # mentat refuses it ("group ... already has an active driver session"), the
  407. # container restarts forever, and the real head waits for GPUs that never
  408. # join. Catch the mismatch here and say which setting.
  409. fixed_role
  410. case "$ROLE" in head|worker) ;; *) echo "FATAL: ROLE=$ROLE; set it to head or worker in .env" >&2; exit 1;; esac
  411. if [[ "$ROLE" == "head" && "$VLLM_HOST_IP" != "$_head_ip" ]]; then
  412. echo "FATAL: ROLE=head, but VLLM_HOST_IP=$VLLM_HOST_IP is not HEAD_HOST=$HEAD_HOST ($_head_ip). Set ROLE=worker in .env on every node except the head." >&2
  413. exit 1
  414. fi
  415. if [[ "$ROLE" == "worker" && "$VLLM_HOST_IP" == "$_head_ip" ]]; then
  416. echo "FATAL: ROLE=worker, but VLLM_HOST_IP=$VLLM_HOST_IP is HEAD_HOST. Set ROLE=head in .env on this node." >&2
  417. exit 1
  418. fi
  419. fi
  420. # mentat (the Ray replacement in this image) rendezvouses subclusters by
  421. # group; every rank of this deployment must carry the same value. Running the
  422. # same model twice means two compose stacks with DISTINCT MENTAT_GROUP values.
  423. export MENTAT_GROUP="${MENTAT_GROUP:-${SERVICE_NAME:-glm53}}"
  424. # The shim reads MENTAT_CLAIM_LAYOUT only when it makes a claim, which needs a
  425. # claim name.
  426. [[ "$FABRIC_LAYOUT" == ring ]] && export MENTAT_CLAIM="${MENTAT_CLAIM:-$MENTAT_GROUP}"
  427. # Pin the executor the mentat shim was audited against. It is vLLM's default
  428. # today (verified in this image AND the DS4 tree), but the default is
  429. # undocumented, and if an upstream bump flips it the legacy executor's
  430. # compiled-DAG surface comes alive -- which mentat does not implement and
  431. # fails loudly on. Pinning turns that failure into a grep-able one-liner.
  432. export VLLM_USE_RAY_V2_EXECUTOR_BACKEND=1
  433. # --- scheduler: per-step token budget, and one prefill's share of it --------
  434. # One setting, not two. The scheduler gives a long prefill
  435. # min(threshold, remaining budget), so a threshold at or above the budget lets
  436. # one big prompt take the entire step -- no decodes of running requests, no
  437. # admissions from the waiting queue. Measured on DS4 2026-08-25: a 90-token
  438. # "ping" sent 20s into a 198K-token prefill took 125.5s to answer.
  439. #
  440. # 16384 with each prefill capped at 2304, so one step carries several prefill
  441. # chunks and every running decode. Measured on TP=4 (2026-09-06): 8192 and
  442. # 16384 prefill a 200k prompt in the same time (234.1 s against 237.7 s); the
  443. # old 9.5x win for 8192 was a PP=2 pipeline bubble, gone at PP=1. Measure this
  444. # at 200k, never at 8k: an 8k prompt fits inside both budgets.
  445. export MAX_NUM_BATCHED_TOKENS="${MAX_NUM_BATCHED_TOKENS:-16384}"
  446. # --- max_num_seqs is capped by the KDA state, not by throughput -------------
  447. # 34 of this model's 45 layers are KDA linear attention, and a linear-attention
  448. # layer keeps a fixed-size RECURRENT STATE per sequence rather than a per-token
  449. # KV entry. vLLM accounts for those as "Mamba cache blocks", one per decode
  450. # sequence, and they are allocated out of whatever is left after weights.
  451. #
  452. # Measured here 2026-08-26: exactly 32 blocks fit at GPU_MEM_UTIL=0.86 with the
  453. # 181 GiB NVFP4 checkpoint. vLLM's default max_num_seqs of 256 therefore aborts
  454. # at CUDA graph capture, AFTER a full 10-minute weight load:
  455. #
  456. # ValueError: max_num_seqs (256) exceeds available Mamba cache blocks (32).
  457. # Each decode sequence requires one Mamba cache block, so CUDA graph capture
  458. # cannot proceed.
  459. #
  460. # This is a hard structural cap, not a tuning preference -- raising
  461. # GPU_MEM_UTIL is the only way to buy more blocks. The default takes all 32.
  462. #
  463. # Keep CUDAGRAPH_CAPTURE_SIZES's largest entry >= this value: with MTP off a
  464. # decode step is one token per sequence, so a full batch is exactly this many
  465. # tokens, and a step larger than the biggest captured size runs uncaptured.
  466. export MAX_NUM_SEQS="${MAX_NUM_SEQS:-32}"
  467. # One prefill's share of the step: 2304, one KDA block. A multiple of the block
  468. # matters -- 2048 produced alternating 2048/256 chunks. With it (2026-09-06),
  469. # prefill ran 2,004 tok/s at 200k and a 12-token ping sent under a 120k prefill
  470. # answered in 4.83 s. DECODE_RESERVE_TOKENS, when set, replaces it with
  471. # budget minus reserve, which is how DS4 expresses the same split.
  472. LONG_PREFILL_TOKEN_THRESHOLD="${LONG_PREFILL_TOKEN_THRESHOLD:-2304}"
  473. if [[ -n "${DECODE_RESERVE_TOKENS:-}" ]]; then
  474. LONG_PREFILL_TOKEN_THRESHOLD=$(( (MAX_NUM_BATCHED_TOKENS - DECODE_RESERVE_TOKENS) / 4 * 4 ))
  475. if (( LONG_PREFILL_TOKEN_THRESHOLD < 4 )); then
  476. echo "FATAL: DECODE_RESERVE_TOKENS=${DECODE_RESERVE_TOKENS} leaves only" >&2
  477. echo "${LONG_PREFILL_TOKEN_THRESHOLD} tokens for prefill out of a ${MAX_NUM_BATCHED_TOKENS} budget." >&2
  478. exit 1
  479. fi
  480. fi
  481. CUDAGRAPH_CAPTURE_SIZES="${CUDAGRAPH_CAPTURE_SIZES:-8 16 32 64 96 128 192 256}"
  482. echo "scheduler: budget=${MAX_NUM_BATCHED_TOKENS} prefill<=${LONG_PREFILL_TOKEN_THRESHOLD}" \
  483. "reserve=$(( MAX_NUM_BATCHED_TOKENS - LONG_PREFILL_TOKEN_THRESHOLD ))/step"
  484. export MAX_JOBS="${MAX_JOBS:-4}"
  485. # --- JIT/autotune cache tiers ----------------------------------------------
  486. # Two tiers, because the caches have different invalidation domains: the shared
  487. # tier is keyed on version/arch/kv-dtype only and is the expensive one to
  488. # rebuild, so it must stay stable across serving-option experiments; the keyed
  489. # tier holds torch.compile, which vLLM hashes against the whole serving config.
  490. CACHE_ROOT="${CACHE_ROOT:-/root/.cache}"
  491. _arch="$(nvidia-smi --query-gpu=compute_cap --format=csv,noheader 2>/dev/null | head -1 | tr -d ' .')"
  492. _arch="${_arch:-unknown}"
  493. # --- KV cache dtype: auto (bf16), NOT fp8 -----------------------------------
  494. # fp8 here selects vLLM's `fp8_ds_mla` packed KV format, which is DeepSeek's and
  495. # hardcodes pe_dim == 64:
  496. #
  497. # RuntimeError: concat_and_cache_mla, cache_kernels.cu:866,
  498. # pe_dim must be 64 for fp8_ds_mla
  499. #
  500. # GLM-5.3-Flash is NoPE sparse MLA -- `qk_rope_head_dim: 0`, no rotary on the
  501. # sparse path at all -- so pe_dim is 0 and that kernel refuses. This is what the
  502. # vendor recipe means by "FlashInfer 0.6.17+ is required for NoPE sparse MLA":
  503. # the NoPE variant is a different code path, not just a newer one.
  504. #
  505. # Copying ds4-flash's `--kv-cache-dtype fp8` across is therefore wrong: DS4 is
  506. # DeepSeek geometry (pe_dim 64) and this model is not. It costs a full weight
  507. # load (~10 min) to find out, because the failure is at KV cache init.
  508. #
  509. # The default below is fp8_e4m3, and it is what every measurement on this model
  510. # was taken with: the Korean corruption probe comes back clean and 200k recall
  511. # passes on it. Only 11 of 45 layers carry a KV cache and they use
  512. # kv_lora_rank 512, so bf16 would also fit -- but nothing here has been measured
  513. # on bf16, so do not switch on the assumption that it is the safer default.
  514. _kv="${KV_CACHE_DTYPE:-fp8_e4m3}"
  515. export SHARED_TAG="${SHARED_TAG:-glm53-${_arch}-${_kv}}"
  516. # Resolve ONCE, above every use. Two MAX_MODEL_LEN defaults in one
  517. # file is the "two places to change and one silently winning" trap the DS4
  518. # compose file warns about -- and it was live here: the cache tag said 131072
  519. # while the server was told 262144.
  520. #
  521. # 524288 of the model's 1M. KV is cheap here -- only 11 of 45 layers carry one,
  522. # at kv_lora_rank 512 -- so the 26 GiB pin below holds 2,632,595 tokens with
  523. # DFlash2, 5.02 requests at full length (head boot log, 2026-09-23).
  524. MAX_MODEL_LEN="${MAX_MODEL_LEN:-524288}"
  525. _optstr="tp=${TP} mtp=${MTP} spec=${SPEC_METHOD} len=${MAX_MODEL_LEN} cg=${CUDAGRAPH_CAPTURE_SIZES}"
  526. _opthash=$(printf '%s' "$_optstr" | sha256sum | cut -c1-8)
  527. export CACHE_TAG="${CACHE_TAG:-${SHARED_TAG}-${_opthash}}"
  528. # FlashInfer autotune: EPHEMERAL on TP>1, and wiped on every start. A persisted
  529. # cache deadlocks the NEXT boot, silently and forever. On the nightly, rank 0
  530. # reads its cache and broadcasts it, but only rank 0 ever saves, and the fused-
  531. # MoE entries it saves key per rank: ranks 1-3 miss what rank 0 hits, go off to
  532. # profile (a CPU-group all_reduce) while rank 0 has moved on to the model's NCCL
  533. # all_reduce, and each waits for the other. Measured 2026-09-23 with py-spy on
  534. # spark-glm53:v3. The first boot always works (nobody has a cache); every boot
  535. # after a good one hangs with the head at 96% GPU and the workers at 0%.
  536. # Wiping on start, not just keeping it out of the bind mount, matters: a head
  537. # engine restart keeps the container's /tmp. Costs ~2 min of autotune a boot.
  538. # AUTOTUNE_CACHE=persist brings the old behaviour back (TP=1 is safe with it).
  539. if [[ "${AUTOTUNE_CACHE:-}" == "persist" || "${TP:-1}" -le 1 ]]; then
  540. export VLLM_FLASHINFER_AUTOTUNE_CACHE_DIR="${CACHE_ROOT}/${SHARED_TAG}/flashinfer_autotune"
  541. else
  542. export VLLM_FLASHINFER_AUTOTUNE_CACHE_DIR=/tmp/flashinfer_autotune
  543. rm -rf "$VLLM_FLASHINFER_AUTOTUNE_CACHE_DIR"
  544. fi
  545. echo "autotune cache: $VLLM_FLASHINFER_AUTOTUNE_CACHE_DIR"
  546. export VLLM_CACHE_ROOT="${CACHE_ROOT}/${CACHE_TAG}/vllm"
  547. mkdir -p "$VLLM_FLASHINFER_AUTOTUNE_CACHE_DIR" "$VLLM_CACHE_ROOT"
  548. # FlashInfer's own JIT cache is deliberately NOT redirected. FLASHINFER_CACHE_DIR
  549. # looks like the knob but is a module-level constant computed from
  550. # FLASHINFER_WORKSPACE_BASE (default $HOME), so setting it does nothing at all.
  551. # $HOME/.cache is the bind mount, so the default already persists.
  552. export TRITON_CACHE_DIR="${TRITON_CACHE_DIR:-${CACHE_ROOT}/${SHARED_TAG}/triton}"
  553. mkdir -p "$TRITON_CACHE_DIR"
  554. # TileLang JIT builds the mHC (Manifold-Constrained Hyper-Connection) kernels
  555. # this architecture adds, and the DSA/MTP kernels. Its default is
  556. # ~/.tilelang/cache -- a SIBLING of the bind-mounted ~/.cache, so left alone it
  557. # lands inside the container and is destroyed on every `compose down`, and every
  558. # boot recompiles. Same invalidation domain as FlashInfer, hence the shared tier.
  559. export TILELANG_CACHE_DIR="${TILELANG_CACHE_DIR:-${CACHE_ROOT}/${SHARED_TAG}/tilelang}"
  560. mkdir -p "$TILELANG_CACHE_DIR"
  561. echo "cache shared: ${CACHE_ROOT}/${SHARED_TAG} (arch=${_arch} kv=${_kv})"
  562. echo "cache keyed : ${CACHE_ROOT}/${CACHE_TAG} ($_optstr)"
  563. echo "cache tilelang: $TILELANG_CACHE_DIR"
  564. # --- status page ------------------------------------------------------------
  565. # Own port, started immediately. Loading 181 GiB of weights takes long enough
  566. # that "connection refused" is a poor answer to "what is it doing?", and this
  567. # model's metrics read zero throughout a long prefill anyway.
  568. export STAGE_FILE="${STAGE_FILE:-/tmp/glm53-stage}"
  569. : > "$STAGE_FILE"
  570. stage() { echo "$1" >> "$STAGE_FILE"; echo "== stage: $1 =="; }
  571. STATUS_PORT="${STATUS_PORT:-8082}" PORT="${API_PORT:-8002}" \
  572. python3 /usr/local/bin/status-server.py &
  573. _status_pid=$!
  574. stage starting
  575. if [[ ! -f "$MODEL/config.json" ]]; then
  576. echo "FATAL: no config.json under MODEL_DIR=$MODEL." >&2
  577. echo "Both ranks read the full checkpoint, so it must be present on THIS node." >&2
  578. exit 1
  579. fi
  580. if [[ -d "${MCP_LOG_DIR:-/logs}" && -w "${MCP_LOG_DIR:-/logs}" ]]; then
  581. # An elected box has no role yet, and its role can change between boots.
  582. _log="${MCP_LOG_DIR:-/logs}/vllm${ROLE:+-$ROLE}.log"
  583. [[ -f "$_log" ]] && mv -f "$_log" "${_log%.log}.prev.log" 2>/dev/null || true
  584. echo "logging to $_log"
  585. exec > >(tee "$_log") 2>&1
  586. fi
  587. # NEUTRALIZED BY MENTAT, kept for real-ray fallback images (the flag is
  588. # accepted and ignored -- mentat has no object store at all, which is the
  589. # actual fix). The history, because it justifies mentat's existence:
  590. #
  591. # Ray's object store defaults to ~30% of RAM. On a discrete-GPU box that is
  592. # harmless -- it is host memory the model never wanted. GB10 is UNIFIED memory,
  593. # so it comes straight out of the pool the weights and KV cache need.
  594. #
  595. # Measured 2026-08-27: the head was capped at 4 GB but the worker was not, and
  596. # `ray status` reported 40.15 GiB of object store cluster-wide -- roughly 36 GiB
  597. # of it on the worker, for a model that passes tensors over NCCL and puts
  598. # essentially nothing in the object store.
  599. RAY_OBJECT_STORE_MEMORY="${RAY_OBJECT_STORE_MEMORY:-4294967296}"
  600. # NEUTRALIZED BY MENTAT, kept for real-ray fallback images (mentat has no
  601. # memory monitor; nothing in it ever kills a worker on a heuristic).
  602. #
  603. # Ray's memory monitor kills worker processes when the NODE crosses 95% memory.
  604. # That heuristic assumes host RAM and GPU memory are separate pools. On GB10
  605. # they are the same pool, so the ~89 GiB of resident model weights count as
  606. # host memory and the node sits at ~95% whenever the model is simply loaded.
  607. #
  608. # Measured 2026-08-27: rank 0 was killed three times (13:41, 13:52, 14:03),
  609. # each time seconds after the engine came up, with the raylet logging
  610. # "Memory on the node was 115.68GB / 121.63GB (0.951074)"
  611. # "Selected to kill: vllm_Worker_..._TP0, pid=3889, actual memory used=2.07GB"
  612. # Ray killed the 2 GB process because the 89 GiB of weights pushed the node
  613. # over its threshold. Nothing was leaking and no CUDA allocation had failed.
  614. #
  615. # This looked like a GPU OOM for hours: NV_ERR_NO_MEMORY appears in dmesg from
  616. # vLLM's own profiling probes, which allocate until they fail by design. Those
  617. # are noise. The kill decision is Ray's, and it is logged only in the raylet
  618. # event log, not in vLLM's output.
  619. #
  620. # 0 disables the monitor. Raising the threshold instead only moves the cliff:
  621. # there is no honest value when the weights alone are 73% of the pool.
  622. export RAY_memory_monitor_refresh_ms="${RAY_MEMORY_MONITOR_REFRESH_MS:-0}"
  623. # --- service announcement (mentat-serve) ------------------------------------
  624. # The agent reads these at `ray start` and carries them in its registration.
  625. # mentat-serve routes by what is announced, and probes it before believing
  626. # it. Purely additive: an image or daemon without this support ignores them.
  627. # Every rank announces its management MCP (the status server runs on both
  628. # roles). Only the head announces the OpenAI endpoint: a worker holds a TP
  629. # rank and serves nothing, and a URL announced from it would route requests
  630. # at a port with nothing behind it. The provider names the engine behind that
  631. # endpoint, which two servers answering /v1/chat/completions do not reveal by
  632. # answering it, so it goes on the rank that announces the endpoint.
  633. #
  634. # Port form (8002/v1): the router resolves it against every address the node
  635. # announces, so a router on the LAN and one on the fabric both reach it. A URL
  636. # pins one address, reachable only from that link. Both servers bind 0.0.0.0.
  637. export MENTAT_MCP_API="${MENTAT_MCP_API:-${STATUS_PORT:-8082}/mcp}"
  638. # --- head election ------------------------------------------------------------
  639. # Each box starts its agent first, with no OpenAI endpoint since the role is
  640. # not known yet, and then elects a head from the group's live agents
  641. # (discover.sh). The agent stays in the background: the head announces the
  642. # endpoint through `python -m ray.register` below, and a worker waits on it.
  643. _agent_pid=""
  644. if [[ -n "$_elect" ]]; then
  645. stage joining
  646. env -u MENTAT_OPENAI_API -u MENTAT_MODEL_PROVIDER \
  647. ray start --block --address="$RAY_ADDRESS" \
  648. --object-store-memory="$RAY_OBJECT_STORE_MEMORY" &
  649. _agent_pid=$!
  650. elect_head
  651. # The status server read ROLE once, before there was one, and took the
  652. # head's stages. A worker restarts it so its page reads as a worker's.
  653. if [[ "$ROLE" == "worker" ]]; then
  654. kill "$_status_pid" 2>/dev/null || true
  655. wait "$_status_pid" 2>/dev/null || true
  656. STATUS_PORT="${STATUS_PORT:-8082}" PORT="${API_PORT:-8002}" \
  657. python3 /usr/local/bin/status-server.py &
  658. fi
  659. fi
  660. if [[ "$ROLE" == "worker" ]]; then
  661. unset MENTAT_OPENAI_API
  662. else
  663. export MENTAT_OPENAI_API="${MENTAT_OPENAI_API:-${API_PORT:-8002}/v1}"
  664. export MENTAT_MODEL_PROVIDER="${MENTAT_MODEL_PROVIDER:-vllm}"
  665. fi
  666. # --- fabric check: every rank meets on the head before vLLM ------------------
  667. # Times an NCCL all-reduce and compares versions, override files and knobs
  668. # across nodes (fabric-check.py); on a ring it compares only. Warnings only;
  669. # FABRIC_CHECK=0 skips it, and a check that hangs is stopped.
  670. if [[ "${FABRIC_CHECK:-1}" == "1" ]]; then
  671. stage fabric-check
  672. FABRIC_LAYOUT="$FABRIC_LAYOUT" timeout "$(( 3 * ${FABRIC_CHECK_TIMEOUT_S:-120} ))" \
  673. python3 /usr/local/bin/fabric-check.py || echo "fabric check: failed or timed out; continuing"
  674. fi
  675. # --- worker: join and block -------------------------------------------------
  676. # Under mentat, `ray start --block --address` retries forever, so the
  677. # head-first ordering below stopped mattering -- start either side first. The
  678. # stage-file dance is kept because it still reads well on the status page,
  679. # and because a real-ray fallback image (where `ray start --address` does NOT
  680. # retry) needs the head up first.
  681. if [[ "$ROLE" == "worker" ]]; then
  682. stage joining
  683. ( for _ in $(seq 1 120); do
  684. if ray status --address="$RAY_ADDRESS" >/dev/null 2>&1; then
  685. stage worker-ready; break
  686. fi
  687. sleep 5
  688. done ) &
  689. # An elected worker's agent is already running. Wait on it as exec would:
  690. # the container exits when the agent does.
  691. if [[ -n "$_agent_pid" ]]; then
  692. wait "$_agent_pid"
  693. exit
  694. fi
  695. exec ray start --block --address="$RAY_ADDRESS" \
  696. --object-store-memory="$RAY_OBJECT_STORE_MEMORY"
  697. fi
  698. # --- head -------------------------------------------------------------------
  699. # --address names the daemon explicitly. mentat 0.7.0 refuses a bare --head
  700. # while RAY_ADDRESS is set ("--head names no daemon, so this agent would
  701. # register with the local one while RAY_ADDRESS names ..."): every agent of a
  702. # group and its driver must reach one daemon, and 0.6.0 let the two disagree
  703. # silently. On the head RAY_ADDRESS is its own daemon, so this changes nothing
  704. # about where it registers -- it only stops mentat having to guess.
  705. #
  706. # An elected head's agent started before the election and announced only the
  707. # MCP endpoint. `python -m ray.register` adds the OpenAI endpoint under a
  708. # second agent id, since two registrations with one id replace each other.
  709. if [[ -n "$_agent_pid" ]]; then
  710. python3 -m ray.register --address="$RAY_ADDRESS" \
  711. --container="${CONTAINER_NAME:-glm53}-api" --mcp= &
  712. else
  713. ray start --head --address="$RAY_ADDRESS" --node-ip-address="$VLLM_HOST_IP" --port=6379 \
  714. --object-store-memory="$RAY_OBJECT_STORE_MEMORY"
  715. fi
  716. # Starting the engine without its workers reaches NCCL and dies there
  717. # ("invalid usage"), so this waits rather than giving up and proceeding. The
  718. # stage stays visible on the status page throughout. WORKER_WAIT_S bounds it;
  719. # 0 waits forever, which is what a switch reboot wants.
  720. stage waiting-workers
  721. _waited=0
  722. while :; do
  723. have=$(ray status 2>/dev/null | grep -oE '[0-9.]+/[0-9.]+ GPU' | cut -d/ -f2 | cut -d. -f1 || echo 0)
  724. [[ "${have:-0}" -ge "$TP" ]] && break
  725. if (( ${WORKER_WAIT_S:-0} > 0 && _waited >= ${WORKER_WAIT_S:-0} )); then
  726. echo "FATAL: only ${have:-0} of "$TP" GPUs after ${_waited}s" >&2
  727. exit 1
  728. fi
  729. (( _waited % 60 )) || echo "waiting for "$TP" GPUs, have ${have:-0} (${_waited}s)"
  730. sleep 5
  731. _waited=$(( _waited + 5 ))
  732. done
  733. # The drafter was chosen at the top (SPEC_METHOD). A missing drafter would
  734. # otherwise fail only after the ~10 minute target load, so check it first.
  735. SPEC=()
  736. case "$SPEC_METHOD" in
  737. dflash)
  738. SPEC_TOKENS="${SPEC_TOKENS:-7}"
  739. DFLASH_MODEL="${DFLASH_MODEL:-/models/glm-5.3-flash-dflash2}"
  740. if [[ ! -f "$DFLASH_MODEL/config.json" ]]; then
  741. echo "FATAL: SPEC_METHOD=dflash but no config.json under DFLASH_MODEL=$DFLASH_MODEL." >&2
  742. echo "Mount the DFlash2 drafter there on every node, or set SPEC_METHOD=mtp." >&2
  743. exit 1
  744. fi
  745. SPEC=(--speculative-config "{\"method\":\"dflash\",\"model\":\"${DFLASH_MODEL}\",\"num_speculative_tokens\":${SPEC_TOKENS}}") ;;
  746. mtp)
  747. SPEC_TOKENS="${SPEC_TOKENS:-4}"
  748. SPEC=(--speculative-config "{\"method\":\"mtp\",\"num_speculative_tokens\":${SPEC_TOKENS}}") ;;
  749. none) ;;
  750. *) echo "FATAL: SPEC_METHOD=$SPEC_METHOD; want mtp, dflash or none" >&2; exit 1 ;;
  751. esac
  752. # SPEC_EXTRA: more keys for the speculative config above, as one JSON object.
  753. # adaptive-k.yaml puts its per-batch-size draft widths, the drafter's attention
  754. # backend and disable_eagle_block_drop here. They used to arrive as a second
  755. # --speculative-config in EXTRA_ARGS, and vLLM keeps the last one, so whenever
  756. # that overlay was loaded SPEC_METHOD, SPEC_TOKENS and DFLASH_MODEL were
  757. # silently ignored (SPEC_METHOD=mtp still served DFlash2). Draft widths above
  758. # SPEC_TOKENS are capped to it.
  759. if [[ ${#SPEC[@]} -gt 0 && "${EXTRA_ARGS:-}" == *--speculative-config* ]]; then
  760. echo "FATAL: EXTRA_ARGS sets --speculative-config too. vLLM keeps the last one, so SPEC_METHOD," \
  761. "SPEC_TOKENS and DFLASH_MODEL would be ignored. Put extra keys in SPEC_EXTRA (a JSON object) instead." >&2
  762. exit 1
  763. fi
  764. if [[ -n "${SPEC_EXTRA:-}" ]]; then
  765. if [[ "$SPEC_METHOD" != dflash ]]; then
  766. echo "FATAL: SPEC_EXTRA is set (adaptive-k.yaml sets it, tuned for DFlash2) but SPEC_METHOD=$SPEC_METHOD." \
  767. "Start without experimental/compose/adaptive-k.yaml to run $SPEC_METHOD." >&2
  768. exit 1
  769. fi
  770. SPEC[1]=$(python3 -c '
  771. import json, sys
  772. spec, extra = json.loads(sys.argv[1]), json.loads(sys.argv[2])
  773. if not isinstance(extra, dict): sys.exit("SPEC_EXTRA must be a JSON object")
  774. fixed = sorted(set(extra) & {"method", "model", "num_speculative_tokens"})
  775. if fixed: sys.exit("SPEC_EXTRA must not set " + ", ".join(fixed) + ": use SPEC_METHOD, DFLASH_MODEL and SPEC_TOKENS")
  776. n = spec["num_speculative_tokens"]
  777. for row in extra.get("num_speculative_tokens_per_batch_size", []): row[2] = min(row[2], n)
  778. spec.update(extra)
  779. print(json.dumps(spec, separators=(",", ":")))' "${SPEC[1]}" "$SPEC_EXTRA") || { echo "FATAL: SPEC_EXTRA: see above ($SPEC_EXTRA)" >&2; exit 1; }
  780. fi
  781. [[ ${#SPEC[@]} -gt 0 ]] && echo "speculative: ${SPEC[*]}"
  782. stage loading
  783. if [[ "${SELF_TEST:-1}" == "1" ]]; then
  784. (
  785. if python3 /usr/local/bin/self-test.py \
  786. --base "http://127.0.0.1:${API_PORT:-8002}" --model "$SERVED"; then
  787. stage serving
  788. else
  789. stage self-test-failed
  790. echo "!! SELF-TEST FAILED. Serving anyway so the model can be probed;" >&2
  791. echo "!! the status page reports unhealthy. A wrong-but-healthy model is" >&2
  792. echo "!! the specific failure this gate exists to catch." >&2
  793. fi
  794. ) &
  795. else
  796. ( for _ in $(seq 1 480); do
  797. curl -sf -o /dev/null --max-time 4 "http://127.0.0.1:${API_PORT:-8002}/v1/models" \
  798. && { stage serving; break; }
  799. sleep 5
  800. done ) &
  801. fi
  802. # --- tool parser --------------------------------------------------------------
  803. # The stock glm47 by default. TOOL_PARSER=glm47_failclosed selects the
  804. # fail-closed plugin baked at /usr/local/share (loaded with
  805. # --tool-parser-plugin), a stopgap for tool calls corrupted upstream. It
  806. # refuses any call to a tool the request did not list, which stalls clients
  807. # that reach deferred tools through a tool-search step (Qwen Code's MCP
  808. # tools). The stock parser drops such a call silently: this vLLM fixes
  809. # validate_tool_names=True in glm47_moe_config, with no flag to turn it off.
  810. # Selected here, not through EXTRA_ARGS: an override restating EXTRA_ARGS once
  811. # dropped the plugin from production.
  812. TOOL_PARSER="${TOOL_PARSER:-glm47}"
  813. TOOL_ARGS=(--enable-auto-tool-choice --tool-call-parser "$TOOL_PARSER")
  814. if [[ "$TOOL_PARSER" == "glm47_failclosed" ]]; then
  815. TOOL_ARGS=(--tool-parser-plugin /usr/local/share/glm47_failclosed.py "${TOOL_ARGS[@]}")
  816. fi
  817. echo "tool parser: $TOOL_PARSER"
  818. # --- sparse indexer top-k -------------------------------------------------------
  819. # GB10 cannot run persistent_topk once the KV pool passes ~3.4M tokens (it would
  820. # oversubscribe 48 CTAs), and the FilteredTopK fallback wants 128 KB of shared
  821. # memory per block against the 101,376 B an SM has. per_row is the same
  822. # computation without the persistent-CTA scheme. This used to be a patch
  823. # (gb10_topk_fallback.py); on main it is a flag. "auto" tries cooperative,
  824. # then persistent, then per_row.
  825. TOPK_BACKEND="${TOPK_BACKEND:-per_row}"
  826. TOPK_ARGS=(--sparse-indexer-topk-backend "$TOPK_BACKEND")
  827. # --load-format auto, NOT sharded_state -- see the header.
  828. #
  829. # Parser names do not match the model version, which is normal here: the vendor
  830. # recipe for 5.3-Flash specifies glm47 and glm45.
  831. # --- MoE backend and CUDA graphs -------------------------------------------
  832. # flashinfer_cutlass, the native NVFP4 kernel, which runs W4A4 off the
  833. # checkpoint's per-projection input scales. Production has served on it with
  834. # the nvidia checkpoint since 2026-09-21; why it replaced marlin then is not
  835. # recorded. It decodes 109.8 / 88.8 / 52.6 tok/s (structured / code / prose,
  836. # v6 + DFlash2, 2026-09-23) and passes the thinking-on count-to-200 probe 8/8.
  837. # It takes vLLM's own CUDA graph sizes; CUDAGRAPH_CAPTURE_SIZES applies to
  838. # marlin only.
  839. #
  840. # marlin was the default before that. It is weight-only: it dequantises to FP16
  841. # and runs bf16 activations, so a checkpoint's input scales are never read. It
  842. # was adopted when the native kernels died on sm_121 with
  843. # cudaErrorNoKernelImageForDevice, a failure both MiaAI-Lab and LibertAI later
  844. # put down to one checkpoint's uninitialised input_scale. It is also not
  845. # bit-deterministic at M >> 1 (dev/TOPK-CORRUPTION.md).
  846. #
  847. # Pairing marlin with --enforce-eager is recipe convention, not a correctness
  848. # constraint. Marlin's workspace helper reuses storage precisely so a captured
  849. # graph's addresses stay valid, and the only eager gate in this vLLM is for
  850. # DeepseekV4. Capture measures 6 s here. CUDA_GRAPHS=0 restores eager.
  851. #
  852. # Capture sizes are in TOKENS and vLLM rounds them to multiples of (1 + draft
  853. # tokens). Above max_num_seqs * (1 + k) the dispatcher returns NONE and decode
  854. # falls back to eager with nothing in the log to say so, which is why the
  855. # ceiling is computed and checked rather than left to whoever edits the list.
  856. MOE=()
  857. MOE_BACKEND="${MOE_BACKEND:-flashinfer_cutlass}"
  858. if [[ "$MOE_BACKEND" == "marlin" ]]; then
  859. if [[ "${CUDA_GRAPHS:-1}" == "1" ]]; then
  860. _k=1
  861. [[ -n "$SPEC_METHOD" && "$SPEC_METHOD" != none ]] && _k=$(( SPEC_TOKENS + 1 ))
  862. _ceil=$(( MAX_NUM_SEQS * _k ))
  863. _largest=$(tr ' ' '\n' <<<"$CUDAGRAPH_CAPTURE_SIZES" | sort -n | tail -1)
  864. if (( _largest > _ceil )); then
  865. echo "WARNING: largest capture size $_largest exceeds max_num_seqs*(1+k)=$_ceil;" >&2
  866. echo "decode above $_ceil tokens will run eager and log nothing." >&2
  867. fi
  868. MOE=(--moe-backend marlin
  869. --cudagraph-capture-sizes ${CUDAGRAPH_CAPTURE_SIZES}
  870. --compilation-config "{\"cudagraph_mode\":\"${CUDAGRAPH_MODE:-FULL_AND_PIECEWISE}\"}")
  871. echo "MoE backend: marlin, cudagraphs ${CUDAGRAPH_MODE:-FULL_AND_PIECEWISE}" \
  872. "(sizes: $CUDAGRAPH_CAPTURE_SIZES, ceiling ${_ceil})"
  873. else
  874. MOE=(--moe-backend marlin --enforce-eager)
  875. echo "MoE backend: marlin (enforce-eager, CUDA_GRAPHS=0)"
  876. fi
  877. else
  878. MOE=(--moe-backend "${MOE_BACKEND}")
  879. # vLLM captures graphs up to max_num_seqs x (1 + drafts) x 2, capped at
  880. # 512 tokens. The top half only serves steps that mix decodes with a short
  881. # new prompt, so stop at the largest decode step: 32 tokens at TP=2 instead
  882. # of 64, for example. Those mixed steps run without a graph.
  883. # A draft width that varies with batch size (adaptive-k.yaml's
  884. # num_speculative_tokens_per_batch_size, through SPEC_EXTRA) decodes 1 + k tokens per request
  885. # for each k in the schedule: 3, 5, 6, 10, 12, 15. vLLM adds those sizes to
  886. # its capture list only when it picks the ceiling itself, so an explicit cap
  887. # leaves them to run without a full graph. Leave the ceiling to vLLM there,
  888. # unless CUDAGRAPH_MAX sets one.
  889. if [[ -z "${CUDAGRAPH_MAX:-}" && "${SPEC[*]:-}" == *num_speculative_tokens_per_batch_size* ]]; then
  890. echo "MoE backend: ${MOE_BACKEND}, vLLM's own CUDA graph sizes (draft width varies with batch size)"
  891. else
  892. _q=1; [[ -n "$SPEC_METHOD" && "$SPEC_METHOD" != none ]] && _q=$(( SPEC_TOKENS + 1 ))
  893. _cgmax=$(( MAX_NUM_SEQS * _q < 512 ? MAX_NUM_SEQS * _q : 512 ))
  894. CUDAGRAPH_MAX="${CUDAGRAPH_MAX:-$_cgmax}"
  895. MOE+=(--max-cudagraph-capture-size "$CUDAGRAPH_MAX")
  896. echo "MoE backend: ${MOE_BACKEND}, CUDA graphs up to ${CUDAGRAPH_MAX} tokens"
  897. fi
  898. fi
  899. # Multimodal: up to 16 images a prompt, no video. Exceeding a cap is a clean
  900. # 400 with an OpenAI-shaped error body, not an engine fault, but a client that
  901. # treats any 400 as fatal dies on it.
  902. # The closing brace is escaped: bash ends a :- default at the first unescaped
  903. # one, so it would cut the default short and append the tail as literal text.
  904. LIMIT_MM="${LIMIT_MM:-{\"image\":16,\"video\":0\}}"
  905. python3 -c 'import json, sys; json.loads(sys.argv[1])' "$LIMIT_MM" 2>/dev/null || {
  906. echo "FATAL: LIMIT_MM is not valid JSON: $LIMIT_MM" >&2
  907. echo " Write the whole object, closing brace included." >&2
  908. exit 1; }
  909. MM=(--limit-mm-per-prompt "$LIMIT_MM")
  910. # 0: run the max-size dummy forward at init, so the vision encoder's peak is
  911. # budgeted at startup instead of coming out of the headroom a long prefill
  912. # needs at run time. 1 skips it, which is faster to boot and was the default
  913. # while images were capped at 4.
  914. [[ "${SKIP_MM_PROFILING:-0}" == "1" ]] && MM+=(--skip-mm-profiling)
  915. # The image's template is nvidia's, with one change: thinking off asks for low
  916. # reasoning effort. GLM-5.3-Flash has no non-thinking mode, and the empty
  917. # <think></think> that thinking off otherwise produces makes long output repeat
  918. # and skip (README, Troubleshooting). The checkpoint's own template is ignored,
  919. # so a fresh download cannot bring the problem back. CHAT_TEMPLATE overrides.
  920. TMPL=()
  921. : "${CHAT_TEMPLATE:=/usr/local/share/glm53-chat-template.jinja}"
  922. if [[ -n "$CHAT_TEMPLATE" ]]; then
  923. TMPL=(--chat-template "$CHAT_TEMPLATE")
  924. echo "chat template: $CHAT_TEMPLATE"
  925. fi
  926. # A 320B MoE takes far longer to init than the default allows.
  927. export VLLM_ENGINE_READY_TIMEOUT_S="${VLLM_ENGINE_READY_TIMEOUT_S:-3600}"
  928. # --- KV cache pin ----------------------------------------------------------
  929. # Left unset, vLLM PROFILES the KV cache and takes everything available -- here
  930. # that was 13.59 GiB, giving 796,361 tokens. FlashInfer's autotune then runs
  931. # during warmup, asks the driver for more, and there is none:
  932. #
  933. # NVRM: Check failed: Out of memory [NV_ERR_NO_MEMORY] ... _memdescAllocInternal
  934. # RayWorkerProc rank=[0] died unexpectedly
  935. #
  936. # On a discrete GPU the profiler's headroom assumptions usually survive this.
  937. # On GB10's unified memory they do not, which is what the upstream recipe means
  938. # by "--kv-cache-memory pin (UMA OOM only)". Pin it below the profiled figure so
  939. # warmup has room.
  940. #
  941. # Measured 2026-08-27: a 10 GiB pin made things WORSE, because skipping the
  942. # profile also discards the gpu_memory_utilization margin autotune relies on.
  943. # That was before TORCH_MEM_FRACTION and the ephemeral autotune cache.
  944. #
  945. # 26 GiB is the pin that serves under DFlash2. At 28 the head sat near 1 GiB
  946. # free and eight concurrent long-context requests had a worker OOM-killed. A
  947. # pin also stops the pool moving by ~0.9 GiB between identical boots with
  948. # page-cache timing at profiling. DFlash2 costs 41% of the pool: 3,437,736
  949. # tokens with speculation off against 2,024,644 with it (pre-nightly image,
  950. # 2026-09-06).
  951. KV_CACHE_MEMORY="${KV_CACHE_MEMORY:-27917287424}"
  952. KV_ARGS=(--kv-cache-memory "${KV_CACHE_MEMORY}")
  953. echo "kv-cache-memory pinned to ${KV_CACHE_MEMORY} bytes ($(( KV_CACHE_MEMORY / 1073741824 )) GiB)"
  954. # eager stages each shard through anonymous memory and loads faster -- 511 s
  955. # against 690 s for lazy. Unpinned, its buffers were still resident when vLLM
  956. # profiled for KV and cost 38% of the cache: 810,576 tokens against 1,296,922.
  957. # With the KV pin above the pool is not profiled, and production runs eager.
  958. # Go back to lazy if the pin is ever dropped.
  959. exec vllm serve "$MODEL" \
  960. --served-model-name "$SERVED" \
  961. --tensor-parallel-size "$TP" \
  962. --distributed-executor-backend ray \
  963. --load-format auto \
  964. --safetensors-load-strategy "${SAFETENSORS_LOAD_STRATEGY:-eager}" \
  965. --gpu-memory-utilization "${GPU_MEM_UTIL:-0.88}" \
  966. --max-model-len "${MAX_MODEL_LEN}" \
  967. --kv-cache-dtype "${KV_CACHE_DTYPE:-fp8_e4m3}" \
  968. --block-size "${BLOCK_SIZE:-2304}" \
  969. "${KV_ARGS[@]}" \
  970. --trust-remote-code \
  971. --enable-prefix-caching \
  972. --max-num-batched-tokens "${MAX_NUM_BATCHED_TOKENS}" \
  973. --max-num-seqs "${MAX_NUM_SEQS}" \
  974. --long-prefill-token-threshold "${LONG_PREFILL_TOKEN_THRESHOLD}" \
  975. "${MOE[@]}" \
  976. "${MM[@]}" \
  977. "${TMPL[@]}" \
  978. ${ITERATION_DETAILS:+--enable-logging-iteration-details} \
  979. "${TOOL_ARGS[@]}" \
  980. "${TOPK_ARGS[@]}" \
  981. --reasoning-parser glm45 \
  982. "${SPEC[@]}" \
  983. --host 0.0.0.0 --port "${API_PORT:-8002}" \
  984. ${EXTRA_ARGS:-}