File size: 6,922 Bytes
aa975a2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env bash
set -euo pipefail

############################################
# vLLM(Qwen-VL) + 批处理流水线(防截断·参数加强)
# - 上下文窗放大:--max-model-len 40960(Qwen3 默认)
# - 显存利用提升:--gpu-memory-utilization 0.95
# - 并发适度下降:--max-num-seqs 8(给长输出更稳定)
# - TP 自适配(整除 attention heads)
# - setsid 进程组 + 组杀,退出优雅清理
############################################

# ======== 可改参数 ========
CSV_PATH="${CSV_PATH:-/mnt/data_nas/guanfb/Edit/CoT_Edit/8BTest_CoT_box/data/Test.csv}"
OUTDIR="${OUTDIR:-./outputs}"
MODEL_SIZE="${MODEL_SIZE:-8b}"                 # 8b | 32b
API_PORT="${API_PORT:-8007}"                   # 8B默认8007,32B可用8032
RESULTS_CSV="${RESULTS_CSV:-$OUTDIR/results.csv}"
DRY_RUN="${DRY_RUN:-0}"
RESUME="${RESUME:-1}"
WAIT_SECS="${WAIT_SECS:-600}"

# === 客户端并发(传给 run.py) ===
CLIENT_HTTP_WORKERS="${CLIENT_HTTP_WORKERS:-2}"    # 建议 2~4
CLIENT_HTTP_TIMEOUT="${CLIENT_HTTP_TIMEOUT:-1200}" # 单请求超时
CLIENT_MAX_TOKENS="${CLIENT_MAX_TOKENS:-4096}"     # 生成上限(配合防截断)

# GPU 绑定(多卡逗号分隔)
GPU_IDS="${GPU_IDS:-0,1,2,3}"
export CUDA_VISIBLE_DEVICES="${GPU_IDS}"
IFS=',' read -ra _g <<< "$GPU_IDS"
NUM_GPUS="${#_g[@]}"

# vLLM 服务参数(防截断关键项)
GPU_UTIL="${GPU_UTIL:-0.95}"
MAX_NUM_SEQS="${MAX_NUM_SEQS:-8}"
MAX_MODEL_LEN="${MAX_MODEL_LEN:-40960}"

SERVED_NAME="${SERVED_NAME:-qwen_vl}"

# 模型权重
if [[ "$MODEL_SIZE" == "32b" ]]; then
  MODEL_NAME="${MODEL_NAME:-/path/to/Qwen3-VL-32B-Instruct}"
  API_PORT="${API_PORT:-8032}"
else
  MODEL_NAME="${MODEL_NAME:-/mnt/data_nas/guanfb/Edit/CoT_Edit/8BTest_CoT_box/Qwen3-VL-8B-Instruct}"
fi

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_DIR="$OUTDIR/logs"
mkdir -p "$LOG_DIR" "$OUTDIR"

echo "[1/6] Installing Python deps if needed..."
pip install -r "$ROOT_DIR/requirements.txt" >/dev/null || true

# ===== TP 自适配(读取 num_attention_heads,整除选 TP) =====
echo "[2/6] Probing model heads & selecting tensor parallel size..."
NUM_HEADS=$(python - <<'PY'
import json, os
m=os.environ.get("MODEL_NAME","")
cfg=os.path.join(m,"config.json")
h=None
try:
    with open(cfg,"r",encoding="utf-8") as f:
        j=json.load(f)
        h=j.get("num_attention_heads") or j.get("n_head") or j.get("num_key_value_heads")
except Exception:
    pass
if h is None:
    h = 64 if any(s in m.lower() for s in ("32","32b")) else 32
print(h)
PY
)
pick_tp() { local heads="$1"; local gpus="$2"; for ((d=gpus; d>=1; d--)); do (( heads % d == 0 )) && { echo "$d"; return; }; done; echo 1; }
TP_SIZE="${TP_SIZE:-$(pick_tp "$NUM_HEADS" "$NUM_GPUS")}"
echo "   - CUDA_VISIBLE_DEVICES=${GPU_IDS} (num_gpus=${NUM_GPUS})"
echo "   - num_attention_heads=${NUM_HEADS}  => tensor_parallel_size=${TP_SIZE}"

API_BASE="http://127.0.0.1:${API_PORT}/v1"
echo "[3/6] Starting vLLM server: $MODEL_NAME on port $API_PORT (served as '$SERVED_NAME')"

# 端口占用兜底清理
if lsof -iTCP:${API_PORT} -sTCP:LISTEN -Pn >/dev/null 2>&1; then
  echo "[CLEANUP] Port ${API_PORT} is in use. Killing existing server..."
  old_pids=$(lsof -iTCP:${API_PORT} -sTCP:LISTEN -t || true)
  if [[ -n "$old_pids" ]]; then
    kill $old_pids 2>/dev/null || true
    sleep 2
    for p in $old_pids; do ps -p $p >/dev/null 2>&1 && kill -9 $p 2>/dev/null || true; done
  fi
fi

VLLM_PID_FILE="$LOG_DIR/vllm_${MODEL_SIZE}.pid"
VLLM_PGID_FILE="$LOG_DIR/vllm_${MODEL_SIZE}.pgid"

# setsid -> 独立进程组,方便组杀
setsid bash -c "
  nohup python -m vllm.entrypoints.openai.api_server \
    --model \"$MODEL_NAME\" \
    --served-model-name \"$SERVED_NAME\" \
    --trust-remote-code \
    --dtype bfloat16 \
    --tensor-parallel-size \"$TP_SIZE\" \
    --gpu-memory-utilization \"$GPU_UTIL\" \
    --max-num-seqs \"$MAX_NUM_SEQS\" \
    --max-model-len \"$MAX_MODEL_LEN\" \
    --port \"$API_PORT\" \
    >\"$LOG_DIR/vllm_${MODEL_SIZE}.log\" 2>&1 &
  echo \$! > \"$VLLM_PID_FILE\"
  sleep 0.5
" >/dev/null 2>&1 &

sleep 1
VLLM_PID="$(cat "$VLLM_PID_FILE" 2>/dev/null || true)"
if [[ -n "${VLLM_PID:-}" ]]; then
  VLLM_PGID="$(ps -o pgid= "$VLLM_PID" 2>/dev/null | tr -d ' ' || true)"
  [[ -n "${VLLM_PGID:-}" ]] && echo "$VLLM_PGID" > "$VLLM_PGID_FILE"
fi

cleanup() {
  set +e
  echo "[CLEANUP] Triggered. Cleaning vLLM..."
  if [[ -f "$VLLM_PGID_FILE" ]]; then
    pgid="$(cat "$VLLM_PGID_FILE" 2>/dev/null || true)"
    if [[ -n "${pgid:-}" ]]; then
      echo "[CLEANUP] kill -TERM -- -$pgid"
      kill -TERM -- "-$pgid" 2>/dev/null || true
      for _ in {1..15}; do lsof -iTCP:${API_PORT} -sTCP:LISTEN -Pn >/dev/null 2>&1 || break; sleep 1; done
      if lsof -iTCP:${API_PORT} -sTCP:LISTEN -Pn >/dev/null 2>&1; then
        echo "[CLEANUP] kill -KILL -- -$pgid"
        kill -KILL -- "-$pgid" 2>/dev/null || true
      fi
    fi
  fi
  if [[ -f "$VLLM_PID_FILE" ]]; then
    pid="$(cat "$VLLM_PID_FILE" 2>/dev/null || true)"
    if [[ -n "${pid:-}" ]] && ps -p "$pid" >/dev/null 2>&1; then
      kill -TERM "$pid" 2>/dev/null || true
      for _ in {1..5}; do ps -p "$pid" >/dev/null 2>&1 || break; sleep 1; done
      ps -p "$pid" >/dev/null 2>&1 && kill -KILL "$pid" 2>/dev/null || true
    fi
  fi
  if lsof -iTCP:${API_PORT} -sTCP:LISTEN -Pn >/dev/null 2>&1; then
    old_pids=$(lsof -iTCP:${API_PORT} -sTCP:LISTEN -t || true)
    [[ -n "$old_pids" ]] && kill $old_pids 2>/dev/null || true
  fi
  rm -f "$VLLM_PID_FILE" "$VLLM_PGID_FILE"
}
trap 'cleanup; exit 130' INT
trap 'cleanup' TERM EXIT

# 等待就绪
echo "[4/6] Waiting for vLLM to be ready at ${API_BASE} (timeout=${WAIT_SECS}s) ..."
ready=0
for (( i=1; i<=WAIT_SECS; i++ )); do
  if curl -m 2 -s "${API_BASE}/models" >/dev/null 2>&1; then ready=1; break; fi
  sleep 1
done
[[ $ready -eq 1 ]] || { echo "ERROR: vLLM not ready after ${WAIT_SECS}s. See $LOG_DIR/vllm_${MODEL_SIZE}.log"; exit 1; }
echo "vLLM is ready."

# 健康检查(标准 text)
if ! curl -m 5 -s -X POST "${API_BASE}/chat/completions" \
    -H 'Content-Type: application/json' \
    -d "{\"model\":\"${SERVED_NAME}\",\"messages\":[{\"role\":\"user\",\"content\":[{\"type\":\"text\",\"text\":\"ping\"}]}],\"max_tokens\":4}" \
    | grep -q '"choices"'; then
  echo "WARNING: /v1/chat/completions 返回异常。确认 OpenAI 兼容入口与依赖是否齐全。"
fi

# 跑流水线(把 served-model-name 传入)
echo "[5/6] Running pipeline..."
CMD=(python "$ROOT_DIR/run.py"
  --csv "$CSV_PATH"
  --outdir "$OUTDIR"
  --api-base "$API_BASE"
  --model "$SERVED_NAME"
  --results-csv "$RESULTS_CSV"
)

# 只有等于 "1" 才加对应 flag;"0" 或空都不加
if [[ "${RESUME:-0}" == "1" ]]; then
  CMD+=("--resume")
fi
if [[ "${DRY_RUN:-0}" == "1" ]]; then
  CMD+=("--dry-run")
fi

echo "Command: ${CMD[*]}"
"${CMD[@]}"

echo "[6/6] Done."
echo "结果汇总 CSV: $RESULTS_CSV"
echo "日志目录    : $LOG_DIR"