Extends the #611 primitive to the estate (multi-cluster) path and adds a post-boot placement check — the foundation Phase A′ (cluster.sh) and C (c3 cluster UX) sit on. - scripts/lib/gpu-select.sh (NEW, shared lib): factors the #611 inline resolver out of launch.sh into gpu_select_indices_to_uuids / gpu_select_export, plus gpu_select_container_uuids + gpu_select_assert_placement. launch.sh sources it (no re-inlined drift) and runs the placement assertion after verify-full. - estate_cli.py: resolve_gpu_uuids() (python twin of the bash resolver) + compose_env / compose_override_doc now UUID-pin CUDA_/NVIDIA_VISIBLE_ DEVICES (ESTATE_GPUS stays index-based — the compose device_ids read the host view). assert_placement[_quiet]() runs after each instance is ready (sequential → stderr, parallel → per-instance boot log), returning the {requested, actual, placement: ok|mismatch|unknown} verdict — the shape cluster.sh status + the c3 badge will read. - Placement uses --query-compute-apps=gpu_uuid (NOT --query-gpu): under CDI the container sees all cards but RUNS on the CUDA-masked set, so compute-apps is the runtime-agnostic ground truth. - test-gpu-select (NEW): asserts the bash + python resolvers agree on real hardware and both fall back identically; test-compose-gpu-mask-passthrough updated to follow the resolver into the lib. LIVE-VERIFIED (2x3090, classic runtime): `launch.sh --gpus 1` → UUID-pinned, verify-full 8/8, "✓ placement verified", host GPU0 = 1 MiB / GPU1 = 20.8 GB. estate compose_env confirmed UUID-izing while ESTATE_GPUS stays index-based. CDI leg still awaits mog. Relevant guard sweep (11) green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
112 lines
5.2 KiB
Bash
112 lines
5.2 KiB
Bash
#!/usr/bin/env bash
|
|
# gpu-select.sh — runtime-agnostic GPU selection helpers (#610 Phase A).
|
|
#
|
|
# ONE mechanism for pinning specific GPUs on BOTH container runtimes:
|
|
# - classic nvidia runtime: honors NVIDIA_VISIBLE_DEVICES but RENUMBERS the
|
|
# exposed set inside the container (host GPUs 1,2 -> 0,1) — so an
|
|
# index-based CUDA mask points at the wrong / a nonexistent card;
|
|
# - CDI runtimes (NixOS, nvidia-ctk cdi): IGNORE NVIDIA_VISIBLE_DEVICES
|
|
# entirely (devices come from the compose's CDI device_ids) — only a
|
|
# CUDA-level mask pins cards there.
|
|
# GPU UUIDs sidestep both: the classic runtime exposes-by-UUID, and
|
|
# CUDA_VISIBLE_DEVICES masks-by-UUID regardless of exposure order.
|
|
#
|
|
# Source this file; it defines functions only (no side effects on source).
|
|
# Shared by scripts/launch.sh (--gpus) and — via the parallel python resolver
|
|
# in estate_cli.py — the estate boot path. test-gpu-select asserts the two
|
|
# resolvers agree.
|
|
|
|
# gpu_select_indices_to_uuids "<idx_csv>"
|
|
# Resolve a comma-separated list of host GPU indices to their UUIDs.
|
|
# Echoes the UUID csv on success; echoes NOTHING (empty) on any failure
|
|
# (no nvidia-smi, an index that doesn't resolve) so callers fall back to
|
|
# raw indices — the pre-UUID behavior.
|
|
gpu_select_indices_to_uuids() {
|
|
local idx_csv="$1" out="" idx uuid
|
|
[[ -z "$idx_csv" ]] && return 0
|
|
command -v nvidia-smi >/dev/null 2>&1 || return 0
|
|
local IFS=','
|
|
read -ra _gpu_sel_idx <<< "$idx_csv"
|
|
for idx in "${_gpu_sel_idx[@]}"; do
|
|
idx="${idx//[[:space:]]/}"
|
|
[[ -z "$idx" ]] && continue
|
|
uuid="$(nvidia-smi --query-gpu=uuid --format=csv,noheader -i "$idx" 2>/dev/null | head -1 | tr -d '[:space:]')"
|
|
if [[ "$uuid" != GPU-* ]]; then
|
|
# Any index that fails to resolve -> abandon the whole set (partial
|
|
# UUID pinning is worse than the index fallback).
|
|
printf '' && return 0
|
|
fi
|
|
out="${out:+${out},}${uuid}"
|
|
done
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
# gpu_select_export "<idx_csv>" ["<log_prefix>"]
|
|
# Resolve the indices to UUIDs and EXPORT both CUDA_VISIBLE_DEVICES and
|
|
# NVIDIA_VISIBLE_DEVICES (UUIDs when resolvable, raw indices otherwise).
|
|
# Emits a one-line note to stderr when UUID-pinned. The launcher's #611
|
|
# inline block, factored out.
|
|
gpu_select_export() {
|
|
local idx_csv="$1" log_prefix="${2:-gpu}" uuids
|
|
[[ -z "$idx_csv" ]] && return 0
|
|
uuids="$(gpu_select_indices_to_uuids "$idx_csv")"
|
|
if [[ -n "$uuids" ]]; then
|
|
export CUDA_VISIBLE_DEVICES="$uuids"
|
|
export NVIDIA_VISIBLE_DEVICES="$uuids"
|
|
echo "[${log_prefix}] GPU selection ${idx_csv} → UUID-pinned (runtime-agnostic: classic nvidia + CDI, #610)" >&2
|
|
else
|
|
export CUDA_VISIBLE_DEVICES="$idx_csv"
|
|
export NVIDIA_VISIBLE_DEVICES="$idx_csv"
|
|
fi
|
|
}
|
|
|
|
# gpu_select_container_uuids "<container>"
|
|
# The GPU UUIDs the container's COMPUTE PROCESSES are actually running on —
|
|
# the runtime-agnostic ground truth of placement. Uses --query-compute-apps
|
|
# (NOT --query-gpu): under CDI the container sees ALL cards via nvidia-smi,
|
|
# but only RUNS on the CUDA-masked set, so the compute-apps view is what
|
|
# reflects the real pinning. Echoes a sorted, unique UUID csv (empty if no
|
|
# compute apps yet / nvidia-smi unavailable in the container).
|
|
gpu_select_container_uuids() {
|
|
local container="$1"
|
|
[[ -z "$container" ]] && return 0
|
|
docker exec "$container" nvidia-smi \
|
|
--query-compute-apps=gpu_uuid --format=csv,noheader 2>/dev/null \
|
|
| tr -d '[:space:]' | grep '^GPU-' | sort -u | paste -sd, -
|
|
}
|
|
|
|
# gpu_select_assert_placement "<container>" "<requested_uuid_csv>" ["<log_prefix>"]
|
|
# Post-boot placement assertion (#610 Phase A): compare where the model
|
|
# ACTUALLY landed against what was requested. A loud warning on mismatch
|
|
# instead of silent wrong-card serving — the failure mode that opened #610.
|
|
# Non-fatal (returns 0): a warning, never a boot abort. Skips silently when
|
|
# the request wasn't UUID-based or placement can't be read.
|
|
gpu_select_assert_placement() {
|
|
local container="$1" requested="$2" log_prefix="${3:-gpu}" actual
|
|
[[ -z "$container" || -z "$requested" ]] && return 0
|
|
# Only meaningful when the request is UUIDs (a specific selection).
|
|
[[ "$requested" == GPU-* ]] || return 0
|
|
actual="$(gpu_select_container_uuids "$container")"
|
|
[[ -z "$actual" ]] && return 0 # no compute apps yet / can't read — don't cry wolf
|
|
# Requested must be a subset of actual (actual may list more if the app
|
|
# spans them; a mismatch is a requested UUID that isn't running).
|
|
local IFS=',' u miss=""
|
|
read -ra _gpu_req <<< "$requested"
|
|
for u in "${_gpu_req[@]}"; do
|
|
[[ -z "$u" ]] && continue
|
|
case ",${actual}," in
|
|
*",${u},"*) : ;;
|
|
*) miss="${miss:+${miss} }${u}" ;;
|
|
esac
|
|
done
|
|
if [[ -n "$miss" ]]; then
|
|
echo "[${log_prefix}] ⚠ PLACEMENT MISMATCH: requested GPU(s) not running the model — ${miss}" >&2
|
|
echo "[${log_prefix}] requested: ${requested}" >&2
|
|
echo "[${log_prefix}] actual: ${actual}" >&2
|
|
echo "[${log_prefix}] On a CDI runtime this usually means CUDA_VISIBLE_DEVICES didn't reach the container (see docs/HARDWARE.md → Pinning specific GPUs)." >&2
|
|
else
|
|
echo "[${log_prefix}] ✓ placement verified — model running on the requested GPU(s)" >&2
|
|
fi
|
|
return 0
|
|
}
|