#!/usr/bin/env bash # pair_window.sh: coordinator PAIR WINDOW on x86host's NVLink pair (GPU-c4e7a371 + GPU-7744b44b), per the GPU ownership # protocol in documents/plans/2026-10-23-dual3090-plan.md. Run ON x86host; open, hand, hold and close are coordinator-only. # pair_window.sh open OWNER take both GPU locks (waits for the lane holding 7744b44b to finish its step), stop # the one-card production worker, write ~/dsv41-offload/PAIR_OK naming OWNER. The # coordinator's hold (tmux session dsv41-pairhold) keeps both locks for the WHOLE # window: OWNER runs its GPU commands with `run`, never with its own flock (a released # hold goes to the lanes queued in `flock -w`, not to a polling owner: window 5). A # third argument ("hold", the old form) is accepted and changes nothing. # pair_window.sh hand OWNER re-name the window's owner; the hold stays (no gap a queued lane could take) # pair_window.sh run OWNER -- CMD OWNER (PAIR_OK must name it; COORD for the coordinator) runs CMD while the hold # keeps both locks. One run at a time; CMD gets DSV41_PAIR_WINDOW=OWNER; CMD is stopped # (SIGTERM to its process group, SIGKILL 30 s later) when the hold disappears or # PAIR_OK stops naming OWNER; exits with CMD's status # pair_window.sh hold re-take both locks for the coordinator inside an open window (PAIR_OK -> COORD) # pair_window.sh close in this order (S12 R3): remove PAIR_OK (no run can start or continue); wait until # no run is active (at most PAIR_CLOSE_WAIT_S, default 60 s: a run stops its command # within ~31 s once PAIR_OK is gone); stop lane E's TP2 unit if active; WITH THE HOLD # STILL HELD require that no compute process runs on either card (else refuse and # keep the hold: an escaped job, e.g. Popen(start_new_session=True), is still there); # only then drop the hold, start the one-card worker and wait for its ready line. # After a refusal: stop what it names, run close again (or `open COORD` to go on) # pair_window.sh status PAIR_OK, holder, the active run (or an orphan holding the run lock), lanes queued on # the locks, units, both cards # hand and hold refuse while a run is active. A child that leaves the run's process group but keeps its fd 9 (the run # lock) is not stopped by run: close refuses while it lives and status names it; the coordinator stops it by hand. The Spark engine runs target-only rounds while no worker answers and # reconnects by itself (backoff cap 5 s). # BEFORE PROMOTION ONLY: open, hand, run, hold and close refuse while the TP2 production unit # (dsv41-dspark-worker-tp2-prod) is enabled or running, because GPU-7744b44b is then production rank 1 and the unit # itself holds both locks; windows after promotion go through offload/launch/prod_window.sh (S12 F1). set -euo pipefail D=$HOME/dsv41-offload; L=$D/locks; PAIR_OK=$D/PAIR_OK; MARK=$L/.pairhold-acquired LOCKS=("$L/gpu-c4e7a371.lock" "$L/gpu-7744b44b.lock") RUNLOCK=$L/.pairrun.lock; RUNINFO=$L/.pairrun ONE=dsv41-dspark-worker.service; TP2=dsv41-dspark-worker-tp2.service; PROD_TP2=dsv41-dspark-worker-tp2-prod.service ONE_LOG=$D/prod/logs/dspark-worker.log CLOSE_WAIT_S=${PAIR_CLOSE_WAIT_S:-60} log() { echo "$(date -u +%FT%TZ) pair_window: $*"; } refuse() { echo "pair_window: refusing $*" >&2; exit 1; } not_promoted() { # refuse before touching anything once the TP2 production unit owns the pair local en=no st systemctl --user is-enabled --quiet $PROD_TP2 2>/dev/null && en=yes st=$(systemctl --user is-active $PROD_TP2 2>/dev/null || true) case "$en/$st" in yes/*|*/active|*/activating|*/deactivating|*/reloading|*/refreshing) echo "pair_window: refusing '$1': $PROD_TP2 is enabled ($en) / $st: GPU-7744b44b is production rank 1 and that" \ "unit holds both GPU locks; use offload/launch/prod_window.sh (training and pair windows after promotion," \ "documents/plans/2026-10-23-dual3090-plan.md)" >&2 exit 1 ;; esac } take_hold() { tmux has-session -t dsv41-pairhold 2>/dev/null && { log "hold already taken"; return; } rm -f "$MARK" tmux new-session -d -s dsv41-pairhold \ "flock $L/gpu-c4e7a371.lock flock $L/gpu-7744b44b.lock sh -c 'date -u > $MARK; exec sleep infinity'" log "waiting for both GPU locks (a lane step may hold 7744b44b for up to 15 min)" for _ in $(seq 1 1800); do [ -f "$MARK" ] && { log "both locks held by dsv41-pairhold"; return; }; sleep 1; done tmux kill-session -t dsv41-pairhold; echo "pair_window: locks not acquired in 30 min" >&2; exit 1 } drop_hold() { tmux kill-session -t dsv41-pairhold 2>/dev/null && log "hold released" || true rm -f "$MARK" } hold_alive() { # the hold's session runs AND both locks are actually held local l tmux has-session -t dsv41-pairhold 2>/dev/null && [ -f "$MARK" ] || return 1 for l in "${LOCKS[@]}"; do flock -n "$l" true && return 1; done return 0 } run_active() { [ -e "$RUNLOCK" ] && ! flock -n "$RUNLOCK" true; } run_desc() { # the active run, or what holds the run lock when its run process is gone (an orphan that kept fd 9) local h if [ -s "$RUNINFO" ]; then cat "$RUNINFO"; return; fi h=$(lock_holders "$RUNLOCK") echo "active, holder unknown: no run process; pid(s) holding the run lock: ${h:-none found} (an orphan that kept fd 9)" } no_run() { run_active && refuse "'$1': a run is active ($(run_desc)); wait for it"; return 0; } # Waiters come from the kernel ("->" lines for the lock file's dev:inode in /proc/locks), never from lslocks (S12 R1: # util-linux 2.39 on x86host misreports flock holders) lock_key() { # dev:inode as /proc/locks prints it (major:minor in hex, the kernel's encoding of st_dev) local d i d=$(stat -c %d "$1" 2>/dev/null) && i=$(stat -c %i "$1") || return 1 printf '%02x:%02x:%s' $(( (d >> 8) & 0xfff )) $(( (d & 0xff) | ((d >> 12) & 0xfff00) )) "$i" } lock_holders() { # lock_holders PATH: pids with their own fd on PATH carrying a "lock:" line for its dev:inode local key f pid key=$(lock_key "$1") || return 0 find /proc/[0-9]*/fd -maxdepth 1 -lname "$1" 2>/dev/null | while read -r f; do pid=${f#/proc/}; pid=${pid%%/*} grep -q "^lock:.* $key " "/proc/$pid/fdinfo/${f##*/}" 2>/dev/null && echo "$pid" done | sort -un | tr '\n' ' ' | sed 's/ $//' } waiters() { # "pid(path)" for each process queued in flock on a pair lock local l key p for l in "${LOCKS[@]}"; do key=$(lock_key "$l") || continue for p in $(awk -v k="$key" '$2 == "->" && $3 == "FLOCK" && $7 == k {print $6}' /proc/locks | sort -un); do printf '%s(%s) ' "$p" "$l" done done | sed 's/ $//' } cards() { nvidia-smi --query-gpu=uuid,memory.used,power.draw,power.limit,clocks.sm,temperature.gpu --format=csv,noheader \ | grep -E "c4e7a371|7744b44b" nvidia-smi --query-compute-apps=gpu_uuid,pid,process_name,used_memory --format=csv,noheader | grep -E "c4e7a371|7744b44b" || true } case "${1:-}" in open|hand|run|hold|close) not_promoted "$1" ;; esac case "${1:-}" in open) owner=${2:?owner (COORD or a lane name)} [ -e "$PAIR_OK" ] && { echo "pair_window: already open: $(cat "$PAIR_OK")" >&2; exit 1; } take_hold systemctl --user stop $ONE; log "one-card worker stopped" printf '%s\n' "$owner" "opened $(date -u +%FT%TZ) by the coordinator (pair_window.sh)" > "$PAIR_OK" log "PAIR_OK -> $owner; the hold keeps both locks: $owner runs GPU commands with pair_window.sh run $owner -- CMD" cards ;; hand) owner=${2:?owner}; [ -e "$PAIR_OK" ] || { echo "pair_window: not open" >&2; exit 1; } no_run hand systemctl --user is-active --quiet $TP2 && { echo "pair_window: stop the TP2 unit first (stop_tp2.sh)" >&2; exit 1; } take_hold # normally a no-op: the hold stays for the whole window printf '%s\n' "$owner" "handed $(date -u +%FT%TZ) by the coordinator (pair_window.sh)" > "$PAIR_OK" log "PAIR_OK -> $owner (the hold stays; $owner runs GPU commands with pair_window.sh run $owner -- CMD)" ;; run) owner=${2:?owner}; shift 2; [ "${1:-}" = -- ] && shift [ "$#" -gt 0 ] || { echo "usage: pair_window.sh run OWNER -- CMD [ARG...]" >&2; exit 2; } [ -e "$PAIR_OK" ] || refuse "run: no pair window is open" named=$(head -1 "$PAIR_OK") [ "$named" = "$owner" ] || refuse "run: PAIR_OK names $named, not $owner" hold_alive || refuse "run: the coordinator's hold does not hold both locks (pair_window.sh hold)" if [ "$owner" != COORD ] && systemctl --user is-active --quiet $TP2; then refuse "run: the TP2 unit runs on the pair (stop_tp2.sh first)" fi exec 9>>"$RUNLOCK" # the run lock; CMD inherits it, so it lasts as long as CMD does flock -n 9 || refuse "run: another run is active ($(run_desc))" # again under the run lock: a close that removed PAIR_OK meanwhile waits for this lock, so it must see us refuse [ "$(head -1 "$PAIR_OK" 2>/dev/null)" = "$owner" ] || refuse "run: PAIR_OK no longer names $owner (the window is closing)" hold_alive || refuse "run: the coordinator's hold does not hold both locks (pair_window.sh hold)" printf '%s\n' "$owner, pid $$, since $(date -u +%FT%TZ): $*" > "$RUNINFO" log "run for $owner under the hold: $*" set -m # CMD in its own process group, so a stop reaches its children DSV41_PAIR_WINDOW=$owner "$@" /dev/null || true for _ in $(seq 1 30); do kill -0 "$cpid" 2>/dev/null || break; sleep 1; done kill -KILL -- "-$cpid" 2>/dev/null || true wait "$cpid" 2>/dev/null || true rm -f "$RUNINFO" } trap 'echo "pair_window: run for $owner interrupted: stopping the command" >&2; stop_cmd; exit 130' INT TERM HUP while kill -0 "$cpid" 2>/dev/null; do if ! hold_alive || [ "$(head -1 "$PAIR_OK" 2>/dev/null)" != "$owner" ]; then echo "pair_window: run for $owner: the hold is gone or PAIR_OK no longer names $owner: stopping the command" >&2 stop_cmd; exit 1 fi sleep 1 done rc=0; wait "$cpid" || rc=$? rm -f "$RUNINFO" log "run for $owner ended (rc $rc)" exit "$rc" ;; hold) [ -e "$PAIR_OK" ] || { echo "pair_window: not open" >&2; exit 1; } no_run hold take_hold printf '%s\n' "COORD" "held $(date -u +%FT%TZ) by the coordinator (pair_window.sh)" > "$PAIR_OK" ;; close) rm -f "$PAIR_OK"; log "PAIR_OK removed: no run can start, a running one stops its command" t0=$SECONDS while run_active; do [ $((SECONDS - t0)) -lt "$CLOSE_WAIT_S" ] || refuse "close: a run is still active after $CLOSE_WAIT_S s" \ "($(run_desc)); the hold stays: stop it, then run close again (or open COORD to go on)" sleep 1 done if systemctl --user is-active --quiet $TP2; then systemctl --user stop $TP2; log "TP2 unit stopped"; fi hold_alive || log "WARNING: the hold does not hold both locks (a lane may already be on 7744b44b)" apps=$(nvidia-smi --query-compute-apps=gpu_uuid,pid,process_name,used_memory --format=csv,noheader) \ || refuse "close: nvidia-smi cannot list compute processes; the hold stays" apps=$(grep -E "c4e7a371|7744b44b" <<<"$apps" || true) [ -z "$apps" ] || refuse "close: compute processes still on the pair: $(tr '\n' ';' <<<"$apps")" \ "the hold stays: stop them (an escaped job of the window?), then run close again (or open COORD to go on)" drop_hold before=$(grep -c "worker ready in" "$ONE_LOG" 2>/dev/null || true) systemctl --user start $ONE for _ in $(seq 1 240); do [ "$(grep -c 'worker ready in' "$ONE_LOG" 2>/dev/null || true)" -gt "${before:-0}" ] && break systemctl --user is-active --quiet $ONE || { echo "pair_window: one-card worker died" >&2; tail -20 "$ONE_LOG" >&2; exit 1; } sleep 1 done grep "worker ready in" "$ONE_LOG" | tail -1; cards ;; status) echo "PAIR_OK: $( [ -e "$PAIR_OK" ] && tr '\n' ' ' < "$PAIR_OK" || echo absent)" echo "hold: $(tmux has-session -t dsv41-pairhold 2>/dev/null && cat "$MARK" 2>/dev/null || echo none)" echo "run: $(run_active && run_desc || echo none)" echo "queued on the locks: $(w=$(waiters); echo "${w:-none}")" echo "$ONE: $(systemctl --user is-active $ONE || true) | $TP2: $(systemctl --user is-active $TP2 || true)" \ "| $PROD_TP2: $(systemctl --user is-active $PROD_TP2 2>/dev/null || true)," \ "$(systemctl --user is-enabled $PROD_TP2 2>/dev/null || true)" cards ;; *) sed -n 2,30p "$0"; exit 2 ;; esac