File size: 9,784 Bytes
7850e45 | 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 | #!/usr/bin/env bash
# Make the FP8 copy of Clef-Flash and compare it with BF16, end to end, on ML-Lab.
#
# 1. Quantize on CPU in a throwaway container (quantize_fp8.py). This step
# is skipped if the FP8 directory already has a MANIFEST.json.
# 2. Start the BF16 model server, send the parity requests, save the answers
# and the server log, and stop the server.
# 3. The same for the FP8 copy.
# 4. Compare the answers and write one report: results/<time>/report.md.
#
# Run it from anywhere: ~/clef-fp8/run_all.sh
# Change a setting with an environment variable, for example:
# PORT=8090 EXTRA_REQUESTS=~/real.jsonl ~/clef-fp8/run_all.sh
#
# The exit code is 0 for PASS and 1 for FAIL or for an error.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
MODEL_DIR="${MODEL_DIR:-$HOME/clef-flash}" # Cloudflare's files (BF16)
FP8_DIR="${FP8_DIR:-$HOME/clef-flash-fp8}" # the FP8 copy goes here
# The model-server image for both servers. Set SERVER_IMAGE="" to run the code
# in CODE_DIR on the vLLM base image instead.
SERVER_IMAGE="${SERVER_IMAGE-docker-registry.wikimedia.org/wikimedia/machinelearning-liftwing-inference-services-decision-models-clef:2026-10-14-101821-publish}"
CODE_DIR="${CODE_DIR:-$HOME/inference-services}" # used only when SERVER_IMAGE=""
PORT="${PORT:-8080}" # gRPC uses PORT + 1
GPU="${GPU:-0}" # GPU index, as rocm-smi numbers it
GPU_MEMORY_UTILIZATION="${GPU_MEMORY_UTILIZATION:-0.5}" # share of the GPU for vLLM
EXTRA_REQUESTS="${EXTRA_REQUESTS:-}" # optional JSONL file of extra requests
BF16_URL="${BF16_URL:-}" # use a BF16 server that is already running, e.g. http://localhost:8080/v1/models/clef-flash:predict
BF16_CONTAINER="${BF16_CONTAINER:-}" # its container name, to read vLLM's memory numbers from its log (optional)
PROXY="${PROXY:-http://webproxy:8080}"
VLLM_BASE="${VLLM_BASE:-docker-registry.wikimedia.org/ml/amd-vllm022:gfx90agfx942rocm7.2.0pytorch2.10.0flash-attn2.8.3aiter0.1.13vllm0.22.1-3}"
RESULTS="${RESULTS:-$HERE/results/$(date -u +%Y%m%dT%H%M%SZ)}"
URL="http://localhost:$PORT/v1/models/clef-flash:predict"
fail() { echo "ERROR: $*" >&2; exit 1; }
# Stop early if the GPU does not have the memory that vLLM asks for. ML-Lab's
# GPU is shared. Skipped if rocm-smi is missing or prints another format.
check_gpu() {
local used need
used=$(rocm-smi --showmemuse 2>/dev/null | sed -n "s/^GPU\[$GPU\].*Allocated (VRAM%): *\([0-9][0-9]*\).*/\1/p" | head -1 || true)
[ -n "$used" ] || return 0
need=$(python3 -c "print(round($GPU_MEMORY_UTILIZATION * 100))")
echo " GPU $GPU: ${used}% of memory in use, vLLM needs ${need}% free"
[ $((used + need)) -le 100 ] || fail "GPU $GPU does not have ${need}% free memory. See what uses it: rocm-smi --showpidgpus; docker ps. Or use GPU=<index> or a lower GPU_MEMORY_UTILIZATION."
}
cleanup() { docker rm -f clef-parity-bf16 clef-parity-fp8 >/dev/null 2>&1 || true; }
trap cleanup EXIT
# --- Checks before anything slow -------------------------------------------
[ -f "$MODEL_DIR/joint_head.safetensors" ] || fail "MODEL_DIR=$MODEL_DIR has no joint_head.safetensors."
[ -n "$SERVER_IMAGE" ] || grep -q collapse_image_pads "$CODE_DIR/src/models/decision_models/clef/model.py" 2>/dev/null \
|| fail "CODE_DIR=$CODE_DIR does not have the vLLM clef server."
python3 -c 'import sys; sys.exit(sys.version_info < (3, 9))' || fail "parity.py needs Python 3.9 or newer."
for p in "$PORT" "$((PORT + 1))"; do
! ss -ltn 2>/dev/null | grep -E ":${p}[[:space:]]" >/dev/null || fail "Port $p is in use. Run with PORT=<another port>."
done
[ -z "$EXTRA_REQUESTS" ] || [ -f "$EXTRA_REQUESTS" ] || fail "EXTRA_REQUESTS=$EXTRA_REQUESTS does not exist."
mkdir -p "$RESULTS"
echo "Results: $RESULTS"
# --- 1. Quantize -------------------------------------------------------------
PARTIAL="$FP8_DIR.partial"
if [ -f "$FP8_DIR/MANIFEST.json" ]; then
echo "== 1/4 The FP8 copy exists ($FP8_DIR). Quantization skipped."
else
[ ! -e "$FP8_DIR" ] || [ -z "$(ls -A "$FP8_DIR")" ] \
|| fail "$FP8_DIR has files but no MANIFEST.json, so it is not a finished FP8 copy. Remove it with: rm -rf $FP8_DIR"
echo "== 1/4 Quantizing on CPU. This takes about 15 minutes and 35 GB of RAM."
# Write to a separate directory, and move it into place only on success. A
# failed run then leaves nothing half-written in FP8_DIR.
rm -rf "$PARTIAL" && mkdir -p "$PARTIAL" && chmod 777 "$PARTIAL"
# The constraint keeps the image's torch and transformers: pip fails rather
# than replace them. pip also installs compressed-tensors 0.18.0, while the
# vLLM in this image pins 0.15.0.1. This container does not run vLLM, so
# --no-warn-conflicts hides that warning. No GPU devices, so llm-compressor
# works on the CPU.
docker run --rm --network host -e http_proxy="$PROXY" -e https_proxy="$PROXY" \
-v "$MODEL_DIR":/src:ro -v "$PARTIAL":/dst -v "$HERE":/scripts:ro "$VLLM_BASE" bash -c '
python3 -c "import torch, transformers; print(f\"torch=={torch.__version__}\ntransformers=={transformers.__version__}\")" > /tmp/keep.txt &&
pip install -q --no-warn-conflicts -c /tmp/keep.txt llmcompressor==0.13.0 &&
python3 /scripts/quantize_fp8.py --src /src --dst /dst' 2>&1 | tee "$RESULTS/quantize.log" \
|| fail "Quantization failed. Log: $RESULTS/quantize.log"
# The container wrote the files as its own user, and some only readable by
# that user. Give them to you, and make them readable for the model servers.
docker run --rm --user 0 -v "$PARTIAL":/dst "$VLLM_BASE" \
bash -c "chown -R $(id -u):$(id -g) /dst && chmod -R a+rX /dst" \
|| fail "Could not change the owner of the files in $PARTIAL."
rmdir "$FP8_DIR" 2>/dev/null || true
mv "$PARTIAL" "$FP8_DIR"
fi
cp "$FP8_DIR/MANIFEST.json" "$RESULTS/"
# --- 2 and 3. Serve each model and ask the same questions ----------------------
ask() { # usage: ask LABEL MODEL_DIRECTORY STEP
local label=$1 dir=$2 step=$3 name="clef-parity-$1" waited=0
echo "== $step/4 Starting the $label server (vLLM start and warmup take a few minutes)"
docker rm -f "$name" >/dev/null 2>&1 || true
check_gpu
local args=(--name "$name" --network host
--device=/dev/kfd --device=/dev/dri --group-add video --group-add 105 --shm-size 8g
-e HIP_VISIBLE_DEVICES="$GPU" -e MODEL_NAME=clef-flash -e MODEL_PATH=/mnt/models -e HF_HUB_OFFLINE=1
-e GPU_MEMORY_UTILIZATION="$GPU_MEMORY_UTILIZATION" -v "$dir":/mnt/models:ro)
if [ -n "$SERVER_IMAGE" ]; then
# The image's entrypoint runs model.py and passes these arguments to it.
docker run -d "${args[@]}" "$SERVER_IMAGE" --http_port "$PORT" --grpc_port "$((PORT + 1))" >/dev/null \
|| fail "Docker could not start the $label container."
else
docker run -d "${args[@]}" -e http_proxy="$PROXY" -e https_proxy="$PROXY" -e no_proxy=localhost,127.0.0.1 \
-v "$CODE_DIR":/srv/app "$VLLM_BASE" \
bash -c "cd /srv/app/src/models/decision_models/clef && pip install -q -r requirements.txt && \
python3 model.py --http_port $PORT --grpc_port $((PORT + 1))" >/dev/null \
|| fail "Docker could not start the $label container."
fi
# grep -c, not grep -q: grep -q stops at the first match, `docker logs` then
# fails with SIGPIPE, and under pipefail the check never passes.
until docker logs "$name" 2>&1 | grep -c "Warmup complete" >/dev/null; do
if [ "$(docker inspect -f '{{.State.Running}}' "$name")" != true ] || [ "$waited" -ge 1800 ]; then
docker logs "$name" >"$RESULTS/server-$label.log" 2>&1
echo "--- Error lines in the $label server log, first one first:"
grep -E "Error|ERROR" "$RESULTS/server-$label.log" | cut -c1-240 | head -15 || true
fail "The $label server did not start. Full log: $RESULTS/server-$label.log"
fi
sleep 10
waited=$((waited + 10))
[ $((waited % 60)) -ne 0 ] || echo " still starting ($((waited / 60)) min): $(docker logs "$name" 2>&1 | tail -1 | cut -c1-100)"
done
echo " Sending the parity requests to the $label server"
https_proxy="$PROXY" python3 "$HERE/parity.py" run --label "$label" --out "$RESULTS/$label.json" \
--url "$URL" --image-dir "$HERE/parity-images" ${EXTRA_REQUESTS:+--requests "$EXTRA_REQUESTS"} | tail -1 \
|| fail "Sending the parity requests to the $label server failed (see the error above)."
docker logs "$name" >"$RESULTS/server-$label.log" 2>&1
docker rm -f "$name" >/dev/null
}
if [ -n "$BF16_URL" ]; then
echo "== 2/4 Using the BF16 server that is already running: $BF16_URL"
curl -sf --noproxy '*' "${BF16_URL%:predict}" >/dev/null || fail "No ready model at ${BF16_URL%:predict}."
https_proxy="$PROXY" python3 "$HERE/parity.py" run --label bf16 --out "$RESULTS/bf16.json" \
--url "$BF16_URL" --image-dir "$HERE/parity-images" ${EXTRA_REQUESTS:+--requests "$EXTRA_REQUESTS"} | tail -1 \
|| fail "Sending the parity requests to the BF16 server failed (see the error above)."
if [ -n "$BF16_CONTAINER" ]; then
docker logs "$BF16_CONTAINER" >"$RESULTS/server-bf16.log" 2>&1 || echo " Could not read the log of $BF16_CONTAINER."
fi
else
ask bf16 "$MODEL_DIR" 2
fi
ask fp8 "$FP8_DIR" 3
# --- 4. Report -----------------------------------------------------------------
echo "== 4/4 Writing the report"
python3 "$HERE/parity.py" report "$RESULTS" \
--note "Servers: ${SERVER_IMAGE:-the code in $CODE_DIR on $VLLM_BASE}, on $(hostname), GPU $GPU, GPU_MEMORY_UTILIZATION=$GPU_MEMORY_UTILIZATION, one request at a time." \
${BF16_URL:+--note "The BF16 server was already running ($BF16_URL), so its settings are whatever it was started with."}
|