spec-b300 / source /examples /train /infinity_parser2 /dspark_infinity_parser2_flash_online.sh
khazic's picture
Archive three-epoch run: logs and provenance part 2
932bc69 verified
Raw History Blame Contribute Delete
13.2 kB
#!/usr/bin/env bash
# Online DSpark training for Infinity-Parser2-Flash on the prepared 800k corpus.
set -Eeuo pipefail
ROOT="/inspire/sfs/project/inf-multimodal/public/wumengke"
REPO="${REPO:-$ROOT/speculators}"
MODEL="/inspire/sfs/project/inf-multimodal/public/data_mllm/publish_models/Infinity-Parser2.1-Flash-2608"
DATA_DIR="$ROOT/datasets/infinity_parsers2_v2_1/dflash_data/full"
RUN_DIR="${RUN_DIR:-$ROOT/model_weights/dspark_parser2_flash_v2_1_vocab32k}"
# A worker does more than wait on HTTP: it deserialises ~400 MB of hidden states
# per packed batch, drops the verifier layer, and collates into a preallocated
# 16384-token buffer -- roughly a CPU-second of memcpy per batch, and the 6 ranks
# between them need ~20 batches/s. At 4 workers each rank's four processes were
# pinned and the step waited on them, so give the (mostly idle) 176-thread host
# more of them. Memory is num_workers x prefetch_factor x ~400 MB x 6 ranks.
NUM_WORKERS="${NUM_WORKERS:-8}"
PREFETCH_FACTOR="${PREFETCH_FACTOR:-3}"
# Per-batch request fan-out. One wave covers a typical packed batch (~10 samples),
# so a rank never pays several serial request rounds for one step.
FETCH_THREADS="${FETCH_THREADS:-16}"
# CPU-side multimodal preprocessing is the throughput ceiling of this run, and it
# scales with API *processes*, not threads: vLLM runs the HF image processor on a
# per-process single-worker pool (vllm/renderers/base.py builds `_mm_executor`
# with max_workers=1), so concurrency equals VLLM_API_SERVER_COUNT exactly.
# Measured on this corpus: ~54 ms of CPU per image (SFS read + JPEG decode +
# Qwen2VL processor), flat from 8 to 64 processes, so throughput is
# ~18.5 x VLLM_API_SERVER_COUNT images/s. A step consumes ~61 images
# (6 ranks x ~10.2 packed samples), so 8 processes capped the pipeline at ~148
# img/s -- under the demand, which is why the training GPUs idled. 32 lifts that
# to ~590 img/s and still leaves most of the 176 threads to the trainer.
VLLM_API_SERVER_COUNT="${VLLM_API_SERVER_COUNT:-32}"
# Media loading (fetch + decode) has its own pool per process. Requests here carry
# ~1.06 images on average, so deep per-process fan-out buys nothing and 32 x 16
# would just oversubscribe the box.
VLLM_MEDIA_LOADING_THREADS="${VLLM_MEDIA_LOADING_THREADS:-4}"
# The processor cache is dead weight on this corpus: images are ~unique across
# 1.8M rows so it never hits, while every API process reserves its own budget
# (4 GiB by default -> 128 GiB at 32 processes). Setting it to 0 also lets vLLM
# skip content-hashing each image on the serialized MM thread, because with
# prefix caching off there is nothing left to key a reuse on.
VLLM_MM_PROCESSOR_CACHE_GB="${VLLM_MM_PROCESSOR_CACHE_GB:-0}"
# Per-request timeout. The tail matters more than the ceiling: a stuck request
# blocks its rank and every peer then waits at the gradient all-reduce, so fail
# fast and let --max-retries retry instead of holding the step for minutes.
REQUEST_TIMEOUT="${REQUEST_TIMEOUT:-120}"
MAX_RETRIES="${MAX_RETRIES:-1}"
CHECKPOINT_DIR="${CHECKPOINT_DIR:-$ROOT/model_weights/dspark_parser2_flash_v2_1_vocab32k}"
LOG_DIR="${LOG_DIR:-$RUN_DIR}"
WANDB_PROJECT="${WANDB_PROJECT:-infinity-parser2-flash}"
WANDB_MODE="${WANDB_MODE:-online}"
WANDB_KEY_FILE="${WANDB_KEY_FILE:-$ROOT/.secrets/wandb_key}"
MEDIA_ROOT="/inspire/sfs/project/inf-multimodal/public"
VLLM_PORT="${VLLM_PORT:-8200}"
BLOCK_SIZE="${BLOCK_SIZE:-16}"
MARKOV_RANK="${MARKOV_RANK:-256}"
MARKOV_HEAD_TYPE="${MARKOV_HEAD_TYPE:-vanilla}"
CONFIDENCE_HEAD_ALPHA="${CONFIDENCE_HEAD_ALPHA:-1.0}"
DEFAULT_LOSS_FN='{"ce": 0.1, "tv": 0.9}'
LOSS_FN="${LOSS_FN:-$DEFAULT_LOSS_FN}"
TARGET_LAYER_IDS=(2 7 12 17 22)
# Draft vocabulary. Empty (the default) trains on the verifier's full 248,320-token
# vocab, which is what the v2.1 run did. Point DRAFT_VOCAB_DIR at a
# scripts/build_vocab_mapping.py output directory to train a pruned vocab instead;
# d2t.npy / t2d.npy are read from there. Set DRAFT_VOCAB_SIZE as well to have the
# size asserted against the files -- train.py raises on a mismatch.
#
# Deliberately not defaulted to $DATA_DIR: train.py auto-discovers d2t.npy/t2d.npy
# sitting in --data-path (scripts/train.py:parse_vocab_mappings), so dropping them
# next to the arrow shards would silently switch *every* run on this corpus to a
# pruned vocab, full-vocab baselines included. Keeping the mappings in their own
# directory is what lets both configurations stay runnable.
# 默认走 32k 剪枝词表(vocab32k 续训配置)。要跑全词表基线时显式传空:
# DRAFT_VOCAB_DIR= DRAFT_VOCAB_SIZE= bash 本脚本
DRAFT_VOCAB_DIR="${DRAFT_VOCAB_DIR-$ROOT/datasets/infinity_parsers2_v2_1/dflash_data/vocab_32768}"
DRAFT_VOCAB_SIZE="${DRAFT_VOCAB_SIZE-32768}"
JOB_TAG="${SLURM_JOB_ID:-${JOB_ID:-$$}}"
HIDDEN_STATES_DIR=""
VLLM_LOG="$RUN_DIR/vllm_${JOB_TAG}.log"
SPEC_PYTHON="$REPO/speculators_venv/bin/python"
TORCHRUN="$REPO/speculators_venv/bin/torchrun"
VLLM_PYTHON="$REPO/vllm_venv/bin/python"
mkdir -p "$RUN_DIR" "$CHECKPOINT_DIR"
for executable in "$SPEC_PYTHON" "$TORCHRUN" "$VLLM_PYTHON"; do
if [[ ! -x "$executable" ]]; then
echo "Missing executable: $executable" >&2
exit 1
fi
done
if [[ ! -f "$MODEL/config.json" ]]; then
echo "Missing model config: $MODEL/config.json" >&2
exit 1
fi
if [[ ! -f "$WANDB_KEY_FILE" ]]; then
echo "Missing W&B key file: $WANDB_KEY_FILE" >&2
exit 1
fi
WANDB_API_KEY="$(tr -d '[:space:]' < "$WANDB_KEY_FILE")"
if [[ -z "$WANDB_API_KEY" ]]; then
echo "W&B key file is empty: $WANDB_KEY_FILE" >&2
exit 1
fi
export WANDB_API_KEY
for path in \
"$DATA_DIR/state.json" \
"$DATA_DIR/dataset_info.json" \
"$DATA_DIR/token_freq.pt"; do
if [[ ! -f "$path" ]]; then
echo "Missing prepared-data artifact: $path" >&2
exit 1
fi
done
VOCAB_ARGS=()
DRAFT_VOCAB_DESC="full (verifier vocab, no d2t/t2d)"
if [[ -n "$DRAFT_VOCAB_DIR" ]]; then
for path in "$DRAFT_VOCAB_DIR/d2t.npy" "$DRAFT_VOCAB_DIR/t2d.npy"; do
if [[ ! -f "$path" ]]; then
echo "Missing vocab mapping: $path" >&2
exit 1
fi
done
VOCAB_ARGS=(
--d2t-path "$DRAFT_VOCAB_DIR/d2t.npy"
--t2d-path "$DRAFT_VOCAB_DIR/t2d.npy"
)
if [[ -n "$DRAFT_VOCAB_SIZE" ]]; then
VOCAB_ARGS+=(--draft-vocab-size "$DRAFT_VOCAB_SIZE")
fi
DRAFT_VOCAB_DESC="$("$SPEC_PYTHON" -c \
'import sys, numpy; print(numpy.load(sys.argv[1]).shape[0])' \
"$DRAFT_VOCAB_DIR/d2t.npy") tokens from $DRAFT_VOCAB_DIR"
fi
for command in curl flock pgrep setsid; do
if ! command -v "$command" >/dev/null 2>&1; then
echo "Missing command: $command" >&2
exit 1
fi
done
ALLOCATED_GPUS="${CUDA_VISIBLE_DEVICES:-0,1,2,3,4,5,6,7}"
IFS=',' read -r -a GPU_LIST <<< "${ALLOCATED_GPUS//[[:space:]]/}"
if (( ${#GPU_LIST[@]} != 8 )); then
echo "Expected exactly 8 GPUs, got: $ALLOCATED_GPUS" >&2
exit 1
fi
# Online training fetches every hidden state from the vLLM side, so the split
# between generation and training GPUs is the main throughput knob. VLLM_GPU_COUNT
# takes the leading GPUs; the rest train. Note that changing the training count
# also changes the data-parallel degree, and therefore the tokens per step.
VLLM_GPU_COUNT="${VLLM_GPU_COUNT:-2}"
if (( VLLM_GPU_COUNT < 1 || VLLM_GPU_COUNT > ${#GPU_LIST[@]} - 1 )); then
echo "VLLM_GPU_COUNT must be between 1 and $(( ${#GPU_LIST[@]} - 1 ))" >&2
exit 1
fi
TRAIN_GPU_COUNT=$(( ${#GPU_LIST[@]} - VLLM_GPU_COUNT ))
VLLM_GPUS=$(IFS=,; printf '%s' "${GPU_LIST[*]:0:VLLM_GPU_COUNT}")
TRAIN_GPUS=$(IFS=,; printf '%s' "${GPU_LIST[*]:VLLM_GPU_COUNT:TRAIN_GPU_COUNT}")
exec 9>"$RUN_DIR/training.lock"
if ! flock -n 9; then
echo "Another job is using $RUN_DIR" >&2
exit 1
fi
VLLM_PID=""
TRAIN_PID=""
terminate_job() {
local pid="$1"
local alive
local -a children=()
[[ "$pid" =~ ^[1-9][0-9]*$ ]] || return 0
mapfile -t children < <(pgrep -P "$pid" 2>/dev/null || true)
if (( ${#children[@]} )); then
kill -TERM "${children[@]}" 2>/dev/null || true
fi
kill -TERM -- "-$pid" 2>/dev/null || true
for _ in {1..30}; do
alive=0
kill -0 "$pid" 2>/dev/null && alive=1
for child in "${children[@]}"; do
kill -0 "$child" 2>/dev/null && alive=1
done
(( alive == 0 )) && return 0
sleep 1
done
if (( ${#children[@]} )); then
kill -KILL "${children[@]}" 2>/dev/null || true
fi
kill -KILL -- "-$pid" 2>/dev/null || true
}
cleanup() {
local status=$?
trap - EXIT INT TERM HUP
terminate_job "$TRAIN_PID"
terminate_job "$VLLM_PID"
[[ -n "$TRAIN_PID" ]] && wait "$TRAIN_PID" 2>/dev/null || true
[[ -n "$VLLM_PID" ]] && wait "$VLLM_PID" 2>/dev/null || true
case "$HIDDEN_STATES_DIR" in
*/dspark_parser2_flash_*) rm -rf -- "$HIDDEN_STATES_DIR" ;;
esac
exit "$status"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM HUP
HIDDEN_STATES_DIR="$(mktemp -d "${TMPDIR:-/tmp}/dspark_parser2_flash_${JOB_TAG}.XXXXXX")"
"$SPEC_PYTHON" - "$VLLM_PORT" <<'PY'
import socket
import sys
port = int(sys.argv[1])
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(1)
if sock.connect_ex(("127.0.0.1", port)) == 0:
raise SystemExit(f"Port {port} is already in use")
PY
echo "Model: $MODEL"
echo "Data: $DATA_DIR"
echo "Draft vocab: $DRAFT_VOCAB_DESC"
echo "Checkpoints: $CHECKPOINT_DIR"
echo "vLLM GPUs: $VLLM_GPUS"
echo "Training GPUs: $TRAIN_GPUS"
echo "Block size: $BLOCK_SIZE"
echo "Markov head: $MARKOV_HEAD_TYPE (rank $MARKOV_RANK)"
echo "DSpark loss: $LOSS_FN"
echo "Fetch: $NUM_WORKERS workers x $FETCH_THREADS threads, prefetch $PREFETCH_FACTOR"
echo "vLLM CPU: $VLLM_API_SERVER_COUNT API processes (= MM concurrency) x $VLLM_MEDIA_LOADING_THREADS media threads, mm cache ${VLLM_MM_PROCESSOR_CACHE_GB}GiB"
echo "Requests: ready-first, no keepalive, timeout ${REQUEST_TIMEOUT}s x $((MAX_RETRIES + 1)) attempts"
setsid env \
CUDA_VISIBLE_DEVICES="$VLLM_GPUS" \
PYTHONUNBUFFERED=1 \
VLLM_MEDIA_LOADING_THREAD_COUNT="$VLLM_MEDIA_LOADING_THREADS" \
"$VLLM_PYTHON" "$REPO/scripts/launch_vllm.py" "$MODEL" \
--target-layer-ids "${TARGET_LAYER_IDS[@]}" \
--hidden-states-path "$HIDDEN_STATES_DIR" \
--tensor-parallel-size 1 \
--data-parallel-size "$VLLM_GPU_COUNT" \
--gpu-memory-utilization 0.9 \
--max_model_len 65536 \
--api-server-count "$VLLM_API_SERVER_COUNT" \
--mm-processor-cache-gb "$VLLM_MM_PROCESSOR_CACHE_GB" \
--served-model-name "$MODEL" \
--allowed-local-media-path "$MEDIA_ROOT" \
--limit-mm-per-prompt '{"image":16}' \
--port "$VLLM_PORT" \
>"$VLLM_LOG" 2>&1 &
VLLM_PID=$!
deadline=$((SECONDS + 1800))
until curl -fsS --max-time 10 "http://127.0.0.1:${VLLM_PORT}/health" >/dev/null 2>&1; do
if ! kill -0 "$VLLM_PID" 2>/dev/null; then
tail -n 100 "$VLLM_LOG" >&2 || true
wait "$VLLM_PID"
fi
if (( SECONDS >= deadline )); then
tail -n 100 "$VLLM_LOG" >&2 || true
echo "Timed out waiting for vLLM" >&2
exit 1
fi
sleep 2
done
setsid env \
CUDA_VISIBLE_DEVICES="$TRAIN_GPUS" \
PYTHONUNBUFFERED=1 \
WANDB_PROJECT="$WANDB_PROJECT" \
WANDB_MODE="$WANDB_MODE" \
"$TORCHRUN" \
--standalone \
--nproc_per_node "$TRAIN_GPU_COUNT" \
"$REPO/scripts/train.py" \
--verifier-name-or-path "$MODEL" \
--data-path "$DATA_DIR" \
--save-path "$CHECKPOINT_DIR" \
"${VOCAB_ARGS[@]}" \
--speculator-type dspark \
--draft-arch qwen3 \
--draft-hidden-act silu \
--num-layers "${NUM_LAYERS:-5}" \
--mask-token-id 248077 \
--block-size "$BLOCK_SIZE" \
--sample-from-anchor \
--max-anchors "${MAX_ANCHORS:-3072}" \
--target-layer-ids "${TARGET_LAYER_IDS[@]}" \
--draft-mrope-full-head-hack \
--sliding-window 2048 \
--sliding-window-non-causal \
--draft-attn-impl simple_flex_attention \
--markov-rank "$MARKOV_RANK" \
--markov-head-type "$MARKOV_HEAD_TYPE" \
--enable-confidence-head \
--confidence-head-with-markov \
--confidence-head-alpha "$CONFIDENCE_HEAD_ALPHA" \
--loss-fn "$LOSS_FN" \
--dflash-decay-gamma 4.0 \
--optimizer muon \
--muon-lr 2e-4 \
--lr 1e-4 \
--scheduler-type cosine \
--scheduler-warmup-ratio 0.01 \
--epochs 3 \
--checkpoint-freq 0.1 \
--total-seq-len 16384 \
--train-data-ratio 0.99 \
--noise-std 0 \
--hidden-states-dtype bfloat16 \
--num-workers "$NUM_WORKERS" \
--prefetch-factor "$PREFETCH_FACTOR" \
--no-dataloader-in-order \
--fetch-threads "$FETCH_THREADS" \
--hidden-states-backend file \
--hidden-states-path "$HIDDEN_STATES_DIR" \
--vllm-endpoint "http://127.0.0.1:${VLLM_PORT}/v1" \
--no-vllm-http-keepalive \
--on-missing generate \
--on-generate delete \
--request-timeout "$REQUEST_TIMEOUT" \
--max-retries "$MAX_RETRIES" \
--fail-on-hidden-state-error \
--seed 42 \
--logger wandb \
--log-dir "$LOG_DIR" \
--run-name "$JOB_TAG" &
TRAIN_PID=$!
wait "$TRAIN_PID"