nfs-share.sh 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. # files/nfs-share.sh — spark2/spark3 read spark1's checkpoint over NFSv4 on ConnectX.
  2. # Sourced by start.sh. Same pattern as vllm-fn-nfs / GLM-5.3 (no local 476 GiB copy).
  3. #
  4. # Pairwise CX7: spark2 talks to head 10.0.22.1, spark3 talks to head 10.0.23.1.
  5. # Live kernel nfsd (vllm-fn-nfs) is reused when present — a second nfsd will not start.
  6. NFS_IMAGE="${NFS_IMAGE:-dsv41-nfs:local}"
  7. NFS_CONTAINER="${NFS_CONTAINER:-dsv41-nfs}"
  8. NFS_VOLUME="${NFS_VOLUME:-dsv41-weights}"
  9. NFS_DOCKERFILE_DIR="${NFS_DOCKERFILE_DIR:-$ROOT/files/nfs-server}"
  10. NFS_EXPORT_NAME="${NFS_EXPORT_NAME:-dsv41-native}"
  11. HF_EXPORT_ROOT="${HF_EXPORT_ROOT:-$HOME/.cache/huggingface}"
  12. NFS_OPTS_CLIENT="${NFS_OPTS_CLIENT:-nfsvers=4.2,ro,nconnect=8,rsize=1048576,wsize=1048576,hard,timeo=600}"
  13. nfs_clients() {
  14. echo "${NFS_CLIENTS:-10.0.0.2,10.0.0.3,10.0.22.0/24,10.0.23.0/24,10.0.33.0/24}"
  15. }
  16. # Head ConnectX address the given worker should mount (not 10.0.0.1).
  17. nfs_server_ip_for() {
  18. local host="$1" idx="$2"
  19. # Per-worker override: NFS_SERVER_IPS="10.0.22.1 10.0.23.1 ..." (one entry per
  20. # worker, or a single entry used for all, e.g. behind a switch), else the legacy
  21. # NFS_SERVER_IP_<n>, else NFS_SERVER_IP.
  22. local explicit=""
  23. local -a _nfs_ips=()
  24. if [[ -n "${NFS_SERVER_IPS:-}" ]]; then
  25. read -r -a _nfs_ips <<<"$(tr ',' ' ' <<<"$NFS_SERVER_IPS")"
  26. if [[ ${#_nfs_ips[@]} -eq 1 ]]; then explicit="${_nfs_ips[0]}"; else explicit="${_nfs_ips[$idx]:-}"; fi
  27. fi
  28. if [[ -z "$explicit" ]]; then
  29. local _v="NFS_SERVER_IP_$((idx + 1))"
  30. explicit="${!_v:-${NFS_SERVER_IP:-}}"
  31. fi
  32. if [[ -n "$explicit" ]]; then
  33. echo "$explicit"
  34. return 0
  35. fi
  36. local peer="${WORKER_IPS[$idx]}"
  37. # Prefer the CX7 source address toward that worker's fabric IP if set:
  38. # WORKER_FABRIC_IPS="10.0.22.2 10.0.23.3 ..." or legacy WORKER<n>_FABRIC_IP.
  39. local fabric_peer=""
  40. local -a _fab=()
  41. if [[ -n "${WORKER_FABRIC_IPS:-}" ]]; then
  42. read -r -a _fab <<<"$(tr ',' ' ' <<<"$WORKER_FABRIC_IPS")"
  43. fabric_peer="${_fab[$idx]:-}"
  44. fi
  45. if [[ -z "$fabric_peer" ]]; then
  46. local _f="WORKER$((idx + 1))_FABRIC_IP"
  47. case "$idx" in
  48. 0) fabric_peer="${!_f:-10.0.22.2}" ;;
  49. 1) fabric_peer="${!_f:-10.0.23.3}" ;;
  50. *) fabric_peer="${!_f:-$peer}" ;;
  51. esac
  52. fi
  53. local src
  54. src=$(ip route get "$fabric_peer" 2>/dev/null \
  55. | awk '{ for (i=1;i<=NF;i++) if ($i=="src") { print $(i+1); exit } }')
  56. if [[ -n "$src" && "$src" != "10.0.0.1" && "$src" != "127.0.0.1" ]]; then
  57. echo "$src"
  58. return 0
  59. fi
  60. src=$(ip route get "$peer" 2>/dev/null \
  61. | awk '{ for (i=1;i<=NF;i++) if ($i=="src") { print $(i+1); exit } }')
  62. echo "${src:-10.0.22.1}"
  63. }
  64. nfs_live_container() {
  65. docker ps --format '{{.Names}}' | grep -E '^(vllm-fn-nfs|glm53-nfs|dsv41-nfs)$' | head -1
  66. }
  67. nfs_rpc_ready() {
  68. local ip="$1"
  69. timeout 3 rpcinfo -t "$ip" nfs 4 >/dev/null 2>&1
  70. }
  71. nfs_write_exports() {
  72. local ctn="$1" clients="$2"
  73. docker exec -e NFS_CLIENTS="$clients" -e NFS_OPTS="${NFS_OPTS:-ro,sync,no_subtree_check,no_root_squash,insecure,fsid=0}" \
  74. "$ctn" bash -lc '
  75. set -e
  76. {
  77. echo "# generated by dsv41-nfs (merged clients)"
  78. IFS="," read -ra addrs <<< "$NFS_CLIENTS"
  79. for addr in "${addrs[@]}"; do
  80. addr="${addr// /}"
  81. [ -z "$addr" ] && continue
  82. echo "/export ${addr}(${NFS_OPTS})"
  83. done
  84. } > /etc/exports
  85. exportfs -rav
  86. '
  87. }
  88. nfs_ensure_server() {
  89. local clients
  90. clients="$(nfs_clients)"
  91. local live
  92. live="$(nfs_live_container || true)"
  93. if [[ -n "$live" ]] && nfs_rpc_ready 127.0.0.1; then
  94. local export_source
  95. export_source=$(docker inspect -f '{{range .Mounts}}{{if eq .Destination "/export"}}{{.Source}}{{end}}{{end}}' "$live")
  96. if [[ "$export_source" == "$MODEL_DIR" ]]; then
  97. NFS_REUSE_EXPORT=0
  98. elif [[ "$export_source" == "$HF_EXPORT_ROOT" ]]; then
  99. NFS_REUSE_EXPORT=1
  100. else
  101. die "NFS $live exports $export_source; expected $MODEL_DIR or $HF_EXPORT_ROOT"
  102. fi
  103. info "NFS already up ($live) — configuring worker clients"
  104. nfs_write_exports "$live" "$clients"
  105. NFS_LIVE_CTN="$live"
  106. return 0
  107. fi
  108. [[ -d "$NFS_DOCKERFILE_DIR" ]] || die "missing $NFS_DOCKERFILE_DIR"
  109. [[ -d "$MODEL_DIR" ]] || die "checkpoint missing: $MODEL_DIR"
  110. info "building NFS image $NFS_IMAGE"
  111. docker build -q -t "$NFS_IMAGE" "$NFS_DOCKERFILE_DIR" >/dev/null
  112. docker rm -f "$NFS_CONTAINER" >/dev/null 2>&1 || true
  113. info "exporting $MODEL_DIR via NFS (clients: $clients)"
  114. docker run -d --name "$NFS_CONTAINER" --restart unless-stopped \
  115. --privileged --network host \
  116. -v "$MODEL_DIR:/export:ro" \
  117. -e "NFS_CLIENTS=$clients" \
  118. "$NFS_IMAGE" >/dev/null
  119. local i
  120. for i in $(seq 1 20); do
  121. if nfs_rpc_ready 127.0.0.1; then
  122. info "NFS server ready ($NFS_CONTAINER)"
  123. NFS_REUSE_EXPORT=0
  124. NFS_LIVE_CTN="$NFS_CONTAINER"
  125. return 0
  126. fi
  127. sleep 0.5
  128. done
  129. docker logs "$NFS_CONTAINER" >&2 || true
  130. die "NFS server did not become ready. Check: docker logs $NFS_CONTAINER"
  131. }
  132. # When reusing vllm-fn-nfs (HF cache as NFSv4 root), publish the checkpoint
  133. # into that tree with hardlinks (same ext4, zero extra bytes).
  134. nfs_publish_model() {
  135. if [[ "${NFS_REUSE_EXPORT:-0}" != "1" ]]; then
  136. NFS_DEVICE=":/"
  137. return 0
  138. fi
  139. local dst="${HF_EXPORT_ROOT}/${NFS_EXPORT_NAME}"
  140. mkdir -p "$dst"
  141. if [[ -f "$dst/config.json" ]]; then
  142. local n
  143. n=$(find "$dst" -maxdepth 1 -name 'model-*-of-*.safetensors' 2>/dev/null | wc -l | tr -d ' ')
  144. if [[ "${n:-0}" -ge "$EXPECTED_SHARDS" ]]; then
  145. info "NFS export already has $NFS_EXPORT_NAME ($n shards)"
  146. NFS_DEVICE=":/${NFS_EXPORT_NAME}"
  147. return 0
  148. fi
  149. fi
  150. info "publishing $MODEL_DIR → $dst (hardlinks into the live NFS export)"
  151. cp -a --link "$MODEL_DIR"/. "$dst"/ 2>/dev/null \
  152. || cp -al "$MODEL_DIR"/. "$dst"/
  153. [[ -f "$dst/config.json" ]] || die "failed to publish checkpoint into $dst"
  154. NFS_DEVICE=":/${NFS_EXPORT_NAME}"
  155. }
  156. nfs_ensure_worker_volume() {
  157. local host="$1" idx="$2"
  158. local addr device
  159. addr="$(nfs_server_ip_for "$host" "$idx")"
  160. device="${NFS_DEVICE:-:/${NFS_EXPORT_NAME}}"
  161. info "NFS volume $NFS_VOLUME on $host → nfs://${addr}${device}"
  162. remote_on "$host" "
  163. set -e
  164. docker volume rm $(printf '%q' "$NFS_VOLUME") >/dev/null 2>&1 || true
  165. docker volume create --driver local \
  166. --opt type=nfs \
  167. --opt $(printf '%q' "o=addr=${addr},${NFS_OPTS_CLIENT}") \
  168. --opt $(printf '%q' "device=${device}") \
  169. $(printf '%q' "$NFS_VOLUME") >/dev/null
  170. "
  171. }
  172. local_model_has_weights() {
  173. [[ -r "$MODEL_DIR/config.json" && -s "$MODEL_DIR/config.json" ]] || return 1
  174. local -a shards=("$MODEL_DIR"/model-*-of-*.safetensors)
  175. [[ ${#shards[@]} -ge "$EXPECTED_SHARDS" ]] || return 1
  176. local shard
  177. for shard in "${shards[@]}"; do
  178. [[ -r "$shard" && -s "$shard" ]] || return 1
  179. done
  180. }
  181. nfs_worker_has_model() {
  182. local host="$1"
  183. # Inspect first: docker run -v would create an empty volume on a typo.
  184. # Use the serving image already required by serve; status must not pull an
  185. # unrelated mutable image, and the probe needs neither network nor GPUs.
  186. local check='test -r /m/config.json && test -s /m/config.json || exit 1
  187. expected=$1
  188. set -- /m/model-*-of-*.safetensors
  189. test "$#" -ge "$expected" || exit 1
  190. for shard do test -r "$shard" && test -s "$shard" || exit 1; done'
  191. remote_on "$host" --timeout 180 \
  192. "docker volume inspect $(printf '%q' "$NFS_VOLUME") >/dev/null 2>&1 &&
  193. docker run --rm --pull=never --network none \
  194. --mount $(printf '%q' "type=volume,source=$NFS_VOLUME,target=/m,readonly") \
  195. --entrypoint /bin/sh $(printf '%q' "$IMAGE") \
  196. -c $(printf '%q' "$check") sh $(printf '%q' "$EXPECTED_SHARDS")" \
  197. >/dev/null 2>&1
  198. }
  199. nfs_unmount_workers() {
  200. local h
  201. for h in "${WORKER_HOSTS[@]}"; do
  202. remote_on "$h" "docker volume rm $(printf '%q' "$NFS_VOLUME") >/dev/null 2>&1 || true" || true
  203. done
  204. }