From a601267fa550fddacacbfa47fda06ad0663935d7 Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Mon, 21 Sep 2026 14:49:21 -0700 Subject: [PATCH] feat(r49): script the voice-adapter ship with sha verification across both hops Shipping a gated adapter was ad-hoc ssh + rsync three times running. This makes it one reproducible command for the rest of the line (Faulkner, Morrison, Chandler are next). The sha verification is the point, not decoration. "Copied the adapter" is a claim with no honest form but a read-back, and a truncated or half-written adapter loads without complaint and serves a subtly different voice. The digest is taken at the source on gx10, after the local hop, and at the destination on fv-ml1; any mismatch aborts before the seat is ever told the file exists. It ships only adapter_config.json and adapter_model.safetensors. A checkpoint dir also holds optimizer state and RNG, which are large, useless to the seat, and would make the destination digest disagree with any future re-ship. It deliberately does NOT edit the compose or restart the seat. Registering an adapter is a reviewed edit to stacks/voices-seat/compose.yaml, because that file is where the gate verdict and its caveats get written down for whoever reads it next -- that is a feature of the current process, not friction to automate away. Verified against the already-shipped lv-hemingway: the live adapter on fv-ml1 is byte-identical to gx10's checkpoint-850 on both files, which confirms the file list and the convention this script encodes. --- scripts/r49-corpus/ship-voice-adapter.sh | 76 ++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100755 scripts/r49-corpus/ship-voice-adapter.sh diff --git a/scripts/r49-corpus/ship-voice-adapter.sh b/scripts/r49-corpus/ship-voice-adapter.sh new file mode 100755 index 0000000..968d421 --- /dev/null +++ b/scripts/r49-corpus/ship-voice-adapter.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Ship an r49 voice LoRA from the training box to the voices seat's adapter store. +# +# scripts/r49-corpus/ship-voice-adapter.sh +# e.g. ... ~/r49-runs/mccarthy-4b-pairs-3ep checkpoint-900 lv-mccarthy-4b-v1 +# +# Run from nh3-dev. gx10 and fv-ml1 both hold only authorized_keys, so neither can +# INITIATE to the other -- the relay through this box is the path, and it costs nothing +# because the WAN hop happens once either way. +# +# ⚠ THE SHA VERIFICATION IS THE POINT, not decoration. "copied the adapter" is a claim +# with no honest form but a read-back, and an adapter that is silently truncated or +# half-written loads without complaint and serves a subtly different voice. The digest is +# taken at the SOURCE, after the local hop, and at the DESTINATION, and any mismatch +# aborts before the seat is ever told about the file. +# +# Deliberately does NOT touch the compose file or restart the seat. Registering the +# adapter is a reviewed edit to stacks/voices-seat/compose.yaml plus deploy-stack.sh, +# because that file is where the gate verdict and its caveats are written down for +# whoever reads it next. +set -euo pipefail + +RUN=${1:?usage: ship-voice-adapter.sh } +CKPT=${2:?missing checkpoint} +NAME=${3:?missing adapter name} +SRC_HOST=${SRC_HOST:-infra-ops@10.100.50.60} # pfi-gx10 +DST_HOST=${DST_HOST:-infra-ops@10.251.50.54} # fv-ml1 +DST_ROOT=${DST_ROOT:-/tank/aimodels/voice-adapters} +STAGE=$(mktemp -d) +trap 'rm -rf "$STAGE"' EXIT + +say(){ echo "[ship $(date +%H:%M:%S)] $*"; } + +say "source $SRC_HOST:$RUN/checkpoints/$CKPT" +say "dest $DST_HOST:$DST_ROOT/$NAME" + +# The two files vLLM actually loads. A checkpoint dir also holds optimizer state and RNG +# that must NOT be shipped -- they are large, useless to the seat, and would make the +# destination digest disagree with any future re-ship. +FILES="adapter_config.json adapter_model.safetensors" + +say "digest at SOURCE" +SRC_SUMS=$(ssh -o BatchMode=yes "$SRC_HOST" "cd '$RUN/checkpoints/$CKPT' && sha256sum $FILES") +echo "$SRC_SUMS" | sed 's/^/ /' + +say "pull to local stage" +for f in $FILES; do + rsync -a "$SRC_HOST:$RUN/checkpoints/$CKPT/$f" "$STAGE/$f" +done +STAGE_SUMS=$(cd "$STAGE" && sha256sum $FILES) +if [ "$SRC_SUMS" != "$STAGE_SUMS" ]; then + say "ABORT: digest changed on the source->local hop"; echo "$STAGE_SUMS"; exit 1 +fi +say " hop 1 verified" + +say "push to dest" +ssh -o BatchMode=yes "$DST_HOST" "mkdir -p '$DST_ROOT/$NAME'" +for f in $FILES; do + rsync -a "$STAGE/$f" "$DST_HOST:$DST_ROOT/$NAME/$f" +done +DST_SUMS=$(ssh -o BatchMode=yes "$DST_HOST" "cd '$DST_ROOT/$NAME' && sha256sum $FILES") +if [ "$SRC_SUMS" != "$DST_SUMS" ]; then + say "ABORT: digest differs at the destination"; echo "$DST_SUMS"; exit 1 +fi +say " hop 2 verified -- byte-identical to the checkpoint on gx10" + +say "destination listing" +ssh -o BatchMode=yes "$DST_HOST" "ls -la '$DST_ROOT/$NAME'" | sed 's/^/ /' +cat <