# ana-ml2 — OPEN the ERP/RP tune window: clear GPU0 completely. # # stop mog-sec (GPU1) -> move gen GPU0 -> GPU1 -> GPU0 empty for training # # Operator call 2026-08-24: rather than train beside `gen`, move `gen` off GPU0 # entirely and stand `sec` down for the night. Training then gets a whole card # (95.60 GiB) instead of a shared one, and the fleet's general seat never goes # dark beyond its own restart. # # ⚠⚠ ORDER IS LOAD-BEARING — DO NOT REORDER THE STEPS. # `gen` runs at --gpu-memory-utilization 0.43, which vLLM reads as a fraction of # TOTAL card memory: 0.43 x 97,887 MiB = 42,091 MiB that must be FREE at startup # or the engine refuses to boot. GPU1 has only 19,446 MiB free while mog-sec is # up. Recreating `gen` onto GPU1 first would take the fleet's main seat down and # leave it down. mog-sec stops FIRST, and step 3 hard-gates on the freed memory # before `gen` is touched at all. # # ⚠ `stop`, never `down`. `down` removes the container; `stop` leaves it in # place so the close playbook can `start` it. Both seats are `restart: # unless-stopped`, which does NOT resurrect a deliberately-stopped container. # # ⚠ device_ids vs nvidia-smi ordering was VERIFIED on this host, not assumed: # gen (GEN_GPU_ID=0) reports under the GPU nvidia-smi indexes 0, mog-sec # (MOG_GPU_ID=1) under index 1. They agree here. (They do NOT on irv-ml1 — # never carry that assumption between boxes.) # # Restore with: playbooks/ana-ml2-training-window-close.yaml # # scripts/elway ana-ml2 --playbook playbooks/ana-ml2-training-window-open.yaml vars: gen_dir: /opt/docker/compose/gen-seat mog_dir: /opt/docker/compose/mog-sec gen_port: "8015" # gen's startup requirement: 0.43 x 97,887 MiB, rounded up. If GPU1 has less # than this free, gen will not boot and the window must not proceed. gen_required_free_mib: "42100" steps: - name: "PREFLIGHT — GPU0 holds vllm-gen and nothing else unexpected" sudo: true shell: | set -e procs=$(nvidia-smi --query-compute-apps=gpu_uuid,pid --format=csv,noheader | wc -l) gpu0_uuid=$(nvidia-smi --query-gpu=uuid --format=csv,noheader -i 0) gpu0_procs=$(nvidia-smi --query-compute-apps=gpu_uuid,pid --format=csv,noheader | grep -c "$gpu0_uuid" || true) echo "GPU0 compute procs: $gpu0_procs (total on box: $procs)" test "$gpu0_procs" -le 1 changed_when: "false" - name: "PREFLIGHT — record gen's current container id (proves the recreate later)" sudo: true shell: docker inspect vllm-gen --format '{{.Id}}' | tee /tmp/gen-container-id-before.txt changed_when: "false" - name: "Stop mog-sec (the `sec` / `sec-reasoning` seat) — frees ~55.3 GiB on GPU1" sudo: true shell: docker compose --project-directory {{ mog_dir }} stop vllm-mog-sec # Skip if already stopped, so the playbook is rerunnable. when: "docker inspect -f '{{.State.Running}}' vllm-mog-sec 2>/dev/null | grep -q true" - name: "Wait for GPU1 memory to actually release (teardown is not instant)" sudo: true shell: | for i in $(seq 1 60); do free=$(nvidia-smi --query-gpu=memory.free --format=csv,noheader,nounits -i 1) if [ "$free" -ge {{ gen_required_free_mib }} ]; then echo "GPU1 free: ${free} MiB"; exit 0 fi sleep 2 done echo "TIMEOUT: GPU1 free is ${free} MiB, need >= {{ gen_required_free_mib }}"; exit 1 changed_when: "false" - name: "HARD GATE — GPU1 has room for gen's 0.43 budget before we touch gen" sudo: true shell: | free=$(nvidia-smi --query-gpu=memory.free --format=csv,noheader,nounits -i 1) echo "GPU1 free ${free} MiB vs required {{ gen_required_free_mib }} MiB" test "$free" -ge {{ gen_required_free_mib }} changed_when: "false" - name: "Point gen at GPU1 in its .env (GEN_GPU_ID 0 -> 1)" sudo: true # ⚠ `sudo` INSIDE the when: expression. A step's `sudo: true` covers the # shell, NOT its when/creates/changed_when guards — those run as the login # user. The .env is root-only 0600, so an unsudo'd grep exits 2 # (permission denied), which is not 0, so the step SILENTLY SKIPS and the # flip never happens. Caught 2026-08-24 by the effective-value assert below. shell: sed -i 's/^GEN_GPU_ID=0$/GEN_GPU_ID=1/' {{ gen_dir }}/.env when: "sudo grep -qx 'GEN_GPU_ID=0' {{ gen_dir }}/.env" - name: "Assert the EFFECTIVE device id, not the .env line" sudo: true # grep on the .env proves a substring is present; only `compose config` # proves what the container will actually be created with. # ⚠ Parse the JSON, do not regex the YAML. The first version of this grepped # for -\s*'?1'? and failed against compose's DOUBLE-quoted `- "1"` — an # assert that fails for the wrong reason is worse than no assert. shell: | docker compose --project-directory {{ gen_dir }} config --format json \ | jq -e '.services["vllm-gen"].deploy.resources.reservations.devices[0].device_ids == ["1"]' changed_when: "false" - name: "Recreate gen onto GPU1 (a device change needs up -d, not restart)" sudo: true shell: docker compose --project-directory {{ gen_dir }} up -d vllm-gen - name: "Wait for gen to serve /health (cold start: weights + CUDA graphs + MTP)" sudo: true shell: | for i in $(seq 1 180); do if curl -sf -o /dev/null http://127.0.0.1:{{ gen_port }}/health; then echo "gen healthy after $((i*5))s"; exit 0 fi sleep 5 done echo "TIMEOUT: gen did not become healthy in 900s"; exit 1 changed_when: "false" verify: - name: "gen was genuinely RECREATED (container id changed)" sudo: true shell: | before=$(cat /tmp/gen-container-id-before.txt) after=$(docker inspect vllm-gen --format '{{.Id}}') echo "before=${before:0:12} after=${after:0:12}" test "$before" != "$after" changed_when: "false" - name: "gen's process is resident on GPU1" sudo: true # ⚠ Match by CGROUP, not by `.State.Pid`. vLLM V1 runs EngineCore as a CHILD # of the container's pid 1, and it is the child that holds the GPU memory — # nvidia-smi never reports `.State.Pid`, so comparing against it always fails. shell: | cid=$(docker inspect vllm-gen --format '{{.Id}}') gpu1_uuid=$(nvidia-smi --query-gpu=uuid --format=csv,noheader -i 1) found=0 for p in $(nvidia-smi --query-compute-apps=gpu_uuid,pid,used_memory --format=csv,noheader \ | grep "$gpu1_uuid" | cut -d, -f2 | tr -d ' '); do if grep -q "$cid" /proc/$p/cgroup 2>/dev/null; then echo "gen pid $p resident on GPU1: $(nvidia-smi --query-compute-apps=pid,used_memory --format=csv,noheader | grep "^$p,")" found=1 fi done test "$found" -eq 1 changed_when: "false" - name: "gen answers a real completion, not just /health" sudo: true shell: | . {{ gen_dir }}/.env curl -sf -m 120 http://127.0.0.1:{{ gen_port }}/v1/chat/completions \ -H "Authorization: Bearer ${API_KEY}" -H 'Content-Type: application/json' \ -d '{"model":"'"${GEN_SERVED_NAME}"'","messages":[{"role":"user","content":"reply with the single word: ok"}],"max_tokens":16}' \ | grep -q '"content"' changed_when: "false" - name: "GPU0 IS EMPTY — zero compute processes" sudo: true shell: | gpu0_uuid=$(nvidia-smi --query-gpu=uuid --format=csv,noheader -i 0) n=$(nvidia-smi --query-compute-apps=gpu_uuid,pid --format=csv,noheader | grep -c "$gpu0_uuid" || true) free=$(nvidia-smi --query-gpu=memory.free --format=csv,noheader,nounits -i 0) echo "GPU0 compute procs=${n} free=${free} MiB" test "$n" -eq 0 && test "$free" -ge 95000 changed_when: "false" - name: "mog-sec is stopped (not removed — close depends on `start` working)" sudo: true shell: | docker inspect -f '{{.State.Status}}' vllm-mog-sec | tee /dev/stderr | grep -qx exited changed_when: "false" - name: "GPU1 still has headroom for Scriberr's on-demand load" sudo: true shell: | free=$(nvidia-smi --query-gpu=memory.free --format=csv,noheader,nounits -i 1) echo "GPU1 free after gen landed: ${free} MiB" test "$free" -ge 12000 changed_when: "false"