Video-ORA-9B / code /eval /task /segmentation /run_eval_vllm.sh
lyhisme's picture
Add OraRL training and evaluation code
53c10a4 verified
Raw
History Blame Contribute Delete
17.5 kB
#!/bin/bash
# =============================================================================
# Segmentation evaluation — vLLM inference + optional SAM2 post-processing.
#
# Usage:
# bash eval/task/segmentation/run_eval_vllm.sh [MODEL_PATH] [OUTPUT_DIR]
#
# Common overrides:
# BENCH_DIR=/path/to/OneThinker-eval DATASETS=reasonseg-val DATA_ROOT=/path/to/OneThinker-eval \
# RUN_SAM2=true SAM2_CKPT=/path/to/sam2.1_hiera_large.pt SAM2_CFG=/path/to/sam2.1_hiera_l.yaml \
# bash eval/task/segmentation/run_eval_vllm.sh /path/to/ckpt
# =============================================================================
set -eo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/../../.." && pwd)"
# ---------- paths ----------
MODEL_PATH="${1:-}"
PROCESSOR_PATH="${PROCESSOR_PATH:-$MODEL_PATH}"
BENCH_DIR="${BENCH_DIR:-${PROJECT_DIR}/data/eval/segmentation}"
DATA_ROOT="${DATA_ROOT:-$BENCH_DIR}"
DATASETS="${DATASETS:-eval_seg_refcoco,eval_seg_refcocop,eval_seg_refcocog,eval_seg_mevis,eval_seg_reasonvos}"
if [[ -z "$MODEL_PATH" || ! -d "$MODEL_PATH" ]]; then
echo "ERROR: MODEL_PATH must name an existing model directory." >&2
exit 2
fi
if [[ ! -d "$BENCH_DIR" || ! -d "$DATA_ROOT" ]]; then
echo "ERROR: BENCH_DIR and DATA_ROOT must name existing directories." >&2
exit 2
fi
# ---------- eval settings ----------
DATA_TYPE="${DATA_TYPE:-all}" # all | image | video
PROMPT_MODE="${PROMPT_MODE:-train_seg}" # think | no_think | bare | onethink_system | train_seg
ENABLE_THINKING="${ENABLE_THINKING:-false}"
MAX_SAMPLES="${MAX_SAMPLES:-}"
BATCH_SIZE="${BATCH_SIZE:-16}"
MAX_NEW_TOKENS="${MAX_NEW_TOKENS:-1024}"
MAX_PIXELS_IMAGE="${MAX_PIXELS_IMAGE:-1048576}"
MIN_PIXELS_IMAGE="${MIN_PIXELS_IMAGE:-4096}"
# Accept both the tracking-style VIDEO_* names and older MAX_PIXELS_VIDEO names.
VIDEO_MAX_PIXELS="${VIDEO_MAX_PIXELS:-${MAX_PIXELS_VIDEO:-262144}}"
VIDEO_MIN_PIXELS="${VIDEO_MIN_PIXELS:-${MIN_PIXELS_VIDEO:-4096}}"
VIDEO_TOTAL_PIXELS="${VIDEO_TOTAL_PIXELS:-${TOTAL_PIXELS_VIDEO:-16777216}}"
MAX_FRAMES="${MAX_FRAMES:-128}"
FPS="${FPS:-2}"
PATCH_SIZE="${PATCH_SIZE:-}"
VIDEO_READER="${VIDEO_READER:-decord}"
case "$VIDEO_READER" in
decord|torchcodec|torchvision) ;;
*)
echo "ERROR: VIDEO_READER must be decord, torchcodec, or torchvision; got '$VIDEO_READER'" >&2
exit 1
;;
esac
export FORCE_QWENVL_VIDEO_READER="$VIDEO_READER"
require_int() {
local name="$1"
local value="$2"
if ! [[ "$value" =~ ^[0-9]+$ ]]; then
echo "ERROR: $name must be a non-empty integer, got '$value'"
exit 1
fi
}
require_int MAX_PIXELS_IMAGE "$MAX_PIXELS_IMAGE"
require_int MIN_PIXELS_IMAGE "$MIN_PIXELS_IMAGE"
require_int VIDEO_MAX_PIXELS "$VIDEO_MAX_PIXELS"
require_int VIDEO_MIN_PIXELS "$VIDEO_MIN_PIXELS"
require_int VIDEO_TOTAL_PIXELS "$VIDEO_TOTAL_PIXELS"
require_int MAX_FRAMES "$MAX_FRAMES"
require_int FPS "$FPS"
require_int BATCH_SIZE "$BATCH_SIZE"
require_int MAX_NEW_TOKENS "$MAX_NEW_TOKENS"
# ---------- vLLM ----------
TP_SIZE="${TP_SIZE:-1}"
GPU_MEM_UTIL="${GPU_MEM_UTIL:-0.85}"
MAX_MODEL_LEN="${MAX_MODEL_LEN:-32768}"
SEED="${SEED:-42}"
VLLM_BASE_PORT="${VLLM_BASE_PORT:-}"
RESUME_SHARDS="${RESUME_SHARDS:-false}"
RETRY_FAILED_SHARDS="${RETRY_FAILED_SHARDS:-true}"
# ---------- SAM2 ----------
RUN_SAM2="${RUN_SAM2:-false}"
SAM2_CKPT="${SAM2_CKPT:-}"
SAM2_CFG="${SAM2_CFG:-}"
ONETHINKER_SEG_POST="${ONETHINKER_SEG_POST:-${PROJECT_DIR}/third_party/OneThinker/Evaluation/Eval/seg_post_sam2.py}"
SAM2_NUM_GPUS="${SAM2_NUM_GPUS:-}"
SAM2_WORKERS_PER_GPU="${SAM2_WORKERS_PER_GPU:-}"
PRE_EXTRACT_THREADS="${PRE_EXTRACT_THREADS:-4}"
# Each SAM2 epoch spawns `world_size` worker processes that EACH reload the SAM2
# model and then handle only their slice of the epoch. Total model loads =
# world_size * num_epochs. A small epoch with many workers means tiny slices and
# constant model reloading (the real bottleneck). Keep the epoch large so there
# is effectively one epoch and each worker amortizes its model load over a big
# contiguous slice.
SAM2_EPOCH_SIZE="${SAM2_EPOCH_SIZE:-100000}"
VIZ_RATIO="${VIZ_RATIO:-0.0}"
# ---------- output ----------
MODEL_TAG=$(basename "${MODEL_PATH%/}")
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
OUTPUT_DIR="${2:-${PROJECT_DIR}/outputs/segmentation/eval_seg_vllm-${MODEL_TAG}-${TIMESTAMP}}"
# ---------- hardware ----------
NVJITLINK_LIB="$(
python - <<'PY'
import site
from pathlib import Path
roots = [*site.getsitepackages(), site.getusersitepackages()]
for root in roots:
candidate = Path(root) / "nvidia" / "nvjitlink" / "lib"
if candidate.is_dir():
print(candidate)
break
PY
)"
if [[ -n "${NVJITLINK_LIB}" ]]; then
export LD_LIBRARY_PATH="${NVJITLINK_LIB}${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
fi
if ! python -c "import torch; print(f'PyTorch preflight: {torch.__version__} CUDA {torch.version.cuda}')"; then
echo "ERROR: PyTorch CUDA libraries cannot be loaded in the active environment." >&2
exit 1
fi
if [ -n "${CUDA_VISIBLE_DEVICES:-}" ]; then
IFS="," read -ra GPULIST <<< "$CUDA_VISIBLE_DEVICES"
else
IFS="," read -ra GPULIST <<< "$(seq -s, 0 $(($(nvidia-smi -L | wc -l)-1)))"
fi
NUM_GPUS=${#GPULIST[@]}
if (( NUM_GPUS % TP_SIZE != 0 )); then
echo "ERROR: NUM_GPUS ($NUM_GPUS) must be divisible by TP_SIZE ($TP_SIZE)"
exit 1
fi
DP_SIZE=$(( NUM_GPUS / TP_SIZE ))
if [[ -z "${VLLM_BASE_PORT}" ]]; then
VLLM_BASE_PORT="$(
python - "$DP_SIZE" <<'PY'
import socket
import sys
count = int(sys.argv[1])
spacing = 16
for base in range(45000, 64000 - spacing * count, 128):
sockets = []
try:
for index in range(count):
for offset in (0, 1):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(("127.0.0.1", base + index * spacing + offset))
sockets.append(sock)
except OSError:
for sock in sockets:
sock.close()
continue
for sock in sockets:
sock.close()
print(base)
break
else:
raise SystemExit("no free segmentation vLLM port block found")
PY
)"
fi
echo "=============================================="
echo "Segmentation Evaluation (vLLM)"
echo "=============================================="
echo "Model: $MODEL_PATH"
echo "Processor: $PROCESSOR_PATH"
echo "Bench dir: $BENCH_DIR"
echo "Data root: $DATA_ROOT"
echo "Datasets: $DATASETS"
echo "Data type: $DATA_TYPE"
echo "Prompt: $PROMPT_MODE (enable_thinking=$ENABLE_THINKING)"
echo "Video pix: min=$VIDEO_MIN_PIXELS max=$VIDEO_MAX_PIXELS total=$VIDEO_TOTAL_PIXELS frames=$MAX_FRAMES fps=$FPS"
echo "Video reader:$VIDEO_READER"
echo "GPUs: ${GPULIST[*]} (${NUM_GPUS} total, TP=${TP_SIZE}, DP=${DP_SIZE})"
echo "Base port: $VLLM_BASE_PORT"
echo "Output: $OUTPUT_DIR"
echo "SAM2: $RUN_SAM2"
echo "=============================================="
mkdir -p "$OUTPUT_DIR"
PY_ARGS=(
--model_path "$MODEL_PATH"
--processor_path "$PROCESSOR_PATH"
--bench_dir "$BENCH_DIR"
--datasets "$DATASETS"
--output_dir "$OUTPUT_DIR"
--base_prefix "$DATA_ROOT"
--data_type "$DATA_TYPE"
--prompt_mode "$PROMPT_MODE"
--batch_size "$BATCH_SIZE"
--max_new_tokens "$MAX_NEW_TOKENS"
--tensor_parallel_size "$TP_SIZE"
--gpu_memory_utilization "$GPU_MEM_UTIL"
--max_model_len "$MAX_MODEL_LEN"
--seed "$SEED"
--max_pixels_image "$MAX_PIXELS_IMAGE"
--min_pixels_image "$MIN_PIXELS_IMAGE"
--max_pixels_video "$VIDEO_MAX_PIXELS"
--min_pixels_video "$VIDEO_MIN_PIXELS"
--total_pixels_video "$VIDEO_TOTAL_PIXELS"
--max_frames "$MAX_FRAMES"
--fps "$FPS"
--skip_missing_media
)
if [ -n "$MAX_SAMPLES" ]; then
PY_ARGS+=(--max_samples "$MAX_SAMPLES")
fi
if [ -n "$PATCH_SIZE" ]; then
PY_ARGS+=(--patch_size "$PATCH_SIZE")
fi
if [ "$ENABLE_THINKING" = "true" ]; then
PY_ARGS+=(--enable_thinking)
fi
PIDS=()
PID_SHARDS=()
cleanup() {
echo ""
echo "Caught interrupt, killing workers ..."
for pid in "${PIDS[@]}"; do
kill -TERM "$pid" 2>/dev/null || true
done
wait 2>/dev/null || true
exit 1
}
trap cleanup INT TERM
dataset_key() {
python -c \
"from pathlib import Path; import sys; p=sys.argv[1]; print(Path(p).stem if Path(p).suffix else p)" \
"$1"
}
shard_is_complete() {
local shard_id="$1"
local dataset
local key
local datasets=()
IFS=',' read -ra datasets <<< "$DATASETS"
for dataset in "${datasets[@]}"; do
key="$(dataset_key "$dataset")"
if [[ ! -s "$OUTPUT_DIR/results_${key}_shard${shard_id}.json" ]]; then
return 1
fi
done
return 0
}
if [ "$DP_SIZE" -eq 1 ]; then
CUDA_VISIBLE_DEVICES=$(IFS=,; echo "${GPULIST[*]}") \
VLLM_PORT="$VLLM_BASE_PORT" \
VLLM_HOST_IP=127.0.0.1 \
MASTER_PORT="$((VLLM_BASE_PORT + 1))" \
MASTER_ADDR=127.0.0.1 \
PYTHONUNBUFFERED=1 python "$SCRIPT_DIR/eval_seg_vllm.py" "${PY_ARGS[@]}" \
2>&1 | tee "$OUTPUT_DIR/run.log"
else
echo ""
echo ">>> Launching $DP_SIZE vLLM workers (TP=$TP_SIZE each) ..."
for IDX in $(seq 0 $((DP_SIZE - 1))); do
if [[ "$RESUME_SHARDS" == "true" ]] && shard_is_complete "$IDX"; then
echo " Reusing completed shard $IDX"
continue
fi
START=$(( IDX * TP_SIZE ))
SHARD_PORT=$((VLLM_BASE_PORT + IDX * 16))
SHARD_MASTER_PORT=$((SHARD_PORT + 1))
SHARD_GPUS=""
for j in $(seq 0 $((TP_SIZE - 1))); do
g=${GPULIST[$((START + j))]}
SHARD_GPUS="${SHARD_GPUS}${SHARD_GPUS:+,}${g}"
done
CUDA_VISIBLE_DEVICES="$SHARD_GPUS" \
VLLM_PORT="$SHARD_PORT" \
VLLM_HOST_IP=127.0.0.1 \
MASTER_PORT="$SHARD_MASTER_PORT" \
MASTER_ADDR=127.0.0.1 \
PYTHONUNBUFFERED=1 \
python "$SCRIPT_DIR/eval_seg_vllm.py" "${PY_ARGS[@]}" \
--chunk "$DP_SIZE" --index "$IDX" \
> "$OUTPUT_DIR/worker_shard${IDX}.log" 2>&1 &
PIDS+=($!)
PID_SHARDS+=("$IDX")
echo " Launched shard $IDX on GPU $SHARD_GPUS (PID ${PIDS[-1]})"
done
FAILED_SHARDS=()
for i in "${!PIDS[@]}"; do
if ! wait "${PIDS[$i]}"; then
FAILED_SHARDS+=("${PID_SHARDS[$i]}")
fi
done
if (( ${#FAILED_SHARDS[@]} > 0 )) && [[ "$RETRY_FAILED_SHARDS" == "true" ]]; then
echo ""
echo ">>> Retrying failed shards sequentially with fresh ports ..."
RETRY_FAILURES=()
for IDX in "${FAILED_SHARDS[@]}"; do
START=$(( IDX * TP_SIZE ))
SHARD_GPUS=""
for j in $(seq 0 $((TP_SIZE - 1))); do
g=${GPULIST[$((START + j))]}
SHARD_GPUS="${SHARD_GPUS}${SHARD_GPUS:+,}${g}"
done
RETRY_PORT="$(
python - <<'PY'
import socket
for base in range(52000, 64000, 8):
sockets = []
try:
for offset in (0, 1):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(("127.0.0.1", base + offset))
sockets.append(sock)
except OSError:
for sock in sockets:
sock.close()
continue
for sock in sockets:
sock.close()
print(base)
break
else:
raise SystemExit("no free vLLM retry ports found")
PY
)"
echo " Retrying shard $IDX on GPU $SHARD_GPUS (ports $RETRY_PORT/$((RETRY_PORT + 1)))"
if ! CUDA_VISIBLE_DEVICES="$SHARD_GPUS" \
VLLM_PORT="$RETRY_PORT" \
VLLM_HOST_IP=127.0.0.1 \
MASTER_PORT="$((RETRY_PORT + 1))" \
MASTER_ADDR=127.0.0.1 \
PYTHONUNBUFFERED=1 \
python "$SCRIPT_DIR/eval_seg_vllm.py" "${PY_ARGS[@]}" \
--chunk "$DP_SIZE" --index "$IDX" \
> "$OUTPUT_DIR/worker_shard${IDX}_retry.log" 2>&1; then
RETRY_FAILURES+=("$IDX")
fi
done
FAILED_SHARDS=("${RETRY_FAILURES[@]}")
fi
if (( ${#FAILED_SHARDS[@]} > 0 )); then
echo "ERROR: Failed shards: ${FAILED_SHARDS[*]}; not merging or running SAM2."
exit 1
fi
echo ""
echo ">>> Merging shard results ..."
IFS=',' read -ra DATASET_LIST <<< "$DATASETS"
for DATASET in "${DATASET_LIST[@]}"; do
DATASET_KEY=$(python -c "from pathlib import Path; import sys; p=sys.argv[1]; print(Path(p).stem if Path(p).suffix else p)" "$DATASET")
python - "$OUTPUT_DIR" "$DATASET_KEY" "$DP_SIZE" <<'PY'
import json
import os
import sys
out_dir, dataset, num_shards = sys.argv[1], sys.argv[2], int(sys.argv[3])
all_samples = []
for sid in range(num_shards):
path = os.path.join(out_dir, f"results_{dataset}_shard{sid}.json")
if os.path.isfile(path):
with open(path, encoding="utf-8") as f:
payload = json.load(f)
all_samples.extend(payload.get("results", []))
n = len(all_samples)
if n == 0:
print(f" No results for {dataset}")
sys.exit(0)
parse_ok = sum(1 for row in all_samples if row.get("parse_ok"))
summary = {
"num_samples": n,
"parse_ok": parse_ok,
"parse_rate": round(parse_ok / n * 100.0, 2),
"by_data_type": {},
}
for data_type in ("image", "video"):
part = [r for r in all_samples if r.get("data_type") == data_type]
if part:
ok = sum(1 for r in part if r.get("parse_ok"))
summary["by_data_type"][data_type] = {
"num_samples": len(part),
"parse_rate": round(ok / len(part) * 100.0, 2),
}
with open(os.path.join(out_dir, f"results_{dataset}.json"), "w", encoding="utf-8") as f:
json.dump({"results": all_samples, "metrics": summary}, f, ensure_ascii=False, indent=2)
summary_path = os.path.join(out_dir, "summary.json")
full_summary = json.load(open(summary_path, encoding="utf-8")) if os.path.isfile(summary_path) else {}
full_summary[dataset] = summary
with open(summary_path, "w", encoding="utf-8") as f:
json.dump(full_summary, f, ensure_ascii=False, indent=2)
print(f" {dataset}: n={n} parse={summary['parse_rate']:.2f}%")
PY
done
fi
if [ "$RUN_SAM2" = "true" ]; then
if [ -z "$SAM2_CKPT" ] || [ -z "$SAM2_CFG" ]; then
echo "ERROR: RUN_SAM2=true requires SAM2_CKPT and SAM2_CFG."
exit 1
fi
echo ""
echo ">>> Running SAM2 post-processing ..."
# SAM2 needs visible GPUs. The DP>1 inference path sets CUDA_VISIBLE_DEVICES
# only inside per-worker subshells, so the parent env may be empty here; an
# empty CUDA_VISIBLE_DEVICES makes torch.cuda.is_available() False and SAM2
# falls back to slow CPU serial. Re-derive a non-empty device list.
export CUDA_VISIBLE_DEVICES="$(IFS=,; echo "${GPULIST[*]}")"
# Default SAM2 GPU count to all visible GPUs unless caller overrides.
if [ -z "$SAM2_NUM_GPUS" ]; then
SAM2_NUM_GPUS="$NUM_GPUS"
fi
echo " SAM2 GPUs: $CUDA_VISIBLE_DEVICES (num_gpus=$SAM2_NUM_GPUS)"
IFS=',' read -ra DATASET_LIST <<< "$DATASETS"
for DATASET in "${DATASET_LIST[@]}"; do
DATASET_KEY=$(python -c "from pathlib import Path; import sys; p=sys.argv[1]; print(Path(p).stem if Path(p).suffix else p)" "$DATASET")
RESULT_JSON="$OUTPUT_DIR/results_${DATASET_KEY}.json"
if [ ! -f "$RESULT_JSON" ]; then
echo " Skip $DATASET_KEY: missing $RESULT_JSON"
continue
fi
SAM2_ARGS=(
--input_json "$RESULT_JSON"
--data_root "$DATA_ROOT"
--sam2_ckpt "$SAM2_CKPT"
--sam2_cfg "$SAM2_CFG"
--onethinker_script "$ONETHINKER_SEG_POST"
--pre_extract_threads "$PRE_EXTRACT_THREADS"
--epoch_size "$SAM2_EPOCH_SIZE"
--viz_ratio "$VIZ_RATIO"
)
if [ -n "$SAM2_NUM_GPUS" ]; then
SAM2_ARGS+=(--num_gpus "$SAM2_NUM_GPUS")
fi
if [ -n "$SAM2_WORKERS_PER_GPU" ]; then
SAM2_ARGS+=(--workers_per_gpu "$SAM2_WORKERS_PER_GPU")
fi
PYTHONUNBUFFERED=1 python "$SCRIPT_DIR/post_sam2.py" "${SAM2_ARGS[@]}" \
2>&1 | tee "$OUTPUT_DIR/sam2_${DATASET_KEY}.log"
# Merge SAM2 metrics (cIoU / gIoU / J&F) back into summary.json.
SAM2_JSON="$OUTPUT_DIR/results_${DATASET_KEY}_sam2.json"
if [ -f "$SAM2_JSON" ]; then
python - "$OUTPUT_DIR" "$DATASET_KEY" "$SAM2_JSON" <<'PY'
import json
import os
import sys
out_dir, dataset, sam2_json = sys.argv[1], sys.argv[2], sys.argv[3]
with open(sam2_json, encoding="utf-8") as f:
payload = json.load(f)
metrics = payload.get("metrics", {})
avg_rewards = payload.get("avg_rewards", {})
summary_path = os.path.join(out_dir, "summary.json")
full_summary = json.load(open(summary_path, encoding="utf-8")) if os.path.isfile(summary_path) else {}
entry = full_summary.get(dataset, {})
if isinstance(metrics, dict):
for k in ("num_samples", "parse_ok", "parse_rate"):
if k in metrics:
entry[k] = metrics[k]
entry["avg_rewards"] = avg_rewards
full_summary[dataset] = entry
with open(summary_path, "w", encoding="utf-8") as f:
json.dump(full_summary, f, ensure_ascii=False, indent=2)
parts = ", ".join(f"{k}={v:.4f}" for k, v in avg_rewards.items())
print(f" {dataset}: {parts}")
PY
fi
done
fi
echo ""
echo "=============================================="
echo "Done. Summary: $OUTPUT_DIR/summary.json"
echo "=============================================="