File size: 13,201 Bytes
0693474 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | #!/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 & # stdin /dev/null: a background group reading a tty stops (SIGTTIN)
cpid=$!
set +m
stop_cmd() {
kill -TERM -- "-$cpid" 2>/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
|