Per maintainer call: "cluster" conventionally means multiple networked machines (a non-goal here — LiteLLM fronts multi-host) and is best reserved for a future enterprise/multi-node meaning. "Pod" is the accurate analogy for what this is — one model on a GPU subset on ONE host (k8s/RunPod sense). The capability is unchanged; only the name. Scoped rename (cluster→pod, case-aware) across the pod feature ONLY: - scripts/cluster.sh → scripts/pod.sh; test-cluster-cli.sh → test-pod-cli.sh; docs/CLUSTERS.md → docs/PODS.md - estate_cli.py verbs + wording; app.py (ClusterCreateScreen→PodCreateScreen, action_new_cluster→new_pod, _populate_clusters→_populate_pods, #cluster-view →#pod-view, the [N] help/empty-state text); services.py cluster_create_plan →pod_create_plan; data.py kind cluster_create→pod_create; tests + doc pointers (HARDWARE/MULTI_CARD/README/c3-README) - UNTOUCHED (unrelated "cluster"): compat.py + test-profiles-compat.sh (the VRAM-topology classifier), services.py:1863 / test_services.py (the scene- table "cluster by group" verb), older docs, .venv Also folds in the [N] discoverability fix (n was already bound to serving_switch — moved to N; empty-estate now shows a "no pods — [N] new pod" affordance + a help entry) and a heterogeneous-rig section in PODS.md: one homogeneous pod per card family (2×3090 · GB10 · 6000 Pro) is the clean pattern — mixing families in one TP pod makes NCCL wait on the slowest + wastes VRAM; a worked 2-pod lifecycle walkthrough. Verified: test-pod-cli + estate/gpu/profiles guards green; 25 c3 pod/binding + 239 fast tests green; pod.sh live create/list/D1-reject on 2×3090; zero stray "cluster" in pod files (scene-verb preserved); no CLUSTERS.md links left; PODS.md leak-clean. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EfF565T9eSLaqGzidyJ1Pm
73 lines
3.1 KiB
Bash
Executable File
73 lines
3.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# test-pod-cli.sh — scripts/pod.sh + the estate_cli pod verbs
|
||
# (#610 Phase A′). Runs hardware-free via CLUB3090_FAKE_GPUS (kv-calc prices
|
||
# by card NAME, no real GPU needed) so CI exercises the full lifecycle:
|
||
# create (with D1 fit) · count!=TP hard-reject · GPU-collision reject ·
|
||
# list --json · status · rm · the index-based estate file.
|
||
set -euo pipefail
|
||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||
cd "$ROOT"
|
||
|
||
# Two fake RTX 3090s so a TP=2 slug fits and a TP=1 slug leaves GPU 0 free.
|
||
export CLUB3090_FAKE_GPUS='0:NVIDIA GeForce RTX 3090:24576:8.6,1:NVIDIA GeForce RTX 3090:24576:8.6'
|
||
TMP="$(mktemp -d)"
|
||
trap 'rm -rf "$TMP"' EXIT
|
||
EST="$TMP/estate.yml"
|
||
CL=(bash scripts/pod.sh)
|
||
|
||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||
|
||
# 1. create a TP=1 pod on GPU 1 — must fit + write the file.
|
||
"${CL[@]}" create chat --gpus 1 --slug vllm/minimal --file "$EST" >/dev/null 2>&1 \
|
||
|| fail "create chat (TP=1 on GPU 1) should succeed"
|
||
[[ -f "$EST" ]] || fail "estate file not written"
|
||
grep -q 'name: chat' "$EST" || fail "chat not in estate file"
|
||
# Indices stay index-based in the file (UUIDs resolved at boot, #610 Phase A).
|
||
grep -qE '^\s*-\s*1\s*$' "$EST" || fail "estate file should store index-based gpus"
|
||
|
||
# 2. D1: count != compose TP -> hard reject (vllm/dual is TP=2).
|
||
if "${CL[@]}" create bad --gpus 1 --slug vllm/dual --file "$EST" >/dev/null 2>&1; then
|
||
fail "create with 1 GPU for a TP=2 slug must be rejected (D1)"
|
||
fi
|
||
err="$("${CL[@]}" create bad --gpus 1 --slug vllm/dual --file "$EST" 2>&1 || true)"
|
||
grep -q 'TP=2' <<<"$err" || fail "D1 rejection should name the TP mismatch (got: $err)"
|
||
|
||
# 3. GPU collision -> validate_estate reject (GPU 1 already claimed by chat).
|
||
if "${CL[@]}" create chat2 --gpus 1 --slug vllm/minimal --file "$EST" >/dev/null 2>&1; then
|
||
fail "second pod on the same GPU must be rejected"
|
||
fi
|
||
|
||
# 4. list --json: one pod, GPU 0 free.
|
||
json="$("${CL[@]}" list --json --file "$EST" 2>/dev/null)"
|
||
python3 - "$json" <<'PY' || exit 1
|
||
import json, sys
|
||
d = json.loads(sys.argv[1])
|
||
assert len(d["pods"]) == 1, d
|
||
assert d["pods"][0]["name"] == "chat", d
|
||
assert d["free_gpus"] == [0], d
|
||
assert d["claimed_gpus"] == [1], d
|
||
print(" list --json ok")
|
||
PY
|
||
|
||
# 5. status (nothing running) -> down, placement unknown, valid --json shape.
|
||
sjson="$("${CL[@]}" status --json --file "$EST" 2>/dev/null)"
|
||
python3 - "$sjson" <<'PY' || exit 1
|
||
import json, sys
|
||
d = json.loads(sys.argv[1])
|
||
c = d["pods"][0]
|
||
assert c["running"] is False, c
|
||
assert c["placement"]["placement"] == "unknown", c
|
||
assert set(c["placement"]) == {"requested", "actual", "placement"}, c
|
||
print(" status --json ok")
|
||
PY
|
||
|
||
# 6. a second, non-colliding pod on GPU 0 succeeds.
|
||
"${CL[@]}" create chat0 --gpus 0 --slug vllm/minimal --file "$EST" >/dev/null 2>&1 \
|
||
|| fail "create chat0 on the free GPU 0 should succeed"
|
||
|
||
# 7. rm removes it from the plan.
|
||
"${CL[@]}" rm chat0 --file "$EST" >/dev/null 2>&1 || fail "rm chat0 should succeed"
|
||
grep -q 'name: chat0' "$EST" && fail "chat0 still in estate file after rm"
|
||
|
||
echo "PASS: pod.sh create/list/status/rm + D1 count!=TP + GPU-collision rejects (fake-GPU)"
|