summaryrefslogtreecommitdiffstats
path: root/tools/guest-diag/phys_claim_cycle.sh
blob: 898aa156ede523a22a2875a5585a4a5f88c98386 (plain)
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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
#!/bin/bash
# phys_claim_cycle.sh - supervised physical-GPU claim/release cycle harness.
#
# Drives one or more full "D0 pre-lock" cycles of the physical-ownership
# backend (open-iov-vfio + gsp_phys_gpu=1 + gsp_phys_hazard_ack=1):
#
#   start persistenced -> wait GPU+root port active -> power/control=on on
#   GPU+port -> dmesg -C -> (stop persistenced && insmod) as ONE command ->
#   verify lock/claim patterns -> hold -> verify while claimed ->
#   rmmod -> verify release patterns -> restore auto + persistenced ->
#   health check.
#
# !!! HOST-FREEZE HAZARD !!!
# Claiming the GPU unbinds the distribution NVIDIA driver. On this host the
# dGPU board then power-gate and fail to come back, hard-freezing the system
# with no crash record (observed twice on 2026-09-30). Run only in a
# supervised session, with other host work stopped and the PART 87 recovery
# plan ready. The harness itself never retries an aborted cycle.
#
# Usage: phys_claim_cycle.sh --yes [--hold SECONDS] [--cycles N] [--layout]
#                             [--dryrun] [--fwsec] [--ko PATH] [--no-restore]
#
#   --yes        required: acknowledge the host-freeze hazard
#   --hold S     seconds to keep the GPU claimed (default 30)
#   --cycles N   cycles to run, each only if the previous was green (default 1)
#   --layout     also pass gsp_phys_layout=1 (rung 3b); "stage 3b complete"
#                then becomes an expected (but non-fatal) pattern
#   --dryrun     also pass gsp_phys_fwsec_dryrun=1 (implies --layout): run the
#                READ-ONLY stage-3c validation (VBIOS/BIT/FWSEC parse and the
#                staging/placement plan, no device writes); expects the
#                "gsp-phys-fwsec-dryrun:" and "stage 3c dry-run complete" lines
#   --fwsec      also pass gsp_phys_fwsec=1 (implies --layout): run the REAL
#                stage-3c write sequence (GSP falcon reset, WPR2 carve, FWSEC);
#                expects "stage 3c complete" and, on release, the WPR2-down
#                teardown line; a failed teardown keeps the locks and refuses
#                the nvidia rebind (the cycle goes red; do not retry)
#   --ko PATH    module to insmod (default kernel/open-iov-vfio.ko)
#   --no-restore do not restore power/control=auto or restart persistenced
#                at the end of each cycle (leaves the D0 hold in place)
#
# Exit codes: 0 = all cycles green; 1 = --yes missing / usage error;
#             2 = abort (hard abort signature, failed verification, red
#             in-window rail event); 3 = precondition failure.
#
# Per-cycle full dmesg is saved to
#   /tmp/opencode/phys_claim_cycle_<timestamp>_<n>.log
#
# Env overrides: GPU_BDF (default 0000:01:00.0), PORT_BDF (default: the GPU's
# upstream bridge, normally 0000:00:01.0), PHYS_CLAIM_LOGDIR.

set -u

HERE="$(dirname "$(readlink -f "$0")")"
REPO_ROOT="$(readlink -f "$HERE/../..")"

USE_YES=0
HOLD=30
CYCLES=1
LAYOUT=0
DRYRUN=0
FWSEC=0
NO_RESTORE=0
KO="$REPO_ROOT/kernel/open-iov-vfio.ko"

GPU_BDF="${GPU_BDF:-0000:01:00.0}"
if [ -z "${PORT_BDF:-}" ] && [ -e "/sys/bus/pci/devices/$GPU_BDF" ]; then
    PORT_BDF="$(basename "$(dirname "$(readlink -f "/sys/bus/pci/devices/$GPU_BDF")")")"
fi
PORT_BDF="${PORT_BDF:-0000:00:01.0}"

LOGDIR="${PHYS_CLAIM_LOGDIR:-/tmp/opencode}"
TS="$(date +%Y%m%d_%H%M%S)"
WATCHERS='pdwatch|watch_ctx|uvmt_poll|invd\.py|bpwait|sempages|track_waits|guest_pin|guest_pmap|vmrestart|push_guest_tools|nvidia-smi -l|dmesg -w'

usage() {
    sed -n '1,50p' "$0" | grep -E '^#( |$)' | sed 's/^# \{0,1\}//'
}

while [ $# -gt 0 ]; do
    case "$1" in
        --yes)        USE_YES=1 ;;
        --hold)       HOLD="${2:-}"; shift ;;
        --cycles)     CYCLES="${2:-}"; shift ;;
        --layout)     LAYOUT=1 ;;
        --dryrun)     DRYRUN=1; LAYOUT=1 ;;
        --fwsec)      FWSEC=1; LAYOUT=1 ;;
        --ko)         KO="${2:-}"; shift ;;
        --no-restore) NO_RESTORE=1 ;;
        -h|--help)    usage; exit 0 ;;
        *) echo "unknown argument: $1" >&2; usage >&2; exit 1 ;;
    esac
    shift
done

case "$HOLD" in ''|*[!0-9]*) echo "--hold must be a non-negative integer" >&2; exit 1 ;; esac
case "$CYCLES" in ''|*[!0-9]*|0) echo "--cycles must be a positive integer" >&2; exit 1 ;; esac
if [ "$DRYRUN" -eq 1 ] && [ "$FWSEC" -eq 1 ]; then
    echo "--dryrun and --fwsec are mutually exclusive" >&2
    exit 1
fi
if [ "$HOLD" -lt 30 ]; then
    echo "WARN: --hold $HOLD is below the verified 30 s minimum" >&2
fi

if [ "$USE_YES" -ne 1 ]; then
    cat >&2 <<'EOF'
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
HOST-FREEZE HAZARD - REFUSING TO RUN WITHOUT --yes

The physical claim unbinds the distribution NVIDIA driver from the dGPU.
On this host the board can then power-gate and fail to come back, hard-
freezing the system with no crash record (observed twice on 2026-09-30).
Run only when supervised, with other host work stopped and the PART 87
recovery plan ready. Re-run with --yes to acknowledge.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
EOF
    exit 1
fi

log()  { echo "[$(date +%H:%M:%S)] $*"; }
warn() { echo "[$(date +%H:%M:%S)] WARN: $*" >&2; }
pre_fail() { echo "PRECONDITION FAIL: $*" >&2; exit 3; }

INTERRUPTED=0
on_signal() {
    INTERRUPTED=1
    echo
    echo "ABORT: interrupted; leaving module/lock state as-is (no cleanup)." >&2
    exit 2
}
trap on_signal INT TERM

# ---------------------------------------------------------------- preflight

[ -c /dev/null ] || pre_fail "no /dev/null?"
if ! sudo -n true 2>/dev/null; then
    sudo -v 2>/dev/null || pre_fail "current user cannot sudo"
fi

[ -f "$KO" ] || pre_fail "module not found: $KO (build it or pass --ko)"
KO_HASH="$(sha256sum "$KO" | awk '{print $1}')"
KO_SRCVER="$(modinfo -F srcversion "$KO" 2>/dev/null || true)"
if [ -d "$REPO_ROOT/.git" ]; then
    HEAD_COMMIT="$(git -C "$REPO_ROOT" rev-parse --short HEAD 2>/dev/null || true)"
    log "preflight: repo HEAD ${HEAD_COMMIT:-unknown}"
    if git -C "$REPO_ROOT" status --porcelain -- kernel 2>/dev/null | grep -q .; then
        warn "kernel/ has uncommitted changes; HEAD does not describe $KO"
    fi
else
    HEAD_COMMIT=""
    warn "not a git checkout; cannot record HEAD"
fi
log "preflight: ko $KO sha256 $KO_HASH srcversion ${KO_SRCVER:-unknown}"
if [ "$KO" -ot "$REPO_ROOT/kernel/gsp_phys_gpu.c" ]; then
    warn "$KO is older than kernel/gsp_phys_gpu.c; rebuild before cycling"
fi

[ -e "/sys/bus/pci/devices/$GPU_BDF" ] || pre_fail "GPU $GPU_BDF not present"
[ -e "/sys/bus/pci/devices/$PORT_BDF" ] || pre_fail "root port $PORT_BDF not present"

if pgrep -f qemu-system >/dev/null 2>&1; then
    pgrep -af qemu-system >&2
    pre_fail "QEMU is running; stop the VM first"
fi

if compgen -G "/sys/bus/mdev/devices/*" >/dev/null 2>&1; then
    ls -d /sys/bus/mdev/devices/* >&2
    pre_fail "mdev devices exist; remove them first"
fi

WATCH_PIDS="$(pgrep -af "$WATCHERS" 2>/dev/null || true)"
if [ -n "$WATCH_PIDS" ]; then
    echo "$WATCH_PIDS" >&2
    pre_fail "stale watcher process(es) running; stop them first"
fi

if lsmod 2>/dev/null | awk '{print $1}' | grep -qx open_iov_vfio; then
    pre_fail "open_iov_vfio is already loaded; rmmod it first"
fi

command -v lspci >/dev/null 2>&1 || pre_fail "lspci not found"
command -v nvidia-smi >/dev/null 2>&1 || pre_fail "nvidia-smi not found"
command -v timeout >/dev/null 2>&1 || pre_fail "timeout not found"

sudo systemctl start nvidia-persistenced 2>/dev/null || true
for _ in $(seq 1 10); do
    systemctl is-active --quiet nvidia-persistenced 2>/dev/null && break
    sleep 1
done
systemctl is-active --quiet nvidia-persistenced 2>/dev/null \
    || pre_fail "nvidia-persistenced did not start"
if ! nvidia-smi -L >/dev/null 2>&1; then
    pre_fail "nvidia-smi -L failed with persistenced running"
fi
log "preflight: persistenced active, nvidia-smi OK, GPU=$GPU_BDF port=$PORT_BDF"

mkdir -p "$LOGDIR" || pre_fail "cannot create $LOGDIR"

# ---------------------------------------------------------------- helpers

save_dmesg() {
    sudo dmesg > "$1" 2>/dev/null || true
}

# Count rail-cycle lines strictly inside the locked window: after the first
# "power domain locked" line and before the first "power domain released"
# line (or the end of the log if the locks never released). Outside the
# window is normal RTD3.
in_window_rail_count() {
    local f="$1" first rel
    first="$(grep -n "power domain locked" "$f" 2>/dev/null | head -1 | cut -d: -f1)"
    [ -n "$first" ] || { echo 0; return; }
    rel="$(grep -n "power domain released" "$f" 2>/dev/null | head -1 | cut -d: -f1)"
    if [ -n "$rel" ]; then
        awk -v a="$first" -v b="$rel" \
            '/xHC error in resume|root hub lost power/ && NR > a && NR < b { n++ } END { print n+0 }' "$f"
    else
        awk -v a="$first" \
            '/xHC error in resume|root hub lost power/ && NR > a { n++ } END { print n+0 }' "$f"
    fi
}

rail_count() {
    grep -c -E "xHC error in resume|root hub lost power" "$1" 2>/dev/null || true
}

abort_signature() {
    grep -q -E "not ready .*after resume" "$1" 2>/dev/null && return 0
    grep -q -E "board unresponsive|gsp-phys.*WARN|WARNING:.*gsp-phys" "$1" 2>/dev/null && return 0
    return 1
}

wait_active() {
    local i
    for i in $(seq 1 10); do
        if [ "$(cat "/sys/bus/pci/devices/$GPU_BDF/power/runtime_status" 2>/dev/null)" = active ] &&
           [ "$(cat "/sys/bus/pci/devices/$PORT_BDF/power/runtime_status" 2>/dev/null)" = active ]; then
            return 0
        fi
        nvidia-smi -L >/dev/null 2>&1 || true
        sleep 1
    done
    return 1
}

set_power_control() {
    local bdf="$1" val="$2"
    sudo sh -c "echo '$val' > '/sys/bus/pci/devices/$bdf/power/control'" 2>/dev/null || return 1
    [ "$(cat "/sys/bus/pci/devices/$bdf/power/control" 2>/dev/null)" = "$val" ] || return 1
    return 0
}

pci_field() {
    lspci -k -s "$1" 2>/dev/null | sed -n "s/.*Kernel driver in use: //p" | head -1
}

abort_now() {
    local n="$1" dlog="$2" msg="$3"
    save_dmesg "$dlog"
    echo "ABORT: cycle $n: $msg" >&2
    echo "ABORT: state left as-is (no rmmod, no restore). Log: $dlog" >&2
    exit 2
}

# ---------------------------------------------------------------- cycles

ALL_GREEN=1
for n in $(seq 1 "$CYCLES"); do
    [ "$INTERRUPTED" -eq 0 ] || exit 2
    DLOG="$LOGDIR/phys_claim_cycle_${TS}_${n}.log"
    log "=== cycle $n/$CYCLES (hold ${HOLD}s, layout=$LAYOUT, ko=$(basename "$KO")) ==="

    sudo systemctl start nvidia-persistenced 2>/dev/null || true
    if ! wait_active; then
        pre_fail "cycle $n: GPU/port did not reach runtime_status=active"
    fi

    set_power_control "$GPU_BDF" on || pre_fail "cycle $n: cannot set GPU power/control=on"
    set_power_control "$PORT_BDF" on || pre_fail "cycle $n: cannot set port power/control=on"

    sudo dmesg -C 2>/dev/null || pre_fail "cycle $n: dmesg -C failed"

    LAYOUT_ARG=""
    [ "$LAYOUT" -eq 1 ] && LAYOUT_ARG="gsp_phys_layout=1"
    DRYRUN_ARG=""
    [ "$DRYRUN" -eq 1 ] && DRYRUN_ARG="gsp_phys_fwsec_dryrun=1"
    FWSEC_ARG=""
    [ "$FWSEC" -eq 1 ] && FWSEC_ARG="gsp_phys_fwsec=1"
    # One command: stop persistenced and insmod with the D0 pre-lock in place.
    if ! { sudo systemctl stop nvidia-persistenced && sudo insmod "$KO" \
            gsp_phys_gpu=1 gsp_phys_hazard_ack=1 $LAYOUT_ARG $DRYRUN_ARG $FWSEC_ARG; }; then
        save_dmesg "$DLOG"
        echo "ABORT: cycle $n: stop-persistenced && insmod failed" >&2
        exit 2
    fi
    sleep 1
    save_dmesg "$DLOG"

    if grep -q "gsp-phys: refusing claim" "$DLOG" 2>/dev/null; then
        echo "PRECONDITION FAIL: cycle $n: module refused the claim (GPU/port were already runtime-suspended)" >&2
        warn "no claim was taken; unloading the module is safe"
        timeout 60 sudo rmmod open_iov_vfio 2>/dev/null || true
        save_dmesg "$DLOG"
        exit 3
    fi

    if abort_signature "$DLOG"; then
        abort_now "$n" "$DLOG" "hard abort signature after insmod"
    fi

    NR_LOCKS="$(grep -c "power domain locked" "$DLOG" 2>/dev/null || true)"
    OK_GPU_LOCK=0; grep -q "power domain locked: $GPU_BDF" "$DLOG" && OK_GPU_LOCK=1
    OK_PORT_LOCK=0; grep -q "power domain locked: $PORT_BDF" "$DLOG" && OK_PORT_LOCK=1
    OK_UNBIND=0;  grep -q "unbinding nvidia from" "$DLOG" && OK_UNBIND=1
    OK_CLAIM=0;   grep -q "physical GPU claimed" "$DLOG" && OK_CLAIM=1
    OK_BOARD=0;   grep -q "board check after claim.*id=10de:1ed3" "$DLOG" && OK_BOARD=1
    OK_STAGE3B=1
    if [ "$LAYOUT" -eq 1 ]; then
        OK_STAGE3B=0; grep -q "stage 3b complete" "$DLOG" && OK_STAGE3B=1
    fi
    OK_DRYRUN=1
    if [ "$DRYRUN" -eq 1 ]; then
        OK_DRYRUN=0
        if grep -q "gsp-phys-fwsec-dryrun:" "$DLOG" && \
           grep -q "stage 3c dry-run complete" "$DLOG"; then
            OK_DRYRUN=1
        fi
    fi
    OK_FWSEC=1
    if [ "$FWSEC" -eq 1 ]; then
        OK_FWSEC=0
        grep -q "stage 3c complete" "$DLOG" && OK_FWSEC=1
    fi

    if [ "$NR_LOCKS" -eq 0 ] && [ "$OK_CLAIM" -eq 0 ]; then
        echo "ABORT: cycle $n: no locks and no claim took effect" >&2
        warn "no D0 locks were taken; unloading the module is safe"
        timeout 60 sudo rmmod open_iov_vfio 2>/dev/null || true
        save_dmesg "$DLOG"
        exit 2
    fi
    if [ "$OK_GPU_LOCK" -ne 1 ] || [ "$OK_PORT_LOCK" -ne 1 ] || \
       [ "$OK_UNBIND" -ne 1 ] || [ "$OK_CLAIM" -ne 1 ] || [ "$OK_BOARD" -ne 1 ]; then
        abort_now "$n" "$DLOG" \
            "claim patterns incomplete (locks=$NR_LOCKS gpu=$OK_GPU_LOCK port=$OK_PORT_LOCK unbind=$OK_UNBIND claim=$OK_CLAIM board=$OK_BOARD)"
    fi
    if [ "$NR_LOCKS" -ne 5 ]; then
        warn "cycle $n: expected 5 power domain locked lines, saw $NR_LOCKS"
        ALL_GREEN=0
    fi
    if [ "$LAYOUT" -eq 1 ] && [ "$OK_STAGE3B" -ne 1 ]; then
        warn "cycle $n: gsp_phys_layout=1 but no 'stage 3b complete' line"
    fi
    if [ "$DRYRUN" -eq 1 ] && [ "$OK_DRYRUN" -ne 1 ]; then
        warn "cycle $n: gsp_phys_fwsec_dryrun=1 but the read-only dry-run did not complete (missing 'gsp-phys-fwsec-dryrun:'/'stage 3c dry-run complete')"
    fi
    if [ "$FWSEC" -eq 1 ] && [ "$OK_FWSEC" -ne 1 ]; then
        warn "cycle $n: gsp_phys_fwsec=1 but 'stage 3c complete' is missing (FWSEC did not report success)"
    fi

    IN_RAIL_BEFORE="$(in_window_rail_count "$DLOG")"
    OUT_RAIL_BEFORE="$(( $(rail_count "$DLOG") - IN_RAIL_BEFORE ))"
    log "cycle $n: claimed and verified (locks=$NR_LOCKS, rail outside=$OUT_RAIL_BEFORE). Holding ${HOLD}s"
    sleep "$HOLD"

    save_dmesg "$DLOG"
    if abort_signature "$DLOG"; then
        abort_now "$n" "$DLOG" "hard abort signature during hold"
    fi

    LSPCI_NN="$(lspci -nn -s "$GPU_BDF" 2>/dev/null || true)"
    DRV="$(pci_field "$GPU_BDF")"
    RSTAT_GPU="$(cat "/sys/bus/pci/devices/$GPU_BDF/power/runtime_status" 2>/dev/null || echo '?')"
    RSTAT_PORT="$(cat "/sys/bus/pci/devices/$PORT_BDF/power/runtime_status" 2>/dev/null || echo '?')"
    HOLD_RED=0
    echo "$LSPCI_NN" | grep -q "10de:1ed3" || HOLD_RED=1
    [ "$DRV" = "open-iov-gsp-phys" ] || HOLD_RED=1
    [ "$RSTAT_GPU" = active ] && [ "$RSTAT_PORT" = active ] || HOLD_RED=1
    if [ "$HOLD_RED" -ne 0 ]; then
        abort_now "$n" "$DLOG" \
            "hold verification failed (lspci='$LSPCI_NN' driver='$DRV' gpu=$RSTAT_GPU port=$RSTAT_PORT)"
    fi

    timeout 60 sudo rmmod open_iov_vfio >/dev/null 2>&1
    RMMOD_RC=$?
    sleep 1
    save_dmesg "$DLOG"
    if [ "$RMMOD_RC" -eq 124 ]; then
        abort_now "$n" "$DLOG" "rmmod timed out after 60s"
    fi
    if [ "$RMMOD_RC" -ne 0 ]; then
        abort_now "$n" "$DLOG" "rmmod failed (rc=$RMMOD_RC)"
    fi
    if abort_signature "$DLOG"; then
        abort_now "$n" "$DLOG" "hard abort signature after rmmod"
    fi

    OK_REBIND=0;  grep -q "rebound to nvidia (rc=1)" "$DLOG" && OK_REBIND=1
    OK_BEFORE=0;  grep -q "board check before rebind.*id=10de:1ed3" "$DLOG" && OK_BEFORE=1
    OK_TEARDOWN=1
    if [ "$FWSEC" -eq 1 ]; then
        OK_TEARDOWN=0
        grep -q "teardown complete: WPR2 down" "$DLOG" && OK_TEARDOWN=1
    fi
    NR_RELEASED="$(grep -c "power domain released" "$DLOG" 2>/dev/null || true)"
    if [ "$OK_REBIND" -ne 1 ] || [ "$NR_RELEASED" -ne 5 ]; then
        ALL_GREEN=0
        warn "cycle $n: release verification failed (rebind=$OK_REBIND released=$NR_RELEASED); locks may still be held - do not retry"
        save_dmesg "$DLOG"
        break
    fi
    if [ "$OK_BEFORE" -ne 1 ]; then
        warn "cycle $n: missing 'board check before rebind id=10de:1ed3' line"
        ALL_GREEN=0
    fi
    if [ "$FWSEC" -eq 1 ] && [ "$OK_TEARDOWN" -ne 1 ]; then
        warn "cycle $n: gsp_phys_fwsec=1 but no 'teardown complete: WPR2 down' on release (WPR2 may still be up; do not retry)"
        ALL_GREEN=0
    fi

    if [ "$NO_RESTORE" -eq 0 ]; then
        set_power_control "$GPU_BDF" auto || warn "cycle $n: failed to restore GPU power/control=auto"
        set_power_control "$PORT_BDF" auto || warn "cycle $n: failed to restore port power/control=auto"
        sudo systemctl start nvidia-persistenced 2>/dev/null || true
        wait_active || warn "cycle $n: GPU/port not active after persistenced restart"
    else
        log "cycle $n: --no-restore: leaving power/control=on and persistenced stopped"
    fi

    HEALTH_RED=0
    nvidia-smi -L >/dev/null 2>&1 || HEALTH_RED=1
    [ "$(pci_field "$GPU_BDF")" = "nvidia" ] || HEALTH_RED=1
    if [ "$NO_RESTORE" -eq 0 ]; then
        [ "$(cat "/sys/bus/pci/devices/$GPU_BDF/power/control" 2>/dev/null)" = auto ] || HEALTH_RED=1
        [ "$(cat "/sys/bus/pci/devices/$PORT_BDF/power/control" 2>/dev/null)" = auto ] || HEALTH_RED=1
    fi
    [ "$(cat "/sys/bus/pci/devices/$GPU_BDF/d3cold_allowed" 2>/dev/null)" = 1 ] || HEALTH_RED=1

    IN_RAIL="$(in_window_rail_count "$DLOG")"
    OUT_RAIL="$(( $(rail_count "$DLOG") - IN_RAIL ))"
    if [ "$IN_RAIL" -gt 0 ]; then
        warn "cycle $n: RED - $IN_RAIL rail-cycle line(s) INSIDE the locked window (lock failed)"
        ALL_GREEN=0
    fi
    if [ "$OUT_RAIL" -gt 0 ]; then
        warn "cycle $n: normal RTD3 rail cycle outside the locked window ($OUT_RAIL line(s))"
    fi

    if [ "$HEALTH_RED" -ne 0 ]; then
        ALL_GREEN=0
        warn "cycle $n: health check failed (nvidia-smi/driver/control/d3cold)"
    fi
    if [ "$OK_STAGE3B" -ne 1 ]; then
        ALL_GREEN=0
    fi
    if [ "$OK_DRYRUN" -ne 1 ]; then
        ALL_GREEN=0
    fi
    if [ "$OK_FWSEC" -ne 1 ]; then
        ALL_GREEN=0
    fi

    if [ "$ALL_GREEN" -eq 1 ] && [ "$IN_RAIL" -eq 0 ] && [ "$HEALTH_RED" -eq 0 ]; then
        log "cycle $n: GREEN (locks=$NR_LOCKS, released=$NR_RELEASED, in-window rail=0, outside rail=$OUT_RAIL, hold=${HOLD}s)"
    else
        log "cycle $n: RED (locks=$NR_LOCKS, released=$NR_RELEASED, in-window rail=$IN_RAIL, outside rail=$OUT_RAIL, health_ok=$((1-HEALTH_RED)))"
        if [ "$IN_RAIL" -gt 0 ]; then
            echo "ABORT: cycle $n: rail cycle inside the locked window; not retrying" >&2
            save_dmesg "$DLOG"
            exit 2
        fi
        break
    fi
done

if [ "$ALL_GREEN" -eq 1 ]; then
    log "VERDICT: all $CYCLES cycle(s) green (exit 0)"
    exit 0
fi
echo "VERDICT: not green; do not retry in this boot (exit 2)" >&2
exit 2