sites: Inventar und Zielbild an echte Knoten (pve44–pve61) angepasst; ADR-0001/0002/0006 ergänzt
ci / test (push) Successful in 25s

Aufbauplan Task 0.4:
- inventory.yaml: pve44/46/48/52/61 und GODE-PC-05/06 (Ada, Windows, cpu_roles: false)
- proxmox.yaml: eine GPU-VM pro Knoten mit allen Karten (D2), local-lvm/tank (D1 B),
  192.168.100.0/24 ohne VLAN, Gast-IPs .220–.239 (ANNAHME bis Task 0.2)
- registry.yaml/prometheus.yml: ops-Labels auf gpu-pve44/46/48, Ada ohne pve_node/vmid
- tofu: dynamische hostpci, Speicher je Gast; Ansible: pve_gpu_hosts/pve_kiosk,
  Replikation auf mehrere Ziele
- Placement: cpu_roles=false schließt Arbeitsplätze/Kiosk von CPU-Rollen aus
- PLAN §1.3/§1.4/§9 und ADR-Nachträge (D1 entschieden, D2/D3 Bestätigung offen)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 01:39:49 +02:00
co-authored by Claude Opus 5.5
parent 8cf27b48ce
commit c81e372c49
20 changed files with 343 additions and 190 deletions
+1 -1
View File
@@ -182,7 +182,7 @@ def plan(nodes: list[NodeProfile], roles: list[RoleSpec], models: list[ModelSpec
missing = role.count - placed missing = role.count - placed
if missing and (not role.needs.gpu or role.cpu_fallback): if missing and (not role.needs.gpu or role.cpu_fallback):
mode: Literal["cpu", "cpu_fallback"] = "cpu" if not role.needs.gpu else "cpu_fallback" mode: Literal["cpu", "cpu_fallback"] = "cpu" if not role.needs.gpu else "cpu_fallback"
candidates = [n for n in nodes if n.memory_gb >= role.needs.min_ram_gb] candidates = [n for n in nodes if n.cpu_roles and n.memory_gb >= role.needs.min_ram_gb]
# bevorzugt Nodes ohne GPU-Rollen, dann wenig belastete, dann viel RAM # bevorzugt Nodes ohne GPU-Rollen, dann wenig belastete, dann viel RAM
gpu_roles = {a.node for a in result.assignments if a.mode == "gpu"} gpu_roles = {a.node for a in result.assignments if a.mode == "gpu"}
candidates.sort(key=lambda n: (n.name in gpu_roles, cpu_load[n.name], -n.memory_gb, n.name)) candidates.sort(key=lambda n: (n.name in gpu_roles, cpu_load[n.name], -n.memory_gb, n.name))
+1
View File
@@ -43,6 +43,7 @@ class NodeProfile(BaseModel):
disks: list[Disk] = Field(default_factory=list) disks: list[Disk] = Field(default_factory=list)
gpus: list[GPU] = Field(default_factory=list) gpus: list[GPU] = Field(default_factory=list)
labels: dict[str, str] = Field(default_factory=dict) labels: dict[str, str] = Field(default_factory=dict)
cpu_roles: bool = True # False: Arbeitsplatz/Kiosk – keine Infra- oder CPU-Fallback-Rollen, nur eigene GPUs
NVIDIA_SMI_QUERY = ["nvidia-smi", "--query-gpu=index,name,memory.total,compute_cap,driver_version", NVIDIA_SMI_QUERY = ["nvidia-smi", "--query-gpu=index,name,memory.total,compute_cap,driver_version",
+23 -11
View File
@@ -24,7 +24,7 @@ def test_healer_labels_available_on_alerts():
"""Labels, die Healer-Regeln brauchen, müssen von Metrik oder Scrape-Target kommen.""" """Labels, die Healer-Regeln brauchen, müssen von Metrik oder Scrape-Target kommen."""
prom = yaml.safe_load((DEPLOY / "prometheus/prometheus.yml").read_text()) prom = yaml.safe_load((DEPLOY / "prometheus/prometheus.yml").read_text())
jobs = {j["job_name"]: j for j in prom["scrape_configs"]} jobs = {j["job_name"]: j for j in prom["scrape_configs"]}
for job in ("node", "gpu", "vllm"): for job in ("node", "gpu", "llamacpp"):
for sc in jobs[job]["static_configs"]: for sc in jobs[job]["static_configs"]:
assert {"slot", "pve_node", "vmid"} <= set(sc["labels"]), (job, sc) assert {"slot", "pve_node", "vmid"} <= set(sc["labels"]), (job, sc)
from knaster.canary import OPS_LABELS from knaster.canary import OPS_LABELS
@@ -46,13 +46,17 @@ def test_compose_files_parse(role):
def test_ops_labels_consistent_between_registry_and_prometheus(): def test_ops_labels_consistent_between_registry_and_prometheus():
"""Das erste Scrape-Ziel eines Slots trägt dieselben ops-Labels wie die Registry (Healer-Ziel)."""
reg = yaml.safe_load((REPO_ROOT / "sites/knaster/registry.yaml").read_text()) reg = yaml.safe_load((REPO_ROOT / "sites/knaster/registry.yaml").read_text())
prom = yaml.safe_load((DEPLOY / "prometheus/prometheus.yml").read_text()) prom = yaml.safe_load((DEPLOY / "prometheus/prometheus.yml").read_text())
vllm = {sc["labels"]["slot"]: sc["labels"] for sc in first: dict[str, dict] = {}
next(j for j in prom["scrape_configs"] if j["job_name"] == "vllm")["static_configs"]} for sc in next(j for j in prom["scrape_configs"] if j["job_name"] == "llamacpp")["static_configs"]:
for slot, labels in vllm.items(): first.setdefault(sc["labels"]["slot"], sc["labels"])
assert first
for slot, labels in first.items():
ops = reg["slots"][slot]["ops"] ops = reg["slots"][slot]["ops"]
assert ops["vmid"] == labels["vmid"] and ops["container"] == labels["container"] assert {k: ops[k] for k in ("docker_host", "container", "pve_node", "vmid")} == \
{k: labels[k] for k in ("docker_host", "container", "pve_node", "vmid")}, slot
WORKFLOWS = sorted((REPO_ROOT / "n8n/workflows").glob("*.json")) WORKFLOWS = sorted((REPO_ROOT / "n8n/workflows").glob("*.json"))
@@ -160,19 +164,27 @@ def test_proxmox_site_consistent_with_registry_and_prometheus():
reg = yaml.safe_load((REPO_ROOT / "sites/knaster/registry.yaml").read_text()) reg = yaml.safe_load((REPO_ROOT / "sites/knaster/registry.yaml").read_text())
vms = site["vms"] vms = site["vms"]
for vm in vms.values(): for vm in vms.values():
assert vm["gpu"] in site["pci_mappings"] for gpu in vm["gpus"]:
assert site["pci_mappings"][vm["gpu"]]["node"] == vm["node"] # Mapping liegt auf dem Host der VM assert gpu in site["pci_mappings"]
assert site["pci_mappings"][gpu]["node"] == vm["node"] # Mapping liegt auf dem Host der VM
assert vm["driver_generation"] in {"legacy", "modern"} assert vm["driver_generation"] in {"legacy", "modern"}
gpus = [vm["gpu"] for vm in vms.values()] assert vm["node"] in site["nodes"] and site["nodes"][vm["node"]]["gpu_host"]
gpus = [g for vm in vms.values() for g in vm["gpus"]]
assert len(gpus) == len(set(gpus)) # keine GPU doppelt durchgereicht assert len(gpus) == len(set(gpus)) # keine GPU doppelt durchgereicht
assert set(gpus) == set(site["pci_mappings"]) # jede gemappte Karte steckt in einer VM
by_vmid = {str(vm["vmid"]): (name, vm) for name, vm in {**vms, **site["containers"]}.items()} by_vmid = {str(vm["vmid"]): (name, vm) for name, vm in {**vms, **site["containers"]}.items()}
for slot, spec in reg["slots"].items(): for slot, spec in reg["slots"].items():
ops = spec.get("ops") ops = spec.get("ops") or {}
if not ops: if "vmid" not in ops: # z. B. Ada-Worker unter Windows (D3), kein Proxmox-Gast
assert "pve_node" not in ops, slot
continue continue
name, vm = by_vmid[ops["vmid"]] name, vm = by_vmid[ops["vmid"]]
assert vm["node"] == ops["pve_node"], slot assert vm["node"] == ops["pve_node"], slot
assert ops["docker_host"].startswith(name.split("-")[0]), slot assert ops["docker_host"] == name, slot
for c in site["containers"].values():
if c.get("ha"):
assert site["nodes"][c["node"]]["zfs_tank"] and c["storage"] == "tank"
assert all(site["nodes"][t]["zfs_tank"] for t in c["replicate_to"])
vmids = [str(v["vmid"]) for v in vms.values()] + [str(c["vmid"]) for c in site["containers"].values()] vmids = [str(v["vmid"]) for v in vms.values()] + [str(c["vmid"]) for c in site["containers"].values()]
assert len(vmids) == len(set(vmids)) assert len(vmids) == len(set(vmids))
# vLLM nur auf modernen Karten # vLLM nur auf modernen Karten
+43 -2
View File
@@ -28,29 +28,70 @@ def test_parsers():
assert disks[0].size_gb == 256.1 and not disks[0].rotational assert disks[0].size_gb == 256.1 and not disks[0].rotational
PVE_GPU_HOSTS = {"pve44", "pve46", "pve48", "pve52"}
def test_site_knaster_matches_real_nodes():
"""Aufbauplan Task 0.4: Site-Dateien beschreiben die echten Knoten, nicht mehr die Annahmen pve-ada/pve1–4."""
import ipaddress
from knaster.util import load_yaml
nodes = {n.name: n for n in load_nodes(REPO_ROOT / "sites/knaster/inventory.yaml")}
assert set(nodes) == {"pve44", "pve46", "pve48", "pve52", "pve61", "gode-pc-05", "gode-pc-06"}
pve = load_yaml(REPO_ROOT / "sites/knaster/proxmox.yaml")
old_net = ipaddress.ip_network("10.20.0.0/24")
ips = []
for section in ("vms", "containers"):
ips += [ipaddress.ip_interface(g["ip"]).ip for g in pve[section].values()]
ips += [ipaddress.ip_address(pve["cluster"][k]) for k in ("gateway", "dns")]
assert not [ip for ip in ips if ip in old_net]
assert {m["node"] for m in pve["pci_mappings"].values()} <= PVE_GPU_HOSTS
assert {v["node"] for v in pve["vms"].values()} <= PVE_GPU_HOSTS
# eine GPU-VM pro GPU-Knoten mit allen Karten des Knotens (D2)
assert sorted(v["node"] for v in pve["vms"].values()) == sorted(PVE_GPU_HOSTS)
for vm in pve["vms"].values():
mapped = {name for name, m in pve["pci_mappings"].items() if m["node"] == vm["node"]}
assert set(vm["gpus"]) == mapped
gpus_inv = [g for g in nodes[vm["node"]].gpus]
assert len(vm["gpus"]) == len(gpus_inv)
# Ada-Rechner bleiben Windows-Arbeitsplätze (D3): keine Infra-/CPU-Rollen
assert not nodes["gode-pc-05"].cpu_roles and not nodes["pve61"].cpu_roles
def test_placement_knaster(): def test_placement_knaster():
roles, models = load_catalog(REPO_ROOT / "catalog") roles, models = load_catalog(REPO_ROOT / "catalog")
p = plan(load_nodes(REPO_ROOT / "sites/knaster/inventory.yaml"), roles, models) p = plan(load_nodes(REPO_ROOT / "sites/knaster/inventory.yaml"), roles, models)
by_role = {} by_role = {}
for a in p.assignments: for a in p.assignments:
by_role.setdefault(a.role, []).append(a) by_role.setdefault(a.role, []).append(a)
assert {by_role["gpu-main"][0].node, by_role["gpu-flex"][0].node} == {"gode-pc-05", "gode-pc-06"}
assert by_role["gpu-main"][0].gpu_name.startswith("NVIDIA RTX 4000 Ada") assert by_role["gpu-main"][0].gpu_name.startswith("NVIDIA RTX 4000 Ada")
assert "qwen3-14b-awq" in by_role["gpu-main"][0].models_fitting assert "qwen3-14b-awq" in by_role["gpu-main"][0].models_fitting
embed_nodes = {a.node for a in by_role["gpu-embed"]} embed_nodes = {a.node for a in by_role["gpu-embed"]}
assert len(embed_nodes) == 2 # Anti-Affinität assert len(embed_nodes) == 2 # Anti-Affinität
assert all("P2000" in a.gpu_name for a in by_role["gpu-embed"] + by_role["gpu-small"]) assert all("P2000" in a.gpu_name for a in by_role["gpu-embed"] + by_role["gpu-small"])
assert by_role["infra"][0].mode == "cpu" assert by_role["infra"][0].mode == "cpu" and by_role["infra"][0].node in PVE_GPU_HOSTS
assert not p.unplaced assert not p.unplaced
# M4000 bewusst außer Betrieb (ADR-0006): weder frei noch belegt # M4000 bewusst außer Betrieb (ADR-0006): weder frei noch belegt
assert p.free_gpus == [] assert p.free_gpus == []
assert len(p.disabled_gpus) == 4 and all("M4000" in g for g in p.disabled_gpus) assert len(p.disabled_gpus) == 4 and all("M4000" in g for g in p.disabled_gpus)
tts = {a.role: a for a in p.assignments if a.role.startswith("tts")} tts = {a.role: a for a in p.assignments if a.role.startswith("tts")}
assert tts["tts-hq"].mode == "shared" and tts["tts-hq"].node == "pve-ada" assert tts["tts-hq"].mode == "shared" and tts["tts-hq"].node == by_role["gpu-flex"][0].node
assert "qwen3-tts-1.7b" in tts["tts-hq"].models_fitting assert "qwen3-tts-1.7b" in tts["tts-hq"].models_fitting
assert tts["tts-cpu"].node == by_role["infra"][0].node assert tts["tts-cpu"].node == by_role["infra"][0].node
assert tts["tts-cpu"].models_fitting == ["piper-de-thorsten-medium"] assert tts["tts-cpu"].models_fitting == ["piper-de-thorsten-medium"]
def test_nodes_without_cpu_roles_get_no_infra():
"""Arbeitsplätze und Kiosk nehmen keine CPU-Rollen, auch wenn sie den meisten RAM haben."""
roles, models = load_catalog(REPO_ROOT / "catalog")
nodes = [NodeProfile(name="workstation", memory_gb=64, cpu_roles=False),
NodeProfile(name="server", memory_gb=32)]
p = plan(nodes, roles, models)
assert {a.node for a in p.assignments if a.mode in ("cpu", "cpu_fallback")} == {"server"}
def test_placement_other_cluster(): def test_placement_other_cluster():
roles, models = load_catalog(REPO_ROOT / "catalog") roles, models = load_catalog(REPO_ROOT / "catalog")
p = plan(load_nodes(REPO_ROOT / "sites/example-single-gpu/inventory.yaml"), roles, models) p = plan(load_nodes(REPO_ROOT / "sites/example-single-gpu/inventory.yaml"), roles, models)
+31 -35
View File
@@ -1,5 +1,8 @@
# Scrape-Ziele. Die Labels an den GPU-Zielen (slot, docker_host, container, pve_node, vmid) landen in den # Scrape-Ziele. Die Labels an den GPU-Zielen (slot, docker_host, container, pve_node, vmid) landen in den
# Alerts und damit beim Healer. Hostnamen = DNS-Namen der VMs (ANNAHME bis Deployment). # Alerts und damit beim Healer. IP-Literale aus sites/knaster/proxmox.yaml (Global Constraints, Aufbauplan).
# ANNAHME bis Task 0.2 (Gast-IPs) und Task 3.3 (welcher Slot auf welcher P2000 läuft).
# Ada-Worker (GODE-PC-05/06, Windows, D3) werden nicht gescrapt: ihre Verfügbarkeit meldet die
# Worker-Registry v2 (Task 6.4), der Canary-Prober des Routers prüft die Slots zusätzlich.
global: global:
scrape_interval: 30s scrape_interval: 30s
evaluation_interval: 30s evaluation_interval: 30s
@@ -15,52 +18,45 @@ scrape_configs:
metrics_path: /metrics metrics_path: /metrics
static_configs: [{targets: ["router:8080"]}] static_configs: [{targets: ["router:8080"]}]
- job_name: vllm # Erstes Ziel je Slot = Healer-Ziel in sites/knaster/registry.yaml (ops)
static_configs:
- targets: ["gpu-main:8000"]
labels: {slot: tier1-general, docker_host: gpu-main, container: vllm, pve_node: pve-ada, vmid: "201"}
- targets: ["gpu-flex:8000"]
labels: {slot: tier1-flex, docker_host: gpu-flex, container: vllm, pve_node: pve-ada, vmid: "202"}
- job_name: llamacpp - job_name: llamacpp
static_configs: static_configs:
- targets: ["gpu-embed-1:8081"] - targets: ["192.168.100.224:8081"]
labels: {slot: embed-default, docker_host: gpu-embed-1, container: llamacpp-embed, pve_node: pve3, vmid: "221"} labels: {slot: embed-default, docker_host: gpu-pve44, container: llamacpp-embed, pve_node: pve44, vmid: "244"}
- targets: ["gpu-embed-1:8082"] - targets: ["192.168.100.226:8081"]
labels: {slot: rerank-default, docker_host: gpu-embed-1, container: llamacpp-rerank, pve_node: pve3, vmid: "221"} labels: {slot: embed-default, docker_host: gpu-pve46, container: llamacpp-embed, pve_node: pve46, vmid: "246"}
- targets: ["gpu-small:8080"] - targets: ["192.168.100.224:8082"]
labels: {slot: verifier-small, docker_host: gpu-small, container: llamacpp-chat, pve_node: pve3, vmid: "231"} labels: {slot: rerank-default, docker_host: gpu-pve44, container: llamacpp-rerank, pve_node: pve44, vmid: "244"}
- targets: ["192.168.100.228:8080"]
labels: {slot: verifier-small, docker_host: gpu-pve48, container: llamacpp-chat, pve_node: pve48, vmid: "248"}
- job_name: gpu - job_name: gpu
static_configs: static_configs:
- targets: ["gpu-main:9835"] - targets: ["192.168.100.224:9835"]
labels: {slot: tier1-general, pve_node: pve-ada, vmid: "201"} labels: {slot: embed-default, pve_node: pve44, vmid: "244"}
- targets: ["gpu-flex:9835"] - targets: ["192.168.100.226:9835"]
labels: {slot: tier1-flex, pve_node: pve-ada, vmid: "202"} labels: {slot: embed-default, pve_node: pve46, vmid: "246"}
- targets: ["gpu-embed-1:9835"] - targets: ["192.168.100.228:9835"]
labels: {slot: embed-default, pve_node: pve3, vmid: "221"} labels: {slot: verifier-small, pve_node: pve48, vmid: "248"}
- targets: ["gpu-embed-2:9835"] - targets: ["192.168.100.232:9835"]
labels: {slot: embed-default, pve_node: pve4, vmid: "222"} labels: {slot: "", pve_node: pve52, vmid: "252"} # nur M4000, außer Betrieb bis Task 3.3
- targets: ["gpu-small:9835"]
labels: {slot: verifier-small, pve_node: pve3, vmid: "231"}
- job_name: node # Labels werden für GpuXidError → Healer (VM-Neustart) gebraucht - job_name: node # Labels werden für GpuXidError → Healer (VM-Neustart) gebraucht
static_configs: static_configs:
- targets: ["gpu-main:9100"] - targets: ["192.168.100.224:9100"]
labels: {slot: tier1-general, pve_node: pve-ada, vmid: "201"} labels: {slot: embed-default, pve_node: pve44, vmid: "244"}
- targets: ["gpu-flex:9100"] - targets: ["192.168.100.226:9100"]
labels: {slot: tier1-flex, pve_node: pve-ada, vmid: "202"} labels: {slot: embed-default, pve_node: pve46, vmid: "246"}
- targets: ["gpu-embed-1:9100"] - targets: ["192.168.100.228:9100"]
labels: {slot: embed-default, pve_node: pve3, vmid: "221"} labels: {slot: verifier-small, pve_node: pve48, vmid: "248"}
- targets: ["gpu-embed-2:9100"] - targets: ["192.168.100.232:9100"]
labels: {slot: embed-default, pve_node: pve4, vmid: "222"} labels: {slot: "", pve_node: pve52, vmid: "252"}
- targets: ["gpu-small:9100"]
labels: {slot: verifier-small, pve_node: pve3, vmid: "231"}
- job_name: proxmox - job_name: proxmox
metrics_path: /pve metrics_path: /pve
params: {module: [default]} params: {module: [default]}
static_configs: [{targets: ["pve1", "pve2", "pve3", "pve4", "pve-ada"]}] static_configs:
- targets: ["192.168.100.210", "192.168.100.209", "192.168.100.212", "192.168.100.207", "192.168.100.214"]
relabel_configs: relabel_configs:
- {source_labels: [__address__], target_label: __param_target} - {source_labels: [__address__], target_label: __param_target}
- {source_labels: [__param_target], target_label: instance} - {source_labels: [__param_target], target_label: instance}
+33 -18
View File
@@ -33,6 +33,7 @@ Ziel: Ein starkes LLM (OpenRouter) erarbeitet neue Aufgaben einmalig und bricht
- **GPU-Nodes als VM.** Grund: Die alten Karten (Maxwell M4000, Pascal P2000) brauchen fest gepinnte Treiber mit CUDA 12.x, die Ada-Karten profitieren von aktuellen Treibern. Mit VMs lebt beides sauber getrennt auf einem Cluster. Treiber-Updates werden per Snapshot rückrollbar. - **GPU-Nodes als VM.** Grund: Die alten Karten (Maxwell M4000, Pascal P2000) brauchen fest gepinnte Treiber mit CUDA 12.x, die Ada-Karten profitieren von aktuellen Treibern. Mit VMs lebt beides sauber getrennt auf einem Cluster. Treiber-Updates werden per Snapshot rückrollbar.
- **CPU-Infrastruktur als LXC oder leichte VM** (n8n, Postgres, LiteLLM, Monitoring). Das spart RAM auf den i7-Nodes. - **CPU-Infrastruktur als LXC oder leichte VM** (n8n, Postgres, LiteLLM, Monitoring). Das spart RAM auf den i7-Nodes.
- Innerhalb der VMs bzw. LXCs läuft Docker Compose, ein Compose-Projekt pro Rolle. - Innerhalb der VMs bzw. LXCs läuft Docker Compose, ein Compose-Projekt pro Rolle.
- **Stand Aufbauplan (D2):** eine GPU-VM pro Knoten mit allen Karten des Knotens statt einer VM pro Rolle. Im Cluster stecken nur noch Maxwell und Pascal, die Ada-Rechner bleiben Windows-Arbeitsplätze (ADR-0001 und ADR-0006, jeweils Nachtrag).
### 1.2 Was Proxmox zusätzlich ermöglicht ### 1.2 Was Proxmox zusätzlich ermöglicht
@@ -47,30 +48,35 @@ Ziel: Ein starkes LLM (OpenRouter) erarbeitet neue Aufgaben einmalig und bricht
### 1.3 Rollenverteilung (aktuelle Hardware) ### 1.3 Rollenverteilung (aktuelle Hardware)
Stand: Aufbauplan vom 12.10.2026 (`docs/superpowers/plans/2026-10-12-aufbau-cluster-knaster.md`). Die Knoten stehen in `sites/knaster/inventory.yaml`. Welche P2000 welche Rolle bekommt, legt erst der Benchmark-Tag fest (Task 3.3).
| Rolle | Hardware | Engine | Aufgabe | | Rolle | Hardware | Engine | Aufgabe |
|---|---|---|---| |---|---|---|---|
| `gpu-main` | RTX 4000 Ada #1 (20 GB) | vLLM, Multi-LoRA | Slot `tier1-general` plus Skill-Adapter | | `gpu-main` | RTX 4000 Ada (20 GB) in GODE-PC-05, Windows | llama-server über Worker-Registry v2 (D3); vLLM/Multi-LoRA erst ab Phase 9 | Slot `tier1-general`, wenn der Worker online ist, sonst tier0 |
| `gpu-flex` | RTX 4000 Ada #2 (20 GB) | vLLM / Unsloth | tagsüber zweiter Slot oder Reserve, nachts LoRA-Training | | `gpu-flex` | RTX 4000 Ada (20 GB) in GODE-PC-06, Windows | wie `gpu-main` | `tier1-flex`; Training erst ab Phase 9 |
| `gpu-embed` | 2× Quadro P2000 (5 GB) | llama.cpp (Embedding/Rerank) | `embed-default`, `rerank-default`, redundant auf zwei Hosts | | `gpu-embed` | 2× Quadro P2000 (5 GB) in `gpu-pve44` und `gpu-pve46` | llama.cpp (Embedding/Rerank) | `embed-default`, `rerank-default`, redundant auf zwei Knoten |
| `gpu-small` | Quadro P2000 (5 GB) | llama.cpp | `verifier-small`, `signature-extractor` (3–4B) | | `gpu-small` | Quadro P2000 (5 GB) in `gpu-pve48` | llama.cpp | `verifier-small`, `signature-extractor` (3–4B) |
| – | 4× Quadro M4000 (8 GB) | – | **vorerst außer Betrieb** (ADR-0006), nur nach Benchmark für llama.cpp-Rollen | | – | 4× Quadro M4000 (8 GB), je eine in pve44/46/48/52 | – | **vorerst außer Betrieb** (ADR-0006), werden aber an die GPU-VM durchgereicht; nach dem Benchmark nur für llama.cpp-Rollen |
| `tts-hq` | teilt RTX 4000 Ada #2 | PyTorch | `tts-de-hq`: Qwen3-TTS 1.7B (ADR-0006) | | `tts-hq` | teilt die Ada in GODE-PC-06 | PyTorch | `tts-de-hq`: Qwen3-TTS 1.7B, erst ab Phase 9 (WSL2, D3) |
| `tts-cpu` | Infra-LXC (CPU) | Piper (ONNX) | `tts-de`: schnelle Sprachausgabe, Fallback | | `tts-cpu` | Infra-LXC (CPU) | Piper (ONNX) | `tts-de`: Sprachausgabe, vorerst einziger TTS-Slot |
| `infra` | i7 6./7. Gen, 32 GB | Docker | n8n, LiteLLM, Postgres+pgvector, Redis, Router, Healer, Langfuse, Prometheus/Grafana | | `infra` | LXC `infra` auf pve44 (`tank`, HA nach pve52/pve48) | Docker | Router, Postgres+pgvector, Healer, Prometheus/Grafana/Alertmanager; **kein eigener LiteLLM** (D4) |
| `strong` | extern | OpenRouter | `tier0-strong`, Skill-Autor (Hermes) | | `ops` | LXC auf pve46 | – | Ansible, OpenTofu, Repo-Clone |
| `strong` | extern | OpenRouter **über den LiteLLM mit PSE auf appsrv08** | `tier0-strong`, Skill-Autor (Hermes) |
pve61 (Surface Book, 8 GB) nimmt keine Gäste auf und zeigt nur das Grafana-Dashboard als Kiosk. n8n läuft weiter auf n8n.mindboost.app, LiteLLM auf appsrv08 (D4).
Wichtig: Auf Maxwell und Pascal läuft kein vLLM (das braucht Compute Capability ≥ 7.0), und CUDA 13 unterstützt sie nicht mehr. Daher auf diesen Nodes Treiber und Images pinnen. Auch PyTorch hat dort keine Zukunft mehr (2.14 ist die letzte Version mit CUDA-12-Wheels). Deshalb laufen auf alten Karten nur llama.cpp-Workloads (ADR-0006). Wichtig: Auf Maxwell und Pascal läuft kein vLLM (das braucht Compute Capability ≥ 7.0), und CUDA 13 unterstützt sie nicht mehr. Daher auf diesen Nodes Treiber und Images pinnen. Auch PyTorch hat dort keine Zukunft mehr (2.14 ist die letzte Version mit CUDA-12-Wheels). Deshalb laufen auf alten Karten nur llama.cpp-Workloads (ADR-0006).
### 1.4 Netzwerk und Storage (entschieden, siehe ADR-0002) ### 1.4 Netzwerk und Storage (entschieden, siehe ADR-0002 mit Nachtrag D1)
Ausgangslage: **1 GbE** zwischen den Nodes, pro physischer Maschine **2–3 SSDs mit 128–256 GB**. Ausgangslage: **1 GbE**, flaches Netz `192.168.100.0/24`. Die Knoten sind mit ext4 und LVM-thin auf einer System-SSD installiert, kein ZFS.
- **Kein Ceph.** Ceph braucht für brauchbare Latenz und Rebuild-Zeiten mindestens 10 GbE und mehr Platz pro OSD. Mit 1 GbE und kleinen SSDs würde jeder Schreibvorgang über das Netz gebremst und ein Rebuild das Netz tagelang belegen. - **Kein Ceph.** Ceph braucht für brauchbare Latenz und Rebuild-Zeiten mindestens 10 GbE und mehr Platz pro OSD. Mit 1 GbE und kleinen SSDs würde jeder Schreibvorgang über das Netz gebremst und ein Rebuild das Netz tagelang belegen.
- **ZFS lokal pro Node.** SSD 1 trägt das Proxmox-System. SSD 2 und 3 bilden, wo vorhanden, einen ZFS-Mirror `tank` für VM-Disks. Auf Nodes mit nur zwei SSDs ist `tank` eine Einzel-SSD, die über Replikation abgesichert wird. - **Variante B (D1, entschieden 12.10.):** Die System-SSD bleibt (`local`, `local-lvm` für die GPU-VMs). Auf pve44, pve48 und pve52 wird die zweite SSD nach Freigabe zur Einzel-SSD `tank`. pve46 und pve61 bleiben ohne ZFS. Der ARC ist auf 4 GiB begrenzt.
- **ZFS-Replikation** (Proxmox-Job, alle 15 Min.) für die Infra-VMs auf einen zweiten Node. Zusammen mit Proxmox HA startet die VM nach einem Node-Ausfall dort mit maximal 15 Min. altem Stand. Für Postgres wird das zusätzlich durch PBS-Backups und WAL-Archivierung abgefedert. - **ZFS-Replikation** (Proxmox-Job, alle 15 Min.) für HA-Gäste (`infra`) zwischen den `tank`-Knoten. Zusammen mit Proxmox HA startet der Gast nach einem Knotenausfall mit höchstens 15 Min. altem Stand. Für Postgres wird das zusätzlich durch PBS-Backups und WAL-Archivierung abgefedert.
- **Modelle liegen lokal** auf der GPU-VM, in einem eigenen Disk-Image (`models`, nicht repliziert, nicht gesichert). Quelle ist Hugging Face oder ein optionaler **Modell-Mirror** auf einem Infra-Node (HTTP/NFS, nur lesend). 10 GB über 1 GbE dauern etwa 90 s, das ist für Modellwechsel unkritisch. - **Modelle liegen lokal** auf der GPU-VM, in einem eigenen Disk-Image (`models`, 60 GB, nicht repliziert, nicht gesichert). Quelle ist Hugging Face oder ein optionaler **Modell-Mirror** auf einem Infra-Node (HTTP/NFS, nur lesend). 10 GB über 1 GbE dauern etwa 90 s, das ist für Modellwechsel unkritisch.
- **Proxmox Backup Server** als VM oder LXC auf dem Node mit dem meisten freien SSD-Platz, oder besser auf einer externen Platte bzw. einem NAS. Gesichert werden Postgres, Konfiguration und Datensätze, keine Modellgewichte. - **Proxmox Backup Server** als VM `pbs` auf pve52, Datastore auf einem externen Datenträger oder NAS (Task 5.1). Gesichert werden Postgres, Konfiguration und Datensätze, keine Modellgewichte.
- **Netztrennung per VLAN** (Proxmox SDN): `mgmt` (Proxmox, PBS), `svc` (Dienste untereinander), `ext` (Zugriff von außen auf n8n und Router). Ohne Bandbreitengewinn, aber Replikation und Backups lassen sich zeitlich steuern, damit sie den Inferenz-Traffic nicht stören. - **Netz (D5):** Die Knoten behalten .207–.214. Gäste bekommen feste IPs aus einem Block außerhalb des DHCP-Bereichs, vorgeschlagen ist `.220–.239` (Prüfung in Task 0.2). Konfigurationen nutzen IP-Literale, keine DNS-Namen. **VLANs** (`mgmt`, `svc`, `ext`) kommen erst später, wenn der Switch das zulässt.
**Speicherbudget pro Rolle (Richtwerte):** **Speicherbudget pro Rolle (Richtwerte):**
@@ -80,7 +86,7 @@ Ausgangslage: **1 GbE** zwischen den Nodes, pro physischer Maschine **2–3 SSDs
| `gpu-embed` / `gpu-small` | 15–25 GB | Embedding-, Rerank- und kleine GGUF-Modelle (je 0,5–3 GB), Images | | `gpu-embed` / `gpu-small` | 15–25 GB | Embedding-, Rerank- und kleine GGUF-Modelle (je 0,5–3 GB), Images |
| `infra` | 40–80 GB | Postgres (Fälle, Traces), Prometheus (15 Tage Retention), Langfuse | | `infra` | 40–80 GB | Postgres (Fälle, Traces), Prometheus (15 Tage Retention), Langfuse |
Die genaue SSD-Verteilung wird beim Inventarlauf erfasst (`sites/knaster/inventory.yaml`). Die SSD-Verteilung steht in `sites/knaster/inventory.yaml` (Bestandsaufnahme vom 12.10.).
--- ---
@@ -303,10 +309,19 @@ Die Portabilität beruht also auf den **Daten und der Pipeline**, nicht auf Gewi
- Infrastruktur heilt deterministisch, ohne LLM-Entscheidungen (ADR-0005) - Infrastruktur heilt deterministisch, ohne LLM-Entscheidungen (ADR-0005)
- Alte GPUs nur llama.cpp, M4000 vorerst aus, TTS mit Piper (CPU) und Qwen3-TTS (Ada) (ADR-0006) - Alte GPUs nur llama.cpp, M4000 vorerst aus, TTS mit Piper (CPU) und Qwen3-TTS (Ada) (ADR-0006)
### Entscheidungen 2026-10 (Aufbauplan)
| # | Thema | Stand |
|---|---|---|
| D1 | Speicher: Variante B, keine Neuinstallation, `tank` auf pve44/48/52 | **entschieden** 12.10. Gelöscht wird erst nach ausdrücklicher Freigabe der alten Windows-Platten |
| D2 | Eine Ubuntu-VM pro GPU-Knoten mit allen Karten (ADR-0001, Nachtrag) | empfohlen, Bestätigung offen (Task 0.3) |
| D3 | Ada-Rechner bleiben Windows, Anbindung über die Worker-Registry v2 (ADR-0006, Nachtrag) | empfohlen, Bestätigung offen |
| D4 | Kein eigener LiteLLM, der Router nutzt den LiteLLM mit PSE auf appsrv08 | empfohlen, Bestätigung offen |
| D5 | Knoten-IPs bleiben, Gäste aus `.220–.239`, kein VLAN | empfohlen, Bestätigung offen. Der IP-Block wird in Task 0.2 geprüft |
### Offen ### Offen
1. Erstes Basismodell für `tier1-general` (8–14B, gut quantisierbar, LoRA-freundlich, gutes Deutsch). Entscheidung per Kalibrierungslauf. 1. Erstes Basismodell für `tier1-general` (8–14B, gut quantisierbar, LoRA-freundlich, gutes Deutsch). Entscheidung per Kalibrierungslauf.
2. Kommen die M4000 zurück? `llama-bench` plus Strommessung am Deployment-Tag (bis dahin außer Betrieb, ADR-0006) 2. Kommen die M4000 zurück? `llama-bench` plus Strommessung am Deployment-Tag (bis dahin außer Betrieb, ADR-0006)
3. Verifier-Strategie pro Aufgabentyp: Code-Checks, Schema, kleines Judge-Modell, Stichproben durch das starke Modell 3. Verifier-Strategie pro Aufgabentyp: Code-Checks, Schema, kleines Judge-Modell, Stichproben durch das starke Modell
4. Schwellwerte: Graduierung (Start 90 %), Fallzahl fürs LoRA-Training (Start 300), Canary-Anteil (Start 10 %) 4. Schwellwerte: Graduierung (Start 90 %), Fallzahl fürs LoRA-Training (Start 300), Canary-Anteil (Start 10 %)
5. Kanal für den Pflege-Agenten: n8n Chat, Telegram oder Matrix 5. Kanal für den Pflege-Agenten: n8n Chat, Telegram oder Matrix
6. Standort des Proxmox Backup Servers (intern auf SSD oder extern) 6. Ziel-Speicher des Proxmox Backup Servers (Aufbauplan Task 5.1, Empfehlung: externer Datenträger oder NAS)
+13 -1
View File
@@ -1,6 +1,6 @@
# ADR-0001: GPU-Nodes als VM, Infrastruktur als LXC # ADR-0001: GPU-Nodes als VM, Infrastruktur als LXC
- Status: angenommen - Status: angenommen, ergänzt am 2026-10-13 (Nachtrag D2, Bestätigung offen)
- Datum: 2026-10-11 - Datum: 2026-10-11
## Kontext ## Kontext
@@ -15,3 +15,15 @@ Im Cluster stecken GPUs aus drei Generationen: Maxwell (M4000), Pascal (P2000) u
- Der VM-RAM ist bei Passthrough vollständig reserviert. Er wird deshalb knapp bemessen: llama.cpp-VMs 6–8 GB, vLLM-VMs 24–32 GB. - Der VM-RAM ist bei Passthrough vollständig reserviert. Er wird deshalb knapp bemessen: llama.cpp-VMs 6–8 GB, vLLM-VMs 24–32 GB.
- Keine Live-Migration für GPU-VMs. Bei einem Ausfall springt der Router auf einen anderen Slot oder auf Stufe 0. - Keine Live-Migration für GPU-VMs. Bei einem Ausfall springt der Router auf einen anderen Slot oder auf Stufe 0.
- Treiber-Updates laufen pro VM, abgesichert durch einen Snapshot. - Treiber-Updates laufen pro VM, abgesichert durch einen Snapshot.
## Nachtrag 2026-10-13: eine GPU-VM pro Knoten (Aufbauplan D2, Bestätigung offen)
Die echte Hardware (Aufbauplan, Ausgangslage) ändert die Voraussetzungen:
- Im Proxmox-Cluster stecken nur Maxwell und Pascal (pve44, pve46, pve48: M4000 + P2000, pve52: nur M4000). Die beiden RTX 4000 Ada sitzen in Windows-Arbeitsplätzen außerhalb des Clusters (D3, ADR-0006). Das Argument „gemischte Generationen in einem Host“ entfällt.
- Die Knoten haben 32 GB RAM. Eine VM pro Rolle würde den RAM mehrfach fest reservieren.
Deshalb gilt, sobald D2 bestätigt ist:
- **Pro GPU-Knoten eine Ubuntu-24.04-VM** (`gpu-pve44` … `gpu-pve52`, vmid 244–252) mit **allen** Karten des Knotens, 12 GB RAM fest, R580 (`driver_generation: legacy`). Darin laufen die Rollen als Docker-Compose-Dienste.
- Resource Mappings heißen `gpu-<typ>-<knoten>` (z. B. `gpu-p2000-pve44`).
- Ausgeschaltete Karten (M4000) werden trotzdem durchgereicht. So braucht ihr Wiedereinsatz nach dem Benchmark keine Änderung an der VM.
- Infra bleibt LXC (`infra`, vmid 101), dazu kommt ein kleiner `ops`-LXC (vmid 100) für Ansible und OpenTofu.
- Die oben genannten RAM-Richtwerte für vLLM-VMs entfallen. Das RAM-Budget je Knoten steht im Aufbauplan.
+11 -1
View File
@@ -1,6 +1,6 @@
# ADR-0002: ZFS lokal + Replikation statt Ceph # ADR-0002: ZFS lokal + Replikation statt Ceph
- Status: angenommen - Status: angenommen, ergänzt am 2026-10-13 (Nachtrag D1, entschieden)
- Datum: 2026-10-11 - Datum: 2026-10-11
## Kontext ## Kontext
@@ -16,3 +16,13 @@ Die Nodes sind mit **1 GbE** verbunden. Jede physische Maschine hat 2–3 SSDs m
- Bei einem Node-Ausfall gehen höchstens 15 Min. Daten der Infra-VMs verloren. Postgres sichert sich zusätzlich über WAL-Archivierung bzw. PBS. - Bei einem Node-Ausfall gehen höchstens 15 Min. Daten der Infra-VMs verloren. Postgres sichert sich zusätzlich über WAL-Archivierung bzw. PBS.
- Modellwechsel verursachen Downloads (10 GB ≈ 90 s über 1 GbE). Das ist akzeptabel. - Modellwechsel verursachen Downloads (10 GB ≈ 90 s über 1 GbE). Das ist akzeptabel.
- Die Speicherbudgets pro Rolle stehen in PLAN.md, Abschnitt 1.4. - Die Speicherbudgets pro Rolle stehen in PLAN.md, Abschnitt 1.4.
## Nachtrag 2026-10-13: Variante B ohne Neuinstallation (Aufbauplan D1, entschieden am 12.10.)
Die Knoten sind bereits mit ext4 und LVM-thin auf der System-SSD installiert. Entschieden ist **Variante B**:
- Die System-SSD bleibt, wie sie ist (`local` und `local-lvm`). Die GPU-VMs liegen auf `local-lvm`.
- Auf **pve44, pve48 und pve52** wird die zweite SSD (bisher altes Windows) nach ausdrücklicher Freigabe remote zur Einzel-SSD `tank` (`ashift=12`, `lz4`). Auf pve48 bleibt `sdc` unberührt.
- **pve46** (eine Platte) und **pve61** (Kiosk) bekommen kein ZFS. Dort laufen nur Gäste ohne HA (GPU-VM, `ops`).
- HA-Gäste (`infra`) liegen auf `tank` und werden alle 15 Min. auf die anderen `tank`-Knoten repliziert.
- Der ZFS-ARC wird auf 4 GiB begrenzt.
- Einen Spiegel für die Systemplatte gibt es nicht. Fällt eine System-SSD aus, wird der Knoten neu installiert, die HA-Gäste laufen repliziert weiter.
- Variante A (Neuinstallation mit ZFS-RAID1 vor Ort) lässt sich später pro Knoten nachholen.
@@ -1,6 +1,6 @@
# ADR-0006: Alte GPUs nur für llama.cpp, M4000 vorerst außer Betrieb, TTS auf Ada und CPU # ADR-0006: Alte GPUs nur für llama.cpp, M4000 vorerst außer Betrieb, TTS auf Ada und CPU
- Status: angenommen - Status: angenommen, ergänzt am 2026-10-13 (Nachtrag D3, Bestätigung offen)
- Datum: 2026-10-12 - Datum: 2026-10-12
## Kontext ## Kontext
@@ -20,3 +20,9 @@
## Folgen ## Folgen
- Bei `gpu-flex` muss sich der Lifecycle die Karte mit TTS teilen. Während des Trainings gehen sowohl `tier1-flex` als auch `tts-de-hq` auf `unavailable`, und LiteLLM fällt auf `tts-de` zurück. - Bei `gpu-flex` muss sich der Lifecycle die Karte mit TTS teilen. Während des Trainings gehen sowohl `tier1-flex` als auch `tts-de-hq` auf `unavailable`, und LiteLLM fällt auf `tts-de` zurück.
- Die tatsächlichen RTF-Werte misst `knaster bench tts` beim Deployment. Sie ersetzen die Schätzungen im Katalog. - Die tatsächlichen RTF-Werte misst `knaster bench tts` beim Deployment. Sie ersetzen die Schätzungen im Katalog.
## Nachtrag 2026-10-13: Ada-Rechner bleiben Windows (Aufbauplan D3, Bestätigung offen)
Die beiden RTX 4000 Ada stecken in **GODE-PC-05 und GODE-PC-06** (je eine Karte, 64 GB RAM). Das sind Windows-Arbeitsplätze, tagsüber haben Mitarbeitende Vorrang. Sie laufen schon heute als LLM-Worker (`LocalLLMWorker.ps1`, llama-server mit CUDA).
- Die Slots `tier1-general` und `tier1-flex` lösen künftig über die **Worker-Registry v2** auf einen Worker auf, der gerade online ist. Ist keiner online, geht die Anfrage an tier0 über den PSE-LiteLLM (Aufbauplan Task 6.4). Bis dahin behalten sie in der Registry ihre Engine.
- **vLLM, Multi-LoRA, QLoRA und Qwen3-TTS (`tts-de-hq`)** laufen unter Windows nicht nativ. Sie kommen erst mit einer späteren Entscheidung über WSL2 bzw. nächtliches vLLM (Phase 9). Bis dahin ist `tts-de` (Piper, CPU) der einzige aktive TTS-Slot.
- Im Inventar stehen die Rechner mit `cpu_roles: false`. Das Placement verplant dort nur die GPU und nie Infra- oder CPU-Fallback-Rollen.
+1 -1
View File
@@ -29,7 +29,7 @@ Deterministisch:
- `prefer: largest` wählt die GPU mit dem meisten VRAM (für LLM-Rollen). - `prefer: largest` wählt die GPU mit dem meisten VRAM (für LLM-Rollen).
- `prefer: smallest_fitting` wählt die kleinste passende GPU (für Embedding und Rerank). So bleiben große Karten frei. - `prefer: smallest_fitting` wählt die kleinste passende GPU (für Embedding und Rerank). So bleiben große Karten frei.
3. Rollen mit `share_with: <rolle>` teilen sich die GPU (bzw. bei `gpu: false` den Node) einer bereits platzierten Rolle. Ein Beispiel ist TTS auf `gpu-flex`. 3. Rollen mit `share_with: <rolle>` teilen sich die GPU (bzw. bei `gpu: false` den Node) einer bereits platzierten Rolle. Ein Beispiel ist TTS auf `gpu-flex`.
4. Rollen mit `gpu: false` bzw. `cpu_fallback: true` werden ohne passende GPU den CPU-Nodes zugewiesen, sortiert nach RAM. 4. Rollen mit `gpu: false` bzw. `cpu_fallback: true` werden ohne passende GPU den CPU-Nodes zugewiesen, sortiert nach RAM. Nodes mit `cpu_roles: false` (Arbeitsplätze, Kiosk) sind davon ausgenommen. Dort wird nur die eigene GPU verplant.
5. Pro zugewiesener Rolle werden die Katalogmodelle gelistet, die passen (Engine, VRAM, CC). 5. Pro zugewiesener Rolle werden die Katalogmodelle gelistet, die passen (Engine, VRAM, CC).
6. Ergebnis: `assignments[{role, node, gpu_index?, models_fitting[]}]` und `unplaced[{role, reason}]`. 6. Ergebnis: `assignments[{role, node, gpu_index?, models_fitting[]}]` und `unplaced[{role, reason}]`.
@@ -138,14 +138,15 @@ Ersetzt PLAN §8 „0 Fundament“ (Teil Inventar) und IMPLEMENTATION „Gemeins
**Interfaces:** **Interfaces:**
- Produces: Proxmox-Knoten `pve44`, `pve46`, `pve48`, `pve52`, `pve61`, dazu die Inventar-Knoten `gode-pc-05` und `gode-pc-06` (Ada, ohne Proxmox, nur im Inventar). PCI-Mappings `gpu-m4000-pve44`, `gpu-p2000-pve44`, `gpu-m4000-pve46`, `gpu-p2000-pve46`, `gpu-m4000-pve52`, `gpu-m4000-pve48`, `gpu-p2000-pve48`. GPU-VMs `gpu-pve44` (vmid 244), `gpu-pve46` (246), `gpu-pve48` (248), `gpu-pve52` (252). Infra-LXC `infra` (vmid 101). Ansible-Gruppen `pve_all`, `pve_gpu_hosts`, `pve_kiosk`. - Produces: Proxmox-Knoten `pve44`, `pve46`, `pve48`, `pve52`, `pve61`, dazu die Inventar-Knoten `gode-pc-05` und `gode-pc-06` (Ada, ohne Proxmox, nur im Inventar). PCI-Mappings `gpu-m4000-pve44`, `gpu-p2000-pve44`, `gpu-m4000-pve46`, `gpu-p2000-pve46`, `gpu-m4000-pve52`, `gpu-m4000-pve48`, `gpu-p2000-pve48`. GPU-VMs `gpu-pve44` (vmid 244), `gpu-pve46` (246), `gpu-pve48` (248), `gpu-pve52` (252). Infra-LXC `infra` (vmid 101). Ansible-Gruppen `pve_all`, `pve_gpu_hosts`, `pve_kiosk`.
- [ ] **Step 1: Failing test.** In `test_inventory_validate.py` den Test `test_site_knaster_matches_real_nodes` anlegen: - [x] **Step 1: Failing test.** In `test_inventory_validate.py` den Test `test_site_knaster_matches_real_nodes` anlegen:
- Die Inventar-Knoten sind genau `{pve44, pve46, pve48, pve52, pve61, gode-pc-05, gode-pc-06}`. - Die Inventar-Knoten sind genau `{pve44, pve46, pve48, pve52, pve61, gode-pc-05, gode-pc-06}`.
- `proxmox.yaml` enthält keine IP aus `10.20.0.0/24`. - `proxmox.yaml` enthält keine IP aus `10.20.0.0/24`.
- Jedes `pci_mappings[*].node` und jedes `vms[*].node` liegt in `{pve44, pve46, pve48, pve52}`. - Jedes `pci_mappings[*].node` und jedes `vms[*].node` liegt in `{pve44, pve46, pve48, pve52}`.
- [ ] **Step 2:** `make test` → FAIL (alte Annahmen `pve-ada`, `pve1`–`pve4`). - [x] **Step 2:** `make test` → FAIL (alte Annahmen `pve-ada`, `pve1`–`pve4`).
- [ ] **Step 3:** Site-Dateien umschreiben. Werte für IDs und Pfade aus `lspci -nn` und dem Healthcheck: M4000 `10de:13f1` auf `0000:01:00`, P2000 `10de:1c30` auf `0000:02:00`. Gateway `192.168.100.1`, DNS `192.168.100.120`, keine `vlan_svc`, Gast-IPs aus D5. Die M4000 bleiben `disabled: true` bis zum Benchmark-Tag (Task 3.3). Die Ada-Slots (`tier1-general`, `tier1-flex`, `tts-de-hq`) behalten vorerst ihre Engine. In `registry.yaml` entfallen bei ihnen nur `pve_node` und `vmid`. Die Umstellung auf die Worker-Registry folgt in Task 6.4. - [x] **Step 3:** Site-Dateien umschreiben. Werte für IDs und Pfade aus `lspci -nn` und dem Healthcheck: M4000 `10de:13f1` auf `0000:01:00`, P2000 `10de:1c30` auf `0000:02:00`. Gateway `192.168.100.1`, DNS `192.168.100.120`, keine `vlan_svc`, Gast-IPs aus D5. Die M4000 bleiben `disabled: true` bis zum Benchmark-Tag (Task 3.3). Die Ada-Slots (`tier1-general`, `tier1-flex`, `tts-de-hq`) behalten vorerst ihre Engine. In `registry.yaml` entfallen bei ihnen nur `pve_node` und `vmid`. Die Umstellung auf die Worker-Registry folgt in Task 6.4.
- [ ] **Step 4:** `make test && make validate` → PASS. `knaster placement plan --inventory sites/knaster/inventory.yaml` liefert eine Zuordnung ohne Fehler. - [x] **Step 4:** `make test && make validate` → PASS. `knaster placement plan --inventory sites/knaster/inventory.yaml` liefert eine Zuordnung ohne Fehler.
- [ ] **Step 5:** Commit `sites: Inventar und Zielbild an echte Knoten (pve44–pve61) angepasst; ADR-0001/0002/0006 ergänzt`. - [x] **Step 5:** Commit `sites: Inventar und Zielbild an echte Knoten (pve44–pve61) angepasst; ADR-0001/0002/0006 ergänzt`.
- Ergebnis (13.10.): Zusätzlich angepasst wurden `platform/proxmox/tofu` (mehrere Karten pro VM, Speicher je Gast, `vlan_svc: null`), `playbooks/site.yml` (`pve-host` nur für `pve_gpu_hosts`), die Replikation in `pve-host` (mehrere Ziele) und `deploy/prometheus/prometheus.yml` (IP-Literale, keine Ada-Ziele). Neues Inventarfeld `cpu_roles: false` für die Ada-Rechner und pve61. Damit verplant das Placement dort keine Infra- und keine CPU-Rollen. Hinweis: Das Placement schlägt `infra` auf pve52 vor, weil dort keine Karte aktiv ist. Laut RAM-Budget steht `infra` auf pve44, so ist es in `proxmox.yaml` eingetragen. Nicht mit `tofu validate` und `ansible-lint` geprüft, beides ist lokal nicht installiert. Das wird im ops-LXC nachgeholt (Task 3.1).
--- ---
@@ -234,7 +235,7 @@ Ersetzt GPU-Knoten 3 (Teil Cluster). Mit 5 Stimmen ist **kein QDevice** nötig.
**Wo:** remote · **Files:** Modify `platform/proxmox/ansible/inventory.yml`, `platform/proxmox/ansible/playbooks/site.yml`, Create `platform/proxmox/README.md` (Abschnitt ops-LXC) **Wo:** remote · **Files:** Modify `platform/proxmox/ansible/inventory.yml`, `platform/proxmox/ansible/playbooks/site.yml`, Create `platform/proxmox/README.md` (Abschnitt ops-LXC)
- [ ] LXC `ops` (Debian 13, vmid 100, auf pve44, IP aus D5) mit `ansible-core`, `opentofu`, `git` und dem Repo-Clone. Ansible läuft nicht auf dem Windows-Laptop. - [ ] LXC `ops` (Debian 13, vmid 100, auf pve46 laut RAM-Budget, IP aus D5) mit `ansible-core`, `opentofu`, `git` und dem Repo-Clone. Ansible läuft nicht auf dem Windows-Laptop.
- [ ] `site.yml`: `pve-host` nur für `pve_gpu_hosts` (pve44, pve46, pve48, pve52). pve61 steht in `pve_kiosk` ohne VFIO und Blacklist. - [ ] `site.yml`: `pve-host` nur für `pve_gpu_hosts` (pve44, pve46, pve48, pve52). pve61 steht in `pve_kiosk` ohne VFIO und Blacklist.
- [ ] Rolle `pve-host` ergänzen: `/etc/modprobe.d/vfio.conf` mit `options vfio-pci ids=<liste>` und `softdep nvidia pre: vfio-pci`. Die Liste enthält die Grafik- **und** die Audio-Funktionen der Karten. Die IDs aus `lspci -nn -s 01:00` und `lspci -nn -s 02:00` auf dem Knoten lesen, nicht aus dem Gedächtnis eintragen; bekannt sind bisher nur die VGA-IDs `10de:13f1` und `10de:1c30`. - [ ] Rolle `pve-host` ergänzen: `/etc/modprobe.d/vfio.conf` mit `options vfio-pci ids=<liste>` und `softdep nvidia pre: vfio-pci`. Die Liste enthält die Grafik- **und** die Audio-Funktionen der Karten. Die IDs aus `lspci -nn -s 01:00` und `lspci -nn -s 02:00` auf dem Knoten lesen, nicht aus dem Gedächtnis eintragen; bekannt sind bisher nur die VGA-IDs `10de:13f1` und `10de:1c30`.
- [ ] `ansible-playbook playbooks/site.yml --limit pve_gpu_hosts --check`, danach der echte Lauf. Knoten **einzeln** neu starten. - [ ] `ansible-playbook playbooks/site.yml --limit pve_gpu_hosts --check`, danach der echte Lauf. Knoten **einzeln** neu starten.
+2 -2
View File
@@ -7,8 +7,8 @@ Proxmox-spezifische Schicht (austauschbar, siehe PLAN §7.2). Eingabe ist immer
## Ablauf ## Ablauf
1. **Inventar:** auf jedem Node `knaster inventory collect` ausführen und `sites/knaster/inventory.yaml` ersetzen. Danach `knaster placement plan --inventory sites/knaster/inventory.yaml` und die Ergebnisse in `proxmox.yaml` übertragen (PCI-Pfade über `lspci -nn | grep -i nvidia`, IOMMU-Gruppen). 1. **Inventar:** auf jedem Node `knaster inventory collect` ausführen und `sites/knaster/inventory.yaml` ersetzen. Danach `knaster placement plan --inventory sites/knaster/inventory.yaml` und die Ergebnisse in `proxmox.yaml` übertragen (PCI-Pfade über `lspci -nn | grep -i nvidia`, IOMMU-Gruppen).
2. **Hosts vorbereiten:** `ansible-playbook playbooks/site.yml --limit pve_hosts`. Setzt IOMMU und VFIO und sperrt die NVIDIA-Treiber auf dem Host. Danach die Hosts neu starten. Der ZFS-Pool `tank` wird **nicht** automatisch angelegt (Datenverlustgefahr), die Rolle gibt dafür den Befehl aus. 2. **Hosts vorbereiten:** `ansible-playbook playbooks/site.yml --limit pve_gpu_hosts`. Setzt IOMMU und VFIO und sperrt die NVIDIA-Treiber, nur auf GPU-Knoten (pve61 bleibt unberührt, Kiosk). Danach die Hosts neu starten. Der ZFS-Pool `tank` wird **nicht** automatisch angelegt (Datenverlustgefahr), die Rolle gibt dafür den Befehl aus.
3. **Template:** pro Node ein Ubuntu-24.04-Cloud-Image als Template mit VMID 9000. 3. **Template:** pro GPU-Knoten ein Ubuntu-24.04-Cloud-Image als Template mit VMID 9000.
4. **VMs/LXC:** `cd tofu && tofu init && tofu plan -var proxmox_api_token=...` und danach `tofu apply`. 4. **VMs/LXC:** `cd tofu && tofu init && tofu plan -var proxmox_api_token=...` und danach `tofu apply`.
5. **Gäste:** `ansible-playbook playbooks/site.yml --limit gpu_vms,infra`. Installiert die Treiber pro Generation gepinnt (legacy/modern), das Container-Toolkit, Docker und den Xid-Exporter. 5. **Gäste:** `ansible-playbook playbooks/site.yml --limit gpu_vms,infra`. Installiert die Treiber pro Generation gepinnt (legacy/modern), das Container-Toolkit, Docker und den Xid-Exporter.
6. **Dienste:** `deploy/compose/<rolle>` auf den jeweiligen Gästen starten. 6. **Dienste:** `deploy/compose/<rolle>` auf den jeweiligen Gästen starten.
+25 -14
View File
@@ -1,29 +1,40 @@
# ANNAHME bis Deployment – Hosts und IPs aus sites/knaster/proxmox.yaml # Hosts und IPs aus sites/knaster/proxmox.yaml (Aufbauplan Task 0.4). Ansible läuft im ops-LXC (Task 3.1).
# Gast-IPs: ANNAHME bis Task 0.2 (DHCP-Ausschluss .220–.239).
all: all:
vars: vars:
knaster_site: knaster knaster_site: knaster
children: children:
pve_hosts: pve_all:
hosts: children:
pve-ada: {} pve_gpu_hosts:
pve1: {} hosts:
pve2: {} pve44: {ansible_host: 192.168.100.210}
pve3: {} pve46: {ansible_host: 192.168.100.209}
pve4: {} pve48: {ansible_host: 192.168.100.212}
pve52: {ansible_host: 192.168.100.207}
pve_kiosk: # keine VFIO-/NVIDIA-Sperre, iGPU bleibt beim Host (Kiosk)
hosts:
pve61: {ansible_host: 192.168.100.214}
vars: vars:
ansible_user: root ansible_user: root
gpu_vms: gpu_vms:
hosts: hosts:
gpu-main: {ansible_host: 10.20.0.201, driver_generation: modern, knaster_role: gpu-vllm} gpu-pve44: {ansible_host: 192.168.100.224}
gpu-flex: {ansible_host: 10.20.0.202, driver_generation: modern, knaster_role: gpu-vllm} gpu-pve46: {ansible_host: 192.168.100.226}
gpu-embed-1: {ansible_host: 10.20.0.221, driver_generation: legacy, knaster_role: gpu-llamacpp, llamacpp_profile: embed} gpu-pve48: {ansible_host: 192.168.100.228}
gpu-embed-2: {ansible_host: 10.20.0.222, driver_generation: legacy, knaster_role: gpu-llamacpp, llamacpp_profile: embed} gpu-pve52: {ansible_host: 192.168.100.232}
gpu-small: {ansible_host: 10.20.0.231, driver_generation: legacy, knaster_role: gpu-llamacpp, llamacpp_profile: chat}
vars: vars:
ansible_user: knaster ansible_user: knaster
ansible_become: true ansible_become: true
driver_generation: legacy # R580, Maxwell/Pascal
knaster_role: gpu-llamacpp
infra: infra:
hosts: hosts:
infra: {ansible_host: 10.20.0.101} infra: {ansible_host: 192.168.100.221}
vars:
ansible_user: root
ops:
hosts:
ops: {ansible_host: 192.168.100.220}
vars: vars:
ansible_user: root ansible_user: root
+4 -3
View File
@@ -1,6 +1,7 @@
# Reihenfolge: Hosts vorbereiten → (tofu apply) → GPU-VMs → Infra # Reihenfolge: GPU-Knoten vorbereiten → (tofu apply) → GPU-VMs → Infra
- name: Proxmox-Hosts (IOMMU, VFIO, ZFS-Replikation) # pve61 (pve_kiosk) bekommt bewusst keine Host-Rolle: keine VFIO-Bindung, keine NVIDIA-Sperre (Review Focus 5).
hosts: pve_hosts - name: Proxmox-GPU-Knoten (IOMMU, VFIO, ZFS-Replikation)
hosts: pve_gpu_hosts
roles: [pve-host] roles: [pve-host]
- name: GPU-VMs (Treiber gepinnt, Container-Toolkit, Docker, Xid-Exporter) - name: GPU-VMs (Treiber gepinnt, Container-Toolkit, Docker, Xid-Exporter)
@@ -50,21 +50,28 @@
- name: Hinweis, falls tank fehlt (Pool wird bewusst NICHT automatisch angelegt – Datenverlustgefahr) - name: Hinweis, falls tank fehlt (Pool wird bewusst NICHT automatisch angelegt – Datenverlustgefahr)
ansible.builtin.debug: ansible.builtin.debug:
msg: "Auf {{ inventory_hostname }} fehlt der ZFS-Pool 'tank'. Anlegen z. B.: zpool create -o ashift=12 tank mirror /dev/disk/by-id/<ssd2> /dev/disk/by-id/<ssd3> && pvesm add zfspool tank -pool tank" msg: >-
when: tank.rc != 0 Auf {{ inventory_hostname }} fehlt der ZFS-Pool 'tank'. Nach Freigabe (D1) und Prüfung mit
`lsblk -o NAME,SIZE,FSTYPE,LABEL /dev/sdb`: zpool create -o ashift=12 -O compression=lz4 -O atime=off
tank /dev/disk/by-id/<ssd2> && pvesm add zfspool tank --pool tank --content images,rootdir
when:
- tank.rc != 0
- knaster_pve.nodes[inventory_hostname].zfs_tank | default(false)
- name: Bestehende Replikationsjobs lesen - name: Bestehende Replikationsjobs lesen
ansible.builtin.command: pvesr list ansible.builtin.command: pvesr list
register: pvesr_jobs register: pvesr_jobs
changed_when: false changed_when: false
- name: ZFS-Replikation für Infra-Gäste # Ein Job pro Ziel: <vmid>-0, <vmid>-1, … (Reihenfolge aus replicate_to)
- name: ZFS-Replikation für HA-Gäste
ansible.builtin.command: > ansible.builtin.command: >
pvesr create-local-job {{ item.value.vmid }}-0 {{ item.value.replicate_to }} pvesr create-local-job {{ item.0.value.vmid }}-{{ idx }} {{ item.1 }}
--schedule '{{ knaster_pve.replication_schedule }}' --comment 'knaster {{ item.key }}' --schedule '{{ knaster_pve.replication_schedule }}' --comment 'knaster {{ item.0.key }}'
loop: "{{ knaster_pve.containers | dict2items }}" loop: "{{ knaster_pve.containers | dict2items | subelements('value.replicate_to', skip_missing=true) }}"
vars:
idx: "{{ item.0.value.replicate_to.index(item.1) }}"
when: when:
- item.value.node == inventory_hostname - item.0.value.node == inventory_hostname
- item.value.replicate_to is defined - ((item.0.value.vmid | string) + '-' + (idx | string)) not in pvesr_jobs.stdout
- (item.value.vmid | string) + '-0' not in pvesr_jobs.stdout
changed_when: true changed_when: true
+23 -16
View File
@@ -41,12 +41,12 @@ resource "proxmox_virtual_environment_vm" "gpu" {
} }
efi_disk { efi_disk {
datastore_id = local.site.cluster.storage_vm datastore_id = try(each.value.storage, local.site.cluster.storage_default)
type = "4m" type = "4m"
} }
disk { disk {
datastore_id = local.site.cluster.storage_vm datastore_id = try(each.value.storage, local.site.cluster.storage_default)
interface = "scsi0" interface = "scsi0"
size = each.value.disk_gb size = each.value.disk_gb
discard = "on" discard = "on"
@@ -55,7 +55,7 @@ resource "proxmox_virtual_environment_vm" "gpu" {
# Modelle: eigenes Disk-Image, nicht repliziert, nicht gesichert (ADR-0002) # Modelle: eigenes Disk-Image, nicht repliziert, nicht gesichert (ADR-0002)
disk { disk {
datastore_id = local.site.cluster.storage_vm datastore_id = try(each.value.storage, local.site.cluster.storage_default)
interface = "scsi1" interface = "scsi1"
size = each.value.models_disk_gb size = each.value.models_disk_gb
discard = "on" discard = "on"
@@ -64,20 +64,24 @@ resource "proxmox_virtual_environment_vm" "gpu" {
replicate = false replicate = false
} }
hostpci { # Alle Karten des Knotens in eine VM (D2); Reihenfolge = Index in der VM
device = "hostpci0" dynamic "hostpci" {
mapping = proxmox_virtual_environment_hardware_mapping_pci.gpu[each.value.gpu].name for_each = each.value.gpus
pcie = true content {
rombar = true device = "hostpci${hostpci.key}"
mapping = proxmox_virtual_environment_hardware_mapping_pci.gpu[hostpci.value].name
pcie = true
rombar = true
}
} }
network_device { network_device {
bridge = local.site.cluster.bridge bridge = local.site.cluster.bridge
vlan_id = local.site.cluster.vlan_svc vlan_id = local.site.cluster.vlan_svc # null = flaches Netz
} }
initialization { initialization {
datastore_id = local.site.cluster.storage_vm datastore_id = try(each.value.storage, local.site.cluster.storage_default)
ip_config { ip_config {
ipv4 { ipv4 {
address = each.value.ip address = each.value.ip
@@ -94,16 +98,16 @@ resource "proxmox_virtual_environment_vm" "gpu" {
} }
} }
resource "proxmox_virtual_environment_container" "infra" { resource "proxmox_virtual_environment_container" "lxc" {
for_each = local.site.containers for_each = local.site.containers
node_name = each.value.node node_name = each.value.node
vm_id = each.value.vmid vm_id = each.value.vmid
tags = ["knaster", "infra"] tags = ["knaster", each.key]
unprivileged = true unprivileged = true
start_on_boot = true start_on_boot = true
features { features {
nesting = true # Docker im LXC nesting = try(each.value.nesting, true) # Docker im LXC
} }
cpu { cpu {
@@ -115,12 +119,12 @@ resource "proxmox_virtual_environment_container" "infra" {
} }
disk { disk {
datastore_id = local.site.cluster.storage_vm datastore_id = try(each.value.storage, local.site.cluster.storage_default)
size = each.value.disk_gb size = each.value.disk_gb
} }
operating_system { operating_system {
template_file_id = "${local.site.cluster.storage_iso}:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst" template_file_id = local.site.cluster.lxc_template
type = "debian" type = "debian"
} }
@@ -132,6 +136,9 @@ resource "proxmox_virtual_environment_container" "infra" {
initialization { initialization {
hostname = each.key hostname = each.key
dns {
servers = [local.site.cluster.dns]
}
ip_config { ip_config {
ipv4 { ipv4 {
address = each.value.ip address = each.value.ip
@@ -149,5 +156,5 @@ resource "proxmox_virtual_environment_haresource" "infra" {
resource_id = "ct:${each.value.vmid}" resource_id = "ct:${each.value.vmid}"
state = "started" state = "started"
comment = "knaster ${each.key}" comment = "knaster ${each.key}"
depends_on = [proxmox_virtual_environment_container.infra] depends_on = [proxmox_virtual_environment_container.lxc]
} }
+2 -2
View File
@@ -2,6 +2,6 @@ output "gpu_vms" {
value = { for k, v in proxmox_virtual_environment_vm.gpu : k => { node = v.node_name, vmid = v.vm_id } } value = { for k, v in proxmox_virtual_environment_vm.gpu : k => { node = v.node_name, vmid = v.vm_id } }
} }
output "infra_containers" { output "containers" {
value = { for k, v in proxmox_virtual_environment_container.infra : k => { node = v.node_name, vmid = v.vm_id } } value = { for k, v in proxmox_virtual_environment_container.lxc : k => { node = v.node_name, vmid = v.vm_id } }
} }
+50 -33
View File
@@ -1,38 +1,55 @@
# Inventar des Clusters "knaster". # Inventar des Clusters "knaster" – Stand 12.10.2026 (Aufbauplan docs/superpowers/plans/2026-10-12-…, Ausgangslage).
# ANNAHME: Die Verteilung der GPUs und SSDs auf die Hosts ist noch nicht erfasst. Bitte beim # Quelle: Healthcheck 08.10. + Inventar pve48 12.10. + Angaben des Nutzers. Beim Bootstrap mit
# gemeinsamen Deployment mit `knaster inventory collect` auf jedem Node ersetzen. # `knaster inventory collect` gegenprüfen; Abweichungen hier eintragen.
# Speicher: Variante B (D1) – sda = System (local + local-lvm), sdb wird nach Freigabe zum ZFS-Pool `tank`.
nodes: nodes:
- name: pve-ada - name: pve44 # 192.168.100.210, ThinkStation P310
cpu: {model: "unbekannt", cores: 6, threads: 12} cpu: {model: "Intel Core i7-6700", cores: 4, threads: 8}
memory_gb: 32
disks: [{name: sda, size_gb: 256}, {name: sdb, size_gb: 256}] # sdb: altes Windows (NTFS) → tank
gpus:
- {index: 0, name: "Quadro M4000", memory_gb: 8, compute_cap: 5.2, disabled: true, note: "außer Betrieb bis Benchmark-Tag (Task 3.3, ADR-0006)"}
- {index: 1, name: "Quadro P2000", memory_gb: 5, compute_cap: 6.1}
- name: pve46 # 192.168.100.209, ThinkStation P310, Legacy-Boot, eine Platte (kein ZFS)
cpu: {model: "Intel Core i7-6700", cores: 4, threads: 8}
memory_gb: 32
disks: [{name: sda, size_gb: 256}]
gpus:
- {index: 0, name: "Quadro M4000", memory_gb: 8, compute_cap: 5.2, disabled: true, note: "außer Betrieb bis Benchmark-Tag (Task 3.3, ADR-0006)"}
- {index: 1, name: "Quadro P2000", memory_gb: 5, compute_cap: 6.1}
- name: pve48 # 192.168.100.212, ThinkStation P310, Legacy-Boot
cpu: {model: "Intel Core i7-6700", cores: 4, threads: 8}
memory_gb: 32
disks: [{name: sda, size_gb: 256}, {name: sdb, size_gb: 256}, {name: sdc, size_gb: 128}] # sdb → tank, sdc bleibt unberührt
gpus:
- {index: 0, name: "Quadro M4000", memory_gb: 8, compute_cap: 5.2, disabled: true, note: "außer Betrieb bis Benchmark-Tag (Task 3.3, ADR-0006)"}
- {index: 1, name: "Quadro P2000", memory_gb: 5, compute_cap: 6.1}
- name: pve52 # 192.168.100.207, ThinkStation P320, nur M4000 verbaut
cpu: {model: "Intel Core i7-7700", cores: 4, threads: 8}
memory_gb: 32
disks: [{name: sda, size_gb: 256}, {name: sdb, size_gb: 256}] # sdb: altes Windows (BitLocker) → tank
gpus:
- {index: 0, name: "Quadro M4000", memory_gb: 8, compute_cap: 5.2, disabled: true, note: "außer Betrieb bis Benchmark-Tag (Task 3.3, ADR-0006)"}
- name: pve61 # 192.168.100.214, Surface Book, LAN über USB-NIC im Dock – nur Kiosk
cpu: {model: "Intel Core i5-8350U", cores: 4, threads: 8}
memory_gb: 8
disks: [{name: nvme0n1, size_gb: 256}]
cpu_roles: false
labels: {platform: proxmox, purpose: kiosk}
# Außerhalb des Proxmox-Clusters (D3): Windows-Arbeitsplätze, tagsüber haben Mitarbeitende Vorrang.
# Leistung kommt über die Worker-Registry v2 (LocalLLMWorker.ps1, llama-server). Nur die eigene GPU wird verplant.
- name: gode-pc-05
cpu: {model: "unbekannt (Task 0.1 e2)", cores: 0, threads: 0}
memory_gb: 64 memory_gb: 64
disks: [{name: sda, size_gb: 256}, {name: sdb, size_gb: 256}] cpu_roles: false
labels: {platform: windows, worker: worker-registry-v2}
gpus: gpus:
- {index: 0, name: "NVIDIA RTX 4000 Ada Generation", memory_gb: 20, compute_cap: 8.9} - {index: 0, name: "NVIDIA RTX 4000 Ada Generation", memory_gb: 20, compute_cap: 8.9}
- {index: 1, name: "NVIDIA RTX 4000 Ada Generation", memory_gb: 20, compute_cap: 8.9} - name: gode-pc-06
- name: pve1 cpu: {model: "unbekannt (Task 0.1 e2)", cores: 0, threads: 0}
cpu: {model: "Intel Core i7 (6. Gen) 3.4 GHz", cores: 4, threads: 8} memory_gb: 64
memory_gb: 32 cpu_roles: false
disks: [{name: sda, size_gb: 128}, {name: sdb, size_gb: 256}] labels: {platform: windows, worker: worker-registry-v2}
gpus: gpus:
- {index: 0, name: "Quadro M4000", memory_gb: 8, compute_cap: 5.2, disabled: true, note: "außer Betrieb bis llama-bench + Strommessung (ADR-0006)"} - {index: 0, name: "NVIDIA RTX 4000 Ada Generation", memory_gb: 20, compute_cap: 8.9}
- {index: 1, name: "Quadro M4000", memory_gb: 8, compute_cap: 5.2, disabled: true, note: "außer Betrieb bis llama-bench + Strommessung (ADR-0006)"}
- name: pve2
cpu: {model: "Intel Core i7 (7. Gen) 3.6 GHz", cores: 4, threads: 8}
memory_gb: 32
disks: [{name: sda, size_gb: 128}, {name: sdb, size_gb: 256}, {name: sdc, size_gb: 256}]
gpus:
- {index: 0, name: "Quadro M4000", memory_gb: 8, compute_cap: 5.2, disabled: true, note: "außer Betrieb bis llama-bench + Strommessung (ADR-0006)"}
- {index: 1, name: "Quadro M4000", memory_gb: 8, compute_cap: 5.2, disabled: true, note: "außer Betrieb bis llama-bench + Strommessung (ADR-0006)"}
- name: pve3
cpu: {model: "Intel Core i7 (7. Gen) 3.6 GHz", cores: 4, threads: 8}
memory_gb: 32
disks: [{name: sda, size_gb: 128}, {name: sdb, size_gb: 256}]
gpus:
- {index: 0, name: "Quadro P2000", memory_gb: 5, compute_cap: 6.1}
- {index: 1, name: "Quadro P2000", memory_gb: 5, compute_cap: 6.1}
- name: pve4
cpu: {model: "Intel Core i7 (7. Gen) 3.6 GHz", cores: 4, threads: 8}
memory_gb: 32
disks: [{name: sda, size_gb: 128}, {name: sdb, size_gb: 256}, {name: sdc, size_gb: 256}]
gpus:
- {index: 0, name: "Quadro P2000", memory_gb: 5, compute_cap: 6.1}
+34 -21
View File
@@ -1,31 +1,44 @@
# Proxmox-Zielbild für "knaster" (Eingabe für platform/proxmox/tofu und ansible). # Proxmox-Zielbild für "knaster" (Eingabe für platform/proxmox/tofu und ansible).
# ANNAHMEN (Hosts, VM-IDs, IPs, PCI-Adressen) bis zum Inventarlauf – siehe docs/IMPLEMENTATION.md. # Grundlage: Aufbauplan docs/superpowers/plans/2026-10-12-aufbau-cluster-knaster.md (D1 B, D2, D5).
# ANNAHME bis Task 0.2: Gast-IPs aus dem vorgeschlagenen Block .220–.239 (DHCP-Ausschluss noch nicht geprüft).
# ANNAHME bis Task 3.1: PCI-Pfade/IOMMU-Gruppen wie auf pve48 geprüft; auf jedem Knoten mit `lspci -nn` bestätigen.
cluster: cluster:
endpoint: https://pve1:8006/ endpoint: https://192.168.100.210:8006/ # pve44, IP-Literal (Global Constraints)
storage_vm: tank # ZFS-Pool auf SSD 2+3 (ADR-0002) storage_default: local-lvm # D1 B: GPU-VMs auf der System-SSD; HA-Gäste auf `tank` (pve44/48/52)
storage_iso: local storage_iso: local
template_vmid: 9000 # Ubuntu 24.04 Cloud-Image-Template (pro Node vorhanden) template_vmid: 9000 # Ubuntu 24.04 Cloud-Image-Template (pro GPU-Knoten vorhanden)
lxc_template: "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst" # ANNAHME: Dateiname nach `pveam update` prüfen
bridge: vmbr0 bridge: vmbr0
vlan_svc: 20 vlan_svc: null # flaches Netz, vorerst kein VLAN (D5)
gateway: 10.20.0.1 gateway: 192.168.100.1
dns: 10.20.0.1 dns: 192.168.100.120
ssh_public_key_file: ~/.ssh/id_ed25519.pub ssh_public_key_file: ~/.ssh/id_ed25519.pub
pci_mappings: # Proxmox Resource Mappings (Datacenter → Resource Mappings → PCI) nodes: # Proxmox-Knoten (Cluster-Mitglieder), Ansible-Gruppen siehe platform/proxmox/ansible
gpu-ada-1: {node: pve-ada, path: "0000:01:00", id: "10de:27b2", subsystem_id: "", iommu_group: 0} pve44: {ip: 192.168.100.210, gpu_host: true, zfs_tank: true}
gpu-ada-2: {node: pve-ada, path: "0000:02:00", id: "10de:27b2", subsystem_id: "", iommu_group: 0} pve46: {ip: 192.168.100.209, gpu_host: true, zfs_tank: false} # eine Platte
gpu-p2000-1: {node: pve3, path: "0000:01:00", id: "10de:1c30", subsystem_id: "", iommu_group: 0} pve48: {ip: 192.168.100.212, gpu_host: true, zfs_tank: true}
gpu-p2000-2: {node: pve3, path: "0000:02:00", id: "10de:1c30", subsystem_id: "", iommu_group: 0} pve52: {ip: 192.168.100.207, gpu_host: true, zfs_tank: true}
gpu-p2000-3: {node: pve4, path: "0000:01:00", id: "10de:1c30", subsystem_id: "", iommu_group: 0} pve61: {ip: 192.168.100.214, gpu_host: false, zfs_tank: false} # Surface, Kiosk, keine Gäste
vms: # GPU-Rollen als VM (ADR-0001) pci_mappings: # Datacenter → Resource Mappings → PCI; Pfad ohne Funktion = Grafik + Audio
gpu-main: {node: pve-ada, vmid: 201, cores: 6, memory_mb: 28672, disk_gb: 40, models_disk_gb: 100, gpu: gpu-ada-1, ip: 10.20.0.201/24, role: gpu-vllm, driver_generation: modern} gpu-m4000-pve44: {node: pve44, path: "0000:01:00", id: "10de:13f1", subsystem_id: "", iommu_group: 1}
gpu-flex: {node: pve-ada, vmid: 202, cores: 6, memory_mb: 28672, disk_gb: 40, models_disk_gb: 100, gpu: gpu-ada-2, ip: 10.20.0.202/24, role: gpu-vllm, driver_generation: modern} gpu-p2000-pve44: {node: pve44, path: "0000:02:00", id: "10de:1c30", subsystem_id: "", iommu_group: 8}
gpu-embed-1: {node: pve3, vmid: 221, cores: 2, memory_mb: 6144, disk_gb: 30, models_disk_gb: 20, gpu: gpu-p2000-1, ip: 10.20.0.221/24, role: gpu-llamacpp, profile: embed, driver_generation: legacy} gpu-m4000-pve46: {node: pve46, path: "0000:01:00", id: "10de:13f1", subsystem_id: "", iommu_group: 1}
gpu-embed-2: {node: pve4, vmid: 222, cores: 2, memory_mb: 6144, disk_gb: 30, models_disk_gb: 20, gpu: gpu-p2000-3, ip: 10.20.0.222/24, role: gpu-llamacpp, profile: embed, driver_generation: legacy} gpu-p2000-pve46: {node: pve46, path: "0000:02:00", id: "10de:1c30", subsystem_id: "", iommu_group: 8}
gpu-small: {node: pve3, vmid: 231, cores: 2, memory_mb: 8192, disk_gb: 30, models_disk_gb: 20, gpu: gpu-p2000-2, ip: 10.20.0.231/24, role: gpu-llamacpp, profile: chat, driver_generation: legacy} gpu-m4000-pve48: {node: pve48, path: "0000:01:00", id: "10de:13f1", subsystem_id: "", iommu_group: 1}
gpu-p2000-pve48: {node: pve48, path: "0000:02:00", id: "10de:1c30", subsystem_id: "", iommu_group: 8}
gpu-m4000-pve52: {node: pve52, path: "0000:01:00", id: "10de:13f1", subsystem_id: "", iommu_group: 1}
containers: # Infrastruktur als LXC (ADR-0001), HA + ZFS-Replikation (ADR-0002) vms: # eine GPU-VM pro Knoten mit allen Karten (D2), 12 GB fest (RAM-Budget)
infra: {node: pve1, vmid: 101, cores: 4, memory_mb: 12288, disk_gb: 60, ip: 10.20.0.101/24, ha: true, replicate_to: pve2} gpu-pve44: {node: pve44, vmid: 244, cores: 4, memory_mb: 12288, disk_gb: 40, models_disk_gb: 60, gpus: [gpu-m4000-pve44, gpu-p2000-pve44], ip: 192.168.100.224/24, role: gpu-llamacpp, driver_generation: legacy}
gpu-pve46: {node: pve46, vmid: 246, cores: 4, memory_mb: 12288, disk_gb: 40, models_disk_gb: 60, gpus: [gpu-m4000-pve46, gpu-p2000-pve46], ip: 192.168.100.226/24, role: gpu-llamacpp, driver_generation: legacy}
gpu-pve48: {node: pve48, vmid: 248, cores: 4, memory_mb: 12288, disk_gb: 40, models_disk_gb: 60, gpus: [gpu-m4000-pve48, gpu-p2000-pve48], ip: 192.168.100.228/24, role: gpu-llamacpp, driver_generation: legacy}
gpu-pve52: {node: pve52, vmid: 252, cores: 4, memory_mb: 12288, disk_gb: 40, models_disk_gb: 60, gpus: [gpu-m4000-pve52], ip: 192.168.100.232/24, role: gpu-llamacpp, driver_generation: legacy}
containers: # LXC (Container zuerst); HA + Replikation nur auf `tank`-Knoten (Task 2.2)
ops: {node: pve46, vmid: 100, cores: 2, memory_mb: 1024, disk_gb: 16, ip: 192.168.100.220/24, storage: local-lvm, nesting: false} # Ansible/OpenTofu, kein HA (RAM-Budget)
infra: {node: pve44, vmid: 101, cores: 4, memory_mb: 8192, disk_gb: 60, ip: 192.168.100.221/24, storage: tank, nesting: true, ha: true, replicate_to: [pve52, pve48]}
# Später, eigene Tasks: pbs (VM 110, pve52, Task 5.1, .222), grp-test (LXC, pve48 auf tank, Phase 8, .223).
replication_schedule: "*/15" replication_schedule: "*/15"
+16 -13
View File
@@ -1,6 +1,9 @@
# Registry für den Cluster "knaster" (SPEC-core §1). # Registry für den Cluster "knaster" (SPEC-core §1).
# Clients sprechen nur Slot-Namen an; litellm_model ist der Alias in deploy/litellm/config.yaml. # Clients sprechen nur Slot-Namen an; litellm_model ist der Alias in deploy/litellm/config.yaml.
# ops: Labels für den Healer (Portainer-Endpoint, Container, Proxmox-Node, VM-ID). # ops: Labels für den Healer (Portainer-Endpoint, Container, Proxmox-Node, VM-ID).
# Stand Aufbauplan Task 0.4: P2000-Slots in den GPU-VMs gpu-pve44/46/48 (Zuordnung ANNAHME bis Benchmark-Tag,
# Task 3.3). Ada-Slots laufen auf Windows-Arbeitsplätzen (D3) ohne Proxmox; Umstellung auf die
# Worker-Registry v2 folgt in Task 6.4, bis dahin behalten sie ihre Engine.
slots: slots:
tier0-strong: tier0-strong:
tier: 0 tier: 0
@@ -15,37 +18,37 @@ slots:
tier: 1 tier: 1
device: gpu device: gpu
engine: vllm engine: vllm
node: gpu-main # RTX 4000 Ada #1 node: gpu-main # RTX 4000 Ada, GODE-PC-05 (Windows, D3)
litellm_model: tier1-general litellm_model: tier1-general
base_model: qwen3-14b-awq # Startkandidat, Entscheidung per Kalibrierungslauf base_model: qwen3-14b-awq # Startkandidat, Entscheidung per Kalibrierungslauf
max_context: 32768 max_context: 32768
effective_context: 16384 effective_context: 16384
supports_lora: true supports_lora: true
fallbacks: [tier1-flex] fallbacks: [tier1-flex]
ops: {docker_host: gpu-main, container: vllm, pve_node: pve-ada, vmid: "201"} # VM-IDs/Hosts: ANNAHME bis Deployment ops: {docker_host: gpu-main, container: vllm} # kein Proxmox (D3); bis Task 6.4
tier1-flex: tier1-flex:
tier: 1 tier: 1
device: gpu device: gpu
engine: vllm engine: vllm
node: gpu-flex # RTX 4000 Ada #2 (nachts Training → unavailable) node: gpu-flex # RTX 4000 Ada, GODE-PC-06 (Windows, D3)
litellm_model: tier1-flex litellm_model: tier1-flex
base_model: qwen3-14b-awq base_model: qwen3-14b-awq
max_context: 32768 max_context: 32768
effective_context: 16384 effective_context: 16384
supports_lora: true supports_lora: true
ops: {docker_host: gpu-flex, container: vllm, pve_node: pve-ada, vmid: "202"} # VM-IDs/Hosts: ANNAHME bis Deployment ops: {docker_host: gpu-flex, container: vllm} # kein Proxmox (D3); bis Task 6.4
verifier-small: verifier-small:
tier: 1 tier: 1
device: gpu device: gpu
engine: llamacpp engine: llamacpp
node: gpu-small # Quadro M4000 (Nutzen per Benchmark prüfen) node: gpu-small # Quadro P2000 in gpu-pve48
litellm_model: verifier-small litellm_model: verifier-small
base_model: qwen3-4b-q4km base_model: qwen3-4b-q4km
max_context: 8192 max_context: 8192
effective_context: 6144 effective_context: 6144
ops: {docker_host: gpu-small, container: llamacpp-chat, pve_node: pve3, vmid: "231"} # VM-IDs/Hosts: ANNAHME bis Deployment ops: {docker_host: gpu-pve48, container: llamacpp-chat, pve_node: pve48, vmid: "248"} # ANNAHME bis Task 3.3
signature-extractor: signature-extractor:
tier: 1 tier: 1
@@ -56,18 +59,18 @@ slots:
base_model: qwen3-4b-q4km base_model: qwen3-4b-q4km
max_context: 8192 max_context: 8192
effective_context: 6144 effective_context: 6144
ops: {docker_host: gpu-small, container: llamacpp-chat, pve_node: pve3, vmid: "231"} # VM-IDs/Hosts: ANNAHME bis Deployment ops: {docker_host: gpu-pve48, container: llamacpp-chat, pve_node: pve48, vmid: "248"} # ANNAHME bis Task 3.3
embed-default: embed-default:
tier: 1 tier: 1
device: gpu device: gpu
engine: llamacpp engine: llamacpp
node: gpu-embed # Quadro P2000 node: gpu-embed # Quadro P2000, redundant in gpu-pve44 und gpu-pve46
litellm_model: embed-default litellm_model: embed-default
base_model: qwen3-embedding-0.6b-q8 base_model: qwen3-embedding-0.6b-q8
max_context: 8192 max_context: 8192
kind: embedding kind: embedding
ops: {docker_host: gpu-embed-1, container: llamacpp-embed, pve_node: pve3, vmid: "221"} # VM-IDs/Hosts: ANNAHME bis Deployment ops: {docker_host: gpu-pve44, container: llamacpp-embed, pve_node: pve44, vmid: "244"} # ANNAHME bis Task 3.3
rerank-default: rerank-default:
tier: 1 tier: 1
@@ -78,7 +81,7 @@ slots:
base_model: bge-reranker-v2-m3-q8 base_model: bge-reranker-v2-m3-q8
max_context: 8192 max_context: 8192
kind: rerank kind: rerank
ops: {docker_host: gpu-embed-1, container: llamacpp-rerank, pve_node: pve3, vmid: "221"} # VM-IDs/Hosts: ANNAHME bis Deployment ops: {docker_host: gpu-pve44, container: llamacpp-rerank, pve_node: pve44, vmid: "244"} # ANNAHME bis Task 3.3
# --- Sprachausgabe (ADR-0006) --- # --- Sprachausgabe (ADR-0006) ---
tts-de: tts-de:
@@ -90,17 +93,17 @@ slots:
base_model: piper-de-thorsten-medium base_model: piper-de-thorsten-medium
max_context: 5000 # Zeichen pro Anfrage (TTS-Dienst-Limit) max_context: 5000 # Zeichen pro Anfrage (TTS-Dienst-Limit)
kind: tts kind: tts
ops: {docker_host: infra, container: tts-piper, pve_node: pve1, vmid: "101"} # ANNAHME bis Deployment ops: {docker_host: infra, container: tts-piper, pve_node: pve44, vmid: "101"} # HA: kann nach pve52/pve48 wandern
tts-de-hq: tts-de-hq:
tier: 1 tier: 1
device: gpu device: gpu
engine: pytorch engine: pytorch
node: gpu-flex # teilt sich die Ada mit tier1-flex; während Training unavailable node: gpu-flex # teilt sich die Ada mit tier1-flex; unter Windows erst ab Phase 9 (WSL2, D3)
litellm_model: tts-de-hq litellm_model: tts-de-hq
base_model: qwen3-tts-1.7b base_model: qwen3-tts-1.7b
max_context: 5000 max_context: 5000
kind: tts kind: tts
ops: {docker_host: gpu-flex, container: qwen-tts, pve_node: pve-ada, vmid: "202"} # ANNAHME bis Deployment ops: {docker_host: gpu-flex, container: qwen-tts} # kein Proxmox (D3)
adapters: [] adapters: []