192 lines
7.2 KiB
Bash
Executable file
192 lines
7.2 KiB
Bash
Executable file
#!/bin/sh
|
|
# Authentic native-server disconnect during prompt prefill, followed by same-process recovery.
|
|
set -eu
|
|
|
|
if [ "$#" -ne 5 ]; then
|
|
echo "usage: $0 MODEL.gguf CONTEXT PROMPT_FILE OUTPUT_DIR PORT" >&2
|
|
exit 2
|
|
fi
|
|
|
|
model=$1
|
|
context=$2
|
|
prompt=$3
|
|
output_dir=$4
|
|
port=$5
|
|
cancel_after=${GEM_CANCEL_AFTER_SECONDS:-5}
|
|
recovery_limit=${GEM_CANCEL_RECOVERY_LIMIT_SECONDS:-60}
|
|
kv_type=${GEM_KV_TYPE:-f32}
|
|
case $context:$port:$cancel_after:$recovery_limit in
|
|
*[!0-9:]*|:*|*::*)
|
|
echo "gem: context, port, and cancellation limits must be positive integers" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
if [ "$context" -eq 0 ] || [ "$port" -eq 0 ] || [ "$cancel_after" -eq 0 ] ||
|
|
[ "$recovery_limit" -eq 0 ]; then
|
|
echo "gem: context, port, and cancellation limits must be positive integers" >&2
|
|
exit 2
|
|
fi
|
|
if [ "$port" -gt 65535 ]; then
|
|
echo "gem: port must not exceed 65535" >&2
|
|
exit 2
|
|
fi
|
|
case $kv_type in
|
|
f32|f16|f16-full) ;;
|
|
*) echo "gem: GEM_KV_TYPE must be f32, f16, or f16-full" >&2; exit 2 ;;
|
|
esac
|
|
if [ ! -r "$model" ] || [ ! -r "$prompt" ]; then
|
|
echo "gem: model and prompt must be readable" >&2
|
|
exit 2
|
|
fi
|
|
if [ -e "$output_dir" ]; then
|
|
echo "gem: output directory already exists: $output_dir" >&2
|
|
exit 2
|
|
fi
|
|
if command -v lsof >/dev/null 2>&1 &&
|
|
lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; then
|
|
echo "gem: cancellation-gate port $port is already occupied" >&2
|
|
exit 1
|
|
fi
|
|
|
|
prompt_count_json=$(build/gem-tokenize --count --file --chat-user "$model" "$prompt")
|
|
prompt_tokens=$(printf '%s\n' "$prompt_count_json" | jq -er '.token_count')
|
|
if [ "$prompt_tokens" -lt 128 ] || [ "$prompt_tokens" -gt $((context - 2)) ]; then
|
|
echo "gem: cancellation prompt must contain 128..$((context - 2)) chat tokens" >&2
|
|
exit 2
|
|
fi
|
|
model_sha256=${GEM_MODEL_SHA256:-}
|
|
if [ -z "$model_sha256" ]; then
|
|
model_sha256=$(shasum -a 256 "$model" | awk '{print $1}')
|
|
fi
|
|
case $model_sha256 in
|
|
*[!0-9a-fA-F]*) echo "gem: invalid model SHA-256" >&2; exit 2 ;;
|
|
esac
|
|
if [ "${#model_sha256}" -ne 64 ]; then
|
|
echo "gem: invalid model SHA-256" >&2
|
|
exit 2
|
|
fi
|
|
prompt_sha256=$(shasum -a 256 "$prompt" | awk '{print $1}')
|
|
|
|
mkdir -p "$output_dir"
|
|
build/gem print-config --model "$model" --context "$context" --kv-type "$kv_type" \
|
|
> "$output_dir/print-config.json"
|
|
jq -e --arg kv_type "$kv_type" --argjson context "$context" \
|
|
'.engine == "gem-native-c-metal" and .context == $context and .kv_type == $kv_type' \
|
|
"$output_dir/print-config.json" >/dev/null
|
|
jq -n --arg model "$model" --arg model_sha256 "$model_sha256" \
|
|
--arg prompt "$prompt" --arg prompt_sha256 "$prompt_sha256" --arg kv_type "$kv_type" \
|
|
--argjson context "$context" --argjson prompt_tokens "$prompt_tokens" \
|
|
--argjson cancel_after_seconds "$cancel_after" \
|
|
--argjson recovery_limit_seconds "$recovery_limit" \
|
|
'{schema_version: 1, engine: "gem-native-c-metal", model: $model,
|
|
model_sha256: $model_sha256, prompt_file: $prompt, prompt_sha256: $prompt_sha256,
|
|
context: $context, kv_type: $kv_type, prompt_tokens: $prompt_tokens,
|
|
cancel_after_seconds: $cancel_after_seconds,
|
|
recovery_limit_seconds: $recovery_limit_seconds}' > "$output_dir/gate-config.json"
|
|
jq -n --rawfile prompt "$prompt" \
|
|
'{model: "gemma4", stream: true, max_tokens: 2,
|
|
messages: [{role: "user", content: $prompt}]}' > "$output_dir/cancel-request.json"
|
|
printf 'Reply with exactly OK.\n' > "$output_dir/recovery-prompt.txt"
|
|
|
|
server_pid=
|
|
cleanup()
|
|
{
|
|
if [ -n "$server_pid" ] && kill -0 "$server_pid" 2>/dev/null; then
|
|
kill "$server_pid"
|
|
wait "$server_pid" 2>/dev/null || true
|
|
fi
|
|
server_pid=
|
|
}
|
|
trap cleanup EXIT INT TERM HUP
|
|
|
|
build/gem serve --model "$model" --alias gemma4 --port "$port" \
|
|
--context "$context" --max-tokens 2 --command-span token --kv-type "$kv_type" \
|
|
> "$output_dir/server.stdout" 2> "$output_dir/server.stderr" &
|
|
server_pid=$!
|
|
ready=0
|
|
attempt=0
|
|
while [ "$attempt" -lt 1200 ]; do
|
|
if curl --fail --silent --max-time 1 \
|
|
"http://127.0.0.1:$port/health" >/dev/null 2>&1; then
|
|
ready=1
|
|
break
|
|
fi
|
|
if ! kill -0 "$server_pid" 2>/dev/null; then
|
|
echo "gem: server exited before cancellation-gate readiness" >&2
|
|
exit 1
|
|
fi
|
|
attempt=$((attempt + 1))
|
|
sleep 0.1
|
|
done
|
|
if [ "$ready" -ne 1 ]; then
|
|
echo "gem: cancellation-gate readiness timed out" >&2
|
|
exit 1
|
|
fi
|
|
|
|
set +e
|
|
curl --silent --show-error --no-buffer --max-time "$cancel_after" \
|
|
-H 'Content-Type: application/json' --data-binary @"$output_dir/cancel-request.json" \
|
|
"http://127.0.0.1:$port/v1/chat/completions" \
|
|
> "$output_dir/cancelled.sse" 2> "$output_dir/curl.stderr"
|
|
curl_status=$?
|
|
set -e
|
|
if [ "$curl_status" -ne 28 ]; then
|
|
echo "gem: cancellation curl returned $curl_status, expected timeout status 28" >&2
|
|
exit 1
|
|
fi
|
|
keepalives=$(grep -c '^: keepalive$' "$output_dir/cancelled.sse" || true)
|
|
if [ "$keepalives" -lt 1 ]; then
|
|
echo "gem: cancellation stream contained no keepalive" >&2
|
|
exit 1
|
|
fi
|
|
if grep -Fq "gem: prompt prefill $prompt_tokens/$prompt_tokens tokens (100.00%)" \
|
|
"$output_dir/server.stderr"; then
|
|
echo "gem: cancellation prompt completed before the disconnect was observed" >&2
|
|
exit 1
|
|
fi
|
|
|
|
recovery_start=$(date +%s)
|
|
health_status=0
|
|
while :; do
|
|
if curl --fail --silent --max-time 1 \
|
|
"http://127.0.0.1:$port/health" >/dev/null 2>&1; then
|
|
health_status=200
|
|
break
|
|
fi
|
|
recovery_now=$(date +%s)
|
|
if [ $((recovery_now - recovery_start)) -ge "$recovery_limit" ]; then
|
|
echo "gem: server did not recover within $recovery_limit seconds" >&2
|
|
exit 1
|
|
fi
|
|
if ! kill -0 "$server_pid" 2>/dev/null; then
|
|
echo "gem: server exited after cancellation" >&2
|
|
exit 1
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
recovery_end=$(date +%s)
|
|
recovery_seconds=$((recovery_end - recovery_start))
|
|
|
|
GEM_STREAM_MAX_TOKENS=16 scripts/server-stream-probe.sh \
|
|
"http://127.0.0.1:$port/v1" gemma4 "$output_dir/recovery-prompt.txt" \
|
|
"$output_dir/recovery.sse" "$output_dir/recovery-measurement.json"
|
|
jq -e '.http_code == 200 and .requested_max_tokens == 16 and
|
|
.visible_event_count > 0 and .finish_reason != null and
|
|
.usage.prompt_tokens > 0 and .usage.completion_tokens > 0 and
|
|
.usage.total_tokens == (.usage.prompt_tokens + .usage.completion_tokens)' \
|
|
"$output_dir/recovery-measurement.json" >/dev/null
|
|
|
|
jq -n --arg model "$model" --arg model_sha256 "$model_sha256" \
|
|
--arg prompt_sha256 "$prompt_sha256" --arg kv_type "$kv_type" \
|
|
--argjson context "$context" --argjson prompt_tokens "$prompt_tokens" \
|
|
--argjson curl_status "$curl_status" --argjson keepalives "$keepalives" \
|
|
--argjson health_status "$health_status" --argjson recovery_seconds "$recovery_seconds" \
|
|
'{schema_version: 1, engine: "gem-native-c-metal", workload: "prefill-cancel-recovery",
|
|
model: $model, model_sha256: $model_sha256, prompt_sha256: $prompt_sha256,
|
|
context: $context, kv_type: $kv_type, prompt_tokens: $prompt_tokens,
|
|
cancellation_curl_status: $curl_status, keepalive_events: $keepalives,
|
|
post_cancel_health_status: $health_status,
|
|
post_cancel_health_recovery_seconds: $recovery_seconds,
|
|
recovery_requested_max_tokens: 16,
|
|
same_process_recovery_request: "passed"}' > "$output_dir/summary.json"
|
|
jq . "$output_dir/summary.json"
|