File size: 8,041 Bytes
53c10a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
RELEASE_DIR="$(cd -- "${SCRIPT_DIR}/.." && pwd)"

usage() {
  cat <<'EOF'
Usage:
  launch_multinode.sh [launcher options] -- <orarl-train options>

Launcher options:
  --hosts LIST          Comma-separated SSH hosts. May also use HOSTS.
  --hostfile FILE       One SSH host per line. May also use HOSTFILE.
  --gpus-per-node N     GPUs per host (default: 8).
  --ray-port N          Ray head port (default: 6379).
  --strict-host-key-checking MODE
                        yes (default), accept-new, ask, or no.
  --run                 Start Ray and training. Default is a safe dry run.
  --dry-run             Print the launch plan without connecting.
  --help                Show this message.

The arguments after -- must include --config, --model, --train-data,
--val-data, and --output. The launcher supplies topology and --run.
EOF
}

HOSTS_VALUE="${HOSTS:-}"
HOSTFILE_VALUE="${HOSTFILE:-}"
GPUS_PER_NODE="${GPUS_PER_NODE:-8}"
RAY_PORT="${RAY_PORT:-6379}"
STRICT_MODE="${STRICT_HOST_KEY_CHECKING:-yes}"
EXECUTE=0

while (($# > 0)); do
  case "$1" in
    --hosts)
      [[ $# -ge 2 ]] || { echo "ERROR: --hosts requires a value" >&2; exit 2; }
      HOSTS_VALUE="$2"
      shift 2
      ;;
    --hostfile)
      [[ $# -ge 2 ]] || { echo "ERROR: --hostfile requires a value" >&2; exit 2; }
      HOSTFILE_VALUE="$2"
      shift 2
      ;;
    --gpus-per-node)
      [[ $# -ge 2 ]] || { echo "ERROR: --gpus-per-node requires a value" >&2; exit 2; }
      GPUS_PER_NODE="$2"
      shift 2
      ;;
    --ray-port)
      [[ $# -ge 2 ]] || { echo "ERROR: --ray-port requires a value" >&2; exit 2; }
      RAY_PORT="$2"
      shift 2
      ;;
    --strict-host-key-checking)
      [[ $# -ge 2 ]] || {
        echo "ERROR: --strict-host-key-checking requires a value" >&2
        exit 2
      }
      STRICT_MODE="$2"
      shift 2
      ;;
    --run)
      EXECUTE=1
      shift
      ;;
    --dry-run)
      EXECUTE=0
      shift
      ;;
    --help|-h)
      usage
      exit 0
      ;;
    --)
      shift
      break
      ;;
    *)
      echo "ERROR: unknown launcher option: $1" >&2
      usage >&2
      exit 2
      ;;
  esac
done

TRAIN_ARGS=("$@")
if ((${#TRAIN_ARGS[@]} == 0)); then
  echo "ERROR: pass orarl-train options after --" >&2
  exit 2
fi
if [[ -n "${HOSTS_VALUE}" && -n "${HOSTFILE_VALUE}" ]]; then
  echo "ERROR: use only one of HOSTS/--hosts and HOSTFILE/--hostfile" >&2
  exit 2
fi
if [[ ! "${GPUS_PER_NODE}" =~ ^[1-9][0-9]*$ ]]; then
  echo "ERROR: GPUs per node must be a positive integer" >&2
  exit 2
fi
if [[ ! "${RAY_PORT}" =~ ^[0-9]+$ ]] || ((RAY_PORT < 1 || RAY_PORT > 65535)); then
  echo "ERROR: Ray port must be between 1 and 65535" >&2
  exit 2
fi
case "${STRICT_MODE}" in
  yes|accept-new|ask|no) ;;
  *)
    echo "ERROR: strict host key checking must be yes, accept-new, ask, or no" >&2
    exit 2
    ;;
esac

for argument in "${TRAIN_ARGS[@]}"; do
  lowered="${argument,,}"
  if [[ "${lowered}" == "--run" || "${lowered}" == "--dry-run" ]]; then
    echo "ERROR: training execution mode is controlled by the launcher" >&2
    exit 2
  fi
  if [[ "${lowered}" =~ (^|[._-])(api[_-]?key|auth[_-]?token|access[_-]?token|secret|password|credential|private[_-]?key)(=|$) ]]; then
    echo "ERROR: credential-shaped arguments are not forwarded" >&2
    exit 2
  fi
  if [[ "${argument}" =~ ://[^/@[:space:]]+:[^@/[:space:]]+@ ]]; then
    echo "ERROR: URLs with embedded authentication are not forwarded" >&2
    exit 2
  fi
done

declare -a RAW_HOSTS=()
if [[ -n "${HOSTFILE_VALUE}" ]]; then
  [[ -f "${HOSTFILE_VALUE}" ]] || {
    echo "ERROR: host file does not exist: ${HOSTFILE_VALUE}" >&2
    exit 2
  }
  while IFS= read -r line || [[ -n "${line}" ]]; do
    line="${line%%#*}"
    line="${line#"${line%%[![:space:]]*}"}"
    line="${line%"${line##*[![:space:]]}"}"
    [[ -n "${line}" ]] && RAW_HOSTS+=("${line}")
  done < "${HOSTFILE_VALUE}"
elif [[ -n "${HOSTS_VALUE}" ]]; then
  IFS=',' read -r -a RAW_HOSTS <<< "${HOSTS_VALUE}"
else
  echo "ERROR: set HOSTS, set HOSTFILE, or pass the matching option" >&2
  exit 2
fi

declare -a HOST_LIST=()
declare -A SEEN_HOSTS=()
for host in "${RAW_HOSTS[@]}"; do
  host="${host#"${host%%[![:space:]]*}"}"
  host="${host%"${host##*[![:space:]]}"}"
  if [[ ! "${host}" =~ ^([A-Za-z0-9._-]+@)?[A-Za-z0-9._-]+$ ]]; then
    echo "ERROR: invalid SSH host entry" >&2
    exit 2
  fi
  if [[ -z "${SEEN_HOSTS[${host}]:-}" ]]; then
    HOST_LIST+=("${host}")
    SEEN_HOSTS["${host}"]=1
  fi
done
if ((${#HOST_LIST[@]} == 0)); then
  echo "ERROR: no hosts were provided" >&2
  exit 2
fi

PYTHON_BIN="${PYTHON_BIN:-$(command -v python || true)}"
if [[ -z "${PYTHON_BIN}" ]]; then
  echo "ERROR: active Python executable was not found" >&2
  exit 2
fi
if ! "${PYTHON_BIN}" -c 'import sys; sys.exit(sys.version_info < (3, 10))' 2>/dev/null; then
  echo "ERROR: OraRL multi-node launch requires Python 3.10 or newer." >&2
  echo "Set PYTHON_BIN to the compatible verl environment." >&2
  exit 2
fi
export PYTHONPATH="${RELEASE_DIR}${PYTHONPATH:+:${PYTHONPATH}}"

FORWARD_VARIABLES=(
  PATH
  PYTHONPATH
  LD_LIBRARY_PATH
  VIRTUAL_ENV
  CONDA_PREFIX
  CONDA_DEFAULT_ENV
  CUDA_VISIBLE_DEVICES
  NCCL_DEBUG
  NCCL_SOCKET_IFNAME
  NCCL_IB_HCA
  NCCL_IB_DISABLE
  NCCL_P2P_DISABLE
  TOKENIZERS_PARALLELISM
  HF_HOME
  TRANSFORMERS_CACHE
  XDG_CACHE_HOME
  ORARL_MEDIA_ROOT
  ORARL_PREPROCESSED_DIR
  ORARL_DATALOADER_WORKERS
  ORARL_VIDEO_MAX_PIXELS
  ORARL_VIDEO_MIN_PIXELS
  ORARL_VIDEO_MAX_FRAMES
  ORARL_VIDEO_FPS
  ORARL_VIDEO_TOTAL_PIXELS
  ORARL_IMAGE_MIN_PIXELS
  ORARL_IMAGE_MAX_PIXELS
  ORARL_VIDEO_SOURCE_MODE
  ORARL_INLINE_VIDEO_TENSORS
  ORARL_MAX_PROMPT_LENGTH
  ORARL_MAX_RESPONSE_LENGTH
  ORARL_ROLLOUT_BATCH_SIZE
  ORARL_VAL_BATCH_SIZE
  ORARL_ENABLE_THINKING
  ORARL_SEED
  ORARL_GLOBAL_BATCH_SIZE
  ORARL_UPDATE_MICRO_BATCH
  ORARL_EXPERIENCE_MICRO_BATCH
  ORARL_MAX_TOKEN_LEN_PER_GPU
  ORARL_LEARNING_RATE
  ORARL_ROLLOUT_BACKEND
)

SSH_OPTIONS=(
  -o "BatchMode=yes"
  -o "StrictHostKeyChecking=${STRICT_MODE}"
)

build_remote_command() {
  local -a parts=("env")
  local name
  for name in "${FORWARD_VARIABLES[@]}"; do
    if [[ -v "${name}" ]]; then
      parts+=("${name}=${!name}")
    fi
  done
  parts+=("$@")
  local rendered
  printf -v rendered '%q ' "${parts[@]}"
  printf '%s' "${rendered% }"
}

display_command() {
  local host="$1"
  local label="$2"
  shift 2
  local rendered
  printf -v rendered '%q ' "$@"
  printf '[dry-run] %s on %s: %s\n' "${label}" "${host}" "${rendered% }"
  printf '          forwards %d allowlisted environment names when set\n' \
    "${#FORWARD_VARIABLES[@]}"
}

run_remote() {
  local host="$1"
  local label="$2"
  shift 2
  if ((EXECUTE == 0)); then
    display_command "${host}" "${label}" "$@"
    return
  fi
  printf '[run] %s on %s\n' "${label}" "${host}"
  remote_command="$(build_remote_command "$@")"
  ssh "${SSH_OPTIONS[@]}" "${host}" "${remote_command}"
}

HEAD_HOST="${HOST_LIST[0]}"
HEAD_ADDRESS="${HEAD_HOST##*@}"
RAY_ADDRESS="${HEAD_ADDRESS}:${RAY_PORT}"
NODE_COUNT="${#HOST_LIST[@]}"

run_remote "${HEAD_HOST}" "Ray head" \
  "${PYTHON_BIN}" -m ray start \
  --head \
  "--port=${RAY_PORT}" \
  "--num-gpus=${GPUS_PER_NODE}" \
  --disable-usage-stats

for ((index = 1; index < NODE_COUNT; index++)); do
  run_remote "${HOST_LIST[index]}" "Ray worker" \
    "${PYTHON_BIN}" -m ray start \
    "--address=${RAY_ADDRESS}" \
    "--num-gpus=${GPUS_PER_NODE}" \
    --disable-usage-stats
done

TRAIN_COMMAND=(
  "${PYTHON_BIN}"
  -m
  orarl.cli.train
  "${TRAIN_ARGS[@]}"
  --nodes
  "${NODE_COUNT}"
  --gpus-per-node
  "${GPUS_PER_NODE}"
  --run
)

if ((EXECUTE == 0)); then
  display_command "${HEAD_HOST}" "trainer (RAY_ADDRESS set at execution)" \
    "${TRAIN_COMMAND[@]}"
else
  printf '[run] trainer on %s\n' "${HEAD_HOST}"
  remote_command="$(build_remote_command "RAY_ADDRESS=${RAY_ADDRESS}" "${TRAIN_COMMAND[@]}")"
  ssh "${SSH_OPTIONS[@]}" "${HEAD_HOST}" "${remote_command}"
fi