File size: 13,220 Bytes
932bc69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
#!/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"