#!/usr/bin/env bash # serve-frank-primary.sh — frank-primary endpoint (R9V radiance-vLLM fork, # Qwen3.8-Flash-Next-Uncensored IQ4_XS + MTP4 drafter, 2x R9700, dankserver # :7770). # # 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 (~43 GiB pinned) and the 16 GiB PLE pinned reserve. 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 (r9v setup qwen38-mtp4-uncensored ...)" >&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 bundle: ./r9v setup qwen38-mtp4-uncensored --state-dir ..." >&2 exit 1 fi # PLE payload at its pinned byte count (stat -c %s, never wc -c), plus the # three target GGUF shards 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_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"