248 lines
7.9 KiB
Bash
248 lines
7.9 KiB
Bash
#!/usr/bin/env bash
|
|
# Extract all 12 EasyCom sessions with three independent workers per GPU.
|
|
#
|
|
# Sessions are assigned round-robin across four GPUs:
|
|
# GPU 0: Session 1, 5, 9
|
|
# GPU 1: Session 2, 6, 10
|
|
# GPU 2: Session 3, 7, 11
|
|
# GPU 3: Session 4, 8, 12
|
|
#
|
|
# Worker launches are deliberately serialized. The launcher waits until one
|
|
# worker has loaded Qwen and produced its first 0.2-second state before starting
|
|
# the next worker, preventing all model checkpoints from loading at once.
|
|
set -uo pipefail
|
|
|
|
PROJECT_ROOT="${PROJECT_ROOT:-/data1/yfzx/wangx/Code/icassp2027-experiment}"
|
|
MODEL="${MODEL:-/data1/yfzx/wangx/Models/Qwen2.5-Omni-3B}"
|
|
VIDEO_ROOT="${VIDEO_ROOT:-/data1/yfzx/wangx/Datasets/easycom/extracted_easycom/Main/Video_Compressed}"
|
|
TARGET_ROOT="${TARGET_ROOT:-/data1/yfzx/wangx/Datasets/easycom/easycom_target_frame}"
|
|
FEATURE_ROOT="${FEATURE_ROOT:-${PROJECT_ROOT}/features/easycom_direct}"
|
|
LOG_ROOT="${LOG_ROOT:-${PROJECT_ROOT}/logs/easycom_direct}"
|
|
|
|
GPUS="${GPUS:-0 1 2 3}"
|
|
SESSIONS="${SESSIONS:-1 2 3 4 5 6 7 8 9 10 11 12}"
|
|
PROCESSES_PER_GPU="${PROCESSES_PER_GPU:-3}"
|
|
MEMORY_SECONDS="${MEMORY_SECONDS:-20}"
|
|
VIDEO_FPS="${VIDEO_FPS:-2}"
|
|
FRAME_CACHE_SIZE="${FRAME_CACHE_SIZE:-64}"
|
|
DTYPE="${DTYPE:-bfloat16}"
|
|
OMP_THREADS_PER_PROCESS="${OMP_THREADS_PER_PROCESS:-4}"
|
|
|
|
# Maximum time allowed for one worker to index media, load Qwen, and produce
|
|
# its first state. A small gap after readiness further separates CUDA startup.
|
|
START_TIMEOUT_SECONDS="${START_TIMEOUT_SECONDS:-1800}"
|
|
START_GAP_SECONDS="${START_GAP_SECONDS:-10}"
|
|
POLL_SECONDS="${POLL_SECONDS:-5}"
|
|
|
|
read -r -a GPU_ARRAY <<<"${GPUS}"
|
|
read -r -a SESSION_ARRAY <<<"${SESSIONS}"
|
|
|
|
if [[ "${#GPU_ARRAY[@]}" -eq 0 ]]; then
|
|
echo "No GPUs supplied through GPUS" >&2
|
|
exit 2
|
|
fi
|
|
if [[ "${PROCESSES_PER_GPU}" -le 0 ]]; then
|
|
echo "PROCESSES_PER_GPU must be positive" >&2
|
|
exit 2
|
|
fi
|
|
|
|
capacity=$((${#GPU_ARRAY[@]} * PROCESSES_PER_GPU))
|
|
if [[ "${#SESSION_ARRAY[@]}" -gt "${capacity}" ]]; then
|
|
echo "${#SESSION_ARRAY[@]} sessions exceed capacity ${capacity}" >&2
|
|
exit 2
|
|
fi
|
|
|
|
mkdir -p "${FEATURE_ROOT}" "${LOG_ROOT}"
|
|
cd "${PROJECT_ROOT}"
|
|
|
|
declare -a CHILD_PIDS=()
|
|
declare -a CHILD_NAMES=()
|
|
declare -a CHILD_PIDFILES=()
|
|
declare -a EXTERNAL_PIDS=()
|
|
declare -a EXTERNAL_NAMES=()
|
|
declare -a EXTERNAL_OUTPUTS=()
|
|
declare -a EXTERNAL_PIDFILES=()
|
|
declare -A GPU_JOB_COUNTS=()
|
|
|
|
for gpu in "${GPU_ARRAY[@]}"; do
|
|
GPU_JOB_COUNTS["${gpu}"]=0
|
|
done
|
|
|
|
wait_until_ready() {
|
|
local pid="$1"
|
|
local log="$2"
|
|
local session="$3"
|
|
local started_at="${SECONDS}"
|
|
|
|
while true; do
|
|
# tqdm writes progress updates with carriage returns; grep -a handles the
|
|
# resulting log as text. This marker appears after the first model forward.
|
|
if grep -aq "media=0.2s" "${log}"; then
|
|
return 0
|
|
fi
|
|
if grep -aq "Traceback (most recent call last)" "${log}"; then
|
|
echo "${session} failed during startup; inspect ${log}" >&2
|
|
return 1
|
|
fi
|
|
if ! kill -0 "${pid}" 2>/dev/null; then
|
|
echo "${session} exited before producing its first state; inspect ${log}" >&2
|
|
return 1
|
|
fi
|
|
if ((SECONDS - started_at >= START_TIMEOUT_SECONDS)); then
|
|
echo "${session} startup timed out after ${START_TIMEOUT_SECONDS}s" >&2
|
|
return 1
|
|
fi
|
|
sleep "${POLL_SECONDS}"
|
|
done
|
|
}
|
|
|
|
startup_status=0
|
|
for index in "${!SESSION_ARRAY[@]}"; do
|
|
sid="${SESSION_ARRAY[${index}]}"
|
|
gpu="${GPU_ARRAY[$((index % ${#GPU_ARRAY[@]}))]}"
|
|
session="Session_${sid}"
|
|
session_dir="${VIDEO_ROOT}/${session}"
|
|
target="${TARGET_ROOT}/${session}.npy"
|
|
output="${FEATURE_ROOT}/${session}.pt"
|
|
log="${LOG_ROOT}/${session}.log"
|
|
pidfile="${LOG_ROOT}/${session}.pid"
|
|
|
|
GPU_JOB_COUNTS["${gpu}"]=$((GPU_JOB_COUNTS["${gpu}"] + 1))
|
|
if [[ "${GPU_JOB_COUNTS[${gpu}]}" -gt "${PROCESSES_PER_GPU}" ]]; then
|
|
echo "GPU ${gpu} was assigned too many workers" >&2
|
|
startup_status=1
|
|
break
|
|
fi
|
|
if [[ ! -d "${session_dir}" ]]; then
|
|
echo "Missing session directory: ${session_dir}" >&2
|
|
startup_status=1
|
|
break
|
|
fi
|
|
if [[ ! -s "${target}" ]]; then
|
|
echo "Missing target: ${target}" >&2
|
|
startup_status=1
|
|
break
|
|
fi
|
|
if [[ -s "${output}" ]]; then
|
|
echo "Skipping completed ${session}: ${output}"
|
|
continue
|
|
fi
|
|
|
|
# Also detect a worker launched manually before this manager. This is useful
|
|
# when Session_1 is already running on GPU 0. Its round-robin slot still
|
|
# counts toward that GPU's three-worker limit.
|
|
running_pid=""
|
|
if command -v pgrep >/dev/null 2>&1; then
|
|
running_pid=$(pgrep -f \
|
|
"streaming_turn_model_directly.py.*${session_dir}" | head -n 1 || true)
|
|
fi
|
|
if [[ -n "${running_pid}" ]] && kill -0 "${running_pid}" 2>/dev/null; then
|
|
echo "Skipping already-running ${session}: PID=${running_pid}"
|
|
echo "${running_pid}" >"${pidfile}"
|
|
EXTERNAL_PIDS+=("${running_pid}")
|
|
EXTERNAL_NAMES+=("${session}")
|
|
EXTERNAL_OUTPUTS+=("${output}")
|
|
EXTERNAL_PIDFILES+=("${pidfile}")
|
|
if [[ -f "${log}" ]]; then
|
|
echo " Confirming the existing worker reached its first state..."
|
|
if ! wait_until_ready "${running_pid}" "${log}" "${session}"; then
|
|
startup_status=1
|
|
break
|
|
fi
|
|
else
|
|
echo " WARNING: no log is available to confirm startup; waiting " \
|
|
"${START_GAP_SECONDS}s before the next load" >&2
|
|
fi
|
|
sleep "${START_GAP_SECONDS}"
|
|
continue
|
|
fi
|
|
if [[ -s "${pidfile}" ]]; then
|
|
existing_pid=$(<"${pidfile}")
|
|
if kill -0 "${existing_pid}" 2>/dev/null; then
|
|
echo "Skipping already-running ${session}: PID=${existing_pid}"
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
: >"${log}"
|
|
echo "Starting ${session} on physical GPU ${gpu} " \
|
|
"(${GPU_JOB_COUNTS[${gpu}]}/${PROCESSES_PER_GPU} workers)"
|
|
|
|
env \
|
|
CUDA_VISIBLE_DEVICES="${gpu}" \
|
|
PYTHONUNBUFFERED=1 \
|
|
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \
|
|
OMP_NUM_THREADS="${OMP_THREADS_PER_PROCESS}" \
|
|
python streaming_turn_model_directly.py \
|
|
--model-id "${MODEL}" \
|
|
--easycom-session-dir "${session_dir}" \
|
|
--target "${target}" \
|
|
--use-audio-in-video \
|
|
--mode open-stream-replay \
|
|
--memory-seconds "${MEMORY_SECONDS}" \
|
|
--video-fps "${VIDEO_FPS}" \
|
|
--frame-cache-size "${FRAME_CACHE_SIZE}" \
|
|
--dtype "${DTYPE}" \
|
|
--flash-attention-2 \
|
|
--device cuda:0 \
|
|
--output "${output}" \
|
|
>"${log}" 2>&1 &
|
|
|
|
pid=$!
|
|
echo "${pid}" >"${pidfile}"
|
|
CHILD_PIDS+=("${pid}")
|
|
CHILD_NAMES+=("${session}")
|
|
CHILD_PIDFILES+=("${pidfile}")
|
|
echo " PID=${pid}; waiting for first state before next model load..."
|
|
|
|
if ! wait_until_ready "${pid}" "${log}" "${session}"; then
|
|
startup_status=1
|
|
tail -n 40 "${log}" >&2
|
|
break
|
|
fi
|
|
echo " ${session} is running; next worker starts in ${START_GAP_SECONDS}s"
|
|
sleep "${START_GAP_SECONDS}"
|
|
done
|
|
|
|
echo "Launcher finished starting workers; waiting for child processes."
|
|
final_status="${startup_status}"
|
|
for index in "${!CHILD_PIDS[@]}"; do
|
|
pid="${CHILD_PIDS[${index}]}"
|
|
session="${CHILD_NAMES[${index}]}"
|
|
pidfile="${CHILD_PIDFILES[${index}]}"
|
|
if wait "${pid}"; then
|
|
echo "Completed ${session}"
|
|
else
|
|
echo "Failed ${session}; inspect ${LOG_ROOT}/${session}.log" >&2
|
|
final_status=1
|
|
fi
|
|
rm -f "${pidfile}"
|
|
done
|
|
|
|
# Non-child workers cannot be handled by bash's wait builtin. Poll any workers
|
|
# that were already running when this launcher started, then validate that they
|
|
# produced their expected output before reporting global success.
|
|
for index in "${!EXTERNAL_PIDS[@]}"; do
|
|
pid="${EXTERNAL_PIDS[${index}]}"
|
|
session="${EXTERNAL_NAMES[${index}]}"
|
|
output="${EXTERNAL_OUTPUTS[${index}]}"
|
|
pidfile="${EXTERNAL_PIDFILES[${index}]}"
|
|
while kill -0 "${pid}" 2>/dev/null; do
|
|
sleep 30
|
|
done
|
|
if [[ -s "${output}" ]]; then
|
|
echo "Completed pre-existing ${session}"
|
|
else
|
|
echo "Pre-existing ${session} exited without ${output}" >&2
|
|
final_status=1
|
|
fi
|
|
rm -f "${pidfile}"
|
|
done
|
|
|
|
if [[ "${final_status}" -ne 0 ]]; then
|
|
echo "One or more workers failed, or worker startup was interrupted." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "All selected EasyCom sessions completed: ${FEATURE_ROOT}"
|