9e8f1bbeed
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
Lint PowerShell / powershell-lint (windows-latest) (push) Has been cancelled
Matrix Smoke / macos-smoke (push) Has been cancelled
242 lines
8.7 KiB
Bash
Executable File
242 lines
8.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# ODS Hardware Classifier — Two-pass GPU matching
|
|
# Pass 1: Match known_gpus by device_id then name_patterns (gpu-database.json)
|
|
# Pass 2: Fall back to heuristic_classes (threshold-based, same as old hardware-classes.json)
|
|
#
|
|
# Accepts both old args (--platform-id, --gpu-vendor) and new args (--device-id, --gpu-name, --ram-mb)
|
|
# Output contract: HW_CLASS_ID, HW_CLASS_LABEL, HW_REC_BACKEND, HW_REC_TIER,
|
|
# HW_REC_COMPOSE_OVERLAYS, HW_BANDWIDTH_GBPS, HW_MEMORY_SOURCE, HW_GPU_LABEL
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
GPU_DB="${ROOT_DIR}/config/gpu-database.json"
|
|
ENV_MODE="false"
|
|
PLATFORM_ID="${PLATFORM_ID:-unknown}"
|
|
GPU_VENDOR="${GPU_VENDOR:-unknown}"
|
|
MEMORY_TYPE="${MEMORY_TYPE:-unknown}"
|
|
VRAM_MB="${VRAM_MB:-0}"
|
|
DEVICE_ID=""
|
|
GPU_NAME=""
|
|
CPU_NAME=""
|
|
RAM_MB="0"
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--platform-id) PLATFORM_ID="${2:-$PLATFORM_ID}"; shift 2 ;;
|
|
--gpu-vendor) GPU_VENDOR="${2:-$GPU_VENDOR}"; shift 2 ;;
|
|
--memory-type) MEMORY_TYPE="${2:-$MEMORY_TYPE}"; shift 2 ;;
|
|
--vram-mb) VRAM_MB="${2:-$VRAM_MB}"; shift 2 ;;
|
|
--device-id) DEVICE_ID="${2:-}"; shift 2 ;;
|
|
--gpu-name) GPU_NAME="${2:-}"; shift 2 ;;
|
|
--cpu-name) CPU_NAME="${2:-}"; shift 2 ;;
|
|
--ram-mb) RAM_MB="${2:-0}"; shift 2 ;;
|
|
--env) ENV_MODE="true"; shift ;;
|
|
--db) GPU_DB="${2:-$GPU_DB}"; shift 2 ;;
|
|
*)
|
|
echo "Unknown argument: $1" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ ! -f "$GPU_DB" ]]; then
|
|
echo "ERROR: GPU database not found: $GPU_DB" >&2
|
|
exit 1
|
|
fi
|
|
|
|
PYTHON_CMD="python3"
|
|
if [[ -f "$ROOT_DIR/lib/python-cmd.sh" ]]; then
|
|
. "$ROOT_DIR/lib/python-cmd.sh"
|
|
PYTHON_CMD="$(ods_detect_python_cmd)"
|
|
elif command -v python >/dev/null 2>&1; then
|
|
PYTHON_CMD="python"
|
|
fi
|
|
|
|
"$PYTHON_CMD" - "$GPU_DB" "$ENV_MODE" "$PLATFORM_ID" "$GPU_VENDOR" "$MEMORY_TYPE" "$VRAM_MB" "$DEVICE_ID" "$GPU_NAME" "$CPU_NAME" "$RAM_MB" <<'PY'
|
|
import json
|
|
import sys
|
|
|
|
db_path = sys.argv[1]
|
|
env_mode = sys.argv[2] == "true"
|
|
platform_id = sys.argv[3]
|
|
gpu_vendor = sys.argv[4]
|
|
memory_type = sys.argv[5]
|
|
vram_mb = int(float(sys.argv[6] or 0))
|
|
device_id = sys.argv[7]
|
|
gpu_name = sys.argv[8]
|
|
cpu_name = sys.argv[9]
|
|
ram_mb = int(float(sys.argv[10] or 0))
|
|
|
|
with open(db_path, "r", encoding="utf-8") as f:
|
|
db = json.load(f)
|
|
|
|
# --- Compose overlay mapping (backend → default overlays) ---
|
|
# CPU backend uses cpu overlay: CPU-only llama.cpp image, no GPU reservation
|
|
OVERLAY_MAP = {
|
|
"amd": ["docker-compose.base.yml", "docker-compose.amd.yml"],
|
|
"nvidia": ["docker-compose.base.yml", "docker-compose.nvidia.yml"],
|
|
"apple": ["docker-compose.base.yml", "docker-compose.apple.yml"],
|
|
"cpu": ["docker-compose.base.yml", "docker-compose.cpu.yml"],
|
|
}
|
|
|
|
# --- Pass 1: Match known_gpus by device_id then name_patterns ---
|
|
selected = None
|
|
best_name_len = 0 # longest matching pattern wins (prevents "XT" matching "XTX")
|
|
best_id_vram_diff = None # closest VRAM wins for device_id-only fallback
|
|
combined_name = f"{gpu_name} {cpu_name}".strip().lower()
|
|
|
|
for entry in db.get("known_gpus", []):
|
|
match = entry.get("match", {})
|
|
|
|
# Try device_id match (exact, most reliable)
|
|
dev_ids = [d.lower() for d in match.get("device_ids", [])]
|
|
id_matched = device_id.lower() in dev_ids if device_id else False
|
|
|
|
# Try name_patterns match (case-insensitive substring against gpu_name + cpu_name)
|
|
patterns = match.get("name_patterns", [])
|
|
matched_patterns = [p for p in patterns if p.lower() in combined_name] if combined_name and patterns else []
|
|
name_matched = len(matched_patterns) > 0
|
|
match_len = max((len(p) for p in matched_patterns), default=0)
|
|
|
|
if id_matched and name_matched:
|
|
# Both match — prefer longest pattern to avoid "XT" matching "XTX"
|
|
if match_len > best_name_len:
|
|
selected = entry
|
|
best_name_len = match_len
|
|
elif id_matched and best_name_len == 0:
|
|
# Device ID matched but name didn't — use VRAM proximity as tiebreaker
|
|
entry_vram = entry.get("specs", {}).get("memory_mb", 0)
|
|
if vram_mb > 0:
|
|
diff = abs(entry_vram - vram_mb)
|
|
else:
|
|
# No VRAM info: prefer smallest card (under-provision is safe,
|
|
# over-provision crashes the model loader)
|
|
diff = entry_vram if entry_vram > 0 else float("inf")
|
|
if best_id_vram_diff is None or diff < best_id_vram_diff:
|
|
selected = entry
|
|
best_id_vram_diff = diff
|
|
elif name_matched and not selected:
|
|
selected = entry
|
|
best_name_len = match_len
|
|
|
|
# --- Pass 2: Heuristic fallback (threshold-based, top-down) ---
|
|
if not selected:
|
|
for entry in db.get("heuristic_classes", []):
|
|
match = entry.get("match", {})
|
|
|
|
# Check vendor
|
|
m_vendor = match.get("vendor", "")
|
|
if m_vendor and m_vendor != gpu_vendor:
|
|
continue
|
|
|
|
# Check memory_type
|
|
m_memtype = match.get("memory_type", "")
|
|
if m_memtype and m_memtype != memory_type:
|
|
continue
|
|
|
|
# Check min_vram_mb
|
|
min_vram = match.get("min_vram_mb", -1)
|
|
if min_vram >= 0 and vram_mb < min_vram:
|
|
continue
|
|
|
|
# Check max_vram_mb. This lets the database model "too small for
|
|
# GPU inference" classes explicitly instead of catching them in broad
|
|
# vendor fallbacks.
|
|
max_vram = match.get("max_vram_mb", -1)
|
|
if max_vram >= 0 and vram_mb > max_vram:
|
|
continue
|
|
|
|
# Check min_ram_mb (for unified memory classes)
|
|
min_ram = match.get("min_ram_mb", -1)
|
|
if min_ram >= 0 and ram_mb < min_ram:
|
|
continue
|
|
|
|
selected = entry
|
|
break
|
|
|
|
# --- Bandwidth lookup ---
|
|
bandwidth = 0
|
|
if selected and "specs" in selected:
|
|
bandwidth = selected["specs"].get("bandwidth_gbps", 0)
|
|
|
|
if bandwidth == 0 and gpu_name:
|
|
# Search bandwidth table by substring match
|
|
vendor_bw = db.get("known_gpu_bandwidth", {}).get(gpu_vendor, {})
|
|
for bw_name, bw_val in vendor_bw.items():
|
|
if bw_name.lower() in gpu_name.lower() or bw_name.lower() in cpu_name.lower():
|
|
bandwidth = bw_val
|
|
break
|
|
|
|
if bandwidth == 0:
|
|
# Fall back to default bandwidth
|
|
backend_key_map = {"nvidia": "cuda", "amd": "rocm", "apple": "metal"}
|
|
bk = backend_key_map.get(gpu_vendor, "cpu_x86")
|
|
bandwidth = db.get("defaults", {}).get("bandwidth_gbps", {}).get(bk, 0)
|
|
|
|
# --- Build result ---
|
|
if selected:
|
|
# Known GPU entry
|
|
if "specs" in selected:
|
|
class_id = selected.get("id", "unknown")
|
|
label = selected["specs"].get("label", selected.get("id", "Unknown"))
|
|
rec = selected.get("recommended", {})
|
|
backend = rec.get("backend", "cpu")
|
|
tier = rec.get("tier", "T1")
|
|
memory_source = selected["specs"].get("memory_source", "vram")
|
|
else:
|
|
# Heuristic class entry
|
|
class_id = selected.get("id", "unknown")
|
|
label = selected.get("id", "Unknown").replace("_", " ").title()
|
|
rec = selected.get("recommended", {})
|
|
backend = rec.get("backend", "cpu")
|
|
tier = rec.get("tier", "T1")
|
|
m_memtype = selected.get("match", {}).get("memory_type", "")
|
|
memory_source = "ram" if backend == "cpu" or m_memtype == "unified" else "vram"
|
|
else:
|
|
class_id = "unknown"
|
|
label = "Unknown"
|
|
backend = "cpu"
|
|
tier = "T1"
|
|
memory_source = "vram"
|
|
|
|
overlays = OVERLAY_MAP.get(backend, ["docker-compose.base.yml"])
|
|
# Darwin hosts running the apple backend use the canonical macOS overlay
|
|
# (installers/macos/docker-compose.macos.yml). The OVERLAY_MAP entry for
|
|
# "apple" still lists docker-compose.apple.yml so Linux hosts selecting
|
|
# --gpu-backend apple (Lemonade) continue to get the top-level overlay.
|
|
if backend == "apple" and platform_id == "macos":
|
|
overlays = ["docker-compose.base.yml", "installers/macos/docker-compose.macos.yml"]
|
|
gpu_label = selected["specs"].get("label", "") if selected and "specs" in selected else ""
|
|
|
|
# --- Output ---
|
|
def out(key, value):
|
|
safe = str(value).replace("\\", "\\\\").replace('"', '\\"')
|
|
print(f'{key}="{safe}"')
|
|
|
|
if env_mode:
|
|
out("HW_CLASS_ID", class_id)
|
|
out("HW_CLASS_LABEL", label)
|
|
out("HW_REC_BACKEND", backend)
|
|
out("HW_REC_TIER", tier)
|
|
out("HW_REC_COMPOSE_OVERLAYS", ",".join(overlays))
|
|
out("HW_BANDWIDTH_GBPS", bandwidth)
|
|
out("HW_MEMORY_SOURCE", memory_source)
|
|
out("HW_GPU_LABEL", gpu_label)
|
|
else:
|
|
result = {
|
|
"id": class_id,
|
|
"label": label,
|
|
"recommended": {
|
|
"backend": backend,
|
|
"tier": tier,
|
|
"compose_overlays": overlays,
|
|
},
|
|
"bandwidth_gbps": bandwidth,
|
|
"memory_source": memory_source,
|
|
"gpu_label": gpu_label,
|
|
}
|
|
print(json.dumps(result, indent=2))
|
|
PY
|