- env: STATE_FILE -> r9v-state/q8xl; add dev-overlay adapter mount (quantized inject shard dequant, fork 363d606), FULL_MUTABLE_CACHE=0 (80-slot tiered vs the overlay's uncensored 160-slot injection), and the qwen-sharp-v22.4.0.jinja chat template via the launcher's new R9V_CHAT_TEMPLATE_FILE/KWARGS passthrough (fork 30d4a32) - serve/deploy: shard gates extended to SHARD4/5/6 (unset-safe, the uncensored rollback state carries none) - seams: STATE_FILE/LLM_PRIMARY_STATE -> q8xl
143 lines
6.0 KiB
Bash
Executable File
143 lines
6.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# serve-frank-primary.sh — frank-primary endpoint (R9V radiance-vLLM fork,
|
|
# unsloth Qwen3.8-Flash-Next Q8_0 + MTP4 drafter, 2x R9700, dankserver
|
|
# :7770; 2026-09-27 cutover from the uncensored IQ4_XS payload, whose state
|
|
# dir stays on disk as the rollback target).
|
|
#
|
|
# The engine is the upstream R9V launcher (scripts/launch.sh in the fork
|
|
# checkout), NOT a hand-frozen podman argv: launch.sh owns the resolved
|
|
# docker/podman args (binds, overlays, device passthrough, publish lines)
|
|
# and the fork's qualification was measured through it. This script's job:
|
|
#
|
|
# 1. layer the on-host R9V setup state (the 104-key tuned shape the
|
|
# profile setup qualified) under this repo's frank-primary.env
|
|
# overrides (bind/port/alias/container);
|
|
# 2. gate on the pinned runtime image (by its OCI manifest digest, the
|
|
# podman RepoDigest), the exact-size PLE payload, and the GGUF shards;
|
|
# 3. exec the launcher, then wait on the container: launch.sh --detach
|
|
# exits as soon as the container is created, so without the wait the
|
|
# unit would be "green" while the engine crashes — the wait is what
|
|
# keeps systemd's Restart=on-failure self-healing real.
|
|
#
|
|
# The root unit (scripts/frank-primary.service, User=dank,
|
|
# LimitMEMLOCK=infinity) grants the pin budget for the tiered-expert UVA
|
|
# host copy and the PLE pinned residency (54.4 GB on the q8 payload). A
|
|
# plain shell session also works (proven 2026-09-27: podman rootless
|
|
# inherits usable limits here), but the unit is the fleet-parity
|
|
# boot-persistent shape.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ENV_FILE="${FRANK_PRIMARY_ENV_FILE:-$SCRIPT_DIR/frank-primary.env}"
|
|
|
|
if [ ! -f "$ENV_FILE" ]; then
|
|
echo "serve-frank-primary: env file missing: $ENV_FILE" >&2
|
|
exit 1
|
|
fi
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
source "$ENV_FILE"
|
|
set +a
|
|
|
|
: "${R9V_HOME:?R9V_HOME must name the fork checkout}"
|
|
: "${R9V_STATE_FILE:?R9V_STATE_FILE must name the setup state}"
|
|
: "${R9V_HOST_BIND:?R9V_HOST_BIND must be set}"
|
|
: "${R9V_HOST_PORT:?R9V_HOST_PORT must be set}"
|
|
: "${R9V_SERVED_MODEL_NAME:?R9V_SERVED_MODEL_NAME must be set}"
|
|
: "${R9V_CONTAINER_NAME:?R9V_CONTAINER_NAME must be set}"
|
|
|
|
LAUNCHER="$R9V_HOME/scripts/launch.sh"
|
|
if [ ! -x "$LAUNCHER" ]; then
|
|
echo "serve-frank-primary: R9V launcher missing: $LAUNCHER" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# The docker->podman shim is REQUIRED: the launcher and its doctor speak
|
|
# docker (OCI-manifest-digest image IDs, LogConfig fields) that podman's
|
|
# native CLI answers differently. The shim carries those laws.
|
|
SHIM_DIR="$R9V_HOME/.podman-shim"
|
|
if [ ! -x "$SHIM_DIR/docker" ]; then
|
|
echo "serve-frank-primary: docker->podman shim missing: $SHIM_DIR/docker" >&2
|
|
exit 1
|
|
fi
|
|
export PATH="$SHIM_DIR:$PATH"
|
|
|
|
# --- layer 1: the qualified setup state (production overrides never come
|
|
# from here; keys already defined in the env file are skipped) -------------
|
|
STATE_FILE="$R9V_STATE_FILE"
|
|
if [ ! -f "$STATE_FILE" ]; then
|
|
echo "serve-frank-primary: R9V setup state missing: $STATE_FILE" >&2
|
|
echo " run the profile setup first (tools/setup_profile.py setup --state-dir ...)" >&2
|
|
exit 1
|
|
fi
|
|
STATE_EXPORTS="$(python3 - "$STATE_FILE" <<'PY'
|
|
import json, shlex, sys, os
|
|
state = json.load(open(sys.argv[1]))
|
|
config = state.get("config") or {}
|
|
if state.get("ready") is not True:
|
|
sys.exit("R9V setup state is not ready; rerun setup before serving")
|
|
env_keys = {k for k in os.environ if k.startswith("R9V_")}
|
|
for key in sorted(config):
|
|
if key.startswith("R9V_") and key not in env_keys:
|
|
print(f"export {key}={shlex.quote(config[key])}")
|
|
PY
|
|
)"
|
|
eval "$STATE_EXPORTS"
|
|
|
|
: "${R9V_IMAGE:?state must carry R9V_IMAGE}"
|
|
: "${R9V_MODEL_DIR:?state must carry R9V_MODEL_DIR}"
|
|
: "${R9V_PLE_PATH:?state must carry R9V_PLE_PATH}"
|
|
|
|
# --- gates: never serve a half-finished payload or a missing image --------
|
|
# Runtime image by pinned OCI manifest digest (podman keys images by config
|
|
# digest but preserves the manifest digest in RepoDigests — the shim's ID law).
|
|
if ! podman images --format '{{.RepoDigests}}' | grep -qF "${R9V_IMAGE#sha256:}"; then
|
|
echo "serve-frank-primary: pinned R9V image absent: $R9V_IMAGE" >&2
|
|
echo " load the pinned image bundle into podman rootless storage" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# PLE payload at its pinned byte count (stat -c %s, never wc -c), plus the
|
|
# six target GGUF shards (mradermacher-style split parts) and the mmproj
|
|
# the launcher mounts.
|
|
PLE_BYTES="${R9V_PLE_EXPECTED_BYTES:-}"
|
|
if [ -n "$PLE_BYTES" ]; then
|
|
ACTUAL="$(stat -c %s "$R9V_PLE_PATH" 2>/dev/null || echo 0)"
|
|
if [ "$ACTUAL" != "$PLE_BYTES" ]; then
|
|
echo "serve-frank-primary: PLE payload wrong size: $R9V_PLE_PATH" >&2
|
|
echo " expected $PLE_BYTES, found $ACTUAL" >&2
|
|
exit 1
|
|
fi
|
|
else
|
|
if [ ! -f "$R9V_PLE_PATH" ]; then
|
|
echo "serve-frank-primary: PLE payload missing: $R9V_PLE_PATH" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
for rel in "$R9V_TARGET_REL" "${R9V_TARGET_SHARD2_REL:-}" "${R9V_TARGET_SHARD3_REL:-}" \
|
|
"${R9V_TARGET_SHARD4_REL:-}" "${R9V_TARGET_SHARD5_REL:-}" "${R9V_TARGET_SHARD6_REL:-}" \
|
|
"$R9V_MMPROJ_REL"; do
|
|
if [ -z "$rel" ]; then continue; fi
|
|
if [ ! -f "$R9V_MODEL_DIR/$rel" ]; then
|
|
echo "serve-frank-primary: payload file missing: $R9V_MODEL_DIR/$rel" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
if [ ! -e /dev/kfd ] || [ ! -d /dev/dri ]; then
|
|
echo "serve-frank-primary: GPU device nodes absent (/dev/kfd, /dev/dri)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# A crashed/killed unit can leave the container name registered (systemd
|
|
# SIGKILLs conmon before cleanup runs); the launcher refuses an existing
|
|
# name forever after. Clear any orphan of OUR name first — idempotent.
|
|
docker rm -f "$R9V_CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
|
|
# launch.sh detaches after preflight; from here on, this script's lifetime
|
|
# IS the container's: wait returns the container's exit code, so a crash is
|
|
# a nonzero ExecStart and Restart=on-failure fires. (systemd stop goes
|
|
# through ExecStop in the unit — docker wait alone cannot stop the child.)
|
|
"$LAUNCHER"
|
|
exec docker wait "$R9V_CONTAINER_NAME"
|