Files

248 lines
7.9 KiB
Bash
Raw Permalink Normal View History

#!/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}"